feat(skills): add TOML-based skill customization system
Replace YAML customization with a three-layer TOML override model (skill defaults → team overrides → personal preferences). Each skill now ships a customize.toml with its full customization surface and a resolve-customization.py script that merges layers just-in-time during activation. Personal .user.toml files are gitignored; team .toml files are committed. Updates all 7 agent skills and product-brief workflow with new activation steps, customization defaults, and merge scripts. Rewrites the customize-bmad how-to guide for the new system.
This commit is contained in:
parent
6b964acd56
commit
9f936213a2
|
|
@ -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/*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -3,31 +3,47 @@ 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
|
||||
### Step 1: Resolve Activation Customization
|
||||
|
||||
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.
|
||||
Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization:
|
||||
Run: `python ./scripts/resolve-customization.py bmad-agent-analyst --key persona --key inject --key additional_resources --key menu`
|
||||
Use the JSON output as resolved values.
|
||||
|
||||
## Identity
|
||||
If script unavailable, read these sections from the following files
|
||||
(first found wins, most specific first):
|
||||
1. `{project-root}/_bmad/customizations/bmad-agent-analyst.user.toml` (if exists)
|
||||
2. `{project-root}/_bmad/customizations/bmad-agent-analyst.toml` (if exists)
|
||||
3. `./customize.toml` (last resort defaults)
|
||||
|
||||
Senior analyst with deep expertise in market research, competitive analysis, and requirements elicitation who specializes in translating vague needs into actionable specs.
|
||||
### Step 2: Apply Customization
|
||||
|
||||
## Communication Style
|
||||
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.
|
||||
4. **Inject after** -- If `inject.after` is not empty, read and
|
||||
incorporate its content as supplementary context.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Principles
|
||||
### Step 3: Load Config, Greet, and Present Capabilities
|
||||
|
||||
- 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.
|
||||
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.
|
||||
|
||||
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
|
||||
#### Capabilities
|
||||
|
||||
| Code | Description | Skill |
|
||||
|------|-------------|-------|
|
||||
|
|
@ -39,21 +55,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.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
# ──────────────────────────────────────────────────────────────────
|
||||
# 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.
|
||||
# 'after' loads after the agent's core instructions.
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
[inject]
|
||||
before = ""
|
||||
after = ""
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
#!/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 os
|
||||
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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys."""
|
||||
return (
|
||||
isinstance(value, list)
|
||||
and len(value) > 0
|
||||
and isinstance(value[0], dict)
|
||||
and "code" in value[0]
|
||||
)
|
||||
|
||||
|
||||
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}
|
||||
for item in override:
|
||||
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)
|
||||
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()
|
||||
|
|
@ -3,31 +3,47 @@ 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
|
||||
### Step 1: Resolve Activation Customization
|
||||
|
||||
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.
|
||||
Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization:
|
||||
Run: `python ./scripts/resolve-customization.py bmad-agent-tech-writer --key persona --key inject --key additional_resources --key menu`
|
||||
Use the JSON output as resolved values.
|
||||
|
||||
## Identity
|
||||
If script unavailable, read these sections from the following files
|
||||
(first found wins, most specific first):
|
||||
1. `{project-root}/_bmad/customizations/bmad-agent-tech-writer.user.toml` (if exists)
|
||||
2. `{project-root}/_bmad/customizations/bmad-agent-tech-writer.toml` (if exists)
|
||||
3. `./customize.toml` (last resort defaults)
|
||||
|
||||
Experienced technical writer expert in CommonMark, DITA, OpenAPI. Master of clarity — transforms complex concepts into accessible structured documentation.
|
||||
### Step 2: Apply Customization
|
||||
|
||||
## Communication Style
|
||||
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.
|
||||
4. **Inject after** -- If `inject.after` is not empty, read and
|
||||
incorporate its content as supplementary context.
|
||||
|
||||
Patient educator who explains like teaching a friend. Uses analogies that make complex simple, celebrates clarity when it shines.
|
||||
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.
|
||||
|
||||
## Principles
|
||||
### Step 3: Load Config, Greet, and Present Capabilities
|
||||
|
||||
- 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.
|
||||
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.
|
||||
|
||||
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
|
||||
#### Capabilities
|
||||
|
||||
| Code | Description | Skill or Prompt |
|
||||
|------|-------------|-------|
|
||||
|
|
@ -37,21 +53,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.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
# ──────────────────────────────────────────────────────────────────
|
||||
# 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.
|
||||
# 'after' loads after the agent's core instructions.
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
[inject]
|
||||
before = ""
|
||||
after = ""
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
#!/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 os
|
||||
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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys."""
|
||||
return (
|
||||
isinstance(value, list)
|
||||
and len(value) > 0
|
||||
and isinstance(value[0], dict)
|
||||
and "code" in value[0]
|
||||
)
|
||||
|
||||
|
||||
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}
|
||||
for item in override:
|
||||
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)
|
||||
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()
|
||||
|
|
@ -7,15 +7,49 @@ description: Create or update product briefs through guided or autonomous discov
|
|||
|
||||
## Overview
|
||||
|
||||
This skill helps you create compelling product briefs through collaborative discovery, intelligent artifact analysis, and web research. Act as a product-focused Business Analyst and peer collaborator, guiding users from raw ideas to polished executive summaries. Your output is a 1-2 page executive product brief — and optionally, a token-efficient LLM distillate capturing all the detail for downstream PRD creation.
|
||||
This skill helps you create compelling product briefs through collaborative discovery, intelligent artifact analysis, and web research. Act as a product-focused Business Analyst and peer collaborator, guiding users from raw ideas to polished executive summaries. Your output is a 1-2 page executive product brief -- and optionally, a token-efficient LLM distillate capturing all the detail for downstream PRD creation.
|
||||
|
||||
The user is the domain expert. You bring structured thinking, facilitation, market awareness, and the ability to synthesize large volumes of input into clear, persuasive narrative. Work together as equals.
|
||||
|
||||
**Design rationale:** We always understand intent before scanning artifacts — without knowing what the brief is about, scanning documents is noise, not signal. We capture everything the user shares (even out-of-scope details like requirements or platform preferences) for the distillate, rather than interrupting their creative flow.
|
||||
**Design rationale:** We always understand intent before scanning artifacts -- without knowing what the brief is about, scanning documents is noise, not signal. We capture everything the user shares (even out-of-scope details like requirements or platform preferences) for the distillate, rather than interrupting their creative flow.
|
||||
|
||||
## Activation Mode Detection
|
||||
## On Activation
|
||||
|
||||
Check activation context immediately:
|
||||
### Step 1: Resolve Activation Customization
|
||||
|
||||
Resolve `inject`, `additional_resources`, and `config.defaultMode` from customization:
|
||||
Run: `python ./scripts/resolve-customization.py bmad-product-brief --key inject --key additional_resources --key config.defaultMode`
|
||||
Use the JSON output as resolved values.
|
||||
|
||||
If script unavailable, read these fields from the following files
|
||||
(first found wins, most specific first):
|
||||
1. `{project-root}/_bmad/customizations/bmad-product-brief.user.toml` (if exists)
|
||||
2. `{project-root}/_bmad/customizations/bmad-product-brief.toml` (if exists)
|
||||
3. `./customize.toml` (last resort defaults)
|
||||
|
||||
### Step 2: Apply Activation Customization
|
||||
|
||||
1. **Inject before** -- If `inject.before` is not empty, read and incorporate
|
||||
its content as high-priority context before proceeding.
|
||||
2. **Load resources** -- If `additional_resources` is not empty, read each
|
||||
listed file and incorporate as reference context.
|
||||
|
||||
Note: `inject.after` is resolved and applied later, in the finalize stage.
|
||||
Note: `config.*` values are resolved JIT by each stage that needs them.
|
||||
|
||||
### Step 3: Load Module Config
|
||||
|
||||
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. Greet user as `{user_name}`, speaking in `{communication_language}`.
|
||||
|
||||
### Step 4: Detect Activation Mode
|
||||
|
||||
Use resolved `config.defaultMode` as the fallback if the user doesn't specify:
|
||||
|
||||
1. **Autonomous mode**: If the user passes `--autonomous`/`-A` flags, or provides structured inputs clearly intended for headless execution:
|
||||
- Ingest all provided inputs, fan out subagents, produce complete brief without interaction
|
||||
|
|
@ -25,45 +59,32 @@ Check activation context immediately:
|
|||
- Ingest everything, draft complete brief upfront, then walk user through refinement
|
||||
- Route to Stage 1 below with `{mode}=yolo`
|
||||
|
||||
3. **Guided mode** (default): Conversational discovery with soft gates
|
||||
3. **Guided mode** (default, or as specified by `config.defaultMode`): Conversational discovery with soft gates
|
||||
- Route to Stage 1 below with `{mode}=guided`
|
||||
|
||||
## 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. **Greet user** as `{user_name}`, speaking in `{communication_language}`.
|
||||
|
||||
3. **Stage 1: Understand Intent** (handled here in SKILL.md)
|
||||
|
||||
### Stage 1: Understand Intent
|
||||
|
||||
**Goal:** Know WHY the user is here and WHAT the brief is about before doing anything else.
|
||||
|
||||
**Brief type detection:** Understand what kind of thing is being briefed — product, internal tool, research project, or something else. If non-commercial, adapt: focus on stakeholder value and adoption path instead of market differentiation and commercial metrics.
|
||||
**Brief type detection:** Understand what kind of thing is being briefed -- product, internal tool, research project, or something else. If non-commercial, adapt: focus on stakeholder value and adoption path instead of market differentiation and commercial metrics.
|
||||
|
||||
**Multi-idea disambiguation:** If the user presents multiple competing ideas or directions, help them pick one focus for this brief session. Note that others can be briefed separately.
|
||||
|
||||
**If the user provides an existing brief** (path to a product brief file, or says "update" / "revise" / "edit"):
|
||||
- Read the existing brief fully
|
||||
- Treat it as rich input — you already know the product, the vision, the scope
|
||||
- Treat it as rich input -- you already know the product, the vision, the scope
|
||||
- Ask: "What's changed? What do you want to update or improve?"
|
||||
- The rest of the workflow proceeds normally — contextual discovery may pull in new research, elicitation focuses on gaps or changes, and draft-and-review produces an updated version
|
||||
- The rest of the workflow proceeds normally -- contextual discovery may pull in new research, elicitation focuses on gaps or changes, and draft-and-review produces an updated version
|
||||
|
||||
**If the user already provided context** when launching the skill (description, docs, brain dump):
|
||||
- Acknowledge what you received — but **DO NOT read document files yet**. Note their paths for Stage 2's subagents to scan contextually. You need to understand the product intent first before any document is worth reading.
|
||||
- Acknowledge what you received -- but **DO NOT read document files yet**. Note their paths for Stage 2's subagents to scan contextually. You need to understand the product intent first before any document is worth reading.
|
||||
- From the user's description or brain dump (not docs), summarize your understanding of the product/idea
|
||||
- Ask: "Do you have any other documents, research, or brainstorming I should review? Anything else to add before I dig in?"
|
||||
|
||||
**If the user provided nothing beyond invoking the skill:**
|
||||
- Ask what their product or project idea is about
|
||||
- Ask if they have any existing documents, research, brainstorming reports, or other materials
|
||||
- Let them brain dump — capture everything
|
||||
- Let them brain dump -- capture everything
|
||||
|
||||
**The "anything else?" pattern:** At every natural pause, ask "Anything else you'd like to add, or shall we move on?" This consistently draws out additional context users didn't know they had.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
# ──────────────────────────────────────────────────────────────────
|
||||
# Customization Defaults: bmad-product-brief
|
||||
# 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-product-brief.toml (team/org, committed to git)
|
||||
# _bmad/customizations/bmad-product-brief.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 workflow context.
|
||||
# Paths are relative to {project-root}.
|
||||
additional_resources = []
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Workflow configuration
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
[config]
|
||||
# Output filename pattern. Available tokens: {planning_artifacts}, {project_name}
|
||||
outputFile = "{planning_artifacts}/product-brief-{project_name}.md"
|
||||
distillateFile = "{planning_artifacts}/product-brief-{project_name}-distillate.md"
|
||||
|
||||
# Default activation mode when not specified by user: guided | yolo | autonomous
|
||||
defaultMode = "guided"
|
||||
|
||||
# Always generate the distillate alongside the brief (true/false)
|
||||
alwaysGenerateDistillate = false
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Brief template defaults - controls which sections appear and their
|
||||
# relative emphasis. Set weight to 0 to omit a section entirely.
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
[config.sections]
|
||||
executiveSummary = { enabled = true, weight = "high" }
|
||||
problem = { enabled = true, weight = "high" }
|
||||
solution = { enabled = true, weight = "high" }
|
||||
differentiators = { enabled = true, weight = "medium" }
|
||||
users = { enabled = true, weight = "medium" }
|
||||
successCriteria = { enabled = true, weight = "medium" }
|
||||
scope = { enabled = true, weight = "medium" }
|
||||
vision = { enabled = true, weight = "low" }
|
||||
|
||||
# Custom sections added to the template (appended after standard sections)
|
||||
# [[config.customSections]]
|
||||
# name = "Section Name"
|
||||
# description = "What this section should cover"
|
||||
# weight = "medium"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Injected prompts - content woven into the workflow's context.
|
||||
# 'before' loads before Stage 1 begins.
|
||||
# 'after' loads after all stages complete (pre-finalize).
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
[inject]
|
||||
before = ""
|
||||
after = ""
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# Review configuration - controls the Stage 4 review fan-out.
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
[review]
|
||||
# Enable/disable the built-in review lenses
|
||||
skepticReview = true
|
||||
opportunityReview = true
|
||||
contextualReview = true
|
||||
|
||||
# Force a specific contextual review lens instead of auto-detecting.
|
||||
# Example: "Regulatory and compliance risk reviewer"
|
||||
contextualReviewLens = ""
|
||||
|
|
@ -6,9 +6,18 @@
|
|||
|
||||
**Goal:** Produce the executive product brief and run it through multiple review lenses to catch blind spots before the user sees the final version.
|
||||
|
||||
## Resolve Stage Customization
|
||||
|
||||
Resolve `config.outputFile`, `config.sections`, `config.customSections`, and `review`
|
||||
from customization:
|
||||
Run: `python ./scripts/resolve-customization.py bmad-product-brief --key config.outputFile --key config.sections --key config.customSections --key review`
|
||||
|
||||
If script unavailable, read these fields from the customization files
|
||||
(most specific first: user > team > skill defaults).
|
||||
|
||||
## Step 1: Draft the Executive Brief
|
||||
|
||||
Use `../resources/brief-template.md` as a guide — adapt structure to fit the product's story.
|
||||
Use `../resources/brief-template.md` as a guide -- adapt structure to fit the product's story.
|
||||
|
||||
**Writing principles:**
|
||||
- **Executive audience** — persuasive, clear, concise. 1-2 pages.
|
||||
|
|
@ -17,7 +26,9 @@ Use `../resources/brief-template.md` as a guide — adapt structure to fit the p
|
|||
- **Confident voice** — this is a pitch, not a hedge
|
||||
- Write in `{document_output_language}`
|
||||
|
||||
**Create the output document at:** `{planning_artifacts}/product-brief-{project_name}.md`
|
||||
**Create the output document at** the resolved `config.outputFile` path (default: `{planning_artifacts}/product-brief-{project_name}.md`).
|
||||
|
||||
Use resolved `config.sections` to control which sections appear and their emphasis. If any `config.customSections` were resolved, append them after the standard sections.
|
||||
|
||||
Include YAML frontmatter:
|
||||
```yaml
|
||||
|
|
@ -32,15 +43,15 @@ inputs: [list of input files used]
|
|||
|
||||
## Step 2: Fan Out Review Subagents
|
||||
|
||||
Before showing the draft to the user, run it through multiple review lenses in parallel.
|
||||
Before showing the draft to the user, run it through multiple review lenses in parallel. Use the resolved `review` config to determine which lenses to run.
|
||||
|
||||
**Launch in parallel:**
|
||||
**Launch in parallel** (skip any where the resolved config disables them):
|
||||
|
||||
1. **Skeptic Reviewer** (`../agents/skeptic-reviewer.md`) — "What's missing? What assumptions are untested? What could go wrong? Where is the brief vague or hand-wavy?"
|
||||
1. **Skeptic Reviewer** (`../agents/skeptic-reviewer.md`) -- if `review.skepticReview` is true: "What's missing? What assumptions are untested? What could go wrong? Where is the brief vague or hand-wavy?"
|
||||
|
||||
2. **Opportunity Reviewer** (`../agents/opportunity-reviewer.md`) — "What adjacent value propositions are being missed? What market angles or partnerships could strengthen this? What's underemphasized?"
|
||||
2. **Opportunity Reviewer** (`../agents/opportunity-reviewer.md`) -- if `review.opportunityReview` is true: "What adjacent value propositions are being missed? What market angles or partnerships could strengthen this? What's underemphasized?"
|
||||
|
||||
3. **Contextual Reviewer** — You (the main agent) pick the most useful third lens based on THIS specific product. Choose the lens that addresses the SINGLE BIGGEST RISK that the skeptic and opportunity reviewers won't naturally catch. Examples:
|
||||
3. **Contextual Reviewer** -- if `review.contextualReview` is true: If `review.contextualReviewLens` is not empty, use that specific lens. Otherwise, you (the main agent) pick the most useful third lens based on THIS specific product. Choose the lens that addresses the SINGLE BIGGEST RISK that the skeptic and opportunity reviewers won't naturally catch. Examples:
|
||||
- For healthtech: "Regulatory and compliance risk reviewer"
|
||||
- For devtools: "Developer experience and adoption friction critic"
|
||||
- For marketplace: "Network effects and chicken-and-egg problem analyst"
|
||||
|
|
|
|||
|
|
@ -6,9 +6,22 @@
|
|||
|
||||
**Goal:** Save the polished brief, offer the LLM distillate, and point the user forward.
|
||||
|
||||
## Resolve Stage Customization
|
||||
|
||||
Resolve `config.outputFile`, `config.distillateFile`, `config.alwaysGenerateDistillate`,
|
||||
and `inject.after` from customization:
|
||||
Run: `python ./scripts/resolve-customization.py bmad-product-brief --key config.outputFile --key config.distillateFile --key config.alwaysGenerateDistillate --key inject.after`
|
||||
|
||||
If script unavailable, read these fields from the customization files
|
||||
(most specific first: user > team > skill defaults).
|
||||
|
||||
## Step 0: Apply Post-Workflow Injection
|
||||
|
||||
If resolved `inject.after` is not empty, read and incorporate its content now. This is the team/user's final checklist or validation gate before the brief is considered complete.
|
||||
|
||||
## Step 1: Polish and Save
|
||||
|
||||
Update the product brief document at `{planning_artifacts}/product-brief-{project_name}.md`:
|
||||
Update the product brief document at the resolved `config.outputFile` path:
|
||||
- Update frontmatter `status` to `"complete"`
|
||||
- Update `updated` timestamp
|
||||
- Ensure formatting is clean and consistent
|
||||
|
|
@ -18,10 +31,12 @@ Update the product brief document at `{planning_artifacts}/product-brief-{projec
|
|||
|
||||
Throughout the discovery process, you likely captured detail that doesn't belong in a 1-2 page executive summary but is valuable for downstream work — requirements hints, platform preferences, rejected ideas, technical constraints, detailed user scenarios, competitive deep-dives, etc.
|
||||
|
||||
**Ask the user:**
|
||||
"Your product brief is complete. During our conversation, I captured additional detail that goes beyond the executive summary — things like [mention 2-3 specific examples of overflow you captured]. Would you like me to create a detail pack for PRD creation? It distills all that extra context into a concise, structured format optimized for the next phase."
|
||||
**If `config.alwaysGenerateDistillate` is true**, skip the offer prompt and create the distillate automatically.
|
||||
|
||||
**If yes, create the distillate** at `{planning_artifacts}/product-brief-{project_name}-distillate.md`:
|
||||
**Otherwise, ask the user:**
|
||||
"Your product brief is complete. During our conversation, I captured additional detail that goes beyond the executive summary -- things like [mention 2-3 specific examples of overflow you captured]. Would you like me to create a detail pack for PRD creation? It distills all that extra context into a concise, structured format optimized for the next phase."
|
||||
|
||||
**If yes (or always-generate is true), create the distillate** at the resolved `config.distillateFile` path:
|
||||
|
||||
```yaml
|
||||
---
|
||||
|
|
|
|||
|
|
@ -0,0 +1,182 @@
|
|||
#!/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 os
|
||||
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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys."""
|
||||
return (
|
||||
isinstance(value, list)
|
||||
and len(value) > 0
|
||||
and isinstance(value[0], dict)
|
||||
and "code" in value[0]
|
||||
)
|
||||
|
||||
|
||||
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}
|
||||
for item in override:
|
||||
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)
|
||||
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()
|
||||
|
|
@ -3,32 +3,47 @@ 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
|
||||
### Step 1: Resolve Activation Customization
|
||||
|
||||
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.
|
||||
Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization:
|
||||
Run: `python ./scripts/resolve-customization.py bmad-agent-pm --key persona --key inject --key additional_resources --key menu`
|
||||
Use the JSON output as resolved values.
|
||||
|
||||
## Identity
|
||||
If script unavailable, read these sections from the following files
|
||||
(first found wins, most specific first):
|
||||
1. `{project-root}/_bmad/customizations/bmad-agent-pm.user.toml` (if exists)
|
||||
2. `{project-root}/_bmad/customizations/bmad-agent-pm.toml` (if exists)
|
||||
3. `./customize.toml` (last resort defaults)
|
||||
|
||||
Product management veteran with 8+ years launching B2B and consumer products. Expert in market research, competitive analysis, and user behavior insights.
|
||||
### Step 2: Apply Customization
|
||||
|
||||
## Communication Style
|
||||
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.
|
||||
4. **Inject after** -- If `inject.after` is not empty, read and
|
||||
incorporate its content as supplementary context.
|
||||
|
||||
Asks "WHY?" relentlessly like a detective on a case. Direct and data-sharp, cuts through fluff to what actually matters.
|
||||
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.
|
||||
|
||||
## Principles
|
||||
### Step 3: Load Config, Greet, and Present Capabilities
|
||||
|
||||
- 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.
|
||||
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.
|
||||
|
||||
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
|
||||
#### Capabilities
|
||||
|
||||
| Code | Description | Skill |
|
||||
|------|-------------|-------|
|
||||
|
|
@ -39,21 +54,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.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
# ──────────────────────────────────────────────────────────────────
|
||||
# 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.
|
||||
# 'after' loads after the agent's core instructions.
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
[inject]
|
||||
before = ""
|
||||
after = ""
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
#!/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 os
|
||||
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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys."""
|
||||
return (
|
||||
isinstance(value, list)
|
||||
and len(value) > 0
|
||||
and isinstance(value[0], dict)
|
||||
and "code" in value[0]
|
||||
)
|
||||
|
||||
|
||||
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}
|
||||
for item in override:
|
||||
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)
|
||||
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()
|
||||
|
|
@ -3,53 +3,52 @@ 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
|
||||
|
||||
### Step 1: Resolve Activation Customization
|
||||
|
||||
Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization:
|
||||
Run: `python ./scripts/resolve-customization.py bmad-agent-ux-designer --key persona --key inject --key additional_resources --key menu`
|
||||
Use the JSON output as resolved values.
|
||||
|
||||
If script unavailable, read these sections from the following files
|
||||
(first found wins, most specific first):
|
||||
1. `{project-root}/_bmad/customizations/bmad-agent-ux-designer.user.toml` (if exists)
|
||||
2. `{project-root}/_bmad/customizations/bmad-agent-ux-designer.toml` (if exists)
|
||||
3. `./customize.toml` (last resort defaults)
|
||||
|
||||
### 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.
|
||||
4. **Inject after** -- If `inject.after` is not empty, read and
|
||||
incorporate its content as supplementary 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.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
# ──────────────────────────────────────────────────────────────────
|
||||
# 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.
|
||||
# 'after' loads after the agent's core instructions.
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
[inject]
|
||||
before = ""
|
||||
after = ""
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
#!/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 os
|
||||
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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys."""
|
||||
return (
|
||||
isinstance(value, list)
|
||||
and len(value) > 0
|
||||
and isinstance(value[0], dict)
|
||||
and "code" in value[0]
|
||||
)
|
||||
|
||||
|
||||
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}
|
||||
for item in override:
|
||||
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)
|
||||
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()
|
||||
|
|
@ -3,52 +3,53 @@ 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
|
||||
|
||||
### Step 1: Resolve Activation Customization
|
||||
|
||||
Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization:
|
||||
Run: `python ./scripts/resolve-customization.py bmad-agent-architect --key persona --key inject --key additional_resources --key menu`
|
||||
Use the JSON output as resolved values.
|
||||
|
||||
If script unavailable, read these sections from the following files
|
||||
(first found wins, most specific first):
|
||||
1. `{project-root}/_bmad/customizations/bmad-agent-architect.user.toml` (if exists)
|
||||
2. `{project-root}/_bmad/customizations/bmad-agent-architect.toml` (if exists)
|
||||
3. `./customize.toml` (last resort defaults)
|
||||
|
||||
### 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.
|
||||
4. **Inject after** -- If `inject.after` is not empty, read and
|
||||
incorporate its content as supplementary 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.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
# ──────────────────────────────────────────────────────────────────
|
||||
# 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.
|
||||
# 'after' loads after the agent's core instructions.
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
[inject]
|
||||
before = ""
|
||||
after = ""
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
#!/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 os
|
||||
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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys."""
|
||||
return (
|
||||
isinstance(value, list)
|
||||
and len(value) > 0
|
||||
and isinstance(value[0], dict)
|
||||
and "code" in value[0]
|
||||
)
|
||||
|
||||
|
||||
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}
|
||||
for item in override:
|
||||
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)
|
||||
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()
|
||||
|
|
@ -3,41 +3,58 @@ 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
|
||||
### Step 1: Resolve Activation Customization
|
||||
|
||||
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.
|
||||
Resolve `persona`, `inject`, `additional_resources`, and `menu` from customization:
|
||||
Run: `python ./scripts/resolve-customization.py bmad-agent-dev --key persona --key inject --key additional_resources --key menu`
|
||||
Use the JSON output as resolved values.
|
||||
|
||||
## Identity
|
||||
If script unavailable, read these sections from the following files
|
||||
(first found wins, most specific first):
|
||||
1. `{project-root}/_bmad/customizations/bmad-agent-dev.user.toml` (if exists)
|
||||
2. `{project-root}/_bmad/customizations/bmad-agent-dev.toml` (if exists)
|
||||
3. `./customize.toml` (last resort defaults)
|
||||
|
||||
Senior software engineer who executes approved stories with strict adherence to story details and team standards and practices.
|
||||
### Step 2: Apply Customization
|
||||
|
||||
## Communication Style
|
||||
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.
|
||||
4. **Inject after** -- If `inject.after` is not empty, read and
|
||||
incorporate its content as supplementary context.
|
||||
|
||||
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/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 +66,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.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
# ──────────────────────────────────────────────────────────────────
|
||||
# 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.
|
||||
# 'after' loads after the agent's core instructions.
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
[inject]
|
||||
before = ""
|
||||
after = ""
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
#!/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 os
|
||||
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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys."""
|
||||
return (
|
||||
isinstance(value, list)
|
||||
and len(value) > 0
|
||||
and isinstance(value[0], dict)
|
||||
and "code" in value[0]
|
||||
)
|
||||
|
||||
|
||||
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}
|
||||
for item in override:
|
||||
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)
|
||||
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()
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
#!/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 os
|
||||
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 Exception 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* looks like a ``[[menu]]`` array of tables with ``code`` keys."""
|
||||
return (
|
||||
isinstance(value, list)
|
||||
and len(value) > 0
|
||||
and isinstance(value[0], dict)
|
||||
and "code" in value[0]
|
||||
)
|
||||
|
||||
|
||||
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}
|
||||
for item in override:
|
||||
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)
|
||||
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()
|
||||
|
|
@ -454,6 +454,11 @@ class OfficialModules {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Skip scripts directory - contains build/customization scripts not needed at install time
|
||||
if (file.startsWith('scripts/') || file === 'scripts') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip sidecar directories - these contain agent-specific assets not needed at install time
|
||||
const isInSidecarDirectory = path
|
||||
.dirname(file)
|
||||
|
|
|
|||
Loading…
Reference in New Issue