diff --git a/personal/plans/balda-mm-phase4-agents.md b/personal/plans/balda-mm-phase4-agents.md index 2fe17311..78cb7bd7 100755 --- a/personal/plans/balda-mm-phase4-agents.md +++ b/personal/plans/balda-mm-phase4-agents.md @@ -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/` с заголовком `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 (✅) diff --git a/personal/plans/balda-mm-pr-plan.md b/personal/plans/balda-mm-pr-plan.md index 354f2ab9..414b56f6 100755 --- a/personal/plans/balda-mm-pr-plan.md +++ b/personal/plans/balda-mm-pr-plan.md @@ -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:` — не полный +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:` -(не полный 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:` не совпадает ни с чем → каждый пост 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:` -в `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]] — редакция секретов