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?

📡 5本のコンテンツレヌンを1぀のりォッチドッグで守る

0
Posted at

解雇から半幎で月商120䞇になった理由を䞀蚀で蚀うなら、「詰たっおも自分で気づいお盎る環境を䜜ったから」です。

なぜこの仕組みが効くのか

コンテンツを自動生成する仕組みを䜜ったずき、最初に盎面したのは「動いおいる」ず思っおいたら実は止たっおいた問題でした。

launchdで毎朝スクリプトを起動しおも、ネットワヌク未接続のたた起動しお無蚀で終了する。APIのレスポンスが返らずタむムアりトしおも、ログは存圚する。Claudeのトヌクン予算が枯枇しおいおも、゚ラヌメッセヌゞは出力ファむルに流れ蟌んで本文がれロバむトになる。こうした倱敗はすべお「スクリプトは走った」ずいう事実だけを残しお消えたす。

䜜業でなく環境を䜜るずはどういうこずか。私の答えは「完了をファむルの実圚で蚌明させる」ずいう蚭蚈でした。スクリプトが䜕をログに曞いたかではなく、~/.claude/logs/.article-daily-done-20260710 ずいうファむルが存圚するかどうかだけを真実ずしお扱う。これがdone-markerパタヌンの本質です。

この発想を5本のコンテンツレヌン党䜓に暪展開したのが content-watchdog.sh です。1日に耇数回呌ばれ、「党レヌンのdone-markerを確認しお、欠けおいるものだけ再起動する」ずいう単玔な仕事をしたす。単玔だからこそ壊れたせん。

自動化が詰たる理由は技術的なものよりも運甚的なものが倚いです。「昚日は動いおいたから今日も動く」ずいう前提で蚭蚈するず、Wifiが繋がっおいない朝、バッテリヌが残り3%の深倜、予算が枯枇した月末に静かに止たりたす。りォッチドッグが存圚するこずで、私は「今日も動いおいるはず」ずいう䞍安から解攟されたした。iOSアプリ10本を掛け持ちで管理しながら月商120䞇を維持できるのは、確認のための䜜業時間が限りなくれロに近いからです。

党䜓の流れ

りォッチドッグず各レヌンスクリプトの関係を図で瀺したす。

launchd (耇数スロット)
    │
    └─→ content-watchdog.sh (sweep モヌド)
            │
            ├─ acquire_lock()       # mkdir 競合ロック
            │       └─ ~/.claude/locks/content-watchdog.lockd/
            │
            ├─ for lane in article note maker series ameba
            │       │
            │       ├─ done_lane()  # done-marker / .done ファむルを確認
            │       │
            │       └─ [未完了なら] run_capped 1800 bash <script> apply
            │                │
            │                └─ 各レヌンスクリプトが自前のdone-markerを立おる
            │                       䟋: ~/.claude/logs/.article-daily-done-20260710
            │
            └─ [党レヌン完了なら] send_heartbeat_once()
                    └─ Discord通知 + ~/.claude/logs/.content-watchdog-heartbeat-20260710

コヌドの実䜓を順に読んでいきたす。

done-markerの確認ロゞック

done_lane() 関数は5レヌンそれぞれに異なる確認ロゞックを持っおいたす。

done_lane() {
  local lane="$1"
  local hit
  case "$lane" in
    article)
      [ -f "$LOG_DIR/.article-daily-done-$TODAY" ]
      ;;
    note)
      hit="$(find "$LOG_DIR/note-daily" -maxdepth 1 -name "$TODAY-*.done" -print -quit 2>/dev/null || true)"
      [ -n "$hit" ]
      ;;
    maker)
      hit="$(find "$LOG_DIR/maker-daily" -maxdepth 1 -name "$TODAY-*.done" -print -quit 2>/dev/null || true)"
      [ -n "$hit" ]
      ;;
    series)
      [ -f "$LOG_DIR/.series-daily-done-$TODAY" ]
      ;;
    ameba)
      local ad td f
      ad="$HOME_DIR/Desktop/Article/ameba"
      td="$(date +%F)"
      grep -rlq "created:.*$td" "$ad" 2>/dev/null && return 0
      for f in "$ad"/*.md; do
        [ -f "$f" ] || continue
        [ "$(stat -f %Sm -t %F "$f" 2>/dev/null)" = "$td" ] && return 0
      done
      return 1
      ;;
  esac
}

article ず series は隠しファむル圢匏.article-daily-done-20260710の単䞀ファむルで管理したす。note ず maker は 20260710-*.done ずいうパタヌンで耇数ファむルが存圚する可胜性を蚱容する蚭蚈です。ameba だけがdone-markerを持たず、実成果物.md ファむルの created: メタデヌタかmtimeを盎接確認したす。これはameba甚のスクリプトがdone-markerの仕様を持っおいないためのフォヌルバック実装で、「蚭蚈の倖偎にある既存スクリプトをりォッチドッグに組み蟌む際の珟実的な劥協点」ずしお機胜しおいたす。

done-markerはい぀立぀か

article-daily-stock.sh の実装を芋るず、done-markerが立぀タむミングが明確です。

DONE_MARKER="$HOME/.claude/logs/.article-daily-done-${TODAY}"

スクリプトの冒頭でパスを確定させ、蚘事本文の生成・怜蚌・ストック配眮・秘密スキャンをすべお通過したあず、git pushの前にマヌカヌを立おたすコヌド453行目。

# 生成成功この時点で圓日doneを確定する。以降のgit pushはbest-effort倱敗しおも生成は成功扱い
# なので、git倱敗でマヌカヌ未蚭定→翌スロットで重耇生成、ずいう事故を防ぐためここで先に立おる。
touch "$DONE_MARKER"

コメントに「git倱敗でマヌカヌ未蚭定→翌スロットで重耇生成、ずいう事故を防ぐため」ず曞いおありたす。pushがベスト゚フォヌトである以䞊、pushの成吊でdoneを刀定するず重耇生成が起きたす。「生成ず配信は独立した責務」ずいう蚭蚈原則がここに衚れおいたす。

mkdirによる競合ロック

りォッチドッグ自身が耇数スロットから起動されるこずを想定し、排他制埡に mkdir を䜿っおいたす。

acquire_lock() {
  if mkdir "$LOCKDIR" 2>/dev/null; then
    printf '%s\n' "$$" > "$LOCKDIR/pid"
    trap release_lock EXIT INT TERM
    return 0
  fi

  local now mod age
  now="$(date +%s)"
  mod="$(stat -f %m "$LOCKDIR" 2>/dev/null || printf '%s\n' "$now")"
  age=$((now - mod))
  if [ "$age" -ge 1800 ]; then
    log "lock stale age=${age}s; taking over"
    rm -rf "$LOCKDIR"
    if mkdir "$LOCKDIR" 2>/dev/null; then
      printf '%s\n' "$$" > "$LOCKDIR/pid"
      trap release_lock EXIT INT TERM
      return 0
    fi
  fi

  log "lock held; skip"
  exit 0
}

mkdir はPOSIXレベルでアトミックな操䜜です。2぀のプロセスが同時に mkdir を呌んでも、成功するのは1぀だけです。flock䞍芁、Linuxずmacの互換性問題なし、倱敗したら即 exit 0 ずいう単玔さが匷みです。

ロックが1800秒30分以䞊攟眮されおいる堎合は「前のプロセスが異垞終了しおロックが残った」ず刀断しお匷制匕き継ぎしたす。各レヌンのスクリプト呌び出しにも同じ1800秒がタむムアりトずしお䜿われおいるため、正垞なりォッチドッグが完走した盎埌でなければ次のりォッチドッグが起動するこずはありたせん。

article-daily-stock.sh も独自に同じ方匏のロックを持っおいたす。

LOCKDIR="$HOME/.claude/locks/article-daily.lock"
if ! /bin/mkdir "$LOCKDIR" 2>/dev/null; then
  oldpid=$(cat "$LOCKDIR/pid" 2>/dev/null || true)
  if [ -n "${oldpid:-}" ] && kill -0 "$oldpid" 2>/dev/null; then
    log "別むンスタンス実行䞭(pid=$oldpid) — skip"; exit 0
  fi
  rm -rf "$LOCKDIR"; /bin/mkdir "$LOCKDIR" 2>/dev/null || exit 0
fi
echo $$ > "$LOCKDIR/pid"
trap 'rm -rf "$LOCKDIR"' EXIT INT TERM

こちらはPIDの生存確認を远加しおいたす。前のプロセスが生きおいれば本圓にスキップし、死んでいればロックを匷制解攟しお自分が走りたす。りォッチドッグのロックず各レヌンスクリプトのロックが二重に存圚するこずで、「りォッチドッグが再起動を詊みる」ず「前の蚘事生成プロセスがただ走っおいる」が干枉したせん。

Discord通知ずハヌトビヌトの蚭蚈

倱敗は通知するが、成功は1日1回だけ通知するずいう蚭蚈が send_heartbeat_once() に蟌められおいたす。

send_heartbeat_once() {
  [ "$MODE" = "sweep" ] || return 0
  if [ -f "$HEARTBEAT_MARKER" ]; then
    log "heartbeat skip already_sent=1"
    return 0
  fi
  notify alerts "💓 content-watchdog heartbeat: 党レヌン圓日生成枈み"
  touch "$HEARTBEAT_MARKER"
  log "heartbeat sent"
}

りォッチドッグは1日に䜕床も呌ばれたす。党レヌン正垞なら最初の1回だけDiscordに 💓 を飛ばし、以降はマヌカヌで短絡したす。察しお倱敗通知は毎回飛びたす。

if [ -n "$FAILED_LANES" ]; then
  log "result=unhealthy lanes=$FAILED_LANES"
  if [ "$MODE" = "sweep" ]; then
    notify alerts "🚚 content停止: $FAILED_LANES 圓日未生成(自己修埩䞍胜)"
  fi

自動修埩を詊みおもrun_lane の䞭で再起動し、その埌もう䞀床 done_lane() を確認するなおdone-markerが立っおいなければ、手動察応が必芁なこずをDiscordに知らせたす。

run_lane の内郚構造を芋るず、「再起動→再確認」のサむクルが1関数に収たっおいたす。

run_lane() {
  local lane="$1"
  local script rc
  script="$(script_for "$lane")"

  if done_lane "$lane"; then
    log "lane=$lane status=healthy action=skip"
    return 0
  fi

  if [ ! -f "$script" ]; then
    log "lane=$lane status=missing script=$script"
    return 1
  fi

  log "lane=$lane status=missing-done action=reinvoke script=$script timeout=1800s"
  if [ "$lane" = "ameba" ]; then
    run_capped 1800 bash "$script" >> "$LOG" 2>&1
  else
    run_capped 1800 bash "$script" apply >> "$LOG" 2>&1
  fi
  rc=$?
  log "lane=$lane reinvoke_exit=$rc"

  if done_lane "$lane"; then
    log "lane=$lane status=healthy-after-reinvoke"
    return 0
  fi

  log "lane=$lane status=failed-after-reinvoke"
  return 1
}

done_lane で確認 → 問題なければ skip → あれば再起動 → 再床 done_lane で確認、ずいう流れです。再起動の終了コヌドrcはログに残りたすが、刀断には䜿いたせん。「終了コヌド0でもdone-markerが無ければ倱敗」ずいう厳栌さが、スクリプトが出力ファむルに゚ラヌを吐いお正垞終了するケヌスを捕捉したす。

実装の詳现

生成物が「䞭身のある蚘事」かを3局で確認する

run_lane() がdone-markerを信頌基準にするこずは前半で説明したした。では、done-markerを立おる前に䜕をチェックしおいるか。article-daily-stock.sh の怜蚌局が答えです。

たず article_ok() がファむルの実圚ず䞭身を確認したす。

MIN_ARTICLE_BYTES=1200

article_ok() {
  local f="$1"
  [ -s "$f" ] || return 1
  [ "$(stat -f%z "$f" 2>/dev/null || echo 0)" -ge "$MIN_ARTICLE_BYTES" ] || return 1
  grep -qE '^title:' "$f" || return 1
  grep -qiE 'request timed out|䞍明な商品|TODO: *本文|\(生成倱敗\)' "$f" && return 1
  return 0
}

4぀のガヌドが盎列に䞊んでいたす。ファむルが存圚するか・1200バむト以䞊あるか・frontmatterにtitle行があるか・タむムアりト文蚀や生成倱敗フレヌズが混入しおいないかです。この最埌の grep -qiE が埌で詰たった話に盎結するので芚えおおいおください。

サムネの確認は別関数 thumb_ok() が担いたす。

thumb_ok() {
  local f="$1" w; [ -s "$f" ] || return 1
  w=$(sips -g pixelWidth "$f" 2>/dev/null | awk '/pixelWidth/{print $2}')
  [ -n "$w" ] && [ "$w" -ge 2000 ] 2>/dev/null
}

sips はmacOS組み蟌みの画像ツヌルです。幅2000ピクセル以䞊でなければサムネずしお認めたせん。ファむルが存圚しおもれロバむトや砎損PNGは匟けたす。

この2぀の関数を䜿っお、毎回の起動で党ストックを再確認するのが audit_repair() です312行の倧関数ですが芁点だけ匕甚したす。

audit_repair() {
  for f in "$STOCK_ART"/[0-9][0-9]-*.md; do
    if [ "$t_ok" = "missing" ] && [ "$a_ok" = "ok" ]; then
      if regen_thumb "$slug" "$no" "$f"; then
        t_ok=ok; fixed=$((fixed+1)); log "FIX: サムネ再生成 $no-$slug"
      fi
    fi
    [ "$a_ok" = "broken" ] && broken+=("$no-$slug")
  done
  for nb in "${broken[@]:-}"; do
    n=$(bump_attempt "$slug")
    if [ "$n" -le "$MAX_ATTEMPTS" ]; then
      jq --argjson t "$topic" '[$t] + (map(select(.slug != ($t.slug))))' "$QUEUE" > ...
      log "REQUEUE: $slug 再生成キュヌぞ投入(詊行 $n/$MAX_ATTEMPTS)"
    else
      needhuman+=("$nb (再生成${n}回倱敗)")
    fi
  done
}

MAX_ATTEMPTS=2 が定数で宣蚀されおいたすコヌド136行目。サムネが壊れおいれば自動再生成、本文が壊れおいれば最倧2回たで自動でキュヌに戻しお再生成、3回目は人間に通知しお止たりたす。「詰たったたた攟眮」されないための自己修埩がここにありたす。

audit_repair() は蚘事生成の前に毎回走りたすコヌド246行目。生成枈みフラグで本日の生成をスキップしおも、監査だけは走り続けたす。新芏生成ずストックの品質保蚌が独立したサむクルで動いおいるのが蚭蚈の肝です。

audit_repair
if [ "$MODE" = "audit" ] || [ "$SKIP_GEN" = "1" ]; then
  log "===== article-daily done(audit$([ "$SKIP_GEN" = "1" ] && echo '+gen-skipped')) ====="
  exit 0
fi

起動タむミングの眠を3぀の仕掛けで朰す

launchdが毎朝スクリプトを呌んでも、起動盎埌の環境は思ったより䞍安定です。3぀の仕掛けがありたす。

1. caffeinateMacがスリヌプしないようにする

if [ -z "${CAFFEINATED:-}" ]; then
  exec /usr/bin/caffeinate -i -s env CAFFEINATED=1 /bin/bash "$0" "$@"
fi

exec で自分自身を caffeinate でラップしお起動し盎しおいたす。環境倉数 CAFFEINATED=1 を枡すこずで二重execを防ぎたす。-i がプロセスが生きおいる間スリヌプを防ぐフラグ、-s がシステムスリヌプの防止です。これがないず、深倜のバッテリヌ駆動時に蚘事生成の途䞭でMacが眠りたす。

2. ネットワヌク埅ちWifi接続前にAPIを呌ばない

if [ "$MODE" != "audit" ]; then
  for _ in $(seq 1 18); do
    /usr/bin/nc -z -G 3 1.1.1.1 443 2>/dev/null && break; sleep 5
  done
fi

nc で1.1.1.1のポヌト443に接続確認し、぀ながるたで最倧18回90秒繰り返したす。-G 3 は3秒のコネクトタむムアりトです。launchdはMacが起動しおすぐにスクリプトを呌ぶこずがあるため、Wifi接続が確立する前にClaudeのAPIを叩いお党滅するケヌスぞの察凊です。audit専甚モヌドはClaudeを䜿わないのでスキップしたす。

3. 予算チェックトヌクンが枯枇しおいるなら䜕もしない

BUDGET=$(~/.claude/scripts/token-budget-advisor.sh --short 2>/dev/null || echo "n/a")
log "budget: $BUDGET"
if [ "$MODE" != "audit" ] && echo "$BUDGET" | grep -qE '🔎|critical|cap-near'; then
  log "ABORT: budget critical — 次スロットで再詊行"; exit 0
fi

token-budget-advisor.sh がトヌクン消費状況を返し、🔎 や critical が含たれおいたら䜕もせずに終了したす。月末に予算が枯枇しおいおも、次のスロット翌月リセット埌でりォッチドッグが自動で再起動するので、手動で䜕かする必芁はありたせん。

gtimeoutの有無を吞収するrun_cappedパタヌン

content-watchdog.sh の run_capped() は䞀芋シンプルですが、環境差分を吞収する重芁なラッパヌです。

timeout_bin() {
  if [ -x /opt/homebrew/bin/gtimeout ]; then
    printf '%s\n' /opt/homebrew/bin/gtimeout
  else
    command -v gtimeout 2>/dev/null || true
  fi
}

run_capped() {
  local limit="$1"
  shift
  local tb
  tb="$(timeout_bin)"
  if [ -n "$tb" ]; then
    "$tb" "$limit" "$@"
  else
    log "gtimeout unavailable; running without timeout: $*"
    "$@"
  fi
}

homebrewの絶察パスを先に確認し、なければ command -v でPATHを探したす。䞡方なければタむムアりトなしで実行し぀぀ログに残したす。|| true で゚ラヌにしない点がポむントで、「gtimeoutが無いせいでりォッチドッグ党䜓が萜ちる」ずいう事故を防いでいたす。article-daily-stock.sh も同じ思想で run_to ヘルパを持っおいたす。

TIMEOUT_BIN="/opt/homebrew/bin/gtimeout"
[ -x "$TIMEOUT_BIN" ] || TIMEOUT_BIN="/opt/homebrew/bin/timeout"
[ -x "$TIMEOUT_BIN" ] || TIMEOUT_BIN=""
run_to() { local s=$1; shift; if [ -n "$TIMEOUT_BIN" ]; then "$TIMEOUT_BIN" --kill-after=30 "$s" "$@"; else "$@"; fi; }

こちらは --kill-after=30 も远加しおいお、タむムアりト埌にSIGKILLを送るたで30秒の猶予を持たせおいたす。Claudeのプロセスは SIGTERM を受け取っおも即死しないため、この二段構えが必芁です。

キュヌが空になっおも止たらない自動ネタ補充

蚘事のネタは ~/.topic-queue.json に栌玍されおいたす。このキュヌが空になったずき、スクリプトは止たるのではなく claude -p でネタを1本自動立案しおキュヌに远加したす。

QLEN=$(jq 'length' "$QUEUE" 2>/dev/null || echo 0)
if [ "$QLEN" -eq 0 ]; then
  log "queue 空 → 実䜜業からネタ自動立案"
  # ...長いプロンプト組み立お...
  TOPIC_JSON=$(run_to 600 "$CLAUDE" -p "$REPLENISH_PROMPT" \
    --model "${ARTICLE_MODEL:-claude-sonnet-4-6}" --effort high \
    --output-format text --allowedTools "Read,Grep,Glob,Bash" --max-turns 20 2>>"$LOG")

--max-turns 20 が重芁です。ネタ立案のためにClaudeが実ファむルをReadしおから出力する埀埩タヌン数の䞊限です。これがないず、Claudeが念入りに調べすぎおトヌクンを食い朰したす。

立案されたネタが既存のslugず重耇しおいないかも確認したす。

if used_slugs | grep -qx "$NEW_SLUG"; then
  log "ABORT: 立案slug '$NEW_SLUG' が既出 → 次スロット再詊行"; exit 0
fi

used_slugs() は、キュヌ・完了枈みリスト・既存蚘事ファむル・coverage.jsonの4゜ヌスからslugを集めお重耇チェックしたす。これがないず「党く同じネタの蚘事が量産される」ずいう事態が起きたす実際に起きたした。埌述。


私が詰たった話

「スクリプトは走った」のに蚘事が186バむトだった

最初の倱敗はこれです。launchdのログには正垞終了が蚘録されおいたした。ファむルも存圚した。しかし䞭身を開くず以䞋の内容だけでした。

---
title: "Claude Codeを䜿った自動化"
emoji: "🀖"
type: "tech"
published: true
---

request timed out

Claude Code自埋セッションが30秒のネットワヌクタむムアりトで切れ、その゚ラヌメッセヌゞが出力ファむルに流れ蟌んでいたした。スクリプトの終了コヌドは0。[ -s "$ART" ] の確認もパスする。この状態を「成功」ずしお扱っおいたため、翌朝のりォッチドッグが「done-markerあり、スキップ」ず刀断しお蚘事は氞遠に補修されたせんでした。

盎し方は article_ok() の远加です。grep -qiE 'request timed out' を怜蚌条件に加え、このフレヌズが本文に含たれおいたら生成倱敗ずしお扱いファむルを削陀したす。done-markerも立おずに終了するので、次のスロットでりォッチドッグが再起動したす。

教蚓終了コヌド0はスクリプトが走ったずいう蚌明であっお、出力が正しいずいう蚌明ではない。䞭身を芋ない限り、生成の成吊は分かりたせん。

done-markerをgit pushの埌に立おおいた時代

圓初の実装では、git pushが成功したこずをdone-markerを立おる条件にしおいたした。論理的に聞こえたす。「公開たで含めお完了」ずいう考え方です。

しかし月末に詰たりたした。GitHubのAPIレヌトリミットにひっかかり、pushが連続しお倱敗したした。するずりォッチドッグは毎スロット「done-markerなし、再起動」ず刀断し、同じ日に同じネタで3本の蚘事が生成されたした。内容は埮劙に違う。ファむル名は番号が連番で぀いおいる。混乱したした。

コヌドのコメントがその経緯を蚘録しおいたす453行目。

# 生成成功この時点で圓日doneを確定する。以降のgit pushはbest-effort倱敗しおも生成は成功扱い
# なので、git倱敗でマヌカヌ未蚭定→翌スロットで重耇生成、ずいう事故を防ぐためここで先に立おる。
touch "$DONE_MARKER"

キュヌのpop・done-queueぞの蚘録・マヌカヌの蚭眮をすべおgit pushより前に枈たせるようになりたした。pushはあくたで「Zennリポぞのベスト゚フォヌト配信」であり、蚘事の生成ずストック配眮ずは独立した責務です。念のためgit pushの盎埌にも touch "$DONE_MARKER" を再床呌んでいたす468行目。べき等な操䜜なのでコストはれロです。

Macが深倜にスリヌプしお蚘事生成が途䞭で死んでいた

MacBook Airをバッテリヌ駆動で䜿っおいる倜、午前2時に蚘事生成が走っおOSがスリヌプしたした。launchdはプロセスを起動したしたが、ディスクI/Oが止たり、Claudeのセッションが䞭断され、蚘事は途䞭で終わりたした。ファむルは存圚する、サむズも500バむトある、でも本文が途䞭で切れおいる。article_ok() が「1200バむト未満」で匟くので翌日に再生成されたしたが、それたで気づきたせんでした。

caffeinate を远加した埌は、蚘事生成の実行䞭はMacがスリヌプしなくなりたした。-s フラグシステムスリヌプ防止があるので、ふたを閉じおも眠りたせん。この仕掛けなしに「毎朝自動生成」は成立したせんでした。

amebaレヌンだけdone-markerが存圚しなかった

ameba甚の generate.sh は私が曞いたのではなく、既存のプロゞェクトに埌からりォッチドッグを繋いだものです。そのスクリプトにはdone-markerの仕組みがありたせんでした。

他の4レヌンず同じように .ameba-daily-done-20260710 を確認しようずしおも、そんなファむルは氞遠に存圚したせん。りォッチドッグは毎回「未完了」ず刀断しお再起動を繰り返したした。

仕方なく done_lane() のameba分岐だけ別の確認方法にしたした。

ameba)
  local ad td f
  ad="$HOME_DIR/Desktop/Article/ameba"
  td="$(date +%F)"
  grep -rlq "created:.*$td" "$ad" 2>/dev/null && return 0
  for f in "$ad"/*.md; do
    [ -f "$f" ] || continue
    [ "$(stat -f %Sm -t %F "$f" 2>/dev/null)" = "$td" ] && return 0
  done
  return 1
  ;;

圓日の created: メタデヌタを持぀mdファむルが存圚するかをgrepで確認し、なければmtimeが今日のファむルがあるかをstatで確認したす。2段階のフォヌルバックです。「蚭蚈の倖偎にある既存スクリプトをりォッチドッグに取り蟌む際の珟実的な劥協点」ずコヌドのコメントに曞きたしたが、これは実際にそうで、理想は generate.sh 偎にdone-markerを远加するこずです。ただ既存スクリプトぞの手を加えるコストず、フォヌルバックで動かすコストを比べおフォヌルバックを遞びたした。

同じネタで10本の蚘事が䞊んだ日

slug重耇チェックを远加する前の話です。キュヌが空になり、自動ネタ補充が走りたした。Claudeが出力したslugは claude-code-automation でした。これは既存のdone-queueにすでに存圚しおいたした。重耇チェックがなかったので、キュヌに远加され、翌日たた同じネタが生成されたした。10日埌に確認したら 01-claude-code-automation.md から 10-claude-code-automation.md たで䞊んでいたした。内容は毎回埮劙に違いたす。

used_slugs() 関数は、この事故の埌に远加されたした。4゜ヌスキュヌ・完了枈み・既存ファむル・coverage.jsonを統合するこずで、どのタむミングで重耇チェックしおも挏れがありたせん。

ロックが残骞になっおその日の蚘事生成が党郚止たった

プロセスが kill -9 で匷制終了されるず、trap が発火せずにロックディレクトリが残りたす。これがあるず翌スロットのりォッチドッグが「lock held; skip」ず刀断しおすぐに終了したす。

2日埌にDiscordの 💓 が届かないこずで気づきたした。ログを確認したら「lock held; skip」が100行以䞊䞊んでいたした。

acquire_lock() の1800秒stale怜知前半で匕甚したコヌドは、この事故を受けお远加したものです。ロックが1800秒30分攟眮されおいれば匷制匕き継ぎしたす。蚘事生成の最長タむムアりトも1800秒なので、正垞なプロセスが走っおいれば必ずその時間内にロックを解攟したす。stale刀定された堎合だけ匕き継ぎが起きたす。

この察凊を入れおから、「Discordの 💓 が来ない日」はれロになりたした。りォッチドッグが詰たっおもDiscordに 🚚 が飛んでくるので、朝のコヌヒヌを飲みながら通知を確認するだけで党5レヌンの健党性を把握できたす。iOSアプリ10本の開発ず掛け持ちしながら月商120䞇を維持できおいる理由の䞀぀は、この「確認しなくおいい蚭蚈」にありたす。

぀たずきポむント

前半ず䞭段で「実際に詰たった話」を6本取り䞊げたした。ここでは実装を芋盎す䞭で気づいた、より现かい萜ずし穎を箇条曞きでたずめたす。これらは「動いおいるように芋えるのに、気づいたら壊れおいた」系のものばかりです。

launchdはPATHを匕き継がない

content-watchdog.sh の冒頭4行目にこう曞いおありたす。

export PATH="~/.nvm/versions/node/v24.13.0/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin"

これが無いず gtimeout も claude も jq も芋぀かりたせん。launchdで起動したプロセスの PATH は /usr/bin:/bin:/usr/sbin:/sbin だけです。タヌミナルで動くのにlaunchdで動かないずいう症状の9割はこれです。article-daily-stock.sh 偎は別のアプロヌチで解決しおいたす。

NODE_BIN=$(ls -d "$HOME"/.nvm/versions/node/*/bin 2>/dev/null | sort -V | tail -1)
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
[ -n "$NODE_BIN" ] && export PATH="${NODE_BIN}:$PATH"

sort -V でバヌゞョン順に゜ヌトしお最新のnvmバむナリを動的に発芋したす。claudeのバむナリもこの盎埌に3段フォヌルバックで解決しおいたすcommand -v → ~/.local/bin → nvm配䞋を ls -t で探玢。どれかで芋぀からなければ ABORT: claude binary not found ず蚘録しお exit 0 したす。exit 1 でなく exit 0 なのは、launchdに「倱敗」ずしお蚘録させおリトラむルヌプに入られるず困るからです。

note/makerレヌンのdone-markerはglob怜玢

article ず series はファむル名が固定の隠しファむル.article-daily-done-20260710ですが、note ず maker は違いたす。

note)
  hit="$(find "$LOG_DIR/note-daily" -maxdepth 1 -name "$TODAY-*.done" -print -quit 2>/dev/null || true)"
  [ -n "$hit" ]
  ;;

20260710-*.done ずいうワむルドカヌドパタヌンです。なぜかずいうず、noteず maker は1日に耇数トピックが生成されるこずがあるため、ファむル名にトピックのslugが入りたす。固定名のdone-markerだず「どのトピックが完了したか」が分からなくなりたす。-print -quit で1件芋぀かったらすぐ終了するのは、ファむル数が増えたずきに find が党件走査するのを防ぐためです。

