毎朝の日記生成にコマンドを1本足し、Claude Code の通知音が鳴らない原因を切り分けた

開発claude-code-tools

毎朝の日記生成にコマンドを1本足し、Claude Code の通知音が鳴らない原因を切り分けた

朝は毎日回る仕組みを1本増やし、夜は鳴っているはずの音が鳴らない理由を追った。 どちらも、普段は黙って動いている部分に手を入れる作業になった。

毎朝の生成に、保有銘柄の値動き要因を足す

日記生成チェーンに、独立したコマンドを1本足すことにした。 持ち高の中心は MU / NVDA / SNDK / AMD で、加えてメモリメーカーとして SK ハイニックスとサムスンの動きを見ている。 この6銘柄について、株価が動いた要因を毎朝1日分まとめさせたい。

引っかかったのは曜日だった。 日本時間の月曜の朝に回しても、米国市場は前の週末から一歩も動いていない。 逆に土曜の朝なら、金曜のセッションが終わった直後で材料が揃っている。 そこで「直近1日分」を固定で取るのではなく、前回どこまで見たかをテキストに残しておき、次に回したときに空いた分を埋める形にしてもらった。 1日休んで翌日に2日分まとめて取る、というパターンにもこれで乗る。

まず NYSE の営業日判定とカバー範囲の算出だけを純関数として切り出させ、テストを23件通した。 ここが固まれば、あとは取得元と出力の話になる。

株価が取れない

足回りで詰まった。

Yahoo から取ろうとしたところ、同じ URL でも curl は 200 を返し、Node の fetch は 429 を返す。 リクエストの見た目で弾かれているらしいので、取得を curl 経由に寄せた。 それでも 429 が続いた。 User-Agent の問題ではなく IP 単位の頻度制限だったので、間隔とリトライを現実的な値に直しても抜けなかった。

待っている時間がもったいないので、別ソースを1つだけ試させ、並行してスキル本体を書かせた。 これが当たりで、stockanalysis.com は Yahoo と同じ値を返し、レート制限にも当たらない。 ただし韓国銘柄はここでは取れない。

結局、米国4銘柄は stockanalysis、韓国2銘柄は Yahoo という二段構えにした。 Yahoo への呼び出しが6回から2回に減るので、制限にも当たりにくくなる。 その韓国側もさらに Naver Finance に置き換えて、6銘柄すべてが揃った。

認証が入っていなかった

コマンド一式ができて、いざ通そうとしたら止まった。 作業機の側に xAI の認証がなく、copilot しか登録されていない。

ターミナルにコマンドを貼って自分でブラウザ認証を済ませ、「もう一回やってみてください」と投げ直した。 今度は通った。 仕組みを作った当日に認証で一度止まるのは、何度目かわからない。

月間レンジが1年分だった

生成された記事を見ると、月間レンジの数字が明らかにおかしい。 生データを確認させたら、range=1M が効かずに1年分の252行が返ってきていた。 表示だけを直すのではなく、直近21営業日と1年レンジをこちらで明示的に計算する形に変えさせた。

ここまで済ませて、実際に前日分の記事を生成し、Chrome で描画まで見た。

ダブルビートなのに売られた理由

出てきた記事を読んで引っかかったのが AMD だった。 決算はコンセンサスを上回っていたはずなのに売られている。 下げた理由も、たしかイーロン・マスクが AMD について何か言ったからだったはずだ。

記事はその点を CapEx と粗利率だけに寄せていて、マスク発言は NVDA 側のセクションにしか書いていなかった。 裏を取らせたら、記事は間違いというより配分を誤っていた。 マスクの発言は AMD 下落の共犯で、しかも AMD の決算とほぼ同時刻に出ていた。

自分の記憶のほうが、生成された記事より正確だった。 記事を直させたうえで、スキル側に「結論は必ず冒頭に置く」「下落要因を片方に寄せない」の2点を仕様として固定させた。 次に回したときにも効くようにしておかないと、同じ配分ミスをまた読む羽目になる。

本人の発言か、市場の解釈か

もう一つ、記憶に引っかかっているものがあった。 マスクが AMD の GPU そのもの、技術構成の方向性について疑問を呈していなかったか。

1回目の検索は「名指ししていない」と返してきた。 自分が見たはずだと思っているものは、たいてい実在する。 そう思って、角度を変えて調べ直させた。

結局4回まわして、輪郭が出た。 本人は AMD の技術にも方向性にも言及していない。 ただし、記憶に近いものは実在した。 途中で、5月に「AMD と NVIDIA の両方を買い続ける」と言っていたという言及も引っかかった。 事実なら本人の方針転換になるので、これも確かめさせた。

本人が言ったことと、市場がそれをどう受け取ったかは別物だ。 この切り分けが今回いちばん効いたので、スキルにルールとして書き足させた。

強調記号が閉じない

生成された記事の表組みで、** が生のまま表示されていた。 全角の句点の直後に文字が続くと、GFM が強調を閉じてくれない。

直したあとに別の行でも同じ症状が出て、今度は開始側が効いていなかった。 ** の直後が全角の括弧で、かつ直前が文字だと開けない。 記法の癖なので、両方向をルールに書かせた。

コミット前に混ざっていたもの

差分を確認したら、package.json に別作業由来の2行が混ざっていた。 自分の3行だけをステージして push した。

片方のリポジトリだけ通知音が鳴らない

夜になって、ずんだもんの音声が再生されていないことに気づいた。 原因を調べさせた。

