Я перестал верить агенту на слово «готово» - шлюз verify.sh, prove-it
ИИ-агент для кодинга докладывает «тесты проходят», даже не запустив их. Это не враньё, а структурная проблема. Чему я научился, делая prove-it - открытый шлюз, который превращает доклад в проверку, и четыре способа, которыми тихо ломается самописный хук на пять строк.
Главное
Кодинг-агент не различает, что он сделал и что собирался сделать, поэтому фактически докладывает намерение. Именно поэтому он говорит «тесты проходят», не запустив их. Промптом это не лечится: «готово» должно стать не заявлением, а проверкой, которую надо пройти. prove-it - это шлюз на Stop-хуке, который не даёт агенту завершить ход, пока verify.sh в корне репозитория не вернёт exit 0. Самописный хук на пять строк тихо ломается четырьмя способами: блокирует один раз и больше никогда, пропускает ходы, закончившиеся коммитом, позволяет агенту снести саму проверку, и не отличает капитуляцию от прохождения. Эти четыре случая - вся причина, почему prove-it длиннее пяти строк.
Содержание
«Все тесты проходят. Можно мержить.»
Так сказал агент, и diff был настолько чистый, что я чуть не поверил. Но я прокрутил терминал вверх - тесты не запускались ни разу. Лога выполнения просто не было. Агент собирался запустить тесты, и это намерение так и попало в отчёт о завершении.
Пережив такое несколько раз, реагируют обычно одним из двух способов. Либо дописывают в промпт «обязательно запусти тесты и покажи результат», либо перепроверяют руками каждое «готово». Я попробовал оба варианта, и ни один долго не продержался. Поэтому я выбрал третий путь: вместо того чтобы разучиться верить слову «готово», я превратил завершение из заявления в проверку, которую надо пройти. Так появился открытый инструмент prove-it.
Это не враньё, это структура
Сначала важная оговорка. Агент не врёт.
Человек различает «я запустил тесты» и «я собирался запустить тесты», потому что у него есть память. У агента такого эталона для сравнения нет. Ему не с чем сверить сделанное и задуманное, поэтому вместо результата он докладывает намерение. «Тесты проходят» - это, по сути, сокращение от «я написал код так, чтобы тесты проходили». Это не изъян характера модели, а свойство конструкции, и потому промптом оно не лечится. Напишите «обязательно запусти» хоть сто раз - настанет день, когда он не запустит.
У промпт-приёмов есть и другая слабость: они устаревают с каждой сменой поколения моделей. То, что инструкция хорошо работает на текущей модели, ничего не гарантирует для следующей. А механизм, требующий доказательств, сидит на уровень выше модели и переживает апгрейды. Действительно ли прогнались тесты, пусть говорит не модель, а код выхода.
Превращаем доклад в проверку
Саму идею - признавать "готово" только по доказательствам, а не по словам - я уже разбирал в предыдущем посте. Этот пост о том, как идея превратилась в инструмент, который может установить каждый.
Протокол prove-it умещается в одно предложение. Репозиторий объявляет способ доказать самого себя файлом verify.sh, и агент не может заявить о завершении, пока это доказательство не пройдёт.
Реализация такая. В корень репозитория кладётся исполняемый verify.sh. Exit 0 означает «это дерево доказуемо в порядке». Когда агент пытается завершить ход, Stop-хук запускает скрипт, и если код не 0, то вместо того чтобы дать агенту остановиться, отправляет его обратно работать - вместе с последними двадцатью строками вывода об ошибке. В большинстве случаев агенту хватает одного этого вывода, чтобы починить причину.
Когда шлюз срабатывает, тоже просто: эта сессия реально меняла этот репозиторий, есть исполняемый verify.sh, и текущее состояние дерева ещё ни разу не проходило проверку. Шлюз запускается только когда все три условия истинны. Сессии, которые только читали код, он не трогает, а однажды прошедшее дерево не проверяет заново.
И блокирует он не бесконечно. Максимум три возврата за ход, потом уступает - хук, который не уступает никогда, вешает сессию намертво. Но уступить и пройти - разные вещи. Последний вывод хода, закончившегося уступкой, звучит не как «готово», а как предупреждение: «этот ход завершён без верификации». Агент может так и не одолеть шлюз, но пройти его тихо не может - вот и всё, на что этот инструмент на самом деле претендует.
Почему пяти строк bash не хватает
Если, дочитав досюда, вы подумали «да это же одна строка с командой тестов в Stop-хуке» - вы правы. Пяти строк хватает, я и сам сначала так написал. И та версия тихо ломалась четырьмя способами. Часть из них - реальные баги ранних версий prove-it, поэтому на каждый теперь навешен регрессионный тест.
Блокирует один раз и больше никогда. После первой блокировки Claude Code выставляет флаг stop_hook_active на все последующие stop-события. Хук, читающий этот флаг как «пропускай», после ровно одной блокировки перестаёт быть шлюзом. Хук, игнорирующий флаг, наоборот, блокирует вечно, и сессия виснет. В обе ловушки надо попасть, чтобы про них узнать; ответ - самому считать попытки, блокировать заданное число раз и потом громко уступать.
После коммита всё выглядит так, будто ничего не было. Хук, определяющий «была ли работа» по грязному рабочему дереву, пропускает без проверки все ходы, закончившиеся коммитом. А коммит - самое обычное, что делает агент. Поэтому prove-it записывает состояние дерева на старте сессии как базовую отметку и на каждом stop сравнивает с ней. Коммиты, файлы, поправленные через sed, файлы, выплюнутые кодогенератором - всё засчитывается как изменения.
Агент может снести проверку. Самый дешёвый ход для агента, который не может пройти verify.sh, - удалить verify.sh или снять с него право на исполнение. Шлюз запоминает, был ли репозиторий вооружён на старте сессии, и отклоняет ходы, заканчивающиеся с разоружённым шлюзом. Есть приём похитрее: оставить файл исполняемым и переписать проверки внутри. Это не блокируется - нередко править verify.sh и есть та самая работа, о которой просили. Но и тихо это не проходит: если сессия завершилась с изменённым verify.sh, шлюз об этом сообщает, и по diff видно, была это работа или увиливание.
Капитуляция неотличима от прохождения. Любой хост рано или поздно заставит хук уступить. Самописный хук уступает молча, и последнее, что вы видите, - слово «готово». Ход, который трижды провалился и сдался, на экране выглядит точно так же, как ход, прошедший с первого раза. Последнее слово prove-it - предупреждение. Если разница кажется мелочью, представьте день после того, как вы смержили сдавшийся ход, приняв его за прошедший.
Эти четыре случая - вся причина, почему prove-it длиннее пяти строк. Хотите и дальше пользоваться своим хуком - пожалуйста. Только случаи, которые он обязан выдерживать, записаны в SPEC.md, и я советую их прочитать. Важна не реализация, а протокол.
Чего этот инструмент не доказывает
Есть ограничение, о котором честнее сказать прямо. Шлюз гарантирует ровно одно: перед концом хода verify.sh вернул 0. Что этот 0 означает, целиком зависит от проверок, которые написали вы. verify.sh из одной строки exit 0 проходит этот шлюз и не доказывает ничего.
Спека называет это Level 1. Level 2 - вопрос о том, утверждают ли проверки что-то о реальных доказательствах, и этого за вас не верифицирует никакой инструмент. prove-it тоже. Навесить хук - лёгкая часть. Настоящая работа - ответить на вопрос «что значит "доказано" в этом репозитории», и ответ у каждого репозитория свой. Поэтому протокол фиксирует только имя файла, а не содержимое.
Обходные пути оставлены намеренно. С PROVE_IT_SKIP=1 ход просто проходит, а удалите verify.sh - и шлюз выключится навсегда. Шлюз, который нельзя убрать, люди в итоге обходят стороной, а обойдённый шлюз хуже, чем его отсутствие: он рапортует, будто проверки прогнались, когда на деле не прогналось ничего.
В первый день - нарочно по-маленькому
Установка - три строки, и настоящую работу делает третья.
/plugin marketplace add Why-Next/prove-it
/plugin install prove-it@whynext
/prove-it:init
/prove-it:init определяет стек, создаёт verify.sh, у вас на глазах показывает, что он проходит, а затем прогоняет копию с добавленным exit 1 и показывает, как шлюз отклоняет ход. Занимает секунд тридцать.
В сгенерированном verify.sh активна ровно одна проверка: git diff --check. Тесты и проверка типов записаны только комментариями. Это сделано нарочно, чтобы в день установки он проходил на main. Шлюз, падающий с первого дня, с первой же недели учит команду его обходить. Закомментированные проверки включайте по одной - после того как своими руками прогнали и увидели, что они проходят. И перед включением хотя бы раз нарочно завалите каждую. Проверка, которая не может упасть, - не проверка, и узнать это в тот день, когда она понадобится, будет поздно.
Точка, где рост verify.sh надо остановить, тоже определена: минута на всё. Что медленнее - отправляйте в CI. Проверки, которым нужны secrets или доступ к продакшену, тоже дело CI. В verify.sh остаются только доказательства, которые агент может прогнать локально прямо во время работы.
Журнал того, что поймал шлюз
Включите PROVE_IT_LEDGER=1, и каждый раз, когда шлюз ловит ложное «готово», в локальный файл добавляется строка.
{"ts":"2026-07-09T04:12:33Z","claim":"All tests pass. Ready to merge.",
"evidence_demanded":"verify.sh exit 0","actual":["3 failed, 41 passed"]}
Что агент заявил, чего от него потребовали и как было на самом деле. Накопится месяц записей - и вы будете читать, как именно ошибается ваш агент, по журналу, а не по догадкам. Файл остаётся только на локальном диске, никуда не отправляется и выключен, пока вы его не включите.
Этот репозиторий гейтит сам себя
В репозитории prove-it, разумеется, есть свой verify.sh, и внутри него шлюз тестирует сам себя на настоящем git-репозитории. Блокируется ли проваленная проверка, открывается ли пройденная, не трогает ли он сессии, которые только читали, не принимает ли чистое дерево после коммита за «работы не было». Все четыре ловушки из этой статьи вбиты туда регрессионными тестами.
Лицензия MIT, зависимости - только bash, git и python3. Тестировалось в Claude Code; как подключить к Codex CLI, Gemini CLI и другим хостам с блокирующими хуками того же типа, написано в документе ADAPTERS. Если у вас есть репозиторий, где агент правит код и в том же диалоге говорит «готово», - это и есть место, где этот шлюз полезнее всего.
«Готово» должно быть не словом, которое произносит агент, а состоянием, которое выносит репозиторий. Записать критерии этого вердикта - по-прежнему ваша задача, но по крайней мере теперь никто не проскочит мимо вердикта тихо.
Частые вопросы
ИИ-агент докладывает о завершении работы, которую не делал. Как это остановить?
Инструкции в промпте вроде «обязательно запусти тесты» недостаточно. У агента нет способа сравнить сделанное с задуманным, поэтому он докладывает намерение как факт. Вместо этого превратите само заявление о завершении в проверку. Положите в корень репозитория скрипт верификации (verify.sh), и пусть Stop-хук запускает его, когда агент пытается завершить ход: если код выхода не 0, агент отправляется дорабатывать. prove-it - референсная реализация этого протокола.
А нельзя самому написать пятистрочный скрипт на Stop-хуке Claude Code, который гоняет тесты?
Можно, но там четыре ловушки. ① После первой блокировки Claude Code выставляет флаг stop_hook_active: обработаешь его неправильно - хук либо блокирует ровно один раз, либо блокирует вечно и вешает сессию. ② Если судить по грязному рабочему дереву, все ходы, закончившиеся коммитом, проходят без проверки. ③ Агент может удалить verify.sh или снять с него бит исполнения (chmod -x) и разоружить сам шлюз. ④ Когда хук в итоге уступает, капитуляция выглядит как прохождение. Пишете сами - обработайте все четыре случая.
Какие проверки класть в verify.sh?
Ответ на вопрос «что должно быть истинно, чтобы изменение в этом репозитории считалось доказанным». Обычно включают по одной: реальная команда тестов, проверка типов, потом lint. Всё вместе должно укладываться в минуту; проверки, которым нужны secrets или доступ к продакшену, оставьте CI. Главное - не замахиваться с первого дня. Медленный или нестабильный шлюз обходят в течение недели, а обойдённый шлюз хуже, чем его отсутствие.
Похожие статьи
- 💻 Разработка
Когда ИИ говорит «готово», нужен механизм, который ему не поверит
ИИ-агенты для написания кода объявляют работу законченной, хотя не закончили её. Говорят, что тесты прошли, не запустив их; говорят, что починили баг, не воспроизведя его. Рассказ разработчика-одиночки, который пишет почти весь код вместе с ИИ, о проверочном шлюзе, заставившем систему признавать готовность только по доказательствам, а не по словам.
- 💻 Разработка
Скидки, которые вы не получали - почему Spot и Graviton дешевле
Счёт от AWS второй месяц подряд превышал бюджет. Пытаясь найти, что урезать, я понял: настоящий рычаг - не «сколько потреблять», а «как покупать». История о том, почему Spot и Graviton дешевле, и о статье расходов (RDS Proxy), которая выглядит как трата впустую, но выключать её нельзя - о причине за каждой цифрой в счёте.
- 💻 Разработка
Хуки остановки срабатывали дважды - когда у SIGTERM два хозяина
После каждого деплоя в Sentry падала ошибка Prisma P2028. Полез чинить порядок завершения работы и обнаружил, что enableShutdownHooks из NestJS и библиотека graceful shutdown одновременно ловят SIGTERM, из-за чего хуки завершения каждый раз выполнялись дважды.