From ba890779a2b86e41a194860ba619b72496c182ff Mon Sep 17 00:00:00 2001 From: Michael Pursifull Date: Tue, 3 Feb 2026 13:13:38 -0600 Subject: [PATCH 1/4] feat: cross-file reference validator for BMAD source files (#1494) * feat: add cross-file reference validator for CI Add tools/validate-file-refs.js that validates cross-file references in BMAD source files (agents, workflows, tasks, steps). Catches broken file paths, missing referenced files, wrong extensions, and absolute path leaks before they reach users. Addresses broken-file-ref and path-handling bug classes which account for 25% of all historical bugs (59 closed issues, 129+ comments). - Scans src/ for YAML, markdown, and XML files - Validates {project-root}/_bmad/ references against source tree - Checks relative path references, exec attributes, invoke-task tags - Detects absolute path leaks (/Users/, /home/, C:\) - Adds validate:refs npm script and CI step in quality.yaml * feat: strip JSON example blocks to reduce false-positive broken refs Add stripJsonExampleBlocks() to the markdown reference extractor so bare JSON example/template blocks (braces on their own lines) are removed before pattern matching. This prevents paths inside example data from being flagged as broken references. * feat: add line numbers, fix utility/ path mapping, improve verbose output - Add utility/ to direct path mapping (was incorrectly falling through to src/modules/utility/) - Show line numbers for broken references in markdown files - Show YAML key path for broken references in YAML files - Print file headers in verbose mode for all files with refs * fix: correct verbose [OK]/[BROKEN] overlap and line number drift Broken refs no longer print [OK] before [BROKEN] in --verbose mode. Code block stripping now preserves newlines so offsetToLine() reports accurate line numbers when code blocks precede broken references. * fix: address review feedback, add CI annotations and step summary Address alexeyv's review findings on PR #1494: - Fix exec-attr prefix handling for {_bmad}/ and bare _bmad/ paths - Fix mapInstalledToSource fallback (remove phantom src/modules/ mapping) - Switch extractYamlRefs to parseDocument() for YAML line numbers Add CI integration (stories 2-1, 2-2): - Emit ::warning annotations for broken refs and abs-path leaks - Write markdown table to $GITHUB_STEP_SUMMARY - Guard both behind environment variable checks Harden CI output: - escapeAnnotation() encodes %, \r, \n per GitHub Actions spec - escapeTableCell() escapes pipe chars in step summary table --------- Co-authored-by: Alex Verkhovsky Co-authored-by: Brian --- .github/workflows/quality.yaml | 3 + package.json | 1 + tools/validate-file-refs.js | 480 +++++++++++++++++++++++++++++++++ 3 files changed, 484 insertions(+) create mode 100644 tools/validate-file-refs.js diff --git a/.github/workflows/quality.yaml b/.github/workflows/quality.yaml index aa281b4a..65194558 100644 --- a/.github/workflows/quality.yaml +++ b/.github/workflows/quality.yaml @@ -113,3 +113,6 @@ jobs: - name: Test agent compilation components run: npm run test:install + + - name: Validate file references + run: npm run validate:refs diff --git a/package.json b/package.json index f3342a41..8df5ea00 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "test:coverage": "c8 --reporter=text --reporter=html npm run test:schemas", "test:install": "node test/test-installation-components.js", "test:schemas": "node test/test-agent-schema.js", + "validate:refs": "node tools/validate-file-refs.js", "validate:schemas": "node tools/validate-agent-schema.js" }, "lint-staged": { diff --git a/tools/validate-file-refs.js b/tools/validate-file-refs.js new file mode 100644 index 00000000..71fd4be7 --- /dev/null +++ b/tools/validate-file-refs.js @@ -0,0 +1,480 @@ +/** + * File Reference Validator + * + * Validates cross-file references in BMAD source files (agents, workflows, tasks, steps). + * Catches broken file paths, missing referenced files, and absolute path leaks. + * + * What it checks: + * - {project-root}/_bmad/ references in YAML and markdown resolve to real src/ files + * - Relative path references (./file.md, ../data/file.csv) point to existing files + * - exec="..." and targets exist + * - Step metadata (thisStepFile, nextStepFile) references are valid + * - Load directives (Load: `./file.md`) target existing files + * - No absolute paths (/Users/, /home/, C:\) leak into source files + * + * What it does NOT check (deferred): + * - {installed_path} variable interpolation (self-referential, low risk) + * - {{mustache}} template variables (runtime substitution) + * - {config_source}:key dynamic YAML dereferences + * + * Usage: + * node tools/validate-file-refs.js # Warn on broken references (exit 0) + * node tools/validate-file-refs.js --strict # Fail on broken references (exit 1) + * node tools/validate-file-refs.js --verbose # Show all checked references + * + * Default mode is warning-only (exit 0) so adoption is non-disruptive. + * Use --strict when you want CI or pre-commit to enforce valid references. + */ + +const fs = require('node:fs'); +const path = require('node:path'); +const yaml = require('yaml'); + +const PROJECT_ROOT = path.resolve(__dirname, '..'); +const SRC_DIR = path.join(PROJECT_ROOT, 'src'); +const VERBOSE = process.argv.includes('--verbose'); +const STRICT = process.argv.includes('--strict'); + +// --- Constants --- + +// File extensions to scan +const SCAN_EXTENSIONS = new Set(['.yaml', '.yml', '.md', '.xml']); + +// Skip directories +const SKIP_DIRS = new Set(['node_modules', '_module-installer', '.git']); + +// Pattern: {project-root}/_bmad/ references +const PROJECT_ROOT_REF = /\{project-root\}\/_bmad\/([^\s'"<>})\]`]+)/g; + +// Pattern: {_bmad}/ shorthand references +const BMAD_SHORTHAND_REF = /\{_bmad\}\/([^\s'"<>})\]`]+)/g; + +// Pattern: exec="..." attributes +const EXEC_ATTR = /exec="([^"]+)"/g; + +// Pattern: content +const INVOKE_TASK = /([^<]+)<\/invoke-task>/g; + +// Pattern: relative paths in quotes +const RELATIVE_PATH_QUOTED = /['"](\.\.\/?[^'"]+\.(?:md|yaml|yml|xml|json|csv|txt))['"]/g; +const RELATIVE_PATH_DOT = /['"](\.\/[^'"]+\.(?:md|yaml|yml|xml|json|csv|txt))['"]/g; + +// Pattern: step metadata +const STEP_META = /(?:thisStepFile|nextStepFile|continueStepFile|skipToStepFile|altStepFile|workflowFile):\s*['"](\.[^'"]+)['"]/g; + +// Pattern: Load directives +const LOAD_DIRECTIVE = /Load[:\s]+`(\.[^`]+)`/g; + +// Pattern: absolute path leaks +const ABS_PATH_LEAK = /(?:\/Users\/|\/home\/|[A-Z]:\\\\)/; + +// --- Output Escaping --- + +function escapeAnnotation(str) { + return str.replaceAll('%', '%25').replaceAll('\r', '%0D').replaceAll('\n', '%0A'); +} + +function escapeTableCell(str) { + return String(str).replaceAll('|', String.raw`\|`); +} + +// Path prefixes/patterns that only exist in installed structure, not in source +const INSTALL_ONLY_PATHS = ['_config/']; + +// Files that are generated at install time and don't exist in the source tree +const INSTALL_GENERATED_FILES = ['config.yaml']; + +// Variables that indicate a path is not statically resolvable +const UNRESOLVABLE_VARS = [ + '{output_folder}', + '{value}', + '{timestamp}', + '{config_source}:', + '{installed_path}', + '{shared_path}', + '{planning_artifacts}', + '{research_topic}', + '{user_name}', + '{communication_language}', + '{epic_number}', + '{next_epic_num}', + '{epic_num}', + '{part_id}', + '{count}', + '{date}', + '{outputFile}', + '{nextStepFile}', +]; + +// --- File Discovery --- + +function getSourceFiles(dir) { + const files = []; + + function walk(currentDir) { + const entries = fs.readdirSync(currentDir, { withFileTypes: true }); + + for (const entry of entries) { + if (SKIP_DIRS.has(entry.name)) continue; + + const fullPath = path.join(currentDir, entry.name); + + if (entry.isDirectory()) { + walk(fullPath); + } else if (entry.isFile() && SCAN_EXTENSIONS.has(path.extname(entry.name))) { + files.push(fullPath); + } + } + } + + walk(dir); + return files; +} + +// --- Code Block Stripping --- + +function stripCodeBlocks(content) { + return content.replaceAll(/```[\s\S]*?```/g, (m) => m.replaceAll(/[^\n]/g, '')); +} + +function stripJsonExampleBlocks(content) { + // Strip bare JSON example blocks: { and } each on their own line. + // These are example/template data (not real file references). + return content.replaceAll(/^\{\s*\n(?:.*\n)*?^\}\s*$/gm, (m) => m.replaceAll(/[^\n]/g, '')); +} + +// --- Path Mapping --- + +function mapInstalledToSource(refPath) { + // Strip {project-root}/_bmad/ or {_bmad}/ prefix + let cleaned = refPath.replace(/^\{project-root\}\/_bmad\//, '').replace(/^\{_bmad\}\//, ''); + + // Also handle bare _bmad/ prefix (seen in some invoke-task) + cleaned = cleaned.replace(/^_bmad\//, ''); + + // Skip install-only paths (generated at install time, not in source) + if (isInstallOnly(cleaned)) return null; + + // core/, bmm/, and utility/ are directly under src/ + if (cleaned.startsWith('core/') || cleaned.startsWith('bmm/') || cleaned.startsWith('utility/')) { + return path.join(SRC_DIR, cleaned); + } + + // Fallback: map directly under src/ + return path.join(SRC_DIR, cleaned); +} + +// --- Reference Extraction --- + +function isResolvable(refStr) { + // Skip refs containing unresolvable runtime variables + if (refStr.includes('{{')) return false; + for (const v of UNRESOLVABLE_VARS) { + if (refStr.includes(v)) return false; + } + return true; +} + +function isInstallOnly(cleanedPath) { + // Skip paths that only exist in the installed _bmad/ structure, not in src/ + for (const prefix of INSTALL_ONLY_PATHS) { + if (cleanedPath.startsWith(prefix)) return true; + } + // Skip files that are generated during installation + const basename = path.basename(cleanedPath); + for (const generated of INSTALL_GENERATED_FILES) { + if (basename === generated) return true; + } + return false; +} + +function extractYamlRefs(filePath, content) { + const refs = []; + + let doc; + try { + doc = yaml.parseDocument(content); + } catch { + return refs; // Skip unparseable YAML (schema validator handles this) + } + + function checkValue(value, range, keyPath) { + if (typeof value !== 'string') return; + if (!isResolvable(value)) return; + + const line = range ? offsetToLine(content, range[0]) : undefined; + + // Check for {project-root}/_bmad/ refs + const prMatch = value.match(/\{project-root\}\/_bmad\/[^\s'"<>})\]`]+/); + if (prMatch) { + refs.push({ file: filePath, raw: prMatch[0], type: 'project-root', line, key: keyPath }); + } + + // Check for {_bmad}/ refs + const bmMatch = value.match(/\{_bmad\}\/[^\s'"<>})\]`]+/); + if (bmMatch) { + refs.push({ file: filePath, raw: bmMatch[0], type: 'project-root', line, key: keyPath }); + } + + // Check for relative paths + const relMatch = value.match(/^\.\.?\/[^\s'"<>})\]`]+\.(?:md|yaml|yml|xml|json|csv|txt)$/); + if (relMatch) { + refs.push({ file: filePath, raw: relMatch[0], type: 'relative', line, key: keyPath }); + } + } + + function walkNode(node, keyPath) { + if (!node) return; + + if (yaml.isMap(node)) { + for (const item of node.items) { + const key = item.key && item.key.value !== undefined ? item.key.value : '?'; + const childPath = keyPath ? `${keyPath}.${key}` : String(key); + walkNode(item.value, childPath); + } + } else if (yaml.isSeq(node)) { + for (const [i, item] of node.items.entries()) { + walkNode(item, `${keyPath}[${i}]`); + } + } else if (yaml.isScalar(node)) { + checkValue(node.value, node.range, keyPath); + } + } + + walkNode(doc.contents, ''); + return refs; +} + +function offsetToLine(content, offset) { + let line = 1; + for (let i = 0; i < offset && i < content.length; i++) { + if (content[i] === '\n') line++; + } + return line; +} + +function extractMarkdownRefs(filePath, content) { + const refs = []; + const stripped = stripJsonExampleBlocks(stripCodeBlocks(content)); + + function runPattern(regex, type) { + regex.lastIndex = 0; + let match; + while ((match = regex.exec(stripped)) !== null) { + const raw = match[1]; + if (!isResolvable(raw)) continue; + refs.push({ file: filePath, raw, type, line: offsetToLine(stripped, match.index) }); + } + } + + // {project-root}/_bmad/ refs + runPattern(PROJECT_ROOT_REF, 'project-root'); + + // {_bmad}/ shorthand + runPattern(BMAD_SHORTHAND_REF, 'project-root'); + + // exec="..." attributes + runPattern(EXEC_ATTR, 'exec-attr'); + + // tags + runPattern(INVOKE_TASK, 'invoke-task'); + + // Step metadata + runPattern(STEP_META, 'relative'); + + // Load directives + runPattern(LOAD_DIRECTIVE, 'relative'); + + // Relative paths in quotes + runPattern(RELATIVE_PATH_QUOTED, 'relative'); + runPattern(RELATIVE_PATH_DOT, 'relative'); + + return refs; +} + +// --- Reference Resolution --- + +function resolveRef(ref) { + if (ref.type === 'project-root') { + return mapInstalledToSource(ref.raw); + } + + if (ref.type === 'relative') { + return path.resolve(path.dirname(ref.file), ref.raw); + } + + if (ref.type === 'exec-attr') { + let execPath = ref.raw; + if (execPath.includes('{project-root}')) { + return mapInstalledToSource(execPath); + } + if (execPath.includes('{_bmad}')) { + return mapInstalledToSource(execPath); + } + if (execPath.startsWith('_bmad/')) { + return mapInstalledToSource(execPath); + } + // Relative exec path + return path.resolve(path.dirname(ref.file), execPath); + } + + if (ref.type === 'invoke-task') { + // Extract file path from invoke-task content + const prMatch = ref.raw.match(/\{project-root\}\/_bmad\/([^\s'"<>})\]`]+)/); + if (prMatch) return mapInstalledToSource(prMatch[0]); + + const bmMatch = ref.raw.match(/\{_bmad\}\/([^\s'"<>})\]`]+)/); + if (bmMatch) return mapInstalledToSource(bmMatch[0]); + + const bareMatch = ref.raw.match(/_bmad\/([^\s'"<>})\]`]+)/); + if (bareMatch) return mapInstalledToSource(bareMatch[0]); + + return null; // Can't resolve โ€” skip + } + + return null; +} + +// --- Absolute Path Leak Detection --- + +function checkAbsolutePathLeaks(filePath, content) { + const leaks = []; + const stripped = stripCodeBlocks(content); + const lines = stripped.split('\n'); + + for (const [i, line] of lines.entries()) { + if (ABS_PATH_LEAK.test(line)) { + leaks.push({ file: filePath, line: i + 1, content: line.trim() }); + } + } + + return leaks; +} + +// --- Main --- + +console.log(`\nValidating file references in: ${SRC_DIR}`); +console.log(`Mode: ${STRICT ? 'STRICT (exit 1 on issues)' : 'WARNING (exit 0)'}${VERBOSE ? ' + VERBOSE' : ''}\n`); + +const files = getSourceFiles(SRC_DIR); +console.log(`Found ${files.length} source files\n`); + +let totalRefs = 0; +let brokenRefs = 0; +let totalLeaks = 0; +let filesWithIssues = 0; +const allIssues = []; // Collect for $GITHUB_STEP_SUMMARY + +for (const filePath of files) { + const relativePath = path.relative(PROJECT_ROOT, filePath); + const content = fs.readFileSync(filePath, 'utf-8'); + const ext = path.extname(filePath); + + // Extract references + let refs; + if (ext === '.yaml' || ext === '.yml') { + refs = extractYamlRefs(filePath, content); + } else { + refs = extractMarkdownRefs(filePath, content); + } + + // Resolve and check + const broken = []; + + if (VERBOSE && refs.length > 0) { + console.log(`\n${relativePath}`); + } + + for (const ref of refs) { + totalRefs++; + const resolved = resolveRef(ref); + + if (resolved && !fs.existsSync(resolved)) { + // For paths without extensions, also check if it's a directory + const hasExt = path.extname(resolved) !== ''; + if (!hasExt) { + // Could be a directory reference โ€” skip if not clearly a file + continue; + } + broken.push({ ref, resolved: path.relative(PROJECT_ROOT, resolved) }); + brokenRefs++; + continue; + } + + if (VERBOSE && resolved) { + console.log(` [OK] ${ref.raw}`); + } + } + + // Check absolute path leaks + const leaks = checkAbsolutePathLeaks(filePath, content); + totalLeaks += leaks.length; + + // Report issues for this file + if (broken.length > 0 || leaks.length > 0) { + filesWithIssues++; + if (!VERBOSE) { + console.log(`\n${relativePath}`); + } + + for (const { ref, resolved } of broken) { + const location = ref.line ? `line ${ref.line}` : ref.key ? `key: ${ref.key}` : ''; + console.log(` [BROKEN] ${ref.raw}${location ? ` (${location})` : ''}`); + console.log(` Target not found: ${resolved}`); + allIssues.push({ file: relativePath, line: ref.line || 1, ref: ref.raw, issue: 'broken ref' }); + if (process.env.GITHUB_ACTIONS) { + const line = ref.line || 1; + console.log(`::warning file=${relativePath},line=${line}::${escapeAnnotation(`Broken reference: ${ref.raw} โ†’ ${resolved}`)}`); + } + } + + for (const leak of leaks) { + console.log(` [ABS-PATH] Line ${leak.line}: ${leak.content}`); + allIssues.push({ file: relativePath, line: leak.line, ref: leak.content, issue: 'abs-path' }); + if (process.env.GITHUB_ACTIONS) { + console.log(`::warning file=${relativePath},line=${leak.line}::${escapeAnnotation(`Absolute path leak: ${leak.content}`)}`); + } + } + } +} + +// Summary +console.log(`\n${'โ”€'.repeat(60)}`); +console.log(`\nSummary:`); +console.log(` Files scanned: ${files.length}`); +console.log(` References checked: ${totalRefs}`); +console.log(` Broken references: ${brokenRefs}`); +console.log(` Absolute path leaks: ${totalLeaks}`); + +const hasIssues = brokenRefs > 0 || totalLeaks > 0; + +if (hasIssues) { + console.log(`\n ${filesWithIssues} file(s) with issues`); + + if (STRICT) { + console.log(`\n [STRICT MODE] Exiting with failure.`); + } else { + console.log(`\n Run with --strict to treat warnings as errors.`); + } +} else { + console.log(`\n All file references valid!`); +} + +console.log(''); + +// Write GitHub Actions step summary +if (process.env.GITHUB_STEP_SUMMARY) { + let summary = '## File Reference Validation\n\n'; + if (allIssues.length > 0) { + summary += '| File | Line | Reference | Issue |\n'; + summary += '|------|------|-----------|-------|\n'; + for (const issue of allIssues) { + summary += `| ${escapeTableCell(issue.file)} | ${issue.line} | ${escapeTableCell(issue.ref)} | ${issue.issue} |\n`; + } + summary += '\n'; + } + summary += `**${files.length} files scanned, ${totalRefs} references checked, ${brokenRefs + totalLeaks} issues found**\n`; + fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary); +} + +process.exit(hasIssues && STRICT ? 1 : 0); From 7ecae1d00061aa3bcdf4f114a5a242eb5062efb1 Mon Sep 17 00:00:00 2001 From: Murat K Ozcan <34237651+muratkeremozcan@users.noreply.github.com> Date: Tue, 3 Feb 2026 13:23:37 -0600 Subject: [PATCH 2/4] test: quinn to qa (#1508) * test: quinn to qa * Removed the TEA sidebar section from the main docs nav --------- Co-authored-by: Brian --- docs/reference/workflow-map.md | 2 +- .../{quinn.agent.yaml => qa.agent.yaml} | 4 +-- src/bmm/module-help.csv | 2 +- test/test-installation-components.js | 2 +- website/astro.config.mjs | 36 +------------------ 5 files changed, 6 insertions(+), 40 deletions(-) rename src/bmm/agents/{quinn.agent.yaml => qa.agent.yaml} (97%) diff --git a/docs/reference/workflow-map.md b/docs/reference/workflow-map.md index 667485ba..1425c469 100644 --- a/docs/reference/workflow-map.md +++ b/docs/reference/workflow-map.md @@ -58,7 +58,7 @@ Build it, one story at a time. | `correct-course` | Handle significant mid-sprint changes | Updated plan or re-routing | | `retrospective` | Review after epic completion | Lessons learned | -**Quinn (QA Agent):** Built-in QA agent for test automation. Trigger with `QA` or `bmad-bmm-automate`. Generates standard API and E2E tests using your project's test framework. Beginner-friendly, no configuration needed. For advanced test strategy, install [Test Architect (TEA)](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/) module. +**Quinn (QA Agent):** Built-in QA agent for test automation. Trigger with `QA` or `bmad-bmm-qa-automate`. Generates standard API and E2E tests using your project's test framework. Beginner-friendly, no configuration needed. For advanced test strategy, install [Test Architect (TEA)](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/) module. ## Quick Flow (Parallel Track) diff --git a/src/bmm/agents/quinn.agent.yaml b/src/bmm/agents/qa.agent.yaml similarity index 97% rename from src/bmm/agents/quinn.agent.yaml rename to src/bmm/agents/qa.agent.yaml index 73ed366c..bad945cd 100644 --- a/src/bmm/agents/quinn.agent.yaml +++ b/src/bmm/agents/qa.agent.yaml @@ -1,6 +1,6 @@ agent: metadata: - id: "_bmad/bmm/agents/quinn" + id: "_bmad/bmm/agents/qa" name: Quinn title: QA Engineer icon: ๐Ÿงช @@ -54,4 +54,4 @@ agent: For comprehensive test strategy, risk-based planning, quality gates, and enterprise features, install the Test Architect (TEA) module: https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/ - Ready to generate some tests? Just say `QA` or `bmad-bmm-automate`! + Ready to generate some tests? Just say `QA` or `bmad-bmm-qa-automate`! diff --git a/src/bmm/module-help.csv b/src/bmm/module-help.csv index af7c52c2..45eeb6ab 100644 --- a/src/bmm/module-help.csv +++ b/src/bmm/module-help.csv @@ -34,5 +34,5 @@ bmm,4-implementation,Validate Story,VS,35,_bmad/bmm/workflows/4-implementation/c bmm,4-implementation,Create Story,CS,30,_bmad/bmm/workflows/4-implementation/create-story/workflow.yaml,bmad-bmm-create-story,true,sm,Create Mode,"Story cycle start: Prepare first found story in the sprint plan that is next, or if the command is run with a specific epic and story designation with context. Once complete, then VS then DS then CR then back to DS if needed or next CS or ER",implementation_artifacts,story, bmm,4-implementation,Dev Story,DS,40,_bmad/bmm/workflows/4-implementation/dev-story/workflow.yaml,bmad-bmm-dev-story,true,dev,Create Mode,"Story cycle: Execute story implementation tasks and tests then CR then back to DS if fixes needed",,, bmm,4-implementation,Code Review,CR,50,_bmad/bmm/workflows/4-implementation/code-review/workflow.yaml,bmad-bmm-code-review,false,dev,Create Mode,"Story cycle: If issues back to DS if approved then next CS or ER if epic complete",,, -bmm,4-implementation,QA Automation Test,QA,45,_bmad/bmm/workflows/qa/automate/workflow.yaml,bmad-bmm-qa-automate,false,quinn,Create Mode,"Generate automated API and E2E tests for implemented code using the project's existing test framework (detects existing well known in use test frameworks). Use after implementation to add test coverage. NOT for code review or story validation - use CR for that.",implementation_artifacts,"test suite", +bmm,4-implementation,QA Automation Test,QA,45,_bmad/bmm/workflows/qa/automate/workflow.yaml,bmad-bmm-qa-automate,false,qa,Create Mode,"Generate automated API and E2E tests for implemented code using the project's existing test framework (detects existing well known in use test frameworks). Use after implementation to add test coverage. NOT for code review or story validation - use CR for that.",implementation_artifacts,"test suite", bmm,4-implementation,Retrospective,ER,60,_bmad/bmm/workflows/4-implementation/retrospective/workflow.yaml,bmad-bmm-retrospective,false,sm,Create Mode,"Optional at epic end: Review completed work lessons learned and next epic or if major issues consider CC",implementation_artifacts,retrospective, diff --git a/test/test-installation-components.js b/test/test-installation-components.js index 78ac650a..0970861b 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -164,7 +164,7 @@ async function runTests() { try { const builder = new YamlXmlBuilder(); - const qaAgentPath = path.join(projectRoot, 'src/bmm/agents/quinn.agent.yaml'); + const qaAgentPath = path.join(projectRoot, 'src/bmm/agents/qa.agent.yaml'); const tempOutput = path.join(__dirname, 'temp-qa-agent.md'); try { diff --git a/website/astro.config.mjs b/website/astro.config.mjs index a57bc648..23a3179c 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -110,41 +110,7 @@ export default defineConfig({ collapsed: true, autogenerate: { directory: 'reference' }, }, - { - label: 'TEA - Testing in BMAD', - collapsed: true, - items: [ - { - label: 'Tutorials', - autogenerate: { directory: 'tea/tutorials' }, - }, - { - label: 'How-To Guides', - items: [ - { - label: 'Workflows', - autogenerate: { directory: 'tea/how-to/workflows' }, - }, - { - label: 'Customization', - autogenerate: { directory: 'tea/how-to/customization' }, - }, - { - label: 'Brownfield', - autogenerate: { directory: 'tea/how-to/brownfield' }, - }, - ], - }, - { - label: 'Explanation', - autogenerate: { directory: 'tea/explanation' }, - }, - { - label: 'Reference', - autogenerate: { directory: 'tea/reference' }, - }, - ], - }, + // TEA docs moved to standalone module site; keep BMM sidebar focused. ], // Credits in footer From 594235522c5ce29a6eaa7336f33ecdd0909b5051 Mon Sep 17 00:00:00 2001 From: Michael Pursifull Date: Tue, 3 Feb 2026 13:24:33 -0600 Subject: [PATCH 3/4] fix: add process control and building automation domains (#1510) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds two operational technology domains to domain-complexity.csv in both PRD and architecture workflows. Addresses the gap in OT domain coverage for physical process control and building systems. process_control: industrial automation, SCADA, PLC, DCS, I&C, P&ID โ€” covers power/utilities, water treatment, oil & gas, manufacturing, chemical, pharmaceutical, food & beverage, mining, and other sectors where software controls physical processes. Key concerns include functional safety, process safety and hazard analysis, environmental compliance, OT cybersecurity, and plant reliability/maintainability. Requires engineering_authority PRD section for PE/EOR credential requirements. building_automation: BAS/BMS, HVAC, fire alarm, fire protection, life safety, elevators, lighting, access control, commissioning โ€” covers commercial and institutional building systems. Key concerns include life safety codes, multi-trade coordination, commissioning, and indoor environmental quality. Both domains are high complexity, include engineering_authority as a required PRD section, and follow established entry patterns. Fixes #1240 Co-authored-by: Brian --- .../2-plan-workflows/create-prd/data/domain-complexity.csv | 2 ++ .../create-architecture/data/domain-complexity.csv | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/bmm/workflows/2-plan-workflows/create-prd/data/domain-complexity.csv b/src/bmm/workflows/2-plan-workflows/create-prd/data/domain-complexity.csv index 2e44a896..60a7b503 100644 --- a/src/bmm/workflows/2-plan-workflows/create-prd/data/domain-complexity.csv +++ b/src/bmm/workflows/2-plan-workflows/create-prd/data/domain-complexity.csv @@ -9,5 +9,7 @@ scientific,"research,algorithm,simulation,modeling,computational,analysis,data s legaltech,"legal,law,contract,compliance,litigation,patent,attorney,court",high,"Legal ethics;Bar regulations;Data retention;Attorney-client privilege;Court system integration","Legal practice rules;Ethics requirements;Court filing systems;Document standards;Confidentiality","domain-research","legal technology ethics {date};law practice management software requirements;court filing system standards;attorney client privilege technology","ethics_compliance;data_retention;confidentiality_measures;court_integration" insuretech,"insurance,claims,underwriting,actuarial,policy,risk,premium",high,"Insurance regulations;Actuarial standards;Data privacy;Fraud detection;State compliance","Insurance regulations by state;Actuarial methods;Risk modeling;Claims processing;Regulatory reporting","domain-research","insurance software regulations {date};actuarial standards software;insurance fraud detection;state insurance compliance","regulatory_requirements;risk_modeling;fraud_detection;reporting_compliance" energy,"energy,utility,grid,solar,wind,power,electricity,oil,gas",high,"Grid compliance;NERC standards;Environmental regulations;Safety requirements;Real-time operations","Energy regulations;Grid standards;Environmental compliance;Safety protocols;SCADA systems","domain-research","energy sector software compliance {date};NERC CIP standards;smart grid requirements;renewable energy software standards","grid_compliance;safety_protocols;environmental_compliance;operational_requirements" +process_control,"industrial automation,process control,PLC,SCADA,DCS,HMI,operational technology,OT,control system,cyberphysical,MES,historian,instrumentation,I&C,P&ID",high,"Functional safety;OT cybersecurity;Real-time control requirements;Legacy system integration;Process safety and hazard analysis;Environmental compliance and permitting;Engineering authority and PE requirements","Functional safety standards;OT security frameworks;Industrial protocols;Process control architecture;Plant reliability and maintainability","domain-research + technical-model","IEC 62443 OT cybersecurity requirements {date};functional safety software requirements {date};industrial process control architecture;ISA-95 manufacturing integration","functional_safety;ot_security;process_requirements;engineering_authority" +building_automation,"building automation,BAS,BMS,HVAC,smart building,lighting control,fire alarm,fire protection,fire suppression,life safety,elevator,access control,DDC,energy management,sequence of operations,commissioning",high,"Life safety codes;Building energy standards;Multi-trade coordination and interoperability;Commissioning and ongoing operational performance;Indoor environmental quality and occupant comfort;Engineering authority and PE requirements","Building automation protocols;HVAC and mechanical controls;Fire alarm, fire protection, and life safety design;Commissioning process and sequence of operations;Building codes and energy standards","domain-research","smart building software architecture {date};BACnet integration best practices;building automation cybersecurity {date};ASHRAE building standards","life_safety;energy_compliance;commissioning_requirements;engineering_authority" gaming,"game,player,gameplay,level,character,multiplayer,quest",redirect,"REDIRECT TO GAME WORKFLOWS","Game design","game-brief","NA","NA" general,"",low,"Standard requirements;Basic security;User experience;Performance","General software practices","continue","software development best practices {date}","standard_requirements" \ No newline at end of file diff --git a/src/bmm/workflows/3-solutioning/create-architecture/data/domain-complexity.csv b/src/bmm/workflows/3-solutioning/create-architecture/data/domain-complexity.csv index 0f1726a7..d619659e 100644 --- a/src/bmm/workflows/3-solutioning/create-architecture/data/domain-complexity.csv +++ b/src/bmm/workflows/3-solutioning/create-architecture/data/domain-complexity.csv @@ -8,4 +8,6 @@ productivity,"productivity,workflow,tasks,management,business,tools",medium,stan media,"content,media,video,audio,streaming,broadcast",high,advanced,"CDN architecture, video encoding, streaming protocols, content delivery" iot,"IoT,sensors,devices,embedded,smart,connected",high,advanced,"device communication, real-time data processing, edge computing, security" government,"government,civic,public,admin,policy,regulation",high,enhanced,"accessibility standards, security clearance, data privacy, audit trails" +process_control,"industrial automation,process control,PLC,SCADA,DCS,HMI,operational technology,control system,cyberphysical,MES,instrumentation,I&C,P&ID",high,advanced,"industrial process control architecture, SCADA system design, OT cybersecurity architecture, real-time control systems" +building_automation,"building automation,BAS,BMS,HVAC,smart building,fire alarm,fire protection,fire suppression,life safety,elevator,DDC,access control,sequence of operations,commissioning",high,advanced,"building automation architecture, BACnet integration patterns, smart building design, building management system security" gaming,"game,gaming,multiplayer,real-time,interactive,entertainment",high,advanced,"real-time multiplayer, game engine architecture, matchmaking, leaderboards" \ No newline at end of file From 5b80649d3a0205c6251a17a077bba821e3bc270a Mon Sep 17 00:00:00 2001 From: Davor Racic Date: Wed, 4 Feb 2026 00:36:54 +0100 Subject: [PATCH 4/4] fix(installer): Multiple installer fixes (#1492) * fix: support CRLF line endings and add task/tool templates for all IDEs * fix: preserve file extensions in IDE task/tool paths and update BMAD branding * fix: double extension issue in wrapper filename generation * fix: correct path handling and variable reference in task/tool command generator * fix: change default BMAD folder name from 'bmad' to '_bmad' across all IDE components * refactor: centralize BMAD_FOLDER_NAME constant in path-utils * fix: Replace the rest of BMAD_FOLDER magic values * fix: add safety checks for setBmadFolderName method calls in IdeManager * fix: convert absolute paths to relative in task-tool-command-generator * fix: support .xml task files in bmad-artifacts task discovery * fix: skip internal tasks in manifest generation and IDE command discovery * fix: skip empty artifact_types targets and remove unused vscode_settings target * fix: skip internal tools in manifest generation and improve Windows path handling in command generator * fix: use csv-parse library for proper CSV handling in manifest generation * refactor: extract CSV text cleaning to reusable method in manifest generator * fix: normalize path separators to forward slashes in agent file copying for cross-platform compatibility --------- Co-authored-by: Alex Verkhovsky Co-authored-by: Brian --- src/core/tasks/workflow.xml | 2 +- .../lib/core/dependency-resolver.js | 2 +- tools/cli/installers/lib/core/installer.js | 4 +- .../installers/lib/core/manifest-generator.js | 210 +++++++++++------- tools/cli/installers/lib/custom/handler.js | 2 +- tools/cli/installers/lib/ide/_base-ide.js | 10 +- .../cli/installers/lib/ide/_config-driven.js | 88 +++++++- tools/cli/installers/lib/ide/codex.js | 8 +- tools/cli/installers/lib/ide/manager.js | 10 +- .../installers/lib/ide/platform-codes.yaml | 3 - .../lib/ide/shared/agent-command-generator.js | 4 +- .../lib/ide/shared/bmad-artifacts.js | 17 +- .../installers/lib/ide/shared/path-utils.js | 11 +- .../ide/shared/task-tool-command-generator.js | 122 +++++++++- .../ide/shared/workflow-command-generator.js | 4 +- .../ide/templates/combined/default-task.md | 10 + .../ide/templates/combined/default-tool.md | 10 + .../ide/templates/combined/gemini-task.toml | 11 + .../ide/templates/combined/gemini-tool.toml | 11 + tools/cli/installers/lib/modules/manager.js | 5 +- tools/cli/lib/agent/installer.js | 2 +- 21 files changed, 423 insertions(+), 123 deletions(-) create mode 100644 tools/cli/installers/lib/ide/templates/combined/default-task.md create mode 100644 tools/cli/installers/lib/ide/templates/combined/default-tool.md create mode 100644 tools/cli/installers/lib/ide/templates/combined/gemini-task.toml create mode 100644 tools/cli/installers/lib/ide/templates/combined/gemini-tool.toml diff --git a/src/core/tasks/workflow.xml b/src/core/tasks/workflow.xml index fcf6f96b..8c55ec37 100644 --- a/src/core/tasks/workflow.xml +++ b/src/core/tasks/workflow.xml @@ -1,4 +1,4 @@ - + Execute given workflow by loading its configuration, following instructions, and producing output diff --git a/tools/cli/installers/lib/core/dependency-resolver.js b/tools/cli/installers/lib/core/dependency-resolver.js index 317b07f8..ee8a8a12 100644 --- a/tools/cli/installers/lib/core/dependency-resolver.js +++ b/tools/cli/installers/lib/core/dependency-resolver.js @@ -146,7 +146,7 @@ class DependencyResolver { const content = await fs.readFile(file.path, 'utf8'); // Parse YAML frontmatter for explicit dependencies - const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (frontmatterMatch) { try { // Pre-process to handle backticks in YAML values diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js index a14c3d19..cb146270 100644 --- a/tools/cli/installers/lib/core/installer.js +++ b/tools/cli/installers/lib/core/installer.js @@ -17,9 +17,7 @@ const { ManifestGenerator } = require('./manifest-generator'); const { IdeConfigManager } = require('./ide-config-manager'); const { CustomHandler } = require('../custom/handler'); const prompts = require('../../../lib/prompts'); - -// BMAD installation folder name - this is constant and should never change -const BMAD_FOLDER_NAME = '_bmad'; +const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils'); class Installer { constructor() { diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index 100164d5..fcaee8ad 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -2,6 +2,7 @@ const path = require('node:path'); const fs = require('fs-extra'); const yaml = require('yaml'); const crypto = require('node:crypto'); +const csv = require('csv-parse/sync'); const { getSourcePath, getModulePath } = require('../../../lib/project-root'); // Load package.json for version info @@ -21,6 +22,19 @@ class ManifestGenerator { this.selectedIdes = []; } + /** + * Clean text for CSV output by normalizing whitespace and escaping quotes + * @param {string} text - Text to clean + * @returns {string} Cleaned text safe for CSV + */ + cleanForCSV(text) { + if (!text) return ''; + return text + .trim() + .replaceAll(/\s+/g, ' ') // Normalize all whitespace (including newlines) to single space + .replaceAll('"', '""'); // Escape quotes for CSV + } + /** * Generate all manifests for the installation * @param {string} bmadDir - _bmad @@ -161,7 +175,7 @@ class ManifestGenerator { workflow = yaml.parse(content); } else { // Parse MD workflow with YAML frontmatter - const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (!frontmatterMatch) { if (debug) { console.log(`[DEBUG] Skipped (no frontmatter): ${fullPath}`); @@ -201,7 +215,7 @@ class ManifestGenerator { // Workflows with standalone: false are filtered out above workflows.push({ name: workflow.name, - description: workflow.description.replaceAll('"', '""'), // Escape quotes for CSV + description: this.cleanForCSV(workflow.description), module: moduleName, path: installPath, }); @@ -319,24 +333,15 @@ class ManifestGenerator { const agentName = entry.name.replace('.md', ''); - // Helper function to clean and escape CSV content - const cleanForCSV = (text) => { - if (!text) return ''; - return text - .trim() - .replaceAll(/\s+/g, ' ') // Normalize whitespace - .replaceAll('"', '""'); // Escape quotes for CSV - }; - agents.push({ name: agentName, displayName: nameMatch ? nameMatch[1] : agentName, title: titleMatch ? titleMatch[1] : '', icon: iconMatch ? iconMatch[1] : '', - role: roleMatch ? cleanForCSV(roleMatch[1]) : '', - identity: identityMatch ? cleanForCSV(identityMatch[1]) : '', - communicationStyle: styleMatch ? cleanForCSV(styleMatch[1]) : '', - principles: principlesMatch ? cleanForCSV(principlesMatch[1]) : '', + role: roleMatch ? this.cleanForCSV(roleMatch[1]) : '', + identity: identityMatch ? this.cleanForCSV(identityMatch[1]) : '', + communicationStyle: styleMatch ? this.cleanForCSV(styleMatch[1]) : '', + principles: principlesMatch ? this.cleanForCSV(principlesMatch[1]) : '', module: moduleName, path: installPath, }); @@ -385,6 +390,11 @@ class ManifestGenerator { const filePath = path.join(dirPath, file); const content = await fs.readFile(filePath, 'utf8'); + // Skip internal/engine files (not user-facing tasks) + if (content.includes('internal="true"')) { + continue; + } + let name = file.replace(/\.(xml|md)$/, ''); let displayName = name; let description = ''; @@ -392,13 +402,13 @@ class ManifestGenerator { if (file.endsWith('.md')) { // Parse YAML frontmatter for .md tasks - const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (frontmatterMatch) { try { const frontmatter = yaml.parse(frontmatterMatch[1]); name = frontmatter.name || name; displayName = frontmatter.displayName || frontmatter.name || name; - description = frontmatter.description || ''; + description = this.cleanForCSV(frontmatter.description || ''); standalone = frontmatter.standalone === true || frontmatter.standalone === 'true'; } catch { // If YAML parsing fails, use defaults @@ -411,7 +421,7 @@ class ManifestGenerator { const descMatch = content.match(/description="([^"]+)"/); const objMatch = content.match(/([^<]+)<\/objective>/); - description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''; + description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''); const standaloneMatch = content.match(/]+standalone="true"/); standalone = !!standaloneMatch; @@ -424,7 +434,7 @@ class ManifestGenerator { tasks.push({ name: name, displayName: displayName, - description: description.replaceAll('"', '""'), + description: description, module: moduleName, path: installPath, standalone: standalone, @@ -474,6 +484,11 @@ class ManifestGenerator { const filePath = path.join(dirPath, file); const content = await fs.readFile(filePath, 'utf8'); + // Skip internal tools (same as tasks) + if (content.includes('internal="true"')) { + continue; + } + let name = file.replace(/\.(xml|md)$/, ''); let displayName = name; let description = ''; @@ -481,13 +496,13 @@ class ManifestGenerator { if (file.endsWith('.md')) { // Parse YAML frontmatter for .md tools - const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (frontmatterMatch) { try { const frontmatter = yaml.parse(frontmatterMatch[1]); name = frontmatter.name || name; displayName = frontmatter.displayName || frontmatter.name || name; - description = frontmatter.description || ''; + description = this.cleanForCSV(frontmatter.description || ''); standalone = frontmatter.standalone === true || frontmatter.standalone === 'true'; } catch { // If YAML parsing fails, use defaults @@ -500,7 +515,7 @@ class ManifestGenerator { const descMatch = content.match(/description="([^"]+)"/); const objMatch = content.match(/([^<]+)<\/objective>/); - description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''; + description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''); const standaloneMatch = content.match(/]+standalone="true"/); standalone = !!standaloneMatch; @@ -513,7 +528,7 @@ class ManifestGenerator { tools.push({ name: name, displayName: displayName, - description: description.replaceAll('"', '""'), + description: description, module: moduleName, path: installPath, standalone: standalone, @@ -773,30 +788,23 @@ class ManifestGenerator { */ async writeAgentManifest(cfgDir) { const csvPath = path.join(cfgDir, 'agent-manifest.csv'); + const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`; // Read existing manifest to preserve entries const existingEntries = new Map(); if (await fs.pathExists(csvPath)) { const content = await fs.readFile(csvPath, 'utf8'); - const lines = content.split('\n').filter((line) => line.trim()); - - // Skip header - for (let i = 1; i < lines.length; i++) { - const line = lines[i]; - if (line) { - // Parse CSV (simple parsing assuming no commas in quoted fields) - const parts = line.split('","'); - if (parts.length >= 11) { - const name = parts[0].replace(/^"/, ''); - const module = parts[8]; - existingEntries.set(`${module}:${name}`, line); - } - } + const records = csv.parse(content, { + columns: true, + skip_empty_lines: true, + }); + for (const record of records) { + existingEntries.set(`${record.module}:${record.name}`, record); } } // Create CSV header with persona fields - let csv = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path\n'; + let csvContent = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path\n'; // Combine existing and new agents, preferring new data for duplicates const allAgents = new Map(); @@ -809,18 +817,38 @@ class ManifestGenerator { // Add/update new agents for (const agent of this.agents) { const key = `${agent.module}:${agent.name}`; - allAgents.set( - key, - `"${agent.name}","${agent.displayName}","${agent.title}","${agent.icon}","${agent.role}","${agent.identity}","${agent.communicationStyle}","${agent.principles}","${agent.module}","${agent.path}"`, - ); + allAgents.set(key, { + name: agent.name, + displayName: agent.displayName, + title: agent.title, + icon: agent.icon, + role: agent.role, + identity: agent.identity, + communicationStyle: agent.communicationStyle, + principles: agent.principles, + module: agent.module, + path: agent.path, + }); } // Write all agents - for (const [, value] of allAgents) { - csv += value + '\n'; + for (const [, record] of allAgents) { + const row = [ + escapeCsv(record.name), + escapeCsv(record.displayName), + escapeCsv(record.title), + escapeCsv(record.icon), + escapeCsv(record.role), + escapeCsv(record.identity), + escapeCsv(record.communicationStyle), + escapeCsv(record.principles), + escapeCsv(record.module), + escapeCsv(record.path), + ].join(','); + csvContent += row + '\n'; } - await fs.writeFile(csvPath, csv); + await fs.writeFile(csvPath, csvContent); return csvPath; } @@ -830,30 +858,23 @@ class ManifestGenerator { */ async writeTaskManifest(cfgDir) { const csvPath = path.join(cfgDir, 'task-manifest.csv'); + const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`; // Read existing manifest to preserve entries const existingEntries = new Map(); if (await fs.pathExists(csvPath)) { const content = await fs.readFile(csvPath, 'utf8'); - const lines = content.split('\n').filter((line) => line.trim()); - - // Skip header - for (let i = 1; i < lines.length; i++) { - const line = lines[i]; - if (line) { - // Parse CSV (simple parsing assuming no commas in quoted fields) - const parts = line.split('","'); - if (parts.length >= 6) { - const name = parts[0].replace(/^"/, ''); - const module = parts[3]; - existingEntries.set(`${module}:${name}`, line); - } - } + const records = csv.parse(content, { + columns: true, + skip_empty_lines: true, + }); + for (const record of records) { + existingEntries.set(`${record.module}:${record.name}`, record); } } // Create CSV header with standalone column - let csv = 'name,displayName,description,module,path,standalone\n'; + let csvContent = 'name,displayName,description,module,path,standalone\n'; // Combine existing and new tasks const allTasks = new Map(); @@ -866,15 +887,30 @@ class ManifestGenerator { // Add/update new tasks for (const task of this.tasks) { const key = `${task.module}:${task.name}`; - allTasks.set(key, `"${task.name}","${task.displayName}","${task.description}","${task.module}","${task.path}","${task.standalone}"`); + allTasks.set(key, { + name: task.name, + displayName: task.displayName, + description: task.description, + module: task.module, + path: task.path, + standalone: task.standalone, + }); } // Write all tasks - for (const [, value] of allTasks) { - csv += value + '\n'; + for (const [, record] of allTasks) { + const row = [ + escapeCsv(record.name), + escapeCsv(record.displayName), + escapeCsv(record.description), + escapeCsv(record.module), + escapeCsv(record.path), + escapeCsv(record.standalone), + ].join(','); + csvContent += row + '\n'; } - await fs.writeFile(csvPath, csv); + await fs.writeFile(csvPath, csvContent); return csvPath; } @@ -884,30 +920,23 @@ class ManifestGenerator { */ async writeToolManifest(cfgDir) { const csvPath = path.join(cfgDir, 'tool-manifest.csv'); + const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`; // Read existing manifest to preserve entries const existingEntries = new Map(); if (await fs.pathExists(csvPath)) { const content = await fs.readFile(csvPath, 'utf8'); - const lines = content.split('\n').filter((line) => line.trim()); - - // Skip header - for (let i = 1; i < lines.length; i++) { - const line = lines[i]; - if (line) { - // Parse CSV (simple parsing assuming no commas in quoted fields) - const parts = line.split('","'); - if (parts.length >= 6) { - const name = parts[0].replace(/^"/, ''); - const module = parts[3]; - existingEntries.set(`${module}:${name}`, line); - } - } + const records = csv.parse(content, { + columns: true, + skip_empty_lines: true, + }); + for (const record of records) { + existingEntries.set(`${record.module}:${record.name}`, record); } } // Create CSV header with standalone column - let csv = 'name,displayName,description,module,path,standalone\n'; + let csvContent = 'name,displayName,description,module,path,standalone\n'; // Combine existing and new tools const allTools = new Map(); @@ -920,15 +949,30 @@ class ManifestGenerator { // Add/update new tools for (const tool of this.tools) { const key = `${tool.module}:${tool.name}`; - allTools.set(key, `"${tool.name}","${tool.displayName}","${tool.description}","${tool.module}","${tool.path}","${tool.standalone}"`); + allTools.set(key, { + name: tool.name, + displayName: tool.displayName, + description: tool.description, + module: tool.module, + path: tool.path, + standalone: tool.standalone, + }); } // Write all tools - for (const [, value] of allTools) { - csv += value + '\n'; + for (const [, record] of allTools) { + const row = [ + escapeCsv(record.name), + escapeCsv(record.displayName), + escapeCsv(record.description), + escapeCsv(record.module), + escapeCsv(record.path), + escapeCsv(record.standalone), + ].join(','); + csvContent += row + '\n'; } - await fs.writeFile(csvPath, csv); + await fs.writeFile(csvPath, csvContent); return csvPath; } diff --git a/tools/cli/installers/lib/custom/handler.js b/tools/cli/installers/lib/custom/handler.js index c8aa52ee..8c730cf3 100644 --- a/tools/cli/installers/lib/custom/handler.js +++ b/tools/cli/installers/lib/custom/handler.js @@ -297,7 +297,7 @@ class CustomHandler { const agentFiles = await this.findFilesRecursively(sourceAgentsPath, ['.agent.yaml']); for (const agentFile of agentFiles) { - const relativePath = path.relative(sourceAgentsPath, agentFile); + const relativePath = path.relative(sourceAgentsPath, agentFile).split(path.sep).join('/'); const targetDir = path.join(targetAgentsPath, path.dirname(relativePath)); await fs.ensureDir(targetDir); diff --git a/tools/cli/installers/lib/ide/_base-ide.js b/tools/cli/installers/lib/ide/_base-ide.js index b16ee518..4ae11677 100644 --- a/tools/cli/installers/lib/ide/_base-ide.js +++ b/tools/cli/installers/lib/ide/_base-ide.js @@ -3,6 +3,7 @@ const fs = require('fs-extra'); const chalk = require('chalk'); const { XmlHandler } = require('../../../lib/xml-handler'); const { getSourcePath } = require('../../../lib/project-root'); +const { BMAD_FOLDER_NAME } = require('./shared/path-utils'); /** * Base class for IDE-specific setup @@ -18,7 +19,7 @@ class BaseIdeSetup { this.configFile = null; // Override in subclasses when detection is file-based this.detectionPaths = []; // Additional paths that indicate the IDE is configured this.xmlHandler = new XmlHandler(); - this.bmadFolderName = 'bmad'; // Default, can be overridden + this.bmadFolderName = BMAD_FOLDER_NAME; // Default, can be overridden } /** @@ -57,7 +58,7 @@ class BaseIdeSetup { if (this.configDir) { const configPath = path.join(projectDir, this.configDir); if (await fs.pathExists(configPath)) { - const bmadRulesPath = path.join(configPath, 'bmad'); + const bmadRulesPath = path.join(configPath, BMAD_FOLDER_NAME); if (await fs.pathExists(bmadRulesPath)) { await fs.remove(bmadRulesPath); console.log(chalk.dim(`Removed ${this.name} BMAD configuration`)); @@ -445,6 +446,11 @@ class BaseIdeSetup { try { const content = await fs.readFile(fullPath, 'utf8'); + // Skip internal/engine files (not user-facing tasks/tools) + if (content.includes('internal="true"')) { + continue; + } + // Check for standalone="true" in XML files if (entry.name.endsWith('.xml')) { // Look for standalone="true" in the opening tag (task or tool) diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index 87be7300..48688926 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -66,6 +66,13 @@ 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 + // 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 targetPath = path.join(projectDir, target_dir); await this.ensureDir(targetPath); @@ -86,10 +93,11 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { results.workflows = await this.writeWorkflowArtifacts(targetPath, artifacts, template_type, config); } - // Install tasks and tools + // 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(); - const taskToolResult = await taskToolGen.generateDashTaskToolCommands(projectDir, bmadDir, targetPath); + 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; } @@ -180,6 +188,53 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup { return count; } + /** + * Write task/tool artifacts to target directory using templates + * @param {string} targetPath - Target directory path + * @param {Array} artifacts - Task/tool artifacts + * @param {string} templateType - Template type to use + * @param {Object} config - Installation configuration + * @returns {Promise} Counts of tasks and tools written + */ + async writeTaskToolArtifacts(targetPath, artifacts, templateType, config = {}) { + let taskCount = 0; + let toolCount = 0; + + // Pre-load templates to avoid repeated file I/O in the loop + const taskTemplate = await this.loadTemplate(templateType, 'task', config, 'default-task'); + const toolTemplate = await this.loadTemplate(templateType, 'tool', config, 'default-tool'); + + const { artifact_types } = config; + + for (const artifact of artifacts) { + if (artifact.type !== 'task' && artifact.type !== 'tool') { + continue; + } + + // Skip if the specific artifact type is not requested in config + if (artifact_types) { + if (artifact.type === 'task' && !artifact_types.includes('tasks')) continue; + if (artifact.type === 'tool' && !artifact_types.includes('tools')) continue; + } + + // Use pre-loaded template based on artifact type + const { content: template, extension } = artifact.type === 'task' ? taskTemplate : toolTemplate; + + const content = this.renderTemplate(template, artifact); + const filename = this.generateFilename(artifact, artifact.type, extension); + const filePath = path.join(targetPath, filename); + await this.writeFile(filePath, content); + + if (artifact.type === 'task') { + taskCount++; + } else { + toolCount++; + } + } + + return { tasks: taskCount, tools: toolCount }; + } + /** * Load template based on type and configuration * @param {string} templateType - Template type (claude, windsurf, etc.) @@ -316,10 +371,24 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} renderTemplate(template, artifact) { // Use the appropriate path property based on artifact type let pathToUse = artifact.relativePath || ''; - if (artifact.type === 'agent-launcher') { - pathToUse = artifact.agentPath || artifact.relativePath || ''; - } else if (artifact.type === 'workflow-command') { - pathToUse = artifact.workflowPath || artifact.relativePath || ''; + switch (artifact.type) { + case 'agent-launcher': { + pathToUse = artifact.agentPath || artifact.relativePath || ''; + + break; + } + case 'workflow-command': { + pathToUse = artifact.workflowPath || artifact.relativePath || ''; + + break; + } + case 'task': + case 'tool': { + pathToUse = artifact.path || artifact.relativePath || ''; + + break; + } + // No default } let rendered = template @@ -351,8 +420,9 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} // Reuse central logic to ensure consistent naming conventions const standardName = toDashPath(artifact.relativePath); - // Clean up potential double extensions from source files (e.g. .yaml.md -> .md) - const baseName = standardName.replace(/\.(yaml|yml)\.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() + const baseName = standardName.replace(/\.(md|yaml|yml|json|xml|toml)\.md$/i, '.md'); // If using default markdown, preserve the bmad-agent- prefix for agents if (extension === '.md') { diff --git a/tools/cli/installers/lib/ide/codex.js b/tools/cli/installers/lib/ide/codex.js index 5cd503e2..29f595f6 100644 --- a/tools/cli/installers/lib/ide/codex.js +++ b/tools/cli/installers/lib/ide/codex.js @@ -104,7 +104,10 @@ class CodexSetup extends BaseIdeSetup { ); 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, @@ -116,7 +119,7 @@ class CodexSetup extends BaseIdeSetup { const workflowCount = await workflowGenerator.writeDashArtifacts(destDir, workflowArtifacts); // Also write tasks using underscore format - const ttGen = new TaskToolCommandGenerator(); + const ttGen = new TaskToolCommandGenerator(this.bmadFolderName); const tasksWritten = await ttGen.writeDashArtifacts(destDir, taskArtifacts); const written = agentCount + workflowCount + tasksWritten; @@ -214,7 +217,10 @@ class CodexSetup extends BaseIdeSetup { 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, diff --git a/tools/cli/installers/lib/ide/manager.js b/tools/cli/installers/lib/ide/manager.js index 2b68dfad..7d00588c 100644 --- a/tools/cli/installers/lib/ide/manager.js +++ b/tools/cli/installers/lib/ide/manager.js @@ -1,6 +1,7 @@ const fs = require('fs-extra'); const path = require('node:path'); const chalk = require('chalk'); +const { BMAD_FOLDER_NAME } = require('./shared/path-utils'); /** * IDE Manager - handles IDE-specific setup @@ -14,7 +15,7 @@ class IdeManager { constructor() { this.handlers = new Map(); this._initialized = false; - this.bmadFolderName = 'bmad'; // Default, can be overridden + this.bmadFolderName = BMAD_FOLDER_NAME; // Default, can be overridden } /** @@ -73,6 +74,9 @@ class IdeManager { if (HandlerClass) { const instance = new HandlerClass(); if (instance.name && typeof instance.name === 'string') { + if (typeof instance.setBmadFolderName === 'function') { + instance.setBmadFolderName(this.bmadFolderName); + } this.handlers.set(instance.name, instance); } } @@ -100,7 +104,9 @@ class IdeManager { if (!platformInfo.installer) continue; const handler = new ConfigDrivenIdeSetup(platformCode, platformInfo); - handler.setBmadFolderName(this.bmadFolderName); + if (typeof handler.setBmadFolderName === 'function') { + handler.setBmadFolderName(this.bmadFolderName); + } this.handlers.set(platformCode, handler); } } diff --git a/tools/cli/installers/lib/ide/platform-codes.yaml b/tools/cli/installers/lib/ide/platform-codes.yaml index 6a9078a8..2ca32aed 100644 --- a/tools/cli/installers/lib/ide/platform-codes.yaml +++ b/tools/cli/installers/lib/ide/platform-codes.yaml @@ -94,9 +94,6 @@ platforms: - target_dir: .github/agents template_type: copilot_agents artifact_types: [agents] - - target_dir: .vscode - template_type: vscode_settings - artifact_types: [] iflow: name: "iFlow" diff --git a/tools/cli/installers/lib/ide/shared/agent-command-generator.js b/tools/cli/installers/lib/ide/shared/agent-command-generator.js index dec22a12..caf60614 100644 --- a/tools/cli/installers/lib/ide/shared/agent-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/agent-command-generator.js @@ -1,14 +1,14 @@ const path = require('node:path'); const fs = require('fs-extra'); const chalk = require('chalk'); -const { toColonPath, toDashPath, customAgentColonName, customAgentDashName } = require('./path-utils'); +const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils'); /** * Generates launcher command files for each agent * Similar to WorkflowCommandGenerator but for agents */ class AgentCommandGenerator { - constructor(bmadFolderName = 'bmad') { + constructor(bmadFolderName = BMAD_FOLDER_NAME) { this.templatePath = path.join(__dirname, '../templates/agent-command-template.md'); this.bmadFolderName = bmadFolderName; } diff --git a/tools/cli/installers/lib/ide/shared/bmad-artifacts.js b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js index e88a64f5..7bcfd6a7 100644 --- a/tools/cli/installers/lib/ide/shared/bmad-artifacts.js +++ b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js @@ -141,13 +141,24 @@ async function getTasksFromDir(dirPath, moduleName) { const files = await fs.readdir(dirPath); for (const file of files) { - if (!file.endsWith('.md')) { + // Include both .md and .xml task files + if (!file.endsWith('.md') && !file.endsWith('.xml')) { continue; } + const filePath = path.join(dirPath, file); + const content = await fs.readFile(filePath, 'utf8'); + + // Skip internal/engine files (not user-facing tasks) + if (content.includes('internal="true"')) { + continue; + } + + // Remove extension to get task name + const ext = file.endsWith('.xml') ? '.xml' : '.md'; tasks.push({ - path: path.join(dirPath, file), - name: file.replace('.md', ''), + path: filePath, + name: file.replace(ext, ''), module: moduleName, }); } diff --git a/tools/cli/installers/lib/ide/shared/path-utils.js b/tools/cli/installers/lib/ide/shared/path-utils.js index d6ad00f5..51966923 100644 --- a/tools/cli/installers/lib/ide/shared/path-utils.js +++ b/tools/cli/installers/lib/ide/shared/path-utils.js @@ -18,6 +18,9 @@ const TYPE_SEGMENTS = ['workflows', 'tasks', 'tools']; const AGENT_SEGMENT = 'agents'; +// BMAD installation folder name - centralized constant for all installers +const BMAD_FOLDER_NAME = '_bmad'; + /** * Convert hierarchical path to flat dash-separated name (NEW STANDARD) * Converts: 'bmm', 'agents', 'pm' โ†’ 'bmad-agent-bmm-pm.md' @@ -59,7 +62,9 @@ function toDashPath(relativePath) { return 'bmad-unknown.md'; } - const withoutExt = relativePath.replace('.md', ''); + // Strip common file extensions to avoid double extensions in generated filenames + // e.g., 'create-story.xml' โ†’ 'create-story', 'workflow.yaml' โ†’ 'workflow' + const withoutExt = relativePath.replace(/\.(md|yaml|yml|json|xml|toml)$/i, ''); const parts = withoutExt.split(/[/\\]/); const module = parts[0]; @@ -183,7 +188,8 @@ function toUnderscoreName(module, type, name) { * @deprecated Use toDashPath instead */ function toUnderscorePath(relativePath) { - const withoutExt = relativePath.replace('.md', ''); + // Strip common file extensions (same as toDashPath for consistency) + const withoutExt = relativePath.replace(/\.(md|yaml|yml|json|xml|toml)$/i, ''); const parts = withoutExt.split(/[/\\]/); const module = parts[0]; @@ -289,4 +295,5 @@ module.exports = { TYPE_SEGMENTS, AGENT_SEGMENT, + BMAD_FOLDER_NAME, }; diff --git a/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js b/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js index a0c4bcf8..60eb5468 100644 --- a/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js @@ -2,12 +2,98 @@ const path = require('node:path'); const fs = require('fs-extra'); const csv = require('csv-parse/sync'); const chalk = require('chalk'); -const { toColonName, toColonPath, toDashPath } = require('./path-utils'); +const { toColonName, toColonPath, toDashPath, BMAD_FOLDER_NAME } = require('./path-utils'); /** * Generates command files for standalone tasks and tools */ class TaskToolCommandGenerator { + /** + * @param {string} bmadFolderName - Name of the BMAD folder for template rendering (default: '_bmad') + * Note: This parameter is accepted for API consistency with AgentCommandGenerator and + * WorkflowCommandGenerator, but is not used for path stripping. The manifest always stores + * filesystem paths with '_bmad/' prefix (the actual folder name), while bmadFolderName is + * used for template placeholder rendering ({{bmadFolderName}}). + */ + constructor(bmadFolderName = BMAD_FOLDER_NAME) { + this.bmadFolderName = bmadFolderName; + } + + /** + * Collect task and tool artifacts for IDE installation + * @param {string} bmadDir - BMAD installation directory + * @returns {Promise} Artifacts array with metadata + */ + async collectTaskToolArtifacts(bmadDir) { + const tasks = await this.loadTaskManifest(bmadDir); + const tools = await this.loadToolManifest(bmadDir); + + // Filter to only standalone items + const standaloneTasks = tasks ? tasks.filter((t) => t.standalone === 'true' || t.standalone === true) : []; + const standaloneTools = tools ? tools.filter((t) => t.standalone === 'true' || t.standalone === true) : []; + + const artifacts = []; + const bmadPrefix = `${BMAD_FOLDER_NAME}/`; + + // Collect task artifacts + for (const task of standaloneTasks) { + let taskPath = (task.path || '').replaceAll('\\', '/'); + // Convert absolute paths to relative paths + if (path.isAbsolute(taskPath)) { + taskPath = path.relative(bmadDir, taskPath).replaceAll('\\', '/'); + } + // Remove _bmad/ prefix if present to get relative path within bmad folder + if (taskPath.startsWith(bmadPrefix)) { + taskPath = taskPath.slice(bmadPrefix.length); + } + + const taskExt = path.extname(taskPath) || '.md'; + artifacts.push({ + type: 'task', + name: task.name, + displayName: task.displayName || task.name, + description: task.description || `Execute ${task.displayName || task.name}`, + module: task.module, + // Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows) + relativePath: `${task.module}/tasks/${task.name}${taskExt}`, + path: taskPath, + }); + } + + // Collect tool artifacts + for (const tool of standaloneTools) { + let toolPath = (tool.path || '').replaceAll('\\', '/'); + // Convert absolute paths to relative paths + if (path.isAbsolute(toolPath)) { + toolPath = path.relative(bmadDir, toolPath).replaceAll('\\', '/'); + } + // Remove _bmad/ prefix if present to get relative path within bmad folder + if (toolPath.startsWith(bmadPrefix)) { + toolPath = toolPath.slice(bmadPrefix.length); + } + + const toolExt = path.extname(toolPath) || '.md'; + artifacts.push({ + type: 'tool', + name: tool.name, + displayName: tool.displayName || tool.name, + description: tool.description || `Execute ${tool.displayName || tool.name}`, + module: tool.module, + // Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows) + relativePath: `${tool.module}/tools/${tool.name}${toolExt}`, + path: toolPath, + }); + } + + return { + artifacts, + counts: { + tasks: standaloneTasks.length, + tools: standaloneTools.length, + }, + }; + } + /** * Generate task and tool commands from manifest CSVs * @param {string} projectDir - Project directory @@ -65,9 +151,35 @@ class TaskToolCommandGenerator { const description = item.description || `Execute ${item.displayName || item.name}`; // Convert path to use {project-root} placeholder + // Handle undefined/missing path by constructing from module and name let itemPath = item.path; - if (itemPath && typeof itemPath === 'string' && itemPath.startsWith('bmad/')) { - itemPath = `{project-root}/${itemPath}`; + if (!itemPath || typeof itemPath !== 'string') { + // Fallback: construct path from module and name if path is missing + const typePlural = type === 'task' ? 'tasks' : 'tools'; + itemPath = `{project-root}/${this.bmadFolderName}/${item.module}/${typePlural}/${item.name}.md`; + } else { + // Normalize path separators to forward slashes + itemPath = itemPath.replaceAll('\\', '/'); + + // Extract relative path from absolute paths (Windows or Unix) + // Look for _bmad/ or bmad/ in the path and extract everything after it + // Match patterns like: /_bmad/core/tasks/... or /bmad/core/tasks/... + // Use [/\\] to handle both Unix forward slashes and Windows backslashes, + // and also paths without a leading separator (e.g., C:/_bmad/...) + const bmadMatch = itemPath.match(/[/\\]_bmad[/\\](.+)$/) || itemPath.match(/[/\\]bmad[/\\](.+)$/); + if (bmadMatch) { + // Found /_bmad/ or /bmad/ - use relative path after it + itemPath = `{project-root}/${this.bmadFolderName}/${bmadMatch[1]}`; + } else if (itemPath.startsWith(`${BMAD_FOLDER_NAME}/`)) { + // Relative path starting with _bmad/ + itemPath = `{project-root}/${this.bmadFolderName}/${itemPath.slice(BMAD_FOLDER_NAME.length + 1)}`; + } else if (itemPath.startsWith('bmad/')) { + // Relative path starting with bmad/ + itemPath = `{project-root}/${this.bmadFolderName}/${itemPath.slice(5)}`; + } else if (!itemPath.startsWith('{project-root}')) { + // For other relative paths, prefix with project root and bmad folder + itemPath = `{project-root}/${this.bmadFolderName}/${itemPath}`; + } } return `--- @@ -187,7 +299,7 @@ Follow all instructions in the ${type} file exactly as written. // Generate command files for tasks for (const task of standaloneTasks) { const commandContent = this.generateCommandContent(task, 'task'); - // Use underscore format: bmad_bmm_name.md + // Use dash format: bmad-bmm-name.md const flatName = toDashPath(`${task.module}/tasks/${task.name}.md`); const commandPath = path.join(baseCommandsDir, flatName); await fs.ensureDir(path.dirname(commandPath)); @@ -198,7 +310,7 @@ Follow all instructions in the ${type} file exactly as written. // Generate command files for tools for (const tool of standaloneTools) { const commandContent = this.generateCommandContent(tool, 'tool'); - // Use underscore format: bmad_bmm_name.md + // Use dash format: bmad-bmm-name.md const flatName = toDashPath(`${tool.module}/tools/${tool.name}.md`); const commandPath = path.join(baseCommandsDir, flatName); await fs.ensureDir(path.dirname(commandPath)); diff --git a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js b/tools/cli/installers/lib/ide/shared/workflow-command-generator.js index 6dab1a3f..5a23fda2 100644 --- a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/workflow-command-generator.js @@ -2,13 +2,13 @@ const path = require('node:path'); const fs = require('fs-extra'); const csv = require('csv-parse/sync'); const chalk = require('chalk'); -const { toColonPath, toDashPath, customAgentColonName, customAgentDashName } = require('./path-utils'); +const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils'); /** * Generates command files for each workflow in the manifest */ class WorkflowCommandGenerator { - constructor(bmadFolderName = 'bmad') { + constructor(bmadFolderName = BMAD_FOLDER_NAME) { this.templatePath = path.join(__dirname, '../templates/workflow-command-template.md'); this.bmadFolderName = bmadFolderName; } diff --git a/tools/cli/installers/lib/ide/templates/combined/default-task.md b/tools/cli/installers/lib/ide/templates/combined/default-task.md new file mode 100644 index 00000000..b865d6ff --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/default-task.md @@ -0,0 +1,10 @@ +--- +name: '{{name}}' +description: '{{description}}' +--- + +# {{name}} + +Read the entire task file at: {project-root}/{{bmadFolderName}}/{{path}} + +Follow all instructions in the task file exactly as written. diff --git a/tools/cli/installers/lib/ide/templates/combined/default-tool.md b/tools/cli/installers/lib/ide/templates/combined/default-tool.md new file mode 100644 index 00000000..11c6aac8 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/default-tool.md @@ -0,0 +1,10 @@ +--- +name: '{{name}}' +description: '{{description}}' +--- + +# {{name}} + +Read the entire tool file at: {project-root}/{{bmadFolderName}}/{{path}} + +Follow all instructions in the tool file exactly as written. diff --git a/tools/cli/installers/lib/ide/templates/combined/gemini-task.toml b/tools/cli/installers/lib/ide/templates/combined/gemini-task.toml new file mode 100644 index 00000000..7d15e216 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/gemini-task.toml @@ -0,0 +1,11 @@ +description = "Executes the {{name}} task from the BMAD Method." +prompt = """ +Execute the BMAD '{{name}}' task. + +TASK INSTRUCTIONS: +1. LOAD the task file from {project-root}/{{bmadFolderName}}/{{path}} +2. READ its entire contents +3. FOLLOW every instruction precisely as specified + +TASK FILE: {project-root}/{{bmadFolderName}}/{{path}} +""" diff --git a/tools/cli/installers/lib/ide/templates/combined/gemini-tool.toml b/tools/cli/installers/lib/ide/templates/combined/gemini-tool.toml new file mode 100644 index 00000000..fc78c6b7 --- /dev/null +++ b/tools/cli/installers/lib/ide/templates/combined/gemini-tool.toml @@ -0,0 +1,11 @@ +description = "Executes the {{name}} tool from the BMAD Method." +prompt = """ +Execute the BMAD '{{name}}' tool. + +TOOL INSTRUCTIONS: +1. LOAD the tool file from {project-root}/{{bmadFolderName}}/{{path}} +2. READ its entire contents +3. FOLLOW every instruction precisely as specified + +TOOL FILE: {project-root}/{{bmadFolderName}}/{{path}} +""" diff --git a/tools/cli/installers/lib/modules/manager.js b/tools/cli/installers/lib/modules/manager.js index 60c087b1..1f523fba 100644 --- a/tools/cli/installers/lib/modules/manager.js +++ b/tools/cli/installers/lib/modules/manager.js @@ -7,6 +7,7 @@ const { XmlHandler } = require('../../../lib/xml-handler'); const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root'); const { filterCustomizationData } = require('../../../lib/agent/compiler'); const { ExternalModuleManager } = require('./external-manager'); +const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils'); /** * Manages the installation, updating, and removal of BMAD modules. @@ -27,7 +28,7 @@ const { ExternalModuleManager } = require('./external-manager'); class ModuleManager { constructor(options = {}) { this.xmlHandler = new XmlHandler(); - this.bmadFolderName = 'bmad'; // Default, can be overridden + this.bmadFolderName = BMAD_FOLDER_NAME; // Default, can be overridden this.customModulePaths = new Map(); // Initialize custom module paths this.externalModuleManager = new ExternalModuleManager(); // For external official modules } @@ -870,7 +871,7 @@ class ModuleManager { for (const agentFile of agentFiles) { if (!agentFile.endsWith('.agent.yaml')) continue; - const relativePath = path.relative(sourceAgentsPath, agentFile); + const relativePath = path.relative(sourceAgentsPath, agentFile).split(path.sep).join('/'); const targetDir = path.join(targetAgentsPath, path.dirname(relativePath)); await fs.ensureDir(targetDir); diff --git a/tools/cli/lib/agent/installer.js b/tools/cli/lib/agent/installer.js index b55502ed..a7650453 100644 --- a/tools/cli/lib/agent/installer.js +++ b/tools/cli/lib/agent/installer.js @@ -42,7 +42,7 @@ function findBmadConfig(startPath = process.cwd()) { * @returns {string} Resolved path */ function resolvePath(pathStr, context) { - return pathStr.replaceAll('{project-root}', context.projectRoot).replaceAll('{bmad-folder}', context_bmadFolder); + return pathStr.replaceAll('{project-root}', context.projectRoot).replaceAll('{bmad-folder}', context.bmadFolder); } /**