16 KiB
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 его включат обратно.
Цель
Оркестратор должен:
- Фильтровать webhook по проекту — игнорировать issue из неизвестных/неконфигурированных проектов.
- Резолвить repo/prefix/Plane-проект из маппинга по
projectid из payload, а не из единственногоdefault_repo. - 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.
Структура
# 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), чтобы система работала из коробки.
Резолверы
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добавить:
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→
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.py9 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, без реальных запусков):
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)