A task carrying the Plane `Bug` label takes a shortened route that skips the `architecture` stage (one opus architect run + ADR + check_architecture_done), replacing heavy analysis with a lite package (bug-report + mandatory regression test plan). EVERY Quality Gate / sub-gate runs UNCHANGED — the route is a scheduler property, not a gate (root invariant NFR-1): STAGE_TRANSITIONS / QG_CHECKS / check_* / machine-verdict keys are byte-for-byte preserved. - src/bug_fast_track.py: new leaf (never-raise) — bug_fast_track_applies (local, network-free, checked first), is_bug_task (labels.has_label, Plane API source), skips_architecture (pure DB-backed routing predicate), snapshot. - src/db.py: additive idempotent tasks.track column (TEXT DEFAULT 'full') + set_task_track / get_task_track helpers (missing/NULL -> 'full', fail-safe). - src/stage_engine.py: routing-override on the analysis-exit edge (track='bug' -> development/developer, skipping architect); brd-review-clock stamp extended to analysis->development. get_next_stage/get_agent_for_stage stay pure. - src/webhooks/plane.py: classify task as bug in start_pipeline (applies-first short-circuit; never-raise -> full cycle on any error). - src/main.py: additive bug_fast_track block in GET /queue + POST /bug-fast-track/escalate (reset 'bug'->'full' to return to the full cycle). - src/config.py: bug_fast_track_enabled / _label / _repos flags (empty CSV -> self-hosting only). - src/notifications.py: optional 🐞 marker on the bug-track card (never-raise). - Prompts: analyst.md (lite bug package + escalation), reviewer.md (regression- test axis) — 52d canon preserved. - Docs: CLAUDE.md, README.md (env + API + section), docs/architecture/README.md, CHANGELOG.md, .env.example. - Tests: tests/test_bug_fast_track*.py + test_db_migrations.py + queue block (TC-01..TC-15). Full regression green (1551 passed). Kill-switch ORCH_BUG_FAST_TRACK_ENABLED=false -> 1:1 pre-ORCH-019 (zero regression; residual track column harmless). Refs: ORCH-019 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
222 KiB
222 KiB
Changelog
Формат: Keep a Changelog. Записи — на смысловой PR/задачу.
[Unreleased]
- Багфикс-трек: упрощённый/дешёвый маршрут конвейера для багов (ORCH-019,
feat): задача с меткой PlaneBugидёт укороченным маршрутом — пропускается стадияarchitecture(отдельный прогон opus-агентаarchitect+ ADR + exit-гейтcheck_architecture_done), тяжёлая аналитика заменяется облегчённым пакетом (короткий bug-report + обязательный план регресс-теста). Все Quality Gate'ы исполняются без изменений (корневой инвариант NFR-1):STAGE_TRANSITIONS/ реестрQG_CHECKS/ сигнатурыcheck_*/ machine-verdict ключи (verdict:/result:/deploy_status:/staging_status:/security_status:/coverage_status:) — байт-в-байт прежние; маршрутизация багфикса — свойство планировщика, не гейт. Аддитивно, под kill-switch, с областью репо, never-raise, fail-safe → полный цикл. ADR:docs/work-items/ORCH-019/06-adr/ADR-001-bug-fast-track.md, сквознойdocs/architecture/adr/adr-0032-bug-fast-track.md.- Классификация (D1, FR-1): новый leaf
src/bug_fast_track.py(never-raise, паттернlabels/serial_gate).bug_fast_track_applies(repo)(локально, без сети) проверяется ПЕРВЫМ → выключенный флаг = нулевой сетевой оверхед;is_bug_task(work_item_id, project_id)делегирует в проверенныйlabels.has_label(ORCH-089:fetch_issue_labels+get_project_labels, нормализация, TTL-кэш). Источник истины — Plane API, не payload вебхука. Чтение метки — только вstart_pipeline, никогда в горячемclaim_next_job(NFR-4). - Хранение типа (D2): аддитивная идемпотентная колонка
tasks.track TEXT DEFAULT 'full'(_ensure_column, паттернtasks.cancelled_atORCH-090); значения'full'(дефолт, ВСЕ существующие и не-баг задачи) |'bug'. Хелперыdb.set_task_track/db.get_task_track(отсутствие/NULL →'full', fail-safe). Сигнатураcreate_task_atomicне меняется. - Routing-override (D3, FR-2): врезка в
advance_stageна ребре выхода изanalysis: приtrack='bug'(через чистый предикатbug_fast_track.skips_architecture)next_stage→development,next_agent→developer(минуяarchitect).get_next_stage/get_agent_for_stage/STAGE_TRANSITIONS— чистые, 1:1; тип читается из БД (без сети, NFR-4). Для не-баг задач (track='full') маршрут байт-в-байт прежний. Сопутствующе: стампmark_brd_review_endedрасширен наanalysis → development(честная метрика ORCH-087 на багфикс-треке). - Гейт
analysisне тронут (D4, FR-6):check_analysis_complete/check_analysis_approvedбайт-в-байт прежние; багфикс-аналитик всё равно эмитит все 4 файла (облегчённые) — сильнейшая позиция NFR-1 (нулевая поверхность правок гейта). - Эскалация (D5, FR-5): админ-эндпоинт
POST /bug-fast-track/escalate?work_item=<id>(по образцуPOST /serial-gate/unfreeze) сбрасываетtrack'bug'→'full'→ следующий переход уходит вarchitecture(полный цикл). Плюс решение мини-аналитика «баг сложный → полный пакет +escalate: full-cycle». - Область / флаги (D6):
bug_fast_track_enabled(kill-switch, envORCH_BUG_FAST_TRACK_ENABLED),bug_fast_track_label(дефолтBug),bug_fast_track_repos(CSV; пусто → self-hosting only — enduro подключается явным CSV).False→ старт и маршрут 1:1 как до ORCH-019 (нулевая регрессия, AC-6). - Наблюдаемость (D7, FR-7): аддитивный read-only блок
bug_fast_trackвGET /queue(флаг/метка/область + счётчик багфикс-задач + метрика сэкономленных стадийarchitecture); лог-строка на решение о маршруте; отметка🐞в Telegram-карточке (never-raise). Композиция (D8, AC-9): багфикс-задача — обычная задача репо для serial-gate (ORCH-088, не обходит его);autoApprove/autoDeploy(ORCH-089), coverage-gate (ORCH-027, союзник BR-4), merge-gate (ORCH-043) — штатно. - Промпты:
analyst.md(облегчённый багфикс-пакет + путь эскалации),reviewer.md(ось «багфикс без регресс-теста → finding ≥P1 / REQUEST_CHANGES») — канон 52d не нарушен. Инфра-предусловие: создать меткуBugв Plane-проекте ORCH (её отсутствие = fail-safe полный цикл). Тесты:tests/test_bug_fast_track*.py+tests/test_db_migrations.py+ блок вtests/test_queue_endpoint.py(TC-01…TC-15). Полный регрессtests/ -qзелёный. Откат:ORCH_BUG_FAST_TRACK_ENABLED=false(мгновенный; остаточная колонкаtrackбезвредна).
- Классификация (D1, FR-1): новый leaf
- Детект legacy root-owned файлов + внятная ошибка worktree при миграции на uid 1000 (ORCH-057, follow-up ORCH-040,
feat): закрыт недоделанный AC ORCH-040 — legacyroot:rootфайлы в/repos(после перевода контейнеров наuser: "1000:1000") ломали создание worktree под uid 1000 (ensure_worktree→ сыройfatal: … Permission denied, агент не стартовал, диагноза не было). Три аддитивных, обратимых kill-switch'ем слоя;STAGE_TRANSITIONS/QG_CHECKS/check_*/ machine-verdict-ключи / схема БД — байт-в-байт прежние. ADR:docs/work-items/ORCH-057/06-adr/ADR-001-legacy-ownership-normalization.md, сквознойdocs/architecture/adr/adr-0031-legacy-ownership-normalization.md.- D1 — actionable-ошибка
ensure_worktree: класс «нет прав» (Permission denied/could not create leading directories/insufficient permission for adding an object/PermissionError/EACCES/EPERM) оборачивается вRuntimeErrorс причиной (legacy root-файлы в/repos/_wt/.gitпосле миграции uid), лечащей командой (chown -R <uid>:<uid> …) и ссылкой наINFRA.md— вместо сырого git stderr. Ошибки, не связанные с правами, сохраняют прежний контракт (меняется только формулировка, не факт сбоя; чистый классификаторfs_normalize.classify_worktree_error). Под выключенным kill-switch контракт ошибки 1:1 как до ORCH-057. - D2 — детект-леаф
src/fs_normalize.py(never-raise, паттернserial_gate/coverage_gate):scan_ownership(roots, target_uid=os.getuid())обходит/repos/_wt,<repo>/.git/{objects,worktrees},data/runsс ранним выходом при первомst_uid != target_uid, TTL-кэшем (fs_scan_cache_ttl_s, по образцуpreflight._cache) иapplies(repo)first (пустой CSV → self-hosting only → enduro-trails не сканируется). Опц.normalize()chown'ит только приgeteuid()==0(под uid 1000 — no-op + честный лог «нужна операторская процедура», НЕ ошибка). - D3 — наблюдаемость, БЕЗ блокировки claim: best-effort вызов
scan_ownership()на стартеmain.lifespan(рядом с lease-reclaim/log-rotation, never-fatal) → WARNING + Telegram при mismatch; read-only блокfs_ownershipвGET /queue; опц. ручнойPOST /fs-normalize/check. Claim не блокируется (preflight repo-слеп → регресс enduro; queue_worker — дорогой FS-обход в hot-path + молчаливое зависание); внятный ранний отказ даёт D1 в точке launch. - Процедура (D5): обязательная операторская нормализация под root на хосте — в
docs/operations/INFRA.md(раздел «Миграция uid: обязательная нормализация legacy root-файлов», все корни:_wt, оба.git,data/runs); фактическийchownостаётся ручным шагом (контейнер без root его сделать не может) — задача гарантирует внятность отказа, а не его отсутствие. - Флаги (
src/config.py, аддитивно):ORCH_FS_NORMALIZE_ENABLED(kill-switch),ORCH_FS_NORMALIZE_REPOS(CSV; пусто → self-hosting only),ORCH_FS_TARGET_UID(1000),ORCH_FS_NORMALIZE_AUTO(детект-only),ORCH_FS_SCAN_ROOTS,ORCH_FS_SCAN_CACHE_TTL_S. Тесты:tests/test_fs_normalize.py,tests/test_git_worktree_perm.py,tests/test_fs_normalize_startup.py,tests/test_api_queue.py(TC-01…TC-12).
- D1 — actionable-ошибка
- Лёгкий read-only
GET /metrics— машинное «сырьё» о самом орке для sidecar F1b (ORCH-099, FND/F1a,feat): добавлен версионируемый JSON-эндпоинтGET /metrics, отдающий снимок внутреннего состояния орка для будущего отдельного sidecar-наблюдателя F1b (watchdog/) — наблюдатель отделён от наблюдаемого (BRD §1): орк отдаёт ТОЛЬКО факты, которые знает лишь он сам; пороги/алерты/история/Telegram — на стороне F1b. Аддитивно, строго read-only, never-raise:STAGE_TRANSITIONS/QG_CHECKS/check_*/ machine-verdict ключи / схема БД — не тронуты;/health//status//queue— байт-в-байт прежние. ADR:docs/work-items/ORCH-099/06-adr/ADR-001-metrics-endpoint.md, сквознойdocs/architecture/adr/adr-0030-metrics-endpoint.md.- Leaf-сборщик + тонкий эндпоинт (D1): новый
src/metrics.py(build_metrics() -> dict, never-raise по разделам, паттернserial_gate.snapshot()) собирает конверт по-раздельно (каждый раздел в своёмtry/except→ безопасный дефолтnull/[]/{}+ WARNING); эндпоинт@app.get("/metrics")вsrc/main.py— тонкая обёртка, возвращает результат как есть (стильGET /queue). Тестируемость без ASGI: разделы проверяются прямым вызовомbuild_metrics(). - Конверт + контракт
schema_version(D2):schema_version(стартует с1),generated_at(UTC ISO-8601, часовой домен орка → дельты CPU иммунны к skew орк↔sidecar, TR-3),clk_tck(os.sysconf("SC_CLK_TCK"), базис тиков). Политика: аддитивные изменения НЕ бампят версию (sidecar обязан игнорировать незнакомые ключи) — бамп только при ломающем (rename/remove/retype). - Разделы сырья (D3–D7):
stages— незавершённые задачи (stage NOT IN ('done','cancelled'), ORCH-090) сwork_item/stage/age_in_stage_s/repo(источникdb.get_active_tasks_for_reconcile()+ фильтр терминалов на потребителе, helper-инвариант ORCH-053/086 не тронут).queue—db.job_status_counts()(+cancelled-ключ дефолтом), глубина, сырьё ретраев (db.queue_retry_stats(): attempts/transient/в-backoff),worker.breaker.snapshot(),max_concurrency.agents(liveness) — по running-job (новый read-onlydb.get_running_agents(), dedicated SELECT, НЕ расширение hot-pathget_running_jobs()):agent/run_id/job_id/pid/runtime_s(=running_age_sотjobs.started_at, D6)/model/effort+ CPU-сырьёcpu_ticks(utime+stime из/proc/<pid>/stat, поля 14+15; орк дельту не считает — stateless, арбитр sidecar).cost—running(по running-job,nullдо завершения = честное сырьё) +aggregate(новыйdb.agent_cost_totals(),COALESCE(SUM(...),0)поagent_runs). - Never-raise сырьё для liveness (FR-6/NFR-2):
metrics._read_cpu_ticks(pid)—pid is None/ нет/proc/<pid>/ мёртвый процесс / не-Linux →cpu_ticks: nullу этого агента, прочие поля и весь эндпоинт целы (НЕ raise). Недоступныйworker→breaker: null/max_concurrency: null, не 500. Пустые таблицы →stages=[]/agents=[]/cost.aggregate=нули. - Kill-switch (D8):
src/config.pymetrics_endpoint_enabled: bool = True(envORCH_METRICS_ENABLEDчерез явныйvalidation_alias— документированное имя контракта реально управляет флагом).False→200с минимальным телом{"schema_version":1,"enabled":false}(НЕ 404 — контракт остаётся парсимым). ДефолтTrue→ нулевая регрессия (эндпоинт доступен из коробки). - Контракт задокументирован (AC-7): формат
/metricsзафиксирован вdocs/architecture/README.md(раздел «Сырьё-эндпоинт/metrics» + строка в таблице API) как стабильный контракт для F1b. Тесты:tests/test_metrics.py(TC-01…TC-11: конверт/4 раздела, исключение терминалов, queue-поля, liveness-сырьё + cpu_ticks на живом pid, never-raise наpid=None/мёртвом pid/бросающем источнике/недоступном breaker, cost-агрегат + пустая таблица, эндпоинт через handler, read-only снимок БД до/после, аддитивность/health//status//queue, пустое состояние, kill-switch). Полный регрессtests/ -qзелёный (1480 → +14). Откат:ORCH_METRICS_ENABLED=false(мгновенный) или удаление модуля/эндпоинта/helper'ов (без следов в БД/схеме).
- Leaf-сборщик + тонкий эндпоинт (D1): новый
- Детерминированный гейт покрытия тестами — защита от тихой деградации 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): leafsrc/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_mainHEAD — ровно том коде, что landed вmain) и ДО image-freshness (фейл до дорогого docker-rebuild). FAIL → штатный откат наdevelopment(+ инкремент developer-retry, capMAX_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 coveragesrc/). Измеритель инкапсулирован за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.absolute→measured ≥ floor−ε;baseline→measured ≥ 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-setUPDATE … 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 onlyis_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(frontmattercoverage_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), измеренным покрытием, порогом/базовой линией и дельтой. Опциональный ручной overridePOST /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, envORCH_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зелёный.
- Точка/порядок (D1, AC-2): под-гейт исполняется ПОСЛЕ merge-gate (покрытие меряется на догнанном
- 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_FAILED→update_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 на границе (<1мрендерится оператору визуально идентично<1м→ видимый формат не меняется). - Категория M (намеренная разметка) неприкосновенна (D5, AC-3): кликабельный номер задачи
num_html(plane_issue_link, внутри уже экранированы href+label),link_for(...)в строке «⏳ ждёт …»,_done_link(...)(«🔗 PR #n · 📦 Внедрено») и уже-экранированныйesc_titleчерез_escне проходят → остаются валидным HTML, номер остаётся кликабельным. Двойное экранирование (&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).
- Экранирование на границе рендера, не в источнике (ADR-001 D1/D2, AC-1/AC-2): новый модуль-локальный хелпер
- Терминальная (done) задача держит
Doneв Plane: terminal-window-aware гард deploy-статусов (ORCH-094,fix): задача с БДstage=doneи 0 активных job'ов (верифицировано на ORCH-061, task 47) стабильно флаппила в PlaneAwaiting 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, идемпотентно).cancelled→SUPPRESS(не штампуем поверх терминала 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. Арм-первым пишетARMED→window_active==True→ALLOWпропускает легитимныйMonitoring; re-drivedeploy→doneпосле закрытия окна (DONEpresent) →window_active==False→CONVERGE_DONE(не воскрешаетMonitoring). Перенос безопасен:arm_monitorлишь пишет sentinel + ставит отложенный job, не зависит от Plane-статуса/merge-lease (release остаётся после terminal-sync). Инварианты ORCH-021 (идемпотентный арм поARMED) и ORCH-066 (deploy→doneself ⇒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-readablework_item_idматчит живой ряд; тумбстоны ORCH-090 имеют суффикс#cancelled-<id>). - Конфиг/откат (FR-5, D6):
src/config.pydeploy_status_guard_enabled: bool = True(envORCH_DEPLOY_STATUS_GUARD_ENABLED;False→ сеттеры терминал-слепы, поведение 1:1 прежнее) /deploy_status_guard_repos: str = ""(envORCH_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-overlaynotifications.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.
- Единый гард на низком чокпоинте (FR-2, D1/D2): новый leaf
- 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-shotmerge_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.pymerge_retry_enabled(kill-switch;False→ ровно один POST = байт-в-байт прежнее one-shot, нулевая регрессия) /merge_retry_max_attempts(3) /merge_retry_backoff_base_s(2) /merge_retry_backoff_max_s(5), envORCH_MERGE_RETRY_*, дескрипторы в.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-guardpr_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-overrideORCH_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.
- Retry-loop транзиента (FR-1/FR-2, AC-1/AC-2/AC-3, D1/D2):
- 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_TRANSITIONS—deploy-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 → developmentORCH-043,review → developmentREQUEST_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).
- Деф.1 — застрявший заголовок «To Analyse» (FR-1/2/3, AC-1/2/3):
- Отмена задачи: 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маршрутизируетstop→handle_stop→stage_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-raisesrc/gitea.py::delete_remote_branch(удаляет только feature-ветку;main/master— явный гард-отказ; без force-push); durablestage='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_windowfail-CLOSED): durabletasks.cancel_requested_at, снимаются толькоqueued-job'ы (running-актор деплоя/мержа не трогается), алерт; детерминированныйrun_deploy_finalizerдоводит необратимый шаг до честного исхода и применяет отмену (cancel_task(force=True); задача, дошедшая доdone, — честный no-op, код уже в проде). «Критическое окно» = реально начатый необратимый шаг: self-deployINITIATED-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 сверяют терминал задачи → помечают jobcancelled, не реквью'ят (закрыта гонка 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, envORCH_STOP_STATUS_ENABLED) +stop_status_repos(CSV, пусто → все репо); leafsrc/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(аддитивные колонки/терминал-набор инертны при отсутствии отменённых задач).
- Распознавание (fail-closed): новый логический ключ
- 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, envORCH_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(миграций нет).
- Периодическая уборка (FR-1/AC-1): каждые
- 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) через stdlibshutil.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, envORCH_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.
- Замер хост-ФС (FR-2/AC-8): каждые
- Промпт-аудит 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(нет машинного поведения/состояния).
- Расхардкод даты/модели (FR-1/FR-2, AC-1/AC-2): во всех 6 промптах копируемые примеры frontmatter несут плейсхолдеры
- Синхронизация обзорных доков (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/breakerqueue_worker.py+ORCH-045; issue-ID → зрелыйplane_syncORCH-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-routingstatus— авторитетный гейт развития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-itemdocs/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-itemORCH-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; canonicaldocker exec orchestrator-staging … staging_check.py, B6-обоснование, ORCH-061INFRA-WAIVED, merge-guardpr_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 revertPR — нет миграций/состояния. Норматив на будущее: новые/изменённые агент-промпты следуют этому канону. - Документация:
.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.
- Единый XML-скелет (5 обязательных секций, нормативный порядок):
- Единый 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/ семантика вердиктов / fallbackworktree→origin/main/ трёх-полевой контракт tester (ORCH-047) — 1:1, без изменений.src/frontmatter.py(контракт): сохранён readerread_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; writerrender_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-switchfrontmatter_validation_strict(envORCH_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.md…17-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 доки (12→verdict:,13→result:,14→deploy_status:,15→staging_status:,17→security_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) и гейт прод-деплоя (deployPhase A: ручнойConfirm Deploy, ORCH-059). Решение выборочно (лейбл Plane на задаче), декларативно, обратимо и не трогает ни одной технической проверки. Аддитивно по образцу условных под-гейтов (ORCH-035/043/058/088): leafsrc/labels.py(never-raise) + две точечные врезки + флаги;STAGE_TRANSITIONS/QG_CHECKS/check_*/схема БД — без изменений.autoApprove→ врезка вstage_engine._handle_analysis_approved_flow(веткаfiles_ok) ПОСЛЕIn Review+коммента:set_issue_approved(индикация) + лог/Telegram/Plane-коммент +advance_stage(..., finished_agent=None)— тот же путь, что человеческий Approved (approved-via-status→analysis → architecture+mark_brd_review_ended). Без дублирования переходной логики; 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(полеlabelsissue;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_labelFalse = ручной режим (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, globaldocs/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. Фрагмент строится в leafsrc/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 фиксировал псевдо-SQLt2.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_branch409 /_create_initial_docs422 = no-op) → безопасно при реклейме/рестарте. Ожидающая задача =queuedanalyst-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-deployDEGRADED(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-repoactive_task/waiting/frozen+reason+at); существующие ключи не меняются. NFR-6: freeze — пассивная остановка стартов, прод-контейнер не рестартится/не роняется. Cross-repo параллелизм сохранён (FR-3/AC-4); при выключенном флаге — нулевая регрессия (enduro не затронут, AC-7). ADRdocs/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).
- Gate-в-claim (
- 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(envORCH_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(envORCH_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). ADRdocs/work-items/ORCH-087/06-adr/ADR-001-tracker-orphan-cleanup.md.
- G1 — полный учёт mid: аддитивная таблица-леджер
- Терминал-скип и
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 — терминальная задача (группа Planecompleted/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_uuid→deduped_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 сохранён. ADRdocs/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, проброс/dedupstate_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, схема БД — без изменений. ADRdocs/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-PRbase!=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_enabled→ensure_open_pr;created|existed→ штатно кmerge_pr→verify_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-switchORCH_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-эндпоинты. ADRdocs/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 срабатывает ТОЛЬКО когда уровни 1–3 пусты и применяется ДО валидации, поэтому: (а) при пустом прод-.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-defaultagent_effort_default(high) — безопасный непустой пол.config.py:agent_effort_developerhigh → xhigh(канон Opus 4.8: coding/agentic роль) — единственное изменение значений; floor подтягивает его автоматически (единый источник правды, ноль риска дрейфа floor-карты). Инварианты НЕ менялись: приоритеты/сигнатуры резолва ORCH-41,_resolve_agent_attr(общий с model-резолвом, не тронут),resolve_agent_model(ORCH-074), путь проброса--effortв_spawn,VALID_EFFORTS, API, схема БД (без миграций). ADRdocs/work-items/ORCH-081/06-adr/ADR-001-effort-resolution-floor.md. Документация:docs/architecture/README.md(таблица «модель/эффорт по ролям»: developerxhigh+ ремарка про 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 НЕ читал frontmattermodel:— это была лживая/мёртвая декларация, противоречащая реально используемой модели (config) и принципу «документация = golden source»; мина: если бы кто-то «починил» launcher читать frontmatter, все агенты молча уехали бы на устаревшие модели. config (agent_model_*/agent_model_default) остаётся единственным источником правды; frontmatter описательный. G2 — валидация имени модели: добавлен чистый helperis_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). Выбран формат-чек, а не allowlistVALID_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 валидные имена проходят без изменения поведения. ADRdocs/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-switchpremerge_rebase_always=False→ прежнее поведение ORCH-043 1:1. Окно сериализации «merge → main-updated» per-repo (для selfdone⇔ 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-switchtask_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: резолв Planeblocked-byUUID → локальный task → запись вjob_deps; источник истины горячего цикла остаётся БД, дефолтdbНЕ ходит в сеть на claim),snapshot(read-only сводка). Видимость: строка «⏳ ждёт ORCH-NNN» в Telegram-карточке (src/notifications.py, never-raise, инвариант «одна карточка на задачу» сохранён); блокtask_depsвGET /queue(src/main.py). Совместимость:reconcilerF-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/области. ADRdocs/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/main(вmainпопадали только их авто 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-switchORCH_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сохранён. ADRdocs/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(envORCH_QG0_TITLE_MAX, дефолт 200). Лимит 80 был гигиеническим, а не структурным (slug режется независимо[:30],tasks.title TEXTбез ограничения), поэтому валидные заголовки 81–200 символов отклонялись на входе без бизнес-причины. Лимит читается из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], нижние лимиты (< 5title,< 20description), soft-QG-0 поведение (warning наwork_item.created), API. ADRdocs/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-effortdelete_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). ADRdocs/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(D1–D9), раздел вdocs/architecture/README.md, runbook постмортемаdocs/operations/PHANTOM_MERGE_RUNBOOK.md(4 проверки + критерий «фантом подтверждён» + remediation). Тесты:tests/test_merge_actor.py,tests/test_merge_verify.py,tests/test_deploy_finalizer_merge_gate.py,tests/test_deploy_restart_merge_recovery.py,tests/test_qg_checks.py,tests/test_stages.py. - Security-гейт: secret-scanning (gitleaks) + dependency audit (pip-audit) перед мержем (ORCH-022): автономный конвейер вливал ветку в
mainбез проверки на утёкший секрет (ключ/токен/пароль/приватный ключ) и уязвимую зависимость (известный CVE) — для self-hostingorchestratorэто особенно остро: один общий прод-инстанс обслуживает все проекты из общей БД, поэтому секрет/CVE, проскочивший через одну задачу, уезжает в прод всех проектов (CLAUDE.md §self-hosting, §8). ORCH-022 вводит детерминированный (без LLM) security-гейт как под-гейт ребраdeploy-staging → deploy, исполняемый ПЕРВЫМ среди edge-под-гейтов (ДО merge-gate ORCH-043 и image-freshness ORCH-058) — дёшево фейлить до дорогих rebase/rebuild, а скан ветки ДО rebase не «обвиняет» задачу в CVE из обновившегосяmain. Паттерн соседей: новый leaf-модульsrc/security_gate.py(контракт «never-raise», по образцуmerge_gate/image_freshness/staging_verdict) + тонкая обёрткаcheck_security_gateв реестреQG_CHECKS(src/qg/checks.py, lazy-import → нет цикла) + врезка_handle_security_gateвsrc/stage_engine.pyв блокcurrent_stage == "deploy-staging"ПЕРВОЙ.STAGE_TRANSITIONSи схема БД — без изменений. Secret-scanning (gitleaks, offline): скан диапазонаorigin/main..HEAD(ровно коммиты задачи); любой секрет вне аллоулиста версионируемого.gitleaks.toml→ вклад в FAIL. Полностью оффлайн (локальные правила) → гарантия «секрет всегда блокирует» (BR-2) безусловна, не зависит от сети; fail-closed при ошибке инструмента/отсутствии бинаря/таймауте (нельзя доказать «секретов нет» → FAIL). Контракт exit-кодов: 0=чисто, 1=найдено, ≥2=ошибка. Dependency audit (pip-audit, OSV/PyPI): аудитrequirements.txt; severity ≥security_dep_block_severity(дефолтHIGH, порядок CRITICAL>HIGH>MEDIUM>LOW) → вклад в FAIL (deps_blocking); ниже порога / UNKNOWN → warning (deps_warning, анти-петля Р-4, не авто-блок). Источник advisory требует сети → недоступность фида fail-open + громкий warning по умолчанию (deps_audit_degraded: true+ Telegram + лог; прецедент анти-петли ORCH-061), флагsecurity_dep_audit_fail_closedпереводит в строгий режим без редеплоя кода. Артефакт17-security-report.md(YAML-frontmattersecurity_status/secrets_found/deps_blocking/deps_warning/deps_audit_degraded+ тело-списки находок); машинный вердикт читается ТОЛЬКО из frontmatter (гейт пишет → читает обратно черезparse_security_status→ возвращает ровно то, что записал: единый источник истины, AC-8), negative-токен (FAIL) авторитетен, нет frontmatter/битый YAML/нет поля → fail-closed на чтении; значения секретов в артефакте маскируются (не ре-лик). FAIL → откат наdevelopment+ developer-retry (общий_developer_retry_count, capMAX_DEVELOPER_RETRIES=3, затемset_issue_blocked+ Telegram, без бесконечного баунса);task_descперезапущенного developer'а несёт дословные находки (extract_security_findings, паттерн ORCH-046) + ссылку на артефакт. Self-hosting safety: гейт только читает/сканирует/пишет артефакт — не вызывает деплой-хук, не рестартит прод-контейнер (под-гейт исполняется ДО захвата merge-lease → при FAIL lease освобождать не нужно). Условность как ORCH-35/43/58:security_gate_enabled(kill-switch) +security_gate_repos(CSV; пусто → только self-hostingorchestrator); таймаутsecurity_scan_timeout_s; never-raise. v1 — Python-only стек; SAST/мульти-стек — follow-up (BR-14). Инфраструктура: pinnedgitleaks(статический Go-бинарь) вDockerfile(+curl/ca-certificates),pip-audit(pinned) вrequirements.txt,.gitleaks.tomlв корне репо. Новые настройки:ORCH_SECURITY_GATE_ENABLED(true),ORCH_SECURITY_GATE_REPOS(""),ORCH_SECURITY_DEP_BLOCK_SEVERITY(HIGH),ORCH_SECURITY_SCAN_TIMEOUT_S(300),ORCH_SECURITY_DEP_AUDIT_FAIL_CLOSED(false),ORCH_SECURITY_SECRETS_BLOCK(true). Инварианты НЕ менялись:STAGE_TRANSITIONS(9 стадий),check_branch_mergeable/check_staging_image_freshи их под-гейты, БАГ-8 откат, terminal-sync, схема БД (без миграций). ADRdocs/work-items/ORCH-022/06-adr/ADR-001-security-gate.md, глобальныйdocs/architecture/adr/adr-0012-security-gate.md. Документация:docs/architecture/README.md,CLAUDE.md,.env.example. Тесты:tests/test_security_gate.py,tests/test_qg_security.py,tests/test_stage_engine_security_gate.py,tests/test_qg_registry_snapshot.py,tests/test_config.py. - Выделенный статус-триггер прод-деплоя «Confirm Deploy» (ORCH-059): жест запуска прод-деплоя отделён от человеческого гейта одобрения. Раньше один Plane-статус
Approvedбыл перегружен: наanalysisон работал как человеческий гейт BRD (check_analysis_approved), а наdeploy— молча триггерил Фазу B прод-деплоя ORCH-036 (advance_stage(deploy, finished_agent=None) → _handle_self_deploy_phase_b → detached host-рестарт прод-контейнера 8500). Привычный жест approve = групповой self-hosting риск (прод обслуживает ВСЕ проекты из одного инстанса). ORCH-059 вводит отдельный логический статусconfirm_deploy(«Confirm Deploy»), который триггерит ТОЛЬКО Фазу B наdeploy;Approvedостаётся исключительно гейтом конвейера. Четыре точечные правки в трёх модулях: (1)src/plane_sync.py— маппинг"Confirm Deploy" → "confirm_deploy"в_PLANE_NAME_TO_KEY; ключ намеренно НЕ добавлен в_DEFAULT_STATES(нет UUID для enduro/fallback) → fail-closed: для проекта ORCH резолвится из живого Plane API (get_project_states(orch)["confirm_deploy"]→ реальный UUID), для сред без статуса (enduro / недоступный API / доска без статуса) ключ просто отсутствует, доступ через.get("confirm_deploy")→None, безKeyError. (2)src/webhooks/plane.py—handle_issue_updatedДО веткиapprovedдобавляет fail-closed-веткуconfirm_state = proj_states.get("confirm_deploy"); if confirm_state and new_state == confirm_state: handle_confirm_deploy(...); новыйhandle_confirm_deployрезолвит задачу, гардstage == "deploy"(иначе no-op с логом — защищает прочие гейты от случайного триггера), иначе →_try_advance_stage(..., confirm_deploy=True).handle_verdict(approved=True)не изменён (продолжает звать_try_advance_stageс дефолтнымconfirm_deploy=False). (3)src/stage_engine.py—advance_stageполучил keyword-only параметрconfirm_deploy: bool = False(обратносовместимо: все существующие вызовы из launcher/reconciler/finalizer передаютfinished_agent); блок Фазы B теперь всегда возвращается рано дляdeploy + finished_agent is Noneself-hosting, но_handle_self_deploy_phase_bвызывается ТОЛЬКО приconfirm_deploy=True, иначе (обычныйApproved) — детерминированный no-op (result.note = "approved-on-deploy-noop"): возврат ДО блока Quality Gate →check_deploy_statusне запускается → нет ложного отката БАГ-8 (вердикта ещё нет, R-2). (4) CTA Фазы A (_handle_self_deploy_phase_a) — Plane-коммент и Telegram просят перевести задачу в статус «Confirm Deploy» (а не «Approved»). Следствие для reconciler F-1 наdeploy(ORCH-053): попадает в no-op-ветку вместо неявного запуска Фазы B → прод-деплой нельзя инициировать автоматически, только явным человеческим «Confirm Deploy» (усиление safety). Условность как ORCH-35/36 (реально только дляself_deploy.self_deploy_applies("orchestrator"); прочие репо — прежний синхронный ssh-деплой агентом, статус не нужен и не влияет). Контракты НЕ менялись:STAGE_TRANSITIONS, реестрQG_CHECKS,check_deploy_status/_parse_deploy_status, exit-код-контракт хука (0/1/2), Фазы A/C, merge-gate, terminal-sync, схема БД (статусы — на стороне Plane; restart-safe состояние деплоя — существующие sentinel-файлы ORCH-036). Эксплуатационное предусловие: в Plane-проекте ORCH создать статус доски «Confirm Deploy» (точное имя, регистр) + сброс кэша состояний —docs/work-items/ORCH-059/07-infra-requirements.md. До создания статуса прод-деплой через approve не запустится (желаемое fail-closed-поведение). ADRdocs/work-items/ORCH-059/06-adr/ADR-001-confirm-deploy-status.md(уточняет триггер Фазы B относительно adr-0007). Тесты:tests/test_plane_states.py,tests/test_plane_confirm_deploy.py,tests/test_stage_engine_phase_b.py,tests/test_stage_engine_phase_a_cta.py,tests/test_confirm_deploy_integration.py,tests/test_deploy_approve.py(обновлён под новый триггер). - Осмысленная статусная модель Plane (слой B — индикация) (ORCH-066): Plane больше не показывает наблюдателю огрублённую/вводящую в заблуждение картину — статусы доски приведены к смыслу стадий конвейера, при этом статус остаётся индикацией, а не управлением. Архитектурный инвариант (ADR-001): меняется ТОЛЬКО слой B (отображение в Plane —
src/plane_sync.pyи точки выставления статуса вstage_engine.py/webhooks/plane.py/reconciler.py), слой A (машина стадийsrc/stages.py::STAGE_TRANSITIONS) остаётся байт-в-байт неизменным (AC-21, регресс-тест TC-22 сверяет полный литерал словаря). Целевая модель:Backlog → Todo → [To Analyse] → Analysis → [In Review → Approved] → Architecture → Development → Code-Review → Testing → Awaiting Deploy → [Confirm Deploy] → Deploying → Monitoring after Deploy → Done. Добавлены 6 новых логических ключей статуса (to_analyse,analysis,code_review,awaiting_deploy,deploying,monitoring) в_DEFAULT_STATES/_PLANE_NAME_TO_KEYплюсSTAGE_VISIBILITY_STATE(analysis→analysis,review→code_review) и_STAGE_TO_STATE_KEY; новые сеттерыset_issue_analysis/code_review/awaiting_deploy/deploying/monitoring+ диспетчерset_issue_stage_state. Project-relative alias-fallback (BR-12): если оператор ещё не создал новый статус в конкретном Plane-проекте, ключ деградирует на базовый UUID ТОГО ЖЕ проекта (_STATE_ALIAS_FALLBACK:analysis→in_progress,code_review→review,awaiting_deploy→in_review,deploying→in_progress,monitoring→done,to_analyse→in_progress), поэтому PATCH остаётся валидным на частичных конфигах, а enduro-trails схлопывает новые ключи на старые базовые статусы → нулевая регрессия. Самодеплой (ORCH-036) теперь индицирует фазы: Phase A →Awaiting Deploy(ожидание ручного approve), Phase B →Deploying, terminal-syncdeploy→doneветвится — для self-hosting (post_deploy.post_deploy_applies(repo)) issue входит в окноMonitoring after Deploy(НЕ терминальный Done), для прочих репо — прежний терминальныйDone(нулевая регрессия, TC-08/TC-09). Post-deploy монитор (ORCH-021) на закрытии окна: HEALTHY →set_issue_done, DEGRADED →set_issue_blocked(только индикация; self-hosting остаётся ALERT_ONLY, прод НИКОГДА не рестартится/не откатывается — BR-5, TC-10/11/12). Reconciler: F-2 триггер старта/резюма расширен наTo Analyse(TC-20), Guard 2_is_blocked_or_needs_inputучитывает новые активные ожидания (awaiting_deploy/deploying/monitoring) с вычитанием базовых рабочих статусов, чтобы алиасинг на частичных проектах не расширял skip-set (анти-регресс, TC-21). Контракт never-raise на всех сеттерах и резолвере состояний сохранён (API Plane недоступен → identity-фоллбэк, сеттеры не бросают — TC-16/17/18). Раскатка управляется оператором (создание 6 статусов в Plane), отдельного kill-switch не вводится — на «голом» Plane всё деградирует на прежнее поведение. Инварианты НЕ менялись (TC-22/TC-23):STAGE_TRANSITIONS(9 стадий), реестрQG_CHECKS(12 чеков), сигнатураcheck_deploy_status(repo, work_item_id, branch), exit-код-контракт хука, merge-gate, схема БД (без миграций). ADRdocs/work-items/ORCH-066/06-adr/ADR-001-plane-status-model.md. Тесты:tests/test_plane_status_model.py,tests/test_plane_to_analyse_resume.py,tests/test_plane_status_failclosed.py,tests/test_plane_webhook.py(TC-15),tests/test_deploy_terminal_sync.py(TC-08/09),tests/test_post_deploy_integration.py(TC-10/11/12),tests/test_orch10_states.py(TC-19),tests/test_reconciler.py(TC-21),tests/test_reconciler_plane.py(TC-20). - Job-reaper + проактивный реклейм протухшего merge-lease + идемпотентная финализация merge (ORCH-065): закрыт класс инцидентов «zombie jobs» — статус job выставлялся ТОЛЬКО в живом процессе launcher'а, поэтому гибель процесса (OOM/рестарт инстанса/segfault Claude-CLI) оставляла строку
jobs.status='running'навсегда; приmax_concurrency=1один такой зомби намертво блокировал очередь ВСЕХ проектов (self-hosting: enduro-trails встаёт из-за зомби ORCH-задачи). Плюс два смежных дефекта: застрявший merge-lease (.merge-lease-<repo>.jsonреклеймился лишь лениво по TTL при чужом acquire, живость pid-holder'а не проверялась) и неидемпотентная финализация merge (rebase+re-test зелёные, но процесс умер до самого merge → нет повторного проигрывания). Решение — новый фоновый daemon-потокsrc/job_reaper.py(контракт «never-raise на единицу работы», паттернreconciler/queue_worker): периодический тик (reaper_interval_s) сканируетrunning-jobs трёхуровневой проверкой живости (ADR Р-1): Tier-1 мёртвый pid (os.kill(pid, 0)→ProcessLookupError) с анти-false-positive порогомreaper_dead_ticksподряд-мёртвых тиков (стрик в памяти); Tier-2agent_runs.exit_codeзаписан, но job всё ещёrunning— но только после finalization-gracereaper_finalize_grace_s(окно неоднозначно: живой monitor пишет exit_code ПЕРВЫМ, затем git push/PR/Plane-комментарии и лишь потом_finalize_job, а pid агента к этому моменту мёртв в обоих случаях — живой финализирующий monitor НЕ реапится); Tier-3 backstop-потолокreaper_max_running_s. Единственная мутирующая запись reaper'а — атомарный терминальный флип черезdb.reap_running_job(... WHERE status='running')(rowcount==1 у победителя, проигравший в гонке сrequeue_running_jobs/launcher видит rowcount==0 — без двойной обработки, TC-06). Для Tier-2 exit0 действие построено по принципу claim-before-act (ADR Р-1): источник истины — канонический QG (не «exit0»), он оценивается read-only (_gate_is_green→stage_engine._run_qg, как у reconciler) ПЕРЕД claim, затем атомарный claimdoneПЕРВЫМ и только победитель claim делает gate-driven advance (_gate_driven_advance→ штатныйlauncher._try_advance_stage, кандидат-стадии агента изSTAGE_TRANSITIONS) — проигравший claim не выполняет НИКАКИХ побочных эффектов (нет дубль-advance / дубль-enqueue следующей стадии); зелёный гейт →done+advance, красный → путь неуспеха (requeue в пределахattempts<max, иначеfailed+Telegram). Реклейм lease:merge_gate.pid_alive(pid)(мёртвый pid ⇔ProcessLookupError;PermissionError/прочее → консервативно жив),reclaim_stale_lease(repo)освобождает lease на (мёртвый pid ИЛИ истёкший TTLmerge_lock_timeout_s), живой в пределах TTL не трогается; реклейм — ТОЛЬКО удаление файла-lease (release_merge_lease), без каких-либо git-операций;reclaim_all_stale_leases()обходит область merge-gate per-repo (вызывается в каждом тике reaper'а И на старте вmain.lifespan). Идемпотентность финализации:merge_gate.pr_already_merged(repo, branch)(lazyhttpx, Gitea APIpulls?state=all&head=..., любая ошибка/не-200 →False, never-raise) — guard перед повторным merge при re-drive. Точка консультации — сам merge-актор: фактический merge feature-PR вmainделает агент deployer (в начале стадииdeploy, см.webhooks/gitea.py), поэтому wiring живёт в его промпте.openclaw/agents/deployer.md— он вызывает ровноpr_already_mergedПЕРЕД любым (повторным) merge и при уже слитом PR делает no-op (без второго merge и без ошибки, AC-11). Merge-gate-чекcheck_branch_mergeableнамеренно НЕ трогается (AC-13): он исполняется на ПЕРВОМ ребреdeploy-staging → deployи не переисполняется при re-drive самой стадииdeploy— где и живёт риск второго merge. Дополнительно re-drive после восстановления короткозамыкается на «branch up-to-date with main» БЕЗ повторного дорогого rebase+re-test, если ветка уже догнана (TC-17). Новая колонкаjobs.pidчерез идемпотентный_ensure_column(без миграции, безопасно на живой прод-БД); pid агента проставляется вsrc/agents/launcher.py::_spawnПОСЛЕPopen(рядом сrun_id/started_at). Старт/стоп reaper'а вsrc/main.pylifespan (послеreconciler.start()/ передreconciler.stop()), снимокreaperвGET /queue(enabled/interval/last_run/reaped_total/last_reaped/lease_reclaimed_total). Условность раскатки реклейма lease зеркалит merge-gate (merge_gate_reposCSV или self-hostingorchestrator). Новые настройки:ORCH_REAPER_ENABLED(true, kill-switch),ORCH_REAPER_INTERVAL_S(60),ORCH_REAPER_DEAD_TICKS(2),ORCH_REAPER_MAX_RUNNING_S(3600),ORCH_REAPER_FINALIZE_GRACE_S(300),ORCH_LEASE_RECLAIM_ENABLED(true). Инварианты НЕ менялись (AC-13):STAGE_TRANSITIONS(9 стадий) и реестрQG_CHECKS(12 чеков) без новых элементов, сигнатура/поведениеcheck_branch_mergeable(repo, work_item_id, branch)intact, БАГ-8 откат и exit-код-контракт хука не тронуты; restart-safe, never-raise на единицу фоновой работы. ADRdocs/work-items/ORCH-065/06-adr/ADR-001-job-reaper-and-lease-reclaim.md, глобальныйdocs/architecture/adr/adr-0011-job-reaper-lease-reclaim.md. Тесты:tests/test_job_reaper.py,tests/test_merge_lease_reclaim.py,tests/test_merge_gate.py(TC-16),tests/test_merge_gate_race.py(TC-17),tests/test_queue.py::TestReaperUnblocksQueue,tests/test_config.py(TC-19/TC-20). - Post-deploy наблюдение прода + реакция на деградацию (ORCH-021): конвейер больше не «забывает про прод» после
deploy → done— раньше «успех» означал прохождение health-check лишь в момент рестарта (~60с-окно хука), и класс инцидентов «зелёный деплой, красный прод» (прецедент ET-8: деградация проявляется через минуты под трафиком,/healthотвечает200 ok, но фича сломана) не ловился. ORCH-021 продлевает ответственность ЗАdone: для применимого репозитория после терминального перехода армится наблюдение окнаpost_deploy_window_s(~15 мин) с интерваломpost_deploy_interval_s; деградация фиксируется детерминированными порогами, при подтверждении — реакция. Новый leaf-модульsrc/post_deploy.py(контракт «never raise», по образцуself_deploy.py/staging_verdict.py; импортирует только config + lazyqg.checks.is_self_hosting_repo):post_deploy_applies(условность раската),probe_signals(один опрос/health200+{"status":"ok"}+ доля 5xx на/status,/queue; сеть/таймаут → консервативный провал, не исключение),classify(чистая, главный предмет юнит-тестов:DEGRADED⇔≥ post_deploy_fail_thresholdПОСЛЕДОВАТЕЛЬНЫХ провалов health ИЛИ доля 5xx окна> post_deploy_5xx_threshold; иначеHEALTHY— одиночный глюк не откатывает),decide_action(self-hosting → ВСЕГДАALERT_ONLY; не-self +post_deploy_auto_rollback=true→ROLLBACK; иначеALERT_ONLY),map_rollback_exit_code(0→ROLLBACK_OK, иначеROLLBACK_FAILED), sentinel-state хелперы (armed/series/doneпод<repos_dir>/.post-deploy-state-<repo>/<wi>/, restart-safe счётчики),build_rollback_command/run_rollback(ssh-хук--rollbackс прод-env, синхронно — только для не-self),build/write_post_deploy_log(артефакт16-post-deploy-log.md),arm_monitor(идемпотентный арм + первый отложенный job),status(снимок для/queue). Механизм наблюдения — reserved-agent jobpost-deploy-monitor(детерминированный, no-LLM, калькаdeploy-finalizer, НЕ стадия и НЕ daemon): арм вstage_engine.advance_stageв блокеnext_stage == "done"ПОСЛЕ terminal-sync/release-lease (post_deploy.arm_monitor, sentinelarmed= идемпотентность при двойном webhook/reconciler/finalizer); один тик = один job — перехват вagents/launcher.launch_jobДО_spawn→stage_engine.run_post_deploy_monitor(один опрос → append вseries→classify→ перепостановка с задержкойavailable_at_delay_sИЛИ реакция+артефакт+mark_done); бюджет тиковwindow_s/interval_s(анти-livelock). Self-hosting safety (BR-5): дляorchestratorтик НИКОГДА не откатывает/рестартит прод-контейнер — реакция всегдаALERT_ONLY(громкий Telegram + Plane-коммент с запросом ручного approve); авто-rollback хуком--rollback— только для не-self репо приpost_deploy_auto_rollback=true(целевой контейнер ≠ orchestrator). Наблюдаемость — блокpost_deployвGET /queue(enabled/window/interval/активные наблюдения). Артефакт16-post-deploy-log.md(YAML-frontmatterpost_deploy_status/action_taken/window_s/checks_total/checks_failed) — машиночитаемо для петли уроков ORCH-8; best-effort. Новые настройки:ORCH_POST_DEPLOY_MONITOR_ENABLED(true, kill-switch),ORCH_POST_DEPLOY_REPOS(CSV; пусто → только self-hosting),ORCH_POST_DEPLOY_WINDOW_S(900),ORCH_POST_DEPLOY_INTERVAL_S(30),ORCH_POST_DEPLOY_FAIL_THRESHOLD(3),ORCH_POST_DEPLOY_5XX_THRESHOLD(0.5),ORCH_POST_DEPLOY_AUTO_ROLLBACK(false),ORCH_POST_DEPLOY_BASE_URL(http://localhost:8500); параметры отката переиспользуютdeploy_prod_*. Инварианты НЕ менялись:STAGE_TRANSITIONS, реестрQG_CHECKS,check_deploy_status/_parse_deploy_status, terminal-syncdeploy→done, merge-gate, exit-код-контракт хука (0/1/2), схема БД (без миграций; состояние — sentinel-файлы). Условность как ORCH-35/36/43/58. ADRdocs/work-items/ORCH-021/06-adr/ADR-001-post-deploy-monitor.md, глобальныйdocs/architecture/adr/adr-0010-post-deploy-monitor.md. Тесты:tests/test_post_deploy.py,tests/test_post_deploy_integration.py. - Провенанс staging-образа перед BUILD-ONCE retag в прод (свежесть артефакта, INV-FRESH) (ORCH-058): BUILD-ONCE retag (ORCH-036) промоутит staging-образ (
orchestrator-orchestrator-staging) в прод без rebuild, полагаясь на «образ свеж и провалидирован» — гарантии не было: конвейер нигде не пересобирал staging-образ из провалидированного коммита, поэтому retag мог тихо промоутнуть УСТАРЕВШИЙ образ (инцидент LESSONS_ORCH-036 п.4 — зелёный деплой молча откатывал прод). Закрыто двумя слоями (defense in depth), только для self-hosting. Новый модульsrc/image_freshness.py(контракт «never raise», по образцуmerge_gate):provenance_verdict(чистая функция вердикта match/mismatch/fail-closed),validated_revision(git rev-parse HEADв worktree валидированного коммита — единый якорь и для штампа A, и дляEXPECTED_REVISIONB),image_revision(OCI-лейблorg.opencontainers.image.revisionчерезdocker image inspect,<no value>/ошибка → пусто),rebuild_staging_image(ssh-хук--build-staging),image_freshness_applies(условность),check_staging_image_fresh(композитный QG). Strategy A (liveness): новый детерминированный QG-под-чекcheck_staging_image_fresh(зарегистрирован вQG_CHECKS,src/qg/checks.py) на ребреdeploy-staging → deployПОСЛЕ merge-gate и ДО Phase A — пересобирает staging-образ из worktree валидированного коммита (хук--build-staging,--build-arg GIT_SHA=<sha>), пересоздаёт 8501 и прогоняетstaging_check.py --mode stubпротив свежего 8501 (health + e2e, внутри staging-контейнера черезdocker exec— канон ORCH-048) → валидируем РОВНО тот артефакт (build + e2e), что промоутится в прод (AC-4); FAIL/не-ноль staging_check → откат наdevelopment(как merge-gate, капMAX_DEVELOPER_RETRIES).rebuild_staging_imageпробрасывает в хук явный staging-таргет (service/port/profile/container), исключая дрейф на прод 8500. Сборки/recreate/validate — только staging (8501), прод (8500) не трогается. Strategy B (safety):DockerfileштампуетLABEL org.opencontainers.image.revision=$GIT_SHA(ARG GIT_SHA);build_deploy_command(src/self_deploy.py) пробрасываетEXPECTED_REVISION; хост-хук шагом 2b ПЕРЕДdocker tagfail-closed сверяет лейблrevisionуSOURCE_IMAGEсEXPECTED_REVISION— несовпадение / пустой лейбл / ошибка inspect →exit 1(FAILED → БАГ-8 откат), делает тихий промоут устаревшего образа структурно невозможным даже при проигравшей гонку/отключённой A. Хост-хукscripts/orchestrator-deploy-hook.shрасширен обратно-совместимым режимом--build-staging(пересборка+recreate staging, exit 0/1) и fail-closed guard'ом (активен только при заданномEXPECTED_REVISION). Единый kill-switchORCH_IMAGE_FRESHNESS_ENABLED(true) включает A+B как целое (нет «B без A» = вечного fail-fast); область —ORCH_IMAGE_FRESHNESS_REPOS(CSV; пусто → только self-hostingorchestrator). Контракты НЕ менялись:STAGE_TRANSITIONS(под-гейт ребра, не стадия), exit-code-контракт хука (0/1/2),map_exit_code_to_status,check_deploy_status/_parse_deploy_status, БАГ-8, terminal-sync, merge-gate; схема БД — без миграций. ADRdocs/work-items/ORCH-058/06-adr/ADR-001-staging-image-provenance.md, глобальныйdocs/architecture/adr/adr-0008-staging-image-provenance.md. Документация:docs/architecture/README.md,docs/operations/DEPLOY_HOOK.md,docs/operations/STAGING.md,docs/operations/INFRA.md,.env.example. Тесты:tests/test_image_freshness.py,tests/test_deploy_hook_provenance.py,tests/test_deploy_build_once.py(TC-06),tests/test_deploy_hook_mapping.py(TC-09),tests/test_stage_engine.py::TestImageFreshnessGate,tests/test_qg_registry_snapshot.py,tests/test_config.py. - Исполняемый самодеплой стадии
deploy(стадия дёргает хост-хук, manual-approve) (ORCH-036): стадияdeployперестаёт быть «бумажной» — для self-hosting репозиторияorchestratordeploy_status: SUCCESSозначает ДОКАЗАННЫЙ health-ok реального рестарта прод-контейнера (8500), а не декларацию LLM. Критический путь self-restart детерминирован (без LLM), по образцу merge-gate ORCH-043, и разбит на три фазы (src/stage_engine.py+ новый модульsrc/self_deploy.py): Фаза A (вход вdeploy) — вместо запуска прод-deployer'а приdeploy_require_manual_approve=trueзадача переводится в approval-pending (set_issue_in_review) и ждёт ручного approve; restart-safe маркерapprove-requested. Фаза B (человек ставит статус Plane →Approved;advance_stage(deploy, finished_agent=None)) — запускается detached host-процесс (ssh + setsid→scripts/orchestrator-deploy-hook.sh, чтобы рестарт 8500 пережил гибель контейнера; орк НЕ убивает себя из docker.sock) с build-once retag staging-образа (SOURCE_IMAGE), ставится детерминированный finalizer-job; маркерinitiated— идемпотентность повторного Approved. Фаза C (run_deploy_finalizer, reserved-agentdeploy-finalizer, claim'ится новым контейнером после рестарта) — читает sentinelresult(exit-code хука, записан host-обёрткой),not-ready→ defer (бюджетdeploy_finalize_max_attempts, restart-safe поtask_content), маппит0→SUCCESS / 1|2|иное→FAILED(чистая функцияmap_exit_code_to_status, unit-тест), пишет14-deploy-log.mdи вызываетadvance_stage(deploy, finished_agent="deployer")→ существующие контракты:SUCCESS → done+ release merge-lease,FAILED → откат БАГ-8 на development+set_issue_blocked. Уведомления Plane+Telegram на approve-request / initiate / success / rollback (BR-5, ни одного «молчаливого» деплоя). Хост-хукscripts/orchestrator-deploy-hook.shрасширен обратно-совместимымSOURCE_IMAGE: при заданном —docker tag $SOURCE_IMAGE $TARGET_IMAGEпередup -d --no-build(деплой РОВНО протестированного образа, безdocker build); не задан → прежнее поведение; exit-code-контракт (0/1/2) и health-loop (10×6с, авто-rollback) не тронуты. Restart-safe состояние — sentinel-файлы (<repos_dir>/.deploy-state-<repo>/<work_item_id>/), без миграции БД. Условность как ORCH-35: реальный самодеплой только дляis_self_hosting_repo("orchestrator"); прочие репо (enduro-trails) — прежний синхронный ssh-путь агентом. Контракты НЕ менялись:STAGE_TRANSITIONS, реестрQG_CHECKS,check_deploy_status/_parse_deploy_status(frontmatter-only), terminal-syncdeploy→done, merge-gate (ORCH-43), БАГ-8. ФлагDEPLOY_REQUIRE_MANUAL_APPROVEостаётсяtrue(полный авто — отдельная задача ORCH-54). Новые настройки:ORCH_DEPLOY_REQUIRE_MANUAL_APPROVE(true),ORCH_DEPLOY_SSH_USER,ORCH_DEPLOY_SSH_HOST,ORCH_DEPLOY_HOOK_SCRIPT,ORCH_DEPLOY_PROD_SOURCE_IMAGE,ORCH_DEPLOY_PROD_TARGET_SERVICE/PORT/IMAGE,ORCH_DEPLOY_FINALIZE_DELAY_S,ORCH_DEPLOY_FINALIZE_MAX_ATTEMPTS. ADRdocs/work-items/ORCH-036/06-adr/ADR-001-executable-self-deploy.md, глобальныйdocs/architecture/adr/adr-0007-executable-self-deploy.md. Документация:.openclaw/agents/deployer.md(стадияdeploy= вызов хука, запрет self-restart),docs/operations/INFRA.md,docs/operations/DEPLOY_HOOK.md. Тесты:tests/test_deploy_hook_mapping.py,tests/test_deploy_approve.py,tests/test_deploy_routing.py,tests/test_deploy_rollback.py,tests/test_deploy_notifications.py,tests/test_deploy_build_once.py,tests/test_deploy_terminal_sync.py,tests/test_staging_precondition.py,tests/test_deploy_hook_rollback_sim.py. - Sweeper потерянных webhook (реконсиляция застрявших стадий) (ORCH-053): фоновый daemon-поток
src/reconciler.py(паттернqueue_worker), который устраняет тихое застревание задач, когда конвейер не двигается из-за потерянного события (502 на ребилде инстанса, отсутствие ретраев у Plane/Gitea, неразрезолвленныйsha→branch— класс инцидента ORCH-044). Реконсилятор периодически (reconcile_interval_s) доигрывает пропущенный переход через те же штатные гейты/обработчики, что и webhook, не дублируя логику конвейера: F-1 gate-side (reconcile_gate_once) — для задачstage≠done, без активного job иage(updated_at) ≥ grace_for_stage(stage)делает read-only пред-оценку канонического QG стадии; зелёный → продвижение строго через неизменныйstage_engine.advance_stage(..., finished_agent=None); красный → тишина (спам нотификаций структурно невозможен —advance_stageна красном гейте не вызывается вовсе);analysisF-1 не трогает (человеческий гейт). F-2 plane-side (reconcile_plane_once) — опрос Plane API per-project (новыйplane_sync.list_issues_by_state, курсорная пагинация, never-raise) и реплей In Progress / Approved / Rejected через существующиеwebhooks.plane.handle_status_start/handle_verdict(async-обработчики вызываются из sync-потока черезasyncio.run). F-3 — усилениеsha→branchвhandle_ci_status: при неразрезолвленном sha — БД-fallback по единственной development-задаче repo (db.get_development_tasks_by_repo; неоднозначность → не резолвим, ложного матча нет),logger.debug→logger.infoдля видимости потерянного CI-события. Анти-дубль на создании задачи (db.create_task_atomicпод process-widethreading.Lock: SELECT-exists→INSERT, проигравший в гонке reconcile↔webhook не плодит второй task/branch/worktree/стартовый analyst-job). Старт/стоп вmain.lifespan(послеworker.start()/ передworker.stop()), restart-safe, never-raise на единицу работы. Наблюдаемость (F-4): при разблокировке — лог-строкаreconciler: <wi> <stage> разблокирована (потерян webhook)+ Telegram (reconcile_notify_unblock) и блокreconcileвGET /queue. Kill-switches:ORCH_RECONCILE_ENABLED(глобально),ORCH_RECONCILE_PLANE_ENABLED(гасит только F-2),ORCH_RECONCILE_INTERVAL_S(120),ORCH_RECONCILE_GRACE_DEFAULT_S(600),ORCH_RECONCILE_GRACE_OVERRIDES_JSON(per-stage),ORCH_RECONCILE_NOTIFY_UNBLOCK(true). Схема БД и реестры (STAGE_TRANSITIONS/QG_CHECKS) НЕ менялись. ADRdocs/work-items/ORCH-053/06-adr/ADR-001-stuck-task-reconciler.md, глобальныйdocs/architecture/adr/adr-0007-reconciler.md. Тесты:tests/test_reconciler.py,tests/test_reconciler_plane.py,tests/test_gitea_sha_resolve.py,tests/test_config.py. - Merge-gate: авто-rebase на текущий
origin/main+ повторный прогон тестов + сериализация мержей (ORCH-043): детерминированный (без LLM) суб-гейт на ребреdeploy-staging → deploy, выполняемый ПЕРЕД мержем PR деплоером. Закрывает класс гонок «две зелёные ветки в одном репо ломаютmain»: пайплайн валидирует ветку против тогоmain, от которого она ответвилась, а не противmainв момент мержа — между «ветка зелёная» и «ветка смержена» параллельная задача может сдвинутьmain(семантический конфликт: git мержит без текстового конфликта, но совмещённыйmainкрасный). Для self-hosting репозиторияorchestratorэто означало бы красныйmainинструмента, обслуживающего ВСЕ проекты. Новый модульsrc/merge_gate.py(контракт «never raise», все git-операции — в per-branch worktree, ORCH-2/S-4):branch_is_behind_main(git merge-base --is-ancestor origin/main HEAD),auto_rebase_onto_main(rebase +git push --force-with-leaseТОЛЬКО ветки задачи —mainНИКОГДА не пушится; текстовый конфликт →rebase --abort+ чистый worktree),retest_branch(python -m pytest <target>в догнанном worktree, бюджетmerge_retest_timeout_s), файловый merge-lease (acquire_merge_lease/release_merge_lease, атомарныйO_CREAT|O_EXCL, holder-aware release, реклейм протухшего/битого лиза — без изменения схемы БД). Новый quality-gatecheck_branch_mergeable(src/qg/checks.py, зарегистрирован вQG_CHECKS) композирует примитивы под лизом: kill-switch/вне-области → no-op pass; lock занят →(False, "merge-lock busy")(сигнал DEFER, не код-фолт); ветка свежая → pass (лиз ДЕРЖИТСЯ до мержа); отстала → rebase → конфликт = fail+release, чисто → retest → зелёный = pass (лиз держится) / красный|timeout = fail+release. Интеграция вsrc/stage_engine.py(суб-гейт наdeploy-staging, БЕЗ новой стадии вSTAGE_TRANSITIONS): pass → advance наdeploy; «merge-lock busy» → DEFER (повторная постановка деплоера наdeploy-stagingс задержкойavailable_at, анти-дедлок приmax_concurrency=1, restart-safe счётчик поtask_content, лимитmerge_defer_max_attempts→ block+Telegram); конфликт/красный retest → ROLLBACK наdevelopment+ ретрай developer-а (капMAX_DEVELOPER_RETRIES, без бесконечного баунса). Лиз освобождается наdeploy→done, на rollback и по webhook смерженного PR (src/webhooks/gitea.py). Новый параметрenqueue_job(..., available_at_delay_s=...)(src/db.py) — отложенная постановка без изменения схемы. Условность раскатки (зеркало ORCH-35):merge_gate_repos(CSV) или по умолчанию только self-hostingorchestrator; глобальный kill-switchmerge_gate_enabled. Новые настройкиORCH_MERGE_GATE_ENABLED(true),ORCH_MERGE_GATE_REPOS(""),ORCH_MERGE_RETEST_TIMEOUT_S(600),ORCH_MERGE_RETEST_TARGET(tests/),ORCH_MERGE_LOCK_TIMEOUT_S(300),ORCH_MERGE_DEFER_DELAY_S(60),ORCH_MERGE_DEFER_MAX_ATTEMPTS(5). ADRdocs/work-items/ORCH-043/06-adr/ADR-001-merge-gate.md, глобальныйdocs/architecture/adr/adr-0006-merge-gate.md. Тесты:tests/test_merge_gate.py,tests/test_qg_merge_gate.py,tests/test_merge_gate_race.py,tests/test_stage_engine.py::TestMergeGate,tests/test_config.py. - Режим
bumplive-трекера Telegram (ORCH-042): новыйORCH_TRACKER_MODE(Settings.tracker_mode, дефолтedit) выбирает поведение карточки задачи.edit(как было) — карточка редактируется на месте (editMessageText).bump— на каждом обновлении старое сообщение удаляется и карточка отправляется заново вниз чата (best-effortdelete_telegram(старый_id)→send_telegram(text, disable_notification=True)→set_tracker_message_id(new_id)), чтобы актуальный статус всегда был последним в чате при активной переписке. Инвариант «одна карточка на задачу» сохранён в обоих режимах: за один вызовupdate_task_trackerшлётся ≤1 нового сообщения;set_tracker_message_idвызывается ТОЛЬКО при успешном send (транзиентныйNoneне затирает указатель); результат delete НЕ блокирует отправку новой карточки (delete-fail у сообщения >48ч → всё равно шлём новое). Резолюция режима вnotifications(case-insensitive, trim): всё, что ≠"bump"(включая пустое/мусор) →edit→ нулевая регрессия и оркестратор не падает на любом значении флага. Новый low-level helperdelete_telegram(message_id) -> bool(контракт «never raises», маркеры_DELETE_GONE_MARKERS):ok:trueили «уже нет / нельзя удалить» →True; неизвестныйok:false/5xx/исключение →False; нет кредов →Falseбез HTTP. Сигнатурыsend_telegram/edit_telegram/update_task_trackerи схема БД (tasks.tracker_message_id) не менялись. ADRdocs/work-items/ORCH-042/06-adr/ADR-001-tracker-bump-mode.md. Тесты:tests/test_tracker_bump.py,tests/test_config.py. - Дословный текст findings reviewer/tester встраивается в
task_descзаворота (ORCH-046): при откате наdevelopmentстрокаtask_desc(попадает в.task-dev.mddeveloper-агента) теперь несёт суть претензий, а не только ссылку на файл — устраняет «испорченный телефон», из-за которого агент шёл «читать файл», терял ключевые P0/P1 / причину FAIL и заворачивался снова, выжигаяMAX_DEVELOPER_RETRIESи токены. Новый defensive-модульsrc/review_parse.py(контракт «never raise», какsrc/frontmatter.py):extract_review_findings(path)— дословные пункты P0/P1 из секции## Findingsфайла12-review.md;extract_test_failures(path)— релевантный фрагмент тела13-test-report.md(приоритет## Вывод pytest→ FAIL-строки## Результаты→## Итог). Обе функции усекают результат доMAX_FINDINGS_CHARS/MAX_FAILURES_CHARS(≈2000) с маркером…(truncated). Две rollback-веткиsrc/stage_engine.py(reviewer REQUEST_CHANGES, testercheck_tests_passedFAIL) встраивают извлечённый текст и сохраняют ссылку на полный файл («Полный контекст»); при пустом/битом артефакте — graceful-фоллбэк на прежнюю ссылку-строку (никаких исключений вadvance_stage). Tester-ветка дополнительно всегда включаетreasonгейта. Последовательность отката,_developer_retry_count, поляAdvanceResultи реестрQG_CHECKSне менялись. ADRdocs/work-items/ORCH-046/06-adr/ADR-001-embed-findings-in-task-desc.md. Тесты:tests/test_review_parse.py,tests/test_stage_engine.py::TestRollbackTaskDescEmbedding. - Поллинг с ретраем в quality-gate
check_ci_green(ORCH-045): гейт CI превращён из single-shot в polling, чтобы устранить race condition — раньше один опрос combined commit-status сразу после пуша developer-а ловил транзиентныйpending(типично 1-3с, реальный кейс ORCH-017: опрос 17:58:54 → pending, CI дозеленел 17:58:55) и задача застревала насмерть без повторного опроса. Теперь:success→ пропуск сразу;failure/error→ провал сразу (терминально, ретрай бессмыслен);pending/unknown →time.sleepи повторный опрос доci_poll_max_attemptsраз; истечение попыток → явный(False, "CI still pending after <T>s")(тупик больше не молчаливый); 404 → как раньше; транзиентнаяhttpx.HTTPErrorна попытке логируется и ретраится в рамках бюджета. Параметры — новые настройкиORCH_CI_POLL_MAX_ATTEMPTS(12) иORCH_CI_POLL_INTERVAL_S(10) вsrc/config.py(~2 мин ожидания pending). Сигнатураcheck_ci_green(repo, branch)и реестрQG_CHECKSне менялись;check_tests_passedне затронут. ADRdocs/architecture/adr/adr-0004-ci-poll-retry.md. Тесты:tests/test_qg.py::TestCheckCIGreen. - Прямые ссылки на BRD и Plane-таску в Telegram-уведомлении об апруве (ORCH-017): пингующее сообщение
notify_approve_requestedтеперь встраивает две HTML-<a>-ссылки — наdocs/work-items/<WI>/01-brd.md(Gitea branch-view:gitea_public_url→gitea_url) и на issue в Plane ({web_base}/{workspace}/projects/{project_id}/issues/{plane_issue_id}/). Новая настройкаORCH_PLANE_WEB_URL(внешний браузерный web-URL Plane; фолбэк наplane_api_url). Loopback-guard: если итоговый Plane web-base указывает на localhost/127.0.0.1/0.0.0.0/::1 или пуст — Plane-ссылка опускается (не выпускаем битый localhost-URL). Graceful degradation: каждая ссылка строится независимо и опускается при нехватке данных, сообщение и призыв «Переведите задачу в статус Approved …» сохраняются всегда; ровно одно пингующее сообщение, разделяемаяsend_telegramне тронута. Динамические подписи экранируютсяhtml.escape,parse_mode=HTMLсохранён. ADRdocs/work-items/ORCH-017/06-adr/ADR-001-telegram-approve-links.md. Тесты:test_notify_approve_links.py,test_analysis_approve_flow_links.py. - Конфигурируемые модель LLM и режим работы (
--effort) агентов (ORCH-41): модель/effort каждого агента вынесены из хардкодаlauncher.pyв конфиг — глобально per-agent (ORCH_AGENT_MODEL_<AGENT>/ORCH_AGENT_EFFORT_<AGENT>, дефолтыORCH_AGENT_MODEL_DEFAULT=claude-opus-4-8,ORCH_AGENT_EFFORT_DEFAULT=high) и per-project (agent_models/agent_effortsвORCH_PROJECTS_JSON). Резолверыresolve_agent_model/resolve_agent_effort(приоритет project > per-agent env > default > пусто), валидация effort{low,medium,high,xhigh,max}, опц.ORCH_AGENT_FALLBACK_MODEL(--fallback-model). Хардкод"model":"opus"(architect/reviewer) удалён. Тесты:test_resolve_agent_model.py,test_resolve_agent_effort.py. - Единый status-коммент агентов в Plane (ORCH-016):
usage.build_status_comment(...)— один хелпер для ВСЕХ ролей (analyst..deployer). HTML-формат: header{icon} {Role} — {описание}, опциональная строкаVerdict/Status: …из YAML-frontmatter артефакта, строкаДлительность: 4m 12s(явныйduration_sот launcher, fallback изagent_runsдля аналитика),<b>Документы:</b><ul><li><a>…</a></li></ul>, тех-хвост<sub>tokens · cost</sub>. Утилитки:usage.fmt_duration,usage.get_agent_duration, новый модульsrc/frontmatter.py(defensive YAML reader). ADRdocs/work-items/ORCH-016/06-adr/ADR-001-unified-status-comment.md. - Документация по канону (ORCH-9):
CLAUDE.md(паспорт проекта), структураdocs/(architecture/+adr/,operations/,work-items/,history/),docs/operations/INFRA.md(RUNBOOK с инфра-изоляцией и self-hosting рисками). - ADR: adr-0001 (multi-repo registry), adr-0002 (job queue), adr-0003 (условный staging-гейт).
- Стадия
deploy-staging(ORCH-35): промежуточный гейт междуtestingиdeploy. QGcheck_staging_status(условный, только для self-hosting repo). PR #31. - Деплой-хук (ORCH-34):
scripts/orchestrator-deploy-hook.shс health-check и авто-rollback. PR #30. - Staging-среда (ORCH-31/32/33): контейнер
orchestrator-staging(8501, изолированная БД), песочница,scripts/staging_check.py. PR #28/#29. - Очередь задач (ORCH-1): таблица
jobs,queue_worker.py, atomic claim, max_concurrency, ретраи, restart-safe, эндпоинт/queue. - Реестр проектов (ORCH-6):
src/projects.py, фильтрация вебхуков по проекту.
Changed
- Русификация и косметика карточки live-трекера Telegram (ORCH-042, оба режима): метка
Подтверждение BRDвместо «Ревью БРД» (_BRD_LABEL); после прохождения approve-gate строка подтверждения BRD начинается с ✅ вместо ⏸️ (ветка ожидания человека сохраняет ⏸️/⏳); русские display-labels стадий в_TRACKER_STAGES(Анализ / Архитектура / Разработка / Код ревью / Тестирование / Внедрение) — применяются и в «✅ …», и в «🔄 … идёт»; финальная строка готовой задачи📦 Внедреновместоdeployed(_done_link). Меняются только отображаемые строки — ключи стадий и имена агентов не трогаются. Существующие ассертыtests/test_telegram_tracker.pyобновлены под русские метки. - Status-коммент агентов теперь HTML и единообразен (ORCH-016):
src/usage.usage_comment(...)помечен deprecated и стал тонкой обёрткой надbuild_status_comment;src/usage.artifact_links(...)теперь возвращает<li><a>…</a></li>HTML-фрагменты (раньше — markdown[label](url));stage_engine._build_analyst_ready_comment(...)— тонкая обёртка, аналитик идёт через ту же веткуbuild_status_comment(agent="analyst", ...). РеестрQG_CHECKSиSTAGE_TRANSITIONSНЕ изменялись. - Цепочка стадий:
... testing → deploy-staging → deploy → done(была безdeploy-staging).
Fixed
- Reconciler (F-2) больше не зацикливается на спаме «разблокирована» по синхронизированной done-задаче (ORCH-068): после мерджа новой статусной модели Plane (ORCH-066) sweeper потерянных webhook (ORCH-053) каждые ~120с слал в Telegram
reconciler: ET-002 done разблокирована (потерян webhook)для полностью синхронизированной задачи (БДstage=done, Planestate=Done) — livelock без advance/jobs/токенов, но 191+ сообщений за ночь (alert-fatigue, подрыв доверия к нотификациям). Два независимых складывающихся дефекта (defense in depth, ADR-001): D1 (выборка) — F-2 различал actionable-статусы по голому UUID, а после ORCH-066 терминальныйDoneперестал однозначно отличаться отapprovedпо UUID (UUID-алиасинг) и done-issue попадала в веткуapproved; терминалы нигде не исключались. Решение: исключение терминалов по группе состояния Plane (state.group ∈ {completed, cancelled}) — проектно-независимый, устойчивый к переименованиям дискриминатор; проверка per-issue (а не сужениемwanted-набора, т.к. при алиасинге терминал физически совпадает с actionable-UUID); fallback по логическим ключамdone/cancelled, когда группа недоступна.get_project_statesрасширен записью{uuid → group}из ТОГО ЖЕ/states/-запроса (без новой сетевой стоимости) + sibling-аксессорget_project_state_groups. D2 (нотификация) —_note_unblockвызывался безусловно сразу после_dispatch, не проверяя, изменил ли обработчик реально состояние;handle_verdict(approved)для уже-doneзадачи — no-op, но нотификация всё равно уходила (нарушение собственного docstring и инварианта silence-when-in-sync). Решение: сравнение стадии задачи до/после_dispatchна стороне reconciler (контрактыhandle_*НЕ тронуты) —_note_unblockтолько при подтверждённом state change; для in_progress-старта подтверждение = задача появилась. Плюс TR-3 — in-memory дедуп-guard{issue_id → last_unblocked_state}(страховка против любого будущего no-op-пути). Вторичный баг кэша (TR-4):_STATES_CACHEжил весь lifetime процесса → новый Plane-статус был невидим без рестарта («stale set → no pipeline action»); добавлен TTLORCH_PLANE_STATES_TTL_S(дефолт 300с;0→ прежний lifetime-кэш) — запись самозалечивается перезапросом/states/(примитив сброса — существующийreload_project_states()); при сбое перезапроса отдаётся stale-but-correct набор, а не enduro-дефолты. Форма возвратаget_project_statesнеизменна (AC-13).STAGE_TRANSITIONS,QG_CHECKS, схема БД, контрактыhandle_status_start/handle_verdict, F-1/F-3 — не тронуты; never-raise per-issue/-project сохранён; self-hosting — тик не рестартит прод. Наблюдаемость: счётчикиskipped_terminal_total/deduped_totalв блокеreconcileснимкаGET /queue. ADRdocs/work-items/ORCH-068/06-adr/ADR-001-reconciler-terminal-exclusion-and-cache-ttl.md. Тесты:tests/test_reconciler_plane.py(TC-01…TC-10),tests/test_plane_states_cache.py(TC-11/TC-12). - Staging-rebuild больше не падает на
COPY data/(worktree-контекст) (ORCH-021):check_staging_image_fresh(ORCH-058, Strategy A) пересобирает staging-образ с worktree задачи в качестве docker build context (docker build … "$BUILD_CONTEXT"). Свежий git-worktree содержит только трекаемые файлы, аDockerfileделалCOPY data/ ./data/— ноdata/(директория SQLite) gitignored и в worktree-контексте отсутствует →docker buildпадал сexit 1(«BUILD-STAGING: docker build failed - aborting»), задачу заворачивало сdeploy-stagingнаdevelopment(петля, выжигание developer-ретраев, инцидент текущего прогона ORCH-021). При этом COPY был мёртвым грузом:data/всегда приходит рантайм-volume'ом (./data:/app/data/./data/staging:/app/dataвdocker-compose.yml), который затеняет всё, что было запечено в образ. Заменено наRUN mkdir -p /app/data(директория-mountpoint существует и без bind-mount, без зависимости от build-контекста). КонтрактыSTAGE_TRANSITIONS/QG_CHECKS, штампLABEL org.opencontainers.image.revision=$GIT_SHA(ORCH-058 Strategy B), exit-код-контракт хука — не тронуты. Регресс-гард:tests/test_deploy_hook_provenance.py::test_tc08b_dockerfile_does_not_copy_gitignored_data_dir(запрещаетCOPYлюбого gitignored-пути). deploy-stagingбольше не зацикливается на infra-only FAIL песочницы (C9a/C9b) (ORCH-061): self-hostingorchestratorкрутился в петлеdeploy-staging → development—scripts/staging_check.pyдавалexit 1при ЛЮБОМ упавшем чеке, поэтому две чисто инфраструктурные проверки C9a (ветка не появилась вorchestrator-sandbox) и C9b (job аналитика не встал в очередь staging) — вызванные тем, что SANDBOX-бот-аккаунты не состоят в sandbox-проекте Plane (шаги 6+ конвейера в песочнице недостижимы, это НЕ регресс конвейера) — приводили кstaging_status: FAILED→ откат → цикл (выжигание developer-ретраев, токенов, паразитная нагрузка общего инстанса). Решение (Direction «б», ADR-001): чеки классифицируются наREAL(все проверки конвейера A*/B*/C7/C8 — fail-closed) иSANDBOX_INFRA(строго allowlist{C9a, C9b}— waivable). Новый leaf-модульsrc/staging_verdict.py(stdlib-only, контракт «never raise», по образцуmerge_gate/image_freshness):classify_check(label)(allowlist по ведущему токену, всё неизвестное/малформенное →REALfail-closed) иcompute_staging_verdict(items, infra_tolerant) -> StagingVerdict: любой REAL-FAIL →FAILED/exit 1 (страховка при ЛЮБОМ значении флага); упали ТОЛЬКО C9a/C9b и толерантность включена →SUCCESS/exit 0 + упавшие метки вwaived(наблюдаемость); только C9a/C9b и толерантность выключена →FAILED/exit 1 (legacy-строгий); любая внутренняя ошибка вердикта →FAILED/exit 1 (никогда не ложный green).scripts/staging_check.py:Resultsавто-классифицирует каждый чек (публичная 3-tuple форма_itemsсохранена — регрессия-гард ORCH-048 b6),categorized_items()отдаёт категорию,summary()печатает разбивку REAL/SANDBOX_INFRA;main()сворачивает прогон через_verdict(...), печатает строкиINFRA-WAIVED:/VERDICT:и делаетsys.exit(verdict.exit_code); новый флаг--strictфорсит строгий режим для одного запуска. Глобальный kill-switchORCH_STAGING_INFRA_TOLERANCE_ENABLED(Settings.staging_infra_tolerance_enabled, defaulttrue;false→ строгий 1:1 до ORCH-061), живёт в.env.staging;--strictимеет приоритет над env. Наблюдаемость на стороне конвейера:src/agents/launcher.pyполучилaction_stage_no_changes_note(stage, repo)— на action-стадиях (deploy-staging/deploy) self-hosting-репо «нет изменений для коммита» логируется как ожидаемое, а не трактуется как недопоставка. Контракты НЕ менялись:STAGE_TRANSITIONS, реестрQG_CHECKS, frontmatterstaging_status: SUCCESS|FAILED/deploy_status:(толерантность применяется в скрипте ДО записи артефакта деплоером), exit-code-контракт хука (0/1/2),check_staging_status/_parse_staging_status; схема БД — без миграций. ADRdocs/work-items/ORCH-061/06-adr/ADR-001-staging-infra-tolerance.md. Документация:docs/architecture/README.md,docs/operations/STAGING_CHECK.md,.openclaw/agents/deployer.md. Тесты:tests/test_staging_check_b6.py,tests/test_qg_checks.py,tests/test_config.py,tests/test_launcher.py,tests/test_qg.py,tests/test_stage_engine.py::TestStagingInfraTolerance.- Reconciler (F-1) больше не разблокирует escalated / Blocked / Needs-Input задачи (ORCH-060): sweeper потерянных webhook (ORCH-053) не отличал «застряла из-за потерянного события» от «исчерпала лимит developer-ретраев и ждёт человека» — если CI зелёный, а reviewer слал REQUEST_CHANGES до
MAX_DEVELOPER_RETRIES, каждый тик F-1 видел зелёныйcheck_ci_greenи доигрывалdevelopment → review→ reviewer снова REQUEST_CHANGES → откат (стадия не меняется, escalated вgitea.pyлишь шлётnotify_error) → следующий тик снова разблокировал. Бесконечная петля (инцидент ET-013: 10 разблокировок за ночь, лишние запуски агентов/токены, спам в Telegram, паразитная нагрузка общего self-hosting-инстанса). ВReconciler._reconcile_gate_task(src/reconciler.py) ПОСЛЕ существующих гардов (analysiscarve-out, нет гейта, активный job, grace) и ДО пред-оценки гейта добавлены два пред-гарда с раннимreturn(молчаливый skip — безadvance, без инкрементаunblocked_total, без нотификаций): Guard 1 (escalated, детерминированный, без сети, проверяется первым) —developer_retry_count(task_id) >= MAX_DEVELOPER_RETRIES; приватныйstage_engine._developer_retry_countповышен до публичногоdeveloper_retry_count(единый источник истины по подсчёту ретраевagent_runs, приватное имя сохранено как алиас), граница берётся изstage_engine.MAX_DEVELOPER_RETRIES(не хардкод3). Guard 2 (явный человеческий Plane-статус, Вариант A — без миграции БД) — новый never-raise хелперplane_sync.fetch_issue_state(issue_id, project_id) -> str|None(тот же endpoint/headers, чтоfetch_issue_sequence_id) +Reconciler._is_blocked_or_needs_input(task): резолв проекта (projects.get_project_by_repo) →get_project_states(pid)→ сверка текущего state issue сblocked/needs_input; любая ошибка/None/нерезолвленный проект → консервативный skip (True: не-разблокировать безопаснее). F-2 по существу не менялся: Blocked/Needs Input не входят в опрашиваемый набор{in_progress, approved, rejected}→ не доигрываются (зафиксировано регресс-тестом). Новый под-флагORCH_RECONCILE_SKIP_BLOCKED_ENABLED(true) гасит ТОЛЬКО сетевой Guard 2 (escape hatch при Plane-outage); Guard 1 всегда активен. Схема БД,STAGE_TRANSITIONS,QG_CHECKS, never-raise на единицу работы,analysiscarve-out и kill-switch'и (reconcile_enabled/reconcile_plane_enabled) не менялись. ADRdocs/work-items/ORCH-060/06-adr/ADR-001-reconciler-skip-escalated.md. Тесты:tests/test_reconciler.py(TC-01…TC-11 + регресс ORCH-053). - Re-deploy после отката больше не зависает на
deploy;.env.exampleдополнен (ORCH-036, review-fix): sentinel-маркеры самодеплоя (approve-requested/initiated/result) ключуются по стабильномуwork_item_id, поэтому при FAILED-деплое и откате БАГ-8 (deploy → development) они оставались на диске — после фикса developer-ом и повторного захода задачи наdeployФаза B по idempotency-guard видела STALEinitiatedи становилась no-op: detached-хук не перезапускался, finalizer не ставился, задача висела наdeployнавсегда (нарушался retry-контракт стадии, AC-4/AC-10; устаревшийresultк тому же был бы перечитан новым finalizer'ом). Добавленself_deploy.clear_state(repo, work_item_id)(never-raise, idempotent, рекурсивное удаление<repos_dir>/.deploy-state-<repo>/<wi>/), вызывается в ветке БАГ-8-откатаcheck_deploy_statusFAILED (src/stage_engine.py) и дополнительно в начале Фазы A (_handle_self_deploy_phase_a) — каждый новый прод-деплой-проход стартует с чистого состояния. Отдельно: канонический.env.example(CLAUDE.md правило №8, ТЗ §2.6) дополнен полным блоком новых дескрипторовORCH_SELF_DEPLOY_*/ORCH_DEPLOY_*(плейсхолдеры, секреты не коммитятся) по образцу merge-gate ORCH-043. КонтрактыSTAGE_TRANSITIONS/QG_CHECKS/_parse_deploy_status/ БАГ-8 / merge-gate не тронуты. Тесты:tests/test_deploy_rollback.py::test_tc11_re_deploy_after_rollback_not_wedged,tests/test_deploy_hook_mapping.py::test_clear_state_removes_all_markers_and_is_idempotent. - Контейнер и агенты бегут под uid хоста (1000:1000), не root (ORCH-040): оба сервиса в
docker-compose.yml(orchestrator,orchestrator-staging) получилиuser: "1000:1000"(slin) — устраняет корень проблемы, при которой Claude-CLI агенты, запускаемые черезsubprocess.Popenвнутри root-контейнера, создавали все артефакты конвейера (git worktree/repos/_wt/..., коммиты вdocs/work-items/...) с владельцемroot:rootна хосте, из-за чегоgit pull/git resetпод slin падали сinsufficient permission for adding an objectи каждый деплой требовал ручногоchown. Теперь файлы сразуslin:slin. Доступ к docker.sock сохранён черезgroup_add: ["999"](МИНА 1 — НЕ удалена). SSH-маунт приведён к единому HOME агента: target/root/.ssh→/home/slin/.ssh(/home/slin/.orchestrator-ssh:/home/slin/.ssh:ro), синхронно сHOME=/home/slin, который launcher форсит в env Popen и git_env — устранён скрытый рассинхрон SSH-маунта с форсимым HOME.src/agents/launcher.pyиDockerfileНЕ менялись (numeric uid работает без записи в/etc/passwd;safe.directory '*'уже покрывает git над bind-mount). Требует host-prerequisites Owner (P-1…P-4, вне кода): блокер P-1 —chown -R 1000:1000 /home/slin/.claudeдля доступа uid 1000 к claude creds (иначе preflight заворачивает конвейер); прод-рестарт self — только в окно тишины (общий инстанс с enduro-trails), страховка — staging-гейт (adr-0003). ADRdocs/work-items/ORCH-040/06-adr/ADR-001-run-agents-as-host-uid.md, глобальныйdocs/architecture/adr/adr-0005-container-runs-as-host-uid.md; INFRA.md обновлён (рантайм-uid, volumes/SSH target, host-prerequisites). Тесты:tests/test_orch040_compose.py. - Staging-чек B6 читает реестр из окружения работающего staging-инстанса (ORCH-048): блок B6 «Registry: sandbox present, prod ET/ORCH absent» в
scripts/staging_check.pyдавал ложный FAIL (prod-ET=YES(BAD!),prod-ORCH=YES(BAD!)) при фактически исправной изоляции — единственный чек suite, который не ходил к инстансу по HTTP, а импортировалsrc.projectsлокально через host-path хакsys.path.insert(0, "/repos/orchestrator")+importlib.reload, строя реестр изORCH_PROJECTS_JSONprocess-env запускающего процесса. При фактическом запуске деплоером с хоста переменная не задана → дефолт_DEFAULT_PROJECTS(ET+ORCH) → ложный FAIL → лишний откатdeploy-staging → development. Решение (вариант «в», ADR-001): host-path хак удалён; suite канонически запускается ВНУТРИ контейнераorchestrator-stagingчерезdocker exec … python3 /repos/orchestrator/scripts/staging_check.py(scripts/доступен только через bind-mount,import src.projectsрезолвится черезPYTHONPATH=/appиз кода контейнера, env —.env.staging) → B6 читает реестр именно работающего инстанса, без HTTP-bootstrap и «курицы-яйца». Логика вердикта вынесена в чистую_evaluate_b6(known) -> (passed, detail)(инвариантpassed ⟺ SANDBOX ∈ known ∧ PROD_ET ∉ known ∧ PROD_ORCH ∉ known, формат detail сохранён) +_known_project_ids_from_registry()/_run_b6()с детерминированным FAIL при недоступности источника (не ложный PASS, не необработанное исключение). Синхронно обновлены.openclaw/agents/deployer.md(команда стадии черезdocker exec) иdocs/operations/STAGING_CHECK.md.src/projects.py,.env*и прочие чеки A/B4/B5/C не тронуты; реестрQG_CHECKSиcheck_staging_status(ADR-0003) не менялись. ADRdocs/work-items/ORCH-048/06-adr/ADR-001-b6-registry-via-in-container-run.md. Тесты:tests/test_staging_check_b6.py. - Testing-гейт
check_tests_passedчитаетresult:наравне сverdict:/status:(ORCH-047): парсер_parse_tests_verdict(src/qg/checks.py) теперь принимает три равноправных машиночитаемых поля frontmatter13-test-report.md—result:(канон промпта тестера.openclaw/agents/tester.md,result: PASS|FAIL), плюс легасиverdict:иstatus:(enduro-trails ET-001..ET-014); достаточно любого одного непустого. Устраняет рассинхрон контракта: тестер честно эмитилresult: PASSбезverdict:/status:, парсер попадал в ветку «нет машинного вердикта» → откатtesting → developmentв петлю до исчерпанияMAX_DEVELOPER_RETRIES(наблюдалось на ORCH-17; ORCH-016 прошёл лишь из-за избыточного дублирования полей). Семантика приоритетов сохранена и распространена на все три поля через объединённую строку: negative-токен в любом поле авторитетен (перебивает positive), наборы токенов заморожены (обратная совместимость). Сигнатура гейта, имя и реестрQG_CHECKSне менялись. ADRdocs/work-items/ORCH-047/06-adr/ADR-001-result-field-in-tests-gate.md. Тесты:tests/test_qg.py::TestCheckTestsPassed. - БАГ-8: провал deploy/deploy-staging → корректный откат на
development. - Изоляция тестов от живого Plane API (PR #27): autouse-фикстура сброса settings.
Историю до введения канона см. в docs/history/ (BUGFIXES_, LESSONS_, INCIDENT_).*