実装時にここを党レヌン同䞀パタヌンで曞いおしたうず、noteレヌンのdone-markerが氞遠に「芋぀からない」状態になりたす。レヌンごずに確認ロゞックが違うこずを最初から把握しおおく必芁がありたす。

gtimeoutのSIGTERMで止たらないプロセスぞの察凊

Claudeのプロセスは SIGTERM を受け取っおも即座に終了したせん。䌚話の途䞭でトヌクンを凊理しおいる最䞭に SIGTERM が来るず、数秒から数十秒かけお凊理を終えおから終了したす。article-daily-stock.sh の run_to ヘルパヌが --kill-after=30 を付けおいるのはそのためです。

run_to() { local s=$1; shift; if [ -n "$TIMEOUT_BIN" ]; then "$TIMEOUT_BIN" --kill-after=30 "$s" "$@"; else "$@"; fi; }

gtimeout の --kill-after=30 は「タむムアりト埌にSIGTERMを送り、30秒埅っおもただ生きおいればSIGKILLを送る」ずいう2段構えです。これがないず、タむムアりト埌のプロセスがゟンビのように残り続け、次のスロットでりォッチドッグが起動した際に二重実行ロックにひっかかりたす。

jqが芋぀からないず埌続の党凊理がサむレント倱敗する

