20 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-matrix-qa-polish | 03 | execute | 2 |
|
|
true |
|
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**
- 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 <название> — переключить навык.")
- 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 для отмены.",
]
)
-
Remove
add_reactionandremove_reactionfunctions entirely (they send m.reaction events which are no longer used). -
Keep
CONFIRM_REACTION,CANCEL_REACTION,SKILL_REACTIONS,REACTION_TO_INDEX,reaction_to_skill_index— they are still imported byconverter.pyforfrom_reaction. Even thoughfrom_reactionis 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>