Skip to content

Commit b75138c

Browse files
authored
Merge pull request #232 from igerber/local-review-updates
Enhance local AI review with full-file context, delta-diff re-review, and cost visibility
2 parents 937dae6 + ac93d46 commit b75138c

4 files changed

Lines changed: 2546 additions & 48 deletions

File tree

.claude/commands/ai-review-local.md

Lines changed: 212 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
description: Run AI code review locally using OpenAI API before opening a PR
3-
argument-hint: "[--full-registry] [--model <model>] [--dry-run]"
3+
argument-hint: "[--context minimal|standard|deep] [--include-files <files>] [--token-budget <n>] [--force-fresh] [--full-registry] [--model <model>] [--dry-run]"
44
---
55

66
# Local AI Code Review
@@ -12,6 +12,15 @@ pre-PR use. Designed for iterative review/revision cycles before submitting a PR
1212
## Arguments
1313

1414
`$ARGUMENTS` may contain optional flags:
15+
- `--context {minimal,standard,deep}`: Context depth (default: `standard`)
16+
- `minimal`: Diff only (original behavior)
17+
- `standard`: Diff + full contents of changed `diff_diff/` source files
18+
- `deep`: Standard + import-graph expansion (files imported by changed files)
19+
- `--include-files <file1,file2,...>`: Extra files to include as read-only context
20+
(filenames resolve under `diff_diff/`, or use paths relative to repo root)
21+
- `--token-budget <n>`: Max estimated input tokens before dropping import-context
22+
files (default: 200000). Changed source files are always included regardless of budget.
23+
- `--force-fresh`: Skip delta-diff mode, run a full fresh review even if previous state exists
1524
- `--full-registry`: Include the entire REGISTRY.md instead of selective sections
1625
- `--model <name>`: Override the OpenAI model (default: `gpt-5.4`)
1726
- `--dry-run`: Print the compiled prompt without calling the API
@@ -31,7 +40,8 @@ before any data is sent externally.
3140
### Step 1: Parse Arguments
3241

3342
Parse `$ARGUMENTS` for the optional flags listed above. All flags are optional —
34-
the default behavior (selective registry, gpt-5.4, live API call) requires no arguments.
43+
the default behavior (standard context, selective registry, gpt-5.4, live API call)
44+
requires no arguments.
3545

3646
### Step 2: Validate Prerequisites
3747

@@ -146,20 +156,149 @@ Options:
146156

147157
If user selects abort, clean up temp files and stop. If continue, proceed.
148158

159+
### Step 3c: Full-File Secret Scan (for standard/deep context)
160+
161+
When `--context` is not `minimal`, full source files will be uploaded to OpenAI. The diff-based scan in Step 3b only covers changed lines, so scan the full content of every file that will be transmitted:
162+
163+
```bash
164+
# Category 1: Changed diff_diff/ source files (standard/deep)
165+
upload_scan_files=""
166+
if [ "$context_level" != "minimal" ]; then
167+
upload_scan_files=$(git diff --name-only "${comparison_ref}...HEAD" | grep "^diff_diff/.*\.py$" || true)
168+
fi
169+
170+
# Category 2: All diff_diff/*.py for deep mode (conservative superset of import expansion)
171+
if [ "$context_level" = "deep" ]; then
172+
upload_scan_files=$(find diff_diff -name "*.py" -not -path "*/__pycache__/*" 2>/dev/null | sort -u)
173+
fi
174+
175+
# Category 3: --include-files (mirror script's path confinement)
176+
if [ -n "$include_files" ]; then
177+
repo_root_real=$(pwd -P)
178+
for f in $(echo "$include_files" | tr ',' ' '); do
179+
# Reject absolute paths (mirrors script's os.path.isabs check)
180+
case "$f" in /*) continue ;; esac
181+
# Mirror script's branching: only bare filenames (no /) resolve under diff_diff/
182+
# Slash-containing inputs resolve relative to repo root only
183+
case "$f" in
184+
*/*) if [ -f "$f" ]; then candidate="$f"; else continue; fi ;;
185+
*) if [ -f "diff_diff/$f" ]; then candidate="diff_diff/$f"; else continue; fi ;;
186+
esac
187+
# Verify within repo root (mirrors script's realpath containment)
188+
real_candidate=$(cd "$(dirname "$candidate")" && pwd -P)/$(basename "$candidate")
189+
case "$real_candidate" in "$repo_root_real"/*) upload_scan_files="$upload_scan_files $candidate" ;; esac
190+
done
191+
fi
192+
193+
# Scan using same canonical content patterns from Step 3b (never echo secret values)
194+
if [ -n "$upload_scan_files" ]; then
195+
secret_hits=$(grep -rlE "(AKIA[A-Z0-9]{16}|ghp_[a-zA-Z0-9]{36}|sk-[a-zA-Z0-9]{48}|gho_[a-zA-Z0-9]{36}|[Aa][Pp][Ii][_-]?[Kk][Ee][Yy][[:space:]]*[=:]|[Ss][Ee][Cc][Rr][Ee][Tt][_-]?[Kk][Ee][Yy][[:space:]]*[=:]|[Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd][[:space:]]*[=:]|[Pp][Rr][Ii][Vv][Aa][Tt][Ee][_-]?[Kk][Ee][Yy]|[Bb][Ee][Aa][Rr][Ee][Rr][[:space:]]+[a-zA-Z0-9_-]+|[Tt][Oo][Kk][Ee][Nn][[:space:]]*[=:])" $upload_scan_files 2>/dev/null || true)
196+
sensitive_hits=$(echo "$upload_scan_files" | tr ' ' '\n' | grep -iE "(\.env|credentials|secret|\.pem|\.key|\.p12|\.pfx|id_rsa|id_ed25519)$" || true)
197+
fi
198+
```
199+
200+
If either `secret_hits` or `sensitive_hits` is non-empty, use AskUserQuestion:
201+
```
202+
Warning: Potential secrets detected in source files that would be uploaded to OpenAI
203+
(full-file scan, not just diff hunks):
204+
- <list filenames>
205+
206+
Options:
207+
1. Abort — review and remove secrets before retrying
208+
2. Continue — I confirm these are not real secrets
209+
```
210+
211+
If user selects abort, clean up temp files and stop. If continue, proceed.
212+
149213
### Step 4: Handle Re-Review State
150214

