1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Code の /loop は「固定間隔」と「セルフペーシング」をどう使い分けるか — launchd 17ジョブ運用からの実装メモ

1
Posted at

はじめに

当社(合同会社ジョインクラス)は、記事公開・SNS配信・売上集計などの業務を launchd の17ジョブと Claude Code で回しています。自動化率は98%ほどです。

その運用で一番迷ったのは「待つ処理」の書き方でした。デプロイが終わるまで待つ、PRのCIが通るまで見張る、レートリミットが解けたら再実行する、といった処理です。

Claude Code の /loop には2つのモードがあります。

  • 固定間隔: /loop 5m /foo のように間隔を指定する
  • セルフペーシング: 間隔を省略する。モデルが毎回 ScheduleWakeup で次の起床時刻を決める

この記事では、僕が実際に使っているスクリプトをもとに、2つのモードの使い分けの基準・手順・落とし穴・検証方法をまとめます。

結論:判断基準は「状態がどれくらいの速さで変わるか」

状況 推奨 理由
監視対象の周期が決まっている(毎時の集計、5分ごとのヘルスチェック) 固定間隔 毎回同じ処理。モデルに間隔を判断させる意味がない
終わる時刻が読めない外部処理(CI、デプロイ、Zennの反映) セルフペーシング 状況に合わせて「8分後に1回だけ見る」のように調整できる
ハーネスが完了を通知してくれる処理(バックグラウンドの Bash やサブエージェント) どちらも使わない/長めのフォールバックだけ置く 完了すると自動で再起動されるので、ポーリングは無駄
毎日決まった時刻に動かす、PCを閉じても続ける /loop ではなく launchd か /schedule /loop はセッションが生きている間しか動かない

3行目は特に見落としやすいです。run_in_background で起動したコマンドは、終わるとエージェントが自動で呼び戻されます。そこへ60秒ごとのポーリングを足しても、トークンが減るだけです。

1. 固定間隔:毎回同じチェックをするとき

当社のジョブ群は、すべての処理で common.sh を source しています。以下はそこから抜き出した部分です(実ファイルは .company/scripts/common.sh)。

common.sh(抜粋)
#!/bin/bash
set -euo pipefail

export PATH="/Users/kyoagun/.nvm/versions/node/v22.17.0/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
export HOME="/Users/kyoagun"

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
COMPANY_DIR="$PROJECT_ROOT/.company"
LOG_DIR="$COMPANY_DIR/scripts/logs"
SCRIPT_NAME="$(basename "${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}" .sh)"
mkdir -p "$LOG_DIR"

on_error_notify() {
  local exit_code=$?
  local line_no=${1:-unknown}
  log_error "スクリプト失敗: $SCRIPT_NAME (行:$line_no, 終了コード:$exit_code)"
  if [ -n "${SLACK_WEBHOOK_URL:-}" ]; then
    curl -sf -X POST -H "Content-Type: application/json" \
      -d "$(jq -n --arg text ":x: [AI-CEO] $SCRIPT_NAME 失敗 (行:$line_no, コード:$exit_code)" '{text: $text}')" \
      "$SLACK_WEBHOOK_URL" >/dev/null 2>&1 || true
  fi
}

ジョブの失敗はここで Slack に通知されます。ただ作業中のセッションでは、「ログの末尾に ERROR が出ていないか」を一定間隔で見ているほうが早く気づけます。このように毎回同じことをするチェックは、固定間隔で十分です。

Claude Code で実行
/loop 10m .company/scripts/logs/ 配下で直近10分に更新された *.log の末尾30行を確認し、ERROR または 終了コード が出ていたら原因と対象スクリプト名を1行で報告。何もなければ「異常なし」とだけ返す

ポイントは次の3つです。

  • 出力を短くするよう指定する: 「何もなければ1行で」と書いておかないと、毎回長いレポートが返ってきて画面が埋まります
  • 間隔はログの更新頻度に合わせる: 当社のジョブは最短でも数十分おきなので、1mにしても新しい情報はほぼありません
  • 止めるときは明示する: セッションを閉じれば止まりますが、続けたまま止めたいときは「ループを止めて」と伝えます

2. セルフペーシング:いつ終わるか読めない外部処理

Zennには「pushは成功したのに記事が公開されない」ことがあります(レートリミットに当たったケースです)。当社では次のスクリプトで再デプロイしています(実ファイルは .company/scripts/retry-zenn-deploy.sh)。

retry-zenn-deploy.sh
#!/bin/bash
# Zennデプロイ失敗記事の再デプロイスクリプト
set -uo pipefail

REPO_DIR="/Users/kyoagun/workspace/zenn-content-company"
LOG_FILE="/Users/kyoagun/workspace/one-ceo/.company/scripts/logs/zenn-publish.log"
ARTICLE="articles/claude-code-morning-dashboard.md"

cd "$REPO_DIR"
echo "" >> "$ARTICLE"   # 空行を足して差分を作る
git add "$ARTICLE"
git commit -m "fix: レートリミットで失敗した記事を再デプロイ (claude-code-morning-dashboard)"
git push origin main

# 同日の通常公開をスキップさせるロック
touch "/tmp/zenn-publish-$(date +%Y%m%d).lock"
echo "$(date): Retry deploy + lock created: claude-code-morning-dashboard.md" >> "$LOG_FILE"

# 1回限りのlaunchdジョブなので自分を削除
launchctl unload ~/Library/LaunchAgents/com.joinclass.retry-zenn-deploy.plist 2>/dev/null
rm -f ~/Library/LaunchAgents/com.joinclass.retry-zenn-deploy.plist

push のあとに公開されたかを確認する処理は、反映まで数十秒で終わることもあれば、十数分かかることもあります。固定5分だと早すぎて空振りが続くか、遅すぎて失敗の検知が遅れます。ここではセルフペーシングを使います。

Claude Code で実行(間隔を省略=セルフペーシング)
/loop https://zenn.dev/joinclass/articles/claude-code-morning-dashboard を curl -s -o /dev/null -w '%{http_code}' で確認。200 になったらループを止めて報告。404 が30分以上続いたら retry-zenn-deploy.sh を1回だけ実行し、その後は間隔を広げて確認を続ける

セルフペーシングでは、モデルが毎回 ScheduleWakeup を呼んで次の起床時刻を決めます。たとえば次のように、状況に合わせて間隔が変わっていきます(イメージです)。

delaySeconds: 240  reason: "push直後。Zennの通常の反映時間に合わせて4分後に1回確認"
delaySeconds: 600  reason: "まだ404。レートリミットの可能性があるので間隔を広げる"
stop: true

セルフペーシングの落とし穴

  1. 短い間隔でポーリングしない: ハーネスが完了を把握している処理(バックグラウンドの Bash など)は、終わると自動で再起動されます。この場合はポーリングせず、1200秒以上のフォールバックだけを置きます
  2. 終了条件をプロンプトに書く: 「200 になったら止める」のような条件がないと、ループが続いて止まりません
  3. 副作用のある処理は回数を制限する: 「1回だけ」と書かないと再デプロイが繰り返され、Zennのレートリミット(1日1件)にまた引っかかります。当社では実際に複数回引っかかったことがあり、その結果、上のロックファイル方式を作りました
  4. 遅延の範囲は60〜3600秒: 1時間以上待つ処理は /loop に向きません。launchd を使います

3. そもそも /loop を使わないケース

僕は以前、macOS の cron をそのまま使っていました。ところがフルディスクアクセスの権限がなかったため、cron ジョブがすべて動いていなかったことがあります。そこで launchd に全面移行しました。毎日決まった時刻に動かす処理や、セッションを閉じても続けたい処理は、今もすべて launchd です。

検証コマンド
# 登録済みジョブと直近の終了コードを確認(2列目が0以外なら失敗)
launchctl list | grep com.joinclass

# 1回限りジョブが自分を削除できたか
ls ~/Library/LaunchAgents/ | grep retry-zenn-deploy || echo "削除済み"

使い分けをまとめると、次のようになります。

  • launchd: 毎日決まった時刻に動かす処理、永続化が必要な処理
  • /loop 固定間隔: 作業中だけ、一定周期で同じチェックを回したいとき
  • /loop セルフペーシング: 終わる時刻が読めない外部の状態を、作業中に見張るとき

今日試すための手順

  1. 自分の自動化から「待っている処理」を1つ選ぶ(CI、デプロイ、ビルドなど)
  2. その処理の終わる時刻が決まっているか、毎回ばらつくかを確認する
  3. 決まっていれば /loop 5m ...、ばらつくなら間隔を省略して実行する
  4. プロンプトに 終了条件・出力量・副作用の回数上限 の3つを書く
  5. 数回動かしたら reason を見て、間隔の判断が妥当かを確認する

おわりに

当社が最初に作った自動化は、間隔をすべて固定にしていました。その結果、空振りのチェックに無駄なトークンを使っていました。「状態がどれくらいの速さで変わるか」でモードを選ぶようにしてからは、監視のコストと検知の遅れの両方が減っています。

launchd・Hooks・/loop を組み合わせた自動化の全体像は、拙著『Claude Code 全自動化バイブル』で解説しています。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?