analyst(ET): auto-commit from analyst run_id=56
Some checks failed
CI / lint (push) Failing after 4s
CI / test (push) Failing after 5s
CI / build (push) Has been skipped

This commit is contained in:
2026-06-02 19:03:41 +00:00
parent 14e0d32050
commit 68076fca1a
4 changed files with 877 additions and 0 deletions

View File

@@ -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`,
строка ~9399): любой новый таск, откуда бы он ни пришёл, получает
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/<repo>/<safe-branch>` уже репо-aware, но
оркестратор должен предполагать наличие main-clone'а для каждого репо
в `/repos/<repo>`. Если main-clone отсутствует — внятная ошибка
в логах + Telegram (а не падение).
### BR-6. Agent prompts и фазы — per-repo
Agent-промпты (`.openclaw/agents/<agent>.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/<branch>`.
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.

View File

@@ -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>-<NNN>`, где `prefix` берётся из конфигурации
проекта.
- Номер — следующий после максимального уже существующего ID **с тем
же префиксом** в таблице `tasks`. Поиск ведётся по
`work_item_id LIKE '<prefix>-%'` (а не по `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/<agent>.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: <repo>` — это уже так. Подтвердить.
### FR-7. Worktree
- **FR-7.1** — `ensure_worktree(repo, branch)` корректно работает для
любого репо. Уже так; нужны лишь интеграционные тесты на 2 разных
репо одновременно.
- **FR-7.2** — если `/repos/<repo>` не существует, оркестратор:
логирует `ERROR`, шлёт Telegram «main-clone для <repo> не найден,
подключение проекта неполное», возвращает 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=<key>` в строке
записи там, где это применимо (push/PR handler, agent launch).
- **FR-8.3** — Telegram-сообщения для конкретной задачи содержат либо
`<work_item_id>` (он уже несёт префикс), либо префикс `[<repo>]`.
Не вводить нового поля, использовать существующий 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/<work_item_id>/...` сохраняется. Имена
файлов (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/<repo>`.
## 8. Связанные ADR / документы
- Будут созданы в стадии Architecture:
- ADR: «Project registry: формат и место хранения».
- ADR: «Plane state mapping per project».
- Возможно: «Migration plan — config rollout».

View File

@@ -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 = <enduro_trails_uuid>`,
**Then** создаётся ветка в gitea-репо `enduro-trails`,
work item ID имеет префикс `ET-`, в Plane состояния обновляются
через workspace/project из конфига `enduro-trails`.
**And When** аналогичный вебхук приходит с
`project_id = <orchestrator_uuid>`,
**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=<refs/heads/...>`. 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/<branch>/.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: <uuid>`,
никаких 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`.

View File

@@ -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 — только что оркестратор формирует правильные запросы"