refactor: all-is-skills - Convert BMAD to skills-based architecture (#1834)
* feat(skills): add canonical bmad- naming via skill manifests Add bmad-skill-manifest.yaml sidecars to all 38 capabilities (tasks, agents, workflows) declaring canonicalId as the single source of truth for skill names. Update Claude Code and Codex installers to prefer canonicalId over path-derived names, with graceful fallback. - 24 manifest files covering 38 capabilities - New shared skill-manifest.js utility for manifest loading - resolveSkillName() in path-utils.js bridges manifest → installer - All command generators propagate canonicalId through CSV manifests - Drops bmm module prefix from all user-facing skill names Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(skills): claude-code installer outputs .claude/skills/<name>/SKILL.md Refactor the config-driven installer to emit Agent Skills Open Standard format for Claude Code: directory-per-skill with SKILL.md entrypoint, unquoted YAML frontmatter, and full canonical names. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(installer): migrate codex to config-driven pipeline Delete the custom codex.js installer (441 lines) and route Codex through the config-driven pipeline via platform-codes.yaml. This fixes 7 task/tool descriptions that were generic due to bypassing manifests, and eliminates duplicate transformToSkillFormat code. Key changes: - Add codex entry to platform-codes.yaml with skill_format + legacy_targets - Remove codex from custom installer list in manager.js - Add installCustomAgentLauncher() to config-driven for custom agent support - Add detect() override for skill_format platforms (bmad-prefix check) - Set configDir from target_dir for base-class detect() compatibility Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(installer): guard codex skill installs in nested directories * fix(installer): warn on stale global legacy skill dirs * feat(installer): migrate cursor to native skills * Migrate Windsurf installer to native skills * Clarify Windsurf skill invocation in checklist * feat(installer): migrate kiro to native skills * docs: record kiro skill visibility verification * Migrate Antigravity installer to native skills * Document Antigravity ancestor skill verification * Synchronize native skills migration checklist * Migrate Auggie installer to native skills * Migrate OpenCode installer to native skills * Document live skill verification for Auggie and OpenCode * fix(test): replace _bmad filesystem dependency with self-contained fixture The installation component tests walked up the filesystem looking for a pre-installed _bmad directory, which exists locally but not in CI. Replace findInstalledBmadDir() with createTestBmadFixture() that creates a minimal temp directory with fake compiled agents, making tests fully self-contained. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Brian <bmadcode@gmail.com>
This commit is contained in:
parent
09ce8559f2
commit
0d3b317598
|
|
@ -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"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
canonicalId: bmad-tech-writer
|
||||||
|
type: agent
|
||||||
|
description: "Technical Writer for documentation, Mermaid diagrams, and standards compliance"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
canonicalId: bmad-create-product-brief
|
||||||
|
type: workflow
|
||||||
|
description: "Create product brief through collaborative discovery"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
canonicalId: bmad-create-ux-design
|
||||||
|
type: workflow
|
||||||
|
description: "Plan UX patterns and design specifications"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
canonicalId: bmad-check-implementation-readiness
|
||||||
|
type: workflow
|
||||||
|
description: "Validate PRD, UX, Architecture and Epics specs are complete"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
canonicalId: bmad-create-architecture
|
||||||
|
type: workflow
|
||||||
|
description: "Create architecture solution design decisions for AI agent consistency"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
canonicalId: bmad-create-epics-and-stories
|
||||||
|
type: workflow
|
||||||
|
description: "Break requirements into epics and user stories"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
canonicalId: bmad-code-review
|
||||||
|
type: workflow
|
||||||
|
description: "Perform adversarial code review finding specific issues"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
canonicalId: bmad-correct-course
|
||||||
|
type: workflow
|
||||||
|
description: "Manage significant changes during sprint execution"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
canonicalId: bmad-create-story
|
||||||
|
type: workflow
|
||||||
|
description: "Creates a dedicated story file with all the context needed for implementation"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
canonicalId: bmad-dev-story
|
||||||
|
type: workflow
|
||||||
|
description: "Execute story implementation following a context-filled story spec file"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
canonicalId: bmad-retrospective
|
||||||
|
type: workflow
|
||||||
|
description: "Post-epic review to extract lessons and assess success"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
canonicalId: bmad-sprint-planning
|
||||||
|
type: workflow
|
||||||
|
description: "Generate sprint status tracking from epics"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
canonicalId: bmad-sprint-status
|
||||||
|
type: workflow
|
||||||
|
description: "Summarize sprint status and surface risks"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
canonicalId: bmad-quick-dev-new-preview
|
||||||
|
type: workflow
|
||||||
|
description: "Unified quick flow - clarify intent, plan, implement, review, present"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
canonicalId: bmad-quick-dev
|
||||||
|
type: workflow
|
||||||
|
description: "Implement a Quick Tech Spec for small changes or features"
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
canonicalId: bmad-document-project
|
||||||
|
type: workflow
|
||||||
|
description: "Document brownfield projects for AI context"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
canonicalId: bmad-generate-project-context
|
||||||
|
type: workflow
|
||||||
|
description: "Create project-context.md with AI rules"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
canonicalId: bmad-qa-generate-e2e-tests
|
||||||
|
type: workflow
|
||||||
|
description: "Generate end-to-end automated tests for existing features"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
canonicalId: bmad-master
|
||||||
|
type: agent
|
||||||
|
description: "BMad Master Executor, Knowledge Custodian, and Workflow Orchestrator"
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
canonicalId: bmad-brainstorming
|
||||||
|
type: workflow
|
||||||
|
description: "Facilitate interactive brainstorming sessions using diverse creative techniques and ideation methods"
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
canonicalId: bmad-party-mode
|
||||||
|
type: workflow
|
||||||
|
description: "Orchestrates group discussions between all installed BMAD agents"
|
||||||
|
|
@ -12,9 +12,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
|
const os = require('node:os');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { YamlXmlBuilder } = require('../tools/cli/lib/yaml-xml-builder');
|
const { YamlXmlBuilder } = require('../tools/cli/lib/yaml-xml-builder');
|
||||||
const { ManifestGenerator } = require('../tools/cli/installers/lib/core/manifest-generator');
|
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
|
// ANSI colors
|
||||||
const 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 <agent tag and frontmatter)
|
||||||
|
const minimalAgent = [
|
||||||
|
'---',
|
||||||
|
'name: "test agent"',
|
||||||
|
'description: "Minimal test agent fixture"',
|
||||||
|
'---',
|
||||||
|
'',
|
||||||
|
'You are a test agent.',
|
||||||
|
'',
|
||||||
|
'<agent id="test-agent.agent.yaml" name="Test Agent" title="Test Agent">',
|
||||||
|
'<persona>Test persona</persona>',
|
||||||
|
'</agent>',
|
||||||
|
].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
|
* Test Suite
|
||||||
*/
|
*/
|
||||||
|
|
@ -158,9 +194,311 @@ async function runTests() {
|
||||||
console.log('');
|
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 {
|
try {
|
||||||
const builder = new YamlXmlBuilder();
|
const builder = new YamlXmlBuilder();
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ const crypto = require('node:crypto');
|
||||||
const csv = require('csv-parse/sync');
|
const csv = require('csv-parse/sync');
|
||||||
const { getSourcePath, getModulePath } = require('../../../lib/project-root');
|
const { getSourcePath, getModulePath } = require('../../../lib/project-root');
|
||||||
const prompts = require('../../../lib/prompts');
|
const prompts = require('../../../lib/prompts');
|
||||||
|
const { loadSkillManifest: loadSkillManifestShared, getCanonicalId: getCanonicalIdShared } = require('../ide/shared/skill-manifest');
|
||||||
|
|
||||||
// Load package.json for version info
|
// Load package.json for version info
|
||||||
const packageJson = require('../../../../../package.json');
|
const packageJson = require('../../../../../package.json');
|
||||||
|
|
@ -23,6 +24,16 @@ class ManifestGenerator {
|
||||||
this.selectedIdes = [];
|
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.
|
* Clean text for CSV output by normalizing whitespace.
|
||||||
* Note: Quote escaping is handled by escapeCsv() at write time.
|
* Note: Quote escaping is handled by escapeCsv() at write time.
|
||||||
|
|
@ -150,6 +161,8 @@ class ManifestGenerator {
|
||||||
// Recursively find workflow.yaml files
|
// Recursively find workflow.yaml files
|
||||||
const findWorkflows = async (dir, relativePath = '') => {
|
const findWorkflows = async (dir, relativePath = '') => {
|
||||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
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) {
|
for (const entry of entries) {
|
||||||
const fullPath = path.join(dir, entry.name);
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
|
@ -221,6 +234,7 @@ class ManifestGenerator {
|
||||||
description: this.cleanForCSV(workflow.description),
|
description: this.cleanForCSV(workflow.description),
|
||||||
module: moduleName,
|
module: moduleName,
|
||||||
path: installPath,
|
path: installPath,
|
||||||
|
canonicalId: this.getCanonicalId(skillManifest, entry.name),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add to files list
|
// Add to files list
|
||||||
|
|
@ -294,6 +308,8 @@ class ManifestGenerator {
|
||||||
async getAgentsFromDir(dirPath, moduleName, relativePath = '') {
|
async getAgentsFromDir(dirPath, moduleName, relativePath = '') {
|
||||||
const agents = [];
|
const agents = [];
|
||||||
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
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) {
|
for (const entry of entries) {
|
||||||
const fullPath = path.join(dirPath, entry.name);
|
const fullPath = path.join(dirPath, entry.name);
|
||||||
|
|
@ -349,6 +365,7 @@ class ManifestGenerator {
|
||||||
principles: principlesMatch ? this.cleanForCSV(principlesMatch[1]) : '',
|
principles: principlesMatch ? this.cleanForCSV(principlesMatch[1]) : '',
|
||||||
module: moduleName,
|
module: moduleName,
|
||||||
path: installPath,
|
path: installPath,
|
||||||
|
canonicalId: this.getCanonicalId(skillManifest, entry.name),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add to files list
|
// Add to files list
|
||||||
|
|
@ -388,6 +405,8 @@ class ManifestGenerator {
|
||||||
async getTasksFromDir(dirPath, moduleName) {
|
async getTasksFromDir(dirPath, moduleName) {
|
||||||
const tasks = [];
|
const tasks = [];
|
||||||
const files = await fs.readdir(dirPath);
|
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) {
|
for (const file of files) {
|
||||||
// Check for both .xml and .md files
|
// Check for both .xml and .md files
|
||||||
|
|
@ -447,6 +466,7 @@ class ManifestGenerator {
|
||||||
module: moduleName,
|
module: moduleName,
|
||||||
path: installPath,
|
path: installPath,
|
||||||
standalone: standalone,
|
standalone: standalone,
|
||||||
|
canonicalId: this.getCanonicalId(skillManifest, file),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add to files list
|
// Add to files list
|
||||||
|
|
@ -486,6 +506,8 @@ class ManifestGenerator {
|
||||||
async getToolsFromDir(dirPath, moduleName) {
|
async getToolsFromDir(dirPath, moduleName) {
|
||||||
const tools = [];
|
const tools = [];
|
||||||
const files = await fs.readdir(dirPath);
|
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) {
|
for (const file of files) {
|
||||||
// Check for both .xml and .md files
|
// Check for both .xml and .md files
|
||||||
|
|
@ -545,6 +567,7 @@ class ManifestGenerator {
|
||||||
module: moduleName,
|
module: moduleName,
|
||||||
path: installPath,
|
path: installPath,
|
||||||
standalone: standalone,
|
standalone: standalone,
|
||||||
|
canonicalId: this.getCanonicalId(skillManifest, file),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add to files list
|
// Add to files list
|
||||||
|
|
@ -735,8 +758,8 @@ class ManifestGenerator {
|
||||||
const csvPath = path.join(cfgDir, 'workflow-manifest.csv');
|
const csvPath = path.join(cfgDir, 'workflow-manifest.csv');
|
||||||
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
||||||
|
|
||||||
// Create CSV header - standalone column removed, everything is canonicalized to 4 columns
|
// Create CSV header - standalone column removed, canonicalId added as optional column
|
||||||
let csv = 'name,description,module,path\n';
|
let csv = 'name,description,module,path,canonicalId\n';
|
||||||
|
|
||||||
// Build workflows map from discovered workflows only
|
// Build workflows map from discovered workflows only
|
||||||
// Old entries are NOT preserved - the manifest reflects what actually exists on disk
|
// Old entries are NOT preserved - the manifest reflects what actually exists on disk
|
||||||
|
|
@ -750,12 +773,19 @@ class ManifestGenerator {
|
||||||
description: workflow.description,
|
description: workflow.description,
|
||||||
module: workflow.module,
|
module: workflow.module,
|
||||||
path: workflow.path,
|
path: workflow.path,
|
||||||
|
canonicalId: workflow.canonicalId || '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write all workflows
|
// Write all workflows
|
||||||
for (const [, value] of allWorkflows) {
|
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';
|
csv += row + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -784,8 +814,8 @@ class ManifestGenerator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create CSV header with persona fields
|
// Create CSV header with persona fields and canonicalId
|
||||||
let csvContent = 'name,displayName,title,icon,capabilities,role,identity,communicationStyle,principles,module,path\n';
|
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
|
// Combine existing and new agents, preferring new data for duplicates
|
||||||
const allAgents = new Map();
|
const allAgents = new Map();
|
||||||
|
|
@ -810,6 +840,7 @@ class ManifestGenerator {
|
||||||
principles: agent.principles,
|
principles: agent.principles,
|
||||||
module: agent.module,
|
module: agent.module,
|
||||||
path: agent.path,
|
path: agent.path,
|
||||||
|
canonicalId: agent.canonicalId || '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -827,6 +858,7 @@ class ManifestGenerator {
|
||||||
escapeCsv(record.principles),
|
escapeCsv(record.principles),
|
||||||
escapeCsv(record.module),
|
escapeCsv(record.module),
|
||||||
escapeCsv(record.path),
|
escapeCsv(record.path),
|
||||||
|
escapeCsv(record.canonicalId),
|
||||||
].join(',');
|
].join(',');
|
||||||
csvContent += row + '\n';
|
csvContent += row + '\n';
|
||||||
}
|
}
|
||||||
|
|
@ -856,8 +888,8 @@ class ManifestGenerator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create CSV header with standalone column
|
// Create CSV header with standalone and canonicalId columns
|
||||||
let csvContent = 'name,displayName,description,module,path,standalone\n';
|
let csvContent = 'name,displayName,description,module,path,standalone,canonicalId\n';
|
||||||
|
|
||||||
// Combine existing and new tasks
|
// Combine existing and new tasks
|
||||||
const allTasks = new Map();
|
const allTasks = new Map();
|
||||||
|
|
@ -877,6 +909,7 @@ class ManifestGenerator {
|
||||||
module: task.module,
|
module: task.module,
|
||||||
path: task.path,
|
path: task.path,
|
||||||
standalone: task.standalone,
|
standalone: task.standalone,
|
||||||
|
canonicalId: task.canonicalId || '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -889,6 +922,7 @@ class ManifestGenerator {
|
||||||
escapeCsv(record.module),
|
escapeCsv(record.module),
|
||||||
escapeCsv(record.path),
|
escapeCsv(record.path),
|
||||||
escapeCsv(record.standalone),
|
escapeCsv(record.standalone),
|
||||||
|
escapeCsv(record.canonicalId),
|
||||||
].join(',');
|
].join(',');
|
||||||
csvContent += row + '\n';
|
csvContent += row + '\n';
|
||||||
}
|
}
|
||||||
|
|
@ -918,8 +952,8 @@ class ManifestGenerator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create CSV header with standalone column
|
// Create CSV header with standalone and canonicalId columns
|
||||||
let csvContent = 'name,displayName,description,module,path,standalone\n';
|
let csvContent = 'name,displayName,description,module,path,standalone,canonicalId\n';
|
||||||
|
|
||||||
// Combine existing and new tools
|
// Combine existing and new tools
|
||||||
const allTools = new Map();
|
const allTools = new Map();
|
||||||
|
|
@ -939,6 +973,7 @@ class ManifestGenerator {
|
||||||
module: tool.module,
|
module: tool.module,
|
||||||
path: tool.path,
|
path: tool.path,
|
||||||
standalone: tool.standalone,
|
standalone: tool.standalone,
|
||||||
|
canonicalId: tool.canonicalId || '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -951,6 +986,7 @@ class ManifestGenerator {
|
||||||
escapeCsv(record.module),
|
escapeCsv(record.module),
|
||||||
escapeCsv(record.path),
|
escapeCsv(record.path),
|
||||||
escapeCsv(record.standalone),
|
escapeCsv(record.standalone),
|
||||||
|
escapeCsv(record.canonicalId),
|
||||||
].join(',');
|
].join(',');
|
||||||
csvContent += row + '\n';
|
csvContent += row + '\n';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
const os = require('node:os');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
|
const yaml = require('yaml');
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
const { BaseIdeSetup } = require('./_base-ide');
|
||||||
const prompts = require('../../../lib/prompts');
|
const prompts = require('../../../lib/prompts');
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||||
|
|
@ -24,6 +26,34 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
super(platformCode, platformConfig.name, platformConfig.preferred);
|
super(platformCode, platformConfig.name, platformConfig.preferred);
|
||||||
this.platformConfig = platformConfig;
|
this.platformConfig = platformConfig;
|
||||||
this.installerConfig = platformConfig.installer || null;
|
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<boolean>}
|
||||||
|
*/
|
||||||
|
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);
|
const conflict = await this.findAncestorConflict(projectDir);
|
||||||
if (conflict) {
|
if (conflict) {
|
||||||
await prompts.log.error(
|
await prompts.log.error(
|
||||||
`Found existing BMAD commands in ancestor installation: ${conflict}\n` +
|
`Found existing BMAD skills in ancestor installation: ${conflict}\n` +
|
||||||
` ${this.name} inherits commands from parent directories, so this would cause duplicates.\n` +
|
` ${this.name} inherits skills from parent directories, so this would cause duplicates.\n` +
|
||||||
` Please remove the BMAD files from that directory first:\n` +
|
` Please remove the BMAD files from that directory first:\n` +
|
||||||
` rm -rf "${conflict}"/bmad*`,
|
` rm -rf "${conflict}"/bmad*`,
|
||||||
);
|
);
|
||||||
|
|
@ -165,8 +195,13 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
for (const artifact of artifacts) {
|
for (const artifact of artifacts) {
|
||||||
const content = this.renderTemplate(template, artifact);
|
const content = this.renderTemplate(template, artifact);
|
||||||
const filename = this.generateFilename(artifact, 'agent', extension);
|
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++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,8 +233,13 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
const { content: template, extension } = await this.loadTemplate(workflowTemplateType, '', config, finalTemplateType);
|
const { content: template, extension } = await this.loadTemplate(workflowTemplateType, '', config, finalTemplateType);
|
||||||
const content = this.renderTemplate(template, artifact);
|
const content = this.renderTemplate(template, artifact);
|
||||||
const filename = this.generateFilename(artifact, 'workflow', extension);
|
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++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -241,8 +281,13 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
|
|
||||||
const content = this.renderTemplate(template, artifact);
|
const content = this.renderTemplate(template, artifact);
|
||||||
const filename = this.generateFilename(artifact, artifact.type, extension);
|
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') {
|
if (artifact.type === 'task') {
|
||||||
taskCount++;
|
taskCount++;
|
||||||
|
|
@ -409,22 +454,146 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
||||||
// No default
|
// 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('{{name}}', artifact.name || '')
|
||||||
.replaceAll('{{module}}', artifact.module || 'core')
|
.replaceAll('{{module}}', artifact.module || 'core')
|
||||||
.replaceAll('{{path}}', pathToUse)
|
.replaceAll('{{path}}', pathToUse)
|
||||||
.replaceAll('{{description}}', artifact.description || `${artifact.name} ${artifact.type || ''}`)
|
.replaceAll('{{description}}', artifact.description || `${artifact.name} ${artifact.type || ''}`)
|
||||||
.replaceAll('{{workflow_path}}', pathToUse);
|
.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;
|
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 <skillDir>/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
|
* Generate filename for artifact
|
||||||
* @param {Object} artifact - Artifact data
|
* @param {Object} artifact - Artifact data
|
||||||
|
|
@ -433,10 +602,11 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
||||||
* @returns {string} Generated filename
|
* @returns {string} Generated filename
|
||||||
*/
|
*/
|
||||||
generateFilename(artifact, artifactType, extension = '.md') {
|
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
|
// 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)
|
// Clean up potential double extensions from source files (e.g. .yaml.md, .xml.md -> .md)
|
||||||
// This handles any extensions that might slip through toDashPath()
|
// 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 (this.installerConfig?.legacy_targets) {
|
||||||
if (!options.silent) await prompts.log.message(' Migrating legacy directories...');
|
if (!options.silent) await prompts.log.message(' Migrating legacy directories...');
|
||||||
for (const legacyDir of this.installerConfig.legacy_targets) {
|
for (const legacyDir of this.installerConfig.legacy_targets) {
|
||||||
await this.cleanupTarget(projectDir, legacyDir, options);
|
if (this.isGlobalPath(legacyDir)) {
|
||||||
await this.removeEmptyParents(projectDir, 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
|
* Cleanup a specific target directory
|
||||||
* @param {string} projectDir - Project directory
|
* @param {string} projectDir - Project directory
|
||||||
|
|
|
||||||
|
|
@ -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 || '<project>/.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<agent-activation CRITICAL="TRUE">\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 <activation> 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' +
|
|
||||||
'</agent-activation>\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 };
|
|
||||||
|
|
@ -8,7 +8,7 @@ const prompts = require('../../../lib/prompts');
|
||||||
* Dynamically discovers and loads IDE handlers
|
* Dynamically discovers and loads IDE handlers
|
||||||
*
|
*
|
||||||
* Loading strategy:
|
* 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
|
* 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns
|
||||||
*/
|
*/
|
||||||
class IdeManager {
|
class IdeManager {
|
||||||
|
|
@ -44,7 +44,7 @@ class IdeManager {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dynamically load all IDE handlers
|
* 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
|
* 2. Load config-driven handlers from platform-codes.yaml
|
||||||
*/
|
*/
|
||||||
async loadHandlers() {
|
async loadHandlers() {
|
||||||
|
|
@ -58,10 +58,11 @@ class IdeManager {
|
||||||
/**
|
/**
|
||||||
* Load custom installer files (unique installation logic)
|
* Load custom installer files (unique installation logic)
|
||||||
* These files have special installation patterns that don't fit the config-driven model
|
* 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() {
|
async loadCustomInstallerFiles() {
|
||||||
const ideDir = __dirname;
|
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) {
|
for (const file of customFiles) {
|
||||||
const filePath = path.join(ideDir, file);
|
const filePath = path.join(ideDir, file);
|
||||||
|
|
@ -189,14 +190,6 @@ class IdeManager {
|
||||||
if (r.tasks > 0) parts.push(`${r.tasks} tasks`);
|
if (r.tasks > 0) parts.push(`${r.tasks} tasks`);
|
||||||
if (r.tools > 0) parts.push(`${r.tools} tools`);
|
if (r.tools > 0) parts.push(`${r.tools} tools`);
|
||||||
detail = parts.join(', ');
|
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) {
|
} else if (handlerResult && handlerResult.modes !== undefined) {
|
||||||
// Kilo handler returns { success, modes, workflows, tasks, tools }
|
// Kilo handler returns { success, modes, workflows, tasks, tools }
|
||||||
const parts = [];
|
const parts = [];
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,11 @@ platforms:
|
||||||
category: ide
|
category: ide
|
||||||
description: "Google's AI development environment"
|
description: "Google's AI development environment"
|
||||||
installer:
|
installer:
|
||||||
target_dir: .agent/workflows
|
legacy_targets:
|
||||||
|
- .agent/workflows
|
||||||
|
target_dir: .agent/skills
|
||||||
template_type: antigravity
|
template_type: antigravity
|
||||||
|
skill_format: true
|
||||||
|
|
||||||
auggie:
|
auggie:
|
||||||
name: "Auggie"
|
name: "Auggie"
|
||||||
|
|
@ -29,8 +32,11 @@ platforms:
|
||||||
category: cli
|
category: cli
|
||||||
description: "AI development tool"
|
description: "AI development tool"
|
||||||
installer:
|
installer:
|
||||||
target_dir: .augment/commands
|
legacy_targets:
|
||||||
|
- .augment/commands
|
||||||
|
target_dir: .augment/skills
|
||||||
template_type: default
|
template_type: default
|
||||||
|
skill_format: true
|
||||||
|
|
||||||
claude-code:
|
claude-code:
|
||||||
name: "Claude Code"
|
name: "Claude Code"
|
||||||
|
|
@ -38,8 +44,11 @@ platforms:
|
||||||
category: cli
|
category: cli
|
||||||
description: "Anthropic's official CLI for Claude"
|
description: "Anthropic's official CLI for Claude"
|
||||||
installer:
|
installer:
|
||||||
target_dir: .claude/commands
|
legacy_targets:
|
||||||
|
- .claude/commands
|
||||||
|
target_dir: .claude/skills
|
||||||
template_type: default
|
template_type: default
|
||||||
|
skill_format: true
|
||||||
ancestor_conflict_check: true
|
ancestor_conflict_check: true
|
||||||
|
|
||||||
cline:
|
cline:
|
||||||
|
|
@ -56,7 +65,15 @@ platforms:
|
||||||
preferred: false
|
preferred: false
|
||||||
category: cli
|
category: cli
|
||||||
description: "OpenAI Codex integration"
|
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:
|
codebuddy:
|
||||||
name: "CodeBuddy"
|
name: "CodeBuddy"
|
||||||
|
|
@ -82,8 +99,11 @@ platforms:
|
||||||
category: ide
|
category: ide
|
||||||
description: "AI-first code editor"
|
description: "AI-first code editor"
|
||||||
installer:
|
installer:
|
||||||
target_dir: .cursor/commands
|
legacy_targets:
|
||||||
|
- .cursor/commands
|
||||||
|
target_dir: .cursor/skills
|
||||||
template_type: default
|
template_type: default
|
||||||
|
skill_format: true
|
||||||
|
|
||||||
gemini:
|
gemini:
|
||||||
name: "Gemini CLI"
|
name: "Gemini CLI"
|
||||||
|
|
@ -123,8 +143,11 @@ platforms:
|
||||||
category: ide
|
category: ide
|
||||||
description: "Amazon's AI-powered IDE"
|
description: "Amazon's AI-powered IDE"
|
||||||
installer:
|
installer:
|
||||||
target_dir: .kiro/steering
|
legacy_targets:
|
||||||
|
- .kiro/steering
|
||||||
|
target_dir: .kiro/skills
|
||||||
template_type: kiro
|
template_type: kiro
|
||||||
|
skill_format: true
|
||||||
|
|
||||||
opencode:
|
opencode:
|
||||||
name: "OpenCode"
|
name: "OpenCode"
|
||||||
|
|
@ -133,15 +156,14 @@ platforms:
|
||||||
description: "OpenCode terminal coding assistant"
|
description: "OpenCode terminal coding assistant"
|
||||||
installer:
|
installer:
|
||||||
legacy_targets:
|
legacy_targets:
|
||||||
|
- .opencode/agents
|
||||||
|
- .opencode/commands
|
||||||
- .opencode/agent
|
- .opencode/agent
|
||||||
- .opencode/command
|
- .opencode/command
|
||||||
targets:
|
target_dir: .opencode/skills
|
||||||
- target_dir: .opencode/agents
|
template_type: opencode
|
||||||
template_type: opencode
|
skill_format: true
|
||||||
artifact_types: [agents]
|
ancestor_conflict_check: true
|
||||||
- target_dir: .opencode/commands
|
|
||||||
template_type: opencode
|
|
||||||
artifact_types: [workflows, tasks, tools]
|
|
||||||
|
|
||||||
qwen:
|
qwen:
|
||||||
name: "QwenCoder"
|
name: "QwenCoder"
|
||||||
|
|
@ -183,8 +205,11 @@ platforms:
|
||||||
category: ide
|
category: ide
|
||||||
description: "AI-powered IDE with cascade flows"
|
description: "AI-powered IDE with cascade flows"
|
||||||
installer:
|
installer:
|
||||||
target_dir: .windsurf/workflows
|
legacy_targets:
|
||||||
|
- .windsurf/workflows
|
||||||
|
target_dir: .windsurf/skills
|
||||||
template_type: windsurf
|
template_type: windsurf
|
||||||
|
skill_format: true
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Installer Config Schema
|
# Installer Config Schema
|
||||||
|
|
@ -203,9 +228,11 @@ platforms:
|
||||||
# artifact_types: [agents, workflows, tasks, tools]
|
# artifact_types: [agents, workflows, tasks, tools]
|
||||||
# artifact_types: array (optional) # Filter which artifacts to install (default: all)
|
# artifact_types: array (optional) # Filter which artifacts to install (default: all)
|
||||||
# skip_existing: boolean (optional) # Skip files that already exist (default: false)
|
# skip_existing: boolean (optional) # Skip files that already exist (default: false)
|
||||||
|
# skill_format: boolean (optional) # Use directory-per-skill output: <name>/SKILL.md
|
||||||
|
# # with clean frontmatter (name + description, unquoted)
|
||||||
# ancestor_conflict_check: boolean (optional) # Refuse install when ancestor dir has BMAD files
|
# ancestor_conflict_check: boolean (optional) # Refuse install when ancestor dir has BMAD files
|
||||||
# # in the same target_dir (for IDEs that inherit
|
# # in the same target_dir (for IDEs that inherit
|
||||||
# # commands from parent directories)
|
# # skills from parent directories)
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Platform Categories
|
# Platform Categories
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ class AgentCommandGenerator {
|
||||||
name: agent.name,
|
name: agent.name,
|
||||||
description: agent.description || `${agent.name} agent`,
|
description: agent.description || `${agent.name} agent`,
|
||||||
module: agent.module,
|
module: agent.module,
|
||||||
|
canonicalId: agent.canonicalId || '',
|
||||||
relativePath: path.join(agent.module, 'agents', agentPathInModule), // For command filename
|
relativePath: path.join(agent.module, 'agents', agentPathInModule), // For command filename
|
||||||
agentPath: agentRelPath, // Relative path to actual agent file
|
agentPath: agentRelPath, // Relative path to actual agent file
|
||||||
content: launcherContent,
|
content: launcherContent,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
|
const { loadSkillManifest, getCanonicalId } = require('./skill-manifest');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helpers for gathering BMAD agents/tasks from the installed tree.
|
* 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 agentDirPath = path.join(standaloneAgentsDir, agentDir.name);
|
||||||
const agentFiles = await fs.readdir(agentDirPath);
|
const agentFiles = await fs.readdir(agentDirPath);
|
||||||
|
const skillManifest = await loadSkillManifest(agentDirPath);
|
||||||
|
|
||||||
for (const file of agentFiles) {
|
for (const file of agentFiles) {
|
||||||
if (!file.endsWith('.md')) continue;
|
if (!file.endsWith('.md')) continue;
|
||||||
|
|
@ -48,6 +50,7 @@ async function getAgentsFromBmad(bmadDir, selectedModules = []) {
|
||||||
path: filePath,
|
path: filePath,
|
||||||
name: file.replace('.md', ''),
|
name: file.replace('.md', ''),
|
||||||
module: 'standalone', // Mark as standalone agent
|
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 entries = await fs.readdir(dirPath, { withFileTypes: true });
|
||||||
|
const skillManifest = await loadSkillManifest(dirPath);
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
// Skip if entry.name is undefined or not a string
|
// 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', ''),
|
name: entry.name.replace('.md', ''),
|
||||||
module: moduleName,
|
module: moduleName,
|
||||||
relativePath: newRelativePath, // Keep the .md extension for the full path
|
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 files = await fs.readdir(dirPath);
|
||||||
|
const skillManifest = await loadSkillManifest(dirPath);
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
// Include both .md and .xml task files
|
// Include both .md and .xml task files
|
||||||
|
|
@ -160,6 +166,7 @@ async function getTasksFromDir(dirPath, moduleName) {
|
||||||
path: filePath,
|
path: filePath,
|
||||||
name: file.replace(ext, ''),
|
name: file.replace(ext, ''),
|
||||||
module: moduleName,
|
module: moduleName,
|
||||||
|
canonicalId: getCanonicalId(skillManifest, file),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
// Backward compatibility aliases (colon format was same as underscore)
|
||||||
const toColonName = toUnderscoreName;
|
const toColonName = toUnderscoreName;
|
||||||
const toColonPath = toUnderscorePath;
|
const toColonPath = toUnderscorePath;
|
||||||
|
|
@ -275,6 +290,7 @@ module.exports = {
|
||||||
// New standard (dash-based)
|
// New standard (dash-based)
|
||||||
toDashName,
|
toDashName,
|
||||||
toDashPath,
|
toDashPath,
|
||||||
|
resolveSkillName,
|
||||||
customAgentDashName,
|
customAgentDashName,
|
||||||
isDashFormat,
|
isDashFormat,
|
||||||
parseDashName,
|
parseDashName,
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -50,6 +50,7 @@ class TaskToolCommandGenerator {
|
||||||
displayName: task.displayName || task.name,
|
displayName: task.displayName || task.name,
|
||||||
description: task.description || `Execute ${task.displayName || task.name}`,
|
description: task.description || `Execute ${task.displayName || task.name}`,
|
||||||
module: task.module,
|
module: task.module,
|
||||||
|
canonicalId: task.canonicalId || '',
|
||||||
// Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows)
|
// Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows)
|
||||||
relativePath: `${task.module}/tasks/${task.name}${taskExt}`,
|
relativePath: `${task.module}/tasks/${task.name}${taskExt}`,
|
||||||
path: taskPath,
|
path: taskPath,
|
||||||
|
|
@ -75,6 +76,7 @@ class TaskToolCommandGenerator {
|
||||||
displayName: tool.displayName || tool.name,
|
displayName: tool.displayName || tool.name,
|
||||||
description: tool.description || `Execute ${tool.displayName || tool.name}`,
|
description: tool.description || `Execute ${tool.displayName || tool.name}`,
|
||||||
module: tool.module,
|
module: tool.module,
|
||||||
|
canonicalId: tool.canonicalId || '',
|
||||||
// Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows)
|
// Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows)
|
||||||
relativePath: `${tool.module}/tools/${tool.name}${toolExt}`,
|
relativePath: `${tool.module}/tools/${tool.name}${toolExt}`,
|
||||||
path: toolPath,
|
path: toolPath,
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ class WorkflowCommandGenerator {
|
||||||
name: workflow.name,
|
name: workflow.name,
|
||||||
description: workflow.description || `${workflow.name} workflow`,
|
description: workflow.description || `${workflow.name} workflow`,
|
||||||
module: workflow.module,
|
module: workflow.module,
|
||||||
|
canonicalId: workflow.canonicalId || '',
|
||||||
relativePath: path.join(workflow.module, 'workflows', `${workflow.name}.md`),
|
relativePath: path.join(workflow.module, 'workflows', `${workflow.name}.md`),
|
||||||
workflowPath: workflowRelPath, // Relative path to actual workflow file
|
workflowPath: workflowRelPath, // Relative path to actual workflow file
|
||||||
content: commandContent,
|
content: commandContent,
|
||||||
|
|
|
||||||
|
|
@ -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/<skill-folder>/` and global skills at `~/.gemini/antigravity/skills/<skill-folder>/`. 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-name>/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
|
||||||
Loading…
Reference in New Issue