docs(sp13): v2 spec + plan after critical review

Spec v2 supersedes v1 with five major fixes from the critical review:
- Drop alpha-vs-benchmark reward (mathematically tautological — Long trades had
  alpha = -costs always against always-long benchmark)
- Split Phase 0 into 0a (Hold-only, clean test of user hypothesis) + 0b (aux
  amplification probe, only if 0a partial), avoiding the v1 confounded experiment
- Bound direction-skill bonus relative to |alpha| (cap_ratio × |alpha|) to prevent
  reward gaming on small-alpha correct-direction losers; spec adds 8-quadrant
  worked-example matrix verifying the no-negative-EV invariant
- Replace 5-epoch absolute-threshold gate with 10-epoch trajectory criterion to
  avoid false-negatives from aux head underconvergence
- Aux_w controller adds dual-EMA stagnation detector (decays toward base when
  no improvement) — prevents permanent destabilization of Q-head in
  data-limited case

Plan v2 mirrors spec changes:
- Phase 0a (Hold elimination + dir_acc instrumentation, ~300 LOC, 1 atomic commit)
- Phase 0b (aux_w controller replacement, conditional, ~80 LOC)
- Layer B (aux head regression -> binary classification, ~120 LOC)
- Layer C (skill bonus + luck discount with calibrated bounds, ~150 LOC)
- Layer D (30-epoch validation + 3 new pearls)

ISV slot allocation [372..380): drops slot 371 (BENCHMARK_PNL_CUMULATIVE),
adds 374 (AUX_DIR_ACC_LONG_EMA — stagnation), 379 (SKILL_BONUS_CAP_RATIO).

Plan integrates Explore-agent touch-list (50-70 sites) with corrected enum
ordering: Short=0, Long=1, Flat=2 (preserves codebase Short-first convention,
avoids 30+ stale-comment churn). Adds direction-bias signal handling at
experience_kernels.cu:5159 (Hold's 0.5 softening gate vanishes -> [1, 1, 0]).

Three new pearls planned for Layer D close-out:
- pearl_redefine_success_for_predictive_skill
- pearl_skill_bonus_must_be_alpha_bounded (calibration lesson)
- pearl_reward_quadrant_audit_required (meta-pearl)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-04 22:24:16 +02:00
parent a1681abc46
commit 0ca45ef61d
2 changed files with 1614 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,386 @@
# SP13 — Redefine Success for Predictive Skill (v2)
**Date**: 2026-05-04 (v2 — supersedes v1 in-place)
**Branch**: continues `sp11-reward-as-controlled-subsystem` (single coherent architectural lineage)
**Builds on**: SP11 (cap stability) + SP12 (event-driven density) + asymmetric loss aversion
**Anchor pearls**:
- `pearl_audit_unboundedness_for_implicit_asymmetry`
- `pearl_event_driven_reward_density_alignment`
- (NEW) `pearl_redefine_success_for_predictive_skill` — to be written post-validation
- (NEW) `pearl_skill_bonus_must_be_alpha_bounded` — to be written post-validation
- (NEW) `pearl_reward_quadrant_audit_required` — meta-pearl from v1 review
## v1 → v2 changes (reasons here for traceability)
| Issue | v1 | v2 |
|---|---|---|
| Alpha-vs-benchmark math broken — Long trades have alpha = costs always (tautology against always-long benchmark) | reward = realized_pnl benchmark_pnl_same_period | **Dropped.** Predictive-skill incentive carried by direction-skill bonus + correct-call asymmetry. Per-fold buy-and-hold delta tracked as HEALTH_DIAG metric only. |
| Phase 0 confounded Hold removal AND aux_w boost — couldn't tell which helped | Single P0 commit | **Split:** P0a Hold-only (cheap test of user hypothesis), P0b aux probe (only if 0a partial) |
| Skill bonus + luck-discount could produce positive reward for correct-direction expected-loser → reward gaming | α=β=1.0 raw constants | **Bounded:** `skill_bonus = sign × min(α_param, ratio × |alpha|)` with ratio=0.3 default. Worked examples below verify no negative-EV gaming. |
| 5-epoch gate too short for aux to converge → false-negative risk | 5-epoch absolute threshold | **10-epoch trajectory criterion:** dir_acc trending up AND final > 0.52 |
| Aux_w controller at hard cap forever in data-limited case → destabilises Q | `clamp(0.05, 1.5)` only | **Stagnation detector:** dual-EMA (fast/slow); if slow ≈ fast for ≥ N epochs, decay aux_w toward base |
| No worked examples of full reward composition | Inline formulas | **Worked-example matrix** for 4 trade-outcome quadrants (correct/wrong × winner/loser) |
## TL;DR
The reward function defines what "success" means to the model. SP1-SP12 lineage rewarded
**any positive bounded P&L** — including P&L from structural extraction (trail-stop
arithmetic on directionally-random bets at 46% win rate). SP13 rewards **predictive
skill specifically** via two surgical changes:
1. **Eliminate Hold action** — model must commit. Tests user's hypothesis that Hold's
availability has been hiding directional signal across the entire SP1-SP12 lineage.
2. **Direction-skill bonus + correct-call asymmetry** — bounded explicit reward for
predicting direction correctly, bounded penalty for being wrong, and discount on
wrong-direction lucky wins. Mathematically calibrated so the model cannot game by
entering expected-loser trades.
Plus three structural supports:
3. **Aux head regression → binary classification** on next-30-bar return sign, wired
into direction Q-head input.
4. **ISV-driven aux_w controller** (replaces SP11 inverted formula) with deficit-driven
amplification AND stagnation detector to prevent destabilization in data-limited case.
5. **Per-fold buy-and-hold delta** tracked as HEALTH_DIAG metric (NOT reward signal).
~600-750 LOC across 4 commits (P0a, P0b, B, C). Greenfield refactor — replay buffer
compatibility intentionally broken.
**Hypothesis under test** (user, 2026-05-04): directional signal IS in the data; ~46% WR
persists because Hold hides it. P0a smoke decides cleanly:
- WR climbs > 0.52 with Hold removed → ✅ confirmed, hypothesis was right
- WR stays flat at 0.46-0.50 → P0b adds aux amplification as second probe
- Both P0a + P0b flat → ❌ data limit, halt SP13
## Background — why current system fails
50-epoch validation `train-multi-seed-pmbwn` and 22-epoch SP12 v3 validation
`train-multi-seed-vd2v5`:
- val_dir_dist: hold=0.45 (lazy default), Long+Short=0.36 (active bets)
- win_rate=0.4651 (slightly below random for direction)
- profit_factor=1.24 (winners 24% bigger than losers)
- aux_w stuck at 0.09 (suppressed by SP11-era `0.1 × health × (1 sharpe)` controller)
- aux next_bar_mse=3.83 (regression error suggests poor signal extraction OR aux never got real gradient)
**The mechanism**: model picks Hold ~half the time. Of the ~37% directional bets, only
46% are correct. Profitability comes from trail-stop arithmetic, not predictive skill.
## The architectural shift
| Current "success" | Proposed "success" |
|---|---|
| Maximize positive bounded P&L | Maximize bounded P&L + bounded direction-skill credit |
| Hold is safe default | Must commit (no Hold option) |
| Wrong direction + lucky exit = full reward | Wrong direction + win = 0.3× reward (luck discount) |
| Aux head suppressed when sharpe high | Aux amplified when WR below target, decayed when stagnant |
| 1-bar reward horizon (next-bar MSE) | 30-bar prediction horizon (sign classification) |
## The five changes
### Change 1: Eliminate Hold action
**Current**: 4-way direction `{Short=0, Hold=1, Long=2, Flat=3}` × magnitude × ord × urg.
**New**: 3-way `{Short=0, Long=1, Flat=2}` — Hold removed, Short stays at 0 (preserves
canonical anchor used across 30+ kernel comments and tests), Long shifts down 2 → 1,
Flat shifts down 3 → 2.
**Action semantics in 3-way world** (replaces `project_hold_action_design`):
- `Long` at t when prior position = Flat → open Long
- `Long` at t when prior position = Long → keep Long (same as old "Hold-Long")
- `Long` at t when prior position = Short → flip to Long
- `Flat` at t when any position → close all positions
- `Short` symmetric
`Long` is no longer "open Long" specifically; it is "have Long position". `Long` is a
state, not a verb.
**Affected** (atomic per `feedback_no_partial_refactor`, ~5070 touch-sites mapped by
Explore agent 2026-05-04):
- Canonical Rust enum at `crates/ml-core/src/common/action.rs:124-132` and
`NUM_DIRECTIONS: 4 → 3` at `crates/ml-core/src/state_layout.rs:48-49`
- `state_layout.cuh` C/CUDA mirror: `#define NUM_DIRECTIONS 3` and `DIRECTION_SHORT=0,
DIRECTION_LONG=1, DIRECTION_FLAT=2` (DIRECTION_HOLD removed)
- Q-head output dim 4 → 3, weight/bias re-init, layout fingerprint bump
- Action selection kernel argmax over 3 (`experience_action_select`)
- C51/IQN per-direction-bucket loops (~6 `for d<4` sites become `< NUM_DIRECTIONS`)
- **Action packing recompacts 12 → 9 slots**: `action_counts` buffer and `exp_idx =
dir*3 + mag` packing in `financials.rs:197` shrink from 12 → 9 (3 dirs × 3 mags).
Real shape change, not slot-3 dead-space. Consumers in `monitoring.rs:341` reindex.
- **Direction-bias signal at `experience_kernels.cu:5159`**: 4-tuple per-direction
bias weights `[Short=1.0, Hold=0.5, Long=1.0, Flat=0.0]` re-derives to 3-tuple
`[Short=1.0, Long=1.0, Flat=0.0]`. Hold's 0.5 was a softening gate; with Hold gone
the gate is no longer needed (Long/Short are full-weight, Flat is the soft branch).
- SP5 Pearl 8 per-direction trail-distance ISV range `TRAIL_DIST_PER_DIR_BASE=270..274`:
producer rewrites at new indices `{0=Short, 1=Long, 2=Flat}`; slot 273 dead-space
(per-direction-indexed ISV ranges keep length 4 to avoid cascade-renumber of all
downstream slots — consumers read by direction index ∈ 0..2 so dead slot is never
touched). Recompaction deferred to SP13 close-out.
- HEALTH_DIAG val_dir_dist 3-bucket emit: `metrics.rs:949`, `magnitude_distribution.rs:88,217`,
`distributional_q_tests.rs:790`
- KL divergence calc (if present)
- 30+ stale doc/comment lines across kernel headers — implementer audits in same commit
### Change 2: Direction-skill bonus (bounded)
```
if action ∈ {Long, Short}:
correct = (sign(action) == sign(realized_30_bar_return))
skill_cap = SKILL_BONUS_CAP_RATIO_INDEX × |alpha_capped| # default 0.3 × |alpha|
if correct:
skill_bonus = +min(DIR_SKILL_BONUS_ALPHA_INDEX, skill_cap)
else:
skill_bonus = -min(DIR_SKILL_BONUS_BETA_INDEX, skill_cap)
else: # action == Flat
skill_bonus = 0
```
**Calibration check** (worked examples — α=β=1.0, ratio=0.3, SP12 cap [-10,+5]):
| Trade outcome | alpha (capped) | skill_bonus | luck-disc | reward | sane? |
|---|---|---|---|---|---|
| correct-Long-winner (big win) | +5 | +min(1.0, 1.5) = +1.0 | n/a | +6.0 | ✓ |
| correct-Long-winner (small win) | +0.5 | +min(1.0, 0.15) = +0.15 | n/a | +0.65 | ✓ |
| correct-Long-loser (small loss) | 0.5 | +min(1.0, 0.15) = +0.15 | n/a | **0.35** | ✓ still negative |
| correct-Long-loser (big loss) | 10 | +min(1.0, 3.0) = +1.0 | n/a | **9.0** | ✓ still very negative |
| wrong-Long-winner LUCKY (big) | +5 | min(1.0, 1.5) = 1.0 | × 0.3 | (51)×0.3 = **+1.2** | ✓ reduced from naive +6 |
| wrong-Long-winner LUCKY (small) | +0.5 | min(1.0, 0.15) = 0.15 | × 0.3 | (0.50.15)×0.3 = +0.105 | ✓ |
| wrong-Long-loser | 10 | min(1.0, 3.0) = 1.0 | n/a | **11.0** | ✓ full penalty + skill penalty |
| Flat in any direction | 0 | 0 | n/a | 0 | ✓ |
**Invariant verified**: every correct-direction loser has reward < 0; every
wrong-direction expected-loser has reward < 0. The model cannot game by entering
trades it expects to lose. ✓
### Change 3: Conditional correct-call asymmetry (luck-win discount)
```
if trade_pnl > 0 AND direction_was_wrong:
reward *= LUCK_WIN_DISCOUNT_INDEX /* default 0.3 */
# else: reward unchanged
```
Penalises specifically lucky wins (positive outcome despite wrong direction prediction).
Forces the model to optimize for correct prediction, not just positive outcome.
ISV-driven discount factor; default 0.3 starts strong, can be relaxed by ISV controller
if luck-win frequency drops below threshold.
### Change 4: Aux head — regression → binary classification
**Current**: `next_bar_return_mse` (regression). MSE 3.83 — worse than predicting zero.
**New**: `next_30_bar_return_sign` cross-entropy. 30 bars matches SP12 min_hold_target.
**Output**: 2-class softmax `[P(positive), P(negative)]`.
**Wired into direction Q-head**: aux signal `logit_diff = softmax[1] softmax[0]` ∈
[1, +1] is concatenated into direction Q-head input layer via ISV[`AUX_DIR_PREDICTION_INDEX`].
This explicitly couples predictive signal to action selection.
**No information leakage**: aux head trained with hindsight labels (sign of
price[t+30] price[t], known at training time only). At inference, aux predicts using
state s_t alone — no future info accessible. Same as existing next_bar_mse pattern.
### Change 5: ISV-driven aux_w controller (with stagnation detector)
**Current** (SP11-era, broken):
```rust
let aux_w = (0.1 * learning_health * (1 - sharpe_tanh)).clamp(0.05, 0.3);
```
Lowers aux when sharpe is high — exactly wrong for the failure mode (high sharpe + low WR).
**New** (deficit-driven + stagnation):
```
target = ISV[TARGET_DIR_ACC_INDEX] # default 0.55
short = ISV[AUX_DIR_ACC_SHORT_EMA_INDEX] # fast EMA (α=0.3, ~3-epoch lookback)
long = ISV[AUX_DIR_ACC_LONG_EMA_INDEX] # slow EMA (α=0.05, ~20-epoch lookback)
deficit = max(0, target - short)
improvement = max(0, short - long) # positive when short pulling ahead of long
stagnation = max(0, 1 - improvement / max(0.005, deficit))
# 1 = fully stagnant (no improvement); 0 = improving fast
aux_w = base * (1 + 5 * deficit) * (1 - 0.7 * stagnation)
aux_w = clamp(aux_w, base * 0.3, base * 3.0) # never zero, never beyond 3× base
```
Rationale:
- When `short < target` and improving (`short > long`): aux_w rises strongly to push harder
- When `short < target` and stagnant (`short ≈ long`): aux_w decays back toward base — "we tried, it didn't help, stop destabilising Q"
- When `short ≥ target`: deficit = 0, aux_w = base. Steady-state.
- Hard floor at 0.3× base prevents complete suppression (different from SP11 inversion bug).
base = 0.5 in Phase 0b and beyond (SP11 base 0.05 was the suppression bug; lifting is
the fix, not a confounder, because the SP11 formula was structurally wrong).
**ISV slots used**: 372 (target), 373 (short EMA), 374 (long EMA). Both EMAs reset per fold.
## ISV slot allocation
8 new slots in `[372..380)`:
| Slot | Name | Purpose | Reset |
|---|---|---|---|
| 372 | `TARGET_DIR_ACC_INDEX` | Directional accuracy target (default 0.55) | static |
| 373 | `AUX_DIR_ACC_SHORT_EMA_INDEX` | Fast EMA of dir_acc (α=0.3) | per-fold |
| 374 | `AUX_DIR_ACC_LONG_EMA_INDEX` | Slow EMA of dir_acc (α=0.05), stagnation detector | per-fold |
| 375 | `AUX_DIR_PREDICTION_INDEX` | Aux head logit_diff per bar (concat into Q-head input) | per-bar |
| 376 | `DIR_SKILL_BONUS_ALPHA_INDEX` | Skill bonus magnitude (correct direction), default 1.0 | per-fold |
| 377 | `DIR_SKILL_BONUS_BETA_INDEX` | Skill bonus penalty (wrong direction), default 1.0 | per-fold |
| 378 | `LUCK_WIN_DISCOUNT_INDEX` | Lucky-win discount factor, default 0.3 | per-fold |
| 379 | `SKILL_BONUS_CAP_RATIO_INDEX` | Cap ratio: bonus ≤ ratio × \|alpha\|, default 0.3 | per-fold |
Bump `SP5_SLOT_END = 380`, `ISV_TOTAL_DIM = 380`.
**Slot 371 (was BENCHMARK_PNL_CUMULATIVE_INDEX in v1) — DROPPED.** Per-fold buy-and-hold
delta is computed at fold-end in HEALTH_DIAG emit (trivial: `price[fold_end]
price[fold_start]`). No kernel, no slot, no reward dependency.
## Implementation phases
### Phase 0a — Hold elimination only (1 atomic commit)
**Purpose**: clean test of user's hypothesis. Hold removed; aux_w controller, dir_acc
instrumentation, skill bonus, aux classification all UNCHANGED from current main.
- ISV slot 372 (target, static), 373 (short EMA), 374 (long EMA), 375 (aux prediction —
but produced by the existing regression aux head, just `tanh(scalar_pred)` for now to
fit the [1, +1] interface)
- `aux_dir_acc_reduce_kernel` — single-block tree-reduce of (correct_count, pos_pred,
pos_label, valid_count) → 3 output floats per epoch
- HEALTH_DIAG `aux_dir_acc accuracy=… pos_pred=… pos_label=…` per epoch
- Hold elimination atomic refactor (state_layout, Q-head, action select, C51/IQN bucket
state, replay encoding, val_dir_dist, KL, Rust enum)
- ~300 LOC
**Phase 0a smoke** (10-epoch L40S):
- ✅ **Pass**: `val_dir_acc[ep9] > val_dir_acc[ep1]` AND `val_dir_acc[ep9] > 0.52` AND
`val_win_rate[ep9] > 0.51` → user's hypothesis confirmed; proceed Layer B.
- ⚠️ **Partial** (one trending up, the other flat): proceed P0b.
- ❌ **Both flat** (`dir_acc < 0.51` AND `win_rate < 0.50` after 10 epochs): proceed P0b
to add aux amplification. If P0b also fails, halt SP13.
10 epochs (not 5) because aux head regression has been suppressed for entire SP1-SP12
lineage — needs time even at unchanged aux_w to show whether signal is recoverable.
Cost: ~€0.60.
### Phase 0b — Add corrected aux_w controller (only if 0a partial/red, atomic commit)
- Replace SP11 inverted formula at `training_loop.rs:4103-4104` with deficit-driven +
stagnation formula from Change 5
- Bump aux_w base 0.05 → 0.5
- ~80 LOC
**Phase 0b smoke** (10-epoch):
- Same gate criterion as P0a
- If still red: halt SP13. Update `project_hold_hides_directional_signal_hypothesis.md`
status to DISCONFIRMED. Pivot to data-augmentation work (separate spec, SP14).
### Layer B — Aux head regression → classification (atomic, only on green Phase 0)
- Aux head output dim 1 → 2 + softmax + cross-entropy loss
- Label generation: `sign(price[t+30] price[t])` as 0/1 class
- `aux_dir_acc_reduce_kernel` adapted: read 2-class softmax instead of regression scalar
- Wire `logit_diff = softmax[1] softmax[0]` → ISV[375] → direction Q-head input concat
(in_dim grows by 1)
- ~120 LOC
### Layer C — Reward composition (atomic)
- ISV slots 376, 377, 378, 379 + state-reset registry
- Direction-skill bonus device-inline function (`compute_direction_skill_bonus_bounded`)
- Luck-win discount device-inline function (`compute_luck_win_discount`)
- Replace SP12 reward composition at exit-event sites: keep SP12 asymmetric cap, ADD
skill bonus, ADD luck discount
- HEALTH_DIAG: add `per_fold_alpha_vs_buy_and_hold = total_pnl (price[fold_end]
price[fold_start])` — METRIC ONLY, not reward signal
- 12+ GPU oracle tests for new reward math (extends SP12 sp12_reward_math_tests scaffold)
- ~150 LOC
### Layer D — 30-epoch validation + close-out (no commit until green)
- 30-epoch L40S full validation
- 3-action SP12 control run for fair baseline (~€1, optional but recommended for clean
before/after comparison)
- If WR ≥ 55% AND profit_factor improvement ≥ 15% over 3-action SP12 control →
close-out commit: audit doc + 2 new pearls (`pearl_redefine_success_for_predictive_skill`,
`pearl_skill_bonus_must_be_alpha_bounded`) + meta-pearl
`pearl_reward_quadrant_audit_required`
- If WR plateaus 50-54%: tune horizon (next_60 instead of next_30) or skill bonus α/β,
re-validate
## Validation targets
**Phase 0a smoke (10-epoch)** — primary gate:
- val_dir_acc trajectory: monotone non-decreasing OR final > start
- val_dir_acc[final] > 0.52
- val_win_rate[final] > 0.51
- Hold action eliminated: val_dir_dist sums to 1.0 over {Long, Short, Flat}
- No NaN/inf
- mean(weights) controller still ≈ 1.0
- aux next_bar_mse trajectory: not blowing up
**Phase 0b smoke (10-epoch)** — secondary gate (only if 0a partial):
- aux_w controller responding: aux_w[ep5] > base when deficit > 0.05
- Stagnation detector engaging: aux_w decays toward base when short EMA ≈ long EMA
- Same dir_acc + win_rate criteria as 0a
**Layer B 5-epoch smoke**:
- Aux CrossEntropy loss decreasing
- val_dir_acc continues at or above Phase 0 level
- aux signal feeding Q-head (verify by perturbing aux output → direction action distribution shifts)
**Full validation (30-epoch)**:
- val_dir_dist: roughly {Long: 0.4, Short: 0.4, Flat: 0.2} — model committing
- **win_rate ≥ 0.55** PRIMARY SUCCESS CRITERION
- profit_factor ≥ 1.5 (SP12 baseline 1.24 × 21% improvement floor for "skill-driven" claim)
- per_fold_alpha_vs_buy_and_hold > 0 sustained (the dropped reward signal, now metric)
- max_drawdown < 0.005
## Operating principles (NON-NEGOTIABLE)
- **`feedback_no_partial_refactor`**: P0a, P0b, B, C each single atomic commit
- **`feedback_isv_for_adaptive_bounds`**: target_dir_acc, aux_w base/cap, bonus α/β,
luck-discount, skill-bonus-cap-ratio — ALL ISV-driven
- **`feedback_no_quickfixes`**: this IS the proper fix per pearls
- **`feedback_no_feature_flags`**: behavior change unconditional
- **End-to-end training**: NO pre-training of aux head
- **`pearl_audit_unboundedness_for_implicit_asymmetry`**: SP12 asymmetric cap [-10,+5]
preserved
- **`pearl_event_driven_reward_density_alignment`**: skill bonus + luck discount fire
only at trade-exit events, not per-bar
- **(NEW) Reward-quadrant audit**: any reward composition change must include the 4-
quadrant table (correct/wrong × winner/loser) showing all signs and inequalities. This
rule will become `pearl_reward_quadrant_audit_required` post-validation.
## Estimated cost
**Implementation**: ~10-12h (~700 LOC across P0a + P0b + B + C + D close-out)
**P0a smoke**: ~€0.60 (10-epoch L40S)
**P0b smoke** (if needed): ~€0.60
**Layer B smoke**: ~€0.30 (5-epoch)
**30-epoch validation**: ~€1.00
**3-action SP12 control** (recommended): ~€1.00
**Total best case** (P0a green, no P0b): ~€2.90 + 10-12h
**Total worst case** (P0a red, P0b green): ~€3.50
**Total disconfirm case** (both P0a + P0b red): ~€1.20 — cheapest learn
## Open questions
1. **Aux prediction horizon**: 30 bars matches SP12 min_hold_target. If 30-epoch
validation shows partial WR climb (50-54%), Layer D iteration tries 60-bar horizon.
2. **Skill-bonus cap ratio**: 0.3 chosen so worst-case bonus contribution to reward at
|alpha|=10 is ±1.0 — comparable to one tick of P&L. ISV-driven, can be tightened to
0.2 if Layer B/C reveals reward gaming.
3. **Stagnation detection threshold**: 0.7 decay coefficient is heuristic. Worth probing
in Layer C smoke (read aux_w trajectory).
4. **Whether to keep SP12 min-hold penalty**: SP12's `min_hold_factor` curriculum still
applies. Spec recommends KEEP — it's orthogonal to skill incentive and prevents
compulsive churn. Re-evaluate at Layer D close-out.
## Decision needed
Approve SP13 v2 spec → update implementation plan to match → dispatch P0a implementer
chain → 10-epoch smoke → branch on outcome.