# worldtree-sdk cutover — SLICE-5 COMPLETE (characters + me/capabilities/models) `[2026-07-19]` Slice 5 of 7 of the worldtree-sdk consumer cutover (issue #20; contract `docs/contracts/worldtree_sdk_cutover.contract.md`). Full House Code Discipline end-to-end: contract slice-notes → TDD → LIVE smoke → heid-code-review → fixup → heid-bug-hunt → fixup. Both heid panels cleared. Suite **488 green**. ## Commits (tags v0.21.16–.18, on `main`, not-yet-pushed) - **`deab762`** feat — the six routes onto `ratatoskr.wt`, hand-rolled deleted. - **`d86d6df`** fix — heid-code-review fixups (CLI presenter degrade-not-crash). - **`4e20030`** fix — heid-bug-hunt fixups (CLI open-world container-type hardening). ## What migrated `get_me` / `get_capabilities` / `list_character_models` / `create_character` / `get_character_state` / `delete_character` moved off the hand-rolled httpx wrappers onto `client.me.get()` / `client.capabilities.get()` / `client.models.available_for_characters()` / `client.characters.create|state|delete`. All six are **open-world reads/acks returned verbatim**; none carries a discriminated SDK error, so each maps any `ApiError` → the `SessionApiFailed` default — **NO new § Error map rows** (exact parity with the retired path, which never discriminated a status/code on these routes). **CLI-only rewire** — `--whoami` (me + capabilities) and `--characters` (models → create → state → delete) build a `wt.build_client` over the injected `_probe_client` transport and catch `wt.SessionApiFailed` + `ConnectFailed`. **No web-server caller** for any of these six routes. Deleted the six hand-rolled `sessions.py` wrappers (net **−5 mypy `no-any-return`** errors); `endpoint_for_plane` + `get_session_bifrost` (slice-6) + the exception classes stay. Retired the matching `test_sessions.py` classes (`TestGetMe`, `TestGetCapabilities`, `TestTransientCharacters`); kept `TestEndpointForPlane` + `TestGetSessionBifrost`. ## Decisions made at TDD (contract § slice-5 notes) - **`create_character` omits `state` when None** — SDK-idiomatic inline literal (per branch, to type-check against the SDK's `CreateCharacterInput` TypedDict without importing its private `_types`); server-equivalent to the retired explicit `state: null` (Worldtree's field defaults None either way). The only wire-shape change; the sole call-site never sets state. - **`delete_character` returns the SDK's open ACK verbatim** (`-> Mapping | None`, not normalized to the hand-rolled `None`; 204 → None). The CLI ignores it. ## LIVE SMOKE (:8081/b128, `WORLDTREE_API_KEY`, INV-CUT-5 / DEC-4 cleared) Drove both probes end-to-end through the CLI (`python -c "from ratatoskr.cli import main; main([...])"` — the `ratatoskr` console script isn't on PATH here; `python -m ratatoskr.cli` imports without calling `main`, no `__main__` guard). `--whoami` rendered real identity (user_id ratatoskr, tier user, scopes incl. `character.*`, key_id c990f0be) + `ephemeral_template echo`. `--characters` drove the full lifecycle: `char-rp` catalog → `created char_…` → `state pad=[0.234,−0.136,0.065]` read-back → `deleted`. Observed real success, not merely non-crash. ## heid-code-review (thread 01KXXRN50K…) — 2 fixups Panel: **Gróa + Regin zero** (adapter/route-map/error-map faithful); **Hulda** flagged 2 source-confirmed CLI open-world-presenter crash holes + a live-smoke test-gap. Both holes fixed (the null/element layer): - `_format_whoami` `scopes`: `', '.join(me.get('scopes', []))` crashes on a present-null `scopes` (`.get(k, [])` returns None, not the default) or a non-string element. The contract names `_format_whoami` the degrade-not-crash exemplar — the cited exemplar had an un-hardened line (`allowed_roles` was hardened in slice-4, `scopes` was not). - `_characters_probe` model `items`: the slice-5 `or []` guarded the list-level null but not each entry (`[None]`/`["x"]`/`[{"name":123}]`). Test-gap (live-smoke not in the file set) → accept (it WAS run + recorded). ## heid-bug-hunt (thread 01KXXS9S45…) — 3 fixups + 1 accept + 1 dismiss Cold spec-free diff-scoped panel over the post-code-review-fixup diff. Adapter + route-map + `ConnectFailed`-at-call-sites **sound against the declared invariants (all arms agree)**. 4 real findings, all CLI open-world paths — the **container-type layer BELOW** the null/element holes the code-review had just fixed (the code comments cite the CR; the two consults were firewalled from each other and converged independently): - **[bug, fixed] non-iterable `scopes`/`allowed_roles`** — `{"scopes": 123}` → `123 or [] == 123` → `for s in 123` TypeError. New `_display_seq(value)` helper degrades any non-list (scalar / bare string / null / absent) to empty; applied to both. - **[bug, fixed] non-iterable `items`** — `{"items": 123}`, same class. Guard `models` is a Mapping AND `items` is a list before iterating. - **[robustness, fixed] non-mapping top-level `created`/`state`** — a non-mapping SDK passthrough (`created=[...]`) → `.get` AttributeError. `isinstance(_, Mapping)` guard → clean exit-20 abort / `pad=None`. - **[robustness, ACCEPTED] the probe leaks its transient character on a mid-lifecycle failure** — create → state → delete linear, no `finally`. PRE-EXISTING (retired probe had the identical structure — "cutover did not worsen it"), TTL-bounded, one-shot diagnostic; a `try/finally` would swallow a happy-path delete-failure (delete is both teardown and a tested step). Gróa + Heid concur accept is defensible. Documented in contract § slice-5 notes. - **[DISMISSED] `sessions.py` dropped `get_me`/etc.** (Hulda, caller-contract) — the intended DEC-3 no-backwards-compat migration (all in-repo callers rewired same-diff); Heid labels it intended-surface-change, not a defect. **Regin BH calibration note:** Regin (glm-5.2 non-reasoning) found only the leak, missed the 3 crash paths, and self-graded "0 confirmed" on a control-flow-guaranteed finding — consistent with the crystallized **Regin-unreliable-on-bug-hunts** pattern (its code-review work this session was reliable). Gróa was the BH standout. ## The cumulative lesson (foot-gun for slices 6-7) Open-world SDK reads need degrade-not-crash guarding at **THREE levels**, and the two heid lenses caught different ones: the CODE-REVIEW (conformance) caught the null/element layer; the cold BUG-HUNT (robustness) caught the container-type layer below it. Run BOTH — they are complementary, not redundant. The three levels: 1. **container-type** — the field value may be a truthy non-iterable scalar (`123`) or a bare string; `or []` only catches null/absent. Guard `isinstance(_, (list, tuple))`. 2. **element-type** — each entry may be a non-mapping; guard `isinstance(m, dict)`. 3. **top-level-mapping** — the whole read may be a non-mapping passthrough; guard `isinstance(_, Mapping)` before any `.get`. See also [[2026-07-19-worldtree-sdk-cutover-slice-4-complete]] (the slice-4 arc + the `-m` double-module class-identity foot-gun).