# Задание A11 (Antigravity Pro, этот ПК): интеграция с Hermes и учётные данные

## Дата поступления
2026-08-23

## База
Проверочный HEAD на момент выдачи: **`cdfd9f1`**.

## Ветка
`antigravity/hermes-integration`

## Кому
Тяжёлая ветка работ: разбор чужого кода, обратная разработка эндпоинтов, проектные решения. Механических правок здесь нет.

---

## Порядок работы с git

**В начале — обновить локальную копию:**

```
cd <каталог репозитория>; git fetch origin --prune; git status
git checkout main; git pull --ff-only origin main
git checkout -b antigravity/hermes-integration
```

Зафиксировать фактический `BASE_SHA` через `git rev-parse --short HEAD`. Не считать `cdfd9f1` актуальным автоматически — параллельно идёт задание A12.

**Ветку отправить в `origin` сразу после первого коммита**, не дожидаясь готовности:

```
git push -u origin antigravity/hermes-integration
```

**В конце — обязательный push:**

```
git push origin antigravity/hermes-integration
```

Работа, которой нет в `origin`, для проекта не существует: проверка идёт исполнением, а не по отчёту.

---

## Параллельная работа: строгая граница по файлам

Одновременно выполняется **A12** вторым исполнителем.

**Ваши файлы:**

```
src/antigravity_provider/hermes_plugin.py
src/antigravity_provider/router/router_engine.py
src/antigravity_provider/router/codex_oauth.py
src/antigravity_provider/router/profile_manager.py
src/antigravity_provider/router/quota_collector.py
src/antigravity_provider/router/adapters/**
src/antigravity_provider/agy_subprocess.py
docs/UI_STATE_CONTRACT.md
tests/test_integration_*.py  (новые файлы)
```

**Не ваши** (зона A12): `router/ui/**`, `router/model_discovery.py`, `router/model_discovery_service.py`, `router/router_config.py`, `installer/**`, `tests/test_ui_*.py`.

Понадобился чужой файл — скажите, будет заказан. Молча не трогать: в проекте уже был случай, когда два исполнителя переписали один файл и слияние дало конфликт в двух местах.

---

## Что принято по A9

Проверено исполнением на живой машине владельца — работа хорошая:

- **миграция конфигурации работает**: 16 → 22 профиля, `claude` и `grok` получили по три слота, `find_free_slot` возвращает существующие профили по всем пяти провайдерам. Это снимает корень жалобы «при подключении грока ошибка»;
- резервная копия `router_profiles.yaml.bak_<ts>` создаётся, **десять профилей antigravity не изменены ни в одном поле**, комментарии не потеряны;
- квоты `grok` и `opencode-go` честно отдают `None` с причиной вместо правдоподобных чисел;
- флаг `/repair` и `/reinstall` задействован, предупреждение CS0219 исчезло;
- граница зоны Codex не нарушена, правка плагина `2d62d39` сохранена.

**Исправлено ревьюером при слиянии** — переделывать не нужно, но знать полезно:

1. Служба обнаружения моделей была **недостижима**. Создан `model_discovery_service.py`, а интерфейс импортирует `model_discovery`. Импорт обёрнут в `except ImportError`, поэтому расхождение не давало ошибки — выбор моделей просто оставался пустым навсегда. Добавлена согласованная точка входа.
2. Служба отдаёт `discovered_at`, каталог искал `fetched_at`. Сведено.
3. Тест `test_ui_routing_graph.py` закреплял выдуманный список `grok-3`; вы верно убрали литералы, и тест начал падать. Приведён к честному поведению.

Урок на будущее: **защитный `except ImportError` прячет несобранную интеграцию.** Когда пишете модуль для чужого потребителя — согласуйте путь и проверьте импорт исполнением, а не глазами.

Два числа из отчёта A9, которые не сошлись: «218 passed, 29 skipped» против **307 passed, 2 skipped** на слитом `main`, и «Release Gate 7/7 PASSED» против **FAILED** в моём замере. Двадцать девять пропусков означают, что UI-тесты у вас не выполнялись. Прогон обязателен в окружении **с** `customtkinter`, `pillow`, `psutil`.

---

## P0-1. Hub перехватывает каждый вызов Hermes и назначает ему роль `orchestrator`

Самое важное в задании.

Владелец сообщил: «зашёл в Гермеса, а там наш хаб не работает, основной оркестратор не выбрался», и заключил, что Hub к Hermes не привязан. Заключение неверное, положение хуже: **Hub привязан и активно ломал Hermes.**

Установлено разбором кода Hermes и его журналов:

1. Плагин регистрируется штатно. `hermes_cli/plugins.py:4789` берёт `register` у модуля, корневой `__init__.py` его экспортирует, `ctx.register_middleware("llm_execution", …)` — допустимое имя (`hermes_cli/middleware.py:23`). Перехватчик **срабатывает на каждом обращении к модели**, это видно в трейсбеке `agent.log`.

2. **Hermes не передаёт роль.** `agent/conversation_loop.py:2950` передаёт `task_id`, `turn_id`, `api_request_id`, `session_id`, `platform`, `model`, `provider`, `base_url`, `api_mode`, `api_call_count`. Ключа `role` нет.

3. `resolve_role` доходит до последней строки и возвращает `config.default_role`. **Каждый вызов Hermes идёт как `orchestrator`.**

4. Цепочка `orchestrator` у владельца исчерпана целиком:

```
ag-orch-fallback  skipped_unhealthy
codex-orch        429: Your account is not active, please check your billing details
opengo-3          No API key found for OpenCode Go profile 'opengo-3'
ag-w1, ag-w3      Antigravity error: agy error: authentication failed or timed out
```

5. Роутер возвращал «⚠️ Hermes Router Failover Exhausted» **как ответ ассистента**, и Hermes показывал это вместо ответа модели, хотя его собственный провайдер работал.

Следствие устранено ревьюером (`2d62d39`): при `router_error` вызов уходит вниз через `next_call`, отказ пишется в журнал уровнем `warning`. Закрыто тестом `tests/test_plugin_passthrough.py`. **Правку не откатывать.**

Принцип, который она закрепляет: **плагин может улучшить маршрутизацию, но не имеет права сделать Hermes хуже, чем без него.**

**От вас — причина.** Сейчас Hub на каждом вызове Hermes сначала пробует цепочку `orchestrator`: лишняя задержка и расход квоты не той роли даже там, где пропуск сработал верно.

1. **Определить роль честно.** Разобрать, что из переданного Hermes пригодно как признак: `task_id`, `session_id`, `platform`, `model`, `provider`. Надёжного признака нет — **не угадывать**. Эвристика в `resolve_role`, ищущая в системном сообщении подстроки «developer», «coding agent», «review agent», — это гадание по тексту промпта, и оно тоже подлежит пересмотру.

2. **Не претендовать на вызов без роли.** Роль не определена достоверно — пропускать вниз сразу, не тратя попыток. Роль по умолчанию для внешнего перехвата — неверная модель поведения.

**Тесты:** вызов без определяемой роли уходит вниз, не тратя попыток роутера; вызов с определённой ролью маршрутизируется; отказ цепочки никогда не возвращается как ответ ассистента.

## P0-2. Граница между учётными системами Hub и Hermes

Владелец прав по существу: Hub профилями Hermes не управляет.

Hermes ведёт собственные профили в каталоге `profiles` своего домашнего каталога:

```
agy-01 … agy-06, worker-fast, worker-research, worker-review,
worker-code, worker-code-2, deepseek
```

и настраивает `delegate_task` отдельно (`max_concurrent_children=3`, `provider=opencode-go`, `model=kimi-k2.7-code`).

Профили Hub — `ag-w1`, `ag-orch-fallback`, `codex-orch`, `opengo-*` — **другое множество идентификаторов**. Один и тот же аккаунт Google живёт в двух учётных системах под разными именами.

**Требуется:**

1. Раздел в `docs/UI_STATE_CONTRACT.md`: что Hub видит от Hermes, чего не видит, чем управляет и чем не управляет. Без этого интерфейс, показывающий «команду агентов», вводит владельца в заблуждение — он видит роли, которых Hermes не спрашивает.

2. **Варианты связывания профилей с оценкой цены каждого:** сопоставление по идентичности аккаунта (email из `id_token`), чтение профилей Hermes как источника, либо явная таблица соответствия. **Решение принимает владелец — вам подготовить варианты, не реализовывать молча.**

## P0-3. Codex: обновление токена и безопасное переключение аккаунта

Владелец прислал, как это делает Cockpit Tools, и просит так же:

```
1. Прочитать данные аккаунта      access_token · id_token · refresh_token
2. Проверить access_token          действителен до 28.08, обновление не требуется
3. Проверить id_token              истёк 5 дней назад — нужно обновление
4. Обновить данные входа           полный набор обновлён и сохранён
5. Остановить прежний процесс      безопасная остановка ChatGPT/Codex и app-server
6. Записать данные клиента
7. Синхронизировать настройки
8. Запустить клиент Codex
```

Ключевое: токены проверяются **по отдельности**, и клиент останавливается **до** подмены учётных данных.

В отчёте A9 упомянуты токены Codex, но проверить это исполнением не удалось: у профилей Codex на машине владельца нет авторизации. Поэтому пункт остаётся открытым и должен быть закрыт **тестами**, а не только кодом:

1. Обновление токена по `refresh_token` с сохранением полного набора и понятной ошибкой, когда `refresh_token` отсутствует или отвергнут.
2. Раздельная проверка срока `access_token` и `id_token` с запасом по времени; в статусе профиля видно, что именно просрочено.
3. Переключение аккаунта как наблюдаемая последовательность: остановка клиента → запись учётных данных → синхронизация → запуск. Каждый шаг сообщает о себе, чтобы интерфейс (зона A12) показал прогресс.
4. Сбой на любом шаге не оставляет промежуточного состояния: либо переключено полностью, либо возврат к прежнему.

