Files
orchestrator/CHANGELOG.md
claude-bot 78b6cdb3f1 docs(changelog): repair duplicated ORCH-095 entry body
Reviewer P1 (ORCH-027 attempt 2): inserting the ORCH-027 changelog
block duplicated the adjacent ORCH-095 entry — its paragraph body was
repeated verbatim, corrupting a golden-source doc and another work
item's artifact (CLAUDE.md §3). Remove the duplicate half, leaving a
single ORCH-095 body. ORCH-027 entry untouched (already correct).

Refs: ORCH-027

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 01:26:24 +03:00

207 KiB
Raw Blame History

Changelog

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

[Unreleased]

  • Детерминированный гейт покрытия тестами — защита от тихой деградации coverage перед merge в main (ORCH-027, feat): существующие тестовые гейты (check_ci_green, check_tests_passed, merge-gate re-test) судят только по факту прохождения, не по полноте — ни один не замечает «300 строк кода, 0 тестов», и при пакетном автономном прогоне (ORCH-088) покрытие монотонно деградирует. Введён детерминированный (без LLM) под-гейт ребра deploy-staging → deploy по образцу security-гейта (ORCH-022): leaf src/coverage_gate.py (never-raise) + тонкая обёртка check_coverage_gate в QG_CHECKS + врезка _handle_coverage_gate в advance_stage. Аддитивно: STAGE_TRANSITIONS / семантика существующих check_* / machine-verdict ключи (verdict:/result:/deploy_status:/staging_status:/security_status:) — байт-в-байт прежние; новая БД-таблица аддитивна (NFR-5/AC-8). См. docs/work-items/ORCH-027/06-adr/ADR-001-coverage-gate.md, сквозной docs/architecture/adr/adr-0029-coverage-gate.md.
    • Точка/порядок (D1, AC-2): под-гейт исполняется ПОСЛЕ merge-gate (покрытие меряется на догнанном auto_rebase_onto_main HEAD — ровно том коде, что landed в main) и ДО image-freshness (фейл до дорогого docker-rebuild). FAIL → штатный откат на development (+ инкремент developer-retry, cap MAX_DEVELOPER_RETRIES) и освобождение merge-lease (merge-gate держал его на своём PASS — зеркало image-freshness rollback, TR-2). STAGE_TRANSITIONS не меняется (под-гейт, как security/merge/image-freshness).
    • Измерение (D2, FR-1/AC-1): python -m pytest tests/ --cov=src --cov-report=json в изолированном per-branch worktree (ensure_worktree, прецедент check_tests_local); метрика — totals.percent_covered (line coverage src/). Измеритель инкапсулирован за measure_coverage(repo, branch) -> float | None (стек-расширяемость BR-6: jest/jacoco — новая ветка measure_*, без переписывания ядра). Тайм-аут coverage_run_timeout_s. Новая pip-зависимость pytest-cov==5.0.0 (offline на момент замера).
    • Чистая функция решения (D3, FR-2/AC-3): compute_coverage_verdict(measured, baseline, floor, policy, epsilon) -> (ok, reason) — детерминированная, без LLM/IO. absolutemeasured ≥ floorε; baselinemeasured ≥ baselineε; both (дефолт) → оба; baseline is None (bootstrap) → baseline-условие не применяется (нельзя регрессировать против пустоты). epsilon — допуск на шум измерения (NFR-4, анти-флап у границы). Покрыто unit-тестами всех режимов/границ/epsilon.
    • Базовая линия + ratchet (D4/D5, FR-4/AC-4): аддитивная БД-таблица coverage_baseline(repo PK, coverage, source_sha, updated_at) (CREATE TABLE IF NOT EXISTS, паттерн repo_freeze/job_deps; существующие таблицы не мигрируются). Хелперы db.get_coverage_baseline/ratchet_coverage_baseline/set_coverage_baseline/all_coverage_baselines. Наращивание только вверх в choke-point подтверждённого merge _handle_merge_verify (ребро deploy → done): coverage_gate.ratchet_baseline_on_merge читает измеренное из 18-coverage-report.md (single source of truth) и применяет атомарный compare-and-set UPDATE … WHERE coverage <= measured (или INSERT — bootstrap) под держимым merge-lease (ORCH-043) → базовая линия никогда не падает даже при гонке. Меньшее значение базовую линию не понижает.
    • Условность + fail-open (D6, FR-5/FR-6/AC-5/AC-6): coverage_gate_applies(repo) (локально) ПЕРВЫМ — дорогой прогон только при applies==True. coverage_gate_enabled=False → инертно (1:1 как до ORCH-027); coverage_gate_repos (CSV; пусто → self-hosting only is_self_hosting_repo, как security/merge/image-freshness) → enduro-trails не затронут (no-op (True, "N/A")). Ошибка/недоступность coverage-инструмента или непарсимая метрика → fail-open + WARNING по умолчанию (coverage_tool_fail_closed=False, анти-петля по образцу ORCH-061/022 dep-audit); флаг переключает в fail-closed.
    • Машинный вердикт + наблюдаемость (D7/D8, FR-7/AC-9): артефакт 18-coverage-report.md (frontmatter coverage_status: PASS|FAIL + measured_coverage/baseline/floor/policy/epsilon/delta), вердикт читается ТОЛЬКО из frontmatter через src/frontmatter.parse_frontmatter (ORCH-052c, регистр фиксирован); гейт сам пишет отчёт и читает вердикт обратно из того же файла (single source of truth, как security_status:). Read-only блок coverage в GET /queue (kill-switch/scope/policy/floor/epsilon/per-repo baselines). При FAIL — send_telegram с кликабельным номером (link_for), измеренным покрытием, порогом/базовой линией и дельтой. Опциональный ручной override POST /coverage/baseline?repo=…&value=… (по образцу POST /serial-gate/unfreeze) для легитимного разового снижения покрытия.
    • Self-hosting безопасность (NFR-1/NFR-3/AC-7): leaf не импортирует stage_engine; любое исключение перехвачено (never-raise); гейт только мерит/читает/пишет/решает — не деплоит, не рестартит прод-контейнер, не пушит/форс-пушит main (структурно проверено AST-тестом TC-12). Прод-деплой ORCH-027 — строго через staging-гейт (8501), без рестарта прод-контейнера (лейбл arch:major-change).
    • Флаги (config.py, env ORCH_COVERAGE_*, .env.example): coverage_gate_enabled (kill-switch), coverage_gate_repos, coverage_min_percent (дефолт 0.0 — безопасный раскат: no-regression ведёт ratchet-базовая линия, floor не фейлит в день один), coverage_policy (дефолт both), coverage_epsilon (0.5), coverage_tool_fail_closed (False), coverage_run_timeout_s (900). Откат: ORCH_COVERAGE_GATE_ENABLED=false → полный no-op (мгновенный обратимый kill-switch).
    • Инфра-предусловие: добавить pytest-cov в прод/staging-образ (requirements.txt). При первом применимом merge базовая линия засевается фактическим покрытием main (bootstrap). Тесты: tests/test_coverage_gate.py (TC-01…TC-15: режимы/границы/epsilon verdict, ratchet up-only + bootstrap + per-repo изоляция, applies/kill-switch, fail-open/closed, never-raise, write/read-back отчёта, self-hosting AST-safety, интеграция в advance_stage с откатом+release lease, реальное измерение pytest-cov на фикстур-репо + тайм-аут, snapshot + неизменность QG_CHECKS/STAGE_TRANSITIONS). Обновлены анти-регресс-реестры QG_CHECKS (test_config/test_plane_status_model/test_qg_registry_snapshot/test_stages_invariants) и edge-тесты test_stage_engine (check_coverage_gate: _pass). Полный регресс tests/ -q зелёный.
  • Live-карточка трекера: HTML-инъекция «<1м» больше не застывает карточку — экранирование всех данных-полей на границе рендера (ORCH-095, fix): карточка задачи (src/notifications.py::render_task_tracker) шлётся/редактируется с parse_mode=HTML. _fmt_minutes для стадии < 60 с возвращает литерал "<1м", который интерполировался в HTML-текст сырым → Telegram парсит <1м как открывающий тег → editMessageText отвечает 400 can't parse entities: Unsupported start tag "1м"edit_telegram классифицирует как EDIT_FAILEDupdate_task_tracker делает ранний return (анти-дубль ORCH-087) → карточка застывает (детерминированно воспроизведено 09.06 на ORCH-093, message_id 18854). Корневой класс шире одного <1м: все подставляемые данные (длительности, статус-лейбл, модель, эффорт, токены/стоимость) вставлялись сырыми; экранирован был только заголовок (esc_title) и href/label внутри plane_issue_link. Аддитивно, never-raise, без нового поведения конвейера: STAGE_TRANSITIONS / QG_CHECKS / check_* / транспорт нотификаций / схема БД — не тронуты (затронут ровно один модуль индикативного слоя); kill-switch не требуется (исправление дефекта корректности, откат = git revert).
    • Экранирование на границе рендера, не в источнике (ADR-001 D1/D2, AC-1/AC-2): новый модуль-локальный хелпер _esc(x) = html.escape(str(x)) (never-raise → "" на исключении) оборачивает каждое подставляемое данные-значение (категория D) ровно один раз в точке интерполяции в render_task_tracker/_stage_line: длительности (_fmt_minutes/_capped_review_str), статус-лейбл (_card_status_label), модель (short_model_name), эффорт (_run_effort), токены/стоимость (fmt_tokens/fmt_cost). Функции-источники остаются HTML-агностичными (данные, не разметка): src/usage.py и _fmt_minutes не тронуты — _fmt_minutes продолжает возвращать "<1м", безопасность даёт escape на границе (&lt;1м рендерится оператору визуально идентично <1м → видимый формат не меняется).
    • Категория M (намеренная разметка) неприкосновенна (D5, AC-3): кликабельный номер задачи num_html (plane_issue_link, внутри уже экранированы href+label), link_for(...) в строке « ждёт …», _done_link(...)🔗 PR #n · 📦 Внедрено») и уже-экранированный esc_title через _esc не проходят → остаются валидным HTML, номер остаётся кликабельным. Двойное экранирование (&amp;lt;) структурно исключено: D-слот → _esc ровно один раз, M-слот → as-is.
    • Defence-in-depth (D3): экранируются и сейчас-безопасные D-поля (токены/стоимость/модель дают только цифры/./k/M/$/^claude-…$) — escape для них no-op, выгода — структурный инвариант «каждый D-слот экранирован», устойчивый к будущей смене формата источника.
    • Восстановление застрявших карточек (D4, AC-4): механизм — достаточное условие FR-4 без нового кода: на ближайшем переходе стадии update_task_tracker рендерит новый безопасный текст → edit_telegram отвечает 200 → застрявшая карточка обновляется на месте. Переклассификация can't parse entities → переотправка отвергнута (после фикса источник из наших данных устранён структурно; касание ветки EDIT_FAILED/леджера рискует анти-дублем ORCH-087). Known-limitation (унаследовано ORCH-087/Telegram-48ч): карточка задачи, завершившейся до деплоя фикса, не восстанавливается (нет будущего рендера).
    • Трассировка: перед правкой блоков, помеченных ORCH-042/067/087/091, прочитаны их ADR — инварианты (одна карточка на задачу, леджер сирот + анти-дубль, отражение откатов + суммирование _stage_line, строка Plane-статуса/кликабельный номер) сохранены по построению (ORCH-095 лишь оборачивает уже вычисленные D-значения в _esc, не меняя состав строк/порядок/логику подавления).
    • Тесты: новый tests/test_tracker_html_escape.py (TC-01..TC-11: sub-minute escape на границе, never-raise _fmt_minutes/_esc на граничных входах, рендер sub-minute без сырого <1м, заголовок со спецсимволами без двойного экранирования, escape статус-лейбла/модели/эффорта, HTML-безопасность токенов/стоимости, регресс кликабельного <a href> номера и _done_link, parse-safe edit-payload, edit-in-place без новой карточки + анти-дубль на транзиентном фейле, never-raise на битых входах). Полный регресс tests/ -q зелёный (1437). ADR: docs/work-items/ORCH-095/06-adr/ADR-001-html-safe-card-data-render.md. Откат: git revert (один модуль + тесты + CHANGELOG, без миграций/kill-switch).
  • Терминальная (done) задача держит Done в Plane: terminal-window-aware гард deploy-статусов (ORCH-094, fix): задача с БД stage=done и 0 активных job'ов (верифицировано на ORCH-061, task 47) стабильно флаппила в Plane Awaiting Deploy ⟷ Monitoring after Deploy (273 активности парами, само не затихает) вместо Done. Корень: три deploy-фазовых сеттера (set_issue_awaiting_deploy/set_issue_deploying/set_issue_monitoring) терминал-слепы — любой стейл/двойной/неизвестный вызов под бот-токеном перезаписывает Done промежуточным deploy-статусом, и обратно, бесконечно. Аддитивно, never-raise, под kill-switch, в зоне self-hosting: STAGE_TRANSITIONS / QG_CHECKS / check_* / machine-verdict ключи (deploy_status:/staging_status:/…) / схема БД — не тронуты (читается существующая tasks.stage, без миграции).
    • Единый гард на низком чокпоинте (FR-2, D1/D2): новый leaf src/deploy_status_guard.py (чистая, never-raise, config-gated логика; по образцу serial_gate.py/labels.py/cancel.py) — decide(work_item_id, target, reason) -> ALLOW | CONVERGE_DONE | SUPPRESS. Гард ставится на входе трёх сеттеров plane_sync (а не в caller'ах stage_engine) → перехватывает любой путь, включая неизвестный актор под бот-токеном. Предикат легитимности: deploy-статус легитимен ⇔ задача нетерминальна ИЛИ (done И активно пост-деплой-окно post_deploy.window_active = ARMED & не DONE). Для done: monitoring+окно-активно → ALLOW; иначе → CONVERGE_DONE (сеттер вместо PATCH'а зовёт set_issue_done, идемпотентно). cancelledSUPPRESS (не штампуем поверх терминала ORCH-090). Нетерминальная задача → ALLOW (рабочий deploy-цикл 1:1, AC-4). Task не найден / не-self репо / kill-switch off / любое исключение → ALLOW (fail-safe к прежнему поведению 1:1, NFR-1).
    • Перенос арм-блока перед terminal-sync (D3, AC-4): в advance_stage (ветка next_stage=="done") блок post_deploy.arm_monitor перемещён выше блока set_issue_monitoring (стр. 404). Критично: update_task_stage(task_id,"done") пишет stage='done' раньше легитимного первого Monitoring — без переноса гард ошибочно свёл бы его к Done. Арм-первым пишет ARMEDwindow_active==TrueALLOW пропускает легитимный Monitoring; re-drive deploy→done после закрытия окна (DONE present) → window_active==FalseCONVERGE_DONE (не воскрешает Monitoring). Перенос безопасен: arm_monitor лишь пишет sentinel + ставит отложенный job, не зависит от Plane-статуса/merge-lease (release остаётся после terminal-sync). Инварианты ORCH-021 (идемпотентный арм по ARMED) и ORCH-066 (deploy→done self ⇒ Monitoring) сохранены.
    • Харднинг пост-деплой-монитора (FR-3, D4, AC-3): run_post_deploy_monitor — существующий идемпотентный страж has_marker(DONE) (no-op завершённого окна) сохранён; аддитивно: тик при БД stage='cancelled' мид-окно → закрыть окно mark_done без статус-PATCH и без перепостановки следующего тика (zombie-tick guard). Перепостановка остаётся строго при HEALTHY and ticks < budget (тик ≡ job; нет job → нет тика). После закрытия окна — 0 последующих статус-PATCH; любой стейл set_issue_monitoring добивается гардом D2.
    • Наблюдаемость (FR-4, D5, AC-5): аддитивный BC-kwarg reason: str | None = None у трёх сеттеров; call-site'ы передают "advance:deploy->done"/"phase_a"/"phase_b". decide эмитит ОДНУ структурную запись на вызов: work_item, caller(reason), target_status, db_stage, window_active, verdict (ALLOW → INFO; CONVERGE_DONE/SUPPRESS → WARNING, «что подавили и почему» — атрибуция будущего флаппа). Новый read-only аксессор db.get_task_by_work_item_id (human-readable work_item_id матчит живой ряд; тумбстоны ORCH-090 имеют суффикс #cancelled-<id>).
    • Конфиг/откат (FR-5, D6): src/config.py deploy_status_guard_enabled: bool = True (env ORCH_DEPLOY_STATUS_GUARD_ENABLED; False → сеттеры терминал-слепы, поведение 1:1 прежнее) / deploy_status_guard_repos: str = "" (env ORCH_DEPLOY_STATUS_GUARD_REPOS; CSV, пусто → self-hosting only — не-self репо (enduro) гард не трогает, нулевая регрессия). Откат: ORCH_DEPLOY_STATUS_GUARD_ENABLED=false (мгновенный runtime) или revert ветки.
    • Источник флаппа (BR-7): code-писатели deploy-статусов — только stage_engine.py:404/1218/1316; реконсилятор F-2 эти статусы не перебирает; live-overlay notifications.py — read-only. Гард — буфер на стороне орка, гасящий маятник за один цикл независимо от актора (известный/стейл/неизвестный под бот-токеном). Если актор — внешняя Plane-automation под другим токеном, code-фикс не закрывает её полностью, но идемпотентное схождение к Done нейтрализует видимый эффект.
    • Трассировка: перед правкой блока next_stage=="done" (маркеры ORCH-021/066/043/088) прочитаны их ADR — инварианты сохранены (deploy→done self ⇒ Monitoring; монитор-close ⇒ Done; терминал-набор {done,cancelled}). Тесты: tests/test_deploy_status_terminal_guard.py (TC-01..05/12), tests/test_post_deploy_monitor_termination.py (TC-06..08), tests/test_deploy_status_observability.py (TC-09), tests/test_reconciler_done_deploy_convergence.py (TC-10), tests/test_self_deploy_cycle_regression.py (TC-11). Обновлены анти-регресс-ассерты tests/test_deploy_terminal_sync.py/test_deploy_approve.py под reason-kwarg. Полный регресс tests/ -q зелёный (1411). ADR: docs/work-items/ORCH-094/06-adr/ADR-001-terminal-window-aware-deploy-status-guard.md, сквозной docs/architecture/adr/adr-0028-terminal-window-aware-deploy-status-guard.md.
  • Merge-актор ретраит транзиентные ошибки Gitea (405/5xx) + гард «ветка уже в main» (ORCH-093, fix): две точечные доработки детерминированного merge-актора src/merge_gate.py, чинящие инцидент ORCH-063: self-deploy прошёл, staging OK, PR был open+mergeable, но POST /pulls/{n}/merge вернул HTTP 405 "Please try again later" (Gitea пересчитывал mergeable сразу после пуша) → one-shot merge_pr мгновенно вернул False → корректная защита ORCH-071/081 удержала задачу на deploy + потребовала ручной домерж; повторный прогон финализатора плодил мусорный пустой PR. Аддитивно, never-raise, под существующими kill-switch'ами: STAGE_TRANSITIONS / QG_CHECKS / схема БД — не тронуты; INV-4 (мерж только через Gitea PR-merge API, никогда push/force-push в main) сохранён 1:1.
    • Retry-loop транзиента (FR-1/FR-2, AC-1/AC-2/AC-3, D1/D2): merge_pr оборачивает только мутирующий POST …/merge в ограниченный retry-loop с экспоненциальным backoff (min(base*2^(i-1), max), дефолты 2/5 с → суммарный сон (N-1)*max ≤ 10 с, monitor-поток не подвешивается). Классификатор _classify_merge_response: транзиент (ретрай) — 405/408/любой 5xx/httpx-таймаут/сетевая ошибка, и 409/422 когда PR всё ещё mergeable; терминал (быстрый честный False, защита ORCH-071/081 как прежде) — 403/404/реальный конфликт (409/422 при mergeable==False). Неоднозначный 409/422 разрешается доп. GET /pulls/{index}mergeable; дефолт-политика mergeable==None/недоступно → транзиент (fail-OPEN-в-ретрай: икота Gitea — наблюдаемый кейс, бюджет конечен, backstop сохранён). Каждая попытка логируется attempt i/N (образец check_ci_green).
    • Гард already-in-main (FR-3/FR-4, AC-4, D3/D4): новый leaf _branch_fully_in_main (git merge-base --is-ancestor HEAD origin/main в per-branch worktree) вызывается в ensure_open_pr между «открытый code-PR не найден» и POST …/pulls: ветка целиком в main (нет коммитов origin/main..HEAD) → новый исход "already-in-main" без создания PR (нет мусорного пустого PR на уже влитой ветке). git-ошибка/ambiguous (None) → fail-OPEN (деградация на create-путь, НЕ ложный no-op). В stage_engine._handle_merge_verify исход already-in-main пропускает merge_pr (мержить нечего) и отдаёт авторитетному SHA-in-main (verify_merged_to_main) довести до done; это НЕ HOLD. SHA-in-main остаётся единственным доказательством мержа (ADR-0014).
    • Конфиг/откат (FR-5, AC-5/AC-7, D5): новые поля src/config.py merge_retry_enabled (kill-switch; False → ровно один POST = байт-в-байт прежнее one-shot, нулевая регрессия) / merge_retry_max_attempts (3) / merge_retry_backoff_base_s (2) / merge_retry_backoff_max_s (5), env ORCH_MERGE_RETRY_*, дескрипторы в .env.example. Гард already-in-main — без отдельного флага (накрыт merge_verify_autocreate_pr_enabled). Откат: ORCH_MERGE_RETRY_ENABLED=false (мгновенный runtime) или revert PR.
    • Трассировка: перед правкой merge_pr/ensure_open_pr/_handle_merge_verify прочитаны ADR ORCH-071/073/082 — инварианты (SHA-in-main authoritative, never-raise, idempotency-guard pr_already_merged, base==main фильтр code-PR) сохранены; в MAIN_REGRESSION_MARKERS добавлена строка ("ORCH-093", "_classify_merge_response", "src/merge_gate.py") (append-only).
    • Тесты: tests/test_merge_gate.py (TC-01..TC-12: 405×2→200, 5xx→200, network→200, реальный конфликт/403 терминал, ambiguous-mergeable, исчерпание ретраев, kill-switch one-shot, already-in-main без POST, create при коммитах сверх main, fail-OPEN на git-ошибке гарда, never-raise; httpx мокается, time.sleep → no-op), tests/test_config.py (TC-13: дефолты + env-override ORCH_MERGE_RETRY_*), tests/test_merge_verify.py (TC-14..TC-16: already-in-main пропускает merge_pr→done; исчерпание+SHA-not-in-main→HOLD; транзиент-успех→done). Обновлён tests/test_orch082_ensure_pr.py (гард запинён на create-путь — у гарда своё покрытие). Полный регресс tests/ -q зелёный (1389). ADR: docs/work-items/ORCH-093/06-adr/ADR-001-merge-transient-retry-and-already-in-main-guard.md, сквозной docs/architecture/adr/adr-0027-merge-actor-transient-retry-and-already-in-main.md.
  • Live-карточка трекера: полнота карты статусов, отражение откатов, суммирование метрик стадии по попыткам (ORCH-091, fix): три верифицированных дефекта рендера Telegram-карточки (src/notifications.py, ORCH-067/087). Аддитивно, never-raise, без нового поведения конвейера: STAGE_TRANSITIONS / QG_CHECKS / check_* / транспорт нотификаций / схема БД — не тронуты (затронут ровно один модуль индикативного слоя); kill-switch не требуется (рендер деградирует безопасно, откат = git revert).
    • Деф.1 — застрявший заголовок «To Analyse» (FR-1/2/3, AC-1/2/3): _STAGE_STATUS_LABEL покрывал 8 из 10 ключей STAGE_TRANSITIONSdeploy-staging и cancelled (ORCH-090) выпадали в дефолт-«To Analyse» (ложный «первый статус» на стадии staging-деплоя). Карта расширена: deploy-staging → "Deploying (staging)" (plain-стиль активной стадии, суффикс «(staging)» снимает коллизию с prod-overlay _LIVE_BRANCH_LABELS['deploying'] и с pause-лейблом deploy), cancelled → "Cancelled" (offline-база ORCH-090, совпадает с overlay-лейблом → нет конфликта precedence). Runtime-фолбэк plane_status_label для немаппленной (будущей/неизвестной) стадии заменён с «To Analyse» на нейтральный капитализированный лейбл (_neutral_stage_label, "deploy-staging" → "Deploy Staging"); created остаётся явным ключом → честная «To Analyse»; битый/None-вход → безопасный дефолт. Полнота карты гарантируется программно тестом, итерирующим STAGE_TRANSITIONS.keys() (единый источник истины) — новая стадия без курируемого лейбла даёт красный тест; автогенерация лейблов в самом модуле запрещена (карта остаётся курируемой/человекочитаемой).
    • Деф.2 — ложная картина при откате (FR-4, AC-4): цикл рендера выводил -строку для каждой стадии с завершённым прогоном её агента без учёта позиции относительно текущей — после отката (deploy-staging → development ORCH-043, review → development REQUEST_CHANGES) карточка показывала абсурд « Внедрение … + 🔄 Разработка». Введён лёгкий read-only хелпер _pipeline_pos от порядка STAGE_TRANSITIONS (не от _TRACKER_STAGES, который не содержит deploy-staging/cancelled и не авторитетен по порядку); гейт подавления: -строка рисуется только если current_pos >= _pipeline_pos(stage_key). Нормализация deploy-staging → deploy применяется только к вычислению текущей позиции (схлопнутая строка «Внедрение» несёт stage_key="deploy"); is_active_stageбез изменений (нулевой регресс активного рендера). Подавлённые откатом прогоны по-прежнему входят в тоталы задачи (намеренная семантика отката).
    • Деф.3 — занижение метрик строки стадии (FR-5, AC-5): _stage_line брал ПОСЛЕДНИЙ прогон (last_done), теряя предыдущие попытки (верифицировано на ORCH-069: developer 3 прогона Σ $3.98 → карточка показывала ~$0.00). Теперь _stage_line агрегирует ВСЕ agent_runs агента стадии теми же per-run-формулами, что и блок тоталов (Σ cost_usd, Σ _input_total, Σ output_tokens, Σ _duration_seconds); модель/эффорт/«попытка N» берутся из последнего прогона (id ASC). Каждый агент привязан ровно к одной строке _TRACKER_STAGES → строгий инвариант сходимости: Σ(строк стадий) ≡ тоталы задачи ≡ SUM(agent_runs) по task_id. Формат строк/тоталов и эффорт-суффикс (ORCH-087) — байт-в-байт.
    • Совместимость/регресс (NFR-2, AC-6): In Review (brd-clock), Awaiting Deploy (deploy), Done, live-overlay ветки (Needs Input / Blocked / Rejected / Cancelled / Confirm Deploy / Deploying / Monitoring), строка «Подтверждение BRD», формат строк/тоталов, эффорт-суффикс — без изменений; все существующие тесты карточки зелёные. Перед правкой кода, помеченного ORCH-067/087/090, прочитаны их ADR — инварианты (single-card, never-raise, разделение offline-ядра и live-overlay, терминал cancelled) сохранены.
    • Тесты: tests/test_tracker_status_line.py (ORCH-091 TC-01..TC-03: полнота карты от STAGE_TRANSITIONS, staging-лейбл, нейтральный фолбэк/never-raise; обновлён test_tc06_* под нейтральный фолбэк), новый tests/test_tracker_rollback_metrics.py (TC-05..TC-08: подавление при откате + анти-регресс forward-progress/deploy-staging-строка; суммирование метрик developer 3 прогона ≈ $3.98; сходимость тоталов с SUM(agent_runs); never-raise на NULL-таймстампах/битой стадии). Полный регресс tests/ -q зелёный (1370). ADR: docs/work-items/ORCH-091/06-adr/ADR-001-tracker-status-rollback-metrics.md. Откат: git revert (docs/code-only, один модуль, без миграций/kill-switch).
  • Отмена задачи: Plane-статус STOP (остановка агента + полный сброс) + закрытие дыры релонча (ORCH-090, feat): выделенный Plane-статус STOP — единый декларативный механизм отмены задачи вместо ручной хирургии по БД/процессам. Вводит новое системное терминальное состояние cancelled (стадия tasks.stage='cancelled' + job-исход jobs.status='cancelled'), равноправное done. Аддитивно, под kill-switch, never-raise, restart-safe: STAGE_TRANSITIONS (exit-гейты рёбер) / QG_CHECKS / check_* / семантика существующих статусов — не тронуты (cancelled — терминальный сток, не новое ребро); enduro не затронут; при stop_status_enabled=false — нулевая регрессия.
    • Распознавание (fail-closed): новый логический ключ stop в _PLANE_NAME_TO_KEY ("STOP" → "stop"), намеренно отсутствует в _DEFAULT_STATES (по образцу confirm_deploy/ORCH-059) → доска без статуса STOP резолвит None → ветка не активируется (нет KeyError, нет слепой отмены). handle_issue_updated маршрутизирует stophandle_stopstage_engine.cancel_task (проверяется ПЕРВЫМ, до to_analyse/approved/rejected).
    • Полный сброс (вне критичного окна, AC-1..AC-4): graceful SIGTERM активного агента через переиспользуемый каскад launcher.stop_process (вынесен из _watchdog: SIGTERM → grace → SIGKILL) по jobs.pid; db.cancel_jobs_for_task (queued/running → терминальный cancelled, нигде не реквью'ится — claim_next_job берёт только queued); git_worktree.remove_worktree + новый never-raise src/gitea.py::delete_remote_branch (удаляет только feature-ветку; main/master — явный гард-отказ; без force-push); durable stage='cancelled' + cancelled_at; тумбстон натуральных ключей суффиксом #cancelled-<id>. Docs-артефакты (01..17) сохраняются.
    • Уточнение ADR-001 D4 (при реализации): ADR предлагал сохранить plane_issue_id нетронутым, но get_task_by_plane_id/create_task_atomic матчат по plane_id OR plane_issue_id — нетумбстоненный plane_issue_id заблокировал бы clean-slate re-create (BR-3/TR-4). Поэтому plane_issue_id тоже тумбстонится; исходный UUID (== исходный plane_id во всех путях создания) парсится из детерминированного суффикса для аудита. Зафиксировано в коде/docs/architecture/README.md/CLAUDE.md.
    • Безопасное прерывание merge/deploy (AC-7, NFR-3): STOP в критическом окне → отложенная отмена (cancel.in_critical_window fail-CLOSED): durable tasks.cancel_requested_at, снимаются только queued-job'ы (running-актор деплоя/мержа не трогается), алерт; детерминированный run_deploy_finalizer доводит необратимый шаг до честного исхода и применяет отмену (cancel_task(force=True); задача, дошедшая до done, — честный no-op, код уже в проде). «Критическое окно» = реально начатый необратимый шаг: self-deploy INITIATED-sentinel (ORCH-036; детач-деплой + поздний merge_pr в _handle_merge_verify идут под тем же маркером) либо держание merge-lease (ORCH-043) И активно бегущий актор (running-job). STOP никогда не трогает main/force-push/прод-контейнер/detached-процесс.
    • Фикс P1 (ORCH-090 review, attempt 2): deferred-cancel недостижим при STOP в ожидании Confirm Deploy → wedge. Для self-hosting merge-lease держится от merge-gate (ребро deploy-staging → deploy) до deploy → done, включая всё время, пока задача припаркована на deploy в ожидании ручного Confirm Deploy (Phase A) — но это окно полностью обратимо (ничего не смержено/задеплоено; необратимый merge_pr идёт позже в _handle_merge_verify уже под INITIATED). Прежде голое держание lease классифицировалось как «критичное» → STOP уходил в deferred-ветку, отмену применял бы только run_deploy_finalizer (после Phase B), которого оператор, нажавший STOP именно чтобы НЕ деплоить, никогда не запустит → отмена не применялась никогда, задача застревала нетерминальной с удержанным lease, клиня serial-gate репо (ORCH-088) и мержи. Фикс: merge-lease-ветка in_critical_window сужена — критично, лишь когда lease держится И есть бегущий актор (_task_has_running_actor, running-job); припаркованное окно без актора → НЕ критично → немедленный полный сброс (сам отпускает lease в шаге 3c). Новые тесты test_d7_lease_held_idle_parking_is_not_critical / test_d7_lease_held_with_running_actor_still_critical / test_d7_stop_on_deploy_awaiting_confirm_full_resets.
    • Кросс-каттинг (adr-0026): предикат «задача терминальна» расширен {done}{done, cancelled} в serial_gate.py (ORCH-088: repo_has_active_task, claim-фрагмент, snapshot), db.claim_next_job/get_unfinished_dependencies (task_deps ORCH-026) и stages.py-сток — иначе отменённая задача заклинила бы очередь репо (TR-1); reconciler-терминал-скип уже знал cancelled (ORCH-086 D2). job_reaper/queue_worker ПЕРЕД авто-requeue сверяют терминал задачи → помечают job cancelled, не реквью'ят (закрыта гонка SIGTERM/reaper, TR-2).
    • Закрытие дыры релонча (AC-5, D6): handle_status_start больше не релончит агента середины пайплайна при ручном переводе в промежуточный статус — relaunch ограничен стадией analysis (единственный владелец Needs Input, ORCH-066); единственный вход к запуску пайплайна остаётся «To Analyse» (start_pipeline). Под stop_status_enabled=false гейт инертен (1:1 как раньше).
    • Флаги/наблюдаемость: stop_status_enabled (kill-switch, env ORCH_STOP_STATUS_ENABLED) + stop_status_repos (CSV, пусто → все репо); leaf src/cancel.py (applies/in_critical_window/snapshot, never-raise); read-only блок stop в GET /queue; лог + Telegram (кликабельный номер) + Plane-коммент + update_task_tracker. Аддитивные идемпотентные миграции (_ensure_column для cancelled_at/cancel_requested_at). Инфра-предусловие: создать статус STOP с группой cancelled на доске Plane проекта ORCH (его отсутствие = fail-safe no-op).
    • Тесты: tests/test_stop_status.py (TC-01..TC-14 + D7-кейсы, включая 3 новых P1-кейса для окна «припаркован на deploy, ждёт Confirm Deploy»; SIGTERM/git/gitea замоканы — ни один тест не шлёт сигнал/не трогает сеть); обновлены анти-регресс-тесты STAGE_TRANSITIONS 5 прошлых задач (добавлен терминал-сток cancelled); полный регресс tests/ зелёный (1348). Документация: docs/architecture/README.md (статус «реализовано» + блок /queue + раздел БД), CLAUDE.md, README.md, .env.example. ADR: docs/work-items/ORCH-090/06-adr/ADR-001-stop-cancel-task.md, сквозной docs/architecture/adr/adr-0026-stop-cancel-task.md. Откат: ORCH_STOP_STATUS_ENABLED=false (аддитивные колонки/терминал-набор инертны при отсутствии отменённых задач).
  • Build-cache-pruner: авто-prune docker build cache на mva154 (ORCH-062, feat): новый фоновый daemon-поток src/build_cache_pruner.py (каркас disk_watchdog) — «вторая половина» disk-watchdog (ORCH-063): watchdog сигналит — pruner убирает. Устраняет корень инцидента 07.06.2026 (docker build cache ≈11 ГБ → диск mva154 100% → падение self-hosting-конвейера всех проектов) автоматически, без оператора. Аддитивно, never-raise: STAGE_TRANSITIONS/QG_CHECKS/check_*/_parse_*/src/stage_engine.py/схема БД — не тронуты, новой миграции нет (состояние last-run/last-result — in-memory, best-effort).
    • Периодическая уборка (FR-1/AC-1): каждые build_cache_prune_interval_s (дефолт 21600с = 6ч) тик выполняет строго docker builder prune -f --filter until=<until> (BuildKit GC). Анти-частота — pure-функция decide_prune(prev_run_ts, now, interval_s) (юнит-тестируема без потока/таймера, время инъецируется). Дефолт until=24h удерживает тёплый недавний кэш (BR-2/AC-2); -a/--all (build_cache_prune_all, дефолт False) — только в паре с возрастным фильтром.
    • Self-hosting безопасность (FR-3/AC-3): команда затрагивает только build cache — нет docker image prune/docker system prune, удаления образов/контейнеров запущенных сервисов, остановки/рестарта контейнеров; прод-контейнер orchestrator никогда не рестартится. Уборка исполняется на хосте через ssh (deploy_ssh_user@deploy_ssh_host, тот же канал, что image_freshness/self_deploy — в образе нет docker CLI). Нет ssh-таргета → тик no-op (наблюдаемо в status().last_error).
    • never-raise (FR-6/AC-4): per-команда (ненулевой rc / TimeoutExpired / OSError/FileNotFoundError / недоступность ssh / parsing-ошибка → лог + проглот, тик жив) и per-tick (внешний try/except в _run, как disk_watchdog). Фоновый цикл и конвейер не падают.
    • Конфигурируемость + kill-switch (FR-5/AC-5/AC-6): флаги build_cache_prune_enabled/_interval_s/_until/_all/_timeout_s/_notify_min_gb (src/config.py, env ORCH_BUILD_CACHE_PRUNE_*) с defensive-валидацией (интервал/таймаут >0, until ~ ^\d+[smhdw]?$, notify_min_gb ≥0 → невалидное к безопасному дефолту + warning, старт не падает). build_cache_prune_enabled=false → демон не стартует (старт/стоп в main.lifespan рядом с disk_watchdog, гард), GET /queue{"enabled": false} — поведение 1:1 как до задачи.
    • Наблюдаемость (FR-4/AC-7): аддитивный read-only блок build_cache_prune в GET /queue (enabled/interval_s/until/all/last_run_ts/last_reclaimed[+_bytes]/last_error); status() never-raise. Опц. Telegram при освобождении ≥ notify_min_gb ГБ (дефолт 0 = тихо). Тесты: tests/test_build_cache_pruner.py (TC-01..TC-12, 23 кейса, docker замокан — ни один тест не трогает реальный docker); полный регресс tests/ зелёный (1319). Документация: docs/operations/INFRA.md (секция авто-prune + env-карта; снята формулировка ORCH-063 «освобождение build cache — ручная операция»), docs/architecture/README.md, .env.example. ADR: docs/work-items/ORCH-062/06-adr/ADR-001-build-cache-pruner.md, сквозной docs/architecture/adr/adr-0025-build-cache-pruner.md. Откат: ORCH_BUILD_CACHE_PRUNE_ENABLED=false (миграций нет).
  • Disk-watchdog: мониторинг заполнения диска mva154 + Telegram-алерт при ≥85% (ORCH-063, feat): новый фоновый daemon-поток src/disk_watchdog.py (каркас reconciler/job_reaper) — недостающий проактивный сигнал о заполнении хост-диска (07.06.2026 диск mva154 тихо дорос до 100% и положил весь self-hosting-конвейер всех проектов). Аддитивно, never-raise: STAGE_TRANSITIONS/QG_CHECKS/check_*/схема БД — не тронуты, новой миграции нет (состояние анти-спама — in-memory).
    • Замер хост-ФС (FR-2/AC-8): каждые disk_monitor_interval_s (дефолт 300с) меряет заполнение смонтированных хост-bind-путей (/repos, /app/data) через stdlib shutil.disk_usageНЕ overlay / контейнера, НЕ субпроцесс df; дедуп путей по физическому устройству (st_dev) → один алерт на раздел. Недоступный путь → пропуск с warning, остальные пути меряются (per-path never-raise).
    • Решение об алерте (FR-3/FR-4/AC-2..AC-4): pure-функция decide_action(used_pct, threshold, prev_state, now, realert_s) (юнит-тестируема без потока/таймера, время инъецируется): алерт на пересечении порога (дефолт 85%, граница >= включительно), cooldown-повтор disk_monitor_realert_s (~6ч, анти-спам — не на каждом тике), однократный recovery при возврате ниже порога. Алерт — send_telegram (notifying, не silent), best-effort.
    • Конфигурируемость + kill-switch (FR-5/AC-5): флаги disk_monitor_enabled/_interval_s/_threshold_pct/_realert_s/_paths (src/config.py, env ORCH_DISK_MONITOR_*) с defensive-валидацией (порог 1..100, интервалы > 0 → невалидное к дефолту + warning). disk_monitor_enabled=false → демон не стартует (старт/стоп в main.lifespan, гард), GET /queue{"enabled": false} — поведение 1:1 как сейчас.
    • Наблюдаемость (FR-6/AC-7): аддитивный read-only блок disk_monitor в GET /queue (enabled/threshold_pct/interval_s/realert_s/last_run_ts/paths[used_pct/free_gb/free_pct/alerting/last_alert_at]); существующие ключи /queue не изменены; status() never-raise.
    • Self-hosting безопасность (NFR-6): watchdog только читает заполнение и шлёт уведомление — не трогает диск/контейнер, не рестартит прод; безопасен для enduro-trails в общем инстансе. Откат тривиален (ORCH_DISK_MONITOR_ENABLED=false, миграций нет). Тесты: tests/test_disk_watchdog.py (TC-01..TC-12, 18 кейсов); полный регресс tests/ зелёный (1296). Документация: docs/architecture/README.md (компонент + блок /queue), docs/operations/INFRA.md (что мониторится/порог/как отключить/реакция на алерт), .env.example. ADR: docs/work-items/ORCH-063/06-adr/ADR-001-disk-watchdog.md, сквозной docs/architecture/adr/adr-0024-disk-watchdog.md.
  • Промпт-аудит 6 агентов: расхардкод даты/модели, сверка гейтов, escalation, чистка (ORCH-092 / эпилог эпика ORCH-52, docs): точечная правка 6 системных промптов .openclaw/agents/*.md + анти-регресс-тестов, устраняющая класс дефектов промптов (хардкод даты/модели в примерах, размазанная эскалация, нереализуемая/конфликтующая инструкция rebase, мёртвая инструкция reviewer, недообогащённый tester). Docs/prompts-only: src/**, STAGE_TRANSITIONS, QG_CHECKS, состав machine-verdict ключей и схема БД — не тронуты; frontmatter_validation_strict остаётся False. Машинные verdict-ключи (verdict:/result:/staging_status:/deploy_status:/security_status: + значения APPROVED/REQUEST_CHANGES/PASS/FAIL/SUCCESS/FAILED) и канон 52d/52c/52e (5 секций, 6 полей) — байт-в-байт.
    • Расхардкод даты/модели (FR-1/FR-2, AC-1/AC-2): во всех 6 промптах копируемые примеры frontmatter несут плейсхолдеры created_at: <YYYY-MM-DD> / model_used: <resolve ORCH-41> + явную врезку «не копируй буквально: подставь date +%F и фактическую модель из конфига». Литерал claude-opus-4-8 остаётся лишь как справка в таблице полей (вне копируемого блока).
    • Сверка имён гейтов (FR-3, AC-3): все check_* в 6 промптах сверены с реестром QG_CHECKS — несовпадений нет (check_tests_passed подтверждён валидным, не «исправлен вслепую»); закреплено интеграционным тестом.
    • developer (FR-4/FR-5/FR-9): « PR>1500 → разбивай на меньшие PR» переформулирован в эскалацию (слишком большой PR = декомпозиция на уровне задач, 1 задача = 1 ветка = 1 PR); добавлена секция <escalation> (негодное ТЗback-to:analysis; новая развилка → к архитектору); убран ручной git rebase origin/main из алгоритма (ADR-001 D1: свежесть базы — инвариант движка serial-gate ORCH-088 + auto_rebase_onto_main под merge-lease, а не ручная мутирующая операция агента, конфликтующая с запретом force-push).
    • reviewer (FR-5/FR-8): удалена мёртвая инструкция «не апрувь PR от того же экземпляра Developer» (защита от несуществующего кейса — reviewer всегда отдельный agent-run); добавлена секция <escalation> (любой P0/P1 → REQUEST_CHANGES). Живые инварианты (REQUEST_CHANGES, «НЕ обновлена», ось трассировки, ось обзорных доков ORCH-079) сохранены.
    • tester (FR-5/FR-7): обогащён — тесты гоняются в worktree ветки задачи (а не в общем /repos/orchestrator → исключена гонка checkout); smoke /queue проверяет наличие блока serial_gate (ORCH-088); <success_criteria> требует покрытия каждого TC из 04-test-plan.yaml; добавлена секция <escalation> (обоснованный FAIL → back-to:dev; смок-сбой инфры → FAIL с диагностикой).
    • deployer (FR-6/FR-10): критичные self-hosting-запреты подняты в видную рамку в начале <context> («NEVER restart prod 8500», запрет docker compose up/правок инфры); язык оставлен английским как зафиксированное исключение канона (ADR-001 D2: самый safety-critical промпт, минимизация регресс-поверхности; перевод не несёт выгоды и угрожает байт-точности ключей/команд). Анти-регресс-маркеры (docker exec orchestrator-staging, pr_already_merged, 8500, INFRA-WAIVED) сохранены.
    • Анти-регресс (FR-11): в tests/test_agent_prompts_canon.py добавлены структурные TC (плейсхолдеры даты/модели в копируемых блоках; сверка гейтов с QG_CHECKS; <escalation> у developer/reviewer/tester после </success_criteria>; переформулировка PR-инструкции; обогащение tester; рамка deployer; удаление мёртвой строки reviewer). Существующие проверки канона 52d и test_agent_frontmatter_no_model.py — зелёные; полный регресс tests/ зелёный (1278). Документация: 6 промптов, CLAUDE.md, docs/architecture/README.md. ADR: docs/work-items/ORCH-092/06-adr/ADR-001-developer-rebase-and-deployer-language.md. Полностью обратимо git revert (нет машинного поведения/состояния).
  • Синхронизация обзорных доков (README) с кодом + reviewer-ось «обзорные доки» (ORCH-079 / ORCH-52f, docs): слой 5 (финал) эпика ORCH-52, замыкающий цепочку 52b (структура) / 52c (frontmatter) / 52d (промпты) / 52e (трассировка). Корневой README.md — обзорная витрина проекта — выдавал решённое за открытое: секция «Известные ограничения» имела битую нумерацию (1,2,3,4,3,4) и пункты, опровергнутые кодом. Docs + prompt-only: src/**, STAGE_TRANSITIONS, QG_CHECKS, check_*/_parse_*, src/frontmatter.py, схема БД — не тронуты; frontmatter_validation_strict остаётся False; новый QG не вводится; правило обзорных доков нормативно-описательное (не машинный гейт), как ось трассировки ORCH-078.
    • README.md приведён в честное состояние по коду (FR-1/FR-2/FR-3, AC-1/AC-2/AC-3): перенумерация «Известные ограничения» сквозная без повторов; 6 решённых/устаревших пунктов перенесены в трейл «Закрыто (история)» с ORCH-ссылками (worktree → ensure_worktree+ORCH-026/088; in-process daemon → очередь ORCH-1; «Gitea CI не настроен» → check_ci_green; «no retry» → backoff/breaker queue_worker.py+ORCH-045; issue-ID → зрелый plane_sync ORCH-010/066/068; Playwright-timeout → watchdog ORCH-7); в «открытых» — только реально открытые, верифицированные кодом/задачей (Telegram-48h ORCH-087, task-deps intra-repo v1 ORCH-026, serial-gate Этап 1 ORCH-088). Точечная сверка с кодом: стадия development в таблице — check_ci_green (был устаревший check_tests_local); строка event-routing status — авторитетный гейт развития check_ci_green (ORCH-045), убран legacy-текст «больше не authoritative».
    • Reviewer-ось «обзорные доки» (FR-5, AC-5): .openclaw/agents/reviewer.md ось 4 «Документация» (<task>) + <constraints> несут точечную врезку «» (канон 52d): PR закрыл пункт README «Известные ограничения», README не обновлён → finding ≥P1; при закрытии правкой src/ без обновления README — совпадает с существующим P0. Машинный ключ verdict: APPROVED|REQUEST_CHANGES — байт-в-байт; 5 XML-секций и 6 полей схемы 52c сохранены. Правило в одном промпте (без выноса в docs/_standards/, в отличие от 52e).
    • Эпик ORCH-52 закрыт: 52b (adr-0019) → 52c (adr-0020) → 52d (adr-0021) → 52e (adr-0022) → 52f (adr-0023). Сквозной docs/architecture/adr/adr-0023-overview-docs-reviewer-axis-and-epic52-close.md + per-work-item docs/work-items/ORCH-079/06-adr/ADR-001-readme-sync-and-reviewer-overview-docs-axis.md.
    • Анти-регресс (FR-6, AC-6): новый структурный tests/test_readme_limitations.py (нумерация без повторов; решённые пункты не значатся открытыми; трейл «Закрыто» с ORCH-ссылками); расширен tests/test_agent_prompts_canon.py (assert наличия оси обзорных доков в reviewer.md); канон 52d (5 секций, 6 полей, регистр verdict-ключей) и test_agent_frontmatter_no_model.py зелёные; полный регресс tests/ зелёный (1257). Документация: README.md, docs/architecture/README.md (слой 5 эпика 52), CLAUDE.md. Полностью обратимо git revert (нет машинного поведения/состояния/kill-switch).
  • Стандарт маркеров-трассировки ORCH-NNN + правило чтения ADR перед правкой (ORCH-078 / ORCH-52e, docs): слой 4 (трассировка) эпика ORCH-52, замыкающий цепочку 52b (структура) / 52c (frontmatter) / 52d (промпты). Маркеры ORCH-NNN/ET-NNN в коде (де-факто 51 уникальный в src/) привязывают нетривиальные инварианты к породившему их work item — была сложившаяся практика без формального контракта. Docs + prompts-only: src/**, STAGE_TRANSITIONS, QG_CHECKS, check_*/_parse_*, src/frontmatter.py, схема БД — не тронуты; frontmatter_validation_strict остаётся False; новый QG не вводится; массовый ретро-фит 51 маркера вне объёма (стандарт нормативен «на будущее»).
    • Новый стандарт docs/_standards/TRACEABILITY.md (рядом с PIPELINE_DOCS.md/HANDOFF_PROTOCOL.md): формат маркера, правило размещения (рядом с нетривиальным инвариантом), чтение истории с реальным проверяемым примером (src/serial_gate.py → ORCH-088 → ADR-001-serial-gate.md), fallback-доступ (git show origin/main:docs/work-items/...), анти-археология (3+ маркеров → сводный сквозной ADR), каноничный текст правила чтения (единый источник).
    • Точечные врезки в промпты (аддитивно, 52d-канон не переписан): developer.md — правило чтения чужого маркера + fallback (« X → Y»); architect.md — правило чтения + анти-археология (3+ → сквозной ADR); reviewer.md — усиление оси «Соответствие ADR» под-пунктом «правка маркированного кода сверена с ADR; слом → finding ≥P1». Все три ссылаются на единый текст в TRACEABILITY.md, не копируют (анти-дубль BR-6).
    • Сопутствующе: CLAUDE.md (правило трассировки + ссылка), docs/architecture/README.md (слой 4 эпика 52), сквозной adr-0022 + per-work-item ORCH-078/06-adr/ADR-001. Анти-регресс: расширен tests/test_agent_prompts_canon.py (наличие правила/ссылок в 3 промптах, существование примера в стандарте); проверки 52d (5 секций, 6 полей, регистр verdict-ключей) и test_agent_frontmatter_no_model.py остаются зелёными. Полностью обратимо git revert (нет машинного поведения/состояния/kill-switch).
  • Канон Anthropic для 6 системных промптов + добровольная эмиссия frontmatter-схемы 52c (ORCH-077 / ORCH-52d, docs): замыкающий слой эпика ORCH-52. 52c заложила writer + валидатор обязательной схемы (REQUIRED_FIELDS), но он работал warning-only «вхолостую» — 6 промптов .openclaw/agents/*.md не эмитили поля схемы. ORCH-077 учит все 6 промптов её эмитить и переписывает их в едином каноне Anthropic. Docs/prompts-only: src/**, STAGE_TRANSITIONS, QG_CHECKS, состав machine-verdict ключей и схема БД — не тронуты; frontmatter_validation_strict остаётся False (эмиссия добровольная, enforcement НЕ включён).
    • Единый XML-скелет (5 обязательных секций, нормативный порядок): <context><task> (+ опц. <thinking> у решающих ролей: architect/reviewer/tester/deployer) → <deliverables><constraints> (запреты « X → Y») → <output_format>. Доп. секции (<success_criteria>/<escalation>) — после пяти обязательных.
    • Аддитивная схема 52c: <output_format> каждого промпта перечисляет 6 полей (work_item/stage/author_agent/status/created_at/model_used) с роле-специфичными значениями (stage/author_agent по карте ролей; model_used: claude-opus-4-8 по резолву ORCH-41) и ставит их рядом с machine-verdict ключом, не меняя его имя/регистр/значения (verdict: APPROVED|REQUEST_CHANGES; result: PASS|FAIL; staging_status:/deploy_status: SUCCESS|FAILED; security_status: PASS|FAIL). Для 04-test-plan.yaml — top-level YAML-ключи. Гейты читают вердикты 1:1 как раньше (NFR-1).
    • Loading-model: промпт cat-ается из git-worktree агента в момент запуска (launcher --system-prompt "$(cat .openclaw/agents/<role>.md)"), НЕ запекается в образ → новые промпты вступают в силу на следующем worktree от main без прод-рестарта; reviewer/tester той же задачи исполняются уже под новыми промптами (in-vivo A/B, BR-6).
    • Анти-регресс (критично, self-hosting): функциональное содержание старых промптов перенесено 1:1 (инвентарь TRZ §FR-6 — Write-tool/4 deliverable у analyst; ADR-формат/сквозной ADR/эскалация у architect; TDD/«не мержить свой PR»/--no-verify/--force-push/«не рестартить прод» у developer; правило «src/ изменён, доки нет → REQUEST_CHANGES» у reviewer; pytest+smoke /health//status//queue у tester; canonical docker exec orchestrator-staging … staging_check.py, B6-обоснование, ORCH-061 INFRA-WAIVED, merge-guard pr_already_merged, «не рестартить 8500 изнутри» у deployer). Защита — структурные тесты tests/test_agent_prompts_canon.py (TC-01…TC-07: 5 XML-секций, 6 полей схемы, точный регистр verdict-ключей, роле-специфичные author_agent/stage, ссылки на docs/_templates/+эталоны ORCH-073/088, self-hosting-маркеры deployer); существующий tests/test_agent_frontmatter_no_model.py (ORCH-074) остаётся зелёным (frontmatter промпта name/description/tools сохранён, model: нет).
    • A/B (BR-6/AC-6): метод зафиксирован в tests/manual/ab_prompt_compare.md (in-vivo: reviewer/tester самой ORCH-077 уже под новыми промптами); результат «новый не хуже» фиксирует тестер в 13-test-report.md. Обратимость: git revert PR — нет миграций/состояния. Норматив на будущее: новые/изменённые агент-промпты следуют этому канону.
    • Документация: .openclaw/agents/{analyst,architect,developer,reviewer,tester,deployer}.md, CLAUDE.md, docs/architecture/README.md. ADR: docs/work-items/ORCH-077/06-adr/ADR-001-anthropic-prompt-canon.md, сквозной docs/architecture/adr/adr-0021-prompt-canon-anthropic.md.
  • Единый frontmatter-контракт (reader + writer + валидатор) + спека handoff (ORCH-076 / ORCH-52c, refactor/docs): слой 2 эпика ORCH-52 — src/frontmatter.py из single-key reader превращён в полный машинный контракт, а разрознённое чтение вердиктов пяти гейтов сведено к одной точке парсинга. Строго обратно совместимо, never-raise; STAGE_TRANSITIONS / состав QG_CHECKS / семантика вердиктов / fallback worktree→origin/main / трёх-полевой контракт tester (ORCH-047) — 1:1, без изменений.
    • src/frontmatter.py (контракт): сохранён reader read_frontmatter_value (контракт неизменен — внешние вызыватели usage.py / notifications.build_status_comment не затронуты, INV-3); добавлены единый парс-примитив parse_frontmatter(content) -> FrontmatterParse (data/has_block/malformed/yaml_error — единственная точка YAML-логики) + ярлыки parse_frontmatter_dict / read_frontmatter; writer render_frontmatter/write_frontmatter (формат байт-совместим с split("---",2)+yaml.safe_load, round-trip render→parse); валидатор схемы validate_schema/SchemaValidation/REQUIRED_FIELDS (work_item/stage/author_agent/status/created_at/model_used); общий strip_frontmatter. Весь модуль — never-raise (NFR-2): любая ошибка I/O/YAML/сериализации → лог + безопасное значение ({}/False/исходный текст).
    • Унифицирован МЕХАНИЗМ, а не семантика (D2): пять вердикт-парсеров — check_reviewer_verdict (verdict:), _parse_tests_verdict (result:/verdict:/status:, ORCH-047), _parse_deploy_status (deploy_status:), _parse_staging_status (staging_status:) в src/qg/checks.py; parse_security_status (security_status:) в src/security_gate.py — заменили дублированный блок startswith/split/safe_load/isinstance на parse_frontmatter(content); token-логика, upper-casing, приоритет негативного токена, reason-строки — сохранены 1:1. Также сняты дубли в security_gate.extract_security_findings и review_parse._strip_frontmatter (делегируют strip_frontmatter).
    • Валидатор не hard-fail по умолчанию (D3, критично для self-hosting): maybe_warn_schema при дефолте только логирует logger.warning("frontmatter schema incomplete: …") и никогда не влияет на boolean-вердикт гейта (инертен). Жёсткий режим — ТОЛЬКО под kill-switch frontmatter_validation_strict (env ORCH_FRONTMATTER_VALIDATION_STRICT, дефолт False; остаётся False в проде/.env.staging, иначе ORCH-52c self-block'нулась бы — её доки без полной схемы). Схема аддитивна: старый док-вердикт без новых полей читается ровно как раньше (FR-5/AC-4).
    • Спека handoff: новый docs/_standards/HANDOFF_PROTOCOL.md — формальный контракт «стадия → обязательные документы + frontmatter-ключи на выходе» + обязательная схема (REQUIRED_FIELDS), согласован 1:1 с PIPELINE_DOCS.md §2§3; PIPELINE_DOCS.md §5§6 обновлён (слой 2 реализован, ссылка на спеку и src/frontmatter.py).
    • Без изменений API / схемы БД (INV-5). Тесты: tests/test_frontmatter.py (TC-01…TC-07: writer/round-trip/валидатор/strict/never-raise/reader), tests/test_qg_verdicts.py (TC-08…TC-15: семантика пяти гейтов 1:1, обратная совместимость, fallback origin/main), tests/test_security_gate.py (TC-12), tests/test_stages_invariants.py (TC-16: QG_CHECKS/STAGE_TRANSITIONS неизменны). Полный регресс tests/ зелёный (1212). Конфиг: src/config.py (frontmatter_validation_strict). Документация: CLAUDE.md, docs/architecture/README.md. ADR: docs/work-items/ORCH-076/06-adr/ADR-001-frontmatter-contract.md, сквозной docs/architecture/adr/adr-0020-frontmatter-contract.md.
  • Стандарт документов конвейера: docs/_standards/PIPELINE_DOCS.md + docs/_templates/ + ADR-naming (ORCH-075 / ORCH-52b, docs): зафиксирован golden source структуры номерных документов work item (00-business-request.md17-security-report.md). Docs-only, нулевой рантайм-риск: STAGE_TRANSITIONS / QG_CHECKS / check_* / src/stage_engine.py / схема БД — не трогаются (изменения только под docs/** + CLAUDE.md).
    • Манифест docs/_standards/PIPELINE_DOCS.md — карта «стадия → агент → документ → категория (required/when-applicable/optional) → гейт/механизм → frontmatter machine-key», сверенная с src/stages.py (STAGE_TRANSITIONS) и src/qg/checks.py (_parse_*). Манифест документирует поведение гейтов, но НЕ источник истины (источник — код, ADR-001 §D2); честно различает machine-verdict доки (12verdict:, 13result:, 14deploy_status:, 15staging_status:, 17security_status:) и информационные (00/08/10/16 — гейтом не парсятся). Под-гейты ребра deploy-staging→deploy (security/merge/image-freshness) помечены как врезки в advance_stage, а не строки STAGE_TRANSITIONS.
    • Шаблоны docs/_templates/* (15 копируемых скелетов) — для каждого required/when-applicable дока; машинные доки несут точный frontmatter-ключ из ground-truth (_parse_*), чтобы скопированный скелет проходил гейт без угадывания. Служебные каталоги docs/_standards/ / docs/_templates/ лежат ВНЕ docs/work-items/<plane-id>/ → невидимы гейтам наличия файлов (check_architecture_done/check_analysis_complete).
    • ADR-naming зафиксирован: docs/work-items/<plane-id>/06-adr/ADR-NNN-<kebab-slug>.md (NNN с 001); сквозные решения дублируются в docs/architecture/adr/adr-NNNN-<slug>.md (4-значная нумерация). Точки-ссылки: CLAUDE.md (раздел «Артефакты задачи» + правило 2), docs/architecture/README.md (раздел «Стандарт документов конвейера»). Тесты: tests/test_orch_52b_docs_standard.py (TC-01…TC-20, структурные проверки наличия/секций/frontmatter). ADR: docs/work-items/ORCH-075/06-adr/ADR-001-pipeline-docs-standard.md, сквозной docs/architecture/adr/adr-0019-pipeline-docs-standard.md.
  • Авто-режим по лейблам: autoApprove (авто-BRD) + autoDeploy (авто-деплой) (ORCH-089, feat): сняты два человеческих гейта конвейера, тормозящих пакетный автономный прогон (эпик ORCH-088) — гейт BRD (analysis: ручной Approved) и гейт прод-деплоя (deploy Phase A: ручной Confirm Deploy, ORCH-059). Решение выборочно (лейбл Plane на задаче), декларативно, обратимо и не трогает ни одной технической проверки. Аддитивно по образцу условных под-гейтов (ORCH-035/043/058/088): leaf src/labels.py (never-raise) + две точечные врезки + флаги; STAGE_TRANSITIONS/QG_CHECKS/check_*/схема БД — без изменений.
    • autoApprove → врезка в stage_engine._handle_analysis_approved_flow (ветка files_ok) ПОСЛЕ In Review+коммента: set_issue_approved (индикация) + лог/Telegram/Plane-коммент + advance_stage(..., finished_agent=None)тот же путь, что человеческий Approved (approved-via-statusanalysis → architecture + mark_brd_review_ended). Без дублирования переходной логики; re-entrancy безопасна (вложенный вызов идёт с finished_agent=None, не входит в analyst-ветку).
    • autoDeploy → врезка в stage_engine._handle_self_deploy_phase_a сразу после advance на deploy+clear_state (ДО «ask-human»): лог/Telegram/Plane-коммент + _handle_self_deploy_phase_b(...) (idempotency-маркер INITIATED, статус Deploying, finalizer). Пропускаются лишь индикативно-человеческие шаги (APPROVE_REQUESTED+Awaiting Deploy+«смените на Confirm Deploy»). BR-5 структурно: Phase A достигается только после зелёных под-гейтов ребра deploy-staging → deploy (security → merge-gate → image-freshness → staging) → autoDeploy физически не деплоит сломанное.
    • Чтение лейбловplane_sync.fetch_issue_labels (поле labels issue; None при ошибке ≠ []) + get_project_labels ({normalized_name→uuid}, TTL-кэш auto_label_states_ttl_s по образцу get_project_states); сопоставление по нормализованному имени (strip().casefold()), неоднозначность (две метки → одно нормализованное имя) → сентинел __AMBIGUOUS__ → «нет лейбла». Новый сеттер set_issue_approved (ключ approved уже в _DEFAULT_STATES). Источник истины — Plane API, не payload вебхука.
    • Флаги (config.py): auto_label_enabled (kill-switch), auto_approve_label/auto_deploy_label, auto_label_repos (CSV; пусто → self-hosting only), auto_label_states_ttl_s. applies(repo) (локальный) проверяется ПЕРВЫМ; has_label (сеть) — только при applies==True → при выключенном флаге нулевой сетевой оверхед, нулевая регрессия для enduro (AC-8).
    • Fail-safe (never auto): любая ошибка/недоступность Plane/неоднозначность → «нет авто» → ручной гейт (never-raise, AC-6). Прозрачность (AC-7): лог + Telegram + Plane-коммент + live-карточка через штатный advance. Read-only блок auto_labels в GET /queue.
    • Инфра-предусловие: создать лейблы autoApprove/autoDeploy в Plane-проекте ORCH (labels API); их отсутствие = has_label False = ручной режим (fail-safe). Детали — docs/work-items/ORCH-089/07-infra-requirements.md.
    • Тесты: tests/test_labels.py, test_plane_sync_labels.py, test_auto_approve_brd.py, test_auto_deploy.py, test_auto_label_combinations.py, test_auto_labels_integration.py, test_auto_labels_invariants.py (TC-01…TC-26). ADR: docs/work-items/ORCH-089/06-adr/ADR-001-auto-label-gates.md, global docs/architecture/adr/adr-0018-auto-label-gates.md.
  • Per-repo serial gate: пакетный автономный режим (Этап 1, serial e2e) (ORCH-088, feat): закрыт логический stale-анализ — ветка задачи N+1 срезалась на входе в анализ (start_pipeline._create_gitea_branch) от main, ещё не содержащего код предшественника N (физическое затирание уже закрыто ORCH-026). Новая задача репо не входит в analysis (не режет ветку, не запускает analyst), пока в репо есть незавершённая задача или репо заморожен. Аддитивно, под kill-switch, область репо, never-raise, restart-safe; STAGE_TRANSITIONS/QG_CHECKS/check_*без изменений.
    • Gate-в-claim (db.claim_next_job): analyst-job (jobs.agent='analyst') применимого репо не выбирается, если EXISTS более ранняя незавершённая задача репо (t2.id < jobs.task_id) ИЛИ активна строка repo_freeze. Фрагмент строится в leaf src/serial_gate.py::build_claim_clause (санитизация repo-токенов ^[A-Za-z0-9._-]+$, fail-OPEN на любой ошибке построения — не заклинить очередь всех проектов, AC-8); только локальная БД (offline hot-path, NFR-2). Job'ы уже активной задачи проходят свободно. FIFO-уточнение реализации (FR-2): ADR-001 D1 фиксировал псевдо-SQL t2.id != jobs.task_id; при != пакет одновременно созданных свежих задач (все в analysis) взаимно блокировался бы → дедлок всей serial-очереди (воспроизведено). < допускает ровно самую раннюю задачу и сериализует остальные за ней (строго по одной, FIFO по jobs.id), сохраняя AC-1 и не блокируя rework-analyst собственной задачи (R-7).
    • Отложенный срез ветки (анти-stale-base, AC-6): для применимого репо start_pipeline создаёт task-row + enqueue analyst, но не создаёт Gitea-ветку/docs; срез релоцирован в launcher._spawn (новый _materialize_deferred_branch, sync через asyncio.run в worker-потоке, R-4) на момент claim analyst-job, когда origin/main уже содержит предшественника (done ⇔ SHA-в-main, ORCH-071/073). ensure_worktree режет от свежего origin/main ⇒ AC-6 структурно. Идемпотентно (_create_gitea_branch 409 / _create_initial_docs 422 = no-op) → безопасно при реклейме/рестарте. Ожидающая задача = queued analyst-job без ветки; tasks.branch хранится как имя (R-5).
    • Durable per-repo freeze (FR-5): новая аддитивная append-only таблица repo_freeze(id, repo, frozen_at, reason, work_item_id, cleared_at) (CREATE TABLE/INDEX IF NOT EXISTS в init_db, идемпотентно, restart-safe). Post-deploy DEGRADED (stage_engine.run_post_deploy_monitor) → serial_gate.set_repo_freeze + Telegram-алерт «пакет заморожен»; gate закрыт безусловно (деградировавшая задача уже done, BR-7 ⇒ отдельный сигнал, независимый от stage) до ручного снятия — новый эндпоинт POST /serial-gate/unfreeze?repo=<repo> (clear_repo_freeze, идемпотентно, + Telegram-подтверждение; альтернатива — UPDATE repo_freeze SET cleared_at=datetime('now') …). freeze в Python-слое (is_repo_frozen) → fail-CLOSED (безопасность прода, AC-9). Независимый тумблер serial_gate_freeze_enabled.
    • Конфигурация (src/config.py): serial_gate_enabled (kill-switch, ORCH_SERIAL_GATE_ENABLED, дефолт true → claim+start_pipeline 1:1 как сейчас при false), serial_gate_repos (CSV, ORCH_SERIAL_GATE_REPOS; пусто ⇒ все репо, в отличие от self-hosting-only ORCH-35/43/58; оператор может сузить), serial_gate_freeze_enabled (ORCH_SERIAL_GATE_FREEZE_ENABLED). Наблюдаемость — аддитивный блок serial_gate в GET /queue (per-repo active_task/waiting/frozen+reason+at); существующие ключи не меняются. NFR-6: freeze — пассивная остановка стартов, прод-контейнер не рестартится/не роняется. Cross-repo параллелизм сохранён (FR-3/AC-4); при выключенном флаге — нулевая регрессия (enduro не затронут, AC-7). ADR docs/work-items/ORCH-088/06-adr/ADR-001-serial-gate.md, данные 08-data-requirements.md, сквозной adr-0017. Документация: docs/architecture/README.md (раздел serial gate + /queue + таблица API + раздел БД), CLAUDE.md. Тесты: tests/test_serial_gate.py (TC-01/02/03/08/15/16/17/19/21), tests/test_serial_gate_e2e.py (TC-04/05/06), tests/test_serial_gate_freeze.py (TC-07/09/10/11/12/18/22), tests/test_serial_gate_branch.py (TC-13/14), tests/test_queue_endpoint.py (TC-20).
  • CI-фикс: per-run путь логов из хардкода /app/data/runs в settings.runs_dir (ORCH-087, fix): тест tests/test_launcher.py::TestEffortStamp::test_spawn_stamps_resolved_effort падал в CI (PermissionError: [Errno 13] … '/app') — зелёный локально-в-контейнере (где /app есть), красный на CI-хосте (act_runner hostexecutor, юзер без доступа к /app). Корень: launcher._spawn хардкодил output_path="/app/data/runs/{run_id}.log" + os.makedirs('/app/data/runs'), а тест дёргал _spawn, не замокав путь → makedirs на недоступном /app бросал. Фикс (корень, не только тест): базовый каталог per-run логов вынесен в Settings.runs_dir (env ORCH_RUNS_DIR, дефолт /app/data/runs — прод-layout 1:1); новый хелпер launcher._run_log_path(run_id) = <settings.runs_dir>/{run_id}.log стал единым источником пути (использован в _spawn + три прежних inline-строки логов/алертов). Тест monkeypatch-ит settings.runs_dir на tmp_path → окружение-независим (подтверждено прогоном с принудительно недоступным /app). STAGE_TRANSITIONS/QG_CHECKS/схема БД — без изменений. Документация: README.md (таблица env), CHANGELOG.md.
  • Live-трекер: зачистка осиротевших карточек + эффорт в строке стадии + честное итоговое время (ORCH-087, fix): в чат периодически попадали «замёрзшие» сироты — старая карточка с заголовком 📍 To Analyse висела на задаче, реально дошедшей до deploy (скриншот ORCH-082). Корень (G0/ADR-001): указатель tasks.tracker_message_id — скаляр (знает лишь ПОСЛЕДНИЙ message_id), поэтому при рассинхроне bump-режима (доминанты: гонка двух update_task_tracker и delete-fail+send-ok) ссылка на прежнюю карточку терялась навсегда → сирота не удалялась и больше не обновлялась (рендер исправен — застывал именно потерянный mid). Фикс (bump сохранён дефолтом — фича «карточка внизу» ORCH-042/067):
    • G1 — полный учёт mid: аддитивная таблица-леджер tracker_messages(task_id, message_id, created_at, deleted_at) (src/db.py) + хелперы add_tracker_message/get_open_tracker_messages/mark_tracker_message_deleted. На каждом bump зачищаются ВСЕ незакрытые mid (deleted_at IS NULL), а не только скаляр: успех/«already gone» (_DELETE_GONE_MARKERS) → deleted_at; transient-delete → остаётся для ретрая; новый mid в леджер + set_tracker_message_id ТОЛЬКО при успешном send (R-3/BR-6). Остаточная гонка самозалечивается за один переход (лок не вводится). Скаляр tracker_message_id сохранён (BC). Known-limitation: Telegram 48ч (сироты старше неудаляемы).
    • G3 — deploy-цикл: в _LIVE_BRANCH_LABELS добавлен ключ confirm_deploy Confirm Deploy — подтвердите прод-деплой», без base-alias) → полнота Awaiting Deploy → Deploying → Confirm Deploy → Monitoring → Done.
    • BR-EFF — эффорт в строке стадии: новая колонка agent_runs.effort TEXT (_ensure_column, идемпотентно); стамп фактического resolve_agent_effort в launcher._spawn в момент запуска (CLI эффорт в result-JSON не возвращает); рендер · {model} · {effort} (developer=xhigh, tester/deployer=medium, прочие=high); пустой effort → суффикс опускается.
    • BR-G5 — честное итоговое время: done-строка ⏱️ Агенты {Σ agent_runs} · твоё {review~cap} · общее с ожиданием {wall} — три независимых подписанных метрики (раньше Всего {wall} читалось как сумма, которой не является — queue-паузы не логируются). «Твоё» ограничено порогом tracker_brd_review_cap_s (env ORCH_TRACKER_BRD_REVIEW_CAP_S, дефолт 2ч; маркер ~ при отсечке аномального застоя из-за рассинхрона In Review→Backlog); wall подписан «с ожиданием».
    • Инварианты: STAGE_TRANSITIONS/QG_CHECKS/стадии — без изменений; миграции аддитивны/идемпотентны (общая прод-БД, enduro не трогается); never-raise, disable_notification, plane_issue_link (ORCH-067), disable_web_page_preview (ORCH-080) сохранены; src/reconciler.py не эродирован (ORCH-086 на месте). Тесты: tests/test_notifications_orphans.py (TC-01..05 + never-raise), tests/test_tracker_effort_time.py (TC-06/11..15 + confirm_deploy), tests/test_launcher.py::TestEffortStamp (TC-09/10). ADR docs/work-items/ORCH-087/06-adr/ADR-001-tracker-orphan-cleanup.md.
  • Терминал-скип и state_uuid-dedup на пути F-1 реконсилятора (ORCH-086, fix): в Telegram периодически (особенно после рестарта орка) прилетало ложное 🔧 reconciler: ET-002 done разблокирована (потерян webhook) — задача давно завершена, ничего не разблокируется, это шум. Корень: ORCH-068 закрыл livelock только на F-2 (plane-side); путь F-1 (gate-side) остался непокрытым по двум причинам — (A) вызов _note_unblock(work_item_id, stage) шёл без state_uuid, поэтому in-memory dedup пропускался; (B) единственным «терминал-фильтром» F-1 была выборка get_active_tasks_for_reconcile (WHERE stage != 'done'), не знающая о статусе issue в Plane — задача с дрейфом «БД орка не-done, а Plane уже Done» проходила фильтр, no-op условные гейты (enduro) давали зелёный → advance → ложное уведомление. Фикс (ADR-001, локализован в src/reconciler.py): (D1) новый _resolve_issue_status(task) делает один сетевой резолв Plane-статуса задачи за тик (states, groups, state_uuid) после дешёвых локальных гардов (busy/young/escalated в Plane не ходят), never-raise → ({}, {}, None) при сбое; (D2) безусловный терминал-скип ДО Guard 2 — терминальная задача (группа Plane completed/cancelled, fallback на логические ключи done/cancelled, ЛИБО стадия в БД орка ∈ {done, cancelled}, т.к. cancelled не отсекается выборкой) → ранний return + skipped_terminal_total++, не подчинён reconcile_skip_blocked_enabled (тот гейтит только Guard 2); (D3) _is_blocked_or_needs_input переиспользует резолв D1 (3-й/4-й опц. аргументы; при _UNSET — самостоятельный резолв для прямых/легаси-вызовов, поведение 1:1); (D4) вызов _note_unblock на F-1 теперь передаёт state_uuid → dedup работает и на F-1 (повтор того же issue_id+state_uuiddeduped_total++, без второго Telegram). Терминальность — тот же _is_terminal_state, что и в F-2 (первичный дискриминатор — группа Plane, устойчив к UUID-алиасингу/мультипроектности; покрывает enduro и orchestrator). Анти-регресс (AC-4): легитимный unblock реально застрявшей не-терминальной задачи по-прежнему advance + ровно один Telegram (unblocked_total++). STAGE_TRANSITIONS, QG_CHECKS, схема БД, сигнатуры advance_stage/advance_if_gate_passed/_note_unblock, форма status()/GET /queue, новые config-флаги — без изменений; never-raise сохранён. ADR docs/work-items/ORCH-086/06-adr/ADR-001-reconciler-f1-terminal-skip-and-dedup.md. Тесты: tests/test_reconciler.py (TC-86-01..09/11: терминал по группе completed/cancelled, fallback по логическому ключу, DB-side cancelled, проброс/dedup state_uuid, анти-регресс, never-raise, независимость от Guard-2-флага), tests/test_reconciler_plane.py (TC-86-10: форма status() неизменна). Документация: docs/architecture/README.md (раздел Reconciler F-1).
  • Подавление Telegram link-preview в карточке трекера / уведомлениях (ORCH-080): под каждой карточкой трекера (bump и edit) и под notify/alert-сообщениями Telegram разворачивал баннер «Plane — Modern project management» для кликабельной ссылки ORCH-NNN на issue. В дефолтном bump-режиме (ORCH-067) карточка пересоздаётся на каждом переходе → баннер дублировался и засорял ленту (жалоба Owner, 08.06). Корень: JSON-payload обоих низкоуровневых примитивов notifications.send_telegram (POST /sendMessage) и notifications.edit_telegram (POST /editMessageText) не содержал ключ disable_web_page_preview. Фикс (ADR-001, минимальная аддитивная правка на уровне примитива): добавлен "disable_web_page_preview": True в payload обоих методов — гасит баннер у ВСЕХ потребителей (update_task_tracker в обоих режимах, notify_approve_requested, notify_error, alert'ы стадий из launcher/stage_engine) без изменения их кода. Безусловно, без kill-switch (превью трекера не нужно никому, риск нулевой). parse_mode: "HTML" сохранён в обоих payload → ссылка ORCH-NNN остаётся кликабельной; disable_notification (карточка тихая), bump/edit-логика, инвариант «одна карточка на задачу», контракты возврата (send_telegram → message_id|None, edit_telegram → EDIT_*) и never-raise — не затронуты. STAGE_TRANSITIONS, QG_CHECKS, схема БД — без изменений. ADR docs/work-items/ORCH-080/06-adr/ADR-001-disable-telegram-link-preview.md. Тесты: tests/test_link_preview_disabled.py (TC-01..06: флаг в обоих payload, регрессия parse_mode/полей, контракты возврата, never-raise). Документация: CLAUDE.md + docs/architecture/README.md (компонент Notifications).
  • Гарантированный идемпотентный код-PR перед merge-verify (фикс ложного HOLD «no open PR») (ORCH-082/ORCH-81): закрыт отсутствующий инвариант «к моменту merge-verify у ветки есть открытый код-PR». Корень (ORCH-074, 08.06): PR создавался единственной launcher._ensure_pr ТОЛЬКО на developer-пути и ТОЛЬКО при свежем worktree-коммите (exit==0 → git status непуст → commit → push → agent=="developer"); после ручных восстановлений main у ветки ORCH-074 не оказалось открытого код-PR → детерминированный merge_gate.merge_pr вернул ("False", "no open PR") → защита ORCH-073 верно удержала задачу (HOLD, не ложный done), но лечила следствие. Фикс (ADR-001, аддитивно, внутри того же под-гейта merge-verify, машина стадий не тронута): (1) новый идемпотентный leaf-актор merge_gate.ensure_open_pr(repo, branch) -> (status, detail) (never-raise): GET …/pulls?state=open с фильтром head.ref==branch И base.ref=="main" (идентичен merge_pr/ORCH-073 FR-3 — авто-docs-PR base!=main НЕ код-PR) → ("existed", N); иначе POST …/pulls("created", N); гонка 409/422 «PR exists» → повторный GET → existed (без дублей); любая иная HTTP/parse/сетевая ошибка → ("failed", reason). (2) Врезка в stage_engine._handle_merge_verify ПОСЛЕ резолва validated_revision и ПЕРЕД merge_pr: при merge_verify_autocreate_pr_enabledensure_open_pr; created|existed → штатно к merge_prverify_merged_to_main; failed → честный HOLD через новый helper _hold_pr_create_failed (текст «PR создать не удалось», result.note="pr-create-failed-hold" — текстуально отличим от not-merged HOLD; задача остаётся на deploy, НЕ done, БЕЗ отката на development). (3) launcher._ensure_pr делегирован в merge_gate.ensure_open_pr (единый код создания PR, общий фильтр head==branch & base==main); триггер «создавать только на developer-пути со свежим коммитом» НЕ ужесточён — менялась только реализация под капотом. Защита ORCH-073 неприкосновенна и приоритетна: подтверждение merge остаётся ТОЛЬКО verify_merged_to_main (SHA-в-main) + check_main_regression; ensure_open_pr устраняет лишь ЛОЖНЫЙ HOLD «no open PR», реально невлитый код → HOLD как прежде. Kill-switch ORCH_MERGE_VERIFY_AUTOCREATE_PR_ENABLED (дефолт true); область — merge_verify_applies(repo) (self-hosting / merge_verify_repos), non-self → no-op; false → поведение ORCH-074 1:1. Идемпотентность из Gitea (наличие открытого PR), без миграции БД (restart-safe); main не push/force-push. Инварианты НЕ менялись: STAGE_TRANSITIONS, реестр QG_CHECKS (под-гейт — врезка в advance_stage, не новый QG), схема БД, check_deploy_status/_parse_deploy_status, exit-коды хука, merge-gate (ORCH-043), image-freshness (ORCH-058), внешние HTTP-эндпоинты. ADR docs/work-items/ORCH-082/06-adr/ADR-001-ensure-open-pr-before-merge-verify.md (+ сквозной adr-0016). Документация: docs/architecture/README.md (блок ORCH-082 в merge-verify). Тесты: tests/test_orch082_ensure_pr.py (TC-01..05: идемпотентный актор, фильтр base==main, гонка 409/422, never-raise), tests/test_orch082_merge_verify_autocreate.py (TC-06..12: врезка, регресс ORCH-073, kill-switch, условность, наблюдаемость).
  • Устойчивость резолва --effort к пустому env + developer → xhigh (ORCH-081/ORCH-52h): фикс конфигурационного бага, из-за которого в проде resolve_agent_effort() возвращал '' для всех 6 агентов и --effort не передавался в Claude CLI (каждый агент бежал на встроенном CLI-дефолте вместо заявленного уровня — прямой удар по предсказуемости качества всего конвейера, включая enduro-trails из общего инстанса). Корень: pydantic Settings трактует ПРИСУТСТВУЮЩУЮ env-переменную, даже пустую (ORCH_AGENT_EFFORT_*= без значения), как явное '' и перебивает class-default; в проде пусты И per-agent, И agent_effort_default, поэтому у цепочки резолва (_resolve_agent_attr: project-override → per-agent env → default → '') не остаётся непустого «пола» для отката. Фикс (вариант c, ADR-001): в resolve_agent_effort (src/agents/launcher.py) добавлен уровень 4 — непустой per-role floor ниже default: новый чистый helper _agent_effort_floor(agent) возвращает декларированный class-default поля agent_effort_<agent> через type(settings).model_fields[...].default (значение, которое пустой env перебить НЕ может). Floor срабатывает ТОЛЬКО когда уровни 13 пусты и применяется ДО валидации, поэтому: (а) при пустом прод-.env каждая роль получает СВОЙ канонический уровень (developer=xhigh, tester/deployer=medium, analyst/architect/reviewer=high), а не общий default; (б) явная опечатка (turbo/ultra) непуста → floor НЕ применяется → значение штатно дропается валидацией VALID_EFFORTS в '' (never-break ORCH-41 не регрессирует, floor не маскирует мусор); (в) непустой явный env/project-override/default по-прежнему ПОБЕЖДАЕТ floor (приоритет резолва сохранён 1:1). Unknown-agent (имя вне 6 ролей) деградирует на class-default agent_effort_default (high) — безопасный непустой пол. config.py: agent_effort_developer high → xhigh (канон Opus 4.8: coding/agentic роль) — единственное изменение значений; floor подтягивает его автоматически (единый источник правды, ноль риска дрейфа floor-карты). Инварианты НЕ менялись: приоритеты/сигнатуры резолва ORCH-41, _resolve_agent_attr (общий с model-резолвом, не тронут), resolve_agent_model (ORCH-074), путь проброса --effort в _spawn, VALID_EFFORTS, API, схема БД (без миграций). ADR docs/work-items/ORCH-081/06-adr/ADR-001-effort-resolution-floor.md. Документация: docs/architecture/README.md (таблица «модель/эффорт по ролям»: developer xhigh + ремарка про floor), .env.example (ORCH_AGENT_EFFORT_DEVELOPER=xhigh + комментарий split/floor). Тесты: tests/test_resolve_agent_effort.py (TC-01..08: канон-дефолты, floor при пустом env per-role, floor-не-маскирует-typo, приоритет, xhigh∈VALID_EFFORTS, сборка флага --effort xhigh/--effort medium).
  • Убран мёртвый frontmatter model: + валидация имени модели (never-break) (ORCH-074): закрыты два дефекта данных/валидации каркаса выбора модели агентов (ORCH-41), без изменения механизма резолва, API или схемы БД. G1 — мёртвый frontmatter: из YAML-frontmatter всех 6 промптов .openclaw/agents/*.md удалена строка model: (claude-sonnet-4-6 у analyst/developer/tester/deployer, claude-opus-4-7 у architect/reviewer). launcher НЕ читал frontmatter model: — это была лживая/мёртвая декларация, противоречащая реально используемой модели (config) и принципу «документация = golden source»; мина: если бы кто-то «починил» launcher читать frontmatter, все агенты молча уехали бы на устаревшие модели. config (agent_model_*/agent_model_default) остаётся единственным источником правды; frontmatter описательный. G2 — валидация имени модели: добавлен чистый helper is_valid_model(name) + _MODEL_NAME_RE (^claude-[a-z0-9.-]+$) рядом с VALID_EFFORTS в src/agents/launcher.py. Резолвенное имя модели валидируется ПЕРЕД попаданием в --model: невалидное (опечатка, gpt-4, пустое, неверный префикс) → logger.warning + откат на следующий валидный уровень каскада ORCH-41 (project-override → env → default), в пределе → "" (без флага --model, CLI-дефолт). Никогда не возвращается мусор и не бросается исключение (never-break, поведенческая аналогия resolve_agent_effort/VALID_EFFORTS). Выбран формат-чек, а не allowlist VALID_MODELS: allowlist воссоздаёт ровно ту мину, что убивается в G1 (статичный список врёт при устаревании — молча дропнул бы корректную будущую claude-opus-4-9); формат-чек forward-compatible (новые claude-* проходят без правки кода), финальный авторитет о существовании модели — сам Claude CLI. Тот же предикат применён к inline-чтению --fallback-model (agent_fallback_model читается напрямую в _spawn, мимо resolve_agent_model — TRZ §4), поэтому опечатка в ORCH_AGENT_FALLBACK_MODEL тоже дропается с warning; для текущего пустого значения регрессии нет. G4 (fallback) НЕ включён (agent_fallback_model="", AC-5 N/A) — ради детерминизма (все агенты на claude-opus-4-8); G3 (routing) НЕ включён (AC-4 N/A) — осознанное решение стейкхолдера (Слава 08.06). Реализация resolve_agent_model рефакторнута на генератор кандидатов _agent_model_candidates (тот же приоритет ORCH-41) + валидация-со-скипом. Инварианты НЕ менялись: приоритеты/сигнатуры резолва ORCH-41, структура CLI-команды _spawn, VALID_EFFORTS-гард эффорта, STAGE_TRANSITIONS, реестр QG_CHECKS, схема БД (без миграций); enduro per-project override валидные имена проходят без изменения поведения. ADR docs/work-items/ORCH-074/06-adr/ADR-001-model-name-validation.md. Документация: docs/architecture/README.md (таблица «модель/эффорт по ролям» + валидация), CLAUDE.md, .env.example (блок ORCH_AGENT_MODEL_*/ORCH_AGENT_EFFORT_*/ORCH_AGENT_FALLBACK_MODEL). Тесты: tests/test_agent_frontmatter_no_model.py (G1: TC-01/02), tests/test_resolve_agent_model.py (G2 never-break: TC-03..09, TC-11 + is_valid_model).
  • Управление зависимостями задач (B ждёт A) + сериализация мержа одного репо (ORCH-026): два уровня по ADR-001, оба условны (kill-switch + CSV-область, never-raise), без новой стадии и без изменения STAGE_TRANSITIONS/реестра QG_CHECKS. Уровень A — сериализация merge/deploy внутри одного репо: переиспользует существующий merge-lease ORCH-043/065 (никакого нового механизма); единственная новая логика — безусловный pre-merge rebase: в check_branch_mergeable (src/qg/checks.py) под удержанным лизом при флаге premerge_rebase_always (дефолт True) auto_rebase_onto_main вызывается всегда (а не только при branch_is_behind_main) — детерминированный структурный анти-фантом на ребре планировщика, дополняющий рубежи ORCH-073. На актуальной ветке это no-op (rebase не сдвигает HEAD, push --force-with-lease → «Everything up-to-date», CI не триггерится); kill-switch premerge_rebase_always=False → прежнее поведение ORCH-043 1:1. Окно сериализации «merge → main-updated» per-repo (для self done ⇔ SHA-in-main, ORCH-073): пока A не в main, B того же репо получает merge-lock busy → defer (не откат); кросс-репо параллелизм сохранён (лиз — per-repo файл). Уровень B — декларативные зависимости задач: аддитивная таблица job_deps(task_id, depends_on_task_id) (идемпотентный CREATE TABLE/INDEX IF NOT EXISTS в init_db, без миграции на живой БД); гейт планировщика в claim_next_job (src/db.py) — NOT EXISTS (job_deps d JOIN tasks t … WHERE d.task_id=jobs.task_id AND t.stage!='done') при task_deps_enabled: задача с незавершённой зависимостью не выбирается и слот max_concurrency не занимает; инертно при пустой job_deps → нулевая регрессия, kill-switch task_deps_enabled=False → запрос 1:1 как ORCH-1. Новый leaf-модуль src/task_deps.py (контракт never-raise): is_task_ready (fail-open → ready), DFS-детектор циклов (detect_cycle/find_any_cycle, итеративный WHITE/GREY/BLACK), handle_cycle (set_issue_blocked по каждой задаче цикла + один Telegram-alert с цепочкой «A → B → A»), declare_dependency (вставка + детект цикла), ingest_plane_relations (только для task_deps_source=plane|hybrid: резолв Plane blocked-by UUID → локальный task → запись в job_deps; источник истины горячего цикла остаётся БД, дефолт db НЕ ходит в сеть на claim), snapshot (read-only сводка). Видимость: строка « ждёт ORCH-NNN» в Telegram-карточке (src/notifications.py, never-raise, инвариант «одна карточка на задачу» сохранён); блок task_deps в GET /queue (src/main.py). Совместимость: reconciler F-1 пропускает dep-заблокированные задачи (is_task_ready, паттерн ORCH-060) + backstop-детект цикла; job_reaper сканирует только running → dep-блок остаётся queued. Зависимости — только intra-repo (v1). Новые настройки: ORCH_PREMERGE_REBASE_ALWAYS (true), ORCH_TASK_DEPS_ENABLED (true), ORCH_TASK_DEPS_SOURCE (db). Инварианты НЕ менялись: STAGE_TRANSITIONS, реестр QG_CHECKS (гейт зависимостей — врезка в claim_next_job, НЕ зарегистрированный QG), схема tasks/jobs/agent_runs, внешние HTTP-эндпоинты; non-self (enduro) — no-op при пустых job_deps/области. ADR docs/work-items/ORCH-026/06-adr/ADR-001-merge-serialization-and-task-deps.md, глобальный docs/architecture/adr/adr-0015-task-deps-and-merge-serialization.md. Документация: docs/architecture/README.md, CLAUDE.md, .env.example. Тесты: tests/test_orch026_premerge_rebase.py, tests/test_orch026_merge_serialize.py, tests/test_orch026_conditionality.py, tests/test_orch026_task_deps.py, tests/test_orch026_dep_cycles.py, tests/test_orch026_dep_visibility.py, tests/test_orch026_migration.py, tests/test_orch026_queue_observability.py, tests/test_orch026_serialize_integration.py, tests/test_orch026_deps_integration.py.
  • 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_).*