Compare commits

...

7 Commits

Author SHA1 Message Date
Jérôme Revillard 8914b2dddb
Merge 4a0c59ff8b into e36f219c81 2026-05-02 09:58:35 +07:00
Alex Verkhovsky e36f219c81
refactor(catalog): rename after/before to preceded-by/followed-by (#2360)
* refactor(catalog): rename after/before columns to preceded-by/followed-by

The bare prepositions `after` and `before` had no subject anchor, leaving
the dependency direction ambiguous: "X has Y in its `after` column" reads
plausibly as either "Y comes after X" or "X comes after Y". An LLM
catalog consumer just got the direction wrong because of this.

`preceded-by` / `followed-by` are passive-voice participles whose grammar
locks the subject (the skill in this row) and forces a single reading:
"X is preceded by Y" can only mean Y comes first.

Rename applied to:
- module-help.csv headers (bmm-skills, core-skills)
- bmad-help SKILL.md schema doc + descriptions
- installer.js mergeModuleHelpCatalogs header string
- plugin-resolver.js _buildSynthesizedHelpCsv header string
- bmad-manifest.json keys (bmad-product-brief, bmad-prfaq)
- distillate-format-reference.md example manifest

The separate `required` column continues to carry hard-gate semantics;
the renamed columns are pure soft sequencing hints, as already documented
in bmad-help.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* style(installer): wrap long header strings per prettier

* feat(installer): warn on non-canonical module-help.csv headers

mergeModuleHelpCatalogs now compares each per-module file's header
against the canonical schema and emits a one-shot prompts.log.warn per
module on drift, naming both the expected and actual header. Data
continues to load positionally so external modules built against the
old after/before schema still install cleanly — the warning is the
maintainer signal to rename their columns.

Centralize the canonical header in modules/module-help-schema.js so the
merger and the synthesizer (PluginResolver._buildSynthesizedHelpCsv)
read the same source of truth; future column renames are one edit.

Verified by installing all four bmad-org external modules
(bmb, cis, gds, tea) — every one ships the legacy after/before header
today and now fires an advisory warning while still merging cleanly
into _bmad/_config/bmad-help.csv with the canonical column names.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 12:28:50 -07:00
Jerome Revillard 4a0c59ff8b fix(skills): update config resolution in SKILL.md files migrated from workflow.md
The 5 skills whose workflow.md was absorbed into SKILL.md by PR #2308
still had the old config.yaml loading instruction. Updated them to use
resolve_config.py like all other skills.
2026-04-29 16:02:08 +02:00
Jerome Revillard 7859186032 fix(skills): use resolve_config.py instead of reading config.yaml directly
Skills were reading _bmad/bmm|core/config.yaml directly, bypassing the
TOML merge mechanism. Now they call resolve_config.py first, with a
fallback to read the merge logic and apply it manually.
2026-04-29 16:02:08 +02:00
Jerome Revillard 834b89a841 fix: update argparse descriptions to match actual layer count 2026-04-29 16:02:08 +02:00
Jerome Revillard 294a03db3a fix(config): correct global layer priority — overrides installer defaults
The global user layer was lowest priority, meaning installer-generated
defaults in _bmad/config.user.toml always shadowed it. Reorder so
global user preferences override installer defaults but are still
overridden by hand-authored project customizations.

New order for resolve_config.py:
  base_team → base_user → global_user → custom_team → custom_user

New order for resolve_customization.py:
  defaults → global_user → team → user
2026-04-29 16:02:08 +02:00
Jerome Revillard bfd602de62 feat(config): add per-user global config layer at ~/.bmad/config/
Users working across multiple worktrees or repos no longer need to
re-enter personal settings (user_name, communication_language,
user_skill_level) in every project. A global user layer at
~/.bmad/config/config.user.toml is merged as the lowest-priority
fallback in both resolve_config.py (5-layer) and
resolve_customization.py (4-layer). Project-level overrides always
win. Missing global dir is fully backward-compatible.

Closes #2338
2026-04-29 16:02:08 +02:00
46 changed files with 446 additions and 60 deletions

View File

@ -46,7 +46,7 @@ Treat every entry in `{agent.persistent_facts}` as foundational context you carr
### Step 5: Load Config ### 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 `{user_name}` for greeting
- Use `{communication_language}` for all communications - Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents - 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 ### 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 `{user_name}` for greeting
- Use `{communication_language}` for all communications - Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents - 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 ### 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 `{user_name}` for greeting
- Use `{communication_language}` for all communications - Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents - Use `{document_output_language}` for output documents

View File

@ -11,7 +11,7 @@
### Configuration Loading ### 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` - `project_knowledge`
- `user_name` - `user_name`

View File

@ -10,7 +10,7 @@
### Configuration Loading ### 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` - `project_knowledge`
- `user_name` - `user_name`

View File

@ -50,7 +50,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config ### 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 `{user_name}` for greeting
- Use `{communication_language}` for all communications - Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents - Use `{document_output_language}` for output documents

View File

@ -7,8 +7,8 @@
"description": "Produces battle-tested PRFAQ document and optional LLM distillate for PRD input.", "description": "Produces battle-tested PRFAQ document and optional LLM distillate for PRD input.",
"supports-headless": true, "supports-headless": true,
"phase-name": "1-analysis", "phase-name": "1-analysis",
"after": ["brainstorming", "perform-research"], "preceded-by": ["brainstorming", "perform-research"],
"before": ["create-prd"], "followed-by": ["create-prd"],
"is-required": false, "is-required": false,
"output-location": "{planning_artifacts}" "output-location": "{planning_artifacts}"
} }

View File

@ -59,7 +59,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config ### 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 `{user_name}` for greeting
- Use `{communication_language}` for all communications - Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents - Use `{document_output_language}` for output documents

View File

@ -8,8 +8,8 @@
"description": "Produces executive product brief and optional LLM distillate for PRD input.", "description": "Produces executive product brief and optional LLM distillate for PRD input.",
"supports-headless": true, "supports-headless": true,
"phase-name": "1-analysis", "phase-name": "1-analysis",
"after": ["brainstorming", "perform-research"], "preceded-by": ["brainstorming", "perform-research"],
"before": ["create-prd"], "followed-by": ["create-prd"],
"is-required": true, "is-required": true,
"output-location": "{planning_artifacts}" "output-location": "{planning_artifacts}"
} }

View File

@ -44,7 +44,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config ### 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 `{user_name}` for greeting
- Use `{communication_language}` for all communications - Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents - 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 ### 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 `{user_name}` for greeting
- Use `{communication_language}` for all communications - Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents - 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 ### 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 `{user_name}` for greeting
- Use `{communication_language}` for all communications - Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents - 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 ### 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 `{user_name}` for greeting
- Use `{communication_language}` for all communications - Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents - 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 ### 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 `{user_name}` for greeting
- Use `{communication_language}` for all communications - Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents - 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 ### 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 `{user_name}` for greeting
- Use `{communication_language}` for all communications - Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents - 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 ### 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 `{user_name}` for greeting
- Use `{communication_language}` for all communications - Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents - 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 ### 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 `{user_name}` for greeting
- Use `{communication_language}` for all communications - Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents - 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 ### 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 `{user_name}` for greeting
- Use `{communication_language}` for all communications - Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents - 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 ### 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 `{user_name}` for greeting
- Use `{communication_language}` for all communications - Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents - 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 ### 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 `{user_name}` for greeting
- Use `{communication_language}` for all communications - Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents - 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 ### 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 `{user_name}` for greeting
- Use `{communication_language}` for all communications - Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents - 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 ### 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 `{user_name}` for greeting
- Use `{communication_language}` for all communications - Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents - 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 ### 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 `{user_name}` for greeting
- Use `{communication_language}` for all communications - Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents - 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 ### 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 `{user_name}` for greeting
- Use `{communication_language}` for all communications - Use `{communication_language}` for all communications
- Use `{document_output_language}` for output documents - 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 ### 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` - `implementation_artifacts`
- `planning_artifacts` - `planning_artifacts`
@ -63,6 +63,18 @@ Activation is complete. Begin the workflow below.
- **Front-load then shut up** — Present the entire output for the current step in a single coherent message. Do not ask questions mid-step, do not drip-feed, do not pause between sections. - **Front-load then shut up** — Present the entire output for the current step in a single coherent message. Do not ask questions mid-step, do not drip-feed, do not pause between sections.
- **Language** — Speak in `{communication_language}`. Write any file output in `{document_output_language}`. - **Language** — Speak in `{communication_language}`. Write any file output in `{document_output_language}`.
<<<<<<< HEAD
=======
## INITIALIZATION
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`
- `communication_language`
- `document_output_language`
>>>>>>> 3846e184 (fix(skills): use resolve_config.py instead of reading config.yaml directly)
## FIRST STEP ## FIRST STEP
Read fully and follow `./step-01-orientation.md` to begin. Read fully and follow `./step-01-orientation.md` to begin.

View File

@ -40,7 +40,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
### Step 4: Load Config ### 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` - `project_name`, `planning_artifacts`, `implementation_artifacts`, `user_name`
- `communication_language`, `document_output_language`, `user_skill_level` - `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 ### 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` - `project_name`, `user_name`
- `communication_language`, `document_output_language` - `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 ### 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` - `project_name`, `user_name`
- `communication_language`, `document_output_language` - `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 ### 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` - `project_name`, `user_name`
- `communication_language`, `document_output_language` - `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 ### 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` - `project_name`, `user_name`
- `communication_language`, `document_output_language` - `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 ### 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` - `project_name`, `planning_artifacts`, `implementation_artifacts`, `user_name`
- `communication_language`, `document_output_language`, `user_skill_level` - `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 ### 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` - `project_name`, `user_name`
- `communication_language`, `document_output_language` - `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 ### 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` - `project_name`, `user_name`
- `communication_language`, `document_output_language` - `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 ### 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` - `project_name`, `user_name`
- `communication_language`, `document_output_language` - `communication_language`, `document_output_language`

View File

@ -1,4 +1,4 @@
module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs
BMad Method,_meta,,,,,,,,,false,https://docs.bmad-method.org/llms.txt, BMad Method,_meta,,,,,,,,,false,https://docs.bmad-method.org/llms.txt,
BMad Method,bmad-document-project,Document Project,DP,Analyze an existing project to produce useful documentation.,,,anytime,,,false,project-knowledge,* BMad Method,bmad-document-project,Document Project,DP,Analyze an existing project to produce useful documentation.,,,anytime,,,false,project-knowledge,*
BMad Method,bmad-generate-project-context,Generate Project Context,GPC,Scan existing codebase to generate a lean LLM-optimized project-context.md. Essential for brownfield projects.,,,anytime,,,false,output_folder,project context BMad Method,bmad-generate-project-context,Generate Project Context,GPC,Scan existing codebase to generate a lean LLM-optimized project-context.md. Essential for brownfield projects.,,,anytime,,,false,output_folder,project context

1 module skill display-name menu-code description action args phase after preceded-by before followed-by required output-location outputs
2 BMad Method _meta false https://docs.bmad-method.org/llms.txt
3 BMad Method bmad-document-project Document Project DP Analyze an existing project to produce useful documentation. anytime false project-knowledge *
4 BMad Method bmad-generate-project-context Generate Project Context GPC Scan existing codebase to generate a lean LLM-optimized project-context.md. Essential for brownfield projects. anytime false output_folder project context

View File

@ -32,7 +32,7 @@ This uses **micro-file architecture** for disciplined execution:
### Configuration Loading ### 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` - `project_name`, `output_folder`, `user_name`
- `communication_language`, `document_output_language`, `user_skill_level` - `communication_language`, `document_output_language`, `user_skill_level`

