Почему агентам Claude Code нельзя доверять отчёты о собственном статусе
Stop-хук, журнал событий и бюджеты в долларах, чтобы управлять сессиями Claude Code как оргструктурой, плюс четыре сюрприза харнесса, которые пропустили 74 зелёных теста.
Запустите больше одной сессии Claude Code одновременно, и рано или поздно вы попробуете очевидное: поручить одной сессии управлять остальными. Это работает минут пять. После нескольких проектов с одним и тем же провалом стало ясно, что полезнее перестать считать это проблемой промптов и взглянуть на неё как на проблему архитектуры. Ниже — что сломалось, что в итоге построили и шесть особенностей харнесса, которые не поймал бы ни один юнит-тест; четыре из них проявились только при запуске настоящей сессии.
Не один провал, а три
Симптом, который замечают все, — тишина: подчинённый так и не отчитывается, даже если ему велели сообщить о завершении. Но это самая безобидная из трёх отдельных проблем.
Подчинённые не отчитываются. Инструкция живёт в контексте модели, а контекст сжимается, заканчивается или просто проигрывает самой задаче. К концу работы напоминание об отчёте вытеснено четырьмя сотнями строк вывода тестов. Дело не в дисциплине. Это проблема иерархии памяти, и вежливые формулировки её не решат.
Руководители ничего не видят. Эту проблему легко упустить, она вытекает из того, как устроен Claude Code. Сессия выполняется только пока идёт ход пользователя, а между ходами не делает вообще ничего: не опрашивает, не ждёт, не думает. Поэтому подчинённый, который всё-таки отправил отчёт, не достучится ни до кого. Сообщение лежит в сессии, которая спит, пока человек что-нибудь не напечатает.
Сбои никто не замечает. Представьте две сессии, которые правят один файл и молча затирают друг друга, сессию, у которой посреди задачи закончился контекст и она умерла, или сессию, застрявшую в цикле. Снаружи каждая из них выглядит просто как медленная.
Третья проблема самая дорогая, и наивное решение — попросить сессии отчитываться — её вообще не затрагивает.
Отчёт должен происходить с моделью, а не по её воле
Всё изменил один принцип: отчёт не может зависеть от того, решит ли модель его отправить. Он должен запускаться вокруг модели, а не ею самой.
Если отчёт держится на том, что модель решит его подать, он надёжен ровно настолько, насколько у модели остался контекст. Заберите его из рук модели, и он станет таким же надёжным, как жизненный цикл процесса.
В Claude Code для этого есть подходящий механизм. Хук Stop срабатывает каждый раз, когда сессия завершает ход, и вызывает его харнесс, а не модель. Модель могла забыть, могла остаться без контекста, могла даже упасть — хук всё равно выполнится:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "node",
"args": ["${CLAUDE_PLUGIN_ROOT}/hooks/stop-report.mjs"],
"timeout": 15
}
]
}
]
}
}
Это и есть единственная несущая идея. Всё остальное — леса вокруг неё.
Устройство
Получившийся инструмент запускает сессии Claude Code как организацию. Executive общается с вами, Managers отвечают за направления, Staff выполняют работу. Роль определяется положением, а не ярлыком: сотрудник Staff, который набрал собственную команду, становится Manager для этой ветки.
you
▲
results ───────┼─────── objective
▼
┌───────────────────┐
│ Executive │
└─────────┬─────────┘
┌─────────────┴─────────────┐
▼ ▼
┌───────────┐ ┌───────────┐
│ Manager │ │ Manager │
└─────┬─────┘ └─────┬─────┘
┌────┴────┐ ┌────┴────┐
▼ ▼ ▼ ▼
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│Staff│ │Staff│ │Staff│ │Staff│
└──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘
└─ peers ─┘ └─ peers ─┘
Каждый участник подчиняется ровно одному руководителю, а эскалация идёт по одному уровню за раз. Коллеги одного уровня сами договариваются о пересекающейся работе и зовут Manager, только если не могут договориться.
Три решения стоит объяснить, потому что каждое могло быть другим.
Состояние — это журнал событий только на дозапись. До двенадцати процессов ОС пишут в одну директорию одновременно. Блокировок нет: каждое изменение состояния — одна дописанная строка, а ОС гарантирует атомарность такой дозаписи. Хранимой оргструктуры тоже нет, её вычисляют свёрткой журнала при каждом чтении. Убийство сессии ничего не может испортить. В худшем случае останется устаревшая запись working, а её отлов — задача детектора зависаний.
Руководители сами теребят подчинённых. Каждый руководитель на каждом ходу сортирует прямых подчинённых и поднимается по фиксированной лестнице: напомнить, прочитать транскрипт, переназначить. Средняя ступень важнее, чем кажется. Долгий прогон тестов и бесконечный цикл снаружи выглядят одинаково, и различить их можно, только прочитав реальный вывод. Это решение для модели, а не для таймаута.
Бюджеты выстроены цепочкой и считаются в долларах. Коллега возразил, что на подписке доллары кажутся абстракцией, а ощущаются именно токены. Доллары всё равно победили, по двум причинам. --max-budget-usd — единственный лимит, который харнесс действительно соблюдает, а потолок по токенам — просто число в JSON-файле, которое никто не обязан уважать. К тому же токены несравнимы между уровнями моделей: миллион токенов на самой мощной модели может стоить примерно в десять раз дороже, чем на самой дешёвой. Бюджет в токенах не может ограничить иерархию, смысл которой как раз в смешении уровней: менеджер легко уложится в него, умножив счёт на десять. Токены пишутся в лог как показатель. Ограничение — доллары.
Последовательность для одного задания от начала до конца включает альтернативную ветку посередине, и именно она несущая: шаг 5 существует для того, чтобы системе никогда не пришлось полагаться на шаг 4.
Состояние, которое не видят часы
Обнаружение зависаний выглядит как обычная задача на таймауты, и в основном так и есть. Но одно состояние одновременно самое срочное в системе и невидимое для всего, что измеряет тишину.
Возьмём участника, который сообщает blocked. Он только что вышел на связь, поэтому время с последнего контакта равно нулю, и любая проверка простоя считает его здоровым. На деле он перестал работать и ждёт решения, которое может дать только его руководитель. Ни одна проверка его не отметит: он ведь вежливо объявил, что застрял.
Смоук-тест вскрыл это рано, ещё до более сложных багов. Подчинённый отправил blocked: beyond_capability, а в статусе у руководителя он значился как OK. Когда это заметишь, исправление небольшое. Но чтобы заметить, понадобился живой запуск.
А потом всё запустили по-настоящему
К этому моменту было 74 юнит-теста, все зелёные, CI проходил на Linux и Windows на двух версиях Node. Дизайн держался и на бумаге, и в тестах, уверенность была высокой.
Затем одной настоящей фоновой сессии дали пустяковое задание: создать файл со словом «hello». Она провалилась четыре раза подряд по четырём не связанным причинам. Ни одна не была ошибкой в коде. Каждый раз харнесс вёл себя не так, как предполагалось, и каждый раз молча.
1. В фоне ID сессии игнорируются
Идентичность строилась вокруг изящной идеи: родитель заранее назначает ребёнку ID сессии, так что оргструктура записана ещё до появления ребёнка и нет ни мгновения, когда подчинённый работает незарегистрированным.
На практике claude --bg --session-id <uuid> игнорирует переданный UUID, и фоновая сессия придумывает свой. Результат получился почти символичным. Stop-хук искал в оргструктуре ID сессии от харнесса, никого не нашёл и вышел, ничего не записав, — то есть страховка, созданная, чтобы гарантировать отчёт, не сделала ничего.
Решение — передавать выбранный родителем ID ребёнку через переменную окружения и считать его главным, а ID харнесса использовать, только если переменной нет.
2. На запрос разрешения некому ответить
Подчинённый идеально выполнил инструкцию. Прочитал задание, понял протокол и запустил первую команду — проверку входящих — ровно как было сказано.
А потом навсегда застыл на запросе разрешения. Режим acceptEdits одобряет правки файлов, но оставляет shell-команды неодобренными, а к фоновой сессии не подключён человек, который нажмёт «да». Руководитель не может отличить это от зависания, хотя подчинённый всё делает правильно и просто ждёт одобрения, которое не придёт.
Исправление — заранее одобрить CLI при запуске через --allowedTools. Указывайте оба shell-инструмента, потому что выбор модели зависит от платформы: PowerShell на Windows, Bash в остальных случаях.
3. --allowedTools принимает переменное число аргументов и проглатывает промпт
С добавленными разрешениями следующая сессия стартовала и сразу сообщила, что простаивает и ждёт промпт.
--allowedTools <tools...> принимает переменное число аргументов, поэтому продолжает забирать их и после булева флага. Такая команда:
claude --allowedTools "Bash(node:*)" --bg "<the actual prompt>"
затягивает промпт в список инструментов и запускает сессию без инструкций, которая терпеливо ждёт ввода, который никогда не придёт. Опция аккуратно останавливается на флаге, принимающем собственное значение, поэтому такая форма работает:
claude --allowedTools "Bash(node:*)" --model haiku --bg "<the actual prompt>"
Теперь правила разрешений выводятся первыми, прямо перед --session-id, чтобы корректность не зависела от порядка аргументов в остальной части команды.
4. Перед правками нужен worktree
С четвёртой попытки сессия объявила, что перед правкой ей нужно войти в worktree. Она попыталась его создать, не смогла, потому что в черновом репозитории не было коммитов (базовую ветку «HEAD» не удалось разрешить), а затем попробовала git commit, которому нужно было одобрение, которое некому дать.
Отчасти тут виновата тестовая среда. Но сам факт полезно знать: фоновые сессии тяготеют к изоляции через worktree при записи, и эта изоляция ближе к поведению по умолчанию, чем к опции.
Ещё два, найденных прощупыванием
--append-system-prompt-file существует, но не задокументирован. В claude --help его нет, однако при проверке он выдаёт ошибку «не хватает аргумента», а не «неизвестная опция». Это важно, потому что описание роли занимает около тридцати строк с кавычками и переводами строк, а передача его прямо в команде на Windows ломается трижды: cmd.exe не умеет переносить переводы строк в аргументе, экранирование кавычек зависит от оболочки, а длина командной строки ограничена примерно 8191 символом. Запись описания в файл и передача пути обходят все три проблемы.
--plugin-dir не доходит до порождённых сессий. Плагин, запущенный из клона, действует только на эту сессию, поэтому порождённые ею сессии стартуют без хуков и без навыков ролей и игнорируют протокол, которого им никто не дал. У установленного плагина такой проблемы нет, так что рекомендуемый путь — нормальная установка, а для случая с клоном в конфиге есть явная опция проброса.
Зелёных тестов всё равно не хватило
Ещё один случай показывает, почему одних живых тестов тоже недостаточно. CI поймал дефект, который локальные прогоны так и не показали. События в журнале упорядочивались по метке времени в миллисекундах, а ничьи разрешались случайным ID события. Быстрые машины часто пишут пачку событий в пределах одной миллисекунды, поэтому «напомнить, напомнить, отчёт» могло свернуться как «отчёт, напомнить, напомнить», и участник, который только что отчитался, всё ещё считался бы не получившим напоминания. Проекция была недетерминированной и могла читаться по-разному при каждой загрузке.
Локально на Windows все 74 теста проходили. На Linux CI упал дважды.
Решением были не более точные метки времени. Раз дозаписи атомарны, порядок строк в файле уже отражает порядок записей во всех процессах. Если разрешать ничьи по позиции строки, порядок становится и детерминированным, и причинно верным.
Что из этого вынести
Всё это не доказывает, что разработка с ИИ ненадёжна. Код был дешёвой частью, и большая его часть оказалась верной. Это показывает, откуда должна браться уверенность. Зелёный набор тестов доказывает, что система делает то, что вы смоделировали. Он не скажет, верна ли ваша модель платформы, а все описанные сбои сидели именно в этой слепой зоне, потому что тесты несли те же ошибочные допущения, что и реализация.
Сработало другое: сделать опровержение дешёвым и повторять его. Четыре живых запуска, по несколько минут каждый и почти бесплатных на самой маленькой модели, вскрыли четыре архитектурных дефекта, которые одобрили 74 теста.
Если вы строите что-то на агентной платформе, сознательно закладывайте время на этот цикл. Интересные баги не в вашем коде. Они живут на стыке вашего кода и харнесса, поведение которого вы вывели, а не проверили. Эти факты о харнессе тоже стоит хранить в отдельном файле заметок, отдельно от спецификации дизайна, ведь они описывают платформу, а не ваш проект.