181 lines
6.4 KiB
Python
Executable File
181 lines
6.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Validate README.md synchronization between root and plugin directories.
|
|
|
|
Ensures that key sections (skills, agents, commands) stay consistent across:
|
|
- /README.md (root - for contributors/developers)
|
|
- /plugins/autonomous-dev/README.md (plugin - for users)
|
|
|
|
**IMPORTANT**: This hook only runs in the autonomous-dev plugin repository.
|
|
It automatically detects user projects and silently succeeds (no blocking).
|
|
Plugin users will never see validation errors from this hook.
|
|
|
|
Exit codes:
|
|
0 - READMEs in sync (or hook skipped in user project)
|
|
1 - Warning (show message but allow commit)
|
|
2 - Block commit (critical sections out of sync in plugin repo)
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Dict, List, Tuple
|
|
|
|
|
|
# Sections that MUST be in sync (critical)
|
|
CRITICAL_SECTIONS = [
|
|
"Skills", # Skill count and architecture
|
|
"Agents", # Agent count and list
|
|
]
|
|
|
|
# Sections that SHOULD be in sync (warning only)
|
|
WARNING_SECTIONS = [
|
|
"Commands", # Command list
|
|
"Version", # Version number
|
|
]
|
|
|
|
|
|
def extract_section(readme_content: str, section_name: str) -> str:
|
|
"""Extract a section from README content."""
|
|
# Match: ### Section Name or ## Section Name
|
|
pattern = rf"^#{2,3}\s+{section_name}.*?(?=^#{2,3}\s+|\Z)"
|
|
match = re.search(pattern, readme_content, re.MULTILINE | re.DOTALL)
|
|
return match.group(0) if match else ""
|
|
|
|
|
|
def extract_key_stats(content: str) -> Dict[str, str]:
|
|
"""Extract key statistics from README content."""
|
|
stats = {}
|
|
|
|
# Extract skill count (e.g., "19 Active Skills")
|
|
skill_match = re.search(r"(\d+)\s+[Aa]ctive\s+[Ss]kills", content)
|
|
if skill_match:
|
|
stats["skill_count"] = skill_match.group(1)
|
|
|
|
# Extract agent count (e.g., "18 AI Specialists" or "18 specialist agents")
|
|
agent_match = re.search(r"(\d+)\s+(?:[Aa][Ii]\s+)?[Ss]pecialists?(?:\s+agents)?", content)
|
|
if agent_match:
|
|
stats["agent_count"] = agent_match.group(1)
|
|
|
|
# Extract command count (e.g., "18 Commands")
|
|
command_match = re.search(r"(\d+)\s+[Cc]ommands", content)
|
|
if command_match:
|
|
stats["command_count"] = command_match.group(1)
|
|
|
|
# Extract version (e.g., "v3.5.0")
|
|
version_match = re.search(r"[Vv]ersion[:\s]+(v?\d+\.\d+\.\d+)", content)
|
|
if version_match:
|
|
stats["version"] = version_match.group(1)
|
|
|
|
return stats
|
|
|
|
|
|
def compare_stats(root_stats: Dict[str, str], plugin_stats: Dict[str, str]) -> List[Tuple[str, str, str]]:
|
|
"""Compare stats between root and plugin READMEs."""
|
|
mismatches = []
|
|
|
|
for key in set(root_stats.keys()) | set(plugin_stats.keys()):
|
|
root_val = root_stats.get(key, "NOT FOUND")
|
|
plugin_val = plugin_stats.get(key, "NOT FOUND")
|
|
|
|
if root_val != plugin_val:
|
|
mismatches.append((key, root_val, plugin_val))
|
|
|
|
return mismatches
|
|
|
|
|
|
def main():
|
|
"""Main validation function."""
|
|
repo_root = Path(__file__).resolve().parents[3] # Up 3 levels from hooks/
|
|
|
|
root_readme = repo_root / "README.md"
|
|
plugin_readme = repo_root / "plugins" / "autonomous-dev" / "README.md"
|
|
|
|
# Auto-detect if we're in the autonomous-dev repository
|
|
# If not, silently skip (this hook is for the plugin repo only)
|
|
is_plugin_repo = (repo_root / "plugins" / "autonomous-dev").exists()
|
|
|
|
if not is_plugin_repo:
|
|
# User project - this hook doesn't apply
|
|
# Silently succeed so we don't block user workflows
|
|
return 0
|
|
|
|
# Check both READMEs exist (only in plugin repo)
|
|
if not root_readme.exists():
|
|
print(f"❌ Root README not found: {root_readme}", file=sys.stderr)
|
|
print("", file=sys.stderr)
|
|
print("This hook is for the autonomous-dev plugin repository only.", file=sys.stderr)
|
|
print("If you're a plugin user, you can safely ignore this.", file=sys.stderr)
|
|
sys.exit(2)
|
|
|
|
if not plugin_readme.exists():
|
|
print(f"❌ Plugin README not found: {plugin_readme}", file=sys.stderr)
|
|
print("", file=sys.stderr)
|
|
print("This hook is for the autonomous-dev plugin repository only.", file=sys.stderr)
|
|
print("If you're a plugin user, you can safely ignore this.", file=sys.stderr)
|
|
sys.exit(2)
|
|
|
|
# Read both READMEs
|
|
root_content = root_readme.read_text()
|
|
plugin_content = plugin_readme.read_text()
|
|
|
|
# Extract key statistics
|
|
root_stats = extract_key_stats(root_content)
|
|
plugin_stats = extract_key_stats(plugin_content)
|
|
|
|
# Compare statistics
|
|
mismatches = compare_stats(root_stats, plugin_stats)
|
|
|
|
if not mismatches:
|
|
# All stats match - success
|
|
return 0
|
|
|
|
# Check if mismatches are critical
|
|
critical_mismatches = [
|
|
(key, root, plugin)
|
|
for key, root, plugin in mismatches
|
|
if key in ["skill_count", "agent_count"]
|
|
]
|
|
|
|
warning_mismatches = [
|
|
(key, root, plugin)
|
|
for key, root, plugin in mismatches
|
|
if key not in ["skill_count", "agent_count"]
|
|
]
|
|
|
|
# Report critical mismatches (block commit)
|
|
if critical_mismatches:
|
|
print("❌ CRITICAL: README.md files out of sync", file=sys.stderr)
|
|
print("", file=sys.stderr)
|
|
print("The following critical statistics differ:", file=sys.stderr)
|
|
print("", file=sys.stderr)
|
|
for key, root_val, plugin_val in critical_mismatches:
|
|
print(f" {key}:", file=sys.stderr)
|
|
print(f" Root README: {root_val}", file=sys.stderr)
|
|
print(f" Plugin README: {plugin_val}", file=sys.stderr)
|
|
print("", file=sys.stderr)
|
|
print("Please update both READMEs to match.", file=sys.stderr)
|
|
print("", file=sys.stderr)
|
|
print("Files to update:", file=sys.stderr)
|
|
print(f" - {root_readme}", file=sys.stderr)
|
|
print(f" - {plugin_readme}", file=sys.stderr)
|
|
sys.exit(2)
|
|
|
|
# Report warning mismatches (allow commit with warning)
|
|
if warning_mismatches:
|
|
print("⚠️ WARNING: README.md minor differences detected", file=sys.stderr)
|
|
print("", file=sys.stderr)
|
|
for key, root_val, plugin_val in warning_mismatches:
|
|
print(f" {key}:", file=sys.stderr)
|
|
print(f" Root README: {root_val}", file=sys.stderr)
|
|
print(f" Plugin README: {plugin_val}", file=sys.stderr)
|
|
print("", file=sys.stderr)
|
|
print("Consider updating both READMEs for consistency.", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|