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:
parent
64d3f02615
commit
4a88d321f7
|
|
@ -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/*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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'];
|
||||
|
|
|
|||
Loading…
Reference in New Issue