This commit is contained in:
Jérôme Revillard 2026-05-13 06:50:15 +00:00 committed by GitHub
commit 2d8262bf75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 402 additions and 45 deletions

View File

@ -46,7 +46,7 @@ Treat every entry in `{agent.persistent_facts}` as foundational context you carr
### Step 5: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents

View File

@ -46,7 +46,7 @@ Treat every entry in `{agent.persistent_facts}` as foundational context you carr
### Step 5: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents

View File

@ -40,7 +40,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents

View File

@ -11,7 +11,7 @@
### Configuration Loading
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- `project_knowledge`
- `user_name`

View File

@ -10,7 +10,7 @@
### Configuration Loading
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- `project_knowledge`
- `user_name`

View File

@ -50,7 +50,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents

View File

@ -21,7 +21,7 @@ Briefs produced here are honest, right-sized to purpose, and built for what come
1. Resolve customization: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow`. On failure, surface the diagnostic and halt.
2. Execute each entry in `{workflow.activation_steps_prepend}` in order.
3. Treat every entry in `{workflow.persistent_facts}` as foundational context for the rest of the run. Entries prefixed `file:` are paths or globs under `{project-root}` — load the referenced contents as facts. All other entries are facts verbatim.
4. Load `{project-root}/_bmad/bmm/config.yaml` (and `config.user.yaml` if present). Resolve `{user_name}`, `{communication_language}`, `{document_output_language}`, `{planning_artifacts}`, `{project_name}`, `{date}`.
4. Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve `{user_name}`, `{communication_language}`, `{document_output_language}`, `{planning_artifacts}`, `{project_name}`, `{date}`.
5. Greet `{user_name}` in `{communication_language}`. Detect intent (create / update / validate). If interactive and intent is unclear, ask; for headless behavior see `## Headless Mode`.
6. Execute each entry in `{workflow.activation_steps_append}` in order.
@ -59,6 +59,13 @@ When invoked headless, do not ask. Complete the intent using what is provided, w
Omit keys for artifacts that were not produced.
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. 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
## Discovery
Conversationally surface what the user brings, why this brief exists, and the domain — echo back how each shapes your approach. Open with space for the full picture: invite a brain dump and ask up front for any source material they already have (memo, deck, transcript, prior brief, slack thread). Read what exists first; ask only what is missing. After the dump, a simple "anything else?" often surfaces what they almost forgot. Drill into specifics only after the broad shape is on the table; premature granular questions interrupt the dump and miss the room. Get a read on stakes early (passion project, internal pitch, investor input, public launch), and let that calibrate how hard you push. Suggest research (web, competitive, market) only when the stakes warrant it.

View File

@ -44,7 +44,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents

View File

@ -44,7 +44,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents

View File

@ -44,7 +44,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents

View File

@ -46,7 +46,7 @@ Treat every entry in `{agent.persistent_facts}` as foundational context you carr
### Step 5: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents

View File

@ -46,7 +46,7 @@ Treat every entry in `{agent.persistent_facts}` as foundational context you carr
### Step 5: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents

View File

@ -73,7 +73,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents

View File

@ -47,7 +47,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents

View File

@ -73,7 +73,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents

View File

@ -73,7 +73,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents

View File

@ -46,7 +46,7 @@ Treat every entry in `{agent.persistent_facts}` as foundational context you carr
### Step 5: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents

View File

@ -69,7 +69,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents

View File

@ -50,7 +50,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents

View File

@ -71,7 +71,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents

View File

@ -50,7 +50,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents

View File

@ -46,7 +46,7 @@ Treat every entry in `{agent.persistent_facts}` as foundational context you carr
### Step 5: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents

View File

@ -40,7 +40,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- `implementation_artifacts`
- `planning_artifacts`

View File

@ -40,7 +40,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- `project_name`, `planning_artifacts`, `implementation_artifacts`, `user_name`
- `communication_language`, `document_output_language`, `user_skill_level`

View File

@ -40,7 +40,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- `project_name`, `user_name`
- `communication_language`, `document_output_language`

View File

@ -47,7 +47,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- `project_name`, `user_name`
- `communication_language`, `document_output_language`

View File

@ -47,7 +47,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- `project_name`, `user_name`
- `communication_language`, `document_output_language`

View File

@ -40,7 +40,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- `project_name`, `user_name`
- `communication_language`, `document_output_language`

View File

