---
phase: 17-kintsugi-aion-repair
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - python/opendeviationbar/kintsugi/_helpers.py
  - python/opendeviationbar/kintsugi/discovery.py
  - python/opendeviationbar/kintsugi/verify.py
  - python/opendeviationbar/kintsugi/telemetry.py
autonomous: true
requirements: [KINT-02, KINT-03]

must_haves:
  truths:
    - "Kintsugi discovers Stathera anomalies for Aion symbols using per-symbol ouroboros_mode from registry"
    - "Verify_repair uses per-symbol ouroboros_mode and skips day-mode oracle for Aion symbols"
    - "Telemetry logs per-symbol ouroboros_mode (not global)"
    - "Dead-letter replay has zero ouroboros_mode references (mode-agnostic)"
    - "Today-guard in _repair() is conditional: enforced for Day, skipped for Aion"
  artifacts:
    - path: "python/opendeviationbar/kintsugi/_helpers.py"
      provides: "_get_mode_for_symbol() helper"
      contains: "_get_mode_for_symbol"
    - path: "python/opendeviationbar/kintsugi/discovery.py"
      provides: "Per-symbol ouroboros_mode in Stathera discovery"
      contains: "_get_mode_for_symbol"
    - path: "python/opendeviationbar/kintsugi/verify.py"
      provides: "Per-symbol mode in verification"
      contains: "_get_mode_for_symbol"
    - path: "python/opendeviationbar/kintsugi/telemetry.py"
      provides: "Per-symbol mode in kintsugi_log writes"
      contains: "_get_mode_for_symbol"
  key_links:
    - from: "python/opendeviationbar/kintsugi/_helpers.py"
      to: "python/opendeviationbar/symbol_registry.py"
      via: "get_symbol_entries() lookup"
      pattern: "get_symbol_entries"
    - from: "python/opendeviationbar/kintsugi/discovery.py"
      to: "python/opendeviationbar/kintsugi/_helpers.py"
      via: "_get_mode_for_symbol import"
      pattern: "_get_mode_for_symbol"
---

<objective>
Wire per-symbol ouroboros_mode lookup throughout kintsugi's discovery, verification, and telemetry modules, replacing all `get_operational_ouroboros_mode()` calls. Add today-guard conditional for Aion symbols in repair.py.

Purpose: Foundation for Aion repair -- kintsugi must discover Aion shards using the correct ouroboros_mode filter and verify them correctly. Without per-symbol mode, Stathera queries filter by the wrong mode and find zero Aion shards.

Output: All 6 call sites in kintsugi use per-symbol mode from registry. Dead-letter paths confirmed mode-agnostic (KINT-03).
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/16-sidecar-aion-startup/16-02-SUMMARY.md

@python/opendeviationbar/kintsugi/\_helpers.py
@python/opendeviationbar/kintsugi/discovery.py
@python/opendeviationbar/kintsugi/verify.py
@python/opendeviationbar/kintsugi/telemetry.py
@python/opendeviationbar/kintsugi/dead_letter.py
@python/opendeviationbar/kintsugi/repair.py
@python/opendeviationbar/ouroboros.py
@python/opendeviationbar/symbol_registry.py
@python/opendeviationbar/clickhouse/stathera_query.py

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

From python/opendeviationbar/symbol_registry.py:

```python
class SymbolEntry:
    ouroboros_mode: OuroborosMode = OuroborosMode.DAY
    # ... other fields

def get_symbol_entries() -> types.MappingProxyType[str, SymbolEntry]:
    """Immutable mapping of symbol name to SymbolEntry."""
```

From python/opendeviationbar/ouroboros.py:

```python
class OuroborosMode(str, Enum):
    DAY = "day"
    AION = "aion"

def get_operational_ouroboros_mode() -> Literal["day", "aion"]:
    """Global mode from Settings -- being REPLACED by per-symbol lookup."""
```

From python/opendeviationbar/clickhouse/stathera_query.py:

```python
def per_pair_filter() -> str:
    """WHERE clause with {ouroboros:String} parameter placeholder."""
```

</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add _get_mode_for_symbol helper + wire discovery, verify, telemetry</name>
  <files>
    python/opendeviationbar/kintsugi/_helpers.py
    python/opendeviationbar/kintsugi/discovery.py
    python/opendeviationbar/kintsugi/verify.py
    python/opendeviationbar/kintsugi/telemetry.py
  </files>
  <read_first>
    python/opendeviationbar/kintsugi/_helpers.py
    python/opendeviationbar/kintsugi/discovery.py
    python/opendeviationbar/kintsugi/verify.py
    python/opendeviationbar/kintsugi/telemetry.py
    python/opendeviationbar/ouroboros.py
    python/opendeviationbar/symbol_registry.py
    python/opendeviationbar/clickhouse/stathera_query.py
  </read_first>
  <action>
