"""Scoped on-demand identifier registrar for the ADR-007 interim. The production fleet sweep is intentionally disabled. This command provides one explicit, repository-scoped way to run the existing State Hub registration adapter without teaching ordinary agent sessions to set ``STATEHUB_REGISTRAR`` themselves. """ from __future__ import annotations import fcntl import os import shutil import subprocess import uuid from dataclasses import dataclass from pathlib import Path from typing import Any import httpx from repo_manager.gitops import GitError, commit_paths, push_ff from repo_manager.parse.record import iter_record_files, parse_record_file from repo_manager.parse.workplan import parse_workplan_file from repo_manager.record_identity import scan_record_identities LOCK_PATH = Path("/tmp/repo-manager-identifier-registrar.lock") @dataclass class RegistrarResult: status: str evidence: dict[str, Any] error: dict[str, Any] | None = None correlation_id: str = "" def to_dict(self) -> dict[str, Any]: result: dict[str, Any] = { "command": "repo.identifiers.registrar_reconcile", "status": self.status, "correlation_id": self.correlation_id, "evidence": self.evidence, } if self.error: result["error"] = self.error return result def _git(repo: Path, *args: str) -> subprocess.CompletedProcess[str]: return subprocess.run( ["git", *args], cwd=repo, capture_output=True, text=True, check=False, ) def _missing_identifiers(repo: Path) -> dict[str, list[str]]: workplans: list[str] = [] tasks: list[str] = [] intakes: list[str] = [] decisions: list[str] = [] workplans_dir = repo / "workplans" if workplans_dir.is_dir(): # Closed archives are frozen under ADR-007 and are not registration work. for path in sorted(workplans_dir.glob("*.md")): parsed = parse_workplan_file(path, repo_root=repo) if parsed.frontmatter.get("type") != "workplan" or not parsed.id: continue if not parsed.state_hub_workstream_id: workplans.append(parsed.id) # C-06 creates a newly discovered workplan and its tasks as one # registration unit, including closed file-backed provenance. # Include those task gaps so post-verification cannot report a # false success after creating only the parent. for task in parsed.tasks: if task.id and not task.state_hub_task_id: tasks.append(task.id) continue # Once a closed workplan is linked, missing task identifiers are # frozen historical artefacts. State Hub's C-11 fixer deliberately # refuses to create them. if (parsed.status or "").strip().lower() in {"finished", "archived"}: continue for task in parsed.tasks: if task.id and not task.state_hub_task_id: tasks.append(task.id) for path in iter_record_files(repo): for record in parse_record_file(path, repo_root=repo): if record.uuid: continue target = intakes if record.kind == "intake" else decisions target.append(record.id) return { "workplans": workplans, "tasks": tasks, "intakes": intakes, "decisions": decisions, } def _check_git(repo: Path) -> tuple[dict[str, Any], str | None]: status = _git(repo, "status", "--porcelain") if status.returncode != 0: return {}, status.stderr.strip() or "not a Git repository" if status.stdout.strip(): return {}, "worktree must be clean before registrar reconciliation" upstream = _git(repo, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}") if upstream.returncode != 0: return {}, "current branch must have an upstream" counts = _git(repo, "rev-list", "--left-right", "--count", "@{u}...HEAD") if counts.returncode != 0: return {}, counts.stderr.strip() or "could not compare HEAD with upstream" try: behind, ahead = (int(value) for value in counts.stdout.split()) except (TypeError, ValueError): return {}, "could not parse Git ahead/behind state" if behind or ahead: return ( {"upstream": upstream.stdout.strip(), "behind": behind, "ahead": ahead}, "branch must match its upstream; pull or push before registrar reconciliation", ) remote = _git(repo, "remote", "get-url", "origin") if remote.returncode != 0: return {}, "origin remote is required" remote_url = remote.stdout.strip() stale_markers = ("gitea-remote", "gitea.coulomb.social", "92.205.130.254") if any(marker in remote_url for marker in stale_markers): return {"origin": remote_url}, "origin still targets the retired Gitea lineage" return { "upstream": upstream.stdout.strip(), "behind": behind, "ahead": ahead, "origin": remote_url, }, None def _check_primary(api_base: str) -> tuple[dict[str, Any], str | None]: try: response = httpx.get(f"{api_base.rstrip('/')}/state/health", timeout=10.0) response.raise_for_status() payload = response.json() except (httpx.HTTPError, ValueError) as exc: return {}, f"primary State Hub health check failed: {exc}" if payload.get("status") != "ok" or payload.get("db") != "connected": return payload, "State Hub is not healthy and database-connected" return payload, None def _workplan_projection_id(repo: Path, canonical_id: str) -> str | None: """Return the authoritative projection UUID for one open workplan.""" workplans_dir = repo / "workplans" if not workplans_dir.is_dir(): return None for path in sorted(workplans_dir.glob("*.md")): parsed = parse_workplan_file(path, repo_root=repo) if parsed.id != canonical_id: continue return parsed.state_hub_workstream_id or None return None def _projection_exists(api_base: str, projection_id: str) -> tuple[dict[str, Any], str | None]: try: response = httpx.get( f"{api_base.rstrip('/')}/workplans/{projection_id}", timeout=10.0, ) response.raise_for_status() payload = response.json() except (httpx.HTTPError, ValueError) as exc: return {}, f"workplan projection verification failed: {exc}" if str(payload.get("id")) != projection_id: return payload, "workplan projection returned an unexpected identity" return payload, None def _check_empty_repo_projection( api_base: str, repo_slug: str ) -> tuple[dict[str, Any], str | None]: try: repo_response = httpx.get(f"{api_base.rstrip('/')}/repos/{repo_slug}", timeout=10.0) repo_response.raise_for_status() repo_payload = repo_response.json() response = httpx.get( f"{api_base.rstrip('/')}/workplans/", params={"repo_id": repo_payload["id"]}, timeout=10.0, ) response.raise_for_status() rows = response.json() except (httpx.HTTPError, KeyError, ValueError) as exc: return {}, f"empty projection preflight failed: {exc}" if not isinstance(rows, list): return {}, "empty projection preflight returned an invalid workplan collection" evidence = {"repo_id": repo_payload["id"], "workplan_count": len(rows)} if rows: return evidence, "target repository projection is not empty" return evidence, None def _authoritative_projection_ids( repo: Path, ) -> tuple[set[str], set[str], dict[str, set[str]], str | None]: workplan_ids: set[str] = set() task_ids: set[str] = set() record_ids: dict[str, set[str]] = {"intake": set(), "decision": set()} for path in sorted((repo / "workplans").glob("*.md")): parsed = parse_workplan_file(path, repo_root=repo) if parsed.frontmatter.get("type") != "workplan" or not parsed.id: continue if not parsed.state_hub_workstream_id: return set(), set(), record_ids, f"{parsed.id} has no authoritative projection UUID" workplan_ids.add(parsed.state_hub_workstream_id) for task in parsed.tasks: if task.id and not task.state_hub_task_id: return set(), set(), record_ids, f"{task.id} has no authoritative projection UUID" if task.state_hub_task_id: task_ids.add(task.state_hub_task_id) if not workplan_ids: return set(), set(), record_ids, "repository has no root workplans to rebuild" for path in iter_record_files(repo): for record in parse_record_file(path, repo_root=repo): if record.kind not in record_ids: continue if not record.uuid: return ( set(), set(), record_ids, f"{record.id} has no authoritative projection UUID", ) record_ids[record.kind].add(record.uuid) return workplan_ids, task_ids, record_ids, None def _verify_full_projection( api_base: str, workplan_ids: set[str], task_ids: set[str], record_ids: dict[str, set[str]], ) -> tuple[dict[str, Any], str | None]: missing_workplans: list[str] = [] missing_tasks: list[str] = [] missing_records: dict[str, list[str]] = {"intake": [], "decision": []} try: for projection_id in sorted(workplan_ids): response = httpx.get( f"{api_base.rstrip('/')}/workplans/{projection_id}", timeout=10.0 ) if response.status_code == 404: missing_workplans.append(projection_id) else: response.raise_for_status() for projection_id in sorted(task_ids): response = httpx.get(f"{api_base.rstrip('/')}/tasks/{projection_id}", timeout=10.0) if response.status_code == 404: missing_tasks.append(projection_id) else: response.raise_for_status() for kind, endpoint in (("intake", "intakes"), ("decision", "decisions")): for projection_id in sorted(record_ids[kind]): response = httpx.get( f"{api_base.rstrip('/')}/{endpoint}/{projection_id}", timeout=10.0 ) if response.status_code == 404: missing_records[kind].append(projection_id) else: response.raise_for_status() except httpx.HTTPError as exc: return {}, f"full projection verification failed: {exc}" evidence = { "expected_workplans": len(workplan_ids), "expected_tasks": len(task_ids), "missing_workplans": missing_workplans, "missing_tasks": missing_tasks, "expected_intakes": len(record_ids["intake"]), "expected_decisions": len(record_ids["decision"]), "missing_intakes": missing_records["intake"], "missing_decisions": missing_records["decision"], } if missing_workplans or missing_tasks or any(missing_records.values()): return evidence, "full projection rebuild is incomplete" return evidence, None def _requested_projection_ids( repo: Path, requested: dict[str, list[str]], ) -> tuple[set[str], set[str], dict[str, set[str]], str | None]: """Resolve only the identifiers requested by this registrar invocation.""" wanted_workplans = set(requested["workplans"]) wanted_tasks = set(requested["tasks"]) wanted_records = { "intake": set(requested["intakes"]), "decision": set(requested["decisions"]), } resolved_workplans: dict[str, str] = {} resolved_tasks: dict[str, str] = {} resolved_records: dict[str, dict[str, str]] = {"intake": {}, "decision": {}} for path in sorted((repo / "workplans").glob("*.md")): parsed = parse_workplan_file(path, repo_root=repo) if parsed.id in wanted_workplans and parsed.state_hub_workstream_id: resolved_workplans[parsed.id] = parsed.state_hub_workstream_id for task in parsed.tasks: if task.id in wanted_tasks and task.state_hub_task_id: resolved_tasks[task.id] = task.state_hub_task_id for path in iter_record_files(repo): for record in parse_record_file(path, repo_root=repo): if record.kind in wanted_records and record.id in wanted_records[record.kind] and record.uuid: resolved_records[record.kind][record.id] = record.uuid unresolved = sorted( (wanted_workplans - resolved_workplans.keys()) | (wanted_tasks - resolved_tasks.keys()) | (wanted_records["intake"] - resolved_records["intake"].keys()) | (wanted_records["decision"] - resolved_records["decision"].keys()) ) record_ids = { kind: set(records.values()) for kind, records in resolved_records.items() } if unresolved: return ( set(resolved_workplans.values()), set(resolved_tasks.values()), record_ids, f"requested records have no assigned projection UUID: {', '.join(unresolved)}", ) return ( set(resolved_workplans.values()), set(resolved_tasks.values()), record_ids, None, ) def _run_statehub(command: list[str], *, env: dict[str, str]) -> subprocess.CompletedProcess[str]: return subprocess.run( command, capture_output=True, text=True, check=False, timeout=300, env=env, ) def _restore_generated_brief(repo: Path, paths: list[str]) -> tuple[list[str], str | None]: """Restore the generated brief after a projection-only repair child.""" if not paths: return [], None if paths != [".custodian-brief.md"]: return [], "projection repair changed files other than the generated Custodian brief" original = _git(repo, "show", "HEAD:.custodian-brief.md") if original.returncode != 0: return [], original.stderr.strip() or "could not read the committed Custodian brief" (repo / ".custodian-brief.md").write_text(original.stdout, encoding="utf-8") remaining = _git(repo, "status", "--porcelain") if remaining.returncode != 0 or remaining.stdout.strip(): return [], remaining.stderr.strip() or "generated brief restoration left a dirty worktree" return paths, None def registrar_reconcile( path: Path, *, api_base: str = "http://127.0.0.1:8000", statehub_bin: str | None = None, confirm_primary: bool = False, push: bool = False, repair_workplan: str | None = None, bootstrap_empty_projection: bool = False, ) -> RegistrarResult: """Register missing workplan/task UUIDs through one scoped child process.""" cid = str(uuid.uuid4()) repo = path.expanduser().resolve() identity = scan_record_identities(repo) before = _missing_identifiers(repo) evidence: dict[str, Any] = { "repo_path": str(repo), "repo_slug": repo.name, "api_base": api_base.rstrip("/"), "missing_before": before, "record_identity": identity, } if identity["identity_collisions"]: return RegistrarResult( "rejected", evidence, { "code": "record_identity_collision", "message": "same canonical work-record id has conflicting or incomplete UUID assignments", }, cid, ) repair_projection_id = None if repair_workplan and bootstrap_empty_projection: return RegistrarResult( "rejected", evidence, { "code": "registrar_mode_conflict", "message": "repair-workplan and bootstrap-empty-projection are mutually exclusive", }, cid, ) if repair_workplan: repair_projection_id = _workplan_projection_id(repo, repair_workplan) evidence["repair_workplan"] = repair_workplan evidence["repair_projection_id"] = repair_projection_id if not repair_projection_id: return RegistrarResult( "rejected", evidence, { "code": "repair_target_invalid", "message": "repair workplan is absent or has no authoritative projection UUID", }, cid, ) if not confirm_primary: return RegistrarResult( "rejected", evidence, { "code": "confirmation_required", "message": "pass --confirm-primary after verifying this is the authoritative hub", }, cid, ) if not any(before.values()) and not repair_projection_id and not bootstrap_empty_projection: evidence["missing_after"] = before return RegistrarResult("noop", evidence, None, cid) git_evidence, git_error = _check_git(repo) evidence["git"] = git_evidence if git_error: return RegistrarResult( "rejected", evidence, {"code": "git_precondition_failed", "message": git_error}, cid ) health, health_error = _check_primary(api_base) evidence["state_hub_health"] = health if health_error: return RegistrarResult( "rejected", evidence, {"code": "primary_unavailable", "message": health_error}, cid ) executable = statehub_bin or shutil.which("statehub") if not executable: return RegistrarResult( "rejected", evidence, {"code": "statehub_missing", "message": "statehub CLI is not installed"}, cid, ) bootstrap_workplans: set[str] = set() bootstrap_tasks: set[str] = set() bootstrap_records: dict[str, set[str]] = {"intake": set(), "decision": set()} if bootstrap_empty_projection: bootstrap_before, bootstrap_error = _check_empty_repo_projection(api_base, repo.name) evidence["bootstrap_projection_before"] = bootstrap_before if bootstrap_error: return RegistrarResult( "rejected", evidence, {"code": "bootstrap_precondition_failed", "message": bootstrap_error}, cid, ) ( bootstrap_workplans, bootstrap_tasks, bootstrap_records, source_error, ) = _authoritative_projection_ids(repo) evidence["bootstrap_source"] = { "workplans": len(bootstrap_workplans), "tasks": len(bootstrap_tasks), "intakes": len(bootstrap_records["intake"]), "decisions": len(bootstrap_records["decision"]), } if source_error: return RegistrarResult( "rejected", evidence, {"code": "bootstrap_source_invalid", "message": source_error}, cid, ) LOCK_PATH.parent.mkdir(parents=True, exist_ok=True) with LOCK_PATH.open("a+", encoding="utf-8") as lock: try: fcntl.flock(lock.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) except BlockingIOError: return RegistrarResult( "rejected", evidence, {"code": "registrar_busy", "message": "another registrar reconciliation is active"}, cid, ) child_env = {**os.environ, "STATEHUB_REGISTRAR": "1"} command = [ executable, "fix-consistency", "--path", str(repo), "--api-base", api_base.rstrip("/"), ] if bootstrap_empty_projection: command.append("--bootstrap-empty-projection") completed = _run_statehub(command, env=child_env) evidence["statehub_exit_code"] = completed.returncode evidence["statehub_stdout_tail"] = completed.stdout[-4000:] evidence["statehub_stderr_tail"] = completed.stderr[-2000:] after = _missing_identifiers(repo) evidence["missing_after"] = after repair_verified = False if repair_projection_id: projection, projection_error = _projection_exists(api_base, repair_projection_id) evidence["repair_projection"] = projection evidence["repair_projection_verified"] = projection_error is None if projection_error: evidence["repair_projection_error"] = projection_error repair_verified = projection_error is None bootstrap_verified = False if bootstrap_empty_projection: projection, projection_error = _verify_full_projection( api_base, bootstrap_workplans, bootstrap_tasks, bootstrap_records, ) evidence["bootstrap_projection"] = projection evidence["bootstrap_projection_verified"] = projection_error is None if projection_error: evidence["bootstrap_projection_error"] = projection_error bootstrap_verified = projection_error is None requested_verified = False if completed.returncode == 1 and any(before.values()) and not any(after.values()): ( requested_workplans, requested_tasks, requested_records, requested_source_error, ) = _requested_projection_ids(repo, before) if requested_source_error: evidence["requested_projection_error"] = requested_source_error else: projection, projection_error = _verify_full_projection( api_base, requested_workplans, requested_tasks, requested_records, ) evidence["requested_projection"] = projection evidence["requested_projection_verified"] = projection_error is None if projection_error: evidence["requested_projection_error"] = projection_error requested_verified = projection_error is None accepted_exit_codes = {0, 2} if repair_verified or bootstrap_verified or requested_verified: # A repository-scoped projection repair may coexist with legacy stale # references that correctly keep the broader consistency report red. accepted_exit_codes.add(1) mode_verification_failed = bool(repair_projection_id and not repair_verified) or ( bootstrap_empty_projection and not bootstrap_verified ) if ( completed.returncode not in accepted_exit_codes or any(after.values()) or mode_verification_failed ): return RegistrarResult( "failed", evidence, { "code": "registration_incomplete", "message": "State Hub reconciliation did not assign every requested identifier", }, cid, ) changed = _git(repo, "status", "--porcelain") paths = [line[3:] for line in changed.stdout.splitlines() if len(line) > 3] if (repair_verified or bootstrap_verified) and paths: restored, restore_error = _restore_generated_brief(repo, paths) evidence["restored_generated_paths"] = restored if restore_error: return RegistrarResult( "failed", evidence, {"code": "projection_repair_file_change", "message": restore_error}, cid, ) paths = [] if paths: try: evidence["git_sha"] = commit_paths( repo, paths, "chore(registrar): assign State Hub identifiers", ) except GitError as exc: return RegistrarResult( "failed", evidence, {"code": "commit_failed", "message": str(exc)}, cid, ) if push: pushed, message = push_ff(repo) evidence["push"] = {"ok": pushed, "message": message} if not pushed: return RegistrarResult( "failed", evidence, {"code": "push_failed", "message": message}, cid, ) return RegistrarResult("applied", evidence, None, cid)