core.autocrlf=true и отсутствие .gitattributes приводили к тому, что
install-linux.sh хранился с CRLF. На Linux это даёт "$'\r': command not
found", а самораспаковывающийся установщик, собранный после checkout на
Windows, ломался бы целиком: пролог обрезает себя по строке-маркеру, а
маркер с \r не совпадает.
Добавлен .gitattributes: *.sh/*.py/*.yaml только LF, *.ps1/*.bat только
CRLF, *.exe/*.ico/*.png помечены binary, чтобы нормализация их не трогала.
Индекс перенормализован.
Проверено: в индексе все четыре shell-скрипта с LF, заголовок MZ у
launcher/*.exe цел, пересобранный dist/hermes-hub-setup.sh не содержит CR
ни в прологе, ни в распакованном install-linux.sh.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
install-linux.sh читал исходники из $REPO_ROOT, поэтому на каждую машину
приходилось приносить весь клон git. Добавлен build_installer_linux.sh: он
собирает один файл dist/hermes-hub-setup.sh — пролог на shell, строка-маркер
и tar.gz побайтово следом. Пролог обрезает себя по маркеру, распаковывает
хвост во временный каталог и запускает оттуда install-linux.sh. Состав
поставки тот же, что у виндового payload, плюс installer/; виндовые .exe и
__pycache__ из линуксовой поставки вычищаются.
Убран запасной коммит-литерал 'fb23bff' в манифесте: при сборке без git он
подставлял чужой номер сборки, из-за чего установленная версия выглядела
определённой, не будучи ею. Теперь коммит берётся из BUILD_COMMIT, который
кладёт сборщик, иначе — 'не определён'.
Проверено: распаковка проходит, в поставке лежат свежие server.py и app.js,
секретов, ключей и почт в ней нет.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец сообщил, что в маршрутизации не переставляются блоки, нигде нет
выбора аккаунта для роли и на «Обзоре» ничего нельзя изменить. На скриншоте
при этом видна кнопка «Изменить цепочку», которой в коде уже нет: A24 её
удалил вместе с renderTeam и renderProviders.
Причина не в логике. FileResponse отдавал app.js и style.css без заголовка
Cache-Control, поэтому браузер применял эвристическое кэширование и держал
скрипт от прошлой сборки. index.html при навигации перепроверялся и был
свежим — отсюда смесь нового текста подсказки со старыми кнопками, а
перетаскивание и выбор модели просто отсутствовали в загруженном коде.
Действие reorder_chain при этом исправно: проверено вызовом, порядок
цепочки меняется и сохраняется в router_profiles.yaml.
Заодно подключённые аккаунты выводятся первыми, а пустые слоты
(«не подключён») — в конце группы: рабочие карточки были разбросаны
между пустыми и их приходилось выискивать.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец открыл карточку Grok и увидел «Grok 2h — Н/Д» и статус «Не
проверялся» — хотя сервер в этот момент уже отдавал настоящие данные:
подписка 14%, GrokChat 13%, GrokBuild 1%.
Проверено на живом сервере: и /api/snapshot, и quota_snapshot внутри
профиля содержали правильные корзины. Дефект целиком в клиенте — окно
рисовалось ОДИН РАЗ при открытии и на опрос не реагировало. Открытое до
завершения прогрева квот, оно навсегда оставалось с заглушкой
_generate_baseline_snapshot, а после успешной проверки подключения
по-прежнему показывало «Не проверялся».
Теперь открытый профиль отслеживается и перерисовывается при каждом
применении снапшота. Объявление переменной поднято выше первого
использования: let не поднимается, и обращение раньше объявления
роняло бы обработчик снапшота целиком.
При закрытии окна сбрасывается отслеживание и останавливается опрос кода
устройства — раньше он продолжал работать после закрытия мастера.
Тесты: 431 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец был прав: «апи не нужен». Подтверждено на его аккаунте GROK PRO —
api.x.ai/v1/chat/completions вернул 200 и ответ модели grok-4.3 БЕЗ
покупки кредитов. Прежний 402 был целиком из-за того, что подключён был
другой аккаунт, без подписки. Моё утверждение про «разные кошельки»
окончательно снято.
1. Квота показывала «Н/Д» при живой подписке. Читались только
prepaidBalance и onDemandCap — у подписчика оба нулевые. А расход
подписки лежит в creditUsagePercent, и разбивка по продуктам в
productUsage. Теперь оттуда и берётся: на живом аккаунте выходит
14% за неделю, GrokChat 13%, GrokBuild 1% — ровно то, что владелец
видит на grok.com. Корзина кредитов создаётся только когда они реально
заведены: нули у подписчика — норма, а не повод рисовать пустое.
2. Выбор модели отвергал настоящие имена: «кэш моделей для grok пуст, а
модель grok-4.5 не найдена». Причина — в _probe_provider грока не было
вовсе, обнаружение знало только antigravity, codex, opencode и local.
При этом api.x.ai/v1/models принимает тот же OAuth-токен и отдаёт 12
моделей. Зонд добавлен; grok-4.5 теперь принимается, выдуманная
grok-99-turbo — отклоняется.
Тесты: 428 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец: «удалить так и не могу ненужный». Backend был починен в
d755a07, но кнопка по-прежнему не работала.
Причина в клиенте: он отправлял только profile_id, без provider. Сервер
не знал, в каком каталоге искать auth.json, строил неверный путь и снова
отвечал «удалять нечего».
Идентификатор профиля однозначен, поэтому провайдер теперь берётся из
конфигурации, когда его не передали. Действие работает независимо от
того, кто его вызвал.
Заодно в клиенте: подтверждение перед необратимым удалением и обновление
экрана после — раньше карточка оставалась в прежнем виде, и было
непонятно, сработало ли.
Проверено через веб-API без параметра provider: до — авторизован,
после — нет.
Тесты: 426 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец прислал анонс x.ai/news/grok-hermes: доступ к Grok даётся по
подписке SuperGrok через OAuth, покупать кредиты API не требуется. Моя
формулировка в интерфейсе утверждала обратное — что подписка кредиты не
пополняет и это отдельный кошелёк. Это неверно, и оно уже попадало
пользователю на экран.
Установлено проверками:
- cli-chat-proxy.grok.com/v1/models принимает наш OAuth-токен -> 200,
отдаёт grok-4.6;
- тот же прокси /chat/completions -> 426, требует версию Grok CLI
не ниже 0.1.202, и она читается не из заголовков, которые я перебрал;
- api.x.ai/v1/responses (путь из документации Hermes) -> 402
personal-team-blocked:spending-limit.
При этом подключён ochenstarik@gmail.com, а подписка SuperGrok — на
victor.trushenko@gmail.com. То есть отказ объясняется аккаунтом, а не
природой подписки, и утверждать иное я не мог.
Сообщение переписано на проверяемое: у аккаунта нет ни баланса, ни
лимита трат, а доступ даёт подписка на ЭТОМ же аккаунте либо купленные
кредиты. Само чтение биллинга остаётся — оно работает и показывает
настоящие числа.
Тесты: 422 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец спросил, где кончились лимиты, и показал экран: еженедельный
лимит SuperGrok израсходован на 14%, запас есть. А вызовы через Hub
падали с 402 «out of credits».
Разгадка — два разных кошелька. Подписка SuperGrok покрывает чат
grok.com, а программный доступ списывается с кредитов API. Проверено на
живом аккаунте: prepaidBalance 0, onDemandCap 0. Именно поэтому 402, и
подписка тут не помогает.
Адрес биллинга взят из плагина hermes-grok-usage, который владелец уже
использует: cli-chat-proxy.grok.com/v1/billing принимает ТОТ ЖЕ
OAuth-токен, что и авторизация. Проверено запросом — 200 и данные.
_collect_grok_quota раньше строила пустые корзины и никуда не ходила,
поэтому Hub показывал «Н/Д» и объяснить ничего не мог. Теперь читает
предоплаченный баланс и лимит по мере использования, а при нулевом
балансе пишет причину прямым текстом, включая различие кошельков.
Процент считается только когда есть от чего считать: при нулевом лимите
доля не определена, и подставлять ноль процентов нельзя — показываются
абсолютные значения.
Тесты: 422 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец подключил не тот аккаунт Grok и не смог его убрать: «кнопка не
функционирует». Хуже: действие возвращало ok=True с сообщением об успехе,
а профиль оставался авторизованным.
Причина: сигнатура get_profile_dir — (profile_id, provider), а
do_delete_credentials звала её наоборот. Внутри функции есть костыль,
молча исправляющий перестановку, но только для трёх провайдеров:
antigravity, openai-codex, opencode-go. Для grok, claude и local путь
получался неверным (grok_profiles/grok вместо grok_profiles/grok-worker-1),
файл «не находился», и срабатывала ветка «учетные данные отсутствовали»
с ok=True.
То есть удаление работало у трёх провайдеров из шести, а у остальных
молча лгало.
Теперь используется get_profile_auth_path, который берёт аргументы в
правильном порядке. Отсутствие файла больше не считается успехом
удаления: возвращается честный отказ «удалять нечего».
Проверено на живом профиле: до — авторизован, после удаления — нет,
повторная попытка сообщает, что удалять нечего. Тест покрывает все
четыре провайдера, у которых костыль не срабатывал.
Тесты: 419 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец упёрся в заглушку «подключение через веб-интерфейс пока не
реализовано» и не смог подключить Grok. Backend был готов давно —
start_grok_oauth и start_codex_oauth возвращают настоящие адрес и код, —
но наружу не выведен: подключить эти провайдеры можно было только из
десктопа.
Добавлены действия start_device_auth и poll_device_auth. Адрес и код
выдаёт ПРОВАЙДЕР, интерфейс их только отображает — никаких подставленных
значений, как было с выдуманными GRK-7842 и CDX-9104.
Клиент показывает ссылку с кнопками «Открыть» и «Копировать», код
крупно и моноширинным, и опрашивает состояние каждые три секунды. Отказ
и просроченный код показываются как окончательные, опрос прекращается —
это работает вместе с правкой 50e4de5, научившей опрос различать
authorization_pending, access_denied и expired_token.
Проверено через веб-API: start отдаёт настоящий адрес accounts.x.ai и
код, poll возвращает pending, несуществующая сессия — честный отказ.
Контракт поднят до 1.4, действий стало двадцать одно.
Это снимает главное препятствие к удалению десктопа: он был
единственным путём подключить Grok и Codex.
Тесты: 414 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец: «не даёт зайти в грок через аутх». Backend при этом исправен —
проверено: провайдер возвращает настоящую сессию, адрес
accounts.x.ai/oauth2/device и код.
Дефект в цикле опроса: коды 400, 403 и 404 скопом считались
«авторизация ещё не подтверждена» и опрос молча продолжался. Но в
device-flow сервер сообщает РАЗНЫЕ вещи одним кодом 400, различая их
полем error в теле: authorization_pending, slow_down, access_denied,
expired_token.
Следствие: отказ пользователя и просроченный код выглядели как ожидание.
Мастер показывал «Ожидание подтверждения...» до самого таймаута и не
говорил, что подтверждение уже отклонено или код давно истёк.
Теперь каждый исход обрабатывается по существу: ожидание продолжает
опрос, slow_down увеличивает интервал, отказ и просрочка прекращают его
с внятным сообщением.
Тесты: 414 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Найдено при проверке Grok на живых данных владельца. Вызов падал с
AttributeError: 'str' object has no attribute 'get' — grok_adapter.py:103.
Поле error провайдеры отдают то объектом {"message": ...}, то строкой.
Код безусловно звал .get у результата, и на строковой форме ОБРАБОТЧИК
ОШИБОК ПАДАЛ САМ: сбой происходил ровно там, где обрабатывался другой
сбой, маршрутизация обрывалась вместо перехода к резерву, а настоящая
причина терялась.
После правки причина видна: Grok API Error (403): The OAuth2 access token
could not be validated. То есть у Grok просто протух токен, а выглядело
как поломка кода.
Та же конструкция стояла ещё в пяти адаптерах: claude, codex, opencode,
deepseek, local. Разбор вынесен в base_adapter.extract_api_error_message,
все шесть переведены на него.
Тест проверяет обе формы ответа и отдельно следит, чтобы копии хрупкой
конструкции не вернулись.
Тесты: 411 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Обе работы приняты, проверено исполнением.
A24: ровно семь разделов, renderTeam и renderProviders удалены, кнопки
«Изменить цепочку» нет, перетаскивание блоков есть. Перестановка цепочки
проверена вживую: сохраняется в router_profiles.yaml и откатывается.
A25: провайдер local подключён к настоящему серверу владельца через
SSH-туннель к 127.0.0.1:8081. health_check проходит, /v1/models отдаёт
модель, реальный вызов возвращает «ОК» за 3.1 с. Профили local-1 и
local-2 добавлены. Квота отдаётся отдельным состоянием
(source=local_provider, «Без ограничений»), а не как отсутствие данных.
Адрес сервера нигде не зашит.
Разрешение конфликта в action_handler: A25 внёс edit_route и assign_role
в список «просто навигация», где они возвращают заглушку. В A24 это
работающие обработчики — сохранение цепочки и назначение роли. Приняв
версию A25 целиком, мы бы молча сломали перестановку блоков. Оставлены
оба: локальный провайдер в add_account и рабочие обработчики ниже.
Исправлено при слиянии:
1. Адаптер отдавал пустой ответ как успех. У сервера владельца
--reasoning on --reasoning-budget 4096: при скромном max_tokens весь
бюджет уходит на рассуждения, llama.cpp возвращает 200, заполняет
reasoning_content и оставляет content пустым. Проверено на живой
модели: max_tokens=40 — ответа нет, 200 — приходит «ОК». Роутер
засчитал бы такой вызов, а пользователь не получил бы ничего.
Теперь это явный отказ с объяснением, и срабатывает переключение.
Проверка вынесена из блока перехвата: иначе оборачивалась в
«Transport Error», хотя транспорт отработал штатно.
2. Заглушка в тесте A25 возвращала "choices": [] — такого настоящий
сервер не отдаёт. Приведена к реальному виду.
Тесты: 404 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец дал доступ по ключу. На 192.168.1.81 работают два сервера
llama.cpp, оба OpenAI-совместимые, проверено запросами:
8081 Qwen3.8-27B-Q4_K_M reasoning on, контекст 65536, -ngl 99
8082 Qwen3-4B-Instruct reasoning off, «compressor»
GET /v1/models и POST /v1/chat/completions отвечают 200 на обоих.
Видеокарта Tesla V100-32GB, занято 28.7 из 32.7 ГБ.
Три следствия внесены в задание как определяющие реализацию:
- оба слушают только 127.0.0.1, снаружи недоступны; варианты решения
предложены владельцу, выбор за ним;
- у обоих --parallel 1, то есть один запрос за раз: лизы Hub обязаны
ограничивать локального провайдера, иначе роли заблокируют друг друга;
- памяти видеокарты на третью модель нет, слотов ровно два.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
У владельца сервер 192.168.1.81 с локальной LLM; хочет использовать её
как субагента. Ценность: локальная модель не имеет квоты и не стоит
денег — идеальный последний резерв. Сейчас у него реально работает один
провайдер из пяти, запаса нет.
Половина работы уже есть: RouterProfileConfig.custom_base_url
существует, deepseek_adapter — готовый образец OpenAI-совместимого
вызова, а профиль через provider: custom у него уже настроен в Hermes.
Первым пунктом — разведка, а не код: ревьюер снаружи увидел открытыми
только 22 и 3000 (Rocket.Chat), порт Ollama закрыт. Что именно слушает,
выясняет владелец на сервере: доступа по SSH нет, вход по ключу не
настроен, пароли не используются.
Отдельным пунктом — честность про квоту: у локальной модели её нет как
понятия, и показывать Н/Д рядом с процентами других провайдеров нельзя,
это читается как «данные не пришли».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Решение владельца по итогам живой эксплуатации: разделов семь вместо
девяти. Убираются «Команда агентов» и «Модели и провайдеры», их
содержимое перераспределяется между маршрутизацией и обзором. Аналитика
остаётся, но должна объяснять свои метрики.
Маршрутизация становится местом управления: перестановка блоков
перетаскиванием, смена модели на месте, кнопка «Изменить цепочку»
убирается.
В задание вынесен вывод из только что найденного дефекта: в веб-мастере
стояли выдуманные коды устройства GRK-7842 и CDX-9104, а тест ТРЕБОВАЛ
наличия неверного адреса, то есть защищал выдумку от исправления.
Пункт P0-6 для второго прохода дополнен проверкой на выдуманные
значения и на тесты, закрепляющие дефект.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец: «грок не даёт добавить https://x.ai/device» — страница отдаёт
404. Причина хуже опечатки в адресе.
Веб-мастер для Grok и Codex не был подключён к серверу ВООБЩЕ. Он
показывал жёстко вписанный адрес x.ai/device (настоящий —
auth.x.ai/device, и тот приходит от провайдера полем verification_uri) и
ВЫДУМАННЫЕ коды устройства GRK-7842 и CDX-9104. Пользователь вводил бы
несуществующий код бесконечно.
Это тот самый класс дефекта, который вычищали из десктопа в первом
аудите, вернувшийся в новом коде.
Выдуманные значения убраны. Пока поток не проведён через веб-API, шаг
честно сообщает, что подключение через веб не реализовано, и указывает
рабочий путь — десктопное приложение, где поток проведён полностью.
Отдельно: тест test_headless_server_auth_matrix ТРЕБОВАЛ наличия
"https://x.ai/device" в коде, то есть закреплял дефект как требование.
Переписан на противоположное — запрещает выдуманные коды и зашитые
адреса провайдеров.
Тесты: 375 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Со скриншота владельца: заголовок «Ролей в строю: 0/6», а ниже шесть
предупреждений «роль работает через резервный аккаунт». Пять ролей
исправно отвечали, интерфейс сообщал, что не работает ни одна.
roles_ready считал только роли со здоровым ОСНОВНЫМ профилем. Роль,
обслуживаемая резервом, попадала в degraded_roles, но не в ready.
Ошибка в худшую сторону: отказ показывался там, где всё работает — а
переключение на резерв это ровно то, ради чего продукт и создан.
Теперь роль с живым резервом считается работающей и одновременно
помечается деградировавшей: оба состояния остаются различимыми.
Попутно склонение: «Есть 1 ролей без рабочего маршрута» заменено на
согласованное с числом — 1 роль, 2 роли, 5 ролей.
Проверено на живых данных: 6/6 в строю, состояние «деградация», а не
«критическое».
Тесты: 375 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1. Главное: ERR_CONNECTION_REFUSED в окне приложения.
browserProc.WaitForExit() возвращался МГНОВЕННО, когда Edge уже был
запущен: новый msedge.exe передаёт окно работающему экземпляру и сразу
завершается. Лаунчер считал, что окно закрыли, и убивал сервер, пока
страница ещё грузилась.
Браузер теперь запускается с отдельным профилем (--user-data-dir), то
есть процесс живёт столько же, сколько окно. Плюс подстраховка: выход
браузера быстрее пяти секунд не считается закрытием окна.
Та же ловушка была в Linux-лаунчере — исправлена там же.
Проверено вживую: сервер отвечает 200 И окно приложения живо
(«Hermes Hub — Панель управления»).
2. Установщик по галочке «Запустить сейчас» открывал ДЕСКТОП. Владелец
получал десктопное окно и принимал его за старую версию — внешне оно
и правда другое. Теперь запускается веб-интерфейс, подпись галочки
уточнена. Десктоп остаётся доступен своим ярлыком.
Тесты: 373 passed, ruff чисто. Установщик пересобран.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Три дефекта из установки владельца на вторую машину.
1. Веб-сервер падал сразу: «web server process terminated unexpectedly».
Установщик ставит в venv Hermes только customtkinter, pillow, pyyaml и
psutil — fastapi и uvicorn отсутствовали в списке вовсе. Добавлены во
все шесть мест: проверка, сообщение, pip, uv, перепроверка.
Поэтому на Windows открывался только десктоп: веб физически не мог
стартовать.
2. Лаунчер показывал голое «terminated unexpectedly» без причины — та же
болезнь, что у кода 12. Теперь перехватывает вывод процесса и выводит
последние строки ошибки в окне.
3. Окно тормозило при перетаскивании и продолжало двигаться несколько
секунд после отпускания мыши. _RouteDiagram перерисовывал всю канву на
КАЖДОЕ событие <Configure>, а при перетаскивании их сотни; очередь не
успевала разгребаться. Гашение в приложении существовало, но
_handle_debounced_resize был пустой заглушкой.
Перерисовка сведена к одной после затишья. Замерено: 199 событий
давали 199 перерисовок, теперь 4.
Тесты: 373 passed, ruff чисто. Установщик пересобран.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Три дефекта, найденные при установке владельцем на две машины.
1. Linux: лаунчер писал «Web server failed to respond», хотя сервер
поднимался нормально — в логе старт API, планировщик квот и прогрев
кэша без ошибок. Причина в самой проверке: sys импортировался ТОЛЬКО
внутри except, а использовался в успешной ветке. На здоровом ответе
возникал NameError, его ловил тот же except, и проверка всегда
возвращала отказ.
Доказано исполнением на живом сервере: старая логика -> код 1,
новая -> код 0.
2. Windows: при нечитаемом манифесте мастер показывал зашитые
InstalledVersion = "0.1.0" и InstalledDate = "19.08.2026" как факт.
Владелец видел «старую версию» на свежей установке, хотя проверка
показала правильный путь и версию 0.1.1. Заглушки заменены на
«не определена» — выдуманный факт хуже отсутствующего.
3. Манифест писался одним File.WriteAllText: прерванная запись оставляла
пустой файл, и разбор версии падал на первом символе — ровно это и
случилось у владельца (JSONDecodeError, char 0). Запись переведена на
временный файл с переносом.
Попутно: git_commit в манифесте был зашит как "8cddc9f", то есть манифест
сообщал неправду о происхождении сборки. Теперь сборщик проставляет
фактический коммит.
Тесты: 373 passed, ruff чисто. Установщик пересобран.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец получил «Ошибка установки (Код: 12)» на чистой машине.
Причина: scripts/verify_multi_provider_router.py, который установщик
запускает после развёртывания, требовал РОВНО 16 профилей и дословно
заданные цепочки ролей. Миграция из A9 законно доводит конфигурацию до
22 профилей, добавляя claude и grok. Проверено: скрипт падал с
«Expected 16 profiles, got 22», то есть установка обрывалась на любой
машине, где миграция отработала.
Проверки переписаны структурными: есть ли профили у каждого провайдера,
непусты ли цепочки ролей и ссылаются ли они только на существующие
профили. Смысл проверки — работоспособна ли маршрутизация, а не совпадает
ли конфигурация с зафиксированной когда-то. Скрипт проходит 10/10.
Отдельно: код 12 возвращался и при отказе проверки, и из общего catch —
владелец видел число без причины. Непредвиденный сбой отделён в код 15,
обе ветки теперь пишут пояснение в интерфейс установщика.
Тесты: 373 passed, ruff чисто. Установщик пересобран.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец: «для винды я думаю нужен exe». Справедливо — «склонируй
репозиторий и собери» это не установка.
HermesHubSetup.exe требовал, чтобы рядом лежали src/, launcher/, assets/,
config/ и scripts/: PerformInstall берёт их из sourceRoot. Поэтому одного
файла не хватало, и на целевую машину пришлось бы копировать репозиторий.
Теперь содержимое упаковывается при сборке и вшивается в exe ресурсом
(/resource:payload.zip,payload). Если рядом с exe и уровнем выше
исходников нет, установщик распаковывает вшитое во временный каталог и
работает с ним. Прежнее поведение сохранено: при запуске из репозитория
используются файлы на диске, ресурс не трогается.
Проверено исполнением: exe скопирован в пустой каталог, из него извлечены
assets, config, launcher, scripts, src; src/antigravity_provider и
launcher/HermesHubWeb.exe на месте. Размер 11.78 МБ.
Тест сборки установщика приведён к реальным ссылкам: добавлена
System.IO.Compression.FileSystem, без неё ZipFile не разрешался.
Тесты: 373 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Найдено проверкой всех шести ролей на живой машине владельца. До правок
работали три роли из шести.
1. antigravity_adapter.classify_error возвращал ErrorCategory.UNKNOWN —
значения с таким именем не существует, есть AUTH_REQUIRED, FATAL,
INVALID_REQUEST, QUOTA_EXHAUSTED, RATE_LIMITED, TRANSIENT. Обращение
роняло сам классификатор с AttributeError, то есть отказ происходил
ровно там, где обрабатывался другой отказ, и маршрутизация обрывалась
вместо перехода к резервному профилю. Заменено на FATAL по образцу
codex и opencode: неразобранная ошибка не должна давать повторов.
2. Уровень усилия не подставлялся, если у конкретного профиля не выполнен
вход agy: карта усилий строится обнаружением ЧЕРЕЗ этот профиль, и при
неудаче оставалась пустой. Уровни же — свойство модели, а не аккаунта.
Добавлен запасной источник: сохранённый на диске список моделей со
склеенными идентификаторами вида gemini-3.7-flash-high, из которых
уровни выводятся напрямую и переживают перезапуск.
После правок работают все шесть ролей, включая живое переключение
'fast': opengo-1 -> ag-w4.
Закрыто тестами, включая защиту от возврата несуществующей категории.
Тесты: 373 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверено исполнением, оба главных пункта закрыты.
P0-1, восстановление после отказа авторизации: выбран самый честный из
предложенных вариантов — снятие отметки по событию починки, а не по
таймеру. HealthTracker подписывается на EVENT_ACCOUNT_ADDED и
EVENT_ACCOUNT_AUTH_CHANGED. Проверено: профиль с AUTH_REQUIRED после
события возвращается в строй без ручного вмешательства.
P0-2, проверка моделей. Выдуманная модель отклоняется; базовое имя
gemini-3.7-flash принимается (поправка учтена); склеенное
gemini-3.1-pro-high тоже. Главное — при ПУСТОМ кэше выдумка больше не
проходит: молчаливое согласие устранено.
P0-3, ручное обновление списка моделей: действие refresh_models плюс
кнопка в клиенте.
Исправлено при слиянии:
1. Блокировка файла состояния была переведена с неблокирующей на
БЕСКОНЕЧНО блокирующую. Исходный вариант был неверен — при неудаче
исключение проглатывалось и запись шла без блокировки, — но
бесконечное ожидание хуже: на Unix flock(LOCK_EX) висит вечно, и один
застрявший держатель подвесил бы приложение целиком. Впереди
Linux-сервер. Ожидание ограничено 5 секундами, дальше честный отказ.
Проверено: 6 потоков по 5 записей — 0.11 с, ошибок нет.
2. Действие refresh_models не было занесено в контракт. Ровно тот дрейф,
ради предотвращения которого контракт и существует. Контракт поднят
до 1.3, действий стало девятнадцать.
Тесты: 370 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Написано под схему владельца: Flash реализует, Pro проводит аудит.
Пункт P0-4 — чек-лист для второго прохода, составлен из дефектов,
которые уже проходили мимо первого.
P0-1: mark_auth_required ставит состояние без срока истечения, а
маршрутизация пропускает нездоровый профиль — успеха не случится,
отметка не снимется никогда. Подтверждено: после починки авторизации в
A22 все шесть профилей Antigravity остались помечены, ревьюер снимал
отметки вручную.
P0-2: do_set_model пропускает проверку целиком при пустом кэше моделей.
Проверено — выдуманная модель записалась в конфигурацию владельца.
Отсутствие данных трактуется как разрешение.
P0-3: ручного обновления списка моделей нет с A18.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец возразил на моё утверждение, что gemini-3.7-flash не существует.
Он прав, утверждение было неверным, и я повторил его в четырёх заданиях.
gemini-3.7-flash — настоящее семейство, уровень усилия у неё отдельный
параметр. В интерфейсе Antigravity это видно прямо: пункт «Gemini 3.7
Flash» с вложенным выбором Low/Medium/High. В коде это отражено:
_display_to_cli разбирает «Gemini 3.7 Flash (High)» в пару
("gemini-3.7-flash", "high"). Меня ввела в заблуждение первая колонка
вывода agy models со склеенными идентификаторами.
Настоящий дефект был в коде: _model_supported_efforts вызывала
discover_models() БЕЗ профиля, то есть в глобальном окружении без входа.
Карта поддерживаемых усилий оставалась пустой, подстановка уровня по
умолчанию не срабатывала, и agy отвергал вызов с «requires --effort» —
при совершенно настоящей модели.
profile_id проведён через agy_generate в _model_supported_efforts.
Проверено исполнением: gemini-3.7-flash без указания усилия отрабатывает
и возвращает ответ.
Моки в test_antigravity_concurrency приведены к терпимости по kwargs.
Добавлена поправка agents/inbox/2026-08-24-CORRECTION-gemini-model-names.md:
ложное утверждение попало в A9, A11, A18 и B8, и без опровержения кто-то
чинил бы несуществующую проблему или сломал рабочую конфигурацию.
Тесты: 359 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Четыре недостающих экрана добавлены: Аналитика, Состояние, Журнал
событий, Настройки. Веб покрывает все девять разделов десктопа.
Два новых эндпоинта реализованы: GET /api/events и GET /api/settings.
Секреты не утекают — проверено исполнением, наружу отдаётся только
признак web_api_token_configured (bool), сам токен отсутствует.
Слияние без конфликтов. Тесты: 359 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Проверено исполнением, все три требуемых доказательства получены.
1. agy models через профиль Hub: обнаружено 14 моделей, включая
gemini-3.1-pro-high и gemini-3.1-pro-low.
2. Реальный вызов adapter.invoke(ag-w1): модель ответила «ОК».
3. route_request(coder-primary): переключение с отказавшего
codex-worker-1 на ag-w1, ответ получен, router_error отсутствует.
Подход из A20 (синтез oauth_creds.json) действительно был тупиковым;
родной вход agy с подменой HOME решает задачу. Реализация аккуратная:
видимая консоль на Windows через CREATE_NEW_CONSOLE, терминалы на Linux,
HOMEDRIVE выставляется корректно.
P0-4 подтверждён: пересохранение профиля не изменяет ни одного файла в
глобальном ~/.gemini.
Исправлено при слиянии:
1. Инструкция в веб-клиенте вела на НЕСУЩЕСТВУЮЩИЙ файл
launcher/main.py. Заменена на реальный вызов launch_native_agy_login
с указанием профиля.
2. test_headless_server_auth_matrix проверял дословную формулировку и
падал при её правке, хотя поведение оставалось верным. Приведён к
проверке сути, добавлена защита от возврата несуществующего пути.
Конфигурация владельца: обоим кодерам поставлена gemini-3.1-pro-high по
его прямой просьбе — теперь это возможно.
Тесты: 355 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A20 закрыто как неверно поставленное. Синтез oauth_creds.json из
OAuth-потока Hub не работает в принципе: проверены и закрыты пять
гипотез (полнота полей, срок токенов, тот же OAuth-клиент, тот же
scope, согласованность активного аккаунта), а решающий опыт показал,
что agy отказывает даже РАБОЧИМ глобальным учётным данным владельца,
положенным в подменённый HOME. Ошибка в гипотезе автора задания.
Полезное следствие разведки: agy уважает подмену HOME — в каталогах
профилей лежат созданные им же файлы. Изоляция работает, не работал
только синтез.
A22 строит вход через родной механизм agy в окружении профиля. Первым
пунктом — разведка: есть ли неинтерактивный вход, что agy выводит,
как определить завершение. Установлено, что без аргументов это TUI:
в пайп ничего не пишет и виснет.
Приёмка только с тремя доказательствами исполнением: непустой вывод
agy models, успешный реальный вызов, route_request без ухода в резерв.
Плюс проверка побочной находки: в глобальном google_accounts.json
активен один аккаунт, а токен принадлежит другому — Hub мог писать
мимо каталога профиля и сломать владельцу обычный agy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Add openid to OAuth scopes for id_token issuance
- Preserve id_token, scope, and token_type on token exchange and refresh
- Atomically write and sync .gemini/oauth_creds.json in profile directories
- Auto-resolve active profile environment in discover_models
- Cache discovered models in models_cache.json with graceful timeout handling
- Add unit test coverage for full OAuth lifecycle and model discovery caching
Веб покрывает пять экранов из девяти. Не хватает Аналитики, Состояния,
Журнала событий и Настроек.
Данные для первых двух уже в снапшоте: metrics.telemetry (19 вызовов,
15 отказов, латентность p50/p95/max) и metrics.host плюс readiness.
Для двух других источника в API нет вовсе — нужны GET /api/events
(EventLogService в backend есть) и GET /api/settings без секретов.
Зона Flash на это задание расширена на router/web/** целиком, включая
server.py: A20 в web/ не заходит, конфликта не будет.
Обнаружение моделей в задание НЕ включено, хотя A18 его пропустил:
причина оказалась глубже и лежит в авторизации agy, это чинит A20.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Маршрутизация через Antigravity не работает ни для одного из шести
аккаунтов при полностью валидных токенах: квоты по ним приходят
настоящими через прямой HTTPS, а путь через CLI падает с
AuthExpiredError.
Причина найдена и проверена: agy читает <HOME>/.gemini/oauth_creds.json
с шестью полями, включая id_token и scope. Hub пишет свой auth.json в
другом месте и другой структурой, а id_token и scope теряет в двух
местах — oauth.py:88-92 и profile_oauth.py:241-249. Ни один профиль их
не хранит.
Проверено и не сработало: подмена HOME на каталог профиля, сборка
oauth_creds.json без id_token. Значит id_token обязателен.
Владелец решил остаться на OAuth, прямой API отклонён.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A17 принято, проверено на живых профилях владельца: «Работает» больше не
ставится непроверенному профилю. Из 22 профилей теперь 1 «Работает»
(есть записанный успех), 7 «Не проверялся», 11 «Аккаунт не добавлен»,
3 «Отключён». Grok и opengo-1, на которые жаловался владелец, показаны
честно. Добавлено поле last_success_at.
A18 принято частично: действие set_model существует и валидирует модель
по списку провайдера. Но обнаружение моделей (P0-2) не сделано вовсе —
model_discovery_service и зонд не менялись, ручного обновления нет,
кэша на диске нет. Из-за этого set_model отклоняет ЛЮБУЮ модель, включая
настоящую: список провайдера пуст, и валидация не с чем сравнивать.
Правка при слиянии: discover_models запускался в ГЛОБАЛЬНОМ окружении,
где вход agy не выполнен, — при шести рабочих OAuth-профилях. Теперь
принимает profile_id и подменяет HOME/USERPROFILE на каталог профиля,
как это делает adapter.invoke. Таймаут поднят с 10 до 60 секунд.
Это не вылечило симптом, и причина оказалась глубже — см. отчёт.
Тесты: 336 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A17 (Pro): «Работает» — ветка else в определении здоровья, она означает
«мы не знаем о проблемах», а подана как утверждение. Отказы попадают в
статус только после боевого сбоя, поэтому непроверенный профиль
автоматически зелёный. Плюс «Проверить подключение» не вызывает модель
вовсе — Grok её проходит и не работает.
A18 (Flash): действия смены модели не существует ни среди семнадцати, ни
в клиенте; десктоп это умеет, но логика заперта в методе интерфейса.
И выбирать не из чего: кэш моделей пуст по всем провайдерам, потому что
agy models нестабильна — в одном прогоне 40 секунд, в следующем висит
больше двух минут.
A19: два установщика. Windows — ярлык, открывающий веб окном приложения
через --app без адресной строки (проверено на машине владельца: Edge и
Chrome есть, окно открывается). Linux — скрипт установки, .desktop и
удаление с сохранением данных, плюс честная подсказка про проброс порта
при пустом DISPLAY.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец: «стоит опенкод аккаунт, который не подключен, у него кончились
лимиты и аккаунт не работает». Лимиты ни при чём.
Мастер подключения сохраняет ключ через ProfileAuthManager, а
_resolve_api_key смотрел только в auth_config из YAML и в переменные
окружения. Хранилище профилей он не читал вовсе — в отличие от grok,
claude и codex, где такая проверка есть.
Следствие: любой аккаунт OpenCode Go, подключённый через интерфейс, был
нерабочим. Маршрутизация падала с «No API key found for OpenCode Go
profile», и эта строка уже попадалась в следе отказов оркестратора.
Проверено на живом профиле владельца: до правки health_check=False и
тест профиля возвращал «локальный runtime недоступен», хотя api_key
лежал в auth.json. После — health_check=True, тест проходит.
Закрыто тестами, включая проверку, что пустое хранилище по-прежнему
даёт отказ, а не ложноположительный результат.
Тесты: 331 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Владелец запустил веб, увидел «Н/Д» у всех аккаунтов и сообщил, что
лимиты не подтягиваются. Через пятнадцать секунд всё появилось: опрос
провайдера просто ещё шёл.
Признак is_loading сервер отдавал (баз�овый снапшот выставляет его при
незавершённом опросе), но клиент его игнорировал и рисовал «Н/Д» — тот
же текст, что у подключённого аккаунта без лимитов. Два разных состояния
выглядели одинаково, и различить их было нельзя.
Теперь во время опроса ячейка показывает «Загрузка…» и «Опрашиваем
провайдера…» вместо прочерка.
Причина отказа важнее флага: если провайдер уже ответил «лимитов не
даю», состояние загрузки подавляется — иначе opencode-go и grok
показывали бы «Загрузка…» бесконечно.
Закреплено тестом в test_web_client_contract.py, включая проверку этого
подавления.
Тесты: 328 passed, ruff чисто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>