Claude Skill

cc-agy

Delegates coding/research tasks to the Google Antigravity CLI (`agy`) for external-model execution (Gemini 3.x, Claude Sonnet/Opus 4.6, GPT-OSS). Replaces the broken `collaborating-with-gemini` skill. Use when: (1) External-model delegation via Antigravity, (2) Multi-model protot

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download Dianel555-DSkills-skills_cc-agy-d2fda23.zip · 15 KB
Part of dianel555/dskills — 14 skills

Install

skills CLI npx skills add https://github.com/Dianel555/DSkills/tree/main/skills/cc-agy
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install dianel555-dskills@llmmart
Git git clone https://github.com/Dianel555/DSkills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole dianel555/dskills collection as a plugin from our marketplace. Git is the plain clone.

README

cc-agy

A Claude Code Agent Skill that bridges Claude with the Google Antigravity CLI (agy) for external-model delegation(Gemini CLI stopped working for person).

Overview

This Skill lets Claude delegate coding/research tasks to agy, which runs external models — Gemini 3.x, Claude Sonnet/Opus 4.6, GPT-OSS — with their own configured MCP servers, skills, and memory doc. Claude orchestrates the workflow and refines the output; agy executes on the external model.

agy --print writes nothing to stdout, so the bridge instead discovers the conversation SQLite DB that agy persists and extracts the assistant reply from its protobuf payload. When a run fails upstream, agy records the executor error in a separate step_type=17 row instead of any reply text; the bridge reads that row and reports the real cause. MCP (~/.gemini/antigravity/mcp_config.json), the memory doc (~/.gemini/GEMINI.md), and skills are pre-configured by the user and auto-loaded by agy — the bridge does not manage them.

Features

  • Multi-turn sessions: Resume conversations via SESSION_ID (maps to agy --conversation)
  • Multi-model prototyping: Switch model per call with --model aliases (flash, pro, sonnet, opus, gpt-oss)
  • JSON output: Structured responses isomorphic to gemini_bridge.py
  • Cross-platform: Windows path / UTF-8 handled automatically
  • Setup probe: check subcommand reports agy install / version / auth / current model
  • Plugin passthrough: plugin subcommand forwards to agy plugin list|import|install|enable|disable

Prerequisites

  1. Install the Antigravity CLI:
    curl -fsSL https://antigravity.google/cli/install.sh | bash
    
  2. Authenticate: run agy once interactively (browser OAuth), or export ANTIGRAVITY_API_KEY.
  3. Pick a default model: open agy and use /model, which writes ~/.gemini/antigravity-cli/settings.json.

Verify with:

python scripts/agy_bridge.py check

Installation

Copy this Skill to your Claude Code skills directory:

  • User-level: ~/.claude/skills/cc-agy/
  • Project-level: .claude/skills/cc-agy/

Or, in the DSkills marketplace, it is registered in .claude-plugin/marketplace.json as the cc-agy plugin.

Usage

Basic

python scripts/agy_bridge.py --cd "/path/to/project" --PROMPT "Analyze the authentication flow"

Multi-turn Session

# Start a session
python scripts/agy_bridge.py --cd "/project" --PROMPT "Review login.py for security issues"
# Response includes SESSION_ID

# Continue the session
python scripts/agy_bridge.py --cd "/project" --SESSION_ID "uuid-from-response" --PROMPT "Suggest fixes for the issues found"

Switch Model Per Call

python scripts/agy_bridge.py --cd "/project" --PROMPT "Review this Rust" --model sonnet
python scripts/agy_bridge.py --cd "/project" --PROMPT "Same code" --model opus

Parameters

Parameter Required Description
--PROMPT Yes Task instruction
--cd Yes Workspace root (agy cwd + --add-dir)
--model No Model alias or canonical string; omit for settings default
--SESSION_ID No Resume a conversation by UUID (maps to --conversation)
--sandbox No Run in agy sandbox mode
--no-skip-permissions No Do NOT pass --dangerously-skip-permissions (WARNING: hangs print mode)
--print-timeout No agy --print-timeout (e.g. 5m, 10m); default 10m
--return-all-messages No Include reasoning + all type=15 steps

Model aliases: flash-low/medium/high, pro-low/high, sonnet, opus, gpt-oss.

Subcommands

Subcommand Description
check Probe agy install / version / auth / current model
plugin Thin passthrough to agy plugin list\|import\|install\|enable\|disable

Output Format

{
  "success": true,
  "SESSION_ID": "uuid",
  "agent_messages": "agy response text"
}

When agy produces no reply, error carries agy's own upstream failure rather than a guess about the bridge:

{
  "success": false,
  "SESSION_ID": "uuid",
  "error": "agy produced no reply because the run failed upstream (rc=1): FAILED_PRECONDITION (code 400): User location is not supported for the API use.",
  "steps_file": "/tmp/agy_steps_xxxx.jsonl"
}

Upstream failures are often transient (geo/egress-IP rejection, model capacity exhausted). The bridge does not retry — the status detail tells the caller whether to wait or switch --model.

Security Note

By default the bridge passes --dangerously-skip-permissions to agy. This is mandatory for non-interactive --print mode: agy's default toolPermission is request-review, which blocks waiting for a human to approve tool calls, and --print captures no TTY — so the process hangs until timeout. With --dangerously-skip-permissions, agy auto-approves all tool calls. The bridge always runs under a hard outer timeout (--print-timeout + 60s) so a hung agy cannot block indefinitely. Only use --no-skip-permissions in a context where interactive permission prompts can be serviced.

Known Limitations

  • Protobuf extraction is schema-dependent. The reply is read from field f1 inside field f20 of step_type=15 rows; upstream errors from f24 → f3 of step_type=17 rows. If agy changes its internal schema, extraction returns empty. Fix location: extract_answer() (replies) or extract_run_error() (errors) in scripts/agy_bridge.py. The reported error names which one to fix, so trust it over guessing.
  • agy models returns empty on this build; model aliases are hardcoded.
  • --continue is intentionally not exposed (target selection is opaque); use --SESSION_ID to resume a specific conversation.

License

MIT License. See LICENSE for details.

Skill manifest

Quick Start

python scripts/agy_bridge.py --cd "/path/to/project" --PROMPT "Your task"

Output: JSON with success, SESSION_ID, agent_messages, steps_file, and optional error / note / stderr. On failure, error names the upstream cause agy recorded (e.g. a 400/503 status) when one exists.

Parameters

usage: agy_bridge.py [-h] --PROMPT PROMPT --cd CD
                     [--model MODEL] [--SESSION_ID SESSION_ID]
                     [--sandbox] [--no-skip-permissions]
                     [--print-timeout PRINT_TIMEOUT]
                     [--return-all-messages]
                     {check,plugin} ...

Antigravity (agy) Bridge

