Merge branch 'main' into main

This commit is contained in:
Brian 2025-09-06 13:46:00 -05:00 committed by GitHub
commit 7371ce9271
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 242 additions and 97 deletions

4
.gitignore vendored
View File

@ -29,13 +29,15 @@ Thumbs.db
# IDE and editor configs # IDE and editor configs
.windsurf/ .windsurf/
.trae/ .trae/
.bmad*/.cursor/ .bmad*/
.cursor/
# AI assistant files # AI assistant files
CLAUDE.md CLAUDE.md
.ai/* .ai/*
.claude .claude
.gemini .gemini
.iflow
# Project-specific # Project-specific
.bmad-core .bmad-core

View File

@ -119,7 +119,7 @@ The BMAD-METHOD™ includes a powerful codebase flattener tool designed to prepa
### Features ### Features
- **AI-Optimized Output**: Generates clean XML format specifically designed for AI model consumption - **AI-Optimized Output**: Generates clean XML format specifically designed for AI model consumption
- **Smart Filtering**: Automatically respects `.gitignore` patterns to exclude unnecessary files - **Smart Filtering**: Automatically respects `.gitignore` patterns to exclude unnecessary files, plus optional project-level `.bmad-flattenignore` for additional exclusions
- **Binary File Detection**: Intelligently identifies and excludes binary files, focusing on source code - **Binary File Detection**: Intelligently identifies and excludes binary files, focusing on source code
- **Progress Tracking**: Real-time progress indicators and comprehensive completion statistics - **Progress Tracking**: Real-time progress indicators and comprehensive completion statistics
- **Flexible Output**: Customizable output file location and naming - **Flexible Output**: Customizable output file location and naming
@ -170,6 +170,18 @@ The generated XML file contains your project's text-based source files in a stru
- File discovery and ignoring - File discovery and ignoring
- Uses `git ls-files` when inside a git repository for speed and correctness; otherwise falls back to a glob-based scan. - Uses `git ls-files` when inside a git repository for speed and correctness; otherwise falls back to a glob-based scan.
- Applies your `.gitignore` plus a curated set of default ignore patterns (e.g., `node_modules`, build outputs, caches, logs, IDE folders, lockfiles, large media/binaries, `.env*`, and previously generated XML outputs). - Applies your `.gitignore` plus a curated set of default ignore patterns (e.g., `node_modules`, build outputs, caches, logs, IDE folders, lockfiles, large media/binaries, `.env*`, and previously generated XML outputs).
- Supports an optional `.bmad-flattenignore` file at the project root for additional ignore patterns (gitignore-style). If present, its rules are applied after `.gitignore` and the defaults.
##### `.bmad-flattenignore` example
Create a `.bmad-flattenignore` file in the root of your project to exclude files that must remain in git but should not be included in the flattened XML:
```text
seeds/**
scripts/private/**
**/*.snap
```
- Binary handling - Binary handling
- Binary files are detected and excluded from the XML content. They are counted in the final summary but not embedded in the output. - Binary files are detected and excluded from the XML content. They are counted in the final summary but not embedded in the output.
- XML format and safety - XML format and safety

View File

@ -21,7 +21,7 @@ To comprehensively validate a story draft before implementation begins, ensuring
### 1. Template Completeness Validation ### 1. Template Completeness Validation
- Load `bmad-core/templates/story-tmpl.md` and extract all section headings from the template - Load `.bmad-core/templates/story-tmpl.yaml` and extract all section headings from the template
- **Missing sections check**: Compare story sections against template sections to verify all required sections are present - **Missing sections check**: Compare story sections against template sections to verify all required sections are present
- **Placeholder validation**: Ensure no template placeholders remain unfilled (e.g., `{{EpicNum}}`, `{{role}}`, `_TBD_`) - **Placeholder validation**: Ensure no template placeholders remain unfilled (e.g., `{{EpicNum}}`, `{{role}}`, `_TBD_`)
- **Agent section verification**: Confirm all sections from template exist for future agent use - **Agent section verification**: Confirm all sections from template exist for future agent use

View File

@ -5,7 +5,13 @@
> Gemini Web's 1M+ token context window or Gemini CLI (when it's working) can analyze your ENTIRE codebase, or critical sections of it, all at once (obviously within reason): > Gemini Web's 1M+ token context window or Gemini CLI (when it's working) can analyze your ENTIRE codebase, or critical sections of it, all at once (obviously within reason):
> >
> - Upload via GitHub URL or use gemini cli in the project folder > - Upload via GitHub URL or use gemini cli in the project folder
> - If working in the web: use `npx bmad-method flatten` to flatten your project into a single file, then upload that file to your web agent. > - If working in the web: use `npx bmad-method flatten` to flatten your project into a single file, then upload that file to your web agent. To exclude additional files that must remain in git but shouldn't be sent to the AI, add a `.bmad-flattenignore` file at the project root (gitignore-style), e.g.:
>
> ```text
> seeds/**
> scripts/private/**
> **/*.snap
> ```
## What is Brownfield Development? ## What is Brownfield Development?

View File

@ -154,9 +154,11 @@ async function parseGitignore(gitignorePath) {
async function loadIgnore(rootDir, extraPatterns = []) { async function loadIgnore(rootDir, extraPatterns = []) {
const ig = ignore(); const ig = ignore();
const gitignorePath = path.join(rootDir, '.gitignore'); const gitignorePath = path.join(rootDir, '.gitignore');
const flattenIgnorePath = path.join(rootDir, '.bmad-flattenignore');
const patterns = [ const patterns = [
...(await readIgnoreFile(gitignorePath)), ...(await readIgnoreFile(gitignorePath)),
...DEFAULT_PATTERNS, ...DEFAULT_PATTERNS,
...(await readIgnoreFile(flattenIgnorePath)),
...extraPatterns, ...extraPatterns,
]; ];
// De-duplicate // De-duplicate

View File

@ -49,7 +49,7 @@ program
.option('-d, --directory <path>', 'Installation directory') .option('-d, --directory <path>', 'Installation directory')
.option( .option(
'-i, --ide <ide...>', '-i, --ide <ide...>',
'Configure for specific IDE(s) - can specify multiple (cursor, claude-code, windsurf, trae, roo, kilo, cline, gemini, qwen-code, github-copilot, codex, codex-web, auggie-cli, other)', 'Configure for specific IDE(s) - can specify multiple (cursor, claude-code, windsurf, trae, roo, kilo, cline, gemini, qwen-code, github-copilot, codex, codex-web, auggie-cli, iflow-cli, other)',
) )
.option( .option(
'-e, --expansion-packs <packs...>', '-e, --expansion-packs <packs...>',
@ -397,6 +397,7 @@ async function promptInstallation() {
choices: [ choices: [
{ name: 'Cursor', value: 'cursor' }, { name: 'Cursor', value: 'cursor' },
{ name: 'Claude Code', value: 'claude-code' }, { name: 'Claude Code', value: 'claude-code' },
{ name: 'iFlow CLI', value: 'iflow-cli' },
{ name: 'Windsurf', value: 'windsurf' }, { name: 'Windsurf', value: 'windsurf' },
{ name: 'Trae', value: 'trae' }, // { name: 'Trae', value: 'trae'} { name: 'Trae', value: 'trae' }, // { name: 'Trae', value: 'trae'}
{ name: 'Roo Code', value: 'roo' }, { name: 'Roo Code', value: 'roo' },

View File

@ -28,6 +28,15 @@ ide-configurations:
# To use BMad agents in Claude Code: # To use BMad agents in Claude Code:
# 1. Type /agent-name (e.g., "/dev", "/pm", "/architect") # 1. Type /agent-name (e.g., "/dev", "/pm", "/architect")
# 2. Claude will switch to that agent's persona # 2. Claude will switch to that agent's persona
iflow-cli:
name: iFlow CLI
rule-dir: .iflow/commands/BMad/
format: multi-file
command-suffix: .md
instructions: |
# To use BMad agents in iFlow CLI:
# 1. Type /agent-name (e.g., "/dev", "/pm", "/architect")
# 2. iFlow will switch to that agent's persona
crush: crush:
name: Crush name: Crush
rule-dir: .crush/commands/BMad/ rule-dir: .crush/commands/BMad/
@ -78,15 +87,15 @@ ide-configurations:
# 4. Rules are stored in .clinerules/ directory in your project # 4. Rules are stored in .clinerules/ directory in your project
gemini: gemini:
name: Gemini CLI name: Gemini CLI
rule-dir: .gemini/bmad-method/ rule-dir: .gemini/commands/BMad/
format: single-file format: multi-file
command-suffix: .md command-suffix: .toml
instructions: | instructions: |
# To use BMad agents with the Gemini CLI: # To use BMad agents with the Gemini CLI:
# 1. The installer creates a .gemini/bmad-method/ directory in your project. # 1. The installer creates a `BMad` folder in `.gemini/commands`.
# 2. It concatenates all agent files into a single GEMINI.md file. # 2. This adds custom commands for each agent and task.
# 3. Simply mention the agent in your prompt (e.g., "As *dev, ..."). # 3. Type /BMad:agents:<agent-name> (e.g., "/BMad:agents:dev", "/BMad:agents:pm") or /BMad:tasks:<task-name> (e.g., "/BMad:tasks:create-doc").
# 4. The Gemini CLI will automatically have the context for that agent. # 4. The agent will adopt that persona for the conversation or preform the task.
github-copilot: github-copilot:
name: Github Copilot name: Github Copilot
rule-dir: .github/chatmodes/ rule-dir: .github/chatmodes/

View File

@ -47,6 +47,9 @@ class IdeSetup extends BaseIdeSetup {
case 'claude-code': { case 'claude-code': {
return this.setupClaudeCode(installDir, selectedAgent); return this.setupClaudeCode(installDir, selectedAgent);
} }
case 'iflow-cli': {
return this.setupIFlowCli(installDir, selectedAgent);
}
case 'crush': { case 'crush': {
return this.setupCrush(installDir, selectedAgent); return this.setupCrush(installDir, selectedAgent);
} }
@ -453,6 +456,134 @@ class IdeSetup extends BaseIdeSetup {
console.log(chalk.dim(` - Tasks in: ${tasksDir}`)); console.log(chalk.dim(` - Tasks in: ${tasksDir}`));
} }
async setupIFlowCli(installDir, selectedAgent) {
// Setup bmad-core commands
const coreSlashPrefix = await this.getCoreSlashPrefix(installDir);
const coreAgents = selectedAgent ? [selectedAgent] : await this.getCoreAgentIds(installDir);
const coreTasks = await this.getCoreTaskIds(installDir);
await this.setupIFlowCliForPackage(
installDir,
'core',
coreSlashPrefix,
coreAgents,
coreTasks,
'.bmad-core',
);
// Setup expansion pack commands
const expansionPacks = await this.getInstalledExpansionPacks(installDir);
for (const packInfo of expansionPacks) {
const packSlashPrefix = await this.getExpansionPackSlashPrefix(packInfo.path);
const packAgents = await this.getExpansionPackAgents(packInfo.path);
const packTasks = await this.getExpansionPackTasks(packInfo.path);
if (packAgents.length > 0 || packTasks.length > 0) {
// Use the actual directory name where the expansion pack is installed
const rootPath = path.relative(installDir, packInfo.path);
await this.setupIFlowCliForPackage(
installDir,
packInfo.name,
packSlashPrefix,
packAgents,
packTasks,
rootPath,
);
}
}
return true;
}
async setupIFlowCliForPackage(installDir, packageName, slashPrefix, agentIds, taskIds, rootPath) {
const commandsBaseDir = path.join(installDir, '.iflow', 'commands', slashPrefix);
const agentsDir = path.join(commandsBaseDir, 'agents');
const tasksDir = path.join(commandsBaseDir, 'tasks');
// Ensure directories exist
await fileManager.ensureDirectory(agentsDir);
await fileManager.ensureDirectory(tasksDir);
// Setup agents
for (const agentId of agentIds) {
// Find the agent file - for expansion packs, prefer the expansion pack version
let agentPath;
if (packageName === 'core') {
// For core, use the normal search
agentPath = await this.findAgentPath(agentId, installDir);
} else {
// For expansion packs, first try to find the agent in the expansion pack directory
const expansionPackPath = path.join(installDir, rootPath, 'agents', `${agentId}.md`);
if (await fileManager.pathExists(expansionPackPath)) {
agentPath = expansionPackPath;
} else {
// Fall back to core if not found in expansion pack
agentPath = await this.findAgentPath(agentId, installDir);
}
}
const commandPath = path.join(agentsDir, `${agentId}.md`);
if (agentPath) {
// Create command file with agent content
let agentContent = await fileManager.readFile(agentPath);
// Replace {root} placeholder with the appropriate root path for this context
agentContent = agentContent.replaceAll('{root}', rootPath);
// Add command header
let commandContent = `# /${agentId} Command\n\n`;
commandContent += `When this command is used, adopt the following agent persona:\n\n`;
commandContent += agentContent;
await fileManager.writeFile(commandPath, commandContent);
console.log(chalk.green(`✓ Created agent command: /${agentId}`));
}
}
// Setup tasks
for (const taskId of taskIds) {
// Find the task file - for expansion packs, prefer the expansion pack version
let taskPath;
if (packageName === 'core') {
// For core, use the normal search
taskPath = await this.findTaskPath(taskId, installDir);
} else {
// For expansion packs, first try to find the task in the expansion pack directory
const expansionPackPath = path.join(installDir, rootPath, 'tasks', `${taskId}.md`);
if (await fileManager.pathExists(expansionPackPath)) {
taskPath = expansionPackPath;
} else {
// Fall back to core if not found in expansion pack
taskPath = await this.findTaskPath(taskId, installDir);
}
}
const commandPath = path.join(tasksDir, `${taskId}.md`);
if (taskPath) {
// Create command file with task content
let taskContent = await fileManager.readFile(taskPath);
// Replace {root} placeholder with the appropriate root path for this context
taskContent = taskContent.replaceAll('{root}', rootPath);
// Add command header
let commandContent = `# /${taskId} Task\n\n`;
commandContent += `When this command is used, execute the following task:\n\n`;
commandContent += taskContent;
await fileManager.writeFile(commandPath, commandContent);
console.log(chalk.green(`✓ Created task command: /${taskId}`));
}
}
console.log(
chalk.green(`\n✓ Created iFlow CLI commands for ${packageName} in ${commandsBaseDir}`),
);
console.log(chalk.dim(` - Agents in: ${agentsDir}`));
console.log(chalk.dim(` - Tasks in: ${tasksDir}`));
}
async setupCrushForPackage(installDir, packageName, slashPrefix, agentIds, taskIds, rootPath) { async setupCrushForPackage(installDir, packageName, slashPrefix, agentIds, taskIds, rootPath) {
const commandsBaseDir = path.join(installDir, '.crush', 'commands', slashPrefix); const commandsBaseDir = path.join(installDir, '.crush', 'commands', slashPrefix);
const agentsDir = path.join(commandsBaseDir, 'agents'); const agentsDir = path.join(commandsBaseDir, 'agents');
@ -690,6 +821,7 @@ class IdeSetup extends BaseIdeSetup {
async getCoreTaskIds(installDir) { async getCoreTaskIds(installDir) {
const allTaskIds = []; const allTaskIds = [];
const glob = require('glob');
// Check core tasks in .bmad-core or root only // Check core tasks in .bmad-core or root only
let tasksDir = path.join(installDir, '.bmad-core', 'tasks'); let tasksDir = path.join(installDir, '.bmad-core', 'tasks');
@ -698,7 +830,6 @@ class IdeSetup extends BaseIdeSetup {
} }
if (await fileManager.pathExists(tasksDir)) { if (await fileManager.pathExists(tasksDir)) {
const glob = require('glob');
const taskFiles = glob.sync('*.md', { cwd: tasksDir }); const taskFiles = glob.sync('*.md', { cwd: tasksDir });
allTaskIds.push(...taskFiles.map((file) => path.basename(file, '.md'))); allTaskIds.push(...taskFiles.map((file) => path.basename(file, '.md')));
} }
@ -706,6 +837,7 @@ class IdeSetup extends BaseIdeSetup {
// Check common tasks // Check common tasks
const commonTasksDir = path.join(installDir, 'common', 'tasks'); const commonTasksDir = path.join(installDir, 'common', 'tasks');
if (await fileManager.pathExists(commonTasksDir)) { if (await fileManager.pathExists(commonTasksDir)) {
const glob = require('glob');
const commonTaskFiles = glob.sync('*.md', { cwd: commonTasksDir }); const commonTaskFiles = glob.sync('*.md', { cwd: commonTasksDir });
allTaskIds.push(...commonTaskFiles.map((file) => path.basename(file, '.md'))); allTaskIds.push(...commonTaskFiles.map((file) => path.basename(file, '.md')));
} }
@ -1208,97 +1340,77 @@ class IdeSetup extends BaseIdeSetup {
return true; return true;
} }
async setupGeminiCli(installDir) { async setupGeminiCli(installDir, selectedAgent) {
const geminiDir = path.join(installDir, '.gemini'); const ideConfig = await configLoader.getIdeConfiguration('gemini');
const bmadMethodDir = path.join(geminiDir, 'bmad-method'); const bmadCommandsDir = path.join(installDir, ideConfig['rule-dir']);
await fileManager.ensureDirectory(bmadMethodDir);
// Update logic for existing settings.json const agentCommandsDir = path.join(bmadCommandsDir, 'agents');
const settingsPath = path.join(geminiDir, 'settings.json'); const taskCommandsDir = path.join(bmadCommandsDir, 'tasks');
if (await fileManager.pathExists(settingsPath)) { await fileManager.ensureDirectory(agentCommandsDir);
try { await fileManager.ensureDirectory(taskCommandsDir);
const settingsContent = await fileManager.readFile(settingsPath);
const settings = JSON.parse(settingsContent);
let updated = false;
// Handle contextFileName property
if (settings.contextFileName && Array.isArray(settings.contextFileName)) {
const originalLength = settings.contextFileName.length;
settings.contextFileName = settings.contextFileName.filter(
(fileName) => !fileName.startsWith('agents/'),
);
if (settings.contextFileName.length !== originalLength) {
updated = true;
}
}
if (updated) {
await fileManager.writeFile(settingsPath, JSON.stringify(settings, null, 2));
console.log(
chalk.green('✓ Updated .gemini/settings.json - removed agent file references'),
);
}
} catch (error) {
console.warn(chalk.yellow('Could not update .gemini/settings.json'), error);
}
}
// Remove old agents directory
const agentsDir = path.join(geminiDir, 'agents');
if (await fileManager.pathExists(agentsDir)) {
await fileManager.removeDirectory(agentsDir);
console.log(chalk.green('✓ Removed old .gemini/agents directory'));
}
// Get all available agents
const agents = await this.getAllAgentIds(installDir);
let concatenatedContent = '';
// Process Agents
const agents = selectedAgent ? [selectedAgent] : await this.getAllAgentIds(installDir);
for (const agentId of agents) { for (const agentId of agents) {
// Find the source agent file
const agentPath = await this.findAgentPath(agentId, installDir); const agentPath = await this.findAgentPath(agentId, installDir);
if (!agentPath) {
if (agentPath) { console.log(chalk.yellow(`✗ Agent file not found for ${agentId}, skipping.`));
const agentContent = await fileManager.readFile(agentPath); continue;
// Create properly formatted agent rule content (similar to trae)
let agentRuleContent = `# ${agentId.toUpperCase()} Agent Rule\n\n`;
agentRuleContent += `This rule is triggered when the user types \`*${agentId}\` and activates the ${await this.getAgentTitle(
agentId,
installDir,
)} agent persona.\n\n`;
agentRuleContent += '## Agent Activation\n\n';
agentRuleContent +=
'CRITICAL: Read the full YAML, start activation to alter your state of being, follow startup section instructions, stay in this being until told to exit this mode:\n\n';
agentRuleContent += '```yaml\n';
// Extract just the YAML content from the agent file
const yamlContent = extractYamlFromAgent(agentContent);
if (yamlContent) {
agentRuleContent += yamlContent;
} else {
// If no YAML found, include the whole content minus the header
agentRuleContent += agentContent.replace(/^#.*$/m, '').trim();
}
agentRuleContent += '\n```\n\n';
agentRuleContent += '## File Reference\n\n';
const relativePath = path.relative(installDir, agentPath).replaceAll('\\', '/');
agentRuleContent += `The complete agent definition is available in [${relativePath}](${relativePath}).\n\n`;
agentRuleContent += '## Usage\n\n';
agentRuleContent += `When the user types \`*${agentId}\`, activate this ${await this.getAgentTitle(
agentId,
installDir,
)} persona and follow all instructions defined in the YAML configuration above.\n`;
// Add to concatenated content with separator
concatenatedContent += agentRuleContent + '\n\n---\n\n';
console.log(chalk.green(`✓ Added context for @${agentId}`));
} }
const agentTitle = await this.getAgentTitle(agentId, installDir);
const commandPath = path.join(agentCommandsDir, `${agentId}.toml`);
// Get relative path from installDir to agent file for @{file} reference
const relativeAgentPath = path.relative(installDir, agentPath).replaceAll('\\', '/');
const tomlContent = `description = "Activates the ${agentTitle} agent from the BMad Method."
prompt = """
CRITICAL: You are now the BMad '${agentTitle}' agent. Adopt its persona, follow its instructions, and use its capabilities. The full agent definition is below.
@{${relativeAgentPath}}
"""`;
await fileManager.writeFile(commandPath, tomlContent);
console.log(chalk.green(`✓ Created agent command: /bmad:agents:${agentId}`));
} }
// Write the concatenated content to GEMINI.md // Process Tasks
const geminiMdPath = path.join(bmadMethodDir, 'GEMINI.md'); const tasks = await this.getAllTaskIds(installDir);
await fileManager.writeFile(geminiMdPath, concatenatedContent); for (const taskId of tasks) {
console.log(chalk.green(`\n✓ Created GEMINI.md in ${bmadMethodDir}`)); const taskPath = await this.findTaskPath(taskId, installDir);
if (!taskPath) {
console.log(chalk.yellow(`✗ Task file not found for ${taskId}, skipping.`));
continue;
}
const taskTitle = taskId
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
const commandPath = path.join(taskCommandsDir, `${taskId}.toml`);
// Get relative path from installDir to task file for @{file} reference
const relativeTaskPath = path.relative(installDir, taskPath).replaceAll('\\', '/');
const tomlContent = `description = "Executes the BMad Task: ${taskTitle}"
prompt = """
CRITICAL: You are to execute the BMad Task defined below.
@{${relativeTaskPath}}
"""`;
await fileManager.writeFile(commandPath, tomlContent);
console.log(chalk.green(`✓ Created task command: /bmad:tasks:${taskId}`));
}
console.log(
chalk.green(`
Created Gemini CLI extension in ${bmadCommandsDir}`),
);
console.log(
chalk.dim('You can now use commands like /bmad:agents:dev or /bmad:tasks:create-doc.'),
);
return true; return true;
} }

View File

@ -492,6 +492,7 @@ class V3ToV4Upgrader {
const ideMessages = { const ideMessages = {
cursor: 'Rules created in .cursor/rules/bmad/', cursor: 'Rules created in .cursor/rules/bmad/',
'claude-code': 'Commands created in .claude/commands/BMad/', 'claude-code': 'Commands created in .claude/commands/BMad/',
'iflow-cli': 'Commands created in .iflow/commands/BMad/',
windsurf: 'Rules created in .windsurf/workflows/', windsurf: 'Rules created in .windsurf/workflows/',
trae: 'Rules created in.trae/rules/', trae: 'Rules created in.trae/rules/',
roo: 'Custom modes created in .roomodes', roo: 'Custom modes created in .roomodes',