From 1bf1376a808c76321474083ef6660502ef0a806c Mon Sep 17 00:00:00 2001 From: Christian Nennemann Date: Sat, 4 Apr 2026 18:39:06 +0200 Subject: [PATCH] feat: implement archeflow-review.sh for Guardian-only diff review Standalone bash script that extracts git diffs for af-review without PDCA orchestration. Supports --branch, --commit, and uncommitted modes. Reports stats (files/lines changed) to stderr, diff to stdout. --- lib/archeflow-review.sh | 197 ++++++++++++++++++++++++++++++++++++++++ skills/review/SKILL.md | 27 +++--- 2 files changed, 213 insertions(+), 11 deletions(-) create mode 100755 lib/archeflow-review.sh diff --git a/lib/archeflow-review.sh b/lib/archeflow-review.sh new file mode 100755 index 0000000..c1d5f73 --- /dev/null +++ b/lib/archeflow-review.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env bash +# archeflow-review.sh — Get a git diff for Guardian review, with stats. +# +# Standalone diff helper for af-review. No PDCA orchestration — just extracts +# the right diff and reports stats so the Claude Code agent can feed it to +# Guardian (or other reviewers). +# +# Usage: +# archeflow-review.sh # Uncommitted changes (staged + unstaged) +# archeflow-review.sh --branch feat/batch-api # Branch diff vs main +# archeflow-review.sh --commit HEAD~3..HEAD # Commit range +# archeflow-review.sh --base develop # Override base branch (default: main) +# archeflow-review.sh --stat-only # Only print stats, no diff output +# +# Output: +# Prints the diff to stdout. Stats go to stderr so they don't pollute the diff. +# Exit code 0 if diff is non-empty, 1 if empty (nothing to review). + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Globals +# --------------------------------------------------------------------------- + +BASE_BRANCH="main" +MODE="uncommitted" # uncommitted | branch | commit +TARGET="" +STAT_ONLY="false" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +die() { + echo "[af-review] ERROR: $*" >&2 + exit 1 +} + +info() { + echo "[af-review] $*" >&2 +} + +# Print diff stats (files changed, insertions, deletions) to stderr. +print_stats() { + local diff_text="$1" + + local files_changed lines_added lines_removed total_lines + files_changed=$(echo "$diff_text" | grep -c '^diff --git' || true) + lines_added=$(echo "$diff_text" | grep -c '^+[^+]' || true) + lines_removed=$(echo "$diff_text" | grep -c '^-[^-]' || true) + total_lines=$(echo "$diff_text" | wc -l | tr -d ' ') + + info "--- Review Stats ---" + info "Files changed: ${files_changed}" + info "Lines added: +${lines_added}" + info "Lines removed: -${lines_removed}" + info "Diff size: ${total_lines} lines" + + if [[ "$total_lines" -gt 500 ]]; then + info "Warning: large diff (>500 lines). Consider reviewing per-file." + fi +} + +# Detect the default base branch (main or master). +detect_base_branch() { + if git show-ref --verify --quiet "refs/heads/main" 2>/dev/null; then + echo "main" + elif git show-ref --verify --quiet "refs/heads/master" 2>/dev/null; then + echo "master" + else + echo "main" + fi +} + +# --------------------------------------------------------------------------- +# Argument parsing +# --------------------------------------------------------------------------- + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --branch) + MODE="branch" + TARGET="${2:?Missing branch name after --branch}" + shift 2 + ;; + --commit) + MODE="commit" + TARGET="${2:?Missing commit range after --commit}" + shift 2 + ;; + --base) + BASE_BRANCH="${2:?Missing base branch after --base}" + shift 2 + ;; + --stat-only) + STAT_ONLY="true" + shift + ;; + -h|--help) + echo "Usage: $0 [--branch ] [--commit ] [--base ] [--stat-only]" + echo "" + echo " (no args) Review uncommitted changes (staged + unstaged)" + echo " --branch Review branch diff against base (default: main)" + echo " --commit Review a commit range (e.g. HEAD~3..HEAD)" + echo " --base Override base branch (default: auto-detect main/master)" + echo " --stat-only Print stats only, no diff output" + exit 0 + ;; + *) + die "Unknown argument: $1. Use --help for usage." + ;; + esac + done +} + +# --------------------------------------------------------------------------- +# Diff extraction +# --------------------------------------------------------------------------- + +get_diff() { + local diff_text="" + + case "$MODE" in + uncommitted) + # Combine staged and unstaged changes against HEAD + diff_text=$(git diff HEAD 2>/dev/null || true) + if [[ -z "$diff_text" ]]; then + # Maybe everything is staged, try just staged + diff_text=$(git diff --cached 2>/dev/null || true) + fi + ;; + branch) + # Verify target branch exists + if ! git show-ref --verify --quiet "refs/heads/${TARGET}" 2>/dev/null; then + # Maybe it's a remote branch + if ! git rev-parse --verify "${TARGET}" &>/dev/null; then + die "Branch '${TARGET}' not found." + fi + fi + diff_text=$(git diff "${BASE_BRANCH}...${TARGET}" 2>/dev/null || true) + ;; + commit) + # Validate commit range resolves + if ! git rev-parse "${TARGET}" &>/dev/null 2>&1; then + die "Invalid commit range: '${TARGET}'" + fi + diff_text=$(git diff "${TARGET}" 2>/dev/null || true) + ;; + esac + + echo "$diff_text" +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +main() { + # Verify we're in a git repo + if ! git rev-parse --is-inside-work-tree &>/dev/null; then + die "Not inside a git repository." + fi + + parse_args "$@" + + # Auto-detect base branch if not overridden + if [[ "$BASE_BRANCH" == "main" ]]; then + BASE_BRANCH=$(detect_base_branch) + fi + + # Describe what we're reviewing + case "$MODE" in + uncommitted) info "Reviewing: uncommitted changes vs HEAD" ;; + branch) info "Reviewing: branch '${TARGET}' vs '${BASE_BRANCH}'" ;; + commit) info "Reviewing: commit range '${TARGET}'" ;; + esac + + local diff_text + diff_text=$(get_diff) + + # Validate non-empty + if [[ -z "$diff_text" ]]; then + info "No changes found. Nothing to review." + exit 1 + fi + + # Print stats to stderr + print_stats "$diff_text" + + # Output the diff to stdout (unless stat-only) + if [[ "$STAT_ONLY" != "true" ]]; then + echo "$diff_text" + fi +} + +main "$@" diff --git a/skills/review/SKILL.md b/skills/review/SKILL.md index 5777d26..d78253d 100644 --- a/skills/review/SKILL.md +++ b/skills/review/SKILL.md @@ -37,23 +37,28 @@ af-review --evidence # Enable evidence-gating (stricter) ### Step 1: Get the Diff -```bash -# Uncommitted changes -DIFF=$(git diff HEAD) +Use `lib/archeflow-review.sh` to extract the diff and stats: -# Branch diff -DIFF=$(git diff main...HEAD) +```bash +# Uncommitted changes (default) +DIFF=$(bash lib/archeflow-review.sh) + +# Branch diff against main +DIFF=$(bash lib/archeflow-review.sh --branch feat/batch-api) # Commit range -DIFF=$(git diff HEAD~3..HEAD) +DIFF=$(bash lib/archeflow-review.sh --commit HEAD~3..HEAD) -# If diff is too large (>500 lines), split by file -if [[ $(echo "$DIFF" | wc -l) -gt 500 ]]; then - # Review per-file to keep context focused - FILES=$(git diff --name-only HEAD) -fi +# Override base branch +DIFF=$(bash lib/archeflow-review.sh --branch feat/x --base develop) + +# Stats only (no diff output) +bash lib/archeflow-review.sh --stat-only ``` +The script prints the diff to stdout and stats to stderr. It exits 1 if the diff +is empty (nothing to review). For large diffs (>500 lines), it warns on stderr. + ### Step 2: Spawn Reviewers Default: Guardian only (fastest, highest ROI).