Files
settled-reach/tooling/clerk-review
T
jpmschweitzerandClaude Opus 4.6 e6a557e8e7 feat(meta): replace sprint workflow with kanban + milestones (D-221)
Sprint-based workflow (38 sprints) replaced by kanban + milestones.
Milestones are many-to-many with tickets and can block each other.

New: /whats-next skill (dependency-driven batch selection with Si
refinement review), /pr-process skill (renamed from pr-push, adds
review comment pickup), clerk agent + pre-push hook for D-record
consistency checks.

Deleted: sprint CLI, sprint-start/sprint-plan/sprint-status skills,
team-scoped file restrictions. Si rewritten as refinement manager.
All 19 agent briefings updated from stale PROJECT_STATE.md reference
to live ticket milestone queries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-03 20:11:00 +02:00

151 lines
4.4 KiB
Python
Executable File

#!/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())