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>
17 KiB
17 KiB
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=failjob не клеймится (_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 — не трогался. ADRdocs/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.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
- 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_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_).*