`[2026-07-19]` **worldtree-sdk cutover SLICE-4 COMPLETE + committed (`c62b4ee`+`aed9429`+`477d98f`, v0.21.13–.15, 475 green).**

Agents/Tier-3 family onto the `ratatoskr.wt` adapter + the `model`→`role` fold (scope B),
full House Code Discipline end-to-end (contract error-map → TDD → rewire → delete → live
smoke → coverage re-anchor → both heid gates → fixups).

**What moved onto the SDK** (`client.agents.*`, open-world dicts, errors mapped by
route+(status,error_code) per INV-CUT-2): `list_agents` (`agents.list`),
`get_persona_state` (`agents.persona_state`; 404 persona_not_configured /
404 agent_not_available / 403 auth_scope_denied dual-key), `define_agent`
(`agents.define`; 429→Tier3QuotaExceeded(retry_after=0) status-only, 403
tier3_user_id_unsupported, 422 layer_deferred), `patch_agent` (`agents.patch`; bare-404→
Tier3AgentNotFound, 422 field_not_mutable), `delete_agent` (`agents.delete`; bare-404).
Rewired the `python -m ratatoskr.tier3` CLI + the web `_agents_endpoint` /
`_persona_state_endpoint`. DELETED the hand-rolled `sessions.list_agents` /
`get_persona_state` / `AgentInfo` and `tier3.define/patch/delete_agent` /
`Tier3AgentInfo` / parse+extract helpers.

**model→role fold:** define/patch response echoes `role` (spec 1.2 / b128), read off the
open-world dict; `LocalAgentEntry.model`→`.role`, local-index `_SCHEMA_VERSION` 1→2 (old
index discarded, no-compat). `project_tier3_agents_model_to_role_pending` is RESOLVED by
this — the deferred scope-B work landed here (doing it standalone would have been throwaway).

**KEY SLICE-4 FOOT-GUNS (for slices 5-7 + any tier3 work):**
- **`python -m` double-module exception identity.** Running tier3 as `__main__` while
  `wt` imports `ratatoskr.tier3` bound TWO copies of each `Tier3*` class → a raised
  `Tier3AgentNotFound` escaped the CLI's `except` as an uncaught traceback (exit 1, not
  the mapped exit 20). Unit tests call `main()` in-process (module is `ratatoskr.tier3`,
  not `__main__`) so they NEVER hit the split — the LIVE SMOKE caught it. Fix: the `Tier3*`
  exceptions live in `sessions.py` (never run as `__main__`) → single class identity, and
  it removed the `wt→tier3` import edge. Rule: caller-semantic exceptions the adapter
  raises + a `-m` CLI catches must NOT live in the `-m` module.
- **Open-world presenters must degrade, never crash.** The heid-bug-hunt panel (5/5/5, two
  3/3 crash sites) caught the CLI (`_run_define`/`_run_patch`) and web (`_agents_endpoint`)
  HARD-INDEXING the open-world dicts (`info["agent_id"]`, `{a["agent_id"] for a in
  upstream}`) → KeyError/TypeError on a partial/drifted 2xx response (incl. `system_prompt:
  null` → `None.splitlines()` AttributeError). The wt tests + live smoke used FULL server
  dicts, so it never surfaced. Fix: `_str_field` (absent/null/non-str → default) in the
  CLI; well-formed-mapping filter in the web endpoint; a no-agent_id 2xx → controlled exit
  20. The invariant "open-world reads degrade, never crash the presenter" must hold at
  EVERY presenter, not just the adapter.

**Both heid gates cleared:**
- **code-review (panel, zero adapter/error-map/model→role drift):** 3 fixups (v0.21.14,
  `aed9429`) — persona endpoint now catches `wt.SessionApiFailed` (parity with 3 sibling
  endpoints; a latent PRE-cutover gap, NOT a slice-4 regression); dual-key NEGATIVE tests
  for define/patch + flat-`field` test; contract documents the `":" in agent_id` PRE.
- **bug-hunt (panel 5/5/5, no false positives):** 3 fixups (v0.21.15, `477d98f`) — the two
  open-world presenter crash sites above + `_error_field_from_body` type-checks `field` is
  str. HELD (contract-intended, Heid-confirmed): the 429→quota / bare-404→not-found
  status-only maps (the arms flagged them spec-free; the § Error map specifies them; the
  SDK's ApiError floor drops Retry-After so retry_after=0 is canonical). Dual-keying
  define's 429 for full row consistency is an available tightening (contract amendment),
  surfaced not applied.

**LIVE-SMOKE on personal :8081 (b128):** valid agent roles are `thoughtful-character` /
`character` / `assistant` (NOT the `/models/available-for-characters` `char-rp` — that's a
character-model, a different vocab; define 422s on it). define→patch→list(6 agents:
forseti/lofn/mask/mimir/vili/…)→persona_state(→PersonaNotConfigured mapped)→delete→index
empty; non-existent-id patch via `-m` → `[agent_not_found]` exit 20 (double-module fix
proven). Throwaway `ratatoskr:slice4-smoke` deleted, server left clean.

**NEXT = slice-5** (characters + me/capabilities/models — the remaining consumer reads);
then slice-6 (admin: `stream_admin_events` + `get_session_bifrost`, admin_auth), slice-7
(teardown: delete residual hand-rolled, drop `httpx-sse`, retire contracts #2/#15, MINOR
bump per DEC-6 w/ operator approval). Consumer layer ONLY; Bifrost provider planes untouched.
