本記事の内容は私(R2-san)の体験(実装コード)・判断・指示に基づきますが、文章作成は生成 AI が行い、私の監修・確認を経て公開しています。
前回、AIも人間も設計判断を忘れる で、設計判断を1件1ファイルで残す仕組みの話を書きました。
ここまで3本書いてきて、まだ一度も見せていない場面があります。R2 が、実際にリポジトリのファイルを書き換えるところです。3本とも「変更を作るまで」か「作ったあとの記録」の話で、肝心の変更が当たる瞬間を飛ばしていました。今回はそこを書きます。
先に結論を書きます。R2 が守っているのは、たった1行です。
失敗したら、何も変えない。そして、成功を主張しない。
うまくいかなかったとき、どちら側へ倒れるかを先に決めておく考え方です。確認が取れないなら「通す」側ではなく「止める・変えない・成功と言わない」側へ倒れる。これを fail-closed と言います(扉や弁が故障したときに、開くのではなく閉じるほう)。ただしこれは R2 自身への規律であって、人の意図を止める仕組みではありません。やりたいことは止めない。代わりに、いつでも戻せる状態にしてから通す。 失敗しても何も変わらないと分かっているから、通せるのです。
AIが出したものは、まだ「提案」でしかない
AI モデルが返してくるのは「こう変えたい」という案です。R2 はそれをそのままファイルへは書かず、いったん受け取って正式な提案に組み直します。形式を検証し、範囲外のパスや保護されたパスを弾き、通ったものだけを確定させる。
確定した提案は書き換えられず、中身から計算される短い印(fingerprint)が付きます。番号のような識別 ID ではなく、中身が 1 文字でも変われば印も変わる、というものです。プレビューも、承認も、適用も、この同じ印を通る。違う印の提案へ承認を使い回すことはできません。これが**「見て承認したもの」と「実際に当たるもの」が同じである**ことの担保です。
R2 の仕様には、一行こう書いてあります。
provider出力を直接Applyしない
承認が本当に守っているのは、これです。AI の出力を、人の目を経ずにファイルへ流し込まない。
「たぶんここだろう」で当てない
変更の表現は、最初は「変更後のファイル全文」でした。一部を直したいだけなのに全文を書き直させると、無駄も間違いも増えます。そこで部分編集を入れました。「今そこにある内容を一字一句そのまま引用して、それをこれに置き換える」という形です。行番号でも位置でもなく、内容そのもので場所を指す。当てる条件は、1つだけです。
一字一句、完全に一致し、
ファイル内でちょうど1回だけ現れる場合のみ、置き換える。
0回でも、2回以上でも、当てない。
「たぶん最初のほうだろう」とは判断しません。よく使われる差分の形式(統一 diff。変更箇所を行番号と前後の行で指すもの)も採りませんでした。行番号がずれていても前後の行がそれらしく揃っていれば、違う場所に当たってしまうからです。
途中まで当たった状態を、作らない
1つのファイルの複数箇所を直すときは、置き換えを並べて書きます。ここで新しい失敗の形が生まれかねませんでした。3つのうち2つまで当たって、3つ目で外れたら、ファイルが中途半端な状態で残る。全部をメモリ上で解決し終えてから、一度だけ書くことにしました。どれか1つでも外れたら、ファイルは1バイトも変わらないまま、まとめて拒否されます。
「外れた分は無視して、当たった分だけ書けばいい」という案は捨てました。却下理由が、この記事の主題そのものです。
「一部だけ密かに成功する」という、このコードベースが最も避けてきた種類の曖昧な挙動を生む。
もう1つ、厳密一致には副作用があります。当たらない。 AI モデルは、文脈にあるファイル内容を引用し直すときに、行末の空白を落としたり足したりします。画面上は何も見えないのに、比較は外れる。この見えない揺れだけは吸収することにしましたが、空白の違いを無視して比べたときも**「ちょうど1回」は崩しません**。編集距離のような賢い近似は捨てました。私が引いた基準は、**「なぜここに当たったのかを、後から人が説明できるか」**です。行末の空白は説明できます。似ているから、は説明になりません。
当てる前に、戻れるようにしておく
適用の直前に、R2 は変更対象のファイルの今の内容を控えとして保存します。途中で失敗したら、そこから書き戻す。珍しい仕組みではありません。問題は、最初の実装がそうなっていなかったことです。
開発で使っている AI が旧リポジトリと比較して調べたところ、控えの中身がプロセス内のメモリにしか存在しないことが分かりました。落として立ち上げ直したら、消える。しかも、その控えを使って実際に元へ戻す操作そのものが、どこにも存在していなかった。「戻すべきか」を決める仕組みはあったのに、決めたあとに戻す手段が無かったのです。控えの永続化と、書き戻す操作の追加は、同時にやると決めました。片方だけでは意味を持たないからです。
そして、書き戻しに失敗した場合。
何も主張せず、「回復が必要」のまま留まります。
「復元しました」とも「もう無理です」とも言わない。もう一度試せる状態にしておく。なお、戻せるのは提案が対象にしたファイルだけです。それ以外の場所にできたファイルは、戻さずに見つけて報告する。調べきれなかったときは、「変更なし」ではなく「分からない」と報告します。
あとから「やっぱり間違いだった」と気づく
ここまでの回復は全部、クラッシュや途中失敗という異常時の話です。一番よく起きるのは、そこではありませんでした。
適用も検証も全部成功して、Task は完了した。しばらくして、やっぱり間違いだったと気づく。
異常は何も起きていません。なのに当時、これに対する導線は1本もありませんでした。そこで、完了した Task を丸ごと巻き戻す経路を別に作りました。
- 単位は Task 全体。 その Task が触れた全ファイルを、Task 開始前の内容へ戻します(部分復元は意図的にやっていません)。
- 押すのはユーザー。 「今の内容」と「復元後の内容」の差分を見て、明示的に確認しない限り書き戻しません。
- 復元操作そのものが、復元可能。 書き戻す直前に必ず今の内容を控えとして保存し、その保存に失敗したら復元は中止して、ファイルを一切変更しません。
- 往復できる。 復元した状態からもう一度同じ操作を呼ぶと、復元前の内容へ戻ります。
危ないのは、完了したあとで誰かが同じファイルを触っている場合です。適用時点の内容を記録しておいて、今のファイルがそれと違えば「乖離あり」として差分を見せます。記録そのものが無い場合も、常に乖離あり側に倒します。
「復元操作そのものが復元可能」を徹底した理由は、次の節です。
回復を、git に委譲しない
git を使っている方は、たぶん一度は思ったはずです。それ、git でよくないですか? そうしません。理由を、記録の言葉のまま引きます。
本製品の対象ユーザーは git を使えない利用者であり、git 熟練者であっても git 操作の誤り(影響範囲の誤認)が事故要因になる。したがって回復・ロールバックは git への委譲ではなく製品自身のスナップショット機構が担う必要があり、「復元に失敗しても git で直せる」という安全網は存在しないものとして設計する。
効いているのは2つ目です。git を使える人でも、git の操作で事故は起きる。 影響範囲を読み違えたまま強い操作をして、戻せなくなる。スキルの不足ではなく、操作そのものが持つ危険です。
だから R2 は「最後は git がある」という前提を置きません。安全網が無いものとして作れば、安全網があってもなくても同じように安全です。逆をやると、安全網が無い人のところだけで事故が起きます。
人は忘れるし、間違えます。存在しないコードは復元されず、誤った操作でコードを永遠に失うこともあります。それは、起きてもらいたくない時によく起こるものです。
控えを取らない復元は却下しました。逃げ道が無いなら、復元操作自体が新しい取り返しのつかない操作になってはならないからです。乖離があるときは復元を禁止する、という案も却下しました。
逃げ道がない場合、復元不能は最悪の結果になる。差分の提示と明示確認を条件に許可するほうが、無自覚な上書きを防ぎながら、選択肢を奪わない。
危ないから禁止する、ではありません。危ないなら、見せて、確認を取って、通す。 これが「R2 自身への規律」の意味です。
人に、名付けさせない・覚えさせない・気を付けさせない
機能の案を評価するとき、私が弾く型が1つあります。人間のスキルに依存する案 ―― ユーザーに「名付ける」「覚える」「気を付ける」のどれかを要求する案です。できる人にはできるので、設計の場ではなかなか却下されません。「ちゃんとやれば大丈夫でしょう」で通り、通ったあとで、できなかった人のところで事故になります。
この記事で書いた2つも、この型で弾いた結果です。復元の単位を Task 全体にしたのは、どのファイルをどの世代へ戻すかを組み立てさせないため。記録が無ければ常に乖離ありにしたのは、「気を付けて確認する」を要求しないためです。
もう1つ、関係する原則があります。承認は、AI の振る舞いに掛けるものです。 AI は短いサイクルでバージョンアップし、その変化は予測できません。AI の振る舞いをユーザーに伝えることが、承認の意義でもあります。一方、R2 はアプリケーションであり、静的であることの信頼性が、この製品の骨格です。 R2 の振る舞いのほうは、製品仕様として定義できます。承認をいくつも積めば安全になる、という話ではありません。
まとめ
AI に変更を作らせる部分は、いちばん楽しいところです。でも、製品として使えるかどうかを決めているのは、作られた変更をどう当てるかのほうでした。
1. 失敗したら何も変えない。そして、成功を主張しない
途中で外れたらファイルは変わらない。調べられなかったことを「変更なし」と言わない。復元に失敗したら、成功も失敗も主張せず、回復できる状態のまま留まる。
2. これは R2 自身への規律であって、人への制限ではない
危ないから禁止する、ではありません。差分を見せて、確認を取って、通す。既定は常に「回復可能にして通す」側です。
3. 回復を、外の道具に委譲しない
「最後は git がある」を前提にすると、安全網が無い人のところだけで事故が起きます。だから R2 は自前の復元を持ち、その復元自体も復元可能にしました。
第1回のリポジトリ理解は「正しく捨てる」仕組み、第3回の設計判断の記録は「捨てない」仕組みでした。今回はその手前にある**「変えたものを、いつでも元に戻せる」**仕組みです。3つとも、AI も人も間違える、という同じ前提から出ています。
現在、この考え方を R2 Fugu Agent Runtime という個人開発の AI coding agent runtime で検証しています。
次回は、三者目のユーザーが握っている、許可と検証の話です。
関連記事
このシリーズ(Qiita・製品設計):
- AI coding agentは、なぜ毎回リポジトリを調べ直すのか ― Repository理解を使い捨てにしない設計
- AIに全部判断させるのをやめた ― AI ModelとCoding Agent Runtimeの責務をどう分けたか
- AIも人間も設計判断を忘れる ― Repository Decision Memoryを作った理由
この製品を AI に作らせる開発の進め方については、Zenn に別のシリーズを書いています:
- 設計も実装もレビューもAIがやる個人開発で、精度をどう出しているか — 全体像
- 作業指示書は、最初に書かせない ― 設計AIとのやり取りを全部見せる — 設計 AI 編
- 実装AIには、指示書のパスを渡すだけ ― コピペで始めた運用が仕組みになるまで — 実装 AI 編
- レビューAIには、報告を信じさせない ― 突合レビューをどうさせているか — レビュー AI 編