diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 9fdfe85..2b15ea2 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -9,8 +9,9 @@ - **Stage Engine** (`src/stage_engine.py`) — исполнение переходов, диспетчеризация QG (`_run_qg`), откаты, синхронизация с Plane. - **Review/Test Parsers** (`src/review_parse.py`, ORCH-046) — defensive-извлечение дословного must-fix текста из артефактов для встраивания в `task_desc` заворота: `extract_review_findings` (P0/P1 из `12-review.md`), `extract_test_failures` (фрагмент тела `13-test-report.md`). Контракт «never raise»: любая ошибка → `""`. - **Quality Gates** (`src/qg/checks.py`) — проверки выхода со стадии, реестр `QG_CHECKS`. -- **Agent Launcher** (`src/agents/launcher.py`) — запуск Claude CLI агентов в изолированном git worktree, мониторинг, auto-advance. -- **Queue** (`src/queue_worker.py`, ORCH-1) — персистентная очередь задач (SQLite `jobs`), atomic claim, max_concurrency, ретраи, restart-safe. +- **Agent Launcher** (`src/agents/launcher.py`) — запуск Claude CLI агентов в изолированном git worktree, мониторинг, auto-advance. **Валидация результата (ORCH-044):** `exit_code==0` считается успехом только если run-лог непустой и содержит валидный result-JSON; пустой/невалидный результат ⇒ job `failed`/retry + алерт, без авто-advance и «успешного» коммента. +- **Preflight** (`src/preflight.py`, ORCH-1/ORCH-044) — дешёвый token-free гейт клейма: `os.path.exists(bin)` + `claude --version` + **проверка авторизации** (чтение `/.claude/.credentials.json` и валидности `claudeAiOauth.expiresAt`; постфактум-маркер `Not logged in`). Кешируется на `preflight_cache_ttl`. Подробнее: [ADR work-item ORCH-044](../work-items/ORCH-044/06-adr/ADR-001-preflight-auth-and-empty-result-failure.md). +- **Queue** (`src/queue_worker.py`, ORCH-1) — персистентная очередь задач (SQLite `jobs`), atomic claim, max_concurrency, ретраи, restart-safe. Не клеймит job при `preflight=fail` (в т.ч. auth-fail). - **Project Registry** (`src/projects.py`, ORCH-6) — Plane project id → repo + prefix; фильтрация вебхуков по проекту. - **Plane Sync** (`src/plane_sync.py`) — синхронизация статусов/комментариев в Plane. diff --git a/docs/work-items/ORCH-044/06-adr/ADR-001-preflight-auth-and-empty-result-failure.md b/docs/work-items/ORCH-044/06-adr/ADR-001-preflight-auth-and-empty-result-failure.md new file mode 100644 index 0000000..24e133b --- /dev/null +++ b/docs/work-items/ORCH-044/06-adr/ADR-001-preflight-auth-and-empty-result-failure.md @@ -0,0 +1,168 @@ +# ADR-001: Token-free auth-preflight + «пустой результат = провал» в запуске агента + +**Work Item:** ORCH-044 +**Статус:** Accepted +**Дата:** 2026-06-06 +**Автор:** Architect + +> ⛔ **Scope (коррекция владельца, 06.06):** `--effort` (P2) **исключён** из ORCH-044 и +> вынесен в **ORCH-50**. Этот ADR покрывает только **P1** (preflight ловит авторизацию) +> и **P3** (пустой лог / нет result-JSON ⇒ job `failed`). Любые решения по effort, +> дефолтам `agent_effort_*` и ORCH-41 effort-докам — **вне этого ADR**. + +--- + +## Контекст + +Инцидент 05.06 (ORCH-17): аналитик-агент стартовал и мгновенно «умирал» — run-лог пустой +(0 байт), job в очереди завис в `running`. Две наложившиеся причины: (1) `claude Not logged +in` после ребилда контейнера; (2) `--effort` гасил stdout. **Системная проблема:** +preflight пропустил заведомо нерабочую задачу в работу, а пустой результат был неотличим +от успеха. Поскольку инстанс общий для всех проектов (self-hosting, общая очередь/БД), +тихое зависание блокирует конвейер **всех** проектов. + +Текущее состояние слоя запуска: +- `src/preflight.py` проверяет только `os.path.exists(bin)` и `claude --version`. `--version` + отвечает успешно **даже когда claude не залогинен** (версия — локальная информация) ⇒ + preflight слеп к авторизации. +- `src/agents/launcher.py::_monitor_agent` трактует `exit_code == 0` как успех **независимо + от формы stdout** (комментарий в `_spawn`, стр. 302) ⇒ пустой лог + exit 0 → `done` + + авто-advance стадии. + +Ограничения (BR-1): preflight обязан быть **локальным и token-free** — никакого prompt-ping +и сетевых вызовов к API модели. + +## Решение + +### P1 — Preflight ловит авторизацию (комбинация проактивной и постфактум-проверок) + +Реализуем **оба** подхода из TR-1.2 (a + b), проактивный — основной гейт, постфактум — +защитная сетка. + +**(a) Проактивно — чтение файла учётных данных (основной гейт).** +`preflight._compute()` после успешного `--version` выполняет `_check_auth()`: +1. Резолвит путь к credentials **согласованно с HOME, под которым launcher реально спавнит + claude** (`/home/slin`), а НЕ из окружения процесса оркестратора. Реализуется зеркально + `_claude_bin()`: новый `_agent_home()` читает `AgentLauncher.AGENT_HOME` (новая константа, + значение `/home/slin`), путь = `settings.claude_credentials_path` если задан, иначе + `/.claude/.credentials.json`. +2. Файла нет / нечитаем / невалидный JSON ⇒ `(False, "claude not logged in: credentials …")`. +3. Нет блока `claudeAiOauth` / accessToken ⇒ `(False, "not logged in: no oauth token")`. +4. `claudeAiOauth.expiresAt` (epoch **ms**) `<= now_ms (+ skew)` ⇒ + `(False, "OAuth token expired at ")`. +5. accessToken есть, но `expiresAt` отсутствует/не число ⇒ **OK** (нельзя доказать истечение; + не плодим ложные срабатывания — см. Риски). +6. Иначе ⇒ `(True, "auth ok")`. + +`_check_auth()` **никогда не бросает**: любое исключение → `(False, "auth check error: …")` +(fail-safe в сторону «не клеймить», BR-2 / TR-3.5). + +Кеширование (TR-1.4 / AC-6): чтение файла встроено в `_compute()`, который уже кешируется +`check()` на `preflight_cache_ttl`. **Отдельный кеш не вводится** — auth-чтение происходит +только на cache-miss, как и `--version`. + +Гейтинг клейма (TR-1.5 / AC-4 / BR-2): **изменений в `queue_worker._drain_once` не требуется** +— он уже не клеймит job при `ok=False`. Информативный auth-reason автоматически попадает в +`worker.last_preflight_reason` и `/queue` (без изменения схемы ответа). + +**(b) Постфактум — маркер `Not logged in` в run-логе (защитная сетка).** +Если агент всё-таки стартовал при протухшей сессии (гонка: токен истёк между preflight и +спавном), `launcher` при финализации детектит auth-маркер в логе +(`preflight.is_auth_failure_text(text)`: «not logged in», «please run /login», +«unauthorized», «401») и: +- включает маркер в `error` job; +- вызывает `preflight.reset_cache()`, чтобы **следующий тик воркера переоценил auth + проактивно** (быстрый подхват re-login ИЛИ дальнейшее гейтирование, если всё ещё битый). + +Auth-провал **не** маршрутизируется как transient (это не 429) и **не** крутит брейкер — +правильный механизм гейтирования здесь preflight, а не circuit breaker. + +### P3 — Пустой лог / нет result-JSON ⇒ провал job + +В `_monitor_agent` для ветки `exit_code == 0` вводим **валидацию результата** перед тем как +считать job успешным. Новый защитный хелпер `_validate_result(output_path) -> (ok, reason)`: +- лог отсутствует / пустой (size 0 или только whitespace) ⇒ невалиден; +- иначе извлекаем result-JSON **тем же контрактом**, что usage-учёт + (`usage._extract_last_json_object` / `parse_usage_from_text`); нет валидного объекта ⇒ + невалиден; +- хелпер обёрнут try/except и **не роняет монитор**; при собственной ошибке — + fail-safe в сторону провала (TR-3.5). + +`success = (exit_code == 0 and result_ok)`. Побочные эффекты успеха выполняются **только при +`success`**: +- `_post_usage_comments(...)` (успешный status-коммент) — **не** постится при невалидном + результате (AC-12); +- `_try_advance_stage(...)` — **не** вызывается при невалидном результате (AC-12); +- при `exit_code == 0 and not result_ok` шлётся Telegram-алерт о «пустом/невалидном + результате». + +Финализация job (`_finalize_job` получает новый флаг `result_ok`): +- `exit_code == 0 and result_ok` ⇒ `done` (как раньше, AC-13 — без регрессии); +- `exit_code != 0` **ИЛИ** `result_ok == False` ⇒ путь провала: + - классификация лога `error_classifier.classify_log_file` (по умолчанию **permanent**; + transient-маркер уводит в transient-путь — TR-3.3); + - permanent: `attempts < max_attempts` ⇒ requeue (`queued`), иначе `failed` + алерт; + - `error` информативен: `empty run log / no result JSON (run_id=…)` для случая пустого + результата. + +Реальный `exit_code` по-прежнему пишется в `agent_runs` без искажения; на решение +done/fail влияет отдельный флаг `result_ok`, а не подменённый код выхода. + +`exit_code == 0` теперь **всегда** завершается терминально/ретраябельно (`done` | +`failed` | `queued`) — путь «быстрая смерть с exit 0 → вечный running» закрыт (AC-14, BR-3). +Watchdog ORCH-7 продолжает закрывать таймауты. + +### Конфигурация (config.py) + +| Настройка | Env | Default | Назначение | +|-----------|-----|---------|------------| +| `preflight_check_auth` | `ORCH_PREFLIGHT_CHECK_AUTH` | `True` | Вкл/выкл auth-проверку (аварийный тумблер) | +| `claude_credentials_path` | `ORCH_CLAUDE_CREDENTIALS_PATH` | `""` | Явный путь; пусто ⇒ `/.claude/.credentials.json` | +| `auth_expiry_skew_seconds` | `ORCH_AUTH_EXPIRY_SKEW_SECONDS` | `0` | Запас на рассинхрон часов при сравнении `expiresAt` | + +`agent_effort_*` дефолты и `--effort` в `_spawn` — **не трогаем** (scope, ORCH-50). + +## Альтернативы + +- **A1. Prompt-ping (ping→pong) для проверки auth.** ❌ Запрещено BR-1 (жжёт rate limit, + латентность). Отвергнуто. +- **A2. Только постфактум-маркер (чистый вариант b).** Ловит auth лишь ПОСЛЕ спавна и траты + цикла; не гейтирует клейм. Оставлен как защитная сетка, но не как основной механизм. +- **A3. Сетевая валидация токена у провайдера.** Нарушает «preflight локальный» (TR-1.6), + добавляет сетевую зависимость в горячий путь воркера. Отвергнуто. +- **A4. Подменять exit_code на ненулевой при пустом результате.** Исказило бы + `agent_runs.exit_code` и классификацию. Выбрали отдельный флаг `result_ok`. +- **A5. Отдельный кеш для auth.** Избыточно — `_compute()` уже под общим TTL. + +## Последствия + +**Плюсы.** +- Заведомо нерабочая (не залогинен / протухший токен) задача **не клеймится** — экономия + цикла и отсутствие тихого зависания. +- Пустая «быстрая смерть» агента теперь видима: `failed`/retry + алерт вместо ложного `done` + и движения стадии вперёд по пустому результату. +- Без изменения схемы БД, без новых QG/стадий, без новых HTTP-endpoint'ов. +- Auth-reason виден в `/queue` для диагностики. + +**Минусы / ограничения.** +- **Риск ложноположительного auth-fail** (см. `10-tech-risks.md` R-1): неверно + резолвленный путь к credentials заблокирует клейм **всех** проектов (общая очередь). + Митигируется: единый источник HOME (`AGENT_HOME`), тумблер `ORCH_PREFLIGHT_CHECK_AUTH`, + обязательная проверка на staging (8501) перед прод-деплоем. +- Проверка `expiresAt` — локальная; реально отозванный, но ещё не истёкший токен ловится + только постфактум-маркером (b). +- `expiresAt`-отсутствие трактуется как OK (компромисс против ложных срабатываний). + +**Self-hosting.** Изменения только в слое preflight/launch; **не** требуют рестарта/падения +прод-контейнера `orchestrator` в рамках задачи. Выкатка — через staging-гейт (AC-17). + +## Связи + +- BRD `01-brd.md` (P1, P3), ТЗ `02-trz.md` (TR-1.x, TR-3.x; scope-коррекция), + Acceptance `03-acceptance-criteria.md` (AC-1…AC-6, AC-10…AC-17). +- Риски: `10-tech-risks.md`. Инфра: `07-infra-requirements.md`. БД: `08-data-requirements.md`. +- Код: `src/preflight.py`, `src/agents/launcher.py` (`_monitor_agent`, `_finalize_job`), + `src/config.py`, `src/usage.py` (`_extract_last_json_object`), + `src/error_classifier.py` (`classify_log_file`), `src/queue_worker.py` (без изменений). +- ORCH-1 (очередь/resilience), ORCH-7 (watchdog), ORCH-41 (resolver — **не трогаем effort**). +- **ORCH-50** — полноценный возврат `--effort` (вынесен из этой задачи). diff --git a/docs/work-items/ORCH-044/07-infra-requirements.md b/docs/work-items/ORCH-044/07-infra-requirements.md new file mode 100644 index 0000000..fe00da6 --- /dev/null +++ b/docs/work-items/ORCH-044/07-infra-requirements.md @@ -0,0 +1,46 @@ +# 07 — Требования к инфраструктуре + +**Work Item:** ORCH-044 +**Основано на:** ADR-001, ТЗ `02-trz.md` + +## Топология +**Без изменений.** Новых контейнеров, портов, сервисов, очередей не вводится. Прод +`orchestrator` (8500) и staging `orchestrator-staging` (8501) остаются как есть +(`docs/operations/INFRA.md`). + +## Учётные данные claude (P1) +- Launcher спавнит claude с `HOME=/home/slin` (`src/agents/launcher.py`). Preflight ДОЛЖЕН + резолвить путь к credentials от **этого же** HOME, а не от окружения процесса оркестратора. +- Ожидаемое расположение файла OAuth-токена: **`/home/slin/.claude/.credentials.json`** + (структура: `claudeAiOauth.expiresAt` — epoch **ms**). +- Файл — секрет; в гит НЕ коммитится (правило агентов №8). На хосте монтируется в контейнер + как раньше; задача его расположение **не меняет**, только начинает читать. +- ⚠️ **Проверить на staging:** реальный путь файла внутри контейнера совпадает с + резолвленным preflight. Несовпадение ⇒ ложный auth-fail и блок очереди (R-1). + +## Новые переменные окружения (env-карта) +Документировать в `docs/operations/INFRA.md` и docstring `src/config.py`: + +| Env | Default | Назначение | +|-----|---------|------------| +| `ORCH_PREFLIGHT_CHECK_AUTH` | `true` | Включение token-free auth-проверки в preflight. Аварийный тумблер: `false` возвращает старое поведение (только bin + `--version`). | +| `ORCH_CLAUDE_CREDENTIALS_PATH` | `""` | Явный путь к `.credentials.json`. Пусто ⇒ `/.claude/.credentials.json`. | +| `ORCH_AUTH_EXPIRY_SKEW_SECONDS` | `0` | Запас на рассинхрон часов при сравнении `expiresAt`. | + +`--effort` env (`ORCH_AGENT_EFFORT_*`) — **вне scope**; прод-хотфикс `ORCH_AGENT_EFFORT_*=""` +в `.env` **оставить как есть** (ORCH-50). + +## Эксплуатационные процедуры +- **Аварийный откат auth-гейта без редеплоя кода:** выставить `ORCH_PREFLIGHT_CHECK_AUTH=false` + в `.env` и перезапустить воркер обычной процедурой выката (НЕ в рамках этой задачи). +- **Диагностика:** auth-причина видна в `GET /queue` (`preflight_reason`) и в warning-логе + `orchestrator.preflight`. +- **Re-login:** при детекте auth-маркера в логе launcher сбрасывает preflight-кеш, поэтому + после ручного `claude /login` следующий тик воркера (≤ `preflight_cache_ttl`) подхватит + валидную сессию автоматически. + +## Self-hosting / деплой (AC-17) +- Изменения только в слое preflight/launch — **не** требуют рестарта/падения прод-контейнера + в рамках задачи. +- Выкатка self-доработки ORCH — **через staging-гейт (8501)** перед прод-деплоем + (CLAUDE.md, `docs/operations/INFRA.md`, ADR-0003). diff --git a/docs/work-items/ORCH-044/08-data-requirements.md b/docs/work-items/ORCH-044/08-data-requirements.md new file mode 100644 index 0000000..2e557e6 --- /dev/null +++ b/docs/work-items/ORCH-044/08-data-requirements.md @@ -0,0 +1,23 @@ +# 08 — Требования к схеме БД + +**Work Item:** ORCH-044 +**Основано на:** ADR-001, ТЗ `02-trz.md` §4 + +## Вердикт: изменений схемы НЕ требуется + +Новых таблиц, колонок, индексов, миграций — **нет**. + +P1 (auth-preflight) и P3 (пустой результат ⇒ провал) работают на **существующих** структурах: + +- **`jobs`** — повторно используются существующие колонки для пути провала: + `status` (`queued`/`running`/`done`/`failed`), `error`, `attempts`, `max_attempts`, + `transient_attempts`, `available_at`, `run_id`. Пустой/невалидный результат идёт тем же + путём, что и обычный permanent/transient провал (`mark_job` / `mark_job_transient`). +- **`agent_runs`** — `exit_code` пишется без искажения (реальный код выхода процесса). + Решение done/fail принимается по отдельному in-memory флагу `result_ok` в `_monitor_agent`, + а не по колонке. + +## Состояние данных +- Никаких бэкофиллов / data-migration. +- Auth-проверка читает **файл** `.credentials.json` (вне БД), результат кешируется in-memory + (`preflight._cache`), не персистится. diff --git a/docs/work-items/ORCH-044/10-tech-risks.md b/docs/work-items/ORCH-044/10-tech-risks.md new file mode 100644 index 0000000..8e1521a --- /dev/null +++ b/docs/work-items/ORCH-044/10-tech-risks.md @@ -0,0 +1,20 @@ +# 10 — Технические риски + +**Work Item:** ORCH-044 +**Основано на:** ADR-001 + +| ID | Риск | Вероятн. | Влияние | Митигация | +|----|------|----------|---------|-----------| +| R-1 | **Ложноположительный auth-fail.** Неверно резолвленный путь к `.credentials.json` (иной HOME/маунт) ⇒ preflight всегда FAIL ⇒ **не клеймится ни одна job всех проектов** (общая очередь, self-hosting). | Средняя | **Высокое** | Единый источник HOME (`AgentLauncher.AGENT_HOME`, зеркально `_claude_bin()`); тумблер `ORCH_PREFLIGHT_CHECK_AUTH=false`; **обязательная проверка на staging** (реальный путь == резолвленный) перед прод-деплоем; информативный reason в `/queue` + warning-лог. | +| R-2 | **Fail-safe-провал на легитимном пустом выводе.** Агент легитимно завершился `exit 0` с непустым логом, но `_validate_result` ошибочно счёл результат невалидным ⇒ ложный `failed`/requeue (регрессия AC-13). | Низкая | Среднее | Контракт извлечения JSON — тот же, что у работающего usage-учёта (`_extract_last_json_object`); регресс-тест TC-15 (валидный лог ⇒ `done`); валидатор не трогает успешный путь, кроме булева флага. | +| R-3 | **`expiresAt` без сетевой валидации.** Реально отозванный, но ещё не истёкший по времени токен пройдёт проактивную проверку (a). | Средняя | Среднее | Защитная сетка постфактум (b): маркер `Not logged in` в логе ⇒ `error` + `preflight.reset_cache()` ⇒ следующий тик переоценивает auth; полная сетевая валидация — вне scope (BR-1). | +| R-4 | **`expiresAt` отсутствует/нечисловой** в файле (иная версия CLI / иной формат) ⇒ проверка трактует как OK и пропускает. | Низкая | Низкое | Осознанный компромисс против ложных срабатываний (см. ADR §P1.5); отсутствие токена/accessToken по-прежнему ⇒ FAIL; постфактум-маркер ловит реальный «не залогинен». | +| R-5 | **Часовой рассинхрон** контейнер vs токен ⇒ валидный токен сочтён истёкшим. | Низкая | Среднее | `ORCH_AUTH_EXPIRY_SKEW_SECONDS` (default 0) для запаса; контейнеры на одном хосте (mva154) — рассинхрон маловероятен. | +| R-6 | **Транзиентный auth (битый JSON в момент записи re-login).** Чтение файла во время атомарной перезаписи ⇒ временный FAIL. | Низкая | Низкое | Кеш TTL сглаживает; следующий тик перечитает; fail-safe в сторону «подождать» (job остаётся `queued`, не теряется). | +| R-7 | **Конфликт test-plan с коррекцией scope.** `04-test-plan.yaml` TC-09/TC-10/TC-11 проверяют `--effort` (variant B: «`--effort` не формируется»), но владелец **исключил** effort из ORCH-044 и оставил дефолты `agent_effort_*` = `high`. При дефолтной тест-конфигурации `_spawn` сформирует `--effort high` ⇒ TC-09 (ожидающий отсутствие флага) **упадёт**. | **Высокая** | Среднее | Developer/Tester: **адаптировать TC-09/10/11** под «effort не трогаем» (assert успешной сборки cmd без требования удаления флага, либо пометить как deferred→ORCH-50). Артефакт `04-test-plan.yaml` — чужой этап (правило №3), архитектор его НЕ редактирует, только фиксирует расхождение здесь. AC-7/AC-8/AC-9 не применяются (см. `03-acceptance-criteria.md` §P2). | +| R-8 | **Постфактум auth-сброс кеша зацикливает.** Повторные auth-провалы ⇒ повторные `reset_cache()`. | Низкая | Низкое | `reset_cache()` лишь форсирует один пересчёт; следующий `check()` снова закеширует на TTL; цикла «горячего» чтения нет; job не клеймится при FAIL. | + +## Сводно +Доминирующий риск — **R-1** (блок очереди ложным auth-fail при неверном пути) и +организационный **R-7** (test-plan vs scope). Оба закрываются: R-1 — staging-проверкой + +тумблером, R-7 — правкой effort-тестов разработчиком/тестером согласно коррекции владельца.