307 lines
8.8 KiB
Python
Executable File
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())
|