diff --git a/.planning/phases/01-matrix-qa-polish/01-RESEARCH.md b/.planning/phases/01-matrix-qa-polish/01-RESEARCH.md
new file mode 100644
index 0000000..3ac72d2
--- /dev/null
+++ b/.planning/phases/01-matrix-qa-polish/01-RESEARCH.md
@@ -0,0 +1,528 @@
+# Phase 1: Matrix QA & Polish — Research
+
+**Researched:** 2026-04-02
+**Domain:** matrix-nio AsyncClient — Space+rooms architecture, OutgoingUI text rendering, !yes/!no confirmation flow
+**Confidence:** HIGH (all critical APIs verified against the installed library)
+
+---
+
+
+## User Constraints (from CONTEXT.md)
+
+### Locked Decisions
+
+- **D-01:** Space+rooms — единственная поддерживаемая модель. DM-first убрать.
+- **D-02:** При первом invite бот создаёт Space `Lambda — {display_name}`, внутри — первую комнату `Чат 1`. Приглашает пользователя.
+- **D-03:** `!new [name]` создаёт новую комнату внутри Space пользователя, приглашает его туда.
+- **D-04:** `!archive` выводит комнату из Space (не удаляет).
+- **D-05:** Маппинг `room_id → space_id` + `room_id → chat_id` хранится в SQLite через `adapter/matrix/store.py`.
+- **D-06:** Реакции (`👍`/`❌`) — убрать полностью. `OutgoingUI` с кнопками рендерится как текст + `!yes` / `!no`.
+- **D-07:** Когда агент запрашивает подтверждение, бот пишет текстовое сообщение с описанием и подсказкой: `Ответьте !yes для подтверждения или !no для отмены.`
+- **D-08:** `!yes` / `!no` работают в текущей комнате. Состояние ожидания подтверждения хранится per (user_id, room_id).
+- **D-09:** Все команды работают из любой комнаты Space — нет выделенной комнаты «Настройки».
+- **D-10:** Команды: `!new [name]`, `!chats`, `!rename `, `!archive`, `!skills`, `!soul`, `!safety`, `!settings`, `!yes`, `!no`.
+- **D-11:** `!start` — не нужен, онбординг через invite flow.
+- **D-12:** `!settings` — read-only дашборд: одно сообщение со статусом всего (скиллы, soul, safety, активные чаты). Ничего не меняет.
+- **D-13:** Изменения через субкоманды: `!skills`, `!skill on/off `, `!soul`, `!soul name/style/priority/reset `, `!safety`, `!safety on/off `.
+- **D-14:** Каждая команда без аргументов (`!skills`, `!soul`, `!safety`) выводит текущее состояние + подсказку как менять.
+
+### Claude's Discretion
+
+- Формат текста в Matrix (plain text vs markdown) — на усмотрение, Matrix клиенты рендерят markdown
+- Структура invite-сообщения в новом Space — на усмотрение, главное: приветствие + список команд
+- Обработка ошибок matrix-nio (room_create fail, join fail) — логировать + ответить пользователю
+
+### Deferred Ideas (OUT OF SCOPE)
+
+- Комната «Настройки» как отдельная закреплённая комната — решили не делать в Phase 1
+- E2EE / python-olm — инфраструктурный трек, вне scope
+- Space discovery (бот находит существующий Space при повторном invite) — Phase 2+
+- Attachment handling (m.file, m.image, m.audio) — Phase 2+
+
+
+---
+
+## Summary
+
+Phase 1 переписывает Matrix адаптер с DM-first на Space+rooms модель, убирает реакции в пользу `!yes`/`!no`, и реализует все команды управления. Большая часть бизнес-логики уже работает через `core/handlers/` и `adapter/matrix/handlers/settings.py`. Главная работа — в трёх точках: `handle_invite` (создание Space + двух комнат), `make_handle_new_chat` (добавление комнаты в Space), и `send_outgoing` (убрать реакции, добавить pending-state для `!yes`/`!no`).
+
+Текущее состояние: 97 тестов зелёные. Для "96+ зелёных" после рефакторинга нужно обновить 3 существующих теста (они проверяют DM-поведение и реакции) и добавить ~12 новых тестов на Space-сценарии. Итого целевой range — 106–110 тестов.
+
+Критическая деталь: `AsyncClient.room_create` принимает `space=True` (булевый параметр, не `room_type="m.space"`) для создания Space. Добавление дочерней комнаты — через `room_put_state` на Space с event_type `m.space.child` и state_key = child room_id. Это проверено против установленной версии matrix-nio.
+
+**Primary recommendation:** Реализовать в трёх независимых задачах Codex: (1) invite flow — Space+rooms creation, (2) send_outgoing — убрать реакции, добавить pending-confirm store, (3) обновить тесты под новое поведение.
+
+---
+
+## Standard Stack
+
+### Core
+| Library | Version | Purpose | Why Standard |
+|---------|---------|---------|--------------|
+| matrix-nio | установлена (проверено: `space=True` параметр присутствует) | Matrix async клиент — room_create, room_put_state, room_invite, join | Единственный maintained async Python Matrix клиент |
+| structlog | уже используется | Логирование | Уже в проекте |
+| pytest-asyncio | уже используется | Async тесты | Уже в проекте |
+
+**Версию matrix-nio не нужно менять.** Установленная версия поддерживает `space=True` в `room_create` и `room_put_state` для state events.
+
+---
+
+## Architecture Patterns
+
+### Паттерн 1: Создание Space + первой комнаты (invite flow)
+
+**Что:** При первом invite бот делает 5 последовательных API вызовов — создание Space, создание chat-комнаты, линковка child→Space, приглашение пользователя в обе, запись в store.
+
+**Verified API** (из installed matrix-nio):
+
+```python
+# 1. Создать Space
+space_resp = await client.room_create(
+ name=f"Lambda — {display_name}",
+ space=True, # <-- булевый флаг, не room_type
+ visibility="private",
+ is_direct=False,
+)
+# space_resp.room_id — строка
+
+# 2. Создать первую chat-комнату
+chat_resp = await client.room_create(
+ name="Чат 1",
+ visibility="private",
+ is_direct=False,
+)
+# chat_resp.room_id — строка
+
+# 3. Добавить комнату в Space как child
+# state_key = room_id дочерней комнаты
+await client.room_put_state(
+ room_id=space_resp.room_id,
+ event_type="m.space.child",
+ content={
+ "via": [homeserver_domain], # например "matrix.org"
+ },
+ state_key=chat_resp.room_id,
+)
+
+# 4. Пригласить пользователя в Space и в chat-комнату
+await client.room_invite(space_resp.room_id, matrix_user_id)
+await client.room_invite(chat_resp.room_id, matrix_user_id)
+
+# 5. Записать в store
+await set_user_meta(store, matrix_user_id, {
+ "space_id": space_resp.room_id,
+ "next_chat_index": 2, # C1 уже занят
+})
+await set_room_meta(store, chat_resp.room_id, {
+ "room_type": "chat",
+ "chat_id": "C1",
+ "display_name": "Чат 1",
+ "matrix_user_id": matrix_user_id,
+ "space_id": space_resp.room_id,
+})
+```
+
+**Важный gotcha:** Бот сам не вступает в Space (join). Он создаёт Space как владелец, поэтому уже является членом. `join` нужен только для входящей DM-комнаты (invite в существующую комнату). В новом flow: бот создаёт комнаты сам, поэтому `join` для Space и chat-комнаты не нужен.
+
+### Паттерн 2: Добавление новой комнаты (!new)
+
+```python
+async def handle_new_chat(...):
+ user_meta = await get_user_meta(store, event.user_id) or {}
+ space_id = user_meta.get("space_id")
+ if not space_id:
+ # Пользователь не прошёл invite flow — не должно случиться, но guard нужен
+ return [OutgoingMessage(chat_id=event.chat_id, text="Ошибка: Space не найден.")]
+
+ chat_id = await next_chat_id(store, event.user_id)
+ room_name = " ".join(event.args).strip() or f"Чат {chat_id}"
+
+ resp = await client.room_create(name=room_name, visibility="private", is_direct=False)
+ room_id = resp.room_id
+
+ homeserver = event.user_id.split(":")[1] # "@user:matrix.org" → "matrix.org"
+ await client.room_put_state(
+ room_id=space_id,
+ event_type="m.space.child",
+ content={"via": [homeserver]},
+ state_key=room_id,
+ )
+ await client.room_invite(room_id, event.user_id)
+ await set_room_meta(store, room_id, {
+ "room_type": "chat",
+ "chat_id": chat_id,
+ "display_name": room_name,
+ "matrix_user_id": event.user_id,
+ "space_id": space_id,
+ })
+```
+
+### Паттерн 3: Archive (!archive) — убрать из Space
+
+```python
+# Убрать child: поставить пустой content (или content без 'via')
+# Matrix spec: отправить m.space.child с пустым {} или без 'via' удаляет связь
+await client.room_put_state(
+ room_id=space_id,
+ event_type="m.space.child",
+ content={}, # пустой content = удалить child relationship
+ state_key=room_id, # room_id архивируемой комнаты
+)
+```
+
+Confidence: MEDIUM — Matrix spec говорит что пустой content убирает child, но поведение Element может варьироваться. Альтернатива: оставить room_put_state с `{"via": []}` (пустой массив).
+
+### Паттерн 4: OutgoingUI → текст + !yes/!no (без реакций)
+
+**Что убрать:**
+- `_button_action_to_reaction` в `bot.py` — удалить целиком
+- Блок `for button in event.buttons: reaction = _button_action_to_reaction(...)` — удалить
+- `ReactionEvent` callback (`on_reaction` + `client.add_event_callback`) — удалить
+- `from_reaction` в converter — оставить (используется для skill-reactions), но skill-reaction инфраструктура тоже под вопросом (D-06 убирает реакции полностью)
+
+**Что добавить в `send_outgoing` для `OutgoingUI`:**
+```python
+if isinstance(event, OutgoingUI):
+ lines = [event.text, ""]
+ for button in event.buttons:
+ lines.append(f"• {button.label}")
+ lines += ["", "Ответьте !yes для подтверждения или !no для отмены."]
+ body = "\n".join(lines)
+ await client.room_send(room_id, "m.room.message", {"msgtype": "m.text", "body": body})
+ # Сохранить pending state per (user_id, room_id)
+ await set_pending_confirm(store, user_id=???, room_id=room_id, action_id=???)
+```
+
+**Проблема:** `send_outgoing` сейчас не знает `user_id` — только `room_id`. Для сохранения pending state нужен либо рефакторинг сигнатуры, либо хранение pending по `room_id` (без user_id — достаточно, т.к. room_id уникален для конкретного пользователя в Space модели).
+
+### Паттерн 5: Pending confirm state
+
+```python
+# Новые helpers в adapter/matrix/store.py
+PENDING_CONFIRM_PREFIX = "matrix_pending_confirm:"
+
+async def get_pending_confirm(store, room_id: str) -> dict | None:
+ return await store.get(f"{PENDING_CONFIRM_PREFIX}{room_id}")
+
+async def set_pending_confirm(store, room_id: str, meta: dict) -> None:
+ await store.set(f"{PENDING_CONFIRM_PREFIX}{room_id}", meta)
+
+async def clear_pending_confirm(store, room_id: str) -> None:
+ await store.delete(f"{PENDING_CONFIRM_PREFIX}{room_id}")
+```
+
+`!yes`/`!no` уже конвертируются в `IncomingCallback(action="confirm"/"cancel")` в `converter.py`. Нужно обновить `handle_confirm`/`handle_cancel` в `adapter/matrix/handlers/confirm.py` чтобы читать pending state и возвращать осмысленный ответ.
+
+### Паттерн 6: Hardcoded "C1" bug fix
+
+```python
+# auth.py:27 — СЕЙЧАС (баг):
+"chat_id": "C1"
+
+# ДОЛЖНО БЫТЬ:
+chat_id = await next_chat_id(store, matrix_user_id) # возвращает "C1" для первого пользователя
+```
+
+`next_chat_id` уже существует в `store.py` и правильно инкрементирует per-user. Нужно просто использовать его в `handle_invite` вместо хардкода.
+
+### Рекомендуемая структура store после рефакторинга
+
+Текущие ключи в store:
+- `matrix_room:{room_id}` → `{room_type, chat_id, display_name, matrix_user_id}` — **добавить `space_id`**
+- `matrix_user:{user_id}` → `{next_chat_index, ...}` — **добавить `space_id`**
+- `matrix_state:{room_id}` → `{state}` — оставить как есть
+- `matrix_skills_msg:{room_id}` → `{event_id}` — оставить (или убрать если реакции полностью уходят)
+
+Новые ключи:
+- `matrix_pending_confirm:{room_id}` → `{action_id, description, expires_at}` — для !yes/!no
+
+---
+
+## Don't Hand-Roll
+
+| Problem | Don't Build | Use Instead | Why |
+|---------|-------------|-------------|-----|
+| Space creation | Кастомный HTTP запрос к Matrix API | `AsyncClient.room_create(space=True)` | Встроено в matrix-nio, управляет session state |
+| Adding child room to Space | Кастомный state event builder | `AsyncClient.room_put_state(room_id, "m.space.child", ...)` | Правильный Content-Type, auth headers автоматически |
+| User invite | Прямой HTTP PUT | `AsyncClient.room_invite(room_id, user_id)` | Обрабатывает ошибки M_FORBIDDEN, already-joined |
+| Error detection | Проверка статус-кодов | `isinstance(resp, RoomCreateError)` / `isinstance(resp, RoomPutStateError)` | matrix-nio возвращает типизированные error-объекты |
+
+---
+
+## Common Pitfalls
+
+### Pitfall 1: `room_create(space=True)` vs `room_type="m.space"`
+
+**What goes wrong:** Передача `room_type="m.space"` как отдельный параметр — работает, но `space=True` — это удобный shortcut в matrix-nio, который внутри устанавливает тот же `room_type`. Оба варианта корректны, но `space=True` проще читается.
+
+**Проверено:** `room_create` signature в installed matrix-nio имеет `space: bool = False`. Нет отдельного `is_space` параметра.
+
+**How to avoid:** Использовать `space=True`, не `room_type="m.space"`.
+
+### Pitfall 2: `room_id` из RoomCreateResponse — не `getattr`
+
+**What goes wrong:** Текущий код в `handlers/chat.py:55`: `room_id = getattr(response, "room_id", None)`. Это работает для RoomCreateResponse, но молча возвращает None если пришёл RoomCreateError (у которого нет `room_id`).
+
+**How to avoid:**
+```python
+from nio.responses import RoomCreateError
+resp = await client.room_create(...)
+if isinstance(resp, RoomCreateError):
+ logger.error("room_create failed", status_code=resp.status_code)
+ return [OutgoingMessage(..., text="Не удалось создать комнату.")]
+room_id = resp.room_id # прямой доступ, не getattr
+```
+
+### Pitfall 3: `m.space.child` — state_key это room_id дочерней комнаты, не пустая строка
+
+**What goes wrong:** `room_put_state` по умолчанию `state_key=""`. Для `m.space.child` state_key ДОЛЖЕН быть room_id дочерней комнаты — иначе Space создастся некорректно.
+
+**How to avoid:** Всегда передавать `state_key=child_room_id` явно.
+
+### Pitfall 4: Бот должен быть в Space чтобы добавлять children
+
+**What goes wrong:** Бот создаёт Space (становится владельцем), потом пытается сделать `room_put_state` на Space. Это работает т.к. создатель автоматически имеет power level 100. Но если бот потерял membership (kicked out), `room_put_state` вернёт `M_FORBIDDEN`.
+
+**How to avoid:** Логировать ошибку и сообщать пользователю. Не ретраить молча.
+
+### Pitfall 5: Дублирование invite flow (идемпотентность)
+
+**What goes wrong:** Текущий `handle_invite` проверяет `get_room_meta(store, room.room_id)` чтобы не запускать flow дважды. После рефакторинга на Space+rooms нужно проверять `get_user_meta(store, matrix_user_id)` — потому что invite может прийти повторно в разные комнаты Space, а Space создаётся один раз per user.
+
+**How to avoid:** Idempotency check переносится на уровень user_meta: `if user_meta.get("space_id"): return`.
+
+### Pitfall 6: `skills_message` реакции — остаток от старого UX
+
+**What goes wrong:** `adapter/matrix/reactions.py` и `build_skills_text` до сих пор рендерят "Реакции 1️⃣-9️⃣ переключают навыки." По D-06 реакции убраны полностью. `build_skills_text` нужно обновить чтобы убрать эту строку и заменить инструкцией `!skill on/off `.
+
+**How to avoid:** Обновить `build_skills_text` + тест `test_reactions.py::test_build_skills_text`.
+
+### Pitfall 7: `on_reaction` callback остаётся зарегистрированным
+
+**What goes wrong:** В `main()` есть `client.add_event_callback(bot.on_reaction, ReactionEvent)`. Если убрать реакции но оставить этот callback — matrix-nio будет продолжать обрабатывать реакции и вызывать `on_reaction`. Нужно удалить и callback-регистрацию, и импорт `ReactionEvent`.
+
+---
+
+## Gaps between Current Implementation and Target
+
+| File | Current State | Target State | Action |
+|------|--------------|-------------|--------|
+| `adapter/matrix/handlers/auth.py` | DM join + hardcoded C1 | Space creation + C1 from next_chat_id | Переписать `handle_invite` |
+| `adapter/matrix/handlers/chat.py` | room_create без Space | room_create + room_put_state в Space | Обновить `make_handle_new_chat` |
+| `adapter/matrix/bot.py` | `on_reaction` + `_button_action_to_reaction` | Без реакций, pending-state для !yes/!no | Убрать reaction code; обновить `send_outgoing` |
+| `adapter/matrix/store.py` | Нет `space_id`, нет pending_confirm | `space_id` в room_meta + user_meta; `pending_confirm` helpers | Добавить поля и helpers |
+| `adapter/matrix/reactions.py` | `build_skills_text` упоминает реакции | `build_skills_text` без реакций, с `!skill on/off` | Обновить текст |
+| `adapter/matrix/handlers/confirm.py` | Заглушка без state | Читает pending_confirm, даёт реальный ответ | Обновить handlers |
+| `adapter/matrix/handlers/settings.py` | `handle_settings` — список команд | `handle_settings` — read-only дашборд (D-12) | Обновить до дашборда со статусом |
+| `adapter/matrix/converter.py` | `from_reaction` используется для skill toggle | Skill toggle через реакции убирается | `from_reaction` можно оставить или удалить |
+
+---
+
+## Code Examples
+
+### Создание Space + child room (verified API)
+
+```python
+# Source: matrix-nio installed version — inspect.signature(AsyncClient.room_create)
+from nio.responses import RoomCreateError, RoomPutStateError
+
+async def create_user_space(client, display_name: str, matrix_user_id: str, store):
+ homeserver = matrix_user_id.split(":")[-1] # "@user:matrix.org" → "matrix.org"
+
+ # Step 1: Create Space
+ space_resp = await client.room_create(
+ name=f"Lambda — {display_name}",
+ space=True,
+ visibility="private",
+ )
+ if isinstance(space_resp, RoomCreateError):
+ return None, None
+ space_id = space_resp.room_id
+
+ # Step 2: Create first chat room
+ chat_resp = await client.room_create(
+ name="Чат 1",
+ visibility="private",
+ is_direct=False,
+ )
+ if isinstance(chat_resp, RoomCreateError):
+ return space_id, None
+ chat_room_id = chat_resp.room_id
+
+ # Step 3: Link child room into Space (state_key = child's room_id)
+ await client.room_put_state(
+ room_id=space_id,
+ event_type="m.space.child",
+ content={"via": [homeserver]},
+ state_key=chat_room_id,
+ )
+
+ # Step 4: Invite user to Space and to chat room
+ await client.room_invite(space_id, matrix_user_id)
+ await client.room_invite(chat_room_id, matrix_user_id)
+
+ return space_id, chat_room_id
+```
+
+### send_outgoing для OutgoingUI (без реакций)
+
+```python
+if isinstance(event, OutgoingUI):
+ lines = [event.text]
+ if event.buttons:
+ lines.append("")
+ for btn in event.buttons:
+ lines.append(f"• {btn.label}")
+ lines.append("")
+ lines.append("Ответьте !yes для подтверждения или !no для отмены.")
+ body = "\n".join(lines)
+ await client.room_send(room_id, "m.room.message", {"msgtype": "m.text", "body": body})
+```
+
+### Проверка ошибок matrix-nio
+
+```python
+from nio.responses import RoomCreateError, RoomPutStateError, RoomInviteError
+
+resp = await client.room_create(...)
+if isinstance(resp, RoomCreateError):
+ logger.error("room_create failed", status_code=resp.status_code)
+ # resp не имеет room_id — безопасный ранний возврат
+ return [OutgoingMessage(chat_id=event.chat_id, text="Не удалось создать комнату.")]
+room_id = resp.room_id # str, гарантированно присутствует
+```
+
+---
+
+## Validation Architecture
+
+nyquist_validation = true в config.json — раздел обязателен.
+
+### Test Framework
+| Property | Value |
+|----------|-------|
+| Framework | pytest + pytest-asyncio |
+| Config file | pytest.ini или pyproject.toml (проверить наличие) |
+| Quick run command | `pytest tests/adapter/matrix/ -q` |
+| Full suite command | `pytest tests/ -q` |
+| Current count | 97 passed |
+
+### Существующие тесты Matrix, требующие обновления
+
+Эти тесты написаны под DM/reaction-based поведение и сломаются после рефакторинга:
+
+| Test | Текущее поведение | После рефакторинга | Действие |
+|------|------------------|-------------------|---------|
+| `test_dispatcher.py::test_invite_event_creates_dm_room_and_sends_welcome` | Проверяет `chat_id == "C1"` через hardcode, join DM | Должен проверять Space creation + chat room creation | Переписать |
+| `test_dispatcher.py::test_new_chat_creates_real_matrix_room_when_client_available` | Проверяет `room_create` без Space | Должен проверять `room_create` + `room_put_state` | Обновить mock + assertions |
+| `test_reactions.py::test_build_skills_text` | Ожидает "Реакции 1️⃣-9️⃣" в тексте | После удаления реакций эта строка исчезнет | Обновить assertion |
+| `test_reactions.py::test_build_confirmation_text` | Проверяет `CONFIRM_REACTION` + "подтвердить" | Если `build_confirmation_text` обновится под D-07 | Обновить |
+
+### Новые тесты, необходимые для покрытия Space+rooms
+
+| ID | Behavior | Test Type | File | Command |
+|----|----------|-----------|------|---------|
+| MAT-01 | handle_invite создаёт Space + Чат 1, сохраняет space_id в user_meta | unit | `tests/adapter/matrix/test_invite_space.py` | `pytest tests/adapter/matrix/test_invite_space.py -x` |
+| MAT-02 | handle_invite идемпотентен: повторный вызов не создаёт второй Space | unit | `tests/adapter/matrix/test_invite_space.py` | `pytest tests/adapter/matrix/test_invite_space.py -x` |
+| MAT-03 | handle_invite использует next_chat_id, не хардкод "C1" | unit | `tests/adapter/matrix/test_invite_space.py` | `pytest tests/adapter/matrix/test_invite_space.py -x` |
+| MAT-04 | make_handle_new_chat вызывает room_put_state с space_id из user_meta | unit | `tests/adapter/matrix/test_chat_space.py` | `pytest tests/adapter/matrix/test_chat_space.py -x` |
+| MAT-05 | make_handle_new_chat без space_id возвращает error message | unit | `tests/adapter/matrix/test_chat_space.py` | `pytest tests/adapter/matrix/test_chat_space.py -x` |
+| MAT-06 | send_outgoing для OutgoingUI рендерит текст + "!yes / !no", без реакций | unit | `tests/adapter/matrix/test_send_outgoing.py` | `pytest tests/adapter/matrix/test_send_outgoing.py -x` |
+| MAT-07 | send_outgoing для OutgoingUI НЕ отправляет m.reaction event | unit | `tests/adapter/matrix/test_send_outgoing.py` | `pytest tests/adapter/matrix/test_send_outgoing.py -x` |
+| MAT-08 | get/set/clear_pending_confirm roundtrip в store | unit | `tests/adapter/matrix/test_store.py` (extend) | `pytest tests/adapter/matrix/test_store.py -x` |
+| MAT-09 | handle_confirm читает pending_confirm и возвращает описание действия | unit | `tests/adapter/matrix/test_confirm.py` | `pytest tests/adapter/matrix/test_confirm.py -x` |
+| MAT-10 | handle_archive вызывает room_put_state с пустым content | unit | `tests/adapter/matrix/test_chat_space.py` | `pytest tests/adapter/matrix/test_chat_space.py -x` |
+| MAT-11 | !settings возвращает дашборд со статусом (не список команд) | unit | `tests/adapter/matrix/test_dispatcher.py` (extend) | `pytest tests/adapter/matrix/test_dispatcher.py -x` |
+| MAT-12 | RoomCreateError обрабатывается корректно (нет crash, есть user message) | unit | `tests/adapter/matrix/test_chat_space.py` | `pytest tests/adapter/matrix/test_chat_space.py -x` |
+
+### Wave 0 Gaps (новые файлы)
+
+- [ ] `tests/adapter/matrix/test_invite_space.py` — покрывает MAT-01, MAT-02, MAT-03
+- [ ] `tests/adapter/matrix/test_chat_space.py` — покрывает MAT-04, MAT-05, MAT-10, MAT-12
+- [ ] `tests/adapter/matrix/test_send_outgoing.py` — покрывает MAT-06, MAT-07
+- [ ] `tests/adapter/matrix/test_confirm.py` — покрывает MAT-09
+
+### Sampling Rate
+
+- **Per task commit:** `pytest tests/adapter/matrix/ -q`
+- **Per wave merge:** `pytest tests/ -q`
+- **Phase gate:** All 97+ tests green (целевой диапазон 106–110 после добавления новых)
+
+### Численный ориентир для "96+ зелёных"
+
+- Сейчас: 97 тестов, все зелёные
+- После рефакторинга без добавления тестов: 4 теста сломаются (3 dispatcher + 1 reactions) → ~93 зелёных
+- После обновления сломанных: 97 зелёных
+- После добавления 12 новых: ~109 зелёных
+- **Итого: требование "96+" выполнено с запасом**
+
+---
+
+## Environment Availability
+
+| Dependency | Required By | Available | Version | Fallback |
+|------------|------------|-----------|---------|----------|
+| matrix-nio | All Matrix API calls | ✓ | установлена, space=True присутствует | — |
+| pytest + pytest-asyncio | Test suite | ✓ | работает (97 passed) | — |
+| SQLite | SQLiteStore | ✓ | встроен в Python | — |
+| Matrix homeserver | Manual QA только | не проверялось | — | Без homeserver — только unit тесты |
+
+**Missing dependencies with no fallback:** Нет (homeserver нужен только для ручного QA, не для автотестов).
+
+---
+
+## Project Constraints (from CLAUDE.md)
+
+| Directive | Impact on Phase |
+|-----------|----------------|
+| `core/protocol.py` — типы не менять | `IncomingCommand`, `OutgoingUI`, `UIButton` используем as-is |
+| Все вызовы платформы через `platform/interface.py` | MockPlatformClient остаётся, SDK не трогать |
+| Хотфиксы < 20 строк → Claude Code напрямую | Небольшие правки реакций-в-текст могут идти напрямую |
+| Реализацию делает Codex | Три задачи — три параллельных Codex запуска |
+| Blueprint перед реализацией | Плану нужны blueprint-документы для каждой задачи |
+| Порядок зависимостей: core/ → platform/ → adapters/ | Все изменения только в adapter/matrix/, core/ не трогаем |
+
+---
+
+## Open Questions
+
+1. **Стоит ли полностью убирать `from_reaction` и `reactions.py`?**
+ - D-06 говорит "убрать реакции полностью"
+ - `reactions.py` содержит `build_confirmation_text` и `build_skills_text` — они нужны после рефакторинга
+ - Рекомендация: оставить `reactions.py`, удалить `CONFIRM_REACTION`/`CANCEL_REACTION`/`add_reaction`/`remove_reaction`, переименовать в `formatting.py` — но это необязательно для Phase 1.
+
+2. **Нужен ли `m.space.parent` event в дочерних комнатах?**
+ - Matrix spec позволяет устанавливать `m.space.parent` в дочерней комнате, чтобы Element показывал ссылку "назад к Space"
+ - Не является обязательным — `m.space.child` в Space достаточно для включения комнаты в Space
+ - Рекомендация: не добавлять в Phase 1, отложить если понадобится.
+
+3. **`via` в `m.space.child` — один сервер или несколько?**
+ - Для single-homeserver деплоя: `["homeserver_domain"]` достаточно
+ - Для федерации: нужны несколько серверов
+ - Рекомендация: парсить из `matrix_user_id.split(":")[-1]` — достаточно для текущего использования.
+
+---
+
+## Sources
+
+### Primary (HIGH confidence)
+- matrix-nio installed package — `AsyncClient.room_create`, `room_put_state`, `room_invite`, `join` — сигнатуры и docstrings проверены через `inspect.signature` и `help()`
+- `nio.responses.RoomCreateResponse`, `RoomCreateError`, `RoomPutStateResponse`, `RoomPutStateError` — поля проверены через `inspect.getsource`
+- Весь codebase прочитан напрямую
+
+### Secondary (MEDIUM confidence)
+- Matrix Spec v1.x — `m.space.child` event format (content `{"via": [...]}`, state_key = child room_id) — стандартное поведение, описано в Matrix spec
+
+---
+
+## Metadata
+
+**Confidence breakdown:**
+- matrix-nio API: HIGH — проверено против installed package через Python introspection
+- Space creation pattern: HIGH — `space=True` параметр подтверждён в room_create signature
+- `m.space.child` content format: MEDIUM — стандарт Matrix spec, не проверен против конкретного homeserver
+- Archive via empty content: MEDIUM — Matrix spec behaviour, может зависеть от homeserver version
+- Тест-план: HIGH — основан на прямом анализе существующих тестов
+
+**Research date:** 2026-04-02
+**Valid until:** 2026-05-02 (matrix-nio обновляется редко, Space API стабилен с Matrix v1.2)