Files
ai-workflow-course/modules/24-assistive-agents/lab/reviewer.py
T
claude 389ac2e460 style(no-slop): remove every em-dash + banned words across all modules + capstone
Apply the no-ai-slop standard (now binding in AGENTS.md): the em-dash character is
banned outright (restructured, not blind-replaced), plus the banned word/phrase
list (delve, leverage, robust, seamless, truly, unlock, etc.). 0 em-dashes remain
in modules + capstone; the only "robust" left is the planted M10 ai-change.patch
trap. Module H1 titles use a colon separator.

All deliberate teaching devices preserved; labs compile/parse (py/sh/yaml/json);
no junk. AGENTS.md updated with the hard no-slop rules.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfzV5QvtPDz8LJS3Pu5VLT
2026-06-22 23:21:09 -04:00

117 lines
4.4 KiB
Python

"""Assistive AI reviewer: local simulation of a PR-reviewer bot.
This stands in for a forge-native reviewer (an app/bot triggered when a PR opens, running on a
runner from Module 19) without needing any hosted account. It does the two deterministic halves of
the job and leaves the one judgment call (what actually happens to the PR) to you.
python reviewer.py prompt # assemble the prompt: rubric + diff, for the agent to review
python reviewer.py apply ai-review.sample.json # ingest the agent's JSON, render it, gate it
The point of this module: the agent produces comments and a recommendation. It never approves,
never requests-changes-as-a-gate, never merges. The `apply` step ends at a HUMAN DECISION, every
time. Stdlib only, no pip install.
"""
import argparse
import json
import sys
from pathlib import Path
HERE = Path(__file__).parent
def load_json_response(path: Path):
"""Parse the JSON the AI returned.
Chat assistants very often wrap their output in a ```json ... ``` code fence (or add a stray
line of text) even when told to "return only the JSON", so a strict json.loads on the raw paste
fails on the most likely real output. Try a strict parse first; if that fails, fall back to the
outermost { ... } block, which survives a code fence or surrounding text. Stdlib only."""
raw = path.read_text()
try:
return json.loads(raw)
except json.JSONDecodeError:
start, end = raw.find("{"), raw.rfind("}")
if start != -1 and end > start:
return json.loads(raw[start : end + 1])
raise
PROMPT_HEADER = """\
You are an assistive code reviewer. Follow the rubric below exactly, then review the diff that
follows it. Return ONLY the JSON object the rubric specifies, with no extra text before or after.
================ REVIEW RUBRIC ================
{rubric}
================ DIFF UNDER REVIEW ============
{diff}
"""
def cmd_prompt(args: argparse.Namespace) -> int:
rubric = Path(args.rubric).read_text()
diff = Path(args.patch).read_text()
print(PROMPT_HEADER.format(rubric=rubric, diff=diff))
return 0
def cmd_apply(args: argparse.Namespace) -> int:
try:
review = load_json_response(Path(args.response))
except (json.JSONDecodeError, FileNotFoundError) as exc:
print(f"error: could not read a JSON review from {args.response}: {exc}")
return 1
summary = review.get("summary", "(no summary)")
recommendation = review.get("recommendation", "comment")
comments = review.get("comments", [])
print("=" * 70)
print("AI REVIEWER: first pass (advisory only)")
print("=" * 70)
print(f"\nSummary: {summary}\n")
if not comments:
print("No line comments.\n")
order = {"blocker": 0, "suggestion": 1, "nit": 2}
for c in sorted(comments, key=lambda c: order.get(c.get("severity", "nit"), 9)):
sev = c.get("severity", "nit").upper()
loc = f"{c.get('file', '?')}:{c.get('line', '?')}"
print(f" [{sev:10}] {loc}")
print(f" {c.get('comment', '')}\n")
print("-" * 70)
print(f"Agent's recommendation: {recommendation}")
print("-" * 70)
print(
"\nThis is the human decision gate. The agent did NOT merge, approve, or block.\n"
"It only commented. You decide what happens next:\n"
" - merge you read the comments, you disagree or they're addressed\n"
" - request changes you agree; push the fix on the branch and re-run\n"
" - dismiss the agent is wrong or noisy; ignore and move on\n"
"\nNothing in this repo changes until you act. That's the whole point of Module 24.\n"
)
return 0
def main(argv: list[str]) -> int:
parser = argparse.ArgumentParser(description=__doc__)
sub = parser.add_subparsers(dest="cmd", required=True)
p = sub.add_parser("prompt", help="assemble the review prompt for the agent to act on")
p.add_argument("--rubric", default=str(HERE / "review-rubric.md"))
p.add_argument("--patch", default=str(HERE / "feature.patch"))
p.set_defaults(func=cmd_prompt)
a = sub.add_parser("apply", help="ingest the AI's JSON review and render the decision gate")
a.add_argument("response", help="path to the JSON the AI returned")
a.set_defaults(func=cmd_apply)
args = parser.parse_args(argv)
return args.func(args)
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))