Files
orchestrator/CHANGELOG.md
claude-bot 5a7f8d4000
All checks were successful
CI / test (push) Successful in 16s
CI / test (pull_request) Successful in 17s
feat(reconciler): sweeper потерянных webhook (реконсиляция застрявших стадий)
Конвейер продвигается только входящими webhook; потерянное событие (502 на
ребилде, отсутствие ретраев у Plane/Gitea, неразрезолвленный sha→branch)
оставляет задачу молча застрявшей (класс инцидента ORCH-044). Новый фоновый
daemon-поток src/reconciler.py (паттерн queue_worker) доигрывает пропущенный
переход через те же штатные гейты/обработчики, что и webhook:

- F-1 gate-side: для задач stage≠done, без активного job и age(updated_at) ≥
  grace_for_stage(stage) — read-only пред-оценка канонического QG; зелёный →
  stage_engine.advance_stage(..., finished_agent=None); красный → тишина (спам
  нотификаций структурно невозможен). analysis F-1 не трогает (человеческий гейт).
- F-2 plane-side: опрос Plane API per-project (plane_sync.list_issues_by_state,
  курсорная пагинация, never-raise) → реплей In Progress/Approved/Rejected через
  существующие handle_status_start/handle_verdict (async из sync-потока, asyncio.run).
- F-3: усиление sha→branch в handle_ci_status — БД-fallback по единственной
  development-задаче repo (неоднозначность → не резолвим), debug→info.
- Анти-дубль на создании (db.create_task_atomic под process-wide Lock): гонка
  reconcile↔webhook не плодит второй task/branch/worktree/analyst-job (AC-4).
- F-4 observability: лог-строка разблокировки + Telegram + блок reconcile в /queue.

Старт/стоп в main.lifespan (после worker.start() / перед worker.stop()),
restart-safe, never-raise на единицу работы. Kill-switches ORCH_RECONCILE_ENABLED
/ ORCH_RECONCILE_PLANE_ENABLED + grace-настройки. Схема БД и реестры
STAGE_TRANSITIONS/QG_CHECKS не менялись.

Тесты: test_reconciler.py, test_reconciler_plane.py, test_gitea_sha_resolve.py,
test_config.py (33 новых, 563 всего зелёные). Документация обновлена (golden source):
architecture/README.md, INFRA.md, README.md, CHANGELOG.md, adr-0007 → accepted.

Refs: ORCH-053

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 20:37:01 +00:00

26 KiB
Raw Blame History

Changelog

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

[Unreleased]

Added

  • Sweeper потерянных webhook (реконсиляция застрявших стадий) (ORCH-053): фоновый daemon-поток src/reconciler.py (паттерн queue_worker), который устраняет тихое застревание задач, когда конвейер не двигается из-за потерянного события (502 на ребилде инстанса, отсутствие ретраев у Plane/Gitea, неразрезолвленный sha→branch — класс инцидента ORCH-044). Реконсилятор периодически (reconcile_interval_s) доигрывает пропущенный переход через те же штатные гейты/обработчики, что и webhook, не дублируя логику конвейера: F-1 gate-side (reconcile_gate_once) — для задач stage≠done, без активного job и age(updated_at) ≥ grace_for_stage(stage) делает read-only пред-оценку канонического QG стадии; зелёный → продвижение строго через неизменный stage_engine.advance_stage(..., finished_agent=None); красный → тишина (спам нотификаций структурно невозможен — advance_stage на красном гейте не вызывается вовсе); analysis F-1 не трогает (человеческий гейт). F-2 plane-side (reconcile_plane_once) — опрос Plane API per-project (новый plane_sync.list_issues_by_state, курсорная пагинация, never-raise) и реплей In Progress / Approved / Rejected через существующие webhooks.plane.handle_status_start / handle_verdict (async-обработчики вызываются из sync-потока через asyncio.run). F-3 — усиление sha→branch в handle_ci_status: при неразрезолвленном sha — БД-fallback по единственной development-задаче repo (db.get_development_tasks_by_repo; неоднозначность → не резолвим, ложного матча нет), logger.debuglogger.info для видимости потерянного CI-события. Анти-дубль на создании задачи (db.create_task_atomic под process-wide threading.Lock: SELECT-exists→INSERT, проигравший в гонке reconcile↔webhook не плодит второй task/branch/worktree/стартовый analyst-job). Старт/стоп в main.lifespan (после worker.start() / перед worker.stop()), restart-safe, never-raise на единицу работы. Наблюдаемость (F-4): при разблокировке — лог-строка reconciler: <wi> <stage> разблокирована (потерян webhook) + Telegram (reconcile_notify_unblock) и блок reconcile в GET /queue. Kill-switches: ORCH_RECONCILE_ENABLED (глобально), ORCH_RECONCILE_PLANE_ENABLED (гасит только F-2), ORCH_RECONCILE_INTERVAL_S (120), ORCH_RECONCILE_GRACE_DEFAULT_S (600), ORCH_RECONCILE_GRACE_OVERRIDES_JSON (per-stage), ORCH_RECONCILE_NOTIFY_UNBLOCK (true). Схема БД и реестры (STAGE_TRANSITIONS/QG_CHECKS) НЕ менялись. ADR docs/work-items/ORCH-053/06-adr/ADR-001-stuck-task-reconciler.md, глобальный docs/architecture/adr/adr-0007-reconciler.md. Тесты: tests/test_reconciler.py, tests/test_reconciler_plane.py, tests/test_gitea_sha_resolve.py, tests/test_config.py.
  • Merge-gate: авто-rebase на текущий origin/main + повторный прогон тестов + сериализация мержей (ORCH-043): детерминированный (без LLM) суб-гейт на ребре deploy-staging → deploy, выполняемый ПЕРЕД мержем PR деплоером. Закрывает класс гонок «две зелёные ветки в одном репо ломают main»: пайплайн валидирует ветку против того main, от которого она ответвилась, а не против main в момент мержа — между «ветка зелёная» и «ветка смержена» параллельная задача может сдвинуть main (семантический конфликт: git мержит без текстового конфликта, но совмещённый main красный). Для self-hosting репозитория orchestrator это означало бы красный main инструмента, обслуживающего ВСЕ проекты. Новый модуль src/merge_gate.py (контракт «never raise», все git-операции — в per-branch worktree, ORCH-2/S-4): branch_is_behind_main (git merge-base --is-ancestor origin/main HEAD), auto_rebase_onto_main (rebase + git push --force-with-lease ТОЛЬКО ветки задачи — main НИКОГДА не пушится; текстовый конфликт → rebase --abort + чистый worktree), retest_branch (python -m pytest <target> в догнанном worktree, бюджет merge_retest_timeout_s), файловый merge-lease (acquire_merge_lease/release_merge_lease, атомарный O_CREAT|O_EXCL, holder-aware release, реклейм протухшего/битого лиза — без изменения схемы БД). Новый quality-gate check_branch_mergeable (src/qg/checks.py, зарегистрирован в QG_CHECKS) композирует примитивы под лизом: kill-switch/вне-области → no-op pass; lock занят → (False, "merge-lock busy") (сигнал DEFER, не код-фолт); ветка свежая → pass (лиз ДЕРЖИТСЯ до мержа); отстала → rebase → конфликт = fail+release, чисто → retest → зелёный = pass (лиз держится) / красный|timeout = fail+release. Интеграция в src/stage_engine.py (суб-гейт на deploy-staging, БЕЗ новой стадии в STAGE_TRANSITIONS): pass → advance на deploy; «merge-lock busy» → DEFER (повторная постановка деплоера на deploy-staging с задержкой available_at, анти-дедлок при max_concurrency=1, restart-safe счётчик по task_content, лимит merge_defer_max_attempts → block+Telegram); конфликт/красный retest → ROLLBACK на development + ретрай developer-а (кап MAX_DEVELOPER_RETRIES, без бесконечного баунса). Лиз освобождается на deploy→done, на rollback и по webhook смерженного PR (src/webhooks/gitea.py). Новый параметр enqueue_job(..., available_at_delay_s=...) (src/db.py) — отложенная постановка без изменения схемы. Условность раскатки (зеркало ORCH-35): merge_gate_repos (CSV) или по умолчанию только self-hosting orchestrator; глобальный kill-switch merge_gate_enabled. Новые настройки ORCH_MERGE_GATE_ENABLED (true), ORCH_MERGE_GATE_REPOS (""), ORCH_MERGE_RETEST_TIMEOUT_S (600), ORCH_MERGE_RETEST_TARGET (tests/), ORCH_MERGE_LOCK_TIMEOUT_S (300), ORCH_MERGE_DEFER_DELAY_S (60), ORCH_MERGE_DEFER_MAX_ATTEMPTS (5). ADR docs/work-items/ORCH-043/06-adr/ADR-001-merge-gate.md, глобальный docs/architecture/adr/adr-0006-merge-gate.md. Тесты: tests/test_merge_gate.py, tests/test_qg_merge_gate.py, tests/test_merge_gate_race.py, tests/test_stage_engine.py::TestMergeGate, tests/test_config.py.
  • Режим bump live-трекера Telegram (ORCH-042): новый ORCH_TRACKER_MODE (Settings.tracker_mode, дефолт edit) выбирает поведение карточки задачи. edit (как было) — карточка редактируется на месте (editMessageText). bump — на каждом обновлении старое сообщение удаляется и карточка отправляется заново вниз чата (best-effort delete_telegram(старый_id)send_telegram(text, disable_notification=True)set_tracker_message_id(new_id)), чтобы актуальный статус всегда был последним в чате при активной переписке. Инвариант «одна карточка на задачу» сохранён в обоих режимах: за один вызов update_task_tracker шлётся ≤1 нового сообщения; set_tracker_message_id вызывается ТОЛЬКО при успешном send (транзиентный None не затирает указатель); результат delete НЕ блокирует отправку новой карточки (delete-fail у сообщения >48ч → всё равно шлём новое). Резолюция режима в notifications (case-insensitive, trim): всё, что ≠ "bump" (включая пустое/мусор) → edit → нулевая регрессия и оркестратор не падает на любом значении флага. Новый low-level helper delete_telegram(message_id) -> bool (контракт «never raises», маркеры _DELETE_GONE_MARKERS): ok:true или «уже нет / нельзя удалить» → True; неизвестный ok:false/5xx/исключение → False; нет кредов → False без HTTP. Сигнатуры send_telegram/edit_telegram/update_task_tracker и схема БД (tasks.tracker_message_id) не менялись. ADR docs/work-items/ORCH-042/06-adr/ADR-001-tracker-bump-mode.md. Тесты: tests/test_tracker_bump.py, tests/test_config.py.
  • Дословный текст findings reviewer/tester встраивается в task_desc заворота (ORCH-046): при откате на development строка task_desc (попадает в .task-dev.md developer-агента) теперь несёт суть претензий, а не только ссылку на файл — устраняет «испорченный телефон», из-за которого агент шёл «читать файл», терял ключевые P0/P1 / причину FAIL и заворачивался снова, выжигая MAX_DEVELOPER_RETRIES и токены. Новый defensive-модуль src/review_parse.py (контракт «never raise», как src/frontmatter.py): extract_review_findings(path) — дословные пункты P0/P1 из секции ## Findings файла 12-review.md; extract_test_failures(path) — релевантный фрагмент тела 13-test-report.md (приоритет ## Вывод pytest → FAIL-строки ## Результаты## Итог). Обе функции усекают результат до MAX_FINDINGS_CHARS/MAX_FAILURES_CHARS (≈2000) с маркером …(truncated). Две rollback-ветки src/stage_engine.py (reviewer REQUEST_CHANGES, tester check_tests_passed FAIL) встраивают извлечённый текст и сохраняют ссылку на полный файл («Полный контекст»); при пустом/битом артефакте — graceful-фоллбэк на прежнюю ссылку-строку (никаких исключений в advance_stage). Tester-ветка дополнительно всегда включает reason гейта. Последовательность отката, _developer_retry_count, поля AdvanceResult и реестр QG_CHECKS не менялись. ADR docs/work-items/ORCH-046/06-adr/ADR-001-embed-findings-in-task-desc.md. Тесты: tests/test_review_parse.py, tests/test_stage_engine.py::TestRollbackTaskDescEmbedding.
  • Поллинг с ретраем в quality-gate check_ci_green (ORCH-045): гейт CI превращён из single-shot в polling, чтобы устранить race condition — раньше один опрос combined commit-status сразу после пуша developer-а ловил транзиентный pending (типично 1-3с, реальный кейс ORCH-017: опрос 17:58:54 → pending, CI дозеленел 17:58:55) и задача застревала насмерть без повторного опроса. Теперь: success → пропуск сразу; failure/error → провал сразу (терминально, ретрай бессмыслен); pending/unknown → time.sleep и повторный опрос до ci_poll_max_attempts раз; истечение попыток → явный (False, "CI still pending after <T>s") (тупик больше не молчаливый); 404 → как раньше; транзиентная httpx.HTTPError на попытке логируется и ретраится в рамках бюджета. Параметры — новые настройки ORCH_CI_POLL_MAX_ATTEMPTS (12) и ORCH_CI_POLL_INTERVAL_S (10) в src/config.py (~2 мин ожидания pending). Сигнатура check_ci_green(repo, branch) и реестр QG_CHECKS не менялись; check_tests_passed не затронут. ADR docs/architecture/adr/adr-0004-ci-poll-retry.md. Тесты: tests/test_qg.py::TestCheckCIGreen.
  • Прямые ссылки на BRD и Plane-таску в Telegram-уведомлении об апруве (ORCH-017): пингующее сообщение notify_approve_requested теперь встраивает две HTML-<a>-ссылки — на docs/work-items/<WI>/01-brd.md (Gitea branch-view: gitea_public_urlgitea_url) и на issue в Plane ({web_base}/{workspace}/projects/{project_id}/issues/{plane_issue_id}/). Новая настройка ORCH_PLANE_WEB_URL (внешний браузерный web-URL Plane; фолбэк на plane_api_url). Loopback-guard: если итоговый Plane web-base указывает на localhost/127.0.0.1/0.0.0.0/::1 или пуст — Plane-ссылка опускается (не выпускаем битый localhost-URL). Graceful degradation: каждая ссылка строится независимо и опускается при нехватке данных, сообщение и призыв «Переведите задачу в статус Approved …» сохраняются всегда; ровно одно пингующее сообщение, разделяемая send_telegram не тронута. Динамические подписи экранируются html.escape, parse_mode=HTML сохранён. ADR docs/work-items/ORCH-017/06-adr/ADR-001-telegram-approve-links.md. Тесты: test_notify_approve_links.py, test_analysis_approve_flow_links.py.
  • Конфигурируемые модель LLM и режим работы (--effort) агентов (ORCH-41): модель/effort каждого агента вынесены из хардкода launcher.py в конфиг — глобально per-agent (ORCH_AGENT_MODEL_<AGENT> / ORCH_AGENT_EFFORT_<AGENT>, дефолты ORCH_AGENT_MODEL_DEFAULT=claude-opus-4-8, ORCH_AGENT_EFFORT_DEFAULT=high) и per-project (agent_models / agent_efforts в ORCH_PROJECTS_JSON). Резолверы resolve_agent_model / resolve_agent_effort (приоритет project > per-agent env > default > пусто), валидация effort {low,medium,high,xhigh,max}, опц. ORCH_AGENT_FALLBACK_MODEL (--fallback-model). Хардкод "model":"opus" (architect/reviewer) удалён. Тесты: test_resolve_agent_model.py, test_resolve_agent_effort.py.
  • Единый status-коммент агентов в Plane (ORCH-016): usage.build_status_comment(...) — один хелпер для ВСЕХ ролей (analyst..deployer). HTML-формат: header {icon} {Role} — {описание}, опциональная строка Verdict/Status: … из YAML-frontmatter артефакта, строка Длительность: 4m 12s (явный duration_s от launcher, fallback из agent_runs для аналитика), <b>Документы:</b><ul><li><a>…</a></li></ul>, тех-хвост <sub>tokens · cost</sub>. Утилитки: usage.fmt_duration, usage.get_agent_duration, новый модуль src/frontmatter.py (defensive YAML reader). ADR docs/work-items/ORCH-016/06-adr/ADR-001-unified-status-comment.md.
  • Документация по канону (ORCH-9): CLAUDE.md (паспорт проекта), структура docs/ (architecture/ + adr/, operations/, work-items/, history/), docs/operations/INFRA.md (RUNBOOK с инфра-изоляцией и self-hosting рисками).
  • ADR: adr-0001 (multi-repo registry), adr-0002 (job queue), adr-0003 (условный staging-гейт).
  • Стадия deploy-staging (ORCH-35): промежуточный гейт между testing и deploy. QG check_staging_status (условный, только для self-hosting repo). PR #31.
  • Деплой-хук (ORCH-34): scripts/orchestrator-deploy-hook.sh с health-check и авто-rollback. PR #30.
  • Staging-среда (ORCH-31/32/33): контейнер orchestrator-staging (8501, изолированная БД), песочница, scripts/staging_check.py. PR #28/#29.
  • Очередь задач (ORCH-1): таблица jobs, queue_worker.py, atomic claim, max_concurrency, ретраи, restart-safe, эндпоинт /queue.
  • Реестр проектов (ORCH-6): src/projects.py, фильтрация вебхуков по проекту.

