手で動かすと通る確認がcronでは止まった。相対パスはどこから読まれるか

手で動かしたときはすんなり通った確認が、その夜のcronでは「ファイルが見つからない」で止まりました。同じ手順、同じ確認です。違ったのは、実行した場所だけでした。
止まったのは、毎晩23:00に動く自動ジョブです。Hermes Agentというエージェントのcronで、記事の下書きを作り、OG画像(SNSでリンクを共有したときに表示される画像)を生成し、下書き状態のPRを出すところまでを無人で行います。この手順の4番目に、OG画像を作る前の決まりを置いています。画像生成の経路が承認済みモデル(OpenRouter経由のgoogle/gemini-3.1-flash-image)になっているかを確認し、違えば何も作らず、Gitの状態も変えずに止める決まりです。生成した後で別のモデルが返ってきたときも止めます。
確認を具体化した未明、手で動かすと通った
10月12日の未明に、この確認のやり方を2点へ具体化しました。1点目は、設定ファイル~/.hermes/config.yamlのimage_genで、provider: openrouterとmodel: google/gemini-3.1-flash-imageになっていること。2点目は、そのモデルIDがプラグインplugins/image_gen/openrouter/__init__.pyの許可リスト_IMAGE_API_MODELSに入っていることです。
具体化した直後に手で1回動かすと、確認は通り、下書きのPRまでできました。確認の中身は正しく、実際に通ったのだから、夜の定期実行も通るものと考えていました。
23:04の報告だけが残った夜
その夜、23:00の定期実行は止まりました。23:04に届いた報告の要旨はこうです。
OG生成の必須確認で
plugins/image_gen/openrouter/__init__.pyが見つからず、指定モデルの許可リスト照合を完了できなかったため停止。Git作業ツリーは変更せず、mainはorigin/mainと同期済み。
記事も画像もPRも、この回は作られていません。残ったのは報告だけ。ただ、この止まり方自体は悪くありませんでした。許可リストを照合できないまま黙って画像を作るより、「見つからないから止める」のほうが決まりに合っています。確認は設計どおりに働きました。働けなかったのは、確認が読むはずのファイルへの道順です。
相対パスは読む側の足元で解決される
原因は手順の書き方にありました。手順の4番目は、プラグインのファイルをplugins/image_gen/openrouter/__init__.pyという相対パスで書いていました。手で動かしたときは、このパスを解決できる場所から実行していました。でも、cronの作業ディレクトリは記事リポジトリ側です。そこから見ると、この相対パスにファイルはありません。
相対パスは、指示を書いた人の足元ではなく、指示を読んで実行する側の足元で解決されます。当たり前のことですが、手で動かして通った成功体験が、この当たり前を隠しました。どの作業ディレクトリから読まれるかを実行する側の事情に任せず、指示文の側で決めておく必要がありました。
絶対パスへ直し、再実行は人に残す
報告を読んだ直後に、手順のパスを絶対パス~/.hermes/hermes-agent/plugins/image_gen/openrouter/__init__.pyへ直しました。あわせて「作業ディレクトリ基準で解決しない」と手順に明記しています。
直した後の確認は読み取りだけです。
test -f ~/.hermes/hermes-agent/plugins/image_gen/openrouter/__init__.py && echo EXISTS
EXISTS
許可リスト_IMAGE_API_MODELSの中身も読み、93行目付近にgoogle/gemini-3.1-flash-imageが入っていることを確かめました。
ジョブは再実行していません。すぐ動かせば今夜のうちに白黒つけられますが、もう一度動かすかどうかは人が決めることにして、次の定期実行まで待たせています。直した直後に手で動かして「通ったから大丈夫」と判断するのは、まさに今回の失敗と同じ形です。手で通ったことは、cronで通る保証にはなりませんでした。次に動かすときは、cron自身の足元で通るかを見ます。
