Compare commits
1 Commits
b1d2824691
...
06a618d6e0
| Author | SHA1 | Date |
|---|---|---|
|
|
06a618d6e0 |
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@ CLAUDE.local.md
|
||||||
.agents/
|
.agents/
|
||||||
|
|
||||||
z*/
|
z*/
|
||||||
!docs/zh-cn/
|
|
||||||
|
|
||||||
_bmad
|
_bmad
|
||||||
_bmad-output
|
_bmad-output
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,4 @@ template: splash
|
||||||
|
|
||||||
您查找的页面不存在或已被移动。
|
您查找的页面不存在或已被移动。
|
||||||
|
|
||||||
[返回首页](./index.md)
|
[返回首页](./index_cn.md)
|
||||||
|
|
@ -15,7 +15,7 @@ sidebar:
|
||||||
- 您希望智能体在每次启动时执行特定操作
|
- 您希望智能体在每次启动时执行特定操作
|
||||||
|
|
||||||
:::note[前置条件]
|
:::note[前置条件]
|
||||||
- 在项目中安装了 BMad(参见[如何安装 BMad](./install-bmad.md))
|
- 在项目中安装了 BMad(参见[如何安装 BMad](./install-bmad_cn.md))
|
||||||
- 用于编辑 YAML 文件的文本编辑器
|
- 用于编辑 YAML 文件的文本编辑器
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
|
@ -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)** - 关于在既有项目上工作的常见问题
|
||||||
|
|
||||||
---
|
---
|
||||||
## 术语说明
|
## 术语说明
|
||||||
|
|
@ -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 指向正确的来源。
|
||||||
|
|
||||||
---
|
---
|
||||||
## 术语说明
|
## 术语说明
|
||||||
|
|
@ -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) — 查看哪些工作流程加载项目上下文
|
||||||
|
|
||||||
---
|
---
|
||||||
## 术语说明
|
## 术语说明
|
||||||
|
|
@ -115,7 +115,7 @@ DEV 智能体也适用于探索不熟悉的代码。在新的聊天中加载它
|
||||||
|
|
||||||
## 何时升级到正式规划
|
## 何时升级到正式规划
|
||||||
|
|
||||||
在以下情况下考虑使用 [Quick Flow](../explanation/quick-flow.md) 或完整的 BMad Method:
|
在以下情况下考虑使用 [Quick Flow](../explanation/quick-flow_cn.md) 或完整的 BMad Method:
|
||||||
|
|
||||||
- 更改影响多个系统或需要在许多文件中进行协调更新
|
- 更改影响多个系统或需要在许多文件中进行协调更新
|
||||||
- 你不确定范围,需要规范来理清思路
|
- 你不确定范围,需要规范来理清思路
|
||||||
|
|
@ -22,7 +22,7 @@ sidebar:
|
||||||
|
|
||||||
### 1. 运行安装程序
|
### 1. 运行安装程序
|
||||||
|
|
||||||
按照[安装程序说明](./install-bmad.md)操作。
|
按照[安装程序说明](./install-bmad_cn.md)操作。
|
||||||
|
|
||||||
### 2. 处理旧版安装
|
### 2. 处理旧版安装
|
||||||
|
|
||||||
|
|
@ -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)** 并构建您的第一个项目。
|
||||||
|
|
||||||
---
|
---
|
||||||
## 术语说明
|
## 术语说明
|
||||||
|
|
@ -83,7 +83,7 @@ BMad 提供两种开始工作的方式,它们服务于不同的目的。
|
||||||
| `/bmad-agent-bmm-architect` | Winston(架构师) | 设计系统架构 |
|
| `/bmad-agent-bmm-architect` | Winston(架构师) | 设计系统架构 |
|
||||||
| `/bmad-agent-bmm-sm` | Bob(Scrum Master) | 管理冲刺和故事 |
|
| `/bmad-agent-bmm-sm` | Bob(Scrum 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)获取描述。
|
||||||
|
|
||||||
## 故障排除
|
## 故障排除
|
||||||
|
|
||||||
|
|
@ -103,7 +103,7 @@ Quinn 的 Automate 工作流出现在 BMad 方法工作流图的第 4 阶段(
|
||||||
|
|
||||||
Quinn 直接从源代码工作,无需加载规划文档(PRD、架构)。TEA 工作流可以与上游规划产物集成以实现可追溯性。
|
Quinn 直接从源代码工作,无需加载规划文档(PRD、架构)。TEA 工作流可以与上游规划产物集成以实现可追溯性。
|
||||||
|
|
||||||
有关测试在整体流程中的位置,请参阅[工作流图](./workflow-map.md)。
|
有关测试在整体流程中的位置,请参阅[工作流图](./workflow-map_cn.md)。
|
||||||
|
|
||||||
---
|
---
|
||||||
## 术语说明
|
## 术语说明
|
||||||
|
|
@ -86,7 +86,7 @@ BMad Method(BMM)是 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)
|
||||||
|
|
||||||
---
|
---
|
||||||
## 术语说明
|
## 术语说明
|
||||||
|
|
@ -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:分析(可选)
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
canonicalId: bmad-tech-writer
|
|
||||||
type: agent
|
|
||||||
description: "Technical Writer for documentation, Mermaid diagrams, and standards compliance"
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
canonicalId: bmad-create-product-brief
|
|
||||||
type: workflow
|
|
||||||
description: "Create product brief through collaborative discovery"
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
canonicalId: bmad-create-ux-design
|
|
||||||
type: workflow
|
|
||||||
description: "Plan UX patterns and design specifications"
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
canonicalId: bmad-check-implementation-readiness
|
|
||||||
type: workflow
|
|
||||||
description: "Validate PRD, UX, Architecture and Epics specs are complete"
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
canonicalId: bmad-create-architecture
|
|
||||||
type: workflow
|
|
||||||
description: "Create architecture solution design decisions for AI agent consistency"
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
canonicalId: bmad-create-epics-and-stories
|
|
||||||
type: workflow
|
|
||||||
description: "Break requirements into epics and user stories"
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
canonicalId: bmad-code-review
|
|
||||||
type: workflow
|
|
||||||
description: "Perform adversarial code review finding specific issues"
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
canonicalId: bmad-correct-course
|
|
||||||
type: workflow
|
|
||||||
description: "Manage significant changes during sprint execution"
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
canonicalId: bmad-create-story
|
|
||||||
type: workflow
|
|
||||||
description: "Creates a dedicated story file with all the context needed for implementation"
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
canonicalId: bmad-dev-story
|
|
||||||
type: workflow
|
|
||||||
description: "Execute story implementation following a context-filled story spec 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>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
canonicalId: bmad-retrospective
|
|
||||||
type: workflow
|
|
||||||
description: "Post-epic review to extract lessons and assess success"
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
canonicalId: bmad-sprint-planning
|
|
||||||
type: workflow
|
|
||||||
description: "Generate sprint status tracking from epics"
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
canonicalId: bmad-sprint-status
|
|
||||||
type: workflow
|
|
||||||
description: "Summarize sprint status and surface risks"
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
canonicalId: bmad-quick-dev-new-preview
|
|
||||||
type: workflow
|
|
||||||
description: "Unified quick flow - clarify intent, plan, implement, review, present"
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
canonicalId: bmad-quick-dev
|
|
||||||
type: workflow
|
|
||||||
description: "Implement a Quick Tech Spec for small changes or features"
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
canonicalId: bmad-document-project
|
|
||||||
type: workflow
|
|
||||||
description: "Document brownfield projects for AI context"
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
canonicalId: bmad-generate-project-context
|
|
||||||
type: workflow
|
|
||||||
description: "Create project-context.md with AI rules"
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
canonicalId: bmad-qa-generate-e2e-tests
|
|
||||||
type: workflow
|
|
||||||
description: "Generate end-to-end automated tests for existing features"
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
canonicalId: bmad-master
|
|
||||||
type: agent
|
|
||||||
description: "BMad Master Executor, Knowledge Custodian, and Workflow Orchestrator"
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
canonicalId: bmad-brainstorming
|
|
||||||
type: workflow
|
|
||||||
description: "Facilitate interactive brainstorming sessions using diverse creative techniques and ideation methods"
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
canonicalId: bmad-party-mode
|
|
||||||
type: workflow
|
|
||||||
description: "Orchestrates group discussions between all installed BMAD agents"
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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 = [];
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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' },
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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() }),
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 构建"
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue