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
# 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/*

View File

@ -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

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: {
// Allow CommonJS patterns for Node CLI scripts
'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`
**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

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`
**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

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`
**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

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`
**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

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`
**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

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`
**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

View File

@ -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,17 +183,28 @@ 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') {
switch (a) {
case '--skill':
case '-s': {
args.skill = argv[++i];
} else if (a === '--key' || a === '-k') {
break;
}
case '--key':
case '-k': {
args.keys.push(argv[++i]);
} else if (a === '--help' || a === '-h') {
break;
}
case '--help':
case '-h': {
printHelp();
process.exit(0);
} else {
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);

View File

@ -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'];