Changed

  • Русификация и косметика карточки live-трекера Telegram (ORCH-042, оба режима): метка Подтверждение BRD вместо «Ревью БРД» (_BRD_LABEL); после прохождения approve-gate строка подтверждения BRD начинается с вместо ⏸️ (ветка ожидания человека сохраняет ⏸️/); русские display-labels стадий в _TRACKER_STAGES (Анализ / Архитектура / Разработка / Код ревью / Тестирование / Внедрение) — применяются и в « …», и в «🔄 … идёт»; финальная строка готовой задачи 📦 Внедрено вместо deployed (_done_link). Меняются только отображаемые строки — ключи стадий и имена агентов не трогаются. Существующие ассерты tests/test_telegram_tracker.py обновлены под русские метки.
  • Status-коммент агентов теперь HTML и единообразен (ORCH-016): src/usage.usage_comment(...) помечен deprecated и стал тонкой обёрткой над build_status_comment; src/usage.artifact_links(...) теперь возвращает <li><a>…</a></li> HTML-фрагменты (раньше — markdown [label](url)); stage_engine._build_analyst_ready_comment(...) — тонкая обёртка, аналитик идёт через ту же ветку build_status_comment(agent="analyst", ...). Реестр QG_CHECKS и STAGE_TRANSITIONS НЕ изменялись.
  • Цепочка стадий: ... testing → deploy-staging → deploy → done (была без deploy-staging).