View File

@ -139,7 +139,7 @@ parts: 1
## Solution Architecture ## Solution Architecture
- Plugins: skill bundles with Anthropic plugin standard as base format + bmad-manifest.json extending for BMAD-specific metadata (installer options, capabilities, help integration, phase ordering, dependencies) - Plugins: skill bundles with Anthropic plugin standard as base format + bmad-manifest.json extending for BMAD-specific metadata (installer options, capabilities, help integration, phase ordering, dependencies)
- Existing manifest example: `{"module-code":"bmm","replaces-skill":"bmad-create-product-brief","capabilities":[{"name":"create-brief","menu-code":"CB","supports-headless":true,"phase-name":"1-analysis","after":["brainstorming"],"before":["create-prd"],"is-required":true}]}` - Existing manifest example: `{"module-code":"bmm","replaces-skill":"bmad-create-product-brief","capabilities":[{"name":"create-brief","menu-code":"CB","supports-headless":true,"phase-name":"1-analysis","preceded-by":["brainstorming"],"followed-by":["create-prd"],"is-required":true}]}`
- Vercel skills CLI handles platform translation; integration pattern (wrap/fork/call) is PRD decision - Vercel skills CLI handles platform translation; integration pattern (wrap/fork/call) is PRD decision
- bmad-setup: global skill scanning installed bmad-manifest.json files, registering capabilities, configuring project settings; always included as base skill in every bundle (solves bootstrapping) - bmad-setup: global skill scanning installed bmad-manifest.json files, registering capabilities, configuring project settings; always included as base skill in every bundle (solves bootstrapping)
- bmad-update: plugin update path without full reinstall; technical approach (diff/replace/preserve customizations) is PRD decision - bmad-update: plugin update path without full reinstall; technical approach (diff/replace/preserve customizations) is PRD decision

View File

@ -23,7 +23,7 @@ When this skill completes, the user should:
## Data Sources ## Data Sources
- **Catalog**: `{project-root}/_bmad/_config/bmad-help.csv` — assembled manifest of all installed module skills - **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 - **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. - **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. - **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.
@ -33,16 +33,16 @@ When this skill completes, the user should:
The catalog uses this format: The catalog uses this format:
``` ```
module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs
``` ```
**Phases** determine the high-level flow: **Phases** determine the high-level flow:
- `anytime` — available regardless of workflow state - `anytime` — available regardless of workflow state
- Numbered phases (`1-analysis`, `2-planning`, etc.) flow in order; naming varies by module - Numbered phases (`1-analysis`, `2-planning`, etc.) flow in order; naming varies by module
**Dependencies** determine ordering within and across phases: **Sequencing** determines recommended ordering within and across phases (these are soft suggestions, not hard gates — see `required` for gating):
- `after` — skills that should ideally complete before this one - `preceded-by` — skills that should ideally complete before this one
- `before` — skills that should run after this one - `followed-by` — skills that should ideally run after this one
- Format: `skill-name` for single-action skills, `skill-name:action` for multi-action skills - Format: `skill-name` for single-action skills, `skill-name:action` for multi-action skills
**Required gates**: **Required gates**:

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. 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 `{user_name}` for greeting
- Use `{communication_language}` for all communications - Use `{communication_language}` for all communications

View File

@ -1,4 +1,4 @@
module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs
Core,_meta,,,,,,,,,false,https://docs.bmad-method.org/llms.txt, Core,_meta,,,,,,,,,false,https://docs.bmad-method.org/llms.txt,
Core,bmad-brainstorming,Brainstorming,BSP,Use early in ideation or when stuck generating ideas.,,,anytime,,,false,{output_folder}/brainstorming,brainstorming session Core,bmad-brainstorming,Brainstorming,BSP,Use early in ideation or when stuck generating ideas.,,,anytime,,,false,{output_folder}/brainstorming,brainstorming session
Core,bmad-party-mode,Party Mode,PM,Orchestrate multi-agent discussions when you need multiple perspectives or want agents to collaborate.,,,anytime,,,false,, Core,bmad-party-mode,Party Mode,PM,Orchestrate multi-agent discussions when you need multiple perspectives or want agents to collaborate.,,,anytime,,,false,,