@ -59,7 +59,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- `project_name`, `planning_artifacts`, `implementation_artifacts`, `user_name`
- `communication_language`, `document_output_language`, `user_skill_level`

View File

@ -56,7 +56,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- `project_name`, `user_name`
- `communication_language`, `document_output_language`

View File

@ -40,7 +40,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- `project_name`, `user_name`
- `communication_language`, `document_output_language`

View File

@ -40,7 +40,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- `project_name`, `user_name`
- `communication_language`, `document_output_language`

View File

@ -32,7 +32,7 @@ This uses **micro-file architecture** for disciplined execution:
### Configuration Loading
Load config from `{project-root}/_bmad/core/config.yaml` and resolve:
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- `project_name`, `output_folder`, `user_name`
- `communication_language`, `document_output_language`, `user_skill_level`

View File

@ -23,7 +23,7 @@ When this skill completes, the user should:
## Data Sources
- **Catalog**: `{project-root}/_bmad/_config/bmad-help.csv` — assembled manifest of all installed module skills
- **Config**: `config.yaml` and `user-config.yaml` files in `{project-root}/_bmad/` and its subfolders — resolve `output-location` variables, provide `communication_language` and `project_knowledge`
- **Config**: Run `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` to get merged config. If that fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself — resolve `output-location` variables, provide `communication_language` and `project_knowledge`
- **Artifacts**: Files matching `outputs` patterns at resolved `output-location` paths reveal which steps are possibly completed; their content may also provide grounding context for recommendations
- **Project knowledge**: If `project_knowledge` resolves to an existing path, read it for grounding context. Never fabricate project-specific details.
- **Module docs**: Rows with `_meta` in the `skill` column carry a URL or path in `output-location` pointing to the module's documentation (e.g., llms.txt). Fetch and use these to answer general questions about that module.

View File

@ -22,7 +22,7 @@ Party mode accepts optional arguments when invoked:
1. **Parse arguments** — check for `--model` and `--solo` flags from the user's invocation.
2. Load config from `{project-root}/_bmad/core/config.yaml` and resolve:
2. Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
- Use `{user_name}` for greeting
- Use `{communication_language}` for all communications

View File

