feat(cli): native hooks adapter for Antigravity CLI (agy) - #1146
Conversation
Antigravity ships two products with unrelated configuration: the IDE, already wired by `connect antigravity`, and the `agy` CLI, which reads its customizations out of ~/.gemini/ and until now was not wired at all. This adds `connect antigravity-cli` for the latter — MCP via ~/.gemini/config/mcp_config.json, plus optional native auto-capture hooks behind --with-hooks. Unlike Droid (rohitg00#1130), the Codex merge engine could not be reused. The Antigravity hooks contract differs in three ways: * hooks.json is a map of *named* hook bundles at the root, not the `{ hooks: { <Event>: [...] } }` envelope, so antigravity-hooks.ts implements a merge that owns top-level keys instead of per-event entries. User-authored bundles are preserved; a re-install replaces only the bundle whose commands point under the bundled plugin dir. * only five events exist (PreToolUse, PostToolUse, PreInvocation, PostInvocation, Stop) — no SessionStart/SessionEnd/UserPromptSubmit, so the session lifecycle is synthesized from the first PreInvocation and from Stop. PostInvocation is left unwired to avoid double-capture. * the stdin payload is camelCase and nested (`toolCall.args` with PascalCase keys, `conversationId`, `workspacePaths`), and stdout must be a JSON object — `pre-tool-use.mjs` writes raw prose when context injection is on. plugin/scripts/antigravity-bridge.mjs bridges all three: it normalizes the payload onto the shape the bundled hooks already accept, maps Cascade tool names (view_file, replace_file_content, …) onto the read/edit/write/grep vocabulary the capture heuristics use, pipes to the right script, discards child stdout and always answers `{}` so Antigravity's own permission decisions are never overridden. Event names, tool names and arg keys were verified against the shipped agy binary rather than docs alone (docs disagree on the global hooks path); the customization dir is ~/.gemini/config/, matching where agy already keeps mcp_config.json and plugins/. Signed-off-by: Bertho Joris <bertho_joris@yahoo.co.id>
|
@berthojoris is attempting to deploy a commit to the rohitg00's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
💤 Files with no reviewable changes (1)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughThe PR adds an ChangesAntigravity CLI integration
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant User
participant AgentMemoryCLI
participant AntigravityConfig
participant AntigravityBridge
participant AgentMemoryHooks
User->>AgentMemoryCLI: connect antigravity-cli --with-hooks
AgentMemoryCLI->>AntigravityConfig: merge MCP and native hook configuration
AntigravityConfig-->>AgentMemoryCLI: persist configuration and backup
AntigravityConfig->>AntigravityBridge: register event commands
AntigravityBridge->>AgentMemoryHooks: normalize and forward hook payload
AgentMemoryHooks-->>AntigravityBridge: complete hook execution
AntigravityBridge-->>AntigravityConfig: emit event response
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
The CI failure here is pre-existing on
Fix is in #1147 (three-line docs sync, no behaviour change). Once that lands I'll merge Nothing in this PR touches |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
test/antigravity-connect-hooks.test.ts (1)
125-140: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtend
ARG_KEY_MAPcoverage for the remaining mappings.This test only exercises
AbsolutePath→file_pathandCommandLine→command.ARG_KEY_MAPinsrc/hooks/antigravity-bridge.tsalso mapsTargetFile→file_path,DirectoryPath/SearchDirectory→path, andPattern/Query→pattern. Add cases for these to catch regressions in the mapping table.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/antigravity-connect-hooks.test.ts` around lines 125 - 140, Extend the normalizePayload test covering ARG_KEY_MAP to also verify TargetFile and DirectoryPath/SearchDirectory map to file_path/path, and Pattern/Query map to pattern. Add representative toolCall argument cases and assertions while preserving the existing checks for native names and original argument keys.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/cli/connect/antigravity-hooks.ts`:
- Around line 96-119: Update the command expansion in resolveBundle so
String.prototype.replace uses a replacer function that returns pluginRoot,
preserving pluginRoot literally even when it contains replacement tokens such as
$$ or $&.
---
Nitpick comments:
In `@test/antigravity-connect-hooks.test.ts`:
- Around line 125-140: Extend the normalizePayload test covering ARG_KEY_MAP to
also verify TargetFile and DirectoryPath/SearchDirectory map to file_path/path,
and Pattern/Query map to pattern. Add representative toolCall argument cases and
assertions while preserving the existing checks for native names and original
argument keys.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 930756fa-802c-4e17-8ae4-73aa28466f77
📒 Files selected for processing (14)
README.mdplugin/hooks/hooks.antigravity.jsonplugin/scripts/antigravity-bridge.mjsplugin/skills/agentmemory-agents/REFERENCE.mdplugin/skills/agentmemory-rest-api/REFERENCE.mdsrc/cli/connect/antigravity-cli.tssrc/cli/connect/antigravity-hooks.tssrc/cli/connect/guidelines.tssrc/cli/connect/index.tssrc/hooks/antigravity-bridge.tstest/antigravity-connect-hooks.test.tstest/cli-connect.test.tstest/connect-guidelines.test.tstsdown.config.ts
…mands
resolveBundle() expanded ${CLAUDE_PLUGIN_ROOT} via
String.prototype.replace with a string argument, so a plugin root
containing `$$`, `$&`, "$`" or `$'` was read as a replacement pattern
and rewritten:
C:/plug$&in -> C:/plug${CLAUDE_PLUGIN_ROOT}in/scripts/...
C:/plug$$in -> C:/plug$in/scripts/...
`$1` and `$<name>` are unaffected — the regex has no capture groups.
Switching to a replacer function keeps the path verbatim. The failure
mode this closes is silent: the hook installs with a broken command and
auto-capture simply never fires.
Regression test builds the manifest against a temp plugin root named
`plug$&$$in` and asserts the resolved command contains it literally.
Reported by CodeRabbit on rohitg00#1146.
Signed-off-by: Bertho Joris <bertho_joris@yahoo.co.id>
rohitg00
left a comment
There was a problem hiding this comment.
This is well-built. I verified the hook surface against the official docs (antigravity.google/docs/hooks) and the config path, the five events, and the camelCase/PascalCase payload keys all match exactly, so it is not guessing an API. The bridge reuses the bundled hook scripts through a thin normalizer rather than hand-rolling capture, which is the right call given agy's payload and named-bundle hooks.json genuinely differ from the Claude envelope. Supply chain is clean, guidelines.ts shares the same idempotent GEMINI.md block as the existing antigravity entry so there is no double-write, and the generated REFERENCE bumps are exactly what skills:gen produces for the new adapter.
The failing CI is not your fault. main currently has an endpoint-count drift (registers 130 routes while the docs say 129), so consistency.test.ts fails on every branch cut from main right now. Your PR adds no REST endpoint. That failure clears once the count fix lands on main; nothing to do here for it.
One real blocker before merge: the PreToolUse hook emits a bare {}. The docs mark decision as required for PreToolUse output, and there is empirical evidence (cmux #5358 on agy 1.0.5) that a {} response makes agy deny every tool call, which would break the agent instead of passively capturing. agy's own bundled vibe-island plugin registers no PreToolUse at all.
Asks:
- Either drop the
PreToolUseregistration (PostToolUse already captures file activity), or have it emit{"decision":"allow"}. - Add a test asserting the PreToolUse stdout contract so this can't regress.
- If you can, confirm against a live
agywhich allow-payload it actually accepts.
Everything else is approvable.
… hook
Antigravity documents `decision` as a required field of PreToolUse hook
output, and agy treats a response that omits it as a denial: the bare `{}`
the bridge used to write made the agent refuse every matched tool call
(reported against agy 1.0.5 in cmux#5358) instead of passively capturing
it. `responseFor` now answers PreToolUse with `{"decision":"allow"}` and
leaves every other event on `{}`, so no event that carries no permission
decision starts overriding the user's own settings.
The response is written from the `finally` block, so a failed capture or an
unparseable payload still produces the contract rather than empty stdout,
which PreToolUse would read the same way as `{}`.
Tests cover both the pure contract and the built bundled script running
end to end with no server listening. Also extends the ARG_KEY_MAP test to
every mapped key and pins that an explicit canonical key wins over a
PascalCase alias.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/hooks/antigravity-bridge.ts (1)
26-29: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the behavior-description comment.
Lines 26-29 describe work that the bridge performs. Keep only the permission-response rationale that is not clear from identifiers.
As per coding guidelines, "In TypeScript source code, avoid code comments explaining WHAT — use clear naming instead."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/hooks/antigravity-bridge.ts` around lines 26 - 29, Remove the behavior-description portion of the comment near responseFor, including the statements about running scripts, discarding stdout, auto-capture, and not blocking or rewriting tool calls. Preserve only the rationale explaining why PreToolUse cannot return a bare {} response.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/hooks/antigravity-bridge.ts`:
- Around line 26-29: Remove the behavior-description portion of the comment near
responseFor, including the statements about running scripts, discarding stdout,
auto-capture, and not blocking or rewriting tool calls. Preserve only the
rationale explaining why PreToolUse cannot return a bare {} response.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 1f94636e-cee4-4ae7-ae21-24e5205051db
📒 Files selected for processing (3)
plugin/scripts/antigravity-bridge.mjssrc/hooks/antigravity-bridge.tstest/antigravity-connect-hooks.test.ts
|
Thanks for the careful read, and for checking the hook surface against the docs yourself. You're right about The response now goes through a small export function responseFor(event: string): string {
return event === "PreToolUse" ? '{"decision":"allow"}' : "{}";
}Every other event stays on One thing I found while fixing it: the bridge wrote its response from the On the test — two of them:
I could not confirm against a live Also picked up CodeRabbit's nitpick while I was in there: the And thanks for tracking down the CI failure, that saves me a detour. |
| default: return []; | ||
| } | ||
| } | ||
| /** |
There was a problem hiding this comment.
Done in 3a095ba — the artifact is down to the three boilerplate lines every other bundled script has.
The cause was that the bundler strips // comments but preserves JSDoc blocks, so the /** */ docs on the bridge's exported helpers were the only ones surviving into plugin/scripts/. Switched those to line comments: the explanation stays in src/hooks/antigravity-bridge.ts and the generated file now matches pre-tool-use.mjs and friends (3 lines, all of them #region/sourcemap markers).
| installHooks: installAntigravityCliHooks, | ||
| }); | ||
|
|
||
| /** |
There was a problem hiding this comment.
Trimmed in 3a095ba. The file header and the per-function block were stating the same things twice — kept one statement of each and dropped the rest. Comment lines: antigravity-cli.ts 26 → 12, antigravity-hooks.ts 65 → 46, antigravity-bridge.ts 70 → 41, which puts them in line with droid.ts.
What I deliberately kept is the agy behaviour I verified against a live 1.0.15 — the two event shapes, the no-quoting rule, the PreToolUse decision contract. That part isn't derivable from the code, and each of those was a real defect here, so a future reader changing the manifest needs the reason. Happy to cut further if you'd rather that lived only in the PR description.
…t 1.0.15
Three defects found by probing a live agy 1.0.15 with an instrumented hook,
each of which stopped the adapter from capturing anything at all.
Lifecycle events take a flat handler list, not the tool-event wrapper. agy
parses `PreToolUse`/`PostToolUse` as `[{matcher, hooks: [...]}]` but
`PreInvocation`/`PostInvocation`/`Stop` as a bare `[{type, command}]`, since
there is no tool name to match on. Wrapping a lifecycle event makes agy read
the wrapper itself as a handler and reject the *whole file* with
`invalid hook "agentmemory": command hook must specify 'command'` — so the
mis-shaped Stop entry disabled every hook in the bundle, and would have
disabled hooks other tools had written to the same file.
`command` is not run through a shell and quotes are not stripped, so the
quoted path resolved to a module name that literally began with a double
quote: `Cannot find module 'C:\Users\…\.gemini\config\"C:\…\bridge.mjs"'`.
Commands are now bare. That also means a path containing spaces cannot be
expressed at all — quoted and unquoted both fail — so the installer refuses
with an explanation instead of writing hooks that can only fail at tool time.
The merge engine reads both shapes when deciding which bundles agentmemory
owns, so a re-install over the old wrapped layout still replaces it rather
than leaving a second copy behind.
Tests pin both event shapes, the absence of quotes, the space check, and
normalization of a payload captured verbatim from the live run — which also
confirms `conversationId`, PascalCase `toolCall.args`, and that agy sends no
`cwd` key at all.
|
Correction to my last comment: I did get access to a live Method: a probe hook that logs the payload agy sends and replies with whatever I put in a file, driven by 1. Three runs, same prompt, only the hook's stdout differing:
So it is not specific to 1.0.5 — current agy still denies on a bare 2. Lifecycle events use a different shape, and getting it wrong killed the whole file This is the one that matters most. agy parses the two event families differently: "PreToolUse": [ { "matcher": "…", "hooks": [ { "type": "command", "command": "…" } ] } ],
"Stop": [ { "type": "command", "command": "…" } ]Tool events take the Not just the bad event — the whole bundle, so nothing fired. Worse, 3. Commands must be unquoted, and paths with spaces are impossible
Note both the literal quotes and the resolution base — hooks run with cwd set to the Verified end to end: with One payload note for you, not fixed here because I'd be guessing at the right behavior. A real {"artifactDirectoryPath":"…/brain/53642203-…","conversationId":"53642203-…",
"modelName":"gemini-3.6-flash-high","stepIdx":3,
"toolCall":{"args":{"DirectoryPath":"C:\\Users\\u\\.gemini\\antigravity-cli"},"name":"list_dir"},
"transcriptPath":"…/transcript_full.jsonl","workspacePaths":[]}
The environment I tested on is restored — the probe |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/cli/connect/antigravity-hooks.ts`:
- Around line 127-136: Update isAgentmemoryBundle to require a stable
AgentMemory ownership marker or an exact generated bridge command, rather than
checking whether any handler command merely contains normalizedScriptsDir.
Ensure user-authored bundles that reference the scripts directory remain
untouched while genuinely generated AgentMemory bundles are still identified for
removal.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 469578bc-6336-4c59-8148-e350a825650b
📒 Files selected for processing (4)
plugin/hooks/hooks.antigravity.jsonsrc/cli/connect/antigravity-cli.tssrc/cli/connect/antigravity-hooks.tstest/antigravity-connect-hooks.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- plugin/hooks/hooks.antigravity.json
- test/antigravity-connect-hooks.test.ts
- src/cli/connect/antigravity-cli.ts
| function isAgentmemoryBundle(bundle: unknown, scriptsDir: string): boolean { | ||
| if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) { | ||
| return false; | ||
| } | ||
| const normalizedScriptsDir = normalizePathForCommandMatch(scriptsDir); | ||
| return allHandlers(bundle as NamedHook).some((handler) => | ||
| normalizePathForCommandMatch(handler?.command ?? "").includes( | ||
| normalizedScriptsDir, | ||
| ), | ||
| ); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Use an exact ownership marker before deleting a bundle.
Line 132 treats any command that contains scriptsDir as AgentMemory-owned. A user-authored bundle can invoke another script under that directory or pass that directory as an argument. The merge then removes the complete bundle, and installAntigravityCliHooks persists the loss.
Match a stable AgentMemory bundle marker or the exact generated bridge command before removal.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/cli/connect/antigravity-hooks.ts` around lines 127 - 136, Update
isAgentmemoryBundle to require a stable AgentMemory ownership marker or an exact
generated bridge command, rather than checking whether any handler command
merely contains normalizedScriptsDir. Ensure user-authored bundles that
reference the scripts directory remain untouched while genuinely generated
AgentMemory bundles are still identified for removal.
The bundled script carried 24 comment lines where every other script in plugin/scripts has three. The bundler strips `//` comments but preserves JSDoc blocks, so the fix is to document the bridge's exported helpers with line comments: the explanations stay in source and the generated artifact comes out as clean as its siblings. The connect adapter and merge engine restated the same facts in a file header and again in a per-function block. Kept one statement of each, dropped the repetition, and left the verified agy behaviour in place since that is the part not derivable from the code.
What
Adds
agentmemory connect antigravity-cli— a new adapter for the AntigravityagyCLI, with optional native auto-capture hooks behind--with-hooks.Antigravity ships two products that do not share configuration:
connect antigravity(app-support dir);agyCLI, which reads its customizations out of~/.gemini/— MCP servers from~/.gemini/config/mcp_config.json, hooks from~/.gemini/config/hooks.json— and until now was not wired at all.This PR covers the second one. Detection keys off
~/.gemini/antigravity-cli/, which only the CLI creates (~/.gemini/alone would also match a Gemini CLI install).Why the Codex merge engine could not be reused
Unlike Droid (#1130), Antigravity's hooks contract differs from the Claude Code / Codex / Droid family in three ways:
hooks.jsonis a map of named hook bundles at the root, not the{ hooks: { <Event>: [...] } }envelope. Sosrc/cli/connect/antigravity-hooks.tsimplements a merge that owns top-level keys instead of per-event entries. User-authored bundles are preserved verbatim; a re-install replaces only the bundle whose commands point under the bundled plugin dir.PreToolUse,PostToolUse,PreInvocation,PostInvocation,Stop. There is noSessionStart,SessionEndorUserPromptSubmit, so the session lifecycle is synthesized from the firstPreInvocationand fromStop.PostInvocationis deliberately left unwired —PostToolUsealready captures the work, and firing both would double-record every turn.toolCall.argswith PascalCase keys such asAbsolutePath/TargetFile/Query, plusconversationIdandworkspacePathsinstead ofsession_id/cwd), and stdout must be a JSON object —pre-tool-use.mjswrites raw prose whenAGENTMEMORY_INJECT_CONTEXT=true, which Antigravity would fail to parse as aPreToolUsedecision.src/hooks/antigravity-bridge.ts(bundled toplugin/scripts/antigravity-bridge.mjs) bridges all three: it normalizes the payload onto the shape the existing hooks already accept, maps Cascade tool names (view_file,replace_file_content, …) onto theread/edit/write/grepvocabulary the capture heuristics use, pipes to the right script, discards child stdout, and always answers{}so Antigravity's own permission decisions are never overridden. The bridge never blocks or rewrites a tool call.Event names, tool names and arg keys were verified against the shipped
agybinary rather than docs alone (the docs disagree on the global hooks path). The customization dir is~/.gemini/config/, matching whereagyalready keepsmcp_config.jsonandplugins/.How to verify
npm install npm run build # compiles clean; plugin/scripts/*.mjs regenerate byte-identical npx vitest run test/antigravity-connect-hooks.test.ts test/cli-connect.test.ts test/connect-guidelines.test.tstest/antigravity-connect-hooks.test.ts(new, 15 cases) covers the merge engine — placeholder resolution, single-named-bundle shape, exact event set,PreToolUsematcher scope, preservation of user bundles, replacement of a stale agentmemory bundle, idempotent re-install — plus bridge payload normalization and event routing.End-to-end against a real install:
Re-running is idempotent; user-authored bundles in
~/.gemini/config/hooks.jsonare left untouched.Notes for the reviewer
plugin/skills/agentmemory-rest-api/REFERENCE.mdmoves118→119endpoints. That isnpm run skills:gencorrecting a count that was already stale onmain— it is not a side effect of this feature. No REST endpoint is added here.PreToolUsematcher lists 11 tools whileTOOL_NAME_MAPmaps 14 (view_line_range,read_url_content,propose_codeare mapped but not matched). This is deliberate —PostToolUseuses an empty matcher and catches them anyway — but happy to widen the matcher if you would rather have the two lists identical.CHANGELOG.mdentry, and the README row is kept to one line, matching the review feedback on feat(cli): native hooks adapter for Droid (Factory.ai) #1130.Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests