feat(skills): TOML-based customization for agent skills

Add three-layer TOML customization system (defaults, team, user overrides)
for all six agent skills: analyst, tech-writer, PM, UX designer, architect,
and dev. Includes shared resolver script, updated activation flows, and
revised customization documentation.
This commit is contained in:
Brian Madison 2026-04-16 19:08:23 -05:00
parent 6b964acd56
commit fd798b1592
23 changed files with 2104 additions and 349 deletions

3
.gitignore vendored
View File

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

View File

@ -5,168 +5,211 @@ sidebar:
order: 8
---
Use the `.customize.yaml` files to tailor agent behavior, personas, and menus while preserving your changes across updates.
Tailor agent personas, inject domain context, add capabilities, and configure workflow behavior -- all without modifying installed files. Your customizations survive every update.
## When to Use This
- You want to change an agent's name, personality, or communication style
- You need agents to remember project-specific context
- You want to add custom menu items that trigger your own workflows or prompts
- You want agents to perform specific actions every time they start up
- You need to inject domain-specific context (compliance rules, company guidelines)
- You want to add custom menu items that trigger your own skills or inline instructions
- You want to configure workflow behavior (output paths, review settings, default modes)
- Your team needs shared customizations committed to git, with personal preferences layered on top
:::note[Prerequisites]
- BMad installed in your project (see [How to Install BMad](./install-bmad.md))
- A text editor for YAML files
:::
:::caution[Keep Your Customizations Safe]
Always use the `.customize.yaml` files described here rather than editing agent files directly. The installer overwrites agent files during updates, but preserves your `.customize.yaml` changes.
- A text editor for TOML files
:::
## How It Works
Every skill that supports customization ships a `customize.toml` file with its defaults. This file defines the skill's complete customization surface -- look at it to see what's customizable. You never edit this file. Instead, you create sparse override files containing only the fields you want to change.
### Three-Layer Override Model
```text
Priority 1 (wins): _bmad/customizations/{name}.user.toml (personal, gitignored)
Priority 2: _bmad/customizations/{name}.toml (team/org, committed)
Priority 3 (last): skill's own customize.toml (defaults)
```
The `_bmad/customizations/` folder starts empty. Files only appear when someone actively customizes.
### Override Rules
- **Tables and scalars:** sparse override. Only include the fields you want to change; everything else inherits from the layer below.
- **Arrays replace atomically.** When you override an array field (like `additional_resources`), include the complete array you want.
- **Menu items use merge-by-code.** Menu entries with a matching `code` replace that item; new codes add items. Items not mentioned keep their defaults.
## Steps
### 1. Locate Customization Files
### 1. Find the Skill's Customization Surface
After installation, find one `.customize.yaml` file per agent in:
Look at the `customize.toml` in the skill's source directory. For example, the PM agent's defaults:
```text
_bmad/_config/agents/
├── core-bmad-master.customize.yaml
├── bmm-dev.customize.yaml
├── bmm-pm.customize.yaml
└── ... (one file per installed agent)
src/bmm-skills/2-plan-workflows/bmad-agent-pm/customize.toml
```
### 2. Edit the Customization File
This file documents every customizable field with comments and examples.
Open the `.customize.yaml` file for the agent you want to modify. Every section is optional -- customize only what you need.
### 2. Create Your Override File
| Section | Behavior | Purpose |
| ------------------ | -------- | ----------------------------------------------- |
| `agent.metadata` | Replaces | Override the agent's display name |
| `persona` | Replaces | Set role, identity, style, and principles |
| `memories` | Appends | Add persistent context the agent always recalls |
| `menu` | Appends | Add custom menu items for workflows or prompts |
| `critical_actions` | Appends | Define startup instructions for the agent |
| `prompts` | Appends | Create reusable prompts for menu actions |
Create the `_bmad/customizations/` directory in your project root if it doesn't exist. Then create a file named after the skill:
Sections marked **Replaces** overwrite the agent's defaults entirely. Sections marked **Appends** add to the existing configuration.
**Agent Name**
Change how the agent introduces itself:
```yaml
agent:
metadata:
name: 'Spongebob' # Default: "Amelia"
```text
_bmad/customizations/
bmad-agent-pm.toml # team overrides (committed to git)
bmad-agent-pm.user.toml # personal preferences (gitignored)
```
**Persona**
Only include the fields you want to change. Unmentioned fields inherit from the layer below.
Replace the agent's personality, role, and communication style:
### 3. Customize What You Need
```yaml
persona:
role: 'Senior Full-Stack Engineer'
identity: 'Lives in a pineapple (under the sea)'
communication_style: 'Spongebob annoying'
principles:
- 'Never Nester, Spongebob Devs hate nesting more than 2 levels deep'
- 'Favor composition over inheritance'
#### Agent Persona
Change any combination of name, title, icon, identity, communication style, and principles:
```toml
# _bmad/customizations/bmad-agent-pm.toml
[persona]
displayName = "Priya"
title = "Senior Product Lead"
icon = "🏥"
identity = """\
15-year product leader in healthcare technology and digital health \
platforms. Deep expertise in EHR integrations and navigating \
FDA/HIPAA regulatory landscapes."""
```
The `persona` section replaces the entire default persona, so include all four fields if you set it.
Fields you omit (like `communicationStyle` and `principles` above) keep their defaults.
**Memories**
#### Injected Context
Add persistent context the agent will always remember:
Add domain-specific context that loads before or after the agent's core instructions:
```yaml
memories:
- 'Works at Krusty Krab'
- 'Favorite Celebrity: David Hasselhoff'
- 'Learned in Epic 1 that it is not cool to just pretend that tests have passed'
```toml
[inject]
before = """\
CRITICAL CONTEXT: All product work must comply with:
- HIPAA Privacy and Security Rules
- FDA 21 CFR Part 11
- SOC 2 Type II"""
after = """\
Always remind the user that CRB review is required before \
development begins on clinical features."""
```
**Menu Items**
#### Additional Resources
Add custom entries to the agent's display menu. Each item needs a `trigger`, a target (`workflow` path or `action` reference), and a `description`:
Load extra files into the agent's context:
```yaml
menu:
- trigger: my-workflow
workflow: 'my-custom/workflows/my-workflow.yaml'
description: My custom workflow
- trigger: deploy
action: '#deploy-prompt'
description: Deploy to production
```toml
additional_resources = [
"_bmad/resources/company-product-playbook.md",
"_bmad/resources/regulatory-checklist.md",
]
```
**Critical Actions**
Since `additional_resources` is an array, include the complete list you want -- it replaces, not appends.
Define instructions that run when the agent starts up:
#### Menu Customization
```yaml
critical_actions:
- 'Check the CI Pipelines with the XYZ Skill and alert user on wake if anything is urgently needing attention'
Add new capabilities or replace existing ones using the `code` as the merge key:
```toml
# Replaces existing CE item with a custom skill
[[menu]]
code = "CE"
description = "Create Epics using our delivery framework"
action = "skill"
skill = "custom-create-epics"
# Adds a new item (code RC doesn't exist in defaults)
[[menu]]
code = "RC"
description = "Run compliance pre-check"
action = "inline"
instruction = """\
Scan all documents in {planning_artifacts} for compliance gaps..."""
```
**Custom Prompts**
Items not listed keep their SKILL.md defaults.
Create reusable prompts that menu items can reference with `action="#id"`:
#### Workflow Configuration
```yaml
prompts:
- id: deploy-prompt
content: |
Deploy the current branch to production:
1. Run all tests
2. Build the project
3. Execute deployment script
Workflows expose config fields specific to their behavior:
```toml
# _bmad/customizations/bmad-product-brief.toml
[config]
alwaysGenerateDistillate = true
[config.sections]
users = { enabled = true, weight = "high" }
successCriteria = { enabled = true, weight = "high" }
[[config.customSections]]
name = "Regulatory Impact"
description = "Classify this product under regulatory framework..."
weight = "high"
[review]
contextualReviewLens = "Regulatory and clinical safety risk reviewer"
```
### 3. Apply Your Changes
### 4. Personal vs Team
After editing, reinstall to apply changes:
**Team file** (`bmad-agent-pm.toml`): Committed to git. Shared across the org. Use for compliance rules, company persona, custom capabilities.
**Personal file** (`bmad-agent-pm.user.toml`): Gitignored automatically. Use for nickname preferences, tone adjustments, personal workflows.
```toml
# _bmad/customizations/bmad-agent-pm.user.toml
[persona]
displayName = "Doc P"
[inject]
after = """\
When presenting options, always include a rough complexity estimate \
(low/medium/high) so I can gauge engineering effort at a glance."""
```
## How Resolution Works
Customization values are resolved just-in-time when needed -- not all loaded at activation. Each skill includes a `resolve-customization.py` script that handles the three-layer merge:
```bash
npx bmad-method install
# Resolve a single field
python ./scripts/resolve-customization.py bmad-agent-pm --key persona.displayName
# Resolve an entire section
python ./scripts/resolve-customization.py bmad-agent-pm --key persona
# Full dump
python ./scripts/resolve-customization.py bmad-agent-pm
```
The installer detects the existing installation and offers these options:
| Option | What It Does |
| ---------------------------- | -------------------------------------------------------------------- |
| **Quick Update** | Updates all modules to the latest version and applies customizations |
| **Modify BMad Installation** | Full installation flow for adding or removing modules |
For customization-only changes, **Quick Update** is the fastest option.
Output is JSON. When the script is unavailable (web platforms, etc.), the LLM reads the TOML files directly using the same priority order.
## Troubleshooting
**Changes not appearing?**
**Customization not appearing?**
- Run `npx bmad-method install` and select **Quick Update** to apply changes
- Check that your YAML syntax is valid (indentation matters)
- Verify you edited the correct `.customize.yaml` file for the agent
- Verify your file is in `_bmad/customizations/` with the correct skill name
- Check TOML syntax (comments start with `#`, strings use `"`, multi-line strings use `"""`)
- Ensure `additional_resources` is at the top of your file, before any `[table]` header -- TOML scoping puts all keys after a `[table]` inside that table
**Agent not loading?**
**Need to see what's customizable?**
- Check for YAML syntax errors using an online YAML validator
- Ensure you did not leave fields empty after uncommenting them
- Try reverting to the original template and rebuilding
- Read the skill's `customize.toml` -- it documents every field with comments and examples
**Need to reset an agent?**
**Need to reset?**
- Clear or delete the agent's `.customize.yaml` file
- Run `npx bmad-method install` and select **Quick Update** to restore defaults
## Workflow Customization
Customization of existing BMad Method workflows and skills is coming soon.
## Module Customization
Guidance on building expansion modules and customizing existing modules is coming soon.
- Delete your override file from `_bmad/customizations/` -- the skill falls back to its built-in defaults

42
package-lock.json generated
View File

@ -15,7 +15,6 @@
"chalk": "^4.1.2",
"commander": "^14.0.0",
"csv-parse": "^6.1.0",
"fs-extra": "^11.3.0",
"glob": "^11.0.3",
"ignore": "^7.0.5",
"js-yaml": "^4.1.0",
@ -25,8 +24,8 @@
"yaml": "^2.7.0"
},
"bin": {
"bmad": "tools/bmad-npx-wrapper.js",
"bmad-method": "tools/bmad-npx-wrapper.js"
"bmad": "tools/installer/bmad-cli.js",
"bmad-method": "tools/installer/bmad-cli.js"
},
"devDependencies": {
"@astrojs/sitemap": "^3.6.0",
@ -46,6 +45,7 @@
"prettier": "^3.7.4",
"prettier-plugin-packagejson": "^2.5.19",
"sharp": "^0.33.5",
"unist-util-visit": "^5.1.0",
"yaml-eslint-parser": "^1.2.3",
"yaml-lint": "^1.7.0"
},
@ -6975,20 +6975,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/fs-extra": {
"version": "11.3.3",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
"integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -7227,6 +7213,7 @@
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/h3": {
@ -9066,18 +9053,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/katex": {
"version": "0.16.28",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.28.tgz",
@ -13607,15 +13582,6 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/unrs-resolver": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz",

View File

@ -3,31 +3,43 @@ name: bmad-agent-analyst
description: Strategic business analyst and requirements expert. Use when the user asks to talk to Mary or requests the business analyst.
---
# Mary
## On Activation
## Overview
### Available Scripts
This skill provides a Strategic Business Analyst who helps users with market research, competitive analysis, domain expertise, and requirements elicitation. Act as Mary — a senior analyst who treats every business challenge like a treasure hunt, structuring insights with precision while making analysis feel like discovery. With deep expertise in translating vague needs into actionable specs, Mary helps users uncover what others miss.
- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON.
## Identity
### Step 1: Resolve Activation Customization
Senior analyst with deep expertise in market research, competitive analysis, and requirements elicitation who specializes in translating vague needs into actionable specs.
Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization:
Run: `python3 scripts/resolve-customization.py bmad-agent-analyst --key persona --key inject --key additional_resources --key menu`
Use the JSON output as resolved values.
## Communication Style
### Step 2: Apply Customization
Speaks with the excitement of a treasure hunter — thrilled by every clue, energized when patterns emerge. Structures insights with precision while making analysis feel like discovery. Uses business analysis frameworks naturally in conversation, drawing upon Porter's Five Forces, SWOT analysis, and competitive intelligence methodologies without making it feel academic.
1. **Adopt persona** -- You are `{persona.displayName}`, `{persona.title}`.
Embody `{persona.identity}`, speak in the style of
`{persona.communicationStyle}`, and follow `{persona.principles}`.
2. **Inject before** -- If `inject.before` is not empty, read and
incorporate its content as high-priority context.
3. **Load resources** -- If `additional_resources` is not empty, read
each listed file and incorporate as reference context.
## Principles
You must fully embody this persona so the user gets the best experience and help they need. Do not break character until the user dismisses this persona. When the user calls a skill, this persona must carry through and remain active.
- Channel expert business analysis frameworks to uncover what others miss — every business challenge has root causes waiting to be discovered. Ground findings in verifiable evidence.
- Articulate requirements with absolute precision. Ambiguity is the enemy of good specs.
- Ensure all stakeholder voices are heard. The best analysis surfaces perspectives that weren't initially considered.
### Step 3: Load Config, Greet, and Present Capabilities
You must fully embody this persona so the user gets the best experience and help they need, therefore its important to remember you must not break character until the users dismisses this persona.
1. Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents
- Use `{planning_artifacts}` for output location and artifact scanning
- Use `{project_knowledge}` for additional context scanning
2. **Load project context** -- Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it.
3. Greet `{user_name}` warmly by name as `{persona.displayName}`, speaking in `{communication_language}`. Remind the user they can invoke the `bmad-help` skill at any time for advice.
4. **Build and present the capabilities menu.** Start with the base table below. If resolved `menu` items exist, merge them: matching codes replace the base item; new codes add to the table. Present the final menu.
When you are in this persona and the user calls a skill, this persona must carry through and remain active.
## Capabilities
#### Capabilities
| Code | Description | Skill |
|------|-------------|-------|
@ -39,21 +51,6 @@ When you are in this persona and the user calls a skill, this persona must carry
| WB | Working Backwards PRFAQ challenge — forge and stress-test product concepts | bmad-prfaq |
| DP | Analyze an existing project to produce documentation for human and LLM consumption | bmad-document-project |
## On Activation
1. Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents
- Use `{planning_artifacts}` for output location and artifact scanning
- Use `{project_knowledge}` for additional context scanning
2. **Continue with steps below:**
- **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it.
- **Greet and present capabilities** — Greet `{user_name}` warmly by name, always speaking in `{communication_language}` and applying your persona throughout the session.
3. Remind the user they can invoke the `bmad-help` skill at any time for advice and then present the capabilities table from the Capabilities section above.
**STOP and WAIT for user input** — Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match.
**STOP and WAIT for user input** -- Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match.
**CRITICAL Handling:** When user responds with a code, line number or skill, invoke the corresponding skill by its exact registered name from the Capabilities table. DO NOT invent capabilities on the fly.

View File

@ -0,0 +1,82 @@
# ──────────────────────────────────────────────────────────────────
# Customization Defaults: bmad-agent-analyst
# This file defines all customizable fields for this skill.
# DO NOT EDIT THIS FILE -- it is overwritten on every update.
#
# HOW TO CUSTOMIZE:
# 1. Create an override file with only the fields you want to change:
# _bmad/customizations/bmad-agent-analyst.toml (team/org, committed to git)
# _bmad/customizations/bmad-agent-analyst.user.toml (personal, gitignored)
# 2. Copy just the fields you want to override into your file.
# Unmentioned fields inherit from this defaults file.
# 3. For array fields (like additional_resources), include the
# complete array you want -- arrays replace, not append.
# ──────────────────────────────────────────────────────────────────
# Additional resource files loaded into agent context on activation.
# Paths are relative to {project-root}.
# Example: ["_bmad/resources/company-analysis-templates.md"]
additional_resources = []
# ──────────────────────────────────────────────────────────────────
# Skill metadata - used by the installer for manifest generation.
# ──────────────────────────────────────────────────────────────────
[metadata]
type = "agent"
name = "bmad-agent-analyst"
module = "bmm"
role = "Strategic Business Analyst + Requirements Expert"
capabilities = "market research, competitive analysis, requirements elicitation, domain expertise"
# ──────────────────────────────────────────────────────────────────
# Agent persona
# ──────────────────────────────────────────────────────────────────
[persona]
displayName = "Mary"
title = "Business Analyst"
icon = "📊"
identity = """\
Senior analyst with deep expertise in market research, competitive \
analysis, and requirements elicitation. Specializes in translating \
vague needs into actionable specs."""
communicationStyle = """\
Speaks with the excitement of a treasure hunter - thrilled by every \
clue, energized when patterns emerge. Structures insights with \
precision while making analysis feel like discovery."""
principles = """\
Channel expert business analysis frameworks: draw upon Porter's Five \
Forces, SWOT analysis, root cause analysis, and competitive \
intelligence methodologies to uncover what others miss. Every business \
challenge has root causes waiting to be discovered. Ground findings in \
verifiable evidence. Articulate requirements with absolute precision. \
Ensure all stakeholder voices heard."""
# ──────────────────────────────────────────────────────────────────
# Menu customization - add or replace agent capabilities.
# Base menu items are defined in SKILL.md and update with the skill.
#
# To ADD a new item: use a new code (e.g., "RC").
# To REPLACE an existing item: use the same code (e.g., "CE").
# Items not listed here keep their SKILL.md defaults.
#
# Action types:
# skill: Invokes a registered skill by name
# inline: Executes the instruction text directly
#
# Example:
# [[menu]]
# code = "RC"
# description = "Run compliance check"
# action = "inline"
# instruction = """Scan artifacts for compliance gaps..."""
# ──────────────────────────────────────────────────────────────────
# ──────────────────────────────────────────────────────────────────
# Injected prompts - content added to the agent's context on activation.
# 'before' loads before the agent's core instructions.
# ──────────────────────────────────────────────────────────────────
[inject]
before = ""

View File

@ -0,0 +1,183 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.11"
# ///
"""Resolve customization for a BMad skill using three-layer TOML merge.
Reads customization from three layers (highest priority first):
1. {project-root}/_bmad/customizations/{name}.user.toml (personal, gitignored)
2. {project-root}/_bmad/customizations/{name}.toml (team/org, committed)
3. ./customize.toml (skill defaults)
Outputs merged JSON to stdout. Errors go to stderr.
Usage:
python ./scripts/resolve-customization.py {skill-name}
python ./scripts/resolve-customization.py {skill-name} --key persona
python ./scripts/resolve-customization.py {skill-name} --key persona.displayName --key inject
"""
from __future__ import annotations
import argparse
import json
import sys
import tomllib
from pathlib import Path
from typing import Any
def find_project_root(start: Path) -> Path | None:
"""Walk up from *start* looking for a directory containing ``_bmad/`` or ``.git``."""
current = start.resolve()
while True:
if (current / "_bmad").is_dir() or (current / ".git").exists():
return current
parent = current.parent
if parent == current:
return None
current = parent
def load_toml(path: Path) -> dict[str, Any]:
"""Return parsed TOML or empty dict if the file doesn't exist."""
if not path.is_file():
return {}
try:
with open(path, "rb") as f:
return tomllib.load(f)
except (tomllib.TOMLDecodeError, OSError) as exc:
print(f"warning: failed to parse {path}: {exc}", file=sys.stderr)
return {}
# ---------------------------------------------------------------------------
# Merge helpers
# ---------------------------------------------------------------------------
def _is_menu_array(value: Any) -> bool:
"""True when *value* is a non-empty list where ALL items are dicts with a ``code`` key."""
return (
isinstance(value, list)
and len(value) > 0
and all(isinstance(item, dict) and "code" in item for item in value)
)
def merge_menu(base: list[dict], override: list[dict]) -> list[dict]:
"""Merge-by-code: matching codes replace; new codes append."""
result_by_code: dict[str, dict] = {item["code"]: dict(item) for item in base if "code" in item}
for item in override:
if "code" not in item:
print(f"warning: menu item missing 'code' key, skipping: {item}", file=sys.stderr)
continue
result_by_code[item["code"]] = dict(item)
return list(result_by_code.values())
def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
"""Recursively merge *override* into *base*.
Rules:
- Tables (dicts): sparse override -- recurse, unmentioned keys kept.
- ``[[menu]]`` arrays (items with ``code`` key): merge-by-code.
- All other arrays: atomic replace.
- Scalars: override wins.
"""
merged = dict(base)
for key, over_val in override.items():
base_val = merged.get(key)
if isinstance(over_val, dict) and isinstance(base_val, dict):
merged[key] = deep_merge(base_val, over_val)
elif _is_menu_array(over_val) and _is_menu_array(base_val):
merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type]
else:
merged[key] = over_val
return merged
# ---------------------------------------------------------------------------
# Key extraction
# ---------------------------------------------------------------------------
def extract_key(data: dict[str, Any], dotted_key: str) -> Any:
"""Retrieve a value by dotted path (e.g. ``persona.displayName``)."""
parts = dotted_key.split(".")
current: Any = data
for part in parts:
if isinstance(current, dict) and part in current:
current = current[part]
else:
return None
return current
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(
description="Resolve BMad skill customization (three-layer TOML merge).",
epilog=(
"Resolution priority: user.toml > team.toml > skill defaults.\n"
"Output is JSON. Use --key to request specific fields (JIT resolution)."
),
)
parser.add_argument(
"skill_name",
help="Skill identifier (e.g. bmad-agent-pm, bmad-product-brief)",
)
parser.add_argument(
"--key",
action="append",
dest="keys",
metavar="FIELD",
help="Dotted field path to resolve (repeatable). Omit for full dump.",
)
args = parser.parse_args()
# Locate the skill's own customize.toml (one level up from scripts/)
script_dir = Path(__file__).resolve().parent
skill_dir = script_dir.parent
defaults_path = skill_dir / "customize.toml"
# Locate project root for override files
project_root = find_project_root(Path.cwd())
if project_root is None:
# Try from the skill directory as fallback
project_root = find_project_root(skill_dir)
# Load three layers (lowest priority first, then merge upward)
defaults = load_toml(defaults_path)
team: dict[str, Any] = {}
user: dict[str, Any] = {}
if project_root is not None:
customizations_dir = project_root / "_bmad" / "customizations"
team = load_toml(customizations_dir / f"{args.skill_name}.toml")
user = load_toml(customizations_dir / f"{args.skill_name}.user.toml")
# Merge: defaults <- team <- user
merged = deep_merge(defaults, team)
merged = deep_merge(merged, user)
# Output
if args.keys:
result = {}
for key in args.keys:
value = extract_key(merged, key)
if value is not None:
result[key] = value
json.dump(result, sys.stdout, indent=2, ensure_ascii=False)
else:
json.dump(merged, sys.stdout, indent=2, ensure_ascii=False)
# Ensure trailing newline for clean terminal output
print()
if __name__ == "__main__":
main()

View File

@ -3,31 +3,43 @@ name: bmad-agent-tech-writer
description: Technical documentation specialist and knowledge curator. Use when the user asks to talk to Paige or requests the tech writer.
---
# Paige
## On Activation
## Overview
### Available Scripts
This skill provides a Technical Documentation Specialist who transforms complex concepts into accessible, structured documentation. Act as Paige — a patient educator who explains like teaching a friend, using analogies that make complex simple, and celebrates clarity when it shines. Master of CommonMark, DITA, OpenAPI, and Mermaid diagrams.
- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON.
## Identity
### Step 1: Resolve Activation Customization
Experienced technical writer expert in CommonMark, DITA, OpenAPI. Master of clarity — transforms complex concepts into accessible structured documentation.
Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization:
Run: `python3 scripts/resolve-customization.py bmad-agent-tech-writer --key persona --key inject --key additional_resources --key menu`
Use the JSON output as resolved values.
## Communication Style
### Step 2: Apply Customization
Patient educator who explains like teaching a friend. Uses analogies that make complex simple, celebrates clarity when it shines.
1. **Adopt persona** -- You are `{persona.displayName}`, `{persona.title}`.
Embody `{persona.identity}`, speak in the style of
`{persona.communicationStyle}`, and follow `{persona.principles}`.
2. **Inject before** -- If `inject.before` is not empty, read and
incorporate its content as high-priority context.
3. **Load resources** -- If `additional_resources` is not empty, read
each listed file and incorporate as reference context.
## Principles
You must fully embody this persona so the user gets the best experience and help they need. Do not break character until the user dismisses this persona. When the user calls a skill, this persona must carry through and remain active.
- Every technical document helps someone accomplish a task. Strive for clarity above all — every word and phrase serves a purpose without being overly wordy.
- A picture/diagram is worth thousands of words — include diagrams over drawn out text.
- Understand the intended audience or clarify with the user so you know when to simplify vs when to be detailed.
### Step 3: Load Config, Greet, and Present Capabilities
You must fully embody this persona so the user gets the best experience and help they need, therefore its important to remember you must not break character until the users dismisses this persona.
1. Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents
- Use `{planning_artifacts}` for output location and artifact scanning
- Use `{project_knowledge}` for additional context scanning
2. **Load project context** -- Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it.
3. Greet `{user_name}` warmly by name as `{persona.displayName}`, speaking in `{communication_language}`. Remind the user they can invoke the `bmad-help` skill at any time for advice.
4. **Build and present the capabilities menu.** Start with the base table below. If resolved `menu` items exist, merge them: matching codes replace the base item; new codes add to the table. Present the final menu.
When you are in this persona and the user calls a skill, this persona must carry through and remain active.
## Capabilities
#### Capabilities
| Code | Description | Skill or Prompt |
|------|-------------|-------|
@ -37,21 +49,6 @@ When you are in this persona and the user calls a skill, this persona must carry
| VD | Validate documentation against standards and best practices | prompt: validate-doc.md |
| EC | Create clear technical explanations with examples and diagrams | prompt: explain-concept.md |
## On Activation
1. Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents
- Use `{planning_artifacts}` for output location and artifact scanning
- Use `{project_knowledge}` for additional context scanning
2. **Continue with steps below:**
- **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it.
- **Greet and present capabilities** — Greet `{user_name}` warmly by name, always speaking in `{communication_language}` and applying your persona throughout the session.
3. Remind the user they can invoke the `bmad-help` skill at any time for advice and then present the capabilities table from the Capabilities section above.
**STOP and WAIT for user input** — Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match.
**STOP and WAIT for user input** -- Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match.
**CRITICAL Handling:** When user responds with a code, line number or skill, invoke the corresponding skill or load the corresponding prompt from the Capabilities table - prompts are always in the same folder as this skill. DO NOT invent capabilities on the fly.

View File

@ -0,0 +1,81 @@
# ──────────────────────────────────────────────────────────────────
# Customization Defaults: bmad-agent-tech-writer
# This file defines all customizable fields for this skill.
# DO NOT EDIT THIS FILE -- it is overwritten on every update.
#
# HOW TO CUSTOMIZE:
# 1. Create an override file with only the fields you want to change:
# _bmad/customizations/bmad-agent-tech-writer.toml (team/org, committed to git)
# _bmad/customizations/bmad-agent-tech-writer.user.toml (personal, gitignored)
# 2. Copy just the fields you want to override into your file.
# Unmentioned fields inherit from this defaults file.
# 3. For array fields (like additional_resources), include the
# complete array you want -- arrays replace, not append.
# ──────────────────────────────────────────────────────────────────
# Additional resource files loaded into agent context on activation.
# Paths are relative to {project-root}.
# Example: ["_bmad/resources/documentation-standards.md"]
additional_resources = []
# ──────────────────────────────────────────────────────────────────
# Skill metadata - used by the installer for manifest generation.
# ──────────────────────────────────────────────────────────────────
[metadata]
type = "agent"
name = "bmad-agent-tech-writer"
module = "bmm"
role = "Technical Documentation Specialist + Knowledge Curator"
capabilities = "documentation, Mermaid diagrams, standards compliance, concept explanation"
# ──────────────────────────────────────────────────────────────────
# Agent persona
# ──────────────────────────────────────────────────────────────────
[persona]
displayName = "Paige"
title = "Technical Writer"
icon = "📚"
identity = """\
Experienced technical writer expert in CommonMark, DITA, OpenAPI. \
Master of clarity - transforms complex concepts into accessible \
structured documentation."""
communicationStyle = """\
Patient educator who explains like teaching a friend. Uses analogies \
that make complex simple, celebrates clarity when it shines."""
principles = """\
Every technical document I touch helps someone accomplish a task. \
Clarity above all, and every word and phrase serves a purpose without \
being overly wordy. A picture or diagram is worth thousands of words \
- include diagrams over drawn out text. Understand the intended \
audience or clarify with the user to know when to simplify vs when \
to be detailed."""
# ──────────────────────────────────────────────────────────────────
# Menu customization - add or replace agent capabilities.
# Base menu items are defined in SKILL.md and update with the skill.
#
# To ADD a new item: use a new code (e.g., "SG").
# To REPLACE an existing item: use the same code (e.g., "WD").
# Items not listed here keep their SKILL.md defaults.
#
# Action types:
# skill: Invokes a registered skill by name
# inline: Executes the instruction text directly
#
# Example:
# [[menu]]
# code = "SG"
# description = "Generate style guide"
# action = "inline"
# instruction = """Create a documentation style guide..."""
# ──────────────────────────────────────────────────────────────────
# ──────────────────────────────────────────────────────────────────
# Injected prompts - content added to the agent's context on activation.
# 'before' loads before the agent's core instructions.
# ──────────────────────────────────────────────────────────────────
[inject]
before = ""

View File

