antigravity

package
v0.11.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 14, 2026 License: MIT Imports: 28 Imported by: 0

Documentation

Overview

Package antigravity implements the Agent interface for Antigravity (Google's agentic coding CLI).

Index

Constants

View Source
const (
	HookNamePreToolUse    = "pre-tool-use"
	HookNamePreInvocation = "pre-invocation"
	HookNameStop          = "stop"
)

Antigravity hook name constants — these become subcommands under `entire hooks antigravity`.

Only the hooks with lifecycle significance are installed. agy also offers PostToolUse and PostInvocation, but neither maps to an Entire lifecycle event (PostInvocation fires before the transcript is written; PostToolUse duplicates what PreToolUse already captures) — installing them would spawn a no-op `entire` subprocess on every completed tool call and model invocation.

View Source
const (
	CondensedRoleUser      = "user"
	CondensedRoleAssistant = "assistant"
	CondensedRoleTool      = "tool"
)

Roles of a CondensedStep.

View Source
const AgentsHooksFileName = "hooks.json"

AgentsHooksFileName is the hooks file used by Antigravity.

View Source
const DoctorProbeEnv = "ENTIRE_ANTIGRAVITY_DOCTOR_PROBE"

DoctorProbeEnv opts `entire doctor` into running ProbeLoadedHooks. Off by default: the probe's zero-quota argument rested on the version gate alone, and on Windows 11 with agy 1.2.7 `agy -p "/hooks"` was observed to run a full model turn (~12k input tokens) instead of answering locally. Until the local-answer behaviour is verified per platform rather than assumed from a version number, a default doctor run must not risk the user's quota. The probe also only proves agy PARSED hooks.json, not that the command in it can run; HooksEntryMatchesHost is the check that catches the failure that actually occurs.

View Source
const MinHooksProbeVersion = "1.1.12"

MinHooksProbeVersion is the first agy release whose print mode answers the read-only `/hooks` slash command locally — one tab-separated record per hook (or a structured payload under --output-format json) — "without starting an agent turn, spending quota or leaving a conversation behind" (agy 1.1.12 release notes). On older releases `-p "/hooks"` is sent to the model as literal prompt text, so the probe must never run there.

Variables

View Source
var ErrHooksProbeUnsupported = errors.New("agy too old to answer /hooks in print mode")

ErrHooksProbeUnsupported is returned when the installed agy predates MinHooksProbeVersion; callers should report "cannot verify" rather than "not loaded".

View Source
var ErrHooksProbeVersionUnknown = errors.New("could not determine the agy version")

ErrHooksProbeVersionUnknown is returned when `agy --version` printed something semver cannot parse (a build suffix, a banner line). It is deliberately distinct from ErrHooksProbeUnsupported: the agy may well be newer than the requirement, so "run `agy update`" would be wrong advice. The probe is skipped either way — a wrong guess is a real model turn.

Functions

func AppendStatusSnapshot

func AppendStatusSnapshot(payload []byte) error

AppendStatusSnapshot parses an agy state-JSON payload and appends a snapshot to the per-conversation JSONL file. The hot path never returns an error for malformed input — only for genuine I/O failures.

agy fires the title command on every agent state change and does not serialize the invocations, so two tees can run at once. The dedup read and the append therefore happen under one advisory lock per conversation (<id>.jsonl.lock, next to the file); without it a concurrent tee could append between the read and the write and the dedup would miss.

func InstallTitleTee

func InstallTitleTee() error

InstallTitleTee installs the title-tee shim into agy's global settings.json. If a user's own title command is already present, it is preserved via --wrap. The call is idempotent: if our marker is already in the command, it returns nil.

func NewAntigravityAgent

func NewAntigravityAgent() agent.Agent

NewAntigravityAgent creates a new AntigravityAgent instance.

func TitleTeeInstalled

func TitleTeeInstalled() bool

TitleTeeInstalled reports whether agy's global settings.json declares a title command containing the title-tee marker. It is used by `entire doctor` to warn when Antigravity hooks are installed in a repo but the global title slot — agy's only token-usage surface — has not been claimed, which would leave token counts missing from checkpoints. A missing or unparseable settings file reports false.

func UninstallTitleTee

func UninstallTitleTee() error

UninstallTitleTee removes or restores the title entry in agy's global settings.json:

  • bare tee (no --wrap) → delete "title" key
  • tee with --wrap 'X' → restore X
  • any other (foreign) cmd → leave untouched
  • missing settings file → no-op

Types

type AntigravityAgent

type AntigravityAgent struct {
	CommandRunner agent.TextCommandRunner
	// contains filtered or unexported fields
}

AntigravityAgent implements the Agent interface for Antigravity.

func (*AntigravityAgent) AreHooksInstalled

func (a *AntigravityAgent) AreHooksInstalled(ctx context.Context) (bool, error)

AreHooksInstalled checks if Entire hooks are installed.

func (*AntigravityAgent) CalculateTokenUsageSince

func (a *AntigravityAgent) CalculateTokenUsageSince(ctx context.Context, sessionID string, baseline json.RawMessage) (*agent.TokenUsage, error)

CalculateTokenUsageSince computes the delta between the baseline snapshot and the latest persisted snapshot.

Exact: InputTokens/OutputTokens (cumulative totals minus baseline totals). Best-effort: cache fields and APICallCount, derived from the snapshot lines appended after the baseline timestamp (the dedup writer appends ~one line per API response, but lines can be missed between agent state changes).

func (*AntigravityAgent) ChunkTranscript

func (a *AntigravityAgent) ChunkTranscript(_ context.Context, content []byte, maxSize int) ([][]byte, error)

func (*AntigravityAgent) CountTranscriptPosition

func (a *AntigravityAgent) CountTranscriptPosition(content []byte) int

CountTranscriptPosition implements agent.LateTranscriptWriter: agy writes its transcript only after the Stop hook, and its offset metric counts non-blank lines. Delegating to forEachNonBlankLine keeps this byte-identical with GetTranscriptPosition/ExtractPrompts (see the iterator's doc comment).

func (*AntigravityAgent) Description

func (a *AntigravityAgent) Description() string

func (*AntigravityAgent) DetectPresence

func (a *AntigravityAgent) DetectPresence(ctx context.Context) (bool, error)

DetectPresence reports whether Entire's Antigravity hooks are configured for this workspace. Antigravity 2.0 stores runtime data user-scope in ~/.gemini/antigravity-cli/, so the only meaningful workspace-level signal is whether our entry exists in .agents/hooks.json.

func (*AntigravityAgent) DiscoverReviewSkills

func (a *AntigravityAgent) DiscoverReviewSkills(ctx context.Context) ([]agent.DiscoveredSkill, error)

DiscoverReviewSkills walks Antigravity's three user-skill scopes looking for review-adjacent skills. agy stores skills as <scope>/<name>/SKILL.md with YAML frontmatter (name + description); the model activates them by description, and they are referenced with a "/name" slash invocation.

Scopes, in precedence order (global wins on name collision):

  • global (agy 1.1+): ~/.gemini/config/skills
  • global (pre-1.1 layouts): ~/.gemini/antigravity-cli/skills, ~/.gemini/skills
  • workspace (agy 1.1+): <repo>/.agents/skills
  • workspace (legacy, honored): <repo>/.agent/skills

agy 1.1 moved the defaults ("Antigravity now defaults to .agents/skills, but still maintains backward support for .agent/skills"; global discovery under ~/.gemini/config/) — all roots are scanned so skills keep resolving across agy versions.

Google's built-in skills under ~/.gemini/antigravity-cli/builtin/skills are deliberately NOT scanned — they are shipped guides (e.g. antigravity-guide), not user review tools.

Best-effort per the SkillDiscoverer contract: unreadable HOME or missing directories yield (nil, nil); malformed SKILL.md files are skipped by the shared scanner with a Debug log.

func (*AntigravityAgent) ExtractModifiedFilesFromOffset

func (a *AntigravityAgent) ExtractModifiedFilesFromOffset(ctx context.Context, path string, startOffset int) (files []string, currentPosition int, err error)

ExtractModifiedFilesFromOffset implements agent.TranscriptAnalyzer. It scans agy step lines after startOffset for mutating tool calls and returns the target file paths they touch, deduplicated, alongside the new line position.

Path convention: returned paths are ABSOLUTE and symlink-resolved — the same shape lifecycle.go's parsePreToolUse records into FilesTouched. The framework relativizes downstream via FilterAndNormalizePaths -> paths.ToRelativePath against the worktree root, so we must NOT pre-relativize here. We mirror parsePreToolUse exactly: decode the double-encoded TargetFile arg, then resolveAgySymlinks so the path matches what attribution diffs against (e.g. macOS /tmp -> /private/tmp). Both helpers live in lifecycle.go (same package) and are reused, not duplicated.

The blank-skip -> lineNum++ -> (lineNum <= startOffset) ordering matches ExtractPrompts so positions stay consistent across analyzer methods.

func (*AntigravityAgent) ExtractPrompts

func (a *AntigravityAgent) ExtractPrompts(sessionRef string, fromOffset int) ([]string, error)

ExtractPrompts implements agent.PromptExtractor. agy's PreInvocation hook carries no prompt, so the user prompt is recovered from the transcript's USER_INPUT steps. fromOffset is a count of non-blank lines already consumed.

func (*AntigravityAgent) ExtractPromptsFromTranscript

func (a *AntigravityAgent) ExtractPromptsFromTranscript(content []byte, fromOffset int) ([]string, error)

ExtractPromptsFromTranscript implements agent.TranscriptPromptExtractor over transcript bytes the caller already holds — condensation's copy of the transcript — with the same offset metric as ExtractPrompts. It never reads a path, so the prompts it returns always describe the bytes being checkpointed.

func (*AntigravityAgent) FormatResumeCommand

func (a *AntigravityAgent) FormatResumeCommand(sessionID string) string

func (*AntigravityAgent) GenerateText

func (a *AntigravityAgent) GenerateText(ctx context.Context, prompt string, model string) (string, error)

GenerateText submits a non-interactive prompt to the Antigravity CLI. The binary is `agy`; -p is the short alias for --print (single-prompt mode).

The prompt travels in argv. Earlier releases accepted it on stdin behind a single-space -p placeholder (the Gemini CLI convention, verified on agy 1.0.16), but agy 1.2.7 ignores stdin in print mode: `-p " "` fails with "Error: empty prompt", and `-p -` is answered as the literal message "-" (both observed live, trail 444, 2026-09-22), which is how `entire dispatch --local --agent antigravity` came to hand agy an empty prompt. argv is the only documented route ("Usage: agy --print 'your prompt here'").

That makes prompt size this agent's problem in a way it is not for agents that use RunIsolatedTextGeneratorCLI's stdin. Linux caps a SINGLE argument at MAX_ARG_STRLEN (128 KiB) however large the total ARG_MAX is, so an unbounded prompt fails with E2BIG. summarize.maxCondensedTranscriptBytes is what keeps summary prompts inside it. Windows' ~32 KiB whole-command-line limit is tighter than any useful transcript budget and is not covered; a long enough prompt still fails there, loudly.

agy also runs with a home directory of its own (isolatedHomeEnv). It reads its window-title command (~/.gemini/antigravity-cli/settings.json), its PreInvocation hooks (~/.gemini/config/hooks.json), MCP servers, and skills from the user's home, and print mode executes the title command and the hooks (observed live on agy 1.3.1). The empty working directory keeps workspace configuration out but not that, and agy has no flag to skip it, so a summary run, whose prompt carries untrusted transcript content, would otherwise execute whatever the user's global configuration names.

func (*AntigravityAgent) GetSessionDir

func (a *AntigravityAgent) GetSessionDir(_ string) (string, error)

func (*AntigravityAgent) GetSessionID

func (a *AntigravityAgent) GetSessionID(input *agent.HookInput) string

func (*AntigravityAgent) GetTranscriptPosition

func (a *AntigravityAgent) GetTranscriptPosition(path string) (int, error)

GetTranscriptPosition implements agent.TranscriptAnalyzer. It returns the number of non-blank JSONL lines in the transcript, which the framework uses as a stable offset to bound subsequent extraction to a single checkpoint range. A missing file yields (0, nil) so a not-yet-flushed transcript (agy writes asynchronously) doesn't fail the hook.

func (*AntigravityAgent) HookConfigRelPath

func (a *AntigravityAgent) HookConfigRelPath() string

HookConfigRelPath implements agent.HookConfigLocator: agy loads workspace hooks from .agents/hooks.json at the worktree root.

func (*AntigravityAgent) HookNames

func (a *AntigravityAgent) HookNames() []string

HookNames returns the hook verbs Antigravity supports. These become subcommands: entire hooks antigravity <verb>

func (*AntigravityAgent) HooksDisabled

func (a *AntigravityAgent) HooksDisabled(ctx context.Context) (bool, error)

HooksDisabled reports whether the repo's "entire" entry is present but explicitly switched off with "enabled": false. InstallHooks already treats that as a deliberate opt-out and leaves the entry alone.

It exists so `entire doctor` can stay quiet about a configuration the user turned off. AreHooksInstalled and DetectPresence deliberately still report such an entry as present: they drive agent auto-detection and `entire agent list`, which describe what is on disk.

func (*AntigravityAgent) HooksEntryMatchesHost

func (a *AntigravityAgent) HooksEntryMatchesHost(ctx context.Context) (installed, current bool, err error)

HooksEntryMatchesHost reports whether the repo's installed "entire" entry is exactly what InstallHooks would write on THIS host. installed is false when there is no entry. A user-disabled entry counts as current.

It exists for `entire doctor`: the hook command's SHAPE is host-specific (agy runs it through cmd.exe on Windows and sh elsewhere), and a hooks.json committed from a macOS checkout carries a sh wrapper that cmd.exe tears apart — the hook exits 1, the failure shows only in agy's log, and nothing is tracked. A file that merely exists proves nothing about that; comparing against the candidate does, at zero cost and without spawning agy.

func (*AntigravityAgent) InstallHooks

func (a *AntigravityAgent) InstallHooks(ctx context.Context, force bool) (int, error)

InstallHooks installs Antigravity hooks in .agents/hooks.json. If force is true, removes existing Entire hooks before installing. Returns the number of hooks installed.

func (*AntigravityAgent) Name

func (a *AntigravityAgent) Name() types.AgentName

func (*AntigravityAgent) ParseHookEvent

func (a *AntigravityAgent) ParseHookEvent(_ context.Context, hookName string, stdin io.Reader) (*agent.Event, error)

ParseHookEvent translates an Antigravity hook into a normalized lifecycle Event. Returns nil if the hook has no lifecycle significance.

func (*AntigravityAgent) PrepareTranscript

func (a *AntigravityAgent) PrepareTranscript(ctx context.Context, transcriptRef string) error

PrepareTranscript implements the optional TranscriptPreparer interface. The framework calls this in handleLifecycleTurnEnd BEFORE its fileExists check — so we use it to handle agy's asynchronous transcript write.

Background: agy writes its transcript file at

~/.gemini/antigravity-cli/brain/<conv-id>/.system_generated/logs/transcript_full.jsonl

AFTER the Stop hook fires (sometimes seconds later, depending on session shutdown timing). Our TurnEnd event maps to Stop, so we routinely race the transcript write. Without PrepareTranscript, the framework's fileExists check fails with "transcript file not found" and our hook returns exit 1, terminating agy's agent turn.

We briefly wait for the real transcript first. If it is still missing, we materialise an empty placeholder. files_touched is already captured via the PreToolUse hook (independent of transcript content), so condensation can still produce a meaningful checkpoint from an empty transcript.

The placeholder is a WRITE on a hook-supplied path, so it is created only inside agy's brain directory (the agent's SessionStore): parents with MkdirAllNoSymlink, the file with O_EXCL through the root, and a path outside that directory refused rather than anchored on its own parent (docs/development/filesystem-safety.md, "The Root Anchors"). Tests place transcripts under ENTIRE_TEST_ANTIGRAVITY_BRAIN_DIR for the same reason.

func (*AntigravityAgent) ProtectedDirs

func (a *AntigravityAgent) ProtectedDirs() []string

func (*AntigravityAgent) ReadSession

func (a *AntigravityAgent) ReadSession(_ *agent.HookInput) (*agent.AgentSession, error)

func (*AntigravityAgent) ReadTranscript

func (a *AntigravityAgent) ReadTranscript(sessionRef string) ([]byte, error)

ReadTranscript reads a transcript agy's hook payload named. When the path is inside agy's brain directory — where every real payload points — the read goes through the agent's SessionStore: a name inside a trusted root, no symlink followed at any component. A path outside it is the read-side gap docs/development/filesystem-safety.md describes (closing it needs RepoPath on HookInput), and it stays the one unconfined read the ratchet in agent/transcript_read_guard_test.go allows this file; it is never anchored on the path's own parent, which would contain nothing while looking like it did.

func (*AntigravityAgent) ReassembleTranscript

func (a *AntigravityAgent) ReassembleTranscript(chunks [][]byte) ([]byte, error)

func (*AntigravityAgent) ResolveSessionFile

func (a *AntigravityAgent) ResolveSessionFile(sessionDir, agentSessionID string) string

func (*AntigravityAgent) SliceTranscriptFromPosition

func (a *AntigravityAgent) SliceTranscriptFromPosition(content []byte, startOffset int) []byte

SliceTranscriptFromPosition implements agent.LateTranscriptWriter. It scopes content the same way CountTranscriptPosition counts it, through the one iterator that owns the metric. transcript.SliceFromLine cannot stand in: it counts raw \n-delimited lines, so a single interior blank line puts the summary's window a line off from the offset that was stored for it.

func (*AntigravityAgent) SnapshotTokenBaseline

func (a *AntigravityAgent) SnapshotTokenBaseline(ctx context.Context, sessionID string) (json.RawMessage, error)

SnapshotTokenBaseline returns the latest persisted snapshot for the conversation, or nil if none exists yet. A nil baseline is exact only for a genuinely fresh conversation; a resumed conversation whose title-tee shim hasn't written a snapshot before the first TurnStart will over-count the prior cumulative total on that first tracked turn.

func (*AntigravityAgent) Type

func (a *AntigravityAgent) Type() types.AgentType

func (*AntigravityAgent) UninstallHooks

func (a *AntigravityAgent) UninstallHooks(ctx context.Context) error

UninstallHooks removes the Entire hook entry from .agents/hooks.json.

func (*AntigravityAgent) WriteSession

func (a *AntigravityAgent) WriteSession(_ context.Context, session *agent.AgentSession) error

type CommonPayload

type CommonPayload struct {
	ConversationID string `json:"conversationId"`
	TranscriptPath string `json:"transcriptPath"`
}

CommonPayload contains the system metadata fields the integration consumes from every hook payload.

type CondensedStep

type CondensedStep struct {
	// Role is one of the CondensedRole* constants.
	Role string
	// Text is the user request (unwrapped from <USER_REQUEST>) or the
	// assistant's text; empty for tool steps.
	Text string
	// ToolName and ToolArgs describe a tool call; ToolArgs values are decoded
	// from agy's double-encoded JSON strings where possible.
	ToolName string
	ToolArgs map[string]any
}

CondensedStep is one summarizable unit of an agy transcript. It exists for the summarizer (cmd/entire/cli/summarize), which owns the prompt shape but not agy's wire format: without it, agy transcripts fell through to the Claude JSONL parser, condensed to nothing, and `explain --generate` reported "transcript has no content to summarize" for a 2.4 KB transcript.

func CondenseTranscript

func CondenseTranscript(content []byte) []CondensedStep

CondenseTranscript reduces agy step JSONL to user requests, assistant text and tool calls, in order. GENERIC steps (tool output) and SYSTEM_MESSAGE steps (agy's own injected notices) are skipped; malformed lines are skipped.

type HookCommand

type HookCommand struct {
	Type    string `json:"type,omitempty"`
	Command string `json:"command"`
	Timeout int    `json:"timeout,omitempty"`
}

HookCommand is a single executable hook command.

type HookConfig

type HookConfig struct {
	Enabled        *bool           `json:"enabled,omitempty"`
	PreToolUse     []ToolHandler   `json:"PreToolUse,omitempty"`
	PostToolUse    []ToolHandler   `json:"PostToolUse,omitempty"`
	PreInvocation  []SimpleHandler `json:"PreInvocation,omitempty"`
	PostInvocation []SimpleHandler `json:"PostInvocation,omitempty"`
	Stop           []SimpleHandler `json:"Stop,omitempty"`
}

HookConfig defines the event handlers for a named hook entry. It mirrors agy's hooks.json schema (https://antigravity.google/docs — Hooks), including event keys we don't install (PostToolUse/PostInvocation) so round-tripping a user's file never drops data and the install idempotency comparison detects stale Entire entries that still carry them.

type HooksFile

type HooksFile = map[string]HookConfig

HooksFile maps hook names to their event configurations. The top-level key is the hook name (user-defined, e.g. "my-linter-hook").

type HooksProbe

type HooksProbe struct {
	// Version is the agy version string reported by `agy --version`.
	Version string
	// Loaded is true when agy lists an Entire hook entry sourced from the
	// workspace's .agents/hooks.json.
	Loaded bool
	// Sources are the hooks.json paths agy reported, for diagnostics.
	Sources []string
}

HooksProbe is the outcome of asking agy which hooks it actually loads for a workspace. Loaded distinguishes "agy sees Entire's entry" from "the file is on disk but agy ignores it" — the latter is the untrusted-workspace trap (agy resolves an untrusted cwd to its scratch workspace, so no hooks fire).

func ProbeLoadedHooks

func ProbeLoadedHooks(ctx context.Context, repoRoot string) (HooksProbe, error)

ProbeLoadedHooks asks the agy binary on PATH which hooks it loads for repoRoot, via `agy -p /hooks --add-dir <repoRoot> --output-format json`. --add-dir is what makes agy treat repoRoot as the workspace (the same flag the e2e harness relies on); without it the probe would answer for agy's scratch workspace and always report nothing loaded.

Zero-quota by construction: the version gate guarantees agy answers /hooks locally. Returns ErrHooksProbeUnsupported for older agy, and any spawn or parse failure otherwise (an unauthenticated agy fails print mode with "authentication required", which surfaces here as an error).

type InvocationPayload

type InvocationPayload struct {
	CommonPayload

	InvocationNum int `json:"invocationNum"`
}

InvocationPayload is the stdin payload for the PreInvocation hook.

invocationNum is 0-indexed (the first model invocation of a conversation is 0). initialNumSteps is deliberately not decoded: agy inserts the user prompt as a step before the first model call, so it is already 1 on the first invocation and unusable as a "first?" signal.

type PreToolUsePayload

type PreToolUsePayload struct {
	CommonPayload

	ToolCall ToolCall `json:"toolCall"`
}

PreToolUsePayload is the stdin payload for the PreToolUse hook.

type SimpleHandler

type SimpleHandler struct {
	Type    string `json:"type,omitempty"`
	Command string `json:"command"`
	Timeout int    `json:"timeout,omitempty"`
}

SimpleHandler is a direct handler entry used for PreInvocation, PostInvocation, and Stop.

type StopPayload

type StopPayload struct {
	CommonPayload

	FullyIdle bool `json:"fullyIdle"` // Required
}

StopPayload is the stdin payload for the Stop hook.

type ToolCall

type ToolCall struct {
	Name string          `json:"name"`
	Args json.RawMessage `json:"args"`
}

ToolCall represents a proposed or completed tool invocation.

type ToolHandler

type ToolHandler struct {
	Matcher string        `json:"matcher,omitempty"`
	Hooks   []HookCommand `json:"hooks,omitempty"`
}

ToolHandler is a matcher + handlers entry used for PreToolUse / PostToolUse.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL