# Deploy-the-Fund Knobs Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Make the fund's capital + risk appetite CLI-configurable via a single persisted SSOT, and switch the crypto allocation from the double-counted `bybit_4edge` book to its individual capacity-bounded sleeves.

**Architecture:** A single-row `fund_config` ORM table (rides the existing `CockpitBase.metadata.create_all`), a `application/fund_config.py` with read (settings-fallback) / write (partial-upsert + validation) / seed, a `fxhnt fund-config show|set` typer CLI, the nightly `cockpit_forward` reads it for equity + an `AllocationPolicy` override, and the registry+provider re-point the crypto allocation to sleeves.

**Tech Stack:** Python 3.12, SQLAlchemy 2.0 (Mapped/mapped_column, DeclarativeBase), typer CLI, pytest. In-memory SQLite for tests (StaticPool), Postgres/Timescale in prod.

## Global Constraints

- Spec: `docs/superpowers/specs/2026-07-16-deploy-fund-config-knobs-design.md`. Copy exact values from it.
- Initial fund-config values: **capital 100_000, target_vol 0.20, kelly_fraction 0.5**.
- Validation (fail loud at the boundary, never silently clamp): `capital > 0`, `0 < target_vol <= 1.0`, `0 < kelly_fraction <= 1.0`.
- **Graceful fallback**: when the `fund_config` table or row id=1 is absent, `read_fund_config` returns `FundConfig(capital=settings.paper_capital, target_vol=AllocationPolicy().target_vol, kelly_fraction=AllocationPolicy().kelly_fraction)` — byte-identical to today's nightly behaviour. A fresh DB or a failed read must NEVER change the allocation.
- **Partial-override upsert**: `write_fund_config(capital=X)` changes only `capital`, leaving `target_vol`/`kelly_fraction` at their current effective values (classic upsert bug to avoid).
- **Seed is insert-if-absent** (`id=1` PK collision → no-op); a re-run must never clobber an operator-set value.
- Marginal LOO gate is KEPT (do not weaken/remove it to force diversification). The ex-ante `K` stays computed from full-history vol (never trailing) — the anti-reactive invariant.
- No heavy DDL on the startup hot path (one tiny `create_all` table is fine; it is IF-NOT-EXISTS and a no-op after first run).
- `record_allocation(dsn, equity, store=None, ...)` with `store=None` and `policy=None` must remain byte-identical to today.
- Commit messages end with: `Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>`.

---

### Task 1: `fund_config` model + read/write/fallback/validation

**Files:**
- Modify: `src/fxhnt/adapters/persistence/cockpit_models.py` (add `FundConfigRow`)
- Create: `src/fxhnt/application/fund_config.py`
- Test: `tests/unit/test_fund_config.py`

**Interfaces:**
- Consumes: `CockpitBase` (cockpit_models), `get_settings()` (config), `AllocationPolicy` (allocation_policy).
- Produces:
  - `FundConfigRow` — ORM model, `__tablename__ = "fund_config"`, single-row (PK `id` fixed 1).
  - `@dataclass(frozen=True) FundConfig(capital: float, target_vol: float, kelly_fraction: float)`
  - `read_fund_config(dsn: str) -> FundConfig` (settings-fallback on absent table/row)
  - `write_fund_config(dsn: str, *, capital: float | None = None, target_vol: float | None = None, kelly_fraction: float | None = None) -> FundConfig` (partial-upsert + validation; returns new effective config)
  - `seed_fund_config(dsn: str) -> None` (insert-if-absent: 100_000 / 0.20 / 0.5)

- [ ] **Step 1: Write failing tests**

