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

# ADR-001: Установщик Lite-тиража `scripts/install_lite.py` — структура, границы, переиспользование канонов

Work Item: **ORCH-104** — Установочный скрипт для Lite (Type A эпика ORCH-10, поверх ORCH-102)
Стадия: **architecture**
Сквозная регистрация: **`docs/architecture/adr/adr-0040-lite-installer-canon.md`** (решение
кросс-каттинговое — вводит новый операторский артефакт в семью тиража и нормативную границу
«connect-only vs bring-up»; продолжает серию adr-0036 → 0037 → 0038).

## Статус
Proposed

## Контекст

ORCH-102 дал золотой источник Lite-тиража — ручной runbook `docs/deployment/LITE_SETUP.md` из 13
разделов («голый хост → работающий конвейер»). Это надёжный, но **ручной** маршрут: оператор сам
проверяет ~6 предусловий хоста, копирует `.env.example`, гоняет `gen_secrets.py`, заполняет ~20
ключей `.env` по 4 группам, настраивает webhook Plane (иногда прямым SQL в Postgres, §5.4),
выпускает токен/webhook Gitea, ставит claude CLI, заводит **два** Telegram-бота, поднимает compose,
регистрирует проект `onboard_project.py`, гоняет smoke. Долго; легко ошибиться; ошибка всплывает
поздно (на `docker compose up`, а не в момент ввода).

ORCH-104 автоматизирует **happy-path §2–§11** одним интерактивным установщиком и добавляет
**раннюю валидацию** (живая проверка каждого секрета/URL ДО записи). Это **scripts + docs + tests**
изменение: рантайм/конвейер (`src/**`, `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`, machine-verdict
ключи, схема БД) — **не трогаются** (NFR-1/INV-1). Каноны не форкаются: секреты —
`gen_secrets.py`, регистрация проекта — `onboard_project.py`, env — из `.env.example` /
`.env.watchdog.example`, стек — из `docker-compose.yml`.

**Сверено по коду/репо (факты, на которых стоит решение):**
- `scripts/bootstrap_bundle.py` (ORCH-103, ~970 строк) — ближайший прецедент: step-движок
  `check→ensure`, режимы `plan`/`apply`/`verify`, exit `0/2/1`, чистые `parse_env`/`render_env`/
  `preflight_verdict`/`collect_facts`, honest `manual_checkpoint` (TTY-aware, verify-loop),
  `_write_private` (chmod 600, маскировка), `_http` (never-raise GET), stdlib-only, **0
  delete-операций**. Subprocess-кирпичи `gen_secrets.py --write <tmp>` и `onboard_project.py
  apply/verify --json` (парс `instructions[]` на префикс `ORCH_PROJECTS_JSON=`).
- `scripts/gen_secrets.py` (ORCH-101) — единственный легитимный источник webhook-секретов
  (`secrets.token_hex(32)`); `--write [PATH]` отказывает на существующем файле (exit 2), перезапись
  только `--force`; stdlib-only.
- `scripts/onboard_project.py` (ORCH-009) — `plan`/`apply`/`verify` (`choices`), exit `0/1/2`
  (`Report.exit_code`), `--json` → отчёт с `instructions[]`, обязательные
  `--name/--repo/--prefix/--stack/--test-cmd/--prod-port/--staging-port/--webhook-url`. **22 статуса
  с точными именами** — только этот кирпич (`plane_sync._PLANE_NAME_TO_KEY`).
- `tests/test_no_host_hardcodes.py` экспортирует `FORBIDDEN` + `find_violations(source, forbidden)`
  (tokenize-исключение комментариев/докстрингов) — переиспользуемы по импорту (single source of
  truth; так уже делает `test_lite_setup_doc.py`).
- `tests/test_lite_setup_doc.py` — структурный анти-дрейф: `SECTIONS` (13 `## N.`-заголовков **в
  порядке**), список обязательных кирпичей, key-sync `.env.watchdog.example`, сверка «22 статуса»
  импортом `plane_sync._PLANE_NAME_TO_KEY`, fenced-скан `FORBIDDEN` + секрет-эвристика.
- `LITE_SETUP.md` §5.4 — webhook Plane CE **не в `/api/v1`**: путь A (UI) / путь Б (raw SQL
  `INSERT INTO webhooks … docker exec plane-db psql`). §7.2 — claude CLI: интерактивный логин
  Anthropic. §6.4 — branch protection на `main` **не включать** (INV-4, ложные HOLD merge-актора).

ТЗ (`02-trz.md` §12) оставило архитектору 7 открытых вопросов (OQ-1…OQ-7). Они разрешены ниже как
решения D1…D11; D12…D15 — структурные решения (анти-дрейф, doc-sync, границы scope).

## Решение

### Сводка

Новый **самодостаточный** stdlib-only установщик `scripts/install_lite.py` — «connect-only»-сородич
`bootstrap_bundle.py`: тот *поднимает* Plane/Gitea, этот *подключается* к уже существующим. Тот же
проверенный каркас (step-движок `check→ensure`, режимы `plan`/`apply`/`verify`, exit `0/2/1`,
honest `manual_checkpoint`, `_write_private` 600, never-raise `_http`), реплицированный в один файл
ради «одного установочного файла» (BR-1) и нулевого риска регресса Bundled-пути. Каноны
(`gen_secrets.py`, `onboard_project.py`, `.env.example`, `docker-compose.yml`) переиспользуются
субпроцессами/рендером, не форкаются. Ключевая граница Lite: установщик говорит **только** с
локальным хостом и собственными Plane/Gitea/Telegram заказчика по их API — он **не** мутирует
чужую инфраструктуру (webhook Plane = верифицируемый manual-step, не raw-SQL).

### D1 — Имя и идентичность: `scripts/install_lite.py` (OQ-1)

Файл — `scripts/install_lite.py` (рекомендация анализа принята). `install_*` понятнее конечному
**оператору**, чем `bootstrap_*`, и точнее отражает суть: Lite **устанавливает/подключает** орк к
готовой инфре, тогда как `bootstrap_bundle.py` **бутстрапит** весь стек (включая Plane/Gitea).
Несимметрия имён — намеренный сигнал разной семантики (D9). Привязка: BR-1, AC-1.

### D2 — Самодостаточный один файл, не общий модуль (OQ-4)

`install_lite.py` **реплицирует** небольшие проверенные примитивы `bootstrap_bundle.py`
(`parse_env`/`render_env`/`manual_checkpoint`/`_http`/`_write_private`/`collect_facts`-стиль/
`preflight_verdict`-стиль/exit-контракт) **внутри себя**, а не импортирует общий модуль.

**Почему так, а не DRY-модуль:**
- **«Один установочный файл» (BR-1)** — оператор копирует/запускает один артефакт; скрытый граф
  импортов противоречит UX-цели.
- **`bootstrap_bundle.py` остаётся байт-в-байт (TRZ §2).** Вынос общего кода потребовал бы
  *править* существующий кирпич → перегон его анти-дрейф-тестов (`test_bootstrap_script.py`) и
  риск регресса **Bundled**-пути. Это scope-creep и риск ради чужой задачи.
- **stdlib-only (NFR-6/INV-7)** соблюдается тривиально в обоих вариантах — DRY его не решает.
- Цена — ~150 строк дублирования примитивов; они малы, генеричны и стабильны (низкий риск дрейфа,
  т.к. два скрипта автоматизируют **разные** runbook'и).
- **Триггер будущего выноса (rule of three):** появится **третий** тиражный скрипт с теми же
  примитивами → вот тогда выносить `scripts/_replication_lib.py` отдельной задачей (с правкой обоих
  существующих под её анти-дрейф). До тех пор — дублирование осознанное и документированное.

Привязка: BR-6, NFR-6, INV-2/INV-7, AC-7/AC-10.

### D3 — Step-движок и режимы: канон `bootstrap_bundle.py` (FR-1)

Каркас 1:1 с эталоном:
- **Режимы** (argparse, позиционный `mode`, `choices=("plan","apply","verify")`, дефолт `plan`):
  `plan` — ноль мутаций (печать плана + read-only preflight); `apply` — полный прогон step-движком;
  `verify` — read-only пост-проверка (health-контракты + `onboard_project.py verify`).
- **Exit-контракт:** `0` успех; `2` — manual-step / незавершённое предусловие / нет TTY; `1` —
  ошибка. Никаких иных кодов (AC-1 FAIL).
- **Step-движок `check→ensure`:** упорядоченный `APPLY_STEPS = ((name, fn), …)`, каждая
  `fn(ctx) -> str` возвращает `"ok"`/`"skipped"`/`"manual-step"` либо бросает `ManualStop`(→2)/
  `InstallError`(→1) (ловятся в `main`). Идемпотентность — встроенная inline-проверка «уже
  сделано» в каждом шаге (повтор = каскад `skipped`, «resume» = повторный запуск). Результаты —
  `ctx["results"]`, печатаются в итоговой сводке.
- **Шаги Lite (порядок):** `preflight` (D4) → `detect_infra` (D5) → `collect_verify` (D6) →
  `secrets` (D7) → `env` (D7) → `up` (D8) → `onboard` (D8) → `plane_webhook` (D9, manual) →
  `health` (D12).

Привязка: BR-1, BR-7, FR-1, AC-1, AC-8.

### D4 — Preflight-скан и управляемая установка зависимостей (FR-2/FR-3, OQ-3)

**Скан (FR-2):** read-only `collect_facts` (образец эталона) — наличие `docker` / `docker compose`
v2 / `git` / `python3` / `node` / `claude` (+ читаемость кред); свободность прод-порта
`ORCH_DEPLOY_PROD_TARGET_PORT` (дефолт 8500; при self-hosting-вилке staging
`ORCH_STAGING_PORT`=8501); `uid`/`gid`/`docker-gid` и владелец каталога репозиториев; наличие
ssh-каталога. Чистая `preflight_verdict(facts) -> (blockers, warnings)` + человекочитаемый список
«есть/нет». `plan`/preflight печатает его и при блокерах отдаёт `exit 2`.

**Управляемая установка (FR-3, D-1 владельца):** в Lite реалистично **все** недостающие
зависимости (docker, node, `@anthropic-ai/claude-code` через npm, git) — системного/root-уровня.
Поэтому **тира «тихого авто-инсталла» нет вообще** — это самый безопасный разрез D-1:
- чистая `install_command(distro, dep) -> str | None` (карта: debian/ubuntu→`apt`, rhel/fedora/
  centos→`dnf`; дистрибутив — из `/etc/os-release` `ID`/`ID_LIKE` + `shutil.which`) печатает
  **точную команду**;
- выполнение — **только** при (a) наличии TTY, (b) явном `y/N`=yes, (c) команда из фиксированного
  allowlist (никакого произвольного exec);
- отказ / нет TTY / неизвестный дистрибутив (`install_command` → `None`) → печать инструкции +
  `exit 2`. **Ни одной молчаливой root-мутации** (AC-3 FAIL).

Привязка: BR-2/BR-3, FR-2/FR-3, AC-2/AC-3, NFR-8.

### D5 — Детект существующих Plane/Gitea + выбор (FR-4, OQ-2)

Best-effort обнаружение (never-raise, NFR-3); его роль — лишь **пред-заполнить догадку**,
авторитет — живая верификация D6 перед записью (митигейшн R-1).
- **Кандидаты из двух источников:** (1) `docker ps` (имена/образы) — Plane: образ/имя
  `makeplane/plane-*`, `plane-{web,space,admin,api,proxy}`, `proxy`; Gitea: образ `gitea/gitea`,
  имя `gitea*`. (2) Слушающие localhost-порты (stdlib socket): Plane — 80/443/8080; Gitea — 3000.
- **Проба живости (ранжир уверенности, без токена):** Plane — `GET <base>/api/instances/`
  (unauth-liveness Plane CE); Gitea — `GET <base>/api/v1/version`. Кандидат с успешной пробой
  ранжируется выше; дедуп по нормализованному base-URL.
- **Чистая `rank_candidates(docker_facts, port_probes) -> [Candidate(url, source, confidence)]`**
  (юнит-тестируема без сети).
- **Поведение по числу (FR-4):** 0 → запрос ручного URL (фолбэк); 1 → предложить с подтверждением;
  ≥2 → **нумерованный список** + выбор (`input()` индекс), вне диапазона → ручной ввод. Выбор
  наполняет `ORCH_PLANE_*` / `ORCH_GITEA_*` и проходит D6.
- **never-raise:** сбой любой пробы → кандидат отброшен, детект продолжается; тотальный сбой →
  пустой список → ручной ввод (AC-4 не должен ронять скрипт).

Привязка: BR-5, FR-4, AC-4, NFR-3.

### D6 — Интерактивный сбор + живая верификация ДО записи (FR-5)

Honest-checkpoint контракт (как `manual_checkpoint` эталона): для каждого требуемого секрета/URL —
печать «откуда взять» (ссылка на LITE_SETUP §5–§8), скрытый ввод секрета (`getpass`),
**верификация онлайн ДО записи**:
- Plane: `GET <api>/api/v1/workspaces/<slug>/projects/` с `X-API-Key:<token>` → 200;
- Gitea: `GET <url>/api/v1/user` с `Authorization: token <token>` → 200;
- Telegram: `GET https://api.telegram.org/bot<token>/getMe` → `{"ok":true}`.

Провал → повтор (до N) либо `exit 2` с подсказкой; значение в `.env` **не пишется** (AC-5 FAIL —
неверный секрет записан / верификация после записи / секрет виден на экране). Авто-детектируемое
(uid/gid/docker-gid/порты/пути/node/claude/выбранные URL) **пред-заполняется** — оператор только
подтверждает. Привязка: BR-4, FR-5, AC-5, NFR-2.

### D7 — Секрет-гигиена и рендер `.env`/`.env.watchdog` (FR-6/FR-7, NFR-2)

- **Webhook-секреты — строго кирпич** `gen_secrets.py` субпроцессом во временный файл, парс
  `parse_env`. **Никакого собственного `secrets.token_hex`/`token_urlsafe` в установщике**
  (анти-форк, проверяется AST-сканом теста, AC-7). Уже присутствуют и валидны → skip (не
  перетирать без `--force`).
- **Рендер** — идемпотентный `render_env(example_text, overrides)` из `.env.example` /
  `.env.watchdog.example`: существующие ключи-override обновляются на месте (комментарии канона
  сохранены), неизвестные — управляемым блоком в конец. Watchdog-ключи (`WATCHDOG_TG_*`) —
  **только** в `.env.watchdog` (файл-носитель, LITE_SETUP §4.3; в `.env` они инертны).
- **Гигиена:** запись через `_write_private` (chmod **600**); значения секретов в stdout/лог
  **не попадают** (лог печатает только имена ключей/пути); молчаливой перезаписи нет.

Привязка: BR-6, FR-6/FR-7, AC-6, NFR-2, INV-4.

### D8 — Подъём стека + регистрация проекта кирпичом (FR-8/FR-9)

- **Up:** `docker compose up -d --build` ровно `orchestrator` + `orchestrator-watchdog` (staging НЕ
  поднимается — за профилем); ожидание готовности поллингом `GET /health` (таймаут).
- **Onboarding — строго кирпич** `onboard_project.py`: сбор параметров проекта (флаги или
  интерактивно), `apply` затем `verify` субпроцессом (`--json`); парс `instructions[]` на
  `ORCH_PROJECTS_JSON=<merged>` → запись в `.env` через `render_env`; manual-пункты отчёта (exit 2)
  пробрасываются оператору. **Никакого собственного создания статусов/лейблов/репо/webhook**
  (22 статуса — только кирпич, AC-7). После записи реестра — управляемый рестарт
  `up -d --force-recreate orchestrator` в тихом окне (как LITE_SETUP §10).

Привязка: BR-1/BR-6, FR-8/FR-9, AC-7/AC-8.

### D9 — Webhook Plane = всегда верифицируемый manual-step (OQ-5) — граница connect-only

**Ключевое отличие Lite от Bundled.** `bootstrap_bundle.py::step_plane_webhook` *владеет*
Plane-Postgres (сам его поднял) и пробует прямой `INSERT` с manual-фолбэком. В Lite Plane — **чужая
инсталляция заказчика** (D-2): установщик может быть не на одном хосте с Plane, не иметь
docker-доступа к её БД, и raw-SQL в чужую БД — ровно та кросс-граничная мутация, что запрещает
INV-5. Поэтому webhook Plane в Lite — **всегда honest manual-step, никогда авто-SQL**:
- инструкция пути A (UI Plane: URL `…/webhook/plane`, secret `ORCH_PLANE_WEBHOOK_SECRET`, события
  Issue + Issue Comment);
- путь Б (raw-SQL из LITE_SETUP §5.4) — **печатается как инструкция** для Plane CE без UI, но
  установщиком **не исполняется**;
- **верификация** — фактическую запись webhook в Plane CE через `/api/v1` прочитать нельзя →
  истинный гейт webhook'а = **smoke** (D12 / §11): реальная задача, переведённая в «To Analyse»,
  даёт analyst-job в `/queue`. `manual_checkpoint` фиксирует факт «оператор настроил» и сообщает,
  что финальная проверка — на smoke.

Привязка: BR-8, OQ-5, AC-5/AC-11, INV-3/INV-5.

### D10 — Логин claude CLI = manual-step с верификацией (OQ-6)

Интерактивный логин — поток Anthropic, **не автоматизируется** (подтверждено). Установщик:
детект `claude` (`shutil.which`/`claude --version`); отсутствует → consent-gated install (D4,
`npm install -g @anthropic-ai/claude-code`); логин → `manual_checkpoint`: инструкция «выполнить
`claude` на хосте под deploy-user», **верификация** = `claude --version` OK **и** читаемость
кред-файла под run-uid (`~/.claude/.credentials.json`, как LITE_SETUP §7.2). Привязка: OQ-6, AC-3,
NFR-3.

### D11 — Не-интерактивный / CI-контур (OQ-7)

- **Флаги:** `mode` (plan|apply|verify); `--force` (перевыпуск секретов → проброс `gen_secrets.py
  --force`); проброс параметров проекта в `onboard_project.py`
  (`--name/--repo/--prefix/--stack/--test-cmd/--prod-port/--staging-port/--description/
  --webhook-url`); `--non-interactive`.
- **Секреты — из окружения, не из argv** (анти-ps-leak, NFR-2): в не-интерактиве токены берутся из
  одноимённых `ORCH_*` env-переменных / существующего `.env`, не из флагов.
- **Fail-closed:** `--non-interactive` **или** `not sys.stdin.isatty()` → любой обязательный,
  но отсутствующий/непрошедший верификацию вход → `exit 2` с **точным именем** недостающего ключа,
  без prompt'а (зеркало `manual_checkpoint` эталона). Скрипт пригоден для повторного/CI-прогона
  (идемпотентный `apply`).

Привязка: BR-7, FR-1, AC-8, NFR-2.

### D12 — Health-гейт и итоговая сводка (FR-10)

После `apply` (и в `verify`): `GET /health` → 200; `GET /queue` / `GET /metrics` → валидный JSON.
Итоговая сводка по шагам (`ok`/`skipped`/`manual-step`) + общий вердикт PASS/FAIL; любой FAIL шага →
`exit 1` с диагностикой (хвост `docker logs orchestrator --tail` / снапшот `/queue`). `verify`
(read-only) повторяет health-контракты + `onboard_project.py verify`. Привязка: BR-7, FR-10, AC-11.

### D13 — Анти-дрейф тесты (NFR-7)

Новый **`tests/test_install_lite_script.py`** — детерминированный (без сети/docker/subprocess/LLM;
HTTP/процессы — через инъекцию фейков):
- **Структурные:** ссылки на кирпичи (`gen_secrets.py`, `onboard_project.py`) присутствуют; **нет
  собственной генерации секретов** (запрет `secrets.token_hex`/`token_urlsafe`/`token_bytes` в
  файле — делегирование кирпичу, AC-7); **нет захардкоженных 22 имён статусов** (AC-7);
  **stdlib-only** (AST-скан import'ов из разрешённого набора; нет `from src …`/`import src …`,
  AC-10/INV-7); **нет delete/force-операций** (`rm -rf`, `docker … rm`, `down -v`, `git push
  --force`, удаление веток, AC-10/INV-3); **FORBIDDEN host-литералы** — импорт `FORBIDDEN` +
  `find_violations` из `test_no_host_hardcodes.py` (single source of truth, не копия, AC-10);
  exit-контракт `{0,1,2}`.
- **Юнит (чистые функции):** `preflight_verdict` (блокеры/варнинги), `install_command(distro,dep)`,
  `rank_candidates` (ранжир/дедуп), `parse_env`/`render_env` (round-trip, сохранение комментариев,
  watchdog-ключи в свой файл), маскирование-хелпер.
- **С фейками:** verify-before-write (битый токен отклонён и не записан), идемпотентный `apply`
  (второй прогон → `skipped`, без дублей), `plan` zero-mutation.

Обновление **`tests/test_lite_setup_doc.py`**: добавить ассерт «LITE_SETUP.md ссылается на
`install_lite.py` как рекомендованный путь»; **все существующие проверки (13 разделов, кирпичи,
key-sync, 22 статуса, FORBIDDEN) остаются зелёными** и не ослабляются. Привязка: BR-9, AC-9/AC-10/
AC-12, NFR-7.

### D14 — Doc-sync и норматив сопровождения (FR-11, BR-9)

- **`LITE_SETUP.md`:** указатель на установщик — **callout-врезка** (`>`-блок) в §1 (или сразу
  перед §2), НЕ новый нумерованный раздел: «Рекомендованный путь — `python3 scripts/install_lite.py`
  (автоматизирует §2–§11); ручной маршрут ниже — референс/фолбэк для траблшутинга». **13 `## N.`
  заголовков сохранены байт-в-байт** (на них ключуется `test_lite_setup_doc.py::SECTIONS`).
- **Обновить в том же PR (golden source, правило агентов №2):** `CLAUDE.md` (раздел тиража),
  `docs/architecture/README.md` (Type A — Lite ← делается этой стадией, D15), `docs/overview/`
  (витрина Lite-install/использования — затронута, см. ORCH-105; финальные правки — на стадии
  development), `CHANGELOG.md`.
- **NFR-5 норматив:** меняешь шаги установки → синхронизируй `LITE_SETUP.md` в том же PR (держит
  анти-дрейф `test_lite_setup_doc.py`). Зеркало норматива ORCH-102/103.

Привязка: BR-9, FR-11, AC-12.

### D15 — Границы scope, сквозная регистрация, отсутствие kill-switch

- **`07-infra-requirements.md` — N/A:** топология **нашего** оркестратора не меняется (установщик
  вне рантайма, исполняется на хосте заказчика). Предусловия хост-контура — золотой источник
  LITE_SETUP §2; форкать их в 07 запрещено каноном «link-first, не дублировать».
- **`08-data-requirements.md` — N/A:** изменений схемы БД нет (TRZ §5; БД создаёт сам орк пустой
  при первом старте, stateless §12).
- **Сквозной ADR `adr-0040`** регистрирует Lite-установщик как операторский артефакт семьи тиража и
  нормативную границу connect-only (D9). Детальный пакет (D1…D15) — в этом work-item ADR.
- **Kill-switch не нужен:** активация — только явный запуск оператором на его хосте (паттерн
  ORCH-009/101/102/103). Рантайм байт-в-байт (INV-1).

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

- **Общий модуль `scripts/_replication_lib.py` (DRY сейчас)** — отвергнуто: потребовал бы править
  замороженный `bootstrap_bundle.py` (риск регресса Bundled, TRZ §2) и противоречит «одному
  установочному файлу» (BR-1). Вынос — позже, по rule-of-three (D2).
- **Имя `bootstrap_lite.py` (симметрия с bundle)** — отвергнуто: `install_*` понятнее оператору и
  верно сигналит «connect-only» против «bring-up» bundle (D1).
- **Авто-драйв webhook Plane raw-SQL (как bundle `step_plane_webhook`)** — отвергнуто: в Lite Plane
  чужой; raw-SQL в чужую БД нарушает INV-5 и может быть недостижим (нет docker-доступа). Только
  manual-step + smoke-верификация (D9).
- **Тихий авто-install системных пакетов под sudo** — отвергнуто D-1: только consent + точная
  команда из allowlist; неизвестный дистрибутив/нет TTY → инструктировать + exit 2 (D4).
- **Полноценный TUI-мастер / сторонние зависимости (rich/click)** — отвергнуто: NFR-6 stdlib-only,
  работа на голом `python3` до первого `docker compose up`; `argparse`+`input`/`getpass` достаточно.
- **Замена `LITE_SETUP.md` установщиком** — отвергнуто: runbook остаётся золотым источником и
  фолбэком (дополняем, не выкидываем; D14).

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

- **+** Оператор проходит §2–§11 одним файлом; ошибки секретов/URL ловятся **в момент ввода**
  (живая верификация), а не на `docker compose up`.
- **+** Идемпотентность/повтор/CI: `plan`/`apply`/`verify`, exit `0/2/1`, fail-closed без TTY.
- **+** Каноны не форкаются → нулевой дрейф статусов/секретов/env; `bootstrap_bundle.py` и
  Bundled-путь не тронуты.
- **+** Self-hosting-безопасно: только scripts/docs/tests; говорит лишь с локальным хостом и
  инфрой заказчика; 0 delete-операций; наш прод не затронут (исполняется на чужом хосте).
- **−** ~150 строк дублирования примитивов с `bootstrap_bundle.py` (осознанно; rule-of-three
  триггер задокументирован — D2).
- **−** Эвристика детекта Plane/Gitea ложно-(положительна/отрицательна) на нестандартных
  именах/портах → митигируется ручным фолбэком и живой верификацией (D5/D6, R-1).
- **−** Webhook Plane остаётся ручным шагом (граница connect-only) → его корректность
  подтверждается только на smoke (D9); это честно сообщается оператору.
- **Откат:** удаление `scripts/install_lite.py` + `tests/test_install_lite_script.py` + revert
  callout-врезки `LITE_SETUP.md` и doc-правок. Состояние 1:1, ни миграций, ни рантайм-изменений.

## Ссылки
- BRD: `docs/work-items/ORCH-104/01-brd.md`
- TRZ: `docs/work-items/ORCH-104/02-trz.md` (OQ-1…OQ-7 → D1…D11)
- Acceptance: `docs/work-items/ORCH-104/03-acceptance-criteria.md` (AC-1…AC-12)
- Tech-risks: `docs/work-items/ORCH-104/10-tech-risks.md`
- Сквозной ADR: `docs/architecture/adr/adr-0040-lite-installer-canon.md`
- Предшественники: adr-0036 (фундамент 10-common), adr-0037 (Lite-канон/`LITE_SETUP.md`),
  adr-0038 (Bundled/`bootstrap_bundle.py`), adr-0035 (онбординг/`onboard_project.py`)
- Сверено по коду: `scripts/bootstrap_bundle.py`, `scripts/gen_secrets.py`,
  `scripts/onboard_project.py`, `tests/test_no_host_hardcodes.py`, `tests/test_lite_setup_doc.py`,
  `docs/deployment/LITE_SETUP.md`, `.env.example`, `.env.watchdog.example`, `docker-compose.yml`
