542 lines
20 KiB
Markdown
542 lines
20 KiB
Markdown
---
|
||
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"
|
||
---
|
||
|
||
<objective>
|
||
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.
|
||
</objective>
|
||
|
||
<execution_context>
|
||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||
</execution_context>
|
||
|
||
<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
|
||
|
||
<interfaces>
|
||
<!-- From adapter/matrix/store.py (after Plan 01 adds pending_confirm helpers): -->
|
||
|
||
```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
|
||
```
|
||
|
||
<!-- From core/protocol.py — OutgoingUI and UIButton: -->
|
||
|
||
```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)
|
||
```
|
||
|
||
<!-- From adapter/matrix/converter.py — how !yes/!no become IncomingCallback: -->
|
||
|
||
```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},
|
||
)
|
||
```
|
||
|
||
<!-- From adapter/matrix/handlers/__init__.py — confirm/cancel registration: -->
|
||
|
||
```python
|
||
dispatcher.register(IncomingCallback, "confirm", handle_confirm)
|
||
dispatcher.register(IncomingCallback, "cancel", handle_cancel)
|
||
```
|
||
|
||
<!-- From sdk.interface.UserSettings — used by settings dashboard: -->
|
||
|
||
```python
|
||
@dataclass
|
||
class UserSettings:
|
||
skills: dict
|
||
connectors: dict
|
||
soul: dict
|
||
safety: dict
|
||
plan: dict
|
||
```
|
||
</interfaces>
|
||
</context>
|
||
|
||
<tasks>
|
||
|
||
<task type="auto">
|
||
<name>Task 1: Remove reactions from bot.py, update send_outgoing for !yes/!no (per D-06, D-07)</name>
|
||
<files>adapter/matrix/bot.py</files>
|
||
<read_first>adapter/matrix/bot.py, adapter/matrix/store.py, core/protocol.py</read_first>
|
||
<action>
|
||
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,
|
||
})
|
||
```
|
||
</action>
|
||
<verify>
|
||
<automated>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')"</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- `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
|
||
</acceptance_criteria>
|
||
<done>bot.py has no reaction code; OutgoingUI renders text + !yes/!no; pending_confirm stored on OutgoingUI send</done>
|
||
</task>
|
||
|
||
<task type="auto">
|
||
<name>Task 2: Update reactions.py text builders + confirm.py handlers + settings.py dashboard (per D-06, D-07, D-08, D-12)</name>
|
||
<files>adapter/matrix/reactions.py, adapter/matrix/handlers/confirm.py, adapter/matrix/handlers/settings.py</files>
|
||
<read_first>adapter/matrix/reactions.py, adapter/matrix/handlers/confirm.py, adapter/matrix/handlers/settings.py, adapter/matrix/store.py</read_first>
|
||
<action>
|
||
**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)]
|
||
```
|
||
</action>
|
||
<verify>
|
||
<automated>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')"</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- `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`
|
||
</acceptance_criteria>
|
||
<done>Reactions removed from text builders; !yes/!no handlers read pending_confirm; !settings is read-only dashboard</done>
|
||
</task>
|
||
|
||
</tasks>
|
||
|
||
<verification>
|
||
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`
|
||
</verification>
|
||
|
||
<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>
|
||
|
||
<output>
|
||
After completion, create `.planning/phases/01-matrix-qa-polish/01-03-SUMMARY.md`
|
||
</output>
|