Compare commits

...

11 Commits

Author SHA1 Message Date
Jérôme Revillard ef991f04d4
Merge 5416e12882 into 48a7ec8bff 2026-04-28 06:06:42 +00:00
Jerome Revillard 5416e12882 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-28 08:06:04 +02:00
Jerome Revillard c95a349ae8 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-28 08:06:04 +02:00
Jerome Revillard 3ae4d13ba4 fix: update argparse descriptions to match actual layer count 2026-04-28 08:06:04 +02:00
Jerome Revillard dfdc18df2f 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-28 08:06:04 +02:00
Jerome Revillard 9f6e83c255 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-28 08:06:04 +02:00
Brian 48a7ec8bff
fix: align bmad-help.csv with documented schema and clean up source rows (#2278) (#2349)
* fix(installer): preserve module-help.csv schema in merged bmad-help.csv (#2278)

The installer's mergeModuleHelpCatalogs was rewriting the merged catalog
under a different schema (module,phase,name,code,sequence,workflow-file,...)
than the documented source schema in every module's module-help.csv
(module,skill,display-name,menu-code,description,action,args,phase,...).

Worse, the parsing assumed the wrong source column order, so column data
was scrambled in the merged output. SKILL.md docs the source schema, so
the bmad-help skill was navigating a catalog whose actual columns no
longer matched its mental model.

Drop the transformation and the agent enrichment columns (which had no
consumers anywhere in the codebase). Emit rows verbatim in the source
schema, padding short rows and filling empty module fields. Sort by
module then phase, stable within phase to preserve authored order.

Closes #2278

* fix(catalog): normalize module-help.csv rows to documented 13-column schema

Many rows in core-skills/module-help.csv and bmm-skills/module-help.csv
were missing one column between description and phase, leaving them at
12 fields instead of 13. CSV consumers that read by header position
were silently mapping data into the wrong columns (description into
action, phase into args, required into before, etc).

Inserted an empty cell at column index 5 across all 31 affected rows
to restore alignment with the documented header
(module,skill,display-name,menu-code,description,action,args,phase,
after,before,required,output-location,outputs).
2026-04-27 23:54:21 -05:00
Brian 3da984a491
fix(config): promote project_name to core (closes #2279) (#2348)
* fix(config): promote project_name to core, fixes #2279

project_name was a bmm-specific prompt despite being a universal
project-level concept used by every module — including core skills like
bmad-brainstorming, which loads from _bmad/core/config.yaml and was
silently broken because project_name lived under bmm. Users without bmm
installed could not run brainstorming at all.

Move:
- src/core-skills/module.yaml: declare project_name with prompt
  "What is your project called?" and default {directory_name}, matching
  what bmm previously had.
- src/bmm-skills/module.yaml: remove the bmm definition; add project_name
  to the "Variables from Core Config inserted" header comment so
  contributors can see what's inherited.

Migration for existing installs:
- tools/installer/modules/official-modules.js: after loadExistingConfig
  reads each per-module config.yaml, hoist any keys that are now declared
  in core but appear under non-core modules. Without this, the partition
  logic in writeCentralConfig (which strips core keys from non-core
  buckets) would silently drop the user's prior project_name on the next
  quick-update. Generic — handles project_name today and any future
  module→core promotions.
- The hoist preserves precedence: an existing core value beats a stale
  module-side copy.

--yes seed:
- tools/installer/ui.js: add project_name to the hardcoded core seed
  (using path.basename(directory) to match the {directory_name} default)
  so non-interactive fresh installs populate it. Without this the seed
  silently omits project_name and core skills fall back to literals.

Tests:
- test/test-installation-components.js Suite 43 (9 assertions) covers
  the schema move, the loadExistingConfig hoist, and the precedence rule.
- Suite 35 fixture updated: project_name moved from bmm bucket to core,
  with a stale bmm copy left in place to verify it gets stripped.

Verified manually:
- Fresh install -y: project_name lands in [core] of config.toml.
- Existing install with project_name in bmm/config.yaml: quick-update
  hoists it to [core] and strips it from [modules.bmm].

* fix(installer): harden config-load against malformed config.yaml

Per augment review on #2348: loadExistingConfig stored any truthy
yaml.parse result (including scalars like '42'), which would later crash
_hoistCoreKeysFromLegacyModuleConfigs at \`key in cfg\` with
"Cannot use 'in' operator to search for ... in 42".

- loadExistingConfig: only keep parses that are plain objects (not
  scalars or arrays). A corrupt config.yaml is now treated the same as
  a parse error — skipped, not crashed-on.
- _hoistCoreKeysFromLegacyModuleConfigs: belt-and-suspenders type guards
  on _existingConfig.core (in case it's populated by some other path)
  and on each module cfg in the loop.
- Test Suite 43 adds 2 assertions covering a scalar core/config.yaml:
  loadExistingConfig must not crash, and bmm.project_name must still
  hoist into a clean core bucket.
2026-04-27 23:31:59 -05:00
Brian 815600e4ca
fix(create-architecture): unprime step-07 validation checklist (#2292) (#2347)
step-07-validation template shipped with all 16 completeness checkboxes
pre-checked and Overall Status hard-coded to READY FOR IMPLEMENTATION,
defeating the gate. Reset checkboxes to unchecked, replace status with a
templated choice tied to the checklist and gap analysis, and instruct the
agent to only mark items the validation actually confirms.

Closes #2292
2026-04-27 23:14:23 -05:00
Brian 7ee5fa313b
fix(installer): require --tools for fresh --yes installs; remove --tools none (#2346)
* fix(installer): require --tools for fresh --yes installs; remove --tools none (closes #2326)

Fresh non-interactive installs without --tools previously produced a
config-only install (~35 files vs ~1400 in the manifest) with no warning
and a "BMAD is ready to use" success card, leaving slash commands
unreachable. --tools none was an explicit opt-in for the same broken
state.

Now: fresh install + -y without --tools throws a helpful error pointing
at --list-tools. --tools none is rejected as an unknown ID. Empty and
typo'd tool IDs are also rejected. Existing-install paths (--action
update, quick-update, modify) are unchanged - they continue to reuse
previously-configured tools when --tools is omitted.

Adds --list-tools flag that prints all 42 supported tool IDs (id, name,
target_dir, preferred star) sourced from platform-codes.yaml.

English docs updated; localized docs (vi-vn, fr, cs, etc.) will sync via
the normal translation pass.

* fix(installer): address review for #2326 — single source of truth, drop dead code, add tests

- Refactor formatPlatformList to use IdeManager so --list-tools and --tools
  validation see the same set of platforms. Eliminates the drift where suspended
  platforms appeared in --list-tools but were rejected at validation.
- Drop unused getValidPlatformIds export.
- Flatten redundant block scope around the throw in the --yes-without-tools
  branch (refactor leftover).
- Drop dead String() defensive cast (Commander always passes a string).
- Add Test Suite 42: 8 unit tests covering _parseToolsFlag empty/whitespace/
  unknown/typo cases plus an integration check that --list-tools output and
  --tools validation agree on the ID set.

* fix(installer): close --tools "" bypass and drop hardcoded tool count

- Replace truthy `if (options.tools)` guard with `!== undefined` in both
  upgrade and fresh-install branches. Empty string now reaches
  _parseToolsFlag and produces the specific "passed empty" error
  instead of falling through to a generic message (fresh-install) or
  being silently ignored (existing-install).
- Drop the hardcoded "42 supported tools" count from the prereqs in
  install-bmad.md so the doc doesn't drift as platform-codes.yaml
  changes.

Addresses augment / coderabbit review on #2346.
2026-04-27 23:01:23 -05:00
Jérôme Revillard 3e89b30b3c
fix: use full update path when --custom-source is passed with --yes (#2336)
* fix: use full update path when --custom-source is passed with --yes

When --yes is used on an existing install, the installer auto-selects
quick-update. However, quick-update never re-clones custom module repos
— it only reads whatever is already in the cache. This means
--custom-source with a new version tag (e.g. @1.1.0) is silently
ignored and the previously cached version (e.g. 1.0.1) is reported as
"already up to date".

Default to the full update path when --custom-source is present, so the
custom repo gets re-cloned at the requested version. Also ensure all
installed modules are included in the selection when --yes is combined
with --custom-source, preventing previously installed modules from being
removed.

* fix: address review feedback on choices.find() and comment clarity

* style: prettier fix for empty-body methods in custom-module-manager

---------

Co-authored-by: Brian <bmadcode@gmail.com>
2026-04-27 20:49:21 -05:00
51 changed files with 890 additions and 244 deletions

View File

@ -18,7 +18,7 @@ Use `npx bmad-method install` to set up BMad in your project. One command handle
- **Node.js** 20+ (the installer requires it) - **Node.js** 20+ (the installer requires it)
- **Git** (for cloning external modules) - **Git** (for cloning external modules)
- **An AI tool** such as Claude Code or Cursor — or install without one using `--tools none` - **An AI tool** such as Claude Code or Cursor (run `npx bmad-method install --list-tools` to see all supported tools)
::: :::
@ -122,7 +122,8 @@ Under `--yes`, patch and minor upgrades apply automatically. Majors stay frozen
| `--yes`, `-y` | Skip all prompts; accept flag values + defaults | | `--yes`, `-y` | Skip all prompts; accept flag values + defaults |
| `--directory <path>` | Install into this directory (default: current working dir) | | `--directory <path>` | Install into this directory (default: current working dir) |
| `--modules <a,b,c>` | Exact module set. Core is auto-added. Not a delta — list everything you want kept. | | `--modules <a,b,c>` | Exact module set. Core is auto-added. Not a delta — list everything you want kept. |
| `--tools <a,b>` or `--tools none` | IDE/tool selection. `none` skips tool config entirely. | | `--tools <a,b>` | IDE/tool selection. Required for fresh `--yes` installs. Run `--list-tools` for valid IDs. |
| `--list-tools` | Print all supported tool/IDE IDs (with target directories) and exit. |
| `--action <type>` | `install`, `update`, or `quick-update`. Defaults based on existing install state. | | `--action <type>` | `install`, `update`, or `quick-update`. Defaults based on existing install state. |
| `--custom-source <urls>` | Install custom modules from Git URLs or local paths | | `--custom-source <urls>` | Install custom modules from Git URLs or local paths |
| `--channel <stable\|next>` | Apply to all externals (aliased as `--all-stable` / `--all-next`) | | `--channel <stable\|next>` | Apply to all externals (aliased as `--all-stable` / `--all-next`) |
@ -165,17 +166,17 @@ npx bmad-method install --yes --modules bmm,bmb --all-next --tools claude-code
```bash ```bash
npx bmad-method install --yes --action update \ npx bmad-method install --yes --action update \
--modules bmm,bmb,gds \ --modules bmm,bmb,gds
--tools none
``` ```
`--tools` is omitted intentionally — `--action update` reuses the tools configured during the first install.
**Mix channels — bmb on next, gds on stable:** **Mix channels — bmb on next, gds on stable:**
```bash ```bash
npx bmad-method install --yes --action update \ npx bmad-method install --yes --action update \
--modules bmm,bmb,cis,gds \ --modules bmm,bmb,cis,gds \
--next=bmb \ --next=bmb
--tools none
``` ```
:::caution[Rate limit on shared IPs] :::caution[Rate limit on shared IPs]
@ -204,7 +205,7 @@ For cross-machine reproducibility, don't rely on rerunning the same `--modules`
```bash ```bash
npx bmad-method install --yes --modules bmb,cis \ npx bmad-method install --yes --modules bmb,cis \
--pin bmb=v1.7.0 --pin cis=v0.4.2 --tools none --pin bmb=v1.7.0 --pin cis=v0.4.2 --tools claude-code
``` ```
## Troubleshooting ## Troubleshooting

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

@ -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

@ -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

@ -227,37 +227,39 @@ Prepare the content to append to the document:
### Architecture Completeness Checklist ### Architecture Completeness Checklist
**✅ Requirements Analysis** Mark each item `[x]` only if validation confirms it; leave `[ ]` if it is missing, partial, or unverified. Any unchecked item must be reflected in the Gap Analysis above and in the Overall Status below.
- [x] Project context thoroughly analyzed **Requirements Analysis**
- [x] Scale and complexity assessed
- [x] Technical constraints identified
- [x] Cross-cutting concerns mapped
**✅ Architectural Decisions** - [ ] Project context thoroughly analyzed
- [ ] Scale and complexity assessed
- [ ] Technical constraints identified
- [ ] Cross-cutting concerns mapped
- [x] Critical decisions documented with versions **Architectural Decisions**
- [x] Technology stack fully specified
- [x] Integration patterns defined
- [x] Performance considerations addressed
**✅ Implementation Patterns** - [ ] Critical decisions documented with versions
- [ ] Technology stack fully specified
- [ ] Integration patterns defined
- [ ] Performance considerations addressed
- [x] Naming conventions established **Implementation Patterns**
- [x] Structure patterns defined
- [x] Communication patterns specified
- [x] Process patterns documented
**✅ Project Structure** - [ ] Naming conventions established
- [ ] Structure patterns defined
- [ ] Communication patterns specified
- [ ] Process patterns documented
- [x] Complete directory structure defined **Project Structure**
- [x] Component boundaries established
- [x] Integration points mapped - [ ] Complete directory structure defined
- [x] Requirements to structure mapping complete - [ ] Component boundaries established
- [ ] Integration points mapped
- [ ] Requirements to structure mapping complete
### Architecture Readiness Assessment ### Architecture Readiness Assessment
**Overall Status:** READY FOR IMPLEMENTATION **Overall Status:** {{READY FOR IMPLEMENTATION | READY WITH MINOR GAPS | NOT READY}} (choose READY FOR IMPLEMENTATION only when all 16 checklist items are `[x]` and no Critical Gaps remain; choose NOT READY when any Critical Gap is open or any Requirements Analysis or Architectural Decisions item is unchecked; otherwise READY WITH MINOR GAPS)
**Confidence Level:** {{high/medium/low}} based on validation results **Confidence Level:** {{high/medium/low}} based on validation results

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,33 +1,33 @@
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,after,before,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
BMad Method,bmad-quick-dev,Quick Dev,QQ,Unified intent-in code-out workflow: clarify plan implement review and present.,,anytime,,,false,implementation_artifacts,spec and project implementation BMad Method,bmad-quick-dev,Quick Dev,QQ,Unified intent-in code-out workflow: clarify plan implement review and present.,,,anytime,,,false,implementation_artifacts,spec and project implementation
BMad Method,bmad-correct-course,Correct Course,CC,Navigate significant changes. May recommend start over update PRD redo architecture sprint planning or correct epics and stories.,,anytime,,,false,planning_artifacts,change proposal BMad Method,bmad-correct-course,Correct Course,CC,Navigate significant changes. May recommend start over update PRD redo architecture sprint planning or correct epics and stories.,,,anytime,,,false,planning_artifacts,change proposal
BMad Method,bmad-agent-tech-writer,Write Document,WD,"Describe in detail what you want, and the agent will follow documentation best practices. Multi-turn conversation with subprocess for research/review.",write,,anytime,,,false,project-knowledge,document BMad Method,bmad-agent-tech-writer,Write Document,WD,"Describe in detail what you want, and the agent will follow documentation best practices. Multi-turn conversation with subprocess for research/review.",write,,anytime,,,false,project-knowledge,document
BMad Method,bmad-agent-tech-writer,Update Standards,US,Update agent memory documentation-standards.md with your specific preferences if you discover missing document conventions.,update-standards,,anytime,,,false,_bmad/_memory/tech-writer-sidecar,standards BMad Method,bmad-agent-tech-writer,Update Standards,US,Update agent memory documentation-standards.md with your specific preferences if you discover missing document conventions.,update-standards,,anytime,,,false,_bmad/_memory/tech-writer-sidecar,standards
BMad Method,bmad-agent-tech-writer,Mermaid Generate,MG,Create a Mermaid diagram based on user description. Will suggest diagram types if not specified.,mermaid,,anytime,,,false,planning_artifacts,mermaid diagram BMad Method,bmad-agent-tech-writer,Mermaid Generate,MG,Create a Mermaid diagram based on user description. Will suggest diagram types if not specified.,mermaid,,anytime,,,false,planning_artifacts,mermaid diagram
BMad Method,bmad-agent-tech-writer,Validate Document,VD,Review the specified document against documentation standards and best practices. Returns specific actionable improvement suggestions organized by priority.,validate,[path],anytime,,,false,planning_artifacts,validation report BMad Method,bmad-agent-tech-writer,Validate Document,VD,Review the specified document against documentation standards and best practices. Returns specific actionable improvement suggestions organized by priority.,validate,[path],anytime,,,false,planning_artifacts,validation report
BMad Method,bmad-agent-tech-writer,Explain Concept,EC,Create clear technical explanations with examples and diagrams for complex concepts.,explain,[topic],anytime,,,false,project_knowledge,explanation BMad Method,bmad-agent-tech-writer,Explain Concept,EC,Create clear technical explanations with examples and diagrams for complex concepts.,explain,[topic],anytime,,,false,project_knowledge,explanation
BMad Method,bmad-brainstorming,Brainstorm Project,BP,Expert guided facilitation through a single or multiple techniques.,,1-analysis,,,false,planning_artifacts,brainstorming session BMad Method,bmad-brainstorming,Brainstorm Project,BP,Expert guided facilitation through a single or multiple techniques.,,,1-analysis,,,false,planning_artifacts,brainstorming session
BMad Method,bmad-market-research,Market Research,MR,"Market analysis competitive landscape customer needs and trends.",,1-analysis,,,false,"planning_artifacts|project-knowledge",research documents BMad Method,bmad-market-research,Market Research,MR,Market analysis competitive landscape customer needs and trends.,,,1-analysis,,,false,planning_artifacts|project-knowledge,research documents
BMad Method,bmad-domain-research,Domain Research,DR,Industry domain deep dive subject matter expertise and terminology.,,1-analysis,,,false,"planning_artifacts|project_knowledge",research documents BMad Method,bmad-domain-research,Domain Research,DR,Industry domain deep dive subject matter expertise and terminology.,,,1-analysis,,,false,planning_artifacts|project_knowledge,research documents
BMad Method,bmad-technical-research,Technical Research,TR,Technical feasibility architecture options and implementation approaches.,,1-analysis,,,false,"planning_artifacts|project_knowledge",research documents BMad Method,bmad-technical-research,Technical Research,TR,Technical feasibility architecture options and implementation approaches.,,,1-analysis,,,false,planning_artifacts|project_knowledge,research documents
BMad Method,bmad-product-brief,Create Brief,CB,An expert guided experience to nail down your product idea in a brief. a gentler approach than PRFAQ when you are already sure of your concept and nothing will sway you.,,-A,1-analysis,,,false,planning_artifacts,product brief BMad Method,bmad-product-brief,Create Brief,CB,An expert guided experience to nail down your product idea in a brief. a gentler approach than PRFAQ when you are already sure of your concept and nothing will sway you.,,-A,1-analysis,,,false,planning_artifacts,product brief
BMad Method,bmad-prfaq,PRFAQ Challenge,WB,Working Backwards guided experience to forge and stress-test your product concept to ensure you have a great product that users will love and need through the PRFAQ gauntlet to determine feasibility and alignment with user needs. alternative to product brief.,,-H,1-analysis,,,false,planning_artifacts,prfaq document BMad Method,bmad-prfaq,PRFAQ Challenge,WB,Working Backwards guided experience to forge and stress-test your product concept to ensure you have a great product that users will love and need through the PRFAQ gauntlet to determine feasibility and alignment with user needs. alternative to product brief.,,-H,1-analysis,,,false,planning_artifacts,prfaq document
BMad Method,bmad-create-prd,Create PRD,CP,Expert led facilitation to produce your Product Requirements Document.,,2-planning,,,true,planning_artifacts,prd BMad Method,bmad-create-prd,Create PRD,CP,Expert led facilitation to produce your Product Requirements Document.,,,2-planning,,,true,planning_artifacts,prd
BMad Method,bmad-validate-prd,Validate PRD,VP,,,[path],2-planning,bmad-create-prd,,false,planning_artifacts,prd validation report BMad Method,bmad-validate-prd,Validate PRD,VP,,,[path],2-planning,bmad-create-prd,,false,planning_artifacts,prd validation report
BMad Method,bmad-edit-prd,Edit PRD,EP,,,[path],2-planning,bmad-validate-prd,,false,planning_artifacts,updated prd BMad Method,bmad-edit-prd,Edit PRD,EP,,,[path],2-planning,bmad-validate-prd,,false,planning_artifacts,updated prd
BMad Method,bmad-create-ux-design,Create UX,CU,"Guidance through realizing the plan for your UX, strongly recommended if a UI is a primary piece of the proposed project.",,2-planning,bmad-create-prd,,false,planning_artifacts,ux design BMad Method,bmad-create-ux-design,Create UX,CU,"Guidance through realizing the plan for your UX, strongly recommended if a UI is a primary piece of the proposed project.",,,2-planning,bmad-create-prd,,false,planning_artifacts,ux design
BMad Method,bmad-create-architecture,Create Architecture,CA,Guided workflow to document technical decisions.,,3-solutioning,,,true,planning_artifacts,architecture BMad Method,bmad-create-architecture,Create Architecture,CA,Guided workflow to document technical decisions.,,,3-solutioning,,,true,planning_artifacts,architecture
BMad Method,bmad-create-epics-and-stories,Create Epics and Stories,CE,,,3-solutioning,bmad-create-architecture,,true,planning_artifacts,epics and stories BMad Method,bmad-create-epics-and-stories,Create Epics and Stories,CE,,,,3-solutioning,bmad-create-architecture,,true,planning_artifacts,epics and stories
BMad Method,bmad-check-implementation-readiness,Check Implementation Readiness,IR,Ensure PRD UX Architecture and Epics Stories are aligned.,,3-solutioning,bmad-create-epics-and-stories,,true,planning_artifacts,readiness report BMad Method,bmad-check-implementation-readiness,Check Implementation Readiness,IR,Ensure PRD UX Architecture and Epics Stories are aligned.,,,3-solutioning,bmad-create-epics-and-stories,,true,planning_artifacts,readiness report
BMad Method,bmad-sprint-planning,Sprint Planning,SP,Kicks off implementation by producing a plan the implementation agents will follow in sequence for every story.,,4-implementation,,,true,implementation_artifacts,sprint status BMad Method,bmad-sprint-planning,Sprint Planning,SP,Kicks off implementation by producing a plan the implementation agents will follow in sequence for every story.,,,4-implementation,,,true,implementation_artifacts,sprint status
BMad Method,bmad-sprint-status,Sprint Status,SS,Anytime: Summarize sprint status and route to next workflow.,,4-implementation,bmad-sprint-planning,,false,, BMad Method,bmad-sprint-status,Sprint Status,SS,Anytime: Summarize sprint status and route to next workflow.,,,4-implementation,bmad-sprint-planning,,false,,
BMad Method,bmad-create-story,Create Story,CS,"Story cycle start: Prepare first found story in the sprint plan that is next or a specific epic/story designation.",create,,4-implementation,bmad-sprint-planning,bmad-create-story:validate,true,implementation_artifacts,story BMad Method,bmad-create-story,Create Story,CS,Story cycle start: Prepare first found story in the sprint plan that is next or a specific epic/story designation.,create,,4-implementation,bmad-sprint-planning,bmad-create-story:validate,true,implementation_artifacts,story
BMad Method,bmad-create-story,Validate Story,VS,Validates story readiness and completeness before development work begins.,validate,,4-implementation,bmad-create-story:create,bmad-dev-story,false,implementation_artifacts,story validation report BMad Method,bmad-create-story,Validate Story,VS,Validates story readiness and completeness before development work begins.,validate,,4-implementation,bmad-create-story:create,bmad-dev-story,false,implementation_artifacts,story validation report
BMad Method,bmad-dev-story,Dev Story,DS,Story cycle: Execute story implementation tasks and tests then CR then back to DS if fixes needed.,,4-implementation,bmad-create-story:validate,,true,, BMad Method,bmad-dev-story,Dev Story,DS,Story cycle: Execute story implementation tasks and tests then CR then back to DS if fixes needed.,,,4-implementation,bmad-create-story:validate,,true,,
BMad Method,bmad-code-review,Code Review,CR,Story cycle: If issues back to DS if approved then next CS or ER if epic complete.,,4-implementation,bmad-dev-story,,false,, BMad Method,bmad-code-review,Code Review,CR,Story cycle: If issues back to DS if approved then next CS or ER if epic complete.,,,4-implementation,bmad-dev-story,,false,,
BMad Method,bmad-checkpoint-preview,Checkpoint,CK,Guided walkthrough of a change from purpose and context into details. Use for human review of commits branches or PRs.,,4-implementation,,,false,, BMad Method,bmad-checkpoint-preview,Checkpoint,CK,Guided walkthrough of a change from purpose and context into details. Use for human review of commits branches or PRs.,,,4-implementation,,,false,,
BMad Method,bmad-qa-generate-e2e-tests,QA Automation Test,QA,Generate automated API and E2E tests for implemented code. NOT for code review or story validation — use CR for that.,,4-implementation,bmad-dev-story,,false,implementation_artifacts,test suite BMad Method,bmad-qa-generate-e2e-tests,QA Automation Test,QA,Generate automated API and E2E tests for implemented code. NOT for code review or story validation — use CR for that.,,,4-implementation,bmad-dev-story,,false,implementation_artifacts,test suite
BMad Method,bmad-retrospective,Retrospective,ER,Optional at epic end: Review completed work lessons learned and next epic or if major issues consider CC.,,4-implementation,bmad-code-review,,false,implementation_artifacts,retrospective BMad Method,bmad-retrospective,Retrospective,ER,Optional at epic end: Review completed work lessons learned and next epic or if major issues consider CC.,,,4-implementation,bmad-code-review,,false,implementation_artifacts,retrospective

Can't render this file because it has a wrong number of fields in line 3.

View File

@ -5,15 +5,11 @@ default_selected: true # This module will be selected by default for new install
# Variables from Core Config inserted: # Variables from Core Config inserted:
## user_name ## user_name
## project_name
## communication_language ## communication_language
## document_output_language ## document_output_language
## output_folder ## output_folder
project_name:
prompt: "What is your project called?"
default: "{directory_name}"
result: "{value}"
user_skill_level: user_skill_level:
prompt: prompt:
- "What is your development experience level?" - "What is your development experience level?"

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

@ -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.

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,13 +1,13 @@
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,after,before,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,,
Core,bmad-help,BMad Help,BH,,,anytime,,,false,, Core,bmad-help,BMad Help,BH,,,,anytime,,,false,,
Core,bmad-index-docs,Index Docs,ID,Use when LLM needs to understand available docs without loading everything.,,anytime,,,false,, Core,bmad-index-docs,Index Docs,ID,Use when LLM needs to understand available docs without loading everything.,,,anytime,,,false,,
Core,bmad-shard-doc,Shard Document,SD,Use when doc becomes too large (>500 lines) to manage effectively.,[path],anytime,,,false,, Core,bmad-shard-doc,Shard Document,SD,Use when doc becomes too large (>500 lines) to manage effectively.,,[path],anytime,,,false,,
Core,bmad-editorial-review-prose,Editorial Review - Prose,EP,Use after drafting to polish written content.,[path],anytime,,,false,report located with target document,three-column markdown table with suggested fixes Core,bmad-editorial-review-prose,Editorial Review - Prose,EP,Use after drafting to polish written content.,,[path],anytime,,,false,report located with target document,three-column markdown table with suggested fixes
Core,bmad-editorial-review-structure,Editorial Review - Structure,ES,Use when doc produced from multiple subprocesses or needs structural improvement.,[path],anytime,,,false,report located with target document, Core,bmad-editorial-review-structure,Editorial Review - Structure,ES,Use when doc produced from multiple subprocesses or needs structural improvement.,,[path],anytime,,,false,report located with target document,
Core,bmad-review-adversarial-general,Adversarial Review,AR,"Use for quality assurance or before finalizing deliverables. Code Review in other modules runs this automatically, but also useful for document reviews.",[path],anytime,,,false,, Core,bmad-review-adversarial-general,Adversarial Review,AR,"Use for quality assurance or before finalizing deliverables. Code Review in other modules runs this automatically, but also useful for document reviews.",,[path],anytime,,,false,,
Core,bmad-review-edge-case-hunter,Edge Case Hunter Review,ECH,Use alongside adversarial review for orthogonal coverage — method-driven not attitude-driven.,[path],anytime,,,false,, Core,bmad-review-edge-case-hunter,Edge Case Hunter Review,ECH,Use alongside adversarial review for orthogonal coverage — method-driven not attitude-driven.,,[path],anytime,,,false,,
Core,bmad-distillator,Distillator,DG,Use when you need token-efficient distillates that preserve all information for downstream LLM consumption.,[path],anytime,,,false,adjacent to source document or specified output_path,distillate markdown file(s) Core,bmad-distillator,Distillator,DG,Use when you need token-efficient distillates that preserve all information for downstream LLM consumption.,,[path],anytime,,,false,adjacent to source document or specified output_path,distillate markdown file(s)
Core,bmad-customize,BMad Customize,BC,"Use when you want to change how an agent or workflow behaves — add persistent facts, swap templates, insert activation hooks, or customize menus. Scans what's customizable, picks the right scope (agent vs workflow), writes the override to _bmad/custom/, and verifies the merge. No TOML hand-authoring required.",,anytime,,,false,{project-root}/_bmad/custom,TOML override files Core,bmad-customize,BMad Customize,BC,"Use when you want to change how an agent or workflow behaves — add persistent facts, swap templates, insert activation hooks, or customize menus. Scans what's customizable, picks the right scope (agent vs workflow), writes the override to _bmad/custom/, and verifies the merge. No TOML hand-authoring required.",,,anytime,,,false,{project-root}/_bmad/custom,TOML override files

Can't render this file because it has a wrong number of fields in line 3.

View File

@ -11,6 +11,11 @@ user_name:
default: "BMad" default: "BMad"
result: "{value}" result: "{value}"
project_name:
prompt: "What is your project called?"
default: "{directory_name}"
result: "{value}"
communication_language: communication_language:
prompt: "What language should agents use when chatting with you?" prompt: "What language should agents use when chatting with you?"
scope: user scope: user

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

@ -1813,12 +1813,12 @@ async function runTests() {
const moduleConfigs = { const moduleConfigs = {
core: { core: {
user_name: 'TestUser', user_name: 'TestUser',
project_name: 'demo-project',
communication_language: 'Spanish', communication_language: 'Spanish',
document_output_language: 'English', document_output_language: 'English',
output_folder: '_bmad-output', output_folder: '_bmad-output',
}, },
bmm: { bmm: {
project_name: 'demo-project',
user_skill_level: 'expert', user_skill_level: 'expert',
planning_artifacts: '{project-root}/_bmad-output/planning-artifacts', planning_artifacts: '{project-root}/_bmad-output/planning-artifacts',
implementation_artifacts: '{project-root}/_bmad-output/implementation-artifacts', implementation_artifacts: '{project-root}/_bmad-output/implementation-artifacts',
@ -1826,7 +1826,10 @@ async function runTests() {
// Spread-from-core pollution: legacy per-module config.yaml merges // Spread-from-core pollution: legacy per-module config.yaml merges
// core values into every module; writeCentralConfig must strip these // core values into every module; writeCentralConfig must strip these
// from [modules.bmm] so core values only live in [core]. // from [modules.bmm] so core values only live in [core].
// project_name is now a core key (#2279), so it joins user_name etc.
// as a spread-from-core key that must be stripped.
user_name: 'TestUser', user_name: 'TestUser',
project_name: 'stale-bmm-copy',
communication_language: 'Spanish', communication_language: 'Spanish',
document_output_language: 'English', document_output_language: 'English',
output_folder: '_bmad-output', output_folder: '_bmad-output',
@ -1874,6 +1877,7 @@ async function runTests() {
assert(teamContent.includes('[core]'), 'config.toml has [core] section'); assert(teamContent.includes('[core]'), 'config.toml has [core] section');
assert(teamContent.includes('document_output_language = "English"'), 'Team-scope core key lands in config.toml'); assert(teamContent.includes('document_output_language = "English"'), 'Team-scope core key lands in config.toml');
assert(teamContent.includes('output_folder = "_bmad-output"'), 'Team-scope output_folder lands in config.toml'); assert(teamContent.includes('output_folder = "_bmad-output"'), 'Team-scope output_folder lands in config.toml');
assert(teamContent.includes('project_name = "demo-project"'), 'project_name lands in [core] (core key as of #2279)');
assert(!teamContent.includes('user_name'), 'user_name (scope: user) is absent from config.toml'); assert(!teamContent.includes('user_name'), 'user_name (scope: user) is absent from config.toml');
assert(!teamContent.includes('communication_language'), 'communication_language (scope: user) is absent from config.toml'); assert(!teamContent.includes('communication_language'), 'communication_language (scope: user) is absent from config.toml');
@ -1888,7 +1892,9 @@ async function runTests() {
assert(bmmTeamMatch !== null, 'config.toml has [modules.bmm] section'); assert(bmmTeamMatch !== null, 'config.toml has [modules.bmm] section');
if (bmmTeamMatch) { if (bmmTeamMatch) {
const bmmTeamBlock = bmmTeamMatch[0]; const bmmTeamBlock = bmmTeamMatch[0];
assert(bmmTeamBlock.includes('project_name = "demo-project"'), 'bmm team-scope key lands under [modules.bmm]'); assert(bmmTeamBlock.includes('planning_artifacts'), 'bmm-owned team-scope key (planning_artifacts) lands under [modules.bmm]');
assert(!bmmTeamBlock.includes('project_name'), 'project_name stripped from [modules.bmm] (now a core key, #2279)');
assert(!bmmTeamBlock.includes('stale-bmm-copy'), 'stale bmm-copy of project_name not leaked into config.toml');
assert(!bmmTeamBlock.includes('user_name'), 'user_name stripped from [modules.bmm] (core-key pollution)'); assert(!bmmTeamBlock.includes('user_name'), 'user_name stripped from [modules.bmm] (core-key pollution)');
assert(!bmmTeamBlock.includes('communication_language'), 'communication_language stripped from [modules.bmm]'); assert(!bmmTeamBlock.includes('communication_language'), 'communication_language stripped from [modules.bmm]');
assert(!bmmTeamBlock.includes('user_skill_level'), 'user_skill_level (scope: user) absent from [modules.bmm] in config.toml'); assert(!bmmTeamBlock.includes('user_skill_level'), 'user_skill_level (scope: user) absent from [modules.bmm] in config.toml');
@ -2773,6 +2779,210 @@ async function runTests() {
console.log(''); console.log('');
// ============================================================
// Test Suite 42: --tools flag parsing & validation (#2326)
// ============================================================
console.log(`${colors.yellow}Test Suite 42: --tools flag parsing & validation${colors.reset}\n`);
try {
const { UI } = require('../tools/installer/ui');
const ui = new UI();
const known = new Set(['claude-code', 'cursor', 'windsurf']);
assert(
JSON.stringify(ui._parseToolsFlag('claude-code', known)) === JSON.stringify(['claude-code']),
'parseToolsFlag returns single ID',
);
assert(
JSON.stringify(ui._parseToolsFlag('claude-code,cursor', known)) === JSON.stringify(['claude-code', 'cursor']),
'parseToolsFlag returns multiple IDs',
);
assert(
JSON.stringify(ui._parseToolsFlag(' claude-code , cursor ', known)) === JSON.stringify(['claude-code', 'cursor']),
'parseToolsFlag trims whitespace',
);
let emptyErr;
try {
ui._parseToolsFlag('', known);
} catch (error) {
emptyErr = error;
}
assert(
emptyErr && emptyErr.expected === true && /empty/i.test(emptyErr.message),
'parseToolsFlag rejects empty string with expected=true',
);
let commasOnlyErr;
try {
ui._parseToolsFlag(' , , ', known);
} catch (error) {
commasOnlyErr = error;
}
assert(commasOnlyErr && commasOnlyErr.expected === true, 'parseToolsFlag rejects whitespace/comma-only input');
let noneErr;
try {
ui._parseToolsFlag('none', known);
} catch (error) {
noneErr = error;
}
assert(noneErr && noneErr.expected === true && /Unknown tool ID/.test(noneErr.message), 'parseToolsFlag rejects "none" as unknown ID');
let typoErr;
try {
ui._parseToolsFlag('claude-code,claude-cdoe', known);
} catch (error) {
typoErr = error;
}
const typoHeader = typoErr ? typoErr.message.split('\n')[0] : '';
assert(
typoErr && typoErr.expected === true && /claude-cdoe/.test(typoHeader) && !/claude-code/.test(typoHeader),
'parseToolsFlag reports only the unknown ID in error header (valid ones not listed as unknown)',
);
// --list-tools and --tools validation must agree on what counts as a valid ID.
const { formatPlatformList } = require('../tools/installer/ide/platform-codes');
const { IdeManager } = require('../tools/installer/ide/manager');
const ideManager42 = new IdeManager();
await ideManager42.ensureInitialized();
const validIds = new Set(ideManager42.getAvailableIdes().map((i) => i.value));
const listed = await formatPlatformList();
// Each entry line starts with ' *' (preferred) or ' ' (other), followed by the ID, then padding.
const entryLines = listed.split('\n').filter((l) => /^( \*| {2})[a-z]/.test(l));
const listedIds = entryLines.map((l) => l.trim().replace(/^\*/, '').split(/\s+/)[0]);
const missingFromList = [...validIds].filter((id) => !listedIds.includes(id));
const extraInList = listedIds.filter((id) => !validIds.has(id));
assert(
missingFromList.length === 0 && extraInList.length === 0,
'--list-tools output matches the IDs that --tools accepts',
`Missing from list: ${missingFromList.join(',') || '(none)'}; Extra in list: ${extraInList.join(',') || '(none)'}`,
);
} catch (error) {
console.log(`${colors.red}Test Suite 42 setup failed: ${error.message}${colors.reset}`);
console.log(error.stack);
failed++;
}
console.log('');
// ============================================================
// Test Suite 43: project_name promoted to core + hoist migration (#2279)
// ============================================================
console.log(`${colors.yellow}Test Suite 43: project_name in core + hoist migration${colors.reset}\n`);
try {
const yamlLib = require('yaml');
const coreSchemaPath = path.join(__dirname, '..', 'src', 'core-skills', 'module.yaml');
const bmmSchemaPath = path.join(__dirname, '..', 'src', 'bmm-skills', 'module.yaml');
const coreSchema = yamlLib.parse(await fs.readFile(coreSchemaPath, 'utf8'));
const bmmSchema = yamlLib.parse(await fs.readFile(bmmSchemaPath, 'utf8'));
assert(
coreSchema.project_name && coreSchema.project_name.prompt && coreSchema.project_name.default === '{directory_name}',
'core/module.yaml declares project_name with {directory_name} default',
);
assert(coreSchema.project_name.scope === undefined, 'project_name has no user scope (project-scoped, not user-scoped)');
assert(bmmSchema.project_name === undefined, 'bmm/module.yaml no longer declares project_name (now inherited from core)');
// Set up a mock existing install: bmm directory has project_name (legacy),
// core has user_name but not project_name. After hoist, project_name should
// move to core, leaving bmm with only its own keys.
const fixtureRoot43 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-fixture-43-'));
const bmadDir43 = path.join(fixtureRoot43, '_bmad');
await fs.ensureDir(path.join(bmadDir43, '_config'));
await fs.writeFile(path.join(bmadDir43, '_config', 'manifest.yaml'), 'modules: []\n', 'utf8');
await fs.ensureDir(path.join(bmadDir43, 'core'));
await fs.ensureDir(path.join(bmadDir43, 'bmm'));
await fs.writeFile(path.join(bmadDir43, 'core', 'config.yaml'), 'user_name: alice\n', 'utf8');
await fs.writeFile(
path.join(bmadDir43, 'bmm', 'config.yaml'),
'project_name: legacy-from-bmm\nuser_skill_level: intermediate\n',
'utf8',
);
const officialModules43 = new OfficialModules();
await officialModules43.loadExistingConfig(fixtureRoot43);
assert(
officialModules43.existingConfig.core?.project_name === 'legacy-from-bmm',
'loadExistingConfig hoists bmm.project_name to core on existing-install upgrade',
);
assert(
!('project_name' in (officialModules43.existingConfig.bmm || {})),
'loadExistingConfig removes project_name from bmm after hoisting',
);
assert(
officialModules43.existingConfig.bmm?.user_skill_level === 'intermediate',
'loadExistingConfig leaves non-core bmm keys (user_skill_level) untouched',
);
assert(officialModules43.existingConfig.core?.user_name === 'alice', 'loadExistingConfig preserves pre-existing core values');
// Precedence: if core already has the key, hoist must NOT overwrite it.
const fixtureRoot43b = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-fixture-43b-'));
const bmadDir43b = path.join(fixtureRoot43b, '_bmad');
await fs.ensureDir(path.join(bmadDir43b, '_config'));
await fs.writeFile(path.join(bmadDir43b, '_config', 'manifest.yaml'), 'modules: []\n', 'utf8');
await fs.ensureDir(path.join(bmadDir43b, 'core'));
await fs.ensureDir(path.join(bmadDir43b, 'bmm'));
await fs.writeFile(path.join(bmadDir43b, 'core', 'config.yaml'), 'project_name: from-core\n', 'utf8');
await fs.writeFile(path.join(bmadDir43b, 'bmm', 'config.yaml'), 'project_name: stale-from-bmm\n', 'utf8');
const officialModules43b = new OfficialModules();
await officialModules43b.loadExistingConfig(fixtureRoot43b);
assert(officialModules43b.existingConfig.core?.project_name === 'from-core', 'hoist does not overwrite an existing core value');
assert(
!('project_name' in (officialModules43b.existingConfig.bmm || {})),
'hoist still strips the duplicate from bmm so writeCentralConfig partition stays clean',
);
// Malformed config.yaml (parses to a scalar) must not crash loadExistingConfig
// or the hoist pass — they should treat it as "no config for that module"
// and continue. Regression for augment review on PR #2348.
const fixtureRoot43c = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-fixture-43c-'));
const bmadDir43c = path.join(fixtureRoot43c, '_bmad');
await fs.ensureDir(path.join(bmadDir43c, '_config'));
await fs.writeFile(path.join(bmadDir43c, '_config', 'manifest.yaml'), 'modules: []\n', 'utf8');
await fs.ensureDir(path.join(bmadDir43c, 'core'));
await fs.ensureDir(path.join(bmadDir43c, 'bmm'));
// Scalar YAML — yaml.parse returns the literal 42 (truthy non-object).
// Pre-fix this crashed _hoistCoreKeysFromLegacyModuleConfigs with
// "Cannot use 'in' operator to search for 'project_name' in 42".
await fs.writeFile(path.join(bmadDir43c, 'core', 'config.yaml'), '42\n', 'utf8');
await fs.writeFile(path.join(bmadDir43c, 'bmm', 'config.yaml'), 'project_name: rescued\n', 'utf8');
const officialModules43c = new OfficialModules();
let crashErr;
try {
await officialModules43c.loadExistingConfig(fixtureRoot43c);
} catch (error) {
crashErr = error;
}
assert(!crashErr, 'loadExistingConfig does not crash on a scalar core/config.yaml', crashErr?.stack);
assert(
officialModules43c.existingConfig.core?.project_name === 'rescued',
'scalar core gets replaced with {} and bmm.project_name still hoists in',
);
await fs.remove(fixtureRoot43).catch(() => {});
await fs.remove(fixtureRoot43b).catch(() => {});
await fs.remove(fixtureRoot43c).catch(() => {});
} catch (error) {
console.log(`${colors.red}Test Suite 43 setup failed: ${error.message}${colors.reset}`);
console.log(error.stack);
failed++;
}
console.log('');
// ============================================================ // ============================================================
// Summary // Summary
// ============================================================ // ============================================================

View File

@ -15,8 +15,9 @@ module.exports = {
['--modules <modules>', 'Comma-separated list of module IDs to install (e.g., "bmm,bmb")'], ['--modules <modules>', 'Comma-separated list of module IDs to install (e.g., "bmm,bmb")'],
[ [
'--tools <tools>', '--tools <tools>',
'Comma-separated list of tool/IDE IDs to configure (e.g., "claude-code,cursor"). Use "none" to skip tool configuration.', 'Comma-separated list of tool/IDE IDs to configure (e.g., "claude-code,cursor"). Required for fresh non-interactive (--yes) installs. Run with --list-tools to see all valid IDs.',
], ],
['--list-tools', 'Print all supported tool/IDE IDs (with target directories) and exit.'],
['--action <type>', 'Action type for existing installations: install, update, or quick-update'], ['--action <type>', 'Action type for existing installations: install, update, or quick-update'],
['--user-name <name>', 'Name for agents to use (default: system username)'], ['--user-name <name>', 'Name for agents to use (default: system username)'],
['--communication-language <lang>', 'Language for agent communication (default: English)'], ['--communication-language <lang>', 'Language for agent communication (default: English)'],
@ -40,6 +41,12 @@ module.exports = {
], ],
action: async (options) => { action: async (options) => {
try { try {
if (options.listTools) {
const { formatPlatformList } = require('../ide/platform-codes');
process.stdout.write((await formatPlatformList()) + '\n');
process.exit(0);
}
// Set debug flag as environment variable for all components // Set debug flag as environment variable for all components
if (options.debug) { if (options.debug) {
process.env.BMAD_DEBUG_MANIFEST = 'true'; process.env.BMAD_DEBUG_MANIFEST = 'true';
@ -81,7 +88,7 @@ module.exports = {
} else { } else {
await prompts.log.error(`Installation failed: ${error.message}`); await prompts.log.error(`Installation failed: ${error.message}`);
} }
if (error.stack) { if (error.stack && !error.expected) {
await prompts.log.message(error.stack); await prompts.log.message(error.stack);
} }
} catch { } catch {

View File

@ -923,29 +923,15 @@ class Installer {
/** /**
* Merge all module-help.csv files into a single bmad-help.csv. * Merge all module-help.csv files into a single bmad-help.csv.
* Scans all installed modules for module-help.csv and merges them. * Scans all installed modules for module-help.csv and merges them.
* Enriches agent info from the in-memory agent list produced by ManifestGenerator. * Output preserves the source schema verbatim see schema below.
* Output is written to _bmad/_config/bmad-help.csv.
* @param {string} bmadDir - BMAD installation directory * @param {string} bmadDir - BMAD installation directory
* @param {Array<Object>} agentEntries - Agents collected from module.yaml (code, name, title, icon, module, ...) * @param {Array<Object>} _agentEntries - Unused; retained for call-site compatibility
*/ */
async mergeModuleHelpCatalogs(bmadDir, agentEntries = []) { async mergeModuleHelpCatalogs(bmadDir, _agentEntries = []) {
const allRows = []; const allRows = [];
const headerRow = const headerRow = 'module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs';
'module,phase,name,code,sequence,workflow-file,command,required,agent-name,agent-command,agent-display-name,agent-title,options,description,output-location,outputs'; const COLUMN_COUNT = 13;
const PHASE_INDEX = 7;
// Build agent lookup from the in-memory list (agent code → command + display fields).
const agentInfo = new Map();
for (const agent of agentEntries) {
if (!agent || !agent.code) continue;
const agentCommand = agent.module ? `bmad:${agent.module}:agent:${agent.code}` : `bmad:agent:${agent.code}`;
const displayName = agent.name || agent.code;
const titleCombined = agent.icon && agent.title ? `${agent.icon} ${agent.title}` : agent.title || agent.code;
agentInfo.set(agent.code, {
command: agentCommand,
displayName,
title: titleCombined,
});
}
// Get all installed module directories // Get all installed module directories
const entries = await fs.readdir(bmadDir, { withFileTypes: true }); const entries = await fs.readdir(bmadDir, { withFileTypes: true });
@ -984,64 +970,19 @@ class Installer {
// Parse the line - handle quoted fields with commas // Parse the line - handle quoted fields with commas
const columns = this.parseCSVLine(line); const columns = this.parseCSVLine(line);
if (columns.length >= 12) { if (columns.length < COLUMN_COUNT - 1) continue;
// Map old schema to new schema
// Old: module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs
// New: module,phase,name,code,sequence,workflow-file,command,required,agent-name,agent-command,agent-display-name,agent-title,options,description,output-location,outputs
const [ // Pad short rows; truncate over-long rows
module, const padded = columns.slice(0, COLUMN_COUNT);
phase, while (padded.length < COLUMN_COUNT) padded.push('');
name,
code,
sequence,
workflowFile,
command,
required,
agentName,
options,
description,
outputLocation,
outputs,
] = columns;
// Pass through _meta rows as-is (module metadata, not a skill) // If module column is empty, fill with this module's name
if (phase === '_meta') { // (core stays empty so its rows render as universal tools)
const finalModule = (!module || module.trim() === '') && moduleName !== 'core' ? moduleName : module || ''; if ((!padded[0] || padded[0].trim() === '') && moduleName !== 'core') {
const metaRow = [finalModule, '_meta', '', '', '', '', '', 'false', '', '', '', '', '', '', outputLocation || '', '']; padded[0] = moduleName;
allRows.push(metaRow.map((c) => this.escapeCSVField(c)).join(','));
continue;
} }
// If module column is empty, set it to this module's name (except for core which stays empty for universal tools) allRows.push(padded.map((c) => this.escapeCSVField(c)).join(','));
const finalModule = (!module || module.trim() === '') && moduleName !== 'core' ? moduleName : module || '';
// Lookup agent info
const cleanAgentName = agentName ? agentName.trim() : '';
const agentData = agentInfo.get(cleanAgentName) || { command: '', displayName: '', title: '' };
// Build new row with agent info
const newRow = [
finalModule,
phase || '',
name || '',
code || '',
sequence || '',
workflowFile || '',
command || '',
required || 'false',
cleanAgentName,
agentData.command,
agentData.displayName,
agentData.title,
options || '',
description || '',
outputLocation || '',
outputs || '',
];
allRows.push(newRow.map((c) => this.escapeCSVField(c)).join(','));
}
} }
if (process.env.BMAD_VERBOSE_INSTALL === 'true') { if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
@ -1053,44 +994,34 @@ class Installer {
} }
} }
// Sort by module, then phase, then sequence // Sort by module, then phase. Stable sort preserves authored order within a phase.
allRows.sort((a, b) => { const decorated = allRows.map((row, index) => ({ row, index, cols: this.parseCSVLine(row) }));
const colsA = this.parseCSVLine(a); decorated.sort((a, b) => {
const colsB = this.parseCSVLine(b); const moduleA = (a.cols[0] || '').toLowerCase();
const moduleB = (b.cols[0] || '').toLowerCase();
if (moduleA !== moduleB) return moduleA.localeCompare(moduleB);
// Module comparison (empty module/universal tools come first) const phaseA = a.cols[PHASE_INDEX] || '';
const moduleA = (colsA[0] || '').toLowerCase(); const phaseB = b.cols[PHASE_INDEX] || '';
const moduleB = (colsB[0] || '').toLowerCase(); if (phaseA !== phaseB) return phaseA.localeCompare(phaseB);
if (moduleA !== moduleB) {
return moduleA.localeCompare(moduleB);
}
// Phase comparison return a.index - b.index;
const phaseA = colsA[1] || '';
const phaseB = colsB[1] || '';
if (phaseA !== phaseB) {
return phaseA.localeCompare(phaseB);
}
// Sequence comparison
const seqA = parseInt(colsA[4] || '0', 10);
const seqB = parseInt(colsB[4] || '0', 10);
return seqA - seqB;
}); });
const sortedRows = decorated.map((d) => d.row);
// Write merged catalog // Write merged catalog
const outputDir = path.join(bmadDir, '_config'); const outputDir = path.join(bmadDir, '_config');
await fs.ensureDir(outputDir); await fs.ensureDir(outputDir);
const outputPath = path.join(outputDir, 'bmad-help.csv'); const outputPath = path.join(outputDir, 'bmad-help.csv');
const mergedContent = [headerRow, ...allRows].join('\n'); const mergedContent = [headerRow, ...sortedRows].join('\n');
await fs.writeFile(outputPath, mergedContent, 'utf8'); await fs.writeFile(outputPath, mergedContent, 'utf8');
// Track the installed file // Track the installed file
this.installedFiles.add(outputPath); this.installedFiles.add(outputPath);
if (process.env.BMAD_VERBOSE_INSTALL === 'true') { if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
await prompts.log.message(` Generated bmad-help.csv: ${allRows.length} workflows`); await prompts.log.message(` Generated bmad-help.csv: ${sortedRows.length} workflows`);
} }
} }

View File

@ -31,7 +31,50 @@ function clearCache() {
_cachedPlatformCodes = null; _cachedPlatformCodes = null;
} }
/**
* Format the installable platform list for human-readable output (used by --list-tools).
* Sourced from IdeManager so this view matches what --tools accepts at install time
* (suspended platforms excluded).
* @returns {Promise<string>} Formatted multi-line string with id, name, target_dir, preferred flag.
*/
async function formatPlatformList() {
const { IdeManager } = require('./manager');
const ideManager = new IdeManager();
await ideManager.ensureInitialized();
const entries = ideManager.getAvailableIdes().map((ide) => {
const handler = ideManager.handlers.get(ide.value);
return {
id: ide.value,
name: ide.name,
targetDir: handler?.installerConfig?.target_dir || '',
preferred: ide.preferred,
};
});
const idWidth = Math.max(...entries.map((e) => e.id.length), 'ID'.length);
const nameWidth = Math.max(...entries.map((e) => e.name.length), 'Name'.length);
const pad = (s, w) => s + ' '.repeat(Math.max(0, w - s.length));
const lines = [
`Supported tool IDs (pass via --tools <id>[,<id>...]):`,
'',
` ${pad('ID', idWidth)} ${pad('Name', nameWidth)} Target dir`,
` ${pad('-'.repeat(idWidth), idWidth)} ${pad('-'.repeat(nameWidth), nameWidth)} ${'-'.repeat(10)}`,
];
for (const e of entries) {
const star = e.preferred ? ' *' : ' ';
lines.push(`${star}${pad(e.id, idWidth)} ${pad(e.name, nameWidth)} ${e.targetDir}`);
}
lines.push('', '* = recommended / preferred', '', 'Example: bmad-method install --modules bmm --tools claude-code');
return lines.join('\n');
}
module.exports = { module.exports = {
loadPlatformCodes, loadPlatformCodes,
clearCache, clearCache,
formatPlatformList,
}; };

View File

@ -903,7 +903,10 @@ class OfficialModules {
try { try {
const content = await fs.readFile(moduleConfigPath, 'utf8'); const content = await fs.readFile(moduleConfigPath, 'utf8');
const moduleConfig = yaml.parse(content); const moduleConfig = yaml.parse(content);
if (moduleConfig) { // Only keep plain object parses. A corrupt config.yaml that parses
// to a scalar or array would crash later code that does `key in cfg`
// / `Object.keys(cfg)`; treat it the same as a parse error.
if (moduleConfig && typeof moduleConfig === 'object' && !Array.isArray(moduleConfig)) {
this._existingConfig[entry.name] = moduleConfig; this._existingConfig[entry.name] = moduleConfig;
foundAny = true; foundAny = true;
} }
@ -914,9 +917,58 @@ class OfficialModules {
} }
} }
if (foundAny) {
await this._hoistCoreKeysFromLegacyModuleConfigs();
}
return foundAny; return foundAny;
} }
/**
* Migrate prior answers when a key has moved from a non-core module to core
* (e.g. project_name moving from bmm to core in #2279). Without this, the
* partition logic in writeCentralConfig drops the value from the bmm bucket
* (because it's now a core key) without re-homing it under [core], so the
* user's prior answer silently disappears on the next install/quick-update.
*/
async _hoistCoreKeysFromLegacyModuleConfigs() {
const coreSchemaPath = path.join(getSourcePath(), 'core-skills', 'module.yaml');
if (!(await fs.pathExists(coreSchemaPath))) return;
let coreSchema;
try {
coreSchema = yaml.parse(await fs.readFile(coreSchemaPath, 'utf8'));
} catch {
return;
}
if (!coreSchema || typeof coreSchema !== 'object') return;
const coreKeys = new Set(
Object.entries(coreSchema)
.filter(([, v]) => v && typeof v === 'object' && 'prompt' in v)
.map(([k]) => k),
);
if (coreKeys.size === 0) return;
// Belt-and-suspenders: loadExistingConfig already filters non-object parses,
// but anyone calling _hoistCoreKeysFromLegacyModuleConfigs in isolation (or
// future code paths populating _existingConfig directly) shouldn't be able
// to crash this with a scalar / array.
const existingCore = this._existingConfig.core;
this._existingConfig.core = existingCore && typeof existingCore === 'object' && !Array.isArray(existingCore) ? existingCore : {};
for (const [moduleName, cfg] of Object.entries(this._existingConfig)) {
if (moduleName === 'core' || !cfg || typeof cfg !== 'object' || Array.isArray(cfg)) continue;
for (const key of Object.keys(cfg)) {
if (!coreKeys.has(key)) continue;
if (!(key in this._existingConfig.core)) {
this._existingConfig.core[key] = cfg[key];
}
delete cfg[key];
}
}
}
/** /**
* Pre-scan module schemas to gather metadata for the configuration gateway prompt. * Pre-scan module schemas to gather metadata for the configuration gateway prompt.
* Returns info about which modules have configurable options. * Returns info about which modules have configurable options.

View File

@ -200,12 +200,15 @@ class UI {
actionType = options.action; actionType = options.action;
await prompts.log.info(`Using action from command-line: ${actionType}`); await prompts.log.info(`Using action from command-line: ${actionType}`);
} else if (options.yes) { } else if (options.yes) {
// Default to quick-update if available, otherwise first available choice // Default to quick-update if available, unless flags that require the
// full update path are present (e.g. --custom-source which re-clones
// modules at a new version — quick-update skips that entirely).
if (choices.length === 0) { if (choices.length === 0) {
throw new Error('No valid actions available for this installation'); throw new Error('No valid actions available for this installation');
} }
const hasQuickUpdate = choices.some((c) => c.value === 'quick-update'); const hasQuickUpdate = choices.some((c) => c.value === 'quick-update');
actionType = hasQuickUpdate ? 'quick-update' : choices[0].value; const needsFullUpdate = !!options.customSource;
actionType = hasQuickUpdate && !needsFullUpdate ? 'quick-update' : (choices.find((c) => c.value === 'update') || choices[0]).value;
await prompts.log.info(`Non-interactive mode (--yes): defaulting to ${actionType}`); await prompts.log.info(`Non-interactive mode (--yes): defaulting to ${actionType}`);
} else { } else {
actionType = await prompts.select({ actionType = await prompts.select({
@ -241,8 +244,11 @@ class UI {
.map((m) => m.trim()) .map((m) => m.trim())
.filter(Boolean); .filter(Boolean);
await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`); await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
} else if (options.customSource) { } else if (options.customSource && !options.yes) {
// Custom source without --modules: start with empty list (core added below) // Custom source without --modules or --yes: start with empty list
// (only custom source modules + core will be installed).
// When --yes is also set, fall through to the --yes branch so all
// installed modules are included alongside the custom source modules.
selectedModules = []; selectedModules = [];
} else if (options.yes) { } else if (options.yes) {
selectedModules = await this.getDefaultModules(installedModuleIds); selectedModules = await this.getDefaultModules(installedModuleIds);
@ -398,6 +404,37 @@ class UI {
* @param {Object} options - Command-line options * @param {Object} options - Command-line options
* @returns {Object} Tool configuration * @returns {Object} Tool configuration
*/ */
_parseToolsFlag(toolsArg, allKnownValues) {
const selectedIdes = toolsArg
.split(',')
.map((t) => t.trim())
.filter(Boolean);
if (selectedIdes.length === 0) {
const err = new Error(
'--tools was passed empty. Provide at least one tool ID (e.g. --tools claude-code) or run with --list-tools to see valid IDs.',
);
err.expected = true;
throw err;
}
const unknown = selectedIdes.filter((id) => !allKnownValues.has(id));
if (unknown.length > 0) {
const err = new Error(
[
`Unknown tool ID${unknown.length === 1 ? '' : 's'}: ${unknown.join(', ')}`,
'',
'Run with --list-tools to see all valid IDs.',
'Common: claude-code, cursor, copilot, windsurf, cline',
].join('\n'),
);
err.expected = true;
throw err;
}
return selectedIdes;
}
async promptToolSelection(projectDir, options = {}) { async promptToolSelection(projectDir, options = {}) {
const { ExistingInstall } = require('./core/existing-install'); const { ExistingInstall } = require('./core/existing-install');
const { Installer } = require('./core/installer'); const { Installer } = require('./core/installer');
@ -432,15 +469,10 @@ class UI {
const allTools = [...preferredIdes, ...otherIdes]; const allTools = [...preferredIdes, ...otherIdes];
// Non-interactive: handle --tools and --yes flags before interactive prompt // Non-interactive: handle --tools and --yes flags before interactive prompt
if (options.tools) { // Use !== undefined so an explicit --tools "" falls through to _parseToolsFlag and
if (options.tools.toLowerCase() === 'none') { // gets a specific "passed empty" error instead of being silently ignored.
await prompts.log.info('Skipping tool configuration (--tools none)'); if (options.tools !== undefined) {
return { ides: [], skipIde: true }; const selectedIdes = this._parseToolsFlag(options.tools, allKnownValues);
}
const selectedIdes = options.tools
.split(',')
.map((t) => t.trim())
.filter(Boolean);
await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`); await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools); await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
return { ides: selectedIdes, skipIde: false }; return { ides: selectedIdes, skipIde: false };
@ -516,21 +548,13 @@ class UI {
let selectedIdes = []; let selectedIdes = [];
// Check if tools are provided via command-line // Check if tools are provided via command-line.
if (options.tools) { // Use !== undefined so an explicit --tools "" still hits _parseToolsFlag's empty-value error.
// Check for explicit "none" value to skip tool installation if (options.tools !== undefined) {
if (options.tools.toLowerCase() === 'none') { selectedIdes = this._parseToolsFlag(options.tools, allKnownValues);
await prompts.log.info('Skipping tool configuration (--tools none)');
return { ides: [], skipIde: true };
} else {
selectedIdes = options.tools
.split(',')
.map((t) => t.trim())
.filter(Boolean);
await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`); await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools); await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
return { ides: selectedIdes, skipIde: false }; return { ides: selectedIdes, skipIde: false };
}
} else if (options.yes) { } else if (options.yes) {
// If --yes flag is set, skip tool prompt and use previously configured tools or empty // If --yes flag is set, skip tool prompt and use previously configured tools or empty
if (configuredIdes.length > 0) { if (configuredIdes.length > 0) {
@ -538,8 +562,18 @@ class UI {
await this.displaySelectedTools(configuredIdes, preferredIdes, allTools); await this.displaySelectedTools(configuredIdes, preferredIdes, allTools);
return { ides: configuredIdes, skipIde: false }; return { ides: configuredIdes, skipIde: false };
} else { } else {
await prompts.log.info('Skipping tool configuration (--yes flag, no previous tools)'); const err = new Error(
return { ides: [], skipIde: true }; [
'--tools is required for non-interactive install (--yes / -y) when no tools are previously configured.',
'',
'Common: claude-code, cursor, copilot, windsurf, cline',
'See all supported tools: bmad-method install --list-tools',
'',
'Example: bmad-method install --modules bmm --tools claude-code -y',
].join('\n'),
);
err.expected = true;
throw err;
} }
} }
@ -724,6 +758,9 @@ class UI {
const defaultUsername = safeUsername.charAt(0).toUpperCase() + safeUsername.slice(1); const defaultUsername = safeUsername.charAt(0).toUpperCase() + safeUsername.slice(1);
configCollector.collectedConfig.core = { configCollector.collectedConfig.core = {
user_name: defaultUsername, user_name: defaultUsername,
// {directory_name} default per src/core-skills/module.yaml — matches what the
// interactive flow resolves via buildQuestion()'s {directory_name} placeholder.
project_name: path.basename(directory),
communication_language: 'English', communication_language: 'English',
document_output_language: 'English', document_output_language: 'English',
output_folder: '_bmad-output', output_folder: '_bmad-output',