# 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).
