---
phase: 03-heartbeat-telemetry-overhaul
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - scripts/heartbeat/checks/stathera.py
  - scripts/heartbeat/formatter.py
  - scripts/heartbeat/config.py
  - scripts/heartbeat/telegram.py
  - tests/test_heartbeat_stathera.py
autonomous: true
requirements: [TELEM-01, TELEM-04]

must_haves:
  truths:
    - "Heartbeat reports one unified Stathera continuity result per (symbol, threshold) -- no intra-day vs midnight-boundary classification"
    - "If a new gap appears in today's bars, a LOUD persistent Telegram alert fires within 5 minutes with exact (symbol, threshold, tid_delta, trade ID range)"
    - "Existing health gating logic still works -- kintsugi_healing suppresses alerts when healing is active"
  artifacts:
    - path: "scripts/heartbeat/checks/stathera.py"
      provides: "Unified per-symbol Stathera check + today-gap LOUD alert"
      contains: "collect_stathera_unified"
    - path: "scripts/heartbeat/formatter.py"
      provides: "Unified Stathera section in Telegram message"
      contains: "per-symbol gap/overlap"
    - path: "tests/test_heartbeat_stathera.py"
      provides: "Test coverage for unified Stathera and today-gap alert"
  key_links:
    - from: "scripts/heartbeat/checks/stathera.py"
      to: "scripts/heartbeat/telegram.py"
      via: "LOUD alert call for today's new gaps"
      pattern: "send_telegram.*silent.*False"
    - from: "scripts/heartbeat/checks/__init__.py"
      to: "scripts/heartbeat/checks/stathera.py"
      via: "collect_stathera call in run_checks()"
      pattern: "collect_stathera"
---

<objective>
Unify the Stathera heartbeat check into a single per-(symbol, threshold) result and add LOUD Telegram alerts when new gaps appear in today's bars.

Purpose: TELEM-01 eliminates the confusing intra-day/adjacent-day/backfill gap classification -- a gap is a gap regardless of where it occurs. TELEM-04 provides immediate alerting when the zero-gap guarantee is violated today, enabling rapid diagnosis without SSH.

Output: Refactored stathera.py with per-symbol unified check, today-gap LOUD alert via telegram.py, updated formatter.py for per-symbol Stathera display, and test coverage.
</objective>

<execution_context>
@~/.claude/get-shit-done/workflows/execute-plan.md
@~/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/REQUIREMENTS.md
@.planning/phases/03-heartbeat-telemetry-overhaul/03-CONTEXT.md

<interfaces>
<!-- Key types and contracts the executor needs -->

From scripts/heartbeat/checks/stathera.py (current):

- `collect_stathera(client, results)` -- main entry point called from checks/**init**.py
- Uses `FULL_TABLE`, `cross_pair_filter()`, `cross_pair_window()` from `opendeviationbar.clickhouse.stathera_query`
- Sets result keys: `stathera_overlaps` (bool), `stathera_gaps` (bool), `stathera_overlap_count` (str), `stathera_gap_count` (str), `stathera_intra_day_gaps` (str), `stathera_adjacent_day_gaps` (str), `stathera_overlap_delta` (str), `stathera_gap_delta` (str), `stathera_overlap_pairs` (str), `stathera_gap_pairs` (str), `stathera_gap_fingerprints` (list), `stathera_all_gaps_known` (str), `cross_midnight_bars` (str), `cross_midnight` (bool)

From scripts/heartbeat/config.py:

- `HEALTH_GATE_KEYS` -- frozenset of result keys that gate healthy/unhealthy
- Contains: "stathera_overlaps", "stathera_gaps", "cross_midnight"
- `KNOWN_GAP_FINGERPRINTS` -- dict mapping (symbol, threshold, first_tid, prev_tid) to root cause info

From scripts/heartbeat/formatter.py:

- `format_message(results, healthy)` -- Stathera section at line ~219 uses intra_day_gaps, adjacent_day_gaps, backfill_gaps classification

From scripts/heartbeat/telegram.py:

- `send_telegram(token, chat_id, message, *, silent: bool) -> int` -- send with silent=False for LOUD

From scripts/heartbeat/state.py:

- `load_stathera_counts() -> dict` / `save_stathera_counts(overlap_count, gap_count)` for delta tracking

From scripts/heartbeat/checks/**init**.py:

- `collect_stathera(client, results)` called at line ~167 inside `run_checks()`
  </interfaces>

</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Unify Stathera check to per-symbol results and add today-gap LOUD alert</name>
  <read_first>
    - scripts/heartbeat/checks/stathera.py (full file -- this is the primary file being rewritten)
    - scripts/heartbeat/checks/__init__.py (lines 165-170 -- how collect_stathera is called)
    - scripts/heartbeat/config.py (HEALTH_GATE_KEYS, KNOWN_GAP_FINGERPRINTS)
    - scripts/heartbeat/state.py (stathera state persistence)
    - scripts/heartbeat/telegram.py (send_telegram signature)
    - python/opendeviationbar/clickhouse/stathera_query.py (SQL helpers: FULL_TABLE, cross_pair_filter, cross_pair_window, enabled_symbols_sql)
  </read_first>
  <files>
    scripts/heartbeat/checks/stathera.py,
    scripts/heartbeat/config.py,
    scripts/heartbeat/telegram.py,
    tests/test_heartbeat_stathera.py
  </files>
  <behavior>
    - Test 1: collect_stathera with zero gaps/overlaps sets `stathera_per_symbol` to empty dict, `stathera_gaps` to True, `stathera_overlaps` to True
    - Test 2: collect_stathera with gaps in one symbol sets `stathera_per_symbol` dict with that symbol's gap count, `stathera_gaps` to False
    - Test 3: collect_stathera with today-only new gap (gap_delta > 0, today's bars) calls send_telegram with silent=False and message containing symbol, threshold, tid_delta, trade ID range
    - Test 4: collect_stathera with today's new gap but kintsugi_healing=True does NOT send LOUD alert (kintsugi will fix)
    - Test 5: LOUD alert message contains exact (symbol, threshold, tid_delta, first_agg_trade_id, prev_last_tid)
  </behavior>
  <action>
    Rewrite `collect_stathera()` in `scripts/heartbeat/checks/stathera.py`:

    1. **Unified per-symbol query**: Replace the current aggregate query with a per-(symbol, threshold) breakdown. Use a single ClickHouse query:
       ```sql
       SELECT symbol, threshold_decimal_bps,
              countIf(tid_delta < 0) AS overlaps,
              countIf(tid_delta > 0) AS gaps
       FROM (
           SELECT symbol, threshold_decimal_bps,
                  first_agg_trade_id - lagInFrame(last_agg_trade_id, 1) OVER w - 1 AS tid_delta,
                  row_number() OVER w AS rn
           FROM {FULL_TABLE} FINAL
           {cross_pair_filter()}
           {cross_pair_window()}
       )
       WHERE rn > 1 AND tid_delta != 0
       GROUP BY symbol, threshold_decimal_bps
       ```
       Store results in `results["stathera_per_symbol"]` as a dict: `{"BTCUSDT@250": {"gaps": 3, "overlaps": 0}, ...}`.

    2. **Remove intra-day vs adjacent-day classification**: Delete the `intra_day_gaps`, `adjacent_day_gaps`, `backfill_gaps` distinction entirely. A gap is a gap (TELEM-01). Remove `stathera_intra_day_gaps` and `stathera_adjacent_day_gaps` result keys.

    3. **Keep aggregate totals for backward compat**: Still compute and set `stathera_overlap_count`, `stathera_gap_count`, `stathera_overlap_delta`, `stathera_gap_delta` from the per-symbol data. Keep delta tracking via `load_stathera_counts()`/`save_stathera_counts()`.

    4. **Keep existing health gating logic**: `stathera_gaps` and `stathera_overlaps` booleans still set based on: no anomalies=True, growing+not_healing=False, else kintsugi_healing. Keep `HEALTH_GATE_KEYS` entries for `stathera_gaps`, `stathera_overlaps`, `cross_midnight`.

    5. **Keep top-5 pairs, fingerprints, cross-midnight oracle**: These sections remain unchanged.

    6. **TELEM-04 -- Today's gap LOUD alert**: Add a SECOND query specifically for today's bars:
       ```sql
       SELECT symbol, threshold_decimal_bps,
              lagInFrame(last_agg_trade_id, 1) OVER w AS prev_last_tid,
              first_agg_trade_id,
              first_agg_trade_id - lagInFrame(last_agg_trade_id, 1) OVER w - 1 AS tid_delta,
              row_number() OVER w AS rn
       FROM {FULL_TABLE} FINAL
       WHERE toDate(open_time_ms / 1000, 'UTC') = today('UTC')
         AND {VALID_TID_FILTER} {enabled_symbols_sql()}
       WINDOW w AS (PARTITION BY symbol, threshold_decimal_bps, ouroboros_mode
                    ORDER BY first_agg_trade_id, close_time_ms)
       ```
       Filter outer query: `WHERE tid_delta > 0 AND rn > 1`.

       If any today gaps found AND `gap_delta > 0` (new gaps since last heartbeat) AND NOT kintsugi_healing:
       - Build LOUD alert message with HTML formatting:
         ```
         <b>NEW STATHERA GAP IN TODAY'S BARS</b>

         {for each gap:}
         {symbol}@{threshold}: tid_delta={tid_delta}
           prev_last_tid={prev_last_tid} -> first_agg_trade_id={first_agg_trade_id}
         ```
       - Call `send_telegram(token, chat_id, message, silent=False)` directly.
       - Token and chat_id: read from `os.environ` (same pattern as `__main__.py`).
       - Set `results["stathera_today_gap_alert"] = True` so formatter can note it.

    7. **Update config.py**: No changes to `HEALTH_GATE_KEYS` needed (stathera_gaps already in set). Remove any references to intra_day/adjacent_day if present.

    8. **Tests**: Create `tests/test_heartbeat_stathera.py` that mocks the ClickHouse client and verifies:
       - Unified per-symbol results are populated correctly
       - No intra_day_gaps or adjacent_day_gaps keys in results
       - LOUD alert fires when today has new gaps and kintsugi is not healing
       - LOUD alert does NOT fire when kintsugi is healing
       - LOUD alert contains exact gap details
       Use `unittest.mock.patch` for ClickHouse client and `send_telegram`.

  </action>
  <verify>
    <automated>cd /Users/terryli/eon/opendeviationbar-py && uv run --python 3.13 pytest tests/test_heartbeat_stathera.py -xvs 2>&1 | tail -30</automated>
  </verify>
  <acceptance_criteria>
    - grep -c "intra_day_gaps\|adjacent_day_gaps" scripts/heartbeat/checks/stathera.py returns 0 (classification removed)
    - grep -c "stathera_per_symbol" scripts/heartbeat/checks/stathera.py returns >= 1 (unified per-symbol results)
    - grep -c "send_telegram" scripts/heartbeat/checks/stathera.py returns >= 1 (LOUD alert wired)
    - grep -c "today" scripts/heartbeat/checks/stathera.py returns >= 2 (today-gap query exists)
    - tests/test_heartbeat_stathera.py exists and has >= 4 test functions
    - pytest tests/test_heartbeat_stathera.py passes
  </acceptance_criteria>
  <done>
    - collect_stathera produces unified per-symbol results with no intra-day/adjacent-day split
    - stathera_per_symbol dict in results maps "SYMBOL@THRESHOLD" to {gaps, overlaps}
    - Today's new gaps trigger LOUD Telegram alert with (symbol, threshold, tid_delta, trade ID range)
    - Alert suppressed when kintsugi is actively healing
    - All aggregate totals (stathera_gap_count, stathera_overlap_count, deltas) still computed
    - Test file covers unified output, LOUD alert firing, and alert suppression
  </done>
</task>

<task type="auto">
  <name>Task 2: Update formatter for unified per-symbol Stathera display</name>
  <read_first>
    - scripts/heartbeat/formatter.py (full file -- Stathera section at line ~219)
    - scripts/heartbeat/checks/stathera.py (after Task 1 -- to understand new result keys)
  </read_first>
  <files>
    scripts/heartbeat/formatter.py,
    scripts/heartbeat/checks/__init__.py
  </files>
  <action>
    1. **Update Stathera section in formatter.py** (line ~219-299): Replace the intra-day/adjacent-day/backfill classification display with unified per-symbol output.

       New Stathera section format when anomalies exist:
       ```
       <b>Stathera</b>
         {icon} {total_overlap_count} overlaps, {total_gap_count} gaps {delta_str}
         Per-symbol:
           BTCUSDT@250: 0 gaps, 0 overlaps
           BTCUSDT@500: 2 gaps, 0 overlaps
           ETHUSDT@250: 0 gaps, 1 overlap
         ...top 5 shown, rest summarized as "+ N more pairs"
       ```

       When clean:
       ```
       <b>Stathera</b>
         checkmark bar-level continuity: clean
       ```

       Read `results["stathera_per_symbol"]` (dict from Task 1). Sort by: gaps DESC, then overlaps DESC. Show top 5 pairs with issues. If > 5 pairs have issues, add "+ N more pairs with anomalies".

       If `results.get("stathera_today_gap_alert")` is True, add a line:
       ```
         rotating_light LOUD alert sent for today's new gaps
       ```

    2. **Remove all references to intra_day_gaps, adjacent_day_gaps, backfill gaps** from formatter.py. Delete the three separate gap category blocks (lines ~264-276 in current code).

    3. **Keep fingerprint display**: The `KNOWN_GAP_FINGERPRINTS` detailed diagnostic block (lines ~279-299) stays -- it's per-gap detail, not a classification.

    4. **No changes to checks/__init__.py needed** -- `collect_stathera(client, results)` call signature is unchanged. Just verify the import still works after Task 1's refactor.

  </action>
  <verify>
    <automated>cd /Users/terryli/eon/opendeviationbar-py && python3 -c "from heartbeat.formatter import format_message; print('import ok')" 2>&1 && grep -c "intra_day_gaps\|adjacent_day_gaps\|backfill_gaps" scripts/heartbeat/formatter.py</automated>
  </verify>
  <acceptance_criteria>
    - grep -c "intra_day_gaps\|adjacent_day_gaps\|backfill_gaps" scripts/heartbeat/formatter.py returns 0
    - grep -c "stathera_per_symbol" scripts/heartbeat/formatter.py returns >= 1
    - grep -c "stathera_today_gap_alert" scripts/heartbeat/formatter.py returns >= 1
    - The Stathera section in format_message reads from stathera_per_symbol dict
    - formatter.py imports still work (no broken references to removed result keys)
  </acceptance_criteria>
  <done>
    - Formatter displays unified per-symbol Stathera results (no intra-day/adjacent-day/backfill categories)
    - Top 5 pairs with anomalies shown, rest summarized
    - LOUD alert indicator displayed when stathera_today_gap_alert is True
    - Fingerprint detail blocks preserved
    - No broken imports or references to removed keys
  </done>
</task>

</tasks>

<verification>
1. `uv run --python 3.13 pytest tests/test_heartbeat_stathera.py -xvs` -- all tests pass
2. `grep -c "intra_day_gaps\|adjacent_day_gaps" scripts/heartbeat/checks/stathera.py scripts/heartbeat/formatter.py` -- returns 0 for both files
3. `grep -c "stathera_per_symbol" scripts/heartbeat/checks/stathera.py scripts/heartbeat/formatter.py` -- returns >= 1 for both files
4. `python3 -c "from heartbeat.checks.stathera import collect_stathera; from heartbeat.formatter import format_message; print('OK')"` -- imports work
</verification>

<success_criteria>

- Unified per-symbol Stathera check with no gap classification (TELEM-01)
- LOUD Telegram alert on new today gaps with exact details (TELEM-04)
- Alert suppressed when kintsugi is healing
- Formatter displays per-symbol results cleanly
- Test coverage for core logic
  </success_criteria>

<output>
After completion, create `.planning/phases/03-heartbeat-telemetry-overhaul/03-01-SUMMARY.md`
</output>
