Quartet / Quintet

2026-09-08  ·  macos · launchd · shellscript · 個人開発 · devops

動いているのに一度も走らない定期実行 — 同じ仕組みを3回作り直した記録

記事を1日1本公開する仕組みがあります。 4日のあいだに、その定期実行の部分だけを3回作り直しました。

3回とも、前の版では見えなかった本物の失敗を直しています。

そしてどの版も、私が与えたテストには通っていました。 テストがいつも「いま動くか」で、「置かれる場所で動くか」ではなかったからです。

第1版: /tmp に置いたループ

nohup bash /tmp/daily-loop.sh &

動きました。公開もできました。予約が4件、これに乗っていました。

そしてマシンを再起動したら、4件とも消えました。

/tmp はプログラムの置き場ではありませんし、nohup は再起動を越えません。 どちらも知っていて、それでもこうしました。 私が確かめたかったのは公開のロジックで、公開のロジックには問題が無かったからです。

消えたことは、どこにも出ませんでした。 翌朝、新しい記事が無いだけ。エラーもありません。 エラーを記録するはずのプロセスが、そもそも存在しないので。

第2版: launchd に KeepAlive で常駐させる

スクリプトをリポジトリに移し、launch agent を書き、 落ちても戻るように KeepAlive: true にしました。

翌朝これが出ていました。

scripts/daily-publish.sh: line 59: node: command not found

launchd から起動されたジョブには、PATH がほとんどありません。 私の場合は /usr/bin:/bin:/usr/sbin:/sbin だけ。 node は /opt/homebrew/bin にあります。 自分のシェルから同じスクリプトを叩けば通ります。シェルには PATH があるので。

高くついたのはここです。同じループは Python で書いた回収処理も回していて、 python3/usr/binあります。 だからそちらは動き続けました。 ログは15分おきに正常な記録で埋まっていました。

部分的に動いている自動化は、止まっている自動化より気づきにくい。 止まっているものにはログがありません。これには、健康そうに見えるログがありました。

直し方は地味です。

export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"

壊れた1本だけでなく、定期実行が触る全部のスクリプトに入れました。 入口が1つだとは限らないからです。別のスクリプトが、別の親から同じものを呼びます。

第3版: ループは走っておらず、凍っていた

PATH を直して公開も通り、そのあとログが3時間止まりました。 プロセスは生きています。

$ ps -o pid,etime,command -p 72127
  PID     ELAPSED COMMAND
72127    02:55:52 sleep 900

sleep 900 が2時間55分生きていました。

これはノートパソコンです。スリープします。 15分で終わるはずの sleep は、システムが停止している間は進みません。 while true; do 仕事; sleep N; done の形は、 スリープするたびに上限のない穴が開きます。

ループは壊れていませんでした。通信で詰まってもいませんでした。 書いたとおりに動いていて、書いた私が「実時間は流れ続ける」と仮定していただけです。

第4版: 定期実行は、定期実行の仕組みに任せる

<key>StartInterval</key>
<integer>900</integer>
<key>RunAtLoad</key>
<true/>

スクリプトは1回分やって終わる形にしました。ループも sleep もありません。

StartInterval はこのために用意されています。 スリープ中に間隔が過ぎていれば、復帰時に起こしてくれます。 同じラベルのジョブを二重に起動しないので、要るはずだったロックも消えました。

古いループのスクリプトは、隣に残さず削除しました。 同じ仕事に入口が2つあると、どちらが動いているのか分からなくなります。

最後にもう1つ罠がありました

間隔が登録できているか確かめました。

$ launchctl list com.example.daily
{
    "Label" = "com.example.daily";
    "LastExitStatus" = 0;
    ...
};

StartInterval がありません。 「登録に失敗している」と結論して、また作り直すところでした。

$ launchctl print gui/$(id -u)/com.example.daily
    state = not running
    runs = 4
    last exit code = 0
    run interval = 900 seconds

launchctl list は、持っているキーを全部は出しません。 print は出します。

片方の道具の出力に無いことは、ジョブについての証拠ではありません。

4つの版に共通していたこと

どの修正も正しかった。どれも確認した。 そして確認は毎回、そのジョブが実際には走らない環境で行われていました。

残った規則は狭いですが、まだ反論できていません。

走る場所を変えたら、その場所で1回走らせるまで、それについて知っていたことを信じない。

ロジックを再テストするのではありません。同じロジックを、新しい環境で、 その環境固有の失敗を探しながら動かします。 PATH は何か、再起動を越えるか、スリープを越えるか、 そして確認に使う道具が何を教えてくれないか。

第4版はいま、何度かのスリープと1回の再起動を越えています。 証明ではありませんが、証拠が「置かれている場所」から出てきた最初の版です。


Claude Code に設計・実装・レビューを別々の人格として分担させ、GitHub Issue と ブランチを軸に並列開発を回すための設定一式を MIT で公開しています。 コピーして ./setup.sh を叩けば動きます。技術スタックには依存しません。

https://github.com/quintetkit/quartet

このワークフローだけで実際にツールを 1 つ作りました。Issue の分割から PR、 レビュー、マージまで記録が全部残っています。うまくいかなかった箇所も消していません。

https://github.com/quintetkit/mdlinkcheck

UI 設計人格・レビュー基準・Issue 単位の並列実行スクリプト・実践ガイド 10 章を 足したものは製品ページにあります。

実践ガイド全10章は Zenn Book で読めます(¥1,500・2章まで無料)。

https://zenn.dev/quintetkit/books/claude-code-parallel-workflow

設定一式(5人格・スクリプト・ガイド全文)の配布はこちらです。

https://quartet-dev.booth.pm/items/8807156

この運用そのものを配っています

4 人格版 Quartet は MIT で無料公開しています。UI 設計人格・レビュー基準・ Issue 単位の並列実行スクリプト・実践ガイド 10 章を足した Quintet は有料版です。

無料版を見る 製品ページ