NanoClaw のアップデートで RTK が pnpm test を素通りしてた話
TL;DR — RTK の rewrite は
pnpm run buildを圧縮するがpnpm test(実体はvitest run)は素通り。加えて/update-nanoclawが/update-skillsを同一コンテキストで自動呼び出ししていた。SKILL.md を2行変えて解決。
NanoClaw の大型アップデート来ましたね。かなり中身が変わっていてローカルでのマージにものすごく時間がかかりました。最終的には、
「もう俺の作ったスキルさえ無事なら後はどうでもいいから全部上書きしちゃって!」
と雑な投げ方をしてアップデートをしてもらいました。
無事(一部無事じゃなかったけど git のログから復旧した)終わったんだけど、後日再度 /update-nanoclaw を実行した時の話です。
/update-nanoclaw を実行したら5時間制限の40%くらいを1回で使い切ってしまった。なにこれ、と思って Claude さんに調べてもらった。
以前 RTK を入れた記事を書いたとき、「効果はこれからに期待」と書いたんだけど、うまく行かなかったようで…
以下は、Claude さんに解説してもらった文章です。
RTK の仕組みをおさらい
RTK は Claude Code の PreToolUse hook として動く。Bash ツールが呼ばれるたびに rtk hook claude が起動し、rtk rewrite "<コマンド>" でコマンドを書き換えられるか確認する。
- 書き換えできる(exit 3)→
rtk <subcommand>が代わりに実行され、出力が圧縮される - 書き換えできない(exit 1)→ 元のコマンドがそのまま実行される(RTK はノータッチ)
何が素通りしていたか
rtk rewrite "pnpm run build"
# → rtk pnpm run build(exit 3: 書き換え成功)
rtk rewrite "pnpm test"
# → (何も出力なし、exit 1: 書き換えなし)
pnpm run build は圧縮される。でも pnpm test は exit 1 で素通り。
nanoclaw の package.json を確認すると "test": "vitest run" だった。
rtk rewrite "vitest run"
# → rtk vitest(exit 3: 書き換え成功)
RTK は vitest run は知っている。ただ pnpm test → vitest run のマッピングがないせいで、pnpm test という形で呼ばれると気づかない。vitest の全出力がそのまま Claude のコンテキストに流れ込んでいた。
もう1つの原因:/update-skills の自動呼び出し
/update-nanoclaw の Step 7 には、upstream に skill/* ブランチが存在するとき AskUserQuestion で確認し、Yes なら /update-skills を同一セッション内で呼び出す処理があった。
スキルが同一コンテキストで実行されると、そのスキル全体の処理がコンテキストに積み重なる。/update-nanoclaw がすでに積んだ git diff・ビルド出力・対話の上に、さらに /update-skills 分が乗ってくる。二重に膨らんでいた。
なぜ .rtk/filters.toml では解決できないか
RTK はプロジェクトローカルの .rtk/filters.toml で出力フィルタを定義できる。ただこのフィルタが効くのは、RTK が自前でコマンドを実行する場合のみ(rtk vitest run のように)。
pnpm test は rtk rewrite で素通りになるため、RTK を経由せずに bash がそのまま実行される。フィルタを書いても届かない。
グローバルな rewrite ルールとして pnpm test → rtk vitest run を追加する手もあるが、pnpm test の中身はプロジェクトによって jest だったり mocha だったりする。グローバルに入れると他のリポジトリで壊れる可能性があるので避けた。
修正:SKILL.md を2行変える
SKILL.md はアップストリームのファイルなので変えると次回 /update-nanoclaw 時に conflict として出る。ただ変更は小さいので conflict 解消は10秒で終わる、ということにして直接変えた。
Step 5(pnpm test → rtk vitest run):
- - `pnpm test` (do not fail the flow if tests are not configured)
+ - `rtk vitest run` (do not fail the flow if tests are not configured)
Step 7(/update-skills 自動呼び出しを削除):
- - Use AskUserQuestion to ask: "Upstream has skill branches. Would you like to check for skill updates?"
- - Option 1: "Yes, check for updates" ...
- - Option 2: "No, skip" ...
- - If user selects yes, invoke `/update-skills` using the Skill tool.
- - After the skill completes (or if user selected no), proceed to Step 8.
+ - Tell the user: "Upstream has skill branches. Run `/update-skills` to apply skill branch updates."
+ - Do not invoke `/update-skills` automatically. Proceed to Step 8.
/update-skills を削除したのはトークン削減だけが理由じゃなく、「アップデートしたいタイミングを自分で選びたい」という面もある。自動で動かれると何が更新されたか把握しにくい。
ちなみにこの記事を投稿した後、
/update-skillsも同様の修正が必要だったので対応しました。(またトークン無駄にした…)
まとめ
RTK は hook が設定されていても、コマンドの実体を知らないと圧縮が効かない。pnpm test のような「スクリプト名が実行コマンドと違う」ケースは盲点になりやすい。自分のプロジェクトでよく使うコマンドが RTK に認識されているか一度 rtk rewrite で確認しておくといい。
rtk rewrite "pnpm test" # exit 1 なら素通り
rtk rewrite "npm run test" # これも確認
rtk rewrite "make test" # など
自分の感想
ということで Claude さんに大変分かりやすく解説してもらいました。
追記として、上記の SKILL.md への修正内容は自分のローカルメモリに記録してもらいました。じゃないと次マージした時のコンフリクトで困るしね。
Claude さんにアドバイスされたけど、rtk gain も定期的に見た方がいいのかも。