14 KiB
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_STATES6 новыми 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.
Надо:
- launcher.py: в claude-команду (строка ~212) добавить
--output-format json. ⚠️ Тогда stdout = ОДИН JSON в конце (не текст). Проверь что мониторинг/парсинг лога не ломается (сейчас лог парсится на ошибки в _monitor_agent). JSON писать в лог как сейчас, плюс распарсить usage по завершении. - 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 — как принято в проекте; глянь как делались прошлые миграции схемы). - По завершении run'а (_monitor_agent / где exit_code пишется): прочитать output_path, распарсить последний JSON-объект, извлечь usage+cost, записать в agent_runs.
- ⚠️ Если JSON не распарсился (агент упал/таймаут) — записать NULL/0, НЕ падать. Логировать warning.
- Коммент в Plane (вариант A): при завершении агента постить в задачу под именем этого агента: например
💻 Developer готов · 45.2k in / 12.1k out · $0.21(форматируй токены k/M, cost 2 знака). Используй существующийadd_comment(..., author=<role>). - Итог по задаче (вариант B): при переходе в deploy/done — Deployer постит сводку: суммарно по всем agent_runs задачи:
📊 Итого по задаче: <N> токенов вход / <M> выход · $<total>+ разбивка по агентам (по строкам). SQL:SELECT agent, SUM(...) FROM agent_runs WHERE task_id=? GROUP BY agent. - (опц., если просто) эндпоинт
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-парса. Если что-то разошлось с кодом — отчитайся, не выдумывай. Один исполнитель.