# 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`.

## Детерминированный staging-раннер вместо LLM-деплойера (ORCH-115)
Первый реализованный срез determinization-roadmap (ORCH-118 A6, `replace-deterministic-now`): на
стадии `deploy-staging` для self-hosting `orchestrator` **LLM-агент `deployer` заменён
детерминированным кодом** (`src/staging_runner.py`). Работа агента на этой стадии была чисто
механической (запуск staging-сюиты + маппинг exit-кода) — теперь её делает leaf, перехватываемый в
`launch_job` **до `_spawn`** (прецедент `deploy-finalizer`/`post-deploy-monitor`,
`launcher.py:389/394`). **Инвариант (NFR-1):** замена *продюсера* артефакта, **не** гейта —
контракт `15-staging-log.md`, гейт/`_parse_staging_status`/имя `check_staging_status`,
`STAGE_TRANSITIONS`, machine-verdict `staging_status:`, схема БД — **байт-в-байт не тронуты**.
Аддитивно, под kill-switch, never-raise, fail-closed.
- **Перехват (D1):** `launch_job` — `if job.agent=="deployer" and staging_runner.should_intercept(job)`
  → `_run_staging_runner_job` (зеркало `_run_deploy_finalizer_job`): синхронно ведёт `jobs`-строку
  через `mark_job`, возвращает `None` (нет `agent_runs`). Дискриминатор «staging vs prod» —
  **стадия задачи** `deploy-staging` (роль `deployer` общая для `deploy-staging`/`deploy`), не имя
  роли; `should_intercept` never-raise → `False` → штатный `_spawn` (fail-safe к LLM-пути).
- **Раннер (D2-D7):** leaf по образцу `self_deploy`/`proc_group`/`staging_verdict` (на импорте только
  `config`/`proc_group`; `db`/`git_worktree`/`stage_engine`/`qg.checks`/`notifications` — лениво).
  Исполняет ту же сюиту (`docker exec orchestrator-staging python3 .../staging_check.py --base-url
  http://localhost:8501 --mode stub`, арги из config — ORCH-101) через `proc_group` (tree-kill,
  таймаут `staging_runner_timeout_s=600`); маппит exit-код **единым** контрактом
  `self_deploy.map_exit_code_to_status` (`0→SUCCESS`/иначе/None→`FAILED`; ORCH-061 infra-tolerance
  остаётся внутри `staging_check.py`, раннер не пересуживает); пишет `15-staging-log.md`
  (`author_agent: staging-runner`/`model_used: n/a`, 52c-схема) + best-effort push в **фичеветку**
  (не в `main` — гейт читает worktree первым, усиливает AC-8); вызывает **существующий**
  `advance_stage(current_stage="deploy-staging", finished_agent="deployer")` — без новых
  рёбер/исходов (lease ORCH-114 берётся внутри `advance_stage`, раннер не трогает).
- **Двухуровневый исход (D5, анти-ORCH-110):** сюита **исполнилась** (реальный exit-код) → verdict→
  advance (FAILED → тот же откат `deploy-staging → development` + developer-retry, что у LLM); сюита
  **не исполнилась** (tool-error: spawn-error/таймаут/`returncode None`) → инфра-сбой ≠ код-фейл →
  bounded **DEFER** (re-queue `deployer`-джоба + restart-safe маркер `staging-runner infra-retry` в
  `task_content`, без правки схемы БД), на исчерпании `staging_runner_infra_max_retries` → fail-closed
  `FAILED` + advance + alert. Никогда тихий advance/ложный green; не клинит очередь; не жжёт
  developer-retry на транзиентной инфре.
