architect(ET): auto-commit from architect run_id=497
This commit is contained in:
@@ -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-<id>`) → повторный
|
||||
«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`,
|
||||
|
||||
106
docs/architecture/adr/adr-0026-stop-cancel-task.md
Normal file
106
docs/architecture/adr/adr-0026-stop-cancel-task.md
Normal file
@@ -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-<id>`) для переиспользования «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)
|
||||
@@ -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) |
|
||||
|
||||
294
docs/work-items/ORCH-090/06-adr/ADR-001-stop-cancel-task.md
Normal file
294
docs/work-items/ORCH-090/06-adr/ADR-001-stop-cancel-task.md
Normal file
@@ -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<max → queued`, иначе `failed`, стр. 315–334).
|
||||
- **Терминал-скип уже учитывает `cancelled`:** `reconciler._is_terminal_state` (group
|
||||
`completed`/`cancelled` или логический ключ `cancelled`, стр. 398–415) и F-1 пропускает
|
||||
`stage in ("done","cancelled")` ДО любой работы (стр. 196, ORCH-086 D2 — `cancelled`-стадия
|
||||
**уже предвосхищена**).
|
||||
- **Но** «незавершённость» задачи в горячем планировщике определена как `stage != 'done'`
|
||||
(БЕЗ `cancelled`) в `src/serial_gate.py` (стр. 115, 120, 270, 334) и `src/task_deps.py`
|
||||
(`stage != 'done'`). Новая терминальная стадия `cancelled`, не распознанная здесь, **заклинит
|
||||
очередь** репо (serial-gate сочтёт отменённую задачу «активной»; task-deps — «незавершённой
|
||||
зависимостью»).
|
||||
- `remove_worktree(repo, branch)` — never-raise локальная очистка (`src/git_worktree.py`,
|
||||
стр. 98–107); функции удаления Gitea-ветки **нет**.
|
||||
- Запуск с нуля — `handle_status_start → start_pipeline` (ветка + docs + analyst, стр. 430–626);
|
||||
`create_task_atomic` с anti-dup по `plane_id`; uniqueness-guard по `work_item_id`
|
||||
(`ensure_unique_work_item_id`).
|
||||
|
||||
Требуется единый, декларативный, обратимый, аддитивный механизм под kill-switch, never-raise,
|
||||
restart-safe; `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*` — без изменений (TRZ §1, NFR-1).
|
||||
|
||||
---
|
||||
|
||||
## Решение
|
||||
|
||||
### Сводка
|
||||
|
||||
Ввести **STOP** как сигнал отмены задачи: новый логический Plane-ключ `stop` (fail-closed, по
|
||||
образцу `confirm_deploy`/ORCH-059), маршрутизируемый в новый обработчик `handle_stop`. Обработчик
|
||||
переводит задачу в **новое системное терминальное состояние `cancelled`** (стадия + durable),
|
||||
останавливая активного агента существующим graceful-каскадом, отменяя все job'ы новым
|
||||
терминальным исходом `jobs.status='cancelled'`, снимая таймеры/мониторы, удаляя рабочую
|
||||
ветку+worktree (docs сохраняются) и **тумбстоня** натуральные ключи (`plane_id`/`work_item_id`),
|
||||
чтобы повторный «To Analyse» создал задачу с нуля. Параллельно закрывается дыра релонча:
|
||||
relaunch в `handle_status_start` ограничивается единственным легитимным владельцем Needs-Input —
|
||||
стадией `analysis`. Чистая логика — leaf `src/cancel.py` (never-raise); оркестрация —
|
||||
`stage_engine.cancel_task`. Всё под флагом `stop_status_enabled`.
|
||||
|
||||
**Ключевой кросс-каттинг (см. adr-0026):** системный предикат «задача терминальна» расширяется с
|
||||
`{done}` до `{done, cancelled}` в трёх горячих местах планировщика (serial-gate, task-deps,
|
||||
`stages.py`-сток), приводя их в соответствие с уже существующим терминал-скипом реконсилятора.
|
||||
|
||||
### D1 — Распознавание и маршрутизация STOP (FR-1, BR-1, BR-5)
|
||||
|
||||
- В `_PLANE_NAME_TO_KEY` добавить `"STOP" → "stop"`. **В `_DEFAULT_STATES` ключ `stop` НЕ
|
||||
добавляется** — fail-closed по образцу ORCH-059: нет UUID-фолбэка для enduro/API-сбоя →
|
||||
`get_project_states(...).get("stop")` вернёт `None` → ветка просто не активируется (нет
|
||||
`KeyError`, нет слепой отмены). Инфра-предусловие — создать статус STOP на доске ORCH с
|
||||
**группой `cancelled`** (07-infra-requirements.md), чтобы терминал-скип по группе работал
|
||||
нативно.
|
||||
- `handle_issue_updated`: добавить ветку `stop_state = proj_states.get("stop")` →
|
||||
`elif stop_state and new_state == stop_state: await handle_stop(data, project_id)`. Ставится
|
||||
**до** `to_analyse`/`approved`/`rejected`, чтобы жесты не алиасили.
|
||||
- `handle_stop` (новый, в `plane.py`): резолвит задачу по `get_task_by_plane_id`; делегирует в
|
||||
`stage_engine.cancel_task(task_id, …)`. Гард kill-switch + repo-scope через `cancel.applies(repo)`.
|
||||
- **Идемпотентность (BR-5):** если задача отсутствует / уже `stage in ("done","cancelled")` →
|
||||
no-op (без повторного kill/удаления/уведомления). Контракт — never-raise (NFR-5): ошибка
|
||||
логируется, вебхук-поток не падает.
|
||||
|
||||
### D2 — Остановка активного агента (FR-2, BR-1a)
|
||||
|
||||
Переиспользовать существующий graceful-каскад, **не изобретать новый kill**. Для running-job'а
|
||||
задачи взять `jobs.pid` и послать `SIGTERM` через путь `launcher._watchdog`
|
||||
(SIGTERM → grace `agent_kill_grace_seconds` → SIGKILL). Вынести из `_watchdog` переиспользуемый
|
||||
хелпер `launcher.stop_process(pid, run_id)` (тот же каскад + `_record_kill`), вызываемый и из
|
||||
cancel-пути. Нет активного процесса (idle/queued) → шаг no-op. **Никогда** не убивать
|
||||
detached-процесс self-deploy (см. D7).
|
||||
|
||||
### D3 — Отмена job'ов и запрет авто-requeue (FR-3, BR-1b/1c)
|
||||
|
||||
- Новый **терминальный** статус job `jobs.status='cancelled'`. Схема не меняется (`status` — TEXT);
|
||||
расширяется лишь набор допустимых значений → `queued|running|done|failed|cancelled`.
|
||||
- Хелпер `db.cancel_jobs_for_task(task_id)` — guarded UPDATE
|
||||
`SET status='cancelled', finished_at=datetime('now') WHERE task_id=? AND status IN ('queued','running')`.
|
||||
`mark_job` расширяется: `cancelled` тоже стампит `finished_at` (в наборе с `done|failed`).
|
||||
- **claim не трогается:** `claim_next_job` выбирает только `status='queued'` → `cancelled`
|
||||
исключён нативно (NFR-6 — без новых JOIN'ов в горячий путь).
|
||||
- **Запрет авто-requeue (race-safe):** `job_reaper._reap_unknown_outcome` и launch-error requeue
|
||||
в `queue_worker` ПЕРЕД реквью читают терминальное состояние задачи; `stage in
|
||||
("done","cancelled")` → job помечается `cancelled` (терминал), **без** возврата в `queued`.
|
||||
Это закрывает гонку «SIGTERM послан, job ещё `running`, reaper видит dead-pid → реквью».
|
||||
Источник истины «не оживлять» — **стадия задачи `cancelled`**, а не статус job.
|
||||
|
||||
### D4 — Durable терминал задачи + переиспользование ключей (FR-5, NFR-4, BR-2/BR-3)
|
||||
|
||||
- Durable-состояние = **`tasks.stage='cancelled'`**. Это уже понимается терминал-скипом
|
||||
реконсилятора (стр. 196) → **ноль нового кода** для NFR-4; после рестарта отменённая задача не
|
||||
оживает (`requeue_running_jobs` флипает только `running`, а job'ы — `cancelled`).
|
||||
- Аддитивная колонка `tasks.cancelled_at TEXT` (через `_ensure_column`) — durable-метка
|
||||
времени для аудита/наблюдаемости.
|
||||
- **Переиспользование натуральных ключей (BR-3):** чтобы повторный «To Analyse» создал задачу с
|
||||
нуля, на cancel выполняется **тумбстон** ключей отменённой строки:
|
||||
`plane_id := plane_id || '#cancelled-' || id`, `work_item_id := work_item_id || '#cancelled-' || id`.
|
||||
Тогда `get_task_by_plane_id(plane_id)` вернёт `None` → `start_pipeline` создаст свежую задачу
|
||||
(новая ветка от актуального `origin/main`, новый analyst); anti-dup `create_task_atomic` и
|
||||
`ensure_unique_work_item_id` не коллизируют. Поле `plane_issue_id` **сохраняется** нетронутым —
|
||||
аудит-связь с issue Plane не теряется. Строка `tasks` **не удаляется** (история + durable
|
||||
терминал).
|
||||
- **Возобновления «с середины» нет** — единственный вход к запуску остаётся `start_pipeline`
|
||||
через «To Analyse» (D6).
|
||||
|
||||
### D5 — Системное терминальное состояние `cancelled` (кросс-каттинг — adr-0026)
|
||||
|
||||
Расширить предикат «задача терминальна/завершена» с `{done}` до `{done, cancelled}` там, где он
|
||||
сейчас захардкожен как `stage != 'done'`, **приводя планировщик в соответствие с уже
|
||||
существующим терминал-скипом реконсилятора** (стр. 196, `{done, cancelled}`):
|
||||
- `src/serial_gate.py` — `repo_has_other_unfinished` (стр. 115/120), claim-фрагмент
|
||||
`t2.stage != 'done'` (стр. 270), snapshot (стр. 334): `stage != 'done'` →
|
||||
`stage NOT IN ('done','cancelled')`. Иначе отменённая задача навсегда заблокирует репо.
|
||||
**Маркер ORCH-088** — сверено по `src/serial_gate.py` и
|
||||
`docs/work-items/ORCH-088/06-adr/ADR-001-serial-gate.md`: инвариант serial-gate — «не входить в
|
||||
analysis, пока есть **более ранняя незавершённая** задача»; «незавершённость» определяется
|
||||
стадией, и расширение терминал-набора `cancelled` лишь harmonизирует определение, не меняя
|
||||
FIFO-семантику (`t2.id < jobs.task_id`).
|
||||
- `src/task_deps.py` — dep-gate `t.stage != 'done'` и `is_task_ready`: `NOT IN ('done','cancelled')`.
|
||||
Иначе отменённая зависимость заблокирует зависимые задачи навсегда. **Маркер ORCH-026** —
|
||||
сверено: зависимость считается «готовой», когда предшественник терминален; `cancelled` —
|
||||
терминальный исход, поэтому его включение в готовность корректно.
|
||||
- `src/stages.py::STAGE_TRANSITIONS` — добавить терминальный **сток**
|
||||
`"cancelled": {"next": None, "agent": None, "qg": None}` (параллельно `done`, стр. 21). Это
|
||||
**не новое ребро** — ни одно exit-гейт ребра не меняется (TRZ §5, NFR-1); сток лишь делает
|
||||
`get_next_stage('cancelled')` корректным (None).
|
||||
- `src/reconciler.py::get_active_tasks_for_reconcile` (фильтр `stage != 'done'`) опционально
|
||||
сузить до `NOT IN ('done','cancelled')` (микро-оптимизация; функционально уже покрыто скипом
|
||||
стр. 196).
|
||||
|
||||
### D6 — Закрытие дыры релонча (FR-6, BR-3/BR-4, OQ-7)
|
||||
|
||||
Маршрутизация уже игнорирует промежуточные статусы (Architecture/Development/… → `else`, no-op),
|
||||
поэтому реальная дыра — relaunch внутри `handle_status_start` при `To Analyse` на существующей
|
||||
задаче. Решение: **ограничить relaunch единственным легитимным владельцем Needs-Input —
|
||||
стадией `analysis`** (единственный, кто ставит Needs Input, ORCH-066). Конкретно: ветку
|
||||
«existing task + idle agent → `enqueue_job(stage_agent,…)`» загейтить условием
|
||||
`current_stage == 'analysis'`. Для существующей задачи любой иной стадии «To Analyse» → **no-op**
|
||||
(лог + best-effort Plane-коммент «для перезапуска с нуля: STOP → To Analyse»). Это сохраняет
|
||||
легитимный «аналитик ответил на вопросы», но устраняет тихий релонч середины пайплайна на старой
|
||||
ветке (инцидент ORCH-087). Гейт — под `stop_status_enabled` (AC-8: флаг off → поведение 1:1 как
|
||||
сейчас).
|
||||
|
||||
### D7 — Безопасное прерывание merge/deploy (FR-7, BR-6, NFR-3, AC-7)
|
||||
|
||||
STOP **никогда** не трогает `main`, не делает force-push, не рестартит/не роняет прод-контейнер и
|
||||
**не SIGKILL'ит** detached-процесс self-deploy. `cancel_task` классифицирует «критическое окно» по
|
||||
существующим маркерам (чистая функция `cancel.in_critical_window(task)`, never-raise):
|
||||
- self-deploy Phase B запущен — sentinel `INITIATED` в `<repos_dir>/.deploy-state-<repo>/<wi>/`
|
||||
(ORCH-036);
|
||||
- задача держит merge-lease `<repos_dir>/.merge-lease-<repo>.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<max → queued`); отдельный
|
||||
терминальный `cancelled`, нигде не реквью'ящийся, самодокументируем и безопаснее.
|
||||
- **Удалять строку `tasks` целиком** (OQ-4) — отвергнуто: теряется durable-аудит и durable
|
||||
терминал в БД; тумбстон ключей (D4) даёт переиспользование с нуля, сохраняя строку и аудит.
|
||||
- **Архивировать ветку (rename `archive/…`)** вместо удаления (OQ-5) — отвергнуто: лишняя
|
||||
Gitea-логика; удаление обратимо в Gitea, аналитику хранят docs.
|
||||
- **Прерывать merge/deploy жёстко (kill detached)** (OQ-6) — отвергнуто: риск half-merge/порчи
|
||||
`main`/прода (NFR-3); отложенная отмена (D7) безопаснее.
|
||||
- **Полностью блокировать «To Analyse» на существующей задаче** (D6) — отвергнуто: сломает
|
||||
легитимный resume аналитика после Needs Input; ограничение релонча стадией `analysis` точечнее.
|
||||
|
||||
---
|
||||
|
||||
## Последствия
|
||||
|
||||
- **+** Оператор получает декларативную кнопку «отменить+сбросить» вместо ручной хирургии;
|
||||
воспроизводимо, наблюдаемо, обратимо (kill-switch).
|
||||
- **+** Дыра релонча закрыта; тихий релонч середины пайплайна на старой ветке исключён.
|
||||
- **+** Терминал-набор планировщика приведён в соответствие с реконсилятором (`{done,cancelled}`)
|
||||
— устранён латентный рассинхрон ORCH-086.
|
||||
- **+** Аддитивно/идемпотентно; при `stop_status_enabled=False` — нулевая регрессия; enduro не
|
||||
затронут.
|
||||
- **−** Вводится **системная терминальная стадия `cancelled`** — затрагивает несколько горячих
|
||||
предикатов (serial-gate/task-deps/stages). Митигейшн: исчерпывающий список точек в adr-0026 +
|
||||
тесты на «отменённая задача не клинит очередь / не реквью'ится / переживает рестарт».
|
||||
- **−** Отложенная отмена в критическом окне (D7) — не мгновенная. Митигейшн: прозрачный алерт
|
||||
«STOP отложен»; необратимый шаг доводится до честного исхода; код в `main` не откатывается (в
|
||||
объёме BRD — STOP ≠ rollback).
|
||||
- **−** Тумбстон `work_item_id` меняет значение колонки на отменённой строке. Митигейшн: формат
|
||||
суффикса `#cancelled-<id>` детерминирован и парсится для аудита; `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)
|
||||
51
docs/work-items/ORCH-090/07-infra-requirements.md
Normal file
51
docs/work-items/ORCH-090/07-infra-requirements.md
Normal file
@@ -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, аддитивность миграций.
|
||||
70
docs/work-items/ORCH-090/08-data-requirements.md
Normal file
70
docs/work-items/ORCH-090/08-data-requirements.md
Normal file
@@ -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-<id>` детерминирован и парсится.
|
||||
|
||||
### Отмена 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).
|
||||
42
docs/work-items/ORCH-090/10-tech-risks.md
Normal file
42
docs/work-items/ORCH-090/10-tech-risks.md
Normal file
@@ -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<max → queued` (авто-requeue отменённой задачи). | Сред. | Выс. | Источник истины «не оживлять» — `tasks.stage='cancelled'`. reaper/worker ПЕРЕД реквью сверяют терминал задачи → помечают job `cancelled`, не реквью'ят. Тест: reaper не возвращает job отменённой задачи в `queued`. |
|
||||
| TR-3 | **STOP во время merge/deploy → half-merge / порча `main` / рестарт прода.** | Низ. | Крит. | D7: критическое окно (`INITIATED`-sentinel self-deploy, держание merge-lease) → отложенная отмена; необратимый шаг доводится до честного исхода; STOP **никогда** не трогает `main`/force-push/прод-контейнер/detached-процесс. Тест/обоснование fail-safe точки. |
|
||||
| TR-4 | **Коллизия натуральных ключей при повторном «To Analyse»** — старая отменённая строка держит `plane_id`/`work_item_id` → anti-dup/uniqueness блокируют пере-создание. | Сред. | Сред. | Тумбстон ключей `#cancelled-<id>` на 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-гейта перед прод-деплоем.
|
||||
Reference in New Issue
Block a user