0
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?

24/7自動化の死活を1コマンドで確かめる ― サイレント失敗を可視化する

0
Last updated at Posted at 2026-06-23

前回の autopilot 記事で launchd から Claude を無人で走らせる構成を紹介しました。今回はその裏側 ―― 無人ジョブが黙って止まっていた事実を、誰も気づかないまま放置してしまう問題とその対処の話です。

無人自動化の本当の怖さはバグではなく、サイレント失敗です。launchd のジョブが exit 78 でクラッシュしていても、hooks のスクリプトが実行権限を失っていても、agentmemory がポートを開いたまま worker 不在の「半生状態」で動いていても、表面上は何も起きていないように見えます。この状態が6日間続いて誰にも気づかれなかった実例が automation-health.sh のコメントに残っています。

困りごと:黙って死ぬ

自律ループを組むと監視対象が増えます。私の構成だけでも:

  • launchd ジョブ(skill-harvest / skill-curate / agentmemory など)
  • Stop hook / PreToolUse hook の実体スクリプト
  • 会話ログを長期記憶に変換する Stop hook
  • Obsidian Vault の自動更新
  • 週次・月次バッチ(env-audit / plugin-purge / dotfiles-snapshot など)

これらが全部正常かどうかを手で確認する気にはなれません。かといって個別に launchctl list を叩いても「全体像」が見えない。

必要なのは 全自動化をまとめて点検して、GREEN / WARN / FAIL の3値で教えてくれる1コマンドです。

設計:3値判定と OK/WN/NG ヘルパ

~/.claude/scripts/automation-health.sh のコアはシンプルです。

fail=0; warn=0
ok()  { printf "  ${GRN}${RST} %s\n" "$1"; }
wn()  { printf "  ${YEL}${RST} %s\n" "$1"; warn=$((warn+1)); }
ng()  { printf "  ${RED}${RST} %s\n" "$1"; fail=$((fail+1)); }

チェック項目を ok / wn / ng のいずれかで評価していき、最後にこう判定します。

if   [ "$fail" -gt 0 ]; then
  printf "  ${RED}✗ FAIL${RST}  red=%d warn=%d\n\n" "$fail" "$warn"; exit 1
elif [ "$warn" -gt 0 ]; then
  printf "  ${YEL}⚠ WARN${RST}  warn=%d (致命的問題なし)\n\n" "$warn"; exit 0
else
  printf "  ${GRN}✓ ALL GREEN${RST}  全自動化が正常稼働\n\n"; exit 0
fi
  • ALL GREEN:全項目 OK。exit 0
  • WARN:致命的ではないが要注意。exit 0
  • FAIL:RED が1つでもあれば exit 1

exit code を分けることで、daily-brief.sh や CI から || で後続処理を繋げられます。

チェック項目:何を見ているか

スクリプトは9節に分かれています。実コードから主要な判定を抜粋します。

1. launchd ジョブの生死

for job in com.shun.skill-harvest com.shun.skill-curate; do
  line=$(launchctl list 2>/dev/null | grep -E "\b${job}\b")
  if [ -z "$line" ]; then
    ng "$job: 未ロード (launchctl load し直しが必要)"
  else
    exitc=$(echo "$line" | awk '{print $2}')
    if [ "$exitc" = "0" ] || [ "$exitc" = "-" ]; then
      ok "$job: ロード済 / last exit=$exitc"
    else
      ng "$job: last exit=$exitc (前回失敗)"
    fi
  fi
done

launchctl list の2列目が last exit code です。- は「まだ起動していない(正常)」、0 は成功、それ以外は失敗です。

2. hooks スクリプトの実在と実行権限

hooks=(pre_git_guard.sh pre_secrets_check.sh pre_env_guard.sh \
       post_audit_log.sh post_format.sh post_tsc_check.sh \
       stop_notify.sh user_prompt_submit.sh)
for h in "${hooks[@]}"; do
  f="$CLAUDE/hooks/$h"
  if   [ ! -f "$f" ]; then ng "$h: 不在"
  elif [ ! -x "$f" ]; then wn "$h: 実行権限なし (chmod +x 推奨)"
  else ok "$h"
  fi
done

settings.json に hook を書いても、実体スクリプトが chmod +x されていないと silently skip されます。これを WARN で検出します。

3. skill-harvest のログ鮮度

hlog="$CLAUDE/skills/auto/.harvest.log"
a=$(age_h "$hlog")   # (now - mtime) / 3600 を返すヘルパ
if [ "$a" -ge 0 ] && [ "$a" -le 48 ]; then
  ok "最終稼働 ${a}h前: ${last##*] }"
else
  wn "最終稼働 ${a}h前 (>48h: cron停止の疑い): ${last##*] }"
fi
nskills=$(find "$CLAUDE/skills/auto" -maxdepth 2 -name SKILL.md | wc -l | tr -d ' ')
ok "生成済 auto-skill: ${nskills} 個"

ログの mtime が 48h 以上古ければ WARN。「ログがある」と「最近走った」は別の話なので mtime で見ます。

4. 会話ログの生存と INDEX.md の鮮度

cnt=$(find "$CONV" -name '*.md' ! -name 'INDEX.md' | wc -l | tr -d ' ')
ok "会話ログ ${cnt} 件 / 最新 ${la}h前"
ia=$(age_h "$idx")
if [ "$ia" -le 24 ]; then ok "INDEX.md 鮮度 ${ia}h前"
else wn "INDEX.md が ${ia}h前 (extract が更新していない可能性)"; fi

5. Obsidian Vault の自動更新マーカー

if [ -f "$hot" ] && grep -q "recent:start auto-updated" "$hot"; then
  ha=$(age_h "$hot"); ok "hot.md 自動更新マーカー有 / ${ha}h前"
else wn "hot.md の自動更新マーカーが見つからない"; fi
# index.md カバレッジ: 実ファイル数 vs 記載ページ数
real=$(find "$VAULT" -name '*.md' -not -path '*/.*' | wc -l | tr -d ' ')
stated=$(grep -oE '総ページ数:[0-9]+' "$vidx" | grep -oE '[0-9]+' | head -1)
if [ "$stated" = "$real" ]; then ok "index.md カバレッジ一致 (${real}p)"
else wn "index.md 記載 ${stated:-?}p ≠ 実 ${real}p"; fi

6. remember 記憶層の重複バースト検知

for f in now.md recent.md archive.md; do
  [ -f "$REMEMBER/$f" ] && ok "$f 存在" || wn "$f 不在 (必須層)"
done
dup=$(grep -vE '^\s*$|^##|^#' "$nowf" | sort | uniq -c | sort -rn | head -1 | awk '{print $1}')
if [ "${dup:-0}" -ge 3 ]; then
  wn "now.md に同一要約が ${dup} 回 (consolidate 遅延の兆候)"
else
  ok "now.md 重複なし (consolidate 健全)"
fi

now.md は consolidate が遅れると同じ要約が繰り返し追記されます。3回以上の重複を WARN で早期検知します。

7. ディスク使用量と残骸ファイル

cdir_mb=$(( ${cdir_total_mb:-0} - ${disabled_mb:-0} ))
if   [ "${cdir_mb:-0}" -lt 2000 ]; then ok "~/.claude 実効 ${cdir_mb}MB"
elif [ "${cdir_mb:-0}" -lt 5000 ]; then wn "~/.claude 実効 ${cdir_mb}MB (>2GB: 大きめ)"
else ng "~/.claude 実効 ${cdir_mb}MB (>5GB: 異常肥大)"; fi
orphan=$(find "$CLAUDE" -maxdepth 1 -name 'security_warnings_state_*.json' -mtime +7 | wc -l | tr -d ' ')
if [ "${orphan:-0}" -ge 5 ]; then
  wn "security_warnings_state_*.json が ${orphan} 個 (>7日前: backups/ へ退避推奨)"
fi

reversible cache(.disabled-cache)は実効容量から除外して判定します。

8. 週次・月次バッチのログ mtime

declare -a CRON_JOBS=(
  "weekly cleanup-misc:$CLAUDE/logs/cleanup-misc.log:192"
  "weekly env-audit:$CLAUDE/logs/env-audit-latest.md:192"
  "monthly plugin-purge:$CLAUDE/logs/plugin-purge.log:744"
  "weekly plugin-auto-disable:$CLAUDE/logs/plugin-auto-disable.log:192"
  "weekly dotfiles-snapshot:$CLAUDE/logs/dotfiles-snapshot.log:192"
  "weekly agents-index:$CLAUDE/logs/agents-index.log:192"
  "daily plugin-usage:$CLAUDE/scripts/plugin-audit-latest.md:48"
)
for entry in "${CRON_JOBS[@]}"; do
  IFS=":" read -r name path budget <<< "$entry"
  a=$(age_h "$path")
  if [ "$a" -le "$budget" ]; then ok "$name: ${a}h前 (budget ${budget}h)"
  else wn "$name: ${a}h前 (budget ${budget}h 超過 — launchd 配送失敗の可能性)"; fi
done

週次ジョブなら 192h(8日)、月次なら 744h(31日)を budget として、それを超えたら WARN です。

agentmemory の「半生状態」問題

agentmemory サーバのチェックだけ、判定が2段階になっています。

