Compare commits

...

6 Commits

Author SHA1 Message Date
miendinh 6b9497c084
Merge 10330b03a0 into 5dbfb588ee 2026-04-08 10:16:33 +07:00
miendinh 10330b03a0
Merge branch 'main' into docs/vi-vn-update-translations 2026-04-08 10:16:28 +07:00
Brian 5dbfb588ee
refactor(installer): remove custom content installation feature (#2227)
* refactor(installer): remove custom content installation feature

Remove the entire local filesystem custom content feature from the
installer to make way for marketplace-based plugin installation.

Deleted: custom-handler.js, custom-module-cache.js, custom-modules.js
Removed: --custom-content CLI flag, interactive custom content prompts,
custom module caching, manifest tracking, missing-source resolution,
and related test suites. Updated docs across all translations.

* fix: address review findings from Augment

Fix admonition syntax (remove accidental space in :::note) across 4
translated docs files, and update stale JSDoc on listAvailable().
2026-04-07 21:41:03 -05:00
Alex Verkhovsky 9ca0316674
refactor(quick-dev): eliminate spec-wip.md singleton (#2214)
* refactor(quick-dev): eliminate spec-wip.md singleton

Write directly to spec-{slug}.md with status: draft instead of using
a shared spec-wip.md file. Use draft status for resume detection in
step-01. Removes wipFile variable from all step frontmatter and
workflow initialization.

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

* fix(quick-dev): address PR review findings

- step-02: preserve Intent block on draft resume instead of regenerating from template (F1)
- step-01: resume existing draft on slug collision rather than creating -2 duplicate (F3)
- step-01: recognize `done` status and ingest as context instead of silently re-implementing (F4)
- step-oneshot: remove unused spec_file frontmatter declaration (F6)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:14:24 -07:00
Alex Verkhovsky 6cecab2626
chore(install): stop copying skill prompts to _bmad by default (#2182)
* chore(install): stop copying skill prompts to _bmad by default

Flip install_to_bmad default from true to false so skill directories
are cleaned from _bmad/ after IDE install. Skills are self-contained
in their IDE directories (.claude/skills/, etc.) and no longer need
duplicate copies in _bmad/.

Two skills (bmad-create-prd, bmad-validate-prd) opt back in via
explicit manifests because bmad-edit-prd cross-references their data
files. Also fixes broken bmm-skills/ path references and corrects
the file-ref validator module-to-source mapping.

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

* refactor(install): make edit-prd self-contained and remove install_to_bmad

Give bmad-edit-prd its own copy of prd-purpose.md and replace the
cross-skill validation workflow reference with a skill invocation, so
all three PRD skills are fully self-contained. With no remaining
consumers, remove the install_to_bmad flag from manifests, CSV output,
the post-install cleanup loop, and the dedicated test file.

* feat(install): clean up skill directories from _bmad after IDE install

Skills are self-contained in IDE directories, so _bmad/ only needs
module-level files (config.yaml, _config/). After all IDE setups
complete, remove skill directories from _bmad/ via skill-manifest.csv.
Also cleans up skill dirs left by older installer versions.

* test(install): drop stale install_to_bmad column from suite 27 CSV row

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:02:59 -07:00
miendinh e7b941068c docs(vi-vn): sync translations and add missing checkpoint-preview page 2026-04-06 18:13:32 +07:00
47 changed files with 410 additions and 2487 deletions

View File

@ -3,6 +3,8 @@
[![Version](https://img.shields.io/npm/v/bmad-method?color=blue&label=version)](https://www.npmjs.com/package/bmad-method)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Node.js Version](https://img.shields.io/badge/node-%3E%3D20.0.0-brightgreen)](https://nodejs.org)
[![Python Version](https://img.shields.io/badge/python-%3E%3D3.10-blue?logo=python&logoColor=white)](https://www.python.org)
[![uv](https://img.shields.io/badge/uv-package%20manager-blueviolet?logo=uv)](https://docs.astral.sh/uv/)
[![Discord](https://img.shields.io/badge/Discord-Join%20Community-7289da?logo=discord&logoColor=white)](https://discord.gg/gk8jAdXWmj)
[English](README.md) | [简体中文](README_CN.md) | Tiếng Việt
@ -36,7 +38,7 @@ Các công cụ AI truyền thống thường làm thay phần suy nghĩ của b
## Bắt đầu nhanh
**Điều kiện tiên quyết**: [Node.js](https://nodejs.org) v20+
**Điều kiện tiên quyết**: [Node.js](https://nodejs.org) v20+ · [Python](https://www.python.org) 3.10+ · [uv](https://docs.astral.sh/uv/)
```bash
npx bmad-method install
@ -80,18 +82,15 @@ BMad Method có thể được mở rộng bằng các mô-đun chính thức ch
## Cộng đồng
- [Discord](https://discord.gg/gk8jAdXWmj) - Nhận trợ giúp, chia sẻ ý tưởng, cộng tác
- [Đăng ký trên YouTube](https://www.youtube.com/@BMadCode) - video hướng dẫn, lớp chuyên sâu và podcast (ra mắt tháng 2 năm 2025)
- [YouTube](https://youtube.com/@BMadCode) - Video hướng dẫn, master class và nhiều nội dung khác
- [X / Twitter](https://x.com/BMadCode)
- [Website](https://bmadcode.com)
- [GitHub Issues](https://github.com/bmad-code-org/BMAD-METHOD/issues) - Báo lỗi và yêu cầu tính năng
- [Discussions](https://github.com/bmad-code-org/BMAD-METHOD/discussions) - Trao đổi cộng đồng
## Hỗ trợ BMad
BMad miễn phí cho tất cả mọi người - và sẽ luôn như vậy. Nếu bạn muốn hỗ trợ quá trình phát triển:
- ⭐ Hãy nhấn sao cho dự án ở góc trên bên phải của trang này
- ☕ [Buy Me a Coffee](https://buymeacoffee.com/bmad) - Tiếp thêm năng lượng cho quá trình phát triển
- 🏢 Tài trợ doanh nghiệp - Nhắn riêng trên Discord
- 🎤 Diễn thuyết và truyền thông - Sẵn sàng cho hội nghị, podcast, phỏng vấn (BM trên Discord)
BMad miễn phí cho tất cả mọi người và sẽ luôn như vậy. Hãy nhấn sao cho repo này, [mời tôi một ly cà phê](https://buymeacoffee.com/bmad), hoặc gửi email tới <contact@bmadcode.com> nếu bạn muốn tài trợ doanh nghiệp.
## Đóng góp

View File

@ -27,7 +27,6 @@ Vyžaduje [Node.js](https://nodejs.org) v20+ a `npx` (součástí npm).
| `--directory <cesta>` | Instalační adresář | `--directory ~/projects/myapp` |
| `--modules <moduly>` | Čárkou oddělená ID modulů | `--modules bmm,bmb` |
| `--tools <nástroje>` | Čárkou oddělená ID nástrojů/IDE (použijte `none` pro přeskočení) | `--tools claude-code,cursor` nebo `--tools none` |
| `--custom-content <cesty>` | Čárkou oddělené cesty k vlastním modulům | `--custom-content ~/my-module,~/another-module` |
| `--action <typ>` | Akce pro existující instalace: `install` (výchozí), `update` nebo `quick-update` | `--action quick-update` |
### Základní konfigurace
@ -108,16 +107,6 @@ npx bmad-method install \
--action quick-update
```
### Instalace s vlastním obsahem
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--modules bmm \
--custom-content ~/my-custom-module,~/another-module \
--tools claude-code
```
## Co získáte
- Plně nakonfigurovaný adresář `_bmad/` ve vašem projektu
@ -159,13 +148,6 @@ Neplatné hodnoty buď:
- Ověřte, že ID modulu je správné
- Externí moduly musí být dostupné v registru
### Neplatná cesta k vlastnímu obsahu
Ujistěte se, že každá cesta k vlastnímu obsahu:
- Ukazuje na adresář
- Obsahuje soubor `module.yaml` v kořeni
- Má pole `code` v `module.yaml`
:::note[Stále jste uvízli?]
Spusťte s `--debug` pro detailní výstup, zkuste interaktivní režim pro izolaci problému, nebo nahlaste na <https://github.com/bmad-code-org/BMAD-METHOD/issues>.
:::

View File

@ -72,7 +72,7 @@ L'installateur affiche les modules disponibles. Sélectionnez ceux dont vous ave
### 5. Suivre les instructions
L'installateur vous guide pour le reste — contenu personnalisé, paramètres, etc.
L'installateur vous guide pour le reste — paramètres, intégrations d'outils, etc.
## Ce que vous obtenez

View File

@ -27,7 +27,6 @@ Nécessite [Node.js](https://nodejs.org) v20+ et `npx` (inclus avec npm).
| `--directory <chemin>` | Répertoire d'installation | `--directory ~/projects/myapp` |
| `--modules <modules>` | IDs de modules séparés par des virgules | `--modules bmm,bmb` |
| `--tools <outils>` | IDs d'outils/IDE séparés par des virgules (utilisez `none` pour ignorer) | `--tools claude-code,cursor` ou `--tools none` |
| `--custom-content <chemins>` | Chemins vers des modules personnalisés séparés par des virgules | `--custom-content ~/my-module,~/another-module` |
| `--action <type>` | Action pour les installations existantes : `install` (par défaut), `update`, ou `quick-update` | `--action quick-update` |
### Configuration principale
@ -120,16 +119,6 @@ npx bmad-method install \
--action quick-update
```
### Installation avec du contenu personnalisé
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--modules bmm \
--custom-content ~/my-custom-module,~/another-module \
--tools claude-code
```
## Ce que vous obtenez
- Un répertoire `_bmad/` entièrement configuré dans votre projet
@ -143,12 +132,11 @@ BMad valide toutes les options fournis :
- **Directory** — Doit être un chemin valide avec des permissions d'écriture
- **Modules** — Avertit des IDs de modules invalides (mais n'échoue pas)
- **Tools** — Avertit des IDs d'outils invalides (mais n'échoue pas)
- **Custom Content** — Chaque chemin doit contenir un fichier `module.yaml` valide
- **Action** — Doit être l'une des suivantes : `install`, `update`, `quick-update`
Les valeurs invalides entraîneront soit :
1. Laffichage dun message d'erreur suivi dun exit (pour les options critiques comme le répertoire)
2. Un avertissement puis la continuation de linstallation (pour les éléments optionnels comme le contenu personnalisé)
2. Un avertissement puis la continuation de linstallation (pour les éléments optionnels)
3. Un retour aux invites interactives (pour les valeurs requises manquantes)
:::tip[Bonnes pratiques]
@ -172,13 +160,6 @@ Les valeurs invalides entraîneront soit :
- Vérifiez que l'ID du module est correct
- Les modules externes doivent être disponibles dans le registre
### Chemin de contenu personnalisé invalide
Assurez-vous que chaque chemin de contenu personnalisé :
- Pointe vers un répertoire
- Contient un fichier `module.yaml` à la racine
- Possède un champ `code` dans `module.yaml`
:::note[Toujours bloqué ?]
Exécutez avec `--debug` pour une sortie détaillée, essayez le mode interactif pour isoler le problème, ou signalez-le à <https://github.com/bmad-code-org/BMAD-METHOD/issues>.
:::

View File

@ -72,7 +72,7 @@ The installer shows available modules. Select whichever ones you need — most u
### 5. Follow the Prompts
The installer guides you through the rest — custom content, settings, etc.
The installer guides you through the rest — settings, tool integrations, etc.
## What You Get

View File

@ -27,7 +27,6 @@ Requires [Node.js](https://nodejs.org) v20+ and `npx` (included with npm).
| `--directory <path>` | Installation directory | `--directory ~/projects/myapp` |
| `--modules <modules>` | Comma-separated module IDs | `--modules bmm,bmb` |
| `--tools <tools>` | Comma-separated tool/IDE IDs (use `none` to skip) | `--tools claude-code,cursor` or `--tools none` |
| `--custom-content <paths>` | Comma-separated paths to custom modules | `--custom-content ~/my-module,~/another-module` |
| `--action <type>` | Action for existing installations: `install` (default), `update`, or `quick-update` | `--action quick-update` |
### Core Configuration
@ -120,16 +119,6 @@ npx bmad-method install \
--action quick-update
```
### Installation with Custom Content
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--modules bmm \
--custom-content ~/my-custom-module,~/another-module \
--tools claude-code
```
## What You Get
- A fully configured `_bmad/` directory in your project
@ -143,12 +132,11 @@ BMad validates all provided flags:
- **Directory** — Must be a valid path with write permissions
- **Modules** — Warns about invalid module IDs (but won't fail)
- **Tools** — Warns about invalid tool IDs (but won't fail)
- **Custom Content** — Each path must contain a valid `module.yaml` file
- **Action** — Must be one of: `install`, `update`, `quick-update`
Invalid values will either:
1. Show an error and exit (for critical options like directory)
2. Show a warning and skip (for optional items like custom content)
2. Show a warning and skip (for optional items)
3. Fall back to interactive prompts (for missing required values)
:::tip[Best Practices]
@ -172,13 +160,6 @@ Invalid values will either:
- Verify the module ID is correct
- External modules must be available in the registry
### Custom content path invalid
Ensure each custom content path:
- Points to a directory
- Contains a `module.yaml` file in the root
- Has a `code` field in the `module.yaml`
:::note[Still stuck?]
Run with `--debug` for detailed output, try interactive mode to isolate the issue, or report at <https://github.com/bmad-code-org/BMAD-METHOD/issues>.
:::

View File

@ -1,53 +1,53 @@
---
title: "Giai đoạn Analysis: từ ý tưởng đến nền tảng"
description: Brainstorming, research, product brief và PRFAQ là gì, và nên dùng từng công cụ khi nào
title: "Giai đoạn phân tích: từ ý tưởng đến nền tảng"
description: Động não, nghiên cứu, product brief và PRFAQ là gì, và nên dùng từng công cụ khi nào
sidebar:
order: 1
---
Giai đoạn Analysis (Phase 1) giúp bạn suy nghĩ rõ ràng về sản phẩm trước khi cam kết bắt tay vào xây dựng. Mọi công cụ trong giai đoạn này đều là tùy chọn, nhưng nếu bỏ qua toàn bộ phần analysis thì PRD của bạn sẽ được dựng trên giả định thay vì insight.
Giai đoạn phân tích (giai đoạn 1) giúp bạn suy nghĩ rõ ràng về sản phẩm trước khi cam kết bắt tay vào xây dựng. Mọi công cụ trong giai đoạn này đều là tùy chọn, nhưng nếu bỏ qua toàn bộ phần phân tích thì PRD của bạn sẽ được dựng trên giả định thay vì hiểu biết thực chất.
## Vì sao cần Analysis trước Planning?
## Vì sao cần phân tích trước khi lập kế hoạch?
PRD trả lời câu hỏi "chúng ta nên xây gì và vì sao?". Nếu đầu vào của nó là những suy nghĩ mơ hồ, bạn sẽ nhận lại một PRD mơ hồ, và mọi tài liệu phía sau đều kế thừa chính sự mơ hồ đó. Kiến trúc dựng trên một PRD yếu sẽ đặt cược sai về mặt kỹ thuật. Stories sinh ra từ một kiến trúc yếu sẽ bỏ sót edge case. Chi phí sẽ dồn lên theo từng tầng.
PRD trả lời câu hỏi "chúng ta nên xây gì và vì sao?". Nếu đầu vào của nó là những suy nghĩ mơ hồ, bạn sẽ nhận lại một PRD mơ hồ, và mọi tài liệu phía sau đều kế thừa chính sự mơ hồ đó. Kiến trúc dựng trên một PRD yếu sẽ đặt cược sai về mặt kỹ thuật. Các story sinh ra từ một kiến trúc yếu sẽ bỏ sót trường hợp biên. Chi phí sẽ dồn lên theo từng tầng.
Các công cụ analysis tồn tại để làm PRD của bạn sắc bén hơn. Chúng tiếp cận vấn đề từ nhiều góc độ khác nhau: khám phá sáng tạo, thực tế thị trường, độ rõ ràng về khách hàng, tính khả thi. Nhờ vậy, đến khi bạn ngồi xuống làm việc với PM agent, bạn đã biết mình đang xây cái gì và cho ai.
Các công cụ phân tích tồn tại để làm PRD của bạn sắc bén hơn. Chúng tiếp cận vấn đề từ nhiều góc độ khác nhau: khám phá sáng tạo, thực tế thị trường, độ rõ ràng về khách hàng, tính khả thi. Nhờ vậy, đến khi bạn ngồi xuống làm việc với agent PM, bạn đã biết mình đang xây cái gì và cho ai.
## Các công cụ
### Brainstorming
### Động não
**Nó là gì.** Một phiên sáng tạo có điều phối, sử dụng các kỹ thuật ideation đã được kiểm chứng. AI đóng vai trò như người huấn luyện, kéo ý tưởng ra từ bạn thông qua các bài tập có cấu trúc, chứ không nghĩ thay cho bạn.
**Nó là gì.** Một phiên sáng tạo có điều phối, sử dụng các kỹ thuật phát ý tưởng đã được kiểm chứng. AI đóng vai trò như người huấn luyện, kéo ý tưởng ra từ bạn thông qua các bài tập có cấu trúc, chứ không nghĩ thay cho bạn.
**Vì sao nó có mặt ở đây.** Ý tưởng thô cần không gian để phát triển trước khi bị khóa cứng thành requirement. Brainstorming tạo ra khoảng không đó. Nó đặc biệt có giá trị khi bạn có một miền vấn đề nhưng chưa có lời giải rõ ràng, hoặc khi bạn muốn khám phá nhiều hướng trước khi commit.
**Vì sao nó có mặt ở đây.** Ý tưởng thô cần không gian để phát triển trước khi bị khóa cứng thành yêu cầu. Động não tạo ra khoảng không đó. Nó đặc biệt có giá trị khi bạn có một miền vấn đề nhưng chưa có lời giải rõ ràng, hoặc khi bạn muốn khám phá nhiều hướng trước khi cam kết.
**Khi nào nên dùng.** Bạn có một hình dung mơ hồ về thứ mình muốn xây nhưng chưa kết tinh được thành khái niệm rõ ràng. Hoặc bạn đã có concept ban đầu nhưng muốn pressure-test nó với các phương án thay thế.
**Khi nào nên dùng.** Bạn có một hình dung mơ hồ về thứ mình muốn xây nhưng chưa kết tinh được thành khái niệm rõ ràng. Hoặc bạn đã có ý tưởng ban đầu nhưng muốn kiểm chứng độ vững của nó bằng các phương án thay thế.
Xem [Brainstorming](./brainstorming.md) để hiểu sâu hơn về cách một phiên làm việc diễn ra.
### Research (Thị trường, miền nghiệp vụ, kỹ thuật)
### Nghiên cứu (thị trường, miền nghiệp vụ, kỹ thuật)
**Nó là gì.** Ba workflow nghiên cứu tập trung vào các chiều khác nhau của ý tưởng. Market research xem xét đối thủ, xu hướng và cảm nhận của người dùng. Domain research xây dựng hiểu biết về miền nghiệp vụ và thuật ngữ. Technical research đánh giá tính khả thi, các lựa chọn kiến trúc và hướng triển khai.
**Nó là gì.** Ba quy trình nghiên cứu tập trung vào các chiều khác nhau của ý tưởng. Nghiên cứu thị trường xem xét đối thủ, xu hướng và cảm nhận của người dùng. Nghiên cứu miền nghiệp vụ xây dựng hiểu biết về lĩnh vực và thuật ngữ. Nghiên cứu kỹ thuật đánh giá tính khả thi, các lựa chọn kiến trúc và hướng triển khai.
**Vì sao nó có mặt ở đây.** Xây dựng dựa trên giả định là con đường nhanh nhất để tạo ra thứ chẳng ai cần. Research đặt concept của bạn xuống mặt đất: đối thủ nào đã tồn tại, người dùng thực sự đang vật lộn với điều gì, điều gì khả thi về kỹ thuật, và bạn sẽ phải đối mặt với những ràng buộc đặc thù ngành nào.
**Vì sao nó có mặt ở đây.** Xây dựng dựa trên giả định là con đường nhanh nhất để tạo ra thứ chẳng ai cần. Nghiên cứu đặt ý tưởng của bạn xuống mặt đất: đối thủ nào đã tồn tại, người dùng thực sự đang vật lộn với điều gì, điều gì khả thi về kỹ thuật, và bạn sẽ phải đối mặt với những ràng buộc đặc thù ngành nào.
**Khi nào nên dùng.** Bạn đang bước vào một miền mới, nghi ngờ có đối thủ nhưng chưa lập bản đồ được, hoặc concept của bạn phụ thuộc vào những năng lực kỹ thuật mà bạn chưa kiểm chứng. Có thể chạy một, hai, hoặc cả ba; mỗi workflow đều đứng độc lập.
**Khi nào nên dùng.** Bạn đang bước vào một miền mới, nghi ngờ có đối thủ nhưng chưa lập bản đồ được, hoặc ý tưởng của bạn phụ thuộc vào những năng lực kỹ thuật mà bạn chưa kiểm chứng. Có thể chạy một, hai, hoặc cả ba; mỗi quy trình đều đứng độc lập.
### Product Brief
**Nó là gì.** Một phiên discovery có hướng dẫn, tạo ra bản tóm tắt điều hành 1-2 trang cho concept sản phẩm của bạn. AI đóng vai trò Business Analyst cộng tác, giúp bạn diễn đạt tầm nhìn, đối tượng mục tiêu, giá trị cốt lõi và phạm vi.
**Vì sao nó có mặt ở đây.** Product brief là con đường nhẹ nhàng hơn để đi vào planning. Nó ghi lại tầm nhìn chiến lược của bạn theo định dạng có cấu trúc và đưa thẳng vào quá trình tạo PRD. Nó hoạt động tốt nhất khi bạn đã có niềm tin tương đối chắc vào concept của mình: bạn biết khách hàng là ai, vấn đề là gì, và đại khái muốn xây gì. Brief sẽ tổ chức lại và làm sắc nét lối suy nghĩ đó.
**Vì sao nó có mặt ở đây.** Product brief là con đường nhẹ nhàng hơn để đi vào giai đoạn lập kế hoạch. Nó ghi lại tầm nhìn chiến lược của bạn theo định dạng có cấu trúc và đưa thẳng vào quá trình tạo PRD. Nó hoạt động tốt nhất khi bạn đã có niềm tin tương đối chắc vào ý tưởng của mình: bạn biết khách hàng là ai, vấn đề là gì, và đại khái muốn xây gì. Brief sẽ tổ chức lại và làm sắc nét lối suy nghĩ đó.
**Khi nào nên dùng.** Concept của bạn đã tương đối rõ và bạn muốn ghi lại nó một cách hiệu quả trước khi tạo PRD. Bạn tin vào hướng đi hiện tại và không cần bị thách thức giả định một cách quá quyết liệt.
**Khi nào nên dùng.** Ý tưởng của bạn đã tương đối rõ và bạn muốn ghi lại nó một cách hiệu quả trước khi tạo PRD. Bạn tin vào hướng đi hiện tại và không cần bị thách thức giả định một cách quá quyết liệt.
### PRFAQ (Working Backwards)
**Nó là gì.** Phương pháp Working Backwards của Amazon được chuyển thành một thử thách tương tác. Bạn viết thông cáo báo chí công bố sản phẩm hoàn thiện trước khi tồn tại dù chỉ một dòng code, rồi trả lời những câu hỏi khó nhất mà khách hàng và stakeholder sẽ đặt ra. AI đóng vai trò product coach dai dẳng nhưng mang tính xây dựng.
**Vì sao nó có mặt ở đây.** PRFAQ là con đường nghiêm ngặt hơn để đi vào planning. Nó buộc bạn đạt đến sự rõ ràng theo hướng customer-first bằng cách bắt bạn bảo vệ từng phát biểu. Nếu bạn không viết nổi một thông cáo báo chí đủ thuyết phục, sản phẩm đó chưa sẵn sàng. Nếu phần FAQ lộ ra những khoảng trống, đó chính là những khoảng trống mà bạn sẽ phát hiện muộn hơn rất nhiều, và với chi phí lớn hơn nhiều, trong lúc triển khai. Bài kiểm tra này bóc tách lối suy nghĩ yếu ngay từ sớm, khi chi phí sửa còn rẻ nhất.
**Vì sao nó có mặt ở đây.** PRFAQ là con đường nghiêm ngặt hơn để đi vào giai đoạn lập kế hoạch. Nó buộc bạn đạt đến sự rõ ràng theo hướng lấy khách hàng làm trung tâm bằng cách bắt bạn bảo vệ từng phát biểu. Nếu bạn không viết nổi một thông cáo báo chí đủ thuyết phục, sản phẩm đó chưa sẵn sàng. Nếu phần FAQ lộ ra những khoảng trống, đó chính là những khoảng trống mà bạn sẽ phát hiện muộn hơn rất nhiều, và với chi phí lớn hơn nhiều, trong lúc triển khai. Bài kiểm tra này bóc tách lối suy nghĩ yếu ngay từ sớm, khi chi phí sửa còn rẻ nhất.
**Khi nào nên dùng.** Bạn muốn stress-test concept trước khi commit tài nguyên. Bạn chưa chắc người dùng có thực sự quan tâm hay không. Bạn muốn xác nhận rằng mình có thể diễn đạt một value proposition rõ ràng và có thể bảo vệ được. Hoặc đơn giản là bạn muốn dùng sự kỷ luật của Working Backwards để làm suy nghĩ của mình sắc bén hơn.
**Khi nào nên dùng.** Bạn muốn kiểm tra độ vững của ý tưởng trước khi cam kết tài nguyên. Bạn chưa chắc người dùng có thực sự quan tâm hay không. Bạn muốn xác nhận rằng mình có thể diễn đạt một giá trị cốt lõi rõ ràng và có thể bảo vệ được. Hoặc đơn giản là bạn muốn dùng sự kỷ luật của Working Backwards để làm suy nghĩ của mình sắc bén hơn.
## Tôi nên dùng cái nào?
@ -65,6 +65,6 @@ Product Brief và PRFAQ đều tạo ra đầu vào cho PRD. Hãy chọn một t
Hãy chạy `bmad-help` và mô tả tình huống của bạn. Nó sẽ gợi ý điểm bắt đầu phù hợp dựa trên những gì bạn đã làm và điều bạn đang muốn đạt được.
:::
## Sau Analysis thì chuyện gì xảy ra?
## Sau giai đoạn phân tích thì chuyện gì xảy ra?
Output từ Analysis đi thẳng vào Phase 2 (Planning). Workflow tạo PRD chấp nhận product brief, tài liệu PRFAQ, kết quả research và báo cáo brainstorming làm đầu vào. Nó sẽ tổng hợp bất cứ thứ gì bạn đã tạo thành các requirement có cấu trúc. Bạn làm analysis càng kỹ, PRD của bạn càng sắc.
Đầu ra từ giai đoạn phân tích đi thẳng vào giai đoạn 2, lập kế hoạch. Quy trình tạo PRD chấp nhận product brief, tài liệu PRFAQ, kết quả nghiên cứu và báo cáo động não làm đầu vào. Nó sẽ tổng hợp bất cứ thứ gì bạn đã tạo thành các yêu cầu có cấu trúc. Bạn làm phân tích càng kỹ, PRD của bạn càng sắc.

View File

@ -1,5 +1,5 @@
---
title: "Party Mode"
title: "Chế độ Party"
description: Cộng tác đa agent - đưa tất cả agent AI vào cùng một cuộc trò chuyện
sidebar:
order: 7

View File

@ -1,5 +1,5 @@
---
title: "Project Context"
title: "Bối cảnh dự án"
description: Cách project-context.md định hướng các agent AI theo quy tắc và ưu tiên của dự án
sidebar:
order: 7

View File

@ -1,73 +1,73 @@
---
title: "Quick Dev"
description: Giảm ma sát human-in-the-loop mà vẫn giữ các checkpoint bảo vệ chất lượng output
title: "Phát triển nhanh"
description: Giảm ma sát có người trong vòng lặp mà vẫn giữ các điểm kiểm tra bảo vệ chất lượng đầu ra
sidebar:
order: 2
---
Đưa ý định vào, nhận thay đổi mã nguồn ra, với số lần cần con người nhảy vào giữa quy trình ít nhất có thể - nhưng không đánh đổi chất lượng.
Nó cho phép model tự vận hành lâu hơn giữa các checkpoint, rồi chỉ đưa con người quay lại khi tác vụ không thể tiếp tục an toàn nếu thiếu phán đoán của con người, hoặc khi đã đến lúc review kết quả cuối.
Nó cho phép mô hình tự vận hành lâu hơn giữa các điểm kiểm tra, rồi chỉ đưa con người quay lại khi tác vụ không thể tiếp tục an toàn nếu thiếu phán đoán của con người, hoặc khi đã đến lúc rà soát kết quả cuối.
![Quick Dev workflow diagram](/diagrams/quick-dev-diagram.png)
## Vì sao nó tồn tại
Các lượt human-in-the-loop vừa cần thiết vừa tốn kém.
Các lượt có người trong vòng lặp vừa cần thiết vừa tốn kém.
LLM hiện tại vẫn thất bại theo những cách dễ đoán: hiểu sai ý định, tự điền vào khoảng trống bằng những phán đoán tự tin, lệch sang công việc không liên quan, và tạo ra các bản review nhiễu. Đồng thời, việc cần con người nhảy vào liên tục làm giảm tốc độ phát triển. Sự chú ý của con người là nút thắt.
`bmad-quick-dev` cân bằng lại đánh đổi đó. Nó tin model có thể chạy tự chủ lâu hơn, nhưng chỉ sau khi workflow đã tạo được một ranh giới đủ mạnh để làm điều đó an toàn.
`bmad-quick-dev` cân bằng lại đánh đổi đó. Nó tin mô hình có thể chạy tự chủ lâu hơn, nhưng chỉ sau khi quy trình đã tạo được một ranh giới đủ mạnh để làm điều đó an toàn.
## Thiết kế cốt lõi
### 1. Nén ý định trước
Workflow bắt đầu bằng việc để con người và model nén yêu cầu thành một mục tiêu thống nhất. Đầu vào có thể bắt đầu như một ý định thô, nhưng trước khi workflow tự vận hành thì nó phải đủ nhỏ, đủ rõ ràng, và đủ ít mâu thuẫn để có thể thực thi.
Quy trình bắt đầu bằng việc để con người và mô hình nén yêu cầu thành một mục tiêu thống nhất. Đầu vào có thể bắt đầu như một ý định thô, nhưng trước khi quy trình tự vận hành thì nó phải đủ nhỏ, đủ rõ ràng, và đủ ít mâu thuẫn để có thể thực thi.
Ý định có thể đến từ nhiều dạng: vài cụm từ, liên kết bug tracker, output từ plan mode, đoạn văn bản copy từ phiên chat, hoặc thậm chí một số story trong `epics.md` của chính BMAD. Ở trường hợp cuối, workflow không hiểu được ngữ nghĩa theo dõi story của BMAD, nhưng vẫn có thể lấy chính story đó và tiếp tục.
Ý định có thể đến từ nhiều dạng: vài cụm từ, liên kết trình theo dõi lỗi, đầu ra từ chế độ lập kế hoạch, đoạn văn bản sao chép từ phiên chat, hoặc thậm chí một số story trong `epics.md` của chính BMAD. Ở trường hợp cuối, quy trình không hiểu được ngữ nghĩa theo dõi story của BMAD, nhưng vẫn có thể lấy chính story đó và tiếp tục.
Workflow này không loại bỏ quyền kiểm soát của con người. Nó chuyển nó về một số thời điểm có giá trị cao:
Quy trình này không loại bỏ quyền kiểm soát của con người. Nó chuyển nó về một số thời điểm có giá trị cao:
- **Làm rõ ý định** - biến một yêu cầu lộn xộn thành một mục tiêu thống nhất, không mâu thuẫn ngầm
- **Phê duyệt spec** - xác nhận rằng cách hiểu đã đóng băng là đúng thứ cần xây
- **Review sản phẩm cuối** - checkpoint chính, nơi con người quyết định kết quả cuối có chấp nhận được hay không
- **Phê duyệt đặc tả** - xác nhận rằng cách hiểu đã được chốt là đúng thứ cần xây
- **Rà soát sản phẩm cuối** - điểm kiểm tra chính, nơi con người quyết định kết quả cuối có chấp nhận được hay không
### 2. Định tuyến theo con đường an toàn nhỏ nhất
Khi mục tiêu đã rõ, workflow sẽ quyết định đây có phải thay đổi one-shot thật sự hay cần đi theo đường đầy đủ hơn. Những thay đổi nhỏ, blast radius gần như bằng 0 có thể đi thẳng vào triển khai. Còn lại sẽ đi qua lập kế hoạch để model có được một ranh giới mạnh hơn trước khi tự chạy lâu hơn.
Khi mục tiêu đã rõ, quy trình sẽ quyết định đây có phải thay đổi thực hiện một lần là xong hay cần đi theo đường đầy đủ hơn. Những thay đổi nhỏ, phạm vi ảnh hưởng gần như bằng 0 có thể đi thẳng vào triển khai. Còn lại sẽ đi qua lập kế hoạch để mô hình có được một ranh giới mạnh hơn trước khi tự chạy lâu hơn.
### 3. Chạy lâu hơn với ít giám sát hơn
Sau quyết định định tuyến đó, model có thể tự gánh thêm công việc. Trên con đường đầy đủ, spec đã được phê duyệt trở thành ranh giới mà model sẽ thực thi với ít giám sát hơn, và đó chính là mục tiêu của thiết kế này.
Sau quyết định định tuyến đó, mô hình có thể tự gánh thêm công việc. Trên con đường đầy đủ, đặc tả đã được phê duyệt trở thành ranh giới mà mô hình sẽ thực thi với ít giám sát hơn, và đó chính là mục tiêu của thiết kế này.
### 4. Chẩn đoán lỗi ở đúng tầng
Nếu triển khai sai vì ý định sai, vậy sửa code không phải cách fix đúng. Nếu code sai vì spec yếu, thì vá diff cũng không phải cách fix đúng. Workflow được thiết kế để chẩn đoán lỗi đã đi vào hệ thống từ tầng nào, quay lại đúng tầng đó, rồi sinh lại từ đấy.
Nếu triển khai sai vì ý định sai, vậy sửa code không phải cách sửa đúng. Nếu code sai vì đặc tả yếu, thì vá diff cũng không phải cách sửa đúng. Quy trình được thiết kế để chẩn đoán lỗi đã đi vào hệ thống từ tầng nào, quay lại đúng tầng đó, rồi sinh lại từ đấy.
Các phát hiện từ review được dùng để xác định vấn đề đến từ ý định, quá trình tạo spec, hay triển khai cục bộ. Chỉ những lỗi thật sự cục bộ mới được sửa tại chỗ.
Các phát hiện từ bước rà soát được dùng để xác định vấn đề đến từ ý định, quá trình tạo đặc tả, hay triển khai cục bộ. Chỉ những lỗi thật sự cục bộ mới được sửa tại chỗ.
### 5. Chỉ đưa con người quay lại khi cần
Bước interview ý định có human-in-the-loop, nhưng nó không giống một checkpoint lặp đi lặp lại. Workflow cố gắng giảm thiểu những checkpoint lặp lại đó. Sau bước định hình ý định ban đầu, con người chủ yếu quay lại khi workflow không thể tiếp tục an toàn nếu thiếu phán đoán, và ở cuối quy trình để review kết quả.
Bước phỏng vấn ý định có người trong vòng lặp, nhưng nó không giống một điểm kiểm tra lặp đi lặp lại. Quy trình cố gắng giảm thiểu những điểm kiểm tra lặp lại đó. Sau bước định hình ý định ban đầu, con người chủ yếu quay lại khi quy trình không thể tiếp tục an toàn nếu thiếu phán đoán, và ở cuối quy trình để rà soát kết quả.
- **Xử lý khoảng trống của ý định** - quay lại khi review cho thấy workflow không thể suy ra an toàn điều được hàm ý
Mọi thứ còn lại đều là ứng viên cho việc thực thi tự chủ lâu hơn. Đánh đổi này là có chủ đích. Các pattern cũ tốn nhiều sự chú ý của con người cho việc giám sát liên tục. Quick Dev đặt nhiều niềm tin hơn vào model, nhưng để dành sự chú ý của con người cho những thời điểm mà lý trí con người có đòn bẩy lớn nhất.
Mọi thứ còn lại đều là ứng viên cho việc thực thi tự chủ lâu hơn. Đánh đổi này là có chủ đích. Các mẫu cũ tốn nhiều sự chú ý của con người cho việc giám sát liên tục. Quick Dev đặt nhiều niềm tin hơn vào mô hình, nhưng để dành sự chú ý của con người cho những thời điểm mà lý trí con người có đòn bẩy lớn nhất.
## Vì sao hệ thống review quan trọng
Giai đoạn review không chỉ để tìm bug. Nó còn để định tuyến cách sửa mà không phá hỏng động lượng.
Giai đoạn rà soát không chỉ để tìm lỗi. Nó còn để định tuyến cách sửa mà không phá hỏng động lượng.
Workflow này hoạt động tốt nhất trên nền tảng có thể spawn subagent, hoặc ít nhất gọi được một LLM khác qua dòng lệnh và đợi kết quả. Nếu nền tảng của bạn không hỗ trợ sẵn, bạn có thể thêm skill để làm việc đó. Các subagent không mang context là một trụ cột trong thiết kế review.
Quy trình này hoạt động tốt nhất trên nền tảng có thể tạo subagent, hoặc ít nhất gọi được một LLM khác qua dòng lệnh và đợi kết quả. Nếu nền tảng của bạn không hỗ trợ sẵn, bạn có thể thêm skill để làm việc đó. Các subagent không mang ngữ cảnh là một trụ cột trong thiết kế rà soát.
Review agentic thường sai theo hai cách:
Rà soát kiểu agent thường sai theo hai cách:
- Tạo quá nhiều phát hiện, buộc con người lọc quá nhiều nhiễu.
- Làm lệch thay đổi hiện tại bằng cách kéo vào các vấn đề không liên quan, biến mỗi lần chạy thành một dự án dọn dẹp ad-hoc.
- Làm lệch thay đổi hiện tại bằng cách kéo vào các vấn đề không liên quan, biến mỗi lần chạy thành một dự án dọn dẹp chắp vá.
Quick Dev xử lý cả hai bằng cách coi review là triage.
Quick Dev xử lý cả hai bằng cách coi rà soát là bước phân loại.
Có những phát hiện thuộc về thay đổi hiện tại. Có những phát hiện không thuộc về nó. Nếu một phát hiện chỉ là ngẫu nhiên xuất hiện, không gắn nhân quả với thay đổi đang làm, workflow có thể trì hoãn nó thay vì ép con người xử lý ngay. Điều đó giữ cho mỗi lần chạy tập trung và ngăn các ngả rẽ ngẫu nhiên ăn hết ngân sách chú ý.
Có những phát hiện thuộc về thay đổi hiện tại. Có những phát hiện không thuộc về nó. Nếu một phát hiện chỉ là ngẫu nhiên xuất hiện, không gắn nhân quả với thay đổi đang làm, quy trình có thể trì hoãn nó thay vì ép con người xử lý ngay. Điều đó giữ cho mỗi lần chạy tập trung và ngăn các ngả rẽ ngẫu nhiên ăn hết ngân sách chú ý.
Quá trình triage này đôi khi sẽ không hoàn hảo. Điều đó chấp nhận được. Thường tốt hơn khi đánh giá sai một số phát hiện còn hơn là nhận về hàng ngàn bình luận review giá trị thấp. Hệ thống tối ưu cho chất lượng tín hiệu, không phải độ phủ tuyệt đối.

View File

@ -5,79 +5,27 @@ sidebar:
order: 4
---
## Bắt đầu tại đây: BMad-Help
Hãy dùng trợ giúp tích hợp sẵn của BMad, tài liệu nguồn, hoặc cộng đồng để tìm câu trả lời, theo thứ tự từ nhanh nhất đến đầy đủ nhất.
**Cách nhanh nhất để tìm câu trả lời về BMad là dùng skill `bmad-help`.** Đây là công cụ hướng dẫn thông minh có thể trả lời hơn 80% các câu hỏi và có sẵn ngay trong IDE khi bạn làm việc.
## 1. Hỏi BMad-Help
BMad-Help không chỉ là công cụ tra cứu, nó còn:
- **Kiểm tra dự án của bạn** để xem những gì đã hoàn thành
- **Hiểu ngôn ngữ tự nhiên** - đặt câu hỏi bằng ngôn ngữ bình thường
- **Thay đổi theo module đã cài** - hiển thị các lựa chọn liên quan
- **Tự động chạy sau workflow** - nói rõ bạn cần làm gì tiếp theo
- **Đề xuất tác vụ đầu tiên cần thiết** - không cần đoán nên bắt đầu từ đâu
### Cách dùng BMad-Help
Gọi nó trực tiếp trong phiên AI của bạn:
Cách nhanh nhất để có câu trả lời. Skill `bmad-help` có sẵn ngay trong phiên AI của bạn và xử lý được hơn 80% câu hỏi. Nó sẽ kiểm tra dự án, nhìn xem bạn đã hoàn thành đến đâu và cho bạn biết nên làm gì tiếp theo.
```text
bmad-help
bmad-help Tôi có ý tưởng SaaS và đã biết tất cả tính năng. Tôi nên bắt đầu từ đâu?
bmad-help Tôi có những lựa chọn nào cho thiết kế UX?
bmad-help Tôi đang bị mắc ở workflow PRD
```
:::tip
Bạn cũng có thể dùng `/bmad-help` hoặc `$bmad-help` tùy nền tảng, nhưng chỉ `bmad-help` là cách nên hoạt động mọi nơi.
:::
Kết hợp với câu hỏi ngôn ngữ tự nhiên:
## 2. Đi sâu hơn với mã nguồn
```text
bmad-help Tôi có ý tưởng SaaS và đã biết tất cả tính năng. Tôi nên bắt đầu từ đâu?
bmad-help Tôi có những lựa chọn nào cho thiết kế UX?
bmad-help Tôi đang bị mắc ở workflow PRD
bmad-help Cho tôi xem tôi đã làm được gì đến giờ
```
BMad-Help dựa trên cấu hình bạn đã cài đặt. Nếu bạn cần tìm hiểu nội bộ, lịch sử, hay kiến trúc của BMad, hoặc đang nghiên cứu BMad trước khi cài, hãy để AI đọc trực tiếp mã nguồn.
BMad-Help sẽ trả lời:
- Điều gì được khuyến nghị cho tình huống của bạn
- Tác vụ đầu tiên cần thiết là gì
- Phần còn lại của quy trình trông thế nào
## Khi nào nên dùng tài liệu này
Hãy xem phần này khi:
- Bạn muốn hiểu kiến trúc hoặc nội bộ của BMad
- Bạn cần câu trả lời nằm ngoài phạm vi BMad-Help cung cấp
- Bạn đang nghiên cứu BMad trước khi cài đặt
- Bạn muốn tự khám phá source code trực tiếp
## Các bước thực hiện
### 1. Chọn nguồn thông tin
| Nguồn | Phù hợp nhất cho | Ví dụ |
| --- | --- | --- |
| **Thư mục `_bmad`** | Cách BMad vận hành: agent, workflow, prompt | "PM agent làm gì?" |
| **Toàn bộ repo GitHub** | Lịch sử, installer, kiến trúc | "v6 thay đổi gì?" |
| **`llms-full.txt`** | Tổng quan nhanh từ tài liệu | "Giải thích bốn giai đoạn của BMad" |
Thư mục `_bmad` được tạo khi bạn cài đặt BMad. Nếu chưa có, hãy clone repo thay thế.
### 2. Cho AI của bạn truy cập nguồn thông tin
**Nếu AI của bạn đọc được tệp (Claude Code, Cursor, ...):**
- **Đã cài BMad:** Trỏ đến thư mục `_bmad` và hỏi trực tiếp
- **Cần bối cảnh sâu hơn:** Clone [repo đầy đủ](https://github.com/bmad-code-org/BMAD-METHOD)
**Nếu bạn dùng ChatGPT hoặc Claude.ai:**
Nạp `llms-full.txt` vào phiên làm việc:
```text
https://bmad-code-org.github.io/BMAD-METHOD/llms-full.txt
```
### 3. Đặt câu hỏi
Hãy clone hoặc mở [repo BMAD-METHOD](https://github.com/bmad-code-org/BMAD-METHOD) rồi hỏi AI của bạn về nó. Bất kỳ công cụ nào có hỗ trợ agent như Claude Code, Cursor, Windsurf... đều có thể đọc mã nguồn và trả lời trực tiếp.
:::note[Ví dụ]
**Q:** "Hãy chỉ tôi cách nhanh nhất để xây dựng một thứ gì đó bằng BMad"
@ -85,29 +33,27 @@ https://bmad-code-org.github.io/BMAD-METHOD/llms-full.txt
**A:** Dùng Quick Flow: Chạy `bmad-quick-dev` - nó sẽ làm rõ ý định, lập kế hoạch, triển khai, review và trình bày kết quả trong một workflow duy nhất, bỏ qua các giai đoạn lập kế hoạch đầy đủ.
:::
## Bạn nhận được gì
**Mẹo để có câu trả lời tốt hơn:**
Các câu trả lời trực tiếp về BMad: agent hoạt động ra sao, workflow làm gì, tại sao cấu trúc lại được tổ chức như vậy, mà không cần chờ người khác trả lời.
- **Hãy hỏi thật cụ thể** - "Bước 3 trong workflow PRD làm gì?" sẽ tốt hơn "PRD hoạt động ra sao?"
- **Kiểm tra lại những câu trả lời nghe lạ** - LLM đôi khi vẫn sai. Hãy kiểm tra file nguồn hoặc hỏi trên Discord.
## Mẹo
### Không dùng agent? Dùng trang docs
- **Xác minh những câu trả lời gây bất ngờ** - LLM vẫn có lúc nhầm. Hãy kiểm tra tệp nguồn hoặc hỏi trên Discord.
- **Đặt câu hỏi cụ thể** - "Bước 3 trong workflow PRD làm gì?" tốt hơn "PRD hoạt động ra sao?"
Nếu AI của bạn không đọc được file cục bộ như ChatGPT hoặc Claude.ai, hãy nạp [llms-full.txt](https://bmad-code-org.github.io/BMAD-METHOD/llms-full.txt) vào phiên làm việc. Đây là bản chụp tài liệu BMad trong một file duy nhất.
## Vẫn bị mắc?
## 3. Hỏi người thật
Đã thử cách tiếp cận bằng LLM mà vẫn cần trợ giúp? Lúc này bạn đã có một câu hỏi tốt hơn để đem đi hỏi.
Nếu cả BMad-Help lẫn mã nguồn vẫn chưa trả lời được câu hỏi của bạn, lúc này bạn đã có một câu hỏi rõ hơn nhiều để đem đi hỏi cộng đồng.
| Kênh | Dùng cho |
| --- | --- |
| `#bmad-method-help` | Câu hỏi nhanh (trò chuyện thời gian thực) |
| `help-requests` forum | Câu hỏi chi tiết (có thể tìm lại, tồn tại lâu dài) |
| `help-requests` forum | Câu hỏi |
| `#suggestions-feedback` | Ý tưởng và đề xuất tính năng |
| `#report-bugs-and-issues` | Báo cáo lỗi |
**Discord:** [discord.gg/gk8jAdXWmj](https://discord.gg/gk8jAdXWmj)
**GitHub Issues:** [github.com/bmad-code-org/BMAD-METHOD/issues](https://github.com/bmad-code-org/BMAD-METHOD/issues) (dành cho các lỗi rõ ràng)
**GitHub Issues:** [github.com/bmad-code-org/BMAD-METHOD/issues](https://github.com/bmad-code-org/BMAD-METHOD/issues)
*Chính bạn,*
*đang mắc kẹt*

View File

@ -72,7 +72,7 @@ Trình cài đặt sẽ hiện các module có sẵn. Chọn những module bạ
### 5. Làm theo các prompt
Trình cài đặt sẽ hướng dẫn các bước còn lại - nội dung tùy chỉnh, cài đặt, và các tùy chọn khác.
Trình cài đặt sẽ hướng dẫn các bước còn lại - cài đặt, tích hợp công cụ, và các tùy chọn khác.
## Bạn nhận được gì

View File

@ -27,7 +27,6 @@ Yêu cầu [Node.js](https://nodejs.org) v20+ và `npx` (đi kèm với npm).
| `--directory <path>` | Thư mục cài đặt | `--directory ~/projects/myapp` |
| `--modules <modules>` | Danh sách ID module, cách nhau bởi dấu phẩy | `--modules bmm,bmb` |
| `--tools <tools>` | Danh sách ID công cụ/IDE, cách nhau bởi dấu phẩy (dùng `none` để bỏ qua) | `--tools claude-code,cursor` hoặc `--tools none` |
| `--custom-content <paths>` | Danh sách đường dẫn đến module tùy chỉnh, cách nhau bởi dấu phẩy | `--custom-content ~/my-module,~/another-module` |
| `--action <type>` | Hành động cho bản cài đặt hiện có: `install` (mặc định), `update`, hoặc `quick-update` | `--action quick-update` |
### Cấu hình cốt lõi
@ -120,16 +119,6 @@ npx bmad-method install \
--action quick-update
```
### Cài đặt với nội dung tùy chỉnh
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--modules bmm \
--custom-content ~/my-custom-module,~/another-module \
--tools claude-code
```
## Bạn nhận được gì
- Thư mục `_bmad/` đã được cấu hình đầy đủ trong dự án của bạn
@ -143,12 +132,11 @@ BMad sẽ kiểm tra tất cả các cờ được cung cấp:
- **Directory** - Phải là đường dẫn hợp lệ và có quyền ghi
- **Modules** - Cảnh báo nếu ID module không hợp lệ (nhưng không thất bại)
- **Tools** - Cảnh báo nếu ID công cụ không hợp lệ (nhưng không thất bại)
- **Custom Content** - Mỗi đường dẫn phải chứa tệp `module.yaml` hợp lệ
- **Action** - Phải là một trong: `install`, `update`, `quick-update`
Giá trị không hợp lệ sẽ dẫn đến một trong các trường hợp sau:
1. Hiện lỗi và thoát (với các tùy chọn quan trọng như directory)
2. Hiện cảnh báo và bỏ qua (với mục tùy chọn như custom content)
2. Hiện cảnh báo và bỏ qua (với mục tùy chọn)
3. Quay lại hỏi interactive (với giá trị bắt buộc bị thiếu)
:::tip[Thực hành tốt]
@ -172,13 +160,6 @@ Giá trị không hợp lệ sẽ dẫn đến một trong các trường hợp
- Xác minh ID module có đúng không
- Module bên ngoài phải có sẵn trong registry
### Đường dẫn custom content không hợp lệ
Đảm bảo mỗi đường dẫn custom content:
- Trỏ tới một thư mục
- Chứa tệp `module.yaml` ở cấp gốc
- Có trường `code` trong tệp `module.yaml`
:::note[Vẫn bị mắc?]
Chạy với `--debug` để xem output chi tiết, thử chế độ interactive để cô lập vấn đề, hoặc báo cáo tại <https://github.com/bmad-code-org/BMAD-METHOD/issues>.
:::

View File

@ -1,5 +1,5 @@
---
title: "Quản lý Project Context"
title: "Quản lý bối cảnh dự án"
description: Tạo và duy trì project-context.md để định hướng cho các agent AI
sidebar:
order: 8

View File

@ -1,5 +1,5 @@
---
title: "Quick Fixes"
title: "Sửa nhanh"
description: Cách thực hiện các sửa nhanh và thay đổi ad-hoc
sidebar:
order: 5

View File

@ -1,5 +1,5 @@
---
title: Agents
title: Các agent
description: Các agent mặc định của BMM cùng skill ID, trigger menu và workflow chính
sidebar:
order: 2

View File

@ -1,5 +1,5 @@
---
title: Skills
title: Các skill
description: Tài liệu tham chiếu cho skill của BMad — skill là gì, hoạt động ra sao và tìm ở đâu.
sidebar:
order: 3

View File

@ -1,31 +1,31 @@
---
title: Core Tools
description: Tài liệu tham chiếu cho mọi task và workflow tích hợp sẵn có trong mọi bản cài BMad mà không cần module bổ sung.
title: Công cụ cốt lõi
description: Tài liệu tham chiếu cho mọi tác vụ và quy trình tích hợp sẵn có trong mọi bản cài BMad mà không cần module bổ sung.
sidebar:
order: 2
---
Mọi bản cài BMad đều bao gồm một tập core skills có thể dùng cùng với bất cứ việc gì bạn đang làm — các task và workflow độc lập hoạt động xuyên suốt mọi dự án, mọi module và mọi phase. Chúng luôn có sẵn bất kể bạn cài những module tùy chọn nào.
Mọi bản cài BMad đều bao gồm một tập skill cốt lõi có thể dùng cùng với bất cứ việc gì bạn đang làm, các tác vụ và quy trình độc lập hoạt động xuyên suốt mọi dự án, mọi module và mọi giai đoạn. Chúng luôn có sẵn bất kể bạn cài những module tùy chọn nào.
:::tip[Lối đi nhanh]
Chạy bất kỳ core tool nào bằng cách gõ tên skill của nó, ví dụ `bmad-help`, trong IDE của bạn. Không cần mở phiên agent trước.
Chạy bất kỳ công cụ cốt lõi nào bằng cách gõ tên skill của nó, ví dụ `bmad-help`, trong IDE của bạn. Không cần mở phiên agent trước.
:::
## Tổng Quan
| Công cụ | Loại | Mục đích |
| --- | --- | --- |
| [`bmad-help`](#bmad-help) | Task | Nhận hướng dẫn có ngữ cảnh về việc nên làm gì tiếp theo |
| [`bmad-brainstorming`](#bmad-brainstorming) | Workflow | Tổ chức các phiên brainstorming có tương tác |
| [`bmad-party-mode`](#bmad-party-mode) | Workflow | Điều phối thảo luận nhóm nhiều agent |
| [`bmad-distillator`](#bmad-distillator) | Task | Nén tài liệu tối ưu cho LLM mà không mất thông tin |
| [`bmad-advanced-elicitation`](#bmad-advanced-elicitation) | Task | Đẩy đầu ra của LLM qua các vòng tinh luyện lặp |
| [`bmad-review-adversarial-general`](#bmad-review-adversarial-general) | Task | Review hoài nghi để tìm chỗ thiếu và chỗ sai |
| [`bmad-review-edge-case-hunter`](#bmad-review-edge-case-hunter) | Task | Phân tích toàn bộ nhánh rẽ để tìm edge case chưa được xử lý |
| [`bmad-editorial-review-prose`](#bmad-editorial-review-prose) | Task | Biên tập câu chữ nhằm tăng độ rõ ràng khi giao tiếp |
| [`bmad-editorial-review-structure`](#bmad-editorial-review-structure) | Task | Biên tập cấu trúc — cắt, gộp và tổ chức lại |
| [`bmad-shard-doc`](#bmad-shard-doc) | Task | Tách file markdown lớn thành các phần có tổ chức |
| [`bmad-index-docs`](#bmad-index-docs) | Task | Tạo hoặc cập nhật mục lục cho toàn bộ tài liệu trong một thư mục |
| [`bmad-help`](#bmad-help) | Tác vụ | Nhận hướng dẫn có ngữ cảnh về việc nên làm gì tiếp theo |
| [`bmad-brainstorming`](#bmad-brainstorming) | Quy trình | Tổ chức các phiên brainstorming có tương tác |
| [`bmad-party-mode`](#bmad-party-mode) | Quy trình | Điều phối thảo luận nhóm nhiều agent |
| [`bmad-distillator`](#bmad-distillator) | Tác vụ | Nén tài liệu tối ưu cho LLM mà không mất thông tin |
| [`bmad-advanced-elicitation`](#bmad-advanced-elicitation) | Tác vụ | Đẩy đầu ra của LLM qua các vòng tinh luyện lặp |
| [`bmad-review-adversarial-general`](#bmad-review-adversarial-general) | Tác vụ | Rà soát hoài nghi để tìm chỗ thiếu và chỗ sai |
| [`bmad-review-edge-case-hunter`](#bmad-review-edge-case-hunter) | Tác vụ | Phân tích toàn bộ nhánh rẽ để tìm trường hợp biên chưa được xử lý |
| [`bmad-editorial-review-prose`](#bmad-editorial-review-prose) | Tác vụ | Biên tập câu chữ nhằm tăng độ rõ ràng khi giao tiếp |
| [`bmad-editorial-review-structure`](#bmad-editorial-review-structure) | Tác vụ | Biên tập cấu trúc — cắt, gộp và tổ chức lại |
| [`bmad-shard-doc`](#bmad-shard-doc) | Tác vụ | Tách file markdown lớn thành các phần có tổ chức |
| [`bmad-index-docs`](#bmad-index-docs) | Tác vụ | Tạo hoặc cập nhật mục lục cho toàn bộ tài liệu trong một thư mục |
## bmad-help
@ -33,7 +33,7 @@ Chạy bất kỳ core tool nào bằng cách gõ tên skill của nó, ví dụ
**Dùng khi:**
- Bạn vừa hoàn tất một workflow và muốn biết tiếp theo là gì
- Bạn vừa hoàn tất một quy trình và muốn biết tiếp theo là gì
- Bạn mới làm quen với BMad và cần định hướng
- Bạn đang mắc kẹt và muốn lời khuyên có ngữ cảnh
- Bạn vừa cài module mới và muốn xem có gì khả dụng
@ -51,7 +51,7 @@ Chạy bất kỳ core tool nào bằng cách gõ tên skill của nó, ví dụ
## bmad-brainstorming
**Tạo ra nhiều ý tưởng đa dạng bằng các kỹ thuật sáng tạo có tương tác.** Đây là một phiên brainstorming có điều phối, nạp các phương pháp phát ý tưởng đã được kiểm chứng từ thư viện kỹ thuật và dẫn bạn đến 100+ ý tưởng trước khi bắt đầu sắp xếp.
**Tạo ra nhiều ý tưởng đa dạng bằng các kỹ thuật sáng tạo có tương tác.** Đây là một phiên động não có điều phối, nạp các phương pháp phát ý tưởng đã được kiểm chứng từ thư viện kỹ thuật và dẫn bạn đến 100+ ý tưởng trước khi bắt đầu sắp xếp.
**Dùng khi:**

View File

@ -1,17 +1,17 @@
---
title: "Workflow Map"
description: Tài liệu trực quan về các phase workflow và output của BMad Method
title: "Sơ đồ workflow"
description: Tài liệu trực quan về các giai đoạn, quy trình và đầu ra của BMad Method
sidebar:
order: 1
---
BMad Method (BMM) là một module trong hệ sinh thái BMad, tập trung vào các thực hành tốt nhất của context engineering và lập kế hoạch. AI agent hoạt động hiệu quả nhất khi có ngữ cảnh rõ ràng và có cấu trúc. Hệ thống BMM xây dựng ngữ cảnh đó theo tiến trình qua 4 phase riêng biệt. Mỗi phase, cùng với nhiều workflow tùy chọn bên trong phase đó, tạo ra các tài liệu làm đầu vào cho phase kế tiếp, nhờ vậy agent luôn biết phải xây gì và vì sao.
BMad Method (BMM) là một module trong hệ sinh thái BMad, tập trung vào các thực hành tốt nhất của kỹ nghệ ngữ cảnh và lập kế hoạch. AI agent hoạt động hiệu quả nhất khi có ngữ cảnh rõ ràng và có cấu trúc. Hệ thống BMM xây dựng ngữ cảnh đó theo tiến trình qua 4 giai đoạn riêng biệt. Mỗi giai đoạn, cùng với nhiều quy trình tùy chọn bên trong nó, tạo ra các tài liệu làm đầu vào cho giai đoạn kế tiếp, nhờ vậy agent luôn biết phải xây gì và vì sao.
Lý do và các khái niệm nền tảng ở đây đến từ các phương pháp agile đã được áp dụng rất thành công trong toàn ngành như một khung tư duy.
Lý do và các khái niệm nền tảng ở đây đến từ các phương pháp Agile đã được áp dụng rất thành công trong toàn ngành như một khung tư duy.
Nếu có lúc nào bạn không chắc nên làm gì, skill `bmad-help` sẽ giúp bạn giữ đúng hướng hoặc biết bước tiếp theo. Bạn vẫn có thể dùng trang này để tham chiếu, nhưng `bmad-help` mang tính tương tác đầy đủ và nhanh hơn nhiều nếu bạn đã cài BMad Method. Ngoài ra, nếu bạn đang dùng thêm các module mở rộng BMad Method hoặc các module bổ sung khác, `bmad-help` cũng sẽ phát triển theo để biết mọi thứ đang có sẵn và đưa ra lời khuyên tốt nhất tại thời điểm đó.
Nếu có lúc nào bạn không chắc nên làm gì, skill `bmad-help` sẽ giúp bạn giữ đúng hướng hoặc biết bước tiếp theo. Bạn vẫn có thể dùng trang này để tham chiếu, nhưng `bmad-help` mang tính tương tác đầy đủ và nhanh hơn nhiều nếu bạn đã cài BMad Method. Ngoài ra, nếu bạn đang dùng thêm các module mở rộng BMad Method hoặc các module bổ sung khác, `bmad-help` cũng sẽ mở rộng theo để biết mọi thứ đang có sẵn và đưa ra lời khuyên tốt nhất tại thời điểm đó.
Lưu ý quan trọng cuối cùng: mọi workflow dưới đây đều có thể chạy trực tiếp bằng công cụ bạn chọn thông qua skill, hoặc bằng cách nạp agent trước rồi chọn mục tương ứng trong menu agent.
Lưu ý quan trọng cuối cùng: mọi quy trình dưới đây đều có thể chạy trực tiếp bằng công cụ bạn chọn thông qua skill, hoặc bằng cách nạp agent trước rồi chọn mục tương ứng trong menu agent.
<iframe src="/workflow-map-diagram.html" title="Sơ đồ Workflow Map của BMad Method" width="100%" height="100%" style="border-radius: 8px; border: 1px solid #334155; min-height: 900px;"></iframe>
@ -19,43 +19,43 @@ Lưu ý quan trọng cuối cùng: mọi workflow dưới đây đều có thể
<a href="/workflow-map-diagram.html" target="_blank" rel="noopener noreferrer">Mở sơ đồ trong tab mới ↗</a>
</p>
## Phase 1: Analysis (Tùy chọn)
## Giai đoạn 1: Phân tích (tùy chọn)
Khám phá không gian vấn đề và xác nhận ý tưởng trước khi cam kết đi vào lập kế hoạch. [**Tìm hiểu từng công cụ làm gì và nên dùng khi nào**](../explanation/analysis-phase.md).
| Workflow | Mục đích | Tạo ra |
| Quy trình | Mục đích | Tạo ra |
| ------------------------------- | -------------------------------------------------------------------------- | ------------------------- |
| `bmad-brainstorming` | Brainstorm ý tưởng dự án với sự điều phối của brainstorming coach | `brainstorming-report.md` |
| `bmad-brainstorming` | Động não ý tưởng dự án với sự điều phối của người dẫn dắt brainstorming | `brainstorming-report.md` |
| `bmad-domain-research`, `bmad-market-research`, `bmad-technical-research` | Xác thực giả định về thị trường, kỹ thuật hoặc miền nghiệp vụ | Kết quả nghiên cứu |
| `bmad-product-brief` | Ghi lại tầm nhìn chiến lược — phù hợp nhất khi concept của bạn đã rõ | `product-brief.md` |
| `bmad-prfaq` | Working Backwards — stress-test và rèn sắc concept sản phẩm của bạn | `prfaq-{project}.md` |
## Phase 2: Planning
## Giai đoạn 2: Lập kế hoạch
Xác định cần xây gì và xây cho ai.
| Workflow | Mục đích | Tạo ra |
| Quy trình | Mục đích | Tạo ra |
| --------------------------- | ---------------------------------------- | ------------ |
| `bmad-create-prd` | Xác định yêu cầu (FR/NFR) | `PRD.md` |
| `bmad-create-ux-design` | Thiết kế trải nghiệm người dùng khi UX là yếu tố quan trọng | `ux-spec.md` |
## Phase 3: Solutioning
## Giai đoạn 3: Định hình giải pháp
Quyết định cách xây và chia nhỏ công việc thành stories.
Quyết định cách xây và chia nhỏ công việc thành các story.
| Workflow | Mục đích | Tạo ra |
| Quy trình | Mục đích | Tạo ra |
| ----------------------------------------- | ------------------------------------------ | --------------------------- |
| `bmad-create-architecture` | Làm rõ các quyết định kỹ thuật | `architecture.md` kèm ADR |
| `bmad-create-epics-and-stories` | Phân rã yêu cầu thành các phần việc có thể triển khai | Các file epic chứa stories |
| `bmad-create-epics-and-stories` | Phân rã yêu cầu thành các phần việc có thể triển khai | Các file epic chứa các story |
| `bmad-check-implementation-readiness` | Cổng kiểm tra trước khi triển khai | Quyết định PASS/CONCERNS/FAIL |
## Phase 4: Implementation
## Giai đoạn 4: Triển khai
Xây dựng từng story một. Tự động hóa toàn bộ phase 4 sẽ sớm ra mắt.
Xây dựng từng story một. Tự động hóa toàn bộ giai đoạn 4 sẽ sớm ra mắt.
| Workflow | Mục đích | Tạo ra |
| Quy trình | Mục đích | Tạo ra |
| -------------------------- | ------------------------------------------------------------------------ | -------------------------------- |
| `bmad-sprint-planning` | Khởi tạo theo dõi, thường chạy một lần mỗi dự án để sắp thứ tự chu trình dev | `sprint-status.yaml` |
| `bmad-sprint-planning` | Khởi tạo theo dõi, thường chạy một lần mỗi dự án để sắp thứ tự chu trình phát triển | `sprint-status.yaml` |
| `bmad-create-story` | Chuẩn bị story tiếp theo cho implementation | `story-[slug].md` |
| `bmad-dev-story` | Triển khai story | Code chạy được + tests |
| `bmad-code-review` | Kiểm tra chất lượng phần triển khai | Được duyệt hoặc yêu cầu thay đổi |
@ -63,22 +63,22 @@ Xây dựng từng story một. Tự động hóa toàn bộ phase 4 sẽ sớm
| `bmad-sprint-status` | Theo dõi tiến độ sprint và trạng thái story | Cập nhật trạng thái sprint |
| `bmad-retrospective` | Review sau khi hoàn tất epic | Bài học rút ra |
## Quick Flow (Nhánh Song Song)
## Luồng nhanh (nhánh song song)
Bỏ qua phase 1-3 đối với những việc nhỏ, rõ và đã hiểu đầy đủ.
Bỏ qua giai đoạn 1-3 đối với những việc nhỏ, rõ và đã hiểu đầy đủ.
| Workflow | Mục đích | Tạo ra |
| Quy trình | Mục đích | Tạo ra |
| ------------------ | --------------------------------------------------------------------------- | ---------------------- |
| `bmad-quick-dev` | Luồng nhanh hợp nhất — làm rõ yêu cầu, lập kế hoạch, triển khai, review và trình bày | `spec-*.md` + mã nguồn |
## Quản Lý Context
## Quản lý ngữ cảnh
Mỗi tài liệu sẽ trở thành context cho phase tiếp theo. PRD cho architect biết những ràng buộc nào quan trọng. Architecture chỉ cho dev agent những pattern cần tuân theo. File story cung cấp context tập trung và đầy đủ cho việc triển khai. Nếu không có cấu trúc này, agent sẽ đưa ra quyết định thiếu nhất quán.
Mỗi tài liệu sẽ trở thành ngữ cảnh cho giai đoạn tiếp theo. PRD cho architect biết những ràng buộc nào quan trọng. Tài liệu kiến trúc chỉ cho dev agent những mẫu cần tuân theo. File story cung cấp ngữ cảnh tập trung và đầy đủ cho việc triển khai. Nếu không có cấu trúc này, agent sẽ đưa ra quyết định thiếu nhất quán.
### Project Context
### Bối cảnh dự án
:::tip[Khuyến nghị]
Hãy tạo `project-context.md` để bảo đảm AI agent tuân theo quy tắc và sở thích của dự án. File này hoạt động như một bản hiến pháp cho dự án của bạn, nó dẫn dắt các quyết định triển khai xuyên suốt mọi workflow. File tùy chọn này có thể được tạo ở cuối bước Architecture Creation, hoặc cũng có thể được sinh trong dự án hiện hữu để ghi lại những điều quan trọng cần giữ đồng bộ với quy ước đang có.
Hãy tạo `project-context.md` để bảo đảm AI agent tuân theo quy tắc và sở thích của dự án. File này hoạt động như một bản hiến pháp cho dự án của bạn, nó dẫn dắt các quyết định triển khai xuyên suốt mọi quy trình. File tùy chọn này có thể được tạo ở cuối bước tạo kiến trúc, hoặc cũng có thể được sinh trong dự án hiện hữu để ghi lại những điều quan trọng cần giữ đồng bộ với quy ước đang có.
:::
**Cách tạo:**

View File

@ -72,7 +72,7 @@ npx github:bmad-code-org/BMAD-METHOD install
### 5. 按照提示操作
安装程序会引导你完成剩余步骤——自定义内容、设置等。
安装程序会引导你完成剩余步骤——设置、工具集成等。
## 你将获得

View File

@ -27,7 +27,6 @@ sidebar:
| `--directory <path>` | 安装目录 | `--directory ~/projects/myapp` |
| `--modules <modules>` | 逗号分隔的模块 ID | `--modules bmm,bmb` |
| `--tools <tools>` | 逗号分隔的工具/IDE ID使用 `none` 跳过) | `--tools claude-code,cursor``--tools none` |
| `--custom-content <paths>` | 逗号分隔的自定义模块路径 | `--custom-content ~/my-module,~/another-module` |
| `--action <type>` | 对现有安装的操作:`install`(默认)、`update` 或 `quick-update` | `--action quick-update` |
### 核心配置
@ -108,16 +107,6 @@ npx bmad-method install \
--action quick-update
```
### 使用自定义内容安装
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--modules bmm \
--custom-content ~/my-custom-module,~/another-module \
--tools claude-code
```
## 安装结果
- 项目中完全配置的 `_bmad/` 目录
@ -131,12 +120,11 @@ BMad 会验证你提供的所有参数:
- **目录** — 必须是具有写入权限的有效路径
- **模块** — 对无效的模块 ID 发出警告(但不会失败)
- **工具** — 对无效的工具 ID 发出警告(但不会失败)
- **自定义内容** — 每个路径必须包含有效的 `module.yaml` 文件
- **操作** — 必须是以下之一:`install`、`update`、`quick-update`
无效值将:
1. 显示错误并退出(对于目录等关键选项)
2. 显示警告并跳过(对于自定义内容等可选项目)
2. 显示警告并跳过(对于可选项目)
3. 回退到交互式提示(对于缺失的必需值)
:::tip[最佳实践]
@ -159,13 +147,6 @@ BMad 会验证你提供的所有参数:
- 验证模块 ID 是否正确
- 外部模块必须在注册表中可用
### 自定义内容路径无效
确保每个自定义内容路径:
- 指向一个目录
- 在根目录中包含 `module.yaml` 文件
- 在 `module.yaml` 中有 `code` 字段
:::note[仍然卡住了?]
使用 `--debug` 获取详细输出,尝试交互模式定位问题,或在 <https://github.com/bmad-code-org/BMAD-METHOD/issues> 提交反馈。
:::

View File

@ -0,0 +1,197 @@
# BMAD PRD Purpose
**The PRD is the top of the required funnel that feeds all subsequent product development work in rhw BMad Method.**
---
## What is a BMAD PRD?
A dual-audience document serving:
1. **Human Product Managers and builders** - Vision, strategy, stakeholder communication
2. **LLM Downstream Consumption** - UX Design → Architecture → Epics → Development AI Agents
Each successive document becomes more AI-tailored and granular.
---
## Core Philosophy: Information Density
**High Signal-to-Noise Ratio**
Every sentence must carry information weight. LLMs consume precise, dense content efficiently.
**Anti-Patterns (Eliminate These):**
- ❌ "The system will allow users to..." → ✅ "Users can..."
- ❌ "It is important to note that..." → ✅ State the fact directly
- ❌ "In order to..." → ✅ "To..."
- ❌ Conversational filler and padding → ✅ Direct, concise statements
**Goal:** Maximum information per word. Zero fluff.
---
## The Traceability Chain
**PRD starts the chain:**
```
Vision → Success Criteria → User Journeys → Functional Requirements → (future: User Stories)
```
**In the PRD, establish:**
- Vision → Success Criteria alignment
- Success Criteria → User Journey coverage
- User Journey → Functional Requirement mapping
- All requirements traceable to user needs
**Why:** Each downstream artifact (UX, Architecture, Epics, Stories) must trace back to documented user needs and business objectives. This chain ensures we build the right thing.
---
## What Makes Great Functional Requirements?
### FRs are Capabilities, Not Implementation
**Good FR:** "Users can reset their password via email link"
**Bad FR:** "System sends JWT via email and validates with database" (implementation leakage)
**Good FR:** "Dashboard loads in under 2 seconds for 95th percentile"
**Bad FR:** "Fast loading time" (subjective, unmeasurable)
### SMART Quality Criteria
**Specific:** Clear, precisely defined capability
**Measurable:** Quantifiable with test criteria
**Attainable:** Realistic within constraints
**Relevant:** Aligns with business objectives
**Traceable:** Links to source (executive summary or user journey)
### FR Anti-Patterns
**Subjective Adjectives:**
- ❌ "easy to use", "intuitive", "user-friendly", "fast", "responsive"
- ✅ Use metrics: "completes task in under 3 clicks", "loads in under 2 seconds"
**Implementation Leakage:**
- ❌ Technology names, specific libraries, implementation details
- ✅ Focus on capability and measurable outcomes
**Vague Quantifiers:**
- ❌ "multiple users", "several options", "various formats"
- ✅ "up to 100 concurrent users", "3-5 options", "PDF, DOCX, TXT formats"
**Missing Test Criteria:**
- ❌ "The system shall provide notifications"
- ✅ "The system shall send email notifications within 30 seconds of trigger event"
---
## What Makes Great Non-Functional Requirements?
### NFRs Must Be Measurable
**Template:**
```
"The system shall [metric] [condition] [measurement method]"
```
**Examples:**
- ✅ "The system shall respond to API requests in under 200ms for 95th percentile as measured by APM monitoring"
- ✅ "The system shall maintain 99.9% uptime during business hours as measured by cloud provider SLA"
- ✅ "The system shall support 10,000 concurrent users as measured by load testing"
### NFR Anti-Patterns
**Unmeasurable Claims:**
- ❌ "The system shall be scalable" → ✅ "The system shall handle 10x load growth through horizontal scaling"
- ❌ "High availability required" → ✅ "99.9% uptime as measured by cloud provider SLA"
**Missing Context:**
- ❌ "Response time under 1 second" → ✅ "API response time under 1 second for 95th percentile under normal load"
---
## Domain-Specific Requirements
**Auto-Detect and Enforce Based on Project Context**
Certain industries have mandatory requirements that must be present:
- **Healthcare:** HIPAA Privacy & Security Rules, PHI encryption, audit logging, MFA
- **Fintech:** PCI-DSS Level 1, AML/KYC compliance, SOX controls, financial audit trails
- **GovTech:** NIST framework, Section 508 accessibility (WCAG 2.1 AA), FedRAMP, data residency
- **E-Commerce:** PCI-DSS for payments, inventory accuracy, tax calculation by jurisdiction
**Why:** Missing these requirements in the PRD means they'll be missed in architecture and implementation, creating expensive rework. During PRD creation there is a step to cover this - during validation we want to make sure it was covered. For this purpose steps will utilize a domain-complexity.csv and project-types.csv.
---
## Document Structure (Markdown, Human-Readable)
### Required Sections
1. **Executive Summary** - Vision, differentiator, target users
2. **Success Criteria** - Measurable outcomes (SMART)
3. **Product Scope** - MVP, Growth, Vision phases
4. **User Journeys** - Comprehensive coverage
5. **Domain Requirements** - Industry-specific compliance (if applicable)
6. **Innovation Analysis** - Competitive differentiation (if applicable)
7. **Project-Type Requirements** - Platform-specific needs
8. **Functional Requirements** - Capability contract (FRs)
9. **Non-Functional Requirements** - Quality attributes (NFRs)
### Formatting for Dual Consumption
**For Humans:**
- Clear, professional language
- Logical flow from vision to requirements
- Easy for stakeholders to review and approve
**For LLMs:**
- ## Level 2 headers for all main sections (enables extraction)
- Consistent structure and patterns
- Precise, testable language
- High information density
---
## Downstream Impact
**How the PRD Feeds Next Artifacts:**
**UX Design:**
- User journeys → interaction flows
- FRs → design requirements
- Success criteria → UX metrics
**Architecture:**
- FRs → system capabilities
- NFRs → architecture decisions
- Domain requirements → compliance architecture
- Project-type requirements → platform choices
**Epics & Stories (created after architecture):**
- FRs → user stories (1 FR could map to 1-3 stories potentially)
- Acceptance criteria → story acceptance tests
- Priority → sprint sequencing
- Traceability → stories map back to vision
**Development AI Agents:**
- Precise requirements → implementation clarity
- Test criteria → automated test generation
- Domain requirements → compliance enforcement
- Measurable NFRs → performance targets
---
## Summary: What Makes a Great BMAD PRD?
**High Information Density** - Every sentence carries weight, zero fluff
**Measurable Requirements** - All FRs and NFRs are testable with specific criteria
**Clear Traceability** - Each requirement links to user need and business objective
**Domain Awareness** - Industry-specific requirements auto-detected and included
**Zero Anti-Patterns** - No subjective adjectives, implementation leakage, or vague quantifiers
**Dual Audience Optimized** - Human-readable AND LLM-consumable
**Markdown Format** - Professional, clean, accessible to all stakeholders
---
**Remember:** The PRD is the foundation. Quality here ripples through every subsequent phase. A dense, precise, well-traced PRD makes UX design, architecture, epic breakdown, and AI development dramatically more effective.

View File

@ -1,6 +1,6 @@
---
# File references (ONLY variables used in this step)
prdPurpose: '{project-root}/_bmad/bmm-skills/2-plan-workflows/bmad-create-prd/data/prd-purpose.md'
prdPurpose: '../data/prd-purpose.md'
---
# Step E-1: Discovery & Understanding

View File

@ -1,7 +1,7 @@
---
# File references (ONLY variables used in this step)
prdFile: '{prd_file_path}'
prdPurpose: '{project-root}/_bmad/bmm-skills/2-plan-workflows/bmad-create-prd/data/prd-purpose.md'
prdPurpose: '../data/prd-purpose.md'
---
# Step E-1B: Legacy PRD Conversion Assessment

View File

@ -2,7 +2,7 @@
# File references (ONLY variables used in this step)
prdFile: '{prd_file_path}'
validationReport: '{validation_report_path}' # If provided
prdPurpose: '{project-root}/_bmad/bmm-skills/2-plan-workflows/bmad-create-prd/data/prd-purpose.md'
prdPurpose: '../data/prd-purpose.md'
---
# Step E-2: Deep Review & Analysis

View File

@ -1,7 +1,7 @@
---
# File references (ONLY variables used in this step)
prdFile: '{prd_file_path}'
prdPurpose: '{project-root}/_bmad/bmm-skills/2-plan-workflows/bmad-create-prd/data/prd-purpose.md'
prdPurpose: '../data/prd-purpose.md'
---
# Step E-3: Edit & Update

View File

@ -1,7 +1,6 @@
---
# File references (ONLY variables used in this step)
prdFile: '{prd_file_path}'
validationWorkflow: '{project-root}/_bmad/bmm-skills/2-plan-workflows/bmad-validate-prd/steps-v/step-v-01-discovery.md'
---
# Step E-4: Complete & Validate
@ -117,8 +116,7 @@ Display:
- Display: "This will run all 13 validation checks on the updated PRD."
- Display: "Preparing to validate: {prd_file_path}"
- Display: "**Proceeding to validation...**"
- Read fully and follow: {validationWorkflow} (steps-v/step-v-01-discovery.md)
- Note: This hands off to the validation workflow which will run its complete 13-step process
- Invoke the `bmad-validate-prd` skill to run the complete validation workflow
- **IF E (Edit More):**
- Display: "**Additional Edits**"

View File

@ -1,5 +1,4 @@
---
wipFile: '{implementation_artifacts}/spec-wip.md'
deferred_work_file: '{implementation_artifacts}/deferred-work.md'
spec_file: '' # set at runtime for both routes before leaving this step
---
@ -21,7 +20,7 @@ Before listing artifacts or prompting the user, check whether you already know t
1. Explicit argument
Did the user pass a specific file path, spec name, or clear instruction this message?
- If it points to a file that matches the spec template (has `status` frontmatter with a recognized value: ready-for-dev, in-progress, or in-review) → set `spec_file` and **EARLY EXIT** to the appropriate step (step-03 for ready/in-progress, step-04 for review).
- If it points to a file that matches the spec template (has `status` frontmatter with a recognized value: draft, ready-for-dev, in-progress, in-review, or done) → set `spec_file` and **EARLY EXIT** to the appropriate step (step-02 for draft, step-03 for ready/in-progress, step-04 for review). For `done`, ingest as context and proceed to INSTRUCTIONS — do not resume.
- Anything else (intent files, external docs, plans, descriptions) → ingest it as starting intent and proceed to INSTRUCTIONS. Do not attempt to infer a workflow state from it.
2. Recent conversation
@ -29,8 +28,8 @@ Before listing artifacts or prompting the user, check whether you already know t
Use the same routing as above.
3. Otherwise — scan artifacts and ask
- `{wipFile}` exists? → Offer resume or archive.
- Active specs (`ready-for-dev`, `in-progress`, `in-review`) in `{implementation_artifacts}`? → List them and HALT. Ask user which to resume (or `[N]` for new).
- Active specs (`draft`, `ready-for-dev`, `in-progress`, `in-review`) in `{implementation_artifacts}`? → List them and HALT. Ask user which to resume (or `[N]` for new).
- If `draft` selected: Set `spec_file`. **EARLY EXIT**`./step-02-plan.md` (resume planning from the draft)
- If `ready-for-dev` or `in-progress` selected: Set `spec_file`. **EARLY EXIT**`./step-03-implement.md`
- If `in-review` selected: Set `spec_file`. **EARLY EXIT**`./step-04-review.md`
- Unformatted spec or intent file lacking `status` frontmatter? → Suggest treating its contents as the starting intent. Do NOT attempt to infer a state and resume it.
@ -65,7 +64,7 @@ Never ask extra questions if you already understand what the user intends.
- On **K**: Proceed as-is.
5. Route — choose exactly one:
Derive a valid kebab-case slug from the clarified intent. If the intent references a tracking identifier (story number, issue number, ticket ID), lead the slug with it (e.g. `3-2-digest-delivery`, `gh-47-fix-auth`). If `{implementation_artifacts}/spec-{slug}.md` already exists, append `-2`, `-3`, etc. Set `spec_file` = `{implementation_artifacts}/spec-{slug}.md`.
Derive a valid kebab-case slug from the clarified intent. If the intent references a tracking identifier (story number, issue number, ticket ID), lead the slug with it (e.g. `3-2-digest-delivery`, `gh-47-fix-auth`). If `{implementation_artifacts}/spec-{slug}.md` already exists: if its status is `draft`, treat it as the same work and resume it (set `spec_file` to that path, **EARLY EXIT**`./step-02-plan.md`); otherwise append `-2`, `-3`, etc. Set `spec_file` = `{implementation_artifacts}/spec-{slug}.md`.
**a) One-shot** — zero blast radius: no plausible path by which this change causes unintended consequences elsewhere. Clear intent, no architectural decisions.

View File

@ -1,5 +1,4 @@
---
wipFile: '{implementation_artifacts}/spec-wip.md'
deferred_work_file: '{implementation_artifacts}/deferred-work.md'
---
@ -12,11 +11,12 @@ deferred_work_file: '{implementation_artifacts}/deferred-work.md'
## INSTRUCTIONS
1. Investigate codebase. _Isolate deep exploration in sub-agents/tasks where available. To prevent context snowballing, instruct subagents to give you distilled summaries only._
2. Read `./spec-template.md` fully. Fill it out based on the intent and investigation, and write the result to `{wipFile}`.
3. Self-review against READY FOR DEVELOPMENT standard.
4. If intent gaps exist, do not fantasize, do not leave open questions, HALT and ask the human.
5. Token count check (see SCOPE STANDARD). If spec exceeds 1600 tokens:
1. Draft resume check. If `{spec_file}` exists with `status: draft`, read it and capture the verbatim `<frozen-after-approval>...</frozen-after-approval>` block as `preserved_intent`. Otherwise `preserved_intent` is empty.
2. Investigate codebase. _Isolate deep exploration in sub-agents/tasks where available. To prevent context snowballing, instruct subagents to give you distilled summaries only._
3. Read `./spec-template.md` fully. Fill it out based on the intent and investigation. If `{preserved_intent}` is non-empty, substitute it for the `<frozen-after-approval>` block in your filled spec before writing. Write the result to `{spec_file}`.
4. Self-review against READY FOR DEVELOPMENT standard.
5. If intent gaps exist, do not fantasize, do not leave open questions, HALT and ask the human.
6. Token count check (see SCOPE STANDARD). If spec exceeds 1600 tokens:
- Show user the token count.
- HALT and ask human: `[S] Split — carve off secondary goals` | `[K] Keep full spec — accept the risks`
- On **S**: Propose the split — name each secondary goal. Append deferred goals to `{deferred_work_file}`. Rewrite the current spec to cover only the main goal — do not surgically carve sections out; regenerate the spec for the narrowed scope. Continue to checkpoint.
@ -26,7 +26,7 @@ deferred_work_file: '{implementation_artifacts}/deferred-work.md'
Present summary. If token count exceeded 1600 and user chose [K], include the token count and explain why it may be a problem. HALT and ask human: `[A] Approve` | `[E] Edit`
- **A**: Rename `{wipFile}` to `{spec_file}`, set status `ready-for-dev`. Everything inside `<frozen-after-approval>` is now locked — only the human can change it. Display the finalized spec path to the user as a CWD-relative path (no leading `/`) so it is clickable in the terminal. → Step 3.
- **A**: Set status `ready-for-dev` in `{spec_file}`. Everything inside `<frozen-after-approval>` is now locked — only the human can change it. Display the finalized spec path to the user as a CWD-relative path (no leading `/`) so it is clickable in the terminal. → Step 3.
- **E**: Apply changes, then return to CHECKPOINT 1.

View File

@ -1,6 +1,5 @@
---
deferred_work_file: '{implementation_artifacts}/deferred-work.md'
spec_file: '' # set by step-01 before entering this step
---
# Step One-Shot: Implement, Review, Present

View File

@ -70,10 +70,6 @@ Load and read full config from `{main_config}` and resolve:
YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`.
### 2. Paths
- `wipFile` = `{implementation_artifacts}/spec-wip.md`
### 3. First Step Execution
### 2. First Step Execution
Read fully and follow: `./step-01-clarify-and-route.md` to begin the workflow.

View File

@ -1,154 +0,0 @@
/**
* install_to_bmad Flag Design Contract Tests
*
* Unit tests against the functions that implement the install_to_bmad flag.
* These nail down the 4 core design decisions:
*
* 1. true/omitted skill stays in _bmad/ (default behavior)
* 2. false skill removed from _bmad/ after IDE install
* 3. No platform no cleanup runs (cleanup lives in installVerbatimSkills)
* 4. Mixed flags each skill evaluated independently
*
* Usage: node test/test-install-to-bmad.js
*/
const path = require('node:path');
const os = require('node:os');
const fs = require('fs-extra');
const { loadSkillManifest, getInstallToBmad } = require('../tools/installer/ide/shared/skill-manifest');
// ANSI colors
const colors = {
reset: '\u001B[0m',
green: '\u001B[32m',
red: '\u001B[31m',
yellow: '\u001B[33m',
cyan: '\u001B[36m',
dim: '\u001B[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++;
}
}
async function runTests() {
console.log(`${colors.cyan}========================================`);
console.log('install_to_bmad — Design Contract Tests');
console.log(`========================================${colors.reset}\n`);
// ============================================================
// 1. true/omitted → getInstallToBmad returns true (keep in _bmad/)
// ============================================================
console.log(`${colors.yellow}Design decision 1: true or omitted → skill stays in _bmad/${colors.reset}\n`);
// Null manifest (no bmad-skill-manifest.yaml) → true
assert(getInstallToBmad(null, 'workflow.md') === true, 'null manifest defaults to true');
// Single-entry, flag omitted → true
assert(
getInstallToBmad({ __single: { type: 'skill' } }, 'workflow.md') === true,
'single-entry manifest with flag omitted defaults to true',
);
// Single-entry, explicit true → true
assert(
getInstallToBmad({ __single: { type: 'skill', install_to_bmad: true } }, 'workflow.md') === true,
'single-entry manifest with explicit true returns true',
);
console.log('');
// ============================================================
// 2. false → getInstallToBmad returns false (remove from _bmad/)
// ============================================================
console.log(`${colors.yellow}Design decision 2: false → skill removed from _bmad/${colors.reset}\n`);
// Single-entry, explicit false → false
assert(
getInstallToBmad({ __single: { type: 'skill', install_to_bmad: false } }, 'workflow.md') === false,
'single-entry manifest with explicit false returns false',
);
// loadSkillManifest round-trip: YAML with false is preserved through load
{
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-itb-'));
await fs.writeFile(path.join(tmpDir, 'bmad-skill-manifest.yaml'), 'type: skill\ninstall_to_bmad: false\n');
const loaded = await loadSkillManifest(tmpDir);
assert(getInstallToBmad(loaded, 'workflow.md') === false, 'loadSkillManifest preserves install_to_bmad: false through round-trip');
await fs.remove(tmpDir);
}
console.log('');
// ============================================================
// 3. No platform → cleanup only runs inside installVerbatimSkills
// (This is a design invariant: getInstallToBmad is only consulted
// during IDE install. Without a platform, the flag has no effect.)
// ============================================================
console.log(`${colors.yellow}Design decision 3: flag is a per-skill property, not a pipeline gate${colors.reset}\n`);
// The flag value is stored but doesn't trigger any side effects by itself.
// Cleanup is driven by reading the CSV column inside installVerbatimSkills.
// We verify the flag is just data — getInstallToBmad doesn't touch the filesystem.
{
const manifest = { __single: { type: 'skill', install_to_bmad: false } };
const result = getInstallToBmad(manifest, 'workflow.md');
assert(typeof result === 'boolean', 'getInstallToBmad returns a boolean (pure data, no side effects)');
assert(result === false, 'false value is faithfully returned for consumer to act on');
}
console.log('');
// ============================================================
// 4. Mixed flags → each skill evaluated independently
// ============================================================
console.log(`${colors.yellow}Design decision 4: mixed flags — each skill independent${colors.reset}\n`);
// Multi-entry manifest: different files can have different flags
{
const manifest = {
'workflow.md': { type: 'skill', install_to_bmad: false },
'other.md': { type: 'skill', install_to_bmad: true },
};
assert(getInstallToBmad(manifest, 'workflow.md') === false, 'multi-entry: workflow.md with false returns false');
assert(getInstallToBmad(manifest, 'other.md') === true, 'multi-entry: other.md with true returns true');
assert(getInstallToBmad(manifest, 'unknown.md') === true, 'multi-entry: unknown file defaults to true');
}
console.log('');
// ============================================================
// Summary
// ============================================================
console.log(`${colors.cyan}========================================`);
console.log('Results:');
console.log(` Passed: ${colors.green}${passed}${colors.reset}`);
console.log(` Failed: ${colors.red}${failed}${colors.reset}`);
console.log(`========================================${colors.reset}\n`);
if (failed === 0) {
console.log(`${colors.green}All install_to_bmad contract tests passed!${colors.reset}\n`);
process.exit(0);
} else {
console.log(`${colors.red}Some install_to_bmad contract tests failed${colors.reset}\n`);
process.exit(1);
}
}
runTests().catch((error) => {
console.error(`${colors.red}Test runner failed:${colors.reset}`, error.message);
console.error(error.stack);
process.exit(1);
});

View File

@ -59,8 +59,8 @@ async function createTestBmadFixture() {
await fs.writeFile(
path.join(fixtureDir, '_config', 'skill-manifest.csv'),
[
'canonicalId,name,description,module,path,install_to_bmad',
'"bmad-master","bmad-master","Minimal test agent fixture","core","_bmad/core/bmad-master/SKILL.md","true"',
'canonicalId,name,description,module,path',
'"bmad-master","bmad-master","Minimal test agent fixture","core","_bmad/core/bmad-master/SKILL.md"',
'',
].join('\n'),
);
@ -103,8 +103,8 @@ async function createSkillCollisionFixture() {
await fs.writeFile(
path.join(configDir, 'skill-manifest.csv'),
[
'canonicalId,name,description,module,path,install_to_bmad',
'"bmad-help","bmad-help","Native help skill","core","_bmad/core/tasks/bmad-help/SKILL.md","true"',
'canonicalId,name,description,module,path',
'"bmad-help","bmad-help","Native help skill","core","_bmad/core/tasks/bmad-help/SKILL.md"',
'',
].join('\n'),
);
@ -128,56 +128,6 @@ async function createSkillCollisionFixture() {
return { root: fixtureRoot, bmadDir: fixtureDir };
}
async function createCustomModuleManifestFixture() {
const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-custom-manifest-'));
const bmadDir = path.join(fixtureRoot, '_bmad');
const configDir = path.join(bmadDir, '_config');
const moduleSourceDir = path.join(fixtureRoot, 'test-module-source');
await fs.ensureDir(configDir);
await fs.ensureDir(moduleSourceDir);
const minimalAgent = '<agent name="Test" title="T"><persona>p</persona></agent>';
await fs.ensureDir(path.join(bmadDir, 'core', 'agents'));
await fs.writeFile(path.join(bmadDir, 'core', 'agents', 'test.md'), minimalAgent);
await fs.ensureDir(path.join(bmadDir, 'test-module', 'agents'));
await fs.writeFile(path.join(bmadDir, 'test-module', 'agents', 'test.md'), minimalAgent);
await fs.writeFile(path.join(moduleSourceDir, 'module.yaml'), ['code: test-module', 'name: Test Module', ''].join('\n'));
await fs.writeFile(
path.join(configDir, 'manifest.yaml'),
[
'installation:',
' version: 6.2.2',
' installDate: 2026-03-30T00:00:00.000Z',
' lastUpdated: 2026-03-30T00:00:00.000Z',
'modules:',
' - name: core',
' version: 6.2.2',
' installDate: 2026-03-30T00:00:00.000Z',
' lastUpdated: 2026-03-30T00:00:00.000Z',
' source: built-in',
' npmPackage: null',
' repoUrl: null',
' - name: test-module',
' version: null',
' installDate: 2026-03-30T00:00:00.000Z',
' lastUpdated: 2026-03-30T00:00:00.000Z',
' source: custom',
' npmPackage: null',
' repoUrl: null',
'customModules:',
' - id: test-module',
' name: "Test Module"',
` sourcePath: ${JSON.stringify(moduleSourceDir)}`,
'ides:',
' - codex',
'',
].join('\n'),
);
return { root: fixtureRoot, bmadDir, manifestPath: path.join(configDir, 'manifest.yaml'), moduleSourceDir };
}
/**
* Test Suite
*/
@ -1306,7 +1256,7 @@ async function runTests() {
const existingCsv27 = await fs.readFile(path.join(configDir27, 'skill-manifest.csv'), 'utf8');
await fs.writeFile(
path.join(configDir27, 'skill-manifest.csv'),
existingCsv27.trimEnd() + '\n"bmad-architect","bmad-architect","Architect","bmm","_bmad/bmm/agents/bmad-architect/SKILL.md","true"\n',
existingCsv27.trimEnd() + '\n"bmad-architect","bmad-architect","Architect","bmm","_bmad/bmm/agents/bmad-architect/SKILL.md"\n',
);
// Run Claude Code setup (which triggers cleanup then install)
@ -1773,107 +1723,6 @@ async function runTests() {
console.log('');
// ============================================================
// Suite 33: Main manifest preserves active customModules only
// ============================================================
console.log(`${colors.yellow}Test Suite 33: Preserve active customModules in main manifest${colors.reset}\n`);
let customManifestFixture = null;
try {
customManifestFixture = await createCustomModuleManifestFixture();
const yaml = require('yaml');
const originalManifest = yaml.parse(await fs.readFile(customManifestFixture.manifestPath, 'utf8'));
originalManifest.customModules.push({
id: 'removed-module',
name: 'Removed Module',
sourcePath: path.join(customManifestFixture.root, 'removed-module-source'),
});
await fs.writeFile(customManifestFixture.manifestPath, yaml.stringify(originalManifest), 'utf8');
const generator33 = new ManifestGenerator();
await generator33.generateManifests(customManifestFixture.bmadDir, ['core', 'test-module'], [], { ides: ['codex'] });
const updatedManifest = yaml.parse(await fs.readFile(customManifestFixture.manifestPath, 'utf8'));
const customModule = updatedManifest.customModules?.find((entry) => entry.id === 'test-module');
assert(Array.isArray(updatedManifest.customModules), 'Main manifest keeps customModules array');
assert(customModule !== undefined, 'Main manifest preserves existing custom module entry');
assert(
customModule && customModule.sourcePath === customManifestFixture.moduleSourceDir,
'Main manifest preserves custom module sourcePath',
);
assert(
!updatedManifest.customModules?.some((entry) => entry.id === 'removed-module'),
'Main manifest drops stale custom module entries',
);
} catch (error) {
assert(false, 'Main manifest preserves customModules test succeeds', error.message);
} finally {
if (customManifestFixture?.root) await fs.remove(customManifestFixture.root).catch(() => {});
}
console.log('');
// ============================================================
// Suite 34: Quick update uses manifest-backed custom sources
// ============================================================
console.log(`${colors.yellow}Test Suite 34: Quick update uses manifest-backed custom module sources${colors.reset}\n`);
let quickUpdateFixture = null;
const originalListAvailable34 = OfficialModules.prototype.listAvailable;
const originalLoadExistingConfig34 = OfficialModules.prototype.loadExistingConfig;
const originalCollectModuleConfigQuick34 = OfficialModules.prototype.collectModuleConfigQuick;
try {
quickUpdateFixture = await createCustomModuleManifestFixture();
const installer34 = new Installer();
installer34.externalModuleManager.hasModule = async () => false;
installer34.externalModuleManager.listAvailable = async () => [];
let capturedInstallConfig34 = null;
installer34.install = async (config) => {
capturedInstallConfig34 = config;
return { success: true };
};
OfficialModules.prototype.listAvailable = async function () {
return { modules: [], customModules: [] };
};
OfficialModules.prototype.loadExistingConfig = async function () {
this.collectedConfig = this.collectedConfig || {};
};
OfficialModules.prototype.collectModuleConfigQuick = async function (moduleName) {
this.collectedConfig = this.collectedConfig || {};
if (!this.collectedConfig[moduleName]) {
this.collectedConfig[moduleName] = {};
}
return false;
};
await installer34.quickUpdate({
directory: quickUpdateFixture.root,
skipPrompts: true,
});
const customModule34 = capturedInstallConfig34?._customModuleSources?.get('test-module');
assert(capturedInstallConfig34 !== null, 'Quick update forwards config to install');
assert(customModule34 !== undefined, 'Quick update keeps manifest-backed custom module updateable');
assert(customModule34 && customModule34.cached === false, 'Quick update uses manifest-backed source before cache');
assert(
customModule34 && customModule34.sourcePath === quickUpdateFixture.moduleSourceDir,
'Quick update uses preserved manifest sourcePath for custom modules',
);
} catch (error) {
assert(false, 'Quick update manifest-backed custom source test succeeds', error.message);
} finally {
OfficialModules.prototype.listAvailable = originalListAvailable34;
OfficialModules.prototype.loadExistingConfig = originalLoadExistingConfig34;
OfficialModules.prototype.collectModuleConfigQuick = originalCollectModuleConfigQuick34;
if (quickUpdateFixture?.root) await fs.remove(quickUpdateFixture.root).catch(() => {});
}
console.log('');
// ============================================================
// Summary
// ============================================================

View File

@ -17,7 +17,6 @@ module.exports = {
'--tools <tools>',
'Comma-separated list of tool/IDE IDs to configure (e.g., "claude-code,cursor"). Use "none" to skip tool configuration.',
],
['--custom-content <paths>', 'Comma-separated list of paths to custom modules/agents/workflows'],
['--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)'],

View File

@ -1,260 +0,0 @@
/**
* Custom Module Source Cache
* Caches custom module sources under _config/custom/ to ensure they're never lost
* and can be checked into source control
*/
const fs = require('fs-extra');
const path = require('node:path');
const crypto = require('node:crypto');
const prompts = require('../prompts');
class CustomModuleCache {
constructor(bmadDir) {
this.bmadDir = bmadDir;
this.customCacheDir = path.join(bmadDir, '_config', 'custom');
this.manifestPath = path.join(this.customCacheDir, 'cache-manifest.yaml');
}
/**
* Ensure the custom cache directory exists
*/
async ensureCacheDir() {
await fs.ensureDir(this.customCacheDir);
}
/**
* Get cache manifest
*/
async getCacheManifest() {
if (!(await fs.pathExists(this.manifestPath))) {
return {};
}
const content = await fs.readFile(this.manifestPath, 'utf8');
const yaml = require('yaml');
return yaml.parse(content) || {};
}
/**
* Update cache manifest
*/
async updateCacheManifest(manifest) {
const yaml = require('yaml');
// Clean the manifest to remove any non-serializable values
const cleanManifest = structuredClone(manifest);
const content = yaml.stringify(cleanManifest, {
indent: 2,
lineWidth: 0,
sortKeys: false,
});
await fs.writeFile(this.manifestPath, content);
}
/**
* Stream a file into the hash to avoid loading entire file into memory
*/
async hashFileStream(filePath, hash) {
return new Promise((resolve, reject) => {
const stream = require('node:fs').createReadStream(filePath);
stream.on('data', (chunk) => hash.update(chunk));
stream.on('end', resolve);
stream.on('error', reject);
});
}
/**
* Calculate hash of a file or directory using streaming to minimize memory usage
*/
async calculateHash(sourcePath) {
const hash = crypto.createHash('sha256');
const isDir = (await fs.stat(sourcePath)).isDirectory();
if (isDir) {
// For directories, hash all files
const files = [];
async function collectFiles(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isFile()) {
files.push(path.join(dir, entry.name));
} else if (entry.isDirectory() && !entry.name.startsWith('.')) {
await collectFiles(path.join(dir, entry.name));
}
}
}
await collectFiles(sourcePath);
files.sort(); // Ensure consistent order
for (const file of files) {
const relativePath = path.relative(sourcePath, file);
// Hash the path first, then stream file contents
hash.update(relativePath + '|');
await this.hashFileStream(file, hash);
}
} else {
// For single files, stream directly into hash
await this.hashFileStream(sourcePath, hash);
}
return hash.digest('hex');
}
/**
* Cache a custom module source
* @param {string} moduleId - Module ID
* @param {string} sourcePath - Original source path
* @param {Object} metadata - Additional metadata to store
* @returns {Object} Cached module info
*/
async cacheModule(moduleId, sourcePath, metadata = {}) {
await this.ensureCacheDir();
const cacheDir = path.join(this.customCacheDir, moduleId);
const cacheManifest = await this.getCacheManifest();
// Check if already cached and unchanged
if (cacheManifest[moduleId]) {
const cached = cacheManifest[moduleId];
if (cached.originalHash && cached.originalHash === (await this.calculateHash(sourcePath))) {
// Source unchanged, return existing cache info
return {
moduleId,
cachePath: cacheDir,
...cached,
};
}
}
// Remove existing cache if it exists
if (await fs.pathExists(cacheDir)) {
await fs.remove(cacheDir);
}
// Copy module to cache
await fs.copy(sourcePath, cacheDir, {
filter: (src) => {
const relative = path.relative(sourcePath, src);
// Skip node_modules, .git, and other common ignore patterns
return !relative.includes('node_modules') && !relative.startsWith('.git') && !relative.startsWith('.DS_Store');
},
});
// Calculate hash of the source
const sourceHash = await this.calculateHash(sourcePath);
const cacheHash = await this.calculateHash(cacheDir);
// Update manifest - don't store absolute paths for portability
// Clean metadata to remove absolute paths
const cleanMetadata = { ...metadata };
if (cleanMetadata.sourcePath) {
delete cleanMetadata.sourcePath;
}
cacheManifest[moduleId] = {
originalHash: sourceHash,
cacheHash: cacheHash,
cachedAt: new Date().toISOString(),
...cleanMetadata,
};
await this.updateCacheManifest(cacheManifest);
return {
moduleId,
cachePath: cacheDir,
...cacheManifest[moduleId],
};
}
/**
* Get cached module info
* @param {string} moduleId - Module ID
* @returns {Object|null} Cached module info or null
*/
async getCachedModule(moduleId) {
const cacheManifest = await this.getCacheManifest();
const cached = cacheManifest[moduleId];
if (!cached) {
return null;
}
const cacheDir = path.join(this.customCacheDir, moduleId);
if (!(await fs.pathExists(cacheDir))) {
// Cache dir missing, remove from manifest
delete cacheManifest[moduleId];
await this.updateCacheManifest(cacheManifest);
return null;
}
// Verify cache integrity
const currentCacheHash = await this.calculateHash(cacheDir);
if (currentCacheHash !== cached.cacheHash) {
await prompts.log.warn(`Cache integrity check failed for ${moduleId}`);
}
return {
moduleId,
cachePath: cacheDir,
...cached,
};
}
/**
* Get all cached modules
* @returns {Array} Array of cached module info
*/
async getAllCachedModules() {
const cacheManifest = await this.getCacheManifest();
const cached = [];
for (const [moduleId, info] of Object.entries(cacheManifest)) {
const cachedModule = await this.getCachedModule(moduleId);
if (cachedModule) {
cached.push(cachedModule);
}
}
return cached;
}
/**
* Remove a cached module
* @param {string} moduleId - Module ID to remove
*/
async removeCachedModule(moduleId) {
const cacheManifest = await this.getCacheManifest();
const cacheDir = path.join(this.customCacheDir, moduleId);
// Remove cache directory
if (await fs.pathExists(cacheDir)) {
await fs.remove(cacheDir);
}
// Remove from manifest
delete cacheManifest[moduleId];
await this.updateCacheManifest(cacheManifest);
}
/**
* Sync cached modules with a list of module IDs
* @param {Array<string>} moduleIds - Module IDs to keep
*/
async syncCache(moduleIds) {
const cached = await this.getAllCachedModules();
for (const cachedModule of cached) {
if (!moduleIds.includes(cachedModule.moduleId)) {
await this.removeCachedModule(cachedModule.moduleId);
}
}
}
}
module.exports = { CustomModuleCache };

View File

@ -10,14 +10,13 @@ const { Manifest } = require('./manifest');
class ExistingInstall {
#version;
constructor({ installed, version, hasCore, modules, ides, customModules }) {
constructor({ installed, version, hasCore, modules, ides }) {
this.installed = installed;
this.#version = version;
this.hasCore = hasCore;
this.modules = Object.freeze(modules.map((m) => Object.freeze({ ...m })));
this.moduleIds = Object.freeze(this.modules.map((m) => m.id));
this.ides = Object.freeze([...ides]);
this.customModules = Object.freeze([...customModules]);
Object.freeze(this);
}
@ -35,7 +34,6 @@ class ExistingInstall {
hasCore: false,
modules: [],
ides: [],
customModules: [],
});
}
@ -53,15 +51,11 @@ class ExistingInstall {
let hasCore = false;
const modules = [];
let ides = [];
let customModules = [];
const manifest = new Manifest();
const manifestData = await manifest.read(bmadDir);
if (manifestData) {
version = manifestData.version;
if (manifestData.customModules) {
customModules = manifestData.customModules;
}
if (manifestData.ides) {
ides = manifestData.ides.filter((ide) => ide && typeof ide === 'string');
}
@ -120,7 +114,7 @@ class ExistingInstall {
return ExistingInstall.empty();
}
return new ExistingInstall({ installed, version, hasCore, modules, ides, customModules });
return new ExistingInstall({ installed, version, hasCore, modules, ides });
}
}

View File

@ -20,14 +20,12 @@ class InstallPaths {
const configDir = path.join(bmadDir, '_config');
const agentsDir = path.join(configDir, 'agents');
const customCacheDir = path.join(configDir, 'custom');
const coreDir = path.join(bmadDir, 'core');
for (const [dir, label] of [
[bmadDir, 'bmad directory'],
[configDir, 'config directory'],
[agentsDir, 'agents config directory'],
[customCacheDir, 'custom modules cache'],
[coreDir, 'core module directory'],
]) {
await ensureWritableDir(dir, label);
@ -40,7 +38,6 @@ class InstallPaths {
bmadDir,
configDir,
agentsDir,
customCacheDir,
coreDir,
isUpdate,
});

View File

@ -2,7 +2,6 @@ const path = require('node:path');
const fs = require('fs-extra');
const { Manifest } = require('./manifest');
const { OfficialModules } = require('../modules/official-modules');
const { CustomModules } = require('../modules/custom-modules');
const { IdeManager } = require('../ide/manager');
const { FileOps } = require('../file-ops');
const { Config } = require('./config');
@ -19,7 +18,6 @@ class Installer {
constructor() {
this.externalModuleManager = new ExternalModuleManager();
this.manifest = new Manifest();
this.customModules = new CustomModules();
this.ideManager = new IdeManager();
this.fileOps = new FileOps();
this.installedFiles = new Set(); // Track all installed files
@ -80,8 +78,6 @@ class Installer {
const officialModules = await OfficialModules.build(config, paths);
const existingInstall = await ExistingInstall.detect(paths.bmadDir);
await this.customModules.discoverPaths(originalConfig, paths);
if (existingInstall.installed) {
await this._removeDeselectedModules(existingInstall, config, paths);
updateState = await this._prepareUpdateState(paths, config, existingInstall, officialModules);
@ -121,17 +117,16 @@ class Installer {
}
}
await this._cacheCustomModules(paths, addResult);
const allModules = config.modules || [];
// Compute module lists: official = selected minus custom, all = both
const customModuleIds = new Set(this.customModules.paths.keys());
const officialModuleIds = (config.modules || []).filter((m) => !customModuleIds.has(m));
const allModules = [...officialModuleIds, ...[...customModuleIds].filter((id) => !officialModuleIds.includes(id))];
await this._installAndConfigure(config, originalConfig, paths, officialModuleIds, allModules, addResult, officialModules);
await this._installAndConfigure(config, originalConfig, paths, allModules, allModules, addResult, officialModules);
await this._setupIdes(config, allModules, paths, addResult, previousSkillIds);
// Skills are now in IDE directories — remove redundant copies from _bmad/.
// Also cleans up skill dirs left by older installer versions.
await this._cleanupSkillDirs(paths.bmadDir);
const restoreResult = await this._restoreUserFiles(paths, updateState);
// Render consolidated summary
@ -238,26 +233,6 @@ class Installer {
}
}
/**
* Cache custom modules into the local cache directory.
* Updates this.customModules.paths in place with cached locations.
*/
async _cacheCustomModules(paths, addResult) {
if (!this.customModules.paths || this.customModules.paths.size === 0) return;
const { CustomModuleCache } = require('./custom-module-cache');
const customCache = new CustomModuleCache(paths.bmadDir);
for (const [moduleId, sourcePath] of this.customModules.paths) {
const cachedInfo = await customCache.cacheModule(moduleId, sourcePath, {
sourcePath: sourcePath,
});
this.customModules.paths.set(moduleId, cachedInfo.cachePath);
}
addResult('Custom modules cached', 'ok');
}
/**
* Install modules, create directories, generate configs and manifests.
*/
@ -280,11 +255,6 @@ class Installer {
installedModuleNames,
});
await this._installCustomModules(config, paths, addResult, officialModules, {
message,
installedModuleNames,
});
return `${allModules.length} module(s) ${isQuickUpdate ? 'updated' : 'installed'}`;
},
});
@ -413,6 +383,33 @@ class Installer {
}
}
/**
* Remove skill directories from _bmad/ after IDE installation.
* Skills are self-contained in IDE directories, so _bmad/ only needs
* module-level files (config.yaml, _config/, etc.).
* Also cleans up skill dirs left by older installer versions.
* @param {string} bmadDir - BMAD installation directory
*/
async _cleanupSkillDirs(bmadDir) {
const csv = require('csv-parse/sync');
const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
if (!(await fs.pathExists(csvPath))) return;
const csvContent = await fs.readFile(csvPath, 'utf8');
const records = csv.parse(csvContent, { columns: true, skip_empty_lines: true });
const bmadFolderName = path.basename(bmadDir);
const bmadPrefix = bmadFolderName + '/';
for (const record of records) {
if (!record.path) continue;
const relativePath = record.path.startsWith(bmadPrefix) ? record.path.slice(bmadPrefix.length) : record.path;
const sourceDir = path.dirname(path.join(bmadDir, relativePath));
if (await fs.pathExists(sourceDir)) {
await fs.remove(sourceDir);
}
}
}
/**
* Restore custom and modified files that were backed up before the update.
* No-op for fresh installs (updateState is null).
@ -484,48 +481,7 @@ class Installer {
}
/**
* Scan the custom module cache directory and register any cached custom modules
* that aren't already known from the manifest or external module list.
* @param {Object} paths - InstallPaths instance
*/
async _scanCachedCustomModules(paths) {
const cacheDir = paths.customCacheDir;
if (!(await fs.pathExists(cacheDir))) {
return;
}
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
for (const cachedModule of cachedModules) {
const moduleId = cachedModule.name;
const cachedPath = path.join(cacheDir, moduleId);
// Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT
if (!(await fs.pathExists(cachedPath)) || !cachedModule.isDirectory()) {
continue;
}
// Skip if we already have this module from manifest
if (this.customModules.paths.has(moduleId)) {
continue;
}
// Check if this is an external official module - skip cache for those
const isExternal = await this.externalModuleManager.hasModule(moduleId);
if (isExternal) {
continue;
}
// Check if this is actually a custom module (has module.yaml)
const moduleYamlPath = path.join(cachedPath, 'module.yaml');
if (await fs.pathExists(moduleYamlPath)) {
this.customModules.paths.set(moduleId, cachedPath);
}
}
}
/**
* Common update preparation: detect files, preserve core config, scan cache, back up.
* Common update preparation: detect files, preserve core config, back up.
* @param {Object} paths - InstallPaths instance
* @param {Object} config - Clean config (may have coreConfig updated)
* @param {Object} existingInstall - Detection result
@ -553,8 +509,6 @@ class Installer {
}
}
await this._scanCachedCustomModules(paths);
const backupDirs = await this._backupUserFiles(paths, customFiles, modifiedFiles);
return {
@ -646,38 +600,6 @@ class Installer {
}
}
/**
* Install custom modules using CustomModules.install().
* Source paths come from this.customModules.paths (populated by discoverPaths).
*/
async _installCustomModules(config, paths, addResult, officialModules, ctx) {
const { message, installedModuleNames } = ctx;
const isQuickUpdate = config.isQuickUpdate();
for (const [moduleName, sourcePath] of this.customModules.paths) {
if (installedModuleNames.has(moduleName)) continue;
installedModuleNames.add(moduleName);
message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
const collectedModuleConfig = officialModules.moduleConfigs[moduleName] || {};
const result = await this.customModules.install(moduleName, paths.bmadDir, (filePath) => this.installedFiles.add(filePath), {
moduleConfig: collectedModuleConfig,
});
// Generate runtime config.yaml with merged values
await this.generateModuleConfigs(paths.bmadDir, {
[moduleName]: { ...config.coreConfig, ...result.moduleConfig, ...collectedModuleConfig },
});
// Get display name from source module.yaml; version from marketplace.json
const moduleInfo = await officialModules.getModuleInfo(sourcePath, moduleName, '');
const displayName = moduleInfo?.name || moduleName;
const version = await this._getMarketplaceVersion(sourcePath);
addResult(displayName, 'ok', '', { moduleCode: moduleName, newVersion: version });
}
}
/**
* Read files-manifest.csv
* @param {string} bmadDir - BMAD installation directory
@ -1222,16 +1144,9 @@ class Installer {
const configuredIdes = existingInstall.ides;
const projectRoot = path.dirname(bmadDir);
const customModuleSources = await this.customModules.assembleQuickUpdateSources(
config,
existingInstall,
bmadDir,
this.externalModuleManager,
);
// Get available modules (what we have source for)
const availableModulesData = await new OfficialModules().listAvailable();
const availableModules = [...availableModulesData.modules, ...availableModulesData.customModules];
const availableModules = [...availableModulesData.modules];
// Add external official modules to available modules
const externalModules = await this.externalModuleManager.listAvailable();
@ -1246,52 +1161,12 @@ class Installer {
}
}
// Add custom modules from manifest if their sources exist
for (const [moduleId, customModule] of customModuleSources) {
const sourcePath = customModule.sourcePath;
if (sourcePath && (await fs.pathExists(sourcePath)) && !availableModules.some((m) => m.id === moduleId)) {
availableModules.push({
id: moduleId,
name: customModule.name || moduleId,
path: sourcePath,
isCustom: true,
fromManifest: true,
});
}
}
// Handle missing custom module sources
const customModuleResult = await this.handleMissingCustomSources(
customModuleSources,
bmadDir,
projectRoot,
'update',
installedModules,
config.skipPrompts || false,
);
const { validCustomModules, keptModulesWithoutSources } = customModuleResult;
const customModulesFromManifest = validCustomModules.map((m) => ({
...m,
isCustom: true,
hasUpdate: true,
}));
const allAvailableModules = [...availableModules, ...customModulesFromManifest];
const availableModuleIds = new Set(allAvailableModules.map((m) => m.id));
const availableModuleIds = new Set(availableModules.map((m) => m.id));
// Only update modules that are BOTH installed AND available (we have source for)
const modulesToUpdate = installedModules.filter((id) => availableModuleIds.has(id));
const skippedModules = installedModules.filter((id) => !availableModuleIds.has(id));
// Add custom modules that were kept without sources to the skipped modules
for (const keptModule of keptModulesWithoutSources) {
if (!skippedModules.includes(keptModule)) {
skippedModules.push(keptModule);
}
}
if (skippedModules.length > 0) {
await prompts.log.warn(`Skipping ${skippedModules.length} module(s) - no source available: ${skippedModules.join(', ')}`);
}
@ -1336,9 +1211,7 @@ class Installer {
actionType: 'install',
_quickUpdate: true,
_preserveModules: skippedModules,
_customModuleSources: customModuleSources,
_existingModules: installedModules,
customContent: config.customContent,
};
await this.install(installConfig);
@ -1473,239 +1346,6 @@ class Installer {
return this._readOutputFolder(bmadDir);
}
/**
* Handle missing custom module sources interactively
* @param {Map} customModuleSources - Map of custom module ID to info
* @param {string} bmadDir - BMAD directory
* @param {string} projectRoot - Project root directory
* @param {string} operation - Current operation ('update', 'compile', etc.)
* @param {Array} installedModules - Array of installed module IDs (will be modified)
* @param {boolean} [skipPrompts=false] - Skip interactive prompts and keep all modules with missing sources
* @returns {Object} Object with validCustomModules array and keptModulesWithoutSources array
*/
async handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, operation, installedModules, skipPrompts = false) {
const validCustomModules = [];
const keptModulesWithoutSources = []; // Track modules kept without sources
const customModulesWithMissingSources = [];
// Check which sources exist
for (const [moduleId, customInfo] of customModuleSources) {
if (await fs.pathExists(customInfo.sourcePath)) {
validCustomModules.push({
id: moduleId,
name: customInfo.name,
path: customInfo.sourcePath,
info: customInfo,
});
} else {
// For cached modules that are missing, we just skip them without prompting
if (customInfo.cached) {
// Skip cached modules without prompting
keptModulesWithoutSources.push({
id: moduleId,
name: customInfo.name,
cached: true,
});
} else {
customModulesWithMissingSources.push({
id: moduleId,
name: customInfo.name,
sourcePath: customInfo.sourcePath,
relativePath: customInfo.relativePath,
info: customInfo,
});
}
}
}
// If no missing sources, return immediately
if (customModulesWithMissingSources.length === 0) {
return {
validCustomModules,
keptModulesWithoutSources: [],
};
}
// Non-interactive mode: keep all modules with missing sources
if (skipPrompts) {
for (const missing of customModulesWithMissingSources) {
keptModulesWithoutSources.push(missing.id);
}
return { validCustomModules, keptModulesWithoutSources };
}
await prompts.log.warn(`Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`);
let keptCount = 0;
let updatedCount = 0;
let removedCount = 0;
for (const missing of customModulesWithMissingSources) {
await prompts.log.message(
`${missing.name} (${missing.id})\n Original source: ${missing.relativePath}\n Full path: ${missing.sourcePath}`,
);
const choices = [
{
name: 'Keep installed (will not be processed)',
value: 'keep',
hint: 'Keep',
},
{
name: 'Specify new source location',
value: 'update',
hint: 'Update',
},
];
// Only add remove option if not just compiling agents
if (operation !== 'compile-agents') {
choices.push({
name: '⚠️ REMOVE module completely (destructive!)',
value: 'remove',
hint: 'Remove',
});
}
const action = await prompts.select({
message: `How would you like to handle "${missing.name}"?`,
choices,
});
switch (action) {
case 'update': {
// Use sync validation because @clack/prompts doesn't support async validate
const newSourcePath = await prompts.text({
message: 'Enter the new path to the custom module:',
default: missing.sourcePath,
validate: (input) => {
if (!input || input.trim() === '') {
return 'Please enter a path';
}
const expandedPath = path.resolve(input.trim());
if (!fs.pathExistsSync(expandedPath)) {
return 'Path does not exist';
}
// Check if it looks like a valid module
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
const agentsPath = path.join(expandedPath, 'agents');
const workflowsPath = path.join(expandedPath, 'workflows');
if (!fs.pathExistsSync(moduleYamlPath) && !fs.pathExistsSync(agentsPath) && !fs.pathExistsSync(workflowsPath)) {
return 'Path does not appear to contain a valid custom module';
}
return; // clack expects undefined for valid input
},
});
// Defensive: handleCancel should have exited, but guard against symbol propagation
if (typeof newSourcePath !== 'string') {
keptCount++;
keptModulesWithoutSources.push(missing.id);
continue;
}
// Update the source in manifest
const resolvedPath = path.resolve(newSourcePath.trim());
missing.info.sourcePath = resolvedPath;
// Remove relativePath - we only store absolute sourcePath now
delete missing.info.relativePath;
await this.manifest.addCustomModule(bmadDir, missing.info);
validCustomModules.push({
id: missing.id,
name: missing.name,
path: resolvedPath,
info: missing.info,
});
updatedCount++;
await prompts.log.success('Updated source location');
break;
}
case 'remove': {
// Extra confirmation for destructive remove
await prompts.log.error(
`WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!\n Module location: ${path.join(bmadDir, missing.id)}`,
);
const confirmDelete = await prompts.confirm({
message: 'Are you absolutely sure you want to delete this module?',
default: false,
});
if (confirmDelete) {
const typedConfirm = await prompts.text({
message: 'Type "DELETE" to confirm permanent deletion:',
validate: (input) => {
if (input !== 'DELETE') {
return 'You must type "DELETE" exactly to proceed';
}
return; // clack expects undefined for valid input
},
});
if (typedConfirm === 'DELETE') {
// Remove the module from filesystem and manifest
const modulePath = path.join(bmadDir, missing.id);
if (await fs.pathExists(modulePath)) {
const fsExtra = require('fs-extra');
await fsExtra.remove(modulePath);
await prompts.log.warn(`Deleted module directory: ${path.relative(projectRoot, modulePath)}`);
}
await this.manifest.removeModule(bmadDir, missing.id);
await this.manifest.removeCustomModule(bmadDir, missing.id);
await prompts.log.warn('Removed from manifest');
// Also remove from installedModules list
if (installedModules && installedModules.includes(missing.id)) {
const index = installedModules.indexOf(missing.id);
if (index !== -1) {
installedModules.splice(index, 1);
}
}
removedCount++;
await prompts.log.error(`"${missing.name}" has been permanently removed`);
} else {
await prompts.log.message('Removal cancelled - module will be kept');
keptCount++;
}
} else {
await prompts.log.message('Removal cancelled - module will be kept');
keptCount++;
}
break;
}
case 'keep': {
keptCount++;
keptModulesWithoutSources.push(missing.id);
await prompts.log.message('Module will be kept as-is');
break;
}
// No default
}
}
// Show summary
if (keptCount > 0 || updatedCount > 0 || removedCount > 0) {
let summary = 'Summary for custom modules with missing sources:';
if (keptCount > 0) summary += `\n${keptCount} module(s) kept as-is`;
if (updatedCount > 0) summary += `\n${updatedCount} module(s) updated with new sources`;
if (removedCount > 0) summary += `\n${removedCount} module(s) permanently deleted`;
await prompts.log.message(summary);
}
return {
validCustomModules,
keptModulesWithoutSources,
};
}
/**
* Find the bmad installation directory in a project
* Always uses the standard _bmad folder name

View File

@ -9,7 +9,6 @@ const {
loadSkillManifest: loadSkillManifestShared,
getCanonicalId: getCanonicalIdShared,
getArtifactType: getArtifactTypeShared,
getInstallToBmad: getInstallToBmadShared,
} = require('../ide/shared/skill-manifest');
// Load package.json for version info
@ -42,11 +41,6 @@ class ManifestGenerator {
return getArtifactTypeShared(manifest, filename);
}
/** Delegate to shared skill-manifest module */
getInstallToBmad(manifest, filename) {
return getInstallToBmadShared(manifest, filename);
}
/**
* Clean text for CSV output by normalizing whitespace.
* Note: Quote escaping is handled by escapeCsv() at write time.
@ -127,7 +121,7 @@ class ManifestGenerator {
* Recursively walk a module directory tree, collecting native SKILL.md entrypoints.
* A directory is discovered as a skill when it contains a SKILL.md file with
* valid name/description frontmatter (name must match directory name).
* Manifest YAML is loaded only when present for install_to_bmad and agent metadata.
* Manifest YAML is loaded only when present for agent metadata.
* Populates this.skills[] and this.skillClaimedDirs (Set of absolute paths).
*/
async collectSkills() {
@ -156,7 +150,7 @@ class ManifestGenerator {
const skillMeta = await this.parseSkillMd(skillMdPath, dir, dirName, debug);
if (skillMeta) {
// Load manifest when present (for install_to_bmad and agent metadata)
// Load manifest when present (for agent metadata)
const manifest = await this.loadSkillManifest(dir);
const artifactType = this.getArtifactType(manifest, skillFile);
@ -182,7 +176,6 @@ class ManifestGenerator {
module: moduleName,
path: installPath,
canonicalId,
install_to_bmad: this.getInstallToBmad(manifest, skillFile),
});
// Add to files list
@ -382,8 +375,6 @@ class ManifestGenerator {
// Read existing manifest to preserve install date
let existingInstallDate = null;
const existingModulesMap = new Map();
let existingCustomModules = [];
if (await fs.pathExists(manifestPath)) {
try {
const existingContent = await fs.readFile(manifestPath, 'utf8');
@ -404,12 +395,6 @@ class ManifestGenerator {
}
}
}
if (existingManifest.customModules && Array.isArray(existingManifest.customModules)) {
// We filter here so manifest regeneration preserves source metadata only for custom modules that
// are still installed. Without that, customModules can retain stale entries for modules that were removed.
existingCustomModules = existingManifest.customModules.filter((customModule) => installedModuleSet.has(customModule?.id));
}
} catch {
// If we can't read existing manifest, continue with defaults
}
@ -445,7 +430,6 @@ class ManifestGenerator {
lastUpdated: new Date().toISOString(),
},
modules: updatedModules,
customModules: existingCustomModules,
ides: this.selectedIdes,
};
@ -472,7 +456,7 @@ class ManifestGenerator {
const csvPath = path.join(cfgDir, 'skill-manifest.csv');
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
let csvContent = 'canonicalId,name,description,module,path,install_to_bmad\n';
let csvContent = 'canonicalId,name,description,module,path\n';
for (const skill of this.skills) {
const row = [
@ -481,7 +465,6 @@ class ManifestGenerator {
escapeCsv(skill.description),
escapeCsv(skill.module),
escapeCsv(skill.path),
escapeCsv(skill.install_to_bmad),
].join(',');
csvContent += row + '\n';
}

View File

@ -97,7 +97,6 @@ class Manifest {
lastUpdated: manifestData.installation?.lastUpdated,
modules: moduleNames, // Simple array of module names for backward compatibility
modulesDetailed: hasDetailedModules ? modules : null, // New detailed format
customModules: manifestData.customModules || [], // Keep for backward compatibility
ides: manifestData.ides || [],
};
} catch (error) {
@ -254,7 +253,6 @@ class Manifest {
lastUpdated: manifest.installation?.lastUpdated,
modules: moduleNames,
modulesDetailed: hasDetailedModules ? modules : null,
customModules: manifest.customModules || [],
ides: manifest.ides || [],
};
}
@ -783,52 +781,6 @@ class Manifest {
return configs;
}
/**
* Add a custom module to the manifest with its source path
* @param {string} bmadDir - Path to bmad directory
* @param {Object} customModule - Custom module info
*/
async addCustomModule(bmadDir, customModule) {
const manifest = await this.read(bmadDir);
if (!manifest) {
throw new Error('No manifest found');
}
if (!manifest.customModules) {
manifest.customModules = [];
}
// Check if custom module already exists
const existingIndex = manifest.customModules.findIndex((m) => m.id === customModule.id);
if (existingIndex === -1) {
// Add new entry
manifest.customModules.push(customModule);
} else {
// Update existing entry
manifest.customModules[existingIndex] = customModule;
}
await this.update(bmadDir, { customModules: manifest.customModules });
}
/**
* Remove a custom module from the manifest
* @param {string} bmadDir - Path to bmad directory
* @param {string} moduleId - Module ID to remove
*/
async removeCustomModule(bmadDir, moduleId) {
const manifest = await this.read(bmadDir);
if (!manifest || !manifest.customModules) {
return;
}
const index = manifest.customModules.findIndex((m) => m.id === moduleId);
if (index !== -1) {
manifest.customModules.splice(index, 1);
await this.update(bmadDir, { customModules: manifest.customModules });
}
}
/**
* Get module version info from source
* @param {string} moduleName - Module name/code
@ -866,29 +818,8 @@ class Manifest {
};
}
// Custom module: resolve path from source or cache before reading version
const customSourcePath = moduleSourcePath || path.join(bmadDir, '_config', 'custom', moduleName);
const version = await this._readMarketplaceVersion(moduleName, customSourcePath);
const cacheDir = path.join(bmadDir, '_config', 'custom', moduleName);
const moduleYamlPath = path.join(cacheDir, 'module.yaml');
if (await fs.pathExists(moduleYamlPath)) {
try {
const yamlContent = await fs.readFile(moduleYamlPath, 'utf8');
const moduleConfig = yaml.parse(yamlContent);
return {
version: version || moduleConfig.version || null,
source: 'custom',
npmPackage: moduleConfig.npmPackage || null,
repoUrl: moduleConfig.repoUrl || null,
};
} catch (error) {
await prompts.log.warn(`Failed to read module.yaml for ${moduleName}: ${error.message}`);
}
}
// Unknown module
const version = await this._readMarketplaceVersion(moduleName, moduleSourcePath);
return {
version,
source: 'unknown',

View File

@ -1,112 +0,0 @@
const path = require('node:path');
const fs = require('fs-extra');
const yaml = require('yaml');
const prompts = require('./prompts');
/**
* Handler for custom content (custom.yaml)
* Discovers custom agents and workflows in the project
*/
class CustomHandler {
/**
* Find all custom.yaml files in the project
* @param {string} projectRoot - Project root directory
* @returns {Array} List of custom content paths
*/
async findCustomContent(projectRoot) {
const customPaths = [];
// Helper function to recursively scan directories
async function scanDirectory(dir, excludePaths = []) {
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
// Skip hidden directories and common exclusions
if (
entry.name.startsWith('.') ||
entry.name === 'node_modules' ||
entry.name === 'dist' ||
entry.name === 'build' ||
entry.name === '.git' ||
entry.name === 'bmad'
) {
continue;
}
// Skip excluded paths
if (excludePaths.some((exclude) => fullPath.startsWith(exclude))) {
continue;
}
if (entry.isDirectory()) {
// Recursively scan subdirectories
await scanDirectory(fullPath, excludePaths);
} else if (entry.name === 'custom.yaml') {
// Found a custom.yaml file
customPaths.push(fullPath);
} else if (
entry.name === 'module.yaml' && // Check if this is a custom module (in root directory)
// Skip if it's in src/modules (those are standard modules)
!fullPath.includes(path.join('src', 'modules'))
) {
customPaths.push(fullPath);
}
}
} catch {
// Ignore errors (e.g., permission denied)
}
}
// Scan the entire project, but exclude source directories
await scanDirectory(projectRoot, [path.join(projectRoot, 'src'), path.join(projectRoot, 'tools'), path.join(projectRoot, 'test')]);
return customPaths;
}
/**
* Get custom content info from a custom.yaml or module.yaml file
* @param {string} configPath - Path to config file
* @param {string} projectRoot - Project root directory for calculating relative paths
* @returns {Object|null} Custom content info
*/
async getCustomInfo(configPath, projectRoot = null) {
try {
const configContent = await fs.readFile(configPath, 'utf8');
// Try to parse YAML with error handling
let config;
try {
config = yaml.parse(configContent);
} catch (parseError) {
await prompts.log.warn('YAML parse error in ' + configPath + ': ' + parseError.message);
return null;
}
// Check if this is an module.yaml (module) or custom.yaml (custom content)
const isInstallConfig = configPath.endsWith('module.yaml');
const configDir = path.dirname(configPath);
// Use provided projectRoot or fall back to process.cwd()
const basePath = projectRoot || process.cwd();
const relativePath = path.relative(basePath, configDir);
return {
id: config.code || 'unknown-code',
name: config.name,
description: config.description || '',
path: configDir,
relativePath: relativePath,
defaultSelected: config.default_selected === true,
config: config,
isInstallConfig: isInstallConfig, // Track which type this is
};
} catch (error) {
await prompts.log.warn('Failed to read ' + configPath + ': ' + error.message);
return null;
}
}
}
module.exports = { CustomHandler };

View File

@ -183,18 +183,6 @@ class ConfigDrivenIdeSetup {
count++;
}
// Post-install cleanup: remove _bmad/ directories for skills with install_to_bmad === "false"
for (const record of records) {
if (record.install_to_bmad === 'false') {
const relativePath = record.path.startsWith(bmadPrefix) ? record.path.slice(bmadPrefix.length) : record.path;
const sourceFile = path.join(bmadDir, relativePath);
const sourceDir = path.dirname(sourceFile);
if (await fs.pathExists(sourceDir)) {
await fs.remove(sourceDir);
}
}
}
return count;
}

View File

@ -54,19 +54,4 @@ function getArtifactType(manifest, filename) {
return null;
}
/**
* Get the install_to_bmad flag for a specific file from a loaded skill manifest.
* @param {Object|null} manifest - Loaded manifest (from loadSkillManifest)
* @param {string} filename - Source filename to look up
* @returns {boolean} install_to_bmad value (defaults to true)
*/
function getInstallToBmad(manifest, filename) {
if (!manifest) return true;
// Single-entry manifest applies to all files in the directory
if (manifest.__single) return manifest.__single.install_to_bmad !== false;
// Multi-entry: look up by filename directly
if (manifest[filename]) return manifest[filename].install_to_bmad !== false;
return true;
}
module.exports = { loadSkillManifest, getCanonicalId, getArtifactType, getInstallToBmad };
module.exports = { loadSkillManifest, getCanonicalId, getArtifactType };

View File

@ -1,302 +0,0 @@
const path = require('node:path');
const fs = require('fs-extra');
const yaml = require('yaml');
const { CustomHandler } = require('../custom-handler');
const { Manifest } = require('../core/manifest');
const prompts = require('../prompts');
class CustomModules {
constructor() {
this.paths = new Map();
}
has(moduleCode) {
return this.paths.has(moduleCode);
}
get(moduleCode) {
return this.paths.get(moduleCode);
}
set(moduleId, sourcePath) {
this.paths.set(moduleId, sourcePath);
}
/**
* Install a custom module from its source path.
* @param {string} moduleName - Module identifier
* @param {string} bmadDir - Target bmad directory
* @param {Function} fileTrackingCallback - Optional callback to track installed files
* @param {Object} options - Install options
* @param {Object} options.moduleConfig - Pre-collected module configuration
* @returns {Object} Install result
*/
async install(moduleName, bmadDir, fileTrackingCallback = null, options = {}) {
const sourcePath = this.paths.get(moduleName);
if (!sourcePath) {
throw new Error(`No source path for custom module '${moduleName}'`);
}
if (!(await fs.pathExists(sourcePath))) {
throw new Error(`Source for custom module '${moduleName}' not found at: ${sourcePath}`);
}
const targetPath = path.join(bmadDir, moduleName);
// Read custom.yaml and merge into module config
let moduleConfig = options.moduleConfig ? { ...options.moduleConfig } : {};
const customConfigPath = path.join(sourcePath, 'custom.yaml');
if (await fs.pathExists(customConfigPath)) {
try {
const content = await fs.readFile(customConfigPath, 'utf8');
const customConfig = yaml.parse(content);
if (customConfig) {
moduleConfig = { ...moduleConfig, ...customConfig };
}
} catch (error) {
await prompts.log.warn(`Failed to read custom.yaml for ${moduleName}: ${error.message}`);
}
}
// Remove existing installation
if (await fs.pathExists(targetPath)) {
await fs.remove(targetPath);
}
// Copy files with filtering
await this._copyWithFiltering(sourcePath, targetPath, fileTrackingCallback);
// Add to manifest
const manifest = new Manifest();
const versionInfo = await manifest.getModuleVersionInfo(moduleName, bmadDir, sourcePath);
await manifest.addModule(bmadDir, moduleName, {
version: versionInfo.version,
source: versionInfo.source,
npmPackage: versionInfo.npmPackage,
repoUrl: versionInfo.repoUrl,
});
return { success: true, module: moduleName, path: targetPath, moduleConfig };
}
/**
* Copy module files, filtering out install-time-only artifacts.
* @param {string} sourcePath - Source module directory
* @param {string} targetPath - Target module directory
* @param {Function} fileTrackingCallback - Optional callback to track installed files
*/
async _copyWithFiltering(sourcePath, targetPath, fileTrackingCallback = null) {
const files = await this._getFileList(sourcePath);
for (const file of files) {
if (file.startsWith('sub-modules/')) continue;
const isInSidecar = path
.dirname(file)
.split('/')
.some((dir) => dir.toLowerCase().endsWith('-sidecar'));
if (isInSidecar) continue;
if (file === 'module.yaml') continue;
if (file === 'config.yaml') continue;
const sourceFile = path.join(sourcePath, file);
const targetFile = path.join(targetPath, file);
// Skip web-only agents
if (file.startsWith('agents/') && file.endsWith('.md')) {
const content = await fs.readFile(sourceFile, 'utf8');
if (/<agent[^>]*\slocalskip="true"[^>]*>/.test(content)) {
continue;
}
}
await fs.ensureDir(path.dirname(targetFile));
await fs.copy(sourceFile, targetFile, { overwrite: true });
if (fileTrackingCallback) {
fileTrackingCallback(targetFile);
}
}
}
/**
* Recursively list all files in a directory.
* @param {string} dir - Directory to scan
* @param {string} baseDir - Base directory for relative paths
* @returns {string[]} Relative file paths
*/
async _getFileList(dir, baseDir = dir) {
const files = [];
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await this._getFileList(fullPath, baseDir)));
} else {
files.push(path.relative(baseDir, fullPath));
}
}
return files;
}
/**
* Discover custom module source paths from all available sources.
* @param {Object} config - Installation configuration
* @param {Object} paths - InstallPaths instance
* @returns {Map<string, string>} Map of module ID to source path
*/
async discoverPaths(config, paths) {
this.paths = new Map();
if (config._quickUpdate) {
if (config._customModuleSources) {
for (const [moduleId, customInfo] of config._customModuleSources) {
this.paths.set(moduleId, customInfo.sourcePath);
}
}
return this.paths;
}
// From UI: selectedFiles
if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) {
const customHandler = new CustomHandler();
for (const customFile of config.customContent.selectedFiles) {
const customInfo = await customHandler.getCustomInfo(customFile, paths.projectRoot);
if (customInfo && customInfo.id) {
this.paths.set(customInfo.id, customInfo.path);
}
}
}
// From UI: sources
if (config.customContent && config.customContent.sources) {
for (const source of config.customContent.sources) {
this.paths.set(source.id, source.path);
}
}
// From UI: cachedModules
if (config.customContent && config.customContent.cachedModules) {
const selectedCachedIds = config.customContent.selectedCachedModules || [];
const shouldIncludeAll = selectedCachedIds.length === 0 && config.customContent.selected;
for (const cachedModule of config.customContent.cachedModules) {
if (cachedModule.id && cachedModule.cachePath && (shouldIncludeAll || selectedCachedIds.includes(cachedModule.id))) {
this.paths.set(cachedModule.id, cachedModule.cachePath);
}
}
}
return this.paths;
}
/**
* Assemble quick-update source candidates before install() hands them to discoverPaths().
* This exists because discoverPaths() consumes already-prepared quick-update sources,
* while quickUpdate() still has to build that source map from manifest, explicit inputs,
* and cache conventions.
* Precedence: manifest-backed paths, explicit sources override them, then cached modules.
* @param {Object} config - Quick update configuration
* @param {Object} existingInstall - Existing installation snapshot
* @param {string} bmadDir - BMAD directory
* @param {Object} externalModuleManager - External module manager
* @returns {Promise<Map<string, Object>>} Map of custom module ID to source info
*/
async assembleQuickUpdateSources(config, existingInstall, bmadDir, externalModuleManager) {
const projectRoot = path.dirname(bmadDir);
const customModuleSources = new Map();
if (existingInstall.customModules) {
for (const customModule of existingInstall.customModules) {
// Skip if no ID - can't reliably track or re-cache without it
if (!customModule?.id) continue;
let sourcePath = customModule.sourcePath;
if (sourcePath && sourcePath.startsWith('_config')) {
// Paths are relative to BMAD dir, but we want absolute paths for install
sourcePath = path.join(bmadDir, sourcePath);
} else if (!sourcePath && customModule.relativePath) {
// Fall back to relativePath
sourcePath = path.resolve(projectRoot, customModule.relativePath);
} else if (sourcePath && !path.isAbsolute(sourcePath)) {
// If we have a sourcePath but it's not absolute, resolve it relative to project root
sourcePath = path.resolve(projectRoot, sourcePath);
}
// If we still don't have a valid source path, skip this module
if (!sourcePath || !(await fs.pathExists(sourcePath))) {
continue;
}
customModuleSources.set(customModule.id, {
id: customModule.id,
name: customModule.name || customModule.id,
sourcePath,
relativePath: customModule.relativePath,
cached: false,
});
}
}
if (config.customContent?.sources?.length > 0) {
for (const source of config.customContent.sources) {
if (source.id && source.path) {
customModuleSources.set(source.id, {
id: source.id,
name: source.name || source.id,
sourcePath: source.path,
cached: false, // From CLI, will be re-cached
});
}
}
}
const cacheDir = path.join(bmadDir, '_config', 'custom');
if (!(await fs.pathExists(cacheDir))) {
return customModuleSources;
}
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
for (const cachedModule of cachedModules) {
const moduleId = cachedModule.name;
const cachedPath = path.join(cacheDir, moduleId);
// Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT
if (!(await fs.pathExists(cachedPath))) {
continue;
}
if (!cachedModule.isDirectory()) {
continue;
}
// Skip if we already have this module from manifest
if (customModuleSources.has(moduleId)) {
continue;
}
// Check if this is an external official module - skip cache for those
const isExternal = await externalModuleManager.hasModule(moduleId);
if (isExternal) {
continue;
}
// Check if this is actually a custom module (has module.yaml)
const moduleYamlPath = path.join(cachedPath, 'module.yaml');
if (await fs.pathExists(moduleYamlPath)) {
customModuleSources.set(moduleId, {
id: moduleId,
name: moduleId,
sourcePath: cachedPath,
cached: true,
});
}
}
return customModuleSources;
}
}
module.exports = { CustomModules };

View File

@ -98,11 +98,10 @@ class OfficialModules {
/**
* List all available built-in modules (core and bmm).
* All other modules come from external-official-modules.yaml
* @returns {Object} Object with modules array and customModules array
* @returns {Object} Object with modules array
*/
async listAvailable() {
const modules = [];
const customModules = [];
// Add built-in core module (directly under src/core-skills)
const corePath = getSourcePath('core-skills');
@ -122,7 +121,7 @@ class OfficialModules {
}
}
return { modules, customModules };
return { modules };
}
/**
@ -133,25 +132,12 @@ class OfficialModules {
* @returns {Object|null} Module info or null if not a valid module
*/
async getModuleInfo(modulePath, defaultName, sourceDescription) {
// Check for module structure (module.yaml OR custom.yaml)
const moduleConfigPath = path.join(modulePath, 'module.yaml');
const rootCustomConfigPath = path.join(modulePath, 'custom.yaml');
let configPath = null;
if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath;
} else if (await fs.pathExists(rootCustomConfigPath)) {
configPath = rootCustomConfigPath;
}
// Skip if this doesn't look like a module
if (!configPath) {
if (!(await fs.pathExists(moduleConfigPath))) {
return null;
}
// Mark as custom if it's using custom.yaml OR if it's outside src/bmm or src/core
const isCustomSource =
sourceDescription !== 'src/bmm-skills' && sourceDescription !== 'src/core-skills' && sourceDescription !== 'src/modules';
const moduleInfo = {
id: defaultName,
path: modulePath,
@ -162,12 +148,11 @@ class OfficialModules {
description: 'BMAD Module',
version: '5.0.0',
source: sourceDescription,
isCustom: configPath === rootCustomConfigPath || isCustomSource,
};
// Read module config for metadata
try {
const configContent = await fs.readFile(configPath, 'utf8');
const configContent = await fs.readFile(moduleConfigPath, 'utf8');
const config = yaml.parse(configContent);
// Use the code property as the id if available
@ -824,20 +809,15 @@ class OfficialModules {
const results = [];
for (const moduleName of modules) {
// Resolve module.yaml path - custom paths first, then standard location, then OfficialModules search
// Resolve module.yaml path - standard location first, then OfficialModules search
let moduleConfigPath = null;
const customPath = this.customModulePaths?.get(moduleName);
if (customPath) {
moduleConfigPath = path.join(customPath, 'module.yaml');
const standardPath = path.join(getModulePath(moduleName), 'module.yaml');
if (await fs.pathExists(standardPath)) {
moduleConfigPath = standardPath;
} else {
const standardPath = path.join(getModulePath(moduleName), 'module.yaml');
if (await fs.pathExists(standardPath)) {
moduleConfigPath = standardPath;
} else {
const moduleSourcePath = await this.findModuleSource(moduleName, { silent: true });
if (moduleSourcePath) {
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
}
const moduleSourcePath = await this.findModuleSource(moduleName, { silent: true });
if (moduleSourcePath) {
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
}
}
@ -882,12 +862,9 @@ class OfficialModules {
* @param {Array} modules - List of modules to configure (including 'core')
* @param {string} projectDir - Target project directory
* @param {Object} options - Additional options
* @param {Map} options.customModulePaths - Map of module ID to source path for custom modules
* @param {boolean} options.skipPrompts - Skip prompts and use defaults (for --yes flag)
*/
async collectAllConfigurations(modules, projectDir, options = {}) {
// Store custom module paths for use in collectModuleConfig
this.customModulePaths = options.customModulePaths || new Map();
this.skipPrompts = options.skipPrompts || false;
this.modulesToCustomize = undefined;
await this.loadExistingConfig(projectDir);
@ -1042,25 +1019,7 @@ class OfficialModules {
}
}
let configPath = null;
let isCustomModule = false;
if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath;
} else {
// Check if this is a custom module with custom.yaml
const moduleSourcePath = await this.findModuleSource(moduleName, { silent: true });
if (moduleSourcePath) {
const rootCustomConfigPath = path.join(moduleSourcePath, 'custom.yaml');
if (await fs.pathExists(rootCustomConfigPath)) {
isCustomModule = true;
// For custom modules, we don't have an install-config schema, so just use existing values
// The custom.yaml values will be loaded and merged during installation
}
}
if (!(await fs.pathExists(moduleConfigPath))) {
// No config schema for this module - use existing values
if (this._existingConfig && this._existingConfig[moduleName]) {
if (!this.collectedConfig[moduleName]) {
@ -1071,7 +1030,7 @@ class OfficialModules {
return false;
}
const configContent = await fs.readFile(configPath, 'utf8');
const configContent = await fs.readFile(moduleConfigPath, 'utf8');
const moduleConfig = yaml.parse(configContent);
if (!moduleConfig) {
@ -1332,16 +1291,7 @@ class OfficialModules {
this.allAnswers = {};
}
// Load module's config
// First, check if we have a custom module path for this module
let moduleConfigPath = null;
if (this.customModulePaths && this.customModulePaths.has(moduleName)) {
const customPath = this.customModulePaths.get(moduleName);
moduleConfigPath = path.join(customPath, 'module.yaml');
} else {
// Try the standard src/modules location
moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
}
let moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
// If not found in src/modules or custom paths, search the project
if (!(await fs.pathExists(moduleConfigPath))) {

View File

@ -2,7 +2,6 @@ const path = require('node:path');
const os = require('node:os');
const fs = require('fs-extra');
const { CLIUtils } = require('./cli-utils');
const { CustomHandler } = require('./custom-handler');
const { ExternalModuleManager } = require('./modules/external-manager');
const { getProjectRoot } = require('./project-root');
const prompts = require('./prompts');
@ -48,19 +47,6 @@ function _extractMarketplaceVersion(data) {
return best;
}
// Separator class for visual grouping in select/multiselect prompts
// Note: @clack/prompts doesn't support separators natively, they are filtered out
class Separator {
constructor(text = '────────') {
this.line = text;
this.name = text;
}
type = 'separator';
}
// Separator for choice lists (compatible interface)
const choiceUtils = { Separator };
/**
* UI utilities for the installer
*/
@ -100,11 +86,6 @@ class UI {
// Check if there's an existing BMAD installation
const hasExistingInstall = await fs.pathExists(bmadDir);
let customContentConfig = { hasCustomContent: false };
if (!hasExistingInstall) {
customContentConfig._shouldAsk = true;
}
// Track action type (only set if there's an existing installation)
let actionType;
@ -153,48 +134,9 @@ class UI {
// Handle quick update separately
if (actionType === 'quick-update') {
// Pass --custom-content through so installer can re-cache if cache is missing
let customContentForQuickUpdate = { hasCustomContent: false };
if (options.customContent) {
const paths = options.customContent
.split(',')
.map((p) => p.trim())
.filter(Boolean);
if (paths.length > 0) {
const customPaths = [];
const selectedModuleIds = [];
const sources = [];
for (const customPath of paths) {
const expandedPath = this.expandUserPath(customPath);
const validation = this.validateCustomContentPathSync(expandedPath);
if (validation) continue;
let moduleMeta;
try {
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
moduleMeta = require('yaml').parse(await fs.readFile(moduleYamlPath, 'utf-8'));
} catch {
continue;
}
if (!moduleMeta?.code) continue;
customPaths.push(expandedPath);
selectedModuleIds.push(moduleMeta.code);
sources.push({ path: expandedPath, id: moduleMeta.code, name: moduleMeta.name || moduleMeta.code });
}
if (customPaths.length > 0) {
customContentForQuickUpdate = {
hasCustomContent: true,
selected: true,
sources,
selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
selectedModuleIds,
};
}
}
}
return {
actionType: 'quick-update',
directory: confirmedDirectory,
customContent: customContentForQuickUpdate,
skipPrompts: options.yes || false,
};
}
@ -225,120 +167,6 @@ class UI {
selectedModules = await this.selectAllModules(installedModuleIds);
}
// After module selection, ask about custom modules
let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } };
if (options.customContent) {
// Use custom content from command-line
const paths = options.customContent
.split(',')
.map((p) => p.trim())
.filter(Boolean);
await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`);
// Build custom content config similar to promptCustomContentSource
const customPaths = [];
const selectedModuleIds = [];
const sources = [];
for (const customPath of paths) {
const expandedPath = this.expandUserPath(customPath);
const validation = this.validateCustomContentPathSync(expandedPath);
if (validation) {
await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`);
continue;
}
// Read module metadata
let moduleMeta;
try {
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
const moduleYaml = await fs.readFile(moduleYamlPath, 'utf-8');
const yaml = require('yaml');
moduleMeta = yaml.parse(moduleYaml);
} catch (error) {
await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`);
continue;
}
if (!moduleMeta) {
await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml is empty`);
continue;
}
if (!moduleMeta.code) {
await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`);
continue;
}
customPaths.push(expandedPath);
selectedModuleIds.push(moduleMeta.code);
sources.push({
path: expandedPath,
id: moduleMeta.code,
name: moduleMeta.name || moduleMeta.code,
});
}
if (customPaths.length > 0) {
customModuleResult = {
selectedCustomModules: selectedModuleIds,
customContentConfig: {
hasCustomContent: true,
selected: true,
sources,
selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
selectedModuleIds: selectedModuleIds,
},
};
}
} else if (options.yes) {
// Non-interactive mode: preserve existing custom modules (matches default: false)
const cacheDir = path.join(bmadDir, '_config', 'custom');
if (await fs.pathExists(cacheDir)) {
const entries = await fs.readdir(cacheDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
customModuleResult.selectedCustomModules.push(entry.name);
}
}
await prompts.log.info(
`Non-interactive mode (--yes): preserving ${customModuleResult.selectedCustomModules.length} existing custom module(s)`,
);
} else {
await prompts.log.info('Non-interactive mode (--yes): no existing custom modules found');
}
} else {
const changeCustomModules = await prompts.confirm({
message: 'Modify custom modules, agents, or workflows?',
default: false,
});
if (changeCustomModules) {
customModuleResult = await this.handleCustomModulesInModifyFlow(confirmedDirectory, selectedModules);
} else {
// Preserve existing custom modules if user doesn't want to modify them
const { Installer } = require('./core/installer');
const installer = new Installer();
const { bmadDir } = await installer.findBmadDir(confirmedDirectory);
const cacheDir = path.join(bmadDir, '_config', 'custom');
if (await fs.pathExists(cacheDir)) {
const entries = await fs.readdir(cacheDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
customModuleResult.selectedCustomModules.push(entry.name);
}
}
}
}
}
// Merge any selected custom modules
if (customModuleResult.selectedCustomModules.length > 0) {
selectedModules.push(...customModuleResult.selectedCustomModules);
}
// Ensure core is in the modules list
if (!selectedModules.includes('core')) {
selectedModules.unshift('core');
@ -357,7 +185,6 @@ class UI {
skipIde: toolSelection.skipIde,
coreConfig: moduleConfigs.core || {},
moduleConfigs: moduleConfigs,
customContent: customModuleResult.customContentConfig,
skipPrompts: options.yes || false,
};
}
@ -383,84 +210,6 @@ class UI {
selectedModules = await this.selectAllModules(installedModuleIds);
}
// Ask about custom content (local modules/agents/workflows)
if (options.customContent) {
// Use custom content from command-line
const paths = options.customContent
.split(',')
.map((p) => p.trim())
.filter(Boolean);
await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`);
// Build custom content config similar to promptCustomContentSource
const customPaths = [];
const selectedModuleIds = [];
const sources = [];
for (const customPath of paths) {
const expandedPath = this.expandUserPath(customPath);
const validation = this.validateCustomContentPathSync(expandedPath);
if (validation) {
await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`);
continue;
}
// Read module metadata
let moduleMeta;
try {
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
const moduleYaml = await fs.readFile(moduleYamlPath, 'utf-8');
const yaml = require('yaml');
moduleMeta = yaml.parse(moduleYaml);
} catch (error) {
await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`);
continue;
}
if (!moduleMeta) {
await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml is empty`);
continue;
}
if (!moduleMeta.code) {
await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`);
continue;
}
customPaths.push(expandedPath);
selectedModuleIds.push(moduleMeta.code);
sources.push({
path: expandedPath,
id: moduleMeta.code,
name: moduleMeta.name || moduleMeta.code,
});
}
if (customPaths.length > 0) {
customContentConfig = {
hasCustomContent: true,
selected: true,
sources,
selectedFiles: customPaths.map((p) => path.join(p, 'module.yaml')),
selectedModuleIds: selectedModuleIds,
};
}
} else if (!options.yes) {
const wantsCustomContent = await prompts.confirm({
message: 'Add custom modules, agents, or workflows from your computer?',
default: false,
});
if (wantsCustomContent) {
customContentConfig = await this.promptCustomContentSource();
}
}
// Add custom content modules if any were selected
if (customContentConfig && customContentConfig.selectedModuleIds) {
selectedModules.push(...customContentConfig.selectedModuleIds);
}
// Ensure core is in the modules list
if (!selectedModules.includes('core')) {
selectedModules.unshift('core');
@ -476,7 +225,6 @@ class UI {
skipIde: toolSelection.skipIde,
coreConfig: moduleConfigs.core || {},
moduleConfigs: moduleConfigs,
customContent: customContentConfig,
skipPrompts: options.yes || false,
};
}
@ -814,90 +562,6 @@ class UI {
return configCollector.collectedConfig;
}
/**
* Get module choices for selection
* @param {Set} installedModuleIds - Currently installed module IDs
* @param {Object} customContentConfig - Custom content configuration
* @returns {Array} Module choices for prompt
*/
async getModuleChoices(installedModuleIds, customContentConfig = null) {
const color = await prompts.getColor();
const moduleChoices = [];
const isNewInstallation = installedModuleIds.size === 0;
const customContentItems = [];
// Add custom content items
if (customContentConfig && customContentConfig.hasCustomContent && customContentConfig.customPath) {
// Existing installation - show from directory
const customHandler = new CustomHandler();
const customFiles = await customHandler.findCustomContent(customContentConfig.customPath);
for (const customFile of customFiles) {
const customInfo = await customHandler.getCustomInfo(customFile);
if (customInfo) {
customContentItems.push({
name: `${color.cyan('\u2713')} ${customInfo.name} ${color.dim(`(${customInfo.relativePath})`)}`,
value: `__CUSTOM_CONTENT__${customFile}`, // Unique value for each custom content
checked: true, // Default to selected since user chose to provide custom content
path: customInfo.path, // Track path to avoid duplicates
hint: customInfo.description || undefined,
});
}
}
}
// Add official modules
const { OfficialModules } = require('./modules/official-modules');
const officialModules = new OfficialModules();
const { modules: availableModules, customModules: customModulesFromCache } = await officialModules.listAvailable();
// First, add all items to appropriate sections
const allCustomModules = [];
// Add custom content items from directory
allCustomModules.push(...customContentItems);
// Add custom modules from cache
for (const mod of customModulesFromCache) {
// Skip if this module is already in customContentItems (by path)
const isDuplicate = allCustomModules.some((item) => item.path && mod.path && path.resolve(item.path) === path.resolve(mod.path));
if (!isDuplicate) {
allCustomModules.push({
name: `${color.cyan('\u2713')} ${mod.name} ${color.dim('(cached)')}`,
value: mod.id,
checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
hint: mod.description || undefined,
});
}
}
// Add separators and modules in correct order
if (allCustomModules.length > 0) {
// Add separator for custom content, all custom modules, and official content separator
moduleChoices.push(
new choiceUtils.Separator('── Custom Content ──'),
...allCustomModules,
new choiceUtils.Separator('── Official Content ──'),
);
}
// Add official modules (only non-custom ones)
for (const mod of availableModules) {
if (!mod.isCustom) {
moduleChoices.push({
name: mod.name,
value: mod.id,
checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
hint: mod.description || undefined,
});
}
}
return moduleChoices;
}
/**
* Select all modules (official + community) using grouped multiselect.
* Core is shown as locked but filtered from the result since it's always installed separately.
@ -941,7 +605,7 @@ class UI {
// Local modules (BMM, BMB, etc.)
const localEntries = [];
for (const mod of localModules) {
if (!mod.isCustom && mod.id !== 'core') {
if (mod.id !== 'core') {
const entry = await buildModuleEntry(mod, mod.id, 'Local');
localEntries.push(entry);
if (entry.selected) {
@ -1316,282 +980,6 @@ class UI {
return existingInstall.ides;
}
/**
* Validate custom content path synchronously
* @param {string} input - User input path
* @returns {string|undefined} Error message or undefined if valid
*/
validateCustomContentPathSync(input) {
// Allow empty input to cancel
if (!input || input.trim() === '') {
return; // Allow empty to exit
}
try {
// Expand the path
const expandedPath = this.expandUserPath(input.trim());
// Check if path exists
if (!fs.pathExistsSync(expandedPath)) {
return 'Path does not exist';
}
// Check if it's a directory
const stat = fs.statSync(expandedPath);
if (!stat.isDirectory()) {
return 'Path must be a directory';
}
// Check for module.yaml in the root
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
if (!fs.pathExistsSync(moduleYamlPath)) {
return 'Directory must contain a module.yaml file in the root';
}
// Try to parse the module.yaml to get the module ID
try {
const yaml = require('yaml');
const content = fs.readFileSync(moduleYamlPath, 'utf8');
const moduleData = yaml.parse(content);
if (!moduleData.code) {
return 'module.yaml must contain a "code" field for the module ID';
}
} catch (error) {
return 'Invalid module.yaml file: ' + error.message;
}
return; // Valid
} catch (error) {
return 'Error validating path: ' + error.message;
}
}
/**
* Prompt user for custom content source location
* @returns {Object} Custom content configuration
*/
async promptCustomContentSource() {
const customContentConfig = { hasCustomContent: true, sources: [] };
// Keep asking for more sources until user is done
while (true) {
// First ask if user wants to add another module or continue
if (customContentConfig.sources.length > 0) {
const action = await prompts.select({
message: 'Would you like to:',
choices: [
{ name: 'Add another custom module', value: 'add' },
{ name: 'Continue with installation', value: 'continue' },
],
default: 'continue',
});
if (action === 'continue') {
break;
}
}
let sourcePath;
let isValid = false;
while (!isValid) {
// Use sync validation because @clack/prompts doesn't support async validate
const inputPath = await prompts.text({
message: 'Path to custom module folder (press Enter to skip):',
validate: (input) => this.validateCustomContentPathSync(input),
});
// If user pressed Enter without typing anything, exit the loop
if (!inputPath || inputPath.trim() === '') {
// If we have no modules yet, return false for no custom content
if (customContentConfig.sources.length === 0) {
return { hasCustomContent: false };
}
return customContentConfig;
}
sourcePath = this.expandUserPath(inputPath);
isValid = true;
}
// Read module.yaml to get module info
const yaml = require('yaml');
const moduleYamlPath = path.join(sourcePath, 'module.yaml');
const moduleContent = await fs.readFile(moduleYamlPath, 'utf8');
const moduleData = yaml.parse(moduleContent);
// Add to sources
customContentConfig.sources.push({
path: sourcePath,
id: moduleData.code,
name: moduleData.name || moduleData.code,
});
await prompts.log.success(`Confirmed local custom module: ${moduleData.name || moduleData.code}`);
}
// Ask if user wants to add these to the installation
const shouldInstall = await prompts.confirm({
message: `Install these ${customContentConfig.sources.length} custom modules?`,
default: true,
});
if (shouldInstall) {
customContentConfig.selected = true;
// Store paths to module.yaml files, not directories
customContentConfig.selectedFiles = customContentConfig.sources.map((s) => path.join(s.path, 'module.yaml'));
// Also include module IDs for installation
customContentConfig.selectedModuleIds = customContentConfig.sources.map((s) => s.id);
}
return customContentConfig;
}
/**
* Handle custom modules in the modify flow
* @param {string} directory - Installation directory
* @param {Array} selectedModules - Currently selected modules
* @returns {Object} Result with selected custom modules and custom content config
*/
async handleCustomModulesInModifyFlow(directory, selectedModules) {
// Get existing installation to find custom modules
const { existingInstall } = await this.getExistingInstallation(directory);
// Check if there are any custom modules in cache
const { Installer } = require('./core/installer');
const installer = new Installer();
const { bmadDir } = await installer.findBmadDir(directory);
const cacheDir = path.join(bmadDir, '_config', 'custom');
const cachedCustomModules = [];
if (await fs.pathExists(cacheDir)) {
const entries = await fs.readdir(cacheDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const moduleYamlPath = path.join(cacheDir, entry.name, 'module.yaml');
if (await fs.pathExists(moduleYamlPath)) {
const yaml = require('yaml');
const content = await fs.readFile(moduleYamlPath, 'utf8');
const moduleData = yaml.parse(content);
cachedCustomModules.push({
id: entry.name,
name: moduleData.name || entry.name,
description: moduleData.description || 'Custom module from cache',
checked: selectedModules.includes(entry.name),
fromCache: true,
});
}
}
}
}
const result = {
selectedCustomModules: [],
customContentConfig: { hasCustomContent: false },
};
// Ask user about custom modules
await prompts.log.info('Custom Modules');
if (cachedCustomModules.length > 0) {
await prompts.log.message('Found custom modules in your installation:');
} else {
await prompts.log.message('No custom modules currently installed.');
}
// Build choices dynamically based on whether we have existing modules
const choices = [];
if (cachedCustomModules.length > 0) {
choices.push(
{ name: 'Keep all existing custom modules', value: 'keep' },
{ name: 'Select which custom modules to keep', value: 'select' },
{ name: 'Add new custom modules', value: 'add' },
{ name: 'Remove all custom modules', value: 'remove' },
);
} else {
choices.push({ name: 'Add new custom modules', value: 'add' }, { name: 'Cancel (no custom modules)', value: 'cancel' });
}
const customAction = await prompts.select({
message: cachedCustomModules.length > 0 ? 'Manage custom modules?' : 'Add custom modules?',
choices: choices,
default: cachedCustomModules.length > 0 ? 'keep' : 'add',
});
switch (customAction) {
case 'keep': {
// Keep all existing custom modules
result.selectedCustomModules = cachedCustomModules.map((m) => m.id);
await prompts.log.message(`Keeping ${result.selectedCustomModules.length} custom module(s)`);
break;
}
case 'select': {
// Let user choose which to keep
const selectChoices = cachedCustomModules.map((m) => ({
name: `${m.name} (${m.id})`,
value: m.id,
checked: m.checked,
}));
// Add "None / I changed my mind" option at the end
const choicesWithSkip = [
...selectChoices,
{
name: '⚠ None / I changed my mind - keep no custom modules',
value: '__NONE__',
checked: false,
},
];
const keepModules = await prompts.multiselect({
message: 'Select custom modules to keep (use arrow keys, space to toggle):',
choices: choicesWithSkip,
required: true,
});
// If user selected both "__NONE__" and other modules, honor the "None" choice
if (keepModules && keepModules.includes('__NONE__') && keepModules.length > 1) {
await prompts.log.warn('"None / I changed my mind" was selected, so no custom modules will be kept.');
result.selectedCustomModules = [];
} else {
// Filter out the special '__NONE__' value
result.selectedCustomModules = keepModules ? keepModules.filter((m) => m !== '__NONE__') : [];
}
break;
}
case 'add': {
// By default, keep existing modules when adding new ones
// User chose "Add new" not "Replace", so we assume they want to keep existing
result.selectedCustomModules = cachedCustomModules.map((m) => m.id);
// Then prompt for new ones (reuse existing method)
const newCustomContent = await this.promptCustomContentSource();
if (newCustomContent.hasCustomContent && newCustomContent.selected) {
result.selectedCustomModules.push(...newCustomContent.selectedModuleIds);
result.customContentConfig = newCustomContent;
}
break;
}
case 'remove': {
// Remove all custom modules
await prompts.log.warn('All custom modules will be removed from the installation');
break;
}
case 'cancel': {
// User cancelled - no custom modules
await prompts.log.message('No custom modules will be added');
break;
}
}
return result;
}
/**
* Display module versions with update availability
* @param {Array} modules - Array of module info objects with version info

View File

@ -156,8 +156,15 @@ function mapInstalledToSource(refPath) {
// Skip install-only paths (generated at install time, not in source)
if (isInstallOnly(cleaned)) return null;
// core/, bmm/, and utility/ are directly under src/
if (cleaned.startsWith('core/') || cleaned.startsWith('bmm/') || cleaned.startsWith('utility/')) {
// Map installed module names to their source directory names
// _bmad/core/ → src/core-skills/, _bmad/bmm/ → src/bmm-skills/
if (cleaned.startsWith('core/')) {
return path.join(SRC_DIR, 'core-skills', cleaned.slice('core/'.length));
}
if (cleaned.startsWith('bmm/')) {
return path.join(SRC_DIR, 'bmm-skills', cleaned.slice('bmm/'.length));
}
if (cleaned.startsWith('utility/')) {
return path.join(SRC_DIR, cleaned);
}