diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 4acb46e2..3c9d1faf 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -128,7 +128,13 @@ Git操作(commit/push)の前に自動的に品質チェックを実行する - `block_git_no_verify.py`: `--no-verify` や `HUSKY=0` の使用をブロック - `pre_git_quality_gates.py`: Git操作前にQuality Gatesを実行 +- `post_git_push_ci.py`: push後のCI監視 +- `post_pr_ai_review.py`: PR作成後のAIレビュー(Codex/Gemini) +- `pre_exit_plan_ai_review.py`: プランモード終了前のレビュー -これらは `.claude/settings.local.json` の `hooks` フィールドで設定されており、Claudeによる `git commit` や `git push` の実行前に自動的にトリガーされます。 +**DevContainer環境(v1.61.0以降)では、これらのHooksはデフォルトで有効化されています。** +設定は `/home/vscode/.claude/settings.json` に含まれており、追加の設定なしで動作します。 + +DevContainer以外の環境では、`.claude/settings.local.json` の `hooks` フィールドで手動設定が必要です。 詳細は [.claude/hooks/README.md](./.claude/hooks/README.md) を参照してください。 diff --git a/.claude/hooks/README.md b/.claude/hooks/README.md index 9a63a6a6..a56426d2 100644 --- a/.claude/hooks/README.md +++ b/.claude/hooks/README.md @@ -85,9 +85,23 @@ Hooksは、Claude Codeの特定のイベント(ツール実行前後、タス ## Hooksの設定方法 -### ステップ1: settings.local.json に設定を追加 +### DevContainer環境(v1.61.0以降) -`.claude/settings.local.json` ファイルに `hooks` フィールドを追加します: +**v1.61.0以降のDevContainerイメージでは、Hooksはデフォルトで有効化されています。** + +DevContainerを使用している場合、以下のHooksが自動的に設定されます: + +- `block_git_no_verify.py` - `--no-verify` のブロック +- `pre_git_quality_gates.py` - Git操作前の品質チェック +- `post_git_push_ci.py` - push後のCI監視 +- `post_pr_ai_review.py` - PR作成後のAIレビュー +- `pre_exit_plan_ai_review.py` - プランモード終了前のレビュー + +これらは `/home/vscode/.claude/settings.json` に設定されており、追加の設定なしで動作します。 + +### 手動設定(DevContainer以外の環境) + +DevContainer以外の環境では、`.claude/settings.local.json` ファイルに `hooks` フィールドを追加します: ```json { diff --git a/.claude/hooks/post_pr_ai_review.py b/.claude/hooks/post_pr_ai_review.py index 69a98764..6714c9a2 100755 --- a/.claude/hooks/post_pr_ai_review.py +++ b/.claude/hooks/post_pr_ai_review.py @@ -4,6 +4,7 @@ gh pr create 成功後に自動的にCodexとGeminiによるコードレビューを実行します。 インストールされているツールのみ実行されます。 +クリティカルな問題(patch is incorrect)が検出された場合は警告を表示します。 """ import sys import json @@ -64,13 +65,89 @@ 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) +# レビュー結果を格納 +review_results = [] + + +def parse_verdict(output: str) -> dict: + """レビュー結果から verdict と confidence を抽出""" + result = { + "verdict": None, + "confidence": None, + "is_incorrect": False, + "issues": [] + } + + if not output: + return result + + output_lower = output.lower() + # verdict を検出(より正確なパターンマッチング) + # 引用符で囲まれた文字列(例: "patch is incorrect" という説明文)を除外 + # verdict/判定/結論の直後に出現するパターンを優先 -def run_codex_review(): + # verdict 行を探す("verdict:" や "**verdict**" の後) + verdict_patterns = [ + r"verdict[:\s*]+\*{0,2}patch is (incorrect|correct)\*{0,2}", + r"overall[^:]*verdict[:\s*]+\*{0,2}patch is (incorrect|correct)\*{0,2}", + r"判定[:\s*]+patch is (incorrect|correct)", + ] + + for pattern in verdict_patterns: + match = re.search(pattern, output_lower) + if match: + if match.group(1) == "incorrect": + result["verdict"] = "incorrect" + result["is_incorrect"] = True + else: + result["verdict"] = "correct" + break + + # 上記で見つからない場合、文脈を考慮して検出 + if result["verdict"] is None: + # 引用符で囲まれていない "patch is incorrect/correct" を検出 + # 引用符内を除外するために、行単位で判定 + for line in output.split('\n'): + line_lower = line.lower() + # 引用符内のテキストを除外 + if '"patch is incorrect"' in line_lower or "'patch is incorrect'" in line_lower: + continue + if '("patch is incorrect")' in line_lower: + continue + + if "patch is incorrect" in line_lower: + result["verdict"] = "incorrect" + result["is_incorrect"] = True + break + elif "patch is correct" in line_lower: + result["verdict"] = "correct" + break + + # confidence を抽出(様々なフォーマットに対応) + confidence_patterns = [ + r"confidence[:\s]+([0-9]+(?:\.[0-9]+)?)", + r"confidence[:\s]+([0-9]+(?:\.[0-9]+)?)\s*/\s*1", + r"([0-9]+(?:\.[0-9]+)?)\s*/\s*1", + ] + for pattern in confidence_patterns: + match = re.search(pattern, output_lower) + if match: + try: + result["confidence"] = float(match.group(1)) + break + except ValueError: + pass + + # 問題点を抽出(行番号を含む行を検出) + issue_pattern = r"[-•]\s*(.+?(?:line|\.(?:py|js|ts|tsx|md|json|yml|yaml))[^\n]*)" + issues = re.findall(issue_pattern, output, re.IGNORECASE) + result["issues"] = issues[:5] # 最大5件 + + return result + + +def run_codex_review() -> str: """Codexによるレビューを実行""" print("", file=sys.stderr) print("## 🤖 Codex Review", file=sys.stderr) @@ -91,20 +168,24 @@ def run_codex_review(): timeout=600 ) - if result.stdout: - print(result.stdout, file=sys.stderr) + output = result.stdout or "" + if output: + print(output, file=sys.stderr) if result.returncode != 0 and result.stderr: - # エラー出力の先頭部分のみ表示 print(f"⚠️ Codexエラー: {result.stderr[:300]}", file=sys.stderr) + return output + except subprocess.TimeoutExpired: print("⚠️ Codexレビューがタイムアウトしました(10分)", file=sys.stderr) + return "" except Exception as e: print(f"⚠️ Codexレビュー実行エラー: {e}", file=sys.stderr) + return "" -def run_gemini_review(): +def run_gemini_review() -> str: """Geminiによるレビューを実行(diffをstdinで渡す)""" print("", file=sys.stderr) print("## ✨ Gemini Review", file=sys.stderr) @@ -122,7 +203,7 @@ def run_gemini_review(): if not merge_base: print("⚠️ マージベースの取得に失敗しました", file=sys.stderr) - return + return "" # diffを取得 diff_result = subprocess.run( @@ -135,7 +216,7 @@ def run_gemini_review(): if not diff_content: print("⚠️ diffが空です", file=sys.stderr) - return + return "" # Gemini用のプロンプト(diffを含める) gemini_prompt = f"""You are acting as a reviewer for a proposed code change. @@ -159,29 +240,116 @@ def run_gemini_review(): 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) + output = result.stdout or "" + if output: + print(output, file=sys.stderr) + + # returncode が 0 でない場合はエラーとして扱う + # ただし、stdout に有効な出力がある場合は警告のみ + if result.returncode != 0: + stderr_content = result.stderr.strip() if result.stderr else "" + + # 既知の警告パターン(致命的でないもの) + warning_patterns = [ + "hook registry initialized", + "failed to connect to ide", + "extension is running" + ] + is_only_warning = stderr_content and all( + any(pattern in line.lower() for pattern in warning_patterns) + for line in stderr_content.split('\n') if line.strip() + ) + + if is_only_warning and output: + # 既知の警告のみで、かつ有効な出力がある場合はスキップ + pass + elif stderr_content: + # stderr に内容がある場合は最初の意味のある行を表示 + first_line = next( + (line.strip() for line in stderr_content.split('\n') if line.strip()), + stderr_content[:100] + ) + print(f"⚠️ Geminiエラー (exit {result.returncode}): {first_line[:300]}", file=sys.stderr) + else: + # stderr が空の場合 + print(f"⚠️ Geminiエラー: 終了コード {result.returncode}", file=sys.stderr) + + return output except subprocess.TimeoutExpired: print("⚠️ Geminiレビューがタイムアウトしました(10分)", file=sys.stderr) + return "" except Exception as e: print(f"⚠️ Geminiレビュー実行エラー: {e}", file=sys.stderr) + return "" -# 利用可能なツールでレビューを実行 +print("", file=sys.stderr) +print("=" * 60, file=sys.stderr) +print("🔍 PR作成完了。AIレビューを実行中...", file=sys.stderr) +print("=" * 60, file=sys.stderr) + +# 利用可能なツールでレビューを実行し、結果を収集 if has_codex: - run_codex_review() + codex_output = run_codex_review() + codex_result = parse_verdict(codex_output) + codex_result["reviewer"] = "Codex" + review_results.append(codex_result) if has_gemini: - run_gemini_review() + gemini_output = run_gemini_review() + gemini_result = parse_verdict(gemini_output) + gemini_result["reviewer"] = "Gemini" + review_results.append(gemini_result) + +# レビュー結果の解析 +incorrect_reviews = [r for r in review_results if r["is_incorrect"]] +has_critical_issues = len(incorrect_reviews) > 0 print("", file=sys.stderr) print("=" * 60, file=sys.stderr) -print("✅ AIレビュー完了", file=sys.stderr) + +if has_critical_issues: + print("🚨 クリティカルな問題が検出されました!", file=sys.stderr) + print("=" * 60, file=sys.stderr) + print("", file=sys.stderr) + + for review in incorrect_reviews: + reviewer = review.get("reviewer", "Unknown") + confidence = review.get("confidence") + confidence_str = f" (confidence: {confidence})" if confidence else "" + print(f"❌ {reviewer}: patch is incorrect{confidence_str}", file=sys.stderr) + + if review.get("issues"): + print(" 主な指摘事項:", file=sys.stderr) + for issue in review["issues"][:3]: + print(f" • {issue[:100]}", file=sys.stderr) + + print("", file=sys.stderr) + print("─" * 60, file=sys.stderr) + print("⚠️ 対応が必要です:", file=sys.stderr) + print(" 1. 上記の指摘事項を確認してください", file=sys.stderr) + print(" 2. 必要に応じてコードを修正してください", file=sys.stderr) + print(" 3. 修正後、PRを更新してください", file=sys.stderr) + print("─" * 60, file=sys.stderr) +else: + print("✅ AIレビュー完了", file=sys.stderr) + + # 成功した場合も verdict サマリーを表示 + for review in review_results: + reviewer = review.get("reviewer", "Unknown") + verdict = review.get("verdict", "unknown") + confidence = review.get("confidence") + confidence_str = f" (confidence: {confidence})" if confidence else "" + + if verdict == "correct": + print(f" ✓ {reviewer}: patch is correct{confidence_str}", file=sys.stderr) + elif verdict: + print(f" ? {reviewer}: {verdict}{confidence_str}", file=sys.stderr) + print("=" * 60, file=sys.stderr) # PostToolUseフックは常に成功で終了(ブロックしない) +# ※ PR は既に作成されているため、ブロックしても意味がない +# 代わりに警告メッセージで対応を促す sys.exit(0)