From 70a7db500dd8e4a65fc1e07ff677eb2484f3b428 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 24 Jul 2026 00:17:46 +0200 Subject: [PATCH] Add fi_brief_status resolver for Freedom Intelligence daily briefs. Mirror binky_rhythm daily due-checks against fi_daily_brief progress events so activity-core can schedule freedom-intelligence research without executing it. --- .../context_resolvers/state_hub.py | 53 ++++++++++++++ tests/test_state_hub_context_resolver.py | 73 +++++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/src/activity_core/context_resolvers/state_hub.py b/src/activity_core/context_resolvers/state_hub.py index 9fed76c..72531d1 100644 --- a/src/activity_core/context_resolvers/state_hub.py +++ b/src/activity_core/context_resolvers/state_hub.py @@ -19,6 +19,8 @@ Supported queries: - legacy_meter_weekly_review: GET {STATE_HUB_URL}/legacy-meter/weekly-review - binky_rhythm_status: due-items for the Binky operating-rhythm definitions, derived from /progress/ events (see _binky_rhythm_status) + - fi_brief_status: due-item for Freedom Intelligence daily research brief, + derived from fi_daily_brief progress events When STATE_HUB_URL points at the state-hub edge relay, allowlisted GET reads may be served from a stale local cache during upstream outages (`X-StateHub-Edge-Cache: @@ -158,6 +160,8 @@ class StateHubContextResolver(ContextResolver): return _legacy_meter_weekly_review(params) if query == "binky_rhythm_status": return _binky_rhythm_status(params) + if query == "fi_brief_status": + return _fi_brief_status(params) return {} @@ -470,6 +474,55 @@ def _binky_progress_events(event_type: str, repo: str) -> list[dict[str, Any]]: return matched +_FI_BRIEF_EVENT_TYPE = "fi_daily_brief" +_FI_BRIEF_DEFAULT_REPO = "freedom-intelligence" + + +def _fi_brief_status(params: dict[str, Any]) -> dict[str, Any]: + """Due-item for Freedom Intelligence daily research brief. + + Params: repo (default "freedom-intelligence"), timezone (IANA name, + default Europe/Berlin — "today" is evaluated in this zone). + + due is true when no progress event with event_type=fi_daily_brief and + detail.repo matching the target repo exists for today. Executing sessions + must post that event on completion (see freedom-intelligence + docs/daily-brief-playbook.md). + """ + repo = str(params.get("repo") or _FI_BRIEF_DEFAULT_REPO) + kind = str(params.get("kind") or "daily_brief") + if kind != "daily_brief": + return {"items": [], "repo": repo, "error": f"unknown kind: {kind}"} + + tz = _binky_timezone(params.get("timezone")) + now_local = _utc_now().astimezone(tz) + today = now_local.date() + + events = _binky_progress_events(_FI_BRIEF_EVENT_TYPE, repo) + timestamps = [ + ts + for ts in ( + _parse_progress_timestamp(item.get("created_at")) for item in events + ) + if ts > datetime.min.replace(tzinfo=timezone.utc) + ] + last_run = max(timestamps, default=None) + due = last_run is None or last_run.astimezone(tz).date() != today + + return { + "items": [ + { + "kind": kind, + "due": due, + "date": today.isoformat(), + "last_run_at": last_run.isoformat() if last_run else None, + } + ], + "repo": repo, + "generated_at": _utc_now().isoformat(), + } + + def _repo_sbom_status(params: dict[str, Any]) -> dict[str, Any]: """Resolve SBOM staleness against the State Hub. diff --git a/tests/test_state_hub_context_resolver.py b/tests/test_state_hub_context_resolver.py index 4a88be2..0f377f1 100644 --- a/tests/test_state_hub_context_resolver.py +++ b/tests/test_state_hub_context_resolver.py @@ -998,3 +998,76 @@ def test_binky_rhythm_unknown_kind_returns_error_shape(monkeypatch) -> None: assert result["items"] == [] assert "unknown kind" in result["error"] + + +def test_fi_brief_status_due_when_no_events(monkeypatch) -> None: + _binky_resolver_env(monkeypatch, {"fi_daily_brief": []}) + + result = StateHubContextResolver().resolve( + "fi_brief_status", None, {"repo": "freedom-intelligence"} + ) + + assert result["repo"] == "freedom-intelligence" + assert result["items"] == [ + { + "kind": "daily_brief", + "due": True, + "date": "2026-07-17", + "last_run_at": None, + } + ] + + +def test_fi_brief_status_not_due_after_todays_run(monkeypatch) -> None: + _binky_resolver_env( + monkeypatch, + { + "fi_daily_brief": [ + { + "event_type": "fi_daily_brief", + "created_at": "2026-07-17T05:30:00+00:00", + "detail": {"repo": "freedom-intelligence"}, + } + ] + }, + ) + + result = StateHubContextResolver().resolve( + "fi_brief_status", None, {"repo": "freedom-intelligence"} + ) + + item = result["items"][0] + assert item["due"] is False + assert item["last_run_at"] == "2026-07-17T05:30:00+00:00" + + +def test_fi_brief_status_ignores_other_repo(monkeypatch) -> None: + _binky_resolver_env( + monkeypatch, + { + "fi_daily_brief": [ + { + "event_type": "fi_daily_brief", + "created_at": "2026-07-17T05:30:00+00:00", + "detail": {"repo": "other-lab"}, + } + ] + }, + ) + + result = StateHubContextResolver().resolve( + "fi_brief_status", None, {"repo": "freedom-intelligence"} + ) + + assert result["items"][0]["due"] is True + + +def test_fi_brief_status_unknown_kind(monkeypatch) -> None: + _binky_resolver_env(monkeypatch, {}) + + result = StateHubContextResolver().resolve( + "fi_brief_status", None, {"repo": "freedom-intelligence", "kind": "weekly"} + ) + + assert result["items"] == [] + assert "unknown kind" in result["error"]