184 KiB
Архитектура 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) — персистентная очередь задач (SQLitejobs), atomic claim, max_concurrency, ретраи, restart-safe. ORCH-026:claim_next_jobгейтит задачи с незавершёнными зависимостями (job_deps,NOT EXISTS) без занятия слота; декларации/циклы — leafsrc/task_deps.py. - Job-reaper (
src/job_reaper.py, ORCH-065 — adr-0011) — фоновый daemon-поток (каркасreconciler), стартует/останавливается вmain.lifespan(послеreconciler.start()/ передworker.stop()). Детектирует «мёртвый»running-job без рестарта процесса (Tier-1 мёртвыйjobs.pidпослеreaper_dead_ticksтиков; Tier-2agent_runs.exit_codeзаписан, а job ещёrunning; Tier-3 backstopreaper_max_running_s) и приводит строку к корректному статусу через те же контракты (_try_advance_stage/_finalize_job, gate-driven; exit≠0/неизвестно →attempts<max→queued, иначеfailed+Telegram). Атомарный reap-claim (guardstatus='running') совместим со стартовымrequeue_running_jobs. Тот же поток периодически делает проактивный реклейм stale/dead merge-lease (см. ниже). never-raise; kill-switchORCH_REAPER_ENABLED; снимок вGET /queue(блокreaper). - Reconciler (
src/reconciler.py, ORCH-053 — реализовано, adr-0007) — фоновый 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 (БД-fallbacksha→branchвhandle_ci_status). Источник истины — гейт/Plane, не событие; идемпотентность (active-job guard + atomic-claim + grace); kill-switchORCH_RECONCILE_ENABLED.analysisF-1 не трогает (человеческий гейт). F-1 также пропускает escalated (retry≥лимита) и Blocked/Needs-Input задачи (ORCH-060). Наблюдаемость — блокreconcileвGET /queue. - Disk-watchdog (
src/disk_watchdog.py, ORCH-063 — adr-0024) — фоновый daemon-поток (каркасreconciler/job_reaper), стартует/останавливается вmain.lifespan(старт последним — послеreaper.start(); стоп первым в reverse-порядке; гардdisk_monitor_enabled). Каждыеdisk_monitor_interval_s(дефолт 300с) меряет заполнение хост-ФС по смонтированным bind-путям (/repos,/app/data) через stdlibshutil.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-switchORCH_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) — фоновый 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 безопасность). В контейнере нетdockerCLI (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-switchORCH_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-switchORCH_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→ Telegram400 can't parse entities→ застывшая карточка, инцидент ORCH-093);_fmt_minutesпо-прежнему даёт<1м(escape рендерит визуально идентично). Застрявшая карточка в окне авто-восстанавливается следующим рендером;edit_telegram/update_task_tracker/леджер сирот не тронуты. Детали — internals.md §7, ADR-087, ORCH-091 ADR-001 и ORCH-095 ADR-001. - 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) и TTLORCH_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) — чистый never-raise leaf (паттернserial_gate/preflight), закрывает пробел ORCH-040: при миграции наuser: "1000:1000"legacyroot: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) и поднимает actionableRuntimeErrorс причиной + лечащей командой (не-прав-ошибки сохраняют прежний контракт — меняется только формулировка, не факт сбоя); (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) — лёгкий 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-switchmetrics_endpoint_enabled(дефолтTrue).STAGE_TRANSITIONS/QG_CHECKS/схема БД — не тронуты. - Lessons journal (
src/lessons.py+ таблицаlessons, ORCH-098 — реализовано, adr-0034) — машинный журнал уроков (структурированная база отклонений конвейера); шаг 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_verifyHOLD),transient_retry(merge-retry/launcher transient budget-exhaustion),deploy_degraded(post-deployDEGRADED → 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-switchlessons_enabledonly, безlessons_repos); репо-разрез — на выборке (repo-колонка/фильтр), enduro не затронут (общая БД, аддитивная таблица).STAGE_TRANSITIONS/QG_CHECKS/check_*/machine-verdict/схемы существующих таблиц — байт-в-байт не тронуты (журнал не участвует в решении гейта). Kill-switchlessons_enabled(envORCH_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) — мозг мониторинга в ОТДЕЛЬНОМ контейнере (наблюдатель отделён от наблюдаемого, C-1): код в репо орка (watchdog/), рантайм — свой образ (watchdog/Dockerfile,python:3.12-slim, stdlib-only) + сервис вdocker-compose.yml(network_mode: host, read-onlydocker.sock,mem_limit: 128m). На каждом тике собирает 4 источника:GET /metricsорка (F1a/ORCH-099), хост (диск/inode/память/CPU, stdlib), статусы контейнеров через read-onlydocker.sock(GET-only, безdockerSDK), пинг Plane/Gitea/Anthropic. Каждый сигнал → обобщённая чистаяdecide(signal_active, prev, now, cooldown)(генерализацияdisk_watchdog.decide_action, per-signal in-memoryAlertState) → алерт в собственный 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-switchWATCHDOG_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.
Конверт ответа:
{
"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, детально —
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-onlydocker.sock— только GET list/inspect (Up/healthy/restarting/exited/unhealthy), безdockerSDK; (d) пинг Plane/Gitea/Anthropic. - Решение — обобщённая чистая функция
decide(signal_active, prev, now, cooldown) -> alert | realert | recovery | none(строгая генерализацияsrc/disk_watchdog.py::decide_action; per-signal in-memoryAlertState, рестарт → корректный повторный алерт стоящей проблемы). Реестр сигналов: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, детально —
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-closedConfirm Deploy/STOP, лейблы, webhook, полнота kit, скан неразрешённых плейсхолдеров). Имена статусов — read-only импортplane_sync._PLANE_NAME_TO_KEY(22, нулевой дрейф); канонические группы фиксированы ADR (код-критично:STOP→cancelledORCH-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, детально —
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, детально —
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.
Type A — Lite (ORCH-102 — design). Поверх 10-common вводится канон Lite-тиража: новый
docs-раздел docs/deployment/ (витрина тиража, читатель — внешний оператор) с golden source
docs/deployment/LITE_SETUP.md — сквозной маршрут «голый хост → работающий конвейер» из 13
нормативных разделов (предусловия → код → конфиг/секреты → Plane → Gitea → LLM → Telegram →
запуск → онбординг проекта → smoke → stateless-проверка → траблшутинг), каждый шаг =
fenced-команда + явная проверка (PASS/FAIL), хост-специфика — только плейсхолдеры. Compose не
форкается: docker-compose.yml сам является Lite-подмножеством (дефолтный up -d поднимает
ровно orchestrator+orchestrator-watchdog; staging — за профилем, в Lite опционален). Канон
watchdog-конфига — новый .env.watchdog.example (key-set = блоку WATCHDOG_* .env.example;
sidecar читает только .env.watchdog; C-1 ORCH-100 — отдельный бот). Норматив тиражной
инсталляции Gitea: branch protection на main НЕ включать (D10 ORCH-009, защита merge-актора).
Анти-дрейф — структурный tests/test_lite_setup_doc.py. Рантайм/конвейер — байт-в-байт
(docs+tests). Подробнее: adr-0037, детально —
docs/work-items/ORCH-102/06-adr/ADR-001-lite-setup-doc-canon.md.
Установщик Lite (ORCH-104 — design). Поверх ручного канона ORCH-102 вводится интерактивный
установщик scripts/install_lite.py — «connect-only»-сородич bootstrap_bundle.py (тот
поднимает Plane/Gitea, этот подключается к уже существующим заказчика), автоматизирующий
happy-path LITE_SETUP.md §2–§11. Самодостаточный stdlib-only один файл (примитивы эталона
реплицированы, не вынесены — bootstrap_bundle.py байт-в-байт; вынос общего модуля — по
rule-of-three): step-движок check→ensure, режимы plan/apply/verify, exit 0/2/1, honest
manual_checkpoint. Скан предусловий хоста + управляемая установка зависимостей (точная команда
под apt/dnf, выполнение только с явным согласием при TTY; тихого root-инсталла нет, D-1);
best-effort детект существующих Plane/Gitea (docker ps + порт-пробы, ранжир unauth-liveness) с
ручным фолбэком; интерактивный сбор токенов/URL с живой верификацией ДО записи (getpass,
Plane /projects/, Gitea /user, Telegram getMe). Каноны не форкаются: webhook-секреты —
gen_secrets.py, регистрация проекта/22 статуса — onboard_project.py, env — рендер из
.env.example/.env.watchdog.example, стек — docker-compose.yml. Граница connect-only
(нормативно): webhook Plane — верифицируемый manual-step (печать инструкции пути A/Б, без
исполнения raw-SQL в чужую БД — INV-5; истинный гейт — smoke §11); логин claude CLI — manual-step
с верификацией. Гигиена секретов: getpass, права 600, без молчаливой перезаписи (NFR-2). Не-интерактив/CI
— fail-closed exit 2 с именем недостающего ключа; секреты из env, не из argv. Анти-дрейф —
новый tests/test_install_lite_script.py (структурный + юнит + фейки) + ассерт ссылки на
установщик в test_lite_setup_doc.py (13 разделов сохранены). Рантайм/конвейер — байт-в-байт
(scripts+docs+tests); kill-switch не нужен (активация — явный запуск оператором). Подробнее:
adr-0040, детально —
docs/work-items/ORCH-104/06-adr/ADR-001-lite-installer.md.
Type B — Bundled (ORCH-103). Закрывает эпик ORCH-10: весь стек одним комплектом
(орк + watchdog + Gitea + Plane CE ≈13–14 контейнеров) для заказчика без собственной
инфраструктуры. Состав Plane — зеркало официального selfhost-référence v0.23.1
(upstream-имена сервисов web/space/admin/api/worker/beat-worker/migrator/live +
plane-db/plane-redis/plane-mq/plane-minio/proxy); Gitea — gitea/gitea:1.22.6
(не rootless, ssh выключен). Новый top-level каталог deploy/ (исполняемые дистрибутивы; дополняет
docs/deployment/ — инструкции): deploy/bundled/docker-compose.yml — один самодостаточный
compose с name: orchestrator-bundle (узнаваемый префикс томов/контейнеров; container_name
не пиннится — нет коллизий с корневым compose на одном хосте), пиннинг сторонних образов
неподвижными тегами литералом (не latest); корневой compose не форкается (заморожен
анти-дрейфом ORCH-102); staging-контур орка в bundle отсутствует, репо orchestrator не
регистрируется → self-deploy-машинерия структурно спит (SELF_HOSTING_REPO-леафы не матчатся).
Сеть — одна bridge: машинный трафик строго сервис-DNS (webhooks в обе стороны, API, /metrics),
наружу — только человеческие порты (Plane 8080 / Gitea 3000 / орк 8500; явный
GITEA__webhook__ALLOWED_HOST_LIST=orchestrator против дефолтного запрета приватных таргетов).
Конфиг-слои: deploy/bundled/.env.example (канон bundle-инфры, key-set-sync тест) → live
deploy/bundled/.env (авто-чтение compose из project dir, без --env-file-футгана); runtime
орка/watchdog — корневые .env/.env.watchdog ровно по канону Lite (env_file: required: false до сборки); единственный писатель live-файлов — bootstrap.
scripts/bootstrap_bundle.py (python stdlib-only, plan-дефолт/apply/verify, step-движок
check→ensure, exit 0/2/1): preflight fail-fast до мутаций → секреты (gen_secrets.py +
stdlib-креды стека, в логи не печатаются) → up+ожидание готовности → init Gitea (полностью
автоматом через CLI; branch protection НЕ включать — D10 ORCH-009) → init Plane CE (честные
manual-step: инструкция → подтверждение → API-верификация результата) → онбординг
sandbox-проекта строго onboard_project.py apply/verify (host-venv, канон ONBOARDING) →
git-доступ агентов token-remote (_push_url-паттерн; ssh-контур не вводится) → сборка env
орка → health/итог; delete-операций в скрипте нет — teardown только документированной
процедурой (§13). Golden source — docs/deployment/BUNDLED_SETUP.md (14 разделов по канону
LITE_SETUP, требования к хосту по замеру тестового развёртывания; REPLICATION §1 — отметка
Type B). Анти-дрейф — tests/test_bundle_compose.py / test_bundled_setup_doc.py /
test_bootstrap_script.py. Рантайм/конвейер — байт-в-байт; kill-switch не нужен (активация —
только явный запуск оператора на целевом хосте, паттерн ORCH-009). Подробнее:
adr-0038, детально —
docs/work-items/ORCH-103/06-adr/ADR-001-bundled-stack-compose-and-bootstrap.md.
Витрина системы docs/overview/ (ORCH-011 — design)
Единая точка входа «бизнес + тех» для трёх аудиторий (заказчик / менеджер / разработчик) —
новый docs-раздел docs/overview/ (семантика разделов: overview/ — «что это за система
и как устроена», architecture/ — инженерный справочник, deployment/ — «как развернуть у
себя», operations/ — «как эксплуатировать наш прод»). Состав — плоский каталог, 10 файлов:
индекс README.md (маршруты 3 аудиторий + норматив сопровождения), business.md
(бизнес-уровень без жаргона), 7 × tech-*.md (= 7 блоков: архитектура / конвейер / агенты /
модель объектов / интеграции / качество-безопасность / наблюдаемость), presentation.md
(слайдо-источник). Link-first: витрина ссылается на golden sources (этот README, internals,
стандарты, ADR), не форкает их; разрешённый дубль — только машинно-сверяемый тестом факт
(стадии/гейты/агенты — derive-тестами из STAGE_TRANSITIONS/QG_CHECKS/glob промптов).
Канон презентации: .pptx (тёмный дизайн) собирается из presentation.md dev-скриптом
scripts/build_presentation.py (python-pptx, запуск только вне рантайма; зависимость в
прод-образ не попадает — машинный гард); собранный бинарь в git не коммитится. Норматив
сопровождения (кросс-каттинг): «изменил функциональность → обнови витрину в том же PR»;
reviewer-ось обзорных доков (ORCH-079) расширена на витрину (finding ≥ P1). Анти-дрейф —
структурный tests/test_system_docs.py (паттерн test_lite_setup_doc.py); новый QG не
вводится, рантайм байт-в-байт. Подробнее: adr-0039,
детально — docs/work-items/ORCH-011/06-adr/ADR-001-system-overview-canon.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; копируемые скелеты — в
docs/_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. ADR:
adr-0019 / adr-0020,
детально — 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; детально —
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 (рядом с 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; детально —
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; детально —
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.
Толерантность 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(envORCH_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, детально — 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-circuitbranch_is_behind_mainпропускается —auto_rebase_onto_mainвызывается всегда под лизом. На актуальной ветке это no-op (rebaseне меняет HEAD,push --force-with-lease→ «Everything up-to-date», CI не триггерится); на отстающей — реальный догон. Детерминированный структурный анти-фантом на уровне планировщика (дополняет рубежи ORCH-073, не заменяет). Kill-switchpremerge_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» (для selfdone⇔ 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, детально — docs/work-items/ORCH-043/06-adr/ADR-001-merge-gate.md.
Безусловный pre-merge rebase + связь с зависимостями задач — adr-0015 (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 coveragesrc/). Тайм-аут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 (capMAX_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-setUPDATE ... WHERE coverage <= measured(базовая линия не падает) под held merge-lease + per-repo сериализацией merge (ORCH-043). - Условность (как ORCH-35/43/58):
coverage_gate_enabled+coverage_gate_repos(пусто → только self-hostingorchestrator); вне области → no-op pass;applies(repo)ПЕРВОЙ. Ошибка инструмента → fail-open + WARNING по умолчанию (coverage_tool_fail_closed=False, анти-петля как ORCH-061); флаг → fail-closed. - Артефакт
18-coverage-report.md(frontmattercoverage_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, детально —
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-switchtask_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); PlaneBlocked— на дедлоке (не на нормальном коротком ожидании, чтобы не флаппить). Инвариант «одна карточка на задачу» сохранён. - Совместимость:
reconcilerF-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, детально — 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_depsNOT EXISTS(ORCH-026); только локальная БД (offline hot-path, NFR-2). Job'ы уже активной задачи проходят свободно. FIFO-уточнение реализации (FR-2): ADR-001 D1 фиксировал псевдо-SQLt2.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_branch409 = no-op). - Durable per-repo freeze (новая аддитивная таблица
repo_freeze,cleared_at IS NULL= активен) — post-deployDEGRADED/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-repoactive_task/waiting/frozen). Cross-repo параллелизм сохранён (FR-3); при выключенном флаге — нулевая регрессия (enduro не затронут).
Подробнее: adr-0017, детально —
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(полеlabelsissue,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_labelFalse = ручной режим (fail-safe).
Подробнее: adr-0018, детально —
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_atORCH-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_labelFalse = полный цикл (fail-safe).
Подробнее: adr-0032, детально —
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-deployINITIATED-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, пусто → все репо); leafsrc/cancel.py(never-raise); read-only блокstopвGET /queue; лог + Telegram (кликабельный номер) + Plane-коммент + live-карточка. При выключенном флаге — нулевая регрессия (enduro не затронут).
Подробнее: adr-0026, детально —
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 retagSOURCE_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, детально —
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получает kwargconfirm_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_finalizerPhase 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 повтор) → иначе GiteaPOST /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-guardmerge_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-switchregression_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во frontmatter14-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-коды хука — без изменений. Диагностика фантома — runbookdocs/operations/PHANTOM_MERGE_RUNBOOK.md(4 проверки постмортема).
Подробнее: adr-0013 +
adr-0014 (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-PRbase != 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 (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), envORCH_MERGE_RETRY_*.STAGE_TRANSITIONS,QG_CHECKS, схема БД, exit-коды хука, merge-gate, image-freshness — без изменений;mainне push/force-push.
Подробнее: adr-0027
(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_s2 /merge_retry_backoff_max_s5; суммарно ≤10 с, не подвешивает monitor-поток). Шаги до POST (idempotencypr_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, envORCH_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 (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-frontmatterpost_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-switchpost_deploy_monitor_enabled, областьpost_deploy_repos(пусто → self-hosting). Условность как ORCH-35/36/43/58.
Подробнее: adr-0010, детально —
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"⇒ на стр. 404ARMEDуже есть ⇒ легитимный первый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-switchORCH_DEPLOY_STATUS_GUARD_ENABLED(→ 1:1), областьORCH_DEPLOY_STATUS_GUARD_REPOS(пусто → self-hosting). Ограничение: внешняя Plane-automation (если таков актор) закрывается буфером сходимости, а не code-фиксом — локализация актора в задаче (BR-7).
Подробнее: adr-0028, детально —
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,
детально — 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-frontmattersecurity_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, детально —
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):
рендер исправен, корень — потеря учёта старых 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,
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); терминальная задача (группа Planecompleted/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, детально — 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-2agent_runs.exit_codeзаписан, а job ещёrunning— но это окно неоднозначно (живой monitor пишет exit_code ПЕРВЫМ, затем git push/PR/Plane-комментарии), поэтому Tier-2 реапит только после finalization-gracereaper_finalize_grace_s(живой финализирующий monitor НЕ реапится); Tier-3 backstop по потолкуreaper_max_running_s(> max agent_timeout+grace). Действие переиспользует контракты по принципу claim-before-act: для exit0 канонический QG оценивается read-only ПЕРЕД атомарным claim, затем claimdoneПЕРВЫМ и только победитель 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 не жив) ИЛИ просрочен (TTLmerge_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 guardpr_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, детально —
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_passedFAIL → откат на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_passedFAIL →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-deployDEGRADED(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 (не участвует в решении гейта). Leafsrc/lessons.pynever-raise, kill-switchlessons_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. Деплой-хук — DEPLOY_HOOK.md. Staging — STAGING.md.
ADR
Сквозные архитектурные решения — adr/. Per-work-item решения — docs/work-items/<id>/06-adr/.
Детали реализации
Схема БД, потоки данных, resilience-слой, детали Dockerfile — 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 НЕ трогаются; обновлять также при изменении этих мест).