From b480e5565d8cd9d46d6ba9f0c767156b66cde7f0 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Thu, 3 Sep 2026 02:42:40 +0800 Subject: [PATCH 1/4] Enable Antigravity tracing plugin during setup --- bt-daemon/src/setup.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bt-daemon/src/setup.rs b/bt-daemon/src/setup.rs index 681e6f3..90de93c 100644 --- a/bt-daemon/src/setup.rs +++ b/bt-daemon/src/setup.rs @@ -439,6 +439,11 @@ fn setup_antigravity_at(runner: &mut impl CommandRunner, config_dir: &Path) -> a &["plugin", "install", ANTIGRAVITY_PLUGIN_SOURCE], antigravity_home(config_dir)?, )?; + runner.run_in_home( + "agy", + &["plugin", "enable", ANTIGRAVITY_PLUGIN], + antigravity_home(config_dir)?, + )?; remove_legacy_antigravity_registration(config_dir) } @@ -852,6 +857,7 @@ mod tests { setup_antigravity_at(&mut runner, &config_dir).unwrap(); assert!(runner.called(&format!("agy plugin install {ANTIGRAVITY_PLUGIN_SOURCE}"))); + assert!(runner.called(&format!("agy plugin enable {ANTIGRAVITY_PLUGIN}"))); let hooks: Value = serde_json::from_slice(&std::fs::read(&hooks_path).unwrap()).unwrap(); assert_eq!(hooks["other-plugin"]["Stop"][0]["command"], "other"); assert!(hooks.get(ANTIGRAVITY_PLUGIN).is_none()); @@ -903,6 +909,7 @@ mod tests { assert!(!config_dir.join("hooks.json").exists()); assert!(runner.called(&format!("agy plugin install {ANTIGRAVITY_PLUGIN_SOURCE}"))); + assert!(runner.called(&format!("agy plugin enable {ANTIGRAVITY_PLUGIN}"))); } #[test] From ba5b04509b2daf4e23dff9be0726c3c310e7d65e Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Thu, 3 Sep 2026 04:05:22 +0800 Subject: [PATCH 2/4] Add Antigravity transcript import support --- bt-daemon/README.md | 4 +- bt-daemon/src/lib.rs | 2 + bt-daemon/src/trace_command.rs | 2 +- .../src/transcript_import/antigravity.rs | 162 ++++++++++++++++++ bt-daemon/src/transcript_import/mod.rs | 85 +++++++++ bt-daemon/tests/replay.rs | 32 ++++ src/plugins/antigravity/content/README.md | 7 +- 7 files changed, 289 insertions(+), 5 deletions(-) create mode 100644 bt-daemon/src/transcript_import/antigravity.rs diff --git a/bt-daemon/README.md b/bt-daemon/README.md index e5feabb..faa5411 100644 --- a/bt-daemon/README.md +++ b/bt-daemon/README.md @@ -115,14 +115,14 @@ echo '{"session_id":"s1","hook_event_name":"Stop"}' | ./target/debug/bt- The first `hook` spawns the daemon detached; it idles out after 5 minutes. -`import ` has a different purpose from restart +`import ` has a different purpose from restart recovery. It locates the native transcript in the selected agent's standard session store, synthesizes the lifecycle triggers that can be recovered from that transcript, and sends them through the normal translator and sink to create a trace for the past session. Hook-only facts absent from a native transcript are not invented. -Add `--attach` to keep following an active Codex or Claude transcript until +Add `--attach` to keep following an active Codex, Claude, or Antigravity transcript until Ctrl-C. `run [ARGS...]` launches the selected agent with inherited stdio and injects live Braintrust hooks for that invocation, so it does not depend on the tracing plugin being installed or enabled. Managed runs diff --git a/bt-daemon/src/lib.rs b/bt-daemon/src/lib.rs index e607980..854d105 100644 --- a/bt-daemon/src/lib.rs +++ b/bt-daemon/src/lib.rs @@ -220,6 +220,8 @@ pub enum ImportSource { Codex, #[value(name = "claude", alias = "claude-code")] Claude, + #[value(name = "antigravity", alias = "agy")] + Antigravity, } #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] diff --git a/bt-daemon/src/trace_command.rs b/bt-daemon/src/trace_command.rs index f0ae928..234aa03 100644 --- a/bt-daemon/src/trace_command.rs +++ b/bt-daemon/src/trace_command.rs @@ -38,7 +38,7 @@ pub enum TraceCommand { /// Gracefully stop the tracing daemon. #[command(hide = true)] Stop(StopArgs), - /// Import a past Codex or Claude Code session by its resume id. + /// Import a past coding-agent session by its resume id. Import(ImportArgs), /// Launch a coding agent with tracing enabled for this invocation. Run(RunArgs), diff --git a/bt-daemon/src/transcript_import/antigravity.rs b/bt-daemon/src/transcript_import/antigravity.rs new file mode 100644 index 0000000..05a862a --- /dev/null +++ b/bt-daemon/src/transcript_import/antigravity.rs @@ -0,0 +1,162 @@ +use super::{envelope, validate_session_id}; +use crate::wire::Envelope; +use anyhow::bail; +use serde_json::{json, Value}; +use std::collections::HashSet; +use std::path::{Path, PathBuf}; + +const TRANSCRIPT_NAME: &str = "transcript_full.jsonl"; + +#[derive(Default)] +pub(super) struct Tail { + started: bool, + reported_tools: HashSet, + last_len: u64, +} + +impl Tail { + pub(super) fn poll( + &mut self, + events: Vec, + len: u64, + finalize: bool, + ) -> anyhow::Result> { + if events.len() < 3 { + bail!("Antigravity import did not produce session boundary events"); + } + let mut out = Vec::new(); + if !self.started { + out.push(events[0].clone()); + self.started = true; + } else if len != self.last_len { + let mut checkpoint = events[0].clone(); + checkpoint.event = "ImportCheckpoint".into(); + out.push(checkpoint); + } + let end = events.len() - 2; + for event in &events[1..end] { + let step = event.payload.get("stepIdx").and_then(Value::as_i64); + if step.is_some_and(|step| self.reported_tools.insert(step)) { + out.push(event.clone()); + } + } + if finalize { + out.extend_from_slice(&events[end..]); + } + self.last_len = len; + Ok(out) + } +} + +pub(super) fn roots(home: &Path) -> Vec { + vec![home.join(".gemini/antigravity-cli/brain")] +} + +pub(super) fn transcript_session_id(path: &Path) -> Option { + if path.file_name().and_then(|name| name.to_str()) != Some(TRANSCRIPT_NAME) { + return None; + } + let session_id = path + .parent()? // logs + .parent()? // .system_generated + .parent()? // conversation directory + .file_name()? + .to_str()?; + validate_session_id(session_id).ok()?; + is_transcript_path(path, session_id).then(|| session_id.to_owned()) +} + +pub(super) fn filename_matches(path: &Path, session_id: &str) -> bool { + validate_session_id(session_id).is_ok() && is_transcript_path(path, session_id) +} + +fn is_transcript_path(path: &Path, session_id: &str) -> bool { + path.file_name().and_then(|name| name.to_str()) == Some(TRANSCRIPT_NAME) + && path + .parent() + .and_then(Path::parent) + .and_then(Path::parent) + .and_then(Path::file_name) + .and_then(|name| name.to_str()) + == Some(session_id) +} + +pub(super) fn envelopes(path: &Path, records: &[Value]) -> anyhow::Result> { + let session_id = transcript_session_id(path) + .ok_or_else(|| anyhow::anyhow!("invalid Antigravity transcript path {}", path.display()))?; + if records.is_empty() { + bail!("Antigravity transcript {} is empty", path.display()); + } + let (start_ms, end_ms) = timestamp_bounds_created(records); + let transcript_path = path.to_string_lossy(); + let mut events = vec![envelope( + "antigravity", + None, + &session_id, + "PreInvocation", + start_ms, + json!({"conversationId": session_id, "invocationNum": 0, "initialNumSteps": 0, + "transcriptPath": transcript_path}), + )]; + for record in records.iter().filter(|record| is_tool_result(record)) { + let Some(step) = record + .get("step_index") + .or_else(|| record.get("stepIndex")) + .and_then(Value::as_i64) + else { + continue; + }; + events.push(envelope( + "antigravity", None, &session_id, "PostToolUse", created_at_ms(record).unwrap_or(end_ms), + json!({"conversationId": session_id, "stepIdx": step, "transcriptPath": transcript_path}), + )); + } + events.push(envelope( + "antigravity", + None, + &session_id, + "PostInvocation", + end_ms, + json!({"conversationId": session_id, "invocationNum": 0, "initialNumSteps": 0, + "transcriptPath": transcript_path}), + )); + events.push(envelope( + "antigravity", None, &session_id, "Stop", end_ms.saturating_add(1), + json!({"conversationId": session_id, "fullyIdle": true, "terminationReason": "transcript_import", + "transcriptPath": transcript_path}), + )); + Ok(events) +} + +fn is_tool_result(record: &Value) -> bool { + !matches!( + record.get("type").and_then(Value::as_str), + Some("USER_INPUT" | "PLANNER_RESPONSE" | "CONVERSATION_HISTORY" | "CHECKPOINT") + ) && record + .get("step_index") + .or_else(|| record.get("stepIndex")) + .is_some() +} + +fn timestamp_bounds_created(records: &[Value]) -> (i64, i64) { + let timestamps = records.iter().filter_map(created_at_ms).collect::>(); + timestamps + .iter() + .copied() + .fold((0, 0), |(min, max), value| { + if min == 0 { + (value, value) + } else { + (min.min(value), max.max(value)) + } + }) +} + +fn created_at_ms(record: &Value) -> Option { + record + .get("created_at") + .or_else(|| record.get("createdAt")) + .and_then(Value::as_str) + .and_then(|value| chrono::DateTime::parse_from_rfc3339(value).ok()) + .map(|value| value.timestamp_millis()) +} diff --git a/bt-daemon/src/transcript_import/mod.rs b/bt-daemon/src/transcript_import/mod.rs index c9c59fd..bb4c391 100644 --- a/bt-daemon/src/transcript_import/mod.rs +++ b/bt-daemon/src/transcript_import/mod.rs @@ -6,6 +6,7 @@ use std::collections::BTreeMap; use std::io::{BufRead, Read, Seek}; use std::path::{Path, PathBuf}; +mod antigravity; mod claude; mod codex; @@ -41,6 +42,7 @@ fn transcript_roots(source: ImportSource) -> Vec { match source { ImportSource::Codex => codex::roots(&home), ImportSource::Claude => claude::roots(&home), + ImportSource::Antigravity => antigravity::roots(&home), } } @@ -123,6 +125,7 @@ fn transcript_session_id(path: &Path, source: ImportSource) -> Option { match source { ImportSource::Codex => codex::transcript_session_id(path), ImportSource::Claude => claude::transcript_session_id(path), + ImportSource::Antigravity => antigravity::transcript_session_id(path), } } @@ -139,6 +142,7 @@ fn resolve_transcript_in( matches.extend(candidates.into_iter().filter(|path| match source { ImportSource::Codex => codex::filename_matches(path, session_id), ImportSource::Claude => claude::filename_matches(path, session_id), + ImportSource::Antigravity => antigravity::filename_matches(path, session_id), })); } matches.sort(); @@ -185,6 +189,7 @@ fn source_name(source: ImportSource) -> &'static str { match source { ImportSource::Codex => "Codex", ImportSource::Claude => "Claude Code", + ImportSource::Antigravity => "Google Antigravity", } } @@ -211,6 +216,7 @@ fn envelopes_from_records( &records.end_offsets, records.read_offset, ), + ImportSource::Antigravity => antigravity::envelopes(path, &records.values), } } @@ -346,6 +352,7 @@ pub(crate) struct TranscriptTail { enum TailState { Codex(codex::Tail), Claude(claude::Tail), + Antigravity(antigravity::Tail), } impl TranscriptTail { @@ -364,6 +371,7 @@ impl TranscriptTail { match source { ImportSource::Codex => TailState::Codex(codex::Tail::default()), ImportSource::Claude => TailState::Claude(claude::Tail::default()), + ImportSource::Antigravity => TailState::Antigravity(antigravity::Tail::default()), } } @@ -395,6 +403,7 @@ impl TranscriptTail { match &mut self.state { TailState::Codex(state) => state.poll(events, len, finalize), TailState::Claude(state) => state.poll(events, len, finalize), + TailState::Antigravity(state) => state.poll(events, len, finalize), } } @@ -505,6 +514,27 @@ mod tests { ); } + #[test] + fn finds_only_exact_antigravity_conversation_directory() { + let temp = tempfile::tempdir().unwrap(); + let root = temp.path().join("brain"); + let transcript = root + .join("conversation-123") + .join(".system_generated/logs/transcript_full.jsonl"); + std::fs::create_dir_all(transcript.parent().unwrap()).unwrap(); + std::fs::write(&transcript, "{}\n").unwrap(); + let unrelated = root + .join("conversation-123-other") + .join(".system_generated/logs/transcript_full.jsonl"); + std::fs::create_dir_all(unrelated.parent().unwrap()).unwrap(); + std::fs::write(unrelated, "{}\n").unwrap(); + + assert_eq!( + resolve_transcript_in("conversation-123", ImportSource::Antigravity, &[root]).unwrap(), + transcript + ); + } + #[test] fn rejects_unsafe_session_ids() { let error = resolve_transcript_in( @@ -749,6 +779,61 @@ mod tests { assert_eq!(tail.poll(true).unwrap().last().unwrap().event, "Stop"); } + #[test] + fn antigravity_tail_keeps_session_open_and_reports_new_records_once() { + let temp = tempfile::tempdir().unwrap(); + let path = temp + .path() + .join("conversation-123/.system_generated/logs/transcript_full.jsonl"); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + let mut records = vec![ + json!({"step_index":0,"source":"USER_EXPLICIT","type":"USER_INPUT","created_at":"2026-01-01T00:00:01Z","content":"one"}), + json!({"step_index":1,"source":"MODEL","type":"PLANNER_RESPONSE","created_at":"2026-01-01T00:00:02Z","content":"answer"}), + ]; + let write = |records: &[Value]| { + std::fs::write( + &path, + records + .iter() + .map(Value::to_string) + .collect::>() + .join("\n") + + "\n", + ) + .unwrap() + }; + write(&records); + let mut tail = TranscriptTail::new(path.clone(), ImportSource::Antigravity); + assert_eq!( + tail.poll(false) + .unwrap() + .iter() + .map(|event| event.event.as_str()) + .collect::>(), + vec!["PreInvocation"] + ); + + records.push(json!({"step_index":2,"source":"MODEL","type":"LIST_DIRECTORY","created_at":"2026-01-01T00:00:03Z","content":"result"})); + write(&records); + assert_eq!( + tail.poll(false) + .unwrap() + .iter() + .map(|event| event.event.as_str()) + .collect::>(), + vec!["ImportCheckpoint", "PostToolUse"] + ); + assert!(tail.poll(false).unwrap().is_empty()); + assert_eq!( + tail.poll(true) + .unwrap() + .iter() + .map(|event| event.event.as_str()) + .collect::>(), + vec!["PostInvocation", "Stop"] + ); + } + #[test] fn incremental_reader_handles_partial_appends_and_replacement() { let temp = tempfile::tempdir().unwrap(); diff --git a/bt-daemon/tests/replay.rs b/bt-daemon/tests/replay.rs index c286a58..533a098 100644 --- a/bt-daemon/tests/replay.rs +++ b/bt-daemon/tests/replay.rs @@ -48,6 +48,38 @@ async fn importing_large_codex_rollout_drains_translator_continuations() { assert_eq!(inserted(&output_rows, "llm"), CALLS); } +#[tokio::test] +async fn importing_antigravity_transcript_uses_the_production_translator() { + let tmp = tempfile::tempdir().unwrap(); + let transcript = tmp + .path() + .join("brain/antigravity-import/.system_generated/logs/transcript_full.jsonl"); + std::fs::create_dir_all(transcript.parent().unwrap()).unwrap(); + write_jsonl( + &transcript, + &[ + json!({"step_index":0,"source":"USER_EXPLICIT","type":"USER_INPUT","status":"DONE","created_at":"2026-01-01T00:00:01Z","content":"\ntrace this\n"}), + json!({"step_index":1,"source":"MODEL","type":"PLANNER_RESPONSE","status":"DONE","created_at":"2026-01-01T00:00:02Z","content":"done"}), + ], + ); + let output = tmp.path().join("spans"); + import_transcript( + &transcript, + ImportSource::Antigravity, + options(&output), + None, + false, + ) + .await + .unwrap(); + let output_rows = rows(&output.join("antigravity-import.ndjson")); + assert_eq!(inserted(&output_rows, "task"), 2); + assert_eq!(inserted(&output_rows, "llm"), 1); + assert!(output_rows + .iter() + .any(|row| { row.pointer("/Insert/input").and_then(Value::as_str) == Some("trace this") })); +} + fn options(output: &std::path::Path) -> ServeOptions { ServeOptions { version: "test".into(), diff --git a/src/plugins/antigravity/content/README.md b/src/plugins/antigravity/content/README.md index 3f91265..b75816d 100644 --- a/src/plugins/antigravity/content/README.md +++ b/src/plugins/antigravity/content/README.md @@ -26,5 +26,8 @@ agy plugin install https://github.com/braintrustdata/braintrust-antigravity-plug ``` The `bt trace enable` command remains the recommended entrypoint because it also -persists the Braintrust destination. Managed-run injection, transcript -import/attach, and Windows support are not currently provided. +persists the Braintrust destination. Existing conversations can be replayed +with `bt trace import antigravity ` or followed live with +`--attach`; both use Antigravity's durable `transcript_full.jsonl` and the same +daemon translator as hooks. Managed-run injection and Windows support are not +currently provided. From a197ea5de5ec27b2ff8787766c787be56a186ae0 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Thu, 3 Sep 2026 04:09:43 +0800 Subject: [PATCH 3/4] Preserve Antigravity replayed tool outcomes --- .../src/transcript_import/antigravity.rs | 41 +++++++++++++++++-- bt-daemon/src/translate/antigravity.rs | 10 ++++- bt-daemon/tests/replay.rs | 12 +++++- 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/bt-daemon/src/transcript_import/antigravity.rs b/bt-daemon/src/transcript_import/antigravity.rs index 05a862a..e37cd21 100644 --- a/bt-daemon/src/transcript_import/antigravity.rs +++ b/bt-daemon/src/transcript_import/antigravity.rs @@ -2,7 +2,7 @@ use super::{envelope, validate_session_id}; use crate::wire::Envelope; use anyhow::bail; use serde_json::{json, Value}; -use std::collections::HashSet; +use std::collections::{HashSet, VecDeque}; use std::path::{Path, PathBuf}; const TRANSCRIPT_NAME: &str = "transcript_full.jsonl"; @@ -98,7 +98,18 @@ pub(super) fn envelopes(path: &Path, records: &[Value]) -> anyhow::Result anyhow::Result anyhow::Result bool { + record + .get("content") + .and_then(Value::as_str) + .is_some_and(|content| content.to_ascii_lowercase().contains("denied")) +} + fn is_tool_result(record: &Value) -> bool { !matches!( record.get("type").and_then(Value::as_str), diff --git a/bt-daemon/src/translate/antigravity.rs b/bt-daemon/src/translate/antigravity.rs index 41275eb..2b330c4 100644 --- a/bt-daemon/src/translate/antigravity.rs +++ b/bt-daemon/src/translate/antigravity.rs @@ -407,7 +407,7 @@ impl AntigravityTranslator { json!({ "step_index": step, }), - Some(ToolApproval::Approved), + tool_approval_from_event(event).or(Some(ToolApproval::Approved)), )), error, ..Default::default() @@ -530,6 +530,14 @@ impl AgentTranslator for AntigravityTranslator { } } +fn tool_approval_from_event(event: &Envelope) -> Option { + match string_field(&event.payload, "toolApproval").as_deref() { + Some("approved") => Some(ToolApproval::Approved), + Some("denied") => Some(ToolApproval::Denied), + _ => None, + } +} + struct TranscriptSource<'a> { path: String, through: u64, diff --git a/bt-daemon/tests/replay.rs b/bt-daemon/tests/replay.rs index 533a098..2fa3b48 100644 --- a/bt-daemon/tests/replay.rs +++ b/bt-daemon/tests/replay.rs @@ -59,7 +59,9 @@ async fn importing_antigravity_transcript_uses_the_production_translator() { &transcript, &[ json!({"step_index":0,"source":"USER_EXPLICIT","type":"USER_INPUT","status":"DONE","created_at":"2026-01-01T00:00:01Z","content":"\ntrace this\n"}), - json!({"step_index":1,"source":"MODEL","type":"PLANNER_RESPONSE","status":"DONE","created_at":"2026-01-01T00:00:02Z","content":"done"}), + json!({"step_index":1,"source":"MODEL","type":"PLANNER_RESPONSE","status":"DONE","created_at":"2026-01-01T00:00:02Z","tool_calls":[{"name":"list_dir","args":{"DirectoryPath":"/tmp"}}]}), + json!({"step_index":2,"source":"MODEL","type":"ERROR_MESSAGE","status":"DONE","created_at":"2026-01-01T00:00:03Z","content":"tool call denied"}), + json!({"step_index":3,"source":"MODEL","type":"PLANNER_RESPONSE","status":"DONE","created_at":"2026-01-01T00:00:04Z","content":"done"}), ], ); let output = tmp.path().join("spans"); @@ -78,6 +80,14 @@ async fn importing_antigravity_transcript_uses_the_production_translator() { assert!(output_rows .iter() .any(|row| { row.pointer("/Insert/input").and_then(Value::as_str) == Some("trace this") })); + assert!(output_rows + .iter() + .any(|row| { row.pointer("/Insert/name").and_then(Value::as_str) == Some("list_dir") })); + assert!(output_rows.iter().any(|row| { + row.pointer("/Merge/metadata/tool_approval") + .and_then(Value::as_str) + == Some("denied") + })); } fn options(output: &std::path::Path) -> ServeOptions { From 0625c1dba4cf71a15734cd007811d35f8026c83e Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Fri, 4 Sep 2026 00:28:03 +0800 Subject: [PATCH 4/4] Simplify Antigravity plugin README --- src/plugins/antigravity/content/README.md | 56 ++++++++++++++--------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/src/plugins/antigravity/content/README.md b/src/plugins/antigravity/content/README.md index b75816d..af37bb5 100644 --- a/src/plugins/antigravity/content/README.md +++ b/src/plugins/antigravity/content/README.md @@ -1,33 +1,45 @@ # Braintrust tracing for Google Antigravity -This Antigravity plugin forwards native lifecycle hooks to the Braintrust -daemon. The daemon combines exact model and tool boundaries from hooks with -the conversation's full JSONL transcript to construct a session, turn, model, -and tool span tree. +Capture your Google Antigravity sessions as Braintrust traces, including +prompts, model responses, and tool activity. -The hook adapter is synchronous, credential-free, and fail-open. Braintrust -authentication and destination routing remain owned by the `bt` CLI. +## Set up tracing -The initial implementation captures `PreInvocation`, `PostInvocation`, -`PostToolUse`, and `Stop`. It intentionally does not register `PreToolUse`: -Antigravity requires that hook to return a permission decision. Live testing -confirmed that an empty decision is handled as a denial, while `allow` would -bypass normal permission checks and `ask` could add prompts. +Use the Braintrust CLI to install the plugin and choose where traces are sent: -This package requires a `bt` CLI that exposes `bt trace hook` and a -Unix-compatible `sh`. Install or refresh the published plugin and configure its -Braintrust route with `bt trace enable antigravity` (`setup` remains an alias); -remove its managed registration with `bt trace disable antigravity`. +```bash +bt trace enable antigravity +``` + +You can disable it later with: + +```bash +bt trace disable antigravity +``` -The published plugin can also be inspected or installed directly with: +The plugin requires the `bt` CLI and a Unix-compatible `sh`. You can also +install it directly with: ```bash agy plugin install https://github.com/braintrustdata/braintrust-antigravity-plugin ``` -The `bt trace enable` command remains the recommended entrypoint because it also -persists the Braintrust destination. Existing conversations can be replayed -with `bt trace import antigravity ` or followed live with -`--attach`; both use Antigravity's durable `transcript_full.jsonl` and the same -daemon translator as hooks. Managed-run injection and Windows support are not -currently provided. +However, `bt trace enable antigravity` is recommended because it also saves +your Braintrust destination. + +## Import an existing conversation + +Replay a previous Antigravity conversation by its conversation ID: + +```bash +bt trace import antigravity +``` + +To continue reporting a conversation while it is active, add `--attach`: + +```bash +bt trace import antigravity --attach +``` + +Managed `bt trace run antigravity` support and Windows support are not yet +available.