Files
orchestrator/CHANGELOG.md
claude-bot aff334e82b fix(merge-gate): SHA-in-main as sole merge-verify criterion + main regression guard
Root-cause fix for main erosion (phantom merge): code of ORCH-067/069 reached
`done` while absent from origin/main (only their auto docs-PRs landed).

- FR-1: verify_merged_to_main confirms merge ONLY by `git merge-base
  --is-ancestor <validated_sha> origin/main`; the OR-branch pr_already_merged is
  removed (a merged PR no longer confirms). Empty SHA / git error -> False.
- FR-2: pr_already_merged demoted to merge_pr idempotency-guard; counts a PR only
  when merged & head.ref==<branch> & base.ref=="main" (explicit in-loop filter).
- FR-3: merge_pr selects the open code-PR by head==<branch> AND base==main.
- FR-5: new deterministic check_main_regression in _handle_merge_verify (after
  confirmed SHA-in-main, before done) verifies MAIN_REGRESSION_MARKERS still in
  origin/main; deterministic count==0 -> alert "main regressed" + HOLD (NOT done,
  no rollback); git error of the grep -> fail-open. Kill-switch
  ORCH_REGRESSION_GUARD_ENABLED; non-self -> no-op.
- FR-4: root .gitattributes `CHANGELOG.md merge=union` so Unreleased edits
  auto-merge on rebase without conflict (branch not rolled back).

Invariants unchanged (STAGE_TRANSITIONS, QG_CHECKS, deploy-status, merge-gate,
image-freshness, DB schema, external HTTP API); non-self repos no-op (INV-5);
never-raise (INV-1); merge only via Gitea PR-API (INV-2).

Docs: CHANGELOG, .env.example (README/ADR updated by architect). Tests:
tests/test_orch073_*.py (TC-01..18); existing merge-gate tests updated for the
new code-PR filter.

Refs: ORCH-073

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 16:30:46 +03:00

91 KiB
Raw Blame History

Changelog

Формат: Keep a Changelog. Записи — на смысловой PR/задачу.

[Unreleased]

  • CRIT: системный фикс эрозии main — SHA-в-main как единственный критерий merge-verify + регресс-гард + .gitattributes (ORCH-073): устранён корень фантомного merge, из-за которого код задач ORCH-067 (plane_issue_link) и ORCH-069 (qg0_title_max) дошёл до done, но физически отсутствовал в origin/mainmain попадали только их авто docs-PR). (FR-1) merge_gate.verify_merged_to_main подтверждает merge ТОЛЬКО прямым фактом git merge-base --is-ancestor <validated_sha> origin/main — OR-ветка pr_already_merged удалена (merged PR больше не подтверждает merge); пустой SHA / git-ошибка → False (fail-closed, never-raise). (FR-2) pr_already_merged понижен до idempotency-guard для merge_pr и засчитывает PR лишь при merged & head.ref==<branch> & base.ref=="main" (явный in-loop фильтр вместо ненадёжного query-параметра head — исключает авто docs-PR). (FR-3) merge_pr выбирает open code-PR строго по head.ref==<branch> И base.ref=="main"; merge только через Gitea PR-merge API, никогда push/force-push в main. (FR-5) новый детерминированный регресс-гард merge_gate.check_main_regression в _handle_merge_verify ПОСЛЕ подтверждённого SHA-в-main и ДО done проверяет, что origin/main содержит декларативный append-only набор маркеров ранее-merged задач (MAIN_REGRESSION_MARKERS, git grep -c <marker> origin/main -- <path>); детерминированный count==0 → alert «main regressed» + HOLD (set_issue_blocked + Telegram + Plane, задача НЕ done, БЕЗ авто-отката на development), git-ошибка самого грепа → fail-OPEN (не блокирует, SHA-в-main остаётся первичным гейтом). Kill-switch ORCH_REGRESSION_GUARD_ENABLED (дефолт true), область — merge_verify_applies (self-hosting / merge_verify_repos), non-self → no-op. (FR-4) корневой .gitattributes с CHANGELOG.md merge=union — правки ## [Unreleased] авто-сливаются при auto_rebase_onto_main без конфликта (обе записи сохраняются), ветка не откатывается в development и не тащит устаревший код-сосед; docs/** под union НЕ ставится. GET /queue::merge_verify_status дополнен счётчиком main_regressed_alerts_total (read-only). Инварианты НЕ менялись: STAGE_TRANSITIONS, реестр QG_CHECKS (под-гейт — врезка в advance_stage), check_deploy_status/_parse_deploy_status, merge-gate, image-freshness, схема БД, внешние HTTP-эндпоинты; non-self (enduro) merge/verify/гард — no-op (INV-5); ручной Confirm Deploy сохранён. ADR docs/work-items/ORCH-073/06-adr/ADR-001-merge-verify-sha-truth-and-regression-guard.md (+ сквозной adr-0014). Документация: docs/architecture/README.md, .env.example. Тесты: tests/test_orch073_*.py (TC-01..18).
  • Конфигурируемый верхний лимит длины заголовка QG-0 (ORCH_QG0_TITLE_MAX, дефолт 200) (ORCH-069): хардкод if len(name) > 80 во входной валидации _qg0_errors (src/webhooks/plane.py) вынесен в настраиваемый параметр Settings.qg0_title_max (env ORCH_QG0_TITLE_MAX, дефолт 200). Лимит 80 был гигиеническим, а не структурным (slug режется независимо [:30], tasks.title TEXT без ограничения), поэтому валидные заголовки 81200 символов отклонялись на входе без бизнес-причины. Лимит читается из settings.qg0_title_max динамически на каждый вызов (тесты патчат значение), текст ошибки подставляет актуальное число; граница строгая (len > limit → FAIL, len == limit → PASS). Graceful-деградация (AC-3, self-hosting safety): пустое/нечисловое значение env не роняет процесс на старте — field_validator(mode="before") _qg0_title_max_default в src/config.py перехватывает сырое env ДО int-парсинга pydantic и при невалидном/пустом входе возвращает дефолт 200 (never-raise), гася ValidationError. Чисто аддитивно и обратносовместимо: дефолт 200 > прежних 80 → все ранее проходившие заголовки проходят (AC-7). Инварианты НЕ менялись: STAGE_TRANSITIONS, реестр QG_CHECKS (QG-0 — inline-валидация входа, не зарегистрированный stage-gate), схема БД, slug-логика [:30], нижние лимиты (< 5 title, < 20 description), soft-QG-0 поведение (warning на work_item.created), API. ADR docs/work-items/ORCH-069/06-adr/ADR-001-configurable-qg0-title-limit.md. Документация: .env.example, .env.staging.example. Тесты: tests/test_qg0_title_limit.py.

Added

  • Telegram live-tracker: bump по умолчанию + статус-строка Plane + кликабельный номер задачи (ORCH-067): три улучшения карточки задачи (src/notifications.py), без изменения транспорта/схемы БД/STAGE_TRANSITIONS/QG. (1) Дефолт tracker_mode сменён edit → bump (src/config.py): актуальный статус всегда последним сообщением в чате при активной переписке; edit остаётся доступен через ORCH_TRACKER_MODE=edit. Логика update_task_tracker (best-effort delete_telegram(old)send_telegram(..., disable_notification=True)set_tracker_message_id только при успешном send) и инвариант «одна карточка на задачу» сохранены. (2) Статус-строка карточки 📍 <status_label> по статусной модели ORCH-066: чистый/детерминированный, never-raise хелпер plane_status_label(task_row) (любая ошибка → дефолт по stage, рендер не ломается). Оффлайн-ядро (stage → Plane-статус; ⏸️ In Review из brd-clock; ⏸️ Awaiting Deploy) работает всегда без сети; ветки, неотличимые offline (❓ Needs Input, Blocked, Rejected, Cancelled, Deploying, Monitoring after Deploy), дорисовывает best-effort live-overlay _live_plane_branch_override — читает живой Plane-статус (reverse-map UUID→имя) с kill-switch'ем, per-issue TTL-кэшем и коротким таймаутом; недоступность сети/ответа → тихая деградация на stage-маппинг, конвейер НИКОГДА не блокируется (ADR Р-2/Р-3/Р-4). (3) Кликабельный номер задачи: единый never-raise хелпер plane_issue_link(work_item_id, plane_issue_id, project_id, repo)<a href={web_base}/{workspace}/projects/{project_id}/issues/{issue_id}/>ORCH-NNN</a>, переиспользует guard'ы ORCH-017 (_plane_issue_url, loopback-base → «нет web URL»); fail-safe (не хватает web_base/workspace/project_id/issue_id) → html.escape(work_item_id) (номер без ссылки). Применён в заголовке карточки (render_task_tracker дочитывает repo/plane_issue_id из tasks, схема не менялась) и во всех точках send_telegram/notify_*, где в тексте есть work_item_id (notify_approve_requested/notify_error, stage_engine.py, agents/launcher.py, merge_gate.py, job_reaper.py, security_gate.py, reconciler.py, main.py — ровно где упоминается номер). Новые настройки: ORCH_TRACKER_LIVE_STATUS (true, kill-switch), ORCH_TRACKER_LIVE_STATUS_TTL_S (60), ORCH_TRACKER_LIVE_STATUS_TIMEOUT_S (3). Самохостинг: смена дефолта bump затрагивает ВСЕ проекты — проверено отсутствие регресса (тесты + staging). ADR docs/work-items/ORCH-067/06-adr/ADR-001-tracker-plane-status-and-link.md. Документация: CLAUDE.md (раздел «Нотификации / Telegram live-tracker»), docs/architecture/README.md, docs/architecture/internals.md (§7), .env.example.
  • 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 (D1D9), раздел в 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-hosting orchestrator это особенно остро: один общий прод-инстанс обслуживает все проекты из общей БД, поэтому секрет/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-frontmatter security_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, cap MAX_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-hosting orchestrator); таймаут security_scan_timeout_s; never-raise. v1 — Python-only стек; SAST/мульти-стек — follow-up (BR-14). Инфраструктура: pinned gitleaks (статический 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, схема БД (без миграций). ADR docs/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.pyhandle_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.pyadvance_stage получил keyword-only параметр confirm_deploy: bool = False (обратносовместимо: все существующие вызовы из launcher/reconciler/finalizer передают finished_agent); блок Фазы B теперь всегда возвращается рано для deploy + finished_agent is None self-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-поведение). ADR docs/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-sync deploy→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, схема БД (без миграций). ADR docs/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-2 agent_runs.exit_code записан, но job всё ещё running — но только после finalization-grace reaper_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_greenstage_engine._run_qg, как у reconciler) ПЕРЕД claim, затем атомарный claim done ПЕРВЫМ и только победитель 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 ИЛИ истёкший TTL merge_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) (lazy httpx, Gitea API pulls?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.py lifespan (после reconciler.start() / перед reconciler.stop()), снимок reaper в GET /queue (enabled/interval/last_run/reaped_total/last_reaped/lease_reclaimed_total). Условность раскатки реклейма lease зеркалит merge-gate (merge_gate_repos CSV или self-hosting orchestrator). Новые настройки: 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 на единицу фоновой работы. ADR docs/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 + lazy qg.checks.is_self_hosting_repo): post_deploy_applies (условность раската), probe_signals (один опрос /health 200+{"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=trueROLLBACK; иначе 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 job post-deploy-monitor (детерминированный, no-LLM, калька deploy-finalizer, НЕ стадия и НЕ daemon): арм в stage_engine.advance_stage в блоке next_stage == "done" ПОСЛЕ terminal-sync/release-lease (post_deploy.arm_monitor, sentinel armed = идемпотентность при двойном webhook/reconciler/finalizer); один тик = один job — перехват в agents/launcher.launch_job ДО _spawnstage_engine.run_post_deploy_monitor (один опрос → append в seriesclassify → перепостановка с задержкой 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-frontmatter post_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-sync deploy→done, merge-gate, exit-код-контракт хука (0/1/2), схема БД (без миграций; состояние — sentinel-файлы). Условность как ORCH-35/36/43/58. ADR docs/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_REVISION B), 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 tag fail-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-switch ORCH_IMAGE_FRESHNESS_ENABLED (true) включает A+B как целое (нет «B без A» = вечного fail-fast); область — ORCH_IMAGE_FRESHNESS_REPOS (CSV; пусто → только self-hosting orchestrator). Контракты НЕ менялись: STAGE_TRANSITIONS (под-гейт ребра, не стадия), exit-code-контракт хука (0/1/2), map_exit_code_to_status, check_deploy_status/_parse_deploy_status, БАГ-8, terminal-sync, merge-gate; схема БД — без миграций. ADR docs/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 репозитория orchestrator deploy_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 + setsidscripts/orchestrator-deploy-hook.sh, чтобы рестарт 8500 пережил гибель контейнера; орк НЕ убивает себя из docker.sock) с build-once retag staging-образа (SOURCE_IMAGE), ставится детерминированный finalizer-job; маркер initiated — идемпотентность повторного Approved. Фаза C (run_deploy_finalizer, reserved-agent deploy-finalizer, claim'ится новым контейнером после рестарта) — читает sentinel result (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-sync deploy→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. ADR docs/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 на красном гейте не вызывается вовсе); analysis F-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.debuglogger.info для видимости потерянного CI-события. Анти-дубль на создании задачи (db.create_task_atomic под process-wide threading.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) НЕ менялись. ADR docs/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-gate check_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-hosting orchestrator; глобальный kill-switch merge_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). ADR docs/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.
  • Режим bump live-трекера Telegram (ORCH-042): новый ORCH_TRACKER_MODE (Settings.tracker_mode, дефолт edit) выбирает поведение карточки задачи. edit (как было) — карточка редактируется на месте (editMessageText). bump — на каждом обновлении старое сообщение удаляется и карточка отправляется заново вниз чата (best-effort delete_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 helper delete_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) не менялись. ADR docs/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.md developer-агента) теперь несёт суть претензий, а не только ссылку на файл — устраняет «испорченный телефон», из-за которого агент шёл «читать файл», терял ключевые 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, tester check_tests_passed FAIL) встраивают извлечённый текст и сохраняют ссылку на полный файл («Полный контекст»); при пустом/битом артефакте — graceful-фоллбэк на прежнюю ссылку-строку (никаких исключений в advance_stage). Tester-ветка дополнительно всегда включает reason гейта. Последовательность отката, _developer_retry_count, поля AdvanceResult и реестр QG_CHECKS не менялись. ADR docs/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 не затронут. ADR docs/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_urlgitea_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 сохранён. ADR docs/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). ADR docs/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. QG check_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, Plane state=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»); добавлен TTL ORCH_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. ADR docs/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-hosting orchestrator крутился в петле deploy-staging → developmentscripts/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 по ведущему токену, всё неизвестное/малформенное → REAL fail-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-switch ORCH_STAGING_INFRA_TOLERANCE_ENABLED (Settings.staging_infra_tolerance_enabled, default true; 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, frontmatter staging_status: SUCCESS|FAILED / deploy_status: (толерантность применяется в скрипте ДО записи артефакта деплоером), exit-code-контракт хука (0/1/2), check_staging_status/_parse_staging_status; схема БД — без миграций. ADR docs/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) ПОСЛЕ существующих гардов (analysis carve-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 на единицу работы, analysis carve-out и kill-switch'и (reconcile_enabled/reconcile_plane_enabled) не менялись. ADR docs/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 видела STALE initiated и становилась 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_status FAILED (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). ADR docs/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_JSON process-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) не менялись. ADR docs/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) теперь принимает три равноправных машиночитаемых поля frontmatter 13-test-report.mdresult: (канон промпта тестера .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 не менялись. ADR docs/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_).*