From ab083ba826ff1741ddc1deff987f2e2eca4767b3 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Tue, 9 Jun 2026 20:37:21 +0300 Subject: [PATCH] architect(ET): auto-commit from architect run_id=497 --- docs/architecture/README.md | 39 +++ .../adr/adr-0026-stop-cancel-task.md | 106 +++++++ docs/architecture/internals.md | 8 +- .../06-adr/ADR-001-stop-cancel-task.md | 294 ++++++++++++++++++ .../ORCH-090/07-infra-requirements.md | 51 +++ .../ORCH-090/08-data-requirements.md | 70 +++++ docs/work-items/ORCH-090/10-tech-risks.md | 42 +++ 7 files changed, 609 insertions(+), 1 deletion(-) create mode 100644 docs/architecture/adr/adr-0026-stop-cancel-task.md create mode 100644 docs/work-items/ORCH-090/06-adr/ADR-001-stop-cancel-task.md create mode 100644 docs/work-items/ORCH-090/07-infra-requirements.md create mode 100644 docs/work-items/ORCH-090/08-data-requirements.md create mode 100644 docs/work-items/ORCH-090/10-tech-risks.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 31ffe17..8d89675 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -278,6 +278,45 @@ Phase A ждёт ручного `Confirm Deploy`, ORCH-059). ORCH-089 снима `docs/work-items/ORCH-089/06-adr/ADR-001-auto-label-gates.md`, `docs/work-items/ORCH-089/07-infra-requirements.md`. +### STOP / отмена задачи: терминал `cancelled` + закрытие дыры релонча (ORCH-090 — design) + +До ORCH-090 не было штатного способа отменить задачу (ручная хирургия по БД/процессам) и +существовала **дыра релонча**: `handle_status_start` при существующей задаче без активного job +безусловно релончил агента текущей стадии на той же ветке. ORCH-090 вводит Plane-статус **STOP** +как единый декларативный сигнал отмены: остановка агента + **полный сброс** прогресса. Аддитивно, +под kill-switch, never-raise, restart-safe; `STAGE_TRANSITIONS` (exit-гейты) / `QG_CHECKS` / +`check_*` — **без изменений**. +- **Новое системное терминальное состояние `cancelled`** (adr-0026) — `tasks.stage='cancelled'` + + `jobs.status='cancelled'`, равноправное `done`. Предикат «задача незавершена» расширяется + `stage != 'done'` → `stage NOT IN ('done','cancelled')` в `serial_gate` (ORCH-088) и `task_deps` + (ORCH-026), приводя их в соответствие с уже существующим терминал-скипом реконсилятора + (`stage in ("done","cancelled")`, ORCH-086 D2). Иначе отменённая задача заклинила бы очередь репо. +- **Распознавание (fail-closed):** новый ключ `stop` в `_PLANE_NAME_TO_KEY` (`"STOP" → "stop"`); + **не** в `_DEFAULT_STATES` (по образцу `confirm_deploy`/ORCH-059) → нет статуса = нет отмены, без + `KeyError`. `handle_issue_updated` маршрутизирует `stop` → новый `handle_stop` → + `stage_engine.cancel_task`. +- **Каскад отмены:** graceful SIGTERM активному агенту (переиспользование каскада + `launcher._watchdog` по `jobs.pid`); `cancel_jobs_for_task` (queued/running → `cancelled`, + не реквью'ятся); снятие таймеров/мониторов (brd-clock, post-deploy monitor, defer'ы); + `remove_worktree` + never-raise удаление **только feature-ветки** Gitea (`main` неприкосновенен, + без force-push); **тумбстон** `plane_id`/`work_item_id` (`#cancelled-`) → повторный + «To Analyse» создаёт задачу с нуля; docs-артефакты (`01..17`) сохраняются. +- **Безопасное прерывание merge/deploy:** STOP в критическом окне (self-deploy `INITIATED`-sentinel + ORCH-036, держание merge-lease ORCH-043/071) → **отложенная отмена** (durable + `cancel_requested_at`, отмена только `queued`-job'ов, алерт); необратимый шаг доводится до + честного исхода; `main`/прод-контейнер не трогаются (NFR-3). +- **Закрытие дыры релонча:** relaunch в `handle_status_start` ограничен стадией `analysis` + (единственный владелец Needs-Input, ORCH-066) — тихий релонч середины пайплайна на старой ветке + устранён; единственный вход к запуску — «To Analyse» (`start_pipeline`). +- **Флаги/наблюдаемость:** kill-switch `stop_status_enabled` + `stop_status_repos` (CSV, пусто → + все репо); leaf `src/cancel.py` (never-raise); read-only блок `stop` в `GET /queue`; лог + + Telegram (кликабельный номер) + Plane-коммент + live-карточка. При выключенном флаге — нулевая + регрессия (enduro не затронут). + +Подробнее: [adr-0026](adr/adr-0026-stop-cancel-task.md), детально — +`docs/work-items/ORCH-090/06-adr/ADR-001-stop-cancel-task.md`, +`docs/work-items/ORCH-090/08-data-requirements.md`. + ### Исполняемый самодеплой стадии `deploy` (ORCH-36) `deploy` перестаёт быть «бумажной»: для self-hosting (`is_self_hosting_repo`) стадия РЕАЛЬНО деплоит прод (8500) через хост-хук `scripts/orchestrator-deploy-hook.sh`, diff --git a/docs/architecture/adr/adr-0026-stop-cancel-task.md b/docs/architecture/adr/adr-0026-stop-cancel-task.md new file mode 100644 index 0000000..e3b8b80 --- /dev/null +++ b/docs/architecture/adr/adr-0026-stop-cancel-task.md @@ -0,0 +1,106 @@ +--- +work_item: ORCH-090 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-09 +model_used: claude-opus-4-8 +--- + +# ADR-0026: Системное терминальное состояние `cancelled` — STOP-отмена задачи + +Сквозной (cross-cutting) ADR. Детальное решение задачи — +`docs/work-items/ORCH-090/06-adr/ADR-001-stop-cancel-task.md`. + +## Статус +Proposed + +## Контекст + +ORCH-090 вводит Plane-статус **STOP** — единый декларативный механизм отмены задачи (остановка +агента + полный сброс прогресса). Самое́ кросс-каттинговое следствие — появление **нового +системного терминального состояния `cancelled`** (стадия `tasks.stage='cancelled'` + терминальный +job-статус `jobs.status='cancelled'`). До ORCH-090 «терминальность задачи» в горячем планировщике +была захардкожена как **`stage == 'done'`** (единственный сток в `STAGE_TRANSITIONS`), и это +определение разъехалось между подсистемами: + +- `src/reconciler.py` **уже** трактует `stage in ("done","cancelled")` как терминал-скип + (ORCH-086 D2 предвосхитил `cancelled`; стр. 196) и `_is_terminal_state` по группе Plane + `{completed, cancelled}` (ORCH-068, стр. 398–415). +- `src/serial_gate.py` (ORCH-088) и `src/task_deps.py` (ORCH-026) считают задачу «незавершённой» + по `stage != 'done'` — **без** `cancelled`. Если ввести `cancelled`-стадию, не тронув их, + отменённая задача навсегда будет «активной»/«незавершённой зависимостью» и **заклинит очередь + репо**. + +Этот ADR фиксирует `cancelled` как первоклассное терминальное состояние, равноправное `done`, и +перечисляет ВСЕ точки, где системный предикат терминальности должен его признавать. + +## Решение + +### Инвариант +**«Задача терминальна» ⇔ `stage ∈ {done, cancelled}`.** Это единое определение для всех +подсистем планировщика/мониторинга. `cancelled` — терминальный **сток** (не новое ребро +конвейера): exit-гейты рёбер `STAGE_TRANSITIONS` и реестр `QG_CHECKS`/`check_*` **не меняются**. + +### Точки, признающие `cancelled` терминальным (исчерпывающе) +1. `src/stages.py::STAGE_TRANSITIONS` — добавить сток + `"cancelled": {"next": None, "agent": None, "qg": None}` (параллельно `done`). +2. `src/serial_gate.py` — `repo_has_other_unfinished` и claim-фрагмент `t2.stage != 'done'`, + snapshot: `stage != 'done'` → `stage NOT IN ('done','cancelled')`. **(маркер ORCH-088)** +3. `src/task_deps.py` — dep-gate и `is_task_ready`: `stage != 'done'` → + `stage NOT IN ('done','cancelled')`. **(маркер ORCH-026)** +4. `src/reconciler.py` — уже покрыто скипом `stage in ("done","cancelled")` (стр. 196); + `get_active_tasks_for_reconcile` опционально сузить до `NOT IN ('done','cancelled')`. +5. `src/job_reaper.py` / `src/queue_worker.py` — перед авто-requeue dead/running-job'а сверять + терминал задачи: `stage in ("done","cancelled")` → job помечается `cancelled`, не реквью'ится. +6. `src/post_deploy.py` / `stage_engine.run_post_deploy_monitor` — монитор не тикает по + отменённой задаче (терминал-проверка/маркер `done`). + +### Новые терминальные исходы +- **Job:** `jobs.status='cancelled'` — нигде не реквью'ится; `claim_next_job` выбирает только + `status='queued'` (изменений в claim нет). `mark_job` стампит `finished_at` для `cancelled`. +- **Задача:** `tasks.stage='cancelled'` + аддитивные колонки `cancelled_at`, + `cancel_requested_at` (отложенная отмена в критическом окне merge/deploy). Натуральные ключи + `plane_id`/`work_item_id` тумбстонятся (`#cancelled-`) для переиспользования «To Analyse» + с нуля; `plane_issue_id` сохраняется (аудит). Детали — 08-data-requirements.md. + +### Точки врезки STOP (компоненты) +- `plane.py` — маршрут `stop` (fail-closed, не в `_DEFAULT_STATES`) → `handle_stop`; гейт релонча + ограничен стадией `analysis`. +- `stage_engine.cancel_task` — оркестрация отмены (graceful SIGTERM, cancel-jobs, worktree+branch, + tombstone, notify); безопасное прерывание merge/deploy (D7 локального ADR). +- leaf `src/cancel.py` — чистая логика (`applies`/`in_critical_window`/`snapshot`), never-raise. +- `src/gitea.py` — `delete_remote_branch` (never-raise; только feature-ветка, `main` неприкосновенен). +- `GET /queue` — read-only блок `stop`. + +### Флаги / совместимость +- Kill-switch `stop_status_enabled` + scope `stop_status_repos` (CSV, пусто → все репо). +- При `stop_status_enabled=False`: STOP-обработка и гейт релонча инертны; расширение + терминал-набора `cancelled` безвредно при отсутствии отменённых задач → **нулевая регрессия**. +- `STAGE_TRANSITIONS` (exit-гейты) / `QG_CHECKS` / `check_*` / семантика + Approved/Rejected/Confirm Deploy / merge-gate (ORCH-043) / merge-verify (ORCH-071/073) / + image-freshness (ORCH-058) / post-deploy (ORCH-021) / serial-gate FIFO (ORCH-088) / auto-label + (ORCH-089) — **без изменений**. +- Миграции БД — только аддитивные/идемпотентные (`_ensure_column`); enduro не затронут (NFR-2). + +## Последствия +- **+** Единое, консистентное определение терминальности — устранён латентный рассинхрон + `done`-only между планировщиком и реконсилятором. +- **+** STOP безопасен для self-hosting: не трогает `main`/прод, отложенная отмена в критическом + окне. +- **−** Терминальность теперь читается из набора `{done, cancelled}`, а не из скаляра `'done'` — + будущие подсистемы обязаны использовать набор. Митигейшн: этот ADR + маркер `ORCH-090` в + изменённых местах + тесты. +- **Откат:** `stop_status_enabled=False`; полный revert — снять врезки и вернуть предикаты к + `stage != 'done'`. + +## Эволюция маркеров `cancelled`-терминала +Места, признающие `cancelled` терминальным (см. список выше), несут маркер `ORCH-090`. Правка +любого из них — сверяться с этим ADR (анти-археология: 3+ маркеров → одна ссылка сюда, +TRACEABILITY.md). + +## Ссылки +- Детальный ADR: `docs/work-items/ORCH-090/06-adr/ADR-001-stop-cancel-task.md` +- Data: `docs/work-items/ORCH-090/08-data-requirements.md` +- Связанные: adr-0017 (serial-gate), adr-0015 (task-deps), adr-0007 (self-deploy), + adr-0006 (merge-gate), adr-0018 (auto-label) diff --git a/docs/architecture/internals.md b/docs/architecture/internals.md index 270a254..82980d7 100644 --- a/docs/architecture/internals.md +++ b/docs/architecture/internals.md @@ -61,9 +61,15 @@ STAGE_TRANSITIONS = { testing: → deploy-staging (agent: deployer, QG: check_tests_passed) deploy-staging: → deploy (agent: deployer, QG: check_staging_status) deploy: → done (agent: None, QG: None) + cancelled: → None (agent: None, QG: None) # ORCH-090: терминал-сток отмены } ``` +**Терминальные стоки (ORCH-090):** `done` и `cancelled` — равноправные терминальные состояния +(`{"next": None, "agent": None, "qg": None}`). `cancelled` — это **не новое ребро** (exit-гейты +рёбер не меняются), а терминал STOP-отмены. Системный предикат «задача завершена» — +`stage ∈ {done, cancelled}` (синхронно в `reconciler`/`serial_gate`/`task_deps`; adr-0026). + ### 3. Quality Gates (`src/qg/checks.py`) | Check | Метод проверки | @@ -329,7 +335,7 @@ webhook (plane/gitea) background thread (queue_worker) | Колонка | Назначение | |--------|------------| -| `status` | `queued` → `running` → `done` \| `failed` | +| `status` | `queued` → `running` → `done` \| `failed` \| `cancelled` (ORCH-090: терминальный исход STOP-отмены, не реквью'ится) | | `attempts` / `max_attempts` | счётчик попыток (инкремент при claim) / лимит ретраев (default 2) | | `run_id` | FK на `agent_runs.id` после старта | | `pid` | (ORCH-065) pid агентского процесса (`proc.pid` из `_spawn`); liveness-сигнал для job-reaper. Добавляется `_ensure_column` (idempotent) | diff --git a/docs/work-items/ORCH-090/06-adr/ADR-001-stop-cancel-task.md b/docs/work-items/ORCH-090/06-adr/ADR-001-stop-cancel-task.md new file mode 100644 index 0000000..071ffc0 --- /dev/null +++ b/docs/work-items/ORCH-090/06-adr/ADR-001-stop-cancel-task.md @@ -0,0 +1,294 @@ +--- +work_item: ORCH-090 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-09 +model_used: claude-opus-4-8 +--- + +# ADR-001: Механизм отмены задачи — Plane-статус STOP (остановка + полный сброс) + +Work Item: **ORCH-090** — единый декларативный механизм отмены/сброса задачи через +Plane-статус STOP. +Стадия: **architecture** +Сквозная регистрация: **`docs/architecture/adr/adr-0026-stop-cancel-task.md`** (решение +кросс-каттинговое — вводит системное терминальное состояние `cancelled`, затрагивающее +планировщик, реконсилятор, serial-gate, task-deps, мониторы). + +## Статус +Proposed + +--- + +## Контекст + +Сегодня в оркестраторе **нет штатного способа отменить/остановить задачу** (BRD §1). Оператор +делает ручную хирургию: убивает процесс агента, ждёт исчерпания ретраев job, чистит +ветку/worktree/строку `tasks` и сбрасывает статус Plane. Медленно, ошибкоопасно, +невоспроизводимо (инцидент 09.06 с ORCH-087). + +Вторая, связанная проблема — **дыра релонча**. Сверено по коду +`src/webhooks/plane.py::handle_status_start` (строки 215–306): при существующей задаче без +активного job функция **безусловно релончит агента текущей стадии** на той же ветке +(`has_active_job_for_task(task_id)` → иначе `enqueue_job(stage_agent, …)`, где +`stage_agent = STAGE_AUTHORS.get(current_stage)`). Этот путь задуман для «аналитик ответил на +Needs Input», но релончит агента **любой** стадии — именно он усугубил инцидент. + +**Факты, сверенные по коду (не изобретать):** +- Машина стадий — `src/stages.py::STAGE_TRANSITIONS`; `done` — терминальный сток + (`{"next": None, "agent": None, "qg": None}`, строка 21). `cancelled`-стадии нет. +- Plane-маппинг — `src/plane_sync.py`: `_PLANE_NAME_TO_KEY` уже содержит + `"Cancelled" → "cancelled"` (стр. 141); `_DEFAULT_STATES` содержит UUID `cancelled` (стр. 102); + имени «STOP» в маппинге нет. Маршрутизация статуса — `handle_issue_updated` (стр. 129–173), + сравнивает `new_state` с per-project UUID из `get_project_states(project_id)`; `to_analyse → + handle_status_start`, `confirm_deploy → handle_confirm_deploy`, `approved/rejected → + handle_verdict`; всё прочее → `else` (no-op). +- Остановка процесса агента уже есть — graceful-каскад `launcher._watchdog` + (SIGTERM → grace `agent_kill_grace_seconds` → SIGKILL, стр. 661–718); PID задачи стампится в + `jobs.pid` (`_spawn`, стр. 607–614). +- Статусы job в `jobs` — `queued | running | done | failed` (`src/db.py`, стр. 56–72); claim + выбирает только `status='queued'` (`claim_next_job`, стр. 586–651). Реквью на dead-running — + `job_reaper._reap_unknown_outcome` (`attempts/.deploy-state-//` + (ORCH-036); +- задача держит merge-lease `/.merge-lease-.json` / merge в процессе (ORCH-043/071). + +**Вне критического окна** — полный сброс немедленно (D2–D4, D8). +**Внутри критического окна** — отложенная отмена: ставится durable-метка +`tasks.cancel_requested_at` (аддитивная колонка), отменяются **только `queued`** job'ы (не +running-актор деплоя/мержа), шлётся алерт «STOP отложен до завершения критичного шага». +Детерминированный finalizer (`run_deploy_finalizer` Phase C / `_handle_merge_verify`) **доводит +необратимый шаг до честного исхода** и на терминальном `advance_stage` сверяется с +`cancel_requested_at`: задача переводится в `cancelled` с очисткой (worktree/ветка; код, уже +влитый в `main`, **не откатывается** — rollback вне объёма, BRD §2). Если шаг достиг `done` — +STOP фиксируется как «no-op после завершения» (честно: код уже в проде). Так AC-7 выполняется без +порчи `main`/прода. + +### D8 — Полный сброс ветки/worktree, сохранение docs (FR-5, BR-2, AC-4) + +- `git_worktree.remove_worktree(repo, branch)` — снять worktree (never-raise, уже есть). +- **Удалить удалённую feature-ветку** через новый never-raise хелпер + `gitea.delete_remote_branch(repo, branch)` (Gitea `DELETE /repos/{owner}/{repo}/branches/{branch}`). + Удаляется **только** ветка задачи; `main` — никогда; force-push отсутствует. Выбор «удалить» (не + архив): ветку легко восстановить из Gitea, а аналитику хранят docs — минимум новой Gitea-логики + (OQ-5). +- **Docs-артефакты (`01..17`) сохраняются** — не удаляются. На диске они в `docs/work-items/ORCH-090/` + (merge'ятся отдельным PR); cancel их не трогает. (Бэкап = они уже в `origin/main`/ветке docs.) + +### D9 — Флаги, leaf-модуль, наблюдаемость (FR-8, BR-8, NFR-1, AC-10) + +- `src/config.py`: `stop_status_enabled: bool = True` (env `ORCH_STOP_STATUS_ENABLED`, + kill-switch) + `stop_status_repos: str = ""` (CSV; **пусто → все репо**, отмена осмысленна и для + enduro; токены санитайзятся `^[A-Za-z0-9._-]+$`) — по образцу `serial_gate_*`. +- Leaf `src/cancel.py` (never-raise, импортирует только `config`/`db`, лениво `plane_sync`): чистая + логика — `applies(repo)`, `in_critical_window(task)`, `snapshot()`. Оркестрация + (SIGTERM/cancel-jobs/worktree/branch/tombstone/notify) — `stage_engine.cancel_task` (там уже есть + доступ к launcher/db/notifications/plane_sync). +- Наблюдаемость: `logger.info/warning`, Telegram-алерт (`send_telegram`, кликабельный + `plane_issue_link`), Plane-коммент (best-effort), `update_task_tracker` (never-raise), + read-only блок `stop` в `GET /queue` (`cancel.snapshot()`: `enabled`/`repos`/счётчик + `stage='cancelled'`/последние отмены). Существующие ключи `/queue` не меняются. + +--- + +## Альтернативы + +- **Переиспользовать существующий статус «Cancelled» (key `cancelled`) вместо нового «STOP»** — + отвергнуто: владелец продукта явно хочет операторскую кнопку «STOP», отличную от встроенного + Plane-«Cancelled» (которым наблюдатели могут пользоваться иначе). Терминал-семантику группы + `cancelled` мы при этом переиспользуем (D1, D5). +- **Job-статус `failed`+маркер вместо нового `cancelled`** (OQ-2) — отвергнуто: `failed` + семантически реквью-абелен (reaper/worker путь `attempts` детерминирован и парсится для аудита; `plane_issue_id` нетронут. +- **Откат:** `stop_status_enabled=False` отключает обработку STOP, гейт релонча и + freeze-неотносимые ветки; аддитивные колонки (`cancelled_at`/`cancel_requested_at`) и расширение + терминал-набора инертны при отсутствии отменённых задач. Полный revert — снять врезки в + `plane.py`/`stage_engine.py`/`serial_gate.py`/`task_deps.py`/`stages.py`, leaf `cancel.py`, флаги. + +--- + +## Ссылки +- BRD: `docs/work-items/ORCH-090/01-brd.md` +- TRZ: `docs/work-items/ORCH-090/02-trz.md` +- Acceptance: `docs/work-items/ORCH-090/03-acceptance-criteria.md` +- Data: `docs/work-items/ORCH-090/08-data-requirements.md` +- Infra: `docs/work-items/ORCH-090/07-infra-requirements.md` +- Риски: `docs/work-items/ORCH-090/10-tech-risks.md` +- Сквозной ADR: `docs/architecture/adr/adr-0026-stop-cancel-task.md` +- Сверено по коду: `src/webhooks/plane.py`, `src/plane_sync.py`, `src/db.py`, + `src/queue_worker.py`, `src/agents/launcher.py`, `src/reconciler.py`, `src/job_reaper.py`, + `src/serial_gate.py`, `src/task_deps.py`, `src/stages.py`, `src/git_worktree.py`, + `src/post_deploy.py`, `src/main.py` +- Маркеры (сверено перед изменением, TRACEABILITY.md): ORCH-088 (`serial_gate`), ORCH-026 + (`task_deps`), ORCH-086/068 (терминал-скип reconciler), ORCH-036/059 (self-deploy phases), + ORCH-043/071 (merge-gate/merge-verify), ORCH-021 (post-deploy), ORCH-087 (brd-clock) diff --git a/docs/work-items/ORCH-090/07-infra-requirements.md b/docs/work-items/ORCH-090/07-infra-requirements.md new file mode 100644 index 0000000..6a2d686 --- /dev/null +++ b/docs/work-items/ORCH-090/07-infra-requirements.md @@ -0,0 +1,51 @@ +--- +work_item: ORCH-090 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-09 +model_used: claude-opus-4-8 +--- + +# 07 — Инфра-требования: ORCH-090 — Механизм отмены задачи (STOP) + +Work Item: **ORCH-090** · Repo: **orchestrator** · Стадия: architecture + +## I-1. Топология / окружения + +Без изменения топологии. Тот же прод-контейнер `orchestrator` (8500) и staging (8501), та же +общая SQLite-БД и очередь. STOP — обработка вебхука внутри существующего сервиса; новых +контейнеров/портов/томов/сетей нет. + +**Инфра-предусловие (обязательно):** на доске Plane проекта ORCH создать статус **«STOP»** с +**группой `cancelled`** (а не `started`/`unstarted`). Группа `cancelled` обеспечивает нативный +терминал-скип реконсилятора (`_is_terminal_state`, ORCH-068/086) без доп-кода. До создания +статуса фича в fail-safe: `get_project_states(...).get("stop")` → `None` → ветка STOP не +активируется (нет `KeyError`, ничего не ломается). После создания — сбросить кэш состояний +(`reload_project_states`) или дождаться TTL `ORCH_PLANE_STATES_TTL_S` (дефолт 300с). + +> Для enduro-trails статус STOP **не** обязателен: `stop` отсутствует в `_DEFAULT_STATES` +> (fail-closed), отмена для enduro станет доступна только при создании статуса на их доске. + +## I-2. Переменные окружения / секреты + +Новые env (в `.env.example`, аддитивно; секретов нет): +- `ORCH_STOP_STATUS_ENABLED` — kill-switch фичи (дефолт `true`). +- `ORCH_STOP_STATUS_REPOS` — CSV области репо (дефолт пусто → все репо). + +Существующие переиспользуются: `ORCH_AGENT_KILL_GRACE_SECONDS` (graceful kill), Gitea-токен +(`delete_remote_branch`), Telegram-токен (алерт). Новых секретов нет. + +## I-3. Деплой / рестарт + +Прод-деплой орка — обязательно через staging-гейт (8501) перед `deploy` (self-hosting инвариант, +INFRA.md). STOP-обработчик сам **никогда** не рестартит/не роняет прод-контейнер и не трогает +`main` (NFR-3): при STOP во время self-deploy критичный detached-шаг не прерывается — отмена +откладывается до его честного завершения (ADR-001 D7). Раскат — поэтапно через `stop_status_repos` +при необходимости; дефолт «все репо». + +## I-4. CI/CD + +Без изменений `.gitea/workflows/`. Добавляются только pytest-тесты (`tests/`, см. +`04-test-plan.yaml`): STOP-каскад, запрет авто-requeue, терминал-скип, закрытие дыры релонча, +kill-switch, аддитивность миграций. diff --git a/docs/work-items/ORCH-090/08-data-requirements.md b/docs/work-items/ORCH-090/08-data-requirements.md new file mode 100644 index 0000000..e765708 --- /dev/null +++ b/docs/work-items/ORCH-090/08-data-requirements.md @@ -0,0 +1,70 @@ +--- +work_item: ORCH-090 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-09 +model_used: claude-opus-4-8 +--- + +# 08 — Требования к данным: ORCH-090 — Механизм отмены задачи (STOP) + +Work Item: **ORCH-090** · Repo: **orchestrator** · Стадия: architecture + +> Общая прод-БД (orchestrator + enduro). Все изменения — **только аддитивные и идемпотентные** +> (`_ensure_column`); существующие таблицы-контракты не переопределяются (NFR-2, AC-9). + +## Изменения схемы БД + +### Таблица `tasks` — аддитивные колонки (через `_ensure_column`) +| Колонка | Тип | Назначение | +|---------|-----|------------| +| `cancelled_at` | `TEXT` | durable-метка времени отмены (аудит/наблюдаемость). NULL для неотменённых. | +| `cancel_requested_at` | `TEXT` | durable-метка «отмена запрошена, но отложена» (STOP в критическом окне merge/deploy, ADR-001 D7). Снимается при доведении отмены до конца. | + +Никаких `ALTER` существующих колонок. `init_db` идемпотентен (повторный вызов — no-op). + +### Без DDL-изменений (расширение допустимых значений TEXT) +- **`jobs.status`** — добавляется значение `cancelled` к набору `queued|running|done|failed`. + Колонка уже `TEXT`; DDL не меняется. `claim_next_job` выбирает только `status='queued'` → + `cancelled` исключён нативно. +- **`tasks.stage`** — добавляется терминальное значение `cancelled` (сток, параллельно `done`). + Колонка уже `TEXT DEFAULT 'created'`; DDL не меняется. `STAGE_TRANSITIONS` exit-гейты рёбер + **не меняются** — `cancelled` это терминальное состояние, не новое ребро. + +### Без изменений +`job_deps`, `agent_runs`, `repo_freeze`, `tracker_messages`, индексы — контракты нетронуты. +`QG_CHECKS` / `check_*` — без изменений. + +## Новые/изменённые сущности + +### Тумбстон натуральных ключей отменённой задачи (ADR-001 D4) +На cancel выполняется UPDATE отменённой строки `tasks`: +- `plane_id := plane_id || '#cancelled-' || id` +- `work_item_id := work_item_id || '#cancelled-' || id` +- `stage := 'cancelled'`, `cancelled_at := datetime('now')` +- `plane_issue_id` — **сохраняется нетронутым** (аудит-связь с issue Plane). + +Цель: освободить натуральные ключи, чтобы повторный «To Analyse» создал свежую задачу +(`get_task_by_plane_id(plane_id)` → `None`; anti-dup `create_task_atomic` / +`ensure_unique_work_item_id` не коллизируют), сохранив строку для аудита. Формат суффикса +`#cancelled-` детерминирован и парсится. + +### Отмена job'ов (ADR-001 D3) +`cancel_jobs_for_task(task_id)` — guarded UPDATE +`SET status='cancelled', finished_at=datetime('now') WHERE task_id=? AND status IN ('queued','running')`. +Терминальный исход, нигде не реквью'ящийся. + +## Совместимость данных / миграции + +- **Аддитивность/идемпотентность:** только `_ensure_column` (no-op если колонка есть) и + расширение наборов TEXT-значений; деструктивных/несовместимых миграций нет (AC-9). Повторная + `init_db` после рестарта не падает. +- **Restart-safe (NFR-4):** durable терминал = `tasks.stage='cancelled'` (уже понимается + терминал-скипом реконсилятора, стр. 196). После рестарта `requeue_running_jobs` флипает только + `running` → отменённые job'ы (`cancelled`) не оживают; отменённая задача не реконсилируется. +- **Влияние на общую прод-БД:** изменения строго per-task; enduro не затрагивается, при + `stop_status_enabled=False` или отсутствии отменённых задач — поведение БД 1:1 как сейчас. +- **Кросс-каттинг (adr-0026):** предикат «задача незавершена» в `serial_gate`/`task_deps` + расширяется `stage != 'done'` → `stage NOT IN ('done','cancelled')`, иначе отменённая задача + заклинит очередь репо. Чтение БД (offline hot-path) не приобретает новых сетевых вызовов (NFR-6). diff --git a/docs/work-items/ORCH-090/10-tech-risks.md b/docs/work-items/ORCH-090/10-tech-risks.md new file mode 100644 index 0000000..b2bd8cb --- /dev/null +++ b/docs/work-items/ORCH-090/10-tech-risks.md @@ -0,0 +1,42 @@ +--- +work_item: ORCH-090 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-09 +model_used: claude-opus-4-8 +--- + +# 10 — Технические риски: ORCH-090 — Механизм отмены задачи (STOP) + +Work Item: **ORCH-090** · Repo: **orchestrator** · Стадия: architecture + +## Реестр рисков + +| ID | Риск | Вер. | Влия. | Митигейшн | +|----|------|------|-------|-----------| +| TR-1 | **Отменённая задача клинит очередь репо** — `serial_gate`/`task_deps` считают `cancelled` «незавершённой» (`stage != 'done'`) → serial-gate блокирует репо, dep-gate вечно держит зависимые. | Выс. | Выс. | Расширить предикат до `stage NOT IN ('done','cancelled')` во ВСЕХ точках (adr-0026, исчерпывающий список). Тест: после STOP другая задача репо стартует; зависимая разблокируется. | +| TR-2 | **Гонка reaper/worker реквью** — SIGTERM послан, job ещё `running`, reaper видит dead-pid → `attempts` на cancel (D4); `plane_issue_id` сохранён. Тест: после STOP «To Analyse» создаёт свежую задачу без коллизии. | +| TR-5 | **Очистка прогресса в общей прод-БД задевает enduro/другие задачи.** | Низ. | Выс. | Все операции строго per-`task_id`; тумбстон/cancel-jobs гардятся `WHERE task_id=?`; аддитивные миграции; при `stop_status_enabled=False` — инертно. Тест: enduro-строки не тронуты. | +| TR-6 | **Закрытие дыры релонча ломает легитимный resume аналитика после Needs Input.** | Сред. | Сред. | Relaunch ограничивается стадией `analysis` (единственный владелец Needs-Input, ORCH-066), а не блокируется целиком (D6). Тест: To Analyse на `analysis` релончит аналитика; на середине пайплайна — no-op. | +| TR-7 | **STOP на «Cancelled»-группе без явного статуса STOP** — fail-closed `stop` не в `_DEFAULT_STATES` может удивить (на доске нет статуса → отмены нет). | Низ. | Низ. | Документировано как fail-safe (07-infra); инфра-предусловие — создать статус STOP (группа `cancelled`). Наблюдаемость: блок `stop` в `/queue` показывает `enabled`/`repos`. | +| TR-8 | **Дубль-уведомления / повторный kill при повторном STOP.** | Низ. | Низ. | Идемпотентность (BR-5/D1): `stage in ("done","cancelled")` → no-op до любых действий. Тест: повторный STOP не меняет состояние и не шлёт дубль. | +| TR-9 | **`delete_remote_branch` падает / ветка уже удалена / Gitea недоступна.** | Низ. | Низ. | never-raise хелпер: ошибка/404 логируется, отмена продолжается; worktree снимается локально независимо; `main` не трогается. | +| TR-10 | **Удаление feature-ветки теряет код, не влитый в `main`.** | Низ. | Сред. | По замыслу: STOP = сброс незавершённого прогресса (BRD §2). docs-артефакты (`01..17`) сохраняются; ветку можно восстановить в Gitea. Влитый в `main` код не откатывается (rollback вне объёма). | + +## Сводный вывод + +Доминирующий класс — **консистентность системного терминал-набора** (TR-1, TR-2): введение +`cancelled` как первоклассного терминала обязывает синхронно обновить ВСЕ предикаты «задача +завершена», иначе латентный клин очереди. Это покрыто исчерпывающим списком в adr-0026 и +маркером `ORCH-090`. Второй класс — **self-hosting safety при STOP во время merge/deploy** (TR-3), +покрыт отложенной отменой (D7) с жёсткими запретами (`main`/прод/force-push/kill detached). + +**Эскалация:** решение вводит **новое системное терминальное состояние `cancelled`** (новая +стадия-сток + новый job-статус + сквозное изменение предиката терминальности) → классифицируется +как `arch:major-change`. Возврат в анализ **не требуется** — ТЗ полно, OQ-1…OQ-7 разрешены в +ADR-001; реализация аддитивна, под kill-switch, с нулевой регрессией при выключенном флаге. +Остаточный риск для прод-конвейера (self-hosting) — **низкий** при условии полного покрытия +тестами TR-1/TR-2/TR-3 и обязательного staging-гейта перед прод-деплоем.