[2026-09-28] eagle: personal/plans/balda-mm-phase4-agents.md personal/plans/balda-mm-pr-plan.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-28 11:44:54 +06:00
1 parent b49ef14a34
commit a03203fc96
2 files changed
+336 -363

No files matched your search

+136 -2
View File
@@ -25,6 +25,7 @@
| 6g | A/B-эксперимент откатом на `0.1.11` | ✅ **доведён 2026-09-27** — `0.1.11` падает С ТЕМ ЖЕ `duplicate tool`. Гипотеза §16.2 («форк обнажил») **ОПРОВЕРГНУТА**: баг был всегда, независимо от раунда. См. §16.4 |
| 6h | Фикс дублирующегося тулсета | ✅ **ИСПРАВЛЕНО 2026-09-27, `balda-mm:0.1.13` задеплоен** — дедуп по эндпоинту в `hostedToolsets` (`runtime-local/agentfactory/agentfactory.go`) + 6 тестов. `go build`/`go vet`/`go test ./...` ✅. Приёмка от владельца: `@balda вызови executor_health` → ответ `**executor_health:** ok`, `turn_complete: true`, `duplicate tool:` = **0**, deadletter = **0**. См. §19.3-19.5 |
| 7 | Приёмка от аккаунта Alex | ✅ **ПРОЙДЕНА 2026-09-27** — пост владельца `cbwsorzwoigbtrdnteugnx4aqo` → ответ бота `edxrgupoxiyig5gzmr79tjypbme`: `**executor_health:** ok`. `settlement_stage: accepted`, `turn_complete: true`, инструмент исполнился. См. §17, §19.5 |
| 8 | 🔴 **CLI исполнителя НЕ залогинен → реальная задача не проходит** | 🔴 **Открыто 2026-09-28** — весь конвейер живой, упирается ТОЛЬКО в логин CLI. `POST /task` через мост → `exit_code: 1`, `duration 0.8 c`, `stdout: "Not logged in · Please run /login"`. Диагноз, доказательства и два пути фикса — **§20**. Ждёт решения Alex |
---
@@ -315,8 +316,12 @@ lsof -nP -iTCP:8790 -sTCP:LISTEN
🔴 **Продолжение 2 (§§15-16, 2026-09-27):** инструменты всё равно не исполнялись — в
`runtime v2` вырезан tool_calls. Форк собран и задеплоен (`0.1.12`), и **обнажил** двойную
паковку инструментов (`duplicate tool` вернулся). Разбор — §16.
4. 🟡 Логин CLI (Alex вручную) — метод приёмки в обход (admin PAT, §17) есть, но сквозная
приёмка упирается в `duplicate tool` (§16).
4. 🟡 Логин CLI (Alex вручную) — **ЭТО ЕДИНСТВЕННЫЙ ОСТАВШИЙСЯ БЛОКЕР Фазы 4**
(перепроверено 2026-09-28). Прежняя формулировка «сквозная приёмка упирается в
`duplicate tool` (§16)» **устарела** — тот дефект закрыт 27.09 (`0.1.13`, §19.5).
Сейчас путь такой: конвейер целиком исправен, `executor_health` вызывается и отвечает,
но **любая настоящая задача падает на `Not logged in · Please run /login`** — потому что
CLI в контейнере-исполнителе не авторизован. Полный диагноз и два пути фикса — **§20**.
5. 🔴 **Завести Access Application** на `agents-bridge.qentra.top` — сейчас туннель
отвечает 200 без авторизации (проверено). Защита — только `X-Bridge-Secret`.
6. ⚠️ **`~/Developer/balda-executor-mcp/docker-compose.yml` в репо → `0.2.0`**;
@@ -1298,6 +1303,135 @@ I/O-wait на HDD). Симптомы, которые **не** являются
---
## 20. 🔴 Проверка 2026-09-28: единственный блокер — CLI не залогинен
Сессия началась с вопроса Alex: «Что далее чтобы заставить все это работать?». Проведён
**аудит всей цепочки живыми вызовами** (не по докам). Итог: Фаза 4 завершена везде, кроме
одного звена — **авторизации CLI внутри контейнера-исполнителя**.
### 20.1 Что проверено живыми вызовами и РАБОТАЕТ
| Узел | Проверка | Результат |
|---|---|---|
| Балда + бот MM на Kraken | `docker ps`, `docker-compose.yml` | `balda-mm:0.1.13` Up 19h ✅ |
| Логи Балды | `docker logs balda-mm --since 6h \| grep -c error` | **0** ошибок ✅ |
| `duplicate tool` не вернулся | grep по логам | чисто ✅ |
| MCP-сервер исполнителя | `initialize` из контейнера `mattermost` | HTTP 200, `serverInfo.name=balda-executor-mcp` ✅ |
| Мост на Mac | `curl 127.0.0.1:8790/healthz` | `status: ok`, `container_running: true` ✅ |
| Cloudflare Tunnel | `curl https://agents-bridge.qentra.top/healthz` | 200 с обеих сторон ✅ |
| Контейнер-исполнитель | `docker ps` | `balda-executor:0.1.0` Up 40h (healthy) ✅ |
| Тулчейн в контейнере | `docker exec … which` | `claude` + `codex` + `node` + `git` на месте ✅ |
| PR #8 | `gh pr view 8` | `OPEN`, `MERGEABLE`, `reviewDecision: ""` — внешний блокер, не наш ✅ |
### 20.2 Что СЛОМАНО — ровно одна вещь
Задача через мост (`POST /task`, `provider: claude`):
```bash
cd ~/Developer/agents-bridge
SECRET=*** # из .env, поле BRIDGE_SECRET
curl -s -X POST http://127.0.0.1:8790/task \
-H "Content-Type: application/json" -H "X-Bridge-Secret: $SECRET" \
-H "Idempotency-Key: probe-auth-$(date +%s)" \
-d '{"prompt":"Reply with exactly: BRIDGE_OK"}'
# → {"task_id":"48fcd4a7d0a44a0c","status":"queued",…}
```
Результат задачи (`GET /task/<id>` с заголовком `X-Bridge-Secret`):
```json
{ "status": "failed", "exit_code": 1, "duration_s": 0.8,
"stdout": "Not logged in · Please run /login\n", "stderr": "" }
```
📌 **Конвейер рабочий**: задача дошла до CLI, CLI ответил, ответ вернулся через мост.
Падение — внутри CLI, на проверке авторизации.
### 20.3 Доказательство, что логина нет НА ХОСТЕ (а не просто в контейнере)
```bash
docker exec balda-executor sh -c 'jq -c "keys" /root/.claude.json'
# → ["firstStartTime","firstStartVersion","hasResetAutoModeOptInForDefaultOffer",
# "machineID","migrationVersion","opusProMigrationComplete","pluginUsage",
# "seenNotifications","sonnet1m45MigrationComplete","userID"]
```
Это **свежий стартовый файл**: 9 служебных ключей, **ключа `oauthAccount` нет**.
Никаких следов авторизации.
```bash
ls -la ~/cli-auth/claude/
# → backups/ projects/ sessions/ ← монтируется в /root/.claude
ls -la ~/cli-auth/claude/backups/
# → .claude.json.backup.1790423979201 (84 байта)
# .claude.json.backup.1790435884422 (84 байта)
```
🔴 **В хост-монтировании `~/cli-auth/claude/` НЕТ файла `.claude.json`.**
Лежат только `backups/`, `projects/`, `sessions/`. Тот файл в контейнере (`692 байта`,
`Sep 26 15:18`) — это **контейнерная копия, созданная самим CLI**, а не проброшенный
логин. Бэкапы по 84 байта — тоже не креды.
**Корень:** логин никогда не был посажен туда, откуда он переживает пересоздание контейнера.
`docker compose up -d` пересоздаёт контейнер, монтирование отдаёт пустой набор файлов
(только каталоги), CLI генерирует себе пустой `.claude.json` с нуля → каждый раз «Not logged in».
### 20.4 Контекст: как CLI вызывается
`~/Developer/agents-bridge/executor.py:21-25`:
```python
PROVIDERS: dict[str, list[str]] = {
"claude": ["claude", "-p", "--output-format", "text"],
"codex": ["codex", "exec", "--skip-git-repo-check"],
}
# :41 — запуск происходит НЕ на хосте, а через docker exec в контейнер
return ["docker", "exec", "-w", WORKDIR, "@container", *base, prompt]
```
Значит авторизация нужна **внутри контейнера**, а не на Mac. `WORKDIR = "/agents"`.
### 20.5 Два пути фикса (решение за Alex — ждёт ответа)
**Путь A (рекомендован) — `claude setup-token`, токен в переменную окружения.**
Почему лучше: долгоживущий токен **не файл** → пересоздание контейнера его не затирает.
Документированный Anthropic путь для headless (см. §0.6 «Юридическая граница» — разрешён,
не дыра).
1. **Alex на Mac:** `claude setup-token` (открывает браузер, аккаунт подписки) → получает токен.
2. Токен кладём в `~/Docker/balda-executor/.env` как `CLAUDE_CODE_OAUTH_TOKEN=…`
(`.env` — `0600`, уже в `env_file` compose → в контейнер попадает автоматически).
3. Проверка: задача через мост → ждём ответ модели вместо `Not logged in`.
⚠️ **Питфолл записи в `.env`:** `write_file` редактирует/калечит секреты
(`personal/tech/hermes-secret-redaction-bypass`) — значение собирать **на хосте**
скриптом, не литералом.
**Путь B — залогиниться в контейнере, затем СКОПИРОВАТЬ креды на хост.**
1. **Alex:** `docker exec -it balda-executor claude` → `/login`.
2. Я **копирую** (не перемещаю) `~/.claude.json` и `.credentials.json` из контейнера в
`~/cli-auth/claude/` **с бэкапом** существующего каталога.
3. Проверка: `docker compose up -d` (пересоздание) → повторить задачу → логин должен выжить.
Почему B хуже: зависит от того, что CLI держит всё в файлах и что структура не поменяется;
`setup-token` — официально поддерживаемый для headless путь.
**Не делать:** логиниться на голом Mac в `~/.claude` и ждать, что это проедет в контейнер —
монтируется `~/cli-auth/claude`, **не** `~/.claude`. На хосте Mac логин вообще не участвует.
### 20.6 Что проверить ПОСЛЕ фикса (сквозная приёмка Фазы 4)
1. `POST /task` через мост → реальный ответ модели, `exit_code: 0`.
2. Пост от **владельца** в MM (`@balda …`) → Балда → `executor_run` → мост → CLI → ответ в чат.
Метод постинга от владельца — §17.1 (admin PAT через `mmctl --local token generate`).
3. `codex` тоже проверить (второй провайдер в том же образе).
4. Обновить §0: строка 8 → ✅, Фаза 4 → закрыта; Фаза 5 (связка и приёмка) в
[[personal/projects/balda/balda-mm-delegation-plan]].
---
## Связанные заметки
- [[personal/projects/balda/balda-mm-delegation-plan]] — общий план (Фазы 4-5)
- [[personal/projects/balda/balda-kraken-mm-deploy]] — Фаза 3 (✅)
+200 -361
View File
@@ -1,44 +1,83 @@
# Balda — Mattermost connector: PR-подготовка (закрыто)
# Balda — Mattermost connector: PR-подготовка
> Создан 2026-09-26. Обновлён **2026-09-28** (сверка PR + разбор незакоммиченных файлов).
> Статус: **✅ PR ОТКРЫТ — baldaworks/balda#8, 8/8 CI PASS, MERGEABLE. Ревью апстрима НЕТ (`reviewDecision` пустой).**
> Обновлён 2026-09-28. Обновлён 2026-09-28 (ревизия: убраны дубли, оставлены факты).
> Статус: **PR ОТКРЫТ — baldaworks/balda#8 · OPEN · MERGEABLE · 8/8 CI PASS · ревью апстрима НЕТ.**
> https://github.com/baldaworks/balda/pull/8
> Команда Alex: «Коммит текущего стейта и продолжай пока не стыдно будет открывать PR mattermost connector.
> Со всеми поддерживаемыми фичами и тестами. Вперёд! Без вопросов и требующих апрува команд.»
> Часть [[personal/projects/balda/balda-mm-delegation-plan]] (Фаза 2 → PR в апстрим).
>
> 📌 **Читать два раздела 2026-09-28 ниже:** «5 незакоммиченных файлов» (НЕ фиксы, не трогать)
> и «Проверка состояния PR» (следующие шаги по приоритету).
## ✅ БОЕВАЯ ПРИЁМКА ПРОЙДЕНА — 2026-09-26 (образ `0.1.10`, коммит `9f1e6c9c`)
---
**Сквозной путь работает ОТ АККАУНТА ALEX.** Это первая настоящая приёмка (прошлая была
ложной — от `testuser`).
## 1. Состояние сейчас (сверено 2026-09-28 через API, не по доке)
| | |
|---|---|
| PR | **baldaworks/balda#8** — `feat(mattermost): add Mattermost channel transport` |
| Состояние | `OPEN` · `MERGEABLE` · `reviewDecision` **пустой** |
| База / голова | `baldaworks/balda:main` ← `mallexxx:feat/mattermost-channel` |
| Объём | **41 файл, +5622 / −18**, 7 коммитов |
| CI | **8/8 PASS** |
```
architecture-lint pass 2m2s
ingress-e2e pass 30s
jetstream-integration pass 1m0s
lint pass 2m46s
postgres-integration pass 57s
projection-replay pass 26s
sqlite-integration pass 1m15s
test pass 6m0s ← главный: полный go test на чистом раннере
```
**Ничего не изменилось с 26.09** — новых проверок нет, ревью апстрима не состоялось.
🔴 **Блокер: `reviewDecision` пустой = PR никто не смотрел.** Мержа не будет, пока внешний
ревьюер не заглянет. Наш ход тут — только периодически перепроверять
`gh pr view 8 --repo baldaworks/balda --json reviewDecision`.
🔴 **Целевой репозиторий — `baldaworks/balda`, НЕ `normahq/balda`.** Проверено через API,
не по названию remote'а:
```
gh repo view mallexxx/balda --json parent
→ parent: baldaworks/balda ← форк от него
gh repo view normahq/balda --json owner
→ owner.id: O_kgDOEgAHrw (= baldaworks) ← редирект, не отдельный репо
```
`origin` = `mallexxx/balda` (форк), `upstream` = `normahq/balda` (редирект на baldaworks).
PR создавать **в `baldaworks/balda`**, head указывать как `mallexxx:<branch>` — не полный
URL форка, иначе `gh` путается.
---
## 2. Боевая приёмка — ПРОЙДЕНА 2026-09-26 (образ `0.1.10`, коммит `9f1e6c9c`)
Сквозной путь работает **от аккаунта Alex**. Это первая настоящая приёмка — прошлая была
ложной (от `testuser`).
### Доказательство — лента канала `balda-test`
```
1790413414689 | edxrgupoxiyg5gzmr79tjypbme | слово ← ОТВЕТ БОТА
1790413414689 | edxrgupoxiyig5gzmr79tjypbme | слово ← ОТВЕТ БОТА
1790413409055 | cbwsorzwoigbtrdnteugnx4aqo | @balda скажи одно слово ← пост ALEX
```
Запрос — `cbwsorzwoigbtrdnteugnx4aqo` (**аккаунт Alex**, `admin`), ответ — бот.
В логах: **`settlement_stage=accepted`** (НЕ `unauthorized`!), `assembled provider user content`,
В логах: **`settlement_stage=accepted`** (НЕ `unauthorized`), `assembled provider user content`,
`delivery settled`.
### Состояние привязки владельца — починено
### Привязка владельца — починена
```
balda_user_bindings:
channel_type = mattermost
principal = cbwsorzwoigbtrdnteugnx4aqo ← БЕЗ префикса ✅ (иначе unauthorized)
principal = cbwsorzwoigbtrdnteugnx4aqo ← БЕЗ префикса ✅ (иначе unauthorized)
display_name = mattermost:cbwsorzwoigbtrdnteugnx4aqo ← ОДИНАРНЫЙ префикс ✅ (нет мусора)
```
Прошлый дефект двойного префикса вылечен **в коде** (идемпотентный `MattermostSubject`),
в БД мусора нет.
Дефект двойного префикса вылечен **в коде** (идемпотентный `MattermostSubject`), в БД мусора нет.
### WS-здоровье
### WS-здоровье и старт
| Метрика | Значение |
|---|---|
@@ -48,29 +87,29 @@ balda_user_bindings:
| `RestartCount` | **0** ✅ |
| Контейнер | `balda-mm:0.1.10` Up стабильно |
### Старт — чистый
```
mattermost transport configured server_url=http://mattermost:8065 bot_user_id_set=true
session manager ready balda_provider=deepseek
mattermost websocket connected ws://mattermost:8065/api/v4/websocket
Balda bot started.
```
Никакого crash-loop, `advertised commands have no actor handler` **отсутствует**.
### ⚠️ Два не-блокера, которые стоит знать
Никакого crash-loop, `advertised commands have no actor handler` отсутствует.
1. **`warn: getMe response missing result`** при старте. Косметика: startup-лог не смог
прочитать identity бота. На работу НЕ влияет — `bot_user_id_set=true` из конфига, ответы
идут. Стоит починить отдельно (low priority, в апстриме).
2. **`transport_user_id = c6udeze8dfrdxm5jbgq55bd58r` в логах сессии** — это **`testuser`**,
id прошлого владельца, зашитый в **persisted-метаданные сессии** `mm-c-7aafea13` (создана
в прошлой сессии от testuser). Авторизация при этом прошла верно (по `principal` из
bindings), ответ ушёл Alex'у. **Но это грязь:** сессия хранит чужой `user_id`.
🔴 Если Alex хочет чистую сессию — `/reset` в канале создаст новую с его `user_id`.
### Два не-блокера
1. **`warn: getMe response missing result`** при старте — косметика: startup-лог не смог
прочитать identity бота. На работу НЕ влияет (`bot_user_id_set=true` из конфига).
Low priority, чинить в апстриме.
2. **`transport_user_id = c6udeze8dfrdxm5jbgq55bd58r`** в логах сессии — это `testuser`,
id прошлого владельца, зашитый в persisted-метаданные сессии `mm-c-7aafea13` (создана
в прошлой сессии от testuser). Авторизация прошла верно (по `principal` из bindings),
ответ ушёл Alex'у, **но сессия хранит чужой `user_id`**. Чистится `/reset` в канале.
На работу не влияет (проверено: ответ пришёл).
### Деплой — как сделано (воспроизводимо)
---
## 3. Сборка и деплой (воспроизводимо)
```bash
# 1. образ из PR-коммита (на Mac)
@@ -86,83 +125,99 @@ docker save balda-mm:0.1.10 -o /tmp/balda-0110.tar
zstd -3 -T0 -o /tmp/balda-0110.tar.zst --force /tmp/balda-0110.tar
zstd -dc /tmp/balda-0110.tar.zst | ssh kraken 'docker load'
# 3. переключение с бэкапом compose (скрипт tools/mm-switch-image.sh)
# 3. переключение с бэкапом compose
ssh kraken "cat > /tmp/mm-switch-image.sh" < tools/mm-switch-image.sh
ssh kraken 'bash -n /tmp/mm-switch-image.sh' < /dev/null # SYNTAX_OK
ssh kraken 'bash /tmp/mm-switch-image.sh' < /dev/null
```
**Скрипты, созданные для этого деплоя:**
| Скрипт | Что делает |
|---|---|
| `tools/mm-switch-image.sh` | смена тега образа в compose + бэкап + `--force-recreate` |
| `tools/mm-whoami.sh` | какой MM-аккаунт у PAT (`/users/me`) — нужен для приёмки от заказчика |
⚠️ **Проверка CA в образе:** `docker save` даёт **OCI**-формат, где файлы в
`blobs/sha256/*`; `tar -tf image.tar | grep ca-certificates` даст **0** (ложный минус).
Проверять по слоям: распаковать и искать в `blobs/`, или верить `docker inspect` →
`SSL_CERT_FILE` в `Config.Env`.
⚠️ **Проверка CA в образе:** `docker save` даёт **OCI**-формат, files в `blobs/sha256/*`;
`tar -tf image.tar | grep ca-certificates` даст **0** (ложный минус). Проверять по слоям или
верить `docker inspect` → `SSL_CERT_FILE` в `Config.Env`.
⚠️ **Приёмка обязывает иметь PAT заказчика.** `/tmp/admin-tok` = PAT Alex
(`cbwsorzwoigbtrdnteugnx4aqo`, `admin`) — сверено через `mm-whoami.sh` **до** постинга,
а не на глаз. Без этого тест снова был бы от `testuser` = ложная приёмка.
(`cbwsorzwoigbtrdnteugnx4aqo`, `admin`) — сверено через `mm-whoami.sh` **до** постинга.
Без этого тест снова был бы от `testuser` = ложная приёмка.
---
## ✅ PR ОТКРЫТ — https://github.com/baldaworks/balda/pull/8 (2026-09-26)
## 4. Что в PR
**Статус: OPEN · MERGEABLE · все 8 CI-джобов PASS.**
| | |
|---|---|
| PR | **baldaworks/balda#8** |
| База | `baldaworks/balda:main` |
| Голова | `mallexxx:feat/mattermost-channel` |
| Объём | **41 файл, +5622 / −18** |
| Коммитов | 7 |
| Mergeable | ✅ MERGEABLE |
### 🔴 Целевой репозиторий — `baldaworks/balda`, не `normahq/balda`
Проверено через API, а не по названию remote'а:
```
gh repo view mallexxx/balda --json parent
→ parent: baldaworks/balda ← форк от него
gh repo view normahq/balda --json owner
→ owner.id: O_kgDOEgAHrw (= baldaworks) ← редирект, не отдельный репо
```
`origin` = `mallexxx/balda` (форк), `upstream` = `normahq/balda` (редирект на baldaworks).
**PR создавать в `baldaworks/balda`**, head указывать как `mallexxx:<branch>`
(не полный URL форка — иначе `gh` путается).
### База ветки — идеальная
`git rev-list --left-right --count upstream/main...HEAD` → `0 7` (0 позади, 7 впереди).
`merge-base HEAD upstream/main` == `upstream/main` == `de273d8f`. Rebase не потребовался.
### CI — 8/8 PASS (прогон на чистом раннере)
### Коммиты (7)
```
architecture-lint pass 2m2s
ingress-e2e pass 30s
jetstream-integration pass 1m0s
lint pass 2m46s
postgres-integration pass 57s
projection-replay pass 26s
sqlite-integration pass 1m15s
test pass 6m0s ← главный: полный go test на чистом раннере
9f1e6c9c chore: ignore local project-editing tooling
58f27120 docs(mattermost): document the transport setup and behavior
80d4f41d test(mattermost): cover the transport, the ingress and the handler
c7f18654 fix(mattermost): send a monotonic seq on every client frame
6dc3077a feat(mattermost): add the inbound handler and wire it into DI
fa1a2855 feat(auth): support the Mattermost channel subject
004fda2f feat(mattermost): add Mattermost transport adapter ← база прошлой сессии
```
📌 **Это и есть настоящая приёмка сборки.** Локально `cmd/balda` падал на
`TestInitCommand_NonInteractiveAutoSelectsRootAndGeneratesDetectedAgents` — в CI он
**прошёл**, то есть падение было особенностью моей локальной среды, а не дефектом.
CI на чистом раннере закрывает ровно тот класс проблем, который локальные гейты не видят.
База ветки идеальная: `git rev-list --left-right --count upstream/main...HEAD` → `0 7`
(0 позади, 7 впереди), `merge-base` == `upstream/main` == `de273d8f`. Rebase не потребовался.
⚠️ **CI-пайплайн в репо ЕСТЬ** (`architecture-lint`, `lint`, `test`, `ingress-e2e`,
`jetstream-integration`, `postgres-integration`, `sqlite-integration`, `projection-replay`).
Значит локальные гейты — необходимое, но не достаточное условие; ждать CI обязательно.
### Гейты — все зелёные на закоммиченном дереве
### Команды (воспроизводимо)
```
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
go test -race (затронутые пакеты) → все ok
тесты mattermost + handlersfx (verbose) → 126 PASS, 0 FAIL, 0 SKIP
```
📌 **Настоящая приёмка сборки — CI на чистом раннере.** Локально `cmd/balda` падал на
`TestInitCommand_NonInteractiveAutoSelectsRootAndGeneratesDetectedAgents` — в CI он **прошёл**,
то есть падение было особенностью локальной среды, а не дефектом. Локальные гейты —
необходимое, но не достаточное условие; ждать CI обязательно.
### Тесты — 103 добавлено (`grep -c '^func Test'`)
| Файл | Тестов | Покрытие |
|---|---|---|
| `channel/mattermost/ingress_test.go` | 23 | 🔴 `bad_seq`-регрессия, монотонность и уникальность `seq` под 16 горутинами, фильтры, panic-recovery, семафор |
| `channel/mattermost/inbound_test.go` | 18 | `NormalizeInbound`, mention-фильтр, `ParseCommand`, whitelist команд, `PostIDFromWebSocket` |
| `channel/mattermost/adapter_test.go` | 17 | все `deliverycmd.Operation`, plain-fallback только на content-rejection, draft-as-post, typing no-op без сети, `root_id` в треде |
| `handlersfx/mattermost_ingress_test.go` | 17 | публикация всех команд, **одиночный** префикс principal, `/start` до авторизации, отказ чужим, nil-safety |
| `channel/mattermost/locator_codec_test.go` | 15 | локаторы, thread↔channel изоляция, `LocatorFromAddressKey`, scope, UserID |
| `channel/mattermost/client_test.go` | 13 | ws/wss derivation, `Bearer`, `APIError`, `isContentRejectedError`, контекст |
### Что починено по существу
1. **Дефект двойного префикса — в коде, а не в БД.** Прошлый фикс был прямым `UPDATE` в
`state.db`, поэтому дефект вернулся. Теперь `auth.MattermostSubject()` идемпотентен (срезает
уже имеющийся префикс `mattermost:` перед повторной квалификацией), а из
`actors/command/start/handler.go` убран локальный `TrimPrefix`-костыль.
Почему ломало прод: `canonicalSubject` режет subject по **первому** `:`, а `GetUserByBinding`
сравнивает **точно** → `mattermost:mattermost:<id>` не совпадает ни с чем → каждый пост Alex
отбивался `settlement_stage=unauthorized`.
Регрессия: `TestMattermostSubjectIsIdempotent`,
`TestMattermostHandlerSendsChannelQualifiedPrincipalExactlyOnce`.
2. **Противоречие `ProgressPolicy.Typing`.** Command-путь ставил `Typing: true`, inbound-путь —
`false`, а `SendTyping` у MM-адаптера осознанный no-op (у ботов MM нет typing API). Мёртвая
ветка + warn-шум на каждом ходу → приведено к `false`.
3. **Доки** — `README.md`, `docs/balda.md`, новый `docs/mattermost.md` (создание бота, минтинг
токена — отдаётся ОДИН раз, membership в команде/канале, 5 ключей конфига, поведение в
DM/канале/треде, troubleshooting `bad_seq`/`unauthorized`/`x509`).
### Предсуществующие падения — НЕ наши (проверено `git stash`)
5 пакетов: `cmd/balda`, `apps/balda`, `mcpruntime`, `session`, `state`. Те же 4 теста падают
**без** наших правок:
`TestInitCommand_NonInteractiveAutoSelectsRootAndGeneratesDetectedAgents`,
`TestCanonicalSessionMemoryRuntimeComposesPortableCapabilities`,
`TestCanonicalSessionMemoryRuntimeMigratesAndReopensGroupedStore`,
`TestResolveWorkspaceBaseBranch_ConfigPreferredWhenValid`.
### Команды PR (воспроизводимо)
```bash
cd ~/Developer/balda
@@ -179,306 +234,90 @@ gh pr view 8 --repo baldaworks/balda --json state,mergeable,changedFiles,additio
gh pr checks 8 --repo baldaworks/balda
```
### Что осталось
1. **Ревью апстрима** — ждать. Правки по замечаниям делать в этой же ветке (push обновит PR).
2. **Боевая проверка на Kraken** — PR-merge ≠ работает на стенде. Пересобрать образ из
`9f1e6c9c`, задеплоить, `@balda` → ответ в ленте канала (см.
[[personal/projects/balda/balda-kraken-mm-deploy]]).
3. Фаза 4 (исполнитель) — [[personal/projects/balda/balda-mm-delegation-plan]].
---
## ✅ Итог: ветка PR-clean (2026-09-26, до пуша)
## 5. Пять файлов на диске — зачем они, НЕ трогать
**7 коммитов**, рабочее дерево **чистое**, `tools/` **исключён** из ветки.
В `~/Developer/balda` пять файлов изменены относительно коммита `9f1e6c9c` и **в PR не входят**.
Это **не фиксы** и **не потерянный код** — так и задумано.
```
9f1e6c9c chore: ignore local project-editing tooling
58f27120 docs(mattermost): document the transport setup and behavior
80d4f41d test(mattermost): cover the transport, the ingress and the handler
c7f18654 fix(mattermost): send a monotonic seq on every client frame
6dc3077a feat(mattermost): add the inbound handler and wire it into DI
fa1a2855 feat(auth): support the Mattermost channel subject
004fda2f feat(mattermost): add Mattermost transport adapter ← база прошлой сессии
```
### 🔴 Уборка `tools/` — важный урок про PR
База `004fda2f` (прошлая сессия, **не мои 5 коммитов**) затащила в ветку **16 файлов
`tools/`**: `apply-project-deltas.py`, `run-project-deltas.sh`, `deltas-mattermost*.json`,
`.deltas-state.json`. **В апстриме каталога `tools/` нет вообще** — проверено
`git ls-tree upstream/main tools/` → пусто.
**Как найдено:** `git diff --stat upstream/main...HEAD` показал `tools/` в списке файлов PR.
**Фикс (коммит `9f1e6c9c`):**
1. `/tools/` добавлен в `.gitignore`;
2. `git rm -r --cached tools/` — файлы **остались на диске** (39 файлов), ушли только из git.
⚠️ **`gitignore` НЕ отвязывает уже отслеживаемые файлы** — нужен именно `git rm --cached`.
⚠️ **Проверять состав PR через `git diff upstream/main...HEAD`, а не по своим коммитам.**
Свои 5 коммитов были чистыми (`git log upstream..HEAD -- tools/` → пусто), но ветка в
целом — нет, из-за базового коммита.
**Результат:** в PR-diff **41 файл**, все — файлы проекта, ни одного `tools/`.
### Финальные гейты — все зелёные на закоммиченном дереве
```
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
go test -race (затронутые пакеты) → все ok
тесты mattermost + handlersfx (verbose) → 126 PASS, 0 FAIL, 0 SKIP
```
**Тестов добавлено:** **103** (`grep -c '^func Test'`), по файлам:
| Файл | Тестов |
|---|---|
| `channel/mattermost/ingress_test.go` | 23 |
| `channel/mattermost/inbound_test.go` | 18 |
| `channel/mattermost/adapter_test.go` | 17 |
| `handlersfx/mattermost_ingress_test.go` | 17 |
| `channel/mattermost/locator_codec_test.go` | 15 |
| `channel/mattermost/client_test.go` | 13 |
### Предсуществующие падения — НЕ наши (проверено `git stash`)
5 пакетов: `cmd/balda`, `apps/balda`, `mcpruntime`, `session`, `state`. Те же 4 теста
падают **без** моих правок:
`TestInitCommand_NonInteractiveAutoSelectsRootAndGeneratesDetectedAgents`,
`TestCanonicalSessionMemoryRuntimeComposesPortableCapabilities`,
`TestCanonicalSessionMemoryRuntimeMigratesAndReopensGroupedStore`,
`TestResolveWorkspaceBaseBranch_ConfigPreferredWhenValid`.
### Лишнее, что я НЕ отправляю в PR
`tools/mm-*.sh` (9 ops-скриптов), `tools/deltas-*.json` (16 файлов дельта-групп),
`tools/apply-project-deltas.py`, `tools/run-project-deltas.sh`, `tools/.deltas-state.json`
— локальный инструментарий, остаётся на диске, в git не идёт.
### Осталось (по желанию Alex)
1. `git push -u origin feat/mattermost-channel` → открыть PR в `baldaworks/balda`.
2. Фаза 3/4 — [[personal/projects/balda/balda-kraken-mm-deploy]].
---
## Итог (промежуточный, до уборки tools/)
Ветка `feat/mattermost-channel`, **6 коммитов** (базовый `004fda2f` + 5 новых). Готово к `git push` и PR.
```
58f27120 docs(mattermost): document the transport setup and behavior
80d4f41d test(mattermost): cover the transport, the ingress and the handler
c7f18654 fix(mattermost): send a monotonic seq on every client frame
6dc3077a feat(mattermost): add the inbound handler and wire it into DI
fa1a2855 feat(auth): support the Mattermost channel subject
004fda2f feat(mattermost): add Mattermost transport adapter ← база
```
## Quality gates — все зелёные (проверено на закоммиченном дереве)
```
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
go test -race ./channel/mattermost/ ./handlersfx/ ./auth/ ./deliveryfmt/
./actors/command/... → все ok, включая start и auth
```
## Что сделано в этом проходе
### 1. Дефект двойного префикса — починен В КОДЕ, а не в БД (главное)
Прошлый фикс был прямым `UPDATE` в `state.db`, поэтому дефект вернулся.
Теперь источник закрыт:
- `auth.MattermostSubject()` **идемпотентен** — срезает уже имеющийся префикс
`mattermost:` перед повторной квалификацией;
- из `actors/command/start/handler.go` убран локальный `TrimPrefix`-костыль,
ветка стала как у остальных транспортов: `auth.MattermostSubject(trimmed)`.
Почему это ломало прод: `canonicalSubject` режет subject по **первому** `:`, а
`GetUserByBinding` сравнивает **точно**. Значение `mattermost:mattermost:<id>`
в `principal` не совпадает ни с чем → каждый пост Alex отбивался
`settlement_stage=unauthorized`.
**Тест-регрессия:** `TestMattermostSubjectIsIdempotent`
(`handlersfx/mattermost_ingress_test.go`) + `TestMattermostHandlerSendsChannelQualifiedPrincipalExactlyOnce`.
### 2. Исправлено противоречие `ProgressPolicy.Typing`
Command-путь ставил `Typing: true`, тогда как inbound-путь — `false`, а
`SendTyping` у MM-адаптера **осознанный no-op** (у ботов MM нет typing API).
Мёртвая ветка + warn-шум на каждом ходу. Приведено к `false`.
### 3. Тесты транспорта — 6 новых файлов (~90 тестов)
| Файл | Покрытие |
|---|---|
| `channel/mattermost/locator_codec_test.go` | локаторы, **thread↔channel изоляция**, `LocatorFromAddressKey`, scope, UserID |
| `channel/mattermost/inbound_test.go` | `NormalizeInbound`, mention-фильтр, `ParseCommand`, whitelist команд, `PostIDFromWebSocket` |
| `channel/mattermost/adapter_test.go` | все `deliverycmd.Operation`, **plain-fallback только на content-rejection**, draft-as-post, typing no-op без сети, `root_id` в треде |
| `channel/mattermost/client_test.go` | ws/wss derivation, `Bearer`, `APIError`, `isContentRejectedError`, контекст |
| `channel/mattermost/ingress_test.go` | 🔴 **`bad_seq`-регрессия**, монотонность и уникальность `seq` под 16 горутинами, фильтры, panic-recovery, семафор |
| `handlersfx/mattermost_ingress_test.go` | публикация всех команд, **одиночный** префикс principal, `/start` до авторизации, отказ чужим, nil-safety |
### 4. Доки — `README.md`, `docs/balda.md`, новый `docs/mattermost.md`
`docs/mattermost.md` — полный гайд: создание бота, минтинг токена (отдаётся
ОДИН раз), membership в команде/канале, 5 ключей конфига, поведение в
DM/канале/треде, troubleshooting (`bad_seq`, `unauthorized`, `x509`).
## Уроки метода (дополнить `balda-go-fx-pitfalls`)
1. **Тест-ассерт — это гипотеза, а не истина.** Дважды падал тест, и оба раза
неверным был ассерт, а не код: (а) «тред и корневой пост = одна сессия» —
на самом деле тред **отдельная** сессия от канала (проверено пробником:
`mm-c-a4aa02ef` vs `mm-t-89bbf64f`, два разных ответа в треде дают ОДНУ
сессию); (b) «`NewIngress` фолбэчит `botUserID` из клиента» — на самом деле
`onStart` **отказывается стартовать** без `bot_user_id`. Прежде чем править
код под падающий тест — проверить, чему код **обязан** соответствовать.
2. **`goconst` в тестах** — повторяющиеся строковые литералы (`"channel-1"` ×15)
валят линтер. Заводить константы (`testChannelID` и т.п.) сразу.
3. **`dupword`** — сообщение `"thread channel = %q, channel channel = %q"`
поймано линтером как повтор слова. Формулировать иначе.
4. **`write_file` + `***` в доке** — плейсхолдеры вида `BALDA_MATTERMOST_TOKEN=***`
в примерах конфига остаются на диске **целыми**; искажается только вывод
чтения. Проверять `awk 'NR==N'`, не глазами (см. `hermes-secret-redaction-bypass`).
5. **Артефакты деплоя — вон из PR.** `tools/mm-*.sh` (9 скриптов) и
`tools/deltas-*.json` (15 файлов) оставлены **незакоммиченными**: это
ops-инструменты и метод правок, в апстрим-репозиторий им не место.
## 🔴 Открытое решение: `tools/` закоммичены в ветку (выявлено в конце сессии)
**Найдено проверкой `git diff --stat upstream/main...HEAD`:** в диапазоне PR — 56 файлов,
и среди них **16 файлов `tools/`**, которых в апстриме **нет вообще**
(`git ls-tree upstream/main tools/` → пусто).
Кто занёс: **базовый коммит `004fda2f`** (из прошлой сессии, не из этого прохода).
Мои 5 новых коммитов чисты — проверено:
```bash
git log --oneline --name-only fa1a2855^..HEAD -- 'tools/' # → пусто
```
Что именно закоммичено в `004fda2f` (лишнее для PR):
```
tools/.deltas-state.json
tools/apply-project-deltas.py
tools/run-project-deltas.sh
tools/deltas-mattermost.json
tools/deltas-mattermost-2.json … -13.json (12 файлов)
```
**✅ РЕШЕНО (2026-09-26):** выбран вариант (a), выполнен по команде Alex «Добавь в ignored».
Коммит `9f1e6c9c chore: ignore local project-editing tooling`.
```bash
echo '/tools/' >> .gitignore # + комментарий, что это локальный инструментарий
git rm -r --cached tools/ # 16 файлов ушли из индекса, ОСТАЛИСЬ на диске
git add .gitignore && git commit # → 9f1e6c9c
```
Результат: `git diff --stat upstream/main...HEAD -- tools/` → **пусто**;
в PR-diff **41 файл**, все — файлы проекта. `tools/` на диске цел (**39 файлов**),
`git status --short` чист.
⚠️ **`git rm -r --cached tools/` уходит на апрув** (рекурсивное удаление). Первая попытка
истекла по таймауту и **не применилась** — состояние было целое. Проверять
`git diff --cached --name-only` перед повтором.
🔴 **`gitignore` НЕ отвязывает уже отслеживаемые файлы** — без `git rm --cached` они
останутся в PR навсегда. Нужны оба шага, в этом порядке.
📌 **Правило на будущее:** проверять состав PR через
`git diff --stat upstream/main...HEAD` **перед** объявлением готовности, а не только
`git status` (последний показывает рабочее дерево, не содержимое коммитов). Иначе
мусор из прошлой сессии всплывает последним.
## Что осталось
**По PR — ничего, кроме ревью.** Всё выполнено:
| Шаг | Статус |
|---|---|
| `tools/` убран из ветки, `/tools/` в `.gitignore` | ✅ коммит `9f1e6c9c` |
| `git push -u origin feat/mattermost-channel` | ✅ запушено |
| PR в `baldaworks/balda` открыт | ✅ **#8**, 41 файл, +5622/−18 |
| CI | ✅ **8/8 PASS** |
| Локальные гейты (build/vet/lint/arch-lint/no-race) | ✅ все зелёные |
**Осталось (по желанию Alex):**
1. **Ревью апстрима** — ждать. Правки по замечаниям делать **в эту же ветку**:
push обновит PR автоматически.
2. 🔴 **Боевая проверка на Kraken — НЕ сделана.** PR-merge ≠ «бот работает на стенде».
Нужно: пересобрать образ из `9f1e6c9c`, задеплоить, `@balda` → ответ в ленте канала.
См. [[personal/projects/balda/balda-kraken-mm-deploy]].
3. **Замержить PR** (после ревью) — тогда Фаза 2 закрывается официально.
⚠️ Скрипты `tools/mm-*.sh` (9 шт.), дельты `tools/deltas-mattermost-14…26` +
`deltas-goal-command-fix.json` + `deltas-mm-prefixfix.json`, `apply-project-deltas.py`,
`run-project-deltas.sh`, `.deltas-state.json` — **untracked, локальный инструментарий**,
git-ignored. Существенное продублировано в
[[personal/projects/balda/balda-kraken-mm-deploy]] и [[personal/tech/balda-go-fx-pitfalls]].
## ✅ Разъяснено 2026-09-28 — 5 незакоммиченных файлов: НЕ трогать, НЕ фиксы
Запрос «что там за фиксы, suitable для PR?» — разобран. Ответ: **фиксов там нет.** Два разных набора, оба остаются на диске как есть (решение Alex: «оставляем»).
**Откуда взялось:** 2026-09-25 18:58 (Zulip `personal/Балда`, msg **116952**) ветка пересоздана от чистого апстрима:
`git branch -f feat/mattermost-channel upstream/main` — «без мержа наших коммитов». WIP прошлых сессий остался висеть на диске. Работа продолжилась в этой же ветке и завершилась PR #8 — **ничего не потеряно**, правки вошли в коммит `9f1e6c9c`.
**Откуда взялось:** 2026-09-25 18:58 (Zulip `personal/Балда`, msg `116952`) ветка пересоздана
от чистого апстрима: `git branch -f feat/mattermost-channel upstream/main` — «без мержа наших
коммитов». WIP прошлых сессий остался висеть на диске. Работа продолжилась в этой же ветке и
завершилась PR #8; существенное вошло в коммит `9f1e6c9c`.
| Файл | Что это | В PR? |
|---|---|---|
| `internal/apps/balda/app.go` | WIP прошлых сессий (MM-импорты без псевдонима) — артефакт переключения ветки, НЕ откат логики | ❌ |
| `internal/apps/balda/preflight.go` | то же + `ClassifyLocatorScope` во вторую карту | ❌ |
| `go.mod` | `replace github.com/normahq/runtime/v2 => ./runtime-local` — потомок `5ad65955` | ❌ |
| `Dockerfile.release` | `COPY runtime-local/` перед `go mod download` — иначе сборка падает | ❌ |
| `go.sum` | −2 хэша runtime v2.0.11 под тот же replace | ❌ |
| `Dockerfile.release` | `COPY runtime-local/` перед `go mod download` | ❌ |
| `internal/apps/balda/app.go` | WIP прошлых сессий (MM-импорты без псевдонима) — артефакт переключения ветки | ❌ |
| `internal/apps/balda/preflight.go` | то же + `ClassifyLocatorScope` во вторую карту | ❌ |
**`runtime-local/`** — локальный форк runtime (592 KB) с патчем `tool_calls` для hostedagent, авторский PR `normahq/runtime#4`. Стенд Kraken живёт на этом форке, но в апстрим `baldaworks/balda` такое не принимается: `replace` на локальную папку ломает сборку у всех, кто клонирует репо. Место ему — в локальной ветке (`local/*`), не в PR. **Решение: оставлено как есть, отдельная ветка не делалась.**
**Зачем они нужны:** без `runtime-local` стенд Balda на Kraken **не собирается**. `go.mod`
берёт не апстримовый runtime, а локальный форк (592 KB) с патчем `tool_calls` для hostedagent
(авторский PR `normahq/runtime#4`) — без него делегирование в CLI-агентов не работает.
Остальные 4 файла обслуживают этот же replace: `Dockerfile.release` копирует папку в образ,
`go.sum` больше не содержит хэшей апстримового runtime.
⚠️ **Риск:** грязное дерево → `git checkout -- .` / `git stash` / `git reset --hard` снесёт эти 5 файлов. Перед любой такой командой в `~/Developer/balda` — сверить `git status --short`.
**Почему не в PR:** `replace` на локальную папку ломает сборку у любого, кто склонирует
`baldaworks/balda`. Такое апстрим не принимает. Место этому — в локальной ветке (`local/*`).
## ✅ Проверка состояния PR — 2026-09-28
**Решение (2026-09-28, Alex):** оставлено как есть, отдельная ветка не делалась.
Свежая сверка через API (не по доке): **PR #8 `OPEN` · `MERGEABLE` · `reviewDecision` пустой · 8/8 CI PASS.**
⚠️ **Риск:** `git checkout -- .` / `git stash` / `git reset --hard` в `~/Developer/balda`
снесёт эти 5 файлов. Перед любой такой командой — сверить `git status --short`.
```
gh pr list --repo baldaworks/balda --state open
→ #8 feat(mattermost): add Mattermost channel transport, MERGEABLE, OPEN
---
gh pr checks 8 --repo baldaworks/balda
→ architecture-lint pass 2m2s | ingress-e2e pass 30s | jetstream-integration pass 1m0s
lint pass 2m46s | postgres-integration pass 57s | projection-replay pass 26s
sqlite-integration pass 1m15s | test pass 6m0s
```
**Ничего не изменилось с 26.09** — новых проверок нет, ревью апстрима не состоялось.
🔴 **Главное: `reviewDecision` пустой = PR никто не смотрел.** Мержа не будет, пока внешний ревьюер не заглянет. Это блокер не нашего производства — перепроверять `gh pr view 8 --json reviewDecision` имеет смысл периодически, но ускорить нельзя.
### Следующие шаги, чтобы «всё работало» — по приоритету
## 6. Следующие шаги, чтобы «всё работало» — по приоритету
| # | Шаг | Тип |
|---|---|---|
| 1 | Дождаться ревью #8 → правки в ту же ветку | ⛔ внешний блокер |
| 2 | **Фаза 4 — исполнитель (делегирование)** — вот что превращает «бот отвечает» в «бот работает» | 🎯 основной фронт |
| 3 | Боевая перепроверка приёмки на Kraken (образ `0.1.10` уже примлен 26.09 — повторять только если что-то менялось) | ↩ по необходимости |
| 1 | Дождаться ревью #8 → правки в ту же ветку (push обновит PR) | ⛔ внешний блокер |
| 2 | **Фаза 4 — исполнитель (делегирование)** — превращает «бот отвечает» в «бот работает» | 🎯 основной фронт |
| 3 | Боевая перепроверка приёмки на Kraken (образ `0.1.10` примлен 26.09 — повторять только если что-то менялось) | ↩ по необходимости |
| 4 | Замержить PR после ревью → Фаза 2 закрывается официально | ⏳ после 1 |
📌 **Пока PR висит на ревью — работать в ветке `local/*` над Фазой 4, не в `feat/mattermost-channel`.** Иначе новые коммиты уедут в открытый PR и обесценят уже пройденный CI.
📌 **Пока PR висит на ревью — работать над Фазой 4 в ветке `local/*`, НЕ в
`feat/mattermost-channel`.** Иначе новые коммиты уедут в открытый PR и обесценят пройденный CI.
---
## 7. Питфоллы, которые стоили времени
1. **`gitignore` НЕ отвязывает уже отслеживаемые файлы.** База `004fda2f` затащила в ветку
**16 файлов `tools/`**, которых в апстриме нет вообще (`git ls-tree upstream/main tools/` →
пусто). Найдено проверкой `git diff --stat upstream/main...HEAD`. Фикс (коммит `9f1e6c9c`):
`/tools/` в `.gitignore` **плюс** `git rm -r --cached tools/` — файлы остались на диске
(39 файлов), ушли только из git. Нужны оба шага, в этом порядке.
⚠️ `git rm -r --cached` уходит на апрув (рекурсивное удаление) — первая попытка истекла по
таймауту и не применилась, состояние было целое. Проверять `git diff --cached --name-only`.
2. **Проверять состав PR через `git diff --stat upstream/main...HEAD`, а не по своим коммитам.**
Свои 5 коммитов были чистыми (`git log upstream..HEAD -- tools/` → пусто), но ветка в
целом — нет, из-за базового коммита. `git status` показывает рабочее дерево, не содержимое
коммитов.
3. **Тест-ассерт — это гипотеза, а не истина.** Дважды падал тест, и оба раза неверным был
ассерт, а не код: (а) «тред и корневой пост = одна сессия» — на самом деле тред **отдельная**
сессия от канала (пробник: `mm-c-a4aa02ef` vs `mm-t-89bbf64f`); (b) «`NewIngress` фолбэчит
`botUserID` из клиента» — на самом деле `onStart` отказывается стартовать без `bot_user_id`.
Прежде чем править код под падающий тест — проверить, чему код **обязан** соответствовать.
4. **`goconst` в тестах** — повторяющиеся строковые литералы (`"channel-1"` ×15) валят линтер.
Заводить константы (`testChannelID`) сразу. **`dupword`** — `"thread channel = %q, channel
channel = %q"` пойман как повтор слова.
5. **`write_file` + `***` в доке** — плейсхолдеры вида `BALDA_MATTERMOST_TOKEN=***` в примерах
конфига остаются на диске **целыми**; искажается только вывод чтения. Проверять
`awk 'NR==N'`, не глазами (см. [[personal/tech/hermes-secret-redaction-bypass]]).
6. **⛔ zstd-фрейм macOS.** `zstd -d` на Kraken не читает фрейм, записанный на macOS (хостовый
zstd 1.5.4). Распаковывать **на Mac**: `zstd -dc file.tar.zst | ssh kraken 'docker load'`.
---
## Связанные заметки
- [[personal/projects/balda/balda-mattermost-connector]] — Фаза 2 (код коннектора)
- [[personal/projects/balda/balda-kraken-mm-deploy]] — Фаза 3 (деплой, боевые причины тишины)
- [[personal/projects/balda/balda-mm-delegation-plan]] — Фаза 4 (делегирование)
- [[personal/tech/balda-go-fx-pitfalls]] — fx-питфоллы + дельта-апплаер
- [[personal/plans/balda-mm-endtoend-plan]] — сквозной путь
- [[personal/tech/hermes-secret-redaction-bypass]] — редакция секретов