TradingAgents/.claude/hooks/unified_structure_enforcer.py

475 lines
15 KiB
Python
Executable File

#!/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()