**Тесты:** просроченный `access_token` при живом `refresh_token` обновляется без повторного входа; отсутствие `refresh_token` даёт понятную ошибку, а не молчаливый провал; прерывание на середине не оставляет смешанных учётных данных.

## P0-4. Обнаружение моделей Antigravity устроено неверно

Служба кэширования из A9 работает, но зонд для Antigravity не возвращает ничего. Проверено:

```
discover_models failed: agy.exe -p x --model __invalid_probe__ ... timed out after 20 seconds
обнаружено моделей: 0
```

`agy_subprocess.py:122` запускает `agy` с **заведомо неверной моделью** и разбирает текст ошибки, надеясь выудить из неё список. Это отказало: команда просто виснет на 20 секунд.

При этом у CLI есть штатная команда. Проверено вручную — `agy models` возвращает четырнадцать настоящих моделей, разделитель табуляция:

```
gemini-3.7-flash-high     Gemini 3.7 Flash (High)
gemini-3.1-pro-high       Gemini 3.1 Pro (High)
claude-sonnet-4-6         Claude Sonnet 4.6 (Thinking)
gpt-oss-120b-medium       GPT-OSS 120B (Medium)
```

**Требуется** заменить зонд на `agy models` с разбором табулированного вывода.

Осторожно с таймаутом: **та же команда в одном прогоне отвечает за 40 секунд, а в следующем висит больше двух минут.** Это замерено, не предположение. Жёсткий таймаут обязателен, при его срабатывании прежний кэш сохраняется.

Заодно убрать выдуманный запасной список в `antigravity_adapter.py:144`:

```python
return list(profile.preferred_models or ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-thinking"])
```

Моделей `gemini-2.5-*` у провайдера **не существует** — настоящие начинаются с `gemini-3`. Это тот же класс дефекта, из-за которого в конфигурацию владельца попал несуществующий `gemini-3.7-flash`, стоящий сейчас у роли `orchestrator` как `default_model`. Нет обнаруженного списка — возвращать пустой.

**Тест:** зонд возвращает непустой список на подготовленном выводе `agy models`; при таймауте прежний кэш не затирается; запасного литерала в адаптере нет.

## P1-5. Квоты: `source` не должен опережать данные

Мелочь, но она из того же семейства, с которым боремся весь проект:

```
opencode-go:opengo-1  source=provider_api
    Общий 5 часов / Недельный / Месячный:  remaining=None
```

`source="provider_api"` заявляет измерение, которого не было — все корзины пусты. Либо `source="baseline"`, когда чисел нет, либо отдельное поле, различающее «провайдер ответил, но лимитов не даёт» и «провайдер не ответил». Причина уже пишется правильно, расходится только `source`.

---

## Что принято и переделке не подлежит

Не откатывать при слиянии:

- живые квоты Antigravity (`retrieveUserQuotaSummary` с обновлением токена при 401): шесть аккаунтов владельца отдают разные измеренные числа;
- правка плагина `2d62d39` и тест `tests/test_plugin_passthrough.py`;
- `_finish` мастера: `destroy()` выполняется всегда;
- исправление `is_expired` в `do_test_profile`;
- миграция конфигурации из A9 и точка входа `model_discovery.py`.

## Ограничения

- Строгая граница по файлам — см. выше.
- Никаких чисел, идентификаторов и названий моделей без измерения.
- Сеть и подпроцессы — не в UI-потоке.
- Тег `v0.1.1` не создавать.

## Критерии приёмки

1. Ветка в `origin`. Ни один файл зоны A12 не изменён.
2. Вызов без достоверно определённой роли уходит вниз, не тратя попыток роутера; отказ цепочки никогда не возвращается как ответ ассистента; правка `2d62d39` сохранена.
3. Граница между учётными системами Hub и Hermes описана в контракте; варианты связывания профилей поданы с ценой каждого, без односторонней реализации.
4. Токен Codex обновляется по `refresh_token`; переключение останавливает клиент до подмены учётных данных и не оставляет промежуточного состояния; закрыто тестами.
5. `agy models` используется как зонд; обнаружение возвращает непустой список; при таймауте кэш сохраняется; выдуманного запасного списка в адаптере нет.
6. `source` квоты не заявляет измерения там, где чисел нет.
7. Прогон **в окружении с UI-зависимостями**; число пропусков объяснено. На `main` набор даёт 307 passed, 2 skipped.
8. `ruff check .` чисто. Релизный гейт: назвать состояние до и после, объяснить расхождение с замером ревьюера; ухудшать нельзя.
9. Отчёт: `START_HEAD`, `FINAL_HEAD`, `origin/main`, `git status`, точный `X passed / Y skipped / Z failed`.

## Порядок сдачи
Передать точный `FINAL_COMMIT_SHA`. **Сдано только после появления ветки в `origin`.**
