From 96a99a09b7ac741365ce43619935b02e746fb2b3 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Tue, 9 Jun 2026 20:06:31 +0300 Subject: [PATCH] analyst(ET): auto-commit from analyst run_id=496 --- docs/work-items/ORCH-090/01-brd.md | 165 +++++++++++++++ docs/work-items/ORCH-090/02-trz.md | 191 ++++++++++++++++++ .../ORCH-090/03-acceptance-criteria.md | 146 +++++++++++++ docs/work-items/ORCH-090/04-test-plan.yaml | 107 ++++++++++ 4 files changed, 609 insertions(+) create mode 100644 docs/work-items/ORCH-090/01-brd.md create mode 100644 docs/work-items/ORCH-090/02-trz.md create mode 100644 docs/work-items/ORCH-090/03-acceptance-criteria.md create mode 100644 docs/work-items/ORCH-090/04-test-plan.yaml diff --git a/docs/work-items/ORCH-090/01-brd.md b/docs/work-items/ORCH-090/01-brd.md new file mode 100644 index 0000000..22497f0 --- /dev/null +++ b/docs/work-items/ORCH-090/01-brd.md @@ -0,0 +1,165 @@ +--- +work_item: ORCH-090 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-09 +model_used: claude-opus-4-8 +--- + +# 01 — BRD (бизнес-требования): ORCH-090 — Механизм отмены задачи: статус STOP в Plane (остановка + полный сброс) + +Work Item: **ORCH-090** · Repo: **orchestrator** · Стадия: analysis + +## 1. Бизнес-контекст и проблема + +Сегодня в оркестраторе **нет штатного способа отменить/остановить задачу**. Оператор вынужден +выполнять разрозненные ручные действия: убить процесс агента, дождаться исчерпания ретраев job, +удалить ветку/worktree/строку task из БД и вручную сбросить статус в Plane. Это медленно, +ошибкоопасно и не воспроизводимо (инцидент 09.06 с ORCH-087 — оператор делал всё это руками). + +Вторая, связанная проблема — **дыра ручного релонча**: `src/webhooks/plane.py::handle_status_start` +при ручном переводе задачи в рабочий статус (через «To Analyse» / In Progress) **повторно ставит в +очередь агента текущей стадии на той же ветке** (`has_active_job_for_task` → иначе +`enqueue_job(stage_agent, …)`). Это означает, что попытка оператора «подтолкнуть» задачу сменой +статуса может незаметно релончить агента — именно этот механизм усугубил сегодняшний инцидент. + +Требуется единый, декларативный механизм: **перевод задачи в новый Plane-статус STOP → +оркестратор немедленно останавливает всю работу по задаче и полностью сбрасывает её прогресс**. +Повторный запуск возможен ТОЛЬКО через «To Analyse» (с нуля). Никакой другой статус пайплайн не +запускает. + +**Установленные факты (по текущему коду, не изобретать):** +- Машина стадий — `src/stages.py::STAGE_TRANSITIONS`; терминальная стадия только `done` + (`cancelled`-стадии нет). +- Plane-маппинг — `src/plane_sync.py`: `_PLANE_NAME_TO_KEY` уже содержит `"Cancelled" → "cancelled"`, + `_DEFAULT_STATES` содержит UUID `cancelled`; имени «STOP» в маппинге сейчас нет. +- Остановка процесса агента уже реализована как graceful-каскад в + `src/agents/launcher.py::_watchdog` (SIGTERM → grace `agent_kill_grace_seconds` → SIGKILL); PID + задачи хранится в `jobs.pid`. +- Статусы job в `jobs` — `queued | running | done | failed`; статуса `cancelled` нет. +- Терминал-скип для реконсилятора/мониторов уже учитывает `done` и `cancelled` + (`src/reconciler.py::_is_terminal_state`, ORCH-068/086). +- Запуск пайплайна с нуля — `handle_status_start → start_pipeline` (создаёт ветку + docs + analyst). + +## 2. Объём (scope) + +### В объёме +- Новый Plane-статус **STOP** как сигнал отмены задачи (распознавание в диспетчере статусов). +- **Остановка задачи (G1):** graceful-стоп активного агента (SIGTERM), отмена всех job'ов задачи + (queued/running → терминальный «cancelled»-исход), исчерпание ретраев (запрет авто-requeue), + снятие таймеров/мониторов (post-deploy monitor, brd-review clock и т.п.). +- **Полный сброс прогресса (G2):** удаление/архив рабочей ветки и worktree, очистка незавершённого + прогресса задачи в БД так, чтобы повторный старт шёл строго через `start_pipeline` (с нуля). + Docs-артефакты задачи — сохранить/забэкапить (не теряем аналитику). +- **Закрытие дыры релонча (G3/G4):** перевод в любой промежуточный рабочий статус + (Development/Architecture/Review/Deploying/…) вручную **не** запускает агента; единственный вход + к запуску пайплайна — «To Analyse» (старт с нуля). +- **Идемпотентность и fail-safe (G5):** STOP на уже остановленной/завершённой задаче — no-op; STOP + во время критичной операции (merge/deploy) — корректное прерывание без порчи `main`/прода. +- Kill-switch фичи; наблюдаемость (лог + Telegram + блок в `GET /queue`). +- Обновление документации (CLAUDE.md, architecture/README.md, CHANGELOG.md) и инфра-предусловие + (создать статус STOP на доске Plane). + +### Вне объёма +- Автоматическая отмена задач по таймауту/эвристике — STOP только по явному человеческому сигналу. +- Возобновление задачи «с середины» после STOP — сознательно НЕ поддерживается (только перезапуск + с нуля через To Analyse). +- Изменение семантики Rejected (откат на стадию назад) — STOP это отдельный путь, не Rejected. +- Изменение состава/семантики `STAGE_TRANSITIONS` exit-гейтов и `QG_CHECKS` / `check_*`. +- Откат уже задеплоенного в прод кода (rollback) — STOP не выполняет rollback; он лишь прерывает + незавершённую работу безопасно. +- Кросс-проектная отмена пакета задач (отменяется одна задача за сигнал). + +## 3. Заинтересованные стороны + +- **Заказчик / владелец продукта:** Слава (идея STOP-статуса). +- **Оператор оркестратора** (Стрим и др.) — главный потребитель: получает кнопку «отменить» вместо + ручной хирургии по БД/процессам. +- **Затрагиваемые проекты:** orchestrator (self-hosting) и enduro-trails (общая прод-БД/очередь) — + изменения должны быть аддитивны и не задевать enduro при выключенном/неприменимом флаге. +- **Принимает результат:** reviewer/tester по критериям приёмки (`03`/`04`). + +## 4. Бизнес-требования (BR) + +- **BR-1 (STOP останавливает работу)** — перевод задачи в Plane-статус STOP → оркестратор + останавливает всю работу по задаче: (a) активному агенту посылается SIGTERM (graceful, с + последующим жёстким kill по существующему grace-каскаду); (b) все job'ы задачи (queued и running) + переводятся в терминальный «отменённый» исход и не выбираются claim'ом; (c) ретраи исчерпываются + (никакого авто-requeue после STOP); (d) таймеры/мониторы задачи (post-deploy monitor, brd-review + clock, merge-lease defer и т.п.) снимаются. Контракт фичи — **never-raise**. +- **BR-2 (STOP = полный сброс)** — после STOP задача НЕ продолжается с середины. Рабочая + ветка+worktree удаляются/архивируются; незавершённый прогресс задачи в БД очищается или + помечается так, что повторный запуск идёт через `start_pipeline` с нуля (свежая ветка от + актуального `origin/main`, новый аналитик). Docs-артефакты (`01..17`) — сохранить/забэкапить. +- **BR-3 (единственный вход — To Analyse)** — единственный Plane-статус, запускающий пайплайн — + «To Analyse» (старт с нуля). После STOP повторный «To Analyse» создаёт задачу заново. +- **BR-4 (закрыть дыру релонча)** — ручной перевод задачи в любой промежуточный рабочий статус + (Architecture/Development/Review/Testing/Deploying/Awaiting Deploy/…) **не** запускает агента + соответствующей стадии. Текущее поведение `handle_status_start`, релончащее агента текущей стадии + на той же ветке, должно быть устранено/загейчено так, чтобы пайплайн стартовал только из + «To Analyse». +- **BR-5 (идемпотентность)** — STOP на задаче, которая уже остановлена (cancelled), уже `done` или + не существует, — **no-op** (без ошибок, без побочных эффектов, без повторного kill). +- **BR-6 (безопасное прерывание критичных операций)** — STOP во время merge/deploy не оставляет + `main` в half-merged состоянии и не роняет/не рестартит прод-контейнер. Если критичный шаг уже + необратимо запущен (детач-деплой/слияние в процессе), STOP не должен его «разорвать» с порчей — + допустимо дождаться/пропустить необратимый шаг и зафиксировать честный итог (детали безопасной + точки прерывания — архитектору). +- **BR-7 (STOP ≠ Rejected)** — STOP это полная остановка+сброс задачи, а не откат на предыдущую + стадию. Существующий путь Rejected (`handle_verdict(approved=False)` → `_rollback_stage`) не + меняется и не смешивается с STOP. +- **BR-8 (наблюдаемость)** — каждое срабатывание STOP прозрачно: лог, Telegram-уведомление (с + кликабельным номером задачи), Plane-коммент (best-effort), отражение в live-карточке и read-only + блок в `GET /queue`. + +## 5. Нефункциональные требования (NFR) + +- **NFR-1 (нулевая регрессия + kill-switch)** — фича под флагом включения (по образцу + `serial_gate_enabled`/`merge_gate_enabled`); при выключенном флаге поведение оркестратора строго + как сейчас. `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / семантика существующих статусов — без + изменений. +- **NFR-2 (общая прод-БД, аддитивность)** — любые изменения схемы БД — только аддитивные и + идемпотентные (`CREATE TABLE IF NOT EXISTS` / `_ensure_column`); enduro-trails не затрагивается. +- **NFR-3 (self-hosting safety)** — STOP не должен убить сам оркестратор / прод и не портить `main`. + Прерывание merge/deploy — fail-safe (не оставлять half-merge; не рестартить прод). +- **NFR-4 (restart-safe)** — состояние «задача отменена» durable (БД); после рестарта контейнера + отменённая задача не «оживает» и не релончится reconciler'ом/reaper'ом (переиспользовать + терминал-скип `done`/`cancelled`). +- **NFR-5 (never-raise)** — обработчик STOP и закрытие дыры релонча не должны валить вебхук-поток; + ошибка на единице работы логируется и не прерывает обработку других задач/проектов. +- **NFR-6 (offline-устойчивость горячего пути)** — закрытие дыры релонча и терминал-скип не должны + добавлять обязательных сетевых вызовов в горячий claim-цикл. + +## 6. Допущения и ограничения + +- На доске Plane проекта ORCH будет создан статус **STOP** (инфра-предусловие); до его создания + фича в режиме fail-safe (нет статуса → нет STOP-действия, ничего не ломается). +- Логический ключ `cancelled` и его UUID/группа уже присутствуют в `plane_sync` — STOP может + переиспользовать «cancelled»-семантику терминал-скипа (точное соответствие имя→ключ и + группа-`cancelled` — решение архитектора). +- Существующий graceful kill-каскад агента (`_watchdog`: SIGTERM→grace→SIGKILL) переиспользуется + для остановки активного агента; новый механизм kill не изобретается. +- Терминал-скип `done`/`cancelled` в `reconciler`/мониторах уже есть и должен покрыть + STOP-отменённые задачи (NFR-4) — переиспользовать, не дублировать. +- Архитектурные решения (хранилище статуса отмены, точка безопасного прерывания merge/deploy, + удаление vs архив ветки, точные точки врезки в `plane.py`) — зона архитектора (`06-adr/`). + +## 7. Критерии успеха + +STOP-статус, выставленный на задаче, приводит к: остановленному агенту, отменённым job'ам без +авто-requeue, снятым таймерам/мониторам, удалённой/заархивированной ветке+worktree, durable-статусу +«отменена» (переживает рестарт), сохранённым docs-артефактам. Ручной перевод в промежуточный +рабочий статус более не релончит агента; пайплайн стартует только из «To Analyse». STOP +идемпотентен и безопасен при merge/deploy. Детальные PASS/FAIL — в `03-acceptance-criteria.md`. + +## 8. Риски + +- Гонка «STOP во время merge/deploy» → риск half-merge/порчи `main` (mitigation: безопасная точка + прерывания, fail-safe — детали архитектору). +- Закрытие дыры релонча может задеть легитимный сценарий resume после «Needs Input» → нужно + сохранить намеренные сценарии возврата к работе, не ломая их (уточнить с архитектором, какой путь + заменяет релонч). +- Очистка прогресса в БД при общей прод-БД → риск задеть enduro/другие задачи (mitigation: + строго per-task, аддитивно). +- Детали — `10-tech-risks.md` (заполняет архитектор). diff --git a/docs/work-items/ORCH-090/02-trz.md b/docs/work-items/ORCH-090/02-trz.md new file mode 100644 index 0000000..b36f1b0 --- /dev/null +++ b/docs/work-items/ORCH-090/02-trz.md @@ -0,0 +1,191 @@ +--- +work_item: ORCH-090 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-09 +model_used: claude-opus-4-8 +--- + +# 02 — ТЗ (TRZ): ORCH-090 — Механизм отмены задачи: статус STOP в Plane (остановка + полный сброс) + +Work Item: **ORCH-090** · Repo: **orchestrator** · Стадия: analysis + +> ТЗ описывает **что** и **где** должно измениться (модули/контракты/артефакты), выведенное из BRD +> и фактического кода. **Как** (хранилище статуса отмены, точка безопасного прерывания merge/deploy, +> удаление vs архив ветки, точные точки врезки) — решает архитектор в `06-adr/`. ТЗ фиксирует +> требования и границы, не предлагает архитектурное решение. + +--- + +## 1. Сводка изменения + +Ввести обработку нового Plane-статуса **STOP** как сигнала отмены задачи. При его получении +оркестратор: (1) останавливает активного агента (graceful SIGTERM через существующий каскад), +(2) отменяет все job'ы задачи и исчерпывает ретраи, (3) снимает таймеры/мониторы, (4) удаляет/ +архивирует рабочую ветку+worktree и сбрасывает незавершённый прогресс в БД до состояния «отменена» +(durable), сохраняя docs-артефакты. Параллельно закрывается **дыра релонча**: ручной перевод в +промежуточный рабочий статус больше не запускает агента — единственный вход к запуску пайплайна +остаётся «To Analyse» (`start_pipeline`). Всё — аддитивно, под kill-switch, never-raise, +restart-safe. `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*` и семантика существующих статусов — +**не меняются**. + +--- + +## 2. Задействованные модули / пути + +| Путь | Действие | +|------|----------| +| `src/webhooks/plane.py` | изменить: добавить распознавание/маршрутизацию STOP (`handle_issue_updated`) → новый обработчик `handle_stop` (имя — на усмотрение архитектора); **загейтить/убрать релонч агента** в `handle_status_start` (промежуточные статусы не запускают агента; пайплайн — только из `To Analyse`/`start_pipeline`) | +| `src/agents/launcher.py` | изменить: предоставить/переиспользовать остановку активного процесса задачи (SIGTERM каскад `_watchdog`; `jobs.pid`), пометку «не релончить» (исчерпание `max_attempts`/запрет авто-requeue для отменённой задачи) | +| `src/queue_worker.py` / `src/db.py` | изменить: отмена job'ов задачи (queued/running → терминальный «cancelled»-исход); claim не выбирает отменённые; helper'ы выборки job'ов задачи; (возможно) новый терминальный статус job `cancelled` ИЛИ переиспользование `failed`+флаг — выбор архитектора; durable-пометка задачи «отменена» в `tasks` | +| `src/git_worktree.py` | изменить/переиспользовать: удаление/архив рабочей ветки и worktree отменённой задачи (`remove_worktree`; удаление/архив Gitea-ветки) — never-raise | +| `src/plane_sync.py` | изменить: маппинг Plane-статуса STOP (`_PLANE_NAME_TO_KEY` / `_DEFAULT_STATES`); переиспользовать группу `cancelled` для терминал-скипа; сеттер статуса (best-effort) | +| `src/stages.py` | при необходимости — терминальная трактовка отменённой задачи (НЕ менять exit-гейты рёбер; добавление `cancelled`-стадии — решение архитектора, см. §5) | +| `src/reconciler.py` | переиспользовать терминал-скип `done`/`cancelled` (`_is_terminal_state`) — отменённая задача не реконсилируется/не релончится | +| `src/job_reaper.py` | согласовать: reaper не «оживляет» отменённые job'ы (терминальный исход не requeue'ится) | +| `src/stage_engine.py` | согласовать: снятие таймеров/мониторов (post-deploy monitor, brd-review clock) и безопасное прерывание merge/deploy при STOP | +| `src/notifications.py` | переиспользовать `send_telegram`/`update_task_tracker` для алерта/карточки отмены (never-raise, кликабельный номер) | +| `src/config.py` | изменить: новый kill-switch `stop_status_enabled` (+ при необходимости область репо/доп-флаги) по образцу `serial_gate_enabled` | +| `src/main.py` | изменить: read-only блок наблюдаемости отмены в `GET /queue` (аддитивно) | +| `docs/architecture/README.md`, `CLAUDE.md`, `CHANGELOG.md` | обновить в том же PR (golden source) | +| `tests/` | добавить тесты (см. `04-test-plan.yaml`) | + +> Чистую логику распознавания/решения по STOP желательно вынести в leaf-модуль (по образцу +> `src/serial_gate.py` / `src/labels.py`, never-raise) — окончательно решает архитектор. + +--- + +## 3. Функциональные требования + +### FR-1 — Распознавание и маршрутизация STOP (BR-1, BR-5) +- `handle_issue_updated` (`webhooks/plane.py`) распознаёт перевод задачи в логический статус STOP + (через `_PLANE_NAME_TO_KEY`/группа `cancelled`) и маршрутизирует в обработчик отмены. +- Обработчик идемпотентен: если задача уже отменена / `done` / отсутствует → no-op (BR-5). +- Контракт — never-raise: ошибка обработки STOP логируется, вебхук-поток не падает (NFR-5). + +### FR-2 — Остановка активного агента (BR-1a) +- Для running-job'а задачи послать активному процессу SIGTERM (graceful) через существующий + каскад `launcher._watchdog` (SIGTERM → grace `agent_kill_grace_seconds` → SIGKILL); PID берётся + из `jobs.pid`. +- Если активного процесса нет (idle/queued) — шаг no-op. + +### FR-3 — Отмена job'ов и исчерпание ретраев (BR-1b, BR-1c) +- Все job'ы задачи (`status IN (queued, running)`) переводятся в **терминальный отменённый исход** + так, что `claim_next_job` их больше не выбирает и `_finalize_*`/reaper не делает авто-requeue. +- Запрет авто-requeue: после STOP `attempts` считаются исчерпанными (либо отдельный терминальный + статус job `cancelled`, либо `failed`+маркер — выбор архитектора). Reaper (`job_reaper.py`) и + `_finalize_permanent` не должны возвращать отменённый job в `queued`. + +### FR-4 — Снятие таймеров и мониторов (BR-1d) +- При STOP снимаются/обнуляются связанные с задачей таймеры и фоновые наблюдатели: post-deploy + monitor (ORCH-021), brd-review clock (ORCH-087), отложенные defer'ы merge-lease/serial-gate. +- Терминал-скип `done`/`cancelled` (`reconciler._is_terminal_state`, ORCH-068/086) применяется к + отменённой задаче, чтобы реконсилятор/мониторы её не трогали (NFR-4). + +### FR-5 — Полный сброс прогресса (BR-2) +- Рабочая ветка и worktree задачи удаляются/архивируются (`git_worktree.remove_worktree` + удаление/ + архив Gitea-ветки; never-raise). `main` не трогается, force-push в `main` запрещён. +- Незавершённый прогресс задачи в БД приводится к durable-состоянию «отменена» так, что повторный + запуск возможен ТОЛЬКО через `start_pipeline` с нуля (новая ветка от свежего `origin/main`, новый + analyst). Конкретика «очистить строку vs пометить cancelled» — архитектору; инвариант: + возобновления «с середины» не происходит. +- **Docs-артефакты задачи (`01..17`) сохраняются/бэкапятся** — не удаляются вместе с прогрессом. + +### FR-6 — Закрытие дыры релонча (BR-3, BR-4) +- `handle_status_start` (или эквивалентная точка) **не должен релончить агента текущей стадии** при + ручном переводе в промежуточный рабочий статус (Architecture/Development/Review/Testing/ + Deploying/Awaiting Deploy/Monitoring/…). +- Запуск пайплайна остаётся возможен **только** через статус «To Analyse» → `start_pipeline` + (создание ветки + docs + enqueue analyst). Любой намеренный сценарий «вернуть задачу в работу» + (например, после Needs Input) должен быть пересмотрен так, чтобы НЕ опираться на авто-релонч + агента сменой рабочего статуса (точный заменяющий механизм — архитектору). + +### FR-7 — Безопасное прерывание критичных операций (BR-6, NFR-3) +- STOP во время merge/deploy не оставляет `main` в half-merged состоянии и не рестартит/не роняет + прод-контейнер. Если необратимый шаг (detached self-deploy / слияние PR) уже запущен — STOP не + «разрывает» его с порчей: допускается дать необратимому шагу завершиться/зафиксировать честный + исход, после чего применить отмену. Точка безопасного прерывания и обработка merge-lease — ADR. + +### FR-8 — Наблюдаемость (BR-8) +- Каждое срабатывание STOP: `logger.info/warning` (что остановлено/сброшено), Telegram-алерт + (`send_telegram`, кликабельный номер `plane_issue_link`), Plane-коммент (best-effort), обновление + live-карточки (`update_task_tracker`, never-raise), read-only блок отмены в `GET /queue`. + +--- + +## 4. Изменения API + +- **Новых обязательных публичных endpoint'ов нет.** Триггер STOP — смена статуса Plane (webhook), + не REST. (По аналогии с ORCH-088 возможен опциональный админ-эндпоинт принудительной отмены — + на усмотрение архитектора; если вводится, описать в ADR и таблице API README.) +- `GET /queue` — **аддитивно**: новый read-only блок (например `stop`/`cancel`) — флаг `enabled`, + счётчик отменённых задач/job'ов, последние отмены. Существующие ключи не меняются; never-raise. +- Внешний контракт вебхука `POST /webhook/plane` — не меняется (новая ветка обработки статуса + внутри `handle_issue_updated`). + +--- + +## 5. Изменения схемы БД + +> Только **аддитивные, идемпотентные** миграции (общая прод-БД; enduro не трогать). +> `CREATE TABLE IF NOT EXISTS` / `_ensure_column`. + +- **Статус job «отменён» (FR-3):** требуется терминальный исход, который не requeue'ится. Варианты + (выбор — архитектор): новый статус `jobs.status='cancelled'` ИЛИ переиспользование `failed` + + аддитивный маркер. Требование к выбранному варианту: claim/finalize/reaper не возвращают его в + `queued`; restart-safe. +- **Состояние задачи «отменена» (FR-5, NFR-4):** durable-признак, что задача отменена и не + возобновляется с середины. Варианты: добавление терминальной стадии `cancelled` в `tasks.stage` + (учитывается терминал-скипом `done`/`cancelled`, уже поддержан reconciler'ом) ИЛИ аддитивная + колонка/таблица. `STAGE_TRANSITIONS` (exit-гейты рёбер) при этом **не меняются** — отмена это + терминальное состояние, не новое ребро конвейера. +- `QG_CHECKS`, `check_*`, `job_deps`, `agent_runs`-контракт, `repo_freeze` — **без изменений**. + +--- + +## 6. Требования к новым/изменённым QG checks + +- **Новых QG-проверок не вводить.** STOP — это решение диспетчера статусов/планировщика (отмена), + а не Quality Gate стадии. Реестр `QG_CHECKS` и `check_*` не меняются (по образцу `task_deps` + ORCH-026 и `serial_gate` ORCH-088 — логика в обработчике/claim, не новый QG). + +--- + +## 7. Совместимость / регресс + +- **Kill-switch:** новый флаг `stop_status_enabled` (env `ORCH_STOP_STATUS_ENABLED`) по образцу + `serial_gate_enabled`; `False` → STOP-обработка и закрытие дыры релонча ведут себя нейтрально + (поведение строго как сейчас, нулевая регрессия). При необходимости — область репо + (`stop_status_repos`, CSV) с дефолтом «все репо» (отмена осмысленна и для enduro). +- **Аддитивность БД (NFR-2):** только идемпотентные миграции; enduro при выключенном/неприменимом + флаге не затрагивается. +- **Инварианты (не нарушать):** `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_*`, exit-коды + deploy-хука, merge-gate (ORCH-043), merge-verify (ORCH-071/073), image-freshness (ORCH-058), + post-deploy контракт (ORCH-021), serial-gate (ORCH-088), auto-label (ORCH-089), семантика + Rejected/Approved/Confirm Deploy — **без изменений**. +- **Self-hosting safety (NFR-3):** STOP не рестартит/не роняет прод-контейнер; не push/force-push в + `main`; merge/deploy прерываются fail-safe (без half-merge). +- **never-raise (NFR-5):** обработчик STOP и закрытие релонча не валят вебхук-поток; ошибка на + единице работы изолирована. +- **Артефакты pipeline (создать/обновить в том же PR):** `docs/work-items/ORCH-090/06-adr/ADR-001-…` + (решение архитектора), `docs/architecture/README.md` (раздел «STOP / отмена задачи (ORCH-090)», + обновление описания `GET /queue`, раздела статусной модели и при новой таблице/колонке — раздела + «База данных»), `CLAUDE.md` (абзац о STOP в статусной модели), `CHANGELOG.md` (`feat:`); при новой + таблице/колонке — `docs/work-items/ORCH-090/08-data-requirements.md`; при админ-эндпоинте — таблица + API в README. + +--- + +## 8. Открытые вопросы для архитектора (не блокируют анализ) + +- OQ-1: Имя Plane-статуса — отдельный «STOP» (новый key) vs переиспользование существующего + «Cancelled» (key `cancelled` уже в `_PLANE_NAME_TO_KEY`). Влияет на маппинг и группу терминал-скипа. +- OQ-2: Статус отменённого job — новый `cancelled` vs `failed`+маркер. +- OQ-3: Состояние отменённой задачи — терминальная стадия `cancelled` vs аддитивная колонка/таблица. +- OQ-4: Сброс прогресса — удалить строку task (полный re-create через To Analyse) vs пометить + cancelled и при To Analyse создавать новую задачу. +- OQ-5: Удаление vs архив рабочей ветки (и Gitea-ветки) — что безопаснее для аудита. +- OQ-6: Точка безопасного прерывания merge/deploy (FR-7) и обработка удерживаемого merge-lease. +- OQ-7: Чем заменить легитимный «resume после Needs Input», который сейчас опирается на релонч в + `handle_status_start` (FR-6), чтобы не сломать намеренный сценарий возврата к работе. diff --git a/docs/work-items/ORCH-090/03-acceptance-criteria.md b/docs/work-items/ORCH-090/03-acceptance-criteria.md new file mode 100644 index 0000000..791a03b --- /dev/null +++ b/docs/work-items/ORCH-090/03-acceptance-criteria.md @@ -0,0 +1,146 @@ +--- +work_item: ORCH-090 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-09 +model_used: claude-opus-4-8 +--- + +# 03 — Критерии приёмки (Acceptance Criteria): ORCH-090 — Механизм отмены задачи: статус STOP в Plane + +Work Item: **ORCH-090** · Repo: **orchestrator** · Стадия: analysis + +Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL** +(что считается провалом). Любой машинный/ручной reviewer проверяет их буквально по файлам +репозитория. + +--- + +## AC-1 — STOP останавливает активного агента + +**Условие:** задача с running-job'ом переведена в Plane-статус STOP. +- **PASS:** активному процессу агента послан SIGTERM через существующий каскад + (`launcher._watchdog`: SIGTERM → grace → SIGKILL); по grace процесс завершён; `agent_runs`/`jobs` + отражают завершение. Тест демонстрирует вызов остановки по `jobs.pid`. +- **FAIL:** процесс агента продолжает работать после STOP, либо kill реализован новым «грязным» + механизмом мимо graceful-каскада, либо STOP падает с исключением. + +--- + +## AC-2 — Все job'ы задачи отменены без авто-requeue + +**Условие:** у задачи есть job'ы в `queued` и/или `running`; пришёл STOP. +- **PASS:** все job'ы задачи переведены в терминальный отменённый исход; `claim_next_job` их не + выбирает; `_finalize_permanent`/`job_reaper` не возвращают их в `queued` (ретраи исчерпаны). + Тест: после STOP claim не возвращает job задачи, reaper не requeue'ит. +- **FAIL:** хотя бы один job задачи остаётся claimable/возвращается в `queued` после STOP, либо + происходит авто-requeue. + +--- + +## AC-3 — Таймеры/мониторы сняты, отменённая задача не реконсилируется + +**Условие:** задача отменена через STOP. +- **PASS:** связанные таймеры/мониторы (post-deploy monitor, brd-review clock, defer'ы) не активны + для задачи; `reconciler` (`_is_terminal_state`, терминал-скип `done`/`cancelled`) и `job_reaper` + не трогают/не «оживляют» отменённую задачу. Тест: reconciler F-1 пропускает отменённую задачу. +- **FAIL:** монитор/таймер срабатывает по отменённой задаче, либо reconciler/reaper её + релончит/реанимирует. + +--- + +## AC-4 — Полный сброс: ветка/worktree удалены/архивированы, прогресс сброшен, docs сохранены + +**Условие:** задача отменена через STOP. +- **PASS:** рабочий worktree удалён (`remove_worktree`, never-raise), рабочая ветка удалена/ + заархивирована; `main` не тронут (force-push в `main` отсутствует); прогресс задачи в БД приведён + к durable-состоянию «отменена» (повторный запуск возможен только с нуля); docs-артефакты + (`docs/work-items/ORCH-090/01..17`) **сохранены/забэкаплены**, не удалены. +- **FAIL:** worktree/ветка остаются как «живой» прогресс, либо тронут `main`, либо docs-артефакты + удалены, либо задача способна продолжиться «с середины». + +--- + +## AC-5 — Единственный вход к запуску — To Analyse; дыра релонча закрыта + +**Условие:** существующая задача (с веткой/прогрессом) вручную переведена в промежуточный рабочий +статус (Architecture/Development/Review/Testing/Deploying/Awaiting Deploy/Monitoring). +- **PASS:** агент соответствующей стадии **не** запускается (нет `enqueue_job` стадийного агента по + факту ручной смены рабочего статуса). Запуск пайплайна происходит ТОЛЬКО при статусе «To Analyse» + (`start_pipeline`). Тест: перевод в Development не порождает job; перевод в To Analyse порождает + старт с нуля. +- **FAIL:** ручной перевод в любой промежуточный рабочий статус релончит агента текущей стадии + (текущее дырявое поведение `handle_status_start`). + +--- + +## AC-6 — Идемпотентность STOP + +**Условие:** STOP приходит на задачу, которая уже отменена / `done` / не существует. +- **PASS:** обработчик — no-op: нет повторного kill, нет повторного удаления ветки, нет ошибок, нет + Telegram-спама дублями. Тест: повторный STOP не меняет состояние и не бросает. +- **FAIL:** повторный STOP бросает исключение, повторно убивает/чистит, либо генерирует + дубль-уведомления. + +--- + +## AC-7 — Безопасное прерывание merge/deploy (self-hosting safety) + +**Условие:** STOP приходит во время merge/deploy задачи. +- **PASS:** `main` не остаётся в half-merged состоянии; прод-контейнер не рестартится/не роняется + обработчиком STOP; force-push в `main` отсутствует. Если необратимый шаг уже запущен — он не + «разрывается» с порчей (исход зафиксирован честно, затем применена отмена). Тест/обоснование + демонстрирует fail-safe точку прерывания. +- **FAIL:** после STOP `main` в неконсистентном состоянии, прод перезапущен/упал по вине STOP, либо + выполнен force-push в `main`. + +--- + +## AC-8 — Kill-switch и нулевая регрессия + +**Условие:** флаг `stop_status_enabled=False`. +- **PASS:** STOP-обработка не активна, дыра релонча в поведении не меняется относительно текущего + кода; `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` не изменены; полный `pytest tests/` зелёный; + enduro-trails не затронут. При `True` — STOP работает по AC-1…AC-7. +- **FAIL:** при выключенном флаге поведение отличается от текущего; изменены exit-гейты/реестр QG; + регресс существующих тестов. + +--- + +## AC-9 — Аддитивность БД и restart-safe + +**Условие:** изменения схемы БД и поведение после рестарта. +- **PASS:** все миграции аддитивны и идемпотентны (`CREATE TABLE IF NOT EXISTS`/`_ensure_column`); + после рестарта контейнера отменённая задача остаётся отменённой и не релончится. Тест: повторная + инициализация БД не падает; отменённая задача durable. +- **FAIL:** деструктивная/неидемпотентная миграция, изменение существующих таблиц-контрактов, либо + «оживание» отменённой задачи после рестарта. + +--- + +## AC-10 — Наблюдаемость STOP + +**Условие:** STOP применён к задаче. +- **PASS:** факт отмены залогирован; отправлен Telegram-алерт с кликабельным номером задачи; + Plane-коммент (best-effort); live-карточка обновлена (never-raise); `GET /queue` несёт read-only + блок отмены. Тест: блок присутствует в ответе `GET /queue`. +- **FAIL:** STOP не оставляет следов в логе/уведомлениях, либо `GET /queue` падает/не отражает + отмену. + +--- + +## Сводная матрица AC ↔ FR/BR + +| AC | Покрывает | +|----|-----------| +| AC-1 | BR-1 / FR-2 | +| AC-2 | BR-1 / FR-3 | +| AC-3 | BR-1 / FR-4 / NFR-4 | +| AC-4 | BR-2 / FR-5 | +| AC-5 | BR-3, BR-4 / FR-6 | +| AC-6 | BR-5 / FR-1 | +| AC-7 | BR-6 / FR-7 / NFR-3 | +| AC-8 | NFR-1 / FR-6 | +| AC-9 | NFR-2, NFR-4 / FR-3, FR-5 | +| AC-10 | BR-8 / FR-8 | diff --git a/docs/work-items/ORCH-090/04-test-plan.yaml b/docs/work-items/ORCH-090/04-test-plan.yaml new file mode 100644 index 0000000..29a4021 --- /dev/null +++ b/docs/work-items/ORCH-090/04-test-plan.yaml @@ -0,0 +1,107 @@ +work_item: ORCH-090 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-09 +model_used: claude-opus-4-8 +title: "STOP-статус: отмена задачи (остановка + полный сброс) и закрытие дыры релонча" +framework: pytest +scope: > + Покрывается: распознавание/маршрутизация STOP, остановка агента, отмена job'ов без авто-requeue, + снятие мониторов/терминал-скип, полный сброс ветки/worktree/прогресса при сохранении docs, + закрытие дыры релонча (только To Analyse стартует пайплайн), идемпотентность, kill-switch, + аддитивность БД/restart-safe, наблюдаемость (GET /queue, уведомления). + Вне покрытия: реальный прод-деплой/рестарт контейнера (self-hosting safety проверяется на уровне + «не вызывается рестарт/force-push», а не живым деплоем); кросс-проектная пакетная отмена. +notes: > + Полный регресс `pytest tests/` должен оставаться зелёным (NFR-1). Регрессом считается: изменение + STAGE_TRANSITIONS/QG_CHECKS/check_*, релонч агента ручной сменой рабочего статуса, авто-requeue + отменённого job, «оживание» отменённой задачи reconciler/reaper, любой push/force-push в main, + рестарт прод-контейнера обработчиком STOP. Тесты должны проходить и при stop_status_enabled=False + (нейтральное поведение). Использовать существующие фикстуры из tests/test_plane_webhook.py / + test_launcher.py / test_queue.py / test_reconciler.py. + +tests: + - id: TC-01 + type: unit + description: "STOP-статус распознаётся и маршрутизируется в обработчик отмены (handle_issue_updated); неизвестная/прочая задача -> no-op, never-raise." + module: tests/test_stop_status.py + expected: PASS + + - id: TC-02 + type: unit + description: "Остановка активного агента: при STOP по running-job посылается SIGTERM по jobs.pid через каскад _watchdog (SIGTERM->grace->SIGKILL); нет активного процесса -> no-op." + module: tests/test_stop_status.py + expected: PASS + + - id: TC-03 + type: unit + description: "Отмена job'ов: queued+running job'ы задачи переведены в терминальный отменённый исход; claim_next_job их не выбирает (AC-2)." + module: tests/test_stop_status.py + expected: PASS + + - id: TC-04 + type: unit + description: "Запрет авто-requeue: _finalize_permanent/job_reaper не возвращают отменённый job в queued (ретраи исчерпаны)." + module: tests/test_stop_status.py + expected: PASS + + - id: TC-05 + type: unit + description: "Полный сброс: при STOP вызывается remove_worktree и удаление/архив рабочей ветки; main не трогается; force-push в main отсутствует (AC-4, AC-7)." + module: tests/test_stop_status.py + expected: PASS + + - id: TC-06 + type: unit + description: "Docs-артефакты задачи (01..17) сохраняются/бэкапятся при сбросе прогресса, не удаляются (AC-4)." + module: tests/test_stop_status.py + expected: PASS + + - id: TC-07 + type: unit + description: "Идемпотентность: повторный STOP на уже отменённой / done / несуществующей задаче -> no-op (нет повторного kill/cleanup, нет исключений, нет дубль-уведомлений) (AC-6)." + module: tests/test_stop_status.py + expected: PASS + + - id: TC-08 + type: unit + description: "Kill-switch: при stop_status_enabled=False STOP-обработка нейтральна, поведение как сейчас; при True -> отмена выполняется (AC-8)." + module: tests/test_stop_status.py + expected: PASS + + - id: TC-09 + type: unit + description: "Наблюдаемость: GET /queue несёт read-only блок отмены; never-raise при ошибке построения блока (AC-10)." + module: tests/test_stop_status.py + expected: PASS + + - id: TC-10 + type: integration + description: "Закрытие дыры релонча: ручной перевод существующей задачи в Development/Architecture/Review/Testing НЕ порождает job стадийного агента (handle_status_start не релончит) (AC-5)." + module: tests/test_stop_status.py + expected: PASS + + - id: TC-11 + type: integration + description: "Единственный вход: перевод в To Analyse запускает start_pipeline (новая ветка от свежего origin/main + analyst) — единственный путь старта пайплайна (AC-5)." + module: tests/test_stop_status.py + expected: PASS + + - id: TC-12 + type: integration + description: "Терминал-скип/restart-safe: отменённая задача durable; reconciler F-1 и job_reaper её не реконсилируют/не оживляют (терминал-скип done/cancelled, _is_terminal_state) (AC-3, AC-9)." + module: tests/test_stop_status.py + expected: PASS + + - id: TC-13 + type: integration + description: "End-to-end STOP: задача со срезанной веткой и активным job -> STOP -> агент остановлен, job'ы отменены, ветка/worktree убраны, статус задачи durable 'отменена', уведомления отправлены (AC-1..AC-4, AC-10)." + module: tests/test_stop_status.py + expected: PASS + + - id: TC-14 + type: unit + description: "Аддитивность БД: миграция нового терминального исхода job/состояния задачи идемпотентна (повторная init_db не падает); существующие таблицы-контракты не изменены (AC-9, NFR-2)." + module: tests/test_stop_status.py + expected: PASS