Rename dispatch → bootstrap across the codebase
The `Dispatch` struct and its wire format were named after the act of
dispatching a run to quire-ci, not what the resource actually is. As a
REST endpoint `/api/runs/:id/dispatch` reads like a verb-trigger rather
than a noun resource. Rename everything to `bootstrap` since that's what
quire-ci uses the payload for — bootstrapping a run — and what the design
doc's own lifecycle description calls it.

- `quire-core/src/ci/dispatch.rs` → `bootstrap.rs`; `Dispatch` → `Bootstrap`
- `dispatch.json` on disk → `bootstrap.json`
- `--dispatch` CLI flag → `--bootstrap`
- `write_dispatch` / `load_dispatch` → `write_bootstrap` / `load_bootstrap`
- `/api/runs/:id/dispatch` in design doc → `/api/runs/:id/bootstrap`

https://claude.ai/code/session_01LEsqtS6DbBkGyyzNM4iRz1
change
commit 8e58d969448154da681b7feb6a48616649a36b50
author Claude <noreply@anthropic.com>
date
parent pmlwyrvx
diff --git a/docs/CI-STATE.md b/docs/CI-STATE.md
index a641ba7..00bd2fe 100644
--- a/docs/CI-STATE.md
+++ b/docs/CI-STATE.md
@@ -164,7 +164,7 @@ sequenceDiagram
 
     Trigger->>Run: execute_via_quire_ci()
     Run->>DB: UPDATE runs SET state='active'
-    Run->>CI: spawn (--dispatch, --events, --out-dir)
+    Run->>CI: spawn (--bootstrap, --events, --out-dir)
     CI->>CI: compile .quire/ci.fnl
     loop per job in topo order
       CI->>CI: enter_job / run-fn / leave_job
diff --git a/docs/plans/2026-05-14-quire-ci-server-api-design.md b/docs/plans/2026-05-14-quire-ci-server-api-design.md
index 7aa153a..2819434 100644
--- a/docs/plans/2026-05-14-quire-ci-server-api-design.md
+++ b/docs/plans/2026-05-14-quire-ci-server-api-design.md
@@ -27,7 +27,7 @@ Out of scope (deliberately deferred to v2):
 
 | Artifact | Path | Writer | Reader | Purpose |
 |---|---|---|---|---|
-| Dispatch | `<run-dir>/dispatch.json` | server | CI | Secrets, git_dir, push meta, Sentry handoff |
+| Bootstrap | `<run-dir>/bootstrap.json` | server | CI | Secrets, git_dir, push meta, Sentry handoff |
 | Events | `<run-dir>/events.jsonl` | CI | server | Job/sh state transitions |
 | Subprocess log | `<run-dir>/quire-ci.log` | CI (stdio) | server UI | Debug subprocess output |
 | Per-sh log | `<run-dir>/jobs/<job>/sh-<n>.log` | CI | server UI | Rendered on job detail page |
@@ -61,7 +61,7 @@ Endpoints (CI calls server):
 
 | Method | Path | Body | Purpose |
 |---|---|---|---|
-| GET    | `/api/runs/:id/dispatch` | — | Fetch dispatch payload. One-shot: server invalidates after first successful read. |
+| GET    | `/api/runs/:id/bootstrap` | — | Fetch bootstrap payload. One-shot: server invalidates after first successful read. |
 | POST   | `/api/runs/:id/jobs/:job_id/start` | `{}` | Job entered execution. Server timestamps. |
 | POST   | `/api/runs/:id/jobs/:job_id/finish` | `{ outcome: "complete" \| "failed" }` | Job done. |
 | POST   | `/api/runs/:id/jobs/:job_id/sh/start` | `{ cmd: string }` | Start the next sh in this job. Server assigns the sh index. |
@@ -78,7 +78,7 @@ Ordering rules enforced server-side, returning 409 on violation:
 - `jobs/:id/finish` requires no sh currently open.
 - `complete` requires every started job to have finished.
 
-Other error codes: 401 for missing/invalid token, 403 for token/run mismatch, 404 for unknown run, 410 if `dispatch` was already fetched, 422 if a path segment doesn't match a known job, 5xx for server errors (CI retries with backoff).
+Other error codes: 401 for missing/invalid token, 403 for token/run mismatch, 404 for unknown run, 410 if `bootstrap` was already fetched, 422 if a path segment doesn't match a known job, 5xx for server errors (CI retries with backoff).
 
 ## Lifecycle
 
@@ -93,9 +93,9 @@ Other error codes: 401 for missing/invalid token, 403 for token/run mismatch, 40
      --workspace <path>
    ```
 
-   No `--dispatch`, no `--events`, no log path. The workspace path remains because that's the checkout CI runs *in*, not a comms channel.
+   No `--bootstrap`, no `--events`, no log path. The workspace path remains because that's the checkout CI runs *in*, not a comms channel.
 
-3. **quire-ci bootstraps.** First call is `GET /api/runs/:id/dispatch`. Server flips the run to `active`, returns the dispatch payload (secrets, git_dir, push meta, Sentry handoff), and invalidates the dispatch resource. Watchdog timer starts.
+3. **quire-ci bootstraps.** First call is `GET /api/runs/:id/bootstrap`. Server flips the run to `active`, returns the bootstrap payload (secrets, git_dir, push meta, Sentry handoff), and invalidates the bootstrap resource. Watchdog timer starts.
 
 4. **quire-ci executes the DAG.** For each job:
    - `POST /jobs/:job_id/start`
@@ -123,7 +123,7 @@ Server-side validation runs before any DB work:
 quire-ci retry policy:
 
 - **Events (start/finish endpoints):** retry on 5xx with exponential backoff, up to ~5 attempts. On final failure, log to stderr and continue. Losing a state-transition POST is bad but should not kill the run.
-- **Dispatch:** retry on 5xx, fail fast on 4xx. If dispatch cannot be fetched, the run is unrunnable; CI exits nonzero and the watchdog catches it.
+- **Bootstrap:** retry on 5xx, fail fast on 4xx. If bootstrap cannot be fetched, the run is unrunnable; CI exits nonzero and the watchdog catches it.
 - **Complete:** the most important POST in the run. Retry with backoff and more attempts than the others.
 - **Log streams:** v1 does not retry. A broken stream logs a stderr warning and execution continues.
 
@@ -131,7 +131,7 @@ Server-side robustness:
 
 - All state-transition endpoints are idempotent by virtue of state checks: a retried `/start` after a successful first call gets 409, and CI treats that as "already recorded, move on."
 - Watchdog: per-run last-contact timestamp, reset on every authenticated request. Configurable timeout (default tuned long enough for slow `npm install`-style steps, short enough that crashes don't strand runs).
-- Sentry handoff is still passed via the dispatch payload, so CI traces continue to link to server traces.
+- Sentry handoff is still passed via the bootstrap payload, so CI traces continue to link to server traces.
 
 ## Rollout
 
@@ -143,7 +143,7 @@ A server-side config flag keeps the filesystem path working while the API is bui
  ...}
 ```
 
-Default is `:filesystem` during build-out. The server reads this at startup and, when spawning quire-ci, passes the choice through (CLI flag or env var — implementation detail). quire-ci implements both paths and dispatches at startup. Each slice (dispatch, jobs/sh state, logs, complete) checks the flag and takes either path.
+Default is `:filesystem` during build-out. The server reads this at startup and, when spawning quire-ci, passes the choice through (CLI flag or env var — implementation detail). quire-ci implements both paths and dispatches at startup. Each slice (bootstrap, jobs/sh state, logs, complete) checks the flag and takes either path.
 
 The flag lives in server config rather than per-repo `ci.fnl` because:
 
@@ -160,7 +160,7 @@ After every slice ships and the API path proves out, delete the filesystem path
 These will be filed as separate ranger tasks:
 
 1. **Auth + transport foundation.** Bearer-token middleware on server. HTTP client in quire-ci. CLI args (`--run-id`, `--server-url`) and `QUIRE_CI_TOKEN` env var threaded through. Config flag wired up. No behavior change yet — both halves still take the filesystem path.
-2. **Dispatch over the API.** `GET /api/runs/:id/dispatch` and corresponding client call, gated by the transport flag.
+2. **Bootstrap over the API.** `GET /api/runs/:id/bootstrap` and corresponding client call, gated by the transport flag.
 3. **Job and sh state transitions over the API.** The four `/start` and `/finish` endpoints, plus their clients.
 4. **Log streaming over the API.** Chunked `POST /sh/logs` with CRI-line bodies.
 5. **`/complete` and watchdog.** Authoritative completion, server-side timeout backstop.
