Skip to main content

#!/usr/bin/env python3 """ Component Registry - Authoritative source of valid CODITECT components.

Provides validated lists of agents, commands, skills, and other components for cross-checking in retrospective analysis and skill optimization.

Usage: from scripts.core.component_registry import ComponentRegistry

registry = ComponentRegistry()

# Check if a skill is valid
if registry.is_valid_skill("classify"):
print("Valid!")

# Get all valid skill names
all_skills = registry.get_all_skills()

# Validate a list of detected skills
valid, invalid = registry.validate_skills(["classify", "fake-skill"])

"""

import json import os import re from dataclasses import dataclass, field from datetime import datetime, timedelta from pathlib import Path from typing import Dict, List, Optional, Set, Tuple

@dataclass class ComponentRegistry: """ Authoritative registry of all valid CODITECT components.

Scans the filesystem to build an accurate component inventory,
with caching for performance.
"""

# Cache configuration
cache_ttl_minutes: int = 30
_cache: Dict = field(default_factory=dict)
_cache_time: Optional[datetime] = None
_root_path: Optional[Path] = None

def __post_init__(self):
"""Initialize the registry."""
self._root_path = self._find_root()
self._refresh_cache()

def _find_root(self) -> Path:
"""Find the coditect-core root directory."""
# Try current directory and parents
current = Path.cwd()

for path in [current] + list(current.parents):
if (path / "agents").exists() and (path / "skills").exists():
return path
if (path / ".coditect").exists():
coditect = path / ".coditect"
if coditect.is_symlink():
return coditect.resolve()
return coditect

# Fallback to known path
fallback = Path("/Users/halcasteel/PROJECTS/coditect-rollout-master/submodules/core/coditect-core")
if fallback.exists():
return fallback

raise RuntimeError("Could not find coditect-core root directory")

def _is_cache_valid(self) -> bool:
"""Check if cache is still valid."""
if not self._cache_time:
return False
return datetime.now() - self._cache_time < timedelta(minutes=self.cache_ttl_minutes)

def _refresh_cache(self, force: bool = False):
"""Refresh the component cache from filesystem."""
if not force and self._is_cache_valid():
return

self._cache = {
"agents": self._scan_agents(),
"commands": self._scan_commands(),
"skills": self._scan_skills(),
"hooks": self._scan_hooks(),
# Combined set for quick lookup
"all_invocable": set(),
}

# Build combined set of all invocable components
self._cache["all_invocable"] = (
self._cache["agents"] |
self._cache["commands"] |
self._cache["skills"]
)

self._cache_time = datetime.now()

def _scan_agents(self) -> Set[str]:
"""Scan agents directory for valid agent names."""
agents = set()
agents_dir = self._root_path / "agents"

if not agents_dir.exists():
return agents

for file in agents_dir.glob("*.md"):
if file.name in ("README.md", "INDEX.md"):
continue
agent_name = file.stem # Remove .md extension
agents.add(agent_name)

return agents

def _scan_commands(self) -> Set[str]:
"""Scan commands directory for valid command names."""
commands = set()
commands_dir = self._root_path / "commands"

if not commands_dir.exists():
return commands

for file in commands_dir.glob("*.md"):
if file.name in ("README.md", "INDEX.md", "GUIDE.md"):
continue
command_name = file.stem
commands.add(command_name)

return commands

def _scan_skills(self) -> Set[str]:
"""Scan skills directory for valid skill names."""
skills = set()
skills_dir = self._root_path / "skills"

if not skills_dir.exists():
return skills

# Skills are directories containing SKILL.md
for skill_file in skills_dir.rglob("SKILL.md"):
skill_name = skill_file.parent.name
skills.add(skill_name)

return skills

def _scan_hooks(self) -> Set[str]:
"""Scan hooks directory for valid hook names."""
hooks = set()
hooks_dir = self._root_path / "hooks"

if not hooks_dir.exists():
return hooks

for file in hooks_dir.iterdir():
if file.suffix in (".md", ".sh", ".py"):
if file.name != "README.md":
hook_name = file.stem
hooks.add(hook_name)

return hooks

# Public API

def is_valid_skill(self, name: str) -> bool:
"""Check if a name is a valid invocable skill/command/agent."""
self._refresh_cache()
return name in self._cache["all_invocable"]

def is_valid_agent(self, name: str) -> bool:
"""Check if a name is a valid agent."""
self._refresh_cache()
return name in self._cache["agents"]

def is_valid_command(self, name: str) -> bool:
"""Check if a name is a valid command."""
self._refresh_cache()
return name in self._cache["commands"]

def get_all_skills(self) -> Set[str]:
"""Get all valid invocable component names."""
self._refresh_cache()
return self._cache["all_invocable"].copy()

def get_agents(self) -> Set[str]:
"""Get all valid agent names."""
self._refresh_cache()
return self._cache["agents"].copy()

def get_commands(self) -> Set[str]:
"""Get all valid command names."""
self._refresh_cache()
return self._cache["commands"].copy()

def get_skills_only(self) -> Set[str]:
"""Get skill names (from skills/ directory only)."""
self._refresh_cache()
return self._cache["skills"].copy()

def validate_skills(self, skills: List[str]) -> Tuple[List[str], List[str]]:
"""
Validate a list of detected skills.

