# DEV TASK — Вердикт ТОЛЬКО статусами (выпил комментного approve + фикс эхо-самоудара) **Проект:** orchestrator | **Сервер:** slin@82.22.50.71 | **Репо:** /home/slin/repos/orchestrator | **Контейнер:** orchestrator (8500) **Ветка:** `fix/status-only-verdict` из свежего main (`git checkout main && git pull && git checkout -b fix/status-only-verdict`). ⚠️ **ГРАБЛЯ push (ORCH-7):** после push `git log origin/main..origin/fix/status-only-verdict` ДОЛЖЕН показать коммиты ДО отчёта «PR готов». ℹ️ Токен Gitea для PR: `docker exec orchestrator printenv ORCH_GITEA_TOKEN`. ## Контекст и ПРАВИЛЬНАЯ модель (источник правды — решение Славы, подтверждено 03.06) Слава управляет конвейером **ТОЛЬКО СТАТУСАМИ**. **Комменты НИКОГДА не триггерят переходы.** Точная модель: - **Одобрение стадии** → Слава ставит статус **Approved** → оркестратор двигает на следующую стадию (сам выставляет статус следующей стадии). - **Отклонение** → Слава **сначала пишет коммент с причиной**, **потом** ставит статус **Rejected** → оркестратор откатывает + читает причину из последнего коммента. - **Ответ на вопросы аналитика** (задача в **Needs Input**) → Слава пишет коммент(ы) с ответами, **потом сам возвращает статус в In Progress** → оркестратор перезапускает агента текущей стадии (analyst читает комменты из Plane). Триггер — СТАТУС In Progress, НЕ коммент. **КОММЕНТНЫЙ МЕХАНИЗМ УПРАВЛЕНИЯ ВЫПИЛИВАЕТСЯ ПОЛНОСТЬЮ.** `:approved:`/`:rejected:` в комментах + «answer-to-questions по комменту» — всё убрать. Они создавали баг 3 (эхо-самоудар): analyst постит свой коммент «жду approved» → handle_comment ловит свой же коммент → откатывает In Review → In Progress. ## БАГ 3 (root) — самоудар + лишний PATCH **Симптом:** analyst довёл задачу до **In Review** корректно, через 0.1с статус откатился в **In Progress**, уведомления нет. **Две причины:** ### Причина A — handle_comment управляет конвейером (выпилить весь механизм) `src/webhooks/plane.py`, `handle_comment` (~397-490): **весь комментный механизм управления выпиливается.** - Ветка `if ":approved:" in comment_body:` (~427-431) → **ВЫПИЛИТЬ.** - Ветка `if ":rejected:" in comment_body:` (~420-424) → **ВЫПИЛИТЬ.** - Ветка `if current_stage == "analysis":` (answer-to-questions по комменту, ~433-490) → **ВЫПИЛИТЬ.** Ответ на вопросы теперь идёт через возврат СТАТУСА в In Progress (см. ниже handle_status_start), не через коммент. - Итого `handle_comment` либо удаляется целиком (и роутер `comment.created` → no-op/лог), либо сводится к чистому логированию без side-effect. Реши сам — главное: **ни один коммент не меняет статус и не запускает агентов.** Сохрани QG-лог если он информативен, но без действий. ### Причина A2 — возврат в In Progress из Needs Input должен ПЕРЕЗАПУСКАТЬ агента `handle_status_start` (~161-172) сейчас идемпотентен: `if existing task → not restarting`. Это правильно для защиты от дублей, НО ломает кейс «ответ на вопросы»: Слава вернул задачу из **Needs Input** в **In Progress**, ожидая что analyst перезапустится. **Надо:** если task существует И задача «ждала» (предыдущий статус был needs_input / стадия analysis в ожидании) → **перезапустить агента текущей стадии** (enqueue), чтобы он прочитал новые комменты Славы. - Как отличить «обычный повторный In Progress» (защита от дублей) от «ответ на вопросы»: проверь предыдущий Plane-статус issue (был needs_input) ИЛИ флаг ожидания в БД. Определи самый надёжный способ на проде (есть ли в БД поле статуса/ожидания; или GET issue даёт прошлый статус через activity). Если нет running job по task И стадия активная (analysis) → перезапуск analyst с пометкой «прочитай свежие комменты Славы и доработай артефакты». Защита от дублей (нет двойного запуска при двойном webhook) — сохрани через проверку running job в очереди. - ⚠️ Если надёжно определить «был needs_input» сложно — отчитайся с вариантом реализации ДО мержа (это ключевая развилка). Минимально: рестарт если стадия analysis И нет активной/running job для task. ### Причина B — handle_verdict(Approved) сам сбивает статус в In Progress `handle_verdict` (~170-200), ветка `if approved:` (~185): ```python set_issue_in_progress(work_item_id) # ← УБРАТЬ await _try_advance_stage(...) ``` `set_issue_in_progress` здесь лишний — он откатывает статус назад перед advance. `_try_advance_stage` сам выставит статус следующей стадии (architecture/development/...). **Убрать `set_issue_in_progress` из ветки approved.** Проверь, что `_try_advance_stage` действительно сам ставит статус стадии (если НЕ ставит — тогда вместо in_progress выставить статус нужной стадии, НЕ in_progress). ## REJECT: причина из коммента в handle_verdict Сейчас `handle_verdict(approved=False)` передаёт фиксированную заглушку причины. **Надо:** при Rejected-статусе дотянуть **последний коммент** issue из Plane API (GET comments, взять самый свежий от человека) как `reason` и передать в `_rollback_stage`. Слава пишет причину отдельным комментом ПЕРЕД/вместе со сменой статуса. - GET comments endpoint: `{PLANE_BASE}/workspaces/{WORKSPACE}/projects/{pid}/issues/{issue_id}/comments/` (тот же PLANE_HEADERS). Взять последний по created_at, стрипнуть HTML. - Если коммента нет → заглушка «Rejected via status, no reason comment». ## Ограничения - 🚫 НЕ трогай: nginx/openclaw.json/.env-секреты/deploy-хук/HMAC/verify_plane_signature/очередь(queue_worker/jobs кроме чтения)/webhook URL в БД Plane/per-agent authorship/PLANE_STATES UUID/M-6 derive/uniqueness-guard(из PR #11)/fetch_issue_description(PR #11)/conftest.py. - Conventional Commits, отдельные коммиты: `fix(webhook): remove comment-based approve, keep status-only verdict`, `fix(webhook): drop redundant in_progress reset on Approved`, `feat(webhook): pull reject reason from latest comment`. - ⚠️ Старые тесты `test_webhooks.py::test_plane_approved_advances_stage`, `test_plane_rejected_rolls_back`, `test_verdict_status.py` проверяют КОММЕНТНЫЙ `:approved:`/`:rejected:`. Раз комментный механизм выпиливается — **перепиши их под статусную модель** (approve/reject через handle_verdict по СТАТУСУ). Тесты, проверяющие что коммент `:approved:` двигает стадию — перепиши на обратное утверждение: «коммент НЕ триггерит переход». ## Тесты (контейнер) `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` Новые/обновлённые: - `test_inreview_comment_does_not_revert`: задача в In Review, прилетает ЛЮБОЙ коммент → статус НЕ меняется, агенты НЕ запускаются. **Главный тест бага 3.** - `test_any_comment_no_pipeline_action`: коммент с `:approved:`/`:rejected:`/произвольным текстом → никаких side-effect (статус/очередь не трогаются). - `test_approved_status_advances_without_inprogress_reset`: Approved-статус → advance, БЕЗ промежуточного PATCH в in_progress. - `test_rejected_status_pulls_reason_from_comment`: Rejected-статус + последний коммент с причиной (мок GET comments) → reason передан в rollback. - `test_inprogress_from_needs_input_relaunches_analyst`: задача в analysis/needs_input → возврат статуса в In Progress → analyst перезапущен (enqueue), читает комменты. + защита: двойной In Progress webhook не плодит двойной запуск. - Перепиши `test_plane_approved_advances_stage`/`test_plane_rejected_rolls_back`/`test_verdict_status` под статусную модель. ## Проверка (ассистент проверит вживую боевым прогоном) | # | Что | Критерий | |---|-----|----------| | 1 | In Review держится | analyst → In Review, статус НЕ откатывается, уведомление с просьбой одобрить приходит | | 2 | Approved-статус | Слава ставит Approved → задача уходит в Architecture, БЕЗ мелькания In Progress | | 3 | Rejected-статус+коммент | Слава ставит Rejected + коммент причины → откат, analyst получает причину | | 4 | коммент не рулит | `:approved:` в комменте больше НЕ двигает задачу | | 5 | тесты | baseline не сломан + новые passed | | 6 | git | PR в main, remote содержит коммиты | ## Отчёт - НЕ деплоить, НЕ мержить (мерж — ассистент после живой проверки + боевого прогона #6). - В отчёте: что именно выпилил (файл:строки), **как реализовал перезапуск агента при возврате In Progress из Needs Input** (как отличаешь от защиты-дублей — ключевая развилка, опиши подробно), как тянешь reject-reason, какие старые тесты переписал и как, вывод релевантных тестов. Если что-то в коде разошлось с этим ТЗ (напр. _try_advance_stage сам НЕ ставит статус стадии, или нет способа узнать прошлый статус) — **отчитайся ДО мержа, не выдумывай**. Один исполнитель.