Skip to content

feat: add Antigravity (agy) co-existence support - #341

Merged
evansenter merged 4 commits into
mainfrom
feat/agy-dual-setup
Sep 2, 2026
Merged

feat: add Antigravity (agy) co-existence support#341
evansenter merged 4 commits into
mainfrom
feat/agy-dual-setup

Conversation

@evansenter

@evansenter evansenter commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Summary

Enables dual-agent co-existence between Claude Code and Google Antigravity (agy), allowing either tool to be used seamlessly without duplicating configurations, rules, skills, or MCP services.

Changes

  • Repository Rules: Symlink AGENTS.md -> CLAUDE.md in repository root so both agents read the exact same instructions.
  • Global Rules: Provision ~/.gemini/GEMINI.md -> ~/.claude/CLAUDE.md in bootstrap.sh, pointing directly into the dotfiles repo so uninstall.sh automatically reclaims it.
  • Shared Skills: Add home/.gemini/config/skills.json pointing to ~/.claude/skills so all current and future skills are discovered by AGY automatically.
  • Lifecycle Hooks & Zellij Status: Add home/.gemini/config/hooks.json mapping AGY PreInvocation and Stop hooks to zj-status.sh ("working" / "waiting"), guarded with < /dev/null so stdin never blocks.
  • MCP Servers: Add install_agy_mcp_servers to bootstrap.sh to provision github (with literal ${GITHUB_TOKEN} placeholder) and obsidian MCP servers for AGY matching Claude's setup without risking set -e aborts.
  • Package Management: Add cask "antigravity-cli" to Brewfile.ai.
  • Teardown & Cleanup: Clean up empty .gemini directories in uninstall.sh.
  • Test Fix: Fix stdin piping in tests/test-hooks.sh for tmux-status.sh and zj-status.sh graceful degradation tests to prevent hangs when run in tasks with open stdin.
  • Tests & Docs: Add behavioral unit test in tests/test-bootstrap.sh verifying symlink_agy_configs link creation, idempotency, and stale recovery with proper return status accumulation and extraction guards. Update CLAUDE.md and README.md.

Verification

Live Verification:

  • brew info --cask antigravity-cli verified formula: antigravity-cli (Google Antigravity CLI): 1.1.22
  • agy mcp add --header 'Authorization: Bearer ${GITHUB_TOKEN}' github https://api.githubcopilot.com/mcp/ verified writing literal ${GITHUB_TOKEN} placeholder to ~/.gemini/config/mcp_config.json
  • test_symlink_agy_configs_behavior verified behaviorally in sandbox (including intentional mutation test verifying failure detection)

Quality Gates:

  • make lint (shellcheck on all .sh files)
  • make test (bash & zsh syntax checks)
  • make test-hooks (129/129 passed)
  • make test-bootstrap (53/53 passed)
  • make check (all gates green)

@evansenter
evansenter force-pushed the feat/agy-dual-setup branch from 3dcd22d to fc64e0b Compare August 31, 2026 21:14
Comment thread bootstrap.sh Outdated
if ! echo "$mcp_list" | grep -q "github"; then
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
echo "Installing agy GitHub MCP server..."
agy mcp add --header "Authorization: Bearer ${GITHUB_TOKEN}" github https://api.githubcopilot.com/mcp/ 2>/dev/null || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Important] The token value is interpolated into the header at install time, unlike install_claude_mcp_servers (bootstrap.sh:1169), which stores the literal ${GITHUB_TOKEN} placeholder in single quotes precisely so it gets expanded per request. The baked value is also a long-lived credential written in plaintext into the agy config file.

Fails when: the user rotates GITHUB_TOKEN in ~/.extra and re-runs ./bootstrap.sh -fagy mcp list still contains github, so this block is skipped and agy keeps sending the revoked bearer token → every agy GitHub MCP call 401s, with no path back short of manually removing the server.

If agy supports env placeholders, single-quote the header as the Claude path does; if it does not, consider a remove-then-add on each run so rotation actually takes effect.

