--- phase: 01-matrix-qa-polish plan: 03 type: execute wave: 2 depends_on: ["01-01", "01-02"] files_modified: - adapter/matrix/bot.py - adapter/matrix/reactions.py - adapter/matrix/handlers/confirm.py - adapter/matrix/handlers/settings.py autonomous: true requirements: [] must_haves: truths: - "OutgoingUI renders as text + '!yes / !no' hint, no m.reaction events sent" - "_button_action_to_reaction function is removed from bot.py" - "on_reaction callback is removed from bot.py" - "ReactionEvent import is removed from bot.py" - "build_skills_text no longer mentions reactions 1-9" - "build_confirmation_text uses !yes/!no instead of reaction emojis" - "!yes reads pending_confirm from store and returns action description" - "!no clears pending_confirm and returns cancellation message" - "!settings returns a read-only dashboard with skills/soul/safety/chats status" artifacts: - path: "adapter/matrix/bot.py" provides: "Clean send_outgoing without reactions, pending_confirm storage on OutgoingUI" contains: "!yes" - path: "adapter/matrix/reactions.py" provides: "Updated text builders without reaction references" - path: "adapter/matrix/handlers/confirm.py" provides: "!yes/!no handlers reading pending_confirm" contains: "get_pending_confirm" - path: "adapter/matrix/handlers/settings.py" provides: "Read-only dashboard for !settings" key_links: - from: "adapter/matrix/bot.py" to: "adapter/matrix/store.py" via: "set_pending_confirm on OutgoingUI send" pattern: "set_pending_confirm" - from: "adapter/matrix/handlers/confirm.py" to: "adapter/matrix/store.py" via: "get_pending_confirm / clear_pending_confirm" pattern: "get_pending_confirm" --- Remove all reaction-based UX from the Matrix adapter and replace with text-based !yes/!no confirmation. Update settings dashboard to read-only format. Purpose: Per D-06/D-07/D-08, reactions are removed entirely. OutgoingUI renders as plain text with !yes/!no hint. Per D-12, !settings becomes a read-only dashboard. Output: Clean bot.py without reactions, working !yes/!no confirmation flow, updated text builders, read-only settings dashboard. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/01-matrix-qa-polish/01-CONTEXT.md @.planning/phases/01-matrix-qa-polish/01-RESEARCH.md @adapter/matrix/bot.py @adapter/matrix/reactions.py @adapter/matrix/handlers/confirm.py @adapter/matrix/handlers/settings.py @adapter/matrix/store.py @adapter/matrix/converter.py @core/protocol.py ```python PENDING_CONFIRM_PREFIX = "matrix_pending_confirm:" async def get_pending_confirm(store: StateStore, room_id: str) -> dict | None async def set_pending_confirm(store: StateStore, room_id: str, meta: dict) -> None async def clear_pending_confirm(store: StateStore, room_id: str) -> None ``` ```python @dataclass class UIButton: label: str action: str payload: dict = field(default_factory=dict) style: str = "secondary" @dataclass class OutgoingUI: chat_id: str text: str buttons: list[UIButton] = field(default_factory=list) @dataclass class IncomingCallback: user_id: str platform: str chat_id: str action: str payload: dict = field(default_factory=dict) ``` ```python # In from_command(): if command in {"yes", "no"}: action = "confirm" if command == "yes" else "cancel" return IncomingCallback( user_id=sender, platform=PLATFORM, chat_id=chat_id, action=action, payload={"source": "command", "command": command}, ) ``` ```python dispatcher.register(IncomingCallback, "confirm", handle_confirm) dispatcher.register(IncomingCallback, "cancel", handle_cancel) ``` ```python @dataclass class UserSettings: skills: dict connectors: dict soul: dict safety: dict plan: dict ``` Task 1: Remove reactions from bot.py, update send_outgoing for !yes/!no (per D-06, D-07) adapter/matrix/bot.py adapter/matrix/bot.py, adapter/matrix/store.py, core/protocol.py Modify `adapter/matrix/bot.py` with these specific changes: **1. Remove ReactionEvent import (line 14):** Change the nio import block from: ```python from nio import ( AsyncClient, AsyncClientConfig, InviteMemberEvent, MatrixRoom, ReactionEvent, RoomMemberEvent, RoomMessageText, ) ``` to: ```python from nio import ( AsyncClient, AsyncClientConfig, InviteMemberEvent, MatrixRoom, RoomMemberEvent, RoomMessageText, ) ``` **2. Remove `from_reaction` import (line 20):** Change: ```python from adapter.matrix.converter import from_reaction, from_room_event ``` to: ```python from adapter.matrix.converter import from_room_event ``` **3. Add store import for pending_confirm:** Add this import: ```python from adapter.matrix.store import set_pending_confirm ``` **4. Delete the entire `on_reaction` method from MatrixBot class (lines 106-114).** **5. Delete the entire `_button_action_to_reaction` function (lines 135-140).** **6. Rewrite the OutgoingUI block in `send_outgoing` function.** Replace the existing `if isinstance(event, OutgoingUI):` block (lines 154-180) with: ```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}) # Store pending confirmation for !yes/!no handler if event.buttons: action_id = event.buttons[0].action if event.buttons else "unknown" payload = event.buttons[0].payload if event.buttons else {} await set_pending_confirm(store, room_id, { "action_id": action_id, "description": event.text, "payload": payload, }) return ``` **PROBLEM:** `send_outgoing` is a module-level function with signature `async def send_outgoing(client, room_id, event)`. It doesn't receive `store`. We need to pass `store` to it. **Solution:** Change `send_outgoing` signature to include `store`: ```python async def send_outgoing(client: AsyncClient, room_id: str, event: OutgoingEvent, store: StateStore | None = None) -> None: ``` And update `MatrixBot._send_all` to pass store: ```python async def _send_all(self, room_id: str, outgoing: list[OutgoingEvent]) -> None: for event in outgoing: await send_outgoing(self.client, room_id, event, store=self.runtime.store) ``` **7. In `main()`, remove the on_reaction callback registration.** Delete this line: ```python client.add_event_callback(bot.on_reaction, ReactionEvent) ``` **8. Add StateStore import at top:** ```python from core.store import InMemoryStore, SQLiteStore, StateStore ``` (StateStore is already imported on line 37 — verify it's there.) The `set_pending_confirm` call in the OutgoingUI handler should guard against store being None: ```python if event.buttons and store is not None: action_id = event.buttons[0].action payload = event.buttons[0].payload await set_pending_confirm(store, room_id, { "action_id": action_id, "description": event.text, "payload": payload, }) ``` cd /Users/a/MAI/sem2/lambda/surfaces-bot && python -c "from adapter.matrix.bot import send_outgoing, MatrixBot, build_runtime; print('OK')" && python -c "import ast; tree = ast.parse(open('adapter/matrix/bot.py').read()); names = [n.name for n in ast.walk(tree) if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]; assert '_button_action_to_reaction' not in names, 'reaction helper still exists'; assert 'on_reaction' not in names, 'on_reaction still exists'; print('REACTION CODE REMOVED')" - `adapter/matrix/bot.py` does NOT contain the string `_button_action_to_reaction` - `adapter/matrix/bot.py` does NOT contain the string `on_reaction` - `adapter/matrix/bot.py` does NOT contain `ReactionEvent` - `adapter/matrix/bot.py` does NOT contain `from_reaction` - `adapter/matrix/bot.py` does NOT contain `m.reaction` - `adapter/matrix/bot.py` contains `Ответьте !yes для подтверждения или !no для отмены.` - `adapter/matrix/bot.py` contains `set_pending_confirm` - `send_outgoing` function signature includes `store` parameter bot.py has no reaction code; OutgoingUI renders text + !yes/!no; pending_confirm stored on OutgoingUI send Task 2: Update reactions.py text builders + confirm.py handlers + settings.py dashboard (per D-06, D-07, D-08, D-12) adapter/matrix/reactions.py, adapter/matrix/handlers/confirm.py, adapter/matrix/handlers/settings.py adapter/matrix/reactions.py, adapter/matrix/handlers/confirm.py, adapter/matrix/handlers/settings.py, adapter/matrix/store.py **Part A: Update adapter/matrix/reactions.py** 1. Update `build_skills_text` — replace the last line "Реакции 1-9 переключают навыки." with instruction for text commands: Replace: ```python lines.append("Реакции 1️⃣-9️⃣ переключают навыки.") ``` With: ```python lines.append("!skill on/off <название> — переключить навык.") ``` 2. Update `build_confirmation_text` — remove reaction emojis, use only !yes/!no: Replace the entire function with: ```python def build_confirmation_text(description: str) -> str: return "\n".join( [ "Lambda", description, "", "Ответьте !yes для подтверждения или !no для отмены.", ] ) ``` 3. Remove `add_reaction` and `remove_reaction` functions entirely (they send m.reaction events which are no longer used). 4. Keep `CONFIRM_REACTION`, `CANCEL_REACTION`, `SKILL_REACTIONS`, `REACTION_TO_INDEX`, `reaction_to_skill_index` — they are still imported by `converter.py` for `from_reaction`. Even though `from_reaction` is no longer called from bot.py, converter.py still exports it and removing would break imports. Keep for backwards compat. Actually, check: `from_reaction` is imported in `converter.py` definition, not as an external import. And `bot.py` no longer imports `from_reaction`. But `converter.py` imports `CANCEL_REACTION, CONFIRM_REACTION, reaction_to_skill_index` from `reactions.py`. So those constants MUST stay. Keep: `CONFIRM_REACTION`, `CANCEL_REACTION`, `SKILL_REACTIONS`, `REACTION_TO_INDEX`, `reaction_to_skill_index`, `build_skills_text`, `build_confirmation_text`. Remove: `add_reaction`, `remove_reaction`. Remove the `AsyncClient` import since add_reaction/remove_reaction used it and nothing else does. Updated file should look like: ```python from __future__ import annotations from sdk.interface import UserSettings CONFIRM_REACTION = "👍" CANCEL_REACTION = "❌" SKILL_REACTIONS = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣"] REACTION_TO_INDEX = {emoji: idx + 1 for idx, emoji in enumerate(SKILL_REACTIONS)} def build_skills_text(settings: UserSettings) -> str: lines: list[str] = ["Скиллы"] for idx, (name, enabled) in enumerate(settings.skills.items(), start=1): state = "on" if enabled else "off" emoji = SKILL_REACTIONS[idx - 1] if idx - 1 < len(SKILL_REACTIONS) else f"{idx}." lines.append(f" {state} {emoji} {name}") lines.append("") lines.append("!skill on/off <название> — переключить навык.") return "\n".join(lines) def build_confirmation_text(description: str) -> str: return "\n".join( [ "Lambda", description, "", "Ответьте !yes для подтверждения или !no для отмены.", ] ) def reaction_to_skill_index(key: str) -> int | None: return REACTION_TO_INDEX.get(key) ``` **Part B: Update adapter/matrix/handlers/confirm.py** Rewrite to read pending_confirm from store. The handlers receive the standard signature `(event, auth_mgr, platform, chat_mgr, settings_mgr)` but need access to `store`. Since they're registered in `__init__.py` as plain functions (not closures), convert them to closure factories. Replace entire file: ```python from __future__ import annotations from adapter.matrix.store import get_pending_confirm, clear_pending_confirm from core.protocol import IncomingCallback, OutgoingMessage def make_handle_confirm(store=None): async def handle_confirm( event: IncomingCallback, auth_mgr, platform, chat_mgr, settings_mgr ) -> list: if store is None: return [OutgoingMessage(chat_id=event.chat_id, text="Нет ожидающих подтверждений.")] pending = await get_pending_confirm(store, event.chat_id) if not pending: return [OutgoingMessage(chat_id=event.chat_id, text="Нет ожидающих подтверждений.")] description = pending.get("description", "действие") action_id = pending.get("action_id", "unknown") await clear_pending_confirm(store, event.chat_id) return [ OutgoingMessage( chat_id=event.chat_id, text=f"Подтверждено: {description}", ) ] return handle_confirm def make_handle_cancel(store=None): async def handle_cancel( event: IncomingCallback, auth_mgr, platform, chat_mgr, settings_mgr ) -> list: if store is None: return [OutgoingMessage(chat_id=event.chat_id, text="Нет ожидающих подтверждений.")] pending = await get_pending_confirm(store, event.chat_id) if not pending: return [OutgoingMessage(chat_id=event.chat_id, text="Нет ожидающих подтверждений.")] await clear_pending_confirm(store, event.chat_id) return [ OutgoingMessage( chat_id=event.chat_id, text="Действие отменено.", ) ] return handle_cancel ``` **Part C: Update adapter/matrix/handlers/__init__.py for new confirm imports** Change confirm imports from: ```python from adapter.matrix.handlers.confirm import handle_cancel, handle_confirm ``` to: ```python from adapter.matrix.handlers.confirm import make_handle_cancel, make_handle_confirm ``` Change registrations from: ```python dispatcher.register(IncomingCallback, "confirm", handle_confirm) dispatcher.register(IncomingCallback, "cancel", handle_cancel) ``` to: ```python dispatcher.register(IncomingCallback, "confirm", make_handle_confirm(store)) dispatcher.register(IncomingCallback, "cancel", make_handle_cancel(store)) ``` **Part D: Update adapter/matrix/handlers/settings.py — handle_settings becomes read-only dashboard (per D-12)** Replace the `handle_settings` function body. Keep ALL other functions unchanged. ```python async def handle_settings( event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr ) -> list: settings = await settings_mgr.get(event.user_id) chats = await chat_mgr.list_active(event.user_id) # Skills section skills_lines = [] for name, enabled in settings.skills.items(): state = "on" if enabled else "off" skills_lines.append(f" {state} {name}") skills_text = "\n".join(skills_lines) if skills_lines else " нет навыков" # Soul section soul_lines = [] for key, value in (settings.soul or {}).items(): soul_lines.append(f" {key}: {value}") soul_text = "\n".join(soul_lines) if soul_lines else " по умолчанию" # Safety section safety_lines = [] for key, value in (settings.safety or {}).items(): state = "on" if value else "off" safety_lines.append(f" {state} {key}") safety_text = "\n".join(safety_lines) if safety_lines else " по умолчанию" # Chats section chat_lines = [f" {c.display_name} ({c.chat_id})" for c in chats] chats_text = "\n".join(chat_lines) if chat_lines else " нет активных чатов" dashboard = "\n".join([ "Настройки", "", "Скиллы:", skills_text, "", "Личность:", soul_text, "", "Безопасность:", safety_text, "", f"Активные чаты ({len(chats)}):", chats_text, "", "Изменить: !skills, !soul, !safety", ]) return [OutgoingMessage(chat_id=event.chat_id, text=dashboard)] ``` cd /Users/a/MAI/sem2/lambda/surfaces-bot && python -c "from adapter.matrix.reactions import build_skills_text, build_confirmation_text; from adapter.matrix.handlers.confirm import make_handle_confirm, make_handle_cancel; from adapter.matrix.handlers.settings import handle_settings; print('OK')" && python -c "from adapter.matrix.handlers import register_matrix_handlers; print('OK')" - `adapter/matrix/reactions.py` does NOT contain `add_reaction` - `adapter/matrix/reactions.py` does NOT contain `remove_reaction` - `adapter/matrix/reactions.py` does NOT contain the string `Реакции 1` - `adapter/matrix/reactions.py` contains `!skill on/off` - `adapter/matrix/reactions.py` contains `!yes` in build_confirmation_text - `adapter/matrix/handlers/confirm.py` contains `get_pending_confirm` - `adapter/matrix/handlers/confirm.py` contains `clear_pending_confirm` - `adapter/matrix/handlers/confirm.py` contains `def make_handle_confirm(` - `adapter/matrix/handlers/confirm.py` contains `def make_handle_cancel(` - `adapter/matrix/handlers/__init__.py` contains `make_handle_confirm(store)` - `adapter/matrix/handlers/__init__.py` contains `make_handle_cancel(store)` - `adapter/matrix/handlers/settings.py` `handle_settings` function contains the string `Настройки` and `Скиллы:` and `Изменить:` - `adapter/matrix/handlers/settings.py` `handle_settings` does NOT contain `!connectors` or `!plan` or `!status` or `!whoami` Reactions removed from text builders; !yes/!no handlers read pending_confirm; !settings is read-only dashboard After both tasks: - `python -c "from adapter.matrix.bot import send_outgoing, MatrixBot; from adapter.matrix.reactions import build_skills_text; from adapter.matrix.handlers.confirm import make_handle_confirm; from adapter.matrix.handlers import register_matrix_handlers; print('ALL OK')"` - No string `m.reaction` in `adapter/matrix/bot.py` - No string `_button_action_to_reaction` in `adapter/matrix/bot.py` - No string `Реакции 1` in `adapter/matrix/reactions.py` - bot.py: no reaction code, OutgoingUI renders text + !yes/!no, stores pending_confirm - reactions.py: build_skills_text says "!skill on/off", build_confirmation_text says "!yes/!no" - confirm.py: !yes reads pending_confirm and confirms, !no clears and cancels - settings.py: !settings returns read-only dashboard - All imports resolve After completion, create `.planning/phases/01-matrix-qa-polish/01-03-SUMMARY.md`