diff --git a/quire-ci/src/main.rs b/quire-ci/src/main.rs
index ff90cb6..8cc34e2 100644
--- a/quire-ci/src/main.rs
+++ b/quire-ci/src/main.rs
@@ -54,8 +54,8 @@ enum Commands {
 
     /// Run the whole pipeline against the workspace, in topo order.
     ///
-    /// `--dispatch <path>` points at a JSON file (see
-    /// [`quire_core::ci::dispatch::Dispatch`]) that supplies push
+    /// `--bootstrap <path>` points at a JSON file (see
+    /// [`quire_core::ci::bootstrap::Bootstrap`]) that supplies push
     /// metadata and secrets when the orchestrator dispatches via
     /// `:executor :quire-ci`. Standalone invocations omit the flag
     /// and fall back to placeholder meta with no secrets — `(secret
@@ -77,12 +77,12 @@ enum Commands {
         #[arg(long)]
         out_dir: Option<PathBuf>,
 
-        /// Path to a JSON dispatch file produced by the orchestrator.
+        /// Path to a JSON bootstrap file produced by the orchestrator.
         /// Carries push metadata and the secrets the run-fns may
         /// resolve. Omit for standalone runs (placeholder meta, no
         /// secrets).
         #[arg(long)]
-        dispatch: Option<PathBuf>,
+        bootstrap: Option<PathBuf>,
 
         #[command(flatten)]
         transport: TransportFlags,
@@ -235,7 +235,7 @@ fn main() -> miette::Result<()> {
         Commands::Run {
             events,
             out_dir,
-            dispatch,
+            bootstrap,
             transport,
         } => {
             let sink: Box<dyn EventSink> = match events {
@@ -259,8 +259,8 @@ fn main() -> miette::Result<()> {
             };
             let auth_token = std::env::var("QUIRE_CI_TOKEN").ok();
             let transport = transport.resolve(auth_token)?;
-            let (git_dir, meta, secrets, sentry_handoff) = match dispatch {
-                Some(path) => load_dispatch(&path)?,
+            let (git_dir, meta, secrets, sentry_handoff) = match bootstrap {
+                Some(path) => load_bootstrap(&path)?,
                 None => (
                     cli.workspace.join(".git"),
                     placeholder_meta(),
@@ -313,7 +313,7 @@ fn main() -> miette::Result<()> {
 /// trace_id (shouldn't happen — the orchestrator emits the canonical
 /// hex form) is logged and skipped rather than aborting Sentry init.
 fn init_sentry(
-    handoff: Option<&quire_core::ci::dispatch::SentryHandoff>,
+    handoff: Option<&quire_core::ci::bootstrap::SentryHandoff>,
     meta: &RunMeta,
 ) -> Option<sentry::ClientInitGuard> {
     let handoff = handoff?;
@@ -341,7 +341,7 @@ fn init_sentry(
                 tracing::warn!(
                     trace_id = %handoff.trace_id,
                     error = %e,
-                    "malformed trace_id in dispatch; quire-ci events won't link to orchestrator",
+                    "malformed trace_id in bootstrap; quire-ci events won't link to orchestrator",
                 );
             }
         }
@@ -372,7 +372,7 @@ fn validate(workspace: PathBuf) -> miette::Result<()> {
 }
 
 /// Standalone runs synthesize a placeholder `quire/push`. Real meta
-/// arrives via `--dispatch` from the orchestrator.
+/// arrives via `--bootstrap` from the orchestrator.
 fn placeholder_meta() -> RunMeta {
     RunMeta {
         sha: "0".repeat(40),
@@ -381,27 +381,27 @@ fn placeholder_meta() -> RunMeta {
     }
 }
 
-/// Read and parse the dispatch file the orchestrator wrote before
+/// Read and parse the bootstrap file the orchestrator wrote before
 /// spawning. Wraps revealed secret values back into `SecretString`.
 ///
 /// Unlinks the file as soon as the bytes are in memory — secrets only
-/// need to live on disk for the moment between `write_dispatch` and
+/// need to live on disk for the moment between `write_bootstrap` and
 /// this read, and getting them off disk early limits the blast radius
 /// of a later panic or crash leaving a 0600 file behind.
 ///
 /// The Sentry handoff, when present, carries the DSN and the
-/// orchestrator's trace id — the 0600 dispatch file is the line of
+/// orchestrator's trace id — the 0600 bootstrap file is the line of
 /// defense for both.
 #[allow(clippy::type_complexity)]
-fn load_dispatch(
+fn load_bootstrap(
     path: &std::path::Path,
 ) -> miette::Result<(
     PathBuf,
     RunMeta,
     HashMap<String, quire_core::secret::SecretString>,
-    Option<quire_core::ci::dispatch::SentryHandoff>,
+    Option<quire_core::ci::bootstrap::SentryHandoff>,
 )> {
-    use quire_core::ci::dispatch::Dispatch;
+    use quire_core::ci::bootstrap::Bootstrap;
     use quire_core::secret::SecretString;
 
     let bytes = fs_err::read(path).into_diagnostic()?;
@@ -410,17 +410,17 @@ fn load_dispatch(
         // will best-effort unlink after we exit. But this is a
         // security-relevant cleanup, so it's worth surfacing.
         eprintln!(
-            "warning: failed to remove dispatch file {}: {e}",
+            "warning: failed to remove bootstrap file {}: {e}",
             path.display()
         );
     }
-    let dispatch: Dispatch = serde_json::from_slice(&bytes).into_diagnostic()?;
-    let secrets = dispatch
+    let bootstrap: Bootstrap = serde_json::from_slice(&bytes).into_diagnostic()?;
+    let secrets = bootstrap
         .secrets
         .into_iter()
         .map(|(name, value)| (name, SecretString::from(value)))
         .collect();
-    Ok((dispatch.git_dir, dispatch.meta, secrets, dispatch.sentry))
+    Ok((bootstrap.git_dir, bootstrap.meta, secrets, bootstrap.sentry))
 }
 
 fn run_pipeline(
@@ -620,12 +620,12 @@ mod tests {
     }
 
     #[test]
-    fn load_dispatch_unlinks_after_read() {
-        use quire_core::ci::dispatch::Dispatch;
+    fn load_bootstrap_unlinks_after_read() {
+        use quire_core::ci::bootstrap::Bootstrap;
 
         let dir = tempfile::tempdir().expect("tempdir");
-        let path = dir.path().join("dispatch.json");
-        let dispatch = Dispatch {
+        let path = dir.path().join("bootstrap.json");
+        let bootstrap = Bootstrap {
             meta: RunMeta {
                 sha: "0".repeat(40),
                 r#ref: "HEAD".to_string(),
@@ -635,10 +635,13 @@ mod tests {
             secrets: HashMap::from([("token".to_string(), "shh".to_string())]),
             sentry: None,
         };
-        fs_err::write(&path, serde_json::to_vec(&dispatch).unwrap()).expect("write");
+        fs_err::write(&path, serde_json::to_vec(&bootstrap).unwrap()).expect("write");
 
-        let (git_dir, meta, secrets, sentry) = load_dispatch(&path).expect("load");
-        assert!(!path.exists(), "dispatch file should be removed after read");
+        let (git_dir, meta, secrets, sentry) = 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_eq!(secrets.len(), 1);
diff --git a/quire-core/src/ci/dispatch.rs b/quire-core/src/ci/bootstrap.rs
similarity index 90%
rename from quire-core/src/ci/dispatch.rs
rename to quire-core/src/ci/bootstrap.rs
index 30981ba..ad90dbd 100644
--- a/quire-core/src/ci/dispatch.rs
+++ b/quire-core/src/ci/bootstrap.rs
@@ -1,8 +1,8 @@
 //! Wire format for handing off a run from the orchestrator to
 //! `quire-ci`.
 //!
-//! The orchestrator writes a [`Dispatch`] as JSON to a file inside
-//! the run directory and passes the path via `--dispatch`. `quire-ci`
+//! The orchestrator writes a [`Bootstrap`] as JSON to a file inside
+//! the run directory and passes the path via `--bootstrap`. `quire-ci`
 //! deserializes it on startup to recover push metadata and the
 //! secrets the run-fns may resolve. Standalone `quire-ci run`
 //! invocations skip the file entirely and fall back to placeholder
@@ -36,7 +36,7 @@ use crate::ci::run::RunMeta;
 /// `git archive` extract with no `.git` inside, so quire-ci has no
 /// way to recover this path on its own.
 #[derive(Debug, Serialize, Deserialize)]
-pub struct Dispatch {
+pub struct Bootstrap {
     pub meta: RunMeta,
     pub git_dir: PathBuf,
     pub secrets: HashMap<String, String>,
@@ -50,7 +50,7 @@ pub struct Dispatch {
 /// What quire-ci needs to mirror the orchestrator's Sentry context.
 ///
 /// The DSN is plaintext like the secrets — the 0600 mode on the
-/// dispatch file is the line of defense. `trace_id` is the hex form
+/// bootstrap file is the line of defense. `trace_id` is the hex form
 /// of [`sentry::protocol::TraceId`]; kept as a string here so
 /// `quire-core` doesn't grow a `sentry` dep.
 #[derive(Debug, Serialize, Deserialize)]
diff --git a/quire-core/src/ci/mod.rs b/quire-core/src/ci/mod.rs
index 40a310e..1cf5bb8 100644
--- a/quire-core/src/ci/mod.rs
+++ b/quire-core/src/ci/mod.rs
@@ -5,7 +5,7 @@
 //! (where `quire-ci` invokes them) and on the server (where the
 //! orchestrator drives them).
 
-pub mod dispatch;
+pub mod bootstrap;
 pub mod event;
 pub mod logs;
 pub mod pipeline;
diff --git a/quire-server/src/ci/mod.rs b/quire-server/src/ci/mod.rs
index 06ec670..d07c747 100644
--- a/quire-server/src/ci/mod.rs
+++ b/quire-server/src/ci/mod.rs
@@ -192,7 +192,7 @@ fn run_ref(
     let sentry_handoff =
         ctx.sentry_dsn
             .as_ref()
-            .map(|dsn| quire_core::ci::dispatch::SentryHandoff {
+            .map(|dsn| quire_core::ci::bootstrap::SentryHandoff {
                 dsn: dsn.clone(),
                 trace_id: trace_id.to_string(),
             });
@@ -233,7 +233,7 @@ fn run_ref_inner(
     pushed_at: jiff::Timestamp,
     push_ref: &PushRef,
     transport: &Transport,
-    sentry: Option<&quire_core::ci::dispatch::SentryHandoff>,
+    sentry: Option<&quire_core::ci::bootstrap::SentryHandoff>,
 ) -> error::Result<()> {
     let ci = ctx.repo.ci();
 
diff --git a/quire-server/src/ci/run.rs b/quire-server/src/ci/run.rs
index 6c46f52..1eacfe7 100644
--- a/quire-server/src/ci/run.rs
+++ b/quire-server/src/ci/run.rs
@@ -328,7 +328,7 @@ impl Run {
         workspace: &Path,
         meta: &RunMeta,
         secrets: &HashMap<String, SecretString>,
-        sentry: Option<&quire_core::ci::dispatch::SentryHandoff>,
+        sentry: Option<&quire_core::ci::bootstrap::SentryHandoff>,
         transport: &Transport,
     ) -> Result<()> {
         self.transition(RunState::Active, None)?;
@@ -336,13 +336,13 @@ impl Run {
         let run_dir = self.path();
         let log_path = run_dir.join("quire-ci.log");
         let events_path = run_dir.join("events.jsonl");
-        let dispatch_path = run_dir.join("dispatch.json");
+        let bootstrap_path = run_dir.join("bootstrap.json");
         // fs_err for the path-bearing IO error; unwrap to std::fs::File so
         // it's convertible into Stdio.
         let log = fs_err::File::create(&log_path)?.into_parts().0;
         let log_clone = log.try_clone()?;
 
-        write_dispatch(&dispatch_path, git_dir, meta, secrets, sentry)?;
+        write_bootstrap(&bootstrap_path, git_dir, meta, secrets, sentry)?;
 
         tracing::info!(
             run_id = %self.id,
@@ -360,8 +360,8 @@ impl Run {
                     .arg(&run_dir)
                     .arg("--events")
                     .arg(&events_path)
-                    .arg("--dispatch")
-                    .arg(&dispatch_path);
+                    .arg("--bootstrap")
+                    .arg(&bootstrap_path);
             }
             Transport::Api(api) => {
                 cmd.arg("--run-id")
@@ -382,18 +382,18 @@ impl Run {
                 source,
             })?;
 
-        // quire-ci unlinks the dispatch file after `load_dispatch`;
+        // quire-ci unlinks the bootstrap file after `load_bootstrap`;
         // this is a best-effort safety net for paths where it didn't
         // get that far (spawn failed mid-exec, arg parsing rejected
         // input, panic before read). `NotFound` is the expected case.
-        if let Err(e) = fs_err::remove_file(&dispatch_path)
+        if let Err(e) = fs_err::remove_file(&bootstrap_path)
             && e.kind() != std::io::ErrorKind::NotFound
         {
             tracing::warn!(
                 run_id = %self.id,
-                path = %dispatch_path.display(),
+                path = %bootstrap_path.display(),
                 error = %e,
-                "failed to remove dispatch file after run"
+                "failed to remove bootstrap file after run"
             );
         }
 
@@ -638,18 +638,18 @@ impl Run {
     }
 }
 
-/// Serialize the dispatch payload as JSON and write it to `path` with
+/// Serialize the bootstrap payload as JSON and write it to `path` with
 /// owner-only permissions on Unix. Secrets cross as plaintext so the
 /// 0600 mode is the line of defense against other local users; failure
-/// to set the mode aborts the dispatch (better than leaking).
-fn write_dispatch(
+/// to set the mode aborts the write (better than leaking).
+fn write_bootstrap(
     path: &Path,
     git_dir: &Path,
     meta: &RunMeta,
     secrets: &HashMap<String, SecretString>,
-    sentry: Option<&quire_core::ci::dispatch::SentryHandoff>,
+    sentry: Option<&quire_core::ci::bootstrap::SentryHandoff>,
 ) -> Result<()> {
-    use quire_core::ci::dispatch::{Dispatch, SentryHandoff};
+    use quire_core::ci::bootstrap::{Bootstrap, SentryHandoff};
 
     let mut revealed: HashMap<String, String> = HashMap::with_capacity(secrets.len());
     for (name, value) in secrets {
@@ -658,7 +658,7 @@ fn write_dispatch(
             value.reveal().map_err(Error::Secret)?.to_string(),
         );
     }
-    let dispatch = Dispatch {
+    let bootstrap = Bootstrap {
         meta: meta.clone(),
         git_dir: git_dir.to_path_buf(),
         secrets: revealed,
@@ -667,7 +667,7 @@ fn write_dispatch(
             trace_id: s.trace_id.clone(),
         }),
     };
-    let json = serde_json::to_vec_pretty(&dispatch).map_err(std::io::Error::other)?;
+    let json = serde_json::to_vec_pretty(&bootstrap).map_err(std::io::Error::other)?;
 
     // Open with mode 0600 from the start so there's no window where
     // the file is world-readable.
@@ -867,26 +867,26 @@ mod tests {
     }
 
     #[test]
-    fn write_dispatch_records_git_dir_for_quire_ci() {
-        use quire_core::ci::dispatch::Dispatch;
+    fn write_bootstrap_records_git_dir_for_quire_ci() {
+        use quire_core::ci::bootstrap::Bootstrap;
 
         let dir = tempfile::tempdir().expect("tempdir");
-        let dispatch_path = dir.path().join("dispatch.json");
+        let bootstrap_path = dir.path().join("bootstrap.json");
         let git_dir = dir.path().join("repos").join("test.git");
 
-        write_dispatch(
-            &dispatch_path,
+        write_bootstrap(
+            &bootstrap_path,
             &git_dir,
             &test_meta(),
             &HashMap::new(),
             None,
         )
-        .expect("write_dispatch");
+        .expect("write_bootstrap");
 
-        let bytes = fs_err::read(&dispatch_path).expect("read dispatch");
-        let dispatch: Dispatch = serde_json::from_slice(&bytes).expect("parse dispatch");
+        let bytes = fs_err::read(&bootstrap_path).expect("read bootstrap");
+        let bootstrap: Bootstrap = serde_json::from_slice(&bytes).expect("parse bootstrap");
         assert_eq!(
-            dispatch.git_dir, git_dir,
+            bootstrap.git_dir, git_dir,
             "quire-ci needs the bare repo path to set GIT_DIR for the mirror job"
         );
     }