# Regime Observer Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. Spec reference: `docs/superpowers/specs/2026-05-31-regime-observer-design.md` (v3).

**Goal:** Implement the unified regime_observer state machine that closes all 3 confirmed failure modes from alpha-rl-zwj68 — Kelly trade-stream-death (Theorem 1), v9 eval-boundary preservation (Theorem 2), and popart blindness (Theorem 6).

**Architecture:** New CUDA kernel `rl_regime_observer.cu` emits 5+ shared regime signals via ISV slots; consumed by Kelly (resurrection override), v9 (slot rename), popart (per-account max envelope floor), and IQN τ (tail-recency boost). Single-thread single-block kernel runs once per step between LobSim and risk-stack. Two helper additions: `rl_regime_flat_count.cu` (batch tree-reduce) and `warp_reduce_max` (popart kernel extension).

**Tech Stack:** Rust 1.85, cudarc 0.19, CUDA 12.4, mapped-pinned ISV bus, single-stream graph capture compatible. NVIDIA-grade kernels per `feedback_nvidia_grade_perf_for_kernels`: no atomics, no nvrtc, no host branches in capture.

**Branch:** new branch `ml-alpha-regime-observer` from current `ml-alpha-adaptive-controller-floors` head (commit `baf971ba5`).

**Phases & parallelism:**
- F1 (foundation): land first, ~1 day, zero behavior change
- F2/F3/F4/F5: parallel after F1, each ~0.5–1 day
- F6 (validation): after F1-F5 all landed, ~2h wall + 1h GPU

---

## Pre-implementation setup

### Task 0: Branch & worktree

**Files:** —

- [ ] **Step 0.1: Create new branch**

```bash
cd /home/jgrusewski/Work/foxhunt
git checkout ml-alpha-adaptive-controller-floors
git pull origin ml-alpha-adaptive-controller-floors
git checkout -b ml-alpha-regime-observer
git push -u origin ml-alpha-regime-observer
```

- [ ] **Step 0.2: Verify baseline compile + smoke**

```bash
SQLX_OFFLINE=true cargo check -p ml-alpha 2>&1 | tail -5
SQLX_OFFLINE=true FOXHUNT_TEST_DATA=test_data/futures-baseline cargo test -p ml-alpha --test integrated_trainer_smoke --release -- --ignored --nocapture 2>&1 | tail -10
```

Expected: clean check; `integrated_trainer_step_with_lobsim_runs_without_panic ... ok`

---

# Phase F1 — Foundation (zero behavior change)

Goal: regime_observer kernel emits signals; NO consumer reads them yet. Validates wiring before consumers depend on it (per `feedback_smoke_validation_before_structural_priors`).

### Task F1.1: Add ISV slot constants

**Files:** Modify `crates/ml-alpha/src/rl/isv_slots.rs`

- [ ] **Step 1.1.1: Read isv_slots.rs to find insertion point**

```bash
grep -n "RL_SLOTS_END\|pub const RL_EVAL_WARMUP\|pub const RL_KELLY_EPS" crates/ml-alpha/src/rl/isv_slots.rs
```

Insertion point: after the v9 block (slots 685-695, ending around line ~1507). Current `RL_SLOTS_END = 696`.

- [ ] **Step 1.1.2: Rename 3 v9 constants (no physical move)**

Find each and rename:
- `RL_EVAL_WARMUP_REMAINING_INDEX` → `RL_REGIME_TRANSITION_REMAINING_INDEX` (slot 685, value unchanged)
- `RL_EVAL_WARMUP_STEPS_CONFIG_INDEX` → `RL_REGIME_TRANSITION_STEPS_CONFIG_INDEX` (slot 686)
- `RL_EVAL_WARMUP_DECAY_STEPS_CONFIG_INDEX` → `RL_REGIME_TRANSITION_DECAY_STEPS_CONFIG_INDEX` (slot 687)

The 8 v9 defensive/normal slots (688-695) stay v9-owned with unchanged names.

- [ ] **Step 1.1.3: Add 20 new ISV slot constants after slot 695**

```rust
// ──────────────────────────────────────────────────────────────────
// Regime observer (spec: 2026-05-31-regime-observer-design.md v3)
// Unified state-machine signals for risk-stack controllers.
// ──────────────────────────────────────────────────────────────────

// regime_observer outputs (6 surface signals + 4 internal Welford state)
pub const RL_REGIME_DEAD_ZONE_FLAG_INDEX:           usize = 696;
pub const RL_REGIME_DEAD_ZONE_DURATION_INDEX:       usize = 697;
pub const RL_REGIME_DEAD_ZONE_TIMEOUT_FLAG_INDEX:   usize = 698;
pub const RL_REGIME_RECOVERY_FACTOR_INDEX:          usize = 699;
pub const RL_REGIME_SESSION_PNL_VARIANCE_EMA_INDEX: usize = 700;
pub const RL_REGIME_TAIL_EVENT_RECENCY_INDEX:       usize = 701;
pub const RL_REGIME_SESSION_PNL_VAR_M2_INDEX:       usize = 702;
pub const RL_REGIME_SESSION_PNL_VAR_MEAN_INDEX:     usize = 703;
pub const RL_REGIME_SESSION_PNL_VAR_COUNT_INDEX:    usize = 704;
pub const RL_REGIME_PREV_WORST_PNL_INDEX:           usize = 705;

// Kelly resurrection (consumed by Kelly kernel in F2)
pub const RL_KELLY_EPS_RECOVERY_LIVE_INDEX:         usize = 706;
pub const RL_KELLY_EPS_RECOVERY_MIN_INDEX:          usize = 707;
pub const RL_KELLY_EPS_RECOVERY_MAX_INDEX:          usize = 708;
pub const RL_KELLY_EPS_RECOVERY_N_RECOVERY_INDEX:   usize = 709;

// Safety net
pub const RL_REGIME_DEAD_ZONE_MAX_DURATION_INDEX:   usize = 710;

// IQN τ tail-boost (consumed in F5)
pub const RL_IQN_TAU_TAIL_BOOST_FACTOR_INDEX:       usize = 711;
pub const RL_IQN_TAU_TAIL_BOOST_N_WINDOW_INDEX:     usize = 712;

// regime_observer config
pub const RL_REGIME_TAIL_SIGMA_THRESHOLD_INDEX:     usize = 713;

// Popart per-account max envelope (consumed by popart in F4)
pub const RL_POPART_MAX_ABS_REWARD_EMA_INDEX:       usize = 714;
pub const RL_POPART_MAX_DECAY_ALPHA_INDEX:          usize = 715;
```

And update `RL_SLOTS_END`:

```rust
pub const RL_SLOTS_END: usize = 716;
```

- [ ] **Step 1.1.4: Verify compile**

```bash
SQLX_OFFLINE=true cargo check -p ml-alpha 2>&1 | tail -10
```

Expected: clean. Some downstream sites will reference the renamed v9 constants; those updates land in F3.

- [ ] **Step 1.1.5: Commit**

