diff --git a/docs/work-items/ET-015/01-brd.md b/docs/work-items/ET-015/01-brd.md new file mode 100644 index 0000000..2d4c5cb --- /dev/null +++ b/docs/work-items/ET-015/01-brd.md @@ -0,0 +1,169 @@ +# BRD — ET-015 / ORCH-6: Multi-repo support для оркестратора + +Work Item ID: ET-015 +Plane title: [ORCH-6] Multi-repo: оркестратор работает по нескольким проектам +Целевой репозиторий изменений: `orchestrator` (а не `enduro-trails`) +Где трекается work item: `enduro-trails/docs/work-items/ET-015/` (организационная задача) + +## 1. Контекст и проблема + +Multi-Agent Orchestrator (FastAPI-сервис на порту 8500) сегодня жёстко +завязан на один проект — `enduro-trails`. Это видно по нескольким местам: + +1. `ORCH_DEFAULT_REPO=enduro-trails` — единственный fallback, который + используется почти везде в `webhooks/plane.py`, `webhooks/gitea.py`, + `agents/launcher.py`. +2. Префикс work item ID жёстко `ET-` (`get_next_work_item_id` в `db.py`, + строка ~93–99): любой новый таск, откуда бы он ни пришёл, получает + ID вида `ET-NNN`. +3. Plane-настройки одиночные: `ORCH_PLANE_WORKSPACE_SLUG`, + `ORCH_PLANE_PROJECT_ID`, словарь `PLANE_STATES` со state-id'ами + конкретного проекта `enduro-trails`. Webhooks от другого Plane-проекта + будут обработаны, но patch'и состояний и комментарии полетят в чужой + проект. +4. Документация (`docs/phases/PH-1..PH-9`, фазы roadmap), agent-prompts + (`.openclaw/agents/*.md`) и Quality Gates конвенции живут в `enduro-trails` + и неявно считаются «универсальными». + +В результате невозможно подключить к оркестратору ни сам `orchestrator` (мета-разработка), +ни любой будущий проект (например `enduro-tiles-data`, `enduro-mobile`). + +## 2. Бизнес-цель + +Дать оркестратору возможность обслуживать **N проектов одновременно**, не теряя +текущей функциональности по `enduro-trails` и без миграции исторических данных. + +Это нужно, чтобы: + +- запускать пайплайн на самом репо `orchestrator` (dogfooding — + улучшения оркестратора проходят через тот же пайплайн); +- начать вести новые проекты той же командой и тем же инструментарием + (тайлы, мобильное приложение, бэкенды для соседних идей); +- иметь единую точку наблюдения за разработкой нескольких репо + (один `/status`, один Telegram-канал, одни логи). + +## 3. Целевые пользователи и сценарии + +| Роль | Сценарий | Ожидание | +|------|----------|----------| +| Owner (homenet542@gmail.com) | Подключить к оркестратору новый проект `foo` | За один шаг (правка конфига + перезапуск) проект становится известен оркестратору. Никаких code-change'ей в `src/`. | +| Stakeholder в Plane | Создаёт work item в проекте `Foo` (Plane workspace) | Получает branch `feature/FOO-NNN-slug` в Gitea-репо `foo`, артефакты в `docs/work-items/FOO-NNN/`, анализ запускается автоматически. | +| Developer (claude-bot) | Анализирует work item, который пришёл из проекта `Foo` | Видит корректный work item ID (`FOO-007`), корректный repo (`foo`), корректную ветку (`feature/FOO-007-...`). Worktree создаётся под нужным репо. | +| Owner | Смотрит `/status` оркестратора | Видит все активные задачи across all repos, с указанием repo для каждой. | +| Owner | Хочет временно поставить проект на паузу | Может выключить конкретный проект в конфиге, не трогая остальные. | + +## 4. Бизнес-требования + +### BR-1. Реестр проектов +Оркестратор должен знать список обслуживаемых проектов. Источник истины +конфигурируется (env / file / иной способ — на усмотрение архитектора). +Для каждого проекта задаются как минимум: +- имя Gitea-репо (`repo`), +- префикс work item ID (`ET`, `ORCH`, `FOO`, ...), +- идентификаторы Plane (workspace_slug, project_id), +- маппинг состояний Plane (`backlog`, `todo`, `in_progress`, + `needs_input`, `in_review`, `blocked`, `done`, `cancelled`) — каждый + Plane-проект имеет собственные UUID состояний. + +### BR-2. Маршрутизация Plane-вебхуков по проектам +При получении вебхука от Plane оркестратор обязан определить, какому +проекту из реестра принадлежит событие (по `project_id` в payload), +и работать с настройками этого проекта (Plane state IDs, workspace, +gitea-репо). + +### BR-3. Маршрутизация Gitea-вебхуков по репозиториям +При получении push / PR / status оркестратор должен использовать +`repository.name` из payload как ключ для поиска проекта в реестре +вместо `settings.default_repo`. + +### BR-4. Префикс work item ID — per-project +Генерация следующего ID должна продолжать существующую нумерацию +**в пределах своего префикса**: для `enduro-trails` — `ET-016` после +`ET-015`; для `orchestrator` — `ORCH-001` для первого таска, `ORCH-002` +далее; ID одного проекта не пересекают и не сдвигают ID другого. + +### BR-5. Изоляция worktree per (repo, branch) +Текущая схема `/repos/_wt//` уже репо-aware, но +оркестратор должен предполагать наличие main-clone'а для каждого репо +в `/repos/`. Если main-clone отсутствует — внятная ошибка +в логах + Telegram (а не падение). + +### BR-6. Agent prompts и фазы — per-repo +Agent-промпты (`.openclaw/agents/.md`), `CLAUDE.md`, `docs/phases/` +у каждого проекта могут отличаться. Оркестратор не должен «протаскивать» +артефакты `enduro-trails` в другие репозитории. + +### BR-7. Обратная совместимость +Существующий проект `enduro-trails` после миграции продолжает работать +без потери истории: +- активные задачи в `tasks` таблице не ломаются; +- ID `ET-015`, `ET-016`, … продолжают выдаваться; +- комментарии и состояния в Plane продолжают летать в текущий + workspace/project. + +### BR-8. Наблюдаемость +`/status` (и любые админ-эндпоинты) выводит `repo` для каждой активной +задачи. Telegram-уведомления и логи также включают `repo`, +чтобы их можно было разделить per-project. + +### BR-9. Минимально необходимый рантайм-конфиг +Подключение нового проекта не требует изменения кода `src/`. +Только конфиг + наличие main-clone'а репозитория на хосте + перезапуск +контейнера. Webhook secret-ы (Plane / Gitea) — также per-project. + +### BR-10. Безопасность +Каждый Plane-проект может иметь собственный webhook secret; +оркестратор должен проверять HMAC с правильным секретом для конкретного +проекта, иначе вебхук отбрасывается с 401. + +## 5. Out of scope для этой задачи + +- Параллельная обработка нескольких задач (S-4 уже сделал worktree per + branch, но in-process очередь задач — отдельный F-2b). +- Веб-UI для управления проектами. +- Автоматическое подтягивание main-clone'а нового репо (`git clone` + выполняется руками или другим инструментом). +- Изменение pipeline-stage машины (`stages.py`). +- Изменение модели данных Plane (state-id'ы определяются в Plane, + оркестратор только подхватывает). + +## 6. Допущения и ограничения + +- Все обслуживаемые репозитории живут в одном Gitea-инстансе + (один `ORCH_GITEA_URL`, один `ORCH_GITEA_TOKEN`, один `owner=admin`). + Multi-Gitea — не в скоупе. +- Все Plane-проекты — в одном Plane-инстансе (`ORCH_PLANE_API_URL`), + но могут быть в разных workspace. +- Claude CLI бинарник один и тот же (`/opt/claude-code/bin/claude.exe`). +- Hooks деплоя per-repo не обязаны существовать на этом этапе: + если у репо нет деплой-стадии — `deploy` помечается `done` без + смоук-теста (либо проект явно объявляет, что у него нет deploy). + +## 7. Метрики успеха + +1. Создание work item'а в Plane-проекте `Orchestrator` приводит к появлению + ветки `feature/ORCH-NNN-…` в Gitea-репо `orchestrator` и запуску + analyst-агента в worktree `/repos/_wt/orchestrator/`. +2. ID нового таска в `orchestrator` имеет префикс `ORCH-`, а не `ET-`. +3. Создание work item в `enduro-trails` продолжает работать: ID `ET-016`, + ветка `feature/ET-016-...`, всё как раньше. +4. `/status` возвращает поле `repo` для каждой задачи. +5. Telegram-уведомление содержит `repo` в префиксе сообщения. +6. Регрессии не наблюдаются на 2 последовательных `ET-NNN` задачах + после раскатки. + +## 8. Зависимости и риски (на уровне бизнеса) + +- Plane state IDs другого проекта надо получить вручную (через Plane UI + или API) — этим занимается Owner перед подключением. +- Webhook secret per-project потребует пересоздать webhook'и + в каждом Plane-проекте и каждом Gitea-репо. +- Schema `tasks` уже хранит `repo` — миграции данных не требуется. + +## 9. Связанные документы + +- 02-trz.md — функциональные/нефункциональные требования (этот пакет). +- 03-acceptance-criteria.md — критерии приёмки. +- 04-test-plan.yaml — план тестов. +- Architecture phase: создаст ADR по способу хранения реестра проектов + и стратегии маппинга Plane state IDs. diff --git a/docs/work-items/ET-015/02-trz.md b/docs/work-items/ET-015/02-trz.md new file mode 100644 index 0000000..bdde86e --- /dev/null +++ b/docs/work-items/ET-015/02-trz.md @@ -0,0 +1,270 @@ +# ТЗ — ET-015 / ORCH-6: Multi-repo support для оркестратора + +Work Item ID: ET-015 +Target repo для кода: `orchestrator` +Документация и трекинг: `enduro-trails/docs/work-items/ET-015/` + +> ТЗ описывает **что** должна делать система. Конкретные технические +> решения (формат реестра, способ хранения state-id'ов, и т.п.) +> принимаются на стадии Architecture (ADR). + +## 1. Глоссарий + +- **Project** — логическое подразделение, обслуживаемое оркестратором. + Каждый project имеет один Gitea-репо, один Plane-проект, один префикс + work item ID. +- **Project key** — короткий уникальный идентификатор проекта в реестре + (например `enduro-trails`, `orchestrator`). Используется внутри + оркестратора как ключ для поиска настроек. +- **Work item prefix** — буквенный префикс, который ставится в ID + задач конкретного проекта (например `ET`, `ORCH`). +- **Project config** — набор настроек конкретного проекта + (см. FR-1). + +## 2. Функциональные требования + +### FR-1. Хранение конфигурации проектов + +Оркестратор должен иметь возможность хранить конфигурацию **N проектов**. +Для каждого проекта должны храниться следующие поля: + +| Поле | Тип | Описание | Обязательное | +|------|-----|----------|--------------| +| `key` | str | Project key, совпадает с именем gitea-репо | да | +| `repo` | str | Имя gitea-репо | да (= key, как правило) | +| `work_item_prefix` | str (A-Z) | Префикс ID задач (например `ET`, `ORCH`) | да | +| `plane_workspace_slug` | str | Plane workspace slug | да | +| `plane_project_id` | str (UUID) | Plane project UUID | да | +| `plane_states` | map | UUID для состояний backlog/todo/in_progress/needs_input/in_review/blocked/done/cancelled | да | +| `plane_webhook_secret` | str | Секрет для HMAC-проверки Plane webhook | опционально (если не задан — проверка отключается, как сейчас) | +| `gitea_webhook_secret` | str | Секрет для HMAC-проверки Gitea webhook | опционально | +| `enabled` | bool | Можно ли принимать события и запускать пайплайн | да, default `true` | +| `default_branch` | str | База для feature-веток (обычно `main`) | опционально, default `main` | +| `has_deploy_stage` | bool | Есть ли deploy-этап (deployer-агент) | опционально, default `true` | + +Способ хранения (env-vars / YAML-файл / JSON / DB) выбирает архитектор. +Важно: подключение нового проекта НЕ должно требовать изменений в `src/`. + +### FR-2. Поиск проекта по событию + +Оркестратор должен уметь определить project key: + +- **FR-2.1** — по Plane webhook payload: по полю `project` / + `project_id` (Plane передаёт UUID проекта). Поиск идёт по + `plane_project_id` в реестре. +- **FR-2.2** — по Gitea webhook payload: по `repository.name`. + Поиск идёт по `repo`. +- **FR-2.3** — по уже существующей задаче в БД: достаточно `tasks.repo`. + +Если событие пришло из неизвестного проекта — оно логируется на уровне +`WARNING` и игнорируется (HTTP 202, не 500). Telegram-нотификация +отправляется один раз в N минут (анти-флуд), чтобы Owner мог заметить, +что вебхук неправильно настроен. + +### FR-3. Генерация work item ID per-prefix + +Функция `get_next_work_item_id(project_key)` (или эквивалентная) +должна: + +- Вернуть `-`, где `prefix` берётся из конфигурации + проекта. +- Номер — следующий после максимального уже существующего ID **с тем + же префиксом** в таблице `tasks`. Поиск ведётся по + `work_item_id LIKE '-%'` (а не по `repo`), чтобы + переименование репо не сбивало нумерацию. +- Нумерация per-project продолжается независимо. Никаких глобальных + счётчиков. + +### FR-4. Plane sync — per-project + +`plane_sync.py` должен: + +- **FR-4.1** — на каждый исходящий запрос (`update_issue_state`, + `add_comment`, `find_issue_id`, `set_issue_*`) определять, к какому + проекту относится `work_item_id`, и брать из конфига этого проекта + `workspace_slug`, `project_id`, `plane_states`. Текущие модульные + константы `WORKSPACE`, `PROJECT_ID`, `PLANE_STATES` должны + превратиться в значения, зависящие от project key. +- **FR-4.2** — `find_issue_id` должен корректно работать с разными + проектами: поиск идёт в Plane-проекте, к которому относится + `work_item_id` (определяется по префиксу или через DB-lookup + `tasks.work_item_id → tasks.repo`). +- **FR-4.3** — `STAGE_TO_STATE` остаётся справочной маппой (`stage → + ключ состояния, например `in_progress`), но `PLANE_STATES` теперь + per-project. Финальный UUID = `project.plane_states[state_key]`. + +### FR-5. Webhook handlers + +- **FR-5.1** — `verify_plane_signature` использует secret того проекта, + которому принадлежит событие. Если проект не определяется — secret + проверяется по дефолтному (текущее поведение для обратной + совместимости), либо событие отбрасывается. Решение оформить в ADR. +- **FR-5.2** — `handle_work_item_created` использует `repo`, `prefix`, + `branch_template`, `plane_states` целевого проекта вместо + `settings.default_repo`. +- **FR-5.3** — `_create_gitea_branch` и `_create_initial_docs` пишут + в **тот** репо, к которому относится событие. +- **FR-5.4** — `handle_comment`, `handle_push`, `handle_pr`, + `handle_ci_status` определяют project key из payload и используют + его настройки. + +### FR-6. Agent launcher + +`AgentLauncher.launch(agent, repo, ...)` уже принимает `repo`. +Изменения: + +- **FR-6.1** — путь к agent-prompts читается из worktree этого репо + (`.openclaw/agents/.md`), а не из шаренного `enduro-trails`. + Текущая реализация уже так делает (worktree-relative); проверить + и подтвердить тестом. +- **FR-6.2** — если у проекта `has_deploy_stage=false`, то стадия + `testing` после успешного теста сразу advance'ится в `done` (без + запуска deployer'а). Поведение `enduro-trails` (где deploy есть) + не меняется. +- **FR-6.3** — task-description, передаваемый в `.task*.md`, содержит + поле `Repo: ` — это уже так. Подтвердить. + +### FR-7. Worktree + +- **FR-7.1** — `ensure_worktree(repo, branch)` корректно работает для + любого репо. Уже так; нужны лишь интеграционные тесты на 2 разных + репо одновременно. +- **FR-7.2** — если `/repos/` не существует, оркестратор: + логирует `ERROR`, шлёт Telegram «main-clone для не найден, + подключение проекта неполное», возвращает 202 (не 500) на вебхук. + Не пытается клонировать сам. + +### FR-8. /status и наблюдаемость + +- **FR-8.1** — `GET /status` возвращает массив задач с полями как + минимум: `task_id`, `work_item_id`, `repo`, `branch`, `stage`, + `agent_running`, `created_at`, `updated_at`. Поле `repo` — обязательно. +- **FR-8.2** — все логи `orchestrator.*` содержат `repo=` в строке + записи там, где это применимо (push/PR handler, agent launch). +- **FR-8.3** — Telegram-сообщения для конкретной задачи содержат либо + `` (он уже несёт префикс), либо префикс `[]`. + Не вводить нового поля, использовать существующий work_item_id. + +### FR-9. Обратная совместимость и миграция + +- **FR-9.1** — После релиза существующие задачи `ET-001..ET-014` + не должны менять состояние, repo или branch. +- **FR-9.2** — После релиза создание новой задачи в Plane-проекте + `enduro-trails` приводит к ID `ET-016` (следующий после `ET-015`), + как и раньше. +- **FR-9.3** — Schema БД не меняется (поле `repo` уже есть). + Если в DB обнаружатся записи с `repo=NULL` — это считается багом + данных и логируется, но не блокирует работу. +- **FR-9.4** — Дефолты `ORCH_PLANE_*` и `ORCH_DEFAULT_REPO` остаются + в `config.py` как fallback для проекта `enduro-trails` (можно убрать + на следующем этапе после стабилизации, не в рамках этого WI). + +### FR-10. Подключение второго проекта (приёмочный сценарий) + +Owner может за следующий список шагов подключить проект `orchestrator`: + +1. Клонировать `orchestrator` в `/repos/orchestrator` (если ещё нет). +2. Создать в Plane workspace проект `Orchestrator`. +3. Получить из Plane UUID состояний (backlog, todo, in_progress, …). +4. Добавить запись в реестр проектов оркестратора (способ — ADR). +5. Создать webhook в Plane (target = оркестратор) и в Gitea для репо + `orchestrator`. +6. Перезапустить контейнер оркестратора. +7. Создать тестовый work item в Plane → проверить, что появилась ветка + `feature/ORCH-001-…` в gitea-репо `orchestrator`, и в `tasks` + создался ряд `repo=orchestrator, work_item_id=ORCH-001`. + +## 3. Нефункциональные требования + +### NFR-1. Конфигурируемость +Добавление и удаление проекта не требует пересборки docker-образа. +Конфигурация загружается на старте. + +### NFR-2. Изоляция +Сбой работы с одним проектом (например, недоступный Plane workspace) +не должен ломать обработку событий других проектов. Каждый webhook +обрабатывается независимо. + +### NFR-3. Производительность +Добавление 5 проектов не должно увеличивать latency обработки +одного вебхука более чем на 10% относительно текущей точки. + +### NFR-4. Логирование +В каждом ERROR/WARNING логе, связанном с конкретной задачей, должны +присутствовать как минимум: `repo`, `work_item_id` (если уже создан), +`branch` (если применимо). + +### NFR-5. Безопасность +HMAC проверяется тем секретом, который привязан к проекту-владельцу +события. Глобальный «accept-all-if-no-secret» оставляется только +для совместимости; в продакшене Owner обязан задать secret для +каждого webhook'а. + +### NFR-6. Надёжность миграции +Релиз должен катиться без остановки активных задач: задача в стадии +`development` не должна потерять контекст. Этого достичь можно тем, +что schema БД не меняется. + +## 4. Интерфейсы + +### 4.1 HTTP endpoints оркестратора + +| Endpoint | Изменения | +|----------|-----------| +| `GET /health` | без изменений | +| `GET /status` | в каждом элементе массива есть `repo` | +| `POST /webhook/plane` | определяет project key по `project_id` payload'а | +| `POST /webhook/gitea` | определяет project key по `repository.name` payload'а | + +### 4.2 Plane webhook payload — какие поля используются + +- `event` (`work_item.created`, `comment.created`, ...). +- `project` или `data.project_id` — Plane project UUID → используется + для поиска project key в реестре. +- остальные поля — без изменений. + +### 4.3 Gitea webhook payload — какие поля используются + +- `repository.name` — используется для поиска project key в реестре. +- остальные — без изменений. + +### 4.4 Файлы артефактов + +Структура `docs/work-items//...` сохраняется. Имена +файлов (BRD, TRZ, AC, TestPlan, ADR) — те же. + +## 5. Что НЕ нужно делать в этом WI + +- Не вводить новые стадии пайплайна. +- Не менять схему БД (нет миграций). +- Не переписывать `git_worktree.py` — он уже multi-repo-ready. +- Не делать UI для управления проектами. +- Не выносить агент-промпты из репо в оркестратор. + +## 6. Открытые вопросы для архитектора + +1. Где хранить реестр проектов: env-vars (`ORCH_PROJECTS_JSON=...`), + YAML-файл (`/app/config/projects.yaml`), таблица в SQLite, + или гибрид? — ADR. +2. Как валидировать конфиг проекта на старте? Что делать при + некорректной записи (отказ при старте vs. skip-с-warning)? — ADR. +3. Нужна ли горячая перезагрузка конфига (SIGHUP / endpoint) или + достаточно рестарта контейнера? — рекомендация: достаточно рестарта. +4. Как маппить event → project key при отсутствии `project_id` + в Plane payload (старые webhook'и)? — ADR. +5. Нужно ли поддерживать миграцию префикса для уже существующих + задач? — рекомендация: нет, ID иммутабельны. + +## 7. Зависимости + +- Plane API доступен (для подтягивания state-id'ов в момент настройки — + это операция Owner'а, не код оркестратора). +- Gitea API доступен (для создания веток). +- Для каждого репо есть main-clone в `/repos/`. + +## 8. Связанные ADR / документы + +- Будут созданы в стадии Architecture: + - ADR: «Project registry: формат и место хранения». + - ADR: «Plane state mapping per project». + - Возможно: «Migration plan — config rollout». diff --git a/docs/work-items/ET-015/03-acceptance-criteria.md b/docs/work-items/ET-015/03-acceptance-criteria.md new file mode 100644 index 0000000..abcc5dd --- /dev/null +++ b/docs/work-items/ET-015/03-acceptance-criteria.md @@ -0,0 +1,185 @@ +# Acceptance Criteria — ET-015 / ORCH-6: Multi-repo support + +Work Item ID: ET-015 +Target repo: `orchestrator` + +Принять можно при выполнении ВСЕХ AC ниже. Формат — Given/When/Then. + +--- + +## AC-1. Реестр проектов загружается на старте + +**Given** оркестратор настроен с реестром из 2 проектов +(`enduro-trails`, `orchestrator`), +**When** контейнер стартует, +**Then** в логах есть запись `Loaded N projects: enduro-trails, orchestrator` +(или эквивалентная), и неработоспособный конфиг (например, отсутствие +`plane_project_id`) приводит к WARNING/ERROR с указанием конкретного +проекта и причины. + +## AC-2. Plane webhook маршрутизируется по project_id + +**Given** в реестре два проекта с разными `plane_project_id`, +**When** в `/webhook/plane` приходит `work_item.created` с +`project_id = `, +**Then** создаётся ветка в gitea-репо `enduro-trails`, +work item ID имеет префикс `ET-`, в Plane состояния обновляются +через workspace/project из конфига `enduro-trails`. + +**And When** аналогичный вебхук приходит с +`project_id = `, +**Then** создаётся ветка в gitea-репо `orchestrator`, +work item ID имеет префикс `ORCH-`, состояния обновляются в +правильном Plane-проекте. + +## AC-3. Gitea webhook маршрутизируется по repository.name + +**Given** в gitea зарегистрированы webhook'и на оба репо, +**When** в `/webhook/gitea` приходит `push` с +`repository.name = "orchestrator"`, +**Then** ищется задача `tasks` с `repo='orchestrator' AND +branch=`. Push'и в `enduro-trails` не должны затрагивать +задачи `orchestrator` и наоборот. + +## AC-4. Префикс work item ID — per-project + +**Given** в БД уже есть `ET-001..ET-015` для `enduro-trails` +и нет ни одной задачи для `orchestrator`, +**When** через webhook создаётся новая задача в `enduro-trails`, +**Then** её ID = `ET-016`. + +**And When** через webhook создаётся первая задача в `orchestrator`, +**Then** её ID = `ORCH-001`. + +**And When** создаётся вторая задача в `orchestrator`, +**Then** её ID = `ORCH-002`, **а не `ORCH-003`** (нумерация +не подскакивает из-за параллельной нумерации в `enduro-trails`). + +## AC-5. Plane state-id'ы берутся per-project + +**Given** Plane state-id'ы у `enduro-trails` и `orchestrator` +заведомо разные, +**When** оркестратор пытается выставить состояние `in_progress` +для work item `ORCH-001`, +**Then** в Plane API улетает PATCH с UUID состояния из конфига +проекта `orchestrator`, а **не** с UUID `b873d9eb-...` (in_progress +для `enduro-trails`). + +## AC-6. Plane comment роутится в правильный проект + +**Given** активны задачи `ET-016` и `ORCH-001`, +**When** оркестратор шлёт `add_comment("ORCH-001", "...")`, +**Then** комментарий появляется в issue `ORCH-001` в Plane-проекте +`Orchestrator`, и **не появляется** в Plane-проекте `enduro-trails`. + +## AC-7. Worktree per repo + +**Given** одновременно активны задачи в `enduro-trails` +(branch `feature/ET-016-x`) и `orchestrator` +(branch `feature/ORCH-001-y`), +**When** оркестратор запускает analyst-агента для обеих, +**Then** существуют два worktree: +- `/repos/_wt/enduro-trails/feature_ET-016-x/` +- `/repos/_wt/orchestrator/feature_ORCH-001-y/` + +И каждый агент работает в своём worktree, не пересекаясь. + +## AC-8. Agent-prompts читаются из своего репо + +**Given** в `orchestrator` лежит свой `.openclaw/agents/analyst.md`, +отличный от `enduro-trails`, +**When** запускается analyst для задачи `ORCH-001`, +**Then** в команде Claude CLI передаётся system-prompt из +`/repos/_wt/orchestrator//.openclaw/agents/analyst.md`, +**а не** из `enduro-trails`. + +## AC-9. Обратная совместимость по `enduro-trails` + +**Given** релиз раскатан, +**When** Owner создаёт новый work item в Plane-проекте `enduro-trails`, +**Then** поведение идентично текущему: ветка `feature/ET-NNN-slug`, +artifact'ы в `docs/work-items/ET-NNN/`, analyst запускается, +ID = следующий после `ET-015`. + +## AC-10. Существующие задачи не ломаются + +**Given** в `tasks` есть задачи в стадиях `analysis`, `development`, +`review` (из ET-001..ET-014, ET-015), +**When** контейнер перезапущен после раскатки фичи, +**Then** все эти задачи продолжают свой stage без потери контекста, +их `repo` остаётся `enduro-trails`, ID остаются прежними, +worktree для активных веток не пересоздаётся (или пересоздаётся без +потери uncommitted changes). + +## AC-11. /status — repo-aware + +**Given** активны 2 задачи в разных репо, +**When** Owner делает `GET /status`, +**Then** ответ содержит массив объектов, в каждом из которых +присутствуют поля `work_item_id`, `repo`, `branch`, `stage`. + +## AC-12. Неизвестный проект — мягкая обработка + +**Given** в `/webhook/plane` приходит событие с `project_id`, которого +нет в реестре, +**When** оркестратор обрабатывает событие, +**Then** ответ 202, в логах WARNING `Unknown project_id: `, +никаких side-эффектов (ни ветки, ни записи в `tasks`). +Telegram-уведомление отправляется не чаще 1 раза в 10 минут +для одного и того же неизвестного `project_id`. + +## AC-13. Отсутствие main-clone — внятная ошибка + +**Given** в реестре проект `foo`, но `/repos/foo` не существует, +**When** приходит вебхук на создание work item в `foo`, +**Then** в логах ERROR `Main repo not found: /repos/foo`, +Telegram-уведомление `[foo] main-clone отсутствует, проект не работает`, +HTTP-ответ 202 (не 500), запись в `tasks` НЕ создаётся (либо +создаётся со специальным флагом — на усмотрение архитектора, но +не должна привести к падению agent launcher'а). + +## AC-14. HMAC проверка per-project + +**Given** для проекта `enduro-trails` задан webhook secret `S1`, +для `orchestrator` — `S2`, +**When** в `/webhook/plane` приходит событие от `orchestrator`, +подписанное `S1`, +**Then** запрос отвергается с 401. + +**And When** то же событие подписано `S2`, +**Then** запрос принимается. + +## AC-15. Проект с `has_deploy_stage=false` + +**Given** проект `orchestrator` объявлен без deploy-стадии, +**When** в задаче `ORCH-001` стадия `testing` успешно завершена +(test report содержит PASS), +**Then** задача переходит сразу в `done`, deployer не запускается, +Plane issue получает state `done`. + +## AC-16. Тесты в репозитории `orchestrator` зелёные + +**Given** код изменений выложен в ветку, +**When** запускается `pytest tests/`, +**Then** все существующие тесты проходят + добавлены новые тесты +на multi-repo логику (см. test plan). + +## AC-17. Документация обновлена + +**Given** код мержится в `main` оркестратора, +**Then** обновлены: +- `README.md`: раздел про конфигурацию проектов; +- `docs/SETUP_WEBHOOKS.md`: инструкция per-project (если есть такой файл); +- Создан ADR в `docs/work-items/ET-015/06-adr/` по реестру проектов + и Plane state mapping. + +--- + +## Definition of Done + +- Все AC-1..AC-17 выполнены. +- Code review пройден (verdict: APPROVED во frontmatter 12-review.md). +- `pytest tests/` зелёный в репо `orchestrator`. +- Integration smoke на тестовом стенде: подключение второго проекта + и end-to-end создание тестового WI проходят успешно. +- Никаких регрессий на `enduro-trails`. diff --git a/docs/work-items/ET-015/04-test-plan.yaml b/docs/work-items/ET-015/04-test-plan.yaml new file mode 100644 index 0000000..65f0438 --- /dev/null +++ b/docs/work-items/ET-015/04-test-plan.yaml @@ -0,0 +1,253 @@ +work_item_id: ET-015 +title: "Multi-repo support для оркестратора" +target_repo: orchestrator +notes: > + Тесты пишутся и запускаются в репо `orchestrator` (pytest). + Поскольку фича чисто бэкендовая (нет UI-изменений в enduro-trails), + UI test cases НЕ создаются. + +unit: + - id: UT-01 + name: get_next_work_item_id — per-prefix numbering + file: tests/test_db_multi_repo.py::test_next_id_per_prefix + given: "tasks table has ET-001..ET-015 (repo=enduro-trails) and no ORCH-* rows" + when: "вызывается get_next_work_item_id для проекта с prefix='ET'" + expect: "возвращает 'ET-016'" + and_when: "вызывается get_next_work_item_id для проекта с prefix='ORCH'" + expect_2: "возвращает 'ORCH-001'" + covers_ac: [AC-4] + + - id: UT-02 + name: get_next_work_item_id — нумерация ORCH идёт независимо от ET + file: tests/test_db_multi_repo.py::test_orch_numbering_independent + given: "в tasks есть ET-001..ET-015 и ORCH-001..ORCH-003" + when: "запрашивается следующий ID для ORCH" + expect: "ORCH-004 (не ORCH-016)" + covers_ac: [AC-4] + + - id: UT-03 + name: ProjectRegistry — load from config + file: tests/test_project_registry.py::test_load_two_projects + given: "конфиг с двумя проектами" + when: "registry загружается на старте" + expect: "len(registry.projects) == 2; lookup по plane_project_id возвращает правильный key" + covers_ac: [AC-1, AC-2] + + - id: UT-04 + name: ProjectRegistry — invalid config + file: tests/test_project_registry.py::test_invalid_config_logged + given: "конфиг с проектом без plane_project_id" + when: "registry загружается" + expect: "запись пропускается с WARNING, остальные грузятся" + covers_ac: [AC-1] + + - id: UT-05 + name: ProjectRegistry — lookup by plane_project_id + file: tests/test_project_registry.py::test_lookup_by_plane_project_id + given: "registry с тремя проектами" + when: "lookup по plane_project_id='unknown-uuid'" + expect: "возвращает None" + covers_ac: [AC-12] + + - id: UT-06 + name: ProjectRegistry — lookup by repo name + file: tests/test_project_registry.py::test_lookup_by_repo + given: "registry с проектом repo='orchestrator'" + when: "lookup('orchestrator')" + expect: "возвращает конфиг проекта" + covers_ac: [AC-3] + + - id: UT-07 + name: plane_sync — workspace/project определяется по work_item_id + file: tests/test_plane_sync_multi.py::test_workspace_per_project + given: "tasks содержит ORCH-001 (repo=orchestrator); registry имеет настройки orchestrator" + when: "вызывается update_issue_state('ORCH-001', 'in_progress')" + expect: "httpx.patch вызывается с URL содержащим workspace проекта orchestrator и state-id из его plane_states" + covers_ac: [AC-5, AC-6] + + - id: UT-08 + name: verify_plane_signature — per-project secret + file: tests/test_webhooks_multi.py::test_hmac_per_project + given: "проект orchestrator имеет secret S2" + when: "вебхук подписан S1" + expect: "verify возвращает False" + and_when: "вебхук подписан S2" + expect_2: "verify возвращает True" + covers_ac: [AC-14] + + - id: UT-09 + name: get_repo_path_or_fail — отсутствие main-clone + file: tests/test_launcher_multi.py::test_missing_main_clone + given: "проект foo в реестре, /repos/foo не существует" + when: "launcher.launch('analyst', 'foo', ...)" + expect: "raises FileNotFoundError И логируется ERROR с текстом 'Main repo not found'" + covers_ac: [AC-13] + + - id: UT-10 + name: has_deploy_stage=false — skip deployer + file: tests/test_launcher_multi.py::test_skip_deploy_stage + given: "проект orchestrator с has_deploy_stage=false; задача в testing с PASS test report" + when: "_try_advance_stage вызывается" + expect: "task.stage становится done; deployer.launch НЕ вызывается" + covers_ac: [AC-15] + + - id: UT-11 + name: STAGE_TO_STATE — стейт берётся из конфига проекта + file: tests/test_plane_sync_multi.py::test_stage_to_state_per_project + given: "два проекта с разными UUID для in_progress" + when: "оба переходят в стадию development" + expect: "patch с разными UUID — каждый со своим" + covers_ac: [AC-5] + + - id: UT-12 + name: handle_work_item_created — branch создаётся в нужном репо + file: tests/test_webhooks_multi.py::test_branch_in_correct_repo + given: "вебхук с project_id orchestrator" + when: "обрабатывается work_item.created" + expect: "POST /api/v1/repos/admin/orchestrator/branches; НЕ /enduro-trails/branches" + covers_ac: [AC-2] + +integration: + - id: IT-01 + name: End-to-end создание задачи в проекте enduro-trails + file: tests/integration/test_e2e_enduro.py::test_create_et_task + given: "оркестратор запущен, registry с обоими проектами; mock Plane + mock Gitea" + when: "POST /webhook/plane с work_item.created для enduro-trails" + expect: + - "Plane mock получил PATCH issue.state = in_progress" + - "Gitea mock получил POST /branches" + - "tasks содержит запись (repo=enduro-trails, work_item_id=ET-NNN, stage=analysis)" + - "Был вызван launcher.launch('analyst', 'enduro-trails', ...)" + covers_ac: [AC-2, AC-9] + + - id: IT-02 + name: End-to-end создание задачи в проекте orchestrator + file: tests/integration/test_e2e_orch.py::test_create_orch_task + given: "то же, что IT-01" + when: "POST /webhook/plane с work_item.created для orchestrator (другой project_id)" + expect: + - "Gitea mock получил POST /repos/admin/orchestrator/branches" + - "tasks содержит (repo=orchestrator, work_item_id=ORCH-001)" + - "launcher.launch('analyst', 'orchestrator', ...) — НЕ enduro-trails" + covers_ac: [AC-2, AC-4] + + - id: IT-03 + name: Параллельные задачи в разных репо не пересекаются + file: tests/integration/test_concurrent_repos.py::test_two_repos + given: "две задачи: ET-099 в enduro-trails и ORCH-099 в orchestrator" + when: "обе на стадии analysis с активными worktree" + expect: + - "/repos/_wt/enduro-trails/feature_ET-099-... существует" + - "/repos/_wt/orchestrator/feature_ORCH-099-... существует" + - "артефакты ET записаны в enduro-trails worktree, ORCH — в orchestrator worktree" + covers_ac: [AC-7] + + - id: IT-04 + name: Gitea push для одного репо не задевает задачи в другом + file: tests/integration/test_gitea_routing.py::test_push_isolation + given: "активны ET-099 и ORCH-099" + when: "приходит push на ветку ORCH-099 в репо orchestrator" + expect: + - "tasks.stage для ORCH-099 обновляется при наличии валидных файлов" + - "tasks.stage для ET-099 НЕ меняется" + covers_ac: [AC-3] + + - id: IT-05 + name: Plane comment :approved: для orchestrator advance'ит правильную задачу + file: tests/integration/test_plane_comment_routing.py::test_approved_routing + given: "ET-099 stage=analysis, ORCH-099 stage=analysis" + when: "comment.created с :approved: на issue ORCH-099" + expect: + - "tasks для ORCH-099 advance в architecture" + - "tasks для ET-099 не меняется" + covers_ac: [AC-6] + + - id: IT-06 + name: Неизвестный project_id игнорируется без 500 + file: tests/integration/test_unknown_project.py::test_unknown_returns_202 + given: "registry с одним проектом enduro-trails" + when: "приходит вебхук с project_id='unknown'" + expect: + - "HTTP 202" + - "WARNING в логах с текстом 'Unknown project_id'" + - "tasks без новых записей" + covers_ac: [AC-12] + + - id: IT-07 + name: Отсутствие main-clone — Telegram + 202 + file: tests/integration/test_missing_clone.py::test_no_main_clone + given: "registry с проектом foo, /repos/foo отсутствует; Telegram mock" + when: "приходит work_item.created для foo" + expect: + - "HTTP 202" + - "ERROR в логах 'Main repo not found: /repos/foo'" + - "Telegram mock получил одно сообщение" + covers_ac: [AC-13] + +e2e: + - id: E2E-01 + name: Smoke: подключение нового проекта на тестовом стенде + type: manual + where: "test stand (mva154)" + steps: + - "Подготовить /repos/orchestrator на хосте (git clone, если ещё нет)" + - "Создать в Plane проект 'Orchestrator-smoke', получить state UUID'ы" + - "Добавить запись в конфиг проектов оркестратора (см. ADR)" + - "Перезапустить контейнер оркестратора: docker compose restart" + - "Проверить логи: 'Loaded 2 projects: enduro-trails, orchestrator-smoke'" + - "Зарегистрировать webhook Plane (target orchestrator) для Orchestrator-smoke" + - "Создать тестовый work item 'Smoke ORCH-1' в Plane" + - "Дождаться появления ветки в gitea-репо orchestrator (~30s)" + - "Проверить tasks в БД оркестратора: repo=orchestrator, work_item_id=ORCH-001" + - "Удалить тестовую запись Plane и проверить, что enduro-trails не пострадал" + expect: "Все шаги без ошибок, регрессий на enduro-trails нет" + covers_ac: [AC-2, AC-4, AC-7, AC-9, AC-17] + + - id: E2E-02 + name: Smoke: создание следующей задачи в enduro-trails после релиза + type: manual + where: "test stand" + steps: + - "После раскатки фичи создать в Plane-проекте enduro-trails новый WI" + - "Дождаться появления ветки feature/ET-016-..." + - "Проверить artifact'ы в docs/work-items/ET-016/" + expect: "ID=ET-016 (следующий после ET-015), всё как раньше" + covers_ac: [AC-9, AC-4] + +regression: + - id: REG-01 + name: Активные задачи продолжают работу после рестарта + type: manual + where: "test stand" + steps: + - "Перед раскаткой запомнить snapshot tasks (id, stage, repo, branch)" + - "Раскатать фичу" + - "Перезапустить orchestrator container" + - "Проверить tasks snapshot после рестарта" + expect: "Записи идентичны (stage не сбился, repo/branch не изменились)" + covers_ac: [AC-10] + + - id: REG-02 + name: Test report check_tests_local продолжает работать + given: "enduro-trails branch с зелёными тестами" + when: "agent developer завершается" + expect: "QG check_tests_local вызывает make test в worktree и проходит" + covers_ac: [AC-9] + +coverage: + required_min_percent: 75 + applies_to: + - src/config.py + - src/db.py + - src/plane_sync.py + - src/webhooks/plane.py + - src/webhooks/gitea.py + - src/agents/launcher.py + new_modules_expected: + - src/project_registry.py # имя ориентировочное, точное название — на усмотрение архитектора + +notes_for_tester: + - "В существующих тестах могут быть моки на settings.default_repo — их нужно расширить под новый registry" + - "find_issue_id в plane_sync.py делает DB lookup — учесть это при моках" + - "Telegram-нотификации логировать через send_telegram моки, чтобы избежать реальных запросов" + - "Не нужно тестировать сами Plane/Gitea API — только что оркестратор формирует правильные запросы"