diff --git a/docs/work-items/ET-012/03-acceptance-criteria.md b/docs/work-items/ET-012/03-acceptance-criteria.md new file mode 100644 index 0000000..d7452d7 --- /dev/null +++ b/docs/work-items/ET-012/03-acceptance-criteria.md @@ -0,0 +1,256 @@ +--- +type: acceptance-criteria +work_item_id: ET-012 +title: "Acceptance Criteria: Единый stage-engine оркестратора (M-3)" +version: 1 +status: draft +created_at: 2026-06-02 +updated_at: 2026-06-02 +authors: + - "agent:analyst" +target_repo: "orchestrator" +--- + +# Acceptance Criteria — ET-012 + +Все критерии формализованы в Gherkin-стиле. Задача считается принятой, +когда **каждый** прошёл проверку в pytest и/или в проверочном +сценарии на test-стенде оркестратора. + +## AC-01 — `StageEngine` существует и имеет каноническую сигнатуру + +**Given** репозиторий `orchestrator` на ветке +`feature/ET-012-m-3-stage-engine-try-advance-s` +**When** developer завершил работу +**Then** существует файл `src/stage_engine.py`, в котором +определён класс `StageEngine` с публичным методом +`advance(task_id: int, trigger: StageTrigger, +ctx: AdvanceContext) -> AdvanceResult`, +и `StageTrigger`, `AdvanceContext`, `AdvanceResult`, +`HookOutcome` экспортированы из этого же модуля. + +## AC-02 — В `launcher.py` остался один тонкий вызов + +**Given** `src/agents/launcher.py` +**When** инспекция кода +**Then**: +- метод `_try_advance_stage` существует; +- его тело занимает ≤ 30 строк (без блока try/except); +- тело состоит из: чтение task, сборка `AdvanceContext`, вызов + `self.stage_engine.advance(...)` с `trigger=AGENT_FINISHED`; +- нет прямых вызовов `update_task_stage`, `QG_CHECKS[...]`, + `notify_stage_change`, `plane_notify_stage`, `launcher.launch(...)` + внутри `_try_advance_stage`. + +## AC-03 — В `plane.py` остался один тонкий вызов + +**Given** `src/webhooks/plane.py` +**When** инспекция кода +**Then**: +- функция `_try_advance_stage` существует; +- её тело занимает ≤ 20 строк; +- тело состоит из сборки `AdvanceContext` и вызова + `await asyncio.to_thread(stage_engine.advance, task_id, + StageTrigger.MANUAL_APPROVE, ctx)`; +- нет прямых вызовов `update_task_stage`, `QG_CHECKS[...]`, + `notify_stage_change`, `plane_notify_stage`, `launcher.launch(...)` + внутри тела. + +## AC-04 — Auto-flow analyst → review request — без регрессии + +**Given** задача в стадии `analysis`, агент `analyst` только что +завершился с `exit_code=0`, в репозитории есть полные артефакты +(`01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`, +`04-test-plan.yaml`) +**When** оркестратор фиксирует завершение subprocess +**Then**: +- `tasks.stage` остаётся `analysis` (стадия НЕ продвигается до + ручного `:approved:`); +- в Plane выставлен state `in_review`; +- в Plane появился комментарий «📋 BRD/ТЗ/AC/TestPlan готовы. Прошу + review и реакцию :approved: для продвижения в Architecture.» (точный + текст совпадает с baseline до M-3); +- Telegram-уведомление `notify_approve_requested` отправлено + один раз. + +## AC-05 — Auto-flow analyst → has questions — без регрессии + +**Given** задача в `analysis`, analyst завершился, артефактов нет, +но есть `docs/work-items//01-questions.md` +**When** subprocess закончился +**Then**: +- `tasks.stage` остаётся `analysis`; +- в Plane выставлен state `needs_input`; +- в Plane комментарий начинается с «❓ Analyst нуждается в + уточнении:» и содержит текст questions-файла; +- Telegram-сообщение «❓ {wid}: Analyst задаёт вопросы. Ответь в Plane.» + отправлено. + +## AC-06 — Manual approve `analysis → architecture` + +**Given** задача в `analysis`, в Plane на любом комментарии стоит +реакция `:approved:` +**When** webhook получил эту реакцию +**Then**: +- `tasks.stage` становится `architecture`; +- агент `architect` запущен (есть запись в `agent_runs` с + `agent='architect'` и тем же `task_id`); +- `plane_notify_stage(work_item_id, "analysis", "architecture")` вызван + ровно один раз с теми же аргументами, что и в baseline. + +## AC-07 — Reviewer REQUEST_CHANGES (auto-flow) — rollback + relaunch + +**Given** задача в `review`, reviewer завершился с +`exit_code=0`, QG `check_reviewer_verdict` возвращает +`(False, "REQUEST_CHANGES: ...")`, в `agent_runs` ранее было 0 +запусков developer на этом task_id +**When** subprocess reviewer завершён +**Then**: +- `tasks.stage` становится `development`; +- agent `developer` запущен с `task_desc`, содержащим строку + `Note: REQUEST_CHANGES from reviewer (attempt 1/3).`; +- `notify_stage_change(task_id, "review", "development")` вызван; +- `plane_notify_stage(wid, "review", "development")` вызван. + +## AC-08 — Reviewer REQUEST_CHANGES — max retries + +**Given** задача в `review`, reviewer завершился, QG False с +REQUEST_CHANGES, в `agent_runs` уже 3 запуска developer на task_id +**When** subprocess reviewer завершён +**Then**: +- `tasks.stage` НЕ становится `development` (или становится — см. + Q-3, но developer **не перезапускается**); +- Telegram-сообщение «⚠️ {wid}: Max developer retries (3) reached. + Manual intervention needed.» отправлено; +- в логе `error: max retries reached`. + +## AC-09 — Tester FAIL — rollback + relaunch + +**Given** задача в `testing`, tester завершился, QG +`check_tests_passed` возвращает `(False, "")`, retry_count +developer = 1 +**When** subprocess tester завершён +**Then**: +- `tasks.stage` становится `development`; +- в Plane state — `in_progress`; +- комментарий в Plane: «❌ Тесты не прошли: . Developer + перезапущен для фикса.»; +- developer запущен с `task_desc`, содержащим + `Note: Tests FAILED. Fix failures described in + docs/work-items//13-test-report.md`. + +## AC-10 — Architect conflict — rollback в analysis + +**Given** задача в `architecture`, architect завершился, QG +`check_architecture_done` False, файл +`docs/work-items//10-conflict.md` существует +**When** subprocess architect завершён +**Then**: +- `tasks.stage` становится `analysis`; +- в Plane state — `in_progress`; +- в Plane комментарий начинается с «⚠️ Architect нашёл конфликт с + ТЗ. Возврат в Analysis.» и содержит первые 500 символов конфликта; +- analyst запущен с `Note: Architect conflict. Revise TRZ.` в + `task_desc`. + +## AC-11 — Manual approve в стадии `review` — путь через +`check_review_approved` + +**Given** задача в `review`, PR-ветка `feature/...` имеет открытый PR +в Gitea, в PR — approval reviewer'а; в Plane стоит `:approved:` +**When** webhook получил реакцию +**Then**: +- `find_pr_by_branch(repo, branch)` вернула PR number; +- `check_review_approved(repo, pr_number)` вернула `(True, ...)`; +- `tasks.stage` становится `testing`; +- agent `tester` запущен. + +## AC-12 — Manual approve в `review`, PR не найден, есть +`12-review.md` + +**Given** задача в `review`, в Gitea PR на этой ветке нет +(закрыт/удалён), но есть файл +`docs/work-items//12-review.md`, в Plane `:approved:` +**When** webhook получил реакцию +**Then**: +- `tasks.stage` становится `testing`; +- agent `tester` запущен. + +## AC-13 — Manual approve в `review`, нет ни PR, ни файла + +**Given** задача в `review`, PR нет, файлов +`12-review.md`/`09-review.md` нет, `:approved:` стоит +**When** webhook +**Then**: +- `tasks.stage` остаётся `review`; +- `notify_qg_failure(task_id, "review", "check_review_approved", + "No open PR found and no review file")` вызван; +- `plane_notify_qg(wid, "review", "check_review_approved", + "No open PR found and no review file")` вызван. + +## AC-14 — Терминальная стадия — noop + +**Given** задача в стадии `done` +**When** по какой-то причине вызвана `StageEngine.advance` +**Then**: +- `tasks.stage` не меняется; +- никаких уведомлений не отправляется; +- `advance` возвращает `AdvanceResult(outcome="noop", + reason="terminal stage", ...)`. + +## AC-15 — Идемпотентность параллельных вызовов + +**Given** одновременно поступают два события на один и тот же task: +завершился agent (AGENT_FINISHED) и пришла реакция `:approved:` +(MANUAL_APPROVE) +**When** оба вызывают `StageEngine.advance` +**Then**: +- стадия в БД продвигается **ровно один раз** (это гарантируется тем, + что `advance` повторно читает `tasks.stage` из БД и сверяет с + ожидаемым `next_stage`); +- не возникает двойного запуска одного и того же агента + (в `agent_runs` не появляются две записи с одинаковым `agent` и + разницей < 1 секунды); +- допустим вариант: один из вызовов получит `outcome="noop"` с + `reason="stage already advanced"`. + +## AC-16 — Регресс-тесты launcher и webhooks проходят + +**Given** ветка ET-012 +**When** `pytest tests/test_launcher.py tests/test_webhooks.py` +**Then** все ранее существовавшие тестовые кейсы проходят (за +исключением тех, что были адаптированы под новый импорт — изменения +свыше переименования модулей не допускаются). + +## AC-17 — Покрытие unit-тестами + +**Given** ветка ET-012 +**When** `pytest --cov=src/stage_engine --cov=src/stage_hooks` +**Then** line-coverage обоих модулей ≥ 90%. + +## AC-18 — Линт и типизация + +**Given** ветка ET-012 +**When** `ruff check src/` и `mypy src/stage_engine.py +src/stage_hooks.py` +**Then** оба запускаются без ошибок (warnings допустимы только в +существующих модулях, не в новых). + +## AC-19 — Текстовые snapshot-тесты для уведомлений + +**Given** ветка ET-012 +**When** `pytest tests/test_notifications_snapshot.py` +**Then** для каждого сценария AC-04..AC-13 текст +`plane_add_comment`/`send_telegram`/`notify_*` совпадает с baseline +(snapshot, зафиксированный по версии перед рефакторингом). + +## AC-20 — Документация + +**Given** ветка ET-012 +**When** инспекция docstrings +**Then**: +- `StageEngine`, `StageEngine.advance`, `HookOutcome`, + `AdvanceContext` имеют docstrings, описывающие назначение и + контракт каждого поля; +- модуль `stage_engine.py` имеет module-level docstring со ссылкой + на ET-012 BRD и ТЗ.