TradingAgents/.claude/hooks/unified_git_automation.py

307 lines
8.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Unified Git Automation Hook - Dispatcher for SubagentStop Git Operations
Consolidates SubagentStop git automation hooks:
- auto_git_workflow.py (commit, push, PR creation)
Hook: SubagentStop (runs when doc-master completes)
Matcher: doc-master (last agent in parallel validation phase)
Environment Variables (opt-in/opt-out):
AUTO_GIT_ENABLED=true/false (default: false)
AUTO_GIT_PUSH=true/false (default: false)
AUTO_GIT_PR=true/false (default: false)
SESSION_FILE=path (default: latest in docs/sessions/)
Environment Variables (provided by Claude Code):
CLAUDE_AGENT_NAME - Name of the subagent that completed
CLAUDE_AGENT_STATUS - Status: "success" or "error"
Exit codes:
0: Always (non-blocking hook - failures are logged but don't block)
Usage:
# As SubagentStop hook (automatic)
CLAUDE_AGENT_NAME=doc-master AUTO_GIT_ENABLED=true python unified_git_automation.py
"""
import json
import os
import sys
from pathlib import Path
from typing import Dict, Optional
# ============================================================================
# Dynamic Library Discovery
# ============================================================================
def find_lib_dir() -> Optional[Path]:
"""
Find the lib directory dynamically.
Searches:
1. Relative to this file: ../lib
2. In project root: plugins/autonomous-dev/lib
3. In global install: ~/.autonomous-dev/lib
Returns:
Path to lib directory or None if not found
"""
candidates = [
Path(__file__).parent.parent / "lib", # Relative to hooks/
Path.cwd() / "plugins" / "autonomous-dev" / "lib", # Project root
Path.home() / ".autonomous-dev" / "lib", # Global install
]
for candidate in candidates:
if candidate.exists():
return candidate
return None
# Add lib to path
LIB_DIR = find_lib_dir()
if LIB_DIR:
sys.path.insert(0, str(LIB_DIR))
# Optional imports with graceful fallback
try:
from security_utils import validate_path, audit_log
HAS_SECURITY_UTILS = True
except ImportError:
HAS_SECURITY_UTILS = False
def audit_log(event_type: str, status: str, context: Dict) -> None:
pass
try:
from auto_implement_git_integration import execute_step8_git_operations
HAS_GIT_INTEGRATION = True
except ImportError:
HAS_GIT_INTEGRATION = False
# ============================================================================
# Configuration
# ============================================================================
def parse_bool(value: str) -> bool:
"""Parse boolean from various formats (case-insensitive)."""
return value.lower() in ('true', 'yes', '1')
# Check configuration from environment
AUTO_GIT_ENABLED = parse_bool(os.environ.get('AUTO_GIT_ENABLED', 'false'))
AUTO_GIT_PUSH = parse_bool(os.environ.get('AUTO_GIT_PUSH', 'false')) if AUTO_GIT_ENABLED else False
AUTO_GIT_PR = parse_bool(os.environ.get('AUTO_GIT_PR', 'false')) if AUTO_GIT_ENABLED else False
# ============================================================================
# Git Workflow Trigger
# ============================================================================
def should_trigger_git_workflow(agent_name: Optional[str]) -> bool:
"""
Check if git workflow should trigger based on agent name.
Only triggers for doc-master (last agent in parallel validation phase).
Args:
agent_name: Name of agent that just completed
Returns:
True if workflow should trigger, False otherwise
"""
if not agent_name:
return False
# Trigger for doc-master (last agent in parallel validation phase)
return agent_name == 'doc-master'
def check_git_workflow_consent() -> Dict[str, bool]:
"""
Check user consent for git operations via environment variables.
Returns:
Dict with consent flags:
{
'git_enabled': bool, # Master switch
'push_enabled': bool, # Push consent
'pr_enabled': bool, # PR consent
'all_enabled': bool # All three enabled
}
"""
all_enabled = AUTO_GIT_ENABLED and AUTO_GIT_PUSH and AUTO_GIT_PR
return {
'git_enabled': AUTO_GIT_ENABLED,
'push_enabled': AUTO_GIT_PUSH,
'pr_enabled': AUTO_GIT_PR,
'all_enabled': all_enabled,
}
def get_session_file_path() -> Optional[Path]:
"""
Get path to session file for workflow metadata.
Checks SESSION_FILE environment variable first, otherwise finds latest
session file in docs/sessions/ directory.
Returns:
Path to session file or None if not found/invalid
"""
session_file_env = os.environ.get('SESSION_FILE')
if session_file_env:
# Use explicit session file (validate security if available)
session_path = Path(session_file_env).resolve()
if HAS_SECURITY_UTILS:
try:
validated_path = validate_path(
session_path,
purpose='session file reading',
allow_missing=True,
)
return validated_path
except ValueError as e:
audit_log(
event_type='session_file_path_validation',
status='rejected',
context={'session_file': str(session_path), 'error': str(e)},
)
return None
else:
return session_path if session_path.exists() else None
# Find latest session file
session_dir = Path("docs/sessions")
if not session_dir.exists():
return None
session_files = list(session_dir.glob("*-pipeline.json"))
if not session_files:
return None
return sorted(session_files)[-1]
def execute_git_workflow(session_file: Path, consent: Dict[str, bool]) -> bool:
"""
Execute git workflow operations.
Args:
session_file: Path to session file with workflow metadata
consent: Consent flags for git operations
Returns:
True if executed successfully, False otherwise
"""
if not HAS_GIT_INTEGRATION:
return False
try:
# Execute git operations via library
result = execute_step8_git_operations(
session_file=session_file,
git_enabled=consent['git_enabled'],
push_enabled=consent['push_enabled'],
pr_enabled=consent['pr_enabled'],
)
return result.get('success', False)
except Exception as e:
if HAS_SECURITY_UTILS:
audit_log(
event_type='git_workflow_execution',
status='error',
context={'error': str(e)},
)
return False
# ============================================================================
# Main Hook Entry Point
# ============================================================================
def main() -> int:
"""
Main hook entry point.
Reads agent info from environment, executes git workflow if appropriate.
Returns:
Always 0 (non-blocking hook - failures logged but don't block)
"""
# Get agent info from environment
agent_name = os.environ.get("CLAUDE_AGENT_NAME")
agent_status = os.environ.get("CLAUDE_AGENT_STATUS", "success")
# Check if workflow should trigger
if not should_trigger_git_workflow(agent_name):
# Not the right agent - skip
output = {
"hookSpecificOutput": {
"hookEventName": "SubagentStop"
}
}
print(json.dumps(output))
return 0
# Only trigger on success
if agent_status != "success":
output = {
"hookSpecificOutput": {
"hookEventName": "SubagentStop"
}
}
print(json.dumps(output))
return 0
# Check consent
consent = check_git_workflow_consent()
if not consent['git_enabled']:
# Git automation disabled
output = {
"hookSpecificOutput": {
"hookEventName": "SubagentStop"
}
}
print(json.dumps(output))
return 0
# Get session file
session_file = get_session_file_path()
if not session_file:
# No session file - can't execute workflow
output = {
"hookSpecificOutput": {
"hookEventName": "SubagentStop"
}
}
print(json.dumps(output))
return 0
# Execute git workflow (non-blocking - errors logged but don't fail hook)
try:
execute_git_workflow(session_file, consent)
except Exception:
# Graceful degradation
pass
# Always succeed (non-blocking hook)
output = {
"hookSpecificOutput": {
"hookEventName": "SubagentStop"
}
}
print(json.dumps(output))
return 0
if __name__ == "__main__":
sys.exit(main())