424 lines
14 KiB
Python
424 lines
14 KiB
Python
"""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,
|
|
},
|
|
)
|