diff --git a/src/bmm/agents/bmad-skill-manifest.yaml b/src/bmm/agents/bmad-skill-manifest.yaml new file mode 100644 index 000000000..2f3930de8 --- /dev/null +++ b/src/bmm/agents/bmad-skill-manifest.yaml @@ -0,0 +1,39 @@ +analyst.agent.yaml: + canonicalId: bmad-analyst + type: agent + description: "Business Analyst for market research, competitive analysis, and requirements elicitation" + +architect.agent.yaml: + canonicalId: bmad-architect + type: agent + description: "Architect for distributed systems, cloud infrastructure, and API design" + +dev.agent.yaml: + canonicalId: bmad-dev + type: agent + description: "Developer Agent for story execution, test-driven development, and code implementation" + +pm.agent.yaml: + canonicalId: bmad-pm + type: agent + description: "Product Manager for PRD creation, requirements discovery, and stakeholder alignment" + +qa.agent.yaml: + canonicalId: bmad-qa + type: agent + description: "QA Engineer for test automation, API testing, and E2E testing" + +quick-flow-solo-dev.agent.yaml: + canonicalId: bmad-quick-flow-solo-dev + type: agent + description: "Quick Flow Solo Dev for rapid spec creation and lean implementation" + +sm.agent.yaml: + canonicalId: bmad-sm + type: agent + description: "Scrum Master for sprint planning, story preparation, and agile ceremonies" + +ux-designer.agent.yaml: + canonicalId: bmad-ux-designer + type: agent + description: "UX Designer for user research, interaction design, and UI patterns" diff --git a/src/bmm/agents/tech-writer/bmad-skill-manifest.yaml b/src/bmm/agents/tech-writer/bmad-skill-manifest.yaml new file mode 100644 index 000000000..78aaa63eb --- /dev/null +++ b/src/bmm/agents/tech-writer/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-tech-writer +type: agent +description: "Technical Writer for documentation, Mermaid diagrams, and standards compliance" diff --git a/src/bmm/workflows/1-analysis/create-product-brief/bmad-skill-manifest.yaml b/src/bmm/workflows/1-analysis/create-product-brief/bmad-skill-manifest.yaml new file mode 100644 index 000000000..cb3969a6e --- /dev/null +++ b/src/bmm/workflows/1-analysis/create-product-brief/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-create-product-brief +type: workflow +description: "Create product brief through collaborative discovery" diff --git a/src/bmm/workflows/2-plan-workflows/create-ux-design/bmad-skill-manifest.yaml b/src/bmm/workflows/2-plan-workflows/create-ux-design/bmad-skill-manifest.yaml new file mode 100644 index 000000000..f0b8a250f --- /dev/null +++ b/src/bmm/workflows/2-plan-workflows/create-ux-design/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-create-ux-design +type: workflow +description: "Plan UX patterns and design specifications" diff --git a/src/bmm/workflows/3-solutioning/check-implementation-readiness/bmad-skill-manifest.yaml b/src/bmm/workflows/3-solutioning/check-implementation-readiness/bmad-skill-manifest.yaml new file mode 100644 index 000000000..3040413b8 --- /dev/null +++ b/src/bmm/workflows/3-solutioning/check-implementation-readiness/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-check-implementation-readiness +type: workflow +description: "Validate PRD, UX, Architecture and Epics specs are complete" diff --git a/src/bmm/workflows/3-solutioning/create-architecture/bmad-skill-manifest.yaml b/src/bmm/workflows/3-solutioning/create-architecture/bmad-skill-manifest.yaml new file mode 100644 index 000000000..6b35ce8e7 --- /dev/null +++ b/src/bmm/workflows/3-solutioning/create-architecture/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-create-architecture +type: workflow +description: "Create architecture solution design decisions for AI agent consistency" diff --git a/src/bmm/workflows/3-solutioning/create-epics-and-stories/bmad-skill-manifest.yaml b/src/bmm/workflows/3-solutioning/create-epics-and-stories/bmad-skill-manifest.yaml new file mode 100644 index 000000000..92b343dd9 --- /dev/null +++ b/src/bmm/workflows/3-solutioning/create-epics-and-stories/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-create-epics-and-stories +type: workflow +description: "Break requirements into epics and user stories" diff --git a/src/bmm/workflows/4-implementation/code-review/bmad-skill-manifest.yaml b/src/bmm/workflows/4-implementation/code-review/bmad-skill-manifest.yaml new file mode 100644 index 000000000..6b1589a4a --- /dev/null +++ b/src/bmm/workflows/4-implementation/code-review/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-code-review +type: workflow +description: "Perform adversarial code review finding specific issues" diff --git a/src/bmm/workflows/4-implementation/correct-course/bmad-skill-manifest.yaml b/src/bmm/workflows/4-implementation/correct-course/bmad-skill-manifest.yaml new file mode 100644 index 000000000..6a95bd4a7 --- /dev/null +++ b/src/bmm/workflows/4-implementation/correct-course/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-correct-course +type: workflow +description: "Manage significant changes during sprint execution" diff --git a/src/bmm/workflows/4-implementation/create-story/bmad-skill-manifest.yaml b/src/bmm/workflows/4-implementation/create-story/bmad-skill-manifest.yaml new file mode 100644 index 000000000..13f0beb24 --- /dev/null +++ b/src/bmm/workflows/4-implementation/create-story/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-create-story +type: workflow +description: "Creates a dedicated story file with all the context needed for implementation" diff --git a/src/bmm/workflows/4-implementation/dev-story/bmad-skill-manifest.yaml b/src/bmm/workflows/4-implementation/dev-story/bmad-skill-manifest.yaml new file mode 100644 index 000000000..2a79cef01 --- /dev/null +++ b/src/bmm/workflows/4-implementation/dev-story/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-dev-story +type: workflow +description: "Execute story implementation following a context-filled story spec file" diff --git a/src/bmm/workflows/4-implementation/retrospective/bmad-skill-manifest.yaml b/src/bmm/workflows/4-implementation/retrospective/bmad-skill-manifest.yaml new file mode 100644 index 000000000..51a5648ef --- /dev/null +++ b/src/bmm/workflows/4-implementation/retrospective/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-retrospective +type: workflow +description: "Post-epic review to extract lessons and assess success" diff --git a/src/bmm/workflows/4-implementation/sprint-planning/bmad-skill-manifest.yaml b/src/bmm/workflows/4-implementation/sprint-planning/bmad-skill-manifest.yaml new file mode 100644 index 000000000..2c02512ee --- /dev/null +++ b/src/bmm/workflows/4-implementation/sprint-planning/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-sprint-planning +type: workflow +description: "Generate sprint status tracking from epics" diff --git a/src/bmm/workflows/4-implementation/sprint-status/bmad-skill-manifest.yaml b/src/bmm/workflows/4-implementation/sprint-status/bmad-skill-manifest.yaml new file mode 100644 index 000000000..437b880e9 --- /dev/null +++ b/src/bmm/workflows/4-implementation/sprint-status/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-sprint-status +type: workflow +description: "Summarize sprint status and surface risks" diff --git a/src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/bmad-skill-manifest.yaml b/src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/bmad-skill-manifest.yaml new file mode 100644 index 000000000..913c63629 --- /dev/null +++ b/src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-quick-dev-new-preview +type: workflow +description: "Unified quick flow - clarify intent, plan, implement, review, present" diff --git a/src/bmm/workflows/bmad-quick-flow/quick-dev/bmad-skill-manifest.yaml b/src/bmm/workflows/bmad-quick-flow/quick-dev/bmad-skill-manifest.yaml new file mode 100644 index 000000000..e04a33271 --- /dev/null +++ b/src/bmm/workflows/bmad-quick-flow/quick-dev/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-quick-dev +type: workflow +description: "Implement a Quick Tech Spec for small changes or features" diff --git a/src/bmm/workflows/bmad-quick-flow/quick-spec/bmad-skill-manifest.yaml b/src/bmm/workflows/bmad-quick-flow/quick-spec/bmad-skill-manifest.yaml new file mode 100644 index 000000000..1a383135c --- /dev/null +++ b/src/bmm/workflows/bmad-quick-flow/quick-spec/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-quick-spec +type: workflow +description: "Very quick process to create implementation-ready quick specs for small changes or features" diff --git a/src/bmm/workflows/document-project/bmad-skill-manifest.yaml b/src/bmm/workflows/document-project/bmad-skill-manifest.yaml new file mode 100644 index 000000000..4e8cb2767 --- /dev/null +++ b/src/bmm/workflows/document-project/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-document-project +type: workflow +description: "Document brownfield projects for AI context" diff --git a/src/bmm/workflows/generate-project-context/bmad-skill-manifest.yaml b/src/bmm/workflows/generate-project-context/bmad-skill-manifest.yaml new file mode 100644 index 000000000..c319972c4 --- /dev/null +++ b/src/bmm/workflows/generate-project-context/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-generate-project-context +type: workflow +description: "Create project-context.md with AI rules" diff --git a/src/bmm/workflows/qa-generate-e2e-tests/bmad-skill-manifest.yaml b/src/bmm/workflows/qa-generate-e2e-tests/bmad-skill-manifest.yaml new file mode 100644 index 000000000..20e08be69 --- /dev/null +++ b/src/bmm/workflows/qa-generate-e2e-tests/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-qa-generate-e2e-tests +type: workflow +description: "Generate end-to-end automated tests for existing features" diff --git a/src/core/agents/bmad-skill-manifest.yaml b/src/core/agents/bmad-skill-manifest.yaml new file mode 100644 index 000000000..21cd90501 --- /dev/null +++ b/src/core/agents/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-master +type: agent +description: "BMad Master Executor, Knowledge Custodian, and Workflow Orchestrator" diff --git a/src/core/tasks/bmad-skill-manifest.yaml b/src/core/tasks/bmad-skill-manifest.yaml new file mode 100644 index 000000000..4f7e6b40e --- /dev/null +++ b/src/core/tasks/bmad-skill-manifest.yaml @@ -0,0 +1,39 @@ +editorial-review-prose.xml: + canonicalId: bmad-editorial-review-prose + type: task + description: "Clinical copy-editor that reviews text for communication issues" + +editorial-review-structure.xml: + canonicalId: bmad-editorial-review-structure + type: task + description: "Structural editor that proposes cuts, reorganization, and simplification while preserving comprehension" + +help.md: + canonicalId: bmad-help + type: task + description: "Analyzes what is done and the users query and offers advice on what to do next" + +index-docs.xml: + canonicalId: bmad-index-docs + type: task + description: "Generates or updates an index.md to reference all docs in the folder" + +review-adversarial-general.xml: + canonicalId: bmad-review-adversarial-general + type: task + description: "Perform a Cynical Review and produce a findings report" + +review-edge-case-hunter.xml: + canonicalId: bmad-review-edge-case-hunter + type: task + description: "Walk every branching path and boundary condition in content, report only unhandled edge cases" + +shard-doc.xml: + canonicalId: bmad-shard-doc + type: task + description: "Splits large markdown documents into smaller, organized files based on sections" + +workflow.xml: + canonicalId: bmad-workflow + type: task + description: "Execute given workflow by loading its configuration and following instructions" diff --git a/src/core/workflows/brainstorming/bmad-skill-manifest.yaml b/src/core/workflows/brainstorming/bmad-skill-manifest.yaml new file mode 100644 index 000000000..39a8f0ca9 --- /dev/null +++ b/src/core/workflows/brainstorming/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-brainstorming +type: workflow +description: "Facilitate interactive brainstorming sessions using diverse creative techniques and ideation methods" diff --git a/src/core/workflows/party-mode/bmad-skill-manifest.yaml b/src/core/workflows/party-mode/bmad-skill-manifest.yaml new file mode 100644 index 000000000..397e8fe3d --- /dev/null +++ b/src/core/workflows/party-mode/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-party-mode +type: workflow +description: "Orchestrates group discussions between all installed BMAD agents" diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 646bd9ef7..63f2567f5 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -12,9 +12,12 @@ */ const path = require('node:path'); +const os = require('node:os'); const fs = require('fs-extra'); const { YamlXmlBuilder } = require('../tools/cli/lib/yaml-xml-builder'); const { ManifestGenerator } = require('../tools/cli/installers/lib/core/manifest-generator'); +const { IdeManager } = require('../tools/cli/installers/lib/ide/manager'); +const { clearCache, loadPlatformCodes } = require('../tools/cli/installers/lib/ide/platform-codes'); // ANSI colors const colors = { @@ -45,6 +48,39 @@ function assert(condition, testName, errorMessage = '') { } } +async function createTestBmadFixture() { + const fixtureDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-fixture-')); + + // Minimal workflow manifest (generators check for this) + await fs.ensureDir(path.join(fixtureDir, '_config')); + await fs.writeFile(path.join(fixtureDir, '_config', 'workflow-manifest.csv'), ''); + + // Minimal compiled agent for core/agents (contains ', + 'Test persona', + '', + ].join('\n'); + + await fs.ensureDir(path.join(fixtureDir, 'core', 'agents')); + await fs.writeFile(path.join(fixtureDir, 'core', 'agents', 'bmad-master.md'), minimalAgent); + // Skill manifest so the installer uses 'bmad-master' as the canonical skill name + await fs.writeFile(path.join(fixtureDir, 'core', 'agents', 'bmad-skill-manifest.yaml'), 'bmad-master.md:\n canonicalId: bmad-master\n'); + + // Minimal compiled agent for bmm module (tests use selectedModules: ['bmm']) + await fs.ensureDir(path.join(fixtureDir, 'bmm', 'agents')); + await fs.writeFile(path.join(fixtureDir, 'bmm', 'agents', 'test-bmm-agent.md'), minimalAgent); + + return fixtureDir; +} + /** * Test Suite */ @@ -158,9 +194,311 @@ async function runTests() { console.log(''); // ============================================================ - // Test 5: QA Agent Compilation + // Test 4: Windsurf Native Skills Install // ============================================================ - console.log(`${colors.yellow}Test Suite 5: QA Agent Compilation${colors.reset}\n`); + console.log(`${colors.yellow}Test Suite 4: Windsurf Native Skills${colors.reset}\n`); + + try { + clearCache(); + const platformCodes = await loadPlatformCodes(); + const windsurfInstaller = platformCodes.platforms.windsurf?.installer; + + assert(windsurfInstaller?.target_dir === '.windsurf/skills', 'Windsurf target_dir uses native skills path'); + + assert(windsurfInstaller?.skill_format === true, 'Windsurf installer enables native skill output'); + + assert( + Array.isArray(windsurfInstaller?.legacy_targets) && windsurfInstaller.legacy_targets.includes('.windsurf/workflows'), + 'Windsurf installer cleans legacy workflow output', + ); + + const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-windsurf-test-')); + const installedBmadDir = await createTestBmadFixture(); + const legacyDir = path.join(tempProjectDir, '.windsurf', 'workflows', 'bmad-legacy-dir'); + await fs.ensureDir(legacyDir); + await fs.writeFile(path.join(tempProjectDir, '.windsurf', 'workflows', 'bmad-legacy.md'), 'legacy\n'); + await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n'); + + const ideManager = new IdeManager(); + await ideManager.ensureInitialized(); + const result = await ideManager.setup('windsurf', tempProjectDir, installedBmadDir, { + silent: true, + selectedModules: ['bmm'], + }); + + assert(result.success === true, 'Windsurf setup succeeds against temp project'); + + const skillFile = path.join(tempProjectDir, '.windsurf', 'skills', 'bmad-master', 'SKILL.md'); + assert(await fs.pathExists(skillFile), 'Windsurf install writes SKILL.md directory output'); + + assert(!(await fs.pathExists(path.join(tempProjectDir, '.windsurf', 'workflows'))), 'Windsurf setup removes legacy workflows dir'); + + await fs.remove(tempProjectDir); + await fs.remove(installedBmadDir); + } catch (error) { + assert(false, 'Windsurf native skills migration test succeeds', error.message); + } + + console.log(''); + + // ============================================================ + // Test 5: Kiro Native Skills Install + // ============================================================ + console.log(`${colors.yellow}Test Suite 5: Kiro Native Skills${colors.reset}\n`); + + try { + clearCache(); + const platformCodes = await loadPlatformCodes(); + const kiroInstaller = platformCodes.platforms.kiro?.installer; + + assert(kiroInstaller?.target_dir === '.kiro/skills', 'Kiro target_dir uses native skills path'); + + assert(kiroInstaller?.skill_format === true, 'Kiro installer enables native skill output'); + + assert( + Array.isArray(kiroInstaller?.legacy_targets) && kiroInstaller.legacy_targets.includes('.kiro/steering'), + 'Kiro installer cleans legacy steering output', + ); + + const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-kiro-test-')); + const installedBmadDir = await createTestBmadFixture(); + const legacyDir = path.join(tempProjectDir, '.kiro', 'steering', 'bmad-legacy-dir'); + await fs.ensureDir(legacyDir); + await fs.writeFile(path.join(tempProjectDir, '.kiro', 'steering', 'bmad-legacy.md'), 'legacy\n'); + await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n'); + + const ideManager = new IdeManager(); + await ideManager.ensureInitialized(); + const result = await ideManager.setup('kiro', tempProjectDir, installedBmadDir, { + silent: true, + selectedModules: ['bmm'], + }); + + assert(result.success === true, 'Kiro setup succeeds against temp project'); + + const skillFile = path.join(tempProjectDir, '.kiro', 'skills', 'bmad-master', 'SKILL.md'); + assert(await fs.pathExists(skillFile), 'Kiro install writes SKILL.md directory output'); + + assert(!(await fs.pathExists(path.join(tempProjectDir, '.kiro', 'steering'))), 'Kiro setup removes legacy steering dir'); + + await fs.remove(tempProjectDir); + await fs.remove(installedBmadDir); + } catch (error) { + assert(false, 'Kiro native skills migration test succeeds', error.message); + } + + console.log(''); + + // ============================================================ + // Test 6: Antigravity Native Skills Install + // ============================================================ + console.log(`${colors.yellow}Test Suite 6: Antigravity Native Skills${colors.reset}\n`); + + try { + clearCache(); + const platformCodes = await loadPlatformCodes(); + const antigravityInstaller = platformCodes.platforms.antigravity?.installer; + + assert(antigravityInstaller?.target_dir === '.agent/skills', 'Antigravity target_dir uses native skills path'); + + assert(antigravityInstaller?.skill_format === true, 'Antigravity installer enables native skill output'); + + assert( + Array.isArray(antigravityInstaller?.legacy_targets) && antigravityInstaller.legacy_targets.includes('.agent/workflows'), + 'Antigravity installer cleans legacy workflow output', + ); + + const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-antigravity-test-')); + const installedBmadDir = await createTestBmadFixture(); + const legacyDir = path.join(tempProjectDir, '.agent', 'workflows', 'bmad-legacy-dir'); + await fs.ensureDir(legacyDir); + await fs.writeFile(path.join(tempProjectDir, '.agent', 'workflows', 'bmad-legacy.md'), 'legacy\n'); + await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n'); + + const ideManager = new IdeManager(); + await ideManager.ensureInitialized(); + const result = await ideManager.setup('antigravity', tempProjectDir, installedBmadDir, { + silent: true, + selectedModules: ['bmm'], + }); + + assert(result.success === true, 'Antigravity setup succeeds against temp project'); + + const skillFile = path.join(tempProjectDir, '.agent', 'skills', 'bmad-master', 'SKILL.md'); + assert(await fs.pathExists(skillFile), 'Antigravity install writes SKILL.md directory output'); + + assert(!(await fs.pathExists(path.join(tempProjectDir, '.agent', 'workflows'))), 'Antigravity setup removes legacy workflows dir'); + + await fs.remove(tempProjectDir); + await fs.remove(installedBmadDir); + } catch (error) { + assert(false, 'Antigravity native skills migration test succeeds', error.message); + } + + console.log(''); + + // ============================================================ + // Test 7: Auggie Native Skills Install + // ============================================================ + console.log(`${colors.yellow}Test Suite 7: Auggie Native Skills${colors.reset}\n`); + + try { + clearCache(); + const platformCodes = await loadPlatformCodes(); + const auggieInstaller = platformCodes.platforms.auggie?.installer; + + assert(auggieInstaller?.target_dir === '.augment/skills', 'Auggie target_dir uses native skills path'); + + assert(auggieInstaller?.skill_format === true, 'Auggie installer enables native skill output'); + + assert( + Array.isArray(auggieInstaller?.legacy_targets) && auggieInstaller.legacy_targets.includes('.augment/commands'), + 'Auggie installer cleans legacy command output', + ); + + assert( + auggieInstaller?.ancestor_conflict_check !== true, + 'Auggie installer does not enable ancestor conflict checks without verified inheritance', + ); + + const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-auggie-test-')); + const installedBmadDir = await createTestBmadFixture(); + const legacyDir = path.join(tempProjectDir, '.augment', 'commands', 'bmad-legacy-dir'); + await fs.ensureDir(legacyDir); + await fs.writeFile(path.join(tempProjectDir, '.augment', 'commands', 'bmad-legacy.md'), 'legacy\n'); + await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n'); + + const ideManager = new IdeManager(); + await ideManager.ensureInitialized(); + const result = await ideManager.setup('auggie', tempProjectDir, installedBmadDir, { + silent: true, + selectedModules: ['bmm'], + }); + + assert(result.success === true, 'Auggie setup succeeds against temp project'); + + const skillFile = path.join(tempProjectDir, '.augment', 'skills', 'bmad-master', 'SKILL.md'); + assert(await fs.pathExists(skillFile), 'Auggie install writes SKILL.md directory output'); + + assert(!(await fs.pathExists(path.join(tempProjectDir, '.augment', 'commands'))), 'Auggie setup removes legacy commands dir'); + + await fs.remove(tempProjectDir); + await fs.remove(installedBmadDir); + } catch (error) { + assert(false, 'Auggie native skills migration test succeeds', error.message); + } + + console.log(''); + + // ============================================================ + // Test 8: OpenCode Native Skills Install + // ============================================================ + console.log(`${colors.yellow}Test Suite 8: OpenCode Native Skills${colors.reset}\n`); + + try { + clearCache(); + const platformCodes = await loadPlatformCodes(); + const opencodeInstaller = platformCodes.platforms.opencode?.installer; + + assert(opencodeInstaller?.target_dir === '.opencode/skills', 'OpenCode target_dir uses native skills path'); + + assert(opencodeInstaller?.skill_format === true, 'OpenCode installer enables native skill output'); + + assert(opencodeInstaller?.ancestor_conflict_check === true, 'OpenCode installer enables ancestor conflict checks'); + + assert( + Array.isArray(opencodeInstaller?.legacy_targets) && + ['.opencode/agents', '.opencode/commands', '.opencode/agent', '.opencode/command'].every((legacyTarget) => + opencodeInstaller.legacy_targets.includes(legacyTarget), + ), + 'OpenCode installer cleans split legacy agent and command output', + ); + + const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-opencode-test-')); + const installedBmadDir = await createTestBmadFixture(); + const legacyDirs = [ + path.join(tempProjectDir, '.opencode', 'agents', 'bmad-legacy-agent'), + path.join(tempProjectDir, '.opencode', 'commands', 'bmad-legacy-command'), + path.join(tempProjectDir, '.opencode', 'agent', 'bmad-legacy-agent-singular'), + path.join(tempProjectDir, '.opencode', 'command', 'bmad-legacy-command-singular'), + ]; + + for (const legacyDir of legacyDirs) { + await fs.ensureDir(legacyDir); + await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n'); + await fs.writeFile(path.join(path.dirname(legacyDir), `${path.basename(legacyDir)}.md`), 'legacy\n'); + } + + const ideManager = new IdeManager(); + await ideManager.ensureInitialized(); + const result = await ideManager.setup('opencode', tempProjectDir, installedBmadDir, { + silent: true, + selectedModules: ['bmm'], + }); + + assert(result.success === true, 'OpenCode setup succeeds against temp project'); + + const skillFile = path.join(tempProjectDir, '.opencode', 'skills', 'bmad-master', 'SKILL.md'); + assert(await fs.pathExists(skillFile), 'OpenCode install writes SKILL.md directory output'); + + for (const legacyDir of ['agents', 'commands', 'agent', 'command']) { + assert( + !(await fs.pathExists(path.join(tempProjectDir, '.opencode', legacyDir))), + `OpenCode setup removes legacy .opencode/${legacyDir} dir`, + ); + } + + await fs.remove(tempProjectDir); + await fs.remove(installedBmadDir); + } catch (error) { + assert(false, 'OpenCode native skills migration test succeeds', error.message); + } + + console.log(''); + + // ============================================================ + // Test 9: OpenCode Ancestor Conflict + // ============================================================ + console.log(`${colors.yellow}Test Suite 9: OpenCode Ancestor Conflict${colors.reset}\n`); + + try { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-opencode-ancestor-test-')); + const parentProjectDir = path.join(tempRoot, 'parent'); + const childProjectDir = path.join(parentProjectDir, 'child'); + const installedBmadDir = await createTestBmadFixture(); + + await fs.ensureDir(path.join(parentProjectDir, '.git')); + await fs.ensureDir(path.join(parentProjectDir, '.opencode', 'skills', 'bmad-existing')); + await fs.ensureDir(childProjectDir); + await fs.writeFile(path.join(parentProjectDir, '.opencode', 'skills', 'bmad-existing', 'SKILL.md'), 'legacy\n'); + + const ideManager = new IdeManager(); + await ideManager.ensureInitialized(); + const result = await ideManager.setup('opencode', childProjectDir, installedBmadDir, { + silent: true, + selectedModules: ['bmm'], + }); + const expectedConflictDir = await fs.realpath(path.join(parentProjectDir, '.opencode', 'skills')); + + assert(result.success === false, 'OpenCode setup refuses install when ancestor skills already exist'); + assert(result.handlerResult?.reason === 'ancestor-conflict', 'OpenCode ancestor rejection reports ancestor-conflict reason'); + assert( + result.handlerResult?.conflictDir === expectedConflictDir, + 'OpenCode ancestor rejection points at ancestor .opencode/skills dir', + ); + + await fs.remove(tempRoot); + await fs.remove(installedBmadDir); + } catch (error) { + assert(false, 'OpenCode ancestor conflict protection test succeeds', error.message); + } + + console.log(''); + + // ============================================================ + // Test 10: QA Agent Compilation + // ============================================================ + console.log(`${colors.yellow}Test Suite 10: QA Agent Compilation${colors.reset}\n`); try { const builder = new YamlXmlBuilder(); diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index 06e2e3f4b..0955a3d6f 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -5,6 +5,7 @@ const crypto = require('node:crypto'); const csv = require('csv-parse/sync'); const { getSourcePath, getModulePath } = require('../../../lib/project-root'); const prompts = require('../../../lib/prompts'); +const { loadSkillManifest: loadSkillManifestShared, getCanonicalId: getCanonicalIdShared } = require('../ide/shared/skill-manifest'); // Load package.json for version info const packageJson = require('../../../../../package.json'); @@ -23,6 +24,16 @@ class ManifestGenerator { this.selectedIdes = []; } + /** Delegate to shared skill-manifest module */ + async loadSkillManifest(dirPath) { + return loadSkillManifestShared(dirPath); + } + + /** Delegate to shared skill-manifest module */ + getCanonicalId(manifest, filename) { + return getCanonicalIdShared(manifest, filename); + } + /** * Clean text for CSV output by normalizing whitespace. * Note: Quote escaping is handled by escapeCsv() at write time. @@ -150,6 +161,8 @@ class ManifestGenerator { // Recursively find workflow.yaml files const findWorkflows = async (dir, relativePath = '') => { const entries = await fs.readdir(dir, { withFileTypes: true }); + // Load skill manifest for this directory (if present) + const skillManifest = await this.loadSkillManifest(dir); for (const entry of entries) { const fullPath = path.join(dir, entry.name); @@ -221,6 +234,7 @@ class ManifestGenerator { description: this.cleanForCSV(workflow.description), module: moduleName, path: installPath, + canonicalId: this.getCanonicalId(skillManifest, entry.name), }); // Add to files list @@ -294,6 +308,8 @@ class ManifestGenerator { async getAgentsFromDir(dirPath, moduleName, relativePath = '') { const agents = []; const entries = await fs.readdir(dirPath, { withFileTypes: true }); + // Load skill manifest for this directory (if present) + const skillManifest = await this.loadSkillManifest(dirPath); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); @@ -349,6 +365,7 @@ class ManifestGenerator { principles: principlesMatch ? this.cleanForCSV(principlesMatch[1]) : '', module: moduleName, path: installPath, + canonicalId: this.getCanonicalId(skillManifest, entry.name), }); // Add to files list @@ -388,6 +405,8 @@ class ManifestGenerator { async getTasksFromDir(dirPath, moduleName) { const tasks = []; const files = await fs.readdir(dirPath); + // Load skill manifest for this directory (if present) + const skillManifest = await this.loadSkillManifest(dirPath); for (const file of files) { // Check for both .xml and .md files @@ -447,6 +466,7 @@ class ManifestGenerator { module: moduleName, path: installPath, standalone: standalone, + canonicalId: this.getCanonicalId(skillManifest, file), }); // Add to files list @@ -486,6 +506,8 @@ class ManifestGenerator { async getToolsFromDir(dirPath, moduleName) { const tools = []; const files = await fs.readdir(dirPath); + // Load skill manifest for this directory (if present) + const skillManifest = await this.loadSkillManifest(dirPath); for (const file of files) { // Check for both .xml and .md files @@ -545,6 +567,7 @@ class ManifestGenerator { module: moduleName, path: installPath, standalone: standalone, + canonicalId: this.getCanonicalId(skillManifest, file), }); // Add to files list @@ -735,8 +758,8 @@ class ManifestGenerator { const csvPath = path.join(cfgDir, 'workflow-manifest.csv'); const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`; - // Create CSV header - standalone column removed, everything is canonicalized to 4 columns - let csv = 'name,description,module,path\n'; + // Create CSV header - standalone column removed, canonicalId added as optional column + let csv = 'name,description,module,path,canonicalId\n'; // Build workflows map from discovered workflows only // Old entries are NOT preserved - the manifest reflects what actually exists on disk @@ -750,12 +773,19 @@ class ManifestGenerator { description: workflow.description, module: workflow.module, path: workflow.path, + canonicalId: workflow.canonicalId || '', }); } // Write all workflows for (const [, value] of allWorkflows) { - const row = [escapeCsv(value.name), escapeCsv(value.description), escapeCsv(value.module), escapeCsv(value.path)].join(','); + const row = [ + escapeCsv(value.name), + escapeCsv(value.description), + escapeCsv(value.module), + escapeCsv(value.path), + escapeCsv(value.canonicalId), + ].join(','); csv += row + '\n'; } @@ -784,8 +814,8 @@ class ManifestGenerator { } } - // Create CSV header with persona fields - let csvContent = 'name,displayName,title,icon,capabilities,role,identity,communicationStyle,principles,module,path\n'; + // Create CSV header with persona fields and canonicalId + let csvContent = 'name,displayName,title,icon,capabilities,role,identity,communicationStyle,principles,module,path,canonicalId\n'; // Combine existing and new agents, preferring new data for duplicates const allAgents = new Map(); @@ -810,6 +840,7 @@ class ManifestGenerator { principles: agent.principles, module: agent.module, path: agent.path, + canonicalId: agent.canonicalId || '', }); } @@ -827,6 +858,7 @@ class ManifestGenerator { escapeCsv(record.principles), escapeCsv(record.module), escapeCsv(record.path), + escapeCsv(record.canonicalId), ].join(','); csvContent += row + '\n'; } @@ -856,8 +888,8 @@ class ManifestGenerator { } } - // Create CSV header with standalone column - let csvContent = 'name,displayName,description,module,path,standalone\n'; + // Create CSV header with standalone and canonicalId columns + let csvContent = 'name,displayName,description,module,path,standalone,canonicalId\n'; // Combine existing and new tasks const allTasks = new Map(); @@ -877,6 +909,7 @@ class ManifestGenerator { module: task.module, path: task.path, standalone: task.standalone, + canonicalId: task.canonicalId || '', }); } @@ -889,6 +922,7 @@ class ManifestGenerator { escapeCsv(record.module), escapeCsv(record.path), escapeCsv(record.standalone), + escapeCsv(record.canonicalId), ].join(','); csvContent += row + '\n'; } @@ -918,8 +952,8 @@ class ManifestGenerator { } } - // Create CSV header with standalone column - let csvContent = 'name,displayName,description,module,path,standalone\n'; + // Create CSV header with standalone and canonicalId columns + let csvContent = 'name,displayName,description,module,path,standalone,canonicalId\n'; // Combine existing and new tools const allTools = new Map(); @@ -939,6 +973,7 @@ class ManifestGenerator { module: tool.module, path: tool.path, standalone: tool.standalone, + canonicalId: tool.canonicalId || '', }); } @@ -951,6 +986,7 @@ class ManifestGenerator { escapeCsv(record.module), escapeCsv(record.path), escapeCsv(record.standalone), + escapeCsv(record.canonicalId), ].join(','); csvContent += row + '\n'; } diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index 813a6e674..d23d8d6d0 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -1,5 +1,7 @@ +const os = require('node:os'); const path = require('node:path'); const fs = require('fs-extra'); +const yaml = require('yaml'); const { BaseIdeSetup } = require('./_base-ide'); const prompts = require('../../../lib/prompts'); const { AgentCommandGenerator } = require('./shared/agent-command-generator'); @@ -24,6 +26,34 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { super(platformCode, platformConfig.name, platformConfig.preferred); this.platformConfig = platformConfig; this.installerConfig = platformConfig.installer || null; + + // Set configDir from target_dir so base-class detect() works + if (this.installerConfig?.target_dir) { + this.configDir = this.installerConfig.target_dir; + } + } + + /** + * Detect whether this IDE already has configuration in the project. + * For skill_format platforms, checks for bmad-prefixed entries in target_dir + * (matching old codex.js behavior) instead of just checking directory existence. + * @param {string} projectDir - Project directory + * @returns {Promise} + */ + async detect(projectDir) { + if (this.installerConfig?.skill_format && this.configDir) { + const dir = path.join(projectDir || process.cwd(), this.configDir); + if (await fs.pathExists(dir)) { + try { + const entries = await fs.readdir(dir); + return entries.some((e) => typeof e === 'string' && e.startsWith('bmad')); + } catch { + return false; + } + } + return false; + } + return super.detect(projectDir); } /** @@ -39,8 +69,8 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { const conflict = await this.findAncestorConflict(projectDir); if (conflict) { await prompts.log.error( - `Found existing BMAD commands in ancestor installation: ${conflict}\n` + - ` ${this.name} inherits commands from parent directories, so this would cause duplicates.\n` + + `Found existing BMAD skills in ancestor installation: ${conflict}\n` + + ` ${this.name} inherits skills from parent directories, so this would cause duplicates.\n` + ` Please remove the BMAD files from that directory first:\n` + ` rm -rf "${conflict}"/bmad*`, ); @@ -165,8 +195,13 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { for (const artifact of artifacts) { const content = this.renderTemplate(template, artifact); const filename = this.generateFilename(artifact, 'agent', extension); - const filePath = path.join(targetPath, filename); - await this.writeFile(filePath, content); + + if (config.skill_format) { + await this.writeSkillFile(targetPath, artifact, content); + } else { + const filePath = path.join(targetPath, filename); + await this.writeFile(filePath, content); + } count++; } @@ -198,8 +233,13 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { const { content: template, extension } = await this.loadTemplate(workflowTemplateType, '', config, finalTemplateType); const content = this.renderTemplate(template, artifact); const filename = this.generateFilename(artifact, 'workflow', extension); - const filePath = path.join(targetPath, filename); - await this.writeFile(filePath, content); + + if (config.skill_format) { + await this.writeSkillFile(targetPath, artifact, content); + } else { + const filePath = path.join(targetPath, filename); + await this.writeFile(filePath, content); + } count++; } } @@ -241,8 +281,13 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { const content = this.renderTemplate(template, artifact); const filename = this.generateFilename(artifact, artifact.type, extension); - const filePath = path.join(targetPath, filename); - await this.writeFile(filePath, content); + + if (config.skill_format) { + await this.writeSkillFile(targetPath, artifact, content); + } else { + const filePath = path.join(targetPath, filename); + await this.writeFile(filePath, content); + } if (artifact.type === 'task') { taskCount++; @@ -409,22 +454,146 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} // No default } - let rendered = template + // Replace _bmad placeholder with actual folder name BEFORE inserting paths, + // so that paths containing '_bmad' are not corrupted by the blanket replacement. + let rendered = template.replaceAll('_bmad', this.bmadFolderName); + + // Replace {{bmadFolderName}} placeholder if present + rendered = rendered.replaceAll('{{bmadFolderName}}', this.bmadFolderName); + + rendered = rendered .replaceAll('{{name}}', artifact.name || '') .replaceAll('{{module}}', artifact.module || 'core') .replaceAll('{{path}}', pathToUse) .replaceAll('{{description}}', artifact.description || `${artifact.name} ${artifact.type || ''}`) .replaceAll('{{workflow_path}}', pathToUse); - // Replace _bmad placeholder with actual folder name - rendered = rendered.replaceAll('_bmad', this.bmadFolderName); - - // Replace {{bmadFolderName}} placeholder if present - rendered = rendered.replaceAll('{{bmadFolderName}}', this.bmadFolderName); - return rendered; } + /** + * Write artifact as a skill directory with SKILL.md inside. + * Writes artifact as a skill directory with SKILL.md inside. + * @param {string} targetPath - Base skills directory + * @param {Object} artifact - Artifact data + * @param {string} content - Rendered template content + */ + async writeSkillFile(targetPath, artifact, content) { + const { resolveSkillName } = require('./shared/path-utils'); + + // Get the skill name (prefers canonicalId, falls back to path-derived) and remove .md + const flatName = resolveSkillName(artifact); + const skillName = path.basename(flatName.replace(/\.md$/, '')); + + if (!skillName) { + throw new Error(`Cannot derive skill name for artifact: ${artifact.relativePath || JSON.stringify(artifact)}`); + } + + // Create skill directory + const skillDir = path.join(targetPath, skillName); + await this.ensureDir(skillDir); + + // Transform content: rewrite frontmatter for skills format + const skillContent = this.transformToSkillFormat(content, skillName); + + await this.writeFile(path.join(skillDir, 'SKILL.md'), skillContent); + } + + /** + * Transform artifact content to Agent Skills format. + * Rewrites frontmatter to contain only unquoted name and description. + * @param {string} content - Original content with YAML frontmatter + * @param {string} skillName - Skill name (must match directory name) + * @returns {string} Transformed content + */ + transformToSkillFormat(content, skillName) { + // Normalize line endings + content = content.replaceAll('\r\n', '\n').replaceAll('\r', '\n'); + + // Parse frontmatter + const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/); + if (!fmMatch) { + // No frontmatter -- wrap with minimal frontmatter + const fm = yaml.stringify({ name: skillName, description: skillName }).trimEnd(); + return `---\n${fm}\n---\n\n${content}`; + } + + const frontmatter = fmMatch[1]; + const body = fmMatch[2]; + + // Parse frontmatter with yaml library to extract description + let description; + try { + const parsed = yaml.parse(frontmatter); + const rawDesc = parsed?.description; + description = typeof rawDesc === 'string' && rawDesc ? rawDesc : `${skillName} skill`; + } catch { + description = `${skillName} skill`; + } + + // Build new frontmatter with only name and description, unquoted + const newFrontmatter = yaml.stringify({ name: skillName, description: String(description) }, { lineWidth: 0 }).trimEnd(); + return `---\n${newFrontmatter}\n---\n${body}`; + } + + /** + * Install a custom agent launcher. + * For skill_format platforms, produces /SKILL.md. + * For flat platforms, produces a single file in target_dir. + * @param {string} projectDir - Project directory + * @param {string} agentName - Agent name (e.g., "fred-commit-poet") + * @param {string} agentPath - Path to compiled agent (relative to project root) + * @param {Object} metadata - Agent metadata + * @returns {Object|null} Info about created file/skill + */ + async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { + if (!this.installerConfig?.target_dir) return null; + + const { customAgentDashName } = require('./shared/path-utils'); + const targetPath = path.join(projectDir, this.installerConfig.target_dir); + await this.ensureDir(targetPath); + + // Build artifact to reuse existing template rendering. + // The default-agent template already includes the _bmad/ prefix before {{path}}, + // but agentPath is relative to project root (e.g. "_bmad/custom/agents/fred.md"). + // Strip the bmadFolderName prefix so the template doesn't produce a double path. + const bmadPrefix = this.bmadFolderName + '/'; + const normalizedPath = agentPath.startsWith(bmadPrefix) ? agentPath.slice(bmadPrefix.length) : agentPath; + + const artifact = { + type: 'agent-launcher', + name: agentName, + description: metadata?.description || `${agentName} agent`, + agentPath: normalizedPath, + relativePath: normalizedPath, + module: 'custom', + }; + + const { content: template } = await this.loadTemplate( + this.installerConfig.template_type || 'default', + 'agent', + this.installerConfig, + 'default-agent', + ); + const content = this.renderTemplate(template, artifact); + + if (this.installerConfig.skill_format) { + const skillName = customAgentDashName(agentName).replace(/\.md$/, ''); + const skillDir = path.join(targetPath, skillName); + await this.ensureDir(skillDir); + const skillContent = this.transformToSkillFormat(content, skillName); + const skillPath = path.join(skillDir, 'SKILL.md'); + await this.writeFile(skillPath, skillContent); + return { path: path.relative(projectDir, skillPath), command: `$${skillName}` }; + } + + // Flat file output + const filename = customAgentDashName(agentName); + const filePath = path.join(targetPath, filename); + await this.writeFile(filePath, content); + return { path: path.relative(projectDir, filePath), command: agentName }; + } + /** * Generate filename for artifact * @param {Object} artifact - Artifact data @@ -433,10 +602,11 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} * @returns {string} Generated filename */ generateFilename(artifact, artifactType, extension = '.md') { - const { toDashPath } = require('./shared/path-utils'); + const { resolveSkillName } = require('./shared/path-utils'); // Reuse central logic to ensure consistent naming conventions - const standardName = toDashPath(artifact.relativePath); + // Prefers canonicalId from manifest when available, falls back to path-derived name + const standardName = resolveSkillName(artifact); // Clean up potential double extensions from source files (e.g. .yaml.md, .xml.md -> .md) // This handles any extensions that might slip through toDashPath() @@ -476,8 +646,12 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} if (this.installerConfig?.legacy_targets) { if (!options.silent) await prompts.log.message(' Migrating legacy directories...'); for (const legacyDir of this.installerConfig.legacy_targets) { - await this.cleanupTarget(projectDir, legacyDir, options); - await this.removeEmptyParents(projectDir, legacyDir); + if (this.isGlobalPath(legacyDir)) { + await this.warnGlobalLegacy(legacyDir, options); + } else { + await this.cleanupTarget(projectDir, legacyDir, options); + await this.removeEmptyParents(projectDir, legacyDir); + } } } @@ -501,6 +675,41 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} } } + /** + * Check if a path is global (starts with ~ or is absolute) + * @param {string} p - Path to check + * @returns {boolean} + */ + isGlobalPath(p) { + return p.startsWith('~') || path.isAbsolute(p); + } + + /** + * Warn about stale BMAD files in a global legacy directory (never auto-deletes) + * @param {string} legacyDir - Legacy directory path (may start with ~) + * @param {Object} options - Options (silent, etc.) + */ + async warnGlobalLegacy(legacyDir, options = {}) { + try { + const expanded = legacyDir.startsWith('~/') + ? path.join(os.homedir(), legacyDir.slice(2)) + : legacyDir === '~' + ? os.homedir() + : legacyDir; + + if (!(await fs.pathExists(expanded))) return; + + const entries = await fs.readdir(expanded); + const bmadFiles = entries.filter((e) => typeof e === 'string' && e.startsWith('bmad')); + + if (bmadFiles.length > 0 && !options.silent) { + await prompts.log.warn(`Found ${bmadFiles.length} stale BMAD file(s) in ${expanded}. Remove manually: rm ${expanded}/bmad-*`); + } + } catch { + // Errors reading global paths are silently ignored + } + } + /** * Cleanup a specific target directory * @param {string} projectDir - Project directory diff --git a/tools/cli/installers/lib/ide/codex.js b/tools/cli/installers/lib/ide/codex.js deleted file mode 100644 index abee979fd..000000000 --- a/tools/cli/installers/lib/ide/codex.js +++ /dev/null @@ -1,440 +0,0 @@ -const path = require('node:path'); -const os = require('node:os'); -const fs = require('fs-extra'); -const yaml = require('yaml'); -const { BaseIdeSetup } = require('./_base-ide'); -const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); -const { AgentCommandGenerator } = require('./shared/agent-command-generator'); -const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); -const { getTasksFromBmad } = require('./shared/bmad-artifacts'); -const { toDashPath, customAgentDashName } = require('./shared/path-utils'); -const prompts = require('../../../lib/prompts'); - -/** - * Codex setup handler (CLI mode) - */ -class CodexSetup extends BaseIdeSetup { - constructor() { - super('codex', 'Codex', false); - } - - /** - * Setup Codex configuration - * @param {string} projectDir - Project directory - * @param {string} bmadDir - BMAD installation directory - * @param {Object} options - Setup options - */ - async setup(projectDir, bmadDir, options = {}) { - if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); - - // Always use CLI mode - const mode = 'cli'; - - const { artifacts, counts } = await this.collectClaudeArtifacts(projectDir, bmadDir, options); - - // Clean up old .codex/prompts locations (both global and project) - const oldGlobalDir = this.getOldCodexPromptDir(null, 'global'); - await this.clearOldBmadFiles(oldGlobalDir, options); - const oldProjectDir = this.getOldCodexPromptDir(projectDir, 'project'); - await this.clearOldBmadFiles(oldProjectDir, options); - - // Install to .agents/skills - const destDir = this.getCodexSkillsDir(projectDir); - await fs.ensureDir(destDir); - await this.clearOldBmadSkills(destDir, options); - - // Collect and write agent skills - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []); - const agentCount = await this.writeSkillArtifacts(destDir, agentArtifacts, 'agent-launcher'); - - // Collect and write task skills - const tasks = await getTasksFromBmad(bmadDir, options.selectedModules || []); - const taskArtifacts = []; - for (const task of tasks) { - const content = await this.readAndProcessWithProject( - task.path, - { - module: task.module, - name: task.name, - }, - projectDir, - ); - taskArtifacts.push({ - type: 'task', - name: task.name, - displayName: task.name, - module: task.module, - path: task.path, - sourcePath: task.path, - relativePath: path.join(task.module, 'tasks', `${task.name}.md`), - content, - }); - } - - const ttGen = new TaskToolCommandGenerator(this.bmadFolderName); - const taskSkillArtifacts = taskArtifacts.map((artifact) => ({ - ...artifact, - content: ttGen.generateCommandContent(artifact, artifact.type), - })); - const tasksWritten = await this.writeSkillArtifacts(destDir, taskSkillArtifacts, 'task'); - - // Collect and write workflow skills - const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); - const { artifacts: workflowArtifacts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); - const workflowCount = await this.writeSkillArtifacts(destDir, workflowArtifacts, 'workflow-command'); - - const written = agentCount + workflowCount + tasksWritten; - - if (!options.silent) { - await prompts.log.success( - `${this.name} configured: ${counts.agents} agents, ${counts.workflows} workflows, ${counts.tasks} tasks, ${written} skills → ${destDir}`, - ); - } - - return { - success: true, - mode, - artifacts, - counts, - destination: destDir, - written, - }; - } - - /** - * Detect Codex installation by checking for BMAD skills - */ - async detect(projectDir) { - const dir = this.getCodexSkillsDir(projectDir || process.cwd()); - - if (await fs.pathExists(dir)) { - try { - const entries = await fs.readdir(dir); - if (entries && entries.some((entry) => entry && typeof entry === 'string' && entry.startsWith('bmad'))) { - return true; - } - } catch { - // Ignore errors - } - } - - return false; - } - - /** - * Collect Claude-style artifacts for Codex export. - * Returns the normalized artifact list for further processing. - */ - async collectClaudeArtifacts(projectDir, bmadDir, options = {}) { - const selectedModules = options.selectedModules || []; - const artifacts = []; - - // Generate agent launchers - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules); - - for (const artifact of agentArtifacts) { - artifacts.push({ - type: 'agent', - module: artifact.module, - sourcePath: artifact.sourcePath, - relativePath: artifact.relativePath, - content: artifact.content, - }); - } - - const tasks = await getTasksFromBmad(bmadDir, selectedModules); - for (const task of tasks) { - const content = await this.readAndProcessWithProject( - task.path, - { - module: task.module, - name: task.name, - }, - projectDir, - ); - - artifacts.push({ - type: 'task', - name: task.name, - displayName: task.name, - module: task.module, - path: task.path, - sourcePath: task.path, - relativePath: path.join(task.module, 'tasks', `${task.name}.md`), - content, - }); - } - - const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); - const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); - artifacts.push(...workflowArtifacts); - - return { - artifacts, - counts: { - agents: agentArtifacts.length, - tasks: tasks.length, - workflows: workflowCounts.commands, - workflowLaunchers: workflowCounts.launchers, - }, - }; - } - - getCodexSkillsDir(projectDir) { - if (!projectDir) { - throw new Error('projectDir is required for project-scoped skill installation'); - } - return path.join(projectDir, '.agents', 'skills'); - } - - /** - * Get the old .codex/prompts directory for cleanup purposes - */ - getOldCodexPromptDir(projectDir = null, location = 'global') { - if (location === 'project' && projectDir) { - return path.join(projectDir, '.codex', 'prompts'); - } - return path.join(os.homedir(), '.codex', 'prompts'); - } - - /** - * Write artifacts as Agent Skills (agentskills.io format). - * Each artifact becomes a directory containing SKILL.md. - * @param {string} destDir - Base skills directory - * @param {Array} artifacts - Artifacts to write - * @param {string} artifactType - Type filter (e.g., 'agent-launcher', 'workflow-command', 'task') - * @returns {number} Number of skills written - */ - async writeSkillArtifacts(destDir, artifacts, artifactType) { - let writtenCount = 0; - - for (const artifact of artifacts) { - // Filter by type if the artifact has a type field - if (artifact.type && artifact.type !== artifactType) { - continue; - } - - // Get the dash-format name (e.g., bmad-bmm-create-prd.md) and remove .md - const flatName = toDashPath(artifact.relativePath); - const skillName = flatName.replace(/\.md$/, ''); - - // Create skill directory - const skillDir = path.join(destDir, skillName); - await fs.ensureDir(skillDir); - - // Transform content: rewrite frontmatter for skills format - const skillContent = this.transformToSkillFormat(artifact.content, skillName); - - // Write SKILL.md with platform-native line endings - const platformContent = skillContent.replaceAll('\n', os.EOL); - await fs.writeFile(path.join(skillDir, 'SKILL.md'), platformContent, 'utf8'); - writtenCount++; - } - - return writtenCount; - } - - /** - * Transform artifact content from Codex prompt format to Agent Skills format. - * Removes disable-model-invocation, ensures name matches directory. - * @param {string} content - Original content with YAML frontmatter - * @param {string} skillName - Skill name (must match directory name) - * @returns {string} Transformed content - */ - transformToSkillFormat(content, skillName) { - // Normalize line endings so body matches rebuilt frontmatter (both LF) - content = content.replaceAll('\r\n', '\n').replaceAll('\r', '\n'); - - // Parse frontmatter - const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/); - if (!fmMatch) { - // No frontmatter -- wrap with minimal frontmatter - const fm = yaml.stringify({ name: skillName, description: skillName }).trimEnd(); - return `---\n${fm}\n---\n\n${content}`; - } - - const frontmatter = fmMatch[1]; - const body = fmMatch[2]; - - // Parse frontmatter with yaml library to handle all quoting variants - let description; - try { - const parsed = yaml.parse(frontmatter); - description = parsed?.description || `${skillName} skill`; - } catch { - description = `${skillName} skill`; - } - - // Build new frontmatter with only skills-spec fields, let yaml handle quoting - const newFrontmatter = yaml.stringify({ name: skillName, description }, { lineWidth: 0 }).trimEnd(); - return `---\n${newFrontmatter}\n---\n${body}`; - } - - /** - * Remove existing BMAD skill directories from the skills directory. - */ - async clearOldBmadSkills(destDir, options = {}) { - if (!(await fs.pathExists(destDir))) { - return; - } - - let entries; - try { - entries = await fs.readdir(destDir); - } catch (error) { - if (!options.silent) await prompts.log.warn(`Warning: Could not read directory ${destDir}: ${error.message}`); - return; - } - - if (!entries || !Array.isArray(entries)) { - return; - } - - for (const entry of entries) { - if (!entry || typeof entry !== 'string') { - continue; - } - if (!entry.startsWith('bmad')) { - continue; - } - - const entryPath = path.join(destDir, entry); - try { - await fs.remove(entryPath); - } catch (error) { - if (!options.silent) { - await prompts.log.message(` Skipping ${entry}: ${error.message}`); - } - } - } - } - - /** - * Clean old BMAD files from legacy .codex/prompts directories. - */ - async clearOldBmadFiles(destDir, options = {}) { - if (!(await fs.pathExists(destDir))) { - return; - } - - let entries; - try { - entries = await fs.readdir(destDir); - } catch (error) { - // Directory exists but can't be read - skip cleanup - if (!options.silent) await prompts.log.warn(`Warning: Could not read directory ${destDir}: ${error.message}`); - return; - } - - if (!entries || !Array.isArray(entries)) { - return; - } - - for (const entry of entries) { - // Skip non-strings or undefined entries - if (!entry || typeof entry !== 'string') { - continue; - } - if (!entry.startsWith('bmad')) { - continue; - } - - const entryPath = path.join(destDir, entry); - try { - await fs.remove(entryPath); - } catch (error) { - if (!options.silent) { - await prompts.log.message(` Skipping ${entry}: ${error.message}`); - } - } - } - } - - async readAndProcessWithProject(filePath, metadata, projectDir) { - const rawContent = await fs.readFile(filePath, 'utf8'); - const content = rawContent.replaceAll('\r\n', '\n').replaceAll('\r', '\n'); - return super.processContent(content, metadata, projectDir); - } - - /** - * Get instructions for project-specific installation - * @param {string} projectDir - Optional project directory - * @param {string} destDir - Optional destination directory - * @returns {string} Instructions text - */ - getProjectSpecificInstructions(projectDir = null, destDir = null) { - const lines = [ - 'Project-Specific Codex Configuration', - '', - `Skills installed to: ${destDir || '/.agents/skills'}`, - '', - 'Codex automatically discovers skills in .agents/skills/ at and above the current directory and in your home directory.', - 'No additional configuration is needed.', - ]; - - return lines.join('\n'); - } - - /** - * Cleanup Codex configuration - cleans both new .agents/skills and old .codex/prompts - */ - async cleanup(projectDir = null) { - // Clean old .codex/prompts locations - const oldGlobalDir = this.getOldCodexPromptDir(null, 'global'); - await this.clearOldBmadFiles(oldGlobalDir); - - if (projectDir) { - const oldProjectDir = this.getOldCodexPromptDir(projectDir, 'project'); - await this.clearOldBmadFiles(oldProjectDir); - - // Clean new .agents/skills location - const destDir = this.getCodexSkillsDir(projectDir); - await this.clearOldBmadSkills(destDir); - } - } - - /** - * Install a custom agent launcher for Codex as an Agent Skill - * @param {string} projectDir - Project directory - * @param {string} agentName - Agent name (e.g., "fred-commit-poet") - * @param {string} agentPath - Path to compiled agent (relative to project root) - * @param {Object} metadata - Agent metadata - * @returns {Object|null} Info about created skill - */ - async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) { - const destDir = this.getCodexSkillsDir(projectDir); - - // Skill name from the dash name (without .md) - const skillName = customAgentDashName(agentName).replace(/\.md$/, ''); - const skillDir = path.join(destDir, skillName); - await fs.ensureDir(skillDir); - - const description = metadata?.description || `${agentName} agent`; - const fm = yaml.stringify({ name: skillName, description }).trimEnd(); - const skillContent = - `---\n${fm}\n---\n` + - "\nYou must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.\n" + - '\n\n' + - `1. LOAD the FULL agent file from @${agentPath}\n` + - '2. READ its entire contents - this contains the complete agent persona, menu, and instructions\n' + - '3. FOLLOW every step in the section precisely\n' + - '4. DISPLAY the welcome/greeting as instructed\n' + - '5. PRESENT the numbered menu\n' + - '6. WAIT for user input before proceeding\n' + - '\n'; - - // Write with platform-native line endings - const platformContent = skillContent.replaceAll('\n', os.EOL); - const skillPath = path.join(skillDir, 'SKILL.md'); - await fs.writeFile(skillPath, platformContent, 'utf8'); - - return { - path: path.relative(projectDir, skillPath), - command: `$${skillName}`, - }; - } -} - -module.exports = { CodexSetup }; diff --git a/tools/cli/installers/lib/ide/manager.js b/tools/cli/installers/lib/ide/manager.js index 9e286fdd3..654574a25 100644 --- a/tools/cli/installers/lib/ide/manager.js +++ b/tools/cli/installers/lib/ide/manager.js @@ -8,7 +8,7 @@ const prompts = require('../../../lib/prompts'); * Dynamically discovers and loads IDE handlers * * Loading strategy: - * 1. Custom installer files (codex.js, github-copilot.js, kilo.js, rovodev.js) - for platforms with unique installation logic + * 1. Custom installer files (github-copilot.js, kilo.js, rovodev.js) - for platforms with unique installation logic * 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns */ class IdeManager { @@ -44,7 +44,7 @@ class IdeManager { /** * Dynamically load all IDE handlers - * 1. Load custom installer files first (codex.js, github-copilot.js, kilo.js, rovodev.js) + * 1. Load custom installer files first (github-copilot.js, kilo.js, rovodev.js) * 2. Load config-driven handlers from platform-codes.yaml */ async loadHandlers() { @@ -58,10 +58,11 @@ class IdeManager { /** * Load custom installer files (unique installation logic) * These files have special installation patterns that don't fit the config-driven model + * Note: codex was migrated to config-driven (platform-codes.yaml) and no longer needs a custom installer */ async loadCustomInstallerFiles() { const ideDir = __dirname; - const customFiles = ['codex.js', 'github-copilot.js', 'kilo.js', 'rovodev.js']; + const customFiles = ['github-copilot.js', 'kilo.js', 'rovodev.js']; for (const file of customFiles) { const filePath = path.join(ideDir, file); @@ -189,14 +190,6 @@ class IdeManager { if (r.tasks > 0) parts.push(`${r.tasks} tasks`); if (r.tools > 0) parts.push(`${r.tools} tools`); detail = parts.join(', '); - } else if (handlerResult && handlerResult.counts) { - // Codex handler returns { success, counts: { agents, workflows, tasks }, written } - const c = handlerResult.counts; - const parts = []; - if (c.agents > 0) parts.push(`${c.agents} agents`); - if (c.workflows > 0) parts.push(`${c.workflows} workflows`); - if (c.tasks > 0) parts.push(`${c.tasks} tasks`); - detail = parts.join(', '); } else if (handlerResult && handlerResult.modes !== undefined) { // Kilo handler returns { success, modes, workflows, tasks, tools } const parts = []; diff --git a/tools/cli/installers/lib/ide/platform-codes.yaml b/tools/cli/installers/lib/ide/platform-codes.yaml index 4e6ca8070..99269552f 100644 --- a/tools/cli/installers/lib/ide/platform-codes.yaml +++ b/tools/cli/installers/lib/ide/platform-codes.yaml @@ -20,8 +20,11 @@ platforms: category: ide description: "Google's AI development environment" installer: - target_dir: .agent/workflows + legacy_targets: + - .agent/workflows + target_dir: .agent/skills template_type: antigravity + skill_format: true auggie: name: "Auggie" @@ -29,8 +32,11 @@ platforms: category: cli description: "AI development tool" installer: - target_dir: .augment/commands + legacy_targets: + - .augment/commands + target_dir: .augment/skills template_type: default + skill_format: true claude-code: name: "Claude Code" @@ -38,8 +44,11 @@ platforms: category: cli description: "Anthropic's official CLI for Claude" installer: - target_dir: .claude/commands + legacy_targets: + - .claude/commands + target_dir: .claude/skills template_type: default + skill_format: true ancestor_conflict_check: true cline: @@ -56,7 +65,15 @@ platforms: preferred: false category: cli description: "OpenAI Codex integration" - # No installer config - uses custom codex.js + installer: + legacy_targets: + - .codex/prompts + - ~/.codex/prompts + target_dir: .agents/skills + template_type: default + skill_format: true + ancestor_conflict_check: true + artifact_types: [agents, workflows, tasks] codebuddy: name: "CodeBuddy" @@ -82,8 +99,11 @@ platforms: category: ide description: "AI-first code editor" installer: - target_dir: .cursor/commands + legacy_targets: + - .cursor/commands + target_dir: .cursor/skills template_type: default + skill_format: true gemini: name: "Gemini CLI" @@ -123,8 +143,11 @@ platforms: category: ide description: "Amazon's AI-powered IDE" installer: - target_dir: .kiro/steering + legacy_targets: + - .kiro/steering + target_dir: .kiro/skills template_type: kiro + skill_format: true opencode: name: "OpenCode" @@ -133,15 +156,14 @@ platforms: description: "OpenCode terminal coding assistant" installer: legacy_targets: + - .opencode/agents + - .opencode/commands - .opencode/agent - .opencode/command - targets: - - target_dir: .opencode/agents - template_type: opencode - artifact_types: [agents] - - target_dir: .opencode/commands - template_type: opencode - artifact_types: [workflows, tasks, tools] + target_dir: .opencode/skills + template_type: opencode + skill_format: true + ancestor_conflict_check: true qwen: name: "QwenCoder" @@ -183,8 +205,11 @@ platforms: category: ide description: "AI-powered IDE with cascade flows" installer: - target_dir: .windsurf/workflows + legacy_targets: + - .windsurf/workflows + target_dir: .windsurf/skills template_type: windsurf + skill_format: true # ============================================================================ # Installer Config Schema @@ -203,9 +228,11 @@ platforms: # artifact_types: [agents, workflows, tasks, tools] # artifact_types: array (optional) # Filter which artifacts to install (default: all) # skip_existing: boolean (optional) # Skip files that already exist (default: false) +# skill_format: boolean (optional) # Use directory-per-skill output: /SKILL.md +# # with clean frontmatter (name + description, unquoted) # ancestor_conflict_check: boolean (optional) # Refuse install when ancestor dir has BMAD files # # in the same target_dir (for IDEs that inherit -# # commands from parent directories) +# # skills from parent directories) # ============================================================================ # Platform Categories diff --git a/tools/cli/installers/lib/ide/shared/agent-command-generator.js b/tools/cli/installers/lib/ide/shared/agent-command-generator.js index 0915c306b..37820992e 100644 --- a/tools/cli/installers/lib/ide/shared/agent-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/agent-command-generator.js @@ -47,6 +47,7 @@ class AgentCommandGenerator { name: agent.name, description: agent.description || `${agent.name} agent`, module: agent.module, + canonicalId: agent.canonicalId || '', relativePath: path.join(agent.module, 'agents', agentPathInModule), // For command filename agentPath: agentRelPath, // Relative path to actual agent file content: launcherContent, diff --git a/tools/cli/installers/lib/ide/shared/bmad-artifacts.js b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js index 7bcfd6a79..d3edf0cd2 100644 --- a/tools/cli/installers/lib/ide/shared/bmad-artifacts.js +++ b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js @@ -1,5 +1,6 @@ const path = require('node:path'); const fs = require('fs-extra'); +const { loadSkillManifest, getCanonicalId } = require('./skill-manifest'); /** * Helpers for gathering BMAD agents/tasks from the installed tree. @@ -34,6 +35,7 @@ async function getAgentsFromBmad(bmadDir, selectedModules = []) { const agentDirPath = path.join(standaloneAgentsDir, agentDir.name); const agentFiles = await fs.readdir(agentDirPath); + const skillManifest = await loadSkillManifest(agentDirPath); for (const file of agentFiles) { if (!file.endsWith('.md')) continue; @@ -48,6 +50,7 @@ async function getAgentsFromBmad(bmadDir, selectedModules = []) { path: filePath, name: file.replace('.md', ''), module: 'standalone', // Mark as standalone agent + canonicalId: getCanonicalId(skillManifest, file), }); } } @@ -84,6 +87,7 @@ async function getAgentsFromDir(dirPath, moduleName, relativePath = '') { } const entries = await fs.readdir(dirPath, { withFileTypes: true }); + const skillManifest = await loadSkillManifest(dirPath); for (const entry of entries) { // Skip if entry.name is undefined or not a string @@ -124,6 +128,7 @@ async function getAgentsFromDir(dirPath, moduleName, relativePath = '') { name: entry.name.replace('.md', ''), module: moduleName, relativePath: newRelativePath, // Keep the .md extension for the full path + canonicalId: getCanonicalId(skillManifest, entry.name), }); } } @@ -139,6 +144,7 @@ async function getTasksFromDir(dirPath, moduleName) { } const files = await fs.readdir(dirPath); + const skillManifest = await loadSkillManifest(dirPath); for (const file of files) { // Include both .md and .xml task files @@ -160,6 +166,7 @@ async function getTasksFromDir(dirPath, moduleName) { path: filePath, name: file.replace(ext, ''), module: moduleName, + canonicalId: getCanonicalId(skillManifest, file), }); } diff --git a/tools/cli/installers/lib/ide/shared/path-utils.js b/tools/cli/installers/lib/ide/shared/path-utils.js index 519669233..45efd2ec1 100644 --- a/tools/cli/installers/lib/ide/shared/path-utils.js +++ b/tools/cli/installers/lib/ide/shared/path-utils.js @@ -264,6 +264,21 @@ function parseUnderscoreName(filename) { }; } +/** + * Resolve the skill name for an artifact. + * Prefers canonicalId from a bmad-skill-manifest.yaml sidecar when available, + * falling back to the path-derived name from toDashPath(). + * + * @param {Object} artifact - Artifact object (must have relativePath; may have canonicalId) + * @returns {string} Filename like 'bmad-create-prd.md' or 'bmad-agent-bmm-pm.md' + */ +function resolveSkillName(artifact) { + if (artifact.canonicalId) { + return `${artifact.canonicalId}.md`; + } + return toDashPath(artifact.relativePath); +} + // Backward compatibility aliases (colon format was same as underscore) const toColonName = toUnderscoreName; const toColonPath = toUnderscorePath; @@ -275,6 +290,7 @@ module.exports = { // New standard (dash-based) toDashName, toDashPath, + resolveSkillName, customAgentDashName, isDashFormat, parseDashName, diff --git a/tools/cli/installers/lib/ide/shared/skill-manifest.js b/tools/cli/installers/lib/ide/shared/skill-manifest.js new file mode 100644 index 000000000..ff940242f --- /dev/null +++ b/tools/cli/installers/lib/ide/shared/skill-manifest.js @@ -0,0 +1,48 @@ +const path = require('node:path'); +const fs = require('fs-extra'); +const yaml = require('yaml'); + +/** + * Load bmad-skill-manifest.yaml from a directory. + * Single-entry manifests (canonicalId at top level) apply to all files in the directory. + * Multi-entry manifests are keyed by source filename. + * @param {string} dirPath - Directory to check for bmad-skill-manifest.yaml + * @returns {Object|null} Parsed manifest or null + */ +async function loadSkillManifest(dirPath) { + const manifestPath = path.join(dirPath, 'bmad-skill-manifest.yaml'); + try { + if (!(await fs.pathExists(manifestPath))) return null; + const content = await fs.readFile(manifestPath, 'utf8'); + const parsed = yaml.parse(content); + if (!parsed || typeof parsed !== 'object') return null; + if (parsed.canonicalId) return { __single: parsed }; + return parsed; + } catch (error) { + console.warn(`Warning: Failed to parse bmad-skill-manifest.yaml in ${dirPath}: ${error.message}`); + return null; + } +} + +/** + * Get the canonicalId for a specific file from a loaded skill manifest. + * @param {Object|null} manifest - Loaded manifest (from loadSkillManifest) + * @param {string} filename - Source filename to look up (e.g., 'pm.md', 'help.md', 'pm.agent.yaml') + * @returns {string} canonicalId or empty string + */ +function getCanonicalId(manifest, filename) { + if (!manifest) return ''; + // Single-entry manifest applies to all files in the directory + if (manifest.__single) return manifest.__single.canonicalId || ''; + // Multi-entry: look up by filename directly + if (manifest[filename]) return manifest[filename].canonicalId || ''; + // Fallback: try alternate extensions for compiled files + const baseName = filename.replace(/\.(md|xml)$/i, ''); + const agentKey = `${baseName}.agent.yaml`; + if (manifest[agentKey]) return manifest[agentKey].canonicalId || ''; + const xmlKey = `${baseName}.xml`; + if (manifest[xmlKey]) return manifest[xmlKey].canonicalId || ''; + return ''; +} + +module.exports = { loadSkillManifest, getCanonicalId }; diff --git a/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js b/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js index 93e5b9a81..f21a5d174 100644 --- a/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js @@ -50,6 +50,7 @@ class TaskToolCommandGenerator { displayName: task.displayName || task.name, description: task.description || `Execute ${task.displayName || task.name}`, module: task.module, + canonicalId: task.canonicalId || '', // Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows) relativePath: `${task.module}/tasks/${task.name}${taskExt}`, path: taskPath, @@ -75,6 +76,7 @@ class TaskToolCommandGenerator { displayName: tool.displayName || tool.name, description: tool.description || `Execute ${tool.displayName || tool.name}`, module: tool.module, + canonicalId: tool.canonicalId || '', // Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows) relativePath: `${tool.module}/tools/${tool.name}${toolExt}`, path: toolPath, diff --git a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js b/tools/cli/installers/lib/ide/shared/workflow-command-generator.js index d94e77db1..793252bac 100644 --- a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/workflow-command-generator.js @@ -93,6 +93,7 @@ class WorkflowCommandGenerator { name: workflow.name, description: workflow.description || `${workflow.name} workflow`, module: workflow.module, + canonicalId: workflow.canonicalId || '', relativePath: path.join(workflow.module, 'workflows', `${workflow.name}.md`), workflowPath: workflowRelPath, // Relative path to actual workflow file content: commandContent, diff --git a/tools/docs/native-skills-migration-checklist.md b/tools/docs/native-skills-migration-checklist.md new file mode 100644 index 000000000..ba8f412ed --- /dev/null +++ b/tools/docs/native-skills-migration-checklist.md @@ -0,0 +1,215 @@ +# Native Skills Migration Checklist + +Branch: `refactor/all-is-skills` + +Scope: migrate the BMAD-supported platforms that fully support the Agent Skills standard from legacy installer outputs to native skills output. + +Current branch status: + +- `Claude Code` has already been moved to `.claude/skills` +- `Codex CLI` has already been moved to `.agents/skills` + +This checklist now includes those completed platforms plus the remaining full-support platforms. + +## Claude Code + +Support assumption: full Agent Skills support. BMAD has already migrated from `.claude/commands` to `.claude/skills`. + +- [ ] Confirm current implementation still matches Claude Code skills expectations +- [ ] Confirm legacy cleanup for `.claude/commands` +- [ ] Test fresh install +- [ ] Test reinstall/upgrade from legacy command output +- [ ] Confirm ancestor conflict protection +- [ ] Implement/extend automated tests as needed +- [ ] Commit any follow-up fixes if required + +## Codex CLI + +Support assumption: full Agent Skills support. BMAD has already migrated from `.codex/prompts` to `.agents/skills`. + +- [ ] Confirm current implementation still matches Codex CLI skills expectations +- [ ] Confirm legacy cleanup for project and global `.codex/prompts` +- [ ] Test fresh install +- [ ] Test reinstall/upgrade from legacy prompt output +- [x] Confirm ancestor conflict protection because Codex inherits parent-directory `.agents/skills` +- [ ] Implement/extend automated tests as needed +- [ ] Commit any follow-up fixes if required + +## Cursor + +Support assumption: full Agent Skills support. BMAD currently installs legacy command files to `.cursor/commands`; target should move to a native skills directory. + +- [x] Confirm current Cursor skills path and that BMAD should target `.cursor/skills` +- [x] Implement installer migration to native skills output +- [x] Add legacy cleanup for `.cursor/commands` +- [x] Test fresh install +- [x] Test reinstall/upgrade from legacy command output +- [x] Confirm no ancestor conflict protection is needed because a child workspace surfaced child `.cursor/skills` entries but not a parent-only skill during manual verification +- [ ] Implement/extend automated tests +- [x] Commit + +## Windsurf + +Support assumption: full Agent Skills support. Windsurf docs confirm workspace skills at `.windsurf/skills` and global skills at `~/.codeium/windsurf/skills`. BMAD has now migrated from `.windsurf/workflows` to `.windsurf/skills`. Manual verification also confirmed that Windsurf custom skills are triggered via `@skill-name`, not slash commands. + +- [x] Confirm Windsurf native skills directory as `.windsurf/skills` +- [x] Implement installer migration to native skills output +- [x] Add legacy cleanup for `.windsurf/workflows` +- [x] Test fresh install +- [x] Test reinstall/upgrade from legacy workflow output +- [x] Confirm no ancestor conflict protection is needed because manual Windsurf verification showed child-local `@` skills loaded while a parent-only skill was not inherited +- [x] Implement/extend automated tests +- [x] Commit + +## Cline + +Support assumption: full Agent Skills support. BMAD currently installs workflow files to `.clinerules/workflows`; target should move to the platform's native skills directory. + +- [ ] Confirm current Cline skills path and whether `.cline/skills` is the correct BMAD target +- [ ] Implement installer migration to native skills output +- [ ] Add legacy cleanup for `.clinerules/workflows` +- [ ] Test fresh install +- [ ] Test reinstall/upgrade from legacy workflow output +- [ ] Confirm ancestor conflict protection where applicable +- [ ] Implement/extend automated tests +- [ ] Commit + +## Google Antigravity + +Support assumption: full Agent Skills support. Antigravity docs confirm workspace skills at `.agent/skills//` and global skills at `~/.gemini/antigravity/skills//`. BMAD has now migrated from `.agent/workflows` to `.agent/skills`. + +- [x] Confirm Antigravity native skills path and project/global precedence +- [x] Implement installer migration to native skills output +- [x] Add legacy cleanup for `.agent/workflows` +- [x] Test fresh install +- [x] Test reinstall/upgrade from legacy workflow output +- [x] Confirm no ancestor conflict protection is needed because manual Antigravity verification in `/tmp/antigravity-ancestor-repro/parent/child` showed only the child-local `child-only` skill, with no inherited parent `.agent/skills` entry +- [x] Implement/extend automated tests +- [x] Commit + +## Auggie + +Support assumption: full Agent Skills support. BMAD currently installs commands to `.augment/commands`; target should move to `.augment/skills`. + +- [x] Confirm Auggie native skills path and compatibility loading from `.claude/skills` and `.agents/skills` via Augment docs plus local `auggie --print` repros +- [x] Implement installer migration to native skills output +- [x] Add legacy cleanup for `.augment/commands` +- [x] Test fresh install +- [x] Test reinstall/upgrade from legacy command output +- [x] Confirm no ancestor conflict protection is needed because local `auggie --workspace-root` repro showed child-local `.augment/skills` loading `child-only` but not parent `parent-only` +- [x] Implement/extend automated tests +- [ ] Commit + +## CodeBuddy + +Support assumption: full Agent Skills support. BMAD currently installs commands to `.codebuddy/commands`; target should move to `.codebuddy/skills`. + +- [ ] Confirm CodeBuddy native skills path and any naming/frontmatter requirements +- [ ] Implement installer migration to native skills output +- [ ] Add legacy cleanup for `.codebuddy/commands` +- [ ] Test fresh install +- [ ] Test reinstall/upgrade from legacy command output +- [ ] Confirm ancestor conflict protection where applicable +- [ ] Implement/extend automated tests +- [ ] Commit + +## Crush + +Support assumption: full Agent Skills support. BMAD currently installs commands to `.crush/commands`; target should move to the platform's native skills location. + +- [ ] Confirm Crush project-local versus global skills path and BMAD's preferred install target +- [ ] Implement installer migration to native skills output +- [ ] Add legacy cleanup for `.crush/commands` +- [ ] Test fresh install +- [ ] Test reinstall/upgrade from legacy command output +- [ ] Confirm ancestor conflict protection where applicable +- [ ] Implement/extend automated tests +- [ ] Commit + +## Kiro + +Support assumption: full Agent Skills support. Kiro docs confirm project skills at `.kiro/skills//SKILL.md` and describe steering as a separate rules mechanism, not a required compatibility layer. BMAD has now migrated from `.kiro/steering` to `.kiro/skills`. Manual app verification also confirmed that Kiro can surface skills in Slash when the relevant UI setting is enabled, and that it does not inherit ancestor `.kiro/skills` directories. + +- [x] Confirm Kiro skills path and verify BMAD should stop writing steering artifacts for this migration +- [x] Implement installer migration to native skills output +- [x] Add legacy cleanup for `.kiro/steering` +- [x] Test fresh install +- [x] Test reinstall/upgrade from legacy steering output +- [x] Confirm no ancestor conflict protection is needed because manual Kiro verification showed Slash-visible skills from the current workspace only, with no ancestor `.kiro/skills` inheritance +- [x] Implement/extend automated tests +- [x] Commit + +## OpenCode + +Support assumption: full Agent Skills support. BMAD currently splits output between `.opencode/agents` and `.opencode/commands`; target should consolidate to `.opencode/skills`. + +- [x] Confirm OpenCode native skills path and compatibility loading from `.claude/skills` and `.agents/skills` in OpenCode docs and with local `opencode run` repros +- [x] Implement installer migration from multi-target legacy output to single native skills target +- [x] Add legacy cleanup for `.opencode/agents`, `.opencode/commands`, `.opencode/agent`, and `.opencode/command` +- [x] Test fresh install +- [x] Test reinstall/upgrade from split legacy output +- [x] Confirm ancestor conflict protection is required because local `opencode run` repros loaded both child-local `child-only` and ancestor `parent-only`, matching the docs that project-local skill discovery walks upward to the git worktree +- [x] Implement/extend automated tests +- [ ] Commit + +## Roo Code + +Support assumption: full Agent Skills support. BMAD currently installs commands to `.roo/commands`; target should move to `.roo/skills` or the correct mode-aware skill directories. + +- [ ] Confirm Roo native skills path and whether BMAD should use generic or mode-specific skill directories +- [ ] Implement installer migration to native skills output +- [ ] Add legacy cleanup for `.roo/commands` +- [ ] Test fresh install +- [ ] Test reinstall/upgrade from legacy command output +- [ ] Confirm ancestor conflict protection where applicable +- [ ] Implement/extend automated tests +- [ ] Commit + +## Trae + +Support assumption: full Agent Skills support. BMAD currently installs rule files to `.trae/rules`; target should move to the platform's native skills directory. + +- [ ] Confirm Trae native skills path and whether the current `.trae/rules` path is still required for compatibility +- [ ] Implement installer migration to native skills output +- [ ] Add legacy cleanup for `.trae/rules` +- [ ] Test fresh install +- [ ] Test reinstall/upgrade from legacy rules output +- [ ] Confirm ancestor conflict protection where applicable +- [ ] Implement/extend automated tests +- [ ] Commit + +## GitHub Copilot + +Support assumption: full Agent Skills support. BMAD currently uses a custom installer that generates `.github/agents`, `.github/prompts`, and `.github/copilot-instructions.md`; target should move to `.github/skills`. + +- [ ] Confirm GitHub Copilot native skills path and whether `.github/agents` remains necessary as a compatibility layer +- [ ] Design the migration away from the custom prompt/agent installer model +- [ ] Implement native skills output, ideally with shared config-driven code where practical +- [ ] Add legacy cleanup for `.github/agents`, `.github/prompts`, and any BMAD-owned Copilot instruction file behavior that should be retired +- [ ] Test fresh install +- [ ] Test reinstall/upgrade from legacy custom installer output +- [ ] Confirm ancestor conflict protection where applicable +- [ ] Implement/extend automated tests +- [ ] Commit + +## KiloCoder + +Support assumption: full Agent Skills support. BMAD currently uses a custom installer that writes `.kilocodemodes` and `.kilocode/workflows`; target should move to native skills output. + +- [ ] Confirm KiloCoder native skills path and whether `.kilocodemodes` should be removed entirely or retained temporarily for compatibility +- [ ] Design the migration away from modes plus workflow markdown +- [ ] Implement native skills output +- [ ] Add legacy cleanup for `.kilocode/workflows` and BMAD-owned entries in `.kilocodemodes` +- [ ] Test fresh install +- [ ] Test reinstall/upgrade from legacy custom installer output +- [ ] Confirm ancestor conflict protection where applicable +- [ ] Implement/extend automated tests +- [ ] Commit + +## Summary Gates + +- [ ] All full-support BMAD platforms install `SKILL.md` directory-based output +- [ ] No full-support platform still emits BMAD command/workflow/rule files as its primary install format +- [ ] Legacy cleanup paths are defined for every migrated platform +- [ ] Automated coverage exists for config-driven and custom-installer migrations +- [ ] Installer docs and migration notes updated after code changes land