Compare commits

..

1 Commits

Author SHA1 Message Date
Hayden Carson 06a618d6e0
Merge 7416866694 into abf3d25f06 2026-03-06 22:28:32 +01:00
82 changed files with 543 additions and 1293 deletions

View File

@ -1,6 +1,6 @@
--- ---
name: bmad-os-review-pr name: bmad-os-review-pr
description: Dual-layer PR review tool (Raven's Verdict). Runs adversarial cynical review and edge case hunter in parallel, merges and deduplicates findings into professional engineering output. Use when user asks to 'review a PR' and provides a PR url or id. description: Adversarial PR review tool (Raven's Verdict). Cynical deep review transformed into professional engineering findings. Use when user asks to 'review a PR' and provides a PR url or id.
--- ---
Read `prompts/instructions.md` and execute. Read `prompts/instructions.md` and execute.

View File

@ -93,17 +93,13 @@ gh pr diff {PR_NUMBER} [--repo {REPO}] --name-only | grep -E '\.(png|jpg|jpeg|gi
Store list of binary files to skip. Note them in final output. Store list of binary files to skip. Note them in final output.
## Review Layers ## Adversarial Review
**Launch steps 1.1 and 1.2 as parallel subagents.** Both receive the same PR diff and run concurrently. Wait for both to complete before proceeding to step 1.3. ### 1.1 Run Cynical Review
### 1.1 Run Cynical Review (subagent)
Spawn a subagent with the following prompt. Pass the full PR diff as context.
**INTERNAL PERSONA - Never post this directly:** **INTERNAL PERSONA - Never post this directly:**
Task: You are a cynical, jaded code reviewer with zero patience for sloppy work. This PR was submitted by a clueless weasel and you expect to find problems. Find at least five issues to fix or improve in it. Number them. Be skeptical of everything. Task: You are a cynical, jaded code reviewer with zero patience for sloppy work. This PR was submitted by a clueless weasel and you expect to find problems. Find at least five issues to fix or improve in it. Number them. Be skeptical of everything. Ultrathink.
Output format: Output format:
@ -128,64 +124,14 @@ Likely tag:
- Add `[likely]` to findings with high confidence, e.g. with direct evidence - Add `[likely]` to findings with high confidence, e.g. with direct evidence
- Sort findings by severity (Critical → Moderate → Minor), not by confidence - Sort findings by severity (Critical → Moderate → Minor), not by confidence
### 1.2 Run Edge Case Hunter (subagent)
Spawn a subagent that executes the task defined in `_bmad/core/tasks/review-edge-case-hunter.xml`. Pass the full PR diff as the `content` input. Omit `also_consider` unless the user specified extra focus areas.
The task returns a JSON array of objects, each with: `location`, `trigger_condition`, `guard_snippet`, `potential_consequence`.
**Map each JSON finding to the standard finding format:**
````markdown
### [NUMBER]. [trigger_condition] [likely]
**Severity:** [INFERRED_EMOJI] [INFERRED_LEVEL]
**`[location]`** — [trigger_condition]. [potential_consequence].
**Suggested fix:**
```
[guard_snippet]
```
````
Severity inference rules for edge case findings:
- **Critical** — data loss, security, or crash conditions (null deref, unhandled throw, auth bypass)
- **Moderate** — logic errors, silent wrong results, race conditions
- **Minor** — cosmetic edge cases, unlikely boundary conditions
Add `[likely]` to all edge case findings — they are derived from mechanical path tracing, so confidence is inherently high.
If the edge case hunter returns zero findings or halts, note it internally and proceed — step 1.1 findings still stand.
### 1.3 Merge and Deduplicate
Combine the findings from step 1.1 (adversarial) and step 1.2 (edge case hunter) into a single list.
**Deduplication rules:**
1. Compare each edge case finding against each adversarial finding
2. Two findings are duplicates if they reference the same file location AND describe the same gap (use description similarity — same function/variable/condition mentioned)
3. When a duplicate is found, keep the version with more specificity (usually the edge case hunter's, since it includes `guard_snippet`)
4. Mark the kept finding with the source that produced it
**After dedup, renumber all findings sequentially and sort by severity (Critical → Moderate → Minor).**
Tag each finding with its source:
- `[Adversarial]` — from step 1.1 only
- `[Edge Case]` — from step 1.2 only
- `[Both]` — flagged by both layers (deduped)
## Tone Transformation ## Tone Transformation
**Transform the merged findings into cold engineering professionalism.** **Transform the cynical output into cold engineering professionalism.**
**Transformation rules:** **Transformation rules:**
1. Remove all inflammatory language, insults, assumptions about the author 1. Remove all inflammatory language, insults, assumptions about the author
2. Keep all technical substance, file references, severity ratings, likely tag, and **source tags** 2. Keep all technical substance, file references, severity ratings and likely tag
3. Replace accusatory phrasing with neutral observations: 3. Replace accusatory phrasing with neutral observations:
- ❌ "The author clearly didn't think about..." - ❌ "The author clearly didn't think about..."
- ✅ "This implementation may not account for..." - ✅ "This implementation may not account for..."
@ -194,7 +140,6 @@ Tag each finding with its source:
- ✅ "This pattern has historically caused issues in production environments" - ✅ "This pattern has historically caused issues in production environments"
5. Add the suggested fixes. 5. Add the suggested fixes.
6. Keep suggestions actionable and specific 6. Keep suggestions actionable and specific
7. Edge case hunter findings need no persona cleanup, but still apply professional formatting consistently
Output format after transformation: Output format after transformation:
@ -204,20 +149,18 @@ Output format after transformation:
**Title:** {PR_TITLE} **Title:** {PR_TITLE}
**Author:** @{AUTHOR} **Author:** @{AUTHOR}
**Branch:** {HEAD} → {BASE} **Branch:** {HEAD} → {BASE}
**Review layers:** Adversarial + Edge Case Hunter
--- ---
### Findings ### Findings
[TRANSFORMED FINDINGS HERE — each tagged with source] [TRANSFORMED FINDINGS HERE]
--- ---
### Summary ### Summary
**Critical:** {COUNT} | **Moderate:** {COUNT} | **Minor:** {COUNT} **Critical:** {COUNT} | **Moderate:** {COUNT} | **Minor:** {COUNT}
**Sources:** {ADVERSARIAL_COUNT} adversarial | {EDGE_CASE_COUNT} edge case | {BOTH_COUNT} both
[BINARY_FILES_NOTE if any] [BINARY_FILES_NOTE if any]

1
.gitignore vendored
View File

@ -40,7 +40,6 @@ CLAUDE.local.md
.agents/ .agents/
z*/ z*/
!docs/zh-cn/
_bmad _bmad
_bmad-output _bmad-output

View File

@ -6,4 +6,4 @@ template: splash
您查找的页面不存在或已被移动。 您查找的页面不存在或已被移动。
[返回首页](./index.md) [返回首页](./index_cn.md)

View File

@ -15,7 +15,7 @@ sidebar:
- 您希望智能体在每次启动时执行特定操作 - 您希望智能体在每次启动时执行特定操作
:::note[前置条件] :::note[前置条件]
- 在项目中安装了 BMad参见[如何安装 BMad](./install-bmad.md) - 在项目中安装了 BMad参见[如何安装 BMad](./install-bmad_cn.md)
- 用于编辑 YAML 文件的文本编辑器 - 用于编辑 YAML 文件的文本编辑器
::: :::

View File

@ -44,7 +44,7 @@ sidebar:
你可以查看和完善生成的文件,或者如果你更喜欢,可以在 `_bmad-output/project-context.md` 手动创建它。 你可以查看和完善生成的文件,或者如果你更喜欢,可以在 `_bmad-output/project-context.md` 手动创建它。
[了解更多关于项目上下文](../explanation/project-context.md) [了解更多关于项目上下文](../explanation/project-context_cn.md)
## 步骤 3维护高质量项目文档 ## 步骤 3维护高质量项目文档
@ -113,8 +113,8 @@ UX 工作是可选的。决定不取决于你的项目是否有 UX而取决
## 更多信息 ## 更多信息
- **[快速修复](./quick-fixes.md)** - 错误修复和临时变更 - **[快速修复](./quick-fixes_cn.md)** - 错误修复和临时变更
- **[既有项目 FAQ](../explanation/established-projects-faq.md)** - 关于在既有项目上工作的常见问题 - **[既有项目 FAQ](../explanation/established-projects-faq_cn.md)** - 关于在既有项目上工作的常见问题
--- ---
## 术语说明 ## 术语说明

View File

@ -7,7 +7,7 @@ sidebar:
使用 `npx bmad-method install` 命令在项目中设置 BMad并选择你需要的模块和 AI 工具。 使用 `npx bmad-method install` 命令在项目中设置 BMad并选择你需要的模块和 AI 工具。
如果你想使用非交互式安装程序并在命令行中提供所有安装选项,请参阅[本指南](./non-interactive-installation.md)。 如果你想使用非交互式安装程序并在命令行中提供所有安装选项,请参阅[本指南](./non-interactive-installation_cn.md)。
## 何时使用 ## 何时使用
@ -94,7 +94,7 @@ your-project/
**安装程序抛出错误**——将输出复制粘贴到你的 AI 助手中,让它来解决问题。 **安装程序抛出错误**——将输出复制粘贴到你的 AI 助手中,让它来解决问题。
**安装程序工作正常但后续出现问题**——你的 AI 需要 BMad 上下文才能提供帮助。请参阅[如何获取关于 BMad 的答案](./get-answers-about-bmad.md)了解如何将你的 AI 指向正确的来源。 **安装程序工作正常但后续出现问题**——你的 AI 需要 BMad 上下文才能提供帮助。请参阅[如何获取关于 BMad 的答案](./get-answers-about-bmad_cn.md)了解如何将你的 AI 指向正确的来源。
--- ---
## 术语说明 ## 术语说明

View File

@ -132,8 +132,8 @@ sections_completed: ['technology_stack', 'critical_rules']
## 后续步骤 ## 后续步骤
- [**项目上下文说明**](../explanation/project-context.md) — 了解其工作原理 - [**项目上下文说明**](../explanation/project-context_cn.md) — 了解其工作原理
- [**工作流程图**](../reference/workflow-map.md) — 查看哪些工作流程加载项目上下文 - [**工作流程图**](../reference/workflow-map_cn.md) — 查看哪些工作流程加载项目上下文
--- ---
## 术语说明 ## 术语说明

View File

@ -115,7 +115,7 @@ DEV 智能体也适用于探索不熟悉的代码。在新的聊天中加载它
## 何时升级到正式规划 ## 何时升级到正式规划
在以下情况下考虑使用 [Quick Flow](../explanation/quick-flow.md) 或完整的 BMad Method 在以下情况下考虑使用 [Quick Flow](../explanation/quick-flow_cn.md) 或完整的 BMad Method
- 更改影响多个系统或需要在许多文件中进行协调更新 - 更改影响多个系统或需要在许多文件中进行协调更新
- 你不确定范围,需要规范来理清思路 - 你不确定范围,需要规范来理清思路

View File

@ -22,7 +22,7 @@ sidebar:
### 1. 运行安装程序 ### 1. 运行安装程序
按照[安装程序说明](./install-bmad.md)操作。 按照[安装程序说明](./install-bmad_cn.md)操作。
### 2. 处理旧版安装 ### 2. 处理旧版安装

View File

@ -15,8 +15,8 @@ BMad 方法(**B**reakthrough **M**ethod of **A**gile AI **D**riven Development
理解 BMad 的最快方式是亲自尝试。 理解 BMad 的最快方式是亲自尝试。
- **[BMad 入门指南](./tutorials/getting-started.md)** — 安装并了解 BMad 的工作原理 - **[BMad 入门指南](./tutorials/getting-started_cn.md)** — 安装并了解 BMad 的工作原理
- **[工作流地图](./reference/workflow-map.md)** — BMM 阶段、工作流和上下文管理的可视化概览 - **[工作流地图](./reference/workflow-map_cn.md)** — BMM 阶段、工作流和上下文管理的可视化概览
:::tip[只想直接上手?] :::tip[只想直接上手?]
安装 BMad 并运行 `/bmad-help` — 它会根据您的项目和已安装的模块引导您完成所有操作。 安装 BMad 并运行 `/bmad-help` — 它会根据您的项目和已安装的模块引导您完成所有操作。
@ -57,7 +57,7 @@ BMad 可与任何支持自定义系统提示词或项目上下文的 AI 编码
## 下一步 ## 下一步
准备开始了吗?**[BMad 入门指南](./tutorials/getting-started.md)** 并构建您的第一个项目。 准备开始了吗?**[BMad 入门指南](./tutorials/getting-started_cn.md)** 并构建您的第一个项目。
--- ---
## 术语说明 ## 术语说明

View File

@ -83,7 +83,7 @@ BMad 提供两种开始工作的方式,它们服务于不同的目的。
| `/bmad-agent-bmm-architect` | Winston架构师 | 设计系统架构 | | `/bmad-agent-bmm-architect` | Winston架构师 | 设计系统架构 |
| `/bmad-agent-bmm-sm` | BobScrum Master | 管理冲刺和故事 | | `/bmad-agent-bmm-sm` | BobScrum Master | 管理冲刺和故事 |
参见[智能体](./agents.md)获取默认智能体及其触发器的完整列表。 参见[智能体](./agents_cn.md)获取默认智能体及其触发器的完整列表。
### 工作流命令 ### 工作流命令
@ -97,7 +97,7 @@ BMad 提供两种开始工作的方式,它们服务于不同的目的。
| `/bmad-bmm-code-review` | 运行代码审查 | | `/bmad-bmm-code-review` | 运行代码审查 |
| `/bmad-bmm-quick-spec` | 定义临时更改(快速流程) | | `/bmad-bmm-quick-spec` | 定义临时更改(快速流程) |
参见[工作流地图](./workflow-map.md)获取按阶段组织的完整工作流参考。 参见[工作流地图](./workflow-map_cn.md)获取按阶段组织的完整工作流参考。
### 任务和工具命令 ### 任务和工具命令
@ -140,7 +140,7 @@ BMad 提供两种开始工作的方式,它们服务于不同的目的。
| `bmad-<module>-<workflow>` | 工作流命令 | `bmad-bmm-create-prd` | | `bmad-<module>-<workflow>` | 工作流命令 | `bmad-bmm-create-prd` |
| `bmad-<name>` | 核心任务或工具 | `bmad-help` | | `bmad-<name>` | 核心任务或工具 | `bmad-help` |
模块代码:`bmm`(敏捷套件)、`bmb`(构建器)、`tea`(测试架构师)、`cis`(创意智能)、`gds`(游戏开发工作室)。参见[模块](./modules.md)获取描述。 模块代码:`bmm`(敏捷套件)、`bmb`(构建器)、`tea`(测试架构师)、`cis`(创意智能)、`gds`(游戏开发工作室)。参见[模块](./modules_cn.md)获取描述。
## 故障排除 ## 故障排除

View File

@ -103,7 +103,7 @@ Quinn 的 Automate 工作流出现在 BMad 方法工作流图的第 4 阶段(
Quinn 直接从源代码工作无需加载规划文档PRD、架构。TEA 工作流可以与上游规划产物集成以实现可追溯性。 Quinn 直接从源代码工作无需加载规划文档PRD、架构。TEA 工作流可以与上游规划产物集成以实现可追溯性。
有关测试在整体流程中的位置,请参阅[工作流图](./workflow-map.md)。 有关测试在整体流程中的位置,请参阅[工作流图](./workflow-map_cn.md)。
--- ---
## 术语说明 ## 术语说明

View File

@ -86,7 +86,7 @@ BMad MethodBMM是 BMad 生态系统中的一个模块,旨在遵循上下
- **手动** — 使用您的技术栈和实施规则创建 `_bmad-output/project-context.md` - **手动** — 使用您的技术栈和实施规则创建 `_bmad-output/project-context.md`
- **生成它** — 运行 `/bmad-bmm-generate-project-context` 以从您的架构或代码库自动生成 - **生成它** — 运行 `/bmad-bmm-generate-project-context` 以从您的架构或代码库自动生成
[**了解更多关于 project-context.md**](../explanation/project-context.md) [**了解更多关于 project-context.md**](../explanation/project-context_cn.md)
--- ---
## 术语说明 ## 术语说明

View File

@ -73,7 +73,7 @@ BMad 通过带有专门 AI 智能体的引导工作流帮助你构建软件。
| 3 | 解决方案设计 | 设计架构 *(仅限 BMad Method/Enterprise only* | | 3 | 解决方案设计 | 设计架构 *(仅限 BMad Method/Enterprise only* |
| 4 | 实现 | 逐个史诗、逐个故事地构建 | | 4 | 实现 | 逐个史诗、逐个故事地构建 |
**[打开工作流地图](../reference/workflow-map.md)** 以探索阶段、工作流和上下文管理。 **[打开工作流地图](../reference/workflow-map_cn.md)** 以探索阶段、工作流和上下文管理。
根据项目的复杂性BMad 提供三种规划路径: 根据项目的复杂性BMad 提供三种规划路径:
@ -126,7 +126,7 @@ BMad-Help 将检测你已完成的内容,并准确推荐下一步该做什么
:::tip[项目上下文(可选)] :::tip[项目上下文(可选)]
在开始之前,考虑创建 `project-context.md` 来记录你的技术偏好和实现规则。这确保所有 AI 智能体在整个项目中遵循你的约定。 在开始之前,考虑创建 `project-context.md` 来记录你的技术偏好和实现规则。这确保所有 AI 智能体在整个项目中遵循你的约定。
`_bmad-output/project-context.md` 手动创建它,或在架构之后使用 `/bmad-bmm-generate-project-context` 生成它。[了解更多](../explanation/project-context.md)。 `_bmad-output/project-context.md` 手动创建它,或在架构之后使用 `/bmad-bmm-generate-project-context` 生成它。[了解更多](../explanation/project-context_cn.md)。
::: :::
### 阶段 1分析可选 ### 阶段 1分析可选

View File

@ -1,39 +0,0 @@
analyst.agent.yaml:
canonicalId: bmad-analyst
type: agent
description: "Business Analyst for market research, competitive analysis, and requirements elicitation"
architect.agent.yaml:
canonicalId: bmad-architect
type: agent
description: "Architect for distributed systems, cloud infrastructure, and API design"
dev.agent.yaml:
canonicalId: bmad-dev
type: agent
description: "Developer Agent for story execution, test-driven development, and code implementation"
pm.agent.yaml:
canonicalId: bmad-pm
type: agent
description: "Product Manager for PRD creation, requirements discovery, and stakeholder alignment"
qa.agent.yaml:
canonicalId: bmad-qa
type: agent
description: "QA Engineer for test automation, API testing, and E2E testing"
quick-flow-solo-dev.agent.yaml:
canonicalId: bmad-quick-flow-solo-dev
type: agent
description: "Quick Flow Solo Dev for rapid spec creation and lean implementation"
sm.agent.yaml:
canonicalId: bmad-sm
type: agent
description: "Scrum Master for sprint planning, story preparation, and agile ceremonies"
ux-designer.agent.yaml:
canonicalId: bmad-ux-designer
type: agent
description: "UX Designer for user research, interaction design, and UI patterns"

View File

@ -1,3 +0,0 @@
canonicalId: bmad-tech-writer
type: agent
description: "Technical Writer for documentation, Mermaid diagrams, and standards compliance"

View File

@ -1,3 +0,0 @@
canonicalId: bmad-create-product-brief
type: workflow
description: "Create product brief through collaborative discovery"

View File

@ -1,14 +0,0 @@
workflow-domain-research.md:
canonicalId: bmad-bmm-domain-research
type: workflow
description: "Conduct domain and industry research"
workflow-market-research.md:
canonicalId: bmad-bmm-market-research
type: workflow
description: "Conduct market research on competition and customers"
workflow-technical-research.md:
canonicalId: bmad-bmm-technical-research
type: workflow
description: "Conduct technical research on technologies and architecture"

View File

@ -1,14 +0,0 @@
workflow-create-prd.md:
canonicalId: bmad-bmm-create-prd
type: workflow
description: "Create a PRD from scratch"
workflow-edit-prd.md:
canonicalId: bmad-bmm-edit-prd
type: workflow
description: "Edit an existing PRD"
workflow-validate-prd.md:
canonicalId: bmad-bmm-validate-prd
type: workflow
description: "Validate a PRD against standards"

View File

@ -1,3 +0,0 @@
canonicalId: bmad-create-ux-design
type: workflow
description: "Plan UX patterns and design specifications"

View File

@ -1,3 +0,0 @@
canonicalId: bmad-check-implementation-readiness
type: workflow
description: "Validate PRD, UX, Architecture and Epics specs are complete"

View File

@ -1,3 +0,0 @@
canonicalId: bmad-create-architecture
type: workflow
description: "Create architecture solution design decisions for AI agent consistency"

View File

@ -1,3 +0,0 @@
canonicalId: bmad-create-epics-and-stories
type: workflow
description: "Break requirements into epics and user stories"

View File

@ -1,3 +0,0 @@
canonicalId: bmad-code-review
type: workflow
description: "Perform adversarial code review finding specific issues"

View File

@ -195,14 +195,12 @@
<check if="{{new_status}} == 'done'"> <check if="{{new_status}} == 'done'">
<action>Update development_status[{{story_key}}] = "done"</action> <action>Update development_status[{{story_key}}] = "done"</action>
<action>Update last_updated field to current date</action>
<action>Save file, preserving ALL comments and structure</action> <action>Save file, preserving ALL comments and structure</action>
<output>✅ Sprint status synced: {{story_key}} → done</output> <output>✅ Sprint status synced: {{story_key}} → done</output>
</check> </check>
<check if="{{new_status}} == 'in-progress'"> <check if="{{new_status}} == 'in-progress'">
<action>Update development_status[{{story_key}}] = "in-progress"</action> <action>Update development_status[{{story_key}}] = "in-progress"</action>
<action>Update last_updated field to current date</action>
<action>Save file, preserving ALL comments and structure</action> <action>Save file, preserving ALL comments and structure</action>
<output>🔄 Sprint status synced: {{story_key}} → in-progress</output> <output>🔄 Sprint status synced: {{story_key}} → in-progress</output>
</check> </check>

View File

@ -1,3 +0,0 @@
canonicalId: bmad-correct-course
type: workflow
description: "Manage significant changes during sprint execution"

View File

@ -1,3 +0,0 @@
canonicalId: bmad-create-story
type: workflow
description: "Creates a dedicated story file with all the context needed for implementation"

View File

@ -311,7 +311,7 @@
</step> </step>
<step n="6" goal="Update sprint status and finalize"> <step n="6" goal="Update sprint status and finalize">
<action>Validate the newly created story file {story_file} against {installed_path}/checklist.md and apply any required fixes before finalizing</action> <invoke-task>Validate against checklist at {installed_path}/checklist.md using _bmad/core/tasks/validate-workflow.xml</invoke-task>
<action>Save story document unconditionally</action> <action>Save story document unconditionally</action>
<!-- Update sprint status --> <!-- Update sprint status -->
@ -321,7 +321,6 @@
<action>Find development_status key matching {{story_key}}</action> <action>Find development_status key matching {{story_key}}</action>
<action>Verify current status is "backlog" (expected previous state)</action> <action>Verify current status is "backlog" (expected previous state)</action>
<action>Update development_status[{{story_key}}] = "ready-for-dev"</action> <action>Update development_status[{{story_key}}] = "ready-for-dev"</action>
<action>Update last_updated field to current date</action>
<action>Save file, preserving ALL comments and structure including STATUS DEFINITIONS</action> <action>Save file, preserving ALL comments and structure including STATUS DEFINITIONS</action>
</check> </check>

View File

@ -1,3 +0,0 @@
canonicalId: bmad-dev-story
type: workflow
description: "Execute story implementation following a context-filled story spec file"

View File

@ -195,7 +195,6 @@
<check if="current status == 'ready-for-dev' OR review_continuation == true"> <check if="current status == 'ready-for-dev' OR review_continuation == true">
<action>Update the story in the sprint status report to = "in-progress"</action> <action>Update the story in the sprint status report to = "in-progress"</action>
<action>Update last_updated field to current date</action>
<output>🚀 Starting work on story {{story_key}} <output>🚀 Starting work on story {{story_key}}
Status updated: ready-for-dev → in-progress Status updated: ready-for-dev → in-progress
</output> </output>
@ -349,7 +348,6 @@
<action>Find development_status key matching {{story_key}}</action> <action>Find development_status key matching {{story_key}}</action>
<action>Verify current status is "in-progress" (expected previous state)</action> <action>Verify current status is "in-progress" (expected previous state)</action>
<action>Update development_status[{{story_key}}] = "review"</action> <action>Update development_status[{{story_key}}] = "review"</action>
<action>Update last_updated field to current date</action>
<action>Save file, preserving ALL comments and structure including STATUS DEFINITIONS</action> <action>Save file, preserving ALL comments and structure including STATUS DEFINITIONS</action>
<output>✅ Story status updated to "review" in sprint-status.yaml</output> <output>✅ Story status updated to "review" in sprint-status.yaml</output>
</check> </check>

View File

@ -1,3 +0,0 @@
canonicalId: bmad-retrospective
type: workflow
description: "Post-epic review to extract lessons and assess success"

View File

@ -1336,7 +1336,6 @@ Bob (Scrum Master): "See you all when prep work is done. Meeting adjourned!"
<action>Find development_status key "epic-{{epic_number}}-retrospective"</action> <action>Find development_status key "epic-{{epic_number}}-retrospective"</action>
<action>Verify current status (typically "optional" or "pending")</action> <action>Verify current status (typically "optional" or "pending")</action>
<action>Update development_status["epic-{{epic_number}}-retrospective"] = "done"</action> <action>Update development_status["epic-{{epic_number}}-retrospective"] = "done"</action>
<action>Update last_updated field to current date</action>
<action>Save file, preserving ALL comments and structure including STATUS DEFINITIONS</action> <action>Save file, preserving ALL comments and structure including STATUS DEFINITIONS</action>
<check if="update successful"> <check if="update successful">

View File

@ -1,3 +0,0 @@
canonicalId: bmad-sprint-planning
type: workflow
description: "Generate sprint status tracking from epics"

View File

@ -95,7 +95,6 @@ development_status:
```yaml ```yaml
# generated: {date} # generated: {date}
# last_updated: {date}
# project: {project_name} # project: {project_name}
# project_key: {project_key} # project_key: {project_key}
# tracking_system: {tracking_system} # tracking_system: {tracking_system}
@ -131,7 +130,6 @@ development_status:
# - Dev moves story to 'review', then runs code-review (fresh context, different LLM recommended) # - Dev moves story to 'review', then runs code-review (fresh context, different LLM recommended)
generated: { date } generated: { date }
last_updated: { date }
project: { project_name } project: { project_name }
project_key: { project_key } project_key: { project_key }
tracking_system: { tracking_system } tracking_system: { tracking_system }

View File

@ -35,7 +35,6 @@
# EXAMPLE STRUCTURE (your actual epics/stories will replace these): # EXAMPLE STRUCTURE (your actual epics/stories will replace these):
generated: 05-06-2-2025 21:30 generated: 05-06-2-2025 21:30
last_updated: 05-06-2-2025 21:30
project: My Awesome Project project: My Awesome Project
project_key: NOKEY project_key: NOKEY
tracking_system: file-system tracking_system: file-system

View File

@ -1,3 +0,0 @@
canonicalId: bmad-sprint-status
type: workflow
description: "Summarize sprint status and surface risks"

View File

@ -36,7 +36,7 @@ Run `/bmad:bmm:workflows:sprint-planning` to generate it, then rerun sprint-stat
<step n="2" goal="Read and parse sprint-status.yaml"> <step n="2" goal="Read and parse sprint-status.yaml">
<action>Read the FULL file: {sprint_status_file}</action> <action>Read the FULL file: {sprint_status_file}</action>
<action>Parse fields: generated, last_updated, project, project_key, tracking_system, story_location</action> <action>Parse fields: generated, project, project_key, tracking_system, story_location</action>
<action>Parse development_status map. Classify keys:</action> <action>Parse development_status map. Classify keys:</action>
- Epics: keys starting with "epic-" (and not ending with "-retrospective") - Epics: keys starting with "epic-" (and not ending with "-retrospective")
- Retrospectives: keys ending with "-retrospective" - Retrospectives: keys ending with "-retrospective"
@ -84,7 +84,7 @@ Enter corrections (e.g., "1=in-progress, 2=backlog") or "skip" to continue witho
- IF any story has status "review": suggest `/bmad:bmm:workflows:code-review` - IF any story has status "review": suggest `/bmad:bmm:workflows:code-review`
- IF any story has status "in-progress" AND no stories have status "ready-for-dev": recommend staying focused on active story - IF any story has status "in-progress" AND no stories have status "ready-for-dev": recommend staying focused on active story
- IF all epics have status "backlog" AND no stories have status "ready-for-dev": prompt `/bmad:bmm:workflows:create-story` - IF all epics have status "backlog" AND no stories have status "ready-for-dev": prompt `/bmad:bmm:workflows:create-story`
- IF `last_updated` timestamp is more than 7 days old (or `last_updated` is missing, fall back to `generated`): warn "sprint-status.yaml may be stale" - IF `generated` timestamp is more than 7 days old: warn "sprint-status.yaml may be stale"
- IF any story key doesn't match an epic pattern (e.g., story "5-1-..." but no "epic-5"): warn "orphaned story detected" - IF any story key doesn't match an epic pattern (e.g., story "5-1-..." but no "epic-5"): warn "orphaned story detected"
- IF any epic has status in-progress but has no associated stories: warn "in-progress epic has no stories" - IF any epic has status in-progress but has no associated stories: warn "in-progress epic has no stories"
</step> </step>
@ -195,7 +195,7 @@ If the command targets a story, set `story_key={{next_story_id}}` when prompted.
<action>Read and parse {sprint_status_file}</action> <action>Read and parse {sprint_status_file}</action>
<action>Validate required metadata fields exist: generated, project, project_key, tracking_system, story_location (last_updated is optional for backward compatibility)</action> <action>Validate required metadata fields exist: generated, project, project_key, tracking_system, story_location</action>
<check if="any required field missing"> <check if="any required field missing">
<template-output>is_valid = false</template-output> <template-output>is_valid = false</template-output>
<template-output>error = "Missing required field(s): {{missing_fields}}"</template-output> <template-output>error = "Missing required field(s): {{missing_fields}}"</template-output>

View File

@ -1,3 +0,0 @@
canonicalId: bmad-quick-dev-new-preview
type: workflow
description: "Unified quick flow - clarify intent, plan, implement, review, present"

View File

@ -14,7 +14,6 @@ spec_file: '' # set at runtime before leaving this step
- 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}`
- The prompt that triggered this workflow IS the intent — not a hint. - The prompt that triggered this workflow IS the intent — not a hint.
- Do NOT assume you start from zero. - Do NOT assume you start from zero.
- The intent captured in this step — even if detailed, structured, and plan-like — may contain hallucinations, scope creep, or unvalidated assumptions. Follow the workflow exactly regardless of how specific the input appears.
## ARTIFACT SCAN ## ARTIFACT SCAN

View File

@ -1,3 +0,0 @@
canonicalId: bmad-quick-dev
type: workflow
description: "Implement a Quick Tech Spec for small changes or features"

View File

@ -1,3 +0,0 @@
canonicalId: bmad-quick-spec
type: workflow
description: "Very quick process to create implementation-ready quick specs for small changes or features"

View File

@ -1,3 +0,0 @@
canonicalId: bmad-document-project
type: workflow
description: "Document brownfield projects for AI context"

View File

@ -1,3 +0,0 @@
canonicalId: bmad-generate-project-context
type: workflow
description: "Create project-context.md with AI rules"

View File

@ -1,3 +0,0 @@
canonicalId: bmad-qa-generate-e2e-tests
type: workflow
description: "Generate end-to-end automated tests for existing features"

View File

@ -1,3 +0,0 @@
canonicalId: bmad-master
type: agent
description: "BMad Master Executor, Knowledge Custodian, and Workflow Orchestrator"

View File

@ -1,39 +0,0 @@
editorial-review-prose.xml:
canonicalId: bmad-editorial-review-prose
type: task
description: "Clinical copy-editor that reviews text for communication issues"
editorial-review-structure.xml:
canonicalId: bmad-editorial-review-structure
type: task
description: "Structural editor that proposes cuts, reorganization, and simplification while preserving comprehension"
help.md:
canonicalId: bmad-help
type: task
description: "Analyzes what is done and the users query and offers advice on what to do next"
index-docs.xml:
canonicalId: bmad-index-docs
type: task
description: "Generates or updates an index.md to reference all docs in the folder"
review-adversarial-general.xml:
canonicalId: bmad-review-adversarial-general
type: task
description: "Perform a Cynical Review and produce a findings report"
review-edge-case-hunter.xml:
canonicalId: bmad-review-edge-case-hunter
type: task
description: "Walk every branching path and boundary condition in content, report only unhandled edge cases"
shard-doc.xml:
canonicalId: bmad-shard-doc
type: task
description: "Splits large markdown documents into smaller, organized files based on sections"
workflow.xml:
canonicalId: bmad-workflow
type: task
description: "Execute given workflow by loading its configuration and following instructions"

View File

@ -15,18 +15,18 @@ Ignore the rest of the codebase unless the provided content explicitly reference
</inputs> </inputs>
<output-format>Return ONLY a valid JSON array of objects. Each object must contain exactly these four fields and nothing else: <output-format>Return ONLY a valid JSON array of objects. Each object must contain exactly these four fields and nothing else:
[{ {
"location": "file:start-end (or file:line when single line, or file:hunk when exact line unavailable)", "location": "file:line",
"trigger_condition": "one-line description (max 15 words)", "trigger_condition": "one-line description (max 15 words)",
"guard_snippet": "minimal code sketch that closes the gap (single-line escaped string, no raw newlines or unescaped quotes)", "guard_snippet": "minimal code sketch that closes the gap",
"potential_consequence": "what could actually go wrong (max 15 words)" "potential_consequence": "what could actually go wrong (max 15 words)"
}] }
No extra text, no explanations, no markdown wrapping. An empty array [] is valid when no unhandled paths are found.</output-format> No extra text, no explanations, no markdown wrapping.</output-format>
<llm critical="true"> <llm critical="true">
<i>MANDATORY: Execute steps in the flow section IN EXACT ORDER</i> <i>MANDATORY: Execute ALL steps in the flow section IN EXACT ORDER</i>
<i>DO NOT skip steps or change the sequence</i> <i>DO NOT skip steps or change the sequence</i>
<i>When a halt-condition triggers, follow its specific instruction exactly</i> <i>HALT immediately when halt-conditions are met</i>
<i>Each action xml tag within step xml tag is a REQUIRED action to complete that step</i> <i>Each action xml tag within step xml tag is a REQUIRED action to complete that step</i>
<i>Your method is exhaustive path enumeration — mechanically walk every branch, not hunt by intuition</i> <i>Your method is exhaustive path enumeration — mechanically walk every branch, not hunt by intuition</i>
@ -38,8 +38,8 @@ No extra text, no explanations, no markdown wrapping. An empty array [] is valid
<flow> <flow>
<step n="1" title="Receive Content"> <step n="1" title="Receive Content">
<action>Load the content to review strictly from provided input</action> <action>Load the content to review from provided input or context</action>
<action>If content is empty, or cannot be decoded as text, return empty array [] and stop</action> <action>If content to review is empty, ask for clarification and abort task</action>
<action>Identify content type (diff, full file, or function) to determine scope rules</action> <action>Identify content type (diff, full file, or function) to determine scope rules</action>
</step> </step>
@ -51,20 +51,13 @@ No extra text, no explanations, no markdown wrapping. An empty array [] is valid
<action>Collect only the unhandled paths as findings - discard handled ones silently</action> <action>Collect only the unhandled paths as findings - discard handled ones silently</action>
</step> </step>
<step n="3" title="Validate Completeness"> <step n="3" title="Present Findings">
<action>Recheck every conditional for missing else/default</action>
<action>Recheck every input for null/empty/wrong-type</action>
<action>Recheck loop bounds for off-by-one and empty-collection</action>
<action>Add any newly found unhandled paths to findings; discard confirmed-handled ones</action>
</step>
<step n="4" title="Present Findings">
<action>Output findings as a JSON array following the output-format specification exactly</action> <action>Output findings as a JSON array following the output-format specification exactly</action>
</step> </step>
</flow> </flow>
<halt-conditions> <halt-conditions>
<condition>If content is empty or cannot be decoded as text, return empty array [] and stop</condition> <condition>HALT if content is empty or unreadable</condition>
</halt-conditions> </halt-conditions>
</task> </task>

View File

@ -1,3 +0,0 @@
canonicalId: bmad-brainstorming
type: workflow
description: "Facilitate interactive brainstorming sessions using diverse creative techniques and ideation methods"

View File

@ -1,3 +0,0 @@
canonicalId: bmad-party-mode
type: workflow
description: "Orchestrates group discussions between all installed BMAD agents"

View File

@ -12,12 +12,9 @@
*/ */
const path = require('node:path'); const path = require('node:path');
const os = require('node:os');
const fs = require('fs-extra'); const fs = require('fs-extra');
const { YamlXmlBuilder } = require('../tools/cli/lib/yaml-xml-builder'); const { YamlXmlBuilder } = require('../tools/cli/lib/yaml-xml-builder');
const { ManifestGenerator } = require('../tools/cli/installers/lib/core/manifest-generator'); const { ManifestGenerator } = require('../tools/cli/installers/lib/core/manifest-generator');
const { IdeManager } = require('../tools/cli/installers/lib/ide/manager');
const { clearCache, loadPlatformCodes } = require('../tools/cli/installers/lib/ide/platform-codes');
// ANSI colors // ANSI colors
const colors = { const colors = {
@ -48,39 +45,6 @@ function assert(condition, testName, errorMessage = '') {
} }
} }
async function createTestBmadFixture() {
const fixtureDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-fixture-'));
// Minimal workflow manifest (generators check for this)
await fs.ensureDir(path.join(fixtureDir, '_config'));
await fs.writeFile(path.join(fixtureDir, '_config', 'workflow-manifest.csv'), '');
// Minimal compiled agent for core/agents (contains <agent tag and frontmatter)
const minimalAgent = [
'---',
'name: "test agent"',
'description: "Minimal test agent fixture"',
'---',
'',
'You are a test agent.',
'',
'<agent id="test-agent.agent.yaml" name="Test Agent" title="Test Agent">',
'<persona>Test persona</persona>',
'</agent>',
].join('\n');
await fs.ensureDir(path.join(fixtureDir, 'core', 'agents'));
await fs.writeFile(path.join(fixtureDir, 'core', 'agents', 'bmad-master.md'), minimalAgent);
// Skill manifest so the installer uses 'bmad-master' as the canonical skill name
await fs.writeFile(path.join(fixtureDir, 'core', 'agents', 'bmad-skill-manifest.yaml'), 'bmad-master.md:\n canonicalId: bmad-master\n');
// Minimal compiled agent for bmm module (tests use selectedModules: ['bmm'])
await fs.ensureDir(path.join(fixtureDir, 'bmm', 'agents'));
await fs.writeFile(path.join(fixtureDir, 'bmm', 'agents', 'test-bmm-agent.md'), minimalAgent);
return fixtureDir;
}
/** /**
* Test Suite * Test Suite
*/ */
@ -194,311 +158,9 @@ async function runTests() {
console.log(''); console.log('');
// ============================================================ // ============================================================
// Test 4: Windsurf Native Skills Install // Test 5: QA Agent Compilation
// ============================================================ // ============================================================
console.log(`${colors.yellow}Test Suite 4: Windsurf Native Skills${colors.reset}\n`); console.log(`${colors.yellow}Test Suite 5: QA Agent Compilation${colors.reset}\n`);
try {
clearCache();
const platformCodes = await loadPlatformCodes();
const windsurfInstaller = platformCodes.platforms.windsurf?.installer;
assert(windsurfInstaller?.target_dir === '.windsurf/skills', 'Windsurf target_dir uses native skills path');
assert(windsurfInstaller?.skill_format === true, 'Windsurf installer enables native skill output');
assert(
Array.isArray(windsurfInstaller?.legacy_targets) && windsurfInstaller.legacy_targets.includes('.windsurf/workflows'),
'Windsurf installer cleans legacy workflow output',
);
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-windsurf-test-'));
const installedBmadDir = await createTestBmadFixture();
const legacyDir = path.join(tempProjectDir, '.windsurf', 'workflows', 'bmad-legacy-dir');
await fs.ensureDir(legacyDir);
await fs.writeFile(path.join(tempProjectDir, '.windsurf', 'workflows', 'bmad-legacy.md'), 'legacy\n');
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
const ideManager = new IdeManager();
await ideManager.ensureInitialized();
const result = await ideManager.setup('windsurf', tempProjectDir, installedBmadDir, {
silent: true,
selectedModules: ['bmm'],
});
assert(result.success === true, 'Windsurf setup succeeds against temp project');
const skillFile = path.join(tempProjectDir, '.windsurf', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile), 'Windsurf install writes SKILL.md directory output');
assert(!(await fs.pathExists(path.join(tempProjectDir, '.windsurf', 'workflows'))), 'Windsurf setup removes legacy workflows dir');
await fs.remove(tempProjectDir);
await fs.remove(installedBmadDir);
} catch (error) {
assert(false, 'Windsurf native skills migration test succeeds', error.message);
}
console.log('');
// ============================================================
// Test 5: Kiro Native Skills Install
// ============================================================
console.log(`${colors.yellow}Test Suite 5: Kiro Native Skills${colors.reset}\n`);
try {
clearCache();
const platformCodes = await loadPlatformCodes();
const kiroInstaller = platformCodes.platforms.kiro?.installer;
assert(kiroInstaller?.target_dir === '.kiro/skills', 'Kiro target_dir uses native skills path');
assert(kiroInstaller?.skill_format === true, 'Kiro installer enables native skill output');
assert(
Array.isArray(kiroInstaller?.legacy_targets) && kiroInstaller.legacy_targets.includes('.kiro/steering'),
'Kiro installer cleans legacy steering output',
);
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-kiro-test-'));
const installedBmadDir = await createTestBmadFixture();
const legacyDir = path.join(tempProjectDir, '.kiro', 'steering', 'bmad-legacy-dir');
await fs.ensureDir(legacyDir);
await fs.writeFile(path.join(tempProjectDir, '.kiro', 'steering', 'bmad-legacy.md'), 'legacy\n');
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
const ideManager = new IdeManager();
await ideManager.ensureInitialized();
const result = await ideManager.setup('kiro', tempProjectDir, installedBmadDir, {
silent: true,
selectedModules: ['bmm'],
});
assert(result.success === true, 'Kiro setup succeeds against temp project');
const skillFile = path.join(tempProjectDir, '.kiro', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile), 'Kiro install writes SKILL.md directory output');
assert(!(await fs.pathExists(path.join(tempProjectDir, '.kiro', 'steering'))), 'Kiro setup removes legacy steering dir');
await fs.remove(tempProjectDir);
await fs.remove(installedBmadDir);
} catch (error) {
assert(false, 'Kiro native skills migration test succeeds', error.message);
}
console.log('');
// ============================================================
// Test 6: Antigravity Native Skills Install
// ============================================================
console.log(`${colors.yellow}Test Suite 6: Antigravity Native Skills${colors.reset}\n`);
try {
clearCache();
const platformCodes = await loadPlatformCodes();
const antigravityInstaller = platformCodes.platforms.antigravity?.installer;
assert(antigravityInstaller?.target_dir === '.agent/skills', 'Antigravity target_dir uses native skills path');
assert(antigravityInstaller?.skill_format === true, 'Antigravity installer enables native skill output');
assert(
Array.isArray(antigravityInstaller?.legacy_targets) && antigravityInstaller.legacy_targets.includes('.agent/workflows'),
'Antigravity installer cleans legacy workflow output',
);
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-antigravity-test-'));
const installedBmadDir = await createTestBmadFixture();
const legacyDir = path.join(tempProjectDir, '.agent', 'workflows', 'bmad-legacy-dir');
await fs.ensureDir(legacyDir);
await fs.writeFile(path.join(tempProjectDir, '.agent', 'workflows', 'bmad-legacy.md'), 'legacy\n');
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
const ideManager = new IdeManager();
await ideManager.ensureInitialized();
const result = await ideManager.setup('antigravity', tempProjectDir, installedBmadDir, {
silent: true,
selectedModules: ['bmm'],
});
assert(result.success === true, 'Antigravity setup succeeds against temp project');
const skillFile = path.join(tempProjectDir, '.agent', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile), 'Antigravity install writes SKILL.md directory output');
assert(!(await fs.pathExists(path.join(tempProjectDir, '.agent', 'workflows'))), 'Antigravity setup removes legacy workflows dir');
await fs.remove(tempProjectDir);
await fs.remove(installedBmadDir);
} catch (error) {
assert(false, 'Antigravity native skills migration test succeeds', error.message);
}
console.log('');
// ============================================================
// Test 7: Auggie Native Skills Install
// ============================================================
console.log(`${colors.yellow}Test Suite 7: Auggie Native Skills${colors.reset}\n`);
try {
clearCache();
const platformCodes = await loadPlatformCodes();
const auggieInstaller = platformCodes.platforms.auggie?.installer;
assert(auggieInstaller?.target_dir === '.augment/skills', 'Auggie target_dir uses native skills path');
assert(auggieInstaller?.skill_format === true, 'Auggie installer enables native skill output');
assert(
Array.isArray(auggieInstaller?.legacy_targets) && auggieInstaller.legacy_targets.includes('.augment/commands'),
'Auggie installer cleans legacy command output',
);
assert(
auggieInstaller?.ancestor_conflict_check !== true,
'Auggie installer does not enable ancestor conflict checks without verified inheritance',
);
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-auggie-test-'));
const installedBmadDir = await createTestBmadFixture();
const legacyDir = path.join(tempProjectDir, '.augment', 'commands', 'bmad-legacy-dir');
await fs.ensureDir(legacyDir);
await fs.writeFile(path.join(tempProjectDir, '.augment', 'commands', 'bmad-legacy.md'), 'legacy\n');
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
const ideManager = new IdeManager();
await ideManager.ensureInitialized();
const result = await ideManager.setup('auggie', tempProjectDir, installedBmadDir, {
silent: true,
selectedModules: ['bmm'],
});
assert(result.success === true, 'Auggie setup succeeds against temp project');
const skillFile = path.join(tempProjectDir, '.augment', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile), 'Auggie install writes SKILL.md directory output');
assert(!(await fs.pathExists(path.join(tempProjectDir, '.augment', 'commands'))), 'Auggie setup removes legacy commands dir');
await fs.remove(tempProjectDir);
await fs.remove(installedBmadDir);
} catch (error) {
assert(false, 'Auggie native skills migration test succeeds', error.message);
}
console.log('');
// ============================================================
// Test 8: OpenCode Native Skills Install
// ============================================================
console.log(`${colors.yellow}Test Suite 8: OpenCode Native Skills${colors.reset}\n`);
try {
clearCache();
const platformCodes = await loadPlatformCodes();
const opencodeInstaller = platformCodes.platforms.opencode?.installer;
assert(opencodeInstaller?.target_dir === '.opencode/skills', 'OpenCode target_dir uses native skills path');
assert(opencodeInstaller?.skill_format === true, 'OpenCode installer enables native skill output');
assert(opencodeInstaller?.ancestor_conflict_check === true, 'OpenCode installer enables ancestor conflict checks');
assert(
Array.isArray(opencodeInstaller?.legacy_targets) &&
['.opencode/agents', '.opencode/commands', '.opencode/agent', '.opencode/command'].every((legacyTarget) =>
opencodeInstaller.legacy_targets.includes(legacyTarget),
),
'OpenCode installer cleans split legacy agent and command output',
);
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-opencode-test-'));
const installedBmadDir = await createTestBmadFixture();
const legacyDirs = [
path.join(tempProjectDir, '.opencode', 'agents', 'bmad-legacy-agent'),
path.join(tempProjectDir, '.opencode', 'commands', 'bmad-legacy-command'),
path.join(tempProjectDir, '.opencode', 'agent', 'bmad-legacy-agent-singular'),
path.join(tempProjectDir, '.opencode', 'command', 'bmad-legacy-command-singular'),
];
for (const legacyDir of legacyDirs) {
await fs.ensureDir(legacyDir);
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
await fs.writeFile(path.join(path.dirname(legacyDir), `${path.basename(legacyDir)}.md`), 'legacy\n');
}
const ideManager = new IdeManager();
await ideManager.ensureInitialized();
const result = await ideManager.setup('opencode', tempProjectDir, installedBmadDir, {
silent: true,
selectedModules: ['bmm'],
});
assert(result.success === true, 'OpenCode setup succeeds against temp project');
const skillFile = path.join(tempProjectDir, '.opencode', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile), 'OpenCode install writes SKILL.md directory output');
for (const legacyDir of ['agents', 'commands', 'agent', 'command']) {
assert(
!(await fs.pathExists(path.join(tempProjectDir, '.opencode', legacyDir))),
`OpenCode setup removes legacy .opencode/${legacyDir} dir`,
);
}
await fs.remove(tempProjectDir);
await fs.remove(installedBmadDir);
} catch (error) {
assert(false, 'OpenCode native skills migration test succeeds', error.message);
}
console.log('');
// ============================================================
// Test 9: OpenCode Ancestor Conflict
// ============================================================
console.log(`${colors.yellow}Test Suite 9: OpenCode Ancestor Conflict${colors.reset}\n`);
try {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-opencode-ancestor-test-'));
const parentProjectDir = path.join(tempRoot, 'parent');
const childProjectDir = path.join(parentProjectDir, 'child');
const installedBmadDir = await createTestBmadFixture();
await fs.ensureDir(path.join(parentProjectDir, '.git'));
await fs.ensureDir(path.join(parentProjectDir, '.opencode', 'skills', 'bmad-existing'));
await fs.ensureDir(childProjectDir);
await fs.writeFile(path.join(parentProjectDir, '.opencode', 'skills', 'bmad-existing', 'SKILL.md'), 'legacy\n');
const ideManager = new IdeManager();
await ideManager.ensureInitialized();
const result = await ideManager.setup('opencode', childProjectDir, installedBmadDir, {
silent: true,
selectedModules: ['bmm'],
});
const expectedConflictDir = await fs.realpath(path.join(parentProjectDir, '.opencode', 'skills'));
assert(result.success === false, 'OpenCode setup refuses install when ancestor skills already exist');
assert(result.handlerResult?.reason === 'ancestor-conflict', 'OpenCode ancestor rejection reports ancestor-conflict reason');
assert(
result.handlerResult?.conflictDir === expectedConflictDir,
'OpenCode ancestor rejection points at ancestor .opencode/skills dir',
);
await fs.remove(tempRoot);
await fs.remove(installedBmadDir);
} catch (error) {
assert(false, 'OpenCode ancestor conflict protection test succeeds', error.message);
}
console.log('');
// ============================================================
// Test 10: QA Agent Compilation
// ============================================================
console.log(`${colors.yellow}Test Suite 10: QA Agent Compilation${colors.reset}\n`);
try { try {
const builder = new YamlXmlBuilder(); const builder = new YamlXmlBuilder();

View File

@ -5,7 +5,6 @@ const crypto = require('node:crypto');
const csv = require('csv-parse/sync'); const csv = require('csv-parse/sync');
const { getSourcePath, getModulePath } = require('../../../lib/project-root'); const { getSourcePath, getModulePath } = require('../../../lib/project-root');
const prompts = require('../../../lib/prompts'); const prompts = require('../../../lib/prompts');
const { loadSkillManifest: loadSkillManifestShared, getCanonicalId: getCanonicalIdShared } = require('../ide/shared/skill-manifest');
// Load package.json for version info // Load package.json for version info
const packageJson = require('../../../../../package.json'); const packageJson = require('../../../../../package.json');
@ -24,16 +23,6 @@ class ManifestGenerator {
this.selectedIdes = []; this.selectedIdes = [];
} }
/** Delegate to shared skill-manifest module */
async loadSkillManifest(dirPath) {
return loadSkillManifestShared(dirPath);
}
/** Delegate to shared skill-manifest module */
getCanonicalId(manifest, filename) {
return getCanonicalIdShared(manifest, filename);
}
/** /**
* Clean text for CSV output by normalizing whitespace. * Clean text for CSV output by normalizing whitespace.
* Note: Quote escaping is handled by escapeCsv() at write time. * Note: Quote escaping is handled by escapeCsv() at write time.
@ -161,8 +150,6 @@ class ManifestGenerator {
// Recursively find workflow.yaml files // Recursively find workflow.yaml files
const findWorkflows = async (dir, relativePath = '') => { const findWorkflows = async (dir, relativePath = '') => {
const entries = await fs.readdir(dir, { withFileTypes: true }); const entries = await fs.readdir(dir, { withFileTypes: true });
// Load skill manifest for this directory (if present)
const skillManifest = await this.loadSkillManifest(dir);
for (const entry of entries) { for (const entry of entries) {
const fullPath = path.join(dir, entry.name); const fullPath = path.join(dir, entry.name);
@ -234,7 +221,6 @@ class ManifestGenerator {
description: this.cleanForCSV(workflow.description), description: this.cleanForCSV(workflow.description),
module: moduleName, module: moduleName,
path: installPath, path: installPath,
canonicalId: this.getCanonicalId(skillManifest, entry.name),
}); });
// Add to files list // Add to files list
@ -308,8 +294,6 @@ class ManifestGenerator {
async getAgentsFromDir(dirPath, moduleName, relativePath = '') { async getAgentsFromDir(dirPath, moduleName, relativePath = '') {
const agents = []; const agents = [];
const entries = await fs.readdir(dirPath, { withFileTypes: true }); const entries = await fs.readdir(dirPath, { withFileTypes: true });
// Load skill manifest for this directory (if present)
const skillManifest = await this.loadSkillManifest(dirPath);
for (const entry of entries) { for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name); const fullPath = path.join(dirPath, entry.name);
@ -365,7 +349,6 @@ class ManifestGenerator {
principles: principlesMatch ? this.cleanForCSV(principlesMatch[1]) : '', principles: principlesMatch ? this.cleanForCSV(principlesMatch[1]) : '',
module: moduleName, module: moduleName,
path: installPath, path: installPath,
canonicalId: this.getCanonicalId(skillManifest, entry.name),
}); });
// Add to files list // Add to files list
@ -405,8 +388,6 @@ class ManifestGenerator {
async getTasksFromDir(dirPath, moduleName) { async getTasksFromDir(dirPath, moduleName) {
const tasks = []; const tasks = [];
const files = await fs.readdir(dirPath); const files = await fs.readdir(dirPath);
// Load skill manifest for this directory (if present)
const skillManifest = await this.loadSkillManifest(dirPath);
for (const file of files) { for (const file of files) {
// Check for both .xml and .md files // Check for both .xml and .md files
@ -466,7 +447,6 @@ class ManifestGenerator {
module: moduleName, module: moduleName,
path: installPath, path: installPath,
standalone: standalone, standalone: standalone,
canonicalId: this.getCanonicalId(skillManifest, file),
}); });
// Add to files list // Add to files list
@ -506,8 +486,6 @@ class ManifestGenerator {
async getToolsFromDir(dirPath, moduleName) { async getToolsFromDir(dirPath, moduleName) {
const tools = []; const tools = [];
const files = await fs.readdir(dirPath); const files = await fs.readdir(dirPath);
// Load skill manifest for this directory (if present)
const skillManifest = await this.loadSkillManifest(dirPath);
for (const file of files) { for (const file of files) {
// Check for both .xml and .md files // Check for both .xml and .md files
@ -567,7 +545,6 @@ class ManifestGenerator {
module: moduleName, module: moduleName,
path: installPath, path: installPath,
standalone: standalone, standalone: standalone,
canonicalId: this.getCanonicalId(skillManifest, file),
}); });
// Add to files list // Add to files list
@ -758,8 +735,8 @@ class ManifestGenerator {
const csvPath = path.join(cfgDir, 'workflow-manifest.csv'); const csvPath = path.join(cfgDir, 'workflow-manifest.csv');
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`; const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
// Create CSV header - standalone column removed, canonicalId added as optional column // Create CSV header - standalone column removed, everything is canonicalized to 4 columns
let csv = 'name,description,module,path,canonicalId\n'; let csv = 'name,description,module,path\n';
// Build workflows map from discovered workflows only // Build workflows map from discovered workflows only
// Old entries are NOT preserved - the manifest reflects what actually exists on disk // Old entries are NOT preserved - the manifest reflects what actually exists on disk
@ -773,19 +750,12 @@ class ManifestGenerator {
description: workflow.description, description: workflow.description,
module: workflow.module, module: workflow.module,
path: workflow.path, path: workflow.path,
canonicalId: workflow.canonicalId || '',
}); });
} }
// Write all workflows // Write all workflows
for (const [, value] of allWorkflows) { for (const [, value] of allWorkflows) {
const row = [ const row = [escapeCsv(value.name), escapeCsv(value.description), escapeCsv(value.module), escapeCsv(value.path)].join(',');
escapeCsv(value.name),
escapeCsv(value.description),
escapeCsv(value.module),
escapeCsv(value.path),
escapeCsv(value.canonicalId),
].join(',');
csv += row + '\n'; csv += row + '\n';
} }
@ -814,8 +784,8 @@ class ManifestGenerator {
} }
} }
// Create CSV header with persona fields and canonicalId // Create CSV header with persona fields
let csvContent = 'name,displayName,title,icon,capabilities,role,identity,communicationStyle,principles,module,path,canonicalId\n'; let csvContent = 'name,displayName,title,icon,capabilities,role,identity,communicationStyle,principles,module,path\n';
// Combine existing and new agents, preferring new data for duplicates // Combine existing and new agents, preferring new data for duplicates
const allAgents = new Map(); const allAgents = new Map();
@ -840,7 +810,6 @@ class ManifestGenerator {
principles: agent.principles, principles: agent.principles,
module: agent.module, module: agent.module,
path: agent.path, path: agent.path,
canonicalId: agent.canonicalId || '',
}); });
} }
@ -858,7 +827,6 @@ class ManifestGenerator {
escapeCsv(record.principles), escapeCsv(record.principles),
escapeCsv(record.module), escapeCsv(record.module),
escapeCsv(record.path), escapeCsv(record.path),
escapeCsv(record.canonicalId),
].join(','); ].join(',');
csvContent += row + '\n'; csvContent += row + '\n';
} }
@ -888,8 +856,8 @@ class ManifestGenerator {
} }
} }
// Create CSV header with standalone and canonicalId columns // Create CSV header with standalone column
let csvContent = 'name,displayName,description,module,path,standalone,canonicalId\n'; let csvContent = 'name,displayName,description,module,path,standalone\n';
// Combine existing and new tasks // Combine existing and new tasks
const allTasks = new Map(); const allTasks = new Map();
@ -909,7 +877,6 @@ class ManifestGenerator {
module: task.module, module: task.module,
path: task.path, path: task.path,
standalone: task.standalone, standalone: task.standalone,
canonicalId: task.canonicalId || '',
}); });
} }
@ -922,7 +889,6 @@ class ManifestGenerator {
escapeCsv(record.module), escapeCsv(record.module),
escapeCsv(record.path), escapeCsv(record.path),
escapeCsv(record.standalone), escapeCsv(record.standalone),
escapeCsv(record.canonicalId),
].join(','); ].join(',');
csvContent += row + '\n'; csvContent += row + '\n';
} }
@ -952,8 +918,8 @@ class ManifestGenerator {
} }
} }
// Create CSV header with standalone and canonicalId columns // Create CSV header with standalone column
let csvContent = 'name,displayName,description,module,path,standalone,canonicalId\n'; let csvContent = 'name,displayName,description,module,path,standalone\n';
// Combine existing and new tools // Combine existing and new tools
const allTools = new Map(); const allTools = new Map();
@ -973,7 +939,6 @@ class ManifestGenerator {
module: tool.module, module: tool.module,
path: tool.path, path: tool.path,
standalone: tool.standalone, standalone: tool.standalone,
canonicalId: tool.canonicalId || '',
}); });
} }
@ -986,7 +951,6 @@ class ManifestGenerator {
escapeCsv(record.module), escapeCsv(record.module),
escapeCsv(record.path), escapeCsv(record.path),
escapeCsv(record.standalone), escapeCsv(record.standalone),
escapeCsv(record.canonicalId),
].join(','); ].join(',');
csvContent += row + '\n'; csvContent += row + '\n';
} }

