Files
BMAD-METHOD/tools/validate-agent-schema.js
Alex Verkhovsky 6b9ab8b201 feat: add agent schema validation with comprehensive testing
Introduce automated validation for agent YAML files using Zod to ensure
schema compliance across all agent definitions. This feature validates
17 agent files across core and module directories, catching structural
errors and maintaining consistency.

Schema Validation (tools/schema/agent.js):
- Zod-based schema validating metadata, persona, menu, prompts, and critical actions
- Module-aware validation: module field required for src/modules/**/agents/,
  optional for src/core/agents/
- Enforces kebab-case unique triggers and at least one command target per menu item
- Validates persona.principles as array (not string)
- Comprehensive refinements for data integrity

CLI Validator (tools/validate-agent-schema.js):
- Scans src/{core,modules/*}/agents/*.agent.yaml
- Parses with js-yaml and validates using Zod schema
- Reports detailed errors with file paths and field paths
- Exits 1 on failures, 0 on success
- Accepts optional project_root parameter for testing

Testing (679 lines across 3 test files):
- test/test-cli-integration.sh: CLI behavior and error handling tests
- test/unit-test-schema.js: Direct schema validation unit tests
- test/test-agent-schema.js: Comprehensive fixture-based tests
- 50 test fixtures covering valid and invalid scenarios
- ESLint configured to support CommonJS test files
- Prettier configured to ignore intentionally broken fixtures

CI Integration (.github/workflows/lint.yaml):
- Renamed from format-check.yaml to lint.yaml
- Added schema-validation job running npm run validate:schemas
- Runs in parallel with prettier and eslint jobs
- Validates on all pull requests

Data Cleanup:
- Fixed src/core/agents/bmad-master.agent.yaml: converted persona.principles
  from string to array format

Documentation:
- Updated schema-classification.md with validation section
- Documents validator usage, enforcement rules, and CI integration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 02:14:33 -07:00

111 lines
3.3 KiB
JavaScript

/**
* Agent Schema Validator CLI
*
* Scans all *.agent.yaml files in src/{core,modules/*}/agents/
* and validates them against the Zod schema.
*
* Usage: node tools/validate-agent-schema.js [project_root]
* Exit codes: 0 = success, 1 = validation failures
*
* Optional argument:
* project_root - Directory to scan (defaults to BMAD repo root)
*/
const { glob } = require('glob');
const yaml = require('js-yaml');
const fs = require('node:fs');
const path = require('node:path');
const { validateAgentFile } = require('./schema/agent.js');
/**
* Main validation routine
* @param {string} [customProjectRoot] - Optional project root to scan (for testing)
*/
async function main(customProjectRoot) {
console.log('🔍 Scanning for agent files...\n');
// Determine project root: use custom path if provided, otherwise default to repo root
const project_root = customProjectRoot || path.join(__dirname, '..');
// Find all agent files
const agentFiles = await glob('src/{core,modules/*}/agents/*.agent.yaml', {
cwd: project_root,
absolute: true,
});
if (agentFiles.length === 0) {
console.log('❌ No agent files found. This likely indicates a configuration error.');
console.log(' Expected to find *.agent.yaml files in src/{core,modules/*}/agents/');
process.exit(1);
}
console.log(`Found ${agentFiles.length} agent file(s)\n`);
const errors = [];
// Validate each file
for (const filePath of agentFiles) {
const relativePath = path.relative(process.cwd(), filePath);
try {
const fileContent = fs.readFileSync(filePath, 'utf8');
const agentData = yaml.load(fileContent);
// Convert absolute path to relative src/ path for module detection
const srcRelativePath = relativePath.startsWith('src/') ? relativePath : path.relative(project_root, filePath).replaceAll('\\', '/');
const result = validateAgentFile(srcRelativePath, agentData);
if (result.success) {
console.log(`${relativePath}`);
} else {
errors.push({
file: relativePath,
issues: result.error.issues,
});
}
} catch (error) {
errors.push({
file: relativePath,
issues: [
{
code: 'parse_error',
message: `Failed to parse YAML: ${error.message}`,
path: [],
},
],
});
}
}
// Report errors
if (errors.length > 0) {
console.log('\n❌ Validation failed for the following files:\n');
for (const { file, issues } of errors) {
console.log(`\n📄 ${file}`);
for (const issue of issues) {
const pathString = issue.path.length > 0 ? issue.path.join('.') : '(root)';
console.log(` Path: ${pathString}`);
console.log(` Error: ${issue.message}`);
if (issue.code) {
console.log(` Code: ${issue.code}`);
}
}
}
console.log(`\n\n💥 ${errors.length} file(s) failed validation`);
process.exit(1);
}
console.log(`\n✨ All ${agentFiles.length} agent file(s) passed validation!\n`);
process.exit(0);
}
// Run with optional command-line argument for project root
const customProjectRoot = process.argv[2];
main(customProjectRoot).catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});