Skip to content

[phase-1] cleanup: journaled legacy migration - #69

Merged
tonythethompson merged 50 commits into
masterfrom
pr-migrate-legacy-installs
Aug 3, 2026
Merged

[phase-1] cleanup: journaled legacy migration#69
tonythethompson merged 50 commits into
masterfrom
pr-migrate-legacy-installs

Conversation

@tonythethompson

@tonythethompson tonythethompson commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

Promotes the legacy single-binary Nu install → versioned-layout transition to a journaled transaction (PreparedRenamedActive) tracked at state/migration-journal.json. Reconciliation lives at two layers: a self-healing reconcile(root)? at the top of every migrate_legacy_install_with_detector call, plus an explicit numan doctor --fix (Auto-tier) catch-up for users who never called numan use.

This branch is the second half of the split that landed alongside #67 — the numan-use feature PR stays small, the journaled migration lands as its own phase-1 cleanup PR.

Changes

New: src/nu/migrate_legacy.rs

Extracted from nu::version_manager:

  • pub type LegacyVersionDetector — injectable version-detection seam for tests.
  • pub type LegacyPostCreateHook — fired after create_dir_all(<version>/) and before the rename, simulating the original cross-device-rename bug.
  • pub fn detect_legacy_version(binary) — production detector (VERSION metadata file preferred, fall back to nu --version).
  • pub fn migrate_legacy_install(root) and pub fn migrate_legacy_install_with_detector(root, detect, post_create) — journaled migration with self-healing reconcile at entry.
  • fn parse_nu_version_from_output(output) (private helper).
  • 11 tests spanning: skip-when-no-legacy, skip-when-any-versioned-dir-present-but-cleanup-the-empty-sibling, post-create-hook recovery, partial-failure proceed, journal-cleared-on-success, Created-only journal reconcile, version-metadata-file detection, etc.

New: src/state/migration_journal.rs

The journal file format, lifecycle stages, and the reconcile(root) self-heal implementation.

  • SCHEMA_VERSION, PendingMigration { schema_version, version, stage }, MigrationStage::{ Prepared, Renamed, Active }.
  • PendingMigration::save / delete / load against state/migration-journal.json (write-after-mkdir-stub via atomic::write_json_atomic).
  • reconcile(root) — for each Prepared-only entry: remove the orphan <root>/tools/nushell/<version>/ subdir the failed create_dir_all left behind, then delete the journal. File-system truth takes precedence over journal stage.

Updated: src/cmd/doctor.rs

  • New finding journal.migration_pending (Severity::Warn, RepairTier::Auto, fix hint numan use).
  • New repair path during apply_repairs: when the finding is present AND PendingMigration::load(...).is_some(), calls migration_journal::reconcile(root) and records the result as journal.migration_repaired.
  • Two new unit tests: doctor_reports_migration_journal_finding and doctor_fix_reconciles_migration_journal.

Updated: src/util/hints.rs

  • pub const CMD_USE: &str = "numan use"; — fix-hint for the migration journal finding.

Updated: src/state/mod.rs

  • pub mod migration_journal;

Updated: src/nu/version_manager.rs

  • write_active_marker is pub(crate) (consumed by migrate_legacy::migrate_legacy_install_with_detector). All other migration-related code and tests moved out.

Updated: src/cmd/use_cmd.rs

  • Once this branch ships, numan use should call crate::nu::migrate_legacy::migrate_legacy_install(root) (and write_active_marker stays as the public hook).

Testing

  • cargo test --lib — 468 passed (26 added: 24 migrate_legacy_* + 2 doctor migration-finding).
  • cargo clippy -- -D warnings clean.
  • cargo fmt --check clean.

Architecture Notes

The migration is a journaled transaction with two explicit stages and three transitions:

  • Prepared written before create_dir_all(<version>/).
  • Renamed written after the legacy binary has been moved into <version>/<bin>.
  • Active reached after write_active_version(root, &version) succeeds.
  • On reaching Active, the journal is deleted.
  • If a process is killed mid-flight, the next numan use invocation or numan doctor --fix reconciles by removing the orphan empty <version>/ directory the failed create_dir_all left behind, then clearing the journal — never auto-deleting a populated install.

This is consistent with Numan's wider journal pattern (journal.rs, autoload_journal.rs, lifecycle_journal.rs, plugin_deactivate_journal.rs): on-disk state is the source of truth, and journals exist to detect interrupted transitions, not to bypass the filesystem.

Future Work

  • Extend reconcile to pick up a Renamed-only entry by completing the write_active_version step when the on-tree binary exists (i.e., a fully renamed but unactivated install).
  • Add CI acceptance matrix entries that toggle the journal during fixture migration to exercise every Prepared-only, Renamed-only, and Active-reached state.
  • Wire the write_active_marker migration call into a follow-up PR that re-adds the migrate line in cmd/use_cmd.rs (post-merge gating by feature flag, then unconditionally after one release cycle).

Review in cubic

tonythethompson and others added 16 commits July 31, 2026 22:20
…nfirm utility

Breaking change: --remove, --use-path, --use-existing are replaced by subcommands (remove, path, use <path>). Hidden backward-compat flags emit deprecation warnings (remove in v0.3.0).

- Add src/util/confirm.rs: confirm_or_auto / confirm_or_bail (non-TTY auto-confirms)

- Replace 16 inline TTY-check sites across 8 files

- Normalize all --yes help text to 'Skip confirmation prompts'

- Redesign NuSetupArgs with Option<NuAction> subcommand + positional VERSION

- Add NuSetupArgs constructors for internal callers (doctor, nu_pin_offer)

- Add 8 CLI-parse tests + update all affected test assertions

- Update README, CHANGELOG, docs/numan-doctor.md, AGENTS.md
- Prevent PATH subcommand from deleting active managed Nu
- Guard loader overwrites with ownership verification
- Reject incompatible legacy Nu setup flags
Fix remaining --version references in resolve.rs, tighten doctor_test assertion, add skip_path guard to legacy compat path, add negative tests for version+subcommand and legacy use_existing+skip_path.
Change 'your PATH Nu is not touched' to 'your existing Nu is not replaced' since setup nu does modify PATH by default.
Introduce a new `numan use <version>` command path (`cli`, `main`, and `cmd::use_cmd`) as a post-1.0 placeholder that currently fails with a clear guidance message to use `numan setup nu <version>`. Update roadmap/docs to reflect the consolidated plan filename, add post-1.0 side-by-side Nu management notes, and register the new command module in AGENTS metadata.
Fixed 4 file(s) based on 5 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
- Add src/nu/version_manager.rs for active version tracking
  - Active version stored in <root>/nu_state/active-version.json
  - Installed versions discovered from <root>/tools/nushell/<version>/
  - Helpers: read/write active version, list installed, check if installed
  - Migration logic for legacy single-binary installs

- Update bootstrap.rs to install to versioned subdirectories
  - install_from_archive() now writes to <root>/tools/nushell/<version>/
  - managed_nu_binary() delegates to version_manager for active version
  - Automatically sets newly installed version as active

- Implement numan use command
  - numan use <version> — switch to specific installed version
  - numan use latest — switch to newest installed version
  - numan use list — show all installed versions with active marker
  - Validates version is installed before switching
  - Provides helpful hints when version not found
  - Auto-migrates legacy single-binary installs on first run

- Remove Commands::Use from root init exclusion (now needs root)

All 425 tests pass, clippy clean, fmt applied.
- Validate and normalize Nu versions
- Propagate legacy migration errors
- Handle invalid active markers gracefully
- Avoid parent path panic
- Update Nu setup guidance
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
…master for setup.rs, snapshot.rs, cli.rs, bootstrap.rs; keep full use_cmd.rs implementation
wip: puts numan use + confirm-gate UX + migration journal + reconcile
into a single baseline commit so the pr-migrate-legacy-installs split can
branch off a known starting point. This commit does not represent either
final PR; the next step surgically extracts migration into its own branch
and reverts the migration hunks on feature/numan-use.
- Pull 251 lines of migration fs (Legacy*Detector type alias,
  LegacyPostCreateHook type alias, detect_legacy_version,
  migrate_legacy_install, migrate_legacy_install_with_detector,
  parse_nu_version_from_output) plus 8 migrate_legacy_* tests +
  create_legacy_binary helper + production_detector_prefers_version_metadata_file
  + test_parse_nu_version_from_output into a new crate::nu::migrate_legacy
  module.
- Add 'pub(crate)' to write_active_marker (now consumed by
  crate::nu::migrate_legacy).
- Retarget use_cmd.rs to call crate::nu::migrate_legacy::migrate_legacy_install.
- Re-register the module in src/nu/mod.rs.

This is the pr-migrate-legacy-installs half of the split: phase 1
cleanup + journaled transaction for legacy single-binary -> versioned
installation with self-healing and numan doctor --fix reconciliation.

Co-authored-by: Codebuff <noreply@codebuff.com>
Copilot AI review requested due to automatic review settings August 2, 2026 10:50

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @tonythethompson, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds journaled migration from legacy single-binary Nu installations to versioned directories. It adds recovery, typed version-state errors, version switching, mutation locking, setup safeguards, and doctor reporting and repair.

Changes

Nu version management

Layer / File(s) Summary
Active version state and executable resolution
src/nu/..., src/state/mod.rs, src/util/hints.rs
Active-marker and installed-version errors now use typed handling. Installed versions require canonical names and regular binaries. Active binary paths reject parent-directory components.
Journaled legacy migration and reconciliation
src/state/migration_journal.rs, src/nu/migrate_legacy.rs, AGENTS.md
Legacy installations use Prepared, Renamed, and Active journal stages. Reconciliation uses filesystem state, removes orphan directories and stray binaries, preserves active selections, and supports retries after failures.
Version switching workflow
src/cmd/use_cmd.rs, src/util/fs_safety.rs, src/util/test_paths.rs
numan use list remains read-only. Mutation commands snapshot state, reconcile legacy installations, and resolve the requested version under the mutation lock.
Setup and bootstrap safeguards
src/cmd/setup.rs, src/nu/bootstrap.rs
Setup normalizes versions, prevents payload overwrites, requires TTY confirmation or --yes, validates external binary containment, clears markers before managed removal, and isolates PATH mutations in tests.
Doctor migration repair reporting
src/cmd/doctor.rs, docs/numan-doctor.md, tests/doctor_test.rs
doctor reports pending or unreadable journals and invalid active markers. doctor --fix reconciles pending journals under the mutation lock and records repair outcomes.

Estimated code review effort: 5 (Critical) | ~100 minutes

Possibly related PRs

Suggested reviewers: greptile-apps