1. In `_helpers.py`, add a `_get_mode_for_symbol(symbol: str) -> str` function:
   ```python
   def _get_mode_for_symbol(symbol: str) -> str:
       """Get ouroboros_mode for a symbol from the registry.

       Returns "day" as safe default for unknown symbols.
       """
       from opendeviationbar.symbol_registry import get_symbol_entries
       entries = get_symbol_entries()
       entry = entries.get(symbol)
       if entry is None:
           return "day"
       return entry.ouroboros_mode.value

````
This uses the already-cached `@lru_cache` registry -- no performance concern.

2. In `discovery.py` -- `_discover_shards_stathera()` (line ~127):
- Remove `from opendeviationbar.ouroboros import get_operational_ouroboros_mode` and `ouroboros_mode = get_operational_ouroboros_mode()`
- Import `_get_mode_for_symbol` from `._helpers`
- Inside the `for sym, thr in valid_pairs:` loop, call `sym_mode = _get_mode_for_symbol(sym)` and pass it as the `"ouroboros"` parameter to the Stathera query (replacing the global `ouroboros_mode`)

3. In `discovery.py` -- `_enrich_shards()` (line ~634):
- Remove `from opendeviationbar.ouroboros import get_operational_ouroboros_mode`
- Import `_get_mode_for_symbol` from `._helpers`
- Inside the `for shard in shards:` loop, call `shard_mode = _get_mode_for_symbol(shard.symbol)` and pass it to `get_ouroboros_boundaries(start_date, end_date, shard_mode)` instead of `get_operational_ouroboros_mode()`

4. In `verify.py` -- `verify_repair()` (line ~32):
- Remove `from opendeviationbar.ouroboros import get_operational_ouroboros_mode` and `ouroboros_mode = get_operational_ouroboros_mode()`
- Import `_get_mode_for_symbol` from `._helpers`
- Set `ouroboros_mode = _get_mode_for_symbol(shard.symbol)`
- The rest of the function (Stathera query with `{ouroboros:String}` parameter, day-mode oracle check) works unchanged because it already branches on `if ouroboros_mode == "day"` for the oracle -- Aion symbols will skip the day-mode oracle check automatically.

5. In `telemetry.py` -- `_log_repair()` (line ~140):
- Remove `from opendeviationbar.ouroboros import get_operational_ouroboros_mode`
- Import `_get_mode_for_symbol` from `._helpers`
- Change `"ouroboros_mode": get_operational_ouroboros_mode()` to `"ouroboros_mode": _get_mode_for_symbol(result.shard.symbol)`

6. Verify `dead_letter.py` has ZERO references to `ouroboros_mode` or `get_operational_ouroboros_mode` (it should -- confirming KINT-03 is already satisfied).
</action>
<verify>
 <automated>cd /Users/terryli/eon/opendeviationbar-py && grep -c 'get_operational_ouroboros_mode' python/opendeviationbar/kintsugi/discovery.py python/opendeviationbar/kintsugi/verify.py python/opendeviationbar/kintsugi/telemetry.py python/opendeviationbar/kintsugi/repair.py | grep -v ':0' | head -5</automated>
</verify>
<acceptance_criteria>
 - grep '_get_mode_for_symbol' python/opendeviationbar/kintsugi/_helpers.py returns a match
 - grep '_get_mode_for_symbol' python/opendeviationbar/kintsugi/discovery.py returns matches (2 call sites: _discover_shards_stathera and _enrich_shards)
 - grep '_get_mode_for_symbol' python/opendeviationbar/kintsugi/verify.py returns a match
 - grep '_get_mode_for_symbol' python/opendeviationbar/kintsugi/telemetry.py returns a match
 - grep 'get_operational_ouroboros_mode' python/opendeviationbar/kintsugi/discovery.py returns 0 matches
 - grep 'get_operational_ouroboros_mode' python/opendeviationbar/kintsugi/verify.py returns 0 matches
 - grep 'get_operational_ouroboros_mode' python/opendeviationbar/kintsugi/telemetry.py returns 0 matches
 - grep 'ouroboros_mode' python/opendeviationbar/kintsugi/dead_letter.py returns 0 matches (KINT-03)
</acceptance_criteria>
<done>
 All kintsugi modules (discovery, verify, telemetry) use per-symbol ouroboros_mode from registry via _get_mode_for_symbol(). No global get_operational_ouroboros_mode() calls remain in discovery.py, verify.py, or telemetry.py. Dead-letter paths confirmed mode-agnostic with zero ouroboros_mode references.
</done>
</task>

<task type="auto">
<name>Task 2: Conditional today-guard for Aion in repair.py</name>
<files>
 python/opendeviationbar/kintsugi/repair.py
</files>
<read_first>
 python/opendeviationbar/kintsugi/repair.py
 python/opendeviationbar/kintsugi/_helpers.py
</read_first>
<action>
In `repair.py` -- `_repair()` function (line ~152):

1. Remove `from opendeviationbar.ouroboros import get_operational_ouroboros_mode` and `ouroboros_mode = get_operational_ouroboros_mode()` (line ~180-182).

2. Import `_get_mode_for_symbol` from `._helpers` (add to existing import line ~16).

3. Set `ouroboros_mode = _get_mode_for_symbol(shard.symbol)` after the anchor_last_tid guard.

4. Make the today-guard conditional on ouroboros_mode (KINT-02). Replace the existing today-guard block (lines ~193-210) with:

```python
# Guard (#280): Writer ownership boundary.
# Day-mode: ABSOLUTE today-guard -- sidecar owns today, kintsugi owns yesterday+.
# Aion-mode: No today-guard -- TID-based ownership. Sidecar's committed_floors
# dedup in live_engine.rs prevents duplicate bars. ReplacingMergeTree is L2 safety.
if ouroboros_mode == "day":
 today_utc = datetime.now(tz=UTC).date()
 if gap_end_date >= today_utc:
     gap_end_date = today_utc - timedelta(days=1)
     if gap_end_date < gap_start_date:
         logger.info(
             "kintsugi day-recompute: DEFERRING %s@%d -- gap touches today "
             "(%s), sidecar owns today (Asclepius). Will retry tomorrow.",
             shard.symbol,
             shard.threshold_decimal_bps,
             today_utc,
         )
         return -1  # sentinel: deferred, not clean -- must NOT be dismissed
