TradingAgents/.claude/hooks/auto_track_issues.py

344 lines
10 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Automatic GitHub Issue Tracking Hook
Automatically creates GitHub Issues from testing results in the background.
Triggers:
- After test completion (UserPromptSubmit)
- Before push (pre-push hook)
- On commit (post-commit hook)
Usage:
- Runs automatically when GITHUB_AUTO_TRACK_ISSUES=true in .env
- Creates issues for:
- Test failures (pytest)
- GenAI validation findings (UX, architecture)
- System performance opportunities
Configuration (.env):
GITHUB_AUTO_TRACK_ISSUES=true # Enable auto-tracking
GITHUB_TRACK_ON_PUSH=true # Track before push
GITHUB_TRACK_ON_COMMIT=false # Track after commit (optional)
GITHUB_TRACK_THRESHOLD=medium # Minimum priority (low/medium/high)
GITHUB_DRY_RUN=false # Preview only
"""
import os
import sys
import json
import subprocess
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional
# Configuration from .env
AUTO_TRACK_ENABLED = os.getenv("GITHUB_AUTO_TRACK_ISSUES", "false").lower() == "true"
TRACK_ON_PUSH = os.getenv("GITHUB_TRACK_ON_PUSH", "true").lower() == "true"
TRACK_ON_COMMIT = os.getenv("GITHUB_TRACK_ON_COMMIT", "false").lower() == "true"
TRACK_THRESHOLD = os.getenv("GITHUB_TRACK_THRESHOLD", "medium").lower()
DRY_RUN = os.getenv("GITHUB_DRY_RUN", "false").lower() == "true"
# Priority thresholds
PRIORITY_LEVELS = {"low": 1, "medium": 2, "high": 3}
def log(message: str, level: str = "INFO"):
"""Log message with timestamp."""
timestamp = datetime.now().strftime("%H:%M:%S")
print(f"[{timestamp}] [{level}] {message}", file=sys.stderr)
def is_gh_authenticated() -> bool:
"""Check if GitHub CLI is authenticated."""
try:
result = subprocess.run(
["gh", "auth", "status"],
capture_output=True,
text=True,
timeout=5
)
return result.returncode == 0
except (subprocess.TimeoutExpired, FileNotFoundError):
return False
def check_prerequisites() -> bool:
"""Check if all prerequisites are met."""
if not AUTO_TRACK_ENABLED:
log("Auto-tracking disabled (GITHUB_AUTO_TRACK_ISSUES=false)", "DEBUG")
return False
# Check if gh CLI is installed
try:
subprocess.run(["gh", "--version"], capture_output=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError):
log("GitHub CLI (gh) not installed. Install: brew install gh", "WARN")
return False
# Check if authenticated
if not is_gh_authenticated():
log("GitHub CLI not authenticated. Run: gh auth login", "WARN")
return False
return True
def parse_pytest_output() -> List[Dict]:
"""Parse pytest output to find test failures."""
issues = []
# Look for pytest cache
pytest_cache = Path(".pytest_cache/v/cache/lastfailed")
if not pytest_cache.exists():
log("No pytest failures found", "DEBUG")
return issues
try:
with open(pytest_cache) as f:
failed_tests = json.load(f)
for test_path, _ in failed_tests.items():
# Extract test info
parts = test_path.split("::")
file_path = parts[0] if parts else "unknown"
test_name = parts[-1] if len(parts) > 1 else test_path
issues.append({
"type": "bug",
"layer": "layer-1",
"title": f"{test_name} fails - test failure",
"body": f"Test failure detected in `{test_path}`\n\nRun: `pytest {test_path} -v`",
"labels": ["bug", "automated", "layer-1", "test-failure"],
"priority": "high",
"source": "pytest",
"test_path": test_path,
"file_path": file_path,
"test_name": test_name
})
except Exception as e:
log(f"Error parsing pytest output: {e}", "ERROR")
return issues
def parse_genai_validation() -> List[Dict]:
"""Parse GenAI validation results for issues."""
issues = []
# Look for recent validation reports in docs/sessions/
sessions_dir = Path("docs/sessions")
if not sessions_dir.exists():
return issues
# Find recent validation files
validation_files = []
for pattern in ["uat-validation-*.md", "architecture-validation-*.md"]:
validation_files.extend(sessions_dir.glob(pattern))
# Sort by modification time, get most recent
validation_files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
for vfile in validation_files[:5]: # Check last 5 validation reports
try:
content = vfile.read_text()
# Parse UX issues (score < 8/10)
if "uat-validation" in vfile.name:
# Simple heuristic: look for low scores
if "UX Score: 6/10" in content or "UX Score: 7/10" in content:
issues.append({
"type": "enhancement",
"layer": "layer-2",
"title": "UX improvement needed",
"body": f"GenAI validation found UX issues\n\nSee: {vfile.name}",
"labels": ["enhancement", "ux", "genai-detected", "layer-2"],
"priority": "medium",
"source": "genai-uat"
})
# Parse architectural drift
if "architecture-validation" in vfile.name:
if "DRIFT" in content or "VIOLATION" in content:
issues.append({
"type": "architecture",
"layer": "layer-2",
"title": "Architectural drift detected",
"body": f"GenAI validation found architectural drift\n\nSee: {vfile.name}",
"labels": ["architecture", "genai-detected", "layer-2"],
"priority": "high",
"source": "genai-architecture"
})
except Exception as e:
log(f"Error parsing {vfile.name}: {e}", "ERROR")
return issues
def parse_performance_analysis() -> List[Dict]:
"""Parse system performance analysis for optimization opportunities."""
issues = []
# Look for performance analysis results
# (This would parse output from /test system-performance)
# For now, return empty - will be implemented when command exists
return issues
def check_existing_issue(title: str) -> Optional[str]:
"""Check if issue with similar title already exists."""
try:
result = subprocess.run(
["gh", "issue", "list", "--search", f"{title} in:title", "--json", "number,title"],
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
issues = json.loads(result.stdout)
if issues:
return issues[0]["number"]
except Exception as e:
log(f"Error checking existing issues: {e}", "WARN")
return None
def create_github_issue(issue: Dict) -> Optional[str]:
"""Create GitHub Issue using gh CLI."""
title = issue["title"]
body = issue["body"]
labels = ",".join(issue["labels"])
# Check for duplicates
existing = check_existing_issue(title)
if existing:
log(f"Skipping duplicate issue: #{existing} - {title}", "DEBUG")
return None
# Check priority threshold
issue_priority = PRIORITY_LEVELS.get(issue["priority"], 1)
threshold_priority = PRIORITY_LEVELS.get(TRACK_THRESHOLD, 2)
if issue_priority < threshold_priority:
log(f"Skipping low priority issue: {title}", "DEBUG")
return None
if DRY_RUN:
log(f"[DRY RUN] Would create issue: {title}", "INFO")
return None
try:
cmd = [
"gh", "issue", "create",
"--title", title,
"--body", body,
"--label", labels
]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
issue_url = result.stdout.strip()
log(f"✅ Created issue: {issue_url}", "INFO")
return issue_url
else:
log(f"Failed to create issue: {result.stderr}", "ERROR")
except Exception as e:
log(f"Error creating issue: {e}", "ERROR")
return None
def collect_issues() -> List[Dict]:
"""Collect all issues from different sources."""
all_issues = []
log("Collecting issues from testing results...", "DEBUG")
# Layer 1: pytest failures
pytest_issues = parse_pytest_output()
all_issues.extend(pytest_issues)
if pytest_issues:
log(f"Found {len(pytest_issues)} test failures", "INFO")
# Layer 2: GenAI validation
genai_issues = parse_genai_validation()
all_issues.extend(genai_issues)
if genai_issues:
log(f"Found {len(genai_issues)} GenAI findings", "INFO")
# Layer 3: Performance analysis
perf_issues = parse_performance_analysis()
all_issues.extend(perf_issues)
if perf_issues:
log(f"Found {len(perf_issues)} optimization opportunities", "INFO")
return all_issues
def track_issues_automatically():
"""Main function - automatically track issues."""
log("Starting automatic issue tracking...", "INFO")
# Check prerequisites
if not check_prerequisites():
log("Prerequisites not met, skipping", "DEBUG")
return
# Collect issues
issues = collect_issues()
if not issues:
log("No issues found to track", "DEBUG")
return
log(f"Found {len(issues)} total issues", "INFO")
# Create GitHub Issues
created = 0
skipped = 0
for issue in issues:
url = create_github_issue(issue)
if url:
created += 1
else:
skipped += 1
# Summary
if created > 0:
log(f"✅ Created {created} GitHub issues", "INFO")
if not DRY_RUN:
log("View: gh issue list --label automated", "INFO")
if skipped > 0:
log(f"⏭️ Skipped {skipped} issues (duplicates or low priority)", "DEBUG")
def main():
"""Entry point."""
try:
track_issues_automatically()
except KeyboardInterrupt:
log("Interrupted by user", "WARN")
sys.exit(1)
except Exception as e:
log(f"Unexpected error: {e}", "ERROR")
sys.exit(1)
if __name__ == "__main__":
main()