今回は、宣言の話です。宣言というのは、前回置いた 2 問——何を検証したいのか、なぜ安い手段では済まないのか——を、実行する前に文章で書かせることです。私はそれを置いたあと、書かずに走った実行が何回あったのかを、一度も数えられませんでした。
1 作目「読まない技術」の第 4 回で、読んだかを測らない。書いていなければ動けない状態だけを見る、と書きました。今回は、その物差しを前回の宣言そのものに当てます。
まず一つだけ、探してみてください。直近のセッションで、AI が撮影やスモークを走らせた実行を 1 つ選びます。その実行の直前に、なぜそれが要るのかを書いた文は、どこかに残っているでしょうか。
前回は「撮影の前に Q1(検証したい問題)と Q2(なぜ安い手段で済まないか)を書かせる」仕組みでした。書こうとした瞬間に、ルールを破りかけていることへ AI 自身が気づく——ガードレールとして効いています。残る課題は一点、宣言の工程を飛ばした実行は素通りすることです。素通りは記録に残らないので、何回起きているかも推測のままでした。
理解したことは一文にできます。
宣言は効く。ただし、書かれたときだけ効く。だから宣言が書かれた形跡を、実行の前提にする。
CC BY 4.0
仕組み: 宣言ファイルの存在を、実行の前提にする
置いたのは PreToolUse hook 1 本です(手元の運用からの簡約版)。
# 撮影・スモークの入口だけを拾う(SKIP_VISUAL_GUARD=1 は明示の逃げ道として通す)
case "$command" in *SKIP_VISUAL_GUARD=*) exit 0 ;; esac
printf '%s' "$command" | grep -qE 'screenshot\.sh|smoke_[a-z_]+\.js' || exit 0
# 1. 宣言ファイルが無ければ動けない
[ -f tmp/visual_verification.md ] || { guide; exit 2; }
# 2. 禁句は機械的に弾く(機械に決められるのはここだけ)
grep -qE '念のため|とりあえず|確認のため' tmp/visual_verification.md && exit 2
# 3. Q1 / Q2 が 1 文ずつ埋まっているか(空欄・テンプレのままを弾く)
# 4. 鮮度 30 分(前の検証の宣言の使い回しを弾く)
線引きは前回の二分法のままです。「単体テストで済むか」の真偽は機械に決められないので、hook は中身の妥当性を測りません。測るのは、判断が実行より先に行われた形跡だけ——ファイルの存在、2 問の充足、禁句、鮮度。前回の言い方では、宣言はルールに従う確率を上げる策、hook はルールをなくす策。ここでなくしたのは「宣言を書いてから実行する」の 1 行だけで、宣言型はやめていません。禁句リストが言い換えに抜かれるのも前回に書いたとおりで、ここで塞いだのは一番安易な経路だけです。
置き換え先のない禁止は、検証の放棄を生む
作ってみて、止めるだけでは足りないと分かりました。ブロックの瞬間、AI の TODO には「スクリーンショットで確認」が積まれています。実行だけ止めると、タスクが宙に浮くか、検証ごと放棄されます。「確認できませんでした」で閉じる作業は、撮影が走るより悪い結末です。
だから案内文に置き換え先を明示しました。「TODO の『撮影で確認』を『grep / 単体テストで確認』に書き換えて消化する。本当に画面が要るなら Q1/Q2 を書いて実行し直す」。1 作目 第 8 回のパターン——方針は問題の瞬間に差し込まれたときだけ読まれる——の変奏です。禁止を置くなら、止めたその画面に次の一手を書いておく。
実測: 導入した日に、2 つ獲れた
1 件目、自動テストが hook 自身のバグを見つけました。 bash の ${#var} はロケール次第で文字数ではなくバイト数を返します。「見たい」は 3 文字ですが 9 バイト。「8 文字未満は空欄扱い」の判定を、3 文字の手抜き宣言が素通りしていました。空欄チェックの自動テスト項目を書いたから見つかり、文字数は python3 で数える形に直しました。
2 件目、hook が導入者である私自身を止めました。 動作を確かめる検証コマンドが、screenshot.sh という文字列を含んでいただけで止まる偽陽性です。案内文に書いておいた逃げ道の環境変数で、私は通過できました。ガードレールは必ず誤作動します。大事なのは、誤作動した瞬間に読める場所に出口が書いてあることです。
hook はガードレールであると同時に計測器です。判定のたびに記録ファイル(日時・止めた理由・コマンド先頭 120 字)へ 1 行追記するので、前回まで推測のままだった「宣言なしの実行が何回あるか」を数え始めました。4 日で 22 回この hook を通り、止まったのは 7 回です。7 回とも、2 件目と同じ偽陽性でした。 コマンド文字列に入口の名前が入っていただけで、撮影の実行は 1 件もありません。うち 2 件は、宣言ファイルを書くコマンド自身です。記録に残った理由は「前の宣言の使い回し」ですが、それは判定の名前であって、止めた相手ではありません。宣言なしの撮影も、禁句も、この 4 日では 1 度も来ていません。
CC BY 4.0 はここまで
読まなくなったもの
宣言の中身です。私は、AI が書いた「なぜ確かめるか」を一度も読んでいません。 hook は中身の妥当性を測らないと決めてあるので、読んで良し悪しを決める役が、どこにもいません。私が読むのは、止まったときに記録へ 1 行増える「いつ・何を止めたか」だけです。それでも、理由を書かずに走った実行はありません——書いていなければ、実行そのものが始まらないからです。
CC BY 4.0
注意: 形は見ていますが、中身は見ていません
この hook を通った宣言が、正しい宣言だとは限りません。Q1 と Q2 にそれらしい文が 1 文ずつ入っていて、禁句が無く、30 分以内に書かれていれば通ります。「単体テストからは呼べない画面なので」と書けば通ります。 それが本当かどうかは、機械には決められません。
塞いでいません。塞ごうとすると、中身の妥当性を判定する側をもう一段作ることになり、そこは結局また誰かの判断になります。ここで手に入れたのは「判断が実行より先に行われた」という形跡だけで、判断そのものの質は、前回のまま AI の側に残っています。
もう一つ。この hook が拾えるのは、拾う対象として書いた入口の名前だけです。⚠️ 手元がまさにそれで、hook に書いたのは内側で動く道具のファイル名でした。私が普段叩いているラッパーのほうは 14 本あって、そちらは置いた日から一度も見られていません。入口を足したら、hook の側にも足す。 名前の取り違えも、足し忘れも、何も言わずに通ります。
検証手順: 止まるべきものが止まり、通るべきものが通る
前提
- この hook を登録し、宣言ファイルの置き場と禁句の一覧を決め終えていること
- 撮影のコマンドが手元にあること
所要時間: 20 分(9 項目を一度に自動テストへ書くなら、もう少しかかります)
手順
あなたが、下の 6 通りを 1 つずつ AI に渡して、hook の反応を控えます。止める側だけを確かめて終わらせないことが、この節の全部です。止まる 4 つと、通る 2 つを併置します。
| # | 何を渡すか | 期待する結果 |
|---|---|---|
| 1 | 宣言ファイルを置かずに撮影のコマンドを実行させる | 止まる。案内に「Q1 と Q2 を書け」と、書き換え先(撮影 → grep / 単体テスト)が出る |
| 2 | Q2 に禁句だけを書いた宣言(「念のため」) | 止まる。どの語が禁句かが出る |
| 3 | Q1 か Q2 が空の宣言 | 止まる |
| 4 | 古い宣言(前の作業のものを残したまま) | 止まる。いつの宣言かが出る |
| 5 | Q1 と Q2 が具体的に埋まった宣言 | 通る(撮影が走る) |
| 6 | 案内文に書いておいた逃げ道(環境変数など) | 通る |
合格条件(すべて満たすこと)
- 1〜4 が、期待する結果のとおりに止まる
- 5 と 6 が、止まらずに通る
- 禁句の判定を hook から 1 行消すと、2 が通ってしまう(= その項目を見ている自動テストが、消したことに気づく)
合格しなかったとき
- 5 と 6 が止まった —— ⚠️ この hook は「撮影を全部止める道具」になっています。止まることしか確かめない自動テストは、hook がすべてを止める壊れ方を映せません——1 作目 第 6 回で見た「落ちたことのない自動テスト」と同じ落とし穴です
- 判定を 1 行消しても、自動テストが気づかない —— その項目は、置いた日から何も見ていません
後始末
- 消した判定の 1 行を戻し、置いた宣言ファイルを片付けます
CC BY 4.0 はここまで
次回は「テストを再実行しない技術」。私は、確認のためにテストを走らせ直すのをやめました。それでも、いま通っているかどうかは毎回わかります。