Пять фаз
Что делает каждая фаза генерации, сколько она стоит, во что деградирует при сбое и как перезапустить только одну из них.
handbook generate выполняет пять фаз. Бесплатна только первая; остальные обращаются к
вашей конечной точке LLM.
| Фаза | Производит | LLM? | Перезапускается отдельно? |
|---|---|---|---|
| 1 | граф вызовов | ❌ | ✅ |
| 2a | одна карточка на просканированный файл | ✅ | ✅ |
| 2b | скелет этапов + назначение файлов | ✅ | ✅ |
| 2c | группировка и упорядочивание внутри этапа | ✅ | ✅ |
| 3 | повествование + межэтапные регистры состояния | ✅ | ✅ |
--phase all # everything (default)
--phase 1 # just the call graph
--phase 2 # 2a + 2b + 2c
--phase 2a # one phase
--phase 2c,3 # a comma listPhase 1 — граф вызовов
Без LLM. Детерминированно. Бесплатно.
Языковые адаптеры разбирают каждый файл с помощью tree-sitter и производят одно языконезависимое промежуточное представление. Затем построитель графа делит рёбра на сохранённые и отброшенные, аннотирует входящую/исходящую степень и синтезирует узлы для конструкторов, на которые есть ссылки, но которые нигде явно не определены.
Он также ставит хеш содержимого на каждый просканированный файл. Именно этот хеш
позволяет resync позже обнаружить правку тела функции на месте, не сдвинувшую номера
строк и сигнатуры, — случай, который чисто структурный diff пропускает полностью.
Что прочитать не удалось
Файл, который обнаружение перечислило, но анализатор не смог превратить в факты,
записывается, а не пропускается молча. Каждый такой файл попадает в
phase1/scan-coverage.json с причиной:
unreadable— не удалось само чтение (права доступа, висячий симлинк, файл, удалённый сборкой посреди запуска). Фактов нет.unparsable— грамматика упала или вообще не вернула дерева. Фактов нет. Обычный случай — shell-скрипт сcase.partial— файл разобрался, но с синтаксическими ошибками. Функции и вызовы, найденные в остальной его части, настоящие; недостаёт того, что попало внутрь узла ошибки.
Файлы первых двух категорий вдобавок исключаются из scannedFiles: файл, не давший
ничего, не должен уходить в phase 2a так, будто он просто пуст. Завершая работу, Phase 1
называет пробел в логе:
[scan] coverage: 409 files analyzed; 3 recorded in scan-coverage.json (partial=1 unparsable=1 unreadable=1)Пустой массив files в этом артефакте — то же самое утверждение в утвердительной форме:
разобралось всё.
Результат: phase1/graph.json, functions.csv, graph.dot, dropped-calls.json,
scan-coverage.json.
Запускайте это первым, всегда
handbook analyze — это ровно эта фаза. Она ничего не стоит, и это единственный способ узнать, что вы
сканируете node_modules или пропускаете целый язык, до того, как потратите токены.
Phase 2a — карточки файлов
LLM. Обычно самая дорогая фаза.
Карточку получает каждый файл, который Phase 1 действительно прочитала, — то есть
scannedFiles из graph.json, откуда исключены нечитаемые и неразбираемые пути,
записанные в scan-coverage.json:
- purpose — одно-два предложения простым языком
- role — из закрытого словаря (
entrypoint,domain_logic,io_transport, …) - lifecycle —
startup,main loop,cross-cutting,none, … - а в
--detail deep: разбор на 120–300 слов плюс назначение, поток данных и связи по каждой функции, наложенные на факты из графа
Как это батчируется
--read-batch-size файлов на запрос, --read-workers батчей в полёте. Глубокий режим по
умолчанию берёт один файл на батч, потому что глубокая карточка — это много вывода, а
упаковка нескольких в один ответ — это то, из-за чего ответы обрезаются.
Трёхуровневая деградация
Если ответ на батч не парсится:
- повторить батч, разбив его на отдельные файлы;
- для файла-переростка повторить по кускам-функциям;
- если и это не помогло — записать честную пустую карточку: только структура, без прозы.
Файл никогда не исчезает из руководства из-за того, что его проза не удалась. Каждый
промах перечислен в phase2/cards/_coverage.json, а ответы, из которых не вышло ничего
пригодного, сохраняются (не более 20, с именами по хешу) в phase2/cards/_rejected/,
чтобы вы могли прочитать, что пошло не так, а не гадать.
Возобновление
Карточки записываются по мере готовности. Ctrl-C безопасен, а --resume пропускает
файлы, у которых уже есть полная карточка требуемой глубины.
handbook generate --source $REPO --work $WORK --phase 2a --detail deep --resumePhase 2b — скелет и назначение
LLM. Фаза, которая решает, чем руководство является.
Два режима.
--synth-mode oneshot (по умолчанию)
Синтезировать скелет этапов из nav-pack (свёртки по каталогам + точки входа), затем назначить каждый файл ровно на один этап, батчами.
Дёшево — и обычно достаточно, чтобы судить, верна ли форма.
--synth-mode doctor
Ремонтный цикл «актор–критик». Каждый раунд:
-
актор предлагает не более трёх структурных изменений — разделить, слить, переместить, переименовать, сменить родителя — на основе достоверной статистики из настоящего графа;
-
три критика рецензируют параллельно, каждый ищет свой вид сбоя:
Критик Ищет инженер Соответствует ли это тому, что код действительно делает? Реальны ли упомянутые элементы? архитектор Нечёткие границы, раздутые этапы, истощённые этапы, неверно размещённые сквозные аспекты читатель Стал ли результат читабельнее? Связные страницы, понятные заголовки, повествование, за которым можно следовать -
выжившие изменения механически перепроверяются по графу — изменение, называющее несуществующий этап или оставляющее файлы сиротами, отклоняется до того, как коснётся скелета;
-
затронутые файлы переназначаются.
Цикл останавливается, когда не осталось неназначенного и ни одно изменение не пережило
рецензию, либо на --max-doctor-rounds (по умолчанию 6), либо после двух раундов без
прогресса.
Критик, чей ответ не удалось распарсить, считается за REJECT. Сломанный рецензент никогда не должен
пропускать изменение.
Свой собственный скелет
handbook generate --source $REPO --work $WORK --skeleton my-skeleton.yamlФайлы назначаются на ваши этапы. С --strategy member вместо этого классифицируются
отдельные функции, а артефакты уровня файлов выводятся из этого.
Результат: phase2/skeleton.yaml, phase2/assignment.json, phase2/strategy.json.
Phase 2c — организация
LLM, но дёшево. Деградирует до детерминированного порядка.
Внутри каждого этапа файлы упорядочиваются по топологии графа вызовов и группируются в 2–8 озаглавленных подгрупп с однострочным резюме у каждой.
Любой сбой деградирует до детерминированного плоского порядка. Файлы никогда не выбрасываются. Вокруг этого инварианта написана вся фаза: нечитаемая группировка — это косметическая проблема, отсутствующий файл — проблема корректности.
С --strategy member эта фаза — no-op: организация уже была выведена
детерминированно на 2b, поэтому запуск с одним лишь --phase 2c вообще не требует LLM.
Результат: phase2/organization.yaml.
Phase 3 — повествование и регистры
LLM. Сильно кешируется.
Повествование, снизу вверх
Сначала листовые этапы, потом родительские — чтобы резюме родителя писалось со знанием того, что говорят его дети, — затем обзор системы, написанный со знанием всего.
Каждый вызов за прозой кешируется в phase3/cache/ с ключом из версии промпта, языка и
полного хеша промпта. Перезапуск Phase 3 после правки одного этапа заново повествует
один этап.
Регистры состояния
«Регистр» — это элемент состояния, который проходит сквозь этапы: пул соединений, фича-флаг, бюджет повторов, токен аутентификации. Извлечение выполняет проход по пробелам до полного исчерпания: оно продолжает спрашивать, пока очередной раунд не найдёт ничего нового.
Это самый полезный артефакт для веерных изменений, потому что «какие этапы касаются этого состояния» — ровно тот вопрос, который задаёт разбросанное изменение.
Результат: phase3/narration.json, phase3/registers.json.
Две стратегии
--strategy file (по умолчанию) | --strategy member | |
|---|---|---|
| Скелет | синтезируется LLM | вы пишете skeleton.yaml |
| Листовая единица | один исходный файл | одна функция или метод |
| Phase 2b | назначить файлы на этапы | классифицировать каждый член, затем вывести артефакты файлов |
| Phase 2c | группировка LLM | уже сделано — детерминированно |
| Лучше всего для | репозитория, который вы ещё не знаете | репозитория, чью форму вы уже знаете |
| Стоимость | ниже | выше — классифицируется каждый член |
Выбранная стратегия записывается в phase2/strategy.json. Частичный перезапуск с другой
--strategy и без --phase 2b отклоняется, потому что стратегия file по умолчанию,
молча перезаписывающая организацию, выведенную из member, — ровно тот вид порчи, который
трудно заметить постфактум.
Что запуск записывает о себе
{
"version": 1,
"model": "gpt-4o-mini",
"phases": ["1", "2a", "2b", "2c", "3"],
"startedAt": "2026-08-08T13:02:11.004Z",
"finishedAt": "2026-08-08T13:19:44.881Z",
"usage": { "promptTokens": 1840221, "completionTokens": 214880, "totalTokens": 2055101 },
"stats": { "phasesRun": ["1", "2a", "2b", "2c", "3"], "nCards": 412, "nStages": 9, "nRegisters": 6 }
}Он описывает последний успешный запуск. Неудачный запуск оставляет предыдущий манифест нетронутым, а прерванный не пишет никакого вовсе.