開発環境のバックアップを仕上げ、復旧手順をスキル化してGitHubで版管理する

開発claude-code-tools

「昨日の積み残しって何でしたっけ」

朝いちばんに聞いたのはそれだった。 返ってきたのは3系統。 noteのシリーズ企画の決裁フォームが0/3で止まったまま、開発環境バックアップ自動化の残り2項目、期日を過ぎたGoogleタスクが2件。

すぐ手を付けられるところから片付けてもらい、進捗のマークダウンも同時に更新させた。 決裁フォームは質問2に矛盾が残っていたので直し、連載名を4つ目の判断項目として足して立て直した。 静的化を済ませたアプリの旧ホスティングは、すでにダイノ0、課金0ドルで止まっていたのでタスクを閉じた。 リサーチ中だったトラックは完了扱いにして、次段のタスクを新しく積んだ。

論点6と7が何の話だったか

番号だけが残っていて、中身を思い出せなくなっていた。 整理させると、論点6は「消えた認証情報をバックアップに入れるか」だった。 論点7は「gitの設定やツール管理の設定のような、認証情報ではないファイルも守るか」だった。

自分はパスワード管理ツールの話だと思って口に出した。 番号が指していたのは別の話だった。 ただ、その案は論点6の第3の選択肢として筋がよかったので、推奨案に据えて決裁フォームを立て直させた。 3項目とも画面に描かれていることを確かめてから答えた。 回答はA、A、A。コメントなし。

認証情報はバックアップに含めない。 パスワード管理ツールに預けて、復旧のときにそこから書き戻す。 含めてしまうと、同期スクリプトが持つ秘密検出の仕掛けに自分で穴を開けることになる。 一方でgitの設定とツール管理の設定は認証情報ではないので、同期対象に含める。

復旧の手順書を、消える場所の外へ

そこで思いついたことをそのまま投げてみた。

これね、復旧の手順書なんですけど、例えば手順をスキルにしておくっていうのはどうですか?ユーザーレベルのスキル。それのスキルをGitHubにプッシュしておく、つまりバージョン管理しておくっていうのはどうですか?

手順書をマークダウンで書いても、置き場所が環境の内側なら、その環境が丸ごと消えた日に一緒に消える。 スキルの形にすれば、置き場所はスキルのディレクトリになる。 そこは毎朝の同期対象に入っている。

確かめさせたら、追加の仕組みは要らなかった。 スキルのディレクトリはすでに追跡対象で、tracked 90ファイル、同期スクリプトのallowlistにも skills/* が入っている。 ファイルを1本置くだけで、翌朝の同期でGitHubに載る。

そのまま復旧スキルを書かせた。 対象ファイルの中身は読ませず、構成だけを確認させた。 認証情報が混じる場所を触るからだ。 最初の一手だけはGitHubのREADMEに置いた。 手元に何も残っていない状態から辿れる起点が要る。

片方のバックアップ先が3回続けて失敗していた

ついでに現状を見に行かせたら、2系統あるバックアップ先の片方が3回連続でこけていた。 原因の仮説を記録に残してから直した。

片系統だけ遅れている状態は、翌日になっても見た目では気づけない。 系統間の差分を判定する処理を足して、1回で検知できるようにさせた。

サーバーの生死をcurlで判定して間違えた

決裁フォームのローカルサーバーが生きているかどうかで3往復した。 最初は落ちていたので立て直した。 次はcurlの結果が「死んでいる」と出たので、また立て直しに走った。 ところがブラウザで開くと普通に表示される。 サーバーは生きていて、curlの結果のほうが誤りだった。 そのあとまた止まり、回答が届かないまま終わった。

Xの動画が落ちてこない。yt-dlpの更新を疑った

朝、Xの動画を保存するボタンを押したら、手動コマンドを促すタブが開いた。 拡張機能が呼んでいるyt-dlpの更新だろうと当たりをつけて、そのディレクトリを見に行かせた。

切り分けは片端から空振りした。

  • 同じコマンドを手で叩く → 成功。yt-dlp自体は動いている
  • ネイティブホストを単体で叩く → 成功
  • 日本語を含む保存先パスで叩く → 成功
  • 拡張機能IDとレジストリ登録 → 一致
  • PATHとyt-dlpの版(2月2日以降の更新なし) → 正常

yt-dlpの更新という線が消えた。 次に疑ったのはChromeで、7月28日に更新が入っていた(拡張機能の最終更新は7月5日)。 拡張機能のコンテキストからcookies APIとネイティブホスト接続を直接叩かせたが、Cookie付きの実運用と同じ経路まで通ってしまった。

今は全経路が正常で、自分が踏んだのは一時的な失敗だった。 そこで時刻の証拠を取らせた。

OS起動が06:09:07。 Chrome起動が06:09:32。 保存先のクラウドドライブのマウント完了が06:10:36。 自分がボタンを押したのは06:10から06:14のあいだ。

PC起動直後、保存先がまだ生えていない1分の窓に入っていた。 しかもネイティブホスト側の os.makedirs がtryの外にあって、例外が出るとPythonがその場で死ぬ。 Chromeに届くのは Native host has exited. だけで、理由は画面に出ない。

自分でも「Gドライブがまだ起動中だったのでは」と言っていて、そこは当たっていた。

落ちても理由が読める形に直す

エラーの内容を画面に明記してほしいと伝えて実装させた。 数秒リトライして、駄目ならダウンロードフォルダへ退避する。 保存先のパスと退避の理由は、通知とトーストに載せる。

保存先の解決だけを単体で検証させた。 正常時は0.1秒、未マウント時は4秒リトライしてから退避に落ちる。 退避したのに「パスをコピー」が元の保存先を返すバグも見つかったので直した。

トーストは実際に描画させて目で見た。 タイトルもフルパスも退避理由もボタンも、折り返しを崩さずに出ている。 退避なしの通常表示も同じ手順で確認した。

学習ゲートを飛ばしてコミットした

コミットを頼み、学習ゲートはスキップでいいと伝えた。 今回の修正は落ちたときの見え方を直す作業で、クイズにする値打ちのある設計判断ではない。

そこには今回の修正と無関係な未コミット変更が混ざっていたので、hunk単位で自分の分だけをステージさせた。 既定ブランチがmasterで、直接コミットしてよいか確認が来た。 個人用のリポジトリで履歴もmaster一直線なので、直コミットで進めた。

残った差分を「作りかけの機能」と説明されたので、中身を見てコミットできるならしておいてほしいと頼んだ。 そこから訂正が2回入った。 1回目は、作りかけではなく完成済みの検索エクスポート機能だった。 2回目は、その機能の既知の制限として説明された内容が、存在しない関数の話だった。 破棄した実装案の頃のメモが書き換え忘れで残っていて、コードを見ずにそれを引き写していた。

メモの「既知の制限」欄は、実装より先に古くなる。

動画を2つに割って、ffmpegを入れ替えるまで

31分21秒の動画を2つに割った。 15分40.5秒で切り、再エンコードなしのストリームコピーで書き出した。

ついでにffmpegを最新版に入れ替えさせた。 ここで3回つまずいた。

  1. scoopがPowerShell 5.1で壊れていた(PS7のモジュールパスを拾っていた)ので、pwshで実行させた
  2. 7zipは入ったがffmpegの展開で失敗した。手で展開すると通るので、キャッシュを疑って入れ直した
  3. 入れ替えたのに ffmpeg が4.2.3のままだった。ImageMagick同梱の古いffmpegがPATHで先にいた

ユーザー環境変数のPATHでscoopのシムを前に動かして解決した、という報告が来たので、もう手当てすることはないかと確かめた。 そこで訂正が入った。 ImageMagickはマシン側のPATHにも入っていて、Windowsはマシン側をユーザー側より先に評価する。 新しいターミナルを開いても4.2.3が勝つ。

管理者権限がないのでマシンPATHは書き換えられない。 ImageMagick側にあるのは ffmpeg.exe だけで ffprobe はないと分かったので、影響はコマンド1つに絞れた。 シェルのプロファイルでPATHを組み直す方式にした。

ここでもう1回間違えた。 「scoopのシムがPATHにあるならスキップ」というガードだと、すでにあるので何もせず、ImageMagickが先頭に残る。 既存の出現を消してから先頭に置く形に直させて、ログインシェルと非ログインシェルの両方で8.1.2が返ることを確かめた。

「もう大丈夫です」が2回ひっくり返った日だった。

フェーズタイマー付きで流したビルドが落ちた

昼前、各工程の所要時間を測るタイマー付きで pnpm generate を回した。 10時32分38秒スタート。 途中で止まった。

止めていたのは検証スクリプトで、当日書いた記事1本がブログのカレンダーに出ていないと言っている(234/235)。 frontmatterを比べても差はない。 記事ページ自体はprerender済みで、HTMLもpayloadも生成されている。 それなのにカレンダーのHTMLには1回も現れない。

サブエージェントを並列で立てて切り分けさせている途中で、セッションの上限が近づいた。 いったんドキュメントに落とさせた。 失敗箇所、確定した事実、未確定の論点、そして「再ビルドは20分かかるのでタイムアウト10分では足りない」ところまで書かせた。 修正は1行も入れていない。

原因はコードではなかった。 pnpm generate の最中に pnpm dev が動いていて、両方が同じNuxt ContentのSQLiteを掴んでいた。 DBにdev用のキャッシュテーブルが同居していること、generateが終わったあともdevが同じDBを書き換え続けていること(更新時刻が10時51分)が確認できた。

もう1本の検証スクリプトは、別のリグレッションを拾った。 ある日記ディレクトリがdistに存在しない。 ビルドログではprerender成功と出ているのに、実物がない。 そのログには、Nuxt Contentの内部クエリAPIのエラーが94回出ていた。 2つの失敗は1つの原因に収束した。

タイマーは工程ごとの時間を測るためのものだが、この日に見つかったのは、generateとdevを同時に走らせていたという運用の穴だった。

学び

  • 復旧の手順書は、消える場所に置くと一緒に消える。手順をスキルにすると、毎朝GitHubに載る場所へそのまま乗る
  • 認証情報はバックアップに入れず、パスワード管理ツールに預ける。同期側の秘密検出に自分で穴を開けない
  • ローカルサーバーの生死は、curlの応答だけでは決めない。ブラウザで開いて確かめる
  • 「たぶんライブラリの更新」は当たらないことがある。起動直後の1分の窓は、時刻の証拠でしか見えない
  • 例外でその場で死ぬプロセスは、利用者に理由を渡せない。リトライ、退避、理由の明記までを1セットにする
  • コードを見ずに書かれた申し送りは信じない
  • ビルド中にdevサーバーを上げたままにしない

持ち越し

  • noteシリーズ企画の決裁フォームは4項目とも未回答。立て直してあるので回答する
  • ビルド失敗は原因まで判明。devを止めてから流し直し、デプロイまで通す