開発claude-code-tools

朝7時のタスクスケジューラが、また空振りしていた。PCがスリープに入ったまま朝を迎えると、楽天証券のポートフォリオを Google Sheets に流し込む update-portfolio.mjs がそのまま無音で一日サボる。気付くのはたいてい昼前で、シートを開いて前日の日付がそのまま残っているのを見て舌打ちする、というのを繰り返していた。

タスクスケジューラを直すより、毎朝必ず叩いている /make-diary の末尾にぶら下げてしまうほうが早い、という結論に行き着いたので、その作業ログを残す。

やりたかったこと

  • 毎朝7時の Windows タスクスケジューラで update-portfolio.mjs(楽天証券の父・母ポートフォリオを Stooq とみんかぶ投信から取得して Google Sheets に upsert)を回している
  • ただし PC がスリープしていれば実行されない。連続性が切れる
  • /make-diary は毎朝必ず手動で叩くので、その末尾にチェーン実行を仕込めば「忘れた日の保険」になる
  • ただし両方走ると Google Sheets を二重に書きにいくのは避けたい

やったこと

Step 1. タスクスケジューラを特定する

まず「朝7時に何が動いているか」が手元で曖昧だったので、Claude Code に schtasks //query を叩かせてタスクを特定してもらった。PowerShell の実行ポリシーで弾かれた回と、Git Bash のパス変換で /queryC:/Program Files/Git/query に化けた回で2回つまずいたあと、Shift-JIS の出力を UTF-8 に変換させてようやく該当タスクの .cmd パスが出てきた。

中身は node update-portfolio.mjs 一発の素朴な起動スクリプトで、Python だと思い込んでいた箇所が Node.js だと判明した、というおまけ付き。

Step 2. ユーザーレベルのスラッシュコマンドに切り出す

/make-diary は既に ~/.claude/commands/ 配下の check-earnings.md 等を Step 9〜11.5 でチェーン参照している。同じ流儀で ~/.claude/commands/update-portfolio.md を1枚足し、/make-diary の末尾に「Step 12. /update-portfolio をチェーン実行」を追記する形に揃えてもらった。

これで /make-diary から見ると、自プロジェクト外のスクリプトでも他のチェーンと同じ顔をして並ぶ。

Step 3. 二重実行をどう避けるか

ここがいちばん悩んだ。タスクスケジューラ側を消すとスリープ問題が直接解決するが、保険として「朝起きていれば7時に動く」レーンも残しておきたい。両方残すと、朝7時に走ったあと10時頃に /make-diary がもう一度走らせて、Google Sheets を二重に書きにいく。

選んだ方針は 「実行側に当日スキップを組み込む」:

  • update-portfolio.mjs 自身に --skip-if-today-success フラグを足す
  • 成功時に .last-success-date という日付だけ書いたファイルを残す
  • フラグ付きで叩かれた場合、当日の成功記録があれば1行ログを吐いて exit 0 する
  • フラグなしで叩けば従来通り強制実行

これでタスクスケジューラ側のショートカットは無印(強制実行)、/make-diary から呼ぶ側だけ --skip-if-today-success を付ける形にすれば、どちらが先に走っても二重書き込みは起きない。

判定ロジックの核心はこれだけ:

if (args.includes('--skip-if-today-success')) {
  const last = readLastSuccessDate() // .last-success-date を読む
  const today = nowJst().toISOString().slice(0, 10)
  if (last === today) {
    console.log(`[skip] already succeeded today (${today})`)
    process.exit(0)
  }
}
// 通常の取得・upsert 処理に進む

nowJst() は元からあった JST 換算ヘルパーをそのまま流用。.last-success-date は git 管理に入れたくないので .gitignore に追記してもらった。

Step 4. 当日分は手動で回す

設計を入れ込んだ今日の段階では、朝7時のタスクスケジューラはスリープで空振りしていた。明日からは /make-diary 経由で勝手に補完される。今日分だけは --skip-if-today-success を外して手動で1回叩いてもらい、Google Sheets の更新と .last-success-date の初期化をまとめて済ませた。

学びメモ

  • タスクスケジューラの空振りは、別レーンで叩く運用で塞ぐほうがコストが低い。Wake on LAN や復帰後の遅延起動を仕込むより、自分が毎朝必ず触る /make-diary にぶら下げるほうが、忘却にも強い
  • 二重実行ガードは「呼び出し側」ではなく「実行側」に置くほうが楽。今回は update-portfolio.mjs に1ファイル増やすだけで済んだ。/make-diary 側に if 文を書こうとすると、フラグや日付の取り回しが他のチェーン Step にも波及する
  • .last-success-date 方式は素朴だが効く。タイムゾーン跨ぎの曖昧さは nowJst() で吸収して、ファイルには YYYY-MM-DD 文字列だけ書く。読み比べが1行で終わる
  • schtasks //query の Git Bash 越し起動でハマる/query がパス変換されるので //query で叩く、出力は Shift-JIS なので iconv -f sjis で UTF-8 に直す、の2点を覚えておく

明日確認すること

  • 翌朝、タスクスケジューラ→/make-diary の順で走ったときに、/make-diary 側で [skip] already succeeded today が1行だけ出て exit 0 しているか
  • スリープで7時の側が落ちた日に、/make-diary 側が代わりに Google Sheets を更新できているか
  • .last-success-date の日付が、JST 基準で当日になっているか(UTC ずれが起きていないか)