Comment thread home/.gemini/config/hooks.json Outdated
"PreInvocation": [
{
"type": "command",
"command": "~/.claude/hooks/zj-status.sh working; echo '{}'"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Important] zj-status.sh starts with cat > /dev/null to consume the Claude hook JSON, and that read blocks if the caller neither writes to nor closes stdin. This PR adds the echo "" | pipes in tests/test-hooks.sh because of exactly that hazard (per the PR body: prevent hangs when run in tasks with open stdin). Nothing here guarantees agy pipes JSON and closes the pipe.

Fails when: agy runs the PreInvocation command with an inherited or open stdin (a terminal, or an idle pipe) → cat never sees EOF → the hook never exits and agy blocks before every invocation.

Cheap, unconditional fix: ~/.claude/hooks/zj-status.sh working < /dev/null; echo {} — same for Stop.

{
"entries": [
{
"path": "~/.claude/skills"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Suggestion] Two unverified assumptions here and in hooks.json: that agy reads ~/.gemini/config/skills.json and ~/.gemini/config/hooks.json at those exact paths and schemas, and that it expands a leading ~ inside a JSON string value (many tools do not — $HOME or an absolute path is safer). The verification section lists only make targets, and the new bootstrap tests just grep for function names, so nothing in CI or the PR body shows agy actually picking up the skills or firing the hooks. Worth noting a live check in the PR body (skills listed in an agy session, zjstatus flipping to working) — otherwise the whole bridge can be silently inert.

Comment thread bootstrap.sh Outdated
if ! echo "$mcp_list" | grep -q "obsidian"; then
echo "Installing agy Obsidian MCP server..."
if is_gateway_host; then
agy mcp add obsidian http://localhost:3010/mcp 2>/dev/null || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Suggestion] 2>/dev/null || true on the agy mcp add calls discards both stderr and the exit status, so a failed registration still prints only "Installing agy Obsidian MCP server..." and the run looks successful. install_claude_mcp_servers lets its claude mcp add calls surface errors. Consider dropping 2>/dev/null, or emitting a Warning: on failure, so broken agy CLI flag syntax is visible rather than silently skipped on every subsequent run.

Comment thread bootstrap.sh Outdated
mkdir -p "$HOME/.gemini"
if [[ ! -L "$HOME/.gemini/GEMINI.md" || "$(readlink "$HOME/.gemini/GEMINI.md")" != "$HOME/.claude/CLAUDE.md" ]]; then
rm -f "$HOME/.gemini/GEMINI.md"
ln -s "$HOME/.claude/CLAUDE.md" "$HOME/.gemini/GEMINI.md"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Suggestion] ~/.gemini/GEMINI.md points at ~/.claude/CLAUDE.md rather than into the repo, so the find ~ -type l -lname "$DOTFILES_DIR/home/*" sweep in uninstall.sh will not match it. After an uninstall removes ~/.claude/CLAUDE.md, this link is left behind dangling. Adding it to uninstall.sh (or handling it the way cleanup_legacy_configs handles stale links) would keep uninstall complete.

Comment thread Brewfile.ai
cask "codex"

# Google Antigravity CLI
cask "antigravity-cli"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Suggestion] Worth confirming the cask is actually named antigravity-cli (brew info --cask antigravity-cli). A nonexistent cask degrades gracefully — brew bundle failures are tolerated at bootstrap.sh:1030 — but it would print a warning on every -p run while never installing anything, and prompt_ai_install would still take the agy-installed branch only on machines where it was installed by other means. Also, the added trailing blank line leaves the file ending in two newlines.

Comment thread tests/test-bootstrap.sh Outdated
}

test_symlink_agy_configs_exists() {
grep -q 'symlink_agy_configs' "$BOOTSTRAP"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Suggestion] These two tests assert only that the function names appear somewhere in bootstrap.sh; they would still pass if the bodies were empty or the calls were never wired into sync_dotfiles. symlink_agy_configs is pure filesystem work, so a behavioral test is cheap: point HOME at a temp dir containing .claude/CLAUDE.md, then assert (a) the link is created, (b) a re-run is a no-op, (c) an existing GEMINI.md with the wrong target gets replaced.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Prompt: evansenter/dotfiles/.../claude-review.md

Code Review — Round 1

Summary

