Decouple `runtime`/`mirror` from the kitchen-sink `ci::Error`
Mirrors the earlier `CompileError` move on the runtime side: a small
`RuntimeError` (sum of `secret::Error`, `mlua::Error`,
`CommandSpawnFailed`, and `Git`) carries failures through
`Runtime::secret`/`sh`, `MirrorJob::execute`, and the `RustRunFn`
callback type. A flattening `From<RuntimeError> for Error` at the
boundary keeps existing kitchen-sink callers working.

Sets up the next move: `pipeline`, `registration`, `mirror`,
`runtime`, and `RunMeta` no longer reference `super::error`, so they
can land in `quire-core` without dragging rusqlite, yaml, and the
rest of the orchestrator-flavored variants along.

Assisted-by: Claude Opus 4.7 (1M context) via Claude Code
change rsotykrzzlqotytsrpzqtrnnqzkunykz
commit 48121fe32be75f1ef9ccd74443c6070fb3c95b6d
author Alpha Chen <alpha@kejadlen.dev>
date
parent uqqlwmpu
diff --git a/quire-server/src/ci/error.rs b/quire-server/src/ci/error.rs
index e4675a1..3edb624 100644
--- a/quire-server/src/ci/error.rs
+++ b/quire-server/src/ci/error.rs
@@ -4,6 +4,7 @@ use miette::Diagnostic;
 
 use super::pipeline::{CompileError, PipelineError};
 use super::run::RunState;
+use super::runtime::RuntimeError;
 use quire_core::fennel::FennelError;
 use quire_core::secret;
 
@@ -81,6 +82,25 @@ impl From<CompileError> for Error {
     }
 }
 