1 module skill display-name menu-code description action args phase after preceded-by before followed-by required output-location outputs
2 Core _meta false https://docs.bmad-method.org/llms.txt
3 Core bmad-brainstorming Brainstorming BSP Use early in ideation or when stuck generating ideas. anytime false {output_folder}/brainstorming brainstorming session
4 Core bmad-party-mode Party Mode PM Orchestrate multi-agent discussions when you need multiple perspectives or want agents to collaborate. anytime false

View File

@ -1,12 +1,13 @@
#!/usr/bin/env python3 #!/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) 1. {project-root}/_bmad/config.toml (installer-owned team)
2. {project-root}/_bmad/config.user.toml (installer-owned user) 2. {project-root}/_bmad/config.user.toml (installer-owned user)
3. {project-root}/_bmad/custom/config.toml (human-authored team, committed) 3. ~/.bmad/config/config.user.toml (global user preferences)
4. {project-root}/_bmad/custom/config.user.toml (human-authored user, gitignored) 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. Outputs merged JSON to stdout. Errors go to stderr.
@ -40,6 +41,7 @@ except ImportError:
_MISSING = object() _MISSING = object()
_KEYED_MERGE_FIELDS = ("code", "id") _KEYED_MERGE_FIELDS = ("code", "id")
GLOBAL_DIR = Path.home() / ".bmad" / "config"
def load_toml(file_path: Path, required: bool = False) -> dict: def load_toml(file_path: Path, required: bool = False) -> dict:
@ -136,7 +138,7 @@ def extract_key(data, dotted_key: str):
def main(): def main():
parser = argparse.ArgumentParser( 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( parser.add_argument(
"--project-root", "-p", required=True, "--project-root", "-p", required=True,
@ -153,10 +155,12 @@ def main():
base_team = load_toml(bmad_dir / "config.toml", required=True) base_team = load_toml(bmad_dir / "config.toml", required=True)
base_user = load_toml(bmad_dir / "config.user.toml") 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_team = load_toml(bmad_dir / "custom" / "config.toml")
custom_user = load_toml(bmad_dir / "custom" / "config.user.toml") custom_user = load_toml(bmad_dir / "custom" / "config.user.toml")
merged = deep_merge(base_team, base_user) merged = deep_merge(base_team, base_user)
merged = deep_merge(merged, global_user)
merged = deep_merge(merged, custom_team) merged = deep_merge(merged, custom_team)
merged = deep_merge(merged, custom_user) merged = deep_merge(merged, custom_user)

View File

@ -1,11 +1,12 @@
#!/usr/bin/env python3 #!/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) 1. {project-root}/_bmad/custom/{name}.user.toml (personal, gitignored)
2. {project-root}/_bmad/custom/{name}.toml (team/org, committed) 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. Skill name is derived from the basename of the skill directory.
@ -51,6 +52,7 @@ except ImportError:
_MISSING = object() _MISSING = object()
_KEYED_MERGE_FIELDS = ("code", "id") _KEYED_MERGE_FIELDS = ("code", "id")
GLOBAL_DIR = Path.home() / ".bmad" / "config"
def find_project_root(start: Path): def find_project_root(start: Path):
@ -179,7 +181,7 @@ def extract_key(data, dotted_key: str):
def main(): def main():
parser = argparse.ArgumentParser( 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, add_help=True,
) )
parser.add_argument( parser.add_argument(
@ -211,7 +213,10 @@ def main():
team = load_toml(custom_dir / f"{skill_name}.toml") team = load_toml(custom_dir / f"{skill_name}.toml")
user = load_toml(custom_dir / f"{skill_name}.user.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) merged = deep_merge(merged, user)
if args.key: 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();

View File

@ -12,6 +12,7 @@ const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
const { InstallPaths } = require('./install-paths'); const { InstallPaths } = require('./install-paths');
const { ExternalModuleManager } = require('../modules/external-manager'); const { ExternalModuleManager } = require('../modules/external-manager');
const { resolveModuleVersion } = require('../modules/version-resolver'); const { resolveModuleVersion } = require('../modules/version-resolver');
const { MODULE_HELP_CSV_HEADER } = require('../modules/module-help-schema');
const { ExistingInstall } = require('./existing-install'); const { ExistingInstall } = require('./existing-install');
const { warnPreNativeSkillsLegacy } = require('./legacy-warnings'); const { warnPreNativeSkillsLegacy } = require('./legacy-warnings');
@ -942,7 +943,7 @@ class Installer {
*/ */
async mergeModuleHelpCatalogs(bmadDir, _agentEntries = []) { async mergeModuleHelpCatalogs(bmadDir, _agentEntries = []) {
const allRows = []; const allRows = [];
const headerRow = 'module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs'; const headerRow = MODULE_HELP_CSV_HEADER;
const COLUMN_COUNT = 13; const COLUMN_COUNT = 13;
const PHASE_INDEX = 7; const PHASE_INDEX = 7;
@ -975,9 +976,19 @@ class Installer {
const content = await fs.readFile(helpFilePath, 'utf8'); const content = await fs.readFile(helpFilePath, 'utf8');
const lines = content.split('\n').filter((line) => line.trim() && !line.startsWith('#')); const lines = content.split('\n').filter((line) => line.trim() && !line.startsWith('#'));
let headerWarned = false;
for (const line of lines) { for (const line of lines) {
// Skip header row // Header row: warn on drift from canonical schema, then skip.
// Data rows are loaded positionally regardless, so the warning
// is advisory — the maintainer should rename their columns.
if (line.startsWith('module,')) { if (line.startsWith('module,')) {
if (!headerWarned && line.trim() !== headerRow) {
await prompts.log.warn(
` ${moduleName}/module-help.csv header does not match canonical schema. ` +
`Expected: ${headerRow} | Found: ${line.trim()} | Data loaded positionally.`,
);
headerWarned = true;
}
continue; continue;
} }

View File

@ -0,0 +1,13 @@
/**
* Canonical schema for per-module `module-help.csv` files.
*
* Both the merger (`Installer.mergeModuleHelpCatalogs`) and the synthesizer
* (`PluginResolver._buildSynthesizedHelpCsv`) emit this exact header. The
* merger compares each per-module file's header against this string and
* warns on drift, so any rename here must be matched in external module
* authors' CSVs (or accepted as a positional fall-through with a warning).
*/
const MODULE_HELP_CSV_HEADER =
'module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs';
module.exports = { MODULE_HELP_CSV_HEADER };

View File

@ -1,6 +1,7 @@
const fs = require('../fs-native'); const fs = require('../fs-native');
const path = require('node:path'); const path = require('node:path');
const yaml = require('yaml'); const yaml = require('yaml');
const { MODULE_HELP_CSV_HEADER } = require('./module-help-schema');
/** /**
* Resolves how to install a plugin from marketplace.json by analyzing * Resolves how to install a plugin from marketplace.json by analyzing
@ -338,8 +339,7 @@ class PluginResolver {
* @returns {string} CSV content * @returns {string} CSV content
*/ */
_buildSynthesizedHelpCsv(moduleName, skillInfos) { _buildSynthesizedHelpCsv(moduleName, skillInfos) {
const header = 'module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs'; const rows = [MODULE_HELP_CSV_HEADER];
const rows = [header];
for (const info of skillInfos) { for (const info of skillInfos) {
const displayName = this._formatDisplayName(info.name || info.dirName); const displayName = this._formatDisplayName(info.name || info.dirName);