AIに書かせたコードを理解せずにコミットしない仕組み — 学習ゲート付き開発ループ(pre-commit × /learnクイズ × Turso)の実装と全リポジトリ適用

開発claude-code-tools

学習ゲート付き開発ループを作った — クイズに合格しないとコミットできない

AIがコードを書き、自分は中身を追わないままコミットが積み上がっていく。この流れに歯止めをかけるため、「ステージ済みの変更についてクイズに全問合格しないとコミットが通らない」開発ループを1日かけて作った。夜には全リポジトリ共通のゲートに昇格させた。

仕組みの全体像

構成要素は4つ。

  • pre-commit フック: 受領証(.git/learn-receipt)が無ければコミットをブロックする
  • /learn コマンド: ステージ済み差分について「直感優先の解説 → ブラウザクイズ → 全問合格 → 受領証発行」を実行する
  • /review コマンド: Turso の履歴DBから due が到来した概念を間違い優先で抽出し、間隔反復で再出題する(コミットゲートには影響しない)
  • learn-quiz MCP サーバー: present_quiz ツールがブラウザにクイズページを開き、回答が送信されるまでツール呼び出しをブロックして待つ。正誤の履歴は Turso に保存する

クイズを作るのは /learn だけで、pre-commit は受領証を照合するだけ。この役割分担を自分が飲み込むまでに一往復かかった(「コミット段階でテストが作られる」と思い込んでいた)。

朝: Downloads に転がっていた計画書のレビューから

前夜に作らせた計画書 learning-gate-spec.html が Downloads に置きっぱなしだったので、まず内容をレビューさせた。MCP の elicitation URL モードを使う前提について裏取りさせると、「概ね裏付けられたが、1点だけ検証必須の穴がある」との報告。

そのまま memo/2026-07-06/ に移動して rev.2 に更新させた。ついでに、元ファイルの mermaid CDN が404で図が一度も描画されていなかったことも見つかり、SRIハッシュを付けて修正、ブラウザで SVG 2個が描画されるところまで確認させた。実装は別セッションでやりたかったので、引き継ぎプロンプトを出させて切り替えた。

実装: スパイクで前提を潰してから本実装

実装セッションではまず、MCP SDK 1.29.0 が elicitation URL モードに対応していることを確認させ、スパイクサーバーで実機テストに入った。計画書段階で「検証必須の穴」とされていた点は、案の定実機で顕在化した。子プロセスの Claude Code から呼ぶと Client does not support url elicitation が返ってくる。クイズはサーバーが直接ブラウザページを開いて回答を待つ方式に落とした。

スパイクと並行して本実装を進めさせた。クイズ画面のテンプレート、サーバー本体、/learn/review コマンド、型チェック、ユニットテストまで。ブラウザ側の自動テストでは、クイズの選択肢をクリックしても DOM に反映されない(checked=0 のまま)場面があり、eval で直接選択して送信させる回避も挟んだ。

MCPサーバーの単体テストを繰り返す

サーバーの疎通は、子プロセスの Claude Code に最小プロンプトを流し込む方式で検証させた。

  • 「present_quiz を exactly once 呼び、結果の JSON をそのまま出力しろ。他のツールは呼ぶな」という1行プロンプト
  • その前段として「Reply with exactly: OK-HAIKU」「OK-DEFAULT」だけを返させるモデル別の起動確認

最初の実行は「MCPサーバーはまだ接続中のため、ツールが利用できません」と言って諦めて終了した。プロンプトに「接続中でも諦めるな、数秒でツールが現れるから呼び続けろ」と一文足したら通った。

肝心の長時間ブロック検証も通った。ツール呼び出しが60秒ブロックするテストと6.5分(390秒)ブロックするテストの両方で、クライアントに切られず結果の JSON が返ってきた。「ツールの戻りを人間の回答待ちに使う」設計がここで成立した。

実運用テスト: 前提を伝えずに「コミットしてください」

scratchpad に learn-gate-test という使い捨てリポジトリを作り、税率計算の calc.js を題材にした。何も前提を伝えず「ステージ済みの変更をコミットしてください」とだけ頼むと、pre-commit がブロックし、「--no-verifyLEARN_SKIP=1 での回避は禁止されているので、先に /learn を実行する必要があります」と返ってきた。設計どおりの門前払い。

