Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ src/
registry.rs — Registry management subcommands
activate.rs — Plugin + module activation (Phase 3 & 4); public entry: execute_with_candidate_runner
init.rs — `numan init [--refresh]`: Nu probe, paths cache, auto-configures official registry
doctor.rs — `numan doctor [--scan]`: health checks + safe repairs by default (or report-only with `--scan`) (Phase 7.2; spec: docs/numan-doctor.md)
doctor.rs — `numan doctor [--scan] [--json]`: repairs by default; `--scan` report-only (Phase 7.2; spec: docs/numan-doctor.md)
snapshot.rs — `numan snapshot list|inspect|delete|rollback` (Phase 5.3)
deactivate.rs — Plugin + module deactivation: journaled plugin unregister (`execute_with_unregistrar`); module full/partial (Phase 4 / Issue #22 PR2)
plugin_lifecycle.rs — Activate/deactivate-owned lifecycle boundary exposed to opt-in update orchestration (Issue #22 PR3)
Expand Down Expand Up @@ -109,7 +109,7 @@ tests/
fixtures/nupm/ — supported/rejected fixture corpus for parser/classifier tests
init_test.rs — `numan init` / `init --refresh` (vendor drift, managed-file revalidation)
completions_test.rs — shell completion script generation (Phase 7.3)
doctor_test.rs — `numan doctor` report-only, --fix auto tier, journal checks (Phase 7.2)
doctor_test.rs — `numan doctor` default repairs, `--scan` report-only, journal checks (Phase 7.2)
nupm_compat_test.rs — Phase 6 integration tests (T13–T25, import/drift/manifest/activation/platform)
nupm_real_nu_test.rs — Phase 6.4 real-Nu #[ignore] acceptance tests (run with `cargo test -- --ignored`)
plugin_lifecycle_real_nu.rs — Issue #22 smoke marker (points at Stage 1 + active-update suite)
Expand Down
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,4 @@ libc = "0.2"
[dev-dependencies]
tempfile = "3"
rand_core = { version = "0.6", features = ["getrandom"] }
libc = "0.2"
wait-timeout = "0.2"
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Numan is **early-stage**. Core install, activate, update, remove, gc, registry,
- **Module autoloads** — managed vendor autoload files with ownership markers and candidate validation
- **Lifecycle management** — `update`, `remove`, and `gc` with pending-lifecycle journal recovery
- **nupm interoperability** — `numan nupm status|inspect|import|diff` for migration from [nupm](https://github.com/nushell/nupm)
- **Health checks** — `numan doctor [--fix]` diagnoses root state and applies safe repairs
- **Health checks** — `numan doctor` diagnoses root health and applies safe repairs by default; `--scan` reports only
- **Shell completions** — bash, fish, zsh, PowerShell, and Nushell via `numan completions`

---
Expand Down Expand Up @@ -187,7 +187,7 @@ Or pick a package yourself (`numan search` hides incompatible hits by default; u
numan search nutest
numan info vyadh/nutest
numan install vyadh/nutest
numan activate vyadh/nutest --yes
numan activate vyadh/nutest
```

Install is **inert** — nothing is registered with Nu until you run `numan activate` (or `numan try`, which activates after install). If a package needs a different Nu minor, Numan explains the mismatch and can offer `numan setup nu <x.y.z>` (activations are per-Nu; re-activate after switching). When no compatible starter exists, `numan try` suggests installing a matching managed Nu version or searching for another package with `numan search`.
Expand Down Expand Up @@ -352,7 +352,7 @@ Global flag: `--root <path>` — override the Numan root directory (all commands
| `nupm status` | `--nupm-home <path>` |
| `nupm inspect` | `--all` scan home; `--nupm-home <path>`; `--exit-on-ineligible` fail on ineligible |
| `nupm import` | `--as owner/name` (single import); `--manifest <file>` (batch); `--nupm-home <path>`; `--yes` skip consent |
| `doctor` | `--scan` report-only; `--json` machine output; `--nupm-home <path>` |
| `doctor` | `--scan` report only; `--json` machine output; `--nupm-home <path>` (repairs by default) |
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
| `setup nu` | `--force` re-download; `--skip-path` don't update PATH; `--yes` skip prompt |

Run `numan <command> --help` for full flag documentation.
Expand Down Expand Up @@ -432,7 +432,7 @@ For the cross-repository plan snapshot (as of 2026-07-29) across `numan`,
| **5** | `update` / `remove` / `gc`, lockfile v2, [snapshots + rollback](docs/snapshots-and-rollback.md) | ✅ (source builds deferred) |
| **6** | [nupm](docs/nupm-compatibility.md) status, inspect, import, drift | ✅ |
| **7** | Doctor, completions, onboarding, CI hardening, [winget packaging](docs/PACKAGING.md) | ✅ — [plan](docs/plans/Phase7Plan.md) |
| **Post-7.6** | Production [official registry](https://tonythethompson.github.io/numan-registry/) cutover; `numan init` and `numan doctor --fix` auto-configure `official` | ✅ (v0.1.4) |
| **Post-7.6** | Production [official registry](https://tonythethompson.github.io/numan-registry/) cutover; `numan init` and `numan doctor` auto-configure `official` | ✅ (v0.1.4) |

### Next (toward 1.0)

Expand Down
78 changes: 49 additions & 29 deletions docs/numan-doctor.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

`numan doctor` diagnoses the health of a Numan root and applies **safe automated repairs** by default — the same pattern as `brew doctor`, `npm doctor`, and similar tooling.

Use `--scan` for **report-only** mode (safe for CI and scripting). Repair mode delegates to existing commands (`init`, `activate`, `registry sync`) rather than inventing new mutation paths.
Default mode repairs. Use `--scan` for report-only output (safe for CI and scripting). Repairs delegate to existing commands (`init`, `activate`, `registry sync`) rather than inventing new mutation paths.

It answers: *“Is this Numan root consistent, safe to mutate, and aligned with the current Nu environment?”* and optionally *“Fix what you can.”*
It answers: *“Is this Numan root consistent, safe to mutate, and aligned with the current Nu environment?”* and *“Fix what you can.”*

## Non-goals

Expand Down Expand Up @@ -52,7 +52,7 @@ Each finding has:
- `severity` — `ok` \| `info` \| `warn` \| `error`
- `message` — human-readable summary
- `fix` — optional suggested command for manual issues (e.g. `numan init` or `numan registry add …`)
- `repair` — `none` \| `auto` \| `confirm` \| `manual` (whether `--fix` can act; see below)
- `repair` — `none` \| `auto` \| `confirm` \| `manual` (whether default repair mode can act; see below)

**Rules:**

Expand All @@ -63,25 +63,34 @@ Each finding has:

## Repair policy

By default, doctor acquires `acquire_mutation_lock(root)` for its own
In default (repair) mode, doctor acquires `acquire_mutation_lock(root)` for its own
filesystem repairs (layout dirs, `registry.none` config writes), then **releases**
that guard before nested `init` / `setup` / `registry sync` / `activate` /
`deactivate` commands. Those nested commands acquire the mutation lock themselves.
Doctor does **not** hold one lock for the entire repair pass end-to-end.
A PreMutation snapshot (`SnapshotTrigger::Doctor`) is taken after the lock is
acquired and before layout/config writes. If snapshot creation fails (for
example a malformed lockfile or missing payload revision), doctor records
`snapshot.pre_mutation` as failed, continues with independent `layout.*`,
`nu.active_version.malformed` cleanup, and `registry.none` repairs, and skips
nested mutations that need a PreMutation baseline (`nu_paths.missing` → `init`,
plus `setup` / `registry sync` / `activate` / `deactivate`) with reason
`snapshot_unavailable`.

Repair steps run in this **order** (each step re-validates only what it changed):

| Tier | Prompt? | Finding IDs | Action |
|------|---------|-------------|--------|
| **auto** | Never | `layout.*` (missing dirs), `nu_paths.missing` | `create_dir_all` for layout; `numan init` |
| **auto** | Never | `layout.*` (missing dirs), `nu.active_version.malformed` | Independent of PreMutation success: `create_dir_all` for layout; clear invalid `nu_state/active-version.json` via `clear_active_version` |
| **auto** | Never | `nu_paths.missing` | `numan init` (skipped with `snapshot_unavailable` when PreMutation fails) |
| **auto** | Never | `registry.index_missing` | `numan registry sync` |
| **auto** | Never | `registry.none` (production trust root only) | Add official registry via same path as `numan init` |
| **confirm** | Always (non-TTY skips) | `nu.binary.missing_on_path` | `numan setup nu` (downloads managed Nushell) |
| **confirm** | Always (non-TTY skips) | `nu.binary.found_off_path` | `numan setup nu use <path>` (adds existing install to PATH) |
| **confirm** | Always (non-TTY skips) | `nu_paths.drift`, `nu_paths.vendor_drift` | `numan init --refresh` |
| **confirm** | Always (non-TTY skips) | `journal.plugin_pending`, `journal.autoload_pending`, `journal.plugin_stale`, `journal.autoload_stale`, `activation.plugin_stale`, `activation.module_stale`, `autoload.projection`, `autoload.managed_missing` | `numan activate` (empty package list — reconciles journals and re-activates stale entries; same entry point as normal activate recovery) |
| **confirm** | Always (non-TTY skips) | `journal.plugin_deactivate_pending` | `numan deactivate <journal package ids>` (reconciles pending-plugin-deactivate journal only; not a full-root deactivate) |
| **confirm** | Always (non-TTY skips) | `journal.plugin_deactivate_stale` | `numan init --refresh` then `numan deactivate` |
| **auto** | Never | `registry.none` (production trust root only) | Add official registry via same path as `numan init` (continues even when PreMutation fails) |
| **manual** | Never auto | `nu.binary.missing_on_path` | Print fix hint (`numan setup nu`); doctor never downloads managed Nu without explicit user opt-in |
| **confirm** | Explicit consent when managed Nu exists | `nu.binary.found_off_path` | `numan setup nu use <path>` (adds existing install to PATH; doctor never passes `--yes`, so a managed wipe stays fail-closed / interactive) |
| **confirm** | Never (applied in default mode) | `nu_paths.drift`, `nu_paths.vendor_drift` | `numan init --refresh` |
| **confirm** | Never (applied in default mode) | `journal.plugin_pending`, `journal.autoload_pending`, `journal.plugin_stale`, `journal.autoload_stale`, `activation.plugin_stale`, `activation.module_stale`, `autoload.projection`, `autoload.managed_missing` | `numan activate` (empty package list — reconciles journals and re-activates stale entries; same entry point as normal activate recovery) |
| **confirm** | Never (applied in default mode) | `journal.plugin_deactivate_pending` | `numan deactivate <journal package ids>` (reconciles pending-plugin-deactivate journal only; not a full-root deactivate) |
| **confirm** | Never (applied in default mode) | `journal.plugin_deactivate_stale` | `numan init --refresh` then `numan deactivate` |
| **manual** | Never auto | `autoload.managed_foreign`, `payload.missing`, `journal.lifecycle_pending`, `journal.lifecycle_stale`, `registry.none` (placeholder trust root), `nu_paths.vendor_missing`, `nupm.*` | Print fix hint only |
| **none** | Never | `activation.plugin_mutation_gated` (`info`) | Informational only; see [docs/active-plugin-gate.md](active-plugin-gate.md) |

Expand All @@ -95,7 +104,7 @@ Repair steps run in this **order** (each step re-validates only what it changed)
6. Mutation lock ownership is **staged**: doctor's lock covers only its direct edits;
nested mutators reacquire after doctor drops the guard (see above).

**Journal note:** Repair mode reconciles plugin/autoload journals via `activate` recovery, and plugin-deactivate journals via `deactivate` recovery scoped to journal package IDs — not by editing journal files directly.
**Journal note:** `--scan` only *reports* journals without acting. Default repair mode may reconcile plugin/autoload journals via `activate` recovery, and plugin-deactivate journals via `deactivate` recovery scoped to journal package IDs — not by editing journal files directly.

## Check catalog

Expand All @@ -115,8 +124,9 @@ Checks run in order below. Implementation should call existing validators (`NuPa
|----|----------|-----------|
| `nu.binary.missing_on_path` | `error` | Nu not on PATH and not under `$NUMAN_ROOT/tools/nushell/` → fix: `numan setup nu` |
| `nu.binary.found_off_path` | `warn` | Nu exists in a known install root (e.g. `~/.cargo/bin`, `%LOCALAPPDATA%\Programs\nushell`) but not on PATH → fix: `numan setup nu use <path>` |
| `nu.path.version` | `info` | PATH-only Nu version (`PATH Nu: 0.114.1`), `PATH Nu: not found`, or `PATH Nu: found at '<path>' but version probe failed (<error>)` when the binary exists but `--version` fails. Does not treat managed Nu as PATH. Report-only (no `--fix`). |
| `nu.managed.version` | `info` | Managed binary under `$NUMAN_ROOT/tools/nushell/` with version, `Managed Nu: not installed`, or `Managed Nu: present at '<path>' but version probe failed (<error>)` when the binary exists but `--version` fails. Report-only (no `--fix`). |
| `nu.path.version` | `info` | PATH-only Nu version (`PATH Nu: 0.114.1`), `PATH Nu: not found`, or `PATH Nu: found at '<path>' but version probe failed (<error>)` when the binary exists but `--version` fails. Does not treat managed Nu as PATH. Report-only (no automatic repair). |
| `nu.managed.version` | `info` | Managed binary under `$NUMAN_ROOT/tools/nushell/` with version, `Managed Nu: not installed`, or `Managed Nu: present at '<path>' but version probe failed (<error>)` when the binary exists but `--version` fails. Report-only (no automatic repair). |
| `nu.active_version.malformed` | `error` | `nu_state/active-version.json` is present but unreadable/invalid JSON. Lookup would otherwise soft-miss the marker and fall back to PATH. **auto:** clear the marker via `clear_active_version` so resolution recovers cleanly. |
| `nu_paths.missing` | `error` | `paths.json` absent → fix: `numan init` |
| `nu_paths.drift` | `error` | `NuPaths::validate_drift()` fails → fix: `numan init --refresh` |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
| `nu_paths.vendor_drift` | `error` | `validate_vendor_drift()` fails when `data_dir` cached → fix: `numan init --refresh` |
Expand Down Expand Up @@ -162,9 +172,9 @@ No re-hash or revision recompute in v1 (too expensive for doctor).

| ID | Severity | Condition |
|----|----------|-----------|
| `registry.none` | `warn` | `config.toml` has no registries → fix: `numan init` before first init; `numan doctor --fix` after init (production trust root); `numan registry add …` for custom/placeholder builds |
| `registry.index_missing` | `info` | Enabled registry has no cached index under `registries/` → fix: `numan registry sync` |
| `registry.trust_root` | `info` | Enabled `official` registry: reports built-in key id (e.g. `official-2026-07-01`). Placeholder builds note that the key is not production. Report-only (no `--fix`). |
| `registry.none` | `warn` | `config.toml` has no registries → fix: `numan init` before first init; `numan doctor` after init (production trust root); `numan registry add …` for custom/placeholder builds |
| `registry.index_missing` | `info` | Enabled registry has no cached index under `registry/` → fix: `numan registry sync` |
| `registry.trust_root` | `info` | Enabled `official` registry: reports built-in key id (e.g. `official-2026-07-01`). Placeholder builds note that the key is not production. Report-only (no automatic repair). |

### 7. nupm coexistence (optional section)

Expand Down Expand Up @@ -205,7 +215,13 @@ Summary: 1 error, 1 warning
Repairs:
✓ Created missing state/ directory
✓ Ran registry sync
→ numan init --refresh required (skipped; user confirmation needed)
✓ Refreshed Nu paths
```

With repairs applied:

```text
Repairs: 3 applied, 0 skipped
```

Use `console` styling consistent with `activate --check`.
Expand All @@ -228,9 +244,13 @@ Use `console` styling consistent with `activate --check`.
],
"repairs": [
{ "id": "registry.index_missing", "status": "applied" },
{ "id": "nu_paths.drift", "status": "skipped", "reason": "not_confirmed" }
{ "id": "nu_paths.drift", "status": "applied" },
{ "id": "nu.binary.missing_on_path", "status": "skipped", "reason": "requires_explicit_setup_nu" }
]
}
```

`repairs` is present in default repair mode and omitted when `--scan` is set.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

## Architecture

Expand All @@ -251,11 +271,11 @@ pub fn execute_with_options(args: &DoctorArgs, root: &Path, options: DoctorOptio

| Command | Role |
|---------|------|
| `numan init` / `init --refresh` | **Repair** Nu path drift (`--fix` delegates here) |
| `numan setup nu` | **Repair** missing Nushell (`nu.binary.missing_on_path`; `--fix` downloads managed binary) |
| `numan setup nu use <path>` | **Repair** off-PATH Nushell (`nu.binary.found_off_path`; `--fix` adds parent dir to user PATH) |
| `numan activate` | **Repair** activation + journal reconciliation (`--fix` delegates here) |
| `numan registry sync` | **Repair** missing index cache (`--fix` auto tier) |
| `numan init` / `init --refresh` | **Repair** Nu path drift (default doctor delegates here) |
| `numan setup nu` | **Manual fix** for missing Nushell (`nu.binary.missing_on_path`; doctor prints the hint and does not download) |
| `numan setup nu use <path>` | **Repair** off-PATH Nushell (`nu.binary.found_off_path`; adds parent dir to user PATH; consented wipe of managed Nu requires `--yes` / TTY; doctor does not auto-approve) |
| `numan activate` | **Repair** activation + journal reconciliation |
| `numan registry sync` | **Repair** missing index cache (auto tier) |
| `numan activate --check` | Deep **module** check only; no repair |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
| `numan nupm status` | nupm-only summary; doctor embeds optional subset |
| `numan update` / `remove` / `gc` | Block on stale lifecycle journal; doctor reports, does not fix lifecycle |
Expand All @@ -265,14 +285,14 @@ pub fn execute_with_options(args: &DoctorArgs, root: &Path, options: DoctorOptio
- [x] `numan doctor`, `numan doctor --scan`, and `numan doctor --json` implemented per check catalog
- [x] `scan_on_doctor` respected
- [x] `--scan` mode: no state mutation (test: hashes unchanged)
- [x] Default mode: applies repair tiers per policy; uses mutation lock; delegates to init/activate/sync
- [x] Default repair mode: only repair tiers in policy; uses mutation lock; PreMutation Doctor snapshot; delegates to init/activate/sync
- [x] Documented in README command table and `AGENTS.md`
- [x] Integration tests: `--scan` report-only, default auto tier, default confirm tier with TTY, manual tier untouched
- [x] Integration tests: `--scan` report-only, default auto repairs, confirm-tier applied by default, manual tier untouched

## Changelog

| Date | Change |
|------|--------|
| 2026-06-30 | Initial spec (Phase 7.2) |
| 2026-06-30 | Add repair policy (auto / confirm / manual tiers) |
| 2026-08-02 | Update to reflect `--scan` flag (repairs by default; `--scan` for report-only) |
| 2026-06-30 | Add `--fix` / `--yes` repair policy (auto / confirm / manual tiers) |
| 2026-08-02 | Invert defaults: repair by default; `--scan` for report-only; remove `--fix` / `--yes` |
Loading
Loading