# nh3-dev → Mac Pro Rackmount migration plan

**Status:** planning — not started. Picked up later.
**Date framed:** 2026-06-07
**Audit snapshot:** 2026-06-07 (the unsaved-work table goes stale fast — re-run before acting).

---

## Locked decisions (operator, 2026-06-07)

| Fork | Decision | Consequence |
|---|---|---|
| **Chipset** | Apple Silicon (2023 Mac Pro Rack, **M2 Ultra, ARM64**) | Docker images need arm64 variants; x86-only binaries need Rosetta 2. macOS, not Linux. |
| **Scope** | **Workstation only** | Move *Hat 1* (dev env). Leave *Hat 2* (fleet sidecars) on the existing Linux VM. |
| **Network** | **Racked in NH3, on the 10.100 subnet** | Direct fleet reach; no WireGuard needed. Gets a **new** IP — `10.100.10.50` stays with the sidecars. |

### The governing insight

`nh3-dev` currently wears **two hats on one box**, and only one follows you to the Mac:

- **Hat 1 — your dev environment** (moves to the Mac): ~40 repos under `~/development`, all of `~/.claude` (skills, agents, auto-memory, settings — **1.8 GB**), `~/.ssh` (incl. `infra-ops_ed25519`), `.zshrc → dotfiles/`, `.gitconfig`, `.cargo`, `.config`, toolchain.
- **Hat 2 — load-bearing fleet sidecars** (STAY on Linux, untouched): `althing-forseti` (file-based agent message-bus curator), the ttyd seats (`ttyd-ro`/`ttyd-rw`/`ttyd-caddy`), **mead-hall** `:5173`, **NH3 egress SOCKS5 proxy** `socks5h://10.100.10.50:1080`, NFS mounts to nh3-nas.
  - *(2026-06-08: `volva.service` + `heid.service` removed — vestigial daemons from before Heid/Volva became CC-session orchestrators; not part of Hat 2. One fewer service in the split.)*

Keeping Hat 2 on the existing VM (a) avoids porting Linux systemd/dante/Docker services to macOS launchd/Colima, and (b) un-tangles a latent bug: today a dev-box reboot kills fleet egress. After the split, the Mac reboots freely and nothing else notices. `10.100.10.50` stays bound to the sidecars.

---

## ⚠️ Phase 0 — Safety net (data-loss clock; do FIRST)

**Why this matters:** a "clean re-clone" on the Mac would silently drop unsaved work in ~15 repos. The correct cutover is **`rsync` the working trees wholesale**, not re-clone. Push everything pushable first as belt-and-suspenders.

### Unsaved-work audit (snapshot 2026-06-07 — RE-RUN before acting)

| Category | Repos | Risk on naive re-clone |
|---|---|---|
| **Local-only (no upstream branch)** | `bloom_music`, `comfy-dev`, `csm-expressiva`, `eitri-smithy`, `iris-friday`, `mead-hall` | May exist ONLY on this box → re-clone impossible. Must be copied. |
| **Unpushed commits** | `eshpfi-management` (**+14**), `heid` (+4), `brokkr-smithy`, `bifrost`, `chatterbox-fast`, `nevermore`, `ratatoskr`, `skaldsong`, `task-board`, `vastai-app` (each +1) | Lost on re-clone. |
| **Uncommitted working changes** | `vastai-app` (**21**), `voice-library` (16), `vor` (7), `heid`/`eitri-smithy` (6), `Worldtree`/`dvalin-smithy`/`task-board` (4)… | Never committed → lost on re-clone. |
| **Stashes** | `skaldsong` (1), `Worldtree` (1) | Stashes don't travel through remotes at all. |

Re-run the audit:
```bash
cd ~/development
for d in */; do d=${d%/}; [ -d "$d/.git" ] || continue; cd "$d"
  dirty=$(git status --porcelain 2>/dev/null | grep -c .)
  stash=$(git stash list 2>/dev/null | wc -l | tr -d ' ')
  if git rev-parse '@{u}' >/dev/null 2>&1; then
    n=$(git rev-list '@{u}..HEAD' --count 2>/dev/null); [ "$n" != "0" ] && state="ahead-$n" || state=""
  else state="NO-UPSTREAM"; fi
  { [ "$dirty" != "0" ] || [ "$stash" != "0" ] || [ -n "$state" ]; } && printf '%-34s dirty=%-3s stash=%-2s %s\n' "$d" "$dirty" "$stash" "$state"
  cd ..; done
```

