# LESSONS — ORCH-048 (B6 staging registry isolation, вариант «в»)

**Дата:** 2026-06-06
**Work item:** ORCH-048 — «staging B6 check reads registry from host worktree, not staging container»
**Статус:** ✅ Done. Merge PR #45 (`2a36ed80`), Plane → Done, task 38 → done. Прод не тронут.

---

## TL;DR

B6-чек staging-suite давал **ложный FAIL** (`prod-ET=YES, prod-ORCH=YES`), блокируя `deploy-staging` у **всех** ORCH-задач, хотя изоляция реестра в staging работала корректно. Починили, выбрав архитектурный вариант, который **не порождает новых ловушек автономности**. По дороге словили три урока, которые стоят дороже самой фичи.

---

## 1. Root cause (для истории)

`scripts/staging_check.py` блок **B6** был единственным чеком suite, который не ходил по HTTP к живому инстансу, а **импортировал Python-код локально**:

```python
sys.path.insert(0, "/repos/orchestrator")        # host-worktree
importlib.reload(sys.modules["src.projects"])     # подхватывает env ТЕКУЩЕГО процесса
known = known_plane_project_ids()
```

Деплоер запускал suite **с хоста**, где `ORCH_PROJECTS_JSON` не задан → `src.projects` грузил встроенный `_DEFAULT_PROJECTS` (ET+ORCH) → `known_plane_project_ids()` возвращал боевые id → **ложный FAIL**. То есть B6 проверял реестр НЕ того окружения, реестр которого реально использует staging-инстанс.

Изоляция при этом была исправна: внутри `orchestrator-staging` `known_plane_project_ids()` корректно отдавал только sandbox (`.env.staging`).

---

## 2. ГЛАВНЫЙ УРОК: «курица-яйцо» в staging-гейте

Архитектор на первом прогоне выбрал **вариант (а): новый HTTP-эндпоинт `GET /projects`**, и B6 стал ходить на него. Решение красивое (единый HTTP-стиль с остальными чеками), **но оно само себя заблокировало**:

- B6 проверяет **работающий** staging-инстанс (порт 8501).
- Эндпоинт `/projects` **запечён в Docker-образ** (`src/main.py`).
- В текущем (ещё не пересобранном) образе эндпоинта НЕТ → `GET /projects` → **404** → B6 FAIL → откат на development.
- Чтобы чек прошёл, нужен **ручной bootstrap-деплой** образа. А деплой не происходит, потому что чек красный. **Тупик by design.**

Подтверждено на проде: `GET /projects` на 8501 и 8500 → 404 → `deploy-staging FAILED`.

**Вывод-правило:**
> Staging-чек НЕ должен проверять то, что появляется в работающем инстансе только ПОСЛЕ деплоя проверяемой ветки. Иначе первый прогон всегда падает и требует ручного bootstrap — это прямая поломка автономности.

**Решение — вариант (в):** запускать suite **ВНУТРИ** staging-контейнера (`docker exec orchestrator-staging`), читать реестр из собственного process-env контейнера, убрать host-path хак. Преимущество принципиальное:
- B6 не зависит от того, что отдаёт инстанс по HTTP.
- `staging_check.py` берётся из bind-mount → свежий код подхватывается **без ребилда образа**.
- **Курицы-яйца нет ни на первом прогоне, ни в будущем.**

Вариант (б) (`docker exec ... python3 -c "..."` + парсинг stdout) отклонён: хрупкое экранирование (см. `LESSONS_2026-06-05.md`).

**Как это попало в реализацию:** после FAIL под (а) — откатили ветку к analyst-артефактам (`git reset --hard <analyst-commit>`), стёрли ADR(а)+код(а), зашили в `02-trz.md §4` блок «РЕШЕНИЕ ПРИНЯТО ВЛАДЕЛЬЦЕМ: вариант (в)» с обоснованием и чек-листом, откатили задачу на `architecture` + поставили job архитектору заново. Второй прогон: arch→dev→review→tester→deploy-staging — без петель, **B6 ✓ PASS, 10/10**.

---

## 3. УРОК: орк мержит в main ТОЛЬКО логи, а не фикс-код

