Add Binky weekly review approach

This commit is contained in:
tegwick 2026-08-08 21:26:29 +02:00
parent 140d1b0dc7
commit 8e98eada89
8 changed files with 700 additions and 12 deletions

View file

@ -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

View file

@ -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 |

View file

@ -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) |

View file

@ -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,
)

View 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,
},
)

View file

@ -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

View file

@ -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
View 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"