```python
# tests/unit/test_fund_config.py
import pytest
from fxhnt.application.fund_config import (
    FundConfig, read_fund_config, write_fund_config, seed_fund_config,
)
from fxhnt.adapters.persistence.cockpit_models import CockpitBase, FundConfigRow
from sqlalchemy import create_engine
from sqlalchemy.pool import StaticPool

def _dsn():
    return "sqlite://"

def _fresh_engine_dsn():
    # a dsn whose table has NOT been created yet (fallback path)
    return "sqlite://"

def test_read_fallback_when_table_absent(monkeypatch):
    # No migrate() called → table absent → settings/policy defaults
    from fxhnt.application.allocation_policy import AllocationPolicy
    from fxhnt.config import get_settings
    cfg = read_fund_config("sqlite://")
    assert cfg.capital == get_settings().paper_capital
    assert cfg.target_vol == AllocationPolicy().target_vol
    assert cfg.kelly_fraction == AllocationPolicy().kelly_fraction

def test_write_then_read_roundtrip():
    dsn = "sqlite:///./.pytest_fund_cfg_rt.db"
    import os
    if os.path.exists("./.pytest_fund_cfg_rt.db"): os.remove("./.pytest_fund_cfg_rt.db")
    write_fund_config(dsn, capital=250_000.0, target_vol=0.20, kelly_fraction=0.5)
    cfg = read_fund_config(dsn)
    assert (cfg.capital, cfg.target_vol, cfg.kelly_fraction) == (250_000.0, 0.20, 0.5)
    os.remove("./.pytest_fund_cfg_rt.db")

def test_partial_override_leaves_other_fields():
    dsn = "sqlite:///./.pytest_fund_cfg_po.db"
    import os
    if os.path.exists("./.pytest_fund_cfg_po.db"): os.remove("./.pytest_fund_cfg_po.db")
    write_fund_config(dsn, capital=100_000.0, target_vol=0.20, kelly_fraction=0.5)
    write_fund_config(dsn, capital=300_000.0)          # only capital
    cfg = read_fund_config(dsn)
    assert cfg.capital == 300_000.0
    assert cfg.target_vol == 0.20                       # unchanged
    assert cfg.kelly_fraction == 0.5                    # unchanged
    os.remove("./.pytest_fund_cfg_po.db")

@pytest.mark.parametrize("kw", [
    {"capital": 0.0}, {"capital": -5.0},
    {"target_vol": 0.0}, {"target_vol": 1.5},
    {"kelly_fraction": 0.0}, {"kelly_fraction": 2.0},
])
def test_validation_rejects(kw):
    dsn = "sqlite:///./.pytest_fund_cfg_val.db"
    import os
    if os.path.exists("./.pytest_fund_cfg_val.db"): os.remove("./.pytest_fund_cfg_val.db")
    with pytest.raises(ValueError):
        write_fund_config(dsn, **kw)
    if os.path.exists("./.pytest_fund_cfg_val.db"): os.remove("./.pytest_fund_cfg_val.db")

def test_seed_is_insert_if_absent():
    dsn = "sqlite:///./.pytest_fund_cfg_seed.db"
    import os
    if os.path.exists("./.pytest_fund_cfg_seed.db"): os.remove("./.pytest_fund_cfg_seed.db")
    seed_fund_config(dsn)
    assert read_fund_config(dsn) == FundConfig(100_000.0, 0.20, 0.5)
    write_fund_config(dsn, capital=500_000.0)
    seed_fund_config(dsn)                                # must NOT clobber
    assert read_fund_config(dsn).capital == 500_000.0
    os.remove("./.pytest_fund_cfg_seed.db")
```

- [ ] **Step 2: Run tests — expect ImportError/failures**

Run: `cd /home/jgrusewski/Work/fxhnt && python3 -m pytest tests/unit/test_fund_config.py -q`
Expected: FAIL (module/model not defined).

- [ ] **Step 3: Add the ORM model**

In `cockpit_models.py`, after the existing simple rows (match the `Mapped`/`mapped_column` style; import `Integer`, `Float`, `DateTime` are already imported — verify and add any missing):

```python
class FundConfigRow(CockpitBase):
    """Single-row (id=1) fund-level knobs — the SSOT for capital + risk appetite, set ONLY via
    `fxhnt fund-config` (never hand-edited), read by the nightly allocation. Absent row/table → the nightly
    falls back to settings/policy defaults (see application/fund_config.read_fund_config)."""
    __tablename__ = "fund_config"
    id: Mapped[int] = mapped_column(Integer, primary_key=True)   # always 1
    capital: Mapped[float] = mapped_column(Float)
    target_vol: Mapped[float] = mapped_column(Float)
    kelly_fraction: Mapped[float] = mapped_column(Float)
    updated_at: Mapped[dt.datetime] = mapped_column(DateTime)
```

- [ ] **Step 4: Write `application/fund_config.py`**

```python
"""The fund-level knobs SSOT: capital + risk appetite (target_vol, kelly_fraction) as a single-row DB record,
set ONLY via the `fxhnt fund-config` CLI, read by the nightly allocation. Absent table/row → settings/policy
defaults (byte-identical to the pre-fund_config nightly). No hand-editing — the CLI is the sole writer."""
from __future__ import annotations

import datetime as dt
from dataclasses import dataclass
from typing import Any

from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool

from fxhnt.adapters.persistence.cockpit_models import CockpitBase, FundConfigRow
from fxhnt.application.allocation_policy import AllocationPolicy
from fxhnt.config import get_settings

_ROW_ID = 1


@dataclass(frozen=True)
class FundConfig:
    capital: float
    target_vol: float
    kelly_fraction: float


def _defaults() -> FundConfig:
    p = AllocationPolicy()
    return FundConfig(capital=get_settings().paper_capital,
                      target_vol=p.target_vol, kelly_fraction=p.kelly_fraction)


def _engine(dsn: str):
    kw: dict[str, Any] = {"future": True}
    if dsn == "sqlite://" or (dsn.startswith("sqlite://") and ":memory:" in dsn):
        kw |= {"connect_args": {"check_same_thread": False}, "poolclass": StaticPool}
    return create_engine(dsn, **kw)


def _validate(capital: float | None, target_vol: float | None, kelly_fraction: float | None) -> None:
    if capital is not None and not (capital > 0):
        raise ValueError(f"capital must be > 0, got {capital}")
    if target_vol is not None and not (0 < target_vol <= 1.0):
        raise ValueError(f"target_vol must be in (0, 1.0], got {target_vol}")
    if kelly_fraction is not None and not (0 < kelly_fraction <= 1.0):
        raise ValueError(f"kelly_fraction must be in (0, 1.0], got {kelly_fraction}")


def read_fund_config(dsn: str) -> FundConfig:
    """Row id=1 as a FundConfig; settings/policy defaults if the table or row is absent (never raises on a
    fresh DB — the nightly must not break)."""
    try:
        eng = _engine(dsn)
        with Session(eng) as s:
            row = s.get(FundConfigRow, _ROW_ID)
    except Exception:   # noqa: BLE001 — absent table on a fresh DB → defaults
        return _defaults()
    if row is None:
        return _defaults()
    return FundConfig(capital=row.capital, target_vol=row.target_vol, kelly_fraction=row.kelly_fraction)


def write_fund_config(dsn: str, *, capital: float | None = None, target_vol: float | None = None,
                      kelly_fraction: float | None = None) -> FundConfig:
    """Partial upsert of row id=1: overlay only the provided fields on the current effective config, validate,
    write. Returns the new effective FundConfig. Creates the table if absent."""
    if capital is None and target_vol is None and kelly_fraction is None:
        raise ValueError("write_fund_config: provide at least one of capital/target_vol/kelly_fraction")
    _validate(capital, target_vol, kelly_fraction)
    eng = _engine(dsn)
    CockpitBase.metadata.create_all(eng, tables=[FundConfigRow.__table__])
    cur = read_fund_config(dsn)
    new = FundConfig(
        capital=cur.capital if capital is None else float(capital),
        target_vol=cur.target_vol if target_vol is None else float(target_vol),
        kelly_fraction=cur.kelly_fraction if kelly_fraction is None else float(kelly_fraction),
    )
    with Session(eng) as s:
        s.merge(FundConfigRow(id=_ROW_ID, capital=new.capital, target_vol=new.target_vol,
                              kelly_fraction=new.kelly_fraction,
                              updated_at=dt.datetime.now(dt.UTC).replace(tzinfo=None)))
        s.commit()
    return new


def seed_fund_config(dsn: str) -> None:
    """Insert row id=1 with the initial knobs (100k / 0.20 / 0.5) IF ABSENT. Never clobbers an operator-set
    value (idempotent)."""
    eng = _engine(dsn)
    CockpitBase.metadata.create_all(eng, tables=[FundConfigRow.__table__])
    with Session(eng) as s:
        if s.get(FundConfigRow, _ROW_ID) is not None:
            return
        s.add(FundConfigRow(id=_ROW_ID, capital=100_000.0, target_vol=0.20, kelly_fraction=0.5,
                            updated_at=dt.datetime.now(dt.UTC).replace(tzinfo=None)))
        s.commit()
```

