# Regime-Invariance — Implementation Plan for the Four Interventions

**Date**: 2026-06-02
**Status**: Plan (task list only; no implementation)
**Branch**: `ml-alpha-regime-observer` (HEAD `63fc16f17`)
**Source spec**: [`2026-06-02-regime-invariance-four-interventions.md`](../specs/2026-06-02-regime-invariance-four-interventions.md)

## Notes for the executor

1. Interventions ship **in series**, one cluster smoke per intervention. Do NOT bundle.
2. Intervention #1 (R-multiple) is the foundational rip-and-replace. The other three build on top.
3. Per `feedback_no_feature_flags`: the surfer-scaffold infra is **deleted** in the same commit that lands R-multiple. No parallel paths.
4. Per `feedback_single_source_of_truth_no_duplicates`: no `v2_`/version suffixes anywhere.
5. Per `feedback_local_smoke_before_cluster`: every `.cu` / trait / buffer change runs the 1k-step local smoke on RTX 3050 Ti before any Argo submit.
6. **Spec inaccuracy correction**: §1.1 of the spec asserts "Read ATR from existing risk-stack signals (already computed for Kelly position sizing)". Verified false against `crates/ml-alpha/cuda/rl_kelly_fraction_controller.cu`: Kelly consumes `win_rate × avg_win/avg_loss` in USD, **not** ATR. There is no existing ATR EMA slot. Intervention #1 must therefore allocate and produce a fresh ATR EMA. The plan reflects this; the spec §7 open question #1 is settled by this finding.

---

## Intervention #1 — R-multiple reward (ships first, single commit)

### Phase A — ISV layout (RL bus surgery)

#### Task 1.1 — Retire surfer-scaffold slots 753-756
- **File**: `crates/ml-alpha/src/rl/isv_slots.rs`
- **Action**: Delete `RL_SURFER_SCAFFOLD_WEIGHT_INDEX` (753), `RL_SURFER_BREAK_EVEN_WR_INDEX` (754), `RL_SURFER_K_SHARPNESS_INDEX` (755), `RL_SURFER_WARMUP_TRADES_INDEX` (756). Remove the §"Adaptive Surfer-Scaffold Shaping" header block (lines 1740-1779). Renumber `RL_SLOTS_END` from 757 → 753.
- **LOC**: −45

#### Task 1.2 — Allocate R-multiple slots
- **File**: `crates/ml-alpha/src/rl/isv_slots.rs`
- **Action**: Reuse the freed 753-756 range with new semantics (no renumbering needed beyond `RL_SLOTS_END`). Allocate:
  - `RL_ATR_EMA_INDEX = 753` — per-batch ATR EMA in USD over closed-trade |Δrealized_pnl| (Wiener-α blend, half-life ~50 trades).
  - `RL_ATR_EMA_ALPHA_INDEX = 754` — config, default 0.02.
  - `RL_ATR_FLOOR_INDEX = 755` — config; floor in USD ≈ 1 ES tick × multiplier × min_size = $12.50.
  - `RL_R_MULTIPLE_CLAMP_INDEX = 756` — config; symmetric clamp on R-multiple reward to ±N×ATR, default 5.0.
- Set `RL_SLOTS_END = 757`. Add a §"R-multiple reward" header documenting the design + linkage to spec.
- **LOC**: +50

#### Task 1.3 — Per-account ATR-at-entry CUDA buffer
- **File**: `crates/ml-alpha/src/trainer/integrated.rs`
- **Action**: Add `unit_atr_at_entry_d: CudaSlice<f32>` (sized `b_size`). Allocated in `IntegratedRlTrainer::new`, zero-init alongside `unit_entry_step_d`. Listed in struct, zeroed by `reset_session_state` per `pearl_adaptive_carryover_discipline`.
- **LOC**: +20

### Phase B — CUDA kernel changes

#### Task 1.4 — Delete `rl_surfer_scaffold_controller.cu`
- **File**: `crates/ml-alpha/cuda/rl_surfer_scaffold_controller.cu`
- **Action**: `rm`. Per `feedback_no_feature_flags`, the kernel + its 753-weight consumer go atomically.
- **LOC**: −102

