Skip to main content

scripts-validate-agent-structure

#!/usr/bin/env python3 """

title: "Required fields for valid agent structure" component_type: script version: "1.0.0" audience: contributor status: stable summary: "Agent Structure Validator Validates that all agent files have proper YAML frontmatter structu..." keywords: ['agent', 'structure', 'validate', 'validation'] tokens: ~500 created: 2025-12-22 updated: 2025-12-22 script_name: "validate-agent-structure.py" language: python executable: true usage: "python3 scripts/validate-agent-structure.py [options]" python_version: "3.10+" dependencies: [] modifies_files: false network_access: false requires_auth: false

Agent Structure Validator Validates that all agent files have proper YAML frontmatter structure for capability extraction and dynamic routing.

Required frontmatter fields:

  • name: Agent identifier
  • description: Clear description of agent capabilities
  • tools: List of tools the agent can use

Optional but recommended:

  • model: LLM model to use
  • capabilities: Explicit capability declarations
  • auto_trigger_integration: For auto-trigger framework

Usage: python scripts/validate-agent-structure.py [--fix] [--verbose] """

import json import os import re import sys from pathlib import Path from typing import Dict, List, Tuple, Any import yaml

Required fields for valid agent structure

REQUIRED_FIELDS = ['name', 'description'] RECOMMENDED_FIELDS = ['tools', 'model'] OPTIONAL_FIELDS = ['capabilities', 'auto_trigger_integration', 'version', 'author']

def extract_yaml_frontmatter(content: str) -> Tuple[Dict[str, Any], str, bool]: """ Extract YAML frontmatter from markdown file. Returns: (frontmatter_dict, error_message, is_valid) """ if not content.strip(): return {}, "File is empty", False

if not content.startswith('---'):
return {}, "Missing YAML frontmatter (should start with ---)", False

try:
# Find closing ---
lines = content.split('\n')
end_idx = None
for i, line in enumerate(lines[1:], 1):
if line.strip() == '---':
end_idx = i
break

if end_idx is None:
return {}, "Missing closing --- for YAML frontmatter", False

yaml_content = '\n'.join(lines[1:end_idx])
frontmatter = yaml.safe_load(yaml_content)

if frontmatter is None:
return {}, "Empty YAML frontmatter", False

if not isinstance(frontmatter, dict):
return {}, f"YAML frontmatter is not a dictionary (got {type(frontmatter).__name__})", False

return frontmatter, "", True

except yaml.YAMLError as e:
return {}, f"YAML parse error: {e}", False

def validate_agent_file(file_path: Path) -> Dict[str, Any]: """ Validate a single agent file and return validation results. """ result = { "file": file_path.name, "valid": True, "errors": [], "warnings": [], "suggestions": [], "frontmatter": {} }

try:
content = file_path.read_text(encoding='utf-8')
except Exception as e:
result["valid"] = False
result["errors"].append(f"Cannot read file: {e}")
return result

# Extract frontmatter
frontmatter, error, is_valid = extract_yaml_frontmatter(content)
result["frontmatter"] = frontmatter

if not is_valid:
result["valid"] = False
result["errors"].append(error)
return result

# Check required fields
for field in REQUIRED_FIELDS:
if field not in frontmatter:
result["valid"] = False
result["errors"].append(f"Missing required field: {field}")
elif not frontmatter[field]:
result["valid"] = False
result["errors"].append(f"Required field '{field}' is empty")

# Check recommended fields
for field in RECOMMENDED_FIELDS:
if field not in frontmatter:
result["warnings"].append(f"Missing recommended field: {field}")
elif not frontmatter[field]:
result["warnings"].append(f"Recommended field '{field}' is empty")

# Validate field content
if 'name' in frontmatter:
name = frontmatter['name']
expected_name = file_path.stem
if name != expected_name:
result["warnings"].append(f"Name '{name}' doesn't match filename '{expected_name}'")

if 'description' in frontmatter:
desc = frontmatter['description']
if len(desc) < 20:
result["warnings"].append(f"Description is too short ({len(desc)} chars) - should be descriptive")
if len(desc) > 500:
result["warnings"].append(f"Description is very long ({len(desc)} chars) - consider shortening")

if 'tools' in frontmatter:
tools = frontmatter['tools']
if isinstance(tools, str) and not tools.strip():
result["warnings"].append("Tools field is empty string")
elif isinstance(tools, list) and len(tools) == 0:
result["warnings"].append("Tools list is empty")

# Check for capability hints
if 'capabilities' not in frontmatter:
result["suggestions"].append("Consider adding 'capabilities' field for better routing")

if 'auto_trigger_integration' not in frontmatter:
result["suggestions"].append("Consider adding 'auto_trigger_integration' for event-based activation")

# Check for common issues
desc = frontmatter.get('description', '')
if desc and desc == frontmatter.get('name', ''):
result["warnings"].append("Description is identical to name - should be more descriptive")

return result

def scan_agents(agents_dir: Path, verbose: bool = False) -> List[Dict]: """Scan all agent files and validate their structure.""" results = []

if not agents_dir.exists():
print(f"Error: Agents directory not found: {agents_dir}")
return results

agent_files = sorted([f for f in agents_dir.glob("*.md") if f.name.lower() != "readme.md"])
print(f"Found {len(agent_files)} agent files to validate\n")

for agent_file in agent_files:
result = validate_agent_file(agent_file)
results.append(result)

if verbose or not result["valid"]:
status = "PASS" if result["valid"] else "FAIL"
print(f"[{status}] {result['file']}")
for error in result["errors"]:
print(f" ERROR: {error}")
if verbose:
for warning in result["warnings"]:
print(f" WARN: {warning}")
for suggestion in result["suggestions"]:
print(f" HINT: {suggestion}")

return results

def generate_fix_template(result: Dict) -> str: """Generate a fix template for an invalid agent file.""" name = result["file"].replace(".md", "") frontmatter = result.get("frontmatter", {})

template = f"""---

name: {frontmatter.get('name', name)} description: {frontmatter.get('description', 'TODO: Add description of agent capabilities')} tools: {frontmatter.get('tools', 'Read, Write, Edit, Bash, Grep, Glob, TodoWrite')} model: {frontmatter.get('model', 'sonnet')}

[Agent content below...] """ return template

def main(): import argparse

parser = argparse.ArgumentParser(description="Validate agent file structure")
parser.add_argument("--verbose", "-v", action="store_true", help="Show all results including passes")
parser.add_argument("--fix", action="store_true", help="Generate fix templates for invalid files")
parser.add_argument("--json", action="store_true", help="Output results as JSON")
args = parser.parse_args()

# Determine project root
script_dir = Path(__file__).parent
project_root = script_dir.parent
agents_dir = project_root / "agents"

print("=" * 60)
print("CODITECT Agent Structure Validator")
print("=" * 60)
print(f"Agents directory: {agents_dir}\n")

results = scan_agents(agents_dir, args.verbose)

# Summary
valid_count = sum(1 for r in results if r["valid"])
invalid_count = len(results) - valid_count
warning_count = sum(len(r["warnings"]) for r in results)

print("\n" + "=" * 60)
print("VALIDATION SUMMARY")
print("=" * 60)
print(f"Total agents: {len(results)}")
print(f"Valid: {valid_count} ({100*valid_count/len(results):.1f}%)")
print(f"Invalid: {invalid_count}")
print(f"Warnings: {warning_count}")

# Show invalid files
if invalid_count > 0:
print("\n" + "-" * 60)
print("INVALID FILES:")
print("-" * 60)
for r in results:
if not r["valid"]:
print(f"\n{r['file']}:")
for error in r["errors"]:
print(f" - {error}")

if args.fix:
print("\n Suggested fix template:")
print(" " + "-" * 40)
for line in generate_fix_template(r).split('\n'):
print(f" {line}")

# Show most common warnings
if warning_count > 0 and args.verbose:
print("\n" + "-" * 60)
print("COMMON WARNINGS:")
print("-" * 60)
warning_counts = {}
for r in results:
for w in r["warnings"]:
# Normalize warning text
key = w.split("'")[0].strip() if "'" in w else w
warning_counts[key] = warning_counts.get(key, 0) + 1
for warning, count in sorted(warning_counts.items(), key=lambda x: -x[1])[:5]:
print(f" {count}x {warning}")

# JSON output
if args.json:
output = {
"total": len(results),
"valid": valid_count,
"invalid": invalid_count,
"warnings": warning_count,
"results": results
}
print("\n" + json.dumps(output, indent=2))

# Exit code
sys.exit(0 if invalid_count == 0 else 1)

if name == "main": main()