"""Weekly founder-review prep via llm-connect and deterministic markdown. The model summarizes bounded repository evidence. Milestone movement and the RISK-005 watch/escalation state are derived in code so they cannot be invented by model output. """ from __future__ import annotations import json import os import re import subprocess from dataclasses import dataclass, field from datetime import date, datetime, timedelta from pathlib import Path from typing import Any, Callable from zoneinfo import ZoneInfo from rein_aharness import hub from rein_aharness.llm_connect_client import ( LLMConnectClient, LLMConnectError, get_llm_connect_client, ) _CONTEXT_FILES = ( "SuccessMilestones.md", "DecisionQueue.md", "RiskRegister.md", "WORK-RECORDS.md", ) _MAX_FILE_CHARS = 7000 _MAX_GIT_LOG = 30 class BriefWeeklyError(RuntimeError): pass @dataclass(frozen=True) class WeeklySignals: milestone_moved: bool activity_present: bool prior_week_milestone_free: bool risk005_state: str milestone_commits: tuple[str, ...] = () @dataclass class BriefWeeklyResult: ok: bool date: str path: str | None = None wrote: bool = False committed: bool = False head_after: str = "" reason: str = "" model_meta: dict[str, Any] = field(default_factory=dict) skipped_existing: bool = False milestone_moved: bool = False risk005_state: str = "" def _berlin_today() -> date: try: return datetime.now(ZoneInfo("Europe/Berlin")).date() except Exception: return date.today() def brief_path_for(repo: Path, day: date) -> Path: return repo / "briefs" / f"{day.isoformat()}-weekly-founder-review.md" def _git(repo: Path, *args: str) -> str: result = subprocess.run( ["git", "-C", str(repo), *args], capture_output=True, text=True, timeout=60, ) if result.returncode != 0: raise BriefWeeklyError( f"git {' '.join(args)} failed: {result.stderr.strip()[:200]}" ) return result.stdout.strip() def _truncate(text: str, n: int) -> str: if len(text) <= n: return text return text[: n - 20] + "\n…(truncated)…\n" def _previous_weekly_review(repo: Path, day: date) -> Path | None: candidates = sorted((repo / "briefs").glob("*-weekly-founder-review.md")) current = brief_path_for(repo, day) prior = [path for path in candidates if path != current and path.name[:10] < day.isoformat()] return prior[-1] if prior else None def derive_signals(repo: Path, day: date) -> WeeklySignals: since = (day - timedelta(days=6)).isoformat() milestone_log = _git( repo, "log", f"--since={since} 00:00:00", "--format=%h %ad %s", "--date=short", "--", "SuccessMilestones.md", ) milestone_commits = tuple(line for line in milestone_log.splitlines() if line.strip()) activity_log = _git( repo, "log", f"--since={since} 00:00:00", "--format=%h %ad %s", "--date=short", f"-{_MAX_GIT_LOG}", ) prior = _previous_weekly_review(repo, day) prior_text = ( prior.read_text(encoding="utf-8", errors="replace").lower() if prior else "" ) prior_free = ( "no milestone status changed" in prior_text or "no milestone moved" in prior_text ) moved = bool(milestone_commits) activity = bool(activity_log.strip()) if moved: risk_state = "clear" elif activity and prior_free: risk_state = "escalate" elif activity: risk_state = "watch" else: risk_state = "quiet" return WeeklySignals( milestone_moved=moved, activity_present=activity, prior_week_milestone_free=prior_free, risk005_state=risk_state, milestone_commits=milestone_commits, ) def collect_context(repo: Path, day: date, signals: WeeklySignals) -> str: since = day - timedelta(days=6) chunks = [ f"Review date (Europe/Berlin): {day.isoformat()}", f"Review window: {since.isoformat()} through {day.isoformat()}", "Deterministic signals: " + json.dumps( { "milestone_moved": signals.milestone_moved, "activity_present": signals.activity_present, "prior_week_milestone_free": signals.prior_week_milestone_free, "risk005_state": signals.risk005_state, "milestone_commits": list(signals.milestone_commits), }, sort_keys=True, ), ] for rel in _CONTEXT_FILES: path = repo / rel body = ( _truncate(path.read_text(encoding="utf-8", errors="replace"), _MAX_FILE_CHARS) if path.is_file() else "(missing)" ) chunks.append(f"## {rel}\n{body}") briefs = sorted((repo / "briefs").glob("*-daily-brief.md")) weekly = [ path for path in briefs if since.isoformat() <= path.name[:10] <= day.isoformat() ] for path in weekly: chunks.append( f"## Daily brief: {path.name}\n" + _truncate(path.read_text(encoding="utf-8", errors="replace"), 3500) ) prior = _previous_weekly_review(repo, day) if prior: chunks.append( f"## Previous weekly review: {prior.name}\n" + _truncate(prior.read_text(encoding="utf-8", errors="replace"), 4500) ) else: chunks.append("## Previous weekly review\n(none)") chunks.append( "## Recent git log\n" + _git(repo, "log", f"-{_MAX_GIT_LOG}", "--oneline", f"--since={since.isoformat()}") ) return "\n\n".join(chunks) def build_prompt(context: str) -> str: return f"""Prepare the Binky weekly founder-review note from the supplied evidence. Green/Blue lane only. Output ONLY valid JSON (no markdown fences). Schema: {{ "milestone_summary": "brief factual answer to whether a milestone moved", "milestone_evidence": ["short evidence bullets"], "founder_actions": ["prepared decisions/actions only, with ids when known"] }} Rules: - Do not decide milestone_moved or RISK-005 state; code supplies those facts. - Do not invent ids, status changes, amounts, commits, or events. - Distinguish milestone-relevant activity from an actual milestone status change. - Founder actions must be prepared packages, never raw questions. Maximum 5. - Keep every field concise enough for a ~15-minute review. ## Context {context} """ def parse_response(text: str) -> dict[str, Any]: cleaned = text.strip() if cleaned.startswith("```"): cleaned = re.sub(r"^```(?:json)?\s*", "", cleaned) cleaned = re.sub(r"\s*```$", "", cleaned) try: data = json.loads(cleaned) except json.JSONDecodeError as exc: match = re.search(r"\{.*\}", cleaned, re.S) if not match: raise BriefWeeklyError(f"LLM response is not JSON: {exc}") from exc try: data = json.loads(match.group(0)) except json.JSONDecodeError as nested: raise BriefWeeklyError(f"LLM response is not JSON: {nested}") from nested if not isinstance(data, dict): raise BriefWeeklyError("LLM JSON root must be an object") return data def _bullets(value: Any, *, empty: str, limit: int = 8) -> str: values = value if isinstance(value, list) else ([value] if value else []) lines = [str(item).lstrip("- ").strip() for item in values if str(item).strip()] if not lines: return f"- {empty}" return "\n".join(f"- {line[:277] + '...' if len(line) > 280 else line}" for line in lines[:limit]) def render_brief(day: date, data: dict[str, Any], signals: WeeklySignals) -> str: moved = "Yes" if signals.milestone_moved else "No" summary = str(data.get("milestone_summary") or "No evidence summary supplied.").strip() evidence = _bullets(data.get("milestone_evidence"), empty="No milestone evidence recorded.") actions = _bullets(data.get("founder_actions"), empty="No founder action needed this week.", limit=5) risk_text = { "clear": "A milestone moved in the review window; the two-week RISK-005 trigger is clear.", "watch": "Activity occurred without milestone movement; this is week 1 of the two-week RISK-005 watch.", "escalate": "RISK-005 escalates: activity occurred without milestone movement for two consecutive review cycles.", "quiet": "No activity was detected; the activity-without-movement trigger does not advance.", }[signals.risk005_state] return ( f"# Weekly Founder Review — {day.isoformat()} (prep note)\n\n" f"> Prepared per `OperatingRhythm.md` § Weekly review (~15 min, " "agent-prepared, founder-consumed).\n\n" "## Read these (in order)\n\n" "1. The newest daily brief in `briefs/`\n" "2. `SuccessMilestones.md`\n\n" "## The one question: did anything move a milestone?\n\n" f"**{moved}.** {summary}\n\n" f"{evidence}\n\n" "## RISK-005 watch\n\n" f"**State: {signals.risk005_state}.** {risk_text}\n\n" "## Pending founder action\n\n" f"{actions}\n" ) def run_brief_weekly( target_repo: Path, *, day: date | None = None, force: bool = False, report_to_hub: bool = True, commit: bool = True, client: LLMConnectClient | None = None, complete_fn: Callable[[str], str] | None = None, ) -> BriefWeeklyResult: repo = target_repo.expanduser().resolve() day = day or _berlin_today() path = brief_path_for(repo, day) try: signals = derive_signals(repo, day) except (BriefWeeklyError, OSError) as exc: result = BriefWeeklyResult(ok=False, date=day.isoformat(), reason=str(exc)[:300]) _hub(result, report_to_hub, repo) return result if path.is_file() and not force: result = BriefWeeklyResult( ok=True, date=day.isoformat(), path=str(path.relative_to(repo)), skipped_existing=True, reason="weekly review already exists for today", milestone_moved=signals.milestone_moved, risk005_state=signals.risk005_state, ) try: result.head_after = _git(repo, "rev-parse", "HEAD") except BriefWeeklyError: pass _hub(result, report_to_hub, repo) return result meta: dict[str, Any] = {} try: prompt = build_prompt(collect_context(repo, day, signals)) if complete_fn is not None: content = complete_fn(prompt) else: llm = client or get_llm_connect_client() model = os.environ.get("BRIEF_WEEKLY_MODEL", "").strip() or os.environ.get( "BRIEF_DAILY_MODEL", "" ).strip() content = llm.complete( prompt, model=model, config={ "temperature": float(os.environ.get("BRIEF_WEEKLY_TEMPERATURE", "0.2")), "max_tokens": int(os.environ.get("BRIEF_WEEKLY_MAX_TOKENS", "1400")), }, ) meta = dict(llm.last_response_metadata or {}) markdown = render_brief(day, parse_response(content), signals) except (LLMConnectError, BriefWeeklyError, OSError) as exc: result = BriefWeeklyResult( ok=False, date=day.isoformat(), reason=str(exc)[:300], model_meta=meta, milestone_moved=signals.milestone_moved, risk005_state=signals.risk005_state, ) _hub(result, report_to_hub, repo) return result path.parent.mkdir(parents=True, exist_ok=True) path.write_text(markdown, encoding="utf-8") rel = str(path.relative_to(repo)) committed = False head_after = "" if commit: try: _git(repo, "add", rel) if _git(repo, "status", "--porcelain", rel): _git(repo, "commit", "-m", f"Weekly founder review {day.isoformat()}: automated llm-connect rhythm") committed = True head_after = _git(repo, "rev-parse", "HEAD") except BriefWeeklyError as exc: result = BriefWeeklyResult( ok=False, date=day.isoformat(), path=rel, wrote=True, reason=f"write ok but commit failed: {exc}", model_meta=meta, milestone_moved=signals.milestone_moved, risk005_state=signals.risk005_state, ) _hub(result, report_to_hub, repo) return result else: try: head_after = _git(repo, "rev-parse", "HEAD") except BriefWeeklyError: pass result = BriefWeeklyResult( ok=True, date=day.isoformat(), path=rel, wrote=True, committed=committed, head_after=head_after, model_meta=meta, milestone_moved=signals.milestone_moved, risk005_state=signals.risk005_state, ) _hub(result, report_to_hub, repo) return result def _hub(result: BriefWeeklyResult, report_to_hub: bool, repo: Path) -> None: if not report_to_hub: return event_type = "binky_weekly_review" if result.ok else "executor_run" summary = ( f"binky weekly review {result.date}" + (" (already present)" if result.skipped_existing else f" wrote={result.wrote} committed={result.committed}") if result.ok else f"binky weekly review failed: {result.reason}" ) hub.post_progress_event( summary=summary, event_type=event_type, detail={ "repo": repo.name, "ok": result.ok, "date": result.date, "path": result.path, "wrote": result.wrote, "committed": result.committed, "skipped_existing": result.skipped_existing, "milestone_moved": result.milestone_moved, "risk005_state": result.risk005_state, "reason": result.reason, "model_meta": result.model_meta, }, )