From b2c6c5613218dea9dbaef303aff22a8d1b07f074 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 4 Oct 2026 13:18:33 +0200 Subject: [PATCH] ACTIVITY-WP-0044-T02: read freshness-run.v1 results[] from rmgr The repo-manager runner reports per-repository rows under results[], not receipts[]; the projection counted nothing against the real output. Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 692292@bnt-lap001 Assistant-Session: 89c7d8e8-e1b3-47eb-9411-a0ab227fca2f --- src/activity_core/bounded_operations.py | 12 +++++++----- tests/test_repo_manager_freshness_definition.py | 16 +++++++++++++--- ...-repo-manager-bounded-freshness-definition.md | 11 +++++++++-- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/activity_core/bounded_operations.py b/src/activity_core/bounded_operations.py index 63d5a8b..5e1c974 100644 --- a/src/activity_core/bounded_operations.py +++ b/src/activity_core/bounded_operations.py @@ -156,11 +156,13 @@ def normalize_operation_result(operation_id: str, raw: Any) -> dict[str, Any]: "script_exit_code": exit_code, } if operation_id == "repo_manager_freshness": - receipts = result.get("receipts") + # repo-manager.freshness-run.v1 reports one row per attempted repository + # under "results"; dry-run rows carry only a planned action, no outcome. + results = result.get("results") outcomes = {name: 0 for name in REPO_MANAGER_FRESHNESS_OUTCOMES} - if isinstance(receipts, list): - for receipt in receipts: - outcome = receipt.get("outcome") if isinstance(receipt, dict) else None + if isinstance(results, list): + for row in results: + outcome = row.get("outcome") if isinstance(row, dict) else None if outcome in outcomes: outcomes[outcome] += 1 exit_code = result.get("exit_code", 0) @@ -171,7 +173,7 @@ def normalize_operation_result(operation_id: str, raw: Any) -> dict[str, Any]: "apply": bool(result.get("apply")), "limit": _safe_count(result.get("limit")), "max_seconds": _safe_count(result.get("max_seconds")), - "receipt_count": len(receipts) if isinstance(receipts, list) else 0, + "result_count": len(results) if isinstance(results, list) else 0, "outcomes": outcomes, "exit_code": exit_code, } diff --git a/tests/test_repo_manager_freshness_definition.py b/tests/test_repo_manager_freshness_definition.py index 65551fd..e87641f 100644 --- a/tests/test_repo_manager_freshness_definition.py +++ b/tests/test_repo_manager_freshness_definition.py @@ -94,7 +94,7 @@ def test_invocation_projects_counts_only(monkeypatch): def fake_run(argv, **kwargs): calls.append((argv, kwargs["timeout"])) - out = {"receipts": [ + out = {"schema": "repo-manager.freshness-run.v1", "results": [ {"repo": "a", "outcome": "applied", "detail": "must not persist"}, {"repo": "b", "outcome": "noop"}, {"repo": "c", "outcome": "refused"}, @@ -106,7 +106,7 @@ def test_invocation_projects_counts_only(monkeypatch): raw = repo_manager.repo_manager_freshness({"limit": 6, "max_seconds": 240, "apply": True}) projected = normalize_operation_result("repo_manager_freshness", raw) assert calls[0][1] == 240 + repo_manager._TIMEOUT_GRACE_SECONDS - assert projected["receipt_count"] == 4 + assert projected["result_count"] == 4 assert projected["outcomes"]["applied"] == 1 assert projected["outcomes"]["noop"] == 1 assert projected["outcomes"]["refused"] == 1 @@ -114,6 +114,16 @@ def test_invocation_projects_counts_only(monkeypatch): assert "must not persist" not in json.dumps(projected) +def test_dry_run_rows_without_outcome_count_nothing(): + raw = {"schema": "repo-manager.freshness-run.v1", "dry_run": True, + "results": [{"slug": "a", "action": "reconcile"}, + {"slug": "b", "action": "skip_noop"}], + "apply": False, "limit": 2, "max_seconds": 60, "exit_code": 0} + projected = normalize_operation_result("repo_manager_freshness", raw) + assert projected["result_count"] == 2 + assert sum(projected["outcomes"].values()) == 0 + + def test_invocation_fails_on_non_json(monkeypatch): monkeypatch.setattr( repo_manager.subprocess, "run", @@ -127,7 +137,7 @@ def test_invocation_fails_on_non_json(monkeypatch): async def test_execute_bounded_operation_dispatches_to_repo_manager(monkeypatch): monkeypatch.setattr( repo_manager, "repo_manager_freshness", - lambda params: {"receipts": [], "apply": False, "limit": 1, "max_seconds": 10}, + lambda params: {"results": [], "apply": False, "limit": 1, "max_seconds": 10}, ) patch = await activities.execute_bounded_operation({"source": _source(apply=False)}) assert patch["freshness"]["kind"] == "repo_manager_freshness" diff --git a/workplans/ACTIVITY-WP-0044-repo-manager-bounded-freshness-definition.md b/workplans/ACTIVITY-WP-0044-repo-manager-bounded-freshness-definition.md index 232dd93..fd08dac 100644 --- a/workplans/ACTIVITY-WP-0044-repo-manager-bounded-freshness-definition.md +++ b/workplans/ACTIVITY-WP-0044-repo-manager-bounded-freshness-definition.md @@ -47,10 +47,9 @@ state_hub_task_id: "0cbbadc8-d217-55a3-bc2a-da81e19ec2a8" ```task id: ACTIVITY-WP-0044-T02 -status: wait +status: progress priority: medium needs_human: false -blocking_reason: "rmgr freshness run is being built in RMGR-WP-0020-T02; JSON output (receipts[].outcome) assumed from the T01 receipt contract" depends_on: [RMGR-WP-0020-T01, RMGR-WP-0020-T02] state_hub_task_id: "e4517812-aec2-57e9-9677-de4a89f3f48f" ``` @@ -60,6 +59,14 @@ JSON object with `receipts[]` carrying `repo-manager.freshness-receipt.v1` outcomes; adjust `normalize_operation_result` if the shape differs. Decide the worker image/packaging of `rmgr` (no deployment in this workplan). +2026-10-04: RMGR-WP-0020-T01/T02 landed (repo-manager ad1ee3e). Flags match +(`--limit`, `--max-seconds` required, `--dry-run`). The run summary is +`repo-manager.freshness-run.v1` with per-repository rows under `results[]` +(not `receipts[]`); dry-run rows carry only a planned `action`. The projection +now reads `results[].outcome` and reports `result_count`; 35 focused tests pass. +Exit 1 with JSON (a failed attempt) still projects; exit 3/4 without JSON raise. +Remaining: decide how `rmgr` is packaged onto the worker. + ## Founder-reviewed activation ```task