+impl From<RuntimeError> for Error {
+    fn from(err: RuntimeError) -> Self {
+        match err {
+            RuntimeError::Secret(e) => Error::Secret(e),
+            RuntimeError::Lua(e) => Error::Lua(e),
+            RuntimeError::CommandSpawnFailed {
+                program,
+                cwd,
+                source,
+            } => Error::CommandSpawnFailed {
+                program,
+                cwd,
+                source,
+            },
+            RuntimeError::Git(s) => Error::Git(s),
+        }
+    }
+}
+
 impl From<FennelError> for Error {
     fn from(err: FennelError) -> Self {
         Error::Fennel(Box::new(err))
diff --git a/quire-server/src/ci/mirror.rs b/quire-server/src/ci/mirror.rs
index a71097d..265770c 100644
--- a/quire-server/src/ci/mirror.rs
+++ b/quire-server/src/ci/mirror.rs
@@ -19,10 +19,9 @@ use std::rc::Rc;
 
 use mlua::{Lua, LuaSerdeExt};
 
-use super::error::{Error, Result};
 use super::pipeline::{self, DefinitionError, Job, RunFn};
 use super::registration::Registration;
-use super::runtime::{Cmd, Runtime, ShOpts};
+use super::runtime::{Cmd, Runtime, RuntimeError, RuntimeResult, ShOpts};
 
 /// Closure state for the `quire/mirror` job's run-fn: everything the
 /// tag-and-push needs at execute time, captured once at registration.
@@ -46,7 +45,7 @@ impl MirrorJob {
     /// `git push` exit lands in the run log alongside any other shell
     /// output. Returns `Err` only for setup failures (unknown secret,
     /// failed tag, base64 spawn).
-    fn execute(&self, rt: &Runtime) -> Result<()> {
+    fn execute(&self, rt: &Runtime) -> RuntimeResult<()> {
         let calling = rt.current_job.borrow();
         let calling = calling
             .as_ref()
@@ -91,7 +90,7 @@ impl MirrorJob {
             git_opts.clone(),
         )?;
         if tag_result.exit != 0 {
-            return Err(Error::Git(format!(
+            return Err(RuntimeError::Git(format!(
                 "git tag failed: {}",
                 tag_result.stderr.trim()
             )));
@@ -600,7 +599,7 @@ mod tests {
         runtime.leave_job();
 
         assert!(
-            matches!(err, Error::Secret(SecretError::UnknownSecret(ref name)) if name == "missing"),
+            matches!(err, RuntimeError::Secret(SecretError::UnknownSecret(ref name)) if name == "missing"),
             "expected UnknownSecret(\"missing\"), got: {err:?}"
         );
     }
diff --git a/quire-server/src/ci/pipeline.rs b/quire-server/src/ci/pipeline.rs
index 564c553..899e01a 100644
--- a/quire-server/src/ci/pipeline.rs
+++ b/quire-server/src/ci/pipeline.rs
@@ -112,7 +112,7 @@ pub struct Job {
 /// A Rust-side run-fn: a closure invoked synchronously by the
 /// executor with the runtime in scope.
 pub(super) type RustRunFn =
-    std::rc::Rc<dyn Fn(&super::runtime::Runtime) -> super::error::Result<()>>;
+    std::rc::Rc<dyn Fn(&super::runtime::Runtime) -> super::runtime::RuntimeResult<()>>;
 
 /// How a job runs at execute time.
 ///
diff --git a/quire-server/src/ci/run.rs b/quire-server/src/ci/run.rs
index 08c81e1..19bc899 100644
--- a/quire-server/src/ci/run.rs
+++ b/quire-server/src/ci/run.rs
@@ -258,7 +258,7 @@ impl Run {
                     let _: mlua::Value = f.call(rt_value.clone())?;
                     Ok(())
                 }
-                RunFn::Rust(f) => f(&runtime),
+                RunFn::Rust(f) => f(&runtime).map_err(Into::into),
             })();
             runtime.leave_job();
 
@@ -1278,7 +1278,9 @@ mod tests {
         );
 
         pipeline.replace_first_run_fn(RunFn::Rust(Rc::new(|_rt| {
-            Err(Error::Git("simulated rust failure".into()))
+            Err(crate::ci::runtime::RuntimeError::Git(
+                "simulated rust failure".into(),
+            ))
         })));
 
         let err = run
diff --git a/quire-server/src/ci/runtime.rs b/quire-server/src/ci/runtime.rs
index 98387e4..ab8ef79 100644
--- a/quire-server/src/ci/runtime.rs
+++ b/quire-server/src/ci/runtime.rs
@@ -15,7 +15,40 @@ use mlua::{IntoLua, Lua, LuaSerdeExt};
 
 use super::pipeline::{Job, Pipeline};
 use super::run::RunMeta;
-use quire_core::secret::{SecretRegistry, SecretString, redact};
+use quire_core::secret::{self, SecretRegistry, SecretString, redact};
+
+/// Errors produced by [`Runtime`] methods and the `RunFn::Rust`
+/// callbacks that hold them. A small sum carved out of the
+/// orchestrator's kitchen-sink error so the runtime layer doesn't
+/// drag rusqlite/yaml/etc. along with it.
+#[derive(Debug, thiserror::Error, miette::Diagnostic)]
+pub enum RuntimeError {
+    #[error(transparent)]
+    Secret(#[from] secret::Error),
+
+    #[error(transparent)]
+    Lua(Box<mlua::Error>),
+
+    #[error("command spawn failed: {program} in {cwd}")]
+    CommandSpawnFailed {
+        program: String,
+        cwd: std::path::PathBuf,
+        #[source]
+        source: std::io::Error,
+    },
+
+    #[error("git error: {0}")]
+    Git(String),
+}
+
+impl From<mlua::Error> for RuntimeError {
+    fn from(err: mlua::Error) -> Self {
+        Self::Lua(Box::new(err))
+    }
+}
+
+pub type RuntimeResult<T> = std::result::Result<T, RuntimeError>;
+
 /// Per-sh timing: (index, started_at, finished_at).
 pub(super) type ShTimings = Vec<(usize, Timestamp, Timestamp)>;
 
@@ -176,23 +209,23 @@ impl Runtime {
     /// the full caveat.
     ///
     /// [`SecretRegistry::resolve`]: quire_core::secret::SecretRegistry::resolve
-    pub(super) fn secret(&self, name: &str) -> super::error::Result<String> {
+    pub(super) fn secret(&self, name: &str) -> RuntimeResult<String> {
         self.registry.borrow_mut().resolve(name).map_err(Into::into)
     }
 
     /// Run `cmd` with `opts` and record its output against the
     /// current job (if one is active). Non-zero exits come back in
     /// `:exit`, not as `Err`.
-    pub(super) fn sh(&self, cmd: Cmd, opts: ShOpts) -> super::error::Result<ShOutput> {
+    pub(super) fn sh(&self, cmd: Cmd, opts: ShOpts) -> RuntimeResult<ShOutput> {
         let started_at = Timestamp::now();
         let program = cmd.program().to_string();
-        let output = cmd.run(opts, &self.workspace).map_err(|e| {
-            super::error::Error::CommandSpawnFailed {
-                program,
-                cwd: self.workspace.clone(),
-                source: e,
-            }
-        })?;
+        let output =
+            cmd.run(opts, &self.workspace)
+                .map_err(|e| RuntimeError::CommandSpawnFailed {
+                    program,
+                    cwd: self.workspace.clone(),
+                    source: e,
+                })?;
         let finished_at = Timestamp::now();
         if let Some(job) = self.current_job.borrow().as_ref() {
             let n = {