- **Флаги** (`config.py`, дефолт = боевое): `staging_runner_enabled` (kill-switch, env
  `ORCH_STAGING_RUNNER_ENABLED`), `staging_runner_repos` (CSV; **пусто → self-hosting only**),
  `staging_runner_timeout_s=600`, `staging_runner_infra_max_retries=2`,
  `staging_runner_infra_retry_delay_s=30`. Откат = `ORCH_STAGING_RUNNER_ENABLED=false` → на
  `deploy-staging` снова LLM-`deployer` через `_spawn` **байт-в-байт**. Наблюдаемость — in-process
  счётчики + read-only блок `staging_runner` в `GET /queue` + один структурный лог-вердикт на прогон
  (различает код-фейл и tool-error). Норматив сопровождения ORCH-118: обновлены
  `llm-call-sites.md`/`llm-determinization-roadmap.md`/`llm-usage-policy.md` (A6 — реализован, машинные
  блоки/инвариант «единственный транспорт S0» целы) + `deployer.md` (LLM-ветвь — fallback). Покрытие —
  `tests/test_orch115_staging_runner.py` (TC-01…TC-13) + зелёные LLM-анти-дрейф тесты (TC-14). Детали —
  `docs/work-items/ORCH-115/06-adr/ADR-001-deterministic-staging-runner.md`, сквозной
  `docs/architecture/adr/adr-0048-deterministic-staging-runner.md`.
- **ORCH-123 (host-side исполнение + классификация environment-дефекта, bug→escalate full-cycle,
  adr-0049):** фикс инцидента **ORCH-116** — раннер ORCH-115 вызывал `docker exec` **изнутри**
  прод-контейнера, где **нет docker CLI** (`Dockerfile:11` несёт только `openssh-client git curl`;
  `docker.sock` смонтирован, клиента нет) → `FileNotFoundError` → постоянный environment-дефект
  ложно маршрутизировался как код-фейл-откат `deploy-staging → development` (с расходом
  developer-retry); любая self-hosting задача на `deploy-staging` была обречена. Аддитивно, под
  флагами, never-raise, скоуп self-hosting; `STAGE_TRANSITIONS`/реестр `QG_CHECKS`/семантика и имена
  `check_staging_status`/`_parse_staging_status`/machine-verdict-ключи/схема БД — **байт-в-байт не
  тронуты** (замена *стратегии исполнения продюсера*, **не** гейта; зеркало ORCH-115 NFR-1).
  **(D1)** `build_staging_command` обёртывает ту же `docker exec orchestrator-staging … staging_check.py
  … --mode stub` в `ssh -o StrictHostKeyChecking=no <deploy_ssh_user>@<deploy_ssh_host> '<…>'`
  (зеркало `self_deploy`/`image_freshness`; канал доверенный `ORCH_DEPLOY_SSH_HOST=127.0.0.1`,
  ssh-ключ `:ro`, `openssh-client` в образе — новых секретов/привилегий нет; сюита по-прежнему бежит
  **внутри** `orchestrator-staging` 8501, меняется лишь инициатор `docker exec`). **(D2 security)**
  docker CLI/SDK в контейнер **не** добавляется, `docker.sock` **не** используется изнутри (это
  root-эквивалентное расширение поверхности атаки, доступное и LLM-агентам). **(D3)** двухуровневый
  исход ORCH-115 заменён **трёхсторонней** чистой `classify_staging_outcome` (зеркало
  `merge_gate.classify_retest_failure`): `suite-ran` (распознанный exit-код ≠255 **без** env-маркера в
  stderr → доверяем коду `0→SUCCESS`/`≠0→FAILED`; анти-over-tolerance BR-3), `permanent-env`
  (spawn-error `rc=None`/нет ssh-target/`rc∈{126,127}`/env-маркер `No such container`/`Cannot connect
  to the Docker daemon` → ретрай бессмыслен), `transient-infra` (timeout/ssh `rc=255`/неизвестно →
  ретрай осмыслен); дизамбигуация `exit=1`-коллизии — скан stderr на env-маркеры, не голый код;
  fail-safe → `transient-infra`. **(D4 инвариант «инфра ≠ код-фейл»)** `permanent-env` → немедленный
  **infra-HOLD** (DEFER пропускается, `15-staging-log.md` НЕ пишется, advance НЕ зовётся, developer-retry
  НЕ жжётся, alert «НЕ дефект кода»); `transient-infra` → bounded DEFER, на исчерпании — тоже
  infra-HOLD+alert (**супершид ORCH-115 D5** fail-closed-FAILED-отката). «Сюита **не** исполнилась»
  (env ИЛИ инфра) **никогда** не оканчивается код-фейл-откатом — закрывает RCA-класс ORCH-110 на
  staging-ребре; задача держится на `deploy-staging` (reconciler `advance_if_gate_passed` на red-гейте
  → `None`, без отката, R-2). **(D5)** `preflight()` пробит host-side канал на старте `main.lifespan`
  (best-effort, never-blocks). **(D6)** новый флаг `staging_runner_exec_host_side=True`
  (env `ORCH_STAGING_RUNNER_EXEC_HOST_SIDE`); откат — `=false` (in-container 1:1) или
  `ORCH_STAGING_RUNNER_ENABLED=false` (LLM-deployer 1:1). **(D8)** счётчик `permanent_env` + `exec_host_side`
  + `preflight` в блоке `staging_runner` `GET /queue`. Покрытие — `tests/test_orch123_staging_runner_exec.py`
  (TC-01 обязательный регресс ORCH-116 red→green; TC-02…TC-14 + R-2) + зелёный анти-дрейф ORCH-115.
  Детали — `docs/work-items/ORCH-123/06-adr/ADR-001-host-side-staging-execution-and-env-classification.md`,
  сквозной `docs/architecture/adr/adr-0049-host-side-docker-execution-boundary.md`.