am_line=$(launchctl list 2>/dev/null | awk '$3=="com.shun.agentmemory"{print $1" "$2}')
am_pid=${am_line%% *}; am_exit=${am_line##* }
if [ -z "$am_line" ]; then
  wn "com.shun.agentmemory が launchd に未ロード"
elif [ "$am_pid" = "-" ] && [ "$am_exit" != "0" ]; then
  ng "com.shun.agentmemory 停止中 (last exit=$am_exit) — クラッシュループの可能性"
else
  am_code=$(curl -s -o /dev/null -w '%{http_code}' -m 3 \
            http://localhost:3111/agentmemory/health 2>/dev/null || echo 000)
  if   [ "$am_code" = "200" ]; then ok "稼働中 (pid=$am_pid / health 200)"
  elif [ "$am_code" = "000" ]; then ng "プロセスは居るが port 3111 無応答"
  else ng "port 3111 は開くが /agentmemory/health=$am_code — worker 不在の半生状態"; fi
fi

コメントに理由が書いてあります。

launchd の exit 78 クラッシュループが6日間誰にも気づかれず、さらに「ポートは開くが worker 不在で全API 404」の半生状態は死活監視では見えない。

launchd の状態チェックだけでは「プロセスがいるが実際には動いていない」ケースを検出できません。curl で HTTP 200 を確認する理由はここにあります。

launchd の PID チェックと HTTP ヘルスエンドポイントの両方を見ないと、「起動しているように見えて全 API が 404」という半生状態を見逃します。MCP サーバや API サーバを launchd で動かすなら、必ず /health エンドポイントを叩く一手間を足してください。

cron ↔ launchd 二重実行の検知

見落としがちな項目が9番目の二重実行チェックです。

cron_sh=$(crontab -l 2>/dev/null | grep -vE '^[[:space:]]*#' | \
          grep -oE '/[^ ]+\.sh' | xargs -n1 basename | sort -u)
launchd_sh=$(grep -hoE '/[^<> ]+\.sh' \
             ~/Library/LaunchAgents/com.shun.*.plist | xargs -n1 basename | sort -u)
dup=$(comm -12 <(printf '%s\n' "$cron_sh") <(printf '%s\n' "$launchd_sh") | \
      grep -vE '^[[:space:]]*$')
if [ -n "$dup" ]; then
  ng "cron と launchd の両方に登録され二重実行されるスクリプト: $(printf '%s' "$dup" | tr '\n' ' ')"
fi

コメントには「2026-06-01: cron→launchd 移行で cron を消し忘れて両方に登録=毎サイクル二重実行が発生」とあります。移行後に古い crontab を消し忘れたパターンです。

daily-brief との連携

daily-brief.sh は毎朝 8:00 に launchd から起動し、冒頭の環境ヘルスに1行だけ組み込みます。

echo "## 🏥 環境ヘルス"
run_to 60 ~/.claude/scripts/automation-health.sh 2>&1 | tail -3 | head -1

tail -3 | head -1 は最終サマリ行(✓ ALL GREEN / ⚠ WARN / ✗ FAIL の行)だけを切り出しています。

さらに daily-brief.sh 側では、ライブ疎通プローブで RED を検出したときだけ ## 🚨 要対応 セクションが現れます。

RED_FLAGS=""
flag_red() { RED_FLAGS="${RED_FLAGS}\n- ❌ $1"; }

if [ -n "$RED_FLAGS" ]; then
  echo "## 🚨 要対応(ライブ疎通でRED)"
  printf '%b\n' "$RED_FLAGS"
fi

問題がないときこのセクションは存在しません。**「失敗中だけ現れる節」**として設計されています。brief は ~/Desktop/Daily Brief/today-brief-YYYYMMDD.md に書き出されるので、デスクトップを開いたときに RED が目に入る形になります。

automation-health.sh の exit 1 を使えば、launchd のラッパーで同様のパターンを追加できます。

bash ~/.claude/scripts/automation-health.sh || touch ~/Desktop/AUTOMATION_FAILED.md
bash ~/.claude/scripts/automation-health.sh && rm -f ~/Desktop/AUTOMATION_FAILED.md

失敗中だけファイルが存在し、直ったら消える ―― ダッシュボードを見なくてもデスクトップが状態を教えてくれます。

踏んだ落とし穴

  • launchctl list の last exit code 0- を同じに扱わないと誤 WARN → どちらも正常として OR 条件で弾く
  • hooks スクリプトが chmod +x されていなくても settings.json はエラーを出さない → WARN で検出するまで気づかなかった
  • agentmemory はポートが開くと launchd が「起動中」と見なす → HTTP /health を叩くまで半生状態を検出できない
  • env-audit の出力先を scripts/ から logs/ に移設したとき、ヘルスチェック側のパスも更新が必要(スクリプト内コメント「2026-06-11: 出力先を scripts/→logs/ に移設」より)
  • index.md カバレッジチェックで .backup 等の隠しディレクトリを除外しないと恒久 WARN になる-not -path '*/.*' で隠しディレクトリを除外
  • cron→launchd 移行後に crontab を消し忘れると二重実行comm -12 で両系統のスクリプト名を比較して検出

まとめ

  • 自律化の死活確認は1コマンドで ALL GREEN / WARN / FAIL の3値に集約する
  • exit 1 を機械判定のシグナルとして使い、daily-brief への1行埋め込みや FAILED ファイルの生成に繋げる
  • **「問題中だけ現れる節やファイル」**を設計することで、ダッシュボードを見なくても異常が目に飛び込む
  • launchd の PID チェックだけでは「半生状態」を見逃す。HTTP ヘルスエンドポイントを必ず一緒に叩く
  • バッチの死活は log の mtime を budget 時間と比較するだけで機械判定できる

次回は、このヘルスチェックと autopilot が生成した改善ログを統合して、週次で何が変わったかをまとめる ―― 週次レポート自動生成の仕組みを書く予定です。


Lily@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています

皆さんの ❤️ やシェアが励みになります!

0
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
0
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?