Фундамент тиража 10-common (эпик ORCH-10): платформа разворачивается на
новой инфре без правки кода — только env/конфиг. Каждый дефолт = боевому
значению (пустой .env => поведение 1:1, kill-switch-природа, NFR-2);
STAGE_TRANSITIONS/QG_CHECKS/check_*/machine-verdict/схема БД не тронуты.
- config: agent_home_dir / agent_git_name / git_email_domain / staging_port
(ADR-001 D2/D4); код-блокеры A1-A4 закрыты: plane_sync ссылки из
gitea_public_url+gitea_owner, launcher - единый agent_git_env() (x2 места),
self_deploy/post_deploy - HOME+домен из Settings (имена системных акторов -
платформенные литералы)
- image_freshness: staging_port из конфига + fail-closed guard
staging_port == прод-порт -> отказ ДО ssh/build (инвариант ORCH-058 AC-9
стал исполняемым); REPO= передаётся хуку явно обоими инвокерами (D7)
- SELF_HOSTING_REPO - нормативная платформенная константа (D3, пин-тест)
- compose: полная ${VAR:-default}-интерполяция (реестр B, карта D6); группа
ORCH-040 uid/gid/HOME/маунты двигается согласованно (build.args APP_*);
group_add "МИНА 1" сохранён x3; оба app-сервиса с явным command:
- Dockerfile: ARG APP_UID/APP_GID/APP_USER/APP_HOME (CMD exec-form 8500
сознательно не тронут - D5); deploy-hook: REPO="${REPO:-...}" (D1 реестра)
- секреты: stdlib scripts/gen_secrets.py (token_hex(32); печать по умолчанию;
--write никогда не перезаписывает существующий .env молча, exit=2;
перезапись только --force); .env.example дополнен до полноты ключей старта
- доки: новый docs/operations/REPLICATION.md (карта env, чек-лист секретов,
smoke-процедура с PASS/FAIL, границы 10-common/Lite/Bundled), INFRA.md,
README, CLAUDE.md, CHANGELOG
- анти-регресс: tests/test_no_host_hardcodes.py (tokenize-сканер запрещённых
литералов, config-модули - структурное исключение, allowlist пуст,
негативная самопроверка) + test_host_config_keys / test_infra_parametrization
/ test_secrets_gen / test_replication_smoke; согласованные структурные
правки test_orch040_compose (судит резолв дефолтов) и
test_deploy_hook_rollback_sim (REPO через env-override = контракт D7)
Полный регресс: 1764 passed.
Refs: ORCH-101
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1196 lines
173 KiB
Markdown
1196 lines
173 KiB
Markdown
# Архитектура Orchestrator
|
||
|
||
## Обзор
|
||
Мульти-агентный оркестратор разработки. Принимает webhooks от Plane (управление задачами) и Gitea (git-события), ведёт задачи по конвейеру стадий через Quality Gates, на каждой стадии запускает Claude CLI агента. Поддерживает несколько проектов (multi-repo) и self-hosting (дорабатывает сам себя).
|
||
|
||
## Компоненты
|
||
- **Webhook Receivers** (`src/webhooks/plane.py`, `gitea.py`) — приём событий, HMAC-проверка, дедупликация (`_dedup.py`). Роуты: `POST /webhook/plane`, `POST /webhook/gitea`.
|
||
- **State Machine** (`src/stages.py`) — `STAGE_TRANSITIONS`: переходы, агент и QG каждой стадии. Хелперы: `get_next_stage`, `get_agent_for_stage`, `get_qg_for_stage`, `get_previous_stage`.
|
||
- **Stage Engine** (`src/stage_engine.py`) — исполнение переходов, диспетчеризация QG (`_run_qg`), откаты, синхронизация с Plane.
|
||
- **Review/Test Parsers** (`src/review_parse.py`, ORCH-046) — defensive-извлечение дословного must-fix текста из артефактов для встраивания в `task_desc` заворота: `extract_review_findings` (P0/P1 из `12-review.md`), `extract_test_failures` (фрагмент тела `13-test-report.md`). Контракт «never raise»: любая ошибка → `""`.
|
||
- **Quality Gates** (`src/qg/checks.py`) — проверки выхода со стадии, реестр `QG_CHECKS`.
|
||
- **Agent Launcher** (`src/agents/launcher.py`) — запуск Claude CLI агентов в изолированном git worktree, мониторинг, auto-advance. Модель/эффорт каждого агента резолвятся из config (`resolve_agent_model`/`resolve_agent_effort`, ORCH-41), а не из frontmatter промпта. **ORCH-74:** имя модели валидируется форматом `^claude-…$` (`is_valid_model`) перед `--model`; невалидное → лог + откат на следующий уровень/CLI-дефолт (never-break, как `VALID_EFFORTS` для эффорта). Тот же предикат гардит inline-чтение `--fallback-model`.
|
||
- **Queue** (`src/queue_worker.py`, ORCH-1) — персистентная очередь задач (SQLite `jobs`), atomic claim, max_concurrency, ретраи, restart-safe. **ORCH-026:** `claim_next_job` гейтит задачи с незавершёнными зависимостями (`job_deps`, `NOT EXISTS`) без занятия слота; декларации/циклы — leaf `src/task_deps.py`.
|
||
- **Job-reaper** (`src/job_reaper.py`, ORCH-065 — [adr-0011](adr/adr-0011-job-reaper-lease-reclaim.md)) — фоновый daemon-поток (каркас `reconciler`), стартует/останавливается в `main.lifespan` (после `reconciler.start()` / перед `worker.stop()`). Детектирует «мёртвый» `running`-job **без рестарта** процесса (Tier-1 мёртвый `jobs.pid` после `reaper_dead_ticks` тиков; Tier-2 `agent_runs.exit_code` записан, а job ещё `running`; Tier-3 backstop `reaper_max_running_s`) и приводит строку к корректному статусу через те же контракты (`_try_advance_stage`/`_finalize_job`, gate-driven; exit≠0/неизвестно → `attempts<max`→`queued`, иначе `failed`+Telegram). Атомарный reap-claim (guard `status='running'`) совместим со стартовым `requeue_running_jobs`. Тот же поток периодически делает проактивный реклейм stale/dead merge-lease (см. ниже). never-raise; kill-switch `ORCH_REAPER_ENABLED`; снимок в `GET /queue` (блок `reaper`).
|
||
- **Reconciler** (`src/reconciler.py`, ORCH-053 — реализовано, [adr-0007](adr/adr-0007-reconciler.md)) — фоновый daemon-поток (паттерн `queue_worker`), стартует/останавливается в `main.lifespan` (после `worker.start()` / перед `worker.stop()`). Реконсилирует рассинхрон «источник истины ≠ стадия задачи» при потерянном webhook. F-1 gate-side (продвигает застрявшую стадию по локальной БД через штатный `advance_stage(..., finished_agent=None)`), F-2 plane-side (опрос Plane API → `handle_*` из `plane.py`), F-3 (БД-fallback `sha→branch` в `handle_ci_status`). Источник истины — гейт/Plane, не событие; идемпотентность (active-job guard + atomic-claim + grace); kill-switch `ORCH_RECONCILE_ENABLED`. `analysis` F-1 не трогает (человеческий гейт). F-1 также пропускает escalated (retry≥лимита) и Blocked/Needs-Input задачи (ORCH-060). Наблюдаемость — блок `reconcile` в `GET /queue`.
|
||
- **Disk-watchdog** (`src/disk_watchdog.py`, ORCH-063 — [adr-0024](adr/adr-0024-disk-watchdog.md)) — фоновый daemon-поток (каркас `reconciler`/`job_reaper`), стартует/останавливается в `main.lifespan` (старт последним — после `reaper.start()`; стоп первым в reverse-порядке; гард `disk_monitor_enabled`). Каждые `disk_monitor_interval_s` (дефолт 300с) меряет заполнение **хост-ФС** по смонтированным bind-путям (`/repos`, `/app/data`) через stdlib `shutil.disk_usage` (не overlay `/` контейнера, не субпроцесс `df`; дедуп путей по `st_dev`). Решение об алерте — pure-функция `decide_action(used_pct, threshold, prev_state, now, realert_s)`: алерт на пересечении порога (дефолт **85%**), cooldown-повтор `disk_monitor_realert_s` (анти-спам, не на каждом тике), однократный recovery при возврате ниже порога. Алерт — `send_telegram` (notifying, best-effort). Состояние анти-спама — in-memory (без миграции БД). never-raise (per-path/per-tick/per-send); только читает и уведомляет — не трогает диск/контейнер, не рестартит прод (self-hosting безопасность). Kill-switch `ORCH_DISK_MONITOR_ENABLED`; снимок — блок `disk_monitor` в `GET /queue` (`enabled`/`threshold_pct`/`interval_s`/`realert_s`/`paths`[`used_pct`/`free_gb`/`alerting`/`last_alert_at`]). `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД — не тронуты. Детали — `docs/work-items/ORCH-063/06-adr/ADR-001-disk-watchdog.md`.
|
||
- **Build-cache-pruner** (`src/build_cache_pruner.py`, ORCH-062 — [adr-0025](adr/adr-0025-build-cache-pruner.md)) — фоновый daemon-поток (каркас `disk_watchdog`), стартует/останавливается в `main.lifespan` (старт последним — после `disk_watchdog.start()`; стоп первым в reverse; гард `build_cache_prune_enabled`). «Вторая половина» disk-watchdog: **watchdog сигналит — pruner убирает**. Каждые `build_cache_prune_interval_s` (дефолт 21600с = 6ч) выполняет **строго `docker builder prune -f --filter until=<until>`** (BuildKit GC; дефолт `until=24h` — удаляет build cache старше суток, тёплый кэш сохраняет; `-a` опционально, только в паре с фильтром). Затрагивает **только** build cache — НЕ образы/контейнеры; рестарт docker daemon/прода не выполняется (self-hosting безопасность). В контейнере нет `docker` CLI (`Dockerfile:11`), поэтому уборка идёт **на хосте через ssh** каналом `deploy_ssh_user@deploy_ssh_host` (как `image_freshness`/`self_deploy`); пустой `deploy_ssh_host` → тик no-op (скоуп на self-host). never-raise (per-команда/per-tick); учёт результата in-memory (без миграции БД). Kill-switch `ORCH_BUILD_CACHE_PRUNE_ENABLED`; снимок — блок `build_cache_prune` в `GET /queue` (`enabled`/`interval_s`/`until`/`last_run_ts`/`last_reclaimed`/`last_error`). `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД — не тронуты. Детали — `docs/work-items/ORCH-062/06-adr/ADR-001-build-cache-pruner.md`.
|
||
- **Notifications / Live-tracker** (`src/notifications.py`, ORCH-042/ORCH-067) — ОДНА live-карточка на задачу (`update_task_tracker`), обновляется на каждом переходе. Режим `ORCH_TRACKER_MODE` (дефолт `bump` с ORCH-067: delete+silent send+repoint внизу чата; `edit` — правка на месте). Карточка несёт строку Plane-статуса `📍 …` (оффлайн-ядро `plane_status_label` + best-effort live-overlay `_live_plane_branch_override`, kill-switch `ORCH_TRACKER_LIVE_STATUS`) и кликабельный номер задачи (`plane_issue_link`/`link_for` → ссылка в Plane, fail-safe на сырой номер). **ORCH-080:** оба низкоуровневых примитива (`send_telegram`/`edit_telegram`) шлют payload с `disable_web_page_preview: True` — Telegram больше не разворачивает баннер link-preview Plane под карточкой/уведомлениями; `parse_mode: HTML` сохранён (ссылка остаётся кликабельной), безусловно без kill-switch. Все алерты, упоминающие `work_item_id`, делают номер кликабельным. **ORCH-087:** bump ведёт авторитетный леджер всех созданных карточек (`tracker_messages`, `deleted_at IS NULL` = жива) и на каждом обновлении зачищает ВСЕ незакрытые mid (а не только скаляр `tracker_message_id`) → класс «замёрзшая сирота» устранён; строка стадии несёт фактический эффорт рядом с моделью (`· {model} · {effort}`, колонка `agent_runs.effort`, стамп в `launcher._spawn`); done-строка времени переписана на три подписанных метрики `⏱️ Агенты · твоё{~cap} · общее с ожиданием` (кап `ORCH_TRACKER_BRD_REVIEW_CAP_S`); deploy-цикл дополнен overlay-ключом `confirm_deploy`. **ORCH-091 (индикация-only):** три корректности рендера — (1) `_STAGE_STATUS_LABEL` покрывает ВСЕ ключи `STAGE_TRANSITIONS` (добавлены `deploy-staging`→«Deploying (staging)», `cancelled`→«Cancelled»; полнота гарантируется тестом по `stages.STAGE_TRANSITIONS`, не статичным списком — NFR-3), runtime-фолбэк для неизвестной стадии стал нейтральным (капитализированное имя) вместо «To Analyse»; (2) при откате конвейера `✅`-строки стадий ПОЗЖЕ текущей позиции (позиция — из порядка `STAGE_TRANSITIONS`, с нормализацией `deploy-staging→deploy` только в гейте подавления; `is_active_stage` не тронут) больше не рисуются; (3) строка стадии суммирует ВСЕ `agent_runs` агента (Σ cost/токены/время теми же формулами, что блок тоталов) → строгая сходимость с `SUM(agent_runs)`. Только `src/notifications.py` + тесты; `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД/транспорт — не тронуты. Контракт всего компонента — never raises; карточка всегда silent. **ORCH-095 (HTML-безопасность данных):** текст карточки шлётся с `parse_mode=HTML`; каждый **data**-слот (длительности `_fmt_minutes`/`_capped_review_str`, статус-лейбл, модель/эффорт, токены/стоимость) экранируется `html.escape` ровно один раз на границе рендера, **markup**-слоты (`num_html`/`link_for`/`_done_link`/`esc_title`) — нет (двойное экранирование запрещено). Устранён класс «неэкранированные данные в HTML» (литерал `<1м` от `_fmt_minutes` → Telegram `400 can't parse entities` → застывшая карточка, инцидент ORCH-093); `_fmt_minutes` по-прежнему даёт `<1м` (escape рендерит визуально идентично). Застрявшая карточка в окне авто-восстанавливается следующим рендером; `edit_telegram`/`update_task_tracker`/леджер сирот не тронуты. Детали — [internals.md](internals.md) §7, [ADR-087](../work-items/ORCH-087/06-adr/ADR-001-tracker-orphan-cleanup.md), [ORCH-091 ADR-001](../work-items/ORCH-091/06-adr/ADR-001-tracker-status-rollback-metrics.md) и [ORCH-095 ADR-001](../work-items/ORCH-095/06-adr/ADR-001-html-safe-card-data-render.md).
|
||
- **Project Registry** (`src/projects.py`, ORCH-6) — Plane project id → repo + prefix; фильтрация вебхуков по проекту.
|
||
- **Plane Sync** (`src/plane_sync.py`) — синхронизация статусов/комментариев в Plane. Резолв статусов проекта `get_project_states` (ORCH-10) кэширует `{logical_key→uuid}` per-project; **ORCH-068** добавляет в кэш-запись `{uuid→group}` (для терминал-исключения F-2) и **TTL** `ORCH_PLANE_STATES_TTL_S` (дефолт 300с; `0` → прежний lifetime-кэш) — устаревший набор статусов самозалечивается без рестарта процесса через существующий `reload_project_states()` (баг кэша после появления нового Plane-статуса). Форма возврата `get_project_states` неизменна (обратная совместимость).
|
||
- **FS ownership detect** (`src/fs_normalize.py`, ORCH-057 — [adr-0031](adr/adr-0031-legacy-ownership-normalization.md)) — чистый **never-raise** leaf (паттерн `serial_gate`/`preflight`), закрывает пробел ORCH-040: при миграции на `user: "1000:1000"` legacy `root:root` файлы в `/repos` ломали создание worktree под uid 1000 (`ensure_worktree` → сырой `fatal: … Permission denied`, агент не стартовал). Три слоя: (1) **D1** — `src/git_worktree.py::ensure_worktree` классифицирует класс «нет прав» (`Permission denied`/`could not create leading directories`/`insufficient permission`/`EACCES`/`EPERM`) и поднимает actionable `RuntimeError` с причиной + лечащей командой (не-прав-ошибки сохраняют прежний контракт — меняется только формулировка, не факт сбоя); (2) **D2** — `scan_ownership(roots, target_uid=os.getuid())` обходит `/repos/_wt`, `<repo>/.git/{objects,worktrees}`, `data/runs` с ранним выходом при первом `st_uid != target_uid` + TTL-кэш; (3) **D3** — best-effort вызов на старте `main.lifespan` → WARNING + Telegram при mismatch (claim **НЕ** блокируется — внятный ранний отказ даёт D1 в точке launch, знающей repo; preflight-блок отвергнут как repo-слепой → регресс enduro). Опц. `normalize()` chown'ит только при `CAP_CHOWN` (под uid 1000 — no-op; init-контейнер/root-entrypoint отвергнуты — реинтродукция root-контекста + self-deploy compose). Фактическая нормализация = **операторская процедура** под root на хосте (`INFRA.md` «Миграция uid»). Условность `applies(repo)` first: `fs_normalize_enabled` (kill-switch) + `fs_normalize_repos` (CSV, пусто → self-hosting only). Наблюдаемость — блок `fs_ownership` в `GET /queue`; опц. `POST /fs-normalize/check`. `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схема БД — не тронуты. Детали — `docs/work-items/ORCH-057/06-adr/ADR-001-legacy-ownership-normalization.md`.
|
||
- **Metrics endpoint** (`src/metrics.py` + `GET /metrics`, ORCH-099 — [adr-0030](adr/adr-0030-metrics-endpoint.md)) — лёгкий **read-only** leaf-сборщик (`build_metrics() -> dict`, never-raise по разделам, паттерн `serial_gate.snapshot()`) + тонкий эндпоинт (стиль `GET /queue`). Отдаёт JSON-«сырьё» о самом орке (стадии задач / очередь jobs / agent-liveness / стоимость-токены) как **стабильный машинный контракт для sidecar F1b** (`watchdog/`, отдельная задача — наблюдатель отделён от наблюдаемого). Только чтение существующих `tasks`/`jobs`/`agent_runs` + in-memory-снапшотов (`worker.breaker`); два read-only helper'а в `db.py` (`get_running_agents`/`agent_cost_totals`). Логику мониторинга (пороги/алерты/история/Telegram) НЕ несёт — это F1b. Контракт ниже (§ «Сырьё-эндпоинт `/metrics`»). Kill-switch `metrics_endpoint_enabled` (дефолт `True`). `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД — не тронуты.
|
||
- **Lessons journal** (`src/lessons.py` + таблица `lessons`, ORCH-098 — реализовано, [adr-0034](adr/adr-0034-lessons-journal.md)) — машинный журнал уроков (структурированная база отклонений конвейера); шаг 1 эпика саморазвития (домен 0 «Фундамент», F2; топливо петли самообучения 8A), фундамент для будущих ретроспективщика (E2)/приоритизатора RICE (E3)/Стрим. Чистый **observer-leaf** (never-raise, паттерн `serial_gate`/`coverage_gate`/`metrics`): `record()`/`get()`/`update()`/`snapshot()`. **Аддитивная идемпотентная таблица `lessons`** (`CREATE TABLE IF NOT EXISTS` в `init_db()`, restart-safe) с полями контекста (`work_item_id`/`task_id`/`stage`/`agent`/`repo`), анализа (`root_cause`/`suggestion`), статуса (`status`/`related_task`) и **атрибуции — сразу и нуллабельно** (`attribution`/`target_repo`/`target_domain`, требование Славы 10.06 / NFR-6, заполняется позже ретроспективщиком/человеком) + `source`/`detail`; без `enum`-констрейнтов (слаги forward-compatible). **Автозапись 4 типов** (`source="auto"`, best-effort, дедуп в окне; `transient_retry` — только на исчерпании бюджета ретраев) тонкими врезками: `gate_failure` (`stage_engine._handle_qg_failure_rollbacks`), `merge_hold` (`merge_gate._handle_merge_verify` HOLD), `transient_retry` (merge-retry/launcher transient budget-exhaustion), `deploy_degraded` (post-deploy `DEGRADED → set_repo_freeze`, урок слоя-3 «деплой OK / прод сломан», ET-8). Эндпоинты `GET /lessons` (read-only, фильтры), `POST /lessons` (ручная запись), `POST /lessons/{id}` (update/доклассификация), + read-only ключ `lessons` в `GET /queue`. **Расхождение с гейт-шаблоном:** журнал observer-only → **НЕ скоупится по репо** (kill-switch `lessons_enabled` only, без `lessons_repos`); репо-разрез — на выборке (`repo`-колонка/фильтр), enduro не затронут (общая БД, аддитивная таблица). `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схемы существующих таблиц — байт-в-байт не тронуты (журнал не участвует в решении гейта). Kill-switch `lessons_enabled` (env `ORCH_LESSONS_ENABLED`, дефолт `True`). Детали — `docs/work-items/ORCH-098/06-adr/ADR-001-lessons-journal.md`.
|
||
- **Sidecar-watchdog F1b** (`watchdog/` + сервис `orchestrator-watchdog`, ORCH-100 — [adr-0033](adr/adr-0033-sidecar-watchdog.md)) — **мозг мониторинга в ОТДЕЛЬНОМ контейнере** (наблюдатель отделён от наблюдаемого, C-1): код в репо орка (`watchdog/`), рантайм — свой образ (`watchdog/Dockerfile`, `python:3.12-slim`, **stdlib-only**) + сервис в `docker-compose.yml` (`network_mode: host`, read-only `docker.sock`, `mem_limit: 128m`). На каждом тике собирает 4 источника: `GET /metrics` орка (F1a/ORCH-099), хост (диск/inode/память/CPU, stdlib), статусы контейнеров через read-only `docker.sock` (GET-only, без `docker` SDK), пинг Plane/Gitea/Anthropic. Каждый сигнал → **обобщённая чистая** `decide(signal_active, prev, now, cooldown)` (генерализация `disk_watchdog.decide_action`, per-signal in-memory `AlertState`) → алерт в **собственный** Telegram-канал sidecar (`WATCHDOG_TG_*`, **НЕ** импорт `src/notifications.py`). Особый сигнал `orch_down` — `/metrics` не отвечает (наблюдатель жив, наблюдаемый лёг). Диск: штатные 85% остаются за `disk_watchdog` (ORCH-063, нулевой дубль), sidecar — `orch_down` + opt-in потолок 97% (default off). never-raise, kill-switch `WATCHDOG_ENABLED`, строго read-only к наблюдаемому; `src/**`/`STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД орка — не тронуты. Подробнее ниже (§ «Sidecar-watchdog F1b»). Детали — `docs/work-items/ORCH-100/06-adr/ADR-001-sidecar-watchdog.md`.
|
||
|
||
## Сырьё-эндпоинт `/metrics` для sidecar (ORCH-099 — design)
|
||
|
||
`GET /metrics` (read-only, never-raise) отдаёт лёгкий JSON-снимок внутреннего «сырья» орка —
|
||
**стабильный контракт** для будущего sidecar-наблюдателя **F1b** (`watchdog/`). Орк отдаёт ТОЛЬКО
|
||
факты, которые знает лишь он сам; арбитраж «застрял/завис», пороги, алерты, история — на стороне
|
||
F1b (рамка C-1: наблюдатель отделён от наблюдаемого). Источник логики — `src/metrics.py`
|
||
(`build_metrics()`), эндпоинт — тонкая обёртка в `src/main.py`.
|
||
|
||
**Конверт ответа:**
|
||
```json
|
||
{
|
||
"schema_version": 1,
|
||
"generated_at": "<UTC ISO-8601 — момент снимка>",
|
||
"clk_tck": 100,
|
||
"stages": [ { "work_item", "stage", "age_in_stage_s", "repo", "task_id?" } ],
|
||
"queue": { "counts": {...}, "depth", "retries": {...}, "breaker": {...}|null, "max_concurrency" },
|
||
"agents": [ { "agent", "run_id", "job_id", "pid", "runtime_s", "model", "effort", "cpu_ticks"|null } ],
|
||
"cost": { "running": [...], "aggregate": { "cost_usd", "input_tokens", "output_tokens",
|
||
"cache_read_tokens", "cache_creation_tokens" } }
|
||
}
|
||
```
|
||
|
||
- **`stages`** — незавершённые задачи (`stage NOT IN ('done','cancelled')`, ORCH-090):
|
||
`work_item`/`stage`/`age_in_stage_s` (секунды с `tasks.updated_at`)/`repo`. Источник —
|
||
`db.get_active_tasks_for_reconcile()` + фильтр терминалов на слое metrics.
|
||
- **`queue`** — `db.job_status_counts()` (+`cancelled`), глубина, сырьё ретраев
|
||
(`attempts`/`max_attempts`/`transient_attempts`/в-backoff), `worker.breaker.snapshot()`
|
||
(`state`/`consecutive_transient`/`pause_remaining_s`), `max_concurrency`.
|
||
- **`agents` (liveness)** — по running-job (`db.get_running_agents()`):
|
||
`agent`/`run_id`/`job_id`/`pid`/`runtime_s` (= `running_age_s` от `jobs.started_at`)/`model`/
|
||
`effort` + **CPU-сырьё** `cpu_ticks` (utime+stime из `/proc/<pid>/stat`, поля 14+15). Орк дельту
|
||
**не считает** (stateless) — sidecar считает CPU-долю по двум опросам через `cpu_ticks`,
|
||
`clk_tck` и `generated_at`. `pid is None`/мёртвый/нет `/proc`/не-Linux → `cpu_ticks: null`.
|
||
- **`cost`** — `running` (по running-job, часто `null` до завершения: токены парсятся из CLI-JSON в
|
||
`launcher._monitor_agent` по окончании — `null` ≠ ноль) + `aggregate` (`db.agent_cost_totals()`,
|
||
`COALESCE(SUM(...),0)` по `agent_runs`).
|
||
|
||
**Контракт версии (NFR-6):** `schema_version` стартует с `1`. Аддитивные изменения (новое
|
||
поле/раздел) **НЕ бампят** версию — sidecar **обязан игнорировать незнакомые ключи и толерировать
|
||
отсутствие опциональных**; бамп — **только** при ломающем (rename/remove/retype существующего поля).
|
||
|
||
**Гарантии:** строго read-only (ни одного `INSERT/UPDATE/DELETE/CREATE/ALTER`, без
|
||
процессов/сети/сканов git); never-raise по разделам (ошибка раздела → `null`/`[]`/`{}` + WARNING,
|
||
эндпоинт всегда `200`); `/health`/`/status`/`/queue` — байт-в-байт прежние; `STAGE_TRANSITIONS`/
|
||
`QG_CHECKS`/`check_*`/machine-verdict-ключи/схема БД — не тронуты. Kill-switch
|
||
`metrics_endpoint_enabled` (env `ORCH_METRICS_ENABLED`, дефолт `True`; `False` → `200` с
|
||
`{"schema_version":1,"enabled":false}`). Self-hosting-безопасно: физически не влияет на конвейер.
|
||
Прямой потребитель контракта — **F1b** (заблокирована этой задачей).
|
||
|
||
Подробнее: [adr-0030](adr/adr-0030-metrics-endpoint.md), детально —
|
||
`docs/work-items/ORCH-099/06-adr/ADR-001-metrics-endpoint.md`.
|
||
|
||
## Sidecar-watchdog F1b (ORCH-100 — design)
|
||
|
||
**Вторая половина пары наблюдаемости.** F1a (ORCH-099) отдаёт сырьё через `GET /metrics`; F1b — мозг,
|
||
который это сырьё читает, дополняет внешними сигналами и превращает в алерты. Ключевая рамка
|
||
заказчика — **наблюдатель отделён от наблюдаемого** (C-1): частичные стражи (`disk_watchdog`/`reaper`/
|
||
`reconciler`) живут ВНУТРИ процесса орка и лягут вместе с ним; sidecar в отдельном контейнере
|
||
переживает падение орка и делает наблюдателя **громче** в инцидент.
|
||
|
||
- **Рантайм:** код в `watchdog/` (репо орка), но **отдельный контейнер** `orchestrator-watchdog`
|
||
(свой `watchdog/Dockerfile`, `python:3.12-slim`, **stdlib-only** — без сторонних зависимостей,
|
||
C-3 «тонкий стек, НЕ Grafana/Prometheus»). `network_mode: host` → `/metrics` достижим как
|
||
`http://127.0.0.1:8500/metrics`; `docker.sock` смонтирован **read-only**; `mem_limit: 128m`;
|
||
`restart: unless-stopped`.
|
||
- **4 коллектора на тик:** (a) `GET /metrics` орка (толерантный парсинг конверта F1a — неизвестные
|
||
ключи игнор, рост `schema_version` → warning); (b) хост — диск (`shutil.disk_usage`)/inode/память
|
||
(`/proc/meminfo`)/CPU; (c) контейнеры через read-only `docker.sock` — **только** GET list/inspect
|
||
(Up/healthy/restarting/exited/unhealthy), без `docker` SDK; (d) пинг Plane/Gitea/Anthropic.
|
||
- **Решение — обобщённая чистая функция** `decide(signal_active, prev, now, cooldown) -> alert |
|
||
realert | recovery | none` (строгая генерализация `src/disk_watchdog.py::decide_action`;
|
||
per-signal in-memory `AlertState`, рестарт → корректный повторный алерт стоящей проблемы). Реестр
|
||
сигналов: `orch_down` (K подряд неудачных опросов), `host_mem`, `host_disk_crit` (opt-in потолок),
|
||
`agent_hung` (доля CPU из Δ`cpu_ticks`/`clk_tck`/Δ`generated_at` < floor при растущем `runtime_s` —
|
||
sidecar stateful-арбитр), `stage_stuck` (`age_in_stage_s`), `job_failed` (edge), `queue_depth`,
|
||
`container_down` (per name), `dep_down` (per name). Пороги/интервалы/URL — из env (`WATCHDOG_*`).
|
||
- **`orch_down` — главный сигнал:** `/metrics` не отвечает (таймаут/refused/5xx/нечитаемо) → алерт
|
||
«орк не отвечает» через ту же машину порога/дедупа/recovery. Наблюдатель жив, наблюдаемый лёг.
|
||
- **Независимый Telegram-канал:** свои `WATCHDOG_TG_BOT_TOKEN`/`WATCHDOG_TG_CHAT_ID`; **запрещено**
|
||
импортировать `src/notifications.py` или использовать токен орка (иначе падение орка утянуло бы и
|
||
алерт-канал — нарушение C-1).
|
||
- **Владелец диск-алерта (BR-10, ADR-001 D6):** штатные 85% — ЕДИНСТВЕННО за внутренним
|
||
`disk_watchdog` (ORCH-063, канал орка) ⇒ **нулевой дубль по построению**; sidecar покрывает провал
|
||
«орк+disk_watchdog мертвы» через `orch_down`, плюс **opt-in** независимый критический потолок
|
||
`host_disk_crit` (97%, `WATCHDOG_DISK_CRIT_ENABLED=false` по умолчанию) — другое событие/канал.
|
||
- **Гарантии:** never-raise (per-source/per-tick/per-send); kill-switch `WATCHDOG_ENABLED=false` →
|
||
демон инертен (idle-loop, нулевой эффект на орк); строго read-only к наблюдаемому (нет
|
||
start/stop/restart/exec/записи в `docker.sock`/БД/`main`) ⇒ self-hosting-безопасно (enduro не
|
||
затронут). `src/**`/`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/схема БД орка — **не тронуты**
|
||
(F1b вне процесса орка и вне конвейера QG — как `disk_watchdog`/`reaper`/`reconciler`). Деплой
|
||
sidecar НЕ рестартит прод-контейнер `orchestrator`; прод-выкат — через staging-гейт (8501).
|
||
- **Инфра-предусловие (разовое, человек):** добавить сервис в compose, создать bot/chat watchdog,
|
||
смонтировать `docker.sock` `:ro` + хост-пути, первый запуск на хосте —
|
||
`docs/work-items/ORCH-100/07-infra-requirements.md`.
|
||
|
||
Подробнее: [adr-0033](adr/adr-0033-sidecar-watchdog.md), детально —
|
||
`docs/work-items/ORCH-100/06-adr/ADR-001-sidecar-watchdog.md`,
|
||
`docs/work-items/ORCH-100/07-infra-requirements.md`.
|
||
|
||
## Turnkey-онбординг проектов (ORCH-009)
|
||
|
||
Операторская способность развернуть **новый** проект одним проходом: Plane-проект (статусы с
|
||
точными именами + лейблы под машинные контракты) → Gitea-репо (+per-repo webhook) → каркас репо
|
||
(kit) → запись реестра → верификация. Реализуется **вне рантайма и вне конвейера**: `src/**`
|
||
байт-в-байт (`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схема БД — не тронуты),
|
||
kill-switch не нужен (активация — только явный запуск CLI человеком). Эталон — сам репозиторий
|
||
orchestrator (каноны ORCH-52b/c/d/e); enduro-trails эталоном не является.
|
||
|
||
- **Kit `onboarding/repo-skeleton/`** — параметризуемый каркас нового репо: 6 промптов агентов
|
||
канона 52d/92 (язык — канон орка: 5 ru + deployer en, ADR-001 D2 ORCH-092), паспорт `CLAUDE.md`,
|
||
`AGENTS.md` (точка входа агентов: карта доков + правила), `CONTRIBUTING.md`, `README`/`CHANGELOG`,
|
||
скелет `docs/` с обязательным `operations/INFRA.md`, `.env.example`. Плейсхолдеры `{{NAME}}` +
|
||
stdlib-рендер (без новых зависимостей); словарь — `onboarding/placeholders.json`. **Канон не
|
||
форкается (BR-2):** `docs/_templates/` + `docs/_standards/` не хранятся в kit — копируются live
|
||
из чекаута орка в момент материализации.
|
||
- **CLI `scripts/onboard_project.py`** — `plan` (дефолт, GET-only, ноль мутаций) / `apply`
|
||
(идемпотентный ensure, без delete-операций) / `verify` (round-trip реестра через фактический
|
||
`projects._parse_projects_json`, резолв всех статусов включая fail-closed `Confirm Deploy`/`STOP`,
|
||
лейблы, webhook, полнота kit, скан неразрешённых плейсхолдеров). Имена статусов — read-only
|
||
импорт `plane_sync._PLANE_NAME_TO_KEY` (22, нулевой дрейф); канонические группы фиксированы ADR
|
||
(код-критично: `STOP`→`cancelled` ORCH-090; терминальные группы только у Done/Cancelled/STOP —
|
||
иначе terminal-detection ORCH-068 ложно терминалит). Gitea-webhook переиспользует глобальный
|
||
`ORCH_GITEA_WEBHOOK_SECRET`; initial push — **только** в свежесозданный пустой репо (INV-4 не
|
||
затрагивается). Скрипт никогда не рестартит прод / не правит `.env` / ничего не удаляет;
|
||
регистрация в реестре = операторские env + управляемый рестарт (runbook). Недоступное в
|
||
Plane CE API → `manual-step` (fail-safe).
|
||
- **Runbook `docs/operations/ONBOARDING.md`** — чеклист всех слоёв, явные ручные шаги, smoke на
|
||
**staging-контуре** (8501, изолированная БД) с одноразовым sandbox-проектом, откат.
|
||
- **Анти-дрейф:** структурные канон-тесты kit (аналог `tests/test_agent_prompts_canon.py`) +
|
||
снапшот-тест `STAGE_TRANSITIONS`/`QG_CHECKS`.
|
||
|
||
Подробнее: [adr-0035](adr/adr-0035-turnkey-project-onboarding.md), детально —
|
||
`docs/work-items/ORCH-009/06-adr/ADR-001-turnkey-onboarding-kit-and-cli.md` (D1…D11),
|
||
`docs/work-items/ORCH-009/07-infra-requirements.md`.
|
||
|
||
## Тираж платформы: фундамент 10-common (ORCH-101)
|
||
|
||
Фундамент эпика ORCH-10 (D5.3 «Масштаб»: раздача платформы заказчикам-тестерам, типы A Lite /
|
||
B Bundled, оба stateless). Платформа разворачивается на новой инфре **без правки кода** — только
|
||
env/конфиг; конвейер (`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схема БД) —
|
||
байт-в-байт не тронут. Три слоя:
|
||
|
||
- **Расхардкод по принципу «дефолт = боевое значение»** (kill-switch-природа: отсутствие новых
|
||
переменных = текущее поведение 1:1). Новые ключи: `ORCH_AGENT_HOME_DIR` (HOME акторских
|
||
процессов: launcher ×2 / self-deploy / post-deploy), `ORCH_AGENT_GIT_NAME` +
|
||
`ORCH_GIT_EMAIL_DOMAIN` (git-идентичности `<actor>@<domain>`; системные имена
|
||
`deploy-finalizer`/`post-deploy-monitor` — платформенные литералы), `ORCH_STAGING_PORT`.
|
||
Ссылки в Plane-комментариях — из существующих `gitea_public_url`/`gitea_owner`.
|
||
`docker-compose.yml` — интерполяция `${VAR:-default}` (карта `ORCH_HOST_*`/`ORCH_DOCKER_GID`/
|
||
`ORCH_RUN_UID/GID`; группа ORCH-040 uid/gid/HOME/маунты двигается одними env насквозь, «МИНА 1»
|
||
`group_add` сохранена); `Dockerfile` — `ARG APP_*` (CMD не трогается: exec-form + `init: true`);
|
||
deploy-hook — `"${REPO:-…}"` + явная передача `REPO=` инвокерами. **Платформенные константы
|
||
(нормативно, НЕ конфиг):** `SELF_HOSTING_REPO="orchestrator"` (узел «empty CSV → self-hosting
|
||
only» всех `*_repos`-leaf'ов), имена сервисов/образов/профиля, контейнерный layout.
|
||
**Инвариант ORCH-058 усилен:** staging-порт конфигурируем только с fail-closed guard'ом
|
||
(`staging_port == прод-порт` → отказ freshness-пути ДО любого ssh/build, без тихого fallback).
|
||
- **Секреты нового хоста:** stdlib `scripts/gen_secrets.py` (криптослучайные webhook-секреты
|
||
`secrets.token_hex(32)`; печать по умолчанию; `--write` отказывает при существующем `.env`,
|
||
перезапись — только явный `--force`) + чек-лист внешних токенов. Норматив: боевые секреты
|
||
текущего хоста не копируются ни на одном шаге.
|
||
- **Smoke-верификация тиража:** runbook `docs/operations/REPLICATION.md` (deployment golden
|
||
source: карта env, чек-лист секретов, пошаговый smoke с PASS/FAIL — `/health` → `/queue`+
|
||
`/metrics` → `onboard_project.py plan/apply/verify` → тестовая задача → артефакты `01–04`;
|
||
расширенно — до `done`); без нового скрипта — кирпичи уже в репо. Анти-регресс — структурный
|
||
сканер `tests/test_no_host_hardcodes.py` (запрещённые литералы в исполняемом коде
|
||
`src/**`+`watchdog/**`; `tokenize`-исключение комментариев/докстрингов; config-модули — канон
|
||
дефолтов, вне скана; allowlist пуст).
|
||
|
||
Подробнее: [adr-0036](adr/adr-0036-replication-foundation-host-parametrization.md), детально —
|
||
`docs/work-items/ORCH-101/06-adr/ADR-001-host-parametrization-secrets-smoke.md` (D1…D10),
|
||
`docs/work-items/ORCH-101/07-infra-requirements.md`, `10-tech-risks.md`.
|
||
|
||
## Конвейер и Quality Gates
|
||
|
||
```
|
||
created → analysis → architecture → development → review → testing → deploy-staging → deploy → done
|
||
↑ │
|
||
└──── REQUEST_CHANGES ──────┘ (откат на development, max 3 retries)
|
||
```
|
||
|
||
| Стадия | Агент (выход) | Quality Gate | Артефакт |
|
||
|--------|---------------|--------------|----------|
|
||
| created | analyst | — | — |
|
||
| analysis | architect | `check_analysis_approved` | 01-brd / 02-trz / 03-acceptance-criteria / 04-test-plan.yaml |
|
||
| architecture | developer | `check_architecture_done` | 06-adr/ |
|
||
| development | reviewer | `check_ci_green` | код + PR |
|
||
| review | tester | `check_reviewer_verdict` | 12-review.md (`verdict:`) |
|
||
| testing | deployer | `check_tests_passed` | 13-test-report.md |
|
||
| deploy-staging | deployer | `check_staging_status` | 15-staging-log.md (`staging_status:`) |
|
||
| deploy | — | `check_deploy_status` | 14-deploy-log.md (`deploy_status:`) |
|
||
| done | — | — | — |
|
||
|
||
**Реестр QG** (`QG_CHECKS`): check_analysis_approved, check_analysis_complete, check_architecture_done, check_ci_green, check_review_approved, check_tests_passed, check_reviewer_verdict, check_tests_local, check_deploy_status, check_staging_status, check_branch_mergeable (ORCH-043), check_staging_image_fresh (ORCH-058), check_security_gate (ORCH-022), check_coverage_gate (ORCH-027).
|
||
|
||
**Канон гейтов:** машинные вердикты читаются ТОЛЬКО из YAML-frontmatter, никогда из прозы. Лог-файлы мержатся в `origin/main` отдельным PR; гейт читает из `origin/main`. **Единый frontmatter-контракт (ORCH-52c / ORCH-076):** парсинг YAML-frontmatter сведён к одной точке — `src/frontmatter.parse_frontmatter` (структура `data/has_block/malformed/yaml_error`, never-raise); пять вердикт-парсеров (`check_reviewer_verdict`, `_parse_tests_verdict`, `_parse_deploy_status`, `_parse_staging_status`, `parse_security_status`) делегируют ей вместо дублированной ad-hoc логики. Модуль также несёт writer (`render/write_frontmatter`), валидатор обязательной схемы (`validate_schema`/`REQUIRED_FIELDS`, warning-only по умолчанию; hard-fail только под kill-switch `frontmatter_validation_strict`, дефолт `False`) и общий `strip_frontmatter`. Семантика вердиктов / `STAGE_TRANSITIONS` / состав `QG_CHECKS` — без изменений (1:1).
|
||
|
||
### Стандарт документов конвейера (ORCH-075, ORCH-52b)
|
||
Структура номерных документов work item (`00-business-request.md` … `17-security-report.md`),
|
||
карта «стадия → агент → документ → категория → гейт/механизм → frontmatter machine-key» и
|
||
конвенция ADR-naming зафиксированы как golden source в
|
||
[`docs/_standards/PIPELINE_DOCS.md`](../_standards/PIPELINE_DOCS.md); копируемые скелеты — в
|
||
[`docs/_templates/`](../_templates/). Манифест **документирует** поведение гейтов (источник истины
|
||
остаётся код: `src/stages.py`, `src/qg/checks.py`), честно различая machine-verdict доки
|
||
(`12/13/14/15/17` — несут читаемый гейтом ключ) и информационные (`00/08/10/16` — гейтом не
|
||
парсятся). Это слой 1 (описательный). **Слой 2 (машинный) реализован в ORCH-52c (ORCH-076):**
|
||
единый frontmatter-контракт `src/frontmatter.py` + формальная спека handoff «стадия → обязательный
|
||
выход» с обязательной frontmatter-схемой (`REQUIRED_FIELDS`) —
|
||
[`docs/_standards/HANDOFF_PROTOCOL.md`](../_standards/HANDOFF_PROTOCOL.md). ADR:
|
||
[adr-0019](adr/adr-0019-pipeline-docs-standard.md) / [adr-0020](adr/adr-0020-frontmatter-contract.md),
|
||
детально — `docs/work-items/ORCH-075/06-adr/ADR-001-pipeline-docs-standard.md`,
|
||
`docs/work-items/ORCH-076/06-adr/ADR-001-frontmatter-contract.md`.
|
||
|
||
#### Слой промптов: канон Anthropic + эмиссия схемы 52c (ORCH-077, 52d — замыкает эпик 52)
|
||
**Слой 3 (промпты).** 52b дал описательный стандарт, 52c — машинный контракт (writer + валидатор
|
||
`REQUIRED_FIELDS`), но валидатор работал warning-only «вхолостую»: 6 системных промптов
|
||
`.openclaw/agents/*.md` **не эмитили** поля схемы. ORCH-077 учит все 6 промптов эмитить схему и
|
||
переписывает их в едином **каноне Anthropic** — замыкающее звено эпика. Это **docs/prompts-only**
|
||
изменение: `src/**`, `STAGE_TRANSITIONS`, `QG_CHECKS`, состав machine-verdict ключей и схема БД —
|
||
**не трогаются**; `frontmatter_validation_strict` остаётся `False` (эмиссия **добровольная**,
|
||
enforcement не включается).
|
||
- **Фиксированный XML-скелет (5 обязательных секций, нормативный порядок):** `<context>` → `<task>`
|
||
(+ опц. `<thinking>` у решающих ролей) → `<deliverables>` → `<constraints>` (запреты в формате
|
||
«❌ X → ✅ Y») → `<output_format>`. Доп. секции (`<success_criteria>`/`<escalation>`) — после.
|
||
- **Аддитивная схема 52c:** `<output_format>` каждого промпта перечисляет 6 полей
|
||
(`work_item`/`stage`/`author_agent`/`status`/`created_at`/`model_used`) с роле-специфичными
|
||
значениями и ставит их **рядом** с machine-verdict ключом, **не меняя его имя/регистр/значения**
|
||
(`verdict:`/`result:`/`staging_status:`/`deploy_status:`/`security_status:` — байт-в-байт). Гейты
|
||
читают вердикты как раньше (NFR-1). Для `04-test-plan.yaml` (чистый YAML) — top-level ключи.
|
||
- **Loading-model (важно для self-hosting):** промпт `cat`-ается из git-worktree агента в момент
|
||
запуска (`launcher` `--system-prompt "$(cat .openclaw/agents/<role>.md)"`), НЕ запекается в образ →
|
||
новые промпты вступают в силу на следующем worktree от `main` **без прод-рестарта**; reviewer/tester
|
||
той же задачи исполняются уже под новыми промптами (естественный in-vivo A/B, BR-6).
|
||
- **Анти-регресс:** структурные тесты `tests/test_agent_prompts_canon.py` (5 секций, 6 полей, точный
|
||
регистр verdict-ключей, self-hosting-маркеры deployer'а); `test_agent_frontmatter_no_model.py`
|
||
остаётся зелёным. **Норматив на будущее:** новые/изменённые агент-промпты следуют этому канону.
|
||
- ADR: [adr-0021](adr/adr-0021-prompt-canon-anthropic.md); детально —
|
||
`docs/work-items/ORCH-077/06-adr/ADR-001-anthropic-prompt-canon.md`.
|
||
- **Промпт-аудит ORCH-092 (эпилог эпика 52, docs/prompts-only):** точечно устранён класс дефектов
|
||
промптов поверх канона 52d. (1) **Расхардкод примеров:** копируемые frontmatter-примеры всех 6
|
||
промптов несут плейсхолдеры `created_at: <YYYY-MM-DD>` / `model_used: <resolve ORCH-41>` +
|
||
врезку «подставь `date +%F` и модель из конфига, не копируй буквально» (литерал `claude-opus-4-8`
|
||
оставлен лишь справкой в таблице полей). (2) **Секция `<escalation>`** добавлена developer/
|
||
reviewer/tester (после `</success_criteria>`, не нарушая порядок 5 обязательных секций):
|
||
developer → `back-to:analysis`, tester → `back-to:dev`, reviewer → `REQUEST_CHANGES`.
|
||
(3) **developer:** убран ручной `git rebase origin/main` — свежесть базы держит движок
|
||
(serial-gate ORCH-088 + `auto_rebase_onto_main` под merge-lease), а ручной rebase конфликтовал с
|
||
собственным запретом force-push (ADR-001 D1); «PR>1500 → разбивай» переформулирован в эскалацию
|
||
на уровне задач. (4) **tester** обогащён worktree-путём, smoke-проверкой блока `serial_gate` и
|
||
требованием покрытия каждого TC. (5) **reviewer:** удалена мёртвая строка «тот же экземпляр
|
||
Developer». (6) **Языковое исключение (ADR-001 D2, нормативно):** `deployer.md` сознательно
|
||
остаётся на **английском** (5 ru + 1 en) — самый safety-critical промпт, минимизация
|
||
регресс-поверхности у байт-точных verdict-ключей/команд; критичные self-hosting-запреты подняты в
|
||
видную рамку в начале `<context>`. Это **документированное исключение**, не дрейф: будущему агенту
|
||
НЕ «чинить» язык deployer вслепую. Машинные verdict-ключи и канон 52d — байт-в-байт; анти-регресс —
|
||
расширенный `tests/test_agent_prompts_canon.py` (ORCH-092 TC-01…TC-08). ADR:
|
||
`docs/work-items/ORCH-092/06-adr/ADR-001-developer-rebase-and-deployer-language.md`.
|
||
|
||
#### Слой трассировки: стандарт маркеров `ORCH-NNN` (ORCH-078, 52e — слой 4 эпика 52)
|
||
**Слой 4 (трассировка).** Маркеры `ORCH-NNN`/`ET-NNN` в коде (де-факто **51 уникальный** в `src/`)
|
||
привязывают нетривиальные инварианты к породившему их work item, но это была **сложившаяся практика
|
||
без формального контракта**. ORCH-078 кодифицирует её как нормативный стандарт
|
||
[`docs/_standards/TRACEABILITY.md`](../_standards/TRACEABILITY.md) (рядом с `PIPELINE_DOCS.md` и
|
||
`HANDOFF_PROTOCOL.md`) и точечно дополняет 3 промпта правилом чтения. Это **docs/prompts-only**
|
||
изменение: `src/**`, `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД — **не трогаются**; стандарт —
|
||
описательно-нормативный, **не машинный гейт** (массовый ретро-фит 51 маркера вне объёма).
|
||
- **Каноничное правило чтения (единый источник):** правишь код с маркером `ORCH-NNN` → прочитай его
|
||
`06-adr` ПЕРЕД изменением, не сломай инвариант. Промпты `developer`/`architect`/`reviewer`
|
||
**ссылаются** на текст в `TRACEABILITY.md`, а не копируют его (нет дрейфа между файлами).
|
||
- **Fallback-доступ:** папки `docs/work-items/ORCH-NNN/` нет в ветке → `git show origin/main:...`.
|
||
- **Анти-археология:** блок с **3+** маркерами → одна сводная ссылка на сквозной ADR
|
||
(`docs/architecture/adr/`) вместо перечисления всех work item.
|
||
- **Контроль:** reviewer ловит правку маркированного кода без сверки с ADR → finding ≥P1.
|
||
- **Анти-регресс:** расширенный `tests/test_agent_prompts_canon.py` (наличие правила/ссылок); канон
|
||
52d (5 секций, 6 полей, регистр verdict-ключей) и `test_agent_frontmatter_no_model.py` зелёные.
|
||
- ADR: [adr-0022](adr/adr-0022-traceability-marker-standard.md); детально —
|
||
`docs/work-items/ORCH-078/06-adr/ADR-001-traceability-marker-standard.md`.
|
||
|
||
#### Слой обзорных доков: reviewer-ось README-ограничений + закрытие эпика 52 (ORCH-079, 52f — слой 5/финал)
|
||
**Слой 5 (финал).** 52b–52e привели в порядок структуру доков, машинный frontmatter, канон промптов
|
||
и трассировку, но **корневой `README.md`** — обзорная витрина проекта — остался незакрытым и **выдавал
|
||
решённое за открытое**: секция «Известные ограничения» имела битую нумерацию (`1,2,3,4,3,4`) и пункты,
|
||
опровергнутые кодом (worktree-гонки → `ensure_worktree`+ORCH-026/088; in-process daemon → очередь
|
||
ORCH-1; «Gitea CI не настроен» → `check_ci_green`; «no retry» → backoff/breaker `queue_worker.py`;
|
||
устаревшие issue-ID → зрелый `plane_sync` ORCH-010/066/068; Playwright-timeout → watchdog ORCH-7).
|
||
ORCH-079 синхронизирует витрину с кодом и закрывает **процессный пробел**: reviewer не контролировал
|
||
обновление обзорных доков. Это **docs + prompt-only** изменение: `src/**`, `STAGE_TRANSITIONS`,
|
||
`QG_CHECKS`, схема БД — **не трогаются**.
|
||
- **Reviewer-ось «обзорные доки»:** `.openclaw/agents/reviewer.md` ось 4 «Документация» + `<constraints>`
|
||
несут врезку «❌→✅»: *PR закрыл пункт README «Известные ограничения», README не обновлён → finding*
|
||
(≥P1; при закрытии правкой `src/` без обновления README — совпадает с существующим P0). Канон 52d
|
||
(5 секций) и `verdict: APPROVED|REQUEST_CHANGES` — байт-в-байт; правило нормативно-описательное (не
|
||
машинный гейт), как ось трассировки ORCH-078.
|
||
- **Витрина по коду (NFR-3):** решённые пункты сняты/перенесены в «Закрыто (история)» с ORCH-ссылками;
|
||
в «открытых» — только реально открытые, верифицированные кодом/задачей; запрет изобретать
|
||
ограничения (анти-scope-creep).
|
||
- **Эпик ORCH-52 закрыт:** 52b (adr-0019) → 52c (adr-0020) → 52d (adr-0021) → 52e (adr-0022) →
|
||
**52f (adr-0023)**.
|
||
- **Анти-регресс:** `tests/test_agent_prompts_canon.py` (assert наличия оси обзорных доков); канон 52d
|
||
и `test_agent_frontmatter_no_model.py` зелёные.
|
||
- ADR: [adr-0023](adr/adr-0023-overview-docs-reviewer-axis-and-epic52-close.md); детально —
|
||
`docs/work-items/ORCH-079/06-adr/ADR-001-readme-sync-and-reviewer-overview-docs-axis.md`.
|
||
|
||
### Модель и эффорт по ролям (ORCH-41, валидация ORCH-74)
|
||
Модель и `--effort` каждого агента берутся из config (`src/config.py`), резолвятся `launcher.resolve_agent_model` / `resolve_agent_effort` по приоритету **project-override (`projects_json` `agent_models`/`agent_efforts`) > `ORCH_AGENT_MODEL_<AGENT>`/`ORCH_AGENT_EFFORT_<AGENT>` > `*_default` > CLI-дефолт (без флага)**. **Эффорт (ORCH-081):** ниже `*_default` добавлен непустой **per-role floor** — class-default поля `agent_effort_<role>` из `config.py` (его пустой env перебить не может). Floor — строго последний уровень (ниже default) и срабатывает ТОЛЬКО когда все уровни пусты, поэтому пустые прод-`ORCH_AGENT_EFFORT_*=` (которые pydantic трактует как явное `''` и обнуляют дефолт) больше не приводят к запуску без `--effort`: каждая роль получает свой канонический пол (developer=`xhigh`, tester/deployer=`medium`, прочие=`high`). Непустой явный конфиг по-прежнему побеждает floor; опечатка вне `VALID_EFFORTS` дропается валидацией ДО floor (never-break, не маскируется). См. `docs/work-items/ORCH-081/06-adr/ADR-001-effort-resolution-floor.md`. frontmatter `model:` в `.openclaw/agents/*.md` **удалён** (ORCH-74 G1) — он был мёртвой/лживой декларацией (launcher его не читает); config — единственный источник правды о модели. Model-routing (G3) НЕ включён — все 6 агентов на `claude-opus-4-8`.
|
||
|
||
| Агент | Модель | Эффорт |
|
||
|-------|--------|--------|
|
||
| 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 |
|
||
|
||
**Валидация (ORCH-74 G2, never-break):** резолвенное имя модели проходит формат-чек `is_valid_model` (`^claude-[a-z0-9.-]+$`) перед попаданием в `--model`. Невалидное (опечатка, `gpt-4`, пустое) → `logger.warning` + откат на следующий валидный уровень (в пределе — без `--model`, CLI-дефолт); мусор **никогда** не уезжает в CLI и запуск не падает. Форма — формат-чек, а не статичный allowlist: forward-compatible (будущие `claude-*` проходят без правки кода). Тот же предикат гардит inline-чтение `--fallback-model` (`agent_fallback_model` читается мимо резолва — TRZ §4). Эффорт валидируется множеством `VALID_EFFORTS` (`low|medium|high|xhigh|max`). Fallback (G4) НЕ включён (`agent_fallback_model=""`). Детали — `docs/work-items/ORCH-074/06-adr/ADR-001-model-name-validation.md`.
|
||
|
||
### Условный staging-гейт (ORCH-35)
|
||
`check_staging_status` реален только для self-hosting (`is_self_hosting_repo(repo)` → `orchestrator`); для остальных проектов → no-op `(True, "Staging gate N/A")`. Для orchestrator парсит `staging_status:` из `15-staging-log.md`; FAILED → откат на `development`. Подробнее: [ADR-0003](adr/adr-0003-staging-gate.md).
|
||
|
||
### Толерантность staging-вердикта к инфра-FAIL (ORCH-061 — design)
|
||
Self-hosting зацикливался на `deploy-staging`: `scripts/staging_check.py` давал ложный FAILED на C9a/C9b (ветка в sandbox / analyst-job в очереди), вызванный **отсутствием sandbox-настроек** (bot-аккаунты не члены SANDBOX-проекта), а не регрессом кода → откат `deploy-staging → development` → петля. ORCH-061 классифицирует проверки suite на **REAL** (pipeline) и **SANDBOX_INFRA** (узкий allowlist `{C9a, C9b}`) и делает вердикт толерантным к инфра-FAIL, сохраняя fail-closed для реальных проверок:
|
||
- Чистая логика — leaf-модуль `src/staging_verdict.py` (`classify_check`, `compute_staging_verdict`, never-raise). Упала хоть одна REAL → FAILED/exit1; упали ТОЛЬКО SANDBOX_INFRA и толерантность вкл → SUCCESS/exit0 (waived); waiver применяется только когда все REAL (вкл. C7/C8) зелёные.
|
||
- `scripts/staging_check.py` помечает проверки категориями, считает вердикт через `staging_verdict`, печатает `INFRA-WAIVED` (наблюдаемость).
|
||
- Kill-switch `staging_infra_tolerance_enabled` (env `ORCH_STAGING_INFRA_TOLERANCE_ENABLED`, дефолт `true`, в `.env.staging`); `false` → 1:1 прежнее строгое поведение.
|
||
- `check_staging_status` / `_parse_staging_status` / `STAGE_TRANSITIONS` / реестр `QG_CHECKS` — **без изменений** (новый QG-чек не вводится); условность ORCH-35 и схема БД сохранены.
|
||
- Инвариант: «no changes to commit» на action-стадиях (`deploy-staging`/`deploy`) не есть недовыполнение — продвижение определяется exit0 + гейт-вердиктом (launcher не откатывает; добавлена observability-строка).
|
||
|
||
Подробнее: [adr-0009](adr/adr-0009-staging-infra-tolerance.md), детально — `docs/work-items/ORCH-061/06-adr/ADR-001-staging-infra-tolerance.md`.
|
||
|
||
### Merge-gate: догон `main` + re-test + сериализация слияний (ORCH-043)
|
||
Детерминированный под-гейт (`check_branch_mergeable`, без LLM) на ребре **`deploy-staging → deploy`**: исполняется ПОСЛЕ `check_staging_status` и ДО запуска deployer'а, который вливает PR в `main` (deployer мержит в начале стадии `deploy`). Стадии (`STAGE_TRANSITIONS`) НЕ меняются — это «под-гейт» ребра, а не отдельная стадия (триггер — то же событие «staging-deployer завершился»).
|
||
|
||
Назначение: ветка валидируется относительно того `main`, из которого создана; параллельная задача могла уйти вперёд → семантический конфликт слияния (зелёная ветка ломает обновлённый `main`). Merge-gate гарантирует проверку против **актуального** `origin/main` перед слиянием:
|
||
- **Догон:** ветка отстаёт (⇔ `origin/main` не предок HEAD) → `rebase origin/main` в worktree + `push --force-with-lease` (ТОЛЬКО ветка задачи; `main` — никогда). Текстовый конфликт → `rebase --abort` → откат на `development`.
|
||
- **Безусловный pre-merge rebase (ORCH-026, A-2):** при `premerge_rebase_always` (дефолт `True`, скоуп `merge_gate_repos`) short-circuit `branch_is_behind_main` пропускается — `auto_rebase_onto_main` вызывается **всегда** под лизом. На актуальной ветке это no-op (`rebase` не меняет HEAD, `push --force-with-lease` → «Everything up-to-date», CI не триггерится); на отстающей — реальный догон. Детерминированный структурный анти-фантом на уровне планировщика (дополняет рубежи ORCH-073, не заменяет). Kill-switch `premerge_rebase_always=False` → прежнее поведение (ребейз только при behind).
|
||
- **Re-test:** `python -m pytest` (`merge_retest_target`, дефолт `tests/`) в worktree догнанной ветки, тайм-аут `merge_retest_timeout_s`. Красный/тайм-аут → откат на `development`.
|
||
- **Сериализация (merge-lock):** файловый **merge-lease** на репо (`<repos_dir>/.merge-lease-<repo>.json`), живёт от гейта до фактического merge. Acquire **неблокирующий** (anti-deadlock при `max_concurrency=1`): busy → **defer** (повторная постановка deployer'а на `deploy-staging` с задержкой через `available_at`), а не откат. Release — на PR-merged вебхуке / `deploy→done` / откате / по возрасту (crash-реклейм). Restart-safe; без изменения схемы БД. **ORCH-026 (A-1):** это окно = «merge → main-updated» (для self `done` ⇔ SHA-in-main, ORCH-073) — пока A не в `main`, B того же репо получает `merge-lock busy` → defer. Окно сериализации per-repo НЕ переписывается; кросс-репо параллелизм сохранён (лиз — per-repo файл).
|
||
- **Условность (как ORCH-35):** реален для `orchestrator`; прочие репо — no-op. Флаги `merge_gate_enabled` / `merge_gate_repos` — поэтапный раскат. Контракт **never-raise**.
|
||
|
||
Подробнее: [adr-0006](adr/adr-0006-merge-gate.md), детально — `docs/work-items/ORCH-043/06-adr/ADR-001-merge-gate.md`.
|
||
Безусловный pre-merge rebase + связь с зависимостями задач — [adr-0015](adr/adr-0015-task-deps-and-merge-serialization.md) (ORCH-026).
|
||
|
||
### Coverage-гейт: защита от деградации покрытия тестами (ORCH-027 — design)
|
||
Существующие тестовые гейты (`check_ci_green`, `check_tests_passed`, merge-gate re-test) судят
|
||
только по факту прохождения тестов, не по **полноте** — фича «300 строк, 0 тестов» проходит
|
||
незамеченной, и при пакетном автономном прогоне (ORCH-088) покрытие монотонно деградирует.
|
||
ORCH-027 вводит детерминированный (без LLM) **гейт покрытия как под-гейт ребра
|
||
`deploy-staging → deploy`** — рядом с security/merge/image-freshness, по тому же паттерну
|
||
(leaf `src/coverage_gate.py` never-raise + обёртка `check_coverage_gate` в `QG_CHECKS` + врезка
|
||
`_handle_coverage_gate` в `advance_stage`). `STAGE_TRANSITIONS` не меняется.
|
||
- **Порядок: security → merge → coverage → image-freshness.** Coverage идёт **ПОСЛЕ merge-gate**
|
||
(ветка догнана на свежий `origin/main` → меряем покрытие landed-кода) и **ДО image-freshness**
|
||
(фейлить дёшево до docker-rebuild). На этой точке merge-lease **held** → FAIL **освобождает
|
||
lease** при откате (как image-freshness rollback; в отличие от security — тот до захвата lease).
|
||
- **Измерение:** `pytest-cov` (`coverage.py`), `python -m pytest tests/ --cov=src
|
||
--cov-report=json` в изолированном worktree (`ensure_worktree`); метрика `totals.percent_covered`
|
||
(line coverage `src/`). Тайм-аут `coverage_run_timeout_s`.
|
||
- **Решение — чистая функция** `compute_coverage_verdict(measured, baseline, floor, policy,
|
||
epsilon)`: `absolute` (≥floor−ε) / `baseline` (≥baseline−ε, ratchet) / `both` (дефолт);
|
||
`baseline=None` → bootstrap. FAIL → откат на `development` + developer-retry (cap
|
||
`MAX_DEVELOPER_RETRIES`), дословный reason в `task_desc` (ORCH-046).
|
||
- **Базовая линия — аддитивная БД-таблица** `coverage_baseline(repo PK, coverage, source_sha,
|
||
updated_at)` (`CREATE TABLE IF NOT EXISTS`, паттерн `repo_freeze`/`job_deps`; выбор БД над
|
||
файлом-в-репо — нет git-churn/конфликтов на ratchet). **Ratchet-up** в choke-point
|
||
подтверждённого merge `_handle_merge_verify` (ребро `deploy→done`, ORCH-071/073): читает
|
||
измеренное покрытие из `18-coverage-report.md`, атомарный compare-and-set
|
||
`UPDATE ... WHERE coverage <= measured` (базовая линия не падает) под held merge-lease +
|
||
per-repo сериализацией merge (ORCH-043).
|
||
- **Условность (как ORCH-35/43/58):** `coverage_gate_enabled` + `coverage_gate_repos` (пусто →
|
||
только self-hosting `orchestrator`); вне области → no-op pass; `applies(repo)` ПЕРВОЙ.
|
||
**Ошибка инструмента → fail-open + WARNING** по умолчанию (`coverage_tool_fail_closed=False`,
|
||
анти-петля как ORCH-061); флаг → fail-closed.
|
||
- **Артефакт `18-coverage-report.md`** (frontmatter `coverage_status: PASS|FAIL` +
|
||
`measured_coverage`/`baseline`/`floor`/`policy`/`delta`), вердикт читается ТОЛЬКО из
|
||
frontmatter через `src/frontmatter.py`. Наблюдаемость — read-only блок `coverage` в
|
||
`GET /queue`; FAIL → Telegram (кликабельный номер, измеренное/порог/дельта); опциональный
|
||
`POST /coverage/baseline` (ручной override). never-raise; гейт не деплоит/не рестартит прод/
|
||
не пушит в `main` (NFR-3). При выключенном флаге — нулевая регрессия (enduro не затронут).
|
||
|
||
Подробнее: [adr-0029](adr/adr-0029-coverage-gate.md), детально —
|
||
`docs/work-items/ORCH-027/06-adr/ADR-001-coverage-gate.md`,
|
||
`docs/work-items/ORCH-027/08-data-requirements.md`.
|
||
|
||
### Зависимости задач: B ждёт A (ORCH-026, Уровень B)
|
||
Плоская очередь ORCH-1 (FIFO по `id` + `available_at` + `max_concurrency`) не выражала логических зависимостей. ORCH-026 вводит декларативные связи «задача B не стартует, пока не готовы её depends-on» — без новой стадии и без изменения `STAGE_TRANSITIONS`/`QG_CHECKS`.
|
||
- **Источник истины планировщика — БД** (аддитивная таблица `job_deps(task_id, depends_on_task_id)`): claim в горячем цикле обслуживает очередь ВСЕХ проектов и обязан быть offline-устойчив (сетевой Plane на каждый claim = встанет очередь всех проектов). Источник **декларации** настраивается `task_deps_source = db|plane|hybrid` (дефолт `db`; `plane`/`hybrid` читают Plane relations в `handle_work_item_created` и кэшируют в `job_deps`).
|
||
- **Гейт планировщика (`claim_next_job`)** — условие `NOT EXISTS (job_deps d JOIN tasks t … WHERE d.task_id=j.task_id AND t.stage!='done')` при `task_deps_enabled`: задача с незавершённой зависимостью **не выбирается** (агент не запускается, слот `max_concurrency` не занимается). Инертно при пустой `job_deps` → нулевая регрессия; kill-switch `task_deps_enabled=False` → запрос 1:1 как ORCH-1.
|
||
- **Детект дедлоков** — DFS-цикл-детектор (leaf `src/task_deps.py::detect_cycle`) при вставке связи + backstop в `reconciler`; цикл → `set_issue_blocked` + alert (Telegram/Plane) с перечислением цикла. Поток остальных задач не блокируется.
|
||
- **Видимость** — строка «⏳ ждёт ORCH-NNN» в Telegram-карточке (`update_task_tracker`, never-raise); Plane `Blocked` — на дедлоке (не на нормальном коротком ожидании, чтобы не флаппить). Инвариант «одна карточка на задачу» сохранён.
|
||
- **Совместимость:** `reconciler` F-1 пропускает dep-заблокированные задачи (`is_task_ready`, паттерн ORCH-060); `reaper` сканирует только `running` → dep-блок остаётся `queued`, не трогается. Зависимости — только intra-repo (v1).
|
||
- **Наблюдаемость:** блок `task_deps` в `GET /queue` (заблокированные задачи, держатель merge-lease, defer-счётчики, обнаруженные циклы) — read-only.
|
||
|
||
Подробнее: [adr-0015](adr/adr-0015-task-deps-and-merge-serialization.md), детально — `docs/work-items/ORCH-026/06-adr/ADR-001-merge-serialization-and-task-deps.md`.
|
||
|
||
### Per-repo serial gate: пакетный автономный режим (ORCH-088 — реализовано)
|
||
Эпик «10–20 задач за ночь», Этап 1 (serial e2e). Закрывает **stale-анализ**: ветка задачи N+1
|
||
срезалась на входе в анализ (`start_pipeline._create_gitea_branch`) от `main`, ещё не содержащего код
|
||
предшественника N (физическое код-затирание уже закрыто ORCH-026; ORCH-088 — **логический** разрыв).
|
||
Новая задача репо не входит в `analysis` (не режет ветку, не запускает analyst), пока в том же репо
|
||
есть незавершённая задача (`stage != 'done'`) или репо заморожен. Аддитивно, под kill-switch, область
|
||
репо, never-raise, restart-safe; `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` — **без изменений**.
|
||
- **Gate-в-claim** (`db.claim_next_job`) — analyst-job (`jobs.agent='analyst'`) применимого репо не
|
||
выбирается, если `EXISTS` **более ранняя** незавершённая задача репо (`t2.id < jobs.task_id`) ИЛИ
|
||
активна строка `repo_freeze`. По образцу `task_deps` `NOT EXISTS` (ORCH-026); только локальная БД
|
||
(offline hot-path, NFR-2). Job'ы уже активной задачи проходят свободно. **FIFO-уточнение реализации
|
||
(FR-2):** ADR-001 D1 фиксировал псевдо-SQL `t2.id != jobs.task_id`; при `!=` пакет одновременно
|
||
созданных свежих задач (все в `analysis`) взаимно блокировался бы (каждая — «другая незавершённая»
|
||
для остальных) ⇒ дедлок всей serial-очереди. `<` допускает ровно самую раннюю задачу и сериализует
|
||
остальные за ней (строго по одной, FIFO по `jobs.id`), при этом по-прежнему не блокирует rework-analyst
|
||
собственной задачи (R-7) и сохраняет AC-1.
|
||
- **Отложенный срез ветки (анти-stale-base, AC-6):** для применимого репо `start_pipeline` создаёт
|
||
task-row + enqueue analyst, но **не** создаёт Gitea-ветку/docs; срез релоцируется на момент claim
|
||
analyst-job (launcher), когда `origin/main` уже содержит предшественника (`done` ⇔ SHA-в-main,
|
||
ORCH-071/073). `ensure_worktree` режет от свежего `origin/main` ⇒ AC-6 структурно. Идемпотентно
|
||
(`_create_gitea_branch` 409 = no-op).
|
||
- **Durable per-repo freeze** (новая аддитивная таблица `repo_freeze`, `cleared_at IS NULL` = активен) —
|
||
post-deploy `DEGRADED`/rollback (ORCH-021) → `set_repo_freeze` + Telegram-алерт; gate закрыт
|
||
безусловно до **ручного** снятия (`POST /serial-gate/unfreeze`). Деградировавшая задача уже `done`
|
||
(BR-7) ⇒ отдельный сигнал, независимый от `stage`.
|
||
- **Согласование NFR-1:** hot-claim тотальный сбой построения gate-фрагмента → **fail-open** (не
|
||
заклинить очередь всех проектов, AC-8); freeze в Python-слое (`is_repo_frozen`) → **fail-closed**
|
||
(безопасность прода, AC-9).
|
||
- Чистая логика — leaf `src/serial_gate.py` (never-raise). Флаги `serial_gate_enabled` (kill-switch),
|
||
`serial_gate_repos` (CSV; **пусто ⇒ все репо**, в отличие от self-hosting-only ORCH-35/43/58),
|
||
`serial_gate_freeze_enabled`. Наблюдаемость — аддитивный блок `serial_gate` в `GET /queue`
|
||
(per-repo `active_task` / `waiting` / `frozen`). Cross-repo параллелизм сохранён (FR-3); при
|
||
выключенном флаге — нулевая регрессия (enduro не затронут).
|
||
|
||
Подробнее: [adr-0017](adr/adr-0017-serial-gate.md), детально —
|
||
`docs/work-items/ORCH-088/06-adr/ADR-001-serial-gate.md`,
|
||
`docs/work-items/ORCH-088/08-data-requirements.md`.
|
||
|
||
### Авто-режим по лейблам: autoApprove + autoDeploy (ORCH-089 — реализовано)
|
||
Конвейер имеет два **человеческих** гейта, тормозящих пакетный автономный прогон (эпик
|
||
ORCH-088): гейт BRD (`analysis`: ждёт ручного `Approved`) и гейт прод-деплоя (`deploy`:
|
||
Phase A ждёт ручного `Confirm Deploy`, ORCH-059). ORCH-089 снимает **только эти два
|
||
человеческих решения** — выборочно (лейбл Plane на задаче), декларативно, обратимо, **не
|
||
трогая ни одной технической проверки**. Аддитивно, по образцу условных под-гейтов
|
||
(ORCH-035/043/058/059/088): leaf `src/labels.py` (never-raise) + точечные врезки + флаги;
|
||
`STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / схема БД — **не трогаются**.
|
||
- **`autoApprove`** → врезка в `stage_engine._handle_analysis_approved_flow` (ветка
|
||
`files_ok`) после `In Review`+коммента: `set_issue_approved` (индикация) +
|
||
лог/Telegram/Plane-коммент + `advance_stage(..., finished_agent=None)` — **тот же путь, что
|
||
человеческий Approved** (`approved-via-status` → `analysis → architecture` +
|
||
`mark_brd_review_ended`). Без дублирования переходной логики.
|
||
- **`autoDeploy`** → врезка в `stage_engine._handle_self_deploy_phase_a` сразу после advance
|
||
на `deploy` + `clear_state`: лог/Telegram/Plane-коммент + `_handle_self_deploy_phase_b(...)`
|
||
(idempotency-маркер `INITIATED`, статус `Deploying`, finalizer). Пропускаются лишь
|
||
индикативно-человеческие шаги (`Awaiting Deploy` + «ask-human»). **BR-5 структурно:** Phase A
|
||
достигается только после зелёных под-гейтов ребра `deploy-staging → deploy` (security →
|
||
merge-gate → image-freshness → staging) → autoDeploy физически не деплоит сломанное.
|
||
- **Чтение лейблов** — `plane_sync.fetch_issue_labels` (поле `labels` issue, `None` при
|
||
ошибке ≠ `[]`) + `get_project_labels` (`{normalized_name→uuid}`, TTL-кэш по образцу
|
||
`get_project_states`); сопоставление по нормализованному имени (`strip().casefold()`),
|
||
неоднозначность → «нет лейбла». Источник истины — Plane API, не payload вебхука. Новый
|
||
сеттер `set_issue_approved` (ключ `approved` уже в `_DEFAULT_STATES`).
|
||
- **Флаги** (`config.py`): `auto_label_enabled` (kill-switch), `auto_approve_label`/
|
||
`auto_deploy_label`, `auto_label_repos` (CSV; **пусто → self-hosting only**),
|
||
`auto_label_states_ttl_s`. `applies(repo)` (локальный) проверяется ПЕРВЫМ; `has_label`
|
||
(сеть) — только при `applies==True` → при выключенном флаге нулевой сетевой оверхед,
|
||
нулевая регрессия для enduro.
|
||
- **Fail-safe (never auto):** любая ошибка/недоступность Plane/неоднозначность →
|
||
«нет авто» → ручной гейт (never-raise). **Идемпотентность:** autoApprove — advance один раз
|
||
(поздний Approved/F-2 видят `architecture`); autoDeploy — маркер `INITIATED`. **Прозрачность
|
||
(AC-7):** лог + Telegram + Plane-коммент + live-карточка; блок `auto_labels` в `GET /queue`.
|
||
- **Инфра-предусловие:** создать лейблы `autoApprove`/`autoDeploy` в Plane-проекте ORCH
|
||
(labels API); их отсутствие = `has_label` False = ручной режим (fail-safe).
|
||
|
||
Подробнее: [adr-0018](adr/adr-0018-auto-label-gates.md), детально —
|
||
`docs/work-items/ORCH-089/06-adr/ADR-001-auto-label-gates.md`,
|
||
`docs/work-items/ORCH-089/07-infra-requirements.md`.
|
||
|
||
### Багфикс-трек: укороченный маршрут для багов (ORCH-019 — реализовано)
|
||
Задача с меткой Plane `Bug` идёт по **укороченному** маршруту `analysis(lite) → development →
|
||
review → testing → deploy-staging → deploy → done`, **минуя стадию `architecture`** (отдельный
|
||
прогон opus-агента `architect` + ADR + exit-гейт `check_architecture_done`). **Корневой инвариант
|
||
(NFR-1):** срезается ТОЛЬКО аналитика/архитектура; ни один Quality Gate / под-гейт
|
||
(security/merge/coverage/image-freshness) / вердикт-ключ — НЕ ослаблен (урок ET-8). Аддитивно, под
|
||
kill-switch, per-repo, never-raise, fail-safe → полный цикл; `STAGE_TRANSITIONS`/`QG_CHECKS`/
|
||
`check_*` — **не трогаются**.
|
||
- **Багфикс-трек = свойство планировщика/точки входа, НЕ Quality Gate.** Классификация —
|
||
leaf `src/bug_fast_track.py` (never-raise, образец `serial_gate`/`labels`): метка `Bug`
|
||
читается аппаратом ORCH-089 (`labels.has_label` + `plane_sync.fetch_issue_labels`), задача
|
||
помечается `track='bug'`. `applies(repo)` (локально, без сети) — ПЕРВЫМ; `has_label` (сеть) —
|
||
только при `applies==True`; чтение метки **только** в `start_pipeline`, никогда в горячем
|
||
`claim_next_job` (NFR-4 anti-stall).
|
||
- **Хранение типа** — аддитивная колонка `tasks.track TEXT DEFAULT 'full'` (`_ensure_column`,
|
||
паттерн `tasks.cancelled_at` ORCH-090); читается в `advance_stage` из БД, не из сети.
|
||
- **Routing-override** — `STAGE_TRANSITIONS`/`get_next_stage`/`get_agent_for_stage` остаются
|
||
чистыми (1:1). В `advance_stage` на ребре выхода из `analysis` при `track='bug'`: `next_stage`
|
||
→ `development` (вместо `architecture`), `next_agent` → `developer` (вместо `architect`).
|
||
- **Гейт `analysis` не трогаем** — `check_analysis_complete`/`check_analysis_approved` байт-в-байт;
|
||
lite-аналитик эмитит все 4 файла (01-bug-report / 02-03 краткие заглушки / 04 план обязательного
|
||
регресс-теста, BR-4). Экономия — пропуск всей стадии `architecture`, не число файлов.
|
||
- **Эскалация** (обратимость BR-5) — `POST /bug-fast-track/escalate?work_item=<id>` сбрасывает
|
||
`track→'full'` (+ self-escalate мини-аналитика) → задача идёт через `architecture`.
|
||
- **Флаги** (`config.py`): `bug_fast_track_enabled` (kill-switch), `bug_fast_track_label`
|
||
(дефолт `Bug`), `bug_fast_track_repos` (CSV; **пусто → self-hosting only**). `False`/неприменимый
|
||
репо → путь старта и маршрут **байт-в-байт** прежние (нулевая регрессия для enduro и orchestrator).
|
||
- **Наблюдаемость (AC-7):** read-only блок `bug_fast_track` в `GET /queue` (флаг/область/метка +
|
||
счётчик `track='bug'` + метрика экономии стадий/agent-runs/токенов/времени из `agent_runs`); лог
|
||
на решение о маршруте; опц. `🐞` в Telegram-карточке.
|
||
- **Инфра-предусловие:** создать метку `Bug` в Plane-проекте ORCH; её отсутствие = `has_label`
|
||
False = полный цикл (fail-safe).
|
||
|
||
Подробнее: [adr-0032](adr/adr-0032-bug-fast-track.md), детально —
|
||
`docs/work-items/ORCH-019/06-adr/ADR-001-bug-fast-track.md`,
|
||
`docs/work-items/ORCH-019/08-data-requirements.md`.
|
||
|
||
### STOP / отмена задачи: терминал `cancelled` + закрытие дыры релонча (ORCH-090 — реализовано)
|
||
|
||
До ORCH-090 не было штатного способа отменить задачу (ручная хирургия по БД/процессам) и
|
||
существовала **дыра релонча**: `handle_status_start` при существующей задаче без активного job
|
||
безусловно релончил агента текущей стадии на той же ветке. ORCH-090 вводит Plane-статус **STOP**
|
||
как единый декларативный сигнал отмены: остановка агента + **полный сброс** прогресса. Аддитивно,
|
||
под kill-switch, never-raise, restart-safe; `STAGE_TRANSITIONS` (exit-гейты) / `QG_CHECKS` /
|
||
`check_*` — **без изменений**.
|
||
- **Новое системное терминальное состояние `cancelled`** (adr-0026) — `tasks.stage='cancelled'` +
|
||
`jobs.status='cancelled'`, равноправное `done`. Предикат «задача незавершена» расширяется
|
||
`stage != 'done'` → `stage NOT IN ('done','cancelled')` в `serial_gate` (ORCH-088) и `task_deps`
|
||
(ORCH-026), приводя их в соответствие с уже существующим терминал-скипом реконсилятора
|
||
(`stage in ("done","cancelled")`, ORCH-086 D2). Иначе отменённая задача заклинила бы очередь репо.
|
||
- **Распознавание (fail-closed):** новый ключ `stop` в `_PLANE_NAME_TO_KEY` (`"STOP" → "stop"`);
|
||
**не** в `_DEFAULT_STATES` (по образцу `confirm_deploy`/ORCH-059) → нет статуса = нет отмены, без
|
||
`KeyError`. `handle_issue_updated` маршрутизирует `stop` → новый `handle_stop` →
|
||
`stage_engine.cancel_task`.
|
||
- **Каскад отмены:** graceful SIGTERM активному агенту (переиспользование каскада
|
||
`launcher._watchdog` по `jobs.pid`); `cancel_jobs_for_task` (queued/running → `cancelled`,
|
||
не реквью'ятся); снятие таймеров/мониторов (brd-clock, post-deploy monitor, defer'ы);
|
||
`remove_worktree` + never-raise удаление **только feature-ветки** Gitea (`gitea.delete_remote_branch`;
|
||
`main`/`master` неприкосновенны — явный гард; без force-push); **тумбстон** `plane_id`/`work_item_id`/
|
||
**`plane_issue_id`** (суффикс `#cancelled-<id>`) → `get_task_by_plane_id` возвращает None → повторный
|
||
«To Analyse» создаёт задачу с нуля; docs-артефакты (`01..17`) сохраняются. Аддитивные колонки
|
||
`tasks.cancelled_at`/`cancel_requested_at` (`_ensure_column`).
|
||
> **Уточнение ADR-001 D4 (при реализации):** ADR предлагал сохранить `plane_issue_id` нетронутым, но
|
||
> `get_task_by_plane_id`/`create_task_atomic` матчат по `plane_id OR plane_issue_id` — нетумбстоненный
|
||
> `plane_issue_id` оставил бы отменённую строку «находимой» и заблокировал бы re-create (BR-3/TR-4).
|
||
> Поэтому он тоже тумбстонится; исходный UUID (== исходный `plane_id` во всех путях создания) парсится
|
||
> из детерминированного суффикса для аудита.
|
||
- **Безопасное прерывание merge/deploy:** STOP в критическом окне → **отложенная отмена** (durable
|
||
`cancel_requested_at`, отмена только `queued`-job'ов, алерт); необратимый шаг доводится до
|
||
честного исхода; `main`/прод-контейнер не трогаются (NFR-3). «Критическое окно» = реально начатый
|
||
необратимый шаг: self-deploy `INITIATED`-sentinel (ORCH-036; детач-деплой + поздний `merge_pr` в
|
||
`_handle_merge_verify` идут под тем же маркером) **либо** держание merge-lease (ORCH-043/071) **И**
|
||
активно бегущий актор (running-job). **P1-уточнение (ORCH-090 review):** удержание merge-lease в
|
||
Phase A на `deploy` в ожидании ручного `Confirm Deploy` без бегущего актора **обратимо** → НЕ
|
||
критично → немедленный полный сброс (он сам отпускает lease). Иначе deferred-отмена ушла бы к
|
||
finalizer'у, который оператор (нажавший STOP, чтобы НЕ подтверждать) никогда не запустит — задача
|
||
застряла бы нетерминальной с удержанным lease, клиня serial-gate репо.
|
||
- **Закрытие дыры релонча:** relaunch в `handle_status_start` ограничен стадией `analysis`
|
||
(единственный владелец Needs-Input, ORCH-066) — тихий релонч середины пайплайна на старой ветке
|
||
устранён; единственный вход к запуску — «To Analyse» (`start_pipeline`).
|
||
- **Флаги/наблюдаемость:** kill-switch `stop_status_enabled` + `stop_status_repos` (CSV, пусто →
|
||
все репо); leaf `src/cancel.py` (never-raise); read-only блок `stop` в `GET /queue`; лог +
|
||
Telegram (кликабельный номер) + Plane-коммент + live-карточка. При выключенном флаге — нулевая
|
||
регрессия (enduro не затронут).
|
||
|
||
Подробнее: [adr-0026](adr/adr-0026-stop-cancel-task.md), детально —
|
||
`docs/work-items/ORCH-090/06-adr/ADR-001-stop-cancel-task.md`,
|
||
`docs/work-items/ORCH-090/08-data-requirements.md`.
|
||
|
||
### Исполняемый самодеплой стадии `deploy` (ORCH-36)
|
||
`deploy` перестаёт быть «бумажной»: для self-hosting (`is_self_hosting_repo`) стадия
|
||
РЕАЛЬНО деплоит прод (8500) через хост-хук `scripts/orchestrator-deploy-hook.sh`,
|
||
а `deploy_status: SUCCESS` означает доказанный health-ok, не декларацию LLM. Три фазы
|
||
(детерминированно, без LLM в критическом пути self-restart):
|
||
- **Фаза A (вход в `deploy`)** — при `deploy_require_manual_approve=true` вместо запуска
|
||
прод-deployer выставляется approval-pending статус Plane + запрос перевести задачу
|
||
в статус **«Confirm Deploy»** (ORCH-059; Plane-коммент + Telegram). Перехват в
|
||
`advance_stage` ПОСЛЕ `check_staging_status` и merge-gate.
|
||
- **Фаза B (Plane → `Confirm Deploy`, ORCH-059)** —
|
||
`advance_stage(deploy, finished_agent=None, confirm_deploy=True)`
|
||
запускает **detached host-процесс** (ssh + setsid → хук с прод-параметрами +
|
||
build-once retag `SOURCE_IMAGE`) и ставит детерминированный **finalizer-job**;
|
||
маркер `initiated` — идемпотентность. Возврат БЕЗ advance (вердикта ещё нет).
|
||
Обычный `Approved` на `deploy` (`confirm_deploy=False`) — детерминированный no-op
|
||
(не деплоит и не откатывает).
|
||
- **Фаза C (finalizer)** — новый контейнер после рестарта читает sentinel `result`
|
||
(exit-code хука), маппит `0→SUCCESS / иначе→FAILED`, пишет `14-deploy-log.md`,
|
||
вызывает `advance_stage(deploy, finished_agent="deployer")` → существующие контракты:
|
||
`SUCCESS → done`, `FAILED → откат БАГ-8 на development`.
|
||
|
||
Триггер прод-деплоя = смена статуса Plane на `Confirm Deploy` (ORCH-059; status-only
|
||
verdict model; комментарии не управляют конвейером). `Approved` остаётся исключительно
|
||
человеческим гейтом конвейера и прод-деплой не запускает. На старте — обязательный
|
||
ручной approve (флаг `true`); полный авто — отдельная задача (ORCH-54). Условность как ORCH-35: реально для `orchestrator`,
|
||
прочие репо — прежний синхронный ssh-деплой агентом. Контракты не меняются:
|
||
`STAGE_TRANSITIONS`, реестр QG, `check_deploy_status`/`_parse_deploy_status`, БАГ-8,
|
||
terminal-sync, merge-gate, exit-code-контракт хука. Restart-safe состояние —
|
||
sentinel-файлы (`<repos_dir>/.deploy-state-<repo>/<wi>/`), без миграции БД.
|
||
Подробнее: [adr-0007](adr/adr-0007-executable-self-deploy.md), детально —
|
||
`docs/work-items/ORCH-036/06-adr/ADR-001-executable-self-deploy.md`.
|
||
|
||
#### Выделенный статус-триггер прод-деплоя «Confirm Deploy» (ORCH-059 — реализовано)
|
||
Перегрузка: один Plane-статус `Approved` служил И человеческим гейтом BRD на
|
||
`analysis` (`check_analysis_approved`), И триггером Фазы B прод-деплоя на `deploy`
|
||
— привычный жест approve молча запускал прод-рестарт (групповой self-hosting
|
||
риск). ORCH-059 разделяет жесты: вводится отдельный логический статус
|
||
`confirm_deploy` («Confirm Deploy»), который триггерит **ТОЛЬКО** Фазу B на
|
||
`deploy`; `Approved` остаётся исключительно гейтом конвейера.
|
||
- `_PLANE_NAME_TO_KEY` += `"Confirm Deploy" → "confirm_deploy"`; в
|
||
`_DEFAULT_STATES` ключ НЕ добавляется (нет UUID для enduro/fallback) →
|
||
**fail-closed**: нет статуса → нет деплоя, без `KeyError` (доступ через `.get`).
|
||
- `handle_issue_updated` маршрутизирует `Confirm Deploy` → `handle_confirm_deploy`
|
||
(гард `stage=="deploy"`) → `_try_advance_stage(..., confirm_deploy=True)`.
|
||
- `advance_stage` получает kwarg `confirm_deploy: bool=False`; блок Фазы B
|
||
(`deploy`+`finished_agent is None`+self-hosting) деплоит ТОЛЬКО при
|
||
`confirm_deploy=True`, иначе (обычный `Approved`) — **no-op** (`check_deploy_status`
|
||
не запускается → нет ложного отката БАГ-8).
|
||
- CTA Фазы A (`_handle_self_deploy_phase_a`) просит «Confirm Deploy», не «Approved».
|
||
- Условность как ORCH-35/36 (только `orchestrator`); Фазы A/C, `STAGE_TRANSITIONS`,
|
||
`QG_CHECKS`, `check_deploy_status`, merge-gate, схема БД — без изменений.
|
||
- Эксплуатация: в Plane-проекте ORCH создать статус «Confirm Deploy» + сброс кэша
|
||
состояний (`docs/work-items/ORCH-059/07-infra-requirements.md`).
|
||
|
||
Детально — `docs/work-items/ORCH-059/06-adr/ADR-001-confirm-deploy-status.md`
|
||
(уточняет/триггер Фазы B относительно adr-0007).
|
||
|
||
#### Merge-в-main + пост-деплой верификация как условие `done` (ORCH-071 — фикс фантомного merge)
|
||
**Фантомный merge** (CRITICAL, постмортем `docs/history/LESSONS_2026-06-08_phantom-merge.md`):
|
||
на self-hosting пути `deploy` агент `deployer` НЕ запускается, а фактический merge PR в `main`
|
||
исторически делал ТОЛЬКО он → детерминированный путь
|
||
(`_handle_self_deploy_phase_b → initiate_deploy → run_deploy_finalizer`) **не содержал шага
|
||
merge-в-main вообще**. Detached host-деплой лишь retag'ал образ + рестартил 8500; `done`
|
||
достигался по `deploy_status: SUCCESS` без верификации `main`. Зелёный деплой (образ из рабочей
|
||
ветки) маскировал отсутствие merge → следующая задача срезала ветку от устаревшего `main` и
|
||
теряла код предшественника (накопительно потеряны ORCH-022/059/066/068). ORCH-071 вводит
|
||
**детерминированный merge-актор + пост-merge верификацию** как **под-гейт ребра `deploy → done`**
|
||
(симметрично edge-под-гейтам `deploy-staging → deploy`), только для self-hosting:
|
||
- **Врезка `_handle_merge_verify` в `advance_stage`** (`current_stage=="deploy"` и
|
||
`next_stage=="done"`, ПОСЛЕ зелёного `check_deploy_status`, ДО `update_task_stage`). Гейтит
|
||
**ВСЕ** пути к `done` единообразно (`run_deploy_finalizer` Phase C, reconciler F-1, job-reaper —
|
||
все идут через `advance_stage`), закрывая дыру обхода merge.
|
||
- **Merge в Phase C (после рестарта), НЕ в Phase B** — finalizer restart-surviving (claim воркером
|
||
нового контейнера, re-drive reaper'ом), merge физически строго ПОСЛЕ рестарта прода → рестарт его
|
||
не убивает (G3 «шаг, переживающий рестарт»; постмортем-урок №3).
|
||
- **Merge-актор `merge_gate.merge_pr`** — `pr_already_merged` (idempotency no-op повтор) → иначе
|
||
Gitea `POST /repos/{owner}/{repo}/pulls/{index}/merge`. Выбор PR строго по `head.ref==branch`
|
||
И `base.ref=="main"`. Никогда push/force-push в `main`.
|
||
- **Верификатор `merge_gate.verify_merged_to_main` (семантика ORCH-073, FR-1):** подтверждение —
|
||
**ТОЛЬКО** `git merge-base --is-ancestor <validated_sha> origin/main` (`validated_revision` —
|
||
якорь ORCH-058). PR-флаг `pr_already_merged` **больше НЕ подтверждает merge** (удалён из verify):
|
||
он понижен до idempotency-guard `merge_pr` и засчитывает merged PR лишь при `head.ref==branch`
|
||
И `base.ref=="main"` (исключает авто docs-PR). Пустой SHA / git-ошибка → `False` (fail-closed),
|
||
never-raise.
|
||
- **Регресс-гард целостности `main` (ORCH-073, FR-5):** `merge_gate.check_main_regression` в
|
||
`_handle_merge_verify` ПОСЛЕ подтверждённого SHA-в-main и ДО `done` проверяет, что `origin/main`
|
||
содержит декларативный набор маркеров ранее-merged задач (`MAIN_REGRESSION_MARKERS`,
|
||
`git grep -c <marker> origin/main -- <path>` > 0). Маркер отсутствует → alert «main regressed» +
|
||
HOLD (НЕ `done`, ALERT-only). Fail-open на git-ошибке грепа (регресс — только при `count==0`).
|
||
Kill-switch `regression_guard_enabled`; non-self → no-op. Набор — append-only константа,
|
||
значимая задача дописывает свой маркер.
|
||
- **Не подтверждено → alert «deploy succeeded but not merged» (Telegram+Plane) + HOLD**
|
||
(`set_issue_blocked`, задача НЕ `done`, БЕЗ авто-отката на `development` — not-merged есть
|
||
инфра-дефект, реакция ALERT-only как ORCH-021 self-hosting). Подтверждено → штатный `deploy →
|
||
done` + `merged_to_main: true` во frontmatter `14-deploy-log.md` (`deploy_status:` нетронут).
|
||
- **Защита от CHANGELOG-затирания (ORCH-073, FR-4):** корневой `.gitattributes` с
|
||
`CHANGELOG.md merge=union` → правки `## [Unreleased]` авто-сливаются при `auto_rebase_onto_main`
|
||
без конфликта, ветка не откатывается в `development` и не тащит устаревший код-сосед. `docs/**`
|
||
под union НЕ ставится (union только для append-only).
|
||
- **Условность как ORCH-35/43/58:** `merge_verify_enabled` (kill-switch, дефолт `true`) +
|
||
`merge_verify_repos` (пусто → только self-hosting); non-self — no-op, merge остаётся за `deployer`.
|
||
never-raise; идемпотентность по **SHA-в-main** (INV-4, не «любой merged PR»); ручной approve
|
||
сохранён (`Confirm Deploy`).
|
||
- **Инварианты:** `STAGE_TRANSITIONS`, `check_deploy_status`/`_parse_deploy_status`, реестр
|
||
`QG_CHECKS` (под-гейт — врезка в `advance_stage`, НЕ новый зарегистрированный QG), схема БД,
|
||
БАГ-8, terminal-sync, merge-gate, image-freshness, exit-коды хука — **без изменений**.
|
||
Диагностика фантома — runbook `docs/operations/PHANTOM_MERGE_RUNBOOK.md` (4 проверки постмортема).
|
||
|
||
Подробнее: [adr-0013](adr/adr-0013-merge-verify-gate.md) +
|
||
[adr-0014](adr/adr-0014-merge-verify-sha-source-of-truth.md) (amends 0013 — SHA-в-main как
|
||
единственный критерий + регресс-гард, ORCH-073); детально —
|
||
`docs/work-items/ORCH-071/06-adr/ADR-001-merge-verify-gate.md`,
|
||
`docs/work-items/ORCH-073/06-adr/ADR-001-merge-verify-sha-truth-and-regression-guard.md`.
|
||
|
||
#### Гарантированный код-PR перед merge-verify (ORCH-082 — фикс ложного HOLD «no open PR»)
|
||
Под-гейт merge-verify (ORCH-071/073) детерминированно мержит **открытый** код-PR ветки в `main`
|
||
(`merge_pr`, фильтр `head.ref==branch` И `base.ref=="main"`). Но конвейер **не гарантировал**, что
|
||
к моменту merge у ветки этот PR есть: PR создаётся единственной `launcher._ensure_pr` **только** на
|
||
developer-пути и **только** при свежем worktree-коммите. На деплое ORCH-074 (08.06, первая задача
|
||
после ручных восстановлений `main`) у ветки не оказалось открытого код-PR → `merge_pr` вернул
|
||
`("False", "no open PR")` → защита ORCH-073 верно удержала задачу (HOLD, не ложный `done`), но это
|
||
лечило следствие. ORCH-082 закрывает **отсутствующий инвариант** «к merge-verify у ветки есть
|
||
открытый код-PR» аддитивно, внутри того же под-гейта, не трогая машину стадий:
|
||
- **Новый leaf-актор `merge_gate.ensure_open_pr(repo, branch) -> (status, detail)`** (never-raise):
|
||
`GET …/pulls?state=open` с фильтром `head.ref==branch` И `base.ref=="main"` (**идентичен**
|
||
`merge_pr`/ORCH-073 FR-3 — авто-docs-PR `base != main` НЕ код-PR) → `("existed", N)`; иначе
|
||
`POST …/pulls` → `("created", N)`; гонка «PR exists»/409/422 → повторный GET → `existed` (без
|
||
дублей); любая иная ошибка → `("failed", reason)`.
|
||
- **Врезка в `_handle_merge_verify`** ПОСЛЕ резолва `validated_revision` и **ПЕРЕД** `merge_pr`:
|
||
`created|existed` → штатно к `merge_pr` → `verify_merged_to_main`; `failed` → честный HOLD+alert
|
||
через новый helper `_hold_pr_create_failed` (текст «PR создать не удалось» — отличим от
|
||
not-merged HOLD; `result.note="pr-create-failed-hold"`), задача остаётся на `deploy`, БЕЗ отката
|
||
на development.
|
||
- **Защита ORCH-073 неприкосновенна и приоритетна:** подтверждение merge остаётся ТОЛЬКО
|
||
`verify_merged_to_main` (SHA-в-main) + `check_main_regression`; `ensure_open_pr` устраняет лишь
|
||
**ложный** HOLD «no open PR», но не маскирует реально невлитый код (тот → HOLD как прежде).
|
||
- **`launcher._ensure_pr`** рекомендуется делегировать в `ensure_open_pr` (единый код создания PR),
|
||
сохранив прежний триггер «только developer-путь».
|
||
- **Условность как ORCH-35/43/58/71:** kill-switch `merge_verify_autocreate_pr_enabled` (дефолт
|
||
`true`); область — `merge_verify_applies(repo)` (self-hosting / `merge_verify_repos`); non-self —
|
||
no-op. `False` → поведение ORCH-074 1:1. Идемпотентность из Gitea (наличие открытого PR), **без
|
||
миграции БД** (restart-safe). `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, `check_deploy_status`,
|
||
exit-коды хука, merge-gate, image-freshness — без изменений; `main` не push/force-push.
|
||
|
||
Подробнее: [adr-0016](adr/adr-0016-ensure-open-pr-before-merge-verify.md) (amends 0013/0014);
|
||
детально — `docs/work-items/ORCH-082/06-adr/ADR-001-ensure-open-pr-before-merge-verify.md`.
|
||
|
||
#### Ретрай транзиентных merge-ошибок Gitea + гард already-in-main (ORCH-093 — фикс ложного HOLD на 405/5xx)
|
||
Инцидент **ORCH-063**: self-deploy прошёл, staging OK, PR был `open`+`mergeable`, конфликтов не было,
|
||
но `POST /pulls/{n}/merge` вернул `HTTP 405 {"message":"Please try again later"}` (Gitea пересчитывал
|
||
`mergeable` сразу после пуша). One-shot `merge_pr` мгновенно вернул `False` → корректная защита
|
||
ORCH-071/073 удержала задачу на `deploy` (HOLD+alert) + потребовала **ручной домерж** (повтор влился с
|
||
первого раза); повторный прогон финализатора плодил **мусорный пустой PR** на уже влитой ветке. У
|
||
Claude-агентов есть transient-breaker, у CI-гейта — `check_ci_green`, а у детерминированного
|
||
merge-актора аналога не было. ORCH-093 закрывает это аддитивно, внутри того же под-гейта, не трогая
|
||
машину стадий:
|
||
- **Retry-loop в `merge_pr` (ORCH-093 D1/D2):** ретраится **только** мутирующий `POST …/merge`
|
||
(идемпотентные шаги до него — без изменений). Классификатор `_classify_merge_response →
|
||
transient|terminal`: **транзиент** (ретрай с backoff) — `405`/`408`/любой `5xx`/`httpx`-таймаут/
|
||
сетевая ошибка, **и** `409`/`422` когда PR всё ещё `mergeable` (доп. `GET /pulls/{index}`);
|
||
**терминал** (быстрый честный `False`, защита ORCH-071/073 как прежде) — `403`/`404`/реальный
|
||
конфликт (`409`/`422` при `mergeable==False`). Дефолт-политика `mergeable==None`/недоступно →
|
||
транзиент (fail-OPEN-в-ретрай: икота Gitea наблюдаема, бюджет конечен, backstop сохранён).
|
||
Backoff экспоненциальный с потолком `min(base*2^(i-1), max)` (дефолты 2/5 с → суммарный сон
|
||
`(N-1)*max ≤ 10 с`, monitor-поток merge-verify не подвешивается). Лог `attempt i/N` (образец
|
||
`check_ci_green`).
|
||
- **Гард already-in-main в `ensure_open_pr` (ORCH-093 D3):** leaf `_branch_fully_in_main`
|
||
(`git merge-base --is-ancestor HEAD origin/main` в per-branch worktree) вызывается **между** «код-PR
|
||
не найден» и `POST …/pulls`: ветка целиком в `main` (нет коммитов `origin/main..HEAD`) → новый исход
|
||
`("already-in-main", …)` **без создания PR** (нет мусорного пустого PR). git-ошибка/ambiguous
|
||
(`None`) → **fail-OPEN** (деградация на create-путь, НЕ ложный no-op). Без отдельного флага —
|
||
накрыт `merge_verify_autocreate_pr_enabled`.
|
||
- **Врезка в `_handle_merge_verify` (ORCH-093 D4):** `pr_status == "already-in-main"` → лог,
|
||
**пропуск** `merge_pr` (мержить нечего), сразу к `verify_merged_to_main` (SHA-в-main подтвердит →
|
||
`done`). Это НЕ HOLD; SHA-в-main остаётся авторитетным (если SHA не в `main` — прежний HOLD,
|
||
fail-closed).
|
||
- **Конфиг/откат:** `merge_retry_enabled` (kill-switch; `False` → ровно один POST = байт-в-байт
|
||
прежнее one-shot) / `merge_retry_max_attempts` (3) / `merge_retry_backoff_base_s` (2) /
|
||
`merge_retry_backoff_max_s` (5), env `ORCH_MERGE_RETRY_*`. `STAGE_TRANSITIONS`, `QG_CHECKS`, схема
|
||
БД, exit-коды хука, merge-gate, image-freshness — без изменений; `main` не push/force-push.
|
||
|
||
Подробнее: [adr-0027](adr/adr-0027-merge-actor-transient-retry-and-already-in-main.md)
|
||
(amends 0013/0014/0016); детально —
|
||
`docs/work-items/ORCH-093/06-adr/ADR-001-merge-transient-retry-and-already-in-main-guard.md`.
|
||
|
||
#### Ретрай транзиентных merge-ошибок Gitea + гард already-in-main (ORCH-093 — фикс ложного HOLD на 405/5xx)
|
||
Инцидент ORCH-063 (09.06): self-deploy прошёл, PR `open`+`mergeable=True`, конфликтов нет — но
|
||
`POST …/merge` вернул `HTTP 405 {"message":"Please try again later"}` (Gitea пересчитывал
|
||
`mergeable` сразу после пуша). `merge_pr` был **one-shot** → мгновенный `False` → ложный HOLD
|
||
ORCH-071/073 + ручной домерж; повторный прогон финализатора после ручного мержа создавал **пустой
|
||
PR** на уже влитой ветке. ORCH-093 аддитивно закрывает оба дефекта, не трогая машину стадий:
|
||
- **Ретрай-loop в `merge_pr`** оборачивает **только** `POST /pulls/{index}/merge` до
|
||
`merge_retry_max_attempts` (дефолт 3) с экспон. backoff и потолком (`merge_retry_backoff_base_s` 2 /
|
||
`merge_retry_backoff_max_s` 5; суммарно ≤10 с, не подвешивает monitor-поток). Шаги до POST
|
||
(idempotency `pr_already_merged`, поиск код-PR) — без изменений. Лог `attempt i/N` (образец
|
||
`check_ci_green`).
|
||
- **Классификатор транзиент/терминал** по коду ответа **и** полю `mergeable`: **транзиент** (ретрай)
|
||
— `405`/`408`/`5xx`/таймаут/сетевое, `409`/`422` при `mergeable==True`; **терминал** (быстрый
|
||
честный `False`) — `403`/`404`, `409`/`422` при `mergeable==False`. Неоднозначный `409/422`
|
||
разрешается доп. `GET /pulls/{index}`; `mergeable==None`/недоступен → транзиент-по-дефолту в рамках
|
||
бюджета (цель — не давать ложного HOLD на икоте; backstop ORCH-071/073 сохранён).
|
||
- **Гард already-in-main в `ensure_open_pr`**: перед созданием PR — `git merge-base --is-ancestor
|
||
<branch> origin/main` (rc==0 → ветка целиком в `main`) → новый исход `("already-in-main", …)`, PR
|
||
**не создаётся**; git-ошибка/ambiguous → **fail-OPEN** на текущий create-путь (икота git не должна
|
||
стать ложным no-op мержа). `_handle_merge_verify` трактует `already-in-main` как «мержить нечего» →
|
||
пропуск `merge_pr` → авторитетный SHA-в-main (`verify_merged_to_main`) доводит до `done` без мусорного
|
||
PR. Это НЕ `failed`-ветка.
|
||
- **Защита ORCH-071/073 неприкосновенна:** реальный конфликт → быстрый честный HOLD; подтверждение
|
||
merge остаётся ТОЛЬКО SHA-в-main. Терминал/исчерпание ретраев → `(False, …)` → прежний HOLD+alert.
|
||
- **Условность / откат:** kill-switch `merge_retry_enabled` (дефолт `true`; `False` → one-shot 1:1,
|
||
env `ORCH_MERGE_RETRY_*`); гард already-in-main — без отдельного флага (накрыт
|
||
`merge_verify_autocreate_pr_enabled`). Область — `merge_verify_applies` (self-hosting; на прочих
|
||
репо мерж за `deployer` — изменение нейтрально). `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД,
|
||
exit-коды хука, merge-gate, image-freshness — без изменений; `main` не push/force-push (INV-4).
|
||
|
||
Подробнее: [adr-0027](adr/adr-0027-merge-actor-transient-retry-and-already-in-main.md) (amends
|
||
0013/0014/0016); детально —
|
||
`docs/work-items/ORCH-093/06-adr/ADR-001-merge-transient-retry-and-already-in-main-guard.md`.
|
||
|
||
### Post-deploy наблюдение прода + реакция на деградацию (ORCH-021 — реализовано)
|
||
Конвейер заканчивался на `deploy → done` и **забывал про прод**: «успех» = health-check
|
||
в момент рестарта (~60с). Класс «зелёный деплой, красный прод» (прецедент ET-8 —
|
||
деградация через минуты под трафиком, health `200 ok`, фича сломана). ORCH-021 продлевает
|
||
ответственность **ЗА** `done`: для применимого репо после терминального перехода армится
|
||
наблюдение окна `post_deploy_window_s` (~15 мин) с интервалом `post_deploy_interval_s`;
|
||
деградация фиксируется по детерминированным порогам, при подтверждении — реакция.
|
||
|
||
Механизм — **reserved-agent job `post-deploy-monitor`** (калька `deploy-finalizer`, НЕ
|
||
стадия и НЕ daemon): арм в `advance_stage` в блоке `next_stage == "done"`
|
||
(`post_deploy.arm_monitor`, sentinel `armed` = идемпотентность); тик перехватывается в
|
||
`launcher.launch_job` ДО `_spawn` → `stage_engine.run_post_deploy_monitor` (один опрос →
|
||
append в `series` → классификация → перепостановка с задержкой ИЛИ реакция+артефакт+`done`).
|
||
Чистая логика — новый leaf-модуль `src/post_deploy.py` (never-raise): `post_deploy_applies`,
|
||
`probe_signals` (`/health` 200+`{"status":"ok"}` + доля 5xx на `/status`,`/queue`),
|
||
`classify` (HEALTHY|DEGRADED — главный предмет юнит-тестов), `decide_action`,
|
||
sentinel-state, `write_post_deploy_log`.
|
||
- **Пороги (BR-3):** `DEGRADED` ⇔ `≥ post_deploy_fail_threshold` ПОСЛЕДОВАТЕЛЬНЫХ провалов
|
||
health ИЛИ доля 5xx `> post_deploy_5xx_threshold`; одиночный глюк → HEALTHY (нет ложных
|
||
откатов).
|
||
- **Реакция:** self-hosting (`orchestrator`) — ВСЕГДА `ALERT_ONLY` (Telegram+Plane, ручной
|
||
approve; тик НИКОГДА не откатывает/рестартит прод-контейнер); не-self +
|
||
`post_deploy_auto_rollback=true` → хук `--rollback` (`0→ROLLBACK_OK`,
|
||
`1/2→ROLLBACK_FAILED`+алерт); дефолт → `ALERT_ONLY`.
|
||
- **Артефакт** `16-post-deploy-log.md` (YAML-frontmatter `post_deploy_status`/
|
||
`action_taken`/…) — машиночитаемо для петли уроков ORCH-8; best-effort.
|
||
- **Наблюдаемость** — блок `post_deploy` в `GET /queue` (образец `reconcile`).
|
||
- **Инварианты:** `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_deploy_status`, terminal-sync,
|
||
merge-gate, exit-коды хука (0/1/2), схема БД — НЕ меняются. Restart-safe (sentinel
|
||
`.post-deploy-state-<repo>/<wi>/` + jobs-очередь). Kill-switch
|
||
`post_deploy_monitor_enabled`, область `post_deploy_repos` (пусто → self-hosting).
|
||
Условность как ORCH-35/36/43/58.
|
||
|
||
Подробнее: [adr-0010](adr/adr-0010-post-deploy-monitor.md), детально —
|
||
`docs/work-items/ORCH-021/06-adr/ADR-001-post-deploy-monitor.md`.
|
||
|
||
### Terminal-window-aware гард deploy-статусов: done-задача держит Done (ORCH-094 — реализовано)
|
||
Терминальная (`done`) задача в Plane **не держала `Done`**: непрерывный флапп
|
||
`Awaiting Deploy ⟷ Monitoring after Deploy` (верифицировано на **ORCH-061**, task 47, done с 07.06 —
|
||
273 активности, само не затихает). Причина: три code-писателя deploy-фазовых статусов
|
||
(`stage_engine.py:404/1218/1316`) делегируют в тонкие сеттеры `plane_sync`, которые **БД-стадию не
|
||
читают** ⇒ терминал-слепы; любой повторный/стейл вызов под бот-токеном орка перезаписывает `Done`
|
||
обратно. Тонкость: `update_task_stage("done")` (стр. 369) пишет стадию **раньше** легитимного
|
||
`set_issue_monitoring` (стр. 404) ⇒ пост-деплой-окно ORCH-021 by-design индицируется поверх уже-`done`
|
||
задачи; наивный гард «stage==done → Done» затёр бы легитимный `Monitoring` (регресс).
|
||
|
||
Решение — **единый terminal-window-aware гард на входе трёх deploy-фазовых сеттеров** (новый leaf
|
||
`src/deploy_status_guard.py`, never-raise, config-gated; образец `serial_gate`/`labels`/`cancel`).
|
||
- **Инвариант:** deploy-фазовый статус легитимен ⇔ задача **нетерминальна** ИЛИ (`done` И активно
|
||
пост-деплой-окно). `decide(work_item_id, target) → ALLOW | CONVERGE_DONE | SUPPRESS`: off / чужой
|
||
issue / не-self репо / нетерминал → ALLOW; `cancelled` → SUPPRESS; `done`+`monitoring`+`window_active`
|
||
→ ALLOW; `done` иначе → CONVERGE_DONE (`set_issue_done`, идемпотентно); исключение → ALLOW+warning.
|
||
- **Окно** — новый `post_deploy.window_active(repo,wi)` = `has_marker(ARMED) and not has_marker(DONE)`
|
||
(restart-safe). **Перенос арм-блока перед terminal-sync** в `advance_stage` блок `next_stage=="done"`
|
||
⇒ на стр. 404 `ARMED` уже есть ⇒ легитимный первый `Monitoring` проходит; re-drive после закрытия
|
||
окна сходится к `Done`.
|
||
- **Харднинг монитора:** страж `has_marker(...DONE)` (ранний return) + тик no-op при `cancelled`
|
||
мид-окно; тики привязаны к активному job'у (нет job → нет тика, нет статус-PATCH).
|
||
- **Наблюдаемость:** каждый вердикт логируется (`work_item`/`caller`/`target`/`db_stage`/
|
||
`window_active`/вердикт); подавление — явно.
|
||
- **Инварианты:** `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict ключи/схема БД — НЕ тронуты;
|
||
`main`/force-push/прод-контейнер/detached-деплой — НЕ тронуты; рабочий self-deploy-цикл 1:1; не-self
|
||
репо инертны. Kill-switch `ORCH_DEPLOY_STATUS_GUARD_ENABLED` (→ 1:1), область
|
||
`ORCH_DEPLOY_STATUS_GUARD_REPOS` (пусто → self-hosting). Ограничение: внешняя Plane-automation (если
|
||
таков актор) закрывается буфером сходимости, а не code-фиксом — локализация актора в задаче (BR-7).
|
||
|
||
Подробнее: [adr-0028](adr/adr-0028-terminal-window-aware-deploy-status-guard.md), детально —
|
||
`docs/work-items/ORCH-094/06-adr/ADR-001-terminal-window-aware-deploy-status-guard.md`.
|
||
|
||
### Свежесть артефакта BUILD-ONCE: провенанс staging-образа (ORCH-058 — реализовано)
|
||
BUILD-ONCE retag (ORCH-36) промоутит `SOURCE_IMAGE=orchestrator-orchestrator-staging` в прод
|
||
**без rebuild**, полагаясь на «staging-образ свеж и провалидирован». Этой гарантии нет:
|
||
конвейер нигде не пересобирает staging-образ из провалидированного коммита → retag мог тихо
|
||
промоутнуть УСТАРЕВШИЙ образ (инцидент LESSONS_ORCH-036 п.4 — зелёный деплой молча
|
||
откатывал прод). ORCH-058 обеспечивает инвариант `INV-FRESH` **двумя слоями** (defense in
|
||
depth), только для self-hosting:
|
||
- **A — пересборка (liveness):** детерминированный QG-под-чек `check_staging_image_fresh` на
|
||
ребре `deploy-staging → deploy` ПОСЛЕ merge-gate и ДО Phase A пересобирает
|
||
`orchestrator-orchestrator-staging` из worktree валидированного коммита
|
||
(`--build-arg GIT_SHA=<sha>`, OCI-лейбл `org.opencontainers.image.revision`), пересоздаёт
|
||
8501 и прогоняет `staging_check` против свежего образа → валидируем и промоутим один
|
||
артефакт. FAIL → откат на `development` (как merge-gate). Сборки/recreate — ТОЛЬКО staging.
|
||
- **B — fail-closed guard (safety):** хук шагом 2b ПЕРЕД `docker tag` сверяет лейбл `revision`
|
||
у `SOURCE_IMAGE` с `EXPECTED_REVISION` (пробрасывает `build_deploy_command`). Несовпадение
|
||
/ пустой лейбл / пустой ожидаемый SHA / ошибка inspect → `exit 1` → FAILED (БАГ-8 откат),
|
||
прод не трогается. Делает тихий промоут устаревшего образа структурно невозможным даже при
|
||
отключённой/проигравшей гонку A.
|
||
|
||
Якорь «провалидированного коммита» — `git rev-parse HEAD` worktree ПОСЛЕ merge-gate (один
|
||
helper `validated_revision` питает и штамп A, и `EXPECTED_REVISION` B). Единый kill-switch
|
||
`image_freshness_enabled` включает A+B **как целое** (нет «B без A» = вечного fail-fast);
|
||
`image_freshness_repos` (пусто → self-hosting). `STAGE_TRANSITIONS`, exit-code хука (0/1/2),
|
||
`check_deploy_status`, БАГ-8, merge-gate, схема БД — НЕ меняются (под-гейт ребра + лейбл
|
||
образа, без миграций). Подробнее: [adr-0008](adr/adr-0008-staging-image-provenance.md),
|
||
детально — `docs/work-items/ORCH-058/06-adr/ADR-001-staging-image-provenance.md`.
|
||
|
||
### Security-гейт: secret-scanning + dependency audit перед мержем (ORCH-022 — реализовано)
|
||
Автономный конвейер вливал ветку в `main` без проверки на утёкший секрет (ключ/токен/пароль/
|
||
приватный ключ) и уязвимую зависимость (CVE); для self-hosting один секрет/CVE через одну
|
||
задачу уезжал в общий прод всех проектов (CLAUDE.md §8). ORCH-022 вводит детерминированный
|
||
(без LLM) **security-гейт как под-гейт ребра `deploy-staging → deploy`**, рядом с merge-gate
|
||
(ORCH-043) и image-freshness (ORCH-058), исполняемый **ПЕРВЫМ** среди edge-под-гейтов
|
||
(ДО merge-gate). Паттерн соседей: leaf `src/security_gate.py` (never-raise) + тонкая обёртка
|
||
`check_security_gate` в `QG_CHECKS` + врезка `_handle_security_gate` в `advance_stage`.
|
||
`STAGE_TRANSITIONS` и схема БД — **без изменений**.
|
||
- **Secret-scanning (`gitleaks`, offline):** скан `origin/main..HEAD`; любой секрет вне
|
||
аллоулиста `.gitleaks.toml` → вклад в FAIL. Offline → гарантия «секрет всегда блокирует»
|
||
не зависит от сети (безусловна).
|
||
- **Dependency audit (`pip-audit`, OSV/PyPI):** severity ≥ `security_dep_block_severity`
|
||
(дефолт `HIGH`) → FAIL; ниже / UNKNOWN → warning. Недоступность фида → **fail-open +
|
||
громкий warning** (анти-петля ORCH-061; флаг `security_dep_audit_fail_closed` для строгого
|
||
режима). best-effort при доступности фида.
|
||
- **ПЕРВЫМ, ДО merge-gate:** дёшево фейлить до дорогих rebase/rebuild; скан ветки ДО rebase
|
||
не «обвиняет» задачу в CVE из обновившегося `main`; до захвата merge-lease → при FAIL lease
|
||
освобождать не нужно.
|
||
- **Артефакт `17-security-report.md`** (YAML-frontmatter `security_status`/`secrets_found`/
|
||
`deps_blocking`/`deps_warning`/`deps_audit_degraded`); вердикт читается ТОЛЬКО из
|
||
frontmatter (гейт пишет → читает обратно через `parse_security_status` → возвращает: единый
|
||
источник истины), negative-токен авторитетен, битый/нет → fail-closed.
|
||
- **FAIL → откат на `development`** + developer-retry (общий `_developer_retry_count`, cap 3,
|
||
затем `set_issue_blocked` + Telegram); `task_desc` несёт дословные находки (ORCH-046).
|
||
- **Условность как ORCH-35/43/58:** `security_gate_enabled` + `security_gate_repos` (пусто →
|
||
только self-hosting); never-raise; таймаут `security_scan_timeout_s`; гейт не деплоит/не
|
||
рестартит прод. v1 — Python-only; SAST/мульти-стек — follow-up (BR-14).
|
||
|
||
Подробнее: [adr-0012](adr/adr-0012-security-gate.md), детально —
|
||
`docs/work-items/ORCH-022/06-adr/ADR-001-security-gate.md`.
|
||
|
||
### Live-трекер: зачистка сирот + эффорт в карточке + честное время (ORCH-087 — реализовано)
|
||
Скалярный `tasks.tracker_message_id` (только последний `message_id`) при рассинхроне
|
||
bump-режима (доминанты: гонка двух `update_task_tracker` и delete-fail+send-ok)
|
||
терял ссылку на прежние карточки → **осиротевшие «замёрзшие»** карточки (скриншот
|
||
ORCH-082: `📍 To Analyse` на задаче, реально дошедшей до `deploy`). G0-расследование
|
||
([ADR-001](../work-items/ORCH-087/06-adr/ADR-001-tracker-orphan-cleanup.md)):
|
||
рендер исправен, корень — потеря учёта старых mid. Решение (bump сохраняется как
|
||
дефолт — фича «карточка внизу» ORCH-042/067):
|
||
- **G1 — полный учёт mid:** аддитивная таблица-леджер `tracker_messages(task_id,
|
||
message_id, created_at, deleted_at)` (вариант A1; JSON-массив A2 отклонён —
|
||
lost-update при гонке). На каждом bump зачищаются ВСЕ незакрытые mid (`deleted_at
|
||
IS NULL`): успех/«already gone» → `deleted_at`, transient → остаётся для ретрая;
|
||
новый mid в леджер + `set_tracker_message_id` ТОЛЬКО при `send is not None` (BR-6).
|
||
Скаляр `tracker_message_id` сохранён (BC). Остаточная гонка самозалечивается за один
|
||
переход (лок не вводится). Known-limitation: Telegram 48ч (сироты старше неудаляемы).
|
||
- **G2/G3 — заголовок/deploy-цикл:** после G1 единственная живая карточка несёт
|
||
заголовок текущей стадии; `_LIVE_BRANCH_LABELS` дополняется ключом `confirm_deploy`
|
||
(полнота цикла `Awaiting Deploy → Deploying → Confirm Deploy → Monitoring → Done`).
|
||
- **BR-EFF — эффорт в строке стадии:** новая колонка `agent_runs.effort TEXT`,
|
||
стамп фактического `resolve_agent_effort` в `launcher._spawn` (CLI эффорт не
|
||
возвращает); рендер `· {model} · {effort}` (developer=`xhigh`, tester/deployer=
|
||
`medium`, прочие=`high`); пустой → суффикс опускается.
|
||
- **BR-G5 — честное время:** done-строка `⏱️ Агенты {agent} · твоё {review~cap} ·
|
||
общее с ожиданием {wall}` — три независимых подписанных метрики; `agent`=Σ
|
||
`agent_runs` (главная, точная); «твоё» ограничено порогом
|
||
`tracker_brd_review_cap_s` (дефолт 2ч, маркер `~` при отсечке аномального застоя);
|
||
`wall` подписан «с ожиданием», не выдаётся за сумму.
|
||
- **Инварианты:** `STAGE_TRANSITIONS`/`QG_CHECKS`/стадии — без изменений; миграции
|
||
аддитивны/идемпотентны (общая прод-БД, enduro не трогается); never-raise,
|
||
`disable_notification`, `plane_issue_link` (ORCH-067), `disable_web_page_preview`
|
||
(ORCH-080) — сохранены; разработка поверх свежего `origin/main` (ORCH-86),
|
||
`reconciler.py` не эродируется.
|
||
|
||
Детально — [ADR-001](../work-items/ORCH-087/06-adr/ADR-001-tracker-orphan-cleanup.md),
|
||
`docs/work-items/ORCH-087/08-data-requirements.md`.
|
||
|
||
### Reconciler: реконсиляция потерянных webhook (ORCH-053 — реализовано)
|
||
Конвейер продвигается только входящими webhook; потерянное событие (502 на ребилде,
|
||
нет ретраев у Plane/Gitea, неразрезолвленный `sha→branch`) → задача застревает молча
|
||
(инцидент ORCH-044). Фоновый поток `reconciler` периодически (`reconcile_interval_s`)
|
||
находит застрявшие задачи и доигрывает пропущенный переход **через те же штатные
|
||
гейты/обработчики**, что и webhook:
|
||
- **F-1 gate-side:** для задач со `stage∉{done}`, без активного job и
|
||
`age(updated_at) ≥ grace_for_stage(stage)` — read-only пред-оценка канонического QG;
|
||
зелёный → `stage_engine.advance_stage(..., finished_agent=None)`; красный →
|
||
тишина (спам нотификаций структурно невозможен). `analysis` не реконсилируется.
|
||
**Skip escalated / Blocked / Needs-Input (ORCH-060):** ДО оценки гейта F-1
|
||
пропускает (молча, без advance/нотификаций) задачи, которые ждут человека —
|
||
(1) исчерпавшие лимит developer-ретраев (`developer_retry_count(task_id) >=
|
||
MAX_DEVELOPER_RETRIES`, детерминированно, без сети — закрывает bounce-петлю
|
||
ET-013) и (2) в явном Plane-статусе **Blocked** / **Needs Input** (Вариант A —
|
||
запрос Plane API, без миграции БД; never-raise → консервативный skip). Гард
|
||
retry-count проверяется первым (дёшево, локальный SQL).
|
||
**ORCH-086 (закрытие F-1-пробела ORCH-068):** терминал-исключение и `state_uuid`-dedup
|
||
(изначально только F-2) распространены на F-1. После дешёвых локальных гардов F-1 делает
|
||
**один** резолв Plane-статуса задачи на тик (общий fetch для Guard 2 + терминал-скипа +
|
||
`_note_unblock`); терминальная задача (группа Plane `completed`/`cancelled`, fallback —
|
||
логические ключи `done`/`cancelled`, ЛИБО стадия в БД орка ∈ `{done, cancelled}`) →
|
||
**безусловный** ранний скип (`skipped_terminal_total++`, без `advance`/уведомления; не подчинён
|
||
`reconcile_skip_blocked_enabled`). Вызов `_note_unblock` на F-1 теперь передаёт `state_uuid` →
|
||
in-memory dedup работает на обоих путях (страховка от повтора после рестарта). Лечит
|
||
периодическое ложное «ET-002 done разблокирована (потерян webhook)» для терминальных в Plane
|
||
задач (enduro/orchestrator), сохраняя легитимный unblock реально застрявшей не-терминальной
|
||
задачи. `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД/сигнатуры/новые флаги — без изменений. Детали —
|
||
`docs/work-items/ORCH-086/06-adr/ADR-001-reconciler-f1-terminal-skip-and-dedup.md`.
|
||
- **F-2 plane-side:** опрос Plane API per-project → `handle_status_start` /
|
||
`handle_verdict` из `webhooks/plane.py` (логика не дублируется).
|
||
**ORCH-068 (livelock-fix):** (1) задачи в **терминальной группе** Plane
|
||
(`state.group ∈ {completed, cancelled}`, fallback — логические ключи
|
||
`done`/`cancelled`) исключаются из actionable-выборки per-issue — проектно-независимо,
|
||
устойчиво к UUID-алиасингу после переименований статусов (ORCH-066); (2) `_note_unblock`
|
||
(лог + Telegram + `unblocked_total`) вызывается ТОЛЬКО при **подтверждённом state change**
|
||
(сравнение стадии задачи до/после `_dispatch`; no-op dispatch → тишина), плюс in-memory
|
||
дедуп по `issue_id→state`. Восстанавливает инвариант silence-when-in-sync (AC-9/AC-10).
|
||
Детали — `docs/work-items/ORCH-068/06-adr/ADR-001-reconciler-terminal-exclusion-and-cache-ttl.md`.
|
||
- **F-3:** усиление `sha→branch` в `handle_ci_status` (БД-fallback по единственной
|
||
development-задаче repo; неоднозначность → не резолвим).
|
||
- **F-4 observability:** при разблокировке — лог-строка `reconciler: <wi> <stage>
|
||
разблокирована (потерян webhook)` + Telegram (`reconcile_notify_unblock`); снимок
|
||
состояния в `GET /queue` (блок `reconcile`). **ORCH-068** добавляет в снимок
|
||
счётчики `skipped_terminal_total` (исключённые терминалы) и `deduped_total`
|
||
(подавленные повторные нотификации).
|
||
|
||
Реализация: `src/reconciler.py` (daemon-поток по образцу `queue_worker`), стартует в
|
||
`main.lifespan` **после** `worker.start()`, останавливается в `finally` **перед**
|
||
`worker.stop()`.
|
||
|
||
Инварианты: источник истины — гейт/Plane, не событие; идемпотентность (active-job
|
||
guard + atomic-claim на создании под process-wide Lock + grace + `max_concurrency=1`);
|
||
never-raise на единицу работы; тишина при синхронности; restart-safe; kill-switch
|
||
`ORCH_RECONCILE_ENABLED` (+ `ORCH_RECONCILE_PLANE_ENABLED` гасит только F-2). Схема БД
|
||
и реестры (`STAGE_TRANSITIONS`/`QG_CHECKS`) не меняются. Подробнее:
|
||
[adr-0007](adr/adr-0007-reconciler.md), детально — `docs/work-items/ORCH-053/06-adr/ADR-001-stuck-task-reconciler.md`.
|
||
|
||
### Job-reaper + проактивный реклейм merge-lease (ORCH-065 — design)
|
||
Финализация статуса job (`done`/`queued`/`failed`) выполняется ТОЛЬКО в
|
||
`launcher._monitor_agent → _finalize_job` внутри живого процесса. Смерть
|
||
monitor-потока/процесса между `proc.wait()` и `_finalize_job` (краш, OOM,
|
||
self-restart во время deploy) оставляла строку `jobs` навсегда `running`; при
|
||
`max_concurrency=1` одна зомби-строка блокирует claim всех job → встаёт конвейер
|
||
ВСЕХ проектов (инциденты 07.06: jobs 236/239/242/254). `requeue_running_jobs()`
|
||
спасал ТОЛЬКО на старте процесса. Симметрично залипал merge-lease (ORCH-043):
|
||
реклейм был лениво-по-TTL и только при чужом `acquire`, liveness держателя по pid
|
||
не проверялся. Это последняя ручная точка автономного self-deploy (блокер ORCH-54).
|
||
ORCH-065 вводит фоновый watchdog, чтобы смерть процесса/потока на любой стадии НЕ
|
||
оставляла навсегда захваченных ресурсов:
|
||
- **Job-reaper** (`src/job_reaper.py`) — daemon-поток по образцу `reconciler`,
|
||
работает **без рестарта**. Трёхуровневая liveness: Tier-1 мёртвый `jobs.pid`
|
||
(новая колонка) после `reaper_dead_ticks` подряд тиков (анти-ложноположительность
|
||
— живой долгий агент не реапится); Tier-2 `agent_runs.exit_code` записан, а job
|
||
ещё `running` — но это окно неоднозначно (живой monitor пишет exit_code ПЕРВЫМ,
|
||
затем git push/PR/Plane-комментарии), поэтому Tier-2 реапит только после
|
||
finalization-grace `reaper_finalize_grace_s` (живой финализирующий monitor НЕ
|
||
реапится); Tier-3 backstop по потолку `reaper_max_running_s` (> max
|
||
agent_timeout+grace). Действие переиспользует контракты по принципу
|
||
**claim-before-act**: для exit0 канонический QG оценивается read-only ПЕРЕД
|
||
атомарным claim, затем claim `done` ПЕРВЫМ и только победитель claim делает
|
||
`_try_advance_stage` (advance+enqueue) — проигравший claim (поздний monitor /
|
||
стартовый requeue) не выполняет побочных эффектов (нет дубль-advance/-enqueue);
|
||
источник истины — канонический QG, не факт «exit0»; гейт красный или exit≠0/
|
||
неизвестно → `attempts<max`→`queued`, иначе `failed`+Telegram. Атомарный
|
||
reap-claim (`UPDATE ... WHERE id=? AND status='running'`) совместим со стартовым
|
||
`requeue_running_jobs` (restart-safe, без двойной обработки).
|
||
- **Проактивный реклейм stale/dead lease** (функции в `merge_gate.py`:
|
||
`pid_alive`, `reclaim_stale_lease`) — на старте (рядом с `requeue_running_jobs`)
|
||
и периодически из тика reaper: освобождает lease, чей держатель **мёртв** (pid
|
||
не жив) ИЛИ **просрочен** (TTL `merge_lock_timeout_s`); живой держатель в
|
||
пределах TTL — НЕ трогать (защита легитимного merge). holder-aware, never-raise,
|
||
условность как ORCH-43 (`merge_gate_repos`/self-hosting).
|
||
- **Идемпотентная финализация merge** — без новой merge-логики: re-drive через
|
||
reaper→`queued`→переисполнение стадии / reconciler; дорогие шаги не повторяются
|
||
(`branch_is_behind_main==False`); добавлен never-raise guard `pr_already_merged`
|
||
(читает состояние PR) — уже слит = no-op. **Консультируется самим merge-актором:**
|
||
фактический merge PR в `main` делает агент `deployer` (в начале стадии `deploy`),
|
||
поэтому wiring — в его промпте `.openclaw/agents/deployer.md`, который вызывает
|
||
`pr_already_merged` ПЕРЕД любым (повторным) merge (AC-11). Чек `check_branch_mergeable`
|
||
НЕ меняется (AC-13): он на ПЕРВОМ ребре `deploy-staging → deploy`, а риск второго
|
||
merge — на re-drive самой стадии `deploy`.
|
||
- **Схема БД:** единственное изменение — `jobs.pid INTEGER` через идемпотентный
|
||
`_ensure_column` (live-safe). `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`, БАГ-8,
|
||
exit-коды хука, файл-схема lease — без изменений.
|
||
- **Наблюдаемость:** блок `reaper` в `GET /queue` (enabled, interval, last_run_ts,
|
||
reaped_total, last_reaped, lease_reclaimed_total); каждый reap/lease-reclaim →
|
||
`logger.warning`; reap→`failed` и lease-reclaim → Telegram.
|
||
- **Kill-switch'и:** `ORCH_REAPER_ENABLED`, `ORCH_REAPER_INTERVAL_S`,
|
||
`ORCH_REAPER_DEAD_TICKS`, `ORCH_REAPER_MAX_RUNNING_S`,
|
||
`ORCH_REAPER_FINALIZE_GRACE_S`, `ORCH_LEASE_RECLAIM_ENABLED`; `false` → строго
|
||
прежнее поведение.
|
||
|
||
Подробнее: [adr-0011](adr/adr-0011-job-reaper-lease-reclaim.md), детально —
|
||
`docs/work-items/ORCH-065/06-adr/ADR-001-job-reaper-and-lease-reclaim.md`.
|
||
|
||
### Осмысленная статусная модель Plane (ORCH-066 — реализовано)
|
||
Plane-доска была семантически перегружена: `In Progress` означал «человек запускает
|
||
конвейер», «идёт анализ», «идёт прод-деплой» и «возврат из Needs Input» одновременно.
|
||
ORCH-066 наводит порядок по утверждённой Owner модели, меняя **только слой B**
|
||
(Plane-индикация: `src/plane_sync.py` + точки простановки в `src/stage_engine.py`/
|
||
`src/webhooks/plane.py`/`src/reconciler.py`) и **не трогая слой A** (`STAGE_TRANSITIONS`,
|
||
инвариант). Статус — индикация, не управление (вердикты по-прежнему из YAML-frontmatter):
|
||
```
|
||
Backlog → Todo → [To Analyse] → Analysis → [In Review → Approved] → Architecture →
|
||
Development → Code-Review → Testing → Awaiting Deploy → [Confirm Deploy] → Deploying →
|
||
Monitoring after Deploy → Done
|
||
```
|
||
`[...]` = человеческий вход-триггер; остальное ставит орк.
|
||
- **6 новых логических ключей** (`to_analyse`/`analysis`/`code_review`/`awaiting_deploy`/
|
||
`deploying`/`monitoring`) в `_PLANE_NAME_TO_KEY` (резолв по имени) + `_DEFAULT_STATES`.
|
||
`To Analyse` заменяет `In Progress` как вход-триггер (старт + resume аналитика из Needs
|
||
Input; fork «старт vs resume» по `get_task_by_plane_id`+`has_active_job_for_task` —
|
||
сохранён). Стадии: analysis→`Analysis`, review→`Code-Review` (`_STAGE_TO_STATE_KEY`).
|
||
- **Self-deploy фазы:** Phase A → `Awaiting Deploy` (разгружает `In Review`), Phase B →
|
||
`Deploying`, Phase C/terminal-sync (self) → `Monitoring after Deploy` (НЕ `Done` сразу);
|
||
post-deploy monitor (ORCH-021): HEALTHY-окно → `Done`, DEGRADED → `Blocked` (тик
|
||
по-прежнему НИКОГДА не рестартит прод — ALERT_ONLY). Не-self репо: `deploy → Done` как
|
||
сейчас (terminal-sync разводится по `post_deploy.post_deploy_applies`).
|
||
- **Fail-closed (project-relative alias-fallback):** отсутствующий новый статус в проекте
|
||
деградирует на **собственный базовый UUID того же проекта** (`to_analyse/analysis→in_progress`,
|
||
`code_review→review`, `awaiting_deploy→in_review`, `deploying→in_progress`,
|
||
`monitoring→done`) — индикация откатывается к текущей, конвейер не ломается, PATCH валиден
|
||
даже при частичной конфигурации. Enduro (статусы не создаются) → строго прежнее поведение.
|
||
Усиленный паттерн ORCH-059 AC-7.
|
||
- **Reconciler:** F-2 триггер `in_progress`→`to_analyse`; Guard 2 skip-set расширен
|
||
активными ожиданиями (`awaiting_deploy`/`deploying`/`monitoring`) с **вычитанием базовых
|
||
рабочих статусов** — на enduro (алиасы схлопнуты) нулевой регресс, на orchestrator skip
|
||
реальных ожиданий (BR-13).
|
||
- **Инварианты:** `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_deploy_status`, exit-коды хука,
|
||
merge-gate, `Confirm Deploy`, механизм `Needs Input` (analyst-only), схема БД — без
|
||
изменений. Без нового kill-switch (раскат гейтится созданием Plane-статусов оператором).
|
||
Инфра-предусловие — `docs/work-items/ORCH-066/07-infra-requirements.md`.
|
||
|
||
Подробнее: `docs/work-items/ORCH-066/06-adr/ADR-001-plane-status-model.md`.
|
||
|
||
## Откаты
|
||
- Reviewer REQUEST_CHANGES → откат на `development` + retry (`MAX_DEVELOPER_RETRIES = 3`).
|
||
- Tester `check_tests_passed` FAIL → откат на `development` + retry.
|
||
- Deploy / deploy-staging FAILED → откат на `development`.
|
||
- Merge-gate FAIL (конфликт rebase / красный re-test, ORCH-043) → откат на `development` + retry; `merge-lock busy` → **defer** (не откат, dev-retry не тратится).
|
||
- `get_previous_stage` использует порядок ключей `STAGE_TRANSITIONS`.
|
||
|
||
### Обогащение `task_desc` при заворотах (ORCH-046)
|
||
При откате на `development` `task_desc` (попадает в `.task-dev.md` developer-агента) несёт **дословный must-fix текст**, а не только ссылку — чтобы агент видел суть претензий сразу и не повторял ту же ошибку:
|
||
- **reviewer REQUEST_CHANGES** → дословные пункты P0/P1 из секции `## Findings` файла `12-review.md` (`extract_review_findings`);
|
||
- **tester `check_tests_passed` FAIL** → `reason` гейта + фрагмент тела `13-test-report.md` (приоритет: `## Вывод pytest` → FAIL-строки `## Результаты` → `## Итог`; `extract_test_failures`).
|
||
|
||
Ссылка на полный файл-артефакт сохраняется всегда («Полный контекст»). Парсеры `src/review_parse.py` — defensive (never-raise); при отсутствующем/битом артефакте `task_desc` graceful-фоллбэк на прежнюю ссылку-строку, последовательность отката и retry-счётчик не меняются (ADR `docs/work-items/ORCH-046/06-adr/ADR-001-embed-findings-in-task-desc.md`).
|
||
|
||
### Plane Sync: единый status-коммент агентов (ORCH-016)
|
||
Все агенты (analyst / architect / developer / reviewer / tester / deployer) пишут финальный коммент через **один хелпер** `usage.build_status_comment(...)` (ADR `docs/work-items/ORCH-016/06-adr/ADR-001-unified-status-comment.md`). Формат HTML, разделители `<br>`:
|
||
|
||
```
|
||
{ICON} {RoleName} — {описание стадии}
|
||
[Verdict|Status: VALUE] # reviewer/tester/deployer, из YAML-frontmatter артефакта
|
||
[Длительность: 4m 12s] # явный duration_s от launcher, либо fallback из agent_runs
|
||
<b>Документы:</b><ul><li><a href="…">label</a></li>…</ul>
|
||
[<sub>8.5M in / 45.8k out · $7.29</sub>] # тех-хвост usage; опускается при нулях
|
||
```
|
||
|
||
- **Длительность** считается launcher'ом (`_monitor_agent`) и пробрасывается в `_post_usage_comments`; для analyst (коммент строится в `stage_engine`) используется DB-фоллбэк `usage.get_agent_duration(task_id, agent)`.
|
||
- **Vердикт-парсер** — единый контракт `src/frontmatter.py` (defensive, never-raise): коммент-хелпер использует `read_frontmatter_value(...)` (single-key, BC), гейты — `parse_frontmatter(...)` (ORCH-52c). Машинные ключи: reviewer → `verdict:` (12-review.md); **testing-гейт `check_tests_passed` (13-test-report.md) → любое из трёх равноправных: `result:` (канон промпта тестера), `verdict:`, `status:`** (ORCH-047, ADR-001); deployer → `deploy_status:` (14-deploy-log.md), `staging_status:` (15-staging-log.md). Negative-токен в любом поле авторитетен (перебивает positive).
|
||
- Формат коммента **не** меняет реестр гейтов и стадий; коммент — отображение, не управление.
|
||
|
||
## База данных (SQLite)
|
||
- `events` — входящие вебхуки (дедуп)
|
||
- `tasks` — задачи и их стадии; колонки `cancelled_at`/`cancel_requested_at` (ORCH-090) — durable-метки STOP-отмены (вторая — отложенная отмена в критичном окне merge/deploy). Терминальная стадия `cancelled` (сток, параллельно `done`); натуральные ключи отменённой строки тумбстонятся суффиксом `#cancelled-<id>` (`plane_id`/`work_item_id`/`plane_issue_id`)
|
||
- `agent_runs` — запуски агентов (run_id, usage, cost)
|
||
- `jobs` — очередь задач (ORCH-1); статусы `queued|running|done|failed|cancelled` (ORCH-090: `cancelled` — терминальный исход STOP, нигде не реквью'ится); колонка `pid` (ORCH-065) — pid агентского процесса для liveness-детекции зомби job-reaper'ом
|
||
- `job_deps` — декларативные зависимости задач (ORCH-026, Уровень B): `(task_id, depends_on_task_id)`, аддитивная; источник истины планировщика для гейта «B ждёт A»
|
||
- `repo_freeze` — durable per-repo rollback-freeze (ORCH-088, FR-5): `(id, repo, frozen_at, reason, work_item_id, cleared_at)`, аддитивная append-only; активный freeze ⇔ строка репо с `cleared_at IS NULL`. Выставляется post-deploy `DEGRADED` (`set_repo_freeze`), снимается вручную (`POST /serial-gate/unfreeze` → `cleared_at=now`). Гейтит serial-claim безусловно (деградировавшая задача уже `done`)
|
||
- `lessons` — машинный журнал отклонений конвейера (ORCH-098, FR-1): `(id, created_at, updated_at, lesson_type, work_item_id, task_id, stage, agent, repo, root_cause, suggestion, status, related_task, attribution, target_repo, target_domain, source, detail)`, аддитивная идемпотентная (`CREATE TABLE IF NOT EXISTS` + три индекса); колонки атрибуции (`attribution`/`target_repo`/`target_domain`) — нуллабельны и присутствуют сразу (NFR-6), без `enum`-констрейнтов (слаги forward-compatible). Автозапись 4 типов (`gate_failure`/`merge_hold`/`transient_retry`/`deploy_degraded`, `source="auto"`, дедуп в окне `lessons_dedup_window_s`) + ручная (`source="manual"`); observer-only (не участвует в решении гейта). Leaf `src/lessons.py` never-raise, kill-switch `lessons_enabled` (без `*_repos` — журнал не скоупится по репо, репо-разрез на выборке)
|
||
|
||
## Изоляция (git worktree, ORCH-2)
|
||
Каждая задача исполняется в отдельном git worktree, ветки не пересекаются. Репозитории проектов разделены под `/repos/<project>`.
|
||
|
||
## API
|
||
| Method | Path | Описание |
|
||
|--------|------|----------|
|
||
| GET | `/health` | health check |
|
||
| GET | `/status` | активные задачи (stage != done) |
|
||
| GET | `/queue` | очередь: counts + max_concurrency + resilience + reconcile (ORCH-053) + reaper (ORCH-065) + post_deploy (ORCH-021) + task_deps (ORCH-026) + serial_gate (ORCH-088) + auto_labels (ORCH-089) + stop (ORCH-090) + lessons (ORCH-098) + последние jobs |
|
||
| GET | `/metrics` | ORCH-099 (FND/F1a): read-only машинное «сырьё» для sidecar F1b — конверт `schema_version`/`generated_at`/`clk_tck` + разделы `stages`/`queue`/`agents` (liveness: pid/runtime/cpu_ticks)/`cost`. never-raise по разделам; kill-switch `ORCH_METRICS_ENABLED` (дефолт `True`). Контракт — см. раздел «Сырьё-эндпоинт `/metrics`» |
|
||
| POST | `/serial-gate/unfreeze` | ORCH-088 (FR-5): ручное снятие per-repo rollback-freeze (query/body `repo=<repo>`) → `{ok, repo, cleared, frozen}`; идемпотентно. Альтернатива — `UPDATE repo_freeze SET cleared_at=datetime('now') WHERE repo=? AND cleared_at IS NULL` |
|
||
| GET | `/lessons` | ORCH-098 (FR-4): read-only выборка журнала уроков; query-фильтры `type`/`status`/`repo`/`work_item`/`limit` → `{enabled, lessons:[…]}` (всегда `200`, чтение не мутирует). При `lessons_enabled=False` → `{enabled:false, lessons:[]}` |
|
||
| POST | `/lessons` | ORCH-098 (FR-5): ручная запись урока (JSON-тело, `lesson_type` обязателен, `source="manual"` не дедупится) → `{id}`; при выключенном флаге → `{enabled:false}` |
|
||
| POST | `/lessons/{id}` | ORCH-098 (FR-5): доклассификация/обновление урока (`status`/`attribution`/`target_*`/`related_task`/`root_cause`/`suggestion`), стампит `updated_at` → `{ok}` |
|
||
| POST | `/webhook/plane` | Plane webhook |
|
||
| POST | `/webhook/gitea` | Gitea webhook (push, PR, CI status) |
|
||
|
||
## Деплой и эксплуатация
|
||
Топология, контейнеры, порты, env-карта, self-hosting риски — [docs/operations/INFRA.md](../operations/INFRA.md). Деплой-хук — [DEPLOY_HOOK.md](../operations/DEPLOY_HOOK.md). Staging — [STAGING.md](../operations/STAGING.md).
|
||
|
||
## ADR
|
||
Сквозные архитектурные решения — [adr/](adr/). Per-work-item решения — `docs/work-items/<id>/06-adr/`.
|
||
|
||
## Детали реализации
|
||
Схема БД, потоки данных, resilience-слой, детали Dockerfile — [internals.md](internals.md).
|
||
|
||
---
|
||
*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-021-post-deploy-rollback (reserved-agent job `post-deploy-monitor`: арм в src/stage_engine.py блок `next_stage == "done"`, тик `run_post_deploy_monitor` + перехват в src/agents/launcher.py ДО _spawn; чистая логика src/post_deploy.py never-raise; флаги `post_deploy_*` в src/config.py; блок `post_deploy` в `/queue`; артефакт 16-post-deploy-log.md; self-hosting всегда ALERT_ONLY — тик не рестартит прод; обновлять также при изменении src/post_deploy.py / арм-блока / launcher-перехвата); ORCH-065 (job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge, adr-0011, `docs/work-items/ORCH-065/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-065 (новый daemon-поток src/job_reaper.py + старт/стоп в src/main.py lifespan; колонка `jobs.pid` через _ensure_column + проставление в src/agents/launcher.py `_spawn`; функции реклейма lease `pid_alive`/`reclaim_stale_lease` + guard `pr_already_merged` в src/merge_gate.py (консультируется merge-актором — промпт `.openclaw/agents/deployer.md`); флаги `reaper_*`/`lease_reclaim_*` в src/config.py; блок `reaper` в `/queue`; обновлять также при изменении этих мест); ORCH-022 (security-гейт: secret-scanning gitleaks + dependency audit pip-audit как под-гейт ребра `deploy-staging → deploy` ПЕРВЫМ, adr-0012, `docs/work-items/ORCH-022/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-022-security-secret-scanning (leaf src/security_gate.py never-raise + check_security_gate в src/qg/checks.py `QG_CHECKS` + врезка _handle_security_gate в src/stage_engine.py блок `current_stage == "deploy-staging"` ПЕРВОЙ; флаги `security_*` в src/config.py; gitleaks (pinned) в Dockerfile, pip-audit в requirements.txt, `.gitleaks.toml` в корне; артефакт 17-security-report.md; обновлять также при изменении этих мест).*
|
||
*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-021-post-deploy-rollback (reserved-agent job `post-deploy-monitor`: арм в src/stage_engine.py блок `next_stage == "done"`, тик `run_post_deploy_monitor` + перехват в src/agents/launcher.py ДО _spawn; чистая логика src/post_deploy.py never-raise; флаги `post_deploy_*` в src/config.py; блок `post_deploy` в `/queue`; артефакт 16-post-deploy-log.md; self-hosting всегда ALERT_ONLY — тик не рестартит прод; обновлять также при изменении src/post_deploy.py / арм-блока / launcher-перехвата); ORCH-065 (job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge, adr-0011, `docs/work-items/ORCH-065/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-065 (новый daemon-поток src/job_reaper.py + старт/стоп в src/main.py lifespan; колонка `jobs.pid` через _ensure_column + проставление в src/agents/launcher.py `_spawn`; функции реклейма lease `pid_alive`/`reclaim_stale_lease` + guard `pr_already_merged` в src/merge_gate.py (консультируется merge-актором — промпт `.openclaw/agents/deployer.md`); флаги `reaper_*`/`lease_reclaim_*` в src/config.py; блок `reaper` в `/queue`; обновлять также при изменении этих мест); ORCH-059 (выделенный статус-триггер прод-деплоя «Confirm Deploy», ADR `docs/work-items/ORCH-059/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-059 (маппинг `"Confirm Deploy"→"confirm_deploy"` в src/plane_sync.py `_PLANE_NAME_TO_KEY`, НЕ в `_DEFAULT_STATES` = fail-closed; ветка `handle_confirm_deploy` + fail-closed `.get("confirm_deploy")` в src/webhooks/plane.py `handle_issue_updated`; keyword-only `confirm_deploy` в src/stage_engine.py `advance_stage` — Фаза B деплоит ТОЛЬКО при `confirm_deploy=True`, иначе `Approved`-на-`deploy` = no-op; CTA Фазы A просит «Confirm Deploy»; эксплуатация — статус доски «Confirm Deploy» в Plane-проекте ORCH, `docs/work-items/ORCH-059/07-infra-requirements.md`).*
|
||
*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-021-post-deploy-rollback (reserved-agent job `post-deploy-monitor`: арм в src/stage_engine.py блок `next_stage == "done"`, тик `run_post_deploy_monitor` + перехват в src/agents/launcher.py ДО _spawn; чистая логика src/post_deploy.py never-raise; флаги `post_deploy_*` в src/config.py; блок `post_deploy` в `/queue`; артефакт 16-post-deploy-log.md; self-hosting всегда ALERT_ONLY — тик не рестартит прод; обновлять также при изменении src/post_deploy.py / арм-блока / launcher-перехвата); ORCH-065 (job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge, adr-0011, `docs/work-items/ORCH-065/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-065 (новый daemon-поток src/job_reaper.py + старт/стоп в src/main.py lifespan; колонка `jobs.pid` через _ensure_column + проставление в src/agents/launcher.py `_spawn`; функции реклейма lease `pid_alive`/`reclaim_stale_lease` + guard `pr_already_merged` в src/merge_gate.py (консультируется merge-актором — промпт `.openclaw/agents/deployer.md`); флаги `reaper_*`/`lease_reclaim_*` в src/config.py; блок `reaper` в `/queue`; обновлять также при изменении этих мест); ORCH-066 (осмысленная статусная модель Plane — слой B, `docs/work-items/ORCH-066/06-adr/ADR-001-plane-status-model.md`) — реализовано в ветке feature/ORCH-066-plane (только Plane-индикация: новые ключи `to_analyse`/`analysis`/`code_review`/`awaiting_deploy`/`deploying`/`monitoring` в `_PLANE_NAME_TO_KEY`/`_DEFAULT_STATES` + project-relative `_STATE_ALIAS_FALLBACK` в get_project_states + `_STAGE_TO_STATE_KEY` analysis/review + 5 новых `set_issue_*` в src/plane_sync.py; триггер `in_progress`→`to_analyse` и `set_issue_analysis` в src/webhooks/plane.py; Phase A→Awaiting Deploy / Phase B→Deploying / terminal-sync split monitoring↔done / post-deploy monitor HEALTHY→Done DEGRADED→Blocked в src/stage_engine.py; F-2 триггер `to_analyse` + Guard 2 skip-set с вычитанием base_working в src/reconciler.py; `STAGE_TRANSITIONS`/QG/схема БД НЕ трогаются; без kill-switch — раскат гейтится созданием 6 Plane-статусов оператором, `docs/work-items/ORCH-066/07-infra-requirements.md`; обновлять при изменении этих мест).*
|
||
*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-021-post-deploy-rollback (reserved-agent job `post-deploy-monitor`: арм в src/stage_engine.py блок `next_stage == "done"`, тик `run_post_deploy_monitor` + перехват в src/agents/launcher.py ДО _spawn; чистая логика src/post_deploy.py never-raise; флаги `post_deploy_*` в src/config.py; блок `post_deploy` в `/queue`; артефакт 16-post-deploy-log.md; self-hosting всегда ALERT_ONLY — тик не рестартит прод; обновлять также при изменении src/post_deploy.py / арм-блока / launcher-перехвата); ORCH-065 (job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge, adr-0011, `docs/work-items/ORCH-065/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-065 (новый daemon-поток src/job_reaper.py + старт/стоп в src/main.py lifespan; колонка `jobs.pid` через _ensure_column + проставление в src/agents/launcher.py `_spawn`; функции реклейма lease `pid_alive`/`reclaim_stale_lease` + guard `pr_already_merged` в src/merge_gate.py (консультируется merge-актором — промпт `.openclaw/agents/deployer.md`); флаги `reaper_*`/`lease_reclaim_*` в src/config.py; блок `reaper` в `/queue`; обновлять также при изменении этих мест); ORCH-068 (livelock-fix reconciler F-2: терминал-исключение по группе состояния + `_note_unblock` только при подтверждённом state change + дедуп; TTL `_STATES_CACHE`, `docs/work-items/ORCH-068/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-068 (D1 терминал-гард по группе `_is_terminal_state` + `get_project_state_groups` в src/plane_sync.py; D2 сравнение стадии до/после `_dispatch` + дедуп-словарь в src/reconciler.py; TTL-запись `_STATES_CACHE` + флаг `plane_states_ttl_s` в src/config.py; счётчики `skipped_terminal_total`/`deduped_total` в `/queue`; обновлять также при изменении src/reconciler.py F-2, src/plane_sync.py `get_project_states`/`get_project_state_groups`/`_STATES_CACHE`).*
|
||
*Актуально на 2026-06-09. Статус доработки: ORCH-088 (per-repo serial gate, Этап 1 serial e2e, adr-0017, `docs/work-items/ORCH-088/06-adr/ADR-001-serial-gate.md`) — реализовано в ветке feature/ORCH-088 (leaf src/serial_gate.py never-raise: gate-фрагмент в src/db.py `claim_next_job` fail-OPEN c FIFO-условием `t2.id < jobs.task_id` + freeze `repo_freeze.cleared_at IS NULL`, freeze-решения fail-CLOSED; отложенный срез ветки src/webhooks/plane.py `start_pipeline` → src/agents/launcher.py `_materialize_deferred_branch` (sync `asyncio.run` в worker-потоке) при claim analyst-job; durable freeze таблица `repo_freeze` (idempotent миграция в init_db) + `set_repo_freeze` в src/stage_engine.py DEGRADED-ветке `run_post_deploy_monitor` + ручное снятие `POST /serial-gate/unfreeze` в src/main.py; флаги `serial_gate_enabled`/`serial_gate_repos`/`serial_gate_freeze_enabled` в src/config.py; блок `serial_gate` в `GET /queue`; `STAGE_TRANSITIONS`/`QG_CHECKS` НЕ трогаются; обновлять также при изменении этих мест).*
|