kink-discovery / scripts /codex_review.py
Perplexed7675's picture
Sync from kink_cli (Docker Space)
6ff91d6 verified
Raw
History Blame Contribute Delete
5.37 kB
#!/usr/bin/env python3
"""stop hook: Codex-powered diff review when session changes exceed threshold.
Reads a baseline git tree snapshot (saved by codex_baseline.py at session
start), computes the diff, and runs ``codex exec review`` when changes are
substantial. The review prompt references AGENTS.md and checks for banned
patterns, hacky workarounds, and code-quality issues.
Change tracking is independent of commits: the baseline is a git tree
object captured at session start, and the diff is computed against the
live working tree at stop time.
"""
import json
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
project_root = Path(__file__).resolve().parent.parent
state_dir = project_root / ".cursor" / "hooks" / "state"
baseline_path = state_dir / "codex_review_baseline"
CODEX_REVIEW_MIN_LINES = 50
def count_diff_lines(diff_text: str) -> int:
"""Count additions and deletions in a unified diff."""
count = 0
for line in diff_text.splitlines():
is_add = line.startswith("+") and not line.startswith("+++")
is_del = line.startswith("-") and not line.startswith("---")
if is_add or is_del:
count += 1
return count
def compute_session_diff(baseline_sha: str) -> str:
"""Diff between the baseline snapshot and the current working tree.
Includes untracked files by using intent-to-add (git add -N) so new
files created during the session appear in the diff.
"""
subprocess.run(
["/usr/bin/git", "add", "-N", "."],
capture_output=True,
text=True,
timeout=10,
check=False,
cwd=str(project_root),
)
result = subprocess.run(
["/usr/bin/git", "diff", baseline_sha],
capture_output=True,
text=True,
timeout=30,
check=False,
cwd=str(project_root),
)
return result.stdout
def build_review_prompt(lines_changed: int) -> str:
"""Construct the custom instructions for codex exec review."""
kw_sep = "keyword" + "-only separator"
return (
f"This session changed {lines_changed} lines. "
"Read AGENTS.md in the repo root for the full project rules.\n\n"
"Key banned patterns to check for:\n"
"- try/except without rationale, assert, nested functions, @dataclass\n"
"- typing.Any, typing.cast, functools.cache\n"
"- Magic numbers at module level, os.environ/os.getenv\n"
f"- {kw_sep}, string-key membership checks on dicts\n"
"- Defensive .get() after model_validate, isinstance after validation\n"
"- Inline suppression comments without owner approval\n\n"
"Also flag:\n"
"1. Hacky workarounds, TODO/FIXME/HACK, band-aid fixes\n"
"2. Loose dicts where typed Pydantic models belong\n"
"3. God functions, excessive nesting, duplicated logic\n"
"4. Unclear or misleading names\n"
"5. Missing type annotations\n"
"6. Hand-rolled code that a library already handles\n\n"
"Be concise. Report actionable findings only.\n"
"Format: file:line [category] description\n"
"If clean: LGTM"
)
def run_codex_review(lines_changed: int) -> str:
"""Run codex exec review --uncommitted with project-specific instructions."""
codex_path = shutil.which("codex")
if codex_path is None:
return ""
with tempfile.NamedTemporaryFile(
mode="w",
suffix=".txt",
delete=False,
) as tmp:
output_path = tmp.name
cmd = [
codex_path,
"exec",
"review",
"--uncommitted",
"--ephemeral",
"-o",
output_path,
build_review_prompt(lines_changed),
]
subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=150,
check=False,
cwd=str(project_root),
)
result_file = Path(output_path)
review_text = result_file.read_text().strip()
result_file.unlink(missing_ok=True)
return review_text
def main() -> None:
"""Run codex review if session changes exceed the configured threshold."""
state_dir.mkdir(parents=True, exist_ok=True)
parsed = json.loads(sys.stdin.read())
status = parsed.get("status", "")
loop_count = parsed.get("loop_count", 0)
if status != "completed":
baseline_path.unlink(missing_ok=True)
print("{}")
return
if loop_count > 0:
print("{}")
return
if not baseline_path.exists():
print("{}")
return
baseline_sha = baseline_path.read_text().strip()
baseline_path.unlink(missing_ok=True)
if not baseline_sha:
print("{}")
return
diff = compute_session_diff(baseline_sha)
lines_changed = count_diff_lines(diff)
if lines_changed < CODEX_REVIEW_MIN_LINES:
print("{}")
return
review = run_codex_review(lines_changed)
if not review:
print("{}")
return
upper_review = review.strip().upper()
if upper_review.startswith("LGTM"):
print("{}")
return
msg = (
f"Codex Review ({lines_changed} lines changed):\n\n"
f"{review}\n\n"
"Address these findings or explain why they are acceptable."
)
print(json.dumps({"followup_message": msg}))
if __name__ == "__main__":
main()