Back to feed

sledtools/pika branch #151

fix-nightly

Fix current nightly CI regressions

Target branch: master

Merge Commit: 8fa28e42295e344d68b22f3c4a9f6620e8415e5e

branch: merged tutorial: ready ci: success
Open CI Details

Continuous Integration

CI: success

Compact status on the review page, with full logs on the CI page.

Open CI Details

Latest run #188 success

10 passed

head f1f62083a153a8b6d5e8737b884dfa537dc06d4e · queued 2026-03-28 12:01:40 · 10 lane(s)

queued 14s · ran 1m 46s

check-pika-rust · success check-pika-followup · success check-notifications · success check-agent-contracts · success check-rmp · success check-pikachat · success check-pikachat-typescript · success check-apple-host-sanity · success check-pikachat-openclaw-e2e · success check-fixture · success

Summary

This branch fixes multiple nightly CI regressions in the sledtools/pika repository. The changes address three distinct failure categories: (1) missing native build dependencies for the nokhwa camera crate by provisioning libclang, linuxHeaders, and BINDGEN_EXTRA_CLANG_ARGS in the default Nix dev shell; (2) a missing cargo build -p pikachat step in the nightly-pika-e2e Just recipe, which caused daemon-boundary integration tests to fail because the pikachat binary was not present in target/debug/; and (3) adapting the pikachat-openclaw extension to upstream SDK breaking changes—namely, relocated imports (DEFAULT_ACCOUNT_ID, ChannelThreadingToolContext) and a renamed plugin action method (listActionsdescribeMessageTool). Each fix is accompanied by a new guardrail test that will fail loudly if the invariant is violated again, preventing silent regressions in future CI runs.

Tutorial Steps

Provision bindgen dependencies in the default Nix dev shell

Intent: The nightly CI lane runs inside `nix develop .#default`, which lacked `linuxHeaders` and the `BINDGEN_EXTRA_CLANG_ARGS` environment variable. When `pika-desktop` depends on `nokhwa` (a camera crate using bindgen), the build fails because bindgen cannot locate kernel and libc headers. This step adds those inputs and exports to the default Linux dev shell.

Affected files: flake.nix

Evidence
@@ -520,6 +520,11 @@
+        linuxBindgenIncludeArgs =
+          if pkgs.stdenv.isLinux then
+            "-I${pkgs.linuxHeaders}/include -I${pkgs.stdenv.cc.libc.dev}/include"
+          else
+            "";
@@ -738,6 +743,7 @@
+            pkgs.linuxHeaders
@@ -770,6 +787,7 @@
+              export BINDGEN_EXTRA_CLANG_ARGS="${linuxBindgenIncludeArgs}''${BINDGEN_EXTRA_CLANG_ARGS:+ $BINDGEN_EXTRA_CLANG_ARGS}"

The nokhwa camera crate (a dependency of pika-desktop) uses bindgen at build time to generate Rust FFI bindings for V4L2 headers. On NixOS/Nix-shell environments the standard system include paths are absent, so bindgen cannot find <linux/videodev2.h> or libc headers.

Three changes are made to flake.nix:

  1. A new let binding linuxBindgenIncludeArgs constructs the -I flags pointing at pkgs.linuxHeaders and pkgs.stdenv.cc.libc.dev (Linux-only; empty string on macOS).
  2. pkgs.linuxHeaders is added to the buildInputs list alongside the existing pkgs.llvmPackages.libclang so the headers are physically available in the shell.
  3. BINDGEN_EXTRA_CLANG_ARGS is exported in the shell hook, prepending the computed include args while preserving any pre-existing value.

These three pieces work together: libclang provides the clang library that bindgen links against, linuxHeaders provides the kernel UAPI headers, and the environment variable tells bindgen where to find them.

Export the Android AAPT2 override for nightly Gradle builds

Intent: Nightly CI runs Android integration tests inside the same default dev shell. The Nix-provided Android SDK ships `aapt2` at a path that Gradle does not discover automatically, causing Android resource compilation to fail. This step detects the installed `aapt2` binary and exports `PIKACI_ANDROID_AAPT2_OVERRIDE` plus the corresponding Gradle system property.

Affected files: flake.nix

Evidence
@@ -759,6 +765,17 @@
+            unset PIKACI_ANDROID_AAPT2_OVERRIDE
+            for candidate in \
+              "$ANDROID_HOME/build-tools/35.0.0/aapt2" \
+              "$ANDROID_HOME/build-tools/34.0.0/aapt2"
+            do
+              if [ -x "$candidate" ]; then
+                export PIKACI_ANDROID_AAPT2_OVERRIDE="$candidate"
+                export GRADLE_OPTS="''${GRADLE_OPTS:+$GRADLE_OPTS }-Dorg.gradle.project.android.aapt2FromMavenOverride=$PIKACI_ANDROID_AAPT2_OVERRIDE"
+                break
+              fi
+            done

When the Nix-managed Android SDK is present, Gradle cannot resolve aapt2 from Maven because the Nix sandbox blocks network fetches. The fix adds a candidate loop in the shell hook that checks for aapt2 under build-tools 35.0.0 and 34.0.0 (in preference order). When found, it:

  • Exports PIKACI_ANDROID_AAPT2_OVERRIDE for any scripts that reference it directly.
  • Appends -Dorg.gradle.project.android.aapt2FromMavenOverride=... to GRADLE_OPTS, which tells the Android Gradle Plugin to use the local binary instead of downloading one from Google's Maven repository.

The unset at the top ensures the variable is clean if the shell hook runs more than once.

Add guardrail tests for the Nix dev shell invariants

Intent: To prevent these Nix regressions from silently reappearing, two new unit tests are added to `jerichoci::model::tests`. They parse `flake.nix` and assert the presence of the critical strings, giving a clear failure message that links cause to fix.

Affected files: crates/jerichoci/src/model.rs

Evidence
@@ -1545,6 +1545,34 @@
+    #[test]
+    fn default_linux_dev_shell_provisions_bindgen_for_nightly_camera_dependencies() {
+        let flake = fs::read_to_string(workspace_root().join("flake.nix")).expect("read flake.nix");
+        let desktop_manifest =
+            fs::read_to_string(workspace_root().join("crates/pika-desktop/Cargo.toml"))
+                .expect("read pika-desktop Cargo.toml");
+
+        if desktop_manifest.contains("nokhwa") {
+            assert!(
+                flake.contains("pkgs.llvmPackages.libclang")
+                    && flake.contains("pkgs.linuxHeaders")
+                    && flake.contains("export BINDGEN_EXTRA_CLANG_ARGS="),
@@ -1545,6 +1545,34 @@
+    #[test]
+    fn default_linux_dev_shell_exports_android_aapt2_override_for_nightly() {
+        let flake = fs::read_to_string(workspace_root().join("flake.nix")).expect("read flake.nix");
+
+        assert!(
+            flake.contains("PIKACI_ANDROID_AAPT2_OVERRIDE")
+                && flake.contains("android.aapt2FromMavenOverride"),

Two new #[test] functions in crates/jerichoci/src/model.rs act as living documentation and regression detectors:

default_linux_dev_shell_provisions_bindgen_for_nightly_camera_dependencies

This test is conditional: it only asserts when pika-desktop/Cargo.toml still lists nokhwa as a dependency. When active, it checks that flake.nix contains:

  • pkgs.llvmPackages.libclang (the clang library for bindgen)
  • pkgs.linuxHeaders (kernel UAPI headers)
  • export BINDGEN_EXTRA_CLANG_ARGS= (the environment variable that passes include paths)

default_linux_dev_shell_exports_android_aapt2_override_for_nightly

This test unconditionally verifies that flake.nix references both PIKACI_ANDROID_AAPT2_OVERRIDE and android.aapt2FromMavenOverride, ensuring the Gradle override path remains wired up.

Both tests produce descriptive failure messages explaining why the invariant matters and which CI lanes depend on it.

Add `cargo build -p pikachat` to the nightly-pika-e2e recipe

Intent: The `call_with_pikachat_daemon_boundary` integration test expects a pre-built `pikachat` binary at `target/debug/pikachat`. The Just recipe was missing the explicit build step, causing the test to fail with a 'binary not found' error on clean CI runners.

Affected files: just/checks.just

Evidence
@@ -254,6 +254,7 @@
 nightly-pika-e2e:
+    cargo build -p pikachat
     cargo test -p pikahut --test integration_deterministic call_over_local_moq_relay_boundary -- --ignored --nocapture

The nightly-pika-e2e recipe in just/checks.just runs several --ignored integration tests that exercise heavy call-path regression boundaries. The call_with_pikachat_daemon_boundary test spawns pikachat as a child process, expecting the binary to already exist at target/debug/pikachat.

On a clean CI runner (or after cargo clean), the binary does not exist, and the test fails with a confusing "No such file or directory" error rather than a meaningful build failure.

The fix adds cargo build -p pikachat as the first line of the recipe, ensuring the binary is compiled before any test attempts to spawn it. This is intentionally a separate build step (not cargo test --build) because the daemon boundary test locates the binary by path, not through the test harness.

Add a guardrail test for the pikachat build step

Intent: A new test in `pikahut/tests/guardrails.rs` parses the Just recipe and asserts that `cargo build -p pikachat` appears before the daemon boundary test commands, preventing the build step from being accidentally removed in the future.

Affected files: crates/pikahut/tests/guardrails.rs

Evidence
@@ -416,6 +416,26 @@
+#[test]
+fn nightly_pika_lane_builds_pikachat_binary_before_daemon_boundary() -> Result<()> {
+    let root = workspace_root();
+    let checks = fs::read_to_string(root.join("just/checks.just"))?;
+    let recipe = extract_just_recipe_body(&checks, "nightly-pika-e2e");
+
+    assert!(
+        !recipe.is_empty(),
+        "checks.just must keep a checked-in nightly-pika-e2e recipe body"
+    );
+    assert!(
+        recipe
+            .iter()
+            .any(|line| line.contains("cargo build -p pikachat")),
+        "nightly-pika-e2e must build pikachat before the daemon-boundary selector expects target/debug/pikachat"
+    );

This guardrail test uses the existing extract_just_recipe_body helper to parse the nightly-pika-e2e recipe out of just/checks.just and verify two invariants:

  1. The recipe body is non-empty (the recipe hasn't been accidentally deleted).
  2. At least one line contains cargo build -p pikachat.

The assertion message explicitly states why the build step is needed ("the daemon-boundary selector expects target/debug/pikachat"), making future debugging straightforward if someone removes the line.

Adapt pikachat-openclaw imports to upstream SDK changes

Intent: The upstream `openclaw/plugin-sdk` moved `DEFAULT_ACCOUNT_ID` to `openclaw/plugin-sdk/account-id`, moved `ChannelThreadingToolContext` to `openclaw/plugin-sdk/channel-contract`, removed the `formatPairingApproveHint` export, and renamed `listActions` to `describeMessageTool` with a new return shape. These import and API changes broke compilation of the pikachat-openclaw extension.

Affected files: pikachat-openclaw/openclaw/extensions/pikachat-openclaw/src/channel.ts, pikachat-openclaw/openclaw/extensions/pikachat-openclaw/src/types.ts

Evidence
@@ -1,12 +1,11 @@
 import {
-  DEFAULT_ACCOUNT_ID,
-  formatPairingApproveHint,
   type AnyAgentTool,
   type ChannelMessageActionContext,
   type ChannelMessageActionName,
   type ChannelPlugin,
-  type ChannelThreadingToolContext,
 } from "openclaw/plugin-sdk";
+import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
+import type { ChannelThreadingToolContext } from "openclaw/plugin-sdk/channel-contract";
@@ -118,6 +117,10 @@
+function formatPairingApproveHint(channelId: string): string {
+  return `Approve via: \`openclaw pairing list ${channelId}\` / \`openclaw pairing approve ${channelId} <code>\``;
+}
@@ -740,7 +743,9 @@
   actions: {
-    listActions: (): ChannelMessageActionName[] => ["react"],
+    describeMessageTool: () => ({
+      actions: ["react"] as const,
+    }),
@@ -1,4 +1,5 @@
-import { DEFAULT_ACCOUNT_ID, type OpenClawConfig } from "openclaw/plugin-sdk";
+import type { OpenClawConfig } from "openclaw/plugin-sdk";
+import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";

The upstream openclaw/plugin-sdk package introduced breaking changes that affected the pikachat-openclaw extension in three ways:

Import path relocations

DEFAULT_ACCOUNT_ID moved from openclaw/plugin-sdk to openclaw/plugin-sdk/account-id. Both channel.ts and types.ts are updated to import from the new path. Similarly, ChannelThreadingToolContext moved to openclaw/plugin-sdk/channel-contract.

Removed export: formatPairingApproveHint

The SDK no longer exports this helper. Since the function is small and specific to the pikachat pairing flow, it is inlined directly in channel.ts as a local function with the same signature and behavior.

Renamed action method: listActionsdescribeMessageTool

The ChannelPlugin.actions contract changed from a listActions() method returning ChannelMessageActionName[] to a describeMessageTool() method returning an object with an actions array. The implementation is updated accordingly, using as const for type narrowing on the string literal array.

Diff