feat: add Antigravity IDE support (hooks, plugin, skill, docs, tests) - #1633
Conversation
…ests) Adds first-class integration with Google's Antigravity IDE (https://antigravity.google/) as a third sibling to the existing Claude Code and Codex hook integrations. Strictly additive — no existing files in main are restructured. What ships ---------- * `.antigravity-plugin/` — verified-minimal plugin package: * `plugin.json` with `{"name": "mempalace"}` (no fabricated fields) * `mcp_config.json` registering the `mempalace-mcp` stdio server * `hooks.json.tmpl` templated with `__PLUGIN_DIR__` substitution * `skills/mempalace/SKILL.md` (real file — no symlinks) * `hooks/antigravity/`: * `lib/common.sh` — shared bash 3.2.57-compatible helpers with sentinel-guarded camelCase JSON parser, antigravity_*-namespaced state files, every existing kill switch, `MEMPAL_SAVE_INTERVAL >= 1` floor (no /0), and fail-open emitters * `mempal_save_hook_antigravity.sh` — Stop event handler: increments per-conversation counter, defers when fullyIdle=False or terminationReason=error, validates transcriptPath against `..` traversal, spawns `mempalace mine --mode convos` in a detached subprocess with a per-conversation pending marker, ALWAYS emits `{}` (never `{"decision":"continue"}` — that would force an infinite agent loop) * `mempal_wake_hook_antigravity.sh` — PreInvocation handler gated to invocationNum==1 with an atomic mkdir loop guard, runs `mempalace wake-up` with a 500ms hard timeout, emits verbatim output as `{"injectSteps":[{"ephemeralMessage":"..."}]}` or `{}` on any failure * `install.sh` — idempotent installer with cmp-gated copies, `__PLUGIN_DIR__` substitution, relative path absolutization, `--dry-run`, and basename-guarded `--uninstall` (refuses to wipe a directory whose basename isn't `mempalace`) * `INVESTIGATION.md` — verbatim quotes + URLs + dates from the five official Antigravity doc pages, recording every surface shipped and every surface deliberately omitted (PreCompact equivalent, slash-commands, rules/, plugin permissions field — the latter is third-party fabrication) * `STDIN_SHAPE.md` — exact stdin/stdout contract per event with worked examples * `README.md` — local hook docs + troubleshooting * `examples/antigravity/{hooks.json,mcp_config.json,README.md}` — standalone configs for users who don't want the full installer * `website/guide/antigravity.md` + sidebar entry — VitePress guide * Updates to `README.md`, `CHANGELOG.md` (Unreleased), `hooks/README.md` Tests (56 new, all passing) --------------------------- * `tests/test_antigravity_plugin_manifest.py` (11 tests) — schema contract on the in-repo `.antigravity-plugin/` directory, including guards against re-introducing the fabricated `permissions` field and against any symlink leak. * `tests/test_antigravity_hooks_shell.py` (31 tests) — invokes the bash hooks via subprocess with synthetic camelCase stdin, asserts `{}` on every failure path, kill-switch coverage (env vars + config.json + palace nuke), divide-by-zero floor, transcript traversal rejection, namespacing, wing inference, and the hard refusal to ever emit `decision=continue` from the Stop hook. * `tests/test_antigravity_hooks_install.py` (14 tests) — `--dry-run` side-effect-free, real install layout, executable bits preserved, byte-identical idempotent re-runs (md5 + filecmp), basename-match uninstall safety, refusal when plugin.json is missing or names a different plugin, relative path absolutization. Skipped on Windows. Verification ------------ * `uv run pytest tests/ --ignore=tests/benchmarks -v` → 2314 passed, 3 skipped (Windows), 1 unrelated warning * `uv run ruff check .` → all checks passed * `uv run ruff format --check .` → 139 files already formatted * `bash -n` clean on common.sh, both hook scripts, install.sh * Local install at `~/.gemini/config/plugins/mempalace/` verified end- to-end: layout correct, paths absolutized in hooks.json, both hooks fire with realistic camelCase JSON in <1s, wing inference picks `wing_mempalace` from workspacePaths[0], state files all `antigravity_*`-namespaced, second `install.sh` run produces byte-identical output (md5 snapshots match), uninstall removes only the mempalace plugin and leaves all 6 sibling Google plugins untouched. Constraints honoured -------------------- bash 3.2.57 (no mapfile / readarray / declare -A / `${var^^}`), verbatim guarantee on all wake injections, hooks <500ms / startup injection <100ms target (kill-switch path returns in <1.5s in CI), zero new runtime dependencies, no telemetry, no external API, strictly additive (existing Claude/Codex hooks unchanged). Refs: hooks/antigravity/INVESTIGATION.md for the full audit.
There was a problem hiding this comment.
Code Review
This pull request introduces first-class support for Google's Antigravity IDE, adding a new .antigravity-plugin/ package, an idempotent installer script (hooks/antigravity/install.sh), lifecycle hooks for background mining and startup memory injection, comprehensive tests, and documentation. The reviewer identified several key issues in the shell scripts and Python helpers: a critical bug in the save hook where a background subshell attempts to wait on a sibling process (which fails immediately and prematurely deletes the pending marker), an issue where the Python JSON parser catches exceptions internally but still prints the success sentinel (defeating bash-side error detection), a potential octal parsing crash in bash arithmetic if MEMPAL_SAVE_INTERVAL contains leading zeros, path resolution issues when calling the mempalace CLI directly instead of using the resolved Python interpreter, and a redundant, no-op subshell directory change in the installer script.
| if command -v mempalace >/dev/null 2>&1; then | ||
| nohup mempalace mine "$TRANSCRIPT_DIR" \ | ||
| --mode convos \ | ||
| --wing "$WING" \ | ||
| >> "$MEMPAL_AGY_LOG" 2>&1 < /dev/null & | ||
|
|
||
| MINE_PID=$! | ||
| mempal_log "stop" "$CONVERSATION_ID" "mine spawned pid=$MINE_PID wing=$WING" | ||
|
|
||
| # Schedule a marker-cleanup detach so the marker doesn't outlive a | ||
| # crashed mine. We can't `wait` because that would block the hook; | ||
| # instead, fire-and-forget a tiny watcher. | ||
| ( | ||
| wait "$MINE_PID" 2>/dev/null | ||
| rm -f "$PENDING_FILE" 2>/dev/null | ||
| ) >/dev/null 2>&1 < /dev/null & | ||
| else | ||
| mempal_log "stop" "$CONVERSATION_ID" "ERROR: mempalace CLI not on PATH; install or set MEMPAL_PYTHON" | ||
| rm -f "$PENDING_FILE" 2>/dev/null | ||
| fi |
There was a problem hiding this comment.
This block contains two issues:
- Sibling Process Wait Bug (Critical): In bash, a background subshell
( wait "$MINE_PID" ... ) &cannot wait for a sibling process spawned by the parent shell;waitwill fail immediately with an error and proceed to delete thePENDING_FILEinstantly. This defeats the concurrency guard. Using awhile kill -0polling loop resolves this. - Python Interpreter Path (Medium): Running
mempalacedirectly relies on the globalPATH. Running it via"$MEMPAL_PYTHON_BIN" -m mempalaceensures it uses the correctly resolved Python interpreter (respectingMEMPAL_PYTHONand virtual environments).
if "$MEMPAL_PYTHON_BIN" -m mempalace --version >/dev/null 2>&1; then
nohup "$MEMPAL_PYTHON_BIN" -m mempalace mine "$TRANSCRIPT_DIR" \
--mode convos \
--wing "$WING" \
>> "$MEMPAL_AGY_LOG" 2>&1 < /dev/null &
MINE_PID=$!
mempal_log "stop" "$CONVERSATION_ID" "mine spawned pid=$MINE_PID wing=$WING"
# Schedule a marker-cleanup detach so the marker doesn't outlive a
# crashed mine. We can't wait because that would block the hook;
# instead, fire-and-forget a tiny watcher.
(
while kill -0 "$MINE_PID" 2>/dev/null; do
sleep 1
done
rm -f "$PENDING_FILE" 2>/dev/null
) >/dev/null 2>&1 < /dev/null &
else
mempal_log "stop" "$CONVERSATION_ID" "ERROR: mempalace is not runnable via $MEMPAL_PYTHON_BIN; install or set MEMPAL_PYTHON"
rm -f "$PENDING_FILE" 2>/dev/null
fi| (cd "$OLDPWD" 2>/dev/null || cd .) >/dev/null 2>&1 | ||
| local base="${PWD}" |
There was a problem hiding this comment.
The subshell (cd "$OLDPWD" 2>/dev/null || cd .) does not affect the parent shell's working directory, making it a no-op. Furthermore, since the script does not change directories in the main shell, "$PWD" already correctly points to the user's invocation directory. This line can be safely removed.
| (cd "$OLDPWD" 2>/dev/null || cd .) >/dev/null 2>&1 | |
| local base="${PWD}" | |
| local base="${PWD}" |
| try: | ||
| data = json.load(sys.stdin) | ||
| except Exception: | ||
| data = {} |
There was a problem hiding this comment.
The try/except block around json.load(sys.stdin) catches parse failures and sets data = {}, but the script still prints __MEMPAL_PARSE_OK__ on line 208. This prevents the calling bash scripts from detecting JSON parse failures via the sentinel check [ "$_marker" != "__MEMPAL_PARSE_OK__" ]. Letting the exception propagate naturally will cause the Python script to exit with a non-zero status and omit the sentinel, allowing the bash scripts to correctly trigger their defense-in-depth error logging.
| try: | |
| data = json.load(sys.stdin) | |
| except Exception: | |
| data = {} | |
| data = json.load(sys.stdin) |
| mempal_save_interval() { | ||
| local raw="${MEMPAL_SAVE_INTERVAL:-15}" | ||
| case "$raw" in | ||
| ''|*[!0-9]*) printf '15'; return 0 ;; | ||
| esac | ||
| if [ "$raw" -lt 1 ] 2>/dev/null; then | ||
| printf '15' | ||
| return 0 | ||
| fi | ||
| printf '%s' "$raw" | ||
| } |
There was a problem hiding this comment.
In bash, arithmetic expansions like $((COUNT % INTERVAL)) treat numbers with leading zeros as octal. If MEMPAL_SAVE_INTERVAL is configured with a leading zero (e.g., 08 or 09), the hook will crash with a value too great for base syntax error. Stripping leading zeros from the returned interval prevents this issue.
| mempal_save_interval() { | |
| local raw="${MEMPAL_SAVE_INTERVAL:-15}" | |
| case "$raw" in | |
| ''|*[!0-9]*) printf '15'; return 0 ;; | |
| esac | |
| if [ "$raw" -lt 1 ] 2>/dev/null; then | |
| printf '15' | |
| return 0 | |
| fi | |
| printf '%s' "$raw" | |
| } | |
| mempal_save_interval() { | |
| local raw="${MEMPAL_SAVE_INTERVAL:-15}" | |
| case "$raw" in | |
| ''|*[!0-9]*) printf '15'; return 0 ;; | |
| esac | |
| # Strip leading zeros to prevent bash treating it as octal in $(( ... )) | |
| while [ "${raw}" != "${raw#0}" ] && [ "${#raw}" -gt 1 ]; do | |
| raw="${raw#0}" | |
| done | |
| if [ "$raw" -lt 1 ] 2>/dev/null; then | |
| printf '15' | |
| return 0 | |
| fi | |
| printf '%s' "$raw" | |
| } |
|
|
||
| try: | ||
| completed = subprocess.run( | ||
| ['mempalace', 'wake-up', '--wing', wing], |
There was a problem hiding this comment.
Running mempalace directly relies on the global PATH, which might not contain the executable if it was installed in a virtual environment. Using sys.executable with the -m flag ensures that the hook runs mempalace using the exact same Python interpreter that was resolved (respecting MEMPAL_PYTHON).
| ['mempalace', 'wake-up', '--wing', wing], | |
| [sys.executable, '-m', 'mempalace', 'wake-up', '--wing', wing], |
Five fixes for issues called out by the gemini-code-assist[bot] review. Each gets a regression test that locks in the correction. 1. CRITICAL: marker-cleanup watcher used POSIX `wait` on a sibling pid (save hook). bash `wait` only works on direct children of the calling shell — the `( wait $MINE_PID ... ) &` subshell runs as a sibling of MINE_PID, so wait fails immediately and the pending marker is deleted within milliseconds, defeating the concurrency guard. Replace with `while kill -0 $MINE_PID; do sleep 1; done`, which queries pid existence regardless of parent-child relationship. Test: test_save_hook_marker_watcher_uses_kill_polling. 2. Bare `mempalace` console-script invocation in the save hook fails when the venv's bin/ is not on the hook's PATH (e.g. uv tool install in some configurations, manually managed virtualenvs). Switch to `"$MEMPAL_PYTHON_BIN" -m mempalace mine ...` so the resolved interpreter runs the package directly via mempalace/__main__.py. Tests: test_save_hook_uses_python_module_invocation, test_save_hook_missing_mempalace_python_module_does_not_crash. 3. Same issue in the wake hook's inner Python helper. Switch `['mempalace', 'wake-up', ...]` to `[sys.executable, '-m', 'mempalace', 'wake-up', ...]` — sys.executable is the same interpreter that resolved MEMPAL_PYTHON in lib/common.sh. Test: test_wake_hook_uses_sys_executable_module_invocation. 4. The Python parser in lib/common.sh wrapped `json.load` in `try/except` and silently fell back to `data = {}`. The script then printed the `__MEMPAL_PARSE_OK__` sentinel even on parse failure, so the bash sentinel-check on the caller side (`[ "$_marker" != "__MEMPAL_PARSE_OK__" ]`) never triggered the defense-in-depth `input parse failed` branch. Remove the try/except so the exception propagates, Python exits non-zero, and the sentinel is omitted on bad JSON. The traceback still lands in antigravity_last_python_err.log for debugging. Test: test_common_sh_parser_omits_sentinel_on_malformed_json. 5. `mempal_save_interval()` failed to strip leading zeros from MEMPAL_SAVE_INTERVAL. Values like "08" or "09" then crashed the modulo step `$((COUNT % INTERVAL))` because bash arithmetic parses tokens starting with `0` as octal, and 8/9 are not valid octal digits ("value too great for base"). Strip leading zeros while preserving the literal "0" (which is then floored to 15). Test: test_save_hook_handles_leading_zero_save_interval (4 cases). Plus one cosmetic fix in install.sh: removed a no-op `(cd "$OLDPWD" 2>/dev/null || cd .) >/dev/null 2>&1` line in mempal_absolutize(). The subshell cd doesn't affect the parent shell, and the installer never cd's in the main shell anyway, so $PWD is already correct. Verification: * 9 new regression tests, all 65 antigravity tests pass * full repo: 2323 passed (was 2314), 3 skipped, 1 unrelated warning * ruff check + ruff format --check both clean across 139 files * bash -n clean on all four shell files * clean reinstall to ~/.gemini/config/plugins/mempalace/ succeeds * idempotent re-run produces zero file writes (cmp-gated) * both hooks return {} exit 0 with synthetic camelCase stdin Co-authored-by: Cursor <cursoragent@cursor.com>
ReviewUnusually careful, genuinely defensive shell work — fail-open on every path, layered kill switches, sentinel-guarded Python JSON parsing with input sanitization, bash 3.2.57 compat throughout, Cleaner philosophy than the sibling Cursor PR (#1632): the Stop hook only background-mines and never emits a token-spending followup — which is the right call under CLAUDE.md's "Background everything — zero tokens in the chat window." 🟡 Specifics
Cross-cutting (also raised on the Cursor PR #1632)
Minor
Mergeable in principle — no blockers. The atomic-counter and foreground- |
…tate-file GC Addresses igorls' review on PR MemPalace#1633 (antigravity branch only): - Atomic counter write: add mempal_write_counter_atomic (same-dir temp + mv -f rename) and use it in the save hook, replacing the truncate-then -write printf that the comment falsely called "atomic". Concurrent readers now always see a complete value. - Background the expensive probe: `mempalace --version` pays the full chromadb/onnx cold-start import (the mine subparser imports mempalace.miner before argparse handles --version), so running it in the foreground blew the <500ms save budget. Probe + mine + pending -marker cleanup now run in one detached subshell; the foreground returns immediately. This also retires the kill -0 watcher (the prior gemini fix) since cleanup is now sequential within the mine's own shell, removing the sibling-PID hazard entirely. - State-file GC: add mempal_state_ttl_days (default 30, env MEMPAL_STATE_TTL_DAYS) and mempal_gc_stale_state, a daily-throttled sweep (antigravity_last_sweep marker) that removes stale antigravity_save_count_*, antigravity_pending_*, and antigravity_woke_* artifacts. Called after the kill-switch check so a disabled hook touches nothing; specific name globs leave shared logs untouched. Tests: atomic-counter behavior + no-temp-leftover, single-subshell structure (no wait/kill -0/MINE_PID), backgrounded-probe timing (3s stub returns in <2s), async log polling for the missing-module path, GC sweep/throttle/TTL-validation, and GC gated by the kill switch. Also hardens the wake-missing test to pin MEMPAL_PYTHON so a shell -exported interpreter can't defeat the simulation. bash 3.2.57 safe, fail-open on every path, {}-only Stop output. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Thanks @igorls — pushed Must-fix1. Non-atomic counter write — Fixed. Added 2. Expensive foreground This also retires the Non-blocking hygiene (done in this PR)State-file GC — Added Measure-first / deferred
Full antigravity suite (74 tests) green; full repo suite (2331 passed, 3 skipped) green; |
|
Maintainer review (on
The deferred cross-IDE wing-naming and the shared-file merge-sequencing with #1632 are fine to settle at coordination/merge time — agreed those shouldn't be pre-empted here. |
`uv tool install mempalace` / `pipx install` place the mempalace console scripts in an isolated environment whose interpreter is not the system python3. mempal_resolve_python previously resolved `command -v python3`, landing on a Python that cannot import mempalace: the `-m mempalace --version` probe failed and mining silently never fired (hit by a real user on PR MemPalace#1633). Resolution now derives the interpreter from the mempalace-mcp / mempalace console-script shebang on PATH (the same script the MCP server launches) before falling back to python3. It is pure shebang parsing + stat — no Python subprocess at source time — so the hook performance budget is preserved. An env-style `#!/usr/bin/env python` shebang and a non-executable interpreter are both rejected and fall through. MEMPAL_PYTHON remains the explicit override. Adds 6 resolver regression tests, documents resolution + MEMPAL_PYTHON in the guide and hooks README (fixing the stale `command -v mempalace` note), and a CHANGELOG entry. Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
Adds first-class integration with Google's Antigravity IDE as a third sibling to the existing Claude Code and Codex hook integrations.
This is strictly additive: no existing files in
mainare restructured. The integration scope is bounded by what Google's official Antigravity docs actually expose — the full audit (with verbatim quotes + URLs + dates + omitted-surface rationale) is inhooks/antigravity/INVESTIGATION.md.What ships
.antigravity-plugin/plugin.json— verified minimal shape.antigravity-plugin/mcp_config.json— registersmempalace-mcpstdio.antigravity-plugin/skills/mempalace/SKILL.md— real file (no symlinks)hooks/antigravity/mempal_save_hook_antigravity.sh— counter + auto-minehooks/antigravity/mempal_wake_hook_antigravity.sh— gated toinvocationNum==1hooks/antigravity/install.sh— idempotent,__PLUGIN_DIR__substitution, basename-guarded uninstallwebsite/guide/antigravity.md+ sidebar entry inconfig.mtsexamples/antigravity/{hooks.json,mcp_config.json,README.md}What is deliberately NOT shipped (with reasons)
Documented in
hooks/antigravity/INVESTIGATION.md:PreCompactequivalent — no external Antigravity surface; only the in-process Python SDK has@hooks.on_compaction. TheStophook still catches end-of-turn state; auto-compaction mid-turn loses some content but the verbatim transcript ingestion covers long-term recall.commands/— Antigravity has nocommands/plugin component. Folded intoSKILL.md's## Common operationssection instead.rules/— would risk colliding with user project rules. Users opt in by dropping their own under.agents/rules/.permissionsfield inplugin.json— fabricated by the third-partyantigravity-pluginscommunity skill; no real Google-shipped plugin uses it. We pin to the verified-minimal shape and have a regression test.~/.gemini/config/plugins/mempalace/is the canonical UX. Workspace-scoped install is documented inhooks/antigravity/README.md.PreToolUse/PostToolUsehooks — out of scope for v1, surface is real and noted for future work.Constraints honoured
mapfile/readarray/declare -A/${var^^}; sentinel-guarded JSON parser usessed -n 'Np'like the existing Claude Code hook.mempalace wake-upstdout through unchanged viajson.dumps; never paraphrased.{}in <500ms on every kill-switch / gate path (test asserts <1.5s on CI), wake hook enforces a 500ms hard timeout onmempalace wake-up.bash+python3(already required by every existing hook).Critical safeguards
{"decision":"continue"}. That output would force Antigravity into an infinite agent re-execution loop. The save hook'smempal_emit_stop_passhard-codesprintf '{}\n'. Tested directly intest_save_hook_never_emits_decision_continue.MEMPAL_SAVE_INTERVALfloored to >= 1. A user settingMEMPAL_SAVE_INTERVAL=0(thinking it disables saves) would otherwise bash-divide-by-zero. Tested intest_save_hook_floors_zero_save_interval_to_avoid_div_by_zero.fullyIdle == falsedefers. Background commands still running means the transcript is in motion; better to skip than ingest a half-finished transcript.mempalace, refuses if the directory has noplugin.jsonor names a different plugin. Tested in three separate refusal tests.__PLUGIN_DIR__substitution + relative-path absolutization. The cursor PR review caught that a relative--install-dirwould bake a relative path intohooks.jsonthat Antigravity couldn't resolve at runtime. Tested intest_relative_install_dir_is_absolutized.Test plan
uv run pytest tests/ --ignore=tests/benchmarks -v→ 2314 passed, 3 skipped, 1 unrelated warninguv run ruff check .→ all checks passeduv run ruff format --check .→ 139 files already formattedbash -nclean onlib/common.sh, save hook, wake hook,install.sh~/.gemini/config/plugins/mempalace/succeedshooks.jsoncarries absolute paths (no__PLUGIN_DIR__leak)wing_mempalacefromworkspacePaths[0]antigravity_*-namespaced (no Claude/Codex collision)install.shrun produces byte-identical output (md5 snapshots match)mempalace, leaves all 6 sibling Google plugins (firebase,chrome-devtools-plugin,google-antigravity-sdk,android-cli-plugin,custom-engineering-skills,modern-web-guidance-plugin) untouchedmempalace-mcpis on$PATHmempalaceshows in the MCP store, skillmempalaceappears in the skill list, Stop event fires save hook on conversation end, PreInvocation event fires wake hook on first model callNew tests (56)
tests/test_antigravity_plugin_manifest.py— 11 tests — schema contract on.antigravity-plugin/, including guards against fabricated fields and symlink leaks.tests/test_antigravity_hooks_shell.py— 31 tests — invokes bash hooks via subprocess, asserts{}on every failure path, kill-switch coverage (env vars +config.json+ palace nuke), divide-by-zero floor, traversal rejection, namespacing, wing inference, decision-continue refusal.tests/test_antigravity_hooks_install.py— 14 tests —--dry-runside-effect-free, real install layout, executable bits preserved, byte-identical idempotent re-runs, basename-match uninstall safety + two further refusal modes, relative-path absolutization, unknown-arg failure. Skipped on Windows.Branching
Branched from
origin/main(per pre-flight constraint), targetsdevelopfor normal feature flow.References
hooks/antigravity/INVESTIGATION.md— full surface audithooks/antigravity/STDIN_SHAPE.md— exact wire format per event with worked exampleshooks/antigravity/README.md— operator-side docs + troubleshootingwebsite/guide/antigravity.md— user-facing guideMade with Cursor