Add Binky weekly review approach
This commit is contained in:
parent
140d1b0dc7
commit
8e98eada89
8 changed files with 700 additions and 12 deletions
|
|
@ -63,8 +63,9 @@ rein-aharness mail-scan --target-repo ~/binky-control
|
|||
export LLM_CONNECT_URL=http://llm-connect.activity-core.svc.cluster.local:8080
|
||||
rein-aharness mail-triage --target-repo ~/binky-control
|
||||
|
||||
# Daily brief via llm-connect (structured JSON → briefs/YYYY-MM-DD-daily-brief.md)
|
||||
# Daily and weekly briefs via llm-connect (structured JSON → deterministic markdown)
|
||||
rein-aharness brief-daily --target-repo ~/binky-control
|
||||
rein-aharness brief-weekly --target-repo ~/binky-control
|
||||
|
||||
# Railiance packaging smoke (commit + optional push + hub; no Claude required)
|
||||
rein-aharness smoke --work-dir ~/work/executor-sandbox
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
| workplan | HARNESS-WP-0002 | finished | — | workplans/HARNESS-WP-0002-rename-and-glas-harness-alignment.md |
|
||||
| workplan | REIN-A-0001 | finished | — | workplans/REIN-A-0001-statehub-bootstrap.md |
|
||||
| workplan | REIN-A-0002 | finished | — | workplans/REIN-A-0002-ops-run-claim-loop.md |
|
||||
| workplan | REIN-A-0003 | proposed | — | workplans/REIN-A-0003-binky-weekly-review-approach.md |
|
||||
| workplan | REIN-A-0003 | active | — | workplans/REIN-A-0003-binky-weekly-review-approach.md |
|
||||
| task | HARNESS-WP-0001-T01 | done | — | workplans/HARNESS-WP-0001-harness-foundation.md |
|
||||
| task | HARNESS-WP-0001-T02 | done | — | workplans/HARNESS-WP-0001-harness-foundation.md |
|
||||
| task | HARNESS-WP-0001-T03 | done | — | workplans/HARNESS-WP-0001-harness-foundation.md |
|
||||
|
|
@ -33,6 +33,6 @@
|
|||
| task | REIN-A-0002-T04 | done | — | workplans/REIN-A-0002-ops-run-claim-loop.md |
|
||||
| task | REIN-A-0002-T05 | done | — | workplans/REIN-A-0002-ops-run-claim-loop.md |
|
||||
| task | REIN-A-0002-T06 | done | — | workplans/REIN-A-0002-ops-run-claim-loop.md |
|
||||
| task | REIN-A-0003-T01 | todo | — | workplans/REIN-A-0003-binky-weekly-review-approach.md |
|
||||
| task | REIN-A-0003-T02 | wait | — | workplans/REIN-A-0003-binky-weekly-review-approach.md |
|
||||
| task | REIN-A-0003-T03 | wait | — | workplans/REIN-A-0003-binky-weekly-review-approach.md |
|
||||
| task | REIN-A-0003-T01 | done | — | workplans/REIN-A-0003-binky-weekly-review-approach.md |
|
||||
| task | REIN-A-0003-T02 | done | — | workplans/REIN-A-0003-binky-weekly-review-approach.md |
|
||||
| task | REIN-A-0003-T03 | progress | — | workplans/REIN-A-0003-binky-weekly-review-approach.md |
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ export ACTIVITY_CORE_WORKER_TOKEN=… # from actcore-runtime-secre
|
|||
| ----- | -------- |
|
||||
| labels `research-brief` / `freedom-intelligence` or fi-daily* | `fi-research-brief` |
|
||||
| labels `rhythm` / `binky-daily` or binky-daily* | `brief-daily` |
|
||||
| labels `binky` + `weekly-review` or binky-weekly-review* | `brief-weekly` |
|
||||
| labels `mail-intake` | `mail-scan` then `mail-triage` |
|
||||
| labels `agent-session` | agentic `run` (TaskSpec) |
|
||||
| none | fail ops_run (no silent drop) |
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ from rein_aharness.taskspec import TaskSpecError
|
|||
# Approach command names (stable; used in metrics + ops_run.result)
|
||||
APPROACH_FI_RESEARCH_BRIEF = "fi-research-brief"
|
||||
APPROACH_BRIEF_DAILY = "brief-daily"
|
||||
APPROACH_BRIEF_WEEKLY = "brief-weekly"
|
||||
APPROACH_MAIL_SCAN = "mail-scan"
|
||||
APPROACH_MAIL_TRIAGE = "mail-triage"
|
||||
APPROACH_MAIL_PIPELINE = "mail-scan+triage"
|
||||
|
|
@ -52,6 +53,14 @@ APPROACH_RULES: tuple[ApproachRule, ...] = (
|
|||
),
|
||||
hint_name=APPROACH_FI_RESEARCH_BRIEF,
|
||||
),
|
||||
ApproachRule(
|
||||
name=APPROACH_BRIEF_WEEKLY,
|
||||
labels_all=frozenset({"binky", "weekly-review"}),
|
||||
blob_contains=frozenset(
|
||||
{"binky-weekly-review", "binky_weekly_review", "weekly founder review"}
|
||||
),
|
||||
hint_name=APPROACH_BRIEF_WEEKLY,
|
||||
),
|
||||
ApproachRule(
|
||||
name=APPROACH_BRIEF_DAILY,
|
||||
labels_any=frozenset({"rhythm", "binky-daily", "daily-brief"}),
|
||||
|
|
@ -113,6 +122,7 @@ def select_approach(run: OpsRun) -> str:
|
|||
known = {
|
||||
APPROACH_FI_RESEARCH_BRIEF,
|
||||
APPROACH_BRIEF_DAILY,
|
||||
APPROACH_BRIEF_WEEKLY,
|
||||
APPROACH_MAIL_SCAN,
|
||||
APPROACH_MAIL_TRIAGE,
|
||||
APPROACH_MAIL_PIPELINE,
|
||||
|
|
@ -125,11 +135,12 @@ def select_approach(run: OpsRun) -> str:
|
|||
blob = _match_blob(run)
|
||||
|
||||
for rule in APPROACH_RULES:
|
||||
if rule.labels_all and not rule.labels_all.issubset(labels):
|
||||
continue
|
||||
if rule.labels_any and (rule.labels_any & labels):
|
||||
return rule.name
|
||||
if rule.blob_contains and any(s in blob for s in rule.blob_contains):
|
||||
matches_all = bool(rule.labels_all) and rule.labels_all.issubset(labels)
|
||||
matches_any = bool(rule.labels_any) and bool(rule.labels_any & labels)
|
||||
matches_blob = bool(rule.blob_contains) and any(
|
||||
substring in blob for substring in rule.blob_contains
|
||||
)
|
||||
if matches_all or matches_any or matches_blob:
|
||||
return rule.name
|
||||
|
||||
return APPROACH_UNMATCHED
|
||||
|
|
@ -182,6 +193,8 @@ def execute_approach(
|
|||
return _run_fi(target, report_to_hub=report_to_hub, commit=commit)
|
||||
if name == APPROACH_BRIEF_DAILY:
|
||||
return _run_brief_daily(target, report_to_hub=report_to_hub, commit=commit)
|
||||
if name == APPROACH_BRIEF_WEEKLY:
|
||||
return _run_brief_weekly(target, report_to_hub=report_to_hub, commit=commit)
|
||||
if name == APPROACH_MAIL_SCAN:
|
||||
return _run_mail_scan(target, report_to_hub=report_to_hub)
|
||||
if name == APPROACH_MAIL_TRIAGE:
|
||||
|
|
@ -261,6 +274,33 @@ def _run_brief_daily(
|
|||
)
|
||||
|
||||
|
||||
def _run_brief_weekly(
|
||||
target: Path, *, report_to_hub: bool, commit: bool
|
||||
) -> ApproachResult:
|
||||
from rein_aharness.brief_weekly import run_brief_weekly
|
||||
|
||||
r = run_brief_weekly(
|
||||
target_repo=target,
|
||||
report_to_hub=report_to_hub,
|
||||
commit=commit,
|
||||
)
|
||||
return ApproachResult(
|
||||
ok=r.ok,
|
||||
approach=APPROACH_BRIEF_WEEKLY,
|
||||
result={
|
||||
"date": r.date,
|
||||
"path": r.path,
|
||||
"wrote": r.wrote,
|
||||
"committed": r.committed,
|
||||
"skipped_existing": r.skipped_existing,
|
||||
"milestone_moved": r.milestone_moved,
|
||||
"risk005_state": r.risk005_state,
|
||||
"head_after": r.head_after,
|
||||
"target_repo": "binky-control",
|
||||
},
|
||||
reason=r.reason,
|
||||
reopen=not r.ok and not r.skipped_existing,
|
||||
)
|
||||
def _run_mail_scan(target: Path, *, report_to_hub: bool) -> ApproachResult:
|
||||
from rein_aharness.mailscan import run_mail_scan
|
||||
|
||||
|
|
@ -357,5 +397,3 @@ def _run_agent_session(
|
|||
reason=r.reason,
|
||||
reopen=not r.ok,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
424
rein_aharness/brief_weekly.py
Normal file
424
rein_aharness/brief_weekly.py
Normal file
|
|
@ -0,0 +1,424 @@
|
|||
"""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,
|
||||
},
|
||||
)
|
||||
|
|
@ -364,6 +364,28 @@ def main(argv: list[str] | None = None) -> int:
|
|||
help="Write brief file but do not git commit",
|
||||
)
|
||||
|
||||
weekly = sub.add_parser(
|
||||
"brief-weekly",
|
||||
help="Write weekly founder-review prep via llm-connect (no coding agent)",
|
||||
)
|
||||
weekly.add_argument("--target-repo", required=True)
|
||||
weekly.add_argument(
|
||||
"--date",
|
||||
default=None,
|
||||
help="Review date YYYY-MM-DD (default: today Europe/Berlin)",
|
||||
)
|
||||
weekly.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Overwrite if today's weekly review already exists",
|
||||
)
|
||||
weekly.add_argument("--no-hub", action="store_true", help="Skip hub reporting")
|
||||
weekly.add_argument(
|
||||
"--no-commit",
|
||||
action="store_true",
|
||||
help="Write review file but do not git commit",
|
||||
)
|
||||
|
||||
fi_brief = sub.add_parser(
|
||||
"fi-research-brief",
|
||||
help=(
|
||||
|
|
@ -599,6 +621,39 @@ def main(argv: list[str] | None = None) -> int:
|
|||
)
|
||||
return 0 if brief_result.ok else 1
|
||||
|
||||
if args.command == "brief-weekly":
|
||||
from datetime import date as date_cls
|
||||
|
||||
from rein_aharness.brief_weekly import run_brief_weekly
|
||||
|
||||
day = date_cls.fromisoformat(args.date) if args.date else None
|
||||
result = run_brief_weekly(
|
||||
target_repo=Path(args.target_repo).expanduser(),
|
||||
day=day,
|
||||
force=bool(args.force),
|
||||
report_to_hub=not args.no_hub,
|
||||
commit=not args.no_commit,
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"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,
|
||||
"head_after": result.head_after,
|
||||
"reason": result.reason,
|
||||
"model_meta": result.model_meta,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
return 0 if result.ok else 1
|
||||
|
||||
if args.command == "fi-research-brief":
|
||||
from datetime import date as date_cls
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
|||
from rein_aharness.approaches import (
|
||||
APPROACH_AGENT_SESSION,
|
||||
APPROACH_BRIEF_DAILY,
|
||||
APPROACH_BRIEF_WEEKLY,
|
||||
APPROACH_FI_RESEARCH_BRIEF,
|
||||
APPROACH_MAIL_PIPELINE,
|
||||
APPROACH_UNMATCHED,
|
||||
|
|
@ -56,6 +57,28 @@ def test_select_binky_rhythm() -> None:
|
|||
)
|
||||
|
||||
|
||||
def test_select_binky_weekly_review_requires_binky_and_weekly_label() -> None:
|
||||
assert (
|
||||
select_approach(
|
||||
_run(labels=["binky", "weekly-review", "automated"])
|
||||
)
|
||||
== APPROACH_BRIEF_WEEKLY
|
||||
)
|
||||
|
||||
|
||||
def test_select_binky_weekly_review_by_definition() -> None:
|
||||
assert (
|
||||
select_approach(
|
||||
_run(activity_definition_id="binky-weekly-review-prep")
|
||||
)
|
||||
== APPROACH_BRIEF_WEEKLY
|
||||
)
|
||||
|
||||
|
||||
def test_weekly_review_label_alone_is_unmatched() -> None:
|
||||
assert select_approach(_run(labels=["weekly-review"])) == APPROACH_UNMATCHED
|
||||
|
||||
|
||||
def test_select_mail_intake() -> None:
|
||||
assert (
|
||||
select_approach(_run(labels=["mail-intake", "automated"]))
|
||||
|
|
|
|||
146
tests/test_brief_weekly.py
Normal file
146
tests/test_brief_weekly.py
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
from rein_aharness import brief_weekly
|
||||
|
||||
|
||||
def _git(repo: Path, *args: str, commit_date: str | None = None) -> None:
|
||||
env = None
|
||||
if commit_date:
|
||||
env = dict(os.environ)
|
||||
env["GIT_AUTHOR_DATE"] = commit_date
|
||||
env["GIT_COMMITTER_DATE"] = commit_date
|
||||
subprocess.run(
|
||||
["git", *args], cwd=repo, check=True, capture_output=True, env=env
|
||||
)
|
||||
|
||||
|
||||
def _repo(tmp_path: Path) -> Path:
|
||||
repo = tmp_path / "binky-control"
|
||||
repo.mkdir()
|
||||
_git(repo, "init")
|
||||
_git(repo, "config", "user.email", "test@example.com")
|
||||
_git(repo, "config", "user.name", "test")
|
||||
for name, body in {
|
||||
"SuccessMilestones.md": "# Milestones\n\nS1: started\n",
|
||||
"DecisionQueue.md": "# Decisions\n\nDEC-1 prepared\n",
|
||||
"RiskRegister.md": "# Risks\n\nRISK-005 open\n",
|
||||
"WORK-RECORDS.md": "# Records\n",
|
||||
}.items():
|
||||
(repo / name).write_text(body, encoding="utf-8")
|
||||
(repo / "briefs").mkdir()
|
||||
(repo / "briefs/2026-08-07-daily-brief.md").write_text(
|
||||
"# Daily Brief\n\nProgress happened.\n", encoding="utf-8"
|
||||
)
|
||||
_git(repo, "add", ".")
|
||||
_git(
|
||||
repo,
|
||||
"commit",
|
||||
"-m",
|
||||
"initial evidence",
|
||||
commit_date="2026-07-01T12:00:00+00:00",
|
||||
)
|
||||
return repo
|
||||
|
||||
|
||||
def test_render_uses_deterministic_risk_state() -> None:
|
||||
signals = brief_weekly.WeeklySignals(
|
||||
milestone_moved=False,
|
||||
activity_present=True,
|
||||
prior_week_milestone_free=True,
|
||||
risk005_state="escalate",
|
||||
)
|
||||
text = brief_weekly.render_brief(
|
||||
date(2026, 8, 7),
|
||||
{
|
||||
"milestone_summary": "Work occurred but no status changed.",
|
||||
"milestone_evidence": ["S1-related work landed"],
|
||||
"founder_actions": ["Review DEC-1"],
|
||||
"risk005_state": "clear",
|
||||
},
|
||||
signals,
|
||||
)
|
||||
assert "**No.**" in text
|
||||
assert "**State: escalate.**" in text
|
||||
assert "Review DEC-1" in text
|
||||
assert "State: clear" not in text
|
||||
|
||||
|
||||
def test_derive_signals_escalates_after_prior_free_week(tmp_path: Path) -> None:
|
||||
repo = _repo(tmp_path)
|
||||
(repo / "briefs/2026-07-31-weekly-founder-review.md").write_text(
|
||||
"# Weekly Founder Review\n\nNo milestone status changed this week.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
_git(repo, "add", ".")
|
||||
_git(repo, "commit", "-m", "prior weekly review")
|
||||
signals = brief_weekly.derive_signals(repo, date.today())
|
||||
assert not signals.milestone_moved
|
||||
assert signals.activity_present
|
||||
assert signals.prior_week_milestone_free
|
||||
assert signals.risk005_state == "escalate"
|
||||
|
||||
|
||||
def test_run_weekly_mock_writes_commits_and_reports(tmp_path: Path, monkeypatch) -> None:
|
||||
repo = _repo(tmp_path)
|
||||
events: list[dict] = []
|
||||
monkeypatch.setattr(
|
||||
brief_weekly.hub,
|
||||
"post_progress_event",
|
||||
lambda **kwargs: events.append(kwargs) or True,
|
||||
)
|
||||
|
||||
def fake(prompt: str) -> str:
|
||||
assert "SuccessMilestones.md" in prompt
|
||||
assert "Deterministic signals" in prompt
|
||||
return json.dumps(
|
||||
{
|
||||
"milestone_summary": "No milestone status changed.",
|
||||
"milestone_evidence": ["Daily work was recorded."],
|
||||
"founder_actions": ["Review DEC-1"],
|
||||
}
|
||||
)
|
||||
|
||||
result = brief_weekly.run_brief_weekly(
|
||||
repo,
|
||||
day=date(2026, 8, 8),
|
||||
force=True,
|
||||
complete_fn=fake,
|
||||
)
|
||||
assert result.ok
|
||||
assert result.wrote
|
||||
assert result.committed
|
||||
assert result.risk005_state == "quiet"
|
||||
assert (repo / "briefs/2026-08-08-weekly-founder-review.md").is_file()
|
||||
assert events[-1]["event_type"] == "binky_weekly_review"
|
||||
assert events[-1]["detail"]["risk005_state"] == result.risk005_state
|
||||
|
||||
|
||||
def test_skip_existing_does_not_call_model(tmp_path: Path, monkeypatch) -> None:
|
||||
repo = _repo(tmp_path)
|
||||
monkeypatch.setattr(brief_weekly.hub, "post_progress_event", lambda **kw: True)
|
||||
day = date(2026, 8, 8)
|
||||
path = brief_weekly.brief_path_for(repo, day)
|
||||
path.write_text("# Weekly Founder Review\n", encoding="utf-8")
|
||||
_git(repo, "add", ".")
|
||||
_git(repo, "commit", "-m", "weekly review")
|
||||
|
||||
result = brief_weekly.run_brief_weekly(
|
||||
repo,
|
||||
day=day,
|
||||
complete_fn=lambda prompt: (_ for _ in ()).throw(AssertionError(prompt)),
|
||||
)
|
||||
assert result.ok
|
||||
assert result.skipped_existing
|
||||
|
||||
|
||||
def test_parse_response_accepts_json_fence() -> None:
|
||||
data = brief_weekly.parse_response(
|
||||
'```json\n{"milestone_summary":"none","milestone_evidence":[],"founder_actions":[]}\n```'
|
||||
)
|
||||
assert data["milestone_summary"] == "none"
|
||||
Loading…
Add table
Add a link
Reference in a new issue