Files
timmy-config/allegro/heartbeat_daemon.py
2026-03-31 20:02:01 +00:00

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()