соло · автоматизация

Подключите ревью на хук: быстрое на коммите, полное на пуше

Гонять полное ревью на каждом коммите — плохая мысль, и это видно по арифметике: на наших прогонах судья съедает около половины цены. Поэтому у двух хуков разные задачи. Хук коммита дешёвый и грубый — одна модель, без судьи, секунды — и останавливает он только за то, что вы вообще не захотели бы вносить в историю. Хук пуша строгий, с судьёй, и стоит там, где код уходит с вашей машины.

Ниже — как поставить обе ступени, какой код возврата блокирует, а какой пропускает, и почему без явного порога хук не блокирует вообще ничего.

Перед началом: нужен работающий в терминале reviewgate и доступ к модели (свой API-ключ, корпоративный шлюз или локальная модель). Если первого ревью ещё не было, начните с первого ревью на своей машине.

Что на самом деле выключает --fast

Это не «взгляд помельче». --fast меняет состав ролей: из всех генераторов остаётся только первый, панель судей обнуляется, арбитр снимается. Вместе с судьями выключаются и стадии, которые на них держатся: судейский проход, сбор полных файлов для судьи, перепроверка готовых фиксов и линза вопросов — та, что превращает часть снятых замечаний в вопросы к автору.

На наших прогонах это выглядело так: чистая проверка на коммите, одна правка в README, — 4 секунды и ≈ 0,01 $. Ветка целиком на пуше, два файла и 31 строка против main, остановленная судьёй, — около двух минут и ≈ 0,26 $. Сколько стоит сам судья, замерено отдельно, на одном и том же диффе: около половины счёта. Это соотношение и есть весь аргумент в пользу двух ступеней вместо одной.

Шаг 1. Хук коммита: дешёвый, грубый, редко мешает

терминал
# .husky/pre-commit
# free checks first — uncomment what you run:
# npx lint-staged
# npm test

# hooks should run on one agreed personal config, not on each developer's own.
# Applied only if the file exists, so a typo cannot disable the review silently:
# CFG="$HOME/.config/reviewgate/config.hooks.yml"
# [ -z "${REVIEWGATE_CONFIG:-}" ] && [ -f "$CFG" ] && export REVIEWGATE_CONFIG="$CFG"

command -v reviewgate >/dev/null 2>&1 || {
  echo "reviewgate: not installed — commit allowed unchecked" >&2
  exit 0
}

code=0
reviewgate review --staged --fast --fail-on blocker || code=$?
case $code in
  0) ;;
  2)
    echo "reviewgate: blocking findings — commit stopped" \
         "(bypass: git commit --no-verify)" >&2
    exit 1
    ;;
  *)
    echo "reviewgate: the review did not run (code $code)" \
         "— commit allowed unchecked" >&2
    ;;
esac
exit 0

Здесь четыре решения. От них зависит, приживётся ли хук или вы снесёте его через неделю.

Важно:

  • --fail-on blocker, а не major. Черновой коммит не должен становиться заложником замечания о стиле. На этом пороге ревью по-прежнему показывает всё найденное — 🟠 major вы увидите, — но останавливает только за то, чего вы вообще не захотели бы иметь в истории: секрет или учётные данные в диффе и безвозвратное уничтожение данных. Это ровно те два класса, которые продукт считает безусловно блокирующими; всё остальное, включая сломанную миграцию и снятую проверку доступа, приходит уровнем 🔴 critical и коммит не останавливает.
  • --fail-on вообще. Без него хук не блокирует никогда. Гейт по умолчанию выключен, поэтому ревью выходит с нулём при любом числе замечаний — а хук, увидев ноль, пропускает коммит. Так и получается хук, который замечания печатает, но ничего не останавливает.
  • Код возврата ловите сразу: || code=$?. husky запускает файл хука командой sh -e, то есть оболочка падает на первой же неуспешной команде. Значит, строка с ревью, вернувшая ненулевой код, обрывает хук целиком, и написанный следом case $? не выполняется вовсе: вы не увидите ни своего сообщения, ни разбора кодов.
  • Сначала бесплатные проверки, потом ревью. Линтер, форматтер и тесты детерминированы и ничего не стоят, а sh -e оборвёт хук на первом же падении, и модель не будет вызвана зря. || code=$? нужен только строке с ревью — остальные команды могут просто упасть.

Шаг 2. Хук пуша: строгий

терминал
# .husky/pre-push
# free checks first — uncomment what you run:
# npm run lint
# npm test

# hooks should run on one agreed personal config, not on each developer's own.
# Applied only if the file exists, so a typo cannot disable the review silently:
# CFG="$HOME/.config/reviewgate/config.hooks.yml"
# [ -z "${REVIEWGATE_CONFIG:-}" ] && [ -f "$CFG" ] && export REVIEWGATE_CONFIG="$CFG"

command -v reviewgate >/dev/null 2>&1 || {
  echo "reviewgate: not installed — push allowed unchecked" >&2
  exit 0
}

code=0
reviewgate review --refs origin/main --fail-on major || code=$?
case $code in
  0) ;;
  2)
    echo "reviewgate: blocking findings — push stopped" \
         "(bypass: git push --no-verify)" >&2
    exit 1
    ;;
  *)
    echo "reviewgate: the review did not run (code $code)" \
         "— push allowed unchecked" >&2
    ;;
esac
exit 0

Та же форма, три отличия: вся ветка вместо индекса, судья вместо одной модели и major вместо blocker — потому что здесь код перестаёт быть только вашим.

блокирует только код возврата 2Любой другой ненулевой код означает, что не сработало само ревью — нет сети, нет ключа, битый конфиг, — и хук пропускает вас со строкой в stderr. Инструмент ревью, который при поломке запирает вас в собственной ветке, хуже, чем его отсутствие. Размен честный, и о нём стоит знать: эту строку в stderr легко не заметить.

Как это выглядит в терминале

Обе ветки открыты в нашем учебном репозитории: та, где токен, — на GitLab и GitHub, та, где скидка, — на GitLab и GitHub.

Разработчик подключает письмо о счёте к почтовому провайдеру и кладёт токен прямо в исходник. Хук коммита не пускает это в историю:

терминал
⛔ src/notifications/notifications.service.ts:18 — В код закоммичен реальный (судя по
   формату mrt_live_...) секретный токен провайдера доставки писем. Такой секрет
   попадает в историю git и виден всем, у кого есть доступ к репозиторию — это утечка
   учётных данных.
   Немедленно отозвать/сменить этот токен у провайдера, вынести адрес и токен в
   конфигурацию через ConfigService (NestJS) вместо хардкода в модуле.

❌ Гейт (blocker) НЕ ПРОЙДЕН. Замечаний: 2 (⛔ 1 · 🔴 0 · 🟠 1 · 🔵 0 · ⚪ 0).
⚙️ Стоимость прогона: ≈ 0,07 $
reviewgate: blocking findings — commit stopped (bypass: git commit --no-verify)
husky - pre-commit script failed (code 1)

0,07 $ и около сорока секунд. Обратите внимание на счётчики: замечание 🟠 major было тоже показано — и ничего не остановило, потому что порог здесь blocker. В этом и смысл низкой планки на коммите: видно всё, а останавливают редко и только за блокирующее.

Разработчик выносит токен в окружение и коммитит снова:

терминал
✅ Гейт (blocker) пройден. Замечаний: 3 (⛔ 0 · 🔴 0 · 🟠 2 · 🔵 1 · ⚪ 0).
⚙️ Стоимость прогона: ≈ 0,07 $
[feat/email-delivery 6c5bf3e] fix: take the delivery credentials from the environment

Блокирующее замечание ушло вместе с токеном, и коммит проходит. Но замечаний стало не меньше, а больше: было одно 🟠 major, теперь два 🟠 major и одно 🔵 minor — дифф изменился, а модель недетерминирована, и второй прогон не повторяет первый. Ничего не замели под ковёр: все три на экране, просто это не повод блокировать черновик.

Тот же репозиторий, другая ветка — та, где скидку по купону посчитали руками вместо общего денежного хелпера:

терминал
❌ Гейт (major) НЕ ПРОЙДЕН. Замечаний: 3 (⛔ 0 · 🔴 2 · 🟠 1 · 🔵 0 · ⚪ 0).
⚙️ Стоимость прогона: ≈ 0,26 $
reviewgate: blocking findings — push stopped (bypass: git push --no-verify)
husky - pre-push script failed (code 1)
error: failed to push some refs

0,26 $ — и ветка остаётся на машине, пока автор не поправит то, на что указали замечания 🔴 critical про деньги.

почему вторая ступень ловит не всёВ этой цепочке есть вероятностное звено. Модель не повторяет саму себя: два прогона по одному и тому же коду дают разные наборы замечаний. Поэтому две ступени — не сито со всё более мелкой ячейкой: область у них разная (индекс против всей ветки), и полный прогон на пуше вполне может найти меньше, чем быстрый на коммите — на своих прогонах мы это видели. Смысл лесенки в другом: на коммите вас дёшево останавливают на самом грубом, на пуше проверяют строже. Гарантии, что вторая ступень подберёт всё, что пропустила первая, нет.

