← 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