← back to Beverlyhillsvideos
social/poster/token-health-run.sh
104 lines
#!/bin/zsh
# BHV posting watchdog: (1) token health, (2) recent live-post failures, (3) SILENT
# missed-slot detection — if the 6h cadence stopped firing (Mac2 asleep/launchd dead),
# no error line is ever written, so we detect it by post.log going stale.
#
# TK-11656 (TK-11431 class) — this watcher WAS invisible on the 7am fleet-health panel
# THREE ways, all fixed here:
# (1) WRONG LOCATION. It wrote ONLY social/poster/data/latest.json. fleet-health-rollup
# globs ~/.claude/skills/*/data/latest.json, so that file is NEVER read. Fixed by
# ALSO publishing a bridge heartbeat under ~/.claude/skills/bhv-ig-token-watchdog/.
# (2) UNRECOGNIZED VOCABULARY. It emitted state='ALERT', a word the rollup's
# GOOD/WARNY/BADY regexes all reject -> null -> row stayed PASS (a dead token read
# GREEN). Fixed by emitting a top-level verdict AND status in the PASS/WARN/FAIL
# vocabulary the rollup actually reads.
# (3) UNVERIFIED ALERT DELIVERY. It alerted via a hand-rolled `curl -s ... || true`,
# which exits 0 on HTTP 4xx/5xx and left no receipt. Fixed by posting through
# ~/.claude/skills/_shared/cncp_post.sh, which asserts a 2xx and writes a delivery
# receipt the rollup maps to WARN on a failed arm.
cd "$(dirname "$0")/../.." || exit 1
LOG=social/poster/post.log
mkdir -p social/poster/data
# --- TESTABILITY SEAM (TK-11431 amendment 3) --------------------------------------------
# test-negative.sh injects a fault via these env vars to prove the watchdog goes RED. The
# launchd plist NEVER sets them, so a scheduled run always measures the real token.
# BHV_WATCHDOG_TEST_OUT — override the token-health.mjs output line (e.g. "DEAD: ...")
# BHV_WATCHDOG_TEST_RC — override its exit code
# BHV_WATCHDOG_TEST_FAILS — override the recent-POST-FAILED count
# BHV_WATCHDOG_TEST_STALE — override hours-since-last-run
# BHV_WATCHDOG_BRIDGE_DIR — override where the bridge heartbeat is written (test scratch)
# BHV_WATCHDOG_NO_ALERT=1 — skip the CNCP post (tests must not spam the real board)
BRIDGE_DIR="${BHV_WATCHDOG_BRIDGE_DIR:-$HOME/.claude/skills/bhv-ig-token-watchdog}"
if [[ -n "$BHV_WATCHDOG_TEST_OUT" ]]; then
OUT="$BHV_WATCHDOG_TEST_OUT"; RC="${BHV_WATCHDOG_TEST_RC:-1}"
else
OUT=$(/opt/homebrew/bin/node social/poster/token-health.mjs 2>&1); RC=$?
fi
FAILS="${BHV_WATCHDOG_TEST_FAILS:-$(tail -60 "$LOG" 2>/dev/null | grep -c "POST FAILED")}"
# Age (hours) of the newest scheduled run header ("----- <ts> · slot=... -----").
if [[ -n "$BHV_WATCHDOG_TEST_STALE" ]]; then
STALE_H="$BHV_WATCHDOG_TEST_STALE"
else
STALE_H=$(/opt/homebrew/bin/node -e '
try{
const fs=require("fs");
const lines=fs.readFileSync("social/poster/post.log","utf8").trim().split("\n").filter(l=>l.startsWith("----- "));
if(!lines.length){console.log(999);process.exit(0);}
const m=lines[lines.length-1].match(/----- (.+?) (?:PDT|PST|[A-Z]{2,4}) ·/);
const t=m?Date.parse(m[1].replace(" ","T")):NaN;
console.log(isNaN(t)?999:((Date.now()-t)/3.6e6).toFixed(1));
}catch{console.log(999);}
' 2>/dev/null)
fi
# 6h cadence + grace: >8h since the last run means a slot was silently missed.
MISSED=$(/opt/homebrew/bin/node -e "console.log(parseFloat('${STALE_H:-999}')>8?1:0)" 2>/dev/null)
# --- Classify the TOKEN state from token-health.mjs output ------------------------------
# token-health.mjs prints one of: "HEALTHY: ...", "EXPIRING: ...", "DEAD: ...". If node
# crashed before printing one of those, we did NOT actually measure the token.
case "$OUT" in
DEAD*) TOKEN="DEAD" ;;
EXPIRING*) TOKEN="EXPIRING" ;;
HEALTHY*) TOKEN="HEALTHY" ;;
*) TOKEN="UNKNOWN" ;; # unmeasured (node missing/crashed/unparseable)
esac
# --- Map to the fleet-health-rollup PASS/WARN/FAIL vocabulary ---------------------------
# FAIL — token DEAD (posting is dead), or live POST FAILED lines (posting is broken).
# WARN — token EXPIRING soon, a cadence slot silently missed, OR UNKNOWN (an unmeasured
# input is NEVER PASS — CLAUDE.md TK-11431 rule 1).
# PASS — token alive + no recent failures + cadence current.
VERDICT="PASS"
if [[ "$TOKEN" == "DEAD" || "$FAILS" -gt 0 ]]; then
VERDICT="FAIL"
elif [[ "$TOKEN" == "EXPIRING" || "$TOKEN" == "UNKNOWN" || "$MISSED" -eq 1 ]]; then
VERDICT="WARN"
fi
TS="$(date -u +%FT%TZ)"
DETAIL="token=[$OUT] recentFailures=$FAILS hoursSinceLastRun=${STALE_H} missedSlot=$MISSED"
# --- (1)(2) Publish heartbeats: bridge (globbed by the rollup) + legacy (unchanged path) ---
write_latest() { # $1 = dir
mkdir -p "$1/data"
printf '{"ts":"%s","verdict":"%s","status":"%s","token":"%s","recentFailures":%s,"hoursSinceLastRun":%s,"missedSlot":%s,"detail":"%s"}\n' \
"$TS" "$VERDICT" "$VERDICT" "$TOKEN" "${FAILS:-0}" "${STALE_H:-999}" "${MISSED:-0}" \
"$(printf '%s' "$DETAIL" | sed 's/"/\\"/g')" > "$1/data/latest.json"
}
write_latest "$BRIDGE_DIR" # ~/.claude/skills/bhv-ig-token-watchdog — the row the rollup reads
write_latest "social/poster" # legacy heartbeat kept for anything still reading it
# --- (3) Alert through the shared sender (asserts 2xx + writes a delivery receipt) -------
if [[ "$VERDICT" != "PASS" && "$BHV_WATCHDOG_NO_ALERT" != "1" ]]; then
MSG="BHV IG watchdog [$VERDICT]: $DETAIL"
# SKILL points the receipt at the bridge row so a failed arm surfaces on THAT skill's line.
SKILL="$BRIDGE_DIR" bash -c '
. "$HOME/.claude/skills/_shared/cncp_post.sh" 2>/dev/null || exit 0
cncp_post "alert://beverlyhillsvideos-ig-token" "$1"
' _ "$MSG" || true
echo "$(date) $VERDICT: $MSG" >> social/poster/watchdog.log
fi