# ONBOARDING — turnkey-онбординг нового проекта (ORCH-009)

> RUNBOOK. Полный чеклист подключения нового проекта к оркестратору одним проходом.
> Исполнитель — оператор; инструмент — CLI `scripts/onboard_project.py`
> (режимы `plan` — дефолт/dry-run, `apply`, `verify`). Каждый шаг, который CLI выполнить
> не может, помечен **🖐 РУЧНОЙ ШАГ** и снабжён командой проверки результата.
> Архитектура решения — см. «Ссылки» внизу.

Запуск CLI — из корня чекаута репо orchestrator, в venv с `requirements.txt`:

```bash
python3 scripts/onboard_project.py plan \
  --name "My Project" --description "зачем проект" \
  --repo my-project --prefix MP \
  --stack "Python 3.12 + FastAPI" --test-cmd "pytest tests/ -q" \
  --prod-port 8600 --staging-port 8601 \
  --webhook-url https://openclaw.mva154.duckdns.org/orchestrator/webhook/gitea
```

`plan` печатает полный план **без единой мутации** (ни сети-POST, ни записи на диск);
`apply` — идемпотентный ensure (существующее → `skipped(exists)`, ничего не удаляется);
exit-коды: `0` — чисто, `2` — есть `manual-step`/gap, `1` — ошибка.

---

## 0. Предусловия

Все значения — в `.env` на хосте (секреты в гит не попадают):

| Переменная | Зачем | Проверка |
|-----------|-------|----------|
| `ORCH_PLANE_API_TOKEN` (+`ORCH_PLANE_API_URL`, `ORCH_PLANE_WORKSPACE_SLUG`) | создание/чтение проекта, статусов, лейблов | `curl -s -H "X-API-Key: $TOKEN" $URL/api/v1/workspaces/$SLUG/projects/ \| head -c 200` |
| `ORCH_GITEA_TOKEN` (+`ORCH_GITEA_URL`) | создание репо + webhook | `curl -s -H "Authorization: token $TOKEN" $URL/api/v1/user \| head -c 200` |
| `ORCH_GITEA_WEBHOOK_SECRET` | HMAC webhook (переиспользуется, один на все репо) | есть строка в `.env`; нет → `apply` сгенерирует и выведет |
| `ORCH_PROJECTS_JSON` | текущий реестр — источник merged-вывода | `grep ORCH_PROJECTS_JSON .env` |

Токен Plane должен иметь право создавать проекты в workspace; токен Gitea — создавать репо и
hooks под выбранным owner (`--gitea-owner`, дефолт из конфига).

---

## 1. Слой Plane: проект + статусы + лейблы

Выполняет `apply` (или вручную при недоступности API CE — каждый отказ CLI помечает
`manual-step`, не падает).

1. **Проект**: создаётся с `identifier = --prefix`. Уже существует → передай
   `--plane-project-id <uuid>` (ensure распознает и пропустит).
2. **Статусы — точные канонические имена** (22, источник — `plane_sync._PLANE_NAME_TO_KEY`;
   опечатка = тихая деградация fail-closed веток):

   | Статус | Группа | | Статус | Группа |
   |--------|--------|-|--------|--------|
   | Backlog | `backlog` | | In Review | `started` |
   | Todo | `unstarted` | | Blocked | `started` |
   | To Analyse | `unstarted` | | Approved | `started` |
   | In Progress | `started` | | Rejected | `started` |
   | Analysis | `started` | | **Confirm Deploy** | `started` |
   | Architecture | `started` | | Needs Input | `started` |
   | Development | `started` | | Done | `completed` |
   | Code-Review | `started` | | Cancelled | `cancelled` |
   | Review | `started` | | **STOP** | **`cancelled`** |
   | Testing | `started` | | Awaiting Deploy | `started` |
   | Deploying | `started` | | Monitoring after Deploy | `started` |

   ⚠️ Код-критично: `STOP` обязан быть в группе `cancelled` (иначе ветка отмены молча не
   активируется); в терминальных группах (`completed`/`cancelled`) — ТОЛЬКО
   Done/Cancelled/STOP, иначе terminal-detection ложно сочтёт живую задачу терминальной.
