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.
This commit is contained in:
commit
86797abb99
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
*.skill
|
||||||
|
__pycache__/
|
||||||
|
.DS_Store
|
||||||
67
CLAUDE.md
Normal file
67
CLAUDE.md
Normal file
@ -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.
|
||||||
141
a-review/SKILL.md
Normal file
141
a-review/SKILL.md
Normal file
@ -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/<project-slug>.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:** <model name/version>
|
||||||
|
**Diff:** HEAD~1 (N files, M lines changed)
|
||||||
|
**Context:** CLAUDE.md + session notes included
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<reviewer output here>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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.
|
||||||
21
a-review/references/review-prompt.md
Normal file
21
a-review/references/review-prompt.md
Normal file
@ -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.
|
||||||
155
a-review/scripts/assemble_context.py
Normal file
155
a-review/scripts/assemble_context.py
Normal file
@ -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()
|
||||||
70
sessions/a-review-skill.md
Normal file
70
sessions/a-review-skill.md
Normal file
@ -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)
|
||||||
Loading…
Reference in New Issue
Block a user