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.
156 lines
4.4 KiB
Python
156 lines
4.4 KiB
Python
#!/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()
|