- [ ] **Step 5: Run tests — expect PASS**

Run: `cd /home/jgrusewski/Work/fxhnt && python3 -m pytest tests/unit/test_fund_config.py -q`
Expected: PASS (all).

- [ ] **Step 6: Commit**

```bash
git add src/fxhnt/adapters/persistence/cockpit_models.py src/fxhnt/application/fund_config.py tests/unit/test_fund_config.py
git commit -m "feat(fund-config): single-row fund_config SSOT (read/write/fallback/validate/seed)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

### Task 2: `fxhnt fund-config` CLI + wire seed into `migrate-cockpit`

**Files:**
- Modify: `src/fxhnt/cli.py` (add `fund-config` typer sub-app or two commands; wire `seed_fund_config` into `migrate_cockpit`)
- Test: `tests/unit/test_fund_config_cli.py`

**Interfaces:**
- Consumes: `read_fund_config`, `write_fund_config`, `seed_fund_config` (Task 1), `typer`, `get_settings().operational_dsn`.
- Produces: CLI `fxhnt fund-config show` and `fxhnt fund-config set [--capital] [--target-vol] [--kelly]`.

- [ ] **Step 1: Write failing tests** (use typer's `CliRunner`)

```python
# tests/unit/test_fund_config_cli.py
import os
from typer.testing import CliRunner
from fxhnt.cli import app
from fxhnt.application.fund_config import read_fund_config

runner = CliRunner()

def _isolate(monkeypatch, path):
    if os.path.exists(path): os.remove(path)
    monkeypatch.setattr("fxhnt.cli.get_settings", lambda: _S(f"sqlite:///{path}"))

class _S:
    def __init__(self, dsn): self.operational_dsn = dsn; self.paper_capital = 100_000.0

def test_set_then_show(monkeypatch):
    path = "./.pytest_fund_cli.db"; _isolate(monkeypatch, path)
    r = runner.invoke(app, ["fund-config", "set", "--capital", "250000", "--target-vol", "0.2", "--kelly", "0.5"])
    assert r.exit_code == 0, r.output
    assert read_fund_config(f"sqlite:///{path}").capital == 250_000.0
    r2 = runner.invoke(app, ["fund-config", "show"])
    assert r2.exit_code == 0
    assert "250000" in r2.output.replace(",", "") or "250000.0" in r2.output.replace(",", "")
    os.remove(path)

def test_set_rejects_bad_value(monkeypatch):
    path = "./.pytest_fund_cli_bad.db"; _isolate(monkeypatch, path)
    r = runner.invoke(app, ["fund-config", "set", "--target-vol", "1.5"])
    assert r.exit_code != 0
    if os.path.exists(path): os.remove(path)

def test_set_no_flags_errors(monkeypatch):
    path = "./.pytest_fund_cli_noflag.db"; _isolate(monkeypatch, path)
    r = runner.invoke(app, ["fund-config", "set"])
    assert r.exit_code != 0
    if os.path.exists(path): os.remove(path)
```

- [ ] **Step 2: Run — expect FAIL** (`fund-config` command absent)

Run: `cd /home/jgrusewski/Work/fxhnt && python3 -m pytest tests/unit/test_fund_config_cli.py -q`
Expected: FAIL.

- [ ] **Step 3: Implement the CLI** (typer sub-app `fund_config_app` registered on `app`)

```python
# in cli.py, near the other command groups
fund_config_app = typer.Typer(help="Fund-level knobs (capital + risk appetite) — the allocation SSOT.")
app.add_typer(fund_config_app, name="fund-config")

@fund_config_app.command("show")
def fund_config_show() -> None:
    """Print the effective fund knobs (capital, target_vol, kelly_fraction)."""
    from fxhnt.application.fund_config import read_fund_config
    cfg = read_fund_config(get_settings().operational_dsn)
    typer.echo(f"capital={cfg.capital:.2f} target_vol={cfg.target_vol:.4f} kelly_fraction={cfg.kelly_fraction:.4f}")

@fund_config_app.command("set")
def fund_config_set(
    capital: float = typer.Option(None, help="Fund capital (equity the allocation sizes against). > 0."),
    target_vol: float = typer.Option(None, "--target-vol", help="Annualised vol target (0, 1.0]."),
    kelly: float = typer.Option(None, "--kelly", help="Kelly fraction (0, 1.0]."),
) -> None:
    """Set one or more fund knobs (partial override; unspecified fields unchanged)."""
    from fxhnt.application.fund_config import write_fund_config
    try:
        cfg = write_fund_config(get_settings().operational_dsn, capital=capital,
                                target_vol=target_vol, kelly_fraction=kelly)
    except ValueError as e:
        typer.echo(f"error: {e}"); raise typer.Exit(code=1) from e
    typer.echo(f"set: capital={cfg.capital:.2f} target_vol={cfg.target_vol:.4f} kelly_fraction={cfg.kelly_fraction:.4f}")
```

Then in `migrate_cockpit()` (after `ForwardNavRepo(dsn).migrate()`), add:

```python
    from fxhnt.application.fund_config import seed_fund_config
    seed_fund_config(dsn)
```

- [ ] **Step 4: Run — expect PASS**

Run: `cd /home/jgrusewski/Work/fxhnt && python3 -m pytest tests/unit/test_fund_config_cli.py -q`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add src/fxhnt/cli.py tests/unit/test_fund_config_cli.py
git commit -m "feat(fund-config): fxhnt fund-config show|set CLI + seed on migrate-cockpit

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

### Task 3: nightly reads `fund_config` → equity + `AllocationPolicy`

**Files:**
- Modify: `src/fxhnt/application/allocation_ingest.py` (optional `policy` param on `record_allocation`)
- Modify: `src/fxhnt/adapters/orchestration/assets.py` (`cockpit_forward`: read fund_config → equity + policy)
- Test: `tests/unit/test_record_allocation_policy.py`

**Interfaces:**
- Consumes: `read_fund_config` (Task 1), `resolve_allocation_policy` / `AllocationPolicy` (allocation_policy), `dataclasses.replace`.
- Produces: `record_allocation(dsn, equity, *, store=None, spread_bps=None, unlock_events=None, policy=None)` — `policy=None` keeps today's resolution (byte-identical); a provided `AllocationPolicy` is used verbatim for both the honest and nav paths.

- [ ] **Step 1: Inspect** how `record_allocation` currently builds its policy (`resolve_allocation_policy`) so the `policy=None` default is provably identical. Read `allocation_ingest.py` lines around the policy build and `allocation_policy.resolve_allocation_policy`.

- [ ] **Step 2: Write failing tests**

```python
# tests/unit/test_record_allocation_policy.py
import dataclasses
from fxhnt.application.allocation_policy import AllocationPolicy, resolve_allocation_policy

def test_policy_none_matches_resolved_default(monkeypatch):
    # record_allocation(policy=None) must build the SAME policy resolve_allocation_policy() returns today.
    # Assert via the helper the code will use: a None arg resolves to resolve_allocation_policy().
    from fxhnt.application import allocation_ingest as ai
    resolved = ai._effective_policy(None)          # helper introduced in Step 3
    assert resolved == resolve_allocation_policy()

def test_policy_passthrough():
    from fxhnt.application import allocation_ingest as ai
    p = dataclasses.replace(resolve_allocation_policy(), target_vol=0.20, kelly_fraction=0.5)
    assert ai._effective_policy(p) is p
```

- [ ] **Step 3: Implement.** In `allocation_ingest.py` add a tiny helper and thread it:

```python
def _effective_policy(policy):
    from fxhnt.application.allocation_policy import resolve_allocation_policy
    return resolve_allocation_policy() if policy is None else policy
```

Add `policy=None` to the `record_allocation` signature and replace its internal `resolve_allocation_policy()` call with `pol = _effective_policy(policy)`, using `pol` wherever the policy is consumed (compute_allocation / target_capital / hashing). Verify every prior use of the resolved policy now reads `pol`.

- [ ] **Step 4: Wire `cockpit_forward`** (`assets.py` ~line 1073). Replace `equity = get_settings().paper_capital` with:

```python
    from fxhnt.application.fund_config import read_fund_config
    import dataclasses as _dc
    from fxhnt.application.allocation_policy import resolve_allocation_policy
    _cfg = read_fund_config(dsn)
    equity = _cfg.capital
    _policy = _dc.replace(resolve_allocation_policy(), target_vol=_cfg.target_vol,
                          kelly_fraction=_cfg.kelly_fraction)
```

and pass `policy=_policy` to BOTH `record_allocation(...)` calls (the honest-store path and the nav-fallback path).

- [ ] **Step 5: Run tests — expect PASS**

Run: `cd /home/jgrusewski/Work/fxhnt && python3 -m pytest tests/unit/test_record_allocation_policy.py tests/unit/test_compute_allocation.py -q`
Expected: PASS (new + existing allocation tests still green).

- [ ] **Step 6: Commit**

```bash
git add src/fxhnt/application/allocation_ingest.py src/fxhnt/adapters/orchestration/assets.py tests/unit/test_record_allocation_policy.py
git commit -m "feat(fund-config): nightly allocation reads fund_config for equity + policy (target_vol/kelly)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

### Task 4: per-sleeve allocation — registry + provider

**Files:**
- Modify: `src/fxhnt/registry.py` (`xsfunding`/`unlock` → `promote_on_pass: True`; `bybit_4edge` → `tier: research`)
- Modify: `src/fxhnt/application/allocation_honest_inputs.py` (crypto-sleeve generalization)
- Test: `tests/unit/test_allocation_honest_inputs.py` (extend existing if present, else create)

**Interfaces:**
- Consumes: `_sleeve_series` (existing, works for any sleeve), `honest_allocation_inputs` (existing signature unchanged).
- Produces: `_CRYPTO_DEPLOY_SIDS = frozenset({"positioning", "xsfunding", "unlock"})`; `_deploy_series` returns the per-sleeve honest series for each of those three (no `bybit_4edge` branch in the allocation path).

- [ ] **Step 1: Write failing tests** (a fake store returning deterministic per-sleeve weights/turnover; assert xsfunding/unlock are sized as sleeves, and bybit_4edge yields no series in the allocation path)

```python
# tests/unit/test_allocation_honest_inputs.py  (extend / create)
from fxhnt.application.allocation_honest_inputs import (
    honest_allocation_inputs, _CRYPTO_DEPLOY_SIDS,
)

def test_crypto_deploy_sids_are_the_three_sleeves():
    assert _CRYPTO_DEPLOY_SIDS == frozenset({"positioning", "xsfunding", "unlock"})

def test_xsfunding_and_unlock_get_a_series_not_equity_default(fake_store, spread_bps, unlock_events):
    # fake_store yields non-empty weights for xsfunding & unlock
    returns, ceilings = honest_allocation_inputs(
        fake_store, spread_bps, unlock_events,
        deploy_sids={"xsfunding", "unlock"}, target_aum=35_000.0, aums=[35_000.0, 350_000.0])
    assert "xsfunding" in returns and returns["xsfunding"]       # sleeve-sized, ISO-keyed
    assert "unlock" in returns and returns["unlock"]
    # ceilings present (not the equity default 10M sentinel for these crypto sleeves)
    assert ceilings["xsfunding"] != 10_000_000.0

def test_bybit_4edge_is_not_a_crypto_deploy_sid():
    assert "bybit_4edge" not in _CRYPTO_DEPLOY_SIDS
```

