Skip to content

feat(engine,cli): apply preflight halts with NIX_NOT_INSTALLED when needed#58

Merged
UnbreakableMJ merged 1 commit into
mainfrom
feat/apply-nix-preflight
Apr 28, 2026
Merged

feat(engine,cli): apply preflight halts with NIX_NOT_INSTALLED when needed#58
UnbreakableMJ merged 1 commit into
mainfrom
feat/apply-nix-preflight

Conversation

@UnbreakableMJ

Copy link
Copy Markdown
Contributor

Summary

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. MockNixInstaller returns its already flag — existing test fixtures unchanged in behaviour.
  • pearlite-engine::ApplyContext gains a nix_installer field. apply_plan now checks needs_nix(plan) (any UserEnvSwitch action) and probes via the adapter before pre-snapshot. New ApplyError::NixNotInstalled and ApplyError::NixProbe(#[from] InstallerError) variants.
  • CLI's apply_error_payload maps these to NIX_NOT_INSTALLED and APPLY_NIX_PROBE_FAILED with class=preflight, exit=2, overriding the recorded-failure-class default since both halt before any FailureRef is written.
  • 20 engine test sites updated via make_ctx; 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 skipped entirely).

Test plan

  • cargo test --workspace --all-features — 316 passing (+2 new)
  • cargo clippy --workspace --all-targets --all-features -- -D warnings — clean
  • cargo fmt --all -- --check — clean
  • scripts/ci/check-spdx.sh — clean
  • pearlite-audit check . — 1 check, 0 violations
  • CI green on T1 / T2 / T3

🤖 Generated with Claude Code

…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>
@UnbreakableMJ
UnbreakableMJ merged commit 41b39a7 into main Apr 28, 2026
3 checks passed
@UnbreakableMJ
UnbreakableMJ deleted the feat/apply-nix-preflight branch April 28, 2026 22:41
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant