"""Close-ritual land check — compare require_paths to origin/main (never merge).""" from __future__ import annotations import hashlib import subprocess from dataclasses import dataclass from pathlib import Path from typing import Any, Callable from adapters.config import CloseRitualConfig, OverseerConfig from adapters.factory import create_adapter from adapters.runner import CommandRunner, SubprocessRunner from tools.governance_freshness import check_governance_freshness from tools.land_closeout import check_land_closeout @dataclass(frozen=True) class LandCheckResult: """Outcome of ``ok land-check``.""" exit_code: int landed: bool mode: str ref: str paths: tuple[dict[str, Any], ...] dirty_paths: tuple[str, ...] messages: tuple[str, ...] auto_merge: bool = False # always False — Tier 3 def _sha256_bytes(data: bytes) -> str: return hashlib.sha256(data).hexdigest() def _git(repo_root: Path, *args: str) -> subprocess.CompletedProcess[str]: return subprocess.run( ["git", *args], cwd=str(repo_root), capture_output=True, text=True, ) def compare_paths_to_main( repo_root: Path, paths: tuple[str, ...], *, remote: str, main_branch: str, ) -> tuple[str, list[dict[str, Any]], list[str]]: """Return (ref, path_reports, dirty_paths).""" ref = f"{remote}/{main_branch}" if _git(repo_root, "rev-parse", "--verify", ref).returncode != 0: if _git(repo_root, "rev-parse", "--verify", main_branch).returncode == 0: ref = main_branch else: return ref, [], list(paths) reports: list[dict[str, Any]] = [] for rel in paths: wt_path = repo_root / rel wt = _sha256_bytes(wt_path.read_bytes()) if wt_path.is_file() else None shown = _git(repo_root, "show", f"{ref}:{rel}") main_sha = _sha256_bytes(shown.stdout.encode("utf-8")) if shown.returncode == 0 else None reports.append( { "path": rel, "workingTreeSha256": wt, "mainSha256": main_sha, "match": wt is not None and main_sha is not None and wt == main_sha, } ) dirty = _git(repo_root, "status", "--porcelain", "--", *paths) dirty_paths = [line[3:].strip() for line in dirty.stdout.splitlines() if line.strip()] return ref, reports, dirty_paths def run_land_check( config: OverseerConfig, repo_root: Path, *, mode: str | None = None, emit: Callable[[str], None] | None = None, runner: CommandRunner | None = None, ) -> LandCheckResult: """Run close_ritual land check. Never merges to main.""" ritual: CloseRitualConfig = config.close_ritual messages: list[str] = [] def _emit(line: str) -> None: messages.append(line) if emit: emit(line) if not ritual.enabled: _emit("close_ritual.enabled is false — land-check is a no-op (exit 0)") return LandCheckResult( exit_code=0, landed=True, mode=mode or ritual.mode, ref="", paths=(), dirty_paths=(), messages=tuple(messages), ) effective_mode = mode or ritual.mode if effective_mode not in {"verify_landed", "prepare_pr"}: _emit(f"unsupported land-check mode: {effective_mode}") return LandCheckResult( exit_code=2, landed=False, mode=effective_mode, ref="", paths=(), dirty_paths=(), messages=tuple(messages), ) if ritual.consumer_verify_script: script = repo_root / ritual.consumer_verify_script if not script.is_file(): _emit(f"consumer_verify_script missing: {ritual.consumer_verify_script}") return LandCheckResult( exit_code=1, landed=False, mode=effective_mode, ref="", paths=(), dirty_paths=(), messages=tuple(messages), ) completed = subprocess.run( ["python3", str(script)], cwd=str(repo_root), capture_output=True, text=True, ) out = (completed.stdout or completed.stderr or "").strip() if out: _emit(out) if completed.returncode != 0: _emit( "note: ok land-check never merges; use ok pr-land --authorized for wait-for-green land" ) return LandCheckResult( exit_code=completed.returncode, landed=False, mode=effective_mode, ref="consumer_script", paths=(), dirty_paths=(), messages=tuple(messages), ) freshness_fail = _freshness_gate( config, repo_root, mode=effective_mode, runner=runner, emit=_emit, messages=messages, ) if freshness_fail is not None: return freshness_fail closeout_fail = _closeout_gate( config, repo_root, mode=effective_mode, runner=runner, emit=_emit, messages=messages, ) if closeout_fail is not None: return closeout_fail _emit("note: ok land-check never merges; use ok pr-land --authorized for wait-for-green land") return LandCheckResult( exit_code=0, landed=True, mode=effective_mode, ref="consumer_script", paths=(), dirty_paths=(), messages=tuple(messages), ) paths = ritual.require_paths if not paths: _emit("close_ritual.require_paths is empty — configure paths or consumer_verify_script") return LandCheckResult( exit_code=2, landed=False, mode=effective_mode, ref="", paths=(), dirty_paths=(), messages=tuple(messages), ) remote = config.vcs.git.remote main_branch = config.vcs.git.main_branch ref, reports, dirty_paths = compare_paths_to_main( repo_root, paths, remote=remote, main_branch=main_branch, ) all_match = all(r.get("match") for r in reports) and not dirty_paths if effective_mode == "prepare_pr": if dirty_paths: _emit("prepare_pr: dirty require_paths — commit before opening PR") for d in dirty_paths: _emit(f" dirty: {d}") _emit( 'Tier 3: use ok pr-land --authorized "…" to wait-for-green merge (never blind --auto)' ) return LandCheckResult( exit_code=1, landed=False, mode=effective_mode, ref=ref, paths=tuple(reports), dirty_paths=tuple(dirty_paths), messages=tuple(messages), ) freshness_fail = _freshness_gate( config, repo_root, mode=effective_mode, runner=runner, emit=_emit, messages=messages, ) if freshness_fail is not None: return freshness_fail _emit("prepare_pr: require_paths clean — push feature branch and open PR") _emit( 'Tier 3: use ok pr-land --authorized "…" to wait-for-green merge (never blind --auto)' ) return LandCheckResult( exit_code=0, landed=False, mode=effective_mode, ref=ref, paths=tuple(reports), dirty_paths=(), messages=tuple(messages), ) # verify_landed if not all_match: _emit(f"verify_landed: FAIL — paths do not match {ref}") for r in reports: if not r.get("match"): _emit(f" mismatch: {r['path']}") for d in dirty_paths: _emit(f" dirty: {d}") _emit( 'Tier 3: use ok pr-land --authorized "…" to wait-for-green merge (never blind --auto)' ) return LandCheckResult( exit_code=1, landed=False, mode=effective_mode, ref=ref, paths=tuple(reports), dirty_paths=tuple(dirty_paths), messages=tuple(messages), ) freshness_fail = _freshness_gate( config, repo_root, mode=effective_mode, runner=runner, emit=_emit, messages=messages, ) if freshness_fail is not None: return freshness_fail closeout_fail = _closeout_gate( config, repo_root, mode=effective_mode, runner=runner, emit=_emit, messages=messages, ) if closeout_fail is not None: return closeout_fail _emit(f"verify_landed: PASS — require_paths match {ref}") _emit("Tier 3 land complete once paths match; further merges use ok pr-land --authorized") return LandCheckResult( exit_code=0, landed=True, mode=effective_mode, ref=ref, paths=tuple(reports), dirty_paths=(), messages=tuple(messages), ) def _freshness_gate( config: OverseerConfig, repo_root: Path, *, mode: str, runner: CommandRunner | None, emit: Callable[[str], None], messages: list[str], ) -> LandCheckResult | None: """§GFG.6: when close_ritual is enabled, fail land-check on stale freshness (exit 2).""" active_runner = runner or SubprocessRunner() adapter = create_adapter(config, repo_root, runner=active_runner) report = check_governance_freshness( config, repo_root, adapter=adapter, runner=active_runner, ) if report.ok: return None emit(f"governance_freshness: {report.state} — {report.message}") if report.remediation: emit(f"governance_freshness-remediation: {report.remediation}") emit("land-check refused — freshness gate (never merges)") return LandCheckResult( exit_code=2, landed=False, mode=mode, ref="", paths=(), dirty_paths=(), messages=tuple(messages), ) def _closeout_gate( config: OverseerConfig, repo_root: Path, *, mode: str, runner: CommandRunner | None, emit: Callable[[str], None], messages: list[str], ) -> LandCheckResult | None: """§PMHF.6.2: ``landed=True`` only when closeout is ``complete``/``not_applicable``. ``land_a_in_progress``, ``post_merge_incomplete``, ``land_b_in_progress``, and ``unreadable`` all refuse landed (exit 2) with remediation — never merges. """ active_runner = runner or SubprocessRunner() report = check_land_closeout( config, repo_root, runner=active_runner, probe_merged_pr=config.vcs.regime != "muse-only", ) if report.state in {"complete", "not_applicable"}: return None emit(f"land_closeout: {report.state} — {report.message}") if report.remediation: emit(f"land_closeout-remediation: {report.remediation}") emit("land-check refused — land closeout incomplete (never merges)") return LandCheckResult( exit_code=2, landed=False, mode=mode, ref="", paths=(), dirty_paths=(), messages=tuple(messages), )