Returns:
Tuple of (valid_skills, invalid_skills)
"""
self._refresh_cache()
valid = []
invalid = []

for skill in skills:
if skill in self._cache["all_invocable"]:
valid.append(skill)
else:
invalid.append(skill)

return valid, invalid

def get_stats(self) -> Dict:
"""Get component statistics."""
self._refresh_cache()
return {
"agents": len(self._cache["agents"]),
"commands": len(self._cache["commands"]),
"skills": len(self._cache["skills"]),
"hooks": len(self._cache["hooks"]),
"total_invocable": len(self._cache["all_invocable"]),
"cache_time": self._cache_time.isoformat() if self._cache_time else None,
}

def export_registry(self, output_path: Optional[Path] = None) -> Dict:
"""
Export the registry to a JSON file.

Useful for debugging or static analysis.
"""
self._refresh_cache()

registry = {
"generated": datetime.now().isoformat(),
"root_path": str(self._root_path),
"stats": self.get_stats(),
"components": {
"agents": sorted(self._cache["agents"]),
"commands": sorted(self._cache["commands"]),
"skills": sorted(self._cache["skills"]),
"hooks": sorted(self._cache["hooks"]),
}
}

if output_path:
with open(output_path, 'w') as f:
json.dump(registry, f, indent=2)

return registry

Singleton instance for convenience

_registry_instance: Optional[ComponentRegistry] = None

def get_registry() -> ComponentRegistry: """Get the singleton registry instance.""" global _registry_instance if _registry_instance is None: _registry_instance = ComponentRegistry() return _registry_instance

def is_valid_skill(name: str) -> bool: """Convenience function to check if a skill is valid.""" return get_registry().is_valid_skill(name)

def validate_skills(skills: List[str]) -> Tuple[List[str], List[str]]: """Convenience function to validate a list of skills.""" return get_registry().validate_skills(skills)

if name == "main": import argparse

parser = argparse.ArgumentParser(description="Component Registry CLI")
parser.add_argument("--stats", action="store_true", help="Show component statistics")
parser.add_argument("--export", type=str, help="Export registry to JSON file")
parser.add_argument("--validate", type=str, nargs="+", help="Validate skill names")
parser.add_argument("--list", choices=["agents", "commands", "skills", "all"], help="List components")

args = parser.parse_args()

registry = ComponentRegistry()

if args.stats:
stats = registry.get_stats()
print("Component Registry Statistics")
print("=" * 40)
print(f"Agents: {stats['agents']:>4}")
print(f"Commands: {stats['commands']:>4}")
print(f"Skills: {stats['skills']:>4}")
print(f"Hooks: {stats['hooks']:>4}")
print("-" * 40)
print(f"Total Invocable: {stats['total_invocable']:>4}")

elif args.export:
registry.export_registry(Path(args.export))
print(f"Registry exported to: {args.export}")

elif args.validate:
valid, invalid = registry.validate_skills(args.validate)
print("Validation Results")
print("=" * 40)
if valid:
print(f"Valid ({len(valid)}):")
for s in valid:
print(f" ✅ {s}")
if invalid:
print(f"Invalid ({len(invalid)}):")
for s in invalid:
print(f" ❌ {s}")

elif args.list:
if args.list == "agents":
for name in sorted(registry.get_agents()):
print(name)
elif args.list == "commands":
for name in sorted(registry.get_commands()):
print(name)
elif args.list == "skills":
for name in sorted(registry.get_skills_only()):
print(name)
elif args.list == "all":
for name in sorted(registry.get_all_skills()):
print(name)

else:
# Default: show stats
stats = registry.get_stats()
print(f"Registry: {stats['total_invocable']} invocable components")
print(f" Run with --help for options")