---
work_item: ORCH-103
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-11
model_used: claude-opus-4-8
---

# ADR-001: Bundled-тираж (Type B) — bundle-compose всего стека + bootstrap-канон

Work Item: **ORCH-103** — ORCH-10b Bundled-тираж: весь стек одним комплектом + bootstrap-скрипт
Стадия: **architecture**
Сквозная регистрация: **`docs/architecture/adr/adr-0038-bundled-replication-canon.md`**
(закрывает Type B эпика ORCH-10; вводит новый top-level каталог `deploy/` и нормативы,
обязательные для будущих задач тиража).

## Статус
Proposed

## Контекст

Эпик ORCH-10 (D5 «Масштаб»), тип **B — Bundled**: заказчик без собственной инфраструктуры
получает **весь стек одним комплектом** (орк + watchdog + Gitea + Plane CE) + bootstrap,
доводящий стек до рабочего конвейера одним запуском. Факты, сверенные с репо:

- **Корневой `docker-compose.yml` заморожен анти-дрейфом** (`tests/test_lite_setup_doc.py::
  test_compose_services_are_exactly_the_lite_set` + `test_compose_has_no_plane_or_gitea_services`):
  ровно 3 сервиса, подстроки `plane`/`gitea` запрещены → bundle обязан быть **отдельным файлом**.
- **Кирпичи уже в `main`:** `scripts/gen_secrets.py` (webhook-секреты, `--write PATH`, exit 0/2),
  `scripts/onboard_project.py` (`plan`/`apply`/`verify`, 22 статуса из
  `plane_sync._PLANE_NAME_TO_KEY`, шаг `plane.workspace-webhook` — уже **MANUAL** в его отчёте,
  token-push `_push_url`, exit 0/2/1; запуск — host-venv, `docs/operations/ONBOARDING.md`),
  smoke `docs/operations/REPLICATION.md` §4, док-канон `docs/deployment/LITE_SETUP.md` (ORCH-102).
- **Хост-параметризация закрыта ORCH-101** (adr-0036): `src/**` без хост-литералов
  (`tests/test_no_host_hardcodes.py`, FORBIDDEN = `82.22.50.71`/`/home/slin`/`mva154`/`duckdns`);
  internal/public split URL уже в конфиге (`ORCH_GITEA_URL`≠`ORCH_GITEA_PUBLIC_URL`,
  `ORCH_PLANE_API_URL`≠`ORCH_PLANE_WEB_URL`).
- **`.gitignore` уже неякорный** для `.env`, `data/`, `.env.watchdog` — вложенные копии этих имён
  игнорируются на любом уровне без правок.
- **Claude CLI не запекается** в образ (маунты `ORCH_HOST_CLAUDE_*`) — внешнее предусловие хоста
  заказчика; bundle его не поставляет (решение Владельца, BRD §1.3).

ТЗ (02-trz.md §9) оставило архитектору OQ-1…OQ-7. Ниже — пакет решений D1…D11.

## Решение

### Сводка

Bundled-комплект живёт в новом top-level каталоге **`deploy/bundled/`**: самодостаточный
compose-файл всего стека (зеркало официального Plane CE selfhost-référence + Gitea + орк +
watchdog, пиннинг неподвижными тегами) на **одной bridge-сети** с сервис-DNS для машинного
трафика и публикацией только «человеческих» портов. **`scripts/bootstrap_bundle.py`**
(python stdlib, режимы `plan`/`apply`/`verify`, step-движок check→ensure, exit 0/2/1) доводит
стек: preflight → секреты → up → init Gitea (полностью автоматом) → init Plane (честные
manual-step с верификацией) → онбординг sandbox-проекта **строго** через `onboard_project.py` →
git-доступ агентов token-remote → сборка `.env`/`.env.watchdog` орка → health/итог. Teardown —
только документированная процедура. Рантайм байт-в-байт: `src/**`, корневой compose,
`Dockerfile`, `STAGE_TRANSITIONS`/`QG_CHECKS`/machine-verdict/схема БД — не тронуты (NFR-1);
kill-switch не нужен — активация только явным запуском оператора на целевом хосте
(паттерн ORCH-009/102).

### D1 — Расположение и изоляция: `deploy/bundled/`, project name `orchestrator-bundle` (OQ-1)

- Новый top-level каталог **`deploy/`** — «дистрибутивы развёртывания» (исполняемые комплекты;
  семантика дополняет `docs/deployment/` — инструкции). Bundle: **`deploy/bundled/docker-compose.yml`**,
  один **самодостаточный** файл (include-композиция отвергнута — см. Альтернативы).
- Top-level **`name: orchestrator-bundle`** в compose: project name фиксирован → все тома/контейнеры
  получают узнаваемый префикс `orchestrator-bundle_*`/`orchestrator-bundle-*` (требование TC-04
  тест-плана «узнаваемый bundle-префикс»; preflight bootstrap детектирует «грязный хост» по этому
  префиксу). **`container_name` не пиннится ни у одного сервиса** — установка Lite и bundle на одном
  хосте не сталкивается по именам с корневым compose (у которого `container_name: orchestrator`
  закреплён).
- **Staging-контур орка в bundle отсутствует вовсе** (ни сервисом, ни профилем): заказчик Type B
  эксплуатирует платформу для СВОИХ проектов, а не развивает её self-hosting'ом; репо `orchestrator`
  в bundle-инсталляции **не регистрируется** как проект → вся self-deploy-машинерия
  (`SELF_HOSTING_REPO="orchestrator"`-леафы, freshness, serial-gate freeze) структурно спит.
  Нужен self-hosting у заказчика → это маршрут Lite/корневого compose (LITE_SETUP §9), не bundle.
- Имена сервисов: `orchestrator`, `orchestrator-watchdog` — платформенные конвенции (ORCH-101 D3);
  `gitea`; Plane-стек — **upstream-имена** сервисов (минимальный дифф к référence, D3 ниже).
  Запрет `plane*`/`gitea*` касается ТОЛЬКО корневого compose — на bundle-файл не распространяется.

### D2 — Конфиг-слои: три файла, один писатель (OQ-1, FR-1/FR-3)

| Файл | Роль | В гите |
|------|------|--------|
| `deploy/bundled/.env.example` | **bundle-конфиг-канон**: 100% ключей bundle-инфры (публичный хост, карта портов, uid/gid, пути Claude CLI, плейсхолдеры внутренних кред Plane/Gitea) | да (только плейсхолдеры/нейтральные дефолты) |
| `deploy/bundled/.env` | live bundle-конфиг; **авто-читается compose** из project dir — все `docker compose -f deploy/bundled/docker-compose.yml …` работают без флагов | нет (покрыт неякорным `.env` в `.gitignore`) |
| корневые `.env` / `.env.watchdog` | runtime-конфиг орка и watchdog — **ровно канон Lite** (REPLICATION §2 / `.env.example` / `.env.watchdog.example` применимы 1:1); в bundle-compose подключаются `env_file: ../../.env` (`required: false` — см. ниже) | нет (уже в `.gitignore`) |

- **Отвергнут** отдельный live-файл с обязательным `--env-file`: оператор неизбежно наберёт голую
  compose-команду без флага → интерполяции молча упадут в дефолты → пересоздание контейнеров с
  чужими портами/путями (труднодиагностируемый класс R-4). Авто-`.env` в project dir — fail-safe
  по построению.
- **`env_file` орка/watchdog — `required: false`** (паттерн уже в корневом compose у watchdog):
  первый `up -d` поднимает ВЕСЬ стек до того, как конфиг орка собран (AC-1 «одна команда»); орк
  без конфига жив (`/health` отвечает), bootstrap пересоздаёт его после сборки env (шаг 8 D5).
- **Bootstrap — единственный писатель** `deploy/bundled/.env` (дозапись сгенерённых кред),
  корневого `.env` и `.env.watchdog` на целевом хосте: дублируемые между слоями ключи
  (`ORCH_AGENT_HOME_DIR`, порт орка) когерентны механически, а не дисциплиной оператора.
  Права `600`; повторный запуск НЕ перетирает существующие значения без явного флага
  (паттерн `--force` `gen_secrets.py`).
- **Неймспейсы ключей:** один факт = одно имя (ORCH-101 D1) — существующие факты переиспользуют
  существующие имена (`ORCH_RUN_UID/GID`, `ORCH_DOCKER_GID`, `ORCH_AGENT_HOME_DIR`,
  `ORCH_HOST_CLAUDE_CODE_DIR`/`ORCH_HOST_NODE_BIN`/`ORCH_HOST_CLAUDE_DIR`/`ORCH_HOST_CLAUDE_JSON`);
  bundle-only факты — префикс **`BUNDLE_*`** (`BUNDLE_PUBLIC_HOST`, `BUNDLE_PLANE_PORT`,
  `BUNDLE_GITEA_HTTP_PORT`, `BUNDLE_ORCH_PORT`); внутренние креды Plane-стека — **upstream-имена**
  Plane CE (значения генерирует bootstrap). Точный состав финализирует developer; форму держит
  key-set-sync тест: **каждая `${VAR}`-интерполяция bundle-compose имеет ключ в
  `deploy/bundled/.env.example`** (паттерн `.env.watchdog.example`, D5 ORCH-102).
- **Дефолты bundle-compose нейтральны** (FORBIDDEN-литералов нет — TC-06 распространяет скан на
  bundle-артефакты): HOME акторов в bundle — `/home/orchestrator` (значение в `.env.example`
  bundle, обе стороны группы ORCH-040 двигаются одной переменной — инвариант сохранён);
  uid/gid/доки docker-gid заполняет bootstrap из `id -u`/`id -g`/`getent group docker`.
- Каталоги данных орка — bind внутри project dir: `./data` → `deploy/bundled/data` (покрыт
  неякорным `data/` в `.gitignore`), `./repos` → `deploy/bundled/repos` (**добавить в
  `.gitignore`**: `deploy/bundled/repos/`); bind, а не named volume — те же uid-причины, что в
  корневом compose (ORCH-040: named volume создаётся root-owned, контейнер бежит под uid
  оператора). Состояние Plane/Gitea (postgres/redis/mq/minio/gitea-data) — **именованные тома**
  проекта (root-владение для них нормально: процессы своих образов).

### D3 — Состав стека и пиннинг: зеркало upstream, неподвижные теги литералом (OQ-2)

- **Plane CE** — зеркало официального selfhost-compose Plane CE (web/space/admin/api/worker/
  beat-worker/migrator/live + postgres/redis/mq/minio/proxy, ≈13–14 сервисов по факту référence
  на момент пиннинга). Структура сервисов/env-контракт — upstream-имена (анти-дрейф к их докам;
  своя «переписанная» топология Plane = неоплачиваемый долг сопровождения).
- **Gitea** — официальный `gitea/gitea` (НЕ rootless: rootless усложняет ssh/тома, а ssh-контур
  и так не вводится — D8).
- **Пиннинг: точный неподвижный тег литералом в compose** (`image: <repo>:<x.y.z>`), не `latest`,
  не плавающий мажор, не `${VERSION}`-интерполяция (версия — не операторская ручка; её смена =
  осознанная правка bundle под тестом). Digest-пиннинг **не требуется**: тег + анти-дрейф формы
  (TC-03: ни одного `:latest`/безтегового образа) достаточны для NFR-6, digest нечитаем и
  затрудняет осознанный апгрейд.
- **Точные теги фиксирует developer при реализации по фактически проверенному стенду** (ADR
  сознательно не выдумывает номера версий — ложная точность хуже честной отсылки к référence);
  обновление версий после ORCH-103 — отдельные задачи (BRD §6).
- Healthchecks: у инфра-сервисов (postgres/redis/minio/gitea) — стандартные; у Plane-сервисов —
  что даёт upstream; недостающее добирает poll-ожидание bootstrap (D5).

### D4 — Сеть: одна bridge, сервис-DNS внутрь, публикация только человеческих портов (OQ-5)

