Files
orchestrator/CLAUDE.md
claude-bot 861b5ee984 fix(plane): sandbox-only fail-closed guard for Plane writes from test process (ORCH-117)
Close the root class of incident ORCH-114: a pytest/worktree process performed a
REAL write (PATCH issues state=<Done> + comment) against the PRODUCTION Plane
project, because test/staging processes inherit the live Plane token
(PLANE_HEADERS/PROJECT_ID are captured at import — a post-hoc env/token swap is a
no-op) and nothing forced them to write only to the sandbox. Symmetric to the
existing _no_telegram autouse floor.

- New pure never-raise leaf src/plane_write_guard.py (decide/audit_block/
  audit_allow), wired into the 3 plane_sync write primitives (update_issue_state /
  add_comment / _set_issue_state_direct) via _guard_allows_write, AT CALL TIME,
  before any network step. Active ONLY in a test process (pytest in sys.modules /
  PYTEST_CURRENT_TEST); live + staging runtimes (uvicorn) are a strict no-op.
- In a test process: default-deny. A write is allowed iff opt-in
  (plane_test_write_enabled) AND target project in the sandbox allowlist
  (plane_test_sandbox_projects, default = the one SANDBOX id). Prod is blocked even
  with opt-in (allowlist sandbox-only); unresolved project -> block (fail-closed).
- Independent second layer: tests/conftest.py::_plane_sandbox_only autouse floor.
  Intentionally NO prod-block kill-switch (anti back-door, NFR-6).
- Audit: block -> loud ERROR; sandbox-allow -> INFO.
- Bypass fixtures for the 3 (+1) pre-existing tests that assert on the mocked
  write primitive's httpx call (header/URL/state logic), the guard is no Quality
  Gate: STAGE_TRANSITIONS / QG_CHECKS / check_* / machine-verdict / DB schema
  untouched.
- Tests: tests/test_orch117_plane_write_isolation.py (TC-01 mandatory ORCH-114
  regression + TC-02..TC-14). Docs: CLAUDE.md, architecture/README.md,
  operations/INFRA.md, .env.example, CHANGELOG.md.

Refs: ORCH-117
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:32:20 +03:00

598 lines
83 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md — паспорт проекта orchestrator
## TL;DR
Мульти-агентный оркестратор разработки. FastAPI-сервис: принимает webhooks от Plane и Gitea, ведёт задачи по конвейеру стадий через Quality Gates, запускает Claude CLI агентов (analyst → architect → developer → reviewer → tester → deployer) на каждой стадии. **Оркестратор дорабатывает в том числе сам себя (self-hosting).**
## Стек
- Backend: FastAPI + uvicorn (Python 3.12)
- БД: SQLite (`src/db.py`)
- Агенты: Claude CLI (`ORCH_CLAUDE_BIN`), по одному промпту на роль в `.openclaw/agents/`. **ORCH-74:** модель/эффорт агента берутся ТОЛЬКО из config (`resolve_agent_model`/`resolve_agent_effort`, ORCH-41) — frontmatter `model:` удалён как мёртвый, frontmatter описательный; имя модели валидируется форматом `^claude-…$` перед `--model` (never-break). **ORCH-077 (52d, замыкает эпик 52):** тело всех 6 промптов переписано в едином **каноне Anthropic** (5 обязательных XML-секций в нормативном порядке `<context>``<task>``<deliverables>``<constraints>``<output_format>`, запреты в формате «❌ X → ✅ Y», `<thinking>` у решающих ролей), и каждый промпт **добровольно** эмитит 6-польную frontmatter-схему 52c (`work_item`/`stage`/`author_agent`/`status`/`created_at`/`model_used`) **аддитивно** — рядом с machine-verdict ключом, НЕ меняя его имя/регистр/значения (`verdict:`/`result:`/`staging_status:`/`deploy_status:`/`security_status:` — байт-в-байт). Это **docs/prompts-only** изменение: `src/**`/`STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД не тронуты; `frontmatter_validation_strict` остаётся `False` (enforcement НЕ включён). Промпт `cat`-ается из worktree в момент запуска → новые промпты вступают в силу на следующем worktree от `main` без прод-рестарта. Анти-регресс — структурные тесты `tests/test_agent_prompts_canon.py` + зелёный `test_agent_frontmatter_no_model.py`. **Норматив на будущее:** новые/изменённые агент-промпты следуют этому канону. Детали — `docs/architecture/adr/adr-0021-prompt-canon-anthropic.md`. **ORCH-092 (эпилог эпика 52, docs/prompts-only):** аудит 6 промптов поверх канона — копируемые frontmatter-примеры расхардкожены (`created_at: <YYYY-MM-DD>`/`model_used: <resolve ORCH-41>` + врезка «подставь `date +%F`/модель из конфига, не копируй буквально»; литерал `claude-opus-4-8` — только справка в таблице полей); добавлена секция `<escalation>` developer/reviewer/tester (после `</success_criteria>`, порядок 5 секций цел); developer лишён ручного `git rebase origin/main` (свежесть базы — инвариант движка serial-gate ORCH-088 + `auto_rebase_onto_main` под merge-lease; ручной rebase конфликтовал с запретом force-push — ADR-001 D1); tester обогащён worktree-путём + smoke `serial_gate` + покрытием каждого TC; из reviewer удалена мёртвая строка «тот же экземпляр Developer». **Языковое исключение (нормативно, ADR-001 D2):** `deployer.md` сознательно остаётся на **английском** (5 ru + 1 en) как самый safety-critical промпт — НЕ «чинить» язык вслепую; критичные self-hosting-запреты подняты в видную рамку. Verdict-ключи и канон 52d — байт-в-байт; анти-регресс`tests/test_agent_prompts_canon.py` (ORCH-092 TC-01…TC-08). Детали — `docs/work-items/ORCH-092/06-adr/ADR-001-developer-rebase-and-deployer-language.md`. **ORCH-109 (timeout-бюджеты + launch-стамп модели, инцидент ORCH-104):** две аддитивные изолированные правки `launcher` (без касания `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схемы БД). (1) **Launch-стамп модели:** резолвенная `resolve_agent_model` пишется в `agent_runs.model` в момент launch объединённым `UPDATE … SET model=?, effort=?` рядом со стампом эффорта (ORCH-087) → модель видна не-`null` при ЛЮБОМ исходе, включая timeout-kill (`exit_code=-9`), и in-flight в `GET /metrics`/`GET /queue`; постфактум `record_usage` (`model=COALESCE(?, model)`) остаётся обогащением (не затирает launch-стамп при `model=None`); пустой резолв → `NULL`; never-raise. (2) **Поднятые per-role бюджеты:** выделенные ключи `agent_timeout_developer_s=3600`/`agent_timeout_reviewer_s=3000` (env `ORCH_AGENT_TIMEOUT_DEVELOPER_S`/`_REVIEWER_S`); лестница `_resolve_timeout`: `agent_timeout_overrides_json[agent]` → выделенный ключ роли → `agent_timeout_seconds=1800` (прочие роли — байт-в-байт; малформный/непозитивный конфиг → дефолт + WARNING, never-break). Инвариант reaper ORCH-065 сохранён синхронным поднятием `reaper_max_running_s` 3600→**5400** (`5400 > 3600+20=3620`). FR-5 анти-salvage — структурно (продвижение гейтится `if exit_code==0`, timeout-kill → `_finalize_job` retry/fail), зафиксировано регресс-тестом; новых ветвей нет. Покрытие — `tests/test_orch109_timeout_model.py`. Детали — `docs/work-items/ORCH-109/06-adr/ADR-001-agent-timeout-budgets-and-launch-model-stamp.md`, сквозной `docs/architecture/adr/adr-0040-agent-timeout-budgets-and-launch-model-stamp.md`.
- Очередь задач: собственная (SQLite `jobs`, `src/queue_worker.py`, ORCH-1). **ORCH-026:** `claim_next_job` гейтит задачи с незавершёнными зависимостями (`job_deps`, `NOT EXISTS`) без занятия слота `max_concurrency`; декларации/детект циклов — leaf `src/task_deps.py` (kill-switch `ORCH_TASK_DEPS_ENABLED`). Сериализация мержа одного репо — безусловный pre-merge rebase под merge-lease (`ORCH_PREMERGE_REBASE_ALWAYS`). **ORCH-088 (serial gate, Этап 1):** новая задача репо не входит в `analysis` (analyst-job не выбирается, ветка не режется), пока в репо есть **более ранняя** незавершённая задача (`t2.id < jobs.task_id`, FIFO) ИЛИ репо заморожен (`repo_freeze`). Срез ветки **отложен** со `start_pipeline` на момент claim analyst-job (`launcher._materialize_deferred_branch`) — база = свежий `origin/main` с кодом предшественника (анти-stale-base). Post-deploy `DEGRADED` → durable per-repo freeze (`repo_freeze`, `cleared_at IS NULL` = активен) + Telegram; снятие — вручную `POST /serial-gate/unfreeze?repo=…`. Leaf `src/serial_gate.py` (claim — fail-OPEN, freeze — fail-CLOSED); флаги `ORCH_SERIAL_GATE_ENABLED` (kill-switch), `ORCH_SERIAL_GATE_REPOS` (CSV; пусто = все репо), `ORCH_SERIAL_GATE_FREEZE_ENABLED`. Блок `serial_gate` в `GET /queue`. `STAGE_TRANSITIONS`/`QG_CHECKS` не тронуты. **ORCH-093 (merge-актор устойчив к икоте Gitea):** детерминированный merge-актор под-гейта `deploy → done` (`src/merge_gate.py`) ретраит **транзиентные** ошибки Gitea вместо ложного HOLD (инцидент ORCH-063: `POST …/merge``405 "try again later"` сразу после пуша). `merge_pr` оборачивает **только** мутирующий `POST …/merge` в ограниченный retry-loop с экспоненциальным backoff (`min(base*2^(i-1), max)`, потолок суммарного сна `(N-1)*max ≤ 10 с`); классификатор `_classify_merge_response`: транзиент (ретрай) — `405`/`408`/`5xx`/таймаут/сетевая + `409`/`422` при `mergeable==True` (доп. `GET /pulls/{index}`; `mergeable==None` → дефолт-транзиент, fail-OPEN-в-ретрай), терминал (быстрый честный `False`, защита ORCH-071/073 как прежде) — `403`/`404`/реальный конфликт (`mergeable==False`). Kill-switch `merge_retry_enabled=false` → ровно один POST (байт-в-байт прежнее one-shot); флаги `ORCH_MERGE_RETRY_*` (`max_attempts=3`, `backoff_base_s=2`, `backoff_max_s=5`). Гард **already-in-main** в `ensure_open_pr` (leaf `_branch_fully_in_main`, `git merge-base --is-ancestor HEAD origin/main`): ветка целиком в `main` → исход `"already-in-main"` без создания мусорного пустого PR; `_handle_merge_verify` пропускает `merge_pr` и отдаёт авторитетному SHA-в-main довести до `done` (НЕ HOLD); git-ошибка → fail-OPEN на create-путь. Без отдельного флага (накрыт `merge_verify_autocreate_pr_enabled`). INV-4 (мерж только через Gitea PR-merge API, никогда push/force-push в `main`), never-raise, `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД — сохранены. Детали — `docs/work-items/ORCH-093/06-adr/ADR-001-merge-transient-retry-and-already-in-main-guard.md`, сквозной `docs/architecture/adr/adr-0027-merge-actor-transient-retry-and-already-in-main.md`.
- Контейнеризация: Docker + Compose
- CI/CD: Gitea Actions (`.gitea/workflows/`)
- Деплой: docker compose на mva154
## Команды
- `uvicorn src.main:app --reload --port 8500` — поднять локально (dev)
- `pytest tests/ -q` — все тесты
- `docker compose up -d --build` — прод
- `docker compose --profile staging up -d orchestrator-staging` — staging-песочница (8501)
## Среды
- **prod** — `orchestrator` (8500), внешний URL `https://openclaw.mva154.duckdns.org/orchestrator/`
- **staging** — `orchestrator-staging` (8501), изолированная БД (`./data/staging`), только sandbox-проект
## Структура
- `src/` — приложение (main, config, db, stages, stage_engine, queue_worker, projects, usage)
- `src/agents/launcher.py` — запуск Claude CLI агентов
- `src/qg/checks.py` — Quality Gate проверки
- `src/webhooks/` — приём вебхуков Plane/Gitea
- `tests/` — pytest
- `docs/` — документация, ADR, work-items, operations; **витрина системы — `docs/overview/`** (единая точка входа «бизнес + тех», ORCH-011)
- `scripts/` — утилиты (staging_check.py, orchestrator-deploy-hook.sh)
## Конвейер (кратко; детали — docs/architecture/README.md)
```
created → analysis → architecture → development → review → testing → deploy-staging → deploy → done
↑ │
└──── REQUEST_CHANGES ──────┘ (откат на development, max 3)
```
## Статусная модель Plane (ORCH-066) — индикация ≠ управление
Статусы Plane — это **слой B (индикация)**, отдельный от **слоя A (машина стадий)** `src/stages.py::STAGE_TRANSITIONS`. Plane показывает наблюдателю осмысленную картину (`Backlog → Todo → Analysis → Architecture → Development → Code-Review → Testing → Awaiting Deploy → Deploying → Monitoring after Deploy → Done` + человеческие гейты `In Review/Approved`, `Confirm Deploy`), но НИКОГДА не управляет конвейером. Маппинг и сеттеры — `src/plane_sync.py` (6 новых ключей: `to_analyse/analysis/code_review/awaiting_deploy/deploying/monitoring`), с project-relative alias-fallback: на частично сконфигурированном проекте новый ключ деградирует на базовый UUID ТОГО ЖЕ проекта (нулевая регрессия для enduro-trails). Детали — `docs/architecture/README.md`.
**Terminal-window-aware гард deploy-статусов (ORCH-094).** Задача с БД `stage=done` и 0 активных job'ов стабильно держит Plane=`Done`: три deploy-фазовых сеттера (`set_issue_awaiting_deploy`/`set_issue_deploying`/`set_issue_monitoring`) были терминал-слепы и флаппили `Awaiting ⟷ Monitoring` (верифицировано на ORCH-061, task 47), т.к. любой стейл/двойной/неизвестный вызов под бот-токеном перезаписывал терминал промежуточным статусом. Новый leaf `src/deploy_status_guard.py` (чистая, never-raise, config-gated; по образцу `serial_gate`/`labels`/`cancel`) — `decide(work_item_id, target, reason) -> ALLOW | CONVERGE_DONE | SUPPRESS` на **входе** трёх сеттеров `plane_sync` (низкий чокпоинт ловит любой путь, включая неизвестный актор). Инвариант: deploy-статус легитимен ⇔ задача **нетерминальна** ИЛИ (`done` И активно пост-деплой-окно `post_deploy.window_active` = ARMED & не DONE); иначе для `done` — идемпотентное `CONVERGE_DONE` (сеттер зовёт `set_issue_done`), для `cancelled``SUPPRESS`. Чтобы легитимный первый `Monitoring` (БД уже `done` к моменту стр. 404) прошёл, арм-блок `post_deploy.arm_monitor` **перенесён выше** terminal-sync-блока в `advance_stage` (ADR-001 D3) → `window_active==True` до выставления `Monitoring`. Монитор-тик при БД `cancelled` мид-окно → закрыть окно без статус-PATCH (zombie-tick guard, FR-3). Наблюдаемость: BC-kwarg `reason` у трёх сеттеров + одна структурная лог-запись на вердикт (`work_item`/`caller`/`target`/`db_stage`/`window_active`/`verdict`; converge/suppress → WARNING). Read-only аксессор `db.get_task_by_work_item_id`. Флаги `deploy_status_guard_enabled` (kill-switch; `False` → 1:1 прежнее) / `deploy_status_guard_repos` (CSV; **пусто → self-hosting only**, enduro не затронут). `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схема БД — не тронуты. Детали — `docs/work-items/ORCH-094/06-adr/ADR-001-terminal-window-aware-deploy-status-guard.md`, сквозной `docs/architecture/adr/adr-0028-terminal-window-aware-deploy-status-guard.md`.
## Нотификации / Telegram live-tracker (ORCH-042/066/067/087)
Каждая задача = **одна карточка** в Telegram (`src/notifications.py`). Поведение карточки:
- **Дефолт `tracker_mode``bump`** (ORCH-067; `edit` доступен через `ORCH_TRACKER_MODE=edit`).
`bump` на каждом обновлении удаляет старую карточку и шлёт свежую вниз чата (тихо), `edit`
редактирует на месте. Инвариант «одна карточка на задачу» — в обоих режимах.
- **Зачистка сирот (ORCH-087):** bump ведёт авторитетный леджер ВСЕХ созданных карточек
(таблица `tracker_messages`, `deleted_at IS NULL` = жива) и на каждом обновлении удаляет
ВСЕ незакрытые mid, а не только скаляр `tracker_message_id` (он сохранён как указатель на
текущую карточку, BC). Это устраняет класс «замёрзшая сирота» (старая карточка с заголовком
ранней стадии, потерявшая ссылку при гонке/`delete`-fail+`send`-ok). Новый mid пишется в
леджер ТОЛЬКО при успешном `send` (BR-6); transient-`delete` остаётся незакрытым для ретрая;
«already gone»/>48ч (`_DELETE_GONE_MARKERS`) → закрывается. Остаточная гонка самозалечивается
за один bump. Known-limitation: Telegram 48ч (сироты старше неудаляемы).
- **Эффорт в строке стадии (ORCH-087):** колонка `agent_runs.effort` стампится фактическим
`resolve_agent_effort` в `launcher._spawn` (CLI его в result-JSON не возвращает); строка
рендерится `· {model} · {effort}` (developer=`xhigh`, tester/deployer=`medium`, прочие=`high`);
пустой/исторический effort → суффикс опускается.
- **Честное итоговое время (ORCH-087):** done-строка = три независимых подписанных метрики
`⏱️ Агенты {Σ agent_runs} · твоё {review~cap} · общее с ожиданием {wall}` (раньше `Всего {wall}`
читалось как сумма, которой не является). «Твоё» ограничено `tracker_brd_review_cap_s`
(`ORCH_TRACKER_BRD_REVIEW_CAP_S`, дефолт 2ч; маркер `~` при отсечке аномального застоя).
- **Статус-строка карточки** (`📍 <status_label>`) показывает текущий Plane-статус по модели
ORCH-066 (`plane_status_label`). Оффлайн-ядро (`stage → статус`, In Review из brd-clock)
работает всегда без сети; best-effort live-overlay (kill-switch `tracker_live_status`,
TTL-кэш, короткий таймаут) лишь дорисовывает ветки, неотличимые offline (Needs Input /
Blocked / Rejected / Cancelled / **Confirm Deploy** / Deploying / Monitoring) и **никогда не
блокирует конвейер**.
- **Кликабельный номер задачи** (`plane_issue_link`) — `ORCH-NNN` в карточке И во всех
уведомлениях (`notify_*`, alert'ы стадий) рендерится как `<a href=…>` на issue в Plane;
fail-safe → просто `html.escape(номер)`, если ссылку построить нельзя. Никогда не падает.
- **Без link-preview (ORCH-080):** оба примитива (`send_telegram`/`edit_telegram`) шлют
payload с `disable_web_page_preview: True` — баннер Plane («Modern project management»)
под кликабельной ссылкой `ORCH-NNN` больше не разворачивается ни в карточке (`bump`/`edit`),
ни в notify/alert-сообщениях. `parse_mode: HTML` сохранён → ссылка остаётся кликабельной.
- Транспорт (`send_telegram`/`edit_telegram`/`delete_telegram`), `disable_notification`
(карточка тихая, пингуют только alert-хелперы), схема БД — не трогаются.
## Авто-режим по лейблам: autoApprove + autoDeploy (ORCH-089)
Конвейер имеет два **человеческих** гейта, тормозящих пакетный автономный прогон
(эпик ORCH-088): гейт BRD (`analysis`: ручной `Approved`) и гейт прод-деплоя
(`deploy` Phase A: ручной `Confirm Deploy`, ORCH-059). ORCH-089 снимает **только эти
два человеческих решения** — выборочно (лейбл Plane на задаче), декларативно,
обратимо, **не трогая ни одной технической проверки**. Инвариант: авто-режим снимает
лишь ожидание человеческого сигнала; `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/схема БД
**не трогаются**. Аддитивно: leaf `src/labels.py` (never-raise) + две точечные врезки.
- **`autoApprove`** → врезка в `stage_engine._handle_analysis_approved_flow` (ветка
`files_ok`): `set_issue_approved` (индикация) + лог/Telegram/Plane-коммент +
`advance_stage(..., finished_agent=None)`**тот же путь, что человеческий Approved**
(`approved-via-status``analysis → architecture` + `mark_brd_review_ended`).
- **`autoDeploy`** → врезка в `stage_engine._handle_self_deploy_phase_a` после advance
на `deploy` + `clear_state`: лог/Telegram/Plane-коммент + `_handle_self_deploy_phase_b`
(маркер `INITIATED`, статус `Deploying`, finalizer). Пропускаются лишь
индикативно-человеческие шаги. **BR-5 структурно:** Phase A достигается только после
зелёных под-гейтов ребра `deploy-staging → deploy` (security → merge-gate →
image-freshness → staging) → autoDeploy физически не деплоит сломанное.
- **Чтение лейблов** — `plane_sync.fetch_issue_labels` (`None` при ошибке ≠ `[]`) +
`get_project_labels` (`{normalized_name→uuid}`, TTL-кэш); сопоставление по
нормализованному имени (`strip().casefold()`), неоднозначность → «нет лейбла».
Источник истины — Plane API, не payload вебхука. Новый сеттер `set_issue_approved`.
- **Флаги** (`config.py`): `auto_label_enabled` (kill-switch), `auto_approve_label`/
`auto_deploy_label`, `auto_label_repos` (CSV; **пусто → self-hosting only**),
`auto_label_states_ttl_s`. `applies(repo)` (локальный) проверяется ПЕРВЫМ; `has_label`
(сеть) — только при `applies==True` → при выключенном флаге нулевой сетевой оверхед.
- **Fail-safe (never auto):** любая ошибка/недоступность Plane/неоднозначность →
«нет авто» → ручной гейт (never-raise). Прозрачность: лог + Telegram + Plane-коммент +
live-карточка; блок `auto_labels` в `GET /queue`. **Инфра-предусловие:** создать лейблы
`autoApprove`/`autoDeploy` в Plane-проекте ORCH (их отсутствие = ручной режим, fail-safe).
Детали — `docs/work-items/ORCH-089/06-adr/ADR-001-auto-label-gates.md`,
`docs/architecture/adr/adr-0018-auto-label-gates.md`.
## Отмена задачи: статус STOP (ORCH-090)
Выделенный Plane-статус **STOP** — операторская кнопка «отменить + сбросить» задачу. Вводит
**новое системное терминальное состояние `cancelled`** (стадия `tasks.stage='cancelled'` + job-исход
`jobs.status='cancelled'`), равноправное `done`. Логический ключ `stop`**fail-closed** (нет в
`_DEFAULT_STATES`, по образцу `confirm_deploy`/ORCH-059): доска без статуса STOP → ветка не
активируется. Маршрут `handle_issue_updated → handle_stop → stage_engine.cancel_task`:
- **Полный сброс** (вне критичного окна): graceful SIGTERM активного агента (`launcher.stop_process`,
переиспользует каскад `_watchdog`), все job'ы → терминальный `cancelled` (не реквью'ятся:
`claim_next_job` берёт только `queued`, reaper/worker сверяют терминал задачи — TR-2), удаление
worktree + **рабочей** Gitea-ветки (`gitea.delete_remote_branch`, **никогда** `main`, без
force-push), durable `stage='cancelled'` + **тумбстон** натуральных ключей (`plane_id`/
`work_item_id`/`plane_issue_id` → суффикс `#cancelled-<id>`; ADR-001 D4 уточнён: тумбстонится и
`plane_issue_id`, т.к. `get_task_by_plane_id`/`create_task_atomic` матчат по нему — иначе re-create
коллизирует; исходный UUID парсится из суффикса для аудита). Docs-артефакты (`01..17`) сохраняются.
- **STOP в критичном окне merge/deploy** (ADR-001 D7): `cancel.in_critical_window`**отложенная**
отмена: `tasks.cancel_requested_at`, снимаются только `queued` job'ы (running-актор деплоя/мержа не
трогается), алерт; детерминированный finalizer (`run_deploy_finalizer`) доводит необратимый шаг до
честного исхода и применяет отмену (`force=True`). «Критичное окно» = реально начатый необратимый
шаг: INITIATED-sentinel self-deploy (ORCH-036; детач-деплой + поздний `merge_pr` в
`_handle_merge_verify` идут под тем же маркером) **либо** держание merge-lease (ORCH-043) **И**
активно бегущий актор (running-job). **Уточнение P1 (ORCH-090 review):** держание merge-lease в
Phase A на стадии `deploy` в ожидании ручного `Confirm Deploy` БЕЗ бегущего актора **полностью
обратимо** (ничего не смержено/задеплоено) → НЕ критично → немедленный полный сброс (сам отпускает
lease). Иначе отмена откладывалась бы к finalizer'у, который оператор (нажавший STOP именно чтобы НЕ
подтверждать деплой) не запускает — задача застревала бы с удержанным lease, клиня serial-gate репо.
STOP **никогда** не трогает `main`/force-push/прод-контейнер/detached-процесс (NFR-3).
- **Кросс-каттинг (adr-0026):** предикат «задача терминальна» расширен `{done}``{done, cancelled}`
в `serial_gate`/`task_deps`/`stages.py`-сток (иначе отменённая задача заклинит очередь репо);
reconciler-терминал-скип уже знал `cancelled` (ORCH-086). `STAGE_TRANSITIONS` exit-гейты рёбер /
`QG_CHECKS` / `check_*`**не тронуты** (`cancelled` — сток, не ребро).
- **Дыра релонча закрыта (D6):** relaunch агента в `handle_status_start` ограничен стадией `analysis`
(единственный владелец Needs Input, ORCH-066); ручной перевод существующей задачи в иной промежуточный
статус больше не релончит середину пайплайна. Запуск пайплайна — только «To Analyse» → `start_pipeline`.
- Флаги `stop_status_enabled` (kill-switch; `False` → всё инертно, нулевая регрессия) / `stop_status_repos`
(CSV; пусто → все репо). Leaf `src/cancel.py` (never-raise). Read-only блок `stop` в `GET /queue`.
Аддитивные колонки `tasks.cancelled_at`/`cancel_requested_at` (`_ensure_column`). **Инфра-предусловие:**
создать статус **STOP** с группой `cancelled` на доске ORCH (его отсутствие = fail-safe no-op). Детали —
`docs/work-items/ORCH-090/06-adr/ADR-001-stop-cancel-task.md`,
`docs/architecture/adr/adr-0026-stop-cancel-task.md`.
## Багфикс-трек: дешёвый маршрут для багов (ORCH-019)
Задача с меткой Plane `Bug` идёт **укороченным маршрутом** — пропускается стадия `architecture`
(отдельный прогон opus-агента `architect` + ADR + exit-гейт `check_architecture_done`); тяжёлая
аналитика заменяется облегчённым пакетом (короткий bug-report + обязательный план регресс-теста,
но всё равно все 4 файла analysis — гейт `check_analysis_complete` не меняется). **Корневой
инвариант (NFR-1):** срезается ТОЛЬКО аналитика/архитектура — **все Quality Gate'ы и под-гейты
исполняются без изменений** (`STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict ключи —
байт-в-байт прежние); маршрутизация багфикса — свойство планировщика, **не** гейт. Аддитивно, под
kill-switch, never-raise, fail-safe → полный цикл.
- **Классификация (D1):** leaf `src/bug_fast_track.py` (never-raise, образец `labels`/`serial_gate`).
`bug_fast_track_applies(repo)` (локально, без сети) ПЕРВЫМ → выключенный флаг = нулевой сетевой
оверхед; `is_bug_task` делегирует в `labels.has_label` (ORCH-089-аппарат, источник истины — Plane
API, не payload). Чтение метки — только в `start_pipeline`, **никогда** в горячем `claim_next_job`
(NFR-4).
- **Хранение типа (D2):** аддитивная идемпотентная колонка `tasks.track TEXT DEFAULT 'full'`
(`_ensure_column`, паттерн `tasks.cancelled_at`); значения `'full'` (дефолт, ВСЕ существующие и
не-баг задачи) | `'bug'`. Хелперы `db.set_task_track`/`get_task_track` (отсутствие/NULL → `'full'`,
fail-safe). Читается в `advance_stage` из БД, не из сети.
- **Routing-override (D3):** врезка в `advance_stage` на ребре выхода из `analysis`: при `track='bug'`
(чистый предикат `bug_fast_track.skips_architecture`) `next_stage``development`, `next_agent`
`developer` (минуя `architect`). `STAGE_TRANSITIONS`/`get_next_stage`/`get_agent_for_stage` — чистые,
1:1. Стамп `mark_brd_review_ended` расширен на `analysis → development` (честная метрика ORCH-087).
- **Эскалация (D5):** `POST /bug-fast-track/escalate?work_item=<id>` сбрасывает `track` `'bug'→'full'`
→ следующий переход уходит в `architecture` (полный цикл). Плюс self-escalate мини-аналитика
(«баг сложный → полный пакет + `escalate: full-cycle`»).
- **Флаги** (`config.py`): `bug_fast_track_enabled` (kill-switch, env `ORCH_BUG_FAST_TRACK_ENABLED`),
`bug_fast_track_label` (дефолт `Bug`), `bug_fast_track_repos` (CSV; **пусто → self-hosting only**).
`False`/неприменимый репо → старт и маршрут байт-в-байт прежние (нулевая регрессия для enduro и
orchestrator). Наблюдаемость — read-only блок `bug_fast_track` в `GET /queue` (флаг/метка/область +
счётчик багфикс-задач + метрика пропущенных стадий `architecture`) + отметка `🐞` в Telegram-карточке
(never-raise). Композиция: багфикс-задача — обычная задача репо для serial-gate (ORCH-088, не
обходит его); `autoApprove`/`autoDeploy` (ORCH-089), coverage-gate (ORCH-027, союзник BR-4),
merge-gate (ORCH-043) — штатно. **Инфра-предусловие:** создать метку **`Bug`** в Plane-проекте ORCH
(её отсутствие = fail-safe полный цикл). Детали —
`docs/work-items/ORCH-019/06-adr/ADR-001-bug-fast-track.md`,
`docs/architecture/adr/adr-0032-bug-fast-track.md`.
## Гейт покрытия тестами (ORCH-027)
Существующие тестовые гейты (`check_ci_green`, `check_tests_passed`, merge-gate re-test) судят
только по **факту** прохождения, не по **полноте** — ни один не замечает «300 строк кода, 0
тестов», и при пакетном автономном прогоне (ORCH-088) покрытие монотонно деградирует. Введён
**детерминированный (без LLM) под-гейт ребра `deploy-staging → deploy`** по образцу security-гейта
(ORCH-022): leaf `src/coverage_gate.py` (never-raise) + тонкая обёртка `check_coverage_gate` в
`QG_CHECKS` + врезка `_handle_coverage_gate` в `advance_stage`. **Инвариант:** `STAGE_TRANSITIONS` /
семантика существующих `check_*` / machine-verdict ключи (`verdict:`/`result:`/`deploy_status:`/
`staging_status:`/`security_status:`) — байт-в-байт прежние; новая БД-таблица аддитивна (NFR-5).
- **Точка/порядок:** **ПОСЛЕ merge-gate** (покрытие меряется на догнанном `auto_rebase_onto_main`
HEAD — ровно том коде, что landed в `main`) и **ДО image-freshness** (фейл до дорогого
docker-rebuild). Порядок под-гейтов: **security → merge → coverage → image-freshness.** FAIL →
штатный откат на `development` (+ инкремент developer-retry, cap `MAX_DEVELOPER_RETRIES`) **и
освобождение merge-lease** (merge-gate держал его на своём PASS — зеркало image-freshness rollback).
- **Измерение:** `python -m pytest tests/ --cov=src --cov-report=json` в изолированном per-branch
worktree (`ensure_worktree`); метрика — `totals.percent_covered` (line coverage `src/`). Измеритель
за `measure_coverage(repo, branch) -> float | None` (стек-расширяемость BR-6). Тайм-аут
`coverage_run_timeout_s`. Новая pip-зависимость `pytest-cov`.
- **Решение — чистая функция** `compute_coverage_verdict(measured, baseline, floor, policy, epsilon)
-> (ok, reason)`: `absolute` → `measured ≥ floorε`; `baseline` → `measured ≥ baselineε`; `both`
(дефолт) → оба; `baseline is None` (bootstrap) → baseline-условие не применяется. `epsilon` —
допуск на шум измерения (анти-флап у границы).
- **Базовая линия — аддитивная БД-таблица** `coverage_baseline(repo PK, coverage, source_sha,
updated_at)` (`CREATE TABLE IF NOT EXISTS`; хелперы `db.get_coverage_baseline`/
`ratchet_coverage_baseline`/`set_coverage_baseline`). Наращивание **только вверх** в choke-point
подтверждённого merge `_handle_merge_verify` (ребро `deploy → done`): `ratchet_baseline_on_merge`
читает измеренное из `18-coverage-report.md` (single source of truth), атомарный compare-and-set
`UPDATE … WHERE coverage <= measured` под держимым merge-lease (ORCH-043) → базовая линия не падает
даже при гонке; bootstrap засевается первым применимым merge.
- **Условность (как ORCH-22/43/58):** `coverage_gate_enabled` (kill-switch; `False` → 1:1 как до
ORCH-027) + `coverage_gate_repos` (CSV; **пусто → self-hosting only** `is_self_hosting_repo` →
enduro не затронут, no-op `(True, "N/A")`); `applies(repo)` (локально) ПЕРВЫМ — дорогой прогон
только при `applies==True`. Ошибка инструмента/непарсимая метрика → **fail-open + WARNING** по
умолчанию (`coverage_tool_fail_closed=False`, анти-петля); флаг → fail-closed.
- **Артефакт `18-coverage-report.md`** (frontmatter `coverage_status: PASS|FAIL` +
`measured_coverage`/`baseline`/`floor`/`policy`/`epsilon`/`delta`), вердикт читается ТОЛЬКО из
frontmatter через `src/frontmatter.py` (single source of truth, как `security_status:`).
Наблюдаемость — read-only блок `coverage` в `GET /queue`; при FAIL — `send_telegram` с кликабельным
номером, измеренным/порогом/дельтой; опциональный ручной override `POST /coverage/baseline`.
Флаги `ORCH_COVERAGE_*` (`MIN_PERCENT`/`POLICY`/`EPSILON`/`TOOL_FAIL_CLOSED`/`RUN_TIMEOUT_S`).
Self-hosting-безопасно: гейт только мерит/читает/пишет/решает — не деплоит/не рестартит прод/не
пушит `main`. **Инфра-предусловие:** `pytest-cov` в прод/staging-образе. Детали —
`docs/work-items/ORCH-027/06-adr/ADR-001-coverage-gate.md`,
`docs/architecture/adr/adr-0029-coverage-gate.md`.
## Merge-gate re-test: инфра-толерантность + tree-kill + контракт re-test (ORCH-110)
Багфикс инцидента **ORCH-109/PR #129** (bug → escalate full-cycle): локальный re-test merge-gate
(`check_branch_mergeable`, ребро `deploy-staging → deploy`) падал по **таймауту** (516.7s-сюит упёрся
в бюджет 600s под CPU-голоданием от **осиротевших** pytest-процессов) при зелёных CI+tester+staging →
маршрутизировался как **код-фейл** в `_handle_merge_gate_rollback` (откат на `development` + расход
developer-retry) → каждый retry падал так же → «Merge-gate still failing after 3 developer retries» →
ручное вмешательство. Аддитивно, под **5 независимыми kill-switch**, never-raise, скоуп self-hosting;
`STAGE_TRANSITIONS`/реестр `QG_CHECKS`/семантика/имя `check_branch_mergeable`/machine-verdict-ключи/
схема БД — **байт-в-байт не тронуты** (merge-gate остаётся под-гейтом-врезкой, не новой стадией/QG);
INV-4 (никогда push/force-push `main`) и запрет рестарта прод-контейнера — соблюдены.
- **D1 (tree-kill, корень утечки):** новый stdlib-only leaf `src/proc_group.py`
(`run_in_process_group`, паттерн чистоты `serial_gate`) спавнит спавненный pytest в **отдельной
группе процессов** (`start_new_session`) и при таймауте убивает **всё дерево** (`os.killpg`, каскад
SIGTERM→grace→SIGKILL, зеркало `launcher.stop_process`, грейс = `agent_kill_grace_seconds`).
Используют его `merge_gate.retest_branch` И `coverage_gate.measure_coverage` (сиблинг-источник
утечки). Контракты возврата сохранены — меняется лишь отсутствие сирот. Fallback never-break:
`subprocess_tree_kill_enabled=False`/не-POSIX → прежний `subprocess.run`.
- **D2/D3 (классификация + маршрутизация):** чистый предикат `merge_gate.classify_retest_failure`
различает `timeout`/`red`/`lock-busy`/`other` (scope-guard: «rebase timeout» git'аНЕ инфра-таймаут
re-test). Инфра-таймаут → `_handle_merge_gate_infra_retry` (зеркало `_handle_merge_gate_defer`:
задача **остаётся на `deploy-staging`**, staging-deployer перезапускается с задержкой, **БЕЗ** отката
на `development` и **БЕЗ** developer-retry; restart-safe счётчик `_merge_infra_retry_count` по маркеру
`merge-gate infra-timeout retry` в `task_content`). Анти-над-толерантность (BR-6): детерминированно
**красный** re-test / конфликт → прежний `_handle_merge_gate_rollback`. Anti-loop: исчерпание
`merge_retest_infra_max_retries` (дефолт 2) → один **инфра-alert** (явно «НЕ дефект кода»,
кликабельный номер), задача НЕ уходит в `development`. Kill-switch `merge_retest_infra_tolerance_enabled`
off → таймаут = прежний rollback (байт-в-байт).
- **D4 (контракт необходимости re-test):** при `premerge_rebase_always=True` re-test **пропускается**,
когда rebase — доказанный no-op (`merge_gate.head_sha` до==после, обе непусты = ветка уже содержит
свежий `origin/main`, тот же коммит уже прошёл CI+tester+staging) → лиз HELD, без re-test. Распространяет
на путь `=True` ту же оптимизацию, что путь `=False` уже имеет для не-behind ветки. Fail-safe: любая
неопределённость (`head_sha` пуст / git-ошибка) → re-test **выполняется** (BR-6/AC-3 не ослаблен).
Kill-switch `merge_retest_skip_when_current_enabled`.
- **D5 (бюджет):** `merge_retest_timeout_s` 600 → **900** (запас 74% над 516.7s) + валидация
`_resolve_retest_timeout` (малформ/непозитив → дефолт 900 + WARNING). Сквозной инвариант ORCH-065/109
`reaper_max_running_s (5400) > Σ(deploy-staging gate-work)+grace (≈4460)` соблюдён **без** правки
`reaper_max_running_s`.
- **D6 (наблюдаемость):** in-process счётчики (`_MERGE_GATE_COUNTERS`) + read-only блок `merge_gate` в
`GET /queue` (отличим от код-фейл-отката); координация с ORCH-111 (`proc_blocking`) без дубля (ORCH-110
предотвращает/толерирует у источника, ORCH-111 наблюдает). Append-only regression-guard:
`("ORCH-110", "classify_retest_failure", "src/merge_gate.py")` в `MAIN_REGRESSION_MARKERS`.
- **Флаги** (`config.py`, дефолт = боевое): `subprocess_tree_kill_enabled`/
`merge_retest_infra_tolerance_enabled`/`merge_retest_infra_max_retries`/`merge_retest_infra_retry_delay_s`/
`merge_retest_skip_when_current_enabled` (env `ORCH_*`). Откат = выставить 4 kill-switch в `False` +
`ORCH_MERGE_RETEST_TIMEOUT_S=600` → байт-в-байт до-ORCH-110. Детали —
`docs/work-items/ORCH-110/06-adr/ADR-001-merge-gate-retest-infra-tolerance-and-tree-kill.md`, сквозной
`docs/architecture/adr/adr-0042-merge-gate-retest-infra-tolerance-and-tree-kill.md`.
## Гигиена shared deploy-базы: устойчивый self-deploy `git pull` (ORCH-112)
Багфикс инцидента **ORCH-111** (bug → escalate full-cycle): прод-self-deploy падал на шаге
`git pull origin main` хост-хука (`scripts/orchestrator-deploy-hook.sh`) с `error: Your local changes
to the following files would be overwritten by merge: src/config.py` — грязь, оставленная
неуспешной/отменённой/брошенной задачей ORCH-104 в **общем** main checkout (`settings.deploy_host_repo_path`).
Деплой вставал → ручное вмешательство; на self-hosting (один прод-инстанс на все проекты) — групповой
риск. **Инвариант (нормативно):** shared main checkout `<host_repos_dir>/<repo>` — **deploy/worktree-management
база, НЕ редактируемый workspace** (агенты — worktree `git_worktree`, build — worktree-контекст, fallback'и
гейтов — read-only `git show origin/main`); локальных правок там быть не должно. Решение — **resilient-pull,
встроенный в хук** (`--deploy`): перед `git pull` хук при грязи приводит базу к чистому актуальному
`origin/main` (`git fetch` + `git reset --hard origin/main` + **скоупленный** `git clean -fd`). Аддитивно,
под kill-switch, never-raise, скоуп self-hosting; `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика и
имена `check_*` / machine-verdict-ключи / схема БД / exit-code-контракт хука (0/1/2, ORCH-036) — **байт-в-байт
не тронуты** (это устойчивость deploy-пути, **не** Quality Gate и **не** стадия).
- **Leaf `src/checkout_hygiene.py` (чистый never-raise):** по образцу `serial_gate`/`cancel`/`self_deploy`
(импортирует только `config`, лениво `self_deploy`/`qg.checks`/`notifications`) — `applies(repo)`
(kill-switch `checkout_hygiene_enabled` + скоуп `checkout_hygiene_repos`, **пусто → self-hosting only**,
локально и ПЕРВЫМ), `hook_env(repo, work_item_id)` (env-префикс `CHECKOUT_HYGIENE=1 HYGIENE_REPORT=<host-path>`,
инжектится в detached-команду `self_deploy.build_deploy_command` только при `applies==True`, иначе `""` →
хук видит `CHECKOUT_HYGIENE` неустановленным → голый `git pull` 1:1 до ORCH-112), `read_report`/`alert_dirty`
(наблюдаемость), `snapshot()` (read-only блок `GET /queue`).
- **Хук-блок «2a. Resilient pull»:** между шагом «1. Capture PREV_IMG» и «2. Pull», под
`if [[ "${CHECKOUT_HYGIENE:-0}" == "1" ]]`. **Сохранность (NFR-2, жёсткий контракт):** `git clean` —
**только `-fd`, НИКОГДА `-x`** (иначе удалил бы gitignored `.env`/прод-секреты, `data/*.db`/БД, `build/`);
явные `-e '.deploy-prev-image-*'` и `-e 'deploy-hook.log'` (untracked-но-НЕ-ignored — иначе сломался бы
rollback `do_rollback`); sibling `<repos_dir>/.deploy-state-*`/`.merge-lease-*.json` и `.git/worktrees/*` —
вне области `git clean` в `$REPO`. Каждый git-шаг — `|| log "...continuing"` (never-break): сбой гигиены не
ухудшает исход относительно голого pull; чистая база → no-op (happy-path/exit-коды байт-в-байт);
`--build-staging` (build из worktree, без pull) не затронут.
- **Сходимость после failed/cancelled (FR-2)** — этим же deploy-time self-heal (база сходится на следующем же
self-deploy); `cancel_task` (ORCH-090) **не расширяется**, фоновый janitor **не вводится**.
**Наблюдаемость (FR-4)** — хук пишет sentinel `hygiene`; Phase-C finalizer (`stage_engine.run_deploy_finalizer`)
читает (`read_report`) и шлёт Telegram-алерт (`alert_dirty`, кликабельный номер, best-effort, never-raise).
- **Флаги** (`config.py`, дефолт = боевое): `checkout_hygiene_enabled` (env `ORCH_CHECKOUT_HYGIENE_ENABLED`),
`checkout_hygiene_repos` (env `ORCH_CHECKOUT_HYGIENE_REPOS`). Откат = `ORCH_CHECKOUT_HYGIENE_ENABLED=false` →
деплой байт-в-байт до ORCH-112. Покрытие — `tests/test_deploy_checkout_hygiene.py` (шелл-симуляция реального
хука во временном git-репо без сети/прода/ssh + unit; TC-01 — обязательный регресс ORCH-111: красный до
фикса, зелёный после). Детали — `docs/work-items/ORCH-112/06-adr/ADR-001-deploy-base-checkout-hygiene.md`,
сквозной `docs/architecture/adr/adr-0044-deploy-base-checkout-hygiene.md`.
## Единое владение side-effectful переходами: durable-lease + expected-stage CAS (ORCH-114)
Закрыт **корневой класс** инцидент-цепочки **ORCH-110/111/112/113**: у side-effectful переходов
стадий не было единого владения. `advance_stage` ре-ентерабельна и писала стадию «голым»
`UPDATE … WHERE id=?` (без compare-and-swap), а ≥5 акторов (монитор / Plane-webhook / reconciler
F-1 / job-reaper / deploy-finalizer) входят в один переход независимо → конкурентный или
после-рестартовый повторный вход **дважды** применял необратимые эффекты (`merge_pr` /
coverage-ratchet / image-rebuild / инициация прод-деплоя) и давал **противоречие rollback↔done**
(инцидент ORCH-111, job 1914 / PR #130). Это **обобщение** процесс-локальной finalizer-liveness
ORCH-113 в **durable cross-path** владение. Аддитивно, под единым kill-switch, never-raise; новый
leaf `src/transition_lease.py`. **Инвариант:** `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика
и имена `check_*` / machine-verdict-ключи / **схемы существующих таблиц** — байт-в-байт (одна
аддитивная таблица `transition_lease`, без epoch-колонки на `tasks`); hot-path `claim_next_job`
lease **не консультирует** (fail-open, очередь репо никогда не клинится).
- **Два комплементарных слоя (оба под `transition_lease_enabled`):** (1) **durable transition-lease**
(таблица `transition_lease`) — владение на ВХОДЕ в side-effectful регион: второй актор, увидев
живого владельца (`is_held_by_live_owner`), не стартует тяжёлые под-гейты вовсе (предотвращение,
не починка постфактум); (2) **expected-stage CAS** (`db.update_task_stage_cas` ↔
`commit_stage_cas`) — на ЗАПИСИ стадии: проигравший гонку аборт без побочных эффектов. CAS
закрывает и **6 путей записи стадии в обход `advance_stage`** (gitea×5 + plane rollback).
- **Liveness владельца = `owner_pid` + `owner_boot_id` (НЕ heartbeat):** блокирующий 900s merge
re-test не может бить heartbeat (довод самого ORCH-113) → рестарт-recovery бесплатен (новый
процесс → новый `boot_id` → все прежние lease мгновенно устаревшие → реклеймятся).
`main.lifespan` зовёт `recover_on_startup()` после `requeue_running_jobs`. Lease **без
собственного TTL**: его потолок возраста = Tier-3 backstop `reaper_max_running_s` (5400) →
сквозной бюджет ORCH-065/109/110/113 не тронут.
- **Интеграция:** `advance_stage` захватывает lease на входе в side-effectful ребро
(`deploy-staging`/`deploy`), пишет стадию через CAS, освобождает lease в `try/finally` (на
любом исходе, включая исключение/откат); job-reaper `_finalizer_owns` обобщён с процесс-локального
ORCH-113 на durable cross-path (defer при живом владельце; Tier-3 backstop игнорирует маркер →
bounded reclaim; реап force-освобождает lease); reconciler F-1 и Plane-webhook (`_try_advance_stage`)
делают **defer** при активном lease.
- **Флаги** (`config.py`, дефолт = боевое): `transition_lease_enabled` (env
`ORCH_TRANSITION_LEASE_ENABLED`; `False` → lease не пишется/не читается, CAS вырождается в прежний
безусловный `update_task_stage` → байт-в-байт до ORCH-114: reaper → ORCH-113 in-memory fallback,
reconciler/webhook skip-guard инертны), `transition_lease_repos` (env `ORCH_TRANSITION_LEASE_REPOS`;
CSV; **пусто → self-hosting only** — где живут необратимые рёбра; зеркало `coverage_gate_repos`,
enduro не затронут). Наблюдаемость — read-only блок `transition_lease` в `GET /queue` +
Telegram-алерт на форсированный/устаревший реклейм + опциональный
`POST /transition-lease/release?work_item=<id>`. Покрытие — `tests/test_orch114_transition_ownership.py`
(TC-01 обязательный регресс класса ORCH-111: красный до фикса, зелёный после; TC-02…TC-14). Детали —
`docs/work-items/ORCH-114/06-adr/ADR-001-transition-ownership-lease-and-stage-cas.md`, сквозной
`docs/architecture/adr/adr-0045-transition-ownership-lease-and-stage-cas.md`.
## Sandbox-only fail-closed изоляция записи в Plane (ORCH-117)
Закрыт корневой класс инцидента **ORCH-114**: тест/worktree-процесс выполнил РЕАЛЬНУЮ запись
(`PATCH …/issues/… state=<Done>` + комментарий «Stage: deploy → done») против **боевого**
Plane-проекта, т.к. тест/staging-процессы наследуют живой боевой Plane-токен
(`PLANE_HEADERS`/`PROJECT_ID` захвачены литералами **на импорте** `plane_sync` — подмена env/токена
постфактум бесполезна, NFR-4) и **ничто** не принуждало их писать только в sandbox. Прямой
прецедент — `tests/conftest.py::_no_telegram` (autouse-глушилка «pytest на проде слал реальные
Telegram-сообщения»); симметричной защиты для Plane-**записи** не было. **Инвариант:** запись в
боевой Plane-проект из любого pytest/worktree-процесса **физически невозможна** независимо от
токена. Аддитивно, never-raise в боевом пути; `STAGE_TRANSITIONS`/реестр `QG_CHECKS`/семантика и
имена `check_*`/machine-verdict-ключи/схема БД — **байт-в-байт не тронуты** (это изоляция клиента
Plane, **не** Quality Gate и **не** стадия).
- **Чокпоинт (D1):** новый чистый leaf `src/plane_write_guard.py` (`decide(project_id, op,
work_item) -> (ALLOW|BLOCK, reason)`, never-raise, по образцу `deploy_status_guard`/`serial_gate`/
`cancel`) врезан в **3 примитива записи** `plane_sync` (`update_issue_state` / `add_comment` /
`_set_issue_state_direct`) **на момент вызова** — сразу после локального `_resolve_project_id` и
**до** любого сетевого шага (ни GET, ни PATCH/POST). Все `set_issue_*`/`notify_*` сводятся к этим
трём примитивам → один гард ловит любой путь, включая будущие.
- **Детект тест-процесса (D2):** `"pytest" in sys.modules` `PYTEST_CURRENT_TEST` (на момент
вызова). Боевой и staging рантаймы — `uvicorn src.main:app`, pytest в процесс **не** импортируют →
гард там строгий **no-op** (NFR-2/NFR-3); worktree `python -m pytest` (инцидентный путь)
гарантированно имеет pytest в `sys.modules` → ловится.
- **Решение (D3):** default-deny. Запись из тест-процесса разрешена ⇔ одновременно (а) opt-in
`plane_test_write_enabled=True` **и** (б) целевой проект ∈ sandbox-allowlist
`plane_test_sandbox_projects` (дефолт = единственный SANDBOX `8c5a3025-4f9d-4190-b79f-fa06276bb27e`).
Нерезолвимый/пустой проект → блок (fail-closed, NFR-1). Боевой проект запрещён **даже при opt-in**
(allowlist sandbox-only). Внутренняя ошибка `decide` в тест-контексте → fail-CLOSED (`guard-error`).
- **Второй слой (D5):** независимый autouse-floor `tests/conftest.py::_plane_sandbox_only` форсит
opt-in OFF для **всего** сьюта (по образцу `_no_telegram`/`_disable_*`); sandbox-e2e ре-энейблит
opt-in в своей фикстуре поверх floor. Два sandbox-bound слоя → нет одиночной точки, чьё выключение
переоткрывает прод.
- **Умышленно БЕЗ kill-switch прод-блока (D4, NFR-6/FR-7, anti-drift):** выключателя,
переоткрывающего прод-запись из pytest, **нет** — единственный реверс — sandbox-bound opt-in. Не
добавлять «общий kill-switch гарда» (реинтродуцирует дефект ORCH-114; reviewer ловит как ≥P1).
- **Аудит (D7):** блок → громкий структурный ERROR (`project_id`/`work_item`/`op`/`reason`:
`prod-project-in-test`/`opt-in-disabled`/`ambiguous-target`/`guard-error`); разрешённая
sandbox-запись → INFO. **Флаги** (`config.py`, дефолты безопасные): `plane_test_write_enabled`
(env `ORCH_PLANE_TEST_WRITE_ENABLED`, дефолт `False`), `plane_test_sandbox_projects` (env
`ORCH_PLANE_TEST_SANDBOX_PROJECTS`, CSV). `scripts/staging_check.py` Block C (E2E в SANDBOX) —
отдельный процесс с собственными httpx-вызовами, гардом не затронут. Покрытие —
`tests/test_orch117_plane_write_isolation.py` (TC-01 — обязательный регресс ORCH-114: красный до
врезки, зелёный после; TC-02…TC-14). Детали —
`docs/work-items/ORCH-117/06-adr/ADR-001-sandbox-only-plane-write-guard.md`, сквозной
`docs/architecture/adr/adr-0046-sandbox-only-plane-write-guard.md`.
## Машинный журнал уроков (ORCH-098)
Шаг 1 («Фундамент», F2) эпика саморазвития: формализует свободнотекстовые «уроки» из `memory/` в
**машинную структурированную таблицу отклонений конвейера** `lessons`, фундамент для будущих
ретроспективщика (E2), приоритизатора RICE (E3) и Стрим. Чистый **observer-leaf** `src/lessons.py`
(never-raise, kill-switch, паттерн `serial_gate`/`coverage_gate`/`metrics`): `record()`/`get()`/
`update()`/`snapshot()`. **Инвариант:** журнал — наблюдатель, **не** Quality Gate; запись урока
никогда не влияет на продвижение по стадиям — `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/
machine-verdict/схемы существующих таблиц байт-в-байт не тронуты.
- **Таблица (D1):** аддитивная идемпотентная `lessons` (`CREATE TABLE IF NOT EXISTS` в `init_db()`,
три индекса) — контекст (`work_item_id`/`task_id`/`stage`/`agent`/`repo`), анализ (`root_cause`/
`suggestion`), статус (`status`/`related_task`), **атрибуция сразу и нуллабельно** (`attribution`/
`target_repo`/`target_domain`, требование Славы 10.06 / NFR-6, заполняется позже через update;
`_ensure_column` форвард-safe на старой таблице) + `source`/`detail`. Без `enum`-констрейнтов —
значения суть forward-compatible слаги. Хелперы `db.record_lesson`/`get_lessons`/`update_lesson`/
`lessons_snapshot`/`lessons_recent_dup_exists`.
- **НЕ скоупится по репо (D2):** в отличие от гейт-leaf'ов (`serial_gate`/`coverage_gate` имеют
`*_repos`, т.к. *действуют* на репо), журнал observer-only → единственный регулятор — глобальный
kill-switch `lessons_enabled` (env `ORCH_LESSONS_ENABLED`, дефолт `True`); **`lessons_repos` НЕ
вводится**. Recorder пишет уроки про **любой** репо (включая enduro-trails — урок ценен для петли);
репо-разрез — на **выборке** (`get(repo=…)`). enduro не затронут (общая БД, аддитивная таблица).
- **Автозапись 4 типов (D3):** тонкие best-effort врезки (`source="auto"`, never-raise, дедуп) —
`gate_failure` (`stage_engine._handle_qg_failure_rollbacks`, откат на `development`), `merge_hold`
(`stage_engine._handle_merge_verify` HOLD-ветка), `transient_retry` (`launcher._finalize_transient`
на **исчерпании** бюджета ретраев, а не на каждом backoff), `deploy_degraded` (post-deploy
`DEGRADED → set_repo_freeze`, урок слоя-3 «деплой OK / прод сломан» ET-8 — `attribution="unknown"`,
классифицируется позже).
- **Дедуп (D4):** для `source="auto"` — один indexed-SELECT по `idx_lessons_wi_type`: дубль с тем же
`(work_item_id, lesson_type, stage)` в окне `lessons_dedup_window_s` (env, дефолт 3600с) → no-op.
`source="manual"` дедуп НЕ проходит (оператор/Стрим всегда пишут).
- **Эндпоинты (D5):** `GET /lessons` (read-only, фильтры `type`/`status`/`repo`/`work_item`/`limit`),
`POST /lessons` (ручная запись, `source="manual"`), `POST /lessons/{id}` (доклассификация/update);
read-only ключ `lessons` в `GET /queue`. Выключенный флаг → `{"enabled": false}`.
- **never-raise (NFR-1):** все публичные функции и врезки изолированы (`try/except` → warning +
безопасный дефолт) — сбой журнала не роняет конвейер. Self-hosting-безопасно: только читает/пишет
свою таблицу, не деплоит/не рестартит прод/не трогает `main`/без процессов/сети. Детали —
`docs/work-items/ORCH-098/06-adr/ADR-001-lessons-journal.md`,
`docs/architecture/adr/adr-0034-lessons-journal.md`.
## Turnkey-онбординг проектов (ORCH-009)
Операторская способность развернуть **новый** проект одним проходом — **вне рантайма и вне
конвейера** (`src/**` байт-в-байт, kill-switch не нужен: активация — только явный запуск CLI
человеком). Три артефакта: **kit** `onboarding/repo-skeleton/` (параметризуемый каркас нового репо:
6 промптов канона 52d/92 — 5 ru + deployer en, паспорт `CLAUDE.md`, `AGENTS.md`, `CONTRIBUTING.md`,
скелет `docs/` с обязательным `operations/INFRA.md`; плейсхолдеры `{{NAME}}`, словарь —
`onboarding/placeholders.json`; **канон не форкается**: `docs/_templates/`+`docs/_standards/`
копируются live из чекаута в момент материализации); **CLI** `scripts/onboard_project.py`
(`plan` — дефолт, GET-only / `apply` — идемпотентный ensure без delete / `verify`): Plane-проект +
22 статуса с точными именами (read-only импорт `plane_sync._PLANE_NAME_TO_KEY`; группы фиксированы
ADR: `STOP`→`cancelled`, терминальные группы только Done/Cancelled/STOP) + лейблы
`autoApprove`/`autoDeploy`/`Bug` → Gitea-репо + per-repo webhook (переиспользует глобальный
`ORCH_GITEA_WEBHOOK_SECRET`) → материализация kit + initial push **только** в свежесозданный пустой
репо → merged-вывод `ORCH_PROJECTS_JSON` (round-trip через фактический `_parse_projects_json`);
скрипт никогда не рестартит прод / не правит `.env` / ничего не удаляет; недоступное в Plane CE
API → `manual-step` (fail-safe); **runbook** `docs/operations/ONBOARDING.md` (ручные шаги: env +
управляемый рестарт; smoke — на staging 8501). Анти-дрейф — структурные тесты
`tests/test_onboarding_{kit,script,invariants}.py`. Детали —
`docs/work-items/ORCH-009/06-adr/ADR-001-turnkey-onboarding-kit-and-cli.md`, сквозной
`docs/architecture/adr/adr-0035-turnkey-project-onboarding.md`.
## Тираж платформы: фундамент 10-common (ORCH-101)
Платформа разворачивается на новой инфре **без правки кода** — только env/конфиг (эпик ORCH-10,
оба типа A Lite / B Bundled, stateless). Принцип: **дефолт каждого параметра = боевому значению**
(пустой `.env` ⇒ поведение байт-в-байт; kill-switch-природа, отдельный флаг не вводится).
`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схема БД — не тронуты.
- **Расхардкод:** ключи `agent_home_dir`/`agent_git_name`/`git_email_domain` (HOME + git-идентичность
акторов: агенты — единый `launcher.agent_git_env()`; системные имена `deploy-finalizer`/
`post-deploy-monitor` — платформенные литералы под тем же доменом), `staging_port`; ссылки
Plane-комментариев — из `gitea_public_url`/`gitea_owner`. `docker-compose.yml` — интерполяция
`${VAR:-default}` (карта `ORCH_HOST_*`/`ORCH_DOCKER_GID`/`ORCH_RUN_UID/GID`; группа ORCH-040
uid/gid/HOME/маунты — одни env насквозь, «МИНА 1» сохранена); `Dockerfile` — `ARG APP_*`
(CMD exec-form 8500 не тронут); deploy-hook — `"${REPO:-…}"` + явная передача `REPO=` обоими
инвокерами. **Платформенные константы (НЕ конфиг):** `SELF_HOSTING_REPO="orchestrator"` (узел
«empty CSV → self-hosting only» всех `*_repos`-leaf'ов), имена сервисов/профиля, контейнерный
layout. **Инвариант ORCH-058 усилен:** guard fail-closed `staging_port == прод-порт` → отказ
freshness-пути ДО любого ssh/build, без тихого fallback.
- **Секреты нового хоста:** stdlib `scripts/gen_secrets.py` (`secrets.token_hex(32)`; печать по
умолчанию; `--write` отказывает при существующем `.env`, перезапись только `--force`); норматив —
боевые секреты не копируются. `.env.example` — канон 100% ключей старта.
- **Smoke тиража:** runbook `docs/operations/REPLICATION.md` (карта env, чек-лист секретов,
пошаговый smoke с PASS/FAIL до артефактов `0104`/`done`, границы 10-common vs Lite vs Bundled).
Анти-регресс — `tests/test_no_host_hardcodes.py` (запрещённые литералы в исполняемом коде
`src/**`+`watchdog/**`; `tokenize`-исключение комментариев/докстрингов; config-модули — канон
дефолтов, вне скана; allowlist пуст). Детали —
`docs/work-items/ORCH-101/06-adr/ADR-001-host-parametrization-secrets-smoke.md`, сквозной
`docs/architecture/adr/adr-0036-replication-foundation-host-parametrization.md`.
## Lite-тираж: орк+watchdog на инфре заказчика (ORCH-102)
Закрыт **Type A** эпика ORCH-10 (поверх фундамента 10-common ORCH-101): заказчик разворачивает
у себя ТОЛЬКО `orchestrator`+`orchestrator-watchdog` и донастраивает окружение
(Plane/Gitea/Telegram/LLM — его инсталляции) по одной сквозной инструкции. **Docs+tests**
(паттерн ORCH-077/092): `src/**`/compose/Dockerfile/`scripts/**` не тронуты; конвейер байт-в-байт.
- **Golden source** — `docs/deployment/LITE_SETUP.md` (новый раздел `docs/deployment/` — витрина
тиража, читатель — внешний оператор; vs `docs/operations/` — эксплуатация НАШЕГО прода): 13
нормативных разделов в порядке маршрута оператора, каждый шаг = fenced-команда + явная
«Проверка:»/PASS/FAIL, хост-специфика только плейсхолдерами; канон не форкается — статусы/env/
вебхуки/smoke ссылками на ONBOARDING §1 / REPLICATION §2§4 / SETUP_WEBHOOKS (явно в доке —
только fail-closed имена `Confirm Deploy`/`STOP` и обязательные ключи нового хоста).
- **Канон watchdog-конфига** — новый `.env.watchdog.example` (key-set = блок `WATCHDOG_*`
`.env.example`, держится key-sync тестом): sidecar читает ТОЛЬКО `.env.watchdog`, ключ
`WATCHDOG_*` в `.env` для него инертен (ловушка файла-носителя закрыта); C-1 ORCH-100 — свой
бот, токен орка не переиспользовать; `.env.watchdog` в `.gitignore`.
- **Нормативы:** Gitea — branch protection на `main` НЕ включать (ADR D10 ORCH-009 / INV-4),
pre-receive не вводится, ОДИН глобальный webhook-секрет; compose НЕ форкается (дефолтный
`up -d` = ровно орк+watchdog, staging строго за `profiles: [staging]` — вилка только под
self-hosting развитие платформы); stateless — данные/задачи/секреты боевого хоста НЕ
переносятся, проверка чистоты через `GET /queue`.
- **Анти-дрейф** — `tests/test_lite_setup_doc.py` (структурный, без сети/LLM/subprocess):
13 разделов в порядке, кирпичи, env-ключи ⊂ `.env.example`, compose-подмножество
(анти-появление `plane*`/`gitea*`), fenced-скан `FORBIDDEN` (импорт из
`test_no_host_hardcodes.py`) + секрет-эвристика, «22 статуса» сверкой импорта
`plane_sync._PLANE_NAME_TO_KEY`, перекрёстность REPLICATION→LITE_SETUP. **Норматив
сопровождения (NFR-5):** меняешь шаги тиража → обнови LITE_SETUP.md в том же PR. Детали —
`docs/work-items/ORCH-102/06-adr/ADR-001-lite-setup-doc-canon.md`, сквозной
`docs/architecture/adr/adr-0037-lite-replication-canon.md`.
## Bundled-тираж: весь стек одним комплектом (ORCH-103)
Закрыт **Type B** эпика ORCH-10 (поверх 10-common ORCH-101 и канона Lite ORCH-102): заказчик
**без собственной инфраструктуры** получает весь стек одним комплектом — новый top-level каталог
**`deploy/bundled/`** (самодостаточный compose: орк + watchdog + Gitea + зеркало upstream Plane CE
≈14 контейнеров; project name `orchestrator-bundle` = узнаваемый префикс томов/контейнеров;
`container_name` не пиннится; staging-контура нет вовсе — самразвитие платформы у заказчика =
маршрут Lite) + **`scripts/bootstrap_bundle.py`** (python stdlib-only, режимы `plan` (дефолт) /
`apply`/`verify`, step-движок check→ensure, exit 0/2/1), доводящий стек одним прогоном: preflight
(fail-fast до мутаций) → секреты (webhook — строго `gen_secrets.py`; bundle-креды — stdlib
`secrets`, без перетирания без `--force-secrets`) → up+готовность → init Gitea (полностью
автоматом, `gitea admin …`; branch protection НЕ включается — D10 ORCH-009/INV-4) → init Plane
(честные manual-step c API-верификацией; молчаливый пропуск запрещён) → онбординг sandbox-проекта
**строго** `onboard_project.py apply+verify` (22 статуса — `plane_sync._PLANE_NAME_TO_KEY`, нулевой
дрейф канона) → git-доступ агентов HTTP token-remote (ssh-контур не вводится) → сборка корневых
`.env`/`.env.watchdog` (bootstrap — единственный писатель live-конфигов) → health/итог.
Сеть — одна bridge, машинный трафик строго сервис-DNS (`http://orchestrator:8500/webhook/*`),
наружу — только человеческие порты (`BUNDLE_ORCH_PORT`/`BUNDLE_PLANE_PORT`/`BUNDLE_GITEA_HTTP_PORT`);
мина Gitea закрыта `GITEA__webhook__ALLOWED_HOST_LIST=orchestrator`. Все сторонние образы пиннованы
неподвижными тегами; teardown — только документированная процедура BUNDLED_SETUP §13 (delete-операций
в скрипте НЕТ вообще). Рантайм байт-в-байт: `src/**`, корневой compose, `Dockerfile`,
`STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД — не тронуты; kill-switch не нужен (активация — только
явный запуск оператором, паттерн ORCH-009/102). Golden source — `docs/deployment/BUNDLED_SETUP.md`
(14 разделов канона LITE_SETUP; общие шаги — ссылками на LITE_SETUP/ONBOARDING/REPLICATION).
Анти-дрейф — `tests/test_bundle_compose.py` (состав/пины/key-set-sync/заморозка корневого compose),
`tests/test_bundled_setup_doc.py` (разделы/FORBIDDEN-импорт/секрет-эвристика/env-ключи/кросс-рефы),
`tests/test_bootstrap_script.py` (кирпичи/stdlib-only ast-сканом/нет delete-операций/unit чистых
функций). **Норматив сопровождения (NFR-5):** меняешь шаги Bundled-тиража → обнови BUNDLED_SETUP.md
в том же PR. Детали — `docs/work-items/ORCH-103/06-adr/ADR-001-bundled-stack-compose-and-bootstrap.md`,
сквозной `docs/architecture/adr/adr-0038-bundled-replication-canon.md`.
## Конвенции
- Conventional Commits (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`)
- Ветки: `feature/ORCH-NNN-slug`, `fix/ORCH-NNN-slug`
- ADR per work-item: `docs/work-items/<plane-id>/06-adr/ADR-NNN-slug.md`
- Global ADR (сквозные решения): `docs/architecture/adr/adr-NNNN-slug.md`
- Work items: `docs/work-items/<plane-id>/`
- Машинные вердикты Quality Gate — строго YAML-frontmatter (`verdict:`, `deploy_status:`, `staging_status:`, `security_status:`), никогда проза. **ORCH-52c (ORCH-076):** парсинг frontmatter сведён к единому контракту `src/frontmatter.py` (reader `read_frontmatter_value` — BC; единый парс-примитив `parse_frontmatter`; writer `render/write_frontmatter`; валидатор схемы `validate_schema`/`REQUIRED_FIELDS` — warning-only по умолчанию, hard-fail только под kill-switch `frontmatter_validation_strict`, дефолт `False`). Пять вердикт-парсеров (`check_reviewer_verdict`, `_parse_tests_verdict`, `_parse_deploy_status`, `_parse_staging_status`, `parse_security_status`) читают через ОДНУ точку парсинга; семантика вердиктов и `STAGE_TRANSITIONS`/состав `QG_CHECKS` — 1:1. Формальная спека «стадия → обязательный выход» + обязательная frontmatter-схема — `docs/_standards/HANDOFF_PROTOCOL.md`
## Артефакты задачи (`docs/work-items/<plane-id>/`)
`00-business-request.md`, `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`, `04-test-plan.yaml`, `06-adr/ADR-NNN-slug.md`, `07-infra-requirements.md`, `08-data-requirements.md`, `10-tech-risks.md`, `12-review.md`, `13-test-report.md`, `14-deploy-log.md`, `15-staging-log.md`, `16-post-deploy-log.md` (post-deploy наблюдение, ORCH-021), `17-security-report.md` (security-гейт: `security_status:`/secrets/deps, ORCH-022), `18-coverage-report.md` (coverage-гейт: `coverage_status:`/measured/baseline, ORCH-027).
**Стандарт документов (ORCH-075, ORCH-52b):** структура каждого дока, карта «стадия→агент→документ→гейт→machine-key» и конвенция ADR-naming зафиксированы в `docs/_standards/PIPELINE_DOCS.md` (golden source); копируемые скелеты — в `docs/_templates/`. Перед написанием номерного дока бери скелет из `docs/_templates/` и не меняй имя machine-key frontmatter (регистр чувствителен — иначе гейт упадёт ложно).
## Правила для агентов
1. Перед любым действием прочесть этот файл и `docs/architecture/README.md`.
2. **Документация = golden source наравне с кодом.** Изменил функционал → обнови доку В ТОМ ЖЕ PR. Архитектурное решение → заведи ADR (формат — `docs/_standards/PIPELINE_DOCS.md` §4). Структура номерных доков и шаблоны — `docs/_standards/PIPELINE_DOCS.md` + `docs/_templates/`. Обнови `CHANGELOG.md`. **Витрина системы `docs/overview/` (ORCH-011):** изменил функциональность платформы → обнови витрину в том же PR (какой файл какому классу изменений — таблица в индексе витрины); машинно-проверяемые факты витрины держит `tests/test_system_docs.py`.
3. Никогда не править артефакты других этапов.
4. Никогда не комментировать ТЗ задним числом — если ТЗ не годится, возвращай в Анализ.
5. Никогда не закрывать задачу самостоятельно — это делает CI / финальная стадия.
6. **Reviewer проверяет: обновлена ли документация. Нет → REQUEST_CHANGES.** Это включает **обзорные доки** (ORCH-079, 52f — финал эпика 52): если PR закрывает пункт `README.md` «Известные ограничения», но README не обновлён → finding ≥P1 (витрина проекта не должна выдавать решённое за открытое). Та же ось покрывает витрину системы (ORCH-011): PR меняет функциональность, описанную в `docs/overview/`, а витрина не обновлена → finding ≥P1.
7. Не использовать `--no-verify` без явного одобрения Owner.
8. Секреты — только в `.env`/`.env.staging` на хосте, в гит НЕ коммитятся (канон — `.env.example`).
9. **Трассировка маркеров (ORCH-078, ORCH-52e):** правишь строку/блок с маркером `ORCH-NNN` →
ПЕРЕД изменением прочитай его `docs/work-items/ORCH-NNN/06-adr/` и не сломай зафиксированный
инвариант; блок с 3+ маркерами → опирайся на сводный сквозной ADR. Стандарт маркеров (формат,
размещение, fallback-доступ, анти-археология, каноничное правило чтения) — `docs/_standards/TRACEABILITY.md`.
## ⚠️ Self-hosting — оркестратор правит САМ СЕБЯ
Задачи проекта ORCH меняют инструмент, который СЕЙЧАС работает в продакшене и обслуживает ДРУГИЕ проекты (enduro-trails) из ОДНОГО инстанса с ОБЩЕЙ БД и общей очередью.
- **НЕ перезапускать / не ронять прод-контейнер** `orchestrator` в рамках задачи — встанет конвейер всех проектов.
- Любой деплой/рестарт self = групповой риск. Детали и топология — `docs/operations/INFRA.md`.
- Стадия `deploy-staging` (порт 8501) — обязательная страховка перед прод-деплоем орка.
- Прод-деплой орка запускается ТОЛЬКО переводом задачи на стадии `deploy` в выделенный
Plane-статус **«Confirm Deploy»** (ORCH-059). Статус `Approved` — человеческий гейт
конвейера и прод-деплой НЕ запускает (на `deploy` — no-op). Это разделяет «одобрить
артефакт» и «выкатить в прод», чтобы привычный approve не ронял прод случайным кликом.
---
*Паспорт проекта orchestrator. Поддерживается агентами при каждой доработке. Изолирован: описывает только этот проект (канон per-repo, см. ORCH-9).*