🚥 Pre-merge checks | ✅ 8
✅ Passed checks (8 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: journaled migration for legacy installations.
Description check ✅ Passed The description directly explains the journaled migration, reconciliation, supporting modules, tests, and follow-up work.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 60.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Pipeline Stage Enum Ordering ✅ Passed The PR contains no SessionWorkflowStage enum, member, or comparison reference; it adds unrelated MigrationStage members only, so this ordering check is not applicable.
Gpu/Cpu Runtime Boundary ✅ Passed The PR changes only Rust, Markdown, and test files; no inference/, managed requirements, main.py, or C# paths are modified, so the runtime-boundary check does not apply.
Managed Host Restart Safety ✅ Passed The PR changes only Numan migration, setup, doctor, and filesystem modules; none of the four managed-host components or restart/lease symbols exist in the PR diff or repository.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pr-migrate-legacy-installs
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch pr-migrate-legacy-installs

Warning

Review ran into problems

🔥 Problems

Linked repositories: Public OSS repositories can only analyze public repositories installed in this organization. Analyzed tonythethompson/QuickShell, tonythethompson/numan, tonythethompson/dependency-chain-substrate, skipped Trackdubllc/Trackdub.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a journaled, crash-recoverable migration for legacy single-binary Nushell installs and expands the Nu version-management surface (numan use, active-version marker, marker-aware Nu discovery) while wiring migration reconciliation into doctor --fix.

Changes:

  • Adds a new migration journal (state/migration-journal.json) with Prepared → Renamed → Active stages and a reconcile(root) self-heal path.
  • Adds legacy-install migration logic (nu/migrate_legacy.rs) and integrates it into numan use plus numan doctor --fix.
  • Introduces/updates active Nu version tracking (nu_state/active-version.json) and marker-aware Nu resolution (nu/paths.rs).

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/util/hints.rs Adds CMD_USE hint text describing numan use and its migration reconciliation role.
src/util/confirm.rs Adds require_tty_or_yes() to fail-closed destructive setup flows in non-TTY sessions without --yes.
src/state/mod.rs Exposes the new migration_journal module.
src/state/migration_journal.rs New journal format + reconcile(root) recovery logic and unit tests.
src/nu/version_manager.rs Active-version marker read/write + installed-version discovery + active binary resolution logic.
src/nu/paths.rs Consults the active-version marker as a hint table during Nu executable discovery (after legacy-managed check).
src/nu/mod.rs Registers new migrate_legacy and version_manager modules.
src/nu/migrate_legacy.rs New journaled migration from legacy tools/nushell/nu to versioned layout with DI seams and tests.
src/nu/bootstrap.rs Adds snapshotting + non-TTY consent guard + (pinned-only) active-marker persistence; adjusts PATH prompt hoisting.
src/cmd/use_cmd.rs Implements `numan use
src/cmd/setup.rs Adds binary validation and consolidated destructive confirmation for setup nu use/path flows via DI seams/tests.
src/cmd/doctor.rs Adds journal.migration_pending finding and an Auto-tier repair that runs migration reconcile.
docs/plans/consolidated-multi-repo-roadmap.md Updates roadmap semantics: numan use never auto-downloads; documents marker shape and ownership.
AGENTS.md Documents new migration journal + active-version marker invariants.
.gitignore Adds /.freebuff ignore entry.

Comment thread src/cmd/setup.rs Outdated
Comment thread src/state/migration_journal.rs
Comment thread src/state/migration_journal.rs Outdated
Comment thread src/nu/migrate_legacy.rs Outdated
Comment thread src/cmd/use_cmd.rs
Comment thread src/nu/bootstrap.rs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e0e467e842

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/nu/bootstrap.rs Outdated
Comment thread src/cmd/use_cmd.rs Outdated
Comment thread src/nu/migrate_legacy.rs Outdated
Comment thread src/cmd/setup.rs Outdated
Comment thread src/cmd/setup.rs
Comment thread src/cmd/doctor.rs Outdated
Comment thread src/state/migration_journal.rs Outdated
@qodo-code-review

qodo-code-review Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 17 rules
✅ REVIEW.md

Grey Divider


Action required

1. Doctor repair lacks lock ✓ Resolved 🐞 Bug ☼ Reliability
Description
apply_repairs() drops the root mutation lock and later runs migration_journal::reconcile(root),
which mutates tools/ and state/, allowing concurrent mutating commands (e.g. numan use) to
interleave and race this repair. This also invalidates the “Applied record honest” gating because
PendingMigration::load(...) can become None between the check and the reconcile call.
Code

src/cmd/doctor.rs[R1341-1344]

+        let id = "journal.migration_repaired".to_string();
+        match migration_journal::reconcile(root) {
+            Ok(_) => records.push(RepairRecord {
+                id,
Relevance

●●● Strong

Team previously required journal reconciliation/mutations to run under mutation lock to prevent
races.

PR-#5
PR-#35

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The code drops the mutation lock mid-function and later calls the new reconcile repair without
reacquiring it; reconcile performs destructive filesystem operations. The project’s own review
guidance requires mutating commands to take the mutation lock, and numan use does so, so this
creates a real cross-process race window.

src/cmd/doctor.rs[1012-1019]
src/cmd/doctor.rs[1079-1080]
src/cmd/doctor.rs[1330-1344]
src/state/migration_journal.rs[163-210]
src/cmd/use_cmd.rs[24-28]
REVIEW.md[22-29]
PR-#5

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new migration journal repair path in `doctor --fix` calls `migration_journal::reconcile(root)` after `apply_repairs()` has explicitly dropped the mutation lock. Since `reconcile()` deletes the journal and may remove directories/files under `tools/nushell`, this can race with other commands that correctly hold the mutation lock (notably `numan use`).

## Issue Context
- `apply_repairs()` acquires the lock, uses it for a small subset of repairs, then `drop(lock.take())` to avoid nested lock acquisition for other subcommands.
- The migration journal repair is implemented as direct filesystem mutation and does not acquire the lock internally.

## Fix Focus Areas
- src/cmd/doctor.rs[1012-1019]
- src/cmd/doctor.rs[1079-1080]
- src/cmd/doctor.rs[1330-1354]

## What to change
1. Re-acquire `acquire_mutation_lock(root)` immediately before the migration journal repair block, and keep it held across:
  - the `PendingMigration::load(root)?.is_some()` gating check
  - the `migration_journal::reconcile(root)` call
2. Optionally, make the Applied/Skipped record reflect reality:
  - If `reconcile(root)` returns `Ok(None)`, record `Skipped` with reason like `no_pending_migration_journal` instead of `Applied`.
3. Add a regression test or targeted unit test seam (if available) that ensures repairs which mutate `state/` run while the lock is held (mirroring the already-accepted race pattern in PR #5).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Prepared reconcile loses state ✓ Resolved 🐞 Bug ≡ Correctness
Description
If the process crashes after the legacy binary is renamed but before the journal advances to
Renamed, migration_journal::reconcile() treats the journal as Prepared, ignores the non-empty
version dir removal failure, and deletes the journal without writing the active-version marker.
Subsequent migrate_legacy_install_with_detector() calls then no-op because the legacy binary is
gone, leaving a versioned Nu binary on disk that find_nu_executable_with_root() won’t discover
without the marker.
Code

src/state/migration_journal.rs[R175-178]

+                let _ = std::fs::remove_dir(&version_dir);
+            }
+            PendingMigration::delete(root)?;
+            Ok(Some(journal))
Relevance

●● Moderate

Subtle crash-window/state-machine correctness change; no closely matching historical decision on
this journal stage handling.

PR-#5
PR-#47

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The migration performs rename() while the journal is still Prepared, and reconcile() deletes a
Prepared journal without checking whether the versioned binary exists; if that crash window
happens, migration won’t rerun (legacy binary missing) and Nu lookup won’t find the versioned binary
without an active marker.

src/nu/migrate_legacy.rs[126-129]
src/nu/migrate_legacy.rs[167-225]
src/state/migration_journal.rs[163-178]
src/nu/paths.rs[284-337]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`migration_journal::reconcile()` currently assumes `MigrationStage::Prepared` implies only an orphan empty `<root>/tools/nushell/<version>/` directory could exist, and it deletes the journal unconditionally. But `migrate_legacy_install_with_detector()` performs `std::fs::rename(...)` while the journal is still `Prepared`, and only then attempts to save `Renamed`. A crash in that window produces a `Prepared` journal even though the versioned binary exists; reconcile then deletes the journal and never writes `nu_state/active-version.json`, permanently losing the recovery path.

## Issue Context
- `migrate_legacy_install_with_detector()` renames before saving `Renamed`.
- `find_nu_executable_with_root()` only consults the versioned layout via the active-version marker; it does not scan `<root>/tools/nushell/<version>/nu` when no marker exists.

## Fix Focus Areas
- src/state/migration_journal.rs[168-211]
- src/nu/migrate_legacy.rs[167-229]

## What to change
1. In `reconcile()`, make the `Prepared` branch consult filesystem truth before deleting the journal:
  - If the versioned binary for `journal.version` exists (`versioned_binary_present(...) == true`), treat this as equivalent to `Renamed` recovery: write the active marker if absent, remove stray legacy binary if present, and then delete the journal.
  - Only do the “remove empty dir + delete journal” cleanup when the versioned binary is NOT present.
2. Add a regression unit test for: journal stage `Prepared` + versioned binary present + legacy binary absent => reconcile writes active-version and clears journal.

(Alternative acceptable approach: introduce an additional stage for “after rename” or reorder stage persistence so the journal cannot remain `Prepared` after a successful rename, but still ensure reconcile gates actions on actual on-disk state.)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Redundant legacy_binary comment ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
A new inline comment restates the immediately-adjacent control flow (the exists() check and early
return) without adding non-obvious rationale. This increases noise and violates the guidance to keep
comments focused on rationale rather than behavior restatement.
Code

src/nu/migrate_legacy.rs[R126-129]

+    // If legacy binary doesn't exist, nothing to migrate.
+    if !legacy_binary.exists() {
+        return Ok(false);
+    }
Relevance

●●● Strong

Trivial comment-noise cleanup; team often accepts simplifications removing redundant
narrative/logic.

PR-#47

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2452624 disallows comments that merely narrate adjacent code. The comment above the
if !legacy_binary.exists() branch is directly inferable from the code and provides no additional
rationale.

Rule 2452624: Restrict code comments to non-obvious rationale, not restating behavior
src/nu/migrate_legacy.rs[126-129]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new comment restates obvious behavior directly visible in the next line(s), rather than documenting rationale or a non-obvious constraint.

## Issue Context
Comments should explain *why* something is done (constraints, invariants, pitfalls), not narrate what the code already says.

## Fix Focus Areas
- src/nu/migrate_legacy.rs[126-129]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. use list mutates state ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
numan use always creates a pre-mutation snapshot and runs legacy migration before dispatching, so
numan use list can unexpectedly rename/delete files and can fail due to snapshot/migration errors
even though it’s a read-only listing operation. This also forces the mutation lock for list,
increasing lock contention for a non-mutating query.
Code

src/cmd/use_cmd.rs[R41-44]

+    // Attempt migration of legacy single-binary install before any operation.
+    // This is a no-op if migration has already occurred or no legacy install exists.
+    crate::nu::migrate_legacy::migrate_legacy_install(root)
+        .with_context(|| "Failed to migrate legacy Nu installation")?;
Relevance

●●● Strong

They’ve accepted making “check/list” style paths read-only and avoiding unnecessary
mutation/locking.

PR-#6

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The implementation calls create_snapshot(...) and migrate_legacy_install(root) before checking
whether the requested subcommand is list, so listing is not read-only and inherits
mutation/failure behavior.

src/cmd/use_cmd.rs[24-50]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`use_cmd::execute()` unconditionally acquires the mutation lock, creates a pre-mutation snapshot, and calls `migrate_legacy_install()` before matching on `args.version`. This makes `numan use list` a mutating operation and introduces new failure modes for a read-only query.

## Issue Context
The current flow:
- lock
- snapshot
- legacy migration (may rename legacy binary and write active marker)
- then `match` to list/latest/switch

## Fix Focus Areas
- src/cmd/use_cmd.rs[24-50]

## What to change
1. Dispatch on `args.version` earlier.
2. For `list`:
  - avoid snapshot creation
  - avoid full migration (at most, consider *journal-only* reconciliation if you still want self-healing, but be explicit that it mutates state)
3. Keep snapshot + migration + lock for `latest` and specific version switches (the truly mutating cases).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Public APIs use anyhow::Result ✓ Resolved 📘 Rule violation ⌂ Architecture
Description
New public library modules expose anyhow::Result in their public API, which prevents callers from
pattern-matching structured error variants. This conflicts with the requirement to use
thiserror-derived error types for public library APIs and reserve anyhow::Result for
application/handler layers.
Code

src/nu/version_manager.rs[R45-46]

+pub fn read_active_version(root: &Path) -> Result<Option<ActiveVersion>> {
+    let path = active_version_path(root);
Relevance

●● Moderate

Architectural/API error-type policy is plausible but no clear repo precedent enforcing “no anyhow in
public API”.

PR-#35
PR-#5

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The rule requires anyhow::Result for application handlers but thiserror-derived concrete error
types for public library APIs. The new modules are exported via src/lib.rs and define multiple
pub fn ... -> Result<...> while importing anyhow::Result, making these public APIs return
anyhow errors.

Rule 2436627: Use anyhow::Result for application handlers and thiserror for library error enums
src/lib.rs[1-9]
src/nu/version_manager.rs[7-8]
src/nu/version_manager.rs[45-59]
src/nu/migrate_legacy.rs[15-17]
src/nu/migrate_legacy.rs[97-113]
src/state/migration_journal.rs[32-35]
src/state/migration_journal.rs[99-118]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Public functions in library modules return `anyhow::Result`, which makes errors opaque to downstream callers and violates the guideline to use `thiserror` error enums for library/public APIs.

## Issue Context
This crate has a `src/lib.rs` exporting modules, so these `pub fn` APIs are part of the library surface.

## Fix Focus Areas
- src/nu/version_manager.rs[7-90]
- src/nu/migrate_legacy.rs[15-113]
- src/state/migration_journal.rs[32-119]
- src/lib.rs[1-9]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread src/nu/version_manager.rs Outdated
Comment thread src/nu/migrate_legacy.rs Outdated
Comment thread src/state/migration_journal.rs Outdated
Comment thread src/cmd/doctor.rs Outdated
Comment thread src/cmd/use_cmd.rs Outdated
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Journal legacy Nu migration and finalize numan use active-version switching

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add a journaled legacy Nu install migration with reconcile + doctor auto-repair.
• Implement numan use (list/latest/specific) backed by an active-version marker.
• Harden setup nu PATH/existing flows with validation, snapshotting, and safer prompts.
Diagram

graph TD
  use["numan use"] --> migrate["legacy migrate"] --> reconcile["journal reconcile"] --> mj[("state/migration-journal.json")]
  doctor["doctor --fix"] --> reconcile
  migrate --> tools["tools/nushell/<v>/"] --> vman["active version"] --> av[("nu_state/active-version.json")]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Pure idempotent migration without a journal
  • ➕ No new state file to maintain
  • ➕ Less code/serialization surface area
  • ➖ Harder to distinguish partial failure vs. clean state
  • ➖ Less explicit recovery semantics; doctor can’t reliably report/repair progress
2. Fold legacy migration into `setup nu` only (not `use`/doctor)
  • ➕ Single entrypoint for legacy-to-versioned transition
  • ➕ Avoids repeated reconcile checks in use
  • ➖ Users who never run setup may remain stuck with half-states
  • ➖ Doctor loses an explicit auto-tier repair hook for stranded users
3. Reuse an existing journal framework type (e.g., lifecycle journal)
  • ➕ Consistency with existing journaling patterns and tooling
  • ➕ Potential shared helpers for schema/versioning
  • ➖ Tighter coupling between otherwise independent concerns
  • ➖ More churn in existing journal code vs. adding a focused migration journal

Recommendation: Keep the dedicated migration-journal.json approach: it matches existing crash-recovery discipline, provides an explicit audit trail, and enables two reconciliation layers (self-heal on use and catch-up via doctor --fix). The added surface area is justified by clearer recovery semantics and better user supportability.

Files changed (15) +2502 / -45

Enhancement (8) +2210 / -31
doctor.rsAdd migration-journal finding + auto-tier repair and tests +122/-1

Add migration-journal finding + auto-tier repair and tests

• Introduces 'journal.migration_pending' (Warn, Auto) when 'state/migration-journal.json' exists, with a fix hint of 'numan use'. During 'doctor --fix', runs 'migration_journal::reconcile(root)' and records an applied/failed repair; adds unit tests for reporting and repair behavior.

src/cmd/doctor.rs

use_cmd.rsImplement 'numan use' (list/latest/version) with snapshot + legacy-migration +207/-17

Implement 'numan use' (list/latest/version) with snapshot + legacy-migration

• Replaces the reserved stub with a real implementation: takes the mutation lock, creates a pre-mutation snapshot, runs legacy migration, then executes 'list', 'latest', or a version switch. Adds unit tests covering empty/list/latest/switch success and not-installed errors.

src/cmd/use_cmd.rs

bootstrap.rsSnapshot before install; persist active marker for pinned installs; safer non-TTY behavior +120/-11

Snapshot before install; persist active marker for pinned installs; safer non-TTY behavior

• Adds a non-interactive guard to prevent implicit destructive setup without '--yes', creates a pre-install snapshot, and writes the active-version marker when installing a pinned version. Updates PATH registration prompting to honor hoisted caller consent and adds a unit test ensuring the pinned install persists the active marker.

src/nu/bootstrap.rs

migrate_legacy.rsAdd journaled legacy single-binary → versioned-layout migration with recovery tests +563/-0

Add journaled legacy single-binary → versioned-layout migration with recovery tests

• Implements a staged migration transaction (Prepared → Renamed → Active) that writes 'state/migration-journal.json', moves the legacy 'tools/nushell/nu' into 'tools/nushell/<version>/', and persists the active marker. Includes robust self-healing via 'migration_journal::reconcile' and extensive tests for partial failures, cleanup, and version detection (VERSION file preferred, fallback to 'nu --version').

src/nu/migrate_legacy.rs

paths.rsResolve Nu binary via active-version marker hint table before PATH fallback +135/-2

Resolve Nu binary via active-version marker hint table before PATH fallback

• Extends 'find_nu_executable_with_root' to consult 'nu_state/active-version.json' for on-tree and off-tree binary hints when the legacy managed binary is absent. Adds tests for on-tree hint, off-tree fallback, and stale-marker fallthrough behavior.

src/nu/paths.rs

version_manager.rsIntroduce active-version marker + version enumeration utilities +559/-0

Introduce active-version marker + version enumeration utilities

• Adds 'ActiveVersion' marker read/write/clear, semver normalization, installed-version listing (with off-tree marker augmentation), and active binary resolution rules (prefer on-tree, fall back to recorded off-tree path, error on dangling). Includes comprehensive unit tests for compatibility and edge cases.

src/nu/version_manager.rs

migration_journal.rsAdd migration journal format + reconcile() crash recovery +462/-0

Add migration journal format + reconcile() crash recovery

• Defines the on-disk journal schema and 'MigrationStage' lifecycle for legacy migration, plus a 'reconcile(root)' routine that removes Prepared orphans, completes Renamed migrations (writing the active marker if absent), and defensively clears Active-stage journals. Includes unit tests for serialization, idempotency, and stage-specific recovery behaviors.

src/state/migration_journal.rs

confirm.rsAdd require_tty_or_yes() for destructive non-interactive safety +42/-0

Add require_tty_or_yes() for destructive non-interactive safety

• Introduces 'require_tty_or_yes' to refuse destructive operations in non-interactive sessions unless '--yes' is explicitly provided, with audit logs for both branches. Adds a unit test pinning the 'yes=true' acceptance behavior.

src/util/confirm.rs

Bug fix (1) +276 / -8
setup.rsValidate PATH/existing Nu before destructive removal; unify prompts via seams +276/-8

Validate PATH/existing Nu before destructive removal; unify prompts via seams

• Adds 'ExecuteUseOpts' seams for validation/confirm injection and validates user-supplied Nu binaries before deleting a managed install. Hoists destructive consent into a single merged prompt (delete + PATH add) and passes consent through to bootstrap to suppress duplicate prompting; adds focused unit tests for the new confirm/decline behavior.

src/cmd/setup.rs

Refactor (2) +3 / -0
mod.rsExport new Nu migration and version manager modules +2/-0

Export new Nu migration and version manager modules

• Adds 'migrate_legacy' and 'version_manager' to the 'nu' module surface so CLI commands can use them.

src/nu/mod.rs

mod.rsRegister migration_journal state module +1/-0

Register migration_journal state module

• Exports 'migration_journal' from the state module tree so it is available to commands and migration code.

src/state/mod.rs

Documentation (3) +12 / -6
AGENTS.mdDocument migration journal and active-version marker semantics +3/-0

Document migration journal and active-version marker semantics

• Extends the agent architecture notes with the new legacy-migration journal lifecycle and the active-version marker schema/authority rules.

AGENTS.md

consolidated-multi-repo-roadmap.mdUpdate roadmap to match shipped 'use' + active-version marker approach +3/-6

Update roadmap to match shipped 'use' + active-version marker approach

• Adjusts 'numan use' expectations (no auto-download), and documents the JSON active marker and PATH responsibilities. Removes outdated reserved-command stub guidance.

docs/plans/consolidated-multi-repo-roadmap.md

hints.rsAdd CMD_USE hint constant for doctor and CLI messaging +6/-0

Add CMD_USE hint constant for doctor and CLI messaging

• Adds 'CMD_USE' as a shared hint string, documenting that 'numan use' is also a migration reconciliation entrypoint.

src/util/hints.rs

Other (1) +1 / -0
.gitignoreIgnore local /.freebuff scratch directory +1/-0

Ignore local /.freebuff scratch directory

• Adds '/.freebuff' to the repo ignore list to avoid committing local scratch artifacts.

.gitignore

@qodo-code-review

Copy link
Copy Markdown
Contributor

Qodo Fixer

✅ Committed (4) · ☑ Fixed (4)

Grey Divider

Commits pushed directly to this PR — no separate fix PR opened.

Process — 4 fixed
  • ☑ Fixed: Doctor repair lacks lock
  • ☑ Fixed: Prepared reconcile loses state
  • ☑ Fixed: Redundant legacy_binary comment
  • ☑ Fixed: use list mutates state
  • ⏭ Skipped (1)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (12)
AGENTS.md (2)

93-96: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the new util/ modules to the structure block.

The util/ subtree lists atomic.rs, fs_safety.rs, and hints.rs. This PR's stack also relies on src/util/stdio_redirect.rs (imported by src/cmd/doctor.rs Line 41 to keep --json stdout clean) and src/util/test_paths.rs (the PathRestoreGuard used to isolate PATH-sensitive tests). Neither appears in the map. The same omission was raised for the nu/ subtree in an earlier review and fixed at Lines 91-92; apply it here.

📝 Proposed documentation addition
   util/
     atomic.rs          — write_json_atomic helper (tempfile+persist)
     fs_safety.rs       — OWNERSHIP_MARKER, acquire_mutation_lock (advisory fd_lock mutex), assert_managed_file_owned (Phase 4)
     hints.rs           — Canonical `fix` hint strings aligned with docs/numan-doctor.md (Phase 7.3)
+    stdio_redirect.rs  — StdoutToStderr guard so nested repair output cannot corrupt `--json` stdout
+    test_paths.rs      — PathRestoreGuard: serializes and restores `PATH` for PATH-sensitive tests

As per coding guidelines: "Update documentation and AGENTS.md when project structure or conventions change."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AGENTS.md` around lines 93 - 96, Update the util/ structure block in
AGENTS.md to include src/util/stdio_redirect.rs and src/util/test_paths.rs, with
concise descriptions matching their roles in doctor JSON output handling and
PATH-sensitive test isolation. Preserve the existing entries for atomic.rs,
fs_safety.rs, and hints.rs.

Source: Coding guidelines


68-68: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

State that mutating numan use arms now run legacy migration.

This line describes the snapshot and the mutation lock but not the migration step added in src/cmd/use_cmd.rs Line 74. Legacy migration renames the Nu binary and writes the active-version marker, so it is a user-visible mutation performed by numan use. The line also does not record that list is exempt from all three (lock, snapshot, migration), which is the contract the command's own comment at Lines 26-29 establishes.

📝 Proposed documentation change
-    use_cmd.rs         — `numan use <version>|latest|list`: activates a previously installed managed Nu version (no auto-download); writes the active-version marker after a PreMutation snapshot under the root mutation lock
+    use_cmd.rs         — `numan use <version>|latest|list`: activates a previously installed managed Nu version (no auto-download); mutating arms take the root mutation lock, snapshot (PreMutation), run journaled legacy migration, then write the active-version marker; `list` is read-only (no lock, snapshot, or migration)

As per coding guidelines: "update AGENTS.md, docs/, or command help when structure, conventions, or user-visible behavior changes."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AGENTS.md` at line 68, Update the `use_cmd.rs` entry in `AGENTS.md` to state
that mutating `numan use` variants acquire the root mutation lock, run the
PreMutation snapshot, perform legacy migration, and write the active-version
marker; explicitly note that `numan use list` is exempt from all four
operations.

Source: Coding guidelines

docs/numan-doctor.md (1)

141-150: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the two migration-journal findings to the check catalog.

Section 3 lists eight journal findings. This PR adds two more and both reach users:

  • journal.migration_pendingwarn, Auto repair, hint numan use (src/cmd/doctor.rs Lines 665-674).
  • journal.migration_invaliderror, Manual, no fix hint (src/cmd/doctor.rs Lines 676-687).

Both are registered in the human report's Journals section at src/cmd/doctor.rs Lines 1546-1547, and journal.migration_pending appears in the repair-policy table at Line 88's neighbourhood only implicitly. This document is the authority for the check catalog, so a reader auditing doctor's output against the spec finds two undocumented ids.

📝 Proposed documentation addition
 | `journal.lifecycle_pending` | `warn` | `state/pending-lifecycle.json` exists | **manual:** re-run or clear per op |
 | `journal.lifecycle_stale` | `error` | Stale lifecycle journal | **manual** |
+| `journal.migration_pending` | `warn` | `state/migration-journal.json` exists and parses (stage `Prepared` \| `Renamed` \| `Active`) | **auto:** `migration_journal::reconcile` under the mutation lock, after a PreMutation snapshot |
+| `journal.migration_invalid` | `error` | `state/migration-journal.json` is present but unreadable, unparseable, or carries an unsupported `schema_version` | **manual:** delete the journal file; `numan use` cannot reconcile an unreadable journal |

Add a matching row to the repair-policy table at Lines 86-99 for the journal.migration_pending auto tier.

As per coding guidelines: "update AGENTS.md, docs/, or command help when structure, conventions, or user-visible behavior changes."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/numan-doctor.md` around lines 141 - 150, Add both migration findings to
the journal check catalog in docs/numan-doctor.md: document
journal.migration_pending as warn with auto repair and the numan use hint, and
journal.migration_invalid as error with manual repair and no fix hint. Also add
journal.migration_pending to the repair-policy table with the auto tier,
matching the corresponding doctor.rs behavior.

Source: Coding guidelines

src/cmd/doctor.rs (2)

2418-2450: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the failing-reconcile counterpart to this test.

doctor_fix_reconciles_migration_journal covers the success path. doctor_fix_continues_after_absent_migration_journal covers the absent path. Nothing covers the path where reconcile returns Err — the branch at Lines 1464-1468 that records journal.migration_repaired as Failed.

That branch matters because it must retain the journal and must not stop later repairs. MigrationJournalError::PreparedOrphanRemoveFailed is trivial to stage: a Prepared journal plus a version directory containing one stray file, the same fixture already used at src/state/migration_journal.rs Lines 568-604.

💚 Proposed test
    #[test]
    fn doctor_fix_records_failed_migration_repair_and_continues() {
        let dir = TempDir::new().unwrap();
        let root = dir.path();

        // `Prepared` journal over a version dir that `remove_dir` cannot clear.
        let tools = root.join("tools").join("nushell");
        let version_dir = tools.join("0.113.1");
        std::fs::create_dir_all(&version_dir).unwrap();
        std::fs::write(version_dir.join("stray.dat"), b"foreign").unwrap();
        PendingMigration {
            schema_version: crate::state::migration_journal::SCHEMA_VERSION,
            version: "0.113.1".to_string(),
            stage: crate::state::migration_journal::MigrationStage::Prepared,
        }
        .save(root)
        .unwrap();

        // A second Auto repair that must still run after the migration fails.
        let marker = root.join("nu_state").join("active-version.json");
        std::fs::create_dir_all(marker.parent().unwrap()).unwrap();
        std::fs::write(&marker, b"{ not valid json").unwrap();

        let args = DoctorArgs {
            scan: false,
            json: false,
            nupm_home: None,
        };
        let report = run_checks_with_options(
            &DoctorArgs {
                scan: true,
                json: false,
                nupm_home: None,
            },
            root,
            &test_doctor_options(),
        )
        .unwrap();
        let repairs = apply_repairs(&args, root, &report.findings, &test_doctor_options()).unwrap();

        assert!(
            repairs.iter().any(|r| {
                r.id == "journal.migration_repaired" && r.status == RepairStatus::Failed
            }),
            "a failed reconcile must be recorded, not swallowed: {repairs:?}"
        );
        assert!(
            PendingMigration::load(root).unwrap().is_some(),
            "the journal must survive a failed repair so the user can retry"
        );
        assert!(
            repairs.iter().any(|r| {
                r.id == "nu.active_version.repaired" && r.status == RepairStatus::Applied
            }),
            "later repairs must still run after a failed migration repair: {repairs:?}"
        );
    }

As per coding guidelines: "Tests must cover failure modes, not only successful execution."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cmd/doctor.rs` around lines 2418 - 2450, Add a test alongside
doctor_fix_reconciles_migration_journal that creates a Prepared PendingMigration
for a version directory containing a stray file, causing reconcile to fail with
the journal retained. Run the repair flow with a second invalid active-version
marker, then assert journal.migration_repaired is Failed, PendingMigration
remains present, and nu.active_version.repaired is Applied to verify later
repairs continue.

Source: Coding guidelines


1476-1494: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

This repair deletes state without a snapshot and without checking snapshot_ok.

Every other snapshot-dependent repair in this function is gated: Lines 1174, 1225, 1252, 1292, 1331, and 1371 all skip with snapshot_unavailable when the PreMutation snapshot failed. The migration repair at Lines 1445-1456 takes its own snapshot and fails the record when that snapshot fails. This block does neither. It acquires the lock at Line 1481 and calls clear_active_version, which removes nu_state/active-version.json outright.

When snapshot_ok is false — a malformed lockfile, a missing payload revision — this is the only repair in the pass that mutates state with no baseline anywhere.

docs/numan-doctor.md Line 88 documents the behavior as intentional: "Independent of PreMutation success". The reasoning holds for a torn marker, since the content is already unusable. Two things do not follow from it:

  1. The repository rule has no carve-out. It requires a snapshot before doctor-repair state mutations.
  2. The marker can hold a valid off-tree binary_path that a partial write corrupted. write_active_version_with_binary records the user's external Nu at an absolute path (src/nu/version_manager.rs Lines 154-185). Deleting it loses the only record of that selection, and no other file holds it.

Pick one and make it explicit in the code, not only in the spec:

🔧 Option A — gate on the existing snapshot like every neighbour
     if findings
         .iter()
         .any(|f| f.id == "nu.active_version.invalid" && f.repair == RepairTier::Auto)
     {
         let id = "nu.active_version.repaired".to_string();
+        if !snapshot_ok {
+            records.push(RepairRecord {
+                id,
+                status: RepairStatus::Skipped,
+                reason: Some("snapshot_unavailable".to_string()),
+            });
+        } else {
         let _lock = acquire_mutation_lock(root)?;
         match version_manager::clear_active_version(root) {
🔧 Option B — keep it unconditional and record why

Preserve the bytes before deleting, so the selection is recoverable:

// The marker is unreadable, so no PreMutation baseline can capture anything
// useful from it. Copy the raw bytes aside before clearing: a torn marker may
// still contain a recoverable off-tree `binary_path`.
let marker = version_manager::active_version_path(root);
let _ = std::fs::rename(&marker, marker.with_extension("json.corrupt"));

Option A matches the surrounding code. Option B matches the documented intent. Either is fine; the current state matches the docs but not the rule, and the code carries no record of the decision.

As per coding guidelines: "Create a snapshot before mutating install, update, remove, activate, deactivate, init --refresh, nupm import, or doctor-repair state."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cmd/doctor.rs` around lines 1476 - 1494, Update the active-version repair
block around version_manager::clear_active_version to explicitly satisfy the
snapshot-before-mutation rule: either skip and record snapshot_unavailable when
snapshot_ok is false, matching neighboring repairs, or preserve the raw
active-version marker before unconditional clearing so a recoverable binary_path
is retained. Make the chosen behavior explicit in the code and keep the existing
RepairRecord outcomes for successful or failed clearing.

Source: Coding guidelines

src/state/migration_journal.rs (1)

521-561: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add a test for the Prepared-with-binary-already-moved recovery path.

The reconcile tests cover the Prepared-orphan branch three ways, but not the branch at Lines 331-350. That branch is the whole point of the filesystem-truth rule: a crash between rename and the Renamed journal advance. Without a test, a future edit can delete the versioned_binary_present check and every test in this module still passes, while the active marker is silently never written.

Two smaller gaps in the same module: MigrationJournalError::UnsafeVersionReconcile (Line 321) and MigrationJournalError::UnsafeVersionWrite (Line 258) are both untested.

💚 Proposed tests
    #[test]
    fn reconcile_prepared_completes_when_binary_already_moved() {
        let tmp = TempDir::new().unwrap();
        let root = tmp.path();

        // Crash window: rename landed, the journal never advanced to Renamed.
        let version_dir = version_install_dir(root, "0.113.1");
        std::fs::create_dir_all(&version_dir).unwrap();
        std::fs::write(version_dir.join(bin_name()), b"binary").unwrap();
        write_journal(root, "0.113.1", MigrationStage::Prepared);

        let recovered = reconcile(root).unwrap().unwrap();
        assert_eq!(recovered.stage, MigrationStage::Prepared);
        assert!(
            version_dir.join(bin_name()).is_file(),
            "the moved binary must survive Prepared recovery"
        );
        let active = read_active_version(root).unwrap().unwrap();
        assert_eq!(
            active.version, "0.113.1",
            "Prepared recovery must complete the active-marker write"
        );
        assert!(PendingMigration::load(root).unwrap().is_none());
    }

    #[test]
    fn reconcile_refuses_traversal_version() {
        let tmp = TempDir::new().unwrap();
        let root = tmp.path();
        // Bypass `save`, which rejects unsafe versions, to simulate tampering.
        let path = PendingMigration::journal_path(root);
        std::fs::create_dir_all(path.parent().unwrap()).unwrap();
        std::fs::write(
            &path,
            format!(
                r#"{{"schema_version":{SCHEMA_VERSION},"version":"../etc","stage":"prepared"}}"#
            ),
        )
        .unwrap();

        let err = reconcile(root).unwrap_err();
        assert!(matches!(
            err,
            MigrationJournalError::UnsafeVersionReconcile { .. }
        ));
        assert!(
            PendingMigration::load(root).unwrap().is_some(),
            "a tampered journal must be retained for `numan doctor --fix`"
        );
    }

    #[test]
    fn save_refuses_unsafe_version_component() {
        let tmp = TempDir::new().unwrap();
        let err = PendingMigration {
            schema_version: SCHEMA_VERSION,
            version: "../escape".to_string(),
            stage: MigrationStage::Prepared,
        }
        .save(tmp.path())
        .unwrap_err();
        assert!(matches!(
            err,
            MigrationJournalError::UnsafeVersionWrite { .. }
        ));
        assert!(!PendingMigration::journal_path(tmp.path()).exists());
    }

As per coding guidelines: "Tests must cover failure modes, not only successful execution."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/state/migration_journal.rs` around lines 521 - 561, Extend the migration
journal tests around reconcile and PendingMigration::save to cover Prepared
recovery when the versioned binary already exists, asserting the binary
survives, the active marker is written, and the journal is cleared. Add
failure-mode tests for traversal versions that assert reconcile returns
UnsafeVersionReconcile while retaining the journal, and for unsafe versions
passed to save that assert UnsafeVersionWrite without creating the journal.

Source: Coding guidelines

src/nu/migrate_legacy.rs (1)

252-256: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Provide a recovery path for RenamedBinaryMissing.

numan use <version> and numan use latest propagate reconcile before switching versions, so the missing binary blocks both commands. numan use list is unaffected. doctor --fix calls the same failing reconciliation and leaves the journal, while journal.migration_pending still recommends numan use. Classify this state as manual recovery and provide an explicit journal path and discard or reinstall command.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/nu/migrate_legacy.rs` around lines 252 - 256, Update the migration
recovery handling around migration_journal::reconcile to explicitly classify
RenamedBinaryMissing as manual recovery instead of propagating a blocking error.
Provide the journal path in the diagnostic and tell the user how to discard the
journal or reinstall the missing version, while preserving existing recovery
behavior for other journal states.
src/cmd/use_cmd.rs (2)

107-118: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Extract the on-tree/off-tree marker write, and add coverage for the off-tree branch.

Lines 109-118 and Lines 163-170 are the same logic: compare the resolved binary against version_binary(root, &version), then call write_active_version or write_active_version_with_binary. Two copies of the rule that preserves a user's off-tree Nu selection. If one copy loses the else arm, numan use silently rewrites an off-tree marker into an on-tree one and Nu resolution breaks for that user.

The tests compound this. create_fake_version at Lines 180-185 only builds on-tree installs, so every test in this module takes the if branch. write_active_version_with_binary — including its BinaryPathTraversal guard — is never exercised from numan use.

♻️ Proposed refactor
+/// Persist the active-version marker, preserving an off-tree binary path when
+/// the resolved binary lives outside `tools/nushell/<version>/`.
+fn select_version(root: &Path, version: &str, installed_binary: &Path) -> Result<()> {
+    if installed_binary == version_manager::version_binary(root, version) {
+        version_manager::write_active_version(root, version)
+    } else {
+        version_manager::write_active_version_with_binary(root, version, installed_binary)
+    }
+    .with_context(|| format!("Failed to switch to Nu {}", version))
+}

Then both call sites collapse to one line:

-            let on_tree = version_manager::version_binary(root, &version);
-            if installed_binary == on_tree {
-                version_manager::write_active_version(root, &version)?;
-            } else {
-                version_manager::write_active_version_with_binary(
-                    root,
-                    &version,
-                    &installed_binary,
-                )?;
-            }
+            select_version(root, &version, &installed_binary)?;
💚 Proposed off-tree test
    #[test]
    fn test_use_switch_preserves_off_tree_binary_path() {
        let tmp = TempDir::new().unwrap();
        let root = tmp.path();

        // Off-tree Nu recorded by `numan setup nu use <path>`.
        let external = tmp.path().join("external-nu");
        std::fs::write(&external, "fake").unwrap();
        version_manager::write_active_version_with_binary(root, "0.113.1", &external).unwrap();

        execute(
            &UseArgs {
                version: "0.113.1".to_string(),
            },
            root,
        )
        .unwrap();

        let active = version_manager::read_active_version(root).unwrap().unwrap();
        assert_eq!(active.version, "0.113.1");
        assert_eq!(
            active.binary_path.as_deref(),
            Some(external.to_string_lossy().as_ref()),
            "`numan use` must not downgrade an off-tree selection to an on-tree path"
        );
    }

As per coding guidelines: "Add or update tests for behavior changes, including relevant failure paths."

Also applies to: 162-170

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cmd/use_cmd.rs` around lines 107 - 118, The active-version marker logic
is duplicated and the off-tree path is untested. Extract the comparison and
write behavior into a shared helper, then replace both call sites in the use
flow with that helper while preserving on-tree and off-tree writes; add coverage
for switching to a version whose recorded binary is outside the managed tree,
including the existing binary-path validation path where appropriate.

Source: Coding guidelines


272-290: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Pin that list runs no migration, not only that it takes no snapshot.

test_use_list_takes_no_snapshot checks for the absence of state/snapshots. The read-only contract at Lines 26-29 makes three claims — no lock, no snapshot, no migration — and only the snapshot claim is tested. Migration is the claim that mutates the most: it renames the Nu binary and writes the active-version marker.

The assertion is also weak on its own terms. The test never runs a mutating arm, so state/snapshots is absent regardless of what list does.

💚 Proposed test
    #[test]
    fn test_use_list_runs_no_migration() {
        // Stage a legacy single-binary layout. A read-only `list` must leave it
        // exactly as-is; only the mutating arms may migrate.
        let tmp = TempDir::new().unwrap();
        let root = tmp.path();
        let legacy = version_manager::versioned_nu_dir(root)
            .join(if cfg!(windows) { "nu.exe" } else { "nu" });
        std::fs::create_dir_all(legacy.parent().unwrap()).unwrap();
        std::fs::write(&legacy, "fake legacy nu").unwrap();

        execute(
            &UseArgs {
                version: "list".to_string(),
            },
            root,
        )
        .unwrap();

        assert!(
            legacy.is_file(),
            "`numan use list` must not migrate the legacy binary"
        );
        assert!(
            version_manager::read_active_version(root).unwrap().is_none(),
            "`numan use list` must not write the active-version marker"
        );
    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cmd/use_cmd.rs` around lines 272 - 290, Extend the use-list test around
`execute` to stage a legacy single-binary layout, then verify that `execute`
with `UseArgs { version: "list" }` leaves the legacy binary in place and
`version_manager::read_active_version(root)` remains unset. Replace or
supplement the snapshot-only assertion so the test directly covers the read-only
no-migration contract.
tests/doctor_test.rs (2)

293-302: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Close stdin on this subprocess; the two sibling subprocess tests already do.

doctor_json_default_stdout_is_valid_json sets .stdin(std::process::Stdio::null()) at Line 825, and doctor_json_scan_omits_repairs_field sets it at Line 865. This invocation does not. It inherits the test harness stdin.

The risk is concrete for this specific test. It runs doctor in default repair mode against a root with a malformed lockfile. Doctor's repair path reaches setup nu use, which is fail-closed on TTY per docs/numan-doctor.md Line 93. If any repair in that path prompts, the child blocks on a read that never returns and CI hangs until the job timeout rather than failing with a diff.

🔧 Proposed fix
         .env("NUMAN_ALLOW_UNSIGNED", "1")
+        .stdin(std::process::Stdio::null())
         .output()
         .expect("run numan doctor --json with malformed lockfile");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/doctor_test.rs` around lines 293 - 302, Add
`.stdin(std::process::Stdio::null())` to the `Command` chain in the
malformed-lockfile doctor subprocess test, before `.output()`, matching the
sibling tests so the child cannot inherit the test harness stdin or block during
repair.

33-69: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This fixture does two unrelated jobs, and it makes the test that uses it misleading.

nu_setup_repair_test is injected as doctor's nu_setup_repair. It asserts the argument contract doctor must honour — NuAction::Use, no deprecated flag, no --yes. That is its job as a test double, and those assertions are correct.

Lines 55-68 then do something else entirely. The fixture seeds a managed install, calls the production setup::execute_nu with a deliberately missing binary path, asserts the managed tree survived, and returns that error. That is a test of setup's wipe-protection, embedded inside doctor's test double.

The consequence surfaces in doctor_fix_registers_off_path_nu_without_network at Lines 521-557. The name says the repair registers the off-path Nu. The fixture guarantees it always fails, which is why Line 556 asserts code == 1. A reader cannot tell from the test name or body that the repair is expected to fail by construction, or why.

Split the two concerns. Keep the doctor double minimal and put the wipe-protection assertion in its own test next to execute_nu_use_existing_refuses_without_consent_and_keeps_managed at Lines 89-110, which already covers that ground.

♻️ Proposed change
 fn nu_setup_repair_test(
     args: &numan_cli::cmd::setup::NuSetupArgs,
-    root: &Path,
+    _root: &Path,
 ) -> anyhow::Result<()> {
     let expected = TEST_OFF_PATH.lock().unwrap().clone();
     let Some(NuAction::Use { path }) = &args.action else {
         panic!("expected NuAction::Use, got {:?}", args.action);
     };
     assert_eq!(Some(path.as_path()), expected.as_ref().map(|p| p.as_path()));
     assert!(
         args.use_existing.is_none(),
         "doctor must not use the deprecated flag"
     );
     assert!(
         !args.yes,
         "doctor found_off_path repair must not pass --yes"
     );
     *TEST_NU_SETUP_CALLED.lock().unwrap() = true;
-
-    // Seed a managed install just before the production use path. ...
-    let managed = managed_nu_binary(root);
-    ...
-    Err(err)
+    Ok(())
 }

Then doctor_fix_registers_off_path_nu_without_network asserts code == 0 and the name matches the behavior. Move the managed-survives assertion into a dedicated test that calls setup::execute_nu directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/doctor_test.rs` around lines 33 - 69, Split nu_setup_repair_test so it
only validates the doctor-to-NuSetupArgs contract and returns success without
invoking setup::execute_nu. Update
doctor_fix_registers_off_path_nu_without_network to expect successful
registration (code 0). Add a separate test beside
execute_nu_use_existing_refuses_without_consent_and_keeps_managed that directly
calls setup::execute_nu with a missing path, seeds the managed binary, and
verifies it remains intact.
src/util/hints.rs (1)

35-83: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

shell_quote produces POSIX-only syntax for a cross-platform hint.

shell_quote always uses POSIX single-quote escaping. Windows paths contain backslashes, so shell_quote wraps them in single quotes (test at line 236-238 pins this). Single-quoted strings are not valid the same way in cmd.exe, and PowerShell escapes embedded quotes with '', not '\''.

setup_nu_use_existing prints this hint on every platform, including Windows. A Windows user who copies the printed numan setup nu use '<path>' hint into cmd.exe gets a broken command.

Branch the quoting style on cfg!(windows), or use double quotes for Windows paths.

Also applies to: 234-239

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/util/hints.rs` around lines 35 - 83, Update shell_quote to produce
Windows-compatible quoting when cfg!(windows) is enabled, using a style valid
for the shell targeted by setup_nu_use_existing, including appropriate
embedded-quote escaping; retain the existing POSIX single-quote behavior on
non-Windows platforms and preserve the unquoted result for safe values.
♻️ Duplicate comments (1)
src/state/migration_journal.rs (1)

331-350: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Extract the shared completion path; these 20 lines are a verbatim copy of the Renamed branch.

Lines 331-350 and Lines 376-406 perform the same three steps: write the active version when no selection exists, remove a stray legacy binary, and surface the same two error variants. Two copies of a recovery path that must stay byte-identical will drift. A later fix applied to one stage will silently miss the other.

♻️ Proposed refactor
     match journal.stage {
         MigrationStage::Prepared => {
             // The rename can complete before the journal advances to Renamed.
             // Trust the filesystem in that crash window and finish recovery.
             if versioned_binary_present(root, &journal.version) {
-                if read_active_version(root)?.is_none() {
-                    write_active_version(root, &journal.version).map_err(|source| {
-                        MigrationJournalError::RecoveryWriteActive {
-                            version: journal.version.clone(),
-                            source,
-                        }
-                    })?;
-                }
-                let bin_name = if cfg!(windows) { "nu.exe" } else { "nu" };
-                let legacy_binary = versioned_nu_dir(root).join(bin_name);
-                if legacy_binary.is_file() {
-                    if let Err(source) = std::fs::remove_file(&legacy_binary) {
-                        return Err(MigrationJournalError::LegacyBinaryRemoveFailed {
-                            path: PendingMigration::journal_path(root),
-                            legacy_binary,
-                            source,
-                        });
-                    }
-                }
+                complete_migration(root, &journal.version)?;
             } else {

Then reuse the same helper in the Renamed arm:

/// Finish a migration whose versioned binary is already in place: adopt the
/// version as active when the user has made no other selection, then clear a
/// stray legacy binary that would otherwise re-trigger migration.
fn complete_migration(root: &Path, version: &str) -> Result<(), MigrationJournalError> {
    if read_active_version(root)?.is_none() {
        write_active_version(root, version).map_err(|source| {
            MigrationJournalError::RecoveryWriteActive {
                version: version.to_string(),
                source,
            }
        })?;
    }
    let legacy_binary = versioned_nu_dir(root).join(nu_binary_name());
    if legacy_binary.is_file() {
        if let Err(source) = std::fs::remove_file(&legacy_binary) {
            return Err(MigrationJournalError::LegacyBinaryRemoveFailed {
                path: PendingMigration::journal_path(root),
                legacy_binary,
                source,
            });
        }
    }
    Ok(())
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/state/migration_journal.rs` around lines 331 - 350, Extract the
duplicated completion logic from the migration recovery flow into a shared
helper, such as complete_migration, covering active-version adoption,
legacy-binary removal, and the existing RecoveryWriteActive and
LegacyBinaryRemoveFailed errors. Replace both the current branch in the
versioned-binary path and the corresponding Renamed arm with calls to this
helper, preserving the existing binary-name selection and behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cmd/doctor.rs`:
- Around line 1440-1444: Update the repair pass around PendingMigration::load
and acquire_mutation_lock so errors are recorded as failed repairs and skipped
without returning early, allowing later repairs to continue. Follow the existing
PendingPluginDeactivate::load handling pattern, preserve the lock guard through
reconciliation, and add a test mirroring
doctor_fix_continues_after_absent_migration_journal that covers an unreadable
migration journal returning Err.
- Around line 352-357: Expose active_version_path with pub(crate) visibility so
it can be reused outside version_manager, then update the doctor error message
near read_active_version to call version_manager::active_version_path(root)
instead of rebuilding the nu_state/active-version.json path inline.

In `@src/cmd/setup.rs`:
- Around line 364-369: In execute_use_path and execute_use_existing, determine
managed_dir_was_present before any TTY/yes validation, then replace both
require_tty_or_yes calls with one opts.confirm.is_none() check using the
branch-appropriate message: the destructive managed Nushell wipe + PATH update
message when present, otherwise the generic PATH Nu registration message. Remove
the later duplicate safety checks while preserving the existing mutation
behavior.

In `@src/cmd/use_cmd.rs`:
- Around line 74-78: Update the migration flow in the caller around
migrate(root) to retain its returned bool and, when migration occurred, print a
concise user-facing message describing the completed legacy Nu layout migration
before continuing to op(root). Do not change the existing error propagation; if
silence is intentionally preserved instead, bind the result to _migrated and
document that intent briefly.

In `@src/nu/bootstrap.rs`:
- Around line 753-797: The already-installed Nushell path in the setup flow
passes a hardcoded true to confirm_or_bail, bypassing consent. In the existing
confirmation block around confirm_or_bail, pass options.yes instead, and add an
interactive test covering options.yes == false to verify the prompt is required
before PATH mutations.

In `@src/nu/migrate_legacy.rs`:
- Around line 308-311: Update the detect error conversion in the migration flow
around detect_legacy_version so DetectFailed wraps the original typed detector
error in a boxed source field rather than storing only e.to_string(). Preserve
the legacy binary path on DetectFailed, and apply the same typed-source wrapping
to the post_create hook error conversion.

In `@src/state/migration_journal.rs`:
- Around line 95-110: Remove the `{source}` interpolation from the
`#[error(...)]` display strings for both affected migration-journal variants,
including `PreparedOrphanRemoveFailed` and the corresponding variant around the
other referenced block. Preserve each `#[source]` field so callers can still
traverse the underlying I/O error through the error chain.
- Around line 240-244: Remove review-task and tool provenance identifiers from
the rationale comments while preserving their technical explanations: in
src/state/migration_journal.rs at lines 240-244, 352, and 563 remove
“copilot”/PR tags; in src/nu/migrate_legacy.rs at lines 234-237 and 416-417
remove the “cubic”/PR tags; in src/cmd/doctor.rs at lines 659-663 remove “PR69
WCk” and at lines 2556-2559 remove “Greptile:”. Keep only durable WHY-focused
rationale, including the unknown schema hard-failure, symlink refusal,
build-hash parsing, surfaced findings, and stale-finding explanation.

In `@src/util/test_paths.rs`:
- Around line 9-16: Update all tests that mutate PATH to use the shared
crate::util::test_paths::PathRestoreGuard, including the tests in
src/cmd/doctor.rs and the five PATH-mutating tests in src/nu/paths.rs. Remove or
replace the separate doctor-specific guard/mutex, and ensure each mutation is
protected for the full test scope through restoration.

---

Outside diff comments:
In `@AGENTS.md`:
- Around line 93-96: Update the util/ structure block in AGENTS.md to include
src/util/stdio_redirect.rs and src/util/test_paths.rs, with concise descriptions
matching their roles in doctor JSON output handling and PATH-sensitive test
isolation. Preserve the existing entries for atomic.rs, fs_safety.rs, and
hints.rs.
- Line 68: Update the `use_cmd.rs` entry in `AGENTS.md` to state that mutating
`numan use` variants acquire the root mutation lock, run the PreMutation
snapshot, perform legacy migration, and write the active-version marker;
explicitly note that `numan use list` is exempt from all four operations.

In `@docs/numan-doctor.md`:
- Around line 141-150: Add both migration findings to the journal check catalog
in docs/numan-doctor.md: document journal.migration_pending as warn with auto
repair and the numan use hint, and journal.migration_invalid as error with
manual repair and no fix hint. Also add journal.migration_pending to the
repair-policy table with the auto tier, matching the corresponding doctor.rs
behavior.

In `@src/cmd/doctor.rs`:
- Around line 2418-2450: Add a test alongside
doctor_fix_reconciles_migration_journal that creates a Prepared PendingMigration
for a version directory containing a stray file, causing reconcile to fail with
the journal retained. Run the repair flow with a second invalid active-version
marker, then assert journal.migration_repaired is Failed, PendingMigration
remains present, and nu.active_version.repaired is Applied to verify later
repairs continue.
- Around line 1476-1494: Update the active-version repair block around
version_manager::clear_active_version to explicitly satisfy the
snapshot-before-mutation rule: either skip and record snapshot_unavailable when
snapshot_ok is false, matching neighboring repairs, or preserve the raw
active-version marker before unconditional clearing so a recoverable binary_path
is retained. Make the chosen behavior explicit in the code and keep the existing
RepairRecord outcomes for successful or failed clearing.

In `@src/cmd/use_cmd.rs`:
- Around line 107-118: The active-version marker logic is duplicated and the
off-tree path is untested. Extract the comparison and write behavior into a
shared helper, then replace both call sites in the use flow with that helper
while preserving on-tree and off-tree writes; add coverage for switching to a
version whose recorded binary is outside the managed tree, including the
existing binary-path validation path where appropriate.
- Around line 272-290: Extend the use-list test around `execute` to stage a
legacy single-binary layout, then verify that `execute` with `UseArgs { version:
"list" }` leaves the legacy binary in place and
`version_manager::read_active_version(root)` remains unset. Replace or
supplement the snapshot-only assertion so the test directly covers the read-only
no-migration contract.

In `@src/nu/migrate_legacy.rs`:
- Around line 252-256: Update the migration recovery handling around
migration_journal::reconcile to explicitly classify RenamedBinaryMissing as
manual recovery instead of propagating a blocking error. Provide the journal
path in the diagnostic and tell the user how to discard the journal or reinstall
the missing version, while preserving existing recovery behavior for other
journal states.

In `@src/state/migration_journal.rs`:
- Around line 521-561: Extend the migration journal tests around reconcile and
PendingMigration::save to cover Prepared recovery when the versioned binary
already exists, asserting the binary survives, the active marker is written, and
the journal is cleared. Add failure-mode tests for traversal versions that
assert reconcile returns UnsafeVersionReconcile while retaining the journal, and
for unsafe versions passed to save that assert UnsafeVersionWrite without
creating the journal.

In `@src/util/hints.rs`:
- Around line 35-83: Update shell_quote to produce Windows-compatible quoting
when cfg!(windows) is enabled, using a style valid for the shell targeted by
setup_nu_use_existing, including appropriate embedded-quote escaping; retain the
existing POSIX single-quote behavior on non-Windows platforms and preserve the
unquoted result for safe values.

In `@tests/doctor_test.rs`:
- Around line 293-302: Add `.stdin(std::process::Stdio::null())` to the
`Command` chain in the malformed-lockfile doctor subprocess test, before
`.output()`, matching the sibling tests so the child cannot inherit the test
harness stdin or block during repair.
- Around line 33-69: Split nu_setup_repair_test so it only validates the
doctor-to-NuSetupArgs contract and returns success without invoking
setup::execute_nu. Update doctor_fix_registers_off_path_nu_without_network to
expect successful registration (code 0). Add a separate test beside
execute_nu_use_existing_refuses_without_consent_and_keeps_managed that directly
calls setup::execute_nu with a missing path, seeds the managed binary, and
verifies it remains intact.

---

Duplicate comments:
In `@src/state/migration_journal.rs`:
- Around line 331-350: Extract the duplicated completion logic from the
migration recovery flow into a shared helper, such as complete_migration,
covering active-version adoption, legacy-binary removal, and the existing
RecoveryWriteActive and LegacyBinaryRemoveFailed errors. Replace both the
current branch in the versioned-binary path and the corresponding Renamed arm
with calls to this helper, preserving the existing binary-name selection and
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 752fe759-b03c-4853-b1e8-0291d0aa2672

📥 Commits

Reviewing files that changed from the base of the PR and between b49bb5d and 432fffb.

📒 Files selected for processing (14)
  • AGENTS.md
  • docs/numan-doctor.md
  • src/cmd/doctor.rs
  • src/cmd/setup.rs
  • src/cmd/use_cmd.rs
  • src/nu/bootstrap.rs
  • src/nu/migrate_legacy.rs
  • src/nu/paths.rs
  • src/nu/version_manager.rs
  • src/state/migration_journal.rs
  • src/util/hints.rs
  • src/util/mod.rs
  • src/util/test_paths.rs
  • tests/doctor_test.rs
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • Trackdubllc/Trackdub (manual)
  • tonythethompson/QuickShell (manual)
  • tonythethompson/numan (manual)
  • tonythethompson/dependency-chain-substrate (manual)
📜 Review details
⏰ Context from checks skipped due to timeout. (4)
  • GitHub Check: Greptile Review
  • GitHub Check: Test (windows-latest)
  • GitHub Check: Real-Nu acceptance (windows-latest)
  • GitHub Check: Analyze (rust)
🧰 Additional context used
📓 Path-based instructions (15)
**/*

📄 CodeRabbit inference engine (CLAUDE.md)

**/*: Use Serena's semantic, symbol-aware tools as the primary tools for reading and editing code; use built-in Read, Glob, Grep, and Edit only under the stated exceptions.
Before editing a code file, inspect its symbol overview, read the specific symbols being changed, and edit them with Serena's symbol-aware tools.
Understand the existing code before changing it and make the smallest change that satisfies the request; avoid unrelated cleanup, premature abstractions, unnecessary error handling, feature flags, and compatibility shims.
Prefer editing existing files over creating new ones, and never create Markdown or README files unless explicitly requested.
For exploratory questions, provide a 2–3 sentence recommendation with the main tradeoff and do not implement until the user agrees.
For UI or frontend changes that cannot be tested in a browser, explicitly state that browser testing was not performed rather than claiming success.
Address security issues when discovered, including injection, XSS, SQL injection, path traversal, and secret leaks.
Pause and obtain confirmation before destructive, hard-to-reverse, externally visible, or third-party-upload actions, including deleting files or branches, dropping tables, force-pushing, modifying CI/CD, posting externally, or uploading content.
When blocked, investigate the root cause instead of bypassing it with --no-verify, --force, or deletion; investigate unfamiliar files, branches, and configuration before deleting them.
Only commit when explicitly asked; do not proactively update git configuration or push changes.
Do not skip Git hooks unless explicitly asked; if a pre-commit hook fails, fix the issue, re-stage, and create a new commit rather than amending.
Stage files by name rather than using git add -A or git add ., to avoid accidentally including secrets or large binaries.
Use a HEREDOC for commit messages, and add co-author attribution only when the user explicitly requests the exact trailer.
Do not force-pus...

Files:

  • src/util/mod.rs
  • docs/numan-doctor.md
  • src/nu/paths.rs
  • tests/doctor_test.rs
  • src/util/test_paths.rs
  • src/cmd/use_cmd.rs
  • src/util/hints.rs
  • src/cmd/doctor.rs
  • src/nu/version_manager.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/nu/migrate_legacy.rs
  • AGENTS.md
  • src/cmd/setup.rs
**/*.{js,jsx,ts,tsx,py,java,go,rs,rb,php,c,cpp,h,hpp,cs,swift,kt,kts}

📄 CodeRabbit inference engine (CLAUDE.md)

Add comments only when the WHY is non-obvious; do not narrate what the code does, reference the current task, or reference the PR in comments.

Files:

  • src/util/mod.rs
  • src/nu/paths.rs
  • tests/doctor_test.rs
  • src/util/test_paths.rs
  • src/cmd/use_cmd.rs
  • src/util/hints.rs
  • src/cmd/doctor.rs
  • src/nu/version_manager.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/nu/migrate_legacy.rs
  • src/cmd/setup.rs
!**/.env,!**/credentials.json,!**/*.pem

📄 CodeRabbit inference engine (CLAUDE.md)

Do not commit files that appear to contain secrets, including .env, credentials.json, and PEM files; warn before doing so even if explicitly requested.

Files:

  • src/util/mod.rs
  • docs/numan-doctor.md
  • src/nu/paths.rs
  • tests/doctor_test.rs
  • src/util/test_paths.rs
  • src/cmd/use_cmd.rs
  • src/util/hints.rs
  • src/cmd/doctor.rs
  • src/nu/version_manager.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/nu/migrate_legacy.rs
  • AGENTS.md
  • src/cmd/setup.rs
**/*.rs

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.rs: Use the Rust 2021 edition.
Use anyhow::Result with .context(...) in application code; use thiserror for library error types that callers match on.
Use clap derive macros for CLI definitions.
Use serde with serde_json or toml for serialization.
Function parameters must use &Path, not &PathBuf.
Library code must not panic; error paths should return anyhow::Result with context where appropriate.
Add or update tests for behavior changes, including relevant failure paths.
New mutating code paths must acquire the mutation lock via acquire_mutation_lock(root) and snapshot the lockfile before writes.
Lockfile, journal, and state-file JSON writes must use write_json_atomic.
numan install must write only to $NUMAN_ROOT; it must not invoke Nu or register plugins/autoloads.
Only activate and deactivate may modify Nu integration state.
Treat the lockfile as the authoritative source of truth; derived projections such as autoload state must not be authoritative.
Install payloads under versioned, content-addressed paths and never overwrite them in place.
Never overwrite foreign autoload files; respect OWNERSHIP_MARKER.
Pass plugin paths through environment variables only; do not use runtime interpolation in Nu program strings.

**/*.rs: All CI gates must pass: cargo test, cargo clippy -- -D warnings, and cargo fmt --check.
Every mutating command—including install, remove, update, gc, and future nupm import—must call acquire_mutation_lock(root).
Lockfiles, journals, and state files must use write_json_atomic; partial writes are not allowed.
Pending activation, autoload, and lifecycle journals must be stored under $NUMAN_ROOT/state/.
Module autoload identity must match all four fields: Nu executable hash, Nu version, vendor autoload directory, and managed file path; the lockfile module_activation value is authoritative.
Never overwrite foreign autoload files; respect OWNERSHIP_MARKER.
Pass paths to Nu only throu...

Files:

  • src/util/mod.rs
  • src/nu/paths.rs
  • tests/doctor_test.rs
  • src/util/test_paths.rs
  • src/cmd/use_cmd.rs
  • src/util/hints.rs
  • src/cmd/doctor.rs
  • src/nu/version_manager.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/nu/migrate_legacy.rs
  • src/cmd/setup.rs
**/*.{rs,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Run and keep cargo fmt/rustfmt clean, and ensure cargo clippy -- -D warnings passes.

Files:

  • src/util/mod.rs
  • src/nu/paths.rs
  • tests/doctor_test.rs
  • src/util/test_paths.rs
  • src/cmd/use_cmd.rs
  • src/util/hints.rs
  • src/cmd/doctor.rs
  • src/nu/version_manager.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/nu/migrate_legacy.rs
  • src/cmd/setup.rs
**/*.{rs,nu}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.{rs,nu}: Real-Nu acceptance tests must be marked #[ignore] and should be run when changes affect activation or nupm import; unit tests must not spawn real nu and should use injectable seams such as FakeCandidateRunner or registrars.
The nupm integration must be read-only toward NUPM_HOME, must not execute build.nu, and must not perform bidirectional synchronization.

Unit tests must use FakeCandidateRunner or injectable registrars and must not spawn a real nu process.

Files:

  • src/util/mod.rs
  • src/nu/paths.rs
  • tests/doctor_test.rs
  • src/util/test_paths.rs
  • src/cmd/use_cmd.rs
  • src/util/hints.rs
  • src/cmd/doctor.rs
  • src/nu/version_manager.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/nu/migrate_legacy.rs
  • src/cmd/setup.rs
**/*.{rs,md}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Match existing naming, module layout, and documentation level in the file being edited; update AGENTS.md, docs/, or command help when structure, conventions, or user-visible behavior changes.

Tests must cover failure modes, not only successful execution.

Files:

  • src/util/mod.rs
  • docs/numan-doctor.md
  • src/nu/paths.rs
  • tests/doctor_test.rs
  • src/util/test_paths.rs
  • src/cmd/use_cmd.rs
  • src/util/hints.rs
  • src/cmd/doctor.rs
  • src/nu/version_manager.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/nu/migrate_legacy.rs
  • AGENTS.md
  • src/cmd/setup.rs
**/*.{rs,md,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Use the repository's established serialization and module conventions rather than introducing unrelated refactors.

Files:

  • src/util/mod.rs
  • docs/numan-doctor.md
  • src/nu/paths.rs
  • tests/doctor_test.rs
  • src/util/test_paths.rs
  • src/cmd/use_cmd.rs
  • src/util/hints.rs
  • src/cmd/doctor.rs
  • src/nu/version_manager.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/nu/migrate_legacy.rs
  • AGENTS.md
  • src/cmd/setup.rs
src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.rs: Use write_json_atomic for all JSON state files to prevent partial-write corruption.
Use &Path rather than &PathBuf in function parameters.
Use anyhow::Result for application errors, thiserror for matchable library errors, add context with .context(...) or ?, and never panic in library code.
Use the binary build target's platform information via #[cfg(target_env)] rather than runtime constants from std::env::consts.

Files:

  • src/util/mod.rs
  • src/nu/paths.rs
  • src/util/test_paths.rs
  • src/cmd/use_cmd.rs
  • src/util/hints.rs
  • src/cmd/doctor.rs
  • src/nu/version_manager.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/nu/migrate_legacy.rs
  • src/cmd/setup.rs
**/*.md

📄 CodeRabbit inference engine (REVIEW.md)

Update documentation and AGENTS.md when project structure or conventions change.

Files:

  • docs/numan-doctor.md
  • AGENTS.md
src/nu/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

Invoke Nu using paths and names supplied through environment variables; the Nu program string must be a compile-time constant with no runtime interpolation.

Files:

  • src/nu/paths.rs
  • src/nu/version_manager.rs
  • src/nu/bootstrap.rs
  • src/nu/migrate_legacy.rs
tests/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

Keep integration tests under tests/, place unit tests inline with source modules, and test platform-specific behavior with mock platforms.

Files:

  • tests/doctor_test.rs
src/cmd/{install,update,remove,activate,deactivate,init,nupm.rs,doctor,gc}.rs

📄 CodeRabbit inference engine (AGENTS.md)

Create a snapshot before mutating install, update, remove, activate, deactivate, init --refresh, nupm import, or doctor-repair state; garbage collection must treat snapshot-referenced payloads as live.

Files:

  • src/cmd/doctor.rs
src/state/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

Lockfiles must pin immutable artifact paths, and cached artifacts referenced by the lockfile must not be deleted.

Files:

  • src/state/migration_journal.rs
src/state/{journal,plugin_deactivate_journal,migration_journal}.rs

📄 CodeRabbit inference engine (AGENTS.md)

Journal state transitions must be written atomically and reconciled after interruption according to their documented stages.

Files:

  • src/state/migration_journal.rs
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: tonythethompson/numan

Timestamp: 2026-08-03T02:41:41.522Z
Learning: Install operations must remain inert: they must not invoke Nu integration and may only write under `$NUMAN_ROOT`.
Learnt from: CR
Repo: tonythethompson/numan

Timestamp: 2026-08-03T02:41:41.522Z
Learning: Run and maintain the CI gates: `cargo test`, `cargo clippy -- -D warnings`, `cargo fmt --check`, and ignored real-Nu acceptance tests where applicable.
🪛 markdownlint-cli2 (0.23.1)
docs/numan-doctor.md

[warning] 181-181: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


[warning] 292-292: Files should end with a single newline character

(MD047, single-trailing-newline)

🔍 Remote MCP DeepWiki, GitHub Copilot

Review-relevant context

  • Migration stages are Prepared → Renamed → Active; reconciliation trusts filesystem state, protects against symlinked managed directories, and retains journals when cleanup fails.

  • The retrieved PR diff makes numan use list acquire the mutation lock and run legacy migration before listing. This conflicts with the stated “read-only list” behavior; current tests only verify no snapshot, not absence of filesystem mutation.

  • Switching commands snapshot before migration, while list migrates without a snapshot, creating inconsistent mutation semantics within numan use.

  • Public version-management APIs now propagate typed VersionManagerError values for malformed markers, invalid versions, directory-read failures, dangling selections, and traversal paths.

Comment thread src/cmd/doctor.rs
Comment thread src/cmd/doctor.rs Outdated
Comment thread src/cmd/setup.rs
Comment thread src/cmd/use_cmd.rs Outdated
Comment thread src/nu/bootstrap.rs
Comment thread src/nu/migrate_legacy.rs
Comment thread src/state/migration_journal.rs
Comment thread src/state/migration_journal.rs Outdated
Comment thread src/util/test_paths.rs
Take #70 injectable is_tty / require_tty_or_yes_with_tty for download and
already-installed setup paths. Keep PR 69 refuse-in-place --force behavior
and both sides' non-TTY regression tests.

Co-authored-by: Anthony Thompson <github@trackdub.com>
cursoragent and others added 3 commits August 3, 2026 03:38
Record migration-journal load errors as Failed without aborting later
doctor repairs; document migration findings and AGENTS util/use_cmd;
gate already-installed confirm on options.yes; extract migration
complete_migration and use select_version; add coverage for Prepared
recovery, unsafe versions, failed migration continuing, off-tree use,
and list no-migration; close stdin on malformed-lockfile doctor
subprocess; share PathRestoreGuard; strip review provenance comments.

Co-authored-by: Anthony Thompson <github@trackdub.com>
Co-authored-by: Anthony Thompson <github@trackdub.com>
Preserve corrupt active-version bytes before doctor clear, classify
RenamedBinaryMissing as manual migration recovery, and split the
doctor off-PATH repair contract from wipe-protection coverage.

Co-authored-by: Anthony Thompson <github@trackdub.com>
greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 3, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/nu/paths.rs (1)

310-399: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Corrupt-marker handling now fails loud; add failure-path tests to lock it in.

The rewritten match in find_nu_executable_with_root (Lines 320-350) no longer treats every Err from read_active_version the same way. It tolerates only ReadActiveMarker with io::ErrorKind::NotFound (the TOCTOU case) and escalates every other ReadActiveMarker and every MalformedActiveMarker with an actionable numan doctor --fix message. This closes the "wrong Nu" gap the earlier review flagged.

None of the three tests you show for this function (Lines 848-933) exercise the escalation branches. Add a test that writes an unreadable or malformed active-version.json and asserts find_nu_executable_with_root returns an Err mentioning doctor --fix, instead of silently falling through.

🧪 Suggested test skeleton
#[test]
fn find_nu_executable_with_root_escalates_malformed_marker() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path().join("numan-root");
    std::fs::create_dir_all(&root).unwrap();
    let marker_path = crate::nu::version_manager::active_version_path(&root);
    std::fs::create_dir_all(marker_path.parent().unwrap()).unwrap();
    std::fs::write(&marker_path, b"not json").unwrap();

    let err = find_nu_executable_with_root(&root).unwrap_err();
    assert!(err.to_string().contains("doctor --fix"));
}

As per coding guidelines: "Add or update tests for behavior changes, including relevant failure paths."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/nu/paths.rs` around lines 310 - 399, Add a failure-path test alongside
the existing find_nu_executable_with_root tests that creates an
active-version.json containing malformed data, invokes
find_nu_executable_with_root, and asserts it returns an error whose message
includes “doctor --fix”. Ensure the test sets up the marker location under a
temporary root and does not allow fallback to PATH Nu.

Source: Coding guidelines

src/nu/bootstrap.rs (2)

1191-1224: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Restore PATH after execute_nu_setup_already_installed_non_tty_requires_yes.

The options_yes branch of this test runs execute_nu_setup_with_installer with yes: true against an already-installed binary. That path calls prepend_process_path(parent)? unconditionally (Lines 807-809), independent of skip_path. The test never saves or restores PATH, so the temp directory's bin parent stays prepended to the process PATH for the rest of the test binary run, even after the TempDir is dropped.

This PR adds a shared PATH_ENV_LOCK-guarded restoration type in src/util/test_paths.rs for exactly this purpose. Use it here so this test does not leak a dangling path entry into PATH for every test that runs afterward in the same process.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/nu/bootstrap.rs` around lines 1191 - 1224, Update the test
execute_nu_setup_already_installed_non_tty_requires_yes to acquire the shared
PATH_ENV_LOCK-guarded restoration type from test_paths before invoking either
setup branch, ensuring the original PATH is restored when the test exits. Keep
the existing assertions and installer behavior unchanged.

1252-1306: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Isolate the real Nu probe from this unit test.

The PATH scan and register_existing_nu both invoke validate_nushell_binary, which runs std::process::Command::new(nu_exe). Mark this test #[ignore] or inject a validation seam.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/nu/bootstrap.rs` around lines 1252 - 1306, Isolate
register_existing_nu_refuses_non_tty_without_yes_before_path_mutation from
executing a real Nu binary: either mark the test #[ignore] or introduce and use
a validation seam so PATH discovery and register_existing_nu do not invoke
validate_nushell_binary through std::process::Command during this unit test.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/numan-doctor.md`:
- Line 89: Update the migration repair handling around acquire_mutation_lock in
the Doctor repair pass so a lock-acquisition failure is treated like other
migration repair failures: record or skip the failure without propagating it via
?, allowing subsequent repairs to continue. Preserve the existing behavior for
successful lock acquisition and reconciliation.

In `@src/cmd/doctor.rs`:
- Around line 1496-1497: Expose version_manager’s active_version_path helper as
pub(crate), then update the doctor code around marker and backup construction to
call it instead of rebuilding the nu_state/active-version.json path. Derive the
corrupt-backup path from the shared active-version path while preserving the
existing backup filename behavior.
- Around line 1448-1451: Update both apply_repairs lock-acquisition sites in
src/cmd/doctor.rs (1448-1451 and 1490-1495) to handle acquire_mutation_lock
errors without returning early: push the corresponding
journal.migration_repaired record with RepairStatus::Failed, continue or return
collected records, and bind the guard only on success. Add a test that holds the
mutation lock during apply_repairs and verifies both repairs are recorded as
Failed and the call returns Ok. No change is needed to docs/numan-doctor.md:89;
its wording should remain unchanged.

In `@src/nu/migrate_legacy.rs`:
- Around line 272-278: Add a test covering the auto-healed Renamed journal path
through migrate_legacy_install_with_detector: create the version install and
Renamed PendingMigration state, use a detector that must not run, assert the
call succeeds with false, and verify the active version marker is set and the
pending journal is cleared.
- Around line 762-765: Replace the OR assertion in the migration diagnostic test
with a direct check for the full “Discard the journal file to unblock” guidance,
so it fails if that recovery instruction is removed. Apply the same tightening
to the corresponding assertion in migration_journal.rs, preserving the existing
diagnostic formatting.

In `@src/state/migration_journal.rs`:
- Around line 305-306: Update the legacy binary path construction in the
migration journal to use the shared Nu binary-name helper, such as
nu_binary_file_name, instead of the inline cfg!(windows) selection. Keep
versioned_nu_dir(root) unchanged and join it with the helper’s result so legacy
and versioned paths use the same naming source.

---

Outside diff comments:
In `@src/nu/bootstrap.rs`:
- Around line 1191-1224: Update the test
execute_nu_setup_already_installed_non_tty_requires_yes to acquire the shared
PATH_ENV_LOCK-guarded restoration type from test_paths before invoking either
setup branch, ensuring the original PATH is restored when the test exits. Keep
the existing assertions and installer behavior unchanged.
- Around line 1252-1306: Isolate
register_existing_nu_refuses_non_tty_without_yes_before_path_mutation from
executing a real Nu binary: either mark the test #[ignore] or introduce and use
a validation seam so PATH discovery and register_existing_nu do not invoke
validate_nushell_binary through std::process::Command during this unit test.

In `@src/nu/paths.rs`:
- Around line 310-399: Add a failure-path test alongside the existing
find_nu_executable_with_root tests that creates an active-version.json
containing malformed data, invokes find_nu_executable_with_root, and asserts it
returns an error whose message includes “doctor --fix”. Ensure the test sets up
the marker location under a temporary root and does not allow fallback to PATH
Nu.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 69cb802c-3097-4793-abd3-a8e85c05e673

📥 Commits

Reviewing files that changed from the base of the PR and between 432fffb and 0b635b6.

📒 Files selected for processing (10)
  • AGENTS.md
  • docs/numan-doctor.md
  • src/cmd/doctor.rs
  • src/cmd/setup.rs
  • src/cmd/use_cmd.rs
  • src/nu/bootstrap.rs
  • src/nu/migrate_legacy.rs
  • src/nu/paths.rs
  • src/state/migration_journal.rs
  • tests/doctor_test.rs
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • Trackdubllc/Trackdub (manual)
  • tonythethompson/QuickShell (manual)
  • tonythethompson/numan (manual)
  • tonythethompson/dependency-chain-substrate (manual)
📜 Review details
⏰ Context from checks skipped due to timeout. (4)
  • GitHub Check: Greptile Review
  • GitHub Check: Test (windows-latest)
  • GitHub Check: Real-Nu acceptance (windows-latest)
  • GitHub Check: Analyze (rust)
🧰 Additional context used
📓 Path-based instructions (15)
**/*

📄 CodeRabbit inference engine (CLAUDE.md)

**/*: Use Serena's semantic, symbol-aware tools as the primary tools for reading and editing code; use built-in Read, Glob, Grep, and Edit only under the stated exceptions.
Before editing a code file, inspect its symbol overview, read the specific symbols being changed, and edit them with Serena's symbol-aware tools.
Understand the existing code before changing it and make the smallest change that satisfies the request; avoid unrelated cleanup, premature abstractions, unnecessary error handling, feature flags, and compatibility shims.
Prefer editing existing files over creating new ones, and never create Markdown or README files unless explicitly requested.
For exploratory questions, provide a 2–3 sentence recommendation with the main tradeoff and do not implement until the user agrees.
For UI or frontend changes that cannot be tested in a browser, explicitly state that browser testing was not performed rather than claiming success.
Address security issues when discovered, including injection, XSS, SQL injection, path traversal, and secret leaks.
Pause and obtain confirmation before destructive, hard-to-reverse, externally visible, or third-party-upload actions, including deleting files or branches, dropping tables, force-pushing, modifying CI/CD, posting externally, or uploading content.
When blocked, investigate the root cause instead of bypassing it with --no-verify, --force, or deletion; investigate unfamiliar files, branches, and configuration before deleting them.
Only commit when explicitly asked; do not proactively update git configuration or push changes.
Do not skip Git hooks unless explicitly asked; if a pre-commit hook fails, fix the issue, re-stage, and create a new commit rather than amending.
Stage files by name rather than using git add -A or git add ., to avoid accidentally including secrets or large binaries.
Use a HEREDOC for commit messages, and add co-author attribution only when the user explicitly requests the exact trailer.
Do not force-pus...

Files:

  • AGENTS.md
  • docs/numan-doctor.md
  • src/nu/paths.rs
  • src/cmd/use_cmd.rs
  • tests/doctor_test.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/cmd/setup.rs
  • src/nu/migrate_legacy.rs
  • src/cmd/doctor.rs
!**/.env,!**/credentials.json,!**/*.pem

📄 CodeRabbit inference engine (CLAUDE.md)

Do not commit files that appear to contain secrets, including .env, credentials.json, and PEM files; warn before doing so even if explicitly requested.

Files:

  • AGENTS.md
  • docs/numan-doctor.md
  • src/nu/paths.rs
  • src/cmd/use_cmd.rs
  • tests/doctor_test.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/cmd/setup.rs
  • src/nu/migrate_legacy.rs
  • src/cmd/doctor.rs
**/*.{rs,md}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Match existing naming, module layout, and documentation level in the file being edited; update AGENTS.md, docs/, or command help when structure, conventions, or user-visible behavior changes.

Tests must cover failure modes, not only successful execution.

Files:

  • AGENTS.md
  • docs/numan-doctor.md
  • src/nu/paths.rs
  • src/cmd/use_cmd.rs
  • tests/doctor_test.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/cmd/setup.rs
  • src/nu/migrate_legacy.rs
  • src/cmd/doctor.rs
**/*.{rs,md,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Use the repository's established serialization and module conventions rather than introducing unrelated refactors.

Files:

  • AGENTS.md
  • docs/numan-doctor.md
  • src/nu/paths.rs
  • src/cmd/use_cmd.rs
  • tests/doctor_test.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/cmd/setup.rs
  • src/nu/migrate_legacy.rs
  • src/cmd/doctor.rs
**/*.md

📄 CodeRabbit inference engine (REVIEW.md)

Update documentation and AGENTS.md when project structure or conventions change.

Files:

  • AGENTS.md
  • docs/numan-doctor.md
**/*.{js,jsx,ts,tsx,py,java,go,rs,rb,php,c,cpp,h,hpp,cs,swift,kt,kts}

📄 CodeRabbit inference engine (CLAUDE.md)

Add comments only when the WHY is non-obvious; do not narrate what the code does, reference the current task, or reference the PR in comments.

Files:

  • src/nu/paths.rs
  • src/cmd/use_cmd.rs
  • tests/doctor_test.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/cmd/setup.rs
  • src/nu/migrate_legacy.rs
  • src/cmd/doctor.rs
**/*.rs

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.rs: Use the Rust 2021 edition.
Use anyhow::Result with .context(...) in application code; use thiserror for library error types that callers match on.
Use clap derive macros for CLI definitions.
Use serde with serde_json or toml for serialization.
Function parameters must use &Path, not &PathBuf.
Library code must not panic; error paths should return anyhow::Result with context where appropriate.
Add or update tests for behavior changes, including relevant failure paths.
New mutating code paths must acquire the mutation lock via acquire_mutation_lock(root) and snapshot the lockfile before writes.
Lockfile, journal, and state-file JSON writes must use write_json_atomic.
numan install must write only to $NUMAN_ROOT; it must not invoke Nu or register plugins/autoloads.
Only activate and deactivate may modify Nu integration state.
Treat the lockfile as the authoritative source of truth; derived projections such as autoload state must not be authoritative.
Install payloads under versioned, content-addressed paths and never overwrite them in place.
Never overwrite foreign autoload files; respect OWNERSHIP_MARKER.
Pass plugin paths through environment variables only; do not use runtime interpolation in Nu program strings.

**/*.rs: All CI gates must pass: cargo test, cargo clippy -- -D warnings, and cargo fmt --check.
Every mutating command—including install, remove, update, gc, and future nupm import—must call acquire_mutation_lock(root).
Lockfiles, journals, and state files must use write_json_atomic; partial writes are not allowed.
Pending activation, autoload, and lifecycle journals must be stored under $NUMAN_ROOT/state/.
Module autoload identity must match all four fields: Nu executable hash, Nu version, vendor autoload directory, and managed file path; the lockfile module_activation value is authoritative.
Never overwrite foreign autoload files; respect OWNERSHIP_MARKER.
Pass paths to Nu only throu...

Files:

  • src/nu/paths.rs
  • src/cmd/use_cmd.rs
  • tests/doctor_test.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/cmd/setup.rs
  • src/nu/migrate_legacy.rs
  • src/cmd/doctor.rs
**/*.{rs,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Run and keep cargo fmt/rustfmt clean, and ensure cargo clippy -- -D warnings passes.

Files:

  • src/nu/paths.rs
  • src/cmd/use_cmd.rs
  • tests/doctor_test.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/cmd/setup.rs
  • src/nu/migrate_legacy.rs
  • src/cmd/doctor.rs
**/*.{rs,nu}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.{rs,nu}: Real-Nu acceptance tests must be marked #[ignore] and should be run when changes affect activation or nupm import; unit tests must not spawn real nu and should use injectable seams such as FakeCandidateRunner or registrars.
The nupm integration must be read-only toward NUPM_HOME, must not execute build.nu, and must not perform bidirectional synchronization.

Unit tests must use FakeCandidateRunner or injectable registrars and must not spawn a real nu process.

Files:

  • src/nu/paths.rs
  • src/cmd/use_cmd.rs
  • tests/doctor_test.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/cmd/setup.rs
  • src/nu/migrate_legacy.rs
  • src/cmd/doctor.rs
src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.rs: Derive platform detection from compile-time #[cfg(target_env)] values, not std::env::consts; LIBC must be a compile-time constant.
Use Rust 2021-compatible code and maintain compatibility with the declared MSRV of Rust 1.88.

Files:

  • src/nu/paths.rs
  • src/cmd/use_cmd.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/cmd/setup.rs
  • src/nu/migrate_legacy.rs
  • src/cmd/doctor.rs
src/nu/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

Use FakeCandidateRunner as the test seam for module candidate validation rather than invoking a real Nu process in unit tests.

Files:

  • src/nu/paths.rs
  • src/nu/bootstrap.rs
  • src/nu/migrate_legacy.rs
src/cmd/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

Create an activation snapshot before mutations performed by install, update, remove, activate, deactivate, init --refresh, nupm import, or doctor repair.

Files:

  • src/cmd/use_cmd.rs
  • src/cmd/setup.rs
  • src/cmd/doctor.rs
tests/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

Maintain unit and integration coverage, test platform-specific behavior with mock platforms, and run real-Nu acceptance tests only in the ignored acceptance suite.

Files:

  • tests/doctor_test.rs
src/state/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

src/state/**/*.rs: Lockfiles must pin immutable artifact paths, and cached artifacts must be retained while referenced.
Write JSON state files atomically using write_json_atomic with a same-directory temporary file and persist operation.

Files:

  • src/state/migration_journal.rs
src/state/{journal,plugin_deactivate_journal,migration_journal,autoload_journal,lifecycle_journal}.rs

📄 CodeRabbit inference engine (AGENTS.md)

Journal multi-step mutations and advance stages atomically so interrupted operations can be reconciled safely.

Files:

  • src/state/migration_journal.rs
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: tonythethompson/numan

Timestamp: 2026-08-03T03:44:24.622Z
Learning: Do not force-push to `master`; use feature branches and squash-merge feature work.
🔍 Remote MCP GitHub Copilot

Additional review context

  • Related PR #82 shows the migration design uses Prepared → Renamed → Active, with atomic journal persistence and filesystem-truth-based reconciliation. Renamed recovery writes the active marker only when no active selection already exists, preserving a user’s existing selection.

  • PR #82’s numan use list implementation acquired the mutation lock and ran legacy migration before listing, whereas the current PR diff changes list to bypass both locking and migration. This is a behavioral change that should be explicitly verified against the intended “read-only list” contract.

  • The migration journal’s Prepared reconciliation removes only the associated version directory and retains the journal if removal fails; Renamed reconciliation requires the versioned binary to exist and preserves an already-selected active version. These are important failure-path invariants for doctor repair.

  • PR #82 reports the scope already spans 23 files and 48 commits, combining CLI confirmation changes, typed version-manager errors, setup locking, migration journaling, and path discovery. This breadth increases the risk of cross-feature regressions and makes focused validation of lock ordering and marker semantics especially important.

🔇 Additional comments (21)
src/state/migration_journal.rs (4)

242-246: Historical narration about a previous revision remains in the comment, but the durable rationale on lines 242-243 is correct and sufficient. Not worth a change request.


113-116: LGTM!


355-398: LGTM!


756-823: LGTM!

src/nu/migrate_legacy.rs (2)

43-53: LGTM!


245-245: LGTM!

Also applies to: 438-438

AGENTS.md (1)

68-68: LGTM!

Also applies to: 83-83, 91-92, 97-99

src/cmd/use_cmd.rs (2)

102-120: LGTM!

Also applies to: 165-165


287-346: LGTM!

src/cmd/doctor.rs (3)

659-688: LGTM!


1666-1666: LGTM!


2444-2661: LGTM!

docs/numan-doctor.md (1)

79-79: LGTM!

Also applies to: 99-99, 134-134, 152-153

tests/doctor_test.rs (3)

35-35: LGTM!

Also applies to: 54-54, 552-562


306-306: LGTM!


98-115: 🩺 Stability & Availability

Keep execute_nu_use_existing_missing_path_keeps_managed ungated. It runs independently of the Unix-only helper and neighboring test, including on Windows.

			> Likely an incorrect or invalid review comment.
src/cmd/setup.rs (3)

302-302: LGTM!

Also applies to: 439-439, 530-530


429-440: 🎯 Functional Correctness

No change required. The no-managed execute_use_existing test passes yes: true, so the nested TTY guard succeeds without an is_tty override. Production callers do not inject confirm.

			> Likely an incorrect or invalid review comment.

188-197: 🎯 Functional Correctness

No lock-ownership issue exists in the setup repair path. execute_nu_impl acquires setup_subcommand_lock once and delegates to execute_nu_impl_locked; execute_nu_repair can call it directly without a missing lock or double-lock deadlock.

			> Likely an incorrect or invalid review comment.
src/nu/bootstrap.rs (2)

468-470: LGTM!

Also applies to: 505-524, 713-717, 776-793, 824-834


1226-1250: LGTM!

Comment thread docs/numan-doctor.md
Comment thread src/cmd/doctor.rs Outdated
Comment thread src/cmd/doctor.rs Outdated
Comment thread src/nu/migrate_legacy.rs
Comment thread src/nu/migrate_legacy.rs
Comment thread src/state/migration_journal.rs Outdated
Treat mutation-lock failures during migration and active-marker repairs as
Failed records so later Auto repairs still run. Share active_version_path,
cover Renamed auto-heal and malformed-marker lookup, isolate bootstrap PATH
tests, and use nu_binary_name in the migration journal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Co-authored-by: Anthony Thompson <github@trackdub.com>
Consolidate setup nu use consent gates, surface legacy migration success,
preserve typed detect/hook errors, share PathRestoreGuard across PATH
tests, and keep doctor repairs moving after an unreadable migration journal.

Co-authored-by: Anthony Thompson <github@trackdub.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 3, 2026
greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 3, 2026
Real-Nu CI runs cargo test with --ignored, which compiles rustdoc
ignore fences. Use a text example so the illustrative snippet is not
treated as a doctest.

Co-authored-by: Anthony Thompson <github@trackdub.com>
@cursor
cursor Bot dismissed stale reviews from greptile-apps[bot] and coderabbitai[bot] via 170f1ed August 3, 2026 10:12

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/nu/bootstrap.rs (1)

541-562: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Write the active-version marker before the persistent PATH change.

persist_path_dir at Line 543 modifies the shell profile on Unix and the user PATH on Windows. write_active_version_with_binary at Line 561 runs after it. If the marker write fails, the user keeps a permanent PATH entry and has no active selection. Nu resolution then falls back to PATH with no record of the registration.

The comment at Lines 505-507 already treats "failing after PATH mutation" as the hazard to avoid. The semver fallback removes one cause; an IO failure on the marker write remains.

Move the marker write ahead of the persistent PATH write. Keep prepend_process_path where it is, because it affects only this process.

♻️ Proposed reorder
     prepend_process_path(&parent)?;
+    version_manager::write_active_version_with_binary(root, &version, &resolved)
+        .with_context(|| format!("Failed to persist active Nu version '{}'", version))?;
+
     if !options.skip_path {
         persist_path_dir(&parent)?;
-    version_manager::write_active_version_with_binary(root, &version, &resolved)
-        .with_context(|| format!("Failed to persist active Nu version '{}'", version))?;
-
     println!();

Add a test that forces the marker write to fail and asserts the shell profile is unchanged.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/nu/bootstrap.rs` around lines 541 - 562, Move
version_manager::write_active_version_with_binary ahead of persist_path_dir
while keeping prepend_process_path unchanged, so marker persistence completes
before any permanent PATH mutation. Add a test covering marker-write failure
that verifies the shell profile remains unchanged.
src/cmd/doctor.rs (1)

1080-1122: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Soft-failing the lock lets later repairs mutate state without the mutation lock.

The soft-fail keeps apply_repairs running, which is the stated goal. It also removes the lock from the repairs that never reacquire it:

  • Line 1103: create_snapshot copies the lockfile while another process can be mutating it.
  • Line 1130: create_dir_all(root.join(dir)) for every layout repair.
  • Line 1154: ensure_official_registry_config writes config.toml.

The comment at Lines 1145-1147 states the contract for that config write: "Config write does not reacquire the mutation lock; keep it under doctor's lock." After this change, that lock can be absent. A concurrent numan install or numan use then interleaves with a doctor snapshot and a config write.

Keep the soft-fail, but gate the lock-owning section on lock.is_some() and record Skipped for those repairs. The migration and active-version repairs reacquire the lock themselves, so they still reach their Failed records.

As per coding guidelines: "New mutating paths must acquire the mutation lock and snapshot the lockfile before making changes."

🔒 Proposed fix
     let mut records = Vec::new();
     // Snapshot failure must not block independent layout/config repairs.
     // Nested mutations that rely on a PreMutation baseline are skipped instead.
     let mut snapshot_ok = true;
-    if needs_lock {
+    if needs_lock && lock.is_some() {
         if let Err(e) = create_snapshot(
-    for dir in LAYOUT_DIRS {
+    for dir in LAYOUT_DIRS {
         let id = format!("layout.{dir}");
         if findings
             .iter()
             .any(|f| f.id == id && f.severity == Severity::Warn)
         {
+            if lock.is_none() {
+                records.push(RepairRecord {
+                    id,
+                    status: RepairStatus::Skipped,
+                    reason: Some("mutation_lock_unavailable".to_string()),
+                });
+                continue;
+            }
             match std::fs::create_dir_all(root.join(dir)) {

Apply the same lock.is_none() guard to the registry.none repair at Line 1148.

Add a test that holds the lock, supplies a layout.* finding and a registry.none finding, and asserts both are recorded as Skipped and no directory or config file is written.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cmd/doctor.rs` around lines 1080 - 1122, Keep the soft-fail behavior in
apply_repairs, but gate lock-owning mutations on lock.is_some(): skip and record
RepairStatus::Skipped for the PreMutation create_snapshot flow, layout.*
directory repairs, ensure_official_registry_config, and registry.none when
lock.is_none(). Leave migration and active-version repairs reachable because
they reacquire the lock themselves. Add a test that holds the lock with layout.*
and registry.none findings, asserting both are Skipped and no directory or
config file is created.

Source: Coding guidelines

src/nu/migrate_legacy.rs (1)

309-332: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Pre-journal empty-subdir cleanup still swallows removal failures.

Line 330 discards the remove_dir result with let _ = .... If the directory is not actually empty, found_installed stays false and migration proceeds as if the sibling directory is harmless. This repeats every invocation without surfacing the problem.

This same failure mode was fixed for the journaled Prepared stage in src/state/migration_journal.rs, where removal failures are now surfaced instead of silently discarded. This loop is the pre-journal-era counterpart and still uses the old, discarded-error pattern.

Treat an unremovable "empty" subdir like a populated install so a foreign directory blocks migration until the user resolves it, instead of retrying the same masked failure indefinitely.

🔧 Proposed fix
             if entry.path().join(bin_name).exists() {
                 found_installed = true;
             } else {
-                // Empty subdir — likely from an aborted previous migration.
-                // Remove it so the user is not permanently stuck with an
-                // empty <version>/ blocking every future attempt.
-                let _ = std::fs::remove_dir(entry.path());
+                // Empty subdir — likely from an aborted previous migration.
+                // Remove it so the user is not permanently stuck with an
+                // empty <version>/ blocking every future attempt. If removal
+                // fails, the directory holds unexpected content; treat it
+                // like a populated install instead of silently retrying.
+                if std::fs::remove_dir(entry.path()).is_err() {
+                    found_installed = true;
+                }
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/nu/migrate_legacy.rs` around lines 309 - 332, Update the
empty-subdirectory cleanup in the legacy migration loop to handle remove_dir
failures instead of discarding them: when removal fails, set found_installed to
true so the foreign or non-empty directory blocks migration; retain the current
removal attempt and successful cleanup behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/nu/paths.rs`:
- Around line 912-935: Gate the test-only `test_paths` module in
`src/util/mod.rs` with `#[cfg(test)]`, ensuring `PathRestoreGuard`, its `new`
and `Default` implementations, and `PATH_ENV_LOCK` are not compiled or publicly
exported in release builds. Preserve access for unit tests such as
`find_nu_executable_with_root` without adding non-test exports.

---

Outside diff comments:
In `@src/cmd/doctor.rs`:
- Around line 1080-1122: Keep the soft-fail behavior in apply_repairs, but gate
lock-owning mutations on lock.is_some(): skip and record RepairStatus::Skipped
for the PreMutation create_snapshot flow, layout.* directory repairs,
ensure_official_registry_config, and registry.none when lock.is_none(). Leave
migration and active-version repairs reachable because they reacquire the lock
themselves. Add a test that holds the lock with layout.* and registry.none
findings, asserting both are Skipped and no directory or config file is created.

In `@src/nu/bootstrap.rs`:
- Around line 541-562: Move version_manager::write_active_version_with_binary
ahead of persist_path_dir while keeping prepend_process_path unchanged, so
marker persistence completes before any permanent PATH mutation. Add a test
covering marker-write failure that verifies the shell profile remains unchanged.

In `@src/nu/migrate_legacy.rs`:
- Around line 309-332: Update the empty-subdirectory cleanup in the legacy
migration loop to handle remove_dir failures instead of discarding them: when
removal fails, set found_installed to true so the foreign or non-empty directory
blocks migration; retain the current removal attempt and successful cleanup
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bf49094a-12dc-4271-93fa-db87b711c79e

📥 Commits

Reviewing files that changed from the base of the PR and between 0b635b6 and 170f1ed.

📒 Files selected for processing (12)
  • src/cmd/doctor.rs
  • src/cmd/setup.rs
  • src/cmd/use_cmd.rs
  • src/nu/bootstrap.rs
  • src/nu/migrate_legacy.rs
  • src/nu/paths.rs
  • src/nu/version_manager.rs
  • src/state/migration_journal.rs
  • src/util/hints.rs
  • src/util/mod.rs
  • src/util/test_paths.rs
  • tests/doctor_test.rs
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • tonythethompson/QuickShell (manual)
  • tonythethompson/numan (manual)
  • tonythethompson/dependency-chain-substrate (manual)
📜 Review details
⏰ Context from checks skipped due to timeout. (4)
  • GitHub Check: Greptile Review
  • GitHub Check: Real-Nu acceptance (windows-latest)
  • GitHub Check: Test (windows-latest)
  • GitHub Check: Analyze (rust)
🧰 Additional context used
📓 Path-based instructions (14)
**/*

📄 CodeRabbit inference engine (CLAUDE.md)

**/*: Use Serena's semantic, symbol-aware tools as the primary tools for reading and editing code; use built-in Read, Glob, Grep, and Edit only under the stated exceptions.
Before editing a code file, inspect its symbol overview, read the specific symbols being changed, and edit them with Serena's symbol-aware tools.
Understand the existing code before changing it and make the smallest change that satisfies the request; avoid unrelated cleanup, premature abstractions, unnecessary error handling, feature flags, and compatibility shims.
Prefer editing existing files over creating new ones, and never create Markdown or README files unless explicitly requested.
For exploratory questions, provide a 2–3 sentence recommendation with the main tradeoff and do not implement until the user agrees.
For UI or frontend changes that cannot be tested in a browser, explicitly state that browser testing was not performed rather than claiming success.
Address security issues when discovered, including injection, XSS, SQL injection, path traversal, and secret leaks.
Pause and obtain confirmation before destructive, hard-to-reverse, externally visible, or third-party-upload actions, including deleting files or branches, dropping tables, force-pushing, modifying CI/CD, posting externally, or uploading content.
When blocked, investigate the root cause instead of bypassing it with --no-verify, --force, or deletion; investigate unfamiliar files, branches, and configuration before deleting them.
Only commit when explicitly asked; do not proactively update git configuration or push changes.
Do not skip Git hooks unless explicitly asked; if a pre-commit hook fails, fix the issue, re-stage, and create a new commit rather than amending.
Stage files by name rather than using git add -A or git add ., to avoid accidentally including secrets or large binaries.
Use a HEREDOC for commit messages, and add co-author attribution only when the user explicitly requests the exact trailer.
Do not force-pus...

Files:

  • src/util/mod.rs
  • src/util/test_paths.rs
  • src/cmd/use_cmd.rs
  • src/util/hints.rs
  • src/nu/paths.rs
  • tests/doctor_test.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/cmd/setup.rs
  • src/nu/version_manager.rs
  • src/cmd/doctor.rs
  • src/nu/migrate_legacy.rs
**/*.{js,jsx,ts,tsx,py,java,go,rs,rb,php,c,cpp,h,hpp,cs,swift,kt,kts}

📄 CodeRabbit inference engine (CLAUDE.md)

Add comments only when the WHY is non-obvious; do not narrate what the code does, reference the current task, or reference the PR in comments.

Files:

  • src/util/mod.rs
  • src/util/test_paths.rs
  • src/cmd/use_cmd.rs
  • src/util/hints.rs
  • src/nu/paths.rs
  • tests/doctor_test.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/cmd/setup.rs
  • src/nu/version_manager.rs
  • src/cmd/doctor.rs
  • src/nu/migrate_legacy.rs
!**/.env,!**/credentials.json,!**/*.pem

📄 CodeRabbit inference engine (CLAUDE.md)

Do not commit files that appear to contain secrets, including .env, credentials.json, and PEM files; warn before doing so even if explicitly requested.

Files:

  • src/util/mod.rs
  • src/util/test_paths.rs
  • src/cmd/use_cmd.rs
  • src/util/hints.rs
  • src/nu/paths.rs
  • tests/doctor_test.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/cmd/setup.rs
  • src/nu/version_manager.rs
  • src/cmd/doctor.rs
  • src/nu/migrate_legacy.rs
**/*.rs

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.rs: Use the Rust 2021 edition.
Use anyhow::Result with .context(...) in application code; use thiserror for library error types that callers match on.
Use clap derive macros for CLI definitions.
Use serde with serde_json or toml for serialization.
Function parameters must use &Path, not &PathBuf.
Library code must not panic; error paths should return anyhow::Result with context where appropriate.
Add or update tests for behavior changes, including relevant failure paths.
New mutating code paths must acquire the mutation lock via acquire_mutation_lock(root) and snapshot the lockfile before writes.
Lockfile, journal, and state-file JSON writes must use write_json_atomic.
numan install must write only to $NUMAN_ROOT; it must not invoke Nu or register plugins/autoloads.
Only activate and deactivate may modify Nu integration state.
Treat the lockfile as the authoritative source of truth; derived projections such as autoload state must not be authoritative.
Install payloads under versioned, content-addressed paths and never overwrite them in place.
Never overwrite foreign autoload files; respect OWNERSHIP_MARKER.
Pass plugin paths through environment variables only; do not use runtime interpolation in Nu program strings.

**/*.rs: All CI gates must pass: cargo test, cargo clippy -- -D warnings, and cargo fmt --check.
Every mutating command—including install, remove, update, gc, and future nupm import—must call acquire_mutation_lock(root).
Lockfiles, journals, and state files must use write_json_atomic; partial writes are not allowed.
Pending activation, autoload, and lifecycle journals must be stored under $NUMAN_ROOT/state/.
Module autoload identity must match all four fields: Nu executable hash, Nu version, vendor autoload directory, and managed file path; the lockfile module_activation value is authoritative.
Never overwrite foreign autoload files; respect OWNERSHIP_MARKER.
Pass paths to Nu only throu...

Files:

  • src/util/mod.rs
  • src/util/test_paths.rs
  • src/cmd/use_cmd.rs
  • src/util/hints.rs
  • src/nu/paths.rs
  • tests/doctor_test.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/cmd/setup.rs
  • src/nu/version_manager.rs
  • src/cmd/doctor.rs
  • src/nu/migrate_legacy.rs
**/*.{rs,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Run and keep cargo fmt/rustfmt clean, and ensure cargo clippy -- -D warnings passes.

Files:

  • src/util/mod.rs
  • src/util/test_paths.rs
  • src/cmd/use_cmd.rs
  • src/util/hints.rs
  • src/nu/paths.rs
  • tests/doctor_test.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/cmd/setup.rs
  • src/nu/version_manager.rs
  • src/cmd/doctor.rs
  • src/nu/migrate_legacy.rs
**/*.{rs,nu}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.{rs,nu}: Real-Nu acceptance tests must be marked #[ignore] and should be run when changes affect activation or nupm import; unit tests must not spawn real nu and should use injectable seams such as FakeCandidateRunner or registrars.
The nupm integration must be read-only toward NUPM_HOME, must not execute build.nu, and must not perform bidirectional synchronization.

Unit tests must use FakeCandidateRunner or injectable registrars and must not spawn a real nu process.

Files:

  • src/util/mod.rs
  • src/util/test_paths.rs
  • src/cmd/use_cmd.rs
  • src/util/hints.rs
  • src/nu/paths.rs
  • tests/doctor_test.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/cmd/setup.rs
  • src/nu/version_manager.rs
  • src/cmd/doctor.rs
  • src/nu/migrate_legacy.rs
**/*.{rs,md}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Match existing naming, module layout, and documentation level in the file being edited; update AGENTS.md, docs/, or command help when structure, conventions, or user-visible behavior changes.

Tests must cover failure modes, not only successful execution.

Files:

  • src/util/mod.rs
  • src/util/test_paths.rs
  • src/cmd/use_cmd.rs
  • src/util/hints.rs
  • src/nu/paths.rs
  • tests/doctor_test.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/cmd/setup.rs
  • src/nu/version_manager.rs
  • src/cmd/doctor.rs
  • src/nu/migrate_legacy.rs
**/*.{rs,md,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Use the repository's established serialization and module conventions rather than introducing unrelated refactors.

Files:

  • src/util/mod.rs
  • src/util/test_paths.rs
  • src/cmd/use_cmd.rs
  • src/util/hints.rs
  • src/nu/paths.rs
  • tests/doctor_test.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/cmd/setup.rs
  • src/nu/version_manager.rs
  • src/cmd/doctor.rs
  • src/nu/migrate_legacy.rs
src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

Pass Nu paths and names only through NUMAN_PLUGIN_BINARY, NUMAN_PLUGIN_CONFIG, and NUMAN_PLUGIN_NAME; keep the Nu program string a compile-time constant with no runtime interpolation.

Files:

  • src/util/mod.rs
  • src/util/test_paths.rs
  • src/cmd/use_cmd.rs
  • src/util/hints.rs
  • src/nu/paths.rs
  • src/nu/bootstrap.rs
  • src/state/migration_journal.rs
  • src/cmd/setup.rs
  • src/nu/version_manager.rs
  • src/cmd/doctor.rs
  • src/nu/migrate_legacy.rs
tests/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

Use unit tests inline with source modules and integration tests under tests/; test platform-specific code with mock platforms.

Files:

  • tests/doctor_test.rs
src/state/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

src/state/**/*.rs: The lockfile is authoritative for module activation; autoload-state.json is only a derived fast-check projection.
Write JSON state files atomically using write_json_atomic with a temporary file in the same directory and persistence.

Files:

  • src/state/migration_journal.rs
src/state/migration_journal.rs

📄 CodeRabbit inference engine (AGENTS.md)

Journal legacy Nushell migration through PreparedRenamedActive; reconcile filesystem truth and self-heal before migration, with doctor repair support.

Files:

  • src/state/migration_journal.rs
src/nu/version_manager.rs

📄 CodeRabbit inference engine (AGENTS.md)

Treat nu_state/active-version.json as the sole authority for the selected managed Nushell version; preserve optional off-tree binary_path data.

Files:

  • src/nu/version_manager.rs
src/cmd/{install,update,remove,activate,deactivate,init,nupm,doctor}.rs

📄 CodeRabbit inference engine (AGENTS.md)

Create a state snapshot before install, update, remove, activate, deactivate, init --refresh, nupm import, and doctor repair mutations.

Files:

  • src/cmd/doctor.rs
🔍 Remote MCP GitHub Copilot

Relevant review context

  • The reviewed PR is #69, “journaled legacy migration”, targeting master; it adds MigrationStage::{Prepared, Renamed, Active} and typed migration/journal errors.
  • Prepared reconciliation removes an orphan version directory and retains the journal if removal fails; Renamed recovery requires the versioned binary, preserves an existing active selection, and retains the journal on manual-recovery errors.
  • Potential inconsistency: migrate_legacy_install_with_detector still ignores failures from cleanup of pre-journal empty/version directories (let _ = std::fs::remove_dir(...)), whereas journaled Prepared reconciliation explicitly surfaces removal failures and retains recovery state. This may allow migration to continue despite an unresolved filesystem artifact.
  • The migration scan treats any existing nu path as a populated install via .exists(), while journal reconciliation requires .is_file(). A directory or symlink named nu could therefore suppress migration in one path but not qualify as a valid binary in recovery.
  • The current detect_legacy_version implementation uses blocking Command::output() without a timeout, despite the stated concern that version probing occurs while migration holds the mutation lock. A hung legacy binary could block migration and serialized mutations indefinitely.
  • Cargo.toml pins Rust 1.88 and thiserror 2; the new typed public error APIs should therefore be checked for compatibility with that declared toolchain.
🔇 Additional comments (15)
src/nu/version_manager.rs (1)

88-90: LGTM!

Also applies to: 514-531, 565-575

src/nu/paths.rs (1)

604-618: LGTM!

Also applies to: 841-841, 867-867, 890-890

src/cmd/doctor.rs (1)

1444-1506: LGTM!

Also applies to: 1512-1564, 2652-2764

src/util/hints.rs (1)

35-39: LGTM!

Also applies to: 75-80, 235-238

src/cmd/setup.rs (2)

365-379: LGTM!

Also applies to: 458-470


436-439: 🗄️ Data Integrity & Integration

No change needed.

remove_managed_nu_if_present only removes the managed Nu directory and does not call clear_active_version, so the active marker already persisted by register_existing_nu remains intact.

src/nu/bootstrap.rs (2)

488-516: LGTM!


1076-1139: LGTM!

Also applies to: 1185-1230, 1258-1298

src/state/migration_journal.rs (1)

37-38: LGTM!

Also applies to: 95-110, 128-139, 288-317, 418-420, 677-679

src/nu/migrate_legacy.rs (2)

12-13: LGTM!

Also applies to: 742-777, 807-810


60-65: 🗄️ Data Integrity & Integration

No external DetectFailed / PostCreateHook destructure found.

The only Rust references for these variants are within src/nu/migrate_legacy.rs, so no caller currently depends on the removed message field.

src/cmd/use_cmd.rs (1)

74-79: LGTM!

src/util/test_paths.rs (1)

1-5: LGTM!

Also applies to: 24-26

src/util/mod.rs (1)

6-7: LGTM!

tests/doctor_test.rs (1)

18-18: LGTM!

Also applies to: 431-432, 479-480, 517-518, 696-697

Comment thread src/nu/paths.rs
Keep PathRestoreGuard out of non-test library exports, and duplicate a
local guard in doctor_test so integration tests still isolate PATH.

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tests/doctor_test.rs (2)

130-146: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Compare the managed binary bytes before the failure.

managed.exists() is too weak: a failed off-path resolve must not delete or overwrite the managed installation, so read managed before execute_nu(...).expect_err(...) and assert the bytes are unchanged afterward.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/doctor_test.rs` around lines 130 - 146, Update
execute_nu_use_existing_missing_path_keeps_managed to read and store the managed
binary’s bytes before calling setup::execute_nu. After the expected resolution
error, read the file again and assert its bytes exactly match the pre-call
contents, replacing the managed.exists() check while preserving the failure
assertion.

86-86: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add a failing setup-repair path for doctor --fix.

nu_setup_repair_test() now records the call and returns success for all inputs. Keep that success case separate and cover a setup-repair failure with TestNushellBinary validation or an injected error, asserting RepairStatus::Failed in JSON. Also cover the pre-condition path where nu.binary.found_off_path is not a warning so setup repair cannot run.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/doctor_test.rs` at line 86, Add failure-path coverage to
nu_setup_repair_test while preserving its existing success case: use
TestNushellBinary validation or an injected error to make setup repair fail,
then assert the doctor --fix JSON reports RepairStatus::Failed. Also add
coverage for the precondition where nu.binary.found_off_path is not a warning
and verify setup repair is not run.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tests/doctor_test.rs`:
- Around line 130-146: Update execute_nu_use_existing_missing_path_keeps_managed
to read and store the managed binary’s bytes before calling setup::execute_nu.
After the expected resolution error, read the file again and assert its bytes
exactly match the pre-call contents, replacing the managed.exists() check while
preserving the failure assertion.
- Line 86: Add failure-path coverage to nu_setup_repair_test while preserving
its existing success case: use TestNushellBinary validation or an injected error
to make setup repair fail, then assert the doctor --fix JSON reports
RepairStatus::Failed. Also add coverage for the precondition where
nu.binary.found_off_path is not a warning and verify setup repair is not run.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 10eac69a-9199-479f-ac50-86d09653950a

📥 Commits

Reviewing files that changed from the base of the PR and between 170f1ed and 10c863d.

📒 Files selected for processing (3)
  • src/util/mod.rs
  • src/util/test_paths.rs
  • tests/doctor_test.rs
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • tonythethompson/QuickShell (manual)
  • tonythethompson/numan (manual)
  • tonythethompson/dependency-chain-substrate (manual)
📜 Review details
⏰ Context from checks skipped due to timeout. (6)
  • GitHub Check: Greptile Review
  • GitHub Check: Test (windows-latest)
  • GitHub Check: Test (ubuntu-latest)
  • GitHub Check: Real-Nu acceptance (windows-latest)
  • GitHub Check: Analyze (python)
  • GitHub Check: Analyze (rust)
🧰 Additional context used
📓 Path-based instructions (10)
**/*

📄 CodeRabbit inference engine (CLAUDE.md)

**/*: Use Serena's semantic, symbol-aware tools as the primary tools for reading and editing code; use built-in Read, Glob, Grep, and Edit only under the stated exceptions.
Before editing a code file, inspect its symbol overview, read the specific symbols being changed, and edit them with Serena's symbol-aware tools.
Understand the existing code before changing it and make the smallest change that satisfies the request; avoid unrelated cleanup, premature abstractions, unnecessary error handling, feature flags, and compatibility shims.
Prefer editing existing files over creating new ones, and never create Markdown or README files unless explicitly requested.
For exploratory questions, provide a 2–3 sentence recommendation with the main tradeoff and do not implement until the user agrees.
For UI or frontend changes that cannot be tested in a browser, explicitly state that browser testing was not performed rather than claiming success.
Address security issues when discovered, including injection, XSS, SQL injection, path traversal, and secret leaks.
Pause and obtain confirmation before destructive, hard-to-reverse, externally visible, or third-party-upload actions, including deleting files or branches, dropping tables, force-pushing, modifying CI/CD, posting externally, or uploading content.
When blocked, investigate the root cause instead of bypassing it with --no-verify, --force, or deletion; investigate unfamiliar files, branches, and configuration before deleting them.
Only commit when explicitly asked; do not proactively update git configuration or push changes.
Do not skip Git hooks unless explicitly asked; if a pre-commit hook fails, fix the issue, re-stage, and create a new commit rather than amending.
Stage files by name rather than using git add -A or git add ., to avoid accidentally including secrets or large binaries.
Use a HEREDOC for commit messages, and add co-author attribution only when the user explicitly requests the exact trailer.
Do not force-pus...

Files:

  • src/util/mod.rs
  • src/util/test_paths.rs
  • tests/doctor_test.rs
**/*.{js,jsx,ts,tsx,py,java,go,rs,rb,php,c,cpp,h,hpp,cs,swift,kt,kts}

📄 CodeRabbit inference engine (CLAUDE.md)

Add comments only when the WHY is non-obvious; do not narrate what the code does, reference the current task, or reference the PR in comments.

Files:

  • src/util/mod.rs
  • src/util/test_paths.rs
  • tests/doctor_test.rs
!**/.env,!**/credentials.json,!**/*.pem

📄 CodeRabbit inference engine (CLAUDE.md)

Do not commit files that appear to contain secrets, including .env, credentials.json, and PEM files; warn before doing so even if explicitly requested.

Files:

  • src/util/mod.rs
  • src/util/test_paths.rs
  • tests/doctor_test.rs
**/*.rs

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.rs: Use the Rust 2021 edition.
Use anyhow::Result with .context(...) in application code; use thiserror for library error types that callers match on.
Use clap derive macros for CLI definitions.
Use serde with serde_json or toml for serialization.
Function parameters must use &Path, not &PathBuf.
Library code must not panic; error paths should return anyhow::Result with context where appropriate.
Add or update tests for behavior changes, including relevant failure paths.
New mutating code paths must acquire the mutation lock via acquire_mutation_lock(root) and snapshot the lockfile before writes.
Lockfile, journal, and state-file JSON writes must use write_json_atomic.
numan install must write only to $NUMAN_ROOT; it must not invoke Nu or register plugins/autoloads.
Only activate and deactivate may modify Nu integration state.
Treat the lockfile as the authoritative source of truth; derived projections such as autoload state must not be authoritative.
Install payloads under versioned, content-addressed paths and never overwrite them in place.
Never overwrite foreign autoload files; respect OWNERSHIP_MARKER.
Pass plugin paths through environment variables only; do not use runtime interpolation in Nu program strings.

**/*.rs: All CI gates must pass: cargo test, cargo clippy -- -D warnings, and cargo fmt --check.
Every mutating command—including install, remove, update, gc, and future nupm import—must call acquire_mutation_lock(root).
Lockfiles, journals, and state files must use write_json_atomic; partial writes are not allowed.
Pending activation, autoload, and lifecycle journals must be stored under $NUMAN_ROOT/state/.
Module autoload identity must match all four fields: Nu executable hash, Nu version, vendor autoload directory, and managed file path; the lockfile module_activation value is authoritative.
Never overwrite foreign autoload files; respect OWNERSHIP_MARKER.
Pass paths to Nu only throu...

Files:

  • src/util/mod.rs
  • src/util/test_paths.rs
  • tests/doctor_test.rs
**/*.{rs,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Run and keep cargo fmt/rustfmt clean, and ensure cargo clippy -- -D warnings passes.

Files:

  • src/util/mod.rs
  • src/util/test_paths.rs
  • tests/doctor_test.rs
**/*.{rs,nu}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.{rs,nu}: Real-Nu acceptance tests must be marked #[ignore] and should be run when changes affect activation or nupm import; unit tests must not spawn real nu and should use injectable seams such as FakeCandidateRunner or registrars.
The nupm integration must be read-only toward NUPM_HOME, must not execute build.nu, and must not perform bidirectional synchronization.

Unit tests must use FakeCandidateRunner or injectable registrars and must not spawn a real nu process.

Files:

  • src/util/mod.rs
  • src/util/test_paths.rs
  • tests/doctor_test.rs
**/*.{rs,md}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Match existing naming, module layout, and documentation level in the file being edited; update AGENTS.md, docs/, or command help when structure, conventions, or user-visible behavior changes.

Tests must cover failure modes, not only successful execution.

Files:

  • src/util/mod.rs
  • src/util/test_paths.rs
  • tests/doctor_test.rs
**/*.{rs,md,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Use the repository's established serialization and module conventions rather than introducing unrelated refactors.

Files:

  • src/util/mod.rs
  • src/util/test_paths.rs
  • tests/doctor_test.rs
src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.rs: Acquire acquire_mutation_lock(root) before mutating shared state; the lock is non-blocking and a second acquisition must fail immediately.
Pass Nu paths and names through environment variables (NUMAN_PLUGIN_BINARY, NUMAN_PLUGIN_CONFIG, and NUMAN_PLUGIN_NAME); keep the Nu program string a compile-time constant without runtime interpolation.
Use PluginActivation identity (nu_executable_sha256, nu_version, plugin_registry_path) rather than a bare active boolean.
Use cargo fmt formatting and keep clippy clean under cargo clippy -- -D warnings.

Files:

  • src/util/mod.rs
  • src/util/test_paths.rs
tests/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

Use inline unit tests and integration tests under tests/; test platform-specific behavior with mock platforms and follow test-first development.

Files:

  • tests/doctor_test.rs
🔍 Remote MCP GitHub Copilot

Relevant review context

  • PR #69 introduces journal stages Prepared → Renamed → Active; reconciliation uses filesystem state and retains journals when cleanup fails.
  • The current code still performs legacy pre-journal cleanup with let _ = std::fs::remove_dir(...), so an unremovable populated directory can be ignored while migration continues.
  • install_from_archive still checks/installs against the managed destination while version discovery expects tools/nushell/<version>/nu; prior review context identifies this as a potential layout mismatch requiring verification.
  • write_active_version_with_binary rejects .. components but still accepts relative paths, making off-tree resolution dependent on the process working directory.
  • numan use updates the active marker but does not refresh cached NuPaths/activation identity state used by later lifecycle operations.
  • The latest reported CI state has several jobs still in progress; earlier PR-head checks reported compilation failures and unused imports, so final green CI should be confirmed before approval.
🔇 Additional comments (7)
src/util/mod.rs (1)

6-8: LGTM!

src/util/test_paths.rs (1)

1-16: LGTM!

Also applies to: 18-35, 37-49, 51-58, 60-64

tests/doctor_test.rs (5)

18-67: LGTM!


245-246: LGTM!


283-283: LGTM!


317-317: LGTM!


463-464: LGTM!

Also applies to: 511-512, 549-550, 728-729

@tonythethompson
tonythethompson merged commit 0a1c0a9 into master Aug 3, 2026
23 checks passed
@tonythethompson
tonythethompson deleted the pr-migrate-legacy-installs branch August 3, 2026 15:10
@linear-code

linear-code Bot commented Aug 3, 2026

Copy link
Copy Markdown

NUM-49

cursor Bot pushed a commit that referenced this pull request Aug 3, 2026
Resolve merge conflicts by taking master's landed product behavior from
#69/#71 while preserving PR 82 thiserror wrappers on migrate_legacy and
migration_journal APIs.

Co-authored-by: Anthony Thompson <github@trackdub.com>
cursor Bot pushed a commit that referenced this pull request Aug 3, 2026
Resolve conflicts between landed #69/#71 on master and PR83 review
hardening. Prefer PR83 safety gates (layout containment, exact latest
dest, off-tree marker preserve, list read-only) while adopting master's
DoctorArgs --scan API, confirm alias, and snapshot-before-setup wiring.

Co-authored-by: Anthony Thompson <github@trackdub.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.

3 participants