#!/usr/bin/env python3 """ Unified Structure Enforcer - Consolidated Enforcement Dispatcher Consolidates 6 enforcement hooks into one dispatcher: - enforce_file_organization.py - enforce_bloat_prevention.py - enforce_command_limit.py - enforce_pipeline_complete.py - enforce_orchestrator.py - verify_agent_pipeline.py Uses dispatcher pattern from pre_tool_use.py: - Environment variable control per enforcer - Graceful degradation on errors - Dynamic lib directory discovery - Clear logging with [PASS], [FAIL], [SKIP] indicators Exit codes: - 0: All checks passed or skipped - 1: One or more checks failed Environment variables (all default to true): - ENFORCE_FILE_ORGANIZATION=true/false - ENFORCE_BLOAT_PREVENTION=true/false - ENFORCE_COMMAND_LIMIT=true/false - ENFORCE_PIPELINE_COMPLETE=true/false - ENFORCE_ORCHESTRATOR=true/false - VERIFY_AGENT_PIPELINE=true/false """ import json import sys import os import subprocess from pathlib import Path from datetime import datetime, timedelta from typing import Tuple, Optional def find_lib_directory(hook_path: Path) -> Optional[Path]: """ Find lib directory dynamically (Issue #113). Checks multiple locations in order: 1. Development: plugins/autonomous-dev/lib (relative to hook) 2. Local install: ~/.claude/lib 3. Marketplace: ~/.claude/plugins/autonomous-dev/lib Args: hook_path: Path to this hook script Returns: Path to lib directory if found, None otherwise (graceful failure) """ # Try development location first (plugins/autonomous-dev/hooks/) dev_lib = hook_path.parent.parent / "lib" if dev_lib.exists() and dev_lib.is_dir(): return dev_lib # Try local install (~/.claude/lib) home = Path.home() local_lib = home / ".claude" / "lib" if local_lib.exists() and local_lib.is_dir(): return local_lib # Try marketplace location (~/.claude/plugins/autonomous-dev/lib) marketplace_lib = home / ".claude" / "plugins" / "autonomous-dev" / "lib" if marketplace_lib.exists() and marketplace_lib.is_dir(): return marketplace_lib # Not found - graceful failure return None # Add lib directory to path dynamically LIB_DIR = find_lib_directory(Path(__file__)) if LIB_DIR: sys.path.insert(0, str(LIB_DIR)) def load_env(): """Load .env file from project root if it exists.""" env_file = Path(os.getcwd()) / ".env" if env_file.exists(): try: with open(env_file, 'r') as f: for line in f: line = line.strip() if not line or line.startswith('#'): continue if '=' in line: key, value = line.split('=', 1) key = key.strip() value = value.strip().strip('"').strip("'") if key not in os.environ: os.environ[key] = value except Exception: pass # Silently skip load_env() def is_enabled(env_var: str, default: bool = True) -> bool: """Check if enforcer is enabled via environment variable.""" value = os.getenv(env_var, str(default)).lower() return value in ('true', '1', 'yes', 'on') # ============================================================================ # Enforcer 1: File Organization # ============================================================================ def enforce_file_organization() -> Tuple[bool, str]: """ Enforce file organization standards. Returns: (passed, reason) """ if not is_enabled("ENFORCE_FILE_ORGANIZATION", True): return True, "[SKIP] File organization enforcement disabled" try: # Get staged files result = subprocess.run( ["git", "diff", "--cached", "--name-only"], capture_output=True, text=True, check=True ) staged_files = [f.strip() for f in result.stdout.split('\n') if f.strip()] if not staged_files: return True, "[PASS] No staged files to check" # Check for violations (root directory clutter) violations = [] for file in staged_files: path = Path(file) # Skip allowed root files if path.parent == Path('.') and path.name in ( 'README.md', 'LICENSE', '.gitignore', '.env', 'pytest.ini', 'setup.py', 'pyproject.toml', 'requirements.txt', 'Makefile' ): continue # Check for new files in root (not subdirectories) if path.parent == Path('.'): # Allow specific patterns if path.suffix in ('.md', '.py', '.sh'): violations.append(f"{file} should be in docs/ or scripts/ directory") if violations: return False, f"[FAIL] File organization violations:\n" + "\n".join(f" - {v}" for v in violations) return True, "[PASS] File organization check passed" except Exception as e: # Graceful degradation return True, f"[SKIP] File organization check error: {e}" # ============================================================================ # Enforcer 2: Bloat Prevention # ============================================================================ def enforce_bloat_prevention() -> Tuple[bool, str]: """ Enforce bloat prevention limits. Returns: (passed, reason) """ if not is_enabled("ENFORCE_BLOAT_PREVENTION", True): return True, "[SKIP] Bloat prevention enforcement disabled" try: # Count documentation files doc_count = len(list(Path("docs").glob("**/*.md"))) if Path("docs").exists() else 0 # Count agent files agent_dir = Path("plugins/autonomous-dev/agents") agent_count = len(list(agent_dir.glob("*.md"))) if agent_dir.exists() else 0 # Count command files cmd_dir = Path("plugins/autonomous-dev/commands") cmd_count = len(list(cmd_dir.glob("*.md"))) if cmd_dir.exists() else 0 violations = [] # Check limits (these are generous to prevent bloat) if doc_count > 100: violations.append(f"Too many doc files: {doc_count} > 100") if agent_count > 25: violations.append(f"Too many agents: {agent_count} > 25 (trust the model)") if cmd_count > 15: violations.append(f"Too many commands: {cmd_count} > 15") if violations: return False, f"[FAIL] Bloat prevention violations:\n" + "\n".join(f" - {v}" for v in violations) return True, "[PASS] Bloat prevention check passed" except Exception as e: # Graceful degradation return True, f"[SKIP] Bloat prevention check error: {e}" # ============================================================================ # Enforcer 3: Command Limit # ============================================================================ def enforce_command_limit() -> Tuple[bool, str]: """ Enforce 15-command limit. Returns: (passed, reason) """ if not is_enabled("ENFORCE_COMMAND_LIMIT", True): return True, "[SKIP] Command limit enforcement disabled" try: commands_dir = Path("plugins/autonomous-dev/commands") if not commands_dir.exists(): return True, "[PASS] No commands directory found" # Find all active commands (not in archive) active_commands = [ f.stem for f in commands_dir.glob("*.md") if f.parent.name != "archive" ] if len(active_commands) > 15: return False, f"[FAIL] Too many commands: {len(active_commands)} > 15\n Commands: {', '.join(sorted(active_commands))}" return True, f"[PASS] Command limit check passed ({len(active_commands)}/15)" except Exception as e: # Graceful degradation return True, f"[SKIP] Command limit check error: {e}" # ============================================================================ # Enforcer 4: Pipeline Complete # ============================================================================ def enforce_pipeline_complete() -> Tuple[bool, str]: """ Enforce complete pipeline execution for auto-implement features. Returns: (passed, reason) """ if not is_enabled("ENFORCE_PIPELINE_COMPLETE", True): return True, "[SKIP] Pipeline completeness enforcement disabled" try: sessions_dir = Path("docs/sessions") if not sessions_dir.exists(): return True, "[PASS] No sessions directory (not using /auto-implement)" today = datetime.now().strftime("%Y%m%d") # Find most recent pipeline file for today pipeline_files = sorted( sessions_dir.glob(f"{today}-*-pipeline.json"), reverse=True ) if not pipeline_files: return True, "[PASS] No pipeline file for today (not using /auto-implement)" # Check if pipeline is complete pipeline_file = pipeline_files[0] try: with open(pipeline_file) as f: data = json.load(f) required_agents = [ "researcher", "planner", "test-master", "implementer", "reviewer", "security-auditor", "doc-master" ] # Check which agents ran agents_run = data.get("agents_completed", []) missing = [a for a in required_agents if a not in agents_run] if missing: return False, f"[FAIL] Incomplete pipeline - missing agents: {', '.join(missing)}\n Tip: Complete the /auto-implement workflow before committing" return True, "[PASS] Pipeline completeness check passed" except Exception as e: # Can't read pipeline file - graceful skip return True, f"[SKIP] Pipeline file read error: {e}" except Exception as e: # Graceful degradation return True, f"[SKIP] Pipeline completeness check error: {e}" # ============================================================================ # Enforcer 5: Orchestrator Validation # ============================================================================ def enforce_orchestrator() -> Tuple[bool, str]: """ Enforce orchestrator PROJECT.md validation. Returns: (passed, reason) """ if not is_enabled("ENFORCE_ORCHESTRATOR", True): return True, "[SKIP] Orchestrator enforcement disabled" try: # Check if strict mode is enabled settings_file = Path(".claude/settings.local.json") strict_mode = False if settings_file.exists(): try: with open(settings_file) as f: settings = json.load(f) strict_mode = settings.get("strict_mode", False) except Exception: pass if not strict_mode: return True, "[SKIP] Strict mode not enabled" # Check if PROJECT.md exists if not Path(".claude/PROJECT.md").exists(): return True, "[PASS] No PROJECT.md (not required)" # Check for orchestrator validation in recent sessions sessions_dir = Path("docs/sessions") if not sessions_dir.exists(): return False, "[FAIL] No orchestrator validation found - use /auto-implement for features" # Look for orchestrator logs in last 24 hours cutoff = datetime.now() - timedelta(hours=24) for session_file in sorted(sessions_dir.glob("*.json"), reverse=True): try: mtime = datetime.fromtimestamp(session_file.stat().st_mtime) if mtime < cutoff: break # Stop searching old files with open(session_file) as f: content = f.read() if "orchestrator" in content.lower() or "project.md" in content.lower(): return True, "[PASS] Orchestrator validation found" except Exception: continue return False, "[FAIL] No orchestrator validation in last 24h - use /auto-implement for features" except Exception as e: # Graceful degradation return True, f"[SKIP] Orchestrator check error: {e}" # ============================================================================ # Enforcer 6: Agent Pipeline Verification # ============================================================================ def verify_agent_pipeline() -> Tuple[bool, str]: """ Verify expected agents ran for feature implementations. Returns: (passed, reason) """ if not is_enabled("VERIFY_AGENT_PIPELINE", True): return True, "[SKIP] Agent pipeline verification disabled" try: sessions_dir = Path("docs/sessions") if not sessions_dir.exists(): return True, "[PASS] No sessions directory (not using agents)" today = datetime.now().strftime("%Y%m%d") # Find today's pipeline file pipeline_files = sorted( sessions_dir.glob(f"{today}-*-pipeline.json"), reverse=True ) if not pipeline_files: return True, "[PASS] No pipeline file for today (not a feature commit)" # Check which agents ran pipeline_file = pipeline_files[0] try: with open(pipeline_file) as f: data = json.load(f) agents_run = data.get("agents_completed", []) # Expected agents for full workflow expected = ["researcher", "test-master", "implementer", "reviewer", "doc-master"] missing = [a for a in expected if a not in agents_run] # Check if strict mode is enabled strict_pipeline = os.getenv("STRICT_PIPELINE", "0") == "1" if missing: msg = f"[WARN] Missing agents: {', '.join(missing)}" if strict_pipeline: return False, f"[FAIL] {msg} (STRICT_PIPELINE=1)" else: return True, f"{msg} (warning only)" return True, f"[PASS] Agent pipeline verification passed ({len(agents_run)} agents ran)" except Exception as e: return True, f"[SKIP] Pipeline file read error: {e}" except Exception as e: # Graceful degradation return True, f"[SKIP] Agent pipeline verification error: {e}" # ============================================================================ # Main Dispatcher # ============================================================================ def main(): """Run all enabled enforcers and aggregate results.""" print("=" * 80) print("UNIFIED STRUCTURE ENFORCER") print("=" * 80) # Run all enforcers results = [ ("File Organization", enforce_file_organization()), ("Bloat Prevention", enforce_bloat_prevention()), ("Command Limit", enforce_command_limit()), ("Pipeline Complete", enforce_pipeline_complete()), ("Orchestrator Validation", enforce_orchestrator()), ("Agent Pipeline", verify_agent_pipeline()), ] # Display results all_passed = True for name, (passed, reason) in results: print(f"\n{name}:") print(f" {reason}") if not passed: all_passed = False print("\n" + "=" * 80) if all_passed: print("RESULT: All checks passed") print("=" * 80) sys.exit(0) else: print("RESULT: One or more checks failed") print("=" * 80) sys.exit(1) if __name__ == "__main__": main()