Fixed

  • Контейнер и агенты бегут под uid хоста (1000:1000), не root (ORCH-040): оба сервиса в docker-compose.yml (orchestrator, orchestrator-staging) получили user: "1000:1000" (slin) — устраняет корень проблемы, при которой Claude-CLI агенты, запускаемые через subprocess.Popen внутри root-контейнера, создавали все артефакты конвейера (git worktree /repos/_wt/..., коммиты в docs/work-items/...) с владельцем root:root на хосте, из-за чего git pull/git reset под slin падали с insufficient permission for adding an object и каждый деплой требовал ручного chown. Теперь файлы сразу slin:slin. Доступ к docker.sock сохранён через group_add: ["999"] (МИНА 1 — НЕ удалена). SSH-маунт приведён к единому HOME агента: target /root/.ssh/home/slin/.ssh (/home/slin/.orchestrator-ssh:/home/slin/.ssh:ro), синхронно с HOME=/home/slin, который launcher форсит в env Popen и git_env — устранён скрытый рассинхрон SSH-маунта с форсимым HOME. src/agents/launcher.py и Dockerfile НЕ менялись (numeric uid работает без записи в /etc/passwd; safe.directory '*' уже покрывает git над bind-mount). Требует host-prerequisites Owner (P-1…P-4, вне кода): блокер P-1 — chown -R 1000:1000 /home/slin/.claude для доступа uid 1000 к claude creds (иначе preflight заворачивает конвейер); прод-рестарт self — только в окно тишины (общий инстанс с enduro-trails), страховка — staging-гейт (adr-0003). ADR docs/work-items/ORCH-040/06-adr/ADR-001-run-agents-as-host-uid.md, глобальный docs/architecture/adr/adr-0005-container-runs-as-host-uid.md; INFRA.md обновлён (рантайм-uid, volumes/SSH target, host-prerequisites). Тесты: tests/test_orch040_compose.py.
  • Staging-чек B6 читает реестр из окружения работающего staging-инстанса (ORCH-048): блок B6 «Registry: sandbox present, prod ET/ORCH absent» в scripts/staging_check.py давал ложный FAIL (prod-ET=YES(BAD!), prod-ORCH=YES(BAD!)) при фактически исправной изоляции — единственный чек suite, который не ходил к инстансу по HTTP, а импортировал src.projects локально через host-path хак sys.path.insert(0, "/repos/orchestrator") + importlib.reload, строя реестр из ORCH_PROJECTS_JSON process-env запускающего процесса. При фактическом запуске деплоером с хоста переменная не задана → дефолт _DEFAULT_PROJECTS (ET+ORCH) → ложный FAIL → лишний откат deploy-staging → development. Решение (вариант «в», ADR-001): host-path хак удалён; suite канонически запускается ВНУТРИ контейнера orchestrator-staging через docker exec … python3 /repos/orchestrator/scripts/staging_check.py (scripts/ доступен только через bind-mount, import src.projects резолвится через PYTHONPATH=/app из кода контейнера, env — .env.staging) → B6 читает реестр именно работающего инстанса, без HTTP-bootstrap и «курицы-яйца». Логика вердикта вынесена в чистую _evaluate_b6(known) -> (passed, detail) (инвариант passed ⟺ SANDBOX ∈ known ∧ PROD_ET ∉ known ∧ PROD_ORCH ∉ known, формат detail сохранён) + _known_project_ids_from_registry() / _run_b6() с детерминированным FAIL при недоступности источника (не ложный PASS, не необработанное исключение). Синхронно обновлены .openclaw/agents/deployer.md (команда стадии через docker exec) и docs/operations/STAGING_CHECK.md. src/projects.py, .env* и прочие чеки A/B4/B5/C не тронуты; реестр QG_CHECKS и check_staging_status (ADR-0003) не менялись. ADR docs/work-items/ORCH-048/06-adr/ADR-001-b6-registry-via-in-container-run.md. Тесты: tests/test_staging_check_b6.py.
  • Testing-гейт check_tests_passed читает result: наравне с verdict:/status: (ORCH-047): парсер _parse_tests_verdict (src/qg/checks.py) теперь принимает три равноправных машиночитаемых поля frontmatter 13-test-report.mdresult: (канон промпта тестера .openclaw/agents/tester.md, result: PASS|FAIL), плюс легаси verdict: и status: (enduro-trails ET-001..ET-014); достаточно любого одного непустого. Устраняет рассинхрон контракта: тестер честно эмитил result: PASS без verdict:/status:, парсер попадал в ветку «нет машинного вердикта» → откат testing → development в петлю до исчерпания MAX_DEVELOPER_RETRIES (наблюдалось на ORCH-17; ORCH-016 прошёл лишь из-за избыточного дублирования полей). Семантика приоритетов сохранена и распространена на все три поля через объединённую строку: negative-токен в любом поле авторитетен (перебивает positive), наборы токенов заморожены (обратная совместимость). Сигнатура гейта, имя и реестр QG_CHECKS не менялись. ADR docs/work-items/ORCH-047/06-adr/ADR-001-result-field-in-tests-gate.md. Тесты: tests/test_qg.py::TestCheckTestsPassed.
  • БАГ-8: провал deploy/deploy-staging → корректный откат на development.
  • Изоляция тестов от живого Plane API (PR #27): autouse-фикстура сброса settings.

Историю до введения канона см. в docs/history/ (BUGFIXES_, LESSONS_, INCIDENT_).*