View File

@ -1,7 +1,5 @@
const os = require('node:os');
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const yaml = require('yaml');
const { BaseIdeSetup } = require('./_base-ide'); const { BaseIdeSetup } = require('./_base-ide');
const prompts = require('../../../lib/prompts'); const prompts = require('../../../lib/prompts');
const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { AgentCommandGenerator } = require('./shared/agent-command-generator');
@ -26,34 +24,6 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
super(platformCode, platformConfig.name, platformConfig.preferred); super(platformCode, platformConfig.name, platformConfig.preferred);
this.platformConfig = platformConfig; this.platformConfig = platformConfig;
this.installerConfig = platformConfig.installer || null; this.installerConfig = platformConfig.installer || null;
// Set configDir from target_dir so base-class detect() works
if (this.installerConfig?.target_dir) {
this.configDir = this.installerConfig.target_dir;
}
}
/**
* Detect whether this IDE already has configuration in the project.
* For skill_format platforms, checks for bmad-prefixed entries in target_dir
* (matching old codex.js behavior) instead of just checking directory existence.
* @param {string} projectDir - Project directory
* @returns {Promise<boolean>}
*/
async detect(projectDir) {
if (this.installerConfig?.skill_format && this.configDir) {
const dir = path.join(projectDir || process.cwd(), this.configDir);
if (await fs.pathExists(dir)) {
try {
const entries = await fs.readdir(dir);
return entries.some((e) => typeof e === 'string' && e.startsWith('bmad'));
} catch {
return false;
}
}
return false;
}
return super.detect(projectDir);
} }
/** /**
@ -69,8 +39,8 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
const conflict = await this.findAncestorConflict(projectDir); const conflict = await this.findAncestorConflict(projectDir);
if (conflict) { if (conflict) {
await prompts.log.error( await prompts.log.error(
`Found existing BMAD skills in ancestor installation: ${conflict}\n` + `Found existing BMAD commands in ancestor installation: ${conflict}\n` +
` ${this.name} inherits skills from parent directories, so this would cause duplicates.\n` + ` ${this.name} inherits commands from parent directories, so this would cause duplicates.\n` +
` Please remove the BMAD files from that directory first:\n` + ` Please remove the BMAD files from that directory first:\n` +
` rm -rf "${conflict}"/bmad*`, ` rm -rf "${conflict}"/bmad*`,
); );
@ -195,13 +165,8 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
for (const artifact of artifacts) { for (const artifact of artifacts) {
const content = this.renderTemplate(template, artifact); const content = this.renderTemplate(template, artifact);
const filename = this.generateFilename(artifact, 'agent', extension); const filename = this.generateFilename(artifact, 'agent', extension);
if (config.skill_format) {
await this.writeSkillFile(targetPath, artifact, content);
} else {
const filePath = path.join(targetPath, filename); const filePath = path.join(targetPath, filename);
await this.writeFile(filePath, content); await this.writeFile(filePath, content);
}
count++; count++;
} }
@ -233,13 +198,8 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
const { content: template, extension } = await this.loadTemplate(workflowTemplateType, '', config, finalTemplateType); const { content: template, extension } = await this.loadTemplate(workflowTemplateType, '', config, finalTemplateType);
const content = this.renderTemplate(template, artifact); const content = this.renderTemplate(template, artifact);
const filename = this.generateFilename(artifact, 'workflow', extension); const filename = this.generateFilename(artifact, 'workflow', extension);
if (config.skill_format) {
await this.writeSkillFile(targetPath, artifact, content);
} else {
const filePath = path.join(targetPath, filename); const filePath = path.join(targetPath, filename);
await this.writeFile(filePath, content); await this.writeFile(filePath, content);
}
count++; count++;
} }
} }
@ -281,13 +241,8 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
const content = this.renderTemplate(template, artifact); const content = this.renderTemplate(template, artifact);
const filename = this.generateFilename(artifact, artifact.type, extension); const filename = this.generateFilename(artifact, artifact.type, extension);
if (config.skill_format) {
await this.writeSkillFile(targetPath, artifact, content);
} else {
const filePath = path.join(targetPath, filename); const filePath = path.join(targetPath, filename);
await this.writeFile(filePath, content); await this.writeFile(filePath, content);
}
if (artifact.type === 'task') { if (artifact.type === 'task') {
taskCount++; taskCount++;
@ -454,146 +409,22 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
// No default // No default
} }
// Replace _bmad placeholder with actual folder name BEFORE inserting paths, let rendered = template
// so that paths containing '_bmad' are not corrupted by the blanket replacement.
let rendered = template.replaceAll('_bmad', this.bmadFolderName);
// Replace {{bmadFolderName}} placeholder if present
rendered = rendered.replaceAll('{{bmadFolderName}}', this.bmadFolderName);
rendered = rendered
.replaceAll('{{name}}', artifact.name || '') .replaceAll('{{name}}', artifact.name || '')
.replaceAll('{{module}}', artifact.module || 'core') .replaceAll('{{module}}', artifact.module || 'core')
.replaceAll('{{path}}', pathToUse) .replaceAll('{{path}}', pathToUse)
.replaceAll('{{description}}', artifact.description || `${artifact.name} ${artifact.type || ''}`) .replaceAll('{{description}}', artifact.description || `${artifact.name} ${artifact.type || ''}`)
.replaceAll('{{workflow_path}}', pathToUse); .replaceAll('{{workflow_path}}', pathToUse);
// Replace _bmad placeholder with actual folder name
rendered = rendered.replaceAll('_bmad', this.bmadFolderName);
// Replace {{bmadFolderName}} placeholder if present
rendered = rendered.replaceAll('{{bmadFolderName}}', this.bmadFolderName);
return rendered; return rendered;
} }
/**
* Write artifact as a skill directory with SKILL.md inside.
* Writes artifact as a skill directory with SKILL.md inside.
* @param {string} targetPath - Base skills directory
* @param {Object} artifact - Artifact data
* @param {string} content - Rendered template content
*/
async writeSkillFile(targetPath, artifact, content) {
const { resolveSkillName } = require('./shared/path-utils');
// Get the skill name (prefers canonicalId, falls back to path-derived) and remove .md
const flatName = resolveSkillName(artifact);
const skillName = path.basename(flatName.replace(/\.md$/, ''));
if (!skillName) {
throw new Error(`Cannot derive skill name for artifact: ${artifact.relativePath || JSON.stringify(artifact)}`);
}
// Create skill directory
const skillDir = path.join(targetPath, skillName);
await this.ensureDir(skillDir);
// Transform content: rewrite frontmatter for skills format
const skillContent = this.transformToSkillFormat(content, skillName);
await this.writeFile(path.join(skillDir, 'SKILL.md'), skillContent);
}
/**
* Transform artifact content to Agent Skills format.
* Rewrites frontmatter to contain only unquoted name and description.
* @param {string} content - Original content with YAML frontmatter
* @param {string} skillName - Skill name (must match directory name)
* @returns {string} Transformed content
*/
transformToSkillFormat(content, skillName) {
// Normalize line endings
content = content.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
// Parse frontmatter
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
if (!fmMatch) {
// No frontmatter -- wrap with minimal frontmatter
const fm = yaml.stringify({ name: skillName, description: skillName }).trimEnd();
return `---\n${fm}\n---\n\n${content}`;
}
const frontmatter = fmMatch[1];
const body = fmMatch[2];
// Parse frontmatter with yaml library to extract description
let description;
try {
const parsed = yaml.parse(frontmatter);
const rawDesc = parsed?.description;
description = typeof rawDesc === 'string' && rawDesc ? rawDesc : `${skillName} skill`;
} catch {
description = `${skillName} skill`;
}
// Build new frontmatter with only name and description, unquoted
const newFrontmatter = yaml.stringify({ name: skillName, description: String(description) }, { lineWidth: 0 }).trimEnd();
return `---\n${newFrontmatter}\n---\n${body}`;
}
/**
* Install a custom agent launcher.
* For skill_format platforms, produces <skillDir>/SKILL.md.
* For flat platforms, produces a single file in target_dir.
* @param {string} projectDir - Project directory
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
* @param {string} agentPath - Path to compiled agent (relative to project root)
* @param {Object} metadata - Agent metadata
* @returns {Object|null} Info about created file/skill
*/
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
if (!this.installerConfig?.target_dir) return null;
const { customAgentDashName } = require('./shared/path-utils');
const targetPath = path.join(projectDir, this.installerConfig.target_dir);
await this.ensureDir(targetPath);
// Build artifact to reuse existing template rendering.
// The default-agent template already includes the _bmad/ prefix before {{path}},
// but agentPath is relative to project root (e.g. "_bmad/custom/agents/fred.md").
// Strip the bmadFolderName prefix so the template doesn't produce a double path.
const bmadPrefix = this.bmadFolderName + '/';
const normalizedPath = agentPath.startsWith(bmadPrefix) ? agentPath.slice(bmadPrefix.length) : agentPath;
const artifact = {
type: 'agent-launcher',
name: agentName,
description: metadata?.description || `${agentName} agent`,
agentPath: normalizedPath,
relativePath: normalizedPath,
module: 'custom',
};
const { content: template } = await this.loadTemplate(
this.installerConfig.template_type || 'default',
'agent',
this.installerConfig,
'default-agent',
);
const content = this.renderTemplate(template, artifact);
if (this.installerConfig.skill_format) {
const skillName = customAgentDashName(agentName).replace(/\.md$/, '');
const skillDir = path.join(targetPath, skillName);
await this.ensureDir(skillDir);
const skillContent = this.transformToSkillFormat(content, skillName);
const skillPath = path.join(skillDir, 'SKILL.md');
await this.writeFile(skillPath, skillContent);
return { path: path.relative(projectDir, skillPath), command: `$${skillName}` };
}
// Flat file output
const filename = customAgentDashName(agentName);
const filePath = path.join(targetPath, filename);
await this.writeFile(filePath, content);
return { path: path.relative(projectDir, filePath), command: agentName };
}
/** /**
* Generate filename for artifact * Generate filename for artifact
* @param {Object} artifact - Artifact data * @param {Object} artifact - Artifact data
@ -602,11 +433,10 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
* @returns {string} Generated filename * @returns {string} Generated filename
*/ */
generateFilename(artifact, artifactType, extension = '.md') { generateFilename(artifact, artifactType, extension = '.md') {
const { resolveSkillName } = require('./shared/path-utils'); const { toDashPath } = require('./shared/path-utils');
// Reuse central logic to ensure consistent naming conventions // Reuse central logic to ensure consistent naming conventions
// Prefers canonicalId from manifest when available, falls back to path-derived name const standardName = toDashPath(artifact.relativePath);
const standardName = resolveSkillName(artifact);
// Clean up potential double extensions from source files (e.g. .yaml.md, .xml.md -> .md) // Clean up potential double extensions from source files (e.g. .yaml.md, .xml.md -> .md)
// This handles any extensions that might slip through toDashPath() // This handles any extensions that might slip through toDashPath()
@ -646,14 +476,10 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
if (this.installerConfig?.legacy_targets) { if (this.installerConfig?.legacy_targets) {
if (!options.silent) await prompts.log.message(' Migrating legacy directories...'); if (!options.silent) await prompts.log.message(' Migrating legacy directories...');
for (const legacyDir of this.installerConfig.legacy_targets) { for (const legacyDir of this.installerConfig.legacy_targets) {
if (this.isGlobalPath(legacyDir)) {
await this.warnGlobalLegacy(legacyDir, options);
} else {
await this.cleanupTarget(projectDir, legacyDir, options); await this.cleanupTarget(projectDir, legacyDir, options);
await this.removeEmptyParents(projectDir, legacyDir); await this.removeEmptyParents(projectDir, legacyDir);
} }
} }
}
// Clean all target directories // Clean all target directories
if (this.installerConfig?.targets) { if (this.installerConfig?.targets) {
@ -675,41 +501,6 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
} }
} }
/**
* Check if a path is global (starts with ~ or is absolute)
* @param {string} p - Path to check
* @returns {boolean}
*/
isGlobalPath(p) {
return p.startsWith('~') || path.isAbsolute(p);
}
/**
* Warn about stale BMAD files in a global legacy directory (never auto-deletes)
* @param {string} legacyDir - Legacy directory path (may start with ~)
* @param {Object} options - Options (silent, etc.)
*/
async warnGlobalLegacy(legacyDir, options = {}) {
try {
const expanded = legacyDir.startsWith('~/')
? path.join(os.homedir(), legacyDir.slice(2))
: legacyDir === '~'
? os.homedir()
: legacyDir;
if (!(await fs.pathExists(expanded))) return;
const entries = await fs.readdir(expanded);
const bmadFiles = entries.filter((e) => typeof e === 'string' && e.startsWith('bmad'));
if (bmadFiles.length > 0 && !options.silent) {
await prompts.log.warn(`Found ${bmadFiles.length} stale BMAD file(s) in ${expanded}. Remove manually: rm ${expanded}/bmad-*`);
}
} catch {
// Errors reading global paths are silently ignored
}
}
/** /**
* Cleanup a specific target directory * Cleanup a specific target directory
* @param {string} projectDir - Project directory * @param {string} projectDir - Project directory

View File

@ -0,0 +1,440 @@
const path = require('node:path');
const os = require('node:os');
const fs = require('fs-extra');
const yaml = require('yaml');
const { BaseIdeSetup } = require('./_base-ide');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
const { getTasksFromBmad } = require('./shared/bmad-artifacts');
const { toDashPath, customAgentDashName } = require('./shared/path-utils');
const prompts = require('../../../lib/prompts');
/**
* Codex setup handler (CLI mode)
*/
class CodexSetup extends BaseIdeSetup {
constructor() {
super('codex', 'Codex', false);
}
/**
* Setup Codex configuration
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
// Always use CLI mode
const mode = 'cli';
const { artifacts, counts } = await this.collectClaudeArtifacts(projectDir, bmadDir, options);
// Clean up old .codex/prompts locations (both global and project)
const oldGlobalDir = this.getOldCodexPromptDir(null, 'global');
await this.clearOldBmadFiles(oldGlobalDir, options);
const oldProjectDir = this.getOldCodexPromptDir(projectDir, 'project');
await this.clearOldBmadFiles(oldProjectDir, options);
// Install to .agents/skills
const destDir = this.getCodexSkillsDir(projectDir);
await fs.ensureDir(destDir);
await this.clearOldBmadSkills(destDir, options);
// Collect and write agent skills
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
const agentCount = await this.writeSkillArtifacts(destDir, agentArtifacts, 'agent-launcher');
// Collect and write task skills
const tasks = await getTasksFromBmad(bmadDir, options.selectedModules || []);
const taskArtifacts = [];
for (const task of tasks) {
const content = await this.readAndProcessWithProject(
task.path,
{
module: task.module,
name: task.name,
},
projectDir,
);
taskArtifacts.push({
type: 'task',
name: task.name,
displayName: task.name,
module: task.module,
path: task.path,
sourcePath: task.path,
relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
content,
});
}
const ttGen = new TaskToolCommandGenerator(this.bmadFolderName);
const taskSkillArtifacts = taskArtifacts.map((artifact) => ({
...artifact,
content: ttGen.generateCommandContent(artifact, artifact.type),
}));
const tasksWritten = await this.writeSkillArtifacts(destDir, taskSkillArtifacts, 'task');
// Collect and write workflow skills
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
const { artifacts: workflowArtifacts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
const workflowCount = await this.writeSkillArtifacts(destDir, workflowArtifacts, 'workflow-command');
const written = agentCount + workflowCount + tasksWritten;
if (!options.silent) {
await prompts.log.success(
`${this.name} configured: ${counts.agents} agents, ${counts.workflows} workflows, ${counts.tasks} tasks, ${written} skills → ${destDir}`,
);
}
return {
success: true,
mode,
artifacts,
counts,
destination: destDir,
written,
};
}
/**
* Detect Codex installation by checking for BMAD skills
*/
async detect(projectDir) {
const dir = this.getCodexSkillsDir(projectDir || process.cwd());
if (await fs.pathExists(dir)) {
try {
const entries = await fs.readdir(dir);
if (entries && entries.some((entry) => entry && typeof entry === 'string' && entry.startsWith('bmad'))) {
return true;
}
} catch {
// Ignore errors
}
}
return false;
}
/**
* Collect Claude-style artifacts for Codex export.
* Returns the normalized artifact list for further processing.
*/
async collectClaudeArtifacts(projectDir, bmadDir, options = {}) {
const selectedModules = options.selectedModules || [];
const artifacts = [];
// Generate agent launchers
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules);
for (const artifact of agentArtifacts) {
artifacts.push({
type: 'agent',
module: artifact.module,
sourcePath: artifact.sourcePath,
relativePath: artifact.relativePath,
content: artifact.content,
});
}
const tasks = await getTasksFromBmad(bmadDir, selectedModules);
for (const task of tasks) {
const content = await this.readAndProcessWithProject(
task.path,
{
module: task.module,
name: task.name,
},
projectDir,
);
artifacts.push({
type: 'task',
name: task.name,
displayName: task.name,
module: task.module,
path: task.path,
sourcePath: task.path,
relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
content,
});
}
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
artifacts.push(...workflowArtifacts);
return {
artifacts,
counts: {
agents: agentArtifacts.length,
tasks: tasks.length,
workflows: workflowCounts.commands,
workflowLaunchers: workflowCounts.launchers,
},
};
}
getCodexSkillsDir(projectDir) {
if (!projectDir) {
throw new Error('projectDir is required for project-scoped skill installation');
}
return path.join(projectDir, '.agents', 'skills');
}
/**
* Get the old .codex/prompts directory for cleanup purposes
*/
getOldCodexPromptDir(projectDir = null, location = 'global') {
if (location === 'project' && projectDir) {
return path.join(projectDir, '.codex', 'prompts');
}
return path.join(os.homedir(), '.codex', 'prompts');
}
/**
* Write artifacts as Agent Skills (agentskills.io format).
* Each artifact becomes a directory containing SKILL.md.
* @param {string} destDir - Base skills directory
* @param {Array} artifacts - Artifacts to write
* @param {string} artifactType - Type filter (e.g., 'agent-launcher', 'workflow-command', 'task')
* @returns {number} Number of skills written
*/
async writeSkillArtifacts(destDir, artifacts, artifactType) {
let writtenCount = 0;
for (const artifact of artifacts) {
// Filter by type if the artifact has a type field
if (artifact.type && artifact.type !== artifactType) {
continue;
}
// Get the dash-format name (e.g., bmad-bmm-create-prd.md) and remove .md
const flatName = toDashPath(artifact.relativePath);
const skillName = flatName.replace(/\.md$/, '');
// Create skill directory
const skillDir = path.join(destDir, skillName);
await fs.ensureDir(skillDir);
// Transform content: rewrite frontmatter for skills format
const skillContent = this.transformToSkillFormat(artifact.content, skillName);
// Write SKILL.md with platform-native line endings
const platformContent = skillContent.replaceAll('\n', os.EOL);
await fs.writeFile(path.join(skillDir, 'SKILL.md'), platformContent, 'utf8');
writtenCount++;
}
return writtenCount;
}
/**
* Transform artifact content from Codex prompt format to Agent Skills format.
* Removes disable-model-invocation, ensures name matches directory.
* @param {string} content - Original content with YAML frontmatter
* @param {string} skillName - Skill name (must match directory name)
* @returns {string} Transformed content
*/
transformToSkillFormat(content, skillName) {
// Normalize line endings so body matches rebuilt frontmatter (both LF)
content = content.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
// Parse frontmatter
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
if (!fmMatch) {
// No frontmatter -- wrap with minimal frontmatter
const fm = yaml.stringify({ name: skillName, description: skillName }).trimEnd();
return `---\n${fm}\n---\n\n${content}`;
}
const frontmatter = fmMatch[1];
const body = fmMatch[2];
// Parse frontmatter with yaml library to handle all quoting variants
let description;
try {
const parsed = yaml.parse(frontmatter);
description = parsed?.description || `${skillName} skill`;
} catch {
description = `${skillName} skill`;
}
// Build new frontmatter with only skills-spec fields, let yaml handle quoting
const newFrontmatter = yaml.stringify({ name: skillName, description }, { lineWidth: 0 }).trimEnd();
return `---\n${newFrontmatter}\n---\n${body}`;
}
/**
* Remove existing BMAD skill directories from the skills directory.
*/
async clearOldBmadSkills(destDir, options = {}) {
if (!(await fs.pathExists(destDir))) {
return;
}
let entries;
try {
entries = await fs.readdir(destDir);
} catch (error) {
if (!options.silent) await prompts.log.warn(`Warning: Could not read directory ${destDir}: ${error.message}`);
return;
}
if (!entries || !Array.isArray(entries)) {
return;
}
for (const entry of entries) {
if (!entry || typeof entry !== 'string') {
continue;
}
if (!entry.startsWith('bmad')) {
continue;
}
const entryPath = path.join(destDir, entry);
try {
await fs.remove(entryPath);
} catch (error) {
if (!options.silent) {
await prompts.log.message(` Skipping ${entry}: ${error.message}`);
}
}
}
}
/**
* Clean old BMAD files from legacy .codex/prompts directories.
*/
async clearOldBmadFiles(destDir, options = {}) {
if (!(await fs.pathExists(destDir))) {
return;
}
let entries;
try {
entries = await fs.readdir(destDir);
} catch (error) {
// Directory exists but can't be read - skip cleanup
if (!options.silent) await prompts.log.warn(`Warning: Could not read directory ${destDir}: ${error.message}`);
return;
}
if (!entries || !Array.isArray(entries)) {
return;
}
for (const entry of entries) {
// Skip non-strings or undefined entries
if (!entry || typeof entry !== 'string') {
continue;
}
if (!entry.startsWith('bmad')) {
continue;
}
const entryPath = path.join(destDir, entry);
try {
await fs.remove(entryPath);
} catch (error) {
if (!options.silent) {
await prompts.log.message(` Skipping ${entry}: ${error.message}`);
}
}
}
}
async readAndProcessWithProject(filePath, metadata, projectDir) {
const rawContent = await fs.readFile(filePath, 'utf8');
const content = rawContent.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
return super.processContent(content, metadata, projectDir);
}
/**
* Get instructions for project-specific installation
* @param {string} projectDir - Optional project directory
* @param {string} destDir - Optional destination directory
* @returns {string} Instructions text
*/
getProjectSpecificInstructions(projectDir = null, destDir = null) {
const lines = [
'Project-Specific Codex Configuration',
'',
`Skills installed to: ${destDir || '<project>/.agents/skills'}`,
'',
'Codex automatically discovers skills in .agents/skills/ at and above the current directory and in your home directory.',
'No additional configuration is needed.',
];
return lines.join('\n');
}
/**
* Cleanup Codex configuration - cleans both new .agents/skills and old .codex/prompts
*/
async cleanup(projectDir = null) {
// Clean old .codex/prompts locations
const oldGlobalDir = this.getOldCodexPromptDir(null, 'global');
await this.clearOldBmadFiles(oldGlobalDir);
if (projectDir) {
const oldProjectDir = this.getOldCodexPromptDir(projectDir, 'project');
await this.clearOldBmadFiles(oldProjectDir);
// Clean new .agents/skills location
const destDir = this.getCodexSkillsDir(projectDir);
await this.clearOldBmadSkills(destDir);
}
}
/**
* Install a custom agent launcher for Codex as an Agent Skill
* @param {string} projectDir - Project directory
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
* @param {string} agentPath - Path to compiled agent (relative to project root)
* @param {Object} metadata - Agent metadata
* @returns {Object|null} Info about created skill
*/
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
const destDir = this.getCodexSkillsDir(projectDir);
// Skill name from the dash name (without .md)
const skillName = customAgentDashName(agentName).replace(/\.md$/, '');
const skillDir = path.join(destDir, skillName);
await fs.ensureDir(skillDir);
const description = metadata?.description || `${agentName} agent`;
const fm = yaml.stringify({ name: skillName, description }).trimEnd();
const skillContent =
`---\n${fm}\n---\n` +
"\nYou must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.\n" +
'\n<agent-activation CRITICAL="TRUE">\n' +
`1. LOAD the FULL agent file from @${agentPath}\n` +
'2. READ its entire contents - this contains the complete agent persona, menu, and instructions\n' +
'3. FOLLOW every step in the <activation> section precisely\n' +
'4. DISPLAY the welcome/greeting as instructed\n' +
'5. PRESENT the numbered menu\n' +
'6. WAIT for user input before proceeding\n' +
'</agent-activation>\n';
// Write with platform-native line endings
const platformContent = skillContent.replaceAll('\n', os.EOL);
const skillPath = path.join(skillDir, 'SKILL.md');
await fs.writeFile(skillPath, platformContent, 'utf8');
return {
path: path.relative(projectDir, skillPath),
command: `$${skillName}`,
};
}
}
module.exports = { CodexSetup };

