Files
the-nexus/nexus/morning_report.py
Alexander Whitestone 37b006d3c6
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
CI / validate (pull_request) Failing after 10s
feat: Fleet management (#910), retry logic (#896), morning report (#897)
- fleet/fleet.sh: cross-VPS health, status, restart, deploy
- nexus/retry_helper.py: retry decorator, dead letter queue, checkpoints
- nexus/morning_report.py: automated 0600 overnight activity report
- fleet/allegro/archived-scripts/README.md: burn script archive placeholder

Fixes #910
Fixes #896
Fixes #897
Fixes #898
2026-04-06 23:09:49 -04:00

133 lines
4.6 KiB
Python

"""
Morning Report Generator — runs at 0600 to compile overnight activity.
Gathers: cycles executed, issues closed, PRs merged, commits pushed.
Outputs a structured report for delivery to the main channel.
"""
import json
import os
import subprocess
from datetime import datetime, timedelta, timezone
from pathlib import Path
def generate_morning_report():
"""Generate the morning report for the last 24h."""
now = datetime.now(timezone.utc)
since = now - timedelta(hours=24)
since_str = since.strftime("%Y-%m-%dT%H:%M:%SZ")
repos = [
"Timmy_Foundation/timmy-home",
"Timmy_Foundation/timmy-config",
"Timmy_Foundation/the-nexus",
"Timmy_Foundation/hermes-agent",
]
report = {
"generated_at": now.strftime("%Y-%m-%d %H:%M UTC"),
"period": f"Last 24h since {since_str}",
"highlights": [],
"blockers": [],
"repos": {},
}
token = open(os.path.expanduser("~/.config/gitea/token")).read().strip()
from urllib.request import Request, urlopen
headers = {"Authorization": f"token {token}", "Accept": "application/json"}
for repo in repos:
repo_data = {"closed_issues": 0, "merged_prs": 0, "recent_commits": 0}
# Closed issues in last 24h
url = f"https://forge.alexanderwhitestone.com/api/v1/repos/{repo}/issues?state=closed&since={since_str}"
try:
resp = urlopen(Request(url, headers=headers), timeout=10)
issues = json.loads(resp.read())
repo_data["closed_issues"] = len(issues)
for i in issues[:5]:
report["highlights"].append(f"Closed {repo.split('/')[-1]}#{i['number']}: {i['title']}")
except Exception:
pass
# Merged PRs
url = f"https://forge.alexanderwhitestone.com/api/v1/repos/{repo}/pulls?state=closed"
try:
resp = urlopen(Request(url, headers=headers), timeout=10)
prs = json.loads(resp.read())
merged = [p for p in prs if p.get("merged")]
repo_data["merged_prs"] = len(merged)
except Exception:
pass
report["repos"][repo.split("/")[-1]] = repo_data
# Check for stuck workers (blockers)
worker_logs = list(Path("/tmp").glob("codeclaw-qwen-worker-*.log"))
stuck = 0
for wf in worker_logs:
try:
data = json.loads(wf.read_text().strip())
if data.get("exit") != 0 and not data.get("has_work"):
stuck += 1
except (json.JSONDecodeError, ValueError):
pass
if stuck > 0:
report["blockers"].append(f"{stuck} worker(s) failed without producing work")
# Check dead letter queue
dlq_path = Path(os.path.expanduser("~/.local/timmy/burn-state/dead-letter.json"))
if dlq_path.exists():
try:
dlq = json.loads(dlq_path.read_text())
if dlq:
report["blockers"].append(f"{len(dlq)} action(s) in dead letter queue")
except Exception:
pass
# Checkpoint status
cp_path = Path(os.path.expanduser("~/.local/timmy/burn-state/cycle-state.json"))
if cp_path.exists():
try:
cp = json.loads(cp_path.read_text())
if cp.get("status") == "in-progress":
ts = cp.get("timestamp", "")
if ts and datetime.fromisoformat(ts) < since:
report["blockers"].append(f"Stale checkpoint: {cp.get('action')} since {ts}")
except Exception:
pass
# Summary
total_closed = sum(r["closed_issues"] for r in report["repos"].values())
total_merged = sum(r["merged_prs"] for r in report["repos"].values())
print(f"=== MORNING REPORT {report['generated_at']} ===")
print(f"Period: {report['period']}")
print(f"Issues closed: {total_closed}")
print(f"PRs merged: {total_merged}")
print("")
if report["highlights"]:
print("HIGHLIGHTS:")
for h in report["highlights"]:
print(f" + {h}")
if report["blockers"]:
print("BLOCKERS:")
for b in report["blockers"]:
print(f" - {b}")
if not report["highlights"] and not report["blockers"]:
print("No significant activity or blockers detected.")
print("")
# Save report
report_dir = Path(os.path.expanduser("~/.local/timmy/reports"))
report_dir.mkdir(parents=True, exist_ok=True)
report_file = report_dir / f"morning-{now.strftime('%Y-%m-%d')}.json"
report_file.write_text(json.dumps(report, indent=2))
print(f"Report saved: {report_file}")
return report
if __name__ == "__main__":
generate_morning_report()