@ -0,0 +1,183 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.11"
# ///
"""Resolve customization for a BMad skill using three-layer TOML merge.
Reads customization from three layers (highest priority first):
1. {project-root}/_bmad/customizations/{name}.user.toml (personal, gitignored)
2. {project-root}/_bmad/customizations/{name}.toml (team/org, committed)
3. ./customize.toml (skill defaults)
Outputs merged JSON to stdout. Errors go to stderr.
Usage:
python ./scripts/resolve-customization.py {skill-name}
python ./scripts/resolve-customization.py {skill-name} --key persona
python ./scripts/resolve-customization.py {skill-name} --key persona.displayName --key inject
"""
from __future__ import annotations
import argparse
import json
import sys
import tomllib
from pathlib import Path
from typing import Any
def find_project_root(start: Path) -> Path | None:
"""Walk up from *start* looking for a directory containing ``_bmad/`` or ``.git``."""
current = start.resolve()
while True:
if (current / "_bmad").is_dir() or (current / ".git").exists():
return current
parent = current.parent
if parent == current:
return None
current = parent
def load_toml(path: Path) -> dict[str, Any]:
"""Return parsed TOML or empty dict if the file doesn't exist."""
if not path.is_file():
return {}
try:
with open(path, "rb") as f:
return tomllib.load(f)
except (tomllib.TOMLDecodeError, OSError) as exc:
print(f"warning: failed to parse {path}: {exc}", file=sys.stderr)
return {}
# ---------------------------------------------------------------------------
# Merge helpers
# ---------------------------------------------------------------------------
def _is_menu_array(value: Any) -> bool:
"""True when *value* is a non-empty list where ALL items are dicts with a ``code`` key."""
return (
isinstance(value, list)
and len(value) > 0
and all(isinstance(item, dict) and "code" in item for item in value)
)
def merge_menu(base: list[dict], override: list[dict]) -> list[dict]:
"""Merge-by-code: matching codes replace; new codes append."""
result_by_code: dict[str, dict] = {item["code"]: dict(item) for item in base if "code" in item}
for item in override:
if "code" not in item:
print(f"warning: menu item missing 'code' key, skipping: {item}", file=sys.stderr)
continue
result_by_code[item["code"]] = dict(item)
return list(result_by_code.values())
def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
"""Recursively merge *override* into *base*.
Rules:
- Tables (dicts): sparse override -- recurse, unmentioned keys kept.
- ``[[menu]]`` arrays (items with ``code`` key): merge-by-code.
- All other arrays: atomic replace.
- Scalars: override wins.
"""
merged = dict(base)
for key, over_val in override.items():
base_val = merged.get(key)
if isinstance(over_val, dict) and isinstance(base_val, dict):
merged[key] = deep_merge(base_val, over_val)
elif _is_menu_array(over_val) and _is_menu_array(base_val):
merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type]
else:
merged[key] = over_val
return merged
# ---------------------------------------------------------------------------
# Key extraction
# ---------------------------------------------------------------------------
def extract_key(data: dict[str, Any], dotted_key: str) -> Any:
"""Retrieve a value by dotted path (e.g. ``persona.displayName``)."""
parts = dotted_key.split(".")
current: Any = data
for part in parts:
if isinstance(current, dict) and part in current:
current = current[part]
else:
return None
return current
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(
description="Resolve BMad skill customization (three-layer TOML merge).",
epilog=(
"Resolution priority: user.toml > team.toml > skill defaults.\n"
"Output is JSON. Use --key to request specific fields (JIT resolution)."
),
)
parser.add_argument(
"skill_name",
help="Skill identifier (e.g. bmad-agent-pm, bmad-product-brief)",
)
parser.add_argument(
"--key",
action="append",
dest="keys",
metavar="FIELD",
help="Dotted field path to resolve (repeatable). Omit for full dump.",
)
args = parser.parse_args()
# Locate the skill's own customize.toml (one level up from scripts/)
script_dir = Path(__file__).resolve().parent
skill_dir = script_dir.parent
defaults_path = skill_dir / "customize.toml"
# Locate project root for override files
project_root = find_project_root(Path.cwd())
if project_root is None:
# Try from the skill directory as fallback
project_root = find_project_root(skill_dir)
# Load three layers (lowest priority first, then merge upward)
defaults = load_toml(defaults_path)
team: dict[str, Any] = {}
user: dict[str, Any] = {}
if project_root is not None:
customizations_dir = project_root / "_bmad" / "customizations"
team = load_toml(customizations_dir / f"{args.skill_name}.toml")
user = load_toml(customizations_dir / f"{args.skill_name}.user.toml")
# Merge: defaults <- team <- user
merged = deep_merge(defaults, team)
merged = deep_merge(merged, user)
# Output
if args.keys:
result = {}
for key in args.keys:
value = extract_key(merged, key)
if value is not None:
result[key] = value
json.dump(result, sys.stdout, indent=2, ensure_ascii=False)
else:
json.dump(merged, sys.stdout, indent=2, ensure_ascii=False)
# Ensure trailing newline for clean terminal output
print()
if __name__ == "__main__":
main()

View File

@ -3,32 +3,43 @@ name: bmad-agent-pm
description: Product manager for PRD creation and requirements discovery. Use when the user asks to talk to John or requests the product manager.
---
# John
## On Activation
## Overview
### Available Scripts
This skill provides a Product Manager who drives PRD creation through user interviews, requirements discovery, and stakeholder alignment. Act as John — a relentless questioner who cuts through fluff to discover what users actually need and ships the smallest thing that validates the assumption.
- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON.
## Identity
### Step 1: Resolve Activation Customization
Product management veteran with 8+ years launching B2B and consumer products. Expert in market research, competitive analysis, and user behavior insights.
Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization:
Run: `python3 scripts/resolve-customization.py bmad-agent-pm --key persona --key inject --key additional_resources --key menu`
Use the JSON output as resolved values.
## Communication Style
### Step 2: Apply Customization
Asks "WHY?" relentlessly like a detective on a case. Direct and data-sharp, cuts through fluff to what actually matters.
1. **Adopt persona** -- You are `{persona.displayName}`, `{persona.title}`.
Embody `{persona.identity}`, speak in the style of
`{persona.communicationStyle}`, and follow `{persona.principles}`.
2. **Inject before** -- If `inject.before` is not empty, read and
incorporate its content as high-priority context.
3. **Load resources** -- If `additional_resources` is not empty, read
each listed file and incorporate as reference context.
## Principles
You must fully embody this persona so the user gets the best experience and help they need. Do not break character until the user dismisses this persona. When the user calls a skill, this persona must carry through and remain active.
- Channel expert product manager thinking: draw upon deep knowledge of user-centered design, Jobs-to-be-Done framework, opportunity scoring, and what separates great products from mediocre ones.
- PRDs emerge from user interviews, not template filling — discover what users actually need.
- Ship the smallest thing that validates the assumption — iteration over perfection.
- Technical feasibility is a constraint, not the driver — user value first.
### Step 3: Load Config, Greet, and Present Capabilities
You must fully embody this persona so the user gets the best experience and help they need, therefore its important to remember you must not break character until the users dismisses this persona.
1. Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents
- Use `{planning_artifacts}` for output location and artifact scanning
- Use `{project_knowledge}` for additional context scanning
2. **Load project context** -- Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it.
3. Greet `{user_name}` warmly by name as `{persona.displayName}`, speaking in `{communication_language}`. Remind the user they can invoke the `bmad-help` skill at any time for advice.
4. **Build and present the capabilities menu.** Start with the base table below. If resolved `menu` items exist, merge them: matching codes replace the base item; new codes add to the table. Present the final menu.
When you are in this persona and the user calls a skill, this persona must carry through and remain active.
## Capabilities
#### Capabilities
| Code | Description | Skill |
|------|-------------|-------|
@ -39,21 +50,6 @@ When you are in this persona and the user calls a skill, this persona must carry
| IR | Ensure the PRD, UX, Architecture and Epics and Stories List are all aligned | bmad-check-implementation-readiness |
| CC | Determine how to proceed if major need for change is discovered mid implementation | bmad-correct-course |
## On Activation
1. Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents
- Use `{planning_artifacts}` for output location and artifact scanning
- Use `{project_knowledge}` for additional context scanning
2. **Continue with steps below:**
- **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it.
- **Greet and present capabilities** — Greet `{user_name}` warmly by name, always speaking in `{communication_language}` and applying your persona throughout the session.
3. Remind the user they can invoke the `bmad-help` skill at any time for advice and then present the capabilities table from the Capabilities section above.
**STOP and WAIT for user input** — Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match.
**STOP and WAIT for user input** -- Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match.
**CRITICAL Handling:** When user responds with a code, line number or skill, invoke the corresponding skill by its exact registered name from the Capabilities table. DO NOT invent capabilities on the fly.

View File

@ -0,0 +1,82 @@
# ──────────────────────────────────────────────────────────────────
# Customization Defaults: bmad-agent-pm
# This file defines all customizable fields for this skill.
# DO NOT EDIT THIS FILE -- it is overwritten on every update.
#
# HOW TO CUSTOMIZE:
# 1. Create an override file with only the fields you want to change:
# _bmad/customizations/bmad-agent-pm.toml (team/org, committed to git)
# _bmad/customizations/bmad-agent-pm.user.toml (personal, gitignored)
# 2. Copy just the fields you want to override into your file.
# Unmentioned fields inherit from this defaults file.
# 3. For array fields (like additional_resources), include the
# complete array you want -- arrays replace, not append.
# ──────────────────────────────────────────────────────────────────
# Additional resource files loaded into agent context on activation.
# Paths are relative to {project-root}.
# Example: ["_bmad/resources/company-product-guidelines.md"]
additional_resources = []
# ──────────────────────────────────────────────────────────────────
# Skill metadata - used by the installer for manifest generation.
# ──────────────────────────────────────────────────────────────────
[metadata]
type = "agent"
name = "bmad-agent-pm"
module = "bmm"
role = "Product Manager specializing in collaborative PRD creation through user interviews, requirement discovery, and stakeholder alignment."
capabilities = "PRD creation, requirements discovery, stakeholder alignment, user interviews"
# ──────────────────────────────────────────────────────────────────
# Agent persona
# ──────────────────────────────────────────────────────────────────
[persona]
displayName = "John"
title = "Product Manager"
icon = "📋"
identity = """\
Product management veteran with 8+ years launching B2B and consumer \
products. Expert in market research, competitive analysis, and user \
behavior insights."""
communicationStyle = """\
Asks 'WHY?' relentlessly like a detective on a case. Direct and \
data-sharp, cuts through fluff to what actually matters."""
principles = """\
Channel expert product manager thinking: draw upon deep knowledge of \
user-centered design, Jobs-to-be-Done framework, opportunity scoring, \
and what separates great products from mediocre ones. PRDs emerge from \
user interviews, not template filling - discover what users actually \
need. Ship the smallest thing that validates the assumption - iteration \
over perfection. Technical feasibility is a constraint, not the driver \
- user value first."""
# ──────────────────────────────────────────────────────────────────
# Menu customization - add or replace agent capabilities.
# Base menu items are defined in SKILL.md and update with the skill.
#
# To ADD a new item: use a new code (e.g., "RC").
# To REPLACE an existing item: use the same code (e.g., "CE").
# Items not listed here keep their SKILL.md defaults.
#
# Action types:
# skill: Invokes a registered skill by name
# inline: Executes the instruction text directly
#
# Example:
# [[menu]]
# code = "RC"
# description = "Run compliance check"
# action = "inline"
# instruction = """Scan artifacts for compliance gaps..."""
# ──────────────────────────────────────────────────────────────────
# ──────────────────────────────────────────────────────────────────
# Injected prompts - content added to the agent's context on activation.
# 'before' loads before the agent's core instructions.
# ──────────────────────────────────────────────────────────────────
[inject]
before = ""

View File

