Compare commits
11 Commits
e1bb620da1
...
ef991f04d4
| Author | SHA1 | Date |
|---|---|---|
|
|
ef991f04d4 | |
|
|
5416e12882 | |
|
|
c95a349ae8 | |
|
|
3ae4d13ba4 | |
|
|
dfdc18df2f | |
|
|
9f6e83c255 | |
|
|
48a7ec8bff | |
|
|
3da984a491 | |
|
|
815600e4ca | |
|
|
7ee5fa313b | |
|
|
3e89b30b3c |
|
|
@ -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)
|
||||
- **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 |
|
||||
| `--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. |
|
||||
| `--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. |
|
||||
| `--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`) |
|
||||
|
|
@ -165,17 +166,17 @@ npx bmad-method install --yes --modules bmm,bmb --all-next --tools claude-code
|
|||
|
||||
```bash
|
||||
npx bmad-method install --yes --action update \
|
||||
--modules bmm,bmb,gds \
|
||||
--tools none
|
||||
--modules bmm,bmb,gds
|
||||
```
|
||||
|
||||
`--tools` is omitted intentionally — `--action update` reuses the tools configured during the first install.
|
||||
|
||||
**Mix channels — bmb on next, gds on stable:**
|
||||
|
||||
```bash
|
||||
npx bmad-method install --yes --action update \
|
||||
--modules bmm,bmb,cis,gds \
|
||||
--next=bmb \
|
||||
--tools none
|
||||
--next=bmb
|
||||
```
|
||||
|
||||
:::caution[Rate limit on shared IPs]
|
||||
|
|
@ -204,7 +205,7 @@ For cross-machine reproducibility, don't rely on rerunning the same `--modules`
|
|||
|
||||
```bash
|
||||
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
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ Treat every entry in `{agent.persistent_facts}` as foundational context you carr
|
|||
|
||||
### Step 5: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
- Use `{user_name}` for greeting
|
||||
- Use `{communication_language}` for all communications
|
||||
- Use `{document_output_language}` for output documents
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ Treat every entry in `{agent.persistent_facts}` as foundational context you carr
|
|||
|
||||
### Step 5: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
- Use `{user_name}` for greeting
|
||||
- Use `{communication_language}` for all communications
|
||||
- Use `{document_output_language}` for output documents
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
- Use `{user_name}` for greeting
|
||||
- Use `{communication_language}` for all communications
|
||||
- Use `{document_output_language}` for output documents
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
### Configuration Loading
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
|
||||
- `project_knowledge`
|
||||
- `user_name`
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
### Configuration Loading
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
|
||||
- `project_knowledge`
|
||||
- `user_name`
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
- Use `{user_name}` for greeting
|
||||
- Use `{communication_language}` for all communications
|
||||
- Use `{document_output_language}` for output documents
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
- Use `{user_name}` for greeting
|
||||
- Use `{communication_language}` for all communications
|
||||
- Use `{document_output_language}` for output documents
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
- Use `{user_name}` for greeting
|
||||
- Use `{communication_language}` for all communications
|
||||
- Use `{document_output_language}` for output documents
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
- Use `{user_name}` for greeting
|
||||
- Use `{communication_language}` for all communications
|
||||
- Use `{document_output_language}` for output documents
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
- Use `{user_name}` for greeting
|
||||
- Use `{communication_language}` for all communications
|
||||
- Use `{document_output_language}` for output documents
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ Treat every entry in `{agent.persistent_facts}` as foundational context you carr
|
|||
|
||||
### Step 5: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
- Use `{user_name}` for greeting
|
||||
- Use `{communication_language}` for all communications
|
||||
- Use `{document_output_language}` for output documents
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ Treat every entry in `{agent.persistent_facts}` as foundational context you carr
|
|||
|
||||
### Step 5: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
- Use `{user_name}` for greeting
|
||||
- Use `{communication_language}` for all communications
|
||||
- Use `{document_output_language}` for output documents
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
- Use `{user_name}` for greeting
|
||||
- Use `{communication_language}` for all communications
|
||||
- Use `{document_output_language}` for output documents
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
- Use `{user_name}` for greeting
|
||||
- Use `{communication_language}` for all communications
|
||||
- Use `{document_output_language}` for output documents
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
- Use `{user_name}` for greeting
|
||||
- Use `{communication_language}` for all communications
|
||||
- Use `{document_output_language}` for output documents
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
- Use `{user_name}` for greeting
|
||||
- Use `{communication_language}` for all communications
|
||||
- Use `{document_output_language}` for output documents
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ Treat every entry in `{agent.persistent_facts}` as foundational context you carr
|
|||
|
||||
### Step 5: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
- Use `{user_name}` for greeting
|
||||
- Use `{communication_language}` for all communications
|
||||
- Use `{document_output_language}` for output documents
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
- Use `{user_name}` for greeting
|
||||
- Use `{communication_language}` for all communications
|
||||
- Use `{document_output_language}` for output documents
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
- Use `{user_name}` for greeting
|
||||
- Use `{communication_language}` for all communications
|
||||
- Use `{document_output_language}` for output documents
|
||||
|
|
|
|||
|
|
@ -227,37 +227,39 @@ Prepare the content to append to the document:
|
|||
|
||||
### 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
|
||||
- [x] Scale and complexity assessed
|
||||
- [x] Technical constraints identified
|
||||
- [x] Cross-cutting concerns mapped
|
||||
**Requirements Analysis**
|
||||
|
||||
**✅ Architectural Decisions**
|
||||
- [ ] Project context thoroughly analyzed
|
||||
- [ ] Scale and complexity assessed
|
||||
- [ ] Technical constraints identified
|
||||
- [ ] Cross-cutting concerns mapped
|
||||
|
||||
- [x] Critical decisions documented with versions
|
||||
- [x] Technology stack fully specified
|
||||
- [x] Integration patterns defined
|
||||
- [x] Performance considerations addressed
|
||||
**Architectural Decisions**
|
||||
|
||||
**✅ Implementation Patterns**
|
||||
- [ ] Critical decisions documented with versions
|
||||
- [ ] Technology stack fully specified
|
||||
- [ ] Integration patterns defined
|
||||
- [ ] Performance considerations addressed
|
||||
|
||||
- [x] Naming conventions established
|
||||
- [x] Structure patterns defined
|
||||
- [x] Communication patterns specified
|
||||
- [x] Process patterns documented
|
||||
**Implementation Patterns**
|
||||
|
||||
**✅ Project Structure**
|
||||
- [ ] Naming conventions established
|
||||
- [ ] Structure patterns defined
|
||||
- [ ] Communication patterns specified
|
||||
- [ ] Process patterns documented
|
||||
|
||||
- [x] Complete directory structure defined
|
||||
- [x] Component boundaries established
|
||||
- [x] Integration points mapped
|
||||
- [x] Requirements to structure mapping complete
|
||||
**Project Structure**
|
||||
|
||||
- [ ] Complete directory structure defined
|
||||
- [ ] Component boundaries established
|
||||
- [ ] Integration points mapped
|
||||
- [ ] Requirements to structure mapping complete
|
||||
|
||||
### 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
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
- Use `{user_name}` for greeting
|
||||
- Use `{communication_language}` for all communications
|
||||
- Use `{document_output_language}` for output documents
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
- Use `{user_name}` for greeting
|
||||
- Use `{communication_language}` for all communications
|
||||
- Use `{document_output_language}` for output documents
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ Treat every entry in `{agent.persistent_facts}` as foundational context you carr
|
|||
|
||||
### Step 5: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
- Use `{user_name}` for greeting
|
||||
- Use `{communication_language}` for all communications
|
||||
- Use `{document_output_language}` for output documents
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
|
||||
- `implementation_artifacts`
|
||||
- `planning_artifacts`
|
||||
|
|
@ -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.
|
||||
- **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
|
||||
|
||||
Read fully and follow `./step-01-orientation.md` to begin.
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
|
||||
- `project_name`, `planning_artifacts`, `implementation_artifacts`, `user_name`
|
||||
- `communication_language`, `document_output_language`, `user_skill_level`
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
|
||||
- `project_name`, `user_name`
|
||||
- `communication_language`, `document_output_language`
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
|
||||
- `project_name`, `user_name`
|
||||
- `communication_language`, `document_output_language`
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
|
||||
- `project_name`, `user_name`
|
||||
- `communication_language`, `document_output_language`
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
|
||||
- `project_name`, `user_name`
|
||||
- `communication_language`, `document_output_language`
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
|
||||
- `project_name`, `planning_artifacts`, `implementation_artifacts`, `user_name`
|
||||
- `communication_language`, `document_output_language`, `user_skill_level`
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
|
||||
- `project_name`, `user_name`
|
||||
- `communication_language`, `document_output_language`
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
|
||||
- `project_name`, `user_name`
|
||||
- `communication_language`, `document_output_language`
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ Treat every entry in `{workflow.persistent_facts}` as foundational context you c
|
|||
|
||||
### Step 4: Load Config
|
||||
|
||||
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
|
||||
- `project_name`, `user_name`
|
||||
- `communication_language`, `document_output_language`
|
||||
|
|
|
|||
|
|
@ -1,33 +1,33 @@
|
|||
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,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-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-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-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-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,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,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-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-technical-research,Technical Research,TR,Technical feasibility architecture options and implementation approaches.,,1-analysis,,,false,"planning_artifacts|project_knowledge",research documents
|
||||
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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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.
|
|
|
@ -5,15 +5,11 @@ default_selected: true # This module will be selected by default for new install
|
|||
|
||||
# Variables from Core Config inserted:
|
||||
## user_name
|
||||
## project_name
|
||||
## communication_language
|
||||
## document_output_language
|
||||
## output_folder
|
||||
|
||||
project_name:
|
||||
prompt: "What is your project called?"
|
||||
default: "{directory_name}"
|
||||
result: "{value}"
|
||||
|
||||
user_skill_level:
|
||||
prompt:
|
||||
- "What is your development experience level?"
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ This uses **micro-file architecture** for disciplined execution:
|
|||
|
||||
### Configuration Loading
|
||||
|
||||
Load config from `{project-root}/_bmad/core/config.yaml` and resolve:
|
||||
Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
|
||||
- `project_name`, `output_folder`, `user_name`
|
||||
- `communication_language`, `document_output_language`, `user_skill_level`
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ When this skill completes, the user should:
|
|||
## Data Sources
|
||||
|
||||
- **Catalog**: `{project-root}/_bmad/_config/bmad-help.csv` — assembled manifest of all installed module skills
|
||||
- **Config**: `config.yaml` and `user-config.yaml` files in `{project-root}/_bmad/` and its subfolders — resolve `output-location` variables, provide `communication_language` and `project_knowledge`
|
||||
- **Config**: Run `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` to get merged config. If that fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself — resolve `output-location` variables, provide `communication_language` and `project_knowledge`
|
||||
- **Artifacts**: Files matching `outputs` patterns at resolved `output-location` paths reveal which steps are possibly completed; their content may also provide grounding context for recommendations
|
||||
- **Project knowledge**: If `project_knowledge` resolves to an existing path, read it for grounding context. Never fabricate project-specific details.
|
||||
- **Module docs**: Rows with `_meta` in the `skill` column carry a URL or path in `output-location` pointing to the module's documentation (e.g., llms.txt). Fetch and use these to answer general questions about that module.
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ Party mode accepts optional arguments when invoked:
|
|||
|
||||
1. **Parse arguments** — check for `--model` and `--solo` flags from the user's invocation.
|
||||
|
||||
2. Load config from `{project-root}/_bmad/core/config.yaml` and resolve:
|
||||
2. Load config by running `python3 {project-root}/_bmad/scripts/resolve_config.py --project-root {project-root}` (requires Python 3.11+). If the command fails, read the merge logic in `{project-root}/_bmad/scripts/resolve_config.py` and apply it yourself to resolve the config variables. Resolve:
|
||||
- Use `{user_name}` for greeting
|
||||
- Use `{communication_language}` for all communications
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
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,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-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-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-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-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-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-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-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-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-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-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-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.
|
|
|
@ -11,6 +11,11 @@ user_name:
|
|||
default: "BMad"
|
||||
result: "{value}"
|
||||
|
||||
project_name:
|
||||
prompt: "What is your project called?"
|
||||
default: "{directory_name}"
|
||||
result: "{value}"
|
||||
|
||||
communication_language:
|
||||
prompt: "What language should agents use when chatting with you?"
|
||||
scope: user
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Resolve BMad's central config using four-layer TOML merge.
|
||||
Resolve BMad's central config using five-layer TOML merge.
|
||||
|
||||
Reads from four layers (highest priority last):
|
||||
Reads from five layers (highest priority last):
|
||||
1. {project-root}/_bmad/config.toml (installer-owned team)
|
||||
2. {project-root}/_bmad/config.user.toml (installer-owned user)
|
||||
3. {project-root}/_bmad/custom/config.toml (human-authored team, committed)
|
||||
4. {project-root}/_bmad/custom/config.user.toml (human-authored user, gitignored)
|
||||
3. ~/.bmad/config/config.user.toml (global user preferences)
|
||||
4. {project-root}/_bmad/custom/config.toml (human-authored team, committed)
|
||||
5. {project-root}/_bmad/custom/config.user.toml (human-authored user, gitignored)
|
||||
|
||||
Outputs merged JSON to stdout. Errors go to stderr.
|
||||
|
||||
|
|
@ -40,6 +41,7 @@ except ImportError:
|
|||
|
||||
_MISSING = object()
|
||||
_KEYED_MERGE_FIELDS = ("code", "id")
|
||||
GLOBAL_DIR = Path.home() / ".bmad" / "config"
|
||||
|
||||
|
||||
def load_toml(file_path: Path, required: bool = False) -> dict:
|
||||
|
|
@ -136,7 +138,7 @@ def extract_key(data, dotted_key: str):
|
|||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Resolve BMad central config using four-layer TOML merge.",
|
||||
description="Resolve BMad central config using five-layer TOML merge.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--project-root", "-p", required=True,
|
||||
|
|
@ -153,10 +155,12 @@ def main():
|
|||
|
||||
base_team = load_toml(bmad_dir / "config.toml", required=True)
|
||||
base_user = load_toml(bmad_dir / "config.user.toml")
|
||||
global_user = load_toml(GLOBAL_DIR / "config.user.toml")
|
||||
custom_team = load_toml(bmad_dir / "custom" / "config.toml")
|
||||
custom_user = load_toml(bmad_dir / "custom" / "config.user.toml")
|
||||
|
||||
merged = deep_merge(base_team, base_user)
|
||||
merged = deep_merge(merged, global_user)
|
||||
merged = deep_merge(merged, custom_team)
|
||||
merged = deep_merge(merged, custom_user)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Resolve customization for a BMad skill using three-layer TOML merge.
|
||||
Resolve customization for a BMad skill using four-layer TOML merge.
|
||||
|
||||
Reads customization from three layers (highest priority first):
|
||||
Reads customization from four layers (highest priority first):
|
||||
1. {project-root}/_bmad/custom/{name}.user.toml (personal, gitignored)
|
||||
2. {project-root}/_bmad/custom/{name}.toml (team/org, committed)
|
||||
3. {skill-root}/customize.toml (skill defaults)
|
||||
3. ~/.bmad/config/{name}.user.toml (global user preferences)
|
||||
4. {skill-root}/customize.toml (skill defaults)
|
||||
|
||||
Skill name is derived from the basename of the skill directory.
|
||||
|
||||
|
|
@ -51,6 +52,7 @@ except ImportError:
|
|||
|
||||
_MISSING = object()
|
||||
_KEYED_MERGE_FIELDS = ("code", "id")
|
||||
GLOBAL_DIR = Path.home() / ".bmad" / "config"
|
||||
|
||||
|
||||
def find_project_root(start: Path):
|
||||
|
|
@ -179,7 +181,7 @@ def extract_key(data, dotted_key: str):
|
|||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Resolve customization for a BMad skill using three-layer TOML merge.",
|
||||
description="Resolve customization for a BMad skill using four-layer TOML merge.",
|
||||
add_help=True,
|
||||
)
|
||||
parser.add_argument(
|
||||
|
|
@ -211,7 +213,10 @@ def main():
|
|||
team = load_toml(custom_dir / f"{skill_name}.toml")
|
||||
user = load_toml(custom_dir / f"{skill_name}.user.toml")
|
||||
|
||||
merged = deep_merge(defaults, team)
|
||||
global_user = load_toml(GLOBAL_DIR / f"{skill_name}.user.toml")
|
||||
|
||||
merged = deep_merge(defaults, global_user)
|
||||
merged = deep_merge(merged, team)
|
||||
merged = deep_merge(merged, user)
|
||||
|
||||
if args.key:
|
||||
|
|
|
|||
|
|
@ -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: '[0m',
|
||||
green: '[32m',
|
||||
red: '[31m',
|
||||
dim: '[2m',
|
||||
};
|
||||
|
||||
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();
|
||||
|
|
@ -1813,12 +1813,12 @@ async function runTests() {
|
|||
const moduleConfigs = {
|
||||
core: {
|
||||
user_name: 'TestUser',
|
||||
project_name: 'demo-project',
|
||||
communication_language: 'Spanish',
|
||||
document_output_language: 'English',
|
||||
output_folder: '_bmad-output',
|
||||
},
|
||||
bmm: {
|
||||
project_name: 'demo-project',
|
||||
user_skill_level: 'expert',
|
||||
planning_artifacts: '{project-root}/_bmad-output/planning-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
|
||||
// core values into every module; writeCentralConfig must strip these
|
||||
// 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',
|
||||
project_name: 'stale-bmm-copy',
|
||||
communication_language: 'Spanish',
|
||||
document_output_language: 'English',
|
||||
output_folder: '_bmad-output',
|
||||
|
|
@ -1874,6 +1877,7 @@ async function runTests() {
|
|||
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('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('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');
|
||||
if (bmmTeamMatch) {
|
||||
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('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');
|
||||
|
|
@ -2773,6 +2779,210 @@ async function runTests() {
|
|||
|
||||
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
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@ module.exports = {
|
|||
['--modules <modules>', 'Comma-separated list of module IDs to install (e.g., "bmm,bmb")'],
|
||||
[
|
||||
'--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'],
|
||||
['--user-name <name>', 'Name for agents to use (default: system username)'],
|
||||
['--communication-language <lang>', 'Language for agent communication (default: English)'],
|
||||
|
|
@ -40,6 +41,12 @@ module.exports = {
|
|||
],
|
||||
action: async (options) => {
|
||||
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
|
||||
if (options.debug) {
|
||||
process.env.BMAD_DEBUG_MANIFEST = 'true';
|
||||
|
|
@ -81,7 +88,7 @@ module.exports = {
|
|||
} else {
|
||||
await prompts.log.error(`Installation failed: ${error.message}`);
|
||||
}
|
||||
if (error.stack) {
|
||||
if (error.stack && !error.expected) {
|
||||
await prompts.log.message(error.stack);
|
||||
}
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -923,29 +923,15 @@ class Installer {
|
|||
/**
|
||||
* Merge all module-help.csv files into a single bmad-help.csv.
|
||||
* Scans all installed modules for module-help.csv and merges them.
|
||||
* Enriches agent info from the in-memory agent list produced by ManifestGenerator.
|
||||
* Output is written to _bmad/_config/bmad-help.csv.
|
||||
* Output preserves the source schema verbatim — see schema below.
|
||||
* @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 headerRow =
|
||||
'module,phase,name,code,sequence,workflow-file,command,required,agent-name,agent-command,agent-display-name,agent-title,options,description,output-location,outputs';
|
||||
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
const headerRow = 'module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs';
|
||||
const COLUMN_COUNT = 13;
|
||||
const PHASE_INDEX = 7;
|
||||
|
||||
// Get all installed module directories
|
||||
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
|
||||
|
|
@ -984,64 +970,19 @@ class Installer {
|
|||
|
||||
// Parse the line - handle quoted fields with commas
|
||||
const columns = this.parseCSVLine(line);
|
||||
if (columns.length >= 12) {
|
||||
// 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
|
||||
if (columns.length < COLUMN_COUNT - 1) continue;
|
||||
|
||||
const [
|
||||
module,
|
||||
phase,
|
||||
name,
|
||||
code,
|
||||
sequence,
|
||||
workflowFile,
|
||||
command,
|
||||
required,
|
||||
agentName,
|
||||
options,
|
||||
description,
|
||||
outputLocation,
|
||||
outputs,
|
||||
] = columns;
|
||||
// Pad short rows; truncate over-long rows
|
||||
const padded = columns.slice(0, COLUMN_COUNT);
|
||||
while (padded.length < COLUMN_COUNT) padded.push('');
|
||||
|
||||
// Pass through _meta rows as-is (module metadata, not a skill)
|
||||
if (phase === '_meta') {
|
||||
const finalModule = (!module || module.trim() === '') && moduleName !== 'core' ? moduleName : module || '';
|
||||
const metaRow = [finalModule, '_meta', '', '', '', '', '', 'false', '', '', '', '', '', '', outputLocation || '', ''];
|
||||
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)
|
||||
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 module column is empty, fill with this module's name
|
||||
// (core stays empty so its rows render as universal tools)
|
||||
if ((!padded[0] || padded[0].trim() === '') && moduleName !== 'core') {
|
||||
padded[0] = moduleName;
|
||||
}
|
||||
|
||||
allRows.push(padded.map((c) => this.escapeCSVField(c)).join(','));
|
||||
}
|
||||
|
||||
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
|
||||
|
|
@ -1053,44 +994,34 @@ class Installer {
|
|||
}
|
||||
}
|
||||
|
||||
// Sort by module, then phase, then sequence
|
||||
allRows.sort((a, b) => {
|
||||
const colsA = this.parseCSVLine(a);
|
||||
const colsB = this.parseCSVLine(b);
|
||||
// Sort by module, then phase. Stable sort preserves authored order within a phase.
|
||||
const decorated = allRows.map((row, index) => ({ row, index, cols: this.parseCSVLine(row) }));
|
||||
decorated.sort((a, 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 moduleA = (colsA[0] || '').toLowerCase();
|
||||
const moduleB = (colsB[0] || '').toLowerCase();
|
||||
if (moduleA !== moduleB) {
|
||||
return moduleA.localeCompare(moduleB);
|
||||
}
|
||||
const phaseA = a.cols[PHASE_INDEX] || '';
|
||||
const phaseB = b.cols[PHASE_INDEX] || '';
|
||||
if (phaseA !== phaseB) return phaseA.localeCompare(phaseB);
|
||||
|
||||
// Phase comparison
|
||||
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;
|
||||
return a.index - b.index;
|
||||
});
|
||||
const sortedRows = decorated.map((d) => d.row);
|
||||
|
||||
// Write merged catalog
|
||||
const outputDir = path.join(bmadDir, '_config');
|
||||
await fs.ensureDir(outputDir);
|
||||
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');
|
||||
|
||||
// Track the installed file
|
||||
this.installedFiles.add(outputPath);
|
||||
|
||||
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`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,50 @@ function clearCache() {
|
|||
_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 = {
|
||||
loadPlatformCodes,
|
||||
clearCache,
|
||||
formatPlatformList,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -336,7 +336,7 @@ class CustomModuleManager {
|
|||
|
||||
const createSpinner = async () => {
|
||||
if (silent) {
|
||||
return { start() { }, stop() { }, error() { } };
|
||||
return { start() {}, stop() {}, error() {} };
|
||||
}
|
||||
return await prompts.spinner();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -903,7 +903,10 @@ class OfficialModules {
|
|||
try {
|
||||
const content = await fs.readFile(moduleConfigPath, 'utf8');
|
||||
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;
|
||||
foundAny = true;
|
||||
}
|
||||
|
|
@ -914,9 +917,58 @@ class OfficialModules {
|
|||
}
|
||||
}
|
||||
|
||||
if (foundAny) {
|
||||
await this._hoistCoreKeysFromLegacyModuleConfigs();
|
||||
}
|
||||
|
||||
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.
|
||||
* Returns info about which modules have configurable options.
|
||||
|
|
|
|||
|
|
@ -200,12 +200,15 @@ class UI {
|
|||
actionType = options.action;
|
||||
await prompts.log.info(`Using action from command-line: ${actionType}`);
|
||||
} 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) {
|
||||
throw new Error('No valid actions available for this installation');
|
||||
}
|
||||
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}`);
|
||||
} else {
|
||||
actionType = await prompts.select({
|
||||
|
|
@ -241,8 +244,11 @@ class UI {
|
|||
.map((m) => m.trim())
|
||||
.filter(Boolean);
|
||||
await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
|
||||
} else if (options.customSource) {
|
||||
// Custom source without --modules: start with empty list (core added below)
|
||||
} else if (options.customSource && !options.yes) {
|
||||
// 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 = [];
|
||||
} else if (options.yes) {
|
||||
selectedModules = await this.getDefaultModules(installedModuleIds);
|
||||
|
|
@ -398,6 +404,37 @@ class UI {
|
|||
* @param {Object} options - Command-line options
|
||||
* @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 = {}) {
|
||||
const { ExistingInstall } = require('./core/existing-install');
|
||||
const { Installer } = require('./core/installer');
|
||||
|
|
@ -432,15 +469,10 @@ class UI {
|
|||
const allTools = [...preferredIdes, ...otherIdes];
|
||||
|
||||
// Non-interactive: handle --tools and --yes flags before interactive prompt
|
||||
if (options.tools) {
|
||||
if (options.tools.toLowerCase() === 'none') {
|
||||
await prompts.log.info('Skipping tool configuration (--tools none)');
|
||||
return { ides: [], skipIde: true };
|
||||
}
|
||||
const selectedIdes = options.tools
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean);
|
||||
// Use !== undefined so an explicit --tools "" falls through to _parseToolsFlag and
|
||||
// gets a specific "passed empty" error instead of being silently ignored.
|
||||
if (options.tools !== undefined) {
|
||||
const selectedIdes = this._parseToolsFlag(options.tools, allKnownValues);
|
||||
await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
|
||||
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
|
||||
return { ides: selectedIdes, skipIde: false };
|
||||
|
|
@ -516,21 +548,13 @@ class UI {
|
|||
|
||||
let selectedIdes = [];
|
||||
|
||||
// Check if tools are provided via command-line
|
||||
if (options.tools) {
|
||||
// Check for explicit "none" value to skip tool installation
|
||||
if (options.tools.toLowerCase() === 'none') {
|
||||
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 this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
|
||||
return { ides: selectedIdes, skipIde: false };
|
||||
}
|
||||
// Check if tools are provided via command-line.
|
||||
// Use !== undefined so an explicit --tools "" still hits _parseToolsFlag's empty-value error.
|
||||
if (options.tools !== undefined) {
|
||||
selectedIdes = this._parseToolsFlag(options.tools, allKnownValues);
|
||||
await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
|
||||
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
|
||||
return { ides: selectedIdes, skipIde: false };
|
||||
} else if (options.yes) {
|
||||
// If --yes flag is set, skip tool prompt and use previously configured tools or empty
|
||||
if (configuredIdes.length > 0) {
|
||||
|
|
@ -538,8 +562,18 @@ class UI {
|
|||
await this.displaySelectedTools(configuredIdes, preferredIdes, allTools);
|
||||
return { ides: configuredIdes, skipIde: false };
|
||||
} else {
|
||||
await prompts.log.info('Skipping tool configuration (--yes flag, no previous tools)');
|
||||
return { ides: [], skipIde: true };
|
||||
const err = new Error(
|
||||
[
|
||||
'--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);
|
||||
configCollector.collectedConfig.core = {
|
||||
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',
|
||||
document_output_language: 'English',
|
||||
output_folder: '_bmad-output',
|
||||
|
|
|
|||
Loading…
Reference in New Issue