- **Вся инсталляция — в одной именованной bridge-сети** compose-проекта. `network_mode: host`
  в bundle **не используется** ни для кого: он был нужен нашему контуру ради ssh-деплоя в
  127.0.0.1 (ORCH-036/058) — в bundle эти пути структурно неактивны (`ORCH_DEPLOY_SSH_HOST`
  пуст → freshness/self-deploy/build-cache-pruner no-op по построению).
- **Машинный трафик — строго сервис-DNS:** Plane→орк webhook `http://orchestrator:8500/webhook/plane`,
  Gitea→орк `http://orchestrator:8500/webhook/gitea`, орк→Plane `ORCH_PLANE_API_URL=http://<plane-proxy-svc>`,
  орк→Gitea `ORCH_GITEA_URL=http://gitea:3000`, watchdog→орк
  `WATCHDOG_METRICS_URL=http://orchestrator:8500/metrics`. Никаких `host-gateway`/`extra_hosts`.
- **Наружу публикуются только человеческие точки** (карта конфигурируема в bundle-каноне,
  дефолты): Plane proxy → `${BUNDLE_PLANE_PORT:-8080}`, Gitea web → `${BUNDLE_GITEA_HTTP_PORT:-3000}`,
  орк API → `${BUNDLE_ORCH_PORT:-8500}` (операторский smoke `curl /health`). **Postgres/redis/
  mq/minio наружу НЕ публикуются** (секрет-гигиена/поверхность атаки).
- **Публичные URL** (браузер оператора, ссылки в Plane-комментариях/Telegram) строятся от
  **`BUNDLE_PUBLIC_HOST`** (дефолт `localhost`): `ORCH_GITEA_PUBLIC_URL=http://$BUNDLE_PUBLIC_HOST:3000`,
  `ORCH_PLANE_WEB_URL=http://$BUNDLE_PUBLIC_HOST:8080`, WEB_URL Plane, ROOT_URL Gitea. Split
  internal/public уже поддержан конфигом орка (ORCH-101) — новых ключей `src/**` не требуется.
- **Мина Gitea закрывается явно:** Gitea по умолчанию запрещает webhook'и в приватные адреса —
  bundle задаёт `GITEA__webhook__ALLOWED_HOST_LIST=orchestrator` (env-конфиг образа Gitea), иначе
  R-4 «задача не появилась» гарантирован. Smoke (FR-6) проверяет оба направления.
- HTTPS/домены/reverse-proxy заказчика — вне bundle (BRD §2.2); `BUNDLE_PUBLIC_HOST` +
  документированный ручной шаг при необходимости.

### D5 — Bootstrap: `scripts/bootstrap_bundle.py`, python stdlib, `plan`/`apply`/`verify` (OQ-4)

- **Язык — python stdlib-only** (NFR-7; паттерн `gen_secrets.py`: работает на голом python3
  целевого хоста ДО `docker compose up`; bash отвергнут — 9-шаговый stateful-визард с
  таймаутами/JSON/чистыми функциями под unit-тесты на bash не тестируем структурно).
  Никаких импортов из `src/**` (bootstrap бежит вне venv платформы; канон-знания — только
  субпроцессами кирпичей, см. ниже).
- **Режимы (паттерн ORCH-009):** `plan` — **дефолт**, ноль мутаций (печать плана + read-only
  preflight-диагностика); `apply` — полный прогон; `verify` — read-only пост-проверка
  (health/queue/metrics + `onboard_project.py verify` субпроцессом). Exit-коды: `0` успех /
  `2` остановка на manual-step или незавершённое предусловие / `1` ошибка (контракт TRZ FR-2).
- **Step-движок check→ensure:** каждый шаг = `check()` (выполнено?) → skip | `ensure()`
  (доводка). Повторный `apply` на инициализированном bundle = каскад skip (AC-8);
  «resume» после manual-step = просто повторный запуск. Чистые функции (preflight-вердикт,
  сборка плана, рендер env-файлов) выделены для unit-тестов (TC-08).