## Детерминированный test-раннер вместо LLM-тестера (ORCH-116)
Второй реализованный срез determinization-roadmap (ORCH-118 A5, `needs-hybrid-fallback`): на стадии
`testing` для self-hosting `orchestrator` **LLM-агент `tester` заменён детерминированным кодом**
(`src/test_runner.py`). PASS/FAIL-ядро было деривируемо (регресс `pytest` + read-only smoke) — теперь
его делает leaf, перехватываемый в `launch_job` **до `_spawn`** (прецедент `deploy-finalizer`/
`post-deploy-monitor`/`staging-runner`, `launcher.py:397/402/405`). **Инвариант (NFR-1):** замена
*продюсера* артефакта, **не** гейта — контракт `13-test-report.md`, гейт/`_parse_tests_verdict`/имя
`check_tests_passed`, `STAGE_TRANSITIONS`, machine-verdict `result:` (+ legacy `verdict:`/`status:`),
схема БД — **байт-в-байт не тронуты**. Аддитивно, под kill-switch, never-raise, fail-closed, гибрид
(LLM строго off-control-path).
- **Перехват (D1):** `launch_job` — `if job.agent=="tester" and test_runner.should_intercept(job)`
  → `_run_test_runner_job` (зеркало `_run_staging_runner_job`): синхронно ведёт `jobs`-строку через
  `mark_job`, возвращает `None` (нет `agent_runs`). Дискриминатор — роль `tester` **И** стадия задачи
  `testing` (defense-in-depth: `tester` — единственный агент входа в `testing`, коллизии стадий нет, в
  отличие от общей роли `deployer`/ORCH-115) **И** `applies(repo)`; `should_intercept` never-raise →
  `False` → штатный `_spawn` (fail-safe к LLM-пути).
