chore: remove dead agent schema validation infrastructure

The *.agent.yaml format was replaced by SKILL.md-based agents.
Zero agent YAML files remain in src/, so remove the Zod schema,
validator CLI, fixture-based test suite (52 fixtures), unit tests,
CLI integration tests, and the CI steps that invoked them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alex Verkhovsky 2026-03-17 20:20:12 -06:00
parent f3f606a9ce
commit f0c7cf41c7
62 changed files with 25 additions and 2873 deletions

View File

@ -4,8 +4,6 @@ name: Quality & Validation
# - Prettier (formatting)
# - ESLint (linting)
# - markdownlint (markdown quality)
# - Schema validation (YAML structure)
# - Agent schema tests (fixture-based validation)
# - Installation component tests (compilation)
# Keep this workflow aligned with `npm run quality` in `package.json`.
@ -105,12 +103,6 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Validate YAML schemas
run: npm run validate:schemas
- name: Run agent schema validation tests
run: npm run test:schemas
- name: Test agent compilation components
run: npm run test:install

View File

@ -24,7 +24,6 @@ tools/build-docs.mjs
tools/fix-doc-links.js
tools/validate-doc-links.js
tools/validate-file-refs.js
tools/validate-agent-schema.js
# Images (branding/marketing only)
banner-bmad-method.png

View File

@ -146,7 +146,6 @@ Keep messages under 72 characters. Each commit = one logical change.
- Web/planning agents can be larger with complex tasks
- Everything is natural language (markdown) — no code in core framework
- Use BMad modules for domain-specific features
- Validate YAML schemas: `npm run validate:schemas`
- Validate file references: `npm run validate:refs`
### File-Pattern-to-Validator Mapping

View File

