Files
orchestrator/CHANGELOG.md
claude-bot 98b47fe021
Some checks failed
CI / test (push) Failing after 14s
CI / test (pull_request) Failing after 13s
feat(preflight): catch logged-out auth and treat empty result as failure
ORCH-044 closes two blind spots that let a single de-authenticated agent
stall the shared queue for all projects:

P1 — preflight auth gate. `claude --version` answers even when logged out,
so version-only preflight was blind to auth. Adds a token-free, network-free
check of <AGENT_HOME>/.claude/.credentials.json: missing/unreadable/no-oauth
or an expired `claudeAiOauth.expiresAt` (epoch ms, vs now + skew) => preflight
FAIL; absent expiry => OK (no false positives). Result is cached on the same
preflight_cache_ttl. Post-factum safety net: launcher detects auth markers
("not logged in" / "/login" / "unauthorized" / 401) in the run log and resets
the preflight cache so the next tick re-evaluates auth. Auth failure is a gate,
not a transient — it does not spin the circuit breaker. Emergency toggle
ORCH_PREFLIGHT_CHECK_AUTH=false restores version-only behaviour.

P3 — empty log / no result-JSON => job failed. exit_code==0 with an empty or
JSON-less run log no longer counts as success: a separate result_ok flag gates
stage advance + usage comments, fires a Telegram alert, and routes the job
through the normal transient/permanent failure path (exit_code integrity in
agent_runs preserved).

Scope: P2 (--effort) is intentionally excluded and tracked in ORCH-50.

New settings: ORCH_PREFLIGHT_CHECK_AUTH, ORCH_CLAUDE_CREDENTIALS_PATH,
ORCH_AUTH_EXPIRY_SKEW_SECONDS. Docs updated (INFRA.md, internals.md, CHANGELOG).

Refs: ORCH-044

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 08:11:27 +00:00

17 KiB
Raw Blame History

Changelog

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

[Unreleased]

Added

  • Надёжность запуска агента: preflight ловит авторизацию + пустой результат = провал (ORCH-044): закрыты две системные дыры, из-за которых разлогиненный/«быстро умерший» агент тихо вешал общую очередь всех проектов (инцидент ORCH-17). P1 — preflight ловит auth (token-free, без сети/prompt-ping, BR-1): после успешного claude --version (который отвечает даже когда claude разлогинен — версия локальна) src/preflight.py читает <AGENT_HOME>/.claude/.credentials.json и валидирует OAuth-токен — нет файла / битый JSON / нет claudeAiOauth.accessToken ⇒ FAIL; claudeAiOauth.expiresAt (epoch ms) <= now + ORCH_AUTH_EXPIRY_SKEW_SECONDS ⇒ протух ⇒ FAIL; нет expiresAt ⇒ OK (не плодим ложных срабатываний). Путь к credentials резолвится от AgentLauncher.AGENT_HOME (/home/slin, HOME под которым launcher реально спавнит claude), а не от HOME процесса орка (новый _agent_home(), зеркально _claude_bin()). Результат кешируется тем же ORCH_PREFLIGHT_CACHE_TTL. При auth=fail job не клеймится (_drain_once уже корректен при ok=False), reason виден в /queue. Защитная сетка постфактум: _handle_auth_marker детектит маркер разлогина в run-логе (is_auth_failure_text) и сбрасывает preflight-кеш, чтобы следующий тик переоценил auth (auth-провал НЕ transient, breaker не крутится). Новые настройки: ORCH_PREFLIGHT_CHECK_AUTH (тумблер, default true), ORCH_CLAUDE_CREDENTIALS_PATH (явный путь), ORCH_AUTH_EXPIRY_SKEW_SECONDS. P3 — пустой лог / нет result-JSON ⇒ провал: exit_code==0 больше не считается успехом сам по себе_monitor_agent валидирует результат (_validate_result: лог непустой + есть trailing result-JSON по контракту usage._extract_last_json_object); success = exit 0 AND result_ok. Только при success постится «успешный» status-коммент и вызывается _try_advance_stage; при exit 0 & not result_ok — Telegram-алерт, стадия НЕ двигается, _finalize_job(result_ok=False) маршрутизирует job в провал (empty run log / no result JSON: по умолчанию permanent → requeue/failed+алерт; transient-маркер в логе → transient-путь). Реальный exit_code пишется в agent_runs без искажения — решение done/fail несёт отдельный флаг result_ok (не подменённый код выхода). Итог: exit 0 всегда завершается терминально/ретраябельно (done|failed|queued) — путь «быстрая смерть с exit 0 → вечный running» закрыт. Scope: --effort (P2) исключён владельцем и вынесен в ORCH-50 — не трогался. ADR docs/work-items/ORCH-044/06-adr/ADR-001-preflight-auth-and-empty-result-failure.md. Тесты: tests/test_preflight_auth.py, tests/test_empty_log_failure.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

  • 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

  • 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_).*