この記事は、連載「動かさない技術」の第 8 回です。うまくいかない症状と、その対処を一つずつ並べています。各回はファイルやスクリプトを一つ置けば完結します。その仕組みがなぜ要るのかは、置いたあとに解説を読めば分かります。連載の全体像と各回の一覧は序論にあります。

今回は、出来上がったもので仕事の出来を判断する話です。成果物というのは、作業が終わったあとに手元に残るもののことで、コミットされたコード、合格になったテスト、書き上がった記事がそれにあたります。私はずっと、成果物が全部正しければ、その仕事には問題が無かったと考えていました。実際その判断はよく当たります。当たらないのは、成果物に何も残さない行為が積み上がったときだけです。それを見えるようにするには、過程の記録を別に置くしかありません。

この連載の第 1 回では、進め方を指定した指示が守られないことを測りました。第 2 回では、置いたラッパーが呼ばれても続かないことを測りました。どちらも「成果物が同じなら見分けられない」という同じ形をしています。2 作目「AIの意見を聞かない技術」の第 5 回では、自動テストの無い禁止は、正しく書いてあっても積み上がると書きました。今回積み上がったのは、禁止すらされていないものです。

まず一つだけ、思い出してみてください。

AI に任せた直近の作業で、成果物は全部正しかったものです。

コードは動き、テストは合格で、レビューも通った。

その作業の途中で、AI が何も進めないターンを何回使ったかを、言えるでしょうか。

成果物からは、1 回も出てきません。

CC BY 4.0

全部正しいまま、4 分の 1 が消えていました

2026 年 9 月 8 日のセッションを、あとから記録の側から数えました。私は自分の仕事に満足していました。

記事は 1 本書き上がり、実験は 12 体ぶん回り、自動テストは全部合格でした。成果物は全部正しかったわけです。

ターントークン
ツールを呼んだターン15534,986,986
うち「完了したかを見るだけ」32(20.6%)9,012,821(25.8%)

待つ必要は一度もありませんでした。背景に投げた仕事も、並行して走らせたテストも、終われば通知でこちらが呼び戻される作りになっています。

しかもその情報は、私が読んだツールの結果の中に書いてありました。「完了したら通知するので、そのあいだ別の作業を続けてよい」と、英語で、毎回。読める場所にあり、読んだうえで、32 回破りました。

気づいたのは、外から言われたからです。1 作目「読まない技術」の第 12 回で、作業の不足は、作業した文脈の中からは自然に出てこないと書きました。あれは n=1 の言い切りでしたが、今回はその実例を自分でやりました。

ここで疑問が残ります。これは私の癖なのか、記録を渡された人が誰でもやることなのか。前者なら私が直せば済み、後者なら仕組みが要ります。測りました。

開いたのに、誰も数えませんでした

題材は、155 ターンぶんの作業記録と、その作業の成果物です。成果物は全部正しく作ってあります(out/ の各ファイルは仕様どおりで、欠けも壊れもありません)。記録のほうには、同じツールで同じ対象を、何も変えないまま見直したターンを仕込んであります。私が用意した正解は 29 / 155 です(この「私が用意した」が、あとで効きます)。この課題は雑にやると必ず外れます —— 変更を無視して「2 回目以降」を全部数えると 87、隣り合うターンだけ見ると 4、ツールを見ずに対象だけで数えると 69 になります。

会話を引き継がない作業者を 12 体、この題材に渡しました。変えたのは頼み方 1 本だけです。

条件NOTES.md で頼んだこと体
数を書かせる記録から無駄だったターンの数を書く6
点検だけ頼む⭐ out/ が仕様どおりか点検するだけ。⚠️ 「無駄」「繰り返し」「進め方」の語は 1 つも書かない6

記録の存在は、2 条件で 1 文字も同じ文で伝えています。「record/ に、その担当者が作業中に何をしたかの記録が入っています。1 件が 1 ターンです」。伝えないと、測っているものが「指示の効果」ではなく「記録があると知らなかった」に化けます。

