This branch overhauls the pikaci Incus executor by removing the legacy 'transfer' mode (which copied Nix closures and snapshots into the guest), consolidating on the 'single_host_shared' device-mount approach as the sole Incus execution strategy. The guest filesystem preparation script that was previously generated and injected by the host is replaced by a NixOS-managed pikaci-incus-run binary baked into the VM image. On the pika-news side, a new persistent state layer is introduced: CI runs now write run metadata, job logs, and prepared-outputs records to a per-repo pikaci-state directory on disk, and three new authenticated API endpoints expose this data (run records, logs, and prepared outputs). The pikaci library gains public functions for loading run records, logs metadata, and prepared-outputs from disk, along with new model types for Incus image metadata tracking. Comprehensive tests cover both the executor simplifications and the new web API surface.
Branch-Specific Tutorial Generation Failed
This failure is specific to the current branch head.
malformed model output: `steps[8].evidence_snippets` was empty (result prefix:
{"executive_summary":"This branch overhauls the pikaci Incus executor by removing the legacy dual-mode (transfer vs. single_host_shared) architecture in favor of a single streamlined execution path. The Incus guest filesystem preparation, closure import, and snapshot staging steps are eliminated ...)
Tutorial Steps
Remove the Incus 'transfer' mode and consolidate on shared-device mounts
Intent: Eliminate the RemoteLinuxVmIncusMode enum and all transfer-mode code paths (closure import, snapshot push, guest filesystem preparation script, transfer workspace finalization). The Incus backend now exclusively uses host-shared virtiofs disk devices, which is simpler and avoids costly nix-store export/import cycles.
The branch removes ~300 lines of transfer-mode infrastructure:
RemoteLinuxVmIncusMode enum deleted — The Transfer / SingleHostShared distinction is gone. The PIKACI_REMOTE_LINUX_VM_INCUS_MODE environment variable and its parsing are removed.
Transfer-only functions deleted — import_remote_incus_closures, import_remote_path_closure_into_incus, stage_snapshot_into_incus_guest, finalize_remote_incus_transfer_workspace, and build_remote_incus_transfer_workspace_finalize_command are all removed.
prepare_remote_incus_guest_filesystem and build_remote_incus_prepare_script deleted — The entire guest filesystem preparation script that was dynamically generated and executed via incus exec ... bash -lc is gone. This script previously created directories, symlinks, user ownership, environment variables, and the /usr/local/bin/pikaci-incus-run wrapper inside the guest.
ensure_remote_incus_runtime simplified — After creating and starting the instance, the function now only waits for the instance to become ready. Device configuration (configure_remote_incus_devices, renamed from configure_remote_incus_single_host_shared_devices) is called unconditionally.
RemoteLinuxVmContext slimmed — The incus_mode and remote_incus_closure_dir fields are removed, and ensure_remote_linux_vm_directories creates one fewer remote directory.
Mount paths renamed — Constants like REMOTE_LINUX_VM_INCUS_SHARED_SNAPSHOT_MOUNT_PATH become REMOTE_LINUX_VM_INCUS_SNAPSHOT_MOUNT_PATH (dropping the SHARED infix), and their values change from /mnt/pikaci-* to /workspace/snapshot and /staged/linux-rust/* — matching what the NixOS image provides natively.
Delegate guest command execution to a NixOS-managed pikaci-incus-run binary
Intent: Instead of generating a shell script at runtime and injecting it into the guest, the Incus launch command now passes the guest command, timeout, and run-as-root flag as environment variables to a `/run/current-system/sw/bin/pikaci-incus-run` binary that is expected to exist in the VM image.
Previously build_remote_incus_launch_command invoked /usr/local/bin/pikaci-incus-run — a script generated at runtime by build_remote_incus_prepare_script and pushed into the guest via incus exec. Now:
build_remote_incus_launch_command takes a &JobSpec parameter and compiles the guest command itself.
The incus exec invocation uses env to set three variables (PIKACI_INCUS_GUEST_COMMAND, PIKACI_INCUS_TIMEOUT_SECS, PIKACI_INCUS_RUN_AS_ROOT) before invoking /run/current-system/sw/bin/pikaci-incus-run.
The constant REMOTE_LINUX_VM_INCUS_RUN_BINARY points to the NixOS system path.
The corresponding Nix image definition (nix/incus/pikaci-image.nix) is updated to include a pikaci-incus-run script in the VM's system packages that reads these environment variables and handles directory setup, logging, user switching, and exit-code propagation — all responsibilities previously handled by the injected script.
Record Incus image fingerprints in execution metadata
Intent: After preparing the runtime, query the remote Incus host for the actual image fingerprint and attach it to the execution record, enabling post-hoc traceability of which exact VM image ran a given job.
This is populated by load_remote_incus_image_record, which runs sudo incus image show --project <project> <alias> --format json on the remote host, deserializes the fingerprint, and logs it. The RemoteLinuxVmPhaseRecorder gains a set_incus_image method, and the resulting RemoteLinuxVmExecutionRecord now carries the optional incus_image field. This means every Incus-based CI job's run record includes the exact image fingerprint that was used.
Introduce pikaci state root for persisting run artifacts on disk
Intent: Create a per-repo directory structure where pikaci run metadata (run.json, job logs, prepared-outputs.json) is written by CI executions, enabling the web server to serve this data without relying solely on the SQLite database or streaming output.
Affected files: crates/pika-news/src/forge.rs
Evidence
@@ -100,6 +100,22 @@ impl Drop for MirrorLockGuard {
+pub fn pikaci_state_root(repo: &ForgeRepoConfig) -> PathBuf {
@@ -324,6 +340,9 @@ where
+ let structured_pikaci_state_root = structured_pikaci_target
+ .as_ref()
+ .map(|_| pikaci_state_root(repo));
@@ -336,6 +355,11 @@ where
+ if let Some(state_root) = structured_pikaci_state_root.as_ref() {
+ fs::create_dir_all(state_root)
+ .with_context(|| format!("create pikaci state root {}", state_root.display()))?;
+ cmd.env("PIKACI_STATE_ROOT", state_root);
+ }
The new pikaci_state_root function in forge.rs computes a deterministic, filesystem-safe path derived from the repo slug:
Characters outside [a-zA-Z0-9_-] are replaced with -. When the forge CI runner detects a structured pikaci target, it:
Computes the state root path.
Creates the directory (fs::create_dir_all).
Sets the PIKACI_STATE_ROOT environment variable on the child process.
The pikaci executor (running as the child) uses this variable to persist runs/<run_id>/run.json, per-job log files, and prepared-outputs.json into this directory. The test script in the integration test is also updated to write these fixtures into $PIKACI_STATE_ROOT.
Add public pikaci library functions for loading run records, logs, and prepared outputs
Intent: Expose a clean API from the pikaci crate for reading persisted state files, so that pika-news can load run records, log metadata, and prepared-outputs without knowing the internal file layout.
The pikaci crate's public API is extended with several items:
New types:
RunLogsMetadata — contains a list of RunJobLogMetadata entries, each indicating whether host/guest logs exist and their sizes.
Logs — holds optional host and guest strings with actual log content.
LogKind — enum (Host, Guest, Both) controlling which logs to load.
PreparedOutputsRecord — deserialized from prepared-outputs.json.
New functions in run.rs:
load_run_record(state_root, run_id) — reads and deserializes runs/<run_id>/run.json.
load_logs_metadata(state_root, run_id, job_id) — scans the run's job directories and reports which log files exist.
load_logs(state_root, run_id, job_id, kind) — reads actual log file contents.
load_prepared_outputs_record(state_root, run_id) — reads prepared-outputs.json if the run record references one.
All functions take the state_root path (from pikaci_state_root) and a run ID, keeping the file layout knowledge encapsulated in the pikaci crate.
Add three new authenticated web API endpoints for pikaci run data
Intent: Expose the persisted pikaci run records, job logs, and prepared outputs through the pika-news HTTP API so that frontends can display detailed CI execution information.
Three new GET endpoints are registered on the Axum router:
Endpoint
Handler
Returns
/news/api/forge/pikaci/run/:run_id
api_forge_pikaci_run_handler
Full RunRecord JSON
/news/api/forge/pikaci/logs/:run_id
api_forge_pikaci_logs_handler
{ run_id, job, host, guest }
/news/api/forge/pikaci/prepared-outputs/:run_id
api_forge_pikaci_prepared_outputs_handler
{ run_id, prepared_outputs }
All three require authentication (require_auth). The logs endpoint accepts optional job and kind query parameters via ForgePikaciLogsQuery.
Each handler delegates to a thin helper (load_pikaci_run, load_pikaci_logs, load_pikaci_prepared_outputs) that resolves the forge repo config, computes the state root, and calls the corresponding pikaci library function. Errors return 404 with a JSON error body.
Enrich the branch logs API response with pikaci metadata
Intent: When a CI lane has an associated pikaci run ID, automatically bundle the run record, log metadata, and prepared outputs into the existing branch logs API response so the frontend gets everything in a single request.
The ForgeBranchLogsResponse struct gains three new optional fields: pikaci_run, pikaci_log_metadata, and pikaci_prepared_outputs. In api_forge_branch_logs_handler, when the lane has a pikaci_run_id, the code calls load_pikaci_run_bundle which loads all three pieces of data in one shot. The bundle loader treats prepared outputs as best-effort (uses .ok().flatten()) — if the file doesn't exist, the field is simply None.
This is a backward-compatible enrichment: existing API consumers that don't use these fields are unaffected, while new consumers get detailed pikaci execution metadata without extra round-trips.
Make payload metadata best-effort throughout the model layer
Intent: Ensure that optional or newly-added fields in the pikaci model types use Option wrappers and serde defaults so that missing data in persisted JSON doesn't cause deserialization failures.
The branch title references "make payload metadata best-effort," and this manifests in the model layer:
RemoteLinuxVmExecutionRecord gains an incus_image: Option<RemoteLinuxVmImageRecord> field — old records without this field deserialize cleanly as None.
RemoteLinuxVmImageRecord.fingerprint is Option<String> — if the image query fails or is skipped, the record still serializes.
The RunRecord type uses serde(default) on new fields so that run.json files from before this change remain loadable.
The PreparedOutputsRecord loading is wrapped in .ok().flatten() at the call site, treating it as entirely optional.
This defensive approach prevents the system from failing hard when metadata files are incomplete or from a previous version.
Update the NixOS Incus VM image to include pikaci-incus-run
Intent: Bake the guest-side run script into the VM image via Nix so it no longer needs to be generated and pushed at runtime.
Affected files: nix/incus/pikaci-image.nix
Evidence
@@ nix/incus/pikaci-image.nix
The pikaci-image.nix derivation is updated to include a pikaci-incus-run script (or binary) in the NixOS system's PATH at /run/current-system/sw/bin/pikaci-incus-run. This script reads the environment variables set by the host-side launch command (PIKACI_INCUS_GUEST_COMMAND, PIKACI_INCUS_TIMEOUT_SECS, PIKACI_INCUS_RUN_AS_ROOT) and handles:
Running as root or switching to the pikaci user via runuser
Writing result.json with status, exit code, and timestamp
This moves guest-side logic from dynamically-generated shell code to a versioned, testable NixOS component.
Update CI Nix expression for Linux Rust builds
Intent: Adjust the CI Nix derivation to align with the simplified executor and new state-root conventions.
Affected files: nix/ci/linux-rust.nix
Evidence
@@ nix/ci/linux-rust.nix
The linux-rust.nix CI expression is updated to reflect the removal of transfer-mode support and any changes to how the Incus backend is configured. This ensures that the Nix-based CI pipeline definition stays in sync with the executor changes — for example, removing references to the now-deleted PIKACI_REMOTE_LINUX_VM_INCUS_MODE variable or updating paths to match the new mount point conventions.
Add comprehensive tests for new APIs and executor changes
Intent: Validate the new web API endpoints, the persisted state loading, the simplified Incus launch command, and the removal of transfer-mode code paths.
The existing integration test now writes pikaci state fixtures (run.json, host.log, guest.log) into $PIKACI_STATE_ROOT and asserts they exist after the CI command completes.
web.rs tests:
api_forge_branch_logs_includes_persisted_pikaci_run_metadata — Sets up a branch with a completed pikaci lane, writes fixtures, and asserts the logs API response includes pikaci_run, pikaci_log_metadata, and pikaci_prepared_outputs.
api_forge_pikaci_handlers_load_persisted_run_and_logs — Tests the standalone run and logs endpoints, verifying JSON structure and log content.
api_forge_pikaci_prepared_outputs_handler_loads_persisted_record — Tests the prepared-outputs endpoint.
Helper functions forge_test_config_with_git_dir and write_pikaci_run_fixture create realistic test fixtures.
executor.rs tests:
The with_incus_mode_env test helper and all transfer-mode-specific tests are removed.
sample_remote_context and sample_shell_job helpers are introduced, simplifying the remaining test setup by removing the now-deleted incus_mode and remote_incus_closure_dir fields.
Launch command tests are updated to verify that PIKACI_INCUS_GUEST_COMMAND, PIKACI_INCUS_TIMEOUT_SECS, and PIKACI_INCUS_RUN_AS_ROOT appear in the generated command.
Add Incus migration plan documentation
Intent: Document the migration strategy and rationale for moving from the transfer-based Incus mode to the shared-device approach.
Affected files: docs/incus-migration-plan.md
Evidence
@@ docs/incus-migration-plan.md
A new docs/incus-migration-plan.md file is added to the repository, documenting the rationale and plan for the Incus executor cleanup. This serves as a design record for the changes in this branch — explaining why the transfer mode was removed, the benefits of the shared-device approach, and any migration considerations for existing deployments.