Compare commits

...

9 Commits

Author SHA1 Message Date
Alex Verkhovsky 25f62c74a1
Merge 2515e81373 into 6bfc937bd3 2026-02-25 21:17:55 +00:00
Alex Verkhovsky 2515e81373 merge: resolve conflict with upstream main
Keep findAncestorConflict() with fs.realpath fix and adopt upstream's
updated JSDoc for removeEmptyParents().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 14:16:34 -07:00
Alex Verkhovsky 1c789c05e2 fix(installer): resolve symlinks before ancestor conflict walk
Use fs.realpath() instead of path.resolve() so the ancestor directory
walk follows the physical filesystem path, not the logical symlink path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 12:39:59 -07:00
Davor Racic 6bfc937bd3
fix(installer): OpenCode integration: replace `name` frontmatter with `mode: all` and update directory names (#1764)
* fix(opencode): use mode: all in agent template, remove name frontmatter, fix directory names

- Replace name: '{{name}}' with mode: all in opencode-agent.md
  mode: all enables both Tab-key agent switching in the TUI and @subagent
  invocation via the Task tool (mode: primary blocked subagent use)
- Remove name: '{{name}}' from opencode-task/tool/workflow/workflow-yaml templates
  OpenCode derives command name from filename, not from a name frontmatter field;
  the bare {{name}} value was overriding the bmad- prefixed filename causing
  name collisions with built-in OpenCode commands (fixes #1762)
- Fix deprecated singular directory names in platform-codes.yaml:
  .opencode/agent -> .opencode/agents, .opencode/command -> .opencode/commands
- Add legacy_targets migration: cleanup() now removes stale bmad-* files from
  old singular directories on reinstall so existing users don't get duplicates
- Fix removeEmptyParents to continue walking up to parent when starting dir is
  already absent instead of breaking early

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(opencode): address code review findings for cleanup and schema docs

- Add project boundary guard to removeEmptyParents() using path.resolve
  and startsWith check to prevent traversal outside projectDir (Augment)
- Fix JSDoc: "Recursively remove" -> "Walk up ancestor directories"
- Add user-visible migration log message when processing legacy_targets
- Document legacy_targets field in Installer Config Schema comment block
  in platform-codes.yaml (CodeRabbit + Augment)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(opencode): improve removeEmptyParents error handling and loop clarity

- Distinguish recoverable errors (ENOTEMPTY, ENOENT) from fatal errors in
  removeEmptyParents() catch block — skip level and continue upward on
  TOCTOU races or concurrent removal, break only on fatal errors (EACCES)
- Add comment clarifying loop invariant for missing-path continue branch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Brian <bmadcode@gmail.com>
2026-02-25 11:12:05 -06:00
Alex Verkhovsky 72a9325a40
docs: rebrand BMAD acronym to Build More Architect Dreams (#1765)
Update the BMAD acronym expansion from "Breakthrough Method of
Agile AI Driven Development" to "Build More Architect Dreams"
across README, docs homepage, and package.json description.

Co-authored-by: Brian <bmadcode@gmail.com>
2026-02-25 11:11:24 -06:00
Alex Verkhovsky 886a070d2b Address code review feedback from CodeRabbit and Augment
- Move "Setting up..." log after conflict check so it only shows when
  install will proceed
- Fix rm command: add -rf flags and correct quoting for glob outside quotes
- Improve error wording: "ancestor installation" instead of misleading
  "ancestor directory"
- Use case-insensitive startsWith for bmad file detection (macOS/Windows)
- Document ancestor_conflict_check in the installer config schema

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 19:34:39 -07:00
Brian Madison 5ed436cda0 6.0.3 2026-02-24 18:47:36 -06:00
Alex Verkhovsky db0c06f9dc
Merge branch 'main' into fix/installer-ancestor-conflict 2026-02-23 17:02:59 -07:00
Alex Verkhovsky 48b3bb1709 fix(installer): refuse install when ancestor dir has BMAD commands
Claude Code inherits slash commands from parent directories, so
installing into a nested project when a parent already has .claude/commands
with bmad-* files causes duplicate entries in the autocomplete.

Add ancestor_conflict_check flag (enabled for claude-code) that walks
up the directory tree before install. If BMAD files are found in an
ancestor target_dir, the installer refuses with an actionable error.

Also fix IdeManager.setup() to propagate handler success status instead
of unconditionally returning success: true.
2026-02-22 14:16:07 -07:00
12 changed files with 100 additions and 18 deletions

View File

@ -5,7 +5,7 @@
[![Node.js Version](https://img.shields.io/badge/node-%3E%3D20.0.0-brightgreen)](https://nodejs.org) [![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) [![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 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. **Build More Architect Dreams** — 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 for a gated community or courses. **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.

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 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 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. The BMad Method (**B**uild **M**ore **A**rchitect **D**reams) 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. If you're comfortable working with AI coding assistants like Claude, Cursor, or GitHub Copilot, you're ready to get started.

4
package-lock.json generated
View File

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

View File

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

View File

@ -34,6 +34,25 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
* @returns {Promise<Object>} Setup result * @returns {Promise<Object>} Setup result
*/ */
async setup(projectDir, bmadDir, options = {}) { async setup(projectDir, bmadDir, options = {}) {
// Check for BMAD files in ancestor directories that would cause duplicates
if (this.installerConfig?.ancestor_conflict_check) {
const conflict = await this.findAncestorConflict(projectDir);
if (conflict) {
await prompts.log.error(
`Found existing BMAD commands in ancestor installation: ${conflict}\n` +
` ${this.name} inherits commands from parent directories, so this would cause duplicates.\n` +
` Please remove the BMAD files from that directory first:\n` +
` rm -rf "${conflict}"/bmad*`,
);
return {
success: false,
reason: 'ancestor-conflict',
error: `Ancestor conflict: ${conflict}`,
conflictDir: conflict,
};
}
}
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
// Clean up any old BMAD installation first // Clean up any old BMAD installation first
@ -453,6 +472,15 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
* @param {string} projectDir - Project directory * @param {string} projectDir - Project directory
*/ */
async cleanup(projectDir, options = {}) { async cleanup(projectDir, options = {}) {
// Migrate legacy target directories (e.g. .opencode/agent → .opencode/agents)
if (this.installerConfig?.legacy_targets) {
if (!options.silent) await prompts.log.message(' Migrating legacy directories...');
for (const legacyDir of this.installerConfig.legacy_targets) {
await this.cleanupTarget(projectDir, legacyDir, options);
await this.removeEmptyParents(projectDir, legacyDir);
}
}
// Clean all target directories // Clean all target directories
if (this.installerConfig?.targets) { if (this.installerConfig?.targets) {
const parentDirs = new Set(); const parentDirs = new Set();
@ -532,24 +560,71 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
} }
} }
/** /**
* Recursively remove empty directories walking up from dir toward projectDir * Check ancestor directories for existing BMAD files in the same target_dir.
* IDEs like Claude Code inherit commands from parent directories, so an existing
* installation in an ancestor would cause duplicate commands.
* @param {string} projectDir - Project directory being installed to
* @returns {Promise<string|null>} Path to conflicting directory, or null if clean
*/
async findAncestorConflict(projectDir) {
const targetDir = this.installerConfig?.target_dir;
if (!targetDir) return null;
const resolvedProject = await fs.realpath(path.resolve(projectDir));
let current = path.dirname(resolvedProject);
const root = path.parse(current).root;
while (current !== root && current.length > root.length) {
const candidatePath = path.join(current, targetDir);
try {
if (await fs.pathExists(candidatePath)) {
const entries = await fs.readdir(candidatePath);
const hasBmad = entries.some((e) => typeof e === 'string' && e.toLowerCase().startsWith('bmad'));
if (hasBmad) {
return candidatePath;
}
}
} catch {
// Can't read directory — skip
}
current = path.dirname(current);
}
return null;
}
/**
* Walk up ancestor directories from relativeDir toward projectDir, removing each if empty
* Stops at projectDir boundary never removes projectDir itself * Stops at projectDir boundary never removes projectDir itself
* @param {string} projectDir - Project root (boundary) * @param {string} projectDir - Project root (boundary)
* @param {string} relativeDir - Relative directory to start from * @param {string} relativeDir - Relative directory to start from
*/ */
async removeEmptyParents(projectDir, relativeDir) { async removeEmptyParents(projectDir, relativeDir) {
const resolvedProject = path.resolve(projectDir);
let current = relativeDir; let current = relativeDir;
let last = null; let last = null;
while (current && current !== '.' && current !== last) { while (current && current !== '.' && current !== last) {
last = current; last = current;
const fullPath = path.join(projectDir, current); const fullPath = path.resolve(projectDir, current);
// Boundary guard: never traverse outside projectDir
if (!fullPath.startsWith(resolvedProject + path.sep) && fullPath !== resolvedProject) break;
try { try {
if (!(await fs.pathExists(fullPath))) break; if (!(await fs.pathExists(fullPath))) {
// Dir already gone — advance current; last is reset at top of next iteration
current = path.dirname(current);
continue;
}
const remaining = await fs.readdir(fullPath); const remaining = await fs.readdir(fullPath);
if (remaining.length > 0) break; if (remaining.length > 0) break;
await fs.rmdir(fullPath); await fs.rmdir(fullPath);
} catch { } catch (error) {
break; // ENOTEMPTY: TOCTOU race (file added between readdir and rmdir) — skip level, continue upward
// ENOENT: dir removed by another process between pathExists and rmdir — skip level, continue upward
if (error.code === 'ENOTEMPTY' || error.code === 'ENOENT') {
current = path.dirname(current);
continue;
}
break; // fatal error (e.g. EACCES) — stop upward walk
} }
current = path.dirname(current); current = path.dirname(current);
} }

View File

@ -206,7 +206,9 @@ class IdeManager {
if (handlerResult.tools > 0) parts.push(`${handlerResult.tools} tools`); if (handlerResult.tools > 0) parts.push(`${handlerResult.tools} tools`);
detail = parts.join(', '); detail = parts.join(', ');
} }
return { success: true, ide: ideName, detail, handlerResult }; // Propagate handler's success status (default true for backward compat)
const success = handlerResult?.success !== false;
return { success, ide: ideName, detail, error: handlerResult?.error, handlerResult };
} catch (error) { } catch (error) {
await prompts.log.error(`Failed to setup ${ideName}: ${error.message}`); await prompts.log.error(`Failed to setup ${ideName}: ${error.message}`);
return { success: false, ide: ideName, error: error.message }; return { success: false, ide: ideName, error: error.message };

View File

@ -40,6 +40,7 @@ platforms:
installer: installer:
target_dir: .claude/commands target_dir: .claude/commands
template_type: default template_type: default
ancestor_conflict_check: true
cline: cline:
name: "Cline" name: "Cline"
@ -131,11 +132,14 @@ platforms:
category: ide category: ide
description: "OpenCode terminal coding assistant" description: "OpenCode terminal coding assistant"
installer: installer:
legacy_targets:
- .opencode/agent
- .opencode/command
targets: targets:
- target_dir: .opencode/agent - target_dir: .opencode/agents
template_type: opencode template_type: opencode
artifact_types: [agents] artifact_types: [agents]
- target_dir: .opencode/command - target_dir: .opencode/commands
template_type: opencode template_type: opencode
artifact_types: [workflows, tasks, tools] artifact_types: [workflows, tasks, tools]
@ -191,12 +195,17 @@ platforms:
# template_type: string # Default template type to use # template_type: string # Default template type to use
# header_template: string (optional) # Override for header/frontmatter template # header_template: string (optional) # Override for header/frontmatter template
# body_template: string (optional) # Override for body/content template # body_template: string (optional) # Override for body/content template
# legacy_targets: array (optional) # Old target dirs to clean up on reinstall (migration)
# - string # Relative path, e.g. .opencode/agent
# targets: array (optional) # For multi-target installations # targets: array (optional) # For multi-target installations
# - target_dir: string # - target_dir: string
# template_type: string # template_type: string
# 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)
# ancestor_conflict_check: boolean (optional) # Refuse install when ancestor dir has BMAD files
# # in the same target_dir (for IDEs that inherit
# # commands from parent directories)
# ============================================================================ # ============================================================================
# Platform Categories # Platform Categories

View File

@ -1,5 +1,5 @@
--- ---
name: '{{name}}' mode: all
description: '{{description}}' description: '{{description}}'
--- ---

View File

@ -1,5 +1,4 @@
--- ---
name: '{{name}}'
description: '{{description}}' description: '{{description}}'
--- ---

View File

@ -1,5 +1,4 @@
--- ---
name: '{{name}}'
description: '{{description}}' description: '{{description}}'
--- ---

View File

@ -1,5 +1,4 @@
--- ---
name: '{{name}}'
description: '{{description}}' description: '{{description}}'
--- ---

View File

@ -1,5 +1,4 @@
--- ---
name: '{{name}}'
description: '{{description}}' description: '{{description}}'
--- ---