"""Dual-run flags and checkout-mutation meter (RMGR-WP-0002 / ArchitectureBlueprint Stage B). Environment variables (shared with State Hub adapter): RM_WRITEBACK=1|true|yes — file+git task writeback via repo-manager RM_RECONCILE=1|true|yes — reconcile path prefers repo-manager for pilot repos RM_PILOT_REPOS=slug1,slug2 — if set, flags only apply to these slugs; empty = all RM_METER_PATH=~/.repo-manager/mutation-meter.jsonl — append-only meter log Rollback: unset flags or set to 0 → State Hub native path only. """ from __future__ import annotations import json import os from datetime import datetime, timezone from pathlib import Path from typing import Any, Literal Source = Literal["state-hub", "repo-manager"] def _truthy(name: str) -> bool: return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"} def writeback_enabled() -> bool: return _truthy("RM_WRITEBACK") def reconcile_enabled() -> bool: return _truthy("RM_RECONCILE") def pilot_slugs() -> set[str] | None: """None means all repos; empty set after parse of blank list means none.""" raw = os.environ.get("RM_PILOT_REPOS") if raw is None: return None raw = raw.strip() if not raw: return set() return {s.strip() for s in raw.split(",") if s.strip()} def slug_allowed(slug: str | None) -> bool: if not slug: return False pilots = pilot_slugs() if pilots is None: return True return slug in pilots def writeback_for_repo(slug: str | None) -> bool: return writeback_enabled() and slug_allowed(slug) def reconcile_for_repo(slug: str | None) -> bool: return reconcile_enabled() and slug_allowed(slug) def meter_path() -> Path: raw = os.environ.get("RM_METER_PATH", "~/.repo-manager/mutation-meter.jsonl") return Path(raw).expanduser() def record_mutation( *, source: Source, kind: str, repo_slug: str | None, detail: dict[str, Any] | None = None, ) -> None: """Append one meter line. Best-effort; never raises to callers.""" try: path = meter_path() path.parent.mkdir(parents=True, exist_ok=True) row = { "ts": datetime.now(timezone.utc).isoformat(), "source": source, "kind": kind, "repo_slug": repo_slug, "detail": detail or {}, } with path.open("a", encoding="utf-8") as fh: fh.write(json.dumps(row, default=str) + "\n") except OSError: pass def meter_summary(path: Path | None = None) -> dict[str, int]: path = path or meter_path() counts: dict[str, int] = {"state-hub": 0, "repo-manager": 0, "total": 0} if not path.is_file(): return counts for line in path.read_text(encoding="utf-8").splitlines(): line = line.strip() if not line: continue try: row = json.loads(line) except json.JSONDecodeError: continue src = row.get("source") if src in counts: counts[src] += 1 counts["total"] += 1 return counts def flags_status() -> dict[str, Any]: return { "RM_WRITEBACK": writeback_enabled(), "RM_RECONCILE": reconcile_enabled(), "RM_PILOT_REPOS": sorted(pilot_slugs()) if pilot_slugs() is not None else None, "RM_METER_PATH": str(meter_path()), "meter": meter_summary(), }