Pass runtime table as run-fn argument
The executor now calls run-fns with the runtime table instead of ().
Lua silently discards extra arguments, so zero-arg (fn [] ...) still
works — but run-fns can now destructure the runtime directly:
(fn [{: sh : secret : jobs}] ...).

Removes the ArityViolation check that rejected one-arg run-fns at
registration time, since the argument is now always provided.

Assisted-by: GLM-5.1 via pi
change uynmmpwtmsususozywzxvvolnnslwlrk
commit 0e945592139a32f1bb20fba1928cace71980a8c7
author Alpha Chen <alpha@kejadlen.dev>
date
parent qwxknmry
diff --git a/quire-ci/src/main.rs b/quire-ci/src/main.rs
index bfb64eb..6cde4ed 100644
--- a/quire-ci/src/main.rs
+++ b/quire-ci/src/main.rs
@@ -413,9 +413,11 @@ fn run_pipeline(
             .clone();
 
         runtime.enter_job(job_id);
+        let rt =
+            RuntimeHandle::runtime_table(runtime.lua()).expect("runtime table should be installed");
         let result: Result<(), RuntimeError> = match run_fn {
             RunFn::Lua(f) => f
-                .call::<mlua::Value>(())
+                .call::<mlua::Value>(rt)
                 .map(|_| ())
                 .map_err(RuntimeError::from),
             RunFn::Rust(f) => f(&runtime),
diff --git a/quire-core/src/ci/pipeline.rs b/quire-core/src/ci/pipeline.rs
index 67fc196..2a3e352 100644
--- a/quire-core/src/ci/pipeline.rs
+++ b/quire-core/src/ci/pipeline.rs
@@ -46,15 +46,6 @@ pub enum DefinitionError {
         #[label("duplicate registration")]
         span: SourceSpan,
     },
-
-    #[error(
-        "Job '{job_id}' run-fn takes an argument — run-fns must be zero-arg `(fn [] …)`; bind the runtime via `(local runtime (require :quire.runtime))` or destructure it from `(require :quire.ci)` instead of destructuring a handle."
-    )]
-    ArityViolation {
-        job_id: String,
-        #[label("here")]
-        span: SourceSpan,
-    },
 }
 
 /// A post-graph structural error found after all jobs have been
diff --git a/quire-core/src/ci/registration.rs b/quire-core/src/ci/registration.rs
index 2086cf8..8242ac6 100644
--- a/quire-core/src/ci/registration.rs
+++ b/quire-core/src/ci/registration.rs
@@ -198,26 +198,6 @@ fn register_job(
         return Ok(());
     }
 