View File

@ -8,7 +8,7 @@ const prompts = require('../../../lib/prompts');
* Dynamically discovers and loads IDE handlers * Dynamically discovers and loads IDE handlers
* *
* Loading strategy: * Loading strategy:
* 1. Custom installer files (github-copilot.js, kilo.js, rovodev.js) - for platforms with unique installation logic * 1. Custom installer files (codex.js, github-copilot.js, kilo.js, rovodev.js) - for platforms with unique installation logic
* 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns * 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns
*/ */
class IdeManager { class IdeManager {
@ -44,7 +44,7 @@ class IdeManager {
/** /**
* Dynamically load all IDE handlers * Dynamically load all IDE handlers
* 1. Load custom installer files first (github-copilot.js, kilo.js, rovodev.js) * 1. Load custom installer files first (codex.js, github-copilot.js, kilo.js, rovodev.js)
* 2. Load config-driven handlers from platform-codes.yaml * 2. Load config-driven handlers from platform-codes.yaml
*/ */
async loadHandlers() { async loadHandlers() {
@ -58,11 +58,10 @@ class IdeManager {
/** /**
* Load custom installer files (unique installation logic) * Load custom installer files (unique installation logic)
* These files have special installation patterns that don't fit the config-driven model * These files have special installation patterns that don't fit the config-driven model
* Note: codex was migrated to config-driven (platform-codes.yaml) and no longer needs a custom installer
*/ */
async loadCustomInstallerFiles() { async loadCustomInstallerFiles() {
const ideDir = __dirname; const ideDir = __dirname;
const customFiles = ['github-copilot.js', 'kilo.js', 'rovodev.js']; const customFiles = ['codex.js', 'github-copilot.js', 'kilo.js', 'rovodev.js'];
for (const file of customFiles) { for (const file of customFiles) {
const filePath = path.join(ideDir, file); const filePath = path.join(ideDir, file);
@ -190,6 +189,14 @@ class IdeManager {
if (r.tasks > 0) parts.push(`${r.tasks} tasks`); if (r.tasks > 0) parts.push(`${r.tasks} tasks`);
if (r.tools > 0) parts.push(`${r.tools} tools`); if (r.tools > 0) parts.push(`${r.tools} tools`);
detail = parts.join(', '); detail = parts.join(', ');
} else if (handlerResult && handlerResult.counts) {
// Codex handler returns { success, counts: { agents, workflows, tasks }, written }
const c = handlerResult.counts;
const parts = [];
if (c.agents > 0) parts.push(`${c.agents} agents`);
if (c.workflows > 0) parts.push(`${c.workflows} workflows`);
if (c.tasks > 0) parts.push(`${c.tasks} tasks`);
detail = parts.join(', ');
} else if (handlerResult && handlerResult.modes !== undefined) { } else if (handlerResult && handlerResult.modes !== undefined) {
// Kilo handler returns { success, modes, workflows, tasks, tools } // Kilo handler returns { success, modes, workflows, tasks, tools }
const parts = []; const parts = [];

View File

@ -20,11 +20,8 @@ platforms:
category: ide category: ide
description: "Google's AI development environment" description: "Google's AI development environment"
installer: installer:
legacy_targets: target_dir: .agent/workflows
- .agent/workflows
target_dir: .agent/skills
template_type: antigravity template_type: antigravity
skill_format: true
auggie: auggie:
name: "Auggie" name: "Auggie"
@ -32,11 +29,8 @@ platforms:
category: cli category: cli
description: "AI development tool" description: "AI development tool"
installer: installer:
legacy_targets: target_dir: .augment/commands
- .augment/commands
target_dir: .augment/skills
template_type: default template_type: default
skill_format: true
claude-code: claude-code:
name: "Claude Code" name: "Claude Code"
@ -44,11 +38,8 @@ platforms:
category: cli category: cli
description: "Anthropic's official CLI for Claude" description: "Anthropic's official CLI for Claude"
installer: installer:
legacy_targets: target_dir: .claude/commands
- .claude/commands
target_dir: .claude/skills
template_type: default template_type: default
skill_format: true
ancestor_conflict_check: true ancestor_conflict_check: true
cline: cline:
@ -65,15 +56,7 @@ platforms:
preferred: false preferred: false
category: cli category: cli
description: "OpenAI Codex integration" description: "OpenAI Codex integration"
installer: # No installer config - uses custom codex.js
legacy_targets:
- .codex/prompts
- ~/.codex/prompts
target_dir: .agents/skills
template_type: default
skill_format: true
ancestor_conflict_check: true
artifact_types: [agents, workflows, tasks]
codebuddy: codebuddy:
name: "CodeBuddy" name: "CodeBuddy"
@ -99,11 +82,8 @@ platforms:
category: ide category: ide
description: "AI-first code editor" description: "AI-first code editor"
installer: installer:
legacy_targets: target_dir: .cursor/commands
- .cursor/commands
target_dir: .cursor/skills
template_type: default template_type: default
skill_format: true
gemini: gemini:
name: "Gemini CLI" name: "Gemini CLI"
@ -143,11 +123,8 @@ platforms:
category: ide category: ide
description: "Amazon's AI-powered IDE" description: "Amazon's AI-powered IDE"
installer: installer:
legacy_targets: target_dir: .kiro/steering
- .kiro/steering
target_dir: .kiro/skills
template_type: kiro template_type: kiro
skill_format: true
opencode: opencode:
name: "OpenCode" name: "OpenCode"
@ -156,14 +133,15 @@ platforms:
description: "OpenCode terminal coding assistant" description: "OpenCode terminal coding assistant"
installer: installer:
legacy_targets: legacy_targets:
- .opencode/agents
- .opencode/commands
- .opencode/agent - .opencode/agent
- .opencode/command - .opencode/command
target_dir: .opencode/skills targets:
- target_dir: .opencode/agents
template_type: opencode template_type: opencode
skill_format: true artifact_types: [agents]
ancestor_conflict_check: true - target_dir: .opencode/commands
template_type: opencode
artifact_types: [workflows, tasks, tools]
qwen: qwen:
name: "QwenCoder" name: "QwenCoder"
@ -205,11 +183,8 @@ platforms:
category: ide category: ide
description: "AI-powered IDE with cascade flows" description: "AI-powered IDE with cascade flows"
installer: installer:
legacy_targets: target_dir: .windsurf/workflows
- .windsurf/workflows
target_dir: .windsurf/skills
template_type: windsurf template_type: windsurf
skill_format: true
# ============================================================================ # ============================================================================
# Installer Config Schema # Installer Config Schema
@ -228,11 +203,9 @@ platforms:
# artifact_types: [agents, workflows, tasks, tools] # artifact_types: [agents, workflows, tasks, tools]
# artifact_types: array (optional) # Filter which artifacts to install (default: all) # artifact_types: array (optional) # Filter which artifacts to install (default: all)
# skip_existing: boolean (optional) # Skip files that already exist (default: false) # skip_existing: boolean (optional) # Skip files that already exist (default: false)
# skill_format: boolean (optional) # Use directory-per-skill output: <name>/SKILL.md
# # with clean frontmatter (name + description, unquoted)
# ancestor_conflict_check: boolean (optional) # Refuse install when ancestor dir has BMAD files # ancestor_conflict_check: boolean (optional) # Refuse install when ancestor dir has BMAD files
# # in the same target_dir (for IDEs that inherit # # in the same target_dir (for IDEs that inherit
# # skills from parent directories) # # commands from parent directories)
# ============================================================================ # ============================================================================
# Platform Categories # Platform Categories

View File

@ -47,7 +47,6 @@ class AgentCommandGenerator {
name: agent.name, name: agent.name,
description: agent.description || `${agent.name} agent`, description: agent.description || `${agent.name} agent`,
module: agent.module, module: agent.module,
canonicalId: agent.canonicalId || '',
relativePath: path.join(agent.module, 'agents', agentPathInModule), // For command filename relativePath: path.join(agent.module, 'agents', agentPathInModule), // For command filename
agentPath: agentRelPath, // Relative path to actual agent file agentPath: agentRelPath, // Relative path to actual agent file
content: launcherContent, content: launcherContent,

View File

@ -1,6 +1,5 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const { loadSkillManifest, getCanonicalId } = require('./skill-manifest');
/** /**
* Helpers for gathering BMAD agents/tasks from the installed tree. * Helpers for gathering BMAD agents/tasks from the installed tree.
@ -35,7 +34,6 @@ async function getAgentsFromBmad(bmadDir, selectedModules = []) {
const agentDirPath = path.join(standaloneAgentsDir, agentDir.name); const agentDirPath = path.join(standaloneAgentsDir, agentDir.name);
const agentFiles = await fs.readdir(agentDirPath); const agentFiles = await fs.readdir(agentDirPath);
const skillManifest = await loadSkillManifest(agentDirPath);
for (const file of agentFiles) { for (const file of agentFiles) {
if (!file.endsWith('.md')) continue; if (!file.endsWith('.md')) continue;
@ -50,7 +48,6 @@ async function getAgentsFromBmad(bmadDir, selectedModules = []) {
path: filePath, path: filePath,
name: file.replace('.md', ''), name: file.replace('.md', ''),
module: 'standalone', // Mark as standalone agent module: 'standalone', // Mark as standalone agent
canonicalId: getCanonicalId(skillManifest, file),
}); });
} }
} }
@ -87,7 +84,6 @@ async function getAgentsFromDir(dirPath, moduleName, relativePath = '') {
} }
const entries = await fs.readdir(dirPath, { withFileTypes: true }); const entries = await fs.readdir(dirPath, { withFileTypes: true });
const skillManifest = await loadSkillManifest(dirPath);
for (const entry of entries) { for (const entry of entries) {
// Skip if entry.name is undefined or not a string // Skip if entry.name is undefined or not a string
@ -128,7 +124,6 @@ async function getAgentsFromDir(dirPath, moduleName, relativePath = '') {
name: entry.name.replace('.md', ''), name: entry.name.replace('.md', ''),
module: moduleName, module: moduleName,
relativePath: newRelativePath, // Keep the .md extension for the full path relativePath: newRelativePath, // Keep the .md extension for the full path
canonicalId: getCanonicalId(skillManifest, entry.name),
}); });
} }
} }
@ -144,7 +139,6 @@ async function getTasksFromDir(dirPath, moduleName) {
} }
const files = await fs.readdir(dirPath); const files = await fs.readdir(dirPath);
const skillManifest = await loadSkillManifest(dirPath);
for (const file of files) { for (const file of files) {
// Include both .md and .xml task files // Include both .md and .xml task files
@ -166,7 +160,6 @@ async function getTasksFromDir(dirPath, moduleName) {
path: filePath, path: filePath,
name: file.replace(ext, ''), name: file.replace(ext, ''),
module: moduleName, module: moduleName,
canonicalId: getCanonicalId(skillManifest, file),
}); });
} }

View File

@ -264,21 +264,6 @@ function parseUnderscoreName(filename) {
}; };
} }
/**
* Resolve the skill name for an artifact.
* Prefers canonicalId from a bmad-skill-manifest.yaml sidecar when available,
* falling back to the path-derived name from toDashPath().
*
* @param {Object} artifact - Artifact object (must have relativePath; may have canonicalId)
* @returns {string} Filename like 'bmad-create-prd.md' or 'bmad-agent-bmm-pm.md'
*/
function resolveSkillName(artifact) {
if (artifact.canonicalId) {
return `${artifact.canonicalId}.md`;
}
return toDashPath(artifact.relativePath);
}
// Backward compatibility aliases (colon format was same as underscore) // Backward compatibility aliases (colon format was same as underscore)
const toColonName = toUnderscoreName; const toColonName = toUnderscoreName;
const toColonPath = toUnderscorePath; const toColonPath = toUnderscorePath;
@ -290,7 +275,6 @@ module.exports = {
// New standard (dash-based) // New standard (dash-based)
toDashName, toDashName,
toDashPath, toDashPath,
resolveSkillName,
customAgentDashName, customAgentDashName,
isDashFormat, isDashFormat,
parseDashName, parseDashName,

View File

@ -1,48 +0,0 @@
const path = require('node:path');
const fs = require('fs-extra');
const yaml = require('yaml');
/**
* Load bmad-skill-manifest.yaml from a directory.
* Single-entry manifests (canonicalId at top level) apply to all files in the directory.
* Multi-entry manifests are keyed by source filename.
* @param {string} dirPath - Directory to check for bmad-skill-manifest.yaml
* @returns {Object|null} Parsed manifest or null
*/
async function loadSkillManifest(dirPath) {
const manifestPath = path.join(dirPath, 'bmad-skill-manifest.yaml');
try {
if (!(await fs.pathExists(manifestPath))) return null;
const content = await fs.readFile(manifestPath, 'utf8');
const parsed = yaml.parse(content);
if (!parsed || typeof parsed !== 'object') return null;
if (parsed.canonicalId) return { __single: parsed };
return parsed;
} catch (error) {
console.warn(`Warning: Failed to parse bmad-skill-manifest.yaml in ${dirPath}: ${error.message}`);
return null;
}
}
/**
* Get the canonicalId 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 (e.g., 'pm.md', 'help.md', 'pm.agent.yaml')
* @returns {string} canonicalId or empty string
*/
function getCanonicalId(manifest, filename) {
if (!manifest) return '';
// Single-entry manifest applies to all files in the directory
if (manifest.__single) return manifest.__single.canonicalId || '';
// Multi-entry: look up by filename directly
if (manifest[filename]) return manifest[filename].canonicalId || '';
// Fallback: try alternate extensions for compiled files
const baseName = filename.replace(/\.(md|xml)$/i, '');
const agentKey = `${baseName}.agent.yaml`;
if (manifest[agentKey]) return manifest[agentKey].canonicalId || '';
const xmlKey = `${baseName}.xml`;
if (manifest[xmlKey]) return manifest[xmlKey].canonicalId || '';
return '';
}
module.exports = { loadSkillManifest, getCanonicalId };

View File

@ -50,7 +50,6 @@ class TaskToolCommandGenerator {
displayName: task.displayName || task.name, displayName: task.displayName || task.name,
description: task.description || `Execute ${task.displayName || task.name}`, description: task.description || `Execute ${task.displayName || task.name}`,
module: task.module, module: task.module,
canonicalId: task.canonicalId || '',
// Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows) // Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows)
relativePath: `${task.module}/tasks/${task.name}${taskExt}`, relativePath: `${task.module}/tasks/${task.name}${taskExt}`,
path: taskPath, path: taskPath,
@ -76,7 +75,6 @@ class TaskToolCommandGenerator {
displayName: tool.displayName || tool.name, displayName: tool.displayName || tool.name,
description: tool.description || `Execute ${tool.displayName || tool.name}`, description: tool.description || `Execute ${tool.displayName || tool.name}`,
module: tool.module, module: tool.module,
canonicalId: tool.canonicalId || '',
// Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows) // Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows)
relativePath: `${tool.module}/tools/${tool.name}${toolExt}`, relativePath: `${tool.module}/tools/${tool.name}${toolExt}`,
path: toolPath, path: toolPath,

View File

@ -93,7 +93,6 @@ class WorkflowCommandGenerator {
name: workflow.name, name: workflow.name,
description: workflow.description || `${workflow.name} workflow`, description: workflow.description || `${workflow.name} workflow`,
module: workflow.module, module: workflow.module,
canonicalId: workflow.canonicalId || '',
relativePath: path.join(workflow.module, 'workflows', `${workflow.name}.md`), relativePath: path.join(workflow.module, 'workflows', `${workflow.name}.md`),
workflowPath: workflowRelPath, // Relative path to actual workflow file workflowPath: workflowRelPath, // Relative path to actual workflow file
content: commandContent, content: commandContent,

View File

@ -1,215 +0,0 @@
# Native Skills Migration Checklist
Branch: `refactor/all-is-skills`
Scope: migrate the BMAD-supported platforms that fully support the Agent Skills standard from legacy installer outputs to native skills output.
Current branch status:
- `Claude Code` has already been moved to `.claude/skills`
- `Codex CLI` has already been moved to `.agents/skills`
This checklist now includes those completed platforms plus the remaining full-support platforms.
## Claude Code
Support assumption: full Agent Skills support. BMAD has already migrated from `.claude/commands` to `.claude/skills`.
- [ ] Confirm current implementation still matches Claude Code skills expectations
- [ ] Confirm legacy cleanup for `.claude/commands`
- [ ] Test fresh install
- [ ] Test reinstall/upgrade from legacy command output
- [ ] Confirm ancestor conflict protection
- [ ] Implement/extend automated tests as needed
- [ ] Commit any follow-up fixes if required
## Codex CLI
Support assumption: full Agent Skills support. BMAD has already migrated from `.codex/prompts` to `.agents/skills`.
- [ ] Confirm current implementation still matches Codex CLI skills expectations
- [ ] Confirm legacy cleanup for project and global `.codex/prompts`
- [ ] Test fresh install
- [ ] Test reinstall/upgrade from legacy prompt output
- [x] Confirm ancestor conflict protection because Codex inherits parent-directory `.agents/skills`
- [ ] Implement/extend automated tests as needed
- [ ] Commit any follow-up fixes if required
## Cursor
Support assumption: full Agent Skills support. BMAD currently installs legacy command files to `.cursor/commands`; target should move to a native skills directory.
- [x] Confirm current Cursor skills path and that BMAD should target `.cursor/skills`
- [x] Implement installer migration to native skills output
- [x] Add legacy cleanup for `.cursor/commands`
- [x] Test fresh install
- [x] Test reinstall/upgrade from legacy command output
- [x] Confirm no ancestor conflict protection is needed because a child workspace surfaced child `.cursor/skills` entries but not a parent-only skill during manual verification
- [ ] Implement/extend automated tests
- [x] Commit
## Windsurf
Support assumption: full Agent Skills support. Windsurf docs confirm workspace skills at `.windsurf/skills` and global skills at `~/.codeium/windsurf/skills`. BMAD has now migrated from `.windsurf/workflows` to `.windsurf/skills`. Manual verification also confirmed that Windsurf custom skills are triggered via `@skill-name`, not slash commands.
- [x] Confirm Windsurf native skills directory as `.windsurf/skills`
- [x] Implement installer migration to native skills output
- [x] Add legacy cleanup for `.windsurf/workflows`
- [x] Test fresh install
- [x] Test reinstall/upgrade from legacy workflow output
- [x] Confirm no ancestor conflict protection is needed because manual Windsurf verification showed child-local `@` skills loaded while a parent-only skill was not inherited
- [x] Implement/extend automated tests
- [x] Commit
## Cline
Support assumption: full Agent Skills support. BMAD currently installs workflow files to `.clinerules/workflows`; target should move to the platform's native skills directory.
- [ ] Confirm current Cline skills path and whether `.cline/skills` is the correct BMAD target
- [ ] Implement installer migration to native skills output
- [ ] Add legacy cleanup for `.clinerules/workflows`
- [ ] Test fresh install
- [ ] Test reinstall/upgrade from legacy workflow output
- [ ] Confirm ancestor conflict protection where applicable
- [ ] Implement/extend automated tests
- [ ] Commit
## Google Antigravity
Support assumption: full Agent Skills support. Antigravity docs confirm workspace skills at `.agent/skills/<skill-folder>/` and global skills at `~/.gemini/antigravity/skills/<skill-folder>/`. BMAD has now migrated from `.agent/workflows` to `.agent/skills`.
- [x] Confirm Antigravity native skills path and project/global precedence
- [x] Implement installer migration to native skills output
- [x] Add legacy cleanup for `.agent/workflows`
- [x] Test fresh install
- [x] Test reinstall/upgrade from legacy workflow output
- [x] Confirm no ancestor conflict protection is needed because manual Antigravity verification in `/tmp/antigravity-ancestor-repro/parent/child` showed only the child-local `child-only` skill, with no inherited parent `.agent/skills` entry
- [x] Implement/extend automated tests
- [x] Commit
## Auggie
Support assumption: full Agent Skills support. BMAD currently installs commands to `.augment/commands`; target should move to `.augment/skills`.
- [x] Confirm Auggie native skills path and compatibility loading from `.claude/skills` and `.agents/skills` via Augment docs plus local `auggie --print` repros
- [x] Implement installer migration to native skills output
- [x] Add legacy cleanup for `.augment/commands`
- [x] Test fresh install
- [x] Test reinstall/upgrade from legacy command output
- [x] Confirm no ancestor conflict protection is needed because local `auggie --workspace-root` repro showed child-local `.augment/skills` loading `child-only` but not parent `parent-only`
- [x] Implement/extend automated tests
- [ ] Commit
## CodeBuddy
Support assumption: full Agent Skills support. BMAD currently installs commands to `.codebuddy/commands`; target should move to `.codebuddy/skills`.
- [ ] Confirm CodeBuddy native skills path and any naming/frontmatter requirements
- [ ] Implement installer migration to native skills output
- [ ] Add legacy cleanup for `.codebuddy/commands`
- [ ] Test fresh install
- [ ] Test reinstall/upgrade from legacy command output
- [ ] Confirm ancestor conflict protection where applicable
- [ ] Implement/extend automated tests
- [ ] Commit
## Crush
Support assumption: full Agent Skills support. BMAD currently installs commands to `.crush/commands`; target should move to the platform's native skills location.
- [ ] Confirm Crush project-local versus global skills path and BMAD's preferred install target
- [ ] Implement installer migration to native skills output
- [ ] Add legacy cleanup for `.crush/commands`
- [ ] Test fresh install
- [ ] Test reinstall/upgrade from legacy command output
- [ ] Confirm ancestor conflict protection where applicable
- [ ] Implement/extend automated tests
- [ ] Commit
## Kiro
Support assumption: full Agent Skills support. Kiro docs confirm project skills at `.kiro/skills/<skill-name>/SKILL.md` and describe steering as a separate rules mechanism, not a required compatibility layer. BMAD has now migrated from `.kiro/steering` to `.kiro/skills`. Manual app verification also confirmed that Kiro can surface skills in Slash when the relevant UI setting is enabled, and that it does not inherit ancestor `.kiro/skills` directories.
- [x] Confirm Kiro skills path and verify BMAD should stop writing steering artifacts for this migration
- [x] Implement installer migration to native skills output
- [x] Add legacy cleanup for `.kiro/steering`
- [x] Test fresh install
- [x] Test reinstall/upgrade from legacy steering output
- [x] Confirm no ancestor conflict protection is needed because manual Kiro verification showed Slash-visible skills from the current workspace only, with no ancestor `.kiro/skills` inheritance
- [x] Implement/extend automated tests
- [x] Commit
## OpenCode
Support assumption: full Agent Skills support. BMAD currently splits output between `.opencode/agents` and `.opencode/commands`; target should consolidate to `.opencode/skills`.
- [x] Confirm OpenCode native skills path and compatibility loading from `.claude/skills` and `.agents/skills` in OpenCode docs and with local `opencode run` repros
- [x] Implement installer migration from multi-target legacy output to single native skills target
- [x] Add legacy cleanup for `.opencode/agents`, `.opencode/commands`, `.opencode/agent`, and `.opencode/command`
- [x] Test fresh install
- [x] Test reinstall/upgrade from split legacy output
- [x] Confirm ancestor conflict protection is required because local `opencode run` repros loaded both child-local `child-only` and ancestor `parent-only`, matching the docs that project-local skill discovery walks upward to the git worktree
- [x] Implement/extend automated tests
- [ ] Commit
## Roo Code
Support assumption: full Agent Skills support. BMAD currently installs commands to `.roo/commands`; target should move to `.roo/skills` or the correct mode-aware skill directories.
- [ ] Confirm Roo native skills path and whether BMAD should use generic or mode-specific skill directories
- [ ] Implement installer migration to native skills output
- [ ] Add legacy cleanup for `.roo/commands`
- [ ] Test fresh install
- [ ] Test reinstall/upgrade from legacy command output
- [ ] Confirm ancestor conflict protection where applicable
- [ ] Implement/extend automated tests
- [ ] Commit
## Trae
Support assumption: full Agent Skills support. BMAD currently installs rule files to `.trae/rules`; target should move to the platform's native skills directory.
- [ ] Confirm Trae native skills path and whether the current `.trae/rules` path is still required for compatibility
- [ ] Implement installer migration to native skills output
- [ ] Add legacy cleanup for `.trae/rules`
- [ ] Test fresh install
- [ ] Test reinstall/upgrade from legacy rules output
- [ ] Confirm ancestor conflict protection where applicable
- [ ] Implement/extend automated tests
- [ ] Commit
## GitHub Copilot
Support assumption: full Agent Skills support. BMAD currently uses a custom installer that generates `.github/agents`, `.github/prompts`, and `.github/copilot-instructions.md`; target should move to `.github/skills`.
- [ ] Confirm GitHub Copilot native skills path and whether `.github/agents` remains necessary as a compatibility layer
- [ ] Design the migration away from the custom prompt/agent installer model
- [ ] Implement native skills output, ideally with shared config-driven code where practical
- [ ] Add legacy cleanup for `.github/agents`, `.github/prompts`, and any BMAD-owned Copilot instruction file behavior that should be retired
- [ ] Test fresh install
- [ ] Test reinstall/upgrade from legacy custom installer output
- [ ] Confirm ancestor conflict protection where applicable
- [ ] Implement/extend automated tests
- [ ] Commit
## KiloCoder
Support assumption: full Agent Skills support. BMAD currently uses a custom installer that writes `.kilocodemodes` and `.kilocode/workflows`; target should move to native skills output.
- [ ] Confirm KiloCoder native skills path and whether `.kilocodemodes` should be removed entirely or retained temporarily for compatibility
- [ ] Design the migration away from modes plus workflow markdown
- [ ] Implement native skills output
- [ ] Add legacy cleanup for `.kilocode/workflows` and BMAD-owned entries in `.kilocodemodes`
- [ ] Test fresh install
- [ ] Test reinstall/upgrade from legacy custom installer output
- [ ] Confirm ancestor conflict protection where applicable
- [ ] Implement/extend automated tests
- [ ] Commit
## Summary Gates
- [ ] All full-support BMAD platforms install `SKILL.md` directory-based output
- [ ] No full-support platform still emits BMAD command/workflow/rule files as its primary install format
- [ ] Legacy cleanup paths are defined for every migrated platform
- [ ] Automated coverage exists for config-driven and custom-installer migrations
- [ ] Installer docs and migration notes updated after code changes land

View File

@ -36,28 +36,11 @@ export default defineConfig({
}, },
integrations: [ integrations: [
// Exclude custom 404 pages (all locales) from the sitemap — they are sitemap(),
// treated as normal content docs by Starlight even with disable404Route.
sitemap({
filter: (page) => !/\/404(\/|$)/.test(new URL(page).pathname),
}),
starlight({ starlight({
title: 'BMAD Method', title: 'BMAD Method',
tagline: 'AI-driven agile development with specialized agents and workflows that scale from bug fixes to enterprise platforms.', tagline: 'AI-driven agile development with specialized agents and workflows that scale from bug fixes to enterprise platforms.',
// i18n: English as root (no URL prefix), Chinese at /zh-cn/
defaultLocale: 'root',
locales: {
root: {
label: 'English',
lang: 'en',
},
'zh-cn': {
label: '简体中文',
lang: 'zh-CN',
},
},
logo: { logo: {
light: './public/img/bmad-light.png', light: './public/img/bmad-light.png',
dark: './public/img/bmad-dark.png', dark: './public/img/bmad-dark.png',
@ -106,29 +89,25 @@ export default defineConfig({
// Sidebar configuration (Diataxis structure) // Sidebar configuration (Diataxis structure)
sidebar: [ sidebar: [
{ label: 'Welcome', translations: { 'zh-CN': '欢迎' }, slug: 'index' }, { label: 'Welcome', slug: 'index' },
{ label: 'Roadmap', translations: { 'zh-CN': '路线图' }, slug: 'roadmap' }, { label: 'Roadmap', slug: 'roadmap' },
{ {
label: 'Tutorials', label: 'Tutorials',
translations: { 'zh-CN': '教程' },
collapsed: false, collapsed: false,
autogenerate: { directory: 'tutorials' }, autogenerate: { directory: 'tutorials' },
}, },
{ {
label: 'How-To Guides', label: 'How-To Guides',
translations: { 'zh-CN': '操作指南' },
collapsed: true, collapsed: true,
autogenerate: { directory: 'how-to' }, autogenerate: { directory: 'how-to' },
}, },
{ {
label: 'Explanation', label: 'Explanation',
translations: { 'zh-CN': '概念说明' },
collapsed: true, collapsed: true,
autogenerate: { directory: 'explanation' }, autogenerate: { directory: 'explanation' },
}, },
{ {
label: 'Reference', label: 'Reference',
translations: { 'zh-CN': '参考' },
collapsed: true, collapsed: true,
autogenerate: { directory: 'reference' }, autogenerate: { directory: 'reference' },
}, },

View File

@ -1,7 +1,6 @@
import { defineCollection } from 'astro:content'; import { defineCollection } from 'astro:content';
import { docsSchema, i18nSchema } from '@astrojs/starlight/schema'; import { docsSchema } from '@astrojs/starlight/schema';
export const collections = { export const collections = {
docs: defineCollection({ schema: docsSchema() }), docs: defineCollection({ schema: docsSchema() }),
i18n: defineCollection({ type: 'data', schema: i18nSchema() }),
}; };

View File

@ -1,28 +0,0 @@
{
"skipLink.label": "跳转到内容",
"search.label": "搜索",
"search.ctrlKey": "Ctrl",
"search.cancelLabel": "取消",
"themeSelect.accessibleLabel": "选择主题",
"themeSelect.dark": "深色",
"themeSelect.light": "浅色",
"themeSelect.auto": "自动",
"languageSelect.accessibleLabel": "选择语言",
"menuButton.accessibleLabel": "菜单",
"sidebarNav.accessibleLabel": "主导航",
"tableOfContents.onThisPage": "本页内容",
"tableOfContents.overview": "概述",
"i18n.untranslatedContent": "此内容尚未提供中文翻译。",
"page.editLink": "编辑页面",
"page.lastUpdated": "最后更新:",
"page.previousLink": "上一页",
"page.nextLink": "下一页",
"page.draft": "此内容为草稿,不会包含在正式版本中。",
"404.text": "页面未找到。请检查 URL 或尝试使用搜索。",
"aside.note": "注意",
"aside.tip": "提示",
"aside.caution": "警告",
"aside.danger": "危险",
"fileTree.directory": "目录",
"builtWithStarlight.label": "使用 Starlight 构建"
}