После прохождения staging орк сам:
- закрыл задачу в `done`,
- смержил в `main` PR с **логами** (`15-staging-log.md`, `14-deploy-log.md`),
- но **сам фикс-код остался в feature-ветке** — `main` всё ещё содержал старый сломанный B6.

Это by design: фичу в main вливает **владелец**. Поймали проверкой:

```bash
git fetch origin -q
git log --oneline origin/main..origin/feature/<branch>   # покажет невлитые коммиты фикса
git show origin/main:scripts/staging_check.py | grep -c '_evaluate_b6'   # 0 = фикс НЕ в main
```

**Правило:**
> Прежде чем считать задачу реально доставленной — проверить `git log origin/main..feature` и наличие ключевой функции/строки фикса в `origin/main`. `done` в Plane + смерженные логи ≠ код в main.

Финальный шаг: смерджить feature-PR в main (Gitea API, `Do: merge`), затем синхронизировать host-репо.

---

## 4. УРОК: rollout bind-mount-фикса = host `git pull`, без ребилда/рестарта прода

ORCH-048 менял только **bind-mounted / non-runtime** артефакты:

| Файл | Как доходит до прода |
|------|----------------------|
| `scripts/staging_check.py` | bind-mount (`/home/slin/repos` → `/repos`); **не** в образе (`scripts/` нет в `/app`) → host `git pull` → live сразу |
| `.openclaw/agents/deployer.md` | bind-mounted промпт, читается при запуске агента → live на следующем запуске |
| `tests/`, `docs/` | не деплоятся |

`src/` и `Dockerfile` НЕ менялись → **рестарт/ребилд прод-контейнера 8500 не нужен и не делался** (zero group-risk для ET).

**Грабли host-репо:** `git pull` в `/home/slin/repos/orchestrator` сначала упёрся в `sudo: a password is required` — ложная тревога. Репо принадлежит `slin`, sudo не нужен; прямой `git pull --ff-only origin main` прошёл. **Сначала проверь `ls -ld` / `stat -c %U` репо — не лезь в sudo вслепую.**

**Верификация rollout в живом bind-mount (обязательна):**
```bash
grep -c '_evaluate_b6' scripts/staging_check.py                       # >=1
grep -c 'sys.path.insert(0, "/repos/orchestrator")' scripts/staging_check.py  # 0
grep -c 'docker exec orchestrator-staging' .openclaw/agents/deployer.md       # >=1
curl -s -o /dev/null -w '%{http_code}' http://localhost:8500/health   # 200
```

---

## 5. Технические заметки (gotchas)

- **В контейнере orchestrator НЕТ `curl`** — для Gitea/Plane API использовать `urllib` через python (script-file → base64 → `docker cp` → `docker exec`).
- **Plane state-id зависят от проекта.** Approved для проекта orchestrator = `63f2c8fe-dcda-4ace-952f-dd88bd0118ff` (НЕ дефолтный `a519a341...` из кода — тот для sandbox/ET). Брать реальные state-id через `GET .../states/`.
- **BRD-апрув = перевод Plane-issue в статус Approved** → webhook ловит смену статуса → путь `agent=None` → `approved-via-status` → гейт пропускает, БЕЗ повторного запуска `check_analysis_approved`.
- **Dockerfile НЕ копирует `scripts/`** в образ — `staging_check.py` доступен в контейнере только через mount. Путь запуска внутри контейнера учитывать (не `/app/scripts`).
- **Перезапуск стадии вручную:** `update_task_stage(task_id, "<stage>")` + `enqueue_job(agent, repo, task_content, task_id)`. Guard перед этим: `agent_running IS NULL` И нет jobs со `status IN ('queued','running')` для task_id.

---

## 6. Итог по гейтам/ядру после серии ORCH-45/46/47/48

- ✅ `check_ci_green` — поллинг (ORCH-45)
- ✅ `check_tests_passed` — читает `result:` (ORCH-47)
- ✅ `stage_engine` — передаёт деву **текст** findings, не только ссылку (ORCH-46)
- ✅ B6 staging — читает реестр ВНУТРИ staging-контейнера, больше не ложный FAIL (ORCH-48) → **deploy-staging разблокирован для всех ORCH-задач**

Конвейер стал по-настоящему автономным: задача проходит analyst→deploy без ручного пинания стадий.
