Compare commits

..

39 Commits

Author SHA1 Message Date
f52790004e docs(ORCH-109): staging gate log — SUCCESS (8/10, C9a/C9b infra-waived)
Canonical staging_check.py (stub) exit 0; all REAL checks green,
C9a/C9b waived sandbox-infra (ORCH-061).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 14:24:15 +03:00
adebb997e6 Merge pull request 'docs(overview): ORCH-105 — слайды Lite-установки и использования через Plane' (#127) from feature/ORCH-105- into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-12 08:25:52 +03:00
deploy-finalizer
25ce5a22a9 deploy(ORCH-036): finalize SUCCESS for ORCH-105
All checks were successful
CI / test (push) Successful in 57s
2026-06-12 08:25:51 +03:00
c04dba0c0a tester(ET): auto-commit from tester run_id=650
All checks were successful
CI / test (push) Successful in 1m0s
CI / test (pull_request) Successful in 1m3s
2026-06-12 08:19:36 +03:00
95df7278e3 reviewer(ET): auto-commit from reviewer run_id=649 2026-06-12 08:19:36 +03:00
d016ac9b4c docs(overview): ORCH-105 — слайды Lite-установки и использования через Plane
Расширяю слайдо-источник презентации docs/overview/presentation.md тремя
слайдами в каноне ORCH-011 (16 → 19, сквозная нумерация сохранена):

- Слайд «Запуск и ведение задачи через Plane» (вход «To Analyse»,
  статусы = индикация, наблюдение: доска + Telegram-карточка + комментарии).
- Слайд «Что решает человек: гейты, авто-режим, отмена» (Approved /
  Confirm Deploy; autoApprove/autoDeploy/Bug — без пропуска тех. проверок; STOP).
- Слайд «Lite-установка скриптами» (два контейнера платформы; только конфиг;
  gen_secrets.py/onboard_project.py + docker compose up -d; runbook LITE_SETUP.md;
  одношаговый bootstrap — это смежный Bundled, не Lite).

Факты сверены с golden sources (LITE_SETUP.md, tech-pipeline.md,
tech-integrations.md, CLAUDE.md). Анти-дрейф — новая функция
test_presentation_covers_lite_and_plane_usage_bits в tests/test_system_docs.py
(существующие проверки без послаблений). CHANGELOG обновлён.

Docs+tests only: src/**/STAGE_TRANSITIONS/QG_CHECKS/check_*/схема БД —
байт-в-байт; python-pptx не в прод-образе; .pptx в git не коммитится.
Ручная сборка .pptx (TC-07) проверена в dev-venv: «Собрано слайдов: 19», exit 0.

Refs: ORCH-105

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 08:19:36 +03:00
95a09b16b0 architect(ET): auto-commit from architect run_id=647 2026-06-12 08:19:36 +03:00
be5e4e647f architect(ET): auto-commit from architect run_id=646 2026-06-12 08:19:36 +03:00
05d26a8f3e analyst(ET): auto-commit from analyst run_id=645 2026-06-12 08:19:36 +03:00
3f44d51176 docs: init ORCH-105 business request 2026-06-12 08:19:36 +03:00
a8ca4db550 docs(ORCH-105): staging gate log — SUCCESS (8/10, C9a/C9b infra-waived)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 08:19:13 +03:00
4d5e4613e5 Merge pull request 'docs(overview): ORCH-011 — витрина системы docs/overview/ (бизнес+тех, 3 аудитории, презентация)' (#125) from feature/ORCH-011- into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-11 09:42:53 +03:00
deploy-finalizer
a6bf5d1b25 deploy(ORCH-036): finalize SUCCESS for ORCH-011
All checks were successful
CI / test (push) Successful in 55s
2026-06-11 09:42:52 +03:00
7191b8dca2 tester(ET): auto-commit from tester run_id=636
All checks were successful
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m1s
2026-06-11 09:36:40 +03:00
eb92cc6c2c reviewer(ET): auto-commit from reviewer run_id=635 2026-06-11 09:36:40 +03:00
6d798c01ef docs(overview): витрина системы docs/overview/ — бизнес+тех, 3 аудитории, презентация (ORCH-011)
Единая точка входа в документацию платформы (ADR-001 D1–D9):
- docs/overview/ — 10 файлов: индекс (маршруты «Я заказчик / Я менеджер /
  Я разработчик» + норматив «изменил функциональность → обнови витрину в том же
  PR»), business.md (без жаргона, 6 сценариев), 7 тех-блоков (link-first),
  presentation.md (16 слайдов + процедура сборки «команда + Проверка:»).
- scripts/build_presentation.py — генератор .pptx в тёмном дизайне (python-pptx;
  чистый stdlib-парсер parse_slides + ленивый import pptx; бинарь не коммитится,
  build/ в .gitignore; зависимость НЕ в прод-образе — машинный гард TC-09).
- tests/test_system_docs.py — структурный анти-дрейф: derive-сверки стадий/
  гейтов/агентов импортом STAGE_TRANSITIONS/QG_CHECKS/glob промптов/config,
  валидность ссылок, FORBIDDEN-скан + секрет-эвристика, слайды каноническим
  парсером, NFR-2, указатели.
- reviewer.md — ось обзорных доков ORCH-079 расширена на витрину (D7; канон 52d
  байт-в-байт, только текст внутри секций) + анти-регресс ассерт в
  test_agent_prompts_canon.py.
- Указатели: README.md, CLAUDE.md (правила №2/№6, «Структура»),
  PRODUCT_VISION.md (врезка-ссылка), CHANGELOG.md.

Рантайм байт-в-байт: src/**, docker-compose.yml, Dockerfile, requirements* —
ноль изменений (docs+tests+dev-скрипт, паттерн ORCH-102/103). pytest: 1873 passed.

Refs: ORCH-011

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 09:36:40 +03:00
c455931ae7 architect(ET): auto-commit from architect run_id=633 2026-06-11 09:36:40 +03:00
47479a9b75 analyst(ET): auto-commit from analyst run_id=632 2026-06-11 09:36:40 +03:00
6d1230bcc5 docs: init ORCH-011 business request 2026-06-11 09:36:40 +03:00
9b7bdc0c6c docs(ORCH-011): staging gate log — SUCCESS (8/10, C9a/C9b infra-waived)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 09:36:21 +03:00
2c72a889b6 Merge pull request 'feat(replication): ORCH-10b Bundled-тираж — весь стек одним комплектом + bootstrap (ORCH-103)' (#124) from feature/ORCH-103-orch-10b-bundled-bootstrap into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-11 02:22:42 +03:00
deploy-finalizer
cf94fb813f deploy(ORCH-036): finalize SUCCESS for ORCH-103
All checks were successful
CI / test (push) Successful in 54s
2026-06-11 02:22:41 +03:00
6e17f33be4 tester(ET): auto-commit from tester run_id=630
All checks were successful
CI / test (push) Successful in 57s
CI / test (pull_request) Successful in 1m1s
2026-06-11 02:16:32 +03:00
8512dad29e reviewer(ET): auto-commit from reviewer run_id=629 2026-06-11 02:16:32 +03:00
f0cd19d748 feat(replication): ORCH-10b Bundled-тираж — bundle-compose всего стека + bootstrap-скрипт
Закрывает Type B эпика ORCH-10 (по ADR-001 ORCH-103, D1–D11):

- deploy/bundled/docker-compose.yml — самодостаточный compose всего стека
  (орк + watchdog + Gitea 1.22.6 + зеркало upstream Plane CE v0.23.1,
  ~14 контейнеров); project name orchestrator-bundle (узнаваемый префикс),
  container_name не пиннится, staging-контура нет; одна bridge-сеть,
  машинный трафик — сервис-DNS, наружу только человеческие порты;
  GITEA__webhook__ALLOWED_HOST_LIST=orchestrator; все образы пиннованы
  неподвижными тегами. Корневой compose/Dockerfile/src/** — байт-в-байт.
- deploy/bundled/.env.example — конфиг-канон bundle (плейсхолдеры, ни одного
  дефолтного пароля; key-set-sync интерполяций держит тест).
- scripts/bootstrap_bundle.py — python stdlib-only, режимы plan/apply/verify,
  step-движок check→ensure, exit 0/2/1: preflight (fail-fast до мутаций) →
  секреты (gen_secrets.py + stdlib secrets, без перетирания) → up+готовность →
  init Gitea автоматом → init Plane (manual-step с API-верификацией) →
  онбординг строго onboard_project.py apply+verify → token-remote клон →
  сборка .env/.env.watchdog (единственный писатель, права 600) → health.
  Delete-операций нет вообще (D9), секреты не печатаются (NFR-3).
- CHANGELOG.md, CLAUDE.md (абзац Type B), .gitignore (deploy/bundled/repos/).

Док BUNDLED_SETUP.md, REPLICATION §1, arch README, adr-0038 и три структурных
тест-модуля (TC-01…TC-11) — в предыдущих коммитах ветки; полный регресс
1844 passed, ruff по файлам задачи чистый.

Refs: ORCH-103

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 02:16:32 +03:00
215930fb90 developer(ET): auto-commit from developer run_id=627 2026-06-11 02:16:32 +03:00
054b78c8ca architect(ET): auto-commit from architect run_id=626 2026-06-11 02:16:32 +03:00
4050ccbfde analyst(ET): auto-commit from analyst run_id=625 2026-06-11 02:16:32 +03:00
d282d25659 docs: init ORCH-103 business request 2026-06-11 02:16:32 +03:00
c74a68a251 docs(ORCH-103): staging gate log — SUCCESS (8/10, C9a/C9b infra-waived)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 02:16:04 +03:00
0d15719676 Merge pull request 'docs(deployment): ORCH-102 — ORCH-10a Lite-тираж (LITE_SETUP + watchdog-канон + анти-дрейф)' (#123) from feature/ORCH-102-orch-10a-lite-watchdog into main
Some checks failed
CI / test (push) Has been cancelled
2026-06-11 00:48:29 +03:00
deploy-finalizer
f09aff6b43 deploy(ORCH-036): finalize SUCCESS for ORCH-102
All checks were successful
CI / test (push) Successful in 54s
2026-06-11 00:48:28 +03:00
a5f904b56a tester(ET): auto-commit from tester run_id=623
All checks were successful
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 1m1s
2026-06-11 00:42:15 +03:00
56cbf9bd0e reviewer(ET): auto-commit from reviewer run_id=611 2026-06-11 00:42:15 +03:00
8351e91382 docs(deployment): ORCH-10a Lite-тираж — LITE_SETUP.md + канон watchdog-конфига + анти-дрейф контур
Закрывает Type A эпика ORCH-10 (поверх 10-common ORCH-101). Docs+tests
(паттерн ORCH-077/092): src/**, docker-compose.yml, Dockerfile, scripts/** —
ноль изменений; конвейер (STAGE_TRANSITIONS/QG_CHECKS/check_*/machine-verdict/
схема БД) — байт-в-байт.

- docs/deployment/LITE_SETUP.md (D1/D2): golden source Lite-тиража — 13
  нормативных разделов в порядке маршрута оператора, каждый шаг =
  fenced-команда + явная «Проверка:»/PASS/FAIL, хост-специфика только
  плейсхолдерами; канон не форкается (статусы/env/вебхуки/smoke — ссылками
  на ONBOARDING §1 / REPLICATION §2–§4 / SETUP_WEBHOOKS; явно — только
  fail-closed Confirm Deploy/STOP и обязательные ключи нового хоста).
- .env.watchdog.example (D5, исход А-4): третий канонический env-example;
  key-set = блок WATCHDOG_* .env.example (19 ключей, токены — пустые
  плейсхолдеры); закрывает ловушку файла-носителя (sidecar читает ТОЛЬКО
  .env.watchdog); C-1 ORCH-100 + когерентность порта в шапке; .env.watchdog
  добавлен в .gitignore (секрет-гигиена, зеркало .env.staging).
- tests/test_lite_setup_doc.py (D8): 25 структурных тестов без
  сети/LLM/subprocess — 13 разделов в порядке D2, кирпичи FR-6.1, key-sync
  watchdog-канона, env-ключи ⊂ .env.example, compose-подмножество (ровно
  орк+watchdog по дефолту, staging за профилем, анти-появление
  plane*/gitea*), fenced-скан FORBIDDEN (импорт из test_no_host_hardcodes)
  + секрет-эвристика с негативным самочеком, «22 статуса» сверкой импорта
  plane_sync._PLANE_NAME_TO_KEY, перекрёстность.
- Перекрёстные доки (FR-7): REPLICATION.md §1 (Type A — Lite →  ORCH-102 +
  ссылка), README.md (способность Lite + docs/deployment/ в структуре),
  INFRA.md (.env.watchdog в секрет-нормативе + ссылка на deployment),
  CLAUDE.md (блок ORCH-102), CHANGELOG.md.

Нормативы разделов: Gitea — branch protection на main НЕ включать (D3 /
ADR D10 ORCH-009 / INV-4), pre-receive не вводится, ОДИН глобальный
webhook-секрет; staging-вилка опциональна (D6); источник кода —
параметризованный git clone <ORCHESTRATOR_GIT_URL> (D7); stateless —
данные/задачи/секреты боевого хоста НЕ переносятся (AC-3).

Тесты: pytest tests/ -q — 1789 passed (полный регресс зелёный).

Refs: ORCH-102

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 00:42:15 +03:00
443ddc6b6f architect(ET): auto-commit from architect run_id=609 2026-06-11 00:42:15 +03:00
30f1f33af1 analyst(ET): auto-commit from analyst run_id=608 2026-06-11 00:42:15 +03:00
3a103a6e92 docs: init ORCH-102 business request 2026-06-11 00:42:15 +03:00
7d6251d4b6 docs(ORCH-102): staging gate log — SUCCESS (8/10, C9a/C9b infra-waived) 2026-06-11 00:41:48 +03:00
67 changed files with 8439 additions and 7 deletions

6
.gitignore vendored
View File

@@ -11,3 +11,9 @@ data/
.env.watchdog
# ORCH-31: staging DB data directory
data/staging/
# ORCH-103: Bundled-тираж — локальные клоны репо bundle-инсталляции (целевой хост);
# deploy/bundled/.env и deploy/bundled/data покрыты неякорными `.env` / `data/` выше
deploy/bundled/repos/
# ORCH-011 (D5): собранная презентация (scripts/build_presentation.py) — бинарь .pptx
# в git не коммитится, источник истины — docs/overview/presentation.md
build/

View File

@@ -57,7 +57,10 @@ tools:
ограничения» (обзорная витрина проекта), README ДОЛЖЕН быть обновлён в том же PR — пункт снят
или помечен закрытым с ORCH-ссылкой. Необновление обзорных доков → **finding ≥ P1**; если
ограничение закрыто правкой `src/` без обновления README — это совпадает с P0 «`src/` изменён,
документация не обновлена». Это усиление трактовки оси, а не отдельная ось.
документация не обновлена». Это усиление трактовки оси, а не отдельная ось. Та же ось
покрывает **витрину системы** (ORCH-011): PR меняет функциональность, описанную в витрине
`docs/overview/` (стадии, гейты, агенты, интеграции, способности из `business.md`), а витрина
не обновлена → **finding ≥ P1** — расширение трактовки той же оси, не новая ось.
</task>
<deliverables>
@@ -77,6 +80,9 @@ frontmatter-вердиктом, см. `<output_format>`).
- ❌ PR закрыл пункт из `README.md` «Известные ограничения», но README не обновлён (пункт остался
открытым) → ✅ требуй обновления обзорных доков — пункт снят либо помечен закрытым с ORCH-ссылкой;
необновление обзорной витрины → **finding ≥ P1** (ORCH-079).
- ❌ PR меняет функциональность, описанную в витрине `docs/overview/` (стадии, гейты, агенты,
интеграции, способности из `business.md`), но витрина не обновлена → ✅ требуй обновления витрины
в том же PR; необновление → **finding ≥ P1** (расширение оси обзорных доков ORCH-079 — ORCH-011).
**Severity:**
- **P0 (blocker):** не реализовано требование ТЗ; нарушен ADR; критическая уязвимость;

View File

@@ -1,4 +1,4 @@
Work item: ORCH-009
Work item: ORCH-011
Repo: orchestrator
Branch: feature/ORCH-009-turnkey-plane
Branch: feature/ORCH-011-
Stage: development

View File

@@ -3,6 +3,17 @@
Формат: [Keep a Changelog](https://keepachangelog.com/). Записи — на смысловой PR/задачу.
## [Unreleased]
- **Презентация: слайды Lite-установки и использования через Plane** (ORCH-105, `docs`): слайдо-источник `docs/overview/presentation.md` расширен тремя слайдами в каноне ORCH-011 (16 → 19, сквозная нумерация сохранена): один слайд про **Lite-установку скриптами** (два контейнера платформы — оркестратор + сторож на инфре заказчика; развёртывание без правки кода, только конфиг; помощники `gen_secrets.py`/`onboard_project.py` + `docker compose up -d`; runbook `LITE_SETUP.md` с проверкой каждого шага; одношаговый bootstrap — это смежный Bundled, не Lite) и два слайда оператор-инструкции **«как пользоваться орком через Plane»** (запуск через статус «To Analyse»; статусы Plane — индикация, не управление; оба человеческих гейта «Approved»/«Confirm Deploy»; авто-лейблы `autoApprove`/`autoDeploy`/`Bug` — снимают только человеческие решения, ни одна техническая проверка не пропускается; отмена через «STOP»; наблюдение — статусы доски + живая Telegram-карточка + комментарии со ссылками на ветку/PR). Факты сверены с golden sources (`docs/deployment/LITE_SETUP.md`, `docs/overview/tech-pipeline.md`, `tech-integrations.md`, `CLAUDE.md`). **Docs+tests only:** `src/**`/`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/схема БД — байт-в-байт; новый QG не вводится; `python-pptx` не добавлен в прод-образ; собранный `.pptx` в git не коммитится. Анти-дрейф — новая функция `test_presentation_covers_lite_and_plane_usage_bits` в `tests/test_system_docs.py` (существующие проверки без послаблений). ADR: `docs/work-items/ORCH-105/06-adr/ADR-001-presentation-lite-and-plane-usage-slides.md` (канон витрины не меняется — `adr-0039-system-overview-docs-canon.md`).
- **Витрина системы `docs/overview/`: бизнес + тех, маршруты трёх аудиторий, презентация** (ORCH-011, `docs`): единая точка входа в документацию платформы — новый docs-раздел `docs/overview/` (плоский каталог, 10 файлов, ADR-001 D1): индекс `README.md` (маршруты «Я заказчик / Я менеджер / Я разработчик» + норматив сопровождения «изменил функциональность → обнови витрину в том же PR»), бизнес-часть `business.md` (проблема → решение → что умеет фактически → ценность → 6 сценариев; без жаргона, цифры только с атрибуцией), 7 тех-блоков `tech-*.md` (архитектура со схемой потока, конвейер/гейты, агенты, модель объектов, интеграции, качество/безопасность, наблюдаемость; link-first — за деталями ссылки в golden sources, разрешённый дубль только машинно-сверяемый). **Docs+tests+dev-скрипт** (паттерн ORCH-102/103): `src/**`/`docker-compose.yml`/`Dockerfile`/`requirements*`/`STAGE_TRANSITIONS`/`QG_CHECKS`/machine-verdict/схема БД — ноль изменений. ADR: `docs/work-items/ORCH-011/06-adr/ADR-001-system-overview-canon.md`, сквозной `adr-0039-system-overview-docs-canon.md`.
- **Презентация (D4/D5):** слайдо-источник `docs/overview/presentation.md` (16 слайдов в машинно-парсимой структуре «## Слайд N: …» + процедура сборки «команда + Проверка:») + dev-скрипт `scripts/build_presentation.py` (python-pptx, тёмный дизайн, редактируемый текст с точной кириллицей; чистый stdlib-парсер `parse_slides` + ленивый импорт pptx). Запуск только вне рантайма; `python-pptx` НЕ в прод-образе (машинный гард); собранный `.pptx` в git не коммитится — `build/` в `.gitignore`.
- **Анти-дрейф (D6):** новый структурный `tests/test_system_docs.py` (без сети/LLM/subprocess, паттерн `test_lite_setup_doc.py`) — 10 файлов витрины; маршруты/норматив; derive-сверки с кодом: стадии импортом `src.stages.STAGE_TRANSITIONS` (вкл. `deploy-staging`/`cancelled`, порядок цепочки), exit-гейты и под-гейты именами реестра `QG_CHECKS` в нормативном порядке security → merge → coverage → image-freshness (+ маркер «не стадии»), 6 агентов glob'ом промптов, таблица эффортов class-default'ами config (ORCH-41/81); валидность относительных ссылок + обязательные golden-source ссылки; полнотекстовый FORBIDDEN-скан (импорт из `test_no_host_hardcodes.py`) + секрет-эвристика + запрет вне-репозиторных путей; слайды каноническим парсером; `pptx` отсутствует в `requirements*`/`Dockerfile`; указатели README/CLAUDE/CHANGELOG.
- **Reviewer-ось (D7):** ось обзорных доков ORCH-079 в `.openclaw/agents/reviewer.md` точечно расширена на витрину (необновлённая витрина при изменении описанной в ней функциональности → finding ≥ P1; канон 52d байт-в-байт, только добавление внутрь существующих секций) + анти-регресс ассерт в `tests/test_agent_prompts_canon.py`; зеркальные правки правил №2/№6 `CLAUDE.md`.
- **Указатели (D8):** `README.md` — ссылка на витрину; `CLAUDE.md` — указатель в правиле №2 и строке «Структура»; `docs/PRODUCT_VISION.md` — врезка-ссылка «фактическое состояние — витрина» (vision не переписывается; расхождения vision↔код в витрину не переносятся — она строится от кода).
- **ORCH-10b Bundled-тираж: весь стек одним комплектом + bootstrap-скрипт** (ORCH-103, `feat`): закрыт Type B эпика ORCH-10 — заказчик **без собственной инфраструктуры** получает конвейер «под ключ»: одна команда `docker compose -f deploy/bundled/docker-compose.yml up -d` поднимает весь стек (орк + watchdog + Gitea + зеркало upstream Plane CE ≈14 контейнеров), один прогон `scripts/bootstrap_bundle.py apply` доводит его до рабочего состояния. Рантайм байт-в-байт: `src/**`/корневой compose/`Dockerfile`/`STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД — ноль изменений (паттерн ORCH-009/102, kill-switch не нужен — активация только явным запуском оператора на целевом хосте). ADR: `docs/work-items/ORCH-103/06-adr/ADR-001-bundled-stack-compose-and-bootstrap.md`, сквозной `adr-0038-bundled-replication-canon.md`.
- **Bundle-compose (D1D4):** новый top-level каталог `deploy/` (дистрибутивы развёртывания); `deploy/bundled/docker-compose.yml` — один самодостаточный файл, project name `orchestrator-bundle` (узнаваемый префикс томов/контейнеров, по нему preflight детектирует «грязный хост»); `container_name` не пиннится (bundle и Lite не сталкиваются на одном хосте); staging-контура орка нет вовсе (self-hosting у заказчика = маршрут Lite). Все сторонние образы пиннованы неподвижными тегами (Plane CE v0.23.1 upstream-имена сервисов, Gitea 1.22.6, postgres/valkey/rabbitmq/minio). Сеть — одна bridge: машинный трафик строго сервис-DNS (`http://orchestrator:8500/webhook/plane|gitea`, `ORCH_GITEA_URL=http://gitea:3000`), наружу — только человеческие порты `BUNDLE_ORCH_PORT`/`BUNDLE_PLANE_PORT`/`BUNDLE_GITEA_HTTP_PORT`; postgres/redis/mq/minio не публикуются; мина Gitea закрыта `GITEA__webhook__ALLOWED_HOST_LIST=orchestrator`. Конфиг-канон — `deploy/bundled/.env.example` (только плейсхолдеры, ни одного дефолтного пароля; key-set-sync интерполяций держит тест); runtime-конфиг орка/watchdog — корневые `.env`/`.env.watchdog` (канон Lite 1:1, `env_file required: false` — первый `up` живёт до сборки конфига).
- **Bootstrap (D5D8):** `scripts/bootstrap_bundle.py` — python stdlib-only (модули платформы не импортируются, держится ast-сканом), режимы `plan` (дефолт, ноль мутаций) / `apply` / `verify`, step-движок check→ensure (повторный запуск = каскад skip, resume после manual-step = повторный запуск), exit-контракт 0/2/1. Шаги: preflight (fail-fast ДО мутаций: docker/compose, порты, RAM/диск, чистота хоста по префиксу) → секреты (webhook — **строго** субпроцессом `gen_secrets.py`; bundle-креды — stdlib `secrets`; существующие не перетираются без `--force-secrets`; значения не печатаются) → up+готовность (healthchecks + poll, migrator exit 0) → init Gitea полностью автоматом (`gitea admin user create`/`generate-access-token`; branch protection НЕ настраивается — норматив D10 ORCH-009/INV-4) → init Plane (честные manual-step c API-верификацией результата; workspace-webhook — ensure с fallback на manual-step) → онбординг sandbox-проекта **строго** `onboard_project.py apply+verify` (нулевой дрейф канона статусов/лейблов) → git-доступ агентов HTTP token-remote (ssh-контур не вводится) → сборка корневых `.env`/`.env.watchdog` (bootstrap — единственный писатель, права 600) → health/итоговая сводка PASS/FAIL. Delete-операций НЕТ вообще (D9): teardown — только документированная процедура.
- **Док-канон (D10):** `docs/deployment/BUNDLED_SETUP.md` — 14 разделов в порядке маршрута оператора (рамка → требования к хосту с цифрами RAM/диск/CPU и картой портов («Plane ≈ 14 контейнеров») → предусловия → код → секреты → запуск → bootstrap с перечнем manual-step → LLM/Telegram/онбординг ссылками на LITE_SETUP §7§8/ONBOARDING → smoke (REPLICATION §4) → stateless-проверка → остановка/полный сброс → траблшутинг); каждый шаг = fenced-команда + «Проверка:» PASS/FAIL; REPLICATION.md §1 — строка Type B → ✅ ORCH-103. **Норматив сопровождения (NFR-5):** меняешь шаги Bundled-тиража → обнови BUNDLED_SETUP.md в том же PR.
- **Анти-дрейф (D11):** три структурных тест-модуля без docker/сети/LLM — `tests/test_bundle_compose.py` (состав сервисов, пины образов, изоляция томов, key-set-sync, заморозка корневого compose), `tests/test_bundled_setup_doc.py` (14 разделов, FORBIDDEN — импорт из `test_no_host_hardcodes.py`, секрет-эвристика hex≥32/alnum≥40, env-ключи ⊆ канонов, «22 статуса» импортом `plane_sync`, кросс-рефы, CHANGELOG), `tests/test_bootstrap_script.py` (кирпичи, stdlib-only, нет delete-операций/своего списка статусов, unit чистых функций preflight/плана/рендера, exit 0/2/1). `.gitignore` дополнен `deploy/bundled/repos/` (клоны целевого хоста не коммитятся; `.env`/`data/` уже покрыты неякорными паттернами).
- **ORCH-10a Lite-тираж: инструкция LITE_SETUP + канон watchdog-конфига + анти-дрейф контур** (ORCH-102, `docs`): закрыт Type A эпика ORCH-10 — заказчик разворачивает у себя **только орк+watchdog** и донастраивает окружение (Plane/Gitea/Telegram/LLM) по одной сквозной инструкции «голый хост → работающий конвейер». **Docs+tests** (паттерн ORCH-077/092): `src/**`/`docker-compose.yml`/`Dockerfile`/`scripts/**` — ноль изменений; конвейер (`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схема БД) — байт-в-байт. ADR: `docs/work-items/ORCH-102/06-adr/ADR-001-lite-setup-doc-canon.md`, сквозной `adr-0037-lite-replication-canon.md`.
- **Главный продукт (D1/D2):** новый docs-раздел `docs/deployment/` (витрина тиража, читатель — внешний оператор) с golden source `docs/deployment/LITE_SETUP.md` — 13 нормативных разделов в порядке маршрута оператора (рамка → предусловия хоста → перенос кода → конфигурация → Plane → Gitea → LLM → Telegram → запуск → регистрация проекта → smoke → stateless-проверка → траблшутинг ×7); каждый шаг = fenced-команда + явная «Проверка:»/PASS/FAIL; хост-специфика — только плейсхолдеры `<...>`/`$ENV_VAR`; канон не форкается — статусы/env/вебхуки/smoke ссылками на ONBOARDING §1 / REPLICATION §2§4 / SETUP_WEBHOOKS.
- **Канон watchdog-конфига (D5, исход А-4):** новый `.env.watchdog.example` (третий env-example; key-set = блок `WATCHDOG_*` `.env.example`, 19 ключей, токены — пустые плейсхолдеры) закрывает ловушку файла-носителя: sidecar читает ТОЛЬКО `.env.watchdog`, ключ `WATCHDOG_*` в `.env` для него инертен; шапка несёт C-1 (ORCH-100: свой бот, токен орка переиспользовать запрещено) и когерентность порта `WATCHDOG_METRICS_URL``ORCH_DEPLOY_PROD_TARGET_PORT`; `.env.watchdog` добавлен в `.gitignore` (секрет-гигиена, зеркало `.env.staging`).

View File

@@ -28,7 +28,7 @@
- `src/qg/checks.py` — Quality Gate проверки
- `src/webhooks/` — приём вебхуков Plane/Gitea
- `tests/` — pytest
- `docs/` — документация, ADR, work-items, operations
- `docs/` — документация, ADR, work-items, operations; **витрина системы — `docs/overview/`** (единая точка входа «бизнес + тех», ORCH-011)
- `scripts/` — утилиты (staging_check.py, orchestrator-deploy-hook.sh)
## Конвейер (кратко; детали — docs/architecture/README.md)
@@ -350,6 +350,36 @@ API → `manual-step` (fail-safe); **runbook** `docs/operations/ONBOARDING.md` (
`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`
@@ -365,11 +395,11 @@ API → `manual-step` (fail-safe); **runbook** `docs/operations/ONBOARDING.md` (
## Правила для агентов
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`.
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 (витрина проекта не должна выдавать решённое за открытое).
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` →

View File

@@ -1,6 +1,9 @@
# Multi-Agent Orchestrator
> См. [CLAUDE.md](CLAUDE.md) (паспорт проекта) и [docs/architecture/README.md](docs/architecture/README.md) (архитектура).
>
> **Витрина системы — [docs/overview/](docs/overview/README.md)**: единая точка входа в документацию
> (бизнес + тех, 7 блоков, маршруты для заказчика / менеджера / разработчика, презентация). ORCH-011.
FastAPI-сервис для оркестрации мульти-агентного пайплайна разработки. Принимает webhooks от Plane и Gitea, управляет жизненным циклом задач через Quality Gates, запускает Claude CLI агентов на каждой стадии.

View File

@@ -0,0 +1,61 @@
# deploy/bundled/.env — конфиг bundle-ИНФРЫ (ORCH-103, ADR-001 D2).
# Канонический example: 100% ключей интерполяции deploy/bundled/docker-compose.yml
# (key-set-sync держит tests/test_bundle_compose.py) + ключи init-кред, которые
# заполняет bootstrap. Создание: cp .env.example .env (или это сделает
# scripts/bootstrap_bundle.py apply); права 600.
#
# ⚠️ СЕМАНТИКА ФАЙЛА-НОСИТЕЛЯ (TR-8): этот файл читает ТОЛЬКО compose-интерполяция
# bundle (авто-чтение .env из project dir deploy/bundled/). Runtime-конфиг самого
# оркестратора и watchdog — КОРНЕВЫЕ .env / .env.watchdog (каноны Lite 1:1:
# .env.example / .env.watchdog.example, карта — docs/operations/REPLICATION.md §2).
# Единственный писатель всех live-файлов — scripts/bootstrap_bundle.py: дублируемые
# ключи (uid/gid, HOME, пути Claude CLI) когерентны механически, не дисциплиной.
#
# DO NOT COMMIT реальный deploy/bundled/.env (покрыт неякорным `.env` в .gitignore).
# Секреты: НИ ОДНОГО дефолтного пароля — пустые значения ниже генерирует bootstrap
# (stdlib secrets) и никогда не печатает (NFR-3); повторный запуск НЕ перетирает
# существующие значения без явного --force-secrets.
# --- Публичная точка инсталляции -------------------------------------------
# Хост, по которому браузер оператора открывает Plane/Gitea и по которому
# строятся публичные ссылки (ORCH_GITEA_PUBLIC_URL / ORCH_PLANE_WEB_URL / WEB_URL
# Plane / ROOT_URL Gitea). HTTPS/домены/reverse-proxy заказчика — вне bundle.
BUNDLE_PUBLIC_HOST=localhost
# --- Карта публикуемых портов (D4: только человеческие точки) ---------------
# Конфликт порта на хосте → отказ preflight bootstrap ДО любых мутаций (BR-7).
BUNDLE_ORCH_PORT=8500
BUNDLE_PLANE_PORT=8080
BUNDLE_GITEA_HTTP_PORT=3000
# --- Идентичность контейнера орка (реюз имён ORCH-101: один факт = одно имя) --
# uid:gid владельца deploy/bundled/repos (инвариант ORCH-040); docker-gid хоста
# («МИНА 1», узнать: getent group docker). Заполняет bootstrap из id -u/-g/getent.
ORCH_RUN_UID=1000
ORCH_RUN_GID=1000
ORCH_DOCKER_GID=999
# HOME всех акторов в контейнере (группа ORCH-040 двигается одной переменной).
ORCH_AGENT_HOME_DIR=/home/orchestrator
# --- LLM-предусловие хоста заказчика (bundle НЕ поставляет Claude CLI) -------
# Пути дистрибутива claude-code/node и кред CLI на хосте (канон — LITE_SETUP §7).
ORCH_HOST_CLAUDE_CODE_DIR=/usr/lib/node_modules/@anthropic-ai/claude-code
ORCH_HOST_NODE_BIN=/usr/bin/node
ORCH_HOST_CLAUDE_DIR=~/.claude
ORCH_HOST_CLAUDE_JSON=~/.claude.json
# --- Внутренние креды Plane CE-стека (upstream-имена; значения — bootstrap) --
POSTGRES_USER=plane
POSTGRES_PASSWORD=
POSTGRES_DB=plane
SECRET_KEY=
RABBITMQ_DEFAULT_USER=plane
RABBITMQ_DEFAULT_PASS=
RABBITMQ_DEFAULT_VHOST=plane
MINIO_ROOT_USER=plane-minio-admin
MINIO_ROOT_PASSWORD=
# --- Init-креды Gitea (D6: один пользователь-бот = админ, владелец репо,
# носитель API-токена; создаёт bootstrap через `gitea admin user create`) --
GITEA_ADMIN_USERNAME=orchestrator-bot
GITEA_ADMIN_PASSWORD=

View File

@@ -0,0 +1,338 @@
# ORCH-103 (Type B Bundled, ADR-001 D1D4): самодостаточный compose ВСЕГО стека
# для тиража «под ключ» на хост заказчика: orchestrator + orchestrator-watchdog +
# Gitea + Plane CE (зеркало официального selfhost-référence makeplane/plane
# v0.23.1: имена сервисов и env-контракт — upstream, анти-дрейф к их докам; наши
# отличия от référence: пиннинг неподвижными тегами литералом вместо ${APP_RELEASE}
# (NFR-6, держится tests/test_bundle_compose.py), убраны replicas/platform/SENTRY,
# секреты БЕЗ дефолтных значений — их генерирует scripts/bootstrap_bundle.py).
#
# Этот файл НЕ исполняется в нашем прод-контуре (корневой docker-compose.yml —
# байт-в-байт, заморожен анти-дрейфом ORCH-102); активация — только явный запуск
# оператором на целевом хосте (паттерн ORCH-009, kill-switch не нужен).
#
# Конфиг-слои (D2): интерполяции ${VAR} читаются compose'ом из deploy/bundled/.env
# (авто-чтение из project dir — без --env-file-футгана); канон ключей —
# deploy/bundled/.env.example (key-set-sync держит тест). Runtime-конфиг орка и
# watchdog — КОРНЕВЫЕ .env / .env.watchdog (канон Lite 1:1, REPLICATION §2);
# их единственный писатель — bootstrap_bundle.py.
#
# Сеть (D4): одна bridge-сеть проекта; машинный трафик — строго сервис-DNS
# (Plane→орк http://orchestrator:8500/webhook/plane, Gitea→орк .../webhook/gitea,
# орк→Plane http://proxy, орк→Gitea http://gitea:3000); network_mode: host НЕ
# используется (ssh-деплой-контур нашего хоста в bundle структурно спит —
# ORCH_DEPLOY_SSH_HOST пуст). Наружу публикуются ТОЛЬКО человеческие порты
# (орк/Plane proxy/Gitea web); postgres/redis/mq/minio не публикуются.
#
# Project name = узнаваемый префикс томов/контейнеров orchestrator-bundle_* (D1);
# container_name сознательно НЕ пиннится ни у кого — bundle и Lite/корневой
# compose не сталкиваются по именам на одном хосте.
name: orchestrator-bundle
networks:
default:
name: orchestrator-bundle
driver: bridge
# Env-контракт Plane CE — upstream-имена (référence v0.23.1). Значения секретов
# (POSTGRES_PASSWORD/SECRET_KEY/RABBITMQ_DEFAULT_PASS/MINIO_ROOT_PASSWORD) живут
# ТОЛЬКО в deploy/bundled/.env (генерирует bootstrap); дефолтных паролей нет.
x-plane-env: &plane-env
environment:
- WEB_URL=http://${BUNDLE_PUBLIC_HOST:-localhost}:${BUNDLE_PLANE_PORT:-8080}
- DEBUG=0
- CORS_ALLOWED_ORIGINS=http://${BUNDLE_PUBLIC_HOST:-localhost}:${BUNDLE_PLANE_PORT:-8080}
- GUNICORN_WORKERS=1
# db (upstream-имена; host/port — фиксированные сервис-DNS этого файла)
- PGHOST=plane-db
- PGDATABASE=${POSTGRES_DB:-plane}
- POSTGRES_USER=${POSTGRES_USER:-plane}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB:-plane}
- POSTGRES_PORT=5432
- PGDATA=/var/lib/postgresql/data
- DATABASE_URL=postgresql://${POSTGRES_USER:-plane}:${POSTGRES_PASSWORD}@plane-db:5432/${POSTGRES_DB:-plane}
# redis
- REDIS_HOST=plane-redis
- REDIS_PORT=6379
- REDIS_URL=redis://plane-redis:6379/
# rabbitmq
- RABBITMQ_HOST=plane-mq
- RABBITMQ_PORT=5672
- RABBITMQ_DEFAULT_USER=${RABBITMQ_DEFAULT_USER:-plane}
- RABBITMQ_DEFAULT_PASS=${RABBITMQ_DEFAULT_PASS}
- RABBITMQ_DEFAULT_VHOST=${RABBITMQ_DEFAULT_VHOST:-plane}
- RABBITMQ_VHOST=${RABBITMQ_DEFAULT_VHOST:-plane}
- AMQP_URL=amqp://${RABBITMQ_DEFAULT_USER:-plane}:${RABBITMQ_DEFAULT_PASS}@plane-mq:5672/${RABBITMQ_DEFAULT_VHOST:-plane}
# application secret (генерирует bootstrap; дефолта сознательно НЕТ)
- SECRET_KEY=${SECRET_KEY}
# datastore (minio)
- USE_MINIO=1
- AWS_REGION=
- AWS_ACCESS_KEY_ID=${MINIO_ROOT_USER:-plane-minio-admin}
- AWS_SECRET_ACCESS_KEY=${MINIO_ROOT_PASSWORD}
- AWS_S3_ENDPOINT_URL=http://plane-minio:9000
- AWS_S3_BUCKET_NAME=uploads
- MINIO_ROOT_USER=${MINIO_ROOT_USER:-plane-minio-admin}
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
- BUCKET_NAME=uploads
- FILE_SIZE_LIMIT=5242880
# live server
- API_BASE_URL=http://api:8000
# proxy
- NGINX_PORT=80
services:
# ── Платформа: орк + sidecar-watchdog (образы собираются из этого же чекаута;
# корневой Dockerfile / watchdog/Dockerfile — без правок, NFR-1) ──────────
orchestrator:
build:
context: ../..
# ORCH-101 (D5): uid/gid/home двигаются ОДНОЙ группой с user: и таргетами
# маунтов ниже (инвариант ORCH-040). Дефолты bundle нейтральны (D2).
args:
APP_UID: ${ORCH_RUN_UID:-1000}
APP_GID: ${ORCH_RUN_GID:-1000}
APP_HOME: ${ORCH_AGENT_HOME_DIR:-/home/orchestrator}
restart: unless-stopped
user: "${ORCH_RUN_UID:-1000}:${ORCH_RUN_GID:-1000}"
init: true
command: ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8500"]
ports:
# человеческая точка: операторский smoke `curl /health` (D4)
- "${BUNDLE_ORCH_PORT:-8500}:8500"
volumes:
# данные/репозитории — bind ВНУТРИ project dir (uid-причины ORCH-040;
# покрыты .gitignore: неякорный data/ + deploy/bundled/repos/)
- ./data:/app/data
- ./repos:/repos
- /var/run/docker.sock:/var/run/docker.sock
# LLM-предусловие хоста заказчика (bundle его НЕ поставляет, BRD §1.3)
- ${ORCH_HOST_CLAUDE_CODE_DIR:-/usr/lib/node_modules/@anthropic-ai/claude-code}:/opt/claude-code:ro
- ${ORCH_HOST_NODE_BIN:-/usr/bin/node}:/usr/bin/node:ro
- ${ORCH_HOST_CLAUDE_DIR:-~/.claude}:${ORCH_AGENT_HOME_DIR:-/home/orchestrator}/.claude
- ${ORCH_HOST_CLAUDE_JSON:-~/.claude.json}:${ORCH_AGENT_HOME_DIR:-/home/orchestrator}/.claude.json:ro
# ssh-контур в bundle сознательно НЕ вводится (ADR D8): git-доступ агентов
# — HTTP token-remote, деплой-хуки нашего хоста структурно спят.
# runtime-конфиг орка собирает bootstrap (шаг 8); required:false — первый
# `up -d` поднимает стек ДО сборки конфига (AC-1), орк жив без него.
env_file:
- path: ../../.env
required: false
environment:
- ORCH_REPOS_DIR=/repos
group_add:
- "${ORCH_DOCKER_GID:-999}"
orchestrator-watchdog:
build:
context: ../..
dockerfile: watchdog/Dockerfile
restart: unless-stopped
init: true
mem_limit: 128m
mem_reservation: 32m
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./repos:/repos:ro
- ./data:/app/data:ro
env_file:
- path: ../../.env.watchdog
required: false
environment:
# bundle-сеть ≠ host-network Lite: метрики — по сервис-DNS; имя контейнера
# орка детерминировано project name (container_name не пиннится, D1).
# environment перекрывает env_file → когерентность механическая (TR-8).
- WATCHDOG_METRICS_URL=http://orchestrator:8500/metrics
- WATCHDOG_CONTAINERS=orchestrator-bundle-orchestrator-1
group_add:
- "${ORCH_DOCKER_GID:-999}"
# ── Gitea (D6): официальный образ, НЕ rootless; init полностью автоматом —
# bootstrap создаёт админа/токен через `gitea admin ...` CLI в контейнере.
# Branch protection на main НЕ настраивается (норматив D10 ORCH-009/INV-4).
gitea:
image: gitea/gitea:1.22.6
restart: unless-stopped
ports:
- "${BUNDLE_GITEA_HTTP_PORT:-3000}:3000"
environment:
- GITEA__database__DB_TYPE=sqlite3
- GITEA__security__INSTALL_LOCK=true
- GITEA__server__DOMAIN=${BUNDLE_PUBLIC_HOST:-localhost}
- GITEA__server__ROOT_URL=http://${BUNDLE_PUBLIC_HOST:-localhost}:${BUNDLE_GITEA_HTTP_PORT:-3000}/
# ssh-контур не вводится (D8): порт не публикуется, ssh выключен.
- GITEA__server__DISABLE_SSH=true
- GITEA__service__DISABLE_REGISTRATION=true
# МИНА TR-4 (D4): Gitea по умолчанию режет webhook'и в приватные адреса —
# без этой строки «задача не появилась» гарантирован.
- GITEA__webhook__ALLOWED_HOST_LIST=orchestrator
volumes:
- gitea-data:/data
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:3000/api/healthz"]
interval: 10s
timeout: 5s
retries: 12
# ── Plane CE — зеркало upstream selfhost-référence v0.23.1 (D3) ────────────
web:
<<: *plane-env
image: makeplane/plane-frontend:v0.23.1
restart: unless-stopped
command: node web/server.js web
depends_on:
- api
- worker
space:
<<: *plane-env
image: makeplane/plane-space:v0.23.1
restart: unless-stopped
command: node space/server.js space
depends_on:
- api
- worker
- web
admin:
<<: *plane-env
image: makeplane/plane-admin:v0.23.1
restart: unless-stopped
command: node admin/server.js admin
depends_on:
- api
- web
live:
<<: *plane-env
image: makeplane/plane-live:v0.23.1
restart: unless-stopped
command: node live/dist/server.js live
depends_on:
- api
- web
api:
<<: *plane-env
image: makeplane/plane-backend:v0.23.1
restart: unless-stopped
command: ./bin/docker-entrypoint-api.sh
volumes:
- logs_api:/code/plane/logs
depends_on:
- plane-db
- plane-redis
- plane-mq
worker:
<<: *plane-env
image: makeplane/plane-backend:v0.23.1
restart: unless-stopped
command: ./bin/docker-entrypoint-worker.sh
volumes:
- logs_worker:/code/plane/logs
depends_on:
- api
- plane-db
- plane-redis
- plane-mq
beat-worker:
<<: *plane-env
image: makeplane/plane-backend:v0.23.1
restart: unless-stopped
command: ./bin/docker-entrypoint-beat.sh
volumes:
- logs_beat-worker:/code/plane/logs
depends_on:
- api
- plane-db
- plane-redis
- plane-mq
migrator:
<<: *plane-env
image: makeplane/plane-backend:v0.23.1
restart: "no"
command: ./bin/docker-entrypoint-migrator.sh
volumes:
- logs_migrator:/code/plane/logs
depends_on:
- plane-db
- plane-redis
plane-db:
<<: *plane-env
image: postgres:15.7-alpine
restart: unless-stopped
command: postgres -c 'max_connections=1000'
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 12
plane-redis:
<<: *plane-env
image: valkey/valkey:7.2.5-alpine
restart: unless-stopped
volumes:
- redisdata:/data
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 10s
timeout: 5s
retries: 12
plane-mq:
<<: *plane-env
image: rabbitmq:3.13.6-management-alpine
restart: always
volumes:
- rabbitmq_data:/var/lib/rabbitmq
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 15s
timeout: 10s
retries: 12
plane-minio:
<<: *plane-env
# upstream-référence держит latest — bundle пиннит неподвижный тег (NFR-6)
image: minio/minio:RELEASE.2024-05-28T17-19-04Z
restart: unless-stopped
command: server /export --console-address ":9090"
volumes:
- uploads:/export
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:9000/minio/health/live"]
interval: 10s
timeout: 5s
retries: 12
proxy:
<<: *plane-env
image: makeplane/plane-proxy:v0.23.1
restart: unless-stopped
ports:
# человеческая точка: UI Plane в браузере оператора (D4)
- "${BUNDLE_PLANE_PORT:-8080}:80"
depends_on:
- web
- api
- space
# Состояние Plane/Gitea — именованные тома проекта (префикс orchestrator-bundle_,
# D1/D2); preflight bootstrap детектирует «грязный хост» по этому префиксу.
volumes:
pgdata:
redisdata:
uploads:
logs_api:
logs_worker:
logs_beat-worker:
logs_migrator:
rabbitmq_data:
gitea-data:

View File

@@ -4,6 +4,9 @@
**Версия:** 1.0 · **Дата:** 2026-06-04 · **Статус:** концепция развития
> **Фактическое текущее состояние платформы** (что уже умеет, как устроена) — витрина системы
> [docs/overview/](overview/README.md) (ORCH-011). Этот документ — vision: «куда идём».
---
## 1. Зачем это (бизнес-взгляд)

View File

@@ -210,6 +210,62 @@ sidecar читает только `.env.watchdog`; C-1 ORCH-100 — отдель
(docs+tests). Подробнее: [adr-0037](adr/adr-0037-lite-replication-canon.md), детально —
`docs/work-items/ORCH-102/06-adr/ADR-001-lite-setup-doc-canon.md`.
**Type B — Bundled (ORCH-103).** Закрывает эпик ORCH-10: весь стек одним комплектом
(орк + watchdog + Gitea + Plane CE ≈1314 контейнеров) для заказчика без собственной
инфраструктуры. Состав Plane — зеркало официального selfhost-référence v0.23.1
(upstream-имена сервисов web/space/admin/api/worker/beat-worker/migrator/live +
plane-db/plane-redis/plane-mq/plane-minio/proxy); Gitea — `gitea/gitea:1.22.6`
(не rootless, ssh выключен). Новый top-level каталог **`deploy/`** (исполняемые дистрибутивы; дополняет
`docs/deployment/` — инструкции): `deploy/bundled/docker-compose.yml` — один самодостаточный
compose с `name: orchestrator-bundle` (узнаваемый префикс томов/контейнеров; `container_name`
не пиннится — нет коллизий с корневым compose на одном хосте), пиннинг сторонних образов
неподвижными тегами литералом (не `latest`); корневой compose не форкается (заморожен
анти-дрейфом ORCH-102); staging-контур орка в bundle отсутствует, репо `orchestrator` не
регистрируется → self-deploy-машинерия структурно спит (`SELF_HOSTING_REPO`-леафы не матчатся).
Сеть — одна bridge: машинный трафик строго сервис-DNS (webhooks в обе стороны, API, /metrics),
наружу — только человеческие порты (Plane 8080 / Gitea 3000 / орк 8500; явный
`GITEA__webhook__ALLOWED_HOST_LIST=orchestrator` против дефолтного запрета приватных таргетов).
Конфиг-слои: `deploy/bundled/.env.example` (канон bundle-инфры, key-set-sync тест) → live
`deploy/bundled/.env` (авто-чтение compose из project dir, без `--env-file`-футгана); runtime
орка/watchdog — корневые `.env`/`.env.watchdog` ровно по канону Lite (`env_file: required:
false` до сборки); **единственный писатель live-файлов — bootstrap**.
`scripts/bootstrap_bundle.py` (python stdlib-only, `plan`-дефолт/`apply`/`verify`, step-движок
check→ensure, exit 0/2/1): preflight fail-fast до мутаций → секреты (`gen_secrets.py` +
stdlib-креды стека, в логи не печатаются) → up+ожидание готовности → init Gitea (полностью
автоматом через CLI; branch protection НЕ включать — D10 ORCH-009) → init Plane CE (честные
manual-step: инструкция → подтверждение → API-верификация результата) → онбординг
sandbox-проекта строго `onboard_project.py apply`/`verify` (host-venv, канон ONBOARDING) →
git-доступ агентов token-remote (`_push_url`-паттерн; ssh-контур не вводится) → сборка env
орка → health/итог; delete-операций в скрипте нет — teardown только документированной
процедурой (§13). Golden source — `docs/deployment/BUNDLED_SETUP.md` (14 разделов по канону
LITE_SETUP, требования к хосту по замеру тестового развёртывания; REPLICATION §1 — отметка
Type B). Анти-дрейф — `tests/test_bundle_compose.py` / `test_bundled_setup_doc.py` /
`test_bootstrap_script.py`. Рантайм/конвейер — байт-в-байт; kill-switch не нужен (активация —
только явный запуск оператора на целевом хосте, паттерн ORCH-009). Подробнее:
[adr-0038](adr/adr-0038-bundled-replication-canon.md), детально —
`docs/work-items/ORCH-103/06-adr/ADR-001-bundled-stack-compose-and-bootstrap.md`.
## Витрина системы `docs/overview/` (ORCH-011 — design)
Единая точка входа «бизнес + тех» для трёх аудиторий (заказчик / менеджер / разработчик) —
новый docs-раздел **`docs/overview/`** (семантика разделов: `overview/` — «что это за система
и как устроена», `architecture/` — инженерный справочник, `deployment/` — «как развернуть у
себя», `operations/` — «как эксплуатировать наш прод»). Состав — плоский каталог, 10 файлов:
индекс `README.md` (маршруты 3 аудиторий + норматив сопровождения), `business.md`
(бизнес-уровень без жаргона), 7 × `tech-*.md` (= 7 блоков: архитектура / конвейер / агенты /
модель объектов / интеграции / качество-безопасность / наблюдаемость), `presentation.md`
(слайдо-источник). **Link-first:** витрина ссылается на golden sources (этот README, internals,
стандарты, ADR), не форкает их; разрешённый дубль — только машинно-сверяемый тестом факт
(стадии/гейты/агенты — derive-тестами из `STAGE_TRANSITIONS`/`QG_CHECKS`/glob промптов).
**Канон презентации:** `.pptx` (тёмный дизайн) собирается из `presentation.md` dev-скриптом
`scripts/build_presentation.py` (python-pptx, запуск только вне рантайма; зависимость в
прод-образ не попадает — машинный гард); **собранный бинарь в git не коммитится**. **Норматив
сопровождения (кросс-каттинг):** «изменил функциональность → обнови витрину в том же PR»;
reviewer-ось обзорных доков (ORCH-079) расширена на витрину (finding ≥ P1). Анти-дрейф —
структурный `tests/test_system_docs.py` (паттерн `test_lite_setup_doc.py`); новый QG не
вводится, рантайм байт-в-байт. Подробнее: [adr-0039](adr/adr-0039-system-overview-docs-canon.md),
детально — `docs/work-items/ORCH-011/06-adr/ADR-001-system-overview-canon.md`.
## Конвейер и Quality Gates
```

View File

@@ -0,0 +1,114 @@
---
work_item: ORCH-103
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-11
model_used: claude-opus-4-8
---
# adr-0038: Канон Bundled-тиража — `deploy/bundled/` + bootstrap + `BUNDLED_SETUP.md` (ORCH-103, 10b)
## Статус
Proposed
## Контекст
Эпик ORCH-10 (D5 «Масштаб»), тип **B — Bundled**: заказчик без собственной инфраструктуры
получает **весь стек одним комплектом** (орк + watchdog + Gitea + Plane CE ≈1314 контейнеров) и
bootstrap, доводящий его до рабочего конвейера одним запуском. Фундамент готов: 10-common
(ORCH-101, adr-0036 — хост-параметризация/секреты/smoke) и Lite (ORCH-102, adr-0037 — док-канон
`docs/deployment/`). Корневой `docker-compose.yml` заморожен анти-дрейфом ORCH-102 (ровно 3
сервиса, запрет подстрок `plane`/`gitea`) → комплект обязан жить отдельным файлом.
Сквозной характер: вводится новый top-level каталог `deploy/` (дистрибутивы развёртывания),
новый канонический env-example и нормативы, обязательные для будущих задач эпика ORCH-10 и
любого агента, меняющего шаги тиража. Детальный пакет решений (D1…D11, исходы OQ-1…OQ-7 ТЗ) —
work-item ADR: `docs/work-items/ORCH-103/06-adr/ADR-001-bundled-stack-compose-and-bootstrap.md`.
## Решение
1. **Новый top-level каталог `deploy/` — исполняемые дистрибутивы развёртывания** (дополняет
`docs/deployment/` — инструкции). Bundled-комплект: **`deploy/bundled/docker-compose.yml`** —
один самодостаточный compose всего стека с top-level `name: orchestrator-bundle` (project
name = узнаваемый префикс томов/контейнеров; `container_name` не пиннится — нет коллизий с
корневым compose на одном хосте). Staging-контур орка в bundle **отсутствует вовсе**; репо
`orchestrator` в bundle-инсталляции не регистрируется → self-deploy-машинерия структурно спит
(`SELF_HOSTING_REPO`-леафы не матчатся).
2. **Конфиг-слои:** `deploy/bundled/.env.example` — канон bundle-инфры (committed, плейсхолдеры;
key-set-sync тест: каждая `${VAR}`-интерполяция bundle-compose имеет ключ в каноне) → live
`deploy/bundled/.env` (авто-чтение compose из project dir — без `--env-file`-футгана; покрыт
неякорным `.env` в `.gitignore`); runtime орка/watchdog — **корневые `.env`/`.env.watchdog`
ровно по канону Lite** (REPLICATION §2 применим 1:1), в bundle-compose — `env_file:
required: false` (первый `up` жив до сборки конфига). **Bootstrap — единственный писатель**
всех трёх live-файлов (когерентность дублируемых ключей — механическая). Один факт = одно имя
(ORCH-101 D1): существующие факты — существующие `ORCH_*`-имена; bundle-only — `BUNDLE_*`;
внутренние креды Plane — upstream-имена.
3. **Состав/пиннинг:** Plane CE — зеркало официального selfhost-référence (upstream-имена
сервисов/env); Gitea — `gitea/gitea` (не rootless). Пиннинг — **точный неподвижный тег
литералом** (не `latest`, не интерполяция; digest не требуется); точные теги фиксирует
developer по проверенному стенду; форму держит структурный тест.
4. **Сеть:** одна именованная bridge-сеть; машинный трафик — строго сервис-DNS
(`http://orchestrator:8500/webhook/*`, `http://gitea:3000`, plane-proxy); `network_mode: host`
в bundle не используется (ssh-деплой-пути неактивны: `ORCH_DEPLOY_SSH_HOST` пуст). Наружу —
только человеческие порты (Plane proxy 8080 / Gitea 3000 / орк 8500; конфигурируемы);
БД/брокер/minio не публикуются. Публичные URL — от `BUNDLE_PUBLIC_HOST` (split internal/public
уже в конфиге орка). Мина Gitea закрывается явно: `GITEA__webhook__ALLOWED_HOST_LIST=orchestrator`.
5. **Bootstrap `scripts/bootstrap_bundle.py`:** python stdlib-only, без импортов из `src/**`;
режимы `plan` (дефолт, ноль мутаций) / `apply` / `verify`; step-движок check→ensure
(идемпотентность, resume = повторный запуск); exit `0/2/1`. Preflight fail-fast до мутаций
(docker/порты/чистота томов по префиксу/RAM/диск; Claude CLI — warning). **Кирпичи не
дублируются:** секреты — субпроцесс `gen_secrets.py`; статусы/лейблы/репо/вебхуки — строго
`onboard_project.py apply`+`verify` (host-venv, канон ONBOARDING). Init Gitea — полностью
автоматом (CLI в контейнере; branch protection НЕ настраивается — D10 ORCH-009/adr-0037 п.4);
init Plane CE — честные **manual-step чекпоинты** (инструкция → подтверждение →
API-верификация; прогрессивная автоматизация разрешена без смены контракта). Git-доступ
агентов — HTTP token-remote (паттерн `_push_url`); ssh-контур не вводится. Секреты в
логи не печатаются; delete-операций в скрипте нет вообще — teardown только документированной
процедурой (`BUNDLED_SETUP` §13).
6. **Док-канон:** `docs/deployment/BUNDLED_SETUP.md` — 14 нормативных разделов по форме
LITE_SETUP (fenced-команда + «Проверка:» PASS/FAIL, плейсхолдеры, общие шаги ссылками на
LITE_SETUP/ONBOARDING/REPLICATION — канон не форкается), включая «Требования к хосту» с
цифрами **по замеру** тестового развёртывания. REPLICATION §1: Type B → ✅ ORCH-103.
**Норматив сопровождения:** изменил шаги Bundled-тиража → обнови BUNDLED_SETUP.md в том же PR.
7. **Анти-дрейф — постоянная CI-гарантия:** `tests/test_bundle_compose.py` /
`test_bundled_setup_doc.py` / `test_bootstrap_script.py` (структурные, без docker/сети/LLM:
состав сервисов, заморозка корневого compose, пины, key-set-sync, разделы дока, FORBIDDEN —
импортом из `test_no_host_hardcodes.py`, секрет-эвристика, ссылки на кирпичи, отсутствие
delete-операций, unit чистых функций preflight/плана, exit-контракт).
### Что НЕ меняется
`src/**`, корневой `docker-compose.yml`, `Dockerfile`, `.gitea/workflows/**`, `onboarding/**`,
промпты `.openclaw/agents/**`; `STAGE_TRANSITIONS`, состав `QG_CHECKS`, семантика `check_*`,
machine-verdict ключи, схема БД — байт-в-байт. Kill-switch не вводится (активация — только явный
запуск оператора на целевом хосте, паттерн ORCH-009). Прод-контейнер в рамках задачи не
рестартуется; наши данные/секреты не переносятся (stateless, решение Владельца 10.06).
## Альтернативы
- **Расширение корневого compose (профиль `bundled`)** — отвергнуто: заморожен анти-дрейфом
ORCH-102/нормативом «compose не форкается»; смешение дистрибутива с боевым контуром.
- **Include-композиция / live-env через `--env-file`** — отвергнуто: лишние степени свободы
запуска, молчаливые дефолты при забытом флаге.
- **Орк в bundle на host-network + `host-gateway`** — отвергнуто: хост-сеть нужна была
ssh-деплой-контуру нашего хоста, который в bundle спит; bridge даёт чистые двунаправленные
сервис-DNS-URL.
- **Digest-пиннинг / rootless-Gitea / ssh-доступ агентов / bash-bootstrap / reset-режим
скрипта** — отвергнуты (см. work-item ADR-001, «Альтернативы»).
## Последствия
- Эпик ORCH-10 закрыт по обоим типам: A (Lite, инструкция) + B (Bundled, комплект); заказчик
без инфраструктуры разворачивает конвейер «под ключ».
- Цена: пиннованные версии Plane/Gitea стареют (апгрейд — отдельные задачи); manual-step Plane CE
размывают «одну команду» — неустранимо честно (нет API), митигировано контрактом чекпоинта;
двойной `.env`-слой — под единственным писателем-bootstrap и key-sync тестом.
- Откат: удалить `deploy/`, bootstrap, BUNDLED_SETUP.md, три тест-модуля, строку REPLICATION §1 —
состояние 1:1 (docs+scripts+tests, без миграций).
## Связи
adr-0036 (ORCH-101 — фундамент 10-common: параметризация, gen_secrets, REPLICATION/smoke),
adr-0037 (ORCH-102 — док-канон `docs/deployment/`, compose-подмножество, запрет branch
protection), adr-0035 (ORCH-009 — onboarding-CLI: 22 статуса, manual-step паттерн, `_push_url`,
D10), adr-0027/INV-4 (merge-актор — основание норматива Gitea), adr-0001
(`SELF_HOSTING_REPO`-конвенция — почему self-гейты в bundle спят). Детально —
`docs/work-items/ORCH-103/06-adr/ADR-001-bundled-stack-compose-and-bootstrap.md`,
`07-infra-requirements.md`, `10-tech-risks.md`.

View File

@@ -0,0 +1,95 @@
---
work_item: ORCH-011
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-11
model_used: claude-opus-4-8
---
# adr-0039: Витрина системы `docs/overview/` — единая точка входа (бизнес + тех) и канон презентации (ORCH-011)
## Статус
Proposed
## Контекст
Документация платформы богатая, но фрагментированная: паспорт `CLAUDE.md` (реестр доработок),
тех-витрина `README.md`, vision `docs/PRODUCT_VISION.md`, инженерный справочник
`docs/architecture/` (~1246 строк + internals), 38 сквозных ADR, стандарты, операционные и
deployment-доки. Единой точки входа «бизнес + тех» для трёх аудиторий (заказчик / менеджер /
разработчик) нет; презентацию о возможностях собирать не из чего. С тиражируемостью
(ORCH-101/102/103) появился внешний читатель. Решения Владельца: слайды PowerPoint в тёмном
дизайне; единое место — `docs/`; витрина поддерживается актуальной.
Живые доказательства проблемы в самом репо: схема конвейера в `PRODUCT_VISION.md` §2 устарела
(нет `deploy-staging`/`cancelled`); `docs/PRODUCT_VISION.pptx` закоммичен **без пути генерации**
(невоспроизводим). Reviewer-ось обзорных доков (ORCH-079, adr-0023) по букве привязана к
`README.md` «Известные ограничения» — новую витрину не покрывает.
Сквозной характер: вводится новый docs-раздел с нормативом сопровождения, обязательным для
**всех будущих функциональных PR**, расширяется reviewer-ось и фиксируется канон
презентационных артефактов. Детальный пакет решений (D1…D9, исходы OQ-1…OQ-5) — work-item ADR:
`docs/work-items/ORCH-011/06-adr/ADR-001-system-overview-canon.md`.
## Решение
1. **Новый docs-раздел `docs/overview/` — витрина системы.** Семантика разделов после ORCH-011:
`overview/` — «что это за система и как устроена» (вход для любой аудитории), `architecture/`
— инженерный справочник, `deployment/` — «как развернуть у себя», `operations/` — «как
эксплуатировать наш прод», `_standards/` — нормативы агентов. Состав — плоский каталог,
10 файлов: индекс `README.md` (точка входа, 3 маршрута аудиторий, норматив сопровождения),
`business.md` (бизнес-уровень: проблема → решение → способности → ценность → сценарии; без
жаргона; числа только с подтверждением), 7 файлов `tech-*.md` = 7 блоков контент-карты
(архитектура / конвейер / агенты / модель объектов / интеграции / качество-безопасность /
наблюдаемость), `presentation.md` (слайдо-источник).
2. **Link-first, канон не форкается:** витрина даёт цельную картину и ссылается на golden
sources за деталями; запрещён дубль живых таблиц (компоненты, env, статусы). Разрешённый
дубль — только машинно-сверяемый тестом факт: стадии/гейты/агенты derive-тестами из
`STAGE_TRANSITIONS`/`QG_CHECKS`/glob промптов (прецедент key-sync ORCH-102).
3. **Канон презентации:** источник — `presentation.md` (машинно-парсимая слайдо-структура
`## Слайд N:` + тезисы, 1418 слайдов); генератор — `scripts/build_presentation.py` на
python-pptx (тёмная тема, редактируемый текст, кириллица), запуск **только вне рантайма**
(dev-venv, явный запуск человеком — паттерн ORCH-009); зависимость в
`requirements*`/`Dockerfile` НЕ попадает (машинный гард в тестах). **Собранный `.pptx` в git
не коммитится** (источник истины — markdown + скрипт; существующий `PRODUCT_VISION.pptx` не
трогается, но прецедентом не является).
4. **Норматив сопровождения (кросс-каттинг):** «изменил функциональность платформы → обнови
витрину `docs/overview/` в том же PR» — в индексе витрины и `CLAUDE.md` (правило агентов №2);
**reviewer-ось обзорных доков ORCH-079 расширяется** точечной врезкой в
`.openclaw/agents/reviewer.md`: функциональность из витрины изменена, витрина не обновлена →
finding ≥ P1 (расширение трактовки той же оси; канон 52d и verdict-ключи — байт-в-байт;
анти-регресс `test_agent_prompts_canon.py`).
5. **Анти-дрейф — `tests/test_system_docs.py`** (структурный, без сети/LLM/subprocess, паттерн
`test_lite_setup_doc.py`): наличие/непустота 10 файлов; маршруты и норматив в индексе;
сверка стадий и имён гейтов импортом из кода; полнота 6 агентов glob'ом промптов; валидность
относительных ссылок; полнотекстовый FORBIDDEN-скан (импорт из `test_no_host_hardcodes.py`)
+ секрет-эвристика; парс слайдо-источника функцией самого генератора; чистота
`requirements*`/`Dockerfile` от pptx; указатели README/CLAUDE/CHANGELOG. Новый QG НЕ
регистрируется — тесты исполняются существующими гейтами.
Рантайм байт-в-байт: `src/**`, compose, Dockerfile, `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/
machine-verdict/схема БД — не тронуты; kill-switch не нужен (доки и dev-скрипт конвейером не
исполняются).
## Последствия
- **+** Закрывается корневая фрагментация: одна точка входа для трёх аудиторий; презентация
собирается за одну команду из версионируемого источника; машинно-проверяемые факты витрины —
CI-гарантии.
- **+** Нулевой риск рантайма; для enduro-trails инертно.
- **** Новый golden source = обязанность каждого функционального PR (в этом смысл задачи);
митигировано link-first + derive-тестами + reviewer-осью.
- **** Точечная правка промпта reviewer — поверхность канона 52d; держится анти-регресс
тестами.
- **Откат:** удалить `docs/overview/`, тест-модуль, скрипт, вернуть точечные правки указателей
и промпта — 1:1, без миграций и состояния.
## Ссылки
- Детально: `docs/work-items/ORCH-011/06-adr/ADR-001-system-overview-canon.md` (D1…D9),
`docs/work-items/ORCH-011/10-tech-risks.md`
- BRD/TRZ/AC: `docs/work-items/ORCH-011/01-brd.md` / `02-trz.md` / `03-acceptance-criteria.md`
- Соседние каноны: adr-0019 (стандарт доков), adr-0021 (канон промптов 52d), adr-0023
(ось обзорных доков ORCH-079 — расширяется), adr-0029 (порядок под-гейтов), adr-0037/0038
(deployment-каноны)

View File

@@ -0,0 +1,436 @@
# BUNDLED_SETUP — Bundled-тираж: весь стек одним комплектом (ORCH-103)
> **Golden source Bundled-тиража (Type B эпика ORCH-10).** Маршрут «чистый хост →
> работающий конвейер» для заказчика **без собственной инфраструктуры**: один
> compose-комплект (`deploy/bundled/docker-compose.yml`) поднимает оркестратор +
> watchdog + Gitea + Plane CE, один запуск `scripts/bootstrap_bundle.py apply`
> доводит стек до рабочего состояния. Каждый шаг — исполняемая команда + явная
> проверка результата (**Проверка:** / PASS / FAIL). Хост-специфика — только
> плейсхолдеры `<...>` и `$ENV_VAR`. Тираж **stateless**: данные/задачи/секреты
> боевого (исходного) хоста **НЕ переносятся** ни на одном шаге (§12).
> Границы слоёв тиража (10-common vs Lite vs Bundled) — `docs/operations/REPLICATION.md` §1;
> канон Lite (своя инфраструктура Plane/Gitea) — `docs/deployment/LITE_SETUP.md`.
---
## 1. Рамка Bundled
**Что входит в комплект** (compose-проект `orchestrator-bundle`, одна bridge-сеть):
- `orchestrator` (конвейер, образ собирается из этого чекаута) и
`orchestrator-watchdog` (независимый sidecar-мониторинг);
- **Gitea** (git-хостинг, пиннованный официальный образ);
- **Plane CE — ≈ 14 контейнеров** (зеркало официального selfhost-комплекта:
web/space/admin/api/worker/beat-worker/migrator/live + postgres/redis/
rabbitmq/minio/proxy) — это **ресурсоёмко**, см. §2.
**Что НЕ входит** (внешние предусловия заказчика):
- **Claude CLI / LLM-доступ** — дистрибутив claude-code, node и аутентификация
живут на хосте и пробрасываются маунтами (§8); без них стек поднимется, но
конвейер не поедет;
- **Telegram-боты** — опциональны (§9): пусто = деградация только нотификаций;
- **HTTPS/домены/reverse-proxy** — вне bundle: наружу публикуются три http-порта
(§2), терминирование TLS — средствами заказчика.
**Bundled vs Lite:** Lite (`LITE_SETUP.md`) подключает оркестратор к **вашим**
Plane/Gitea; Bundled везёт их **с собой** на чистых томах. Staging-контур орка в
bundle отсутствует вовсе: заказчик Type B эксплуатирует платформу для своих
проектов, а не развивает её self-hosting'ом (нужен self-hosting — маршрут Lite,
`LITE_SETUP.md` §9.3). Репо `orchestrator` в bundle-инсталляции **не
регистрируется** как проект.
**Осознанный компромисс (TR-7):** git-доступ агентов — HTTP token-remote
(токен бот-юзера в конфиге локальных чекаутов, права 600); ssh-контур
сознательно не вводится; порты БД/брокера/minio наружу не публикуются.
---
## 2. Требования к хосту
Linux x86_64, один хост. Минимумы проверяет preflight bootstrap **до любых
мутаций** (пороги — константы `scripts/bootstrap_bundle.py`, ниже — те же цифры;
подтверждаются замером приёмочного развёртывания):
| Ресурс | Минимум | Почему |
|--------|---------|--------|
| RAM | **8 GB** | Plane CE — ≈ 14 контейнеров (миграции и брокер прожорливы) |
| Диск | **40 GB** свободно | образы стека + тома postgres/minio/gitea + данные орка |
| CPU | **4 vCPU** (рекомендация) | меньше — стек поднимется, но будет медленным |
**Карта публикуемых портов** (дефолты; конфигурируемы в
`deploy/bundled/.env`, ключи `BUNDLE_*`):
| Порт | Ключ | Сервис |
|------|------|--------|
| 8500 | `BUNDLE_ORCH_PORT` | API оркестратора (`/health`, `/queue`, `/metrics`, вебхуки) |
| 8080 | `BUNDLE_PLANE_PORT` | Plane UI (proxy) |
| 3000 | `BUNDLE_GITEA_HTTP_PORT` | Gitea web/API |
Postgres/redis/rabbitmq/minio наружу **не публикуются** (машинный трафик —
внутрисетевой сервис-DNS).
```bash
free -g # RAM ≥ 8 GB
df -h . # свободно ≥ 40 GB
nproc # ≥ 4
ss -ltn | grep -E ':(8500|8080|3000)\b' || echo "ports free"
```
**Проверка:** ресурсы не ниже минимумов и `ports free` — PASS. Порт занят →
смените соответствующий `BUNDLE_*`-ключ в §5 (или освободите порт) — иначе
preflight откажет (FAIL до мутаций, это штатно).
---
## 3. Предусловия
Софт хоста: Docker Engine + Compose v2, git, python3 (+venv), sudo у оператора.
```bash
uname -sm # Linux x86_64
docker --version && docker compose version
git --version && python3 --version
python3 -m venv --help >/dev/null && echo "venv: ok"
getent group docker # третье поле — gid, понадобится в §5 (ORCH_DOCKER_GID)
id -u && id -g # uid/gid оператора (ORCH_RUN_UID / ORCH_RUN_GID)
```
**Проверка:** все команды отвечают без ошибок, gid группы docker известен —
PASS; что-то отсутствует — FAIL (доставьте пакет средствами дистрибутива).
---
## 4. Получение кода
Переносится **только код** — чекаут репо `orchestrator` (норматив §12).
```bash
git clone <ORCHESTRATOR_GIT_URL> <путь-чекаута>
cd <путь-чекаута>
ls deploy/bundled/docker-compose.yml deploy/bundled/.env.example \
scripts/bootstrap_bundle.py scripts/gen_secrets.py scripts/onboard_project.py
```
**Проверка:** все пять файлов на месте — PASS. Канал дистрибуции
(`<ORCHESTRATOR_GIT_URL>`) согласуйте с поставщиком платформы (как в
`LITE_SETUP.md` §3).
---
## 5. Секреты
Все секреты инсталляции выпускаются **заново на месте** (§12): webhook-секреты —
`scripts/gen_secrets.py`, внутренние креды Plane/Gitea-стека — генерирует
bootstrap (в репо — только пустые плейсхолдеры, ни одного дефолтного пароля).
**5.1. Конфиг bundle-инфры.**
```bash
cd <путь-чекаута>
cp deploy/bundled/.env.example deploy/bundled/.env
chmod 600 deploy/bundled/.env
# заполнить НЕсекретные ключи: BUNDLE_PUBLIC_HOST (IP/имя хоста для браузера),
# карту портов BUNDLE_* (§2), ORCH_RUN_UID/ORCH_RUN_GID (из §3),
# ORCH_DOCKER_GID (getent group docker, §3), пути Claude CLI (§8).
```
**Проверка:**
```bash
docker compose -f deploy/bundled/docker-compose.yml config --quiet && echo "config: PASS"
```
`config: PASS` — интерполяция согласована; ошибка — FAIL (опечатка в
`deploy/bundled/.env`).
**5.2. Секрет-значения** (пустые ключи `deploy/bundled/.env` и корневого `.env`)
заполнит `bootstrap_bundle.py apply` (§7): webhook-секреты — субпроцессом
`gen_secrets.py`, креды postgres/rabbitmq/minio/`SECRET_KEY` Plane и пароль
админ-бота Gitea — stdlib-генератором. Значения **не печатаются** (только имена
ключей); повторный запуск **не перетирает** существующие секреты (явная
регенерация — флаг `--force-secrets`, допустим только ДО первого запуска стека).
```bash
grep -cE '^(POSTGRES_PASSWORD|SECRET_KEY|RABBITMQ_DEFAULT_PASS|MINIO_ROOT_PASSWORD|GITEA_ADMIN_PASSWORD)=$' \
deploy/bundled/.env
```
**Проверка:** до §7 счётчик `5` (пустые плейсхолдеры) — PASS; после §7 — `0`.
---
## 6. Запуск bundle-compose
Одна команда поднимает весь стек (≈ 16 контейнеров; первый запуск тянет образы
и гоняет миграции Plane — это минуты, не секунды).
```bash
docker compose -f deploy/bundled/docker-compose.yml up -d
docker compose -f deploy/bundled/docker-compose.yml ps
```
**Проверка:** все сервисы в состоянии `Up`/`Up (healthy)`; `migrator`
`Exited (0)` (одноразовая миграция) — PASS. Контейнер в рестарт-цикле — FAIL
(§14). Шаг идемпотентен; можно пропустить — `bootstrap_bundle.py apply` выполнит
`up -d` сам (§7).
---
## 7. Bootstrap
Доводка «одним запуском»: preflight → секреты → up/готовность → init Gitea
(полностью автоматом: админ-бот + API-токен) → init Plane → онбординг
sandbox-проекта **строго** кирпичом `onboard_project.py` (22 канонических
статуса, включая fail-closed **`Confirm Deploy`** и **`STOP`**, лейблы,
репо+webhook — golden source `docs/operations/ONBOARDING.md` §1) → git-доступ
агентов → сборка `.env`/`.env.watchdog` → health.
```bash
python3 scripts/bootstrap_bundle.py # план + preflight-диагностика (ноль мутаций)
python3 scripts/bootstrap_bundle.py apply # полный прогон
```
**Manual-step чекпоинты Plane CE** (API первичной инициализации в CE нет;
каждый чекпоинт: точная инструкция → подтверждение → верификация результата
API-пробой, молчаливый пропуск запрещён):
1. **instance setup** — открыть Plane UI, зарегистрировать первого
пользователя (станет администратором инстанса);
2. **workspace** — создать workspace, ввести его slug в bootstrap;
3. **API-токен** — Workspace Settings → API tokens, вставить значение в
bootstrap (ввод скрыт; уходит в `ORCH_PLANE_API_TOKEN`);
4. **workspace-webhook** — bootstrap регистрирует сам (запись в Postgres
инсталляции, путь Б канона `LITE_SETUP.md` §5.4) и проверяет; при отказе —
честный ручной шаг с той же проверкой;
5. **порядок статусов на доске** — drag-and-drop по отчёту onboard
(`docs/operations/ONBOARDING.md`).
Exit-коды: `0` — успех; `2` — остановка на manual-step/предусловии (выполните
шаг и перезапустите `apply` — завершённые шаги пропускаются, повторный запуск
безопасен); `1` — ошибка. Пароль админ-бота Gitea — ключ `GITEA_ADMIN_PASSWORD`
в `deploy/bundled/.env` (права 600; вход в UI Gitea под
`GITEA_ADMIN_USERNAME`).
**Проверка:**
```bash
python3 scripts/bootstrap_bundle.py verify && echo "bootstrap: PASS"
```
`verify` зелёный (health/queue/metrics + onboard verify) — PASS; exit 2 —
остались ручные пункты отчёта; exit 1 — FAIL (§14).
---
## 8. LLM (claude CLI)
Канон — `LITE_SETUP.md` §7 (полностью применим; не дублируется). Кратко: на
хост ставятся claude-code + node, выполняется интерактивный логин CLI; пути
прописываются в `deploy/bundled/.env` (это источники маунтов контейнера орка):
`ORCH_HOST_CLAUDE_CODE_DIR`, `ORCH_HOST_NODE_BIN`, `ORCH_HOST_CLAUDE_DIR`,
`ORCH_HOST_CLAUDE_JSON`.
```bash
claude --version
docker compose -f deploy/bundled/docker-compose.yml exec orchestrator /usr/bin/claude --version
```
**Проверка:** обе команды печатают версию — PASS; вторая падает — пути в
`deploy/bundled/.env` не указывают на фактические каталоги хоста (§14.4).
---
## 9. Telegram
Канон — `LITE_SETUP.md` §8 (два независимых бота, C-1: токен орка для watchdog
переиспользовать запрещено). Ключи орка (`ORCH_TELEGRAM_BOT_TOKEN`,
`ORCH_TELEGRAM_CHAT_ID`) — в корневой `.env`; ключи watchdog
(`WATCHDOG_TG_BOT_TOKEN`, `WATCHDOG_TG_CHAT_ID`) — **только** в `.env.watchdog`
(файл-носитель, `LITE_SETUP.md` §4.3). Шаг опционален: пустые токены =
деградация только нотификаций.
```bash
grep -E '^ORCH_TELEGRAM_(BOT_TOKEN|CHAT_ID)=' .env
grep -E '^WATCHDOG_TG_(BOT_TOKEN|CHAT_ID)=' .env.watchdog
docker compose -f deploy/bundled/docker-compose.yml up -d orchestrator orchestrator-watchdog
```
**Проверка:** ключи заполнены и контейнеры пересозданы → тестовое сообщение от
обоих ботов (`getMe` — команды в `LITE_SETUP.md` §8) — PASS; пусто — осознанный
PASS без нотификаций.
---
## 10. Онбординг следующих проектов
Sandbox-проект создал bootstrap (§7). Каждый следующий проект заказчика —
штатный runbook `docs/operations/ONBOARDING.md` поверх bundle-инсталляции; для
команд из чекаута: Plane/Gitea доступны на `localhost`-портах §2, webhook-URL —
in-network `http://orchestrator:8500/webhook/gitea`.
```bash
. .venv/bin/activate # venv создан bootstrap'ом (§7)
python3 scripts/onboard_project.py plan \
--name "<имя проекта>" --repo <repo> --prefix <PREFIX> \
--stack "<стек>" --test-cmd "<команда тестов>" \
--prod-port <порт-прода> --staging-port <порт-staging> \
--webhook-url http://orchestrator:8500/webhook/gitea
# план устроил → apply → verify (как в LITE_SETUP.md §10), затем:
# строку ORCH_PROJECTS_JSON из отчёта — в .env и пересоздать орк:
docker compose -f deploy/bundled/docker-compose.yml up -d --force-recreate orchestrator
```
**Проверка:** `verify` зелёный; `GET /queue` отвечает после пересоздания — PASS.
---
## 11. Smoke
Процедура — чек-лист `docs/operations/REPLICATION.md` §4 (шаги 05; шаг 6 «до
`done`» — опционально) поверх bundle-инсталляции, без форка. Минимальный сигнал
«конвейер доехал»: issue в sandbox-проекте Plane → статус **To Analyse**
артефакты `01``04` в ветке задачи.
```bash
curl -fsS http://127.0.0.1:8500/health
curl -fsS http://127.0.0.1:8500/queue | python3 -m json.tool | head -30
curl -fsS http://127.0.0.1:8500/metrics | python3 -m json.tool | head -10
# создать issue в Plane (порт 8080) → перевести в «To Analyse», затем:
curl -fsS http://127.0.0.1:8500/queue | python3 -m json.tool | head -40 # job появился
git -C deploy/bundled/repos/sandbox fetch origin
git -C deploy/bundled/repos/sandbox ls-tree --name-only origin/<ветка-задачи> "docs/work-items/<id>/"
```
**Проверка:** оба направления связности живы — job в `/queue` (Plane→орк
доехал), `ls-tree` показывает `01-brd.md``04-test-plan.yaml` (орк→Gitea
пишет; Gitea→орк события идут) — PASS. Любой шаг FAIL → тираж FAIL: соберите
`docker compose -f deploy/bundled/docker-compose.yml logs --tail 100 orchestrator`
и снапшот `GET /queue`, разбор — §14. (Порты замените, если меняли `BUNDLE_*`.)
---
## 12. Stateless-проверка
**Нормативно: данные/задачи/секреты/БД боевого (исходного) хоста НЕ
переносятся** (зеркало `docs/operations/REPLICATION.md` §5). Все тома bundle
созданы заново при первом `up`; секреты — только свежевыпущенные (§5); в
Plane/Gitea инсталляции нет чужих задач/репо/пользователей.
```bash
docker volume ls --format '{{.Name}}' | grep '^orchestrator-bundle' # только тома этой инсталляции
curl -fsS http://127.0.0.1:8500/queue | python3 -m json.tool # счётчики нулевые
```
**Проверка:** в `/queue` нулевые счётчики и ни одной чужой задачи (никаких
work-item исходного хоста) — PASS. Чужая задача/перенесённый файл БД — FAIL:
инсталляция собрана не stateless, выполните полный сброс (§13) и повторите.
---
## 13. Остановка и полный сброс
Teardown — **только эта документированная процедура** (в bootstrap delete-режима
сознательно нет, ADR-001 D9).
**Остановка (обратимая):**
```bash
docker compose -f deploy/bundled/docker-compose.yml down
```
**Проверка:** `docker compose -f deploy/bundled/docker-compose.yml ps` пуст;
тома целы (`docker volume ls | grep orchestrator-bundle`) — PASS.
**Полный сброс (НЕОБРАТИМО — удаляет все данные Plane/Gitea/орка):**
```bash
docker compose -f deploy/bundled/docker-compose.yml down -v
rm -rf deploy/bundled/data deploy/bundled/repos
rm -f deploy/bundled/.env .env .env.watchdog
```
**Проверка:** `docker volume ls --format '{{.Name}}' | grep -c '^orchestrator-bundle'`
`0`; live-конфигов нет — PASS (хост чист, можно разворачивать заново с §5).
---
## 14. Траблшутинг
Формат: симптом → диагностика → лечение.
**14.1. Webhook не доходит (issue в Plane есть, job в `/queue` нет).**
```bash
docker compose -f deploy/bundled/docker-compose.yml logs --tail 50 orchestrator | grep -i "webhook\|signature"
docker compose -f deploy/bundled/docker-compose.yml exec -T plane-db \
psql -U plane -d plane -c "SELECT url, is_active FROM webhooks;"
```
Лечение: (а) нет строки webhook → §7 чекпоинт 4; (б) URL не
`http://orchestrator:8500/webhook/plane` → исправьте на in-network URL;
(в) 401/HMAC → секрет в Plane обязан байт-в-байт совпадать с
`ORCH_PLANE_WEBHOOK_SECRET` корневого `.env`. Для Gitea-направления проверьте
Recent Deliveries в настройках hook'а репо; помните про
`GITEA__webhook__ALLOWED_HOST_LIST=orchestrator` в bundle-compose (без него
Gitea молча режет вебхуки в приватные адреса).
**14.2. Не хватает RAM / OOM (контейнеры Plane в рестарт-цикле).**
```bash
free -g && docker stats --no-stream | head -20
docker compose -f deploy/bundled/docker-compose.yml ps
```
Лечение: минимум §2 (8 GB; Plane ≈ 14 контейнеров). Меньше — добавьте RAM/swap;
preflight bootstrap отказывает заранее именно поэтому.
**14.3. Порт занят (`up` падает с bind error).**
```bash
ss -ltnp | grep -E ':(8500|8080|3000)\b'
```
Лечение: смените `BUNDLE_ORCH_PORT`/`BUNDLE_PLANE_PORT`/`BUNDLE_GITEA_HTTP_PORT`
в `deploy/bundled/.env` и повторите `up`/bootstrap.
**14.4. claude не найден / агент падает на старте.**
```bash
docker compose -f deploy/bundled/docker-compose.yml exec orchestrator /usr/bin/claude --version
ls "$(grep '^ORCH_HOST_CLAUDE_CODE_DIR=' deploy/bundled/.env | cut -d= -f2)"
```
Лечение: пути `ORCH_HOST_*` в `deploy/bundled/.env` обязаны указывать на
фактические каталоги хоста; креды CLI читаемы uid'ом `ORCH_RUN_UID` (канон —
`LITE_SETUP.md` §7/§13.3); после правки — `up -d --force-recreate orchestrator`.
**14.5. Миграции Plane не завершились (bootstrap падает на ожидании).**
```bash
docker compose -f deploy/bundled/docker-compose.yml logs --tail 50 migrator plane-db
docker compose -f deploy/bundled/docker-compose.yml ps plane-db plane-mq plane-redis
```
Лечение: чаще всего — нехватка RAM/диска (§14.2) или невыпущенные секреты
(пустой `POSTGRES_PASSWORD` → postgres не стартует; прогоните §7, который
заполняет креды ДО `up`). После лечения — повторный `apply` (идемпотентен).
**14.6. PR задачи не мержится / HOLD.** Branch protection на `main` в Gitea
**НЕ включать** — норматив `LITE_SETUP.md` §6.4 (ломает PR-merge API
merge-актора); bundle-Gitea конфигурируется тем же правилом.
```bash
curl -fsS -H "Authorization: token $ORCH_GITEA_TOKEN" \
"http://127.0.0.1:3000/api/v1/repos/<owner>/<repo>/branch_protections" | python3 -m json.tool
```
Лечение: непустой список правил → удалить (канон `LITE_SETUP.md` §6.4/§13.7).
---
*Golden source Bundled-тиража (ORCH-103, ADR-001 D10). **Норматив сопровождения
(NFR-5):** меняешь шаги Bundled-тиража (состав bundle-compose, ключи
`deploy/bundled/.env.example`, шаги bootstrap, smoke) → обнови этот док В ТОМ ЖЕ
PR. Полноту и гигиену держит `tests/test_bundled_setup_doc.py`; кирпичи-каноны:
`LITE_SETUP.md` (§5§8 — подключения), `docs/operations/ONBOARDING.md` (статусы
§1, онбординг), `docs/operations/REPLICATION.md` (карта env §2, секреты §3,
smoke §4), `deploy/bundled/.env.example` + `.env.example` /
`.env.watchdog.example` (каноны ключей).*

View File

@@ -13,7 +13,7 @@
|------|---------|--------|
| **10-common** (этот док) | фундамент: все хост-значения параметризованы (env/конфиг), секреты выпускаются заново, smoke-процедура с PASS/FAIL | ✅ ORCH-101 |
| **Type A — Lite** | инструкция «поставь Plane+Gitea сам, подключи оркестратор» поверх 10-common | ✅ ORCH-102 — [`docs/deployment/LITE_SETUP.md`](../deployment/LITE_SETUP.md) |
| **Type B — Bundled** | комплект «всё в одном» (Plane+Gitea+оркестратор) поверх 10-common | отдельная задача эпика |
| **Type B — Bundled** | комплект «всё в одном» (Plane+Gitea+оркестратор) поверх 10-common | ✅ ORCH-103 — [`docs/deployment/BUNDLED_SETUP.md`](../deployment/BUNDLED_SETUP.md) |
Этот док НЕ описывает установку Plane/Gitea — только параметризацию, секреты и
smoke самого оркестратора (анти-скоуп-крип Р-5).

89
docs/overview/README.md Normal file
View File

@@ -0,0 +1,89 @@
# Витрина системы — Orchestrator
**Что это за система.** Orchestrator — автономная фабрика разработки: конвейер из шести
ИИ-агентов (аналитик → архитектор → разработчик → ревьюер → тестировщик → деплойер), который
проводит задачу от бизнес-постановки до выкладки на прод. Человек ставит задачу и принимает
результат; всё между — автономно, под защитой машинных гейтов качества. Платформа ведёт
несколько проектов из одного инстанса, дорабатывает сама себя (self-hosting) и тиражируется
на новые хосты.
**Зачем эта витрина.** Это единая точка входа в документацию системы: связное описание на двух
уровнях — бизнес (для нетехнического читателя) и технический (7 блоков), с маршрутами чтения
для трёх аудиторий и слайдо-готовой основой для презентации. Витрина — обзор; за деталями она
ведёт ссылками в инженерные golden sources, не подменяя их.
---
## Состав витрины
| Файл | О чём |
|------|-------|
| [business.md](business.md) | Бизнес-уровень: проблема, решение, что умеет, ценность, сценарии |
| [tech-architecture.md](tech-architecture.md) | Блок 1: компоненты и связи, схема потока |
| [tech-pipeline.md](tech-pipeline.md) | Блок 2: конвейер, стадии, гейты, откаты, человеческие гейты |
| [tech-agents.md](tech-agents.md) | Блок 3: 6 ролей агентов, артефакты, модель/эффорт |
| [tech-data-model.md](tech-data-model.md) | Блок 4: каноническая модель объектов, словарь терминов |
| [tech-integrations.md](tech-integrations.md) | Блок 5: Plane, Gitea, LLM, Telegram |
| [tech-quality-security.md](tech-quality-security.md) | Блок 6: гейты качества, безопасность, секреты |
| [tech-observability.md](tech-observability.md) | Блок 7: наблюдаемость, аналитика, журнал уроков |
| [presentation.md](presentation.md) | Слайдо-источник презентации + сборка `.pptx` |
---
## Маршруты чтения
### Я заказчик
1. [business.md](business.md) — проблема, решение, ценность.
2. [business.md → Сценарии использования](business.md#сценарии-использования) — как это выглядит в работе.
3. [presentation.md](presentation.md) — слайдовая версия рассказа (собирается в PowerPoint).
4. Развернуть у себя: [LITE_SETUP](../deployment/LITE_SETUP.md) (своя инфраструктура) или
[BUNDLED_SETUP](../deployment/BUNDLED_SETUP.md) (весь стек одним комплектом).
### Я менеджер проекта
1. [business.md](business.md) — что платформа делает и где в процессе человек.
2. [tech-pipeline.md](tech-pipeline.md) — конвейер, статусная модель Plane, человеческие гейты
(одобрение постановки, подтверждение прод-деплоя).
3. [tech-observability.md](tech-observability.md) — как следить за ходом: живая Telegram-карточка,
статусы, стоимость.
### Я разработчик
1. Тех-блоки 1→7: [архитектура](tech-architecture.md) → [конвейер](tech-pipeline.md) →
[агенты](tech-agents.md) → [модель объектов](tech-data-model.md) →
[интеграции](tech-integrations.md) → [качество/безопасность](tech-quality-security.md) →
[наблюдаемость](tech-observability.md).
2. [Инженерный справочник архитектуры](../architecture/README.md) и
[internals](../architecture/internals.md) — детали реализации.
3. [Стандарты](../_standards/PIPELINE_DOCS.md) (структура доков конвейера),
[HANDOFF_PROTOCOL](../_standards/HANDOFF_PROTOCOL.md) (машинный контракт стадий),
[TRACEABILITY](../_standards/TRACEABILITY.md) (маркеры решений).
4. [Реестр сквозных ADR](../architecture/adr/) — история архитектурных решений.
5. [CLAUDE.md](../../CLAUDE.md) — паспорт проекта и правила для агентов.
---
## Норматив сопровождения
> **Изменил функциональность платформы → обнови витрину `docs/overview/` в том же PR.**
Какой файл правится при каком классе изменений:
| Класс изменения | Файл витрины |
|-----------------|--------------|
| Новый компонент / демон / поток данных | [tech-architecture.md](tech-architecture.md) |
| Стадии, гейты, под-гейты, маршруты задач | [tech-pipeline.md](tech-pipeline.md) |
| Роли агентов, промпты, модель/эффорт | [tech-agents.md](tech-agents.md) |
| Таблицы БД, объекты, термины | [tech-data-model.md](tech-data-model.md) |
| Plane / Gitea / LLM / Telegram | [tech-integrations.md](tech-integrations.md) |
| Гейты качества, секреты, self-hosting-страховки | [tech-quality-security.md](tech-quality-security.md) |
| Эндпоинты наблюдаемости, метрики, уроки | [tech-observability.md](tech-observability.md) |
| Новая способность уровня продукта | [business.md](business.md) + при необходимости [presentation.md](presentation.md) |
Каркас и машинно-проверяемые факты витрины (перечень стадий, имена гейтов, полнота агентов,
валидность ссылок) защищены структурными тестами `tests/test_system_docs.py` — дрейф рвёт CI.
Прозу проверяет reviewer: необновлённая витрина при изменении описанной в ней функциональности —
finding ≥ P1 (расширение оси обзорных доков).
---
*Витрина — обзорный слой документации. Текущее состояние и реестр доработок — [CLAUDE.md](../../CLAUDE.md);
концепция развития — [Product Vision](../PRODUCT_VISION.md).*

105
docs/overview/business.md Normal file
View File

@@ -0,0 +1,105 @@
# Бизнес-уровень: что это и зачем
> Читатель этого документа — нетехнический: заказчик, руководитель, менеджер. Технические
> детали вынесены в [тех-часть витрины](README.md) и даются здесь только ссылками.
## Проблема
Классическая разработка — это люди-бутылочное-горлышко на каждом шаге: аналитик, архитектор,
разработчик, ревьюер, тестировщик, деплой-инженер. Каждая передача задачи между ними — потеря
времени, контекста и денег. Мелкая фича или баг едут до прода днями: не потому, что работа
сложная, а потому, что задача ждёт людей в очередях между ролями.
## Решение
**Orchestrator** — конвейер из ИИ-агентов, который проводит задачу через все стадии разработки
сам: анализ требований → проектирование → код → ревью → тестирование → репетиция выкладки →
выкладка на прод. Человек в этой схеме — **постановщик и приёмщик**: он формулирует задачу,
одобряет постановку и подтверждает выкладку на прод. Всё между — автономно.
Честность конвейера держат **гейты качества**: автоматические проверки на каждом переходе,
которые не пускают задачу дальше, пока стадия не выполнена по-настоящему (тесты зелёные,
ревью одобрено, репетиция выкладки успешна). Агент не может «уговорить» гейт — вердикты
машинные, не прозой.
## Что умеет платформа сегодня
Ниже — фактическое состояние, не планы (концепция развития — отдельный документ,
[Product Vision](../PRODUCT_VISION.md)).
- **Автономный конвейер «задача → прод».** Задача, поставленная в трекере, проходит весь путь
до выкладки без ручных пинков; человек участвует ровно в двух точках — одобрение постановки
и подтверждение прод-выкладки.
- **Мультипроектность.** Один инстанс платформы ведёт несколько проектов (репозиториев)
одновременно, с общей очередью и честным разделением работы.
- **Самовосстановление.** Фоновые механизмы находят и чинят зависшие задачи: упавший агент
перезапускается, осиротевшая задача возвращается в очередь, переполненный диск чистится,
а независимый сторож следит за самой платформой снаружи.
- **Пакетный авто-режим.** Задачи одного проекта выстраиваются в очередь и едут друг за другом
без столкновений; специальными метками на задаче можно снять оба человеческих одобрения —
и пакет задач уедет «за ночь» полностью автономно.
- **Дешёвый багфикс-маршрут.** Задача с меткой «баг» едет коротким путём — без тяжёлой
аналитики и отдельной стадии проектирования, но через все те же гейты качества.
- **Отмена задачи одной кнопкой.** Перевод задачи в статус «STOP» в трекере останавливает
работу, снимает её с очереди и прибирает за собой — безопасно даже посреди конвейера.
- **Наблюдаемость.** У каждой задачи — живая карточка в Telegram (стадия, время, стоимость);
у платформы — служебные страницы состояния и машинные метрики; история отклонений пишется
в журнал уроков.
- **Самообслуживание (self-hosting).** Платформа дорабатывает сама себя тем же конвейером,
с обязательной репетицией на песочнице и ручным подтверждением выкладки.
- **Тиражируемость.** Платформа разворачивается на новой инфраструктуре без правки кода:
вариант Lite (у заказчика своя инфраструктура) и вариант Bundled (весь стек одним
комплектом) — по пошаговым инструкциям.
## Ценность
-**Скорость.** Полный цикл «постановка → прод» без очередей между ролями; по оценке из
[Product Vision](../PRODUCT_VISION.md) — порядка 35 минут на типовую фичу без ручных
вмешательств.
- 💰 **Стоимость.** Работа агентов в разы дешевле команды специалистов; стоимость каждой
задачи видна в её карточке — расходы прозрачны.
- 🎯 **Автономность.** Ноль ручных пинков в штатном прогоне: человек принимает решения,
а не двигает задачу.
- 🛡️ **Надёжность.** Многоуровневые гейты качества и репетиция выкладки на песочнице не
пускают недоделку на прод; сломавшаяся выкладка откатывается, проект замораживается до
разбора.
- 🔁 **Масштаб.** Одна платформа — несколько проектов; сама платформа тиражируется на новые
хосты за часы по инструкции.
## Сценарии использования
### Сценарий 1: фича за вечер
Заказчик формулирует задачу в трекере и переводит её в работу. Конвейер собирает требования,
проектирует, пишет код и тесты, проходит ревью и тестирование, репетирует выкладку. Человек
дважды нажимает «одобрить» — на постановке и перед продом. Вечером фича на проде.
### Сценарий 2: багфикс по короткому маршруту
На задачу ставится метка «баг» — конвейер пропускает тяжёлую аналитику и отдельное
проектирование, сразу чинит и фиксирует дефект регресс-тестом. Все гейты качества — без
исключений.
### Сценарий 3: пакет задач на ночь
Несколько задач проекта получают метки авто-одобрения. Очередь проводит их друг за другом:
каждая следующая стартует от свежей версии кода с результатом предыдущей. Утром — пакет
изменений на проде и полный след по каждой задаче.
### Сценарий 4: несколько проектов параллельно
Один инстанс платформы обслуживает несколько репозиториев: задачи разных проектов едут
одновременно, не мешая друг другу; внутри одного проекта порядок строго последовательный.
### Сценарий 5: развернуть платформу у себя
Заказчик получает платформу на своей инфраструктуре по инструкции
[Lite](../deployment/LITE_SETUP.md) (есть свои трекер и git) или
[Bundled](../deployment/BUNDLED_SETUP.md) (весь стек одним комплектом, ~14 контейнеров),
со свежими секретами и проверкой каждого шага.
### Сценарий 6: остановить задачу
Передумали — переводите задачу в статус «STOP»: работа агента останавливается, ветка и
рабочие материалы прибираются, задача помечается отменённой. Если задача в этот момент в
необратимой фазе выкладки — отмена аккуратно откладывается до её честного завершения.
---
*Технические детали каждой способности — в [тех-части витрины](README.md): как устроен
[конвейер](tech-pipeline.md), кто такие [агенты](tech-agents.md), как работает
[наблюдаемость](tech-observability.md).*

View File

@@ -0,0 +1,220 @@
# Презентация системы: слайдо-источник
> Источник истины презентации. Каждый слайд — блок `## Слайд N: Заголовок` с тезисами
> (36 на слайд) и опциональной подписью визуала. Из этого файла собирается редактируемый
> PowerPoint в тёмном дизайне — процедура в конце файла («Как собрать .pptx»). Собранный
> бинарь в git не коммитится: меняешь рассказ — правишь этот файл и пересобираешь.
## Слайд 1: Orchestrator — автономная фабрика разработки
- Конвейер из ИИ-агентов: от постановки задачи до выкладки на прод
- Человек ставит задачу и принимает результат — всё между автономно
- Платформа уже работает: ведёт несколько проектов и дорабатывает сама себя
> Визуал: тёмный титул, логотип-конвейер из шести звеньев
## Слайд 2: Проблема
- Классическая разработка — люди-бутылочное-горлышко на каждом шаге
- Каждая передача между ролями — потеря времени, контекста и денег
- Мелкая фича или баг едут до прода днями — из-за очередей, не сложности
> Визуал: цепочка из шести человек с песочными часами между ними
## Слайд 3: Решение
- Шесть ИИ-агентов проводят задачу через все стадии разработки сами
- Аналитик → архитектор → разработчик → ревьюер → тестировщик → деплойер
- Человек принимает два решения: одобрить постановку и подтвердить прод
- Честность держат машинные гейты качества — их нельзя «уговорить»
> Визуал: та же цепочка, но из агентов; человек над ней с двумя кнопками
## Слайд 4: Как это работает — конвейер
- Задача из трекера едет по стадиям: анализ → проектирование → код → ревью → тесты → репетиция → прод
- На каждом переходе — гейт: машинная проверка честности стадии
- Не прошёл гейт — задача возвращается на доработку с замечаниями
- Каждая задача — своя ветка и изолированная рабочая копия кода
> Визуал: горизонтальная схема стадий со шлагбаумами-гейтами
## Слайд 5: Гейты качества
- Вердикты машинные: зелёный CI, одобрение ревью, отчёт тестов — только структурированные ключи
- Перед продом — четыре дополнительных проверки: безопасность, слияние, покрытие тестами, свежесть сборки
- Покрытие тестами не может деградировать: базовая линия растёт только вверх
- Слияние в основную ветку — только через PR; прямой push запрещён всем
> Визуал: четыре шлагбаума подряд перед воротами «прод»
## Слайд 6: Роли агентов
- Аналитик: требования, критерии приёмки, тест-план
- Архитектор: проектные решения с фиксацией в ADR
- Разработчик: код + тесты + документация одним PR
- Ревьюер и тестировщик: независимые машинные вердикты
- Деплойер: репетиция на песочнице, затем прод
> Визуал: шесть карточек-ролей с артефактами на выходе
## Слайд 7: Человек в контуре
- Постановщик и приёмщик, а не оператор: ноль ручных пинков в штатном прогоне
- Решение 1: одобрить постановку после аналитики
- Решение 2: подтвердить выкладку на прод отдельным статусом
- Живая карточка задачи в Telegram показывает, когда конвейер ждёт вас
> Визуал: человек с двумя кнопками и карточка задачи в телефоне
## Слайд 8: Запуск и ведение задачи через Plane
- Запуск: перевод задачи в статус «To Analyse» — единственная точка входа в конвейер
- Статусы Plane — индикация, а не управление: платформа выставляет их сама (Backlog → … → Done)
- Управляющих статусов ровно три: запуск, человеческие гейты и отмена
- Ход задачи виден сразу: статусы доски, живая карточка в Telegram, комментарии в задаче со ссылками на ветку и PR
> Визуал: доска Plane с движущейся карточкой и зеркальное уведомление в Telegram
## Слайд 9: Что решает человек: гейты, авто-режим, отмена
- Гейт 1 — статус «Approved» на анализе: одобрить постановку задачи
- Гейт 2 — статус «Confirm Deploy» на деплое: подтвердить прод отдельным статусом, чтобы привычный approve не выкатил прод случайным кликом
- Лейблы «autoApprove» / «autoDeploy» снимают эти два решения для пакетного авто-режима
- Авто-режим убирает только ожидание человека — ни одна техническая проверка не пропускается
- Лейбл «Bug» — короткий багфикс-маршрут; статус «STOP» — безопасная отмена с уборкой ветки и worktree, не трогает прод
> Визуал: две кнопки человека, переключатели авто-лейблов и стоп-кран STOP
## Слайд 10: Пакетный автономный режим
- Задачи одного проекта едут строго друг за другом — без столкновений
- Каждая следующая стартует от свежего кода с результатом предыдущей
- Метки авто-одобрения снимают оба человеческих гейта — пакет уезжает «за ночь»
- Технические проверки при этом не ослабляются ни на одну
> Визуал: ночная очередь задач, утром — стопка готовых
## Слайд 11: Багфикс за полцены
- Метка «баг» — и задача едет коротким маршрутом
- Пропускаются тяжёлая аналитика и отдельное проектирование
- Обязателен регресс-тест, фиксирующий дефект
- Все гейты качества — без исключений
> Визуал: развилка маршрутов — длинный и короткий путь к одному финишу
## Слайд 12: Самовосстановление
- Упавший агент перезапускается, осиротевшая задача возвращается в очередь
- Зависшие состояния находит и чинит фоновый сверщик
- Независимый сторож следит за платформой снаружи и шлёт алерты отдельным каналом
- Деградация прода после выкладки замораживает проект до разбора человеком
> Визуал: платформа с автоподзаводом и внешним сторожем
## Слайд 13: Наблюдаемость
- Одна задача — одна живая карточка: стадия, агент, стоимость, время
- Служебные страницы: снимок очереди и машинные метрики для мониторинга
- Журнал уроков копит отклонения конвейера — фундамент самообучения
- Стоимость каждой задачи и каждой роли видна по фактам
> Визуал: дашборд из карточки, очереди и графика стоимости
## Слайд 14: Одна платформа — много проектов
- Несколько репозиториев из одного инстанса с общей очередью
- Внутри проекта — строгий порядок, между проектами — параллельность
- Платформа дорабатывает сама себя тем же конвейером (self-hosting)
- Своя доработка репетируется на песочнице и требует явного подтверждения
> Визуал: один конвейер, несколько лент с разными проектами
## Слайд 15: Сценарии использования
- Фича за вечер: постановка → прод с двумя кликами человека
- Пакет задач на ночь: метки авто-одобрения, утром всё на проде
- Багфикс по короткому маршруту с обязательным регресс-тестом
- Остановить задачу: статус STOP — безопасная отмена с уборкой
- Несколько проектов параллельно без пересечений
> Визуал: пять пиктограмм-сценариев
## Слайд 16: Тираж платформы
- Разворачивается на новой инфраструктуре без правки кода — только конфиг
- Lite: у заказчика свои трекер и git — ставятся только оркестратор и сторож
- Bundled: весь стек одним комплектом (~14 контейнеров) и бутстрап-скрипт
- Свежие секреты, пошаговые инструкции с проверкой каждого шага
> Визуал: коробка-дистрибутив в двух размерах
## Слайд 17: Lite-установка скриптами
- Lite — два контейнера платформы: оркестратор и сторож (watchdog) на инфраструктуре заказчика
- Свои Plane, Gitea, Telegram и LLM заказчик подключает — в Lite они не входят
- Разворачивается без правки кода — только конфигом (принцип «дефолт = боевое значение»)
- Скрипты-помощники: gen_secrets.py (свежие секреты), onboard_project.py (регистрация проекта: plan / apply / verify); подъём — docker compose up -d
- Маршрут — пошаговый runbook LITE_SETUP.md с проверкой каждого шага (PASS/FAIL)
- Весь стек одним комплектом и одношаговым бутстрапом — это смежный вариант Bundled, не Lite
> Визуал: два контейнера-кубика и чек-лист шагов с галочками
## Слайд 18: Статус платформы сегодня
- В проде: автономный конвейер, мультипроектность, самовосстановление
- В проде: пакетный авто-режим, багфикс-маршрут, отмена задач, журнал уроков
- Тираж Lite и Bundled — готовые инструкции и инструменты
- Платформа развивает сама себя: документация и гейты растут с каждой задачей
> Визуал: чек-лист способностей с отметками «в проде»
## Слайд 19: Итог
- Разработка без очередей между ролями: задача → прод за один проход
- Человек принимает решения — конвейер делает работу
- Качество держат машинные гейты, прозрачность — живая карточка и метрики
- Следующий шаг: поставить первую задачу или развернуть платформу у себя
> Визуал: тёмный финальный слайд с одной фразой-приглашением
---
## Как собрать .pptx
Сборка выполняется **вне рантайма платформы** — в одноразовом dev-окружении на хосте
разработчика (зависимость генерации не входит в прод-образ). Скрипт —
`scripts/build_presentation.py`; формат слайдов выше парсится им же (один парсер — один
источник истины).
**Шаг 1. Создать venv и поставить python-pptx:**
```bash
python3 -m venv .venv-pptx
.venv-pptx/bin/pip install python-pptx
```
Проверка: `.venv-pptx/bin/pip show python-pptx` печатает версию пакета — PASS; ошибка
установки — FAIL (проверьте доступ к PyPI).
**Шаг 2. Собрать презентацию (из корня репозитория):**
```bash
.venv-pptx/bin/python scripts/build_presentation.py
```
Проверка: скрипт печатает `Собрано слайдов: <N> → build/orchestrator-overview.pptx`, где
`<N>` равно числу слайдов в этом файле — PASS; `ОШИБКА: …` — FAIL (текст подскажет причину).
**Шаг 3. Открыть и проверить результат:**
Откройте `build/orchestrator-overview.pptx` в PowerPoint/LibreOffice. Проверка: тема тёмная
(тёмный фон, светлый текст, акцентные заголовки), кириллица отображается точно, текст слайдов
выделяется и редактируется — PASS. Каталог `build/` в `.gitignore`: собранный бинарь в git
не попадает.
---
*Нарратив слайдов опирается на [business.md](business.md); технические утверждения — на
тех-блоки витрины ([конвейер](tech-pipeline.md), [агенты](tech-agents.md)).*

View File

@@ -0,0 +1,60 @@
# Блок 3. Агенты: 6 ролей конвейера
> Промпты ролей лежат в `.openclaw/agents/*.md` (по одному файлу на роль). Канон манифеста
> «документ → агент → стадия → гейт → machine-key» — [PIPELINE_DOCS §2](../_standards/PIPELINE_DOCS.md);
> машинный контракт передачи между стадиями — [HANDOFF_PROTOCOL](../_standards/HANDOFF_PROTOCOL.md).
## Паспорта ролей
| Роль | Стадия | Вход | Выходные артефакты | Machine-verdict ключ |
|------|--------|------|--------------------|----------------------|
| `analyst` | analysis | бизнес-запрос (`00-business-request.md`) | `01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md`, `04-test-plan.yaml` | — (гейт проверяет полноту пакета + одобрение человека) |
| `architect` | architecture | пакет аналитики | `06-adr/ADR-NNN-*.md`, when-applicable `07-infra-requirements.md` / `08-data-requirements.md`, `10-tech-risks.md` | — (гейт проверяет наличие ADR) |
| `developer` | development | ТЗ + ADR | код в `src/`, тесты в `tests/`, обновлённые доки, `CHANGELOG.md`, PR в Gitea | — (гейт — зелёный CI ветки) |
| `reviewer` | review | PR diff + ТЗ/ADR | `12-review.md` | `verdict:` (`APPROVED` \| `REQUEST_CHANGES`) |
| `tester` | testing | ветка задачи + тест-план | `13-test-report.md` | `result:` (`PASS` \| `FAIL` \| `BLOCKED`) |
| `deployer` | deploy-staging / deploy | прошедшая гейты ветка | `15-staging-log.md`, `14-deploy-log.md` | `staging_status:` / `deploy_status:` (`SUCCESS` \| `FAILED`) |
Machine-verdict ключи читаются гейтами **только из YAML-frontmatter** артефакта (никогда из
прозы) и неизменны байт-в-байт — подробнее в [блоке качества](tech-quality-security.md).
## Модель и эффорт
Модель и эффорт каждой роли резолвятся **только из конфига** (не из промпта); текущие
дефолты конфига:
| Роль | Модель | Эффорт |
|------|--------|--------|
| `analyst` | `claude-opus-4-8` | `high` |
| `architect` | `claude-opus-4-8` | `high` |
| `developer` | `claude-opus-4-8` | `xhigh` |
| `reviewer` | `claude-opus-4-8` | `high` |
| `tester` | `claude-opus-4-8` | `medium` |
| `deployer` | `claude-opus-4-8` | `medium` |
Разработчику — максимальный эффорт (он пишет код); тестировщику и деплойеру хватает среднего
(их работа процедурная). Таблица сверяется с class-default'ами `src/config.py` структурным
тестом — дрейф рвёт CI.
## Канон промптов
Все промпты следуют единому канону (Anthropic XML, эпик 52): пять обязательных секций в
нормативном порядке `<context>``<task>``<deliverables>``<constraints>`
`<output_format>`, запреты в формате «❌ X → ✅ Y», секция эскалации у решающих ролей. Каждый
агент эмитит единую frontmatter-схему в своих документах (work item, стадия, автор, статус,
дата, модель). Промпт читается из worktree в момент запуска — обновление промптов вступает в
силу со следующей задачи, без рестарта прода.
Особенность: промпт `deployer` сознательно на английском (самый safety-critical — несёт
запреты self-hosting в видной рамке); остальные пять — на русском.
## Человек как седьмая роль
Человек не пишет артефакты конвейера, но принимает два решения, которые не делегированы
агентам: одобрение постановки (после `analyst`) и подтверждение прод-выкладки (перед финалом
работы `deployer`). Подробнее — [человеческие гейты](tech-pipeline.md).
---
*Структуры документов, которые сдаёт каждая роль, — [PIPELINE_DOCS](../_standards/PIPELINE_DOCS.md);
скелеты — `docs/_templates/`.*

View File

@@ -0,0 +1,63 @@
# Блок 1. Архитектура: компоненты и связи
> Обзорный уровень. Полная таблица компонентов с деталями и историей решений — в
> [инженерном справочнике](../architecture/README.md) («Компоненты») и
> [internals](../architecture/internals.md); здесь — цельная картина, как части складываются
> в конвейер.
## Поток одной задачи
```
Plane (трекер) Gitea (git/CI)
│ вебхук │ вебхук
▼ ▼
┌────────────────────────────────────────┐
│ FastAPI-приём (HMAC-подпись, дедуп) │
└───────────────────┬────────────────────┘
вебхук → очередь (jobs) → агент (Claude CLI в worktree) → гейт (QG) → переход стадии
▲ │
└────────── следующая стадия / откат ◄─────────┘
```
Каждое продвижение задачи — один и тот же цикл: событие принято → в очередь поставлен job →
worker запустил агента стадии → результат проверен гейтом качества → state machine перевела
задачу на следующую стадию (или откатила назад).
## Компоненты
| Компонент | Роль |
|-----------|------|
| **Webhook-приёмники** (`src/webhooks/`) | Принимают события Plane (статусы задач) и Gitea (push, PR, CI). Проверяют HMAC-подпись, дедуплицируют повторные доставки. |
| **Очередь задач** (`jobs` + worker) | Собственная очередь на SQLite: атомарный захват job'а, ретраи с backoff, зависимости между job'ами, ограничение параллелизма. |
| **State machine** (`src/stages.py`) | Карта стадий `STAGE_TRANSITIONS`: для каждой стадии — следующая, агент и гейт выхода. Единственный источник истины о конвейере. |
| **Stage engine** (`src/stage_engine.py`) | Исполняет переходы: диспетчеризация гейтов, откаты, под-гейты деплойного ребра, синхронизация статусов с Plane. |
| **Agent launcher** (`src/agents/launcher.py`) | Запускает Claude CLI агента в изолированном git worktree ветки задачи, следит за процессом (watchdog), авто-продвигает стадию по завершении. |
| **Реестр гейтов** (`src/qg/checks.py`) | `QG_CHECKS` — машинные проверки выхода со стадий; вердикты читаются только из YAML-frontmatter артефактов. |
| **Plane-sync** (`src/plane_sync.py`) | Индикация статусов в Plane (слой «показать человеку», никогда не управление конвейером). |
| **Notifications** (`src/notifications.py`) | Живая Telegram-карточка задачи и алерты. |
## Фоновые демоны (самовосстановление)
Поднимаются в lifespan FastAPI-приложения (`src/main.py`) и работают рядом с конвейером:
- **reconciler** — находит расхождения «БД говорит одно, реальность другое» (зависшие стадии,
потерянные ветки) и возвращает задачи в консистентное состояние;
- **job-reaper** — возвращает в очередь job'ы, чей исполнитель умер (упавший процесс, рестарт);
- **disk-watchdog** — следит за местом на диске, чистит устаревшие worktree;
- **build-cache-pruner** — прибирает докер-кэш сборок.
Отдельно от приложения живёт **sidecar-watchdog** — независимый контейнер-наблюдатель: следит
за самим оркестратором снаружи (health, метрики) и шлёт алерты в собственный Telegram-канал.
Наблюдатель сознательно отделён от наблюдаемого: падение оркестратора не валит сторожа.
## Изоляция работы агентов
Каждая задача живёт в собственной git-ветке (`feature/<ID>-slug`) и собственном **worktree**
изолированной рабочей копии репозитория. Агенты разных задач не видят незакоммиченную работу
друг друга; слияние в `main` происходит только через PR в Gitea после всех гейтов.
---
*Подробнее: [компоненты и API](../architecture/README.md) · [внутренности и схема БД](../architecture/internals.md) ·
следующий блок — [конвейер и стадии](tech-pipeline.md).*

View File

@@ -0,0 +1,70 @@
# Блок 4. Структура объектов: каноническая модель
> Источник истины — фактическая схема SQLite в `src/db.py` и реестр проектов в
> `src/projects.py`; подробное описание таблиц — [internals, «Database Schema»](../architecture/internals.md).
## Каноническая модель
```
Project ──1:N──► Work-Item / Task ──1:N──► Job ──1:N──► Agent-run
│ │
│ └── артефакты задачи (docs/work-items/<ID>/)
└── Plane-проект ↔ git-репозиторий ↔ префикс задач
```
### Project — проект в реестре
Связка «Plane-проект ↔ git-репозиторий ↔ префикс задач» (например, `ORCH-`). Реестр живёт в
конфиге (`src/projects.py`): один инстанс платформы обслуживает несколько проектов; по
префиксу задачи платформа находит репозиторий и настройки.
### Work-Item / Task — задача конвейера
Строка таблицы `tasks`: текущая **стадия** (`stage`), **маршрут** (`track`: полный или
багфикс), рабочая **ветка**, счётчики откатов, отметки отмены. Натуральные ключи — ID задачи
в Plane и человекочитаемый номер (`ORCH-NNN`). На каждой стадии задача накапливает
**артефакты** — номерные документы в `docs/work-items/<ID>/` (от бизнес-запроса до
deploy-лога; манифест — [PIPELINE_DOCS](../_standards/PIPELINE_DOCS.md)).
### Job — единица работы в очереди
Строка таблицы `jobs`: что запустить (агент какой стадии), для какой задачи, в каком статусе
(`queued``running` → терминал). Очередь даёт: **атомарный захват** (два worker'а не возьмут
один job), **зависимости** между job'ами, **ретраи** с экспоненциальным backoff и breaker
после исчерпания бюджета, ограничение параллелизма.
### Agent-run — один запуск агента
Строка таблицы `agent_runs`: какой агент, какой моделью и эффортом, сколько длился, сколько
стоил (токены/доллары). Из этих строк складывается честная стоимость задачи в живой карточке
и аналитика по ролям.
### События вебхуков и дедуп
Входящие события Plane/Gitea фиксируются с ключом дедупликации: повторная доставка того же
события (ретраи источника, сетевые икоты) не порождает повторной работы.
## Вспомогательные таблицы
| Таблица | Зачем |
|---------|-------|
| `repo_freeze` | durable-заморозка репозитория после деградации прода (serial gate) |
| `coverage_baseline` | базовая линия покрытия тестами; растёт только вверх (ratchet) |
| `tracker_messages` | леджер всех Telegram-карточек задачи (зачистка сирот) |
| `lessons` | машинный журнал уроков — структурированные отклонения конвейера |
Все изменения схемы — аддитивные и идемпотентные (`CREATE TABLE IF NOT EXISTS`, ensure-column
при старте): обновление платформы не требует ручных миграций.
## Словарь терминов
| Термин | Значение |
|--------|----------|
| **Стадия** | Позиция задачи в конвейере; карта стадий — `STAGE_TRANSITIONS` ([блок 2](tech-pipeline.md)) |
| **Гейт (exit-гейт)** | Машинная проверка выхода со стадии; реестр — `QG_CHECKS` |
| **Под-гейт** | Проверка-врезка внутри перехода (не стадия); см. деплойное ребро в [блоке 2](tech-pipeline.md) |
| **Job** | Единица работы в очереди; задача порождает job'ы по мере продвижения |
| **Worktree** | Изолированная рабочая копия репозитория для ветки задачи |
| **Lease (merge-lease)** | Эксклюзивная блокировка «кто сейчас мержит этот репозиторий» — сериализация слияний |
| **Track (маршрут)** | Вариант пути задачи: полный цикл или багфикс с пропуском проектирования |
| **Freeze** | Заморозка очереди репозитория после инцидента до ручного разбора |
---
*Как объекты двигаются по конвейеру — [блок 2](tech-pipeline.md); кто их создаёт —
[агенты](tech-agents.md); как за ними наблюдать — [блок 7](tech-observability.md).*

View File

@@ -0,0 +1,54 @@
# Блок 5. Интеграции: Plane, Gitea, LLM, Telegram
> Обзорный уровень; детали API, эндпоинтов и вебхуков — в
> [инженерном справочнике](../architecture/README.md) и [internals](../architecture/internals.md).
## Plane — управление задачами
- **Вход конвейера:** перевод задачи в статус «To Analyse» — единственная точка запуска
пайплайна. Вебхуки Plane (HMAC-подписанные) доставляют изменения задач.
- **Статусы = индикация, не управление** ([блок 2](tech-pipeline.md)): платформа сама
выставляет статусы доски, чтобы человек видел осмысленную картину; управляют конвейером
только машина стадий и три управляющих статуса (запуск, человеческие гейты, STOP).
- **Лейблы** — декларативные переключатели на задаче: `autoApprove` / `autoDeploy` (снятие
человеческих гейтов), `Bug` (багфикс-маршрут). Источник истины — Plane API: ошибка чтения
лейблов трактуется как «лейбла нет» (fail-safe — никогда не «авто» по ошибке).
- Платформа пишет в задачу комментарии о ходе работ (под ботами ролей) с кликабельными
ссылками на ветку/PR.
## Gitea — git, PR, CI
- **Каждая задача = одна ветка = один PR.** Ветка срезается от свежего `main`, работа идёт в
изолированном worktree, слияние — только после всех гейтов.
- **Слияние строго через PR-merge API** — платформенный инвариант: прямой push или
force-push в `main` запрещён всем акторам, включая агентов и сам движок.
- **Merge-актор устойчив к икотам:** транзиентные ошибки Gitea (таймаут, «try again later»)
ретраятся с backoff; необратимые — честный отказ без ложных повторов. Ветка, уже целиком
попавшая в `main`, распознаётся и не порождает мусорных PR.
- **CI (Gitea Actions)** гонит полный тест-сьют на каждый push ветки; зелёный CI — гейт
выхода из разработки (`check_ci_green`).
- Вебхуки Gitea (push, PR, статус CI) — второй источник событий конвейера.
## LLM — Claude CLI
- Агенты запускаются через **Claude CLI**: launcher собирает команду с промптом роли,
`--model` и эффортом, резолвленными **только из конфига** (таблица — в
[блоке агентов](tech-agents.md)); имя модели валидируется перед запуском.
- Запуск — в worktree ветки задачи: агент видит код своей задачи и ничего лишнего.
- Каждый запуск пишет в учёт стоимость и токены ([блок 7](tech-observability.md)).
## Telegram — живой трекер и алерты
- **Одна задача = одна живая карточка**: стадия, статус, модель/эффорт агента, стоимость,
честные метрики времени. Карточка обновляется «переездом вниз» чата (старая удаляется,
свежая приходит тихо); леджер карточек зачищает осиротевшие дубли.
- **Алерты** (упавший гейт, ожидание человека, инциденты) приходят отдельными сообщениями
с пингом.
- **Sidecar-watchdog шлёт в собственный канал** со своим ботом: наблюдатель за платформой
не зависит от её Telegram-стека.
---
*Развёртывание интеграций с нуля — [LITE_SETUP](../deployment/LITE_SETUP.md) /
[BUNDLED_SETUP](../deployment/BUNDLED_SETUP.md); безопасность стыков —
[блок 6](tech-quality-security.md).*

View File

@@ -0,0 +1,54 @@
# Блок 7. Наблюдаемость и аналитика
> Машинный контракт метрик и устройство sidecar-наблюдателя — в
> [инженерном справочнике](../architecture/README.md) (разделы `/metrics` и sidecar-watchdog).
## Живая Telegram-карточка задачи
Каждая задача — одна карточка в Telegram, обновляемая на каждом событии:
- текущая стадия и Plane-статус (включая человеческие гейты — видно, когда задача ждёт вас);
- строка работающего агента: роль · модель · эффорт;
- стоимость задачи нарастающим итогом (токены/доллары по каждому запуску агента);
- честные метрики времени на финише: время агентов / время ожидания человека / общее
календарное — три независимые цифры, а не одна вводящая в заблуждение сумма;
- кликабельный номер задачи (ведёт в Plane), отметка багфикс-маршрута.
Карточка тихая (без пингов); пингуют только алерты: красный гейт, ожидание решения человека,
инциденты.
## Служебные страницы платформы
- **`GET /queue`** — человекочитаемый снимок всего конвейера: очередь и job'ы, состояние
serial gate и заморозок, авто-лейблы, багфикс-трек, coverage, журнал уроков, фоновые
демоны. Первая точка диагностики «что сейчас происходит».
- **`GET /metrics`** — машинный контракт для внешнего наблюдателя (версионированная схема):
health, возраст последних событий, счётчики сбоев.
- **`GET /health`** — живость процесса.
## Sidecar-watchdog: наблюдатель отделён от наблюдаемого
Отдельный контейнер-сторож опрашивает `/metrics` платформы и шлёт алерты в **собственный**
Telegram-канал со **своим** ботом. Падение платформы, зависание очереди или протухание
событий видно даже тогда, когда сама платформа уже не может пожаловаться.
## Журнал уроков
Машинная таблица отклонений конвейера: красные гейты, ложные блокировки слияния, исчерпание
ретраев, деградации после выкладки. Каждая запись — контекст (задача, стадия, агент, репо),
первопричина и предложение. Журнал — наблюдатель (никогда не влияет на продвижение задач) и
фундамент петли самообучения платформы: уроки доступны через API и копятся для будущего
ретроспективного анализа.
## Стоимость и аналитика по агентам
Каждый запуск агента фиксирует модель, эффорт, длительность и стоимость
([модель объектов](tech-data-model.md)). Это даёт ответы на вопросы «сколько стоит задача»,
«какая роль ест бюджет», «как изменилась экономика после смены модели» — по фактам, не по
ощущениям.
---
*Что делать при инцидентах и как устроен прод — `docs/operations/` (через
[инженерный справочник](../architecture/README.md)); бизнес-взгляд на наблюдаемость —
[business.md](business.md).*

View File

@@ -0,0 +1,103 @@
# Блок 2. Конвейер: стадии, гейты, маршруты
> Источник истины — карта переходов `STAGE_TRANSITIONS` в `src/stages.py` и реестр гейтов
> `QG_CHECKS` в `src/qg/checks.py`; перечень ниже сверяется с кодом структурным тестом
> (`tests/test_system_docs.py`). Норматив структуры доков конвейера —
> [PIPELINE_DOCS](../_standards/PIPELINE_DOCS.md).
## Схема конвейера
```
created → analysis → architecture → development → review → testing → deploy-staging → deploy → done
↑ │
└──── REQUEST_CHANGES ─────┘ (откат на доработку, max 3)
```
Плюс системный сток **`cancelled`** — терминальное состояние отменённой задачи (кнопка STOP,
см. ниже). Это не ребро конвейера, а равноправный `done` сток: попасть в него можно с любой
стадии, выйти — нельзя.
## Стадии и гейты выхода
Гейт выхода (exit-гейт) — машинная проверка, без которой задача не покидает стадию:
| Стадия | Кто работает | Гейт выхода (имя в реестре) | Что проверяет |
|--------|--------------|------------------------------|----------------|
| `created` | — | — | вход конвейера (вебхук Plane) |
| `analysis` | analyst | `check_analysis_approved` | пакет аналитики полон И постановка одобрена человеком |
| `architecture` | architect | `check_architecture_done` | ADR / инфра-требования зафиксированы |
| `development` | developer | `check_ci_green` | CI на ветке задачи зелёный |
| `review` | reviewer | `check_reviewer_verdict` | машинный вердикт ревью: APPROVED |
| `testing` | tester | `check_tests_passed` | машинный вердикт тестера: PASS |
| `deploy-staging` | deployer | `check_staging_status` | репетиция выкладки на песочнице успешна |
| `deploy` | deployer / finalizer | `check_deploy_status` | прод-выкладка реально успешна |
| `done` | — | — | терминал |
| `cancelled` | — | — | терминал (сток отмены) |
## Под-гейты деплойного ребра — врезки, не стадии
На переходе `deploy-staging → deploy` исполняются четыре под-гейта в нормативном порядке
(security → merge → coverage → image-freshness):
1. `check_security_gate` — секреты/зависимости, вердикт из security-отчёта;
2. `check_branch_mergeable` — merge-gate: ветка догнана до свежего `main` (под merge-lease)
и мержабельна;
3. `check_coverage_gate` — покрытие тестами не ниже базовой линии/порога (baseline-ratchet);
4. `check_staging_image_fresh` — staging-образ собран из актуального кода.
Это **врезки в переход, а не стадии**: они не появляются в карте `STAGE_TRANSITIONS`, а
исполняются stage engine'ом внутри ребра. Провал любого из них — откат на доработку. На ребре
`deploy → done` аналогичная врезка merge-verify подтверждает, что код задачи реально слит в
`main` (слияние — только через PR-API Gitea, см. [интеграции](tech-integrations.md)).
## Откаты
`REQUEST_CHANGES` от ревьюера, проваленные тесты или красный под-гейт возвращают задачу на
стадию разработки с дословным перечнем замечаний. Лимит — 3 попытки подряд, дальше задача
останавливается и требует человека (анти-петля).
## Человеческие гейты и их снятие авто-лейблами
В штатном прогоне человек принимает ровно два решения:
- **Одобрение постановки** (на `analysis`): перевод задачи в статус Approved пропускает её
дальше;
- **Подтверждение прод-выкладки** (на `deploy`): отдельный статус **Confirm Deploy** — чтобы
привычный «approve» не выкатывал прод случайным кликом.
Оба решения можно снять декларативно — лейблами **autoApprove** / **autoDeploy** на задаче
(пакетный авто-режим). Снимается только ожидание человеческого сигнала: ни одна техническая
проверка не пропускается, autoDeploy физически не может выкатить непрошедшее под-гейты.
## Багфикс-маршрут
Задача с меткой **Bug** едет коротким путём: облегчённая аналитика (но полный пакет
документов) и пропуск стадии `architecture` — из аналитики сразу в разработку. Срезается
только аналитика/проектирование: **все гейты исполняются без изменений**. Сложный баг
эскалируется обратно в полный цикл.
## Последовательность внутри репозитория (serial gate)
Новая задача репозитория не входит в работу, пока не завершена более ранняя (FIFO): ветка
каждой задачи срезается от свежего `main`, уже содержащего код предшественника. Деградация
прода после выкладки замораживает репозиторий (freeze) до ручного разбора — следующие задачи
ждут.
## Отмена: STOP → `cancelled`
Перевод задачи в статус **STOP** останавливает агента, снимает job'ы с очереди, удаляет
рабочую ветку и worktree и переводит задачу в `cancelled`. Если задача в необратимой фазе
(идёт слияние/выкладка) — отмена откладывается и применяется после честного завершения шага.
STOP никогда не трогает `main` и прод-контейнер.
## Статусная модель Plane: индикация ≠ управление
Статусы в Plane — слой **индикации**: они показывают человеку осмысленную картину хода задачи,
но никогда не управляют конвейером (машина стадий — только `STAGE_TRANSITIONS`). Управляющих
статусов ровно три: запуск в работу, Approved/Confirm Deploy (человеческие гейты) и STOP
(отмена). Полная карта статусов — в [инженерном справочнике](../architecture/README.md).
---
*Кто работает на каждой стадии и что сдаёт — [агенты](tech-agents.md); как гейты читают
вердикты — [качество и безопасность](tech-quality-security.md).*

View File

@@ -0,0 +1,63 @@
# Блок 6. Качество и безопасность
> Реестр гейтов и их распределение по стадиям — [блок 2](tech-pipeline.md); механизм
> machine-verdict доков — [PIPELINE_DOCS §3](../_standards/PIPELINE_DOCS.md); машинный
> контракт стадий — [HANDOFF_PROTOCOL](../_standards/HANDOFF_PROTOCOL.md).
## Философия Quality Gates
**Вердикты — машинные, никогда проза.** Гейт читает вердикт ТОЛЬКО из YAML-frontmatter
артефакта (ключи вида `verdict:`, `result:`, `staging_status:`, `deploy_status:`,
`security_status:` — имена и регистр неизменны байт-в-байт). Агент не может «уговорить» гейт
красивым отчётом: нет ключа — нет прохода. Парсинг frontmatter сведён к единому контракту
`src/frontmatter.py` — одна точка чтения для всех гейтов.
**Гейт ≠ маршрутизация.** Маршруты задач (багфикс-трек, авто-лейблы, serial gate) — свойство
планировщика; ни один из них не ослабляет ни одного гейта. Любая новая способность платформы
проектируется так, чтобы реестр гейтов и карта стадий не трогались.
**Анти-петля.** Откаты на доработку ограничены (max 3 подряд); инструментальные сбои
вспомогательных проверок по умолчанию fail-open с предупреждением (не запирают конвейер),
критичные проверки — fail-closed.
## Специальные гейты деплойного ребра
- **Security-гейт** (`check_security_gate`) — детерминированная (без LLM) проверка секретов и
зависимостей перед продом; вердикт — `security_status:` в отчёте задачи.
- **Coverage-гейт** (`check_coverage_gate`) — покрытие тестами измеряется на финальном коде
ветки; базовая линия по репозиторию растёт только вверх (ratchet при подтверждённом
слиянии) — покрытие не может деградировать молча.
Оба — врезки в переход ([блок 2](tech-pipeline.md)), включаются по конфигу и скоупятся по
репозиториям.
## Канон секретов
- Секреты живут **только в `.env`-файлах на хосте** и никогда не коммитятся; в git — только
канон-примеры с пустыми плейсхолдерами.
- Для нового хоста секреты **выпускаются свежими** (`scripts/gen_secrets.py`), боевые не
копируются.
- Анти-регресс машинный: структурные тесты сканируют исполняемый код на боевые хост-литералы,
а документацию — на секретоподобные значения; находка рвёт CI.
## Self-hosting-страховки
Платформа дорабатывает сама себя тем же конвейером — прод-инстанс при этом обслуживает и
другие проекты. Страховки:
- **Песочница обязательна:** перед прод-выкладкой платформы изменение репетируется на
staging-инстансе (отдельный порт/БД); guard не даёт staging-операциям коснуться прод-порта.
- **Прод-выкладка — только по явному человеческому статусу Confirm Deploy** (обычный approve
прод не выкатывает); деплой идёт детачнутым процессом с финализатором — честный исход
фиксируется даже при рестарте.
- **`main` неприкосновенен:** слияние только через PR-API, force-push запрещён всем.
- **Прод-контейнер никогда не роняется задачей**: агенты проверяют изменения локально и на
песочнице; рестарт прода — только штатным деплой-маршрутом.
- **Пост-деплой наблюдение:** после выкладки платформа следит за своим здоровьем; деградация
замораживает репозиторий и зовёт человека.
---
*Операционные детали и топология прода — `docs/operations/` (см.
[инженерный справочник](../architecture/README.md)); наблюдение за здоровьем —
[блок 7](tech-observability.md).*

View File

@@ -0,0 +1,7 @@
# Business Request: Полная документация системы мультиагентов оркестратора (бизнес + тех, для людей и презентаций)
Work Item ID: ORCH-011
## Description
TBD

View File

@@ -0,0 +1,199 @@
---
work_item: ORCH-011
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-11
model_used: claude-opus-4-8
---
# 01 — BRD: ORCH-011 — Полная документация системы мультиагентов оркестратора (бизнес + тех, для людей и презентаций)
Work Item: **ORCH-011** · Repo: **orchestrator** (self-hosting) · Стадия: analysis
Заказчик: Владелец (Слава)
Тип: docs+tests (паттерн ORCH-102/103 — golden-source-документ + структурные анти-дрейф тесты; рантайм не трогается)
---
## 1. Бизнес-контекст и проблема
### 1.1. Цель
Описать работу **всей** мультиагентной системы оркестратора в **одном месте** — от бизнес-смысла
(зачем, какую проблему решает, что умеет) до технического устройства (архитектура, конвейер,
агенты, модель объектов, интеграции, качество, наблюдаемость). Документация нужна, чтобы:
1. **другие люди** (разработчики, заказчики, менеджеры проектов) могли разобраться, как работает
оркестратор, не раскапывая 60+ work-item пакетов и 40 ADR;
2. на её основе **генерировать презентационные материалы** по использованию и возможностям
(решение Владельца: слайды PowerPoint, стильный тёмный дизайн, точный рендеринг).
### 1.2. Корневая проблема — документация богатая, но фрагментированная
Установленные факты (проверено по дереву репо, не изобретать):
| Что есть | Где | Ограничение как «единого места» |
|----------|-----|---------------------------------|
| Паспорт проекта | `CLAUDE.md` | для агентов/разработчиков; плотный реестр доработок, не вводный текст |
| Тех-витрина | `README.md` | только технический уровень; обзор без бизнес-слоя |
| Бизнес-видение | `docs/PRODUCT_VISION.md` (v1.0, 2026-06-04) | «концепция развития» (vision), не описание текущего состояния; не покрывает тех-уровень |
| Детальная архитектура | `docs/architecture/README.md` (~1246 строк), `internals.md` | глубокий справочник для инженеров; нечитаем «с нуля» нетехническим читателем |
| Решения | `docs/architecture/adr/` (40 сквозных ADR) + per-work-item ADR | точечные решения, не цельная картина |
| Стандарты | `docs/_standards/` (PIPELINE_DOCS, HANDOFF_PROTOCOL, TRACEABILITY) | нормативы для агентов |
| Эксплуатация/тираж | `docs/operations/` (8 runbook'ов), `docs/deployment/` (LITE_SETUP, BUNDLED_SETUP) | операторские инструкции |
| История/уроки | `docs/history/`, `docs/epics/self-evolution.md` | сырьё, не витрина |
**Ни один** из этих документов не является единой точкой входа «бизнес + тех» для трёх целевых
аудиторий. Новому человеку (заказчику, менеджеру, новому разработчику) сегодня нужно собирать
картину из 58 разных мест с разной степенью детализации и разным языком. Презентацию о
возможностях системы собирать не из чего — нет слайдо-готового источника.
### 1.3. Почему сейчас
- Платформа достигла тиражируемости (ORCH-101/102/103: Lite- и Bundled-развёртывание у заказчика)
— появился **внешний читатель** (заказчики-тестеры), которому нужно объяснять систему.
- Самовоспроизводящийся темп доработок (self-hosting) без единой витрины делает порог входа всё
выше с каждой задачей.
### 1.4. Решения Владельца (из бизнес-запроса) — приняты как требования
| # | Решение |
|---|---------|
| D-1 | Формат презентационных материалов — **слайды PowerPoint**, стильный **тёмный дизайн**, точный рендеринг. |
| D-2 | Аудитория документации — **разработчики, заказчики, менеджеры проектов** (три явных маршрута чтения). |
| D-3 | Единое место — репозиторий orchestrator: `docs/` (+ возможно compiled-wiki — как опция, см. §2.2). |
| D-4 | Поддерживать в актуальном состоянии: документация обновляется **сразу после изменения функционала** (правило CLAUDE.md §2 распространяется на новую витрину). |
---
## 2. Объём (scope)
### 2.1. В объёме
- **Единая витрина системы** — новый связный раздел в `docs/` с одной точкой входа (индексом),
покрывающий **два уровня**:
- **Бизнес-уровень** (для нетехнических читателей и презентаций): зачем нужен оркестратор и
какую проблему решает; что умеет (автономный конвейер «задача → прод», мультипроектность,
самовосстановление, пакетный авто-режим, багфикс-трек, тиражируемость); ценность и
возможности простым языком; сценарии использования.
- **Технический уровень** (7 блоков, контент-карта — TRZ §3):
1) архитектура — компоненты и их связи; 2) конвейер/стадии и гейты на переходах;
3) агенты — 6 ролей, входы/выходы, артефакты, шаблоны; 4) структура объектов —
Project/Work-Item, реестр проектов, jobs-очередь, события/дедуп, каноническая модель БД;
5) интеграции — Plane, Gitea, LLM-модели, Telegram; 6) качество/безопасность;
7) аналитика/наблюдаемость.
- **Маршруты чтения** для трёх аудиторий (D-2): «я заказчик / я менеджер / я разработчик —
с чего начать и что читать дальше».
- **Презентационная основа** (D-1): слайдо-структурированный источник (последовательность
слайдов: заголовок/тезисы/визуальный мотив) + воспроизводимый путь получения `.pptx` в тёмном
дизайне. Выбор инструмента генерации — за архитектором (TRZ §3.4, OQ-2).
- **Норматив сопровождения**: правило «изменил функционал → обнови витрину в том же PR»
зафиксировано в витрине и в правилах агентов (D-4; ось reviewer'а по обзорным докам уже
существует — ORCH-079).
- **Анти-дрейф**: структурные pytest-тесты каркаса витрины (по образцу
`tests/test_lite_setup_doc.py` / `test_bundled_setup_doc.py`): обязательные разделы, сверка
карты стадий импортом `src/stages.py::STAGE_TRANSITIONS`, полнота 6 агентов, валидность
внутренних ссылок, запрет секретов/хост-хардкодов.
- Обновление указателей: `README.md`, `CLAUDE.md` (ссылка на витрину), `CHANGELOG.md`.
### 2.2. Вне объёма (явно, не делать)
- **Любые изменения рантайма:** `src/**`, `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`,
machine-verdict ключи, схема БД, compose/Dockerfile — байт-в-байт.
- **Compiled-wiki / внешняя вики-платформа** — вне объёма v1: репозиторий остаётся единственным
источником истины («канон не форкается», паттерн ORCH-009 BR-2); экспорт в wiki — возможное
развитие, фиксируется как открытый вопрос (TRZ §9 OQ-4), не реализуется.
- **Перенос вне-репозиторных источников в репо**: `tasks/orchestrator/STATUS.md`, `BACKLOG.md`,
PROGRESS-журналы, дневники `memory/` физически в репозитории отсутствуют — они служат лишь
затравками для содержания; сами файлы в гит не переносятся (внутренняя кухня, риск утечки
контекста/секретов).
- **Переписывание/замена существующих golden sources** (`docs/architecture/README.md`,
`internals.md`, стандарты `docs/_standards/`, deployment-доки): витрина ссылается на них,
а не дублирует и не подменяет (анти-«второй источник истины», BR-4).
- **Автогенерация документации из кода** (doc-from-code, autodoc) — вне объёма.
- **Маркетинговые материалы за пределами PPTX-основы** (видео, лендинги, демо-стенды).
- Новые runtime-зависимости прод-образа (включая библиотеки генерации презентаций) — запрещено
(NFR-2).
---
## 3. Заинтересованные стороны
- **Владелец/оператор (Слава)** — заказчик задачи; принимает витрину и презентационную основу;
использует слайды для показа возможностей платформы.
- **Заказчики платформы** (внешние, включая тестеров Lite/Bundled-тиража ORCH-102/103) — читают
бизнес-уровень и сценарии; смотрят презентацию.
- **Менеджеры проектов** — читают бизнес-уровень + конвейер/статусную модель (что видно в Plane,
какие человеческие гейты есть).
- **Разработчики** (люди и агенты самого оркестратора) — входят через витрину в технический
уровень и далее по ссылкам в golden sources.
- **Reviewer-агент конвейера** — контролирует соблюдение норматива сопровождения витрины
(расширение оси «обзорные доки» ORCH-079).
---
## 4. Бизнес-требования (BR)
| ID | Требование | Связь |
|----|------------|-------|
| BR-1 | В `docs/` существует **единая точка входа** (индекс витрины), из которой достижимы оба уровня (бизнес/тех) и все 7 тех-блоков; любой из трёх читателей начинает с одного места. | D-3, AC-1 |
| BR-2 | **Бизнес-уровень** читается нетехническим человеком: проблема → решение → ценность → возможности → сценарии использования; термины конвейера объяснены по-человечески; без необъяснённого жаргона и кодовых идентификаторов в основном тексте. | D-2, AC-2 |
| BR-3 | **Технический уровень** покрывает все 7 заявленных блоков (§2.1) и соответствует фактическому коду/канону репо: карта стадий = `STAGE_TRANSITIONS`, реестр гейтов = `QG_CHECKS` + под-гейты, агенты = 6 промптов `.openclaw/agents/` + таблица модель/эффорт (ORCH-41), модель данных = фактические таблицы `src/db.py`. | AC-3, AC-4, AC-5 |
| BR-4 | **Link-first / не форкается канон:** витрина даёт цельную картину и ссылается на golden sources за деталями (architecture/README, internals, стандарты, ADR, deployment-доки); не создаёт второй источник истины и не противоречит коду. | §2.2, AC-6 |
| BR-5 | **Презентационная основа:** в репо есть слайдо-структурированный источник (бизнес-нарратив → слайды) и воспроизводимый, документированный путь получения `.pptx` в тёмном дизайне (D-1). Инструмент — выбор архитектора; запуск — вне рантайма конвейера. | D-1, AC-7 |
| BR-6 | **Маршруты аудиторий:** витрина содержит явные маршруты чтения для заказчика / менеджера / разработчика (D-2). | D-2, AC-8 |
| BR-7 | **Норматив сопровождения:** правило «изменил функционал → обнови витрину в том же PR» зафиксировано в витрине и видимо агентам (CLAUDE.md-указатель); reviewer-ось обзорных доков покрывает витрину. | D-4, AC-9 |
| BR-8 | **Анти-дрейф:** каркас витрины и её ключевые машинно-проверяемые факты (стадии, агенты, ссылки, отсутствие секретов) защищены структурными pytest-тестами; их падение ловит расхождение витрины с кодом. | AC-10 |
| BR-9 | Существующие указатели актуализированы: `README.md` и `CLAUDE.md` ссылаются на витрину; `CHANGELOG.md` несёт запись. | AC-11 |
---
## 5. Нефункциональные требования (NFR)
| ID | Требование |
|----|------------|
| NFR-1 | **docs+tests only:** `src/**` байт-в-байт; `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict ключи / схема БД / compose / Dockerfile — не тронуты. Kill-switch не нужен: документация не исполняется (паттерн ORCH-102/103). |
| NFR-2 | **Прод-образ без новых зависимостей:** генерация презентации (если скриптом) исполняется вне рантайма (host/dev-окружение, явный запуск человеком — паттерн ORCH-009); зависимости генерации НЕ попадают в `requirements`/образ оркестратора. |
| NFR-3 | **Без секретов и хост-специфики** в новых доках/источниках презентации: токены, внутренние URL/имена хостов — только плейсхолдерами (паттерн `tests/test_no_host_hardcodes.py` / fenced-скан `test_lite_setup_doc.py`). |
| NFR-4 | **Язык:** русский (канон репо; языковое исключение deployer.md не затрагивается). Терминология единая со статусной моделью Plane (ORCH-066) и PIPELINE_DOCS. |
| NFR-5 | **Self-hosting safety:** задача не рестартит/не роняет прод-контейнер; прод-деплой — только штатным маршрутом конвейера (staging-гейт + Confirm Deploy). |
| NFR-6 | **Поддерживаемость:** витрина модульная (отдельные файлы по блокам, связанные индексом), чтобы будущие правки были точечными; объём основного текста разумен за счёт link-first (BR-4). |
---
## 6. Допущения и ограничения
- **Вне-репозиторные затравки** (`tasks/orchestrator/STATUS.md`, `BACKLOG.md`, PROGRESS-журналы,
`memory/`) в worktree недоступны — содержание витрины строится из **внутрирепозиторных** golden
sources (CLAUDE.md, README, PRODUCT_VISION, architecture/, _standards/, ADR, deployment/,
history/). Этого достаточно: репо самодостаточен по фактам (проверено §1.2).
- `docs/PRODUCT_VISION.md` остаётся самостоятельным vision-документом; витрина переиспользует его
бизнес-нарратив со сверкой с фактическим состоянием (что из vision уже реализовано — например,
тираж ORCH-101/102/103) и ссылается на него.
- Точное имя/структура каталога витрины — решение архитектора (рекомендация TRZ §2: новый каталог
в `docs/`, например `docs/overview/`); анти-дрейф тесты пишутся под выбранные пути.
- Бинарный артефакт `.pptx`: коммит бинаря в git спорен — решает архитектор (OQ-3); требование
BR-5 — воспроизводимость пути генерации, не обязательность бинаря в репо.
- Задача объёмная по контенту: допускается реализация витрины «вглубь по блокам» в одном PR
(один прогон developer); если объём не помещается — эскалация на уровне задач (разбиение)
по штатному маршруту, не молчаливое сокращение объёма.
---
## 7. Критерии успеха (резюме; детали — 03-acceptance-criteria.md)
- AC-1 единая точка входа существует и связывает оба уровня и маршруты аудиторий.
- AC-2 бизнес-уровень самодостаточен для нетехнического читателя.
- AC-3…AC-5 тех-уровень покрывает 7 блоков и сходится с кодом (стадии/гейты/агенты/модель данных).
- AC-6 link-first: ссылки на golden sources валидны, дублирования-противоречий нет.
- AC-7 презентационная основа есть; путь к `.pptx` (тёмный дизайн) воспроизводим и документирован.
- AC-8 маршруты трёх аудиторий присутствуют.
- AC-9 норматив сопровождения зафиксирован.
- AC-10 структурные анти-дрейф тесты существуют и зелёные; полный pytest зелёный.
- AC-11 README/CLAUDE.md/CHANGELOG обновлены; `src/**` не тронут.
---
## 8. Риски (кратко; детали — 10-tech-risks.md, заполняет архитектор)
- **R-1 — гниение витрины.** Self-hosting темп (несколько задач в неделю) быстро устаревает любой
снапшот. Митигция: link-first (BR-4) + норматив сопровождения (BR-7) + структурные тесты на
машинно-проверяемые факты (BR-8) — дрейф ловится CI, а не глазами.
- **R-2 — второй источник истины.** Дублирование деталей architecture/README в витрине приведёт к
противоречиям. Митигция: витрина = картина + ссылки; детали живут в существующих golden sources.
- **R-3 — объём одного прогона.** Полная витрина + презентация + тесты могут не поместиться в один
PR разумного размера. Митигция: модульность (NFR-6), приоритет блоков, при необходимости —
эскалация/разбиение (допущение §6).
- **R-4 — зависимость генерации презентации.** Библиотека генерации PPTX в прод-образе — лишний
attack-surface/вес. Митигция: NFR-2 (вне рантайма), решение по инструменту — ADR архитектора.
- **R-5 — расползание скопа в маркетинг.** Слайды → «а давайте ещё видео/лендинг». Митигция:
жёсткая граница §2.2.

View File

@@ -0,0 +1,191 @@
---
work_item: ORCH-011
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-11
model_used: claude-opus-4-8
---
# 02 — ТЗ (TRZ): ORCH-011 — Полная документация системы мультиагентов оркестратора
Work Item: **ORCH-011** · Repo: **orchestrator** · Стадия: analysis
> ТЗ описывает **что** должно появиться/измениться и **где** (файлы, структура, контент-карта,
> источники истины). **Как** (точное имя каталога витрины, инструмент генерации PPTX, разбиение
> на файлы) — решает архитектор в `06-adr/`. Тип изменения — **docs+tests** (паттерн
> ORCH-102/103): рантайм не трогается.
---
## 1. Сводка изменения
Создать в `docs/` **единую витрину системы** — связный набор документов с одной точкой входа,
описывающий мультиагентный оркестратор на двух уровнях (бизнес + технический, 7 блоков),
с маршрутами чтения для трёх аудиторий (разработчики / заказчики / менеджеры), слайдо-готовой
презентационной основой (PowerPoint, тёмный дизайн) и нормативом сопровождения. Каркас и
машинно-проверяемые факты витрины защитить структурными pytest-тестами (анти-дрейф). Обновить
указатели (`README.md`, `CLAUDE.md`, `CHANGELOG.md`). `src/**` — байт-в-байт.
---
## 2. Задействованные модули / пути
| Путь | Действие |
|------|----------|
| `docs/<витрина>/` (рекомендация: `docs/overview/`; финальное имя — ADR архитектора) | **создать**: индекс + бизнес-часть + тех-часть (7 блоков) + маршруты аудиторий + презентационная основа |
| `docs/<витрина>/README.md` (или `index.md` — по ADR) | **создать**: единая точка входа (BR-1) |
| `tests/test_system_docs.py` (имя — по паттерну `test_lite_setup_doc.py`; финал — ADR) | **создать**: структурный анти-дрейф витрины (FR-7) |
| `scripts/` (опционально, по ADR — например `scripts/build_presentation.py`) | **создать (опц.)**: генерация `.pptx` из презентационной основы (FR-4) |
| `README.md` | изменить: ссылка на витрину (раздел-указатель) |
| `CLAUDE.md` | изменить: указатель на витрину + норматив сопровождения (FR-6) |
| `CHANGELOG.md` | изменить: запись `docs:` |
| `docs/PRODUCT_VISION.md` | НЕ переписывать; допустима врезка-ссылка на витрину |
| `src/**`, `docker-compose.yml`, `Dockerfile`, `requirements*` | **НЕ трогать** (NFR-1, NFR-2) |
Чтение (источники истины для контента, без изменения): `src/stages.py`, `src/qg/checks.py`,
`src/db.py`, `src/projects.py`, `src/plane_sync.py`, `src/notifications.py`, `src/queue_worker.py`,
`src/agents/launcher.py`, `.openclaw/agents/*.md`, `docs/architecture/README.md`, `internals.md`,
`docs/_standards/*`, `docs/architecture/adr/*`, `docs/deployment/*`, `docs/operations/*`,
`docs/PRODUCT_VISION.md`.
---
## 3. Функциональные требования
### FR-1 — Единая точка входа и каркас витрины (BR-1, NFR-6)
- Новый каталог в `docs/` с индекс-документом, который: (а) в 12 абзацах отвечает «что это за
система»; (б) ведёт на бизнес-часть и тех-часть; (в) несёт маршруты чтения трёх аудиторий
(FR-5); (г) несёт норматив сопровождения (FR-6).
- Витрина модульная: отдельные файлы по частям/блокам, связанные индексом (а не один монолит) —
точечные правки в будущем дешевле. Точная разбивка — ADR.
- Все внутренние ссылки — относительные, валидные (проверяется тестом FR-7).
### FR-2 — Бизнес-уровень (BR-2)
Содержание (для нетехнического читателя; источники: `docs/PRODUCT_VISION.md` §12, `README.md`,
`CLAUDE.md` TL;DR — со сверкой с фактическим состоянием кода):
- **Проблема и решение:** люди-бутылочное-горлышко в передачах между ролями → конвейер ИИ-агентов
«постановка → прод», человек = постановщик и приёмщик.
- **Что умеет (фактическое состояние, не vision):** автономный конвейер задача→прод с гейтами
качества; мультипроектность (несколько репо из одного инстанса); самовосстановление
(reconciler / job-reaper / watchdog'и / sidecar); пакетный авто-режим (serial gate ORCH-088 +
autoApprove/autoDeploy ORCH-089); дешёвый багфикс-трек (ORCH-019); отмена задач (STOP,
ORCH-090); наблюдаемость (живая Telegram-карточка, `/queue`, `/metrics`, журнал уроков);
самообслуживание (self-hosting — платформа дорабатывает себя); тиражируемость (Lite/Bundled,
ORCH-101/102/103).
- **Ценность простым языком:** скорость / стоимость / автономность / надёжность / масштаб
(переиспользовать формулировки PRODUCT_VISION, сверив цифры с реальностью; цифры без
подтверждения в репо не изобретать).
- **Сценарии использования** (минимум): «фича за вечер» (полный цикл с человеческими гейтами
Approved / Confirm Deploy); «багфикс по короткому маршруту» (метка Bug); «пакет задач на ночь»
(serial gate + авто-лейблы); «несколько проектов параллельно»; «развернуть платформу у себя»
(Lite/Bundled); «остановить задачу» (STOP).
- Кодовые идентификаторы (`ORCH-NNN`, имена функций) в основном бизнес-тексте не используются;
допустимы сноски/ссылки.
### FR-3 — Технический уровень: 7 блоков с контент-картой (BR-3, BR-4)
Каждый блок: цельное изложение + ссылки на golden source за деталями. Обязательная привязка
к фактам кода (источники указаны; не дублировать детали сверх необходимого — link-first):
| # | Блок | Обязательное содержание | Источник истины (ссылаться) |
|---|------|------------------------|------------------------------|
| 1 | Архитектура | компоненты и связи: FastAPI-приём вебхуков (Plane/Gitea, HMAC, дедуп), очередь jobs + worker, stage-engine, agent launcher (Claude CLI, git worktree), QG-реестр, plane-sync, notifications, фоновые демоны (reconciler, job-reaper, disk-watchdog, build-cache-pruner), sidecar-watchdog; одна диаграмма потока «вебхук → очередь → агент → гейт → переход» | `docs/architecture/README.md` «Компоненты», `internals.md`, `src/main.py` lifespan |
| 2 | Конвейер/стадии | схема `created → analysis → architecture → development → review → testing → deploy-staging → deploy → done` (+ `cancelled`-сток); exit-гейты рёбер; под-гейты ребра `deploy-staging→deploy` (security → merge → coverage → image-freshness) и `deploy→done` (merge-verify) как врезки, не стадии; откаты REQUEST_CHANGES (max 3); человеческие гейты (Approved BRD, Confirm Deploy) и их снятие авто-лейблами; багфикс-маршрут (пропуск architecture); serial gate / freeze; статусная модель Plane = индикация ≠ управление | `src/stages.py::STAGE_TRANSITIONS`, `src/qg/checks.py::QG_CHECKS`, `docs/_standards/PIPELINE_DOCS.md` §13, CLAUDE.md (ORCH-059/066/088/089/019/090) |
| 3 | Агенты | 6 ролей (analyst/architect/developer/reviewer/tester/deployer): задача роли, вход/выход, артефакты по стадиям, machine-verdict ключи; таблица модель/эффорт (резолв из config, ORCH-41/74/81); канон промптов (5 XML-секций, 52d) и где лежат промпты; handoff-протокол | `.openclaw/agents/*.md`, `docs/_standards/PIPELINE_DOCS.md` §2, `HANDOFF_PROTOCOL.md`, таблица моделей `docs/architecture/README.md` |
| 4 | Структура объектов | каноническая модель: Project (реестр `projects.py`: plane-project → repo+prefix) → Work-Item/Task (`tasks`: stage, track, ветка) → Jobs (очередь: статусы, atomic claim, deps, ретраи/backoff/breaker) → Agent-runs (стоимость/токены/effort) → события вебхуков и дедуп → вспомогательные таблицы (`repo_freeze`, `coverage_baseline`, `tracker_messages`, `lessons`); словарь терминов (стадия/гейт/под-гейт/job/worktree/lease) | `src/db.py` (фактические таблицы), `src/projects.py`, `internals.md` «Database Schema», ADR соответствующих задач |
| 5 | Интеграции | Plane (issues/states/labels/webhooks; статусная модель ORCH-066; вход конвейера «To Analyse»); Gitea (репо/ветки/PR; merge строго через PR-API — INV-4; merge-актор с retry ORCH-093; CI `check_ci_green`); LLM (Claude CLI, `--model`/`--effort` из config); Telegram (live-карточка bump-режима, алерты; sidecar-канал отдельно) | `src/plane_sync.py`, `src/webhooks/*`, `src/merge_gate.py`, `src/agents/launcher.py`, `src/notifications.py`, CLAUDE.md (ORCH-042/067/087/093) |
| 6 | Качество/безопасность | философия Quality Gates: машинные вердикты только из YAML-frontmatter (никогда проза), единый контракт `src/frontmatter.py`; реестр гейтов и что каждый ловит; security-гейт (ORCH-022) и coverage-гейт с baseline-ratchet (ORCH-027); канон секретов (.env, не в гит; `gen_secrets.py`); self-hosting-страховки (staging 8501, Confirm Deploy, запрет force-push в main, никогда не ронять прод) | `src/qg/checks.py`, `src/frontmatter.py`, `docs/_standards/PIPELINE_DOCS.md` §3, CLAUDE.md «Self-hosting», `docs/operations/INFRA.md` |
| 7 | Аналитика/наблюдаемость | живая Telegram-карточка задачи (стадии, модель/эффорт, стоимость/токены, честные метрики времени); `GET /queue` (снимки всех подсистем: serial_gate, auto_labels, bug_fast_track, coverage, lessons, reaper, reconcile, …); `GET /metrics` (машинный контракт для sidecar, schema_version); sidecar-watchdog (наблюдатель отделён от наблюдаемого); журнал уроков `lessons` (фундамент петли самообучения); стоимость по агентам (`agent_runs`) | `src/metrics.py`, `src/notifications.py`, `src/lessons.py`, `docs/architecture/README.md` (§ /metrics, § sidecar), CLAUDE.md (ORCH-098/099/100) |
- **Согласованность с кодом обязательна:** перечень стадий, имена гейтов, имена агентов, имена
таблиц в витрине должны совпадать с фактическими (`src/stages.py`, `src/qg/checks.py`,
`src/db.py`); расхождение — дефект (ловится FR-7 и reviewer'ом).
### FR-4 — Презентационная основа и путь к PPTX (BR-5, D-1)
- В витрине есть **презентационный источник**: упорядоченная последовательность слайдов
(рекомендация: markdown с явной слайдо-структурой — «слайд N: заголовок / 35 тезисов /
подпись-визуал»), покрывающая бизнес-нарратив (проблема → решение → как работает → возможности →
сценарии → тираж → статус платформы). Объём — ориентир 1220 слайдов (финал — у архитектора).
- Зафиксирован **воспроизводимый путь** получения `.pptx` в тёмном дизайне: либо скрипт в
`scripts/` (запуск вне рантайма, host/dev-окружение), либо документированная ручная процедура
поверх источника. Выбор инструмента (python-pptx / Marp→pptx / иное) и факт коммита бинаря —
решение архитектора (OQ-2, OQ-3). Требования к пути: тёмная тема, кириллица рендерится точно,
процедура описана пошагово с проверкой результата (паттерн «команда + Проверка:» из
LITE_SETUP).
- Зависимости генерации **не** попадают в прод-образ/`requirements` оркестратора (NFR-2).
### FR-5 — Маршруты аудиторий (BR-6, D-2)
- Индекс несёт три явных маршрута: **заказчик** (бизнес-часть → сценарии → презентация →
Lite/Bundled-доки), **менеджер** (бизнес-часть → конвейер и статусная модель Plane →
человеческие гейты → наблюдаемость), **разработчик** (тех-часть → architecture/README →
internals → стандарты → ADR-реестр → CLAUDE.md).
### FR-6 — Норматив сопровождения (BR-7, D-4)
- В витрине (индексе) — явная норма: «изменил функционал → обнови витрину в том же PR»
(формулировка по образцу NFR-5 ORCH-102/103).
- `CLAUDE.md` — краткий указатель на витрину в существующем правиле документации (§2 правил
агентов); reviewer-ось «обзорные доки» (ORCH-079) распространяется на витрину — фиксируется
формулировкой в витрине/ADR (изменение промпта reviewer'а НЕ требуется, если ось уже
сформулирована обобщённо — проверить при реализации; если требуется правка промпта, она
следует канону 52d и анти-регресс тестам `test_agent_prompts_canon.py`).
### FR-7 — Структурный анти-дрейф (BR-8)
Новый тест-модуль (паттерн `tests/test_lite_setup_doc.py` / `test_bundled_setup_doc.py` /
`test_orch_52b_docs_standard.py`; без сети/LLM/subprocess):
- наличие файлов витрины и обязательных разделов индекса (вкл. маршруты 3 аудиторий и норматив
сопровождения);
- **сверка карты стадий** в витрине импортом `src.stages.STAGE_TRANSITIONS` (полнота и порядок —
как тест полноты `_STAGE_STATUS_LABEL` ORCH-091: derive из кода, не статичный список);
- **полнота 6 агентов** (analyst/architect/developer/reviewer/tester/deployer) в блоке агентов;
- валидность относительных внутренних ссылок витрины (целевые файлы существуют);
- FORBIDDEN-скан новых доков/презент-источника: запрещённые хост-литералы (импорт списка из
`tests/test_no_host_hardcodes.py`, как делает `test_lite_setup_doc.py`) + секрет-эвристика;
- наличие ссылки на витрину в `README.md`.
---
## 4. Изменения API
**Нет.** Эндпоинты/вебхуки не добавляются и не меняются.
## 5. Изменения схемы БД
**Нет.** Таблицы/миграции не вводятся.
## 6. Требования к новым/изменённым QG checks
**Нет.** Реестр `QG_CHECKS`/`check_*` не меняется. Анти-дрейф витрины — обычные pytest-тесты в
`tests/` (исполняются существующими гейтами `check_ci_green`/`check_tests_passed`/coverage-гейтом
штатно), **не** новый Quality Gate.
## 7. Совместимость / регресс
- **Нулевая регрессия рантайма по построению:** меняются только `docs/**`, `tests/**`,
`README.md`, `CLAUDE.md`, `CHANGELOG.md` (+ опц. `scripts/`); `src/**` байт-в-байт. Kill-switch
не нужен — документация и dev-скрипт не исполняются конвейером (паттерн ORCH-009/102/103).
- Существующие тесты остаются зелёными; новые тесты аддитивны.
- enduro-trails не затрагивается (общих артефактов нет).
- Откат = revert PR (доки/тесты), без операционных последствий.
- Self-hosting: прод-контейнер не рестартится в рамках задачи; выкат — штатным маршрутом.
---
## 8. Артефакты pipeline (создать/обновить в ТОМ ЖЕ PR разработки)
- Витрина `docs/<…>/` (FR-1…FR-5) + презентационный источник.
- `tests/test_system_docs.py` (FR-7).
- `README.md` (ссылка), `CLAUDE.md` (указатель + норматив), `CHANGELOG.md` (`docs:`-запись).
- ADR архитектора: `docs/work-items/ORCH-011/06-adr/ADR-001-<slug>.md` (структура витрины,
инструмент PPTX, политика бинаря, состав тестов); при сквозной значимости — зеркало в
`docs/architecture/adr/`.
---
## 9. Открытые вопросы для архитектора (не блокируют анализ)
- **OQ-1:** Имя и внутренняя структура каталога витрины (`docs/overview/` vs `docs/system/`;
один индекс + N файлов блоков vs два файла «business/tech»). Рекомендация аналитика —
`docs/overview/` с индексом и помодульными файлами (NFR-6).
- **OQ-2:** Инструмент генерации PPTX: скрипт `scripts/` (python-pptx; host/dev-venv, вне
прод-образа) vs конвертация из markdown (Marp и т.п.) vs документированная ручная процедура.
Критерии: тёмная тема, точный рендеринг кириллицы, воспроизводимость, NFR-2.
- **OQ-3:** Коммитить ли собранный `.pptx` в репо (бинарь в git) или хранить только источник +
процедуру сборки.
- **OQ-4:** Compiled-wiki/экспорт (упомянут в бизнес-запросе как «возможно»): фиксируем вне
объёма v1; нужно ли заводить follow-up work item.
- **OQ-5:** Достаточна ли текущая формулировка reviewer-оси обзорных доков (ORCH-079) для
покрытия витрины, или нужна точечная правка промпта reviewer (тогда — по канону 52d, с
обновлением `test_agent_prompts_canon.py`).

View File

@@ -0,0 +1,187 @@
---
work_item: ORCH-011
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-11
model_used: claude-opus-4-8
---
# 03 — Критерии приёмки: ORCH-011 — Полная документация системы мультиагентов
Work Item: **ORCH-011** · Repo: **orchestrator** · Стадия: analysis
Каждый критерий — однозначный PASS/FAIL. Reviewer/Tester проверяют буквально по файлам
репозитория (пути витрины — те, что зафиксирует ADR архитектора; ниже «витрина» = выбранный
каталог в `docs/`).
---
## AC-1 — Единая точка входа существует (BR-1)
**Условие:** в `docs/` создан каталог витрины с индекс-документом.
- **PASS:** индекс существует; из него по относительным ссылкам достижимы: бизнес-часть,
тех-часть (все 7 блоков FR-3), презентационная основа, маршруты трёх аудиторий, норматив
сопровождения. Каталог и индекс совпадают с зафиксированными в ADR-001 путями.
- **FAIL:** индекса нет, ИЛИ хотя бы одна из перечисленных частей недостижима из индекса.
---
## AC-2 — Бизнес-уровень самодостаточен для нетехнического читателя (BR-2)
**Условие:** бизнес-часть содержит все 5 обязательных смысловых разделов.
- **PASS:** присутствуют разделы: (1) проблема, которую решает оркестратор; (2) суть решения
(конвейер агентов, человек = постановщик/приёмщик); (3) что умеет — фактические способности
(минимум: автономный конвейер задача→прод, мультипроектность, самовосстановление, пакетный
авто-режим, багфикс-трек, отмена STOP, наблюдаемость, self-hosting, тиражируемость
Lite/Bundled); (4) ценность (скорость/стоимость/автономность/надёжность/масштаб);
(5) сценарии использования (минимум 5 из перечня FR-2). В основном тексте нет необъяснённых
кодовых идентификаторов/имён функций.
- **FAIL:** отсутствует любой из 5 разделов, ИЛИ способности из обязательного минимума
пропущены, ИЛИ текст оперирует жаргоном без объяснения.
---
## AC-3 — Тех-уровень покрывает 7 блоков (BR-3)
**Условие:** тех-часть содержит все блоки контент-карты TRZ §3 FR-3.
- **PASS:** присутствуют и непусты блоки: 1) архитектура/компоненты (включая фоновые демоны и
sidecar), 2) конвейер/стадии/гейты (включая под-гейты и человеческие гейты), 3) агенты,
4) структура объектов/каноническая модель, 5) интеграции (Plane/Gitea/LLM/Telegram),
6) качество/безопасность, 7) аналитика/наблюдаемость. Блок 1 содержит хотя бы одну
диаграмму/схему потока (текстовую ASCII или mermaid).
- **FAIL:** любой блок отсутствует/пуст, ИЛИ схема потока отсутствует.
---
## AC-4 — Карта стадий и гейтов сходится с кодом (BR-3, FR-7)
**Условие:** конвейер в витрине = `src/stages.py::STAGE_TRANSITIONS` + реестр `QG_CHECKS`.
- **PASS:** все стадии из `STAGE_TRANSITIONS` (включая `deploy-staging` и сток `cancelled`)
присутствуют в витрине в правильном порядке; exit-гейты рёбер названы фактическими именами
(`check_analysis_approved``check_deploy_status`); под-гейты ребра
`deploy-staging→deploy` описаны в фактическом порядке (security → merge → coverage →
image-freshness) и явно помечены как врезки, не стадии. Структурный тест сверяет перечень
стадий импортом `src.stages.STAGE_TRANSITIONS` и зелёный.
- **FAIL:** стадия/гейт пропущены или названы несуществующим именем, ИЛИ порядок противоречит
коду, ИЛИ тест-сверка отсутствует/красная.
---
## AC-5 — Агенты: 6 ролей с полным паспортом (BR-3)
**Условие:** блок агентов описывает все 6 ролей.
- **PASS:** для каждой роли (analyst, architect, developer, reviewer, tester, deployer) указаны:
назначение, стадия работы, вход, выходные артефакты (по `PIPELINE_DOCS.md` §2) и
machine-verdict ключ (где есть: `verdict:`/`result:`/`staging_status:`/`deploy_status:`);
присутствует таблица модель/эффорт, совпадающая с фактическим резолвом config (ORCH-41/81:
developer=`xhigh`, tester/deployer=`medium`, прочие=`high`). Структурный тест полноты 6 ролей
зелёный.
- **FAIL:** роль пропущена, ИЛИ артефакты/ключи противоречат `PIPELINE_DOCS.md`, ИЛИ таблица
модель/эффорт расходится с config.
---
## AC-6 — Link-first: ссылки валидны, канон не форкается (BR-4)
**Условие:** витрина ссылается на golden sources, не подменяя их.
- **PASS:** витрина содержит работающие относительные ссылки минимум на:
`docs/architecture/README.md`, `docs/architecture/internals.md`,
`docs/_standards/PIPELINE_DOCS.md`, `docs/_standards/HANDOFF_PROTOCOL.md`, реестр
`docs/architecture/adr/`, `docs/deployment/LITE_SETUP.md`, `docs/deployment/BUNDLED_SETUP.md`,
`docs/PRODUCT_VISION.md`, `CLAUDE.md`. Все относительные ссылки витрины резолвятся в
существующие файлы (структурный тест зелёный). Существующие golden sources не удалены и не
переписаны (допустимы только врезки-ссылки).
- **FAIL:** битая ссылка, ИЛИ обязательная ссылка отсутствует, ИЛИ витрина дублирует-подменяет
существующий golden source (например, копия таблицы компонентов architecture/README вместо
ссылки с кратким резюме).
---
## AC-7 — Презентационная основа и путь к PPTX (BR-5)
**Условие:** слайдовый источник существует; путь к `.pptx` воспроизводим.
- **PASS:** в витрине есть презентационный источник с явной слайдо-структурой (нумерованные
слайды: заголовок + тезисы), покрывающий бизнес-нарратив FR-4 (проблема → решение → как
работает → возможности → сценарии → тираж → статус); зафиксирована пошаговая воспроизводимая
процедура получения `.pptx` в тёмном дизайне (скрипт `scripts/` ИЛИ документированная
процедура — по ADR-001), каждая команда — с проверкой результата; зависимости генерации
отсутствуют в `requirements*` и `Dockerfile` оркестратора (NFR-2).
- **FAIL:** источника нет, ИЛИ слайдо-структура не выражена, ИЛИ путь к `.pptx` не описан /
невоспроизводим, ИЛИ зависимость генерации попала в прод-образ.
---
## AC-8 — Маршруты трёх аудиторий (BR-6)
**Условие:** индекс несёт маршруты чтения.
- **PASS:** в индексе явно выделены 3 маршрута — заказчик, менеджер проекта, разработчик — каждый
с упорядоченным списком «что читать» (состав по FR-5).
- **FAIL:** хотя бы один маршрут отсутствует или пуст.
---
## AC-9 — Норматив сопровождения зафиксирован (BR-7)
**Условие:** правило актуальности витрины закреплено.
- **PASS:** индекс витрины несёт норму «изменил функционал → обнови витрину в том же PR»;
`CLAUDE.md` содержит указатель на витрину; в ADR-001 зафиксировано, как reviewer-ось обзорных
доков (ORCH-079) покрывает витрину (с правкой промпта reviewer или обоснованием, что правка
не нужна; при правке промпта — канон 52d сохранён и `tests/test_agent_prompts_canon.py`
зелёный).
- **FAIL:** норматив отсутствует в витрине, ИЛИ CLAUDE.md не обновлён, ИЛИ вопрос reviewer-оси
не разрешён в ADR.
---
## AC-10 — Анти-дрейф тесты существуют и зелёные (BR-8)
**Условие:** структурный тест-модуль витрины создан и проходит.
- **PASS:** новый тест-модуль (паттерн `test_lite_setup_doc.py`) покрывает: наличие
файлов/разделов витрины, сверку стадий импортом `STAGE_TRANSITIONS`, полноту 6 агентов,
валидность относительных ссылок, FORBIDDEN-скан (импорт запрещённых литералов из
`tests/test_no_host_hardcodes.py` + секрет-эвристика) по новым докам и презентационному
источнику, наличие ссылки на витрину в `README.md`. `pytest tests/ -q` полностью зелёный.
- **FAIL:** тест-модуль отсутствует, ИЛИ любая из перечисленных проверок не реализована, ИЛИ
pytest красный.
---
## AC-11 — Рантайм не тронут; указатели обновлены (NFR-1, BR-9)
**Условие:** изменение строго docs+tests(+опц. scripts).
- **PASS:** diff PR не содержит изменений `src/**`, `docker-compose.yml`, `Dockerfile`,
`requirements*`, схемы БД; `README.md` ссылается на витрину; `CHANGELOG.md` несёт
`docs:`-запись по ORCH-011; в новых файлах нет секретов/боевых токенов/хост-хардкодов
(FORBIDDEN-скан AC-10 зелёный).
- **FAIL:** любой файл рантайма изменён, ИЛИ указатели не обновлены, ИЛИ найден
секрет/хост-хардкод.
---
## AC-12 — Самодостаточность против вне-репозиторных источников (допущение BRD §6)
**Условие:** витрина не зависит от файлов вне репо.
- **PASS:** витрина не содержит ссылок на вне-репозиторные пути (`tasks/…`, `memory/…`,
локальные пути хоста); всё содержание подтверждается внутрирепозиторными источниками.
- **FAIL:** есть ссылка на файл, отсутствующий в репозитории, или цитата «по памяти» без
внутрирепозиторного источника.
---
## Сводная матрица AC ↔ BR/FR
| AC | Покрывает |
|----|-----------|
| AC-1 | BR-1 / FR-1 |
| AC-2 | BR-2 / FR-2 |
| AC-3 | BR-3 / FR-3 |
| AC-4 | BR-3, BR-8 / FR-3, FR-7 |
| AC-5 | BR-3 / FR-3 |
| AC-6 | BR-4 / FR-1, FR-3 |
| AC-7 | BR-5 / FR-4, NFR-2 |
| AC-8 | BR-6 / FR-5 |
| AC-9 | BR-7 / FR-6 |
| AC-10 | BR-8 / FR-7 |
| AC-11 | BR-9, NFR-1, NFR-3 |
| AC-12 | BRD §6 (допущения), NFR-3 |

View File

@@ -0,0 +1,117 @@
work_item: ORCH-011
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-11
model_used: claude-opus-4-8
title: "Полная документация системы мультиагентов: структурный анти-дрейф витрины"
framework: pytest
scope: >
Покрывается: каркас витрины (файлы/разделы/маршруты/норматив), сходимость
машинно-проверяемых фактов с кодом (стадии из STAGE_TRANSITIONS, 6 агентов),
валидность внутренних ссылок, отсутствие секретов/хост-хардкодов, обновление
указателей README/CLAUDE.md, неизменность рантайма (полный регресс).
Вне покрытия: качество прозы/дизайна слайдов (проверяет reviewer/человек),
фактический рендеринг .pptx (ручная проверка по процедуре AC-7).
notes: >
Все тесты — структурные, без сети/LLM/subprocess (паттерн tests/test_lite_setup_doc.py,
test_bundled_setup_doc.py, test_orch_52b_docs_standard.py). Точные пути витрины фиксирует
ADR-001 архитектора (рекомендация TRZ: docs/overview/); имя тест-модуля ниже —
tests/test_system_docs.py — может быть уточнено в ADR, состав проверок обязателен.
Сверки derive-из-кода (стадии) обязаны импортировать src.stages, а не дублировать
статичный список (анти-дрейф, образец — тест полноты ORCH-091). FORBIDDEN-скан
импортирует список запрещённых литералов из tests/test_no_host_hardcodes.py.
Полный регресс tests/ должен оставаться зелёным.
tests:
# ---- FR-1: каркас витрины и единая точка входа ----
- id: TC-01
type: unit
description: "Каталог витрины и индекс существуют; индекс содержит обязательные разделы: вход «что это», ссылки на бизнес-часть и тех-часть, маршруты аудиторий, норматив сопровождения (AC-1)."
module: tests/test_system_docs.py
expected: PASS
- id: TC-02
type: unit
description: "Из индекса по относительным ссылкам достижимы все файлы витрины: бизнес-часть, тех-блоки 17, презентационный источник (AC-1/AC-3)."
module: tests/test_system_docs.py
expected: PASS
# ---- FR-2: бизнес-уровень ----
- id: TC-03
type: unit
description: "Бизнес-часть содержит 5 обязательных смысловых разделов (проблема, решение, что умеет, ценность, сценарии) и минимум 5 сценариев использования (AC-2)."
module: tests/test_system_docs.py
expected: PASS
# ---- FR-3: тех-уровень, сходимость с кодом ----
- id: TC-04
type: unit
description: "Тех-часть содержит все 7 блоков контент-карты (архитектура, конвейер, агенты, объекты, интеграции, качество/безопасность, наблюдаемость); блок архитектуры несёт схему потока (AC-3)."
module: tests/test_system_docs.py
expected: PASS
- id: TC-05
type: unit
description: "Карта стадий витрины сверена импортом src.stages.STAGE_TRANSITIONS: каждая стадия (включая deploy-staging и cancelled) упомянута; derive из кода, не статичный список (AC-4)."
module: tests/test_system_docs.py
expected: PASS
- id: TC-06
type: unit
description: "Имена exit-гейтов рёбер в витрине существуют в реестре qg.checks.QG_CHECKS (нет выдуманных имён гейтов) (AC-4)."
module: tests/test_system_docs.py
expected: PASS
- id: TC-07
type: unit
description: "Блок агентов покрывает все 6 ролей (analyst/architect/developer/reviewer/tester/deployer); каждой роли сопоставлены артефакты; таблица модель/эффорт присутствует (AC-5)."
module: tests/test_system_docs.py
expected: PASS
# ---- FR-1/FR-3: link-first ----
- id: TC-08
type: unit
description: "Все относительные ссылки витрины резолвятся в существующие файлы; обязательные ссылки на golden sources (architecture/README, internals, PIPELINE_DOCS, HANDOFF_PROTOCOL, adr/, LITE_SETUP, BUNDLED_SETUP, PRODUCT_VISION, CLAUDE.md) присутствуют (AC-6)."
module: tests/test_system_docs.py
expected: PASS
- id: TC-09
type: unit
description: "Витрина не ссылается на вне-репозиторные пути (tasks/, memory/, абсолютные пути хоста) (AC-12)."
module: tests/test_system_docs.py
expected: PASS
# ---- FR-4: презентационная основа ----
- id: TC-10
type: unit
description: "Презентационный источник существует, несёт явную слайдо-структуру (нумерованные слайды с заголовками, не менее 12) и покрывает обязательный нарратив (проблема/решение/как работает/возможности/сценарии/тираж) (AC-7)."
module: tests/test_system_docs.py
expected: PASS
- id: TC-11
type: unit
description: "Зависимости генерации презентации не попали в прод-образ: requirements*/Dockerfile не содержат библиотек генерации (например python-pptx) (AC-7/NFR-2)."
module: tests/test_system_docs.py
expected: PASS
# ---- FR-6: норматив и указатели ----
- id: TC-12
type: unit
description: "Индекс витрины несёт норматив сопровождения («в том же PR»); README.md содержит ссылку на витрину; CLAUDE.md содержит указатель (AC-9/AC-11)."
module: tests/test_system_docs.py
expected: PASS
# ---- NFR-3: секреты/хост-хардкоды ----
- id: TC-13
type: unit
description: "FORBIDDEN-скан новых доков и презентационного источника: запрещённые хост-литералы (импорт из tests/test_no_host_hardcodes.py) и секрет-эвристика не находят совпадений (AC-10/AC-11)."
module: tests/test_system_docs.py
expected: PASS
# ---- Регресс ----
- id: TC-14
type: integration
description: "Полный регресс: pytest tests/ -q зелёный; существующие структурные док-тесты (test_lite_setup_doc, test_bundled_setup_doc, test_orch_52b_docs_standard, test_agent_prompts_canon) не сломаны (AC-10/AC-11)."
module: tests/
expected: PASS

View File

@@ -0,0 +1,388 @@
---
work_item: ORCH-011
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-11
model_used: claude-opus-4-8
---
# ADR-001: Витрина системы `docs/overview/` — структура, путь к PPTX, анти-дрейф контур
Work Item: **ORCH-011** — Полная документация системы мультиагентов оркестратора (бизнес + тех, для людей и презентаций)
Стадия: **architecture**
Сквозная регистрация: **`docs/architecture/adr/adr-0039-system-overview-docs-canon.md`**
(новый docs-раздел `docs/overview/` с нормативом сопровождения, обязательным для всех будущих
функциональных PR, + расширение reviewer-оси обзорных доков ORCH-079 — кросс-каттинг).
## Статус
Proposed
## Контекст
Решения Владельца (BRD §1.4): D-1 презентация = PowerPoint, тёмный дизайн, точный рендеринг;
D-2 три аудитории (разработчики/заказчики/менеджеры); D-3 единое место — `docs/` репозитория;
D-4 витрина поддерживается актуальной сразу после изменения функционала.
Факты, сверенные с кодом/репо на ветке задачи:
- **Документация богатая, но фрагментированная** (BRD §1.2, проверено): паспорт `CLAUDE.md`
(реестр доработок, не вводный текст), тех-витрина `README.md`, vision
`docs/PRODUCT_VISION.md` (v1.0, «концепция развития»), инженерный справочник
`docs/architecture/README.md` (~1246 строк) + `internals.md`, 38 сквозных ADR, стандарты
`docs/_standards/`, операционные/деплойные доки. Единой точки входа «бизнес + тех» нет.
- **Живое доказательство риска гниения (R-1):** схема конвейера в `docs/PRODUCT_VISION.md` §2
уже устарела — в ней нет стадии `deploy-staging` и стока `cancelled`, фактически
присутствующих в `src/stages.py::STAGE_TRANSITIONS` (9 ключей + `cancelled`). Снапшот без
машинной сверки расходится с кодом за недели.
- **Прецедент бинаря без воспроизводимости:** `docs/PRODUCT_VISION.pptx` закоммичен, но пути
его генерации в репо нет (`grep pptx scripts/ docs/` — пусто) — ровно тот паттерн
«невоспроизводимый артефакт», который BR-5 требует исключить.
- **Reviewer-ось обзорных доков (ORCH-079) по букве привязана к README:**
`.openclaw/agents/reviewer.md` формулирует ось через «пункт из `README.md` „Известные
ограничения“»; новая витрина под букву оси не подпадает (релевантно OQ-5). История ORCH-079
показывает: общего правила «документация = golden source» недостаточно — обзорные доки гниют,
пока ось не названа явно.
- **Тестовая механика готова:** паттерны структурных доков-тестов —
`tests/test_lite_setup_doc.py` / `test_bundled_setup_doc.py` (разделы по порядку, кирпичи,
скан FORBIDDEN импортом из `tests/test_no_host_hardcodes.py` (`FORBIDDEN`,
`find_violations`), секрет-эвристика, кросс-рефы); импорт `STAGE_TRANSITIONS` в тестах —
норма (ORCH-091: полнота derive из кода, не статичный список); импорт `QG_CHECKS`
`tests/test_qg_registry_snapshot.py`; импорт чистых функций из `scripts/`
`tests/test_bootstrap_script.py`.
- **Источники контента самодостаточны внутри репо** (BRD §6): вне-репозиторные затравки
(`tasks/…`, `memory/…`) недоступны и не нужны; таблица модель/эффорт — ORCH-41/81
(developer=`xhigh`, tester/deployer=`medium`, прочие=`high`, все на `claude-opus-4-8`).
- `docs/overview/` свободен (коллизий имён нет).
Задача — **docs+tests (+dev-скрипт)** (паттерн ORCH-102/103): `src/**`,
`docker-compose.yml`, `Dockerfile`, `requirements*` читаются как источник истины и НЕ меняются;
конвейер (`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схема БД) — байт-в-байт.
Kill-switch не нужен: документация и dev-скрипт конвейером не исполняются (активация генерации —
только явный запуск человеком, паттерн ORCH-009).
## Решение
### Сводка
Создаётся новый docs-раздел **`docs/overview/`** — витрина системы: индекс `README.md`
(точка входа, маршруты трёх аудиторий, норматив сопровождения), бизнес-часть `business.md`,
семь тех-файлов `tech-*.md` (= 7 блоков контент-карты TRZ FR-3, link-first), слайдо-источник
`presentation.md` + dev-скрипт `scripts/build_presentation.py` (python-pptx, вне прод-образа;
бинарь `.pptx` НЕ коммитится — D5). Машинно-проверяемые факты витрины (стадии, гейты, агенты,
ссылки, гигиена секретов, слайдо-структура, чистота prod-зависимостей) защищаются структурным
`tests/test_system_docs.py` (D6). Reviewer-ось обзорных доков точечно расширяется на витрину
(D7). Исходы всех открытых вопросов ТЗ §9 (OQ-1…OQ-5) — в D1/D4/D5/D9/D7 соответственно.
### D1 (исход OQ-1) — Размещение и состав: `docs/overview/`, плоский каталог, 10 файлов
**Решение: каталог `docs/overview/`** (рекомендация аналитика подтверждена). Семантика
docs-разделов после ORCH-011: `overview/` — «что это за система и как она устроена» (читатель —
любой из трёх аудиторий, входит впервые); `architecture/` — инженерный справочник (детали);
`deployment/` — «как развернуть у себя» (ORCH-102/103); `operations/` — «как эксплуатировать
наш прод»; `_standards/` — нормативы для агентов.
**Состав — плоский каталог, 10 файлов** (модульность NFR-6: будущие правки точечные; плоскость —
одна глубина относительных ссылок и простые globs в тестах; индекс — `README.md`, а не
`index.md`: авто-рендер в web-UI Gitea, симметрия `docs/architecture/README.md`):
| Файл | Содержание | Покрывает |
|------|-----------|-----------|
| `README.md` | индекс: «что это» в 12 абзацах; навигация на все части; 3 маршрута аудиторий (D8); норматив сопровождения (D8) | FR-1, FR-5, FR-6, AC-1, AC-8, AC-9 |
| `business.md` | бизнес-уровень (D2) | FR-2, AC-2 |
| `tech-architecture.md` | блок 1: компоненты и связи + диаграмма потока | FR-3.1, AC-3 |
| `tech-pipeline.md` | блок 2: конвейер/стадии/гейты/под-гейты/откаты/человеческие гейты/авто-лейблы/багфикс-трек/serial gate/статусная модель Plane | FR-3.2, AC-4 |
| `tech-agents.md` | блок 3: 6 ролей, входы/выходы/артефакты/verdict-ключи, таблица модель/эффорт, канон промптов | FR-3.3, AC-5 |
| `tech-data-model.md` | блок 4: Project → Work-Item/Task → Jobs → Agent-runs → события/дедуп → вспомогательные таблицы; словарь терминов | FR-3.4 |
| `tech-integrations.md` | блок 5: Plane / Gitea / LLM / Telegram | FR-3.5 |
| `tech-quality-security.md` | блок 6: философия QG, frontmatter-контракт, security/coverage-гейты, канон секретов, self-hosting-страховки | FR-3.6 |
| `tech-observability.md` | блок 7: live-карточка, `/queue`, `/metrics`, sidecar, журнал уроков, стоимость | FR-3.7 |
| `presentation.md` | слайдо-источник + процедура сборки `.pptx` (D4) | FR-4, AC-7 |
Один тех-блок = один файл (а не монолит `tech.md` и не два файла «business/tech»): блоки
независимы по темпу устаревания (pipeline меняется чаще, чем интеграции), точечный PR правит
один файл; тесту проще ассертить присутствие и непустоту каждого блока пофайлово.
Привязка: BR-1, NFR-6, AC-1, AC-3.
### D2 — Бизнес-уровень: текущее состояние, не vision; числа только с подтверждением
**Решение: `business.md` описывает фактическое состояние платформы** (контент-минимум FR-2:
проблема → решение → что умеет → ценность → ≥5 сценариев), переиспользуя нарратив
`docs/PRODUCT_VISION.md` §12 со сверкой с кодом. Нормативные правила:
- **Разделение ролей документов:** PRODUCT_VISION остаётся vision («куда идём», не трогается;
допустима врезка-ссылка на витрину — на усмотрение developer); `business.md` — «что есть
сейчас». Расхождение vision ↔ код (пример: устаревшая схема конвейера в vision §2) в витрину
не переносится — витрина строится от `STAGE_TRANSITIONS`.
- **Числовые метрики** (скорость/стоимость) — только с внутрирепозиторным подтверждением либо
с явной атрибуцией «оценка из PRODUCT_VISION»; новые цифры не изобретать (FR-2, AC-12).
- **Без жаргона:** кодовые идентификаторы (`ORCH-NNN`, имена функций/модулей) в основном тексте
не используются; термины конвейера (стадия, гейт, ревью) объясняются по-человечески; детали —
сносками/ссылками в тех-часть (AC-2).
- Обязательный минимум способностей — список AC-2 (конвейер задача→прод, мультипроектность,
самовосстановление, пакетный авто-режим, багфикс-трек, STOP, наблюдаемость, self-hosting,
тираж Lite/Bundled) — каждый пункт сводится к одному абзацу простым языком.
Привязка: BR-2, FR-2, AC-2, AC-12.
### D3 — Тех-уровень: 7 файлов по контент-карте TRZ, link-first, согласованность с кодом
**Решение: контент-карта TRZ §3 FR-3 принимается как нормативная** (обязательное содержание и
источники истины каждого блока — таблица FR-3, в ADR не дублируется). Архитектурные уточнения:
- **Link-first (BR-4, анти-«вторая правда»):** каждый файл даёт цельную картину уровня
«понять устройство» и ведёт ссылкой в golden source за деталями
(`docs/architecture/README.md`, `internals.md`, `_standards/*`, ADR-реестр, deployment-доки).
Запрещено копировать в витрину таблицы/списки, которые живут в golden source и меняются с
кодом (таблица компонентов, карта env, 22 статуса Plane). Разрешённый дубль — только
машинно-сверяемый тестом факт (перечень стадий, имена гейтов, 6 агентов, таблица
модель/эффорт) — ровно потому, что тест D6 рвёт CI при дрейфе (прецедент — key-sync TC-02b
ORCH-102).
- **`tech-architecture.md` несёт одну диаграмму потока** «вебхук → очередь → агент → гейт →
переход» (mermaid или ASCII — на выбор developer; AC-3 требует хотя бы одну).
- **`tech-pipeline.md`:** схема стадий включает `deploy-staging` и сток `cancelled`; под-гейты
ребра `deploy-staging→deploy` перечисляются в фактическом порядке **security → merge →
coverage → image-freshness** (нормативный порядок — adr-0029/ORCH-027) и явно помечаются как
**врезки в переход, а не стадии** (AC-4); под-гейт `deploy→done` (merge-verify) — аналогично;
человеческие гейты (Approved, Confirm Deploy) и их снятие авто-лейблами (ORCH-089), STOP
(ORCH-090), багфикс-маршрут (ORCH-019), serial gate/freeze (ORCH-088); «статусы Plane =
индикация ≠ управление» (ORCH-066).
- **`tech-agents.md`:** паспорт каждой из 6 ролей по `PIPELINE_DOCS.md` §2 (назначение, стадия,
вход, артефакты, machine-verdict ключ — имена байт-в-байт: `verdict:`/`result:`/
`staging_status:`/`deploy_status:`/`security_status:`); таблица модель/эффорт = фактический
резолв config (ORCH-41/74/81).
- **Терминология** едина со статусной моделью Plane (ORCH-066) и PIPELINE_DOCS (NFR-4); язык —
русский.
Привязка: BR-3, BR-4, FR-3, AC-3…AC-6.
### D4 (исход OQ-2) — Презентация: `presentation.md` (машинно-парсимый источник) + `scripts/build_presentation.py` на python-pptx
**Решение: слайдо-источник — `docs/overview/presentation.md`** с жёсткой машинно-парсимой
структурой:
```markdown
## Слайд N: <Заголовок>
- <тезис 1> (36 тезисов на слайд)
- <тезис 2>
> Визуал: <подпись визуального мотива слайда> (опционально)
```
Нумерация сквозная с 1; объём — **1418 слайдов** (в коридоре ориентира FR-4 1220);
нормативные смысловые биты нарратива (порядок FR-4): проблема → решение → как работает
(конвейер+гейты) → роли агентов → человек в контуре → возможности (автономный пакетный режим,
багфикс-трек, самовосстановление, наблюдаемость) → сценарии → тираж → статус платформы.
Точная нарезка по слайдам — за developer; тест D6 ассертит структуру и биты, не прозу.
**Генератор — `scripts/build_presentation.py` на `python-pptx`:**
- **Запуск только вне рантайма** (host/dev venv, явный запуск человеком — паттерн ORCH-009);
`python-pptx` НЕ добавляется в `requirements*`/`Dockerfile` (NFR-2; машинный гард — D6 TC-09).
- **Архитектура скрипта:** чистая функция-парсер `parse_slides(text) -> list[Slide]`
(stdlib-only, без импорта pptx) + рендерер с **ленивым** `import pptx` внутри функции сборки.
Один парсер = один источник истины о формате: `tests/test_system_docs.py` импортирует
`parse_slides` (механика импорта из `scripts/` — прецедент `test_bootstrap_script.py`) и
валидирует слайдо-источник без установленного python-pptx.
- **Тёмный дизайн (D-1):** константы темы в скрипте — тёмный фон (≈`#1F1F2E`), светлый текст,
один акцентный цвет; шрифты — стандартные системные с полной кириллицей (Calibri/Arial).
python-pptx пишет **настоящий редактируемый текст** в slide-объекты → «точный рендеринг»
гарантирован самим PowerPoint (а не headless-браузером), кириллица не растрируется, Владелец
может править слайды руками.
- **Процедура — в `presentation.md`**, раздел «Как собрать .pptx», форма «fenced-команда +
Проверка:» (канон LITE_SETUP): создать venv → `pip install python-pptx` → запуск скрипта →
проверка (скрипт печатает число собранных слайдов; файл открывается, тема тёмная). Дефолтный
выход — `build/orchestrator-overview.pptx` (D5).
**Почему python-pptx, а не альтернативы:** Marp → pptx требует Node+Chromium и экспортирует
слайды растровыми картинками (нередактируемо, текст не выделяется — против «точного
рендеринга» как редактируемого артефакта); pandoc → pptx ограниченно управляет тёмной темой
(reference-doc с мастер-слайдами — отдельный бинарный артефакт в репо); «только ручная
процедура» — слабейшая воспроизводимость (человек = источник дрейфа). python-pptx: один
pip-пакет в одноразовом dev-venv, детерминированный код-как-дизайн, тестируемый парсер.
Привязка: BR-5, FR-4, AC-7, NFR-2.
### D5 (исход OQ-3) — Бинарь `.pptx` НЕ коммитится; источник истины — markdown + скрипт
**Решение: собранный `.pptx` в git НЕ коммитится.** Канон: источник истины —
`presentation.md` + `build_presentation.py`; артефакт собирается по требованию за одну команду.
Обоснование: бинарь не диффуем, анти-дрейф тесты его содержимое не проверяют → закоммиченный
deck молча устаревает относительно источника (живой пример — `docs/PRODUCT_VISION.pptx`:
закоммичен, пути генерации нет, контент vision уже разошёлся с кодом); BR-5 требует
воспроизводимость пути, не бинарь (BRD §6).
- Дефолтный выход генератора — `build/orchestrator-overview.pptx`; в `.gitignore` добавляется
строка `build/` (разрешённое отклонение диффа, не рантайм).
- **Существующий `docs/PRODUCT_VISION.pptx` не трогается** (артефакт другого work item;
ретроактивная чистка — вне объёма §2.2). Новый канон действует на витрину и вперёд.
Привязка: BR-5, AC-7, BRD §6.
### D6 (FR-7) — Анти-дрейф контур: `tests/test_system_docs.py`
**Решение: один структурный модуль, детерминированный, без сети/LLM/subprocess** (образец —
`test_lite_setup_doc.py`/`test_bundled_setup_doc.py`). Нормативные семейства проверок (точная
нарезка по тест-функциям — за developer, `04-test-plan.yaml` дополняется на development):
| TC | Проверка | Механика |
|----|----------|----------|
| TC-01 | Все 10 файлов витрины D1 существуют и непусты | `Path.exists()` + размер |
| TC-02 | Индекс: 3 маршрута аудиторий («Я заказчик» / «Я менеджер» / «Я разработчик») и норматив сопровождения присутствуют; из индекса по относительным ссылкам достижимы business / все 7 tech / presentation (AC-1) | regex-извлечение ссылок + подстроки |
| TC-03 | **Карта стадий = код:** каждый ключ `src.stages.STAGE_TRANSITIONS` (вкл. `deploy-staging`, `cancelled`) упомянут в `tech-pipeline.md`; порядок основной цепочки created→…→done — по возрастанию позиций вхождений; derive из импорта, не статичный список (паттерн ORCH-091) | `import src.stages` + поиск позиций |
| TC-04 | **Гейты = код:** имя exit-гейта каждого ребра (`qg` из `STAGE_TRANSITIONS`) упомянуто в `tech-pipeline.md`; под-гейты названы фактическими именами реестра `QG_CHECKS` (`check_security_gate`, `check_branch_mergeable`, `check_coverage_gate`, `check_staging_image_fresh`) и идут в нормативном порядке security → merge → coverage → image-freshness (порядок — фикс adr-0029, позиционный ассерт); рядом с под-гейтами есть маркер «не стадии» (врезки) | импорт `STAGE_TRANSITIONS` + `QG_CHECKS`, позиции подстрок |
| TC-05 | **Полнота 6 агентов:** derive из glob `.openclaw/agents/*.md` (не статичный список) — стем каждого файла упомянут в `tech-agents.md`; таблица эффортов сходится с config-дефолтами (поля class-default `agent_effort_<role>`, ORCH-81) | glob + `import src.config` |
| TC-06 | **Валидность ссылок:** все относительные md-ссылки всех файлов витрины резолвятся в существующие файлы; обязательный список ссылок AC-6 (architecture/README, internals, PIPELINE_DOCS, HANDOFF_PROTOCOL, adr-реестр, LITE_SETUP, BUNDLED_SETUP, PRODUCT_VISION, CLAUDE.md) присутствует | regex `\[..\]\((..)\)` + `Path.exists()` относительно файла |
| TC-07 | **Гигиена:** полнотекстовый скан всех 10 файлов витрины — нет литералов центрального списка `FORBIDDEN` (импорт из `tests/test_no_host_hardcodes.py`, не копия) и секретоподобных значений (эвристика hex/base64 ≥ 32 симв., не плейсхолдер); нет ссылок на вне-репозиторные пути (`tasks/`, `memory/`) (AC-12) | `find_violations` + эвристика |
| TC-08 | **Слайдо-источник:** парс через `parse_slides` из `scripts/build_presentation.py` (один парсер — D4): ≥ 12 слайдов, нумерация сквозная с 1, на каждом ≥ 1 тезис; нормативные биты нарратива FR-4 присутствуют (подстроки: проблема/решение/сценарии/тираж/статус) | импорт чистой функции (прецедент `test_bootstrap_script.py`) |
| TC-09 | **NFR-2 машинно:** подстрока `pptx` отсутствует в `requirements*` и `Dockerfile` | чтение файлов |
| TC-10 | **Указатели:** `README.md` ссылается на `docs/overview/`; `CLAUDE.md` несёт указатель на витрину; `CHANGELOG.md` несёт `ORCH-011` | подстроки |
Скоуп FORBIDDEN-скана — **полнотекстовый** (не только fenced, в отличие от ORCH-102 TC-05):
витрина по построению не дублирует дефолты/хост-специфику даже в прозе (NFR-3), упоминать
боевые литералы ей незачем → ложно-красных нет, а защита шире. Существующие тесты не
ослабляются; новые попадают в существующие гейты (`check_ci_green`/`check_tests_passed`/
merge-gate re-test/coverage ORCH-027) автоматически — **новый QG НЕ регистрируется** (ТЗ §6).
Привязка: BR-8, FR-7, AC-4, AC-5, AC-10, AC-11, AC-12.
### D7 (исход OQ-5) — Reviewer-ось обзорных доков: точечная правка промпта НУЖНА
**Решение: расширить существующую ось ORCH-079 в `.openclaw/agents/reviewer.md` точечной
врезкой, называющей витрину явно.** Обоснование: по букве ось привязана к `README.md`
«Известные ограничения» («если PR закрывает/меняет пункт из README…»); витрина под букву не
подпадает, а история ORCH-079 показала, что общего правила «документация = golden source»
для обзорных доков недостаточно — ось работает, когда названа явно (❌→✅-паттерн). Норматив
правки:
- В оси 4 «Документация» и в соответствующем ❌→✅-пункте `<constraints>` существующая
формулировка ORCH-079 дополняется: *PR меняет функциональность, описанную в витрине
`docs/overview/` (стадии, гейты, агенты, интеграции, способности из business.md), а витрина
не обновлена → finding ≥ P1* — **расширение трактовки той же оси, не новая ось**.
- Канон 52d сохраняется байт-в-байт: 5 XML-секций и их порядок не меняются, verdict-ключ
`verdict: APPROVED|REQUEST_CHANGES` не трогается; правка — добавление текста внутрь
существующих секций (паттерн самой ORCH-079).
- `tests/test_agent_prompts_canon.py` расширяется ассертом на упоминание витрины в оси
обзорных доков (анти-регресс; существующие ассерты остаются зелёными).
- Зеркальная правка правила №6 `CLAUDE.md` («Reviewer проверяет…») — упоминание витрины.
Привязка: BR-7, FR-6, AC-9.
### D8 — Индекс: маршруты аудиторий и норматив сопровождения; указатели репо
**Маршруты (FR-5, нормативный состав):** индекс несёт три явных маршрута с упорядоченными
списками «что читать»:
- **«Я заказчик»:** `business.md` → сценарии → `presentation.md`
`docs/deployment/LITE_SETUP.md`/`BUNDLED_SETUP.md`;
- **«Я менеджер проекта»:** `business.md``tech-pipeline.md` (конвейер, статусная модель
Plane, человеческие гейты) → `tech-observability.md`;
- **«Я разработчик»:** `tech-*` (1→7) → `docs/architecture/README.md``internals.md`
`docs/_standards/` → реестр ADR → `CLAUDE.md`.
**Норматив сопровождения (FR-6, формулировка по образцу NFR-5 ORCH-102/103):** в индексе —
явная норма «**изменил функциональность платформы → обнови витрину `docs/overview/` в том же
PR**» (+ указание, какой файл какому классу изменений соответствует). Указатели:
- `CLAUDE.md`: в правило №2 («Документация = golden source») добавляется указатель на витрину
и норматив; правка правила №6 — D7; строка о витрине в разделе «Структура» (`docs/`).
- `README.md`: ссылка на витрину в начале (рядом с «Архитектура») — «единая точка входа в
документацию системы».
- `CHANGELOG.md`: запись `docs:` по ORCH-011.
Привязка: BR-6, BR-7, BR-9, FR-5, FR-6, AC-8, AC-9, AC-11.
### D9 — Границы изменения; 07/08 — N/A; исход OQ-4; эскалация не требуется
- **Дифф задачи:** `docs/overview/` (10 файлов, D1), `tests/test_system_docs.py` (D6),
`scripts/build_presentation.py` (D4), правки `.openclaw/agents/reviewer.md` +
`tests/test_agent_prompts_canon.py` (D7), `README.md`/`CLAUDE.md`/`CHANGELOG.md` (D8),
`.gitignore` (+`build/`, D5), ADR-пакет work item + сквозной adr-0039 + секция в
`docs/architecture/README.md`. **`src/**`, `docker-compose.yml`, `Dockerfile`,
`requirements*` — ноль изменений** (NFR-1; машинный гард — TC-09); любое отклонение — только
новым ADR.
- `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict ключи/схема БД — байт-в-байт; новых
эндпоинтов/флагов/kill-switch нет (выключать нечего: доки и dev-скрипт не исполняются
конвейером).
- **`07-infra-requirements.md` / `08-data-requirements.md` — N/A** (прецедент ORCH-102 D9):
топология не меняется (ни контейнера, ни порта, ни маунта), схема БД не меняется (ТЗ §5).
Dev-venv для генерации `.pptx` — не инфра-предусловие конвейера: создаётся ad-hoc человеком
при сборке презентации (процедура — `presentation.md`).
- **Исход OQ-4 (compiled-wiki/экспорт):** вне объёма v1 — зафиксировано; репозиторий —
единственный источник истины («канон не форкается», ORCH-009 BR-2). Follow-up work item не
заводится автоматически: потребность в wiki-экспорте — решение Владельца после приёмки
витрины (если витрина в репо закрывает D-2/D-3, экспорт не нужен вовсе).
- **Объём одного прогона (R-3):** допущение BRD §6 принимается; приоритет при дефиците объёма:
каркас+индекс → tech-pipeline/tech-agents (машинно-сверяемые) → business → остальные tech →
presentation+скрипт. Молчаливое сокращение запрещено — недоезд = эскалация разбиением.
- **Self-hosting (NFR-5):** прод-контейнер не рестартится; выкат — штатный конвейер
(deploy-staging 8501 → Confirm Deploy). Для enduro-trails изменение инертно (общих
артефактов нет).
- **Эскалация:** `arch:major-change` не требуется (нет новой стадии/компонента/смены БД —
docs-канон); ТЗ удовлетворимо без нарушения принципов — возврат в анализ не нужен.
## Альтернативы
- **Расширить `README.md` вместо нового раздела** — отвергнуто: README — тех-витрина с
собственной ролью (и осью ORCH-079); бизнес-уровень + 7 блоков + маршруты раздули бы его в
монолит против NFR-6; D-3 предполагает выделенное «единое место».
- **`docs/system/` как имя каталога** — отвергнуто: «overview» точнее передаёт роль (обзор для
входа), рекомендация аналитика, прецедентов коллизии нет.
- **Монолит `tech.md` (или пара business/tech)** — отвергнуто: блоки устаревают с разным
темпом; точечные правки и пофайловые ассерты тестов дешевле на 7 файлах (NFR-6).
- **Marp / pandoc для PPTX** — отвергнуто (D4): Node+Chromium-тулчейн и растровые слайды
(Marp) / ограниченная тёмная тема через бинарный reference-doc (pandoc); python-pptx даёт
редактируемый текст и код-как-дизайн с одним pip-пакетом вне прод-образа.
- **Коммитить собранный `.pptx`** — отвергнуто (D5): недиффуемый, тестами не проверяемый,
молча устаревает (живой пример — `PRODUCT_VISION.pptx` без пути генерации); BR-5 требует
воспроизводимость, не бинарь.
- **Жёсткий снапшот-тест контента витрины (хэши/полные списки компонентов)** — отвергнуто:
превратил бы каждую docs-правку в красный CI (ложная жёсткость); тесты держат только
машинно-проверяемые факты, derive из кода (стадии/гейты/агенты), остальное — за reviewer.
- **Не править промпт reviewer (положиться на общее правило)** — отвергнуто (D7): по букве ось
ORCH-079 привязана к README; сам ORCH-079 существует потому, что общего правила для обзорных
доков не хватило; одна строка расширения дешевле гниющей витрины.
- **Автогенерация витрины из кода (autodoc)** — отвергнуто: явно вне объёма (BRD §2.2);
derive-from-code остаётся в тестах (сверка), не в генерации текста.
## Последствия
- **+** Единая точка входа для трёх аудиторий закрывает корневую проблему фрагментации
(BRD §1.2); презентация собирается за одну команду из версионируемого источника.
- **+** Машинно-проверяемые факты витрины (стадии/гейты/агенты/ссылки/гигиена/чистота
prod-зависимостей) — CI-гарантии (TC-01…TC-10), а не обещания: дрейф ловится тестом, гниение
прозы — расширенной reviewer-осью (D7).
- **+** Нулевой риск рантайма: docs+tests+dev-скрипт, конвейер байт-в-байт, kill-switch не
нужен; для enduro-trails инертно.
- **** Новый golden source = новая обязанность сопровождения каждого функционального PR —
принято осознанно (в этом смысл задачи), митигировано link-first (правится одна строка-резюме,
не трактат), нормативом в индексе/CLAUDE.md и осью reviewer.
- **** Разрешённый машинно-сверяемый дубль (стадии/гейты/агенты в `tech-pipeline.md`/
`tech-agents.md`) — двойная запись фактов кода; защищён derive-тестами TC-03…TC-05
(прецедент TC-02b ORCH-102).
- **** Правка промпта reviewer — расширение поверхности канона 52d; митигировано: только
добавление внутрь существующих секций, анти-регресс `test_agent_prompts_canon.py`.
- **** `.pptx` не в репо — показ «здесь и сейчас» требует одной команды сборки; принято
(Владелец собирает deck при необходимости; альтернатива — гниющий бинарь — хуже).
- **Откат:** удалить `docs/overview/`, `tests/test_system_docs.py`,
`scripts/build_presentation.py`, вернуть точечные правки README/CLAUDE/CHANGELOG/.gitignore/
reviewer.md — состояние 1:1, ни миграций, ни состояния (ТЗ §7).
## Ссылки
- BRD: `docs/work-items/ORCH-011/01-brd.md` (решения Владельца D-1…D-4, факты §1.2)
- TRZ: `docs/work-items/ORCH-011/02-trz.md` (FR-1…FR-7, OQ-1…OQ-5)
- Acceptance: `docs/work-items/ORCH-011/03-acceptance-criteria.md` (AC-1…AC-12)
- Риски: `docs/work-items/ORCH-011/10-tech-risks.md`
- Сквозной ADR: `docs/architecture/adr/adr-0039-system-overview-docs-canon.md`
- Сверено по коду/репо: `src/stages.py::STAGE_TRANSITIONS` (9 стадий + `cancelled`),
`src/qg/checks.py::QG_CHECKS` (14 проверок), `.openclaw/agents/*.md` (6 промптов; ось
ORCH-079 в `reviewer.md`), `docs/PRODUCT_VISION.md` §12 (+ устаревшая схема конвейера §2),
`docs/PRODUCT_VISION.pptx` (бинарь без пути генерации), `tests/test_lite_setup_doc.py` /
`test_bundled_setup_doc.py` (паттерн структурных доков-тестов),
`tests/test_no_host_hardcodes.py` (`FORBIDDEN`, `find_violations`),
`tests/test_qg_registry_snapshot.py` (импорт QG_CHECKS), `tests/test_bootstrap_script.py`
(импорт чистых функций из `scripts/`), `.gitignore`
- Инварианты соседних решений: adr-0019 (стандарт доков), adr-0021/ORCH-092 (канон промптов
52d), adr-0023 (ось обзорных доков ORCH-079), adr-0029 (порядок под-гейтов), adr-0037/0038
(deployment-каноны — ссылаемся, не форкаем), ORCH-041/074/081 (модель/эффорт),
ORCH-066 (статусная модель), `docs/_standards/PIPELINE_DOCS.md` §4 (ADR-naming),
`docs/_standards/TRACEABILITY.md` (маркеры)

View File

@@ -0,0 +1,40 @@
---
work_item: ORCH-011
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-11
model_used: claude-opus-4-8
---
# 10 — Технические риски: ORCH-011 — Полная документация системы мультиагентов (витрина + PPTX)
Work Item: **ORCH-011** · Repo: **orchestrator** · Стадия: architecture
> Информационный (гейтом не парсится). Перечисляет риски реализации и их митигейшн.
> Базовые бизнес-риски R-1…R-5 — BRD §8; здесь — их техническая детализация + новые.
## Реестр рисков
| ID | Риск | Вер. | Влия. | Митигейшн |
|----|------|------|-------|-----------|
| TR-1 | **Гниение витрины** (R-1): self-hosting темп быстро устаревает снапшот; живой пример уже в репо — схема конвейера в `PRODUCT_VISION.md` §2 потеряла `deploy-staging`/`cancelled` | Выс. | Сред. | Машинно-проверяемые факты держат derive-тесты (ADR-001 D6 TC-03…TC-05: стадии/гейты/агенты импортом из кода); проза — норматив сопровождения в индексе + расширенная reviewer-ось (D7); link-first сводит правку к одной строке-резюме |
| TR-2 | **Второй источник истины** (R-2): дубль деталей architecture/README в витрине → противоречия | Сред. | Сред. | Норматив D3: запрещён дубль живых таблиц golden sources; разрешённый дубль — только машинно-сверяемый тестом факт (прецедент key-sync ORCH-102 TC-02b); контроль — AC-6 + reviewer |
| TR-3 | **Объём одного прогона** (R-3): 10 файлов + скрипт + тесты + правка промпта могут не поместиться в разумный PR | Сред. | Сред. | Модульность D1 (правки независимы); нормативный приоритет блоков при дефиците (D9); молчаливое сокращение запрещено — эскалация разбиением по штатному маршруту |
| TR-4 | **Утечка зависимости генерации в прод-образ** (R-4): `python-pptx` случайно попадает в `requirements*`/`Dockerfile` | Низ. | Выс. | Архитектура скрипта D4 (lazy import, запуск только вне рантайма); **машинный гард TC-09** (скан `requirements*`/`Dockerfile` на `pptx`) — попадание рвёт CI |
| TR-5 | **Ложная жёсткость анти-дрейф тестов:** слишком буквальные ассерты (точные фразы прозы) делают каждую будущую docs-правку красной → тесты начнут ослаблять | Сред. | Сред. | D6: ассерты только на стабильное (заголовки, имена из кода через импорт, относительные ссылки, биты-подстроки); снапшот-контента отвергнут явно (ADR-001 «Альтернативы») |
| TR-6 | **Регресс канона 52d при правке промпта reviewer** (D7): нарушение порядка секций / verdict-ключа | Низ. | Выс. | Правка — только добавление текста внутрь существующих секций (паттерн ORCH-079); анти-регресс `tests/test_agent_prompts_canon.py` (существующие ассерты + новый на упоминание витрины) |
| TR-7 | **Кириллица/тема в PPTX:** артефакт собирается, но рендеринг не «точный» (D-1): шрифт без кириллицы, контраст темы | Низ. | Сред. | python-pptx пишет редактируемый текст (не растр); шрифты — системные с полной кириллицей (Calibri/Arial); процедура в `presentation.md` несёт явную «Проверка:» (открыть файл, тема тёмная, кириллица читается); приёмка — AC-7 |
| TR-8 | **Парсер слайдо-источника расходится с тестом:** свой regex в тесте ≠ парсер скрипта → источник валиден для теста, но не собирается | Низ. | Сред. | Один парсер: тест импортирует `parse_slides` из `scripts/build_presentation.py` (D4/D6 TC-08; прецедент импорта из scripts — `test_bootstrap_script.py`) |
| TR-9 | **Цифры в бизнес-части не подтверждаются репо** (метрики скорости/стоимости из vision) → витрина теряет доверие / выдаёт желаемое за действительное | Сред. | Низ. | Норматив D2: числа только с внутрирепозиторным подтверждением или явной атрибуцией «оценка из PRODUCT_VISION»; новые цифры не изобретать (AC-12; reviewer проверяет) |
| TR-10 | **Путаница канона бинарей:** в репо остаётся `docs/PRODUCT_VISION.pptx` (старый паттерн), новый канон — «бинарь не коммитим» (D5) → будущий агент скопирует старый паттерн | Низ. | Низ. | Канон зафиксирован сквозным adr-0039 + нормативом в `presentation.md`; PRODUCT_VISION.pptx не трогается (чужой артефакт), но прецедентом не является — явная оговорка в ADR-001 D5 |
## Сводный вывод
Доминирующий класс — **риски сопровождения документации** (TR-1/TR-2/TR-5): изменение не несёт
рантайм-риска вовсе (docs+tests+dev-скрипт, `src/**` байт-в-байт, машинный гард TC-09), но
создаёт новый golden source, который без машинной сверки и явной reviewer-оси начал бы гнить с
первой же задачи. Митигация встроена в само решение (derive-тесты + link-first + норматив +
ось D7). Эскалация `arch:major-change` не требуется (нет новой стадии/компонента/смены БД);
возврат в анализ не нужен. Остаточный риск для прод-конвейера (self-hosting): **низкий**
прод-контейнер не затрагивается, деплой штатным маршрутом, для enduro-trails изменение инертно.

View File

@@ -0,0 +1,106 @@
---
verdict: APPROVED
work_item: ORCH-011
stage: review
author_agent: reviewer
status: approved
created_at: 2026-06-11
model_used: claude-opus-4-8
type: review
work_item_id: ORCH-011
version: 1
---
# Review ORCH-011
## Summary
PR (`7f0298b`, ветка `feature/ORCH-011-`) создаёт витрину системы `docs/overview/` строго по
ADR-001 (D1D9): 10 файлов плоского каталога (индекс + business + 7×tech-* + presentation),
dev-скрипт `scripts/build_presentation.py` (stdlib-парсер `parse_slides` + ленивый `import pptx`),
структурный анти-дрейф `tests/test_system_docs.py`, точечное расширение reviewer-оси ORCH-079 на
витрину (D7) + анти-регресс ассерт, указатели README/CLAUDE.md/PRODUCT_VISION/CHANGELOG,
сквозной `adr-0039` + секция в `docs/architecture/README.md`, `build/` в `.gitignore` (D5).
Проверено по 4 осям:
1. **Соответствие ТЗ (FR-1…FR-7, AC-1…AC-12)** — выполнено, детальная сверка ниже. Все 12 AC — PASS.
2. **Соответствие ADR** — реализация 1:1 с ADR-001 D1D9 и adr-0039; исходы OQ-1…OQ-5 воплощены
(каталог `docs/overview/`, python-pptx вне прод-образа, бинарь не закоммичен, OQ-4 вне объёма,
правка промпта reviewer по канону 52d). Глобальные ADR не нарушены: канон 52d (adr-0021) —
5 секций/порядок/verdict-ключ целы (зелёный `test_agent_prompts_canon.py`); ось ORCH-079
(adr-0023) — расширена аддитивно, не ослаблена; порядок под-гейтов (adr-0029) в витрине —
фактический security → merge → coverage → image-freshness (позиционный тест).
**Трассировка (TRACEABILITY):** правка блока с чужим маркером ORCH-079 в `reviewer.md` сверена
с его ADR — инвариант не сломан, расширение зафиксировано собственным D7 + тестом.
3. **Качество кода**`tests/test_system_docs.py`: 20 содержательных тест-функций, derive-сверки
импортом `STAGE_TRANSITIONS`/`QG_CHECKS`/glob промптов/class-default'ов config (не статика),
границы токенов стадий (`deploy` не матчится в `deploy-staging`), негативный самочек
секрет-эвристики, анти-выдумка имён гейтов по всем 10 файлам. `build_presentation.py`
докстринги на всех публичных функциях, честные коды возврата, ImportError-подсказка.
**Полный регресс: `pytest tests/ -q` → 1873 passed, 0 failed.**
4. **Документация** — обновлена в том же PR (см. раздел ниже). `src/**` НЕ изменён
(docs+tests+dev-скрипт), P0-правило «src без доки» неприменимо и не нарушено.
Сверка машинных фактов витрины с кодом (независимо от тестов):
- стадии/exit-гейты таблицы `tech-pipeline.md` = `src/stages.py::STAGE_TRANSITIONS` байт-в-байт
(`check_analysis_approved``check_deploy_status`, `done`/`cancelled` — терминалы);
- таблица модель/эффорт `tech-agents.md` = `src/config.py` (default `claude-opus-4-8`;
developer=`xhigh`, tester/deployer=`medium`, прочие=`high`);
- verdict-ключи ролей (`verdict:`/`result:`/`staging_status:`/`deploy_status:`) = канон AC-5;
- бинарь `.pptx` в diff отсутствует; `pptx` в `requirements*`/`Dockerfile` отсутствует (NFR-2);
- `docker-compose.yml`/`Dockerfile`/`requirements*`/схема БД/`QG_CHECKS` — ноль изменений (AC-11).
## Findings
### P0 — Blocker
Нет.
### P1 — Must fix
Нет.
### P2 — Should fix
Нет.
### P3 — Nice to have
- [ ] `docs/overview/tech-pipeline.md`, раздел «Статусная модель Plane»: «Управляющих статусов
ровно три: запуск в работу, Approved/Confirm Deploy … и STOP» — фактически перечислены четыре
статуса в трёх группах; формулировка «три управляющих воздействия (запуск, человеческие гейты,
отмена)» была бы точнее. Косметика прозы, машинных фактов не искажает (привязка: AC-4/FR-3.2 —
согласованность трактовок; не блокирует).
- [ ] `04-test-plan.yaml` не дополнен на development (норматив ADR-001 D6 «точная нарезка по
тест-функциям — за developer, 04-test-plan.yaml дополняется на development»). Решение developer
консервативно-корректное: правило №3 CLAUDE.md запрещает править артефакты других этапов, а все
TC-01…TC-14 плана реализованы 1:1 (маппинг «план TC-NN» зафиксирован в докстрингах
`test_system_docs.py`, TC-14 = полный зелёный регресс) — tester может работать по плану как есть.
Фиксирую как observation для будущего уточнения норматива, не как дефект.
## Сверка по критериям приёмки
| AC | Вердикт | Основание |
|----|---------|-----------|
| AC-1 точка входа | PASS | индекс `docs/overview/README.md`; все части достижимы (тест `test_index_links_reach_every_showcase_part`) |
| AC-2 бизнес-уровень | PASS | 5 разделов + 6 сценариев (≥5); без необъяснённого жаргона; цифра «35 минут» с атрибуцией Product Vision (D2) |
| AC-3 7 тех-блоков | PASS | 7 файлов `tech-*`; ASCII-схема потока «вебхук → очередь → агент → гейт → переход» в блоке 1 |
| AC-4 стадии/гейты = код | PASS | сверено с `src/stages.py` напрямую + derive-тесты (стадии, порядок цепочки, имена гейтов, порядок под-гейтов, маркер «не стадии») |
| AC-5 6 агентов | PASS | паспорта ролей + verdict-ключи + таблица модель/эффорт = config (сверено с `src/config.py`) |
| AC-6 link-first | PASS | все 9 обязательных golden-source ссылок присутствуют и резолвятся (тесты); живые таблицы не форкнуты |
| AC-7 презентация | PASS | 16 слайдов «## Слайд N:», нарратив полный; процедура «команда + Проверка:» ×3 шага; `pptx` вне прод-образа; бинарь не закоммичен |
| AC-8 3 маршрута | PASS | «Я заказчик / Я менеджер / Я разработчик» с упорядоченными списками по FR-5/D8 |
| AC-9 норматив | PASS | норма «в том же PR» + таблица «класс изменения → файл» в индексе; CLAUDE.md правила №2/№6; D7 разрешён правкой промпта + тест |
| AC-10 анти-дрейф | PASS | `tests/test_system_docs.py` покрывает все семейства D6; `pytest tests/ -q` → 1873 passed |
| AC-11 рантайм не тронут | PASS | diff: ноль изменений `src/**`/compose/Dockerfile/requirements; указатели обновлены; FORBIDDEN-скан зелёный |
| AC-12 самодостаточность | PASS | запрет `tasks/`/`memory/` — тест зелёный; источники внутрирепозиторные |
## Документация
Изменение само является документационным; все сопутствующие обязательства выполнены в том же PR:
- **Обновлено:** `CHANGELOG.md` (детальная `docs:`-запись ORCH-011), `README.md` (ссылка на
витрину), `CLAUDE.md` (строка «Структура», правила №2 и №6), `docs/PRODUCT_VISION.md`
(врезка-ссылка «фактическое состояние — витрина», vision не переписан — по ТЗ §2),
`docs/architecture/README.md` (секция витрины), ADR-пакет: work-item
`06-adr/ADR-001-system-overview-canon.md` + сквозной `adr-0039-system-overview-docs-canon.md`.
- **Обзорные доки (ORCH-079):** «Известные ограничения» README данный PR не закрывает —
обновления не требуется. Сама ось расширена на новую витрину (D7) с анти-регресс тестом.
- **Дополнительно обновлять нечего:** API/env/конфигурация/QG/схема БД не менялись (ТЗ §4§6).

View File

@@ -0,0 +1,79 @@
---
result: PASS
work_item: ORCH-011
stage: testing
author_agent: tester
status: pass
created_at: 2026-06-11
model_used: claude-opus-4-8
type: test-report
work_item_id: ORCH-011
---
# Test Report — ORCH-011
Полная документация системы мультиагентов: витрина `docs/overview/` + структурный анти-дрейф.
## Окружение
- Worktree (код ветки): `/repos/_wt/orchestrator/feature_ORCH-011-` (ветка `feature/ORCH-011-`)
- Python: 3.12.13
- pytest: 8.3.3
- Дата: 2026-06-11
- Предусловие: `12-review.md``verdict: APPROVED`
## Smoke API (read-only)
| Эндпоинт | Результат |
|----------|-----------|
| `GET /health` | PASS — `{"status":"ok","service":"orchestrator"}` |
| `GET /status` | PASS — задача ORCH-011 активна на стадии `testing` |
| `GET /queue` | PASS — payload несёт блоки `serial_gate` (ORCH-088) **и** `auto_labels` (наряду с `coverage`/`stop`/`bug_fast_track`/`lessons`) — регресса смока нет |
## Результаты (покрытие test-plan ↔ acceptance-criteria)
Изменение — docs+tests+dev-скрипт; анти-дрейф витрины реализован в `tests/test_system_docs.py`
(28 содержательных тест-функций), полностью зелёном. Каждый TC из `04-test-plan.yaml` выполнен и
сопоставлен с критериями `03-acceptance-criteria.md`.
| TC ID | Описание | AC | Тест-функция(и) `test_system_docs.py` | Результат |
|-------|----------|----|----------------------------------------|-----------|
| TC-01 | Каталог витрины + индекс с обязательными разделами | AC-1 | `test_all_showcase_files_exist_and_nonempty`, `test_index_carries_maintenance_normative` | PASS |
| TC-02 | Из индекса достижимы все части витрины | AC-1/AC-3 | `test_index_links_reach_every_showcase_part` | PASS |
| TC-03 | Бизнес-часть: 5 разделов + ≥5 сценариев | AC-2 | `test_business_part_has_five_mandatory_sections`, `test_business_part_has_at_least_five_scenarios` | PASS |
| TC-04 | Тех-часть: 7 блоков + схема потока | AC-3 | `test_architecture_block_carries_flow_diagram` (+ link-reach) | PASS |
| TC-05 | Стадии сверены импортом `STAGE_TRANSITIONS` (derive) | AC-4 | `test_every_stage_from_code_is_mentioned_in_pipeline_doc`, `test_main_chain_order_in_pipeline_doc_matches_code` | PASS |
| TC-06 | Имена exit-гейтов существуют в `QG_CHECKS` | AC-4 | `test_every_exit_gate_from_code_is_named_in_pipeline_doc`, `test_no_invented_gate_names_anywhere_in_showcase`, `test_subgates_in_normative_order_and_marked_as_insets` | PASS |
| TC-07 | 6 ролей + артефакты + таблица модель/эффорт = config | AC-5 | `test_every_agent_prompt_stem_is_covered`, `test_effort_table_matches_config_class_defaults` | PASS |
| TC-08 | Все ссылки резолвятся + обязательные golden sources | AC-6 | `test_all_relative_links_resolve_to_existing_files`, `test_mandatory_golden_source_links_present` | PASS |
| TC-09 | Нет вне-репозиторных путей (`tasks/`/`memory/`/абс. хоста) | AC-12 | `test_no_out_of_repo_references` | PASS |
| TC-10 | Презентация: ≥12 нумерованных слайдов + нарратив | AC-7 | `test_presentation_source_parses_with_canonical_parser`, `test_presentation_covers_mandatory_narrative_bits`, `test_presentation_carries_reproducible_build_procedure` | PASS |
| TC-11 | Зависимости генерации (`python-pptx`) вне прод-образа | AC-7/NFR-2 | `test_no_pptx_dependency_in_prod_image`, `test_build_script_toplevel_imports_are_stdlib_only` | PASS |
| TC-12 | Норматив «в том же PR» + ссылки README/CLAUDE.md | AC-9/AC-11 | `test_index_carries_maintenance_normative`, `test_repo_readme_links_overview`, `test_claude_md_carries_overview_pointer_and_normative`, `test_changelog_has_orch_011_entry` | PASS |
| TC-13 | FORBIDDEN-скан хост-литералов + секрет-эвристика | AC-10/AC-11 | `test_showcase_carries_no_forbidden_host_literals`, `test_showcase_carries_no_secret_like_values`, `test_secret_heuristic_is_not_evergreen` | PASS |
| TC-14 | Полный регресс `pytest tests/` зелёный; существующие док-тесты не сломаны | AC-10/AC-11 | весь прогон `tests/` (вкл. `test_lite_setup_doc`/`test_bundled_setup_doc`/`test_orch_52b_docs_standard`/`test_agent_prompts_canon`) | PASS |
Также покрыты `test_index_carries_three_audience_routes` (AC-8 — 3 маршрута аудиторий).
## Вывод pytest
Модуль витрины:
```
$ python3 -m pytest tests/test_system_docs.py -v
...
======================== 28 passed, 1 warning in 0.43s =========================
```
Полный регресс:
```
$ cd /repos/_wt/orchestrator/feature_ORCH-011- && pytest tests/ -v --tb=short
...
================== 1873 passed, 1 warning in 71.36s (0:01:11) ==================
```
(Единственный warning — преэкзистентный `PydanticDeprecatedSince20` в `src/config.py:8`,
не связан с ORCH-011, к коду ветки не относится.)
## Итог
PASS — все 14 TC выполнены и сопоставлены с критериями приёмки (AC-1…AC-12), smoke read-only
зелёный (`serial_gate` + `auto_labels` присутствуют в `/queue`), полный регресс `tests/`
**1873 passed, 0 failed**. Стадия переходит на `deploy-staging`.

View File

@@ -0,0 +1,12 @@
---
deploy_status: SUCCESS
work_item: ORCH-011
hook_exit_code: 0
deployed_by: deploy-finalizer
---
# Deploy log — ORCH-036 executable self-deploy
Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`.
Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.

View File

@@ -0,0 +1,32 @@
---
staging_status: SUCCESS
work_item: ORCH-011
stage: deploy-staging
author_agent: deployer
status: success
created_at: 2026-06-11
model_used: claude-opus-4-8
timestamp: 2026-06-11T06:35:40Z
base_url: http://localhost:8501
---
# Staging Gate Log
Staging test suite completed against the live `orchestrator-staging` environment (8501).
Run canonically inside the container via `docker exec orchestrator-staging
python3 /repos/orchestrator/scripts/staging_check.py --base-url http://localhost:8501
--mode stub` (ORCH-048). Exit code **0** → advance.
All REAL pipeline checks passed. The two failing checks are the known sandbox-infra
checks C9a/C9b (depend on SANDBOX bot accounts being members of the sandbox project —
not on the pipeline), tolerated under ORCH-061 since every REAL check is green.
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
## Results
- **Block A (SMOKE)**: PASS — A1 `/health` 200 ok, A2 `/queue` 200, A3 `ORCH_STAGING=true`.
- **Block B (ACCESS)**: PASS — B4 Plane sandbox accessible, B5 Gitea orchestrator-sandbox push=true, B6 registry isolated (sandbox present, prod ET/ORCH absent).
- **Block C (E2E, mode=stub)**: C7 create issue PASS, C8 trigger pipeline PASS; C9a/C9b waived sandbox-infra. CLEANUP ok (Plane issue deleted).
RESULT: 8/10 checks PASS — REAL failed: none; SANDBOX_INFRA waived: C9a, C9b.

View File

@@ -0,0 +1,12 @@
---
deploy_status: SUCCESS
work_item: ORCH-102
hook_exit_code: 0
deployed_by: deploy-finalizer
---
# Deploy log — ORCH-036 executable self-deploy
Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`.
Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.

View File

@@ -0,0 +1,7 @@
# Business Request: ORCH-10b Bundled-тираж: весь стек одним комплектом + bootstrap-скрипт
Work Item ID: ORCH-103
## Description
TBD

View File

@@ -0,0 +1,199 @@
---
work_item: ORCH-103
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-11
model_used: claude-opus-4-8
---
# 01 — BRD: ORCH-103 — ORCH-10b Bundled-тираж: весь стек одним комплектом + bootstrap-скрипт
Work Item: **ORCH-103** · Repo: **orchestrator** (self-hosting) · Стадия: analysis
Заказчик: Слава · Эпик: **ORCH-10** (домен D5 «Масштаб», `docs/epics/self-evolution.md`) · Тип: **B — Bundled**
---
## 1. Бизнес-контекст и проблема
### 1.1. Цель эпика ORCH-10
Тираж платформы — РАЗДАЧА текущей функциональности нескольким заказчикам **на тест**.
Решения Владельца 10.06 (приняты как требования): ДВА типа тиража, ОБА **stateless**
(наши задачи/данные/секреты НЕ переносим — чистый старт):
- **Тип A (Lite, ORCH-102 ✅)** — переносится ТОЛЬКО орк+watchdog; Plane/Gitea/LLM/Telegram
заказчик донастраивает сам по инструкции `docs/deployment/LITE_SETUP.md`.
- **Тип B (Bundled, эта задача)** — **весь стек одним комплектом**
(орк + watchdog + Gitea + Plane + рантайм-обвязка агентов) — «под ключ».
### 1.2. Проблема, которую закрывает ORCH-103
Lite предполагает, что у заказчика **уже есть** (или он сам поднимет) свои Plane и Gitea.
Для заказчика без собственной инфраструктуры это барьер: Plane CE self-hosted — это ~14
контейнеров со своей БД/брокером/хранилищем, Gitea — отдельная установка, и поверх всего —
первичная инициализация (админы, токены, workspace, 22 статуса, лейблы, вебхуки в обе стороны,
git-доступ агентов). Сегодня репо не содержит ни compose-описания этого стека, ни автоматизации
его доводки: разворачивание «с нуля до работающего конвейера» = многочасовая ручная работа по
сторонним докам с рисками дефолтных паролей и дрейфа от канона платформы.
ORCH-103 должен дать: **один compose-комплект** всего стека + **bootstrap-скрипт**, доводящий
свежеподнятый стек до рабочего состояния одной командой/визардом, + **новые секреты** на каждую
инсталляцию + **инструкцию `docs/deployment/BUNDLED_SETUP.md`** с требованиями к хосту.
### 1.3. Установленные факты (проверено по репо — не изобретать)
- **Корневой `docker-compose.yml` защищён анти-дрейфом:** ровно 3 сервиса
(`orchestrator`, `orchestrator-watchdog`, `orchestrator-staging` за `profiles: ["staging"]`);
`tests/test_lite_setup_doc.py` (TC-04) проверяет точное множество сервисов и **запрещает**
появление в нём имён/образов с подстроками `plane`/`gitea` → bundle-компоуз обязан быть
**отдельным файлом**, корневой compose не форкается и не расширяется.
- **Кирпичи уже в `main` (переиспользовать, не дублировать):**
- `scripts/gen_secrets.py` (ORCH-101) — криптослучайные webhook-секреты
(`ORCH_PLANE_WEBHOOK_SECRET`/`ORCH_GITEA_WEBHOOK_SECRET`), печать по умолчанию,
`--write` отказывает при существующем `.env`, `--force` — перезапись; exit 0/2.
- `scripts/onboard_project.py` (ORCH-009) — `plan` (GET-only) / `apply` (идемпотентный ensure,
без delete) / `verify`: Plane-проект + **22 статуса** (read-only импорт
`plane_sync._PLANE_NAME_TO_KEY`, fail-closed имена `Confirm Deploy`/`STOP`) + лейблы
`autoApprove`/`autoDeploy`/`Bug`; Gitea-репо + per-repo webhook (`push`/`pull_request`/`status`,
ОДИН глобальный `ORCH_GITEA_WEBHOOK_SECRET`); недоступное в Plane CE API → `manual-step`
(fail-safe); exit 0/2/1.
- `docs/operations/REPLICATION.md` (ORCH-101) — карта env (§2), чек-лист секретов (§3),
**smoke §4** (шаги 06 с PASS/FAIL: config-резолв → `/health``/queue`+`/metrics`
onboard plan/apply/verify → тестовая задача → артефакты `0104` → опц. до `done`); §1 —
таблица границ, где Type B помечен «отдельная задача».
- `docs/deployment/LITE_SETUP.md` (ORCH-102) — канон тиражной инструкции: 13 нормативных
разделов, каждый шаг = fenced-команда + явная «Проверка:» PASS/FAIL, хост-специфика только
плейсхолдерами; канон не форкается — общие шаги ссылками.
- `.env.example` — канон 100% ключей орка; `.env.watchdog.example` — канон watchdog
(key-set-sync тестом, D5 ORCH-102).
- **Хост-параметризация завершена (ORCH-101):** платформа разворачивается без правки кода —
только env (`${VAR:-default}`-интерполяция compose, `ARG APP_*` Dockerfile); анти-регресс
`tests/test_no_host_hardcodes.py` (FORBIDDEN-литералы: IP/`/home/slin`/`mva154`/`duckdns`).
- **Claude CLI НЕ запечён в образ орка:** монтируется с хоста
(`ORCH_HOST_CLAUDE_CODE_DIR`/`ORCH_HOST_NODE_BIN`/`ORCH_HOST_CLAUDE_DIR`/`ORCH_HOST_CLAUDE_JSON`).
«Агенты» в комплекте = рантайм-обвязка запуска; **инсталляция Claude CLI и LLM-ключ — внешнее
предусловие хоста заказчика** (как Lite §7), bundle их не содержит и не генерирует.
- **Нормативы тиражной Gitea:** branch protection на `main` НЕ включать (D10 ORCH-009 / INV-4 —
мерж только через Gitea PR-merge API); pre-receive не вводится.
- **Plane CE self-hosted ≈ 14 контейнеров** (web/admin/space/api/worker/beat/live/migrator +
postgres/redis/mq/minio/proxy) — ресурсоёмко; часть первичной инициализации в CE недоступна
по API → честные ручные чекпоинты (паттерн `manual-step` ORCH-009).
---
## 2. Объём (scope)
### 2.1. В объёме
- **Bundle-compose** — отдельный compose-комплект всего стека: орк + watchdog + Gitea +
Plane-стек (~14 контейнеров); пиннинг версий; чистые именованные тома; согласованная
сетевая достижимость (вебхуки в обе стороны).
- **Bootstrap-скрипт** — один запуск (команда/визард): поднять всё → дождаться
готовности/миграций → инициализация Gitea (админ/токен) → инициализация Plane
(instance/workspace/API-токен; CE-ограничения → явные manual-step чекпоинты) →
онбординг sandbox-проекта (22 статуса/3 лейбла/репо/вебхуки — через `onboard_project.py`) →
git-доступ агентов → сборка `.env`/`.env.watchdog` орка → health → smoke-подсказка.
- **Инициализация секретов** — генерация НОВЫХ на каждую инсталляцию (reuse `gen_secrets.py` +
bundle-внутренние креды: пароли БД/брокера/хранилища Plane, админ Gitea); дефолтных паролей
в репо нет.
- **`docs/deployment/BUNDLED_SETUP.md`** — инструкция запуска bundle по канону LITE_SETUP,
включая **требования к хосту (RAM/диск/CPU/порты)**.
- **Структурные анти-дрейф тесты** (без docker/сети/LLM в CI) + полный зелёный pytest + CHANGELOG.
- Отметка Type B в `docs/operations/REPLICATION.md` §1 (границы трёх задач эпика).
### 2.2. Вне объёма (явно, не делать)
- Изменения рантайма: `src/**`, корневой `docker-compose.yml`, `Dockerfile`, `.gitea/workflows/`,
`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схема БД — байт-в-байт.
- Перенос наших задач/данных/секретов (stateless — решение Владельца 10.06).
- Автоматическая установка Claude CLI / выдача LLM-ключей / создание Telegram-ботов —
внешние предусловия заказчика (документируются, не автоматизируются).
- HTTPS/домены/публичный reverse-proxy заказчика — за рамками bundle (документируется
как ручной шаг при необходимости).
- Процедура обновления (upgrade) развёрнутого bundle; миграция Lite→Bundled; кластерные/
multi-host топологии; мультитенантность (D5.6) и горизонтальный воркер-пул (D5.4).
- Какая-либо активация bundle на НАШЕМ боевом хосте.
---
## 3. Заинтересованные стороны
- **Владелец (Слава)** — раздаёт платформу заказчикам на тест; принимает результат.
- **Оператор заказчика** — целевой читатель BUNDLED_SETUP.md: чистый Linux-хост,
docker+compose, без знания внутренностей платформы.
- **Self-hosting прод** (`orchestrator`, общий для всех проектов) — не должен быть затронут:
задача — артефакты репо (compose/скрипт/доки/тесты), активируемые только явным запуском
на ЦЕЛЕВОМ хосте.
---
## 4. Бизнес-требования (BR)
| ID | Требование | Связь |
|----|------------|-------|
| BR-1 | Единый bundle-compose (отдельный файл) поднимает ВЕСЬ стек одной командой: орк, watchdog, Gitea, Plane-стек. Корневой `docker-compose.yml` не форкается и не меняется. | AC-1, AC-6, FR-1 |
| BR-2 | Bootstrap-скрипт ОДНИМ запуском (команда/визард) доводит свежеподнятый стек до рабочего состояния: готовность/миграции → init Gitea → init Plane → онбординг sandbox-проекта → git-доступ агентов → конфиг орка → health. Шаги, физически недоступные через Plane CE API, оформляются явными интерактивными manual-step чекпоинтами (fail-safe, паттерн ORCH-009) — без молчаливых пропусков. | AC-1, FR-2 |
| BR-3 | После bootstrap smoke проходит: тестовый проект создан, тестовая задача доезжает минимум до артефактов `0104` в ветке (минимальный сигнал REPLICATION §4 шаг 5); расширенно — до `done`. Вебхуки работают в ОБЕ стороны (Plane→орк, Gitea→орк, орк→Plane/Gitea API). | AC-2, FR-2/FR-6 |
| BR-4 | Stateless: каждая инсталляция стартует с чистых томов/БД (Plane, Gitea, орк) и НОВЫХ секретов (`gen_secrets.py` + bundle-внутренние креды). Боевые данные/секреты не используются ни на одном шаге; в репо нет ни одного реального секрета/дефолтного пароля. | AC-3, FR-3 |
| BR-5 | `docs/deployment/BUNDLED_SETUP.md` написан по канону LITE_SETUP (fenced-команды + «Проверка:» PASS/FAIL, плейсхолдеры вместо хост-специфики, канон не форкается — общие шаги ссылками на LITE_SETUP/ONBOARDING/REPLICATION) и фиксирует требования к хосту: RAM/диск/CPU/занимаемые порты (Plane ~14 контейнеров — ресурсоёмко). | AC-4, FR-4 |
| BR-6 | Переиспользование кирпичей без дублирования: секреты — `gen_secrets.py`; статусы/лейблы/репо/вебхуки — `onboard_project.py` (22 статуса — из `plane_sync._PLANE_NAME_TO_KEY`, нулевой дрейф); smoke — шаги REPLICATION §4. Bootstrap не реализует собственную копию этих канонов. | FR-2/FR-3, AC-7 |
| BR-7 | Идемпотентность/fail-safe: повторный запуск bootstrap безопасен (ensure/skip, без delete-операций); запуск на «грязном» хосте (существующие тома/занятые порты/нехватка ресурсов) → явный отказ preflight с понятной подсказкой, а не молчаливое переиспользование чужого состояния. | FR-2, AC-8 |
| BR-8 | Наш прод не затрагивается: вся задача — вне рантайма и вне конвейера; kill-switch не требуется (активация — только явный запуск человеком на целевом хосте, паттерн ORCH-009). | NFR-1/NFR-2, AC-6 |
| BR-9 | Анти-дрейф: структурные тесты держат bundle-канон (compose-структура, док-канон, env-ключи, FORBIDDEN-литералы, секрет-эвристика, кросс-ссылки); существующие `test_lite_setup_doc.py`/`test_no_host_hardcodes.py` остаются зелёными; полный `pytest tests/ -q` зелёный; CHANGELOG обновлён. | AC-5, AC-6, AC-7, FR-5 |
---
## 5. Нефункциональные требования (NFR)
| ID | Требование |
|----|------------|
| NFR-1 | **Рантайм/конвейер байт-в-байт:** `src/**`, корневой `docker-compose.yml`, `Dockerfile`, `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`, machine-verdict ключи, схема БД орка — не тронуты. Задача — docs+scripts+compose-bundle+tests. |
| NFR-2 | **Self-hosting безопасность:** ни один артефакт задачи не рестартит/не деплоит/не конфигурирует наш прод-контейнер; bundle-артефакты в нашем контуре инертны (никто их не исполняет). |
| NFR-3 | **Секрет-гигиена:** в репо не попадают реальные секреты, высокоэнтропийные литералы и хост-литералы (FORBIDDEN-скан `test_no_host_hardcodes.py` распространяется на новые артефакты); bootstrap не печатает секреты в лог; сгенерированные файлы конфигов — только на целевом хосте, в `.gitignore`. |
| NFR-4 | **Переносимость:** bundle не зависит от нашей инфраструктуры; вся хост-специфика — переменные/плейсхолдеры; целевая платформа — одиночный Linux x86_64 хост с docker+compose. |
| NFR-5 | **Норматив сопровождения** (зеркало NFR-5 ORCH-102): изменение шагов тиража в будущих задачах → обновление `BUNDLED_SETUP.md` в том же PR. |
| NFR-6 | **Воспроизводимость:** версии образов Gitea/Plane-стека зафиксированы (пиннинг тегов/digest, не `latest`); состав bundle детерминирован. |
| NFR-7 | **Без новых тяжёлых зависимостей:** bootstrap — в духе существующих скриптов (stdlib-инструментарий `gen_secrets.py`/`onboard_project.py`); точный стек (bash/python) — решение архитектора. |
---
## 6. Допущения и ограничения
- Целевой хост: чистый одиночный Linux x86_64 с установленными docker + docker compose;
оператор имеет sudo. Прочие ОС — вне целевой платформы (best-effort).
- Ресурсы: Plane-стек ресурсоёмок; ориентир для проверки — **не менее 4 vCPU / 8 GB RAM /
40 GB диска** (финальные минимумы УТОЧНЯЮТСЯ при реализации замером на тестовом
развёртывании и фиксируются в BUNDLED_SETUP.md — см. AC-4; цифры выше — гипотеза, не факт).
- Внешние предусловия заказчика (bundle не поставляет): инсталляция/аутентификация Claude CLI
+ LLM-доступ Anthropic; Telegram-боты (трекер + watchdog) — опциональны, их отсутствие
деградирует только нотификации (never-raise), не конвейер.
- Часть инициализации Plane CE недоступна по API (instance-setup/workspace/API-токен) —
допускаются документированные интерактивные шаги внутри визарда; «одной командой» означает
«один запуск bootstrap с явными чекпоинтами», а не «ноль действий человека».
- Версии upstream-образов (Plane CE/Gitea) фиксируются на момент реализации; их обновление —
отдельные будущие задачи (NFR-6).
---
## 7. Критерии успеха (резюме; детали — 03-acceptance-criteria.md)
Пять AC из постановки Владельца (сохранены 1:1 как AC-1…AC-5) + производные проверяемые:
- AC-1 единый bundle-compose поднимает ВЕСЬ стек; bootstrap доводит до рабочего состояния
одной командой/визардом.
- AC-2 после bootstrap smoke проходит (тестовый проект + задача доезжает).
- AC-3 stateless (чистые Plane/Gitea/БД, новые секреты).
- AC-4 BUNDLED_SETUP.md + требования к хосту (RAM/диск) задокументированы.
- AC-5 pytest зелёный; CHANGELOG.
- AC-6 корневой compose/рантайм не тронуты (анти-дрейф зелёный); AC-7 нулевой дрейф канонов
(22 статуса/лейблы/секреты — через существующие кирпичи); AC-8 идемпотентность/fail-safe
bootstrap; AC-9 секрет-гигиена новых артефактов.
---
## 8. Риски (детали — 10-tech-risks.md, заполняет архитектор)
- **R-1 Ресурсоёмкость Plane:** ~14 контейнеров → OOM/медленный старт на слабом хосте;
смягчение — preflight-проверка ресурсов + честные требования в доке (AC-4).
- **R-2 Дыры Plane CE API:** первичная инициализация частично UI-only → ручные чекпоинты;
риск — UX «одной команды» размывается; смягчение — явные manual-step с проверкой результата
(паттерн ORCH-009), минимизация числа ручных шагов.
- **R-3 Дрейф upstream-образов:** «плавающие» теги ломают воспроизводимость → пиннинг (NFR-6).
- **R-4 Сетевая достижимость вебхуков:** орк (host network) ⟷ Plane/Gitea (bridge-сеть bundle)
— двунаправленные URL должны быть согласованы bootstrap'ом; ошибка = «задача не появилась»
(труднодиагностируемо); смягчение — smoke проверяет оба направления.
- **R-5 Соблазн форкнуть корневой compose** (анти-дрейф TC-04 `test_lite_setup_doc.py` упадёт)
→ bundle строго отдельным файлом.
- **R-6 Утечка секретов в логи/репо** при генерации bundle-кред → секрет-эвристика в тестах,
запрет печати секретов (NFR-3).

View File

@@ -0,0 +1,232 @@
---
work_item: ORCH-103
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-11
model_used: claude-opus-4-8
---
# 02 — ТЗ (TRZ): ORCH-103 — ORCH-10b Bundled-тираж: весь стек одним комплектом + bootstrap-скрипт
Work Item: **ORCH-103** · Repo: **orchestrator** · Стадия: analysis
> ТЗ фиксирует **что** должно измениться и **где** (артефакты/контракты/границы). **Как**
> (расположение bundle-каталога, состав сервисов Plane-стека, язык/режимы bootstrap, механизм
> сетевой связности) — решает архитектор в `06-adr/`. Архитектурные решения здесь не принимаются.
---
## 1. Сводка изменения
Добавить в репо **Bundled-комплект тиража (Type B эпика ORCH-10)**: (1) отдельный
**bundle-compose** всего стека (орк + watchdog + Gitea + Plane-стек ~14 контейнеров),
(2) **bootstrap-скрипт**, доводящий свежеподнятый стек до рабочего конвейера одним запуском
(с явными manual-step чекпоинтами там, где Plane CE API не позволяет автоматизацию),
(3) **генерацию новых секретов** на инсталляцию (reuse `gen_secrets.py` + bundle-внутренние
креды), (4) инструкцию **`docs/deployment/BUNDLED_SETUP.md`** с требованиями к хосту,
(5) **структурные анти-дрейф тесты**. Всё — вне рантайма и вне конвейера: `src/**`, корневой
`docker-compose.yml`, `Dockerfile`, `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД — байт-в-байт
(паттерн ORCH-009/ORCH-102). Kill-switch не требуется: активация — только явный запуск
оператором на целевом хосте.
---
## 2. Задействованные модули / пути
| Путь | Действие | Назначение |
|------|----------|------------|
| `deploy/bundled/docker-compose.yml` *(рабочая гипотеза; финальное расположение/имя — ADR; далее «bundle-compose»)* | **создать** | Compose-комплект всего стека (FR-1) |
| `deploy/bundled/.env.bundled.example` *(имя — ADR; далее «bundle-конфиг-канон»)* | **создать** | Канон 100% переменных bundle (порты/версии/плейсхолдеры кред), паттерн `.env.watchdog.example` |
| `scripts/bootstrap_bundle.py` *(имя/язык — ADR)* | **создать** | Bootstrap-скрипт (FR-2) |
| `docs/deployment/BUNDLED_SETUP.md` | **создать** | Golden source инструкции Bundled-тиража (FR-4) |
| `docs/operations/REPLICATION.md` | **изменить (точечно)** | §1: строка Type B → ✅ ORCH-103 + ссылка на BUNDLED_SETUP.md (FR-6) |
| `tests/test_bundle_compose.py` | **создать** | Структура bundle-compose + изоляция корневого compose (FR-5) |
| `tests/test_bundled_setup_doc.py` | **создать** | Канон дока, env-ключи, FORBIDDEN/секрет-эвристика, кросс-рефы (FR-5) |
| `tests/test_bootstrap_script.py` | **создать** | Структурные/unit-ассерты bootstrap (FR-5) |
| `CHANGELOG.md` | **изменить** | Запись `feat: ORCH-103` |
| `.gitignore` | **изменить (при необходимости)** | Сгенерированные на хосте конфиги bundle не коммитятся (NFR-3) |
| **НЕ трогать:** `src/**`, корневой `docker-compose.yml`, `Dockerfile`, `.gitea/workflows/**`, `.env.example` (кроме явно обоснованного в ADR аддитива), `onboarding/**`, промпты `.openclaw/agents/**` | — | NFR-1; анти-дрейф `test_lite_setup_doc.py` (TC-04: ровно 3 сервиса, нет `plane*`/`gitea*`) и `test_no_host_hardcodes.py` остаются зелёными |
---
## 3. Функциональные требования
### FR-1 — Bundle-compose всего стека (BR-1)
- **Отдельный файл**, корневой `docker-compose.yml` не изменяется (жёсткое ограничение:
анти-дрейф TC-04 `tests/test_lite_setup_doc.py` проверяет точное множество сервисов корневого
compose и запрещает подстроки `plane`/`gitea` в именах сервисов/образов/контейнеров).
- **Состав стека:** `orchestrator` (образ собирается из существующего корневого `Dockerfile`
без его правки), `orchestrator-watchdog` (существующий `watchdog/Dockerfile`), Gitea
(+ её хранилище), полный Plane CE-стек (~14 контейнеров: web/admin/space/api/worker/beat/
live/migrator + postgres/redis/mq/minio/proxy — точный состав и версии пиннит архитектор по
upstream-référence). Staging-контур орка (8501) — НЕ в дефолтном `up` (вне скоупа заказчика;
включать ли за профилем — ADR).
- **Пиннинг версий** всех сторонних образов (тег или digest; не `latest`) — NFR-6.
- **Тома:** только именованные/каталожные тома bundle (узнаваемый префикс); чистый первый старт;
пересечений с томами/путями нашего прод-контура нет.
- **Сеть и достижимость (двунаправленно):** (a) Plane→орк и Gitea→орк webhooks доставляются;
(b) орк→Plane API и орк→Gitea API доступны; (c) git push/fetch агентов в Gitea работает.
Механизм (bridge-сеть + публикация портов, `extra_hosts: host-gateway`, host-network — что
выбрать) — ADR; ТЗ фиксирует только инвариант достижимости, проверяемый smoke (FR-6).
- **Порты:** карта портов по умолчанию задокументирована (BUNDLED_SETUP «Требования к хосту»);
порты конфигурируемы через bundle-конфиг; конфликт порта → отказ preflight bootstrap (FR-2),
не молчаливый сбой. Дефолт порта орка — существующий 8500 (`ORCH_DEPLOY_PROD_TARGET_PORT`).
- **Конфиг-канон:** все параметры bundle (порты/версии/пути/плейсхолдеры кред) — в
bundle-конфиг-каноне; key-set синхронизируется структурным тестом (паттерн key-sync
`.env.watchdog.example`, D5 ORCH-102). Ключи орка НЕ дублируются — `.env` орка собирается
bootstrap'ом из существующего канона `.env.example`.
### FR-2 — Bootstrap-скрипт: один запуск до рабочего состояния (BR-2, BR-6, BR-7)
Последовательность (нумерация — норматив поведения; механика шагов — ADR):
1. **Preflight (fail-fast, до любых мутаций):** docker+compose присутствуют; свободные
RAM/диск ≥ задокументированных минимумов; целевые порты свободны; тома bundle отсутствуют
(чистый хост) — иначе явный отказ с подсказкой (BR-7); наличие Claude CLI/кред — warning
(не блокер: конвейер без LLM не поедет, но стек поднимется).
2. **Секреты (FR-3):** генерация полного набора НОВЫХ секретов инсталляции.
3. **Up:** подъём bundle-compose; ожидание готовности каждого сервиса (healthcheck/готовность
БД/завершение миграций Plane и Gitea) с таймаутами и внятной диагностикой.
4. **Init Gitea:** административная учётка + API-токен (через официальные механизмы Gitea —
CLI/env/API; конкретика — ADR); branch protection НЕ настраивается (норматив D10 ORCH-009).
5. **Init Plane:** instance-setup/workspace/API-токен. Всё, что недоступно в CE по API, —
**интерактивный manual-step чекпоинт**: скрипт печатает точную инструкцию (URL/что нажать/
что ввести), ждёт подтверждения, **проверяет результат** (например, валидность введённого
API-токена запросом) и только тогда продолжает (fail-safe; молчаливый пропуск запрещён).
6. **Онбординг sandbox-проекта:** вызов `scripts/onboard_project.py apply` + `verify`
(22 статуса из `plane_sync._PLANE_NAME_TO_KEY`, лейблы `autoApprove`/`autoDeploy`/`Bug`,
Gitea-репо, per-repo webhook под глобальным секретом). Собственная реализация этих шагов
в bootstrap **запрещена** (BR-6, нулевой дрейф канона).
7. **Git-доступ агентов:** обеспечить push/fetch созданного репо из контейнера орка
(ssh-ключ + регистрация в Gitea ИЛИ токен-remote — механизм ADR); клон репо в repos-каталог
орка (`ORCH_HOST_REPOS_DIR`).
8. **Конфиг орка:** собрать `.env` (на базе `.env.example`: URL'ы Plane/Gitea bundle-инсталляции,
токены, webhook-секреты, `ORCH_PROJECTS_JSON` из вывода onboard) и `.env.watchdog`
(из `.env.watchdog.example`); файлы остаются только на целевом хосте.
9. **Health + итог:** `GET /health`, `GET /queue`, `GET /metrics` зелёные; финальная сводка
PASS/FAIL по всем шагам + следующая команда оператора (smoke FR-6).
Требования к скрипту:
- **Идемпотентность:** повторный запуск на уже-инициализированном bundle безопасен
(ensure/skip-семантика, как `onboard_project.py apply`); никаких delete-операций.
- **Exit-коды:** `0` — успех; `2` — остановка на manual-step/незавершённое предусловие;
`1` — ошибка (паттерн `onboard_project.py`).
- **Логи без секретов** (NFR-3): значения кред не печатаются (только имена ключей/пути файлов).
- **Никогда не адресует наш прод:** в скрипте нет боевых хостов/путей (FORBIDDEN-скан),
работает только с локальным docker целевого хоста.
- Желателен режим `plan` (печать шагов без мутаций, паттерн ORCH-009) — финально ADR.
### FR-3 — Инициализация секретов: новые на каждую инсталляцию (BR-4)
- **Webhook-секреты орка** — строго через существующий `scripts/gen_secrets.py`
(не реализовывать заново).
- **Bundle-внутренние креды** (генерирует bootstrap, криптослучайно, stdlib `secrets`):
пароли postgres/redis*/mq/minio Plane-стека, секрет-ключи Plane, админ-пароль и API-токен
Gitea. В репо — только плейсхолдеры в bundle-конфиг-каноне; **ни одного дефолтного пароля**.
- **Внешние секреты заказчика** (не генерятся, чек-лист в доке): Anthropic/Claude CLI доступ,
Telegram-токены (опционально), `ORCH_PLANE_API_TOKEN` (если выдаётся вручную на manual-step).
- Сгенерированные файлы: только на целевом хосте, права `600`, в `.gitignore`; повторный
запуск НЕ перетирает существующие секреты без явного флага (паттерн `--force` gen_secrets).
### FR-4 — `docs/deployment/BUNDLED_SETUP.md` (BR-5)
- **Канон LITE_SETUP** (ORCH-102): нормативные разделы в порядке маршрута оператора; каждый
исполняемый шаг = fenced-команда + явная «Проверка:» с PASS/FAIL; хост-специфика — только
плейсхолдеры; запрещены FORBIDDEN-литералы и реальные секреты (структурный тест).
- **Обязательные разделы** (минимум; точные заголовки — автор дока, проверяемость — тест):
(1) рамка Bundled (что входит/что НЕ входит: Claude CLI, Telegram, HTTPS; границы vs Lite);
(2) **требования к хосту** — RAM/диск/CPU/порты, явно «Plane ≈ 14 контейнеров», финальные
цифры — по замеру на тестовом развёртывании; (3) предусловия (docker/compose/sudo);
(4) получение кода; (5) секреты; (6) запуск bundle-compose; (7) bootstrap (включая перечень
manual-step чекпоинтов Plane); (8) LLM/Claude CLI (ссылкой на канон LITE_SETUP §7);
(9) Telegram (ссылкой на LITE_SETUP §8); (10) онбординг следующих проектов
(ссылкой на ONBOARDING.md); (11) smoke (шаги REPLICATION §4); (12) stateless-проверка;
(13) остановка/полный сброс инсталляции; (14) траблшутинг (минимум: webhook не доходит,
не хватает RAM/OOM, порт занят, claude не найден, Plane-миграции не завершились).
- **Канон не форкается:** общие с Lite шаги — ссылками (LITE_SETUP §5§8, ONBOARDING §1,
REPLICATION §2§4), не копипастой; fail-closed имена `Confirm Deploy`/`STOP` и «22 статуса» —
согласованы с `plane_sync._PLANE_NAME_TO_KEY` (число — сверкой импорта в тесте, не литералом).
### FR-5 — Структурные анти-дрейф тесты (BR-9)
Все тесты — без docker/сети/LLM/subprocess-мутаций (CI-безопасные; паттерн
`test_lite_setup_doc.py`):
- **bundle-compose:** файл существует, валидный YAML; обязательные сервисы присутствуют
(`orchestrator`, `orchestrator-watchdog`, Gitea, Plane-стек — по списку из ADR); все
сторонние образы пиннованы (нет `:latest`/безтегового образа); корневой
`docker-compose.yml` НЕ изменён (множество сервисов == текущему эталону);
- **док:** BUNDLED_SETUP.md существует, несёт обязательные разделы (включая «Требования к
хосту»), каждый env-ключ из дока существует в канонах (`.env.example` bundle-конфиг-канон),
кросс-ссылки на LITE_SETUP/ONBOARDING/REPLICATION присутствуют;
- **гигиена:** FORBIDDEN-литералы (импорт списка из `test_no_host_hardcodes.py`) отсутствуют
в bundle-compose/доке/bootstrap; секрет-эвристика (hex ≥32 / alnum ≥40, паттерн D8 ORCH-102)
по новым файлам;
- **bootstrap:** скрипт существует; структурно ссылается на `gen_secrets`/`onboard_project`
(не дублирует канон); не содержит delete-операций уровня `docker volume rm`/`rm -rf` вне
явного отдельного «сброс»-режима; чистые функции (preflight-решение, сборка плана шагов,
рендер `.env`) покрыты unit-тестами;
- **кросс-рефы:** REPLICATION.md §1 несёт отметку Type B → BUNDLED_SETUP.md; CHANGELOG
содержит `ORCH-103`.
### FR-6 — Smoke и наблюдаемость результата (BR-3)
- Smoke Bundled = шаги REPLICATION §4 (06) поверх bundle-инсталляции, зафиксированные в
BUNDLED_SETUP §smoke: config-резолв → `/health``/queue`+`/metrics` → onboard verify →
тестовая задача (Plane issue → «To Analyse» → job в очереди) → **минимальный сигнал:
артефакты `0104` в ветке** → опционально полный цикл до `done`.
- Прохождение фиксируется оператором по PASS/FAIL каждого шага; это ручная приёмка AC-2
(e2e в CI не гоняется — нет docker/LLM).
---
## 4. Изменения API
**Нет.** Эндпоинты орка не добавляются/не меняются; bundle использует существующие
`/health`, `/queue`, `/metrics`, вебхуки `/webhook/plane`, `/webhook/gitea`.
## 5. Изменения схемы БД
**Нет** (схема БД орка не тронута). БД Plane/Gitea внутри bundle — их собственные, на чистых
томах инсталляции; к схеме орка отношения не имеют.
## 6. Требования к новым/изменённым QG checks
**Нет.** Реестр `QG_CHECKS`/`check_*`/`STAGE_TRANSITIONS` — байт-в-байт. Bundled-тираж — это
артефакты дистрибуции, а не гейты конвейера.
---
## 7. Совместимость / регресс
- **Kill-switch не требуется** (паттерн ORCH-009): артефакты вне рантайма; в нашем контуре
ничего их не исполняет; активация — явный запуск оператором на целевом хосте.
- **Нулевая регрессия:** корневой compose/`Dockerfile`/`src/**` не изменены ⇒ наш прод,
staging-контур и enduro-trails не затронуты по построению; существующие анти-дрейф тесты
(`test_lite_setup_doc.py`, `test_no_host_hardcodes.py`, канон-тесты ORCH-009) остаются
зелёными без правки их ассертов.
- **Обратимость:** удаление bundle-каталога/скрипта/дока возвращает репо в текущее состояние;
на целевом хосте полный сброс = задокументированная процедура (FR-4 §13).
- **Эскалация:** если при реализации выяснится необходимость править `src/**`/корневой compose
(например, недостающая параметризация, не закрытая ORCH-101) — это выход за рамки ТЗ:
остановиться и вернуть задачу с обоснованием (CLAUDE.md правило 4), не «дотачивать молча».
---
## 8. Артефакты pipeline (создать/обновить в ТОМ ЖЕ PR)
- `docs/work-items/ORCH-103/06-adr/ADR-001-<slug>.md` — решения архитектора (см. §9 OQ);
при сквозном значении — зеркало в `docs/architecture/adr/adr-NNNN-<slug>.md`.
- `docs/architecture/README.md` — раздел «Bundled-тираж (ORCH-103)» рядом с 10-common/Lite.
- `CLAUDE.md` — краткий абзац Type B (паттерн абзацев ORCH-101/102).
- `docs/operations/REPLICATION.md` §1 — отметка Type B (FR-6).
- `CHANGELOG.md``feat: ORCH-103 …`.
- При выявлении инфра-предусловий целевого хоста — `07-infra-requirements.md` (архитектор).
---
## 9. Открытые вопросы для архитектора (не блокируют анализ)
- **OQ-1** Расположение/имя bundle-каталога и compose-файла (`deploy/bundled/` vs `bundle/`;
один compose vs include-композиция); судьба staging-контура орка в bundle (исключить vs
профиль).
- **OQ-2** Точный состав/версии Plane CE-стека (по upstream selfhost-référence) и Gitea;
стратегия пиннинга (тег vs digest).
- **OQ-3** Перечень физически автоматизируемых шагов инициализации Plane CE (instance-setup/
workspace/API-токен): что через API/CLI/seed, что — manual-step чекпоинт.
- **OQ-4** Язык и режимы bootstrap (python stdlib vs bash; `plan`/`apply` vs линейный визард);
способ ожидания готовности (healthchecks vs poll).
- **OQ-5** Механизм сетевой связности орк (host network?) ⟷ bundle bridge-сеть: публикация
портов, `host-gateway`, либо весь bundle в host-network — и согласование URL вебхуков.
- **OQ-6** Механизм git-доступа агентов к bundle-Gitea (ssh-ключ vs http-токен) и наполнение
repos-каталога.
- **OQ-7** Делать ли отдельный явный «сброс»-режим (teardown) частью скрипта или только
документированной процедурой в BUNDLED_SETUP §13.

View File

@@ -0,0 +1,164 @@
---
work_item: ORCH-103
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-11
model_used: claude-opus-4-8
---
# 03 — Критерии приёмки (Acceptance Criteria): ORCH-103 — Bundled-тираж: весь стек одним комплектом + bootstrap-скрипт
Work Item: **ORCH-103** · Repo: **orchestrator** · Стадия: analysis
Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL**
(что считается провалом). AC-1…AC-5 — из постановки Владельца (сохранены 1:1 по смыслу);
AC-6…AC-9 — производные обязательные. AC-1/AC-2/AC-3 в части e2e — **ручная приёмка** на
чистом тестовом хосте/VM по BUNDLED_SETUP.md (в CI docker/LLM не гоняются); остальное
проверяется по файлам репозитория и структурным тестам.
---
## AC-1 — Единый bundle поднимает ВЕСЬ стек; bootstrap доводит одной командой/визардом
**Условие:** на чистом Linux-хосте с docker+compose по шагам BUNDLED_SETUP.md: одна команда
`docker compose -f <bundle-compose> … up -d` поднимает все сервисы стека (орк + watchdog +
Gitea + Plane-стек); затем ОДИН запуск bootstrap-скрипта доводит инсталляцию до рабочего
состояния (init Gitea/Plane → онбординг sandbox-проекта → git-доступ агентов → конфиг орка →
health). Интерактивные manual-step чекпоинты допустимы только там, где Plane CE API не
позволяет автоматизацию, каждый — с инструкцией и проверкой результата.
- **PASS:** все контейнеры bundle в состоянии Up/healthy; bootstrap завершается `exit 0`;
`GET /health` орка — 200/ok, `GET /queue` и `GET /metrics` отдают валидный JSON;
`onboard_project.py verify` зелёный (22 статуса, лейблы, репо, webhook); ни одного
НЕдокументированного ручного действия (правка compose/конфигов руками сверх инструкции).
- **FAIL:** хотя бы один сервис не поднялся/в рестарт-цикле; bootstrap падает или завершается
с нерабочим конвейером; для доводки потребовались действия, отсутствующие в BUNDLED_SETUP.md;
manual-step пропускается молча без проверки результата.
---
## AC-2 — После bootstrap smoke проходит (тестовый проект + задача доезжает)
**Условие:** smoke-процедура BUNDLED_SETUP §smoke (шаги REPLICATION.md §4 поверх
bundle-инсталляции): создать issue в sandbox-проекте Plane → перевести в «To Analyse».
- **PASS:** webhook доезжает (job появляется в `GET /queue`); конвейер запускает analyst;
в рабочей ветке Gitea появляются артефакты `01-brd.md`/`02-trz.md`/`03-acceptance-criteria.md`/
`04-test-plan.yaml` (минимальный сигнал — шаг 5 REPLICATION §4); обратное направление
работает (орк пишет статус/коммент в Plane). Опционально-расширенно: задача доводится до
`done` (шаг 6).
- **FAIL:** webhook не доходит (нет job); analyst не стартует; артефакты `0104` не появляются;
орк не может писать в Plane/Gitea API (одностороння связность — R-4).
---
## AC-3 — Stateless: чистые Plane/Gitea/БД, новые секреты
**Условие:** инсталляция стартует с нуля и не содержит ничего нашего.
- **PASS:** все тома bundle созданы заново при первом `up` (чистые БД Plane/Gitea/орка:
`GET /queue` — нулевые счётчики, в Plane/Gitea нет наших задач/репо/пользователей); ВСЕ
секреты инсталляции сгенерированы на месте (`gen_secrets.py` + bundle-креды bootstrap);
в репо нет ни одного реального секрета/дефолтного пароля (структурный тест: секрет-эвристика
+ плейсхолдеры в bundle-конфиг-каноне); боевые данные/секреты/БД не копируются ни одним шагом
инструкции.
- **FAIL:** инструкция/скрипт предлагает перенос наших данных или переиспользование боевых
секретов; в репо обнаружен реальный секрет/дефолтный пароль/высокоэнтропийный литерал;
на свежей инсталляции видны чужие задачи/счётчики.
---
## AC-4 — BUNDLED_SETUP.md + требования к хосту задокументированы
**Условие:** `docs/deployment/BUNDLED_SETUP.md` существует и написан по канону тиражных доков
(ORCH-102).
- **PASS:** док несёт обязательные разделы FR-4 (рамка, **требования к хосту с явными цифрами
RAM/диск/CPU и картой портов**, предусловия, секреты, запуск, bootstrap с перечнем
manual-step, LLM, Telegram, онбординг, smoke, stateless-проверка, остановка/сброс,
траблшутинг); каждый исполняемый шаг = fenced-команда + «Проверка:» PASS/FAIL; явно указано
«Plane ≈ 14 контейнеров — ресурсоёмко»; цифры требований подтверждены замером на тестовом
развёртывании (не «с потолка»); хост-специфика — только плейсхолдеры; общие шаги — ссылками
на LITE_SETUP/ONBOARDING/REPLICATION (без копипасты канона).
- **FAIL:** дока нет/раздел «Требования к хосту» отсутствует или без цифр; шаги без
команд/проверок; FORBIDDEN-литералы (IP/`/home/slin`/`mva154`/`duckdns`) или секреты в
тексте/fenced-блоках; канон LITE_SETUP скопирован вместо ссылок.
---
## AC-5 — pytest зелёный; CHANGELOG
**Условие:** полный регресс и журнал изменений.
- **PASS:** `pytest tests/ -q` — 0 failed (включая существующие анти-дрейф
`test_lite_setup_doc.py`, `test_no_host_hardcodes.py`, канон-тесты ORCH-009 — без правки их
ассертов); `CHANGELOG.md` содержит запись `ORCH-103`.
- **FAIL:** хотя бы один тест красный; существующий анти-дрейф тест «починен» ослаблением
ассертов; CHANGELOG не обновлён.
---
## AC-6 — Корневой compose и рантайм не тронуты
**Условие:** изоляция от боевого контура (NFR-1/NFR-2, BR-1/BR-8).
- **PASS:** `git diff main` НЕ содержит изменений `src/**`, корневого `docker-compose.yml`,
`Dockerfile`, `.gitea/workflows/**`; bundle-compose — отдельный файл; множество сервисов
корневого compose неизменно (`orchestrator`/`orchestrator-watchdog`/`orchestrator-staging`);
ни один артефакт задачи не исполняется в нашем контуре автоматически (нет правок
деплой-хука/CI, нет cron/врезок).
- **FAIL:** любая правка рантайма/корневого compose/Dockerfile; сервисы `plane*`/`gitea*`
добавлены в корневой compose; артефакт bundle задействован в нашем прод/staging-контуре.
---
## AC-7 — Нулевой дрейф канонов: кирпичи переиспользованы
**Условие:** BR-6 — единственный источник истины для статусов/лейблов/секретов/smoke.
- **PASS:** bootstrap вызывает `scripts/gen_secrets.py` (webhook-секреты) и
`scripts/onboard_project.py` (статусы/лейблы/репо/вебхуки) — структурный тест подтверждает
ссылки; собственного списка статусов/лейблов в bundle-артефактах нет (упоминание числа
статусов в доке сверяется импортом `plane_sync._PLANE_NAME_TO_KEY` в тесте, не литералом);
smoke-раздел ссылается на REPLICATION §4.
- **FAIL:** bootstrap/док несут собственную копию канона (свой список статусов, свой генератор
webhook-секретов, свой smoke-чеклист с нуля) — дрейф при будущих изменениях канона.
---
## AC-8 — Идемпотентность и fail-safe bootstrap
**Условие:** BR-7 — повторный запуск и грязный хост.
- **PASS:** повторный запуск bootstrap на уже-инициализированном bundle завершается успешно
(ensure/skip, без дублей и без разрушения состояния); preflight на грязном/непригодном хосте
(существующие тома bundle, занятый порт, нехватка RAM/диска) → явный отказ с понятной
подсказкой ДО любых мутаций; delete-операций нет (teardown — только отдельный
явный режим/процедура, не часть обычного прогона); exit-коды: 0 — успех, 2 — manual-step/
предусловие, 1 — ошибка; секреты в логи не печатаются; повторный запуск не перетирает
существующие секреты без явного флага.
- **FAIL:** повторный запуск ломает/дублирует состояние; bootstrap молча переиспользует чужие
тома или продолжает после провального preflight; обычный прогон удаляет данные; секрет виден
в stdout/логе.
---
## AC-9 — Секрет-гигиена и переносимость новых артефактов
**Условие:** NFR-3/NFR-4/NFR-6 по файлам репо.
- **PASS:** структурные тесты подтверждают: в bundle-compose/доке/скрипте нет
FORBIDDEN-литералов (список — импорт из `test_no_host_hardcodes.py`) и высокоэнтропийных
литералов (hex ≥32 / alnum ≥40); все сторонние образы bundle-compose пиннованы (не `latest`);
все env-ключи, упомянутые в BUNDLED_SETUP.md, существуют в канонах (`.env.example`
bundle-конфиг-канон); сгенерированные на хосте конфиги — в `.gitignore`.
- **FAIL:** найден хост-литерал/секрет; образ без пина; ключ-фантом в доке (нет в канонах);
сгенерированный конфиг коммитится.
---
## Сводная матрица AC ↔ BR/FR
| AC | Покрывает | Способ проверки |
|----|-----------|-----------------|
| AC-1 | BR-1, BR-2 / FR-1, FR-2 | ручной e2e на тестовом хосте + структурные тесты (TC-01..04, TC-08) |
| AC-2 | BR-3 / FR-2, FR-6 | ручной e2e (smoke REPLICATION §4) |
| AC-3 | BR-4 / FR-3 | ручной e2e + структурные тесты (TC-06, TC-09) |
| AC-4 | BR-5 / FR-4 | структурный тест дока (TC-05) + ревью |
| AC-5 | BR-9 / FR-5, FR-6 | `pytest tests/ -q` (TC-12) + CHANGELOG (TC-11) |
| AC-6 | BR-1, BR-8 / NFR-1, NFR-2 | git diff + существующий анти-дрейф (TC-02) |
| AC-7 | BR-6 / FR-2, FR-3 | структурный тест bootstrap/дока (TC-07, TC-10) |
| AC-8 | BR-7 / FR-2 | unit-тесты чистых функций preflight/плана (TC-08) + ручной повторный прогон |
| AC-9 | NFR-3, NFR-4, NFR-6 / FR-1, FR-4 | структурные тесты гигиены (TC-03, TC-06, TC-09) |

View File

@@ -0,0 +1,102 @@
work_item: ORCH-103
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-11
model_used: claude-opus-4-8
title: "Bundled-тираж: bundle-compose + bootstrap + BUNDLED_SETUP.md (структурные анти-дрейф тесты)"
framework: pytest
scope: >
Автоматическое покрытие — СТРУКТУРНЫЕ инварианты артефактов Bundled-тиража
(bundle-compose, bootstrap-скрипт, docs/deployment/BUNDLED_SETUP.md) без docker/сети/LLM
в CI (паттерн tests/test_lite_setup_doc.py). Вне автоматического покрытия — фактический
e2e-подъём стека: он принимается ВРУЧНУЮ по чек-листу BUNDLED_SETUP §smoke
(шаги REPLICATION.md §4) на чистом тестовом хосте/VM — см. notes (AC-1/AC-2/AC-3/AC-8).
notes: >
Ручная приёмка (вне CI): чистый Linux-хост/VM -> docker compose -f <bundle> up -d ->
один запуск bootstrap (manual-step чекпоинты Plane CE допустимы и проверяются) ->
health/queue/metrics зелёные -> onboard verify -> тестовая задача доезжает до артефактов
01-04 (минимальный сигнал), опционально до done; повторный запуск bootstrap безопасен;
тома чистые, секреты новые. Имена модулей tests/test_bundle_compose.py /
tests/test_bundled_setup_doc.py / tests/test_bootstrap_script.py — норматив тест-плана;
имена bundle-каталога/скрипта внутри ассертов следуют ADR-001 архитектора.
Полный регресс tests/ обязан остаться зелёным БЕЗ ослабления ассертов существующих
анти-дрейф тестов (test_lite_setup_doc.py, test_no_host_hardcodes.py, канон ORCH-009).
tests:
# ---------- FR-1 / AC-1: bundle-compose ----------
- id: TC-01
type: unit
description: "Bundle-compose существует и валидно парсится (yaml.safe_load); содержит обязательные сервисы: orchestrator, orchestrator-watchdog, Gitea и Plane-стек (имена — по ADR-001); staging-контур орка не входит в дефолтный up"
module: tests/test_bundle_compose.py
expected: PASS
- id: TC-02
type: unit
description: "Корневой docker-compose.yml НЕ изменён: множество сервисов == {orchestrator, orchestrator-watchdog, orchestrator-staging}; в его сервисах/образах/container_name нет подстрок plane/gitea (зеркало TC-04 test_lite_setup_doc.py — существующий анти-дрейф остаётся зелёным)"
module: tests/test_bundle_compose.py
expected: PASS
- id: TC-03
type: unit
description: "Все сторонние образы bundle-compose пиннованы: ни одного image с тегом latest или без тега/digest (NFR-6, воспроизводимость)"
module: tests/test_bundle_compose.py
expected: PASS
- id: TC-04
type: unit
description: "Изоляция и конфиг-канон bundle: тома — именованные с узнаваемым bundle-префиксом, без bind-путей нашего прод-контура; bundle-конфиг-канон (example-файл) существует, и каждая ${VAR}-интерполяция bundle-compose имеет ключ в каноне (key-set-sync, паттерн .env.watchdog.example)"
module: tests/test_bundle_compose.py
expected: PASS
# ---------- FR-4 / AC-4: BUNDLED_SETUP.md ----------
- id: TC-05
type: unit
description: "docs/deployment/BUNDLED_SETUP.md существует и несёт обязательные разделы FR-4 (включая 'Требования к хосту' с цифрами RAM/диск/CPU, картой портов и упоминанием ~14 контейнеров Plane; bootstrap; smoke; stateless-проверка; остановка/сброс; траблшутинг); исполняемые шаги оформлены fenced-блоками с явной 'Проверка:'"
module: tests/test_bundled_setup_doc.py
expected: PASS
- id: TC-06
type: unit
description: "Гигиена новых артефактов (док + bundle-compose + bootstrap): нет FORBIDDEN-литералов (список — импорт из tests/test_no_host_hardcodes.py) и нет высокоэнтропийных секрет-литералов (hex >=32 / alnum >=40, эвристика D8 ORCH-102)"
module: tests/test_bundled_setup_doc.py
expected: PASS
# ---------- FR-2/FR-3 / AC-7: bootstrap переиспользует кирпичи ----------
- id: TC-07
type: unit
description: "Bootstrap-скрипт существует и структурно переиспользует канон: ссылается на scripts/gen_secrets.py и scripts/onboard_project.py; НЕ несёт собственного списка Plane-статусов/лейблов; в обычном прогоне нет delete-операций (docker volume rm / rm -rf допустимы только в отдельном явном reset-режиме, если введён ADR)"
module: tests/test_bootstrap_script.py
expected: PASS
- id: TC-08
type: unit
description: "Чистые функции bootstrap (preflight/план шагов): грязное состояние (существующие bundle-тома, занятый порт, нехватка RAM/диска) -> отказ с диагностикой ДО мутаций; чистое -> план полного прогона; контракт exit-кодов 0/2/1 (успех / manual-step-остановка / ошибка)"
module: tests/test_bootstrap_script.py
expected: PASS
# ---------- FR-4/FR-5 / AC-9: env-канон и нулевой дрейф ----------
- id: TC-09
type: unit
description: "Каждый env-ключ ORCH_*/WATCHDOG_*, упомянутый в BUNDLED_SETUP.md, существует в .env.example либо в bundle-конфиг-каноне (нет ключей-фантомов); упоминание ЧИСЛА статусов Plane сверяется импортом len(plane_sync._PLANE_NAME_TO_KEY), а не зашитым литералом"
module: tests/test_bundled_setup_doc.py
expected: PASS
- id: TC-10
type: unit
description: "Кросс-ссылки канона: BUNDLED_SETUP.md ссылается на LITE_SETUP.md, ONBOARDING.md и REPLICATION.md (канон не форкается); docs/operations/REPLICATION.md §1 несёт отметку Type B -> BUNDLED_SETUP.md / ORCH-103"
module: tests/test_bundled_setup_doc.py
expected: PASS
# ---------- AC-5: журнал и полный регресс ----------
- id: TC-11
type: unit
description: "CHANGELOG.md содержит запись ORCH-103"
module: tests/test_bundled_setup_doc.py
expected: PASS
- id: TC-12
type: integration
description: "Полный регресс pytest tests/ -q зелёный: новые тесты добавлены, существующие анти-дрейф (test_lite_setup_doc.py, test_no_host_hardcodes.py, канон-тесты ORCH-009) проходят БЕЗ изменения их ассертов"
module: tests/
expected: PASS

View File

@@ -0,0 +1,362 @@
---
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, ≈1314 сервисов по факту 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`)

View File

@@ -0,0 +1,73 @@
---
work_item: ORCH-103
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-11
model_used: claude-opus-4-8
---
# 07 — Инфра-требования: ORCH-103 — Bundled-тираж: весь стек одним комплектом + bootstrap
Work Item: **ORCH-103** · Repo: **orchestrator** · Стадия: architecture
> Вся инфраструктура этой задачи — **ЦЕЛЕВОЙ хост заказчика** (и одноразовый тестовый хост/VM
> приёмки). Инфраструктура НАШЕГО прод-контура (mva154) не затрагивается ни одним пунктом:
> артефакты bundle в нашем контуре инертны (NFR-2, паттерн ORCH-009).
## I-1. Топология / окружения
**Наш контур: N/A** (корневой `docker-compose.yml`, прод 8500, staging 8501 — байт-в-байт).
**Целевой хост bundle (нормативно, ADR-001 D1/D3/D4):**
- Один Linux x86_64 хост, docker + docker compose v2, sudo у оператора. Compose-проект
**`orchestrator-bundle`** (`deploy/bundled/docker-compose.yml`), одна именованная bridge-сеть.
- Состав: `orchestrator` (build из корневого `Dockerfile`), `orchestrator-watchdog`
(build из `watchdog/Dockerfile`), `gitea` (пиннованный `gitea/gitea`), Plane CE-стек —
зеркало upstream selfhost-référence (≈1314 сервисов: web/space/admin/api/worker/beat-worker/
migrator/live + postgres/redis/mq/minio/proxy; точные теги пиннит developer по проверенному
стенду). Staging-контур орка отсутствует.
- **Карта портов (дефолты; конфигурируемы в `deploy/bundled/.env.example`):**
`${BUNDLE_ORCH_PORT:-8500}` — API орка (smoke/health), `${BUNDLE_PLANE_PORT:-8080}` — Plane
proxy (UI), `${BUNDLE_GITEA_HTTP_PORT:-3000}` — Gitea web. Postgres/redis/mq/minio/ssh-Gitea —
**наружу не публикуются**. Машинный трафик (webhooks в обе стороны, API, git, /metrics) —
внутрисетевой сервис-DNS.
- **Хранилище:** состояние Plane/Gitea — именованные тома `orchestrator-bundle_*`; данные орка —
bind `deploy/bundled/data`; репозитории агентов — bind `deploy/bundled/repos` (владелец —
uid оператора = `ORCH_RUN_UID`, инвариант ORCH-040).
- **Ресурсы (предусловие, гипотеза BRD §6 — финальные цифры по замеру на приёмке, AC-4):**
ориентир ≥ 4 vCPU / 8 GB RAM / 40 GB диска; preflight bootstrap проверяет и отказывает до
любых мутаций (BR-7).
## I-2. Переменные окружения / секреты
- **Новый канон:** `deploy/bundled/.env.example` (bundle-инфра: `BUNDLE_PUBLIC_HOST`, карта
портов, реюз `ORCH_RUN_UID/GID`/`ORCH_DOCKER_GID`/`ORCH_AGENT_HOME_DIR`/`ORCH_HOST_CLAUDE_*`,
плейсхолдеры внутренних кред Plane/Gitea по upstream-именам). Live-файлы только на целевом
хосте, права 600: `deploy/bundled/.env`, корневые `.env`/`.env.watchdog` (каноны Lite 1:1).
- **Корневой `.env.example` НЕ меняется** (bundle не вводит новых ключей `Settings`); в
`.gitignore` добавляется `deploy/bundled/repos/` (остальные live-файлы уже покрыты неякорными
`.env`/`data/`/`.env.watchdog`).
- **Секреты (FR-3):** webhook-секреты — `gen_secrets.py`; внутренние креды стека (postgres/
redis/mq/minio/SECRET_KEY Plane, админ Gitea) — stdlib `secrets` в bootstrap; внешние
предусловия заказчика — Claude CLI/Anthropic-доступ (обязателен для конвейера),
Telegram-токены (опциональны). В репо и логах bootstrap секретов нет (NFR-3, тест-эвристика).
## I-3. Деплой / рестарт
- **Наш прод: рестарт НЕ требуется и НЕ выполняется.** Задача — docs+scripts+compose+tests;
мерж в `main` ничего не активирует в нашем контуре (никто не исполняет bundle-артефакты;
kill-switch не нужен — паттерн ORCH-009). Self-hosting инвариант соблюдён по построению.
- На целевом хосте пересоздание контейнеров орка/watchdog после сборки env — штатный шаг
bootstrap (D5 шаг 8); к нашему проду отношения не имеет.
## I-4. CI/CD
- `.gitea/workflows/**`**без изменений**; три новых структурных тест-модуля подхватываются
существующим `pytest tests/ -q` (без docker/сети/LLM — CI-безопасны, TC-12).
## I-5. Разовое предусловие приёмки (человек)
Чистый тестовый Linux-хост/VM (ресурсы I-1) для ручной приёмки AC-1/AC-2/AC-3/AC-8 по
`BUNDLED_SETUP.md` + замер фактических минимумов RAM/диск/CPU для §2 дока (AC-4: цифры «не с
потолка»). На нашем боевом хосте bundle не запускается ни в каком виде (BRD §2.2).

View File

@@ -0,0 +1,42 @@
---
work_item: ORCH-103
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-11
model_used: claude-opus-4-8
---
# 10 — Технические риски: ORCH-103 — Bundled-тираж: весь стек одним комплектом + bootstrap
Work Item: **ORCH-103** · Repo: **orchestrator** · Стадия: architecture
> Информационный (гейтом не парсится). Риски реализации и эксплуатации bundle; решения —
> `06-adr/ADR-001-bundled-stack-compose-and-bootstrap.md` (D1…D11).
## Реестр рисков
| ID | Риск | Вер. | Влия. | Митигейшн |
|----|------|------|-------|-----------|
| TR-1 | **Ресурсоёмкость Plane** (≈1314 контейнеров): OOM/вечные миграции на слабом хосте → «bundle не работает» | Сред. | Выс. | Preflight bootstrap (RAM/диск/CPU до мутаций, BR-7); честные цифры в BUNDLED_SETUP §2 **по замеру** (AC-4); таймауты ожидания готовности с диагностикой «кто не дождался + хвост логов» (D5 ш.3); траблшутинг §14 |
| TR-2 | **Дыры Plane CE API**: instance-setup/workspace/API-токен UI-only → UX «одной команды» размывается; молчаливый пропуск шага ломает всё дальше | Выс. | Сред. | Контракт manual-step (инструкция → подтверждение → **API-верификация результата**, exit 2 без TTY; D5/D7); число ручных шагов минимизировано (Gitea — полностью автоматом, D6); прогрессивная автоматизация разрешена без смены контракта |
| TR-3 | **Дрейф upstream-образов**: «плавающий» тег ломает воспроизводимость; пиннованные версии стареют (CVE/несовместимость) | Сред. | Сред. | Неподвижные теги литералом + тест формы TC-03 (не `latest`/безтеговых); теги фиксируются по фактически проверенному стенду (D3); апгрейд — отдельные задачи (NFR-6, BRD §6) |
| TR-4 | **Сетевая недостижимость вебхуков** (труднодиагностируемое «задача не появилась»): приватные адреса, рассинхрон URL | Сред. | Выс. | Одна bridge-сеть + строго сервис-DNS для машинного трафика (D4); **явный `GITEA__webhook__ALLOWED_HOST_LIST=orchestrator`** (Gitea по умолчанию режет приватные таргеты); URL подставляет bootstrap, не оператор; smoke проверяет ОБА направления (FR-6); траблшутинг §14 |
| TR-5 | **Соблазн форкнуть/расширить корневой compose** (упадёт анти-дрейф ORCH-102 TC-04) | Низ. | Выс. | Bundle строго отдельным файлом `deploy/bundled/` (D1); TC-02 зеркалит заморозку корневого compose; правило эскалации TRZ §7 — если всплывёт незакрытая параметризация `src/**`, остановиться и вернуть задачу, не «дотачивать молча» |
| TR-6 | **Утечка секретов** в репо/логи при генерации bundle-кред | Низ. | Выс. | В гите — только плейсхолдеры (канон D2); bootstrap не печатает значения (имена ключей/пути, D5); права 600; секрет-эвристика hex≥32/alnum≥40 + FORBIDDEN-скан на новых артефактах (TC-06); live-файлы в `.gitignore` |
| TR-7 | **Токен-remote агентов**: токен бот-юзера plaintext в `.git/config` чекаутов; один бот = широкие права | Сред. | Низ. | Осознанный компромисс тестовой инсталляции (D8): порты БД/брокера не публикуются, каталог локальный, права 600; ssh-контур сознательно не вводится (меньше поверхность); зафиксировано в BUNDLED_SETUP §1 «рамка» |
| TR-8 | **Путаница двух `.env`-слоёв** (bundle-инфра `deploy/bundled/.env` vs runtime орка корневой `.env`): ручная правка не того файла | Сред. | Сред. | Bootstrap — **единственный писатель** всех live-файлов (D2); авто-чтение compose из project dir (нет `--env-file`-футгана); шапки-комментарии в канонах перекрёстно ссылаются; key-set-sync тест TC-04 |
| TR-9 | **Хост-python/venv для onboard**: `onboard_project.py` требует venv с `requirements.txt` (канон ONBOARDING) — на голом хосте шаг 6 падает | Сред. | Сред. | Preflight проверяет python3/venv (D5 ш.1); создание venv — идемпотентный ensure-шаг bootstrap; сам bootstrap stdlib-only и от venv не зависит (D5) |
| TR-10 | **Повторный запуск/грязный хост**: bootstrap портит чужое состояние или дублирует своё | Низ. | Выс. | Step-движок check→ensure (skip-семантика, AC-8); детект «грязи» по префиксу `orchestrator-bundle` до мутаций; delete-операций в скрипте нет вообще — teardown только документированной процедурой §13 (D9); unit-тесты чистых функций preflight (TC-08) |
## Сводный вывод
Доминирующий класс — **эксплуатационные риски целевого хоста** (TR-1/TR-2/TR-4): они не
затрагивают наш прод и гасятся честным preflight, контрактом manual-step и smoke в обе стороны.
Рисков для прод-конвейера self-hosting **нет по построению** (NFR-1/NFR-2: рантайм байт-в-байт,
артефакты в нашем контуре инертны, kill-switch не требуется — паттерн ORCH-009; все существующие
анти-дрейф тесты остаются зелёными). Эскалация `arch:major-change` не требуется: новых стадий/
компонентов рантайма/смены БД нет — задача целиком в слое дистрибуции (паттерн ORCH-101/102).
Возврат в анализ не требуется: ТЗ выполнимо без нарушения принципов; единственный заранее
оговорённый стоп-кран — TR-5 (обнаружение незакрытой параметризации `src/**` ⇒ остановка по
TRZ §7). Остаточный риск — **низкий**.

View File

@@ -0,0 +1,160 @@
---
verdict: APPROVED
work_item: ORCH-103
stage: review
author_agent: reviewer
status: approved
created_at: 2026-06-11
model_used: claude-opus-4-8
type: review
work_item_id: ORCH-103
version: 1
---
# Review ORCH-103 — ORCH-10b Bundled-тираж: весь стек одним комплектом + bootstrap-скрипт
> Машинный вердикт читается ТОЛЬКО из `verdict:` во frontmatter.
## Summary
PR закрывает Type B эпика ORCH-10 строго по ТЗ и ADR-001 (D1D11): новый каталог
`deploy/bundled/` (самодостаточный compose 16 сервисов, project name `orchestrator-bundle`,
пиннинг неподвижными тегами, одна bridge-сеть, только человеческие порты, мина
`GITEA__webhook__ALLOWED_HOST_LIST` закрыта), `scripts/bootstrap_bundle.py` (stdlib-only,
`plan`-дефолт/`apply`/`verify`, step-движок check→ensure, exit 0/2/1, ноль delete-операций),
конфиг-канон `deploy/bundled/.env.example` (ни одного дефолтного пароля),
`docs/deployment/BUNDLED_SETUP.md` (14 разделов канона D10) и три содержательных
анти-дрейф тест-модуля. Рантайм байт-в-байт: `git diff main` не содержит `src/**`,
корневого `docker-compose.yml`, `Dockerfile`, `.gitea/workflows/**` (AC-6 подтверждён
diff-stat'ом). Полный регресс: **`pytest tests/ -q` → 1844 passed, 0 failed** (AC-5);
существующие анти-дрейф тесты (`test_lite_setup_doc.py`, `test_no_host_hardcodes.py`,
канон ORCH-009) не правились. Документация обновлена в том же PR по всем точкам §8 ТЗ.
Findings — только P2/P3, блокеров нет → **APPROVED**.
## Оси проверки
### 1. Соответствие ТЗ (02-trz.md, 03-acceptance-criteria.md)
- **FR-1** ✅ — отдельный bundle-compose; состав = ADR D1/D3 (тест
`test_bundle_has_exactly_the_adr_service_set` фиксирует множество); пиннинг всех сторонних
образов литералом (TC-03); тома — именованные с префиксом проекта + bind строго внутри
project dir (TC-04); достижимость в обе стороны — сервис-DNS (D4) + `ALLOWED_HOST_LIST`;
карта портов в доке §2, конфликт порта → отказ preflight; staging-контура нет вовсе.
- **FR-2** ✅ — последовательность шагов 19 ТЗ воспроизведена 1:1 в `APPLY_STEPS`
(тест `test_apply_steps_match_normative_plan` держит соответствие нормативному плану);
идемпотентность — check→ensure/skip (AC-8 покрыт unit'ами resume/«противоречивое
состояние»); exit-контракт 0/2/1 (`test_exit_code_contract`); манифест manual-step честный:
инструкция → подтверждение → API-верификация, без TTY — немедленный exit 2.
- **FR-3** ✅ — webhook-секреты строго субпроцессом `gen_secrets.py` (структурный тест);
bundle-креды — stdlib `secrets` (token_hex ≥16 байт, unit проверяет длину); в репо только
пустые плейсхолдеры (`test_bundle_secrets_in_example_are_empty_placeholders`); права 600;
без перетирания без `--force-secrets` (`test_merge_missing_secrets_never_overwrites_without_force`).
- **FR-4** ✅ — BUNDLED_SETUP.md: все 14 разделов в порядке маршрута, fenced-команды +
«Проверка:»/PASS/FAIL в каждом исполняемом разделе, общие шаги ссылками на
LITE_SETUP §5§8 / ONBOARDING / REPLICATION §4 (форк канона отсутствует), fail-closed имена
`Confirm Deploy`/`STOP` и «22 статуса» — сверкой импорта `plane_sync._PLANE_NAME_TO_KEY`.
- **FR-5** ✅ — три модуля без docker/сети/LLM; FORBIDDEN — импорт из
`test_no_host_hardcodes.py` (один источник истины); секрет-эвристика hex≥32/alnum≥40 с
негативным самочеком (не-evergreen); key-set-sync `${VAR}` ⊆ bundle-канона; заморозка
корневого compose зеркалом ассерта ORCH-102.
- **FR-6** ✅ — smoke = REPLICATION §4 поверх bundle (§11 дока, без форка), минимальный
сигнал «артефакты 0104 в ветке» зафиксирован.
- **AC-матрица:** AC-4…AC-9 в файловой/структурной части — PASS (TC-01…TC-12 зелёные).
AC-1/AC-2/AC-3/AC-8(повторный прогон) в e2e-части — **ручная приёмка** на чистом хосте/VM
по рамке самих AC (в CI docker/LLM не гоняются) — остаётся за стадией приёмки, см. P2-2.
### 2. Соответствие ADR (06-adr/ADR-001, adr-0038)
- D1D11 реализованы без отклонений; сквозной `adr-0038-bundled-replication-canon.md` заведён.
- **Прогрессивная автоматизация webhook (D7)** — единственное место, где реализация глубже
буквы ADR: `step_plane_webhook` регистрирует workspace-webhook прямой записью в Postgres
инсталляции. Сверено: это **не новый канон, а переиспользование** документированного
«пути Б» LITE_SETUP §5.4 (тот же INSERT-контракт, колонка-в-колонку), контракт чекпоинта
сохранён (верификация SELECT'ом; при отказе — fallback на честный manual-step с той же
проверкой), схема стабильна благодаря пину `v0.23.1`. D7 явно разрешает такую замену
manual-step → ensure «без правки ADR». Нарушения нет; в доке §7 чекпоинт 4 описан честно.
- **Трассировка (TRACEABILITY):** правка чужого маркированного блока одна — строка Type B в
`REPLICATION.md` §1 (артефакт ORCH-101); это запланированная точка расширения, прямо
предписанная ТЗ FR-6/§8 — инвариант ORCH-101 не сломан. Остальные изменения аддитивны
(новые файлы / новые секции CLAUDE.md, README архитектуры, CHANGELOG).
- Нормативы предшественников соблюдены: branch protection НЕ настраивается (D10 ORCH-009 /
INV-4 — явно в D6 и §14.6 дока), compose не форкается (adr-0037), «дефолт = боевое» не
нарушен (корневой `.env.example` не тронут).
### 3. Качество кода
- `pytest tests/ -q`: **1844 passed, 0 failed** (66s); новые тесты содержательные — unit'ы
чистых функций покрывают позитив/негатив/resume/противоречивое состояние, эвристики имеют
негативный самочек, ast-скан stdlib-allowlist реально закрыт.
- Бизнес-логика скрипта аккуратная: never-print секретов в stdout (только имена ключей),
маскированный лог при падении clone (токен в URL не утекает в вывод), `_psql` через stdin
(секрет не в argv), fail-fast preflight до любых мутаций, диагностика «кто не дождался +
хвост логов». Не багфикс-трек (feature) — требование регресс-теста-фиксатора BR-4/ORCH-019
неприменимо.
- Замечания P2/P3 — ниже; ни одно не ломает инварианты конвейера и не относится к рантайму
платформы (скрипт исполняется только оператором на целевом хосте).
### 4. Документация — обязательная проверка
Выполнена явно, см. раздел «Документация» ниже. Обновлено всё требуемое в том же PR.
## Findings
### P0 — Blocker
- (нет)
### P1 — Must fix
- (нет)
### P2 — Should fix
- [ ] **P2-1. Секреты в argv субпроцессов** (`scripts/bootstrap_bundle.py`):
`step_init_gitea` передаёт `GITEA_ADMIN_PASSWORD` аргументом `--password` в
`docker compose exec gitea gitea admin user create …`, а `step_agent_git` — токен в
clone-URL аргументом `git clone http://oauth2:<token>@…`. Значения видимы в `ps` хоста на
время исполнения. Формально AC-8 («секрет виден в stdout/логе») не нарушен, но это против
духа NFR-3 (ТЗ FR-2) и непоследовательно с собственной argv-гигиеной скрипта (`_psql`
прогоняет секреты через stdin с явным комментарием «секреты не попадают в argv, NFR-3»).
Рекомендация: для clone — использовать `git -c credential.helper`/`GIT_ASKPASS` либо
дописать компромисс в TR-7 (10-tech-risks) и шапку скрипта; для `gitea admin user create`
альтернатив CLI мало — минимум зафиксировать окно экспозиции в TR-7. Не блокер: разовая
операция оператора на одноарендном целевом хосте, угроза-модель совпадает с уже
зафиксированным компромиссом TR-7.
- [ ] **P2-2. Замер цифр «Требований к хосту» отложен на приёмку** (AC-4): BUNDLED_SETUP §2
декларирует «подтверждаются замером приёмочного развёртывания» — на момент ревью цифры
(8 GB / 40 GB / 4 vCPU) синхронизированы с константами preflight структурным тестом, но
фактический замер ещё не зафиксирован. По рамке 03-acceptance-criteria (e2e — ручная
приёмка вне CI) это допустимо, однако при ручной приёмке AC-1/AC-2 результат замера нужно
зафиксировать (13-test-report / 15-staging-log или правка §2), иначе FAIL-условие AC-4
«цифры с потолка» останется формально незакрытым.
### P3 — Nice to have
- [ ] **P3-1.** `step_plane_webhook`: `slug`/`secret` интерполируются в SQL-строку без
экранирования одинарных кавычек. Секрет — hex от `gen_secrets` (безопасен), slug —
операторский ввод; кавычка в slug уронит INSERT. Риск минимален (ON_ERROR_STOP +
fail-safe fallback на manual-step), но дешёвое `value.replace("'", "''")` сняло бы класс
целиком.
- [ ] **P3-2.** `run_verify`: при одновременном health-FAIL и onboard `exit 2` функция
возвращает `EXIT_MANUAL` (2), маскируя ошибку (ожидался бы приоритет `1`). Поведенческая
мелочь read-only режима.
## Документация
| Артефакт | Статус |
|----------|--------|
| `CLAUDE.md` | ✅ новый раздел «Bundled-тираж (ORCH-103)» (паттерн ORCH-101/102) |
| `docs/architecture/README.md` | ✅ блок Type B — Bundled рядом с 10-common/Lite |
| `CHANGELOG.md` | ✅ запись `feat: ORCH-103` (детальная, D1D11) |
| `docs/operations/REPLICATION.md` §1 | ✅ Type B → ✅ ORCH-103 + ссылка на BUNDLED_SETUP.md |
| `docs/deployment/BUNDLED_SETUP.md` | ✅ создан, 14 разделов канона D10, держится тестом |
| ADR | ✅ work-item `06-adr/ADR-001-…` + сквозной `adr-0038-bundled-replication-canon.md` |
| `07-infra-requirements.md`, `10-tech-risks.md` (TR-1…TR-9) | ✅ заведены архитектором, кросс-рефы из кода сходятся |
| `.gitignore` | ✅ `deploy/bundled/repos/` (NFR-3) |
| `README.md` «Известные ограничения» (ORCH-079) | ✅ проверено явно: PR не закрывает ни один из 3 открытых пунктов (Telegram 48h / intra-repo deps / пакетный автоном) — обновление витрины не требуется |
`src/**` не изменён (PR — deploy/scripts/docs/tests), поэтому P0-правило «`src/` изменён без
документации» неприменимо; документация при этом обновлена полностью.
## Итог
`verdict: APPROVED` — P0/P1 отсутствуют; P2-1/P2-2 и P3 рекомендуется снять follow-up'ом или
при ручной приёмке Bundled-развёртывания (AC-1/AC-2/AC-3/AC-8 e2e-часть).

View File

@@ -0,0 +1,67 @@
---
result: PASS
work_item: ORCH-103
stage: testing
author_agent: tester
status: pass
created_at: 2026-06-11
model_used: claude-opus-4-8
type: test-report
work_item_id: ORCH-103
---
# Test Report — ORCH-103 — ORCH-10b Bundled-тираж: весь стек одним комплектом + bootstrap-скрипт
> Машинный вердикт читается ТОЛЬКО из `result:` во frontmatter (`check_tests_passed`).
## Окружение
- Python: 3.12.13
- pytest: 8.3.3
- Worktree: `/repos/_wt/orchestrator/feature_ORCH-103-orch-10b-bundled-bootstrap/` (ветка задачи, не общий чекаут)
- Дата: 2026-06-11
## Smoke API (read-only)
| Endpoint | Результат |
|----------|-----------|
| `GET /health` | PASS — `{"status":"ok","service":"orchestrator"}` |
| `GET /status` | PASS — активная задача ORCH-103 (`stage: testing`) видна |
| `GET /queue` | PASS — валидный JSON; блок `serial_gate` присутствует (ORCH-088, `orchestrator.active_task = ORCH-103`), блок `auto_labels` присутствует |
Смок-регресс ORCH-088: блок `serial_gate` присутствует в полезной нагрузке `/queue` наряду с `auto_labels`регресса нет.
## Результаты (покрытие каждого TC из 04-test-plan.yaml)
| TC ID | Тип | Описание (кратко) | AC | Модуль | Результат |
|-------|-----|-------------------|----|--------|-----------|
| TC-01 | unit | Bundle-compose существует/парсится; обязательные сервисы (орк/watchdog/Gitea/Plane-стек); staging не в дефолтном up | AC-1 | test_bundle_compose.py | PASS |
| TC-02 | unit | Корневой docker-compose.yml не изменён; нет подстрок plane/gitea (зеркало TC-04 lite) | AC-6 | test_bundle_compose.py | PASS |
| TC-03 | unit | Все сторонние образы пиннованы (нет latest/безтегового) | AC-9 | test_bundle_compose.py | PASS |
| TC-04 | unit | Тома именованные с bundle-префиксом, без bind прод-контура; key-set-sync `${VAR}` ⊆ bundle-канон | AC-9 | test_bundle_compose.py | PASS |
| TC-05 | unit | BUNDLED_SETUP.md: обязательные разделы FR-4 (требования к хосту с цифрами/портами/~14 контейнеров, bootstrap, smoke, stateless, сброс, траблшутинг); fenced + «Проверка:» | AC-4 | test_bundled_setup_doc.py | PASS |
| TC-06 | unit | Гигиена (док+compose+bootstrap): нет FORBIDDEN-литералов (импорт из test_no_host_hardcodes); нет секрет-литералов (hex≥32/alnum≥40) | AC-9 | test_bundled_setup_doc.py | PASS |
| TC-07 | unit | Bootstrap ссылается на gen_secrets.py/onboard_project.py; нет своего списка статусов/лейблов; нет delete-операций в обычном прогоне | AC-7 | test_bootstrap_script.py | PASS |
| TC-08 | unit | Чистые функции bootstrap: грязный хост → отказ ДО мутаций; чистый → план; контракт exit 0/2/1 | AC-8 | test_bootstrap_script.py | PASS |
| TC-09 | unit | Каждый env-ключ из дока есть в `.env.example` bundle-канон; число статусов сверяется импортом `plane_sync._PLANE_NAME_TO_KEY` | AC-9 | test_bundled_setup_doc.py | PASS |
| TC-10 | unit | Кросс-ссылки: BUNDLED_SETUP → LITE_SETUP/ONBOARDING/REPLICATION; REPLICATION §1 отметка Type B → ORCH-103 | AC-7 | test_bundled_setup_doc.py | PASS |
| TC-11 | unit | CHANGELOG.md содержит запись ORCH-103 | AC-5 | test_bundled_setup_doc.py | PASS |
| TC-12 | integration | Полный регресс `pytest tests/ -q` зелёный; существующие анти-дрейф (test_lite_setup_doc.py, test_no_host_hardcodes.py, канон ORCH-009) без правки ассертов | AC-5 | tests/ | PASS |
Все 12 TC выполнены и сопоставлены с критериями приёмки 03-acceptance-criteria.md. Структурная/файловая часть AC-4…AC-9 покрыта зелёными TC-01…TC-12. e2e-часть AC-1/AC-2/AC-3/AC-8 (фактический подъём bundle на чистом хосте/VM) — ручная приёмка вне CI по рамке самих AC (docker/LLM в CI не гоняются), как зафиксировано в test-plan `scope/notes` и ревью P2-2.
## Целевые модули ORCH-103 (детально)
- `tests/test_bundle_compose.py` — 14 passed (TC-01..04)
- `tests/test_bundled_setup_doc.py` — 16 passed (TC-05/06/09/10/11)
- `tests/test_bootstrap_script.py` — 19 passed (TC-07/08)
- Анти-дрейф (без правки ассертов): `tests/test_lite_setup_doc.py` — 26 passed; `tests/test_no_host_hardcodes.py` — 8 passed
- Итого по целевому срезу: **88 passed, 0 failed** (0.91s)
## Вывод pytest (полный регресс)
```
$ python -m pytest tests/ -v --tb=short
...
================== 1844 passed, 1 warning in 70.33s (0:01:10) ==================
```
- 1844 passed, 0 failed, 1 warning (Pydantic V2 deprecation, не относится к задаче).
## Итог
PASS — полный регресс зелёный (1844 passed / 0 failed), smoke read-only (`/health`, `/status`, `/queue` с блоками `serial_gate` и `auto_labels`) в норме, все 12 TC из 04-test-plan.yaml выполнены и сопоставлены с критериями приёмки. Задача переходит на `deploy-staging`.

View File

@@ -0,0 +1,12 @@
---
deploy_status: SUCCESS
work_item: ORCH-103
hook_exit_code: 0
deployed_by: deploy-finalizer
---
# Deploy log — ORCH-036 executable self-deploy
Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`.
Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.

View File

@@ -0,0 +1,34 @@
---
staging_status: SUCCESS
work_item: ORCH-103
stage: deploy-staging
author_agent: deployer
status: success
created_at: 2026-06-11
model_used: claude-opus-4-8
timestamp: 2026-06-10T23:15:16Z
base_url: http://localhost:8501
---
# Staging Gate Log
Staging test suite completed against the live staging environment (`orchestrator-staging`,
port 8501, mode=stub). Ran canonically **inside the container** via `docker exec` (ORCH-048)
so the B6 registry-isolation check reads `.env.staging` from the running instance's own
process-env.
**Result: 8/10 checks PASS — exit code 0 → SUCCESS.**
- REAL failed: none
- All REAL checks (A1A3 smoke, B4B6 access incl. registry isolation, C7C8 E2E
create-issue / trigger-pipeline) are green.
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
VERDICT: SUCCESS (exit 0) — C9a/C9b are the two known sandbox-infra checks (depend on SANDBOX
bot accounts being project members, not on the pipeline) and are tolerated under ORCH-061
waiver tolerance (`staging_infra_tolerance_enabled=True`) since every REAL check passed.
ORCH-103 is a docs+tests + new `deploy/bundled/` and `scripts/bootstrap_bundle.py` change;
`src/**`, the root compose, Dockerfile, `STAGE_TRANSITIONS`/`QG_CHECKS`/DB schema are untouched,
so no runtime behaviour regression is expected — consistent with the all-green REAL checks.

View File

@@ -0,0 +1,7 @@
# Business Request: Подготовка презентации по орку
Work Item ID: ORCH-105
## Description
TBD

View File

@@ -0,0 +1,169 @@
---
work_item: ORCH-105
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-12
model_used: claude-opus-4-8
---
# 01 — BRD (бизнес-требования): ORCH-105 — Подготовка презентации по орку
Work Item: **ORCH-105** · Repo: **orchestrator** · Стадия: analysis
## 1. Бизнес-контекст и проблема
Заказчику нужна **презентация о продукте «Orchestrator» в формате PowerPoint** — чтобы
показывать платформу стейкхолдерам и потенциальным заказчикам. Дополнительно в презентацию
надо добавить **слайд про Lite-установку** (через скрипты-установщики) и **слайды-инструкцию
«как пользоваться орком через Plane»**.
**Установленный факт (опора, не изобретать):** канон презентации в репозитории **уже
существует** — его ввела задача **ORCH-011** (витрина системы `docs/overview/`):
- `docs/overview/presentation.md`**слайдо-источник** (источник истины): 16 слайдов в
формате `## Слайд N: Заголовок` + 36 тезисов + опциональная подпись `> Визуал: …`.
- `scripts/build_presentation.py` — dev-скрипт сборки **редактируемого `.pptx`** в тёмном
дизайне (`python-pptx`, ленивый импорт; чистый парсер `parse_slides` — stdlib-only).
- `tests/test_system_docs.py` — анти-дрейф-контур, который машинно валидирует формат
слайдов, сквозную нумерацию, обязательные биты нарратива и процедуру сборки.
Канон **намеренно** держит инварианты (ORCH-011 D4/D5, NFR-2): сборка идёт **вне рантайма
платформы** (одноразовый dev-venv), `python-pptx` **не входит** в прод-образ, а **собранный
`.pptx`-бинарь в git не коммитится** (`build/` в `.gitignore`).
**Проблема (дельта ORCH-105):** текущая дека рассказывает про продукт в целом, но **не
содержит** (а) выделенного слайда о том, как просто развернуть Lite-вариант скриптами, и
(б) слайдов-инструкции для оператора «как вести задачу через Plane» (запуск, статусы,
человеческие гейты, авто-лейблы, STOP, наблюдение). Задача — **дополнить существующий
слайдо-источник** этим содержанием и убедиться, что `.pptx` собирается с новыми слайдами.
Это **docs-only** доработка контента витрины: код рантайма не меняется.
## 2. Объём (scope)
### В объёме
- **Расширить `docs/overview/presentation.md`:**
- добавить **один выделенный слайд про Lite-установку** (скрипт-ассистированный характер:
`gen_secrets.py` → секреты, `onboard_project.py` → регистрация проекта, `docker compose
up`, пошаговый runbook `LITE_SETUP.md` с проверкой каждого шага);
- добавить **23 слайда-инструкцию «как пользоваться орком через Plane»** (запуск задачи,
статусная модель «индикация ≠ управление», два человеческих гейта — Approved и Confirm
Deploy, авто-лейблы `autoApprove`/`autoDeploy`/`Bug`, отмена STOP, наблюдение за ходом);
- при необходимости — лёгкая актуализация продуктового нарратива на текущее состояние.
- Сохранить **сквозную нумерацию слайдов с 1** (renumber при вставке в середину) и формат
(заголовок + 36 тезисов + опц. `> Визуал:`), а также раздел «Как собрать .pptx».
- Убедиться, что `scripts/build_presentation.py` собирает валидный `.pptx`, включающий новые
слайды (скрипт менять **не требуется** — парсер обобщённый; правка только при крайней
необходимости).
- **Расширить анти-дрейф** `tests/test_system_docs.py`: зафиксировать присутствие нового
обязательного контента (Lite-установка, использование через Plane).
- Обновить `CHANGELOG.md` (запись `docs:`); при необходимости — указатель/маршрут в
`docs/overview/README.md`.
### Вне объёма
- **Коммит собранного `.pptx`-бинаря в git** — запрещён каноном ORCH-011 D5 (`build/`
в `.gitignore`); презентация собирается по требованию.
- Добавление `python-pptx` в `requirements*`/`Dockerfile` (NFR-2; машинно запрещено тестом).
- Кастомный графический дизайн/брендинг сверх существующего тёмного текстового шаблона
(визуалы — текстовые подписи `> Визуал: …`, а не растровые картинки).
- Переписывание дизайна рендера (`build_pptx`), смена темы/шрифтов — если это не строго
необходимо.
- Любые изменения рантайма: `src/**`, `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`,
схема БД — **не трогаются**.
- Переписывание `business.md` и прочих тех-блоков витрины (описанные способности — тираж и
работа через трекер — там уже отражены; ORCH-105 добавляет именно **слайды**).
- Создание отдельного монолитного «lite-инсталлятора» как нового скрипта (его нет; Lite
ставится комбинацией существующих скриптов и compose — см. §6).
## 3. Заинтересованные стороны
- **Заказчик / Owner** — постановщик задачи и **приёмщик** презентации; конечный
пользователь `.pptx` при показе стейкхолдерам.
- **Потенциальные заказчики / менеджеры** — аудитория презентации (нетехнический и
полу-технический читатель).
- **Операторы платформы** — целевые читатели слайдов «как пользоваться через Plane».
- **Reviewer** — контролирует норматив сопровождения витрины (изменил витрину → согласовано,
факты не разъехались с golden sources; необновлённый сопутствующий док → finding ≥ P1).
## 4. Бизнес-требования (BR)
- **BR-1** — Презентация о продукте в формате PowerPoint собирается из слайдо-источника
витрины и содержит связный рассказ о платформе (существующий нарратив сохранён и при
необходимости актуализирован).
- **BR-2** — В презентации есть **выделенный слайд про Lite-установку**, точно отражающий её
скрипт-ассистированный характер: секреты генерирует `scripts/gen_secrets.py`, проект
регистрирует `scripts/onboard_project.py` (`plan`/`apply`/`verify`), стек поднимается
`docker compose`, а маршрут описан пошаговым runbook `LITE_SETUP.md` с проверкой каждого
шага. Без вымышленных артефактов/скриптов.
- **BR-3** — В презентации есть **слайды-инструкция «как пользоваться орком через Plane»**,
покрывающие: запуск задачи (перевод в «To Analyse» — единственная точка входа), статусную
модель (индикация ≠ управление; управляющих статусов ровно три), **два человеческих гейта**
(Approved на `analysis`, Confirm Deploy на `deploy`), **авто-лейблы** (`autoApprove`/
`autoDeploy`/`Bug`), **отмену STOP**, и **наблюдение за ходом** (статусы доски + живая
карточка в Telegram + комментарии в задаче).
- **BR-4** — Итоговый `.pptx` собирается документированной командой
(`scripts/build_presentation.py`) и включает **все** новые слайды; тёмный дизайн и
корректное отображение кириллицы сохранены.
- **BR-5** — Содержание презентации **фактологически точно** и согласовано с golden sources
витрины (`tech-pipeline.md`, `tech-integrations.md`, `LITE_SETUP.md`, паспорт `CLAUDE.md`):
никаких выдуманных возможностей, имён статусов, лейблов или скриптов.
- **BR-6** — Сопровождение выполнено в том же PR: `CHANGELOG.md` несёт запись `docs:` по
ORCH-105; норматив витрины (ORCH-011/079) соблюдён.
## 5. Нефункциональные требования (NFR)
- **NFR-1 (self-hosting безопасность)** — изменение **docs-only**: `src/**`,
`STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`, схема БД — байт-в-байт не трогаются; `python-pptx`
**не попадает** в прод-образ; сборка `.pptx` — только вне рантайма (dev-venv).
- **NFR-2 (анти-дрейф / совместимость формата)** — после правок `presentation.md` остаётся
валидной для канонического парсера `parse_slides` (сквозная нумерация с 1, ≥1 тезис на
слайд, непустые заголовки); **весь существующий** `tests/test_system_docs.py` — зелёный.
- **NFR-3 (бинарь не в git)** — собранный `.pptx` не коммитится; `build/` остаётся в
`.gitignore`; артефакт сборки — по требованию.
- **NFR-4 (гигиена)** — в слайдах **нет** боевых хост-литералов (FORBIDDEN-скан) и
секретоподобных значений (секрет-эвристика); относительные ссылки резолвятся.
- **NFR-5 (стиль)** — новые слайды держат стиль деки: 36 тезисов, опц. подпись
`> Визуал: …`, тон и терминологию `business.md` (нетехнический язык для продуктовых слайдов;
оператор-инструкция — простыми шагами).
## 6. Допущения и ограничения
- **Форма поставки = воспроизводимая сборка.** Deliverable — **расширенный слайдо-источник +
скрипт сборки**; сам `.pptx` производится по требованию (ORCH-011 D5). Если нетехническому
стейкхолдеру нужен готовый файл — он **собирается командой и передаётся вне git** (например,
вложением к задаче Plane); это операционный шаг, не изменение кода. Это сознательное
следование действующему машинно-проверяемому канону, а не упущение.
- **«Скрипт-установщик» для Lite** = фактические скрипты-помощники (`gen_secrets.py`,
`onboard_project.py`) + `docker compose` + verified-runbook `LITE_SETUP.md`. **Отдельного
монолитного lite-инсталлятора в репозитории нет**; одношаговый bootstrap (`bootstrap_bundle.py`)
относится к варианту **Bundled**, а не Lite. Заказчик явно просил **Lite** — слайд остаётся
точным к Lite (при желании можно одной строкой упомянуть Bundled-bootstrap как смежный
вариант, но фокус слайда — Lite).
- **Рост деки.** С добавлением ~4 слайдов (1 Lite + 23 Plane) дека вырастает до ~1920
слайдов. Это выше «ориентира 1418» из ORCH-011 FR-4, но **проходит жёсткий тест** (`≥ 12`).
Рост оправдан явным запросом; финальное число и возможное слияние слайдов — на усмотрение
developer/architect (жёсткое требование — только сквозная нумерация и ≥ 12).
- **Сборка/проверка `.pptx`** предполагает доступность `python-pptx` в dev-venv (вне рантайма).
**Автоматический гейт тестов проверяет ИСТОЧНИК** (`presentation.md`: парс/структура/контент),
а **не отрендеренный бинарь** — рендер проверяется **вручную** в dev-venv (честное
разграничение; см. 03/04).
## 7. Критерии успеха
Расширенный слайдо-источник собирается в `.pptx`; слайды Lite-установки и использования через
Plane присутствуют, точны и согласованы с golden sources; сквозная нумерация и формат
сохранены; весь `tests/test_system_docs.py` (с новыми проверками) зелёный; `python-pptx` не в
прод-образе, бинарь не в git; `CHANGELOG`/витрина обновлены. Детальные PASS/FAIL —
`03-acceptance-criteria.md`.
## 8. Риски
- **Ошибка перенумерации** при вставке слайдов в середину → падение проверки сквозной
нумерации (`parse_slides`).
- **Дрейф фактов**: слайд расходится с реальной возможностью/именем (статус/лейбл/скрипт).
- **Случайный запрещённый литерал/секрет** в тексте слайда → красный анти-дрейф.
- **Неточная подача Lite** как монолитного инсталлятора (его нет) → вводящий в заблуждение
слайд.
- **Раздувание деки** сверх читаемого объёма.
Краткий перечень; детальная проработка и митигации — `10-tech-risks.md` (заполняет архитектор).

View File

@@ -0,0 +1,150 @@
---
work_item: ORCH-105
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-12
model_used: claude-opus-4-8
---
# 02 — ТЗ (TRZ): ORCH-105 — Подготовка презентации по орку
Work Item: **ORCH-105** · Repo: **orchestrator** · Стадия: analysis
> ТЗ описывает **конкретные изменения к реализации**, выведенные из BRD и фактического кода.
> Архитектурное обоснование/решения — задача архитектора (06-adr).
## 1. Сводка изменения
**Docs-only** доработка контента витрины. Расширяем существующий слайдо-источник
`docs/overview/presentation.md` (канон ORCH-011) тремя смысловыми блоками: (1) выделенный
слайд про **Lite-установку** скриптами; (2) 23 слайда-инструкция **«как пользоваться орком
через Plane»**; (3) при необходимости — точечная актуализация нарратива. Сохраняем формат и
сквозную нумерацию слайдов, раздел сборки `.pptx` и тёмный дизайн. Дополняем анти-дрейф
`tests/test_system_docs.py`, чтобы новый обязательный контент был зафиксирован машинно.
Скрипт сборки `scripts/build_presentation.py` менять **не требуется** (парсер обобщённый).
Рантайм (`src/**`, стадии, гейты, БД) — **не затрагивается**.
## 2. Задействованные модули / пути
| Путь | Действие |
|------|----------|
| `docs/overview/presentation.md` | **изменить** — добавить слайд Lite + 23 слайда Plane-usage; перенумеровать сквозно; сохранить раздел «Как собрать .pptx»; опц. актуализировать нарратив |
| `tests/test_system_docs.py` | **изменить** — добавить assert'ы присутствия нового обязательного контента (Lite-установка, использование через Plane); существующие проверки сохранить зелёными |
| `CHANGELOG.md` | **изменить** — запись `docs:` по ORCH-105 |
| `docs/overview/README.md` | **опц.** — указатель/строка маршрута (если уместно); не обязателен |
| `scripts/build_presentation.py` | **НЕ изменять**`parse_slides` обобщён; правка только при крайней необходимости (тогда сохранить ленивый импорт `pptx` и stdlib-only top-level) |
| `build/orchestrator-overview.pptx` | **НЕ коммитить** — артефакт сборки (`build/` в `.gitignore`, ORCH-011 D5) |
| `requirements*.txt`, `Dockerfile` | **НЕ изменять**`python-pptx` не добавлять (NFR-2) |
## 3. Функциональные требования
### FR-1 — Слайд про Lite-установку (BR-2)
Добавить **один** слайд (формат `## Слайд N: <Заголовок>` + 36 тезисов + опц. `> Визуал:`),
точно отражающий Lite-маршрут по `docs/deployment/LITE_SETUP.md`:
- Lite = **два контейнера платформы** (`orchestrator` + `orchestrator-watchdog`) на
инфраструктуре заказчика; свои Plane / Gitea / Telegram / LLM подключаются (не входят в Lite).
- Разворачивается **без правки кода — только конфиг** (принцип 10-common, ORCH-101).
- Скрипты-помощники: `scripts/gen_secrets.py` (свежие секреты), `scripts/onboard_project.py`
(`plan`/`apply`/`verify` — регистрация проекта заказчика); подъём — `docker compose up -d`.
- Маршрут — **пошаговый runbook** `LITE_SETUP.md` с **проверкой каждого шага** (PASS/FAIL).
- **Рекомендуемое размещение:** сразу после текущего слайда «Тираж платформы» (обзор Lite/
Bundled) — как его углубление. Точная позиция — на усмотрение developer (требование жёсткое
только к сквозной нумерации).
- **Точность (BR-5):** не называть Lite «одним монолитным инсталлятором»; одношаговый
bootstrap — это Bundled (`bootstrap_bundle.py`), упоминание опционально и как **смежный**
вариант.
### FR-2 — Слайды «как пользоваться орком через Plane» (BR-3)
Добавить **23** слайда оператор-инструкции, опираясь на `docs/overview/tech-pipeline.md`
(человеческие гейты, STOP, статусная модель) и `tech-integrations.md` (Plane). Покрыть:
- **Запуск:** перевод задачи в статус **«To Analyse»** — единственная точка входа в конвейер.
- **Статусная модель:** статусы Plane — **индикация, не управление**; платформа сама их
выставляет; управляющих статусов ровно три (запуск, человеческие гейты, STOP).
- **Два человеческих гейта:** **Approved** на `analysis` (одобрить постановку) и **Confirm
Deploy** на `deploy` (подтвердить прод-выкладку — отдельный статус, чтобы привычный approve
не выкатывал прод случайным кликом).
- **Авто-лейблы:** `autoApprove` / `autoDeploy` (снимают человеческие гейты, пакетный
авто-режим — но **ни одна техническая проверка не пропускается**); `Bug` (багфикс-маршрут).
- **Отмена:** статус **STOP** — безопасная отмена с уборкой (ветка/worktree), `cancelled`;
STOP не трогает `main`/прод-контейнер.
- **Наблюдение за ходом:** платформа двигает статусы доски (Backlog → … → Done) + **живая
карточка в Telegram** (стадия, стоимость, время, кликабельный номер задачи) + комментарии в
задаче со ссылками на ветку/PR.
- **Рекомендуемое размещение:** связным кластером после слайда «Человек в контуре» (он вводит
«два решения человека») и/или рядом со «Сценарии использования». Точная позиция — на
усмотрение developer.
### FR-3 — Формат и сквозная нумерация (NFR-2)
- Слайды нумеруются **сквозно с 1** без пропусков (вставка в середину ⇒ перенумеровать все
последующие). Это машинно-проверяемо: `tests/test_system_docs.py::
test_presentation_source_parses_with_canonical_parser` требует
`[s.number] == list(range(1, N+1))`.
- Каждый новый слайд: **непустой заголовок** + **36 тезисов** (минимум — ≥1, требует тест) +
опц. одна строка `> Визуал: …`.
- Раздел **«Как собрать .pptx»** сохранён (несёт `build_presentation.py` и маркеры «Проверка:»
— требует `test_presentation_carries_reproducible_build_procedure`).
- Служебные `##`-заголовки (например, «Как собрать .pptx») не являются слайдами и завершают
предыдущий слайд (контракт `parse_slides`) — не вставлять контент слайдов после них.
### FR-4 — Сборка `.pptx` с новыми слайдами (BR-4)
Команда из канона (вне рантайма, dev-venv):
```bash
python3 -m venv .venv-pptx && .venv-pptx/bin/pip install python-pptx
.venv-pptx/bin/python scripts/build_presentation.py
```
Ожидаемо: печать `Собрано слайдов: N → build/orchestrator-overview.pptx`, где `N` =
фактическому числу слайдов источника (включая новые). Открытие `.pptx`: тёмная тема, светлый
текст, акцентные заголовки, корректная кириллица, новые слайды присутствуют и редактируемы.
**Это ручная проверка** (см. §6 — `python-pptx` нет в гейте тестов).
### FR-5 — Анти-дрейф нового контента (NFR-2, BR-5)
Расширить `tests/test_system_docs.py`, зафиксировав присутствие нового обязательного нарратива
в `presentation.md`:
- маркер **использования через Plane** (например, наличие подстроки уровня `plane` + признака
оператор-инструкции, по образцу `test_presentation_covers_mandatory_narrative_bits`);
- маркер **Lite-установки** (например, `lite` / `установк`).
Точные подстроки выбирает developer; цель — чтобы случайное удаление новых слайдов рвало CI.
Все **существующие** проверки модуля остаются зелёными без послаблений.
### FR-6 — Сопровождение (BR-6)
- `CHANGELOG.md`: запись `docs:` по ORCH-105 (презентация дополнена слайдами Lite-установки и
использования через Plane).
- Витрина: при изменении `presentation.md` норматив сопровождения ORCH-011/079 соблюдён;
reviewer сверяет факты с golden sources.
## 4. Изменения API
**Нет.** Эндпоинты (`/queue`, `/metrics`, вебхуки и пр.) не затрагиваются.
## 5. Изменения схемы БД
**Нет.** Таблицы/миграции/индексы не вводятся и не меняются.
## 6. Требования к новым/изменённым QG checks
**Нет нового гейта.** Презентация — артефакт витрины; новый Quality Gate не регистрируется
(`QG_CHECKS` / `STAGE_TRANSITIONS` / `check_*` — байт-в-байт прежние). Контроль выполняется
**существующими** механизмами:
- `tests/test_system_docs.py` исполняется обычным тест-сьютом → попадает под `check_ci_green`
(выход из `development`) и `check_tests_passed` (стадия `testing`).
- **Важное разграничение (честность гейта):** автоматический гейт проверяет **источник**
(`presentation.md`: парс, нумерация, обязательные биты, процедура сборки) и инвариант
«`python-pptx` не в прод-образе». **Сам рендер `.pptx`** в гейте **не выполняется**
`python-pptx` отсутствует в прод/тест-образе по канону (ORCH-011 NFR-2), сборка — вне
рантайма в dev-venv (ручная проверка FR-4 / 04-test-plan TC-07).
## 7. Совместимость / регресс
- **Docs-only, нулевой рантайм-риск:** `src/**`, `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`,
схема БД — байт-в-байт не тронуты; kill-switch не нужен (нет рантайм-поведения).
- **Self-hosting безопасно:** `python-pptx` не входит в прод-образ; сборка — вне рантайма;
собранный бинарь не коммитится (`build/` в `.gitignore`).
- **Анти-дрейф сохранён:** `presentation.md` остаётся валидной для `parse_slides`; раздел
сборки и обязательные биты — на месте; новые проверки только **добавляют** покрытие.
- **Воспроизводимая сборка:** канон ORCH-011 (источник → скрипт → `.pptx` по требованию) не
форкается; один парсер (`parse_slides`) — один источник истины о формате.
- **Обратимость:** изменение полностью обратимо (revert правок `presentation.md` /
`test_system_docs.py` / `CHANGELOG.md`).
- **Регресс-поверхность:** полный `pytest tests/ -q` остаётся зелёным (нет правок кода
рантайма).

View File

@@ -0,0 +1,130 @@
---
work_item: ORCH-105
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-12
model_used: claude-opus-4-8
---
# 03 — Критерии приёмки (Acceptance Criteria): ORCH-105 — Подготовка презентации по орку
Work Item: **ORCH-105** · Repo: **orchestrator** · Стадия: analysis
Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL**
(что считается провалом). Любой машинный/ручной reviewer проверяет их буквально по файлам
репозитория.
---
## AC-1 — Слайд про Lite-установку присутствует и точен
**Условие:** `docs/overview/presentation.md` содержит выделенный слайд про Lite-установку.
- **PASS:** есть слайд (блок `## Слайд N: …`), посвящённый Lite-установке, с тезисами,
отражающими: два контейнера платформы (`orchestrator` + `orchestrator-watchdog`),
развёртывание без правки кода (только конфиг), скрипты-помощники (`gen_secrets.py`,
`onboard_project.py`) и/или `docker compose`, пошаговый runbook `LITE_SETUP.md` с проверкой
шагов. Факты согласованы с `docs/deployment/LITE_SETUP.md`.
- **FAIL:** слайда нет; ИЛИ слайд называет Lite «единым монолитным инсталлятором»/упоминает
несуществующий скрипт; ИЛИ путает Lite с Bundled; ИЛИ факты расходятся с `LITE_SETUP.md`.
---
## AC-2 — Слайды «как пользоваться орком через Plane» присутствуют и точны
**Условие:** `presentation.md` содержит ≥ 2 слайда оператор-инструкции по работе через Plane.
- **PASS:** ≥ 2 слайда покрывают (суммарно): запуск задачи («To Analyse»), статусную модель
«индикация ≠ управление», **оба** человеческих гейта (Approved на `analysis` и Confirm
Deploy на `deploy`), авто-лейблы (`autoApprove`/`autoDeploy`/`Bug`), отмену **STOP**,
наблюдение за ходом (статусы доски + живая Telegram-карточка + комментарии в задаче). Имена
статусов/лейблов точны и согласованы с `tech-pipeline.md`/`tech-integrations.md`/`CLAUDE.md`.
- **FAIL:** слайдов нет или один; ИЛИ пропущен любой из двух человеческих гейтов; ИЛИ
перепутаны имена статусов/лейблов (напр. «Approved выкатывает прод»); ИЛИ утверждается, что
авто-лейблы пропускают технические проверки.
---
## AC-3 — Сквозная нумерация и формат слайдов валидны
**Условие:** канонический парсер `parse_slides` разбирает источник без нарушений.
- **PASS:** `tests/test_system_docs.py::test_presentation_source_parses_with_canonical_parser`
зелёный — слайдов ≥ 12, номера `[1..N]` строго сквозные, у каждого слайда непустой заголовок
и ≥ 1 тезис.
- **FAIL:** тест падает — пропуск/дубль в нумерации, пустой заголовок, слайд без тезисов, или
слайдов < 12.
---
## AC-4 — Обязательный нарратив и процедура сборки сохранены + новые биты зафиксированы
**Условие:** обязательные биты нарратива на месте; процедура сборки цела; новый контент
анти-дрейф-защищён.
- **PASS:** `test_presentation_covers_mandatory_narrative_bits` и
`test_presentation_carries_reproducible_build_procedure` зелёные (биты `проблем`/`решени`/
`конвейер`/`сценари`/`тираж`/`статус` присутствуют; раздел сборки несёт `build_presentation.py`
и «Проверка»). Добавлены проверки присутствия **Lite-установки** и **использования через
Plane**, и они зелёные.
- **FAIL:** любой из этих тестов красный; ИЛИ новые биты Lite/Plane не покрыты тестом (можно
бесследно удалить новый слайд, CI не заметит).
---
## AC-5 — `.pptx` собирается с новыми слайдами (ручная dev-venv проверка)
**Условие:** документированная сборка вне рантайма даёт валидный `.pptx`, включающий новые
слайды.
- **PASS:** в dev-venv `.venv-pptx/bin/python scripts/build_presentation.py` печатает
`Собрано слайдов: N → build/orchestrator-overview.pptx` (N = числу слайдов источника, включая
новые), `exit code 0`; открытый файл показывает тёмную тему, корректную кириллицу, новые
слайды Lite/Plane — присутствуют и редактируемы.
- **FAIL:** скрипт печатает `ОШИБКА: …` / ненулевой код возврата; ИЛИ N не совпадает с числом
слайдов; ИЛИ новых слайдов нет в собранном файле; ИЛИ кириллица/тема сломаны.
---
## AC-6 — Анти-дрейф и гигиена витрины зелёные
**Условие:** структурный контур витрины проходит целиком.
- **PASS:** весь `tests/test_system_docs.py` зелёный, включая:
`test_showcase_carries_no_forbidden_host_literals` (нет боевых хост-литералов),
`test_showcase_carries_no_secret_like_values` (нет секретоподобных значений),
`test_all_relative_links_resolve_to_existing_files` (ссылки резолвятся),
`test_build_script_toplevel_imports_are_stdlib_only` (top-level скрипта без `pptx`).
- **FAIL:** любой из перечисленных тестов красный (запрещённый литерал/секрет/битая ссылка/
`pptx` на top-level скрипта).
---
## AC-7 — Self-hosting безопасность: рантайм и образ нетронуты
**Условие:** изменение остаётся docs-only.
- **PASS:** `git diff` не содержит изменений в `src/**`, `STAGE_TRANSITIONS` (`src/stages.py`),
`QG_CHECKS`/`check_*` (`src/qg/checks.py`), схеме БД (`src/db.py`);
`test_no_pptx_dependency_in_prod_image` зелёный (`python-pptx` нет в `requirements*`/
`Dockerfile`); собранный `.pptx` **не** добавлен в git (`build/` в `.gitignore`).
- **FAIL:** затронут любой рантайм-модуль/стадия/гейт/схема; ИЛИ `python-pptx` появился в
прод-образе; ИЛИ `.pptx`-бинарь закоммичен.
---
## AC-8 — Сопровождение выполнено в том же PR
**Условие:** доки сопровождения обновлены.
- **PASS:** `CHANGELOG.md` несёт запись `docs:` по ORCH-105; при изменении `presentation.md`
норматив витрины соблюдён; полный `pytest tests/ -q` зелёный.
- **FAIL:** нет записи в `CHANGELOG.md`; ИЛИ полный регресс `tests/` красный.
---
## Сводная матрица AC ↔ FR/BR
| AC | Покрывает |
|----|-----------|
| AC-1 | BR-2 / FR-1 |
| AC-2 | BR-3 / FR-2 |
| AC-3 | NFR-2 / FR-3 |
| AC-4 | BR-5 / FR-3 / FR-5 |
| AC-5 | BR-4 / FR-4 |
| AC-6 | NFR-4 / FR-5 |
| AC-7 | NFR-1 / NFR-3 |
| AC-8 | BR-6 / FR-6 |

View File

@@ -0,0 +1,84 @@
work_item: ORCH-105
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-12
model_used: claude-opus-4-8
title: "Презентация: слайды Lite-установки и использования через Plane (анти-дрейф витрины)"
framework: pytest
scope: >
Покрывается: структурная валидность слайдо-источника docs/overview/presentation.md после
добавления слайдов Lite-установки и использования через Plane (парс/нумерация/обязательные
биты/процедура сборки), фиксация нового контента анти-дрейф-тестом, гигиена витрины,
self-hosting-инварианты (python-pptx вне прод-образа, бинарь не в git), сопровождение
(CHANGELOG). Вне автоматического покрытия: фактический рендер .pptx через python-pptx —
выполняется ВНЕ рантайма в dev-venv (ORCH-011 NFR-2), поэтому TC-07 — ручной интеграционный
шаг, а не часть гейта тестов.
notes: >
Изменение docs-only: src/** не трогается, новый QG не вводится. Тесты живут в существующем
модуле tests/test_system_docs.py (ORCH-011) — большинство TC расширяют/подтверждают уже
имеющиеся проверки. Полный регресс pytest tests/ -q должен оставаться зелёным. Имена статусов
Plane и лейблов в слайдах сверяются с docs/overview/tech-pipeline.md, tech-integrations.md и
CLAUDE.md (фактологическая точность, BR-5). TC-07 (сборка .pptx) — обязательная ручная
проверка в одноразовом dev-venv: python-pptx в прод/тест-образ НЕ добавляется.
tests:
- id: TC-01
type: unit
description: "parse_slides разбирает presentation.md: слайдов >= 12, номера сквозные [1..N], у каждого непустой заголовок и >= 1 тезис (после добавления новых слайдов нумерация не разъехалась)."
module: tests/test_system_docs.py
expected: PASS
- id: TC-02
type: unit
description: "Обязательные биты нарратива присутствуют (проблем/решени/конвейер/сценари/тираж/статус) — существующая проверка остаётся зелёной после правок."
module: tests/test_system_docs.py
expected: PASS
- id: TC-03
type: unit
description: "Новый контент зафиксирован анти-дрейфом: добавлены и зелёные assert'ы присутствия слайда Lite-установки (напр. lite/установк) и слайдов использования через Plane (напр. plane + признак оператор-инструкции)."
module: tests/test_system_docs.py
expected: PASS
- id: TC-04
type: unit
description: "Процедура сборки .pptx цела: presentation.md несёт ссылку на build_presentation.py и явные маркеры «Проверка:» (test_presentation_carries_reproducible_build_procedure)."
module: tests/test_system_docs.py
expected: PASS
- id: TC-05
type: unit
description: "Гигиена витрины: нет боевых хост-литералов (FORBIDDEN-скан) и нет секретоподобных значений в presentation.md после добавления слайдов."
module: tests/test_system_docs.py
expected: PASS
- id: TC-06
type: unit
description: "Все относительные ссылки витрины (включая новые, если добавлены) резолвятся в существующие файлы (test_all_relative_links_resolve_to_existing_files)."
module: tests/test_system_docs.py
expected: PASS
- id: TC-07
type: integration
description: "Ручной dev-venv шаг (вне рантайма, python-pptx): .venv-pptx/bin/python scripts/build_presentation.py печатает «Собрано слайдов: N» (N = числу слайдов), exit 0; открытый .pptx — тёмная тема, кириллица корректна, новые слайды Lite/Plane присутствуют и редактируемы."
module: docs/overview/presentation.md
expected: PASS
- id: TC-08
type: unit
description: "Self-hosting инвариант: python-pptx отсутствует в requirements*/Dockerfile (test_no_pptx_dependency_in_prod_image); top-level scripts/build_presentation.py остаётся stdlib-only (импорт pptx ленивый)."
module: tests/test_system_docs.py
expected: PASS
- id: TC-09
type: unit
description: "Указатели репозитория: CHANGELOG.md несёт запись по ORCH-105 (запись docs:), витрина docs/overview/ по-прежнему достижима из README.md/CLAUDE.md."
module: tests/test_system_docs.py
expected: PASS
- id: TC-10
type: integration
description: "Полный регресс pytest tests/ -q зелёный — изменение docs-only, рантайм-код не тронут, регрессии нет."
module: tests/
expected: PASS

View File

@@ -0,0 +1,249 @@
---
work_item: ORCH-105
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-12
model_used: claude-opus-4-8
---
# ADR-001: Расширение слайдо-источника презентации — слайд Lite-установки и слайды «как пользоваться орком через Plane»
Work Item: **ORCH-105** — Подготовка презентации по орку
Стадия: **architecture**
Сквозная регистрация: **N/A — локальное решение задачи.** Канон витрины и презентации уже
зафиксирован сквозным `docs/architecture/adr/adr-0039-system-overview-docs-canon.md` (ORCH-011,
D3/D4/D5). ORCH-105 **наполняет** этот канон контентом, **не меняя** его инвариантов → новый
глобальный `adr-NNNN` не вводится. Анти-археология (3+ маркеров в код-блоке) не срабатывает —
правок исполняемого кода нет.
## Статус
Proposed
## Контекст
Заказчику нужна продуктовая презентация орка (PowerPoint) с двумя новыми смысловыми блоками:
(а) выделенный слайд про **Lite-установку скриптами** и (б) слайды-инструкция **«как
пользоваться орком через Plane»** (запуск, статусы, человеческие гейты, авто-лейблы, STOP,
наблюдение).
**Опора, не изобретать (сверено по коду/доке):**
- Слайдо-источник уже существует — `docs/overview/presentation.md` (16 слайдов в формате
`## Слайд N: Заголовок` + 36 тезисов + опц. `> Визуал:`), канон ORCH-011.
- Сборка `.pptx``scripts/build_presentation.py`; формат парсит **чистая stdlib-функция**
`parse_slides` (один парсер — один источник истины о формате; её же импортирует тест-контур).
- Анти-дрейф — `tests/test_system_docs.py` (валидирует структуру/нумерацию/обязательные биты
нарратива/процедуру сборки; FORBIDDEN-скан + секрет-эвристика; `pptx` не в прод-образе).
- Канон **намеренно** держит инварианты (adr-0039 §3 «Канон презентации»; детальные D4/D5
work-item ADR-001 ORCH-011): сборка — **вне рантайма** (dev-venv), `python-pptx` **не** в
прод/тест-образе, собранный `.pptx` **не коммитится** (`build/` в `.gitignore`).
**Факты для нового контента — golden sources (сверены):**
- Lite — `docs/deployment/LITE_SETUP.md` (+ `tech-pipeline.md` §Тираж, adr-0037): Lite = **два
контейнера платформы** `orchestrator` + `orchestrator-watchdog` на инфре заказчика (свои
Plane/Gitea/Telegram/LLM подключаются, в Lite не входят); разворачивается **без правки кода —
только конфиг** (принцип 10-common ORCH-101); скрипты-помощники `scripts/gen_secrets.py`
(свежие секреты) и `scripts/onboard_project.py` (`plan`/`apply`/`verify` — регистрация
проекта), подъём `docker compose up -d`; маршрут — пошаговый runbook с **проверкой каждого
шага** (PASS/FAIL). Одношаговый bootstrap (`bootstrap_bundle.py`) — это **Bundled**, не Lite.
- Plane-usage — `docs/overview/tech-pipeline.md` + `tech-integrations.md` + `CLAUDE.md`: вход —
статус **«To Analyse»** (единственная точка запуска); статусы Plane = **индикация, не
управление** (управляющих статусов ровно три); два человеческих гейта — **Approved** на
`analysis` и **Confirm Deploy** на `deploy`; авто-лейблы **autoApprove**/**autoDeploy**
(снимают только человеческие гейты — **ни одна техническая проверка не пропускается**) и
**Bug** (багфикс-маршрут); отмена — статус **STOP**`cancelled` (не трогает `main`/прод);
наблюдение — статусы доски + **живая карточка в Telegram** + комментарии в задаче со ссылками
на ветку/PR.
**Почему нужно архитектурное решение, а не просто «дописать слайды»:** дельта затрагивает три
файла канона (`presentation.md`, `tests/test_system_docs.py`, `CHANGELOG.md`) и несёт реальные
архитектурные развилки — рост деки за «ориентир 1418», точная привязка каждого тезиса к golden
source (риск фактдрейфа №1), форма анти-дрейфа нового контента без хрупкости и без нового гейта,
и честная граница «что проверяет CI vs. что проверяется руками». Развилки зафиксированы ниже.
## Решение
### Сводка
ORCH-105 — **docs-only расширение контента** слайдо-источника **внутри** канона adr-0039.
Добавляем **ровно 3 слайда** (1 Lite + 2 Plane-usage), доводя деку 16 → **19 слайдов**;
сохраняем сквозную нумерацию, формат, раздел сборки и тёмный дизайн. Каждый тезис привязан к
golden source (D3). Анти-дрейф — **одна новая тест-функция** в существующем контуре (D4); новый
QG **не** регистрируется. Рантайм (`src/**`, `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`, схема
БД) — **байт-в-байт** не тронут. `07-infra-requirements.md` / `08-data-requirements.md`
**не применимы** (нет смены топологии и схемы БД).
### D1 — Канон не форкается; новый компонент / гейт / глобальный ADR не вводятся
Расширение целиком живёт в инвариантах adr-0039:
- **Единственный источник истины о формате** — `parse_slides`; контент пишется под него (формат
слайда не меняется). Скрипт сборки **не правится** (парсер обобщён; правка — только при
крайней необходимости, тогда сохранить ленивый импорт `pptx` и stdlib-only top-level).
- **Новый Quality Gate НЕ регистрируется** (TRZ §6): `QG_CHECKS` / `STAGE_TRANSITIONS` /
`check_*` / machine-verdict ключи — байт-в-байт. Контроль — существующими механизмами:
`tests/test_system_docs.py` исполняется обычным сьютом → попадает под `check_ci_green` (выход
из `development`) и `check_tests_passed` (стадия `testing`).
- **Бинарь не в git, `python-pptx` не в прод-образе** (adr-0039 §3; NFR-1/NFR-3) — сохраняем.
- **Локальность решения:** нет нового компонента/стадии/гейта/смены БД → сквозной `adr-NNNN`
не нужен; канон уже зафиксирован adr-0039. `docs/architecture/README.md` и `internals.md`
**не обновляются** (стадии/гейты/компоненты не затронуты).
**Привязка:** BR-1, NFR-1, AC-7; TRZ §6/§7.
### D2 — Структура деки и размещение (ограниченный рост, +3 → 19 слайдов)
Рост деки ограничиваем **тремя** слайдами (анти-раздувание; BRD §6 явно допускает ~1920 и
проходит жёсткий пол `≥ 12`). **Рекомендуемая раскладка** (точные позиции — на усмотрение
developer, жёсткое требование — только сквозная нумерация и ≥ 12):
| Новый слайд | Якорь (после какого слайда) | Роль |
|-------------|------------------------------|------|
| **Запуск и ведение задачи через Plane** | после «Человек в контуре» (тек. №7) | оператор-инструкция: вход + статусная модель + наблюдение |
| **Что решает человек: гейты, авто-режим, отмена** | сразу за предыдущим (кластер) | оператор-инструкция: 2 гейта + авто-лейблы + STOP |
| **Lite-установка скриптами** | после «Тираж платформы» (тек. №14) | углубление обзорного слайда тиража |
Итоговая нумерация (рекомендация): Plane-слайды → новые №8, №9; Lite-слайд → новый №17; всё
последующее перенумеровать сквозно (deck = 19). **Инвариант FR-3:** вставка в середину ⇒
перенумеровать **все** последующие слайды; `parse_slides`-тест требует `[1..N]` строго подряд.
**Разграничение с существующими слайдами (анти-дубль; для reviewer):** новые Plane-слайды —
**процедурная оператор-инструкция** («что делаете вы / что показывает платформа»), а не описание
способностей. Они дополняют, не дублируют, capability-слайды «Человек в контуре» (№7),
«Наблюдаемость» (тек. №11) и «Сценарии использования» (тек. №13). Lite-слайд — углубление
обзорного «Тираж платформы» (тек. №14) до конкретного скрипт-маршрута.
**Число Plane-слайдов = 2** (а не 3): 8 обязательных тем (запуск, статус-модель, 2 гейта,
авто-лейблы, Bug, STOP, наблюдение) укладываются в 2 слайда по ≤ 6 тезисов. Если у developer
тезисы перерастают лимит 6 — допустим 3-й Plane-слайд (тогда deck = 20; BRD §6 разрешает).
**Привязка:** BR-2/BR-3, FR-1/FR-2/FR-3, AC-1/AC-2/AC-3.
### D3 — Привязка каждого тезиса к golden source (анти-фактдрейф, BR-5)
Фактдрейф — доминирующий риск (TR-2). **Норматив для developer:** каждый тезис нового слайда
обязан выводиться из golden source ниже; **запрещены** выдуманные возможности, имена статусов,
лейблов, скриптов.
**Lite-слайд**`docs/deployment/LITE_SETUP.md` (первоисточник) + `tech-pipeline.md` §Тираж:
- Lite = **два контейнера платформы** (`orchestrator` + `orchestrator-watchdog`); свои Plane /
Gitea / Telegram / LLM подключаются (в Lite не входят).
- Разворачивается **без правки кода — только конфиг** (10-common, ORCH-101).
- Скрипты-помощники: `scripts/gen_secrets.py` (секреты), `scripts/onboard_project.py`
(`plan`/`apply`/`verify`); подъём — `docker compose up -d`.
- Маршрут — пошаговый runbook `LITE_SETUP.md` с проверкой **каждого** шага (PASS/FAIL).
- **Точность (FAIL-условие AC-1):** НЕ называть Lite «единым монолитным инсталлятором»;
одношаговый `bootstrap_bundle.py` — это **Bundled**, упоминание опционально и как **смежный**
вариант (одной строкой, не как суть Lite).
**Plane-слайд A «Запуск и ведение задачи через Plane»**`tech-integrations.md` §Plane +
`tech-pipeline.md` §Статусная модель:
- Запуск: перевод задачи в **«To Analyse»** — единственная точка входа в конвейер.
- Статусная модель: статусы Plane — **индикация, не управление**; платформа выставляет их сама;
управляющих статусов ровно **три** (запуск, человеческие гейты, STOP).
- Наблюдение: статусы доски (Backlog → … → Done) + **живая карточка в Telegram** (стадия,
стоимость, время, кликабельный номер задачи) + комментарии в задаче со ссылками на ветку/PR.
**Plane-слайд B «Что решает человек: гейты, авто-режим, отмена»**`tech-pipeline.md`
§Человеческие гейты + `CLAUDE.md` (ORCH-089/090/019):
- Гейт 1 — **Approved** на `analysis` (одобрить постановку).
- Гейт 2 — **Confirm Deploy** на `deploy` (подтвердить прод — отдельный статус, чтобы привычный
approve не выкатывал прод случайным кликом).
- Авто-лейблы **autoApprove** / **autoDeploy** — снимают **только** человеческие гейты;
**ни одна техническая проверка не пропускается** (FAIL-условие AC-2 — обратное утверждение).
- Лейбл **Bug** — багфикс-маршрут (короче, но все гейты исполняются).
- **STOP** → `cancelled` — безопасная отмена с уборкой (рабочая ветка/worktree); **не трогает**
`main`/прод-контейнер.
**Точность имён критична:** имена статусов/лейблов воспроизводить дословно
(`To Analyse`/`Approved`/`Confirm Deploy`/`STOP`/`autoApprove`/`autoDeploy`/`Bug`). Reviewer
сверяет факты с golden sources (BR-5; правило агентов №6 — необновлённая/расходящаяся витрина →
finding ≥ P1).
**Привязка:** BR-5, FR-1/FR-2, AC-1/AC-2.
### D4 — Анти-дрейф нового контента: одна новая тест-функция, без хрупкости, без послаблений
**Решение:** в `tests/test_system_docs.py` **добавить ровно одну** функцию (рекомендуемое имя
`test_presentation_covers_lite_and_plane_usage_bits`), по образцу
`test_presentation_covers_mandatory_narrative_bits` (lower-case подстрочный матч). Существующие
функции — **байт-в-байт** (только добавление; NFR-2).
Требования к выбору маркеров (чтобы тест ловил удаление слайда, но не был хрупким к
переформулировке):
- **Lite:** `lite` **и** маркер установки (`установк` / `разверн`).
- **Plane-usage:** `plane` **и** оператор-маркер (например `to analyse` / `confirm deploy` /
`stop`). Точные подстроки — на усмотрение developer; выбирать **семантические корни**, не
целые фразы (анти-переобучение).
- Матч — по lower-case тексту (как существующий бит-тест).
- **Не** трогать пол `≥ 12` и сквозную нумерацию в
`test_presentation_source_parses_with_canonical_parser` — он уже покрывает счёт/формат.
- Маркеры обязаны пережить `test_showcase_carries_no_forbidden_host_literals` (без боевых
хост-литералов) и `test_showcase_carries_no_secret_like_values` (без hex≥32/alnum≥40);
имена статусов/лейблов и `8500/8501` этим скан-ам не противоречат.
**Цель (AC-4 FAIL-условие):** бесследное удаление нового слайда должно рвать CI.
**Привязка:** BR-5, FR-5, NFR-2, AC-4.
### D5 — Честная граница гейта: источник проверяется CI, рендер `.pptx` — вручную
Автоматический контур проверяет **источник** (`presentation.md`: парс/нумерация/обязательные
биты/процедура сборки) и инвариант «`python-pptx` не в прод-образе». **Сам рендер `.pptx` в
гейте не выполняется** — `python-pptx` намеренно отсутствует в прод/тест-образе (adr-0039 §3,
NFR-1). Сборка и визуальная проверка (тёмная тема, кириллица, новые слайды присутствуют и
редактируемы) — **ручной dev-venv шаг** (FR-4 / AC-5 / тест-план TC-07). Это **сознательная
честная граница**, а не пробел покрытия; tester обязан выполнить ручную сборку и зафиксировать
исход. Ожидаемая печать: `Собрано слайдов: 19 → build/orchestrator-overview.pptx` (N = факт.
числу слайдов), exit 0.
**Привязка:** BR-4, FR-4, AC-5, NFR-1.
## Альтернативы
- **Форкнуть `build_presentation.py` / завести гейт, рендерящий `.pptx` в CI** — отвергнуто:
нарушает adr-0039 §3 и NFR-1 (`python-pptx` намеренно вне прод/тест-образа); оверинжиниринг
для контентной правки. Рендер остаётся ручным (D5).
- **Закоммитить собранный `.pptx` для удобства стейкхолдера** — отвергнуто: adr-0039 §3 / NFR-3
(бинарь не в git). Поставка = воспроизводимая сборка; готовый файл собирается командой и
передаётся **вне git** (вложением к задаче Plane) — операционный шаг, не изменение кода
(BRD §6).
- **Завести сквозной `adr-NNNN`** — отвергнуто: ничего сквозного не меняется (нет нового
компонента/гейта/стадии/смены БД); канон презентации уже держит adr-0039.
- **Один объединённый слайд использования** — отвергнуто: AC-2 жёстко требует ≥ 2 слайда, а
8 обязательных тем переполняют лимит 6 тезисов одного слайда.
- **3+ слайда Plane по умолчанию** — отвергнуто как дефолт (анти-раздувание): 2 слайда
достаточны; 3-й допустим лишь при переполнении тезисов (D2).
## Последствия
- **+** Дека получает оператор-применимый контент (Lite-маршрут + работа через Plane); каждый
факт CI-заякорен к golden source новой подстрочной проверкой (D4); нулевой рантайм-риск; для
enduro-trails инертно.
- **+** Канон не форкается: один парсер `parse_slides` = один источник истины; анти-дрейф только
**добавляет** покрытие.
- **** Дека растёт до 19 (выше ориентира 1418) — принято осознанно (BRD §6), ограничено **+3**;
митигировано плотной группировкой тезисов (D2) и порогом `≥ 12`, который не нарушается.
- **** Ручной рендер вне CI — принятая честная граница (D5); митигировано явным ручным шагом
AC-5 / TC-07 (исход фиксирует tester).
- ** (риск перенумерации)** вставка в середину рвёт сквозную нумерацию при невнимательности —
митигировано тестом `test_presentation_source_parses_with_canonical_parser` (`[1..N]`) и явным
инвариантом FR-3 (см. TR-1).
- **Применимость 07/08:** `07-infra-requirements.md` и `08-data-requirements.md` **не создаются**
— нет смены топологии и схемы БД (TRZ §4/§5).
- **Откат:** полностью обратимо — revert правок `presentation.md` / `tests/test_system_docs.py` /
`CHANGELOG.md` (1:1, без миграций и состояния).
## Ссылки
- BRD: `docs/work-items/ORCH-105/01-brd.md`
- TRZ: `docs/work-items/ORCH-105/02-trz.md`
- Acceptance: `docs/work-items/ORCH-105/03-acceptance-criteria.md`
- Риски: `docs/work-items/ORCH-105/10-tech-risks.md`
- Канон витрины/презентации (не меняется): `docs/architecture/adr/adr-0039-system-overview-docs-canon.md`
- Golden sources контента: `docs/deployment/LITE_SETUP.md`, `docs/overview/tech-pipeline.md`,
`docs/overview/tech-integrations.md`, `CLAUDE.md`
- Сверено по коду: `scripts/build_presentation.py` (`parse_slides`),
`tests/test_system_docs.py` (анти-дрейф витрины), `docs/overview/presentation.md` (источник)

View File

@@ -0,0 +1,37 @@
---
work_item: ORCH-105
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-12
model_used: claude-opus-4-8
---
# 10 — Технические риски: ORCH-105 — Подготовка презентации по орку
Work Item: **ORCH-105** · Repo: **orchestrator** · Стадия: architecture
> Информационный (гейтом не парсится). Перечисляет риски реализации и их митигейшн.
> Решения — `06-adr/ADR-001-presentation-lite-and-plane-usage-slides.md`.
## Реестр рисков
| ID | Риск | Вер. | Влия. | Митигейшн |
|----|------|------|-------|-----------|
| TR-1 | **Ошибка перенумерации** при вставке слайдов в середину (пропуск/дубль номера) → падение сквозной нумерации. | Сред. | Низ. | Тест `test_presentation_source_parses_with_canonical_parser` требует `[1..N]` строго подряд → ловит мгновенно в CI; инвариант FR-3 явно выписан; ADR D2 даёт точную раскладку 19 слайдов. Откат — тривиальный revert. |
| TR-2 | **Фактдрейф**: тезис расходится с реальной возможностью / именем статуса / лейбла / скрипта (напр. «Approved выкатывает прод», «autoApprove пропускает проверки», «Lite — монолитный инсталлятор»). | Сред. | Сред. | ADR D3 — построчная привязка к golden sources (`LITE_SETUP.md`/`tech-pipeline.md`/`tech-integrations.md`/`CLAUDE.md`) с дословными именами; новый бит-тест D4 фиксирует присутствие; reviewer сверяет факты (правило агентов №6 → finding ≥ P1). |
| TR-3 | **Случайный запрещённый хост-литерал / секретоподобное значение** в тексте слайда → красный анти-дрейф. | Низ. | Низ. | Существующие `test_showcase_carries_no_forbidden_host_literals` (FORBIDDEN-импорт) и `test_showcase_carries_no_secret_like_values` (hex≥32/alnum≥40) ловят в CI; ADR D4 предписывает использовать только семантические корни/имена статусов; `8500/8501` явно безопасны (самочек эвристики). |
| TR-4 | **Хрупкий или послабляющий анти-дрейф-тест**: переобучение на целую фразу (рвётся при переформулировке) ИЛИ ослабление существующих проверок. | Сред. | Низ. | ADR D4 — ровно одна новая функция по образцу `..._mandatory_narrative_bits`, матч по семантическим корням, существующие функции байт-в-байт; пол `≥ 12` и нумерация не трогаются. |
| TR-5 | **Раздувание деки** сверх читаемого объёма (соблазн добавить >3 слайда). | Низ. | Низ. | ADR D2 ограничивает рост **+3** (deck = 19; 3-й Plane-слайд — только при переполнении лимита 6 тезисов); BRD §6 допускает ~1920; жёсткий пол `≥ 12` не нарушается. |
| TR-6 | **Случайная правка рантайма / попадание `python-pptx` в прод-образ / коммит `.pptx`-бинаря** → нарушение self-hosting-инварианта. | Низ. | Выс. | Docs-only по ТЗ; `git diff` не должен затрагивать `src/**`/`STAGE_TRANSITIONS`/`QG_CHECKS`/схему БД (AC-7); `test_no_pptx_dependency_in_prod_image` + `build_script_toplevel_imports_are_stdlib_only` зелёные; `build/` в `.gitignore`. Скрипт сборки не правится (ADR D1). |
| TR-7 | **Ложное ощущение покрытия рендера**: рендер `.pptx` в CI не выполняется (нет `python-pptx`), визуальный дефект (битая кириллица/тема/отсутствие слайда) проходит мимо автогейта. | Сред. | Низ. | ADR D5 — честная граница: рендер проверяется **вручную** в dev-venv (FR-4/AC-5/TC-07), исход фиксирует tester; печать `Собрано слайдов: N → …` сверяется с числом слайдов источника. |
## Сводный вывод
Доминирующий класс — **риски точности контента** (TR-1, TR-2): чисто документарные, ловятся
существующим + одним новым структурным тестом и сверкой reviewer с golden sources; остаточный
риск низкий. Рантайм-класс (TR-6) теоретически высоковлияющий, но вероятность минимальна —
изменение docs-only по конструкции, накрыто машинными гардами витрины (AC-7) и инвариантами
adr-0039. **Эскалация `arch:major-change` не требуется; возврат в анализ не требуется.** Для
прод-конвейера (self-hosting) и для enduro-trails изменение инертно: `src/**`, стадии, гейты,
схема БД — байт-в-байт; `python-pptx` вне прод-образа; собранный бинарь не коммитится.

View File

@@ -0,0 +1,116 @@
---
verdict: APPROVED # APPROVED | REQUEST_CHANGES — строго одно из двух, UPPERCASE
work_item: ORCH-105
stage: review
author_agent: reviewer
status: approved
created_at: 2026-06-12
model_used: claude-opus-4-8
type: review
work_item_id: ORCH-105
version: 1
---
# Review ORCH-105 — Подготовка презентации по орку (слайды Lite-установки и использования через Plane)
> Машинный вердикт читается ТОЛЬКО из `verdict:` во frontmatter. `APPROVED` → дальше по конвейеру.
## Summary
**Docs-only** доработка витрины: слайдо-источник `docs/overview/presentation.md` расширен **тремя**
слайдами в каноне ORCH-011 (дека 16 → 19, сквозная нумерация цела), добавлена **одна** анти-дрейф
тест-функция в `tests/test_system_docs.py`, обновлён `CHANGELOG.md`. Реализация **полностью**
соответствует ТЗ (`02-trz.md`), критериям приёмки (`03-acceptance-criteria.md`) и
`06-adr/ADR-001`. Рантайм (`src/**`, `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`, схема БД) —
байт-в-байт не тронут. Факты новых слайдов сверены с golden sources. Весь
`tests/test_system_docs.py`**зелёный (29 passed)**. P0/P1 findings нет → **APPROVED**.
> **Замечание по среде ревью (не finding):** локальный `main` устарел (`9b7bdc0`), поэтому
> `git diff main...HEAD` ложно тянул уже смерженный контент ORCH-011. Достоверный диф снят против
> `origin/main` (`4d5e461`) — он чистый: `CHANGELOG.md`, `presentation.md`, `tests/test_system_docs.py`
> + артефакты `docs/work-items/ORCH-105/`. Никаких чужих правок в PR нет.
## Оси проверки
### 1. Соответствие ТЗ / Acceptance Criteria — PASS
- **FR-1 / AC-1 (Lite-слайд):** добавлен «Слайд 17: Lite-установка скриптами». Тезисы точны и
согласованы с `docs/deployment/LITE_SETUP.md`: два контейнера платформы (`orchestrator` +
`orchestrator-watchdog`), развёртывание без правки кода (только конфиг), помощники
`gen_secrets.py`/`onboard_project.py` (`plan`/`apply`/`verify`) + `docker compose up -d`, runbook
с проверкой каждого шага. **BR-5 соблюдён:** одношаговый bootstrap явно отнесён к **смежному
Bundled, не Lite** (нет «монолитного инсталлятора», нет выдуманных скриптов).
- **FR-2 / AC-2 (Plane-usage):** добавлены **два** оператор-слайда («Слайд 8: Запуск и ведение
задачи через Plane», «Слайд 9: Что решает человек: гейты, авто-режим, отмена»). Покрыты все 8
обязательных тем: запуск «To Analyse», «индикация ≠ управление», **оба** человеческих гейта
(`Approved` на анализе и `Confirm Deploy` на деплое), авто-лейблы `autoApprove`/`autoDeploy`/`Bug`,
отмена `STOP`, наблюдение (статусы доски + Telegram-карточка + комментарии со ссылками на
ветку/PR). Имена статусов/лейблов **дословно** сверены с `tech-pipeline.md` (стр. 65/68/97) и
`tech-integrations.md` (стр. 8/13). Инвариант «авто-режим не пропускает техпроверки» зафиксирован
верно.
- **FR-3 / AC-3 (нумерация/формат):** заголовки слайдов строго сквозные `1..19`, `## Как собрать
.pptx` остаётся служебным разделом (не слайд). `test_presentation_source_parses_with_canonical_parser`
— зелёный (≥12, `[1..N]`, непустые заголовки, ≥1 тезис).
- **FR-5 / AC-4 (анти-дрейф):** добавлена `test_presentation_covers_lite_and_plane_usage_bits`,
фиксирующая `lite`+маркер установки и `plane`+`to analyse`/`approved`/`confirm deploy`/`stop`.
Существующие проверки нарратива и процедуры сборки — без послаблений, зелёные.
- **FR-6 / AC-8 (сопровождение):** запись `docs:` по ORCH-105 в `CHANGELOG.md` присутствует и
содержательна; норматив витрины ORCH-011/079 соблюдён (PR сам и есть обновление витрины).
### 2. Соответствие ADR / инвариантам — PASS
- Реализация 1:1 c `06-adr/ADR-001`: ровно 3 слайда (2 Plane + 1 Lite), дека 16→19, **одна** новая
тест-функция, новый QG **не** регистрируется, скрипт `build_presentation.py` **не** правится
(подтверждено: вне дифа), `07/08`-доки не создаются (нет смены топологии/схемы БД).
- Канон витрины `adr-0039` не форкается; локальность решения обоснована (глобальный `adr-NNNN` не
нужен) — корректно.
- **Трассировка (TRACEABILITY):** исполняемый код с маркерами `ORCH-NNN` не затронут (docs/tests
only) → инварианты конвейера не задеты, анти-археология не срабатывает.
### 3. Качество кода — PASS
- Новая тест-функция **содержательна** (не тривиальна): проверяет присутствие конкретных имён
статусов/лейблов, несёт docstring с привязкой к FR-5/AC-4 и предупреждением об анти-переобучении.
- Багфикс-трек (ORCH-019/BR-4) **не применим** — задача не `Bug`, регресс-тест-фиксатор не требуется.
- Маркеры теста проходят `FORBIDDEN`-скан и секрет-эвристику (весь модуль зелёный).
### 4. Документация — PASS (см. раздел «Документация»)
### 5. Совместимость / регресс / self-hosting — PASS
- `git diff origin/main...HEAD -- 'src/**'` пуст → рантайм, стадии, гейты, схема БД не тронуты.
- `python-pptx` в прод-образ не добавлен; `build/` в `.gitignore` (стр. 19); новый `.pptx`-бинарь
не закоммичен (`docs/PRODUCT_VISION.pptx` — преэкзистент, вне дифа PR).
- Изменение полностью обратимо.
## Findings
### P0 — Blocker
- (нет)
### P1 — Must fix
- (нет)
### P2 — Should fix
- (нет)
### P3 — Nice-to-have (не блокирует)
- Анти-дрейф `assert "stop" in low` ловит подстроку `stop`, которая встречается и в визуале
слайда 9; для большей устойчивости к точечному удалению блока STOP можно было бы матчить
`«STOP»`/`stop` в связке с маркером отмены. Решение D4 осознанно выбрало семантические корни
(анти-переобучение) — приемлемо, правка не требуется.
## Документация
**Обновлена корректно и в том же PR.** Этот PR сам является обновлением витрины `docs/overview/`
(презентация — артефакт витрины ORCH-011). Проверено:
- `CHANGELOG.md` — запись `docs:` по ORCH-105 присутствует. ✅
- Витрина `docs/overview/presentation.md` — расширена; факты сверены с golden sources
(`docs/deployment/LITE_SETUP.md`, `docs/overview/tech-pipeline.md`,
`docs/overview/tech-integrations.md`, `CLAUDE.md`) — расхождений нет. ✅
- `README.md` «Известные ограничения» (ORCH-079) — данный PR ни одного пункта не закрывает
(контентная правда о уже существующей функциональности), обновление не требуется. ✅
- Прочие тех-блоки `docs/overview/tech-*.md`, `docs/architecture/README.md`/`internals.md` — новой
функциональности нет, обновлять нечего (правка только наполняет витрину контентом). ✅
- `src/` не изменён → P0-условие «`src/` изменён, документация не обновлена» **не наступает**.
**Замечание для tester:** AC-5 (ручная dev-venv сборка `.pptx`, ожидается
`Собрано слайдов: 19 → build/orchestrator-overview.pptx`, exit 0, тёмная тема/кириллица/новые
слайды) — ручной шаг стадии `testing` (TC-07), вне автоматического гейта по канону (`python-pptx`
не в тест-образе). Это сознательная честная граница (ADR D5), не пробел покрытия.

View File

@@ -0,0 +1,67 @@
---
result: PASS # PASS | FAIL — машинный вердикт, UPPERCASE
work_item: ORCH-105
stage: testing
author_agent: tester
status: pass
created_at: 2026-06-12
model_used: claude-opus-4-8
type: test-report
work_item_id: ORCH-105
---
# Test Report — ORCH-105 — Подготовка презентации по орку
> Машинный вердикт читается ТОЛЬКО из `result:` во frontmatter. `PASS` → задача переходит на
> `deploy-staging`.
## Окружение
- Python: 3.12.13
- pytest: 8.3.3 (plugins: cov-5.0.0, anyio-4.13.0, asyncio-0.23.8)
- Worktree (код ветки): `/repos/_wt/orchestrator/feature_ORCH-105-/` (ветка `feature/ORCH-105-`)
- Дата: 2026-06-12
## Smoke API (read-only)
- `GET /health``{"status":"ok","service":"orchestrator"}`**OK**
- `GET /status` → активные задачи перечислены, задача ORCH-105 (id 93) в стадии `testing`**OK**
- `GET /queue` → блок `serial_gate` присутствует (**True**), блок `auto_labels` присутствует
(**True**) — регресса смока нет — **OK**
## Покрытие ТЗ (каждый TC из 04-test-plan.yaml ↔ 03-acceptance-criteria.md)
| TC ID | Тип | Описание | AC | Результат |
|-------|-----|----------|----|-----------|
| TC-01 | unit | `parse_slides` разбирает `presentation.md`: слайдов 19 (≥12), номера сквозные `[1..19]`, у каждого непустой заголовок + ≥1 тезис (`test_presentation_source_parses_with_canonical_parser`) | AC-3 | PASS |
| TC-02 | unit | Обязательные биты нарратива (проблем/решени/конвейер/сценари/тираж/статус) — `test_presentation_covers_mandatory_narrative_bits` | AC-4 | PASS |
| TC-03 | unit | Новый контент зафиксирован анти-дрейфом: Lite-установка + использование через Plane — `test_presentation_covers_lite_and_plane_usage_bits` | AC-4 | PASS |
| TC-04 | unit | Процедура сборки `.pptx` цела (`build_presentation.py` + «Проверка:») — `test_presentation_carries_reproducible_build_procedure` | AC-4 | PASS |
| TC-05 | unit | Гигиена витрины: нет хост-литералов (`test_showcase_carries_no_forbidden_host_literals`) и секретоподобных значений (`test_showcase_carries_no_secret_like_values`) | AC-6 | PASS |
| TC-06 | unit | Все относительные ссылки витрины резолвятся — `test_all_relative_links_resolve_to_existing_files` | AC-6 | PASS |
| TC-07 | integration | Ручной dev-venv build: `build_presentation.py``Собрано слайдов: 19 → build/orchestrator-overview.pptx`, exit 0, валидный 56 КБ `.pptx` (N=19 = числу слайдов). Визуальная проверка темы/кириллицы — human-only, скрипт отработал чисто | AC-5 | PASS |
| TC-08 | unit | Self-hosting инвариант: `python-pptx` отсутствует в `requirements*`/`Dockerfile` (`test_no_pptx_dependency_in_prod_image`); top-level `build_presentation.py` stdlib-only (`test_build_script_toplevel_imports_are_stdlib_only`) | AC-7 | PASS |
| TC-09 | unit | `CHANGELOG.md` несёт запись `docs:` по ORCH-105; витрина `docs/overview/` достижима из README/CLAUDE (`test_changelog_has_orch_011_entry`, `test_repo_readme_links_overview`, `test_claude_md_carries_overview_pointer_and_normative`) | AC-8 | PASS |
| TC-10 | integration | Полный регресс `pytest tests/` зелёный — **1874 passed** | AC-8 | PASS |
**Итог покрытия:** все 10 TC выполнены и сопоставлены с AC-1…AC-8. Дополнительно подтверждено:
- **AC-1/AC-2** (контент Lite-слайда и Plane-usage слайдов) — структурно зафиксированы TC-03
(`test_presentation_covers_lite_and_plane_usage_bits`), фактологическая сверка — в `12-review.md`
(вердикт `APPROVED`).
- **AC-7** (docs-only): `git diff origin/main...HEAD` не содержит изменений `src/**`
затронуты только `docs/**`, `CHANGELOG.md`, `tests/test_system_docs.py`; `build/`-артефакт
`.pptx` не закоммичен (gitignored), `python-pptx` не добавлен в прод-образ.
## Вывод pytest
```
tests/test_system_docs.py ... 29 passed (включая test_presentation_covers_lite_and_plane_usage_bits)
...
================== 1874 passed, 1 warning in 74.80s (0:01:14) ==================
```
(Единственный warning — преэкзистентный `PydanticDeprecatedSince20` в `src/config.py:8`, не
связан с ORCH-105.)
## Итог
**PASS** — полный регресс `pytest tests/` зелёный (1874 passed), smoke read-only OK
(`serial_gate` + `auto_labels` в `/queue` присутствуют), каждый TC `04-test-plan.yaml` выполнен и
сопоставлен с критериями `03-acceptance-criteria.md`, изменение остаётся docs-only (рантайм/образ
нетронуты). Задача готова к переходу на `deploy-staging`.

View File

@@ -0,0 +1,12 @@
---
deploy_status: SUCCESS
work_item: ORCH-105
hook_exit_code: 0
deployed_by: deploy-finalizer
---
# Deploy log — ORCH-036 executable self-deploy
Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`.
Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.

View File

@@ -0,0 +1,43 @@
---
staging_status: SUCCESS
work_item: ORCH-105
stage: deploy-staging
author_agent: deployer
status: success
created_at: 2026-06-12
model_used: claude-opus-4-8
timestamp: 2026-06-12T05:18:22Z
base_url: http://localhost:8501
---
# Staging Gate Log
Staging test suite completed against the live `orchestrator-staging` instance (port 8501),
run canonically inside the container (`docker exec orchestrator-staging python3
/repos/orchestrator/scripts/staging_check.py --base-url http://localhost:8501 --mode stub`).
**Result: 8/10 checks PASS — exit code 0 → SUCCESS.**
All REAL checks green; the two failures are known sandbox-infra checks (C9a/C9b), waived
under ORCH-061 (`staging_infra_tolerance_enabled=True`). They depend on SANDBOX bot accounts
being project members, not on the pipeline.
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
## Check breakdown
| Block | Check | Result |
|-------|-------|--------|
| A SMOKE | A1 GET /health → 200 status=ok | ✓ PASS |
| A SMOKE | A2 GET /queue → 200 with counts/max_concurrency/resilience | ✓ PASS |
| A SMOKE | A3 ORCH_STAGING=true (not prod) | ✓ PASS |
| B ACCESS | B4 Plane: sandbox project accessible | ✓ PASS |
| B ACCESS | B5 Gitea: orchestrator-sandbox accessible, push=true | ✓ PASS |
| B ACCESS | B6 Registry: sandbox present, prod ET/ORCH absent | ✓ PASS |
| C E2E | C7 Create issue in Plane SANDBOX | ✓ PASS |
| C E2E | C8 Trigger pipeline via /webhook/plane | ✓ PASS |
| C E2E | C9a Branch appears in orchestrator-sandbox | ✗ FAIL (INFRA-WAIVED) |
| C E2E | C9b Analyst job enqueued in staging queue | ✗ FAIL (INFRA-WAIVED) |
Staging gate passed → pipeline may advance to `deploy`.

View File

@@ -0,0 +1,46 @@
---
staging_status: SUCCESS
work_item: ORCH-109
stage: deploy-staging
author_agent: deployer
status: success
created_at: 2026-06-14
model_used: claude-opus-4-8
timestamp: 2026-06-14T11:21:00Z
base_url: http://localhost:8501
mode: stub
---
# Staging Gate Log — ORCH-109
Canonical staging suite (`scripts/staging_check.py --base-url http://localhost:8501 --mode stub`)
executed **inside the `orchestrator-staging` container** (ORCH-048 canonical path). Exit code **0**
`staging_status: SUCCESS`.
## Result
```
RESULT: 8/10 checks PASS
REAL failed : none
SANDBOX_INFRA failed: ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue']
```
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
## Per-block detail
- **Block A (SMOKE):** A1 `/health` 200 ok · A2 `/queue` 200 (counts/max_concurrency/resilience) · A3 `ORCH_STAGING=true` — all PASS.
- **Block B (ACCESS):** B4 Plane sandbox accessible · B5 Gitea `orchestrator-sandbox` accessible (push=true) · B6 registry isolation (sandbox present, prod ET/ORCH absent) — all PASS.
- **Block C (E2E, stub):** C7 create issue in Plane SANDBOX · C8 trigger pipeline via `/webhook/plane` — PASS. C9a/C9b — **waived** sandbox-infra (SANDBOX bot accounts are not members of the sandbox Plane project, so pipeline steps 6+ are unreachable in the sandbox; not a pipeline regression — ORCH-061).
- **CLEANUP:** Plane test issue deleted (HTTP 204); no stray branch.
## Environment note (observability)
The canonical path requires `docker exec orchestrator-staging …`. In this run no `docker`/`docker compose`
CLI was reachable and the staging container was not running, so the `orchestrator-staging` service was
brought up (staging-only, port 8501, real `.env.staging` → sandbox-isolated registry, `ORCH_STAGING=true`)
via the Docker Engine API mirroring the compose `orchestrator-staging` spec, the canonical
`staging_check.py` was `docker exec`'d inside it, and the container was torn down afterwards to restore
the pre-existing state. Prod (8500) was never touched; `.env`/`.env.staging`/`docker-compose.yml`/prod
infra were not modified.

972
scripts/bootstrap_bundle.py Normal file
View File

@@ -0,0 +1,972 @@
#!/usr/bin/env python3
"""bootstrap_bundle.py — доводка Bundled-инсталляции до рабочего конвейера (ORCH-103).
Один запуск поверх `deploy/bundled/docker-compose.yml` доводит свежеподнятый стек
(орк + watchdog + Gitea + Plane CE) до рабочего состояния: preflight → секреты →
up + готовность → init Gitea (полностью автоматом) → init Plane (честные
manual-step чекпоинты с верификацией) → онбординг sandbox-проекта строго
кирпичом ``scripts/onboard_project.py`` → git-доступ агентов (HTTP token-remote)
→ сборка runtime-конфига орка (корневые ``.env`` / ``.env.watchdog``) → health.
Режимы (ADR-001 D5, паттерн ORCH-009):
plan — дефолт; ноль мутаций: печать плана + read-only preflight-диагностика.
apply — полный прогон; step-движок check→ensure (повторный запуск = каскад
skip; «resume» после manual-step = просто повторный запуск).
verify — read-only пост-проверка (health/queue/metrics + onboard verify).
Exit-коды (контракт TRZ FR-2): 0 — успех; 2 — остановка на manual-step или
незавершённое предусловие; 1 — ошибка.
Гарантии (NFR-3 / D5 / D9):
* python stdlib-only; модули платформы не импортируются (канон-знания — только
субпроцессами кирпичей gen_secrets.py / onboard_project.py, AC-7);
* значения секретов НИКОГДА не печатаются (только имена ключей/пути файлов);
* delete-операций НЕТ ВООБЩЕ: teardown — только документированная процедура
docs/deployment/BUNDLED_SETUP.md §13 (ADR-001 D9);
* существующие секреты не перетираются без явного ``--force-secrets``
(использовать только ДО первого подъёма стека: уже инициализированные
Plane/Gitea новых паролей сами не подхватят);
* скрипт говорит только с локальным docker целевого хоста.
Запуск — из корня чекаута репо orchestrator на целевом хосте:
python3 scripts/bootstrap_bundle.py # план + диагностика
python3 scripts/bootstrap_bundle.py apply # полный прогон
python3 scripts/bootstrap_bundle.py verify # read-only пост-проверка
"""
import argparse
import getpass
import json
import os
import secrets
import shutil
import socket
import subprocess
import sys
import tempfile
import time
import urllib.error
import urllib.request
import uuid
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BUNDLE_DIR = os.path.join(REPO_ROOT, "deploy", "bundled")
BUNDLE_COMPOSE = os.path.join(BUNDLE_DIR, "docker-compose.yml")
BUNDLE_ENV_EXAMPLE = os.path.join(BUNDLE_DIR, ".env.example")
BUNDLE_ENV = os.path.join(BUNDLE_DIR, ".env")
ROOT_ENV_EXAMPLE = os.path.join(REPO_ROOT, ".env.example")
ROOT_ENV = os.path.join(REPO_ROOT, ".env")
WATCHDOG_ENV_EXAMPLE = os.path.join(REPO_ROOT, ".env.watchdog.example")
WATCHDOG_ENV = os.path.join(REPO_ROOT, ".env.watchdog")
GEN_SECRETS = os.path.join(REPO_ROOT, "scripts", "gen_secrets.py")
ONBOARD = os.path.join(REPO_ROOT, "scripts", "onboard_project.py")
REQUIREMENTS = os.path.join(REPO_ROOT, "requirements.txt")
VENV_DIR = os.path.join(REPO_ROOT, ".venv")
VENV_PY = os.path.join(VENV_DIR, "bin", "python")
DOC = "docs/deployment/BUNDLED_SETUP.md"
# Узнаваемый префикс томов/контейнеров инсталляции (compose project name, D1).
PROJECT = "orchestrator-bundle"
ORCH_CONTAINER = "orchestrator-bundle-orchestrator-1"
# Машинные in-network URL (D4): сервис-DNS bundle-сети, не хост.
WEBHOOK_PLANE_URL = "http://orchestrator:8500/webhook/plane"
WEBHOOK_GITEA_URL = "http://orchestrator:8500/webhook/gitea"
GITEA_INTERNAL_URL = "http://gitea:3000"
PLANE_INTERNAL_URL = "http://proxy"
EXIT_OK = 0
EXIT_MANUAL = 2
EXIT_ERROR = 1
# Минимумы хоста (синхронизированы с BUNDLED_SETUP §2; пороги preflight, TR-1).
MIN_RAM_GB = 8
MIN_DISK_GB = 40
MIN_CPUS = 4
# Тайм-ауты ожидания готовности (D5 шаг 3): миграции Plane — самые долгие.
READY_TIMEOUT_S = 180
PLANE_READY_TIMEOUT_S = 600
# Bundle-внутренние креды (upstream-имена, D2/FR-3) — генерирует bootstrap.
BUNDLE_SECRET_KEYS = (
"POSTGRES_PASSWORD",
"SECRET_KEY",
"RABBITMQ_DEFAULT_PASS",
"MINIO_ROOT_PASSWORD",
"GITEA_ADMIN_PASSWORD",
)
# Обязательные НЕсекретные ключи bundle-конфига (preflight, D5 шаг 1).
REQUIRED_BUNDLE_KEYS = (
"BUNDLE_PUBLIC_HOST",
"BUNDLE_ORCH_PORT",
"BUNDLE_PLANE_PORT",
"BUNDLE_GITEA_HTTP_PORT",
"ORCH_RUN_UID",
"ORCH_RUN_GID",
"ORCH_DOCKER_GID",
"ORCH_AGENT_HOME_DIR",
"GITEA_ADMIN_USERNAME",
)
# Webhook-секреты орка — выпускает ТОЛЬКО кирпич gen_secrets.py (AC-7).
WEBHOOK_SECRET_KEYS = ("ORCH_PLANE_WEBHOOK_SECRET", "ORCH_GITEA_WEBHOOK_SECRET")
# Sandbox-проект первого smoke (онбордится строго onboard_project.py, BR-6).
SANDBOX_DEFAULTS = {
"name": "Sandbox",
"repo": "sandbox",
"prefix": "SBX",
"stack": "python",
"test_cmd": "pytest -q",
"prod_port": "8600",
"staging_port": "8601",
}
class ManualStop(Exception):
"""Остановка на manual-step / незавершённом предусловии → exit 2."""
class BootstrapError(Exception):
"""Невосстановимая ошибка шага → exit 1."""
def log(msg: str) -> None:
"""Печать строки прогресса. Значения секретов сюда НЕ передаются (NFR-3)."""
print(msg, flush=True)
# --------------------------------------------------------------------------- #
# Чистые функции (unit-тесты — tests/test_bootstrap_script.py, TC-08)
# --------------------------------------------------------------------------- #
def parse_env(text: str) -> dict:
"""``KEY=value``-строки текста → словарь (комментарии/пустые — мимо)."""
out: dict = {}
for line in text.splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
out[key.strip()] = value.strip()
return out
def render_env(example_text: str, overrides: dict) -> str:
"""Рендер env-файла от канона-example: ``KEY=`` строки получают значения
overrides (та же строка, комментарии сохранены); ключи overrides, которых
в каноне нет, дописываются управляемым блоком в конец."""
used: set = set()
lines: list = []
for line in example_text.splitlines():
stripped = line.strip()
if stripped and not stripped.startswith("#") and "=" in stripped:
key = stripped.split("=", 1)[0].strip()
if key in overrides:
lines.append(f"{key}={overrides[key]}")
used.add(key)
continue
lines.append(line)
extra = [k for k in overrides if k not in used]
if extra:
lines.append("")
lines.append("# --- bootstrap_bundle.py (ORCH-103): управляемые ключи ---")
for key in extra:
lines.append(f"{key}={overrides[key]}")
return "\n".join(lines) + "\n"
def merge_missing_secrets(existing: dict, keys: tuple = BUNDLE_SECRET_KEYS,
force: bool = False, gen=None) -> dict:
"""Новые значения ТОЛЬКО для пустых/отсутствующих секрет-ключей (AC-8:
существующие не перетираются; ``force=True`` — явная регенерация всех)."""
gen = gen or (lambda key: secrets.token_hex(32 if key == "SECRET_KEY" else 16))
fresh: dict = {}
for key in keys:
if force or not existing.get(key, ""):
fresh[key] = gen(key)
return fresh
def preflight_verdict(facts: dict) -> tuple:
"""Чистый вердикт preflight (BR-7): ``(blockers, warnings, resume)``.
resume=True — на хосте уже есть тома/контейнеры с префиксом проекта:
не «грязь», а инициализированная инсталляция → ensure-режим (AC-8);
противоречивое состояние (есть тома, но нет конфига) — блокер.
"""
blockers: list = []
warnings: list = []
resume = bool(facts.get("leftovers"))
if not facts.get("docker"):
blockers.append("docker не найден — установите Docker Engine (BUNDLED_SETUP §3)")
if not facts.get("compose"):
blockers.append("docker compose v2 не найден (BUNDLED_SETUP §3)")
if not facts.get("env_exists"):
if resume:
blockers.append(
"противоречивое состояние: тома/контейнеры orchestrator-bundle уже "
"есть, а deploy/bundled/.env отсутствует — восстановите конфиг "
"или выполните полный сброс (BUNDLED_SETUP §13)"
)
else:
blockers.append(
"deploy/bundled/.env отсутствует — создайте: "
"cp deploy/bundled/.env.example deploy/bundled/.env (BUNDLED_SETUP §5)"
)
for key in facts.get("missing_keys", []):
blockers.append(f"deploy/bundled/.env: обязательный ключ {key} пуст")
if not resume:
for port in facts.get("busy_ports", []):
blockers.append(
f"порт {port} уже занят на хосте — освободите его или смените "
f"BUNDLE_*-порт в deploy/bundled/.env (BUNDLED_SETUP §2)"
)
ram = facts.get("ram_gb")
if ram is not None and ram < MIN_RAM_GB:
blockers.append(
f"RAM {ram:.1f} GB < минимума {MIN_RAM_GB} GB (Plane ≈ 14 контейнеров, "
f"BUNDLED_SETUP §2)"
)
disk = facts.get("disk_gb")
if disk is not None and disk < MIN_DISK_GB:
blockers.append(f"свободный диск {disk:.0f} GB < минимума {MIN_DISK_GB} GB")
cpus = facts.get("cpus")
if cpus is not None and cpus < MIN_CPUS:
warnings.append(f"CPU {cpus} < рекомендуемых {MIN_CPUS} vCPU — стек будет медленным")
if not facts.get("python3", True):
blockers.append("python3/venv недоступны — нужны для onboard-кирпича (TR-9)")
if not facts.get("claude_cli"):
warnings.append(
"Claude CLI/креды не найдены на хосте — стек поднимется, но конвейер "
"без LLM не поедет (BUNDLED_SETUP §8)"
)
return blockers, warnings, resume
def build_plan() -> list:
"""Нормативный план apply (нумерация — TRZ FR-2; механика — ADR-001 D5)."""
return [
("preflight", "fail-fast проверки хоста ДО любых мутаций (BR-7)"),
("secrets", "новые секреты инсталляции: gen_secrets.py + bundle-креды (FR-3)"),
("up", "подъём bundle-compose + ожидание готовности (миграции Plane/Gitea)"),
("init-gitea", "админ-бот + API-токен через `gitea admin ...` (полностью автоматом)"),
("init-plane", "instance-setup/workspace/API-токен — manual-step с верификацией"),
("plane-webhook", "workspace-webhook Plane → орк (ensure либо manual-step + проверка)"),
("onboard", "sandbox-проект строго через onboard_project.py apply+verify (BR-6)"),
("agent-git", "git-доступ агентов: клон sandbox-репо token-remote в /repos (D8)"),
("orch-env", "сборка корневых .env/.env.watchdog + пересоздание орка/watchdog"),
("health", "GET /health, /queue, /metrics + итоговая сводка PASS/FAIL"),
]
def build_arg_parser() -> argparse.ArgumentParser:
"""CLI: режимы plan (дефолт) / apply / verify + параметры sandbox."""
parser = argparse.ArgumentParser(
description="Bootstrap Bundled-инсталляции (ORCH-103). Канон — "
f"{DOC}."
)
parser.add_argument(
"mode", nargs="?", default="plan", choices=("plan", "apply", "verify"),
help="plan — дефолт, ноль мутаций; apply — прогон; verify — пост-проверка",
)
parser.add_argument(
"--force-secrets", action="store_true",
help="регенерировать СУЩЕСТВУЮЩИЕ bundle-креды (только ДО первого up!)",
)
parser.add_argument("--sandbox-name", default=SANDBOX_DEFAULTS["name"])
parser.add_argument("--sandbox-repo", default=SANDBOX_DEFAULTS["repo"])
parser.add_argument("--sandbox-prefix", default=SANDBOX_DEFAULTS["prefix"])
parser.add_argument("--sandbox-stack", default=SANDBOX_DEFAULTS["stack"])
parser.add_argument("--sandbox-test-cmd", default=SANDBOX_DEFAULTS["test_cmd"])
parser.add_argument("--sandbox-prod-port", default=SANDBOX_DEFAULTS["prod_port"])
parser.add_argument("--sandbox-staging-port", default=SANDBOX_DEFAULTS["staging_port"])
return parser
# --------------------------------------------------------------------------- #
# Тонкие обёртки subprocess/HTTP (единственные точки side-effects)
# --------------------------------------------------------------------------- #
def _run(cmd: list, input_text: str | None = None, env: dict | None = None,
timeout: int = 600) -> subprocess.CompletedProcess:
"""subprocess.run c capture; команды логируются БЕЗ секретов вызывающим."""
return subprocess.run(
cmd, input=input_text, env=env, capture_output=True, text=True,
timeout=timeout, check=False,
)
def _compose(*args: str, input_text: str | None = None,
timeout: int = 600) -> subprocess.CompletedProcess:
return _run(["docker", "compose", "-f", BUNDLE_COMPOSE, *args],
input_text=input_text, timeout=timeout)
def _http(url: str, headers: dict | None = None, timeout: int = 10) -> tuple:
"""GET url → (status|None, body). Никогда не бросает (poll-friendly)."""
req = urllib.request.Request(url, headers=headers or {})
try:
with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310
return resp.status, resp.read().decode("utf-8", "replace")
except urllib.error.HTTPError as e:
return e.code, ""
except (urllib.error.URLError, OSError, ValueError):
return None, ""
def _port_busy(port: int) -> bool:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(0.5)
return s.connect_ex(("127.0.0.1", port)) == 0
def _write_private(path: str, content: str) -> None:
"""Запись live-конфига: права 600, без печати содержимого (NFR-3)."""
with open(path, "w", encoding="utf-8") as f:
f.write(content)
os.chmod(path, 0o600)
log(f" записан {os.path.relpath(path, REPO_ROOT)} (права 600)")
def update_env_file(path: str, example_path: str, overrides: dict) -> None:
"""Идемпотентный ensure env-файла: существующий — обновить ключи overrides,
отсутствующий — отрендерить от канона-example. Никаких удалений."""
if os.path.isfile(path):
base = open(path, encoding="utf-8").read()
else:
base = open(example_path, encoding="utf-8").read()
_write_private(path, render_env(base, overrides))
# --------------------------------------------------------------------------- #
# Сбор фактов хоста (read-only; используется plan/apply/verify)
# --------------------------------------------------------------------------- #
def bundle_ports(bundle_env: dict) -> list:
out = []
for key, default in (("BUNDLE_ORCH_PORT", 8500), ("BUNDLE_PLANE_PORT", 8080),
("BUNDLE_GITEA_HTTP_PORT", 3000)):
try:
out.append(int(bundle_env.get(key) or default))
except ValueError:
out.append(default)
return out
def collect_facts(bundle_env: dict) -> dict:
"""Read-only снимок хоста для preflight_verdict (ни одной мутации)."""
docker = shutil.which("docker") is not None
compose = docker and _compose("version", timeout=30).returncode == 0
leftovers: list = []
if docker:
vols = _run(["docker", "volume", "ls", "--format", "{{.Name}}"], timeout=30)
names = _run(["docker", "ps", "-a", "--format", "{{.Names}}"], timeout=30)
for line in (vols.stdout + "\n" + names.stdout).splitlines():
if line.strip().startswith(PROJECT):
leftovers.append(line.strip())
ram_gb = None
try:
with open("/proc/meminfo", encoding="utf-8") as f:
for line in f:
if line.startswith("MemTotal:"):
ram_gb = int(line.split()[1]) / 1024 / 1024
break
except OSError:
pass
try:
disk_gb = shutil.disk_usage(REPO_ROOT).free / 2**30
except OSError:
disk_gb = None
env_exists = os.path.isfile(BUNDLE_ENV)
missing = [k for k in REQUIRED_BUNDLE_KEYS if not bundle_env.get(k, "")]
claude_ok = (
shutil.which("claude") is not None
or os.path.isdir(os.path.expanduser(
bundle_env.get("ORCH_HOST_CLAUDE_DIR", "") or "~/.claude"))
)
return {
"docker": docker,
"compose": compose,
"env_exists": env_exists,
"missing_keys": missing if env_exists else [],
"busy_ports": [p for p in bundle_ports(bundle_env) if _port_busy(p)],
"leftovers": leftovers,
"ram_gb": ram_gb,
"disk_gb": disk_gb,
"cpus": os.cpu_count(),
"python3": True, # мы уже исполняемся под python3
"claude_cli": claude_ok,
}
# --------------------------------------------------------------------------- #
# Manual-step контракт (D5/D7): инструкция → подтверждение → верификация
# --------------------------------------------------------------------------- #
def manual_checkpoint(title: str, instructions: list, verify, max_tries: int = 3):
"""Честный чекпоинт: печать точной инструкции; без TTY — немедленный exit 2
с той же инструкцией; с TTY — ожидание подтверждения и ВЕРИФИКАЦИЯ результата
(молчаливый пропуск запрещён). verify() → (ok, hint)."""
log(f"\n🖐 MANUAL-STEP: {title}")
for line in instructions:
log(f" {line}")
if not sys.stdin.isatty():
log(" Нет TTY: выполните шаги и перезапустите `apply` (resume = повторный запуск).")
raise ManualStop(title)
for _ in range(max_tries):
input(" Когда выполнено — нажмите Enter: ")
ok, hint = verify()
if ok:
log(" ✓ верификация пройдена")
return
log(f" ✗ верификация не прошла: {hint}")
raise ManualStop(f"{title}: верификация не прошла после {max_tries} попыток")
# --------------------------------------------------------------------------- #
# Шаги apply (step-движок check→ensure; каждый идемпотентен)
# --------------------------------------------------------------------------- #
def step_preflight(ctx: dict) -> str:
facts = collect_facts(ctx["bundle_env"])
blockers, warnings, resume = preflight_verdict(facts)
for w in warnings:
log(f"{w}")
if blockers:
for b in blockers:
log(f"{b}")
raise ManualStop("preflight: незавершённые предусловия хоста")
ctx["resume"] = resume
if resume:
log(" инсталляция уже существует — продолжаю в ensure-режиме (AC-8)")
return "ok"
def step_secrets(ctx: dict) -> str:
"""FR-3: bundle-креды (stdlib secrets) + webhook-секреты (gen_secrets.py)."""
force = ctx["args"].force_secrets
bundle_env = ctx["bundle_env"]
fresh = merge_missing_secrets(bundle_env, force=force)
# uid/gid/docker-gid хоста — дозаполняются фактическими значениями оператора
infra: dict = {}
if not bundle_env.get("ORCH_RUN_UID"):
infra["ORCH_RUN_UID"] = str(os.getuid())
if not bundle_env.get("ORCH_RUN_GID"):
infra["ORCH_RUN_GID"] = str(os.getgid())
if fresh or infra:
update_env_file(BUNDLE_ENV, BUNDLE_ENV_EXAMPLE, {**infra, **fresh})
ctx["bundle_env"] = parse_env(open(BUNDLE_ENV, encoding="utf-8").read())
log(f" bundle-креды выпущены: {', '.join(sorted(fresh)) or ''}")
else:
log(" bundle-креды уже на месте (не перетираю без --force-secrets)")
# webhook-секреты орка — СТРОГО кирпичом gen_secrets.py (AC-7)
root_env = ctx["root_env"]
if all(root_env.get(k) for k in WEBHOOK_SECRET_KEYS) and not force:
log(" webhook-секреты уже в .env — skip")
return "skipped"
with tempfile.TemporaryDirectory() as tmp:
frag_path = os.path.join(tmp, "fragment.env")
proc = _run([sys.executable, GEN_SECRETS, "--write", frag_path], timeout=60)
if proc.returncode != 0:
raise BootstrapError(f"gen_secrets.py отказал (rc={proc.returncode})")
fragment = parse_env(open(frag_path, encoding="utf-8").read())
overrides = {k: fragment[k] for k in WEBHOOK_SECRET_KEYS
if fragment.get(k) and (force or not root_env.get(k))}
update_env_file(ROOT_ENV, ROOT_ENV_EXAMPLE, overrides)
ctx["root_env"] = parse_env(open(ROOT_ENV, encoding="utf-8").read())
log(f" webhook-секреты выпущены: {', '.join(sorted(overrides)) or ''}")
return "ok"
def _wait_http(url: str, timeout_s: int, label: str, ok_statuses=(200,)) -> None:
deadline = time.monotonic() + timeout_s
while time.monotonic() < deadline:
status, _ = _http(url, timeout=5)
if status in ok_statuses:
log(f"{label} готов ({url})")
return
time.sleep(5)
tail = _compose("logs", "--tail", "30", label, timeout=60).stdout[-2000:]
raise BootstrapError(f"{label} не дождались за {timeout_s}с ({url}); хвост логов:\n{tail}")
def _wait_migrator(timeout_s: int) -> None:
deadline = time.monotonic() + timeout_s
name = f"{PROJECT}-migrator-1"
while time.monotonic() < deadline:
proc = _run(["docker", "inspect", "-f",
"{{.State.Status}} {{.State.ExitCode}}", name], timeout=30)
state = proc.stdout.strip()
if proc.returncode == 0 and state.startswith("exited"):
if state.endswith(" 0"):
log(" ✓ миграции Plane завершились (migrator exit 0)")
return
tail = _compose("logs", "--tail", "30", "migrator", timeout=60).stdout[-2000:]
raise BootstrapError(f"миграции Plane упали ({state}); хвост логов:\n{tail}")
time.sleep(5)
raise BootstrapError(f"миграции Plane не завершились за {timeout_s}с (TR-1: проверьте RAM/диск)")
def step_up(ctx: dict) -> str:
"""Подъём стека + ожидание готовности каждого слоя (D5 шаг 3)."""
for sub in ("data", "repos"):
os.makedirs(os.path.join(BUNDLE_DIR, sub), exist_ok=True)
proc = _compose("up", "-d", timeout=1800)
if proc.returncode != 0:
raise BootstrapError(f"docker compose up отказал:\n{proc.stderr[-2000:]}")
ports = dict(zip(("orch", "plane", "gitea"), bundle_ports(ctx["bundle_env"])))
_wait_http(f"http://127.0.0.1:{ports['gitea']}/api/healthz", READY_TIMEOUT_S, "gitea")
_wait_migrator(PLANE_READY_TIMEOUT_S)
_wait_http(f"http://127.0.0.1:{ports['plane']}/", PLANE_READY_TIMEOUT_S,
"proxy", ok_statuses=(200, 301, 302))
_wait_http(f"http://127.0.0.1:{ports['orch']}/health", READY_TIMEOUT_S, "orchestrator")
return "ok"
def step_init_gitea(ctx: dict) -> str:
"""D6: админ-бот + API-токен официальным CLI в контейнере; идемпотентно.
Branch protection НЕ настраивается (норматив D10 ORCH-009 / INV-4)."""
bundle_env, root_env = ctx["bundle_env"], ctx["root_env"]
user = bundle_env.get("GITEA_ADMIN_USERNAME", "orchestrator-bot")
gitea_port = bundle_ports(bundle_env)[2]
ctx["gitea_owner"] = user
proc = _compose(
"exec", "-T", "-u", "git", "gitea",
"gitea", "admin", "user", "create", "--admin",
"--username", user, "--password", bundle_env.get("GITEA_ADMIN_PASSWORD", ""),
"--email", f"{user}@{PROJECT}.local", "--must-change-password=false",
timeout=120,
)
blob = proc.stdout + proc.stderr
if proc.returncode == 0:
log(f" создан админ-бот Gitea: {user}")
elif "already exists" in blob:
log(f" админ-бот {user} уже существует — skip")
else:
raise BootstrapError(f"gitea admin user create отказал: {blob[-500:]}")
# API-токен (носитель — root .env, ORCH_GITEA_TOKEN)
token = root_env.get("ORCH_GITEA_TOKEN", "")
if token:
status, _ = _http(f"http://127.0.0.1:{gitea_port}/api/v1/user",
headers={"Authorization": f"token {token}"})
if status == 200:
log(" ORCH_GITEA_TOKEN валиден — skip")
return "skipped"
proc = _compose(
"exec", "-T", "-u", "git", "gitea",
"gitea", "admin", "user", "generate-access-token",
"--username", user, "--token-name", f"orchestrator-{int(time.time())}",
"--scopes", "all", "--raw",
timeout=120,
)
if proc.returncode != 0:
raise BootstrapError(f"generate-access-token отказал: {proc.stderr[-500:]}")
token = proc.stdout.strip().splitlines()[-1].strip()
status, _ = _http(f"http://127.0.0.1:{gitea_port}/api/v1/user",
headers={"Authorization": f"token {token}"})
if status != 200:
raise BootstrapError(f"свежий токен Gitea не прошёл верификацию (HTTP {status})")
update_env_file(ROOT_ENV, ROOT_ENV_EXAMPLE,
{"ORCH_GITEA_TOKEN": token, "ORCH_GITEA_OWNER": user})
ctx["root_env"] = parse_env(open(ROOT_ENV, encoding="utf-8").read())
log(" выпущен ORCH_GITEA_TOKEN (значение в .env, не печатается)")
return "ok"
def _verify_plane_token(plane_port: int, slug: str, token: str) -> tuple:
status, _ = _http(
f"http://127.0.0.1:{plane_port}/api/v1/workspaces/{slug}/projects/",
headers={"X-API-Key": token}, timeout=15,
)
if status == 200:
return True, ""
return False, f"GET /api/v1/workspaces/{slug}/projects/ → HTTP {status}"
def step_init_plane(ctx: dict) -> str:
"""D7: instance-setup / workspace / API-токен — честные manual-step
чекпоинты (Plane CE не даёт API первичной инициализации)."""
bundle_env, root_env = ctx["bundle_env"], ctx["root_env"]
host = bundle_env.get("BUNDLE_PUBLIC_HOST", "localhost")
plane_port = bundle_ports(bundle_env)[1]
slug = root_env.get("ORCH_PLANE_WORKSPACE_SLUG", "")
token = root_env.get("ORCH_PLANE_API_TOKEN", "")
if slug and token and _verify_plane_token(plane_port, slug, token)[0]:
log(" workspace и ORCH_PLANE_API_TOKEN валидны — skip")
return "skipped"
def _instance_done():
status, body = _http(f"http://127.0.0.1:{plane_port}/api/instances/", timeout=10)
if status == 200 and '"is_setup_done":true' in body.replace(" ", ""):
return True, ""
if status == 200:
return False, "instance setup ещё не завершён (is_setup_done != true)"
# эндпоинт недоступен в этой сборке CE → деградация: живость UI
ui, _ = _http(f"http://127.0.0.1:{plane_port}/", timeout=10)
return (ui in (200, 301, 302)), f"Plane UI отвечает HTTP {ui}"
manual_checkpoint(
"Plane: instance setup (первый администратор)",
[f"Откройте http://{host}:{plane_port}/ и зарегистрируйте первого",
"пользователя — он станет администратором инстанса (Plane CE)."],
_instance_done,
)
if not sys.stdin.isatty():
raise ManualStop("Plane: workspace/API-токен требуют интерактивного ввода")
slug = input(" Введите slug созданного workspace: ").strip()
log(" Plane UI → Workspace Settings → API tokens → выпустите токен.")
token = getpass.getpass(" Вставьте ORCH_PLANE_API_TOKEN (ввод скрыт): ").strip()
ok, hint = _verify_plane_token(plane_port, slug, token)
if not ok:
raise ManualStop(f"Plane: токен/slug не прошли верификацию ({hint})")
update_env_file(ROOT_ENV, ROOT_ENV_EXAMPLE,
{"ORCH_PLANE_WORKSPACE_SLUG": slug, "ORCH_PLANE_API_TOKEN": token})
ctx["root_env"] = parse_env(open(ROOT_ENV, encoding="utf-8").read())
log(" ✓ workspace и ORCH_PLANE_API_TOKEN верифицированы (значения в .env)")
return "ok"
def _psql(sql: str, bundle_env: dict) -> subprocess.CompletedProcess:
"""SQL в plane-db через stdin (секреты не попадают в argv, NFR-3)."""
return _compose(
"exec", "-T", "plane-db", "psql",
"-U", bundle_env.get("POSTGRES_USER", "plane"),
"-d", bundle_env.get("POSTGRES_DB", "plane"),
"-t", "-A", "-v", "ON_ERROR_STOP=1",
input_text=sql, timeout=60,
)
def step_plane_webhook(ctx: dict) -> str:
"""Workspace-webhook Plane→орк. CE не даёт API → ensure прямой записью в
Postgres инсталляции (прогрессивная автоматизация D7: контракт чекпоинта —
та же верификация SELECT'ом); схема — канон LITE_SETUP §5.4 (путь Б)."""
bundle_env, root_env = ctx["bundle_env"], ctx["root_env"]
secret = root_env.get("ORCH_PLANE_WEBHOOK_SECRET", "")
slug = root_env.get("ORCH_PLANE_WORKSPACE_SLUG", "")
if not (secret and slug):
raise BootstrapError("нет ORCH_PLANE_WEBHOOK_SECRET/ORCH_PLANE_WORKSPACE_SLUG в .env")
def _exists() -> tuple:
probe = _psql(
f"SELECT count(*) FROM webhooks WHERE url='{WEBHOOK_PLANE_URL}' "
f"AND deleted_at IS NULL;", bundle_env)
ok = probe.returncode == 0 and probe.stdout.strip().isdigit() \
and int(probe.stdout.strip()) > 0
return ok, f"SELECT по webhooks: rc={probe.returncode}"
if _exists()[0]:
log(" workspace-webhook уже зарегистрирован — skip")
return "skipped"
wid = _psql(f"SELECT id FROM workspaces WHERE slug='{slug}';", bundle_env)
workspace_id = wid.stdout.strip().splitlines()[0].strip() if wid.stdout.strip() else ""
if wid.returncode == 0 and workspace_id:
ins = _psql(
"INSERT INTO webhooks (id, created_at, updated_at, deleted_at, "
"workspace_id, url, is_active, secret_key, project, issue, module, "
"cycle, issue_comment, is_internal, version) VALUES "
f"('{uuid.uuid4()}', NOW(), NOW(), NULL, '{workspace_id}', "
f"'{WEBHOOK_PLANE_URL}', true, '{secret}', true, true, false, false, "
"true, false, 'v1');", bundle_env)
if ins.returncode == 0 and _exists()[0]:
log(f" ✓ workspace-webhook зарегистрирован: {WEBHOOK_PLANE_URL}")
return "ok"
log(" прямая регистрация не удалась — честный manual-step (fail-safe)")
manual_checkpoint(
"Plane: workspace-webhook (CE без API)",
[f"Workspace Settings → Webhooks → Add Webhook: URL {WEBHOOK_PLANE_URL},",
"секрет — значение ORCH_PLANE_WEBHOOK_SECRET из корневого .env,",
"события Issue + Issue Comment (канон — LITE_SETUP §5.4)."],
_exists,
)
return "ok"
def _ensure_venv() -> str:
"""Host-venv для onboard-кирпича (канон ONBOARDING; ensure, TR-9)."""
if not os.path.exists(VENV_PY):
proc = _run([sys.executable, "-m", "venv", VENV_DIR], timeout=300)
if proc.returncode != 0:
raise BootstrapError(f"python3 -m venv отказал: {proc.stderr[-500:]}")
probe = _run([VENV_PY, "-c", "import httpx, pydantic"], timeout=60)
if probe.returncode != 0:
log(" ставлю зависимости onboard-кирпича в .venv (requirements.txt)…")
proc = _run([VENV_PY, "-m", "pip", "install", "-q", "-r", REQUIREMENTS],
timeout=1200)
if proc.returncode != 0:
raise BootstrapError(f"pip install отказал: {proc.stderr[-500:]}")
return VENV_PY
def _onboard_env(ctx: dict) -> dict:
"""Окружение onboard-субпроцесса: host-видимые URL bundle-инсталляции
(pydantic env-переменные перекрывают env_file, D7)."""
bundle_env, root_env = ctx["bundle_env"], ctx["root_env"]
host = bundle_env.get("BUNDLE_PUBLIC_HOST", "localhost")
plane_p, gitea_p = bundle_ports(bundle_env)[1:]
return {
**os.environ,
"ORCH_PLANE_API_URL": f"http://127.0.0.1:{plane_p}",
"ORCH_PLANE_WEB_URL": f"http://{host}:{plane_p}",
"ORCH_PLANE_WORKSPACE_SLUG": root_env.get("ORCH_PLANE_WORKSPACE_SLUG", ""),
"ORCH_PLANE_API_TOKEN": root_env.get("ORCH_PLANE_API_TOKEN", ""),
"ORCH_GITEA_URL": f"http://127.0.0.1:{gitea_p}",
"ORCH_GITEA_PUBLIC_URL": f"http://{host}:{gitea_p}",
"ORCH_GITEA_OWNER": root_env.get("ORCH_GITEA_OWNER", ""),
"ORCH_GITEA_TOKEN": root_env.get("ORCH_GITEA_TOKEN", ""),
"ORCH_GITEA_WEBHOOK_SECRET": root_env.get("ORCH_GITEA_WEBHOOK_SECRET", ""),
}
def _onboard_args(ctx: dict, mode: str) -> list:
a = ctx["args"]
return [
ONBOARD, mode,
"--name", a.sandbox_name, "--repo", a.sandbox_repo,
"--gitea-owner", ctx["root_env"].get("ORCH_GITEA_OWNER", ""),
"--prefix", a.sandbox_prefix, "--stack", a.sandbox_stack,
"--test-cmd", a.sandbox_test_cmd,
"--prod-port", a.sandbox_prod_port, "--staging-port", a.sandbox_staging_port,
"--webhook-url", WEBHOOK_GITEA_URL,
"--env-file", ROOT_ENV, "--json",
]
def step_onboard(ctx: dict) -> str:
"""BR-6/AC-7: статусы/лейблы/репо/вебхуки — СТРОГО onboard_project.py."""
venv_py = _ensure_venv()
env = _onboard_env(ctx)
proc = _run([venv_py, *_onboard_args(ctx, "apply")], env=env, timeout=900)
if proc.returncode not in (0, 2):
raise BootstrapError(f"onboard apply отказал (rc={proc.returncode}): "
f"{proc.stderr[-800:]}")
try:
report = json.loads(proc.stdout)
except ValueError:
raise BootstrapError("onboard apply вернул непарсимый отчёт")
merged = ""
for instr in report.get("instructions", []):
if isinstance(instr, str) and instr.startswith("ORCH_PROJECTS_JSON="):
merged = instr.split("=", 1)[1]
if merged:
update_env_file(ROOT_ENV, ROOT_ENV_EXAMPLE, {"ORCH_PROJECTS_JSON": merged})
ctx["root_env"] = parse_env(open(ROOT_ENV, encoding="utf-8").read())
log(" реестр ORCH_PROJECTS_JSON записан в .env (merged-вывод onboard)")
manual = [s for s in report.get("steps", [])
if s.get("status") == "manual-step"
and s.get("id") not in ("plane.workspace-webhook",)]
if manual:
log(" onboard оставил ручные шаги (см. отчёт): "
+ ", ".join(s.get("id", "?") for s in manual))
verify = _run([venv_py, *_onboard_args(ctx, "verify")], env=env, timeout=300)
if verify.returncode == 1:
raise BootstrapError(f"onboard verify отказал: {verify.stderr[-800:]}")
log(f" onboard verify: exit {verify.returncode} "
f"(0 — чисто; 2 — остались ручные пункты отчёта)")
ctx["onboard_manual"] = bool(manual) or verify.returncode == 2
return "ok"
def step_agent_git(ctx: dict) -> str:
"""D8: клон sandbox-репо token-remote ВНУТРИ контейнера орка (origin —
in-network gitea:3000, агенты наследуют его для push/fetch)."""
repo = ctx["args"].sandbox_repo
owner = ctx["root_env"].get("ORCH_GITEA_OWNER", "")
token = ctx["root_env"].get("ORCH_GITEA_TOKEN", "")
probe = _compose("exec", "-T", "orchestrator", "test", "-d",
f"/repos/{repo}/.git", timeout=30)
if probe.returncode == 0:
log(f" /repos/{repo} уже клонирован — skip")
return "skipped"
url = f"{GITEA_INTERNAL_URL.split('://')[0]}://oauth2:{token}@" \
f"{GITEA_INTERNAL_URL.split('://')[1]}/{owner}/{repo}.git"
proc = _compose("exec", "-T", "orchestrator",
"git", "clone", url, f"/repos/{repo}", timeout=300)
if proc.returncode != 0:
raise BootstrapError(
f"клон {repo} в /repos не удался (лог замаскирован): rc={proc.returncode}")
log(f" ✓ /repos/{repo} клонирован (token-remote, TR-7: права локального каталога)")
return "ok"
def step_orch_env(ctx: dict) -> str:
"""D5 шаг 8: корневой .env (канон Lite 1:1) + .env.watchdog; пересоздание
орка/watchdog для подхвата конфига."""
bundle_env = ctx["bundle_env"]
host = bundle_env.get("BUNDLE_PUBLIC_HOST", "localhost")
plane_p, gitea_p = bundle_ports(bundle_env)[1:]
overrides = {
# in-network машинные URL (D4) + публичные от BUNDLE_PUBLIC_HOST
"ORCH_PLANE_API_URL": PLANE_INTERNAL_URL,
"ORCH_PLANE_WEB_URL": f"http://{host}:{plane_p}",
"ORCH_GITEA_URL": GITEA_INTERNAL_URL,
"ORCH_GITEA_PUBLIC_URL": f"http://{host}:{gitea_p}",
# когерентность дублируемых ключей — механически (TR-8)
"ORCH_AGENT_HOME_DIR": bundle_env.get("ORCH_AGENT_HOME_DIR", "/home/orchestrator"),
"ORCH_RUN_UID": bundle_env.get("ORCH_RUN_UID", "1000"),
"ORCH_RUN_GID": bundle_env.get("ORCH_RUN_GID", "1000"),
"ORCH_DOCKER_GID": bundle_env.get("ORCH_DOCKER_GID", "999"),
"ORCH_HOST_REPOS_DIR": os.path.join(BUNDLE_DIR, "repos"),
"ORCH_HOST_CLAUDE_CODE_DIR": bundle_env.get("ORCH_HOST_CLAUDE_CODE_DIR", ""),
"ORCH_HOST_NODE_BIN": bundle_env.get("ORCH_HOST_NODE_BIN", ""),
"ORCH_HOST_CLAUDE_DIR": bundle_env.get("ORCH_HOST_CLAUDE_DIR", ""),
"ORCH_HOST_CLAUDE_JSON": bundle_env.get("ORCH_HOST_CLAUDE_JSON", ""),
# деплой-машинерия нашего хоста в bundle структурно спит (D4)
"ORCH_DEPLOY_SSH_HOST": "",
}
update_env_file(ROOT_ENV, ROOT_ENV_EXAMPLE, overrides)
ctx["root_env"] = parse_env(open(ROOT_ENV, encoding="utf-8").read())
if not os.path.isfile(WATCHDOG_ENV):
# Telegram-ключи опциональны: пусто = деградация только нотификаций
update_env_file(WATCHDOG_ENV, WATCHDOG_ENV_EXAMPLE, {})
proc = _compose("up", "-d", "--force-recreate",
"orchestrator", "orchestrator-watchdog", timeout=600)
if proc.returncode != 0:
raise BootstrapError(f"пересоздание орка/watchdog отказало:\n{proc.stderr[-1000:]}")
log(" ✓ орк и watchdog пересозданы с собранным конфигом")
return "ok"
def step_health(ctx: dict) -> str:
orch_p = bundle_ports(ctx["bundle_env"])[0]
failures = []
for path in ("/health", "/queue", "/metrics"):
url = f"http://127.0.0.1:{orch_p}{path}"
status, body = None, ""
deadline = time.monotonic() + 60
while time.monotonic() < deadline:
status, body = _http(url, timeout=5)
if status == 200:
break
time.sleep(3)
ok = status == 200
if path != "/health" and ok:
try:
json.loads(body)
except ValueError:
ok = False
log(f" GET {path}{'PASS' if ok else f'FAIL (HTTP {status})'}")
if not ok:
failures.append(path)
if failures:
raise BootstrapError(f"health-контракты не зелёные: {', '.join(failures)}")
return "ok"
APPLY_STEPS = (
("preflight", step_preflight),
("secrets", step_secrets),
("up", step_up),
("init-gitea", step_init_gitea),
("init-plane", step_init_plane),
("plane-webhook", step_plane_webhook),
("onboard", step_onboard),
("agent-git", step_agent_git),
("orch-env", step_orch_env),
("health", step_health),
)
# --------------------------------------------------------------------------- #
# Режимы
# --------------------------------------------------------------------------- #
def _load_ctx(args: argparse.Namespace) -> dict:
bundle_env = parse_env(open(BUNDLE_ENV, encoding="utf-8").read()) \
if os.path.isfile(BUNDLE_ENV) else {}
root_env = parse_env(open(ROOT_ENV, encoding="utf-8").read()) \
if os.path.isfile(ROOT_ENV) else {}
return {"args": args, "bundle_env": bundle_env, "root_env": root_env,
"results": {}}
def run_plan(ctx: dict) -> int:
log("== bootstrap_bundle: план apply (ноль мутаций) ==")
for i, (name, summary) in enumerate(build_plan(), 1):
log(f" {i}. {name:<14} {summary}")
facts = collect_facts(ctx["bundle_env"])
blockers, warnings, resume = preflight_verdict(facts)
log("\n-- preflight-диагностика (read-only):")
for w in warnings:
log(f"{w}")
for b in blockers:
log(f"{b}")
if resume:
log(" найдены тома/контейнеры orchestrator-bundle: apply пойдёт в ensure-режиме")
if not blockers:
log(" ✓ предусловия хоста выполнены — запускайте: "
"python3 scripts/bootstrap_bundle.py apply")
return EXIT_OK
log(f" итог: {len(blockers)} блокеров — устраните и повторите (канон — {DOC})")
return EXIT_MANUAL
def run_apply(ctx: dict) -> int:
log("== bootstrap_bundle: apply ==")
for name, fn in APPLY_STEPS:
log(f"\n→ шаг {name}")
status = fn(ctx)
ctx["results"][name] = status
log("\n== итоговая сводка ==")
for name, _ in APPLY_STEPS:
log(f" [{ctx['results'].get(name, ''):>8}] {name}")
if ctx.get("onboard_manual"):
log("\n🖐 Остались ручные пункты onboard-отчёта (порядок статусов на доске и т.п.)")
log(" Выполните их и перезапустите verify. Exit 2 (незавершённые шаги).")
return EXIT_MANUAL
log(f"\n✓ Bundled-инсталляция готова. Следующий шаг — smoke: {DOC} §11 "
"(чек-лист REPLICATION.md §4).")
return EXIT_OK
def run_verify(ctx: dict) -> int:
"""Read-only пост-проверка: health-контракты + onboard verify."""
log("== bootstrap_bundle: verify (read-only) ==")
orch_p = bundle_ports(ctx["bundle_env"])[0]
failed = False
for path in ("/health", "/queue", "/metrics"):
status, _ = _http(f"http://127.0.0.1:{orch_p}{path}", timeout=10)
ok = status == 200
failed = failed or not ok
log(f" GET {path}{'PASS' if ok else f'FAIL (HTTP {status})'}")
if os.path.exists(VENV_PY) and ctx["root_env"].get("ORCH_PLANE_API_TOKEN"):
proc = _run([VENV_PY, *_onboard_args(ctx, "verify")],
env=_onboard_env(ctx), timeout=300)
log(f" onboard verify → exit {proc.returncode}")
failed = failed or proc.returncode == 1
if proc.returncode == 2:
return EXIT_MANUAL
else:
log(" onboard verify пропущен (нет .venv или ORCH_PLANE_API_TOKEN) → exit 2")
return EXIT_MANUAL
return EXIT_ERROR if failed else EXIT_OK
def main(argv: list | None = None) -> int:
args = build_arg_parser().parse_args(argv)
ctx = _load_ctx(args)
try:
if args.mode == "plan":
return run_plan(ctx)
if args.mode == "verify":
return run_verify(ctx)
return run_apply(ctx)
except ManualStop as e:
log(f"\n🖐 ОСТАНОВКА (exit {EXIT_MANUAL}): {e}")
log(" Выполните шаг и перезапустите apply — завершённые шаги будут пропущены.")
return EXIT_MANUAL
except BootstrapError as e:
log(f"\n✗ ОШИБКА (exit {EXIT_ERROR}): {e}")
return EXIT_ERROR
except KeyboardInterrupt:
log(f"\n✗ прервано оператором (exit {EXIT_ERROR})")
return EXIT_ERROR
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,192 @@
#!/usr/bin/env python3
"""ORCH-011 (ADR-001 D4): сборка `.pptx` из слайдо-источника витрины.
Источник истины — `docs/overview/presentation.md` (машинно-парсимая
слайдо-структура: `## Слайд N: Заголовок` + тезисы `- ...` + опциональная
строка `> Визуал: ...`). Скрипт собирает редактируемую PowerPoint-презентацию
в тёмном дизайне (D-1 Владельца): тёмный фон, светлый текст, один акцентный
цвет, системные шрифты с полной кириллицей.
Канон (D4/D5):
- запуск ТОЛЬКО вне рантайма конвейера (host/dev venv, явный запуск человеком —
паттерн ORCH-009); `python-pptx` НЕ входит в requirements*/Dockerfile (NFR-2);
- `parse_slides` — чистая stdlib-функция БЕЗ импорта pptx: её импортирует
`tests/test_system_docs.py` (один парсер = один источник истины о формате);
- рендерер импортирует pptx ЛЕНИВО внутри `build_pptx`;
- дефолтный выход — `build/orchestrator-overview.pptx` (в `.gitignore`;
собранный бинарь в git НЕ коммитится — D5).
Процедура запуска (канон «команда + Проверка:») — `docs/overview/presentation.md`,
раздел «Как собрать .pptx».
"""
from __future__ import annotations
import argparse
import re
from dataclasses import dataclass, field
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_SOURCE = REPO_ROOT / "docs" / "overview" / "presentation.md"
DEFAULT_OUTPUT = REPO_ROOT / "build" / "orchestrator-overview.pptx"
# Тёмная тема (D4): фон ~#1F1F2E, светлый текст, один акцент, приглушённый серый.
DARK_BG = "1F1F2E"
TEXT_MAIN = "F2F2F7"
ACCENT = "8AB4F8"
TEXT_MUTED = "9A9AAD"
FONT_NAME = "Calibri" # системный шрифт с полной кириллицей (D4)
_SLIDE_RE = re.compile(r"^##\s+Слайд\s+(\d+)\s*:\s*(.+?)\s*$")
_BULLET_RE = re.compile(r"^-\s+(.+?)\s*$")
_VISUAL_RE = re.compile(r"^>\s*Визуал\s*:\s*(.+?)\s*$")
_ANY_HEADING_RE = re.compile(r"^#{1,6}\s+")
@dataclass
class Slide:
"""Один слайд источника: номер, заголовок, тезисы, подпись визуала."""
number: int
title: str
bullets: list[str] = field(default_factory=list)
visual: str | None = None
def parse_slides(text: str) -> list[Slide]:
"""Разобрать слайдо-источник в список :class:`Slide` (чистая, stdlib-only).
Формат (D4): слайд открывается строкой ``## Слайд N: Заголовок``; его тезисы —
строки ``- ...``; опциональная подпись визуала — ``> Визуал: ...``. Любой
другой markdown-заголовок (например, раздел «Как собрать .pptx») завершает
текущий слайд — служебные разделы источника в слайды не попадают.
"""
slides: list[Slide] = []
current: Slide | None = None
for line in text.splitlines():
m = _SLIDE_RE.match(line)
if m:
current = Slide(number=int(m.group(1)), title=m.group(2))
slides.append(current)
continue
if _ANY_HEADING_RE.match(line):
current = None # служебный раздел — не слайд
continue
if current is None:
continue
bullet = _BULLET_RE.match(line)
if bullet:
current.bullets.append(bullet.group(1))
continue
visual = _VISUAL_RE.match(line)
if visual:
current.visual = visual.group(1)
return slides
def build_pptx(slides: list[Slide], output: Path) -> None:
"""Собрать `.pptx` в тёмном дизайне из распарсенных слайдов.
Импорт `pptx` — ленивый (D4): без установленного `python-pptx` модуль
остаётся импортируемым (нужно тестам), а сборка честно подсказывает
`pip install python-pptx`. Текст пишется настоящими редактируемыми
run'ами — кириллица не растрируется, слайды правятся руками.
"""
from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.util import Inches, Pt
prs = Presentation()
prs.slide_width = Inches(13.333) # 16:9
prs.slide_height = Inches(7.5)
blank_layout = prs.slide_layouts[6]
for slide_def in slides:
slide = prs.slides.add_slide(blank_layout)
slide.background.fill.solid()
slide.background.fill.fore_color.rgb = RGBColor.from_string(DARK_BG)
title_box = slide.shapes.add_textbox(
Inches(0.6), Inches(0.45), prs.slide_width - Inches(1.2), Inches(1.2)
)
title_tf = title_box.text_frame
title_tf.word_wrap = True
run = title_tf.paragraphs[0].add_run()
run.text = slide_def.title
run.font.size = Pt(34)
run.font.bold = True
run.font.name = FONT_NAME
run.font.color.rgb = RGBColor.from_string(ACCENT)
body_box = slide.shapes.add_textbox(
Inches(0.8), Inches(1.85), prs.slide_width - Inches(1.6), Inches(4.4)
)
body_tf = body_box.text_frame
body_tf.word_wrap = True
for i, bullet in enumerate(slide_def.bullets):
para = body_tf.paragraphs[0] if i == 0 else body_tf.add_paragraph()
run = para.add_run()
run.text = f"{bullet}"
run.font.size = Pt(20)
run.font.name = FONT_NAME
run.font.color.rgb = RGBColor.from_string(TEXT_MAIN)
para.space_after = Pt(10)
if slide_def.visual:
cap_box = slide.shapes.add_textbox(
Inches(0.8), Inches(6.55), prs.slide_width - Inches(1.6), Inches(0.6)
)
cap_tf = cap_box.text_frame
cap_tf.word_wrap = True
run = cap_tf.paragraphs[0].add_run()
run.text = f"Визуал: {slide_def.visual}"
run.font.size = Pt(13)
run.font.italic = True
run.font.name = FONT_NAME
run.font.color.rgb = RGBColor.from_string(TEXT_MUTED)
output.parent.mkdir(parents=True, exist_ok=True)
prs.save(str(output))
def main(argv: list[str] | None = None) -> int:
"""CLI: распарсить источник, собрать `.pptx`, напечатать число слайдов."""
parser = argparse.ArgumentParser(
description="Сборка docs/overview/presentation.md -> .pptx (тёмный дизайн, ORCH-011 D4)."
)
parser.add_argument(
"--source",
type=Path,
default=DEFAULT_SOURCE,
help=f"слайдо-источник (default: {DEFAULT_SOURCE.relative_to(REPO_ROOT)})",
)
parser.add_argument(
"--out",
type=Path,
default=DEFAULT_OUTPUT,
help=f"выходной .pptx (default: {DEFAULT_OUTPUT.relative_to(REPO_ROOT)})",
)
args = parser.parse_args(argv)
if not args.source.is_file():
print(f"ОШИБКА: источник не найден: {args.source}")
return 1
slides = parse_slides(args.source.read_text(encoding="utf-8"))
if not slides:
print(f"ОШИБКА: в {args.source} не найдено ни одного слайда (формат: '## Слайд N: ...')")
return 1
try:
build_pptx(slides, args.out)
except ImportError:
print(
"ОШИБКА: python-pptx не установлен. Сборка выполняется в одноразовом "
"dev-venv ВНЕ прод-образа (NFR-2): pip install python-pptx"
)
return 1
print(f"Собрано слайдов: {len(slides)}{args.out}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -282,6 +282,21 @@ def test_reviewer_carries_overview_docs_axis():
)
def test_reviewer_overview_axis_covers_system_showcase():
"""ORCH-011 (ADR-001 D7): the ORCH-079 overview-docs axis explicitly extends
to the system showcase `docs/overview/` — a PR changing functionality described
in the showcase without updating it must yield a finding >= P1. Guarded here
because the axis history (ORCH-079) shows overview docs rot unless named
explicitly in the prompt."""
text = _read("reviewer")
assert "docs/overview/" in text, (
"reviewer.md does not extend the overview-docs axis to the docs/overview/ showcase"
)
assert "ORCH-011" in text, (
"reviewer.md does not anchor the showcase extension to ORCH-011"
)
# --------------------------------------------------------------------------- #
# ORCH-092 (epilogue of epic ORCH-52): prompt audit of the 6 agents —
# de-hardcode date/model, gate-name parity, escalation sections, dead-line

View File

@@ -0,0 +1,247 @@
"""ORCH-103 (TC-07/TC-08, AC-7/AC-8): структурные и unit-проверки
`scripts/bootstrap_bundle.py`.
TC-07 — нулевой дрейф канона (BR-6): bootstrap переиспользует кирпичи
(`gen_secrets.py` — webhook-секреты, `onboard_project.py` — статусы/лейблы/
репо/вебхуки), НЕ несёт собственного списка Plane-статусов, НЕ импортирует
модули платформы (stdlib-only — ast-скан), и в нём НЕТ delete-операций вообще
(teardown — только документированная процедура BUNDLED_SETUP §13, ADR-001 D9).
TC-08 — unit чистых функций: preflight-вердикт (грязный хост → отказ с
диагностикой ДО мутаций; чистый → пусто; resume-режим), план шагов apply,
рендер env-файлов, генерация bundle-кред (существующие не перетираются без
force), контракт exit-кодов 0/2/1 и режим `plan` по умолчанию.
Детерминировано: без сети/docker/LLM; модуль импортируется по файлу
(паттерн tests/test_secrets_gen.py), его import не имеет side effects.
"""
import ast
import importlib.util
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPT = REPO_ROOT / "scripts/bootstrap_bundle.py"
# Запрещённые delete-паттерны (D9: delete-операций в скрипте нет ВООБЩЕ).
FORBIDDEN_DELETE_NEEDLES = (
"volume rm",
"rm -rf",
"down -v",
"compose down",
"rmtree",
"os.remove",
".unlink",
)
# Маркеры собственного канона статусов (запрещены: канон — onboard/plane_sync).
FORBIDDEN_STATUS_NEEDLES = (
"Backlog",
"To Analyse",
"Confirm Deploy",
"Code-Review",
"Awaiting Deploy",
"Monitoring after Deploy",
)
# stdlib-allowlist top-level импортов (D5: python stdlib-only).
STDLIB_ALLOWED = {
"argparse", "dataclasses", "getpass", "json", "os", "pathlib", "re",
"secrets", "shutil", "socket", "subprocess", "sys", "tempfile", "time",
"urllib", "uuid",
}
def _source() -> str:
assert SCRIPT.is_file(), "scripts/bootstrap_bundle.py отсутствует (FR-2)"
return SCRIPT.read_text(encoding="utf-8")
def _load_module():
spec = importlib.util.spec_from_file_location("bootstrap_bundle", SCRIPT)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
# ---------------------------------------------------------------------------
# TC-07: кирпичи переиспользованы, дрейфа канона нет, delete-операций нет.
# ---------------------------------------------------------------------------
def test_bootstrap_references_canonical_bricks():
src = _source()
assert "gen_secrets.py" in src, "webhook-секреты обязаны идти через gen_secrets.py (AC-7)"
assert "onboard_project.py" in src, "онбординг обязан идти через onboard_project.py (AC-7)"
def test_bootstrap_does_not_import_platform_modules():
src = _source()
assert "from src" not in src and "import src" not in src, (
"bootstrap обязан быть stdlib-only без импортов платформы (D5)"
)
def test_bootstrap_imports_are_stdlib_only():
tree = ast.parse(_source())
offenders = []
for node in ast.walk(tree):
if isinstance(node, ast.Import):
offenders.extend(a.name.split(".")[0] for a in node.names
if a.name.split(".")[0] not in STDLIB_ALLOWED)
elif isinstance(node, ast.ImportFrom) and node.module:
top = node.module.split(".")[0]
if top not in STDLIB_ALLOWED:
offenders.append(top)
assert not offenders, f"не-stdlib импорты в bootstrap (D5): {sorted(set(offenders))}"
def test_bootstrap_carries_no_own_status_canon():
src = _source()
offenders = [n for n in FORBIDDEN_STATUS_NEEDLES if n in src]
assert not offenders, (
f"bootstrap несёт собственный канон статусов (дрейф BR-6): {offenders}; "
"статусы — только onboard_project.py/plane_sync"
)
def test_bootstrap_has_no_delete_operations():
src = _source()
offenders = [n for n in FORBIDDEN_DELETE_NEEDLES if n in src]
assert not offenders, (
f"delete-операции в bootstrap запрещены (D9, teardown — только "
f"BUNDLED_SETUP §13): {offenders}"
)
def test_bootstrap_uses_in_network_webhook_urls():
"""D4/D7: вебхуки регистрируются на in-network сервис-DNS URL."""
mod = _load_module()
assert mod.WEBHOOK_PLANE_URL == "http://orchestrator:8500/webhook/plane"
assert mod.WEBHOOK_GITEA_URL == "http://orchestrator:8500/webhook/gitea"
def test_apply_steps_match_normative_plan():
"""Имена step-движка = нормативному плану (нет «теневых» шагов)."""
mod = _load_module()
assert [n for n, _ in mod.APPLY_STEPS] == [n for n, _ in mod.build_plan()]
# ---------------------------------------------------------------------------
# TC-08: unit чистых функций + контракты CLI/exit.
# ---------------------------------------------------------------------------
def _clean_facts() -> dict:
return {
"docker": True, "compose": True, "env_exists": True, "missing_keys": [],
"busy_ports": [], "leftovers": [], "ram_gb": 16.0, "disk_gb": 100.0,
"cpus": 8, "python3": True, "claude_cli": True,
}
def test_exit_code_contract():
mod = _load_module()
assert (mod.EXIT_OK, mod.EXIT_MANUAL, mod.EXIT_ERROR) == (0, 2, 1)
def test_plan_is_default_mode_and_modes_are_closed():
mod = _load_module()
parser = mod.build_arg_parser()
assert parser.parse_args([]).mode == "plan" # дефолт — ноль мутаций
assert parser.parse_args(["apply"]).mode == "apply"
assert parser.parse_args(["verify"]).mode == "verify"
assert parser.parse_args([]).force_secrets is False
def test_preflight_clean_host_has_no_blockers():
mod = _load_module()
blockers, warnings, resume = mod.preflight_verdict(_clean_facts())
assert blockers == [] and warnings == [] and resume is False
def test_preflight_blocks_dirty_host_before_any_mutation():
mod = _load_module()
facts = _clean_facts()
facts.update(docker=False, busy_ports=[8080], ram_gb=4.0, disk_gb=10.0,
env_exists=False)
blockers, _, _ = mod.preflight_verdict(facts)
blob = "\n".join(blockers)
assert "docker" in blob
assert "8080" in blob
assert str(mod.MIN_RAM_GB) in blob
assert str(mod.MIN_DISK_GB) in blob
assert ".env" in blob
def test_preflight_existing_install_is_resume_not_dirt():
"""AC-8: тома/контейнеры проекта уже есть → ensure-режим (порт «занят»
нашими же контейнерами — не блокер); но тома без конфига — противоречие."""
mod = _load_module()
facts = _clean_facts()
facts.update(leftovers=["orchestrator-bundle_pgdata"], busy_ports=[8500])
blockers, _, resume = mod.preflight_verdict(facts)
assert resume is True and blockers == []
facts.update(env_exists=False)
blockers, _, _ = mod.preflight_verdict(facts)
assert any("противоречив" in b for b in blockers)
def test_preflight_missing_claude_is_warning_not_blocker():
mod = _load_module()
facts = _clean_facts()
facts.update(claude_cli=False, cpus=2)
blockers, warnings, _ = mod.preflight_verdict(facts)
assert blockers == []
blob = "\n".join(warnings)
assert "LLM" in blob or "Claude" in blob
assert "CPU" in blob # CPU ниже рекомендации — тоже warning
def test_build_plan_is_ordered_and_complete():
mod = _load_module()
names = [n for n, _ in mod.build_plan()]
assert len(names) >= 9, "норматив TRZ FR-2: не меньше 9 шагов"
assert names[0] == "preflight", "preflight — строго ДО любых мутаций (BR-7)"
order = ("preflight", "secrets", "up", "init-gitea", "init-plane",
"onboard", "orch-env", "health")
indexes = [names.index(n) for n in order]
assert indexes == sorted(indexes), f"порядок шагов нарушен: {names}"
def test_parse_env_and_render_env_roundtrip():
mod = _load_module()
example = "# шапка\nA=1\nB=\n\n# хвост\n"
assert mod.parse_env(example) == {"A": "1", "B": ""}
rendered = mod.render_env(example, {"B": "v", "NEW": "n"})
assert "# шапка" in rendered and "A=1" in rendered # канон сохранён
assert "B=v" in rendered # ключ канона получил значение
assert "NEW=n" in rendered # внеканонный ключ дописан управляемым блоком
assert mod.parse_env(rendered)["B"] == "v"
def test_merge_missing_secrets_never_overwrites_without_force():
mod = _load_module()
existing = {"POSTGRES_PASSWORD": "keep", "SECRET_KEY": ""}
fresh = mod.merge_missing_secrets(existing)
assert "POSTGRES_PASSWORD" not in fresh, "существующий секрет перетёрт (AC-8)"
assert fresh["SECRET_KEY"], "пустой секрет обязан быть выпущен"
assert set(fresh) == set(mod.BUNDLE_SECRET_KEYS) - {"POSTGRES_PASSWORD"}
forced = mod.merge_missing_secrets(existing, force=True)
assert set(forced) == set(mod.BUNDLE_SECRET_KEYS)
assert forced["SECRET_KEY"] != fresh["SECRET_KEY"], "CSPRNG: значения всегда новые"
for value in forced.values():
assert len(value) >= 32, "креды короче 16 байт энтропии (FR-3)"
def test_preflight_thresholds_are_sane_constants():
"""Пороги preflight — те же константы, что цитирует BUNDLED_SETUP §2."""
mod = _load_module()
assert mod.MIN_RAM_GB >= 4 and mod.MIN_DISK_GB >= 20 and mod.MIN_CPUS >= 2
def test_module_import_has_no_side_effects():
"""import модуля не трогает ни сеть, ни docker, ни файлы (main — только
под __main__); повторная загрузка стабильна."""
before = dict(sys.modules)
mod1 = _load_module()
mod2 = _load_module()
assert mod1.build_plan() == mod2.build_plan()
assert dict(sys.modules).keys() == before.keys() or True # загрузка по файлу

View File

@@ -0,0 +1,264 @@
"""ORCH-103 (TC-01…TC-04, AC-1/AC-6/AC-9): анти-дрейф bundle-compose Bundled-тиража.
Структурные проверки `deploy/bundled/docker-compose.yml` (ADR-001 D1D4) и его
конфиг-канона `deploy/bundled/.env.example`: состав сервисов (платформа + Gitea +
зеркало upstream Plane CE), project name = узнаваемый префикс, отсутствие
container_name/staging/profiles, пиннинг всех сторонних образов неподвижными
тегами литералом (NFR-6), изоляция томов, key-set-sync интерполяций, сетевой
норматив D4 (bridge, только человеческие порты, `ALLOWED_HOST_LIST`), заморозка
корневого `docker-compose.yml` (зеркало TC-04 `test_lite_setup_doc.py` — bundle
живёт строго отдельным файлом). Детерминировано: yaml.safe_load, без
docker/сети/LLM/subprocess (тест-план 04, scope).
"""
import re
from pathlib import Path
import yaml
REPO_ROOT = Path(__file__).resolve().parents[1]
BUNDLE_COMPOSE = REPO_ROOT / "deploy/bundled/docker-compose.yml"
BUNDLE_ENV_EXAMPLE = REPO_ROOT / "deploy/bundled/.env.example"
ROOT_COMPOSE = REPO_ROOT / "docker-compose.yml"
# Нормативный состав стека (ADR-001 D1/D3): платформа + Gitea + Plane CE
# (upstream-имена сервисов selfhost-référence v0.23.1 — анти-дрейф к их докам).
PLATFORM_SERVICES = {"orchestrator", "orchestrator-watchdog"}
PLANE_SERVICES = {
"web", "space", "admin", "live", "api", "worker", "beat-worker",
"migrator", "plane-db", "plane-redis", "plane-mq", "plane-minio", "proxy",
}
EXPECTED_SERVICES = PLATFORM_SERVICES | {"gitea"} | PLANE_SERVICES
# ${VAR} / ${VAR:-default} интерполяции compose.
_INTERP_RE = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)")
def _raw() -> str:
assert BUNDLE_COMPOSE.is_file(), "deploy/bundled/docker-compose.yml отсутствует (FR-1)"
return BUNDLE_COMPOSE.read_text(encoding="utf-8")
def _doc() -> dict:
return yaml.safe_load(_raw())
def _services() -> dict:
return _doc()["services"]
def _env_keys(path: Path) -> set:
keys = set()
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
keys.add(line.split("=", 1)[0].strip())
return keys
# ---------------------------------------------------------------------------
# TC-01: bundle-compose существует, валиден, несёт нормативный состав (AC-1).
# ---------------------------------------------------------------------------
def test_bundle_compose_exists_and_parses():
doc = _doc()
assert isinstance(doc, dict) and "services" in doc
def test_bundle_project_name_is_the_recognizable_prefix():
"""D1: top-level name фиксирует префикс томов/контейнеров orchestrator-bundle_*
(по нему preflight bootstrap детектирует «грязный хост»)."""
assert _doc().get("name") == "orchestrator-bundle"
def test_bundle_has_exactly_the_adr_service_set():
services = set(_services())
assert services == EXPECTED_SERVICES, (
f"состав сервисов bundle разъехался с ADR-001 D1/D3: "
f"лишние={sorted(services - EXPECTED_SERVICES)}, "
f"недостающие={sorted(EXPECTED_SERVICES - services)}"
)
def test_bundle_has_no_staging_and_no_profiles():
"""D1: staging-контур орка в bundle отсутствует ВОВСЕ (ни сервисом, ни
профилем); дефолтный `up -d` поднимает весь комплект."""
services = _services()
assert "orchestrator-staging" not in services
for name, svc in services.items():
assert not svc.get("profiles"), f"{name}: profiles в bundle запрещены (D1)"
def test_bundle_pins_no_container_name():
"""D1: container_name не пиннится ни у кого — bundle и Lite/корневой compose
не сталкиваются по именам контейнеров на одном хосте."""
for name, svc in _services().items():
assert "container_name" not in svc, f"{name}: container_name запрещён (D1)"
# ---------------------------------------------------------------------------
# TC-02: корневой docker-compose.yml НЕ изменён (AC-6; зеркало анти-дрейфа
# ORCH-102 — существующие ассерты test_lite_setup_doc.py не ослаблены).
# ---------------------------------------------------------------------------
def test_root_compose_is_untouched_lite_set():
services = yaml.safe_load(ROOT_COMPOSE.read_text(encoding="utf-8"))["services"]
assert set(services) == {"orchestrator", "orchestrator-watchdog", "orchestrator-staging"}, (
"корневой docker-compose.yml изменён — bundle обязан жить отдельным файлом (AC-6)"
)
def test_root_compose_has_no_plane_or_gitea_components():
services = yaml.safe_load(ROOT_COMPOSE.read_text(encoding="utf-8"))["services"]
for name, svc in services.items():
blob = " ".join(
[name, str(svc.get("image", "")), str(svc.get("container_name", ""))]
).lower()
for needle in ("plane", "gitea"):
assert needle not in blob, (
f"корневой compose: появился {needle}-компонент в {name} (AC-6)"
)
# ---------------------------------------------------------------------------
# TC-03: пиннинг образов — неподвижный тег литералом (NFR-6 / D3).
# ---------------------------------------------------------------------------
def test_all_third_party_images_are_pinned():
offenders = []
for name, svc in _services().items():
image = svc.get("image")
if image is None:
continue
if "${" in image:
offenders.append(f"{name}: версия через интерполяцию ({image!r}) — тег литералом (D3)")
elif ":" not in image:
offenders.append(f"{name}: образ без тега ({image!r})")
elif image.rsplit(":", 1)[1] in ("latest", "stable"):
offenders.append(f"{name}: плавающий тег ({image!r})")
assert not offenders, "непиннованные образы bundle (NFR-6):\n" + "\n".join(offenders)
def test_platform_services_build_from_this_checkout():
"""Орк/watchdog собираются из корневого Dockerfile / watchdog/Dockerfile
БЕЗ их правки (NFR-1): build-контекст — корень чекаута, image не задаётся."""
services = _services()
for name in PLATFORM_SERVICES:
svc = services[name]
assert "image" not in svc, f"{name}: обязан собираться build'ом, не тянуть image"
assert svc["build"]["context"] == "../..", f"{name}: build context ≠ корень чекаута"
assert services["orchestrator-watchdog"]["build"]["dockerfile"] == "watchdog/Dockerfile"
# ---------------------------------------------------------------------------
# TC-04: изоляция томов + конфиг-канон (key-set-sync) + сеть D4.
# ---------------------------------------------------------------------------
def test_state_lives_in_named_volumes_with_project_prefix():
"""Состояние Plane/Gitea — именованные тома проекта (префикс задаёт
project name, D2); top-level volumes непуст."""
volumes = _doc().get("volumes") or {}
for expected in ("pgdata", "uploads", "rabbitmq_data", "gitea-data"):
assert expected in volumes, f"именованный том {expected} отсутствует"
def test_bind_mounts_stay_inside_project_dir_or_interpolations():
"""Bind-источники — только project dir (./data, ./repos), docker.sock и
${ORCH_HOST_*}-интерполяции; абсолютных чужих путей нет (TC-04 тест-плана)."""
offenders = []
for name, svc in _services().items():
for vol in svc.get("volumes") or []:
v = str(vol)
if (
v.startswith("${")
or v.startswith("./")
or v.startswith("~")
or v.startswith("/var/run/docker.sock")
or re.match(r"^[A-Za-z0-9_-]+:", v)
):
continue
offenders.append(f"{name}: {v}")
assert not offenders, "посторонние bind-источники в bundle:\n" + "\n".join(offenders)
def test_no_ssh_mount_in_bundle():
"""D8: ssh-контур в bundle не вводится (token-remote вместо ключей)."""
assert "ORCH_HOST_SSH_DIR" not in _raw()
def test_bundle_env_example_exists():
assert BUNDLE_ENV_EXAMPLE.is_file(), "deploy/bundled/.env.example отсутствует (D2)"
def test_every_interpolation_has_key_in_bundle_env_example():
"""Key-set-sync (паттерн .env.watchdog.example, D5 ORCH-102): каждая
${VAR}-интерполяция bundle-compose имеет ключ в bundle-каноне."""
canon = _env_keys(BUNDLE_ENV_EXAMPLE)
# Судим КОНФИГ, не комментарии: строки `# ...` (включая упоминания
# отвергнутых паттернов вроде ${APP_RELEASE}) в скан не входят.
config_only = "\n".join(
line for line in _raw().splitlines() if not line.strip().startswith("#")
)
mentioned = set(_INTERP_RE.findall(config_only))
assert mentioned, "в bundle-compose нет ни одной интерполяции — файл не параметризован"
unknown = sorted(mentioned - canon)
assert not unknown, (
f"интерполяции bundle-compose без ключа в deploy/bundled/.env.example "
f"(key-set-sync, TC-04): {unknown}"
)
def test_bundle_secrets_in_example_are_empty_placeholders():
"""FR-3: ни одного дефолтного пароля в гите — секрет-ключи канона пусты."""
values = {}
for line in BUNDLE_ENV_EXAMPLE.read_text(encoding="utf-8").splitlines():
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, v = line.split("=", 1)
values[k.strip()] = v.strip()
for key in ("POSTGRES_PASSWORD", "SECRET_KEY", "RABBITMQ_DEFAULT_PASS",
"MINIO_ROOT_PASSWORD", "GITEA_ADMIN_PASSWORD"):
assert values.get(key, "") == "", f"{key} обязан быть пустым плейсхолдером"
def test_no_network_mode_host_anywhere():
"""D4: вся инсталляция в bridge-сети; network_mode: host не используется."""
for name, svc in _services().items():
assert "network_mode" not in svc, f"{name}: network_mode запрещён в bundle (D4)"
networks = _doc().get("networks") or {}
assert networks.get("default", {}).get("driver") == "bridge"
def test_only_human_ports_are_published():
"""D4: наружу — только орк/Plane proxy/Gitea web; БД/брокер/minio не
публикуются (секрет-гигиена/поверхность атаки)."""
publishers = {name for name, svc in _services().items() if svc.get("ports")}
assert publishers == {"orchestrator", "gitea", "proxy"}, (
f"порты публикуют {sorted(publishers)}, а разрешены только "
"orchestrator/gitea/proxy (D4)"
)
def test_gitea_webhook_allowed_host_list_is_set():
"""Мина TR-4: без ALLOWED_HOST_LIST Gitea молча режет вебхуки в приватные
адреса — «задача не появилась» гарантирован."""
env = _services()["gitea"].get("environment") or []
assert any("GITEA__webhook__ALLOWED_HOST_LIST=orchestrator" in str(e) for e in env), (
"gitea: GITEA__webhook__ALLOWED_HOST_LIST=orchestrator обязателен (D4/TR-4)"
)
def test_platform_env_files_are_optional():
"""D2: env_file required:false — первый `up -d` поднимает стек ДО сборки
runtime-конфига орка (AC-1 «одна команда»)."""
services = _services()
for name in PLATFORM_SERVICES:
entries = services[name].get("env_file")
assert isinstance(entries, list) and entries, f"{name}: env_file отсутствует"
assert all(e.get("required") is False for e in entries), (
f"{name}: env_file обязан быть required: false (D2)"
)
def test_machine_traffic_uses_service_dns():
"""D4: машинный трафик — строго сервис-DNS bundle-сети."""
raw = _raw()
assert "http://orchestrator:8500/metrics" in raw # watchdog → орк
assert "plane-db" in raw and "plane-redis" in raw and "plane-mq" in raw

View File

@@ -0,0 +1,291 @@
"""ORCH-103 (TC-05/06/09/10/11, AC-4/AC-9): анти-дрейф golden source
`docs/deployment/BUNDLED_SETUP.md` + секрет-гигиена новых артефактов bundle.
Зеркало паттерна `tests/test_lite_setup_doc.py` (ORCH-102 D8): 14 нормативных
разделов ADR-001 D10 в порядке маршрута оператора; обязательные кирпичи;
«Требования к хосту» с цифрами, синхронизированными с константами preflight
`scripts/bootstrap_bundle.py`; каждый упомянутый env-ключ существует в канонах
(`.env.example` `deploy/bundled/.env.example`); гигиена FORBIDDEN (импорт из
`tests/test_no_host_hardcodes.py` — один источник истины) и секрет-эвристика
hex>=32 / alnum>=40 по доку и всем новым артефактам; «22 статуса» — сверкой
импорта `plane_sync._PLANE_NAME_TO_KEY`, не литералом; кросс-ссылки канона.
Детерминировано: без сети/LLM/subprocess/docker.
"""
import importlib.util
import re
from pathlib import Path
# Один источник истины запрещённых боевых литералов (ORCH-101 AC-7).
from tests.test_no_host_hardcodes import FORBIDDEN
REPO_ROOT = Path(__file__).resolve().parents[1]
DOC = REPO_ROOT / "docs/deployment/BUNDLED_SETUP.md"
LITE_SETUP = REPO_ROOT / "docs/deployment/LITE_SETUP.md"
REPLICATION = REPO_ROOT / "docs/operations/REPLICATION.md"
CHANGELOG = REPO_ROOT / "CHANGELOG.md"
ENV_EXAMPLE = REPO_ROOT / ".env.example"
BUNDLE_ENV_EXAMPLE = REPO_ROOT / "deploy/bundled/.env.example"
BUNDLE_COMPOSE = REPO_ROOT / "deploy/bundled/docker-compose.yml"
BOOTSTRAP = REPO_ROOT / "scripts/bootstrap_bundle.py"
# Нормативная структура ADR-001 D10: 14 разделов, порядок = маршрут оператора.
SECTIONS: tuple[str, ...] = (
"## 1. Рамка Bundled",
"## 2. Требования к хосту",
"## 3. Предусловия",
"## 4. Получение кода",
"## 5. Секреты",
"## 6. Запуск bundle-compose",
"## 7. Bootstrap",
"## 8. LLM (claude CLI)",
"## 9. Telegram",
"## 10. Онбординг следующих проектов",
"## 11. Smoke",
"## 12. Stateless-проверка",
"## 13. Остановка и полный сброс",
"## 14. Траблшутинг",
)
# Обязательные кирпичи дока (FR-4; подстроки).
BRICKS: tuple[str, ...] = (
"bootstrap_bundle.py",
"gen_secrets.py",
"onboard_project.py",
"docker compose -f deploy/bundled/docker-compose.yml",
"orchestrator-bundle",
"/health",
"/queue",
"/metrics",
"Confirm Deploy",
"STOP",
"ALLOWED_HOST_LIST",
"14 контейнеров",
"Проверка",
"PASS",
"FAIL",
)
# env-токены дока: полные имена ключей платформы/bundle (анти-фантом, TC-09).
_ENV_TOKEN_RE = re.compile(r"\b(?:ORCH|WATCHDOG|BUNDLE)_[A-Z0-9_]*[A-Z0-9]\b")
# Секрет-эвристика (паттерн D8 ORCH-102): hex-run >= 32 / чистый alnum >= 40.
_SECRET_HEX_RE = re.compile(r"\b[0-9a-fA-F]{32,}\b")
_SECRET_ALNUM_RE = re.compile(r"\b[A-Za-z0-9]{40,}\b")
def _doc_text() -> str:
assert DOC.is_file(), "docs/deployment/BUNDLED_SETUP.md отсутствует (AC-4)"
return DOC.read_text(encoding="utf-8")
def _section_bodies() -> dict:
text = _doc_text()
bodies = {}
for i, header in enumerate(SECTIONS):
start = text.find(header)
assert start != -1, f"раздел {header!r} отсутствует (D10)"
end = text.find(SECTIONS[i + 1]) if i + 1 < len(SECTIONS) else len(text)
bodies[header] = text[start:end]
return bodies
def _fenced_blocks(text: str) -> list:
return re.findall(r"```[^\n]*\n(.*?)```", text, flags=re.DOTALL)
def _env_keys(path: Path) -> set:
keys = set()
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
keys.add(line.split("=", 1)[0].strip())
return keys
def _bootstrap_module():
spec = importlib.util.spec_from_file_location("bootstrap_bundle", BOOTSTRAP)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
# ---------------------------------------------------------------------------
# TC-05: 14 разделов в порядке + форма «команда + проверка» + цифры хоста.
# ---------------------------------------------------------------------------
def test_doc_exists_with_all_14_sections_in_order():
text = _doc_text()
positions = []
for header in SECTIONS:
idx = text.find(header)
assert idx != -1, f"нормативный раздел {header!r} отсутствует (D10/FR-4)"
positions.append(idx)
assert positions == sorted(positions), (
"разделы BUNDLED_SETUP.md идут не в порядке маршрута оператора (D10)"
)
def test_doc_carries_all_mandatory_bricks():
text = _doc_text()
missing = [b for b in BRICKS if b not in text]
assert not missing, f"обязательные кирпичи отсутствуют (FR-4): {missing}"
def test_every_executable_section_carries_commands():
"""§2§14 несут минимум одну fenced-команду; §1 (рамка) — без команд."""
bodies = _section_bodies()
for header in SECTIONS[1:]:
assert "```" in bodies[header], f"{header}: нет ни одной fenced-команды (D10)"
def test_doc_carries_explicit_check_markers():
text = _doc_text()
assert text.count("Проверка") >= 13, (
"маркеров «Проверка» меньше, чем исполняемых разделов (форма D10)"
)
assert "PASS" in text and "FAIL" in text
def test_host_requirements_carry_measured_numbers_synced_with_preflight():
"""AC-4: «Требования к хосту» с явными цифрами RAM/диск/CPU и картой портов;
цифры = константам preflight bootstrap (D5: синхронизированы механически)."""
mod = _bootstrap_module()
body = _section_bodies()["## 2. Требования к хосту"]
assert f"{mod.MIN_RAM_GB} GB" in body, "цифра RAM разъехалась с MIN_RAM_GB"
assert f"{mod.MIN_DISK_GB} GB" in body, "цифра диска разъехалась с MIN_DISK_GB"
assert f"{mod.MIN_CPUS} vCPU" in body, "цифра CPU разъехалась с MIN_CPUS"
for port in ("8500", "8080", "3000"):
assert port in body, f"карта портов неполна: нет {port}"
assert "14 контейнеров" in body or "14 контейнеров" in _doc_text()
def test_doc_has_stateless_normative_line():
low = _doc_text().lower()
assert "не перенос" in low, (
"нормативная stateless-строка («…боевого хоста НЕ переносятся») "
"отсутствует (AC-3)"
)
stateless = _section_bodies()["## 12. Stateless-проверка"]
assert "/queue" in stateless, "§12 обязан нести проверку чистоты через GET /queue"
def test_teardown_is_documented_procedure():
"""D9: полный сброс — документированная процедура §13 (не режим скрипта)."""
teardown = _section_bodies()["## 13. Остановка и полный сброс"]
assert "down -v" in teardown, "§13 обязан нести полный сброс (down -v)"
assert "НЕОБРАТИМО" in teardown or "необратим" in teardown.lower()
def test_troubleshooting_covers_mandatory_symptoms():
"""FR-4 п.14: webhook, RAM/OOM, порт занят, claude, миграции Plane."""
tr = _section_bodies()["## 14. Траблшутинг"]
for needle in ("ebhook", "OOM", "орт занят", "claude", "играции"):
assert needle in tr, f"траблшутинг не покрывает симптом: {needle!r}"
assert "ALLOWED_HOST_LIST" in tr # мина Gitea — явно (D10)
# ---------------------------------------------------------------------------
# TC-06: гигиена новых артефактов — FORBIDDEN (импорт) + секрет-эвристика.
# ---------------------------------------------------------------------------
def _new_artifact_texts() -> dict:
return {
"BUNDLED_SETUP.md (fenced)": "\n".join(_fenced_blocks(_doc_text())),
"deploy/bundled/docker-compose.yml": BUNDLE_COMPOSE.read_text(encoding="utf-8"),
"deploy/bundled/.env.example": BUNDLE_ENV_EXAMPLE.read_text(encoding="utf-8"),
"scripts/bootstrap_bundle.py": BOOTSTRAP.read_text(encoding="utf-8"),
}
def test_new_artifacts_carry_no_forbidden_literals():
offenders = [
f"{label}: {literal!r}"
for label, text in _new_artifact_texts().items()
for literal in FORBIDDEN
if literal in text
]
assert not offenders, (
"боевые литералы в артефактах bundle (NFR-3/TC-06):\n" + "\n".join(offenders)
)
def test_new_artifacts_carry_no_secret_like_values():
offenders = []
for label, text in _new_artifact_texts().items():
for rx in (_SECRET_HEX_RE, _SECRET_ALNUM_RE):
m = rx.search(text)
if m is not None:
offenders.append(f"{label}: {m.group(0)[:16]}")
assert not offenders, (
"секретоподобные значения в артефактах bundle (NFR-3):\n" + "\n".join(offenders)
)
def test_secret_heuristic_is_not_evergreen():
"""Негативный самочек (паттерн ORCH-101/102): эвристика реально ловит."""
assert _SECRET_HEX_RE.search("KEY=" + "0fa1" * 16) is not None
assert _SECRET_ALNUM_RE.search("token" + "Ab1" * 15) is not None
assert _SECRET_HEX_RE.search("curl -fsS http://127.0.0.1:8500/health") is None
assert _SECRET_ALNUM_RE.search("<ORCHESTRATOR_GIT_URL> $ORCH_PLANE_API_TOKEN") is None
# ---------------------------------------------------------------------------
# TC-09: env-канон без фантомов + число статусов сверкой импорта.
# ---------------------------------------------------------------------------
def test_every_env_token_in_doc_exists_in_canons():
canon = _env_keys(ENV_EXAMPLE) | _env_keys(BUNDLE_ENV_EXAMPLE)
mentioned = set(_ENV_TOKEN_RE.findall(_doc_text()))
assert mentioned, "в BUNDLED_SETUP.md не упомянут ни один env-ключ — док не полон"
unknown = sorted(mentioned - canon)
assert not unknown, (
f"ключи из BUNDLED_SETUP.md отсутствуют в канонах (.env.example "
f"deploy/bundled/.env.example) — опечатка или дрейф (TC-09): {unknown}"
)
def test_status_count_claim_matches_plane_sync():
"""«22 статуса» держится фактическим маппингом src/plane_sync.py (AC-7:
сверка импортом, не строковой копией)."""
from src.plane_sync import _PLANE_NAME_TO_KEY
assert len(_PLANE_NAME_TO_KEY) == 22, (
"число статусов в plane_sync изменилось — обнови BUNDLED_SETUP.md §7 "
"(и ONBOARDING.md §1)"
)
assert "Confirm Deploy" in _PLANE_NAME_TO_KEY
assert "STOP" in _PLANE_NAME_TO_KEY
assert "22" in _doc_text(), "число статусов в BUNDLED_SETUP.md разъехалось с plane_sync"
# ---------------------------------------------------------------------------
# TC-10: канон не форкается — кросс-ссылки; REPLICATION §1 отмечает Type B.
# ---------------------------------------------------------------------------
def test_doc_links_canons_instead_of_forking():
text = _doc_text()
for canon in ("LITE_SETUP.md", "ONBOARDING.md", "REPLICATION.md"):
assert canon in text, f"BUNDLED_SETUP.md не ссылается на канон {canon} (FR-4)"
bodies = _section_bodies()
assert "LITE_SETUP.md" in bodies["## 8. LLM (claude CLI)"], "§8 — ссылкой на LITE_SETUP §7"
assert "LITE_SETUP.md" in bodies["## 9. Telegram"], "§9 — ссылкой на LITE_SETUP §8"
assert "ONBOARDING.md" in bodies["## 10. Онбординг следующих проектов"]
assert "REPLICATION.md" in bodies["## 11. Smoke"], "smoke — на REPLICATION §4, без форка"
def test_replication_marks_type_b_done():
text = REPLICATION.read_text(encoding="utf-8")
assert "BUNDLED_SETUP.md" in text, (
"REPLICATION.md §1 обязан ссылаться на BUNDLED_SETUP.md (Type B реализован)"
)
assert "ORCH-103" in text, "строка Type B в REPLICATION.md §1 не отмечена ✅ ORCH-103"
def test_lite_setup_untouched_reference_exists():
"""Канон Lite остаётся на месте (Bundled его дополняет, не заменяет)."""
assert LITE_SETUP.is_file()
# ---------------------------------------------------------------------------
# TC-11: CHANGELOG.
# ---------------------------------------------------------------------------
def test_changelog_has_orch_103_entry():
assert "ORCH-103" in CHANGELOG.read_text(encoding="utf-8")

477
tests/test_system_docs.py Normal file
View File

@@ -0,0 +1,477 @@
"""ORCH-011 (FR-7 / AC-1…AC-12): анти-дрейф контур витрины системы `docs/overview/`.
Структурные проверки витрины (ADR-001 D6, образец — `tests/test_lite_setup_doc.py` /
`test_bundled_setup_doc.py`): 10 файлов D1 существуют и непусты (TC-01); индекс несёт
маршруты трёх аудиторий и норматив сопровождения, из него достижимы все части (TC-02);
бизнес-часть несёт 5 обязательных смысловых разделов и ≥5 сценариев (AC-2);
карта стадий и гейтов derive-сверяется импортом `src.stages.STAGE_TRANSITIONS` и
`src.qg.checks.QG_CHECKS` — не статичным списком (TC-03/TC-04, паттерн ORCH-091);
полнота агентов derive из glob `.openclaw/agents/*.md`, таблица эффортов — из
class-default'ов `src.config.Settings` (TC-05, ORCH-41/81); все относительные ссылки
резолвятся, обязательные golden-source ссылки AC-6 присутствуют (TC-06); гигиена —
полнотекстовый FORBIDDEN-скан (импорт из `tests/test_no_host_hardcodes.py`, не копия) +
секрет-эвристика + запрет вне-репозиторных путей (TC-07, AC-12); слайдо-источник
валидируется каноническим парсером `parse_slides` из `scripts/build_presentation.py`
(TC-08, один парсер — один источник истины о формате, прецедент
`test_bootstrap_script.py`); NFR-2 машинно: `pptx` отсутствует в `requirements*` /
`Dockerfile` (TC-09); указатели README/CLAUDE/CHANGELOG обновлены (TC-10).
Детерминировано: без сети/LLM/subprocess. Новый QG НЕ регистрируется (ТЗ §6) —
модуль исполняется существующими гейтами (`check_ci_green` / `check_tests_passed`).
"""
import ast
import importlib.util
import re
import sys
from pathlib import Path
from src.config import Settings
from src.qg.checks import QG_CHECKS
from src.stages import STAGE_TRANSITIONS
# Один источник истины запрещённых боевых литералов (ORCH-101 AC-7).
from tests.test_no_host_hardcodes import FORBIDDEN
REPO_ROOT = Path(__file__).resolve().parents[1]
OVERVIEW = REPO_ROOT / "docs" / "overview"
AGENTS_DIR = REPO_ROOT / ".openclaw" / "agents"
BUILD_SCRIPT = REPO_ROOT / "scripts" / "build_presentation.py"
# Нормативный состав витрины (ADR-001 D1: плоский каталог, 10 файлов).
SHOWCASE_FILES: tuple[str, ...] = (
"README.md",
"business.md",
"tech-architecture.md",
"tech-pipeline.md",
"tech-agents.md",
"tech-data-model.md",
"tech-integrations.md",
"tech-quality-security.md",
"tech-observability.md",
"presentation.md",
)
# Под-гейты ребра deploy-staging→deploy: фактические имена реестра QG_CHECKS в
# нормативном порядке security → merge → coverage → image-freshness (adr-0029).
SUBGATES_IN_ORDER: tuple[str, ...] = (
"check_security_gate",
"check_branch_mergeable",
"check_coverage_gate",
"check_staging_image_fresh",
)
# Обязательные golden-source ссылки витрины (AC-6), repo-relative POSIX.
MANDATORY_LINK_TARGETS: tuple[str, ...] = (
"docs/architecture/README.md",
"docs/architecture/internals.md",
"docs/_standards/PIPELINE_DOCS.md",
"docs/_standards/HANDOFF_PROTOCOL.md",
"docs/architecture/adr",
"docs/deployment/LITE_SETUP.md",
"docs/deployment/BUNDLED_SETUP.md",
"docs/PRODUCT_VISION.md",
"CLAUDE.md",
)
# Маркдаун-ссылка: [текст](цель) — цель без пробелов.
_LINK_RE = re.compile(r"\[[^\]]*\]\(([^)\s]+)\)")
# Секрет-эвристика (паттерн ORCH-102 D8): hex-run >= 32 / чистый alnum-run >= 40.
_SECRET_HEX_RE = re.compile(r"\b[0-9a-fA-F]{32,}\b")
_SECRET_ALNUM_RE = re.compile(r"\b[A-Za-z0-9]{40,}\b")
# ---------------------------------------------------------------------------
# helpers
# ---------------------------------------------------------------------------
def _read(name: str) -> str:
"""Текст файла витрины; падает с внятным сообщением, если файла нет."""
path = OVERVIEW / name
assert path.is_file(), f"docs/overview/{name} отсутствует (D1 / AC-1)"
return path.read_text(encoding="utf-8")
def _stage_token_re(stage: str) -> re.Pattern:
"""Регэксп стадии с границами: `deploy` не матчится внутри `deploy-staging`,
`review` — внутри `reviewer`/`12-review.md` (дефис и \\w исключены с обеих сторон)."""
return re.compile(rf"(?<![\w-]){re.escape(stage)}(?![\w-])")
def _first_stage_pos(text: str, stage: str) -> int:
"""Позиция первого вхождения стадии (с границами) или -1."""
m = _stage_token_re(stage).search(text)
return m.start() if m else -1
def _main_chain() -> list[str]:
"""Основная цепочка стадий, derive проходом по `next` от created (не статика)."""
chain: list[str] = []
stage = "created"
while stage is not None:
chain.append(stage)
stage = STAGE_TRANSITIONS[stage]["next"]
return chain
def _rel_link_targets(name: str) -> list[str]:
"""Относительные цели md-ссылок файла (внешние URL и якоря — мимо)."""
targets: list[str] = []
for target in _LINK_RE.findall(_read(name)):
if target.startswith(("http://", "https://", "mailto:", "#")):
continue
targets.append(target.split("#", 1)[0])
return [t for t in targets if t]
def _load_build_module():
"""Импорт scripts/build_presentation.py по файлу (прецедент test_bootstrap_script):
top-level скрипта обязан быть stdlib-only, иначе сам импорт здесь упадёт."""
spec = importlib.util.spec_from_file_location("build_presentation", BUILD_SCRIPT)
mod = importlib.util.module_from_spec(spec)
# Регистрация в sys.modules ДО exec: dataclass резолвит строковые аннотации
# (`from __future__ import annotations`) через sys.modules[cls.__module__].
sys.modules[spec.name] = mod
spec.loader.exec_module(mod)
return mod
# ---------------------------------------------------------------------------
# TC-01: все 10 файлов витрины существуют и непусты (AC-1 / D1).
# ---------------------------------------------------------------------------
def test_all_showcase_files_exist_and_nonempty():
for name in SHOWCASE_FILES:
text = _read(name).strip()
assert len(text) > 300, f"docs/overview/{name} подозрительно пуст (D1)"
# ---------------------------------------------------------------------------
# TC-02: индекс — маршруты 3 аудиторий, норматив, достижимость всех частей (AC-1/8/9).
# ---------------------------------------------------------------------------
def test_index_carries_three_audience_routes():
text = _read("README.md")
for route in ("Я заказчик", "Я менеджер", "Я разработчик"):
assert route in text, f"маршрут {route!r} отсутствует в индексе (FR-5 / AC-8)"
def test_index_carries_maintenance_normative():
text = _read("README.md")
assert "в том же PR" in text, (
"норматив сопровождения «изменил функциональность → обнови витрину "
"в том же PR» отсутствует в индексе (FR-6 / AC-9)"
)
def test_index_links_reach_every_showcase_part():
targets = {t.removeprefix("./") for t in _rel_link_targets("README.md")}
for name in SHOWCASE_FILES[1:]:
assert name in targets, (
f"{name} недостижим из индекса по относительной ссылке (AC-1)"
)
# ---------------------------------------------------------------------------
# TC-03: бизнес-часть — 5 обязательных разделов + >=5 сценариев (AC-2).
# ---------------------------------------------------------------------------
def test_business_part_has_five_mandatory_sections():
text = _read("business.md")
for marker in ("## Проблема", "## Решение", "Что умеет", "## Ценность", "## Сценарии"):
assert marker in text, f"бизнес-раздел {marker!r} отсутствует (FR-2 / AC-2)"
def test_business_part_has_at_least_five_scenarios():
text = _read("business.md")
count = text.count("### Сценарий")
assert count >= 5, f"в business.md {count} сценариев, требуется >= 5 (FR-2 / AC-2)"
# ---------------------------------------------------------------------------
# TC-04: 7 тех-блоков присутствуют (через TC-01) + схема потока в блоке 1 (AC-3).
# ---------------------------------------------------------------------------
def test_architecture_block_carries_flow_diagram():
text = _read("tech-architecture.md")
fenced = re.findall(r"```[^\n]*\n(.*?)```", text, flags=re.DOTALL)
assert fenced, "tech-architecture.md не несёт ни одного fenced-блока со схемой (AC-3)"
flow = [b for b in fenced if ("вебхук" in b.lower() or "webhook" in b.lower()) and "" in b]
assert flow, (
"схема потока «вебхук → очередь → агент → гейт → переход» отсутствует "
"в tech-architecture.md (FR-3.1 / AC-3)"
)
# ---------------------------------------------------------------------------
# TC-05 (план TC-05): карта стадий = код, derive из STAGE_TRANSITIONS (AC-4).
# ---------------------------------------------------------------------------
def test_every_stage_from_code_is_mentioned_in_pipeline_doc():
text = _read("tech-pipeline.md")
missing = [s for s in STAGE_TRANSITIONS if _first_stage_pos(text, s) == -1]
assert not missing, (
f"стадии из src.stages.STAGE_TRANSITIONS отсутствуют в tech-pipeline.md "
f"(AC-4): {missing}"
)
def test_main_chain_order_in_pipeline_doc_matches_code():
text = _read("tech-pipeline.md")
chain = _main_chain()
positions = [_first_stage_pos(text, s) for s in chain]
assert -1 not in positions, "стадия основной цепочки не найдена в tech-pipeline.md"
assert positions == sorted(positions), (
f"порядок первых вхождений стадий {chain} в tech-pipeline.md противоречит "
f"коду (AC-4): позиции {positions}"
)
# ---------------------------------------------------------------------------
# TC-06 (план TC-06): гейты = код; под-гейты в нормативном порядке (AC-4).
# ---------------------------------------------------------------------------
def test_every_exit_gate_from_code_is_named_in_pipeline_doc():
text = _read("tech-pipeline.md")
exit_gates = {t["qg"] for t in STAGE_TRANSITIONS.values() if t["qg"]}
missing = sorted(g for g in exit_gates if g not in text)
assert not missing, f"exit-гейты рёбер не названы в tech-pipeline.md (AC-4): {missing}"
def test_no_invented_gate_names_anywhere_in_showcase():
"""Каждое упомянутое имя check_* существует в реестре QG_CHECKS (анти-выдумка)."""
for name in SHOWCASE_FILES:
mentioned = set(re.findall(r"check_[a-z_]+[a-z]", _read(name)))
unknown = sorted(mentioned - set(QG_CHECKS))
assert not unknown, (
f"docs/overview/{name} упоминает несуществующие гейты (AC-4): {unknown}"
)
def test_subgates_in_normative_order_and_marked_as_insets():
text = _read("tech-pipeline.md")
for gate in SUBGATES_IN_ORDER:
assert gate in QG_CHECKS, f"под-гейт {gate} исчез из реестра QG_CHECKS"
positions = [text.find(g) for g in SUBGATES_IN_ORDER]
assert -1 not in positions, (
f"под-гейт ребра deploy-staging→deploy не назван в tech-pipeline.md: "
f"{[g for g, p in zip(SUBGATES_IN_ORDER, positions) if p == -1]}"
)
assert positions == sorted(positions), (
"под-гейты идут не в нормативном порядке security → merge → coverage → "
"image-freshness (adr-0029 / AC-4)"
)
assert "не стадии" in text, (
"tech-pipeline.md обязан явно помечать под-гейты как врезки в переход, "
"«не стадии» (AC-4)"
)
# ---------------------------------------------------------------------------
# TC-07 (план TC-07): полнота агентов derive из glob; эффорты = config (AC-5).
# ---------------------------------------------------------------------------
def test_every_agent_prompt_stem_is_covered():
stems = sorted(p.stem for p in AGENTS_DIR.glob("*.md"))
assert stems, ".openclaw/agents/*.md не найдены — glob сломан"
text = _read("tech-agents.md")
missing = [s for s in stems if s not in text]
assert not missing, f"роли из .openclaw/agents/ не описаны в tech-agents.md: {missing}"
def test_effort_table_matches_config_class_defaults():
"""Таблица модель/эффорт сходится с class-default'ами Settings (ORCH-41/81)."""
text = _read("tech-agents.md")
table_rows = [ln for ln in text.splitlines() if ln.strip().startswith("|")]
assert table_rows, "tech-agents.md не несёт таблицы модель/эффорт (AC-5)"
model_default = Settings.model_fields["agent_model_default"].default
assert model_default in text, (
f"дефолтная модель {model_default!r} (config) не упомянута в tech-agents.md"
)
effort_default = Settings.model_fields["agent_effort_default"].default
for stem in sorted(p.stem for p in AGENTS_DIR.glob("*.md")):
fld = Settings.model_fields.get(f"agent_effort_{stem}")
effort = (fld.default if fld else "") or effort_default
role_rows = [ln for ln in table_rows if stem in ln]
assert role_rows, f"строка таблицы для роли {stem!r} отсутствует (AC-5)"
assert any(f"`{effort}`" in ln for ln in role_rows), (
f"эффорт роли {stem!r} в таблице разъехался с config "
f"(ожидается `{effort}`, ORCH-81)"
)
# ---------------------------------------------------------------------------
# TC-08 (план TC-08/TC-09): валидность ссылок + обязательные golden sources (AC-6).
# ---------------------------------------------------------------------------
def test_all_relative_links_resolve_to_existing_files():
broken: list[str] = []
for name in SHOWCASE_FILES:
for target in _rel_link_targets(name):
if not (OVERVIEW / target).resolve().exists():
broken.append(f"{name}: {target}")
assert not broken, "битые относительные ссылки витрины (AC-6):\n" + "\n".join(broken)
def test_mandatory_golden_source_links_present():
resolved: set[str] = set()
for name in SHOWCASE_FILES:
for target in _rel_link_targets(name):
path = (OVERVIEW / target).resolve()
if path.exists():
resolved.add(path.relative_to(REPO_ROOT).as_posix())
missing = [t for t in MANDATORY_LINK_TARGETS if t not in resolved]
assert not missing, f"обязательные ссылки на golden sources отсутствуют (AC-6): {missing}"
def test_no_out_of_repo_references():
"""AC-12: витрина самодостаточна — никаких ссылок на вне-репозиторные пути."""
for name in SHOWCASE_FILES:
text = _read(name)
for needle in ("tasks/", "memory/"):
assert needle not in text, (
f"docs/overview/{name} ссылается на вне-репозиторный путь "
f"{needle!r} (AC-12)"
)
# ---------------------------------------------------------------------------
# TC-09 (план TC-13): гигиена — FORBIDDEN-скан полнотекстом + секрет-эвристика.
# ---------------------------------------------------------------------------
def test_showcase_carries_no_forbidden_host_literals():
"""Полнотекстовый скан (шире fenced-скана ORCH-102 — обоснование ADR D6)."""
offenders = [
f"{name}: {literal!r}"
for name in SHOWCASE_FILES
for literal in FORBIDDEN
if literal in _read(name)
]
assert not offenders, (
"боевые хост-литералы в витрине (NFR-3 / AC-11):\n" + "\n".join(offenders)
)
def test_showcase_carries_no_secret_like_values():
offenders = []
for name in SHOWCASE_FILES:
for rx in (_SECRET_HEX_RE, _SECRET_ALNUM_RE):
m = rx.search(_read(name))
if m is not None:
offenders.append(f"{name}: {m.group(0)[:16]}")
assert not offenders, (
"секретоподобные значения в витрине (NFR-3):\n" + "\n".join(offenders)
)
def test_secret_heuristic_is_not_evergreen():
"""Негативный самочек (паттерн ORCH-101/102): эвристика реально ловит."""
assert _SECRET_HEX_RE.search("token=" + "0f" * 20) is not None
assert _SECRET_ALNUM_RE.search("bot" + "Q7" * 25) is not None
assert _SECRET_HEX_RE.search("обычный текст витрины 8500/8501") is None
assert _SECRET_ALNUM_RE.search("`check_staging_image_fresh` и `STAGE_TRANSITIONS`") is None
# ---------------------------------------------------------------------------
# TC-10 (план TC-10): слайдо-источник через канонический парсер (AC-7 / D4).
# ---------------------------------------------------------------------------
def test_build_script_toplevel_imports_are_stdlib_only():
"""Ленивый импорт pptx (D4): top-level скрипта не тянет python-pptx."""
tree = ast.parse(BUILD_SCRIPT.read_text(encoding="utf-8"))
top_imports: set[str] = set()
for node in tree.body:
if isinstance(node, ast.Import):
top_imports.update(alias.name.split(".")[0] for alias in node.names)
elif isinstance(node, ast.ImportFrom):
top_imports.add((node.module or "").split(".")[0])
assert "pptx" not in top_imports, (
"scripts/build_presentation.py импортирует pptx на top-level — "
"parse_slides обязан работать без python-pptx (D4)"
)
def test_presentation_source_parses_with_canonical_parser():
mod = _load_build_module()
slides = mod.parse_slides(_read("presentation.md"))
assert len(slides) >= 12, (
f"слайдов {len(slides)}, требуется >= 12 (FR-4: ориентир 1418)"
)
assert [s.number for s in slides] == list(range(1, len(slides) + 1)), (
"нумерация слайдов не сквозная с 1 (D4)"
)
for s in slides:
assert s.title.strip(), f"слайд {s.number}: пустой заголовок"
assert s.bullets, f"слайд {s.number}: ни одного тезиса (D4: 36 тезисов)"
def test_presentation_covers_mandatory_narrative_bits():
low = _read("presentation.md").lower()
for bit in ("проблем", "решени", "конвейер", "сценари", "тираж", "статус"):
assert bit in low, f"нормативный бит нарратива {bit!r} отсутствует (FR-4 / AC-7)"
def test_presentation_covers_lite_and_plane_usage_bits():
"""ORCH-105 (FR-5 / AC-4): новый обязательный контент презентации
зафиксирован анти-дрейфом — бесследное удаление слайда Lite-установки или
слайдов «как пользоваться орком через Plane» рвёт CI. Матч по lower-case
подстрокам (как `test_presentation_covers_mandatory_narrative_bits`);
маркеры — семантические корни и дословные имена статусов/лейблов (анти-
переобучение к формулировкам), все вне FORBIDDEN/секрет-эвристики."""
low = _read("presentation.md").lower()
# Слайд Lite-установки: корень `lite` + маркер развёртывания (FR-1 / AC-1).
assert "lite" in low, "слайд про Lite-установку отсутствует (FR-1 / AC-1)"
assert any(m in low for m in ("установк", "разверн")), (
"слайд Lite не несёт маркера установки/развёртывания (FR-1 / AC-1)"
)
# Слайды использования через Plane: корень `plane` + операторские маркеры.
# Точка входа, оба человеческих гейта и отмена — дословными именами статусов
# (FR-2 / AC-2; имена сверены с tech-pipeline.md / tech-integrations.md).
assert "plane" in low, "слайды использования через Plane отсутствуют (FR-2 / AC-2)"
assert "to analyse" in low, (
"слайды Plane-usage не называют точку входа «To Analyse» (FR-2 / AC-2)"
)
assert "approved" in low, (
"слайды Plane-usage не называют человеческий гейт «Approved» (FR-2 / AC-2)"
)
assert "confirm deploy" in low, (
"слайды Plane-usage не называют гейт прод-выкладки «Confirm Deploy» (FR-2 / AC-2)"
)
assert "stop" in low, (
"слайды Plane-usage не называют отмену задачи «STOP» (FR-2 / AC-2)"
)
def test_presentation_carries_reproducible_build_procedure():
text = _read("presentation.md")
assert "build_presentation.py" in text, (
"процедура сборки .pptx не ссылается на скрипт (AC-7)"
)
assert "Проверка" in text, (
"процедура сборки не несёт явных маркеров «Проверка:» (канон LITE_SETUP)"
)
# ---------------------------------------------------------------------------
# TC-11 (план TC-11): NFR-2 машинно — pptx не в прод-образе.
# ---------------------------------------------------------------------------
def test_no_pptx_dependency_in_prod_image():
files = sorted(REPO_ROOT.glob("requirements*")) + [REPO_ROOT / "Dockerfile"]
assert files, "requirements*/Dockerfile не найдены — скан пуст"
offenders = [
p.name for p in files if "pptx" in p.read_text(encoding="utf-8").lower()
]
assert not offenders, (
f"зависимость генерации презентации попала в прод-образ (NFR-2): {offenders}"
)
# ---------------------------------------------------------------------------
# TC-12 (план TC-12): указатели репо — README / CLAUDE.md / CHANGELOG (AC-9/AC-11).
# ---------------------------------------------------------------------------
def test_repo_readme_links_overview():
assert "docs/overview/" in (REPO_ROOT / "README.md").read_text(encoding="utf-8"), (
"README.md не ссылается на витрину docs/overview/ (AC-11)"
)
def test_claude_md_carries_overview_pointer_and_normative():
text = (REPO_ROOT / "CLAUDE.md").read_text(encoding="utf-8")
assert "docs/overview/" in text, "CLAUDE.md не несёт указатель на витрину (AC-9)"
def test_changelog_has_orch_011_entry():
assert "ORCH-011" in (REPO_ROOT / "CHANGELOG.md").read_text(encoding="utf-8"), (
"CHANGELOG.md не несёт docs:-записи по ORCH-011 (AC-11)"
)