@ -39,15 +39,12 @@
"lint:fix": "eslint . --ext .js,.cjs,.mjs,.yaml --fix",
"lint:md": "markdownlint-cli2 \"**/*.md\"",
"prepare": "command -v husky >/dev/null 2>&1 && husky || exit 0",
"quality": "npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run validate:schemas && npm run test:schemas && npm run test:install && npm run validate:refs",
"quality": "npm run format:check && npm run lint && npm run lint:md && npm run docs:build && npm run test:install && npm run validate:refs",
"rebundle": "node tools/cli/bundlers/bundle-web.js rebundle",
"test": "npm run test:schemas && npm run test:refs && npm run test:install && npm run validate:schemas && npm run lint && npm run lint:md && npm run format:check",
"test:coverage": "c8 --reporter=text --reporter=html npm run test:schemas",
"test": "npm run test:refs && npm run test:install && npm run lint && npm run lint:md && npm run format:check",
"test:install": "node test/test-installation-components.js",
"test:refs": "node test/test-file-refs-csv.js",
"test:schemas": "node test/test-agent-schema.js",
"validate:refs": "node tools/validate-file-refs.js --strict",
"validate:schemas": "node tools/validate-agent-schema.js"
"validate:refs": "node tools/validate-file-refs.js --strict"
},
"lint-staged": {
"*.{js,cjs,mjs}": [

View File

@ -1,295 +1,38 @@
# Agent Schema Validation Test Suite
# Test Suite
Comprehensive test coverage for the BMAD agent schema validation system.
## Overview
This test suite validates the Zod-based schema validator (`tools/schema/agent.js`) that ensures all `*.agent.yaml` files conform to the BMAD agent specification.
## Test Statistics
- **Total Test Fixtures**: 50
- **Valid Test Cases**: 18
- **Invalid Test Cases**: 32
- **Code Coverage**: 100% all metrics (statements, branches, functions, lines)
- **Exit Code Tests**: 4 CLI integration tests
Tests for the BMAD-METHOD tooling infrastructure.
## Quick Start
```bash
# Run all tests
npm test
# Run all quality checks
npm run quality
# Run with coverage report
npm run test:coverage
# Run CLI integration tests
./test/test-cli-integration.sh
# Validate actual agent files
npm run validate:schemas
# Run individual test suites
npm run test:install # Installation component tests
npm run test:refs # File reference CSV tests
npm run validate:refs # File reference validation (strict)
```
## Test Organization
### Test Fixtures
Located in `test/fixtures/agent-schema/`, organized by category:
```
test/fixtures/agent-schema/
├── valid/ # 15 fixtures that should pass
│ ├── top-level/ # Basic structure tests
│ ├── metadata/ # Metadata field tests
│ ├── persona/ # Persona field tests
│ ├── critical-actions/ # Critical actions tests
│ ├── menu/ # Menu structure tests
│ ├── menu-commands/ # Command target tests
│ ├── menu-triggers/ # Trigger format tests
│ └── prompts/ # Prompts field tests
└── invalid/ # 32 fixtures that should fail
├── top-level/ # Structure errors
├── metadata/ # Metadata validation errors
├── persona/ # Persona validation errors
├── critical-actions/ # Critical actions errors
├── menu/ # Menu errors
├── menu-commands/ # Command target errors
├── menu-triggers/ # Trigger format errors
├── prompts/ # Prompts errors
└── yaml-errors/ # YAML parsing errors
```
## Test Categories
### 1. Top-Level Structure Tests (4 fixtures)
Tests the root-level agent structure:
- ✅ Valid: Minimal core agent with required fields
- ❌ Invalid: Empty YAML file
- ❌ Invalid: Missing `agent` key
- ❌ Invalid: Extra top-level keys (strict mode)
### 2. Metadata Field Tests (7 fixtures)
Tests agent metadata validation:
- ✅ Valid: Module agent with correct `module` field
- ❌ Invalid: Missing required fields (`id`, `name`, `title`, `icon`)
- ❌ Invalid: Empty strings in metadata
- ❌ Invalid: Module agent missing `module` field
- ❌ Invalid: Core agent with unexpected `module` field
- ❌ Invalid: Wrong `module` value (doesn't match path)
- ❌ Invalid: Extra unknown metadata fields
### 3. Persona Field Tests (6 fixtures)
Tests persona structure and validation:
- ✅ Valid: Complete persona with all fields
- ❌ Invalid: Missing required fields (`role`, `identity`, etc.)
- ❌ Invalid: `principles` as string instead of array
- ❌ Invalid: Empty `principles` array
- ❌ Invalid: Empty strings in `principles` array
- ❌ Invalid: Extra unknown persona fields
### 4. Critical Actions Tests (5 fixtures)
Tests optional `critical_actions` field:
- ✅ Valid: No `critical_actions` field (optional)
- ✅ Valid: Empty `critical_actions` array
- ✅ Valid: Valid action strings
- ❌ Invalid: Empty strings in actions
- ❌ Invalid: Actions as non-array type
### 5. Menu Field Tests (4 fixtures)
Tests required menu structure:
- ✅ Valid: Single menu item
- ✅ Valid: Multiple menu items with different commands
- ❌ Invalid: Missing `menu` field
- ❌ Invalid: Empty `menu` array
### 6. Menu Command Target Tests (4 fixtures)
Tests menu item command targets:
- ✅ Valid: All 6 command types (`workflow`, `validate-workflow`, `exec`, `action`, `tmpl`, `data`)
- ✅ Valid: Multiple command targets in one menu item
- ❌ Invalid: No command target fields
- ❌ Invalid: Empty string command targets
### 7. Menu Trigger Validation Tests (7 fixtures)
Tests trigger format enforcement:
- ✅ Valid: Kebab-case triggers (`help`, `list-tasks`, `multi-word-trigger`)
- ❌ Invalid: Leading asterisk (`*help`)
- ❌ Invalid: CamelCase (`listTasks`)
- ❌ Invalid: Snake_case (`list_tasks`)
- ❌ Invalid: Spaces (`list tasks`)
- ❌ Invalid: Duplicate triggers within agent
- ❌ Invalid: Empty trigger string
### 8. Prompts Field Tests (8 fixtures)
Tests optional `prompts` field:
- ✅ Valid: No `prompts` field (optional)
- ✅ Valid: Empty `prompts` array
- ✅ Valid: Prompts with required `id` and `content`
- ✅ Valid: Prompts with optional `description`
- ❌ Invalid: Missing `id`
- ❌ Invalid: Missing `content`
- ❌ Invalid: Empty `content` string
- ❌ Invalid: Extra unknown prompt fields
### 9. YAML Parsing Tests (2 fixtures)
Tests YAML parsing error handling:
- ❌ Invalid: Malformed YAML syntax
- ❌ Invalid: Invalid indentation
## Test Scripts
### Main Test Runner
### Installation Component Tests
**File**: `test/test-agent-schema.js`
**File**: `test/test-installation-components.js`
Automated test runner that:
Validates that the installer compiles and assembles agents correctly.
- Loads all fixtures from `test/fixtures/agent-schema/`
- Validates each against the schema
- Compares results with expected outcomes (parsed from YAML comments)
- Reports detailed results by category
- Exits with code 0 (pass) or 1 (fail)
### File Reference Tests
**Usage**:
**File**: `test/test-file-refs-csv.js`
```bash
npm test
# or
node test/test-agent-schema.js
Tests the CSV-based file reference validation logic.
## Test Fixtures
Located in `test/fixtures/`:
```text
test/fixtures/
└── file-refs-csv/ # Fixtures for file reference CSV tests
```
### Coverage Report
**Command**: `npm run test:coverage`
Generates code coverage report using c8:
- Text output to console
- HTML report in `coverage/` directory
- Tracks statement, branch, function, and line coverage
**Current Coverage**:
- Statements: 100%
- Branches: 100%
- Functions: 100%
- Lines: 100%
### CLI Integration Tests
**File**: `test/test-cli-integration.sh`
Bash script that tests CLI behavior:
1. Validates existing agent files
2. Verifies test fixture validation
3. Checks exit code 0 for valid files
4. Verifies test runner output format
**Usage**:
```bash
./test/test-cli-integration.sh
```
## Manual Testing
See **[MANUAL-TESTING.md](./MANUAL-TESTING.md)** for detailed manual testing procedures, including:
- Testing with invalid files
- GitHub Actions workflow verification
- Troubleshooting guide
- PR merge blocking tests
## Coverage Achievement
**100% code coverage achieved!** All branches, statements, functions, and lines in the validation logic are tested.
Edge cases covered include:
- Malformed module paths (e.g., `src/bmm` without `/agents/`)
- Empty module names in paths (e.g., `src/modules//agents/`)
- Whitespace-only module field values
- All validation error paths
- All success paths for valid configurations
## Adding New Tests
To add new test cases:
1. Create a new `.agent.yaml` file in the appropriate `valid/` or `invalid/` subdirectory
2. Add comment metadata at the top:
```yaml
# Test: Description of what this tests
# Expected: PASS (or FAIL - error description)
# Path context: src/bmm/agents/test.agent.yaml (if needed)
```
3. Run the test suite to verify: `npm test`
## Integration with CI/CD
The validation is integrated into the GitHub Actions workflow:
**File**: `.github/workflows/lint.yaml`
**Job**: `agent-schema`
**Runs on**: All pull requests
**Blocks merge if**: Validation fails
## Files
- `test/test-agent-schema.js` - Main test runner
- `test/test-cli-integration.sh` - CLI integration tests
- `test/MANUAL-TESTING.md` - Manual testing guide
- `test/fixtures/agent-schema/` - Test fixtures (47 files)
- `tools/schema/agent.js` - Validation logic (under test)
- `tools/validate-agent-schema.js` - CLI wrapper
## Dependencies
- **zod**: Schema validation library
- **yaml**: YAML parsing
- **glob**: File pattern matching
- **c8**: Code coverage reporting
## Success Criteria
All success criteria from the original task have been exceeded:
- ✅ 50 test fixtures covering all validation rules (target: 47+)
- ✅ Automated test runner with detailed reporting
- ✅ CLI integration tests verifying exit codes and output
- ✅ Manual testing documentation
- ✅ **100% code coverage achieved** (target: 99%+)
- ✅ Both positive and negative test cases
- ✅ Clear and actionable error messages
- ✅ GitHub Actions integration verified
- ✅ Aggressive defensive assertions implemented
## Resources
- **Schema Documentation**: `schema-classification.md`
- **Validator Implementation**: `tools/schema/agent.js`
- **CLI Tool**: `tools/validate-agent-schema.js`
- **Project Guidelines**: `CLAUDE.md`

View File

@ -1,27 +0,0 @@
# Test: critical_actions as non-array
# Expected: FAIL
# Error code: invalid_type
# Error path: agent.critical_actions
# Error expected: array
agent:
metadata:
id: actions-string
name: Actions String
title: Actions String
icon:
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
critical_actions: This should be an array
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,30 +0,0 @@
# Test: critical_actions with empty strings
# Expected: FAIL
# Error code: custom
# Error path: agent.critical_actions[1]
# Error message: agent.critical_actions[] must be a non-empty string
agent:
metadata:
id: empty-action-string
name: Empty Action String
title: Empty Action String
icon:
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
critical_actions:
- Valid action
- " "
- Another valid action
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,25 +0,0 @@
# Test: Menu item with empty string command target
# Expected: FAIL
# Error code: custom
# Error path: agent.menu[0].action
# Error message: agent.menu[].action must be a non-empty string
agent:
metadata:
id: empty-command
name: Empty Command Target
title: Empty Command
icon:
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
menu:
- trigger: help
description: Show help
action: " "

View File

@ -1,24 +0,0 @@
# Test: Menu item with no command target fields
# Expected: FAIL
# Error code: custom
# Error path: agent.menu[0]
# Error message: agent.menu[] entries must include at least one command target field
agent:
metadata:
id: no-command
name: No Command Target
title: No Command
icon:
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
menu:
- trigger: help
description: Show help but no command target

View File

@ -1,25 +0,0 @@
# Test: CamelCase trigger
# Expected: FAIL
# Error code: custom
# Error path: agent.menu[0].trigger
# Error message: agent.menu[].trigger must be kebab-case (lowercase words separated by hyphen)
agent:
metadata:
id: camel-case-trigger
name: CamelCase Trigger
title: CamelCase
icon:
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
menu:
- trigger: listTasks
description: Invalid CamelCase trigger
action: list_tasks

View File

@ -1,25 +0,0 @@
# Test: Compound trigger with invalid format
# Expected: FAIL
# Error code: custom
# Error path: agent.menu[0].trigger
# Error message: agent.menu[].trigger compound format error: invalid compound trigger format
agent:
metadata:
id: compound-invalid-format
name: Invalid Format
title: Invalid Format Test
icon: 🧪
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
menu:
- trigger: TS or tech-spec
description: Missing fuzzy match clause
action: test

View File

@ -1,25 +0,0 @@
# Test: Compound trigger with old format (no longer supported)
# Expected: FAIL
# Error code: custom
# Error path: agent.menu[0].trigger
# Error message: agent.menu[].trigger compound format error: invalid compound trigger format
agent:
metadata:
id: compound-mismatched-kebab
name: Old Format
title: Old Format Test
icon: 🧪
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
menu:
- trigger: TS or tech-spec or fuzzy match on tech-spec
description: Old format with middle kebab-case (no longer supported)
action: test

View File

@ -1,31 +0,0 @@
# Test: Duplicate triggers within same agent
# Expected: FAIL
# Error code: custom
# Error path: agent.menu[2].trigger
# Error message: agent.menu[].trigger duplicates "help" within the same agent
agent:
metadata:
id: duplicate-triggers
name: Duplicate Triggers
title: Duplicate
icon:
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
menu:
- trigger: help
description: First help command
action: display_help
- trigger: list-tasks
description: List tasks
action: list_tasks
- trigger: help
description: Duplicate help command
action: show_help

View File

@ -1,25 +0,0 @@
# Test: Empty trigger string
# Expected: FAIL
# Error code: custom
# Error path: agent.menu[0].trigger
# Error message: agent.menu[].trigger must be a non-empty string
agent:
metadata:
id: empty-trigger
name: Empty Trigger
title: Empty
icon:
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
menu:
- trigger: " "
description: Empty trigger
action: display_help

View File

@ -1,25 +0,0 @@
# Test: Trigger with leading asterisk
# Expected: FAIL
# Error code: custom
# Error path: agent.menu[0].trigger
# Error message: agent.menu[].trigger must be kebab-case (lowercase words separated by hyphen)
agent:
metadata:
id: asterisk-trigger
name: Asterisk Trigger
title: Asterisk
icon:
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
menu:
- trigger: "*help"
description: Invalid trigger with asterisk
action: display_help

View File

@ -1,25 +0,0 @@
# Test: Snake_case trigger
# Expected: FAIL
# Error code: custom
# Error path: agent.menu[0].trigger
# Error message: agent.menu[].trigger must be kebab-case (lowercase words separated by hyphen)
agent:
metadata:
id: snake-case-trigger
name: Snake Case Trigger
title: Snake Case
icon:
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
menu:
- trigger: list_tasks
description: Invalid snake_case trigger
action: list_tasks

View File

@ -1,25 +0,0 @@
# Test: Trigger with spaces
# Expected: FAIL
# Error code: custom
# Error path: agent.menu[0].trigger
# Error message: agent.menu[].trigger must be kebab-case (lowercase words separated by hyphen)
agent:
metadata:
id: spaces-trigger
name: Spaces Trigger
title: Spaces
icon:
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
menu:
- trigger: list tasks
description: Invalid trigger with spaces
action: list_tasks

View File

@ -1,22 +0,0 @@
# Test: Empty menu array
# Expected: FAIL
# Error code: too_small
# Error path: agent.menu
# Error minimum: 1
agent:
metadata:
id: empty-menu
name: Empty Menu
title: Empty Menu
icon:
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
menu: []

View File

@ -1,20 +0,0 @@
# Test: Missing menu field
# Expected: FAIL
# Error code: invalid_type
# Error path: agent.menu
# Error expected: array
agent:
metadata:
id: missing-menu
name: Missing Menu
title: Missing Menu
icon:
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle

View File

@ -1,26 +0,0 @@
# Test: Module field with whitespace only
# Expected: FAIL
# Error code: custom
# Error path: agent.metadata.module
# Error message: agent.metadata.module must be a non-empty string
# Path context: src/bmm/agents/empty-module-string.agent.yaml
agent:
metadata:
id: empty-module
name: Empty Module String
title: Empty Module
icon:
module: " "
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,24 +0,0 @@
# Test: Empty string in metadata.name field
# Expected: FAIL
# Error code: custom
# Error path: agent.metadata.name
# Error message: agent.metadata.name must be a non-empty string
agent:
metadata:
id: empty-name-test
name: " "
title: Empty Name Test
icon:
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,27 +0,0 @@
# Test: Extra unknown fields in metadata
# Expected: FAIL
# Error code: unrecognized_keys
# Error path: agent.metadata
# Error keys: ["unknown_field", "another_extra"]
agent:
metadata:
id: extra-fields
name: Extra Fields
title: Extra Fields
icon:
hasSidecar: false
unknown_field: This is not allowed
another_extra: Also invalid
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,23 +0,0 @@
# Test: Missing required metadata.id field
# Expected: FAIL
# Error code: invalid_type
# Error path: agent.metadata.id
# Error expected: string
agent:
metadata:
name: Missing ID Agent
title: Missing ID
icon:
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,24 +0,0 @@
# Test: Empty principles array
# Expected: FAIL
# Error code: too_small
# Error path: agent.persona.principles
# Error minimum: 1
agent:
metadata:
id: empty-principles
name: Empty Principles
title: Empty Principles
icon:
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles: []
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,27 +0,0 @@
# Test: Empty string in principles array
# Expected: FAIL
# Error code: custom
# Error path: agent.persona.principles[1]
# Error message: agent.persona.principles[] must be a non-empty string
agent:
metadata:
id: empty-principle-string
name: Empty Principle String
title: Empty Principle
icon:
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Valid principle
- " "
- Another valid principle
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,27 +0,0 @@
# Test: Extra unknown fields in persona
# Expected: FAIL
# Error code: unrecognized_keys
# Error path: agent.persona
# Error keys: ["extra_field", "another_extra"]
agent:
metadata:
id: extra-persona-fields
name: Extra Persona Fields
title: Extra Persona
icon:
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
extra_field: Not allowed
another_extra: Also invalid
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,24 +0,0 @@
# Test: Missing required persona.role field
# Expected: FAIL
# Error code: invalid_type
# Error path: agent.persona.role
# Error expected: string
agent:
metadata:
id: missing-role
name: Missing Role
title: Missing Role
icon:
hasSidecar: false
persona:
identity: Test identity
communication_style: Test style
principles:
- Test principle
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,29 +0,0 @@
# Test: Prompt with empty content string
# Expected: FAIL
# Error code: custom
# Error path: agent.prompts[0].content
# Error message: agent.prompts[].content must be a non-empty string
agent:
metadata:
id: empty-content
name: Empty Content
title: Empty Content
icon:
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
prompts:
- id: prompt1
content: " "
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,31 +0,0 @@
# Test: Extra unknown fields in prompts
# Expected: FAIL
# Error code: unrecognized_keys
# Error path: agent.prompts[0]
# Error keys: ["extra_field"]
agent:
metadata:
id: extra-prompt-fields
name: Extra Prompt Fields
title: Extra Fields
icon:
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
prompts:
- id: prompt1
content: Valid content
description: Valid description
extra_field: Not allowed
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,28 +0,0 @@
# Test: Prompt missing required content field
# Expected: FAIL
# Error code: invalid_type
# Error path: agent.prompts[0].content
# Error expected: string
agent:
metadata:
id: prompt-missing-content
name: Prompt Missing Content
title: Missing Content
icon:
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
prompts:
- id: prompt1
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,28 +0,0 @@
# Test: Prompt missing required id field
# Expected: FAIL
# Error code: invalid_type
# Error path: agent.prompts[0].id
# Error expected: string
agent:
metadata:
id: prompt-missing-id
name: Prompt Missing ID
title: Missing ID
icon:
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
prompts:
- content: Prompt without ID
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,5 +0,0 @@
# Test: Empty YAML file
# Expected: FAIL
# Error code: invalid_type
# Error path:
# Error expected: object

View File

@ -1,28 +0,0 @@
# Test: Extra top-level keys beyond 'agent'
# Expected: FAIL
# Error code: unrecognized_keys
# Error path:
# Error keys: ["extra_key", "another_extra"]
agent:
metadata:
id: extra-test
name: Extra Test Agent
title: Extra Test
icon: 🧪
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
menu:
- trigger: help
description: Show help
action: display_help
extra_key: This should not be allowed
another_extra: Also invalid

View File

@ -1,11 +0,0 @@
# Test: Missing required 'agent' top-level key
# Expected: FAIL
# Error code: invalid_type
# Error path: agent
# Error expected: object
metadata:
id: bad-test
name: Bad Test Agent
title: Bad Test
icon:

View File

@ -1,19 +0,0 @@
# Test: Invalid YAML structure with inconsistent indentation
# Expected: FAIL - YAML parse error
agent:
metadata:
id: invalid-indent
name: Invalid Indentation
title: Invalid
icon:
persona:
role: Test
identity: Test
communication_style: Test
principles:
- Test
menu:
- trigger: help
description: Help
action: help

View File

@ -1,18 +0,0 @@
# Test: Malformed YAML with syntax errors
# Expected: FAIL - YAML parse error
agent:
metadata:
id: malformed
name: Malformed YAML
title: [Malformed
icon: 🧪
persona:
role: Test
identity: Test
communication_style: Test
principles:
- Test
menu:
- trigger: help
description: Help

View File

@ -1,24 +0,0 @@
# Test: Empty critical_actions array
# Expected: PASS - empty array is valid for optional field
agent:
metadata:
id: empty-critical-actions
name: Empty Critical Actions
title: Empty Critical Actions
icon: 🧪
hasSidecar: false
persona:
role: Test agent with empty critical actions
identity: I am a test agent with empty critical actions array.
communication_style: Clear
principles:
- Test empty arrays
critical_actions: []
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,22 +0,0 @@
# Test: No critical_actions field (optional)
# Expected: PASS
agent:
metadata:
id: no-critical-actions
name: No Critical Actions
title: No Critical Actions
icon: 🧪
hasSidecar: false
persona:
role: Test agent without critical actions
identity: I am a test agent without critical actions.
communication_style: Clear
principles:
- Test optional fields
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,27 +0,0 @@
# Test: critical_actions with valid strings
# Expected: PASS
agent:
metadata:
id: valid-critical-actions
name: Valid Critical Actions
title: Valid Critical Actions
icon: 🧪
hasSidecar: false
persona:
role: Test agent with critical actions
identity: I am a test agent with valid critical actions.
communication_style: Clear
principles:
- Test valid arrays
critical_actions:
- Load configuration from disk
- Initialize user context
- Set communication preferences
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,38 +0,0 @@
# Test: Menu items with all valid command target types
# Expected: PASS
agent:
metadata:
id: all-commands
name: All Command Types
title: All Commands
icon: 🧪
hasSidecar: false
persona:
role: Test agent with all command types
identity: I test all available command target types.
communication_style: Clear
principles:
- Test all command types
menu:
- trigger: workflow-test
description: Test workflow command
exec: path/to/workflow
- trigger: validate-test
description: Test validate-workflow command
validate-workflow: path/to/validation
- trigger: exec-test
description: Test exec command
exec: npm test
- trigger: action-test
description: Test action command
action: perform_action
- trigger: tmpl-test
description: Test tmpl command
tmpl: path/to/template
- trigger: data-test
description: Test data command
data: path/to/data

View File

@ -1,23 +0,0 @@
# Test: Menu item with multiple command targets
# Expected: PASS - multiple targets are allowed
agent:
metadata:
id: multiple-commands
name: Multiple Commands
title: Multiple Commands
icon: 🧪
hasSidecar: false
persona:
role: Test agent with multiple command targets
identity: I test multiple command targets per menu item.
communication_style: Clear
principles:
- Test multiple targets
menu:
- trigger: multi-command
description: Menu item with multiple command targets
exec: path/to/workflow
action: perform_action

View File

@ -1,31 +0,0 @@
# Test: Valid compound triggers
# Expected: PASS
agent:
metadata:
id: compound-triggers
name: Compound Triggers
title: Compound Triggers Test
icon: 🧪
hasSidecar: false
persona:
role: Test agent with compound triggers
identity: I test compound trigger validation.
communication_style: Clear
principles:
- Test compound format
menu:
- trigger: TS or fuzzy match on tech-spec
description: "[TS] Two-word compound trigger"
action: tech_spec
- trigger: DS or fuzzy match on dev-story
description: "[DS] Another two-word compound trigger"
action: dev_story
- trigger: WI or fuzzy match on three-name-thing
description: "[WI] Three-word compound trigger (uses first 2 words for shortcut)"
action: three_name_thing
- trigger: H or fuzzy match on help
description: "[H] Single-word compound trigger (1-letter shortcut)"
action: help

View File

@ -1,34 +0,0 @@
# Test: Valid kebab-case triggers
# Expected: PASS
agent:
metadata:
id: kebab-triggers
name: Kebab Case Triggers
title: Kebab Triggers
icon: 🧪
hasSidecar: false
persona:
role: Test agent with kebab-case triggers
identity: I test kebab-case trigger validation.
communication_style: Clear
principles:
- Test kebab-case format
menu:
- trigger: help
description: Single word trigger
action: display_help
- trigger: list-tasks
description: Two word trigger
action: list_tasks
- trigger: three-word-process
description: Three word trigger
action: init_workflow
- trigger: test123
description: Trigger with numbers
action: test
- trigger: multi-word-kebab-case-trigger
description: Long kebab-case trigger
action: long_action

View File

@ -1,31 +0,0 @@
# Test: Menu with multiple valid items using different command types
# Expected: PASS
agent:
metadata:
id: multiple-menu
name: Multiple Menu Items
title: Multiple Menu
icon: 🧪
hasSidecar: false
persona:
role: Test agent with multiple menu items
identity: I am a test agent with diverse menu commands.
communication_style: Clear
principles:
- Test multiple menu items
menu:
- trigger: help
description: Show help
action: display_help
- trigger: start-workflow
description: Start a workflow
exec: path/to/workflow
- trigger: execute
description: Execute command
exec: npm test
- trigger: use-template
description: Use template
tmpl: path/to/template

View File

@ -1,22 +0,0 @@
# Test: Menu with single valid item
# Expected: PASS
agent:
metadata:
id: single-menu
name: Single Menu Item
title: Single Menu
icon: 🧪
hasSidecar: false
persona:
role: Test agent with single menu item
identity: I am a test agent.
communication_style: Clear
principles:
- Test minimal menu
menu:
- trigger: help
description: Show help information
action: display_help

View File

@ -1,24 +0,0 @@
# Test: Core agent can have module field
# Expected: PASS
# Note: Core agents can now include module field if needed
agent:
metadata:
id: core-with-module
name: Core With Module
title: Core Agent
icon:
module: bmm
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,24 +0,0 @@
# Test: Empty module name in path (src/modules//agents/)
# Expected: PASS - treated as core agent (empty module normalizes to null)
# Path context: src/modules//agents/test.agent.yaml
agent:
metadata:
id: empty-module-path
name: Empty Module in Path
title: Empty Module Path
icon: 🧪
hasSidecar: false
# No module field - path has empty module name, treated as core
persona:
role: Test agent for empty module name in path
identity: I test the edge case where module name in path is empty.
communication_style: Clear
principles:
- Test path parsing edge cases
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,24 +0,0 @@
# Test: Malformed module path (no slash after module name) treated as core
# Expected: PASS - malformed path returns null, treated as core agent
# Path context: src/bmm
agent:
metadata:
id: malformed-path
name: Malformed Path Test
title: Malformed Path
icon: 🧪
hasSidecar: false
# No module field - will be treated as core since path parsing returns null
persona:
role: Test agent for malformed path edge case
identity: I test edge cases in path parsing.
communication_style: Clear
principles:
- Test edge case handling
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,24 +0,0 @@
# Test: Valid module agent with correct module field
# Expected: PASS
# Path context: src/bmm/agents/module-agent-correct.agent.yaml
agent:
metadata:
id: bmm-test
name: BMM Test Agent
title: BMM Test
icon: 🧪
module: bmm
hasSidecar: false
persona:
role: Test module agent
identity: I am a module-scoped test agent.
communication_style: Professional
principles:
- Test module validation
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,23 +0,0 @@
# Test: Module agent can omit module field
# Expected: PASS
# Note: Module field is optional
agent:
metadata:
id: bmm-missing-module
name: No Module
title: Optional Module
icon:
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,24 +0,0 @@
# Test: Module agent can have any module value
# Expected: PASS
# Note: Module validation removed - agents can declare any module
agent:
metadata:
id: wrong-module
name: Any Module
title: Any Module Value
icon:
module: cis
hasSidecar: false
persona:
role: Test agent
identity: Test identity
communication_style: Test style
principles:
- Test principle
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,24 +0,0 @@
# Test: All persona fields properly filled
# Expected: PASS
agent:
metadata:
id: complete-persona
name: Complete Persona Agent
title: Complete Persona
icon: 🧪
hasSidecar: false
persona:
role: Comprehensive test agent with all persona fields
identity: I am a test agent designed to validate complete persona structure with multiple characteristics and attributes.
communication_style: Professional, clear, and thorough with attention to detail
principles:
- Validate all persona fields are present
- Ensure array fields work correctly
- Test comprehensive documentation
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,24 +0,0 @@
# Test: Empty prompts array
# Expected: PASS - empty array valid for optional field
agent:
metadata:
id: empty-prompts
name: Empty Prompts
title: Empty Prompts
icon: 🧪
hasSidecar: false
persona:
role: Test agent with empty prompts
identity: I am a test agent with empty prompts array.
communication_style: Clear
principles:
- Test empty arrays
prompts: []
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,22 +0,0 @@
# Test: No prompts field (optional)
# Expected: PASS
agent:
metadata:
id: no-prompts
name: No Prompts
title: No Prompts
icon: 🧪
hasSidecar: false
persona:
role: Test agent without prompts
identity: I am a test agent without prompts field.
communication_style: Clear
principles:
- Test optional fields
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,28 +0,0 @@
# Test: Prompts with required id and content only
# Expected: PASS
agent:
metadata:
id: valid-prompts-minimal
name: Valid Prompts Minimal
title: Valid Prompts
icon: 🧪
hasSidecar: false
persona:
role: Test agent with minimal prompts
identity: I am a test agent with minimal prompt structure.
communication_style: Clear
principles:
- Test minimal prompts
prompts:
- id: prompt1
content: This is a valid prompt content
- id: prompt2
content: Another valid prompt
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,30 +0,0 @@
# Test: Prompts with optional description field
# Expected: PASS
agent:
metadata:
id: valid-prompts-description
name: Valid Prompts With Description
title: Valid Prompts Desc
icon: 🧪
hasSidecar: false
persona:
role: Test agent with prompts including descriptions
identity: I am a test agent with complete prompt structure.
communication_style: Clear
principles:
- Test complete prompts
prompts:
- id: prompt1
content: This is a valid prompt content
description: This prompt does something useful
- id: prompt2
content: Another valid prompt
description: This prompt does something else
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,24 +0,0 @@
# Test: Valid core agent with only required fields
# Expected: PASS
# Path context: src/core/agents/minimal-core-agent.agent.yaml
agent:
metadata:
id: minimal-test
name: Minimal Test Agent
title: Minimal Test
icon: 🧪
hasSidecar: false
persona:
role: Test agent with minimal configuration
identity: I am a minimal test agent used for schema validation testing.
communication_style: Clear and concise
principles:
- Validate schema requirements
- Demonstrate minimal valid structure
menu:
- trigger: help
description: Show help
action: display_help

View File

@ -1,387 +0,0 @@
/**
* Agent Schema Validation Test Runner
*
* Runs all test fixtures and verifies expected outcomes.
* Reports pass/fail for each test and overall coverage statistics.
*
* Usage: node test/test-agent-schema.js
* Exit codes: 0 = all tests pass, 1 = test failures
*/
const fs = require('node:fs');
const path = require('node:path');
const yaml = require('yaml');
const { validateAgentFile } = require('../tools/schema/agent.js');
const { glob } = require('glob');
// ANSI color codes
const colors = {
reset: '\u001B[0m',
green: '\u001B[32m',
red: '\u001B[31m',
yellow: '\u001B[33m',
blue: '\u001B[34m',
cyan: '\u001B[36m',
dim: '\u001B[2m',
};
/**
* Parse test metadata from YAML comments
* @param {string} filePath
* @returns {{shouldPass: boolean, errorExpectation?: object, pathContext?: string}}
*/
function parseTestMetadata(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split('\n');
let shouldPass = true;
let pathContext = null;
const errorExpectation = {};
for (const line of lines) {
if (line.includes('Expected: PASS')) {
shouldPass = true;
} else if (line.includes('Expected: FAIL')) {
shouldPass = false;
}
// Parse error metadata
const codeMatch = line.match(/^# Error code: (.+)$/);
if (codeMatch) {
errorExpectation.code = codeMatch[1].trim();
}
const pathMatch = line.match(/^# Error path: (.+)$/);
if (pathMatch) {
errorExpectation.path = pathMatch[1].trim();
}
const messageMatch = line.match(/^# Error message: (.+)$/);
if (messageMatch) {
errorExpectation.message = messageMatch[1].trim();
}
const minimumMatch = line.match(/^# Error minimum: (\d+)$/);
if (minimumMatch) {
errorExpectation.minimum = parseInt(minimumMatch[1], 10);
}
const expectedMatch = line.match(/^# Error expected: (.+)$/);
if (expectedMatch) {
errorExpectation.expected = expectedMatch[1].trim();
}
const receivedMatch = line.match(/^# Error received: (.+)$/);
if (receivedMatch) {
errorExpectation.received = receivedMatch[1].trim();
}
const keysMatch = line.match(/^# Error keys: \[(.+)\]$/);
if (keysMatch) {
errorExpectation.keys = keysMatch[1].split(',').map((k) => k.trim().replaceAll(/['"]/g, ''));
}
const contextMatch = line.match(/^# Path context: (.+)$/);
if (contextMatch) {
pathContext = contextMatch[1].trim();
}
}
return {
shouldPass,
errorExpectation: Object.keys(errorExpectation).length > 0 ? errorExpectation : null,
pathContext,
};
}
/**
* Convert dot-notation path string to array (handles array indices)
* e.g., "agent.menu[0].trigger" => ["agent", "menu", 0, "trigger"]
*/
function parsePathString(pathString) {
return pathString
.replaceAll(/\[(\d+)\]/g, '.$1') // Convert [0] to .0
.split('.')
.map((part) => {
const num = parseInt(part, 10);
return isNaN(num) ? part : num;
});
}
/**
* Validate error against expectations
* @param {object} error - Zod error issue
* @param {object} expectation - Expected error structure
* @returns {{valid: boolean, reason?: string}}
*/
function validateError(error, expectation) {
// Check error code
if (expectation.code && error.code !== expectation.code) {
return { valid: false, reason: `Expected code "${expectation.code}", got "${error.code}"` };
}
// Check error path
if (expectation.path) {
const expectedPath = parsePathString(expectation.path);
const actualPath = error.path;
if (JSON.stringify(expectedPath) !== JSON.stringify(actualPath)) {
return {
valid: false,
reason: `Expected path ${JSON.stringify(expectedPath)}, got ${JSON.stringify(actualPath)}`,
};
}
}
// For custom errors, strictly check message
if (expectation.code === 'custom' && expectation.message && error.message !== expectation.message) {
return {
valid: false,
reason: `Expected message "${expectation.message}", got "${error.message}"`,
};
}
// For Zod errors, check type-specific fields
if (expectation.minimum !== undefined && error.minimum !== expectation.minimum) {
return { valid: false, reason: `Expected minimum ${expectation.minimum}, got ${error.minimum}` };
}
if (expectation.expected && error.expected !== expectation.expected) {
return { valid: false, reason: `Expected type "${expectation.expected}", got "${error.expected}"` };
}
if (expectation.received && error.received !== expectation.received) {
return { valid: false, reason: `Expected received "${expectation.received}", got "${error.received}"` };
}
if (expectation.keys) {
const expectedKeys = expectation.keys.sort();
const actualKeys = (error.keys || []).sort();
if (JSON.stringify(expectedKeys) !== JSON.stringify(actualKeys)) {
return {
valid: false,
reason: `Expected keys ${JSON.stringify(expectedKeys)}, got ${JSON.stringify(actualKeys)}`,
};
}
}
return { valid: true };
}
/**
* Run a single test case
* @param {string} filePath
* @returns {{passed: boolean, message: string}}
*/
function runTest(filePath) {
const metadata = parseTestMetadata(filePath);
const { shouldPass, errorExpectation, pathContext } = metadata;
try {
const fileContent = fs.readFileSync(filePath, 'utf8');
let agentData;
try {
agentData = yaml.parse(fileContent);
} catch (parseError) {
// YAML parse error
if (shouldPass) {
return {
passed: false,
message: `Expected PASS but got YAML parse error: ${parseError.message}`,
};
}
return {
passed: true,
message: 'Got expected YAML parse error',
};
}
// Determine validation path
// If pathContext is specified in comments, use it; otherwise derive from fixture location
let validationPath = pathContext;
if (!validationPath) {
// Map fixture location to simulated src/ path
const relativePath = path.relative(path.join(__dirname, 'fixtures/agent-schema'), filePath);
const parts = relativePath.split(path.sep);
if (parts.includes('metadata') && parts[0] === 'valid') {
// Valid metadata tests: check if filename suggests module or core
const filename = path.basename(filePath);
if (filename.includes('module')) {
validationPath = 'src/bmm/agents/test.agent.yaml';
} else {
validationPath = 'src/core/agents/test.agent.yaml';
}
} else if (parts.includes('metadata') && parts[0] === 'invalid') {
// Invalid metadata tests: derive from filename
const filename = path.basename(filePath);
if (filename.includes('module') || filename.includes('wrong-module')) {
validationPath = 'src/bmm/agents/test.agent.yaml';
} else if (filename.includes('core')) {
validationPath = 'src/core/agents/test.agent.yaml';
} else {
validationPath = 'src/core/agents/test.agent.yaml';
}
} else {
// Default to core agent path
validationPath = 'src/core/agents/test.agent.yaml';
}
}
const result = validateAgentFile(validationPath, agentData);
if (result.success && shouldPass) {
return {
passed: true,
message: 'Validation passed as expected',
};
}
if (!result.success && !shouldPass) {
const actualError = result.error.issues[0];
// If we have error expectations, validate strictly
if (errorExpectation) {
const validation = validateError(actualError, errorExpectation);
if (!validation.valid) {
return {
passed: false,
message: `Error validation failed: ${validation.reason}`,
};
}
return {
passed: true,
message: `Got expected error (${errorExpectation.code}): ${actualError.message}`,
};
}
// No specific expectations - just check that it failed
return {
passed: true,
message: `Got expected validation error: ${actualError?.message}`,
};
}
if (result.success && !shouldPass) {
return {
passed: false,
message: 'Expected validation to FAIL but it PASSED',
};
}
if (!result.success && shouldPass) {
return {
passed: false,
message: `Expected validation to PASS but it FAILED: ${result.error.issues[0]?.message}`,
};
}
return {
passed: false,
message: 'Unexpected test state',
};
} catch (error) {
return {
passed: false,
message: `Test execution error: ${error.message}`,
};
}
}
/**
* Main test runner
*/
async function main() {
console.log(`${colors.cyan}╔═══════════════════════════════════════════════════════════╗${colors.reset}`);
console.log(`${colors.cyan}║ Agent Schema Validation Test Suite ║${colors.reset}`);
console.log(`${colors.cyan}╚═══════════════════════════════════════════════════════════╝${colors.reset}\n`);
// Find all test fixtures
const testFiles = await glob('test/fixtures/agent-schema/**/*.agent.yaml', {
cwd: path.join(__dirname, '..'),
absolute: true,
});
if (testFiles.length === 0) {
console.log(`${colors.yellow}⚠️ No test fixtures found${colors.reset}`);
process.exit(0);
}
console.log(`Found ${colors.cyan}${testFiles.length}${colors.reset} test fixture(s)\n`);
// Group tests by category
const categories = {};
for (const testFile of testFiles) {
const relativePath = path.relative(path.join(__dirname, 'fixtures/agent-schema'), testFile);
const parts = relativePath.split(path.sep);
const validInvalid = parts[0]; // 'valid' or 'invalid'
const category = parts[1]; // 'top-level', 'metadata', etc.
const categoryKey = `${validInvalid}/${category}`;
if (!categories[categoryKey]) {
categories[categoryKey] = [];
}
categories[categoryKey].push(testFile);
}
// Run tests by category
let totalTests = 0;
let passedTests = 0;
const failures = [];
for (const [categoryKey, files] of Object.entries(categories).sort()) {
const [validInvalid, category] = categoryKey.split('/');
const categoryLabel = category.replaceAll('-', ' ').toUpperCase();
const validLabel = validInvalid === 'valid' ? '✅' : '❌';
console.log(`${colors.blue}${validLabel} ${categoryLabel} (${validInvalid})${colors.reset}`);
for (const testFile of files) {
totalTests++;
const testName = path.basename(testFile, '.agent.yaml');
const result = runTest(testFile);
if (result.passed) {
passedTests++;
console.log(` ${colors.green}${colors.reset} ${testName} ${colors.dim}${result.message}${colors.reset}`);
} else {
console.log(` ${colors.red}${colors.reset} ${testName} ${colors.red}${result.message}${colors.reset}`);
failures.push({
file: path.relative(process.cwd(), testFile),
message: result.message,
});
}
}
console.log('');
}
// Summary
console.log(`${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}`);
console.log(`${colors.cyan}Test Results:${colors.reset}`);
console.log(` Total: ${totalTests}`);
console.log(` Passed: ${colors.green}${passedTests}${colors.reset}`);
console.log(` Failed: ${passedTests === totalTests ? colors.green : colors.red}${totalTests - passedTests}${colors.reset}`);
console.log(`${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}\n`);
// Report failures
if (failures.length > 0) {
console.log(`${colors.red}❌ FAILED TESTS:${colors.reset}\n`);
for (const failure of failures) {
console.log(`${colors.red}${colors.reset} ${failure.file}`);
console.log(` ${failure.message}\n`);
}
process.exit(1);
}
console.log(`${colors.green}✨ All tests passed!${colors.reset}\n`);
process.exit(0);
}
// Run
main().catch((error) => {
console.error(`${colors.red}Fatal error:${colors.reset}`, error);
process.exit(1);
});

View File

@ -1,159 +0,0 @@
#!/bin/bash
# CLI Integration Tests for Agent Schema Validator
# Tests the CLI wrapper (tools/validate-agent-schema.js) behavior and error handling
# NOTE: Tests CLI functionality using temporary test fixtures
echo "========================================"
echo "CLI Integration Tests"
echo "========================================"
echo ""
# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
PASSED=0
FAILED=0
# Get the repo root (assuming script is in test/ directory)
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
# Create temp directory for test fixtures
TEMP_DIR=$(mktemp -d)
cleanup() {
rm -rf "$TEMP_DIR"
}
trap cleanup EXIT
# Test 1: CLI fails when no files found (exit 1)
echo "Test 1: CLI fails when no agent files found (should exit 1)"
mkdir -p "$TEMP_DIR/empty/src/core/agents"
OUTPUT=$(node "$REPO_ROOT/tools/validate-agent-schema.js" "$TEMP_DIR/empty" 2>&1)
EXIT_CODE=$?
if [ $EXIT_CODE -eq 1 ] && echo "$OUTPUT" | grep -q "No agent files found"; then
echo -e "${GREEN}${NC} CLI fails correctly when no files found (exit 1)"
PASSED=$((PASSED + 1))
else
echo -e "${RED}${NC} CLI failed to handle no files properly (exit code: $EXIT_CODE)"
FAILED=$((FAILED + 1))
fi
echo ""
# Test 2: CLI reports validation errors with exit code 1
echo "Test 2: CLI reports validation errors (should exit 1)"
mkdir -p "$TEMP_DIR/invalid/src/core/agents"
cat > "$TEMP_DIR/invalid/src/core/agents/bad.agent.yaml" << 'EOF'
agent:
metadata:
id: bad
name: Bad
title: Bad
icon: 🧪
persona:
role: Test
identity: Test
communication_style: Test
principles: []
menu: []
EOF
OUTPUT=$(node "$REPO_ROOT/tools/validate-agent-schema.js" "$TEMP_DIR/invalid" 2>&1)
EXIT_CODE=$?
if [ $EXIT_CODE -eq 1 ] && echo "$OUTPUT" | grep -q "failed validation"; then
echo -e "${GREEN}${NC} CLI reports errors correctly (exit 1)"
PASSED=$((PASSED + 1))
else
echo -e "${RED}${NC} CLI failed to report errors (exit code: $EXIT_CODE)"
FAILED=$((FAILED + 1))
fi
echo ""
# Test 3: CLI discovers and counts agent files correctly
echo "Test 3: CLI discovers and counts agent files"
mkdir -p "$TEMP_DIR/valid/src/core/agents"
cat > "$TEMP_DIR/valid/src/core/agents/test1.agent.yaml" << 'EOF'
agent:
metadata:
id: test1
name: Test1
title: Test1
icon: 🧪
persona:
role: Test
identity: Test
communication_style: Test
principles: [Test]
menu:
- trigger: help
description: Help
action: help
EOF
cat > "$TEMP_DIR/valid/src/core/agents/test2.agent.yaml" << 'EOF'
agent:
metadata:
id: test2
name: Test2
title: Test2
icon: 🧪
persona:
role: Test
identity: Test
communication_style: Test
principles: [Test]
menu:
- trigger: help
description: Help
action: help
EOF
OUTPUT=$(node "$REPO_ROOT/tools/validate-agent-schema.js" "$TEMP_DIR/valid" 2>&1)
EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ] && echo "$OUTPUT" | grep -q "Found 2 agent file"; then
echo -e "${GREEN}${NC} CLI discovers and counts files correctly"
PASSED=$((PASSED + 1))
else
echo -e "${RED}${NC} CLI file discovery failed"
echo "Output: $OUTPUT"
FAILED=$((FAILED + 1))
fi
echo ""
# Test 4: CLI provides detailed error messages
echo "Test 4: CLI provides detailed error messages"
OUTPUT=$(node "$REPO_ROOT/tools/validate-agent-schema.js" "$TEMP_DIR/invalid" 2>&1)
if echo "$OUTPUT" | grep -q "Path:" && echo "$OUTPUT" | grep -q "Error:"; then
echo -e "${GREEN}${NC} CLI provides error details (Path and Error)"
PASSED=$((PASSED + 1))
else
echo -e "${RED}${NC} CLI error details missing"
FAILED=$((FAILED + 1))
fi
echo ""
# Test 5: CLI validates real BMAD agents (smoke test)
echo "Test 5: CLI validates actual BMAD agents (smoke test)"
OUTPUT=$(node "$REPO_ROOT/tools/validate-agent-schema.js" 2>&1)
EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ] && echo "$OUTPUT" | grep -qE "Found [0-9]+ agent file"; then
echo -e "${GREEN}${NC} CLI validates real BMAD agents successfully"
PASSED=$((PASSED + 1))
else
echo -e "${RED}${NC} CLI failed on real BMAD agents (exit code: $EXIT_CODE)"
FAILED=$((FAILED + 1))
fi
echo ""
# Summary
echo "========================================"
echo "Test Results:"
echo " Passed: ${GREEN}$PASSED${NC}"
echo " Failed: ${RED}$FAILED${NC}"
echo "========================================"
if [ $FAILED -eq 0 ]; then
echo -e "\n${GREEN}✨ All CLI integration tests passed!${NC}\n"
exit 0
else
echo -e "\n${RED}❌ Some CLI integration tests failed${NC}\n"
exit 1
fi

View File

@ -1,133 +0,0 @@
/**
* Unit Tests for Agent Schema Edge Cases
*
* Tests internal functions to achieve 100% branch coverage
*/
const { validateAgentFile } = require('../tools/schema/agent.js');
console.log('Running edge case unit tests...\n');
let passed = 0;
let failed = 0;
// Test 1: Path with malformed module structure (no slash after module name)
// This tests line 213: slashIndex === -1
console.log('Test 1: Malformed module path (no slash after module name)');
try {
const result = validateAgentFile('src/bmm', {
agent: {
metadata: {
id: 'test',
name: 'Test',
title: 'Test',
icon: '🧪',
},
persona: {
role: 'Test',
identity: 'Test',
communication_style: 'Test',
principles: ['Test'],
},
menu: [{ trigger: 'help', description: 'Help', action: 'help' }],
},
});
if (result.success) {
console.log('✗ Should have failed - missing module field');
failed++;
} else {
console.log('✓ Correctly handled malformed path (treated as core agent)');
passed++;
}
} catch (error) {
console.log('✗ Unexpected error:', error.message);
failed++;
}
console.log('');
// Test 2: Module option with empty string
// This tests line 222: trimmed.length > 0
console.log('Test 2: Module agent with empty string in module field');
try {
const result = validateAgentFile('src/bmm/agents/test.agent.yaml', {
agent: {
metadata: {
id: 'test',
name: 'Test',
title: 'Test',
icon: '🧪',
module: ' ', // Empty after trimming
},
persona: {
role: 'Test',
identity: 'Test',
communication_style: 'Test',
principles: ['Test'],
},
menu: [{ trigger: 'help', description: 'Help', action: 'help' }],
},
});
if (result.success) {
console.log('✗ Should have failed - empty module string');
failed++;
} else {
console.log('✓ Correctly rejected empty module string');
passed++;
}
} catch (error) {
console.log('✗ Unexpected error:', error.message);
failed++;
}
console.log('');
// Test 3: Core agent path (src/core/agents/...) - tests the !filePath.startsWith(marker) branch
console.log('Test 3: Core agent path returns null for module');
try {
const result = validateAgentFile('src/core/agents/test.agent.yaml', {
agent: {
metadata: {
id: 'test',
name: 'Test',
title: 'Test',
icon: '🧪',
// No module field - correct for core agent
},
persona: {
role: 'Test',
identity: 'Test',
communication_style: 'Test',
principles: ['Test'],
},
menu: [{ trigger: 'help', description: 'Help', action: 'help' }],
},
});
if (result.success) {
console.log('✓ Core agent validated correctly (no module required)');
passed++;
} else {
console.log('✗ Core agent should pass without module field');
failed++;
}
} catch (error) {
console.log('✗ Unexpected error:', error.message);
failed++;
}
console.log('');
// Summary
console.log('═══════════════════════════════════════');
console.log('Edge Case Unit Test Results:');
console.log(` Passed: ${passed}`);
console.log(` Failed: ${failed}`);
console.log('═══════════════════════════════════════\n');
if (failed === 0) {
console.log('✨ All edge case tests passed!\n');
process.exit(0);
} else {
console.log('❌ Some edge case tests failed\n');
process.exit(1);
}

View File

@ -1,489 +0,0 @@
// Zod schema definition for *.agent.yaml files
const assert = require('node:assert');
const { z } = require('zod');
const COMMAND_TARGET_KEYS = ['validate-workflow', 'exec', 'action', 'tmpl', 'data'];
const TRIGGER_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
const COMPOUND_TRIGGER_PATTERN = /^([A-Z]{1,3}) or fuzzy match on ([a-z0-9]+(?:-[a-z0-9]+)*)$/;
/**
* Derive the expected shortcut from a kebab-case trigger.
* - Single word: first letter (e.g., "help" "H")
* - Multi-word: first letter of first two words (e.g., "tech-spec" "TS")
* @param {string} kebabTrigger The kebab-case trigger name.
* @returns {string} The expected uppercase shortcut.
*/
function deriveShortcutFromKebab(kebabTrigger) {
const words = kebabTrigger.split('-');
if (words.length === 1) {
return words[0][0].toUpperCase();
}
return (words[0][0] + words[1][0]).toUpperCase();
}
/**
* Parse and validate a compound trigger string.
* Format: "<SHORTCUT> or fuzzy match on <kebab-case>"
* @param {string} triggerValue The trigger string to parse.
* @returns {{ valid: boolean, shortcut?: string, kebabTrigger?: string, error?: string }}
*/
function parseCompoundTrigger(triggerValue) {
const match = COMPOUND_TRIGGER_PATTERN.exec(triggerValue);
if (!match) {
return { valid: false, error: 'invalid compound trigger format' };
}
const [, shortcut, kebabTrigger] = match;
return { valid: true, shortcut, kebabTrigger };
}
// Public API ---------------------------------------------------------------
/**
* Validate an agent YAML payload against the schema derived from its file location.
* Exposed as the single public entry point, so callers do not reach into schema internals.
*
* @param {string} filePath Path to the agent file (used to infer module scope).
* @param {unknown} agentYaml Parsed YAML content.
* @returns {import('zod').SafeParseReturnType<unknown, unknown>} SafeParse result.
*/
function validateAgentFile(filePath, agentYaml) {
const expectedModule = deriveModuleFromPath(filePath);
const schema = agentSchema({ module: expectedModule });
return schema.safeParse(agentYaml);
}
module.exports = { validateAgentFile };
// Internal helpers ---------------------------------------------------------
/**
* Build a Zod schema for validating a single agent definition.
* The schema is generated per call so module-scoped agents can pass their expected
* module slug while core agents leave it undefined.
*
* @param {Object} [options]
* @param {string|null|undefined} [options.module] Module slug for module agents; omit or null for core agents.
* @returns {import('zod').ZodSchema} Configured Zod schema instance.
*/
function agentSchema(options = {}) {
const expectedModule = normalizeModuleOption(options.module);
return (
z
.object({
agent: buildAgentSchema(expectedModule),
})
.strict()
// Refinement: enforce trigger format and uniqueness rules after structural checks.
.superRefine((value, ctx) => {
const seenTriggers = new Set();
let index = 0;
for (const item of value.agent.menu) {
// Handle legacy format with trigger field
if (item.trigger) {
const triggerValue = item.trigger;
let canonicalTrigger = triggerValue;
// Check if it's a compound trigger (contains " or ")
if (triggerValue.includes(' or ')) {
const result = parseCompoundTrigger(triggerValue);
if (!result.valid) {
ctx.addIssue({
code: 'custom',
path: ['agent', 'menu', index, 'trigger'],
message: `agent.menu[].trigger compound format error: ${result.error}`,
});
return;
}
// Validate that shortcut matches description brackets
const descriptionMatch = item.description?.match(/^\[([A-Z]{1,3})\]/);
if (!descriptionMatch) {
ctx.addIssue({
code: 'custom',
path: ['agent', 'menu', index, 'description'],
message: `agent.menu[].description must start with [SHORTCUT] where SHORTCUT matches the trigger shortcut "${result.shortcut}"`,
});
return;
}
const descriptionShortcut = descriptionMatch[1];
if (descriptionShortcut !== result.shortcut) {
ctx.addIssue({
code: 'custom',
path: ['agent', 'menu', index, 'description'],
message: `agent.menu[].description shortcut "[${descriptionShortcut}]" must match trigger shortcut "${result.shortcut}"`,
});
return;
}
canonicalTrigger = result.kebabTrigger;
} else if (!TRIGGER_PATTERN.test(triggerValue)) {
ctx.addIssue({
code: 'custom',
path: ['agent', 'menu', index, 'trigger'],
message: 'agent.menu[].trigger must be kebab-case (lowercase words separated by hyphen)',
});
return;
}
if (seenTriggers.has(canonicalTrigger)) {
ctx.addIssue({
code: 'custom',
path: ['agent', 'menu', index, 'trigger'],
message: `agent.menu[].trigger duplicates "${canonicalTrigger}" within the same agent`,
});
return;
}
seenTriggers.add(canonicalTrigger);
}
// Handle multi format with triggers array (new format)
else if (item.triggers && Array.isArray(item.triggers)) {
for (const [triggerIndex, triggerItem] of item.triggers.entries()) {
let triggerName = null;
// Extract trigger name from all three formats
if (triggerItem.trigger) {
// Format 1: Simple flat format with trigger field
triggerName = triggerItem.trigger;
} else {
// Format 2a or 2b: Object-key format
const keys = Object.keys(triggerItem);
if (keys.length === 1 && keys[0] !== 'trigger') {
triggerName = keys[0];
}
}
if (triggerName) {
if (!TRIGGER_PATTERN.test(triggerName)) {
ctx.addIssue({
code: 'custom',
path: ['agent', 'menu', index, 'triggers', triggerIndex],
message: `agent.menu[].triggers[] must be kebab-case (lowercase words separated by hyphen) - got "${triggerName}"`,
});
return;
}
if (seenTriggers.has(triggerName)) {
ctx.addIssue({
code: 'custom',
path: ['agent', 'menu', index, 'triggers', triggerIndex],
message: `agent.menu[].triggers[] duplicates "${triggerName}" within the same agent`,
});
return;
}
seenTriggers.add(triggerName);
}
}
}
index += 1;
}
})
// Refinement: suggest conversational_knowledge when discussion is true
.superRefine((value, ctx) => {
if (value.agent.discussion === true && !value.agent.conversational_knowledge) {
ctx.addIssue({
code: 'custom',
path: ['agent', 'conversational_knowledge'],
message: 'It is recommended to include conversational_knowledge when discussion is true',
});
}
})
);
}
/**
* Assemble the full agent schema using the module expectation provided by the caller.
* @param {string|null} expectedModule Trimmed module slug or null for core agents.
*/
function buildAgentSchema(expectedModule) {
return z
.object({
metadata: buildMetadataSchema(expectedModule),
persona: buildPersonaSchema(),
critical_actions: z.array(createNonEmptyString('agent.critical_actions[]')).optional(),
menu: z.array(buildMenuItemSchema()).min(1, { message: 'agent.menu must include at least one entry' }),
prompts: z.array(buildPromptSchema()).optional(),
discussion: z.boolean().optional(),
conversational_knowledge: z.array(z.object({}).passthrough()).min(1).optional(),
})
.strict();
}
/**
* Validate metadata shape.
* @param {string|null} expectedModule Trimmed module slug or null when core agent metadata is expected.
* Note: Module field is optional and can be any value - no validation against path.
*/
function buildMetadataSchema(expectedModule) {
const schemaShape = {
id: createNonEmptyString('agent.metadata.id'),
name: createNonEmptyString('agent.metadata.name'),
title: createNonEmptyString('agent.metadata.title'),
icon: createNonEmptyString('agent.metadata.icon'),
module: createNonEmptyString('agent.metadata.module').optional(),
capabilities: createNonEmptyString('agent.metadata.capabilities').optional(),
hasSidecar: z.boolean(),
};
return z.object(schemaShape).strict();
}
function buildPersonaSchema() {
return z
.object({
role: createNonEmptyString('agent.persona.role'),
identity: createNonEmptyString('agent.persona.identity'),
communication_style: createNonEmptyString('agent.persona.communication_style'),
principles: z.union([
createNonEmptyString('agent.persona.principles'),
z
.array(createNonEmptyString('agent.persona.principles[]'))
.min(1, { message: 'agent.persona.principles must include at least one entry' }),
]),
})
.strict();
}
function buildPromptSchema() {
return z
.object({
id: createNonEmptyString('agent.prompts[].id'),
content: z.string().refine((value) => value.trim().length > 0, {
message: 'agent.prompts[].content must be a non-empty string',
}),
description: createNonEmptyString('agent.prompts[].description').optional(),
})
.strict();
}
/**
* Schema for individual menu entries ensuring they are actionable.
* Supports both legacy format and new multi format.
*/
function buildMenuItemSchema() {
// Legacy menu item format
const legacyMenuItemSchema = z
.object({
trigger: createNonEmptyString('agent.menu[].trigger'),
description: createNonEmptyString('agent.menu[].description'),
'validate-workflow': createNonEmptyString('agent.menu[].validate-workflow').optional(),
exec: createNonEmptyString('agent.menu[].exec').optional(),
action: createNonEmptyString('agent.menu[].action').optional(),
tmpl: createNonEmptyString('agent.menu[].tmpl').optional(),
data: z.string().optional(),
checklist: createNonEmptyString('agent.menu[].checklist').optional(),
document: createNonEmptyString('agent.menu[].document').optional(),
'ide-only': z.boolean().optional(),
'web-only': z.boolean().optional(),
discussion: z.boolean().optional(),
})
.strict()
.superRefine((value, ctx) => {
const hasCommandTarget = COMMAND_TARGET_KEYS.some((key) => {
const commandValue = value[key];
return typeof commandValue === 'string' && commandValue.trim().length > 0;
});
if (!hasCommandTarget) {
ctx.addIssue({
code: 'custom',
message: 'agent.menu[] entries must include at least one command target field',
});
}
});
// Multi menu item format
const multiMenuItemSchema = z
.object({
multi: createNonEmptyString('agent.menu[].multi'),
triggers: z
.array(
z.union([
// Format 1: Simple flat format (has trigger field)
z
.object({
trigger: z.string(),
input: createNonEmptyString('agent.menu[].triggers[].input'),
route: createNonEmptyString('agent.menu[].triggers[].route').optional(),
action: createNonEmptyString('agent.menu[].triggers[].action').optional(),
data: z.string().optional(),
type: z.enum(['exec', 'action', 'workflow']).optional(),
})
.strict()
.refine((data) => data.trigger, { message: 'Must have trigger field' })
.superRefine((value, ctx) => {
// Must have either route or action (or both)
if (!value.route && !value.action) {
ctx.addIssue({
code: 'custom',
message: 'agent.menu[].triggers[] must have either route or action (or both)',
});
}
}),
// Format 2a: Object with array format (like bmad-builder.agent.yaml)
z
.object({})
.passthrough()
.refine(
(value) => {
const keys = Object.keys(value);
if (keys.length !== 1) return false;
const triggerItems = value[keys[0]];
return Array.isArray(triggerItems);
},
{ message: 'Must be object with single key pointing to array' },
)
.superRefine((value, ctx) => {
const triggerName = Object.keys(value)[0];
const triggerItems = value[triggerName];
if (!Array.isArray(triggerItems)) {
ctx.addIssue({
code: 'custom',
message: `Trigger "${triggerName}" must be an array of items`,
});
return;
}
// Check required fields in the array
const hasInput = triggerItems.some((item) => 'input' in item);
const hasRouteOrAction = triggerItems.some((item) => 'route' in item || 'action' in item);
if (!hasInput) {
ctx.addIssue({
code: 'custom',
message: `Trigger "${triggerName}" must have an input field`,
});
}
if (!hasRouteOrAction) {
ctx.addIssue({
code: 'custom',
message: `Trigger "${triggerName}" must have a route or action field`,
});
}
}),
// Format 2b: Object with direct fields (like analyst.agent.yaml)
z
.object({})
.passthrough()
.refine(
(value) => {
const keys = Object.keys(value);
if (keys.length !== 1) return false;
const triggerFields = value[keys[0]];
return !Array.isArray(triggerFields) && typeof triggerFields === 'object';
},
{ message: 'Must be object with single key pointing to object' },
)
.superRefine((value, ctx) => {
const triggerName = Object.keys(value)[0];
const triggerFields = value[triggerName];
// Check required fields
if (!triggerFields.input || typeof triggerFields.input !== 'string') {
ctx.addIssue({
code: 'custom',
message: `Trigger "${triggerName}" must have an input field`,
});
}
if (!triggerFields.route && !triggerFields.action) {
ctx.addIssue({
code: 'custom',
message: `Trigger "${triggerName}" must have a route or action field`,
});
}
}),
]),
)
.min(1, { message: 'agent.menu[].triggers must have at least one trigger' }),
discussion: z.boolean().optional(),
})
.strict()
.superRefine((value, ctx) => {
// Check for duplicate trigger names
const seenTriggers = new Set();
for (const [index, triggerItem] of value.triggers.entries()) {
let triggerName = null;
// Extract trigger name from either format
if (triggerItem.trigger) {
// Format 1
triggerName = triggerItem.trigger;
} else {
// Format 2
const keys = Object.keys(triggerItem);
if (keys.length === 1) {
triggerName = keys[0];
}
}
if (triggerName) {
if (seenTriggers.has(triggerName)) {
ctx.addIssue({
code: 'custom',
path: ['agent', 'menu', 'triggers', index],
message: `Trigger name "${triggerName}" is duplicated`,
});
}
seenTriggers.add(triggerName);
// Validate trigger name format
if (!TRIGGER_PATTERN.test(triggerName)) {
ctx.addIssue({
code: 'custom',
path: ['agent', 'menu', 'triggers', index],
message: `Trigger name "${triggerName}" must be kebab-case (lowercase words separated by hyphen)`,
});
}
}
}
});
return z.union([legacyMenuItemSchema, multiMenuItemSchema]);
}
/**
* Derive the expected module slug from a file path residing under src/<module>/agents/.
* @param {string} filePath Absolute or relative agent path.
* @returns {string|null} Module slug if identifiable, otherwise null.
*/
function deriveModuleFromPath(filePath) {
assert(filePath, 'validateAgentFile expects filePath to be provided');
assert(typeof filePath === 'string', 'validateAgentFile expects filePath to be a string');
assert(filePath.startsWith('src/'), 'validateAgentFile expects filePath to start with "src/"');
const marker = 'src/';
if (!filePath.startsWith(marker)) {
return null;
}
const remainder = filePath.slice(marker.length);
const slashIndex = remainder.indexOf('/');
return slashIndex === -1 ? null : remainder.slice(0, slashIndex);
}
function normalizeModuleOption(moduleOption) {
if (typeof moduleOption !== 'string') {
return null;
}
const trimmed = moduleOption.trim();
return trimmed.length > 0 ? trimmed : null;
}
// Primitive validators -----------------------------------------------------
function createNonEmptyString(label) {
return z.string().refine((value) => value.trim().length > 0, {
message: `${label} must be a non-empty string`,
});
}

View File

@ -1,110 +0,0 @@
/**
* 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('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,bmm}/agents/**/*.agent.yaml', {
cwd: project_root,
absolute: true,
});
if (agentFiles.length === 0) {
console.log(' No *.agent.yaml files found — agents may use the new SKILL.md format.');
console.log(' Skipping legacy agent schema validation.\n');
process.exit(0);
}
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.parse(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);
});