定時ジョブが黙って止まったのを検知して自動修復する見張り番の型
無人ジョブの死活を3層(起動・終了コード・今日の成果物)で判定し、固定した作業コピーでの再実行で自動修復する型。判定不能と非冪等を触らない切り分け、1日1回・3日で打ち切る暴走防止、相互監視、実装を安価モデルへ委譲したときに実行して初めて出る欠陥まで。
約3.3万トークンの節約 (API料金換算で約50円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。
この巻物について
「定時ジョブが黙って止まったのを検知して自動修復する見張り番の型」は、業務自動化カテゴリのAI指示書(MDファイル)です。無人ジョブの死活を3層(起動・終了コード・今日の成果物)で判定し、固定した作業コピーでの再実行で自動修復する型。判定不能と非冪等を触らない切り分け、1日1回・3日で打ち切る暴走防止、相互監視、実装を安価モデルへ委譲したときに実行して初めて出る欠陥まで。この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約3.3万トークン(API料金換算で約50円)・73%のトークンを節約できます。
- カテゴリ
- 業務自動化
- 対応AI
- claude-code、cursor、codex-cli
- ライセンス
- 商用利用可 (再販不可)
- 価格
- 無料
- ゼロから開発時
- 約4.5万トークン
- この巻物使用時
- 約1.2万トークン
- 節約量
- 約3.3万トークン (約50円)
- 更新日
- 2026-09-02
使い方 (AIに渡す3つの方法)
いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。
claude "https://makimono-md.vercel.app/api/v1/files/md-b8aee832/raw を読み込んで、この指示書どおりに実装して"
中身
定時ジョブが黙って止まったのを検知して「自分で直す」見張り番の型
毎朝・毎晩の定時ジョブ(バッチ、同期、レポート生成、スクレイピング)は必ず止まる。 問題は止まること自体ではなく、止まったまま誰も気づかないことと、 気づいた後に毎回人間が同じ手で復旧していることだ。
この指示書は「検知」の一歩先、直せるものは無人で直し、直せないものだけ人に上げる見張り番の 作り方を示す。あわせて、無人の自動修復が暴走してAPI遮断やアカウント凍結を招かないための 打ち切り設計も示す。
前提: 言語は任意(例は Node.js)。スケジューラは Windows タスクスケジューラ / cron / systemd timer。 通知先はチャット webhook(Slack / Discord / Teams いずれでも同型)。
1. 死活は3層で見る。終了コードだけでは判定できない
実際に起きた2種類の事故が、どちらも「終了コードだけ見ていた」ことで見逃された。
- 嘘の成功: ラッパースクリプトが1行も実行せずに
exit 0を返し、タスク履歴は「成功」なのに 成果物が2日ぶん欠落していた。 - 遅れて気づく失敗: 終了コードは正しく1を返していたが、誰もそれを見ていなかった。
so, 3層で見る。
| 層 | 見るもの | 捕まえる失敗 |
|---|---|---|
| 1 | 最終実行時刻が「今日の予定時刻」以降か | 端末停止・スリープ・タスク無効化 |
| 2 | 終了コード | 普通の失敗 |
| 3 | 今日の日付の成果物が最小サイズ以上で存在するか | 嘘の成功 |
3層目が「成功に見える失敗」を捕まえる唯一の層なので、必ず入れる。
判定結果は真偽2値ではなく、次のように分ける。分けないと後段の修復が乱暴になる。
ok 正常
degraded 成果物は出ているが終了コードだけ非0 = 走り切ったうえで中身の異常を申告している
stale 終了コード0なのに成果物が無い = 嘘の成功
failed 終了コード非0で成果物も無い
did-not-run 予定時刻を過ぎても起動していない
disabled タスクが無効化されている
unknown 判定できなかった
2. 「取れなかった」と「無い」を同じ false にしない(最重要)
成果物の確認が API 越しになる場合(外部サービスの実行履歴を問い合わせる等)、
呼び出しが失敗したこととジョブが走らなかったことは、素朴に書くとどちらも false になる。
この取り違えは実際に誤報を出した。APIが一瞬こけただけで「ジョブが動いていません」と通知が飛ぶ。
成果物判定は3値にする。
// true = ある / false = 無い / null = 判定できなかった
async function checkArtifact(job, today) {
if (job.artifact.kind === 'remoteStatus') {
for (let attempt = 0; attempt < 2; attempt++) {
try {
const status = await fetchJobStatus(); // 外部APIの実行履歴
if (!status?.lastRun?.timestamp) return false; // 応答は取れた。中身が無いのは本当に無い
return status.lastRun.timestamp.slice(0, 10) === today && status.lastRun.result === 'success';
} catch {
if (attempt === 0) await new Promise(r => setTimeout(r, 3000)); // 一度だけ間を置く
}
}
return null; // 2回試してダメ = 判定不能。異常と決めつけない
}
// ローカルファイルは stat の失敗=本当に無い、でよい
try {
const st = fs.statSync(expand(job.artifact.path, today));
return dateOf(st.mtime) === today && st.size >= (job.artifact.minBytes || 0);
} catch { return false; }
}
null は unknown に写し、修復の対象外にする。異常と決まっていないものに手を出すほうが危ない。
3. 原因分類は「裸の数字」で断定しない
ログの末尾を読んで原因を分類すると通知が一気に読みやすくなる。ただし雑に書くと誤分類する。
実際にやらかした例: /\b401\b|\b403\b/ で認証エラーを判定していたところ、
ログ末尾に並んでいた**部屋番号「403」**に当たって、単なるクラッシュを「認証エラー」と報告した。
export function classifyCause(logTail) {
if (/Cannot find module|MODULE_NOT_FOUND/.test(logTail)) return 'missing-file';
if (/rate.?limit|too many requests|\b429\b/i.test(logTail)) return 'rate-limit';
// 401/403 は HTTP という文脈が揃ったときだけ認証と見なす
if (/unauthorized|forbidden|invalid[\s_-]{0,3}(?:api[\s_-]{0,3})?key|credential|(?:HTTP|status(?:Code)?)\s*[:=]?\s*40[13]\b/i.test(logTail)) return 'auth';
if (/is not recognized|ENOENT/i.test(logTail)) return 'no-runtime';
return 'unknown';
}
分類は通知の読みやすさのためであって、修復の分岐に使わない。分類を間違えても被害が出ないようにしておく。
4. 自動修復は「1手」に絞る。増やすほど壊れる
修復手段を原因ごとに用意したくなるが、無人で動くものは手数が増えるほど事故る。 経験上、ほとんどの復旧は「きれいな場所で、もう一度走らせる」で足りる。
4.1 「きれいな場所」を用意する(コード置き場を実行環境から切り離す)
最も多い失敗の一つが、ジョブが参照するスクリプトが実行時に存在しないというもの。
共有の作業ディレクトリでジョブを走らせていると、誰か(人でも自動化でも)がブランチを切り替えた
瞬間にファイルが消え、Cannot find module で落ちる。常駐ジョブを作業ツリーのHEADに依存させてはいけない。
対策は、既定ブランチに固定した専用の作業コピーを1つ用意し、そこから走らせること (git なら worktree、それ以外ならデプロイ用の別ディレクトリ)。
function ensurePinnedTree() {
if (!fs.existsSync(PINNED_DIR)) {
run('git', ['-C', REPO, 'worktree', 'add', '--detach', PINNED_DIR, 'origin/main']);
}
run('git', ['-C', PINNED_DIR, 'fetch', '--quiet', 'origin']);
run('git', ['-C', PINNED_DIR, 'reset', '--hard', 'origin/main', '--quiet']);
}
これ1つで「ファイルが消えた」と「一時的なネットワーク断・レート制限」の両方が直る。
4.2 設置してから気づく罠: コードの置き場と成果物の出る場所は別
見張り番を固定ディレクトリから起動すると、成果物を探す先まで固定ディレクトリになる。 成果物が出るのは「ジョブが実際に動いている作業ディレクトリ」のほうなので、 何も見つからず全ジョブが永久に異常判定になる。
探索先は起動時に明示する(--repo <実際の作業ディレクトリ> / 環境変数)。
コードの場所(CODE_ROOT)と監視対象の場所(TARGET_ROOT)を別の変数として持つこと。
4.3 修復の成功判定は「実行が0で終わった」と「成果物が出た」の両方
片方だけで判定すると必ず誤報する。実際に踏んだのは次の2つ。
- 成果物の有無だけ見ていたので、元から成果物があっただけなのに「修復で直った」と報告した (ドライラン中で実際には何も実行していないのに、である)。
- 実行が0で終わったことだけ見ていたので、走ったが何も出さなかったのを成功と数えた。
const outcome = rerun(job); // { ok, message }
const artifactAfter = await checkArtifact(job, today);
if (outcome.ok && artifactAfter) { status = 'ok'; repaired = true; }
else if (outcome.ok && !artifactAfter) message += '(実行は通ったが成果物が出ていない)';
5. 修復してはいけないものを、はっきり決めておく
無人の再実行が許されるのは、もう一度走らせても外に副作用が出ないジョブだけ。 台帳(マニフェスト)に明示し、判定はコードで機械的に落とす。
idempotent: false(決済、外部への価格・在庫送信、メール送信、投稿)→ 絶対に自動再実行しない。通知のみunknown(判定不能)→ 触らないdegraded(成果物は出ていて終了コードだけ非0)→ 再実行しても同じ結論になるので叩かない
3つ目は見落としやすい。「中身の異常を自己申告して非0で終わる」設計のジョブは、 再実行しても同じ申告をするだけで、無駄な負荷と誤解を生む。
6. 打ち切りを必ず入れる(無人の再試行はアカウントを飛ばす)
無人のリトライを野放しにすると、外部APIのレート制限やアカウント凍結を招く。 実例として、個人アカウントでストレージAPIを高頻度に叩き続けてアカウントが凍結された報告がある。 再試行の上限と、諦める条件を先に書く。
// 台帳(例)
{
"id": "daily-report",
"task": "DailyReportTask", // スケジューラ上の名前
"scheduledAt": "07:00",
"artifact": { "kind": "file", "path": "reports/{today}.md", "minBytes": 1000 },
"logPath": { "path": "reports/logs/{today}.log" },
"repair": { "mode": "rerun", "idempotent": true,
"exec": "node", "args": ["tools/daily-report.js", "--quiet"],
"copyBack": ["reports/{today}.md"], "timeoutMs": 900000 },
"maxRepairsPerDay": 1
}
状態は1ファイルに持つ。
{ "lastRunAt": "...",
"jobs": { "daily-report": { "lastRepairDate": "2026-01-02", "repairsToday": 1,
"consecutiveFailDays": 2, "lastFailDate": "2026-01-02",
"escalated": false, "lastStatus": "ok" } } }
- 再実行は 1ジョブ1日1回まで
- 3日連続で異常なら以後は再実行せず通知のみ(
escalated)。正常に戻ったら解除 - ドライランでは状態を書かない(予算を空撃ちで消費してしまう)
落とし穴: 連続失敗は「回数」ではなく「日」で数える
consecutiveFailDays++ を実行のたびに回すと、同じ日に手で2回動かしただけで打ち切りに入る。
「起動できなかった分を後で走らせる」設定(Windows の StartWhenAvailable 等)でも二重に増える。
lastFailDate を持ち、日付が変わったときだけ加算する。
if (finalStatus === 'ok') { s.consecutiveFailDays = 0; s.escalated = false; s.lastFailDate = ''; }
else if (s.lastFailDate !== today) {
s.consecutiveFailDays = (s.consecutiveFailDays || 0) + 1;
s.lastFailDate = today;
if (s.consecutiveFailDays >= 3) s.escalated = true;
}
7. 見張り番自身が死んだら誰も気づかない → 相互監視
見張り番は沈黙で異常を表現してしまう。最小コストの対策は、別の時間に走る既存ジョブに1項目足すこと。
- 見張り番は毎回
lastRunAtを状態ファイルに書く - 早い時間に走るジョブ側で「
lastRunAtが26時間以内か」を1行チェックし、外れていたら異常として報告する
これで片方が死んでももう片方が言う。両方同時に死ぬと沈黙する点は残る(そこは受け入れる)。
8. スケジューラ登録の実務
- ラッパースクリプトを噛ませない。 ランタイムを直接 Execute に指定し、ログはスクリプト自身に書かせる。 ラッパーを1層挟むたびに「成功に見える失敗」が増える。 (Windows の PowerShell 5.1 は BOM 無し UTF-8 の .ps1 を既定コードページとして読むため、 非ASCIIコメントのバイト列が行継続に化けて後続行を飲み込み、何も実行せず exit 0 を返すことがある。 非ASCIIを含む .ps1 は BOM 付きで保存する)
- 端末が寝ていた場合に後から走る設定を入れる(Windows:
StartWhenAvailable、cron:anacron相当) - 実行時刻は監視対象で最も遅いジョブより後に置く
- 終了コードの意味を決めて文書化する:
0全健全 /1異常が残った /2実行不能。 見張り番自身の終了コードが1でも「異常を見つけて報告した」正常動作である点は、運用者に明示しておく
9. テストで固定すべき挙動(回帰しやすい順)
純粋関数(classifyHealth / classifyCause / decideRepair / nextState / 通知本文の整形)を
副作用から切り離して export し、最低これを固定する。
- 終了コード0でも成果物が無ければ
stale - 終了コード非0でも成果物があれば
degraded(かつ再実行しない) - 判定不能(
null)はunknown(かつ再実行しない) - 非冪等ジョブは常に再実行しない
- 1日の再実行上限に達したら再実行しない
- 同じ日に2回失敗しても連続失敗日数は1のまま
- 3日連続で初めて打ち切りになる
- 正常に戻ったら連続失敗日数と打ち切りが解除される
- 原因分類が裸の数字(部屋番号・金額)に反応しない
10. 実装を外部モデルに委譲するときの注意
この型はコード量が中程度(本体400行・テスト100行)なので、安価なモデルへの委譲が効く。 実際にやった手順と、そこで出た欠陥の傾向を書いておく。
- 仕様書をファイルに書き、引数ではなくファイルパスで渡す (引数渡しはシェルがバッククォートをコマンド置換として実行してしまう事故がある)
- 出力は
===== FILE: <パス> =====の区切りで複数ファイルを一度に受け取る - 生成物をそのまま信じない。 1回目のレビューで出た典型的な欠陥:
const変数への再代入- エントリポイント判定を文字列連結で組み立てており、Windows のパス表記で常に false
async関数をawaitせず、Promise を真偽値として扱っている- 日付比較で片方だけ区切り文字を変換しており、永久に一致しない
- コピー元とコピー先に同じパスを渡している
- シェル/PowerShell に渡すコマンド文字列が構文として成立していない(例外は握り潰されて空配列になる)
- レビューでは出ず、実行して初めて出る欠陥がある。 本件では17件中5件がそれだった (ドライランの誤報、API一時失敗の誤報、原因の誤分類、連続失敗の数え方、探索先のズレ)。 必ず実機で1回走らせ、スケジューラ経由でも1回起動して確かめる。
まとめ
- 死活は3層(起動したか / 終了コード / 今日の成果物)で見る
- 「取れなかった」と「無い」を同じ値にしない
- 修復は「固定した作業コピーで再実行」の1手に絞る
- 修復しないもの(非冪等・判定不能・成果物は出ている)をコードで落とす
- 1日1回・3日で打ち切り。連続失敗は日で数える
- 見張り番自身は別ジョブから相互監視する
- 生成コードは実機とスケジューラの両方で走らせるまで信用しない
よくある質問
+「定時ジョブが黙って止まったのを検知して自動修復する見張り番の型」とは何ですか?
無人ジョブの死活を3層(起動・終了コード・今日の成果物)で判定し、固定した作業コピーでの再実行で自動修復する型。判定不能と非冪等を触らない切り分け、1日1回・3日で打ち切る暴走防止、相互監視、実装を安価モデルへ委譲したときに実行して初めて出る欠陥まで。
+どれくらいトークン(費用)を節約できますか?
ゼロから開発すると約4.5万トークンかかりますが、この巻物を使えば約1.2万トークンで済みます。差し引き約3.3万トークン(API料金換算で約50円)・73%の節約です。
+どうやって使いますか?
無料です。MDファイルを Claude Code などのAIに読み込ませるだけ。ワンライナーをターミナルに貼れば実装が始まります。要件定義や技術調査を省いて実装だけにトークンを使えます。
+どのAIツールに対応していますか?
claude-code、cursor、codex-cli に対応しています。
+商用利用できますか?
ライセンスは「商用利用可 (再販不可)」です。
🤝 自分でAIを動かすのは、まだ不安…という方へ
この巻物の内容を、AIを使うプロに丸ごと任せることもできます。姉妹サービスAI代行堂なら「LINEで頼むだけで、仕事が完成」。
関連する巻物
Google Meet 自動参加&動画配信Bot 開発指示書
指定した時刻に Google Meet へ自動参加し、動画を再生しながら画面共有する Bot を、Claude Code に一発で作らせる開発指示 MD。朝会の定例動画配信・ウェビナーの自動放送に。
受信メール添付を案件フォルダへ自動取込するパイプライン
メールを読むアプリとドライブに書くアプリが別、という現実的な構成で顧客メールの添付を案件フォルダへ無人保存する設計。権限追加を避ける理由、実行時間制限下の予算3本立て、二重の重複防止、base64url/行数上限/変換判定などの実装罠、案件と顧客のマッチング、名寄せは候補提示+人の承認にする型まで。
Gmail 自動仕分け&返信ドラフト生成MD
受信メールを AI が分類 (要返信/情報/営業/スパム) してラベル付けし、要返信メールには返信ドラフトまで自動生成する仕組みを作らせる指示書。DWD (ドメイン全体委任) 設定手順込み。
この巻物、誰かのトークンも救えます
𝕏 で節約レシートをシェア