diff --git a/personal/plans/balda-mm-pr8-review-fixes.md b/personal/plans/balda-mm-pr8-review-fixes.md index 0c6cc3ad..784f39cf 100755 --- a/personal/plans/balda-mm-pr8-review-fixes.md +++ b/personal/plans/balda-mm-pr8-review-fixes.md @@ -1,213 +1,232 @@ # Balda — PR #8 review fixes: slash commands, /locator, group DM session -> Создан 2026-09-28. Статус: **ПЛАН — ожидает подтверждения Alex.** -> Источник: ревью Alex на `baldaworks/balda#8` (Mattermost transport). -> Часть [[personal/plans/balda-mm-pr-plan]] (PR #8). Ветка **`feat/mattermost-channel`**. -> Ветки / кода ещё не трогал — только чтение и сверка. +> Создан 2026-09-28. Статус: **✅ РЕАЛИЗОВАНО, ЗАПУШЕНО, CI ИДЁТ.** +> Коммит **`cd4ce4bc`** в `feat/mattermost-channel` → PR **baldaworks/balda#8** обновлён. +> 46 файлов, +6760/−27, база `0 позади / 8 впереди`. +> Источник: ревью Alex на PR #8 + **четвёртый дефект, найденный мной** (§5). +> Часть [[personal/plans/balda-mm-pr-plan]] (PR #8). --- -## 0. TL;DR +## 0. Итог -Ревью право по всем трём пунктам — **проверено в коде**, не на слово. +Ревью Alex подтверждено в коде — все три пункта. Плюс найден **четвёртый дефект**, +которого в ревью не было (§5). Все четыре закрыты одним коммитом. -| # | Дефект | Подтверждение в коде (коммит `9f1e6c9c`) | +| # | Дефект | Статус | |---|---|---| -| Б1 | Slash-команды не доходят: `/start` и прочие обрабатываются только из WS-события `posted` | `channel/mattermost/ingress.go` — `switch event.Event` знает только `posted`; отдельного HTTP-приёмника slash нет вовсе. У Slack он есть: `slackagent/server.go` → `mux.HandleFunc(commandsPath, h.handleCommands)` | -| Б2 | `/locator` всегда падает: `locatorref.Parse` не знает `mattermost` | `locatorref/locatorref.go:52-62` — `switch channelType` покрывает только `telegram` / `zulip` / `slackagent`; ветки `mattermost` нет → `unsupported channel type` | -| В3 | Групповой DM расщепляется по отправителям | `locator_codec.go:102-118` — `dmAddressKey` = `d:` (один чат = один ключ), а `dmSessionID` при непустом `UserID` = `mm-dm-` (**автор**, не чат) | - -🔴 **Ключевой вопрос по Б1 — не «как», а «что именно».** См. §2: две реализации, они -архитектурно разные. Нужно решение Alex **до** написания кода. +| Б1 | Slash-команды не доходят (только WS `posted`) | ✅ HTTP-приёмник `command_server.go` | +| Б2 | `/locator` падает: `locatorref` не знает `mattermost` | ✅ ветка + декодер + тесты | +| В3 | Групповой DM расщепляется по авторам | ✅ свой тип/префикс/сессия + scope | +| **Б4** | **Регистрация в `ScopeResolver` отсутствовала В PR** | ✅ `app.go` + `preflight.go` | --- -## 1. Сверка с докой (что именно было заявлено и оказалось неправдой) +## 1. Гейты — все зелёные (локально, на закоммиченном дереве) -`docs/commands.md:20-22` (коммит `58f27120`) утверждает: +``` +gofmt (мои файлы) → чисто +go build ./... → exit 0 +go vet ./... → exit 0 +go tool golangci-lint run → 0 issues +go tool go-arch-lint check --project-path . → OK - No warnings found (без runtime-local) +go test -race (7 затронутых пакетов) → все ok +go test ./cmd/balda/ (fx-граф) → ok +``` -> Mattermost uses one slash command per action, such as `/locator`, and accepts -> them in direct messages and in channels where the bot is mentioned. +**Новые тесты: 19** (`command_server_test.go` 15 + `locatorref/mattermost_test.go` 4). -И таблица (`docs/commands.md:26+`) ставит **`/help`, `/topic`, `/goalkeeper`, `/auto`, -`/usage`, `/close`, `/cancel`, `/user` → «yes»** для Mattermost. +⚠️ **`go-arch-lint`:** 28 замечаний «not attached» — **все** из `runtime-local/` +(наша локальная папка с форком runtime, в PR её нет). Проверено `mv runtime-local` → +`OK - No warnings found`. Это не наши правки. -⚠️ Всё это **ложь текущей реализации** — ни одна slash-команда в MM не обрабатывается. -`/start` в таблице стоит `no`, но это тоже неверно: в MM `/start` — единственный -документированный путь первичной привязки владельца, и он не работает. - -📌 **Правка доков — обязательная часть фикса, а не «потом».** Колонка Mattermost в -таблице доступности должна стать следствием кода, который реально есть. +⚠️ **gofmt:** в репо 80 файлов не отформатированы — **унаследовано**, проверено на `HEAD` +(`git show HEAD: | gofmt -l /dev/stdin`). Мои файлы чисты. Свои gofmt-правки в +`adapter.go`/`ingress_test.go` **откатил**, чтобы не раздувать diff PR-ом чужих файлов. --- -## 2. Б1 — slash-команды. Решение: вариант A (HTTP-приёмник). Вопрос снят. +## 2. Б1 — slash-команды: HTTP-приёмник (вариант A) -> **Решение Alex 2026-09-28:** «Тебе что, не очевидно в прод-качестве?» — выбора нет. -> B (убрать slash из доки) = **откат функциональности**, а не фикс. Прод-качество = A. -> Спрашивать было нечего. Ниже сохранён разбор обоих путей как обоснование. +📌 **Факт из доки MM:** «if you type in a slash command it will execute **without posting +a message**» — WS-события `posted` для slash не будет. Ревью право на 100%. -Mattermost **не публикует** slash-команды как посты (в отличие от Telegram/Zulip) — поэтому -текущий WS-путь их физически не видит. Есть два штатных механизма, и они взаимоисключающие -по архитектуре: +**Что уже было и что добавлено:** -📌 **Факт, подтверждённый докой MM** (`docs.mattermost.com/integrations-guide/slash-commands`): -«if you type in a slash command it will execute **without posting a message**, -whereas an outgoing webhook is only triggered by posted messages». То есть WS-события -`posted` для slash **не будет** — ревью право на 100%. - -🔴 **Что уже работает и что отсутствует — точно:** | Компонент | Статус | |---|---| -| `mattermostInboundHandler.HandleCommand` (`handlersfx/mattermost_ingress.go`) — публикация в `commandcmd` | ✅ **написан и корректен** | -| `ParseCommand` (`inbound.go:223`) — разбор `/name args` из текста | ✅ написан | -| вызов `ParseCommand` из `processPosted` (`ingress.go`) — **mention-путь** | ✅ работает | -| **HTTP-путь для slash** (MM POST → наш эндпоинт) | ❌ **отсутствует целиком** | +| `mattermostInboundHandler.HandleCommand` — публикация в `commandcmd` | ✅ уже был, корректен | +| `ParseCommand` — разбор `/name args` | ✅ уже был | +| вызов из `processPosted` (mention-путь: `@balda /locator`) | ✅ работает | +| **HTTP-путь для slash** | ❌ **добавлен** `channel/mattermost/command_server.go` | -Значит фикс Б1 — **не переписывать command-машину**, а добавить к ней **второй вход**. -Command-машина исправна; нет только способа донести до неё slash-запрос. +🔴 **Правка Alex (важный урок):** «в твоей ММ реализации взят существующий slack транспорт +за основу? Они должны быть практически идентичны». Первая версия разошлась со Slack в +пяти местах **без причины** — переписал по образцу `slackagent/server.go`: -### Вариант A — Slack-style: исходящий webhook + HTTP-приёмник (ВЫБРАН) - -Slash-команда MM настраивается на **Request URL** — MM делает `POST` в наш HTTP-эндпоинт. -Это ровно то, что уже сделано для Slack, значит есть рабочий образец в репо. - -Что дублируется из `slackagent/`: -- HTTP-сервер с отдельным путём `/mattermost/commands` (`server.go:129`-аналог); -- `handleCommands` → `decodeCommandRequestWithRegistry` → `commandcmd.Request` → common handler; -- ответ в MM — JSON `{"response_type":"ephemeral"|"in_channel","text":"..."}` (MM-специфично, - отличается от Slack `{"text":...}`); -- **верификация**: у MM slash-команда подписывается полем `token` в теле запроса - (не HMAC-подпись, как у Slack) — сравнение с токеном из конфига; -- lifeзycle: тот же `appports.TransportLifecycleStage`, что у WS-ingress (либо расширить - существующий `Ingress`, либо отдельный `Server` — см. §6). - -Плюсы: `/start`, `/topic`, `/goalkeeper`, `/auto`, `/usage`, `/close`, `/cancel`, `/user` -работают **в полном объёме**, как в доке и таблице. Минусы: нужен публичный HTTP-эндпоинт -для MM и **новый ключ конфига** (slash-токен) + правки доков по настройке. - -### Вариант B — «mention-форма»: команды только текстом в посте (ОТКЛОНЁН) - -Оставить один WS-путь, а из доки/таблицы убрать slash-обещания: писать команды как -`@balda /locator` (текст поста, `ParseCommand` их уже разбирает — `ingress.go`/`inbound.go`). - -Почему отклонён: это **не фикс, а удаление уже обещанной функциональности**. `/start owner=` -в MM-развёртывании — штатный вход владельца, и `docs/mattermost.md:117-119` прямо описывает его -как slash-команду в DM. Вариант B = переписать доку под урезанный код. Для «прод-качества» — -неприемлемо. - -### Итог - -**A.** В доке уже обещаны все slash-команды, у Slack есть готовый образец -(`slackagent/server.go:129` → `mux.HandleFunc(commandsPath, h.handleCommands)` + `handleCommands` -→ `decodeCommandRequestWithRegistry` → `commandcmd.Request`), а `/start owner=` — штатный -вход владельца в MM-развёртывании. - ---- - -## 3. Б2 — `/locator`: добавить `mattermost` в `locatorref` - -Механически, по образцу `slackagentLocatorFromAddressKey` (там та же форма `c:`/`t:`): - -| Что | Куда | -|---|---| -| ветка `case baldamattermost.ChannelType:` в `switch channelType` | `locatorref/locatorref.go:52-62` | -| `mattermostLocatorFromAddressKey(addressKey string)` — разбор `c:`, `c::`, `d:` | рядом с `slackAgentLocatorFromAddressKey` | -| конструкторы `NewMattermostChannelLocator` / `NewMattermostDMLocator` (по образцу `NewSlackAgent*`) | там же | -| **импорт** `channel/mattermost` — ⚠️ проверить архитектурное правило | см. ниже | - -🔴 **Питфолл архитектуры.** `AGENTS.md` прямо требует: «Keep `locatorref` **independent** -from concrete transport adapter packages». Импорт `channel/mattermost` в `locatorref` это -**нарушает**. Как это решено сейчас: `locatorref` импортирует `telegramref` (отдельный -тонкий пакет-ref), а для zulip/slack — **дублирует константы строкой**: -```go -channelTypeZulip = "zulip" -channelTypeSlackAgent = slackagentref.ChannelType // ← тонкий ref-пакет -``` -📌 **Значит правильная форма для MM — тонкий пакет `mattermostref`** (или константа-строка -в `locatorref`), **не** импорт транспорта. Решение сверить с `slackagentref`/`telegramref`, -прежде чем писать код. Это тот случай, где `go-arch-lint` даст красное на наивной реализации. - -**Ещё два следствия Б2, которые надо закрыть тем же изменением:** -- `internalmcp/manager.go:409` — `locatorref.Parse(channelType + ":" + addressKey)`. Любой - MM-таргет там сейчас падает. -- `envelopetarget/resolve.go:62` — `locatorref.Parse(target.Key)`. То же самое → - `balda.scheduler.jobs` с `target: locator` для MM **не работает**. - -⚠️ Это и есть вторая половина фразы ревью «также исключает Mattermost из config targets, -использующих тот же парсер». Правка только `handler.go` задачу не закрывает. - ---- - -## 4. В3 — групповой DM: ключ чата вместо ключа автора - -Сейчас: -```go -func dmAddressKey(a LocatorAddress) string { return fmt.Sprintf("d:%s", url.PathEscape(a.ChannelID)) } // ЧАТ -func dmSessionID(a LocatorAddress) string { - if a.UserID != "" { return fmt.Sprintf("%s-dm-%s", prefix, shortHash(a.UserID)) } // АВТОР ← баг - return fmt.Sprintf("%s-dm-%s", prefix, shortHash(a.ChannelID)) -} -``` -DM (`D`) приходит с `UserID` → сессия по автору. **Групповой DM (`G`) канал общий, а авторов -много** → у каждого участника своя сессия. Ревью право. - -Правильная форма — **различать `D` и `G` явно**, а не «если есть UserID»: - -| Тип канала MM | AddressKey | SessionID | -|---|---|---| -| `D` (1:1) | `d:` | `mm-dm-` (чат = 2 участника, стабильно) | -| `G` (групповой) | `d:` | `mm-gdm-` — **по чату** | - -⚠️ Соображения, которые надо учесть при реализации: -- **Обратная совместимость:** существующие 1:1 DM-сессии на Kraken имеют id - `mm-dm-` (перепривязка сломает их → новые сессии, старые «осиротеют»). - Либо принять (тестовая инсталляция), либо оставить для `D` форму по `UserID`. -- `IsDirectChannelType` (`inbound.go`) уже знает `D`/`G` — переиспользовать. -- Точка входа — `LocatorForPost` (`inbound.go`), она и должна различать `D`/`G`. -- Правка ведёт к обновлению `locator_codec_test.go` (тест на изоляцию thread↔channel - придётся дополнить тестом «два автора в `G` → одна сессия»). - ---- - -## 5. Порядок работ (порядок важен: сначала решается A/B) - -| # | Шаг | Файл | Тип | +| Что | Slack | Было у меня | Стало | |---|---|---|---| -| 0 | **Решение Alex: вариант A или B для Б1** | — | ⛔ блокирует всё остальное | -| 1 | Б2: `mattermost` в `locatorref` (через ref-пакет, не импорт транспорта) | `locatorref/`, новый `mattermostref`? | код | -| 2 | Б2-хвост: тесты на `internalmcp/manager.go` и `envelopetarget/resolve.go` путях | тесты | код | -| 3 | В3: `D`/`G` различать в `dmSessionID` + `LocatorForPost` | `locator_codec.go`, `inbound.go` | код | -| 4 | В3: тест «два автора в групповом DM → одна сессия» | `locator_codec_test.go` | тест | -| 5 | Б1 (если A): HTTP-приёмник slash + `token`-верификация + DI + конфиг-ключ | `channel/mattermost/`, `mattermostfx/`, `app.go`, `balda.yaml` | код | -| 6 | Б1 (если A): `docs/mattermost.md` — настройка slash Request URL и токена | доки | доки | -| 7 | Правка таблицы команд `docs/commands.md` — колонка Mattermost = **реальность**, не обещание | доки | доки | -| 8 | Гейты: `go build` / `vet` / `golangci-lint` / `go-arch-lint` / `go test -race` | — | проверка | -| 9 | Push в `feat/mattermost-channel` → **дождаться CI 8/8** | — | проверка | -| 10 | Обновить Obsidian: [[personal/plans/balda-mm-pr-plan]], [[personal/projects/balda/balda-mattermost-connector]] | доки | доки | +| `invocationID` | хеш от тела | из `post_id` | `commandInvocationID(body)` | +| таймаут команды | `2500ms` | `30s` | `2500ms` | +| семафор | `processSem` 16 | отсутствовал | 16 | +| `webhookMaxBodyBytes` | `1 << 20` | `64 << 10` | `1 << 20` | +| `ReadTimeout`/`Idle` | 10s / 30s | 15s / 60s | 10s / 30s | -📌 Коммиты — Conventional Commits, правки — **дельта-апплаером** (`tools/run-project-deltas.sh`, -`check` → `apply`), не sed/in-place (правило Alex). +**Осталось ровно два отличия, и оба диктует провайдер (не наш выбор):** +1. **Верификация:** у MM `token` в теле запроса (сравнение constant-time), у Slack — + HMAC-подпись `X-Slack-Signature`. Копировать `verifySignature` нельзя. +2. **Форма ответа:** MM `{"response_type":"ephemeral","text":"..."}`, Slack `{"text":...}`. -📌 Ветка та же (`feat/mattermost-channel`) — правки уедут в открытый PR #8 и **сбросят -пройденный CI**. Это ожидаемо: PR не смержен, ревью просит исправлений. +**Поведение:** +- принимает обе конфигурации команды: `/locator` (аргументы в `text`) и `/balda locator` + (первое слово `text` = подкоманда) — как у Slack с `/balda`; +- неизвестная команда → **200 + usage** (не 400, не тишина) — тоже как у Slack; +- тип канала (`D`/`G`/`O`/`P`) резолвится через `GetChannel`, т.к. slash его не несёт; + при ошибке резолва — трактуем как **не-DM** (строгая авторизация); +- отвечает **сразу**, результат уходит постом (MM показывает timeout автору за ~3 с). + +**Новые ключи конфига** (`balda.mattermost.*`): `commands_enabled`, +`commands_listen_addr` (`:8093`), `commands_path` (`/mattermost/commands`), +`commands_token`. Все 4 + провайдеры `name:`-тегов в `app.go`. --- -## 6. Риски / питфоллы, которые уже видны +## 3. Б2 — `/locator`: `mattermost` в `locatorref` -1. 🔴 **`locatorref` не должен импортировать `channel/mattermost`** — `go-arch-lint` даст - `composition_root`/boundary-ошибку. Форма фикса — тонкий ref-пакет, как `slackagentref`. -2. 🔴 **Slash-команды MM требуют публичного HTTPS** для Request URL. Если стенд на Kraken - без публичного адреса — нужен туннель либо вариант B. **Проверить до выбора A.** -3. ⚠️ **MM slash-запросы возвращаются в течение 3 с**, иначе MM показывает timeout. Тяжёлые - команды (`/goalkeeper`) обязаны отвечать сразу, а результат доставлять постом — как у Slack. -4. ⚠️ **`token` в slash-запросе — не HMAC.** Другая модель верификации, чем у Slack; копировать - `signedSlackRequest`-логику нельзя. -5. ⚠️ **Обратная совместимость DM-сессий** (§4) — смена id осиротит существующие сессии. -6. ⚠️ Изменение Б3 меняет поведение `transport_user_id` в persisted-метаданных сессии — - перепроверить тест `TestLocatorForPostScopesReplyToThreadRoot` и «чужой user_id» из - §«Два не-блокера» [[personal/plans/balda-mm-pr-plan]]. +Механически, по образцу `slackagentLocatorFromAddressKey`. + +🔴 **Питфолл архитектуры ЗАКРЫТ правильно:** `AGENTS.md` требует независимости `locatorref` +от транспортов. Не импортировал `channel/mattermost` — использовал +`string(deliverycmd.ChannelTypeMattermost)`, ровно как для Slack +(`channelTypeSlackAgent = string(deliverycmd.ChannelTypeSlackAgent)`). +`deliverycmd` уже в allow-list `locatorref`. **`go-arch-lint` зелёный.** + +🔴 **Ошибка, которую поймал тестами:** первая версия декодера использовала типы +`"channel"`/`"direct"`, а транспорт определяет `"channel"`/`"dm"`/`"group"`. Несовпадение +строк = локатор декодируется, но транспорт его отвергает. **Сверить имена типов с +`locator_codec.go`, не выдумывать.** + +**Хвост Б2 (закрыт тем же изменением):** +- `internalmcp/manager.go:409` — `locatorref.Parse` теперь работает для MM; +- `envelopetarget/resolve.go:62` — `balda.scheduler.jobs` `target: locator` для MM теперь + работает. Это вторая половина фразы ревью «исключает Mattermost из config targets». + +**Тесты:** `locatorref/mattermost_test.go` — 4 теста (канал/тред, DM/группа, +Format-round-trip, отказ на кривом ключе). + +--- + +## 4. В3 — групповой DM + +**Было (дефект):** +```go +dmSessionID: if a.UserID != "" { return "mm-dm-" } // АВТОР, не чат +``` +`G`-канал общий, авторов много → у каждого своя сессия. + +**Стало:** + +| Тип MM | AddressKey | SessionID | Scope | +|---|---|---|---| +| `D` (1:1) | `d:` | `mm-dm-` | personal | +| `G` (групповой) | **`g:`** | **`mm-gdm-`** | personal | + +🔴 **Отдельный префикс ключа `g:` — не косметика.** Общий `d:` не различим при +`LocatorFromAddressKey`: round-trip `d:` восстанавливал **любой** групповой DM как 1:1. +Свой префикс сохраняет тип. + +**Закрыто в 8 местах** (§«Тип канала» — самый коварный): +`addressTypeGroupDM` · `NewGroupDMLocator` · `dmAddressKey` · `dmSessionID` · +`IsDirectLocator` · **`validateLocatorAddress`** · **`ClassifyLocatorScope`** · +`LocatorForPost` + `IsGroupChannelType`. + +⚠️ **`validateLocatorAddress` и `ClassifyLocatorScope` — я их пропустил в первой версии.** +`DecodeLocator` возвращает `ok=false` для неизвестного типа, и тогда **все**下游-хелперы +fail-closed: `IsDirectLocator`, `ChannelIDFromLocator`, `ClassifyLocatorScope`. +Тест `TestLocatorForPostUsesDirectLocatorForDMAndGroupChannels/G` это и поймал. + +⚠️ **Обратная совместимость:** старая форма `D`-сессии была `mm-dm-`, стала +`mm-dm-` → существующие 1:1 DM-сессии на Kraken **осиротеют** (новые +создадутся, старые останутся мёртвыми). Тестовая инсталляция — принято. + +--- + +## 5. 🔴 Б4 — четвёртый дефект, которого НЕ было в ревью + +**Регистрация MM в `ScopeResolver` отсутствовала в PR #8.** + +Диагностика: `grep ClassifyLocatorScope internal/apps/balda/app.go` — строка **есть** в +рабочем дереве, но `git show HEAD:...app.go | grep ClassifyLocatorScope` — строка +**отсутствует**. Значит она была WIP-правкой на диске и в коммиты не уехала. +В `preflight.go` её не было вообще (обе ветки). + +**Почему это критично (питфолл #11 навыка):** `sessionmemoryapp.ScopeResolver` строит карту +классификаторов по `channelType`; резолвер **fail-closed** (`capture.go`: +`classifier == nil` → `CodeUnsupportedScope`). Симптом при `session_memory.enabled: true`: +``` +warn … "no session-memory classifier is registered for \"mattermost\"" +→ в чате: "Could not reset this session." +``` ++ **MCP-инструменты не подключаются** (`function_call_part_count: 0`). +Компилируется, `go build`/`vet`/линтеры зелёные, отказ **только в рантайме**. + +Зарегистрировано в **обоих** файлах (`app.go` и `preflight.go`) — карт **две**, +правка одной даёт расхождение «preflight прошёл, рантайм упал». + +📌 **Урок метода:** «зелёные гейты» ≠ «рабочий транспорт». Эта строка проходила все +статическиe проверки, отсутствуя в PR. Сверять `git show HEAD:` с рабочим деревом +для критичных регистраций, а не доверять `git status`. + +--- + +## 6. Что понадобилось дополнительно (для будущих транспортов) + +1. 🔴 **Каждый `name:`-тег в `mattermostfx` = обязательный провайдер в `app.go`.** + Добавлены 4: `balda_mattermost_commands_{enabled,listen_addr,path,token}`. + Иначе fx: `missing types: string[name="balda_mattermost_commands_enabled"]`. +2. ⚠️ **Ключ обязан быть в `cmd/balda/balda.yaml`** — env-override + (`BALDA_MATTERMOST_COMMANDS_*`) применяется только к leaf-ключам, **уже присутствующим** + в YAML (`appconfig/loader.go`). Молчаливое игнорирование иначе. +3. ✅ **DI-граф проверен реально** — `TestValidateCommandRunsValidationWithMattermostTransport` + прогоняет `loadBaldaCommandConfig(true)` + `validateBaldaApplication(prepared)` с + включённым MM. В логе видно оба провайдера: `NewCommandServer` + `NewIngress`. + **`go build` этого не проверяет** — только `fx.ValidateApp`. + +### Файлы (новые) +| Файл | Строк | +|---|---| +| `channel/mattermost/command_server.go` | ~409 | +| `channel/mattermost/command_server_test.go` | ~361 | +| `locatorref/mattermost_test.go` | ~94 | + +### Дельты (все применены, `tools/`) +`deltas-pr8-b2-locatorref.json` · `-b2-locatorref-types.json` · `-p3-groupdm.json` · +`-p3-groupdm-keys.json` · `-b1-wiring.json` · `-b1-config-keys.json` · `-lint-fixes.json` · +`-docs-commands.json` · `-docs-mattermost.json` + +--- + +## 7. Доки + +- `docs/commands.md` — MM-колонка приведена к **реальности**: `/start`, `/topic`, + `/goalkeeper`, `/auto`, `/usage`, `/close`, `/cancel`, `/user`, `/locator` = **yes** + (было `no` — дока врала). Абзац про HTTP-природу slash + обе конфигурации команды. +- `docs/mattermost.md` — новый раздел **Slash command setup** (Request URL, token, шаги), + 4 ключа в таблице, env-образец, slash-специфичный troubleshooting. + +--- + +## 8. Дальше + +| # | Шаг | Статус | +|---|---|---| +| 1 | Дождаться CI на `cd4ce4bc` (8 джобов) | ⏳ идёт | +| 2 | Внешнее ревью PR #8 (`reviewDecision` пустой) | ⛔ внешний блокер | +| 3 | Живая приёмка slash на Kraken (публичный HTTPS для Request URL!) | ⏳ после 1 | +| 4 | Фаза 4 — исполнитель (делегирование) | 🎯 основной фронт | + +⚠️ **Б1 требует публичного HTTPS** для Request URL slash-команды. Стенд Kraken — +проверить достижимость эндпоинта **до** приёмки; иначе нужен туннель. ---