382 lines
14 KiB
Python
Executable File
382 lines
14 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Allegro Heartbeat Daemon - 15-minute autonomous wakeups
|
|
Checks Gitea, performs high-leverage actions, logs EVERYTHING
|
|
Makes Alexander proud with comprehensive overnight production
|
|
"""
|
|
|
|
import subprocess
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
# Configuration
|
|
LOG_DIR = Path("/root/allegro/heartbeat_logs")
|
|
LOG_DIR.mkdir(exist_ok=True)
|
|
GITEA_URL = "http://143.198.27.163:3000"
|
|
TOKEN = "05d9e6b4ef5f9e0402c0da7dc8cabfb5aa92ccf2"
|
|
|
|
def log(message, level="INFO"):
|
|
"""Log with timestamp and level"""
|
|
timestamp = datetime.now().isoformat()
|
|
log_entry = f"[{timestamp}] [{level}] {message}"
|
|
print(log_entry)
|
|
|
|
# Write to daily log file
|
|
log_file = LOG_DIR / f"heartbeat_{datetime.now().strftime('%Y-%m-%d')}.log"
|
|
with open(log_file, 'a') as f:
|
|
f.write(log_entry + '\n')
|
|
|
|
def check_gitea_health():
|
|
"""Check if Gitea is responsive"""
|
|
try:
|
|
result = subprocess.run(
|
|
['curl', '-s', '-o', '/dev/null', '-w', '%{http_code}',
|
|
f'{GITEA_URL}/api/v1/version'],
|
|
capture_output=True, text=True, timeout=10
|
|
)
|
|
status = result.stdout.strip()
|
|
latency = 0 # Would need curl timing info
|
|
|
|
if status == '200':
|
|
log(f"Gitea health check: HTTP {status} ✓", "SUCCESS")
|
|
return {"healthy": True, "status": status, "latency_ms": latency}
|
|
else:
|
|
log(f"Gitea returned status {status}", "WARNING")
|
|
return {"healthy": False, "status": status, "latency_ms": latency}
|
|
except Exception as e:
|
|
log(f"Gitea check failed: {e}", "ERROR")
|
|
return {"healthy": False, "status": "ERROR", "error": str(e)}
|
|
|
|
def scan_repositories():
|
|
"""Scan all Timmy Foundation repositories"""
|
|
repos = ['timmy-home', 'timmy-config', 'the-nexus', '.profile']
|
|
repo_stats = {}
|
|
|
|
for repo in repos:
|
|
try:
|
|
# Get open issues count
|
|
result = subprocess.run([
|
|
'curl', '-s',
|
|
f'{GITEA_URL}/api/v1/repos/Timmy_Foundation/{repo}/issues?state=open&limit=1',
|
|
'-H', f'Authorization: token {TOKEN}'
|
|
], capture_output=True, text=True, timeout=10)
|
|
|
|
issues = json.loads(result.stdout)
|
|
open_count = len(issues) if isinstance(issues, list) else 0
|
|
|
|
# Get open PRs count
|
|
result = subprocess.run([
|
|
'curl', '-s',
|
|
f'{GITEA_URL}/api/v1/repos/Timmy_Foundation/{repo}/pulls?state=open',
|
|
'-H', f'Authorization: token {TOKEN}'
|
|
], capture_output=True, text=True, timeout=10)
|
|
|
|
prs = json.loads(result.stdout)
|
|
pr_count = len(prs) if isinstance(prs, list) else 0
|
|
|
|
repo_stats[repo] = {
|
|
'open_issues': open_count,
|
|
'open_prs': pr_count
|
|
}
|
|
|
|
log(f"Scanned {repo}: {open_count} issues, {pr_count} PRs open", "SCAN")
|
|
|
|
except Exception as e:
|
|
log(f"Failed to scan {repo}: {e}", "ERROR")
|
|
repo_stats[repo] = {'error': str(e)}
|
|
|
|
return repo_stats
|
|
|
|
def get_actionable_items():
|
|
"""Get list of actionable items from Gitea - COMPREHENSIVE SCAN"""
|
|
actions = []
|
|
|
|
log("Beginning comprehensive actionable item scan...", "SCAN")
|
|
|
|
# 1. Check for mergeable PRs (HIGHEST PRIORITY)
|
|
try:
|
|
result = subprocess.run([
|
|
'curl', '-s',
|
|
f'{GITEA_URL}/api/v1/repos/Timmy_Foundation/timmy-home/pulls?state=open',
|
|
'-H', f'Authorization: token {TOKEN}'
|
|
], capture_output=True, text=True, timeout=15)
|
|
|
|
prs = json.loads(result.stdout)
|
|
log(f"Found {len(prs)} open PRs in timmy-home", "SCAN")
|
|
|
|
for pr in prs:
|
|
# Check detailed PR info for mergeable status
|
|
pr_num = pr.get('number')
|
|
pr_title = pr.get('title', '')[:60]
|
|
|
|
# Get detailed PR info
|
|
detail_result = subprocess.run([
|
|
'curl', '-s',
|
|
f'{GITEA_URL}/api/v1/repos/Timmy_Foundation/timmy-home/pulls/{pr_num}',
|
|
'-H', f'Authorization: token {TOKEN}'
|
|
], capture_output=True, text=True, timeout=10)
|
|
|
|
try:
|
|
pr_detail = json.loads(detail_result.stdout)
|
|
mergeable = pr_detail.get('mergeable', False)
|
|
|
|
if mergeable:
|
|
actions.append({
|
|
'type': 'merge_pr',
|
|
'priority': 100,
|
|
'title': pr_title,
|
|
'number': pr_num,
|
|
'repo': 'timmy-home',
|
|
'description': f"Mergeable PR #{pr_num}",
|
|
'estimated_time': '2 minutes'
|
|
})
|
|
log(f"PRIORITY: Mergeable PR found - #{pr_num}: {pr_title}", "HIGH")
|
|
except:
|
|
pass
|
|
|
|
except Exception as e:
|
|
log(f"PR scan error: {e}", "ERROR")
|
|
|
|
# 2. Check for issues needing triage (no comments, no labels)
|
|
try:
|
|
result = subprocess.run([
|
|
'curl', '-s',
|
|
f'{GITEA_URL}/api/v1/repos/Timmy_Foundation/timmy-home/issues?state=open&limit=20',
|
|
'-H', f'Authorization: token {TOKEN}'
|
|
], capture_output=True, text=True, timeout=15)
|
|
|
|
issues = json.loads(result.stdout)
|
|
untriaged = [i for i in issues if i.get('comments', 0) == 0 and not i.get('labels')]
|
|
|
|
log(f"Found {len(untriaged)} untriaged issues", "SCAN")
|
|
|
|
for issue in untriaged[:3]: # Top 3
|
|
actions.append({
|
|
'type': 'triage',
|
|
'priority': 50,
|
|
'title': issue.get('title', '')[:60],
|
|
'number': issue.get('number'),
|
|
'repo': 'timmy-home',
|
|
'description': f"Issue #{issue['number']} needs triage (no comments, no labels)",
|
|
'estimated_time': '1 minute'
|
|
})
|
|
|
|
except Exception as e:
|
|
log(f"Issue scan error: {e}", "ERROR")
|
|
|
|
# 3. Check for stale issues (old, no activity)
|
|
try:
|
|
# This would need date filtering - simplified for now
|
|
pass
|
|
except:
|
|
pass
|
|
|
|
# 4. Check for documentation gaps
|
|
try:
|
|
result = subprocess.run([
|
|
'curl', '-s',
|
|
f'{GITEA_URL}/api/v1/repos/Timmy_Foundation/timmy-home/issues?state=open&labels=documentation',
|
|
'-H', f'Authorization: token {TOKEN}'
|
|
], capture_output=True, text=True, timeout=10)
|
|
|
|
docs = json.loads(result.stdout)
|
|
if docs:
|
|
log(f"Found {len(docs)} documentation issues", "SCAN")
|
|
|
|
except:
|
|
pass
|
|
|
|
# Sort by priority (descending)
|
|
actions.sort(key=lambda x: -x['priority'])
|
|
|
|
log(f"Actionable items found: {len(actions)} (top priority: {actions[0]['priority'] if actions else 'N/A'})", "SUMMARY")
|
|
|
|
return actions
|
|
|
|
def perform_action(action):
|
|
"""Perform the selected high-leverage action - WITH FULL LOGGING"""
|
|
action_type = action['type']
|
|
number = action['number']
|
|
repo = action['repo']
|
|
|
|
log(f"EXECUTING: {action_type} on #{number} in {repo}", "ACTION")
|
|
log(f" Title: {action['title']}", "DETAIL")
|
|
log(f" Priority: {action['priority']}", "DETAIL")
|
|
log(f" Est. time: {action['estimated_time']}", "DETAIL")
|
|
|
|
if action_type == 'merge_pr':
|
|
try:
|
|
log(f"Initiating merge of PR #{number}...", "ACTION")
|
|
|
|
result = subprocess.run([
|
|
'curl', '-s', '-X', 'POST',
|
|
f'{GITEA_URL}/api/v1/repos/Timmy_Foundation/{repo}/pulls/{number}/merge',
|
|
'-u', 'allegro:RUU0dwlHKvpfH!Uw2-yw4TmDVO%e',
|
|
'-H', 'Content-Type: application/json',
|
|
'-d', json.dumps({
|
|
'do': 'merge',
|
|
'delete_branch_after_merge': False
|
|
})
|
|
], capture_output=True, text=True, timeout=30)
|
|
|
|
# Check if merge succeeded
|
|
verify_result = subprocess.run([
|
|
'curl', '-s',
|
|
f'{GITEA_URL}/api/v1/repos/Timmy_Foundation/{repo}/pulls/{number}',
|
|
'-u', 'allegro:RUU0dwlHKvpfH!Uw2-yw4TmDVO%e'
|
|
], capture_output=True, text=True, timeout=10)
|
|
|
|
try:
|
|
pr_status = json.loads(verify_result.stdout)
|
|
if pr_status.get('merged', False):
|
|
merge_commit = pr_status.get('merge_commit_sha', 'unknown')[:16]
|
|
log(f"SUCCESSFULLY MERGED PR #{number} - Commit: {merge_commit}", "SUCCESS")
|
|
log(f" Title: {action['title']}", "DETAIL")
|
|
return {
|
|
'success': True,
|
|
'type': 'merge_pr',
|
|
'pr_number': number,
|
|
'merge_commit': merge_commit,
|
|
'title': action['title']
|
|
}
|
|
else:
|
|
log(f"Merge verification failed for PR #{number}", "ERROR")
|
|
return {'success': False, 'type': 'merge_pr', 'error': 'Verification failed'}
|
|
except Exception as e:
|
|
log(f"Merge error: {e}", "ERROR")
|
|
return {'success': False, 'type': 'merge_pr', 'error': str(e)}
|
|
|
|
except Exception as e:
|
|
log(f"CRITICAL ERROR merging PR #{number}: {e}", "ERROR")
|
|
return {'success': False, 'type': 'merge_pr', 'error': str(e)}
|
|
|
|
elif action_type == 'triage':
|
|
try:
|
|
log(f"Adding triage comment to issue #{number}...", "ACTION")
|
|
|
|
comment = f"""## 🏷️ Automated Triage Check
|
|
|
|
**Timestamp:** {datetime.now().isoformat()}
|
|
**Agent:** Allegro Heartbeat
|
|
|
|
This issue has been identified as needing triage:
|
|
|
|
### Checklist
|
|
- [ ] Clear acceptance criteria defined
|
|
- [ ] Priority label assigned (p0-critical / p1-important / p2-backlog)
|
|
- [ ] Size estimate added (quick-fix / day / week / epic)
|
|
- [ ] Owner assigned
|
|
- [ ] Related issues linked
|
|
|
|
### Context
|
|
- No comments yet - needs engagement
|
|
- No labels - needs categorization
|
|
- Part of automated backlog maintenance
|
|
|
|
---
|
|
*Automated triage from Allegro 15-minute heartbeat*"""
|
|
|
|
result = subprocess.run([
|
|
'curl', '-s', '-X', 'POST',
|
|
f'{GITEA_URL}/api/v1/repos/Timmy_Foundation/{repo}/issues/{number}/comments',
|
|
'-H', f'Authorization: token {TOKEN}',
|
|
'-H', 'Content-Type: application/json',
|
|
'-d', json.dumps({'body': comment})
|
|
], capture_output=True, text=True, timeout=15)
|
|
|
|
response = json.loads(result.stdout)
|
|
if 'id' in response:
|
|
log(f"SUCCESSFULLY TRIAGED issue #{number}", "SUCCESS")
|
|
log(f" Comment ID: {response['id']}", "DETAIL")
|
|
return {
|
|
'success': True,
|
|
'type': 'triage',
|
|
'issue_number': number,
|
|
'comment_id': response['id']
|
|
}
|
|
else:
|
|
log(f"Triage comment failed for issue #{number}", "ERROR")
|
|
return {'success': False, 'type': 'triage', 'error': 'Comment creation failed'}
|
|
|
|
except Exception as e:
|
|
log(f"ERROR triaging issue #{number}: {e}", "ERROR")
|
|
return {'success': False, 'type': 'triage', 'error': str(e)}
|
|
|
|
log(f"Unknown action type: {action_type}", "WARNING")
|
|
return {'success': False, 'type': action_type, 'error': 'Unknown action type'}
|
|
|
|
def main():
|
|
"""Main heartbeat cycle - COMPREHENSIVE"""
|
|
log("=" * 70, "SESSION")
|
|
log("HEARTBEAT WAKEUP INITIATED", "SESSION")
|
|
log(f"Timestamp: {datetime.now().isoformat()}", "SESSION")
|
|
log(f"Session ID: {datetime.now().strftime('%Y%m%d_%H%M%S')}", "SESSION")
|
|
log("=" * 70, "SESSION")
|
|
|
|
session_results = {
|
|
'timestamp': datetime.now().isoformat(),
|
|
'gitea_health': None,
|
|
'repo_scan': None,
|
|
'actions_found': 0,
|
|
'action_taken': None,
|
|
'action_result': None,
|
|
'errors': []
|
|
}
|
|
|
|
# 1. Health Check
|
|
log("PHASE 1: Infrastructure Health Check", "PHASE")
|
|
health = check_gitea_health()
|
|
session_results['gitea_health'] = health
|
|
|
|
if not health['healthy']:
|
|
log("CRITICAL: Gitea unhealthy - aborting session", "ERROR")
|
|
session_results['errors'].append('Gitea unhealthy')
|
|
return session_results
|
|
|
|
# 2. Repository Scan
|
|
log("PHASE 2: Repository Status Scan", "PHASE")
|
|
repo_stats = scan_repositories()
|
|
session_results['repo_scan'] = repo_stats
|
|
|
|
# 3. Find Actionable Items
|
|
log("PHASE 3: Actionable Item Discovery", "PHASE")
|
|
actions = get_actionable_items()
|
|
session_results['actions_found'] = len(actions)
|
|
|
|
if not actions:
|
|
log("No high-leverage actions identified this cycle", "SUMMARY")
|
|
log("System healthy, no intervention required", "SUMMARY")
|
|
else:
|
|
log(f"{len(actions)} actionable items discovered", "SUMMARY")
|
|
|
|
# 4. Execute Top Action
|
|
log("PHASE 4: Action Execution", "PHASE")
|
|
top_action = actions[0]
|
|
session_results['action_taken'] = top_action
|
|
|
|
result = perform_action(top_action)
|
|
session_results['action_result'] = result
|
|
|
|
if result['success']:
|
|
log(f"ACTION COMPLETED: {result['type']}", "SUCCESS")
|
|
else:
|
|
log(f"ACTION FAILED: {result.get('error', 'Unknown error')}", "ERROR")
|
|
session_results['errors'].append(result.get('error', 'Action failed'))
|
|
|
|
# 5. Session Summary
|
|
log("=" * 70, "SESSION")
|
|
log("HEARTBEAT SESSION COMPLETE", "SESSION")
|
|
log(f"Actions found: {session_results['actions_found']}", "SUMMARY")
|
|
log(f"Action taken: {session_results['action_taken']['type'] if session_results['action_taken'] else 'None'}", "SUMMARY")
|
|
log(f"Success: {session_results['action_result']['success'] if session_results['action_result'] else 'N/A'}", "SUMMARY")
|
|
log(f"Errors: {len(session_results['errors'])}", "SUMMARY")
|
|
log("=" * 70, "SESSION")
|
|
|
|
return session_results
|
|
|
|
if __name__ == "__main__":
|
|
main()
|