(Reuse the existing test's `fake_store`/fixtures if the test file already exists; otherwise build a minimal fake exposing `read_panel` + the `sleeve_weights_and_contributions` path — mirror the pattern already used in the honest-reference tests.)

- [ ] **Step 2: Run — expect FAIL**

Run: `cd /home/jgrusewski/Work/fxhnt && python3 -m pytest tests/unit/test_allocation_honest_inputs.py -q`
Expected: FAIL.

- [ ] **Step 3: Registry edits** (`registry.py`):
  - `xsfunding` entry: add `"promote_on_pass": True,` (top-level key, sibling of `tier`).
  - `unlock` entry: add `"promote_on_pass": True,`.
  - `bybit_4edge` entry: change `"tier": "deploy"` → `"tier": "research"`. Leave everything else (gate_spec, definition, inception_t0) unchanged. Update the entry's leading comment to note it is now a tracked REFERENCE aggregate (allocation runs per-sleeve; see the 2026-07-16 spec).

- [ ] **Step 4: Provider edits** (`allocation_honest_inputs.py`):
  - `_CRYPTO_DEPLOY_SIDS = frozenset({"positioning", "xsfunding", "unlock"})`.
  - `_deploy_series`: replace the positioning/bybit_4edge branches with: `if sid in _CRYPTO_DEPLOY_SIDS: return _sleeve_series(store, sid, spread_bps, unlock_events, aum=aum, participation_cap=participation_cap, impact_k=impact_k)` then `return {}`.
  - Keep `_book_series` / `_BOOK_SLEEVES` (still used by the honest-reference REPORT), but remove the `bybit_4edge` dispatch from `_deploy_series`. Update the module docstring's deploy→honest mapping to the three sleeves + note the book is no longer an allocation target.

- [ ] **Step 5: Run — expect PASS** (and re-run the honest-reference tests to confirm `_book_series` still works)

Run: `cd /home/jgrusewski/Work/fxhnt && python3 -m pytest tests/unit/test_allocation_honest_inputs.py tests/unit/ -k "honest or allocation" -q`
Expected: PASS.

- [ ] **Step 6: Commit**

```bash
git add src/fxhnt/registry.py src/fxhnt/application/allocation_honest_inputs.py tests/unit/test_allocation_honest_inputs.py
git commit -m "feat(alloc): per-sleeve crypto allocation (promote xsfunding/unlock; bybit_4edge->reference)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```

---

### Task 5: full-suite green + book-family sanity

**Files:** none (verification task)

- [ ] **Step 1: Run the allocation + fund-config + honest-reference unit suites**

Run: `cd /home/jgrusewski/Work/fxhnt && python3 -m pytest tests/unit -k "fund_config or allocation or honest or compute_allocation" -q`
Expected: all PASS.

- [ ] **Step 2: Grep for any remaining hard reference to `bybit_4edge` as a deploy-tier allocation target** (e.g. code that assumed the book is allocated). Fix or note any found.

Run: `cd /home/jgrusewski/Work/fxhnt && grep -rn "bybit_4edge" src/fxhnt/application/ src/fxhnt/adapters/ | grep -iv "exec\|levered\|ref\|comment"`
Expected: no code that *allocates* to `bybit_4edge` (execution wiring is out of scope / suspended).

- [ ] **Step 3: Commit any fixes** from Step 2 (if none, skip).

---

## Post-implementation (controller-run, NOT a subagent task — in-cluster + local)

These are the spec's §5 falsification gate + cockpit verification. The controller runs them after the branch is green, per the user's "run queries in the cluster" and "verify cockpit locally" directives:

1. **Deploy** (git-sync nightly picks up master) + `fxhnt migrate-cockpit` (creates `fund_config`, seeds 100k/0.20/0.5) in-cluster.
2. **G1/G2/G3** — in-cluster diagnostic: dump `survivors_after_marginal_prune`, the per-sleeve weights, deployed gross $ and %, and confirm `K` uses full-history vol. Report the numbers (do NOT override the gate).
3. **Cockpit local verify** — `bybit_4edge`-as-reference renders sanely (deploy section shows the sleeves, no broken headline).
4. Report the honest verdict (per-sleeve deploys more, or it concentrates to positioning + the vol-target is the lever).

## Self-Review

- **Spec coverage:** §4a→T1, §4b→T2, §4c→T3, §4d→T4, §4e→T1(seed)+T2(wire), §5→post-impl gate, §7 risks→Global Constraints + T4 + post-impl. Covered.
- **Placeholders:** none — every step has concrete code/commands.
- **Type consistency:** `FundConfig`, `read_fund_config`, `write_fund_config`, `seed_fund_config`, `_effective_policy`, `_CRYPTO_DEPLOY_SIDS` used identically across tasks.
