Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "antigravity",
"version": "0.22.4",
"version": "0.22.5",
"description": "Run the Antigravity CLI (Gemini) as a collaborating AI inside Claude Code, with intelligent model routing across the SDLC. Claude conducts — requirements, architecture, the hard 20%, verification, review — and routes deterministic, high-volume work (scaffolding, tests, first-pass review, migrations, web/Vertex AI Search) to Antigravity. Hybrid agentic engineering; lower token cost as a financial lever.",
"author": {
"name": "linyuting"
Expand Down
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,52 @@
All notable changes to **Antigravity for Claude Code**. Format loosely follows
[Keep a Changelog](https://keepachangelog.com/); versions are in `.claude-plugin/plugin.json`.

## 0.22.5
- **`doctor` now asks agy which model it will run, instead of inferring it from a version
string.** 0.22.4 added a warning for agy below 1.1.10, where `--model` was ignored in
headless `-p`. That warning is the best a version comparison can do, and a version
comparison is a proxy: it is right about the releases we know about and silent about
every other way the flag can fail to land. agy 1.1.11 answers the read-only slash
commands in print mode without starting an agent turn, so `doctor` requests a tier model
with `-p /model` and reports which one comes back — `usage.total_tokens: 0`, no quota
spent, no conversation left behind. It reads the tab-separated reply's slug and matches
it against a tier configured as a display name, the same either-direction comparison
`agy models` needed in 0.20.x.
**Gated at 1.1.11 and tested as a fact, not as prose.** Below that version the slash
command is not recognised, falls through as literal prompt text, and the model answers as
though it had run — so probing there would spend a real turn *and* then trust the answer
it invented. The stub agy in the suite logs every invocation, so "never probes below
1.1.11" is asserted against the log. An empty answer draws no conclusion in either
direction: an older build than the version claims, a hang, or a plan that refuses the
probe is not evidence that routing is broken.
- **`doctor` validates `permissions.allow` entries.** The plugin recommends such a rule in
eight places as the *narrow* alternative to `--yolo`, and the recommendation ships a
placeholder — `write_file(<dir>)`. A rule agy cannot parse announces itself in neither
direction, and which way it fails depends on the version: from **1.1.11** it matches
nothing, so the grant is absent and the write is soft-denied (exit 15) with the rule
sitting visibly in the file; **before 1.1.11** an entry that tokenized to zero command
words matched **every** command and silently auto-approved anything the agent ran —
broader than the `--yolo` it was chosen instead of. `doctor` flags the entry, names the
reason, and reports the consequence that applies to *your* agy rather than both.
The zero-command-word test follows upstream's own examples (`command(time)` — a shell
reserved word that prefixes a command without being one — a comment-only entry, and an
empty compound `()`), plus the unsubstituted `<...>` placeholder, which is ours. Rules it
cannot judge are left alone: unbalanced quotes are agy's parser's business, and
`write_file(...)` is a different matcher from `command(...)`. A false positive here sends
someone to edit a rule that was always fine, so the well-formed case is pinned as hard as
the broken ones.
- **Verified against agy 1.1.11, no change needed:** all three tiers still resolve
(`flash` → `gemini-3.5-flash-high`, `flash-lo` → `-low`, `pro` → `gemini-3.1-pro-high`);
the exit-14 classifier still fires on an unknown model; and 1.1.11's reworded
model-loading errors do not disturb the exit-15 path, whose message still contains
`auto-denied` and `permissions.allow` verbatim — two independent anchors the classifier
already matches.
**`/usage` and `/quota` were considered for `doctor` and rejected on evidence:** they
return zero bytes here, which is not a bug but this account type — 1.1.11's own notes say
credits do not apply to accounts signed in through a Google Cloud project or ADC.
`/model`, `/effort` and `/skills` all return data on the same setup, which is what made
the probe above possible.

## 0.22.4
- **`--tier` did nothing on agy below 1.1.10, and nothing said so.** agy 1.1.10 fixed
`--model` and `--effort` being *ignored in headless `-p`* — the flag was applied after
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,13 @@ Delegation doesn't save money by itself — these do (also in the skill):
- **`permissions.allow` in `~/.gemini/antigravity-cli/settings.json`** — a
`write_file(<dir>)` entry allows writes **recursively beneath `<dir>`** and needs no
flag. This is the narrower grant and usually the right one.
**`<dir>` is a placeholder — substitute a real path.** A rule agy cannot parse says
nothing in either direction: from **agy 1.1.11** it matches nothing, so the grant you
think you have is absent and the write is soft-denied for no visible reason; **before
1.1.11** an entry that tokenized to zero command words (upstream's examples:
`command(time)`, a comment-only entry, `()`) matched **every** command and silently
auto-approved anything the agent ran — broader than the `--yolo` it was chosen instead
of. `agy-doctor` checks your entries and names which failure applies to your version.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paragraph is about the write_file(<dir>) placeholder specifically, but the "before 1.1.11 ... matched every command" consequence it describes is scoped to command(...)-shaped rules with zero words — per scripts/doctor.sh's own bad_allow_rules() (and the PR's second commit message: "A mistyped write_file(<dir>) never had it. Telling someone holding one that their config auto-approves everything is a security claim about a situation they are not in.").

bad_allow_rules() classifies an unsubstituted <dir> placeholder as "unparseable", not "zerowords" — only "zerowords" (the command(time)/comment-only/() examples) carries the match-everything history, and doctor.sh prints that sentence only when a zerowords entry is present. As written, this bullet tells a reader whose write_file(<dir>) was never substituted that pre-1.1.11 it "matched every command and silently auto-approved anything the agent ran" — which, per the code's own distinction, isn't true for that matcher.

Same overclaim is copy-pasted into docs/TROUBLESHOOTING.md:113-118, docs/POC-PLAYBOOK.md:106-111, skills/antigravity/SKILL.md:220-225, and (shorter form) agents/antigravity-delegate.md:89-92. commands/delegate.md avoids it and reads correctly.

- **`--yolo`** (`--dangerously-skip-permissions`) — auto-approves **all** tools, not just
writes. Needed when no rule covers the target, and for web / Vertex AI Search / terminal
tools.
Expand Down
8 changes: 8 additions & 0 deletions docs/TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@ run still "succeeds"** ([#10](https://github.com/yuting0624/antigravity-for-clau
This is the rule agy's own soft-deny message is naming. Confirmed on agy 1.1.9 by a
controlled A/B ([#37](https://github.com/yuting0624/antigravity-for-claude-code/issues/37));
a glob form (`write_file(/path/**)`) was reported *not* to match.
**Substitute a real path for `<dir>`** — and if the rule is in place and the write is
*still* soft-denied, suspect the rule before suspecting agy. An entry agy cannot parse
is silent both ways: from **1.1.11** it matches nothing (so you get exactly this
exit 15, with the rule sitting right there in the file), and **before 1.1.11** an entry
that tokenized to zero command words — `command(time)`, a comment-only entry, `()` —
matched **every** command and silently auto-approved anything the agent ran. Run
`agy-doctor`: it validates each entry and tells you which of the two applies to your
version.
- **Or pass `--yolo`** (`--dangerously-skip-permissions`) — works across all agy versions,
but auto-approves **all** tools, not just the write. Required anyway for web / Vertex AI
Search / terminal when no rule covers them. (`--mode accept-edits` only wrote headless on
Expand Down
122 changes: 122 additions & 0 deletions scripts/doctor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,69 @@ sys.exit(0 if n else 1)
' "${files[@]}" 2>/dev/null
}

# Report `permissions.allow` entries agy cannot use as written. Prints one
# TAB-separated `<entry>\t<reason>` line each and returns 0 when any were found.
#
# WHY doctor owns this. The plugin recommends a `permissions.allow` rule in eight
# places as the NARROW alternative to `--yolo`, and that recommendation carries a
# placeholder — `write_file(<dir>)`. A rule that agy cannot parse does not announce
# itself in either direction: before 1.1.11 an entry that tokenizes to zero command
# words matched EVERY command and silently auto-approved anything the agent ran, which
# is broader than the `--yolo` it was chosen over; from 1.1.11 it matches nothing, so
# the grant simply is not there and the write is soft-denied for no visible reason.
# Same typo, opposite failures, no message either time.
bad_allow_rules() {
command -v python3 >/dev/null 2>&1 || return 1
python3 -c '
import json, shlex, sys

try:
perm = (json.load(open(sys.argv[1])) or {}).get("permissions") or {}
allow = perm.get("allow")
except Exception:
sys.exit(1)
if not isinstance(allow, list):
sys.exit(1)

# Shell reserved words that may PREFIX a command without being one, so a rule made
# only of them names no command. agy 1.1.11 describes the class it fixed as an entry
# that "tokenizes to zero command words" and gives command(time) as its own example.
PREFIX = {"time", "!", "{", "}", "[[", "]]", "if", "then", "elif", "else", "fi",
"case", "esac", "for", "select", "while", "until", "do", "done", "in",
"function", "coproc"}

bad = []
for e in allow:
if not isinstance(e, str):
bad.append((repr(e), "not a string")); continue
t = e.strip()
if not t:
bad.append(("(empty string)", "empty entry")); continue
if "<" in t or ">" in t:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor false-positive risk: "<" in t or ">" in t flags any occurrence of these characters anywhere in the entry as an "unsubstituted placeholder", not just <...> placeholder syntax. A well-formed command(...) rule that happens to use a shell redirect or comparison (e.g. command(sort < file.txt)) would be misreported as a placeholder rather than left alone like other well-formed rules. Given the "well-formed case is pinned as hard as the broken ones" bar this PR sets for itself, this one path isn't covered by a test — worth a quick check on whether that shape is realistic for permissions.allow entries, and if so a placeholder-specific pattern (e.g. requiring <...> to actually bracket something) would avoid it.

bad.append((t, "unsubstituted placeholder — replace <...> with a real value")); continue
i = t.find("(")
if i < 0 or not t.endswith(")"):
continue # not the NAME(...) shape; not ours to judge
name, inner = t[:i].strip(), t[i + 1:-1]
if not inner.strip():
bad.append((t, "empty rule body")); continue
if name != "command":
continue # write_file(...) etc. are a different matcher
try:
words = shlex.split(inner, comments=True)
except ValueError:
continue # unbalanced quotes: agy parses these, not us
while words and words[0] in PREFIX:
words.pop(0)
if not words:
bad.append((t, "tokenizes to zero command words"))

for t, why in bad:
print("%s\t%s" % (t, why))
sys.exit(0 if bad else 1)
' "$1" 2>/dev/null
}

# True when version $1 is strictly older than $2. Pure shell on purpose: `sort -V` is
# not universal, and when it is missing the command substitution comes back EMPTY, the
# comparison quietly fails, and the check it guards never fires — a version gate that
Expand Down Expand Up @@ -193,6 +256,42 @@ if command -v agy >/dev/null 2>&1; then
info "agy is multi-model/plan-dependent — remap tiers via CLAUDE_PLUGIN_OPTION_TIER_* (or set _DEFAULT_MODEL), or pass --model <name from \`agy models\`)"
fi
done

# 2c. Does --model actually TAKE EFFECT? Being listed in `agy models` and being
# honoured are different questions, and for three releases the answer to the second
# one was no (see the 1.1.10 warning above) while everything here still read green.
#
# The check above can only infer that from a version string. 1.1.11 answers the
# read-only slash commands in print mode without starting an agent turn, so doctor
# can stop inferring and ASK: request a tier model, see which one comes back. Costs
# no tokens, no quota, and leaves no conversation behind (`usage.total_tokens: 0`).
#
# Gated at 1.1.11 on purpose. Below it the command is not recognised, falls through
# as literal prompt text, and the model answers as though it had run — so the probe
# would spend a real turn AND return a made-up answer. Verified on 1.1.11: display
# names and slugs both work, and all three tiers come back as themselves.
case "${AGY_VER:-}" in
''|*[!0-9.]*) : ;;
*)
if ! ver_lt "$AGY_VER" 1.1.11; then
# cut -f1: the reply is one tab-separated record, `<slug>\t<display name>`.
EFF="$(agy_guard 20 --model "$FLASH" -p /model 2>/dev/null | head -1 | cut -f1)"
EFF_N="$(norm_model "$EFF")"; WANT_N="$(norm_model "$FLASH")"
if [ -z "$EFF_N" ]; then
# No answer at all: a hang, an older build than the version claims, or a
# plan that refuses the probe. Not evidence of breakage — stay quiet rather
# than report a failure doctor cannot actually substantiate.
:
elif case "$WANT_N" in *"$EFF_N"*) true ;; *) case "$EFF_N" in *"$WANT_N"*) true ;; *) false ;; esac ;; esac; then
ok "--model takes effect (asked for '$FLASH', agy reports '$EFF')"
else
warn "--model does NOT take effect: asked for '$FLASH', agy reports '$EFF'"
info "every delegation runs '$EFF' instead, and nothing in the output says so."
info "--tier / tier_* remaps are therefore inert. Check for a persisted default"
info "or a profile overriding it, then re-run doctor."
fi
fi ;;
esac
elif [ "$AGY_TIMED_OUT" -eq 0 ]; then
# Empty WITHOUT a timeout kill: genuinely no models -> auth/network is the likely cause.
bad "agy could not list models (not authenticated, or no network)"
Expand All @@ -208,6 +307,29 @@ if [ -f "$SETTINGS" ]; then
LOC="$(sed -n 's/.*"location"[: ]*"\([^"]*\)".*/\1/p' "$SETTINGS" | head -1)"
ok "agy settings: ${SETTINGS/#$HOME/~}"
[ -n "$PROJ" ] && info "GCP project: $PROJ location: ${LOC:-?}"

# 3b. permissions.allow entries agy cannot use as written.
if BAD_RULES="$(bad_allow_rules "$SETTINGS")"; then
warn "permissions.allow: $(printf '%s\n' "$BAD_RULES" | grep -c .) entry/entries agy cannot use as written"
printf '%s\n' "$BAD_RULES" | while IFS="$(printf '\t')" read -r rule why; do
info "$rule — $why"
done
# Same broken entry, opposite consequence, depending on which side of the fix
Comment thread
quorum-code-review[bot] marked this conversation as resolved.
Outdated
# you are on. Say which one applies rather than describing both.
case "${AGY_VER:-}" in
''|*[!0-9.]*)
info "consequence depends on the agy version (see the 1.1.11 changelog entry on zero-word allow rules)." ;;
*)
if ver_lt "$AGY_VER" 1.1.11; then
info "on agy $AGY_VER such an entry matches EVERY command and silently auto-approves"
info "anything the agent runs — broader than the --yolo it was chosen instead of."
info "fix: correct the entry, or \`agy update\` to 1.1.11+ where it matches nothing."
else
info "agy $AGY_VER ignores it, so this grant is NOT in effect and the tool it was"
info "meant to cover is still soft-denied (exit 15) with nothing naming the rule."
fi ;;
esac
fi
else
info "no agy settings.json yet (${SETTINGS/#$HOME/~})"
fi
Expand Down
15 changes: 12 additions & 3 deletions skills/antigravity/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: antigravity
description: Run the Antigravity CLI (Gemini) as a collaborating AI inside Claude Code, with intelligent model routing across the software development lifecycle. Claude is the conductor/orchestrator — requirements, architecture, the hard 20%, verification, and review — and routes deterministic, high-volume work (scaffolding, boilerplate, test generation, first-pass review, migrations, web/Vertex AI Search) to Antigravity (Gemini), the cheaper, faster model. Use when the user wants to "use Antigravity / agy", "vibe code / agentic engineering", "accelerate the SDLC", "delegate to Gemini", "scaffold / generate tests / migrate", "first-pass code review", "search web or internal/company data", "deep research / multi-source research report", "second-model cross-check", or "lower token cost on a big job". Claude always verifies Antigravity's output and re-checks itself if unsatisfied.
version: 0.22.4
version: 0.22.5
---