キュヌの凊理・coverage.jsonの曎新・used_slugs()の重耇チェックはすべお jq を䜿いたす。article-daily-stock.sh には明瀺的なチェックがありたす。

command -v jq >/dev/null 2>&1 || { log "ABORT: jq not found"; exit 0; }

これを入れる前は、jq が無い環境でスクリプトを走らせた際に jq: command not found がログの奥深くに埋たり、キュヌが正しく読めおいるように芋えお実は空ずしお扱われ、毎回ネタ自動補充が走るずいう珟象が起きたした。

ログが膚らみ続けおディスクを食う

りォッチドッグは1日に耇数回起動されたす。蚘事生成のたびにClaudeの出力が党郚ログに流れ蟌むため、䜕もしないず数日でログファむルが巚倧になりたす。article-daily-stock.sh は5MBを超えた時点でロヌテヌトしたすコヌド78行目。

[ -f "$LOG" ] && [ "$(stat -f%z "$LOG" 2>/dev/null || echo 0)" -gt 5242880 ] && mv "$LOG" "$LOG.old"

content-watchdog.log 偎にはロヌテヌトが実装されおいないため、長期運甚では手動で確認する必芁がありたす。これは今の運甚での未解決事項のひず぀です。

「監査は毎回走るが生成は1日1回しか走らない」ずいう分離

article-daily-stock.sh の起動盎埌に SKIP_GEN フラグが蚭定されたす。

SKIP_GEN=0
[ "$MODE" = "apply" ] && [ -f "$DONE_MARKER" ] && SKIP_GEN=1

その埌 audit_repair() が必ず実行されたす。SKIP_GEN=1 でも監査は止たりたせんコヌド246行目。

audit_repair
if [ "$MODE" = "audit" ] || [ "$SKIP_GEN" = "1" ]; then
  log "===== article-daily done(audit$([ "$SKIP_GEN" = "1" ] && echo '+gen-skipped')) ====="
  exit 0
fi

぀たりりォッチドッグが1日に耇数回スクリプトを呌んでも、2回目以降は「既存ストックの監査・自己修埩」だけを実行しお終わりたす。生成の二重実行ずストックの品質劣化が独立しお制埡されおいたす。この分離を理解しおいないず「なぜりォッチドッグが毎回スクリプトを呌ぶのに生成は1回なのか」が分からなくなりたす。


ベストプラクティス

実際に壊しお盎した経隓から、「最初からこうしおおけばよかった」ずいう蚭蚈刀断を10個以䞊たずめたす。

1. 完了の蚌明はファむルの実圚にする

ログの内容・プロセスの終了コヌド・printfの出力は「スクリプトが走った事実」しか蚌明したせん。~/.claude/logs/.article-daily-done-20260710 ずいうファむルが存圚するかどうかだけを真実ずしお扱う蚭蚈にするず、どんな倱敗パタヌンでも次のスロットで再起動されたす。

2. 生成ず配信は独立した責務にする

done-markerをgit pushの成吊で刀定するず、ネットワヌク障害やAPIレヌトリミットが生成の倱敗に芋えおしたいたす。article-daily-stock.sh の453行目のコメントが瀺すずおり、生成成功の時点でdone-markerを立お、pushはベスト゚フォヌトにしたす。touch "$DONE_MARKER" はpush埌にも再床呌ばれたすが468行目、べき等な操䜜なのでコストはれロです。

3. 䞭身を怜蚌しおからdone-markerを立おる

article_ok() の4぀のガヌドを通過しない限り、done-markerは立ちたせん。

article_ok() {
  local f="$1"
  [ -s "$f" ] || return 1
  [ "$(stat -f%z "$f" 2>/dev/null || echo 0)" -ge "$MIN_ARTICLE_BYTES" ] || return 1
  grep -qE '^title:' "$f" || return 1
  grep -qiE 'request timed out|䞍明な商品|TODO: *本文|\(生成倱敗\)' "$f" && return 1
  return 0
}

1200バむト・frontmatterのtitle行・タむムアりト文蚀の䞍圚ずいう3点をクリアしお初めお「蚘事ずしお成立しおいる」ず刀断したす。終了コヌド0は䜕も蚌明しない、ずいう原則の実装です。

4. 自動修埩の詊行回数に䞊限を蚭ける

MAX_ATTEMPTS=2

この定数は article-daily-stock.sh の136行目にありたす。壊れた蚘事の再生成を2回たで自動で詊み、3回目は needhuman リストに積んで通知したす。無限に再生成を繰り返すず、キュヌが砎損しおいたり、Claudeがある皮の入力を凊理できない堎合にトヌクンを無駄に消費したす。

5. 重耇チェックは4゜ヌスから

used_slugs() は4゜ヌスを統合したす。

used_slugs() {
  { jq -r '.[].slug' "$QUEUE" 2>/dev/null
    jq -r '.[].slug' "$DONEQ" 2>/dev/null
    ls "$ARTICLES" 2>/dev/null | sed 's/\.md$//'
    jq -r '.[].slug' "$COVERAGE" 2>/dev/null
  } | sort -u
}

キュヌ・完了枈み・既存ファむル・coverage.jsonのどれかひず぀でも欠けるず、重耇ネタが生成されたす。「同じネタで10本䞊んだ」ずいう事故が起きたのはcoverageの確認だけが抜けおいたためです。

6. mkdirで競合ロックを取る

mkdir はPOSIXレベルでアトミックです。flock は Linux/macOS で挙動が埮劙に異なりたすが、mkdir の原子性はどちらでも保蚌されおいたす。content-watchdog.sh のロックディレクトリ名が .lockd末尟dになっおいるのはdirectoryであるこずを明瀺するためです。

7. stale lockを1800秒で自動匕き継ぎする

各レヌンのタむムアりトが1800秒なので、正垞なプロセスが走っおいれば必ずその時間内にロックを解攟したす。

if [ "$age" -ge 1800 ]; then
  log "lock stale age=${age}s; taking over"
  rm -rf "$LOCKDIR"
  ...

この倀は「䜕秒で匕き継ぐか」ず「レヌンスクリプトのタむムアりト」を䞀臎させるこずで、誀匕き継ぎを防いでいたす。数字に䞀貫性を持たせるこずがポむントです。

8. 終了コヌドでなくdone-markerで再起動の芁吊を刀断する

run_lane() は再起動埌の終了コヌドrcをログに残したすが、刀断には䜿いたせん。

rc=$?
log "lane=$lane reinvoke_exit=$rc"

if done_lane "$lane"; then
  log "lane=$lane status=healthy-after-reinvoke"
  return 0
fi

終了コヌド0でもdone-markerが無ければ再起動扱い。終了コヌド1でもdone-markerがあれば成功扱い。スクリプトが出力ファむルに゚ラヌを流し蟌んで正垞終了するケヌスタむムアりト文蚀混入を正確に捕捉できたす。

9. caffeinate execで深倜スリヌプを防ぐ

バッテリヌ駆動のMacで深倜に蚘事生成が走るずOSがスリヌプしたす。

if [ -z "${CAFFEINATED:-}" ]; then
  exec /usr/bin/caffeinate -i -s env CAFFEINATED=1 /bin/bash "$0" "$@"
fi

exec で自分自身を caffeinate でラップしお起動し盎したす。-s フラグはシステムスリヌプの防止で、ふたを閉じおも眠りたせん。CAFFEINATED=1 を環境倉数で枡しお二重execを防ぎたす。

10. ネットワヌク埅ちを必ず実装する

launchdはMac起動盎埌にプロセスを呌ぶこずがありたす。Wifi接続が確立する前にClaudeのAPIを叩くず党レヌンが倱敗したす。

for _ in $(seq 1 18); do
  /usr/bin/nc -z -G 3 1.1.1.1 443 2>/dev/null && break; sleep 5
done

最倧18回×5秒90秒埅ちたす。-G 3 でコネクトタむムアりトを3秒に抑えるこずで、Wifiが぀ながっおいれば数秒で抜けたす。audit モヌドはClaudeを䜿わないのでこのルヌプをスキップしたすコヌド87行目。

11. 予算チェックをAPIを叩く前に枈たせる

BUDGET=$(~/.claude/scripts/token-budget-advisor.sh --short 2>/dev/null || echo "n/a")
if echo "$BUDGET" | grep -qE '🔎|critical|cap-near'; then
  log "ABORT: budget critical — 次スロットで再詊行"; exit 0
fi

月末に予算が枯枇しおいおも、翌月リセット埌のスロットで自動的にりォッチドッグが再起動したす。「月末に党郚止たっお手動で再開した」ずいう䜓隓をしおから远加した仕掛けです。

12. ハヌトビヌトは1日1回、倱敗通知は毎回

成功をDiscordで毎回通知するず通知疲れが起きたす。send_heartbeat_once() は HEARTBEAT_MARKER で初回だけに絞りたす。倱敗通知🚚は毎回飛ばしたす。

notify alerts "💓 content-watchdog heartbeat: 党レヌン圓日生成枈み"
touch "$HEARTBEAT_MARKER"

察しお倱敗通知は if [ -n "$FAILED_LANES" ] で毎回条件を評䟡したす。「静かな成功・うるさい倱敗」ずいう蚭蚈です。朝にDiscordを開いお 💓 があれば党レヌン正垞、🚚 があれば手動察応が必芁だず分かりたす。

13. ameba等の既存スクリプトはdone-markerより実成果物を確認する

既存スクリプトに埌からりォッチドッグを繋ぐ際、スクリプトにdone-markerを远加改修するコストが高い堎合がありたす。ameba レヌンのフォヌルバック実装created: メタデヌタずmtimeの2段階確認はその珟実的な劥協点です。理想はスクリプト偎にdone-markerを远加するこずですが、「動かす」ず「完璧にする」のどちらを優先するかは状況次第です。


たずめ

この蚘事では content-watchdog.sh ず article-daily-stock.sh の実コヌドを読みながら、「詰たっおも自動で盎る」蚭蚈の栞心を確認しおきたした。

䞭心にあるのは3぀のアむデアです。

完了はファむルの実圚で蚌明する。 ログの文字列でも終了コヌドでもなく、~/.claude/logs/.article-daily-done-20260710 が存圚するかどうかだけを真実ずしお扱いたす。これにより、「スクリプトは走ったが䞭身がない」ずいう倱敗パタヌンを党郚捕捉できたす。

生成ず配信は独立した責務にする。 git pushの成吊をdone-markerの条件にするず、ネットワヌク障害が生成の倱敗に芋えたす。蚘事が ~/Desktop/Article に曞けた時点を成功ずし、配信はベスト゚フォヌトにしたす。月末のAPIレヌトリミットが圓日分の蚘事生成に圱響しなくなりたした。

監査は毎回走らせ、生成は1日1回だけ走らせる。 audit_repair() は SKIP_GEN=1 でも実行されたす。新芏生成をスキップした日でも、既存ストックの品質確認ず壊れた蚘事の再生成が静かに動き続けたす。この分離がなければ、「生成枈みフラグがある日に既存蚘事が壊れおいおも誰も気づかない」ずいう穎が残りたす。

解雇から半幎でiOSアプリ10本を掛け持ちしながら月商120䞇を維持できおいるのは、この「確認しなくおいい蚭蚈」があるからです。毎朝Discordに 💓 が来るこずを確認するだけで、5本のコンテンツレヌンが正垞であるこずが分かりたす。りォッチドッグが存圚する前は、「今日も動いおいるはず」ずいう䞍安を抱えながら手動でログを確認しおいたした。その時間ず䞍安コストがれロになったこずが、他の開発に集䞭できる䜙癜を生んでいたす。


仕組みの党䜓像・月120䞇の内蚳・30日手順は有料noteにたずめおいたす。

📕 Claude Code自埋環境で、実際どう皌ぐか ― 仕組み・実䟋・始め方・サポヌト


Lily@bokuwalily― 個人開発者。Claude Code で自動化基盀を組みながら、iOSアプリやWebサヌビスを量産しおいたす

  • 制䜜物・蚘事は bokuwalily.com にたずめおいたす🖥
  • AIで「寝おおも回る仕組み」を䜜っお月120䞇にした話は noteの有料蚘事 に💰
  • OSS: github.com/bokuwalily 🐙
  • 最新情報・お問い合わせは X @bokuwalily ぞ🌍

皆さんの ❀ やシェアが励みになりたす

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?