Add CHANGELOG entry for the phantom-merge fix (merge-verify sub-gate, deterministic merge actor, post-deploy verification, kill-switch). Addresses P0 blocker from reviewer (attempt 2/3): docs = golden source per CLAUDE.md §2/§6 and AC-5. Refs: ORCH-071 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
82 KiB
82 KiB
Changelog
Формат: Keep a Changelog. Записи — на смысловой PR/задачу.
[Unreleased]
Added
- Merge-в-
main+ пост-деплой верификация как обязательное условиеdone(фикс «фантомного merge») (ORCH-071): задача могла дойти доdone, хотя ветка фактически НЕ влита вmain(«фантомный merge») — конвейер рапортовал успех без реального состояния репозитория. Введён под-гейт ребраdeploy → done: единственная точка переходаadvance_stageтеперь гейтится_handle_merge_verify(src/stage_engine.py), который покрывает ВСЕ пути финализации (finalizer Phase C, reconciler F-1, job-reaper). Добавлены детерминированный merge-актор и пост-деплой верификатор (src/merge_gate.py): merge выполняется ТОЛЬКО через PR-merge API (без push/force-push, INV-4) в restart-surviving Phase C, верификация подтверждает фактическое слияние вmainпрежде чем разрешить переход вdone. Раскат условный и снабжён kill-switch (src/config.py,src/main.py, по образцу условности ORCH-35/43/58), never-raise контракты соблюдены. Документация: глобальныйdocs/architecture/adr/adr-0013-merge-verify-gate.md, детальныйdocs/work-items/ORCH-071/06-adr/ADR-001-merge-verify-gate.md(D1–D9), раздел вdocs/architecture/README.md, runbook постмортемаdocs/operations/PHANTOM_MERGE_RUNBOOK.md(4 проверки + критерий «фантом подтверждён» + remediation). Тесты:tests/test_merge_actor.py,tests/test_merge_verify.py,tests/test_deploy_finalizer_merge_gate.py,tests/test_deploy_restart_merge_recovery.py,tests/test_qg_checks.py,tests/test_stages.py. - Security-гейт: secret-scanning (gitleaks) + dependency audit (pip-audit) перед мержем (ORCH-022): автономный конвейер вливал ветку в
mainбез проверки на утёкший секрет (ключ/токен/пароль/приватный ключ) и уязвимую зависимость (известный CVE) — для self-hostingorchestratorэто особенно остро: один общий прод-инстанс обслуживает все проекты из общей БД, поэтому секрет/CVE, проскочивший через одну задачу, уезжает в прод всех проектов (CLAUDE.md §self-hosting, §8). ORCH-022 вводит детерминированный (без LLM) security-гейт как под-гейт ребраdeploy-staging → deploy, исполняемый ПЕРВЫМ среди edge-под-гейтов (ДО merge-gate ORCH-043 и image-freshness ORCH-058) — дёшево фейлить до дорогих rebase/rebuild, а скан ветки ДО rebase не «обвиняет» задачу в CVE из обновившегосяmain. Паттерн соседей: новый leaf-модульsrc/security_gate.py(контракт «never-raise», по образцуmerge_gate/image_freshness/staging_verdict) + тонкая обёрткаcheck_security_gateв реестреQG_CHECKS(src/qg/checks.py, lazy-import → нет цикла) + врезка_handle_security_gateвsrc/stage_engine.pyв блокcurrent_stage == "deploy-staging"ПЕРВОЙ.STAGE_TRANSITIONSи схема БД — без изменений. Secret-scanning (gitleaks, offline): скан диапазонаorigin/main..HEAD(ровно коммиты задачи); любой секрет вне аллоулиста версионируемого.gitleaks.toml→ вклад в FAIL. Полностью оффлайн (локальные правила) → гарантия «секрет всегда блокирует» (BR-2) безусловна, не зависит от сети; fail-closed при ошибке инструмента/отсутствии бинаря/таймауте (нельзя доказать «секретов нет» → FAIL). Контракт exit-кодов: 0=чисто, 1=найдено, ≥2=ошибка. Dependency audit (pip-audit, OSV/PyPI): аудитrequirements.txt; severity ≥security_dep_block_severity(дефолтHIGH, порядок CRITICAL>HIGH>MEDIUM>LOW) → вклад в FAIL (deps_blocking); ниже порога / UNKNOWN → warning (deps_warning, анти-петля Р-4, не авто-блок). Источник advisory требует сети → недоступность фида fail-open + громкий warning по умолчанию (deps_audit_degraded: true+ Telegram + лог; прецедент анти-петли ORCH-061), флагsecurity_dep_audit_fail_closedпереводит в строгий режим без редеплоя кода. Артефакт17-security-report.md(YAML-frontmattersecurity_status/secrets_found/deps_blocking/deps_warning/deps_audit_degraded+ тело-списки находок); машинный вердикт читается ТОЛЬКО из frontmatter (гейт пишет → читает обратно черезparse_security_status→ возвращает ровно то, что записал: единый источник истины, AC-8), negative-токен (FAIL) авторитетен, нет frontmatter/битый YAML/нет поля → fail-closed на чтении; значения секретов в артефакте маскируются (не ре-лик). FAIL → откат наdevelopment+ developer-retry (общий_developer_retry_count, capMAX_DEVELOPER_RETRIES=3, затемset_issue_blocked+ Telegram, без бесконечного баунса);task_descперезапущенного developer'а несёт дословные находки (extract_security_findings, паттерн ORCH-046) + ссылку на артефакт. Self-hosting safety: гейт только читает/сканирует/пишет артефакт — не вызывает деплой-хук, не рестартит прод-контейнер (под-гейт исполняется ДО захвата merge-lease → при FAIL lease освобождать не нужно). Условность как ORCH-35/43/58:security_gate_enabled(kill-switch) +security_gate_repos(CSV; пусто → только self-hostingorchestrator); таймаутsecurity_scan_timeout_s; never-raise. v1 — Python-only стек; SAST/мульти-стек — follow-up (BR-14). Инфраструктура: pinnedgitleaks(статический Go-бинарь) вDockerfile(+curl/ca-certificates),pip-audit(pinned) вrequirements.txt,.gitleaks.tomlв корне репо. Новые настройки:ORCH_SECURITY_GATE_ENABLED(true),ORCH_SECURITY_GATE_REPOS(""),ORCH_SECURITY_DEP_BLOCK_SEVERITY(HIGH),ORCH_SECURITY_SCAN_TIMEOUT_S(300),ORCH_SECURITY_DEP_AUDIT_FAIL_CLOSED(false),ORCH_SECURITY_SECRETS_BLOCK(true). Инварианты НЕ менялись:STAGE_TRANSITIONS(9 стадий),check_branch_mergeable/check_staging_image_freshи их под-гейты, БАГ-8 откат, terminal-sync, схема БД (без миграций). ADRdocs/work-items/ORCH-022/06-adr/ADR-001-security-gate.md, глобальныйdocs/architecture/adr/adr-0012-security-gate.md. Документация:docs/architecture/README.md,CLAUDE.md,.env.example. Тесты:tests/test_security_gate.py,tests/test_qg_security.py,tests/test_stage_engine_security_gate.py,tests/test_qg_registry_snapshot.py,tests/test_config.py. - Выделенный статус-триггер прод-деплоя «Confirm Deploy» (ORCH-059): жест запуска прод-деплоя отделён от человеческого гейта одобрения. Раньше один Plane-статус
Approvedбыл перегружен: наanalysisон работал как человеческий гейт BRD (check_analysis_approved), а наdeploy— молча триггерил Фазу B прод-деплоя ORCH-036 (advance_stage(deploy, finished_agent=None) → _handle_self_deploy_phase_b → detached host-рестарт прод-контейнера 8500). Привычный жест approve = групповой self-hosting риск (прод обслуживает ВСЕ проекты из одного инстанса). ORCH-059 вводит отдельный логический статусconfirm_deploy(«Confirm Deploy»), который триггерит ТОЛЬКО Фазу B наdeploy;Approvedостаётся исключительно гейтом конвейера. Четыре точечные правки в трёх модулях: (1)src/plane_sync.py— маппинг"Confirm Deploy" → "confirm_deploy"в_PLANE_NAME_TO_KEY; ключ намеренно НЕ добавлен в_DEFAULT_STATES(нет UUID для enduro/fallback) → fail-closed: для проекта ORCH резолвится из живого Plane API (get_project_states(orch)["confirm_deploy"]→ реальный UUID), для сред без статуса (enduro / недоступный API / доска без статуса) ключ просто отсутствует, доступ через.get("confirm_deploy")→None, безKeyError. (2)src/webhooks/plane.py—handle_issue_updatedДО веткиapprovedдобавляет fail-closed-веткуconfirm_state = proj_states.get("confirm_deploy"); if confirm_state and new_state == confirm_state: handle_confirm_deploy(...); новыйhandle_confirm_deployрезолвит задачу, гардstage == "deploy"(иначе no-op с логом — защищает прочие гейты от случайного триггера), иначе →_try_advance_stage(..., confirm_deploy=True).handle_verdict(approved=True)не изменён (продолжает звать_try_advance_stageс дефолтнымconfirm_deploy=False). (3)src/stage_engine.py—advance_stageполучил keyword-only параметрconfirm_deploy: bool = False(обратносовместимо: все существующие вызовы из launcher/reconciler/finalizer передаютfinished_agent); блок Фазы B теперь всегда возвращается рано дляdeploy + finished_agent is Noneself-hosting, но_handle_self_deploy_phase_bвызывается ТОЛЬКО приconfirm_deploy=True, иначе (обычныйApproved) — детерминированный no-op (result.note = "approved-on-deploy-noop"): возврат ДО блока Quality Gate →check_deploy_statusне запускается → нет ложного отката БАГ-8 (вердикта ещё нет, R-2). (4) CTA Фазы A (_handle_self_deploy_phase_a) — Plane-коммент и Telegram просят перевести задачу в статус «Confirm Deploy» (а не «Approved»). Следствие для reconciler F-1 наdeploy(ORCH-053): попадает в no-op-ветку вместо неявного запуска Фазы B → прод-деплой нельзя инициировать автоматически, только явным человеческим «Confirm Deploy» (усиление safety). Условность как ORCH-35/36 (реально только дляself_deploy.self_deploy_applies("orchestrator"); прочие репо — прежний синхронный ssh-деплой агентом, статус не нужен и не влияет). Контракты НЕ менялись:STAGE_TRANSITIONS, реестрQG_CHECKS,check_deploy_status/_parse_deploy_status, exit-код-контракт хука (0/1/2), Фазы A/C, merge-gate, terminal-sync, схема БД (статусы — на стороне Plane; restart-safe состояние деплоя — существующие sentinel-файлы ORCH-036). Эксплуатационное предусловие: в Plane-проекте ORCH создать статус доски «Confirm Deploy» (точное имя, регистр) + сброс кэша состояний —docs/work-items/ORCH-059/07-infra-requirements.md. До создания статуса прод-деплой через approve не запустится (желаемое fail-closed-поведение). ADRdocs/work-items/ORCH-059/06-adr/ADR-001-confirm-deploy-status.md(уточняет триггер Фазы B относительно adr-0007). Тесты:tests/test_plane_states.py,tests/test_plane_confirm_deploy.py,tests/test_stage_engine_phase_b.py,tests/test_stage_engine_phase_a_cta.py,tests/test_confirm_deploy_integration.py,tests/test_deploy_approve.py(обновлён под новый триггер). - Осмысленная статусная модель Plane (слой B — индикация) (ORCH-066): Plane больше не показывает наблюдателю огрублённую/вводящую в заблуждение картину — статусы доски приведены к смыслу стадий конвейера, при этом статус остаётся индикацией, а не управлением. Архитектурный инвариант (ADR-001): меняется ТОЛЬКО слой B (отображение в Plane —
src/plane_sync.pyи точки выставления статуса вstage_engine.py/webhooks/plane.py/reconciler.py), слой A (машина стадийsrc/stages.py::STAGE_TRANSITIONS) остаётся байт-в-байт неизменным (AC-21, регресс-тест TC-22 сверяет полный литерал словаря). Целевая модель: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) в_DEFAULT_STATES/_PLANE_NAME_TO_KEYплюсSTAGE_VISIBILITY_STATE(analysis→analysis,review→code_review) и_STAGE_TO_STATE_KEY; новые сеттерыset_issue_analysis/code_review/awaiting_deploy/deploying/monitoring+ диспетчерset_issue_stage_state. Project-relative alias-fallback (BR-12): если оператор ещё не создал новый статус в конкретном Plane-проекте, ключ деградирует на базовый UUID ТОГО ЖЕ проекта (_STATE_ALIAS_FALLBACK:analysis→in_progress,code_review→review,awaiting_deploy→in_review,deploying→in_progress,monitoring→done,to_analyse→in_progress), поэтому PATCH остаётся валидным на частичных конфигах, а enduro-trails схлопывает новые ключи на старые базовые статусы → нулевая регрессия. Самодеплой (ORCH-036) теперь индицирует фазы: Phase A →Awaiting Deploy(ожидание ручного approve), Phase B →Deploying, terminal-syncdeploy→doneветвится — для self-hosting (post_deploy.post_deploy_applies(repo)) issue входит в окноMonitoring after Deploy(НЕ терминальный Done), для прочих репо — прежний терминальныйDone(нулевая регрессия, TC-08/TC-09). Post-deploy монитор (ORCH-021) на закрытии окна: HEALTHY →set_issue_done, DEGRADED →set_issue_blocked(только индикация; self-hosting остаётся ALERT_ONLY, прод НИКОГДА не рестартится/не откатывается — BR-5, TC-10/11/12). Reconciler: F-2 триггер старта/резюма расширен наTo Analyse(TC-20), Guard 2_is_blocked_or_needs_inputучитывает новые активные ожидания (awaiting_deploy/deploying/monitoring) с вычитанием базовых рабочих статусов, чтобы алиасинг на частичных проектах не расширял skip-set (анти-регресс, TC-21). Контракт never-raise на всех сеттерах и резолвере состояний сохранён (API Plane недоступен → identity-фоллбэк, сеттеры не бросают — TC-16/17/18). Раскатка управляется оператором (создание 6 статусов в Plane), отдельного kill-switch не вводится — на «голом» Plane всё деградирует на прежнее поведение. Инварианты НЕ менялись (TC-22/TC-23):STAGE_TRANSITIONS(9 стадий), реестрQG_CHECKS(12 чеков), сигнатураcheck_deploy_status(repo, work_item_id, branch), exit-код-контракт хука, merge-gate, схема БД (без миграций). ADRdocs/work-items/ORCH-066/06-adr/ADR-001-plane-status-model.md. Тесты:tests/test_plane_status_model.py,tests/test_plane_to_analyse_resume.py,tests/test_plane_status_failclosed.py,tests/test_plane_webhook.py(TC-15),tests/test_deploy_terminal_sync.py(TC-08/09),tests/test_post_deploy_integration.py(TC-10/11/12),tests/test_orch10_states.py(TC-19),tests/test_reconciler.py(TC-21),tests/test_reconciler_plane.py(TC-20). - Job-reaper + проактивный реклейм протухшего merge-lease + идемпотентная финализация merge (ORCH-065): закрыт класс инцидентов «zombie jobs» — статус job выставлялся ТОЛЬКО в живом процессе launcher'а, поэтому гибель процесса (OOM/рестарт инстанса/segfault Claude-CLI) оставляла строку
jobs.status='running'навсегда; приmax_concurrency=1один такой зомби намертво блокировал очередь ВСЕХ проектов (self-hosting: enduro-trails встаёт из-за зомби ORCH-задачи). Плюс два смежных дефекта: застрявший merge-lease (.merge-lease-<repo>.jsonреклеймился лишь лениво по TTL при чужом acquire, живость pid-holder'а не проверялась) и неидемпотентная финализация merge (rebase+re-test зелёные, но процесс умер до самого merge → нет повторного проигрывания). Решение — новый фоновый daemon-потокsrc/job_reaper.py(контракт «never-raise на единицу работы», паттернreconciler/queue_worker): периодический тик (reaper_interval_s) сканируетrunning-jobs трёхуровневой проверкой живости (ADR Р-1): Tier-1 мёртвый pid (os.kill(pid, 0)→ProcessLookupError) с анти-false-positive порогомreaper_dead_ticksподряд-мёртвых тиков (стрик в памяти); Tier-2agent_runs.exit_codeзаписан, но job всё ещёrunning— но только после finalization-gracereaper_finalize_grace_s(окно неоднозначно: живой monitor пишет exit_code ПЕРВЫМ, затем git push/PR/Plane-комментарии и лишь потом_finalize_job, а pid агента к этому моменту мёртв в обоих случаях — живой финализирующий monitor НЕ реапится); Tier-3 backstop-потолокreaper_max_running_s. Единственная мутирующая запись reaper'а — атомарный терминальный флип черезdb.reap_running_job(... WHERE status='running')(rowcount==1 у победителя, проигравший в гонке сrequeue_running_jobs/launcher видит rowcount==0 — без двойной обработки, TC-06). Для Tier-2 exit0 действие построено по принципу claim-before-act (ADR Р-1): источник истины — канонический QG (не «exit0»), он оценивается read-only (_gate_is_green→stage_engine._run_qg, как у reconciler) ПЕРЕД claim, затем атомарный claimdoneПЕРВЫМ и только победитель claim делает gate-driven advance (_gate_driven_advance→ штатныйlauncher._try_advance_stage, кандидат-стадии агента изSTAGE_TRANSITIONS) — проигравший claim не выполняет НИКАКИХ побочных эффектов (нет дубль-advance / дубль-enqueue следующей стадии); зелёный гейт →done+advance, красный → путь неуспеха (requeue в пределахattempts<max, иначеfailed+Telegram). Реклейм lease:merge_gate.pid_alive(pid)(мёртвый pid ⇔ProcessLookupError;PermissionError/прочее → консервативно жив),reclaim_stale_lease(repo)освобождает lease на (мёртвый pid ИЛИ истёкший TTLmerge_lock_timeout_s), живой в пределах TTL не трогается; реклейм — ТОЛЬКО удаление файла-lease (release_merge_lease), без каких-либо git-операций;reclaim_all_stale_leases()обходит область merge-gate per-repo (вызывается в каждом тике reaper'а И на старте вmain.lifespan). Идемпотентность финализации:merge_gate.pr_already_merged(repo, branch)(lazyhttpx, Gitea APIpulls?state=all&head=..., любая ошибка/не-200 →False, never-raise) — guard перед повторным merge при re-drive. Точка консультации — сам merge-актор: фактический merge feature-PR вmainделает агент deployer (в начале стадииdeploy, см.webhooks/gitea.py), поэтому wiring живёт в его промпте.openclaw/agents/deployer.md— он вызывает ровноpr_already_mergedПЕРЕД любым (повторным) merge и при уже слитом PR делает no-op (без второго merge и без ошибки, AC-11). Merge-gate-чекcheck_branch_mergeableнамеренно НЕ трогается (AC-13): он исполняется на ПЕРВОМ ребреdeploy-staging → deployи не переисполняется при re-drive самой стадииdeploy— где и живёт риск второго merge. Дополнительно re-drive после восстановления короткозамыкается на «branch up-to-date with main» БЕЗ повторного дорогого rebase+re-test, если ветка уже догнана (TC-17). Новая колонкаjobs.pidчерез идемпотентный_ensure_column(без миграции, безопасно на живой прод-БД); pid агента проставляется вsrc/agents/launcher.py::_spawnПОСЛЕPopen(рядом сrun_id/started_at). Старт/стоп reaper'а вsrc/main.pylifespan (послеreconciler.start()/ передreconciler.stop()), снимокreaperвGET /queue(enabled/interval/last_run/reaped_total/last_reaped/lease_reclaimed_total). Условность раскатки реклейма lease зеркалит merge-gate (merge_gate_reposCSV или self-hostingorchestrator). Новые настройки:ORCH_REAPER_ENABLED(true, kill-switch),ORCH_REAPER_INTERVAL_S(60),ORCH_REAPER_DEAD_TICKS(2),ORCH_REAPER_MAX_RUNNING_S(3600),ORCH_REAPER_FINALIZE_GRACE_S(300),ORCH_LEASE_RECLAIM_ENABLED(true). Инварианты НЕ менялись (AC-13):STAGE_TRANSITIONS(9 стадий) и реестрQG_CHECKS(12 чеков) без новых элементов, сигнатура/поведениеcheck_branch_mergeable(repo, work_item_id, branch)intact, БАГ-8 откат и exit-код-контракт хука не тронуты; restart-safe, never-raise на единицу фоновой работы. ADRdocs/work-items/ORCH-065/06-adr/ADR-001-job-reaper-and-lease-reclaim.md, глобальныйdocs/architecture/adr/adr-0011-job-reaper-lease-reclaim.md. Тесты:tests/test_job_reaper.py,tests/test_merge_lease_reclaim.py,tests/test_merge_gate.py(TC-16),tests/test_merge_gate_race.py(TC-17),tests/test_queue.py::TestReaperUnblocksQueue,tests/test_config.py(TC-19/TC-20). - 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; деградация фиксируется детерминированными порогами, при подтверждении — реакция. Новый leaf-модульsrc/post_deploy.py(контракт «never raise», по образцуself_deploy.py/staging_verdict.py; импортирует только config + lazyqg.checks.is_self_hosting_repo):post_deploy_applies(условность раската),probe_signals(один опрос/health200+{"status":"ok"}+ доля 5xx на/status,/queue; сеть/таймаут → консервативный провал, не исключение),classify(чистая, главный предмет юнит-тестов:DEGRADED⇔≥ post_deploy_fail_thresholdПОСЛЕДОВАТЕЛЬНЫХ провалов health ИЛИ доля 5xx окна> post_deploy_5xx_threshold; иначеHEALTHY— одиночный глюк не откатывает),decide_action(self-hosting → ВСЕГДАALERT_ONLY; не-self +post_deploy_auto_rollback=true→ROLLBACK; иначеALERT_ONLY),map_rollback_exit_code(0→ROLLBACK_OK, иначеROLLBACK_FAILED), sentinel-state хелперы (armed/series/doneпод<repos_dir>/.post-deploy-state-<repo>/<wi>/, restart-safe счётчики),build_rollback_command/run_rollback(ssh-хук--rollbackс прод-env, синхронно — только для не-self),build/write_post_deploy_log(артефакт16-post-deploy-log.md),arm_monitor(идемпотентный арм + первый отложенный job),status(снимок для/queue). Механизм наблюдения — reserved-agent jobpost-deploy-monitor(детерминированный, no-LLM, калькаdeploy-finalizer, НЕ стадия и НЕ daemon): арм вstage_engine.advance_stageв блокеnext_stage == "done"ПОСЛЕ terminal-sync/release-lease (post_deploy.arm_monitor, sentinelarmed= идемпотентность при двойном webhook/reconciler/finalizer); один тик = один job — перехват вagents/launcher.launch_jobДО_spawn→stage_engine.run_post_deploy_monitor(один опрос → append вseries→classify→ перепостановка с задержкойavailable_at_delay_sИЛИ реакция+артефакт+mark_done); бюджет тиковwindow_s/interval_s(анти-livelock). Self-hosting safety (BR-5): дляorchestratorтик НИКОГДА не откатывает/рестартит прод-контейнер — реакция всегдаALERT_ONLY(громкий Telegram + Plane-коммент с запросом ручного approve); авто-rollback хуком--rollback— только для не-self репо приpost_deploy_auto_rollback=true(целевой контейнер ≠ orchestrator). Наблюдаемость — блокpost_deployвGET /queue(enabled/window/interval/активные наблюдения). Артефакт16-post-deploy-log.md(YAML-frontmatterpost_deploy_status/action_taken/window_s/checks_total/checks_failed) — машиночитаемо для петли уроков ORCH-8; best-effort. Новые настройки:ORCH_POST_DEPLOY_MONITOR_ENABLED(true, kill-switch),ORCH_POST_DEPLOY_REPOS(CSV; пусто → только self-hosting),ORCH_POST_DEPLOY_WINDOW_S(900),ORCH_POST_DEPLOY_INTERVAL_S(30),ORCH_POST_DEPLOY_FAIL_THRESHOLD(3),ORCH_POST_DEPLOY_5XX_THRESHOLD(0.5),ORCH_POST_DEPLOY_AUTO_ROLLBACK(false),ORCH_POST_DEPLOY_BASE_URL(http://localhost:8500); параметры отката переиспользуютdeploy_prod_*. Инварианты НЕ менялись:STAGE_TRANSITIONS, реестрQG_CHECKS,check_deploy_status/_parse_deploy_status, terminal-syncdeploy→done, merge-gate, exit-код-контракт хука (0/1/2), схема БД (без миграций; состояние — sentinel-файлы). Условность как ORCH-35/36/43/58. ADRdocs/work-items/ORCH-021/06-adr/ADR-001-post-deploy-monitor.md, глобальныйdocs/architecture/adr/adr-0010-post-deploy-monitor.md. Тесты:tests/test_post_deploy.py,tests/test_post_deploy_integration.py. - Провенанс staging-образа перед BUILD-ONCE retag в прод (свежесть артефакта, INV-FRESH) (ORCH-058): BUILD-ONCE retag (ORCH-036) промоутит staging-образ (
orchestrator-orchestrator-staging) в прод без rebuild, полагаясь на «образ свеж и провалидирован» — гарантии не было: конвейер нигде не пересобирал staging-образ из провалидированного коммита, поэтому retag мог тихо промоутнуть УСТАРЕВШИЙ образ (инцидент LESSONS_ORCH-036 п.4 — зелёный деплой молча откатывал прод). Закрыто двумя слоями (defense in depth), только для self-hosting. Новый модульsrc/image_freshness.py(контракт «never raise», по образцуmerge_gate):provenance_verdict(чистая функция вердикта match/mismatch/fail-closed),validated_revision(git rev-parse HEADв worktree валидированного коммита — единый якорь и для штампа A, и дляEXPECTED_REVISIONB),image_revision(OCI-лейблorg.opencontainers.image.revisionчерезdocker image inspect,<no value>/ошибка → пусто),rebuild_staging_image(ssh-хук--build-staging),image_freshness_applies(условность),check_staging_image_fresh(композитный QG). Strategy A (liveness): новый детерминированный QG-под-чекcheck_staging_image_fresh(зарегистрирован вQG_CHECKS,src/qg/checks.py) на ребреdeploy-staging → deployПОСЛЕ merge-gate и ДО Phase A — пересобирает staging-образ из worktree валидированного коммита (хук--build-staging,--build-arg GIT_SHA=<sha>), пересоздаёт 8501 и прогоняетstaging_check.py --mode stubпротив свежего 8501 (health + e2e, внутри staging-контейнера черезdocker exec— канон ORCH-048) → валидируем РОВНО тот артефакт (build + e2e), что промоутится в прод (AC-4); FAIL/не-ноль staging_check → откат наdevelopment(как merge-gate, капMAX_DEVELOPER_RETRIES).rebuild_staging_imageпробрасывает в хук явный staging-таргет (service/port/profile/container), исключая дрейф на прод 8500. Сборки/recreate/validate — только staging (8501), прод (8500) не трогается. Strategy B (safety):DockerfileштампуетLABEL org.opencontainers.image.revision=$GIT_SHA(ARG GIT_SHA);build_deploy_command(src/self_deploy.py) пробрасываетEXPECTED_REVISION; хост-хук шагом 2b ПЕРЕДdocker tagfail-closed сверяет лейблrevisionуSOURCE_IMAGEсEXPECTED_REVISION— несовпадение / пустой лейбл / ошибка inspect →exit 1(FAILED → БАГ-8 откат), делает тихий промоут устаревшего образа структурно невозможным даже при проигравшей гонку/отключённой A. Хост-хукscripts/orchestrator-deploy-hook.shрасширен обратно-совместимым режимом--build-staging(пересборка+recreate staging, exit 0/1) и fail-closed guard'ом (активен только при заданномEXPECTED_REVISION). Единый kill-switchORCH_IMAGE_FRESHNESS_ENABLED(true) включает A+B как целое (нет «B без A» = вечного fail-fast); область —ORCH_IMAGE_FRESHNESS_REPOS(CSV; пусто → только self-hostingorchestrator). Контракты НЕ менялись:STAGE_TRANSITIONS(под-гейт ребра, не стадия), exit-code-контракт хука (0/1/2),map_exit_code_to_status,check_deploy_status/_parse_deploy_status, БАГ-8, terminal-sync, merge-gate; схема БД — без миграций. ADRdocs/work-items/ORCH-058/06-adr/ADR-001-staging-image-provenance.md, глобальныйdocs/architecture/adr/adr-0008-staging-image-provenance.md. Документация:docs/architecture/README.md,docs/operations/DEPLOY_HOOK.md,docs/operations/STAGING.md,docs/operations/INFRA.md,.env.example. Тесты:tests/test_image_freshness.py,tests/test_deploy_hook_provenance.py,tests/test_deploy_build_once.py(TC-06),tests/test_deploy_hook_mapping.py(TC-09),tests/test_stage_engine.py::TestImageFreshnessGate,tests/test_qg_registry_snapshot.py,tests/test_config.py. - Исполняемый самодеплой стадии
deploy(стадия дёргает хост-хук, manual-approve) (ORCH-036): стадияdeployперестаёт быть «бумажной» — для self-hosting репозиторияorchestratordeploy_status: SUCCESSозначает ДОКАЗАННЫЙ health-ok реального рестарта прод-контейнера (8500), а не декларацию LLM. Критический путь self-restart детерминирован (без LLM), по образцу merge-gate ORCH-043, и разбит на три фазы (src/stage_engine.py+ новый модульsrc/self_deploy.py): Фаза A (вход вdeploy) — вместо запуска прод-deployer'а приdeploy_require_manual_approve=trueзадача переводится в approval-pending (set_issue_in_review) и ждёт ручного approve; restart-safe маркерapprove-requested. Фаза B (человек ставит статус Plane →Approved;advance_stage(deploy, finished_agent=None)) — запускается detached host-процесс (ssh + setsid→scripts/orchestrator-deploy-hook.sh, чтобы рестарт 8500 пережил гибель контейнера; орк НЕ убивает себя из docker.sock) с build-once retag staging-образа (SOURCE_IMAGE), ставится детерминированный finalizer-job; маркерinitiated— идемпотентность повторного Approved. Фаза C (run_deploy_finalizer, reserved-agentdeploy-finalizer, claim'ится новым контейнером после рестарта) — читает sentinelresult(exit-code хука, записан host-обёрткой),not-ready→ defer (бюджетdeploy_finalize_max_attempts, restart-safe поtask_content), маппит0→SUCCESS / 1|2|иное→FAILED(чистая функцияmap_exit_code_to_status, unit-тест), пишет14-deploy-log.mdи вызываетadvance_stage(deploy, finished_agent="deployer")→ существующие контракты:SUCCESS → done+ release merge-lease,FAILED → откат БАГ-8 на development+set_issue_blocked. Уведомления Plane+Telegram на approve-request / initiate / success / rollback (BR-5, ни одного «молчаливого» деплоя). Хост-хукscripts/orchestrator-deploy-hook.shрасширен обратно-совместимымSOURCE_IMAGE: при заданном —docker tag $SOURCE_IMAGE $TARGET_IMAGEпередup -d --no-build(деплой РОВНО протестированного образа, безdocker build); не задан → прежнее поведение; exit-code-контракт (0/1/2) и health-loop (10×6с, авто-rollback) не тронуты. Restart-safe состояние — sentinel-файлы (<repos_dir>/.deploy-state-<repo>/<work_item_id>/), без миграции БД. Условность как ORCH-35: реальный самодеплой только дляis_self_hosting_repo("orchestrator"); прочие репо (enduro-trails) — прежний синхронный ssh-путь агентом. Контракты НЕ менялись:STAGE_TRANSITIONS, реестрQG_CHECKS,check_deploy_status/_parse_deploy_status(frontmatter-only), terminal-syncdeploy→done, merge-gate (ORCH-43), БАГ-8. ФлагDEPLOY_REQUIRE_MANUAL_APPROVEостаётсяtrue(полный авто — отдельная задача ORCH-54). Новые настройки:ORCH_DEPLOY_REQUIRE_MANUAL_APPROVE(true),ORCH_DEPLOY_SSH_USER,ORCH_DEPLOY_SSH_HOST,ORCH_DEPLOY_HOOK_SCRIPT,ORCH_DEPLOY_PROD_SOURCE_IMAGE,ORCH_DEPLOY_PROD_TARGET_SERVICE/PORT/IMAGE,ORCH_DEPLOY_FINALIZE_DELAY_S,ORCH_DEPLOY_FINALIZE_MAX_ATTEMPTS. ADRdocs/work-items/ORCH-036/06-adr/ADR-001-executable-self-deploy.md, глобальныйdocs/architecture/adr/adr-0007-executable-self-deploy.md. Документация:.openclaw/agents/deployer.md(стадияdeploy= вызов хука, запрет self-restart),docs/operations/INFRA.md,docs/operations/DEPLOY_HOOK.md. Тесты:tests/test_deploy_hook_mapping.py,tests/test_deploy_approve.py,tests/test_deploy_routing.py,tests/test_deploy_rollback.py,tests/test_deploy_notifications.py,tests/test_deploy_build_once.py,tests/test_deploy_terminal_sync.py,tests/test_staging_precondition.py,tests/test_deploy_hook_rollback_sim.py. - Sweeper потерянных webhook (реконсиляция застрявших стадий) (ORCH-053): фоновый daemon-поток
src/reconciler.py(паттернqueue_worker), который устраняет тихое застревание задач, когда конвейер не двигается из-за потерянного события (502 на ребилде инстанса, отсутствие ретраев у Plane/Gitea, неразрезолвленныйsha→branch— класс инцидента ORCH-044). Реконсилятор периодически (reconcile_interval_s) доигрывает пропущенный переход через те же штатные гейты/обработчики, что и webhook, не дублируя логику конвейера: F-1 gate-side (reconcile_gate_once) — для задачstage≠done, без активного job иage(updated_at) ≥ grace_for_stage(stage)делает read-only пред-оценку канонического QG стадии; зелёный → продвижение строго через неизменныйstage_engine.advance_stage(..., finished_agent=None); красный → тишина (спам нотификаций структурно невозможен —advance_stageна красном гейте не вызывается вовсе);analysisF-1 не трогает (человеческий гейт). F-2 plane-side (reconcile_plane_once) — опрос Plane API per-project (новыйplane_sync.list_issues_by_state, курсорная пагинация, never-raise) и реплей In Progress / Approved / Rejected через существующиеwebhooks.plane.handle_status_start/handle_verdict(async-обработчики вызываются из sync-потока черезasyncio.run). F-3 — усилениеsha→branchвhandle_ci_status: при неразрезолвленном sha — БД-fallback по единственной development-задаче repo (db.get_development_tasks_by_repo; неоднозначность → не резолвим, ложного матча нет),logger.debug→logger.infoдля видимости потерянного CI-события. Анти-дубль на создании задачи (db.create_task_atomicпод process-widethreading.Lock: SELECT-exists→INSERT, проигравший в гонке reconcile↔webhook не плодит второй task/branch/worktree/стартовый analyst-job). Старт/стоп вmain.lifespan(послеworker.start()/ передworker.stop()), restart-safe, never-raise на единицу работы. Наблюдаемость (F-4): при разблокировке — лог-строкаreconciler: <wi> <stage> разблокирована (потерян webhook)+ Telegram (reconcile_notify_unblock) и блокreconcileвGET /queue. Kill-switches:ORCH_RECONCILE_ENABLED(глобально),ORCH_RECONCILE_PLANE_ENABLED(гасит только F-2),ORCH_RECONCILE_INTERVAL_S(120),ORCH_RECONCILE_GRACE_DEFAULT_S(600),ORCH_RECONCILE_GRACE_OVERRIDES_JSON(per-stage),ORCH_RECONCILE_NOTIFY_UNBLOCK(true). Схема БД и реестры (STAGE_TRANSITIONS/QG_CHECKS) НЕ менялись. ADRdocs/work-items/ORCH-053/06-adr/ADR-001-stuck-task-reconciler.md, глобальныйdocs/architecture/adr/adr-0007-reconciler.md. Тесты:tests/test_reconciler.py,tests/test_reconciler_plane.py,tests/test_gitea_sha_resolve.py,tests/test_config.py. - Merge-gate: авто-rebase на текущий
origin/main+ повторный прогон тестов + сериализация мержей (ORCH-043): детерминированный (без LLM) суб-гейт на ребреdeploy-staging → deploy, выполняемый ПЕРЕД мержем PR деплоером. Закрывает класс гонок «две зелёные ветки в одном репо ломаютmain»: пайплайн валидирует ветку против тогоmain, от которого она ответвилась, а не противmainв момент мержа — между «ветка зелёная» и «ветка смержена» параллельная задача может сдвинутьmain(семантический конфликт: git мержит без текстового конфликта, но совмещённыйmainкрасный). Для self-hosting репозиторияorchestratorэто означало бы красныйmainинструмента, обслуживающего ВСЕ проекты. Новый модульsrc/merge_gate.py(контракт «never raise», все git-операции — в per-branch worktree, ORCH-2/S-4):branch_is_behind_main(git merge-base --is-ancestor origin/main HEAD),auto_rebase_onto_main(rebase +git push --force-with-leaseТОЛЬКО ветки задачи —mainНИКОГДА не пушится; текстовый конфликт →rebase --abort+ чистый worktree),retest_branch(python -m pytest <target>в догнанном worktree, бюджетmerge_retest_timeout_s), файловый merge-lease (acquire_merge_lease/release_merge_lease, атомарныйO_CREAT|O_EXCL, holder-aware release, реклейм протухшего/битого лиза — без изменения схемы БД). Новый quality-gatecheck_branch_mergeable(src/qg/checks.py, зарегистрирован вQG_CHECKS) композирует примитивы под лизом: kill-switch/вне-области → no-op pass; lock занят →(False, "merge-lock busy")(сигнал DEFER, не код-фолт); ветка свежая → pass (лиз ДЕРЖИТСЯ до мержа); отстала → rebase → конфликт = fail+release, чисто → retest → зелёный = pass (лиз держится) / красный|timeout = fail+release. Интеграция вsrc/stage_engine.py(суб-гейт наdeploy-staging, БЕЗ новой стадии вSTAGE_TRANSITIONS): pass → advance наdeploy; «merge-lock busy» → DEFER (повторная постановка деплоера наdeploy-stagingс задержкойavailable_at, анти-дедлок приmax_concurrency=1, restart-safe счётчик поtask_content, лимитmerge_defer_max_attempts→ block+Telegram); конфликт/красный retest → ROLLBACK наdevelopment+ ретрай developer-а (капMAX_DEVELOPER_RETRIES, без бесконечного баунса). Лиз освобождается наdeploy→done, на rollback и по webhook смерженного PR (src/webhooks/gitea.py). Новый параметрenqueue_job(..., available_at_delay_s=...)(src/db.py) — отложенная постановка без изменения схемы. Условность раскатки (зеркало ORCH-35):merge_gate_repos(CSV) или по умолчанию только self-hostingorchestrator; глобальный kill-switchmerge_gate_enabled. Новые настройкиORCH_MERGE_GATE_ENABLED(true),ORCH_MERGE_GATE_REPOS(""),ORCH_MERGE_RETEST_TIMEOUT_S(600),ORCH_MERGE_RETEST_TARGET(tests/),ORCH_MERGE_LOCK_TIMEOUT_S(300),ORCH_MERGE_DEFER_DELAY_S(60),ORCH_MERGE_DEFER_MAX_ATTEMPTS(5). ADRdocs/work-items/ORCH-043/06-adr/ADR-001-merge-gate.md, глобальныйdocs/architecture/adr/adr-0006-merge-gate.md. Тесты:tests/test_merge_gate.py,tests/test_qg_merge_gate.py,tests/test_merge_gate_race.py,tests/test_stage_engine.py::TestMergeGate,tests/test_config.py. - Режим
bumplive-трекера Telegram (ORCH-042): новыйORCH_TRACKER_MODE(Settings.tracker_mode, дефолтedit) выбирает поведение карточки задачи.edit(как было) — карточка редактируется на месте (editMessageText).bump— на каждом обновлении старое сообщение удаляется и карточка отправляется заново вниз чата (best-effortdelete_telegram(старый_id)→send_telegram(text, disable_notification=True)→set_tracker_message_id(new_id)), чтобы актуальный статус всегда был последним в чате при активной переписке. Инвариант «одна карточка на задачу» сохранён в обоих режимах: за один вызовupdate_task_trackerшлётся ≤1 нового сообщения;set_tracker_message_idвызывается ТОЛЬКО при успешном send (транзиентныйNoneне затирает указатель); результат delete НЕ блокирует отправку новой карточки (delete-fail у сообщения >48ч → всё равно шлём новое). Резолюция режима вnotifications(case-insensitive, trim): всё, что ≠"bump"(включая пустое/мусор) →edit→ нулевая регрессия и оркестратор не падает на любом значении флага. Новый low-level helperdelete_telegram(message_id) -> bool(контракт «never raises», маркеры_DELETE_GONE_MARKERS):ok:trueили «уже нет / нельзя удалить» →True; неизвестныйok:false/5xx/исключение →False; нет кредов →Falseбез HTTP. Сигнатурыsend_telegram/edit_telegram/update_task_trackerи схема БД (tasks.tracker_message_id) не менялись. ADRdocs/work-items/ORCH-042/06-adr/ADR-001-tracker-bump-mode.md. Тесты:tests/test_tracker_bump.py,tests/test_config.py. - Дословный текст findings reviewer/tester встраивается в
task_descзаворота (ORCH-046): при откате наdevelopmentстрокаtask_desc(попадает в.task-dev.mddeveloper-агента) теперь несёт суть претензий, а не только ссылку на файл — устраняет «испорченный телефон», из-за которого агент шёл «читать файл», терял ключевые P0/P1 / причину FAIL и заворачивался снова, выжигаяMAX_DEVELOPER_RETRIESи токены. Новый defensive-модульsrc/review_parse.py(контракт «never raise», какsrc/frontmatter.py):extract_review_findings(path)— дословные пункты P0/P1 из секции## Findingsфайла12-review.md;extract_test_failures(path)— релевантный фрагмент тела13-test-report.md(приоритет## Вывод pytest→ FAIL-строки## Результаты→## Итог). Обе функции усекают результат доMAX_FINDINGS_CHARS/MAX_FAILURES_CHARS(≈2000) с маркером…(truncated). Две rollback-веткиsrc/stage_engine.py(reviewer REQUEST_CHANGES, testercheck_tests_passedFAIL) встраивают извлечённый текст и сохраняют ссылку на полный файл («Полный контекст»); при пустом/битом артефакте — graceful-фоллбэк на прежнюю ссылку-строку (никаких исключений вadvance_stage). Tester-ветка дополнительно всегда включаетreasonгейта. Последовательность отката,_developer_retry_count, поляAdvanceResultи реестрQG_CHECKSне менялись. ADRdocs/work-items/ORCH-046/06-adr/ADR-001-embed-findings-in-task-desc.md. Тесты:tests/test_review_parse.py,tests/test_stage_engine.py::TestRollbackTaskDescEmbedding. - Поллинг с ретраем в quality-gate
check_ci_green(ORCH-045): гейт CI превращён из single-shot в polling, чтобы устранить race condition — раньше один опрос combined commit-status сразу после пуша developer-а ловил транзиентныйpending(типично 1-3с, реальный кейс ORCH-017: опрос 17:58:54 → pending, CI дозеленел 17:58:55) и задача застревала насмерть без повторного опроса. Теперь:success→ пропуск сразу;failure/error→ провал сразу (терминально, ретрай бессмыслен);pending/unknown →time.sleepи повторный опрос доci_poll_max_attemptsраз; истечение попыток → явный(False, "CI still pending after <T>s")(тупик больше не молчаливый); 404 → как раньше; транзиентнаяhttpx.HTTPErrorна попытке логируется и ретраится в рамках бюджета. Параметры — новые настройкиORCH_CI_POLL_MAX_ATTEMPTS(12) иORCH_CI_POLL_INTERVAL_S(10) вsrc/config.py(~2 мин ожидания pending). Сигнатураcheck_ci_green(repo, branch)и реестрQG_CHECKSне менялись;check_tests_passedне затронут. ADRdocs/architecture/adr/adr-0004-ci-poll-retry.md. Тесты:tests/test_qg.py::TestCheckCIGreen. - Прямые ссылки на BRD и Plane-таску в Telegram-уведомлении об апруве (ORCH-017): пингующее сообщение
notify_approve_requestedтеперь встраивает две HTML-<a>-ссылки — наdocs/work-items/<WI>/01-brd.md(Gitea branch-view:gitea_public_url→gitea_url) и на issue в Plane ({web_base}/{workspace}/projects/{project_id}/issues/{plane_issue_id}/). Новая настройкаORCH_PLANE_WEB_URL(внешний браузерный web-URL Plane; фолбэк наplane_api_url). Loopback-guard: если итоговый Plane web-base указывает на localhost/127.0.0.1/0.0.0.0/::1 или пуст — Plane-ссылка опускается (не выпускаем битый localhost-URL). Graceful degradation: каждая ссылка строится независимо и опускается при нехватке данных, сообщение и призыв «Переведите задачу в статус Approved …» сохраняются всегда; ровно одно пингующее сообщение, разделяемаяsend_telegramне тронута. Динамические подписи экранируютсяhtml.escape,parse_mode=HTMLсохранён. ADRdocs/work-items/ORCH-017/06-adr/ADR-001-telegram-approve-links.md. Тесты:test_notify_approve_links.py,test_analysis_approve_flow_links.py. - Конфигурируемые модель LLM и режим работы (
--effort) агентов (ORCH-41): модель/effort каждого агента вынесены из хардкодаlauncher.pyв конфиг — глобально per-agent (ORCH_AGENT_MODEL_<AGENT>/ORCH_AGENT_EFFORT_<AGENT>, дефолтыORCH_AGENT_MODEL_DEFAULT=claude-opus-4-8,ORCH_AGENT_EFFORT_DEFAULT=high) и per-project (agent_models/agent_effortsвORCH_PROJECTS_JSON). Резолверыresolve_agent_model/resolve_agent_effort(приоритет project > per-agent env > default > пусто), валидация effort{low,medium,high,xhigh,max}, опц.ORCH_AGENT_FALLBACK_MODEL(--fallback-model). Хардкод"model":"opus"(architect/reviewer) удалён. Тесты:test_resolve_agent_model.py,test_resolve_agent_effort.py. - Единый status-коммент агентов в Plane (ORCH-016):
usage.build_status_comment(...)— один хелпер для ВСЕХ ролей (analyst..deployer). HTML-формат: header{icon} {Role} — {описание}, опциональная строкаVerdict/Status: …из YAML-frontmatter артефакта, строкаДлительность: 4m 12s(явныйduration_sот launcher, fallback изagent_runsдля аналитика),<b>Документы:</b><ul><li><a>…</a></li></ul>, тех-хвост<sub>tokens · cost</sub>. Утилитки:usage.fmt_duration,usage.get_agent_duration, новый модульsrc/frontmatter.py(defensive YAML reader). ADRdocs/work-items/ORCH-016/06-adr/ADR-001-unified-status-comment.md. - Документация по канону (ORCH-9):
CLAUDE.md(паспорт проекта), структураdocs/(architecture/+adr/,operations/,work-items/,history/),docs/operations/INFRA.md(RUNBOOK с инфра-изоляцией и self-hosting рисками). - ADR: adr-0001 (multi-repo registry), adr-0002 (job queue), adr-0003 (условный staging-гейт).
- Стадия
deploy-staging(ORCH-35): промежуточный гейт междуtestingиdeploy. QGcheck_staging_status(условный, только для self-hosting repo). PR #31. - Деплой-хук (ORCH-34):
scripts/orchestrator-deploy-hook.shс health-check и авто-rollback. PR #30. - Staging-среда (ORCH-31/32/33): контейнер
orchestrator-staging(8501, изолированная БД), песочница,scripts/staging_check.py. PR #28/#29. - Очередь задач (ORCH-1): таблица
jobs,queue_worker.py, atomic claim, max_concurrency, ретраи, restart-safe, эндпоинт/queue. - Реестр проектов (ORCH-6):
src/projects.py, фильтрация вебхуков по проекту.
Changed
- Русификация и косметика карточки live-трекера Telegram (ORCH-042, оба режима): метка
Подтверждение BRDвместо «Ревью БРД» (_BRD_LABEL); после прохождения approve-gate строка подтверждения BRD начинается с ✅ вместо ⏸️ (ветка ожидания человека сохраняет ⏸️/⏳); русские display-labels стадий в_TRACKER_STAGES(Анализ / Архитектура / Разработка / Код ревью / Тестирование / Внедрение) — применяются и в «✅ …», и в «🔄 … идёт»; финальная строка готовой задачи📦 Внедреновместоdeployed(_done_link). Меняются только отображаемые строки — ключи стадий и имена агентов не трогаются. Существующие ассертыtests/test_telegram_tracker.pyобновлены под русские метки. - Status-коммент агентов теперь HTML и единообразен (ORCH-016):
src/usage.usage_comment(...)помечен deprecated и стал тонкой обёрткой надbuild_status_comment;src/usage.artifact_links(...)теперь возвращает<li><a>…</a></li>HTML-фрагменты (раньше — markdown[label](url));stage_engine._build_analyst_ready_comment(...)— тонкая обёртка, аналитик идёт через ту же веткуbuild_status_comment(agent="analyst", ...). РеестрQG_CHECKSиSTAGE_TRANSITIONSНЕ изменялись. - Цепочка стадий:
... testing → deploy-staging → deploy → done(была безdeploy-staging).
Fixed
- Reconciler (F-2) больше не зацикливается на спаме «разблокирована» по синхронизированной done-задаче (ORCH-068): после мерджа новой статусной модели Plane (ORCH-066) sweeper потерянных webhook (ORCH-053) каждые ~120с слал в Telegram
reconciler: ET-002 done разблокирована (потерян webhook)для полностью синхронизированной задачи (БДstage=done, Planestate=Done) — livelock без advance/jobs/токенов, но 191+ сообщений за ночь (alert-fatigue, подрыв доверия к нотификациям). Два независимых складывающихся дефекта (defense in depth, ADR-001): D1 (выборка) — F-2 различал actionable-статусы по голому UUID, а после ORCH-066 терминальныйDoneперестал однозначно отличаться отapprovedпо UUID (UUID-алиасинг) и done-issue попадала в веткуapproved; терминалы нигде не исключались. Решение: исключение терминалов по группе состояния Plane (state.group ∈ {completed, cancelled}) — проектно-независимый, устойчивый к переименованиям дискриминатор; проверка per-issue (а не сужениемwanted-набора, т.к. при алиасинге терминал физически совпадает с actionable-UUID); fallback по логическим ключамdone/cancelled, когда группа недоступна.get_project_statesрасширен записью{uuid → group}из ТОГО ЖЕ/states/-запроса (без новой сетевой стоимости) + sibling-аксессорget_project_state_groups. D2 (нотификация) —_note_unblockвызывался безусловно сразу после_dispatch, не проверяя, изменил ли обработчик реально состояние;handle_verdict(approved)для уже-doneзадачи — no-op, но нотификация всё равно уходила (нарушение собственного docstring и инварианта silence-when-in-sync). Решение: сравнение стадии задачи до/после_dispatchна стороне reconciler (контрактыhandle_*НЕ тронуты) —_note_unblockтолько при подтверждённом state change; для in_progress-старта подтверждение = задача появилась. Плюс TR-3 — in-memory дедуп-guard{issue_id → last_unblocked_state}(страховка против любого будущего no-op-пути). Вторичный баг кэша (TR-4):_STATES_CACHEжил весь lifetime процесса → новый Plane-статус был невидим без рестарта («stale set → no pipeline action»); добавлен TTLORCH_PLANE_STATES_TTL_S(дефолт 300с;0→ прежний lifetime-кэш) — запись самозалечивается перезапросом/states/(примитив сброса — существующийreload_project_states()); при сбое перезапроса отдаётся stale-but-correct набор, а не enduro-дефолты. Форма возвратаget_project_statesнеизменна (AC-13).STAGE_TRANSITIONS,QG_CHECKS, схема БД, контрактыhandle_status_start/handle_verdict, F-1/F-3 — не тронуты; never-raise per-issue/-project сохранён; self-hosting — тик не рестартит прод. Наблюдаемость: счётчикиskipped_terminal_total/deduped_totalв блокеreconcileснимкаGET /queue. ADRdocs/work-items/ORCH-068/06-adr/ADR-001-reconciler-terminal-exclusion-and-cache-ttl.md. Тесты:tests/test_reconciler_plane.py(TC-01…TC-10),tests/test_plane_states_cache.py(TC-11/TC-12). - Staging-rebuild больше не падает на
COPY data/(worktree-контекст) (ORCH-021):check_staging_image_fresh(ORCH-058, Strategy A) пересобирает staging-образ с worktree задачи в качестве docker build context (docker build … "$BUILD_CONTEXT"). Свежий git-worktree содержит только трекаемые файлы, аDockerfileделалCOPY data/ ./data/— ноdata/(директория SQLite) gitignored и в worktree-контексте отсутствует →docker buildпадал сexit 1(«BUILD-STAGING: docker build failed - aborting»), задачу заворачивало сdeploy-stagingнаdevelopment(петля, выжигание developer-ретраев, инцидент текущего прогона ORCH-021). При этом COPY был мёртвым грузом:data/всегда приходит рантайм-volume'ом (./data:/app/data/./data/staging:/app/dataвdocker-compose.yml), который затеняет всё, что было запечено в образ. Заменено наRUN mkdir -p /app/data(директория-mountpoint существует и без bind-mount, без зависимости от build-контекста). КонтрактыSTAGE_TRANSITIONS/QG_CHECKS, штампLABEL org.opencontainers.image.revision=$GIT_SHA(ORCH-058 Strategy B), exit-код-контракт хука — не тронуты. Регресс-гард:tests/test_deploy_hook_provenance.py::test_tc08b_dockerfile_does_not_copy_gitignored_data_dir(запрещаетCOPYлюбого gitignored-пути). deploy-stagingбольше не зацикливается на infra-only FAIL песочницы (C9a/C9b) (ORCH-061): self-hostingorchestratorкрутился в петлеdeploy-staging → development—scripts/staging_check.pyдавалexit 1при ЛЮБОМ упавшем чеке, поэтому две чисто инфраструктурные проверки C9a (ветка не появилась вorchestrator-sandbox) и C9b (job аналитика не встал в очередь staging) — вызванные тем, что SANDBOX-бот-аккаунты не состоят в sandbox-проекте Plane (шаги 6+ конвейера в песочнице недостижимы, это НЕ регресс конвейера) — приводили кstaging_status: FAILED→ откат → цикл (выжигание developer-ретраев, токенов, паразитная нагрузка общего инстанса). Решение (Direction «б», ADR-001): чеки классифицируются наREAL(все проверки конвейера A*/B*/C7/C8 — fail-closed) иSANDBOX_INFRA(строго allowlist{C9a, C9b}— waivable). Новый leaf-модульsrc/staging_verdict.py(stdlib-only, контракт «never raise», по образцуmerge_gate/image_freshness):classify_check(label)(allowlist по ведущему токену, всё неизвестное/малформенное →REALfail-closed) иcompute_staging_verdict(items, infra_tolerant) -> StagingVerdict: любой REAL-FAIL →FAILED/exit 1 (страховка при ЛЮБОМ значении флага); упали ТОЛЬКО C9a/C9b и толерантность включена →SUCCESS/exit 0 + упавшие метки вwaived(наблюдаемость); только C9a/C9b и толерантность выключена →FAILED/exit 1 (legacy-строгий); любая внутренняя ошибка вердикта →FAILED/exit 1 (никогда не ложный green).scripts/staging_check.py:Resultsавто-классифицирует каждый чек (публичная 3-tuple форма_itemsсохранена — регрессия-гард ORCH-048 b6),categorized_items()отдаёт категорию,summary()печатает разбивку REAL/SANDBOX_INFRA;main()сворачивает прогон через_verdict(...), печатает строкиINFRA-WAIVED:/VERDICT:и делаетsys.exit(verdict.exit_code); новый флаг--strictфорсит строгий режим для одного запуска. Глобальный kill-switchORCH_STAGING_INFRA_TOLERANCE_ENABLED(Settings.staging_infra_tolerance_enabled, defaulttrue;false→ строгий 1:1 до ORCH-061), живёт в.env.staging;--strictимеет приоритет над env. Наблюдаемость на стороне конвейера:src/agents/launcher.pyполучилaction_stage_no_changes_note(stage, repo)— на action-стадиях (deploy-staging/deploy) self-hosting-репо «нет изменений для коммита» логируется как ожидаемое, а не трактуется как недопоставка. Контракты НЕ менялись:STAGE_TRANSITIONS, реестрQG_CHECKS, frontmatterstaging_status: SUCCESS|FAILED/deploy_status:(толерантность применяется в скрипте ДО записи артефакта деплоером), exit-code-контракт хука (0/1/2),check_staging_status/_parse_staging_status; схема БД — без миграций. ADRdocs/work-items/ORCH-061/06-adr/ADR-001-staging-infra-tolerance.md. Документация:docs/architecture/README.md,docs/operations/STAGING_CHECK.md,.openclaw/agents/deployer.md. Тесты:tests/test_staging_check_b6.py,tests/test_qg_checks.py,tests/test_config.py,tests/test_launcher.py,tests/test_qg.py,tests/test_stage_engine.py::TestStagingInfraTolerance.- Reconciler (F-1) больше не разблокирует escalated / Blocked / Needs-Input задачи (ORCH-060): sweeper потерянных webhook (ORCH-053) не отличал «застряла из-за потерянного события» от «исчерпала лимит developer-ретраев и ждёт человека» — если CI зелёный, а reviewer слал REQUEST_CHANGES до
MAX_DEVELOPER_RETRIES, каждый тик F-1 видел зелёныйcheck_ci_greenи доигрывалdevelopment → review→ reviewer снова REQUEST_CHANGES → откат (стадия не меняется, escalated вgitea.pyлишь шлётnotify_error) → следующий тик снова разблокировал. Бесконечная петля (инцидент ET-013: 10 разблокировок за ночь, лишние запуски агентов/токены, спам в Telegram, паразитная нагрузка общего self-hosting-инстанса). ВReconciler._reconcile_gate_task(src/reconciler.py) ПОСЛЕ существующих гардов (analysiscarve-out, нет гейта, активный job, grace) и ДО пред-оценки гейта добавлены два пред-гарда с раннимreturn(молчаливый skip — безadvance, без инкрементаunblocked_total, без нотификаций): Guard 1 (escalated, детерминированный, без сети, проверяется первым) —developer_retry_count(task_id) >= MAX_DEVELOPER_RETRIES; приватныйstage_engine._developer_retry_countповышен до публичногоdeveloper_retry_count(единый источник истины по подсчёту ретраевagent_runs, приватное имя сохранено как алиас), граница берётся изstage_engine.MAX_DEVELOPER_RETRIES(не хардкод3). Guard 2 (явный человеческий Plane-статус, Вариант A — без миграции БД) — новый never-raise хелперplane_sync.fetch_issue_state(issue_id, project_id) -> str|None(тот же endpoint/headers, чтоfetch_issue_sequence_id) +Reconciler._is_blocked_or_needs_input(task): резолв проекта (projects.get_project_by_repo) →get_project_states(pid)→ сверка текущего state issue сblocked/needs_input; любая ошибка/None/нерезолвленный проект → консервативный skip (True: не-разблокировать безопаснее). F-2 по существу не менялся: Blocked/Needs Input не входят в опрашиваемый набор{in_progress, approved, rejected}→ не доигрываются (зафиксировано регресс-тестом). Новый под-флагORCH_RECONCILE_SKIP_BLOCKED_ENABLED(true) гасит ТОЛЬКО сетевой Guard 2 (escape hatch при Plane-outage); Guard 1 всегда активен. Схема БД,STAGE_TRANSITIONS,QG_CHECKS, never-raise на единицу работы,analysiscarve-out и kill-switch'и (reconcile_enabled/reconcile_plane_enabled) не менялись. ADRdocs/work-items/ORCH-060/06-adr/ADR-001-reconciler-skip-escalated.md. Тесты:tests/test_reconciler.py(TC-01…TC-11 + регресс ORCH-053). - Re-deploy после отката больше не зависает на
deploy;.env.exampleдополнен (ORCH-036, review-fix): sentinel-маркеры самодеплоя (approve-requested/initiated/result) ключуются по стабильномуwork_item_id, поэтому при FAILED-деплое и откате БАГ-8 (deploy → development) они оставались на диске — после фикса developer-ом и повторного захода задачи наdeployФаза B по idempotency-guard видела STALEinitiatedи становилась no-op: detached-хук не перезапускался, finalizer не ставился, задача висела наdeployнавсегда (нарушался retry-контракт стадии, AC-4/AC-10; устаревшийresultк тому же был бы перечитан новым finalizer'ом). Добавленself_deploy.clear_state(repo, work_item_id)(never-raise, idempotent, рекурсивное удаление<repos_dir>/.deploy-state-<repo>/<wi>/), вызывается в ветке БАГ-8-откатаcheck_deploy_statusFAILED (src/stage_engine.py) и дополнительно в начале Фазы A (_handle_self_deploy_phase_a) — каждый новый прод-деплой-проход стартует с чистого состояния. Отдельно: канонический.env.example(CLAUDE.md правило №8, ТЗ §2.6) дополнен полным блоком новых дескрипторовORCH_SELF_DEPLOY_*/ORCH_DEPLOY_*(плейсхолдеры, секреты не коммитятся) по образцу merge-gate ORCH-043. КонтрактыSTAGE_TRANSITIONS/QG_CHECKS/_parse_deploy_status/ БАГ-8 / merge-gate не тронуты. Тесты:tests/test_deploy_rollback.py::test_tc11_re_deploy_after_rollback_not_wedged,tests/test_deploy_hook_mapping.py::test_clear_state_removes_all_markers_and_is_idempotent. - Контейнер и агенты бегут под uid хоста (1000:1000), не root (ORCH-040): оба сервиса в
docker-compose.yml(orchestrator,orchestrator-staging) получилиuser: "1000:1000"(slin) — устраняет корень проблемы, при которой Claude-CLI агенты, запускаемые черезsubprocess.Popenвнутри root-контейнера, создавали все артефакты конвейера (git worktree/repos/_wt/..., коммиты вdocs/work-items/...) с владельцемroot:rootна хосте, из-за чегоgit pull/git resetпод slin падали сinsufficient permission for adding an objectи каждый деплой требовал ручногоchown. Теперь файлы сразуslin:slin. Доступ к docker.sock сохранён черезgroup_add: ["999"](МИНА 1 — НЕ удалена). SSH-маунт приведён к единому HOME агента: target/root/.ssh→/home/slin/.ssh(/home/slin/.orchestrator-ssh:/home/slin/.ssh:ro), синхронно сHOME=/home/slin, который launcher форсит в env Popen и git_env — устранён скрытый рассинхрон SSH-маунта с форсимым HOME.src/agents/launcher.pyиDockerfileНЕ менялись (numeric uid работает без записи в/etc/passwd;safe.directory '*'уже покрывает git над bind-mount). Требует host-prerequisites Owner (P-1…P-4, вне кода): блокер P-1 —chown -R 1000:1000 /home/slin/.claudeдля доступа uid 1000 к claude creds (иначе preflight заворачивает конвейер); прод-рестарт self — только в окно тишины (общий инстанс с enduro-trails), страховка — staging-гейт (adr-0003). ADRdocs/work-items/ORCH-040/06-adr/ADR-001-run-agents-as-host-uid.md, глобальныйdocs/architecture/adr/adr-0005-container-runs-as-host-uid.md; INFRA.md обновлён (рантайм-uid, volumes/SSH target, host-prerequisites). Тесты:tests/test_orch040_compose.py. - Staging-чек B6 читает реестр из окружения работающего staging-инстанса (ORCH-048): блок B6 «Registry: sandbox present, prod ET/ORCH absent» в
scripts/staging_check.pyдавал ложный FAIL (prod-ET=YES(BAD!),prod-ORCH=YES(BAD!)) при фактически исправной изоляции — единственный чек suite, который не ходил к инстансу по HTTP, а импортировалsrc.projectsлокально через host-path хакsys.path.insert(0, "/repos/orchestrator")+importlib.reload, строя реестр изORCH_PROJECTS_JSONprocess-env запускающего процесса. При фактическом запуске деплоером с хоста переменная не задана → дефолт_DEFAULT_PROJECTS(ET+ORCH) → ложный FAIL → лишний откатdeploy-staging → development. Решение (вариант «в», ADR-001): host-path хак удалён; suite канонически запускается ВНУТРИ контейнераorchestrator-stagingчерезdocker exec … python3 /repos/orchestrator/scripts/staging_check.py(scripts/доступен только через bind-mount,import src.projectsрезолвится черезPYTHONPATH=/appиз кода контейнера, env —.env.staging) → B6 читает реестр именно работающего инстанса, без HTTP-bootstrap и «курицы-яйца». Логика вердикта вынесена в чистую_evaluate_b6(known) -> (passed, detail)(инвариантpassed ⟺ SANDBOX ∈ known ∧ PROD_ET ∉ known ∧ PROD_ORCH ∉ known, формат detail сохранён) +_known_project_ids_from_registry()/_run_b6()с детерминированным FAIL при недоступности источника (не ложный PASS, не необработанное исключение). Синхронно обновлены.openclaw/agents/deployer.md(команда стадии черезdocker exec) иdocs/operations/STAGING_CHECK.md.src/projects.py,.env*и прочие чеки A/B4/B5/C не тронуты; реестрQG_CHECKSиcheck_staging_status(ADR-0003) не менялись. ADRdocs/work-items/ORCH-048/06-adr/ADR-001-b6-registry-via-in-container-run.md. Тесты:tests/test_staging_check_b6.py. - Testing-гейт
check_tests_passedчитаетresult:наравне сverdict:/status:(ORCH-047): парсер_parse_tests_verdict(src/qg/checks.py) теперь принимает три равноправных машиночитаемых поля frontmatter13-test-report.md—result:(канон промпта тестера.openclaw/agents/tester.md,result: PASS|FAIL), плюс легасиverdict:иstatus:(enduro-trails ET-001..ET-014); достаточно любого одного непустого. Устраняет рассинхрон контракта: тестер честно эмитилresult: PASSбезverdict:/status:, парсер попадал в ветку «нет машинного вердикта» → откатtesting → developmentв петлю до исчерпанияMAX_DEVELOPER_RETRIES(наблюдалось на ORCH-17; ORCH-016 прошёл лишь из-за избыточного дублирования полей). Семантика приоритетов сохранена и распространена на все три поля через объединённую строку: negative-токен в любом поле авторитетен (перебивает positive), наборы токенов заморожены (обратная совместимость). Сигнатура гейта, имя и реестрQG_CHECKSне менялись. ADRdocs/work-items/ORCH-047/06-adr/ADR-001-result-field-in-tests-gate.md. Тесты:tests/test_qg.py::TestCheckTestsPassed. - БАГ-8: провал deploy/deploy-staging → корректный откат на
development. - Изоляция тестов от живого Plane API (PR #27): autouse-фикстура сброса settings.
Историю до введения канона см. в docs/history/ (BUGFIXES_, LESSONS_, INCIDENT_).*