From 86797abb9987d463bd31cc9887da7ba9bb689d20 Mon Sep 17 00:00:00 2001 From: Travis Herbranson Date: Mon, 27 Apr 2026 20:32:56 -0400 Subject: [PATCH] Initial a-review skill: model-agnostic independent code review Skill that sends git diffs to an independent AI model (Gemini CLI, Ollama) for security, bug, performance, and convention review. Assembles a context packet from CLAUDE.md + session notes + expanded diff so the reviewer evaluates intent vs. implementation. Includes context assembler script, structured review prompt template, and review-ready CLAUDE.md with key patterns documented. --- .gitignore | 3 + CLAUDE.md | 67 ++++++++++++ a-review/SKILL.md | 141 ++++++++++++++++++++++++ a-review/references/review-prompt.md | 21 ++++ a-review/scripts/assemble_context.py | 155 +++++++++++++++++++++++++++ sessions/a-review-skill.md | 70 ++++++++++++ 6 files changed, 457 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 a-review/SKILL.md create mode 100644 a-review/references/review-prompt.md create mode 100644 a-review/scripts/assemble_context.py create mode 100644 sessions/a-review-skill.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..996cdd0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.skill +__pycache__/ +.DS_Store diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1a5699c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,67 @@ +# a-review — Agent Code Review Skill + +Claude Code skill that sends code changes to an independent AI model for +review. Model-agnostic — supports Gemini CLI, Ollama, or any text-in/text-out +backend. + +## Architecture + +``` +a-review-skill/ + a-review/ + SKILL.md — Skill definition (triggers, workflow) + references/ + review-prompt.md — Prompt template sent to the reviewer + scripts/ + assemble_context.py — Builds context packet from project files +``` + +## Key Patterns + +**Context packet.** The reviewer gets four pieces of information assembled +by `scripts/assemble_context.py`: CLAUDE.md (architecture + conventions), +latest session notes (intent), expanded diff (changes in surrounding context), +and diff stat (scope summary). This mirrors how a human reviewer works — +read the PR description, understand conventions, then evaluate the diff. + +**Model-agnostic backend.** The skill doesn't call the reviewer directly. +It builds the context packet and pipes it to whatever backend is configured. +Gemini CLI uses `gemini -p` with `--approval-mode plan` (read-only). Ollama +uses the HTTP generate API. New backends only need a way to accept text in +and return text out. + +**Read-only reviewer.** The reviewer model runs in plan/read-only mode. It +cannot modify files. Its job is to produce findings, not fixes. The authoring +agent (Claude Code) decides what to act on. + +**Complements zero-check, doesn't replace it.** zero-check is deterministic +(lint, SAST, tests, deps). a-review is subjective (security patterns, logic +gaps, convention violations, intent-vs-implementation). They run in sequence: +zero-check first, a-review after. + +## Development + +```bash +# Test the context assembler +cd /path/to/any/git/project +python /path/to/a-review-skill/a-review/scripts/assemble_context.py + +# Run a review manually with Gemini +python scripts/assemble_context.py | \ + GEMINI_CLI_TRUST_WORKSPACE=true gemini -p "$(cat references/review-prompt.md)" \ + --approval-mode plan + +# Package as .skill file +cd a-review && zip -r ../a-review.skill SKILL.md references/ scripts/ +``` + +## Session Notes + +After each coding session, append a session entry to +`sessions/a-review-skill.md` in this project folder. +See that file for the format convention and existing entries. + +Granularity: one entry per logical change. A new function, a new route, +a new template, a new config setting, or a bug fix each get their own +entry. Don't combine multiple changes into one entry. If it would be a +separate item in a code review, it's a separate entry here. diff --git a/a-review/SKILL.md b/a-review/SKILL.md new file mode 100644 index 0000000..7dc1ae1 --- /dev/null +++ b/a-review/SKILL.md @@ -0,0 +1,141 @@ +--- +name: a-review +description: > + Run an independent AI code review after coding sessions. Sends the git diff, + CLAUDE.md, and session notes to a second model (Gemini CLI, Ollama, or any + text-in/text-out backend) for security, bug, performance, and convention + review. Use this skill after completing code changes, after zero-check passes, + when the user asks for a review, or when you want a second opinion on code + quality. Also triggers on "review my changes", "get a second opinion", + "run a-review", or "check this with gemini/ollama". +--- + +# a-review — Agent Code Review + +Send code changes to an independent AI for review. The reviewer gets project +context (architecture, conventions, intent) alongside the diff so it can +evaluate what was done against what was intended. + +This is not a linter or SAST tool — that's what zero-check does. This is a +subjective second opinion from a different model: "did you miss something +a linter wouldn't catch?" + +## When to Run + +- After a coding session with substantive changes +- After zero-check passes (review complements validation) +- When the user asks for a review or second opinion +- Before committing changes to a shared branch + +Do not run on trivial changes (typo fixes, comment edits, formatting-only). + +## Workflow + +### Step 1: Collect the diff + +```bash +# Default: changes since last commit +git diff HEAD~1 + +# Or staged changes if nothing committed yet +git diff --cached + +# Expanded context for reviewer (10 lines surrounding each change) +git diff -U10 HEAD~1 +``` + +Use `HEAD~1` by default. If the user says "review staged changes" or +there are no commits yet, use `--cached`. + +### Step 2: Assemble the context packet + +The reviewer needs four things: + +1. **CLAUDE.md** — read from the project root. This gives the reviewer + the architecture, key patterns, and conventions to review against. + If CLAUDE.md doesn't exist, skip this and note it in the output. + +2. **Session notes** — read the latest session entry from + `sessions/.md`. This tells the reviewer what was + intended, so it can compare intent vs. implementation. + If no session notes exist, skip this. + +3. **Expanded diff** — `git diff -U10 HEAD~1` so the reviewer sees + each change in its surrounding context (the function it lives in, + the class it belongs to). + +4. **Diff stat** — `git diff --stat HEAD~1` for a quick summary of + what files changed and how much. + +Run the assembler script to combine these: + +```bash +python scripts/assemble_context.py +``` + +This reads the project, builds the context packet, and prints it to +stdout. Pipe it to the reviewer. + +### Step 3: Send to the reviewer + +Check for a `.a-review.yml` config file in the project root. If it +exists, use the configured backend. If not, default to Gemini CLI. + +**Gemini CLI:** +```bash +python scripts/assemble_context.py | \ + GEMINI_CLI_TRUST_WORKSPACE=true \ + gemini -p "$(cat references/review-prompt.md)" \ + --approval-mode plan +``` + +**Ollama:** +```bash +CONTEXT=$(python scripts/assemble_context.py) +curl -s http://localhost:11434/api/generate \ + -d "{\"model\": \"codellama\", \"prompt\": \"$(cat references/review-prompt.md)\n\n$CONTEXT\", \"stream\": false}" \ + | jq -r '.response' +``` + +The `--approval-mode plan` flag for Gemini is important — it keeps +the reviewer in read-only mode so it can't modify files. + +### Step 4: Save the output + +Save the review to `reviews/a-review-YYYY-MM-DD.md` in the project. +Create the `reviews/` directory if it doesn't exist. + +If a review already exists for today, suffix with `-2`, `-3`, etc. + +Add a header to the saved file: + +```markdown +# a-review — YYYY-MM-DD + +**Model:** +**Diff:** HEAD~1 (N files, M lines changed) +**Context:** CLAUDE.md + session notes included + +--- + + +``` + +### Step 5: Surface findings + +Read the saved review and summarize the findings for the user. Group +by severity if the reviewer used severity labels. Highlight anything +marked critical or security-related. + +If the reviewer found actionable issues, offer to fix them. + +## Reference Files + +| File | Purpose | +|------|---------| +| `references/review-prompt.md` | The prompt template sent to the reviewer | +| `scripts/assemble_context.py` | Builds the context packet from project files | + +Load `references/review-prompt.md` when building the review command. +Load `scripts/assemble_context.py` when you need to understand or +modify the context assembly logic. diff --git a/a-review/references/review-prompt.md b/a-review/references/review-prompt.md new file mode 100644 index 0000000..1efdf99 --- /dev/null +++ b/a-review/references/review-prompt.md @@ -0,0 +1,21 @@ +You are reviewing code changes to a software project. You will receive a context packet containing the project's architecture documentation (CLAUDE.md), the developer's session notes describing what they intended, and the actual diff of what changed. + +Review the changes for: + +1. **Security vulnerabilities** — injection, path traversal, auth bypass, data exposure, hardcoded secrets, unsafe deserialization +2. **Bugs and logic errors** — null/None handling, off-by-one, race conditions, missing error handling, unreachable code, incorrect conditionals +3. **Performance issues** — unnecessary loops, missing caching, repeated I/O, N+1 queries, unbounded data structures +4. **Convention violations** — patterns defined in CLAUDE.md that the changes don't follow (e.g., "all writes go through the cache layer" but a new write bypasses it) +5. **Intent vs. implementation** — compare what the session notes say was intended against what the diff actually does. Flag gaps, unfinished work, or deviations from stated intent. + +For each finding: +- State the severity: [CRITICAL], [WARNING], or [INFO] +- Name the file and approximate location +- Explain what the issue is and why it matters +- Suggest a fix if one is obvious + +Skip stylistic preferences (naming conventions, comment style, blank lines) unless they violate documented conventions in CLAUDE.md. Focus on correctness and safety. + +Output your findings as structured markdown with severity headers. End with a brief summary of overall code quality and any patterns you noticed. + +If the changes look clean and you have no findings, say so explicitly — don't manufacture issues to fill space. diff --git a/a-review/scripts/assemble_context.py b/a-review/scripts/assemble_context.py new file mode 100644 index 0000000..9a4d30b --- /dev/null +++ b/a-review/scripts/assemble_context.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""Assemble the context packet for a-review. + +Reads CLAUDE.md, latest session notes, expanded diff, and diff stat +from the current git repository. Outputs a structured context document +to stdout for piping to the reviewer model. + +Usage: + python scripts/assemble_context.py # diff against HEAD~1 + python scripts/assemble_context.py --staged # diff of staged changes + python scripts/assemble_context.py --context 15 # 15 lines of context +""" + +from __future__ import annotations + +import argparse +import subprocess +import sys +from pathlib import Path + + +def _run(cmd: list[str], cwd: Path | None = None) -> str: + """Run a command and return stdout, or empty string on failure.""" + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + cwd=cwd, + check=False, + ) + return result.stdout.strip() + except FileNotFoundError: + return "" + + +def _find_project_root() -> Path: + """Find the git repository root.""" + root = _run(["git", "rev-parse", "--show-toplevel"]) + if root: + return Path(root) + return Path.cwd() + + +def _read_file_if_exists(path: Path) -> str: + """Read a file if it exists, return empty string otherwise.""" + if path.exists(): + return path.read_text() + return "" + + +def _find_latest_session_entry(project_root: Path) -> str: + """Find and return the latest session notes entry.""" + sessions_dir = project_root / "sessions" + if not sessions_dir.exists(): + return "" + + # Find session note files + session_files = sorted(sessions_dir.glob("*.md"), key=lambda f: f.stat().st_mtime, reverse=True) + if not session_files: + return "" + + content = session_files[0].read_text() + + # Extract the last ## Session entry + sections = content.split("\n## Session") + if len(sections) < 2: + return content # Return full file if no session headers + + last_section = "## Session" + sections[-1] + return last_section.strip() + + +def assemble(staged: bool = False, context_lines: int = 10) -> str: + """Assemble the full context packet.""" + root = _find_project_root() + parts: list[str] = [] + + # Header + project_name = root.name + parts.append(f"# Code Review Context: {project_name}") + parts.append("") + + # CLAUDE.md + claude_md = _read_file_if_exists(root / "CLAUDE.md") + if claude_md: + parts.append("## Project Documentation (CLAUDE.md)") + parts.append("") + parts.append(claude_md) + parts.append("") + else: + parts.append("## Project Documentation") + parts.append("No CLAUDE.md found in project root.") + parts.append("") + + # Session notes + session = _find_latest_session_entry(root) + if session: + parts.append("## Latest Session Notes") + parts.append("") + parts.append(session) + parts.append("") + else: + parts.append("## Session Notes") + parts.append("No session notes found.") + parts.append("") + + # Diff stat + diff_ref = ["--cached"] if staged else ["HEAD~1"] + stat = _run(["git", "diff", "--stat", *diff_ref], cwd=root) + if stat: + parts.append("## Diff Summary") + parts.append("```") + parts.append(stat) + parts.append("```") + parts.append("") + + # Expanded diff (with surrounding context) + expanded = _run( + ["git", "diff", f"-U{context_lines}", *diff_ref], + cwd=root, + ) + if expanded: + parts.append(f"## Expanded Diff ({context_lines} lines context)") + parts.append("```diff") + parts.append(expanded) + parts.append("```") + parts.append("") + + # Standard diff + diff = _run(["git", "diff", *diff_ref], cwd=root) + if diff: + parts.append("## Diff") + parts.append("```diff") + parts.append(diff) + parts.append("```") + elif not expanded: + parts.append("## Diff") + parts.append("No changes found.") + + return "\n".join(parts) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Assemble a-review context packet") + parser.add_argument("--staged", action="store_true", help="Review staged changes instead of HEAD~1") + parser.add_argument("--context", type=int, default=10, help="Lines of surrounding context (default: 10)") + args = parser.parse_args() + + output = assemble(staged=args.staged, context_lines=args.context) + sys.stdout.write(output) + + +if __name__ == "__main__": + main() diff --git a/sessions/a-review-skill.md b/sessions/a-review-skill.md new file mode 100644 index 0000000..a202c6f --- /dev/null +++ b/sessions/a-review-skill.md @@ -0,0 +1,70 @@ +# a-review Skill — Session Notes + +Session log for the a-review-skill project. Each session is a dated +heading with a list of changes. Each change has a label, narrative, and +four structured fields (What, Why, How, Touches). + +--- + +## Session — 2026-04-27 + +### SKILL.md Definition +Wrote the skill definition with trigger description, five-step workflow +(collect diff, assemble context, send to reviewer, save output, surface +findings), and reference file pointers. Triggers automatically after +coding sessions and on manual requests. Supports Gemini CLI and Ollama +backends with examples for both. + +- **What:** SKILL.md with model-agnostic review workflow +- **Why:** Need a reusable skill that runs independent code review from + any AI backend, not just Gemini CLI one-offs +- **How:** Five-step workflow: git diff, assemble_context.py, pipe to + backend with review-prompt.md, save to reviews/ folder, summarize + findings. Backend selection via .a-review.yml config or default to + Gemini CLI. +- **Touches:** a-review/SKILL.md (new) + +### Review Prompt Template +Wrote the structured prompt sent to the reviewer model. Covers five +review areas (security, bugs, performance, conventions, intent-vs- +implementation) with severity levels and output format guidance. +Explicitly tells the reviewer to skip stylistic preferences and not +manufacture findings. + +- **What:** Review prompt template at references/review-prompt.md +- **Why:** Structured prompt produces consistent, actionable findings + across different models +- **How:** Markdown document with numbered review areas, severity + definitions ([CRITICAL], [WARNING], [INFO]), and output format + template. Includes instruction to compare session notes intent + against diff implementation. +- **Touches:** a-review/references/review-prompt.md (new) + +### Context Packet Assembler +Python script that collects CLAUDE.md, latest session notes, expanded +diff (with surrounding context), and diff stat from the current git repo. +Outputs a structured markdown document to stdout for piping to the +reviewer. + +- **What:** `assemble_context.py` CLI script with --staged and --context options +- **Why:** Reviewer needs project context alongside the diff to produce + meaningful findings — a naked diff review misses convention violations + and intent mismatches +- **How:** Uses subprocess to call git commands. Finds project root via + `git rev-parse --show-toplevel`. Reads CLAUDE.md and latest session + entry from sessions/ directory. Builds markdown document with labeled + sections. Supports --staged for uncommitted changes and --context N + for surrounding line count (default 10). +- **Touches:** a-review/scripts/assemble_context.py (new) + +### Project Documentation +Added CLAUDE.md with key patterns documenting the context packet approach, +model-agnostic backend design, read-only reviewer constraint, and the +complementary relationship with zero-check. + +- **What:** CLAUDE.md with architecture and key patterns +- **Why:** Future sessions need to understand design decisions, especially + the separation between context assembly, prompt template, and backend +- **How:** Follows the review-ready CLAUDE.md convention with file tree, + key patterns, dev workflow, and session notes pointer. +- **Touches:** CLAUDE.md (new)