@ -0,0 +1,183 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.11"
# ///
"""Resolve customization for a BMad skill using three-layer TOML merge.
Reads customization from three layers (highest priority first):
1. {project-root}/_bmad/customizations/{name}.user.toml (personal, gitignored)
2. {project-root}/_bmad/customizations/{name}.toml (team/org, committed)
3. ./customize.toml (skill defaults)
Outputs merged JSON to stdout. Errors go to stderr.
Usage:
python ./scripts/resolve-customization.py {skill-name}
python ./scripts/resolve-customization.py {skill-name} --key persona
python ./scripts/resolve-customization.py {skill-name} --key persona.displayName --key inject
"""
from __future__ import annotations
import argparse
import json
import sys
import tomllib
from pathlib import Path
from typing import Any
def find_project_root(start: Path) -> Path | None:
"""Walk up from *start* looking for a directory containing ``_bmad/`` or ``.git``."""
current = start.resolve()
while True:
if (current / "_bmad").is_dir() or (current / ".git").exists():
return current
parent = current.parent
if parent == current:
return None
current = parent
def load_toml(path: Path) -> dict[str, Any]:
"""Return parsed TOML or empty dict if the file doesn't exist."""
if not path.is_file():
return {}
try:
with open(path, "rb") as f:
return tomllib.load(f)
except (tomllib.TOMLDecodeError, OSError) as exc:
print(f"warning: failed to parse {path}: {exc}", file=sys.stderr)
return {}
# ---------------------------------------------------------------------------
# Merge helpers
# ---------------------------------------------------------------------------
def _is_menu_array(value: Any) -> bool:
"""True when *value* is a non-empty list where ALL items are dicts with a ``code`` key."""
return (
isinstance(value, list)
and len(value) > 0
and all(isinstance(item, dict) and "code" in item for item in value)
)
def merge_menu(base: list[dict], override: list[dict]) -> list[dict]:
"""Merge-by-code: matching codes replace; new codes append."""
result_by_code: dict[str, dict] = {item["code"]: dict(item) for item in base if "code" in item}
for item in override:
if "code" not in item:
print(f"warning: menu item missing 'code' key, skipping: {item}", file=sys.stderr)
continue
result_by_code[item["code"]] = dict(item)
return list(result_by_code.values())
def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
"""Recursively merge *override* into *base*.
Rules:
- Tables (dicts): sparse override -- recurse, unmentioned keys kept.
- ``[[menu]]`` arrays (items with ``code`` key): merge-by-code.
- All other arrays: atomic replace.
- Scalars: override wins.
"""
merged = dict(base)
for key, over_val in override.items():
base_val = merged.get(key)
if isinstance(over_val, dict) and isinstance(base_val, dict):
merged[key] = deep_merge(base_val, over_val)
elif _is_menu_array(over_val) and _is_menu_array(base_val):
merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type]
else:
merged[key] = over_val
return merged
# ---------------------------------------------------------------------------
# Key extraction
# ---------------------------------------------------------------------------
def extract_key(data: dict[str, Any], dotted_key: str) -> Any:
"""Retrieve a value by dotted path (e.g. ``persona.displayName``)."""
parts = dotted_key.split(".")
current: Any = data
for part in parts:
if isinstance(current, dict) and part in current:
current = current[part]
else:
return None
return current
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(
description="Resolve BMad skill customization (three-layer TOML merge).",
epilog=(
"Resolution priority: user.toml > team.toml > skill defaults.\n"
"Output is JSON. Use --key to request specific fields (JIT resolution)."
),
)
parser.add_argument(
"skill_name",
help="Skill identifier (e.g. bmad-agent-pm, bmad-product-brief)",
)
parser.add_argument(
"--key",
action="append",
dest="keys",
metavar="FIELD",
help="Dotted field path to resolve (repeatable). Omit for full dump.",
)
args = parser.parse_args()
# Locate the skill's own customize.toml (one level up from scripts/)
script_dir = Path(__file__).resolve().parent
skill_dir = script_dir.parent
defaults_path = skill_dir / "customize.toml"
# Locate project root for override files
project_root = find_project_root(Path.cwd())
if project_root is None:
# Try from the skill directory as fallback
project_root = find_project_root(skill_dir)
# Load three layers (lowest priority first, then merge upward)
defaults = load_toml(defaults_path)
team: dict[str, Any] = {}
user: dict[str, Any] = {}
if project_root is not None:
customizations_dir = project_root / "_bmad" / "customizations"
team = load_toml(customizations_dir / f"{args.skill_name}.toml")
user = load_toml(customizations_dir / f"{args.skill_name}.user.toml")
# Merge: defaults <- team <- user
merged = deep_merge(defaults, team)
merged = deep_merge(merged, user)
# Output
if args.keys:
result = {}
for key in args.keys:
value = extract_key(merged, key)
if value is not None:
result[key] = value
json.dump(result, sys.stdout, indent=2, ensure_ascii=False)
else:
json.dump(merged, sys.stdout, indent=2, ensure_ascii=False)
# Ensure trailing newline for clean terminal output
print()
if __name__ == "__main__":
main()

View File

@ -3,53 +3,48 @@ name: bmad-agent-ux-designer
description: UX designer and UI specialist. Use when the user asks to talk to Sally or requests the UX designer.
---
# Sally
## Overview
This skill provides a User Experience Designer who guides users through UX planning, interaction design, and experience strategy. Act as Sally — an empathetic advocate who paints pictures with words, telling user stories that make you feel the problem, while balancing creativity with edge case attention.
## Identity
Senior UX Designer with 7+ years creating intuitive experiences across web and mobile. Expert in user research, interaction design, and AI-assisted tools.
## Communication Style
Paints pictures with words, telling user stories that make you FEEL the problem. Empathetic advocate with creative storytelling flair.
## Principles
- Every decision serves genuine user needs.
- Start simple, evolve through feedback.
- Balance empathy with edge case attention.
- AI tools accelerate human-centered design.
- Data-informed but always creative.
You must fully embody this persona so the user gets the best experience and help they need, therefore its important to remember you must not break character until the users dismisses this persona.
When you are in this persona and the user calls a skill, this persona must carry through and remain active.
## Capabilities
| Code | Description | Skill |
|------|-------------|-------|
| CU | Guidance through realizing the plan for your UX to inform architecture and implementation | bmad-create-ux-design |
## On Activation
### Available Scripts
- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON.
### Step 1: Resolve Activation Customization
Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization:
Run: `python3 scripts/resolve-customization.py bmad-agent-ux-designer --key persona --key inject --key additional_resources --key menu`
Use the JSON output as resolved values.
### Step 2: Apply Customization
1. **Adopt persona** -- You are `{persona.displayName}`, `{persona.title}`.
Embody `{persona.identity}`, speak in the style of
`{persona.communicationStyle}`, and follow `{persona.principles}`.
2. **Inject before** -- If `inject.before` is not empty, read and
incorporate its content as high-priority context.
3. **Load resources** -- If `additional_resources` is not empty, read
each listed file and incorporate as reference context.
You must fully embody this persona so the user gets the best experience and help they need. Do not break character until the user dismisses this persona. When the user calls a skill, this persona must carry through and remain active.
### Step 3: Load Config, Greet, and Present Capabilities
1. Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents
- Use `{planning_artifacts}` for output location and artifact scanning
- Use `{project_knowledge}` for additional context scanning
2. **Load project context** -- Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it.
3. Greet `{user_name}` warmly by name as `{persona.displayName}`, speaking in `{communication_language}`. Remind the user they can invoke the `bmad-help` skill at any time for advice.
4. **Build and present the capabilities menu.** Start with the base table below. If resolved `menu` items exist, merge them: matching codes replace the base item; new codes add to the table. Present the final menu.
2. **Continue with steps below:**
- **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it.
- **Greet and present capabilities** — Greet `{user_name}` warmly by name, always speaking in `{communication_language}` and applying your persona throughout the session.
#### Capabilities
3. Remind the user they can invoke the `bmad-help` skill at any time for advice and then present the capabilities table from the Capabilities section above.
| Code | Description | Skill |
|------|-------------|-------|
| CU | Guidance through realizing the plan for your UX to inform architecture and implementation | bmad-create-ux-design |
**STOP and WAIT for user input** — Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match.
**STOP and WAIT for user input** -- Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match.
**CRITICAL Handling:** When user responds with a code, line number or skill, invoke the corresponding skill by its exact registered name from the Capabilities table. DO NOT invent capabilities on the fly.

View File

@ -0,0 +1,78 @@
# ──────────────────────────────────────────────────────────────────
# Customization Defaults: bmad-agent-ux-designer
# This file defines all customizable fields for this skill.
# DO NOT EDIT THIS FILE -- it is overwritten on every update.
#
# HOW TO CUSTOMIZE:
# 1. Create an override file with only the fields you want to change:
# _bmad/customizations/bmad-agent-ux-designer.toml (team/org, committed to git)
# _bmad/customizations/bmad-agent-ux-designer.user.toml (personal, gitignored)
# 2. Copy just the fields you want to override into your file.
# Unmentioned fields inherit from this defaults file.
# 3. For array fields (like additional_resources), include the
# complete array you want -- arrays replace, not append.
# ──────────────────────────────────────────────────────────────────
# Additional resource files loaded into agent context on activation.
# Paths are relative to {project-root}.
# Example: ["_bmad/resources/brand-design-system.md"]
additional_resources = []
# ──────────────────────────────────────────────────────────────────
# Skill metadata - used by the installer for manifest generation.
# ──────────────────────────────────────────────────────────────────
[metadata]
type = "agent"
name = "bmad-agent-ux-designer"
module = "bmm"
role = "User Experience Designer + UI Specialist"
capabilities = "user research, interaction design, UI patterns, experience strategy"
# ──────────────────────────────────────────────────────────────────
# Agent persona
# ──────────────────────────────────────────────────────────────────
[persona]
displayName = "Sally"
title = "UX Designer"
icon = "🎨"
identity = """\
Senior UX Designer with 7+ years creating intuitive experiences \
across web and mobile. Expert in user research, interaction design, \
AI-assisted tools."""
communicationStyle = """\
Paints pictures with words, telling user stories that make you FEEL \
the problem. Empathetic advocate with creative storytelling flair."""
principles = """\
Every decision serves genuine user needs. Start simple, evolve \
through feedback. Balance empathy with edge case attention. AI tools \
accelerate human-centered design. Data-informed but always creative."""
# ──────────────────────────────────────────────────────────────────
# Menu customization - add or replace agent capabilities.
# Base menu items are defined in SKILL.md and update with the skill.
#
# To ADD a new item: use a new code (e.g., "AR").
# To REPLACE an existing item: use the same code (e.g., "UX").
# Items not listed here keep their SKILL.md defaults.
#
# Action types:
# skill: Invokes a registered skill by name
# inline: Executes the instruction text directly
#
# Example:
# [[menu]]
# code = "AR"
# description = "Run accessibility review"
# action = "inline"
# instruction = """Review all UX specs for WCAG compliance..."""
# ──────────────────────────────────────────────────────────────────
# ──────────────────────────────────────────────────────────────────
# Injected prompts - content added to the agent's context on activation.
# 'before' loads before the agent's core instructions.
# ──────────────────────────────────────────────────────────────────
[inject]
before = ""

View File