-    // Arity check: one-arg run-fns use the old `(fn [{: sh}] …)`
-    // pattern. Reject them so users get a clear message instead of
-    // a runtime-nil panic when the ambient runtime is absent.
-    //
-    // Fail open on `debug.getinfo` errors — if the debug library is
-    // unavailable or the call shape changes, treat the function as
-    // zero-arg and let the user surface any real arity mismatch at
-    // execution time.
-    let nparams: u32 = lua
-        .load("return debug.getinfo(...).nparams")
-        .call(&run_fn)
-        .unwrap_or(0);
-    if nparams != 0 {
-        let span = pipeline::span_for_line(&r.source, line);
-        r.errors
-            .borrow_mut()
-            .push(DefinitionError::ArityViolation { job_id: id, span });
-        return Ok(());
-    }
-
     match Job::new(id, inputs, RunFn::Lua(run_fn), line, &r.source) {
         Ok(job) => r.add_job(job, line),
         Err(e) => r.errors.borrow_mut().push(e),
diff --git a/quire-core/src/ci/runtime.rs b/quire-core/src/ci/runtime.rs
index 3053338..c83d0c3 100644
--- a/quire-core/src/ci/runtime.rs
+++ b/quire-core/src/ci/runtime.rs
@@ -380,6 +380,13 @@ pub struct RuntimeHandle {
 }
 
 impl RuntimeHandle {
+    /// The Lua table at `package.loaded["quire.ci"].runtime`.
+    /// Populated by [`install`](Self::install) and cleared by
+    /// [`uninstall`](Self::uninstall).
+    pub fn runtime_table(lua: &Lua) -> mlua::Result<mlua::Table> {
+        runtime_stub(lua)
+    }
+
     /// Install the runtime on the Lua VM. Hold the returned guard for
     /// the duration of the run; dropping it tears the install down.
     pub fn install(runtime: Rc<Runtime>, lua: &Lua) -> mlua::Result<Self> {
@@ -656,6 +663,13 @@ mod tests {
     /// tears down the install). Tests in this module exercise the
     /// `RunFn::Lua` path; if the first job turns out to be a `Rust`
     /// variant the test setup is wrong.
+    /// Call `run_fn` with the runtime table as its argument, matching
+    /// how the executor invokes Lua run-fns.
+    fn call_fn(runtime: &Rc<Runtime>, run_fn: &mlua::Function) -> mlua::Result<mlua::Value> {
+        let rt = runtime_stub(runtime.lua()).expect("runtime table");
+        run_fn.call(rt)
+    }
+
     fn rt(
         source: &str,
         secrets: HashMap<String, SecretString>,
@@ -762,7 +776,7 @@ mod tests {
         }));
         *runtime.current_job.borrow_mut() = Some("go".to_string());
 
-        let _: mlua::Value = run_fn.call(()).expect("sh call");
+        let _: mlua::Value = call_fn(&runtime, &run_fn).expect("sh call");
 
         let calls = received.borrow();
         assert_eq!(calls.len(), 2, "expected 2 events, got: {calls:?}");
@@ -784,7 +798,7 @@ mod tests {
         let log_dir = runtime.log_dir().to_path_buf();
         *runtime.current_job.borrow_mut() = Some("go".to_string());
 
-        let _: mlua::Value = run_fn.call(()).expect("sh call");
+        let _: mlua::Value = call_fn(&runtime, &run_fn).expect("sh call");
 
         let log_path = log_dir.join("jobs").join("go").join("sh-1.log");
         assert!(log_path.exists(), "expected sh-1.log at {log_path:?}");
@@ -810,7 +824,7 @@ mod tests {
         }));
         // No enter_job — current_job stays None.
 
-        let _: mlua::Value = run_fn.call(()).expect("sh call");
+        let _: mlua::Value = call_fn(&runtime, &run_fn).expect("sh call");
 
         assert_eq!(*count.borrow(), 0, "callback should not fire outside a job");
     }
@@ -820,7 +834,7 @@ mod tests {
     /// table as ShOutput.
     fn run_sh_via_job(source: &str) -> ShOutput {
         let (runtime, run_fn, _guard) = rt(source, HashMap::new());
-        let value: mlua::Value = run_fn.call(()).expect("sh call should return a value");
+        let value: mlua::Value = call_fn(&runtime, &run_fn).expect("sh call should return a value");
         runtime.lua().from_value(value).expect("decode ShOutput")
     }
 
@@ -843,7 +857,7 @@ mod tests {
         // assertion by writing the field directly.
         *runtime.current_job.borrow_mut() = Some("go".to_string());
 
-        let value: mlua::Value = run_fn.call(()).expect("sh call");
+        let value: mlua::Value = call_fn(&runtime, &run_fn).expect("sh call");
         let returned: ShOutput = runtime.lua().from_value(value).expect("decode");
 
         // The Lua caller still sees the raw value — echo printed it.
@@ -1066,8 +1080,8 @@ mod tests {
             git_dir = bare.display(),
         );
 
-        let (_runtime, run_fn, _guard) = rt(&source, secrets);
-        let _: mlua::Value = run_fn.call(()).expect("mirror should succeed");
+        let (runtime, run_fn, _guard) = rt(&source, secrets);
+        let _: mlua::Value = call_fn(&runtime, &run_fn).expect("mirror should succeed");
 
         // Tag landed in the target repo, pointing at the head SHA.
         let resolved = git(&["rev-parse", "refs/tags/v1"], &target);