scripts-validate-component
#!/usr/bin/env python3 """
title: "Get script directory for path resolution (works from any cwd)" component_type: script version: "1.0.0" audience: contributor status: stable summary: "CODITECT Component Validator - Automated QA Validation" keywords: ['component', 'validate', 'validation'] tokens: ~500 created: 2025-12-22 updated: 2025-12-22 script_name: "validate-component.py" language: python executable: true usage: "python3 scripts/validate-component.py [options]" python_version: "3.10+" dependencies: [] modifies_files: false network_access: false requires_auth: false
CODITECT Component Validator - Automated QA Validation
Validates CODITECT framework components (agents, skills, commands, scripts) against established standards. Provides structural validation, scoring, and actionable recommendations.
Usage: python3 validate-component.py [path] python3 validate-component.py --type agent python3 validate-component.py --all --min-score 80
Examples: python3 validate-component.py agents/memory-context-agent.md python3 validate-component.py --type skill --verbose python3 validate-component.py --all --json
Exit Codes: 0: All validations passed (score >= min_score) 1: Validation failed (score < min_score) 2: File not found or invalid path 3: Configuration error
Version: 1.0.0 """
import argparse import json import os import re import sys from dataclasses import dataclass, field from datetime import datetime from enum import Enum from pathlib import Path from typing import Any, Optional
import yaml
Get script directory for path resolution (works from any cwd)
SCRIPT_DIR = Path(file).resolve().parent CORE_ROOT = SCRIPT_DIR.parent
class CheckStatus(Enum): PASS = "pass" WARN = "warn" FAIL = "fail"
@dataclass class CheckResult: """Result of a single validation check.""" id: str name: str status: CheckStatus message: str fix: Optional[str] = None severity: str = "medium"
@dataclass class ValidationReport: """Complete validation report for a component.""" component: str component_type: str timestamp: str score: float grade: str status: str checks: list[CheckResult] = field(default_factory=list)
def to_dict(self) -> dict:
"""Convert to dictionary."""
return {
"component": self.component,
"type": self.component_type,
"timestamp": self.timestamp,
"score": self.score,
"grade": self.grade,
"status": self.status,
"checks": {
"passed": len([c for c in self.checks if c.status == CheckStatus.PASS]),
"warnings": len([c for c in self.checks if c.status == CheckStatus.WARN]),
"failures": len([c for c in self.checks if c.status == CheckStatus.FAIL])
},
"details": [
{
"id": c.id,
"name": c.name,
"status": c.status.value,
"message": c.message,
"fix": c.fix
}
for c in self.checks
]
}
def get_grade(score: float) -> str: """Get letter grade from score.""" if score >= 90: return "A" elif score >= 80: return "B" elif score >= 70: return "C" elif score >= 60: return "D" else: return "F"
def get_status(score: float, min_score: float) -> str: """Get pass/fail status.""" return "pass" if score >= min_score else "fail"
def detect_component_type(path: str) -> Optional[str]: """Detect component type from path.""" path_lower = path.lower()
if path_lower.startswith("agents/") or "/agents/" in path_lower:
return "agent"
elif path_lower.startswith("skills/") or "/skills/" in path_lower:
return "skill"
elif path_lower.startswith("commands/") or "/commands/" in path_lower:
return "command"
elif path_lower.startswith("scripts/") or "/scripts/" in path_lower:
return "script"
# Check by extension
if path.endswith(".py") or path.endswith(".sh"):
return "script"
elif path.endswith(".md"):
# Check content for clues
return None
return None
def parse_yaml_frontmatter(content: str) -> tuple[Optional[dict], str]: """Extract YAML frontmatter from markdown content.""" if not content.startswith("---"): return None, content
# Find end of frontmatter
end_match = re.search(r"\n---\n", content[3:])
if not end_match:
return None, content
frontmatter_str = content[3:end_match.start() + 3]
body = content[end_match.end() + 3:]
try:
frontmatter = yaml.safe_load(frontmatter_str)
return frontmatter, body
except yaml.YAMLError:
return None, content
def validate_agent(path: str, content: str) -> list[CheckResult]: """Validate an agent file.""" results = [] frontmatter, body = parse_yaml_frontmatter(content) filename = Path(path).stem
# YAML Frontmatter
if frontmatter:
results.append(CheckResult(
id="yaml_frontmatter",
name="YAML Frontmatter",
status=CheckStatus.PASS,
message="Valid YAML frontmatter present"
))
else:
results.append(CheckResult(
id="yaml_frontmatter",
name="YAML Frontmatter",
status=CheckStatus.FAIL,
message="Missing or invalid YAML frontmatter",
fix="Add YAML frontmatter with --- delimiters"
))
return results # Can't continue without frontmatter
# Name field
if "name" in frontmatter:
name = frontmatter["name"]
# Check kebab-case
if re.match(r"^[a-z][a-z0-9-]*[a-z0-9]$", name) or re.match(r"^[a-z]+$", name):
results.append(CheckResult(
id="name_kebab_case",
name="Name Kebab Case",
status=CheckStatus.PASS,
message=f"Name '{name}' follows kebab-case"
))
else:
results.append(CheckResult(
id="name_kebab_case",
name="Name Kebab Case",
status=CheckStatus.FAIL,
message=f"Name '{name}' should be kebab-case",
fix="Use lowercase with hyphens: my-agent-name"
))
# Check matches filename
if name == filename:
results.append(CheckResult(
id="name_matches_file",
name="Name Matches Filename",
status=CheckStatus.PASS,
message=f"Name matches filename '{filename}'"
))
else:
results.append(CheckResult(
id="name_matches_file",
name="Name Matches Filename",
status=CheckStatus.FAIL,
message=f"Name '{name}' doesn't match filename '{filename}'",
fix=f"Rename file to {name}.md or change name to {filename}"
))
else:
results.append(CheckResult(
id="name_field",
name="Name Field",
status=CheckStatus.FAIL,
message="Missing name field in frontmatter",
fix="Add: name: agent-name"
))
# Description
if "description" in frontmatter:
desc = frontmatter["description"]
desc_len = len(desc)
if 10 <= desc_len <= 300:
results.append(CheckResult(
id="description",
name="Description",
status=CheckStatus.PASS,
message=f"Description present ({desc_len} chars)"
))
elif desc_len < 10:
results.append(CheckResult(
id="description",
name="Description",
status=CheckStatus.FAIL,
message=f"Description too short ({desc_len} chars)",
fix="Add more detail (minimum 10 characters)"
))
else:
results.append(CheckResult(
id="description",
name="Description",
status=CheckStatus.WARN,
message=f"Description very long ({desc_len} chars)",
fix="Consider shortening to under 200 characters"
))
else:
results.append(CheckResult(
id="description",
name="Description",
status=CheckStatus.FAIL,
message="Missing description field",
fix="Add: description: One-sentence description"
))
# Tools
if "tools" in frontmatter:
tools = frontmatter["tools"]
if isinstance(tools, str):
tools_list = [t.strip() for t in tools.split(",")]
elif isinstance(tools, list):
tools_list = tools
else:
tools_list = []
valid_tools = {"Read", "Write", "Edit", "Bash", "Glob", "Grep", "LS",
"TodoWrite", "WebSearch", "WebFetch", "Task"}
invalid = [t for t in tools_list if t not in valid_tools]
if not invalid:
results.append(CheckResult(
id="tools_valid",
name="Tools Valid",
status=CheckStatus.PASS,
message=f"All tools valid: {', '.join(tools_list)}"
))
else:
results.append(CheckResult(
id="tools_valid",
name="Tools Valid",
status=CheckStatus.WARN,
message=f"Unknown tools: {', '.join(invalid)}",
fix=f"Valid tools: {', '.join(valid_tools)}"
))
else:
results.append(CheckResult(
id="tools_field",
name="Tools Field",
status=CheckStatus.FAIL,
message="Missing tools field",
fix="Add: tools: Read, Write, Edit, Bash"
))
# Model
if "model" in frontmatter:
model = frontmatter["model"]
if model in ["sonnet", "haiku"]:
results.append(CheckResult(
id="model_valid",
name="Model Valid",
status=CheckStatus.PASS,
message=f"Model '{model}' is valid"
))
else:
results.append(CheckResult(
id="model_valid",
name="Model Valid",
status=CheckStatus.FAIL,
message=f"Invalid model '{model}'",
fix="Use: sonnet or haiku"
))
else:
results.append(CheckResult(
id="model_field",
name="Model Field",
status=CheckStatus.FAIL,
message="Missing model field",
fix="Add: model: sonnet"
))
# License (recommended)
if "license" not in frontmatter:
results.append(CheckResult(
id="license_field",
name="License Field",
status=CheckStatus.WARN,
message="Missing license field (recommended)",
fix="Add: license: MIT",
severity="low"
))
# Content checks
# Opening statement
if "you are a" in body.lower()[:500]:
results.append(CheckResult(
id="opening_statement",
name="Opening Statement",
status=CheckStatus.PASS,
message="Starts with 'You are a...' statement"
))
else:
results.append(CheckResult(
id="opening_statement",
name="Opening Statement",
status=CheckStatus.WARN,
message="Missing 'You are a...' opening statement",
fix="Start with: You are a [Role] responsible for..."
))
# Core Responsibilities
if "## core responsibilities" in body.lower():
results.append(CheckResult(
id="core_responsibilities",
name="Core Responsibilities",
status=CheckStatus.PASS,
message="Core Responsibilities section present"
))
else:
results.append(CheckResult(
id="core_responsibilities",
name="Core Responsibilities",
status=CheckStatus.WARN,
message="Missing Core Responsibilities section",
fix="Add: ## Core Responsibilities"
))
# When to Use
if "when to use" in body.lower() or "✅" in body:
results.append(CheckResult(
id="when_to_use",
name="When to Use",
status=CheckStatus.PASS,
message="When to Use guidance present"
))
else:
results.append(CheckResult(
id="when_to_use",
name="When to Use",
status=CheckStatus.WARN,
message="Missing When to Use section",
fix="Add: ## When to Use\\n\\n✅ Use when...\\n❌ Don't use when..."
))
return results
def validate_skill(path: str, content: str) -> list[CheckResult]: """Validate a skill SKILL.md file.""" results = [] frontmatter, body = parse_yaml_frontmatter(content) directory = Path(path).parent.name
# Check it's SKILL.md
if not path.endswith("SKILL.md"):
results.append(CheckResult(
id="skill_md",
name="SKILL.md",
status=CheckStatus.WARN,
message="Not named SKILL.md",
fix="Rename to SKILL.md"
))
# YAML Frontmatter
if frontmatter:
results.append(CheckResult(
id="yaml_frontmatter",
name="YAML Frontmatter",
status=CheckStatus.PASS,
message="Valid YAML frontmatter present"
))
else:
results.append(CheckResult(
id="yaml_frontmatter",
name="YAML Frontmatter",
status=CheckStatus.FAIL,
message="Missing or invalid YAML frontmatter",
fix="Add YAML frontmatter with --- delimiters"
))
return results
# Name matches directory
if "name" in frontmatter:
name = frontmatter["name"]
if name == directory:
results.append(CheckResult(
id="name_matches_dir",
name="Name Matches Directory",
status=CheckStatus.PASS,
message=f"Name '{name}' matches directory"
))
else:
results.append(CheckResult(
id="name_matches_dir",
name="Name Matches Directory",
status=CheckStatus.FAIL,
message=f"Name '{name}' doesn't match directory '{directory}'",
fix=f"Change name to: {directory}"
))
else:
results.append(CheckResult(
id="name_field",
name="Name Field",
status=CheckStatus.FAIL,
message="Missing name field",
fix=f"Add: name: {directory}"
))
# Description
if "description" in frontmatter:
results.append(CheckResult(
id="description",
name="Description",
status=CheckStatus.PASS,
message="Description present"
))
else:
results.append(CheckResult(
id="description",
name="Description",
status=CheckStatus.FAIL,
message="Missing description",
fix="Add: description: Skill capability statement"
))
# License
if "license" in frontmatter:
results.append(CheckResult(
id="license",
name="License",
status=CheckStatus.PASS,
message=f"License: {frontmatter['license']}"
))
else:
results.append(CheckResult(
id="license",
name="License",
status=CheckStatus.FAIL,
message="Missing license field",
fix="Add: license: MIT"
))
# Allowed tools
if "allowed-tools" in frontmatter:
results.append(CheckResult(
id="allowed_tools",
name="Allowed Tools",
status=CheckStatus.PASS,
message="Allowed tools defined"
))
else:
results.append(CheckResult(
id="allowed_tools",
name="Allowed Tools",
status=CheckStatus.FAIL,
message="Missing allowed-tools field",
fix="Add: allowed-tools: [Read, Write, Edit]"
))
# Content checks
if "when to use" in body.lower():
results.append(CheckResult(
id="when_to_use",
name="When to Use",
status=CheckStatus.PASS,
message="When to Use section present"
))
else:
results.append(CheckResult(
id="when_to_use",
name="When to Use",
status=CheckStatus.FAIL,
message="Missing When to Use section",
fix="Add: ## When to Use This Skill"
))
if "core capabilities" in body.lower() or "## core" in body.lower():
results.append(CheckResult(
id="core_capabilities",
name="Core Capabilities",
status=CheckStatus.PASS,
message="Core Capabilities section present"
))
else:
results.append(CheckResult(
id="core_capabilities",
name="Core Capabilities",
status=CheckStatus.WARN,
message="Missing Core Capabilities section",
fix="Add: ## Core Capabilities"
))
if "usage" in body.lower():
results.append(CheckResult(
id="usage_pattern",
name="Usage Pattern",
status=CheckStatus.PASS,
message="Usage documentation present"
))
else:
results.append(CheckResult(
id="usage_pattern",
name="Usage Pattern",
status=CheckStatus.WARN,
message="Missing Usage Pattern section",
fix="Add: ## Usage Pattern"
))
return results
def validate_command(path: str, content: str) -> list[CheckResult]: """Validate a command file.""" results = [] frontmatter, body = parse_yaml_frontmatter(content) filename = Path(path).stem
# Filename kebab-case
if re.match(r"^[a-z][a-z0-9-]*[a-z0-9]$", filename) or re.match(r"^[a-z]+$", filename):
results.append(CheckResult(
id="filename_kebab",
name="Filename Kebab Case",
status=CheckStatus.PASS,
message=f"Filename '{filename}' follows kebab-case"
))
else:
results.append(CheckResult(
id="filename_kebab",
name="Filename Kebab Case",
status=CheckStatus.WARN,
message=f"Filename '{filename}' should be kebab-case",
fix="Rename to use lowercase with hyphens"
))
# YAML frontmatter (optional for commands)
if frontmatter:
results.append(CheckResult(
id="yaml_frontmatter",
name="YAML Frontmatter",
status=CheckStatus.PASS,
message="YAML frontmatter present"
))
if "name" in frontmatter:
if frontmatter["name"] == filename:
results.append(CheckResult(
id="name_matches",
name="Name Matches",
status=CheckStatus.PASS,
message="Name matches filename"
))
else:
results.append(CheckResult(
id="name_matches",
name="Name Matches",
status=CheckStatus.WARN,
message="Name doesn't match filename"
))
if "description" in frontmatter:
results.append(CheckResult(
id="description",
name="Description",
status=CheckStatus.PASS,
message="Description present"
))
# Content checks
# Title
if body.strip().startswith("#"):
results.append(CheckResult(
id="title",
name="Title",
status=CheckStatus.PASS,
message="Title present"
))
else:
results.append(CheckResult(
id="title",
name="Title",
status=CheckStatus.FAIL,
message="Missing title",
fix="Add: # /command-name - Description"
))
# Usage
if "## usage" in body.lower() or "```bash" in body.lower():
results.append(CheckResult(
id="usage",
name="Usage",
status=CheckStatus.PASS,
message="Usage documentation present"
))
else:
results.append(CheckResult(
id="usage",
name="Usage",
status=CheckStatus.FAIL,
message="Missing Usage section",
fix="Add: ## Usage\\n\\n```bash\\n/command [options]\\n```"
))
# Examples
example_count = body.lower().count("example")
if example_count >= 2 or body.count("```") >= 4:
results.append(CheckResult(
id="examples",
name="Examples",
status=CheckStatus.PASS,
message="Multiple examples present"
))
elif example_count >= 1 or body.count("```") >= 2:
results.append(CheckResult(
id="examples",
name="Examples",
status=CheckStatus.WARN,
message="Limited examples",
fix="Add more usage examples"
))
else:
results.append(CheckResult(
id="examples",
name="Examples",
status=CheckStatus.FAIL,
message="Missing examples",
fix="Add: ## Examples with code blocks"
))
return results
def validate_script(path: str, content: str) -> list[CheckResult]: """Validate a script file (Python or Bash).""" results = [] is_python = path.endswith(".py") is_bash = path.endswith(".sh") lines = content.split("\n")
# Shebang
if lines and lines[0].startswith("#!"):
shebang = lines[0]
if is_python and "python" in shebang:
results.append(CheckResult(
id="shebang",
name="Shebang",
status=CheckStatus.PASS,
message="Correct Python shebang"
))
elif is_bash and "bash" in shebang:
results.append(CheckResult(
id="shebang",
name="Shebang",
status=CheckStatus.PASS,
message="Correct Bash shebang"
))
else:
results.append(CheckResult(
id="shebang",
name="Shebang",
status=CheckStatus.WARN,
message=f"Unexpected shebang: {shebang}"
))
else:
results.append(CheckResult(
id="shebang",
name="Shebang",
status=CheckStatus.FAIL,
message="Missing shebang",
fix="Add: #!/usr/bin/env python3 or #!/bin/bash"
))
if is_python:
# Module docstring
if '"""' in content[:500] or "'''" in content[:500]:
results.append(CheckResult(
id="docstring",
name="Module Docstring",
status=CheckStatus.PASS,
message="Module docstring present"
))
# Usage in docstring
if "usage:" in content.lower()[:2000]:
results.append(CheckResult(
id="usage_doc",
name="Usage Documentation",
status=CheckStatus.PASS,
message="Usage documented in docstring"
))
else:
results.append(CheckResult(
id="usage_doc",
name="Usage Documentation",
status=CheckStatus.WARN,
message="Missing Usage in docstring",
fix="Add Usage: section to docstring"
))
# Exit codes
if "exit" in content.lower()[:2000] and ("code" in content.lower()[:2000] or "0:" in content[:2000]):
results.append(CheckResult(
id="exit_codes",
name="Exit Codes",
status=CheckStatus.PASS,
message="Exit codes documented"
))
else:
results.append(CheckResult(
id="exit_codes",
name="Exit Codes",
status=CheckStatus.WARN,
message="Missing Exit Codes documentation",
fix="Add: Exit Codes:\\n 0: Success\\n 1: Error"
))
else:
results.append(CheckResult(
id="docstring",
name="Module Docstring",
status=CheckStatus.FAIL,
message="Missing module docstring",
fix='Add module docstring with """..."""'
))
# Type hints
func_pattern = r"def\s+\w+\s*\([^)]*\)\s*(->\s*\w+)?\s*:"
funcs = re.findall(func_pattern, content)
typed_funcs = len([f for f in funcs if f])
if typed_funcs > 0:
results.append(CheckResult(
id="type_hints",
name="Type Hints",
status=CheckStatus.PASS if typed_funcs == len(funcs) else CheckStatus.WARN,
message=f"Type hints on {typed_funcs}/{len(funcs)} functions"
))
elif len(funcs) > 0:
results.append(CheckResult(
id="type_hints",
name="Type Hints",
status=CheckStatus.WARN,
message="Missing type hints",
fix="Add return type hints: def func() -> ReturnType:"
))
# Main guard
if 'if __name__ == "__main__"' in content or "if __name__ == '__main__'" in content:
results.append(CheckResult(
id="main_guard",
name="Main Guard",
status=CheckStatus.PASS,
message="Main guard present"
))
else:
results.append(CheckResult(
id="main_guard",
name="Main Guard",
status=CheckStatus.WARN,
message="Missing main guard",
fix='Add: if __name__ == "__main__":'
))
elif is_bash:
# set -euo pipefail
if "set -euo pipefail" in content or "set -e" in content:
results.append(CheckResult(
id="set_options",
name="Set Options",
status=CheckStatus.PASS,
message="Error handling options set"
))
else:
results.append(CheckResult(
id="set_options",
name="Set Options",
status=CheckStatus.FAIL,
message="Missing set -euo pipefail",
fix="Add: set -euo pipefail"
))
# Header comments
comment_lines = len([l for l in lines[:20] if l.startswith("#")])
if comment_lines >= 5:
results.append(CheckResult(
id="header_comments",
name="Header Comments",
status=CheckStatus.PASS,
message="Header comments present"
))
else:
results.append(CheckResult(
id="header_comments",
name="Header Comments",
status=CheckStatus.WARN,
message="Limited header comments",
fix="Add description, usage, examples in comments"
))
# Main function
if "main()" in content or "main " in content:
results.append(CheckResult(
id="main_function",
name="Main Function",
status=CheckStatus.PASS,
message="Main function present"
))
else:
results.append(CheckResult(
id="main_function",
name="Main Function",
status=CheckStatus.WARN,
message="Missing main() function",
fix="Add: main() { ... }\\nmain \"$@\""
))
# Executable check (if file exists)
if os.path.exists(path):
if os.access(path, os.X_OK):
results.append(CheckResult(
id="executable",
name="Executable",
status=CheckStatus.PASS,
message="File is executable"
))
else:
results.append(CheckResult(
id="executable",
name="Executable",
status=CheckStatus.WARN,
message="File not executable",
fix=f"Run: chmod +x {path}"
))
return results
def validate_component(path: str) -> ValidationReport: """Validate a single component.""" # Read file try: with open(path, "r", encoding="utf-8") as f: content = f.read() except FileNotFoundError: return ValidationReport( component=path, component_type="unknown", timestamp=datetime.now().isoformat(), score=0, grade="F", status="error", checks=[CheckResult( id="file_exists", name="File Exists", status=CheckStatus.FAIL, message=f"File not found: {path}" )] )
# Detect type
component_type = detect_component_type(path)
if not component_type:
# Try to detect from content
if "tools:" in content and "model:" in content:
component_type = "agent"
elif "allowed-tools:" in content:
component_type = "skill"
elif path.endswith(".py") or path.endswith(".sh"):
component_type = "script"
else:
component_type = "command"
# Run appropriate validation
if component_type == "agent":
checks = validate_agent(path, content)
elif component_type == "skill":
checks = validate_skill(path, content)
elif component_type == "command":
checks = validate_command(path, content)
elif component_type == "script":
checks = validate_script(path, content)
else:
checks = []
# Calculate score
if checks:
# Weight: PASS=1, WARN=0.5, FAIL=0
score_sum = sum(
1.0 if c.status == CheckStatus.PASS else
0.5 if c.status == CheckStatus.WARN else
0.0
for c in checks
)
score = (score_sum / len(checks)) * 100
else:
score = 0
grade = get_grade(score)
return ValidationReport(
component=path,
component_type=component_type,
timestamp=datetime.now().isoformat(),
score=round(score, 1),
grade=grade,
status="pass" if score >= 80 else "fail",
checks=checks
)
def format_report_text(report: ValidationReport, verbose: bool = False) -> str: """Format report as text.""" lines = [ f"QA VALIDATION: {report.component}", "━" * 50, "", f"Score: {report.score}% ({report.grade}) {'✅ PASS' if report.status == 'pass' else '❌ FAIL'}", "" ]
passed = [c for c in report.checks if c.status == CheckStatus.PASS]
warnings = [c for c in report.checks if c.status == CheckStatus.WARN]
failures = [c for c in report.checks if c.status == CheckStatus.FAIL]
if passed:
lines.append(f"✅ PASSED ({len(passed)} checks)")
if verbose:
for c in passed:
lines.append(f" • {c.message}")
lines.append("")
if warnings:
lines.append(f"⚠️ WARNINGS ({len(warnings)})")
for c in warnings:
lines.append(f" • {c.message}")
if c.fix:
lines.append(f" Fix: {c.fix}")
lines.append("")
if failures:
lines.append(f"❌ FAILURES ({len(failures)})")
for c in failures:
lines.append(f" • {c.message}")
if c.fix:
lines.append(f" Fix: {c.fix}")
lines.append("")
return "\n".join(lines)
def find_components(component_type: Optional[str] = None) -> list[str]: """Find all components of a given type.
Uses CORE_ROOT to find components regardless of current working directory.
"""
components = []
if component_type is None or component_type == "agent":
components.extend(str(p) for p in (CORE_ROOT / "agents").glob("*.md")
if p.name != "README.md")
if component_type is None or component_type == "skill":
components.extend(str(p) for p in (CORE_ROOT / "skills").glob("*/SKILL.md"))
if component_type is None or component_type == "command":
components.extend(str(p) for p in (CORE_ROOT / "commands").glob("*.md")
if p.name not in ["README.md", "INDEX.md", "GUIDE.md"])
if component_type is None or component_type == "script":
components.extend(str(p) for p in (CORE_ROOT / "scripts").glob("*.py"))
components.extend(str(p) for p in (CORE_ROOT / "scripts").glob("*.sh"))
return sorted(components)
def main() -> int: """Main entry point.""" parser = argparse.ArgumentParser( description="CODITECT Component Validator", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=doc )
parser.add_argument("path", nargs="?", help="Path to component to validate")
parser.add_argument("--type", choices=["agent", "skill", "command", "script"],
help="Validate all of type")
parser.add_argument("--all", action="store_true", help="Validate all components")
parser.add_argument("--min-score", type=float, default=80,
help="Minimum passing score (default: 80)")
parser.add_argument("--json", action="store_true", help="Output as JSON")
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
args = parser.parse_args()
# Determine what to validate
if args.all:
paths = find_components()
elif args.type:
paths = find_components(args.type)
elif args.path:
paths = [args.path]
else:
parser.print_help()
return 2
if not paths:
print("No components found to validate")
return 2
# Validate
reports = []
for path in paths:
report = validate_component(path)
reports.append(report)
# Output
if args.json:
output = {
"timestamp": datetime.now().isoformat(),
"total": len(reports),
"passed": len([r for r in reports if r.status == "pass"]),
"failed": len([r for r in reports if r.status == "fail"]),
"reports": [r.to_dict() for r in reports]
}
print(json.dumps(output, indent=2))
else:
for report in reports:
print(format_report_text(report, args.verbose))
if len(reports) > 1:
passed = len([r for r in reports if r.status == "pass"])
print("=" * 50)
print(f"SUMMARY: {passed}/{len(reports)} passed")
avg_score = sum(r.score for r in reports) / len(reports)
print(f"Average Score: {avg_score:.1f}%")
# Exit code
all_passed = all(r.score >= args.min_score for r in reports)
return 0 if all_passed else 1
if name == "main": sys.exit(main())