最初に返ってきたのは、VOICEVOX エンジンが応答せずプロセスも見つからないという報告だった。 ただ、それでは症状に合わない。 このセッションでは鳴っている。 鳴っていないのは book-knowledge-base のほうだ。 「片方だけ鳴らない」という形にして投げ直した。

先に外れた仮説

そこから切り分けが始まった。

グローバルの Stop hook は、事前生成した wav をランダムに再生する方式だった。 その場でエンジンを叩くわけではないので、エンジンが落ちていることは症状と関係ない。 最初の報告はここで外れた。

次にプロジェクト側の上書きを疑わせた。 book-knowledge-base に hooks 設定はなく、上書きは起きていない。 wav も10個そろっている。 設定を更新したのは前日で、問題のセッションが開いたのは 22:41 だから、更新より後に始まっている。 タイミングの話でもない。

自分のテストが失敗した理由

その途中で、再生テスト自体が失敗した。 これは調査対象ではなく、テストの走らせ方の問題だった。 テストが zsh で走っていて、zsh の配列は1始まりだから添字がずれる。 hook 本体は bash -c で動くので、bash で再テストさせたら音が鳴った。

仕組みは生きている。 壊れているのは別の場所だ。

Stop hook が鳴る瞬間

決定的だったのは、book 側のログに hook の実行痕跡があるかどうかだった。 確認させると、book 側でも hook 機構自体は動いていた。 別の hook が exitCode 0 で完了している。

残るのは Stop hook の発火条件だけになった。 Stop hook は応答を終えた瞬間に鳴る。 book 側のセッションは 22:43 にプロンプトを受けたあと、確認した時点までツールを実行し続けていた。 一度も応答完了に到達していない。

故障ではなく、鳴るべき瞬間がまだ来ていなかっただけだった。

どの音がランダムで、どの音が固定か

ここで、鳴っている音の側にも疑問が出てきた。 メッセージを送ったときの音はいくつかの中からランダムに選ばれている気がするが、codex のレビュー完了には専用の音がある。 セッション開始の音もだいたい同じだ。 全部がランダムということはないはずだった。

実装を全部読ませて対応表を作らせたら、そのとおりだった。 ランダムなのは2つだけで、残りは全部固定音源だった。

ついでに、README に書かれていた対応表が実装と食い違っていることも出てきた。 セリフの定義は一度もコミットされておらず、生成スクリプトは初回コミットから変わっていない。 README の記述のほうが誤りだった。

失われたセリフを wav から起こせないかと思ったが、作業機に VOICEVOX 本体も音声認識ツールも入っていない。 代わりにセッション履歴を探させたら、6月に生成したときの記録が残っていて、そこからセリフを取り出せた。 エンジンを入れ直せば作り直せるが、1.7GB あるので判断を保留にした。 未生成のまま残っていた1つは、その用途自体を作業機で使わない方針なので、生成不要と決めて理由をルールに書かせた。

生成スクリプトには上書きガードを入れさせた。 定義が失われている20本を --force でも対象外にしておかないと、次に再生成したときに黙って消える。

無関係として外したスクリプト

一度コミットして push したあとに、引っかかっていたことを聞いた。 codex のレビュー完了音のスクリプトは、以前に自分が作らせたものではなかったか。

調べさせたら、当たりだった。 調査の途中で「今回の変更とは無関係」として素通りした箇所で、中身も日時も見ていなかった。

そのスクリプトは誤爆バグの修正版で、README のコミットより後に作られていた。 そして現在の設定はその修正版を呼んでおらず、バグのある旧実装のままだった。 スクリプトのコメントにこう書いてあった。 標準入力の全体を grep すると、設定ファイルを cat した中身に含まれる codex exec の文字列に誤ヒットして鳴ってしまう。

今回まさにそれをやっていた。 このセッションで codex は一度も回していない。 コマンドとしてはそのとおりだが、音としては codex 完了音が5回鳴っていた。 鳴らない音を調べに行って、鳴りすぎている音を見つけたことになる。

経緯は単純で、6月に修正版のスクリプトを作ったところで満足し、設定をそちらに切り替える作業が抜けていた。

差し替えの前に、スクリプトが python3 固定になっている点を先に潰させた。 この設定ファイルは2台で共有していて、Windows 機では python で呼ぶ必要がある。 そのうえで bash -x でトレースを取らせ、誤爆パターンでは再生コマンドまで到達せず、本物のパターンでは到達することを確認してから差し替えた。 差分は該当 hook の2行だけで、両パターンとも正常終了している。

今日の学び

  • 前回どこまで進んだかを外に残しておくと、毎日回すコマンドを毎日回さなくてよくなる。土日と市場の営業日がずれる以上、固定の「1日分」では取りこぼす
  • レート制限に当たったら、間隔を伸ばす前に別ソースを1つ試したほうが早い。回数を減らす設計に変えられることが多い
  • 自分の記憶と生成された記事が食い違ったら、記憶を捨てる前に裏を取らせる。今回は記憶が正しく、記事の配分が間違っていた
  • 本人が言ったことと、市場がそれをどう解釈したかは別に扱う。混ぜると原因の配分を誤る
  • 「動いていない」と「発火条件を満たしていない」は別物。hook の調査は、まず hook 機構自体が生きているかを別の hook の実行痕跡で確かめると早い
  • 調査中に「無関係」と判断したものは、中身と日時を見てから外す。今回はそこに本丸があった
  • 修正版のスクリプトを作っただけで満足せず、設定をそちらに向けるところまでやる。切り替え忘れは、動いているように見えるので気づけない