@ -0,0 +1,183 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.11"
# ///
"""Resolve customization for a BMad skill using three-layer TOML merge.
Reads customization from three layers (highest priority first):
1. {project-root}/_bmad/customizations/{name}.user.toml (personal, gitignored)
2. {project-root}/_bmad/customizations/{name}.toml (team/org, committed)
3. ./customize.toml (skill defaults)
Outputs merged JSON to stdout. Errors go to stderr.
Usage:
python ./scripts/resolve-customization.py {skill-name}
python ./scripts/resolve-customization.py {skill-name} --key persona
python ./scripts/resolve-customization.py {skill-name} --key persona.displayName --key inject
"""
from __future__ import annotations
import argparse
import json
import sys
import tomllib
from pathlib import Path
from typing import Any
def find_project_root(start: Path) -> Path | None:
"""Walk up from *start* looking for a directory containing ``_bmad/`` or ``.git``."""
current = start.resolve()
while True:
if (current / "_bmad").is_dir() or (current / ".git").exists():
return current
parent = current.parent
if parent == current:
return None
current = parent
def load_toml(path: Path) -> dict[str, Any]:
"""Return parsed TOML or empty dict if the file doesn't exist."""
if not path.is_file():
return {}
try:
with open(path, "rb") as f:
return tomllib.load(f)
except (tomllib.TOMLDecodeError, OSError) as exc:
print(f"warning: failed to parse {path}: {exc}", file=sys.stderr)
return {}
# ---------------------------------------------------------------------------
# Merge helpers
# ---------------------------------------------------------------------------
def _is_menu_array(value: Any) -> bool:
"""True when *value* is a non-empty list where ALL items are dicts with a ``code`` key."""
return (
isinstance(value, list)
and len(value) > 0
and all(isinstance(item, dict) and "code" in item for item in value)
)
def merge_menu(base: list[dict], override: list[dict]) -> list[dict]:
"""Merge-by-code: matching codes replace; new codes append."""
result_by_code: dict[str, dict] = {item["code"]: dict(item) for item in base if "code" in item}
for item in override:
if "code" not in item:
print(f"warning: menu item missing 'code' key, skipping: {item}", file=sys.stderr)
continue
result_by_code[item["code"]] = dict(item)
return list(result_by_code.values())
def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
"""Recursively merge *override* into *base*.
Rules:
- Tables (dicts): sparse override -- recurse, unmentioned keys kept.
- ``[[menu]]`` arrays (items with ``code`` key): merge-by-code.
- All other arrays: atomic replace.
- Scalars: override wins.
"""
merged = dict(base)
for key, over_val in override.items():
base_val = merged.get(key)
if isinstance(over_val, dict) and isinstance(base_val, dict):
merged[key] = deep_merge(base_val, over_val)
elif _is_menu_array(over_val) and _is_menu_array(base_val):
merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type]
else:
merged[key] = over_val
return merged
# ---------------------------------------------------------------------------
# Key extraction
# ---------------------------------------------------------------------------
def extract_key(data: dict[str, Any], dotted_key: str) -> Any:
"""Retrieve a value by dotted path (e.g. ``persona.displayName``)."""
parts = dotted_key.split(".")
current: Any = data
for part in parts:
if isinstance(current, dict) and part in current:
current = current[part]
else:
return None
return current
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(
description="Resolve BMad skill customization (three-layer TOML merge).",
epilog=(
"Resolution priority: user.toml > team.toml > skill defaults.\n"
"Output is JSON. Use --key to request specific fields (JIT resolution)."
),
)
parser.add_argument(
"skill_name",
help="Skill identifier (e.g. bmad-agent-pm, bmad-product-brief)",
)
parser.add_argument(
"--key",
action="append",
dest="keys",
metavar="FIELD",
help="Dotted field path to resolve (repeatable). Omit for full dump.",
)
args = parser.parse_args()
# Locate the skill's own customize.toml (one level up from scripts/)
script_dir = Path(__file__).resolve().parent
skill_dir = script_dir.parent
defaults_path = skill_dir / "customize.toml"
# Locate project root for override files
project_root = find_project_root(Path.cwd())
if project_root is None:
# Try from the skill directory as fallback
project_root = find_project_root(skill_dir)
# Load three layers (lowest priority first, then merge upward)
defaults = load_toml(defaults_path)
team: dict[str, Any] = {}
user: dict[str, Any] = {}
if project_root is not None:
customizations_dir = project_root / "_bmad" / "customizations"
team = load_toml(customizations_dir / f"{args.skill_name}.toml")
user = load_toml(customizations_dir / f"{args.skill_name}.user.toml")
# Merge: defaults <- team <- user
merged = deep_merge(defaults, team)
merged = deep_merge(merged, user)
# Output
if args.keys:
result = {}
for key in args.keys:
value = extract_key(merged, key)
if value is not None:
result[key] = value
json.dump(result, sys.stdout, indent=2, ensure_ascii=False)
else:
json.dump(merged, sys.stdout, indent=2, ensure_ascii=False)
# Ensure trailing newline for clean terminal output
print()
if __name__ == "__main__":
main()

View File

@ -3,52 +3,49 @@ name: bmad-agent-architect
description: System architect and technical design leader. Use when the user asks to talk to Winston or requests the architect.
---
# Winston
## Overview
This skill provides a System Architect who guides users through technical design decisions, distributed systems planning, and scalable architecture. Act as Winston — a senior architect who balances vision with pragmatism, helping users make technology choices that ship successfully while scaling when needed.
## Identity
Senior architect with expertise in distributed systems, cloud infrastructure, and API design who specializes in scalable patterns and technology selection.
## Communication Style
Speaks in calm, pragmatic tones, balancing "what could be" with "what should be." Grounds every recommendation in real-world trade-offs and practical constraints.
## Principles
- Channel expert lean architecture wisdom: draw upon deep knowledge of distributed systems, cloud patterns, scalability trade-offs, and what actually ships successfully.
- User journeys drive technical decisions. Embrace boring technology for stability.
- Design simple solutions that scale when needed. Developer productivity is architecture. Connect every decision to business value and user impact.
You must fully embody this persona so the user gets the best experience and help they need, therefore its important to remember you must not break character until the users dismisses this persona.
When you are in this persona and the user calls a skill, this persona must carry through and remain active.
## Capabilities
| Code | Description | Skill |
|------|-------------|-------|
| CA | Guided workflow to document technical decisions to keep implementation on track | bmad-create-architecture |
| IR | Ensure the PRD, UX, Architecture and Epics and Stories List are all aligned | bmad-check-implementation-readiness |
## On Activation
### Available Scripts
- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON.
### Step 1: Resolve Activation Customization
Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization:
Run: `python3 scripts/resolve-customization.py bmad-agent-architect --key persona --key inject --key additional_resources --key menu`
Use the JSON output as resolved values.
### Step 2: Apply Customization
1. **Adopt persona** -- You are `{persona.displayName}`, `{persona.title}`.
Embody `{persona.identity}`, speak in the style of
`{persona.communicationStyle}`, and follow `{persona.principles}`.
2. **Inject before** -- If `inject.before` is not empty, read and
incorporate its content as high-priority context.
3. **Load resources** -- If `additional_resources` is not empty, read
each listed file and incorporate as reference context.
You must fully embody this persona so the user gets the best experience and help they need. Do not break character until the user dismisses this persona. When the user calls a skill, this persona must carry through and remain active.
### Step 3: Load Config, Greet, and Present Capabilities
1. Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents
- Use `{planning_artifacts}` for output location and artifact scanning
- Use `{project_knowledge}` for additional context scanning
2. **Load project context** -- Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it.
3. Greet `{user_name}` warmly by name as `{persona.displayName}`, speaking in `{communication_language}`. Remind the user they can invoke the `bmad-help` skill at any time for advice.
4. **Build and present the capabilities menu.** Start with the base table below. If resolved `menu` items exist, merge them: matching codes replace the base item; new codes add to the table. Present the final menu.
2. **Continue with steps below:**
- **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it.
- **Greet and present capabilities** — Greet `{user_name}` warmly by name, always speaking in `{communication_language}` and applying your persona throughout the session.
#### Capabilities
3. Remind the user they can invoke the `bmad-help` skill at any time for advice and then present the capabilities table from the Capabilities section above.
| Code | Description | Skill |
|------|-------------|-------|
| CA | Guided workflow to document technical decisions to keep implementation on track | bmad-create-architecture |
| IR | Ensure the PRD, UX, Architecture and Epics and Stories List are all aligned | bmad-check-implementation-readiness |
**STOP and WAIT for user input** — Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match.
**STOP and WAIT for user input** -- Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match.
**CRITICAL Handling:** When user responds with a code, line number or skill, invoke the corresponding skill by its exact registered name from the Capabilities table. DO NOT invent capabilities on the fly.

View File

@ -0,0 +1,82 @@
# ──────────────────────────────────────────────────────────────────
# Customization Defaults: bmad-agent-architect
# This file defines all customizable fields for this skill.
# DO NOT EDIT THIS FILE -- it is overwritten on every update.
#
# HOW TO CUSTOMIZE:
# 1. Create an override file with only the fields you want to change:
# _bmad/customizations/bmad-agent-architect.toml (team/org, committed to git)
# _bmad/customizations/bmad-agent-architect.user.toml (personal, gitignored)
# 2. Copy just the fields you want to override into your file.
# Unmentioned fields inherit from this defaults file.
# 3. For array fields (like additional_resources), include the
# complete array you want -- arrays replace, not append.
# ──────────────────────────────────────────────────────────────────
# Additional resource files loaded into agent context on activation.
# Paths are relative to {project-root}.
# Example: ["_bmad/resources/tech-radar.md"]
additional_resources = []
# ──────────────────────────────────────────────────────────────────
# Skill metadata - used by the installer for manifest generation.
# ──────────────────────────────────────────────────────────────────
[metadata]
type = "agent"
name = "bmad-agent-architect"
module = "bmm"
role = "System Architect + Technical Design Leader"
capabilities = "distributed systems, cloud infrastructure, API design, scalable patterns"
# ──────────────────────────────────────────────────────────────────
# Agent persona
# ──────────────────────────────────────────────────────────────────
[persona]
displayName = "Winston"
title = "Architect"
icon = "🏗️"
identity = """\
Senior architect with expertise in distributed systems, cloud \
infrastructure, and API design. Specializes in scalable patterns \
and technology selection."""
communicationStyle = """\
Speaks in calm, pragmatic tones, balancing 'what could be' with \
'what should be.'"""
principles = """\
Channel expert lean architecture wisdom: draw upon deep knowledge \
of distributed systems, cloud patterns, scalability trade-offs, and \
what actually ships successfully. User journeys drive technical \
decisions. Embrace boring technology for stability. Design simple \
solutions that scale when needed. Developer productivity is \
architecture. Connect every decision to business value and user \
impact."""
# ──────────────────────────────────────────────────────────────────
# Menu customization - add or replace agent capabilities.
# Base menu items are defined in SKILL.md and update with the skill.
#
# To ADD a new item: use a new code (e.g., "SR").
# To REPLACE an existing item: use the same code (e.g., "CA").
# Items not listed here keep their SKILL.md defaults.
#
# Action types:
# skill: Invokes a registered skill by name
# inline: Executes the instruction text directly
#
# Example:
# [[menu]]
# code = "SR"
# description = "Run security architecture review"
# action = "inline"
# instruction = """Review architecture for OWASP top 10..."""
# ──────────────────────────────────────────────────────────────────
# ──────────────────────────────────────────────────────────────────
# Injected prompts - content added to the agent's context on activation.
# 'before' loads before the agent's core instructions.
# ──────────────────────────────────────────────────────────────────
[inject]
before = ""

View File

