auto-sync: 2026-06-02 22:20:01

This commit is contained in:
Stream
2026-06-02 22:20:01 +03:00
parent b542f15e74
commit f82ca6ac0c
3 changed files with 310 additions and 1 deletions

View File

@@ -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 в неправильном репо) — откатить или использовать?
---
## ИНЦИДЕНТ: 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)?

View File

@@ -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)*

View File

@@ -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