|
| 1 | +#!/bin/sh |
| 2 | +set -eu |
| 3 | + |
| 4 | +remote=$1 |
| 5 | +schema=$(dirname "$0")/review-result.schema.json |
| 6 | +result_dir= |
| 7 | + |
| 8 | +trap 'if [ -n "$result_dir" ]; then rm -rf "$result_dir"; fi' 0 |
| 9 | +trap 'exit 1' HUP INT TERM |
| 10 | + |
| 11 | +while read -r local_ref local_sha remote_ref remote_sha |
| 12 | +do |
| 13 | + case "$remote_ref" in |
| 14 | + refs/heads/*) ;; |
| 15 | + *) continue ;; |
| 16 | + esac |
| 17 | + |
| 18 | + case "$local_sha" in |
| 19 | + *[!0]*) ;; |
| 20 | + *) continue ;; |
| 21 | + esac |
| 22 | + |
| 23 | + if [ "$local_sha" = "$remote_sha" ]; then |
| 24 | + continue |
| 25 | + fi |
| 26 | + |
| 27 | + case "$remote_sha" in |
| 28 | + *[!0]*) |
| 29 | + base=$remote_sha |
| 30 | + if ! git cat-file -e "$base^{commit}" 2>/dev/null; then |
| 31 | + printf '%s\n' "리뷰 기준 커밋을 찾을 수 없습니다. git fetch $remote 실행 후 다시 푸시하세요." >&2 |
| 32 | + exit 1 |
| 33 | + fi |
| 34 | + ;; |
| 35 | + *) |
| 36 | + if ! base_ref=$(git symbolic-ref --quiet "refs/remotes/$remote/HEAD"); then |
| 37 | + printf '%s\n' "원격 기본 브랜치를 찾을 수 없습니다. git remote set-head $remote -a 실행 후 다시 푸시하세요." >&2 |
| 38 | + exit 1 |
| 39 | + fi |
| 40 | + if ! base=$(git merge-base "$base_ref" "$local_sha"); then |
| 41 | + printf '%s\n' '리뷰 기준이 될 공통 조상 커밋을 찾을 수 없습니다.' >&2 |
| 42 | + exit 1 |
| 43 | + fi |
| 44 | + ;; |
| 45 | + esac |
| 46 | + |
| 47 | + if git diff --quiet "$base" "$local_sha" --; then |
| 48 | + continue |
| 49 | + else |
| 50 | + status=$? |
| 51 | + if [ "$status" -ne 1 ]; then |
| 52 | + exit "$status" |
| 53 | + fi |
| 54 | + fi |
| 55 | + |
| 56 | + if ! command -v claude >/dev/null 2>&1; then |
| 57 | + printf '%s\n' 'Claude Code CLI를 찾을 수 없습니다. 설치와 로그인을 확인하세요.' >&2 |
| 58 | + exit 1 |
| 59 | + fi |
| 60 | + if ! command -v python3 >/dev/null 2>&1; then |
| 61 | + printf '%s\n' '리뷰 결과 판독에 필요한 python3를 찾을 수 없습니다.' >&2 |
| 62 | + exit 1 |
| 63 | + fi |
| 64 | + if [ -z "$result_dir" ]; then |
| 65 | + result_dir=$(mktemp -d "${TMPDIR:-/tmp}/devlog-pre-push.XXXXXX") |
| 66 | + fi |
| 67 | + result=$result_dir/result.json |
| 68 | + rm -f "$result" |
| 69 | + schema_json=$(cat "$schema") |
| 70 | + |
| 71 | + printf '%s\n' "Claude 리뷰 시작: $local_ref ($base..$local_sha)" >&2 |
| 72 | + if ! claude -p \ |
| 73 | + --model claude-opus-5-5 \ |
| 74 | + --effort high \ |
| 75 | + --permission-mode plan \ |
| 76 | + --permission-prompts none \ |
| 77 | + --no-session-persistence \ |
| 78 | + --output-format json \ |
| 79 | + --json-schema "$schema_json" \ |
| 80 | + <<EOF > "$result" |
| 81 | +You are code_reviewer for a user-authorized pre-push review in $(pwd). |
| 82 | +This is a separate Claude Code CLI review. Perform the review yourself without spawning subagents or other agents. |
| 83 | +
|
| 84 | +Read CLAUDE.md and fetch its DevLog Agent Policy index through the connected Notion MCP before inspecting code. Verify that the index is under the DevLog page and use only policies marked Active. Load the Active General, iOS, and Claude Code policies and every Active task-specific policy whose route matches this review. Apply the current code_reviewer contract. |
| 85 | +If Notion MCP, the policy index, any required Active policy, or required code is unavailable, set verdict to "BLOCKED" and report the missing source. Do not use historical memory as a policy fallback. |
| 86 | +
|
| 87 | +Review only the committed changes from $base to $local_sha for $local_ref being pushed to $remote_ref. |
| 88 | +Use git diff $base $local_sha -- for the exact change set. Inspect supporting files at $local_sha with git show. Do not substitute HEAD or include unrelated staged, unstaged, or untracked changes. |
| 89 | +Use the current request or issue and acceptance criteria when identifiable. Keep the review within the actual diff and applicable scope exclusions. |
| 90 | +
|
| 91 | +Inspect correctness, regressions, error and cancellation handling, concurrency and resource lifetimes, persistence and event integrity, architecture and dependency boundaries, and meaningful test coverage against existing contracts. |
| 92 | +Inspect layout that can break on compact iPhones such as mini and SE models, including hardcoded dimensions, fixed widths or heights, and fixed offsets that do not adapt to the available width, height, or Safe Area. Report a finding only with the concrete screen condition and the element that overflows, clips, or overlaps. |
| 93 | +Read only. Do not create, edit, or delete files, policies, Git state, or external records. Do not request escalation, build, execute tests, or run, launch, install, boot, or open the app or Simulator. |
| 94 | +
|
| 95 | +Return a JSON object matching the provided JSON schema, with only "verdict" and "review" fields. |
| 96 | +Set verdict to "FAIL" when an actionable defect requires correction. Set verdict to "PASS" only when no blocking finding exists and every required policy and code source was available. Set verdict to "BLOCKED" if a required source or execution setting is unavailable. |
| 97 | +In review, state the inspected scope, available verification evidence, unexecuted checks, and remaining verification limits. For PASS, explicitly state that there are no blocking findings. A PASS does not establish that tests passed or runtime behavior was verified. |
| 98 | +Write review in Korean and preserve implementation names. Include file paths and line numbers for actionable findings. |
| 99 | +EOF |
| 100 | + then |
| 101 | + printf '%s\n' 'Claude 리뷰 실행에 실패하여 푸시를 중단합니다.' >&2 |
| 102 | + exit 1 |
| 103 | + fi |
| 104 | + |
| 105 | + if ! python3 - "$result" <<'PY' |
| 106 | +import json |
| 107 | +import sys |
| 108 | +
|
| 109 | +
|
| 110 | +def unique_object(pairs): |
| 111 | + result = dict(pairs) |
| 112 | + if len(result) != len(pairs): |
| 113 | + raise ValueError("duplicate keys") |
| 114 | + return result |
| 115 | +
|
| 116 | +
|
| 117 | +try: |
| 118 | + with open(sys.argv[1], encoding="utf-8") as file: |
| 119 | + envelope = json.load(file, object_pairs_hook=unique_object) |
| 120 | + if ( |
| 121 | + not isinstance(envelope, dict) |
| 122 | + or envelope.get("type") != "result" |
| 123 | + or envelope.get("subtype") != "success" |
| 124 | + or envelope.get("is_error") is not False |
| 125 | + ): |
| 126 | + raise ValueError("unsuccessful claude result") |
| 127 | + result = envelope.get("structured_output") |
| 128 | + if result is None: |
| 129 | + text = envelope.get("result") |
| 130 | + if not isinstance(text, str): |
| 131 | + raise ValueError("missing review result") |
| 132 | + result = json.loads(text, object_pairs_hook=unique_object) |
| 133 | + if ( |
| 134 | + not isinstance(result, dict) |
| 135 | + or set(result) != {"verdict", "review"} |
| 136 | + or not isinstance(result["verdict"], str) |
| 137 | + or result["verdict"] not in {"PASS", "FAIL", "BLOCKED"} |
| 138 | + or not isinstance(result["review"], str) |
| 139 | + or not result["review"].strip() |
| 140 | + ): |
| 141 | + raise ValueError("invalid review result") |
| 142 | +except (OSError, UnicodeError, ValueError): |
| 143 | + print("리뷰 결과가 없거나 형식이 올바르지 않습니다.", file=sys.stderr) |
| 144 | + sys.exit(1) |
| 145 | +
|
| 146 | +print(result["review"], file=sys.stderr) |
| 147 | +sys.exit(0 if result["verdict"] == "PASS" else 1) |
| 148 | +PY |
| 149 | + then |
| 150 | + printf '%s\n' 'PASS 판정을 확인하지 못하여 푸시를 중단합니다.' >&2 |
| 151 | + exit 1 |
| 152 | + fi |
| 153 | +done |
0 commit comments