"""Daily brief via llm-connect (structured JSON + deterministic markdown). Server path for Binky daily rhythm — no Claude Code / host coding agent. Format: OperatingRhythm.md (Decide now / Progress / Risks / Best next hour). """ from __future__ import annotations import json import os import re import subprocess from dataclasses import dataclass, field from datetime import date, datetime, timezone from pathlib import Path from typing import Any, Callable from zoneinfo import ZoneInfo from agent_harness import hub from agent_harness.llm_connect_client import ( LLMConnectClient, LLMConnectError, get_llm_connect_client, ) _CONTEXT_FILES = ( "DecisionQueue.md", "OfficeHourQueue.md", "AutopilotWorkQueue.md", "RiskRegister.md", "SuccessMilestones.md", "WORK-RECORDS.md", ) _MAX_FILE_CHARS = 6000 _MAX_GIT_LOG = 15 class BriefDailyError(RuntimeError): pass @dataclass class BriefDailyResult: 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 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()}-daily-brief.md" def collect_context(repo: Path, day: date) -> str: chunks: list[str] = [f"Brief date (Europe/Berlin): {day.isoformat()}\n"] for rel in _CONTEXT_FILES: path = repo / rel if not path.is_file(): chunks.append(f"## {rel}\n(missing)\n") continue text = path.read_text(encoding="utf-8", errors="replace") chunks.append(f"## {rel}\n{_truncate(text, _MAX_FILE_CHARS)}\n") # Recent briefs (titles only + last brief body capped) briefs_dir = repo / "briefs" if briefs_dir.is_dir(): briefs = sorted(briefs_dir.glob("*-daily-brief.md")) names = [p.name for p in briefs[-5:]] chunks.append(f"## Recent daily briefs\n{names}\n") if briefs: last = briefs[-1] if last.name != f"{day.isoformat()}-daily-brief.md": chunks.append( f"## Previous brief ({last.name})\n" f"{_truncate(last.read_text(encoding='utf-8', errors='replace'), 2500)}\n" ) try: log = _git(repo, "log", f"-{_MAX_GIT_LOG}", "--oneline") chunks.append(f"## Recent git log\n{log}\n") except BriefDailyError: chunks.append("## Recent git log\n(unavailable)\n") return "\n".join(chunks) def build_prompt(context: str, day: date) -> str: return f"""You write the Binky Hedgehog / Operational Knowledge company **daily brief**. Green/Blue lane only. Output ONLY valid JSON (no markdown fences). Schema: {{ "decide_now": ["0-3 bullets; prepared decisions only, with ids if known"], "progress": ["what moved; milestone-relevant only"], "risks": ["RiskRegister changes only; empty list if none"], "best_next_hour": "single highest-leverage founder action, or 'no founder action needed today'" }} Rules: - Decide now: only prepared decision packages, never raw questions. Max 3. - Progress: short bullets of real movement (workplans, pulls, cutovers). - Risks: only changes; do not restate the whole register. - Best next hour: ONE item (or explicit no action needed). - Be factual from context; do not invent ids, amounts, or events. - Prefer "no founder action needed today" when nothing is blocking. - Keep total brief under one screen (~12 short bullets total). ## Context {context} """ def parse_brief_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: m = re.search(r"\{.*\}", cleaned, re.S) if not m: raise BriefDailyError(f"LLM response is not JSON: {exc}") from exc try: data = json.loads(m.group(0)) except json.JSONDecodeError as exc2: raise BriefDailyError(f"LLM response is not JSON: {exc2}") from exc2 if not isinstance(data, dict): raise BriefDailyError("LLM JSON root must be an object") return data def render_brief(day: date, data: dict[str, Any]) -> str: decide = _bullet_list(data.get("decide_now"), empty="(none)") progress = _bullet_list(data.get("progress"), empty="(none recorded)") risks = _bullet_list(data.get("risks"), empty="No RiskRegister changes today.") best = data.get("best_next_hour") or "no founder action needed today" best = str(best).strip().replace("\n", " ") if not best: best = "no founder action needed today" return ( f"# Daily Brief — {day.isoformat()}\n" f"\n" f"## Decide now\n" f"{decide}\n" f"\n" f"## Progress\n" f"{progress}\n" f"\n" f"## Risks\n" f"{risks}\n" f"\n" f"## Best next hour\n" f"{best}\n" ) def _bullet_list(value: Any, *, empty: str) -> str: if not value: return f"- {empty}" if isinstance(value, str): lines = [value] elif isinstance(value, list): lines = [str(x).strip() for x in value if str(x).strip()] else: lines = [str(value)] if not lines: return f"- {empty}" out = [] for line in lines[:8]: line = line.lstrip("- ").strip() if len(line) > 280: line = line[:277] + "..." out.append(f"- {line}") return "\n".join(out) def _truncate(text: str, n: int) -> str: if len(text) <= n: return text return text[: n - 20] + "\n…(truncated)…\n" 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 BriefDailyError( f"git {' '.join(args)} failed: {result.stderr.strip()[:200]}" ) return result.stdout.strip() def run_brief_daily( 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, ) -> BriefDailyResult: repo = target_repo.expanduser().resolve() day = day or _berlin_today() path = brief_path_for(repo, day) if path.is_file() and not force: result = BriefDailyResult( ok=True, date=day.isoformat(), path=str(path.relative_to(repo)), skipped_existing=True, reason="brief already exists for today", ) try: result.head_after = _git(repo, "rev-parse", "HEAD") except BriefDailyError: pass _hub(result, report_to_hub, repo) return result context = collect_context(repo, day) prompt = build_prompt(context, day) meta: dict[str, Any] = {} try: if complete_fn is not None: content = complete_fn(prompt) else: llm = client or get_llm_connect_client() model = os.environ.get("BRIEF_DAILY_MODEL", "").strip() or os.environ.get( "MAIL_TRIAGE_MODEL", "" ).strip() content = llm.complete( prompt, model=model, config={ "temperature": float(os.environ.get("BRIEF_DAILY_TEMPERATURE", "0.2")), "max_tokens": int(os.environ.get("BRIEF_DAILY_MAX_TOKENS", "1200")), }, ) meta = dict(llm.last_response_metadata or {}) data = parse_brief_response(content) markdown = render_brief(day, data) except (LLMConnectError, BriefDailyError, OSError) as exc: result = BriefDailyResult( ok=False, date=day.isoformat(), reason=str(exc)[:300], model_meta=meta, ) _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) status = _git(repo, "status", "--porcelain", rel) if status.strip(): _git( repo, "commit", "-m", f"Daily brief {day.isoformat()}: automated llm-connect rhythm", ) committed = True head_after = _git(repo, "rev-parse", "HEAD") except BriefDailyError as exc: result = BriefDailyResult( ok=False, date=day.isoformat(), path=rel, wrote=True, reason=f"write ok but commit failed: {exc}", model_meta=meta, ) _hub(result, report_to_hub, repo) return result else: try: head_after = _git(repo, "rev-parse", "HEAD") except BriefDailyError: pass result = BriefDailyResult( ok=True, date=day.isoformat(), path=rel, wrote=True, committed=committed, head_after=head_after, model_meta=meta, ) _hub(result, report_to_hub, repo) return result def _hub(result: BriefDailyResult, report_to_hub: bool, repo: Path) -> None: if not report_to_hub: return # Idempotence: only successful brief writes/skips use binky_daily_brief if result.ok: event_type = "binky_daily_brief" summary = ( f"binky daily brief {result.date}" + ( " (already present)" if result.skipped_existing else f" wrote={result.wrote} committed={result.committed}" ) ) else: event_type = "executor_run" summary = f"binky daily brief 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, "reason": result.reason, "model_meta": result.model_meta, }, )