````

This preserves the existing Day-mode today-guard exactly, and skips it entirely for Aion symbols. The sidecar's committed_floors dedup (L8 defense layer) and ReplacingMergeTree (L2) prevent any issues from concurrent writes.

1. No other changes to repair.py in this task -- the actual Aion repair strategy (\_repair_aion) is Plan 02.
   </action>

  <verify>
    <automated>cd /Users/terryli/eon/opendeviationbar-py && grep -c 'get_operational_ouroboros_mode' python/opendeviationbar/kintsugi/repair.py && grep -c '_get_mode_for_symbol' python/opendeviationbar/kintsugi/repair.py && grep 'ouroboros_mode == "day"' python/opendeviationbar/kintsugi/repair.py</automated>
  </verify>
  <acceptance_criteria>
    - grep 'get_operational_ouroboros_mode' python/opendeviationbar/kintsugi/repair.py returns 0 matches
    - grep '_get_mode_for_symbol' python/opendeviationbar/kintsugi/repair.py returns at least 1 match
    - grep 'ouroboros_mode == "day"' python/opendeviationbar/kintsugi/repair.py returns a match (conditional today-guard)
    - The today-guard block is wrapped in `if ouroboros_mode == "day":` so Aion symbols skip it
  </acceptance_criteria>
  <done>
    repair.py uses per-symbol ouroboros_mode. Today-guard is conditional: enforced for Day (unchanged behavior), skipped for Aion (KINT-02). No global get_operational_ouroboros_mode() calls remain in any kintsugi module.
  </done>
</task>

</tasks>

<verification>
After both tasks:
1. `grep -r 'get_operational_ouroboros_mode' python/opendeviationbar/kintsugi/` should return 0 matches
2. `grep -r '_get_mode_for_symbol' python/opendeviationbar/kintsugi/` should show matches in _helpers.py (definition), discovery.py (2), verify.py (1), telemetry.py (1), repair.py (1)
3. `grep 'ouroboros_mode' python/opendeviationbar/kintsugi/dead_letter.py` should return 0 matches (KINT-03)
4. `uv run --python 3.13 python -c "from opendeviationbar.kintsugi._helpers import _get_mode_for_symbol; print(_get_mode_for_symbol('BTCUSDT'))"` should print "aion"
</verification>

<success_criteria>

- Zero `get_operational_ouroboros_mode()` calls in the kintsugi package
- Per-symbol mode lookup via `_get_mode_for_symbol()` at all 6 call sites
- Today-guard conditional on mode (Day=enforced, Aion=skipped)
- Dead-letter replay confirmed mode-agnostic (no ouroboros_mode references)
  </success_criteria>

<output>
After completion, create `.planning/phases/17-kintsugi-aion-repair/17-01-SUMMARY.md`
</output>