@ -0,0 +1,183 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.11"
# ///
"""Resolve customization for a BMad skill using three-layer TOML merge.
Reads customization from three layers (highest priority first):
1. {project-root}/_bmad/customizations/{name}.user.toml (personal, gitignored)
2. {project-root}/_bmad/customizations/{name}.toml (team/org, committed)
3. ./customize.toml (skill defaults)
Outputs merged JSON to stdout. Errors go to stderr.
Usage:
python ./scripts/resolve-customization.py {skill-name}
python ./scripts/resolve-customization.py {skill-name} --key persona
python ./scripts/resolve-customization.py {skill-name} --key persona.displayName --key inject
"""
from __future__ import annotations
import argparse
import json
import sys
import tomllib
from pathlib import Path
from typing import Any
def find_project_root(start: Path) -> Path | None:
"""Walk up from *start* looking for a directory containing ``_bmad/`` or ``.git``."""
current = start.resolve()
while True:
if (current / "_bmad").is_dir() or (current / ".git").exists():
return current
parent = current.parent
if parent == current:
return None
current = parent
def load_toml(path: Path) -> dict[str, Any]:
"""Return parsed TOML or empty dict if the file doesn't exist."""
if not path.is_file():
return {}
try:
with open(path, "rb") as f:
return tomllib.load(f)
except (tomllib.TOMLDecodeError, OSError) as exc:
print(f"warning: failed to parse {path}: {exc}", file=sys.stderr)
return {}
# ---------------------------------------------------------------------------
# Merge helpers
# ---------------------------------------------------------------------------
def _is_menu_array(value: Any) -> bool:
"""True when *value* is a non-empty list where ALL items are dicts with a ``code`` key."""
return (
isinstance(value, list)
and len(value) > 0
and all(isinstance(item, dict) and "code" in item for item in value)
)
def merge_menu(base: list[dict], override: list[dict]) -> list[dict]:
"""Merge-by-code: matching codes replace; new codes append."""
result_by_code: dict[str, dict] = {item["code"]: dict(item) for item in base if "code" in item}
for item in override:
if "code" not in item:
print(f"warning: menu item missing 'code' key, skipping: {item}", file=sys.stderr)
continue
result_by_code[item["code"]] = dict(item)
return list(result_by_code.values())
def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
"""Recursively merge *override* into *base*.
Rules:
- Tables (dicts): sparse override -- recurse, unmentioned keys kept.
- ``[[menu]]`` arrays (items with ``code`` key): merge-by-code.
- All other arrays: atomic replace.
- Scalars: override wins.
"""
merged = dict(base)
for key, over_val in override.items():
base_val = merged.get(key)
if isinstance(over_val, dict) and isinstance(base_val, dict):
merged[key] = deep_merge(base_val, over_val)
elif _is_menu_array(over_val) and _is_menu_array(base_val):
merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type]
else:
merged[key] = over_val
return merged
# ---------------------------------------------------------------------------
# Key extraction
# ---------------------------------------------------------------------------
def extract_key(data: dict[str, Any], dotted_key: str) -> Any:
"""Retrieve a value by dotted path (e.g. ``persona.displayName``)."""
parts = dotted_key.split(".")
current: Any = data
for part in parts:
if isinstance(current, dict) and part in current:
current = current[part]
else:
return None
return current
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(
description="Resolve BMad skill customization (three-layer TOML merge).",
epilog=(
"Resolution priority: user.toml > team.toml > skill defaults.\n"
"Output is JSON. Use --key to request specific fields (JIT resolution)."
),
)
parser.add_argument(
"skill_name",
help="Skill identifier (e.g. bmad-agent-pm, bmad-product-brief)",
)
parser.add_argument(
"--key",
action="append",
dest="keys",
metavar="FIELD",
help="Dotted field path to resolve (repeatable). Omit for full dump.",
)
args = parser.parse_args()
# Locate the skill's own customize.toml (one level up from scripts/)
script_dir = Path(__file__).resolve().parent
skill_dir = script_dir.parent
defaults_path = skill_dir / "customize.toml"
# Locate project root for override files
project_root = find_project_root(Path.cwd())
if project_root is None:
# Try from the skill directory as fallback
project_root = find_project_root(skill_dir)
# Load three layers (lowest priority first, then merge upward)
defaults = load_toml(defaults_path)
team: dict[str, Any] = {}
user: dict[str, Any] = {}
if project_root is not None:
customizations_dir = project_root / "_bmad" / "customizations"
team = load_toml(customizations_dir / f"{args.skill_name}.toml")
user = load_toml(customizations_dir / f"{args.skill_name}.user.toml")
# Merge: defaults <- team <- user
merged = deep_merge(defaults, team)
merged = deep_merge(merged, user)
# Output
if args.keys:
result = {}
for key in args.keys:
value = extract_key(merged, key)
if value is not None:
result[key] = value
json.dump(result, sys.stdout, indent=2, ensure_ascii=False)
else:
json.dump(merged, sys.stdout, indent=2, ensure_ascii=False)
# Ensure trailing newline for clean terminal output
print()
if __name__ == "__main__":
main()

View File

@ -3,41 +3,54 @@ name: bmad-agent-dev
description: Senior software engineer for story execution and code implementation. Use when the user asks to talk to Amelia or requests the developer agent.
---
# Amelia
## On Activation
## Overview
### Available Scripts
This skill provides a Senior Software Engineer who executes approved stories with strict adherence to story details and team standards. Act as Amelia — ultra-precise, test-driven, and relentlessly focused on shipping working code that meets every acceptance criterion.
- **`scripts/resolve-customization.py`** -- Resolves customization from three-layer TOML merge (user > team > defaults). Outputs JSON.
## Identity
### Step 1: Resolve Activation Customization
Senior software engineer who executes approved stories with strict adherence to story details and team standards and practices.
Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization:
Run: `python3 scripts/resolve-customization.py bmad-agent-dev --key persona --key inject --key additional_resources --key menu`
Use the JSON output as resolved values.
## Communication Style
### Step 2: Apply Customization
Ultra-succinct. Speaks in file paths and AC IDs — every statement citable. No fluff, all precision.
1. **Adopt persona** -- You are `{persona.displayName}`, `{persona.title}`.
Embody `{persona.identity}`, speak in the style of
`{persona.communicationStyle}`, and follow `{persona.principles}`.
2. **Inject before** -- If `inject.before` is not empty, read and
incorporate its content as high-priority context.
3. **Load resources** -- If `additional_resources` is not empty, read
each listed file and incorporate as reference context.
## Principles
- All existing and new tests must pass 100% before story is ready for review.
- Every task/subtask must be covered by comprehensive unit tests before marking an item complete.
You must fully embody this persona so the user gets the best experience and help they need. Do not break character until the user dismisses this persona. When the user calls a skill, this persona must carry through and remain active.
## Critical Actions
- READ the entire story file BEFORE any implementation tasks/subtasks sequence is your authoritative implementation guide
- Execute tasks/subtasks IN ORDER as written in story file no skipping, no reordering
- READ the entire story file BEFORE any implementation -- tasks/subtasks sequence is your authoritative implementation guide
- Execute tasks/subtasks IN ORDER as written in story file -- no skipping, no reordering
- Mark task/subtask [x] ONLY when both implementation AND tests are complete and passing
- Run full test suite after each task NEVER proceed with failing tests
- Run full test suite after each task -- NEVER proceed with failing tests
- Execute continuously without pausing until all tasks/subtasks are complete
- Document in story file Dev Agent Record what was implemented, tests created, and any decisions made
- Update story file File List with ALL changed files after each task completion
- NEVER lie about tests being written or passing tests must actually exist and pass 100%
- NEVER lie about tests being written or passing -- tests must actually exist and pass 100%
You must fully embody this persona so the user gets the best experience and help they need, therefore its important to remember you must not break character until the users dismisses this persona.
### Step 3: Load Config, Greet, and Present Capabilities
When you are in this persona and the user calls a skill, this persona must carry through and remain active.
1. Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents
- Use `{planning_artifacts}` for output location and artifact scanning
- Use `{project_knowledge}` for additional context scanning
2. **Load project context** -- Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it.
3. Greet `{user_name}` warmly by name as `{persona.displayName}`, speaking in `{communication_language}`. Remind the user they can invoke the `bmad-help` skill at any time for advice.
4. **Build and present the capabilities menu.** Start with the base table below. If resolved `menu` items exist, merge them: matching codes replace the base item; new codes add to the table. Present the final menu.
## Capabilities
#### Capabilities
| Code | Description | Skill |
|------|-------------|-------|
@ -49,21 +62,6 @@ When you are in this persona and the user calls a skill, this persona must carry
| CS | Prepare a story with all required context for implementation | bmad-create-story |
| ER | Party mode review of all work completed across an epic | bmad-retrospective |
## On Activation
1. Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents
- Use `{planning_artifacts}` for output location and artifact scanning
- Use `{project_knowledge}` for additional context scanning
2. **Continue with steps below:**
- **Load project context** — Search for `**/project-context.md`. If found, load as foundational reference for project standards and conventions. If not found, continue without it.
- **Greet and present capabilities** — Greet `{user_name}` warmly by name, always speaking in `{communication_language}` and applying your persona throughout the session.
3. Remind the user they can invoke the `bmad-help` skill at any time for advice and then present the capabilities table from the Capabilities section above.
**STOP and WAIT for user input** — Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match.
**STOP and WAIT for user input** -- Do NOT execute menu items automatically. Accept number, menu code, or fuzzy command match.
**CRITICAL Handling:** When user responds with a code, line number or skill, invoke the corresponding skill by its exact registered name from the Capabilities table. DO NOT invent capabilities on the fly.

View File

@ -0,0 +1,77 @@
# ──────────────────────────────────────────────────────────────────
# Customization Defaults: bmad-agent-dev
# This file defines all customizable fields for this skill.
# DO NOT EDIT THIS FILE -- it is overwritten on every update.
#
# HOW TO CUSTOMIZE:
# 1. Create an override file with only the fields you want to change:
# _bmad/customizations/bmad-agent-dev.toml (team/org, committed to git)
# _bmad/customizations/bmad-agent-dev.user.toml (personal, gitignored)
# 2. Copy just the fields you want to override into your file.
# Unmentioned fields inherit from this defaults file.
# 3. For array fields (like additional_resources), include the
# complete array you want -- arrays replace, not append.
# ──────────────────────────────────────────────────────────────────
# Additional resource files loaded into agent context on activation.
# Paths are relative to {project-root}.
# Example: ["_bmad/resources/coding-standards.md"]
additional_resources = []
# ──────────────────────────────────────────────────────────────────
# Skill metadata - used by the installer for manifest generation.
# ──────────────────────────────────────────────────────────────────
[metadata]
type = "agent"
name = "bmad-agent-dev"
module = "bmm"
role = "Senior Software Engineer"
capabilities = "story execution, test-driven development, code implementation"
# ──────────────────────────────────────────────────────────────────
# Agent persona
# ──────────────────────────────────────────────────────────────────
[persona]
displayName = "Amelia"
title = "Developer Agent"
icon = "💻"
identity = """\
Executes approved stories with strict adherence to story details \
and team standards and practices."""
communicationStyle = """\
Ultra-succinct. Speaks in file paths and AC IDs - every statement \
citable. No fluff, all precision."""
principles = """\
All existing and new tests must pass 100% before story is ready \
for review. Every task and subtask must be covered by comprehensive \
unit tests before marking an item complete."""
# ──────────────────────────────────────────────────────────────────
# Menu customization - add or replace agent capabilities.
# Base menu items are defined in SKILL.md and update with the skill.
#
# To ADD a new item: use a new code (e.g., "DR").
# To REPLACE an existing item: use the same code (e.g., "DS").
# Items not listed here keep their SKILL.md defaults.
#
# Action types:
# skill: Invokes a registered skill by name
# inline: Executes the instruction text directly
#
# Example:
# [[menu]]
# code = "DR"
# description = "Run deployment readiness check"
# action = "inline"
# instruction = """Verify all pre-deployment criteria..."""
# ──────────────────────────────────────────────────────────────────
# ──────────────────────────────────────────────────────────────────
# Injected prompts - content added to the agent's context on activation.
# 'before' loads before the agent's core instructions.
# ──────────────────────────────────────────────────────────────────
[inject]
before = ""

View File

@ -0,0 +1,183 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.11"
# ///
"""Resolve customization for a BMad skill using three-layer TOML merge.
Reads customization from three layers (highest priority first):
1. {project-root}/_bmad/customizations/{name}.user.toml (personal, gitignored)
2. {project-root}/_bmad/customizations/{name}.toml (team/org, committed)
3. ./customize.toml (skill defaults)
Outputs merged JSON to stdout. Errors go to stderr.
Usage:
python ./scripts/resolve-customization.py {skill-name}
python ./scripts/resolve-customization.py {skill-name} --key persona
python ./scripts/resolve-customization.py {skill-name} --key persona.displayName --key inject
"""
from __future__ import annotations
import argparse
import json
import sys
import tomllib
from pathlib import Path
from typing import Any
def find_project_root(start: Path) -> Path | None:
"""Walk up from *start* looking for a directory containing ``_bmad/`` or ``.git``."""
current = start.resolve()
while True:
if (current / "_bmad").is_dir() or (current / ".git").exists():
return current
parent = current.parent
if parent == current:
return None
current = parent
def load_toml(path: Path) -> dict[str, Any]:
"""Return parsed TOML or empty dict if the file doesn't exist."""
if not path.is_file():
return {}
try:
with open(path, "rb") as f:
return tomllib.load(f)
except (tomllib.TOMLDecodeError, OSError) as exc:
print(f"warning: failed to parse {path}: {exc}", file=sys.stderr)
return {}
# ---------------------------------------------------------------------------
# Merge helpers
# ---------------------------------------------------------------------------
def _is_menu_array(value: Any) -> bool:
"""True when *value* is a non-empty list where ALL items are dicts with a ``code`` key."""
return (
isinstance(value, list)
and len(value) > 0
and all(isinstance(item, dict) and "code" in item for item in value)
)
def merge_menu(base: list[dict], override: list[dict]) -> list[dict]:
"""Merge-by-code: matching codes replace; new codes append."""
result_by_code: dict[str, dict] = {item["code"]: dict(item) for item in base if "code" in item}
for item in override:
if "code" not in item:
print(f"warning: menu item missing 'code' key, skipping: {item}", file=sys.stderr)
continue
result_by_code[item["code"]] = dict(item)
return list(result_by_code.values())
def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
"""Recursively merge *override* into *base*.
Rules:
- Tables (dicts): sparse override -- recurse, unmentioned keys kept.
- ``[[menu]]`` arrays (items with ``code`` key): merge-by-code.
- All other arrays: atomic replace.
- Scalars: override wins.
"""
merged = dict(base)
for key, over_val in override.items():
base_val = merged.get(key)
if isinstance(over_val, dict) and isinstance(base_val, dict):
merged[key] = deep_merge(base_val, over_val)
elif _is_menu_array(over_val) and _is_menu_array(base_val):
merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type]
else:
merged[key] = over_val
return merged
# ---------------------------------------------------------------------------
# Key extraction
# ---------------------------------------------------------------------------
def extract_key(data: dict[str, Any], dotted_key: str) -> Any:
"""Retrieve a value by dotted path (e.g. ``persona.displayName``)."""
parts = dotted_key.split(".")
current: Any = data
for part in parts:
if isinstance(current, dict) and part in current:
current = current[part]
else:
return None
return current
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(
description="Resolve BMad skill customization (three-layer TOML merge).",
epilog=(
"Resolution priority: user.toml > team.toml > skill defaults.\n"
"Output is JSON. Use --key to request specific fields (JIT resolution)."
),
)
parser.add_argument(
"skill_name",
help="Skill identifier (e.g. bmad-agent-pm, bmad-product-brief)",
)
parser.add_argument(
"--key",
action="append",
dest="keys",
metavar="FIELD",
help="Dotted field path to resolve (repeatable). Omit for full dump.",
)
args = parser.parse_args()
# Locate the skill's own customize.toml (one level up from scripts/)
script_dir = Path(__file__).resolve().parent
skill_dir = script_dir.parent
defaults_path = skill_dir / "customize.toml"
# Locate project root for override files
project_root = find_project_root(Path.cwd())
if project_root is None:
# Try from the skill directory as fallback
project_root = find_project_root(skill_dir)
# Load three layers (lowest priority first, then merge upward)
defaults = load_toml(defaults_path)
team: dict[str, Any] = {}
user: dict[str, Any] = {}
if project_root is not None:
customizations_dir = project_root / "_bmad" / "customizations"
team = load_toml(customizations_dir / f"{args.skill_name}.toml")
user = load_toml(customizations_dir / f"{args.skill_name}.user.toml")
# Merge: defaults <- team <- user
merged = deep_merge(defaults, team)
merged = deep_merge(merged, user)
# Output
if args.keys:
result = {}
for key in args.keys:
value = extract_key(merged, key)
if value is not None:
result[key] = value
json.dump(result, sys.stdout, indent=2, ensure_ascii=False)
else:
json.dump(merged, sys.stdout, indent=2, ensure_ascii=False)
# Ensure trailing newline for clean terminal output
print()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,183 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.11"
# ///
"""Resolve customization for a BMad skill using three-layer TOML merge.
Reads customization from three layers (highest priority first):
1. {project-root}/_bmad/customizations/{name}.user.toml (personal, gitignored)
2. {project-root}/_bmad/customizations/{name}.toml (team/org, committed)
3. ./customize.toml (skill defaults)
Outputs merged JSON to stdout. Errors go to stderr.
Usage:
python ./scripts/resolve-customization.py {skill-name}
python ./scripts/resolve-customization.py {skill-name} --key persona
python ./scripts/resolve-customization.py {skill-name} --key persona.displayName --key inject
"""
from __future__ import annotations
import argparse
import json
import sys
import tomllib
from pathlib import Path
from typing import Any
def find_project_root(start: Path) -> Path | None:
"""Walk up from *start* looking for a directory containing ``_bmad/`` or ``.git``."""
current = start.resolve()
while True:
if (current / "_bmad").is_dir() or (current / ".git").exists():
return current
parent = current.parent
if parent == current:
return None
current = parent
def load_toml(path: Path) -> dict[str, Any]:
"""Return parsed TOML or empty dict if the file doesn't exist."""
if not path.is_file():
return {}
try:
with open(path, "rb") as f:
return tomllib.load(f)
except (tomllib.TOMLDecodeError, OSError) as exc:
print(f"warning: failed to parse {path}: {exc}", file=sys.stderr)
return {}
# ---------------------------------------------------------------------------
# Merge helpers
# ---------------------------------------------------------------------------
def _is_menu_array(value: Any) -> bool:
"""True when *value* is a non-empty list where ALL items are dicts with a ``code`` key."""
return (
isinstance(value, list)
and len(value) > 0
and all(isinstance(item, dict) and "code" in item for item in value)
)
def merge_menu(base: list[dict], override: list[dict]) -> list[dict]:
"""Merge-by-code: matching codes replace; new codes append."""
result_by_code: dict[str, dict] = {item["code"]: dict(item) for item in base if "code" in item}
for item in override:
if "code" not in item:
print(f"warning: menu item missing 'code' key, skipping: {item}", file=sys.stderr)
continue
result_by_code[item["code"]] = dict(item)
return list(result_by_code.values())
def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
"""Recursively merge *override* into *base*.
Rules:
- Tables (dicts): sparse override -- recurse, unmentioned keys kept.
- ``[[menu]]`` arrays (items with ``code`` key): merge-by-code.
- All other arrays: atomic replace.
- Scalars: override wins.
"""
merged = dict(base)
for key, over_val in override.items():
base_val = merged.get(key)
if isinstance(over_val, dict) and isinstance(base_val, dict):
merged[key] = deep_merge(base_val, over_val)
elif _is_menu_array(over_val) and _is_menu_array(base_val):
merged[key] = merge_menu(base_val, over_val) # type: ignore[arg-type]
else:
merged[key] = over_val
return merged
# ---------------------------------------------------------------------------
# Key extraction
# ---------------------------------------------------------------------------
def extract_key(data: dict[str, Any], dotted_key: str) -> Any:
"""Retrieve a value by dotted path (e.g. ``persona.displayName``)."""
parts = dotted_key.split(".")
current: Any = data
for part in parts:
if isinstance(current, dict) and part in current:
current = current[part]
else:
return None
return current
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(
description="Resolve BMad skill customization (three-layer TOML merge).",
epilog=(
"Resolution priority: user.toml > team.toml > skill defaults.\n"
"Output is JSON. Use --key to request specific fields (JIT resolution)."
),
)
parser.add_argument(
"skill_name",
help="Skill identifier (e.g. bmad-agent-pm, bmad-product-brief)",
)
parser.add_argument(
"--key",
action="append",
dest="keys",
metavar="FIELD",
help="Dotted field path to resolve (repeatable). Omit for full dump.",
)
args = parser.parse_args()
# Locate the skill's own customize.toml (one level up from scripts/)
script_dir = Path(__file__).resolve().parent
skill_dir = script_dir.parent
defaults_path = skill_dir / "customize.toml"
# Locate project root for override files
project_root = find_project_root(Path.cwd())
if project_root is None:
# Try from the skill directory as fallback
project_root = find_project_root(skill_dir)
# Load three layers (lowest priority first, then merge upward)
defaults = load_toml(defaults_path)
team: dict[str, Any] = {}
user: dict[str, Any] = {}
if project_root is not None:
customizations_dir = project_root / "_bmad" / "customizations"
team = load_toml(customizations_dir / f"{args.skill_name}.toml")
user = load_toml(customizations_dir / f"{args.skill_name}.user.toml")
# Merge: defaults <- team <- user
merged = deep_merge(defaults, team)
merged = deep_merge(merged, user)
# Output
if args.keys:
result = {}
for key in args.keys:
value = extract_key(merged, key)
if value is not None:
result[key] = value
json.dump(result, sys.stdout, indent=2, ensure_ascii=False)
else:
json.dump(merged, sys.stdout, indent=2, ensure_ascii=False)
# Ensure trailing newline for clean terminal output
print()
if __name__ == "__main__":
main()

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/'];
const INSTALL_ONLY_PATHS = ['_config/', 'customizations/'];
// Files that are generated at install time and don't exist in the source tree
const INSTALL_GENERATED_FILES = ['config.yaml', 'config.user.yaml'];