diff --git a/src/core/tasks/bmad-shard-doc-skill-prototype/SKILL.md b/src/core/tasks/bmad-shard-doc-skill-prototype/SKILL.md new file mode 100644 index 000000000..afa37c831 --- /dev/null +++ b/src/core/tasks/bmad-shard-doc-skill-prototype/SKILL.md @@ -0,0 +1,12 @@ +--- +name: bmad-shard-doc-skill-prototype +description: Prototype native skill wrapper for shard-doc during transition. +--- + +# bmad-shard-doc-skill-prototype + +Prototype marker: source-authored-skill + +Read and execute from: {project-root}/_bmad/core/tasks/shard-doc.xml + +Follow all shard-doc task instructions exactly as written. diff --git a/src/core/tasks/bmad-shard-doc-skill-prototype/bmad-skill-manifest.yaml b/src/core/tasks/bmad-shard-doc-skill-prototype/bmad-skill-manifest.yaml new file mode 100644 index 000000000..29ce5aeb6 --- /dev/null +++ b/src/core/tasks/bmad-shard-doc-skill-prototype/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-shard-doc-skill-prototype +type: task +description: "Prototype native skill wrapper for shard-doc during installer transition" diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 1654bc110..d783dd837 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -17,6 +17,7 @@ const fs = require('fs-extra'); const { YamlXmlBuilder } = require('../tools/cli/lib/yaml-xml-builder'); const { ManifestGenerator } = require('../tools/cli/installers/lib/core/manifest-generator'); const { IdeManager } = require('../tools/cli/installers/lib/ide/manager'); +const { ConfigDrivenIdeSetup } = require('../tools/cli/installers/lib/ide/_config-driven'); const { clearCache, loadPlatformCodes } = require('../tools/cli/installers/lib/ide/platform-codes'); // ANSI colors @@ -81,6 +82,70 @@ async function createTestBmadFixture() { return fixtureDir; } +async function createShardDocPrototypeFixture() { + const fixtureDir = await createTestBmadFixture(); + + await fs.ensureDir(path.join(fixtureDir, 'core', 'tasks')); + await fs.writeFile( + path.join(fixtureDir, 'core', 'tasks', 'shard-doc.xml'), + 'Test objective\n', + ); + + await fs.writeFile( + path.join(fixtureDir, 'core', 'tasks', 'bmad-skill-manifest.yaml'), + [ + 'shard-doc.xml:', + ' canonicalId: bmad-shard-doc', + ' type: task', + ' description: "Splits large markdown documents into smaller, organized files based on sections"', + '', + ].join('\n'), + ); + + await fs.ensureDir(path.join(fixtureDir, 'core', 'tasks', 'bmad-shard-doc-skill-prototype')); + await fs.writeFile( + path.join(fixtureDir, 'core', 'tasks', 'bmad-shard-doc-skill-prototype', 'SKILL.md'), + [ + '---', + 'name: bmad-shard-doc-skill-prototype', + 'description: Source-authored prototype skill', + '---', + '', + '# bmad-shard-doc-skill-prototype', + '', + 'Prototype marker: source-authored-skill', + '', + 'Read and execute from: {project-root}/_bmad/core/tasks/shard-doc.xml', + '', + 'Follow all shard-doc task instructions exactly as written.', + '', + ].join('\n'), + ); + await fs.writeFile( + path.join(fixtureDir, 'core', 'tasks', 'bmad-shard-doc-skill-prototype', 'bmad-skill-manifest.yaml'), + [ + 'canonicalId: bmad-shard-doc-skill-prototype', + 'type: task', + 'description: "Prototype native skill wrapper for shard-doc during installer transition"', + '', + ].join('\n'), + ); + + await fs.writeFile( + path.join(fixtureDir, '_config', 'task-manifest.csv'), + [ + 'name,displayName,description,module,path,standalone,canonicalId', + 'shard-doc,Shard Document,Test shard-doc task,core,_bmad/core/tasks/shard-doc.xml,true,bmad-shard-doc', + '', + ].join('\n'), + ); + + // Ensure tool manifest exists to avoid parser edge-cases in some environments. + await fs.writeFile(path.join(fixtureDir, '_config', 'tool-manifest.csv'), ''); + + return fixtureDir; +} + /** * Test Suite */ @@ -827,6 +892,99 @@ async function runTests() { console.log(''); // ============================================================ + // Test 11: Shard-doc Prototype Duplication (Skill/Non-Skill Scope) + // ============================================================ + console.log(`${colors.yellow}Test Suite 11: Shard-doc Prototype Duplication${colors.reset}\n`); + + let tempSkillProjectDir; + let tempNonSkillProjectDir; + let installedBmadDir; + try { + clearCache(); + const platformCodes = await loadPlatformCodes(); + const skillFormatEntry = Object.entries(platformCodes.platforms || {}).find(([_, platform]) => { + const installer = platform?.installer; + if (!installer || installer.skill_format !== true || typeof installer.target_dir !== 'string') return false; + if (Array.isArray(installer.artifact_types) && !installer.artifact_types.includes('tasks')) return false; + return true; + }); + + assert(Boolean(skillFormatEntry), 'Found a skill_format platform that installs task artifacts'); + if (!skillFormatEntry) throw new Error('No suitable skill_format platform found for shard-doc prototype test'); + + const [skillFormatPlatformCode, skillFormatPlatform] = skillFormatEntry; + const skillInstaller = skillFormatPlatform.installer; + + tempSkillProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-skill-prototype-test-')); + tempNonSkillProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-nonskill-prototype-test-')); + installedBmadDir = await createShardDocPrototypeFixture(); + + const ideManager = new IdeManager(); + await ideManager.ensureInitialized(); + + const skillResult = await ideManager.setup(skillFormatPlatformCode, tempSkillProjectDir, installedBmadDir, { + silent: true, + selectedModules: ['bmm'], + }); + + assert(skillResult.success === true, `${skillFormatPlatformCode} setup succeeds for shard-doc prototype fixture`); + + const canonicalSkillPath = path.join(tempSkillProjectDir, skillInstaller.target_dir, 'bmad-shard-doc', 'SKILL.md'); + const prototypeSkillPath = path.join(tempSkillProjectDir, skillInstaller.target_dir, 'bmad-shard-doc-skill-prototype', 'SKILL.md'); + assert(await fs.pathExists(canonicalSkillPath), `${skillFormatPlatformCode} install writes canonical shard-doc skill`); + assert(await fs.pathExists(prototypeSkillPath), `${skillFormatPlatformCode} install writes duplicated shard-doc prototype skill`); + + const canonicalSkillContent = await fs.readFile(canonicalSkillPath, 'utf8'); + const prototypeSkillContent = await fs.readFile(prototypeSkillPath, 'utf8'); + + assert(canonicalSkillContent.includes('name: bmad-shard-doc'), 'Canonical shard-doc skill keeps canonical frontmatter name'); + assert( + canonicalSkillContent.includes('Read the entire task file at: {project-root}/_bmad/core/tasks/shard-doc.xml'), + 'Canonical shard-doc skill points to shard-doc.xml', + ); + assert( + prototypeSkillContent.includes('name: bmad-shard-doc-skill-prototype'), + 'Prototype shard-doc skill uses prototype frontmatter name', + ); + assert( + prototypeSkillContent.includes('Prototype marker: source-authored-skill'), + 'Prototype shard-doc skill is copied from source SKILL.md', + ); + assert( + prototypeSkillContent.includes('Read and execute from: {project-root}/_bmad/core/tasks/shard-doc.xml'), + 'Prototype shard-doc skill preserves source-authored shard-doc.xml reference', + ); + + const nonSkillInstaller = { + ...skillInstaller, + target_dir: '.legacy/prototype-commands', + skill_format: false, + artifact_types: ['tasks'], + }; + const nonSkillHandler = new ConfigDrivenIdeSetup('prototype-nonskill-test', { + name: 'Prototype Non-Skill Test', + preferred: false, + installer: nonSkillInstaller, + }); + const nonSkillResult = await nonSkillHandler.setup(tempNonSkillProjectDir, installedBmadDir, { + silent: true, + selectedModules: ['bmm'], + }); + + assert(nonSkillResult.success === true, 'Synthetic non-skill-format setup succeeds for shard-doc prototype fixture'); + + const nonSkillTargetDir = path.join(tempNonSkillProjectDir, nonSkillInstaller.target_dir); + const nonSkillEntries = await fs.readdir(nonSkillTargetDir); + const hasCanonical = nonSkillEntries.some((entry) => /^bmad-shard-doc(\.|$)/.test(entry)); + const hasPrototype = nonSkillEntries.some((entry) => /^bmad-shard-doc-skill-prototype(\.|$)/.test(entry)); + assert(hasCanonical, 'Non-skill-format install writes canonical shard-doc artifact'); + assert(!hasPrototype, 'Non-skill-format install does not write duplicated shard-doc prototype artifact'); + } catch (error) { + assert(false, 'Shard-doc prototype duplication test succeeds', error.message); + } finally { + await Promise.allSettled([tempSkillProjectDir, tempNonSkillProjectDir, installedBmadDir].filter(Boolean).map((dir) => fs.remove(dir))); + } + // Test 17: GitHub Copilot Native Skills Install // ============================================================ console.log(`${colors.yellow}Test Suite 17: GitHub Copilot Native Skills${colors.reset}\n`); diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index 714aa752b..996e5c094 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -150,7 +150,7 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { if (!artifact_types || artifact_types.includes('tasks') || artifact_types.includes('tools')) { const taskToolGen = new TaskToolCommandGenerator(this.bmadFolderName); const { artifacts } = await taskToolGen.collectTaskToolArtifacts(bmadDir); - const taskToolResult = await this.writeTaskToolArtifacts(targetPath, artifacts, template_type, config); + const taskToolResult = await this.writeTaskToolArtifacts(targetPath, artifacts, template_type, config, bmadDir); results.tasks = taskToolResult.tasks || 0; results.tools = taskToolResult.tools || 0; } @@ -258,7 +258,7 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { * @param {Object} config - Installation configuration * @returns {Promise} Counts of tasks and tools written */ - async writeTaskToolArtifacts(targetPath, artifacts, templateType, config = {}) { + async writeTaskToolArtifacts(targetPath, artifacts, templateType, config = {}, bmadDir = null) { let taskCount = 0; let toolCount = 0; @@ -286,7 +286,7 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { const filename = this.generateFilename(artifact, artifact.type, extension); if (config.skill_format) { - await this.writeSkillFile(targetPath, artifact, content); + await this.writeSkillFile(targetPath, artifact, content, bmadDir); } else { const filePath = path.join(targetPath, filename); await this.writeFile(filePath, content); @@ -481,7 +481,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} * @param {Object} artifact - Artifact data * @param {string} content - Rendered template content */ - async writeSkillFile(targetPath, artifact, content) { + async writeSkillFile(targetPath, artifact, content, bmadDir = null) { const { resolveSkillName } = require('./shared/path-utils'); // Get the skill name (prefers canonicalId, falls back to path-derived) and remove .md @@ -500,6 +500,30 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} const skillContent = this.transformToSkillFormat(content, skillName); await this.writeFile(path.join(skillDir, 'SKILL.md'), skillContent); + + await this.writeShardDocPrototypeSkill(targetPath, bmadDir, skillName); + } + + /** + * Copy shard-doc prototype skill during transition when installing skill-format targets. + * Keeps scope literal for the first PoC without introducing generalized prototype linkage. + * @param {string} targetPath - Base skills directory + * @param {string|null} bmadDir - Installed bmad directory + * @param {string} skillName - Canonical skill name being written + */ + async writeShardDocPrototypeSkill(targetPath, bmadDir, skillName) { + if (!bmadDir || skillName !== 'bmad-shard-doc') return; + + const prototypeId = 'bmad-shard-doc-skill-prototype'; + const sourceSkillPath = path.join(bmadDir, 'core', 'tasks', prototypeId, 'SKILL.md'); + if (!(await fs.pathExists(sourceSkillPath))) return; + + const sourceSkillContent = await fs.readFile(sourceSkillPath, 'utf8'); + if (!sourceSkillContent) return; + + const prototypeDir = path.join(targetPath, prototypeId); + await this.ensureDir(prototypeDir); + await this.writeFile(path.join(prototypeDir, 'SKILL.md'), sourceSkillContent); } /**