surfaces/.planning/phases/01-matrix-qa-polish/01-03-PLAN.md

20 KiB
Raw Blame History

phase plan type wave depends_on files_modified autonomous requirements must_haves
01-matrix-qa-polish 03 execute 2
01-01
01-02
adapter/matrix/bot.py
adapter/matrix/reactions.py
adapter/matrix/handlers/confirm.py
adapter/matrix/handlers/settings.py
true
truths artifacts key_links
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
path provides contains
adapter/matrix/bot.py Clean send_outgoing without reactions, pending_confirm storage on OutgoingUI !yes
path provides
adapter/matrix/reactions.py Updated text builders without reaction references
path provides contains
adapter/matrix/handlers/confirm.py !yes/!no handlers reading pending_confirm get_pending_confirm
path provides
adapter/matrix/handlers/settings.py Read-only dashboard for !settings
from to via pattern
adapter/matrix/bot.py adapter/matrix/store.py set_pending_confirm on OutgoingUI send set_pending_confirm
from to via pattern
adapter/matrix/handlers/confirm.py adapter/matrix/store.py get_pending_confirm / clear_pending_confirm 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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.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

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
@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)
# 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},
    )
dispatcher.register(IncomingCallback, "confirm", handle_confirm)
dispatcher.register(IncomingCallback, "cancel", handle_cancel)
@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:

from nio import (
    AsyncClient,
    AsyncClientConfig,
    InviteMemberEvent,
    MatrixRoom,
    ReactionEvent,
    RoomMemberEvent,
    RoomMessageText,
)

to:

from nio import (
    AsyncClient,
    AsyncClientConfig,
    InviteMemberEvent,
    MatrixRoom,
    RoomMemberEvent,
    RoomMessageText,
)

2. Remove from_reaction import (line 20): Change:

from adapter.matrix.converter import from_reaction, from_room_event

to:

from adapter.matrix.converter import from_room_event

3. Add store import for pending_confirm: Add this import:

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:

    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:

async def send_outgoing(client: AsyncClient, room_id: str, event: OutgoingEvent, store: StateStore | None = None) -> None:

And update MatrixBot._send_all to pass store:

    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:

    client.add_event_callback(bot.on_reaction, ReactionEvent)

8. Add StateStore import at top:

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:

        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:

    lines.append("Реакции 1⃣-9⃣ переключают навыки.")

With:

    lines.append("!skill on/off <название> — переключить навык.")
  1. Update build_confirmation_text — remove reaction emojis, use only !yes/!no:

Replace the entire function with:

def build_confirmation_text(description: str) -> str:
    return "\n".join(
        [
            "Lambda",
            description,
            "",
            "Ответьте !yes для подтверждения или !no для отмены.",
        ]
    )
  1. Remove add_reaction and remove_reaction functions entirely (they send m.reaction events which are no longer used).

  2. 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:

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:

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:

from adapter.matrix.handlers.confirm import handle_cancel, handle_confirm

to:

from adapter.matrix.handlers.confirm import make_handle_cancel, make_handle_confirm

Change registrations from:

    dispatcher.register(IncomingCallback, "confirm", handle_confirm)
    dispatcher.register(IncomingCallback, "cancel", handle_cancel)

to:

    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.

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`

<success_criteria>

  • 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 </success_criteria>
After completion, create `.planning/phases/01-matrix-qa-polish/01-03-SUMMARY.md`