TradingAgents/.claude/hooks/enforce_orchestrator.py

252 lines
8.1 KiB
Python
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Enforce Orchestrator Validation - PROJECT.md Gatekeeper (Phase 1)
Ensures orchestrator validated PROJECT.md alignment before implementation.
This prevents:
- Users bypassing /auto-implement
- Features implemented without PROJECT.md alignment check
- Work proceeding without strategic direction validation
Source of truth: PROJECT.md ARCHITECTURE (orchestrator PRIMARY MISSION)
Exit codes:
0: Orchestrator validation found (or strict mode disabled)
2: No orchestrator validation - BLOCKS commit
Usage:
# As PreCommit hook (automatic in strict mode)
python enforce_orchestrator.py
"""
import json
import sys
from pathlib import Path
from datetime import datetime, timedelta
import subprocess
def is_strict_mode_enabled() -> bool:
"""Check if strict mode is enabled."""
settings_file = Path(".claude/settings.local.json")
if not settings_file.exists():
return False
try:
with open(settings_file) as f:
settings = json.load(f)
return settings.get("strict_mode", False)
except Exception:
return False
def has_project_md() -> bool:
"""Check if PROJECT.md exists."""
return Path(".claude/PROJECT.md").exists()
def check_orchestrator_in_sessions() -> bool:
"""
Check for orchestrator activity in recent session files.
Looks for evidence in last 3 session files or files from last hour.
"""
sessions_dir = Path("docs/sessions")
if not sessions_dir.exists():
return False
# Get recent session files (last 3 or last hour)
cutoff_time = datetime.now() - timedelta(hours=1)
recent_sessions = []
for session_file in sessions_dir.glob("*.md"):
# Check modification time
mtime = datetime.fromtimestamp(session_file.stat().st_mtime)
if mtime > cutoff_time:
recent_sessions.append(session_file)
# If no sessions in last hour, check last 3 files
if not recent_sessions:
all_sessions = sorted(sessions_dir.glob("*.md"),
key=lambda f: f.stat().st_mtime,
reverse=True)
recent_sessions = all_sessions[:3]
# Search for orchestrator evidence
for session in recent_sessions:
try:
content = session.read_text().lower()
# Look for orchestrator markers
markers = [
"orchestrator",
"project.md alignment",
"validates alignment",
"alignment check",
]
if any(marker in content for marker in markers):
return True
except Exception:
continue
return False
def check_commit_message() -> bool:
"""Check if commit message indicates orchestrator validation."""
try:
# Get the staged commit message if it exists
result = subprocess.run(
["git", "log", "-1", "--pretty=%B"],
capture_output=True,
text=True,
check=False
)
if result.returncode == 0:
commit_msg = result.stdout.lower()
# Look for orchestrator markers in commit message
if "orchestrator" in commit_msg or "project.md" in commit_msg:
return True
except Exception:
pass
return False
def get_staged_files() -> list:
"""Get list of staged files."""
try:
result = subprocess.run(
["git", "diff", "--cached", "--name-only"],
capture_output=True,
text=True,
check=True
)
return [f for f in result.stdout.strip().split('\n') if f]
except Exception:
return []
def is_docs_only_commit() -> bool:
"""Check if this is a documentation-only commit (allow without orchestrator)."""
staged = get_staged_files()
if not staged:
return True
# If all files are docs, markdown, or configs, allow
doc_extensions = {'.md', '.txt', '.json', '.yml', '.yaml', '.toml'}
doc_paths = {'docs/', 'README', 'CHANGELOG', 'LICENSE', '.claude/'}
for file in staged:
# Skip if it's a source file
if file.startswith('src/') or file.startswith('lib/'):
return False
# Check extension
ext = Path(file).suffix.lower()
if ext and ext not in doc_extensions:
return False
# Check if in doc path
if not any(file.startswith(path) for path in doc_paths):
# Check if it's a hook or test file (allow)
if not (file.startswith('hooks/') or file.startswith('tests/')):
return False
return True
def main():
"""Enforce orchestrator validation in strict mode."""
# Only run on PreCommit
try:
data = json.loads(sys.stdin.read())
if data.get("hook") != "PreCommit":
sys.exit(0)
except Exception:
# If not running as hook, exit
sys.exit(0)
# Check if strict mode is enabled
if not is_strict_mode_enabled():
# Not in strict mode - no enforcement
sys.exit(0)
# Check if PROJECT.md exists
if not has_project_md():
# No PROJECT.md - can't enforce alignment
print(" No PROJECT.md found - orchestrator enforcement skipped",
file=sys.stderr)
sys.exit(0)
# Check if this is a docs-only commit (allow without orchestrator)
if is_docs_only_commit():
print(" Documentation-only commit - orchestrator not required",
file=sys.stderr)
sys.exit(0)
# Check for orchestrator evidence
has_orchestrator = (
check_orchestrator_in_sessions() or
check_commit_message()
)
if has_orchestrator:
print("✅ Orchestrator validation detected", file=sys.stderr)
sys.exit(0)
# No orchestrator evidence - BLOCK
print("\n" + "=" * 80, file=sys.stderr)
print("❌ ORCHESTRATOR VALIDATION REQUIRED", file=sys.stderr)
print("=" * 80, file=sys.stderr)
print(file=sys.stderr)
print("Strict mode requires orchestrator to validate PROJECT.md alignment",
file=sys.stderr)
print("before implementation work begins.", file=sys.stderr)
print(file=sys.stderr)
print("PROJECT.md ARCHITECTURE (orchestrator PRIMARY MISSION):", file=sys.stderr)
print(" 1. Read PROJECT.md (GOALS, SCOPE, CONSTRAINTS)", file=sys.stderr)
print(" 2. Validate: Does feature serve GOALS?", file=sys.stderr)
print(" 3. Validate: Is feature IN SCOPE?", file=sys.stderr)
print(" 4. Validate: Respects CONSTRAINTS?", file=sys.stderr)
print(" 5. BLOCK if not aligned OR proceed with agent pipeline",
file=sys.stderr)
print(file=sys.stderr)
print("No orchestrator activity found in:", file=sys.stderr)
print(" - Recent session files (docs/sessions/)", file=sys.stderr)
print(" - Commit message", file=sys.stderr)
print(file=sys.stderr)
print("=" * 80, file=sys.stderr)
print("HOW TO FIX", file=sys.stderr)
print("=" * 80, file=sys.stderr)
print(file=sys.stderr)
print("Option 1: Use /auto-implement (recommended):", file=sys.stderr)
print(" /auto-implement \"your feature description\"", file=sys.stderr)
print(" → orchestrator validates alignment automatically", file=sys.stderr)
print(" → Full 7-agent pipeline executes", file=sys.stderr)
print(file=sys.stderr)
print("Option 2: Manual orchestrator invocation:", file=sys.stderr)
print(" \"orchestrator: validate this feature against PROJECT.md\"", file=sys.stderr)
print(" → Creates session file with validation evidence", file=sys.stderr)
print(file=sys.stderr)
print("Option 3: Disable strict mode (not recommended):", file=sys.stderr)
print(" Edit .claude/settings.local.json:", file=sys.stderr)
print(' {"strict_mode": false}', file=sys.stderr)
print(file=sys.stderr)
print("=" * 80, file=sys.stderr)
print("Strict mode enforces PROJECT.md as gatekeeper.", file=sys.stderr)
print("This prevents scope drift and misaligned features.", file=sys.stderr)
print("=" * 80, file=sys.stderr)
print(file=sys.stderr)
sys.exit(2) # Block commit
if __name__ == "__main__":
main()