feat(engine,cli): apply preflight halts with NIX_NOT_INSTALLED when needed#58
Merged
Conversation
…eeded Implements ADR-0012 decision 3: when a plan would dispatch a UserEnvSwitch but `nix --version` fails, apply halts at preflight (class 1, exit 2) before any snapshot or db sync. The CLI's hint is the literal `pearlite bootstrap` invocation. - pearlite-userenv::NixInstaller gains a `nix_installed()` probe method. LiveNixInstaller hoists the previously-private `nix_already_installed` body to satisfy it; `install_if_missing` now calls the public method instead. MockNixInstaller returns its `already` flag, so existing test fixtures are unchanged in behavior. - pearlite-engine::ApplyContext gains a `nix_installer` field. The apply_plan body checks `needs_nix(plan)` (any UserEnvSwitch action) and probes via the adapter before pre-snapshot. New ApplyError::NixNotInstalled (preflight) and ApplyError::NixProbe (#[from] InstallerError) variants surface the failure modes. - CLI dispatch's apply_error_payload maps the two variants to typed codes NIX_NOT_INSTALLED and APPLY_NIX_PROBE_FAILED with class "preflight" / exit 2, overriding the recorded-failure-class default since these errors halt before any FailureRef is written. The hint for NIX_NOT_INSTALLED is `pearlite bootstrap --installer-script <path>`. - 20 engine test sites updated via make_ctx to thread the new field; the 3 injected-failure tests update their ApplyContext literals. - Two new engine tests cover the preflight branch (halts before snapshot, no FailureRef) and the no-UserEnvSwitch path (nix probe is skipped entirely). Tests: 316 passing (+2). Clippy clean. fmt clean. audit clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Apr 28, 2026
UnbreakableMJ
added a commit
that referenced
this pull request
May 5, 2026
* docs(todo): refresh post-#65 reconcile + bootstrap status TODO.md was 8 days stale. Marks the M4 W1 reconcile read-side (Engine::reconcile, #65) as done; resolves the M3 W1 runuser / per-user nix.conf line as the bootstrap stack (#55-#58) plus ADR-0013 (Home Manager owns per-user nix.conf); updates the M3 W2 vm-09 prose to reflect that vm-09-nix-bootstrap.sh shipped in M4 W1 (#59) rather than remaining deferred. Bumps Last updated to 2026-05-04. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): pearlite reconcile subcommand wires Engine::reconcile Read-only CLI surface for the M4 W1 reconcile read-side that landed in #65. Adds Command::Reconcile (no flags), dispatch_reconcile, reconcile_outcome_view, and reconcile_error_payload mapping every pearlite_engine::ReconcileError variant to a typed error code: RECONCILE_PROBE_FAILED — probe adapter failure RECONCILE_EMPTY_HOSTNAME — /etc/hostname is blank RECONCILE_INVALID_HOSTNAME — `/`, `\`, or NUL in hostname RECONCILE_ALREADY_EXISTS — refuses to clobber operator review RECONCILE_IO_FAILED — mkdir or atomic-write failure All five are class=preflight, exit_code=2 — reconcile is read-only with respect to state.toml; the only system-side effect is the atomic write of <config_dir>/hosts/<hostname>.imported.ncl, and a failed write leaves the operator's config repo untouched (tempfile is dropped before rename). Three dispatch tests cover the happy path (writes the imported.ncl to disk and returns hostname + path in the envelope), the already-exists guard (pre-seeded file is preserved verbatim), and the metadata.command label. Out of scope for this chunk: - `--commit` and `--adopt-all` flags (need Engine::reconcile_commit) - vm-10-reconcile-fresh-install.sh Refs: PRD §11, Plan §7.5 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(cli): rustfmt reconcile dispatch tests Two mechanical wrappings flagged by `cargo fmt --all --check`: - assert! is_file() chain breaks across lines - let preserved = ... fits on a single line at 100 cols No behavior change. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(cli): extract dispatch_plan_or_status to fit too_many_lines Adding `Command::Reconcile` pushed `dispatch()` to 101 lines (clippy limit is 100). Pulled the inline Plan/Status arm into `dispatch_plan_or_status`, mirroring the existing extraction pattern for Apply, Bootstrap, Reconcile, etc. Behavior is identical; read-only test run via cargo test -p pearlite-cli passes 42 tests under WSL Arch. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(vm): vm-10 reconcile-fresh-install scenario Verifies the read-side reconcile pipeline end-to-end against the `pearlite reconcile` CLI shipped earlier in this branch. Phase A (happy path): - exit 0 - envelope: command=pearlite reconcile, hostname non-empty, imported_path resolves to <sandbox>/repo/hosts/<hostname>.imported.ncl - on-disk file contains the Nickel record markers emit_host produces (meta = {, kernel = {, packages = {, services = {) Phase B (clobber refusal): - re-running with the same --config-dir exits 2 with RECONCILE_ALREADY_EXISTS, class=preflight - the original .imported.ncl is byte-identical to the Phase A capture (cmp -s) Whitelisted alongside vm-01 in scripts/ci/run-vm-tests.sh -- vm-10 is read-only with respect to system state (only mutation is a single Nickel file inside a tempdir), so it runs unconditionally without PEARLITE_VM_TEST=1. Verified locally via WSL Arch: bash tests/vm/vm-10-reconcile-fresh-install.sh -> PASS Refs: PRD §11, Plan §7.5 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(todo): mark vm-10 reconcile-fresh-install done Shipped earlier in this branch (commit 3d22c36) and verified locally via WSL Arch. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(agents): document pearlite reconcile flow + error codes Adds a Reconcile-flow section enumerating the five RECONCILE_* error codes and the read-only / interactive split, so future agents can discover the surface without re-deriving it from dispatch.rs. Notes the users / config empty-array placeholders as intentional per PRD §11, references vm-10 for end-to-end coverage, and points at the M4 W1 remainder (reconcile --commit). Bumps Last updated to 2026-05-04. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements ADR-0012 decision 3 — when a plan would dispatch a
UserEnvSwitchbutnix --versionfails, apply halts at preflight (class 1, exit 2) before any snapshot or db sync. The CLI's hint is the literalpearlite bootstrapinvocation.pearlite-userenv::NixInstallergains anix_installed()probe method.LiveNixInstallerhoists the previously-privatenix_already_installedbody to satisfy it;install_if_missingnow calls the public method.MockNixInstallerreturns itsalreadyflag — existing test fixtures unchanged in behaviour.pearlite-engine::ApplyContextgains anix_installerfield.apply_plannow checksneeds_nix(plan)(anyUserEnvSwitchaction) and probes via the adapter before pre-snapshot. NewApplyError::NixNotInstalledandApplyError::NixProbe(#[from] InstallerError)variants.apply_error_payloadmaps these toNIX_NOT_INSTALLEDandAPPLY_NIX_PROBE_FAILEDwith class=preflight, exit=2, overriding the recorded-failure-class default since both halt before anyFailureRefis written.make_ctx; the 3 injected-failure tests update theirApplyContextliterals.FailureRef) and the no-UserEnvSwitchpath (nix probe skipped entirely).Test plan
cargo test --workspace --all-features— 316 passing (+2 new)cargo clippy --workspace --all-targets --all-features -- -D warnings— cleancargo fmt --all -- --check— cleanscripts/ci/check-spdx.sh— cleanpearlite-audit check .— 1 check, 0 violations🤖 Generated with Claude Code