スケジュール実行のダッシュボードを開くと、気持ちのいい緑色が縦に並んでいます。全部「成功」。実行時間も安定しています。ところが先週、私はその緑の列に、もう三週間ほど実質的には何も生み出していない自動化が一本まぎれていることに気づきました。
そのタスクは毎日きちんと動き、きちんと終了コード0を返し、きちんと「完了しました」というログを残していました。ただ、処理対象の件数が、いつからか0件になっていたのです。対象が枯れても、空の集合をまじめに処理して、まじめに成功していた。見かけの成功率は100%のまま、価値だけが静かに抜け落ちていました。
エージェントに実作業を委ねる時代の運用では、この「緑のまま痩せていく自動化」が一番やっかいな相手だと感じています。落ちてくれれば気づけます。けれど、成功し続ける失敗には、こちらから見に行かないと気づけません。
「成功」というログが、いちばん静かな嘘をつく
私たちが監視の起点に置いているのは、たいてい「落ちたかどうか」です。終了コード、例外、タイムアウト。これらは自動化が壊れた瞬間を教えてくれます。
けれど自動化の寿命は、壊れる前に尽きることがあります。前提が変わる。対象が枯れる。上流のフォーマットが差し替わって、パースは通るけれど中身が空になる。どれもコードは無傷で、テストも緑のまま、しかし成果はゼロに近づいていきます。
私はこの状態を「成果の痩せ(value decay)」と呼んで区別するようにしました。壊れた自動化は修理の対象ですが、痩せた自動化は判断の対象です。直す価値があるのか、役目を終えたのか。その判断を、緑色のログだけからは下せません。
成果の痩せは、なぜ観測しにくいのか
痩せが見えにくいのには、構造的な理由が三つあります。
第一に、成功と価値が同じシグナルに畳み込まれていることです。多くのタスクは「終了コード0=成功=OK」で運用されます。ここには「何件処理したか」「その処理に意味があったか」という情報が入っていません。
第二に、痩せは連続的に進むことです。昨日は12件、今日は9件、来週は2件、と滑らかに減っていくと、どこにも「異常」の瞬間がありません。閾値ベースのアラートは、この手のなだらかな死を捉えられません。
第三に、成功が心理的な安心を与えることです。緑のログは「見なくていい」という合図として働きます。個人開発では監視に割ける時間が限られているぶん、この安心はそのまま盲点になります。私自身、AdMob 関連の集計タスクを一本、二週間気づかずに空回りさせていました。
自動化に「価値の申告」を義務づける
対策の出発点は、成功と価値を別々のシグナルとして記録することです。各自動化に、終了時、自分がどれだけの価値を生んだかを一行で申告させます。私はこれを outcome ledger(成果台帳)と呼んでいます。
申告に含める項目は、次の五つに絞りました。
taskId — どの自動化か
ranAt — いつ走ったか(ISO 8601・UTC)
status — 従来どおりの成否(ok / fail)
valueMetric — その回に生んだ価値の量(処理件数・変更行数・生成物の数など、タスク固有の一次量)
evidence — 価値の根拠になる短い証跡(出力先パス、要約文字列など)
肝は valueMetric を「成功か否か」から独立させることです。status が ok でも valueMetric が 0 なら、それは「成功した空振り」として記録に残ります。
TypeScript なら、各タスクの末尾で呼ぶ小さなヘルパーにできます。追記専用の JSONL に一行足すだけです。
import { appendFile } from "node:fs/promises" ;
type Outcome = {
taskId : string ;
ranAt : string ; // ISO 8601 (UTC)
status : "ok" | "fail" ;
valueMetric : number ; // その回の一次的な成果量
evidence : string ; // 価値の根拠(出力先や要約)
notes ?: string ;
};
export async function recordOutcome ( o : Omit < Outcome , "ranAt" >) : Promise < void > {
const line = JSON . stringify ({ ... o, ranAt: new Date (). toISOString () });
// 追記専用。評価器はこの台帳だけを読む。
await appendFile (process.env. OUTCOME_LEDGER ?? "./ledger.jsonl" , line + " \n " );
}
// 呼び出し例(スクリーンショット差分を検出するタスクの末尾)
const changed = await diffScreenshots ();
await recordOutcome ({
taskId: "visual-regression" ,
status: "ok" ,
valueMetric: changed. length , // 検出した差分の数がこのタスクの価値
evidence: changed. length ? changed. slice ( 0 , 3 ). join ( ", " ) : "no diff" ,
});
台帳は、数日ぶんが溜まると次のような姿になります。status はずっと ok のまま、valueMetric だけが痩せていく様子が、はじめて一目で追えるようになります。
{ "taskId" : "asset-audit" , "ranAt" : "2026-06-30T01:00:00Z" , "status" : "ok" , "valueMetric" : 11 , "evidence" : "11 assets flagged" }
{ "taskId" : "asset-audit" , "ranAt" : "2026-07-01T01:00:00Z" , "status" : "ok" , "valueMetric" : 4 , "evidence" : "4 assets flagged" }
{ "taskId" : "asset-audit" , "ranAt" : "2026-07-02T01:00:00Z" , "status" : "ok" , "valueMetric" : 1 , "evidence" : "1 asset flagged" }
{ "taskId" : "asset-audit" , "ranAt" : "2026-07-03T01:00:00Z" , "status" : "ok" , "valueMetric" : 0 , "evidence" : "nothing flagged" }
{ "taskId" : "asset-audit" , "ranAt" : "2026-07-04T01:00:00Z" , "status" : "ok" , "valueMetric" : 0 , "evidence" : "nothing flagged" }
{ "taskId" : "asset-audit" , "ranAt" : "2026-07-05T01:00:00Z" , "status" : "ok" , "valueMetric" : 0 , "evidence" : "nothing flagged" }
痩せを検出する評価器
台帳が貯まれば、痩せの検出は難しくありません。タスクごとに直近N回の valueMetric を眺め、「成功はしているのに、価値が連続してゼロ、あるいは細り続けている」ものを退役候補として抜き出します。
閾値で殴るのではなく、二つの観点で見ます。ひとつは連続ゼロ回数。もうひとつは、前半と後半の平均を比べた痩せ率です。滑らかに死んでいく自動化を捉えるには、後者が効きます。
import json, sys
from collections import defaultdict
WINDOW = 9 # 直近何回を見るか
ZERO_STREAK = 5 # 連続ゼロがこの回数で退役候補
DECAY_RATIO = 0.4 # 後半平均 / 前半平均 がこの値未満なら「痩せ」
def load (path):
runs = defaultdict( list )
with open (path) as f:
for line in f:
line = line.strip()
if not line:
continue
r = json.loads(line)
runs[r[ "taskId" ]].append(r)
return runs
def classify (recs):
recent = recs[ - WINDOW :]
values = [r[ "valueMetric" ] for r in recent]
ok_rate = sum ( 1 for r in recent if r[ "status" ] == "ok" ) / len (recent)
trailing_zero = 0
for v in reversed (values):
if v == 0 :
trailing_zero += 1
else :
break
half = max ( 1 , len (values) // 2 )
first = sum (values[:half]) / half
last = sum (values[half:]) / max ( 1 , len (values) - half)
decay = (last / first) if first > 0 else ( 0.0 if last == 0 else 1.0 )
if ok_rate < 1.0 and trailing_zero < ZERO_STREAK :
return "repair" # 落ちている→修理
if trailing_zero >= ZERO_STREAK :
return "retire?" # 緑のまま連続ゼロ→退役を検討
if first > 0 and decay < DECAY_RATIO :
return "decaying" # なだらかに痩せている→観察強化
return "healthy"
def main (path):
for task_id, recs in load(path).items():
if len (recs) < 3 :
continue
verdict = classify(recs)
if verdict != "healthy" :
tail = [r[ "valueMetric" ] for r in recs[ - WINDOW :]]
print ( f "[ { verdict :9 } ] { task_id :22 } values= { tail } " )
if __name__ == "__main__" :
main(sys.argv[ 1 ] if len (sys.argv) > 1 else "ledger.jsonl" )
先ほどの asset-audit を通すと、[retire? ] asset-audit values=[11, 4, 1, 0, 0, 0] のように、連続ゼロが手前に寄った形で浮かび上がります。落ちてはいないので既存の監視は沈黙したまま。それでも、この評価器なら拾えます。
ここで一つ、運用上の落とし穴があります。valueMetric が本来「たまに0でも正常」なタスク(例えば異常検知のように、何も見つからない日が健全なもの)を、そのまま同じ閾値にかけると誤検知します。私はタスク定義側に zeroIsHealthy: true の印を持たせ、そういうタスクは連続ゼロ判定から除外する運用にしました。回避策としては地味ですが、これを入れておかないと評価器そのものが信頼を失います。
畳むか、直すか、残すか
退役候補が挙がっても、すぐ消すわけではありません。「成功しているか」と「価値を生んでいるか」の二軸で、四象限に整理して判断します。
価値を生んでいる
価値が痩せた / ゼロ
成功している(緑)
健全。そのまま回す
本記事の主対象。畳むか、対象を作り直す
失敗している(赤)
惜しい。修理して価値を守る
最優先で畳む。壊れていて、しかも要らない
右上の象限に落ちたタスクには、さらに二つの問いを当てます。第一に、価値がゼロになったのは対象が枯れたからか、それとも上流が壊れて空を掴んでいるだけか。前者なら退役、後者は evidence を手掛かりにした修理です。第二に、このタスクをもう一度ゼロから書くとして、今の自分はそれを書くか。書かないなら、それは畳むべき合図だと私は考えています。
畳むと決めたら、ただ無効化するのではなく、台帳に退役の一行を残します。いつ、なぜ畳んだか。数か月後に「あの集計、どこいった」と自分が探すとき、この一行が効きます。
実際に一本を畳んでみて分かったこと
冒頭のタスク——対象が枯れて空回りしていた集計——を、私は結局畳みました。畳んでみて意外だったのは、消したことそのものより、消せると判断できたことの効果でした。
緑のログが一本減ると、残りの緑の意味が濃くなります。「全部成功」がノイズではなく信号に戻る。個人開発では監視に注げる注意力が何よりの希少資源で、痩せた自動化はその注意力を薄く広く食い潰していました。実行コストの数十回ぶんより、奪われていた注意力のほうが高くついていた、というのが正直な実感です。
もう一つ、副産物がありました。退役を判断する過程で「このタスクの価値は本当は何だったのか」を言語化することになります。valueMetric に何を選ぶか——件数なのか、変更行数なのか、節約できた手作業の分数なのか。この問いは、生きているタスクの設計にもそのまま跳ね返ってきました。App Store と Google Play 双方のリリース補助タスクについても、私は valueMetric の定義を見直しました。
運用に組み込む
最後に、この仕組みを日々の運用に載せる部分です。評価器は毎日走らせる必要はありません。私は週次で回し、退役候補が出たときだけ通知を受け取る形にしています。緑のログを毎日見に行かない代わりに、痩せだけを週に一度そっと知らせてもらう。
#!/usr/bin/env bash
# weekly-decay-review.sh — 週次で成果台帳を評価し、候補が出たら通知
set -euo pipefail
LEDGER = "${ OUTCOME_LEDGER :- $HOME / ops / ledger . jsonl }"
REPORT = "$( python3 evaluate_decay.py " $LEDGER ")"
if [ -n " $REPORT " ]; then
# 退役候補・痩せ候補があるときだけ本文を送る(失敗のみ通知の思想と同じ)
printf '成果が痩せた自動化の候補:\n%s\n' " $REPORT " | notify-ops --title "weekly decay review"
else
echo "no decay candidates" # 静かな週は静かに終える
fi
推奨する導入順序は、まず valueMetric の記録だけを既存タスクに一行ずつ足していくことです。評価も通知も後回しで構いません。台帳が数週間ぶん貯まって、はじめて痩せが「見える」ようになります。最初から完璧な閾値を狙う必要はなく、本番運用の中で ZERO_STREAK や DECAY_RATIO を自分の実行頻度に合わせて調整していくのが現実的だと感じています。
自動化を増やすことには、私たちはずいぶん慣れました。けれど、役目を終えた自動化を見送る作法は、まだあまり語られていない気がします。緑のログの中に静かに痩せていくものがいないか、週に一度だけ、そっと確かめる。その一手間が、残りの緑を信じられるものに保ってくれます。実装の起点になれば幸いです。お読みいただきありがとうございました。