# DEV TASK — Конвейер UX: триггер-по-статусу, вердикт-статусы, видимость стадий, расход токенов **Проект:** orchestrator | **Сервер:** slin@82.22.50.71 | **Репо:** /home/slin/repos/orchestrator | **Контейнер:** orchestrator (8500) **Ветка:** `feature/pipeline-ux` из свежего main (`git checkout main && git pull && git checkout -b feature/pipeline-ux`). ⚠️ **ГРАБЛЯ push (ORCH-7):** после push `git log origin/main..origin/feature/pipeline-ux` ДОЛЖЕН показать коммиты ДО отчёта «PR готов». ℹ️ Токен Gitea для PR — рабочий из env контейнера: `docker exec orchestrator printenv ORCH_GITEA_TOKEN` (.env-токен может быть 401). ## Контекст Слава ведёт бэклог в Plane и хочет: (1) задача лежит в бэклоге пока он не запустит; (2) одобрение/отклонение — СТАТУСАМИ, не только комментами; (3) видеть на доске какая стадия идёт; (4) видеть расход токенов каждого агента. **ИНФРА УЖЕ ГОТОВА Стримом (НЕ трогай БД Plane):** в проекте enduro (`7a79f0a9-5278-49cd-9007-9a338f238f9c`) созданы статусы с UUID: ``` architecture = 3020bbb7-6122-4663-930c-0315ba8dfa3d development = 9920609b-f140-4e46-ab95-89acda8412c8 review = ba0d802c-5218-41d4-ab43-978b0ea123ed testing = 7855d807-b1bf-42ef-8dae-6cde0df92d02 approved = a519a341-dada-4a91-8910-7604f82b79c5 rejected = ba958f3c-5db5-461d-8f82-89425e413b97 ``` Существующие (PLANE_STATES уже в `src/plane_sync.py`): backlog/todo/in_progress/needs_input/in_review/blocked/done/cancelled. ⚠️ **ВАЖНО про реестр проектов (ORCH-6):** UUID статусов выше — ТОЛЬКО для проекта enduro. Сейчас `PLANE_STATES` — глобальный словарь. Для текущей задачи **достаточно расширить глобальный PLANE_STATES** этими UUID (один проект в проде). Но в коде ОСТАВЬ TODO-коммент: «при онбординге новых проектов статусы должны резолвиться по проекту (см. ORCH-10 в BACKLOG.md)». НЕ переделывай резолв на per-project сейчас — это отдельный эпик. --- ## ФИЧА 1 — Старт конвейера по статусу (не по созданию) **Сейчас (`src/webhooks/plane.py:94`):** `work_item.created` → сразу `handle_work_item_created` → enqueue analyst. Бэклог невозможен. **Надо:** - `work_item.created` БОЛЬШЕ НЕ запускает конвейер. На создание — только лог (опц. валидация QG-0 можно оставить как «мягкую» проверку, но НЕ создавать ветку/не запускать analyst). - Запуск конвейера = **переход статуса в `In Progress`** (`state changed → in_progress`) из бэклог-статуса (Backlog/Todo/Triage). - Plane шлёт webhook на изменение issue (`event="issue", action="updated"` либо `work_item.updated`) с новым state. Определи фактический формат payload (проверь `delivery`/payload Plane на проде — см. ниже «как смотреть webhook»). Лови переход на in_progress и вызывай существующую логику создания задачи + запуска analyst (вынеси из handle_work_item_created переиспользуемую функцию `start_pipeline(data, project_id)` если нужно). - Идемпотентность: если задача для этого plane_id УЖЕ есть в БД (`tasks`) — повторный переход в in_progress НЕ создаёт дубль и НЕ перезапускает analyst (просто лог). insert_event_dedup уже есть — используй. ⚠️ Грабля: не сломай существующий `handle_comment` (он тоже дёргает set_issue_in_progress при approve/ответах — это НЕ должно триггерить повторный старт; защита через «задача уже существует»). ## ФИЧА 2 — Вердикт статусами (вариант B): Approved / Rejected **Сейчас:** approve/reject только через комменты `:approved:`/`:rejected:` в `handle_comment`. **Надо ДОБАВИТЬ (комменты ОСТАВИТЬ рабочими — оба механизма):** - Перевод issue в статус **Approved** = то же, что коммент `:approved:` → QG текущей стадии → advance. Переиспользуй `_try_advance_stage`. - Перевод в статус **Rejected** = то же, что `:rejected:` → откат на предыдущую стадию + перезапуск агента. Переиспользуй существующую rollback-ветку. - ⚠️ Для Rejected причина не приходит со статусом → агенту передавай «Reason: (rejected via status, see latest comment)». Слава причину пишет отдельным комментом (это его процесс). - После обработки вердикта верни issue в рабочий статус стадии (Approved→ статус следующей стадии; Rejected→ статус предыдущей) — см. Фича 3 (set_issue по стадии). ## ФИЧА 3 — Видимость стадий на доске **Сейчас:** статус меняется только на in_review/needs_input/in_progress/blocked/done. Стадии architecture/development/review/testing невидимы (всё In Progress). **Надо:** - Расширить `PLANE_STATES` 6 новыми UUID (выше). - Маппинг стадия→статус, напр. `STAGE_TO_STATE = {"architecture":"architecture","development":"development","review":"review","testing":"testing"}` (analysis остаётся как есть — in_progress/in_review/needs_input по текущей логике). - При входе в стадию (в `advance_stage` / при enqueue агента стадии) дёргать `set_issue_state(work_item_id, STAGE_TO_STATE[stage])`. Добавь хелпер `set_issue_stage_state(work_item_id, stage, project_id=None)` в plane_sync по образцу `set_issue_in_review`. - На доске должно быть видно: задача физически переезжает Architecture→Development→Review→Testing→Done. - ⚠️ НЕ ломай Needs Input / In Review / Blocked — они приоритетнее (если агент задал вопрос — статус Needs Input, а не Development). ## ФИЧА 4 — Расход токенов по агентам **CLI подтверждён вживую:** Claude Code 2.1.142, `--output-format json` отдаёт single-result JSON с полями: `total_cost_usd`, `usage.input_tokens`, `usage.output_tokens`, `usage.cache_read_input_tokens`, `usage.cache_creation_input_tokens`, `modelUsage`, `num_turns`, `duration_ms`. **Надо:** 1. **launcher.py:** в claude-команду (строка ~212) добавить `--output-format json`. ⚠️ Тогда stdout = ОДИН JSON в конце (не текст). Проверь что мониторинг/парсинг лога не ломается (сейчас лог парсится на ошибки в _monitor_agent). JSON писать в лог как сейчас, плюс распарсить usage по завершении. 2. **db.py:** ALTER/добавить в `agent_runs` колонки: `input_tokens INTEGER`, `output_tokens INTEGER`, `cache_read_tokens INTEGER`, `cost_usd REAL`. Сделать через идемпотентную миграцию (try ALTER TABLE ADD COLUMN, ignore if exists — как принято в проекте; глянь как делались прошлые миграции схемы). 3. По завершении run'а (_monitor_agent / где exit_code пишется): прочитать output_path, распарсить последний JSON-объект, извлечь usage+cost, записать в agent_runs. - ⚠️ Если JSON не распарсился (агент упал/таймаут) — записать NULL/0, НЕ падать. Логировать warning. 4. **Коммент в Plane (вариант A):** при завершении агента постить в задачу под именем этого агента: например `💻 Developer готов · 45.2k in / 12.1k out · $0.21` (форматируй токены k/M, cost 2 знака). Используй существующий `add_comment(..., author=)`. 5. **Итог по задаче (вариант B):** при переходе в deploy/done — Deployer постит сводку: суммарно по всем agent_runs задачи: `📊 Итого по задаче: токенов вход / выход · $` + разбивка по агентам (по строкам). SQL: `SELECT agent, SUM(...) FROM agent_runs WHERE task_id=? GROUP BY agent`. 6. (опц., если просто) эндпоинт `GET /tasks/{work_item_id}/usage` — JSON со сводкой. Не обязательно, но плюс. --- ## Ограничения - 🚫 НЕ трогай: БД Plane (статусы/боты уже созданы — только ЧИТАЙ UUID), nginx, openclaw.json, deploy-хук, HMAC/verify_plane_signature, очередь (queue_worker/jobs — кроме чтения), ORCH-6 резолв проектов (оставь TODO, не переделывай), get_next_work_item_id, M-6, per-agent authorship код (используй add_comment(author=), не ломай). - Комменты `:approved:`/`:rejected:` ОСТАЮТСЯ рабочими (оба механизма параллельно). - Baseline: **166 passed**, 9 pre-existing 401 — не чинить/не ломать. - Conventional Commits, отдельные коммиты по фичам (feat(webhook) старт-по-статусу, feat(webhook) вердикт-статусы, feat(plane) видимость стадий, feat(metrics) токены). ## Как смотреть формат Plane webhook (для Фич 1,2) - Прод принимает на `/webhook/plane`. Реальные payload'ы есть в таблице дедупликации событий (`insert_event_dedup` пишет body). Посмотри последние: `docker exec orchestrator python3 -c "import sqlite3;[print(r) for r in sqlite3.connect('/app/data/orchestrator.db').execute('SELECT source,event_type,substr(payload,1,400) FROM webhook_events ORDER BY id DESC LIMIT 5')]"` (уточни имя таблицы/колонок в db.py). - Если событий обновления статуса нет — попроси Стрим перевести тестовую задачу между статусами в Plane, чтобы поймать payload. Не выдумывай структуру. ## Тесты (контейнер) `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` - `tests/test_status_trigger.py`: created НЕ запускает; переход→in_progress запускает; повторный переход не дублирует. - `tests/test_verdict_status.py`: Approved→advance; Rejected→rollback; комменты по-прежнему работают. - `tests/test_stage_visibility.py`: вход в стадию → set правильного state UUID; Needs Input приоритетнее. - `tests/test_usage.py`: парсинг usage из примера CLI-JSON; запись в agent_runs; коммент с форматом токенов; сводка по задаче. Мокать httpx/subprocess. ## Проверка (Стрим проверит вживую) | # | Что | Критерий | |---|-----|----------| | 1 | старт по статусу | created не стартует; Backlog→In Progress стартует analyst; нет дублей | | 2 | вердикт-статусы | Approved=advance, Rejected=rollback; комменты живы | | 3 | стадии на доске | задача переезжает по колонкам Architecture→…→Testing; Needs Input/Blocked приоритетны | | 4 | токены | agent_runs пишет токены+cost; коммент агента с расходом; сводка Deployer | | 5 | тесты | 166 → 166+новые passed, 9 baseline не тронуты | | 6 | git | PR в main, remote содержит коммиты | ## Отчёт - НЕ деплоить, НЕ мержить (мерж — Стрим после живой проверки). Запушь ветку (ПРОВЕРЬ remote!), PR в main. - В отчёте: какой РЕАЛЬНЫЙ формат Plane-webhook на обновление статуса ты нашёл (event/action/где state); таблица «фича → файлы/функции»; пример строки usage-коммента; вывод одного boevoго usage-парса. Если что-то разошлось с кодом — отчитайся, не выдумывай. Один исполнитель.