Compare commits

...

23 Commits

Author SHA1 Message Date
Wendy Smoak 3ba2a626ee
Merge f0ad64efc4 into aa573bdbb8 2026-02-17 09:25:09 +03:00
Brian Madison aa573bdbb8 chore: bump version to 6.0.0 stable release
V6 Stable Release! The End of Beta!
2026-02-16 23:40:33 -06:00
Brian Madison 3f688d5669 docs: update CHANGELOG for v6.0.0 stable release
V6 Stable Release! The End of Beta!
2026-02-16 23:40:14 -06:00
Brian Madison eb88384d9e docs: add BMad Builder link to index for extenders
Adds a new "Extend and Customize" section with a link to the BMad Builder documentation for users interested in creating custom agents, workflows, or modules.
2026-02-16 23:29:07 -06:00
Brian Madison 469a2e288c docs: update README for V6 stable release and enhance installer next steps
- Update README to reflect V6 stable release and platform positioning
- Emphasize BMad Method as a module within the Module Ecosystem
- Simplify quick start and module sections
- Add Discord, GitHub, and YouTube links to installer next steps
2026-02-16 23:23:24 -06:00
Wendy Smoak f0ad64efc4 Handle Windows line endings like manifest-generator.js:175 2026-02-15 11:54:30 -05:00
Wendy Smoak decf15b5da Add more tests for Coderabbit 2026-02-15 11:27:49 -05:00
Wendy Smoak 64e5a9c696 guard against missing project directory 2026-02-15 10:12:49 -05:00
Wendy Smoak b318d9242e fix(codex): use yaml.stringify for skill frontmatter to escape special characters
Prevents invalid YAML when agentName or skillName contains quotes or
other special characters. Aligns the fallback path in transformToSkillFormat
and installCustomAgentLauncher with the existing yaml.stringify usage on
the main code path.
2026-02-15 10:03:54 -05:00
Wendy Smoak 6db629278a Use /Users/wsmoak instead of ~ 2026-02-15 09:49:16 -05:00
Wendy Smoak 94666bd05b
Merge branch 'main' into skills-for-codex 2026-02-15 09:37:23 -05:00
Wendy Smoak dfd961944c add more tests 2026-02-15 09:17:53 -05:00
Wendy Smoak d0ef58b421 Handle Windows line-endings 2026-02-15 09:06:16 -05:00
Wendy Smoak 4bd43ec8b9
Merge branch 'main' into skills-for-codex 2026-02-15 08:05:23 -05:00
Wendy Smoak 7f81518896 Replace description regex with yaml.parse/stringify in codex.js
Use the yaml library (already a project dependency) instead of a
hand-rolled regex to parse and re-serialize frontmatter descriptions,
matching the pattern used in manifest-generator.js. Update tests to
validate round-trip correctness rather than exact quoting style.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 07:13:30 -05:00
Wendy Smoak 43672d33c1 simplify test 2026-02-14 19:39:24 -05:00
Wendy Smoak 26dd80e021 Fix description regex to handle quotes in transformToSkillFormat
The regex that extracts descriptions from YAML frontmatter did not
properly parse quoted values. Descriptions containing apostrophes
(e.g. "can't") produced invalid YAML when re-wrapped in single quotes.

Replace the naive regex with proper handling of single-quoted,
double-quoted, and unquoted YAML values, and escape inner single
quotes using YAML '' syntax before re-wrapping.

Add tests for transformToSkillFormat covering plain, double-quoted,
and single-quoted descriptions with embedded apostrophes.
2026-02-14 19:31:04 -05:00
Wendy Smoak 746bd50d19 Add .agents directory to .gitignore 2026-02-14 19:19:02 -05:00
Wendy Smoak fcf781fa39 clean up comments 2026-02-14 19:07:49 -05:00
Wendy Smoak 44f07bf341 Clarify where Codex searches for skills 2026-02-14 19:05:09 -05:00
Wendy Smoak 79855b2c4c default to per-project instead of global 2026-02-14 18:51:53 -05:00
Wendy Smoak 62e0b0ba52 switch default to per-project skills to align with other tools 2026-02-14 18:31:14 -05:00
Wendy Smoak 18a4a489c8 feat(codex): convert installer from deprecated prompts to agentskills.io skills format
Codex CLI has deprecated custom prompts in favor of skills following the
agentskills.io specification. Updates the Codex installer to write
{name}/SKILL.md directories into .agents/skills/ instead of flat files
into .codex/prompts/. Removes CODEX_HOME requirement for project-scoped
installs since Codex auto-discovers .agents/skills/ directories.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 15:06:12 -05:00
10 changed files with 582 additions and 193 deletions

1
.gitignore vendored
View File

@ -36,6 +36,7 @@ cursor
CLAUDE.local.md
.serena/
.claude/settings.local.json
.agents
z*/

View File

@ -1,5 +1,40 @@
# Changelog
## [6.0.0]
V6 Stable Release! The End of Beta!
### 🎁 Features
* Add PRD workflow steps 2b (vision/differentiators) and 2c (executive summary) for more complete product requirements documentation
* Add new `bmad uninstall` command with interactive and non-interactive modes for selective component removal
* Add dedicated GitHub Copilot installer that generates enriched `.agent.md`, `.prompt.md` files and project configuration
* Add TEA browser automation prerequisite prompts to guide Playwright CLI/MCP setup after configuration
### 🐛 Bug Fixes
* Fix version comparison to use semantic versioning, preventing incorrect downgrade recommendations to older beta versions
* Fix `--custom-content` flag to properly populate sources and selected files in module config
* Fix module configuration UX messaging to show accurate completion status and improve feedback timing
* Fix changelog URL in installer start message for proper GitHub resolution
* Remove incorrect `mode: primary` from OpenCode agent template and restore `name` field across all templates
* Auto-discover PRD files in validate-prd workflow to reduce manual path input
* Fix installer non-interactive mode hanging and improve IDE configuration handling during updates
* Fix workflow-level config.yaml copying for custom content modules
### ♻️ Refactoring
* Remove alias variables from Phase 4 workflows, use canonical `{implementation_artifacts}` and `{planning_artifacts}`
* Add missing `project_context` references to workflows for consistency
### 📚 Documentation
* Add post-install notes documentation for modules
* Improve project-context documentation and fix folder structure
* Add BMad Builder link to index for extenders
---
## [6.0.0-Beta.8]
**Release: February 8, 2026**

116
README.md
View File