Clean, well-scoped dual-agent bridge: the recursive symlink_dotfiles already handles home/.gemini/config/*.json, the new bootstrap functions mirror their Claude equivalents (including is_gateway_host gating), and the test-hooks.sh stdin fix is applied consistently across every remaining tmux-status.sh / zj-status.sh invocation. Two blocking findings, both in newly added wiring.

Findings

  • [Important] bootstrap.sh:1221 — the agy GitHub MCP header interpolates the literal GITHUB_TOKEN value at install time, where the Claude path (bootstrap.sh:1169) deliberately stores the placeholder for per-request expansion.
  • [Important] home/.gemini/config/hooks.json:6 — the hook command has no stdin guard, and zj-status.sh blocks on cat > /dev/null until EOF.
  • 5 Suggestions posted inline: agy config path/tilde validation, silenced agy mcp add errors, uninstall leaving ~/.gemini/GEMINI.md dangling, cask name, grep-only bootstrap tests.

Previously Addressed (Filtered)

None — no prior automated review rounds on this PR.

Verdict

REQUEST_CHANGES — rotating GITHUB_TOKEN in ~/.extra leaves agy sending the revoked bearer token indefinitely, because agy mcp list still reports github so bootstrap skips re-registration; and if agy invokes hooks without closing stdin, zj-status.sh never exits and blocks every agy invocation. Both are one-line fixes.


Automated review by Claude Code

Comment thread tests/test-bootstrap.sh
ln -sf "/tmp/wrong-target" "$fake_home/.gemini/GEMINI.md"
HOME="$fake_home" symlink_agy_configs >/dev/null
[[ "$(readlink "$fake_home/.gemini/GEMINI.md")" == "$SCRIPT_DIR/../home/.claude/CLAUDE.md" ]]
rm -rf "$fake_home"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Important] This test always passes. run_test invokes it as if eval "$cmd", and a command in an if condition disables set -e for the whole call tree — so the bare [[ ]] assertions on lines 320, 321, 326 and 331 do not abort the function when they fail. With no explicit return, the function exit status is that of its last command, rm -rf "$fake_home", which succeeds unconditionally.

This is the hazard the neighbouring tests already document and guard against — test_legacy_cleanup_keeps_live_iterm_profile (line 153) comments "run_test disables set -e" and uses the local ok=0; [[ ... ]] || ok=1; rm -rf "$home"; return $ok pattern for exactly this reason.

Fails when: symlink_agy_configs regresses so it never creates the link (e.g. home/.claude/CLAUDE.md is renamed, so the [[ -f "$target_claude_md" ]] guard goes false) → every assertion in this test fails, execution continues to rm -rf, the function returns 0 → make test-bootstrap prints a passing symlink_agy_configs behavior line and make check stays green while the feature is broken.

Suggested fix, matching the existing pattern: accumulate the assertions into a local ok=0 / || ok=1 variable, then rm -rf "$fake_home" and return $ok.

Comment thread tests/test-bootstrap.sh Outdated
}

_load_symlink_agy_configs() {
eval "$(sed -n '/^symlink_agy_configs() {/,/^}/p' "$BOOTSTRAP" \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Suggestion] Related to the finding below, and worth fixing in the same pass: this extractor has no guard for an empty sed result. _run_cleanup_legacy_configs (line 181) does [[ -n "$body" ]] || return 1, with a comment explaining it exists "so a reformatted header fails the negative test loudly instead of passing vacuously on nothing ran".

Here, if the symlink_agy_configs() header is ever reformatted so /^symlink_agy_configs() {/ stops matching, eval "" defines nothing and the later calls fail with command not found. Capturing the body into a variable and checking it is non-empty first would make the reason for that failure obvious rather than a confusing readlink mismatch.

Comment thread bootstrap.sh Outdated
if ! echo "$mcp_list" | grep -q "github"; then
echo "Installing agy GitHub MCP server..."
# shellcheck disable=SC2016
agy mcp add --header 'Authorization: Bearer ${GITHUB_TOKEN}' github https://api.githubcopilot.com/mcp/

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Suggestion] Dropping 2>/dev/null || true surfaces errors as round 1 asked, but it also removed the failure tolerance. bootstrap.sh runs under set -euo pipefail (line 2) and sync_dotfiles is called unguarded (line 1408), so a non-zero agy mcp add now aborts the entire run — install_tmux_plugin_manager, init_submodules, the bat/btop/eza/yazi/zellij themes, install_launch_agents, cleanup_legacy_cron and cleanup_legacy_configs all sit after this and would never run, leaving a half-synced machine.

That exposure is real but not new in kind — install_claude_mcp_servers has the identical shape with bare claude mcp add, so this is convention-consistent, which is why it is a Suggestion rather than a blocker. The difference is confidence: claude mcp add syntax is in daily use, whereas nothing in this PR or CI exercises agy mcp add, and this form omits any transport flag where the Claude equivalent needs --transport http for a remote HTTP server. If agy defaults to stdio and rejects a bare URL, this line is the abort.

The middle ground round 1 was pointing at keeps both properties — append || echo " Warning: agy mcp add github failed" so the error is visible without taking down the rest of phase 2. Same for the two Obsidian calls on lines 1241 and 1248.

{
"entries": [
{
"path": "~/.claude/skills"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Suggestion] Re-raising from round 1 since it still gates whether any of this works: nothing in the PR body, CI, or the new tests demonstrates that agy actually reads ~/.gemini/config/skills.json and ~/.gemini/config/hooks.json at these paths and schemas, or that it expands a leading ~ inside a JSON string value (many tools do not — $HOME or an absolute path is safer, though an absolute path is awkward to keep portable in a tracked file).

The verification section lists only make targets, all of which are static checks against the repo — none of them can observe agy. If any of the three assumptions is wrong the bridge is silently inert: no error, no skills, no status flip, and nothing here would catch it.

A line in the PR body recording a live check (skills listed in an agy session, zjstatus flipping to the working indicator on invocation) would close this out cheaply. The same applies to the antigravity-cli cask name, which is still unconfirmed from round 1.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Prompt: evansenter/dotfiles/.../claude-review.md

Code Review — Round 2

Summary

Round 1 blocking findings are properly fixed: the agy GitHub MCP header now stores the literal ${GITHUB_TOKEN} placeholder (bootstrap.sh:1228) matching the Claude path, and both hooks.json commands redirect < /dev/null so the cat > /dev/null in zj-status.sh cannot block. The GEMINI.md retarget to $dotfiles_dir/home/.claude/CLAUDE.md is a nice fix — it puts the link inside the uninstall.sh -lname "$DOTFILES_DIR/home/*" sweep, resolving the dangling-link concern at the root rather than patching it.

One blocking finding, in the behavioral test added this round. test_symlink_agy_configs_behavior (tests/test-bootstrap.sh:314) returns the exit status of its trailing rm -rf "$fake_home", so it passes unconditionally regardless of its four assertions. This repo already documents that exact hazard — test_legacy_cleanup_keeps_live_iterm_profile carries the comment "run_test disables set -e" and uses an ok variable to work around it — but the new test does not follow that pattern.

Findings

  • [Important] tests/test-bootstrap.sh:332 — the new behavioral test can never fail.
  • 3 Suggestions posted inline: missing extraction guard in _load_symlink_agy_configs, set -e abort risk from the now-unguarded agy mcp add calls, and the still-unverified agy config paths.

Previously Addressed (Filtered)

No "Feedback Addressed" comments were posted, but round 1 findings were verified as fixed in the diff and are not re-raised: token interpolation, hook stdin, uninstall teardown, grep-only tests (now behavioral), Brewfile trailing newline. Two round 1 Suggestions remain open and are re-raised once: the unverified agy config paths/schemas, and the antigravity-cli cask name.

Verdict

REQUEST_CHANGES — the round 2 behavioral test for symlink_agy_configs returns the rm -rf exit status, so the "49/49 passed" figure in the PR body does not actually cover the function the test was added to cover. One-line fix: accumulate the assertions into a status variable and return it after the cleanup, as the neighbouring tests do.


Automated review by Claude Code

Comment thread tests/test-bootstrap.sh

test_symlink_agy_configs_behavior() {
_load_symlink_agy_configs || return 1
local fake_home; fake_home=$(mktemp -d)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Suggestion] Missing the empty-mktemp guard that both neighbouring temp-HOME tests carry. test_legacy_cleanup_reclaims_dangling_iterm_profile (line 142) and test_legacy_cleanup_keeps_live_iterm_profile (line 159) each do [[ -n "$home" ]] || return 1 with the comment "Unguarded, an empty $home makes the paths below absolute (/Library/...), aiming the sandbox at the system tree."

The same reasoning applies verbatim here: if mktemp -d fails, fake_home is empty, run_test has disabled set -e, and the function runs HOME="" symlink_agy_configs, which does mkdir -p "/.gemini" and ln -s ... "/.gemini/GEMINI.md" against the filesystem root. On a CI runner with a writable root that succeeds and leaves a stray link behind; the three readlink assertions then fail, so the test still reports failure, but it has already written outside the sandbox.

A one-line [[ -n "$fake_home" ]] || return 1 after line 319 matches the existing pattern and closes it.

Comment thread tests/test-bootstrap.sh
body=$(sed -n '/^symlink_agy_configs() {/,/^}/p' "$BOOTSTRAP" \
| sed -e 's|^\([[:space:]]*\)dotfiles_dir="\$(cd .*|\1dotfiles_dir="${TEST_DOTFILES_DIR:-$SCRIPT_DIR/..}"|')
[[ -n "$body" ]] || return 1
eval "$body"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Suggestion] eval "$body" runs in the caller shell here, so the rewritten symlink_agy_configs (with dotfiles_dir replaced by the TEST_DOTFILES_DIR substitution) is defined globally for every test that runs after this one. _run_cleanup_legacy_configs (line 182) deliberately wraps its eval in a subshell with the comment "eval defines cleanup_legacy_configs globally regardless of nesting, and so do the stubs — scoping them here keeps them out of later tests."

Nothing downstream calls symlink_agy_configs today, so this is inert rather than broken — but it is the one place in the file that departs from a convention the file explains in a comment, and a future test that extracts the real function would silently get this doctored copy instead. Wrapping the eval plus the three call sites in a subshell (as _run_cleanup_legacy_configs does) would keep the two extractors symmetric.

Comment thread CLAUDE.md Outdated
8. Installs LaunchAgents (macOS) and cron jobs
9. Clears stale symlinks for configs the repo no longer tracks (OpenClaw, spotify-player, iTerm2 dynamic profile) — see `cleanup_legacy_configs`
5. Configures Claude Code `settings.local.json` with remote MCP URLs (skipped on gateway host)
6. Configures Antigravity (`agy`) bridge configs (`skills.json`, `hooks.json`, `GEMINI.md`) and MCP servers

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Suggestion] Two small overstatements in the new docs.

Step 6 says bootstrap "Configures Antigravity (agy) bridge configs (skills.json, hooks.json, GEMINI.md)", but symlink_agy_configs (bootstrap.sh:1193) only handles GEMINI.md. skills.json and hooks.json reach ~/.gemini/config/ via the generic symlink_dotfiles sweep in step 2, exactly like every other file under home/ — which is worth saying explicitly, since it is why the "Adding new configs" recipe below applies to them unchanged and why uninstall.sh reclaims them.

Relatedly, line 106 says "AGENTS.md symlinked to CLAUDE.md in repository roots" (plural). Nothing provisions that per-repo; it is a single symlink committed to this repo root. "in this repo root" would avoid implying a bootstrap step that does not exist.

claude[bot]
claude Bot previously approved these changes Sep 2, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Prompt: evansenter/dotfiles/.../claude-review.md

Code Review — Round 3

Summary

The round 2 blocker is properly fixed. test_symlink_agy_configs_behavior (tests/test-bootstrap.sh:317) now uses the local ok=0 / || ok=1 / return $ok pattern the neighbouring test_legacy_cleanup_* tests document, so all four assertions are load-bearing, and _load_symlink_agy_configs gained the [[ -n "$body" ]] || return 1 extraction guard so a reformatted function header fails loudly instead of vacuously.

I re-verified the surrounding wiring rather than taking it on trust:

  • symlink_dotfiles (bootstrap.sh:198) walks find "$dotfiles_dir/home" -type f with exclusions only under .claude/, so home/.gemini/config/{skills,hooks}.json are picked up and symlinked to ~/.gemini/config/.
  • The uninstall.sh sweep find ~ -maxdepth 4 -type l -lname "$DOTFILES_DIR/home/*" reaches ~/.gemini/GEMINI.md (depth 2) and ~/.gemini/config/*.json (depth 3), so the round 1 dangling-link concern is genuinely closed by the retarget, and the rmdir calls are a correct no-op when agy state lives in ~/.gemini.
  • Every tmux-status.sh / zj-status.sh invocation in tests/test-hooks.sh (lines 1194, 1205, 1216, 1227, 1239, 1251, 1277, 1286) now feeds stdin — the fix is complete, not partial.
  • agy mcp add ... || echo " Warning: ..." restores set -e tolerance while keeping failures visible, which is the middle ground round 2 asked for.

Findings

No Critical or Important findings. Three Suggestions posted inline, all on test hygiene and docs:

  • tests/test-bootstrap.sh:319 — the new test omits the [[ -n "$fake_home" ]] guard that the two neighbouring temp-HOME tests carry with an explanatory comment.
  • tests/test-bootstrap.sh:314_load_symlink_agy_configs evals into the global shell, unlike _run_cleanup_legacy_configs, which subshells specifically to keep its rewritten definitions out of later tests.
  • CLAUDE.md:57 — step 6 and the new Key Components entry each claim slightly more than symlink_agy_configs and the committed AGENTS.md actually do.

Previously Addressed (Filtered)

Round 1 and round 2 findings verified fixed in the diff and not re-raised: token interpolation, hook stdin guard, uninstall teardown, grep-only tests, Brewfile.ai trailing newline, the always-passing behavioral test, and the missing extraction guard. The agy mcp add failure-tolerance Suggestion from round 2 is addressed. The antigravity-cli cask name is now confirmed in the PR body (1.1.22).

One Suggestion remains genuinely open and is not re-raised inline, having been posted in both prior rounds: nothing yet demonstrates that agy reads ~/.gemini/config/{skills,hooks}.json at those paths and schemas, or that it expands a leading ~ inside a JSON string value. The PR body Live Verification section now covers agy mcp add and the cask, but not the two bridge config files. If either assumption is wrong the bridge is silently inert. It is non-blocking, and repeating it a third time would be noise rather than signal.

Verdict

APPROVE — no blocking findings. The round 2 blocker is fixed and the fix follows the pattern this repo already documents; everything remaining is polish.


Automated review by Claude Code

Comment thread tests/test-bootstrap.sh
_load_symlink_agy_configs() {
local body
body=$(sed -n '/^symlink_agy_configs() {/,/^}/p' "$BOOTSTRAP" \
| sed -e 's|^\([[:space:]]*\)dotfiles_dir="\$(cd .*|\1dotfiles_dir="${TEST_DOTFILES_DIR:-$SCRIPT_DIR/..}"|')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Suggestion] Two small things about this rewrite, now that the subshell isolation is in place:

  1. TEST_DOTFILES_DIR is never set by any caller in this file, so the default-value branch is a dead knob — the SCRIPT_DIR/.. fallback is always what runs. Either drop the override in favour of assigning SCRIPT_DIR/.. directly, or set TEST_DOTFILES_DIR from test_symlink_agy_configs_behavior so it actually exercises a second root.

  2. The rewrite replaces the pwd -P subshell with a relative path, so the test asserts the link target is SCRIPT_DIR/../home/.claude/CLAUDE.md while production writes a pwd -P-normalized absolute path. That is inherent to substituting the whole line rather than just the value, but it does mean the assertion cannot catch a regression in how the target path is formed — only in its suffix. Feeding a resolved directory through TEST_DOTFILES_DIR (per point 1) would let the assertion compare absolute paths and close that gap.

Comment thread tests/test-bootstrap.sh
# sync_dotfiles must invoke symlink_agy_configs and install_agy_mcp_servers
local sync_body
sync_body=$(sed -n '/^sync_dotfiles()/,/^}/p' "$BOOTSTRAP")
echo "$sync_body" | grep -v '^\s*#' | grep -q 'symlink_agy_configs' && \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Suggestion] \s is a GNU-grep extension, not POSIX BRE. Under BSD grep (macOS, the primary target per CLAUDE.md), the pattern ^\s*# matches a line starting with zero-or-more literal s characters followed by # — so tab-indented comments are not filtered and this grep -v is effectively inert there.

It happens not to matter today, because the comment above the calls in sync_dotfiles (# Configure Antigravity (AGY) bridge configs & MCP servers) does not contain either function name. But the filter will not do its job if that comment is ever reworded to name them, and the test would then pass on a sync_dotfiles where both calls had been commented out.

^[[:space:]]*# is portable and matches the bracket-expression style the rest of this file already uses.

Comment thread CLAUDE.md
- Session name cached in `$TMPDIR/claude-statusline/` (pre-populated by session-start hook)

**Antigravity (`agy`) Co-existence** (`home/.gemini/`) - Dual-agent support alongside Claude Code:
- `AGENTS.md` symlinked to `CLAUDE.md` in repository roots for shared repo guidelines

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Suggestion] "in repository roots" (plural) reads as if bootstrap provisions an AGENTS.md symlink in every repo it touches. It does not — this is a single symlink committed at the root of this repo, and test_agents_md_symlink_exists asserts exactly that one file.

Something closer to "AGENTS.md committed at this repo root as a symlink to CLAUDE.md, so agy reads the same repo guidelines" would match what the diff actually does. Same nit as the step 6 wording you already tightened in this commit.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Prompt: evansenter/dotfiles/.../claude-review.md

Code Review — Round 4

Summary

This round (e47a593) is docs and test hygiene only, and it closes all three round 3 Suggestions cleanly:

  • test_symlink_agy_configs_behavior now carries the empty-mktemp guard that both neighbouring temp-HOME tests carry, so a failed mktemp -d can no longer turn the trailing rm -rf loose on a path derived from an empty variable.
  • All three symlink_agy_configs invocations moved into subshells, so the sed-rewritten definition no longer leaks into the global test shell where a later test could pick up the mutated function — matching what _run_cleanup_legacy_configs already does. The || exit 1 inside each subshell keeps the extraction guard load-bearing, and || ok=1 on each keeps the accumulate-and-return pattern intact, so all four assertions still gate the result.
  • CLAUDE.md step 6 now claims only GEMINI.md plus MCP servers (skills.json and hooks.json arrive via the generic symlink_dotfiles walk, not symlink_agy_configs), and the Key Components entry points GEMINI.md at home/.claude/CLAUDE.md in the repo — which is what the code links, and what makes the uninstall.sh -lname sweep reclaim it.

I re-checked the bootstrap wiring rather than assuming it was untouched: symlink_agy_configs and install_agy_mcp_servers remain inside the INSTALL_AI block after configure_claude_local_settings; the agy mcp add ... || echo " Warning: ..." calls still keep set -e tolerant while surfacing failures; and the is_gateway_host branch still mirrors the Claude path.

Findings

No Critical or Important findings. Three Suggestions posted inline, all on test and doc precision:

  • tests/test-bootstrap.sh:312TEST_DOTFILES_DIR is never set by any caller, and the line-level rewrite drops pwd -P, so the assertions pin an unnormalized path.
  • tests/test-bootstrap.sh:305 — the comment filter uses \s, a GNU-grep extension, so it is inert under BSD grep on macOS.
  • CLAUDE.md:106 — "in repository roots" reads as if bootstrap creates AGENTS.md per repo; it is one committed symlink in this repo.

Previously Addressed (Filtered)

Rounds 1-3 findings verified fixed and not re-raised: the literal GITHUB_TOKEN placeholder, the hooks.json stdin guard, uninstall teardown and the dangling GEMINI.md, grep-only bootstrap tests replaced by a behavioral one, the always-passing rm -rf return status, the missing extraction guard, agy mcp add failure tolerance, the antigravity-cli cask name, the mktemp guard, the eval-into-global-shell leak, and the CLAUDE.md overstatements.

Still open and deliberately not re-raised for a fourth time: nothing yet demonstrates that agy reads ~/.gemini/config/skills.json and hooks.json at those paths and schemas, or that it expands a leading tilde and shell syntax inside a JSON command string. If either assumption is wrong the bridge is silently inert rather than broken. It stays non-blocking, and a fourth posting would be noise rather than signal.

Verdict

APPROVE - No blocking findings. Round 3 Suggestions are all addressed, and this round adds no new surface beyond docs and test isolation; everything remaining is polish.


Automated review by Claude Code

@evansenter
evansenter merged commit 1f331f8 into main Sep 2, 2026
12 checks passed
@evansenter
evansenter deleted the feat/agy-dual-setup branch September 2, 2026 17:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant