| name | babysit-stale-watchdog |
| description | Detect enabled babysit crons whose referenced PR is MERGED or CLOSED and disable them. Companion to the in-script `is_pr_terminal()` check in babysit.py. Without this watchdog, babysit crons can run hundreds of polls for weeks after the PR they were guarding has merged or closed. |
| trigger | Run every 30 min via launchd. Also manually via `python3 ~/.hermes/scripts/babysit_stale_watchdog.py`. |
🚨 IF YOU ARE READING THIS BECAUSE THE USER ASKED WHY A THREAD IS FULL OF CRON SPAM, JUMP TO references/echo-loop-stale-babysit-recovery.md FIRST 🚨
The recovery recipe there is the single-call cleanup. Do not read the rest of this SKILL.md until the babysit is cancelled and the thread is quiet. Companion skill for the loop half: gateway-loop-standdown (read its top-of-file warning before posting any reply in that noisy thread).
babysit-stale-watchdog
Why
Bug-ref: 2026-07-03 — babysit-wa-2403-PR7711 fired 251 polls over 11 days after PR #7711 merged. The original babysit.py only recognized "PR created" (a worker output event) as terminal, not "PR MERGED on GitHub" (an external event). The cron kept spamming Slack with "TERMINAL: merged" pings until Jeffrey noticed.
What
Two-layer fix:
-
In-script (babysit.py is_pr_terminal()): poll() now extracts any PR ref from the task_summary, calls gh pr view --json state, and if the PR is MERGED or CLOSED, posts one terminal message and exits the babysit loop. 19/19 tests pass.
-
Watchdog (babysit_stale_watchdog.py): belt-and-suspenders. Runs every 30 min via launchd. Even if babysit.py is broken or running against an old prompt, the watchdog catches the stale job and disables it within 30 min. 9/9 tests pass.
Install
cp ~/.hermes/launchd/ai.hermes.schedule.babysit-stale-watchdog.plist.template \
~/Library/LaunchAgents/ai.hermes.schedule.babysit-stale-watchdog.plist
plutil -lint ~/Library/LaunchAgents/ai.hermes.schedule.babysit-stale-watchdog.plist
launchctl load -w ~/Library/LaunchAgents/ai.hermes.schedule.babysit-stale-watchdog.plist
launchctl kickstart -k gui/$(id -u)/ai.hermes.schedule.babysit-stale-watchdog
launchctl list | grep babysit-stale-watchdog
tail -5 ~/.hermes/cron/output/babysit-stale-watchdog.log
Verify
python3 ~/.hermes/scripts/babysit_stale_watchdog.py
python3 ~/.hermes/scripts/tests/test_babysit_stale_watchdog.py
python3 ~/.hermes/skills/ao-babysit/scripts/test_babysit_pr_exit.py
Files
~/.hermes/scripts/babysit_stale_watchdog.py — the watchdog
~/.hermes/scripts/tests/test_babysit_stale_watchdog.py — 9 tests
~/.hermes/skills/ao-babysit/scripts/babysit.py — restored + patched with is_pr_terminal()
~/.hermes/skills/ao-babysit/scripts/test_babysit_pr_exit.py — 19 tests
~/.hermes/launchd/ai.hermes.schedule.babysit-stale-watchdog.plist.template — committed plist template
Pitfalls
-
babysit.py was deleted in commit 4a7befcfa4 (squash-merge of feat/hermes-agent-default into main) — only the .pyc survived. Every cron prompt that referenced python3 ~/.hermes/skills/ao-babysit/scripts/babysit.py poll ... was failing silently. Restored from git show 4a7befcfa4^:skills/ao-babysit/scripts/babysit.py plus the new merged-PR check.
-
Default repo for bare PR #NNNN refs: defaults to $GITHUB_REPOSITORY since that's the most common babysit target (5 of 5 in the 2026-07-03 sweep). If babysits for other repos get created, the URL form https://github.com/o/r/pull/N is exact — prefer that.
-
Parked-handoff end-state (worker never spawned) — see references/worker-not-spawned-parked-handoff.md for the three-way check (session + PR + operator reply) and the verbatim cancel recipe. Verified 2026-07-22 on cron 5a771c731157 (thread C0AH3RY3DK6/p1784721612.183329); this is the third end-state the watchdog catches, alongside PR-MERGED and PR-CLOSED.
-
gh failures fall through to "not terminal" — if gh is down or rate-limited, the watchdog leaves the job enabled rather than disabling it. The in-script is_pr_terminal() has the same fallback.
-
cronjob CLI may not exist on PATH (verified 2026-07-31, thread C09GRLXF9GR/p1784235989.925899): the recovery recipe in references/echo-loop-stale-babysit-recovery.md assumes cronjob action=remove job_id=<id> works, but cronjob returned command not found from the gateway session. ~/.hermes/scripts/cronjob* files absent, hermes_tools Python module not importable. When the recovery recipe's cronjob invocation fails, fall back in this order: (a) launchctl bootout gui/$(id -u)/ai.hermes.schedule.cronjob-<name> if the babysit runs under a named launchd plist; (b) look for a cronjob dispatcher under ~/.local/bin/, ~/.hermes/bin/, or ~/bin/ — many Hermes installs put it there rather than on PATH; (c) ask the user to type stop reminder <babysit-name> in-thread (the babysit prints this hint itself); (d) treat the babysit as an external system you cannot cancel and focus on the closeout post instead. Do NOT loop on "tool not found" — pick one fallback, post the closeout, and stop.
Companion pattern — no-agent hermes cron PR watchdog
For PRs that don't need an AO worker babysit, use hermes cron create --no-agent --script <watch-script>.sh to run a bash polling script on a fixed cadence. The script exits silently except on state transitions (OPEN with failures / OPEN with all-green / MERGED / CLOSED). Zero LLM tokens burned, instant state-transition pings to a Slack thread. See references/no-agent-hermes-cron-pr-watchdog.md for the full recipe plus the verified worked example (PR 8466 cron 124ad03896f5, 2026-07-20).
The script writes MERGED/CLOSED to its state file and exits 0; the launchd babysit-stale-watchdog cron disables the stale cron within 30 min via this skill.
First-time investigation recipe (when you arrive at a thread that's already full of babysit noise)
If a user surfaces a thread where a babysit cron has been spamming rate-limit errors for days/weeks (the cron is stale, the PR is already MERGED/CLOSED, but nobody canceled the cron), use references/stale-babysit-investigation-recipe.md for the 5-step investigate → cancel → verify recipe. Verified 2026-07-31 (cron 124ad03896f5, ~40 noise posts/day for 7 days).