#### Task 1.5 — Rewrite Phase 5 of `rl_fused_reward_pipeline.cu` for R-multiple
- **File**: `crates/ml-alpha/cuda/rl_fused_reward_pipeline.cu`
- **Action**: Replace Phase 5 / 5b entirely (lines 217-272). New structure:
  - Capture `unit_atr_at_entry[b]` on flat→positioned transition. Bootstrap = `max(isv[RL_ATR_EMA_INDEX], isv[RL_ATR_FLOOR_INDEX])`.
  - At close (`done > 0.5`): `r = clamp(realized_pnl_delta / max(unit_atr_at_entry[b], floor), ±R_MULTIPLE_CLAMP)`.
  - Drop entry-cost / short-hold-penalty / long-ride / per-step hold-bonus / inventory-penalty.
  - Drop `RL_SURFER_SCAFFOLD_WEIGHT_INDEX` read and all `w *` multipliers.
  - Drop `#define`s for `RL_SURFER_SCAFFOLD_WEIGHT_INDEX`, `RL_ENTRY_COST_INDEX`, `RL_SHORT_HOLD_MIN_STEPS_INDEX`, `RL_SHORT_HOLD_PENALTY_INDEX`, `RL_HOLD_BONUS_INDEX`, `RL_INVENTORY_PENALTY_BETA_INDEX`.
  - Add `#define`s for `RL_ATR_EMA_INDEX`, `RL_ATR_FLOOR_INDEX`, `RL_R_MULTIPLE_CLAMP_INDEX`.
  - Add new kernel arg `float* unit_atr_at_entry` (after `unit_active`).
- **LOC**: −80 / +40 (net −40)

#### Task 1.6 — New ATR EMA producer kernel
- **File**: `crates/ml-alpha/cuda/rl_atr_ema_update.cu` (NEW)
- **Action**: Single-thread (1,1,1)/(1,1,1) launch. On done, read `|realized_pnl_delta|` from the per-batch close, average across the closed-trade batch (single-block tree reduce, no atomics per `feedback_no_atomicadd`), Wiener-α blend into `RL_ATR_EMA_INDEX` with `RL_ATR_EMA_ALPHA_INDEX`. Bootstraps from first-observation per `pearl_first_observation_bootstrap`.
- **LOC**: +90

### Phase C — Trainer wiring

#### Task 1.7 — Drop scaffold controller from trainer
- **File**: `crates/ml-alpha/src/trainer/integrated.rs`
- **Action**: Delete the four `rl_surfer_scaffold_controller`-related blocks:
  - Constant `RL_SURFER_SCAFFOLD_CONTROLLER_CUBIN` (line 299-300).
  - Struct fields `_rl_surfer_scaffold_controller_module` + `rl_surfer_scaffold_controller_fn` (839-840).
  - `load_cubin` / `load_function` blocks (1807-1812).
  - Field assignment in struct literal (2959-2960).
  - Method `launch_rl_surfer_scaffold_controller` (4261-4275).
  - Call site in step pipeline (7757-7762).
- **LOC**: −60

#### Task 1.8 — Wire ATR EMA kernel + buffer into reward pipeline launch
- **File**: `crates/ml-alpha/src/trainer/integrated.rs`
- **Action**:
  - Add `RL_ATR_EMA_UPDATE_CUBIN` include + module/function load alongside other reward kernels.
  - Add `unit_atr_at_entry_d` to the `rl_fused_reward_pipeline` args block (after `unit_active_d`).
  - Add a `launch_rl_atr_ema_update` method.
  - Call it immediately AFTER the fused reward pipeline kernel (so the new ATR observation propagates the next time entry capture fires).
  - Update `reset_session_state` to zero `unit_atr_at_entry_d` and `RL_ATR_EMA_INDEX` per `pearl_adaptive_carryover_discipline`.
- **LOC**: +90

