Skip to content
Merged
79 changes: 79 additions & 0 deletions .claude/hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,85 @@ result = subprocess.run(
)
```

### 3. `post_pr_ai_review.py`

**目的**: PR作成後にAI(Codex + Gemini)による自動コードレビューを実行

**トリガー**: `PostToolUse(Bash)` で `gh pr create` の成功を検出

**動作**:

- `gh pr create` 成功後に自動実行
- インストールされているAIツール(Codex、Gemini)でレビューを実行
- 各AIがコード変更をレビュー(正確性、パフォーマンス、セキュリティ、保守性)
- verdict("patch is correct" / "patch is incorrect")と信頼度スコアを出力
- ブロックはしない(レビュー結果を表示のみ)

**前提条件**:

- Codex CLI(`npm install -g @openai/codex`)またはGemini CLI(`npm install -g @google/gemini-cli`)がインストール済み
- 両方インストールされていれば両方でレビューを実行
- どちらも未インストールの場合は自動スキップ

**設定例**:

```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "tool_name == 'Bash'",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/post_pr_ai_review.py"
}
]
}
]
}
}
```

### 4. `pre_exit_plan_ai_review.py`

**目的**: プラン作成後、ExitPlanMode実行前にAI(Codex + Gemini)によるプランレビューを実行

**トリガー**: `PreToolUse(ExitPlanMode)`

**動作**:

- ExitPlanMode実行前に自動発火
- 最新のプランファイル(`~/.claude/plans/*.md`)を検出
- インストールされているAIツールでプランをレビュー(完全性、技術的実現可能性、リスク、依存関係)
- いずれかのAIが "plan needs revision" と判定した場合は exit code 2 でブロック
- いずれかのAIが "plan is ready" と判定した場合は続行を許可

**前提条件**:

- Codex CLIまたはGemini CLIがインストール済み
- プランファイルが `~/.claude/plans/` に存在

**設定例**:

```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "tool_name == 'ExitPlanMode'",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/pre_exit_plan_ai_review.py"
}
]
}
]
}
}
```

## カスタムHooksの作成

新しいHookスクリプトを作成する場合の基本構造:
Expand Down
187 changes: 187 additions & 0 deletions .claude/hooks/post_pr_ai_review.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
#!/usr/bin/env python3
"""
PR作成後にAIレビューを自動実行するPostToolUseフック

gh pr create 成功後に自動的にCodexとGeminiによるコードレビューを実行します。
インストールされているツールのみ実行されます。
"""
import sys
import json
import subprocess
import shutil
import os
import re

# Read input from Claude
data = json.load(sys.stdin)

tool_name = data.get("tool_name", "")
tool_input = data.get("tool_input", {}) or {}
tool_response = data.get("tool_response", {}) or {}

# Bashツールでない場合はスキップ
if tool_name != "Bash":
sys.exit(0)

# コマンドを取得
command = tool_input.get("command", "").strip()

# gh pr create コマンドかどうかを厳密に判定(プレフィックス判定)
if not command.startswith("gh pr create"):
sys.exit(0)

# ヘルプコマンドは除外
if "--help" in command or "-h" in command:
sys.exit(0)

# ツール実行が成功したかチェック
stdout = tool_response.get("stdout", "")
stderr = tool_response.get("stderr", "")

# PR URLパターン(https://github.com/owner/repo/pull/123 形式)
pr_url_pattern = r"https://github\.com/[^/]+/[^/]+/pull/\d+"
combined_output = stdout + stderr

# PR URLが出力に含まれていれば成功と判断
if not re.search(pr_url_pattern, combined_output):
sys.exit(0)

# 利用可能なAIツールを確認
has_codex = shutil.which("codex") is not None
has_gemini = shutil.which("gemini") is not None

if not has_codex and not has_gemini:
print("⚠️ AIレビューツール(Codex/Gemini)がインストールされていません。スキップします。", file=sys.stderr)
sys.exit(0)

# レビュープロンプト
review_prompt = """You are acting as a reviewer for a proposed code change made by another engineer.
Focus on issues that impact correctness, performance, security, maintainability, or developer experience.
Flag only actionable issues introduced by the change.
When you flag an issue, provide a short, direct explanation and cite the affected file and line range.
Prioritize severe issues and avoid nit-level comments unless they block understanding of the diff.
After listing findings, produce an overall correctness verdict ('patch is correct' or 'patch is incorrect') with a concise justification and a confidence score between 0 and 1.
Review the current branch against origin/main.
Use git merge-base to find the merge base, then review the diff from that merge base to HEAD."""

print("", file=sys.stderr)
print("=" * 60, file=sys.stderr)
print("🔍 PR作成完了。AIレビューを実行中...", file=sys.stderr)
print("=" * 60, file=sys.stderr)


def run_codex_review():
"""Codexによるレビューを実行"""
print("", file=sys.stderr)
print("## 🤖 Codex Review", file=sys.stderr)
print("-" * 40, file=sys.stderr)

codex_command = [
"codex", "exec",
"--sandbox", "read-only",
review_prompt
]

try:
result = subprocess.run(
codex_command,
cwd=os.getcwd(),
capture_output=True,
text=True,
timeout=600
)

if result.stdout:
print(result.stdout, file=sys.stderr)

if result.returncode != 0 and result.stderr:
# エラー出力の先頭部分のみ表示
print(f"⚠️ Codexエラー: {result.stderr[:300]}", file=sys.stderr)

except subprocess.TimeoutExpired:
print("⚠️ Codexレビューがタイムアウトしました(10分)", file=sys.stderr)
except Exception as e:
print(f"⚠️ Codexレビュー実行エラー: {e}", file=sys.stderr)


def run_gemini_review():
"""Geminiによるレビューを実行(diffをstdinで渡す)"""
print("", file=sys.stderr)
print("## ✨ Gemini Review", file=sys.stderr)
print("-" * 40, file=sys.stderr)

try:
# マージベースを取得
merge_base_result = subprocess.run(
["git", "merge-base", "origin/main", "HEAD"],
capture_output=True,
text=True,
timeout=30
)
merge_base = merge_base_result.stdout.strip()

if not merge_base:
print("⚠️ マージベースの取得に失敗しました", file=sys.stderr)
return

# diffを取得
diff_result = subprocess.run(
["git", "diff", merge_base, "HEAD"],
capture_output=True,
text=True,
timeout=60
)
diff_content = diff_result.stdout

if not diff_content:
print("⚠️ diffが空です", file=sys.stderr)
return

# Gemini用のプロンプト(diffを含める)
gemini_prompt = f"""You are acting as a reviewer for a proposed code change.
Focus on issues that impact correctness, performance, security, maintainability, or developer experience.
Flag only actionable issues introduced by the change.
When you flag an issue, provide a short, direct explanation and cite the affected file and line range.
Prioritize severe issues and avoid nit-level comments unless they block understanding of the diff.
After listing findings, produce an overall correctness verdict ('patch is correct' or 'patch is incorrect') with a concise justification and a confidence score between 0 and 1.

## Git Diff to Review:

{diff_content[:50000]}"""

gemini_command = ["gemini", "-p", gemini_prompt]

result = subprocess.run(
gemini_command,
cwd=os.getcwd(),
capture_output=True,
text=True,
timeout=600
)

if result.stdout:
print(result.stdout, file=sys.stderr)

if result.returncode != 0 and result.stderr:
print(f"⚠️ Geminiエラー: {result.stderr[:300]}", file=sys.stderr)

except subprocess.TimeoutExpired:
print("⚠️ Geminiレビューがタイムアウトしました(10分)", file=sys.stderr)
except Exception as e:
print(f"⚠️ Geminiレビュー実行エラー: {e}", file=sys.stderr)


# 利用可能なツールでレビューを実行
if has_codex:
run_codex_review()

if has_gemini:
run_gemini_review()

print("", file=sys.stderr)
print("=" * 60, file=sys.stderr)
print("✅ AIレビュー完了", file=sys.stderr)
print("=" * 60, file=sys.stderr)

# PostToolUseフックは常に成功で終了(ブロックしない)
sys.exit(0)
Loading