/learn を実行するとブラウザにクイズが出た。4問中3問正解。浮動小数点誤差の影響を問う1問を落とした。「エラーになる」ではなく「税込価格が本来より1円安く計算される、気づきにくいズレが静かに起きる」が正解だった。誤答の解説を読んでから、その概念だけ再出題され、全問正解で受領証が発行され、コミットが通った。自分で作らせたゲートに自分が1問落ちる。ゲートを作った意味はここにあると思う。

/review も動かした。Turso の履歴から due の概念2件が間違い優先で出てきて、2問中1問正解。正解した概念は次回の出題が先送りされ、間隔反復の間隔が伸びることを確認した。

小ネタ: テストセッションで /learn と打ったら、Git Bash のパス展開で「C:/Program Files/Git/learn」という実在しないパスに化けて届いた。それでもテストセッション側は意図を推測して /learn を実行してくれた。

履歴の保存先確認と、解説文を使い捨てない改修

mdx-playground 本体でも「コミットしてみてください」→ ブロック → /learn → 3コミット完了、の流れを通した。その後 Turso のテーブルを眺めながら「今回の履歴はどこに載っているか」を確認させると、attempts テーブルに正誤は残るが、クイズ前に生成される解説文はブラウザに表示するだけの使い捨てだと分かった。

せっかくの解説コンテンツなので、再生成より保存の方が安いと判断し、解説マークダウンを mdx-playground のレッスンコンテンツとして自動保存する改修を入れさせた。ローカル dev 限定の /lessons ページで一覧でき、ブログ一覧と本番ビルドからは除外する。保存フォーマットは「解説+図+扱った論点リスト」の汎用トーンに直させ、設問・選択肢・個人の正誤は本文から消した。dev 環境の描画確認まで済ませ、派生した問題集アプリの計画書は Codex にレビューさせて指摘4件を反映した。

午後: 別リポジトリで learn が発動しない

午後、別リポジトリでコミットしたとき学習ゲートが発動しなかった。理由を調べさせたら単純で、pre-commit は git のリポジトリ単位の仕組みであり、フックを入れたのは mdx-playground だけだった。

ここで引っかかったのが、/learn~/.claude/commands/ にあるユーザーレベルコマンドなのに、なぜ門だけプロジェクト単位なのかという点。答えは両者が別システムに住んでいる非対称性だった。Claude Code のコマンドには最初からユーザーレベルの置き場所があるのに対し、git にはユーザーレベルのフック機構がデフォルトでは存在しない。

そこで昇格させた。

git config --global core.hooksPath ~/.claude/githooks

これで全リポジトリ共通のゲートになる。スクラッチリポジトリでブロックが機能することも検証させた。なお、リポジトリローカルに core.hooksPath を持つリポジトリ(mdx-playground や husky 系)ではそちらが優先される。

仕上げ: ~/.claude のコミットと LEARN_SKIP

最後に ~/.claude のフックと CLAUDE.md 追記を、テーマごとに2コミットに分けてコミットさせた。このコミット自体は LEARN_SKIP=1 で通っていたので理由を問い詰めたところ、「設定同期のような学習価値のない機械的コミットは、理由をログに残してスキップしてよい」という自分で決めた運用ルールに沿った判断だった。逃げ道の使われ方まで含めて設計どおりに動いていたので、そのまま了承した。

学び

  • MCP のツール呼び出しは6分半ブロックしても切られない。ツールの戻り値を「人間がブラウザで回答するまでの待ち」に使う設計が成り立つ
  • Claude Code クライアントは elicitation URL モードに未対応だった(Client does not support url elicitation)。計画書の「検証必須の穴」がそのまま実機で出たので、スパイクを先に回した判断が効いた
  • 子プロセスの Claude Code は MCP サーバーの接続完了前に諦めることがある。テスト用プロンプトには「接続中でも待って呼び続けろ」を明示する
  • git のフックはリポジトリ単位が前提。core.hooksPath のグローバル設定でユーザーレベルに昇格できるが、ローカル設定を持つリポジトリではそちらが勝つ
  • ゲートには正規の逃げ道(LEARN_SKIP+理由ログ)を最初から用意しておくと、機械的コミットで運用が破綻しない