options:
  -h, --help            show this help message and exit
  --PROMPT PROMPT       Instruction for the task to send to agy.
  --cd CD               Workspace root for agy (sets cwd + --add-dir).
  --model MODEL         Model alias (flash-low/medium/high, pro-low/high, sonnet,
                        opus, gpt-oss) or canonical string. Omit to use the
                        settings.json default.
  --SESSION_ID SESSION_ID
                        Resume a conversation by UUID. Maps to agy --conversation.
  --sandbox             Run in agy sandbox mode.
  --no-skip-permissions
                        Do NOT pass --dangerously-skip-permissions. WARNING:
                        with default toolPermission=request-review, print mode
                        WILL HANG. Only for interactive review workflows.
  --print-timeout PRINT_TIMEOUT
                        agy --print-timeout (e.g. 5m, 10m). Default 10m.
  --return-all-messages
                        Include reasoning + all type=15 steps in the response.

subcommands:
  check                 Probe agy install / version / auth / current model.
  plugin                Thin passthrough to `agy plugin list|import|install|enable|disable`.

Multi-turn Sessions

Always capture SESSION_ID from the first response for follow-up (maps to agy --conversation <UUID>, which appends to the same conversation DB):

# Initial task
python scripts/agy_bridge.py --cd "/project" --PROMPT "Analyze auth in login.py"

# Continue with SESSION_ID
python scripts/agy_bridge.py --cd "/project" --SESSION_ID "uuid-from-response" --PROMPT "Write unit tests for that"

Common Patterns

Prototyping (request diffs):

python scripts/agy_bridge.py --cd "/project" --PROMPT "Generate unified diff to add logging" --model pro

Switch model per call:

python scripts/agy_bridge.py --cd "/project" --PROMPT "Review this Rust" --model sonnet
python scripts/agy_bridge.py --cd "/project" --PROMPT "Same code" --model opus

Setup check:

python scripts/agy_bridge.py check

Manage skills/plugins (passthrough to agy):

python scripts/agy_bridge.py plugin list
python scripts/agy_bridge.py plugin import /path/to/plugin

How It Works

agy --print writes nothing to stdout. The bridge instead runs agy, discovers the new conversation SQLite DB at ~/.gemini/antigravity-cli/conversations/<UUID>.db, and extracts the assistant reply from the step_type=15 rows' step_payload protobuf (field f20 → f1). MCP servers (~/.gemini/antigravity/mcp_config.json), the memory doc (~/.gemini/GEMINI.md), and skills are pre-configured by the user and auto-loaded by agy — the bridge does not manage them.

When a run fails upstream, agy writes no reply text at all and instead records the executor error in a step_type=17 row (f24 → f3: f1 user-facing line, f2 status detail). The bridge reads that row and reports the real cause — e.g. FAILED_PRECONDITION (code 400): User location is not supported for the API use. or UNAVAILABLE (code 503): No capacity available for model ... — instead of blaming its own parsing. stderr alone carries only agy's generic Agent execution terminated due to error. and no status code.

On resume, both extractors window on idx > after_idx, where the boundary spans all step types: a failed run's type=17 row lands after its last type=15 step, so a type=15-only boundary would re-attribute that stale error to the next run.

The bridge does not retry. A 400 geo rejection will almost certainly fail again on an immediate retry, and 503 capacity exhaustion is better answered by switching --model than by replaying — which could also repeat tool side effects. The structured error is passed through so the caller decides.

Security Note

By default the bridge passes --dangerously-skip-permissions to agy. This is mandatory for non-interactive --print mode because agy's default toolPermission is request-review, which blocks waiting for a human to approve tool calls — and since --print captures no TTY, the process hangs until timeout. With --dangerously-skip-permissions, agy auto-approves all tool calls. Only pass --no-skip-permissions if agy can service interactive permission prompts. The bridge always runs under a hard outer timeout (--print-timeout + 60s) so a hung agy cannot block indefinitely.

Known Limitations

  • Protobuf extraction is schema-dependent. The bridge parses agy's conversation DB without a .proto file: replies from f20 → f1 of step_type=15 rows, upstream errors from f24 → f3 of step_type=17 rows. If agy changes its internal schema, extraction returns empty. The reported error distinguishes the two cases — a message naming an upstream status means agy failed and the bridge worked; the schema-drift hint appears only when no error row explains the empty reply. Fix locations: extract_answer() and extract_run_error() in scripts/agy_bridge.py.
  • agy models returns empty on this build; model aliases are hardcoded.
  • --continue is intentionally NOT exposed (target selection is opaque); use --SESSION_ID to resume a specific conversation.
