diff --git a/src/bmm/agents/quick-flow-solo-dev.agent.yaml b/src/bmm/agents/quick-flow-solo-dev.agent.yaml index a999bb56d..553cc9d4d 100644 --- a/src/bmm/agents/quick-flow-solo-dev.agent.yaml +++ b/src/bmm/agents/quick-flow-solo-dev.agent.yaml @@ -27,8 +27,8 @@ agent: exec: "{project-root}/_bmad/bmm/workflows/bmad-quick-flow/quick-dev/workflow.md" description: "[QD] Quick-flow Develop: Implement a story tech spec end-to-end (Core of Quick Flow)" - - trigger: QQ or fuzzy match on quick-dev-new-preview - exec: "{project-root}/_bmad/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/workflow.md" + - trigger: QQ or fuzzy match on bmad-quick-dev-new-preview + exec: "{project-root}/_bmad/bmm/workflows/bmad-quick-flow/bmad-quick-dev-new-preview/workflow.md" description: "[QQ] Quick Dev New (Preview): Unified quick flow — clarify intent, plan, implement, review, present (experimental)" - trigger: CR or fuzzy match on code-review diff --git a/src/bmm/module-help.csv b/src/bmm/module-help.csv index b8378cf3c..98da248dc 100644 --- a/src/bmm/module-help.csv +++ b/src/bmm/module-help.csv @@ -3,7 +3,7 @@ bmm,anytime,Document Project,DP,,_bmad/bmm/workflows/document-project/workflow.y bmm,anytime,Generate Project Context,GPC,,_bmad/bmm/workflows/generate-project-context/workflow.md,bmad-bmm-generate-project-context,false,analyst,Create Mode,"Scan existing codebase to generate a lean LLM-optimized project-context.md containing critical implementation rules patterns and conventions for AI agents. Essential for brownfield projects and quick-flow.",output_folder,"project context", bmm,anytime,Quick Spec,QS,,_bmad/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md,bmad-bmm-quick-spec,false,quick-flow-solo-dev,Create Mode,"Do not suggest for potentially very complex things unless requested or if the user complains that they do not want to follow the extensive planning of the bmad method. Quick one-off tasks small changes simple apps brownfield additions to well established patterns utilities without extensive planning",planning_artifacts,"tech spec", bmm,anytime,Quick Dev,QD,,_bmad/bmm/workflows/bmad-quick-flow/quick-dev/workflow.md,bmad-bmm-quick-dev,false,quick-flow-solo-dev,Create Mode,"Quick one-off tasks small changes simple apps utilities without extensive planning - Do not suggest for potentially very complex things unless requested or if the user complains that they do not want to follow the extensive planning of the bmad method, unless the user is already working through the implementation phase and just requests a 1 off things not already in the plan",,, -bmm,anytime,Quick Dev New Preview,QQ,,_bmad/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/workflow.md,bmad-bmm-quick-dev-new-preview,false,quick-flow-solo-dev,Create Mode,"Unified quick flow (experimental): clarify intent plan implement review and present in a single workflow",implementation_artifacts,"tech spec implementation", +bmm,anytime,Quick Dev New Preview,QQ,,skill:bmad-quick-dev-new-preview,bmad-bmm-quick-dev-new-preview,false,quick-flow-solo-dev,Create Mode,"Unified quick flow (experimental): clarify intent plan implement review and present in a single workflow",implementation_artifacts,"tech spec implementation", bmm,anytime,Correct Course,CC,,_bmad/bmm/workflows/4-implementation/correct-course/workflow.yaml,bmad-bmm-correct-course,false,sm,Create Mode,"Anytime: Navigate significant changes. May recommend start over update PRD redo architecture sprint planning or correct epics and stories",planning_artifacts,"change proposal", bmm,anytime,Write Document,WD,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,,false,tech-writer,,"Describe in detail what you want, and the agent will follow the documentation best practices defined in agent memory. Multi-turn conversation with subprocess for research/review.",project-knowledge,"document", bmm,anytime,Update Standards,US,,_bmad/bmm/agents/tech-writer/tech-writer.agent.yaml,,false,tech-writer,,"Update agent memory documentation-standards.md with your specific preferences if you discover missing document conventions.",_bmad/_memory/tech-writer-sidecar,"standards", 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 94% 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 index 1d84cbf22..6f856a30f 100644 --- 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 @@ -48,5 +48,5 @@ spec_file: '' # set at runtime before leaving this step ## NEXT -- One-shot / ready-for-dev: Read fully and follow `{installed_path}/steps/step-03-implement.md` -- Plan-code-review: Read fully and follow `{installed_path}/steps/step-02-plan.md` +- One-shot / ready-for-dev: Read fully and follow `./steps/step-03-implement.md` +- Plan-code-review: Read fully and follow `./steps/step-02-plan.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 93% 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 index 4d102bc0a..87e5c86cb 100644 --- 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 @@ -2,7 +2,7 @@ name: 'step-02-plan' description: 'Investigate, generate spec, present for approval' -templateFile: '{installed_path}/tech-spec-template.md' +templateFile: '../tech-spec-template.md' wipFile: '{implementation_artifacts}/tech-spec-wip.md' deferred_work_file: '{implementation_artifacts}/deferred-work.md' --- @@ -36,4 +36,4 @@ Present summary. If token count exceeded 1600 and user chose [K], include the to ## NEXT -Read fully and follow `{installed_path}/steps/step-03-implement.md` +Read fully and follow `./steps/step-03-implement.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 94% 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 index c9aa55472..97d189272 100644 --- 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 @@ -32,4 +32,4 @@ Otherwise (`execution_mode = "plan-code-review"`): hand `{spec_file}` to a sub-a ## NEXT -Read fully and follow `{installed_path}/steps/step-04-review.md` +Read fully and follow `./steps/step-04-review.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 95% 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 index d9ebbc182..e13e7652c 100644 --- 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 @@ -46,7 +46,7 @@ Do NOT `git add` anything — this is read-only inspection. - **reject** — noise. Drop silently. When unsure between defer and reject, prefer reject — only defer findings you are confident are real. 3. Process findings in cascading order. If intent_gap or bad_spec findings exist, they trigger a loopback — lower findings are moot since code will be re-derived. If neither exists, process patch and defer normally. Increment `{specLoopIteration}` on each loopback. If it exceeds 5, HALT and escalate to the human. On any loopback, re-evaluate routing — if scope has grown beyond one-shot, escalate `execution_mode` to plan-code-review. - **intent_gap** — Root cause is inside ``. Revert code changes. Loop back to the human to resolve, then re-run steps 2–4. - - **bad_spec** — Root cause is outside ``. Before reverting code: extract KEEP instructions for positive preservation (what worked well and must survive re-derivation). Revert code changes. Read the `## Spec Change Log` in `{spec_file}` and strictly respect all logged constraints when amending the non-frozen sections that contain the root cause. Append a new change-log entry recording: the triggering finding, what was amended, the known-bad state avoided, and the KEEP instructions. Read fully and follow `{installed_path}/steps/step-03-implement.md` to re-derive the code, then this step will run again. + - **bad_spec** — Root cause is outside ``. Before reverting code: extract KEEP instructions for positive preservation (what worked well and must survive re-derivation). Revert code changes. Read the `## Spec Change Log` in `{spec_file}` and strictly respect all logged constraints when amending the non-frozen sections that contain the root cause. Append a new change-log entry recording: the triggering finding, what was amended, the known-bad state avoided, and the KEEP instructions. Read fully and follow `./steps/step-03-implement.md` to re-derive the code, then this step will run again. - **patch** — Auto-fix. These are the only findings that survive loopbacks. - **defer** — Append to `{deferred_work_file}`. - **reject** — Drop silently. @@ -54,4 +54,4 @@ Do NOT `git add` anything — this is read-only inspection. ## NEXT -Read fully and follow `{installed_path}/steps/step-05-present.md` +Read fully and follow `./steps/step-05-present.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 93% 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..eae636173 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,10 +81,9 @@ 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` -- `templateFile` = `{installed_path}/tech-spec-template.md` +- `templateFile` = `./tech-spec-template.md` - `wipFile` = `{implementation_artifacts}/tech-spec-wip.md` ### 3. First Step Execution -Read fully and follow: `{installed_path}/steps/step-01-clarify-and-route.md` to begin the workflow. +Read fully and follow: `./steps/step-01-clarify-and-route.md` to begin the workflow. 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/src/core/tasks/help.md b/src/core/tasks/help.md index 54a23b5f1..88f4a4d55 100644 --- a/src/core/tasks/help.md +++ b/src/core/tasks/help.md @@ -21,6 +21,12 @@ description: 'Analyzes what is done and the users query and offers advice on wha When `command` field has a value: - Show the command prefixed with `/` (e.g., `/bmad-bmm-create-prd`) +### Skill-Referenced Workflows +When `workflow-file` starts with `skill:`: +- The value is a skill reference (e.g., `skill:bmad-quick-dev-new-preview`), NOT a file path +- Do NOT attempt to resolve or load it as a file path +- Display using the `command` column value prefixed with `/` (same as command-based workflows) + ### Agent-Based Workflows When `command` field is empty: - User loads agent first via `/agent-command` diff --git a/test/test-install-to-bmad.js b/test/test-install-to-bmad.js new file mode 100644 index 000000000..0367dbe93 --- /dev/null +++ b/test/test-install-to-bmad.js @@ -0,0 +1,154 @@ +/** + * install_to_bmad Flag — Design Contract Tests + * + * Unit tests against the functions that implement the install_to_bmad flag. + * These nail down the 4 core design decisions: + * + * 1. true/omitted → skill stays in _bmad/ (default behavior) + * 2. false → skill removed from _bmad/ after IDE install + * 3. No platform → no cleanup runs (cleanup lives in installVerbatimSkills) + * 4. Mixed flags → each skill evaluated independently + * + * Usage: node test/test-install-to-bmad.js + */ + +const path = require('node:path'); +const os = require('node:os'); +const fs = require('fs-extra'); +const { loadSkillManifest, getInstallToBmad } = require('../tools/cli/installers/lib/ide/shared/skill-manifest'); + +// ANSI colors +const colors = { + reset: '\u001B[0m', + green: '\u001B[32m', + red: '\u001B[31m', + yellow: '\u001B[33m', + cyan: '\u001B[36m', + dim: '\u001B[2m', +}; + +let passed = 0; +let failed = 0; + +function assert(condition, testName, errorMessage = '') { + if (condition) { + console.log(`${colors.green}✓${colors.reset} ${testName}`); + passed++; + } else { + console.log(`${colors.red}✗${colors.reset} ${testName}`); + if (errorMessage) { + console.log(` ${colors.dim}${errorMessage}${colors.reset}`); + } + failed++; + } +} + +async function runTests() { + console.log(`${colors.cyan}========================================`); + console.log('install_to_bmad — Design Contract Tests'); + console.log(`========================================${colors.reset}\n`); + + // ============================================================ + // 1. true/omitted → getInstallToBmad returns true (keep in _bmad/) + // ============================================================ + console.log(`${colors.yellow}Design decision 1: true or omitted → skill stays in _bmad/${colors.reset}\n`); + + // Null manifest (no bmad-skill-manifest.yaml) → true + assert(getInstallToBmad(null, 'workflow.md') === true, 'null manifest defaults to true'); + + // Single-entry, flag omitted → true + assert( + getInstallToBmad({ __single: { type: 'skill' } }, 'workflow.md') === true, + 'single-entry manifest with flag omitted defaults to true', + ); + + // Single-entry, explicit true → true + assert( + getInstallToBmad({ __single: { type: 'skill', install_to_bmad: true } }, 'workflow.md') === true, + 'single-entry manifest with explicit true returns true', + ); + + console.log(''); + + // ============================================================ + // 2. false → getInstallToBmad returns false (remove from _bmad/) + // ============================================================ + console.log(`${colors.yellow}Design decision 2: false → skill removed from _bmad/${colors.reset}\n`); + + // Single-entry, explicit false → false + assert( + getInstallToBmad({ __single: { type: 'skill', install_to_bmad: false } }, 'workflow.md') === false, + 'single-entry manifest with explicit false returns false', + ); + + // loadSkillManifest round-trip: YAML with false is preserved through load + { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-itb-')); + await fs.writeFile(path.join(tmpDir, 'bmad-skill-manifest.yaml'), 'type: skill\ninstall_to_bmad: false\n'); + const loaded = await loadSkillManifest(tmpDir); + assert(getInstallToBmad(loaded, 'workflow.md') === false, 'loadSkillManifest preserves install_to_bmad: false through round-trip'); + await fs.remove(tmpDir); + } + + console.log(''); + + // ============================================================ + // 3. No platform → cleanup only runs inside installVerbatimSkills + // (This is a design invariant: getInstallToBmad is only consulted + // during IDE install. Without a platform, the flag has no effect.) + // ============================================================ + console.log(`${colors.yellow}Design decision 3: flag is a per-skill property, not a pipeline gate${colors.reset}\n`); + + // The flag value is stored but doesn't trigger any side effects by itself. + // Cleanup is driven by reading the CSV column inside installVerbatimSkills. + // We verify the flag is just data — getInstallToBmad doesn't touch the filesystem. + { + const manifest = { __single: { type: 'skill', install_to_bmad: false } }; + const result = getInstallToBmad(manifest, 'workflow.md'); + assert(typeof result === 'boolean', 'getInstallToBmad returns a boolean (pure data, no side effects)'); + assert(result === false, 'false value is faithfully returned for consumer to act on'); + } + + console.log(''); + + // ============================================================ + // 4. Mixed flags → each skill evaluated independently + // ============================================================ + console.log(`${colors.yellow}Design decision 4: mixed flags — each skill independent${colors.reset}\n`); + + // Multi-entry manifest: different files can have different flags + { + const manifest = { + 'workflow.md': { type: 'skill', install_to_bmad: false }, + 'other.md': { type: 'skill', install_to_bmad: true }, + }; + assert(getInstallToBmad(manifest, 'workflow.md') === false, 'multi-entry: workflow.md with false returns false'); + assert(getInstallToBmad(manifest, 'other.md') === true, 'multi-entry: other.md with true returns true'); + assert(getInstallToBmad(manifest, 'unknown.md') === true, 'multi-entry: unknown file defaults to true'); + } + + console.log(''); + + // ============================================================ + // Summary + // ============================================================ + console.log(`${colors.cyan}========================================`); + console.log('Results:'); + console.log(` Passed: ${colors.green}${passed}${colors.reset}`); + console.log(` Failed: ${colors.red}${failed}${colors.reset}`); + console.log(`========================================${colors.reset}\n`); + + if (failed === 0) { + console.log(`${colors.green}All install_to_bmad contract tests passed!${colors.reset}\n`); + process.exit(0); + } else { + console.log(`${colors.red}Some install_to_bmad contract tests failed${colors.reset}\n`); + process.exit(1); + } +} + +runTests().catch((error) => { + console.error(`${colors.red}Test runner failed:${colors.reset}`, error.message); + console.error(error.stack); + process.exit(1); +}); diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index 0955a3d6f..34a8fbbb4 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -5,7 +5,12 @@ 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, + getInstallToBmad: getInstallToBmadShared, +} = require('../ide/shared/skill-manifest'); // Load package.json for version info const packageJson = require('../../../../../package.json'); @@ -16,6 +21,7 @@ const packageJson = require('../../../../../package.json'); class ManifestGenerator { constructor() { this.workflows = []; + this.skills = []; this.agents = []; this.tasks = []; this.tools = []; @@ -34,6 +40,16 @@ class ManifestGenerator { return getCanonicalIdShared(manifest, filename); } + /** Delegate to shared skill-manifest module */ + getArtifactType(manifest, filename) { + return getArtifactTypeShared(manifest, filename); + } + + /** Delegate to shared skill-manifest module */ + getInstallToBmad(manifest, filename) { + return getInstallToBmadShared(manifest, filename); + } + /** * Clean text for CSV output by normalizing whitespace. * Note: Quote escaping is handled by escapeCsv() at write time. @@ -89,6 +105,9 @@ class ManifestGenerator { // Filter out any undefined/null values from IDE list this.selectedIdes = resolvedIdes.filter((ide) => ide && typeof ide === 'string'); + // Reset files list (defensive: prevent stale data if instance is reused) + this.files = []; + // Collect workflow data await this.collectWorkflows(selectedModules); @@ -105,6 +124,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), @@ -127,6 +147,7 @@ class ManifestGenerator { */ async collectWorkflows(selectedModules) { this.workflows = []; + this.skills = []; // Use updatedModules which already includes deduplicated 'core' + selectedModules for (const moduleName of this.updatedModules) { @@ -228,6 +249,25 @@ 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, + install_to_bmad: this.getInstallToBmad(skillManifest, entry.name), + }); + + 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 +833,32 @@ 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,install_to_bmad\n'; + + for (const skill of this.skills) { + const row = [ + escapeCsv(skill.canonicalId), + escapeCsv(skill.name), + escapeCsv(skill.description), + escapeCsv(skill.module), + escapeCsv(skill.path), + escapeCsv(skill.install_to_bmad), + ].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..e72b61481 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 @@ -116,39 +117,48 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { async installToTarget(projectDir, bmadDir, config, options) { const { target_dir, template_type, artifact_types } = config; - // Skip targets with explicitly empty artifact_types array + // Skip targets with explicitly empty artifact_types and no verbatim skills // 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 } }; + const skipStandardArtifacts = Array.isArray(artifact_types) && artifact_types.length === 0; + if (skipStandardArtifacts && !config.skill_format) { + 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')) { - const agentGen = new AgentCommandGenerator(this.bmadFolderName); - const { artifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules); - results.agents = await this.writeAgentArtifacts(targetPath, artifacts, template_type, config); + // Install standard artifacts (agents, workflows, tasks, tools) + if (!skipStandardArtifacts) { + // Install agents + if (!artifact_types || artifact_types.includes('agents')) { + const agentGen = new AgentCommandGenerator(this.bmadFolderName); + const { artifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules); + results.agents = await this.writeAgentArtifacts(targetPath, artifacts, template_type, config); + } + + // Install workflows + if (!artifact_types || artifact_types.includes('workflows')) { + const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName); + const { artifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir); + results.workflows = await this.writeWorkflowArtifacts(targetPath, artifacts, template_type, config); + } + + // Install tasks and tools using template system (supports TOML for Gemini, MD for others) + 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); + results.tasks = taskToolResult.tasks || 0; + results.tools = taskToolResult.tools || 0; + } } - // Install workflows - if (!artifact_types || artifact_types.includes('workflows')) { - const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName); - const { artifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir); - results.workflows = await this.writeWorkflowArtifacts(targetPath, artifacts, template_type, config); - } - - // Install tasks and tools using template system (supports TOML for Gemini, MD for others) - 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); - results.tasks = taskToolResult.tasks || 0; - 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); @@ -164,7 +174,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 +183,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 +633,94 @@ 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++; + } + + // Post-install cleanup: remove _bmad/ directories for skills with install_to_bmad === "false" + for (const record of records) { + if (record.install_to_bmad === 'false') { + const relativePath = record.path.replace(new RegExp(`^${bmadFolderName}/`), ''); + const sourceFile = path.join(bmadDir, relativePath); + const sourceDir = path.dirname(sourceFile); + if (await fs.pathExists(sourceDir)) { + await fs.remove(sourceDir); + } + } + } + + return count; + } + /** * Print installation summary * @param {Object} results - Installation results @@ -634,6 +733,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..22a7cceef 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,46 @@ 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; +} + +/** + * Get the install_to_bmad flag 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 {boolean} install_to_bmad value (defaults to true) + */ +function getInstallToBmad(manifest, filename) { + if (!manifest) return true; + // Single-entry manifest applies to all files in the directory + if (manifest.__single) return manifest.__single.install_to_bmad !== false; + // Multi-entry: look up by filename directly + if (manifest[filename]) return manifest[filename].install_to_bmad !== false; + // 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].install_to_bmad !== false; + const xmlKey = `${baseName}.xml`; + if (manifest[xmlKey]) return manifest[xmlKey].install_to_bmad !== false; + return true; +} + +module.exports = { loadSkillManifest, getCanonicalId, getArtifactType, getInstallToBmad }; diff --git a/tools/validate-file-refs.js b/tools/validate-file-refs.js index bf92f31f8..a3b91f2fb 100644 --- a/tools/validate-file-refs.js +++ b/tools/validate-file-refs.js @@ -324,6 +324,8 @@ function extractCsvRefs(filePath, content) { const raw = record['workflow-file']; if (!raw || raw.trim() === '') continue; if (!isResolvable(raw)) continue; + // skill: prefixed references are resolved by the IDE/CLI, not as file paths + if (raw.startsWith('skill:')) continue; // Line = header (1) + data row index (0-based) + 1 const line = i + 2;