3. **Лейблы**: `autoApprove`, `autoDeploy`, `Bug` (имена — из конфига оркестратора; их
   отсутствие = fail-safe ручной режим / полный цикл).
4. **🖐 РУЧНОЙ ШАГ — порядок статусов на доске**: drag-and-drop в UI (API не управляет
   порядком). Проверка: открой доску проекта — колонки в порядке конвейера.
5. **Workspace-webhook**: уже **существует** (один на весь workspace, создан на уровне
   workspace заранее) — CLI его НЕ создаёт, только напоминает проверить:

   ```bash
   docker exec -e PGPASSWORD=plane plane-app-plane-db-1 psql -U plane -d plane -c \
     "SELECT id, url, is_active FROM webhooks;"
   ```

---

## 2. Слой Gitea: репо + per-repo webhook

1. **Репо** `--gitea-owner/--repo`: создаётся пустым (`auto_init=false`; ветку `main` создаст
   initial push следующего слоя). Существует → `skipped(exists)`.
2. **Per-repo webhook**: `events: push/pull_request/status`, `content_type: json`,
   `branch_filter: *`, URL = `--webhook-url`. **Секрет переиспользуется** из
   `ORCH_GITEA_WEBHOOK_SECRET` (приёмник валидирует ОДИН глобальный секрет на все репо;
   новый секрет сломал бы HMAC существующих вебхуков). Секрета нет в env → CLI сгенерирует и
   выведет строку для `.env` — **🖐 РУЧНОЙ ШАГ**: добавить её в `.env` (в гит не коммитить).
   Формат и проверка — `docs/operations/SETUP_WEBHOOKS.md`. Проверка:

   ```bash
   curl -s -H "Authorization: token $ORCH_GITEA_TOKEN" \
     "$ORCH_GITEA_URL/api/v1/repos/<owner>/<repo>/hooks" | python3 -m json.tool
   ```
3. **Branch protection `main` НЕ включать** (ADR D10): required-approvals/status-checks ломают
   PR-merge API merge-актора конвейера (ложные HOLD). Защита держится конвенцией + скоупом
   токенов.

---

## 3. Слой kit: материализация + initial push

1. `apply` рендерит kit (`onboarding/repo-skeleton/`, плейсхолдеры `{{NAME}}` из
   `onboarding/placeholders.json`) во временный каталог, докладывает live-copy канона
   (`docs/_templates/` 16 скелетов + `docs/_standards/` 3 стандарта — verbatim из текущего
   чекаута, BR-2 «канон не форкается») и пушит **ТОЛЬКО в свежесозданный/пустой репо**
   (единственный разрешённый push; коммит `feat: onboarding skeleton (ORCH-009 kit)`).
2. Репо непустой → шаг помечается `manual-step`: **🖐 РУЧНОЙ ШАГ** — занеси недостающие
   файлы обычным PR с ревью; поверх существующего контента ничего не пушится (BR-9).
3. После рендера не должно остаться ни одного `{{...}}`: CLI падает на этом сам; повторная
   проверка — `verify` (скан плейсхолдеров в файлах репо).

---

## 4. Регистрация в реестре оркестратора

> ⚠️ **САМЫЙ ВАЖНЫЙ РУЧНОЙ СЛОЙ.** CLI `.env` прода НЕ правит и контейнер НЕ рестартит
> (инвариант NFR-2) — он только печатает готовую строку.

1. **🖐 РУЧНОЙ ШАГ — env**: возьми из отчёта `apply` строку
   `ORCH_PROJECTS_JSON=[...полный merged-массив...]` (существующие записи verbatim + новая в
   конец; строка уже провалидирована фактическим парсером реестра) и замени ею строку в `.env`
   оркестратора на хосте. Вставляется атомарно одной строкой — ручное слияние JSON не нужно.
2. **🖐 РУЧНОЙ ШАГ — управляемый рестарт оркестратора**: реестр строится при импорте, нужна
   перезагрузка процесса. **Self-hosting предупреждение: прод-контейнер один на ВСЕ проекты —
   рестарт = групповое окно** (встаёт конвейер всех проектов). Выполняй осознанно: дождись
   тихого окна (`GET /queue` — нет бегущих job), затем штатный рестарт по
   `docs/operations/INFRA.md`. Проверка после рестарта:

   ```bash
   curl -s http://localhost:8500/health
   curl -s http://localhost:8500/queue | python3 -m json.tool | head -30   # реестр жив, конвейер пуст/цел
   ```