Files (dskills)
  • scripts
    • agy_bridge.py 18.8 KB
      """
      Antigravity (agy) Bridge for Claude Agent Skills.
      Wraps the Google Antigravity CLI (`agy`) to provide a JSON-based interface.
      
      agy `--print` writes nothing to stdout; the assistant reply is persisted in a
      SQLite conversation DB at ~/.gemini/antigravity-cli/conversations/<UUID>.db,
      inside the last step_type=15 row's step_payload protobuf (field f20 -> f1).
      When a run fails upstream, agy writes no reply text and instead records the
      executor error in a step_type=17 row (f24 -> f3: f1 user-facing line, f2
      status detail).
      This bridge runs agy, discovers the conversation DB, extracts the reply, and
      returns JSON isomorphic to gemini_bridge.py.
      """
      
      import contextlib
      import json
      import os
      import re
      import shutil
      import sqlite3
      import subprocess
      import sys
      import tempfile
      from pathlib import Path
      
      CONVERSATIONS_DIR = Path.home() / ".gemini" / "antigravity-cli" / "conversations"
      SETTINGS_FILE = Path.home() / ".gemini" / "antigravity-cli" / "settings.json"
      
      AGY_BIN_CANDIDATES = [
          "agy",
          str(Path.home() / "AppData/Local/agy/bin/agy.exe"),
          str(Path.home() / "AppData/Local/agy/bin/agy"),
          str(Path.home() / ".local/bin/agy"),
          "/opt/antigravity/bin/agy",
          "/usr/local/bin/agy",
      ]
      
      MODEL_ALIASES = {
          "flash-low": "Gemini 3.5 Flash (Low)",
          "flash-medium": "Gemini 3.5 Flash (Medium)",
          "flash-med": "Gemini 3.5 Flash (Medium)",
          "flash": "Gemini 3.5 Flash (High)",
          "flash-high": "Gemini 3.5 Flash (High)",
          "pro-low": "Gemini 3.1 Pro (Low)",
          "pro": "Gemini 3.1 Pro (High)",
          "pro-high": "Gemini 3.1 Pro (High)",
          "sonnet": "Claude Sonnet 4.6 (Thinking)",
          "claude-sonnet": "Claude Sonnet 4.6 (Thinking)",
          "opus": "Claude Opus 4.6 (Thinking)",
          "claude-opus": "Claude Opus 4.6 (Thinking)",
          "gpt-oss": "GPT-OSS 120B (Medium)",
          "gpt-oss-120b": "GPT-OSS 120B (Medium)",
      }
      CANONICAL_MODELS = set(MODEL_ALIASES.values())
      
      INSTALL_HINT = "install with: curl -fsSL https://antigravity.google/cli/install.sh | bash"
      
      OUTPUT_PROTOCOL = (
          "\n\nOUTPUT PROTOCOL: The complete deliverable (full code, analysis, or "
          "report) MUST appear in your final text reply. Tool receipts, status "
          "messages like 'WROTE <n>', and brief acknowledgements alone are NOT "
          "valid final answers."
      )
      
      
      # --- protobuf parsing (verified against agy v1.0.10 conversation DBs) ---
      
      
      def read_varint(b: bytes, i: int) -> tuple[int, int]:
          shift = val = 0
          while i < len(b):
              c = b[i]
              i += 1
              val |= (c & 0x7F) << shift
              if not (c & 0x80):
                  break
              shift += 7
          return val, i
      
      
      def scan_protobuf(b: bytes) -> list[tuple[int, int, object]]:
          i = 0
          out = []
          while i < len(b):
              try:
                  tag, i = read_varint(b, i)
              except IndexError:
                  break
              fn, wt = tag >> 3, tag & 7
              if wt == 0:
                  v, i = read_varint(b, i)
                  out.append((fn, 0, v))
              elif wt == 2:
                  ln, i = read_varint(b, i)
                  out.append((fn, 2, b[i : i + ln]))
                  i += ln
              elif wt == 1:
                  out.append((fn, 1, b[i : i + 8]))
                  i += 8
              elif wt == 5:
                  out.append((fn, 5, b[i : i + 4]))
                  i += 4
              else:
                  break
          return out
      
      
      def max_step_idx(db_path: Path) -> int:
          """Return the latest idx of any step in a DB, or -1 when absent.
      
          Spans every step type, not just 15: a failed run's type=17 error row
          lands after its last type=15 step, so a type=15-only boundary would
          re-attribute that stale error to the next run on resume.
          """
          if not db_path.is_file():
              return -1
          con = sqlite3.connect(str(db_path))
          try:
              row = con.execute("SELECT MAX(idx) FROM steps").fetchone()
          finally:
              con.close()
          return row[0] if row and row[0] is not None else -1
      
      
      def extract_answer(db_path: Path, include_reasoning: bool = False, after_idx: int = -1) -> tuple[str, str, list[dict]]:
          """Return (answer, reasoning, all_messages) from an agy conversation DB.
      
          Only step_type=15 rows with idx > after_idx belong to this run; the
          assistant's reply is ALL non-empty f20.f1 fragments of the run joined
          with blank lines (agy may split a deliverable across several steps and
          end with a short closing line; last-wins would drop the deliverable).
          """
          con = sqlite3.connect(str(db_path))
          cur = con.cursor()
          rows = cur.execute(
              "SELECT idx, step_payload FROM steps WHERE step_type=15 AND idx > ? ORDER BY idx",
              (after_idx,),
          ).fetchall()
          con.close()
      
          answers = []
          reasoning = ""
          all_msgs = []
          for idx, blob in rows:
              if not blob:
                  continue
              top = scan_protobuf(blob)
              f20 = next((v for fn, wt, v in top if fn == 20 and wt == 2), None)
              if f20 is None:
                  continue
              f1 = f3 = ""
              for fn, wt, v in scan_protobuf(f20):
                  if wt == 2 and fn in (1, 3, 8):
                      try:
                          s = v.decode("utf-8")
                      except UnicodeDecodeError:
                          continue
                      if fn in (1, 8) and s:
                          f1 = s  # f8 is a duplicate of f1
                      elif fn == 3:
                          f3 = s
              if f1:
                  answers.append(f1)
              if f3:
                  reasoning = f3
              all_msgs.append({"idx": idx, "answer": f1, "reasoning": f3})
          return "\n\n".join(answers), reasoning, all_msgs
      
      
      def extract_run_error(db_path: Path, after_idx: int = -1) -> str:
          """Return agy's own upstream failure for this run, or "" when absent.
      
          step_type=17 is agy's error channel: f24.f3 holds f1 (user-facing line),
          f2 (status detail, e.g. "FAILED_PRECONDITION (code 400): ...") and f9 (a
          duplicate of f2). Uses the same idx window as extract_answer so a resumed
          session cannot resurface the previous run's error.
      
          An error row that cannot be decoded still returns a non-empty string: the
          row's mere existence proves the run failed upstream, and reporting "" here
          would send the caller to extract_answer() over an error-channel change.
          """
          con = sqlite3.connect(str(db_path))
          try:
              rows = con.execute(
                  "SELECT step_payload FROM steps WHERE step_type=17 AND idx > ? ORDER BY idx",
                  (after_idx,),
              ).fetchall()
          finally:
              con.close()
      
          for (blob,) in rows:
              if not blob:
                  continue
              f24 = next((v for fn, wt, v in scan_protobuf(blob) if fn == 24 and wt == 2), None)
              if f24 is None:
                  continue
              f3 = next((v for fn, wt, v in scan_protobuf(f24) if fn == 3 and wt == 2), None)
              if f3 is None:
                  continue
              parts = {}
              for fn, wt, v in scan_protobuf(f3):
                  if wt == 2 and fn in (1, 2, 9):
                      with contextlib.suppress(UnicodeDecodeError):
                          parts[fn] = v.decode("utf-8")
              detail = parts.get(2) or parts.get(9) or parts.get(1)
              if detail:
                  return detail
          if rows:
              return "agy recorded an error step whose detail could not be decoded (fix: extract_run_error())"
          return ""
      
      
      # --- agy binary resolution ---
      
      
      def find_agy() -> str | None:
          for candidate in AGY_BIN_CANDIDATES:
              resolved = shutil.which(candidate) if candidate == "agy" else None
              if resolved:
                  return resolved
              p = Path(candidate)
              if p.is_file():
                  return str(p)
          return None
      
      
      def auth_status() -> str:
          if os.environ.get("ANTIGRAVITY_API_KEY"):
              return "api-key"
          if (Path.home() / ".config/antigravity").is_dir() or (Path.home() / ".gemini/antigravity-cli").is_dir():
              return "oauth"
          return "missing"
      
      
      def resolve_model_alias(user_input: str) -> str:
          if user_input in CANONICAL_MODELS:
              return user_input
          return MODEL_ALIASES.get(user_input.lower(), user_input)
      
      
      # --- conversation DB discovery ---
      
      
      def snapshot_db_uuids() -> set:
          if not CONVERSATIONS_DIR.is_dir():
              return set()
          return {p.stem for p in CONVERSATIONS_DIR.glob("*.db")}
      
      
      def new_db_uuid(before: set, after: set) -> str | None:
          new_uuids = after - before
          if not new_uuids:
              return None
          if len(new_uuids) == 1:
              return next(iter(new_uuids))
          # >1 new DB: pick newest by mtime, log warning via stderr
          candidates = [CONVERSATIONS_DIR / f"{u}.db" for u in new_uuids]
          candidates.sort(key=lambda p: p.stat().st_mtime, reverse=True)
          print(
              f"[agy_bridge] warning: {len(new_uuids)} new conversation DBs detected; picking newest: {candidates[0].stem}",
              file=sys.stderr,
          )
          return candidates[0].stem
      
      
      # --- model / timeout helpers ---
      
      
      def current_default_model() -> str:
          try:
              with open(SETTINGS_FILE, encoding="utf-8") as f:
                  return json.load(f).get("model", "")
          except (OSError, json.JSONDecodeError):
              return ""
      
      
      def parse_timeout_to_seconds(s: str) -> int:
          """Parse agy-style durations (e.g. '5m', '30s', '2h', '90'). Default 5m."""
          s = (s or "").strip()
          m = re.fullmatch(r"(\d+)\s*(s|m|h)?", s, re.IGNORECASE)
          if not m:
              return 300
          n = int(m.group(1))
          unit = (m.group(2) or "s").lower()
          return n * {"s": 1, "m": 60, "h": 3600}[unit]
      
      
      # --- core run ---
      
      
      def short_answer_note(answer: str, all_msgs: list[dict]) -> str | None:
          """Warn when the reply is very short but the run had thinking-only steps."""
          if len(answer) < 60 and any(m["reasoning"] and not m["answer"] for m in all_msgs):
              return (
                  "This run produced a very short text reply and contained thinking-only "
                  "steps; the full deliverable may be in tool outputs rather than the reply."
              )
          return None
      
      
      def run_agy_print(cmd: list[str], cwd: str, timeout_s: int) -> tuple[int, str, str, bool]:
          try:
              cp = subprocess.run(
                  cmd,
                  cwd=cwd,
                  stdin=subprocess.DEVNULL,
                  capture_output=True,
                  timeout=timeout_s,
                  check=False,
              )
              return (
                  cp.returncode,
                  cp.stdout.decode("utf-8", "replace"),
                  cp.stderr.decode("utf-8", "replace"),
                  False,
              )
          except subprocess.TimeoutExpired as e:
              out = (e.stdout or b"").decode("utf-8", "replace") if e.stdout else ""
              err = (e.stderr or b"").decode("utf-8", "replace") if e.stderr else ""
              return (-1, out, err, True)
          except FileNotFoundError:
              return (127, "", "agy binary not found", False)
      
      
      def build_agy_cmd(agy_path: str, args) -> list[str]:
          cmd = [
              agy_path,
              "--print",
              args.PROMPT + OUTPUT_PROTOCOL,
              "--print-timeout",
              args.print_timeout,
              "--add-dir",
              str(args.cd),
          ]
          if not args.no_skip_permissions:
              cmd.append("--dangerously-skip-permissions")
          if args.model:
              cmd += ["--model", resolve_model_alias(args.model)]
          if args.SESSION_ID:
              cmd += ["--conversation", args.SESSION_ID]
          if args.sandbox:
              cmd.append("--sandbox")
          return cmd
      
      
      def configure_windows_stdio() -> None:
          if os.name != "nt":
              return
          for stream in (sys.stdout, sys.stderr):
              reconfigure = getattr(stream, "reconfigure", None)
              if callable(reconfigure):
                  with contextlib.suppress(ValueError, OSError):
                      reconfigure(encoding="utf-8")
      
      
      def emit(result: dict) -> None:
          print(json.dumps(result, indent=2, ensure_ascii=False))
      
      
      # --- subcommands ---
      
      
      def cmd_check() -> None:
          path = find_agy()
          if not path:
              emit(
                  {
                      "installed": False,
                      "path": "",
                      "version": "",
                      "auth": "unknown",
                      "model": current_default_model(),
                      "conversations_dir": str(CONVERSATIONS_DIR),
                      "error": f"agy binary not found; {INSTALL_HINT}",
                  }
              )
              return
          try:
              version = (
                  subprocess.run(
                      [path, "--version"],
                      capture_output=True,
                      timeout=15,
                      text=True,
                      check=False,
                  )
                  .stdout.strip()
                  .splitlines()[0]
              )
          except (subprocess.TimeoutExpired, IndexError, OSError):
              version = "unknown"
          emit(
              {
                  "installed": True,
                  "path": path,
                  "version": version,
                  "auth": auth_status(),
                  "model": current_default_model(),
                  "conversations_dir": str(CONVERSATIONS_DIR),
                  "error": "",
              }
          )
      
      
      def cmd_plugin(extra: list[str]) -> None:
          path = find_agy()
          if not path:
              emit(
                  {
                      "success": False,
                      "output": "",
                      "error": f"agy not installed; {INSTALL_HINT}",
                  }
              )
              return
          cmd = [path, "plugin"] + extra
          try:
              cp = subprocess.run(cmd, capture_output=True, timeout=120, text=True, check=False)
              emit({"success": cp.returncode == 0, "output": cp.stdout, "error": cp.stderr})
          except subprocess.TimeoutExpired:
              emit({"success": False, "output": "", "error": "agy plugin timed out"})
      
      
      # --- main run path ---
      
      
      def cmd_run(args) -> None:
          cd: Path = args.cd
          if not cd.exists():
              emit(
                  {
                      "success": False,
                      "error": f"The workspace root directory `{cd.absolute()}` does not exist. "
                      f"Please check the path and try again.",
                  }
              )
              return
      
          agy_path = find_agy()
          if not agy_path:
              emit({"success": False, "error": f"agy is not installed; {INSTALL_HINT}"})
              return
          if auth_status() == "missing":
              emit(
                  {
                      "success": False,
                      "error": "agy is not authenticated. Run `agy` once interactively, or export ANTIGRAVITY_API_KEY.",
                  }
              )
              return
      
          before = snapshot_db_uuids()
          after_idx = max_step_idx(CONVERSATIONS_DIR / f"{args.SESSION_ID}.db") if args.SESSION_ID else -1
          cmd = build_agy_cmd(agy_path, args)
          outer_timeout = parse_timeout_to_seconds(args.print_timeout) + 60
          rc, _, err, timed_out = run_agy_print(cmd, cwd=str(cd.absolute()), timeout_s=outer_timeout)
      
          target_uuid = args.SESSION_ID or new_db_uuid(before, snapshot_db_uuids())
      
          if target_uuid is None:
              if timed_out:
                  emit(
                      {
                          "success": False,
                          "error": f"agy timed out after {outer_timeout}s with no conversation DB created. stderr: {err}",
                      }
                  )
              else:
                  emit(
                      {
                          "success": False,
                          "error": f"agy exited (rc={rc}) but created no conversation DB. stderr: {err}",
                      }
                  )
              return
      
          db_path = CONVERSATIONS_DIR / f"{target_uuid}.db"
          if not db_path.exists():
              emit(
                  {
                      "success": False,
                      "SESSION_ID": target_uuid,
                      "error": f"conversation DB not found: {db_path}",
                  }
              )
              return
      
          answer, reasoning, all_msgs = extract_answer(
              db_path, include_reasoning=args.return_all_messages, after_idx=after_idx
          )
          note = short_answer_note(answer, all_msgs)
      
          # Persist every extracted step as JSONL so partial results survive a
          # crash/timeout and the raw run can be audited later (mirrors the
          # codex_bridge stream_file pattern).
          sfd, stream_path = tempfile.mkstemp(prefix="agy_steps_", suffix=".jsonl")
          with os.fdopen(sfd, "w", encoding="utf-8") as fp:
              for m in all_msgs:
                  fp.write(json.dumps({"SESSION_ID": target_uuid, **m}, ensure_ascii=False) + "\n")
      
          result = {"success": bool(answer), "SESSION_ID": target_uuid}
          if answer:
              result["agent_messages"] = answer
              if args.return_all_messages:
                  result["all_messages"] = all_msgs
                  result["reasoning"] = reasoning
          else:
              upstream = extract_run_error(db_path, after_idx=after_idx)
              if upstream:
                  result["error"] = f"agy produced no reply because the run failed upstream (rc={rc}): {upstream}"
              elif timed_out:
                  result["error"] = (
                      f"agy timed out after {outer_timeout}s before producing a reply in DB {target_uuid}. stderr: {err}"
                  )
              else:
                  result["error"] = (
                      f"agy exited rc={rc} and DB {target_uuid} contained no extractable assistant "
                      f"reply after idx {after_idx} (0 new type=15 steps with non-empty f1), and no "
                      f"type=17 error row explains it. This may indicate agy performed only tool "
                      f"calls, or that the protobuf schema changed (fix: extract_answer()). "
                      f"stderr: {err}"
                  )
          if note:
              result["note"] = note
          result["steps_file"] = stream_path
          if err.strip():
              result["stderr"] = err.strip()
          emit(result)
      
      
      def main() -> None:
          configure_windows_stdio()
          import argparse
      
          parser = argparse.ArgumentParser(description="Antigravity (agy) Bridge")
          parser.add_argument("--PROMPT", help="Instruction for the task to send to agy.")
          parser.add_argument("--cd", type=Path, help="Workspace root for agy (cwd + --add-dir).")
          parser.add_argument(
              "--model",
              default="",
              help="Model alias (flash-low/medium/high, pro-low/high, sonnet, "
              "opus, gpt-oss) or canonical string. Omit to use settings default.",
          )
          parser.add_argument(
              "--SESSION_ID",
              default="",
              help="Resume a conversation by UUID. Maps to agy --conversation.",
          )
          parser.add_argument("--sandbox", action="store_true", help="Run in agy sandbox mode.")
          parser.add_argument(
              "--no-skip-permissions",
              action="store_true",
              help="Do NOT pass --dangerously-skip-permissions. WARNING: with "
              "default toolPermission=request-review, print mode WILL HANG.",
          )
          parser.add_argument(
              "--print-timeout",
              default="10m",
              help="agy --print-timeout (e.g. 5m, 10m). Default 10m.",
          )
          parser.add_argument(
              "--return-all-messages",
              action="store_true",
              help="Include reasoning + all type=15 steps in the response.",
          )
          sub = parser.add_subparsers(dest="subcommand")
          sub.add_parser("check", help="Probe agy install / version / auth / current model.")
          sub.add_parser("plugin", help="Thin passthrough to `agy plugin`.")
      
          args = parser.parse_args()
      
          if args.subcommand == "check":
              cmd_check()
              return
          if args.subcommand == "plugin":
              # re-parse to capture plugin's own args verbatim
              rest = sys.argv[sys.argv.index("plugin") + 1 :]
              cmd_plugin(rest)
              return
      
          if not args.PROMPT or not args.cd:
              parser.error("the following arguments are required: --PROMPT, --cd")
          cmd_run(args)
      
      
      if __name__ == "__main__":
          main()
      
  • tests
    • test_agy_bridge.py 8.6 KB
      """Regression tests for agy_bridge.py.
      
      Run: python -m pytest skills/cc-agy/tests/test_agy_bridge.py
      These mock run_agy_print / find_agy; no real agy process is launched.
      
      Covers the two misjudgment modes seen in session 7f6edd35:
        A. deliverable split across steps, last one a short closing -> must join all
        B. resume run ending with a tool receipt ('WROTE 17253') -> short-answer note
      Plus: resume with no new f1 must NOT fall back to the previous run's answer,
      and the PROMPT sent to agy must carry OUTPUT_PROTOCOL.
      Also covers session cc532d4c: an upstream failure (type=17 row) must be
      reported instead of the protobuf-schema guess, and must not leak across runs.
      """
      
      import importlib.util
      import sqlite3
      import types
      from pathlib import Path
      from unittest.mock import patch
      
      _SRC = Path(__file__).resolve().parents[1] / "scripts" / "agy_bridge.py"
      _spec = importlib.util.spec_from_file_location("agy_bridge", _SRC)
      bridge = importlib.util.module_from_spec(_spec)
      _spec.loader.exec_module(bridge)
      
      
      def write_varint(v: int) -> bytes:
          out = b""
          while True:
              byte = v & 0x7F
              v >>= 7
              out += bytes([byte | 0x80]) if v else bytes([byte])
              if not v:
                  return out
      
      
      def field(fn: int, payload: bytes) -> bytes:
          return write_varint(fn << 3 | 2) + write_varint(len(payload)) + payload
      
      
      def step_payload(text: str, reasoning: str = "") -> bytes:
          inner = field(1, text.encode("utf-8")) if text else b""
          if reasoning:
              inner += field(3, reasoning.encode("utf-8"))
          return field(20, inner)
      
      
      def error_payload(line: str, detail: str = "") -> bytes:
          """A step_type=17 payload: f24 -> f3 -> {f1: line, f2: detail}."""
          f3 = field(1, line.encode("utf-8"))
          if detail:
              f3 += field(2, detail.encode("utf-8"))
          return field(24, field(3, f3))
      
      
      def make_db(tmp: Path) -> tuple[Path, sqlite3.Connection]:
          tmp.mkdir(parents=True, exist_ok=True)
          db = tmp / "conv.db"
          con = sqlite3.connect(str(db))
          con.execute(
              "CREATE TABLE `steps` (`idx` integer,`step_type` integer NOT NULL DEFAULT 0,"
              "`status` integer NOT NULL DEFAULT 0,`has_subtrajectory` numeric NOT NULL DEFAULT false,"
              "`metadata` blob,`error_details` blob,`permissions` blob,`task_details` blob,"
              "`render_info` blob,`step_payload` blob,`step_format` integer NOT NULL DEFAULT 0,"
              "PRIMARY KEY (`idx`))"
          )
          return db, con
      
      
      CSS = ".zg-panel { display: flex; flex-direction: column; width: 100%; " * 5 + "}"
      CLOSING = "好的,上述 CSS 代码已经可以满足你所有的环境约束、功能要求并复原截图中反映的所有 UI 细节。\n\n如有其他地方需要细调,随时告诉我!"
      
      
      def test_case_a_join_all_fragments(tmp_path: Path):
          db, con = make_db(tmp_path / "a")
          con.execute(
              "INSERT INTO steps (idx, step_type, step_payload) VALUES (39, 15, ?)",
              (sqlite3.Binary(step_payload(CSS)),),
          )
          con.execute(
              "INSERT INTO steps (idx, step_type, step_payload) VALUES (42, 15, ?)",
              (sqlite3.Binary(step_payload(CLOSING)),),
          )
          con.commit()
          con.close()
          answer, _, _ = bridge.extract_answer(db)
          assert CSS in answer and CLOSING in answer, "deliverable fragment was dropped"
          assert answer.index(CSS) < answer.index(CLOSING), "fragments out of order"
      
      
      def test_case_b_incremental_snapshot(tmp_path: Path):
          db, con = make_db(tmp_path / "b")
          con.execute(
              "INSERT INTO steps (idx, step_type, step_payload) VALUES (42, 15, ?)",
              (sqlite3.Binary(step_payload(CLOSING)),),
          )
          con.execute(
              "INSERT INTO steps (idx, step_type, step_payload) VALUES (50, 15, ?)",
              (sqlite3.Binary(step_payload("", reasoning="thinking about file write")),),
          )
          con.execute(
              "INSERT INTO steps (idx, step_type, step_payload) VALUES (51, 15, ?)",
              (sqlite3.Binary(step_payload("WROTE 17253")),),
          )
          con.commit()
          con.close()
          assert bridge.max_step_idx(db) == 51
          answer, _, all_msgs = bridge.extract_answer(db, after_idx=42)
          assert answer == "WROTE 17253", "resume leaked pre-snapshot content"
          assert bridge.short_answer_note(answer, all_msgs) is not None, "missing note"
      
      
      def test_resume_no_fallback_to_old_answer(tmp_path: Path):
          db, con = make_db(tmp_path / "c")
          con.execute(
              "INSERT INTO steps (idx, step_type, step_payload) VALUES (39, 15, ?)",
              (sqlite3.Binary(step_payload(CSS)),),
          )
          con.commit()
          con.close()
          snapshot = bridge.max_step_idx(db)
          answer, _, _ = bridge.extract_answer(db, after_idx=snapshot)
          assert answer == "", "stale answer from previous run leaked into this run"
      
      
      GEO_ERR = "FAILED_PRECONDITION (code 400): User location is not supported for the API use."
      TERMINATED = "Agent execution terminated due to error."
      
      
      def run_failed_session(db: Path, new_error: str = "") -> dict:
          """Drive cmd_run over a stubbed agy that fails without producing a reply.
      
          Asserting on cmd_run's emitted JSON (not extract_run_error's return) is
          the point: the defect being guarded is WHAT GETS REPORTED, and the
          misleading schema hint only ever appears in cmd_run.
          """
          emitted = []
      
          def fake_run_agy_print(cmd, cwd, timeout_s):
              con = sqlite3.connect(str(db))
              nxt = con.execute("SELECT COALESCE(MAX(idx), -1) + 1 FROM steps").fetchone()[0]
              # a type=15 row whose f20 holds only a varint (f12) -> no reply text
              con.execute(
                  "INSERT INTO steps (idx, step_type, step_payload) VALUES (?, 15, ?)",
                  (nxt, sqlite3.Binary(field(20, write_varint(12 << 3) + write_varint(18)))),
              )
              if new_error:
                  con.execute(
                      "INSERT INTO steps (idx, step_type, step_payload) VALUES (?, 17, ?)",
                      (nxt + 1, sqlite3.Binary(error_payload(TERMINATED, new_error))),
                  )
              con.commit()
              con.close()
              return 1, "", f"Error: {TERMINATED}", False
      
          args = types.SimpleNamespace(
              PROMPT="probe",
              cd=db.parent,
              no_skip_permissions=False,
              model="",
              SESSION_ID=db.stem,
              sandbox=False,
              print_timeout="10m",
              return_all_messages=False,
          )
          with patch.multiple(
              bridge,
              CONVERSATIONS_DIR=db.parent,
              find_agy=lambda: "agy",
              auth_status=lambda: "oauth",
              run_agy_print=fake_run_agy_print,
              emit=emitted.append,
          ):
              bridge.cmd_run(args)
      
          assert len(emitted) == 1, f"expected one emit, got {len(emitted)}"
          Path(emitted[0]["steps_file"]).unlink()
          return emitted[0]
      
      
      def test_upstream_error_is_reported(tmp_path: Path):
          """Session cc532d4c: upstream rejected the run, type=15 row carries no f1.
      
          The real cause sits in the type=17 row; cmd_run must report it instead of
          blaming its own (correct) protobuf parsing.
          """
          db, con = make_db(tmp_path / "d")
          con.close()
      
          result = run_failed_session(db, new_error=GEO_ERR)
          assert result["success"] is False
          assert GEO_ERR in result["error"], f"upstream cause not reported: {result['error']!r}"
          assert "protobuf schema changed" not in result["error"], (
              "misleading schema hint survived alongside a known upstream cause"
          )
      
      
      def test_schema_hint_kept_when_no_error_row(tmp_path: Path):
          """With no type=17 row, the schema-drift hint is the only honest guess."""
          db, con = make_db(tmp_path / "f")
          con.close()
      
          result = run_failed_session(db)
          assert "protobuf schema changed" in result["error"], (
              "schema hint must survive when nothing explains the empty reply"
          )
      
      
      def test_resume_does_not_resurface_old_error(tmp_path: Path):
          """A previous run's type=17 row must not be attributed to a new run."""
          db, con = make_db(tmp_path / "e")
          con.execute(
              "INSERT INTO steps (idx, step_type, step_payload) VALUES (1, 15, ?)",
              (sqlite3.Binary(step_payload("old answer")),),
          )
          con.execute(
              "INSERT INTO steps (idx, step_type, step_payload) VALUES (2, 17, ?)",
              (sqlite3.Binary(error_payload(TERMINATED, GEO_ERR)),),
          )
          con.commit()
          con.close()
      
          assert bridge.max_step_idx(db) == 2, "boundary must span all step types, not only type=15"
          result = run_failed_session(db)
          assert GEO_ERR not in result["error"], "resume resurfaced the previous run's error as this run's cause"
      
      
      def test_prompt_carries_output_protocol():
          args = types.SimpleNamespace(
              PROMPT="write CSS",
              cd=Path("."),
              no_skip_permissions=False,
              model="",
              SESSION_ID="",
              sandbox=False,
              print_timeout="10m",
          )
          cmd = bridge.build_agy_cmd("agy", args)
          assert "OUTPUT PROTOCOL" in cmd[2], "OUTPUT_PROTOCOL missing from PROMPT"
      
  • README.md 5.8 KB
    # cc-agy
    
    A Claude Code **Agent Skill** that bridges Claude with the Google Antigravity CLI (`agy`) for external-model delegation(Gemini CLI stopped working for person).
    
    ## Overview
    
    This Skill lets Claude delegate coding/research tasks to `agy`, which runs external models — Gemini 3.x, Claude Sonnet/Opus 4.6, GPT-OSS — with their own configured MCP servers, skills, and memory doc. Claude orchestrates the workflow and refines the output; agy executes on the external model.
    
    `agy --print` writes nothing to stdout, so the bridge instead discovers the conversation SQLite DB that agy persists and extracts the assistant reply from its protobuf payload. When a run fails upstream, agy records the executor error in a separate `step_type=17` row instead of any reply text; the bridge reads that row and reports the real cause. MCP (`~/.gemini/antigravity/mcp_config.json`), the memory doc (`~/.gemini/GEMINI.md`), and skills are pre-configured by the user and auto-loaded by agy — the bridge does not manage them.
    
    ## Features
    
    - **Multi-turn sessions**: Resume conversations via `SESSION_ID` (maps to agy `--conversation`)
    - **Multi-model prototyping**: Switch model per call with `--model` aliases (flash, pro, sonnet, opus, gpt-oss)
    - **JSON output**: Structured responses isomorphic to `gemini_bridge.py`
    - **Cross-platform**: Windows path / UTF-8 handled automatically
    - **Setup probe**: `check` subcommand reports agy install / version / auth / current model
    - **Plugin passthrough**: `plugin` subcommand forwards to `agy plugin list|import|install|enable|disable`
    
    ## Prerequisites
    
    1. Install the Antigravity CLI:
       ```bash
       curl -fsSL https://antigravity.google/cli/install.sh | bash
       ```
    2. Authenticate: run `agy` once interactively (browser OAuth), or export `ANTIGRAVITY_API_KEY`.
    3. Pick a default model: open `agy` and use `/model`, which writes `~/.gemini/antigravity-cli/settings.json`.
    
    Verify with:
    ```bash
    python scripts/agy_bridge.py check
    ```
    
    ## Installation
    
    Copy this Skill to your Claude Code skills directory:
    - User-level: `~/.claude/skills/cc-agy/`
    - Project-level: `.claude/skills/cc-agy/`
    
    Or, in the DSkills marketplace, it is registered in `.claude-plugin/marketplace.json` as the `cc-agy` plugin.
    
    ## Usage
    
    ### Basic
    
    ```bash
    python scripts/agy_bridge.py --cd "/path/to/project" --PROMPT "Analyze the authentication flow"
    ```
    
    ### Multi-turn Session
    
    ```bash
    # Start a session
    python scripts/agy_bridge.py --cd "/project" --PROMPT "Review login.py for security issues"
    # Response includes SESSION_ID
    
    # Continue the session
    python scripts/agy_bridge.py --cd "/project" --SESSION_ID "uuid-from-response" --PROMPT "Suggest fixes for the issues found"
    ```
    
    ### Switch Model Per Call
    
    ```bash
    python scripts/agy_bridge.py --cd "/project" --PROMPT "Review this Rust" --model sonnet
    python scripts/agy_bridge.py --cd "/project" --PROMPT "Same code" --model opus
    ```
    
    ### Parameters
    
    | Parameter | Required | Description |
    |-----------|----------|-------------|
    | `--PROMPT` | Yes | Task instruction |
    | `--cd` | Yes | Workspace root (agy cwd + `--add-dir`) |
    | `--model` | No | Model alias or canonical string; omit for settings default |
    | `--SESSION_ID` | No | Resume a conversation by UUID (maps to `--conversation`) |
    | `--sandbox` | No | Run in agy sandbox mode |
    | `--no-skip-permissions` | No | Do NOT pass `--dangerously-skip-permissions` (WARNING: hangs print mode) |
    | `--print-timeout` | No | agy `--print-timeout` (e.g. `5m`, `10m`); default `10m` |
    | `--return-all-messages` | No | Include reasoning + all `type=15` steps |
    
    Model aliases: `flash-low/medium/high`, `pro-low/high`, `sonnet`, `opus`, `gpt-oss`.
    
    ### Subcommands
    
    | Subcommand | Description |
    |------------|-------------|
    | `check` | Probe agy install / version / auth / current model |
    | `plugin` | Thin passthrough to `agy plugin list\|import\|install\|enable\|disable` |
    
    ### Output Format
    
    ```json
    {
      "success": true,
      "SESSION_ID": "uuid",
      "agent_messages": "agy response text"
    }
    ```
    
    When agy produces no reply, `error` carries agy's own upstream failure rather than a guess about the bridge:
    
    ```json
    {
      "success": false,
      "SESSION_ID": "uuid",
      "error": "agy produced no reply because the run failed upstream (rc=1): FAILED_PRECONDITION (code 400): User location is not supported for the API use.",
      "steps_file": "/tmp/agy_steps_xxxx.jsonl"
    }
    ```
    
    Upstream failures are often transient (geo/egress-IP rejection, model capacity exhausted). The bridge does not retry — the status detail tells the caller whether to wait or switch `--model`.
    
    ## Security Note
    
    By default the bridge passes `--dangerously-skip-permissions` to agy. This is **mandatory for non-interactive `--print` mode**: agy's default `toolPermission` is `request-review`, which blocks waiting for a human to approve tool calls, and `--print` captures no TTY — so the process hangs until timeout. With `--dangerously-skip-permissions`, agy auto-approves all tool calls. The bridge always runs under a hard outer timeout (`--print-timeout` + 60s) so a hung agy cannot block indefinitely. Only use `--no-skip-permissions` in a context where interactive permission prompts can be serviced.
    
    ## Known Limitations
    
    - **Protobuf extraction is schema-dependent.** The reply is read from field `f1` inside field `f20` of `step_type=15` rows; upstream errors from `f24` → `f3` of `step_type=17` rows. If agy changes its internal schema, extraction returns empty. Fix location: `extract_answer()` (replies) or `extract_run_error()` (errors) in `scripts/agy_bridge.py`. The reported error names which one to fix, so trust it over guessing.
    - `agy models` returns empty on this build; model aliases are hardcoded.
    - `--continue` is intentionally not exposed (target selection is opaque); use `--SESSION_ID` to resume a specific conversation.
    
    ## License
    
    MIT License. See [LICENSE](LICENSE) for details.
    
  • ruff.toml 656 B
    # Lint and format baseline for this skill.
    #
    # Kept explicit rather than relying on ruff's default rule set: CI installs ruff
    # unpinned, so a future release that widens its defaults would otherwise turn
    # this job red without any change to the code.
    line-length = 120
    target-version = "py311"
    
    [lint]
    select = [
        "E",   # pycodestyle errors
        "F",   # pyflakes
        "W",   # pycodestyle warnings
        "I",   # isort
        "N",   # pep8-naming
        "UP",  # pyupgrade
        "B",   # flake8-bugbear
        "A",   # flake8-builtins
        "C4",  # flake8-comprehensions
        "SIM", # flake8-simplify
    ]
    ignore = ["E501"] # line too long (handled by line-length above)
    
  • SKILL.md 6.5 KB
    ---
    name: cc-agy
    description: |
      Delegates coding/research tasks to the Google Antigravity CLI (`agy`) for external-model execution (Gemini 3.x, Claude Sonnet/Opus 4.6, GPT-OSS). Replaces the broken `collaborating-with-gemini` skill. Use when: (1) External-model delegation via Antigravity, (2) Multi-model prototyping (switch model per call), (3) Backend/logic implementation, (4) Algorithm design and optimization, (5) Bug analysis and debugging, (6) API/database code generation, (7) Code review. Triggers: "delegate to agy", "use Antigravity", "external model", "agy", "Gemini 3.5", "Claude Sonnet 4.6", "GPT-OSS". IMPORTANT: Always request unified diff patches only. Supports multi-turn sessions via SESSION_ID.
    ---
    
    ## Quick Start
    
    ```bash
    python scripts/agy_bridge.py --cd "/path/to/project" --PROMPT "Your task"
    ```
    
    **Output:** JSON with `success`, `SESSION_ID`, `agent_messages`, `steps_file`, and optional `error` / `note` / `stderr`. On failure, `error` names the upstream cause agy recorded (e.g. a 400/503 status) when one exists.
    
    ## Parameters
    
    ```
    usage: agy_bridge.py [-h] --PROMPT PROMPT --cd CD
                         [--model MODEL] [--SESSION_ID SESSION_ID]
                         [--sandbox] [--no-skip-permissions]
                         [--print-timeout PRINT_TIMEOUT]
                         [--return-all-messages]
                         {check,plugin} ...
    
    Antigravity (agy) Bridge
    
    options:
      -h, --help            show this help message and exit
      --PROMPT PROMPT       Instruction for the task to send to agy.
      --cd CD               Workspace root for agy (sets cwd + --add-dir).
      --model MODEL         Model alias (flash-low/medium/high, pro-low/high, sonnet,
                            opus, gpt-oss) or canonical string. Omit to use the
                            settings.json default.
      --SESSION_ID SESSION_ID
                            Resume a conversation by UUID. Maps to agy --conversation.
      --sandbox             Run in agy sandbox mode.
      --no-skip-permissions
                            Do NOT pass --dangerously-skip-permissions. WARNING:
                            with default toolPermission=request-review, print mode
                            WILL HANG. Only for interactive review workflows.
      --print-timeout PRINT_TIMEOUT
                            agy --print-timeout (e.g. 5m, 10m). Default 10m.
      --return-all-messages
                            Include reasoning + all type=15 steps in the response.
    
    subcommands:
      check                 Probe agy install / version / auth / current model.
      plugin                Thin passthrough to `agy plugin list|import|install|enable|disable`.
    ```
    
    ## Multi-turn Sessions
    
    **Always capture `SESSION_ID`** from the first response for follow-up (maps to agy `--conversation <UUID>`, which appends to the same conversation DB):
    
    ```bash
    # Initial task
    python scripts/agy_bridge.py --cd "/project" --PROMPT "Analyze auth in login.py"
    
    # Continue with SESSION_ID
    python scripts/agy_bridge.py --cd "/project" --SESSION_ID "uuid-from-response" --PROMPT "Write unit tests for that"
    ```
    
    ## Common Patterns
    
    **Prototyping (request diffs):**
    ```bash
    python scripts/agy_bridge.py --cd "/project" --PROMPT "Generate unified diff to add logging" --model pro
    ```
    
    **Switch model per call:**
    ```bash
    python scripts/agy_bridge.py --cd "/project" --PROMPT "Review this Rust" --model sonnet
    python scripts/agy_bridge.py --cd "/project" --PROMPT "Same code" --model opus
    ```
    
    **Setup check:**
    ```bash
    python scripts/agy_bridge.py check
    ```
    
    **Manage skills/plugins (passthrough to agy):**
    ```bash
    python scripts/agy_bridge.py plugin list
    python scripts/agy_bridge.py plugin import /path/to/plugin
    ```
    
    ## How It Works
    
    `agy --print` writes nothing to stdout. The bridge instead runs agy, discovers the new conversation SQLite DB at `~/.gemini/antigravity-cli/conversations/<UUID>.db`, and extracts the assistant reply from the `step_type=15` rows' `step_payload` protobuf (field `f20` → `f1`). MCP servers (`~/.gemini/antigravity/mcp_config.json`), the memory doc (`~/.gemini/GEMINI.md`), and skills are pre-configured by the user and auto-loaded by agy — the bridge does not manage them.
    
    When a run fails upstream, agy writes no reply text at all and instead records the executor error in a `step_type=17` row (`f24` → `f3`: `f1` user-facing line, `f2` status detail). The bridge reads that row and reports the real cause — e.g. `FAILED_PRECONDITION (code 400): User location is not supported for the API use.` or `UNAVAILABLE (code 503): No capacity available for model ...` — instead of blaming its own parsing. `stderr` alone carries only agy's generic `Agent execution terminated due to error.` and no status code.
    
    On resume, both extractors window on `idx > after_idx`, where the boundary spans **all** step types: a failed run's `type=17` row lands after its last `type=15` step, so a `type=15`-only boundary would re-attribute that stale error to the next run.
    
    The bridge does not retry. A 400 geo rejection will almost certainly fail again on an immediate retry, and 503 capacity exhaustion is better answered by switching `--model` than by replaying — which could also repeat tool side effects. The structured error is passed through so the caller decides.
    
    ## Security Note
    
    By default the bridge passes `--dangerously-skip-permissions` to agy. This is **mandatory for non-interactive `--print` mode** because agy's default `toolPermission` is `request-review`, which blocks waiting for a human to approve tool calls — and since `--print` captures no TTY, the process hangs until timeout. With `--dangerously-skip-permissions`, agy auto-approves all tool calls. Only pass `--no-skip-permissions` if agy can service interactive permission prompts. The bridge always runs under a hard outer timeout (`--print-timeout` + 60s) so a hung agy cannot block indefinitely.
    
    ## Known Limitations
    
    - **Protobuf extraction is schema-dependent.** The bridge parses agy's conversation DB without a `.proto` file: replies from `f20` → `f1` of `step_type=15` rows, upstream errors from `f24` → `f3` of `step_type=17` rows. If agy changes its internal schema, extraction returns empty. The reported error distinguishes the two cases — a message naming an upstream status means agy failed and the bridge worked; the schema-drift hint appears only when no error row explains the empty reply. Fix locations: `extract_answer()` and `extract_run_error()` in `scripts/agy_bridge.py`.
    - `agy models` returns empty on this build; model aliases are hardcoded.
    - `--continue` is intentionally NOT exposed (target selection is opaque); use `--SESSION_ID` to resume a specific conversation.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related