From 6da9e55ce5b30a1d05a8d51eade6666b114a23dd Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Sat, 7 Mar 2026 19:17:21 -0700 Subject: [PATCH] feat(skills): add type:skill manifest for verbatim skill directory copying Introduce `type: skill` in bmad-skill-manifest.yaml to signal the installer to copy entire skill directories verbatim into IDE skill directories, replacing the launcher-based approach. Changes: - skill-manifest.js: fix single-entry detection for type-only manifests, add getArtifactType export - manifest-generator.js: collect type:skill entries separately, write skill-manifest.csv, derive canonicalId from directory name - _config-driven.js: add installVerbatimSkills with YAML-safe SKILL.md generation, stale file cleanup, and warning on parse failures - Rename quick-dev-new-preview to bmad-quick-dev-new-preview so directory name is the canonical ID - Update workflow.md installed_path to reference IDE skill base directory Co-Authored-By: Claude Opus 4.6 --- .../bmad-skill-manifest.yaml | 1 + .../steps/step-01-clarify-and-route.md | 0 .../steps/step-02-plan.md | 0 .../steps/step-03-implement.md | 0 .../steps/step-04-review.md | 0 .../steps/step-05-present.md | 0 .../tech-spec-template.md | 0 .../workflow.md | 2 +- .../bmad-skill-manifest.yaml | 3 - .../installers/lib/core/manifest-generator.js | 56 +++++++++++- .../cli/installers/lib/ide/_config-driven.js | 90 ++++++++++++++++++- .../lib/ide/shared/skill-manifest.js | 25 +++++- 12 files changed, 167 insertions(+), 10 deletions(-) create mode 100644 src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/bmad-skill-manifest.yaml rename src/bmm/workflows/bmad-quick-flow/{quick-dev-new-preview => bmad-quick-dev-new-preview}/steps/step-01-clarify-and-route.md (100%) rename src/bmm/workflows/bmad-quick-flow/{quick-dev-new-preview => bmad-quick-dev-new-preview}/steps/step-02-plan.md (100%) rename src/bmm/workflows/bmad-quick-flow/{quick-dev-new-preview => bmad-quick-dev-new-preview}/steps/step-03-implement.md (100%) rename src/bmm/workflows/bmad-quick-flow/{quick-dev-new-preview => bmad-quick-dev-new-preview}/steps/step-04-review.md (100%) rename src/bmm/workflows/bmad-quick-flow/{quick-dev-new-preview => bmad-quick-dev-new-preview}/steps/step-05-present.md (100%) rename src/bmm/workflows/bmad-quick-flow/{quick-dev-new-preview => bmad-quick-dev-new-preview}/tech-spec-template.md (100%) rename src/bmm/workflows/bmad-quick-flow/{quick-dev-new-preview => bmad-quick-dev-new-preview}/workflow.md (97%) delete mode 100644 src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/bmad-skill-manifest.yaml diff --git a/src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/bmad-skill-manifest.yaml b/src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/bmad-skill-manifest.yaml new file mode 100644 index 000000000..d0f08abdb --- /dev/null +++ b/src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/bmad-skill-manifest.yaml @@ -0,0 +1 @@ +type: skill diff --git a/src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/steps/step-01-clarify-and-route.md b/src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/steps/step-01-clarify-and-route.md similarity index 100% rename from src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/steps/step-01-clarify-and-route.md rename to src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/steps/step-01-clarify-and-route.md diff --git a/src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/steps/step-02-plan.md b/src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/steps/step-02-plan.md similarity index 100% rename from src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/steps/step-02-plan.md rename to src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/steps/step-02-plan.md diff --git a/src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/steps/step-03-implement.md b/src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/steps/step-03-implement.md similarity index 100% rename from src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/steps/step-03-implement.md rename to src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/steps/step-03-implement.md diff --git a/src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/steps/step-04-review.md b/src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/steps/step-04-review.md similarity index 100% rename from src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/steps/step-04-review.md rename to src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/steps/step-04-review.md diff --git a/src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/steps/step-05-present.md b/src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/steps/step-05-present.md similarity index 100% rename from src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/steps/step-05-present.md rename to src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/steps/step-05-present.md diff --git a/src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/tech-spec-template.md b/src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/tech-spec-template.md similarity index 100% rename from src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/tech-spec-template.md rename to src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/tech-spec-template.md diff --git a/src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/workflow.md b/src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/workflow.md similarity index 97% rename from src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/workflow.md rename to src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/workflow.md index 08733ea47..9c384e52a 100644 --- a/src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/workflow.md +++ b/src/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/workflow.md @@ -81,7 +81,7 @@ YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config ` ### 2. Paths -- `installed_path` = `{project-root}/_bmad/bmm/workflows/bmad-quick-flow/quick-dev-new-preview` +- `installed_path` = the directory containing this workflow.md file (the skill's base directory) - `templateFile` = `{installed_path}/tech-spec-template.md` - `wipFile` = `{implementation_artifacts}/tech-spec-wip.md` diff --git a/src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/bmad-skill-manifest.yaml b/src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/bmad-skill-manifest.yaml deleted file mode 100644 index 913c63629..000000000 --- a/src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/bmad-skill-manifest.yaml +++ /dev/null @@ -1,3 +0,0 @@ -canonicalId: bmad-quick-dev-new-preview -type: workflow -description: "Unified quick flow - clarify intent, plan, implement, review, present" diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index 0955a3d6f..f855bcabf 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -5,7 +5,11 @@ const crypto = require('node:crypto'); const csv = require('csv-parse/sync'); const { getSourcePath, getModulePath } = require('../../../lib/project-root'); const prompts = require('../../../lib/prompts'); -const { loadSkillManifest: loadSkillManifestShared, getCanonicalId: getCanonicalIdShared } = require('../ide/shared/skill-manifest'); +const { + loadSkillManifest: loadSkillManifestShared, + getCanonicalId: getCanonicalIdShared, + getArtifactType: getArtifactTypeShared, +} = require('../ide/shared/skill-manifest'); // Load package.json for version info const packageJson = require('../../../../../package.json'); @@ -16,6 +20,7 @@ const packageJson = require('../../../../../package.json'); class ManifestGenerator { constructor() { this.workflows = []; + this.skills = []; this.agents = []; this.tasks = []; this.tools = []; @@ -34,6 +39,11 @@ class ManifestGenerator { return getCanonicalIdShared(manifest, filename); } + /** Delegate to shared skill-manifest module */ + getArtifactType(manifest, filename) { + return getArtifactTypeShared(manifest, filename); + } + /** * Clean text for CSV output by normalizing whitespace. * Note: Quote escaping is handled by escapeCsv() at write time. @@ -105,6 +115,7 @@ class ManifestGenerator { const manifestFiles = [ await this.writeMainManifest(cfgDir), await this.writeWorkflowManifest(cfgDir), + await this.writeSkillManifest(cfgDir), await this.writeAgentManifest(cfgDir), await this.writeTaskManifest(cfgDir), await this.writeToolManifest(cfgDir), @@ -228,6 +239,24 @@ class ManifestGenerator { ? `${this.bmadFolderName}/core/workflows/${relativePath}/${entry.name}` : `${this.bmadFolderName}/${moduleName}/workflows/${relativePath}/${entry.name}`; + // Check if this is a type:skill entry — collect separately, skip workflow CSV + const artifactType = this.getArtifactType(skillManifest, entry.name); + if (artifactType === 'skill') { + const canonicalId = path.basename(dir); + this.skills.push({ + name: workflow.name, + description: this.cleanForCSV(workflow.description), + module: moduleName, + path: installPath, + canonicalId, + }); + + if (debug) { + console.log(`[DEBUG] ✓ Added skill (skipped workflow CSV): ${workflow.name} as ${canonicalId}`); + } + continue; + } + // Workflows with standalone: false are filtered out above workflows.push({ name: workflow.name, @@ -793,6 +822,31 @@ class ManifestGenerator { return csvPath; } + /** + * Write skill manifest CSV + * @returns {string} Path to the manifest file + */ + async writeSkillManifest(cfgDir) { + const csvPath = path.join(cfgDir, 'skill-manifest.csv'); + const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`; + + let csvContent = 'canonicalId,name,description,module,path\n'; + + for (const skill of this.skills) { + const row = [ + escapeCsv(skill.canonicalId), + escapeCsv(skill.name), + escapeCsv(skill.description), + escapeCsv(skill.module), + escapeCsv(skill.path), + ].join(','); + csvContent += row + '\n'; + } + + await fs.writeFile(csvPath, csvContent); + return csvPath; + } + /** * Write agent manifest CSV * @returns {string} Path to the manifest file diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index 0a311a68d..599cc0f67 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -7,6 +7,7 @@ const prompts = require('../../../lib/prompts'); const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); +const csv = require('csv-parse/sync'); /** * Config-driven IDE setup handler @@ -119,14 +120,14 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { // Skip targets with explicitly empty artifact_types array // This prevents creating empty directories when no artifacts will be written if (Array.isArray(artifact_types) && artifact_types.length === 0) { - return { success: true, results: { agents: 0, workflows: 0, tasks: 0, tools: 0 } }; + return { success: true, results: { agents: 0, workflows: 0, tasks: 0, tools: 0, skills: 0 } }; } const targetPath = path.join(projectDir, target_dir); await this.ensureDir(targetPath); const selectedModules = options.selectedModules || []; - const results = { agents: 0, workflows: 0, tasks: 0, tools: 0 }; + const results = { agents: 0, workflows: 0, tasks: 0, tools: 0, skills: 0 }; // Install agents if (!artifact_types || artifact_types.includes('agents')) { @@ -151,6 +152,11 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { results.tools = taskToolResult.tools || 0; } + // Install verbatim skills (type: skill) + if (config.skill_format) { + results.skills = await this.installVerbatimSkills(projectDir, bmadDir, targetPath, config); + } + await this.printSummary(results, target_dir, options); return { success: true, results }; } @@ -164,7 +170,7 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { * @returns {Promise} Installation result */ async installToMultipleTargets(projectDir, bmadDir, targets, options) { - const allResults = { agents: 0, workflows: 0, tasks: 0, tools: 0 }; + const allResults = { agents: 0, workflows: 0, tasks: 0, tools: 0, skills: 0 }; for (const target of targets) { const result = await this.installToTarget(projectDir, bmadDir, target, options); @@ -173,6 +179,7 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { allResults.workflows += result.results.workflows || 0; allResults.tasks += result.results.tasks || 0; allResults.tools += result.results.tools || 0; + allResults.skills += result.results.skills || 0; } } @@ -622,6 +629,82 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} return baseName.replace(/\.md$/, extension); } + /** + * Install verbatim skill directories (type: skill entries from skill-manifest.csv). + * Copies the entire source directory into the IDE skill directory, auto-generating SKILL.md. + * @param {string} projectDir - Project directory + * @param {string} bmadDir - BMAD installation directory + * @param {string} targetPath - Target skills directory + * @param {Object} config - Installation configuration + * @returns {Promise} Count of skills installed + */ + async installVerbatimSkills(projectDir, bmadDir, targetPath, config) { + const bmadFolderName = path.basename(bmadDir); + const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv'); + + if (!(await fs.pathExists(csvPath))) return 0; + + const csvContent = await fs.readFile(csvPath, 'utf8'); + const records = csv.parse(csvContent, { + columns: true, + skip_empty_lines: true, + }); + + let count = 0; + + for (const record of records) { + const canonicalId = record.canonicalId; + if (!canonicalId) continue; + + // Derive source directory from path column + // path is like "_bmad/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/workflow.md" + // Strip bmadFolderName prefix and join with bmadDir, then get dirname + const relativePath = record.path.replace(new RegExp(`^${bmadFolderName}/`), ''); + const sourceFile = path.join(bmadDir, relativePath); + const sourceDir = path.dirname(sourceFile); + + if (!(await fs.pathExists(sourceDir))) continue; + + // Clean target before copy to prevent stale files + const skillDir = path.join(targetPath, canonicalId); + await fs.remove(skillDir); + await fs.ensureDir(skillDir); + + // Parse workflow.md frontmatter for description + let description = `${canonicalId} skill`; + try { + const workflowContent = await fs.readFile(sourceFile, 'utf8'); + const fmMatch = workflowContent.match(/^---\r?\n([\s\S]*?)\r?\n---/); + if (fmMatch) { + const frontmatter = yaml.parse(fmMatch[1]); + if (frontmatter?.description) { + description = frontmatter.description; + } + } + } catch (error) { + await prompts.log.warn(`Failed to parse frontmatter from ${sourceFile}: ${error.message}`); + } + + // Generate SKILL.md with YAML-safe frontmatter + const frontmatterYaml = yaml.stringify({ name: canonicalId, description: String(description) }, { lineWidth: 0 }).trimEnd(); + const skillMd = `---\n${frontmatterYaml}\n---\n\nIT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL workflow.md, READ its entire contents and follow its directions exactly!\n`; + await fs.writeFile(path.join(skillDir, 'SKILL.md'), skillMd); + + // Copy all files except bmad-skill-manifest.yaml + const entries = await fs.readdir(sourceDir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.name === 'bmad-skill-manifest.yaml') continue; + const srcPath = path.join(sourceDir, entry.name); + const destPath = path.join(skillDir, entry.name); + await fs.copy(srcPath, destPath); + } + + count++; + } + + return count; + } + /** * Print installation summary * @param {Object} results - Installation results @@ -634,6 +717,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} if (results.workflows > 0) parts.push(`${results.workflows} workflows`); if (results.tasks > 0) parts.push(`${results.tasks} tasks`); if (results.tools > 0) parts.push(`${results.tools} tools`); + if (results.skills > 0) parts.push(`${results.skills} skills`); await prompts.log.success(`${this.name} configured: ${parts.join(', ')} → ${targetDir}`); } diff --git a/tools/cli/installers/lib/ide/shared/skill-manifest.js b/tools/cli/installers/lib/ide/shared/skill-manifest.js index ff940242f..26e15b2bd 100644 --- a/tools/cli/installers/lib/ide/shared/skill-manifest.js +++ b/tools/cli/installers/lib/ide/shared/skill-manifest.js @@ -16,7 +16,7 @@ async function loadSkillManifest(dirPath) { 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 }; + if (parsed.canonicalId || parsed.type) return { __single: parsed }; return parsed; } catch (error) { console.warn(`Warning: Failed to parse bmad-skill-manifest.yaml in ${dirPath}: ${error.message}`); @@ -45,4 +45,25 @@ function getCanonicalId(manifest, filename) { return ''; } -module.exports = { loadSkillManifest, getCanonicalId }; +/** + * Get the artifact type 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 + * @returns {string|null} type or null + */ +function getArtifactType(manifest, filename) { + if (!manifest) return null; + // Single-entry manifest applies to all files in the directory + if (manifest.__single) return manifest.__single.type || null; + // Multi-entry: look up by filename directly + if (manifest[filename]) return manifest[filename].type || null; + // 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].type || null; + const xmlKey = `${baseName}.xml`; + if (manifest[xmlKey]) return manifest[xmlKey].type || null; + return null; +} + +module.exports = { loadSkillManifest, getCanonicalId, getArtifactType };