refactor(skills): rename _bmad/customizations/ to _bmad/custom/

Shorter, cleaner path for user override files. Updates:
- resolver script path lookup + docstring
- all 6 agent SKILL.md fallback paths
- how-to doc examples
- .gitignore (also corrects stale .toml to .yaml)
- validator INSTALL_ONLY_PATHS entry

Extends ESLint CLI-script overrides to cover src/scripts/** and drops an
unused helper from the resolver to satisfy the pre-commit lint gate.
This commit is contained in:
Brian Madison 2026-04-18 11:43:22 -05:00
parent 64d3f02615
commit 4a88d321f7
11 changed files with 56 additions and 58 deletions

2
.gitignore vendored
View File

@ -52,7 +52,7 @@ _bmad
_bmad-output _bmad-output
# Personal customization files (team files are committed, personal files are not) # Personal customization files (team files are committed, personal files are not)
_bmad/customizations/*.user.toml _bmad/custom/*.user.yaml
.clinerules .clinerules
# .augment/ is gitignored except tracked config files — add exceptions explicitly # .augment/ is gitignored except tracked config files — add exceptions explicitly
.augment/* .augment/*

View File

@ -28,12 +28,12 @@ Every agent skill ships a `customize.yaml` file with its defaults. This file def
### Three-Layer Override Model ### Three-Layer Override Model
```text ```text
Priority 1 (wins): _bmad/customizations/{skill-name}.user.yaml (personal, gitignored) Priority 1 (wins): _bmad/custom/{skill-name}.user.yaml (personal, gitignored)
Priority 2: _bmad/customizations/{skill-name}.yaml (team/org, committed) Priority 2: _bmad/custom/{skill-name}.yaml (team/org, committed)
Priority 3 (last): skill's own customize.yaml (defaults) 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) ### 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 ### 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 ```text
_bmad/customizations/ _bmad/custom/
bmad-agent-pm.yaml # team overrides (committed to git) bmad-agent-pm.yaml # team overrides (committed to git)
bmad-agent-pm.user.yaml # personal preferences (gitignored) 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): Team override (shallow merge on metadata):
```yaml ```yaml
# _bmad/customizations/bmad-agent-pm.yaml # _bmad/custom/bmad-agent-pm.yaml
agent: agent:
metadata: metadata:
@ -133,7 +133,7 @@ Procedural startup steps the agent must execute before presenting its menu:
agent: agent:
critical_actions: critical_actions:
- "Scan {project-root}/docs/compliance/ and load any HIPAA-related documents as context." - "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. Critical actions append too. They run top-to-bottom on every activation.
@ -154,7 +154,7 @@ agent:
- code: RC - code: RC
description: "Run compliance pre-check" description: "Run compliance pre-check"
prompt: | 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. and scan all documents in {planning_artifacts} against it.
Report any gaps and cite the relevant regulatory section. Report any gaps and cite the relevant regulatory section.
``` ```
@ -163,7 +163,7 @@ Items not listed in your override keep their defaults.
#### Referencing Files #### 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 ### 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. **Personal file** (`bmad-agent-pm.user.yaml`): Gitignored automatically. Use for nickname preferences, tone adjustments, personal workflows.
```yaml ```yaml
# _bmad/customizations/bmad-agent-pm.user.yaml # _bmad/custom/bmad-agent-pm.user.yaml
agent: agent:
metadata: metadata:
@ -191,7 +191,7 @@ node {project-root}/_bmad/scripts/resolve-customization.js \
--key agent --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: Useful invocations:
@ -221,7 +221,7 @@ Some workflows expose their own customization surface (output paths, review sett
**Customization not appearing?** **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 - 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 - 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 - 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?** **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

View File

@ -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: { rules: {
// Allow CommonJS patterns for Node CLI scripts // Allow CommonJS patterns for Node CLI scripts
'unicorn/prefer-module': 'off', 'unicorn/prefer-module': 'off',

View File

@ -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` 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 ### Step 2: Adopt Persona

View File

@ -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` 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 ### Step 2: Adopt Persona

View File

@ -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` 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 ### Step 2: Adopt Persona

View File

@ -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` 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 ### Step 2: Adopt Persona

View File

@ -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` 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 ### Step 2: Adopt Persona

View File

@ -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` 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 ### Step 2: Adopt Persona

View File

@ -1,10 +1,9 @@
#!/usr/bin/env node
/** /**
* Resolve customization for a BMad skill using three-layer YAML merge. * Resolve customization for a BMad skill using three-layer YAML merge.
* *
* Reads customization from three layers (highest priority first): * Reads customization from three layers (highest priority first):
* 1. {project-root}/_bmad/customizations/{name}.user.yaml (personal, gitignored) * 1. {project-root}/_bmad/custom/{name}.user.yaml (personal, gitignored)
* 2. {project-root}/_bmad/customizations/{name}.yaml (team/org, committed) * 2. {project-root}/_bmad/custom/{name}.yaml (team/org, committed)
* 3. {skill-root}/customize.yaml (skill defaults) * 3. {skill-root}/customize.yaml (skill defaults)
* *
* Skill name is derived from the basename of the skill directory. * Skill name is derived from the basename of the skill directory.
@ -36,10 +35,7 @@ const yaml = require('yaml');
function findProjectRoot(start) { function findProjectRoot(start) {
let current = path.resolve(start); let current = path.resolve(start);
while (true) { while (true) {
if ( if (fs.existsSync(path.join(current, '_bmad')) || fs.existsSync(path.join(current, '.git'))) {
fs.existsSync(path.join(current, '_bmad')) ||
fs.existsSync(path.join(current, '.git'))
) {
return current; return current;
} }
const parent = path.dirname(current); const parent = path.dirname(current);
@ -53,9 +49,9 @@ function loadYaml(filePath) {
const raw = fs.readFileSync(filePath, 'utf8'); const raw = fs.readFileSync(filePath, 'utf8');
const parsed = yaml.parse(raw); const parsed = yaml.parse(raw);
return parsed && typeof parsed === 'object' ? parsed : {}; return parsed && typeof parsed === 'object' ? parsed : {};
} catch (err) { } catch (error) {
if (err.code === 'ENOENT') return {}; if (error.code === 'ENOENT') return {};
process.stderr.write(`warning: failed to parse ${filePath}: ${err.message}\n`); process.stderr.write(`warning: failed to parse ${filePath}: ${error.message}\n`);
return {}; return {};
} }
} }
@ -64,10 +60,6 @@ function isPlainObject(value) {
return value !== null && typeof value === 'object' && !Array.isArray(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) { function mergeByKey(base, override, keyName) {
const result = []; const result = [];
const indexByKey = new Map(); const indexByKey = new Map();
@ -157,12 +149,8 @@ function mergeAgentBlock(base, override) {
// Merge by `code` when both sides use it; otherwise append. // Merge by `code` when both sides use it; otherwise append.
const baseArr = Array.isArray(baseVal) ? baseVal : []; const baseArr = Array.isArray(baseVal) ? baseVal : [];
const overArr = Array.isArray(overVal) ? overVal : []; const overArr = Array.isArray(overVal) ? overVal : [];
const anyHasCode = [...baseArr, ...overArr].some( const anyHasCode = [...baseArr, ...overArr].some((item) => isPlainObject(item) && item.code !== undefined);
(item) => isPlainObject(item) && item.code !== undefined, mergedAgent[key] = anyHasCode ? mergeByKey(baseArr, overArr, 'code') : appendArrays(baseArr, overArr);
);
mergedAgent[key] = anyHasCode
? mergeByKey(baseArr, overArr, 'code')
: appendArrays(baseArr, overArr);
break; break;
} }
default: { default: {
@ -185,7 +173,7 @@ function extractKey(data, dottedKey) {
if (isPlainObject(current) && part in current) { if (isPlainObject(current) && part in current) {
current = current[part]; current = current[part];
} else { } else {
return undefined; return;
} }
} }
return current; return current;
@ -195,15 +183,26 @@ function parseArgs(argv) {
const args = { skill: null, keys: [] }; const args = { skill: null, keys: [] };
for (let i = 0; i < argv.length; i++) { for (let i = 0; i < argv.length; i++) {
const a = argv[i]; const a = argv[i];
if (a === '--skill' || a === '-s') { switch (a) {
args.skill = argv[++i]; case '--skill':
} else if (a === '--key' || a === '-k') { case '-s': {
args.keys.push(argv[++i]); args.skill = argv[++i];
} else if (a === '--help' || a === '-h') { break;
printHelp(); }
process.exit(0); case '--key':
} else { case '-k': {
process.stderr.write(`warning: unknown argument: ${a}\n`); 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; return args;
@ -241,15 +240,14 @@ function main() {
process.stderr.write(`warning: no defaults found at ${defaultsPath}\n`); process.stderr.write(`warning: no defaults found at ${defaultsPath}\n`);
} }
const projectRoot = const projectRoot = findProjectRoot(process.cwd()) || findProjectRoot(skillDir);
findProjectRoot(process.cwd()) || findProjectRoot(skillDir);
let team = {}; let team = {};
let user = {}; let user = {};
if (projectRoot) { if (projectRoot) {
const customizationsDir = path.join(projectRoot, '_bmad', 'customizations'); const customDir = path.join(projectRoot, '_bmad', 'custom');
team = loadYaml(path.join(customizationsDir, `${skillName}.yaml`)); team = loadYaml(path.join(customDir, `${skillName}.yaml`));
user = loadYaml(path.join(customizationsDir, `${skillName}.user.yaml`)); user = loadYaml(path.join(customDir, `${skillName}.user.yaml`));
} }
let merged = mergeAgentBlock(defaults, team); let merged = mergeAgentBlock(defaults, team);

View File

@ -80,7 +80,7 @@ function escapeTableCell(str) {
} }
// Path prefixes/patterns that only exist in installed structure, not in source // 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 // Files that are generated at install time and don't exist in the source tree
const INSTALL_GENERATED_FILES = ['config.yaml', 'config.user.yaml']; const INSTALL_GENERATED_FILES = ['config.yaml', 'config.user.yaml'];