diff --git a/memory/2026-06-02.md b/memory/2026-06-02.md index 26f25b7..222ec1a 100644 --- a/memory/2026-06-02.md +++ b/memory/2026-06-02.md @@ -263,4 +263,28 @@ ssh slin@82.22.50.71 " ### Открытые вопросы Славе 1. Смержить PR #19 (enduro-trails)? 2. Смержить PR #1 (orchestrator worktree)? -3. Что делать с ET-012 (M-3 analyst в неправильном репо) — откатить или использовать? \ No newline at end of file + +--- + +## ИНЦИДЕНТ: Plane-webhook авто-запустил конвейер по всем ORCH-1..7 (2026-06-02 19:00) + +### Что случилось +- Создала в Plane задачи ORCH-1..7 — **Plane-webhook поймал каждую и авто-запустил конвейер** (analyst→architect, auto-commit) +- Всё ушло в **неправильный репо enduro-trails** (`plane.py:91` hardcode `repo=settings.default_repo`), наплодило мусор ET-010..016 +- Корень: Plane-webhook (id `93f0c342-a614-4248-9d0f-c107276f5620`) срабатывает на ЛЮБ\u041eЕ issue в workspace, без фильтра по проекту +- ⚠️ **ПОЗИТИВ:** автономность реально работает — analyst exit=0, auto-commit, worktree (ORCH-2) сработал + +### Меры (сделано) +- 🛡️ **Plane-webhook ДЕАКТИВИРОВАН** в Plane postgres: `UPDATE webhooks SET is_active=false`. Проверено: `is_active=f` + - Plane postgres: контейнер `plane-app-plane-db-1`, `PGPASSWORD=plane`, db `plane`, user `plane`, table `webhooks` + - Обратно включить после ORCH-6: `UPDATE webhooks SET is_active=true WHERE id=...` +- 🧹 Вычищено: все ветки ET-010..016 (git local+remote, 204), worktree `_wt/enduro-trails/*` (root-owned → sudo rm), тестовые iso-A/iso-B, tasks≥19 в БД орка, agent_runs≥19 +- Plane чист: orchestrator=7 (ORCH-1..7), enduro=5 (родные). Junk ET-issues в Plane не было (орк генерил ET-номера сам в git+БД) +- Заметка на сервере: `orchestrator/docs/INCIDENT_2026-06-02_webhook_autorun.txt` + +### ROOT FIX = ORCH-6 (multi-repo) +- `plane.py:91` `repo=settings.default_repo` → нужен фильтр по `plane_project_id` + маппинг repo per project +- Пока ORCH-6 не сделан — webhook ДЕРЖАТЬ ВЫКЛЮЧЕННЫМ + +### Открытые вопросы +- Смержить PR #19 (enduro-trails) + PR #1 (orchestrator worktree)? \ No newline at end of file diff --git a/tasks/orchestrator/DEV_TASK_ORCH6_MULTIREPO.md b/tasks/orchestrator/DEV_TASK_ORCH6_MULTIREPO.md new file mode 100644 index 0000000..fc1f322 --- /dev/null +++ b/tasks/orchestrator/DEV_TASK_ORCH6_MULTIREPO.md @@ -0,0 +1,245 @@ +# DEV TASK: ORCH-6 — Multi-repo (фильтр проекта + маппинг repo per project) + +**Статус:** Ready for dev +**Проект:** orchestrator +**Plane:** ORCH-6 (project id `8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a`) +**Источник:** `tasks/orchestrator/AUDIT_2026-06-02.md` + ИНЦИДЕНТ 2026-06-02 (webhook auto-run в неправильный репо) +**Исполнитель:** Dev-агент (model: tokenator/claude-opus-4-8) +**Приоритет:** 🔴 №1 — закрывает корень инцидента и снимает предохранитель (отключённый Plane-webhook) + +--- + +## Контекст инцидента (зачем эта задача) + +При создании задач ORCH-1..7 в Plane (проект `orchestrator`) Plane-webhook поймал каждую и +запустил конвейер — но **всё ушло в репо `enduro-trails`** (`plane.py:91` хардкодит +`repo = settings.default_repo`), наплодив мусор ET-010..016. Plane-webhook слушает **весь +workspace без фильтра по проекту**. + +**Сейчас Plane-webhook ДЕАКТИВИРОВАН** (предохранитель, `is_active=false` в Plane postgres, +webhook id `93f0c342-a614-4248-9d0f-c107276f5620`). После ORCH-6 его включат обратно. + +## Цель + +Оркестратор должен: +1. **Фильтровать webhook по проекту** — игнорировать issue из неизвестных/неконфигурированных проектов. +2. **Резолвить repo/prefix/Plane-проект из маппинга** по `project` id из payload, а не из единственного `default_repo`. +3. **Plane-sync (state/comment) ходить в ПРАВИЛЬНЫЙ проект** (сейчас `PROJECT_ID` хардкод на enduro). + +**Критерий приёмки:** оркестратор корректно ведёт задачу в проекте `orchestrator` (repo `orchestrator`, +prefix `ORCH`, свой Plane-проект) и в `enduro-trails` (repo `enduro-trails`, prefix `ET`) — не путая их. + +--- + +## Инфраструктура + +| Параметр | Значение | +|----------|----------| +| Сервер | `slin@82.22.50.71` (SSH key) | +| Репо орка | `/home/slin/repos/orchestrator/` (remote `admin/orchestrator`) | +| Контейнер | `orchestrator` (port 8500, network host) | +| Health | `curl -s http://localhost:8500/health` | +| Тесты | `IMG=$(docker inspect orchestrator --format '{{.Config.Image}}'); docker run --rm -v /home/slin/repos/orchestrator:/code -w /code --entrypoint python3 $IMG -m pytest tests/ -q` | +| Plane API | `http://localhost:8091/api/v1`, token из `ORCH_PLANE_API_TOKEN` | +| Plane workspace | `ag_proj` (slug), workspace uuid `903e12e8-65c9-40a0-a7f6-0186f8af42d4` | + +⚠️ Хостовый `.venv` сломан — тесты ТОЛЬКО через образ. + +### Известные id (для маппинга по умолчанию) + +| Проект | Plane project id | repo (gitea) | prefix | +|--------|------------------|--------------|--------| +| enduro-trails | `7a79f0a9-5278-49cd-9007-9a338f238f9c` | `enduro-trails` | `ET` | +| orchestrator | `8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a` | `orchestrator` | `ORCH` | + +--- + +## Корневые точки в коде (собрано Стрим — не ищи вслепую) + +| Файл:строка | Сейчас | Проблема | +|-------------|--------|----------| +| `src/webhooks/plane.py:91` | `repo = settings.default_repo` | хардкод, игнорит `data.project` | +| `src/webhooks/plane.py:125` | `get_next_work_item_id(repo)` | prefix `ET` хардкод в db.py | +| `src/webhooks/plane.py:105,240` | импорт `PROJECT_ID` из plane_sync | хардкод на enduro | +| `src/plane_sync.py:12` | `PROJECT_ID = settings.plane_project_id or "7a79f0a9..."` | хардкод; все sync-URL (строки 55,97,113,150) на единственный проект | +| `src/qg/checks.py:177-182` | импорт+использование `PROJECT_ID` | хардкод | +| `src/db.py:82` `get_next_work_item_id` | `prefix = "ET"` если нет записей | prefix не зависит от проекта | +| `src/webhooks/gitea.py:85,148,169,219` | `repo = payload["repository"]["name"]` (fallback default) | уже почти ок, repo из payload | +| `src/config.py` | плоский, один `default_repo`, один `plane_project_id` | нет маппинга | +| payload webhook | `data.project` = Plane project uuid | **есть** чем фильтровать ✅ | + +--- + +## Решение (архитектура) + +Ввести **реестр проектов** — список конфигов, ключ = Plane project id. + +### Структура + +```python +# src/projects.py (НОВЫЙ модуль) +@dataclass +class ProjectConfig: + plane_project_id: str # uuid Plane-проекта (ключ маппинга) + repo: str # имя gitea-репо (= папка в /repos) + work_item_prefix: str # ET / ORCH + name: str # человекочитаемое + +# источник: settings.projects_json (JSON в .env) ИЛИ дефолт-список в коде +``` + +**Конфиг через .env (НЕ трогать секреты, добавить новый ключ):** +`ORCH_PROJECTS_JSON` — JSON-массив. Если пусто — дефолт = два проекта из таблицы выше +(enduro-trails + orchestrator), чтобы система работала из коробки. + +### Резолверы + +```python +def get_project_by_plane_id(plane_project_id) -> ProjectConfig | None +def get_project_by_repo(repo) -> ProjectConfig | None +def known_plane_project_ids() -> set[str] +``` + +--- + +## Задачи + +### Task 1: контекст + модуль реестра + +- [ ] **1.1** Прочитай `AUDIT_2026-06-02.md`, текущие `plane.py`, `plane_sync.py`, `config.py`, `db.py`. Пойми все точки из таблицы выше. +- [ ] **1.2** Работай в ветке `feature/ORCH-6-multirepo` репо orchestrator. `git status` чистый. +- [ ] **1.3** Создать `src/projects.py` с `ProjectConfig`, дефолт-списком (enduro-trails + orchestrator из таблицы) и резолверами. Источник переопределения — `settings.projects_json` (если задан и валиден). +- [ ] **1.4** В `config.py` добавить: +```python +projects_json: str = "" # ORCH-6: JSON-массив ProjectConfig; пусто = дефолт в projects.py +``` +**Критерий:** `get_project_by_plane_id("8da6aa25-...")` → orchestrator; `get_project_by_repo("enduro-trails")` → ET. + +--- + +### Task 2: фильтр + резолв в plane-webhook + +**Файл:** `src/webhooks/plane.py` + +- [ ] **2.1** В `plane_webhook()` (или в `handle_work_item_created`/`handle_comment`): извлечь `project_id = data.get("project")`. Если `project_id` НЕ в `known_plane_project_ids()` — **залогировать и проигнорировать** (`return {"status":"ignored","reason":"unknown project"}`). Это и есть фильтр, который предотвратит инцидент. +- [ ] **2.2** `plane.py:91`: вместо `repo = settings.default_repo` → +```python +proj = get_project_by_plane_id(project_id) +repo = proj.repo +``` +- [ ] **2.3** `get_next_work_item_id(repo)` — prefix должен браться из `proj.work_item_prefix` (см. Task 4). +- [ ] **2.4** Все локальные использования `PROJECT_ID` (строки ~105-117, ~240-244) — заменить на `proj.plane_project_id` (Plane-sync для ЭТОЙ задачи должен идти в её проект). Передавай project_id дальше в helper'ы. + +**Критерий:** issue из проекта orchestrator → repo `orchestrator`, sync в проект orchestrator; неизвестный проект → ignored. + +--- + +### Task 3: plane_sync — параметризовать проект + +**Файл:** `src/plane_sync.py` + +- [ ] **3.1** Функции (`notify_stage_change`, `notify_qg_failure`, `notify_done`, `find_issue_id`, и др.), которые строят URL с `PROJECT_ID` — должны принимать `project_id` параметром (с дефолтом на enduro для обратной совместимости, ЧТОБЫ не сломать существующие вызовы). Резолвить project_id из task (по repo задачи через `get_project_by_repo`). +- [ ] **3.2** Вызовы из `plane.py` и `qg/checks.py` — передавать правильный project_id (из задачи/проекта). + ⚠️ Если task знает только `repo` — резолвь `get_project_by_repo(repo).plane_project_id`. + +**Критерий:** state/comment задачи orchestrator пишутся в Plane-проект orchestrator, а не в enduro. + +--- + +### Task 4: work_item prefix per project + +**Файл:** `src/db.py` (`get_next_work_item_id`) + +- [ ] **4.1** Сделать prefix параметром: `get_next_work_item_id(repo, prefix="ET")`. Нумерация — по последней записи ЭТОГО repo+prefix. Вызов из plane.py передаёт `proj.work_item_prefix`. +- [ ] **4.2** Убедиться, что фильтр `WHERE repo=?` корректен (orchestrator-задачи нумеруются ORCH-001, ORCH-002 независимо от ET). + +**Критерий:** задача в orchestrator → `ORCH-001`; в enduro → `ET-010` (продолжая существующую нумерацию). + +--- + +### Task 5: gitea-webhook (минимально) + +**Файл:** `src/webhooks/gitea.py` + +- [ ] **5.1** Уже берёт `repo` из `payload.repository.name` — проверить, что для repo вне реестра — игнор (по аналогии с Task 2.1: `get_project_by_repo(repo)` None → ignored). Не запускать стадии для неизвестного repo. + +**Критерий:** push в неизвестный repo не триггерит конвейер. + +--- + +### Task 6: тесты + документация + +- [ ] **6.1** `tests/test_projects.py`: резолверы (by plane_id, by repo, unknown → None, known_plane_project_ids), парсинг `projects_json`. +- [ ] **6.2** `tests/test_plane_webhook.py` (или дополнить существующий): webhook из неизвестного проекта → ignored; из orchestrator → repo=orchestrator; из enduro → repo=enduro-trails. Мокать launcher.launch чтобы не запускать реальных агентов. +- [ ] **6.3** Прогнать ВСЕ тесты (команда из Инфраструктуры). `test_webhooks.py` 9 pre-existing падений (401/signature) — не твои, не сломай остальное. +- [ ] **6.4** Обновить `docs/ARCHITECTURE.md` (раздел multi-repo, реестр проектов), `README` (как добавить новый проект через `ORCH_PROJECTS_JSON`). Создать `docs/BUGFIXES_2026-06-03.md` (или дату прогона) с описанием ORCH-6 + ссылкой на инцидент. + +**Критерий:** тесты зелёные, доки описывают как заводить новый проект. + +--- + +### Task 7: деплой + проверка (БЕЗ включения webhook!) + +- [ ] **7.1** Коммиты (Conventional Commits), push в `feature/ORCH-6-multirepo`, PR в orchestrator. +- [ ] **7.2** Пересобрать: `cd /home/slin/repos/orchestrator && docker compose up -d --build && sleep 5 && curl -s http://localhost:8500/health`. +- [ ] **7.3** **Тест резолва (offline, без реальных запусков):** +```bash +docker exec orchestrator python3 -c " +import sys; sys.path.insert(0,'/app') +from src.projects import get_project_by_plane_id, get_project_by_repo, known_plane_project_ids +o = get_project_by_plane_id('8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a') +e = get_project_by_plane_id('7a79f0a9-5278-49cd-9007-9a338f238f9c') +assert o.repo=='orchestrator' and o.work_item_prefix=='ORCH', o +assert e.repo=='enduro-trails' and e.work_item_prefix=='ET', e +assert get_project_by_plane_id('00000000-0000-0000-0000-000000000000') is None +print('RESOLVE OK:', o.repo, e.repo, '| known:', len(known_plane_project_ids())) +" +``` +- [ ] **7.4** **Тест фильтра (симуляция webhook неизвестного проекта)** — отправить POST /webhook/plane с `data.project` = фейковый uuid, убедиться что ответ `ignored` и НЕ создаётся task/ветка. (Можно через pytest с TestClient — предпочтительно, чтобы не дёргать живой эндпоинт.) +- [ ] **7.5** ⚠️ **НЕ включать Plane-webhook** (is_active остаётся false). Включение — отдельный шаг Стрим после ревью. +- [ ] **7.6** Отчитаться Стрим: резолв + фильтр подтверждены, готово к включению webhook. + +**Критерий:** резолв корректен, неизвестный проект игнорируется, webhook остаётся выключенным. + +--- + +## Acceptance (что проверит Стрим) + +| # | Проверка | Ожидаемо | +|---|----------|----------| +| 1 | резолв by plane_id | orchestrator→ORCH/repo, enduro→ET/repo | +| 2 | неизвестный проект | None / ignored | +| 3 | webhook из orchestrator-проекта | repo=orchestrator, sync в его Plane-проект | +| 4 | prefix per project | ORCH-001 vs ET-010 | +| 5 | gitea неизвестный repo | ignored | +| 6 | тесты | all pass (кроме 9 pre-existing) | +| 7 | webhook is_active | остаётся false (Стрим включит после ревью) | +| 8 | доки | multi-repo + как добавить проект | + +--- + +## Ограничения + +- 🚫 **НЕ трогай:** nginx, openclaw.json, .env-СЕКРЕТЫ (можно добавить НОВЫЙ ключ `ORCH_PROJECTS_JSON`, но не трогать существующие токены), deploy-хук. +- 🚫 **НЕ включай Plane-webhook** (`is_active`) — это сделает Стрим после ревью. +- ⚠️ **Обратная совместимость plane_sync:** существующие вызовы без project_id должны продолжать работать (дефолт на enduro). Не сломай ET-009/текущие задачи. +- ⚠️ **Не ломай ORCH-2 (worktree)**, B-1/B-2/S-1/S-5 фиксы. +- 🚫 Per-project deploy-хуки и per-project агентские инструкции — в этой задаче НЕ обязательны (можно вынести в подзадачу). Минимум для снятия предохранителя = фильтр проекта + resolve repo/prefix + plane_sync в правильный проект. +- 🚫 Авто-clone нового repo — НЕ в этой задаче (оба repo уже в /repos). + +## Деплой-чеклист +- [ ] `src/projects.py` + реестр + резолверы +- [ ] plane-webhook фильтр по проекту + resolve repo +- [ ] plane_sync параметризован по project_id (обратно совместимо) +- [ ] prefix per project в db.py +- [ ] gitea неизвестный repo → ignored +- [ ] тесты зелёные (кроме pre-existing) +- [ ] орк пересобран, health ok +- [ ] резолв + фильтр подтверждены +- [ ] webhook ОСТАЁТСЯ выключенным +- [ ] доки + BUGFIXES +- [ ] отчёт Стрим + +--- + +*Создано: 2026-06-02 | Автор ТЗ: Стрим | Исполнитель: Dev (Opus 4.8 Tokenator)* diff --git a/tasks/orchestrator/reports/dev-2026-06-02-orch6-multirepo.md b/tasks/orchestrator/reports/dev-2026-06-02-orch6-multirepo.md new file mode 100644 index 0000000..040a73f --- /dev/null +++ b/tasks/orchestrator/reports/dev-2026-06-02-orch6-multirepo.md @@ -0,0 +1,40 @@ +# Dev Report: ORCH-6 — Multi-repo (фильтр проекта + маппинг repo per project) +Дата: 2026-06-02 +Статус: IN PROGRESS + +## Задача +Plane-webhook ловит issue из любого проекта и льёт всё в default_repo=enduro-trails (хардкод). +Нужно: реестр проектов (Plane project id → repo + prefix), фильтр webhook по проекту, +resolve repo/prefix из маппинга, plane_sync в правильный проект. + +ТЗ: tasks/orchestrator/DEV_TASK_ORCH6_MULTIREPO.md + +## Инфра +- Сервер slin@82.22.50.71, репо /home/slin/repos/orchestrator (remote admin/orchestrator) +- Контейнер orchestrator (port 8500) +- Ветка: feature/ORCH-6-multirepo (от feature/ORCH-2-worktree) + +## Сделано +- [x] Task 0: прочитал ТЗ, аудит, весь релевантный код. Создал ветку. +- [ ] Task 1: src/projects.py — реестр + резолверы + config.projects_json +- [ ] Task 2: plane-webhook фильтр + resolve +- [ ] Task 3: plane_sync параметризован по project_id (обратно совместимо) +- [ ] Task 4: db.py prefix per project +- [ ] Task 5: gitea неизвестный repo → ignored +- [ ] Task 6: тесты + доки +- [ ] Task 7: деплой + проверка резолва/фильтра + +## Изменённые файлы +(заполняется по ходу) + +## Результат +(в конце) + +## Проблемы и решения +- ВАЖНО: существующие тесты в test_webhooks.py используют `"project": "proj-1"` — + это НЕ в реестре. После добавления фильтра задача не создастся → тесты сломаются. + Решение: зарегистрировать proj-1 через ORCH_PROJECTS_JSON в env тестов + (gitea-тесты используют repo enduro-trails — уже в дефолтном реестре, ок). + +## Следующий шаг +Task 1 — создать src/projects.py