Files
the-nexus/nexus/morning_report.py
2026-04-07 14:44:05 +00:00

178 lines
6.7 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.
Includes a HEARTBEAT PANEL that checks all cron job heartbeats via
bezalel_heartbeat_check.py (poka-yoke #1096). Any stale jobs surface
as blockers in the report.
"""
import importlib.util
import json
import os
import subprocess
import sys
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("")
# ── Heartbeat panel (poka-yoke #1096) ────────────────────────────────────
# Import bezalel_heartbeat_check via importlib so we don't need __init__.py
# or a sys.path hack. If the module is missing or the dir doesn't exist,
# we print a "not provisioned" notice and continue — never crash the report.
_hb_result = None
try:
_project_root = Path(__file__).parent.parent
_hb_spec = importlib.util.spec_from_file_location(
"bezalel_heartbeat_check",
_project_root / "bin" / "bezalel_heartbeat_check.py",
)
if _hb_spec is not None:
_hb_mod = importlib.util.module_from_spec(_hb_spec)
sys.modules.setdefault("bezalel_heartbeat_check", _hb_mod)
_hb_spec.loader.exec_module(_hb_mod) # type: ignore[union-attr]
_hb_result = _hb_mod.check_cron_heartbeats()
except Exception:
_hb_result = None
print("HEARTBEAT PANEL:")
if _hb_result is None or not _hb_result.get("jobs"):
print(" HEARTBEAT PANEL: no data (bezalel not provisioned)")
report["heartbeat_panel"] = {"status": "not_provisioned"}
else:
for _job in _hb_result["jobs"]:
_prefix = "+" if _job["healthy"] else "-"
print(f" {_prefix} {_job['job']}: {_job['message']}")
if not _job["healthy"]:
report["blockers"].append(
f"Stale heartbeat: {_job['job']}{_job['message']}"
)
print("")
report["heartbeat_panel"] = {
"checked_at": _hb_result.get("checked_at"),
"healthy_count": _hb_result.get("healthy_count", 0),
"stale_count": _hb_result.get("stale_count", 0),
"jobs": _hb_result.get("jobs", []),
}
# 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()