feat: brief-daily via llm-connect (structured daily brief)
Generate OperatingRhythm-format briefs with OpenRouter-backed llm-connect JSON + deterministic markdown write/commit. Server path for Binky daily rhythm without host Claude (BINKY-WP-0006-T05).
This commit is contained in:
parent
7520a53831
commit
f770825975
4 changed files with 536 additions and 0 deletions
|
|
@ -37,6 +37,9 @@ agent-harness mail-scan --target-repo ~/binky-control
|
||||||
export LLM_CONNECT_URL=http://llm-connect.activity-core.svc.cluster.local:8080
|
export LLM_CONNECT_URL=http://llm-connect.activity-core.svc.cluster.local:8080
|
||||||
agent-harness mail-triage --target-repo ~/binky-control
|
agent-harness mail-triage --target-repo ~/binky-control
|
||||||
|
|
||||||
|
# Daily brief via llm-connect (structured JSON → briefs/YYYY-MM-DD-daily-brief.md)
|
||||||
|
agent-harness brief-daily --target-repo ~/binky-control
|
||||||
|
|
||||||
# Railiance packaging smoke (commit + optional push + hub; no Claude required)
|
# Railiance packaging smoke (commit + optional push + hub; no Claude required)
|
||||||
agent-harness smoke --work-dir ~/work/executor-sandbox
|
agent-harness smoke --work-dir ~/work/executor-sandbox
|
||||||
```
|
```
|
||||||
|
|
|
||||||
353
agent_harness/brief_daily.py
Normal file
353
agent_harness/brief_daily.py
Normal file
|
|
@ -0,0 +1,353 @@
|
||||||
|
"""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,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
@ -220,6 +220,28 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
help="Apply log updates but do not git commit",
|
help="Apply log updates but do not git commit",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
brief = sub.add_parser(
|
||||||
|
"brief-daily",
|
||||||
|
help="Write daily brief via llm-connect (JSON + markdown apply; no Claude)",
|
||||||
|
)
|
||||||
|
brief.add_argument("--target-repo", required=True)
|
||||||
|
brief.add_argument(
|
||||||
|
"--date",
|
||||||
|
default=None,
|
||||||
|
help="Brief date YYYY-MM-DD (default: today Europe/Berlin)",
|
||||||
|
)
|
||||||
|
brief.add_argument(
|
||||||
|
"--force",
|
||||||
|
action="store_true",
|
||||||
|
help="Overwrite if today's brief already exists",
|
||||||
|
)
|
||||||
|
brief.add_argument("--no-hub", action="store_true", help="Skip hub reporting")
|
||||||
|
brief.add_argument(
|
||||||
|
"--no-commit",
|
||||||
|
action="store_true",
|
||||||
|
help="Write brief file but do not git commit",
|
||||||
|
)
|
||||||
|
|
||||||
validate = sub.add_parser(
|
validate = sub.add_parser(
|
||||||
"validate",
|
"validate",
|
||||||
help="Validate instance manifest (.kaizen/schedule.yml + harness fields)",
|
help="Validate instance manifest (.kaizen/schedule.yml + harness fields)",
|
||||||
|
|
@ -352,6 +374,39 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
)
|
)
|
||||||
return 0 if triage_result.ok else 1
|
return 0 if triage_result.ok else 1
|
||||||
|
|
||||||
|
if args.command == "brief-daily":
|
||||||
|
from datetime import date as date_cls
|
||||||
|
|
||||||
|
from agent_harness.brief_daily import run_brief_daily
|
||||||
|
|
||||||
|
day = None
|
||||||
|
if args.date:
|
||||||
|
day = date_cls.fromisoformat(args.date)
|
||||||
|
brief_result = run_brief_daily(
|
||||||
|
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": brief_result.ok,
|
||||||
|
"date": brief_result.date,
|
||||||
|
"path": brief_result.path,
|
||||||
|
"wrote": brief_result.wrote,
|
||||||
|
"committed": brief_result.committed,
|
||||||
|
"skipped_existing": brief_result.skipped_existing,
|
||||||
|
"head_after": brief_result.head_after,
|
||||||
|
"reason": brief_result.reason,
|
||||||
|
"model_meta": brief_result.model_meta,
|
||||||
|
},
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return 0 if brief_result.ok else 1
|
||||||
|
|
||||||
if args.command == "mail-scan":
|
if args.command == "mail-scan":
|
||||||
from agent_harness.mailscan import run_mail_scan
|
from agent_harness.mailscan import run_mail_scan
|
||||||
|
|
||||||
|
|
|
||||||
125
tests/test_brief_daily.py
Normal file
125
tests/test_brief_daily.py
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from datetime import date
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from agent_harness import brief_daily
|
||||||
|
|
||||||
|
|
||||||
|
def _git_repo(tmp_path: Path) -> Path:
|
||||||
|
repo = tmp_path / "binky-control"
|
||||||
|
repo.mkdir()
|
||||||
|
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
|
||||||
|
subprocess.run(
|
||||||
|
["git", "config", "user.email", "test@example.com"],
|
||||||
|
cwd=repo,
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
["git", "config", "user.name", "test"],
|
||||||
|
cwd=repo,
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
(repo / "DecisionQueue.md").write_text(
|
||||||
|
"# DecisionQueue\n\n## Open\n\n### DEC-2026-999\nstatus: prepared\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(repo / "OfficeHourQueue.md").write_text("# OH\n", encoding="utf-8")
|
||||||
|
(repo / "AutopilotWorkQueue.md").write_text("# AWQ\n", encoding="utf-8")
|
||||||
|
(repo / "RiskRegister.md").write_text("# Risks\n", encoding="utf-8")
|
||||||
|
(repo / "briefs").mkdir()
|
||||||
|
subprocess.run(["git", "add", "."], cwd=repo, check=True, capture_output=True)
|
||||||
|
subprocess.run(
|
||||||
|
["git", "commit", "-m", "init"],
|
||||||
|
cwd=repo,
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
return repo
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_brief_format() -> None:
|
||||||
|
md = brief_daily.render_brief(
|
||||||
|
date(2026, 7, 22),
|
||||||
|
{
|
||||||
|
"decide_now": ["DEC-1 approve cutover"],
|
||||||
|
"progress": ["WP-0005 finished"],
|
||||||
|
"risks": [],
|
||||||
|
"best_next_hour": "Approve DEC-1",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert md.startswith("# Daily Brief — 2026-07-22\n")
|
||||||
|
assert "## Decide now" in md
|
||||||
|
assert "## Progress" in md
|
||||||
|
assert "## Risks" in md
|
||||||
|
assert "## Best next hour" in md
|
||||||
|
assert "No RiskRegister changes" in md
|
||||||
|
assert "Approve DEC-1" in md
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_brief_response_fences() -> None:
|
||||||
|
data = brief_daily.parse_brief_response(
|
||||||
|
'```json\n{"decide_now":[],"progress":["x"],"risks":[],'
|
||||||
|
'"best_next_hour":"none"}\n```'
|
||||||
|
)
|
||||||
|
assert data["progress"] == ["x"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_brief_daily_mock(tmp_path: Path, monkeypatch) -> None:
|
||||||
|
repo = _git_repo(tmp_path)
|
||||||
|
monkeypatch.setattr(brief_daily.hub, "post_progress_event", lambda **kw: True)
|
||||||
|
day = date(2026, 7, 22)
|
||||||
|
|
||||||
|
def fake(prompt: str) -> str:
|
||||||
|
assert "DecisionQueue" in prompt
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"decide_now": ["DEC-2026-999 go"],
|
||||||
|
"progress": ["scaffold landed"],
|
||||||
|
"risks": [],
|
||||||
|
"best_next_hour": "no founder action needed today",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = brief_daily.run_brief_daily(
|
||||||
|
repo, day=day, complete_fn=fake, force=True
|
||||||
|
)
|
||||||
|
assert result.ok
|
||||||
|
assert result.wrote
|
||||||
|
assert result.committed
|
||||||
|
path = repo / "briefs" / "2026-07-22-daily-brief.md"
|
||||||
|
assert path.is_file()
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
assert "DEC-2026-999" in text
|
||||||
|
assert "scaffold landed" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_skip_existing_brief(tmp_path: Path, monkeypatch) -> None:
|
||||||
|
repo = _git_repo(tmp_path)
|
||||||
|
monkeypatch.setattr(brief_daily.hub, "post_progress_event", lambda **kw: True)
|
||||||
|
day = date(2026, 7, 22)
|
||||||
|
path = brief_daily.brief_path_for(repo, day)
|
||||||
|
path.write_text("# Daily Brief — 2026-07-22\n\nalready\n", encoding="utf-8")
|
||||||
|
subprocess.run(["git", "add", "."], cwd=repo, check=True, capture_output=True)
|
||||||
|
subprocess.run(
|
||||||
|
["git", "commit", "-m", "brief"],
|
||||||
|
cwd=repo,
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
called = {"n": 0}
|
||||||
|
|
||||||
|
def boom(prompt: str) -> str:
|
||||||
|
called["n"] += 1
|
||||||
|
raise AssertionError("should not call LLM")
|
||||||
|
|
||||||
|
result = brief_daily.run_brief_daily(
|
||||||
|
repo, day=day, complete_fn=boom, force=False
|
||||||
|
)
|
||||||
|
assert result.ok
|
||||||
|
assert result.skipped_existing
|
||||||
|
assert called["n"] == 0
|
||||||
Loading…
Add table
Add a link
Reference in a new issue