diff --git a/.gitignore b/.gitignore index 8421b6025..e3fe614fb 100644 --- a/.gitignore +++ b/.gitignore @@ -52,7 +52,7 @@ _bmad _bmad-output # Personal customization files (team files are committed, personal files are not) -_bmad/customizations/*.user.toml +_bmad/custom/*.user.yaml .clinerules # .augment/ is gitignored except tracked config files — add exceptions explicitly .augment/* diff --git a/docs/how-to/customize-bmad.md b/docs/how-to/customize-bmad.md index 4667202da..6828ba964 100644 --- a/docs/how-to/customize-bmad.md +++ b/docs/how-to/customize-bmad.md @@ -28,12 +28,12 @@ Every agent skill ships a `customize.yaml` file with its defaults. This file def ### Three-Layer Override Model ```text -Priority 1 (wins): _bmad/customizations/{skill-name}.user.yaml (personal, gitignored) -Priority 2: _bmad/customizations/{skill-name}.yaml (team/org, committed) +Priority 1 (wins): _bmad/custom/{skill-name}.user.yaml (personal, gitignored) +Priority 2: _bmad/custom/{skill-name}.yaml (team/org, committed) Priority 3 (last): skill's own customize.yaml (defaults) ``` -The `_bmad/customizations/` folder starts empty. Files only appear when someone actively customizes. +The `_bmad/custom/` folder starts empty. Files only appear when someone actively customizes. ### Merge Rules (per field) @@ -64,10 +64,10 @@ This file is the canonical schema. Every field you see is customizable. ### 2. Create Your Override File -Create the `_bmad/customizations/` directory in your project root if it doesn't exist. Then create a file named after the skill: +Create the `_bmad/custom/` directory in your project root if it doesn't exist. Then create a file named after the skill: ```text -_bmad/customizations/ +_bmad/custom/ bmad-agent-pm.yaml # team overrides (committed to git) bmad-agent-pm.user.yaml # personal preferences (gitignored) ``` @@ -83,7 +83,7 @@ Change any combination of name, title, icon, role, identity, communication style Team override (shallow merge on metadata): ```yaml -# _bmad/customizations/bmad-agent-pm.yaml +# _bmad/custom/bmad-agent-pm.yaml agent: metadata: @@ -133,7 +133,7 @@ Procedural startup steps the agent must execute before presenting its menu: agent: critical_actions: - "Scan {project-root}/docs/compliance/ and load any HIPAA-related documents as context." - - "Read {project-root}/_bmad/customizations/company-glossary.md if it exists." + - "Read {project-root}/_bmad/custom/company-glossary.md if it exists." ``` Critical actions append too. They run top-to-bottom on every activation. @@ -154,7 +154,7 @@ agent: - code: RC description: "Run compliance pre-check" prompt: | - Read {project-root}/_bmad/customizations/compliance-checklist.md + Read {project-root}/_bmad/custom/compliance-checklist.md and scan all documents in {planning_artifacts} against it. Report any gaps and cite the relevant regulatory section. ``` @@ -163,7 +163,7 @@ Items not listed in your override keep their defaults. #### Referencing Files -When a field's text needs to point at a file (in `memories`, `critical_actions`, or a menu item's `prompt`), use a full path rooted at `{project-root}`. Even if the file sits next to your override in `_bmad/customizations/`, spell out the full path: `{project-root}/_bmad/customizations/info.md`. The agent resolves `{project-root}` at runtime. +When a field's text needs to point at a file (in `memories`, `critical_actions`, or a menu item's `prompt`), use a full path rooted at `{project-root}`. Even if the file sits next to your override in `_bmad/custom/`, spell out the full path: `{project-root}/_bmad/custom/info.md`. The agent resolves `{project-root}` at runtime. ### 4. Personal vs Team @@ -172,7 +172,7 @@ When a field's text needs to point at a file (in `memories`, `critical_actions`, **Personal file** (`bmad-agent-pm.user.yaml`): Gitignored automatically. Use for nickname preferences, tone adjustments, personal workflows. ```yaml -# _bmad/customizations/bmad-agent-pm.user.yaml +# _bmad/custom/bmad-agent-pm.user.yaml agent: metadata: @@ -191,7 +191,7 @@ node {project-root}/_bmad/scripts/resolve-customization.js \ --key agent ``` -`--skill` points at the skill's installed directory (where `customize.yaml` lives). The skill name is derived from the directory's basename, and the script looks up `_bmad/customizations/{skill-name}.yaml` and `{skill-name}.user.yaml` automatically. +`--skill` points at the skill's installed directory (where `customize.yaml` lives). The skill name is derived from the directory's basename, and the script looks up `_bmad/custom/{skill-name}.yaml` and `{skill-name}.user.yaml` automatically. Useful invocations: @@ -221,7 +221,7 @@ Some workflows expose their own customization surface (output paths, review sett **Customization not appearing?** -- Verify your file is in `_bmad/customizations/` with the correct skill name +- Verify your file is in `_bmad/custom/` with the correct skill name - Check YAML indentation (spaces only, no tabs) and make sure block scalars (`|`) are correctly indented - For agents, customization lives under `agent:` -- keys written below it belong to that key until another top-level key begins - Remember `agent.persona` is replace-wholesale: include every persona field you want, not just the ones you're changing @@ -232,4 +232,4 @@ Some workflows expose their own customization surface (output paths, review sett **Need to reset?** -- Delete your override file from `_bmad/customizations/` -- the skill falls back to its built-in defaults +- Delete your override file from `_bmad/custom/` -- the skill falls back to its built-in defaults diff --git a/eslint.config.mjs b/eslint.config.mjs index 9282fdacb..1bf3e270e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -84,9 +84,9 @@ export default [ }, }, - // CLI scripts under tools/** and test/** + // CLI scripts under tools/**, test/**, and src/scripts/** { - files: ['tools/**/*.js', 'tools/**/*.mjs', 'test/**/*.js', 'test/**/*.mjs'], + files: ['tools/**/*.js', 'tools/**/*.mjs', 'test/**/*.js', 'test/**/*.mjs', 'src/scripts/**/*.js', 'src/scripts/**/*.mjs'], rules: { // Allow CommonJS patterns for Node CLI scripts 'unicorn/prefer-module': 'off', diff --git a/src/bmm-skills/1-analysis/bmad-agent-analyst/SKILL.md b/src/bmm-skills/1-analysis/bmad-agent-analyst/SKILL.md index 489f58aec..fc16273c7 100644 --- a/src/bmm-skills/1-analysis/bmad-agent-analyst/SKILL.md +++ b/src/bmm-skills/1-analysis/bmad-agent-analyst/SKILL.md @@ -15,7 +15,7 @@ description: Strategic business analyst and requirements expert. Use when the us Run: `node {project-root}/_bmad/scripts/resolve-customization.js --skill {skill-root} --key agent` -**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/customizations/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped). +**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/custom/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped). ### Step 2: Adopt Persona diff --git a/src/bmm-skills/1-analysis/bmad-agent-tech-writer/SKILL.md b/src/bmm-skills/1-analysis/bmad-agent-tech-writer/SKILL.md index 0e36ef4b3..782ab7441 100644 --- a/src/bmm-skills/1-analysis/bmad-agent-tech-writer/SKILL.md +++ b/src/bmm-skills/1-analysis/bmad-agent-tech-writer/SKILL.md @@ -15,7 +15,7 @@ description: Technical documentation specialist and knowledge curator. Use when Run: `node {project-root}/_bmad/scripts/resolve-customization.js --skill {skill-root} --key agent` -**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/customizations/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped). +**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/custom/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped). ### Step 2: Adopt Persona diff --git a/src/bmm-skills/2-plan-workflows/bmad-agent-pm/SKILL.md b/src/bmm-skills/2-plan-workflows/bmad-agent-pm/SKILL.md index 0f5e88956..76cc2758e 100644 --- a/src/bmm-skills/2-plan-workflows/bmad-agent-pm/SKILL.md +++ b/src/bmm-skills/2-plan-workflows/bmad-agent-pm/SKILL.md @@ -15,7 +15,7 @@ description: Product manager for PRD creation and requirements discovery. Use wh Run: `node {project-root}/_bmad/scripts/resolve-customization.js --skill {skill-root} --key agent` -**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/customizations/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped). +**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/custom/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped). ### Step 2: Adopt Persona diff --git a/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/SKILL.md b/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/SKILL.md index 124353fa8..d16c1f03a 100644 --- a/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/SKILL.md +++ b/src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer/SKILL.md @@ -15,7 +15,7 @@ description: UX designer and UI specialist. Use when the user asks to talk to Sa Run: `node {project-root}/_bmad/scripts/resolve-customization.js --skill {skill-root} --key agent` -**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/customizations/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped). +**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/custom/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped). ### Step 2: Adopt Persona diff --git a/src/bmm-skills/3-solutioning/bmad-agent-architect/SKILL.md b/src/bmm-skills/3-solutioning/bmad-agent-architect/SKILL.md index 35c3ee8c9..094a802d7 100644 --- a/src/bmm-skills/3-solutioning/bmad-agent-architect/SKILL.md +++ b/src/bmm-skills/3-solutioning/bmad-agent-architect/SKILL.md @@ -15,7 +15,7 @@ description: System architect and technical design leader. Use when the user ask Run: `node {project-root}/_bmad/scripts/resolve-customization.js --skill {skill-root} --key agent` -**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/customizations/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped). +**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/custom/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped). ### Step 2: Adopt Persona diff --git a/src/bmm-skills/4-implementation/bmad-agent-dev/SKILL.md b/src/bmm-skills/4-implementation/bmad-agent-dev/SKILL.md index 14bcec85f..0b27804f5 100644 --- a/src/bmm-skills/4-implementation/bmad-agent-dev/SKILL.md +++ b/src/bmm-skills/4-implementation/bmad-agent-dev/SKILL.md @@ -15,7 +15,7 @@ description: Senior software engineer for story execution and code implementatio Run: `node {project-root}/_bmad/scripts/resolve-customization.js --skill {skill-root} --key agent` -**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/customizations/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped). +**If the script fails**, resolve the `agent` block yourself from `customize.yaml`, with `{project-root}/_bmad/custom/{skill-name}.yaml` overriding, and `{skill-name}.user.yaml` overriding both (any missing file is skipped). ### Step 2: Adopt Persona diff --git a/src/scripts/resolve-customization.js b/src/scripts/resolve-customization.js index 8b4f581f5..ceda8de20 100644 --- a/src/scripts/resolve-customization.js +++ b/src/scripts/resolve-customization.js @@ -1,10 +1,9 @@ -#!/usr/bin/env node /** * Resolve customization for a BMad skill using three-layer YAML merge. * * Reads customization from three layers (highest priority first): - * 1. {project-root}/_bmad/customizations/{name}.user.yaml (personal, gitignored) - * 2. {project-root}/_bmad/customizations/{name}.yaml (team/org, committed) + * 1. {project-root}/_bmad/custom/{name}.user.yaml (personal, gitignored) + * 2. {project-root}/_bmad/custom/{name}.yaml (team/org, committed) * 3. {skill-root}/customize.yaml (skill defaults) * * Skill name is derived from the basename of the skill directory. @@ -36,10 +35,7 @@ const yaml = require('yaml'); function findProjectRoot(start) { let current = path.resolve(start); while (true) { - if ( - fs.existsSync(path.join(current, '_bmad')) || - fs.existsSync(path.join(current, '.git')) - ) { + if (fs.existsSync(path.join(current, '_bmad')) || fs.existsSync(path.join(current, '.git'))) { return current; } const parent = path.dirname(current); @@ -53,9 +49,9 @@ function loadYaml(filePath) { const raw = fs.readFileSync(filePath, 'utf8'); const parsed = yaml.parse(raw); return parsed && typeof parsed === 'object' ? parsed : {}; - } catch (err) { - if (err.code === 'ENOENT') return {}; - process.stderr.write(`warning: failed to parse ${filePath}: ${err.message}\n`); + } catch (error) { + if (error.code === 'ENOENT') return {}; + process.stderr.write(`warning: failed to parse ${filePath}: ${error.message}\n`); return {}; } } @@ -64,10 +60,6 @@ function isPlainObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); } -function isMenuArray(value) { - return Array.isArray(value) && value.length > 0 && value.every((item) => isPlainObject(item)); -} - function mergeByKey(base, override, keyName) { const result = []; const indexByKey = new Map(); @@ -157,12 +149,8 @@ function mergeAgentBlock(base, override) { // Merge by `code` when both sides use it; otherwise append. const baseArr = Array.isArray(baseVal) ? baseVal : []; const overArr = Array.isArray(overVal) ? overVal : []; - const anyHasCode = [...baseArr, ...overArr].some( - (item) => isPlainObject(item) && item.code !== undefined, - ); - mergedAgent[key] = anyHasCode - ? mergeByKey(baseArr, overArr, 'code') - : appendArrays(baseArr, overArr); + const anyHasCode = [...baseArr, ...overArr].some((item) => isPlainObject(item) && item.code !== undefined); + mergedAgent[key] = anyHasCode ? mergeByKey(baseArr, overArr, 'code') : appendArrays(baseArr, overArr); break; } default: { @@ -185,7 +173,7 @@ function extractKey(data, dottedKey) { if (isPlainObject(current) && part in current) { current = current[part]; } else { - return undefined; + return; } } return current; @@ -195,15 +183,26 @@ function parseArgs(argv) { const args = { skill: null, keys: [] }; for (let i = 0; i < argv.length; i++) { const a = argv[i]; - if (a === '--skill' || a === '-s') { - args.skill = argv[++i]; - } else if (a === '--key' || a === '-k') { - args.keys.push(argv[++i]); - } else if (a === '--help' || a === '-h') { - printHelp(); - process.exit(0); - } else { - process.stderr.write(`warning: unknown argument: ${a}\n`); + switch (a) { + case '--skill': + case '-s': { + args.skill = argv[++i]; + break; + } + case '--key': + case '-k': { + args.keys.push(argv[++i]); + break; + } + case '--help': + case '-h': { + printHelp(); + process.exit(0); + break; + } + default: { + process.stderr.write(`warning: unknown argument: ${a}\n`); + } } } return args; @@ -241,15 +240,14 @@ function main() { process.stderr.write(`warning: no defaults found at ${defaultsPath}\n`); } - const projectRoot = - findProjectRoot(process.cwd()) || findProjectRoot(skillDir); + const projectRoot = findProjectRoot(process.cwd()) || findProjectRoot(skillDir); let team = {}; let user = {}; if (projectRoot) { - const customizationsDir = path.join(projectRoot, '_bmad', 'customizations'); - team = loadYaml(path.join(customizationsDir, `${skillName}.yaml`)); - user = loadYaml(path.join(customizationsDir, `${skillName}.user.yaml`)); + const customDir = path.join(projectRoot, '_bmad', 'custom'); + team = loadYaml(path.join(customDir, `${skillName}.yaml`)); + user = loadYaml(path.join(customDir, `${skillName}.user.yaml`)); } let merged = mergeAgentBlock(defaults, team); diff --git a/tools/validate-file-refs.js b/tools/validate-file-refs.js index b1261a4d7..7e137763c 100644 --- a/tools/validate-file-refs.js +++ b/tools/validate-file-refs.js @@ -80,7 +80,7 @@ function escapeTableCell(str) { } // Path prefixes/patterns that only exist in installed structure, not in source -const INSTALL_ONLY_PATHS = ['_config/', 'customizations/']; +const INSTALL_ONLY_PATHS = ['_config/', 'custom/']; // Files that are generated at install time and don't exist in the source tree const INSTALL_GENERATED_FILES = ['config.yaml', 'config.user.yaml'];