# Antigravity for Claude Code — hybrid SDLC
Expand Down Expand Up @@ -61,7 +61,11 @@ the cross-model verification value (Claude executing Claude loses both).
> configuration had initialised, so the run silently fell back to the persisted default.
> This wrapper resolves every `--tier` to `--model` and always runs `-p`, so on an older
> agy **tier selection does nothing and looks like it works**: the call succeeds, returns
> sensible text, reports usage. `doctor` warns when it sees one.
> sensible text, reports usage. `doctor` warns when it sees one — and on agy ≥ 1.1.11 it
> stops inferring and **asks**: it requests a tier model via `-p /model` (a read-only slash
> command that costs no tokens and starts no agent turn) and reports which model agy says
> it would actually run. Below 1.1.11 it does not probe, because there the slash command
> falls through as prompt text and the model answers as though it had run.
>
> The `flash` default is **Gemini 3.5 Flash (High)**, for broad plan availability — newer
> models can lag on enterprise Vertex. Gemini 3.6 Flash is available and its **output is
Expand Down Expand Up @@ -215,7 +219,12 @@ commands** (`--yolo` grants write + terminal):
denial text names the rule and offers `--yolo` as the *alternative*. `--yolo` auto-approves
**all** tools and is what you need when no rule covers the target, or for web / Vertex AI
Search / terminal. Not verified below 1.1.9; a glob form (`write_file(/path/**)`) was
reported not to match. Run write tasks on a branch and verify with `git status`.
reported not to match. `<dir>` is a placeholder: a rule agy cannot parse is silent both
ways — from 1.1.11 it matches nothing (the grant is absent, exit 15 with the rule
visibly present in the file), and before 1.1.11 an entry tokenizing to zero command
words (`command(time)`, comment-only, `()`) matched EVERY command. If a user reports a
rule that "should" work, have them run `agy-doctor` before changing anything else.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This caveat (placeholder must be substituted, malformed rule fails silently in opposite ways either side of 1.1.11, run agy-doctor) lands here, in README.md, POC-PLAYBOOK.md and TROUBLESHOOTING.md — but not in the two places that arguably matter most:

  • scripts/agy-delegate.sh:497, the exit-15 message shown at the moment a write is soft-denied (the exact symptom this whole feature diagnoses). It still just says "add a write_file(<dir>) rule" with no mention that an already-present rule could be the cause, and no pointer to agy-doctor. The parallel prose in docs/TROUBLESHOOTING.md for this exact same exit-15 case was updated to say "suspect the rule before suspecting agy" and to run agy-doctor — the runtime string wasn't kept in sync.
  • agents/antigravity-delegate.md:88 and commands/delegate.md:15, the operational instructions actually consulted when constructing a delegation, still show the bare write_file(<dir>) recommendation with no placeholder/version caveat at all.

Given "doc drift across more than the file in the diff" is a recurring failure class here, worth deciding whether these should get at least a short pointer (e.g. "malformed rule? run agy-doctor") rather than silently diverging from the docs that were updated.

Run write tasks on a branch and verify with `git status`.
prompt for or block `--dangerously-skip-permissions` — approve it or pre-allow
`Bash(agy-delegate*)`. Always verify files actually changed **in the workspace** with
`git status` (the wrapper maps a 1.1.3 soft-deny to exit `15` so you're not left guessing).
Expand Down
Loading
Loading