5 つのフェーズ
各生成フェーズが何をするか、何を消費するか、失敗時に何へデグレードするか、そして 1 つだけ再実行する方法。
handbook generate は 5 つのフェーズを実行します。無料なのは最初だけで、残りはあなたの
LLM エンドポイントと対話します。
| Phase | 生成物 | 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 ですべてのファイルをパースし、言語非依存の中間表現を 1 つ 生成します。次にグラフビルダーがエッジを保持と破棄に分割し、入次数・出次数を注釈し、 参照はされているものの明示的には定義されていないコンストラクタのノードを合成します。
また、スキャンしたファイルごとのコンテンツハッシュを刻印します。このハッシュがある
からこそ、resync は後で、行番号もシグネチャも変えないインプレースの本体編集 — 純粋に
構造的な差分では完全に見逃すケース — を検出できます。
読めなかったもの
探索では列挙されたのに、アナライザが事実へ変換できなかったファイルは、黙ってスキップされる
のではなく書き残されます。1 つ残らず、理由と共に phase1/scan-coverage.json に入ります:
unreadable— 読み取り自体が失敗した(パーミッション、リンク切れのシンボリックリンク、 ラン中にビルドが削除したファイル)。事実はゼロです。unparsable— 文法が例外を投げたか、木を返さなかった。事実はゼロです。よくあるのはcaseを含むシェルスクリプトです。partial— パースはできたが、構文エラーを含む。それ以外の部分で見つかった関数と 呼び出しは本物です。欠けているのは、エラーノードの中にあったものだけです。
最初の 2 つのカテゴリのファイルは 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 をスキャンしていることや、 1
つの言語がまるごと欠けていることを、トークンを使う前に知る唯一の方法です。
Phase 2a — ファイルカード
LLM。通常、最も高くつくフェーズ。
Phase 1 が実際に読めたファイルがカードを得ます — 対象は graph.json の scannedFiles で、
scan-coverage.json に記録された「読めなかった」「パースできなかった」パスはそこから
除かれています:
- purpose — 平易な言葉で 1〜2 文
- role — 閉じた語彙から(
entrypoint,domain_logic,io_transport, …) - lifecycle —
startup、main loop、cross-cutting、none、… - さらに
--detail deepでは: 120–300 語のウォークスルーに加え、グラフの事実にマージされる 関数ごとの purpose、データフロー、関係
バッチ処理のしくみ
リクエストあたり --read-batch-size ファイル、同時に --read-workers バッチ。deep モード
のデフォルトはバッチあたり 1 ファイルです。deep なカードは大量の出力であり、複数を
1 つの返答に詰め込むことこそ、返答が途中で切れる原因だからです。
3 段階のデグラデーション
バッチの返答がパースできない場合:
- バッチを単一ファイルに分割してリトライします;
- 大きすぎる単一ファイルは関数チャンクごとにリトライします;
- それでも失敗する場合は、正直な空カード — 構造のみ、文章なし — を書きます。
文章が失敗したせいでファイルがハンドブックから消えることは決してありません。 すべての
取りこぼしは phase2/cards/_coverage.json に列挙され、使えるものを何も生まなかった返答は
phase2/cards/_rejected/ の下に保持される(20 個上限、ハッシュ名)ため、何が悪かったのかを
推測ではなく「読む」ことができます。
再開
カードは完成した順に書き込まれます。Ctrl-C は安全で、--resume は要求された深さの完全な
カードをすでに持つファイルをスキップします。
handbook generate --source $REPO --work $WORK --phase 2a --detail deep --resumePhase 2b — スケルトンと割り当て
LLM。ハンドブックが「何であるか」を決めるフェーズ。
2 つのモードがあります。
--synth-mode oneshot(デフォルト)
nav-pack(ディレクトリの集計 + エントリポイント)からステージのスケルトンを合成し、その後 すべてのファイルをちょうど 1 つのステージへ、バッチで割り当てます。
安価で、形が正しいかどうかを判断するには通常これで十分です。
--synth-mode doctor
アクター–クリティックの修復ループです。各ラウンド:
-
アクターが、実グラフ由来のグラウンドトゥルース統計に対して、最大 3 つの構造変更 — 分割、統合、移動、改題、親の付け替え — を提案します;
-
3 人のクリティックが並列にレビューし、それぞれ別の失敗を探します:
クリティック 探すもの engineer これはコードが実際にやっていることと一致するか? 参照されている項目は実在するか? architect 不明瞭な境界、肥大したステージ、痩せ細ったステージ、置き場所を誤った横断的関心事 reader 結果は「より読みやすい」か? まとまりのあるページ、直感的なタイトル、追っていける物語 -
生き残った変更は、グラフに対して機械的に再検証されます — 存在しないステージを名指し する変更や、ファイルを孤児にしてしまう変更は、スケルトンに触れる前に拒否されます;
-
影響を受けるファイルが再割り当てされます。
未割り当てが何もなく、レビューを生き残る変更もないとき、あるいは --max-doctor-rounds
(デフォルト 6)に達したとき、あるいは進捗のないラウンドが 2 回続いたときに停止します。
返答がパースできないクリティックは 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。ただし安価。決定的な並び順にデグレードします。
各ステージ内で、ファイルはコールグラフのトポロジー順に並べられ、それぞれ 1 行の要約が付いた 2–8 個のタイトル付きサブグループにまとめられます。
あらゆる失敗は、決定的なフラットな並び順にデグレードします。ファイルは決して落とされ ません。 フェーズ全体がこの不変条件を軸に書かれています: 読みにくいグルーピングは見た目の 問題ですが、欠けたファイルは正しさの問題です。
--strategy member では、このフェーズは何もしません — 編成は 2b の時点で決定的に導出
済みなので、素の --phase 2c ランは LLM をまったく必要としません。
出力: phase2/organization.yaml。
Phase 3 — ナレーションとレジスタ
LLM。強力にキャッシュされます。
ナレーションは、ボトムアップ
まず末端のステージ、次に親 — 親の要約は子が何を言っているかを知ったうえで書かれます — そして最後に、すべてを知ったうえでシステム概要が書かれます。
すべての文章の呼び出しは phase3/cache/ の下に、プロンプトバージョン、言語、完全な
プロンプトハッシュをキーとしてキャッシュされます。1 つのステージに触れた後の Phase 3 の
再実行は、1 つのステージだけを再ナレーションします。
状態レジスタ
「レジスタ」とは、ステージを横断して流れる状態のこと — コネクションプール、フィーチャー フラグ、リトライバジェット、認証トークン。抽出は尽きるまで回るギャップパスを実行します: ラウンドが何も新しいものを見つけなくなるまで、尋ね続けます。
これはファンアウトする変更にとって唯一最も有用な成果物です。「どのステージがこの状態に 触れるか」こそ、散らばった変更が発する問いそのものだからです。
出力: phase3/narration.json、phase3/registers.json。
2 つの戦略
--strategy file(デフォルト) | --strategy member | |
|---|---|---|
| スケルトン | LLM が合成 | あなたが skeleton.yaml を書く |
| 末端の単位 | ソースファイル 1 つ | 関数またはメソッド 1 つ |
| 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 }
}これは最後に成功したランを記述します。失敗したランは前のマニフェストに手を付けず、 中断されたランは何も書きません。