@ -1,12 +1,13 @@
#!/usr/bin/env python3
"""
Resolve BMad's central config using four-layer TOML merge.
Resolve BMad's central config using five-layer TOML merge.
Reads from four layers (highest priority last):
Reads from five layers (highest priority last):
1. {project-root}/_bmad/config.toml (installer-owned team)
2. {project-root}/_bmad/config.user.toml (installer-owned user)
3. {project-root}/_bmad/custom/config.toml (human-authored team, committed)
4. {project-root}/_bmad/custom/config.user.toml (human-authored user, gitignored)
3. ~/.bmad/config/config.user.toml (global user preferences)
4. {project-root}/_bmad/custom/config.toml (human-authored team, committed)
5. {project-root}/_bmad/custom/config.user.toml (human-authored user, gitignored)
Outputs merged JSON to stdout. Errors go to stderr.
@ -40,6 +41,7 @@ except ImportError:
_MISSING = object()
_KEYED_MERGE_FIELDS = ("code", "id")
GLOBAL_DIR = Path.home() / ".bmad" / "config"
def load_toml(file_path: Path, required: bool = False) -> dict:
@ -136,7 +138,7 @@ def extract_key(data, dotted_key: str):
def main():
parser = argparse.ArgumentParser(
description="Resolve BMad central config using four-layer TOML merge.",
description="Resolve BMad central config using five-layer TOML merge.",
)
parser.add_argument(
"--project-root", "-p", required=True,
@ -153,10 +155,12 @@ def main():
base_team = load_toml(bmad_dir / "config.toml", required=True)
base_user = load_toml(bmad_dir / "config.user.toml")
global_user = load_toml(GLOBAL_DIR / "config.user.toml")
custom_team = load_toml(bmad_dir / "custom" / "config.toml")
custom_user = load_toml(bmad_dir / "custom" / "config.user.toml")
merged = deep_merge(base_team, base_user)
merged = deep_merge(merged, global_user)
merged = deep_merge(merged, custom_team)
merged = deep_merge(merged, custom_user)

View File

@ -1,11 +1,12 @@
#!/usr/bin/env python3
"""
Resolve customization for a BMad skill using three-layer TOML merge.
Resolve customization for a BMad skill using four-layer TOML merge.
Reads customization from three layers (highest priority first):
Reads customization from four layers (highest priority first):
1. {project-root}/_bmad/custom/{name}.user.toml (personal, gitignored)
2. {project-root}/_bmad/custom/{name}.toml (team/org, committed)
3. {skill-root}/customize.toml (skill defaults)
3. ~/.bmad/config/{name}.user.toml (global user preferences)
4. {skill-root}/customize.toml (skill defaults)
Skill name is derived from the basename of the skill directory.
@ -51,6 +52,7 @@ except ImportError:
_MISSING = object()
_KEYED_MERGE_FIELDS = ("code", "id")
GLOBAL_DIR = Path.home() / ".bmad" / "config"
def find_project_root(start: Path):
@ -179,7 +181,7 @@ def extract_key(data, dotted_key: str):
def main():
parser = argparse.ArgumentParser(
description="Resolve customization for a BMad skill using three-layer TOML merge.",
description="Resolve customization for a BMad skill using four-layer TOML merge.",
add_help=True,
)
parser.add_argument(
@ -211,7 +213,10 @@ def main():
team = load_toml(custom_dir / f"{skill_name}.toml")
user = load_toml(custom_dir / f"{skill_name}.user.toml")
merged = deep_merge(defaults, team)
global_user = load_toml(GLOBAL_DIR / f"{skill_name}.user.toml")
merged = deep_merge(defaults, global_user)
merged = deep_merge(merged, team)
merged = deep_merge(merged, user)
if args.key:

View File

@ -0,0 +1,341 @@
/**
* Config Resolution Tests
*
* Tests the Python config resolution scripts by invoking them as subprocesses
* with temporary TOML fixtures. Validates:
* - Global user layer is loaded and merged with correct priority
* - Project layers override global layers
* - Missing global dir doesn't break anything (backward compat)
* - resolve_customization.py global skill layer
*
* Usage: node test/test-config-resolution.js
*/
const path = require('node:path');
const os = require('node:os');
const fs = require('node:fs/promises');
const { execSync } = require('node:child_process');
const SCRIPTS_DIR = path.resolve(__dirname, '..', 'src', 'scripts');
const colors = {
reset: '',
green: '',
red: '',
dim: '',
};
let passed = 0;
let failed = 0;
function assert(condition, testName, errorMessage = '') {
if (condition) {
console.log(`${colors.green}${colors.reset} ${testName}`);
passed++;
} else {
console.log(`${colors.red}${colors.reset} ${testName}`);
if (errorMessage) {
console.log(` ${colors.dim}${errorMessage}${colors.reset}`);
}
failed++;
}
}
function writeToml(filePath, data) {
const lines = [];
for (const [key, val] of Object.entries(data)) {
if (typeof val === 'string') {
lines.push(`${key} = "${val}"`);
} else if (typeof val === 'number' || typeof val === 'boolean') {
lines.push(`${key} = ${val}`);
}
}
return fs.writeFile(filePath, lines.join('\n') + '\n');
}
async function withTempDir(fn) {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-config-test-'));
try {
return await fn(dir);
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
}
async function testResolveConfig() {
console.log('\n--- resolve_config.py ---\n');
// Test 1: global user overrides installer defaults
await withTempDir(async (tmpDir) => {
const globalDir = path.join(tmpDir, '.bmad', 'config');
await fs.mkdir(globalDir, { recursive: true });
await writeToml(path.join(globalDir, 'config.user.toml'), {
user_name: 'GlobalAlice',
communication_language: 'en',
});
const projectDir = path.join(tmpDir, 'project');
const bmadDir = path.join(projectDir, '_bmad');
await fs.mkdir(path.join(bmadDir, 'custom'), { recursive: true });
await writeToml(path.join(bmadDir, 'config.toml'), {
project_name: 'TestProject',
user_name: 'InstallerDefault',
});
await writeToml(path.join(bmadDir, 'config.user.toml'), {
user_name: 'InstallerUserDefault',
});
const origHome = process.env.HOME;
process.env.HOME = tmpDir;
try {
const result = JSON.parse(execSync(`python3 ${SCRIPTS_DIR}/resolve_config.py --project-root ${projectDir}`, { encoding: 'utf-8' }));
assert(
result.user_name === 'GlobalAlice',
'global user overrides installer defaults',
`Expected "GlobalAlice", got "${result.user_name}"`,
);
assert(
result.communication_language === 'en',
'global communication_language preserved when installer has no override',
`Expected "en", got "${result.communication_language}"`,
);
assert(
result.project_name === 'TestProject',
'installer team config values preserved',
`Expected "TestProject", got "${result.project_name}"`,
);
} finally {
process.env.HOME = origHome;
}
});
// Test 2: missing global dir — backward compat
await withTempDir(async (tmpDir) => {
const projectDir = path.join(tmpDir, 'project');
const bmadDir = path.join(projectDir, '_bmad');
await fs.mkdir(path.join(bmadDir, 'custom'), { recursive: true });
await writeToml(path.join(bmadDir, 'config.toml'), {
project_name: 'NoGlobalProject',
});
const origHome = process.env.HOME;
process.env.HOME = tmpDir;
try {
const result = JSON.parse(execSync(`python3 ${SCRIPTS_DIR}/resolve_config.py --project-root ${projectDir}`, { encoding: 'utf-8' }));
assert(
result.project_name === 'NoGlobalProject',
'works fine without global config dir',
`Expected "NoGlobalProject", got "${result.project_name}"`,
);
} finally {
process.env.HOME = origHome;
}
});
// Test 3: full priority chain — base_team < base_user < global < custom_team < custom_user
await withTempDir(async (tmpDir) => {
const globalDir = path.join(tmpDir, '.bmad', 'config');
await fs.mkdir(globalDir, { recursive: true });
await writeToml(path.join(globalDir, 'config.user.toml'), {
user_name: 'L2-Global',
});
const projectDir = path.join(tmpDir, 'project');
const bmadDir = path.join(projectDir, '_bmad');
await fs.mkdir(path.join(bmadDir, 'custom'), { recursive: true });
await writeToml(path.join(bmadDir, 'config.toml'), { user_name: 'L0-BaseTeam' });
await writeToml(path.join(bmadDir, 'config.user.toml'), { user_name: 'L1-BaseUser' });
await writeToml(path.join(bmadDir, 'custom', 'config.toml'), { user_name: 'L3-CustomTeam' });
await writeToml(path.join(bmadDir, 'custom', 'config.user.toml'), { user_name: 'L4-CustomUser' });
const origHome = process.env.HOME;
process.env.HOME = tmpDir;
try {
const result = JSON.parse(execSync(`python3 ${SCRIPTS_DIR}/resolve_config.py --project-root ${projectDir}`, { encoding: 'utf-8' }));
assert(
result.user_name === 'L4-CustomUser',
'highest priority layer (custom user) wins',
`Expected "L4-CustomUser", got "${result.user_name}"`,
);
} finally {
process.env.HOME = origHome;
}
});
// Test 4: --key flag works with global layer
await withTempDir(async (tmpDir) => {
const globalDir = path.join(tmpDir, '.bmad', 'config');
await fs.mkdir(globalDir, { recursive: true });
await writeToml(path.join(globalDir, 'config.user.toml'), {
user_name: 'KeyTestGlobal',
communication_language: 'fr',
});
const projectDir = path.join(tmpDir, 'project');
const bmadDir = path.join(projectDir, '_bmad');
await fs.mkdir(path.join(bmadDir, 'custom'), { recursive: true });
await writeToml(path.join(bmadDir, 'config.toml'), {
project_name: 'KeyTestProject',
});
const origHome = process.env.HOME;
process.env.HOME = tmpDir;
try {
const result = JSON.parse(
execSync(`python3 ${SCRIPTS_DIR}/resolve_config.py --project-root ${projectDir} --key user_name --key communication_language`, {
encoding: 'utf-8',
}),
);
assert(
Object.keys(result).length === 2,
'--key returns only requested keys',
`Expected 2 keys, got ${Object.keys(result).length}: ${JSON.stringify(Object.keys(result))}`,
);
assert(
result.user_name === 'KeyTestGlobal',
'--key user_name returns global value',
`Expected "KeyTestGlobal", got "${result.user_name}"`,
);
assert(
result.communication_language === 'fr',
'--key communication_language returns global value',
`Expected "fr", got "${result.communication_language}"`,
);
} finally {
process.env.HOME = origHome;
}
});
}
async function testResolveCustomization() {
console.log('\n--- resolve_customization.py ---\n');
// Test 1: global skill user overrides skill defaults
await withTempDir(async (tmpDir) => {
const globalDir = path.join(tmpDir, '.bmad', 'config');
await fs.mkdir(globalDir, { recursive: true });
await writeToml(path.join(globalDir, 'test-skill.user.toml'), {
agent: 'global-agent-prompt',
});
const skillDir = path.join(tmpDir, 'skill', 'test-skill');
await fs.mkdir(skillDir, { recursive: true });
await writeToml(path.join(skillDir, 'customize.toml'), {
agent: 'default-agent-prompt',
version: '1.0.0',
});
const origHome = process.env.HOME;
process.env.HOME = tmpDir;
try {
const result = JSON.parse(execSync(`python3 ${SCRIPTS_DIR}/resolve_customization.py --skill ${skillDir}`, { encoding: 'utf-8' }));
assert(
result.agent === 'global-agent-prompt',
'global user overrides skill defaults',
`Expected "global-agent-prompt", got "${result.agent}"`,
);
assert(result.version === '1.0.0', 'skill default values preserved', `Expected "1.0.0", got "${result.version}"`);
} finally {
process.env.HOME = origHome;
}
});
// Test 2: global skill layer provides value not in defaults
await withTempDir(async (tmpDir) => {
const globalDir = path.join(tmpDir, '.bmad', 'config');
await fs.mkdir(globalDir, { recursive: true });
await writeToml(path.join(globalDir, 'test-skill.user.toml'), {
extra_global_key: 'from-global',
});
const skillDir = path.join(tmpDir, 'skill', 'test-skill');
await fs.mkdir(skillDir, { recursive: true });
await writeToml(path.join(skillDir, 'customize.toml'), {
version: '2.0.0',
});
const origHome = process.env.HOME;
process.env.HOME = tmpDir;
try {
const result = JSON.parse(execSync(`python3 ${SCRIPTS_DIR}/resolve_customization.py --skill ${skillDir}`, { encoding: 'utf-8' }));
assert(
result.extra_global_key === 'from-global',
'global key not in defaults is preserved',
`Expected "from-global", got "${result.extra_global_key}"`,
);
assert(result.version === '2.0.0', 'skill defaults still present', `Expected "2.0.0", got "${result.version}"`);
} finally {
process.env.HOME = origHome;
}
});
// Test 3: missing global dir — backward compat
await withTempDir(async (tmpDir) => {
const skillDir = path.join(tmpDir, 'skill', 'test-skill');
await fs.mkdir(skillDir, { recursive: true });
await writeToml(path.join(skillDir, 'customize.toml'), {
version: '3.0.0',
});
const origHome = process.env.HOME;
process.env.HOME = tmpDir;
try {
const result = JSON.parse(execSync(`python3 ${SCRIPTS_DIR}/resolve_customization.py --skill ${skillDir}`, { encoding: 'utf-8' }));
assert(result.version === '3.0.0', 'works without global config dir', `Expected "3.0.0", got "${result.version}"`);
} finally {
process.env.HOME = origHome;
}
});
// Test 4: full priority chain — defaults < global < team < user
await withTempDir(async (tmpDir) => {
const globalDir = path.join(tmpDir, '.bmad', 'config');
await fs.mkdir(globalDir, { recursive: true });
await writeToml(path.join(globalDir, 'test-skill.user.toml'), {
agent: 'L1-Global',
});
const skillDir = path.join(tmpDir, 'project', '_bmad', 'skills', 'test-skill');
await fs.mkdir(skillDir, { recursive: true });
await writeToml(path.join(skillDir, 'customize.toml'), { agent: 'L0-Defaults' });
const customDir = path.join(tmpDir, 'project', '_bmad', 'custom');
await fs.mkdir(customDir, { recursive: true });
await writeToml(path.join(customDir, 'test-skill.toml'), { agent: 'L2-Team' });
await writeToml(path.join(customDir, 'test-skill.user.toml'), { agent: 'L3-User' });
const origHome = process.env.HOME;
process.env.HOME = tmpDir;
try {
const result = JSON.parse(execSync(`python3 ${SCRIPTS_DIR}/resolve_customization.py --skill ${skillDir}`, { encoding: 'utf-8' }));
assert(result.agent === 'L3-User', 'highest priority layer (project user) wins', `Expected "L3-User", got "${result.agent}"`);
} finally {
process.env.HOME = origHome;
}
});
}
async function main() {
console.log('Config Resolution Tests\n');
try {
await testResolveConfig();
await testResolveCustomization();
} catch (error) {
console.error(`${colors.red}Fatal error: ${error.message}${colors.reset}`);
process.exit(1);
}
console.log(`\n${colors.green}${passed} passed${colors.reset}, ${colors.red}${failed} failed${colors.reset}`);
process.exit(failed > 0 ? 1 : 0);
}
main();