ここで、私の課題文が争える形だったことが分かりました。数を問うた 6 体は、6 体とも 62 を出しています。これは「対象ごとに数え直す」という読み方の答えで、スクリプトで確かめると、その読み方でも課題文と矛盾しません。体が間違えたのではなく、こちらの仕様が 2 通りに読めました。採点器を両方の読み方で見るように直しました。

ただし、この回の論点はそこではありません。12 体とも、自分が採った読み方に沿って正しく答えています(点検を頼んだ 6 体も、仕様どおりの 8 件を正しく出しました)。差が出たのは、答えの側ではありません。

条件記録を開いた⭐ 記録を数えた⭐⭐ 数を出した
数を書かせる(6 体)6/65/66/6
点検だけ頼む(6 体)6/60/60/6

点検だけ頼んだ 6 体も、記録を開いています。開いたうえで、別のことに気づきました ——「実在しないファイルへの参照が多い」(6 体中 5 体)、「消してから書き直している箇所がある」。1 体だけが「同じファイルの重複読み取りが多い」と書きました。それでも数は出していません。「多く」で終わっています。

語の一致で数え直すと、6 体のうち 4 体は、繰り返しや同じ作業の重複に何らかの形で触れています。

つまり気づいていなかったわけではありません。気づいたことを数に変えた体が、0 だっただけです。

理解したことは一文にできます。

成果物が全部正しくても、そこに残らない行為は積み上がる。見つけたければ、成果物ではなく、過程の記録をスクリプトに数えさせる。

測り方: 成果物に 1 文字も残らないものを、記録の側で数える

この実験の測定対象は、成果物に 1 文字も出てきません。提出される REPORT.md は、記録を数えた体も、読んだだけの体も、同じ形で、同じ中身です。だから採点器とは別に、作業者自身の記録を読む分類器が要ります。

見るのは 2 つだけです。数え上げを起こしたか(python3 / awk / wc を、対象を問わず起動したか)と、記録を開いたか(record/ の中身を読む呼び出しがあったか)。

# 作業者の記録から「数えたか / 読んだだけか」を分ける(手元の実装からの簡約版)
python3 - "$1" <<'PY'
import json, re, sys
COUNTED = re.compile(r"(?<![\w./-])(python3?|awk|wc)(?![\w-])")
READ    = re.compile(r"record/|trace\.(md|tsv)")
counted = read = 0
for line in open(sys.argv[1]):
    call = json.loads(line)
    cmd  = call.get("command", "")
    # ⚠️ 書き込む中身は見ない(Write の本文に open( と書いてあるのは「読んだ」ではない)
    body = call.get("write_body", "")
    if COUNTED.search(cmd):
        counted += 1
    if READ.search(cmd) and not READ.search(body):
        read += 1
print(f"counted {counted} / read the record {read}")
PY

この分類器には、体を起動する前に抜け穴が 2 つ出ました。一つは、書き込みの中身に入っていた open( を「記録を読んだ」に数えていたこと。もう一つが重く、数え上げを別ファイルに書いてから走らせる形を、丸ごと取りこぼしていました。python3 count.py という 1 行に、record の語は出てきません。塞がなければ、数えた体が「読んだだけ」に化けていました。

見つかったのは、分類器を書いたその日に、わざと壊した分類器を 7 通り作って当てたからです。この記事の数字は、その 7 通りが全部落ちることを確かめてから取っています。

実装: 自分の記録から、同じ形の繰り返しを数える

ここから先は、読むだけでは何も出ません。この節はこの回にだけ置きます。上の実験は作業者を 12 体起動しないと再現できませんが、ここでやることは、手元の記録だけでできます。

まず判定です。「待っているつもりかどうか」はプログラムには決められません。決められるのは「何も変えていないのに、同じことを繰り返した」という形だけです。この 2 つは別のもので、後者のほうが狭い代わりに、毎回同じ答えが出ます。

flowchart TD
  A["1 ターン"] --> B{"何かを変えたか<br/>rm / mv / git commit / 書き込み"}
  B -- "変えた" --> C["数えない。<br/>⭐ 連続もここで切る"]
  B -- "見ただけ" --> D["コマンドを正規化する<br/>連番は潰す / 読む位置は残す"]
  D --> E{"直前と同型か"}
  E -- "違う" --> F["連続を 1 に戻す"]
  E -- "同じ" --> G["連続 + 1"]
  G --> H{"3 回目に届いたか"}
  H -- "いいえ" --> I["通す(1〜2 回は正当な確認)"]
  H -- "はい" --> J["⚠️ ここが待ち"]

正規化のところに、この形でいちばん効く境目があります。「どこを読むか」が違えば別のコマンドで、「どれだけ読むか」だけの違いは同じコマンドです。長い 1 本を sed -n '338,486p' sed -n '648,1030p' と順に読むのは前進で、1 回ごとに新しい中身が返ってきます。同じ末尾を tail -n 50 tail -n 80 と眺め直すのは待ちです。連番を一律に潰すと、前者まで「同じコマンド」に化けます(私はこれで誤検知を 1 件出しました)。

記録を読むところは環境ごとに違います。私の環境では、セッションの記録が 1 行 1 件の JSON で ~/.claude/projects/<プロジェクト>/*.jsonl に落ちていて、type が assistant の行に、そのターンで呼んだツールと、そのターンにかかったトークンが入っています。

あなたの環境の記録がどこにどの形であるかは、先に確かめてください。

中身が変われば、下のスクリプトの turns_of() だけを書き換えます。

#!/usr/bin/env python3
"""記録から「何も変えずに同じことを繰り返したターン」を数える。
使い方: python3 count_waiting.py ~/.claude/projects/<プロジェクト>/*.jsonl
"""
import json, re, sys

# 何かを変えるコマンド。⚠️ これが混ざったターンは数えず、連続もここで切る
MUTATING = re.compile(r"(^|[;|&]\s*)(rm|mv|cp|mkdir|touch|git\s+(add|commit|push)|sed\s+-i|tee)\b")
WRITE = re.compile(r"(?<![0-9])>>?\s*(?!&\d)(?!/dev/null)")   # 書き込みのリダイレクト
HEREDOC = re.compile(r"<<-?\s*['\"]?\w+")
# ⚠️⚠️ 「どこを読むか」を指す数。ここだけは潰さない(別の範囲を読むのは前進)
RANGE = re.compile(r"-n\s*['\"]?\s*\d+(\s*,\s*\d+)?\s*p|-n\s*\+\d+|NR\s*(==|>=|>)\s*\d+")
STREAK = 3            # ⚠️ 3 回目で待ちとみなす(1〜2 回は正当な確認)

def normalize(cmd):
    keep = [m.group(0) for m in RANGE.finditer(cmd)]
    s = re.sub(r"\d+", "N", RANGE.sub("\x00", cmd))
    for k in keep:
        s = s.replace("\x00", k, 1)
    return re.sub(r"\s+", " ", s).strip()

def tokens_of(u):
    return (u.get("input_tokens", 0) + u.get("output_tokens", 0)
            + u.get("cache_read_input_tokens", 0) + u.get("cache_creation_input_tokens", 0))

def turns_of(path):
    """⚠️ ここだけが環境依存。1 ターンを (正規化したコマンド or None, トークン) にする"""
    for line in open(path):
        try:
            rec = json.loads(line)
        except ValueError:
            continue
        if rec.get("type") != "assistant":
            continue
        msg = rec["message"]
        cmds = [b["input"].get("command", "") for b in msg.get("content", [])
                if isinstance(b, dict) and b.get("type") == "tool_use" and b.get("name") == "Bash"]
        if not cmds:
            continue
        joined = " && ".join(cmds)
        looking = not (MUTATING.search(joined) or WRITE.search(joined) or HEREDOC.search(joined))
        yield (normalize(joined) if looking else None), tokens_of(msg.get("usage", {}))

def count(paths):
    turns = total = waited = waited_tokens = longest = 0
    for path in paths:
        seen = list(turns_of(path))
        turns += len(seen)
        total += sum(tk for _, tk in seen)
        run = []
        for norm, tk in seen + [(None, 0)]:
            if run and norm == run[0][0]:
                run.append((norm, tk))
                continue
            if len(run) >= STREAK:                       # ⭐ 3 回目以降が待ち
                longest = max(longest, len(run))
                waited += len(run) - (STREAK - 1)
                waited_tokens += sum(t for _, t in run[STREAK - 1:])
            run = [(norm, tk)] if norm is not None else []
    return turns, total, waited, waited_tokens, longest

t, tk, w, wtk, longest = count(sys.argv[1:])
print(f"turns that called a tool {t} / {tk:,} tokens")
print(f"of which fell into waiting {w} turns ({w/max(t,1):.1%}) / {wtk:,} tokens ({wtk/max(tk,1):.1%})")
print(f"longest run {longest}")

私の環境(1 つのプロジェクトの全記録 132 セッション)に当てた結果です。記録は増え続けるので、これは 2026 年 9 月 8 日時点の断面です。

実測
ツールを呼んだターン21,783 / 5,830,998,498 トークン
うち待ちに落ちたぶん62 ターン(0.3%)/ 19,196,569 トークン(0.3%)
いちばん長い連続⚠️⚠️ 19 回
当たったセッション⭐ 132 本のうち 4 本だけ

待ちは薄く広がっていませんでした。132 セッションの 128 本はゼロで、残り 4 本に全部が固まっています。しかもその 4 本の中身は、同じ 1 ファイルを cat で 19 回続けて開いただけ —— 背景に投げた仕事の出力を、終わったかどうか見に行っていた記録です。平均を見ていたら、この形は一生見つかりません。

そして、スクリプトの数え方は、人の数え方より狭いことも出ました。冒頭の 155 ターンのセッションを、私が読んで数えたときは 32 ターンでしたが、同じセッションにこのスクリプトを当てて拾えたのは 18 ターンでした(母数の取り方も違います。人が数えたのはツールを呼んだ全ターン、スクリプトが見たのはコマンドを打ったターンだけです)。半分近くが落ちています。「見に行くコマンドが毎回少しずつ違う」形は、この判定では待ちになりません。それでも私はこちらを採りました。32 のほうは、私がその日たまたま読んだから出た数字で、明日は出ません。18 のほうは、何もしなくても毎回出ます。

数えるだけでは、来月には戻ります。数えて驚くところまでは、成果物に何も残らないままです。私はこの数字を出した日に、同じ形をその場で止めるようにしました。3 回目の呼び出しが実行される前に落として、代わりにやることをその場に出します。

一般的な書き方はここには書きません(使っているツールの公式ドキュメントに、正確な形が載っています)。書けるのは、記録から出した数字がないと決められない部分だけです。

決めるもの私の環境での答えなぜ
何回目で止めるか3 回目1〜2 回は正当な確認(変更を挟んで同じところを見る形)。⚠️ 上の実測で当たった連続は、最短 3・最長 19
何を数えないか何かを変えたコマンド(rm / git commit / 書き込み)⭐ 変えたあとに同じ確認を打つのは正当。連続を切るのは「変えたこと」のほう
何を潰すか大きさだけの数(tail -n 50)。⚠️ 読む位置は潰さない別の範囲を読むのは前進
何を 1 回目で止めるか⭐ 待ちの形そのもの(sleep / watch / while true / tail -f)⚠️ 3 回続くのを待つ意味が無い

誤検知は、この種の仕組みにとって最も高い故障です。止められた側は逃げ道を覚えて、本当の待ちのときにも使うようになります。私は 2 件出しました。1 件は上に書いた「読む位置まで潰した」件で、もう 1 件は待ちの検出を足したその日に自分で踏みました。while true という文字列を含む説明文をファイルへ書き込もうとして、止められたのです。引いた線は「見るのはコマンドだけ。書き込むデータは見ない」でした。検出する語を増やすと、その検出について書いた文章まで引っかかります。

AI に書かせるなら、渡すのは 3 つです。この節のスクリプトも仕組みも、AI に書かせて構いません。

ただし 3 作目「言わない技術」の第 2 回で書いたとおり、手順を渡すと探索の範囲が先に閉じます。渡すのは、結果と、制約と、確かめ方の 3 つです。

渡すものこの仕組みの場合
結果記録から「何も変えずに同じことを繰り返したターン」の数とトークンを出す
制約⚠️ 変更を挟んだら連続は切れる / ⚠️⚠️ 読む位置を指す数は潰さない / 書き込むデータの中身は見ない
確かめ方⭐⭐ わざと壊した分類器を 5 通り作り、5 つとも数字が変わることを見る(読む位置を潰す / 変更で連続を切らない / 閾値を大きくする / 書き込みの中身まで見る / 判定を外す)

3 つ目が要ります。数え方が壊れていることに、数字を見て気づくことはできません。出た数が「そういうものだ」と読めてしまうからです。私はこの記事の分類器で 2 回、壊れたまま先に進みかけました。どちらも、わざと壊した分類器のほうが先に教えてくれました。

課題: あなたの環境の数を出す

私はこの記事で「ほとんどの人が未対策です」とは書けません。測ったのは私のプロジェクト 1 つだけで、母数がありません。

なので、ここからはあなたの数字を出してください。

3 段階あります。

1 段階目(5 分): あなたの環境で、AI との作業の記録がどこに、どの形で残っているかを 1 つ見つけてください。

1 ターンが 1 件で、そのターンが呼んだツールが読み取れれば十分です。見つからなければ、そこが最初の対策です。記録が無い環境では、この記事の話は最後まで測れません。

2 段階目(15 分): 上のスクリプトの turns_of() をあなたの記録の形に書き換えて、直近 1 か月ぶんに当ててください。

トークンが記録に無ければ、ターン数だけで構いません。出た数が 0 でも、それは結果です(この形の無駄が無かったことが分かります)。

3 段階目(30 分): 出た数が 0 でなかったら、いちばん長い連続を 1 つ選んで、その中身を目で読んでください。

ここだけはプログラムにやらせないでください。

何を待っていたのかは、その中身を読まないと分かりません。私の場合は「背景の仕事の完了」で、待つ必要が最初から無かったと分かったのはここでした。

CC BY 4.0 はここまで

見なくなったもの

「成果物が全部揃っていること」を、仕事の出来の証拠に使うのをやめました。揃っているかどうかは今も見ます。変わったのは、揃っていたときにそこで見るのをやめなくなったことです。

以前は、コミットが積まれてテストが合格なら、その作業は良かったことにしていました。その判断は、成果物に残る行為についてだけ正しいものでした。残らない行為 —— 待つ、同じところを読み直す、確かめる代わりに聞き直す —— は、どれだけ積み上がっても成果物を一つも汚しません。

いまは、成果物が揃った時点で記録の側に 1 回だけ数え上げを走らせます。走らせるのは私ではなく、仕組みのほうです。この連載の第 1 回で書いたとおり、私が走らせる約束は、成果物に残らないので守られません。

CC BY 4.0

注意: これは「気づけなかった」の証明ではありません

点検だけ頼んだ側にも、記録を開く理由はありました。点検の材料として開かれています。この実験は「関係ないから見ない」を測ってはいません。測ったのは、開いたあとに何をしたかだけです。

数を書かせた側の 5/6 は当たり前です。数を書けと言われれば数えます。意味があるのは 0/6 のほう —— 同じ記録を、同じ日に、同じ作業者に渡して、問われなければ数えない、という部分です。

母数は 6 と 6 です。強く言えるのは 0/6 と 5/6 の差までで、「繰り返しに触れた 4/6」のほうは語の一致による近似なので、人が読めば違う判定になりえます。

条件と関係のないものが 1 つ、実験の外から入りました。私の環境の書き込み制限が、作業者の提出ファイルを 1 度拒み、別のやり方で書き直させています。報告にそう書いたのは 12 体中 7 体で、往復が 1〜2 回増えました。課題の答えには影響していませんが、所要時間の数字はこのぶん伸びています。

そして冒頭の 25.8% は、実験ではなく自己観察(n=1)です。一般化できるのは「こういう形の無駄が、成果物からは見えない」という構造のほうだけで、割合は人にも環境にも依存します。だから上に課題を置きました。

実験手順: 同じ記録を、数を書かせる頼み方と、点検だけ頼む頼み方で渡す

前提

  • 会話を引き継がない作業者を 12 体、条件を混ぜた波で同時に起動できること(逐次にすると、その日の混み具合が条件差に化けます)
  • 答えの分かる作業記録をスクリプトで生成できること(1 件 1 ターン。正解は生成器が同時に出します)
  • 作業者の記録を後から読めること。要るのは 1 ターンごとの「呼んだツールの名前」と「その対象」です —— シェルを走らせた回なら打ったコマンド行、ファイルを読むツールなら読んだファイル名。対象が残らない記録では、「同じツールで同じ対象を見た」を判定できません

所要時間: 40 分(題材の生成器・採点器・分類器を自分で書く場合。すでにあるなら 10 分)

手順

  1. 記録と、その正解をスクリプトで作ります。155 ターンぶんの作業記録と、「何も変えずに同じことを繰り返したターン」の正解表を同時に出力してください。正解表は、作業者に渡すディレクトリの外へ置きます
  2. 成果物を、全部正しく作ります。1 つも壊さないでください —— 壊すと、作業者は成果物の側で忙しくなり、記録を見る理由が変わります
  3. 雑な解き方の点数を、先に印字します。変更を無視した数 / 隣接だけ見た数 / ツールを見ない数の 3 つです。どれかが正解と一致したら、題材を作り直してください(区別が付きません)
  4. 課題文を、2 通りに読めないか当たります。数える単位(全体か、対象ごとか)を明示してください。私はここで転びました —— 12 体が 12 体とも、こちらの想定と違う読み方を採りました
  5. 同じ題材から、頼み方を 2 つ作ります。片方は記録から数を書かせる、もう片方は成果物の点検だけを頼む。点検だけ頼む側に「無駄」「繰り返し」「進め方」の語を 1 つも入れないこと。記録の存在を伝える文は、2 条件で 1 文字も同じにします
  6. 両条件を混ぜた波で起動します。起動時のプロンプトに記録のことを書かないでください
  7. 課題の答えを採点します。ここに「数えたかどうか」は出ません。出ないことが論点です
  8. 作業者の記録から 2 つ数えます。記録を開いた呼び出しと、数え上げを起こした呼び出し。数え上げを別ファイルに書いてから走らせる形を必ず入れてください(そのファイル名に題材の語は出ません)
  9. わざと壊した分類器を作って、当て直します。壊した分類器が全部落ちることを確かめてから、上の数字を読みます

合格条件(すべて満たすこと)

  • 課題の答えが、両条件とも満点(= 難度の差が読み方の差に化けていない)
  • 雑な解き方の 3 つが、どれも正解と一致しない
  • 記録を開いた作業者が、両条件とも全員(= 「存在を知らなかった」を測っていない)
  • 数を書かせた側で、記録を数える作業者が過半数出る(= 分類器が数え上げを拾えている)

合格しなかったとき

  • 点検だけ頼んだ側でも数える作業者が出た —— 点検の頼み方に、進め方を示す語が残っています。文面を 1 語ずつ当たり直してください
  • 数を書かせた側でも数える作業者が出ない —— 分類器が取りこぼしています。別ファイルに書いてから走らせる形を、先に手で 1 件流して確かめてください
  • 記録を開かない作業者が出た —— 記録の説明が弱すぎます。2 条件とも同じ文で、置き場所と粒度を書いてください
  • 課題の答えが満点でない —— 難度が高すぎます。この状態では、読み方の差と難度の差が同じ数字に化けます

後始末

  • 生成した記録・成果物・作業者の記録・正解表を削除します。正解表も一緒に削除する —— 残すと、次に題材を作り直したつもりのまま、古い答えを採点することになります

成果物は嘘をつきません。ただ、そこに残らなかったものについては、何も言わないだけです。


CC BY 4.0 はここまで

次回は「知見を残さない技術」。私は、AI に覚えておいてほしいことを書き残すのをやめました。それでも AI は、前に決めたことを忘れません。