AIワークフローをコードにした翌日、自分で壊した
7月25日、「AIワークフローをコードにしてみた」という記事を公開した。
その翌日、記事の前提を自分でひっくり返した。
あまりに早くて、少し笑ってしまう。記事を書き終えた時点では、BiliKitを作る中で増えていった工程管理がようやく安定したと思っていた。リスク分類、品質ゲート、独立レビュー、複雑さの上限。それらを再利用可能なCodex Skillにもまとめた。
ところが翌日、BiliKit自身が反例になった。
どんどん完成していったスクロール位置
作業していたのはM5.0だった。要件自体は単純だ。
一覧から動画を開き、再生画面から戻ったとき、入る前に見ていた場所へできるだけ戻す。
当時のBiliKitは独自のルーティングを使っていた。再生画面を開くと一覧Viewがツリーから外れ、戻ると作り直される。そのため、一覧の内容もスクロール位置も手動で復元する必要があった。
最初は選択した動画IDだけを保存した。これでカードを画面内へ戻すことはできたが、離れる前のviewportまでは戻らない。
そこで生のoffsetも記録するようにした。
その後に出てきた問題は、どれも筋が通っていた。
- コンテンツの高さが変わると、以前のoffsetへ到達できないことがある。
- 古い
ScrollViewのcallbackが新しいsnapshotを上書きする可能性がある。 - 非同期処理の遅い結果にはrequest identityとgenerationの確認が必要になる。
- ユーザーが自分でスクロールしたら、プログラム側の復元状態を解除しなければならない。
- 戻った画面が本当に離れる前と同じか、決定的テストとXCUIでも確認したい。
問題を一つ直すたび、実装は完成に近づいていった。
テストも通っていた。
今振り返ると、そこがいちばん厄介だった。テストもレビューも品質ゲートも、復元処理が正しく書けているかを確認していた。次の疑問は残ったままだった。
そもそも、なぜ一覧画面を破棄する必要があるのか。
いったん作業を止め、小さなSwiftUIのprobeを作った。条件分岐によるViewの置き換え、独自の復元、標準のNavigationStackを並べて比較した。
結果は分かりやすかった。
NavigationStackではroot Viewが作り直されず、元のstate identityも維持された。苦労して解いていた復元問題のかなりの部分は、自分たちで選んだナビゲーション構造から生まれていた。
そこでBiliKitは標準のTabView(.sidebarAdaptable)へ移行し、各TabがそれぞれNavigationStack(path:)を持つ構成に変えた。独自のAppRouteとAppReturnSnapshotは削除した。一方、上限付きの一覧workset、requestのキャンセル、プレイヤーのライフサイクル管理は残した。スクロール位置はSwiftUIの意味的なIDに任せている。
実際の人気動画一覧で2回往復した結果は次の通りだった。
0.195066 → 0.1948670.389877 → 0.389447ViewModelが生の数値offsetを保存し、clamp、復元latch、古いcallbackの分離まで抱える必要はなくなった。
プロセスはきちんと動いていた
今回、明らかに手を抜いたAgentがいたわけではない。
リポジトリの規約は読まれていた。リスクも拾えていた。検証レベルの選択にも大きな問題はなかった。古いcallbackによる汚染、到達不能なoffset、ライフサイクルの後始末、XCUIの操作など、実際の不具合も見つけている。
すべての工程が、同じ前提を受け入れていた。現在のナビゲーション構造を維持したまま、状態復元を正しく実装するという前提だ。
それがtask contractに入ると、後続のAgentはその方針に沿って非常にうまく作業する。
Reviewerは境界を確認し、red reviewerは失敗経路を探す。テストは増え、文書は詳しくなる。各工程が現在の案を正しくすると同時に、このまま進むのが当然のように見せていた。
前の記事で、project-governance-bootstrapを小さなコンパイラのようなものだと書いた。
リポジトリの事実+ アーキテクチャ文書+ テスト+ セキュリティ境界→ プロジェクトのルール今は、この例えには大きな穴があると思っている。
コンパイラが扱えるのは、入力されたものだけだ。
アーキテクチャ、task contract、既存実装のすべてがより単純な案を見落としていれば、Skillは今の案をさらに厳密にすることしかできない。経路依存は消えず、読みやすい文書、追加のチェック、強い慣性へ変わっていく。
修正まで別のプロセスにしかけた
問題に気づいた後も、最初に考えたのは今回の経験をルールにすることだった。
高リスク作業では先に代替案を列挙する。challengerを必須にする。decision ledgerを作る。問題設定を確認するreviewerを追加する。
書いている途中で、また違う気がしてきた。
プロセスが増えすぎるのを防ぐためにプロセスを増やしていた。Agentがtask contractを機械的に実行しないよう、さらに長いtask contractを作ろうとしていた。
さっき壊したものと、それほど変わらない。
今回はworkflow v2を作らなかった。
BiliKitの共同作業ルールをそのまま軽くした。一つのcommitで設定と文書を766行削除し、固定のAgent定義も5つ消した。現在のAGENTS.mdに残っているのは、プロジェクトの事実、アーキテクチャ境界、安全上の制約、実際に使える検証コマンド、commitの規約だ。
品質チェックは残っている。
SwiftPM、xcodebuild、ライフサイクルテスト、セキュリティ境界のチェックには、今も答えられる問いがある。ビルドが壊れていないか、キャンセルが正しいか、リソースが解放されるか、実際のAppが動くか。
固定のリスク色、task contractの書式、reviewer chain、複雑さの予算は、毎回行う儀式から外した。
本当に必要な変更では使える。すべての作業で工程を守った証明から始める必要はない。
二つのSkillsをどうするか
apple-dev-loopは、今のところ大きく変えなくてもよさそうだ。
Appleプラットフォームで検証方法を選び、実行する。SwiftPMで十分な場面、xcodebuildが必要な場面、.xcresult、署名済みApp、XCUI、Instrumentsまで必要な場面を切り分ける。
担当範囲は比較的はっきりしている。
project-governance-bootstrapは事情が違う。
これはリポジトリから作業ルールを作るためのSkillだった。十分な情報を読めば、そのプロジェクトに合ったワークフローを生成できるという前提が含まれている。
今回のBiliKitは、リポジトリ自身が間違った方針を詳しく説明していることもあると示した。
このSkillはもう一度考え直す。既存の事実と検証入口を整理するだけの道具まで縮めるかもしれないし、そのままarchiveするかもしれない。まだ決めていない。
新しい答えはまだない
現在の主要なソースとテストのディレクトリを数えると、BiliKitのSwiftは約2万5,800行ある。
プロジェクトが大きくなれば、一つのpromptと一度のdiff reviewだけでは足りない。以前のルールも無駄だったわけではなく、実際に多くの問題を見つけてくれた。
ただ、その経験を汎用ワークフローにまとめ、次のプロジェクトで進む方向まで自動的に選べるとは思わなくなった。
少なくとも今回はできなかった。
7月25日の記事は残す。あの時点で本当にそう考えていた。消したり、最初から分かっていたように書き換えたりしても面白くない。
この記事は、その翌日に起きたことの記録だ。
この先は、またプロジェクトにやり方を変えさせられたときに考える。