new file mode 100644
@@ -0,0 +1,197 @@
+# AI Review Suite for OVS
+
+This directory contains tools for conducting AI-powered code reviews of OVS
+patches using Claude (Anthropic API).
+
+## Setup
+
+1. Install the required Python packages:
+ ```bash
+ pip install -r .ci/ai_review/requirements.txt
+ ```
+
+ Or install directly:
+ ```bash
+ pip install anthropic
+ ```
+
+2. Set your Anthropic API key:
+ ```bash
+ export ANTHROPIC_API_KEY='your-api-key-here'
+ ```
+
+## Usage
+
+### Batch Review Multiple Commits (Recommended)
+
+The easiest way to review multiple commits or email patches is using the
+`run_code_review_session.sh` script:
+
+```bash
+# Review last 3 commits
+.ci/ai_review/run_code_review_session.sh HEAD~2 HEAD~1 HEAD
+
+# Review a range of commits from a branch
+.ci/ai_review/run_code_review_session.sh $(git rev-list main..feature-branch)
+
+# Review specific commits
+.ci/ai_review/run_code_review_session.sh abc123 def456 789ghi
+
+# Review patch files from email (with automatic email reply formatting)
+.ci/ai_review/run_code_review_session.sh patch1.patch patch2.patch
+
+# Custom output directory
+.ci/ai_review/run_code_review_session.sh HEAD~2 HEAD~1 HEAD \
+ --output-dir /tmp/reviews
+```
+
+The script will:
+1. Generate patches for each commit using `git format-patch` (or use provided
+patch files)
+2. Reset the tree to each commit's parent (for git commits)
+3. Run the AI review
+4. **If patch has email headers**: Format output as email reply with proper
+To:, Subject:, In-Reply-To:, and References: headers
+5. Save outputs as `message_0001`, `message_0002`, etc.
+6. Restore your original git state when done
+
+#### Email Header Support
+
+When reviewing patches from email (e.g., from a mailing list), the script
+automatically:
+- Detects email headers (Message-ID, From, Subject, etc.)
+- Formats the review output as a proper email reply:
+ - `To:` set to the original patch author (From:)
+ - `Subject:` prefixed with "Re:"
+ - `In-Reply-To:` set to original Message-ID
+ - `References:` includes the original Message-ID and any existing references
+
+This makes it easy to send the review back to the mailing list as a proper
+threaded reply.
+
+### Basic Review (Single Patch)
+
+To review a patch file directly:
+
+```bash
+.ci/ai_review/review.py path/to/patch.patch
+```
+
+This will output the review to stdout.
+
+### Save Review to File
+
+```bash
+.ci/ai_review/review.py path/to/patch.patch --output review.txt
+```
+
+### Custom Prompt
+
+By default, the script uses the `review-start` prompt. To use a different
+prompt:
+
+```bash
+.ci/ai_review/review.py path/to/patch.patch --prompt custom-prompt
+```
+
+(This will look for `.ci/ai_review/prompts/custom-prompt.md`)
+
+### Advanced Options
+
+```bash
+.ci/ai_review/review.py \
+ path/to/patch.patch \
+ --prompt review-start \
+ --model claude-sonnet-4-20250514 \
+ --max-tokens 16000 \
+ --output review.txt
+```
+
+### Including Additional Context
+
+If the prompt references other scripts or files that Claude needs access to, you
+can provide them as context:
+
+```bash
+.ci/ai_review/review.py path/to/patch.patch \
+ --context lib/odp-util.c \
+ --context utilities/checkpatch.py \
+ --output review.txt
+```
+
+You can specify `--context` multiple times to include multiple files. These
+files will be included in the API call so Claude can reference them during the
+review.
+
+### Disable Git Context
+
+By default, the script automatically extracts git context (current branch,
+recent commits, commit SHA from patch). To disable this:
+
+```bash
+.ci/ai_review/review.py path/to/patch.patch --no-git-context
+```
+
+## Command Line Options
+
+- `patch_file` (required): Path to the patch file to review
+- `--prompt PROMPT`: Name of the prompt file to use (default: review-start)
+- `--model MODEL`: Claude model to use (default: claude-sonnet-4-20250514)
+- `--max-tokens N`: Maximum tokens for response (default: 16000)
+- `--output FILE`: Output file for the review (default: stdout)
+- `--context FILE`: Additional context file to include (can be used multiple
+times)
+- `--no-git-context`: Disable automatic git context extraction
+
+## Prompts
+
+Review prompts are stored in `.ci/ai_review/prompts/` as Markdown files. The
+script will automatically load the specified prompt and combine it with the
+patch content before sending to Claude.
+
+To create a new prompt, add a new `.md` file to the prompts directory.
+
+### Context Files for Prompts
+
+If your prompt references scripts, tools, or specific source files that Claude
+should have access to, you have two options:
+
+#### Option 1: Automatic Loading with @ref: (Recommended)
+
+Add `@ref:filename` references directly in your prompt file. The script will
+automatically detect and load these files:
+
+```markdown
+<!-- In .ci/ai_review/prompts/review-start.md -->
+
+Please review this patch according to the coding standards in @ref:CODING_STYLE.md
+and compare against the reference implementation in @ref:lib/common-functions.c
+
+When checking for memory leaks, refer to @ref:memory-patterns.md
+```
+
+The script will automatically:
+- Detect all `@ref:filename` patterns in the prompt
+- Load the referenced files (searches in prompts directory first, then relative
+paths)
+- Include them in the context sent to Claude
+
+#### Option 2: Manual Context with --context Flag
+
+You can also provide context files manually using the `--context` flag:
+
+```bash
+.ci/ai_review/review.py patch.patch \
+ --context CODING_STYLE.md \
+ --context lib/common-functions.c
+```
+
+**Note:** Both methods can be combined. Files specified with `@ref:` in prompts
+will be merged with files specified via `--context` flags.
+
+## Requirements
+
+- Python 3.6 or later
+- anthropic Python package
+- Valid Anthropic API key
+- Git repository (script auto-detects git root)
new file mode 100644
@@ -0,0 +1,110 @@
+Produce a report of regressions found based on this template.
+
+- Reviews should be in plain text only. Do not use markdown, special
+characters, emoji-alikes. Only plain text is suitable for the ovs-dev mailing
+list.
+
+- Any long lines present in the unified diff should be preserved, but any
+summary, comments, or questions you add should be wrapped at 79 characters.
+
+- Never mention line numbers when referencing code locations. Instead
+use the function name and also call chain if it makes it more clear. Avoid
+complex paragraphs, and instead use call chains fA()->fB() to explain.
+
+- Always end the report with a blank line.
+
+- The report must be conversational with undramatic wording, fit for sending
+as a reply to the patch being analyzed to the ovs-dev mailing list.
+
+- Explain any regressions as questions about the code, but do not mention
+the authors. Don't say "Did you do X?" but rather say, "Can this X?" or
+"Does this code X?"
+
+- Vary question phrasing. Do not always start all questions in the same
+manner.
+
+- Ask your question specifically about the sources you're referencing.
+ - If you suspect a leak, ask specifically about the resource being leaked.
+ "Does this code leak this thing?"
+ - Don't say "Does this loop have a bounds checking issue?" Name the variable
+ you think is overflowing: "Does this code overflow xyz[]"
+
+- Don't make long paragraphs, ask short questions backed up by code snippets,
+or call chains.
+
+- Ensure that the code follows the official coding style guide found in
+https://github.com/openvswitch/ovs/blob/main/Documentation/internals/contributing/coding-style.rst
+
+- Verify that the commit subject and message comply with the project's
+submission guide found in
+https://github.com/openvswitch/ovs/blob/main/Documentation/internals/contributing/submitting-patches.rst
+
+- For dynamic string management, confirm that functions like `ds_init()` are
+not being called repeatedly in a loop when `ds_clear()` should be used to
+reuse the buffer.
+
+- Verify proper use of `ds_init()`, `ds_clear()`, and `ds_destroy()` (no
+redundant init, no leaks inside loops).
+
+- Check that all dynamically allocated resources (`xmalloc()`, `json_*()`, etc.)
+are properly freed or reused.
+
+- Portability: Verify that the patch does not rely on undefined or
+platform-specific behavior.
+
+- Error Handling: Ensure proper error detection, logging, and cleanup on
+failure.
+
+- Readability & Maintainability: Evaluate naming, comments, and modularity.
+
+- Be sure to wrap all comments at 79 characters.
+
+- Make sure to check for whitespace errors (things like aligned whitespace at
+the start of a line, and incorrect whitespace in includes).
+
+- Check for common mistake patters such as calling `strcmp` with NULL.
+
+Create a TodoWrite for these items, all of which your report should include:
+
+- [ ] git sha of the commit
+- [ ] Author: line from the commit
+- [ ] One line subject of the commit
+
+- [ ] A brief summary of the commit. Use the full commit message if the bug is
+in the commit message itself.
+
+- [ ] A unified diff of the commit, quoted as though it's in an email reply.
+ - [ ] The diff must not be generated from existing context.
+ - [ ] You must regenerate the diff by calling out to semcode's commit
+ function,
+ using git log, or re-reading any patch files you were asked to review.
+ - [ ] You must ensure the quoted portions of the diff exactly match the
+ original commit or patch.
+
+- [ ] Place your questions about the regressions you found alongside the code
+ in the diff that introduced them. Do not put the quoting '> ' characters in
+ front of your new text.
+- [ ] Place your questions as close as possible to the buggy section of code.
+
+- [ ] Snip portions of the quoted content unrelated to your review
+ - [ ] Create a TodoWrite with every hunk in the diff. Check every hunk
+ to see if it is relevant to the review comments.
+ - [ ] ensure diff headers are retained for the files owning any hunks keep
+ - [ ] Replace any content you snip with [ ... ]
+ - [ ] Never include diff headers for entirely snipped files
+ - [ ] snip entire files unrelated to the review comments
+ - [ ] snip entire hunks from quoted files if they unrelated to the review
+ - [ ] snip entire functions from the quoted hunks unrelated to the review
+ - [ ] snip any portions of large functions from quoted hunks if unrelated to
+ the review
+ - [ ] ensure you only keep enough quoted material for the review to make sense
+ - [ ] snip trailing hunks and files after your last review comments unless
+ you need them for the review to make sense
+ - [ ] The review should contain only the portions of hunks needed to explain the review's concerns.
+
+Use the following sample as a guideline:
+
+```
+
+```
+
new file mode 100644
@@ -0,0 +1 @@
+anthropic>=0.39.0
new file mode 100755
@@ -0,0 +1,411 @@
+#!/usr/bin/env python3
+"""
+AI-powered code review script for OVS patches.
+
+This script uses Claude (Anthropic API) to review git patches based on
+prompts defined in .ci/ai_review/prompts/.
+"""
+
+import argparse
+import os
+import re
+import subprocess
+import sys
+from pathlib import Path
+from typing import Optional, List, Dict, Set
+
+try:
+ import anthropic
+except ImportError:
+ print("Error: anthropic package not installed.", file=sys.stderr)
+ print("Please install it with: pip install anthropic", file=sys.stderr)
+ sys.exit(1)
+
+
+def find_git_root() -> Path:
+ """Find the root of the git repository."""
+ current = Path.cwd()
+ while current != current.parent:
+ if (current / ".git").exists():
+ return current
+ current = current.parent
+ raise RuntimeError("Not in a git repository")
+
+
+def find_referenced_files(content: str, prompts_dir: Path) -> Set[Path]:
+ """
+ Find all files referenced in the content using @ref:filename pattern.
+
+ Args:
+ content: The text content to scan for references
+ prompts_dir: Directory where prompt files are located
+
+ Returns:
+ Set of Path objects for referenced files
+ """
+ # Pattern to match @ref:filename.md or @ref:path/to/file.ext
+ pattern = r'@ref:([^\s\)]+)'
+ matches = re.findall(pattern, content)
+
+ referenced_files = set()
+ for match in matches:
+ # Try relative to prompts directory first
+ ref_path = prompts_dir / match
+ if ref_path.exists():
+ referenced_files.add(ref_path)
+ else:
+ # Try as absolute or relative to git root
+ ref_path = Path(match)
+ if ref_path.exists():
+ referenced_files.add(ref_path)
+ else:
+ print(f"Warning: Referenced file not found: {match}", file=sys.stderr)
+
+ return referenced_files
+
+
+def read_prompt_file(prompts_dir: Path, prompt_name: str) -> tuple[str, Set[Path]]:
+ """
+ Read a prompt file from the prompts directory and find referenced files.
+
+ Args:
+ prompts_dir: Directory where prompt files are located
+ prompt_name: Name of the prompt (without .md extension)
+
+ Returns:
+ Tuple of (prompt_content, set_of_referenced_files)
+ """
+ prompt_file = prompts_dir / f"{prompt_name}.md"
+ if not prompt_file.exists():
+ raise FileNotFoundError(f"Prompt file not found: {prompt_file}")
+
+ with open(prompt_file, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ # Find all referenced files
+ referenced_files = find_referenced_files(content, prompts_dir)
+
+ return content, referenced_files
+
+
+def read_patch_file(patch_path: Path) -> str:
+ """Read the patch file contents."""
+ if not patch_path.exists():
+ raise FileNotFoundError(f"Patch file not found: {patch_path}")
+
+ with open(patch_path, 'r', encoding='utf-8') as f:
+ return f.read()
+
+
+def get_api_key() -> str:
+ """Get the Anthropic API key from environment."""
+ api_key = os.environ.get("ANTHROPIC_API_KEY")
+ if not api_key:
+ raise RuntimeError(
+ "ANTHROPIC_API_KEY environment variable not set.\n"
+ "Please set it with: export ANTHROPIC_API_KEY='your-api-key'"
+ )
+ return api_key
+
+
+def run_git_command(git_root: Path, command: List[str]) -> Optional[str]:
+ """
+ Run a git command and return its output.
+
+ Args:
+ git_root: Root of the git repository
+ command: Git command as list (e.g., ['log', '--oneline', '-1'])
+
+ Returns:
+ Command output as string, or None if command fails
+ """
+ try:
+ result = subprocess.run(
+ ['git'] + command,
+ cwd=git_root,
+ capture_output=True,
+ text=True,
+ timeout=30
+ )
+ if result.returncode == 0:
+ return result.stdout.strip()
+ else:
+ print(f"Git command failed: {' '.join(command)}", file=sys.stderr)
+ print(f"Error: {result.stderr}", file=sys.stderr)
+ return None
+ except subprocess.TimeoutExpired:
+ print(f"Git command timed out: {' '.join(command)}", file=sys.stderr)
+ return None
+ except Exception as e:
+ print(f"Error running git command: {e}", file=sys.stderr)
+ return None
+
+
+def extract_git_context(git_root: Path, patch_content: str) -> Dict[str, str]:
+ """
+ Extract git repository context that might be useful for the review.
+
+ Args:
+ git_root: Root of the git repository
+ patch_content: The patch content (may contain commit info)
+
+ Returns:
+ Dictionary with git context information
+ """
+ context = {}
+
+ # Try to extract commit SHA from patch if it's a git format-patch style
+ lines = patch_content.split('\n')
+ for line in lines[:50]: # Check first 50 lines
+ if line.startswith('From '):
+ parts = line.split()
+ if len(parts) >= 2 and len(parts[1]) >= 7:
+ context['commit_sha'] = parts[1][:40] # Full or short SHA
+ break
+
+ # Get current branch info
+ branch = run_git_command(git_root, ['rev-parse', '--abbrev-ref', 'HEAD'])
+ if branch:
+ context['current_branch'] = branch
+
+ # Get recent commits for context
+ recent_log = run_git_command(git_root, ['log', '--oneline', '-5'])
+ if recent_log:
+ context['recent_commits'] = recent_log
+
+ return context
+
+
+def read_context_files(context_paths: List[Path]) -> Dict[str, str]:
+ """
+ Read additional context files (e.g., related source files, scripts).
+
+ Args:
+ context_paths: List of file paths to include as context
+
+ Returns:
+ Dictionary mapping file paths to their contents
+ """
+ context_files = {}
+
+ for path in context_paths:
+ if not path.exists():
+ print(f"Warning: Context file not found: {path}", file=sys.stderr)
+ continue
+
+ try:
+ with open(path, 'r', encoding='utf-8') as f:
+ content = f.read()
+ context_files[str(path)] = content
+ print(f"Loaded context file: {path}", file=sys.stderr)
+ except Exception as e:
+ print(f"Warning: Could not read {path}: {e}", file=sys.stderr)
+
+ return context_files
+
+
+def conduct_review(
+ patch_content: str,
+ prompt_content: str,
+ git_context: Dict[str, str] = None,
+ context_files: Dict[str, str] = None,
+ model: str = "claude-sonnet-4-20250514",
+ max_tokens: int = 16000
+) -> str:
+ """
+ Conduct a code review using Claude API.
+
+ Args:
+ patch_content: The patch file content to review
+ prompt_content: The review instructions/prompt
+ git_context: Git repository context (commit info, branches, etc.)
+ context_files: Additional context files (scripts, related sources)
+ model: The Claude model to use
+ max_tokens: Maximum tokens for the response
+
+ Returns:
+ The review text from Claude
+ """
+ api_key = get_api_key()
+ client = anthropic.Anthropic(api_key=api_key)
+
+ # Construct the full message with all context
+ message_parts = [prompt_content]
+
+ # Add git context if available
+ if git_context:
+ message_parts.append("\n## Git Repository Context\n")
+ for key, value in git_context.items():
+ message_parts.append(f"{key}: {value}\n")
+
+ # Add context files if provided
+ if context_files:
+ message_parts.append("\n## Additional Context Files\n")
+ for file_path, content in context_files.items():
+ message_parts.append(f"\n### File: {file_path}\n")
+ message_parts.append(f"```\n{content}\n```\n")
+
+ # Add the patch to review
+ message_parts.append("\n## Patch to Review\n")
+ message_parts.append(patch_content)
+
+ message_content = "".join(message_parts)
+
+ print(f"Starting review with model: {model}", file=sys.stderr)
+ print(f"Patch size: {len(patch_content)} characters", file=sys.stderr)
+ if git_context:
+ print(f"Git context items: {len(git_context)}", file=sys.stderr)
+ if context_files:
+ print(f"Context files: {len(context_files)}", file=sys.stderr)
+
+ try:
+ message = client.messages.create(
+ model=model,
+ max_tokens=max_tokens,
+ messages=[
+ {
+ "role": "user",
+ "content": message_content
+ }
+ ]
+ )
+
+ # Extract the text response
+ response_text = ""
+ for block in message.content:
+ if block.type == "text":
+ response_text += block.text
+
+ return response_text
+
+ except anthropic.APIError as e:
+ print(f"Anthropic API error: {e}", file=sys.stderr)
+ raise
+ except Exception as e:
+ print(f"Unexpected error during review: {e}", file=sys.stderr)
+ raise
+
+
+def main():
+ """Main entry point for the review script."""
+ parser = argparse.ArgumentParser(
+ description="Conduct AI-powered code review of OVS patches using Claude"
+ )
+ parser.add_argument(
+ "patch_file",
+ type=Path,
+ help="Path to the patch file to review"
+ )
+ parser.add_argument(
+ "--prompt",
+ type=str,
+ default="review-start",
+ help="Name of the prompt to use (without .md extension). Default: review-start"
+ )
+ parser.add_argument(
+ "--model",
+ type=str,
+ #default="claude-sonnet-4-20250929-v1",
+ default="claude-sonnet-4-20250514",
+ help="Claude model to use. Default: claude-sonnet-4-20250514"
+ )
+ parser.add_argument(
+ "--max-tokens",
+ type=int,
+ default=16000,
+ help="Maximum tokens for the response. Default: 16000"
+ )
+ parser.add_argument(
+ "--output",
+ type=Path,
+ help="Output file for the review (default: stdout)"
+ )
+ parser.add_argument(
+ "--context",
+ type=Path,
+ action="append",
+ dest="context_files",
+ help="Additional context files to include (can be specified multiple times)"
+ )
+ parser.add_argument(
+ "--no-git-context",
+ action="store_true",
+ help="Disable automatic git context extraction"
+ )
+
+ args = parser.parse_args()
+
+ try:
+ # Find git root and construct paths
+ git_root = find_git_root()
+ prompts_dir = git_root / ".ci" / "ai_review" / "prompts"
+
+ if not prompts_dir.exists():
+ print(f"Error: Prompts directory not found: {prompts_dir}", file=sys.stderr)
+ sys.exit(1)
+
+ # Read the prompt and find referenced files
+ print(f"Reading prompt: {args.prompt}", file=sys.stderr)
+ prompt_content, referenced_files = read_prompt_file(prompts_dir, args.prompt)
+
+ if referenced_files:
+ print(f"Found {len(referenced_files)} referenced file(s) in prompt:", file=sys.stderr)
+ for ref_file in referenced_files:
+ print(f" - {ref_file}", file=sys.stderr)
+
+ print(f"Reading patch: {args.patch_file}", file=sys.stderr)
+ patch_content = read_patch_file(args.patch_file)
+
+ # Extract git context unless disabled
+ git_context = None
+ if not args.no_git_context:
+ print("Extracting git context...", file=sys.stderr)
+ git_context = extract_git_context(git_root, patch_content)
+
+ # Merge referenced files with explicitly provided context files
+ all_context_paths = list(referenced_files)
+ if args.context_files:
+ all_context_paths.extend(args.context_files)
+
+ # Read all context files
+ context_files = None
+ if all_context_paths:
+ print(f"Loading {len(all_context_paths)} total context file(s)...", file=sys.stderr)
+ context_files = read_context_files(all_context_paths)
+
+ # Conduct the review
+ review_result = conduct_review(
+ patch_content=patch_content,
+ prompt_content=prompt_content,
+ git_context=git_context,
+ context_files=context_files,
+ model=args.model,
+ max_tokens=args.max_tokens
+ )
+
+ # Output the result
+ if args.output:
+ with open(args.output, 'w', encoding='utf-8') as f:
+ f.write(review_result)
+ print(f"Review written to: {args.output}", file=sys.stderr)
+ else:
+ print(review_result)
+
+ print("\nReview completed successfully!", file=sys.stderr)
+
+ except FileNotFoundError as e:
+ print(f"Error: {e}", file=sys.stderr)
+ sys.exit(1)
+ except RuntimeError as e:
+ print(f"Error: {e}", file=sys.stderr)
+ sys.exit(1)
+ except KeyboardInterrupt:
+ print("\nReview interrupted by user", file=sys.stderr)
+ sys.exit(130)
+ except Exception as e:
+ print(f"Unexpected error: {e}", file=sys.stderr)
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
new file mode 100755
@@ -0,0 +1,302 @@
+#!/bin/bash
+# Script to review multiple git commits using AI
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REVIEW_SCRIPT="$SCRIPT_DIR/review.py"
+GIT_ROOT="$(git rev-parse --show-toplevel)"
+WORK_DIR="$GIT_ROOT/.ci/ai_review/reviews"
+
+# Check if API key is set
+if [ -z "$ANTHROPIC_API_KEY" ]; then
+ echo "Error: ANTHROPIC_API_KEY environment variable not set"
+ echo "Please set it with: export ANTHROPIC_API_KEY='your-api-key'"
+ exit 1
+fi
+
+# Function to extract email header
+extract_header() {
+ local file="$1"
+ local header="$2"
+ # Extract header value, handling multi-line headers
+ awk -v header="$header:" '
+ tolower($0) ~ "^" tolower(header) {
+ sub(/^[^:]+: */, "")
+ value = $0
+ # Handle continuation lines (starting with whitespace)
+ while (getline > 0 && /^[ \t]/) {
+ sub(/^[ \t]+/, " ")
+ value = value $0
+ }
+ print value
+ exit
+ }
+ ' "$file"
+}
+
+# Function to check if patch has email headers
+has_email_headers() {
+ local file="$1"
+ grep -q "^Message-ID:" "$file" || grep -q "^Message-Id:" "$file"
+}
+
+# Function to generate email headers for reply
+generate_email_headers() {
+ local to="$1"
+ local subject="$2"
+ local message_id="$3"
+ local references="$4"
+
+ # Generate To: header
+ if [ -n "$to" ]; then
+ echo "To: $to"
+ fi
+
+ # Generate Subject: header
+ if [ -n "$subject" ]; then
+ # Remove any existing Re: prefix and add our own
+ subject=$(echo "$subject" | sed 's/^[Rr][Ee]: *//')
+ echo "Subject: Re: $subject"
+ fi
+
+ # Generate In-Reply-To: header
+ if [ -n "$message_id" ]; then
+ echo "In-Reply-To: $message_id"
+ fi
+
+ # Generate References: header
+ if [ -n "$message_id" ]; then
+ # Add the original Message-ID to references
+ if [ -n "$references" ]; then
+ # Append to existing references
+ echo "References: $references $message_id"
+ else
+ echo "References: $message_id"
+ fi
+ elif [ -n "$references" ]; then
+ echo "References: $references"
+ fi
+
+ echo ""
+}
+
+usage() {
+ cat <<EOF
+Usage: $0 <commit1> <commit2> [commit3 ...]
+Usage: $0 <patch or mbox file>
+
+Review multiple git commits using AI-powered code review.
+
+Arguments:
+ commit1 commit2 ... Git commit IDs to review (can be SHAs, branches, tags,
+ etc.)
+ patch or mbox file... Patch file or MBOX file.
+
+Options:
+ --output-dir DIR Directory to save reviews (default: .ci/ai_review/reviews)
+ --prompt PROMPT Prompt to use (default: review-start)
+ --help Show this help message
+
+Examples:
+ # Review last 3 commits
+ $0 HEAD~2 HEAD~1 HEAD
+
+ # Review a range of commits
+ $0 \$(git rev-list main..feature-branch)
+
+ # Review specific commits
+ $0 abc123 def456 789ghi
+
+Output:
+ Reviews will be saved in the output directory as:
+ message_0001 (review of first commit)
+ message_0002 (review of second commit)
+ ...
+EOF
+ exit 0
+}
+
+# Parse arguments
+OUTPUT_DIR=""
+PROMPT="review-start"
+COMMITS=()
+
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ --help|-h)
+ usage
+ ;;
+ --output-dir)
+ OUTPUT_DIR="$2"
+ shift 2
+ ;;
+ --prompt)
+ PROMPT="$2"
+ shift 2
+ ;;
+ *)
+ COMMITS+=("$1")
+ shift
+ ;;
+ esac
+done
+
+# Check if commits were provided
+if [ ${#COMMITS[@]} -eq 0 ]; then
+ echo "Error: No commits specified"
+ echo ""
+ usage
+fi
+
+# Set default output directory if not specified
+if [ -z "$OUTPUT_DIR" ]; then
+ OUTPUT_DIR="$WORK_DIR"
+fi
+
+# Create output directory
+mkdir -p "$OUTPUT_DIR"
+
+echo "==================================="
+echo "AI Review Tool - Batch Mode"
+echo "==================================="
+echo "Git Root: $GIT_ROOT"
+echo "Output Directory: $OUTPUT_DIR"
+echo "Prompt: $PROMPT"
+echo "Number of commits: ${#COMMITS[@]}"
+echo ""
+
+# Save current branch/HEAD for restoration
+ORIGINAL_HEAD=$(git rev-parse HEAD)
+ORIGINAL_BRANCH=$(git symbolic-ref -q HEAD || echo "")
+
+# Function to restore git state
+restore_git_state() {
+ echo ""
+ echo "Restoring git state..."
+ if [ -n "$ORIGINAL_BRANCH" ]; then
+ git checkout -q "${ORIGINAL_BRANCH#refs/heads/}"
+ else
+ git checkout -q "$ORIGINAL_HEAD"
+ fi
+}
+
+# Set trap to restore state on exit
+trap restore_git_state EXIT
+
+# Process each commit
+counter=1
+for commit in "${COMMITS[@]}"; do
+ # Format counter with leading zeros (0001, 0002, etc.)
+ counter_formatted=$(printf "%04d" $counter)
+ PATCH_FILE="$OUTPUT_DIR/patch_${counter_formatted}.patch"
+
+ if [ ! -f "$commit" ]; then
+ echo "==================================="
+ echo "Processing commit $counter_formatted: $commit"
+ echo "==================================="
+
+ # Verify commit exists
+ if ! git rev-parse --verify "$commit" >/dev/null 2>&1; then
+ echo "Error: Invalid commit: $commit"
+ echo "Skipping..."
+ echo ""
+ counter=$((counter + 1))
+ continue
+ fi
+
+ # Get full commit SHA
+ COMMIT_SHA=$(git rev-parse "$commit")
+ echo "Commit SHA: $COMMIT_SHA"
+
+ # Reset tree to COMMIT~1
+ PARENT_COMMIT="${COMMIT_SHA}~1"
+
+ # Check if parent exists (not initial commit)
+ if git rev-parse --verify "$PARENT_COMMIT" >/dev/null 2>&1; then
+ echo "Checking out parent: $PARENT_COMMIT"
+ git checkout -q "$PARENT_COMMIT"
+ else
+ echo "This is the initial commit, checking out commit itself"
+ git checkout -q "$COMMIT_SHA"
+ fi
+
+ # Generate patch file
+ echo "Generating patch: $PATCH_FILE"
+ git format-patch -1 "$COMMIT_SHA" --stdout > "$PATCH_FILE"
+ else
+ echo "==================================="
+ echo "Processing Patch $counter_formatted: $commit"
+ echo "==================================="
+
+ cp "$commit" "$PATCH_FILE"
+ fi
+
+ # Run review to temporary file first
+ TEMP_REVIEW=$(mktemp)
+ echo "Running AI review..."
+ echo "Output: $OUTPUT_FILE"
+
+ if "$REVIEW_SCRIPT" "$PATCH_FILE" --prompt "$PROMPT" --output "$TEMP_REVIEW"; then
+ echo "✓ Review completed successfully"
+
+ # Check if patch has email headers
+ if has_email_headers "$PATCH_FILE"; then
+ echo " Detected email headers, formatting as reply..."
+
+ # Extract email headers
+ FROM=$(extract_header "$PATCH_FILE" "From")
+ SUBJECT=$(extract_header "$PATCH_FILE" "Subject")
+ MESSAGE_ID=$(extract_header "$PATCH_FILE" "Message-ID")
+ [ -z "$MESSAGE_ID" ] && MESSAGE_ID=$(extract_header "$PATCH_FILE" "Message-Id")
+ REFERENCES=$(extract_header "$PATCH_FILE" "References")
+
+ # Create output with email headers
+ OUTPUT_FILE="$OUTPUT_DIR/message_${counter_formatted}"
+ {
+ generate_email_headers "$FROM" "$SUBJECT" "$MESSAGE_ID" "$REFERENCES"
+ cat "$TEMP_REVIEW"
+ } > "$OUTPUT_FILE"
+
+ echo " To: $FROM"
+ echo " Subject: Re: $(echo "$SUBJECT" | sed 's/^[Rr][Ee]: *//')"
+ else
+ # No email headers, just copy the review
+ OUTPUT_FILE="$OUTPUT_DIR/message_${counter_formatted}"
+ cp "$TEMP_REVIEW" "$OUTPUT_FILE"
+ fi
+
+ rm -f "$TEMP_REVIEW"
+
+ # Show summary
+ REVIEW_SIZE=$(wc -l < "$OUTPUT_FILE")
+ echo " Review size: $REVIEW_SIZE lines"
+ else
+ echo "✗ Review failed"
+ rm -f "$TEMP_REVIEW"
+ echo " Check $OUTPUT_FILE for details"
+ fi
+
+ if [ -f "$commit" ]; then
+ echo "✓ Advancing tree by running [ git am \"$commit\" ]..."
+ git am "$commit"
+ fi
+
+ echo ""
+ counter=$((counter + 1))
+done
+
+# Restore git state (will also be called by trap)
+restore_git_state
+
+echo "==================================="
+echo "All reviews completed!"
+echo "==================================="
+echo "Output directory: $OUTPUT_DIR"
+echo "Files generated:"
+ls -1 "$OUTPUT_DIR"/message_* 2>/dev/null | while read -r file; do
+ echo " - $(basename "$file")"
+done
+echo ""
+echo "To view a review:"
+echo " cat $OUTPUT_DIR/message_0001"
This commit adds a new infrastructure for CI-based testing that can leverage Anthropic AI's Claude sonnet model to assist when doing code reviews. The eventual goal is for the 0-day Robot to run this automatically for each series that comes into the mailing list. Rather than relying on the GitHub infrastructure, this provides a few base tools for doing reviews as well as a prompting infrastructure that we can extend for domain specific hints to the LLM for analysis. The prompt is most useful for inline diffs, mimicing a real interactive development session on the mailing list. This can help to do a few rounds of internal commenting and development before submitting to the list (using your own API key), and then run a final pass based on the upstream. This is a first cut of the functionality. A good chunk of it was generated using Claude LLM, but there is also a heavy amount of manual editing involved as well. A first pass run has been posted to the list against the series found at: https://mail.openvswitch.org/pipermail/ovs-dev/2025-November/427714.html This can give a good idea of what currently is possible. Signed-off-by: Aaron Conole <aconole@redhat.com> --- .ci/ai_review/README.md | 197 +++++++++++ .ci/ai_review/prompts/review-start.md | 110 ++++++ .ci/ai_review/requirements.txt | 1 + .ci/ai_review/review.py | 411 +++++++++++++++++++++++ .ci/ai_review/run_code_review_session.sh | 302 +++++++++++++++++ 5 files changed, 1021 insertions(+) create mode 100644 .ci/ai_review/README.md create mode 100644 .ci/ai_review/prompts/review-start.md create mode 100644 .ci/ai_review/requirements.txt create mode 100755 .ci/ai_review/review.py create mode 100755 .ci/ai_review/run_code_review_session.sh