TradingAgents/.claude/hooks/validate_project_alignment.py

217 lines
6.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""
PROJECT.md Alignment Validation Hook - Gatekeeper for STRICT MODE
This hook enforces that PROJECT.md exists and all work aligns with it.
It's a BLOCKING hook that prevents commits if alignment fails.
What it checks:
- PROJECT.md exists
- PROJECT.md has required sections (GOALS, SCOPE, CONSTRAINTS)
- Current changes align with PROJECT.md SCOPE
- Documentation mentions PROJECT.md
This is the GATEKEEPER for strict mode - nothing proceeds without alignment.
Relevant Skills:
- project-alignment-validation: Alignment checklist, semantic validation approach
Usage:
Add to .claude/settings.local.json PreCommit hooks:
{
"hooks": {
"PreCommit": [
{
"type": "command",
"command": "python .claude/hooks/validate_project_alignment.py || exit 1"
}
]
}
}
Exit codes:
- 0: PROJECT.md aligned
- 1: PROJECT.md missing or misaligned (blocks commit)
"""
import sys
import re
from pathlib import Path
from typing import Tuple
def get_project_root() -> Path:
"""Find project root directory."""
current = Path.cwd()
# Look for PROJECT.md or .git directory
while current != current.parent:
if (current / "PROJECT.md").exists() or (current / ".git").exists():
return current
current = current.parent
return Path.cwd()
def check_project_md_exists(project_root: Path) -> Tuple[bool, str]:
"""Check if PROJECT.md exists."""
project_md_path = project_root / "PROJECT.md"
if not project_md_path.exists():
# Check alternate locations
alt_path = project_root / ".claude" / "PROJECT.md"
if alt_path.exists():
return True, f"✅ PROJECT.md found at {alt_path}"
return False, (
"❌ PROJECT.md NOT FOUND\n"
"\n"
"STRICT MODE requires PROJECT.md to define strategic direction.\n"
"\n"
"Create PROJECT.md with:\n"
" 1. GOALS - What you're building and success metrics\n"
" 2. SCOPE - What's in/out of scope\n"
" 3. CONSTRAINTS - Technical stack, performance, security limits\n"
" 4. ARCHITECTURE - System design and patterns\n"
"\n"
"Quick setup:\n"
" /setup --create-project-md\n"
"\n"
"Or copy template:\n"
" cp .claude/templates/PROJECT.md PROJECT.md\n"
)
return True, f"✅ PROJECT.md found at {project_md_path}"
def check_required_sections(project_root: Path) -> Tuple[bool, str]:
"""Check PROJECT.md has required sections."""
project_md_path = project_root / "PROJECT.md"
alt_path = project_root / ".claude" / "PROJECT.md"
# Use whichever exists
path_to_check = project_md_path if project_md_path.exists() else alt_path
if not path_to_check.exists():
return False, "PROJECT.md not found"
content = path_to_check.read_text()
required_sections = ["GOALS", "SCOPE", "CONSTRAINTS"]
missing_sections = []
for section in required_sections:
# Look for section headers (## GOALS, # GOALS, etc.)
if not re.search(rf'^#+\s*{section}', content, re.MULTILINE | re.IGNORECASE):
missing_sections.append(section)
if missing_sections:
return False, (
f"❌ PROJECT.md missing required sections:\n"
+ "\n".join(f" - {s}" for s in missing_sections) +
f"\n\nAdd these sections to define strategic direction.\n"
f"See .claude/templates/PROJECT.md for structure."
)
return True, f"✅ PROJECT.md has all required sections ({', '.join(required_sections)})"
def check_scope_alignment(project_root: Path) -> Tuple[bool, str]:
"""
Check if current changes align with PROJECT.md SCOPE.
This is a basic check - full alignment validation happens in orchestrator.
Just verifies that someone has considered alignment.
"""
project_md_path = project_root / "PROJECT.md"
alt_path = project_root / ".claude" / "PROJECT.md"
path_to_check = project_md_path if project_md_path.exists() else alt_path
if not path_to_check.exists():
return False, "PROJECT.md not found"
content = path_to_check.read_text()
# Check if SCOPE section has content (not empty)
scope_match = re.search(
r'^\s*#+\s*SCOPE\s*\n(.*?)(?=\n#+\s|\Z)',
content,
re.MULTILINE | re.IGNORECASE | re.DOTALL
)
if not scope_match:
return False, (
"❌ PROJECT.md SCOPE section empty or missing\n"
"\n"
"Define what's IN SCOPE and OUT OF SCOPE to guide development.\n"
)
scope_content = scope_match.group(1).strip()
if len(scope_content) < 50: # Arbitrary minimum
return False, (
"❌ PROJECT.md SCOPE section too brief\n"
"\n"
"Add specific items to SCOPE section:\n"
" - What features are in scope\n"
" - What features are explicitly out of scope\n"
" - Boundaries and constraints\n"
)
return True, "✅ PROJECT.md SCOPE defined (alignment enforced by orchestrator)"
def main() -> int:
"""
Run PROJECT.md alignment validation.
Returns:
0 if aligned
1 if misaligned (blocks commit)
"""
print("🔍 Validating PROJECT.md alignment (STRICT MODE)...\n")
project_root = get_project_root()
# Run all checks
checks = [
("PROJECT.md exists", check_project_md_exists(project_root)),
("Required sections", check_required_sections(project_root)),
("SCOPE defined", check_scope_alignment(project_root)),
]
all_passed = True
for check_name, (passed, message) in checks:
if passed:
print(message)
else:
print(f"{check_name} FAILED:")
print(f" {message}")
print()
all_passed = False
print()
if all_passed:
print("✅ PROJECT.md alignment validation PASSED")
print()
print("NOTE: Orchestrator will perform detailed alignment check")
print(" before feature implementation begins.")
return 0
else:
print("❌ PROJECT.md alignment validation FAILED")
print()
print("STRICT MODE: Cannot commit without PROJECT.md alignment.")
print()
print("Fix the issues above, then retry commit.")
print()
print("To bypass (NOT RECOMMENDED):")
print(" git commit --no-verify")
return 1
if __name__ == "__main__":
sys.exit(main())