Open Reach Tech Inc.

状態ファイルを持たない再入可能オーケストレーター

Jiro Yamamotoのプロフィール写真
Jiro YamamotoBackend Developer

「Hora Kit の設計」シリーズの 6 本目。Hora Kit には state.json のような状態ファイルがありません。状態は .hora/ というディレクトリそのもので、Markdown のチェックボックスが状態です。実行のたびに現在地を判定する再入可能なオーケストレーターの設計と、それが保守で効いてくる理由を書きます。

Banner of 状態ファイルを持たない再入可能オーケストレーター

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

送信完了!

お問い合わせいただきありがとうございます。 担当者より24〜72時間以内にご連絡いたします。