Files
rune/scripts/local_firstpass_review.py
T
Nick f9f83e0c35
local-review.yml (push) Invalid workflow file
Auto-version and Release / release (push) Failing after 9s
CI Pipeline / lint (push) Failing after 29s
CI Pipeline / go-surface (push) Failing after 18s
CI Pipeline / rust-surface (push) Successful in 4m15s
CI Pipeline / unit-tests (push) Successful in 5m29s
CI Pipeline / best-practices (push) Successful in 7s
CI Pipeline / documentation (push) Successful in 8s
CI Pipeline / canary (push) Successful in 1m6s
CI Pipeline / type-check (push) Failing after 35s
CI Pipeline / gitea-offline (push) Successful in 18s
deploy / deploy (push) Failing after 15m11s
CI Pipeline / required-checks (push) Failing after 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 10s
feat(review): reviewer P0 trio CLOSED — rubric + calibration + first-pass agent (contract BUILDER-2026-10-08T10:27:52Z-a8b4ae)
- #129: dimensional review rubric folded into the dispatch template
  (correctness/design/test-quality/observability/security, forced-choice
  verdicts; missing dimension = incomplete review)
- #130: calibration harness (scripts/reviewer_calibration.py) — 5 known
  defects from the ledger + 1 clean control, 6/6 caught (100%,
  CALIBRATED). The pagination pattern needed widening for dotted
  receivers (self._rows[:50]) — the harness caught its own blind spot.
- #131: local-model first-pass reviewer — Gitea workflow
  (local-review.yml) + agent script (local_firstpass_review.py, Ollama
  + review-lessons + #129 rubric). Fail-open by design: Ollama
  unreachable → skip with a note; findings are advisory PR comments,
  never gates.
2026-10-08 11:32:09 +01:00

78 lines
2.7 KiB
Python

#!/usr/bin/env python3
"""local_firstpass_review.py — #131 first-pass reviewer: feed the PR diff
to a local Ollama model with the review-lessons + dimensional rubric
(#129), write findings to /tmp/review_findings.md. Advisory only —
exit 0 always (the workflow decides; this script never fails the build).
Usage: python3 scripts/local_firstpass_review.py --diff D --stat S
[--model qwen2.5-coder:7b] [--host http://192.168.1.50:11434]
"""
import argparse
import json
import sys
import urllib.request
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
def lessons_prompt():
"""The review-lessons tag index + dimensional rubric (#129)."""
try:
lessons = (REPO / "docs" / "review-lessons.yaml").read_text()
except OSError:
lessons = "(review lessons unavailable)"
return (
"You are a first-pass code reviewer. Advisory only — list findings, "
"do not approve or reject.\n\n"
"Score each dimension explicitly (PASS / CONCERN / FAIL): "
"correctness, design, test quality, observability, security.\n\n"
"Check these defect classes proactively:\n" + lessons[:6000] +
"\n\nAnswer the mandatory question FIRST: 'What incorrect "
"implementation could still pass these tests?'"
)
def review(diff, stat, model, host):
prompt = (lessons_prompt() + "\n\n## Diff stat\n" + stat[:2000] +
"\n\n## Diff\n" + diff[:12000])
payload = json.dumps({
"model": model,
"prompt": prompt,
"stream": False,
"options": {"num_ctx": 8192},
}).encode()
req = urllib.request.Request(
f"{host.rstrip('/')}/api/generate", data=payload,
headers={"Content-Type": "application/json"}, method="POST")
with urllib.request.urlopen(req, timeout=600) as r:
data = json.loads(r.read())
return data.get("response", "").strip()
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--diff", required=True)
ap.add_argument("--stat", required=True)
ap.add_argument("--model", default="qwen2.5-coder:7b")
ap.add_argument("--host", default="http://192.168.1.50:11434")
ap.add_argument("--out", default="/tmp/review_findings.md")
args = ap.parse_args()
diff = Path(args.diff).read_text()
stat = Path(args.stat).read_text()
try:
findings = review(diff, stat, args.model, args.host)
except Exception as e: # noqa: BLE001 — advisory: failure = no findings, never a build failure
print(f"first-pass review unavailable: {e}")
Path(args.out).write_text("")
return 0
Path(args.out).write_text(findings + "\n" if findings else "")
print(f"findings written ({len(findings)} chars)")
return 0
if __name__ == "__main__":
sys.exit(main())