feat(cancel): STOP-status task cancellation + relaunch-hole close (ORCH-090)
All checks were successful
CI / test (push) Successful in 33s
CI / test (pull_request) Successful in 32s

Introduce the dedicated Plane STOP status as a single declarative task-cancel
mechanism: stop the active agent (graceful SIGTERM cascade), cancel all jobs
(terminal `cancelled`, never requeued), remove the worktree + delete the remote
feature branch (never main, never force-push), drive the task to the new
system-terminal state `cancelled` and tombstone the natural keys so a later
"To Analyse" re-creates it from scratch (docs artefacts preserved). STOP during a
critical merge/deploy window is deferred until the irreversible step finishes
honestly. Also closes the relaunch hole: handle_status_start relaunch is gated to
the `analysis` stage; the only pipeline-start entry point remains "To Analyse".

Cross-cutting (adr-0026): the "task terminal" predicate is widened {done} ->
{done, cancelled} in serial_gate / task_deps / stages sink + reaper/worker
requeue guards. STAGE_TRANSITIONS exit-gates / QG_CHECKS / check_* are unchanged
(`cancelled` is a sink, not a new edge). Additive, never-raise, restart-safe,
under kill-switch ORCH_STOP_STATUS_ENABLED (off -> zero regression).

New: src/cancel.py (leaf), src/gitea.py (delete_remote_branch), tasks columns
cancelled_at/cancel_requested_at, jobs status `cancelled`, GET /queue `stop` block.
Tests: tests/test_stop_status.py (TC-01..TC-14 + D7); full suite green (1345).
Docs updated in-PR (architecture README, CLAUDE.md, README.md, .env.example,
CHANGELOG). ADR-001 D4 refinement: plane_issue_id is tombstoned too (the lookup
ORs on it) — original UUID recoverable from the parseable suffix.

Refs: ORCH-090

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 21:01:57 +03:00
parent 269cbde3e3
commit ad5bd901e3
27 changed files with 1394 additions and 38 deletions

View File

@@ -278,7 +278,7 @@ 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)
### STOP / отмена задачи: терминал `cancelled` + закрытие дыры релонча (ORCH-090 — реализовано)
До ORCH-090 не было штатного способа отменить задачу (ручная хирургия по БД/процессам) и
существовала **дыра релонча**: `handle_status_start` при существующей задаче без активного job
@@ -298,9 +298,16 @@ Phase A ждёт ручного `Confirm Deploy`, ORCH-059). ORCH-089 снима
- **Каскад отмены:** 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`) сохраняются.
`remove_worktree` + never-raise удаление **только feature-ветки** Gitea (`gitea.delete_remote_branch`;
`main`/`master` неприкосновенны — явный гард; без force-push); **тумбстон** `plane_id`/`work_item_id`/
**`plane_issue_id`** (суффикс `#cancelled-<id>`) → `get_task_by_plane_id` возвращает None → повторный
«To Analyse» создаёт задачу с нуля; docs-артефакты (`01..17`) сохраняются. Аддитивные колонки
`tasks.cancelled_at`/`cancel_requested_at` (`_ensure_column`).
> **Уточнение ADR-001 D4 (при реализации):** ADR предлагал сохранить `plane_issue_id` нетронутым, но
> `get_task_by_plane_id`/`create_task_atomic` матчат по `plane_id OR plane_issue_id` — нетумбстоненный
> `plane_issue_id` оставил бы отменённую строку «находимой» и заблокировал бы re-create (BR-3/TR-4).
> Поэтому он тоже тумбстонится; исходный UUID (== исходный `plane_id` во всех путях создания) парсится
> из детерминированного суффикса для аудита.
- **Безопасное прерывание merge/deploy:** STOP в критическом окне (self-deploy `INITIATED`-sentinel
ORCH-036, держание merge-lease ORCH-043/071) → **отложенная отмена** (durable
`cancel_requested_at`, отмена только `queued`-job'ов, алерт); необратимый шаг доводится до
@@ -782,9 +789,9 @@ Monitoring after Deploy → Done
## База данных (SQLite)
- `events` — входящие вебхуки (дедуп)
- `tasks` — задачи и их стадии
- `tasks` — задачи и их стадии; колонки `cancelled_at`/`cancel_requested_at` (ORCH-090) — durable-метки STOP-отмены (вторая — отложенная отмена в критичном окне merge/deploy). Терминальная стадия `cancelled` (сток, параллельно `done`); натуральные ключи отменённой строки тумбстонятся суффиксом `#cancelled-<id>` (`plane_id`/`work_item_id`/`plane_issue_id`)
- `agent_runs` — запуски агентов (run_id, usage, cost)
- `jobs` — очередь задач (ORCH-1); колонка `pid` (ORCH-065) — pid агентского процесса для liveness-детекции зомби job-reaper'ом
- `jobs` — очередь задач (ORCH-1); статусы `queued|running|done|failed|cancelled` (ORCH-090: `cancelled` — терминальный исход STOP, нигде не реквью'ится); колонка `pid` (ORCH-065) — pid агентского процесса для liveness-детекции зомби job-reaper'ом
- `job_deps` — декларативные зависимости задач (ORCH-026, Уровень B): `(task_id, depends_on_task_id)`, аддитивная; источник истины планировщика для гейта «B ждёт A»
- `repo_freeze` — durable per-repo rollback-freeze (ORCH-088, FR-5): `(id, repo, frozen_at, reason, work_item_id, cleared_at)`, аддитивная append-only; активный freeze ⇔ строка репо с `cleared_at IS NULL`. Выставляется post-deploy `DEGRADED` (`set_repo_freeze`), снимается вручную (`POST /serial-gate/unfreeze` → `cleared_at=now`). Гейтит serial-claim безусловно (деградировавшая задача уже `done`)
@@ -796,7 +803,7 @@ Monitoring after Deploy → Done
|--------|------|----------|
| GET | `/health` | health check |
| GET | `/status` | активные задачи (stage != done) |
| GET | `/queue` | очередь: counts + max_concurrency + resilience + reconcile (ORCH-053) + reaper (ORCH-065) + post_deploy (ORCH-021) + task_deps (ORCH-026) + serial_gate (ORCH-088) + последние jobs |
| GET | `/queue` | очередь: counts + max_concurrency + resilience + reconcile (ORCH-053) + reaper (ORCH-065) + post_deploy (ORCH-021) + task_deps (ORCH-026) + serial_gate (ORCH-088) + auto_labels (ORCH-089) + stop (ORCH-090) + последние jobs |
| POST | `/serial-gate/unfreeze` | ORCH-088 (FR-5): ручное снятие per-repo rollback-freeze (query/body `repo=<repo>`) → `{ok, repo, cleared, frozen}`; идемпотентно. Альтернатива — `UPDATE repo_freeze SET cleared_at=datetime('now') WHERE repo=? AND cleared_at IS NULL` |
| POST | `/webhook/plane` | Plane webhook |
| POST | `/webhook/gitea` | Gitea webhook (push, PR, CI status) |