151-
If `.claude/reviews/local-review-latest.md` exists, preserve it for re-review context:
215+
Check for existing review state and generate delta diff if applicable. **Validate that the
216+
stored state matches the current branch and base** before reusing — stale state from a
217+
different branch can contaminate re-review context.
218+
219+
**If `--force-fresh` is set**, delete prior state but still seed a new baseline:
152220
```bash
153-
if [ -f .claude/reviews/local-review-latest.md ]; then
154-
cp .claude/reviews/local-review-latest.md .claude/reviews/local-review-previous.md
155-
echo "Previous review preserved for re-review context."
221+
rm -f .claude/reviews/review-state.json
222+
rm -f .claude/reviews/local-review-latest.md
223+
rm -f .claude/reviews/local-review-previous.md
224+
echo "Force-fresh: deleted all prior review state. Fresh review will seed new baseline."
225+
# Do NOT pass --previous-review or --delta-diff/--delta-changed-files
226+
# DO still pass --review-state, --commit-sha, --base-ref so the fresh run seeds a new baseline
227+
```
228+
229+
**Otherwise**, validate existing review state using the Python validator (single-point
230+
validation that checks schema version, branch/base match, and required finding fields):
231+
```bash
232+
if [ -f .claude/reviews/review-state.json ]; then
233+
# Use the script's validate_review_state() for comprehensive validation
234+
# Returns: last_reviewed_commit and is_valid flag
235+
validation_result=$(python3 -c "
236+
import importlib.util, sys
237+
spec = importlib.util.spec_from_file_location('openai_review', '.claude/scripts/openai_review.py')
238+
mod = importlib.util.module_from_spec(spec)
239+
spec.loader.exec_module(mod)
240+
_, _, commit, valid = mod.validate_review_state(
241+
'.claude/reviews/review-state.json', '$branch_name', '$comparison_ref'
242+
)
243+
print(f'{commit}|{valid}')
244+
" 2>/dev/null || echo "|False")
245+
last_reviewed_commit=$(echo "$validation_result" | cut -d'|' -f1)
246+
state_valid=$(echo "$validation_result" | cut -d'|' -f2)
247+
248+
if [ "$state_valid" != "True" ]; then
249+
echo "Warning: review-state.json validation failed. Running fresh review."
250+
rm -f .claude/reviews/review-state.json
251+
rm -f .claude/reviews/local-review-previous.md
252+
last_reviewed_commit=""
253+
fi
254+
255+
# Only generate delta when state is valid AND commit is an ancestor of HEAD
256+
if [ -n "$last_reviewed_commit" ] && git merge-base --is-ancestor "$last_reviewed_commit" HEAD 2>/dev/null; then
257+
# SHA is a valid ancestor — generate delta diff
258+
git diff --unified=5 "${last_reviewed_commit}...HEAD" > /tmp/ai-review-delta-diff.patch
259+
git diff --name-status "${last_reviewed_commit}...HEAD" > /tmp/ai-review-delta-files.txt
260+
261+
# Check if delta is empty
262+
if [ ! -s /tmp/ai-review-delta-diff.patch ]; then
263+
echo "No changes since last review (commit ${last_reviewed_commit:0:7}). Use --force-fresh to re-review."
264+
rm -f /tmp/ai-review-delta-diff.patch /tmp/ai-review-delta-files.txt
265+
rm -f /tmp/ai-review-diff.patch /tmp/ai-review-files.txt
266+
# Stop here
267+
fi
268+
# State validated and delta generated — preserve previous review for re-review context
269+
if [ -f .claude/reviews/local-review-latest.md ]; then
270+
cp .claude/reviews/local-review-latest.md .claude/reviews/local-review-previous.md
271+
echo "Previous review preserved for re-review context."
272+
fi
273+
else
274+
echo "Warning: Previous review commit is not an ancestor of HEAD (likely rebase). Running fresh review."
275+
rm -f .claude/reviews/review-state.json
276+
rm -f .claude/reviews/local-review-previous.md
277+
fi
278+
fi
279+
280+
# Final cleanup: if delta mode was NOT activated (no delta files generated),
281+
# ensure no stale previous-review file can leak into the fresh review.
282+
# This covers the case where review-state.json is missing entirely but
283+
# local-review-previous.md lingers from a prior run.
284+
if [ ! -f /tmp/ai-review-delta-diff.patch ]; then
285+
rm -f .claude/reviews/local-review-previous.md
156286
fi
157287
```
158288

289+
**Important**: Previous review text is ONLY preserved when delta mode is active (delta files
290+
generated and state validated). When delta mode is NOT active — whether because
291+
`review-state.json` is missing, branch/base mismatch, non-ancestor, or `--force-fresh`
292+
the previous review file is deleted to prevent stale findings from leaking into a fresh
293+
review via `--previous-review`.
294+
159295
### Step 5: Run the Review Script
160296

161-
Build and run the command. Include `--previous-review` only if the previous review file
162-
exists from Step 4.
297+
Build and run the command. Include optional arguments only when their conditions are met:
298+
- `--previous-review`: only if `.claude/reviews/local-review-previous.md` exists AND `--force-fresh` was NOT set
299+
- `--delta-diff` and `--delta-changed-files`: only if delta files were generated in Step 4
300+
- `--review-state`, `--commit-sha`, `--base-ref`: always include (even with `--force-fresh`, to seed a new baseline)
301+
- `--context`, `--include-files`, `--token-budget`: pass through from parsed arguments
163302

164303
```bash
165304
python3 .claude/scripts/openai_review.py \
@@ -169,14 +308,26 @@ python3 .claude/scripts/openai_review.py \
169308
--changed-files /tmp/ai-review-files.txt \
170309
--output .claude/reviews/local-review-latest.md \
171310
--branch-info "$branch_name" \
311+
--repo-root "$(pwd)" \
312+
--context "$context_level" \
313+
--review-state .claude/reviews/review-state.json \
314+
--commit-sha "$(git rev-parse HEAD)" \
315+
--base-ref "$comparison_ref" \
172316
[--previous-review .claude/reviews/local-review-previous.md] \
317+
[--delta-diff /tmp/ai-review-delta-diff.patch] \
318+
[--delta-changed-files /tmp/ai-review-delta-files.txt] \
319+
[--include-files "$include_files"] \
320+
[--token-budget "$token_budget"] \
173321
[--full-registry] \
174322
[--model <model>] \
175323
[--dry-run]
176324
```
177325

178-
If `--dry-run`: display the prompt output and stop. Report the estimated token count
179-
and model that would be used.
326+
Note: `--force-fresh` is a skill-only flag — it controls whether delta diffs are
327+
generated in Step 4 and is NOT passed to the script.
328+
329+
If `--dry-run`: display the prompt output and stop. Report the estimated token count,
330+
cost estimate, and model that would be used.
180331

181332
If the script exits non-zero, display the error output and stop.
182333

@@ -191,6 +342,10 @@ Parse the review output to extract ALL findings. For each finding, capture:
191342
- Section (Methodology, Code Quality, etc.)
192343
- One-line summary of the issue
193344

345+
Note: The script handles writing `review-state.json` automatically (finding tracking
346+
across rounds). The skill does NOT need to write JSON — just pass `--review-state`
347+
and `--commit-sha` to the script.
348+
194349
Present a **findings summary** showing every finding, grouped by severity:
195350

196351
```
@@ -243,13 +398,21 @@ directly — for P0/P1 issues use `EnterPlanMode` for a structured approach; for
243398
issues, fix them directly since they are minor.
244399

245400
After fixes are committed, the user re-runs `/ai-review-local` for a follow-up review.
401+
On re-review, the script automatically activates delta-diff mode (comparing only
402+
changes since the last reviewed commit) and shows a structured findings table
403+
tracking which previous findings have been addressed.
246404

247405
### Step 8: Cleanup
248406

249407
```bash
250-
rm -f /tmp/ai-review-diff.patch /tmp/ai-review-files.txt
408+
rm -f /tmp/ai-review-diff.patch /tmp/ai-review-files.txt \
409+
/tmp/ai-review-delta-diff.patch /tmp/ai-review-delta-files.txt
251410
```
252411

412+
Note: `.claude/reviews/review-state.json` is preserved across review rounds (it
413+
tracks the last reviewed commit and findings). It is cleaned up when the user
414+
runs `--force-fresh` or when a rebase invalidates the tracked commit.
415+
253416
## Error Handling
254417

255418
| Scenario | Response |
@@ -260,28 +423,57 @@ rm -f /tmp/ai-review-diff.patch /tmp/ai-review-files.txt
260423
| Script exits non-zero | Display stderr output from script |
261424
| Previous review file missing on re-run | Script warns and continues as fresh review |
262425
| User aborts due to uncommitted changes | Clean exit |
426+
| No changes since last review (empty delta) | Report and stop, suggest `--force-fresh` |
427+
| Rebase invalidates last reviewed commit | Warn, delete stale state, run fresh review |
428+
| `review-state.json` schema mismatch | Script warns and starts fresh |
263429

264430
## Examples
265431

266432
```bash
267-
# Standard review of current branch vs main
433+
# Standard review of current branch vs main (default: full source file context)
268434
/ai-review-local
269435

270-
# Review with full methodology registry
271-
/ai-review-local --full-registry
436+
# Review with minimal context (diff only, original behavior)
437+
/ai-review-local --context minimal
438+
439+
# Review with deep context (changed files + imported files)
440+
/ai-review-local --context deep
441+
442+
# Include specific files as extra context
443+
/ai-review-local --include-files linalg.py,utils.py
272444

273445
# Preview the compiled prompt without calling the API
274446
/ai-review-local --dry-run
275447

276-
# Use a different model
277-
/ai-review-local --model gpt-4.1
448+
# Force a fresh review (ignore previous review state)
449+
/ai-review-local --force-fresh
450+
451+
# Use a different model with full registry
452+
/ai-review-local --model gpt-4.1 --full-registry
453+
454+
# Limit token budget for faster/cheaper reviews
455+
/ai-review-local --token-budget 100000
278456
```
279457

280458
## Notes
281459

282460
- This skill does NOT modify source files — it only generates temp files and
283461
review artifacts in `.claude/reviews/` (which is gitignored). It may also
284462
create a commit if there are uncommitted changes (Step 3).
463+
- **Context levels**: By default (`standard`), the full contents of changed
464+
`diff_diff/` source files are sent alongside the diff. This catches "sins of
465+
omission" — code that should have changed but wasn't (e.g., a wrapper missing
466+
a new parameter). Use `--context deep` to also include files imported by
467+
changed files as read-only reference.
468+
- **Delta-diff re-review**: When `review-state.json` exists from a previous run,
469+
the script automatically generates a delta diff (changes since the last reviewed
470+
commit) and focuses the reviewer on those changes. The full branch diff is
471+
included as reference context. Use `--force-fresh` to bypass this.
472+
- **Finding tracking**: The script writes structured findings to `review-state.json`
473+
after each review. On re-review, previous findings are shown in a table with
474+
their status (open/addressed), enabling the reviewer to focus on what changed.
475+
- **Cost visibility**: The script shows estimated cost before the API call and
476+
actual cost (from the API response) after completion.
285477
- Re-review mode activates automatically when a previous review exists in
286478
`.claude/reviews/local-review-latest.md`
287479
- The review criteria are adapted from `.github/codex/prompts/pr_review.md` (same
@@ -290,8 +482,9 @@ rm -f /tmp/ai-review-diff.patch /tmp/ai-review-files.txt
290482
- The CI review (Codex action with full repo access) remains the authoritative final
291483
check — local review is a fast first pass to catch most issues early
292484
- **Data transmission**: In non-dry-run mode, this skill transmits the unified diff,
293-
changed-file metadata, selected methodology registry text, and prior review context
294-
(if present) to OpenAI via the Chat Completions API. Use `--dry-run` to preview
295-
exactly what would be sent.
485+
changed-file metadata, full source file contents (in standard/deep mode),
486+
import-context files (in deep mode), selected methodology registry text, and
487+
prior review context (if present) to OpenAI via the Chat Completions API.
488+
Use `--dry-run` to preview exactly what would be sent.
296489
- This skill pairs naturally with the iterative workflow:
297490
`/ai-review-local` -> address findings -> `/ai-review-local` -> `/submit-pr`

0 commit comments

Comments
 (0)