@ -5,20 +5,22 @@
[![Node.js Version](https://img.shields.io/badge/node-%3E%3D20.0.0-brightgreen)](https://nodejs.org)
[![Discord](https://img.shields.io/badge/Discord-Join%20Community-7289da?logo=discord&logoColor=white)](https://discord.gg/gk8jAdXWmj)
**Breakthrough Method of Agile AI Driven Development** — An AI-driven agile development framework with 21 specialized agents, 50+ guided workflows, and scale-adaptive intelligence that adjusts from bug fixes to enterprise systems.
**Breakthrough Method of Agile AI Driven Development** — An AI-driven agile development module for the BMad Method Module Ecosystem, the best and most comprehensive Agile AI Driven Development framework that has true scale-adaptive intelligence that adjusts from bug fixes to enterprise systems.
**100% free and open source.** No paywalls. No gated content. No gated Discord. We believe in empowering everyone, not just those who can pay.
**100% free and open source.** No paywalls. No gated content. No gated Discord. We believe in empowering everyone, not just those who can pay for a gated community or courses.
## Why BMad?
## Why the BMad Method?
Traditional AI tools do the thinking for you, producing average results. BMad agents and facilitated workflow act as expert collaborators who guide you through a structured process to bring out your best thinking in partnership with the AI.
Traditional AI tools do the thinking for you, producing average results. BMad agents and facilitated workflows act as expert collaborators who guide you through a structured process to bring out your best thinking in partnership with the AI.
- **AI Intelligent Help**: Brand new for beta - AI assisted help will guide you from the beginning to the end - just ask for `/bmad-help` after you have installed BMad to your project
- **Scale-Domain-Adaptive**: Automatically adjusts planning depth and needs based on project complexity, domain and type - a SaaS Mobile Dating App has different planning needs from a diagnostic medical system, BMad adapts and helps you along the way
- **Structured Workflows**: Grounded in agile best practices across analysis, planning, architecture, and implementation
- **Specialized Agents**: 12+ domain experts (PM, Architect, Developer, UX, Scrum Master, and more)
- **Party Mode**: Bring multiple agent personas into one session to plan, troubleshoot, or discuss your project collaboratively, multiple perspectives with maximum fun
- **Complete Lifecycle**: From brainstorming to deployment, BMad is there with you every step of the way
- **AI Intelligent Help** — Ask `/bmad-help` anytime for guidance on what's next
- **Scale-Domain-Adaptive** — Automatically adjusts planning depth based on project complexity
- **Structured Workflows** — Grounded in agile best practices across analysis, planning, architecture, and implementation
- **Specialized Agents** — 12+ domain experts (PM, Architect, Developer, UX, Scrum Master, and more)
- **Party Mode** — Bring multiple agent personas into one session to collaborate and discuss
- **Complete Lifecycle** — From brainstorming to deployment
[Learn more at **docs.bmad-method.org**](http://docs.bmad-method.org)
## Quick Start
@ -28,103 +30,39 @@ Traditional AI tools do the thinking for you, producing average results. BMad ag
npx bmad-method install
```
Follow the installer prompts, then open your AI IDE (Claude Code, Cursor, Windsurf, etc.) in the project folder.
Follow the installer prompts, then open your AI IDE (Claude Code, Codex, Windsurf, etc.) in your project folder.
**Non-Interactive Installation**: For CI/CD pipelines or automated deployments, use command-line flags:
**Non-Interactive Installation** (for CI/CD):
```bash
npx bmad-method install --directory /path/to/project --modules bmm --tools claude-code --yes
```
See [Non-Interactive Installation Guide](http://docs.bmad-method.org/how-to/non-interactive-installation/) for all available options.
[See all installation options](http://docs.bmad-method.org/how-to/non-interactive-installation/)
> **Not sure what to do?** Run `/bmad-help` — it tells you exactly what's next and what's optional. You can also ask it questions like:
- `/bmad-help How should I build a web app for my TShirt Business that can scale to millions?`
- `/bmad-help I just finished the architecture, I am not sure what to do next`
And the amazing thing is BMad Help evolves depending on what modules you install also!
- `/bmad-help Im interested in really exploring creative ways to demo BMad at work, what do you recommend to help plan a great slide deck and compelling narrative?`, and if you have the Creative Intelligence Suite installed, it will offer you different or complimentary advice than if you just have BMad Method Module installed!
The workflows below show the fastest path to working code. You can also load agents directly for a more structured process, extensive planning, or to learn about agile development practices — the agents guide you with menus, explanations, and elicitation at each step.
### Simple Path (Quick Flow)
Bug fixes, small features, clear scope — 3 commands - 1 Optional Agent:
1. `/quick-spec` — analyzes your codebase and produces a tech-spec with stories
2. `/dev-story` — implements each story
3. `/code-review` — validates quality
### Full Planning Path (BMad Method)
Products, platforms, complex features — structured planning then build:
1. `/product-brief` — define problem, users, and MVP scope
2. `/create-prd` — full requirements with personas, metrics, and risks
3. `/create-architecture` — technical decisions and system design
4. `/create-epics-and-stories` — break work into prioritized stories
5. `/sprint-planning` — initialize sprint tracking
6. **Repeat per story:** `/create-story``/dev-story``/code-review`
Every step tells you what's next. Optional phases (brainstorming, research, UX design) are available when you need them — ask `/bmad-help` anytime. For a detailed walkthrough, see the [Getting Started Tutorial](http://docs.bmad-method.org/tutorials/getting-started/).
> **Not sure what to do?** Run `/bmad-help` — it tells you exactly what's next and what's optional. You can also ask questions like `/bmad-help I just finished the architecture, what do I do next?`
## Modules
BMad Method extends with official modules for specialized domains. Modules are available during installation and can be added to your project at any time. After the V6 beta period these will also be available as Plugins and Granular Skills.
BMad Method extends with official modules for specialized domains. Available during installation or anytime after.
| Module | GitHub | NPM | Purpose |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| **BMad Method (BMM)** | [bmad-code-org/BMAD-METHOD](https://github.com/bmad-code-org/BMAD-METHOD) | [bmad-method](https://www.npmjs.com/package/bmad-method) | Core framework with 34+ workflows across 4 development phases |
| **BMad Builder (BMB)** | [bmad-code-org/bmad-builder](https://github.com/bmad-code-org/bmad-builder) | [bmad-builder](https://www.npmjs.com/package/bmad-builder) | Create custom BMad agents, workflows, and domain-specific modules |
| **Test Architect (TEA)** 🆕 | [bmad-code-org/tea](https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise) | [tea](https://www.npmjs.com/package/bmad-method-test-architecture-enterprise) | Risk-based test strategy, automation, and release gates (8 workflows) |
| **Game Dev Studio (BMGD)** | [bmad-code-org/bmad-module-game-dev-studio](https://github.com/bmad-code-org/bmad-module-game-dev-studio) | [bmad-game-dev-studio](https://www.npmjs.com/package/bmad-game-dev-studio) | Game development workflows for Unity, Unreal, and Godot |
| **Creative Intelligence Suite (CIS)** | [bmad-code-org/bmad-module-creative-intelligence-suite](https://github.com/bmad-code-org/bmad-module-creative-intelligence-suite) | [bmad-creative-intelligence-suite](https://www.npmjs.com/package/bmad-creative-intelligence-suite) | Innovation, brainstorming, design thinking, and problem-solving |
* More modules are coming in the next 2 weeks from BMad Official, and a community marketplace for the installer also will be coming with the final V6 release!
## Testing Agents
BMad provides two testing options to fit your needs:
### Quinn (QA) - Built-in
**Quick test automation for rapid coverage**
- ✅ **Always available** in BMM module (no separate install)
- ✅ **Simple**: One workflow (`QA` - Automate)
- ✅ **Beginner-friendly**: Standard test framework patterns
- ✅ **Fast**: Generate tests and ship
**Use Quinn for:** Small projects, quick coverage, standard patterns
### Test Architect (TEA) - Optional Module
**Enterprise-grade test strategy and quality engineering**
- 🆕 **Standalone module** (install separately)
- 🏗️ **Comprehensive**: 8 workflows covering full test lifecycle
- 🎯 **Advanced**: Risk-based planning, quality gates, NFR assessment
- 📚 **Knowledge-driven**: 34 testing patterns and best practices
- 📖 [Test Architect Documentation](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/)
**Use TEA for:** Enterprise projects, test strategy, compliance, release gates
---
| Module | Purpose |
| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| **[BMad Method (BMM)](https://github.com/bmad-code-org/BMAD-METHOD)** | Core framework with 34+ workflows |
| **[BMad Builder (BMB)](https://github.com/bmad-code-org/bmad-builder)** | Create custom BMad agents and workflows |
| **[Test Architect (TEA)](https://github.com/bmad-code-org/tea)** | Risk-based test strategy and automation |
| **[Game Dev Studio (BMGD)](https://github.com/bmad-code-org/bmad-module-game-dev-studio)** | Game development workflows (Unity, Unreal, Godot) |
| **[Creative Intelligence Suite (CIS)](https://github.com/bmad-code-org/bmad-module-creative-intelligence-suite)** | Innovation, brainstorming, design thinking |
## Documentation
**[BMad Documentation](http://docs.bmad-method.org)** — Tutorials, how-to guides, concepts, and reference
**[Test Architect Documentation](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/)** — TEA standalone module documentation
[BMad Method Docs Site](http://docs.bmad-method.org) — Tutorials, guides, concepts, and reference
**Quick links:**
- [Getting Started Tutorial](http://docs.bmad-method.org/tutorials/getting-started/)
- [Upgrading from Previous Versions](http://docs.bmad-method.org/how-to/upgrade-to-v6/)
- [Test Architect Migration Guide](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/migration/) — Upgrading from BMM-embedded TEA
- [Test Architect Documentation](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/)
### For v4 Users
- **[v4 Documentation](https://github.com/bmad-code-org/BMAD-METHOD/tree/V4/docs)**
- If you need to install V4, you can do this with `npx bmad-method@4.44.3 install` - similar for any past version.
## Community

View File

@ -3,7 +3,7 @@ title: Welcome to the BMad Method
description: AI-driven development framework with specialized agents, guided workflows, and intelligent planning
---
The BMad Method (**B**reakthrough **M**ethod of **A**gile AI **D**riven Development) is an AI-driven development framework that helps you build software through the whole process from ideation and planning all the way through agentic implementation. It provides specialized AI agents, guided workflows, and intelligent planning that adapts to your project's complexity, whether you're fixing a bug or building an enterprise platform.
The BMad Method (**B**reakthrough **M**ethod of **A**gile AI **D**riven Development) is an AI-driven development framework module within the BMad Method Ecosystem that helps you build software through the whole process from ideation and planning all the way through agentic implementation. It provides specialized AI agents, guided workflows, and intelligent planning that adapts to your project's complexity, whether you're fixing a bug or building an enterprise platform.
If you're comfortable working with AI coding assistants like Claude, Cursor, or GitHub Copilot, you're ready to get started.
@ -25,6 +25,10 @@ These docs are organized into four sections based on what you're trying to do:
| **Explanation** | Understanding-oriented. Deep dives into concepts and architecture. Read when you want to know *why*. |
| **Reference** | Information-oriented. Technical specifications for agents, workflows, and configuration. |
## Extend and Customize
Want to expand BMad with your own agents, workflows, or modules? The **[BMad Builder](https://bmad-builder-docs.bmad-method.org/)** provides the framework and tools for creating custom extensions, whether you're adding new capabilities to BMad or building entirely new modules from scratch.
## What You'll Need
BMad works with any AI coding assistant that supports custom system prompts or project context. Popular options include:

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "bmad-method",
"version": "6.0.0-Beta.8",
"version": "6.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bmad-method",
"version": "6.0.0-Beta.8",
"version": "6.0.0",
"license": "MIT",
"dependencies": {
"@clack/core": "^1.0.0",

View File

@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "bmad-method",
"version": "6.0.0-Beta.8",
"version": "6.0.0",
"description": "Breakthrough Method of Agile AI-driven Development",
"keywords": [
"agile",

View File

@ -0,0 +1,145 @@
/**
* Tests for CodexSetup.transformToSkillFormat
*
* Validates that descriptions round-trip correctly through parse/stringify,
* producing valid YAML regardless of input quoting style.
*
* Usage: node test/test-codex-transform.js
*/
const path = require('node:path');
const yaml = require('yaml');
// ANSI colors
const colors = {
reset: '\u001B[0m',
green: '\u001B[32m',
red: '\u001B[31m',
cyan: '\u001B[36m',
dim: '\u001B[2m',
};
let passed = 0;
let failed = 0;
function assert(condition, testName, detail) {
if (condition) {
console.log(` ${colors.green}PASS${colors.reset} ${testName}`);
passed++;
} else {
console.log(` ${colors.red}FAIL${colors.reset} ${testName}`);
if (detail) console.log(` ${colors.dim}${detail}${colors.reset}`);
failed++;
}
}
/**
* Parse the output frontmatter and return the description value.
* Validates the output is well-formed YAML that parses back correctly.
*/
function parseOutputDescription(output) {
const match = output.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!match) return null;
const parsed = yaml.parse(match[1]);
return parsed?.description;
}
// Import the class under test
const { CodexSetup } = require(path.join(__dirname, '..', 'tools', 'cli', 'installers', 'lib', 'ide', 'codex.js'));
const setup = new CodexSetup();
console.log(`\n${colors.cyan}CodexSetup.transformToSkillFormat tests${colors.reset}\n`);
// --- Simple description, no quotes ---
{
const input = `---\ndescription: A simple description\n---\n\nBody content here.`;
const result = setup.transformToSkillFormat(input, 'my-skill');
const desc = parseOutputDescription(result);
assert(desc === 'A simple description', 'simple description round-trips', `got description: ${JSON.stringify(desc)}`);
assert(result.includes('\nBody content here.'), 'body preserved for simple description');
}
// --- Description with embedded single quotes (from double-quoted YAML input) ---
{
const input = `---\ndescription: "can't stop won't stop"\n---\n\nBody content here.`;
const result = setup.transformToSkillFormat(input, 'my-skill');
const desc = parseOutputDescription(result);
assert(desc === "can't stop won't stop", 'description with apostrophes round-trips', `got description: ${JSON.stringify(desc)}`);
assert(result.includes('\nBody content here.'), 'body preserved for quoted description');
}
// --- Description with embedded single quote ---
{
const input = `---\ndescription: "it's a test"\n---\n\nBody.`;
const result = setup.transformToSkillFormat(input, 'test-skill');
const desc = parseOutputDescription(result);
assert(desc === "it's a test", 'description with apostrophe round-trips', `got description: ${JSON.stringify(desc)}`);
}
// --- Single-quoted input with pre-escaped apostrophe (YAML '' escape) ---
{
const input = `---\ndescription: 'don''t panic'\n---\n\nBody.`;
const result = setup.transformToSkillFormat(input, 'test-skill');
const desc = parseOutputDescription(result);
assert(desc === "don't panic", 'single-quoted escaped apostrophe round-trips', `got description: ${JSON.stringify(desc)}`);
}
// --- Verify name is set correctly ---
{
const input = `---\ndescription: test\n---\n\nBody.`;
const result = setup.transformToSkillFormat(input, 'my-custom-skill');
const match = result.match(/^---\n([\s\S]*?)\n---/);
const parsed = yaml.parse(match[1]);
assert(parsed.name === 'my-custom-skill', 'name field matches skillName argument', `got name: ${JSON.stringify(parsed.name)}`);
}
// --- Extra frontmatter keys are stripped ---
{
const input = `---\ndescription: foo\ndisable-model-invocation: true\ncustom-field: bar\n---\n\nBody.`;
const result = setup.transformToSkillFormat(input, 'strip-extra');
const desc = parseOutputDescription(result);
assert(desc === 'foo', 'description preserved when extra keys present', `got description: ${JSON.stringify(desc)}`);
const match = result.match(/^---\n([\s\S]*?)\n---/);
const parsed = yaml.parse(match[1]);
assert(parsed.name === 'strip-extra', 'name equals skillName after stripping extras', `got name: ${JSON.stringify(parsed.name)}`);
assert(!('disable-model-invocation' in parsed), 'disable-model-invocation stripped', `keys: ${Object.keys(parsed).join(', ')}`);
assert(!('custom-field' in parsed), 'custom-field stripped', `keys: ${Object.keys(parsed).join(', ')}`);
const keys = Object.keys(parsed).sort();
assert(
keys.length === 2 && keys[0] === 'description' && keys[1] === 'name',
'only name and description remain',
`keys: ${keys.join(', ')}`,
);
}
// --- No frontmatter wraps content ---
{
const input = 'Just some content without frontmatter.';
const result = setup.transformToSkillFormat(input, 'bare-skill');
const desc = parseOutputDescription(result);
assert(desc === 'bare-skill', 'no-frontmatter fallback uses skillName as description', `got description: ${JSON.stringify(desc)}`);
assert(result.includes('Just some content without frontmatter.'), 'body preserved when no frontmatter');
}
// --- No frontmatter with single-quote in skillName ---
{
const input = 'Body content for the skill.';
const result = setup.transformToSkillFormat(input, "it's-a-task");
const desc = parseOutputDescription(result);
assert(desc === "it's-a-task", 'no-frontmatter skillName with single quote round-trips', `got description: ${JSON.stringify(desc)}`);
assert(result.includes('Body content for the skill.'), 'body preserved for single-quote skillName');
}
// --- CRLF frontmatter is parsed correctly (Windows line endings) ---
{
const input = '---\r\ndescription: windows line endings\r\n---\r\n\r\nBody.';
const result = setup.transformToSkillFormat(input, 'crlf-skill');
const desc = parseOutputDescription(result);
assert(desc === 'windows line endings', 'CRLF frontmatter parses correctly', `got description: ${JSON.stringify(desc)}`);
assert(result.includes('Body.'), 'body preserved for CRLF input');
}
// --- Summary ---
console.log(`\n${passed} passed, ${failed} failed\n`);
process.exit(failed > 0 ? 1 : 0);

View File

@ -0,0 +1,216 @@
/**
* Tests for CodexSetup.writeSkillArtifacts
*
* Validates directory creation, SKILL.md file writing, type filtering,
* and integration with transformToSkillFormat.
*
* Usage: node test/test-codex-write-skills.js
*/
const path = require('node:path');
const fs = require('fs-extra');
const os = require('node:os');
const yaml = require('yaml');
// ANSI colors
const colors = {
reset: '\u001B[0m',
green: '\u001B[32m',
red: '\u001B[31m',
cyan: '\u001B[36m',
dim: '\u001B[2m',
};
let passed = 0;
let failed = 0;
function assert(condition, testName, detail) {
if (condition) {
console.log(` ${colors.green}PASS${colors.reset} ${testName}`);
passed++;
} else {
console.log(` ${colors.red}FAIL${colors.reset} ${testName}`);
if (detail) console.log(` ${colors.dim}${detail}${colors.reset}`);
failed++;
}
}
// Import the class under test
const { CodexSetup } = require(path.join(__dirname, '..', 'tools', 'cli', 'installers', 'lib', 'ide', 'codex.js'));
const setup = new CodexSetup();
// Create a temp directory for each test run
let tmpDir;
async function createTmpDir() {
tmpDir = path.join(os.tmpdir(), `bmad-test-skills-${Date.now()}`);
await fs.ensureDir(tmpDir);
return tmpDir;
}
async function cleanTmpDir() {
if (tmpDir) {
await fs.remove(tmpDir);
}
}
async function runTests() {
console.log(`\n${colors.cyan}CodexSetup.writeSkillArtifacts tests${colors.reset}\n`);
// --- Writes a single artifact as a skill directory with SKILL.md ---
{
const destDir = await createTmpDir();
const artifacts = [
{
type: 'task',
relativePath: 'bmm/tasks/create-story.md',
content: '---\ndescription: Create a user story\n---\n\nStory creation instructions.',
},
];
const count = await setup.writeSkillArtifacts(destDir, artifacts, 'task');
assert(count === 1, 'single artifact returns count 1');
const skillDir = path.join(destDir, 'bmad-bmm-create-story');
assert(await fs.pathExists(skillDir), 'skill directory created');
const skillFile = path.join(skillDir, 'SKILL.md');
assert(await fs.pathExists(skillFile), 'SKILL.md file created');
const content = await fs.readFile(skillFile, 'utf8');
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
assert(fmMatch !== null, 'SKILL.md has frontmatter');
const parsed = yaml.parse(fmMatch[1]);
assert(parsed.name === 'bmad-bmm-create-story', 'name matches skill directory name', `got: ${parsed.name}`);
assert(parsed.description === 'Create a user story', 'description preserved', `got: ${parsed.description}`);
assert(content.includes('Story creation instructions.'), 'body content preserved');
await cleanTmpDir();
}
// --- Filters artifacts by type ---
{
const destDir = await createTmpDir();
const artifacts = [
{
type: 'task',
relativePath: 'bmm/tasks/create-story.md',
content: '---\ndescription: A task\n---\n\nTask body.',
},
{
type: 'workflow-command',
relativePath: 'bmm/workflows/plan-project.md',
content: '---\ndescription: A workflow\n---\n\nWorkflow body.',
},
{
type: 'agent-launcher',
relativePath: 'bmm/agents/pm.md',
content: '---\ndescription: An agent\n---\n\nAgent body.',
},
];
const count = await setup.writeSkillArtifacts(destDir, artifacts, 'task');
assert(count === 1, 'only matching type is written when filtering for task');
const entries = await fs.readdir(destDir);
assert(entries.length === 1, 'only one skill directory created', `got ${entries.length}: ${entries.join(', ')}`);
assert(entries[0] === 'bmad-bmm-create-story', 'correct artifact was written', `got: ${entries[0]}`);
await cleanTmpDir();
}
// --- Writes multiple artifacts of the same type ---
{
const destDir = await createTmpDir();
const artifacts = [
{
type: 'workflow-command',
relativePath: 'bmm/workflows/plan-project.md',
content: '---\ndescription: Plan\n---\n\nPlan body.',
},
{
type: 'workflow-command',
relativePath: 'core/workflows/review.md',
content: '---\ndescription: Review\n---\n\nReview body.',
},
];
const count = await setup.writeSkillArtifacts(destDir, artifacts, 'workflow-command');
assert(count === 2, 'two artifacts written');
const entries = new Set((await fs.readdir(destDir)).sort());
assert(entries.has('bmad-bmm-plan-project'), 'first skill directory exists');
assert(entries.has('bmad-review'), 'second skill directory exists (core module)');
await cleanTmpDir();
}
// --- Returns 0 when no artifacts match type ---
{
const destDir = await createTmpDir();
const artifacts = [
{
type: 'agent-launcher',
relativePath: 'bmm/agents/pm.md',
content: '---\ndescription: An agent\n---\n\nBody.',
},
];
const count = await setup.writeSkillArtifacts(destDir, artifacts, 'task');
assert(count === 0, 'returns 0 when no types match');
const entries = await fs.readdir(destDir);
assert(entries.length === 0, 'no directories created when no types match');
await cleanTmpDir();
}
// --- Handles empty artifacts array ---
{
const destDir = await createTmpDir();
const count = await setup.writeSkillArtifacts(destDir, [], 'task');
assert(count === 0, 'returns 0 for empty artifacts array');
await cleanTmpDir();
}
// --- Artifacts without type field are always written ---
{
const destDir = await createTmpDir();
const artifacts = [
{
relativePath: 'bmm/tasks/no-type.md',
content: '---\ndescription: No type field\n---\n\nBody.',
},
];
const count = await setup.writeSkillArtifacts(destDir, artifacts, 'task');
assert(count === 1, 'artifact without type field is written (no filtering)');
await cleanTmpDir();
}
// --- Content without frontmatter gets minimal frontmatter added ---
{
const destDir = await createTmpDir();
const artifacts = [
{
type: 'task',
relativePath: 'bmm/tasks/bare.md',
content: 'Just plain content, no frontmatter.',
},
];
const count = await setup.writeSkillArtifacts(destDir, artifacts, 'task');
assert(count === 1, 'bare content artifact written');
const skillFile = path.join(destDir, 'bmad-bmm-bare', 'SKILL.md');
const content = await fs.readFile(skillFile, 'utf8');
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
assert(fmMatch !== null, 'frontmatter added to bare content');
const parsed = yaml.parse(fmMatch[1]);
assert(parsed.name === 'bmad-bmm-bare', 'name set for bare content', `got: ${parsed.name}`);
assert(content.includes('Just plain content, no frontmatter.'), 'original content preserved');
await cleanTmpDir();
}
// --- Summary ---
console.log(`\n${passed} passed, ${failed} failed\n`);
process.exit(failed > 0 ? 1 : 0);
}
runTests().catch((error) => {
console.error('Test runner error:', error);
process.exit(1);
});

View File

@ -1378,8 +1378,11 @@ class Installer {
lines.push(
'',
' Next steps:',
` Docs: ${color.dim('https://docs.bmad-method.org/')}`,
` Run ${color.cyan('/bmad-help')} in your IDE to get started`,
` Read our new Docs Site: ${color.dim('https://docs.bmad-method.org/')}`,
` Join our Discord: ${color.dim('https://discord.gg/gk8jAdXWmj')}`,
` Star us on GitHub: ${color.dim('https://github.com/bmad-code-org/BMAD-METHOD/')}`,
` Subscribe on YouTube: ${color.dim('https://www.youtube.com/@BMadCode')}`,
` Run ${color.cyan('/bmad-help')} with your IDE Agent and ask it how to get started`,
);
await prompts.note(lines.join('\n'), 'BMAD is ready to use!');

View File

@ -7,10 +7,13 @@ 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 yaml = require('yaml');
const prompts = require('../../../lib/prompts');
/**
* Codex setup handler (CLI mode)
* Writes BMAD artifacts as Agent Skills (agentskills.io format)
* into .agents/skills/ directories.
*/
class CodexSetup extends BaseIdeSetup {
constructor() {
@ -23,35 +26,35 @@ class CodexSetup extends BaseIdeSetup {
* @returns {Object} Collected configuration
*/
async collectConfiguration(options = {}) {
// Non-interactive mode: use default (global)
// Non-interactive mode: use default (project)
if (options.skipPrompts) {
return { installLocation: 'global' };
return { installLocation: 'project' };
}
let confirmed = false;
let installLocation = 'global';
let installLocation = 'project';
while (!confirmed) {
installLocation = await prompts.select({
message: 'Where would you like to install Codex CLI prompts?',
message: 'Where would you like to install Codex CLI skills?',
choices: [
{
name: 'Global - Simple for single project ' + '(~/.codex/prompts, but references THIS project only)',
value: 'global',
},
{
name: `Project-specific - Recommended for real work (requires CODEX_HOME=<project-dir>${path.sep}.codex)`,
name: 'Project-specific - Recommended (<project>/.agents/skills)',
value: 'project',
},
{
name: 'Global - ($HOME/.agents/skills)',
value: 'global',
},
],
default: 'global',
default: 'project',
});
// Show brief confirmation hint (detailed instructions available via verbose)
if (installLocation === 'project') {
await prompts.log.info('Prompts installed to: <project>/.codex/prompts (requires CODEX_HOME)');
await prompts.log.info('Skills installed to: <project>/.agents/skills');
} else {
await prompts.log.info('Prompts installed to: ~/.codex/prompts');
await prompts.log.info('Skills installed to: $HOME/.agents/skills');
}
// Confirm the choice
@ -80,20 +83,21 @@ class CodexSetup extends BaseIdeSetup {
// Always use CLI mode
const mode = 'cli';
// Get installation location from pre-collected config or default to global
const installLocation = options.preCollectedConfig?.installLocation || 'global';
// Get installation location from pre-collected config or default to project
const installLocation = options.preCollectedConfig?.installLocation || 'project';
const { artifacts, counts } = await this.collectClaudeArtifacts(projectDir, bmadDir, options);
const destDir = this.getCodexPromptDir(projectDir, installLocation);
const destDir = this.getCodexSkillsDir(projectDir, installLocation);
await fs.ensureDir(destDir);
await this.clearOldBmadFiles(destDir, options);
await this.clearOldBmadSkills(destDir, options);
// Collect artifacts and write using underscore format
// Collect and write agent skills
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
const agentCount = await agentGen.writeDashArtifacts(destDir, agentArtifacts);
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) {
@ -117,19 +121,23 @@ class CodexSetup extends BaseIdeSetup {
});
}
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 workflowGenerator.writeDashArtifacts(destDir, workflowArtifacts);
// Also write tasks using underscore format
const ttGen = new TaskToolCommandGenerator(this.bmadFolderName);
const tasksWritten = await ttGen.writeDashArtifacts(destDir, taskArtifacts);
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} files → ${destDir}`,
`${this.name} configured: ${counts.agents} agents, ${counts.workflows} workflows, ${counts.tasks} tasks, ${written} skills → ${destDir}`,
);
}
@ -145,13 +153,82 @@ class CodexSetup extends BaseIdeSetup {
}
/**
* Detect Codex installation by checking for BMAD prompt exports
* 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
await fs.writeFile(path.join(skillDir, 'SKILL.md'), skillContent);
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) {
// 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 }).trimEnd();
return `---\n${newFrontmatter}\n---\n${body}`;
}
/**
* Detect Codex installation by checking for BMAD skills
*/
async detect(projectDir) {
// Check both global and project-specific locations
const globalDir = this.getCodexPromptDir(null, 'global');
const globalDir = this.getCodexSkillsDir(null, 'global');
const projectDir_local = projectDir || process.cwd();
const projectSpecificDir = this.getCodexPromptDir(projectDir_local, 'project');
const projectSpecificDir = this.getCodexSkillsDir(projectDir_local, 'project');
// Check global location
if (await fs.pathExists(globalDir)) {
@ -240,27 +317,20 @@ class CodexSetup extends BaseIdeSetup {
};
}
getCodexPromptDir(projectDir = null, location = 'global') {
getCodexSkillsDir(projectDir = null, location = 'project') {
if (location === 'project' && projectDir) {
return path.join(projectDir, '.codex', 'prompts');
return path.join(projectDir, '.agents', 'skills');
}
return path.join(os.homedir(), '.codex', 'prompts');
if (location === 'project' && !projectDir) {
throw new Error('projectDir is required for project-scoped skill installation');
}
return path.join(os.homedir(), '.agents', 'skills');
}
async flattenAndWriteArtifacts(artifacts, destDir) {
let written = 0;
for (const artifact of artifacts) {
const flattenedName = this.flattenFilename(artifact.relativePath);
const targetPath = path.join(destDir, flattenedName);
await fs.writeFile(targetPath, artifact.content);
written++;
}
return written;
}
async clearOldBmadFiles(destDir, options = {}) {
/**
* Remove existing BMAD skill directories from the skills directory.
*/
async clearOldBmadSkills(destDir, options = {}) {
if (!(await fs.pathExists(destDir))) {
return;
}
@ -299,7 +369,8 @@ class CodexSetup extends BaseIdeSetup {
}
async readAndProcessWithProject(filePath, metadata, projectDir) {
const content = await fs.readFile(filePath, 'utf8');
const rawContent = await fs.readFile(filePath, 'utf8');
const content = rawContent.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
return super.processContent(content, metadata, projectDir);
}
@ -311,14 +382,14 @@ class CodexSetup extends BaseIdeSetup {
const lines = [
'IMPORTANT: Codex Configuration',
'',
'/prompts installed globally to your HOME DIRECTORY.',
'Skills installed globally to your HOME DIRECTORY ($HOME/.agents/skills).',
'',
'These prompts reference a specific _bmad path.',
'These skills reference a specific _bmad path.',
"To use with other projects, you'd need to copy the _bmad dir.",
'',
'You can now use /commands in Codex CLI',
' Example: /bmad_bmm_pm',
' Type / to see all available commands',
'Skills are available in Codex CLI automatically.',
' Use /skills to see available skills',
' Skills can also be invoked implicitly based on task description',
];
return lines.join('\n');
}
@ -330,40 +401,15 @@ class CodexSetup extends BaseIdeSetup {
* @returns {string} Instructions text
*/
getProjectSpecificInstructions(projectDir = null, destDir = null) {
const isWindows = os.platform() === 'win32';
const commonLines = [
const lines = [
'Project-Specific Codex Configuration',
'',
`Prompts will be installed to: ${destDir || '<project>/.codex/prompts'}`,
'',
'REQUIRED: You must set CODEX_HOME to use these prompts',
`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.',
];
const windowsLines = [
'Create a codex.cmd file in your project root:',
'',
' @echo off',
' set CODEX_HOME=%~dp0.codex',
' codex %*',
'',
String.raw`Then run: .\codex instead of codex`,
'(The %~dp0 gets the directory of the .cmd file)',
];
const unixLines = [
'Add this alias to your ~/.bashrc or ~/.zshrc:',
'',
' alias codex=\'CODEX_HOME="$PWD/.codex" codex\'',
'',
'After adding, run: source ~/.bashrc (or source ~/.zshrc)',
'(The $PWD uses your current working directory)',
];
const closingLines = ['', 'This tells Codex CLI to use prompts from this project instead of ~/.codex'];
const lines = [...commonLines, ...(isWindows ? windowsLines : unixLines), ...closingLines];
return lines.join('\n');
}
@ -372,31 +418,34 @@ class CodexSetup extends BaseIdeSetup {
*/
async cleanup(projectDir = null) {
// Clean both global and project-specific locations
const globalDir = this.getCodexPromptDir(null, 'global');
await this.clearOldBmadFiles(globalDir);
const globalDir = this.getCodexSkillsDir(null, 'global');
await this.clearOldBmadSkills(globalDir);
if (projectDir) {
const projectSpecificDir = this.getCodexPromptDir(projectDir, 'project');
await this.clearOldBmadFiles(projectSpecificDir);
const projectSpecificDir = this.getCodexSkillsDir(projectDir, 'project');
await this.clearOldBmadSkills(projectSpecificDir);
}
}
/**
* Install a custom agent launcher for Codex
* @param {string} projectDir - Project directory (not used, Codex installs to home)
* 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 command
* @returns {Object|null} Info about created skill
*/
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
const destDir = this.getCodexPromptDir(projectDir, 'project');
await fs.ensureDir(destDir);
const destDir = this.getCodexSkillsDir(projectDir, 'project');
const launcherContent = `---
name: '${agentName}'
description: '${agentName} agent'
disable-model-invocation: true
// 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 fm = yaml.stringify({ name: skillName, description: `${agentName} agent` }).trimEnd();
const skillContent = `---
${fm}
---
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
@ -411,14 +460,12 @@ You must fully embody this agent's persona and follow all activation instruction
</agent-activation>
`;
// Use underscore format: bmad_custom_fred-commit-poet.md
const fileName = customAgentDashName(agentName);
const launcherPath = path.join(destDir, fileName);
await fs.writeFile(launcherPath, launcherContent, 'utf8');
const skillPath = path.join(skillDir, 'SKILL.md');
await fs.writeFile(skillPath, skillContent, 'utf8');
return {
path: path.relative(projectDir, launcherPath),
command: `/${fileName.replace('.md', '')}`,
path: path.relative(projectDir, skillPath),
command: `$${skillName}`,
};
}
}