Агент, который писал текст, сам поставил ему «принято». Все пункты чек-листа были закрыты, статус в шапке стоял правильный. На следующий день я прочитал и отклонил работу целиком. С кодом всё устроено ровно так же: тот же агент пишет и проверяет.

Спорить пришлось не про сам файл, а про проверку. Автор и проверяющий были одним агентом с одной инструкцией, поэтому агент закрывал чек-лист и пропускал результат дальше. Формально всё сходилось. По сути проверки не было.

Harness — это обвязка вокруг модели: правила, роли, права на запись, журнал прогонов. Не текст запроса, а то, что стоит вокруг него. Я разбираю эту обвязку по частям, и здесь речь про одну часть, которая называется гейтом.

Дальше я буду говорить не «агент», а проход, потому что важно не то, сколько у вас окон чата, а сколько раз работа проходит через модель со своей инструкцией. Проходов три. Управляющий раздаёт задачи и ветвится по вердикту. Исполняющий правит рабочие файлы. Проверяющий читает только переданные пути и пишет вердикт. Контур одинаков для кода, документа и конфига.

Субагенты за последний год стали штатным механизмом в Cursor, Claude Code и Codex. Разводить проходы при этом почти никто не разводит, и получается, что один и тот же чат делает работу и он же объявляет её готовой.

Ломается в такой конструкции три вещи. Вердикт ставит тот, кто делал. Факт между шагами живёт в переписке, поэтому следующий проход узнаёт о нём с чужих слов. Два прохода правят один файл, потому что зоны никто не разделил.

Хотите сразу попробовать: идите в §«Минимальный контур на три промпта». Там три блока, которые копируются в чат целиком, файлы агент создаёт сам. Теорию можно прочитать после первого прогона.

Один гейт целиком

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

Разница с обычным чатом видна на четырёх вопросах.

Один чат

Цепочка с гейтом

кто исполняет

он же

отдельный проход

кто выносит вердикт

он же

другой проход, без памяти первого

где живёт факт между шагами

в переписке

в файле, который следующий открывает сам

что при сбое

правка тем же ходом

отказ строкой, работа возвращается автору

Правая колонка держится на трёх условиях, и каждое звучит банально, пока не посмотришь, как оно нарушено у тебя. Вердикт ставит не тот, кто делал, иначе «принято» остаётся самооценкой. Следующий проход открывает файл, а не читает пересказ, потому что в пересказ уже вложено мнение того, кто его писал. У каждого файла один хозяин, иначе два прохода правят одно место и результат потом не свести.

Если правка одна или скрипт разовый, гейт добавит только минуты и токены, а ревьюер в пул-реквесте закроет ровно то же самое. Цепочка с гейтом начинает окупаться, когда ошибка дорогая или незаметная, а шагов уже столько, что условия первого никто не помнит.

Схема: управляющий, исполняющий, артефакт на диске, проверяющий и три ветки по вердикту
Схема: управляющий, исполняющий, артефакт на диске, проверяющий и три ветки по вердикту

Минимальный контур на три промпта

Дальше идут блоки, которые копируются в чат целиком. Файлы создаёт агент, а не вы руками, и это не стилистическое предпочтение. Мой файл роли настроен под мои сбои, у вас сбои другие, поэтому скопированная роль либо не сработает, либо будет ловить воображаемое.

Промпт 1. Завести проверяющий проход. Для Cursor:

Создай субагента-ревьюера в .cursor/agents/verifier.md.

Frontmatter: name verifier; description одной фразой про момент вызова
(«проверка завершённой работы перед тем, как считать задачу закрытой»);
model тот же, что у основного прохода.

Права: проверяемые файлы не трогает, пишет только свой файл вердикта.

Промпт роли:
- читает только перечисленные во входе пути и открывает файлы сам;
- проверяет три вещи: заявленное сделано, проверки прогнаны, ничего вне
  названной зоны не тронуто;
- пишет файл вердикта: первая строка verdict: PASS или verdict: FAIL,
  ниже список того, что не сошлось, с путями;
- не правит проверяемое и не дописывает недостающее.

Никаких «в целом хорошо». Проверить нечем — FAIL с указанием, чего не хватает.

Что проверяете в ответе: агент показал созданный файл и в нём есть строка про формат вердикта. Если он написал роль общими словами вроде «внимательно проверяет качество», переспросите. По такой формулировке первая строка вердикта не появится, а вся дальнейшая развилка стоит именно на ней.

Промпт 2. Убедиться, что проверяющего вообще позовут.

Перечисли доступных субагентов с их описаниями. По задаче ничего не делай.

Что проверяете: verifier есть в списке, и его описание читается как момент вызова, а не как титул. Если проверяющего в списке нет, гейта нет и всё остальное бессмысленно, поэтому сначала чините файл роли.

