Replace clap with figue and move transport creds to env vars
- quire-ci now reads QUIRE__RUN_ID, QUIRE__SERVER_URL, QUIRE__AUTH_TOKEN,
  QUIRE__TRANSPORT, and QUIRE__SENTRY_DSN from the environment instead
  of positional CLI flags; uses figue for layered config (CLI → env →
  defaults) via #[derive(Facet)] structs
- TransportMode derives Facet so it can be used directly in the config
  struct rather than as a plain String
- quire-server sets those env vars on the subprocess instead of passing
  CLI flags
- SentryHandoff removed; bootstrap carries only sentry_trace_id: Option<String>
  and the DSN travels separately via QUIRE__SENTRY_DSN
- facet promoted to a workspace dependency

https://claude.ai/code/session_01NFGc15XXN1PGGa2hFom7RA
change
commit f36f9b591e21aa1039090d1425cc4261f8327a37
author Claude <noreply@anthropic.com>
date
parent b3f6e09b
diff --git a/Cargo.toml b/Cargo.toml
index 71040d8..0713734 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,6 +4,7 @@ resolver = "3"
 
 [workspace.dependencies]
 clap = { version = "*", features = ["derive", "env"] }
+facet = "*"
 fs-err = "*"
 jiff = { version = "*", features = ["serde"] }
 miette = "*"
diff --git a/quire-ci/Cargo.toml b/quire-ci/Cargo.toml
index 41fcc05..8c4db26 100644
--- a/quire-ci/Cargo.toml
+++ b/quire-ci/Cargo.toml
@@ -4,7 +4,7 @@ version = "0.1.0"
 edition = "2024"
 
 [dependencies]
-facet = "*"
+facet = { workspace = true }
 figue = "*"
 fs-err = { workspace = true }
 jiff = { workspace = true }
diff --git a/quire-ci/src/main.rs b/quire-ci/src/main.rs
index e00e8d5..a2b19b0 100644
--- a/quire-ci/src/main.rs
+++ b/quire-ci/src/main.rs
@@ -9,7 +9,7 @@ use facet::Facet;
 use figue::{self as args, Driver, FigueBuiltins};
 use miette::{IntoDiagnostic, Result};
 use quire_core::api::SecretResponse;
-use quire_core::ci::bootstrap::{Bootstrap, SentryHandoff};
+use quire_core::ci::bootstrap::Bootstrap;
 use quire_core::ci::event::{Event, EventKind, JobOutcome, RunOutcome};
 use quire_core::ci::pipeline::{self, Pipeline, RunFn};
 use quire_core::ci::run::RunMeta;
@@ -280,7 +280,7 @@ fn main() -> Result<()> {
                 }
             };
 
-            let (git_dir, meta, sentry_handoff) = load_bootstrap(&PathBuf::from(&bootstrap))?;
+            let (git_dir, meta, sentry_trace_id) = load_bootstrap(&PathBuf::from(&bootstrap))?;
 
             // Sentry's reqwest transport spawns Tokio tasks for HTTP
             // sends, so the client must be constructed and dropped from
@@ -296,8 +296,11 @@ fn main() -> Result<()> {
 
             // Drop order: `_sentry` flushes first (still inside the
             // runtime), then `_enter`, then `rt`.
-            let trace_id = sentry_handoff.as_ref().map(|h| h.trace_id.as_str());
-            let _sentry = init_sentry(cli.quire.sentry_dsn.as_deref(), trace_id, &meta);
+            let _sentry = init_sentry(
+                cli.quire.sentry_dsn.as_deref(),
+                sentry_trace_id.as_deref(),
+                &meta,
+            );
 
             // No type registrations: quire-ci's user-level errors
             // (CompileError, JobError, FennelError) are no longer logged
@@ -397,7 +400,7 @@ fn validate(workspace: PathBuf) -> Result<()> {
 /// Unlinks the file as soon as the bytes are in memory — getting it off
 /// disk early limits the blast radius of a later panic or crash leaving
 /// a 0600 file behind.
-fn load_bootstrap(path: &std::path::Path) -> Result<(PathBuf, RunMeta, Option<SentryHandoff>)> {
+fn load_bootstrap(path: &std::path::Path) -> Result<(PathBuf, RunMeta, Option<String>)> {
     let bytes = fs_err::read(path).into_diagnostic()?;
     if let Err(e) = fs_err::remove_file(path) {
         // Don't abort — the bytes are already loaded and the server
@@ -409,7 +412,7 @@ fn load_bootstrap(path: &std::path::Path) -> Result<(PathBuf, RunMeta, Option<Se
         );
     }
     let bootstrap: Bootstrap = serde_json::from_slice(&bytes).into_diagnostic()?;
-    Ok((bootstrap.git_dir, bootstrap.meta, bootstrap.sentry))
+    Ok((bootstrap.git_dir, bootstrap.meta, bootstrap.sentry_trace_id))
 }
 
 fn run_pipeline(
@@ -616,17 +619,17 @@ mod tests {
                 pushed_at: jiff::Timestamp::now(),
             },
             git_dir: PathBuf::from("/tmp/repo.git"),
-            sentry: None,
+            sentry_trace_id: None,
         };
         fs_err::write(&path, serde_json::to_vec(&bootstrap).unwrap()).expect("write");
 
-        let (git_dir, meta, sentry) = load_bootstrap(&path).expect("load");
+        let (git_dir, meta, sentry_trace_id) = load_bootstrap(&path).expect("load");
         assert!(
             !path.exists(),
             "bootstrap file should be removed after read"
         );
         assert_eq!(git_dir, PathBuf::from("/tmp/repo.git"));
         assert_eq!(meta.r#ref, "HEAD");
-        assert!(sentry.is_none());
+        assert!(sentry_trace_id.is_none());
     }
 }
diff --git a/quire-core/Cargo.toml b/quire-core/Cargo.toml
index 5d63cdf..9c9763e 100644
--- a/quire-core/Cargo.toml
+++ b/quire-core/Cargo.toml
@@ -4,7 +4,7 @@ version = "0.1.0"
 edition = "2024"
 
 [dependencies]
-facet = "*"
+facet = { workspace = true }
 fs-err = { workspace = true }
 jiff = { workspace = true }
 miette = { workspace = true }
diff --git a/quire-core/src/ci/bootstrap.rs b/quire-core/src/ci/bootstrap.rs
index f8bc818..6e43157 100644
--- a/quire-core/src/ci/bootstrap.rs
+++ b/quire-core/src/ci/bootstrap.rs
@@ -28,20 +28,10 @@ use crate::ci::run::RunMeta;
 pub struct Bootstrap {
     pub meta: RunMeta,
     pub git_dir: PathBuf,
-    /// Sentry handoff, present only when the orchestrator's global
-    /// config sets a DSN. Carries the matching trace id so both
-    /// sides' events land on the same trace.
+    /// Sentry trace id for the orchestrator's span, present only when
+    /// the global config sets a DSN. Allows quire-ci to attach its
+    /// events to the same trace. The DSN itself travels via the
+    /// `QUIRE__SENTRY_DSN` environment variable.
     #[serde(default, skip_serializing_if = "Option::is_none")]
-    pub sentry: Option<SentryHandoff>,
-}
-
-/// What quire-ci needs to mirror the orchestrator's Sentry trace.
-///
-/// `trace_id` is the hex form of [`sentry::protocol::TraceId`]; kept
-/// as a string here so `quire-core` doesn't grow a `sentry` dep.
-/// The DSN itself travels via the `QUIRE__SENTRY_DSN` environment
-/// variable, not this struct.
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct SentryHandoff {
-    pub trace_id: String,
+    pub sentry_trace_id: Option<String>,
 }
diff --git a/quire-server/src/ci/mod.rs b/quire-server/src/ci/mod.rs
index 67249fb..ac8a6a5 100644
--- a/quire-server/src/ci/mod.rs
+++ b/quire-server/src/ci/mod.rs
@@ -185,12 +185,7 @@ fn run_ref(
     span_id: sentry::protocol::SpanId,
 ) {
     let transport = new_transport(ctx.transport_mode, ctx.port);
-    let sentry_handoff =
-        ctx.sentry_dsn
-            .as_ref()
-            .map(|_| quire_core::ci::bootstrap::SentryHandoff {
-                trace_id: trace_id.to_string(),
-            });
+    let sentry_trace_id = ctx.sentry_dsn.as_ref().map(|_| trace_id.to_string());
     sentry::with_scope(
         |scope| {
             scope.set_context(
@@ -209,7 +204,7 @@ fn run_ref(
                 pushed_at,
                 push_ref,
                 &transport,
-                sentry_handoff.as_ref(),
+                sentry_trace_id.as_deref(),
                 ctx.sentry_dsn.as_deref(),
             ) {
                 tracing::error!(
@@ -229,7 +224,7 @@ fn run_ref_inner(
     pushed_at: jiff::Timestamp,
     push_ref: &PushRef,
     transport: &Transport,
-    sentry: Option<&quire_core::ci::bootstrap::SentryHandoff>,
+    sentry_trace_id: Option<&str>,
     sentry_dsn: Option<&str>,
 ) -> error::Result<()> {
     let ci = ctx.repo.ci();
@@ -263,7 +258,7 @@ fn run_ref_inner(
                 &ctx.repo.path(),
                 &workspace,
                 &meta,
-                sentry,
+                sentry_trace_id,
                 sentry_dsn,
                 Some(transport),
             )?;
diff --git a/quire-server/src/ci/run.rs b/quire-server/src/ci/run.rs
index 2857386..9c23569 100644
--- a/quire-server/src/ci/run.rs
+++ b/quire-server/src/ci/run.rs
@@ -306,7 +306,7 @@ impl Run {
         git_dir: &Path,
         workspace: &Path,
         meta: &RunMeta,
-        sentry: Option<&quire_core::ci::bootstrap::SentryHandoff>,
+        sentry_trace_id: Option<&str>,
         sentry_dsn: Option<&str>,
         transport: Option<&Transport>,
     ) -> Result<()> {
@@ -321,7 +321,7 @@ impl Run {
         let log = fs_err::File::create(&log_path)?.into_parts().0;
         let log_clone = log.try_clone()?;
 
-        write_bootstrap(&bootstrap_path, git_dir, meta, sentry)?;
+        write_bootstrap(&bootstrap_path, git_dir, meta, sentry_trace_id)?;
 
         tracing::info!(
             run_id = %self.id,
@@ -625,14 +625,14 @@ fn write_bootstrap(
     path: &Path,
     git_dir: &Path,
     meta: &RunMeta,
-    sentry: Option<&quire_core::ci::bootstrap::SentryHandoff>,
+    sentry_trace_id: Option<&str>,
 ) -> Result<()> {
     use quire_core::ci::bootstrap::Bootstrap;
 
     let bootstrap = Bootstrap {
         meta: meta.clone(),
         git_dir: git_dir.to_path_buf(),
-        sentry: sentry.cloned(),
+        sentry_trace_id: sentry_trace_id.map(Into::into),
     };
     let json = serde_json::to_vec_pretty(&bootstrap).map_err(std::io::Error::other)?;