Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) を参照してください。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

リンクの相対パスが誤っている可能性があります。

.claude/CLAUDE.md からの相対リンクで ./.claude/hooks/README.md を指定すると .claude/.claude/hooks/README.md に解決され、リンク切れになります。./hooks/README.md に修正してください。

✅ 修正案
-詳細は [.claude/hooks/README.md](./.claude/hooks/README.md) を参照してください。
+詳細は [.claude/hooks/README.md](./hooks/README.md) を参照してください。
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
詳細は [.claude/hooks/README.md](./.claude/hooks/README.md) を参照してください。
詳細は [.claude/hooks/README.md](./hooks/README.md) を参照してください。
🤖 Prompt for AI Agents
In @.claude/CLAUDE.md at line 140, `.claude/CLAUDE.md` 内の相対リンクが誤っており現在
"./.claude/hooks/README.md"
に記載されているためリンク先が二重になる(".claude/.claude/hooks/README.md")ので、該当リンク文字列
"./.claude/hooks/README.md" を正しい相対パス "./hooks/README.md" に置き換えてリンク切れを修正してください。

18 changes: 16 additions & 2 deletions .claude/hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
208 changes: 188 additions & 20 deletions .claude/hooks/post_pr_ai_review.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

gh pr create 成功後に自動的にCodexとGeminiによるコードレビューを実行します。
インストールされているツールのみ実行されます。
クリティカルな問題(patch is incorrect)が検出された場合は警告を表示します。
"""
import sys
import json
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -122,7 +203,7 @@ def run_gemini_review():

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

# diffを取得
diff_result = subprocess.run(
Expand All @@ -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.
Expand All @@ -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)