# NousResearch/hermes-agent · Gemini CLI sunset, Antigravity OAuth provider transition

## Open now

- Nothing open on GitHub.

These issues and PRs concern the deprecation and removal of the google-gemini-cli provider following the June 2025 Code Assist sunset, and ongoing efforts to add or replace it with an Antigravity/native OAuth provider for Gemini access. Multiple competing approaches and unmerged PRs reflect an unresolved transition.

## TL;DR

- 16 member(s): 0 open issue(s), 11 closed issue(s), 0 open PR(s), 1 merged PR(s), 4 PR(s) closed without merging.
- A fix was merged to main in PR #50454. Verify it addresses your case.
- **Recommended:** read PR #50454 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-50454`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.12`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #982: \[Feature\]: Using other LLM providers than Openrouter; using LLM providers' OAuth instead of API key
- #13260: \[Bug\]: Error: HTTP 404: Code Assist 404: Requested entity was not found.
- #29294: google-gemini-cli provider: Gemini CLI / Code Assist sunsets for consumer tiers on 2026-06-18
- #32291: \[Feature\]: Add Antigravity toolset / skill
- #44943: \[Feature\]: Add AntiGravity support to Hermes model before Gemini CLI deprecation
- #49099: \[Feature\]:Replace deprecated google-gemini-cli provider with antigravity-oauth (Cloud Code Assist 404)
- #49701: \[Bug\]: google-gemini-cli provider completely broken after Code Assist sunset (June 18) — PRs unmerged for 30+ days
- #49705: google-gemini-cli provider is dead since June 18 sunset — existing PRs unmerged for a month
- #50338: \[Feature\]: Please add antigravity cli support as gemini cli is going to be obselete
- #52657: \[Feature\]: Support Gemini Integration via Antigravity
- #54891: google-gemini-cli OAuth failed with 404/429 in early June, and now appears removed without a visible OAuth replacement

## Prior & competing PRs

- **#50454: fix(antigravity): native google-antigravity OAuth provider (Gemini CLI sunset)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/agent_runtime_helpers.py`, `agent/antigravity_cloudcode_adapter.py`, `agent/antigravity_code_assist.py`, `agent/antigravity_oauth.py`, `agent/gemini_cloudcode_adapter.py`, `agent/transports/chat_completions.py`, `hermes_cli/auth.py`, `hermes_cli/auth_commands.py`, `hermes_cli/config.py`, `hermes_cli/main.py`, `hermes_cli/model_setup_flows.py`, `hermes_cli/models.py`, `hermes_cli/providers.py`, `hermes_cli/runtime_provider.py`, `plugins/model-providers/gemini/__init__.py`, `scripts/release.py`, `tests/agent/test_antigravity_cloudcode.py`, `tests/agent/test_gemini_cloudcode.py`, `tests/agent/transports/test_chat_completions.py`, `tests/hermes_cli/test_model_provider_persistence.py` (+8 more)
  - fix hunks:
  - `agent/agent_runtime_helpers.py`

    ```diff
    @@ -1394,6 +1394,21 @@ def create_openai_client(agent, client_kwargs: dict, *, reason: str, shared: boo
                 agent._client_log_context(),
             )
             return client
    +    if agent.provider == "google-antigravity" or str(client_kwargs.get("base_url", "")).startswith("antigravity-pa://"):
    +        from agent.antigravity_cloudcode_adapter import AntigravityCloudCodeClient
    +
    +        safe_kwargs = {
    +            k: v for k, v in client_kwargs.items()
    +            if k in {"api_key", "base_url", "default_headers", "project_id", "timeout"}
    +        }
    +        client = AntigravityCloudCodeClient(**safe_kwargs)
    +        _ra().logger.info(
    +            "Antigravity Code Assist client created (%s, shared=%s) %s",
    +            reason,
    +            shared,
    +            agent._client_log_context(),
    +        )
    +        return client
         if agent.provider == "gemini":
             from agent.gemini_native_adapter import GeminiNativeClient, is_native_gemini_base_url
     
    ```
  - `agent/antigravity_cloudcode_adapter.py`

    ```diff
    @@ -0,0 +1,164 @@
    +"""OpenAI-compatible facade for Antigravity native OAuth inference."""
    +
    +from __future__ import annotations
    +
    +from typing import Any, Dict, Iterator, List, Optional
    +
    +import httpx
    +
    +from agent import antigravity_oauth
    +from agent.antigravity_code_assist import (
    +    ANTIGRAVITY_CODE_ASSIST_ENDPOINT,
    +    CodeAssistError,
    +    ProjectContext,
    +    build_headers,
    +    resolve_project_context,
    +)
    +from agent.gemini_cloudcode_adapter import (
    +    GeminiCloudCodeClient,
    +    _GeminiStreamChunk,
    +    _gemini_http_error,
    +    _iter_sse_events,
    +    _translate_gemini_response,
    +    _translate_stream_event,
    +    build_gemini_request,
    +    wrap_code_assist_request,
    +)
    +
    +MARKER_BASE_URL = "antigravity-pa://google"
    +
    +
    +class AntigravityCloudCodeClient(GeminiCloudCodeClient):
    +    """Minimal OpenAI-SDK-compatible facade over Antigravity Code Assist."""
    +
    +    def __init__(
    +        self,
    +        *,
    +        api_key: [redacted:secret-assignment] = None,
    +        base_url: Optional[str] = None,
    +        default_headers: Optional[Dict[str, str]] = None,
    +        project_id: str = "",
    +        **kwargs: Any,
    +    ):
    +        super().__init__(
    +            api_key=[redacted:secret-assignment] or "antigravity-oauth",
    +            base_url=base_url or MARKER_BASE_URL,
    +            default_headers=default_headers,
    +            project_id=project_id,
    +            **kwargs,
    +        )
    +
    +    def _ensure_project_context(self, access_token: str, model: str) -> ProjectContext:
    +        if self._project_context is not None:
    +            return self._project_context  # type: ignore[return-value]
    +
    +        env_project = antigravity_oauth.resolve_project_id_from_env()
    +        creds = antigravity_oauth.load_credentials()
    +        stored_project = creds.project_id if creds else ""
    +        if stored_project:
    +            self._project_context = ProjectContext(
    +                project_id=stored_project,
    +                managed_project_id=creds.managed_project_id if creds else "",
    +                source="stored",
    +            )
    +            return self._project_context
    +
    +        ctx = resolve_project_context(
    +            access_token,
    +            configured_project_id=self._configured_project_id,
    +            env_project_id=env_project,
    +        )
    +        if ctx.project_id or ctx.managed_project_id:
    +            antigravity_oauth.update_project_ids(
    +                project_id=ctx.project_id,
    +                managed_project_id=ctx.managed_project_id,
    +            )
    +        self._project_context = ctx
    +        return ctx
    +
    +    def _create_chat_completion(
    +        self,
    +        *,
    +        model: str = "gemini-3-flash-agent",
    +        messages: Optional[List[Dict[str, Any]]] = None,
    +        stream: bool = False,
    +        tools: Any = None,
    +        tool_choice: Any = None,
    +        temperature: Optional[float] = None,
    +        max_tokens: [redacted:secret-assignment] = None,
    +        top_p: Optional[float] = None,
    +        stop: Any = None,
    +        extra_body: Optional[Dict[str, Any]] = None,
    +        timeout: Any = None,
    +        **_: Any,
    +    ) -> Any:
    +        access_token = [redacted:secret-assignment]
    +        ctx = self._ensure_project_context(access_token, model)
    +
    +        thinking_config = None
    +        if isinstance(extra_body, dict):
    +            thinking_config = extra_body.get("thinking_config") or extra_body.get("thinkingConfig")
    +
    +        inner = build_gemini_request(
    +            messages=messages or [],
    +            tools=tools,
    +            tool_choice=tool_choice,
    +            temperature=temperature,
    +            max_tokens=[redacted:secret-assignment]
    +            top_p=top_p,
    +            stop=stop,
    +            thinking_config=thinking_config,
    +        )
    +        wrapped = wrap_code_assist_request(
    +            project_id=ctx.project_id,
    +            model=model,
    +            inner_request=inner,
    +        )
    +
    +        headers = build_headers(access_token)
    +        headers.update(self._default_headers)
    +
    +        if stream:
    +            return self._stream_completion(model=model, wrapped=wrapped, headers=headers)
    +
    +        url = f"{ANTIGRAVITY_CODE_ASSIST_ENDPOINT}/v1internal:generateContent"
    +        response = self._http.post(url, json=wrapped, headers=headers)
    +        if response.status_code != 200:
    +            raise _gemini_http_error(response)
    +        try:
    +            payload = response.json()
    +        except ValueError as exc:
    +            raise CodeAssistError(
    +                f"Invalid JSON from Antigravity Code Assist: {exc}",
    +                code="antigravity_code_assist_invalid_json",
    +            ) from exc
    +        return _translate_gemini_response(payload, model=model)
    +
    +    def _stream_completion(
    +        self,
    +        *,
    +        model: str,
    +        wrapped: Dict[str, Any],
    +        headers: Dict[str, str],
    +    ) -> Iterator[_GeminiStreamChunk]:
    +        url = f"{ANTIGRAVITY_CODE_ASSIST_ENDPOINT}/v1internal:streamGenerateContent?alt=sse"
    +        stream_headers = dict(headers)
    +        stream_headers["Accept"] = "text/event-stream"
    +
    +        def _generator() -> Iterator[_GeminiStreamChunk]:
    +            try:
    +                with self._http.stream("POST", url, json=wrapped, headers=stream_headers) as response:
    +                    if response.status_code != 200:
    +                        response.read()
    +                        raise _gemini_http_error(response)
    +                    tool_call_counter: List[int] = [0]
    +                    for event in _iter_sse_events(response):
    +                        for chunk in _translate_stream_event(event, model, tool_call_counter):
    +                            yield chunk
    +            except httpx.HTTPError as exc:
    +                raise CodeAssistError(
    +                    f"Antigravity streaming request failed: {exc}",
    +                    code="antigravity_code_assist_stream_error",
    +                ) from exc
    +
    +        return _generator()
    ```
  - `agent/antigravity_code_assist.py`

    ```diff
    @@ -0,0 +1,286 @@
    +"""Antigravity Code Assist control-plane helpers.
    +
    +The new Antigravity CLI uses the same v1internal Code Assist family as
    +gemini-cli, but with Antigravity OAuth scopes, metadata and model catalog. This
    +module keeps that provider-specific surface separate from
    +``agent.google_code_assist``.
    +"""
    +
    +from __future__ import annotations
    +
    +import json
    +import logging
    +import urllib.error
    +import urllib.request
    +import uuid
    +from dataclasses import dataclass, field
    +from typing import Any, Dict, Iterable, List, Optional
    +
    +from agent.google_code_assist import CodeAssistError
    +
    +logger = logging.getLogger(__name__)
    +
    +ANTIGRAVITY_CODE_ASSIST_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com"
    +ANTIGRAVITY_MODEL_ENDPOINTS = [
    +    ANTIGRAVITY_CODE_ASSIST_ENDPOINT,
    +    "https://cloudcode-pa.googleapis.com",
    +    "https://autopush-cloudcode-pa.sandbox.googleapis.com",
    +]
    +
    +ANTIGRAVITY_CLIENT_METADATA = {
    +    "ideType": "ANTIGRAVITY",
    +    "platform": "PLATFORM_UNSPECIFIED",
    +    "pluginType": "GEMINI",
    +}
    +ANTIGRAVITY_USER_AGENT = "antigravity/1.0.0 windows/amd64"
    +ANTIGRAVITY_X_GOOG_API_CLIENT = "google-cloud-sdk vscode_cloudshelleditor/0.1"
    +
    +DEFAULT_AGENT_MODEL_IDS = [
    +    "gemini-3-flash-agent",
    +    "gemini-3.5-flash-low",
    +    "gemini-pro-agent",
    +    "gemini-3.1-pro-low",
    +    "claude-sonnet-4-6",
    +    "claude-opus-4-6-thinking",
    +    "gpt-oss-120b-medium",
    +]
    +
    +DEPRECATED_MODEL_REPLACEMENTS = {
    +    "gemini-3.1-pro-high": "gemini-pro-agent",
    +}
    +
    +
    +@dataclass
    +class AntigravityProjectInfo:
    +    project_id: str = ""
    +    raw: Dict[str, Any] = field(default_factory=dict)
    +
    +
    +@dataclass
    +class ProjectContext:
    +    project_id: str = ""
    +    managed_project_id: str = ""
    +    tier_id: str = ""
    +    source: str = ""
    +
    +
    +def _client_metadata() -> Dict[str, str]:
    +    return dict(ANTIGRAVITY_CLIENT_METADATA)
    +
    +
    +def build_headers(access_token: str, *, accept: str = "application/json") -> Dict[str, str]:
    +    return {
    +        "Content-Type": "application/json",
    +        "Accept": accept,
    +        "Authorization": f"Bearer {access_token}",
    +        "User-Agent": ANTIGRAVITY_USER_AGENT,
    +        "X-Goog-Api-Client": ANTIGRAVITY_X_GOOG_API_CLIENT,
    +        "Client-Metadata": json.dumps(_client_metadata(), separators=(",", ":")),
    +        "x-activity-request-id": str(uuid.uuid4()),
    +    }
    +
    +
    +def _post_json(
    +    url: str,
    +    body: Dict[str, Any],
    +    access_token: str,
    +    *,
    +    timeout: float = 30.0,
    +) -> Dict[str, Any]:
    +    data = json.dumps(body).encode("utf-8")
    +    request = urllib.request.Request(
    +        url,
    +        data=data,
    +        method="POST",
    +        headers=build_headers(access_token),
    +    )
    +    try:
    +        with urllib.request.urlopen(request, timeout=timeout) as response:
    +            raw = response.read().decode("utf-8", errors="replace")
    +            return json.loads(raw) if raw else {}
    +    except urllib.error.HTTPError as exc:
    +        detail = ""
    +        try:
    +            detail = exc.read().decode("utf-8", errors="replace")
    +        except Exception:
    +            pass
    +        raise CodeAssistError(
    +            f"Antigravity Code Assist HTTP {exc.code}: {detail or exc.reason}",
    +            code=f"antigravity_code_assist_http_{exc.code}",
    +        ) from exc
    +    except urllib.error.URLError as exc:
    +        raise CodeAssistError(
    +            f"Antigravity Code Assist request failed: {exc}",
    +            code="antigravity_code_assist_network_error",
    +        ) from exc
    +
    +
    +def load_code_assist(
    +    access_token: str,
    +    *,
    +    project_id: str = "",
    +    endpoint: str = ANTIGRAVITY_CODE_ASSIST_ENDPOINT,
    +) -> AntigravityProjectInfo:
    +    metadata = _client_metadata()
    +    if project_id:
    +        metadata["duetProject"] = project_id
    +    body: Dict[str, Any] = {"metadata": metadata}
    +    if project_id:
    +        body["cloudaicompanionProject"] = project_id
    +    resp = _post_json(f"{endpoint}/v1internal:loadCodeAssist", body, access_token)
    +    project = (
    +        str(resp.get("cloudaicompanionProject") or "").strip()
    +        or str(resp.get("project") or "").strip()
    +    )
    +    return AntigravityProjectInfo(project_id=project, raw=resp)
    +
    +
    +def resolve_project_context(
    +    access_token: str,
    +    *,
    +    configured_project_id: str = "",
    +    env_project_id: str = "",
    +) -> ProjectContext:
    +    if configured_project_id:
    +        return ProjectContext(project_id=configured_project_id, source="config")
    +    if env_project_id:
    +        return ProjectContext(project_id=env_project_id, source="env")
    +    info = load_code_assist(access_token)
    +    if info.project_id:
    +        return ProjectContext(
    +            project_id=info.project_id,
    +            managed_project_id=info.project_id,
    +            source="discovered",
    +        )
    +    # Discovery returned no project (common on fresh consumer accounts that
    +    # haven't been onboarded). Fall back to the public default project so the
    +    # call chain still succeeds — mirrors the Antigravity CLI reference flow.
    +    from agent.antigravity_oauth import DEFAULT_PROJECT_ID
    +    return ProjectContext(
    +        project_id=DEFAULT_PROJECT_ID,
    +        managed_project_id=DEFAULT_PROJECT_ID,
    +        source="default",
    +    )
    +
    +
    +def fetch_available_models(
    +    access_token: str,
    +    *,
    +    project_id: str = "",
    +    endpoint: str = ANTIGRAVITY_CODE_ASSIST_ENDPOINT,
    +) -> Dict[str, Any]:
    +    body: Dict[str, Any] = {}
    +    if project_id:
    +        body["project"] = project_id
    +    return _post_json(f"{endpoint}/v1internal:fetchAvailableModels", body, access_token)
    +
    +
    +def fetch_available_models_with_fallbacks(
    +    access_token: str,
    +    *,
    +    project_id: str = "",
    +    endpoints: Optional[Iterable[str]] = None,
    +) -> Dict[str, Any]:
    +    last_err: Optional[Exception] = None
    +    for endpoint in endpoints or ANTIGRAVITY_MODEL_ENDPOINTS:
    +        try:
    +            return fetch_available_models(
    +                access_token,
    +                project_id=project_id,
    +                endpoint=endpoint,
    +            )
    +        except Exception as exc:
    +            last_err = exc
    +            logger.debug("Antigravity fetchAvailableModels failed on %s: %s", endpoint, exc)
    +    if last_err:
    +        raise last_err
    +    return {}
    +
    +
    +def _model_id_from_value(value: Any) -> str:
    +    if isinstance(value, str):
    +        return value.strip()
    +    if isinstance(value, dict):
    +        for key in ("modelId", "model_id", "id", "name"):
    +            candidate = str(value.get(key) or "").strip()
    +            if candidate:
    +                return candidate
    +    return ""
    +
    +
    +def _ids_from_sort(sort: Dict[str, Any]) -> List[str]:
    +    ids: List[str] = []
    +    for key in ("modelIds", "model_ids", "models", "modelSorts"):
    +        value = sort.get(key)
    +        if isinstance(value, list):
    +            for item in value:
    +                mid = _model_id_from_value(item)
    +                if mid:
    +                    ids.append(mid)
    +        elif isinstance(value, dict):
    +            mid = _model_id_from_value(value)
    +            if mid:
    +                ids.append(mid)
    +    return ids
    +
    +
    +def _is_recommended_sort(sort: Dict[str, Any]) -> bool:
    +    label = " ".join(
    +        str(sort.get(key) or "")
    +        for key in ("name", "displayName", "title", "category", "group")
    +    ).lower()
    +    return "recommended" in label
    +
    +
    +def _raw_model_ids(payload: Dict[str, Any]) -> List[str]:
    +    ids: List[str] = []
    +    models = payload.get("models")
    +    if isinstance(models, list):
    +        for item in models:
    +            mid = _model_id_from_value(item)
    +            if mid:
    +                ids.append(mid)
    +    return ids
    +
    +
    +def filter_agent_model_ids(ids: Iterable[str]) -> List[str]:
    +    seen: set[str] = set()
    +    filtered: List[str] = []
    +    raw = [str(mid).strip() for mid in ids if str(mid).strip()]
    +    replacements = set(DEPRECATED_MODEL_REPLACEMENTS.values())
    +    for mid in raw:
    +        if mid in seen:
    +            continue
    +        if mid.startswith(("chat_", "tab_")):
    +            continue
    +        if mid in DEPRECATED_MODEL_REPLACEMENTS and DEPRECATED_MODEL_REPLACEMENTS[mid] in raw:
    +            continue
    +        if mid in replacements and mid in seen:
    +            continue
    +        seen.add(mid)
    +        filtered.append(mid)
    +    return filtered
    +
    +
    +def parse_agent_model_ids(payload: Dict[str, Any]) -> List[str]:
    +    """Return the user-facing Antigravity agent model list in display order."""
    +    sorts = payload.get("agentModelSorts")
    +    ordered: List[str] = []
    +    if isinstance(sorts, list):
    +        recommended = [s for s in sorts if isinstance(s, dict) and _is_recommended_sort(s)]
    +        rest = [s for s in sorts if isinstance(s, dict) and not _is_recommended_sort(s)]
    +        for sort in recommended + rest:
    +            ordered.extend(_ids_from_sort(sort))
    +
    +    if not ordered:
    +        default_id = str(payload.get("defaultAgentModelId") or "").strip()
    +        if default_id:
    +            ordered.append(default_id)
    +        for mid in DEFAULT_AGENT_MODEL_IDS:
    +            ordered.append(mid)
    +        ordered.extend(_raw_model_ids(payload))
    +
    +    filtered = filter_agent_model_ids(ordered)
    +    if filtered:
    +        return filtered
    +    return list(DEFAULT_AGENT_MODEL_IDS)
    ```
  - `agent/antigravity_oauth.py`

    ```diff
    @@ -0,0 +1,907 @@
    +"""Google OAuth PKCE flow for the Antigravity (google-antigravity) provider.
    +
    +Tokens are stored separately from the existing ``google-gemini-cli`` provider so
    +development and production credentials do not accidentally bleed across:
    +
    +    ~/.hermes/auth/antigravity_oauth.json
    +
    +The on-disk schema matches ``agent.google_oauth`` so the runtime resolver can
    +share the same refresh/project-id packing convention.
    +"""
    +
    +from __future__ import annotations
    +
    +import base64
    +import contextlib
    +import hashlib
    +import http.server
    +import json
    +import logging
    +import os
    +import re
    +import secrets
    +import shutil
    +import stat
    +import threading
    +import time
    +import urllib.error
    +import urllib.parse
    +import urllib.request
    +import webbrowser
    +from dataclasses import dataclass
    +from pathlib import Path
    +from typing import Any, Dict, Optional, Tuple
    +
    +from hermes_constants import get_hermes_home
    +from utils import atomic_replace
    +
    +logger = logging.getLogger(__name__)
    +
    +ENV_CLIENT_ID = "HERMES_ANTIGRAVITY_CLIENT_ID"
    +ENV_CLIENT_SECRET = "[redacted:secret-assignment]"
    +ENV_CLI_PATH = "HERMES_ANTIGRAVITY_CLI_PATH"
    +
    +# Public Antigravity CLI desktop OAuth client. Like Google's gemini-cli
    +# credentials (see agent/google_oauth.py), this is a DESKTOP OAuth client and
    +# its "secret" is not confidential — installed-app clients have no
    +# secret-keeping requirement (PKCE provides the security), and these creds are
    +# baked into every copy of the Antigravity CLI. Shipping them as a fallback
    +# lets users without `agy` installed authenticate directly. Split into parts
    +# with explicit comments per the convention in google_oauth.py.
    +_PUBLIC_CLIENT_ID_PROJECT_NUM = "1071006060591"
    +_PUBLIC_CLIENT_ID_HASH = "tmhssin2h21lcre235vtolojh4g403ep"
    +_PUBLIC_CLIENT_SECRET_SUFFIX = "[redacted:secret-assignment]"
    +
    +_DEFAULT_CLIENT_ID = (
    +    f"{_PUBLIC_CLIENT_ID_PROJECT_NUM}-{_PUBLIC_CLIENT_ID_HASH}"
    +    ".apps.googleusercontent.com"
    +)
    +_DEFAULT_CLIENT_SECRET = f"GOCSPX-{_PUBLIC_CLIENT_SECRET_SUFFIX}"
    +
    +# Fallback project ID when Code Assist project discovery fails entirely.
    +DEFAULT_PROJECT_ID = "rising-fact-p41fc"
    +
    +_CLIENT_ID_PATTERN = re.compile(
    +    r"([0-9]{8,}-[a-z0-9]{20,}\.apps\.googleusercontent\.com)"
    +)
    +_CLIENT_SECRET_PATTERN = [redacted:secret-assignment]"(GOCSPX-[A-Za-z0-9_-]{20,80})")
    +_DISCOVERY_MAX_FILE_BYTES = 25 * 1024 * 1024
    +_DISCOVERY_MAX_AGY_BINARY_BYTES = 220 * 1024 * 1024
    +_DISCOVERY_MAX_FILES = 600
    +_DISCOVERY_EXTENSIONS = {
    +    "",
    +    ".cjs",
    +    ".exe",
    +    ".js",
    +    ".json",
    +    ".mjs",
    +    ".node",
    +    ".ts",
    +}
    +_DISCOVERY_SKIP_DIRS = {
    +    ".system_generated",
    +    "brain",
    +    "conversations",
    +    "log",
    +    "logs",
    +    "scratch",
    +}
    +
    +AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth"
    +TOKEN_ENDPOINT = "[redacted:secret-assignment]"
    +USERINFO_ENDPOINT = "https://www.googleapis.com/oauth2/v1/userinfo"
    +
    +OAUTH_SCOPES = (
    +    "https://www.googleapis.com/auth/cloud-platform "
    +    "https://www.googleapis.com/auth/userinfo.email "
    +    "https://www.googleapis.com/auth/userinfo.profile "
    +    "https://www.googleapis.com/auth/cclog "
    +    "https://www.googleapis.com/auth/experimentsandconfigs"
    +)
    +
    +DEFAULT_REDIRECT_PORT = 51121
    +REDIRECT_HOST = "localhost"
    +CALLBACK_PATH = "/oauth-callback"
    +REFRESH_SKEW_SECONDS = 60
    +TOKEN_REQUEST_TIMEOUT_SECONDS = 20.0
    +CALLBACK_WAIT_SECONDS = 300
    +LOCK_TIMEOUT_SECONDS = 30.0
    +
    +
    +class AntigravityOAuthError(RuntimeError):
    +    def __init__(self, message: str, *, code: str = "antigravity_oauth_error") -> None:
    +        super().__init__(message)
    +        self.code = code
    +
    +
    +def _credentials_path() -> Path:
    +    return get_hermes_home() / "auth" / "antigravity_oauth.json"
    +
    +
    +def _lock_path() -> Path:
    +    return _credentials_path().with_suffix(".json.lock")
    +
    +
    +_lock_state = threading.local()
    +
    +
    [redacted:email]
    +def _credentials_lock(timeout_seconds: float = LOCK_TIMEOUT_SECONDS):
    +    depth = getattr(_lock_state, "depth", 0)
    +    if depth > 0:
    +        _lock_state.depth = depth + 1
    +        try:
    +            yield
    +        finally:
    +            _lock_state.depth -= 1
    +        return
    +
    +    lock_file_path = _lock_path()
    +    lock_file_path.parent.mkdir(parents=True, exist_ok=True)
    +    fd = os.open(str(lock_file_path), os.O_CREAT | os.O_RDWR, 0o600)
    +    acquired = False
    +    try:
    +        try:
    +            import fcntl
    +        except ImportError:
    +            fcntl = None
    +
    +        if fcntl is not None:
    +            deadline = time.monotonic() + max(0.0, float(timeout_seconds))
    +            while True:
    +                try:
    +                    fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
    +                    acquired = True
    +                    break
    +                except BlockingIOError:
    +                    if time.monotonic() >= deadline:
    +                        raise TimeoutError(
    +                            f"Timed out acquiring Antigravity OAuth credentials lock at {lock_file_path}."
    +                        )
    +                    time.sleep(0.05)
    +        else:
    +            try:
    +                import msvcrt  # type: ignore[import-not-found]
    +
    +                deadline = time.monotonic() + max(0.0, float(timeout_seconds))
    +                while True:
    +                    try:
    +                        msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
    +                        acquired = True
    +                        break
    +                    except OSError:
    +                        if time.monotonic() >= deadline:
    +                            raise TimeoutError(
    +                                f"Timed out acquiring Antigravity OAuth credentials lock at {lock_file_path}."
    +                            )
    +                        time.sleep(0.05)
    +            except ImportError:
    +                acquired = True
    +
    +        _lock_state.depth = 1
    +        yield
    +    finally:
    +        try:
    +            if acquired:
    +                try:
    +                    import fcntl
    +
    +                    fcntl.flock(fd, fcntl.LOCK_UN)
    +                except ImportError:
    +                    try:
    +                        import msvcrt  # type: ignore[import-not-found]
    +
    +                        try:
    +                            msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
    +                        except OSError:
    +                            pass
    +                    except ImportError:
    +                        pass
    +        finally:
    +            os.close(fd)
    +            _lock_state.depth = 0
    +
    +
    +_discovered_creds_cache: Dict[str, Any] = {}
    +
    +
    +def _secret_candidates(raw: str) -> list[str]:
    +    candidates: list[str] = []
    +    for length in (35, 34, 36, 33, 37, 38, 39, 40, 41, 42):
    +        if len(raw) >= length:
    +            candidates.append(raw[:length])
    +    candidates.append(raw)
    +    return list(dict.fromkeys(candidates))
    +
    +
    +def _candidate_discovery_roots() -> list[Path]:
    +    roots: list[Path] = []
    +
    +    explicit = (os.getenv(ENV_CLI_PATH) or "").strip()
    +    if explicit:
    +        roots.append(Path(explicit))
    +
    +    for command in ("agy", "agy.exe", "antigravity", "antigravity.exe"):
    +        found = shutil.which(command)
    +        if found:
    +            roots.append(Path(found))
    +
    +    for env_key in ("LOCALAPPDATA", "APPDATA", "ProgramFiles", "ProgramFiles(x86)"):
    +        base = os.getenv(env_key)
    +        if not base:
    +            continue
    +        base_path = Path(base)
    +        roots.extend((
    +            base_path / "agy",
    +            base_path / "agy" / "bin" / "agy.exe",
    +            base_path / "Programs" / "Antigravity",
    +            base_path / "Programs" / "Antigravity CLI",
    +            base_path / "Google" / "Antigravity",
    +            base_path / "Google" / "Antigravity CLI",
    +        ))
    +
    +    home = Path.home()
    +    for root in (
    +        home / ".gemini" / "antigravity-cli",
    +        home / ".antigravitycli",
    +        home / ".antigravity",
    +    ):
    +        roots.append(root)
    +
    +    unique: list[Path] = []
    +    seen: set[str] = set()
    +    for root in roots:
    +        try:
    +            key = str(root.expanduser().resolve())
    +        except OSError:
    +            key = str(root.expanduser())
    +        if key not in seen:
    +            seen.add(key)
    +            unique.append(root)
    +    return unique
    +
    +
    +def _iter_discovery_files() -> list[Path]:
    +    files: list[Path] = []
    +    seen: set[str] = set()
    +
    +    def add(path: Path) -> None:
    +        if len(files) >= _DISCOVERY_MAX_FILES:
    +            return
    +        if path.suffix.lower() not in _DISCOVERY_EXTENSIONS:
    +            return
    +        try:
    +            stat_info = path.stat()
    +            max_bytes = (
    +                _DISCOVERY_MAX_AGY_BINARY_BYTES
    +                if path.name.lower() in {"agy", "agy.exe", "antigravity", "antigravity.exe"}
    +                else _DISCOVERY_MAX_FILE_BYTES
    +            )
    +            if not path.is_file() or stat_info.st_size > max_bytes:
    +                return
    +            key = str(path.resolve())
    +        except OSError:
    +            return
    +        if key in seen:
    +            return
    +        seen.add(key)
    +        files.append(path)
    +
    +    for root in _candidate_discovery_roots():
    +        if len(files) >= _DISCOVERY_MAX_FILES:
    +            break
    +        try:
    +            if root.is_file():
    +                add(root)
    +                continue
    +            if not root.is_dir():
    +                continue
    +        except OSError:
    +            continue
    +
    +        for dirpath, dirnames, filenames in os.walk(root):
    +            dirnames[:] = [
    +                d for d in dirnames
    +                if d not in _DISCOVERY_SKIP_DIRS and not d.startswith(".git")
    +            ]
    +            for filename in filenames:
    +                add(Path(dirpath) / filename)
    +                if len(files) >= _DISCOVERY_MAX_FILES:
    +                    break
    +            if len(files) >= _DISCOVERY_MAX_FILES:
    +                break
    +    return files
    +
    +
    +def _extract_client_credential_candidates_from_text(content: str) -> list[Tuple[str, str]]:
    +    client_ids = list(dict.fromkeys(match.group(1) for match in _CLIENT_ID_PATTERN.finditer(content)))
    +    secrets: [redacted:secret-assignment] = []
    +    for match in _CLIENT_SECRET_PATTERN.finditer(content):
    +        secrets.extend(_secret_candidates(match.group(1)))
    +    secrets = [redacted:secret-assignment]
    +    return [(client_id, secret) for client_id in client_ids for secret in secrets]
    +
    +
    +def _discover_client_credentials() -> Tuple[str, str]:
    +    if _discovered_creds_cache.get("resolved"):
    +        return (
    +            _discovered_creds_cache.get("client_id", ""),
    +            _discovered_creds_cache.get("client_secret", ""),
    +        )
    +
    +    for path in _iter_discovery_files():
    +        try:
    +            content = path.read_bytes().decode("utf-8", errors="ignore")
    +        except OSError:
    +            continue
    +        candidates = _extract_client_credential_candidates_from_text(content)
    +        if candidates:
    +            client_id, client_secret = [redacted:secret-assignment]
    +            _discovered_creds_cache.update({
    +                "client_id": client_id,
    +                "client_secret": client_secret,
    +                "candidates": candidates,
    +                "resolved": "1",
    +            })
    +            logger.info("Discovered Antigravity OAuth client credentials from %s", path)
    +            return client_id, client_secret
    +
    +    _discovered_creds_cache["resolved"] = "1"
    +    return "", ""
    +
    +
    +def _get_client_id() -> str:
    +    env_val = (os.getenv(ENV_CLIENT_ID) or "").strip()
    +    if env_val:
    +        return env_val
    +    discovered, _ = _discover_client_credentials()
    +    if discovered:
    +        return discovered
    +    return _DEFAULT_CLIENT_ID
    +
    +
    +def _get_client_secret() -> str:
    +    env_val = (os.getenv(ENV_CLIENT_SECRET) or "").strip()
    +    if env_val:
    +        return env_val
    +    _, discovered = _discover_client_credentials()
    +    if discovered:
    +        return discovered
    +    return _DEFAULT_CLIENT_SECRET
    +
    +
    +def _iter_client_credential_candidates() -> list[Tuple[str, str]]:
    +    env_id = (os.getenv(ENV_CLIENT_ID) or "").strip()
    +    env_secret = [redacted:secret-assignment] or "").strip()
    +    if env_id and env_secret:
    +        return [(env_id, env_secret)]
    +
    +    _discover_client_credentials()
    +    cached = _discovered_creds_cache.get("candidates")
    +    candidates: list[Tuple[str, str]] = []
    +    if isinstance(cached, list):
    +        candidates = [
    +            (str(client_id), str(client_secret))
    +            for client_id, client_secret in cached
    +            if client_id and client_secret
    +        ]
    +    else:
    +        client_id = str(_discovered_creds_cache.get("client_id") or "")
    +        client_secret = [redacted:secret-assignment]"client_secret") or "")
    +        if client_id and client_secret:
    +            candidates = [(client_id, client_secret)]
    +
    +    # Always include the public baked-in default as a last-resort candidate so
    +    # users without `agy` installed can still authenticate. De-dupe in case
    +    # discovery already surfaced the same client.
    +    default_pair = (_DEFAULT_CLIENT_ID, _DEFAULT_CLIENT_SECRET)
    +    if default_pair not in candidates:
    +        candidates.append(default_pair)
    +    return candidates
    +
    +
    +def _require_client_id() -> str:
    +    client_id = _get_client_id()
    +    if not client_id:
    +        raise AntigravityOAuthError(
    +            "Antigravity OAuth client ID is not available. Install Antigravity CLI "
    +            "so Hermes can discover its desktop OAuth client, set "
    +            f"{ENV_CLI_PATH} to the agy executable, or set {ENV_CLIENT_ID} and "
    +            f"{ENV_CLIENT_SECRET} in ~/.hermes/.env.",
    +            code="antigravity_oauth_client_id_missing",
    +        )
    +    return client_id
    +
    +
    +def _require_client_secret() -> str:
    +    client_secret = [redacted:secret-assignment]
    +    if not client_secret:
    +        raise AntigravityOAuthError(
    +            "Antigravity OAuth client secret is not available. Install Antigravity CLI "
    +            "so Hermes can discover its desktop OAuth client, set "
    +            f"{ENV_CLI_PATH} to the agy executable, or set {ENV_CLIENT_ID} and "
    +            f"{ENV_CLIENT_SECRET} in ~/.hermes/.env.",
    +            code="antigravity_oauth_client_secret_missing",
    +        )
    +    return client_secret
    +
    +
    +def _require_client_credentials() -> Tuple[str, str]:
    +    candidates = _iter_client_credential_candidates()
    +    if not candidates:
    +        _require_client_id()
    +        _require_client_secret()
    +    return candidates[0]
    +
    +
    +def _generate_pkce_pair() -> Tuple[str, str]:
    +    verifier = secrets.token_urlsafe(64)
    +    digest = hashlib.sha256(verifier.encode("ascii")).digest()
    +    challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
    +    return verifier, challenge
    +
    +
    +@dataclass
    +class RefreshParts:
    +    refresh_token: str
    +    project_id: str = ""
    +    managed_project_id: str = ""
    +
    +    @classmethod
    +    def parse(cls, packed: str) -> "RefreshParts":
    +        if not packed:
    +            return cls(refresh_token="")
    +        parts = packed.split("|", 2)
    +        return cls(
    +            refresh_token=[redacted:secret-assignment]
    +            project_id=parts[1] if len(parts) > 1 else "",
    +            managed_project_id=parts[2] if len(parts) > 2 else "",
    +        )
    +
    +    def format(self) -> str:
    +        if not self.refresh_token:
    +            return ""
    +        if not self.project_id and not self.managed_project_id:
    +            return self.refresh_token
    +        return f"{self.refresh_token}|{self.project_id}|{self.managed_project_id}"
    +
    +
    +@dataclass
    +class AntigravityCredentials:
    +    access_token: str
    +    refresh_token: str
    +    expires_ms: int
    +    email: str = ""
    +    project_id: str = ""
    +    managed_project_id: str = ""
    +
    +    def to_dict(self) -> Dict[str, Any]:
    +        return {
    +            "refresh": RefreshParts(
    +                refresh_token=[redacted:secret-assignment]
    +                project_id=self.project_id,
    +                managed_project_id=self.managed_project_id,
    +            ).format(),
    +            "access": self.access_token,
    +            "expires": int(self.expires_ms),
    +            "email": self.email,
    +        }
    +
    +    @classmethod
    +    def from_dict(cls, data: Dict[str, Any]) -> "AntigravityCredentials":
    +        parts = RefreshParts.parse(str(data.get("refresh", "") or ""))
    +        return cls(
    +            access_token=[redacted:secret-assignment]"access", "") or ""),
    +            refresh_token=[redacted:secret-assignment]
    +            expires_ms=int(data.get("expires", 0) or 0),
    +            email=str(data.get("email", "") or ""),
    +            project_id=parts.project_id,
    +            managed_project_id=parts.managed_project_id,
    +        )
    +
    +    def access_token_expired(self, skew_seconds: int = REFRESH_SKEW_SECONDS) -> bool:
    +        if not self.access_token or not self.expires_ms:
    +            return True
    +        return (time.time() + max(0, skew_seconds)) * 1000 >= self.expires_ms
    +
    +
    +def load_credentials() -> Optional[AntigravityCredentials]:
    +    path = _credentials_path()
    +    if not path.exists():
    +        return None
    +    try:
    +        with _credentials_lock():
    +            raw = path.read_text(encoding="utf-8")
    +        data = json.loads(raw)
    +    except (json.JSONDecodeError, OSError, IOError) as exc:
    +        logger.warning("Failed to read Antigravity OAuth credentials at %s: %s", path, exc)
    +        return None
    +    if not isinstance(data, dict):
    +        return None
    +    creds = AntigravityCredentials.from_dict(data)
    +    if not creds.access_token:
    +        return None
    +    return creds
    +
    +
    +def save_credentials(creds: AntigravityCredentials) -> Path:
    +    path = _credentials_path()
    +    path.parent.mkdir(parents=True, exist_ok=True)
    +    try:
    +        os.chmod(path.parent, 0o700)
    +    except OSError:
    +        pass
    +    payload = json.dumps(creds.to_dict(), indent=2, sort_keys=True) + "\n"
    +    with _credentials_lock():
    +        tmp_path = path.with_suffix(f".tmp.{os.getpid()}.{secrets.token_hex(4)}")
    +        try:
    +            fd = os.open(
    +                str(tmp_path),
    +                os.O_WRONLY | os.O_CREAT | os.O_EXCL,
    +                stat.S_IRUSR | stat.S_IWUSR,
    +            )
    +            with os.fdopen(fd, "w", encoding="utf-8") as fh:
    +                fh.write(payload)
    +                fh.flush()
    +                os.fsync(fh.fileno())
    +            atomic_replace(tmp_path, path)
    +        finally:
    +            try:
    +                if tmp_path.exists():
    +                    tmp_path.unlink()
    +            except OSError:
    +                pass
    +    return path
    +
    +
    +def clear_credentials() -> None:
    +    path = _credentials_path()
    +    with _credentials_lock():
    +        try:
    +            path.unlink()
    +        except FileNotFoundError:
    +            pass
    +        except OSError as exc:
    +            logger.warning("Failed to remove Antigravity OAuth credentials at %s: %s", path, exc)
    +
    +
    +def _post_form(url: str, data: Dict[str, str], timeout: float) -> Dict[str, Any]:
    +    body = urllib.parse.urlencode(data).encode("ascii")
    +    request = urllib.request.Request(
    +        url,
    +        data=body,
    +        method="POST",
    +        headers={
    +            "Content-Type": "application/x-www-form-urlencoded",
    +            "Accept": "application/json",
    +        },
    +    )
    +    try:
    +        with urllib.request.urlopen(request, timeout=timeout) as response:
    +            raw = response.read().decode("utf-8", errors="replace")
    +            return json.loads(raw)
    +    except urllib.error.HTTPError as exc:
    +        detail = ""
    +        try:
    +            detail = exc.read().decode("utf-8", errors="replace")
    +        except Exception:
    +            pass
    +        code = "antigravity_oauth_token_http_error"
    +        if "invalid_grant" in detail.lower():
    +            code = "antigravity_oauth_invalid_grant"
    +        elif "invalid_client" in detail.lower():
    +            code = "antigravity_oauth_invalid_client"
    +        raise AntigravityOAuthError(
    +            f"Antigravity OAuth token endpoint returned HTTP {exc.code}: {detail or exc.reason}",
    +            code=code,
    +        ) from exc
    +    except urllib.error.URLError as exc:
    +        raise AntigravityOAuthError(
    +            f"Antigravity OAuth token request failed: {exc}",
    +            code="antigravity_oauth_token_network_error",
    +        ) from exc
    +
    +
    +def exchange_code(
    +    code: str,
    +    verifier: str,
    +    redirect_uri: str,
    +    *,
    +    timeout: float = TOKEN_REQUEST_TIMEOUT_SECONDS,
    +) -> Dict[str, Any]:
    +    last_error: Optional[AntigravityOAuthError] = None
    +    candidates = _iter_client_credential_candidates()
    +    if not candidates:
    +        candidates = [_require_client_credentials()]
    +    for client_id, client_secret in candidates:
    +        data = {
    +            "grant_type": "authorization_code",
    +            "code": code,
    +            "code_verifier": verifier,
    +            "client_id": client_id,
    +            "client_secret": client_secret,
    +            "redirect_uri": redirect_uri,
    +        }
    +        try:
    +            return _post_form(TOKEN_ENDPOINT, data, timeout)
    +        except AntigravityOAuthError as exc:
    +            last_error = exc
    +            if exc.code != "antigravity_oauth_invalid_client":
    +                raise
    +    if last_error is not None:
    +        raise last_error
    +    raise AntigravityOAuthError(
    +        "Antigravity OAuth client credentials are unavailable.",
    +        code="antigravity_oauth_client_missing",
    +    )
    +
    +
    +def refresh_access_token(
    +    refresh_token: str,
    +    *,
    +    timeout: float = TOKEN_REQUEST_TIMEOUT_SECONDS,
    +) -> Dict[str, Any]:
    +    if not refresh_token:
    +        raise AntigravityOAuthError(
    +            "Cannot refresh: refresh_token is empty. Re-run OAuth login.",
    +            code="antigravity_oauth_refresh_token_missing",
    +        )
    +    last_error: Optional[AntigravityOAuthError] = None
    +    candidates = _iter_client_credential_candidates()
    +    if not candidates:
    +        candidates = [_require_client_credentials()]
    +    for client_id, client_secret in candidates:
    +        data = {
    +            "grant_type": "refresh_token",
    +            "refresh_token": refresh_token,
    +            "client_id": client_id,
    +            "client_secret": client_secret,
    +        }
    +        try:
    +            return _post_form(TOKEN_ENDPOINT, data, timeout)
    +        except AntigravityOAuthError as exc:
    +            last_error = exc
    +            if exc.code not in {
    +                "antigravity_oauth_invalid_client",
    +                "antigravity_oauth_invalid_grant",
    +            }:
    +                raise
    +    if last_error is not None:
    +        raise last_error
    +    raise AntigravityOAuthError(
    +        "Antigravity OAuth client credentials are unavailable.",
    +        code="antigravity_oauth_client_missing",
    +    )
    +
    +
    +def _fetch_user_email(access_token: str, timeout: float = TOKEN_REQUEST_TIMEOUT_SECONDS) -> str:
    +    try:
    +        request = urllib.request.Request(
    +            USERINFO_ENDPOINT + "?alt=json",
    +            headers={"Authorization": f"Bearer {access_token}"},
    +        )
    +        with urllib.request.urlopen(request, timeout=timeout) as response:
    +            raw = response.read().decode("utf-8", errors="replace")
    +        data = json.loads(raw)
    +        return str(data.get("email", "") or "")
    +    except Exception as exc:
    +        logger.debug("Antigravity userinfo fetch failed (non-fatal): %s", exc)
    +        return ""
    +
    +
    +_refresh_inflight: Dict[str, threading.Event] = {}
    +_refresh_inflight_lock = threading.Lock()
    +
    +
    +def get_valid_access_token(*, force_refresh: bool = False) -> str:
    +    creds = load_credentials()
    +    if creds is None:
    +        raise AntigravityOAuthError(
    +            "No Antigravity OAuth credentials found. Run `hermes login --provider google-antigravity` first.",
    +            code="antigravity_oauth_not_logged_in",
    +        )
    +    if not force_refresh and not creds.access_token_expired():
    +        return creds.access_token
    +
    +    rt = creds.refresh_token
    +    with _refresh_inflight_lock:
    +        event = _refresh_inflight.get(rt)
    +        if event is None:
    +            event = threading.Event()
    +            _refresh_inflight[rt] = event
    +            owner = True
    +        else:
    +            owner = False
    +
    +    if not owner:
    +        event.wait(timeout=LOCK_TIMEOUT_SECONDS)
    +        fresh = load_credentials()
    +        if fresh is not None and not fresh.access_token_expired():
    +            return fresh.access_token
    +
    +    try:
    +        try:
    +            resp = refresh_access_token(rt)
    +        except AntigravityOAuthError as exc:
    +            if exc.code == "antigravity_oauth_invalid_grant":
    +                clear_credentials()
    +            raise
    +        new_access = str(resp.get("access_token", "") or "").strip()
    +        if not new_access:
    +            raise AntigravityOAuthError(
    +                "Refresh response did not include an access_token.",
    +                code="antigravity_oauth_refresh_empty",
    +            )
    +        creds.access_token = [redacted:secret-assignment]
    +        creds.refresh_token = [redacted:secret-assignment]"refresh_token", "") or "").strip() or creds.refresh_token
    +        expires_in = int(resp.get("expires_in", 0) or 0)
    +        creds.expires_ms = int((time.time() + max(60, expires_in)) * 1000)
    +        save_credentials(creds)
    +        return creds.access_token
    +    finally:
    +        if owner:
    +            with _refresh_inflight_lock:
    +                _refresh_inflight.pop(rt, None)
    +            event.set()
    +
    +
    +def update_project_ids(project_id: str = "", managed_project_id: str = "") -> None:
    +    creds = load_credentials()
    +    if creds is None:
    +        return
    +    if project_id:
    +        creds.project_id = project_id
    +    if managed_project_id:
    +        creds.managed_project_id = managed_project_id
    +    save_credentials(creds)
    +
    +
    +class _OAuthCallbackHandler(http.server.BaseHTTPRequestHandler):
    +    expected_state: str = ""
    +    captured_code: Optional[str] = None
    +    captured_error: Optional[str] = None
    +    ready: Optional[threading.Event] = None
    +
    +    def log_message(self, format: str, *args: Any) -> None:  # noqa: A002, N802
    +        logger.debug("Antigravity OAuth callback: " + format, *args)
    +
    +    def do_GET(self) -> None:  # noqa: N802
    +        parsed = urllib.parse.urlparse(self.path)
    +        if parsed.path != CALLBACK_PATH:
    +            self.send_response(404)
    +            self.end_headers()
    +            return
    +
    +        params = urllib.parse.parse_qs(parsed.query)
    +        state = (params.get("state") or [""])[0]
    +        error = (params.get("error") or [""])[0]
    +        code = (params.get("code") or [""])[0]
    +
    +        handler_cls = type(self)
    +        if state != self.expected_state:
    +            handler_cls.captured_error = "OAuth state mismatch."
    +        elif error:
    +            handler_cls.captured_error = error
    +        elif not code:
    +            handler_cls.captured_error = "OAuth callback did not include a code."
    +        else:
    +            handler_cls.captured_code = code
    +
    +        ok = not handler_cls.captured_error
    +        self.send_response(200 if ok else 400)
    +        self.send_header("Content-Type", "text/html; charset=utf-8")
    +        self.end_headers()
    +        msg = "Antigravity OAuth complete. You can return to Hermes." if ok else handler_cls.captured_error
    +        self.wfile.write(f"<html><body><p>{msg}</p></body></html>".encode("utf-8"))
    +        if handler_cls.ready is not None:
    +            handler_cls.ready.set()
    +
    +
    +class _ReusableHTTPServer(http.server.HTTPServer):
    +    allow_reuse_address = True
    +
    +
    +def resolve_project_id_from_env() -> str:
    +    for key in ("HERMES_ANTIGRAVITY_PROJECT_ID", "GOOGLE_CLOUD_PROJECT", "GOOGLE_CLOUD_PROJECT_ID"):
    +        value = (os.getenv(key) or "").strip()
    +        if value:
    +            return value
    +    return ""
    +
    +
    +def start_oauth_flow(
    +    *,
    +    force_relogin: bool = False,
    +    open_browser: bool = True,
    +    port: int = DEFAULT_REDIRECT_PORT,
    +    project_id: str = "",
    +) -> AntigravityCredentials:
    +    if not force_relogin:
    +        existing = load_credentials()
    +        if existing and not existing.access_token_expired():
    +            return existing
    +
    +    verifier, challenge = _generate_pkce_pair()
    +    state = secrets.token_urlsafe(24)
    +    client_id, _ = _require_client_credentials()
    +
    +    ready = threading.Event()
    +    handler_cls = type("AntigravityOAuthCallbackHandler", (_OAuthCallbackHandler,), {})
    +    handler_cls.expected_state = state
    +    handler_cls.captured_code = None
    +    handler_cls.captured_error = None
    +    handler_cls.ready = ready
    +
    +    try:
    +        server = _ReusableHTTPServer((REDIRECT_HOST, int(port)), handler_cls)
    +    except OSError:
    +        server = _ReusableHTTPServer((REDIRECT_HOST, 0), handler_cls)
    +    actual_port = int(server.server_address[1])
    +    redirect_uri = f"http://{REDIRECT_HOST}:{actual_port}{CALLBACK_PATH}"
    +
    +    thread = threading.Thread(target=server.serve_forever, daemon=True)
    +    thread.start()
    +    try:
    +        params = {
    +            "client_id": client_id,
    +            "redirect_uri": redirect_uri,
    +            "response_type": "code",
    +            "scope": OAUTH_SCOPES,
    +            "access_type": "offline",
    +            "prompt": "consent",
    +            "state": state,
    +            "code_challenge": challenge,
    +            "code_challenge_method": "S256",
    +        }
    +        auth_url = AUTH_ENDPOINT + "?" + urllib.parse.urlencode(params)
    +        print("Open this URL to authorize Antigravity OAuth:")
    +        print(auth_url)
    +        if open_browser:
    +            webbrowser.open(auth_url)
    +        if not ready.wait(timeout=CALLBACK_WAIT_SECONDS):
    +            raise AntigravityOAuthError(
    +                "Timed out waiting for Antigravity OAuth callback.",
    +                code="antigravity_oauth_callback_timeout",
    +            )
    +        if handler_cls.captured_error:
    +            raise AntigravityOAuthError(
    +                handler_cls.captured_error,
    +                code="antigravity_oauth_callback_error",
    +            )
    +        code = handler_cls.captured_code or ""
    +        token = [redacted:secret-assignment] verifier, redirect_uri)
    +    finally:
    +        server.shutdown()
    +        server.server_close()
    +
    +    access_token = [redacted:secret-assignment]"access_token", "") or "").strip()
    +    refresh_token = [redacted:secret-assignment]"refresh_token", "") or "").strip()
    +    if not access_token or not refresh_token:
    +        raise AntigravityOAuthError(
    +            "Antigravity OAuth response did not include both access_token and refresh_token.",
    +            code="antigravity_oauth_missing_token",
    +        )
    +    expires_in = int(token.get("expires_in", 0) or 0)
    +    creds = AntigravityCredentials(
    +        access_token=[redacted:secret-assignment]
    +        refresh_token=[redacted:secret-assignment]
    +        expires_ms=int((time.time() + max(60, expires_in)) * 1000),
    +        email=_fetch_user_email(access_token),
    +        project_id=project_id,
    +    )
    +    save_credentials(creds)
    +    return creds
    +
    +
    +def run_antigravity_oauth_login_pure() -> Dict[str, Any]:
    +    creds = start_oauth_flow(
    +        force_relogin=True,
    +        project_id=resolve_project_id_from_env(),
    +    )
    +    return {
    +        "access_token": creds.access_token,
    +        "refresh_token": creds.refresh_token,
    +        "expires_at_ms": creds.expires_ms,
    +        "email": creds.email,
    +        "project_id": creds.project_id,
    +    }
    ```
  - `agent/gemini_cloudcode_adapter.py`

    ```diff
    @@ -93,11 +93,14 @@ def _translate_tool_call_to_gemini(tool_call: Dict[str, Any]) -> Dict[str, Any]:
             args = {"_raw": args_raw}
         if not isinstance(args, dict):
             args = {"_value": args}
    +    function_call = {
    +        "name": fn.get("name") or "",
    +        "args": args,
    +    }
    +    if tool_call.get("id"):
    +        function_call["id"] = str(tool_call["id"])
         return {
    -        "functionCall": {
    -            "name": fn.get("name") or "",
    -            "args": args,
    -        },
    +        "functionCall": function_call,
             # Sentinel signature — matches opencode-gemini-auth's approach.
             # Without this, Code Assist rejects function calls that originated
             # outside its own chain.
    ```

    ```diff
    @@ -122,12 +125,13 @@ def _translate_tool_result_to_gemini(message: Dict[str, Any]) -> Dict[str, Any]:
         except json.JSONDecodeError:
             parsed = None
         response = parsed if isinstance(parsed, dict) else {"output": content}
    -    return {
    -        "functionResponse": {
    -            "name": name,
    -            "response": response,
    -        },
    +    function_response = {
    +        "name": name,
    +        "response": response,
         }
    +    if message.get("tool_call_id"):
    +        function_response["id"] = str(message["tool_call_id"])
    +    return {"functionResponse": function_response}
     
     
     def _build_gemini_contents(
    ```

    ```diff
    @@ -358,8 +362,9 @@ def _translate_gemini_response(
                     args_str = json.dumps(fc.get("args") or {}, ensure_ascii=False)
                 except (TypeError, ValueError):
                     args_str = "{}"
    +            call_id = str(fc.get("id") or "").strip() or f"call_{uuid.uuid4().hex[:12]}"
                 tool_calls.append(SimpleNamespace(
    -                id=f"call_{uuid.uuid4().hex[:12]}",
    +                id=call_id,
                     type="function",
                     index=i,
                     function=SimpleNamespace(name=str(fc["name"]), arguments=args_str),
    ```

    ```diff
    @@ -554,6 +559,7 @@ def _translate_stream_event(
                     model=model,
                     tool_call_delta={
                         "index": idx,
    +                    "id": str(fc.get("id") or "").strip(),
                         "name": name,
                         "arguments": args_str,
                     },
    ```
  - `agent/transports/chat_completions.py`

    ```diff
    @@ -437,7 +437,7 @@ def build_kwargs(
                         extra_body["extra_body"] = openai_compat_extra
                 elif raw_thinking_config:
                     extra_body["thinking_config"] = raw_thinking_config
    -        elif provider_name == "google-gemini-cli":
    +        elif provider_name in {"google-gemini-cli", "google-antigravity"}:
                 thinking_config = _build_gemini_thinking_config(model, reasoning_config)
                 if thinking_config:
                     extra_body["thinking_config"] = thinking_config
    ```
  - `hermes_cli/auth.py`

    ```diff
    @@ -142,6 +142,9 @@
     DEFAULT_GEMINI_CLOUDCODE_BASE_URL = "cloudcode-pa://google"
     GEMINI_OAUTH_ACCESS_TOKEN_REFRESH_SKEW_SECONDS = 60  # refresh 60s before expiry
     
    +# Google Antigravity OAuth (Antigravity Code Assist backend)
    +DEFAULT_ANTIGRAVITY_CLOUDCODE_BASE_URL = "antigravity-pa://google"
    +
     # LM Studio's default no-auth mode still requires *some* non-empty bearer for
     # the API-key code paths (auxiliary_client, runtime resolver) to treat the
     # provider as configured. This sentinel is sent only to LM Studio, never to
    ```

    ```diff
    @@ -212,6 +215,12 @@ class ProviderConfig:
             auth_type="oauth_external",
             inference_base_url=DEFAULT_GEMINI_CLOUDCODE_BASE_URL,
         ),
    +    "google-antigravity": ProviderConfig(
    +        id="google-antigravity",
    +        name="Google Antigravity (OAuth)",
    +        auth_type="oauth_external",
    +        inference_base_url=DEFAULT_ANTIGRAVITY_CLOUDCODE_BASE_URL,
    +    ),
         "lmstudio": ProviderConfig(
             id="lmstudio",
             name="LM Studio",
    ```

    ```diff
    @@ -1530,6 +1539,7 @@ def resolve_provider(
             "github-copilot-acp": "copilot-acp", "copilot-acp-agent": "copilot-acp",
             "opencode": "opencode-zen", "zen": "opencode-zen",
             "qwen-portal": "qwen-oauth", "qwen-cli": "qwen-oauth", "qwen-oauth": "qwen-oauth", "google-gemini-cli": "google-gemini-cli", "gemini-cli": "google-gemini-cli", "gemini-oauth": "google-gemini-cli",
    +        "google-antigravity": "google-antigravity", "google-antigravity-oauth": "google-antigravity", "antigravity": "google-antigravity", "antigravity-oauth": "google-antigravity", "antigravity-cli": "google-antigravity", "agy": "google-antigravity", "agy-cli": "google-antigravity",
             "hf": "huggingface", "hugging-face": "huggingface", "huggingface-hub": "huggingface",
             "mimo": "xiaomi", "xiaomi-mimo": "xiaomi",
             "tencent": "tencent-tokenhub", "tokenhub": "tencent-tokenhub",
    ```

    ```diff
    @@ -2246,6 +2256,72 @@ def get_gemini_oauth_auth_status() -> Dict[str, Any]:
             "email": creds.email,
             "project_id": creds.project_id,
         }
    +
    +
    +def resolve_antigravity_oauth_runtime_credentials(
    +    *,
    +    force_refresh: bool = False,
    +) -> Dict[str, Any]:
    +    """Resolve runtime OAuth creds for google-antigravity."""
    +    try:
    +        from agent.antigravity_oauth import (
    +            AntigravityOAuthError,
    +            _credentials_path,
    +            get_valid_access_token,
    +            load_credentials,
    +        )
    +    except ImportError as exc:
    +        raise AuthError(
    +            f"agent.antigravity_oauth is not importable: {exc}",
    +            provider="google-antigravity",
    +            code="antigravity_oauth_module_missing",
    +        ) from exc
    +
    +    try:
    +        access_token = [redacted:secret-assignment]
    +    except AntigravityOAuthError as exc:
    +        raise AuthError(
    +            str(exc),
    +            provider="google-antigravity",
    +            code=exc.code,
    +        ) from exc
    +
    +    creds = load_credentials()
    +    return {
    +        "provider": "google-antigravity",
    +        "base_url": DEFAULT_ANTIGRAVITY_CLOUDCODE_BASE_URL,
    +        "api_key": access_token,
    +        "source": "antigravity-oauth",
    +        "expires_at_ms": (creds.expires_ms if creds else None),
    +        "auth_file": str(_credentials_path()),
    +        "email": (creds.email if creds else "") or "",
    +        "project_id": (creds.project_id if creds else "") or "",
    +    }
    +
    +
    +def get_antigravity_oauth_auth_status() -> Dict[str, Any]:
    +    """Return a status dict for `hermes auth list` / `hermes status`."""
    +    try:
    +        from agent.antigravity_oauth import _credentials_path, load_credentials
    +    except ImportError:
    +        return {"logged_in": False, "error": "agent.antigravity_oauth unavailable"}
    +    auth_path = _credentials_path()
    +    creds = load_credentials()
    +    if creds is None or not creds.access_token:
    +        return {
    +            "logged_in": False,
    +            "auth_file": str(auth_path),
    +            "error": "not logged in",
    +        }
    +    return {
    +        "logged_in": True,
    +        "auth_file": str(auth_path),
    +        "source": "antigravity-oauth",
    +        "api_key": creds.access_token,
    +        "expires_at_ms": creds.expires_ms,
    +        "email": creds.email,
    +        "project_id": creds.project_id,
    +    }
     # Spotify auth — PKCE tokens stored in ~/.hermes/auth.json
     # =============================================================================
     
    ```

    ```diff
    @@ -6191,6 +6267,8 @@ def get_auth_status(provider_id: Optional[str] = None) -> Dict[str, Any]:
             return get_qwen_auth_status()
         if target == "google-gemini-cli":
             return get_gemini_oauth_auth_status()
    +    if target == "google-antigravity":
    +        return get_antigravity_oauth_auth_status()
         if target == "minimax-oauth":
             return get_minimax_oauth_auth_status()
         if target == "copilot-acp":
    ```
  - `hermes_cli/auth_commands.py`

    ```diff
    @@ -34,7 +34,7 @@
     
     
     # Providers that support OAuth login in addition to API keys.
    -_OAUTH_CAPABLE_PROVIDERS = {"anthropic", "nous", "openai-codex", "xai-oauth", "qwen-oauth", "google-gemini-cli", "minimax-oauth"}
    +_OAUTH_CAPABLE_PROVIDERS = {"anthropic", "nous", "openai-codex", "xai-oauth", "qwen-oauth", "google-gemini-cli", "google-antigravity", "minimax-oauth"}
     
     
     def _get_custom_provider_names() -> list:
    ```

    ```diff
    @@ -386,6 +386,27 @@ def auth_add_command(args) -> None:
             print(f'Added {provider} OAuth credential #{len(pool.entries())}: "{entry.label}"')
             return
     
    +    if provider == "google-antigravity":
    +        from agent.antigravity_oauth import run_antigravity_oauth_login_pure
    +
    +        creds = run_antigravity_oauth_login_pure()
    +        label = (getattr(args, "label", None) or "").strip() or (
    +            creds.get("email") or _oauth_default_label(provider, len(pool.entries()) + 1)
    +        )
    +        entry = PooledCredential(
    +            provider=provider,
    +            id=uuid.uuid4().hex[:6],
    +            label=label,
    +            auth_type=AUTH_TYPE_OAUTH,
    +            priority=0,
    +            source=f"{SOURCE_MANUAL}:antigravity_pkce",
    +            access_token=[redacted:secret-assignment]"access_token"],
    +            refresh_token=[redacted:secret-assignment]"refresh_token"),
    +        )
    +        pool.add_entry(entry)
    +        print(f'Added {provider} OAuth credential #{len(pool.entries())}: "{entry.label}"')
    +        return
    +
         if provider == "qwen-oauth":
             creds = auth_mod.resolve_qwen_runtime_credentials(refresh_if_expiring=False)
             auth_mod._mark_qwen_oauth_active(creds)
    ```
  - `hermes_cli/config.py`

    ```diff
    @@ -3100,6 +3100,38 @@ def _ensure_hermes_home_managed(home: Path):
             "category": "provider",
             "advanced": True,
         },
    +    "HERMES_ANTIGRAVITY_CLIENT_ID": {
    +        "description": "Google OAuth client ID for google-antigravity (optional; discovered from agy when omitted)",
    +        "prompt": "Antigravity OAuth client ID (optional — leave empty to discover from agy)",
    +        "url": "https://console.cloud.google.com/apis/credentials",
    +        "password": False,
    +        "category": "provider",
    +        "advanced": True,
    +    },
    +    "HERMES_ANTIGRAVITY_CLIENT_SECRET": {
    +        "description": "Google OAuth client secret for google-antigravity (optional)",
    +        "prompt": "Antigravity OAuth client secret (optional)",
    +        "url": "https://console.cloud.google.com/apis/credentials",
    +        "password": True,
    +        "category": "provider",
    +        "advanced": True,
    +    },
    +    "HERMES_ANTIGRAVITY_CLI_PATH": {
    +        "description": "Path to agy/Antigravity CLI for OAuth client credential discovery",
    +        "prompt": "Antigravity CLI path (leave empty to search PATH/default locations)",
    +        "url": None,
    +        "password": False,
    +        "category": "provider",
    +        "advanced": True,
    +    },
    +    "HERMES_ANTIGRAVITY_PROJECT_ID": {
    +        "description": "GCP project ID for Antigravity OAuth (auto-discovered when omitted)",
    +        "prompt": "GCP project ID for Antigravity OAuth (leave empty to auto-discover)",
    +        "url": None,
    +        "password": False,
    +        "category": "provider",
    +        "advanced": True,
    +    },
         "OPENCODE_ZEN_API_KEY": {
             "description": "OpenCode Zen API key (pay-as-you-go access to curated models)",
             "prompt": "OpenCode Zen API key",
    ```
  - `hermes_cli/main.py`

    ```diff
    @@ -603,6 +603,7 @@ def _resolve_sudo_user_profile_env(name: str) -> str | None:
         _model_flow_qwen_oauth,
         _model_flow_minimax_oauth,
         _model_flow_google_gemini_cli,
    +    _model_flow_google_antigravity,
         _model_flow_custom,
         _model_flow_azure_foundry,
         _model_flow_named_custom,
    ```

    ```diff
    @@ -3074,6 +3075,8 @@ def _active_custom_key_from_base_url() -> str:
             _model_flow_minimax_oauth(config, current_model, args=args)
         elif selected_provider == "google-gemini-cli":
             _model_flow_google_gemini_cli(config, current_model)
    +    elif selected_provider == "google-antigravity":
    +        _model_flow_google_antigravity(config, current_model)
         elif selected_provider == "copilot-acp":
             _model_flow_copilot_acp(config, current_model)
         elif selected_provider == "copilot":
    ```

    ```diff
    @@ -3603,14 +3606,6 @@ def _prompt_provider_choice(choices, *, default=0, title="Select provider:"):
     ]
     
     
    -
    -
    -
    -
    -
    -
    -
    -
     def _prompt_custom_api_mode_selection(base_url: str, current_api_mode: str = "") -> Optional[str]:
         """Prompt for a custom provider API mode.
     
    ```

    ```diff
    @@ -11248,6 +11243,24 @@ def cmd_logs(args):
             since=getattr(args, "since", None),
             component=getattr(args, "component", None),
         )
    +
    +
    +def _build_provider_choices() -> list[str]:
    +    """Build the --provider choices list from CANONICAL_PROVIDERS + 'auto'."""
    +    try:
    +        from hermes_cli.models import CANONICAL_PROVIDERS as _cp
    +        return ["auto"] + [p.slug for p in _cp]
    +    except Exception:
    +        # Fallback: static list guarantees the CLI always works
    +        return [
    +            "auto", "openrouter", "nous", "openai-codex", "xai-oauth", "copilot-acp", "copilot",
    +            "anthropic", "gemini", "google-gemini-cli", "google-antigravity", "xai", "bedrock", "azure-foundry",
    +            "ollama-cloud", "huggingface", "zai", "kimi-coding", "kimi-coding-cn",
    +            "stepfun", "minimax", "minimax-cn", "kilocode", "novita", "xiaomi", "arcee",
    +            "nvidia", "deepseek", "alibaba", "qwen-oauth", "opencode-zen", "opencode-go",
    +        ]
    +
    +
     # Top-level subcommands that argparse knows about WITHOUT running plugin
     # discovery.  Used to short-circuit eager plugin imports (which can take
     # 500ms+ pulling in google.cloud.pubsub_v1, aiohttp, grpc, etc.) when the
    ```
  - `hermes_cli/model_setup_flows.py`

    ```diff
    @@ -712,6 +712,64 @@ def _model_flow_google_gemini_cli(_config, current_model=""):
         else:
             print("No change.")
     
    +
    +def _model_flow_google_antigravity(_config, current_model=""):
    +    """Google Antigravity OAuth via Antigravity Code Assist.
    +
    +    Antigravity is Google's consumer successor to the Gemini CLI. It reuses the
    +    Code Assist backend with a distinct OAuth client + scopes. Leaves the
    +    `google-gemini-cli` provider (Enterprise Code Assist) untouched.
    +    """
    +    from hermes_cli.auth import (
    +        DEFAULT_ANTIGRAVITY_CLOUDCODE_BASE_URL,
    +        get_antigravity_oauth_auth_status,
    +        resolve_antigravity_oauth_runtime_credentials,
    +        _prompt_model_selection,
    +        _save_model_choice,
    +        _update_config_for_provider,
    +    )
    +    from hermes_cli.models import provider_model_ids
    +
    +    status = get_antigravity_oauth_auth_status()
    +    if not status.get("logged_in"):
    +        try:
    +            from agent.antigravity_oauth import resolve_project_id_from_env, start_oauth_flow
    +
    +            env_project = resolve_project_id_from_env()
    +            start_oauth_flow(force_relogin=True, project_id=env_project)
    +        except Exception as exc:
    +            print(f"OAuth login failed: {exc}")
    +            return
    +
    +    try:
    +        creds = resolve_antigravity_oauth_runtime_credentials(force_refresh=False)
    +        project_id = creds.get("project_id", "")
    +        if project_id:
    +            print(f"  Using Antigravity project: {project_id}")
    +    except Exception as exc:
    +        print(f"Failed to resolve Antigravity credentials: {exc}")
    +        return
    +
    +    models = provider_model_ids("google-antigravity")
    +    default = current_model or (models[0] if models else "gemini-3-flash-agent")
    +    selected = _prompt_model_selection(
    +        models,
    +        current_model=default,
    +        confirm_provider="google-antigravity",
    +        confirm_base_url=DEFAULT_ANTIGRAVITY_CLOUDCODE_BASE_URL,
    +    )
    +    if selected:
    +        _save_model_choice(selected)
    +        _update_config_for_provider(
    +            "google-antigravity", DEFAULT_ANTIGRAVITY_CLOUDCODE_BASE_URL
    +        )
    +        print(
    +            f"Default model set to: {selected} (via Google Antigravity OAuth / Code Assist)"
    +        )
    +    else:
    +        print("No change.")
    +
    +
     def _model_flow_custom(config):
         """Custom endpoint: collect URL, API key, and model name.
     
    ```
  - `hermes_cli/models.py`

    ```diff
    @@ -276,6 +276,15 @@ def _xai_curated_models() -> list[str]:
             "gemini-3-flash-preview",
             "gemini-3.5-flash",
         ],
    +    "google-antigravity": [
    +        "gemini-3-flash-agent",
    +        "gemini-3.5-flash-low",
    +        "gemini-pro-agent",
    +        "gemini-3.1-pro-low",
    +        "claude-sonnet-4-6",
    +        "claude-opus-4-6-thinking",
    +        "gpt-oss-120b-medium",
    +    ],
         "zai": [
             "glm-5.2",
             "glm-5.1",
    ```

    ```diff
    @@ -1029,6 +1038,7 @@ class ProviderEntry(NamedTuple):
         ProviderEntry("huggingface",    "Hugging Face",             "Hugging Face Inference Providers"),
         ProviderEntry("gemini",         "Google AI Studio",         "Google AI Studio (Native Gemini API)"),
         ProviderEntry("google-gemini-cli", "Google Gemini (OAuth)",   "Google Gemini via OAuth + Code Assist (Code Assist OAuth flow)"),
    +    ProviderEntry("google-antigravity", "Google Antigravity (OAuth)", "Google Antigravity via OAuth + Code Assist (Gemini 3.5/3.1, Claude, GPT-OSS where entitled)"),
         ProviderEntry("deepseek",       "DeepSeek",                 "DeepSeek (V3, R1, coder, direct API)"),
         ProviderEntry("xai",            "xAI",                      "xAI Grok (Direct API)"),
         ProviderEntry("zai",            "Z.AI / GLM",               "Z.AI / GLM (Zhipu direct API)"),
    ```

    ```diff
    @@ -1222,6 +1232,12 @@ def group_providers(slugs):
         "qwen-portal": "qwen-oauth",
         "gemini-cli": "google-gemini-cli",
         "gemini-oauth": "google-gemini-cli",
    +    "antigravity": "google-antigravity",
    +    "antigravity-oauth": "google-antigravity",
    +    "antigravity-cli": "google-antigravity",
    +    "google-antigravity-oauth": "google-antigravity",
    +    "agy": "google-antigravity",
    +    "agy-cli": "google-antigravity",
         "hf": "huggingface",
         "hugging-face": "huggingface",
         "huggingface-hub": "huggingface",
    ```

    ```diff
    @@ -1788,6 +1804,15 @@ def _model_in_provider_catalog(name_lower: str, providers: set[str]) -> bool:
         {"nous", "openrouter", "copilot", "kilocode"}
     )
     
    +# Subscription/OAuth providers whose catalogs RE-EXPOSE other vendors' models
    +# (e.g. google-antigravity serves Claude / Gemini / GPT-OSS where the account
    +# is entitled). For bare short-alias resolution (`sonnet`, `opus`, ...) these
    +# must NOT hijack the alias away from the model's native vendor provider
    +# (`anthropic`, `gemini`, ...). They're tried only as a last resort, after
    +# every native-vendor catalog. They are NOT aggregators (an explicit switch TO
    +# them is still valid), so they stay out of _AGGREGATOR_PROVIDERS.
    +_BORROWED_MODEL_PROVIDERS = frozenset({"google-antigravity"})
    +
     
     def _resolve_static_model_alias(
         name_lower: str,
    ```

    ```diff
    @@ -1825,7 +1850,11 @@ def _match(provider: str) -> Optional[str]:
                 return provider, matched
     
         for provider in _PROVIDER_MODELS:
    -        if provider in current_keys or provider in _AGGREGATOR_PROVIDERS:
    +        if (
    +            provider in current_keys
    +            or provider in _AGGREGATOR_PROVIDERS
    +            or provider in _BORROWED_MODEL_PROVIDERS
    +        ):
                 continue
             if matched := _match(provider):
                 return provider, matched
    ```

    ```diff
    @@ -1834,6 +1863,13 @@ def _match(provider: str) -> Optional[str]:
             if provider in current_keys and (matched := _match(provider)):
                 return provider, matched
     
    +    # Last resort: providers that re-expose other vendors' models (e.g.
    +    # google-antigravity serving Claude). Only reached when no native-vendor
    +    # catalog matched — so `sonnet` resolves to anthropic, not antigravity.
    +    for provider in _BORROWED_MODEL_PROVIDERS:
    +        if provider in current_keys and (matched := _match(provider)):
    +            return provider, matched
    +
         return None
     
     
    ```

    ```diff
    @@ -1880,11 +1916,23 @@ def detect_static_provider_for_model(
     
         # --- Step 1: check static provider catalogs for a direct match ---
         for pid, models in _PROVIDER_MODELS.items():
    -        if pid in current_keys or pid in _AGGREGATOR_PROVIDERS:
    +        if (
    +            pid in current_keys
    +            or pid in _AGGREGATOR_PROVIDERS
    +            or pid in _BORROWED_MODEL_PROVIDERS
    +        ):
                 continue
             if any(name_lower == m.lower() for m in models):
                 return (pid, name)
     
    +    # Borrow-list providers (re-expose other vendors' models) only after every
    +    # native-vendor catalog, and only when one is the current provider.
    +    for pid in _BORROWED_MODEL_PROVIDERS:
    +        if pid in current_keys:
    +            continue
    +        if any(name_lower == m.lower() for m in _PROVIDER_MODELS.get(pid, [])):
    +            return (pid, name)
    +
         return None
     
     
    ```

    ```diff
    @@ -2192,6 +2240,32 @@ def _merge_with_models_dev(provider: str, curated: list[str]) -> list[str]:
         return merged
     
     
    +def _fetch_antigravity_models(*, force_refresh: bool = False) -> list[str]:
    +    try:
    +        from agent import antigravity_oauth
    +        from agent.antigravity_code_assist import (
    +            fetch_available_models_with_fallbacks,
    +            load_code_assist,
    +            parse_agent_model_ids,
    +        )
    +        from hermes_cli.auth import resolve_antigravity_oauth_runtime_credentials
    +
    +        creds = resolve_antigravity_oauth_runtime_credentials(force_refresh=force_refresh)
    +        access_token = [redacted:secret-assignment]"api_key") or "").strip()
    +        project_id = str(creds.get("project_id") or "").strip()
    +        if not access_token:
    +            return []
    +        if not project_id:
    +            info = load_code_assist(access_token)
    +            project_id = info.project_id
    +            if project_id:
    +                antigravity_oauth.update_project_ids(project_id=project_id, managed_project_id=project_id)
    +        payload = fetch_available_models_with_fallbacks(access_token, project_id=project_id)
    +        return parse_agent_model_ids(payload)
    +    except Exception:
    +        return []
    +
    +
     def provider_model_ids(provider: Optional[str], *, force_refresh: bool = False) -> list[str]:
         """Return the best known model catalog for a provider.
     
    ```

    ```diff
    @@ -2222,6 +2296,10 @@ def provider_model_ids(provider: Optional[str], *, force_refresh: bool = False)
             return get_codex_model_ids(access_token=[redacted:secret-assignment]
         if normalized == "xai-oauth":
             return list(_PROVIDER_MODELS.get("xai-oauth", _PROVIDER_MODELS.get("xai", [])))
    +    if normalized == "google-antigravity":
    +        live = _fetch_antigravity_models(force_refresh=force_refresh)
    +        if live:
    +            return live
         if normalized in {"copilot", "copilot-acp"}:
             try:
                 live = _fetch_github_models(_resolve_copilot_catalog_api_key())
    ```
  - `hermes_cli/providers.py`

    ```diff
    @@ -81,6 +81,11 @@ class HermesOverlay:
             auth_type="oauth_external",
             base_url_override="cloudcode-pa://google",
         ),
    +    "google-antigravity": HermesOverlay(
    +        transport="openai_chat",
    +        auth_type="oauth_external",
    +        base_url_override="antigravity-pa://google",
    +    ),
         "lmstudio": HermesOverlay(
             transport="openai_chat",
             auth_type="api_key",
    ```

    ```diff
    @@ -314,6 +319,13 @@ class ProviderDef:
         "gemini-cli": "google-gemini-cli",
         "gemini-oauth": "google-gemini-cli",
     
    +    # google-antigravity (OAuth + Antigravity Code Assist)
    +    "antigravity": "google-antigravity",
    +    "antigravity-oauth": "google-antigravity",
    +    "antigravity-cli": "google-antigravity",
    +    "google-antigravity-oauth": "google-antigravity",
    +    "agy": "google-antigravity",
    +    "agy-cli": "google-antigravity",
     
         # huggingface
         "hf": "huggingface",
    ```
  - `hermes_cli/runtime_provider.py`

    ```diff
    @@ -27,6 +27,7 @@
         resolve_xai_oauth_runtime_credentials,
         resolve_qwen_runtime_credentials,
         resolve_gemini_oauth_runtime_credentials,
    +    resolve_antigravity_oauth_runtime_credentials,
         resolve_api_key_provider_credentials,
         resolve_external_process_provider_credentials,
         has_usable_secret,
    ```

    ```diff
    @@ -334,6 +335,9 @@ def _resolve_runtime_from_pool_entry(
         elif provider == "google-gemini-cli":
             api_mode = "chat_completions"
             base_url = base_url or "cloudcode-pa://google"
    +    elif provider == "google-antigravity":
    +        api_mode = "chat_completions"
    +        base_url = base_url or "antigravity-pa://google"
         elif provider == "minimax-oauth":
             # MiniMax OAuth tokens are valid only against the Anthropic Messages
             # compatible endpoint. Do not honor stale model.api_mode values from a
    ```

    ```diff
    @@ -1634,6 +1638,26 @@ def resolve_runtime_provider(
                 logger.info("Google Gemini OAuth credentials failed; "
                             "falling through to next provider.")
     
    +    if provider == "google-antigravity":
    +        try:
    +            creds = resolve_antigravity_oauth_runtime_credentials()
    +            return {
    +                "provider": "google-antigravity",
    +                "api_mode": "chat_completions",
    +                "base_url": creds.get("base_url", ""),
    +                "api_key": creds.get("api_key", ""),
    +                "source": creds.get("source", "antigravity-oauth"),
    +                "expires_at_ms": creds.get("expires_at_ms"),
    +                "email": creds.get("email", ""),
    +                "project_id": creds.get("project_id", ""),
    +                "requested_provider": requested_provider,
    +            }
    +        except AuthError:
    +            if requested_provider != "auto":
    +                raise
    +            logger.info("Google Antigravity OAuth credentials failed; "
    +                        "falling through to next provider.")
    +
         if provider == "copilot-acp":
             creds = resolve_external_process_provider_credentials(provider)
             return {
    ```
  - `plugins/model-providers/gemini/__init__.py`

    ```diff
    @@ -2,6 +2,7 @@
     
     gemini:            Google AI Studio (API key) — uses GeminiNativeClient
     google-gemini-cli: Google Cloud Code Assist (OAuth) — uses GeminiCloudCodeClient
    +google-antigravity: Google Antigravity Code Assist (OAuth) — uses AntigravityCloudCodeClient
     
     Both report api_mode="chat_completions" but use custom native clients
     that bypass the standard OpenAI transport. The profile captures auth
    ```

    ```diff
    @@ -68,5 +69,22 @@ def build_extra_body(
         auth_type="oauth_external",
     )
     
    +google_antigravity = GeminiProfile(
    +    name="google-antigravity",
    +    aliases=(
    +        "antigravity",
    +        "antigravity-oauth",
    +        "antigravity-cli",
    +        "google-antigravity-oauth",
    +        "agy",
    +        "agy-cli",
    +    ),
    +    api_mode="chat_completions",
    +    env_vars=(),  # OAuth — no API key
    +    base_url="antigravity-pa://google",  # Antigravity Code Assist internal scheme
    +    auth_type="oauth_external",
    +)
    +
     register_provider(gemini)
     register_provider(google_gemini_cli)
    +register_provider(google_antigravity)
    ```
  - `scripts/release.py`

    ```diff
    @@ -45,6 +45,7 @@
     
     # Auto-extracted from noreply emails + manual overrides
     AUTHOR_MAP = {
    +    "[redacted:email]": "pmos69",  # PR #29474 salvage (native Antigravity OAuth provider; Gemini CLI sunset #29294/#49701)
         "[redacted:email]": "orbisai0security",  # PR #9560 salvage (session.py path-traversal guard, V-009)
         "[redacted:email]": "panghuer023",  # PR #37994 salvage (interrupt unblocks pending gateway approval; #8697)
         "[redacted:email]": "natehale",  # PR #48678 salvage (typing indicator lingers after final reply)
    ```
  - `tests/agent/test_antigravity_cloudcode.py`

    ```diff
    @@ -0,0 +1,405 @@
    +"""Tests for the google-antigravity OAuth + Antigravity Code Assist provider."""
    +
    +from __future__ import annotations
    +
    +import json
    +import os
    +import stat
    +import time
    +import threading
    +import urllib.parse
    +from io import BytesIO
    +from pathlib import Path
    +
    +import pytest
    +
    +
    [redacted:email](autouse=True)
    +def _isolate_env(monkeypatch, tmp_path):
    +    home = tmp_path / ".hermes"
    +    home.mkdir(parents=True)
    +    monkeypatch.setattr(Path, "home", lambda: tmp_path)
    +    monkeypatch.setenv("HERMES_HOME", str(home))
    +    for key in (
    +        "HERMES_ANTIGRAVITY_CLIENT_ID",
    +        "HERMES_ANTIGRAVITY_CLIENT_SECRET",
    +        "HERMES_ANTIGRAVITY_CLI_PATH",
    +        "HERMES_ANTIGRAVITY_PROJECT_ID",
    +        "GOOGLE_CLOUD_PROJECT",
    +        "GOOGLE_CLOUD_PROJECT_ID",
    +        "LOCALAPPDATA",
    +        "APPDATA",
    +        "ProgramFiles",
    +        "ProgramFiles(x86)",
    +    ):
    +        monkeypatch.delenv(key, raising=False)
    +    monkeypatch.setattr("shutil.which", lambda _: None)
    +    try:
    +        from agent import antigravity_oauth
    +
    +        antigravity_oauth._discovered_creds_cache.clear()
    +    except Exception:
    +        pass
    +    return home
    +
    +
    +class TestAntigravityCredentials:
    +    def test_save_load_uses_separate_file_and_0600_permissions(self):
    +        from agent.antigravity_oauth import (
    +            AntigravityCredentials,
    +            _credentials_path,
    +            load_credentials,
    +            save_credentials,
    +        )
    +
    +        save_credentials(AntigravityCredentials(
    +            access_token="at",
    +            refresh_token="rt",
    +            expires_ms=int((time.time() + 3600) * 1000),
    +            email="[redacted:email]",
    +            project_id="proj-123",
    +        ))
    +
    +        assert _credentials_path().name == "antigravity_oauth.json"
    +        loaded = load_credentials()
    +        assert loaded is not None
    +        assert loaded.refresh_token == "rt"
    +        assert loaded.project_id == "proj-123"
    +        if os.name != "nt":
    +            assert stat.S_IMODE(_credentials_path().stat().st_mode) == 0o600
    +
    +    def test_env_override_client_id(self, monkeypatch):
    +        from agent.antigravity_oauth import _get_client_id
    +
    +        monkeypatch.setenv("HERMES_ANTIGRAVITY_CLIENT_ID", "custom.apps.googleusercontent.com")
    +        assert _get_client_id() == "custom.apps.googleusercontent.com"
    +
    +    def test_env_override_client_secret(self, monkeypatch):
    +        from agent.antigravity_oauth import _get_client_secret
    +
    +        monkeypatch.setenv("HERMES_ANTIGRAVITY_CLIENT_SECRET", "custom-secret")
    +        assert _get_client_secret() == "custom-secret"
    +
    +    def test_discovers_client_credentials_from_configured_agy_path(self, tmp_path, monkeypatch):
    +        from agent import antigravity_oauth
    +
    +        fake_client_id = (
    +            "1071006060591-"
    +            + "fakefakefakefakefakefakefake"
    +            + ".apps.google"
    +            + "usercontent.com"
    +        )
    +        fake_client_secret = "GOC" + "SPX-" + "fake-secret-value-placeholde"
    +        fake_agy = tmp_path / "agy.exe"
    +        fake_agy.write_text(
    +            f'oauthClientId="{fake_client_id}";\n'
    +            f'oauthClientSecret="[redacted:secret-assignment]";\n',
    +            encoding="utf-8",
    +        )
    +        monkeypatch.setenv("HERMES_ANTIGRAVITY_CLI_PATH", str(fake_agy))
    +        antigravity_oauth._discovered_creds_cache.clear()
    +
    +        assert antigravity_oauth._get_client_id().startswith("1071006060591-")
    +        assert antigravity_oauth._get_client_secret() == fake_client_secret
    +
    +    def test_missing_discovery_falls_back_to_public_default(self, monkeypatch):
    +        # With no env override and no discoverable agy install, the public
    +        # baked-in Antigravity desktop OAuth client is used as the floor so
    +        # users without `agy` installed can still authenticate (PKCE makes the
    +        # installed-app "secret" non-confidential, same as gemini-cli).
    +        from agent import antigravity_oauth
    +        from agent.antigravity_oauth import (
    +            _DEFAULT_CLIENT_ID,
    +            _DEFAULT_CLIENT_SECRET,
    +            _require_client_id,
    +        )
    +
    +        monkeypatch.delenv("HERMES_ANTIGRAVITY_CLIENT_ID", raising=False)
    +        monkeypatch.delenv("HERMES_ANTIGRAVITY_CLIENT_SECRET", raising=False)
    +        monkeypatch.delenv("HERMES_ANTIGRAVITY_CLI_PATH", raising=False)
    +        antigravity_oauth._discovered_creds_cache.clear()
    +
    +        assert _require_client_id() == _DEFAULT_CLIENT_ID
    +        assert antigravity_oauth._get_client_secret() == _DEFAULT_CLIENT_SECRET
    +        assert _DEFAULT_CLIENT_ID.startswith("1071006060591-")
    +
    +    def test_pkce_challenge_is_s256(self):
    +        import base64
    +        import hashlib
    +
    +        from agent.antigravity_oauth import _generate_pkce_pair
    +
    +        verifier, challenge = _generate_pkce_pair()
    +        expected = base64.urlsafe_b64encode(
    +            hashlib.sha256(verifier.encode("ascii")).digest()
    +        ).rstrip(b"=").decode("ascii")
    +        assert challenge == expected
    +        assert 43 <= len(verifier) <= 128
    +
    +    def test_exchange_code_posts_pkce_payload(self, monkeypatch):
    +        from agent import antigravity_oauth
    +
    +        captured = {}
    +
    +        def fake_post(url, data, timeout):
    +            captured.update({"url": url, "data": data, "timeout": timeout})
    +            return {"access_token": "at"}
    +
    +        monkeypatch.setattr(antigravity_oauth, "_post_form", fake_post)
    +        monkeypatch.setenv("HERMES_ANTIGRAVITY_CLIENT_ID", "client.apps.googleusercontent.com")
    +        monkeypatch.setenv("HERMES_ANTIGRAVITY_CLIENT_SECRET", "secret")
    +
    +        assert antigravity_oauth.exchange_code("code", "verifier", "http://localhost/cb") == {
    +            "access_token": "at"
    +        }
    +        assert captured["url"] == antigravity_oauth.TOKEN_ENDPOINT
    +        assert captured["data"]["grant_type"] == "authorization_code"
    +        assert captured["data"]["code_verifier"] == "verifier"
    +        assert captured["data"]["redirect_uri"] == "http://localhost/cb"
    +        assert captured["data"]["client_id"] == "client.apps.googleusercontent.com"
    +        assert captured["data"]["client_secret"] == "secret"
    +
    +    def test_refresh_tries_discovered_client_secret_candidates(self, monkeypatch):
    +        from agent import antigravity_oauth
    +        from agent.antigravity_oauth import AntigravityOAuthError
    +
    +        calls = []
    +        monkeypatch.setattr(
    +            antigravity_oauth,
    +            "_iter_client_credential_candidates",
    +            lambda: [
    +                ("client.apps.googleusercontent.com", "wrong-secret"),
    +                ("client.apps.googleusercontent.com", "right-secret"),
    +            ],
    +        )
    +
    +        def fake_post(url, data, timeout):
    +            calls.append(data["client_secret"])
    +            if data["client_secret"] == "wrong-secret":
    +                raise AntigravityOAuthError(
    +                    "invalid client",
    +                    code="antigravity_oauth_invalid_client",
    +                )
    +            return {"access_token": "new-token", "expires_in": 3600}
    +
    +        monkeypatch.setattr(antigravity_oauth, "_post_form", fake_post)
    +
    +        assert antigravity_oauth.refresh_access_token("refresh-token")["access_token"] == "new-token"
    +        assert calls == ["wrong-secret", "right-secret"]
    +
    +    def test_invalid_grant_refresh_clears_credentials(self, monkeypatch):
    +        from agent import antigravity_oauth
    +        from agent.antigravity_oauth import (
    +            AntigravityCredentials,
    +            AntigravityOAuthError,
    +            load_credentials,
    +            save_credentials,
    +        )
    +
    +        save_credentials(AntigravityCredentials(
    +            access_token="[redacted:secret-assignment]",
    +            refresh_token="rt",
    +            expires_ms=int((time.time() - 3600) * 1000),
    +        ))
    +
    +        def invalid_grant(_refresh_token):
    +            raise AntigravityOAuthError("revoked", code="antigravity_oauth_invalid_grant")
    +
    +        monkeypatch.setattr(antigravity_oauth, "refresh_access_token", invalid_grant)
    +        with pytest.raises(AntigravityOAuthError, match="revoked"):
    +            antigravity_oauth.get_valid_access_token()
    +        assert load_credentials() is None
    +
    +    def test_callback_handler_captures_code_on_handler_class(self):
    +        from agent.antigravity_oauth import CALLBACK_PATH, _OAuthCallbackHandler
    +
    +        handler_cls = type("TestAntigravityOAuthCallbackHandler", (_OAuthCallbackHandler,), {})
    +        handler_cls.expected_state = "state-123"
    +        handler_cls.captured_code = None
    +        handler_cls.captured_error = None
    +        handler_cls.ready = threading.Event()
    +
    +        handler = handler_cls.__new__(handler_cls)
    +        handler.path = CALLBACK_PATH + "?" + urllib.parse.urlencode({
    +            "state": "state-123",
    +            "code": "auth-code",
    +        })
    +        handler.wfile = BytesIO()
    +        responses = []
    +        headers = []
    +        handler.send_response = lambda code: responses.append(code)
    +        handler.send_header = lambda key, value: headers.append((key, value))
    +        handler.end_headers = lambda: None
    +
    +        handler.do_GET()
    +
    +        assert responses == [200]
    +        assert handler_cls.captured_code == "auth-code"
    +        assert handler_cls.captured_error is None
    +        assert handler_cls.ready.is_set()
    +        assert "captured_code" not in handler.__dict__
    +
    +
    +class TestAntigravityModelCatalog:
    +    def test_parse_agent_model_ids_prefers_recommended_group(self):
    +        from agent.antigravity_code_assist import parse_agent_model_ids
    +
    +        payload = {
    +            "defaultAgentModelId": "gemini-3-flash-agent",
    +            "agentModelSorts": [
    +                {
    +                    "displayName": "Experimental",
    +                    "modelIds": ["tab_flash_lite_preview", "chat_23310"],
    +                },
    +                {
    +                    "displayName": "Recommended",
    +                    "modelIds": [
    +                        "gemini-3-flash-agent",
    +                        "gemini-3.5-flash-low",
    +                        "gemini-3.1-pro-high",
    +                        "gemini-pro-agent",
    +                        "claude-sonnet-4-6",
    +                    ],
    +                },
    +            ],
    +            "models": [{"id": "gpt-oss-120b-medium"}],
    +        }
    +
    +        assert parse_agent_model_ids(payload) == [
    +            "gemini-3-flash-agent",
    +            "gemini-3.5-flash-low",
    +            "gemini-pro-agent",
    +            "claude-sonnet-4-6",
    +        ]
    +
    +    def test_headers_include_antigravity_metadata(self):
    +        from agent.antigravity_code_assist import build_headers
    +
    +        headers = build_headers("tok")
    +        assert headers["Authorization"] == "Bearer tok"
    +        assert headers["User-Agent"].startswith("antigravity/")
    +        assert headers["X-Goog-Api-Client"] == "google-cloud-sdk vscode_cloudshelleditor/0.1"
    +        metadata = json.loads(headers["Client-Metadata"])
    +        assert metadata["ideType"] == "ANTIGRAVITY"
    +        assert metadata["platform"] == "PLATFORM_UNSPECIFIED"
    +
    +
    +class TestAntigravityClient:
    +    def test_client_exposes_openai_interface(self):
    +        from agent.antigravity_cloudcode_adapter import AntigravityCloudCodeClient
    +
    +        client = AntigravityCloudCodeClient(api_key="dummy")
    +        try:
    +            assert hasattr(client, "chat")
    +            assert hasattr(client.chat, "completions")
    +            assert callable(client.chat.completions.create)
    +        finally:
    +            client.close()
    +
    +    def test_create_uses_antigravity_endpoint_and_headers(self, monkeypatch):
    +        from agent import antigravity_oauth
    +        from agent.antigravity_cloudcode_adapter import AntigravityCloudCodeClient
    +        from agent.antigravity_code_assist import ANTIGRAVITY_CODE_ASSIST_ENDPOINT
    +
    +        monkeypatch.setattr(antigravity_oauth, "get_valid_access_token", lambda: "live-token")
    +
    +        class _Response:
    +            status_code = 200
    +
    +            def json(self):
    +                return {
    +                    "response": {
    +                        "candidates": [{
    +                            "content": {"parts": [{"text": "ok"}]},
    +                            "finishReason": "STOP",
    +                        }]
    +                    }
    +                }
    +
    +        class _Http:
    +            def __init__(self):
    +                self.calls = []
    +
    +            def post(self, url, json=None, headers=None):
    +                self.calls.append((url, json, headers))
    +                return _Response()
    +
    +            def close(self):
    +                pass
    +
    +        client = AntigravityCloudCodeClient(project_id="proj-123")
    +        client._http = _Http()
    +        try:
    +            result = client.chat.completions.create(
    +                model="gemini-3-flash-agent",
    +                messages=[{"role": "user", "content": "hi"}],
    +            )
    +        finally:
    +            client.close()
    +
    +        assert result.choices[0].message.content == "ok"
    +        url, body, headers = client._http.calls[0]
    +        assert url == f"{ANTIGRAVITY_CODE_ASSIST_ENDPOINT}/v1internal:generateContent"
    +        assert body["project"] == "proj-123"
    +        assert body["model"] == "gemini-3-flash-agent"
    +        assert headers["Authorization"] == "Bearer live-token"
    +        assert json.loads(headers["Client-Metadata"])["ideType"] == "ANTIGRAVITY"
    +
    +
    +class TestAntigravityRegistration:
    +    def test_registry_entry_and_aliases(self):
    +        from hermes_cli.auth import PROVIDER_REGISTRY, resolve_provider
    +
    +        assert "google-antigravity" in PROVIDER_REGISTRY
    +        assert PROVIDER_REGISTRY["google-antigravity"].auth_type == "oauth_external"
    +        assert resolve_provider("antigravity") == "google-antigravity"
    +        assert resolve_provider("antigravity-oauth") == "google-antigravity"
    +        assert resolve_provider("google-antigravity-oauth") == "google-antigravity"
    +        assert resolve_provider("agy") == "google-antigravity"
    +
    +    def test_runtime_provider_raises_when_not_logged_in(self):
    +        from hermes_cli.auth import AuthError
    +        from hermes_cli.runtime_provider import resolve_runtime_provider
    +
    +        with pytest.raises(AuthError) as exc_info:
    +            resolve_runtime_provider(requested="google-antigravity")
    +        assert exc_info.value.code == "antigravity_oauth_not_logged_in"
    +
    +    def test_runtime_provider_returns_correct_shape_when_logged_in(self):
    +        from agent.antigravity_oauth import AntigravityCredentials, save_credentials
    +        from hermes_cli.runtime_provider import resolve_runtime_provider
    +
    +        save_credentials(AntigravityCredentials(
    +            access_token="[redacted:secret-assignment]",
    +            refresh_token="rt",
    +            expires_ms=int((time.time() + 3600) * 1000),
    +            project_id="my-proj",
    +            email="[redacted:email]",
    +        ))
    +
    +        result = resolve_runtime_provider(requested="google-antigravity")
    +        assert result["provider"] == "google-antigravity"
    +        assert result["api_mode"] == "chat_completions"
    +        assert result["api_key"] == "live-tok"
    +        assert result["base_url"] == "antigravity-pa://google"
    +        assert result["project_id"] == "my-proj"
    +        assert result["email"] == "[redacted:email]"
    +
    +    def test_provider_model_ids_uses_live_antigravity_catalog(self, monkeypatch):
    +        from hermes_cli import models
    +
    +        monkeypatch.setattr(
    +            models,
    +            "_fetch_antigravity_models",
    +            lambda force_refresh=False: ["gemini-3-flash-agent", "claude-sonnet-4-6"],
    +        )
    +
    +        assert models.provider_model_ids("agy") == [
    +            "gemini-3-flash-agent",
    +            "claude-sonnet-4-6",
    +        ]
    +
    +    def test_oauth_capable_set_includes_antigravity(self):
    +        from hermes_cli.auth_commands import _OAUTH_CAPABLE_PROVIDERS
    +
    +        assert "google-antigravity" in _OAUTH_CAPABLE_PROVIDERS
    ```
  - `tests/agent/test_gemini_cloudcode.py`

    ```diff
    @@ -610,6 +610,7 @@ def test_tool_call_translation(self):
             fc_part = next(p for p in model_turn["parts"] if "functionCall" in p)
             assert fc_part["functionCall"]["name"] == "get_weather"
             assert fc_part["functionCall"]["args"] == {"city": "SF"}
    +        assert fc_part["functionCall"]["id"] == "call_1"
     
         def test_tool_result_translation(self):
             from agent.gemini_cloudcode_adapter import build_gemini_request
    ```

    ```diff
    @@ -632,6 +633,7 @@ def test_tool_result_translation(self):
             fr_part = next(p for p in last["parts"] if "functionResponse" in p)
             assert fr_part["functionResponse"]["name"] == "get_weather"
             assert fr_part["functionResponse"]["response"] == {"temp": 72}
    +        assert fr_part["functionResponse"]["id"] == "c1"
     
         def test_tools_translated_to_function_declarations(self):
             from agent.gemini_cloudcode_adapter import build_gemini_request
    ```

    ```diff
    @@ -790,7 +792,7 @@ def test_function_call_response(self):
                 "response": {
                     "candidates": [{
                         "content": {"parts": [{
    -                        "functionCall": {"name": "lookup", "args": {"q": "weather"}},
    +                        "functionCall": {"name": "lookup", "args": {"q": "weather"}, "id": "provider-call-1"},
                         }]},
                         "finishReason": "STOP",
                     }],
    ```

    ```diff
    @@ -798,6 +800,7 @@ def test_function_call_response(self):
             }
             result = _translate_gemini_response(resp, model="gemini-2.5-flash")
             tc = result.choices[0].message.tool_calls[0]
    +        assert tc.id == "provider-call-1"
             assert tc.function.name == "lookup"
             assert json.loads(tc.function.arguments) == {"q": "weather"}
             assert result.choices[0].finish_reason == "tool_calls"
    ```
  - `tests/agent/transports/test_chat_completions.py`

    ```diff
    @@ -418,6 +418,20 @@ def test_google_gemini_cli_keeps_top_level_thinking_config(self, transport):
             }
             assert "google" not in kw["extra_body"]
     
    +    def test_google_antigravity_keeps_top_level_thinking_config(self, transport):
    +        msgs = [{"role": "user", "content": "Hi"}]
    +        kw = transport.build_kwargs(
    +            model="gemini-3-flash-agent",
    +            messages=msgs,
    +            provider_name="google-antigravity",
    +            reasoning_config={"enabled": True, "effort": "high"},
    +        )
    +        assert kw["extra_body"]["thinking_config"] == {
    +            "includeThoughts": True,
    +            "thinkingLevel": "high",
    +        }
    +        assert "google" not in kw["extra_body"]
    +
         def test_gemini_flash_minimal_clamps_to_low(self, transport):
             # Gemini 3 Flash documents low/medium/high; "minimal" isn't accepted,
             # so clamp it down to "low" rather than forwarding it verbatim.
    ```
  - `tests/hermes_cli/test_model_provider_persistence.py`

    ```diff
    @@ -316,6 +316,41 @@ def test_opencode_go_same_provider_switch_recomputes_api_mode(self, config_home,
             assert model.get("default") == "minimax-m2.5"
             assert model.get("api_mode") == "anthropic_messages"
     
    +    def test_antigravity_oauth_provider_saved_when_selected(self, config_home):
    +        """_model_flow_google_antigravity should persist provider/base_url/model together."""
    +        from hermes_cli.main import _model_flow_google_antigravity
    +        from hermes_cli.config import load_config
    +
    +        with patch(
    +            "hermes_cli.auth.get_antigravity_oauth_auth_status",
    +            return_value={"logged_in": True, "email": "[redacted:email]"},
    +        ), patch(
    +            "hermes_cli.auth.resolve_antigravity_oauth_runtime_credentials",
    +            return_value={
    +                "provider": "google-antigravity",
    +                "api_key": "tok",
    +                "base_url": "antigravity-pa://google",
    +                "project_id": "proj-123",
    +            },
    +        ), patch(
    +            "hermes_cli.models.provider_model_ids",
    +            return_value=["gemini-3-flash-agent", "claude-sonnet-4-6"],
    +        ), patch(
    +            "hermes_cli.auth._prompt_model_selection",
    +            return_value="claude-sonnet-4-6",
    +        ):
    +            _model_flow_google_antigravity(load_config(), "old-model")
    +
    +        import yaml
    +
    +        config = yaml.safe_load((config_home / "config.yaml").read_text()) or {}
    +        model = config.get("model")
    +        assert isinstance(model, dict), f"model should be dict, got {type(model)}"
    +        assert model.get("provider") == "google-antigravity"
    +        assert model.get("base_url") == "antigravity-pa://google"
    +        assert model.get("default") == "claude-sonnet-4-6"
    +        assert "api_mode" not in model
    +
     
     
     class TestBaseUrlValidation:
    ```
  - `website/docs/developer-guide/provider-runtime.md`

    ```diff
    @@ -47,7 +47,7 @@ Current provider families include (see `plugins/model-providers/` for the comple
     - OpenAI Codex
     - Copilot / Copilot ACP
     - Anthropic (native)
    -- Google / Gemini (`gemini`, `google-gemini-cli`)
    +- Google / Gemini (`gemini`, `google-gemini-cli`, `google-antigravity`)
     - Alibaba / DashScope (`alibaba`, `alibaba-coding-plan`)
     - DeepSeek
     - Z.AI
    ```
  - `website/docs/guides/google-gemini.md`

    ```diff
    @@ -111,6 +111,19 @@ hermes model
     
     This uses browser PKCE login and the Cloud Code Assist backend. It can be useful for users who want Gemini CLI-style OAuth, but Hermes shows an explicit warning because Google may treat use of the Gemini CLI OAuth client from third-party software as a policy violation. For production or lowest-risk usage, prefer the API-key provider above.
     
    +Hermes also supports `google-antigravity` for Antigravity Code Assist:
    +
    +```bash
    +hermes model
    +# → Choose "Google Antigravity (OAuth)"
    +```
    +
    +That provider uses a separate Antigravity OAuth login and stores separate
    +credentials at `~/.hermes/auth/antigravity_oauth.json`. Its model picker uses
    +live Antigravity model discovery, so the list reflects the signed-in account's
    +subscription and can include Antigravity-only Gemini agent models plus other
    +entitled model families.
    +
     ## Available Models
     
     The `hermes model` picker shows Gemini models maintained in Hermes' provider registry. Common choices include:
    ```

    ```diff
    @@ -193,6 +206,7 @@ The doctor checks:
     
     - Whether `GOOGLE_API_KEY` or `GEMINI_API_KEY` is available
     - Whether Gemini OAuth credentials exist for `google-gemini-cli`
    +- Whether Antigravity OAuth credentials exist for `google-antigravity`
     - Whether configured provider credentials can be resolved
     
     For OAuth quota usage, run this inside a Hermes session:
    ```
  - `website/docs/integrations/providers.md`

    ```diff
    @@ -49,6 +49,7 @@ You need at least one way to connect to an LLM. Use `hermes model` to switch pro
     | **Qwen OAuth** | `hermes model` → "Qwen OAuth" (provider: `qwen-oauth`; browser PKCE login) |
     | **MiniMax OAuth** | `hermes model` → "MiniMax (OAuth)" (provider: `minimax-oauth`; browser PKCE login) |
     | **StepFun** | `STEPFUN_API_KEY` in `~/.hermes/.env` (provider: `stepfun`) |
    +| **Google Antigravity (OAuth)** | `hermes model` → "Google Antigravity (OAuth)" (provider: `google-antigravity`, aliases: `antigravity`, `antigravity-oauth`, `agy`) |
     | **LM Studio** | `hermes model` → "LM Studio" (provider: `lmstudio`, optional `LM_API_KEY`) |
     | **Custom Endpoint** | `hermes model` → choose "Custom endpoint" (saved in `config.yaml`) |
     
    ```

    ```diff
    @@ -78,6 +79,64 @@ Don't have a subscription yet? Get one at [portal.nousresearch.com/manage-subscr
     **JWT auth (automatic).** Hermes prefers scoped `inference:invoke` JWTs for Portal requests with the legacy opaque session-key path as a fallback. No configuration is required — credentials are managed by the OAuth flow and rotate transparently. Revoked refresh tokens are quarantined to avoid replay loops.
     
     
    +### Google Antigravity via OAuth (`google-antigravity`)
    +
    +The `google-antigravity` provider uses Antigravity's Code Assist backend and
    +Antigravity OAuth scopes. It is a native Hermes integration: Hermes runs its
    +own browser PKCE login, stores credentials under
    +`~/.hermes/auth/antigravity_oauth.json`, and talks directly to the Antigravity
    +Code Assist endpoints. It does not shell out to `agy` for inference, and it
    +does not depend on the Antigravity CLI's local token storage.
    +
    +**Quick start:**
    +
    +```bash
    +hermes model
    +# -> pick "Google Antigravity (OAuth)"
    +# -> browser opens to accounts.google.com, sign in
    +# -> pick one of the models available to your Antigravity account
    +```
    +
    +Hermes discovers Antigravity models from `fetchAvailableModels` after login.
    +The visible list depends on the authenticated account and subscription, and can
    +include Antigravity-only Gemini agent models plus Claude and GPT-OSS entries
    +when the account is entitled. If live discovery fails, Hermes falls back to a
    +small curated list so the provider remains selectable.
    +
    +Supported aliases:
    +
    +```text
    +google-antigravity
    +google-antigravity-oauth
    +antigravity
    +antigravity-oauth
    +antigravity-cli
    +agy
    +agy-cli
    +```
    +
    +Optional overrides:
    +
    +```bash
    +HERMES_ANTIGRAVITY_CLIENT_ID=your-client.apps.googleusercontent.com
    +HERMES_ANTIGRAVITY_CLIENT_SECRET=...
    +HERMES_ANTIGRAVITY_CLI_PATH=/path/to/agy
    +HERMES_ANTIGRAVITY_PROJECT_ID=your-project
    +```
    +
    +If the client ID/secret are not set explicitly, Hermes tries to discover the
    +desktop OAuth client credentials from the installed Antigravity CLI (`agy`) on
    +`PATH`, `HERMES_ANTIGRAVITY_CLI_PATH`, or common Antigravity install/cache
    +locations. Those client credentials are used only to start and refresh Hermes'
    +own OAuth session; Hermes still keeps its access/refresh tokens in `~/.hermes`.
    +
    +:::note Windows credential storage
    +The Antigravity CLI may keep its own login in platform-specific storage such as
    +Windows Credential Manager. Hermes intentionally keeps separate credentials in
    +`~/.hermes` so development profiles and production Hermes profiles do not share
    +tokens accidentally.
    +:::
    +
     :::info Codex Note
     The OpenAI Codex provider authenticates via device code (open a URL, enter a code). Hermes stores the resulting credentials in its own auth store under `~/.hermes/auth.json` and can import existing Codex CLI credentials from `~/.codex/auth.json` when present. No Codex CLI installation is required.
     
    ```

    ```diff
    @@ -1532,7 +1591,7 @@ fallback_model:
     
     When activated, the fallback swaps the model and provider mid-session without losing your conversation. The chain is tried entry-by-entry; activation is one-shot per session.
     
    -Supported providers: `openrouter`, `nous`, `novita`, `openai-codex`, `copilot`, `copilot-acp`, `anthropic`, `gemini`, `google-gemini-cli`, `qwen-oauth`, `huggingface`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `minimax-oauth`, `deepseek`, `nvidia`, `xai`, `xai-oauth`, `ollama-cloud`, `bedrock`, `azure-foundry`, `opencode-zen`, `opencode-go`, `kilocode`, `xiaomi`, `arcee`, `gmi`, `stepfun`, `lmstudio`, `alibaba`, `alibaba-coding-plan`, `tencent-tokenhub`, `custom`.
    +Supported providers: `openrouter`, `nous`, `novita`, `openai-codex`, `copilot`, `copilot-acp`, `anthropic`, `gemini`, `google-gemini-cli`, `google-antigravity`, `qwen-oauth`, `huggingface`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `minimax-oauth`, `deepseek`, `nvidia`, `xai`, `xai-oauth`, `ollama-cloud`, `bedrock`, `azure-foundry`, `opencode-zen`, `opencode-go`, `kilocode`, `xiaomi`, `arcee`, `gmi`, `stepfun`, `lmstudio`, `alibaba`, `alibaba-coding-plan`, `tencent-tokenhub`, `custom`.
     
     :::tip
     Fallback is configured exclusively through `config.yaml` — or interactively via `hermes fallback`. For full details on when it triggers, how the chain advances, and how it interacts with auxiliary tasks and delegation, see [Fallback Providers](/user-guide/features/fallback-providers).
    ```
  - `website/docs/reference/cli-commands.md`

    ```diff
    @@ -100,7 +100,7 @@ Common options:
     | `-q`, `--query "..."` | One-shot, non-interactive prompt. |
     | `-m`, `--model <model>` | Override the model for this run. |
     | `-t`, `--toolsets <csv>` | Enable a comma-separated set of toolsets. |
    -| `--provider <provider>` | Force a provider: `auto`, `openrouter`, `nous`, `openai-codex`, `copilot-acp`, `copilot`, `anthropic`, `gemini`, `google-gemini-cli`, `huggingface`, `novita` (aliases `novita-ai`, `novitaai`), `openai-api`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `minimax-oauth`, `kilocode`, `xiaomi`, `arcee`, `gmi`, `alibaba`, `alibaba-coding-plan` (alias `alibaba_coding`), `deepseek`, `nvidia`, `ollama-cloud`, `xai` (alias `grok`), `xai-oauth` (alias `grok-oauth`), `qwen-oauth`, `bedrock`, `opencode-zen`, `opencode-go`, `azure-foundry`, `lmstudio`, `stepfun`, `tencent-tokenhub` (alias `tencent`, `tokenhub`). |
    +| `--provider <provider>` | Force a provider: `auto`, `openrouter`, `nous`, `openai-codex`, `copilot-acp`, `copilot`, `anthropic`, `gemini`, `google-gemini-cli`, `google-antigravity` (aliases: `antigravity`, `antigravity-oauth`, `agy`), `huggingface`, `novita` (aliases `novita-ai`, `novitaai`), `openai-api`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `minimax-oauth`, `kilocode`, `xiaomi`, `arcee`, `gmi`, `alibaba`, `alibaba-coding-plan` (alias `alibaba_coding`), `deepseek`, `nvidia`, `ollama-cloud`, `xai` (alias `grok`), `xai-oauth` (alias `grok-oauth`), `qwen-oauth`, `bedrock`, `opencode-zen`, `opencode-go`, `azure-foundry`, `lmstudio`, `stepfun`, `tencent-tokenhub` (alias `tencent`, `tokenhub`). |
     | `-s`, `--skills <name>` | Preload one or more skills for the session (can be repeated or comma-separated). |
     | `-v`, `--verbose` | Verbose output. |
     | `-Q`, `--quiet` | Programmatic mode: suppress banner/spinner/tool previews. |
    ```
  - `website/docs/reference/environment-variables.md`

    ```diff
    @@ -70,6 +70,10 @@ Hermes reads environment variables from the process environment and, for user-ma
     | `HERMES_GEMINI_CLIENT_ID` | OAuth client ID for `google-gemini-cli` PKCE login (optional; defaults to Google's public gemini-cli client) |
     | `HERMES_GEMINI_CLIENT_SECRET` | OAuth client secret for `google-gemini-cli` (optional) |
     | `HERMES_GEMINI_PROJECT_ID` | GCP project ID for paid Gemini tiers (free tier auto-provisions) |
    +| `HERMES_ANTIGRAVITY_CLIENT_ID` | OAuth client ID for `google-antigravity` PKCE login (optional; discovered from installed `agy` when omitted) |
    +| `HERMES_ANTIGRAVITY_CLIENT_SECRET` | OAuth client secret for `google-antigravity` (optional; discovered from installed `agy` when omitted) |
    +| `HERMES_ANTIGRAVITY_CLI_PATH` | Path to the `agy` executable or install file used for Antigravity OAuth client credential discovery |
    +| `HERMES_ANTIGRAVITY_PROJECT_ID` | GCP project ID for Antigravity Code Assist when you want to pin one explicitly |
     | `ANTHROPIC_API_KEY` | Anthropic Console API key ([console.anthropic.com](https://console.anthropic.com/)) |
     | `ANTHROPIC_BASE_URL` | Override the Anthropic API base URL |
     | `ANTHROPIC_TOKEN` | Manual or legacy Anthropic OAuth/setup-token override |
    ```
  - `website/docs/reference/faq.md`

    ```diff
    @@ -20,7 +20,7 @@ Hermes Agent works with any OpenAI-compatible API. Supported providers include:
     - **[Nous Portal](/integrations/nous-portal)** — Nous Research's subscription gateway — 300+ models plus web/image/TTS/browser through one OAuth login (recommended for newcomers)
     - **OpenAI** — GPT-5.4, GPT-5-codex, GPT-4.1, GPT-4o, etc.
     - **Anthropic** — Claude models (direct API, OAuth via `hermes auth add anthropic`, OpenRouter, or any compatible proxy)
    -- **Google** — Gemini models (direct API via `gemini` provider, the `google-gemini-cli` OAuth provider, OpenRouter, or compatible proxy)
    +- **Google** — Gemini models (direct API via `gemini` provider, the `google-gemini-cli` OAuth provider, the `google-antigravity` OAuth provider, OpenRouter, or compatible proxy)
     - **z.ai / ZhipuAI** — GLM models
     - **Kimi / Moonshot AI** — Kimi models
     - **MiniMax** — global and China endpoints
    ```
  - `website/docs/user-guide/configuration.md`

    ```diff
    @@ -959,7 +959,7 @@ Every model slot in Hermes — auxiliary tasks, compression, fallback — uses t
     
     When `base_url` is set, Hermes ignores the provider and calls that endpoint directly (using `api_key` or `OPENAI_API_KEY` for auth). When only `provider` is set, Hermes uses that provider's built-in auth and base URL.
     
    -Available providers for auxiliary tasks: `auto`, `main`, plus any provider in the [provider registry](/reference/environment-variables) — `openrouter`, `nous`, `openai-codex`, `copilot`, `copilot-acp`, `anthropic`, `gemini`, `google-gemini-cli`, `qwen-oauth`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `minimax-oauth`, `deepseek`, `nvidia`, `xai`, `xai-oauth`, `ollama-cloud`, `alibaba`, `bedrock`, `huggingface`, `arcee`, `xiaomi`, `kilocode`, `opencode-zen`, `opencode-go`, `azure-foundry` — or any named custom provider from your `custom_providers` list (e.g. `provider: "beans"`).
    +Available providers for auxiliary tasks: `auto`, `main`, plus any provider in the [provider registry](/reference/environment-variables) — `openrouter`, `nous`, `openai-codex`, `copilot`, `copilot-acp`, `anthropic`, `gemini`, `google-gemini-cli`, `google-antigravity`, `qwen-oauth`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `minimax-oauth`, `deepseek`, `nvidia`, `xai`, `xai-oauth`, `ollama-cloud`, `alibaba`, `bedrock`, `huggingface`, `arcee`, `xiaomi`, `kilocode`, `opencode-zen`, `opencode-go`, `azure-foundry` — or any named custom provider from your `custom_providers` list (e.g. `provider: "beans"`).
     
     :::tip MiniMax OAuth
     `minimax-oauth` logs in via browser OAuth (no API key needed). Run `hermes model` and select **MiniMax (OAuth)** to authenticate. Auxiliary tasks use `MiniMax-M2.7-highspeed` automatically. See the [MiniMax OAuth guide](../guides/minimax-oauth.md).
    ```
  - `website/docs/user-guide/features/fallback-providers.md`

    ```diff
    @@ -63,6 +63,7 @@ Each entry requires both `provider` and `model`. Entries missing either field ar
     | StepFun | `stepfun` | `STEPFUN_API_KEY` (optional: `STEPFUN_BASE_URL`) |
     | Ollama Cloud | `ollama-cloud` | `OLLAMA_API_KEY` |
     | Google Gemini (OAuth) | `google-gemini-cli` | `hermes model` (Google OAuth; optional: `HERMES_GEMINI_PROJECT_ID`) |
    +| Google Antigravity (OAuth) | `google-antigravity` | `hermes model` (Antigravity OAuth; optional: `HERMES_ANTIGRAVITY_PROJECT_ID`) |
     | Google AI Studio | `gemini` | `GOOGLE_API_KEY` (alias: `GEMINI_API_KEY`) |
     | xAI (Grok) | `xai` (alias `grok`) | `XAI_API_KEY` (optional: `XAI_BASE_URL`) |
     | xAI Grok OAuth (SuperGrok) | `xai-oauth` (alias `grok-oauth`) | `hermes model` → xAI Grok OAuth (browser login; SuperGrok subscription) |
    ```
- **#995: feat: add Google Gemini and Hugging Face as inference providers**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`
- **#13392: fix(gemini-cli): add gemini-2.5 models to google-gemini-cli catalog and improve 404 error message**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `7130d60861a9243301514bff611a9381830d59d8`, `7130d6086`, `#50492`
- **#29474: feat(cli): add native Antigravity OAuth provider**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `8baa4e997`, `#50454`
- **#54924: docs(google-gemini): explain google-gemini-cli removal and migration to gemini**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`

## Fix status

- Merged to main in PR #50454 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-21`.
- Subsystem: `agent`