Промпт 3. Прогнать задачу двумя проходами.

Задача: <одна фраза>.
Сначала выполни работу обычным проходом.
Затем отдельным обращением вызови verifier и передай ему только пути
к результату, без нашей переписки.
В ответе покажи два разных обращения и файл вердикта.

Что проверяете, и это здесь главное: в ответе видно два разных обращения. Если пришло одно, в духе «сделал и проверил», контур не собран и вы получили самопроверку под другим названием.

Дальше я открываю файл вердикта сам и смотрю первую строку. Реплике «всё ок» в чате я не верю с тех пор, как она несколько раз оказалась вежливым пересказом того, чего в файле не было. PASS — закрываю задачу. FAIL — возвращаю исполняющему проходу, и чинит он, а не проверяющий.

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

Где заводится роль: Cursor, Claude Code, Codex

Субагент — это отдельный проход со своим окном, своей инструкцией, своими инструментами и своими правами. Историю родительского чата он не видит, наверх возвращается только итог. Для гейта важно ровно одно следствие: проверяющий не знает, какими соображениями исполняющий проход оправдывал спорное место.

Cursor

Claude Code

Codex

файл

.cursor/agents/name.md

.claude/agents/name.md

.codex/agents/name.toml

формат

markdown с шапкой

markdown с шапкой

TOML

промпт роли

тело файла

тело файла

developer_instructions

права

readonly

tools и disallowedTools

sandbox_mode

модель

model

model

model

когда звать

description

description

description

Промпт 1 написан под Cursor. Для двух других инструментов меняется формат файла, а не устройство контура, поэтому к тому же промпту добавляется абзац.

Claude Code, файл .claude/agents/verifier.md:

Во frontmatter укажи tools: только чтение файлов, поиск и запись в один файл вердикта.
Инструменты правки проверяемого кода в список не включай.
Description напиши так, чтобы родитель звал эту роль автоматически в момент
«работа заявлена завершённой», а не по слову «ревью».

Codex, файл .codex/agents/verifier.toml:

Создай субагента в .codex/agents/verifier.toml.
Поля: name, description (момент вызова), developer_instructions (промпт роли
из блока выше), model, sandbox_mode с минимальными правами.
Проверь секцию [agents] в config.toml: субагенты включены, потолок
одновременных потоков задан.

Теперь про description, потому что от него зависит, позовут проверяющего или нет. Пока у меня там стояло «ревьюер кода», его не позвали ни разу и никакого сигнала об этом не было, работа просто шла дальше без вердикта. Я переписал описание на «проверка завершённой работы перед тем, как считать задачу закрытой», и он начал появляться сам. Описание пишется как условие вызова, а не как должность.

Отдельно про Codex. Один раз я поймал там tool-backed сессию: файл .codex/agents/*.toml лежал по документации, а отвечал на запрос кто-то другой. Теперь после запуска я смотрю, кто на самом деле в чате. В Cursor и Claude Code такого не встречал.

Правило, которое держит статус

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

В <файле> статус стоит «принято», файла вердикта от отдельного прохода нет.
Верни статус в черновик и добавь в правило строку: статус действителен только
при существующем файле вердикта; отсутствие файла означает откат.
Покажи, какой файл изменил.

Что проверяете: агент назвал конкретный файл правил и показал диффом, что именно дописал. Служебную шапку файла правил (в Cursor у .mdc она своя, и без неё правило просто не подгрузится) он ставит сам по документации инструмента, вам её знать не нужно.

Правило может быть заведено и всё равно не доехать до модели, если оно лежит не на том уровне или тонет в раздутом always-файле. Это отдельная тема, я разбирал её в предыдущей статье цикла, и здесь предполагаю, что с доставкой у вас порядок.

Что передаётся между проходами и как выглядит вердикт

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

Поэтому в промпт идут пути, от трёх до семи, и требование открыть их самому. Всё, что длиннее полусотни строк, передаётся путём, а не вставкой. Форма передачи такая:

Задача: <одна фраза, что должно существовать после прохода>
Читать (3–7 путей):
- <путь>: <зачем>
Писать: <одна зона>
Готово, когда: <проверяемое условие>

Инструкция ставится сверху, условие приёмки снизу, потому что середина длинного промпта читается заметно хуже краёв.

В живом чате после исполняющего прохода это выглядит так, и это буквально то, что вы пишете:

Вызови verifier. Передай ему только пути:
- src/parser.py
- tests/test_parser.py
- задача: docs/task-parser.md
Нашу переписку не пересказывай. Верни файл вердикта.

Обратно приходит файл. Первая строка машиночитаемая, по ней я ветвлюсь:

verdict: PASS | FAIL
проверял: <пути, которые открыл>
не сошлось:
- <пункт>: <что именно и где>

Вот настоящий вердикт из моего августовского журнала, не учебный parser.py:

verdict: FAIL
проверял: docs/spec-task.md, docs/plan-task.md, журнал прогонов
не сошлось:
- в шапке спеки и плана «принято», файлов вердикта от другого прохода нет
- прогон тестов прошёл, журнал прогонов пуст — стадии формально не пройдены

Исполняющий проход к этому моменту уже писал код. Проверяющий остановил его, пока «сделано» в сводке ещё можно было откатить.

Для кода форма ровно та же:

verdict: FAIL
проверял: src/parser.py, tests/test_parser.py
не сошлось:
- заявлен разбор дат, в коде разобран только ISO (parser.py:41)
- тест на невалидный вход отсутствует

Файлы вердиктов я кладу рядом с работой. Если работа лежит в docs/task-parser.md, вердикт будет в docs/review-task-parser.md, иначе через неделю я не вспомню, что к чему.

FAIL уходит автору, и чинить проверяющий не должен, потому что если он начнёт править, судить станет некому. Когда отказ приходит второй раз с той же причиной, повторять ту же итерацию бесполезно и нужно поднимать уровень. На это тоже есть промпт:

По этой стадии второй отказ подряд с той же причиной.
Ту же итерацию не повторяй. Подними уровень: сначала правился текст,
теперь разбираем механику; если и это второй раз, обсуждаем сам замысел.
Запиши отказ отдельной строкой в журнал с причиной.

Тот, кто раздаёт задачи и сверяет результат, называется управляющим проходом, и он не пишет ни продукт, ни чужие вердикты. Его файл один, это журнал, где отказ стоит отдельной строкой с причиной. Журнал, в котором двадцать девять записей из двадцати девяти успешные, а работа при этом не закрыта, бесполезнее пустого. Одна стадия за ответ, потому что три стадии в одном ходу означают, что двух вердиктов между ними не было.

Права и зоны записи

Здесь я сам сделал ошибку, которая выглядела как правильное решение.

«Проверяющий ничего не пишет» звучит безупречно, и в августе я так и поставил, отобрав у роли права на запись. Через несколько прогонов выяснилось, что файл ревью всё равно появляется, только пишет его вызывающий чат. То есть исполняющий проход. Запрет записи произвёл ровно тот самоаппрув, против которого вводился, и заметить это было тяжело, потому что файл вердикта существовал и автоматическая проверка «есть ли вердикт» проходила.

Была история того же класса, но обиднее. Слот вердикта в файле внешнего ревьюера заполнил управляющий проход, даже не открыв проверяемый черновик. Файл с verdict: PASS существовал, проверка на наличие вердикта проходила, а живой читатель отклонил результат на следующий день. С тех пор в правилах записано, что слот вердикта заполняет только тот проход, который проверял.

Так что права раздаются иначе. Проверяющий не трогает проверяемое, но свой вердикт пишет сам, и зона записи у него одна — собственный файл. Полный запрет записи годится только той роли, которая отдаёт вердикт репликой и ничего после себя не оставляет.

Проще всего держать это таблицей на три строки и открывать её при заведении каждой новой роли.

Кто

Пишет

Не трогает никогда

управляющий

журнал прогонов

продукт, чужие вердикты

исполняющий

рабочие файлы задачи

файлы вердиктов

проверяющий

свой файл вердикта

всё проверяемое

Правая колонка тут важнее левой. Пока она не написана, любая роль по умолчанию считает своей зоной весь проект, и обнаруживается это в тот момент, когда параллельные проходы наступают друг другу на один и тот же файл.

Чего это стоит и где цепочка врёт

Цену я считаю по прогонам, и количество проверяющих ролей тут ни при чём. За месяц по четырём проектам вышло 167 запусков исполняющего прохода и 277 проверяющих, суммарно 444 обращения к модели. Числа 277 и 167 не означают «три проверки на задачу»: часть прогонов это повтор после FAIL, часть — разные стадии одной работы. Без гейта было бы 167 обращений, то есть разница примерно 2,7×.

Что это значит на практике, если вы живёте не в API, а в лимите подписки. Множитель тот же самый: пакет обращений, которого раньше хватало на неделю, кончится где-то к середине третьего дня. Своих долларов я не мерил и цифру эту не замерял, это просто деление, но порядок понятен, и планировать стоит от него.

Теперь что вы за это получаете. Не-PASS среди проверок — 30 из 277, примерно каждый девятый прогон проверяющего. Из 167 задач отказ получили около 30, то есть каждая шестая работа. Медиана от запуска исполняющего прохода до вердикта — пять минут, причём отметки времени у меня с точностью до минуты, так что это скорее нижняя граница.

Читать эти числа стоит так. Разницу в обращениях вы платите всегда, а ловите примерно каждую шестую работу. Если на боевых задачах доля отказов месяцами держится около нуля, гейт ничего не находит и его можно снять. Это не то же самое, что подсадные утки, про которые ниже: на сломанном и чистом входе вердикты обязаны различаться, и если они совпали, проверяющий сломан, даже когда на проде одни PASS.

Про токены своих замеров у меня нет, и выдумывать я их не буду. Публичная цифра есть у Anthropic: агент расходует примерно вчетверо больше токенов, чем чат, а многоагентная система примерно в пятнадцать раз. Мерили на исследовательских задачах, и там же сказано, что в разработке распараллеливаемого заметно меньше.

Подсадные утки

У контура своя отладка, и главный вопрос в ней такой: проверяющий стабильно даёт FAIL, это плохой исполняющий проход или плохой проверяющий. Я гоняю подсадных уток, то есть заведомо сломанный вход и заведомо чистый.

Прогони verifier дважды на разных входах:
1) заведомо сломанный артефакт (удали обязательный раздел);
2) заведомо чистый.
Покажи оба вердикта.

Что проверяете: на сломанном FAIL, на чистом PASS. Если так и вышло, проверяющий живой и разбираться надо с исполняющим проходом. Если на обоих входах вердикт одинаковый, чинить нужно проверяющего, и до этого его вердиктам верить нельзя.

Ещё одна ловушка: файл на диске не значит «свежий». Между проходами кто-то мог перезаписать артефакт, и проверяющий откроет вчерашний. Перед запуском я смотрю отметку времени или передаю конкретный коммит. Само это не починится.

И модель у проверяющего та же самая, что у исполняющего, поэтому чек-лист автора ему лучше не давать. Получив тот же список, по которому писали работу, он сверит галочки и поставит PASS по форме, пропустив текст, который живой человек закрыл бы на третьем абзаце. У меня для этого заведена вторая роль, ортогональный проверяющий. Чек-листа она не получает вовсе, а в её промпте прямо написано игнорировать попавший в контекст чек-лист и отметить это в вердикте. Судит она по одному живому вопросу, вроде «взял бы этот код в свою ветку».

Заведи вторую проверяющую роль. Ей намеренно НЕ передаётся чек-лист требований:
если чек-лист оказался в промпте, роль обязана его проигнорировать и отметить это.
Вердикт по одному живому вопросу: <ваш вопрос>.

Cognition в «Don't Build Multi-Agents» пишет, что делиться нужно полной трассой, а не обрывками сообщений, иначе решения разъедутся и свести их не выйдет. Их аргумент бьёт в мой контур прямо, потому что проверяющий видит файл, а не то, что исполняющий проход пробовал и выкинул. Мой проверяющий продукт не проектирует, так что конфликтовать с исполняющим ему нечем, но дыру это не закрывает.

Пока в журнале стоит строка FAIL, я знаю, на каком файле спорить. Пока в шапке стоит только PASS от того же прохода, который делал работу, я узнаю об ошибке на следующий день, как в самом начале этой статьи.

Проверка по своему репо

Один прогон по минимальному контуру не гарантирует, что гейт держится на всём проекте. Раз в пару недель я открываю чистый чат и прошу перечислить субагентов с описаниями. Если проверяющего в списке нет, я откладываю остальное и сначала завожу файл роли. Дальше читаю description как условие вызова, а не как характеристику, и спрашиваю себя, позовут ли verifier в момент, когда работа уже названа завершённой. Если из текста этого не следует, роль будет молчать, и об отсутствии вердикта вы узнаете с задержкой.

Параллельно смотрю шапки файлов и проверяю, кто именно поставил «готово». Если тот же проход, который правил файл, я меняю правило так, чтобы статус без чужого вердикта не держался. Потом смотрю, кто создал последний review-*.md: при отобранных правах записи автором часто оказывается исполняющий проход, и тогда гейт декоративный. Утиный тест из раздела выше закрывает последний вопрос, живой ли проверяющий вообще.

Ещё одно место, где я обжёгся: чек-лист проверки иногда живёт копией внутри промпта роли. У меня правило обновили, а промпт остался прежним, и проверяющий несколько прогонов сверял работу по устаревшему списку.

Промпты из этой статьи лежат в папке recipes/many-agents/ репозитория ai-engineering-recipes, там их больше, чем поместилось сюда. notation.md держит форму гейта: инварианты, зоны записи, формат вердикта. prompts.md собирает промпты целиком. sources.md говорит агенту, какие ваши файлы брать как основание, чтобы он работал из вашей практики. Есть ещё verifier.md.example, но это заглушка на посмотреть, а не файл на перенос: настроенной роли там нет и быть не может, потому что мой проверяющий собран под мои сбои.

Цифры и проверки сняты в августе 2026 на моих проектах. У вас порядок величин может оказаться другим.

Источники

Подход и ограничения:

Где заводятся роли: