#!/usr/bin/env python3 """ Clerk pre-push review — spawns the clerk agent to check D-record consistency, ticket drift, and decision contradictions against the current diff. Outputs exactly one word to stdout: APPROVED or REJECTED. Writes verbose findings to .cache/pre-push-review.md. Exit codes: 0 = APPROVED 1 = REJECTED (hard contradiction found) 2 = TIMEOUT (clerk didn't respond in time — treated as REJECTED) """ import os import subprocess import sys import tempfile from pathlib import Path REPO_ROOT = Path(subprocess.check_output( ["git", "rev-parse", "--show-toplevel"], text=True ).strip()) CACHE_DIR = REPO_ROOT / ".cache" FINDINGS_FILE = CACHE_DIR / "pre-push-review.md" TIMEOUT_SECONDS = 120 CLERK_PROMPT = """You are the CLERK, the institutional guardrail for The Settled Reach. You are reviewing a pre-push diff for D-record consistency, ticket drift, and decision contradictions. ## The diff {diff} ## Your task 1. Read `decisions/README.md` for the domain index. 2. For each changed file in the diff, check: - Does the change contradict any active D-record? (grep decisions/*.md for relevant keywords) - If code references a D/Q/R-ID, does that ID exist and is it active? - If commits reference ticket #NNN, does the implementation match the ticket description? 3. Surface any open Q-records relevant to changed files. ## Output format First, write your detailed findings. Then on the VERY LAST LINE of your response, output exactly one word — either APPROVED or REJECTED. REJECTED only for hard contradictions with active D-records. Everything else (drift, open Q-records, suggestions) is a finding but not a block. """ def get_diff(): """Get the diff that would be pushed.""" branch = subprocess.check_output( ["git", "branch", "--show-current"], text=True ).strip() for ref in [f"origin/{branch}", "origin/main"]: try: subprocess.check_output( ["git", "rev-parse", "--verify", ref], stderr=subprocess.DEVNULL, text=True ) return subprocess.check_output( ["git", "diff", f"{ref}...HEAD"], text=True ) except subprocess.CalledProcessError: continue return subprocess.check_output(["git", "diff", "HEAD~1"], text=True) def run_clerk(diff_text): """Spawn clerk via claude --print, return (verdict, findings).""" max_diff = 50000 if len(diff_text) > max_diff: diff_text = diff_text[:max_diff] + f"\n\n... (truncated, {len(diff_text)} total chars)" prompt = CLERK_PROMPT.format(diff=diff_text) with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f: f.write(prompt) prompt_file = f.name try: result = subprocess.run( [ "claude", "-p", "--model", "sonnet", "--max-turns", "3", ], input=prompt, capture_output=True, text=True, timeout=TIMEOUT_SECONDS, cwd=str(REPO_ROOT), ) output = result.stdout.strip() except subprocess.TimeoutExpired: return "TIMEOUT", "Clerk agent timed out after {} seconds.".format(TIMEOUT_SECONDS) finally: os.unlink(prompt_file) if not output: return "REJECTED", "Clerk agent produced no output." lines = output.strip().split("\n") last_line = lines[-1].strip().upper() if last_line == "APPROVED": verdict = "APPROVED" elif last_line == "REJECTED": verdict = "REJECTED" else: verdict = "REJECTED" output += "\n\n(No clear verdict on last line — defaulting to REJECTED)" return verdict, output def main(): CACHE_DIR.mkdir(parents=True, exist_ok=True) diff_text = get_diff() if not diff_text.strip(): print("APPROVED") FINDINGS_FILE.write_text("# Clerk Review\n\nNo diff to review.\n\nVerdict: APPROVED\n") return 0 print(" clerk: reviewing diff...", file=sys.stderr) verdict, findings = run_clerk(diff_text) FINDINGS_FILE.write_text(f"# Clerk Pre-Push Review\n\n{findings}\n") print(f" clerk: verdict — {verdict} (details: {FINDINGS_FILE})", file=sys.stderr) print(verdict) if verdict == "APPROVED": return 0 elif verdict == "TIMEOUT": return 2 else: return 1 if __name__ == "__main__": sys.exit(main())