- **Последовательность apply (норматив TRZ FR-2, механика):**
  1. **Preflight (fail-fast, до мутаций):** docker+compose есть; `deploy/bundled/.env` существует
     и обязательные ключи заполнены (пути Claude CLI — существуют на хосте, иначе warning-блок:
     стек поднимется, конвейер без LLM не поедет); целевые порты свободны; томов/контейнеров с
     префиксом `orchestrator-bundle` нет (иначе — явный «уже инициализирован, продолжаю в
     ensure-режиме» либо отказ при противоречивом состоянии); RAM/диск ≥ минимумов из
     BUNDLED_SETUP (пороги — константы скрипта, синхронизированы с доком); python3+venv доступны.
  2. **Секреты (FR-3):** webhook-секреты — **субпроцессом `scripts/gen_secrets.py --write <tmp>`**
     (не реализуются заново, AC-7); bundle-внутренние (пароли postgres/redis/mq/minio,
     SECRET_KEY Plane, админ-пароль Gitea) — stdlib `secrets`; запись в `deploy/bundled/.env` +
     корневой `.env`; существующие значения не перетираются без `--force-secrets`; значения
     в stdout/лог **не печатаются** (только имена ключей).
  3. **Up + готовность:** `docker compose -f deploy/bundled/docker-compose.yml up -d`
     (идемпотентен по построению — оба прочтения AC-1 истинны: оператор мог выполнить up сам);
     ожидание готовности poll-циклами с таймаутами (health контейнеров, `migrator` завершился
     `exit 0`, HTTP-пробы Plane/Gitea); по таймауту — диагностика «какой сервис не дождались +
     хвост его логов».
  4. **Init Gitea — полностью автоматом** (D6).
  5. **Init Plane — manual-step чекпоинты** (D7).
  6. **Онбординг sandbox-проекта — строго `onboard_project.py apply` + `verify`** (D7).
  7. **Git-доступ агентов** (D8) + клон sandbox-репо в `deploy/bundled/repos/`.
  8. **Конфиг орка:** сборка корневого `.env` (база — канон `.env.example`: URL'ы D4, токены,
     секреты, `ORCH_PROJECTS_JSON` из вывода onboard; `ORCH_DEPLOY_SSH_HOST=` пусто —
     деплой-машинерия спит) и `.env.watchdog` (база — `.env.watchdog.example`; Telegram-ключи
     опциональны — пусто = деградация только нотификаций); пересоздание
     `up -d orchestrator orchestrator-watchdog` для подхвата env.
  9. **Health + итог:** `GET /health`, `/queue`, `/metrics`; финальная сводная таблица PASS/FAIL
     по шагам; следующая команда оператора — smoke BUNDLED_SETUP §smoke (REPLICATION §4).
- **Контракт manual-step (fail-safe, BR-2):** печать точной инструкции (URL/что нажать/что
  ввести) → ожидание подтверждения (интерактивно при TTY; без TTY — немедленный `exit 2` с той
  же инструкцией) → **верификация результата API-пробой** (например, валидность введённого
  `ORCH_PLANE_API_TOKEN` запросом к workspace) → только затем продолжение. Молчаливый пропуск
  запрещён.
- **Запретов в скрипте нет:** delete-операций (`docker volume rm`/`rm -rf`/`down -v`) — **ноль**
  (teardown — D9); боевых литералов FORBIDDEN — ноль (TC-06); печати секретов — ноль (NFR-3);
  наш прод недостижим по построению (скрипт говорит только с локальным docker целевого хоста).

### D6 — Init Gitea: полностью автоматизирован, branch protection НЕ настраивается (OQ-3-часть)

- Административная учётка — **официальный CLI в контейнере**:
  `docker compose … exec gitea gitea admin user create --admin …` (idempotent: предсуществование
  пользователя → skip); API-токен — `gitea admin user generate-access-token` (или REST под basic
  auth — равнозначно, выбирает developer по фактической версии Gitea) → `ORCH_GITEA_TOKEN`.
- **Один пользователь-бот** — владелец (`ORCH_GITEA_OWNER`) sandbox-репо и носитель токена для
  API орка и token-remote агентов (D8). Отдельная россыпь пользователей на тестовый bundle —
  неоправданная сложность (зафиксировано как осознанный компромисс в 10-tech-risks TR-7).