### Phase 0 actions
- [ ] Push every repo with a remote + unpushed commits.
- [ ] For the 6 "no-upstream" repos: confirm whether a remote exists at all (`git remote -v`). Truly local-only → copy is the ONLY path; flag them.
- [ ] Do NOT auto-commit dirty trees — `rsync` carries them verbatim; pushing is just the redundant backup.
- [ ] Confirm `nh3-dev` will keep running headless after cutover (Hat 2 lives on).

---

## Phase 1 — Provision the Mac (Homebrew baseline)
- [ ] Xcode Command Line Tools + Homebrew.
- [ ] `brew install`: `uv`, `node`, `rustup`, `git`, `gh`, `tea`, `zellij`, `ripgrep`, `fd`, `jq`, `rsync`. Install Claude Code.
- [ ] zsh is already the macOS default — but prune Linux-isms from `.zshrc` (`apt`, `/etc/profile.d` sourcing, `PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright`).

## Phase 2 — Re-home data (rsync over the 10.100 link)
- [ ] `rsync` (trees + dirty state): `~/development`, `~/.claude` (1.8 GB), `~/.ssh`, `~/.config`, `~/.gitconfig`, `~/.cargo`, `dotfiles/`.
- [ ] `chmod 600 ~/.ssh/*` after copy (rsync can mangle perms; macOS ssh is strict).

## Phase 3 — Repath for macOS ⚠️ (the silent breaker)
- [ ] **macOS home is `/Users/lkraven`, not `/home/lkraven`.** Sweep all of `~/.claude` + dotfiles for `/home/lkraven` and rewrite → `$HOME` / `/Users/lkraven`.
  - Already-known hits in `~/.claude/settings.json`: the `statusline-command.sh` path and the `althing/plugin` path.
  - `grep -rn '/home/lkraven' ~/.claude ~/dotfiles` to find the rest.
- [ ] Reinstall MCP/plugins (`task-board` from gitea `vh/task-board`, `althing`) and re-point absolute paths.
- [ ] ARM64 reality:
  - Docker (if needed locally) → OrbStack/Colima + **arm64 images**.
  - Playwright → `npx playwright install chromium` (arm64 supported; no `/opt/ms-playwright` shared trick needed on a single-user box).
  - Python wheels are almost all arm64-mac now — only flag x86-only wheels (→ Rosetta 2).

## Phase 4 — Fleet citizenship for the new box
- [ ] Assign a **new** IP on 10.100 (NOT `10.100.10.50` — that's the sidecars'). Add to `~/.ssh/config`, fleet DNS, and `scripts/add-host.sh` if tracking here.
- [ ] WireGuard: **not needed** (no WG tunnel terminates on this box; Irvine reach was never a Hat-1 concern).
- [ ] Stand up a backup for the Mac (Time Machine, or a new restic client → `rest-server-nh3`). The risk table is the argument for not skipping this — real local-only state travels with you.

---

## Small decisions left (with recommendations)

- **NFS `/mnt/smithy` + `/mnt/backup` on the Mac?** → Recommend `/mnt/smithy` **yes** (R14/smithy workflows use it), `/mnt/backup` **no**. *(macOS NFS works, but the Synology export must list the Mac's new IP; macOS prefers autofs over fstab.)*
- **iPad/web seat into the Mac's dev session?** → Recommend **defer**. *(ttyd seats stay pointed at the Linux zellij session; a Mac seat is a fresh launchd port — only if you still drive from iPad.)*
- **Hostname** → Avoid `nh3-dev2` confusion; pick role-clear, e.g. `nh3-mac` / `nh3-workbench`.

---

## `/elitk` takeaway

Treat this as "set up a new workbench and copy my stuff over the network," **not** "move the server." Push everything first, then copy the folders wholesale (don't re-clone — you'd lose unsaved work in ~15 repos), fix the `/home/` → `/Users/` paths, and leave the little fleet-utility box humming untouched in the rack.

**Next action when resumed:** run Phase 0 (push pushable repos; confirm which of the 6 no-upstream repos are truly local-only). It's the only step with a data-loss clock; everything else waits on the hardware.
