v1.91.67.0 feat(hosts): Antigravity CLI (agy) host (#3089)

* feat(hosts): Antigravity CLI (agy) host

Adds Google's Antigravity CLI as an experimental host through defineHost():
skills in ~/.gemini/antigravity-cli/skills/gstack-*, a runtime root with
.source-path for /gstack-upgrade, names equal to their gstack-<skill>
directories, name+description frontmatter with the 1024-char limit, the
Gemini overlay, and a preamble glossary mapping Claude tool names to
Antigravity's documented tools (run_command, view_file, write_to_file,
replace_file_content, grep_search, find_by_name, invoke_subagent,
ask_question).

Setup: ./setup --host agy (alias antigravity), --host auto on the agy
binary or ~/.gemini/antigravity-cli, never a Gemini-CLI-only ~/.gemini.
Registry tier, --status known roots, uninstall and team-init cover it.
Gemini CLI's ~/.gemini/skills and settings are never touched.

Co-authored-by: Shagun Prasad <109907086+0xshae@users.noreply.github.com>
Co-authored-by: ManchalaSashank <134257068+ManchalaSashank@users.noreply.github.com>
Co-authored-by: VoIP Expert <5742479+voipexpert@users.noreply.github.com>
Co-authored-by: Mattias Petersson <93470851+KiDDarn@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(agy): golden refresh after main merge; agy is recorded as an installed render host

* v1.91.67.0: CHANGELOG, version

---------

Co-authored-by: Shagun Prasad <109907086+0xshae@users.noreply.github.com>
Co-authored-by: ManchalaSashank <134257068+ManchalaSashank@users.noreply.github.com>
Co-authored-by: VoIP Expert <5742479+voipexpert@users.noreply.github.com>
Co-authored-by: Mattias Petersson <93470851+KiDDarn@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-08 20:05:06 +00:00
1 parent 8bb6d65270
commit 54efba6dd5
22 files changed
+1644 -32

No files matched your search

+1
View File
@@ -35,6 +35,7 @@ lib/cso/images/gstack-cso-preparation
.slate/ .slate/
.cursor/ .cursor/
.copilot/ .copilot/
.agy/
.openclaw/ .openclaw/
.hermes/ .hermes/
.gbrain/ .gbrain/
+31
View File
@@ -1,5 +1,36 @@
# Changelog # Changelog
## [1.91.67.0] - 2026-10-08
**Antigravity CLI is now a gstack host.**
Eight contributors sent PRs asking for Antigravity support. This release builds one host on the current host system, using paths and tool names checked against Google's Antigravity docs on 2026-10-08.
### What this means for you
Run `./setup --host agy` (or `--host antigravity`). Every skill installs into `~/.gemini/antigravity-cli/skills/`, and you invoke each one as `/gstack-<skill>`. `--host auto` picks Antigravity up when `agy` is on your PATH or `~/.gemini/antigravity-cli` exists.
Skills are rendered with Antigravity's own tool names (`run_command`, `view_file`, `ask_question`, `invoke_subagent` and the rest) and the Gemini model profile. The host is experimental until a real Antigravity session certifies it.
### Behavior changes you may notice
- **Gemini CLI's files are left alone.** Gemini CLI shares `~/.gemini`, but gstack never touches `~/.gemini/skills` or Gemini CLI's settings. A machine with only Gemini CLI is never detected as Antigravity.
- **Not covered yet:** the Antigravity 2.0 app and the IDE, which read `~/.gemini/config/skills`.
### Itemized changes
#### Added
- The `agy` host (alias `antigravity`):
- Frontmatter is limited to `name` and `description`, and the build fails if a description exceeds 1,024 characters.
- A tool glossary, the Gemini overlay, and an `AGENTS.md` rewrite.
- Advisory-only safety skills, with Review Army suppressed, as on Copilot.
- Setup, `--status`, uninstall and team-init cover the host. Its render is kept under "render only installed hosts", and setup fails if no skills were linked.
#### For contributors
- docs/ADDING_A_HOST.md records how each of the nine Antigravity PRs was used, or why it wasn't.
- Built on work by @0xshae (#2134), @ManchalaSashank (#2893), @voipexpert (#2726) and @KiDDarn (#2244).
- Still to verify on a real install: symlinked skill directories, the `/gstack-*` commands appearing, `ask_question` option handling, partial `view_file` reads on long skills, and upgrading an existing install.
## [1.91.66.0] - 2026-10-08 ## [1.91.66.0] - 2026-10-08
**/autoplan no longer asks permission to read its own files after a background reviewer finishes.** **/autoplan no longer asks permission to read its own files after a background reviewer finishes.**
+2 -2
View File
@@ -126,8 +126,8 @@ The same `gen:skill-docs` run writes two more generated files from `lib/`:
and commit both; never edit the generated file and commit both; never edit the generated file
(`test/design-checklist-sync.test.ts` fails on drift). (`test/design-checklist-sync.test.ts` fails on drift).
Generation uses each host's `defaultModel` (`claude` for existing hosts, `gpt` Generation uses each host's `defaultModel` (`claude` for most hosts, `gpt`
for Codex) unless `--model` is explicit. Codex installs additionally read the for Codex, `gemini` for Antigravity's `agy`) unless `--model` is explicit. Codex installs additionally read the
top-level model from `${CODEX_HOME:-~/.codex}/config.toml`; rerun top-level model from `${CODEX_HOME:-~/.codex}/config.toml`; rerun
`./setup --host codex` after changing that model. Note: `bun run build` and a `./setup --host codex` after changing that model. Note: `bun run build` and a
bare `gen:skill-docs --host codex` render the host default (gpt) — if your bare `gen:skill-docs --host codex` render the host default (gpt) — if your
+9
View File
@@ -143,6 +143,7 @@ installs and passes the conformance tests but has no certification run yet;
| Factory Droid | experimental | `--host factory` → `~/.factory/skills/gstack-*/` | ask for `gstack-office-hours` | advisory, not blocked | Codex CLI, signed in | `./setup --host factory` | | Factory Droid | experimental | `--host factory` → `~/.factory/skills/gstack-*/` | ask for `gstack-office-hours` | advisory, not blocked | Codex CLI, signed in | `./setup --host factory` |
| Kiro | experimental | `--host kiro` → `~/.kiro/skills/gstack-*/` | ask for `gstack-office-hours` | advisory, not blocked | Codex CLI, signed in | `./setup --host kiro` | | Kiro | experimental | `--host kiro` → `~/.kiro/skills/gstack-*/` | ask for `gstack-office-hours` | advisory, not blocked | Codex CLI, signed in | `./setup --host kiro` |
| GitHub Copilot CLI | experimental | `--host copilot` → `~/.copilot/skills/gstack-*/` | `/gstack-office-hours` | advisory, not blocked | Codex CLI, signed in | `./setup --host copilot` | | GitHub Copilot CLI | experimental | `--host copilot` → `~/.copilot/skills/gstack-*/` | `/gstack-office-hours` | advisory, not blocked | Codex CLI, signed in | `./setup --host copilot` |
| Antigravity CLI | experimental | `--host agy` (or `--host antigravity`) → `~/.gemini/antigravity-cli/skills/gstack-*/` | `/gstack-office-hours` | advisory, not blocked | Codex CLI, signed in | `./setup --host agy` |
| Slate | instruction-only | `--host slate` (points at the Claude install; Slate reads `.claude/skills`) | `/office-hours` via the Claude install | advisory, not blocked | — | `./setup --host claude` | | Slate | instruction-only | `--host slate` (points at the Claude install; Slate reads `.claude/skills`) | `/office-hours` via the Claude install | advisory, not blocked | — | `./setup --host claude` |
| OpenClaw | instruction-only | `--host openclaw` (prints the digest path; ACP spawns Claude Code — [docs/OPENCLAW.md](docs/OPENCLAW.md)) | "Load gstack. Run /review" | advisory, not blocked | — | re-copy the digest after upgrades | | OpenClaw | instruction-only | `--host openclaw` (prints the digest path; ACP spawns Claude Code — [docs/OPENCLAW.md](docs/OPENCLAW.md)) | "Load gstack. Run /review" | advisory, not blocked | — | re-copy the digest after upgrades |
| Hermes | instruction-only | `--host hermes` (prints the digest path and `gen:skill-docs --host hermes`) | copy the digest, or render skills yourself | advisory, not blocked | — | re-copy the digest after upgrades | | Hermes | instruction-only | `--host hermes` (prints the digest path and `gen:skill-docs --host hermes`) | copy the digest, or render skills yourself | advisory, not blocked | — | re-copy the digest after upgrades |
@@ -153,6 +154,13 @@ Copilot invokes gstack skills by their prefixed names (`/gstack-review`) because
`/freeze` only advise there, and `COPILOT_HOME` other than `~/.copilot` is not `/freeze` only advise there, and `COPILOT_HOME` other than `~/.copilot` is not
supported yet (setup refuses and changes nothing). supported yet (setup refuses and changes nothing).
Antigravity CLI also invokes skills by prefixed name (`/gstack-review`). Setup
writes only `~/.gemini/antigravity-cli/skills/gstack*`: Gemini CLI's
`~/.gemini/skills` and settings are never touched, and `--host auto` selects
Antigravity only when `agy` is on `PATH` or `~/.gemini/antigravity-cli` exists,
never for a Gemini-CLI-only `~/.gemini`. The Antigravity 2.0 app and IDE read
`~/.gemini/config/skills`, which this host does not install.
Outside reviews require the selected CLI to be installed and authenticated: Claude Code when using gstack in Codex, or Codex on other harnesses. External harnesses discover these commands as `/gstack-claude-code` and `/gstack-codex`; each harness omits its own wrapper. Explicit provider requests keep that provider. The existing `codex_reviews` setting controls automatic outside reviews where supported, regardless of the provider selected. Outside reviews require the selected CLI to be installed and authenticated: Claude Code when using gstack in Codex, or Codex on other harnesses. External harnesses discover these commands as `/gstack-claude-code` and `/gstack-codex`; each harness omits its own wrapper. Explicit provider requests keep that provider. The existing `codex_reviews` setting controls automatic outside reviews where supported, regardless of the provider selected.
`/claude` has been renamed to `/claude-code`. Re-run `./setup --host <name>` to migrate managed installations, including other harnesses sharing the checkout. Setup preserves the previous installation if replacement generation or installation fails and prints repair instructions. `/claude` has been renamed to `/claude-code`. Re-run `./setup --host <name>` to migrate managed installations, including other harnesses sharing the checkout. Setup preserves the previous installation if replacement generation or installation fails and prints repair instructions.
@@ -562,6 +570,7 @@ rm -rf ~/.kiro/skills/gstack* 2>/dev/null
rm -rf ~/.openclaw/skills/gstack* 2>/dev/null rm -rf ~/.openclaw/skills/gstack* 2>/dev/null
rm -rf ~/.cursor/skills/gstack* 2>/dev/null rm -rf ~/.cursor/skills/gstack* 2>/dev/null
rm -rf ~/.config/opencode/skills/gstack* 2>/dev/null rm -rf ~/.config/opencode/skills/gstack* 2>/dev/null
rm -rf ~/.gemini/antigravity-cli/skills/gstack* 2>/dev/null
# 6. Remove temp files # 6. Remove temp files
rm -f /tmp/gstack-* 2>/dev/null rm -f /tmp/gstack-* 2>/dev/null
+1 -1
View File
@@ -1 +1 @@
1.91.66.0 1.91.67.0
+1 -1
View File
@@ -1,4 +1,4 @@
# gstack digest v1.91.66.0 — regenerate/re-copy after upgrading gstack # gstack digest v1.91.67.0 — regenerate/re-copy after upgrading gstack
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
for agent hosts without a full skill install. The full skills add workflows, for agent hosts without a full skill install. The full skills add workflows,
+1 -1
View File
@@ -24,7 +24,7 @@
# host:render-dir for every host whose render lives in its own checkout dir. # host:render-dir for every host whose render lives in its own checkout dir.
# Claude renders into the tracked tree. test/host-renders.test.ts keeps this # Claude renders into the tracked tree. test/host-renders.test.ts keeps this
# equal to hostSubdir in hosts/*.ts. # equal to hostSubdir in hosts/*.ts.
GSTACK_HOST_RENDER_DIRS="codex:.agents kiro:.kiro factory:.factory opencode:.opencode cursor:.cursor copilot:.copilot slate:.slate openclaw:.openclaw hermes:.hermes gbrain:.gbrain" GSTACK_HOST_RENDER_DIRS="codex:.agents kiro:.kiro factory:.factory opencode:.opencode cursor:.cursor copilot:.copilot agy:.agy slate:.slate openclaw:.openclaw hermes:.hermes gbrain:.gbrain"
gstack_render_hosts_file() { printf '%s/.gstack-installed-hosts\n' "$1"; } gstack_render_hosts_file() { printf '%s/.gstack-installed-hosts\n' "$1"; }
+2 -1
View File
@@ -160,7 +160,7 @@ gstack_install_render_for() {
gstack_host_tier() { gstack_host_tier() {
case "$1" in case "$1" in
claude) echo full ;; claude) echo full ;;
codex|kiro|factory|opencode|cursor|copilot) echo experimental ;; codex|kiro|factory|opencode|cursor|copilot|agy) echo experimental ;;
slate|openclaw|hermes|gbrain) echo instruction-only ;; slate|openclaw|hermes|gbrain) echo instruction-only ;;
*) echo unknown ;; *) echo unknown ;;
esac esac
@@ -231,6 +231,7 @@ gstack_install_known_roots() {
printf 'opencode\tglobal\t%s\t%s\n' "$HOME/.config/opencode/skills" "$HOME/.config/opencode/skills/gstack" printf 'opencode\tglobal\t%s\t%s\n' "$HOME/.config/opencode/skills" "$HOME/.config/opencode/skills/gstack"
printf 'cursor\tglobal\t%s\t%s\n' "$HOME/.cursor/skills" "$HOME/.cursor/skills/gstack" printf 'cursor\tglobal\t%s\t%s\n' "$HOME/.cursor/skills" "$HOME/.cursor/skills/gstack"
printf 'copilot\tglobal\t%s\t%s\n' "$HOME/.copilot/skills" "$HOME/.copilot/skills/gstack" printf 'copilot\tglobal\t%s\t%s\n' "$HOME/.copilot/skills" "$HOME/.copilot/skills/gstack"
printf 'agy\tglobal\t%s\t%s\n' "$HOME/.gemini/antigravity-cli/skills" "$HOME/.gemini/antigravity-cli/skills/gstack"
proj="$(git rev-parse --show-toplevel 2>/dev/null || true)" proj="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$proj" ] && [ "$proj" != "$HOME" ]; then if [ -n "$proj" ] && [ "$proj" != "$HOME" ]; then
printf 'claude\tproject\t%s\t%s\n' "$proj/.claude/skills" "$proj/.claude/skills/gstack" printf 'claude\tproject\t%s\t%s\n' "$proj/.claude/skills" "$proj/.claude/skills/gstack"
+2 -2
View File
@@ -81,7 +81,7 @@ else
```bash ```bash
_GS="" _GS=""
for _D in "${GSTACK_ROOT:-}" "$HOME/.claude/skills/gstack" "$HOME/.codex/skills/gstack" "$HOME/.factory/skills/gstack" "$HOME/.kiro/skills/gstack" "$HOME/.config/opencode/skills/gstack" "$HOME/.slate/skills/gstack" "$HOME/.cursor/skills/gstack" "$HOME/.openclaw/skills/gstack" "$HOME/.hermes/skills/gstack" "$HOME/.gbrain/skills/gstack" "$HOME/.copilot/skills/gstack" "$HOME/.gstack/repos/gstack"; do for _D in "${GSTACK_ROOT:-}" "$HOME/.claude/skills/gstack" "$HOME/.codex/skills/gstack" "$HOME/.factory/skills/gstack" "$HOME/.kiro/skills/gstack" "$HOME/.config/opencode/skills/gstack" "$HOME/.slate/skills/gstack" "$HOME/.cursor/skills/gstack" "$HOME/.openclaw/skills/gstack" "$HOME/.hermes/skills/gstack" "$HOME/.gbrain/skills/gstack" "$HOME/.copilot/skills/gstack" "$HOME/.gemini/antigravity-cli/skills/gstack" "$HOME/.gstack/repos/gstack"; do
[ -z "$_GS" ] && [ -n "$_D" ] && [ -d "$_D/bin" ] && _GS="$_D" [ -z "$_GS" ] && [ -n "$_D" ] && [ -d "$_D/bin" ] && _GS="$_D"
done done
[ -n "$_GS" ] && echo "GSTACK_OK: $_GS" || echo "GSTACK_MISSING" [ -n "$_GS" ] && echo "GSTACK_OK: $_GS" || echo "GSTACK_MISSING"
@@ -146,7 +146,7 @@ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
const roots = [process.env.GSTACK_ROOT || ''].concat([ const roots = [process.env.GSTACK_ROOT || ''].concat([
'.claude/skills/gstack', '.codex/skills/gstack', '.factory/skills/gstack', '.kiro/skills/gstack', '.claude/skills/gstack', '.codex/skills/gstack', '.factory/skills/gstack', '.kiro/skills/gstack',
'.config/opencode/skills/gstack', '.slate/skills/gstack', '.cursor/skills/gstack', '.openclaw/skills/gstack', '.config/opencode/skills/gstack', '.slate/skills/gstack', '.cursor/skills/gstack', '.openclaw/skills/gstack',
'.hermes/skills/gstack', '.gbrain/skills/gstack', '.copilot/skills/gstack', '.gstack/repos/gstack', '.hermes/skills/gstack', '.gbrain/skills/gstack', '.copilot/skills/gstack', '.gemini/antigravity-cli/skills/gstack', '.gstack/repos/gstack',
].map((rel) => path.join(home, rel))); ].map((rel) => path.join(home, rel)));
const gstackRoot = roots.find((root) => { const gstackRoot = roots.find((root) => {
try { return root !== '' && fs.statSync(path.join(root, 'bin')).isDirectory(); } catch { return false; } try { return root !== '' && fs.statSync(path.join(root, 'bin')).isDirectory(); } catch { return false; }
+18
View File
@@ -23,6 +23,7 @@
# ~/.cursor/skills/gstack* — Cursor skill install + per-skill symlinks # ~/.cursor/skills/gstack* — Cursor skill install + per-skill symlinks
# ~/.config/opencode/skills/gstack* — OpenCode skill install + per-skill symlinks # ~/.config/opencode/skills/gstack* — OpenCode skill install + per-skill symlinks
# ~/.copilot/skills/gstack* — GitHub Copilot CLI skill install + per-skill symlinks # ~/.copilot/skills/gstack* — GitHub Copilot CLI skill install + per-skill symlinks
# ~/.gemini/antigravity-cli/skills/gstack* — Antigravity CLI skill install + per-skill symlinks
# installs.tsv rows — install registry rows for removed installs # installs.tsv rows — install registry rows for removed installs
# ~/.gstack/ — global state (config, analytics, sessions, projects, # ~/.gstack/ — global state (config, analytics, sessions, projects,
# repos, installation-id, browse error logs) # repos, installation-id, browse error logs)
@@ -135,6 +136,7 @@ if [ "$FORCE" -eq 0 ]; then
[ -d "$HOME/.cursor/skills" ] && echo " ~/.cursor/skills/gstack*" [ -d "$HOME/.cursor/skills" ] && echo " ~/.cursor/skills/gstack*"
[ -d "$HOME/.config/opencode/skills" ] && echo " ~/.config/opencode/skills/gstack*" [ -d "$HOME/.config/opencode/skills" ] && echo " ~/.config/opencode/skills/gstack*"
[ -d "$HOME/.copilot/skills" ] && echo " ~/.copilot/skills/gstack*" [ -d "$HOME/.copilot/skills" ] && echo " ~/.copilot/skills/gstack*"
[ -d "$HOME/.gemini/antigravity-cli/skills" ] && echo " ~/.gemini/antigravity-cli/skills/gstack*"
[ "$KEEP_STATE" -eq 0 ] && [ -d "$STATE_DIR" ] && echo " $STATE_DIR" [ "$KEEP_STATE" -eq 0 ] && [ -d "$STATE_DIR" ] && echo " $STATE_DIR"
[ -n "$_LEAVE_ROOT" ] && echo " (kept) $_LEAVE_ROOT: state root selected by $RESOLVED_VAR; left in place" [ -n "$_LEAVE_ROOT" ] && echo " (kept) $_LEAVE_ROOT: state root selected by $RESOLVED_VAR; left in place"
@@ -512,6 +514,22 @@ if [ -d "$COPILOT_SKILLS" ]; then
done done
fi fi
# ─── Remove Antigravity CLI skills ──────────────────────────
# Same provenance gate. Only gstack* entries under antigravity-cli/skills go:
# the rest of ~/.gemini belongs to Antigravity and Gemini CLI.
ANTIGRAVITY_SKILLS="$HOME/.gemini/antigravity-cli/skills"
if [ -d "$ANTIGRAVITY_SKILLS" ]; then
for _ITEM in "$ANTIGRAVITY_SKILLS"/gstack*; do
[ -e "$_ITEM" ] || [ -L "$_ITEM" ] || continue
if _cursor_item_is_gstack_managed "$_ITEM"; then
rm -rf "$_ITEM"
REMOVED+=("agy/$(basename "$_ITEM")")
else
_SKIPPED_DIRS+=("$_ITEM")
fi
done
fi
# ─── Reconcile the install registry ───────────────────────── # ─── Reconcile the install registry ─────────────────────────
# A registry that survives (--keep-state, or a state root selected by an env # A registry that survives (--keep-state, or a state root selected by an env
# var and left in place) must forget the installs removed above, or the next # var and left in place) must forget the installs removed above, or the next
+69 -3
View File
@@ -2,7 +2,7 @@
gstack uses a declarative host config system. Each supported AI coding agent gstack uses a declarative host config system. Each supported AI coding agent
(Claude, Codex, Factory, Kiro, OpenCode, Slate, Cursor, OpenClaw, Hermes, (Claude, Codex, Factory, Kiro, OpenCode, Slate, Cursor, OpenClaw, Hermes,
GBrain) is defined as a typed TypeScript config object built by the GBrain, Copilot, Antigravity) is defined as a typed TypeScript config object built by the
`defineHost()` factory. Rendering a new host means creating one file and `defineHost()` factory. Rendering a new host means creating one file and
re-exporting it: the generator, tests, and validation pick it up. Installing re-exporting it: the generator, tests, and validation pick it up. Installing
it is not free: setup still has one install arm per installable host (see it is not free: setup still has one install arm per installable host (see
@@ -76,13 +76,13 @@ Inventory of every setup side effect, by phase (line numbers are approximate):
|---|---|---| |---|---|---|
| Build | `browse/dist`, `design/dist`, `make-pdf/dist`, `bin/gstack-cso-*` in the source checkout | source | | Build | `browse/dist`, `design/dist`, `make-pdf/dist`, `bin/gstack-cso-*` in the source checkout | source |
| Hosts record | `.gstack-installed-hosts` in the source checkout: every host setup installed from it, additive (#1694) | source | | Hosts record | `.gstack-installed-hosts` in the source checkout: every host setup installed from it, additive (#1694) | source |
| Generation | renders for the recorded hosts only (`.agents/`, `.kiro/`, `.factory/`, `.opencode/`, `.cursor/`, `.copilot/`) in the source checkout; `gstack-patch-names` rewrites Claude `name:` fields in place | source | | Generation | renders for the recorded hosts only (`.agents/`, `.kiro/`, `.factory/`, `.opencode/`, `.cursor/`, `.copilot/`, `.agy/`) in the source checkout; `gstack-patch-names` rewrites Claude `name:` fields in place | source |
| Render prune | renders of hosts the record does not name: links into the checkout removed, generated files moved to `$GSTACK_STATE_ROOT/backups/host-renders/<ts>-<id>/`, other files kept | source / state root | | Render prune | renders of hosts the record does not name: links into the checkout removed, generated files moved to `$GSTACK_STATE_ROOT/backups/host-renders/<ts>-<id>/`, other files kept | source / state root |
| Chromium | Playwright cache (`~/.cache/ms-playwright`), lock under the state root | machine | | Chromium | Playwright cache (`~/.cache/ms-playwright`), lock under the state root | machine |
| Config | `skill_prefix`, `timeline_stop_hook`, team-mode keys in `$GSTACK_STATE_ROOT/config.yaml` | state root | | Config | `skill_prefix`, `timeline_stop_hook`, team-mode keys in `$GSTACK_STATE_ROOT/config.yaml` | state root |
| Claude arm | `~/.claude/skills/gstack` link, one dir per skill with a `SKILL.md` link, alias copies | selected host | | Claude arm | `~/.claude/skills/gstack` link, one dir per skill with a `SKILL.md` link, alias copies | selected host |
| Codex arm | `${CODEX_HOME:-~/.codex}/skills/gstack` runtime root, `gstack-*` links into `.agents/skills`, `.agents/skills/gstack` sidecar | selected host | | Codex arm | `${CODEX_HOME:-~/.codex}/skills/gstack` runtime root, `gstack-*` links into `.agents/skills`, `.agents/skills/gstack` sidecar | selected host |
| Kiro, Factory, OpenCode, Cursor, Copilot arms | the host's `skills/gstack` runtime root (built beside the live one and swapped in) and `gstack-*` links | selected host | | Kiro, Factory, OpenCode, Cursor, Copilot, Antigravity arms | the host's `skills/gstack` runtime root (built beside the live one and swapped in) and `gstack-*` links | selected host |
| OpenCode commands | `~/.config/opencode/commands/gstack-*.md` with a managed marker (#2629); user command files are never touched | selected host | | OpenCode commands | `~/.config/opencode/commands/gstack-*.md` with a managed marker (#2629); user command files are never touched | selected host |
| Registry | one row per activated install in `installs.tsv` | state root | | Registry | one row per activated install in `installs.tsv` | state root |
| Migrations | whatever each `v*.sh` repairs; marker in the state root | state root | | Migrations | whatever each `v*.sh` repairs; marker in the state root | state root |
@@ -211,6 +211,70 @@ answer `ask_user` itself, plan mode blocks mutating shell commands, symlinked
skill dirs need Copilot CLI 1.0.62 or later, and `COPILOT_HOME` other than skill dirs need Copilot CLI 1.0.62 or later, and `COPILOT_HOME` other than
`~/.copilot` is refused for now. `~/.copilot` is refused for now.
## Antigravity CLI
Shipped as `experimental` (`hosts/agy.ts`, `./setup --host agy`, alias
`--host antigravity`) for Google's Antigravity CLI. The host is named after the
`agy` binary: with the longer name, the per-fence runtime prelude was 408 bytes
against its 400-byte budget (the global root is long); `agy` renders at
exactly 400, so the next byte added to that prelude must come with a budget
change. Verified against Google's docs on 2026-10-08:
| Fact | Value | Source |
|---|---|---|
| Global skills | `~/.gemini/antigravity-cli/skills/<skill>/SKILL.md` | [Agent skills](https://antigravity.google/docs/skills/) (CLI section), [Plugins & skills](https://antigravity.google/docs/cli/plugins/), [Migrating from Gemini CLI](https://antigravity.google/docs/cli/gcli-migration/) |
| Workspace skills | `.agents/skills/<skill>/SKILL.md` | same pages |
| Frontmatter | `name` (optional, defaults to the directory), `description` (required) | [Agent skills](https://antigravity.google/docs/skills/) |
| Description limit | 1024 characters (Agent Skills format) | [What are Agent Skills?](https://cloud.google.com/discover/ai-agent-skills) |
| Tool names | `run_command`, `view_file`, `write_to_file`, `replace_file_content`, `multi_replace_file_content`, `grep_search`, `find_by_name`, `invoke_subagent`, `ask_question` | [Hooks: supported tools](https://antigravity.google/docs/hooks/) |
| Plan mode | `agy --mode=plan` / `/plan`, no exit-plan tool | [Execution modes](https://antigravity.google/docs/cli/modes/) |
| Binary | `agy`, installed to `~/.local/bin/agy`; config in `~/.gemini/antigravity-cli/` | [Installation](https://antigravity.google/docs/cli/install/), [Settings](https://antigravity.google/docs/cli/settings/) |
What the host does: skills in `~/.gemini/antigravity-cli/skills/gstack-*`,
runtime root `~/.gemini/antigravity-cli/skills/gstack` with `.source-path` for
`/gstack-upgrade`, `name:` equal to the `gstack-<skill>` directory (the CLI
turns each name into a slash command, and has `/plan`, `/diff`, `/skills` and
other built-ins), frontmatter limited to `name` and `description`, the Gemini
model overlay, and a one-paragraph tool glossary ahead of the preamble's STATUS
rules. The glossary also tells the agent to keep reading when `view_file`
returns only part of a long SKILL.md: users report a first read that stops at
800 lines ([forum](https://discuss.ai.google.dev/t/make-the-800-line-forced-first-read-in-view-file-configurable-or-opt-out/172257)),
which Google's docs do not state, so the rendered text names no number.
**Shared `~/.gemini`.** Antigravity CLI and Gemini CLI both live under
`~/.gemini`. Gemini CLI reads `~/.gemini/skills` (and `~/.agents/skills`);
Antigravity CLI reads `~/.gemini/antigravity-cli/skills`. Setup writes only
gstack entries in the latter, uninstall removes only gstack-managed entries
there, and `--host auto` selects Antigravity only for `agy` on `PATH` or an
existing `~/.gemini/antigravity-cli`, never for a bare `~/.gemini`.
`test/setup-install-registry.test.ts` holds both directions. Gemini CLI as a
host is not supported. Two overlaps remain: workspace `.agents/skills` is also
where a project-local Codex install puts its Codex-flavored `gstack-*`
skills, which Antigravity will list in that repo, and the Antigravity 2.0 app
and IDE read `~/.gemini/config/skills`, which this host does not install
(Google's codelab says that directory also reaches the CLI; the CLI docs do
not, so setup follows the CLI docs).
Disposition of the community attempts:
| PR | Decision | Why |
|---|---|---|
| #2134 (@0xshae) | Selectively reused, with credit | Kept: `agy` as the host name and binary, `.agents/skills` as the workspace root, and a `.agy` render directory separate from Codex's `.agents`. Not kept: the hand-written config (predates `defineHost()`), `~/.gemini/config/skills` as the CLI root, and the setup arm without the ownership gates. |
| #2893 (@ManchalaSashank) | Selectively reused, with credit | Kept: `agy`/`antigravity` as host name and alias in setup and generation, the Gemini overlay, uninstall coverage. Not kept: `~/.gemini/config/skills`, auto-detect on a bare `~/.gemini` (Gemini CLI's directory), `.gemini` as the render directory, and the repo-local `.gemini/skills` sidecar. |
| #2726 (@voipexpert) | Selectively reused, with credit | Kept: Antigravity's tool names from its own docs, and that `manage_task` (background processes) is not a to-do tool. Not kept: blanket tool-name rewrites (a preamble glossary replaces them) and suppressing outside reviews, which run through the shell. |
| #2244 (@KiDDarn) | Selectively reused, with credit | Kept: fail the install when no skill was linked. Not kept: the `~/.gemini/config/plugins` plugin layout (CLI plugins live under `~/.gemini/antigravity-cli/plugins` and are installed by `agy plugin install`), the benchmark provider work, and extra frontmatter fields. |
| #413 (@jnMetaCode) | Rejected | Installs to `~/.gemini/skills` and detects `gemini`: that is Gemini CLI. A comment there (@doogiehowzer) reported the partial `view_file` read the glossary now handles. |
| #2079 (@FACUTUCCI10) | Rejected | Gemini CLI paths (`~/.gemini/skills`). |
| #2187 (@NotArsal) | Rejected | Duplicate of #2134 without setup. |
| #1490 (@7H0M45-4N70NY) | Rejected | `~/.antigravity/skills`, which no Antigravity surface reads. |
| #565 (@ChetanTekur) | Rejected | Gemini CLI `GEMINI.md` support; Gemini CLI is out of scope. |
Unverified without a real Antigravity CLI (why it is not `full`): that the
CLI follows symlinked skill directories, that it does not import the runtime
root's extra markdown files (`ETHOS.md`, `review/*.md`) as skills, the
partial-read behavior of `view_file` on long skills, `ask_question` option
handling, and an upgrade from an existing install.
## Instruction-only hosts ## Instruction-only hosts
OpenClaw, Hermes, Slate and GBrain have no install arm. Hermes stays OpenClaw, Hermes, Slate and GBrain have no install arm. Hermes stays
@@ -234,6 +298,8 @@ hosts/
├── openclaw.ts # OpenClaw ├── openclaw.ts # OpenClaw
├── hermes.ts # Hermes (Nous Research) ├── hermes.ts # Hermes (Nous Research)
├── gbrain.ts # GBrain ├── gbrain.ts # GBrain
├── copilot.ts # GitHub Copilot CLI
├── agy.ts # Google Antigravity CLI
└── index.ts # Registry: imports all, derives Host type └── index.ts # Registry: imports all, derives Host type
``` ```
+63
View File
@@ -0,0 +1,63 @@
import { defineHost, GBRAIN_RESOLVERS, preambleToolGlossary } from './define-host';
// Google Antigravity CLI, named after its `agy` binary: the longer name pushed
// the runtime prelude past PRELUDE_BYTE_BUDGET. Rewritten on defineHost() from
// the community attempts; disposition and doc sources in docs/ADDING_A_HOST.md
// "Antigravity CLI".
// Paths: the CLI reads global skills from ~/.gemini/antigravity-cli/skills and
// workspace skills from .agents/skills (antigravity.google/docs/skills, CLI
// section). Tool names: antigravity.google/docs/hooks "Supported tools".
const ANTIGRAVITY_TOOL_GLOSSARY = '**Antigravity tool names:** `AskUserQuestion` means your `ask_question` tool (options as choices); there is no `mcp__*__AskUserQuestion` variant. The Bash tool means `run_command`, the Read tool `view_file`, the Write tool `write_to_file`, the Edit tool `replace_file_content` (`multi_replace_file_content` for several edits in one file), Grep `grep_search`, Glob `find_by_name`, and the Agent tool `invoke_subagent`. There is no Skill tool: load a skill by reading its SKILL.md with `view_file`. If a step says to call `ExitPlanMode`, tell the user the plan is ready and wait. `view_file` can return only part of a long file: when a SKILL.md or a file it tells you to read ends before its last line, read the remaining line ranges before acting on it.';
const agy = defineHost({
name: 'agy',
displayName: 'Antigravity CLI',
cliAliases: ['antigravity'],
defaultModel: 'gemini',
tier: 'experimental',
capabilities: { toolExecution: true, questions: 'native', planMode: false, delegation: true, browser: true, safetyHooks: 'advisory' },
globalRoot: '.gemini/antigravity-cli/skills/gstack',
localSkillRoot: '.agents/skills/gstack',
// Render output only. .agents is Codex's render tree, and .gemini is Gemini
// CLI's workspace directory.
hostSubdir: '.agy',
// Antigravity reads name and description only; name defaults to the
// directory, and the CLI turns each name into a slash command, so names
// match their gstack-<skill> directories instead of shadowing /plan-style
// built-ins. 1024 is the Agent Skills description limit.
frontmatter: {
mode: 'allowlist',
keepFields: ['name', 'description'],
descriptionLimit: 1024,
descriptionLimitBehavior: 'error',
nameMatchesDirectory: true,
},
// Literal ~/.gemini/antigravity-cli paths (the Copilot model) so skills
// without the preamble still resolve; .source-path (written by setup) lets
// /gstack-upgrade find the checkout behind the runtime root.
pathRewrites: [
{ from: 'if [ -d "$HOME/.claude/skills/gstack/.git" ]', to: 'if [ -d "$(cat "$HOME/.gemini/antigravity-cli/skills/gstack/.source-path" 2>/dev/null)/.git" ]' },
{ from: 'INSTALL_DIR="$HOME/.claude/skills/gstack"', to: 'INSTALL_DIR="$(cat "$HOME/.gemini/antigravity-cli/skills/gstack/.source-path")"' },
{ from: '$HOME/.claude/skills/gstack', to: '$HOME/.gemini/antigravity-cli/skills/gstack' },
{ from: '~/.claude/skills/gstack', to: '~/.gemini/antigravity-cli/skills/gstack' },
{ from: '.claude/skills/gstack', to: '.agents/skills/gstack' },
{ from: '.claude/skills/review', to: '.agents/skills/gstack/review' },
{ from: '.claude/skills', to: '.agents/skills' },
{ from: 'CLAUDE.md', to: 'AGENTS.md' },
],
toolRewrites: preambleToolGlossary(ANTIGRAVITY_TOOL_GLOSSARY),
suppressedResolvers: ['REVIEW_ARMY', ...GBRAIN_RESOLVERS],
runtimeRoot: {
globalSymlinks: ['bin', 'lib', 'browse/dist', 'browse/bin', 'design/dist', 'make-pdf/dist', 'freeze/bin', 'careful/bin', 'gstack-upgrade', 'ETHOS.md'],
globalFiles: {
'review': ['checklist.md', 'design-checklist.md', 'greptile-triage.md', 'TODOS-format.md'],
},
},
});
export default agy;
+4 -3
View File
@@ -17,9 +17,10 @@ import openclaw from './openclaw';
import hermes from './hermes'; import hermes from './hermes';
import gbrain from './gbrain'; import gbrain from './gbrain';
import copilot from './copilot'; import copilot from './copilot';
import agy from './agy';
/** All registered host configs. Add new hosts here. */ /** All registered host configs. Add new hosts here. */
export const ALL_HOST_CONFIGS: HostConfig[] = [claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain, copilot]; export const ALL_HOST_CONFIGS: HostConfig[] = [claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain, copilot, agy];
/** Map from host name to config. */ /** Map from host name to config. */
export const HOST_CONFIG_MAP: Record<string, HostConfig> = Object.fromEntries( export const HOST_CONFIG_MAP: Record<string, HostConfig> = Object.fromEntries(
@@ -43,7 +44,7 @@ export function getHostConfig(name: string): HostConfig {
/** /**
* Resolve a host name from a CLI argument, handling aliases. * Resolve a host name from a CLI argument, handling aliases.
* e.g., 'agents' → 'codex', 'droid' → 'factory' * e.g., 'agents' → 'codex', 'droid' → 'factory', 'antigravity' → 'agy'
*/ */
export function resolveHostArg(arg: string): string { export function resolveHostArg(arg: string): string {
// Direct name match // Direct name match
@@ -66,4 +67,4 @@ export function getExternalHosts(): HostConfig[] {
} }
// Re-export individual configs for direct import // Re-export individual configs for direct import
export { claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain, copilot }; export { claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain, copilot, agy };
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "gstack", "name": "gstack",
"version": "1.91.66", "version": "1.91.67",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
+125 -7
View File
@@ -22,7 +22,7 @@ Usage: ./setup [options]
Options: Options:
--host <name> Install for one host. Default: claude. Hosts by tier: --host <name> Install for one host. Default: claude. Hosts by tier:
full: claude full: claude
experimental: codex, kiro, factory, opencode, cursor, copilot experimental: codex, kiro, factory, opencode, cursor, copilot, agy
instruction-only: slate, openclaw, hermes, gbrain instruction-only: slate, openclaw, hermes, gbrain
auto: every installed agent that setup detects auto: every installed agent that setup detects
An explicit --host never changes another host's install. An explicit --host never changes another host's install.
@@ -193,7 +193,7 @@ _setup_unknown_option() {
} }
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
--host) [ -z "$2" ] && echo "Missing value for --host (expected claude, codex, kiro, factory, opencode, cursor, copilot, slate, openclaw, hermes, gbrain, or auto)" >&2 && exit 1; HOST="$2"; HOST_FLAG_SET=1; shift 2 ;; --host) [ -z "$2" ] && echo "Missing value for --host (expected claude, codex, kiro, factory, opencode, cursor, copilot, agy, slate, openclaw, hermes, gbrain, or auto)" >&2 && exit 1; HOST="$2"; HOST_FLAG_SET=1; shift 2 ;;
--host=*) HOST="${1#--host=}"; HOST_FLAG_SET=1; shift ;; --host=*) HOST="${1#--host=}"; HOST_FLAG_SET=1; shift ;;
--model) [ -z "$2" ] && echo "Missing value for --model" >&2 && exit 1; MODEL_OVERRIDE="$2"; MODEL_OVERRIDE_SET=1; shift 2 ;; --model) [ -z "$2" ] && echo "Missing value for --model" >&2 && exit 1; MODEL_OVERRIDE="$2"; MODEL_OVERRIDE_SET=1; shift 2 ;;
--model=*) MODEL_OVERRIDE="${1#--model=}"; MODEL_OVERRIDE_SET=1; shift ;; --model=*) MODEL_OVERRIDE="${1#--model=}"; MODEL_OVERRIDE_SET=1; shift ;;
@@ -234,8 +234,9 @@ print_instruction_tier() {
echo "" echo ""
} }
[ "$HOST" = "antigravity" ] && HOST="agy" # Antigravity CLI is named after its binary
case "$HOST" in case "$HOST" in
claude|codex|kiro|factory|opencode|cursor|copilot|auto) ;; claude|codex|kiro|factory|opencode|cursor|copilot|agy|auto) ;;
slate) slate)
echo "" echo ""
echo "Slate is not yet a first-class install target (docs/designs/SLATE_HOST.md —" echo "Slate is not yet a first-class install target (docs/designs/SLATE_HOST.md —"
@@ -287,7 +288,7 @@ case "$HOST" in
echo "" echo ""
exit 0 ;; exit 0 ;;
*) *)
echo "Unknown --host value: $HOST — nothing was installed or changed. Expected one of: claude (default), codex, kiro, factory, opencode, cursor, copilot, slate, openclaw, hermes, gbrain, or auto. Tiers: ./setup --help" >&2; exit 1 ;; echo "Unknown --host value: $HOST — nothing was installed or changed. Expected one of: claude (default), codex, kiro, factory, opencode, cursor, copilot, agy (alias antigravity), slate, openclaw, hermes, gbrain, or auto. Tiers: ./setup --help" >&2; exit 1 ;;
esac esac
# Bun versions come from one sourced file (floor and tested; E1). # Bun versions come from one sourced file (floor and tested; E1).
@@ -481,6 +482,8 @@ CURSOR_GSTACK="$CURSOR_SKILLS/gstack"
KIRO_SKILLS="$HOME/.kiro/skills" KIRO_SKILLS="$HOME/.kiro/skills"
COPILOT_SKILLS="$HOME/.copilot/skills" COPILOT_SKILLS="$HOME/.copilot/skills"
COPILOT_GSTACK="$COPILOT_SKILLS/gstack" COPILOT_GSTACK="$COPILOT_SKILLS/gstack"
AGY_SKILLS="$HOME/.gemini/antigravity-cli/skills"
AGY_GSTACK="$AGY_SKILLS/gstack"
SKILLS_BASENAME="$(basename "$INSTALL_SKILLS_DIR")" SKILLS_BASENAME="$(basename "$INSTALL_SKILLS_DIR")"
SKILLS_PARENT_BASENAME="$(basename "$(dirname "$INSTALL_SKILLS_DIR")")" SKILLS_PARENT_BASENAME="$(basename "$(dirname "$INSTALL_SKILLS_DIR")")"
@@ -1299,6 +1302,7 @@ INSTALL_FACTORY=0
INSTALL_OPENCODE=0 INSTALL_OPENCODE=0
INSTALL_CURSOR=0 INSTALL_CURSOR=0
INSTALL_COPILOT=0 INSTALL_COPILOT=0
INSTALL_AGY=0
if [ "$HOST" = "auto" ]; then if [ "$HOST" = "auto" ]; then
command -v claude >/dev/null 2>&1 && INSTALL_CLAUDE=1 command -v claude >/dev/null 2>&1 && INSTALL_CLAUDE=1
command -v codex >/dev/null 2>&1 && INSTALL_CODEX=1 command -v codex >/dev/null 2>&1 && INSTALL_CODEX=1
@@ -1310,8 +1314,13 @@ if [ "$HOST" = "auto" ]; then
command -v cursor >/dev/null 2>&1 && INSTALL_CURSOR=1 command -v cursor >/dev/null 2>&1 && INSTALL_CURSOR=1
[ -d "$HOME/.cursor" ] && INSTALL_CURSOR=1 [ -d "$HOME/.cursor" ] && INSTALL_CURSOR=1
command -v copilot >/dev/null 2>&1 && INSTALL_COPILOT=1 command -v copilot >/dev/null 2>&1 && INSTALL_COPILOT=1
# agy's installer puts the binary in ~/.local/bin, which may not be on PATH;
# ~/.gemini/antigravity-cli is agy's own config dir. A bare ~/.gemini is
# Gemini CLI's and never selects Antigravity.
command -v agy >/dev/null 2>&1 && INSTALL_AGY=1
[ -d "$HOME/.gemini/antigravity-cli" ] && INSTALL_AGY=1
# If none found, default to claude # If none found, default to claude
if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_KIRO" -eq 0 ] && [ "$INSTALL_FACTORY" -eq 0 ] && [ "$INSTALL_OPENCODE" -eq 0 ] && [ "$INSTALL_CURSOR" -eq 0 ] && [ "$INSTALL_COPILOT" -eq 0 ]; then if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_KIRO" -eq 0 ] && [ "$INSTALL_FACTORY" -eq 0 ] && [ "$INSTALL_OPENCODE" -eq 0 ] && [ "$INSTALL_CURSOR" -eq 0 ] && [ "$INSTALL_COPILOT" -eq 0 ] && [ "$INSTALL_AGY" -eq 0 ]; then
INSTALL_CLAUDE=1 INSTALL_CLAUDE=1
fi fi
elif [ "$HOST" = "claude" ]; then elif [ "$HOST" = "claude" ]; then
@@ -1328,13 +1337,15 @@ elif [ "$HOST" = "cursor" ]; then
INSTALL_CURSOR=1 INSTALL_CURSOR=1
elif [ "$HOST" = "copilot" ]; then elif [ "$HOST" = "copilot" ]; then
INSTALL_COPILOT=1 INSTALL_COPILOT=1
elif [ "$HOST" = "agy" ]; then
INSTALL_AGY=1
fi fi
# A host that passes --host validation but sets no INSTALL_* flag would # A host that passes --host validation but sets no INSTALL_* flag would
# silently configure nothing and exit 0 (the #2361 slate failure class). # silently configure nothing and exit 0 (the #2361 slate failure class).
# Fail loudly if a future host lands in the accept-list without a dispatch arm. # Fail loudly if a future host lands in the accept-list without a dispatch arm.
if [ "$HOST" != "auto" ] && [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_KIRO" -eq 0 ] && [ "$INSTALL_FACTORY" -eq 0 ] && [ "$INSTALL_OPENCODE" -eq 0 ] && [ "$INSTALL_CURSOR" -eq 0 ] && [ "$INSTALL_COPILOT" -eq 0 ]; then if [ "$HOST" != "auto" ] && [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_KIRO" -eq 0 ] && [ "$INSTALL_FACTORY" -eq 0 ] && [ "$INSTALL_OPENCODE" -eq 0 ] && [ "$INSTALL_CURSOR" -eq 0 ] && [ "$INSTALL_COPILOT" -eq 0 ] && [ "$INSTALL_AGY" -eq 0 ]; then
echo "Error: no install arm exists for host '$HOST' — it passed --host validation but sets no INSTALL_* flag, so setup would configure nothing and exit 0. This is a setup bug. Valid install targets: claude, codex, kiro, factory, opencode, cursor, copilot (informational: slate, openclaw, hermes, gbrain)." >&2 echo "Error: no install arm exists for host '$HOST' — it passed --host validation but sets no INSTALL_* flag, so setup would configure nothing and exit 0. This is a setup bug. Valid install targets: claude, codex, kiro, factory, opencode, cursor, copilot, agy (informational: slate, openclaw, hermes, gbrain)." >&2
exit 1 exit 1
fi fi
@@ -1729,6 +1740,7 @@ _RENDER_HOSTS_SELECTED=""
[ "$INSTALL_OPENCODE" -eq 1 ] && _RENDER_HOSTS_SELECTED="$_RENDER_HOSTS_SELECTED opencode" [ "$INSTALL_OPENCODE" -eq 1 ] && _RENDER_HOSTS_SELECTED="$_RENDER_HOSTS_SELECTED opencode"
[ "$INSTALL_CURSOR" -eq 1 ] && _RENDER_HOSTS_SELECTED="$_RENDER_HOSTS_SELECTED cursor" [ "$INSTALL_CURSOR" -eq 1 ] && _RENDER_HOSTS_SELECTED="$_RENDER_HOSTS_SELECTED cursor"
[ "$INSTALL_COPILOT" -eq 1 ] && _RENDER_HOSTS_SELECTED="$_RENDER_HOSTS_SELECTED copilot" [ "$INSTALL_COPILOT" -eq 1 ] && _RENDER_HOSTS_SELECTED="$_RENDER_HOSTS_SELECTED copilot"
[ "$INSTALL_AGY" -eq 1 ] && _RENDER_HOSTS_SELECTED="$_RENDER_HOSTS_SELECTED agy"
# shellcheck disable=SC2086 # the host list splits on purpose # shellcheck disable=SC2086 # the host list splits on purpose
if ! gstack_render_hosts_add "$SOURCE_GSTACK_DIR" $_RENDER_HOSTS_SELECTED; then if ! gstack_render_hosts_add "$SOURCE_GSTACK_DIR" $_RENDER_HOSTS_SELECTED; then
echo " warning: could not write $(gstack_render_hosts_file "$SOURCE_GSTACK_DIR"); this build renders every host and nothing is pruned" >&2 echo " warning: could not write $(gstack_render_hosts_file "$SOURCE_GSTACK_DIR"); this build renders every host and nothing is pruned" >&2
@@ -1994,6 +2006,16 @@ if [ "$INSTALL_COPILOT" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
) )
fi fi
# 1g. Generate .agy/ Antigravity CLI skill docs
if [ "$INSTALL_AGY" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
log "Generating .agy/ skill docs..."
(
cd "$SOURCE_GSTACK_DIR"
bun_cmd install --frozen-lockfile 2>/dev/null || bun_cmd install
bun_cmd run gen:skill-docs --host agy ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"}
)
fi
# 2. Ensure Playwright's Chromium is available # 2. Ensure Playwright's Chromium is available
_PLAYWRIGHT_PLATFORM_OVERRIDE="" _PLAYWRIGHT_PLATFORM_OVERRIDE=""
_PLAYWRIGHT_UNSUPPORTED_ARCH="" _PLAYWRIGHT_UNSUPPORTED_ARCH=""
@@ -3213,6 +3235,83 @@ link_copilot_skill_dirs() {
fi fi
} }
# ─── Helper: ~/.gemini/antigravity-cli/skills/gstack runtime root ────────────
# Same shape as the Copilot root: root SKILL.md + runtime assets, with the
# .source-path _link_runtime_dists writes for /gstack-upgrade. Antigravity CLI
# and Gemini CLI share ~/.gemini; setup writes only gstack entries under
# ~/.gemini/antigravity-cli/skills, never Gemini CLI's ~/.gemini/skills.
# Built through _activate_runtime_root.
create_agy_runtime_root() {
local gstack_dir="$1"
local agy_gstack="$2"
local agy_dir="$gstack_dir/.agy/skills"
local asset f
mkdir -p "$agy_gstack" "$agy_gstack/browse" "$agy_gstack/gstack-upgrade" "$agy_gstack/review"
if [ -f "$agy_dir/gstack/SKILL.md" ]; then
_copy_skill_md "$agy_dir/gstack/SKILL.md" "$agy_gstack/SKILL.md"
fi
for asset in bin lib browse/dist browse/bin; do
if [ -d "$gstack_dir/$asset" ]; then
_link_or_copy "$gstack_dir/$asset" "$agy_gstack/$asset"
fi
done
_link_runtime_dists "$gstack_dir" "$agy_gstack"
if [ -f "$agy_dir/gstack-upgrade/SKILL.md" ]; then
_copy_skill_md "$agy_dir/gstack-upgrade/SKILL.md" "$agy_gstack/gstack-upgrade/SKILL.md"
fi
_copy_runtime_skill_refs "$agy_dir" "$agy_gstack"
if [ -f "$gstack_dir/ETHOS.md" ]; then
_link_or_copy "$gstack_dir/ETHOS.md" "$agy_gstack/ETHOS.md"
fi
if [ -f "$gstack_dir/supabase/config.sh" ]; then
mkdir -p "$agy_gstack/supabase"
_link_or_copy "$gstack_dir/supabase/config.sh" "$agy_gstack/supabase/config.sh"
fi
}
# Frontmatter names already equal the gstack-<skill> directories
# (nameMatchesDirectory), so no name patching. Fails when nothing was linked.
link_agy_skill_dirs() {
local gstack_dir="$1"
local skills_dir="$2"
local agy_dir="$gstack_dir/.agy/skills"
local linked=()
if [ ! -d "$agy_dir" ]; then
echo " Generating .agy/ skill docs..."
( cd "$gstack_dir" && bun_cmd run gen:skill-docs --host agy ${_DISABLED_CSV:+"--disabled-skills=$_DISABLED_CSV"} )
fi
if [ ! -d "$agy_dir" ]; then
echo " warning: .agy/skills/ generation failed — run 'bun run gen:skill-docs --host agy' manually" >&2
return 1
fi
for skill_dir in "$agy_dir"/gstack*/; do
if [ -f "$skill_dir/SKILL.md" ]; then
skill_name="$(basename "$skill_dir")"
[ "$skill_name" = "gstack" ] && continue
case "${_DISABLED_SKILLS:- }" in *" ${skill_name#gstack-} "*) _remove_disabled_host_entry "$skills_dir/$skill_name"; continue ;; esac
target="$skills_dir/$skill_name"
if [ "$IS_WINDOWS" -eq 1 ] || [ -L "$target" ] || [ ! -e "$target" ]; then
if _owned_for_windows_refresh "$target"; then
_link_or_copy "$skill_dir" "$target"
linked+=("$skill_name")
else
echo " left in place (existing dir not gstack-managed — no generated banner): $target" >&2
fi
fi
fi
done
if [ ${#linked[@]} -gt 0 ]; then
echo " linked skills: ${linked[*]}"
fi
if ! ls "$skills_dir"/gstack-*/SKILL.md >/dev/null 2>&1; then
echo " error: no gstack skills are installed in $skills_dir" >&2
return 1
fi
}
# 4. Install for Claude (default) # 4. Install for Claude (default)
# _claude_scope SKILLS_DIR — "global" for the user's Claude skills dir, else # _claude_scope SKILLS_DIR — "global" for the user's Claude skills dir, else
# "project <root>". # "project <root>".
@@ -3608,6 +3707,25 @@ if [ "$INSTALL_COPILOT" -eq 1 ]; then
fi fi
fi fi
# 6f. Install for Antigravity CLI (agy)
if [ "$INSTALL_AGY" -eq 1 ]; then
if _sidecar_root_user_owned "$AGY_GSTACK"; then
echo " left in place (existing dir not gstack-managed — no generated banner): $AGY_GSTACK" >&2
_setup_row agy global "$AGY_SKILLS" - "$SETUP_VERSION" skipped "left alone: $AGY_GSTACK is not gstack-managed (no generated banner)"
else
_setup_arm_begin agy global "$AGY_SKILLS"
mkdir -p "$AGY_SKILLS"
_prune_stale_generated "$SOURCE_GSTACK_DIR" "$SOURCE_GSTACK_DIR/.agy/skills" "$AGY_SKILLS"
link_agy_skill_dirs "$SOURCE_GSTACK_DIR" "$AGY_SKILLS"
_activate_runtime_root create_agy_runtime_root "$SOURCE_GSTACK_DIR" "$AGY_GSTACK"
_setup_arm_publish agy global - "$AGY_SKILLS" "$AGY_GSTACK" -
echo "gstack ready (agy)."
echo " browse: $BROWSE_BIN"
_browser_hint
echo " agy skills: $AGY_SKILLS (invoke as /gstack-<skill>)"
fi
fi
# 7. Create .agents/ sidecar symlinks for the real Codex skill target. # 7. Create .agents/ sidecar symlinks for the real Codex skill target.
# The root Codex skill ends up pointing at $SOURCE_GSTACK_DIR/.agents/skills/gstack, # The root Codex skill ends up pointing at $SOURCE_GSTACK_DIR/.agents/skills/gstack,
# so the runtime assets must live there for both global and repo-local installs. # so the runtime assets must live there for both global and repo-local installs.
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -3597,7 +3597,7 @@ describe('setup script validation', () => {
expect(setupContent).toContain('--host'); expect(setupContent).toContain('--host');
// #2361: slate moved OUT of the install accept-list (it was accepted but // #2361: slate moved OUT of the install accept-list (it was accepted but
// never dispatched — a silent exit-0 no-op) into an informational arm. // never dispatched — a silent exit-0 no-op) into an informational arm.
expect(setupContent).toContain('claude|codex|kiro|factory|opencode|cursor|copilot|auto'); expect(setupContent).toContain('claude|codex|kiro|factory|opencode|cursor|copilot|agy|auto');
expect(setupContent).toMatch(/^ {2}slate\)/m); expect(setupContent).toMatch(/^ {2}slate\)/m);
}); });
+59 -5
View File
@@ -23,9 +23,11 @@ import {
slate, slate,
cursor, cursor,
openclaw, openclaw,
agy,
} from '../hosts/index'; } from '../hosts/index';
import { HOST_PATHS } from '../scripts/resolvers/types'; import { HOST_PATHS } from '../scripts/resolvers/types';
import { RESOLVERS } from '../scripts/resolvers'; import { RESOLVERS } from '../scripts/resolvers';
import { expectTokens } from './helpers/prompt-structure';
import { runGeneration } from '../scripts/gen-skill-docs'; import { runGeneration } from '../scripts/gen-skill-docs';
const ROOT = path.resolve(import.meta.dir, '..'); const ROOT = path.resolve(import.meta.dir, '..');
@@ -34,8 +36,8 @@ const RESOLVER_NAMES = new Set(Object.keys(RESOLVERS));
// ─── hosts/index.ts ───────────────────────────────────────── // ─── hosts/index.ts ─────────────────────────────────────────
describe('hosts/index.ts', () => { describe('hosts/index.ts', () => {
test('ALL_HOST_CONFIGS has 11 hosts', () => { test('ALL_HOST_CONFIGS has 12 hosts', () => {
expect(ALL_HOST_CONFIGS.length).toBe(11); expect(ALL_HOST_CONFIGS.length).toBe(12);
}); });
test('ALL_HOST_NAMES matches config names', () => { test('ALL_HOST_NAMES matches config names', () => {
@@ -57,6 +59,7 @@ describe('hosts/index.ts', () => {
expect(slate.name).toBe('slate'); expect(slate.name).toBe('slate');
expect(cursor.name).toBe('cursor'); expect(cursor.name).toBe('cursor');
expect(openclaw.name).toBe('openclaw'); expect(openclaw.name).toBe('openclaw');
expect(agy.name).toBe('agy');
}); });
test('getHostConfig returns correct config', () => { test('getHostConfig returns correct config', () => {
@@ -78,6 +81,7 @@ describe('hosts/index.ts', () => {
test('resolveHostArg resolves aliases', () => { test('resolveHostArg resolves aliases', () => {
expect(resolveHostArg('agents')).toBe('codex'); expect(resolveHostArg('agents')).toBe('codex');
expect(resolveHostArg('droid')).toBe('factory'); expect(resolveHostArg('droid')).toBe('factory');
expect(resolveHostArg('antigravity')).toBe('agy');
}); });
test('resolveHostArg throws on unknown alias', () => { test('resolveHostArg throws on unknown alias', () => {
@@ -445,7 +449,7 @@ describe('golden-file regression', () => {
const GOLDEN_OUT = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-golden-out-')); const GOLDEN_OUT = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-golden-out-'));
beforeAll(() => { beforeAll(() => {
for (const host of ['codex', 'factory']) { for (const host of ['codex', 'factory', 'agy']) {
const result = Bun.spawnSync( const result = Bun.spawnSync(
['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', host, '--out-dir', GOLDEN_OUT], ['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', host, '--out-dir', GOLDEN_OUT],
{ cwd: ROOT, timeout: 120_000 }, { cwd: ROOT, timeout: 120_000 },
@@ -498,14 +502,21 @@ describe('golden-file regression', () => {
const current = fs.readFileSync(path.join(GOLDEN_OUT, '.factory', 'skills', 'gstack-ship', 'SKILL.md'), 'utf-8'); const current = fs.readFileSync(path.join(GOLDEN_OUT, '.factory', 'skills', 'gstack-ship', 'SKILL.md'), 'utf-8');
expect(current).toBe(golden); expect(current).toBe(golden);
}); });
test('Antigravity ship skill matches golden baseline', () => {
const golden = fs.readFileSync(path.join(GOLDEN_DIR, 'agy-ship-SKILL.md'), 'utf-8');
const current = fs.readFileSync(path.join(GOLDEN_OUT, '.agy', 'skills', 'gstack-ship', 'SKILL.md'), 'utf-8');
expect(current).toBe(golden);
});
}); });
// ─── Individual host config correctness ───────────────────── // ─── Individual host config correctness ─────────────────────
describe('host config correctness', () => { describe('host config correctness', () => {
test('Codex host renders with generic GPT overlay while existing hosts retain Claude overlay', () => { test('Codex renders the GPT overlay, Antigravity the Gemini overlay, every other host the Claude overlay', () => {
expect(codex.defaultModel).toBe('gpt'); expect(codex.defaultModel).toBe('gpt');
for (const host of ALL_HOST_CONFIGS.filter(h => h.name !== 'codex')) { expect(agy.defaultModel).toBe('gemini');
for (const host of ALL_HOST_CONFIGS.filter(h => h.name !== 'codex' && h.name !== 'agy')) {
expect(host.defaultModel).toBe('claude'); expect(host.defaultModel).toBe('claude');
} }
}); });
@@ -756,6 +767,49 @@ describe('Copilot host render (#393)', () => {
}); });
}); });
describe('Antigravity CLI host render', () => {
const OUT = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-agy-'));
beforeAll(() => {
const r = Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'agy', '--out-dir', OUT], { cwd: ROOT, timeout: 120_000 });
if (r.exitCode !== 0) throw new Error(r.stderr.toString());
});
afterAll(() => fs.rmSync(OUT, { recursive: true, force: true }));
const dir = path.join(OUT, '.agy', 'skills');
const read = (skill: string) => fs.readFileSync(path.join(dir, skill, 'SKILL.md'), 'utf8');
test('paths name the Antigravity CLI install, never Claude, Codex or Gemini CLI skill dirs', () => {
for (const skill of ['gstack-review', 'gstack-ship', 'gstack-careful', 'gstack-upgrade']) {
const md = read(skill);
expect(md).not.toContain('~/.claude/skills/gstack');
expect(md).not.toContain('$HOME/.claude/skills/gstack');
expect(md).not.toContain('.codex/skills');
expect(md).not.toMatch(/\.gemini\/(?:config\/)?skills/);
}
expect(read('gstack-review')).toContain('_r=~/.gemini/antigravity-cli/skills/gstack');
expect(read('gstack-upgrade')).toContain('$HOME/.gemini/antigravity-cli/skills/gstack/.source-path');
expect(read('gstack-upgrade')).toContain('./setup --host agy --refresh-registered');
});
test('the tool-name glossary names the documented tools ahead of the preamble STATUS rules', () => {
const md = read('gstack-review');
const glossary = md.indexOf('**Antigravity tool names:**');
expect(glossary).toBeGreaterThan(-1);
expect(glossary).toBeLessThan(md.indexOf('Read the echoed `KEY: value` STATUS lines'));
const line = md.slice(glossary, md.indexOf('\n', glossary));
expectTokens(line, ['`ask_question`', '`run_command`', '`view_file`', '`write_to_file`', '`replace_file_content`', '`grep_search`', '`find_by_name`', '`invoke_subagent`']);
});
test('frontmatter keeps only name and description, and every name equals its directory', () => {
const names = fs.readdirSync(dir).filter(d => fs.existsSync(path.join(dir, d, 'SKILL.md')));
expect(names.length).toBeGreaterThan(20);
for (const name of names) {
const fm = read(name).split('\n---\n')[0];
expect(fm.match(/^name:\s*(\S+)/m)![1]).toBe(name);
expect(fm.split('\n').filter(l => /^[a-z-]+:/.test(l)).map(l => l.split(':')[0])).toEqual(['name', 'description']);
}
});
});
describe('host renders name the host\'s own tools and identities (#2626, #2015, #2825, #2338)', () => { describe('host renders name the host\'s own tools and identities (#2626, #2015, #2825, #2338)', () => {
const OUT = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-host-tools-')); const OUT = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-host-tools-'));
beforeAll(() => { beforeAll(() => {
+3 -2
View File
@@ -29,9 +29,10 @@ const DISCOVERY: Record<string, string> = {
opencode: '.config/opencode/skills', opencode: '.config/opencode/skills',
cursor: '.cursor/skills', cursor: '.cursor/skills',
copilot: '.copilot/skills', copilot: '.copilot/skills',
agy: '.gemini/antigravity-cli/skills',
}; };
/** Every directory any host (or a shared agents layout) reads. */ /** Every directory any host (or a shared agents layout) reads; .gemini/skills is Gemini CLI's. */
const ALL_DISCOVERY_DIRS = [...Object.values(DISCOVERY), '.agents/skills', '.copilot/skills', '.hermes/skills']; const ALL_DISCOVERY_DIRS = [...Object.values(DISCOVERY), '.agents/skills', '.copilot/skills', '.hermes/skills', '.gemini/skills'];
const installable = ALL_HOST_CONFIGS.filter(c => c.tier !== 'instruction-only').map(c => c.name); const installable = ALL_HOST_CONFIGS.filter(c => c.tier !== 'instruction-only').map(c => c.name);
const instructionOnly = ALL_HOST_CONFIGS.filter(c => c.tier === 'instruction-only').map(c => c.name); const instructionOnly = ALL_HOST_CONFIGS.filter(c => c.tier === 'instruction-only').map(c => c.name);
+2 -2
View File
@@ -17,7 +17,7 @@ import { runGeneration } from '../scripts/gen-skill-docs';
import { cleanupFixtures, cleanupSeed, makeFixture, makeSource, put, runSetup, type Fixture } from './helpers/install-fixture'; import { cleanupFixtures, cleanupSeed, makeFixture, makeSource, put, runSetup, type Fixture } from './helpers/install-fixture';
const ROOT = path.resolve(import.meta.dir, '..'); const ROOT = path.resolve(import.meta.dir, '..');
const OTHER_DIRS = ['.agents', '.factory', '.kiro', '.opencode', '.cursor', '.copilot', '.slate', '.openclaw', '.hermes', '.gbrain']; const OTHER_DIRS = ['.agents', '.factory', '.kiro', '.opencode', '.cursor', '.copilot', '.agy', '.slate', '.openclaw', '.hermes', '.gbrain'];
afterEach(cleanupFixtures); afterEach(cleanupFixtures);
afterAll(cleanupSeed); afterAll(cleanupSeed);
@@ -68,7 +68,7 @@ describe.skipIf(process.platform === 'win32')('setup prunes unrecorded host rend
expect(fs.existsSync(path.join(f.dir, 'outside'))).toBe(true); expect(fs.existsSync(path.join(f.dir, 'outside'))).toBe(true);
expect(fs.existsSync(path.join(src, '.cursor/skills/gstack/bin'))).toBe(false); expect(fs.existsSync(path.join(src, '.cursor/skills/gstack/bin'))).toBe(false);
expect(fs.existsSync(path.join(src, 'bin/gstack-config'))).toBe(true); expect(fs.existsSync(path.join(src, 'bin/gstack-config'))).toBe(true);
for (const dir of ['.agents', '.opencode', '.copilot', '.slate', '.openclaw', '.hermes', '.gbrain']) expect(fs.existsSync(path.join(src, dir)), dir).toBe(false); for (const dir of ['.agents', '.opencode', '.copilot', '.agy', '.slate', '.openclaw', '.hermes', '.gbrain']) expect(fs.existsSync(path.join(src, dir)), dir).toBe(false);
expect(r.stdout).toContain('kept .factory/notes.md: not proven generated (no gstack banner or byte match)'); expect(r.stdout).toContain('kept .factory/notes.md: not proven generated (no gstack banner or byte match)');
expect(r.stdout).toContain('kept .kiro/skills/my-skill/SKILL.md: not proven generated'); expect(r.stdout).toContain('kept .kiro/skills/my-skill/SKILL.md: not proven generated');
+1
View File
@@ -36,6 +36,7 @@ const ROOT_BUILDERS: Record<string, string> = {
opencode: 'create_opencode_runtime_root', opencode: 'create_opencode_runtime_root',
cursor: 'create_cursor_runtime_root', cursor: 'create_cursor_runtime_root',
copilot: 'create_copilot_runtime_root', copilot: 'create_copilot_runtime_root',
agy: 'create_agy_runtime_root',
}; };
// Deferred, with the reason recorded in TODOS.md: /ship's measure loop runs // Deferred, with the reason recorded in TODOS.md: /ship's measure loop runs
+53
View File
@@ -185,6 +185,59 @@ describe.skipIf(process.platform === 'win32')('disabled_skills (#1206)', () => {
}, 120_000); }, 120_000);
}); });
describe.skipIf(process.platform === 'win32')('Antigravity CLI shares ~/.gemini with Gemini CLI', () => {
const geminiCliFiles = (home: string) => ({
settings: readFileSync(join(home, '.gemini/settings.json'), 'utf8'),
skill: readFileSync(join(home, '.gemini/skills/my-skill/SKILL.md'), 'utf8'),
});
test.skipIf(Bun.which('agy') !== null)('auto-detect ignores a Gemini-CLI-only ~/.gemini and selects agy by its own config dir', () => {
const f = makeFixture();
const src = makeSource(f, join(f.home, 'gstack'));
put(join(f.home, '.gemini/settings.json'), '{"theme":"mine"}\n');
put(join(f.home, '.gemini/skills/my-skill/SKILL.md'), '---\nname: my-skill\ndescription: mine\n---\n');
const before = geminiCliFiles(f.home);
const geminiOnly = runSetup(f, join(src, 'setup'), ['--host', 'auto']);
expect(geminiOnly.status, geminiOnly.stdout + geminiOnly.stderr).toBe(0);
expect(existsSync(join(f.home, '.gemini/antigravity-cli'))).toBe(false);
expect(registryRows(f).map(row => row[0])).not.toContain('agy');
mkdirSync(join(f.home, '.gemini/antigravity-cli'), { recursive: true });
const withAgy = runSetup(f, join(src, 'setup'), ['--host', 'auto']);
expect(withAgy.status, withAgy.stdout + withAgy.stderr).toBe(0);
expect(existsSync(join(f.home, '.gemini/antigravity-cli/skills/gstack-review/SKILL.md'))).toBe(true);
expect(registryRows(f).map(row => row[0])).toContain('agy');
expect(geminiCliFiles(f.home)).toEqual(before);
expect(readdirSync(join(f.home, '.gemini/skills'))).toEqual(['my-skill']);
}, 120_000);
test('--host antigravity (alias) installs under antigravity-cli/skills only; uninstall removes only gstack entries', () => {
const f = makeFixture();
const src = makeSource(f, join(f.home, 'gstack'));
put(join(f.home, '.gemini/settings.json'), '{"theme":"mine"}\n');
put(join(f.home, '.gemini/skills/my-skill/SKILL.md'), '---\nname: my-skill\ndescription: mine\n---\n');
put(join(f.home, '.gemini/antigravity-cli/settings.json'), '{"agentMode":"plan"}\n');
put(join(f.home, '.gemini/antigravity-cli/skills/gstack-notes/SKILL.md'), '---\nname: gstack-notes\ndescription: my notes\n---\n');
const before = geminiCliFiles(f.home);
const r = runSetup(f, join(src, 'setup'), ['--host', 'antigravity']);
expect(r.status, r.stdout + r.stderr).toBe(0);
expect(registryRows(f).map(row => [row[0], row[3]])).toEqual([['agy', join(f.home, '.gemini/antigravity-cli/skills')]]);
expect(readFileSync(join(f.home, '.gemini/antigravity-cli/skills/gstack/.source-path'), 'utf8').trim()).toBe(src);
expect(readFileSync(join(src, '.gstack-installed-hosts'), 'utf8').split('\n')).toContain('agy');
expect(existsSync(join(src, '.agy/skills/gstack-review/SKILL.md'))).toBe(true);
expect(geminiCliFiles(f.home)).toEqual(before);
const un = spawnSync('bash', [join(src, 'bin/gstack-uninstall'), '--force', '--keep-state'], { cwd: f.home, env: f.env, encoding: 'utf8', timeout: 30_000 });
expect(un.status, un.stderr).toBe(0);
expect(readdirSync(join(f.home, '.gemini/antigravity-cli/skills'))).toEqual(['gstack-notes']);
expect(readFileSync(join(f.home, '.gemini/antigravity-cli/settings.json'), 'utf8')).toBe('{"agentMode":"plan"}\n');
expect(geminiCliFiles(f.home)).toEqual(before);
expect(registryRows(f)).toEqual([]);
}, 120_000);
});
describe('install registry helper', () => { describe('install registry helper', () => {
const helper = join(ROOT, 'bin/gstack-install-registry.sh'); const helper = join(ROOT, 'bin/gstack-install-registry.sh');