- **Branch protection на `main` НЕ включается; pre-receive не вводится** — норматив D10 ORCH-009 /
  adr-0037 п.4 (ломают PR-merge API merge-актора, INV-4); bundle-Gitea конфигурируется тем же
  правилом, BUNDLED_SETUP фиксирует его явно (ссылкой на LITE_SETUP §6, не копией).
- `GITEA__webhook__ALLOWED_HOST_LIST=orchestrator` — см. D4.

### D7 — Init Plane: честные manual-step + онбординг строго кирпичом (OQ-3)

- **Не автоматизируется (CE):** instance-setup/первый админ, создание workspace
  (`ORCH_PLANE_WORKSPACE_SLUG`), выпуск `ORCH_PLANE_API_TOKEN` — три **manual-step чекпоинта**
  контракта D5 (инструкция → подтверждение → API-верификация). **Прогрессивная автоматизация
  разрешена:** если на момент реализации у конкретной пиннованной версии Plane CE обнаружится
  стабильный API/seed-механизм для шага — developer вправе заменить manual-step на ensure **без
  изменения контракта чекпоинта** (верификация результата остаётся той же) и без правки ADR.
- **Онбординг sandbox-проекта — ТОЛЬКО субпроцессом** `python3 scripts/onboard_project.py apply`
  + `verify` из **host-venv чекаута** (канон запуска — ONBOARDING.md: venv с `requirements.txt`;
  создание venv — ensure-шаг bootstrap). Env для субпроцесса (URL'ы/токены D4) bootstrap передаёт
  через окружение процесса (pydantic env-переменные перекрывают `env_file`) — корневой `.env` к
  этому моменту уже собран либо передаётся фрагментом. Собственная реализация статусов/лейблов/
  webhook в bootstrap **запрещена** (BR-6/AC-7; 22 статуса остаются за
  `plane_sync._PLANE_NAME_TO_KEY`).
- **Workspace-webhook Plane** (Plane→орк) — остаётся manual-step **самого onboard-CLI**
  (его шаг `plane.workspace-webhook` уже MANUAL — CE не даёт API); bootstrap лишь подставляет
  правильный in-network URL `http://orchestrator:8500/webhook/plane` и секрет-имя в инструкцию
  и верифицирует доставку на smoke (FR-6).
- `--webhook-url` для Gitea per-repo hook — `http://orchestrator:8500/webhook/gitea` (D4).

### D8 — Git-доступ агентов: HTTP token-remote; ssh-контур не вводится (OQ-6)

- Клон sandbox-репо в `deploy/bundled/repos/<repo>` с remote-URL вида
  `http://<token>@gitea:3000/<owner>/<repo>.git` — **паттерн уже в каноне**
  (`onboard_project.py::_push_url` делает initial push именно так); агенты наследуют origin
  чекаута (push/fetch из контейнера — bridge-DNS, D4).
- **Ssh-контур в bundle не вводится:** ssh-порт Gitea не публикуется, маунт `ORCH_HOST_SSH_DIR`
  в bundle-compose отсутствует (это нашему контуру нужен ssh к хосту/Gitea; в bundle —
  лишняя поверхность: генерация ключей, known_hosts, регистрация в Gitea).
- Компромисс «токен в `.git/config` plaintext» зафиксирован честно: каталог `deploy/bundled/repos`
  — локальный, права на токен-носители `600`, токен — бот-юзера одной тестовой инсталляции;
  риск/митигейшн — 10-tech-risks TR-7. Git-идентичность агентов — существующие
  `ORCH_AGENT_GIT_NAME`/`ORCH_GIT_EMAIL_DOMAIN` (дефолты годятся).

### D9 — Teardown: только документированная процедура, не режим скрипта (OQ-7)

`BUNDLED_SETUP.md` §13 «Остановка и полный сброс»: `docker compose -f … down` (остановка) /
`down -v` + удаление сгенерённых конфигов и `deploy/bundled/{data,repos}` (полный сброс) — с
явным предупреждением о необратимости. **Reset-режим в bootstrap отвергнут:** одна опечатка
флага = снос томов; ценность против fenced-команды — нулевая; зато скрипт получает структурную
гарантию «delete-операций НЕТ вообще» (упрощение TC-07 и ревью).

### D10 — Док-канон: `docs/deployment/BUNDLED_SETUP.md`, 14 разделов, ссылки вместо форка (FR-4)

Форма — канон LITE_SETUP (adr-0037 D2): нормативные разделы в порядке маршрута оператора,
каждый исполняемый шаг = fenced-команда + «Проверка:» PASS/FAIL, хост-специфика — только
плейсхолдеры. **14 разделов** (норматив; точные заголовки — за developer'ом, проверяемость —
структурный тест): (1) рамка Bundled (включая «что НЕ входит»: Claude CLI, Telegram, HTTPS;
границы vs Lite); (2) **требования к хосту** (RAM/диск/CPU **по замеру тестового развёртывания**,
карта портов D4, явное «Plane ≈ 14 контейнеров — ресурсоёмко»); (3) предусловия;
(4) получение кода; (5) секреты; (6) запуск bundle-compose; (7) bootstrap (+ перечень
manual-step Plane); (8) LLM — ссылкой на LITE_SETUP §7; (9) Telegram — ссылкой на LITE_SETUP §8;
(10) онбординг следующих проектов — ссылкой на ONBOARDING.md; (11) smoke — шаги REPLICATION §4;
(12) stateless-проверка; (13) остановка/полный сброс (D9); (14) траблшутинг (минимум: webhook
не доходит — включая `ALLOWED_HOST_LIST`, OOM/нехватка RAM, порт занят, claude не найден,
Plane-миграции не завершились). Fail-closed имена `Confirm Deploy`/`STOP` и «22 статуса» —
сверкой импорта в тесте, не литералом. `docs/operations/REPLICATION.md` §1: строка Type B →
✅ ORCH-103 + ссылка. **Норматив сопровождения (NFR-5):** изменил шаги Bundled-тиража → обнови
BUNDLED_SETUP.md в том же PR.

### D11 — Анти-дрейф: три структурных тест-модуля (FR-5; без docker/сети/LLM)

По тест-плану `04-test-plan.yaml` (имена модулей — норматив): `tests/test_bundle_compose.py`
(TC-01…04: yaml.safe_load, обязательные сервисы, заморозка корневого compose зеркалом
существующего ассерта, пины образов, префикс томов, key-set-sync `${VAR}` ⊆
`deploy/bundled/.env.example`), `tests/test_bundled_setup_doc.py` (TC-05/06/09/10/11: разделы
D10, FORBIDDEN — **импорт** из `test_no_host_hardcodes.py`, секрет-эвристика hex≥32/alnum≥40 —
паттерн D8 ORCH-102, env-ключи ⊆ канонов, число статусов — импортом `plane_sync`, кросс-рефы,
CHANGELOG), `tests/test_bootstrap_script.py` (TC-07/08: ссылки на кирпичи, отсутствие
delete-операций и собственного списка статусов, unit чистых функций preflight/плана/рендера,
контракт exit 0/2/1). Существующие анти-дрейф тесты остаются зелёными **без правки их ассертов**
(AC-5/AC-6).

## Альтернативы

- **Расширить корневой `docker-compose.yml` (профиль `bundled`)** — отвергнуто: заморожен
  анти-дрейфом ORCH-102 (TC-04) и нормативом adr-0037 п.2 «compose не форкается»; смешение
  боевого контура с дистрибутивом = групповой риск self-hosting.
- **Include-композиция (`include:`/несколько `-f`)** — отвергнуто: многофайловость = новые
  степени свободы запуска (забытый `-f` молча меняет состав), сложнее структурный тест; один
  самодостаточный файл проще и детерминированнее (NFR-6).
- **Live env через `--env-file deploy/bundled/.env.bundled`** — отвергнуто: footgun голой
  compose-команды без флага (молчаливые дефолты) — см. D2.
- **Орк в bundle под `network_mode: host` + `host-gateway` для webhook'ов** — отвергнуто:
  хост-сеть нужна была нашему ssh-деплой-контуру, который в bundle спит; bridge даёт чистые
  стабильные сервис-DNS-URL обоих направлений и нулевые порт-конфликты (D4).
- **Digest-пиннинг образов** — отвергнуто: нечитаем, усложняет осознанный апгрейд; неподвижный
  тег + тест формы достаточны для NFR-6 (D3).
- **Ssh-доступ агентов к bundle-Gitea** — отвергнуто: три лишних механизма (ключи, known_hosts,
  регистрация) против уже существующего token-remote-паттерна onboard (D8).
- **Bash-bootstrap** — отвергнуто: нет unit-тестируемых чистых функций (TC-08), JSON/поллинг/
  стейт-машина шагов на bash хрупки (D5).
- **Reset-режим bootstrap** — отвергнуто: риск-поверхность против нулевой ценности (D9).
- **Переписать Plane-стек «по-своему» (свои имена сервисов/env)** — отвергнуто: дрейф от
  upstream-доков, неоплачиваемое сопровождение (D3).

## Последствия

- **+** Эпик ORCH-10 закрывается по типу B: заказчик без инфраструктуры получает конвейер
  «под ключ» одной командой + одним bootstrap-прогоном с честными чекпоинтами.
- **+** Нулевой дрейф канонов: статусы/лейблы/секреты/smoke/док-форма — переиспользованы
  (gen_secrets, onboard_project, REPLICATION §4, форма LITE_SETUP); рантайм байт-в-байт.
- **+** Наш прод недостижим по построению: артефакты инертны в нашем контуре, kill-switch не
  нужен (паттерн ORCH-009); все существующие анти-дрейф тесты остаются зелёными.
- **−** Новая поверхность сопровождения: пиннованные версии Plane/Gitea стареют (апгрейд —
  отдельные задачи, NFR-6); двойной `.env`-слой (bundle-инфра vs runtime орка) требует
  дисциплины «писатель — bootstrap» (митигировано D2: один писатель + key-sync тест).
- **−** Manual-step Plane CE размывают UX «одной команды» — неустранимо честно (CE без API
  первичной инициализации); митигировано контрактом чекпоинта (инструкция+верификация) и
  прогрессивной автоматизацией (D7).
- **−** Токен в remote-URL агентских чекаутов — осознанный компромисс тестовой инсталляции
  (TR-7; права 600, непубликуемые порты БД, один бот-юзер).
- **Откат:** удалить `deploy/`, `scripts/bootstrap_bundle.py`, `docs/deployment/BUNDLED_SETUP.md`,
  три тест-модуля, строку REPLICATION §1 и записи CHANGELOG/CLAUDE.md/README — состояние репо 1:1
  (docs+scripts+tests, без миграций); на целевых хостах — процедура §13 (D9).

## Ссылки

- BRD: `docs/work-items/ORCH-103/01-brd.md`
- TRZ: `docs/work-items/ORCH-103/02-trz.md` (OQ-1…OQ-7 → D1…D9)
- Acceptance: `docs/work-items/ORCH-103/03-acceptance-criteria.md`; тест-план: `04-test-plan.yaml`
- Сквозной ADR: `docs/architecture/adr/adr-0038-bundled-replication-canon.md`
- Предшественники: adr-0035 (ORCH-009 onboarding: D10 branch-protection, manual-step, `_push_url`),
  adr-0036 (ORCH-101 10-common: параметризация/«дефолт=боевое»/gen_secrets/REPLICATION),
  adr-0037 (ORCH-102 Lite: док-канон/`.env.watchdog.example`/compose-подмножество)
- Сверено по коду/репо: `tests/test_lite_setup_doc.py` (заморозка корневого compose, FORBIDDEN-импорт,
  секрет-эвристика), `tests/test_no_host_hardcodes.py` (`FORBIDDEN`), `scripts/gen_secrets.py`
  (`--write PATH`, exit 0/2), `scripts/onboard_project.py` (закрытый src-импорт-лист, MANUAL
  `plane.workspace-webhook`, `_push_url`, exit 0/2/1), `docs/operations/ONBOARDING.md` (host-venv),
  `docker-compose.yml` (паттерны `${VAR:-default}`, `env_file required:false`, группа ORCH-040),
  `.gitignore` (неякорные `.env`/`data/`/`.env.watchdog`)