Во сколько обходится лесенка

Наши прогоны, тот же репозиторий, 7 сентября 2026; цены по прайс-листу Anthropic на 23 сентября 2026 года. Цена зависит и от кэша: промпт кэшируется на час, и первый прогон в течение часа дороже следующих.

СтупеньОбластьВремяЦена
коммит, чистоиндекс, правка в README4 с≈ 0,01 $
коммит, есть замечанияиндекс, один файл~40 с≈ 0,07 $
пуш, чистоветка с правкой README против main3 с≈ 0,01 $
пуш, остановленветка со скидкой против main~120 с≈ 0,26 $

Четыре вещи, которые ломают лесенку

Lock-файл значительно увеличит ваш счёт. Наш самый первый прогон хука стоил 0,33 $ на диффе в 37 строк, потому что pnpm-lock.yaml уехал в контекст модели целиком: 143 962 байта из 146 179, которые модель прочитала, и это вылилось в 96 тысяч токенов входа ради четырёх изменённых файлов — файл из хешей упаковывается в токены куда плотнее прозы. С lock-файлом в ignore тот же коммит обошёлся в 0,01 $: более чем в двадцать раз дешевле и в десять раз быстрее. Кладите lock-файлы, генерированный код и фикстуры в ignore до того, как поставите хук на коммит, а не после.

git push --dry-run запускает хук. Мы проверили: сухой прогон вызывает pre-push ровно так же, как настоящий, поэтому «просто посмотреть, что будет» стоит полного ревью с судьёй. Подавляется это флагом --no-verify.

Личный конфиг разработчика побеждает командный. Домашняя схема заменяет роли команды, а не дополняет их: коллега, у которого в личном конфиге другой провайдер, прогонит командное ревью на своей модели — а если ключ не подойдёт, получит упавший прогон, который хук пропустит — со строкой в stderr, которую легко не заметить. Если хуки обязаны идти на командной схеме, задайте прямо в хуке переменную REVIEWGATE_CONFIG с путём к нужному файлу: она перебивает домашний конфиг. Так сделан и наш собственный хук — и путь там подставляется, только если файл существует: если файла нет, ReviewGate решит, что личного конфига нет вовсе, прогон упадёт на llm_unavailable, а хук пропустит вас дальше.

--no-verify — штатный выход, а не взлом. Обойти оба хука может кто угодно, и будут обходить: на хотфиксе в полночь, на откате, на ветке, которая и так сломана. Локальные хуки работают ровно до тех пор, пока их не обходят. Проверка, которую обойти нельзя, стоит на стороне площадки — это бот в merge request или pull request, и подключается он отдельно.

Когда что-то пошло не так

Хук ничего не блокирует. Почти всегда — забытый --fail-on: гейт по умолчанию выключен, ревью выходит с нулём при любых замечаниях, а хук получает нулевой код и пропускает коммит. Проверить: выполните ту же команду руками и посмотрите код возврата — заблокированный прогон даёт 2:

терминал
reviewgate review --staged --fast --fail-on blocker; echo $?

Хук не запускается вовсе — ни вывода, ни задержки. Git про него не знает: путь к хукам прописывает husky при установке, и только там, где отработал prepare. На свежем клоне их ставит на место pnpm install. Проверить: git config core.hooksPath должен показать .husky/_.

Коммит думает минутами. В контекст модели уезжает что-то большое — чаще всего lock-файл, снапшот или генерируемый код. Посмотрите в выводе прогона строку Judge context: там названо число файлов и символов. Лечится это ignore.

В stderr написано «the review did not run (code N)». Сломалось само ревью — нет ключа, нет сети, конфиг не читается, — и хук намеренно вас пропустил. Ваш коммит или пуш ушёл непроверенным. Запустите команду руками, чтобы увидеть настоящую ошибку: код возврата назовёт класс сбоя.

Коммит, который делает ИИ-агент, обрывается по таймауту. Агенты запускают команды со своим лимитом времени, и полное ревью в него может не уложиться. Тогда коммита не будет вовсе: изменения останутся в индексе, а начатый прогон продолжится в фоне, и вы за него заплатите. Дальше агент, увидев ошибку, легко повторит коммит с --no-verify — вот тогда код уйдёт непроверенным. Поэтому на коммите и держат --fast: он укладывается в секунды.

Дальше