3. TTL-self-heal статусов Plane (300с) рестарта НЕ требует: статусы/лейблы, созданные после
   регистрации, подхватятся сами.

---

## 5. Верификация

1. **`verify`-режим CLI** (read-only):

   ```bash
   python3 scripts/onboard_project.py verify --name ... --repo ... --prefix ... \
     --plane-project-id <uuid> --stack ... --test-cmd ... --prod-port ... --staging-port ... \
     --webhook-url https://openclaw.mva154.duckdns.org/orchestrator/webhook/gitea
   ```

   Проверяет: запись реестра парсится и совпадает по полям; все 22 статуса резолвятся
   (включая fail-closed `Confirm Deploy`/`STOP`); лейблы на месте; webhook существует и
   активен; kit-файлы в репо (6 промптов, `AGENTS.md`, `INFRA.md`, `_templates`/`_standards`);
   нет неразрешённых плейсхолдеров. Любой gap → exit `2` с перечнем.

2. **Smoke на песочнице (ADR D8)** — контур: **staging-оркестратор (порт 8501, изолированная
   БД `./data/staging`)** + одноразовый sandbox-проект (рекомендуемые имена: проект
   `onboarding-smoke`, префикс `SMK`, репо `onboarding-smoke`):
   1. Онборди sandbox самим CLI (слои 1–3 этого runbook).
   2. **🖐 РУЧНОЙ ШАГ**: зарегистрируй sandbox в `ORCH_PROJECTS_JSON` **`.env.staging`**
      (не прода!) и перезапусти staging-контейнер (он свободен от прод-инварианта):
      `docker compose --profile staging up -d orchestrator-staging`.
   3. Создай тестовую задачу в sandbox-проекте → доведи до стадии analysis в песочнице.
   4. Критерий PASS: агент по своему промпту **прочитал доку проекта** (следы чтения
      `CLAUDE.md`/`AGENTS.md` в выводе) и **записал артефакты** в `docs/work-items/SMK-…/`
      по канону `PIPELINE_DOCS.md`.
   5. Запротоколируй прогон в «Журнале smoke-прогонов» (ниже). Для приёмки ORCH-009 первый
      протокол обязателен.

---

## 6. Откат

CLI ничего не удаляет (BR-9) — откат всегда ручной и осознанный:

| Что создано | Как откатить | Проверка |
|-------------|--------------|----------|
| Plane-проект (+статусы/лейблы) | удалить проект в UI Plane | проект исчез из списка workspace |
| Gitea-репо (+webhook) | удалить репо в UI/API Gitea (webhook умрёт вместе с ним) | `GET /api/v1/repos/<owner>/<repo>` → 404 |
| Строка реестра | убрать запись из `ORCH_PROJECTS_JSON` в `.env` + управляемый рестарт (см. слой 4, то же групповое окно) | `GET /queue` — проекта нет в реестре |
| Sandbox-артефакты smoke | удалить sandbox-проект/репо после прогона (или архивировать) | см. выше |

---

## Журнал smoke-прогонов

| Дата | Оператор | Параметры (проект/префикс/репо) | Контур | Результат (PASS/FAIL) | Протокол |
|------|----------|----------------------------------|--------|------------------------|----------|
| —    | —        | — (первый прогон фиксируется при приёмке ORCH-009) | staging 8501 | — | — |

---

## Ссылки

- Архитектура решения: `docs/work-items/ORCH-009/06-adr/ADR-001-turnkey-onboarding-kit-and-cli.md`
  (D1…D11); сквозной ADR — `docs/architecture/adr/adr-0035-turnkey-project-onboarding.md`.
- Устройство набора шаблонов и словарь плейсхолдеров: `onboarding/README.md`.
- Формат вебхуков: `docs/operations/SETUP_WEBHOOKS.md`; топология и рестарты —
  `docs/operations/INFRA.md`.
