From 4c4c1b66a58a8ea310534b80f6c8b5a907eb329d Mon Sep 17 00:00:00 2001 From: ProtocolWarden Date: Sat, 13 Jun 2026 19:20:53 -0400 Subject: [PATCH] fix(spec-hygiene): project only active campaigns into active.json (campaign GC) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _rebuild_active_projection wrote every campaign group — including complete and cancelled ones — into state/campaigns/active.json, the projection OperatorConsole's campaign pane reads. Terminal campaigns are history (their record lives in Plane), but they accumulated in the projection forever, so the status pane filled with finished campaigns (observed: 11 records, 10 terminal, 0 genuinely active). Skip non-active campaigns when rebuilding the projection so active.json reflects only in-flight work. A one-time manual prune of active.json does not stick — the file is rebuilt every spec-hygiene cycle — so this projection filter is the durable prune. +1 test (a mix of active/cancelled/done issues → only the active one is projected). Co-Authored-By: Claude Opus 4.8 --- .console/log.md | 9 ++++++ .../entrypoints/spec_hygiene/main.py | 8 ++++++ tests/maintenance/test_spec_hygiene_task.py | 28 +++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/.console/log.md b/.console/log.md index 2f9f02561..84a2b9755 100644 --- a/.console/log.md +++ b/.console/log.md @@ -1,3 +1,12 @@ +## 2026-06-13 — fix(spec-hygiene): active.json projects only active campaigns (campaign GC) + +_rebuild_active_projection wrote every campaign — incl. complete/cancelled — to state/campaigns/ +active.json, which OperatorConsole's campaign pane reads. Terminal campaigns are history (their +record lives in Plane) but accumulated forever in the projection (observed: 11 records, 10 terminal, +0 truly active — cluttering the pane). Fix: skip non-active campaigns when rebuilding the projection. ++1 test. (A one-time prune of active.json doesn't stick — it's rebuilt each spec-hygiene cycle — so +the projection filter IS the durable prune.) + ## 2026-06-13 — fix(reviewer): gate merge on the full required-check set (guard D) #272 + Guard C close "merge on red/incomplete/no-checks CI", but a hole remained: a required check diff --git a/src/operations_center/entrypoints/spec_hygiene/main.py b/src/operations_center/entrypoints/spec_hygiene/main.py index 50e2e870a..2f86f5e10 100644 --- a/src/operations_center/entrypoints/spec_hygiene/main.py +++ b/src/operations_center/entrypoints/spec_hygiene/main.py @@ -147,6 +147,14 @@ def _rebuild_active_projection( else: status = "active" + # active.json is the ACTIVE projection (OperatorConsole's campaign pane + # reads it). Terminal campaigns (complete/cancelled) are history — their + # record lives in Plane — so they are not projected here. Without this the + # projection accumulates every finished campaign indefinitely, cluttering + # the status pane (observed: 11 records, 10 terminal, only 0 truly active). + if status != "active": + continue + # Pull slug + spec_file + created_at from the parent issue when we can. parent = next( (i for i in issues if str(i.get("name", "")).startswith("[Campaign]")), diff --git a/tests/maintenance/test_spec_hygiene_task.py b/tests/maintenance/test_spec_hygiene_task.py index 1082661a7..f2c1556c8 100644 --- a/tests/maintenance/test_spec_hygiene_task.py +++ b/tests/maintenance/test_spec_hygiene_task.py @@ -98,3 +98,31 @@ def _boom(settings, client): # noqa: ARG001 result = task.run_once(MaintenanceContext(cycle_id="c", now=datetime.now(timezone.utc))) assert result.status == "failed" assert result.error == "plane down" + + +def test_rebuild_active_projection_excludes_terminal(tmp_path): + """active.json projects only active campaigns; terminal ones (complete/cancelled) + are history (in Plane) and must not accumulate in the pane projection.""" + from operations_center.spec_author.state import CampaignStateManager + + mgr = CampaignStateManager(state_path=tmp_path / "active.json") + + def issue(cid: str, state: str) -> dict: + return { + "name": f"[Campaign] {cid}", + "labels": [{"name": "source: spec-campaign"}, {"name": f"campaign-id: {cid}"}], + "state": {"name": state}, + } + + spec_hygiene_main._rebuild_active_projection( + mgr, + [ + issue("active1", "in progress"), # → active + issue("cancelled1", "cancelled"), # → cancelled (excluded) + issue("done1", "done"), # → complete (excluded) + ], + ) + + saved = mgr.load() + assert [c.slug for c in saved.campaigns] == ["active1"] + assert all(c.status == "active" for c in saved.campaigns)