- **Раннер (D2-D7):** leaf по образцу `staging_runner`/`self_deploy`/`proc_group` (на импорте только
  `config`/`proc_group`; `db`/`git_worktree`/`self_deploy`/`qg.checks`/`stage_engine`/`notifications`
  — лениво). `applies` = kill-switch `test_runner_enabled` + скоуп `test_runner_repos` (пусто →
  self-hosting only) **И** резолв тест-контракта `_has_test_contract` (BR-9: репо без контракта →
  LLM-tester, enduro-trails 1:1 даже если руками в CSV). Исполняет `python -m pytest
  <test_runner_target>` **в worktree ветки** (анти checkout-гонка) через `proc_group`
  (tree-kill, таймаут `test_runner_timeout_s=900`) + опц. read-only smoke (`/health`/`/status`/`/queue`
  + блок `serial_gate`, stdlib `urllib`; транзиент → ограниченный ретрай, не-200/нет блока → немедленный
  FAIL; `test_runner_smoke_enabled`); маппит exit-код **единым** `self_deploy.map_exit_code_to_status`
  в токенах `result:` (`0→PASS`/иначе/None→`FAIL`, fail-closed; smoke-провал AND-ится в `FAIL`); пишет
  `13-test-report.md` (`author_agent: test-runner`/`model_used: n/a`, 52c-схема) + best-effort push в
  фичеветку; вызывает **существующий** `advance_stage(current_stage="testing", finished_agent="tester")`
  — без новых рёбер/исходов (lease ORCH-114 берётся внутри `advance_stage`).
- **Анти-коллизия 52c-`status:` ↔ парсер (D6.1, специфично для tester):** `_parse_tests_verdict` читает
  вердикт из **трёх** равноранговых полей (`verdict:`/`status:`/`result:`) с negative-token-priority →
  52c-обязательное `status:` раннера **ВСЕГДА выровнено** по вердикту (`PASS → status: success`, `FAIL
  → status: failed`), иначе негативный токен в `status:` при `result: PASS` дал бы ложный FAIL. Прибито
  unit-тестом через неизменённый парсер.
- **Двухуровневый исход (D5, анти-ORCH-110):** сюита **исполнилась** (реальный exit-код) → verdict→
  advance (FAIL → тот же откат `testing → development` + developer-retry, что у LLM); сюита **не
  исполнилась** (tool-error: spawn-error/таймаут/`returncode None`) → инфра-сбой ≠ код-фейл → bounded
  **DEFER** (re-queue **`tester`**-джоба + restart-safe маркер `test-runner infra-retry` в
  `task_content`, без правки схемы БД), на исчерпании `test_runner_infra_max_retries` → fail-closed
  `FAIL` + advance + alert. Никогда тихий advance/ложный green; не клинит очередь; не жжёт
  developer-retry на транзиентной инфре.
- **Флаги** (`config.py`, дефолт = боевое): `test_runner_enabled` (kill-switch, env
  `ORCH_TEST_RUNNER_ENABLED`), `test_runner_repos` (CSV; **пусто → self-hosting only**),
  `test_runner_target=tests/`, `test_runner_timeout_s=900`, `test_runner_smoke_enabled`,
  `test_runner_infra_max_retries=2`, `test_runner_infra_retry_delay_s=30`. Откат =
  `ORCH_TEST_RUNNER_ENABLED=false` → на `testing` снова LLM-`tester` через `_spawn` **байт-в-байт**.
  Наблюдаемость — in-process счётчики + read-only блок `test_runner` в `GET /queue` + один структурный
  лог-вердикт на прогон (различает код-фейл и tool-error). **Гибрид (BR-8/NFR-7):** LLM строго
  off-control-path — детерминированный раннер единственный продюсер `result:`; будущий триаж падений не
  выносит/не переопределяет вердикт и не добавляет ребро в `STAGE_TRANSITIONS` (Phase 1 не реализован).
  Норматив сопровождения ORCH-118: обновлены `llm-call-sites.md`/`llm-determinization-roadmap.md`/
  `llm-usage-policy.md` (A5/rank 2 — реализован, машинные блоки/инвариант «ровно один `first_slice`»
  целы) + `tester.md` (LLM-ветвь — fallback). Покрытие — `tests/test_orch116_test_runner.py`
  (TC-01…TC-14) + зелёные LLM-анти-дрейф тесты (TC-15). Детали —
  `docs/work-items/ORCH-116/06-adr/ADR-001-deterministic-test-runner.md`, сквозной
  `docs/architecture/adr/adr-0050-deterministic-test-runner.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 до артефактов `01–04`/`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).*
