"""Publish the weekly retro (AGENTIC-WP-0010 T02). The retro is published to the State Hub as a **read model** — a progress event of ``event_type=coding_retro`` whose ``detail`` carries the structured report. This is exactly how ``daily-triage-report`` surfaces, and it is what activity-core's ``coding_retro`` resolver (ACTIVITY-WP-0008) reads. A local JSON + markdown report is always written; the hub publish is best-effort and **degrades gracefully** when the hub is unreachable. """ from __future__ import annotations import json import os import urllib.request from typing import Callable, Optional DEFAULT_HUB = "http://127.0.0.1:8000" def render_markdown(report: dict) -> str: w = report.get("window", {}) lines = [ f"# Weekly Coding Retro ({w.get('since', '')[:10]} → {w.get('until', '')[:10]})", f"_{report.get('n_sessions', 0)} real sessions · generated {report.get('generated_at', '')}_", "", "## Top improvement suggestions (cross-flavor first, ≤3 per repo)", ] if not report.get("suggestions"): lines.append("- (no recurring problems above threshold this week)") for s in report.get("suggestions", []): flag = " [CROSS-FLAVOR]" if s.get("cross_flavor") else "" lines.append(f"- **{s['repo']}** ({s['priority']}, score={s['score']}){flag}: " f"{s['title']} — {s['recommendation']}") m = report.get("measure", {}) lines += ["", "## Fleet snapshot", f"- infra-overhead median: {m.get('infra_overhead_share_median')}", f"- error rate: {m.get('error_rate')} · schema-thrash: {m.get('schema_thrash_sessions')}", f"- success rate: {m.get('success_rate')} · tokens p50: {m.get('tokens_p50')}"] return "\n".join(lines) def write_local(report: dict, json_path: str, md_path: Optional[str] = None) -> None: os.makedirs(os.path.dirname(json_path) or ".", exist_ok=True) with open(json_path, "w", encoding="utf-8") as fh: json.dump(report, fh, indent=2, sort_keys=True) fh.write("\n") if md_path: with open(md_path, "w", encoding="utf-8") as fh: fh.write(render_markdown(report)) fh.write("\n") def _http_post(url: str, payload: dict) -> None: req = urllib.request.Request(url, data=json.dumps(payload).encode(), headers={"Content-Type": "application/json"}, method="POST") with urllib.request.urlopen(req, timeout=10) as r: r.read() def publish_to_hub(report: dict, *, base_url: str = DEFAULT_HUB, poster: Optional[Callable[[str, dict], None]] = None) -> bool: """POST the retro as an event_type=coding_retro progress event. Best-effort.""" poster = poster or _http_post n = report.get("n_sessions", 0) k = len(report.get("suggestions", [])) payload = { "event_type": "coding_retro", "author": "helix-forge", "summary": f"Weekly coding retro: {k} ranked suggestions across " f"{report.get('window', {}).get('days', 7)} days ({n} sessions).", "detail": report, } try: poster(f"{base_url.rstrip('/')}/progress/", payload) return True except Exception: return False