```bash
git add crates/ml-alpha/src/rl/isv_slots.rs
git commit -m "$(cat <<'EOF'
feat(rl): regime_observer ISV slot allocation (F1.1)

20 new slots (696-715) + 3 v9 slot renames (685-687).
Per spec docs/superpowers/specs/2026-05-31-regime-observer-design.md v3.

Slot map:
- 696-705: regime_observer outputs + internal Welford state
- 706-709: Kelly resurrection config + live value
- 710: dead-zone timeout config
- 711-712: IQN τ tail-boost config
- 713: tail σ threshold
- 714-715: popart max envelope (F4)

v9 renames are zero-move (same physical addresses):
- 685 RL_EVAL_WARMUP_REMAINING → RL_REGIME_TRANSITION_REMAINING
- 686 RL_EVAL_WARMUP_STEPS_CONFIG → RL_REGIME_TRANSITION_STEPS_CONFIG
- 687 RL_EVAL_WARMUP_DECAY_STEPS_CONFIG → RL_REGIME_TRANSITION_DECAY_STEPS_CONFIG

RL_SLOTS_END: 696 → 716
EOF
)"
```

---

### Task F1.2: Implement rl_regime_flat_count.cu

**Files:** Create `crates/ml-alpha/cuda/rl_regime_flat_count.cu`

- [ ] **Step 1.2.1: Write kernel**

Block-reduce lots[b] → flat_count_d (single int). Spec lines 331-353.

```c
// Tree-reduce flat count from per-account lots buffer.
// Per feedback_no_atomicadd: shared-mem tree-reduce only, no atomics.

#include <stdint.h>

extern "C" __global__ void rl_regime_flat_count(
    const int* __restrict__ lots,
    int* __restrict__ flat_count_out,
    int b_size
) {
    extern __shared__ int s_count[];
    int tid = threadIdx.x;
    int sum = 0;
    for (int b = tid; b < b_size; b += blockDim.x) {
        sum += (lots[b] == 0) ? 1 : 0;
    }
    s_count[tid] = sum;
    __syncthreads();
    for (int s = blockDim.x / 2; s > 0; s >>= 1) {
        if (tid < s) s_count[tid] += s_count[tid + s];
        __syncthreads();
    }
    if (tid == 0) flat_count_out[0] = s_count[0];
}
```

- [ ] **Step 1.2.2: Register in build.rs**

Find the cubin list in `crates/ml-alpha/build.rs` (alphabetical around the other `rl_regime*` or `rl_*` entries) and add `"rl_regime_flat_count"`.

- [ ] **Step 1.2.3: Compile-verify**

```bash
SQLX_OFFLINE=true cargo build -p ml-alpha --profile dev-release 2>&1 | tail -5
ls -la target/dev-release/build/ml-alpha-*/out/rl_regime_flat_count.cubin
```

Expected: cubin exists, ~3-5 KB.

- [ ] **Step 1.2.4: Commit**

```bash
git add crates/ml-alpha/cuda/rl_regime_flat_count.cu crates/ml-alpha/build.rs
git commit -m "feat(rl): rl_regime_flat_count kernel (F1.2)"
```

---

### Task F1.3: Implement rl_regime_observer.cu

**Files:** Create `crates/ml-alpha/cuda/rl_regime_observer.cu`

- [ ] **Step 1.3.1: Write kernel**

Single-thread single-block. Spec lines 228-307. Header `#define`s reference slot numbers explicitly (kernel can't include Rust constants):

```c
// rl_regime_observer.cu — unified state-machine for risk-stack controllers.
//
// Spec: docs/superpowers/specs/2026-05-31-regime-observer-design.md v3
// Pearls:
//   - [[pearl_kelly_trade_stream_death]] — empirical motivation
//   - [[pearl_dead_signal_resurrection_discipline]] — meta-principle
//
// Per feedback_no_atomicadd: single-thread single-block, no atomics.
// Per feedback_cpu_is_read_only: pure device kernel.
// Per feedback_nvidia_grade_perf_for_kernels: no host branches in capture.

#include <stdint.h>

// ─── ISV slot indices (mirror Rust constants from isv_slots.rs) ─────
#define RL_KELLY_FRACTION_INDEX                676
#define RL_COOLDOWN_REMAINING_STEPS_INDEX      668  // adjust per actual slot
#define RL_SESSION_PNL_WORST_INDEX             658  // adjust per actual slot

#define RL_REGIME_DEAD_ZONE_FLAG_INDEX           696
#define RL_REGIME_DEAD_ZONE_DURATION_INDEX       697
#define RL_REGIME_DEAD_ZONE_TIMEOUT_FLAG_INDEX   698
#define RL_REGIME_RECOVERY_FACTOR_INDEX          699
#define RL_REGIME_SESSION_PNL_VARIANCE_EMA_INDEX 700
#define RL_REGIME_TAIL_EVENT_RECENCY_INDEX       701
#define RL_REGIME_SESSION_PNL_VAR_M2_INDEX       702
#define RL_REGIME_SESSION_PNL_VAR_MEAN_INDEX     703
#define RL_REGIME_SESSION_PNL_VAR_COUNT_INDEX    704
#define RL_REGIME_PREV_WORST_PNL_INDEX           705
#define RL_KELLY_EPS_RECOVERY_LIVE_INDEX         706
#define RL_KELLY_EPS_RECOVERY_MIN_INDEX          707
#define RL_KELLY_EPS_RECOVERY_MAX_INDEX          708
#define RL_KELLY_EPS_RECOVERY_N_RECOVERY_INDEX   709
#define RL_REGIME_DEAD_ZONE_MAX_DURATION_INDEX   710
#define RL_REGIME_TAIL_SIGMA_THRESHOLD_INDEX     713

extern "C" __global__ void rl_regime_observer(
    float* __restrict__ isv,
    const int* __restrict__ flat_count_d,
    int b_size
) {
    if (threadIdx.x != 0 || blockIdx.x != 0) return;

    // Phase 1: dead-zone detection (composite kelly==0 ∧ all-flat ∧ no-cooldown)
    const float kelly        = isv[RL_KELLY_FRACTION_INDEX];
    const float cooldown     = isv[RL_COOLDOWN_REMAINING_STEPS_INDEX];
    const int   flat_count   = flat_count_d[0];

    const int dead_zone = (kelly == 0.0f)
                       && (flat_count == b_size)
                       && (cooldown == 0.0f) ? 1 : 0;
    isv[RL_REGIME_DEAD_ZONE_FLAG_INDEX] = (float)dead_zone;

    float duration = isv[RL_REGIME_DEAD_ZONE_DURATION_INDEX];
    if (dead_zone) duration += 1.0f; else duration = 0.0f;
    isv[RL_REGIME_DEAD_ZONE_DURATION_INDEX] = duration;

    const float max_dur = isv[RL_REGIME_DEAD_ZONE_MAX_DURATION_INDEX];
    const int timeout = (duration > max_dur) ? 1 : 0;
    isv[RL_REGIME_DEAD_ZONE_TIMEOUT_FLAG_INDEX] = (float)timeout;

    // Phase 2: session_pnl_change Welford variance
    const float worst_now  = isv[RL_SESSION_PNL_WORST_INDEX];
    const float worst_prev = isv[RL_REGIME_PREV_WORST_PNL_INDEX];
    const float dx         = worst_now - worst_prev;
    isv[RL_REGIME_PREV_WORST_PNL_INDEX] = worst_now;

    float mean   = isv[RL_REGIME_SESSION_PNL_VAR_MEAN_INDEX];
    float m2     = isv[RL_REGIME_SESSION_PNL_VAR_M2_INDEX];
    float count  = isv[RL_REGIME_SESSION_PNL_VAR_COUNT_INDEX];

    if (worst_now != 0.0f || worst_prev != 0.0f) {
        count += 1.0f;
        const float delta  = dx - mean;
        mean += delta / count;
        const float delta2 = dx - mean;
        m2 += delta * delta2;
        isv[RL_REGIME_SESSION_PNL_VAR_MEAN_INDEX]  = mean;
        isv[RL_REGIME_SESSION_PNL_VAR_M2_INDEX]    = m2;
        isv[RL_REGIME_SESSION_PNL_VAR_COUNT_INDEX] = count;
        const float variance = (count > 1.0f) ? (m2 / (count - 1.0f)) : 0.0f;
        isv[RL_REGIME_SESSION_PNL_VARIANCE_EMA_INDEX] = variance;
    }

    // Phase 3: tail-event detection (3σ threshold, gated by count >= 10)
    const float var       = isv[RL_REGIME_SESSION_PNL_VARIANCE_EMA_INDEX];
    const float sigma_thr = isv[RL_REGIME_TAIL_SIGMA_THRESHOLD_INDEX];
    const float sigma     = sqrtf(var);

    const int sufficient_count = (count >= 10.0f) ? 1 : 0;
    const int is_tail = sufficient_count && (sigma > 0.0f) && (fabsf(dx) > sigma_thr * sigma);

    float recency = isv[RL_REGIME_TAIL_EVENT_RECENCY_INDEX];
    if (is_tail) recency = 0.0f; else recency += 1.0f;
    isv[RL_REGIME_TAIL_EVENT_RECENCY_INDEX] = recency;

    // Phase 4: emit recovery_factor and ε_recovery_live (drift-free for consumers + diag)
    const float n_recovery = isv[RL_KELLY_EPS_RECOVERY_N_RECOVERY_INDEX];
    const float eps_min    = isv[RL_KELLY_EPS_RECOVERY_MIN_INDEX];
    const float eps_max    = isv[RL_KELLY_EPS_RECOVERY_MAX_INDEX];

    const float recovery_factor = fminf(1.0f, recency / fmaxf(n_recovery, 1.0f));
    const float eps_live        = eps_min + (eps_max - eps_min) * recovery_factor;

    isv[RL_REGIME_RECOVERY_FACTOR_INDEX]  = recovery_factor;
    isv[RL_KELLY_EPS_RECOVERY_LIVE_INDEX] = eps_live;
}
```

**IMPORTANT**: verify `RL_COOLDOWN_REMAINING_STEPS_INDEX` and `RL_SESSION_PNL_WORST_INDEX` against actual values in `isv_slots.rs` before writing the kernel. The values shown above are placeholders.

```bash
grep -nE "RL_(COOLDOWN_REMAINING_STEPS|SESSION_PNL_WORST|KELLY_FRACTION)_INDEX:[[:space:]]*usize" crates/ml-alpha/src/rl/isv_slots.rs
```

- [ ] **Step 1.3.2: Register in build.rs**

Add `"rl_regime_observer"` to cubin list.

- [ ] **Step 1.3.3: Compile-verify**

```bash
SQLX_OFFLINE=true cargo build -p ml-alpha --profile dev-release 2>&1 | tail -5
ls -la target/dev-release/build/ml-alpha-*/out/rl_regime_observer.cubin
```

- [ ] **Step 1.3.4: Commit**

```bash
git add crates/ml-alpha/cuda/rl_regime_observer.cu crates/ml-alpha/build.rs
git commit -m "feat(rl): rl_regime_observer kernel (F1.3)"
```

---

### Task F1.4: Trainer kernel loaders + struct fields + launchers

**Files:** Modify `crates/ml-alpha/src/trainer/integrated.rs`

- [ ] **Step 1.4.1: Add cubin include_bytes! constants**

Find the existing cubin constants (around line 200-260) and add:

```rust
const RL_REGIME_FLAT_COUNT_CUBIN: &[u8] =
    include_bytes!(concat!(env!("OUT_DIR"), "/rl_regime_flat_count.cubin"));
const RL_REGIME_OBSERVER_CUBIN: &[u8] =
    include_bytes!(concat!(env!("OUT_DIR"), "/rl_regime_observer.cubin"));
```

- [ ] **Step 1.4.2: Add struct fields**

In `IntegratedTrainer` struct, after the existing v9 fields:

```rust
_rl_regime_flat_count_module: Arc<CudaModule>,
rl_regime_flat_count_fn: CudaFunction,
_rl_regime_observer_module:  Arc<CudaModule>,
rl_regime_observer_fn:        CudaFunction,
flat_count_d:                 CudaSlice<i32>,  // per-batch helper buffer (length 1)
```

- [ ] **Step 1.4.3: Add cubin loaders in new()**

After the v9 cubin loader block:

```rust
let rl_regime_flat_count_module = ctx
    .load_cubin(RL_REGIME_FLAT_COUNT_CUBIN.to_vec())
    .context("load rl_regime_flat_count cubin")?;
let rl_regime_flat_count_fn = rl_regime_flat_count_module
    .load_function("rl_regime_flat_count")
    .context("load rl_regime_flat_count function")?;

let rl_regime_observer_module = ctx
    .load_cubin(RL_REGIME_OBSERVER_CUBIN.to_vec())
    .context("load rl_regime_observer cubin")?;
let rl_regime_observer_fn = rl_regime_observer_module
    .load_function("rl_regime_observer")
    .context("load rl_regime_observer function")?;

let flat_count_d = stream.alloc_zeros::<i32>(1)
    .context("alloc flat_count_d")?;
```

- [ ] **Step 1.4.4: Assign struct fields in new()'s return value**

```rust
_rl_regime_flat_count_module: rl_regime_flat_count_module,
rl_regime_flat_count_fn,
_rl_regime_observer_module: rl_regime_observer_module,
rl_regime_observer_fn,
flat_count_d,
```

- [ ] **Step 1.4.5: Add launcher methods**

After existing risk-stack launchers, add:

```rust
pub fn launch_rl_regime_flat_count(&mut self, lots_d: &CudaSlice<i32>, b_size: usize) -> Result<()> {
    let mut args = RawArgs::new();
    args.push_ptr(lots_d.raw_ptr());
    args.push_ptr(self.flat_count_d.raw_ptr());
    args.push_i32(b_size as i32);
    let mut ptrs = args.build_arg_ptrs();
    let smem_bytes = (256 * std::mem::size_of::<i32>()) as u32;
    unsafe {
        raw_launch(
            self.rl_regime_flat_count_fn.cu_function(),
            (1, 1, 1), (256, 1, 1), smem_bytes,
            self.raw_stream,
            &mut ptrs[..args.len()],
        ).map_err(|e| anyhow::anyhow!("rl_regime_flat_count: {:?}", e))?;
    }
    Ok(())
}

pub fn launch_rl_regime_observer(&self, b_size: usize) -> Result<()> {
    let mut args = RawArgs::new();
    args.push_ptr(self.isv_dev_ptr);
    args.push_ptr(self.flat_count_d.raw_ptr());
    args.push_i32(b_size as i32);
    let mut ptrs = args.build_arg_ptrs();
    unsafe {
        raw_launch(
            self.rl_regime_observer_fn.cu_function(),
            (1, 1, 1), (1, 1, 1), 0,
            self.raw_stream,
            &mut ptrs[..args.len()],
        ).map_err(|e| anyhow::anyhow!("rl_regime_observer: {:?}", e))?;
    }
    Ok(())
}
```

- [ ] **Step 1.4.6: Compile-verify**

```bash
SQLX_OFFLINE=true cargo check -p ml-alpha 2>&1 | tail -10
```

- [ ] **Step 1.4.7: Commit**

```bash
git add crates/ml-alpha/src/trainer/integrated.rs
git commit -m "feat(rl): regime_observer trainer wiring — loaders + struct + launchers (F1.4)"
```

---

### Task F1.5: Wire into step_with_lobsim

**Files:** Modify `crates/ml-alpha/src/trainer/integrated.rs`

- [ ] **Step 1.5.1: Find launch position between LobSim and risk-stack**

Find where `launch_rl_cmdp_constraints_check` is called in `step_with_lobsim`. Insert regime_observer launch BEFORE the risk-stack controllers (specifically before CMDP).

- [ ] **Step 1.5.2: Add launches**

```rust
// Regime observer (spec 2026-05-31-regime-observer-design.md v3).
// Reads PRIOR-step kelly_f + CURRENT-step flat_count + worst_pnl + cooldown.
// Writes 6 surface signals (dead_zone_flag, duration, timeout, recovery_factor,
// session_pnl_var_ema, tail_event_recency) + internal Welford state +
// drift-free ε_recovery_live for Kelly resurrection (F2).
self.launch_rl_regime_flat_count(&self.lots_d, b_size)
    .context("launch_rl_regime_flat_count")?;
self.launch_rl_regime_observer(b_size)
    .context("launch_rl_regime_observer")?;
```

(verify `self.lots_d` is the correct name for the position lots buffer; adjust if needed)

- [ ] **Step 1.5.3: Compile-verify**

```bash
SQLX_OFFLINE=true cargo check -p ml-alpha 2>&1 | tail -10
```

- [ ] **Step 1.5.4: Commit**

```bash
git add crates/ml-alpha/src/trainer/integrated.rs
git commit -m "feat(rl): wire regime_observer + flat_count into step_with_lobsim (F1.5)"
```

---

### Task F1.6: Bootstrap entries

**Files:** Modify `crates/ml-alpha/src/trainer/integrated.rs`

- [ ] **Step 1.6.1: Find with_controllers_bootstrapped() and the isv_constants array**

```bash
grep -n "with_controllers_bootstrapped\|isv_constants:" crates/ml-alpha/src/trainer/integrated.rs
```

Currently `[(usize, f32); 179]` (per v9 commit).

- [ ] **Step 1.6.2: Add 20 new bootstrap entries**

Increase the array size to `[(usize, f32); 199]` (179 + 20). Append after the v9 block:

```rust
// regime_observer foundation (F1)
(crate::rl::isv_slots::RL_REGIME_DEAD_ZONE_FLAG_INDEX,           0.0),
(crate::rl::isv_slots::RL_REGIME_DEAD_ZONE_DURATION_INDEX,       0.0),
(crate::rl::isv_slots::RL_REGIME_DEAD_ZONE_TIMEOUT_FLAG_INDEX,   0.0),
(crate::rl::isv_slots::RL_REGIME_RECOVERY_FACTOR_INDEX,          1.0),     // start at max recovery
(crate::rl::isv_slots::RL_REGIME_SESSION_PNL_VARIANCE_EMA_INDEX, 0.0),
(crate::rl::isv_slots::RL_REGIME_TAIL_EVENT_RECENCY_INDEX,       1.0e6),   // sentinel "no tails seen"
(crate::rl::isv_slots::RL_REGIME_SESSION_PNL_VAR_M2_INDEX,       0.0),     // Welford
(crate::rl::isv_slots::RL_REGIME_SESSION_PNL_VAR_MEAN_INDEX,     0.0),     // Welford
(crate::rl::isv_slots::RL_REGIME_SESSION_PNL_VAR_COUNT_INDEX,    0.0),     // Welford
(crate::rl::isv_slots::RL_REGIME_PREV_WORST_PNL_INDEX,           0.0),     // sentinel
// Kelly resurrection config
(crate::rl::isv_slots::RL_KELLY_EPS_RECOVERY_LIVE_INDEX,         0.50),    // = ε_max
(crate::rl::isv_slots::RL_KELLY_EPS_RECOVERY_MIN_INDEX,          0.05),
(crate::rl::isv_slots::RL_KELLY_EPS_RECOVERY_MAX_INDEX,          0.50),
(crate::rl::isv_slots::RL_KELLY_EPS_RECOVERY_N_RECOVERY_INDEX,   100.0),
// Safety
(crate::rl::isv_slots::RL_REGIME_DEAD_ZONE_MAX_DURATION_INDEX,   1000.0),
// IQN τ tail boost config
(crate::rl::isv_slots::RL_IQN_TAU_TAIL_BOOST_FACTOR_INDEX,       1.5),
(crate::rl::isv_slots::RL_IQN_TAU_TAIL_BOOST_N_WINDOW_INDEX,     100.0),
// regime_observer config
(crate::rl::isv_slots::RL_REGIME_TAIL_SIGMA_THRESHOLD_INDEX,     3.0),
// F4 popart envelope (bootstrap only; kernel modification lands in F4)
(crate::rl::isv_slots::RL_POPART_MAX_ABS_REWARD_EMA_INDEX,       0.0),     // sentinel
(crate::rl::isv_slots::RL_POPART_MAX_DECAY_ALPHA_INDEX,          0.01),    // ~69-step half-life
```

- [ ] **Step 1.6.3: Compile-verify**

```bash
SQLX_OFFLINE=true cargo check -p ml-alpha 2>&1 | tail -10
```

- [ ] **Step 1.6.4: Commit**

```bash
git add crates/ml-alpha/src/trainer/integrated.rs
git commit -m "feat(rl): regime_observer bootstrap entries (F1.6) — array 179→199"
```

---

### Task F1.7: Extend reset_session_state with regime boundary policy

**Files:** Modify `crates/ml-alpha/src/trainer/integrated.rs`

- [ ] **Step 1.7.1: Find reset_session_state**

```bash
grep -n "fn reset_session_state" crates/ml-alpha/src/trainer/integrated.rs
```

- [ ] **Step 1.7.2: Add regime_observer slot resets**

Per spec section "Regime observer reset policy at fold/eval boundaries". Add inside reset_session_state, in the existing direct-ISV-write loop:

```rust
// Regime observer transient state (resets per regime boundary per
// [[pearl_adaptive_carryover_discipline]]).
(crate::rl::isv_slots::RL_REGIME_DEAD_ZONE_FLAG_INDEX,           0.0_f32),
(crate::rl::isv_slots::RL_REGIME_DEAD_ZONE_DURATION_INDEX,       0.0_f32),
(crate::rl::isv_slots::RL_REGIME_DEAD_ZONE_TIMEOUT_FLAG_INDEX,   0.0_f32),
(crate::rl::isv_slots::RL_REGIME_TAIL_EVENT_RECENCY_INDEX,       1.0e6_f32),  // sentinel
(crate::rl::isv_slots::RL_REGIME_RECOVERY_FACTOR_INDEX,          1.0_f32),
(crate::rl::isv_slots::RL_KELLY_EPS_RECOVERY_LIVE_INDEX,         0.50_f32),
// CRITICAL: PREV_WORST_PNL must be aligned with the just-reset session_pnl_worst
// (which is reset to 0 by the existing loop above) to prevent spurious tail event.
// Issue γ from v2 critical review.
(crate::rl::isv_slots::RL_REGIME_PREV_WORST_PNL_INDEX,           0.0_f32),
// F4 popart envelope (sentinel reset at boundary so new regime starts fresh).
(crate::rl::isv_slots::RL_POPART_MAX_ABS_REWARD_EMA_INDEX,       0.0_f32),
// Note: Welford state (M2, MEAN, COUNT) intentionally NOT reset — variance
// of session_pnl_change is a regime-invariant property.
```

- [ ] **Step 1.7.3: Compile + smoke**

```bash
SQLX_OFFLINE=true cargo check -p ml-alpha 2>&1 | tail -10
SQLX_OFFLINE=true FOXHUNT_TEST_DATA=test_data/futures-baseline cargo test -p ml-alpha --test integrated_trainer_smoke --release -- --ignored --nocapture 2>&1 | tail -10
```

Expected: smoke passes.

- [ ] **Step 1.7.4: Commit**

```bash
git add crates/ml-alpha/src/trainer/integrated.rs
git commit -m "feat(rl): reset_session_state regime_observer boundary policy (F1.7)"
```

---

### Task F1.8: Diag emit structure

**Files:** Modify `crates/ml-alpha/examples/alpha_rl_train.rs`

- [ ] **Step 1.8.1: Add ISV slot imports**

Add to the `use ml_alpha::rl::isv_slots::{...}` block:

```rust
RL_REGIME_DEAD_ZONE_FLAG_INDEX, RL_REGIME_DEAD_ZONE_DURATION_INDEX,
RL_REGIME_DEAD_ZONE_TIMEOUT_FLAG_INDEX, RL_REGIME_RECOVERY_FACTOR_INDEX,
RL_REGIME_SESSION_PNL_VARIANCE_EMA_INDEX, RL_REGIME_TAIL_EVENT_RECENCY_INDEX,
RL_REGIME_SESSION_PNL_VAR_COUNT_INDEX,
RL_REGIME_DEAD_ZONE_MAX_DURATION_INDEX, RL_REGIME_TAIL_SIGMA_THRESHOLD_INDEX,
RL_REGIME_TRANSITION_REMAINING_INDEX,    // existing slot, renamed
RL_REGIME_TRANSITION_STEPS_CONFIG_INDEX,
RL_REGIME_TRANSITION_DECAY_STEPS_CONFIG_INDEX,
RL_KELLY_EPS_RECOVERY_LIVE_INDEX, RL_KELLY_EPS_RECOVERY_MIN_INDEX,
RL_KELLY_EPS_RECOVERY_MAX_INDEX, RL_KELLY_EPS_RECOVERY_N_RECOVERY_INDEX,
RL_IQN_TAU_TAIL_BOOST_FACTOR_INDEX, RL_IQN_TAU_TAIL_BOOST_N_WINDOW_INDEX,
RL_POPART_MAX_ABS_REWARD_EMA_INDEX, RL_POPART_MAX_DECAY_ALPHA_INDEX,
```

- [ ] **Step 1.8.2: Add regime block to risk_stack diag**

Find the existing `"risk_stack": { ... }` JSON literal and append a new `"regime": {...}` block per spec section "Diag emit structure" (spec lines 748-790). Use direct `isv[...]` reads (no inline computation per drift-free principle).

- [ ] **Step 1.8.3: Compile + smoke + check JSONL**

```bash
SQLX_OFFLINE=true cargo check -p ml-alpha --examples 2>&1 | tail -10
SQLX_OFFLINE=true FOXHUNT_TEST_DATA=test_data/futures-baseline cargo test -p ml-alpha --test integrated_trainer_smoke --release -- --ignored --nocapture 2>&1 | tail -10
```

- [ ] **Step 1.8.4: Commit**

```bash
git add crates/ml-alpha/examples/alpha_rl_train.rs
git commit -m "feat(rl): regime_observer diag emit (F1.8)"
```

---

### Task F1.9: Local 1k smoke validation

**Files:** None (test only)

- [ ] **Step 1.9.1: Run integrated trainer smoke**

```bash
SQLX_OFFLINE=true FOXHUNT_TEST_DATA=test_data/futures-baseline cargo test -p ml-alpha --test integrated_trainer_smoke --release -- --ignored --nocapture 2>&1 | tail -10
```

Expected: PASS.

- [ ] **Step 1.9.2: Run all existing risk_stack invariants (G1-G14)**

```bash
SQLX_OFFLINE=true cargo test -p ml-alpha --release --test risk_stack_invariants -- --ignored --nocapture --test-threads=1 2>&1 | tail -20
```

Expected: 13/13 PASS (no regression).

- [ ] **Step 1.9.3: Push F1**

```bash
git push origin ml-alpha-regime-observer
```

F1 complete. Foundation in place, zero behavior change confirmed.

---

# Phase F2 — Kelly resurrection (Problem 1 fix)

Goal: Kelly reads DEAD_ZONE_FLAG and overrides with ε_recovery_live, respecting TIMEOUT_FLAG.

### Task F2.1: Audit + extend kelly fraction kernel

**Files:** Modify `crates/ml-alpha/cuda/rl_kelly_fraction_controller.cu`

- [ ] **Step 2.1.1: Audit per `feedback_extending_existing_code_audits_for_existing_violations`**

```bash
grep -E "TODO|FIXME|XXX|atomicAdd|#allow|_ =" crates/ml-alpha/cuda/rl_kelly_fraction_controller.cu
```

Expected: clean. If any violations, fix in a separate commit before F2.

- [ ] **Step 2.1.2: Add #define for new ISV slots**

```c
#define RL_REGIME_DEAD_ZONE_FLAG_INDEX         696
#define RL_REGIME_DEAD_ZONE_TIMEOUT_FLAG_INDEX  698
#define RL_KELLY_EPS_RECOVERY_LIVE_INDEX        706
```

- [ ] **Step 2.1.3: Append resurrection block at END of kernel (after final clamp + ISV write)**

```c
// Kelly resurrection (Theorem 1): override analytic kelly if DEAD_ZONE_FLAG fires.
// TIMEOUT_FLAG safety net: if dead-zone has exceeded MAX_DURATION, stop trying.
const int dead_zone = (int)isv[RL_REGIME_DEAD_ZONE_FLAG_INDEX];
const int timeout   = (int)isv[RL_REGIME_DEAD_ZONE_TIMEOUT_FLAG_INDEX];
if (dead_zone && !timeout) {
    isv[RL_KELLY_FRACTION_INDEX] = isv[RL_KELLY_EPS_RECOVERY_LIVE_INDEX];
}
```

- [ ] **Step 2.1.4: Compile**

```bash
SQLX_OFFLINE=true cargo build -p ml-alpha --profile dev-release 2>&1 | tail -5
```

- [ ] **Step 2.1.5: Commit**

```bash
git add crates/ml-alpha/cuda/rl_kelly_fraction_controller.cu
git commit -m "feat(rl): Kelly resurrection override (F2.1)"
```

---

### Task F2.2: Mirror in fused controllers

**Files:** Modify `crates/ml-alpha/cuda/rl_fused_controllers.cu`

- [ ] **Step 2.2.1: Find Layer-4 (Kelly) branch in fused kernel**

```bash
grep -n "Layer 4\|kelly_fraction\|RL_KELLY_FRACTION_INDEX" crates/ml-alpha/cuda/rl_fused_controllers.cu
```

- [ ] **Step 2.2.2: Append same resurrection block at end of Kelly branch**

Same 5-line snippet as Task F2.1.3.

- [ ] **Step 2.2.3: Compile + commit**

```bash
SQLX_OFFLINE=true cargo build -p ml-alpha --profile dev-release 2>&1 | tail -5
git add crates/ml-alpha/cuda/rl_fused_controllers.cu
git commit -m "feat(rl): Kelly resurrection — fused controller mirror (F2.2)"
```

---

### Task F2.3: Update existing tests for back-compat

**Files:** Modify `crates/ml-alpha/tests/risk_stack_invariants.rs`

- [ ] **Step 2.3.1: Add explicit dead-zone disable to G10/G11/G12**

In each of `g10_kelly_at_known_edge_returns_half_kelly`, `g11_kelly_at_negative_edge_clamps_to_zero`, `g12_kelly_held_at_bootstrap_during_warmup`, before the Kelly kernel launch:

```rust
// Explicit dead-zone disable: these tests verify analytic-Kelly behavior,
// not resurrection. Without this, alloc_zeros leaves DEAD_ZONE_FLAG = 0
// which is fine, but make it explicit for clarity.
write_isv(&stream, &mut isv, RL_REGIME_DEAD_ZONE_FLAG_INDEX, 0.0).unwrap();
write_isv(&stream, &mut isv, RL_REGIME_DEAD_ZONE_TIMEOUT_FLAG_INDEX, 0.0).unwrap();
```

- [ ] **Step 2.3.2: Run G10/G11/G12 to confirm no regression**

```bash
SQLX_OFFLINE=true cargo test -p ml-alpha --release --test risk_stack_invariants g10 g11 g12 -- --ignored --nocapture --test-threads=1 2>&1 | tail -15
```

- [ ] **Step 2.3.3: Commit**

```bash
git add crates/ml-alpha/tests/risk_stack_invariants.rs
git commit -m "test(rl): G10/G11/G12 back-compat — explicit dead-zone disable (F2.3)"
```

---

### Task F2.4: Add new resurrection tests G15-G21

**Files:** Modify `crates/ml-alpha/tests/risk_stack_invariants.rs`

- [ ] **Step 2.4.1: Add fixtures**

Per spec lines 627-674. Each is ~30 LOC following the existing G1-G14 pattern. Specifically:

- **G15**: DEAD_ZONE_FLAG fires when composite (kelly=0 ∧ flat=n_batch ∧ cooldown=0)
- **G16**: DEAD_ZONE_FLAG = 0 when any condition violated
- **G17**: Kelly resurrection sets kelly_f = ε_recovery_live when flag set
- **G18**: Kelly retains analytic value when flag NOT set
- **G19**: ε_recovery ramps linearly from ε_min at T=0 to ε_max at T≥N_recovery
- **G20**: TIMEOUT_FLAG fires when DURATION > MAX_DURATION
- **G21**: Kelly resurrection NOT triggered when TIMEOUT_FLAG = 1

- [ ] **Step 2.4.2: Run all G15-G21**

```bash
SQLX_OFFLINE=true cargo test -p ml-alpha --release --test risk_stack_invariants -- --ignored --nocapture --test-threads=1 2>&1 | grep -E "g1[5-9]|g2[0-1]" | head -20
```

Expected: 7 PASSES.

- [ ] **Step 2.4.3: Commit**

```bash
git add crates/ml-alpha/tests/risk_stack_invariants.rs
git commit -m "test(rl): G15-G21 Kelly resurrection invariants (F2.4)"
```

---

### Task F2.5: Synthetic dead-zone smoke

**Files:** None (manual probe)

- [ ] **Step 2.5.1: Run integrated trainer smoke to confirm no regression**

```bash
SQLX_OFFLINE=true FOXHUNT_TEST_DATA=test_data/futures-baseline cargo test -p ml-alpha --test integrated_trainer_smoke --release -- --ignored --nocapture 2>&1 | tail -10
```

- [ ] **Step 2.5.2: Push F2**

```bash
git push origin ml-alpha-regime-observer
```

F2 complete. Kelly trap fix landed.

---

# Phase F3 — v9 slot rename refactor

Goal: v9 reads renamed slot constants; bit-identical behavior.

### Task F3.1: Update kernel + trainer references

**Files:** Modify `crates/ml-alpha/cuda/rl_eval_warmup_decay.cu` AND `crates/ml-alpha/src/trainer/integrated.rs` AND `crates/ml-alpha/examples/alpha_rl_train.rs`

- [ ] **Step 3.1.1: Find all references**

```bash
grep -rn "RL_EVAL_WARMUP_REMAINING\|RL_EVAL_WARMUP_STEPS_CONFIG\|RL_EVAL_WARMUP_DECAY_STEPS_CONFIG" crates/ml-alpha/
```

- [ ] **Step 3.1.2: Search-and-replace constant names**

Use Edit tool with replace_all to rename in each file:

- `RL_EVAL_WARMUP_REMAINING_INDEX` → `RL_REGIME_TRANSITION_REMAINING_INDEX`
- `RL_EVAL_WARMUP_STEPS_CONFIG_INDEX` → `RL_REGIME_TRANSITION_STEPS_CONFIG_INDEX`
- `RL_EVAL_WARMUP_DECAY_STEPS_CONFIG_INDEX` → `RL_REGIME_TRANSITION_DECAY_STEPS_CONFIG_INDEX`

For the `.cu` file the kernel uses `#define` not Rust consts; rename the `#define`s too.

- [ ] **Step 3.1.3: Verify no stragglers**

```bash
grep -rn "RL_EVAL_WARMUP_" crates/ml-alpha/
```

Expected: empty (all renamed).

- [ ] **Step 3.1.4: Compile + smoke**

```bash
SQLX_OFFLINE=true cargo check -p ml-alpha 2>&1 | tail -10
SQLX_OFFLINE=true FOXHUNT_TEST_DATA=test_data/futures-baseline cargo test -p ml-alpha --test integrated_trainer_smoke --release -- --ignored --nocapture 2>&1 | tail -10
```

- [ ] **Step 3.1.5: Commit**

```bash
git add -A
git commit -m "$(cat <<'EOF'
refactor(rl): v9 slot rename — RL_EVAL_WARMUP_* → RL_REGIME_TRANSITION_* (F3.1)

Zero physical migration: slots 685-687 keep their addresses; only the
Rust/C constant identifiers change. Theorem 2 of spec proves
bit-identical behavior.
EOF
)"
```

---

### Task F3.2: Push F3

- [ ] **Step 3.2.1**

```bash
git push origin ml-alpha-regime-observer
```

F3 complete.

---

# Phase F4 — Popart per-account max envelope (Problem 3 fix)

Goal: popart_sigma responds to single-account tail shocks within one step.

### Task F4.1: Audit + extend popart kernel

**Files:** Modify `crates/ml-alpha/cuda/rl_popart_normalize.cu`

- [ ] **Step 4.1.1: Audit existing kernel**

```bash
grep -E "TODO|FIXME|XXX|atomicAdd|#allow|_ =" crates/ml-alpha/cuda/rl_popart_normalize.cu
```

- [ ] **Step 4.1.2: Add warp_reduce_max helper (after existing warp_reduce_sum)**

```c
__device__ __forceinline__ float warp_reduce_max(float val) {
    for (int offset = 16; offset > 0; offset >>= 1) {
        val = fmaxf(val, __shfl_down_sync(0xFFFFFFFF, val, offset));
    }
    return val;
}
```

- [ ] **Step 4.1.3: Add #define for new ISV slots**

```c
#define RL_POPART_MAX_ABS_REWARD_EMA_INDEX 714
#define RL_POPART_MAX_DECAY_ALPHA_INDEX    715
```

- [ ] **Step 4.1.4: Extend Pass 1 with local_max_abs**

Modify the existing `for (int i = tid; ...)` loop:

```c
float local_max_abs = 0.0f;
for (int i = tid; i < b_size; i += block_dim) {
    float r = rewards[i];
    local_sum += r;
    local_sum_sq += r * r;
    local_max_abs = fmaxf(local_max_abs, fabsf(r));  // NEW
}
```

- [ ] **Step 4.1.5: Add warp + shared-mem max reduction**

After existing `warp_reduce_sum` calls:

```c
float warp_max_abs = warp_reduce_max(local_max_abs);  // NEW
// In shared-mem section, allocate 3rd bank:
//   float* s_max_abs = sdata + block_dim * 2;  // NEW
if (lane_id == 0) {
    s_sum[warp_id]     = warp_sum;
    s_sum_sq[warp_id]  = warp_sum_sq;
    s_max_abs[warp_id] = warp_max_abs;     // NEW
}
__syncthreads();

if (tid < 32) {
    float v_sum = (tid < n_warps) ? s_sum[tid]     : 0.0f;
    float v_ssq = (tid < n_warps) ? s_sum_sq[tid]  : 0.0f;
    float v_max = (tid < n_warps) ? s_max_abs[tid] : 0.0f;
    v_sum = warp_reduce_sum(v_sum);
    v_ssq = warp_reduce_sum(v_ssq);
    v_max = warp_reduce_max(v_max);  // NEW

    if (tid == 0) {
        // ... existing batch_mean, batch_var, Welford updates, mean_old/sigma_old saves ...
        // ... new_sigma computation ...

        // NEW: envelope detector + sigma floor
        const float max_r_this_step = v_max;
        const float decay_alpha = isv[RL_POPART_MAX_DECAY_ALPHA_INDEX];
        float max_r_ema = isv[RL_POPART_MAX_ABS_REWARD_EMA_INDEX];
        if (max_r_this_step > max_r_ema) {
            max_r_ema = max_r_this_step;  // fast-up
        } else {
            max_r_ema = (1.0f - decay_alpha) * max_r_ema + decay_alpha * max_r_this_step;
        }
        isv[RL_POPART_MAX_ABS_REWARD_EMA_INDEX] = max_r_ema;

        new_sigma = fmaxf(new_sigma, max_r_ema);  // floor applied

        isv[POPART_SIGMA_INDEX] = new_sigma;

        // CRITICAL: broadcast slots moved (3rd bank inserted)
        sdata[block_dim * 3 + 0] = new_mean;
        sdata[block_dim * 3 + 1] = new_sigma;

        __threadfence_system();
    }
}
__syncthreads();
```

- [ ] **Step 4.1.6: Update Pass 3 broadcast read indices (CRITICAL — Issue β fix)**

Change Pass 3:

```c
// WAS: float mean  = sdata[block_dim * 2 + 0];
// WAS: float sigma = sdata[block_dim * 2 + 1];
float mean  = sdata[block_dim * 3 + 0];  // updated indices
float sigma = sdata[block_dim * 3 + 1];
```

- [ ] **Step 4.1.7: Update shared mem allocation in extern declaration if needed**

The `extern __shared__ float sdata[]` size is set at kernel launch; the kernel itself doesn't size it. Just need to update the launcher (next task).

- [ ] **Step 4.1.8: Compile**

```bash
SQLX_OFFLINE=true cargo build -p ml-alpha --profile dev-release 2>&1 | tail -10
```

- [ ] **Step 4.1.9: Commit**

```bash
git add crates/ml-alpha/cuda/rl_popart_normalize.cu
git commit -m "feat(rl): popart per-account max envelope (F4.1)"
```

---

### Task F4.2: Update trainer launch smem_bytes

**Files:** Modify `crates/ml-alpha/src/trainer/integrated.rs`

- [ ] **Step 4.2.1: Find popart launch in step_with_lobsim**

```bash
grep -n "rl_popart_normalize_fn\|popart.*smem_bytes\|block_x \* 2 + 2" crates/ml-alpha/src/trainer/integrated.rs
```

- [ ] **Step 4.2.2: Update smem_bytes calculation**

Find the existing `smem_bytes` expression like `(block_x * 2 + 2) * (std::mem::size_of::<f32>() as u32)` and change to:

```rust
let smem_bytes = (block_x * 3 + 3) * (std::mem::size_of::<f32>() as u32);
```

- [ ] **Step 4.2.3: Compile + smoke**

```bash
SQLX_OFFLINE=true cargo check -p ml-alpha 2>&1 | tail -10
SQLX_OFFLINE=true FOXHUNT_TEST_DATA=test_data/futures-baseline cargo test -p ml-alpha --test integrated_trainer_smoke --release -- --ignored --nocapture 2>&1 | tail -10
```

- [ ] **Step 4.2.4: Commit**

```bash
git add crates/ml-alpha/src/trainer/integrated.rs
git commit -m "feat(rl): popart launch smem_bytes update — (block*2+2) → (block*3+3) (F4.2)"
```

---

### Task F4.3: Add F4 tests G26-G28

**Files:** Modify `crates/ml-alpha/tests/risk_stack_invariants.rs`

- [ ] **Step 4.3.1: Add 3 fixtures**

- **G26**: Inject rewards = `[0, 0, ..., -100, ..., 0]` (single outlier) → popart_sigma jumps to ≥100 in one step
- **G27**: After G26 spike, run 100 more steps with quiet rewards → max_r_ema decays per α=0.01 (predicted half-life 69 steps)
- **G28**: Run G26 then V regression step → V loss bounded (no spike) thanks to V-correct affine fixup

- [ ] **Step 4.3.2: Run tests**

```bash
SQLX_OFFLINE=true cargo test -p ml-alpha --release --test risk_stack_invariants -- --ignored --nocapture --test-threads=1 2>&1 | grep -E "g2[6-8]" | head -10
```

- [ ] **Step 4.3.3: Commit + push**

```bash
git add crates/ml-alpha/tests/risk_stack_invariants.rs
git commit -m "test(rl): G26-G28 popart envelope invariants (F4.3)"
git push origin ml-alpha-regime-observer
```

F4 complete.

---

# Phase F5 — IQN τ tail-recency consumer

Goal: τ_min temporarily raised when TAIL_EVENT_RECENCY < N_window.

### Task F5.1: Extend IQN τ kernel + fused mirror

**Files:** Modify `crates/ml-alpha/cuda/rl_iqn_action_tau_controller.cu` AND `crates/ml-alpha/cuda/rl_fused_controllers.cu`

- [ ] **Step 5.1.1: Audit IQN τ kernel**

```bash
grep -E "TODO|FIXME|XXX|atomicAdd|#allow|_ =" crates/ml-alpha/cuda/rl_iqn_action_tau_controller.cu
```

- [ ] **Step 5.1.2: Add #define for new ISV slots**

```c
#define RL_REGIME_TAIL_EVENT_RECENCY_INDEX  701
#define RL_IQN_TAU_TAIL_BOOST_FACTOR_INDEX  711
#define RL_IQN_TAU_TAIL_BOOST_N_WINDOW_INDEX 712
```

- [ ] **Step 5.1.3: Insert tail-boost block before final τ clamp**

```c
const float recency     = isv[RL_REGIME_TAIL_EVENT_RECENCY_INDEX];
const float tail_window = isv[RL_IQN_TAU_TAIL_BOOST_N_WINDOW_INDEX];
const float boost       = isv[RL_IQN_TAU_TAIL_BOOST_FACTOR_INDEX];

float tau_min_eff = isv[RL_IQN_ACTION_TAU_MIN_INDEX];
if (recency < tail_window) {
    tau_min_eff *= boost;
}
tau_action = fmaxf(tau_action, tau_min_eff);
```

- [ ] **Step 5.1.4: Mirror in Layer-2 branch of rl_fused_controllers.cu**

Same block.

- [ ] **Step 5.1.5: Compile + commit**

```bash
SQLX_OFFLINE=true cargo build -p ml-alpha --profile dev-release 2>&1 | tail -5
git add crates/ml-alpha/cuda/rl_iqn_action_tau_controller.cu crates/ml-alpha/cuda/rl_fused_controllers.cu
git commit -m "feat(rl): IQN τ_min tail-boost consumer (F5.1)"
```

---

### Task F5.2: Add F5 tests G24/G25

**Files:** Modify `crates/ml-alpha/tests/risk_stack_invariants.rs`

- [ ] **Step 5.2.1: Add fixtures**

- **G24**: With TAIL_EVENT_RECENCY < N_window, τ_action ≥ τ_min × boost_factor
- **G25**: With TAIL_EVENT_RECENCY ≥ N_window, τ_action behavior unchanged

- [ ] **Step 5.2.2: Run + commit + push**

```bash
SQLX_OFFLINE=true cargo test -p ml-alpha --release --test risk_stack_invariants -- --ignored --nocapture --test-threads=1 2>&1 | grep -E "g2[4-5]" | head -5
git add crates/ml-alpha/tests/risk_stack_invariants.rs
git commit -m "test(rl): G24-G25 IQN τ tail-boost invariants (F5.2)"
git push origin ml-alpha-regime-observer
```

F5 complete.

---

# Phase F6 — Validation (local + cluster)

### Task F6.1: Local validation gate

**Files:** None

- [ ] **Step 6.1.1: Full risk_stack_invariants suite**

```bash
SQLX_OFFLINE=true cargo test -p ml-alpha --release --test risk_stack_invariants -- --ignored --nocapture --test-threads=1 2>&1 | tail -30
```

Expected: ALL of G1-G28 PASS.

- [ ] **Step 6.1.2: Controller adaptive floors**

```bash
SQLX_OFFLINE=true cargo test -p ml-alpha --release --test controller_adaptive_floors -- --ignored --nocapture --test-threads=1 2>&1 | tail -10
```

- [ ] **Step 6.1.3: Integrated trainer smoke**

```bash
SQLX_OFFLINE=true FOXHUNT_TEST_DATA=test_data/futures-baseline cargo test -p ml-alpha --test integrated_trainer_smoke --release -- --ignored --nocapture 2>&1 | tail -10
```

- [ ] **Step 6.1.4: 1k local smoke (RTX 3050 Ti)**

```bash
SQLX_OFFLINE=true FOXHUNT_TEST_DATA=test_data/futures-baseline cargo run -p ml-alpha --bin alpha_rl_train --release -- \
  --n-steps 1000 --n-backtests 128 --seq-len 32 --per-capacity 1024 --seed 16962 --log-every 500 2>&1 | tail -20
```

Expected: completes without anomalies; diag JSONL shows `risk_stack.regime.*` fields populated.

---

### Task F6.2: Bit-equivalence verification (F3)

**Files:** None

- [ ] **Step 6.2.1: Run v9-only test before F3 took effect (use git stash on F3 changes)**

This verification ensures F3's rename was truly zero-behavior-change. Approach:

```bash
# Save the F3 commit hash
F3_COMMIT=$(git log --format=%h --grep="v9 slot rename" -1)
PRE_F3_COMMIT=$(git rev-parse ${F3_COMMIT}^)

# Run smoke at PRE_F3 (current state minus the rename)
git stash  # save any uncommitted work
git checkout $PRE_F3_COMMIT
# (run smoke, capture diag.jsonl as PRE)
# checkout back to current
git checkout ml-alpha-regime-observer
git stash pop
# (run smoke, capture diag.jsonl as POST)
# diff
```

Note: this is exploratory; if v9 mechanics aren't exercised in local smoke, the bit-equivalence check is automatically satisfied. The real test is cluster behavior at the fold boundary.

---

### Task F6.3: Cluster validation

**Files:** None

- [ ] **Step 6.3.1: Push commit + cluster smoke**

```bash
git push origin ml-alpha-regime-observer
SHA=$(git rev-parse --short HEAD)

# Cluster smoke (5k b=128) for graph stability
./scripts/argo-alpha-rl.sh \
  --sha $SHA \
  --branch ml-alpha-regime-observer \
  --gpu-pool ci-training-l40s \
  --n-steps 5000 \
  --seq-len 32 \
  --n-backtests 128 \
  --per-capacity 4096 \
  --seed 16962 \
  --instrument-mode all \
  --fold-idx 0 \
  --n-folds 1 \
  --n-eval-steps 0
```

Watch for: no crashes, sps stable, diag emits regime signals.

- [ ] **Step 6.3.2: Cluster full fold-1 walk-forward**

```bash
./scripts/argo-alpha-rl.sh \
  --sha $SHA \
  --branch ml-alpha-regime-observer \
  --gpu-pool ci-training-l40s \
  --n-steps 20000 \
  --seq-len 32 \
  --n-backtests 1024 \
  --per-capacity 4096 \
  --seed 16962 \
  --instrument-mode all \
  --fold-idx 1 \
  --n-folds 3 \
  --n-eval-steps 5000
```

- [ ] **Step 6.3.3: Diag analysis**

Per spec success criteria:
1. No permanent kelly_f=0 trap (or TIMEOUT_FLAG fires if regime adverse)
2. v9 mechanics fire correctly at train→eval boundary
3. ε-recovery cycle visible if any dead-zone occurs
4. F4 popart_sigma_effective tracks tail magnitudes
5. Eval pnl ≥ -$507k (v8 fold-1 baseline)

Validate by querying diag.jsonl for `risk_stack.regime.*` fields.

---

## Self-review checklist

After all phases complete, before considering this plan executed:

- [ ] All 6 phases committed and pushed
- [ ] G1-G28 all pass on RTX 3050 Ti
- [ ] Local smoke unaffected (no behavior change in normal regime)
- [ ] Cluster fold-1 walk-forward completes
- [ ] Eval pnl ≥ baseline
- [ ] Diag JSONL contains `risk_stack.regime.*` fields throughout
- [ ] No NaN or anomaly in regime signals
- [ ] Pre-commit hooks pass on every commit (no skip-hooks)
- [ ] Branch ready to merge per `superpowers:finishing-a-development-branch`

---

## Notes

- Per `feedback_commit_per_phase_for_long_coder_tasks`: each task commits independently so progress survives session interruption.
- Per `feedback_local_smoke_before_cluster`: F1.9, F2.5, F4.2.3 run local smoke before any cluster submit.
- Per `feedback_push_before_deploy`: F6.3.1 pushes before cluster invocation.
- Per `feedback_verify_cluster_before_submit`: F6.3 doesn't include the cluster pre-flight checks (kubectl get pod, billing) — implementer must do those manually before each submit.
- The investigator pod `v9-investigator` from the prior debug session is still running on the L40S pool (4h TTL). If still alive, can be reused for cluster diag analysis; otherwise spawn a fresh one.

## Linked

- Spec: `docs/superpowers/specs/2026-05-31-regime-observer-design.md` (v3)
- Pearls: [[pearl_kelly_trade_stream_death]], [[pearl_dead_signal_resurrection_discipline]], [[pearl_popart_blind_to_session_signals]], [[pearl_adaptive_carryover_discipline]]
