状態ファイルを持たない再入可能オーケストレーター
「Hora Kit の設計」シリーズの 6 本目。Hora Kit には state.json のような状態ファイルがありません。状態は .hora/ というディレクトリそのもので、Markdown のチェックボックスが状態です。実行のたびに現在地を判定する再入可能なオーケストレーターの設計と、それが保守で効いてくる理由を書きます。
1. はじめに
この記事は、弊社(Open Reach Tech)が OSS にした AI 開発フレームワーク『Hora Kit』の設計を解説するシリーズの 6 本目です。メイン記事はこちらです。
本当の意味での自動開発を実現するためのAI開発フレームワーク『Hora Kit』をOSSにしました
用語の確認。 Hora Kit で打つコマンドは
/horaの 1 つだけです。/horaが状況に応じて/hora-spec(仕様書を書く)、/hora-setup(実装リポジトリを作る)、/hora-plan(版を確定し、機能一覧と契約を書く)、/hora-build(1 機能を 18 の関所に通す)、/hora-accept(検収する)を順に呼びます。/hora-hotfixだけは/horaが呼ばず、人が直接打ちます。docs はこのうち/hora-specを「決める側」、残りを「作る側」と呼んでいて、この記事でもその呼び方を使います。
メイン記事で「保守で一番重宝しているのは .hora/ に記録が残り続けること」と書きました。この記事では .hora/ の中身と、それが普通の「状態ファイル」とは違う理由を書きます。
オーケストレーターを自作したことがある方は、状態をどこにどう持たせるかで悩んだ経験があると思います。Hora Kit の答えは少し変わっているので、自分ならどうするかを考えながら読んでみてください。
2. 状態ファイルはない。チェックボックスが状態
Hora Kit には state.json のようなものがありません。状態は .hora/ というディレクトリそのもので、その中の Markdown のチェックボックスが状態です。
.hora/
tree/<repository>.md /hora-setup が実地に読んだ内容と、読んだ時点のタグ
digests/<skill-name>.md 装備済みスキル 1 つの規約を短くしたもの。由来パッケージとバージョン付き
spec/<version>/_stages.md /hora-spec 自身の、どこまで進んだかの記録
spec/<version>/_assets.md ステージ 0 が何を、どこから、どのコミット時点で読んだか
spec/<version>/_divergence.md 文書とコードの食い違い。1 件につき 1 行
tasks/<version>/
_plan.md 機能の実装順と、検収タスク
<feature-id>.md 1 機能と、その 18 の関所
contracts/<version>/ 消費者が他所にいるサーバー 1 つにつき 1 ファイル
questions/<version>/open.md 追記のみ。specs/ を編集して答える
acceptance/<version>/
<feature-id>.md ある機能の検収実行すべて。1 実行 = 1 ブロック追記
_sweep.md 版全体の掃引
glossary.md 追記のみ。版で分けない
hotfix/<hotfix-id>.md hotfix が飛ばしたものの記録
何が走ったかの履歴は git log .hora/ で追えます。docs には「他に記録している場所はなく、その必要もありません」とあります。例外は各パッケージのインストーラが書く equip-*.json だけで、これらはプロジェクトの状態ではなくインストーラの状態なので gitignore されています。
3. 機能ファイルの実物
1 機能 1 ファイルで、中身はこういうチェックボックスです。
# #attendance 勤怠の記録と一覧
<!-- spec: attendance @ sha256:abc123... -->
<!-- repositories: backend, frontend-employee -->
## Spec gate
- [x] 1. Draft or confirm the specification
- [x] 2. Verify the use cases can be met
## Backend gate
- [x] 3. DB and API schemas
- [x] 4. Stub API
- [ ] 5. The modules the implementation needs
...
- [x] 7. Worker <!-- n/a: this feature triggers no background job -->
1 行目のコメントに、仕様の該当節のハッシュが入っています。仕様が変わればハッシュが変わり、次の /hora-plan がそれを検出します。
この形式にしている理由は、人が読めて、diff が読めて、git が管理できるからです。JSON の状態ファイルは機械には便利ですが、git log -p で「いつ関所 5 が通ったか」を追うには向きません。Markdown のチェックボックスなら、履歴をそのまま人が読めます。
4. /hora は毎回、現在地を判定する
1 セッションでプロジェクトが終わる前提はありません。/hora は必要なだけ開始・再開され、毎回どこにいるかを自分で判定します。

毎回、最初にやることはこの順です。
.claude/skills/ に実装スキルはあるか 無ければ → 止まり、装備を求める
0. どこでも git fetch origin --prune。hotfix が main に入ったか確認
1. その版に仕様書はあるか 無ければ → /hora-spec
2. 宣言されたリポジトリは全部あるか 無ければ → /hora-setup
3. 常に /hora-plan を走らせる
4. 未解決の blocking な質問はあるか あれば → 止まり、何を直すか伝える
5. _plan.md に未完了の機能はあるか あれば → 準備できた最初の 1 つに /hora-build
6. 全機能が終わり、掃引が合格でない → /hora-accept(版全体)
7. 掃引が合格 → main へ merge
作業を始める前に、判断を 1 行で報告します。たとえば「continuing 1.0.0. 4 of 11 features done, building #payroll from checkpoint 6.」のような形です。
手順 3 は、機能一覧が既にあっても毎回走ります。実装中も仕様書は動き続けて、節が追加・変更・撤回されるので、毎回突き合わせること以外に、それが計画に届く経路がないからです。
5. 書く時点と、コミットする時点を分ける
ここが、この設計の細かいけれど大事なところです。
| いつ | なぜ | |
|---|---|---|
関所の [x] を書く | 通過した瞬間 | 中断した実行は、止まったその関所から再開しなければならない |
.hora/ をコミットする | ゲートごとに 1 回(2 / 9 / 17 / 18 の後) | 1 機能 18 コミットは、誰も読まない履歴 |
一緒にすると、このどちらかの性質が失われます。通過のたびにコミットすると履歴が 18 倍に膨れて誰も読まなくなりますし、ゲートまで書かないと、途中で中断したときに 5 関所ぶんやり直しになります。分けておくコストはゼロなので、分けています。
6. 誰が書けるか
| ディレクトリ | 書くのは |
|---|---|
specs/ | 人間と、人間に代わって書く 2 つの skill |
.hora/ | その作業を記録する skill、hora-digester、各パッケージのインストーラ |
| 実装リポジトリ | /hora-setup、hora-implementer、メインセッション |
.hora/ に人間は書きません。読むだけです。逆に specs/ にキットは書きません。/hora-spec と /hora-plan が、人がたった今読んで承認した文言だけを書きます。
7. 質問は一方向に流れる
questions/<version>/open.md は追記のみのファイルです。
/hora-build が関所の終了条件を満たせないとき、自分で決めずに、ここに質問を書いて止まります。人が答えるときは、このファイルに返事を書くのではなく、specs/ を編集します。
.hora/questions/open.md ← skill が書く。「仕様のここが決まっていない」
│
▼ 人が読む
specs/<version>/spec.md ← 人が編集する。答えは仕様の文面になる
│
▼ 次の /hora-plan が突き合わせる
.hora/tasks/ ← 計画が更新される
質問はどれも仕様の穴なので、答えは仕様に書かれるべきだ、という考え方です。会話の中で「はい、こうして」と答えたことは、次のセッションでは消えています。仕様に書かれた答えは消えません。
8. 保守で .hora/ をどう読むか
しばらく前に作った機能について「なぜこの判断になったのか」を追うとき、見る場所は決まっています。.hora/questions/<version>/open.md には、その機能について何が質問されたかが残っています。.hora/acceptance/<version>/<feature-id>.md には、検収で何が指摘され、どの関所に戻されたかが残っています。関所がいつ通ったかは git log .hora/tasks/<version>/<feature-id>.md で分かります。
会話ログを掘り返す必要がありません。記録が構造化されていて、しかも git に入っている。これが、弊社が保守で一番重宝している場所です。
9. まとめ
再入可能なオーケストレーターの設計で、弊社が大事だと考えているのは 3 つです。状態は人が読める形で置くこと(チェックボックス付き Markdown で十分でした)、「状態を書く時点」と「状態をコミットする時点」を分けること、質問の答えは会話ではなく仕様に書くこと、です。
似た仕組みを作ろうとしている方の参考になれば幸いです。
原典はこちらです。 https://github.com/openreachtech/hora-core/blob/main/docs/architecture.ja.md