Fix Git Migration Ref and Metadata Handling · Entire
Review of PR #1287 — feat(agent): add Antigravity (agy) CLI agent
This PR adds first-class (preview) support for Google's Antigravity CLI (agy) as an Entire agent: lifecycle hooks, JSONL transcript analysis, an out-of-band token pipeline (a global title-slot "tee" shim, since agy exposes tokens only via its title/statusline pipe), review-skill discovery, resume tracking, plus docs and E2E wiring. It's ~7.8k lines (≈60% tests) and unusually well-defended — many suspected regressions I chased were refuted: filterToUncommittedFiles scope is unchanged from main, the token-accumulation paths are provably mutually exclusive (no double-count), the claude-code discovery rewiring onto the shared scanner is behavior-identical, and the resolveAgySymlinks/decodeAgyString path shapes match across the two call sites.
The findings below survived verification, most-severe first.
Correctness
1. cmd/entire/cli/lifecycle.go:~1379 (shouldSuppressConditionalTurnStart) — a resumed turn is silently dropped for up to an hour after an agy crash. (CONFIRMED)
The guard is SuppressIfSessionActive && state.Phase.IsActive() && !state.IsStuckActive(). IsStuckActive() is purely the 1h StuckActiveThreshold timer — but the codebase has a purpose-built immediate crash detector, state.OwnerExited() (owner PID liveness), and captureSessionOwner does record the owner on agy's turn start. Failure: agy crashes/is killed mid-turn (Stop never fires → session stuck ACTIVE with a dead owner); the user runs agy --conversation <id> within the hour; its first PreInvocation has invocationNum>0 → SuppressIfSessionActive=true; the session is still ACTIVE and <1h so the resumed TurnStart is dropped. handleLifecycleTurnStart never runs — no fresh token baseline, no untracked-file snapshot — so TurnEnd computes the delta/attribution against the stale crashed-turn state, over-counting tokens and misattributing files. This is exactly the case the code comment says "must NOT suppress," just inside the 1h window. Fix: also fire when state.OwnerExited().
2. cmd/entire/cli/agent/antigravity/hooks.go:210 (writeJSONMapFile) — agy's machine-global settings.json is written non-atomically. (CONFIRMED)
It uses os.WriteFile, while jsonutil.WriteFileAtomic (temp-file + rename, used in 7 config sites for exactly this reason) exists. Failure: a crash/SIGKILL mid-write truncates `/.gemini/antigravity-cli/settings.json. That file's titleslot is shared across **every repo on the machine** (per the code's own comments), so one interrupted write corrupts token tracking everywhere and can leave agy spawning a broken title command on each state change. Route the final write throughjsonutil.WriteFileAtomic`.
3. cmd/entire/cli/strategy/manual_commit_condensation.go:~201 (skip gate) — a dangling Entire-Checkpoint trailer on an agy mid-turn commit. (PLAUSIBLE — independently flagged by two angles)
For agy, an empty live transcript now degrades instead of erroring. If FilesTouched is also empty, the skip gate (len(Transcript)==0 && len(FilesTouched)==0 → Skipped) fires before filterFilesTouched can adopt the committed files. Failure: first agy turn edits a file via a tool other than the three recognized ones (write_to_file/replace_file_content/multi_replace_file_content) — e.g. a shell command — so PreToolUse records nothing; agy commits mid-turn; prepare-commit-msg stamps the trailer; condensation reads the not-yet-flushed (empty) transcript, degrades, resolves 0 files, and returns Skipped. The commit permanently carries a trailer for a checkpoint that was never written; entire explain/rewind find nothing. Other agents error→retry here, so this is agy-specific.
4. cmd/entire/cli/strategy/manual_commit_condensation.go:~1327 + late-flush recovery — mid-turn-commit checkpoints may record the previous turn's prompt. (PLAUSIBLE)
Because agy writes its transcript after Stop, a mid-turn commit condenses an empty transcript, so CheckpointTranscriptStart is stored as the count of only previously-flushed turns. resolvePromptsFromLateFlushedTranscript(offset=CheckpointTranscriptStart) on the next condensation then reads the now-populated transcript from that lagging offset. Failure: in a multi-turn interactive agy session that commits each turn mid-flight, each checkpoint's recovered prompt is shifted by ~one turn, so entire explain/metadata attributes the wrong user prompt. Worth validating against the 385 captured transcripts with a multi-turn mid-commit sequence — the PR documents that prompt recovery is deferred, but not that it can be misattributed.
Conventions / test isolation
5. cmd/entire/cli/agent/antigravity/statusline.go:66 (statusDir) — bypasses the mandated cache-path resolver. (CONFIRMED — CLAUDE.md rule)
Repo CLAUDE.md, "Config/Cache/Keyring Isolation": "internal/entireclient/userdirs is the only place that resolves … the cache dir (userdirs.Cache()). Never derive these paths anywhere else." statusDir calls os.UserCacheDir() directly. Consequences: (a) os.UserCacheDir() ignores $XDG_CACHE_HOME on macOS, so the harness-wide XDG_CACHE_HOME isolation the integration TestMain sets does not redirect it on darwin; (b) it skips the userdirs go-test throwaway fallback, so any in-process test reaching it without setting ENTIRE_ANTIGRAVITY_STATUS_DIR writes to the developer's real ~/Library/Caches/entire. Route through userdirs.Cache().
Altitude
6. cmd/entire/cli/strategy/manual_commit_condensation.go:997, 1090, 509 — three AgentType == Antigravity special-cases threaded into shared condensation code. (design)
Empty-transcript degrade, non-blank line counting, and the JSONL slice case all switch on the concrete agent type, whereas this PR itself models the same kind of trait cleanly as capability interfaces (OutOfBandTokenSource, TranscriptPreparer). Cost: the next agent whose transcript lands after Stop must be hand-added to the != Antigravity list or it silently hits errors.New("live transcript is empty") after the trailer is stamped (finding #3's mechanism); the non-blank counter duplicates forEachNonBlankLine across a package boundary that the code comments admit must stay byte-identical; and external/plugin agents can never opt in. A LateTranscript/content-based-position capability would keep the strategy agent-agnostic.
Efficiency
7. cmd/entire/cli/agent/antigravity/statusline.go:235 (readLastStatusSnapshot) — full front-to-back scan on the hottest path. (CONFIRMED)
agy fires the title command on every state change; each call scans the entire growing JSONL just to fetch the last line (both the dedup compare and every-TurnStart SnapshotTokenBaseline). That's O(n) per fire → O(n²) over a conversation, paid even on the dedup-skip path. A bounded tail-read (Seek from EOF, take bytes after the last \n) is O(1). (Related minor: os.MkdirAll runs on every fire though the dir exists after the first — gate on isNew.)
Cross-platform
8. cmd/entire/cli/hooks_antigravity_title.go:50 — --wrap runs the user's title command via sh -c, silently breaking on Windows. (PLAUSIBLE — contingent on agy Windows support)exec.CommandContext(ctx, "sh", "-c", wrap) with the error deliberately swallowed (_ = wrapped.Run()). Failure: a Windows user who had a custom title command loses it silently — no sh on PATH, Run() errors, nothing surfaces (token capture still works, so nothing signals the breakage).
Also noted, not ranked (lower-value or documented): shellSingleQuote is a 5th byte-identical copy of shellQuote (strategy/hooks.go, resume_picker.go, e2e/tmux.go) — extract one exported helper; AppendStatusSnapshot's full-context_window dedup can drop a distinct pure-cache-read snapshot, undercounting CacheRead/APICallCount (the code already calls this out as best-effort); PrepareTranscript's re-stat before the O_EXCL create is redundant (the IsExist handling already covers the race); and HookConfig.PostToolUse/PostInvocation are dead fields for a first release (no prior version could have written them).
Net: solid, carefully-built integration. #1 and #2 are the ones I'd want addressed before merge even for a preview; #3–#5 are narrow but real; the rest are polish.