Plugin and migration
How agb finds its own plugin root and the adlc-antigravity companion plugin, what bundled mode ignores, how the vendored tarball is pinned, and the agb migrate state machine.
agb ships as a native agy plugin named antigravity-booster, installed under ~/.gemini/config/plugins/. It depends on a second plugin, adlc-antigravity (npm package @adlc/antigravity), which carries the ADLC doctrine skills, the prosecutor agent and the rails-guard hook. This page covers how both are located, verified and installed, and how agb migrate moves an older npm-global install onto the plugin.
The design is specified in .adlc/specs/native-plugin-installation.md; the earlier draft lives at docs/specs/native-plugin-installation.md.
Finding the booster plugin root
resolvePluginRoot walks up from its own file until it finds a plugin.json whose name is antigravity-booster (or starts with antigravity-booster-). PLUGIN_ROOT is honored only if it resolves to that same directory, so tooling such as direnv cannot point agb at different assets. If no root is found it throws, because a missing root means a broken installation (lib/plugin-paths.mjs).
Bundled mode
The plugin runs esbuild bundles (see Build and bundle) built with --define:__AGB_BUNDLED__=true. Source runs (npm test, local development) see the identifier as undefined. The switch is a single constant (lib/adlc-bridge.mjs):
export const IS_BUNDLED = typeof __AGB_BUNDLED__ !== 'undefined' && __AGB_BUNDLED__ === true;When it is true, developer overrides are ignored, so a repository's .envrc cannot redirect a user's install:
| Override | Effect when unbundled | Bundled |
|---|---|---|
AGB_PLUGIN_DIR | Where the installed adlc-antigravity manifest is read from (the contract handshake) | Ignored; always ~/.gemini/config/plugins/adlc-antigravity (lib/adlc-bridge.mjs) |
ADLC_CLI_PATH / AGB_ADLC_BIN with AGB_ALLOW_CUSTOM_ADLC_CLI=1 | A custom adlc binary, ahead of the vendored one | Ignored; only the digest-pinned vendored adlc under vendor/adlc/ is used (lib/adlc-bridge.mjs) |
ADLC_ANTIGRAVITY_PLUGIN_PATH with AGB_DEV_ALLOW_UNVERIFIED_PLUGIN=1 | agb bootstrap installs the companion plugin from that directory, unverified | Ignored (lib/bootstrap.mjs) |
The MCP server has its own copy of the same check to choose between the bundled dist/agb.mjs and bin/agb.mjs (mcp/server.mjs). Every bundled-mode guard on an environment variable is listed in Environment variables.
Resolving the companion plugin source (unbundled)
In source runs, resolvePluginPath picks the adlc-antigravity source directory in this order (lib/bootstrap.mjs):
ADLC_ANTIGRAVITY_PLUGIN_PATH, if set.@adlc/antigravityinnode_modules, found withrequire.resolve('@adlc/antigravity/package.json')(the package has nomain/exports, so thepackage.jsonsubpath is what resolves).- The sibling checkout
../adlc/plugins/adlc-antigravity.
Vendored tarball pinning
The plugin must work from a git-URL install with no npm install, so the repository commits the pristine npm release tarball of @adlc/antigravity at vendor/cache/adlc-antigravity-<version>.tgz. Its version and SHA-512 are constants (lib/plugin-paths.mjs). The pin is checked three ways:
- At install time:
installPluginTarballrefuses a tarball whose SHA-512 does not match (vendored-adlc-antigravity-tampered), validates the archive's entries, extracts it into a temp directory and installs it withagy plugin install(lib/plugin-paths.mjs). The integrity value cannot be overridden by a caller. - In CI: the
plugin-integrityjob runsnpm pack @adlc/antigravity@<version>and fails unless the committed tarball is byte-identical (.github/workflows/ci.yml). The version comes frompackage.jsondevDependencies, so the lockfile integrity must agree as well. - After install: the staged plugin's directory tree digest must match the pinned digest for its version (below).
Bootstrap and doctor decision table
agb bootstrap and agb doctor share one evaluation of ~/.gemini/config/plugins/adlc-antigravity, evaluateStagedAdlcPlugin, applied in order (lib/doctor.mjs):
| Staged plugin | Report | doctor exit | bootstrap action |
|---|---|---|---|
No plugin.json | not-installed | 1 | install |
Manifest unreadable, invalid, or without a semver version | corrupt-manifest | 1 | fail (leave untouched) |
| Older than the bundled version | outdated-plugin | 1 | install |
| Pinned version, tree digest mismatch | corrupt-tree | 1 | reinstall |
| Pinned version, digest matches, contract not compatible | incompatible-contract | 1 | reinstall |
Pinned version, digest matches, adlcContract compatible | compatible (rails trusted) | 0 | preserve |
| Same as bundled version but no digest recorded | corrupt-tree | 1 | reinstall |
| Newer, unpinned, compatible contract | compatible (newer-unpinned) | 0 | preserve |
Newer, unpinned, no adlcContract | tolerant (unconfirmed-contract) | 0 | preserve |
| Newer, unpinned, other contract | incompatible-contract | 1 | fail |
Bootstrap installs only from the vendored tarball, never rewrites files inside a staged plugin, and re-evaluates afterwards: the install counts only if it lands on a row where doctor exits 0 (lib/bootstrap.mjs). --force-reinstall always reinstalls.
agb migrate
agb migrate moves an npm-global or checkout install onto the native plugin, and agb migrate --rollback restores the state from before the first migration. All state lives under ~/.gemini/antigravity-cli/plugin_data/antigravity-booster/ (lib/migrate.mjs):
| Path | Contents |
|---|---|
migration-state.json | The single state file |
.migration.lock.d/ | The migration lock (meta.json: pid, start time, token) |
snapshots/<stamp>/plugins/<name>/ | Copies of both plugin trees, excluding node_modules, .worktrees and .git |
snapshots/<stamp>/shim/agb | The previous ~/.local/bin/agb |
snapshots/<stamp>/import_manifest.json | The previous booster and adlc import entries |
snapshots/<stamp>/pre-migration.baseline.json | Written once, never overwritten; every restore reads from it |
Lock
The migration lock is separate from the repository run lock in lib/lock.mjs. It is an atomic mkdir. A holder is reclaimed only when it is positively dead (the PID is gone, or was recycled, detected by comparing process start times). A live or unverifiable holder is never stolen, and reclaiming never removes a lock directory whose token changed since it was inspected (lib/migration-lock.mjs). agb migrate --break-lock removes a stuck lock after a [y/N] prompt, or without one with --force.
States
Each forward step writes the state file only after it finishes (lib/migrate.mjs, L348, L366), so a crash resumes from the last completed state.
Runs a pre-flight validation of the booster plugin, takes the lock, then acts on the current state (lib/migrate.mjs):
- none: start from the snapshot step.
SNAPSHOT_CREATED,SYMLINKS_RECORDED,PLUGINS_STAGED: resume from that state.MIGRATED: refuse unless--force, which re-runs from a new snapshot (the baseline is kept).ROLLBACK_IN_PROGRESS: refuse; finish the rollback first.ROLLED_BACK_PENDING_UNINSTALL: finish the uninstall if the booster plugin is still present; otherwise treat as rolled back.ROLLED_BACK: archive the old state file and start fresh.- unknown: refuse.
A staged plugin larger than 100 MB (after exclusions) stops the snapshot step.
All migrate entry points return an exit code (0 or 1) and do not throw.