#### Task 1.9 — ISV bootstrap entries
- **File**: `crates/ml-alpha/src/trainer/integrated.rs`
- **Action**: In the init `isv_seed` table (~line 3919), remove the 4 surfer entries and add R-multiple bootstrap entries:
  - `(RL_ATR_EMA_INDEX, 0.0_f32)` (sentinel — first-observation bootstrap)
  - `(RL_ATR_EMA_ALPHA_INDEX, 0.02_f32)`
  - `(RL_ATR_FLOOR_INDEX, 12.50_f32)` (1 ES tick = $12.50)
  - `(RL_R_MULTIPLE_CLAMP_INDEX, 5.0_f32)`
- Also remove the 4 surfer entries from `reset_session_state`'s seed list (~line 3646 area).
- **LOC**: +12 / −10

#### Task 1.10 — Drop scaffold diag emission
- **File**: `crates/ml-alpha/src/trainer/integrated.rs`
- **Action**: Replace the `"surfer_scaffold_weight"` JSONL field (~line 10053) with `"atr_ema_usd"` reading `isv[RL_ATR_EMA_INDEX]` and `"r_multiple_last"` (per-step max abs R-multiple if cheap; otherwise just ATR).
- **LOC**: +5 / −5

### Phase D — Build registration

#### Task 1.11 — Update `build.rs` kernel list
- **File**: `crates/ml-alpha/build.rs`
- **Action**: Remove `"rl_surfer_scaffold_controller"` (line 118). Add `"rl_atr_ema_update"` in its place.
- **LOC**: 0 (1 line swap)

### Phase E — Tests + cubin sanity

#### Task 1.12 — GPU-oracle test for the new fused reward pipeline
- **File**: `crates/ml-alpha/tests/reward_alignment_invariants.rs` (extend existing)
- **Action**: Add a deterministic-seed sub-test asserting:
  - After 1000 sim steps, `Pearson(rewards.sum, Δpnl) ≥ 0.7` per `pearl_reward_signal_anti_aligned_with_pnl`.
  - On every close event, observed `reward == clamp(Δpnl / max(atr_at_entry, floor), ±5)`.
  - `unit_atr_at_entry_d[b]` is overwritten on every flat→positioned transition and unchanged otherwise.
- Per `feedback_no_cpu_test_fallbacks`: oracle is GPU-resident; test reads back via mapped-pinned only.
- **LOC**: +120

#### Task 1.13 — ISV invariants test
- **File**: `crates/ml-alpha/tests/risk_stack_invariants.rs` (extend)
- **Action**: Assert `RL_SLOTS_END == 757` and the four R-multiple slots resolve to 753/754/755/756.
- **LOC**: +15

#### Task 1.14 — Audit-wiring + audit-isv dry run
- **Files**: scripts `scripts/audit-wiring.sh`, `scripts/audit-isv.sh`
- **Action**: Run both; verify no orphan references to `surfer_scaffold_*` survive (consumers, configs, JSONL keys). No new orphans introduced by ATR slots.
- **LOC**: 0 (script run only)

### Phase F — Local validation

#### Task 1.15 — Local 1k-step smoke (RTX 3050 Ti)
- **Command**:
  ```bash
  FOXHUNT_TEST_DATA=test_data/futures-baseline \
    SQLX_OFFLINE=true \
    cargo test -p ml-alpha --lib --release -- smoke_tests --ignored --nocapture
  ```
- **Pass gate**: zero kernel asserts, no NaNs in diag JSONL, `atr_ema_usd > 0` after the first close event, `Pearson(reward, Δpnl)` from `reward_alignment_invariants` test ≥ 0.7.
- **LOC**: 0

#### Task 1.16 — Local 5k-step extended smoke
- **Command**: same as 1.15 but with `--n-steps 5000`. Validates fold-1 train wr ≥ 0.30 trend before cluster commit.
- **LOC**: 0

### Phase G — Cluster smoke

#### Task 1.17 — Cluster smoke fold-1 b=1024 20k+5k
- **Command**:
  ```bash
  ./scripts/argo-alpha-rl.sh \
    --sha <COMMIT_SHA> \
    --branch ml-alpha-regime-observer \
    --gpu-pool ci-training-l40s \
    --batch-size 1024 \
    --n-steps 20000 \
    --n-eval-steps 5000 \
    --n-folds 9 --fold-idx 0
  ```
- **Pass gate**: spec §1.3 — train wr ≥ 0.30 by step 5k AND eval wr ≥ 0.28 AND eval pnl > 0.
- **Fail gate**: eval pnl < −$5M → kill and pivot to Intervention #3.
- **Note**: per `pearl_argo_branch_ref_trap_strikes_again`, pin the exact SHA in the script invocation.
- **LOC**: 0

**Intervention #1 total LOC**: ~+250 / −300 = **net −50** (matches spec §1.1 "Net code REDUCTION").

---

## Intervention #2 — Wire `regime_observer` features into policy network input

Defer until #1 result known. Spec §2 says best deployed AFTER or WITH #3.

#### Task 2.1 — Confirm encoder input dim path
- **File**: `crates/ml-alpha/src/trainer/integrated.rs` (read-only audit)
- **Action**: Locate the encoder input projection used by `rl_encoder_context_broadcast` (build.rs line 94). Identify the current K-dim count it broadcasts (encoder input `[B, K, 56]` per existing comment).
- **LOC**: 0 (research)

#### Task 2.2 — Extend `rl_encoder_context_broadcast.cu` to inject 5 regime features
- **File**: `crates/ml-alpha/cuda/rl_encoder_context_broadcast.cu`
- **Action**: Add a 5-slot tail (cols 56-60) populated from ISV:
  - `RL_REGIME_DEAD_ZONE_FLAG_INDEX` (696)
  - `RL_REGIME_SESSION_PNL_VARIANCE_EMA_INDEX` (700)
  - `RL_REGIME_RECOVERY_FACTOR_INDEX` (699)
  - `RL_REGIME_TAIL_EVENT_RECENCY_INDEX` (701) (normalize by `RL_IQN_TAU_TAIL_BOOST_N_WINDOW_INDEX` = 712)
  - `RL_EDGE_PH_MEAN_INDEX` (750) (normalize against `RL_EDGE_PH_THRESHOLD_INDEX` = 748)
- Each broadcast as a constant across the K-axis (same value all timesteps in the encoder window).
- **LOC**: +40

#### Task 2.3 — Bump encoder input projection from 56 → 61 dims
- **File**: `crates/ml-alpha/src/trainer/integrated.rs` + any `HIDDEN_INPUT_DIM` const
- **Action**: Identify the constant carrying `56`, replace with `61` (or named const `ENCODER_INPUT_DIM`). Per `feedback_use_consts_not_literals_for_structural_dims`, all consumers must reference the const not the literal.
- **LOC**: +10 / −0

#### Task 2.4 — Expand projection weight matrix `[in, hidden]`
- **File**: `crates/ml-alpha/src/trainer/perception.rs` (encoder init)
- **Action**: Grow input-projection weight matrix from `[56, hidden]` to `[61, hidden]`. New columns Xavier-init small. Update checkpoint format version bump constant.
- **LOC**: +25

#### Task 2.5 — Local 1k-step smoke + 5k extended smoke
- **Command**: same as Task 1.15-1.16. Verifies the wider encoder runs cleanly and doesn't explode action entropy.
- **LOC**: 0

#### Task 2.6 — Cluster smoke
- Same as Task 1.17. Pass gate per spec §5 (eval pnl > 0). Compare action-entropy curve against #1 baseline.
- **LOC**: 0

**Intervention #2 total LOC**: ~+75

---

## Intervention #3 — Cross-fold training (domain randomization)

#### Task 3.1 — Audit walk-forward sampler
- **File**: `crates/ml-alpha/examples/alpha_rl_train.rs` (lines 314-340)
- **Action**: Current sampler splits `n_files` into `n_folds` blocks then picks `train = 0..=fold_idx`, `eval = fold_idx+1`. Map this to a per-batch fold-randomization variant.
- **LOC**: 0 (research)

#### Task 3.2 — Add `--cross-fold-training` CLI flag
- **File**: `crates/ml-alpha/examples/alpha_rl_train.rs`
- **Action**: Add boolean flag `cross_fold_training: bool` defaulting `false`. When set, each rollout batch samples a random training fold uniformly from `0..=fold_idx`; eval stays on `fold_idx + 1` (held-out).
- **LOC**: +15

#### Task 3.3 — Per-batch-element fold sampling in rollout loop
- **File**: `crates/ml-alpha/examples/alpha_rl_train.rs` + downstream sampler in `services/ml_training_service`
- **Action**: When cross-fold mode is active, the per-batch GPU sample-and-gather path (`gpu_sample_and_gather` kernel) takes a randomly-chosen fold's file list per batch element. Verify `gpu_sample_and_gather` already supports per-batch file selection (it does — the dataset gather is per-snapshot-anchor). If not, extend.
- **LOC**: +30

#### Task 3.4 — Verify all training folds load cleanly
- **File**: deployment-time check (no code change)
- **Action**: Per `pearl_mbp10_data_is_structurally_broken` correction (cluster PVC has 148GB coherent 24-month ES MBP-10), confirm 9 quarterly files load without size/shape mismatch. Add an assertion in `alpha_rl_train.rs` that all `train_files` parse before training begins (and abort early if not).
- **LOC**: +10

#### Task 3.5 — Local 1k-step smoke
- **Command**: same as Task 1.15 with `--cross-fold-training` flag.
- **Pass gate**: trade rate doesn't crash to zero; reward Pearson ≥ 0.7 preserved.
- **LOC**: 0

#### Task 3.6 — Cluster smoke
- Same as Task 1.17 with `--cross-fold-training` and `--n-folds 9 --fold-idx 7` (train on folds 0-6, eval on fold 7, held-out tail).
- **LOC**: 0

**Intervention #3 total LOC**: ~+55

---

## Intervention #4 — Mixture-of-experts gated by regime (deferred)

Spec §4.2 marks this as 2-day, 250-LOC effort, defer until #1-#3 validated. Plan stays at task-level only.

#### Task 4.1 — Expert architecture design doc
- **File**: `docs/superpowers/specs/2026-06-XX-moe-regime-gating.md` (CREATE only if Intervention #1-#3 fails)
- **Action**: Capture expert-A / expert-B / gating-net dimensions, per-expert reward shaping plan, training schedule.
- **LOC**: ~150 markdown

#### Task 4.2 — Two-expert MLP heads (parallel to current PPO policy head)
- **File**: `crates/ml-alpha/src/rl/ppo.rs` + new `crates/ml-alpha/cuda/rl_moe_policy_head.cu`
- **Action**: 2 separate `[hidden, N_ACTIONS]` weight matrices, forward + backward kernels. Gating net is a 2-layer MLP on regime features feeding softmax(K=2).
- **LOC**: +250

#### Task 4.3 — Per-expert reward shaping
- **File**: `crates/ml-alpha/cuda/rl_fused_reward_pipeline.cu`
- **Action**: Add per-expert reward path — expert A uses R-multiple-by-hold-duration, expert B uses R-multiple-of-inverse-hold (mean-revert specialty). Gated by `expert_assignment[b]` (sampled from gate softmax at trade open).
- **LOC**: +40

#### Task 4.4 — Trainer wiring + 6 ISV slots
- **File**: `crates/ml-alpha/src/rl/isv_slots.rs`, `crates/ml-alpha/src/trainer/integrated.rs`
- **Action**: 6 new slots (gate temp, K=2 gate weights × 2 layers' hyperparams, per-expert KL trackers). Bumps `RL_SLOTS_END` from 757 → 763.
- **LOC**: +80

#### Task 4.5 — Cluster smoke
- Same as Task 1.17. Spec §4 falsification: eval pnl > 0.
- **LOC**: 0

**Intervention #4 total LOC**: ~+370 (excluding spec doc)

---

## Cross-cutting tasks (apply to all interventions)

#### Task X.1 — Run `scripts/audit-wiring.sh` and `scripts/audit-isv.sh` after each intervention
- **Action**: Confirms no orphan kernel registrations, no dead ISV slots, no hardcoded literals where ISV reads should be used.
- **LOC**: 0

#### Task X.2 — Memory pearl update on cluster verdict
- **File**: `/home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/`
- **Action**: After each cluster smoke, append a pearl-style entry to `MEMORY.md` topic file with the verdict (PASS / MARGINAL / FAIL with eval pnl, wr, action entropy). Per `feedback_trust_code_not_docs`, the cluster log is ground truth; pearl summarizes what was measured.
- **LOC**: ~5 per intervention

#### Task X.3 — Pre-commit `precommit` zen + local smoke
- **Action**: Run `mcp__zen__precommit` or equivalent + `feedback_local_smoke_before_cluster` 1k-step smoke before each cluster submission. No exceptions.
- **LOC**: 0

---

## Sequencing decision tree (recap from spec §5)

```
[Ship #1] ──→ cluster smoke ──→ eval pnl > 0? ──yes──→ DONE, deploy
                                       │
                                       no
                                       ↓
                          [Ship #3] ──→ cluster smoke ──→ eval pnl > 0? ──yes──→ DONE
                                                                    │
                                                                    no
                                                                    ↓
                                                       [Ship #2] ──→ cluster smoke ──→ eval pnl > 0? ──yes──→ DONE
                                                                                                │
                                                                                                no
                                                                                                ↓
                                                                                  [Ship #4] ──→ cluster smoke ──→ verdict
                                                                                                                  │
                                                                                                                  no
                                                                                                                  ↓
                                                                                                       Termination clause §5
```

---

## Open questions resolved by this plan

1. **ATR source** (spec §7 Q1): No existing Kelly ATR. Allocate fresh `RL_ATR_EMA_INDEX` slot 753 + new `rl_atr_ema_update.cu` producer kernel.
2. **ATR bootstrap** (Q2): Per `pearl_first_observation_bootstrap` + sentinel-zero protocol. Use `max(atr_ema, atr_floor=$12.50)`.
3. **Eval-boundary discipline** (Q3): Per `pearl_adaptive_carryover_discipline`, zero `RL_ATR_EMA_INDEX` and `unit_atr_at_entry_d` in `reset_session_state`. R-multiple denominator falls back to `atr_floor` for the first eval trade — preferred over carrying train-end calibration into a regime-shifted eval window.
4. **Fold sampling weight** (Q4): Equal-weight (uniform) in Intervention #3 v1. Active domain randomization (regime-novelty weighting) is a future #3.1 if Plain #3 underperforms.

---

## Estimated total LOC by intervention

| # | Intervention                  | Net LOC | Files touched | Cluster smokes |
|---|-------------------------------|---------|----------------|----------------|
| 1 | R-multiple reward             | −50     | 8              | 1              |
| 2 | Regime features → encoder     | +75     | 3              | 1              |
| 3 | Cross-fold training           | +55     | 2              | 1              |
| 4 | Mixture-of-experts (deferred) | +370    | 5              | 1+             |

---

## Files this plan touches (canonical list)

**Intervention #1**:
- `crates/ml-alpha/src/rl/isv_slots.rs`
- `crates/ml-alpha/cuda/rl_fused_reward_pipeline.cu`
- `crates/ml-alpha/cuda/rl_surfer_scaffold_controller.cu` (DELETE)
- `crates/ml-alpha/cuda/rl_atr_ema_update.cu` (CREATE)
- `crates/ml-alpha/src/trainer/integrated.rs`
- `crates/ml-alpha/build.rs`
- `crates/ml-alpha/tests/reward_alignment_invariants.rs`
- `crates/ml-alpha/tests/risk_stack_invariants.rs`

**Intervention #2**:
- `crates/ml-alpha/cuda/rl_encoder_context_broadcast.cu`
- `crates/ml-alpha/src/trainer/integrated.rs`
- `crates/ml-alpha/src/trainer/perception.rs`

**Intervention #3**:
- `crates/ml-alpha/examples/alpha_rl_train.rs`
- `services/ml_training_service/src/k8s_dispatcher.rs` (if any K8s arg surface needs `--cross-fold-training`)

**Intervention #4** (deferred):
- `docs/superpowers/specs/2026-06-XX-moe-regime-gating.md` (CREATE)
- `crates/ml-alpha/src/rl/ppo.rs`
- `crates/ml-alpha/cuda/rl_moe_policy_head.cu` (CREATE)
- `crates/ml-alpha/cuda/rl_fused_reward_pipeline.cu`
- `crates/ml-alpha/src/rl/isv_slots.rs`
- `crates/ml-alpha/src/trainer/integrated.rs`
