Skip to content

feat(notify): ループの節目を macOS に通知する loop-notify を追加する - #1

Open
tomoyahiroe wants to merge 1 commit into
mainfrom
feat/loop-notify
Open

feat(notify): ループの節目を macOS に通知する loop-notify を追加する#1
tomoyahiroe wants to merge 1 commit into
mainfrom
feat/loop-notify

Conversation

@tomoyahiroe

Copy link
Copy Markdown
Owner

背景

ループは 2 時間おきに黙って回る。人間がそれを知る手段は /loop-status を叩くか loops/STATE.md を開くかで、どちらも人間から見に行く必要がある。押し出しの経路が 1 本も無かった。

参考実装(akashic-recorder)で実害が出た。Verifier が PR の本物の欠陥を見つけて request-changes 相当の判定を出したのに 11 日間放置され、その間ループは 63 回発火して dispatch 0 件だった。さらに単一アカウント運用では GitHub がセルフレビューを拒むため、approve 相当も request-changes も人間から見える結果は同じ needs-human ラベルになる。その PR は欠陥のまま merge された。

変更

.loop/bin/loop-notify を追加。opt-in、macOS 専用。

.loop/bin/loop-notify --install     # 登録(= 有効化)
.loop/bin/loop-notify --test        # 配線の確認
.loop/bin/loop-notify --status      # 今どうなっているか
.loop/bin/loop-notify --uninstall   # 解除

firing / dispatch- には一切手を入れていない。*

設計上の要点

  • 信号源は loops/STATE.md## Budget 節。 record_state が dispatch の最後にだけ追記し、maker/verifier/fixer/gate/自動 merge の節目がすべてここを通る。loops/runs/*-verifier-pr-N.mdagent-run --log が非追記 tee なので Verifier の開始時に作られる。「ファイルができた = 終わった」にならず信号として使えない
  • ホストの LaunchAgent が実行する。 コンテナからは通知センターに届かない。plist は WatchPathsStartInterval の両方を持つ(コンテナ内の書き込みがホストの FSEvents に必ず伝わる保証がないため、速さと確実さを別の仕掛けで担保する)
  • 夜間は鳴らさない(既定 06:00-23:30)。窓の外では通知位置を進めずに持ち越し、次に窓へ入った最初の実行でまとめて出す。位置を進めると夜間ぶんが永久に消えるので、この順番が要点。持ち越しが max_burst(既定 5)を超えたら 1 通にまとめる
  • 有効/無効のスイッチを config に置かない。 --install が有効化そのもので、真実は 1 つ。enabled = true なのに未登録という静かな食い違いを作らない
  • 判定の読み取りはヒューリスティック。 Verifier の判定は自由文で書式が揃っていない(**判定: approve 相当** / ## 検証結果: request-changes)。取り違え得る前提で、検証ログの 1 行目を通知本文に載せて人間が気づけるようにしている

設計の全体は docs/superpowers/specs/2026-08-25-notify-design.md

テスト

.loop/tests/loop-notify.bats に 13 件。時刻(LOOP_NOTIFY_NOW)と通知先(LOOP_NOTIFIER_BIN)を環境変数で差し替える(実時刻に依存させると 23:35 に走らせたテストだけ落ちる)。

loop-doctor に項目が 1 つ増えたため loop-doctor.bats の SKIP 件数を 4 → 5 に更新。

全 384 件緑。

未解決(別 PR)

  • events.mjs の KINDS に verify を足し、dispatch-verifier が判定を構造化して記録すれば、消費側が散文を読まずに済む
  • 通知は「起きたこと」しか出せない。「10 日間 dispatch 0 件」のような起きていないことの検知は別の仕組みが要る

🤖 Generated with Claude Code

https://claude.ai/code/session_01AbLUmrArzNbRtaUJFqmEZY

ループは 2 時間おきに黙って回り、人間が知る手段は /loop-status か
STATE.md を開くかで、どちらも人間から見に行く必要がある。押し出しの
経路が 1 本も無かった。

参考実装で実害が出た。Verifier が PR の本物の欠陥を見つけて
request-changes 相当の判定を出したのに 11 日放置され、その間ループは
63 回発火して dispatch 0 件だった。単一アカウント運用では GitHub が
セルフレビューを拒むため approve 相当も request-changes も同じ
needs-human ラベルになり、ラベルだけでは合否が区別できない。
その PR は欠陥のまま merge された。

信号源は loops/STATE.md の "## Budget" 節。record_state が dispatch の
最後にだけ追記し、maker/verifier/fixer/gate/自動 merge の節目がすべて
ここを通る。loops/runs/*-verifier-pr-N.md は agent-run の非追記 tee が
Verifier の開始時に作るので「ファイルができた = 終わった」にならず、
信号として使えない。

通知はホストの LaunchAgent が出す(コンテナからは通知センターに
届かない)。plist は WatchPaths と StartInterval の両方を持つ。
コンテナ内の書き込みがホストの FSEvents に必ず伝わる保証がないため、
速さと確実さを別々の仕掛けで担保する。

夜間は鳴らさない(既定 06:00-23:30)。窓の外では通知位置を進めずに
持ち越し、次に窓へ入った最初の実行でまとめて出す。位置を進めると
夜間ぶんが永久に消えるため、この順番が要点。持ち越しが max_burst を
超えたら 1 通にまとめる。

有効/無効のスイッチは config に置かない。--install(LaunchAgent の
登録)が有効化そのもので、真実は 1 つ。enabled = true なのに未登録、
という静かな食い違いを作らないため。loop-doctor は登録状態を必ず出す
(opt-in なので未登録は NG ではなく SKIP)。

Verifier の判定は自由文で書式が揃っていないため読み取りは
ヒューリスティック。取り違え得る前提で、検証ログの 1 行目を通知本文に
載せて人間が気づけるようにしている。

テストは時刻と通知先を環境変数で差し替える(実時刻に依存させると
23:35 に走らせたテストだけ落ちる)。全 384 件緑。

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AbLUmrArzNbRtaUJFqmEZY
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant