doc updates, build folder renamed to tools, readme clarity for v4

This commit is contained in:
Brian Madison
2025-06-08 10:36:23 -05:00
parent 6c4ff90c50
commit aa3d8eba67
25 changed files with 20397 additions and 520 deletions

View File

@@ -0,0 +1,304 @@
/**
* BMAD v4 Bundle Optimizer
* Optimizes bundles by deduplicating resources and minimizing size
*/
const fs = require('fs');
const path = require('path');
class BundleOptimizer {
constructor(rootPath = process.cwd()) {
this.rootPath = rootPath;
this.corePath = path.join(rootPath, 'bmad-core');
}
/**
* Optimize a bundle by loading and processing resources
*/
optimizeBundle(bundleConfig, agentDependencies) {
const optimizedBundle = {
metadata: {
name: bundleConfig.name,
version: bundleConfig.version,
environment: bundleConfig.target_environment,
generatedAt: new Date().toISOString(),
optimization: bundleConfig.optimize || false
},
agents: {},
resources: {
personas: {},
tasks: {},
templates: {},
checklists: {},
data: {},
utils: {}
},
sections: [],
statistics: {}
};
// Process each agent
agentDependencies.agents.forEach(agentDep => {
optimizedBundle.agents[agentDep.agent] = {
name: agentDep.config.name,
id: agentDep.config.id,
title: agentDep.config.title,
description: agentDep.config.description,
persona: agentDep.config.persona,
customize: agentDep.config.customize || '',
capabilities: agentDep.config.capabilities || [],
workflow: agentDep.config.workflow || []
};
});
// Load and process resources
this.loadResources(optimizedBundle, agentDependencies.bundleResources, agentDependencies.agents);
// Create optimized sections for web output
if (bundleConfig.target_environment === 'web') {
this.createWebSections(optimizedBundle, bundleConfig);
}
// Calculate statistics
optimizedBundle.statistics = this.calculateBundleStats(optimizedBundle, agentDependencies);
return optimizedBundle;
}
/**
* Load resources from core directory
*/
loadResources(bundle, resourceLists, agentDeps = []) {
const resourceTypes = ['tasks', 'templates', 'checklists', 'data', 'utils'];
resourceTypes.forEach(type => {
const resourceDir = path.join(this.corePath, type);
(resourceLists[type] || []).forEach(resourceName => {
const content = this.loadResourceFile(resourceDir, resourceName);
if (content) {
bundle.resources[type][resourceName] = {
name: resourceName,
content: content,
size: content.length
};
}
});
});
// Load personas for agents
const personaDir = path.join(this.corePath, 'personas');
agentDeps.forEach(agentDep => {
const agentId = agentDep.agent;
const personaName = agentDep.config.persona || agentId;
const personaContent = this.loadResourceFile(personaDir, personaName);
if (personaContent) {
bundle.resources.personas[agentId] = {
name: personaName,
content: personaContent,
size: personaContent.length
};
}
});
}
/**
* Load a resource file from disk
*/
loadResourceFile(dir, name) {
const extensions = ['.md', '.yml', '.yaml'];
for (const ext of extensions) {
const filePath = path.join(dir, `${name}${ext}`);
if (fs.existsSync(filePath)) {
return fs.readFileSync(filePath, 'utf8');
}
}
console.warn(`Resource file not found: ${name} in ${dir}`);
return null;
}
/**
* Create web-formatted sections with markers
*/
createWebSections(bundle, bundleConfig) {
const sections = [];
// Create personas section
if (Object.keys(bundle.resources.personas).length > 0) {
const personasContent = Object.entries(bundle.resources.personas)
.map(([id, persona]) =>
`==================== START: personas#${id} ====================\n` +
persona.content +
`\n==================== END: personas#${id} ====================`
).join('\n\n');
sections.push({
name: 'personas',
filename: 'personas.txt',
content: personasContent,
size: personasContent.length
});
}
// Create other resource sections
['tasks', 'templates', 'checklists', 'data', 'utils'].forEach(type => {
const resources = bundle.resources[type];
if (Object.keys(resources).length > 0) {
const sectionContent = Object.entries(resources)
.map(([name, resource]) =>
`==================== START: ${type}#${name} ====================\n` +
resource.content +
`\n==================== END: ${type}#${name} ====================`
).join('\n\n');
sections.push({
name: type,
filename: `${type}.txt`,
content: sectionContent,
size: sectionContent.length
});
}
});
bundle.sections = sections;
}
/**
* Create standalone agent bundle
*/
createStandaloneAgent(agentId, environment = 'web') {
const DependencyResolver = require('./dependency-resolver');
const resolver = new DependencyResolver(this.rootPath);
const agentDep = resolver.resolveAgentDependencies(agentId, environment);
const bundleConfig = {
name: `${agentDep.config.name} Standalone`,
version: agentDep.config.version || '1.0.0',
target_environment: environment,
optimize: true
};
// Create bundle with just this agent
const agentDependencies = {
agents: [agentDep],
bundleResources: agentDep.resources
};
const optimizedBundle = this.optimizeBundle(bundleConfig, agentDependencies);
// For standalone agents, create a single combined content
if (environment === 'web') {
optimizedBundle.standaloneContent = this.createStandaloneContent(optimizedBundle, agentId);
}
return optimizedBundle;
}
/**
* Create single-file content for standalone agent
*/
createStandaloneContent(bundle, agentId) {
const agent = bundle.agents[agentId];
const persona = bundle.resources.personas[agentId];
let content = `# ${agent.name}\n\n`;
content += `${agent.description}\n\n`;
if (persona) {
content += `==================== START: personas#${agentId} ====================\n`;
content += `${persona.content}\n`;
content += `==================== END: personas#${agentId} ====================\n\n`;
}
// Add required resources inline
const resourceTypes = ['tasks', 'templates', 'checklists', 'data', 'utils'];
resourceTypes.forEach(type => {
const resources = bundle.resources[type];
if (Object.keys(resources).length > 0) {
Object.entries(resources).forEach(([name, resource]) => {
content += `==================== START: ${type}#${name} ====================\n`;
content += `${resource.content}\n`;
content += `==================== END: ${type}#${name} ====================\n\n`;
});
}
});
return content;
}
/**
* Calculate bundle statistics
*/
calculateBundleStats(bundle, agentDependencies) {
const stats = {
agents: Object.keys(bundle.agents).length,
totalResources: 0,
resourcesByType: {},
totalSize: 0,
sizeByType: {},
averageResourceSize: 0
};
// Count resources and calculate sizes
Object.entries(bundle.resources).forEach(([type, resources]) => {
const count = Object.keys(resources).length;
stats.resourcesByType[type] = count;
stats.totalResources += count;
const typeSize = Object.values(resources).reduce((sum, r) => sum + (r.size || 0), 0);
stats.sizeByType[type] = typeSize;
stats.totalSize += typeSize;
});
if (stats.totalResources > 0) {
stats.averageResourceSize = Math.round(stats.totalSize / stats.totalResources);
}
// Add web-specific stats
if (bundle.sections) {
stats.webSections = bundle.sections.length;
stats.webTotalSize = bundle.sections.reduce((sum, s) => sum + s.size, 0);
}
return stats;
}
/**
* Validate bundle against size constraints
*/
validateBundle(bundle, constraints = {}) {
const issues = [];
const stats = bundle.statistics;
// Check max bundle size
if (constraints.maxBundleSize && stats.totalSize > constraints.maxBundleSize) {
issues.push({
type: 'size_exceeded',
message: `Bundle size ${stats.totalSize} exceeds limit ${constraints.maxBundleSize}`,
severity: 'error'
});
}
// Check web section sizes
if (bundle.sections) {
bundle.sections.forEach(section => {
if (constraints.maxSectionSize && section.size > constraints.maxSectionSize) {
issues.push({
type: 'section_size_exceeded',
message: `Section ${section.name} size ${section.size} exceeds limit ${constraints.maxSectionSize}`,
severity: 'warning'
});
}
});
}
return {
valid: issues.filter(i => i.severity === 'error').length === 0,
issues: issues
};
}
}
module.exports = BundleOptimizer;

View File

@@ -0,0 +1,262 @@
/**
* BMAD v4 Dependency Resolver
* Analyzes agent configurations and resolves resource dependencies
*/
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
class DependencyResolver {
constructor(rootPath = process.cwd()) {
this.rootPath = rootPath;
this.agentsPath = path.join(rootPath, 'agents');
this.corePath = path.join(rootPath, 'bmad-core');
this.cache = new Map();
}
/**
* Load and parse an agent configuration
*/
loadAgentConfig(agentId) {
if (this.cache.has(agentId)) {
return this.cache.get(agentId);
}
const configPath = path.join(this.agentsPath, `${agentId}.yml`);
if (!fs.existsSync(configPath)) {
throw new Error(`Agent configuration not found: ${configPath}`);
}
const configContent = fs.readFileSync(configPath, 'utf8');
const rawConfig = yaml.load(configContent);
// Extract agent config from nested structure if present
const config = rawConfig.agent || rawConfig;
// Merge other root-level fields that might be needed
if (rawConfig.dependencies) {
config.dependencies = rawConfig.dependencies;
}
// Validate required fields
this.validateAgentConfig(config, agentId);
this.cache.set(agentId, config);
return config;
}
/**
* Validate agent configuration structure
*/
validateAgentConfig(config, agentId) {
const required = ['name', 'id'];
for (const field of required) {
if (!config[field]) {
throw new Error(`Missing required field '${field}' in agent ${agentId}`);
}
}
if (config.id !== agentId) {
throw new Error(`Agent ID mismatch: expected '${agentId}', got '${config.id}'`);
}
// Ensure persona exists
if (!config.persona) {
// Default to agent id if no persona specified
config.persona = config.id;
}
// Ensure dependencies exist with defaults
if (!config.dependencies) {
config.dependencies = {
tasks: [],
templates: [],
checklists: [],
data: []
};
}
}
/**
* Resolve dependencies for a single agent
*/
resolveAgentDependencies(agentId, environment = 'web') {
const config = this.loadAgentConfig(agentId);
const dependencies = {
agent: agentId,
config: config,
resources: {
tasks: config.dependencies?.tasks || [],
templates: config.dependencies?.templates || [],
checklists: config.dependencies?.checklists || [],
data: config.dependencies?.data || [],
utils: config.dependencies?.utils || []
}
};
// Validate that all required resources exist
this.validateResourceExistence(dependencies.resources);
return dependencies;
}
/**
* Resolve dependencies for multiple agents (bundle)
*/
resolveBundleDependencies(agentIds, environment = 'web', optimize = true) {
const agentDependencies = [];
const allResources = {
tasks: new Set(),
templates: new Set(),
checklists: new Set(),
data: new Set(),
utils: new Set()
};
// Collect dependencies for each agent
for (const agentId of agentIds) {
const deps = this.resolveAgentDependencies(agentId, environment);
agentDependencies.push(deps);
// Aggregate all resources
Object.keys(allResources).forEach(type => {
deps.resources[type].forEach(resource => {
allResources[type].add(resource);
});
});
}
const result = {
agents: agentDependencies,
bundleResources: {
tasks: Array.from(allResources.tasks),
templates: Array.from(allResources.templates),
checklists: Array.from(allResources.checklists),
data: Array.from(allResources.data),
utils: Array.from(allResources.utils)
},
optimized: optimize
};
if (optimize) {
result.statistics = this.calculateOptimizationStats(agentDependencies, result.bundleResources);
}
return result;
}
/**
* Calculate optimization statistics
*/
calculateOptimizationStats(agentDeps, bundleResources) {
const totalAgents = agentDeps.length;
const totalResources = Object.values(bundleResources).reduce((sum, arr) => sum + arr.length, 0);
// Calculate how many resources would be needed without optimization
const unoptimizedTotal = agentDeps.reduce((sum, agent) => {
return sum + Object.values(agent.resources).reduce((agentSum, arr) => agentSum + arr.length, 0);
}, 0);
const savings = unoptimizedTotal - totalResources;
const savingsPercentage = unoptimizedTotal > 0 ? (savings / unoptimizedTotal * 100).toFixed(1) : 0;
return {
totalAgents,
totalUniqueResources: totalResources,
unoptimizedResourceCount: unoptimizedTotal,
resourcesSaved: savings,
optimizationPercentage: savingsPercentage
};
}
/**
* Validate that all required resources exist in core
*/
validateResourceExistence(resources) {
const resourceTypes = ['tasks', 'templates', 'checklists', 'data', 'utils'];
for (const type of resourceTypes) {
const resourceDir = path.join(this.corePath, type);
for (const resource of resources[type] || []) {
const resourcePath = path.join(resourceDir, `${resource}.md`);
const altPath = path.join(resourceDir, `${resource}.yml`);
if (!fs.existsSync(resourcePath) && !fs.existsSync(altPath)) {
throw new Error(`Resource not found: ${type}/${resource} (checked .md and .yml)`);
}
}
}
}
/**
* Get all available agents
*/
getAvailableAgents() {
if (!fs.existsSync(this.agentsPath)) {
return [];
}
return fs.readdirSync(this.agentsPath)
.filter(file => {
return (file.endsWith('.yml') || file.endsWith('.yaml')) &&
fs.statSync(path.join(this.agentsPath, file)).isFile();
})
.map(file => path.basename(file, path.extname(file)));
}
/**
* Generate dependency graph for visualization
*/
generateDependencyGraph(agentIds = null) {
const agents = agentIds || this.getAvailableAgents();
const graph = {
nodes: [],
edges: []
};
for (const agentId of agents) {
const config = this.loadAgentConfig(agentId);
// Add agent node
graph.nodes.push({
id: agentId,
type: 'agent',
label: config.name,
description: config.description
});
// Add resource nodes and edges
Object.entries(config.requires).forEach(([type, resources]) => {
resources.forEach(resource => {
const resourceId = `${type}:${resource}`;
// Add resource node if not exists
if (!graph.nodes.find(n => n.id === resourceId)) {
graph.nodes.push({
id: resourceId,
type: type,
label: resource,
category: type
});
}
// Add edge
graph.edges.push({
from: agentId,
to: resourceId,
type: 'requires'
});
});
});
}
return graph;
}
}
module.exports = DependencyResolver;

View File

@@ -0,0 +1,262 @@
/**
* BMAD v5 Unified Dependency Resolver
* Works with unified agent configurations that can generate both IDE and web outputs
*/
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
class UnifiedDependencyResolver {
constructor(rootPath = process.cwd()) {
this.rootPath = rootPath;
this.agentsPath = path.join(rootPath, 'agents');
this.corePath = path.join(rootPath, 'bmad-core');
this.cache = new Map();
}
/**
* Load and parse a unified agent configuration
*/
loadUnifiedAgentConfig(agentId) {
if (this.cache.has(agentId)) {
return this.cache.get(agentId);
}
const configPath = path.join(this.agentsPath, `${agentId}.yml`);
if (!fs.existsSync(configPath)) {
throw new Error(`Unified agent configuration not found: ${configPath}`);
}
try {
const content = fs.readFileSync(configPath, 'utf8');
const config = yaml.load(content);
// Validate unified config structure
this.validateUnifiedConfig(config, agentId);
this.cache.set(agentId, config);
return config;
} catch (error) {
throw new Error(`Failed to load unified agent config ${agentId}: ${error.message}`);
}
}
/**
* Validate unified configuration structure
*/
validateUnifiedConfig(config, agentId) {
if (!config.agent || !config.agent.id || !config.agent.name) {
throw new Error(`Invalid unified config for ${agentId}: missing agent identity`);
}
if (!config.dependencies) {
throw new Error(`Invalid unified config for ${agentId}: missing dependencies`);
}
if (!config.environments || !config.environments.web || !config.environments.ide) {
throw new Error(`Invalid unified config for ${agentId}: missing environment configurations`);
}
// CRITICAL: Ensure ONLY BMAD has bmad-kb access
const hasBmadKb = (
config.dependencies.data?.includes('bmad-kb') ||
config.environments.ide.dependencies?.data?.includes('bmad-kb') ||
config.environments.web.dependencies?.data?.includes('bmad-kb')
);
if (hasBmadKb && agentId !== 'bmad') {
throw new Error(`SECURITY VIOLATION: Agent ${agentId} has bmad-kb access but only BMAD should have it!`);
}
if (!hasBmadKb && agentId === 'bmad') {
throw new Error(`Configuration error: BMAD agent missing required bmad-kb access`);
}
}
/**
* Resolve dependencies for an agent in a specific environment
*/
resolveAgentDependencies(agentId, environment = 'web', bundleContext = null) {
const config = this.loadUnifiedAgentConfig(agentId);
// Start with base dependencies
const baseDeps = {
tasks: [...(config.dependencies.tasks || [])],
templates: [...(config.dependencies.templates || [])],
checklists: [...(config.dependencies.checklists || [])],
data: [...(config.dependencies.data || [])]
};
// Apply environment-specific overrides
const envConfig = config.environments[environment];
if (envConfig && envConfig.dependencies) {
Object.keys(envConfig.dependencies).forEach(type => {
if (envConfig.dependencies[type]) {
baseDeps[type] = [...new Set([...baseDeps[type], ...envConfig.dependencies[type]])];
}
});
}
// Special handling for team bundles containing BMAD
if (bundleContext && bundleContext.agents && bundleContext.agents.includes('bmad')) {
// Only add bmad-kb if this IS the BMAD agent
if (agentId === 'bmad') {
if (!baseDeps.data.includes('bmad-kb')) {
baseDeps.data.push('bmad-kb');
}
}
}
// Resolve file paths and validate existence
const resolvedDeps = {};
Object.keys(baseDeps).forEach(type => {
resolvedDeps[type] = baseDeps[type].map(dep => {
const filePath = this.resolveResourcePath(type, dep);
if (!fs.existsSync(filePath)) {
throw new Error(`Resource not found: ${filePath} (required by ${agentId})`);
}
return {
id: dep,
path: filePath,
content: fs.readFileSync(filePath, 'utf8')
};
});
});
return {
config: config,
agentId: agentId,
environment: environment,
resources: resolvedDeps
};
}
/**
* Resolve path for a resource
*/
resolveResourcePath(type, resourceId) {
const resourceMap = {
'tasks': 'tasks',
'templates': 'templates',
'checklists': 'checklists',
'data': 'data',
'personas': 'personas',
'utils': 'utils'
};
const subdir = resourceMap[type];
if (!subdir) {
throw new Error(`Unknown resource type: ${type}`);
}
return path.join(this.corePath, subdir, `${resourceId}.md`);
}
/**
* Get all available agents
*/
getAvailableAgents() {
if (!fs.existsSync(this.agentsPath)) {
return [];
}
return fs.readdirSync(this.agentsPath)
.filter(file => file.endsWith('.yml'))
.map(file => path.basename(file, '.yml'))
.sort();
}
/**
* Generate dependency graph for all agents
*/
generateDependencyGraph() {
const agents = this.getAvailableAgents();
const nodes = [];
const edges = [];
// Add agent nodes
agents.forEach(agentId => {
try {
const config = this.loadUnifiedAgentConfig(agentId);
nodes.push({
id: agentId,
type: 'agent',
label: config.agent.name,
title: config.agent.title,
hasBmadKb: config.dependencies.data?.includes('bmad-kb') || false
});
} catch (error) {
console.warn(`Skipping ${agentId}: ${error.message}`);
}
});
// Add resource nodes and edges
agents.forEach(agentId => {
try {
const deps = this.resolveAgentDependencies(agentId, 'web');
Object.entries(deps.resources).forEach(([type, resources]) => {
resources.forEach(resource => {
const resourceNodeId = `${type}:${resource.id}`;
// Add resource node if not exists
if (!nodes.find(n => n.id === resourceNodeId)) {
nodes.push({
id: resourceNodeId,
type: 'resource',
resourceType: type,
label: resource.id
});
}
// Add edge from agent to resource
edges.push({
from: agentId,
to: resourceNodeId,
type: 'requires'
});
});
});
} catch (error) {
console.warn(`Failed to analyze dependencies for ${agentId}: ${error.message}`);
}
});
return { nodes, edges };
}
/**
* Validate all configurations
*/
validateAllConfigurations() {
const agents = this.getAvailableAgents();
const results = {
valid: [],
invalid: [],
bmadKbViolations: []
};
agents.forEach(agentId => {
try {
const deps = this.resolveAgentDependencies(agentId, 'web');
results.valid.push(agentId);
// Check for bmad-kb violations
const hasBmadKb = deps.resources.data?.some(d => d.id === 'bmad-kb');
if (hasBmadKb && agentId !== 'bmad') {
results.bmadKbViolations.push(agentId);
}
} catch (error) {
results.invalid.push({
agentId: agentId,
error: error.message
});
}
});
return results;
}
}
module.exports = UnifiedDependencyResolver;