Compare commits

...

18 Commits

Author SHA1 Message Date
don-petry 7b4ade1085
Merge 9924dc6344 into 6b964acd56 2026-04-14 08:59:38 +07:00
Brian 6b964acd56
Merge pull request #2254 from lrliang/docs/zh-cn-missing-translations
docs(zh-cn): add missing Chinese translations
2026-04-13 18:13:06 -05:00
Brian 723bca4e38
Merge branch 'main' into docs/zh-cn-missing-translations 2026-04-13 18:12:56 -05:00
Brian 262fa882ef
Merge pull request #2257 from bmad-code-org/issue-fixes
fix(installer): add missing fs-native exports
2026-04-13 18:06:56 -05:00
Brian Madison 0f958cf713 fix(installer): add missing sync and async methods to fs-native wrapper
Closes #2256
2026-04-13 09:59:41 -05:00
Brian b336cd0987
Merge pull request #2255 from bmad-code-org/fix-skill-scanner-recursion
fix(installer): stop skill scanner from recursing into discovered skills
2026-04-13 01:13:06 -05:00
Brian Madison 9ffb5b80ab fix(installer): stop skill scanner from recursing into discovered skills
Skills don't nest. Once the manifest generator finds a valid SKILL.md
in a directory, it should not recurse into that skill's subdirectories
looking for more skills. Template files (like bmb's setup-skill-template)
inside a skill's assets/ would be incorrectly scanned and produce
spurious errors.
2026-04-13 01:11:28 -05:00
梁山河 8ee35aaea3
Merge branch 'main' into docs/zh-cn-missing-translations 2026-04-13 13:55:50 +08:00
Brian 5456b26ab7
Merge pull request #2253 from bmad-code-org/fix-fs-extra-graceful-fs
fix(installer): replace fs-extra with native node:fs to prevent file loss
2026-04-13 00:55:09 -05:00
Brian Madison c6c8301ea1 fix(installer): add move() and overwrite support to fs-native
Add missing move() with cross-device fallback (rename → copy+rm on
EXDEV), needed by OfficialModules.createModuleDirectories for directory
migrations during upgrades.

Honor overwrite/errorOnExist options in copy() to match fs-extra
behavior for callers that pass these flags.
2026-04-13 00:52:41 -05:00
Brian Madison a6d075bd0b fix(installer): replace fs-extra with native node:fs to prevent file loss
fs-extra routes all operations through graceful-fs, which globally
monkey-patches node:fs with a deferred retry queue. During multi-module
installs (~500+ file ops), retried unlink operations from one module's
remove phase can fire after the next module's copy phase has written
files, silently deleting them non-deterministically.

Replace fs-extra with a thin fs-native.js wrapper over node:fs/promises
and node:fs. All 21 consumers now use native APIs with no global
monkey-patching, eliminating the retry-queue race condition entirely.

Closes #1779
2026-04-13 00:44:28 -05:00
Brian 82632a4872
Merge pull request #1927 from sunilp/fix/prd-scoping-permission-model
fix(prd): require user confirmation before de-scoping requirements or inventing phases
2026-04-13 00:21:58 -05:00
Brian 5f848c27c8
Merge branch 'main' into fix/prd-scoping-permission-model 2026-04-13 00:21:44 -05:00
leon 10c194c2a6 docs(zh-cn): add missing Chinese translations for 3 documents
Translate the remaining untranslated English docs to Chinese:
- explanation/analysis-phase.md
- explanation/checkpoint-preview.md
- how-to/install-custom-modules.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 10:35:23 +08:00
DJ 9924dc6344 fix: address CodeRabbit review feedback on cleanup-legacy.py
- Fix config restoration for zero-byte files by using `is not None`
  instead of truthiness checks on `config_backup` (empty bytes `b""` is
  falsy, causing silent loss of empty config.yaml files)
- Move config restore into the try/except block so mkdir/write_bytes
  errors are caught and reported as structured JSON instead of tracebacks
- Add logging.error() call on failure for observability
- Replace rglob("SKILL.md") with targeted glob() calls to avoid
  unnecessary deep traversal — only the two canonical installable
  layouts are checked
- Add docstring note explaining why find_skill_dirs() is intentionally
  stricter than the installer's recursive collectSkills()
- Add path traversal validation rejecting "..", "/", "\\" in dir names

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 04:24:27 -07:00
DJ db7b497eeb fix: scope find_skill_dirs to installable positions and preserve config.yaml
The cleanup-legacy.py script used an overly broad rglob("SKILL.md") that
matched template and asset files nested deep in the directory tree (e.g.
bmad-module-builder/assets/setup-skill-template/SKILL.md). This caused
cleanup to abort when it couldn't verify non-installable templates at the
skills directory.

Scopes find_skill_dirs() to only match SKILL.md at recognized installable
positions: direct children ({name}/SKILL.md) and skills subfolder
(skills/{name}/SKILL.md). Also adds config.yaml backup/restore around
shutil.rmtree() so per-module configs needed by bmad-init are preserved.

Fixes #2175

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 00:31:04 -07:00
sdev 36f9df69bf fix: address CodeRabbit review feedback for PRD scoping step
step-08-scoping.md:
- Neutral title replacing hard-coded "MVP & Future Features"
- Task statement no longer mandates phase-based prioritization
- Confirmation gate now covers artifact creation, not just de-scoping
- Phased delivery uses user-defined phase labels/count instead of fixed 3
- "wants phased" phrasing replaced with "requests/chooses"
- Development sequence question branches by release mode
- Menu text conditional on delivery mode (no "phased roadmap" for single-release)
- Handoff to step-09 now persists releaseMode in frontmatter
- New failure mode for unapproved phase artifact creation

step-11-polish.md:
- Preservation rule now includes consent-critical evidence from step 8
2026-03-27 18:13:18 +05:30
sdev 4655bb1482 fix(prd): require explicit user confirmation before de-scoping requirements or inventing phases 2026-03-27 18:13:17 +05:30
29 changed files with 873 additions and 51 deletions

View File

@ -0,0 +1,70 @@
---
title: "分析阶段:从想法到基础"
description: 头脑风暴、调研、产品简报和 PRFAQ 分别是什么——以及何时使用
sidebar:
order: 1
---
分析阶段Phase 1帮助你在决定动手构建之前把产品想清楚。这个阶段的每个工具都是可选的但如果完全跳过分析你的 PRD 就是建立在假设而非洞察之上。
## 为什么先分析再规划?
PRD 回答的是"我们应该构建什么、为什么?"如果输入的是模糊的思考,得到的就是模糊的 PRD——而下游的每一份文档都会继承这种模糊。基于薄弱 PRD 搭建的架构会押错技术方向;从薄弱架构派生的 story 会遗漏边界场景。代价是层层叠加的。
分析工具的作用就是让你的 PRD 变得锐利。它们从不同角度攻击问题——创意探索、市场现实、客户画像、可行性——这样当你坐下来和 PM agent 协作时,你已经清楚要构建什么、为谁构建。
## 工具介绍
### 头脑风暴
**是什么。** 一个使用经过验证的创意技法的引导式创意会议。AI 充当教练,通过结构化练习从你身上引出想法——而不是替你生成想法。
**为什么在这里。** 原始想法需要发展空间,然后才能被锁定为需求。头脑风暴创造了这个空间。当你有一个问题领域但还没有清晰的解决方案时,或者你想在确定方向之前探索多种可能性时,它尤其有价值。
**何时使用。** 你对想要构建什么有一个模糊的感觉,但概念尚未结晶。或者你有了概念,但想在备选方案中做压力测试。
详见[头脑风暴](./brainstorming.md)了解会议的具体运作方式。
### 调研(市场、领域、技术)
**是什么。** 三个聚焦的调研工作流,分别调查你的想法的不同维度。市场调研考察竞争对手、趋势和用户情绪;领域调研建立专业知识和术语体系;技术调研评估可行性、架构选项和实现方案。
**为什么在这里。** 基于假设构建产品是最快做出没人需要的东西的方式。调研让你的概念扎根于现实——已有哪些竞争对手、用户真正的痛点是什么、技术上是否可行、所在行业有哪些特定约束。
**何时使用。** 你正在进入一个不熟悉的领域,你怀疑竞品存在但还没有做过梳理,或者你的概念依赖于尚未验证的技术能力。可以只做一项、两项或三项全做——每项都是独立的。
### 产品简报
**是什么。** 一个引导式发现会议,输出 1-2 页的产品概念执行摘要。AI 充当协作式业务分析师,帮你阐明愿景、目标受众、价值主张和范围。
**为什么在这里。** 产品简报是进入规划阶段的较温和路径。它以结构化格式捕获你的战略愿景,可以直接输入到 PRD 的创建中。当你已经对概念有了信心——你了解客户、了解问题、大致知道想构建什么时——它效果最好。简报的作用是组织和打磨这些思考。
**何时使用。** 你的概念相对清晰,希望在创建 PRD 之前高效地记录下来。你对方向有信心,不需要有人来激烈挑战你的假设。
### PRFAQ逆向工作法
**是什么。** 亚马逊的逆向工作法Working Backwards改编为交互式挑战。你在写一行代码之前先撰写宣布成品的新闻稿然后回答客户和利益相关者会提出的最刁钻的问题。AI 充当不留情面但有建设性的产品教练。
**为什么在这里。** PRFAQ 是进入规划阶段的严格路径。它通过让你为每一个论断辩护,来强制实现以客户为中心的清晰度。如果你写不出一篇有说服力的新闻稿,说明产品还没准备好。如果客户 FAQ 的回答暴露了缺口,那些就是你在实现阶段才会——以更高代价——发现的缺口。这道关卡在成本最低的时候暴露薄弱的思考。
**何时使用。** 你希望在投入资源之前对概念进行压力测试。你不确定用户是否真的在意。你想验证自己能否阐述一个清晰、站得住脚的价值主张。或者你只是想借助逆向工作法的纪律来打磨你的思考。
## 我该用哪个?
| 情境 | 推荐工具 |
| ---- | -------- |
| "我有一个模糊的想法,不知道从哪里开始" | 头脑风暴 |
| "我需要先了解市场再做决定" | 调研 |
| "我知道要构建什么,只需要记录下来" | 产品简报 |
| "我想确认这个想法是否真的值得构建" | PRFAQ |
| "我想先探索,再验证,再记录" | 头脑风暴 → 调研 → PRFAQ 或 简报 |
产品简报和 PRFAQ 都会为 PRD 提供输入——根据你想要多大程度的挑战来选择。简报是协作式发现PRFAQ 是严格的关卡挑战。两者通往同一个目的地PRFAQ 检验你的概念是否配得上到达那里。
:::tip[不确定?]
运行 `bmad-help`,描述你的情况。它会根据你已经做了什么、想达成什么来推荐合适的起点。
:::
## 分析之后呢?
分析阶段的输出直接进入 Phase 2规划。PRD 工作流接受产品简报、PRFAQ 文档、调研成果和头脑风暴报告作为输入——它会将你产出的所有内容综合成结构化需求。分析做得越充分PRD 就越锐利。

View File

@ -0,0 +1,92 @@
---
title: "检查点预览"
description: LLM 辅助的人机协作审查,引导你从目的到细节逐步走过一个变更
sidebar:
order: 3
---
`bmad-checkpoint-preview` 是一个交互式的、LLM 辅助的人机协作审查工作流。它带你逐步走过一个代码变更——从目的和上下文到细节——让你能做出知情决策:是发布、返工,还是深入挖掘。
![检查点预览工作流图](/diagrams/checkpoint-preview-diagram.png)
## 典型流程
你运行 `bmad-quick-dev`。它澄清你的意图、构建规范、实现变更,完成后将审查线索追加到 spec 文件并在编辑器中打开。你查看 spec发现这次变更涉及跨多个模块的 20 个文件。
你可以肉眼扫一遍 diff。但 20 个文件正是肉眼审查开始失效的临界点——你会丢失线索,漏掉两个相距甚远的变更之间的关联,或者批准了自己没有完全理解的东西。所以你改为说 "checkpoint",让 LLM 带你走一遍。
这种交接——从自主实现回到人工判断——就是核心使用场景。Quick-dev 以最少的监督长时间运行,检查点预览则是你重新掌舵的地方。
## 为什么需要它
代码审查有两种失败模式。一种是审查者浏览 diff什么也没发现直接批准。另一种是逐文件仔细阅读但丢失了全局线索——见树不见林。两种模式的结果相同审查没有抓住真正重要的东西。
根本问题在于顺序。原始 diff 按文件顺序呈现变更,而这几乎从来不是构建理解的顺序。你先看到一个辅助函数,却不知道它存在的原因;先看到一个 schema 变更,却不了解它支撑什么功能。审查者必须从零散的线索中重建作者的意图,而这个重建过程正是注意力失效的地方。
检查点预览通过让 LLM 完成重建工作来解决这个问题。它读取 diff、spec如果有的话和周围的代码库然后按照有利于理解的顺序——而不是 `git diff` 的顺序——呈现变更。
## 工作原理
工作流分为五个步骤。每一步都建立在前一步的基础上,逐步从"这是什么?"过渡到"我们该不该发布?"
### 1. 定向
工作流识别变更来源(来自 PR、commit、分支、spec 文件或当前 git 状态),生成一行意图摘要以及表面积统计:变更文件数、涉及模块数、逻辑行数、边界穿越数和新增公共接口数。
这是"这是不是我以为的那个东西?"的时刻。在阅读任何代码之前,审查者确认自己看的是正确的东西,并对范围建立预期。
### 2. 走查
变更按**关注点**——而非按文件——组织。关注点是内聚的设计意图,例如"输入验证"或"API 契约"。每个关注点附带简短说明——*为什么选择这种方案*,然后列出可点击的 `path:line` 停靠点,审查者可以沿着这些停靠点在代码中导航。
这是设计判断步骤。审查者评估的是方案对系统是否合理,而不是代码是否正确。关注点按自顶向下排列:最高层意图在前,支撑实现在后。审查者永远不会遇到引用了自己尚未看过的内容。
### 3. 细节审视
在审查者理解了设计之后,工作流浮出 2-5 个"出错代价最高"的位置。这些位置按风险类别标记——`[auth]`、`[schema]`、`[billing]`、`[public API]`、`[security]` 等——并按出错后的影响范围排序。
这不是找 bug。自动化测试和 CI 负责正确性。细节审视激活的是风险意识:"这些是出错成本最高的地方。"如果审查者想在某个领域深入,可以说 "dig into [area]" 来触发一次聚焦正确性的重新审查。
如果 spec 经过了对抗性审查循环(机器硬化),那些发现也会在这里浮出——不是已修复的 bug而是审查循环标记出的、审查者应当知晓的决策。
### 4. 测试
建议 2-5 种手动观察变更生效的方式。不是自动化测试命令——而是能构建信心、但测试套件无法提供的手动观察。一个可以尝试的 UI 交互、一条可以运行的 CLI 命令、一个可以发送的 API 请求,以及每项的预期结果。
如果变更没有用户可见的行为,它会明确说明。不发明多余的忙活。
### 5. 总结
审查者做出决定:批准、返工或继续讨论。如果批准 PR工作流可以协助执行 `gh pr review --approve`。如果需要返工它帮助诊断问题出在方案、spec 还是实现,并帮助起草与具体代码位置关联的可操作反馈。
## 它是对话,不是报告
工作流将每一步呈现为起点,而非定论。在步骤之间——或步骤中间——你可以与 LLM 对话、提问、挑战它的框架,或调用其他技能来获取不同视角:
- **"run advanced elicitation on the error handling"** — 推动 LLM 重新思考并细化对特定领域的分析
- **"party mode on whether this schema migration is safe"** — 引入多个 agent 视角进行聚焦辩论
- **"run code review"** — 生成包含对抗性和边界场景分析的结构化 agentic 审查报告
检查点工作流不会把你锁在线性路径上。它在你需要结构时提供结构,在你想探索时让开。五个步骤确保你看到全貌,但每一步深入到什么程度——以及调用什么工具——完全由你决定。
## 审查线索
走查步骤在有**建议审查顺序**时效果最好——这是 spec 作者编写的停靠点列表,用于引导审查者走过变更。当 spec 包含此内容时,工作流直接使用它。
当没有作者提供的线索时,工作流会从 diff 和代码库上下文生成一份。生成的线索质量不如作者编写的,但远好于按文件顺序阅读变更。
## 何时使用
主要场景是 `bmad-quick-dev` 的交接实现完成spec 文件在编辑器中打开并追加了审查线索,你需要决定是否发布。说 "checkpoint" 即可开始。
它也可以独立使用:
- **审查 PR** — 尤其是涉及多个文件或跨模块变更的 PR
- **了解一个变更** — 当你需要理解一个不是你写的分支上发生了什么
- **Sprint 审查** — 工作流可以提取 sprint 状态文件中标记为 `review` 的 story
通过说 "checkpoint" 或 "walk me through this change" 来调用。它在任何终端中都能工作,但在 IDE 中——VS Code、Cursor 或类似工具——你会获得更多,因为工作流在每一步都生成 `path:line` 引用。在嵌入 IDE 的终端中,这些引用是可点击的,你可以沿着审查线索在文件间跳转。
## 它不是什么
检查点预览不是自动化审查的替代品。它不运行 linter、类型检查器或测试套件。它不打分也不给出通过/不通过的判定。它是一份阅读指南,帮助人类在最重要的地方运用自己的判断力。

View File

@ -0,0 +1,180 @@
---
title: "安装自定义和社区模块"
description: 从社区注册表、Git 仓库或本地路径安装第三方模块
sidebar:
order: 3
---
使用 BMad 安装程序从社区注册表、第三方 Git 仓库或本地文件路径添加模块。
## 何时使用
- 从 BMad 注册表安装社区贡献的模块
- 从第三方 Git 仓库安装模块GitHub、GitLab、Bitbucket、自托管
- 使用 BMad Builder 测试本地开发中的模块
- 从私有或自托管 Git 服务器安装模块
:::note[前置条件]
需要 [Node.js](https://nodejs.org) v20+ 和 `npx`npm 自带)。自定义和社区模块可以在全新安装时选择,也可以添加到现有安装中。
:::
## 社区模块
社区模块收录在 [BMad 插件市场](https://github.com/bmad-code-org/bmad-plugins-marketplace)。它们按类别组织,并锁定在经过审核的 commit 上以确保安全。
### 1. 运行安装程序
```bash
npx bmad-method install
```
### 2. 浏览社区目录
选择官方模块后,安装程序会询问:
```
Would you like to browse community modules?
```
选择 **Yes** 进入目录浏览器。你可以:
- 按类别浏览
- 查看推荐模块
- 查看所有可用模块
- 按关键词搜索
### 3. 选择模块
从任意类别中选取模块。安装程序显示描述、版本和信任等级。已安装的模块会预选以便更新。
### 4. 继续安装
选择社区模块后,安装程序将继续到自定义来源,然后是工具/IDE 配置及其余安装流程。
## 自定义来源Git URL 和本地路径)
自定义模块可以来自任何 Git 仓库或本地目录。安装程序会解析来源、分析模块结构,并将其与其他模块一起安装。
### 交互式安装
安装过程中,在社区模块步骤之后,安装程序会询问:
```
Would you like to install from a custom source (Git URL or local path)?
```
选择 **Yes**,然后提供来源:
| 输入类型 | 示例 |
| -------- | ---- |
| HTTPS URL任意主机 | `https://github.com/org/repo` |
| 带子目录的 HTTPS URL | `https://github.com/org/repo/tree/main/my-module` |
| SSH URL | `git@github.com:org/repo.git` |
| 本地路径 | `/Users/me/projects/my-module` |
| 使用 ~ 的本地路径 | `~/projects/my-module` |
安装程序会克隆仓库URL 来源)或直接从磁盘读取(本地路径),然后展示发现的模块供你选择。
### 非交互式安装
使用 `--custom-source` 标志从命令行安装自定义模块:
```bash
npx bmad-method install \
--directory . \
--custom-source /path/to/my-module \
--tools claude-code \
--yes
```
提供 `--custom-source` 但未指定 `--modules` 时,只安装 core 和自定义模块。要同时包含官方模块,需添加 `--modules`
```bash
npx bmad-method install \
--directory . \
--modules bmm \
--custom-source https://gitlab.com/myorg/my-module \
--tools claude-code \
--yes
```
多个来源可用逗号分隔:
```bash
--custom-source /path/one,https://github.com/org/repo,/path/two
```
## 模块发现机制
安装程序使用两种模式在来源中查找可安装的模块:
| 模式 | 触发条件 | 行为 |
| ---- | -------- | ---- |
| 发现模式 | 来源包含 `.claude-plugin/marketplace.json` | 列出清单中的所有插件;你选择要安装哪些 |
| 直接模式 | 未找到 marketplace.json | 扫描目录中的 skill包含 `SKILL.md` 的子目录),作为单个模块解析 |
发现模式适用于已发布的模块。直接模式适合本地开发时指向 skills 目录。
:::note[关于 `.claude-plugin/`]
`.claude-plugin/marketplace.json` 路径是多个 AI 工具安装程序采用的标准约定,用于插件可发现性。它不依赖 Claude不使用 Claude API也不影响你使用哪个 AI 工具。任何包含此文件的模块都可以被遵循此约定的安装程序发现。
:::
## 本地开发工作流
如果你正在使用 [BMad Builder](https://github.com/bmad-code-org/bmad-builder) 构建模块,可以直接从工作目录安装:
```bash
npx bmad-method install \
--directory ~/my-project \
--custom-source ~/my-module-repo/skills \
--tools claude-code \
--yes
```
本地来源通过路径引用,不会复制到缓存。当你更新模块源码并重新安装时,安装程序会获取最新变更。
:::caution[来源移除]
如果你在安装后删除了本地来源目录,`_bmad/` 中已安装的模块文件会保留。在恢复来源路径之前,该模块在更新时会被跳过。
:::
## 安装结果
安装后,自定义模块与官方模块一起出现在 `_bmad/` 中:
```
your-project/
├── _bmad/
│ ├── core/ # 内置核心模块
│ ├── bmm/ # 官方模块(如已选择)
│ ├── my-module/ # 你的自定义模块
│ │ ├── my-skill/
│ │ │ └── SKILL.md
│ │ └── module-help.csv
│ └── _config/
│ └── manifest.yaml # 跟踪所有模块、版本和来源
└── ...
```
manifest 记录每个自定义模块的来源Git 来源为 `repoUrl`,本地来源为 `localPath`),以便快速更新时能重新定位来源。
## 更新自定义模块
自定义模块参与正常的更新流程:
- **快速更新**`--action quick-update`):从原始来源刷新所有模块。基于 Git 的模块会重新拉取;本地模块会从来源路径重新读取。
- **完整更新**:重新运行模块选择,你可以添加或移除自定义模块。
## 创建自己的模块
使用 [BMad Builder](https://github.com/bmad-code-org/bmad-builder) 创建可供他人安装的模块:
1. 运行 `bmad-module-builder` 搭建模块结构
2. 使用各种 BMad Builder 工具添加 skill、agent 和 workflow
3. 发布到 Git 仓库或共享文件夹集合
4. 他人使用 `--custom-source <your-repo-url>` 安装
要让模块支持发现模式,请在仓库根目录包含 `.claude-plugin/marketplace.json`(这是跨工具约定,非 Claude 专属)。格式详见 [BMad Builder 文档](https://github.com/bmad-code-org/bmad-builder)。
:::tip[先在本地测试]
开发期间,使用本地路径安装模块以快速迭代,发布到 Git 仓库之前先确认一切正常。
:::

View File

@ -70,7 +70,6 @@
"chalk": "^4.1.2", "chalk": "^4.1.2",
"commander": "^14.0.0", "commander": "^14.0.0",
"csv-parse": "^6.1.0", "csv-parse": "^6.1.0",
"fs-extra": "^11.3.0",
"glob": "^11.0.3", "glob": "^11.0.3",
"ignore": "^7.0.5", "ignore": "^7.0.5",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",

View File

@ -1,4 +1,4 @@
# Step 8: Scoping Exercise - MVP & Future Features # Step 8: Scoping Exercise - Scope Definition (Phased or Single-Release)
**Progress: Step 8 of 11** - Next: Functional Requirements **Progress: Step 8 of 11** - Next: Functional Requirements
@ -12,6 +12,8 @@
- 📋 YOU ARE A FACILITATOR, not a content generator - 📋 YOU ARE A FACILITATOR, not a content generator
- 💬 FOCUS on strategic scope decisions that keep projects viable - 💬 FOCUS on strategic scope decisions that keep projects viable
- 🎯 EMPHASIZE lean MVP thinking while preserving long-term vision - 🎯 EMPHASIZE lean MVP thinking while preserving long-term vision
- ⚠️ NEVER de-scope, defer, or phase out requirements that the user explicitly included in their input documents without asking first
- ⚠️ NEVER invent phasing (MVP/Growth/Vision) unless the user requests phased delivery — if input documents define all components as core requirements, they are ALL in scope
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}` - ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
- ✅ YOU MUST ALWAYS WRITE all artifact and document content in `{document_output_language}` - ✅ YOU MUST ALWAYS WRITE all artifact and document content in `{document_output_language}`
@ -34,7 +36,7 @@
## YOUR TASK: ## YOUR TASK:
Conduct comprehensive scoping exercise to define MVP boundaries and prioritize features across development phases. Conduct comprehensive scoping exercise to define release boundaries and prioritize features based on the user's chosen delivery mode (phased or single-release).
## SCOPING SEQUENCE: ## SCOPING SEQUENCE:
@ -75,30 +77,41 @@ Use structured decision-making for scope:
- Advanced functionality that builds on MVP - Advanced functionality that builds on MVP
- Ask what features could be added in versions 2, 3, etc. - Ask what features could be added in versions 2, 3, etc.
**⚠️ SCOPE CHANGE CONFIRMATION GATE:**
- If you believe any user-specified requirement should be deferred or de-scoped, you MUST present this to the user and get explicit confirmation BEFORE removing it from scope
- Frame it as a recommendation, not a decision: "I'd recommend deferring X because [reason]. Do you agree, or should it stay in scope?"
- NEVER silently move user requirements to a later phase or exclude them from MVP
- Before creating any consequential phase-based artifacts (e.g., phase tags, labels, or follow-on prompts), present artifact creation as a recommendation and proceed only after explicit user approval
### 4. Progressive Feature Roadmap ### 4. Progressive Feature Roadmap
Create phased development approach: **CRITICAL: Phasing is NOT automatic. Check the user's input first.**
- Guide mapping of features across development phases
- Structure as Phase 1 (MVP), Phase 2 (Growth), Phase 3 (Vision)
- Ensure clear progression and dependencies
- Core user value delivery Before proposing any phased approach, review the user's input documents:
- Essential user journeys
- Basic functionality that works reliably
**Phase 2: Growth** - **If the input documents define all components as core requirements with no mention of phases:** Present all requirements as a single release scope. Do NOT invent phases or move requirements to fabricated future phases.
- **If the input documents explicitly request phased delivery:** Guide mapping of features across the phases the user defined.
- **If scope is unclear:** ASK the user whether they want phased delivery or a single release before proceeding.
- Additional user types **When the user requests phased delivery**, guide mapping of features across the phases the user defines:
- Enhanced features
- Scale improvements
**Phase 3: Expansion** - Use user-provided phase labels and count; if none are provided, propose a default (e.g., MVP/Growth/Vision) and ask for confirmation
- Ensure clear progression and dependencies between phases
- Advanced capabilities **Each phase should address:**
- Platform features
- New markets or use cases
**Where does your current vision fit in this development sequence?**" - Core user value delivery and essential journeys for that phase
- Clear boundaries on what ships in each phase
- Dependencies on prior phases
**When the user chooses a single release**, define the complete scope:
- All user-specified requirements are in scope
- Focus must-have vs nice-to-have analysis on what ships in this release
- Do NOT create phases — use must-have/nice-to-have priority within the single release
**If phased delivery:** "Where does your current vision fit in this development sequence?"
**If single release:** "How does your current vision map to this upcoming release?"
### 5. Risk-Based Scoping ### 5. Risk-Based Scoping
@ -129,6 +142,8 @@ Prepare comprehensive scoping section:
#### Content Structure: #### Content Structure:
**If user chose phased delivery:**
```markdown ```markdown
## Project Scoping & Phased Development ## Project Scoping & Phased Development
@ -160,11 +175,39 @@ Prepare comprehensive scoping section:
**Resource Risks:** {{contingency_approach}} **Resource Risks:** {{contingency_approach}}
``` ```
**If user chose single release (no phasing):**
```markdown
## Project Scoping
### Strategy & Philosophy
**Approach:** {{chosen_approach}}
**Resource Requirements:** {{team_size_and_skills}}
### Complete Feature Set
**Core User Journeys Supported:**
{{all_journeys}}
**Must-Have Capabilities:**
{{list_of_must_have_features}}
**Nice-to-Have Capabilities:**
{{list_of_nice_to_have_features}}
### Risk Mitigation Strategy
**Technical Risks:** {{mitigation_approach}}
**Market Risks:** {{validation_approach}}
**Resource Risks:** {{contingency_approach}}
```
### 7. Present MENU OPTIONS ### 7. Present MENU OPTIONS
Present the scoping decisions for review, then display menu: Present the scoping decisions for review, then display menu:
- Show strategic scoping plan (using structure from step 6) - Show strategic scoping plan (using structure from step 6)
- Highlight MVP boundaries and phased roadmap - Highlight release boundaries and prioritization (phased roadmap only if phased delivery was selected)
- Ask if they'd like to refine further, get other perspectives, or proceed - Ask if they'd like to refine further, get other perspectives, or proceed
- Present menu options naturally as part of conversation - Present menu options naturally as part of conversation
@ -173,7 +216,7 @@ Display: "**Select:** [A] Advanced Elicitation [P] Party Mode [C] Continue to Fu
#### Menu Handling Logic: #### Menu Handling Logic:
- IF A: Invoke the `bmad-advanced-elicitation` skill with the current scoping analysis, process the enhanced insights that come back, ask user if they accept the improvements, if yes update content then redisplay menu, if no keep original content then redisplay menu - IF A: Invoke the `bmad-advanced-elicitation` skill with the current scoping analysis, process the enhanced insights that come back, ask user if they accept the improvements, if yes update content then redisplay menu, if no keep original content then redisplay menu
- IF P: Invoke the `bmad-party-mode` skill with the scoping context, process the collaborative insights on MVP and roadmap decisions, ask user if they accept the changes, if yes update content then redisplay menu, if no keep original content then redisplay menu - IF P: Invoke the `bmad-party-mode` skill with the scoping context, process the collaborative insights on MVP and roadmap decisions, ask user if they accept the changes, if yes update content then redisplay menu, if no keep original content then redisplay menu
- IF C: Append the final content to {outputFile}, update frontmatter by adding this step name to the end of the stepsCompleted array, then read fully and follow: ./step-09-functional.md - IF C: Append the final content to {outputFile}, update frontmatter by adding this step name to the end of the stepsCompleted array (also add `releaseMode: phased` or `releaseMode: single-release` to frontmatter based on user's choice), then read fully and follow: ./step-09-functional.md
- IF Any other: help user respond, then redisplay menu - IF Any other: help user respond, then redisplay menu
#### EXECUTION RULES: #### EXECUTION RULES:
@ -189,8 +232,9 @@ When user selects 'C', append the content directly to the document using the str
✅ Complete PRD document analyzed for scope implications ✅ Complete PRD document analyzed for scope implications
✅ Strategic MVP approach defined and justified ✅ Strategic MVP approach defined and justified
✅ Clear MVP feature boundaries established ✅ Clear feature boundaries established (phased or single-release, per user preference)
✅ Phased development roadmap created ✅ All user-specified requirements accounted for — none silently removed or deferred
✅ Any scope reduction recommendations presented to user with rationale and explicit confirmation obtained
✅ Key risks identified and mitigation strategies defined ✅ Key risks identified and mitigation strategies defined
✅ User explicitly agrees to scope decisions ✅ User explicitly agrees to scope decisions
✅ A/P/C menu presented and handled correctly ✅ A/P/C menu presented and handled correctly
@ -202,8 +246,11 @@ When user selects 'C', append the content directly to the document using the str
❌ Making scope decisions without strategic rationale ❌ Making scope decisions without strategic rationale
❌ Not getting explicit user agreement on MVP boundaries ❌ Not getting explicit user agreement on MVP boundaries
❌ Missing critical risk analysis ❌ Missing critical risk analysis
❌ Not creating clear phased development approach
❌ Not presenting A/P/C menu after content generation ❌ Not presenting A/P/C menu after content generation
**CRITICAL**: Silently de-scoping or deferring requirements that the user explicitly included in their input documents
**CRITICAL**: Inventing phasing (MVP/Growth/Vision) when the user did not request phased delivery
**CRITICAL**: Making consequential scoping decisions (what is in/out of scope) without explicit user confirmation
**CRITICAL**: Creating phase-based artifacts (tags, labels, follow-on prompts) without explicit user approval
**CRITICAL**: Reading only partial step file - leads to incomplete understanding and poor decisions **CRITICAL**: Reading only partial step file - leads to incomplete understanding and poor decisions
**CRITICAL**: Proceeding with 'C' without fully reading and understanding the next step file **CRITICAL**: Proceeding with 'C' without fully reading and understanding the next step file

View File

@ -138,7 +138,7 @@ Make targeted improvements:
- All user success criteria - All user success criteria
- All functional requirements (capability contract) - All functional requirements (capability contract)
- All user journey narratives - All user journey narratives
- All scope decisions (MVP, Growth, Vision) - All scope decisions (whether phased or single-release), including consent-critical evidence (explicit user confirmations and rationales for any scope changes from step 8)
- All non-functional requirements - All non-functional requirements
- Product differentiator and vision - Product differentiator and vision
- Domain-specific requirements - Domain-specific requirements

View File

@ -13,7 +13,7 @@
const path = require('node:path'); const path = require('node:path');
const os = require('node:os'); const os = require('node:os');
const fs = require('fs-extra'); const fs = require('../tools/installer/fs-native');
const { Installer } = require('../tools/installer/core/installer'); const { Installer } = require('../tools/installer/core/installer');
const { ManifestGenerator } = require('../tools/installer/core/manifest-generator'); const { ManifestGenerator } = require('../tools/installer/core/manifest-generator');
const { OfficialModules } = require('../tools/installer/modules/official-modules'); const { OfficialModules } = require('../tools/installer/modules/official-modules');

View File

@ -19,7 +19,7 @@ module.exports = {
const { bmadDir } = await installer.findBmadDir(projectDir); const { bmadDir } = await installer.findBmadDir(projectDir);
// Check if bmad directory exists // Check if bmad directory exists
const fs = require('fs-extra'); const fs = require('../fs-native');
if (!(await fs.pathExists(bmadDir))) { if (!(await fs.pathExists(bmadDir))) {
await prompts.log.warn('No BMAD installation found in the current directory.'); await prompts.log.warn('No BMAD installation found in the current directory.');
await prompts.log.message(`Expected location: ${bmadDir}`); await prompts.log.message(`Expected location: ${bmadDir}`);

View File

@ -1,5 +1,5 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('../fs-native');
const prompts = require('../prompts'); const prompts = require('../prompts');
const { Installer } = require('../core/installer'); const { Installer } = require('../core/installer');

View File

@ -1,5 +1,5 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('../fs-native');
const yaml = require('yaml'); const yaml = require('yaml');
const { Manifest } = require('./manifest'); const { Manifest } = require('./manifest');

View File

@ -1,5 +1,5 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('../fs-native');
const { getProjectRoot } = require('../project-root'); const { getProjectRoot } = require('../project-root');
const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils'); const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');

View File

@ -1,5 +1,5 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('../fs-native');
const { Manifest } = require('./manifest'); const { Manifest } = require('./manifest');
const { OfficialModules } = require('../modules/official-modules'); const { OfficialModules } = require('../modules/official-modules');
const { IdeManager } = require('../ide/manager'); const { IdeManager } = require('../ide/manager');

View File

@ -1,5 +1,5 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('../fs-native');
const yaml = require('yaml'); const yaml = require('yaml');
const crypto = require('node:crypto'); const crypto = require('node:crypto');
const csv = require('csv-parse/sync'); const csv = require('csv-parse/sync');
@ -193,12 +193,14 @@ class ManifestGenerator {
} }
} }
// Recurse into subdirectories // Recurse into subdirectories — but not inside a discovered skill
if (!skillMeta) {
for (const entry of entries) { for (const entry of entries) {
if (!entry.isDirectory()) continue; if (!entry.isDirectory()) continue;
if (entry.name.startsWith('.') || entry.name.startsWith('_')) continue; if (entry.name.startsWith('.') || entry.name.startsWith('_')) continue;
await walk(path.join(dir, entry.name)); await walk(path.join(dir, entry.name));
} }
}
}; };
await walk(modulePath); await walk(modulePath);

View File

@ -1,5 +1,5 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('../fs-native');
const crypto = require('node:crypto'); const crypto = require('node:crypto');
const { getProjectRoot } = require('../project-root'); const { getProjectRoot } = require('../project-root');
const prompts = require('../prompts'); const prompts = require('../prompts');

View File

@ -1,4 +1,4 @@
const fs = require('fs-extra'); const fs = require('./fs-native');
const path = require('node:path'); const path = require('node:path');
const crypto = require('node:crypto'); const crypto = require('node:crypto');

View File

@ -0,0 +1,116 @@
// Drop-in replacement for fs-extra using native node:fs APIs.
// Eliminates graceful-fs monkey-patching that causes non-deterministic
// file loss during multi-module installs on macOS (issue #1779).
const fsp = require('node:fs/promises');
const fs = require('node:fs');
const path = require('node:path');
async function pathExists(p) {
try {
await fsp.access(p);
return true;
} catch {
return false;
}
}
async function ensureDir(dir) {
await fsp.mkdir(dir, { recursive: true });
}
async function remove(p) {
await fsp.rm(p, { recursive: true, force: true });
}
async function copy(src, dest, options = {}) {
const filterFn = options.filter;
const overwrite = options.overwrite !== false;
const srcStat = await fsp.stat(src);
if (srcStat.isFile()) {
if (filterFn && !(await filterFn(src, dest))) return;
await fsp.mkdir(path.dirname(dest), { recursive: true });
if (!overwrite) {
try {
await fsp.access(dest);
if (options.errorOnExist) throw new Error(`${dest} already exists`);
return;
} catch (error) {
if (error.message.includes('already exists')) throw error;
}
}
await fsp.copyFile(src, dest);
return;
}
if (srcStat.isDirectory()) {
if (filterFn && !(await filterFn(src, dest))) return;
await fsp.mkdir(dest, { recursive: true });
const entries = await fsp.readdir(src, { withFileTypes: true });
for (const entry of entries) {
await copy(path.join(src, entry.name), path.join(dest, entry.name), options);
}
}
}
async function move(src, dest) {
try {
await fsp.rename(src, dest);
} catch (error) {
if (error.code === 'EXDEV') {
await copy(src, dest);
await fsp.rm(src, { recursive: true, force: true });
} else {
throw error;
}
}
}
function readJsonSync(p) {
return JSON.parse(fs.readFileSync(p, 'utf8'));
}
async function writeJson(p, data, options = {}) {
const spaces = options.spaces ?? 2;
await fsp.writeFile(p, JSON.stringify(data, null, spaces) + '\n', 'utf8');
}
module.exports = {
// Native async (node:fs/promises)
readFile: fsp.readFile,
writeFile: fsp.writeFile,
stat: fsp.stat,
readdir: fsp.readdir,
access: fsp.access,
realpath: fsp.realpath,
rename: fsp.rename,
rmdir: fsp.rmdir,
unlink: fsp.unlink,
chmod: fsp.chmod,
mkdir: fsp.mkdir,
mkdtemp: fsp.mkdtemp,
copyFile: fsp.copyFile,
rm: fsp.rm,
// fs-extra compatible helpers (native implementations)
pathExists,
ensureDir,
remove,
copy,
move,
readJsonSync,
writeJson,
// Sync methods from core node:fs
existsSync: fs.existsSync.bind(fs),
readFileSync: fs.readFileSync.bind(fs),
writeFileSync: fs.writeFileSync.bind(fs),
statSync: fs.statSync.bind(fs),
accessSync: fs.accessSync.bind(fs),
readdirSync: fs.readdirSync.bind(fs),
createReadStream: fs.createReadStream.bind(fs),
pathExistsSync: fs.existsSync.bind(fs),
// Constants
constants: fs.constants,
};

View File

@ -1,6 +1,6 @@
const os = require('node:os'); const os = require('node:os');
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('../fs-native');
const yaml = require('yaml'); const yaml = require('yaml');
const prompts = require('../prompts'); const prompts = require('../prompts');
const csv = require('csv-parse/sync'); const csv = require('csv-parse/sync');

View File

@ -1,4 +1,4 @@
const fs = require('fs-extra'); const fs = require('../fs-native');
const path = require('node:path'); const path = require('node:path');
const yaml = require('yaml'); const yaml = require('yaml');

View File

@ -1,5 +1,5 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('../../fs-native');
const yaml = require('yaml'); const yaml = require('yaml');
/** /**

View File

@ -1,4 +1,4 @@
const fs = require('fs-extra'); const fs = require('./fs-native');
const path = require('node:path'); const path = require('node:path');
const yaml = require('yaml'); const yaml = require('yaml');
const prompts = require('./prompts'); const prompts = require('./prompts');

View File

@ -1,4 +1,4 @@
const fs = require('fs-extra'); const fs = require('../fs-native');
const os = require('node:os'); const os = require('node:os');
const path = require('node:path'); const path = require('node:path');
const { execSync } = require('node:child_process'); const { execSync } = require('node:child_process');

View File

@ -1,4 +1,4 @@
const fs = require('fs-extra'); const fs = require('../fs-native');
const os = require('node:os'); const os = require('node:os');
const path = require('node:path'); const path = require('node:path');
const { execSync } = require('node:child_process'); const { execSync } = require('node:child_process');

View File

@ -1,4 +1,4 @@
const fs = require('fs-extra'); const fs = require('../fs-native');
const os = require('node:os'); const os = require('node:os');
const path = require('node:path'); const path = require('node:path');
const { execSync } = require('node:child_process'); const { execSync } = require('node:child_process');

View File

@ -1,5 +1,5 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('../fs-native');
const yaml = require('yaml'); const yaml = require('yaml');
const prompts = require('../prompts'); const prompts = require('../prompts');
const { getProjectRoot, getSourcePath, getModulePath } = require('../project-root'); const { getProjectRoot, getSourcePath, getModulePath } = require('../project-root');

View File

@ -1,4 +1,4 @@
const fs = require('fs-extra'); const fs = require('../fs-native');
const path = require('node:path'); const path = require('node:path');
const yaml = require('yaml'); const yaml = require('yaml');

View File

@ -1,5 +1,5 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('./fs-native');
/** /**
* Find the BMAD project root directory by looking for package.json * Find the BMAD project root directory by looking for package.json

View File

@ -1,6 +1,6 @@
const path = require('node:path'); const path = require('node:path');
const os = require('node:os'); const os = require('node:os');
const fs = require('fs-extra'); const fs = require('./fs-native');
const { CLIUtils } = require('./cli-utils'); const { CLIUtils } = require('./cli-utils');
const { ExternalModuleManager } = require('./modules/external-manager'); const { ExternalModuleManager } = require('./modules/external-manager');
const { getProjectRoot } = require('./project-root'); const { getProjectRoot } = require('./project-root');

View File

@ -3,7 +3,7 @@
* This should be run once to update existing installations * This should be run once to update existing installations
*/ */
const fs = require('fs-extra'); const fs = require('./installer/fs-native');
const path = require('node:path'); const path = require('node:path');
const yaml = require('yaml'); const yaml = require('yaml');
const chalk = require('chalk'); const chalk = require('chalk');

View File

@ -0,0 +1,316 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.9"
# dependencies = []
# ///
"""Remove legacy module directories from _bmad/ after config migration.
After merge-config.py and merge-help-csv.py have migrated config data and
deleted individual legacy files, this script removes the now-redundant
directory trees. These directories contain skill files that are already
installed at .claude/skills/ (or equivalent) only the config files at
_bmad/ root need to persist.
When --skills-dir is provided, the script verifies that every skill found
in the legacy directories exists at the installed location before removing
anything. Directories without skills (like _config/) are removed directly.
Exit codes: 0=success (including nothing to remove), 1=validation error, 2=runtime error
"""
import argparse
import json
import logging
import shutil
import sys
from pathlib import Path
logger = logging.getLogger(__name__)
def parse_args():
parser = argparse.ArgumentParser(
description="Remove legacy module directories from _bmad/ after config migration."
)
parser.add_argument(
"--bmad-dir",
required=True,
help="Path to the _bmad/ directory",
)
parser.add_argument(
"--module-code",
required=True,
help="Module code being cleaned up (e.g. 'bmb')",
)
parser.add_argument(
"--also-remove",
action="append",
default=[],
help="Additional directory names under _bmad/ to remove (repeatable)",
)
parser.add_argument(
"--skills-dir",
help="Path to .claude/skills/ — enables safety verification that skills "
"are installed before removing legacy copies",
)
parser.add_argument(
"--verbose",
action="store_true",
help="Print detailed progress to stderr",
)
return parser.parse_args()
def find_skill_dirs(base_path: str) -> list:
"""Find installable skill directories under base_path.
Only considers SKILL.md files at recognized installable positions:
- Direct children: base_path/{name}/SKILL.md (legacy flat layout)
- Skills subfolder: base_path/skills/{name}/SKILL.md (current layout)
SKILL.md files nested deeper (e.g. in tasks/, assets/, or within a
skill's own subdirectories) are not installable skills and are skipped.
NOTE: These discovery rules are intentionally stricter than the installer's
recursive collectSkills() behavior. The installer is permissive it walks
the entire tree to find all SKILL.md files for installation. Cleanup must
be conservative: we only match the two canonical installable layouts so we
never accidentally validate a SKILL.md buried in tasks/, assets/, or other
non-installable subdirectories as proof that a skill is present.
Returns:
List of skill directory names (e.g. ['bmad-agent-builder', 'bmad-builder-setup'])
"""
skills = []
root = Path(base_path)
if not root.exists():
return skills
# Direct child: {name}/SKILL.md
for skill_md in root.glob("*/SKILL.md"):
skills.append(skill_md.parent.name)
# Skills subfolder: skills/{name}/SKILL.md
skills_root = root / "skills"
if skills_root.exists():
for skill_md in skills_root.glob("*/SKILL.md"):
skills.append(skill_md.parent.name)
return sorted(set(skills))
def verify_skills_installed(
bmad_dir: str, dirs_to_check: list, skills_dir: str, verbose: bool = False
) -> list:
"""Verify that skills in legacy directories exist at the installed location.
Scans each directory in dirs_to_check for skill folders (containing SKILL.md),
then checks that a matching directory exists under skills_dir. Directories
that contain no skills (like _config/) are silently skipped.
Returns:
List of verified skill names.
Raises SystemExit(1) if any skills are missing from skills_dir.
"""
all_verified = []
missing = []
for dirname in dirs_to_check:
legacy_path = Path(bmad_dir) / dirname
if not legacy_path.exists():
continue
skill_names = find_skill_dirs(str(legacy_path))
if not skill_names:
if verbose:
print(
f"No skills found in {dirname}/ — skipping verification",
file=sys.stderr,
)
continue
for skill_name in skill_names:
installed_path = Path(skills_dir) / skill_name
if installed_path.is_dir():
all_verified.append(skill_name)
if verbose:
print(
f"Verified: {skill_name} exists at {installed_path}",
file=sys.stderr,
)
else:
missing.append(skill_name)
if verbose:
print(
f"MISSING: {skill_name} not found at {installed_path}",
file=sys.stderr,
)
if missing:
error_result = {
"status": "error",
"error": "Skills not found at installed location",
"missing_skills": missing,
"skills_dir": str(Path(skills_dir).resolve()),
}
print(json.dumps(error_result, indent=2))
sys.exit(1)
return sorted(set(all_verified))
def count_files(path: Path) -> int:
"""Count all files recursively in a directory."""
count = 0
for item in path.rglob("*"):
if item.is_file():
count += 1
return count
def cleanup_directories(
bmad_dir: str, dirs_to_remove: list, verbose: bool = False
) -> tuple:
"""Remove specified directories under bmad_dir.
Preserves config.yaml files if present (needed by bmad-init at runtime).
Returns:
(removed, not_found, total_files_removed) tuple
"""
removed = []
not_found = []
total_files = 0
for dirname in dirs_to_remove:
target = Path(bmad_dir) / dirname
if not target.exists():
not_found.append(dirname)
if verbose:
print(f"Not found (skipping): {target}", file=sys.stderr)
continue
if not target.is_dir():
if verbose:
print(f"Not a directory (skipping): {target}", file=sys.stderr)
not_found.append(dirname)
continue
# Validate directory name to prevent path traversal
if ".." in dirname or "/" in dirname or "\\" in dirname:
error_result = {
"status": "error",
"error": f"Invalid directory name (path traversal rejected): {dirname}",
"directories_removed": removed,
"directories_failed": dirname,
}
print(json.dumps(error_result, indent=2))
sys.exit(2)
# Preserve config.yaml if present (bmad-init needs per-module configs)
config_path = target / "config.yaml"
config_backup = None
if config_path.exists():
config_backup = config_path.read_bytes()
if verbose:
print(f"Preserving config.yaml in {dirname}/", file=sys.stderr)
file_count = count_files(target)
if config_backup is not None:
file_count -= 1 # Don't count the preserved file
if verbose:
print(
f"Removing {target} ({file_count} files)",
file=sys.stderr,
)
try:
shutil.rmtree(target)
# Restore preserved config.yaml
if config_backup is not None:
target.mkdir(parents=True, exist_ok=True)
config_path.write_bytes(config_backup)
if verbose:
print(
f"Restored config.yaml in {dirname}/",
file=sys.stderr,
)
except OSError as e:
logger.error("Failed during cleanup of %s: %s", target, e)
error_result = {
"status": "error",
"error": f"Failed to remove {target}: {e}",
"directories_removed": removed,
"directories_failed": dirname,
}
print(json.dumps(error_result, indent=2))
sys.exit(2)
removed.append(dirname)
total_files += file_count
return removed, not_found, total_files
def main():
args = parse_args()
bmad_dir = args.bmad_dir
module_code = args.module_code
# Build the list of directories to remove
dirs_to_remove = [module_code, "core"] + args.also_remove
# Deduplicate while preserving order
seen = set()
unique_dirs = []
for d in dirs_to_remove:
if d not in seen:
seen.add(d)
unique_dirs.append(d)
dirs_to_remove = unique_dirs
if args.verbose:
print(f"Directories to remove: {dirs_to_remove}", file=sys.stderr)
# Safety check: verify skills are installed before removing
verified_skills = None
if args.skills_dir:
if args.verbose:
print(
f"Verifying skills installed at {args.skills_dir}",
file=sys.stderr,
)
verified_skills = verify_skills_installed(
bmad_dir, dirs_to_remove, args.skills_dir, args.verbose
)
# Remove directories
removed, not_found, total_files = cleanup_directories(
bmad_dir, dirs_to_remove, args.verbose
)
# Build result
result = {
"status": "success",
"bmad_dir": str(Path(bmad_dir).resolve()),
"directories_removed": removed,
"directories_not_found": not_found,
"files_removed_count": total_files,
}
if args.skills_dir:
result["safety_checks"] = {
"skills_verified": True,
"skills_dir": str(Path(args.skills_dir).resolve()),
"verified_skills": verified_skills,
}
else:
result["safety_checks"] = None
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()