# Venv ↔ worktree ↔ repo layout — design note

**Status:** Open design question, drafted 2026-05-06
**Surfaced by:** axiom-memory MCP work (`feat/axiom-memory-mcp`); the editable-install path bound to one worktree-source means the venv "follows one branch" while interactive sessions span many.
**Related:**
- `feedback_axiom_aeos_worktree_venv.md` — established pattern: axiom-aeos-tests/ has its own .venv
- `feedback_axiom_memory_captures_all_claude_sessions.md` — the cross-tool MCP requires a stable python path baked into `~/.claude.json`; venv switching invalidates it
- `feedback_axiom_mcp_always_available.md` — auto-registration / self-heal requires the registration path stay valid across switches
- ADR-031 extension self-containment — relevant for "is this an axi-internal config or per-extension?"

---

## The four scenarios

```mermaid
flowchart TB
    subgraph S1[Scenario 1: branches in one worktree]
        S1a[axiom/ on feat/x]:::sit
        S1b[git switch feat/y]:::sit
        S1c[same .venv editable pth, but src files changed underneath]:::risk
    end

    subgraph S2[Scenario 2: multiple worktrees of the same repo]
        S2a[axiom/ on feat/x]:::sit
        S2b[axiom-classroom-prague/ on feat/classroom-x]:::sit
        S2c[axiom-memory-mcp/ on feat/memory]:::sit
        S2d[only ONE worktree's src/ can be the editable target]:::risk
    end

    subgraph S3[Scenario 3: sibling repos in workspace]
        S3a[axiom/]:::sit
        S3b[Neutron_OS/]:::sit
        S3c[dendra/]:::sit
        S3d[shared .venv, multiple editable installs ok]:::ok
    end

    subgraph S4[Scenario 4: cwd anywhere]
        S4a[cwd in subdir of axiom/]:::sit
        S4b[cwd in /tmp]:::sit
        S4c[cwd in supra-dir UT_Computational_NE/]:::sit
        S4d[axi shim resolves via PATH; venv is whatever is active]:::ok
    end

    S1c -.- A{Active editable install vs branch divergence}:::core
    S2d -.- A
    classDef sit fill:#1e3a5f,color:#e8f0fa,stroke:#4a7ab8
    classDef ok fill:#1e5a3a,color:#e8fae8,stroke:#4ab87a
    classDef risk fill:#5a1e3a,color:#fae8f0,stroke:#b04a7a
    classDef core fill:#5a3d1e,color:#fae8d8,stroke:#b07a4a
```

S3 and S4 mostly work today. The pain is S1 and S2: the editable install can only point at one source path, and `~/.claude.json` bakes that path in.

---

## The concrete pain we hit today

While shipping `feat/axiom-memory-mcp`:

1. The workspace `.venv`'s `_editable_impl_axiom_os_lm.pth` initially pointed at `/private/tmp/axiom-main/src` — a stale path from a previous install run that didn't survive a reboot. `axi memory` failed with `ModuleNotFoundError: No module named 'axiom'`. We repointed to `axiom/src` (the bare repo's main worktree).
2. To do TDD in the new `axiom-memory-mcp/` worktree, we had to `pip install -e ./axiom-memory-mcp` to repoint the same `.pth` at the new worktree. This breaks every other shell that was importing axi from `axiom/src`.
3. After `claude mcp add`, `~/.claude.json` baked in the venv's python path. If the user later switches the venv to another worktree, the registered MCP command still works (same python binary), but the `axiom` it imports is whichever worktree's source the `.pth` currently points at — a silent context shift.

---

## Why the obvious answers don't fit

| Answer | What it gets right | Where it breaks |
|---|---|---|
| **Per-worktree `.venv`** | Strong isolation; each worktree's editable install is independent. Already the pattern for `axiom-aeos-tests/`. | Heavy: each venv is 1–2 GB. Slow to set up. Duplicates shared deps. Increases the `axi` shim resolution problem (PATH ambiguity) unless direnv handles it. |
| **Single shared `.venv`, switch editable install on branch change** | Simple to reason about. | Repointing breaks every running session in another worktree. Background TIDY / daemons / `axi memory ingest --watch` would silently see a different code path. |
| **uv workspaces** | Native multi-package monorepo support; one venv, many editable members. | Doesn't help the *worktrees-of-same-package* case — uv still has one editable target per package. |
| **`axi` shim with cwd-aware `sys.path` shim-out** | Each `axi` invocation chooses the right source based on cwd. | Hacky, fragile, hard to debug, breaks when `axi` is invoked outside any repo. |

---

## Recommended path

**Keep the workspace `.venv` for sibling repos (S3) + cwd-anywhere (S4).** Layer **per-worktree `.venv`s with direnv activation** for active-development worktrees of the same repo (S1, S2).

```mermaid
flowchart TB
    subgraph WS[UT_Computational_NE/]
        SHARED[(.venv shared by sibling repos<br/>Neutron_OS, dendra, NeutronOS consumers<br/>pinned axi version, no editable axi)]:::shared
    end

    subgraph AX[axiom worktrees]
        WT_MAIN[axiom/ &middot; main worktree<br/>own .venv via direnv<br/>editable: axiom/src]:::wt
        WT_CR[axiom-classroom-prague/<br/>own .venv via direnv<br/>editable: axiom-classroom-prague/src]:::wt
        WT_MEM[axiom-memory-mcp/<br/>own .venv via direnv<br/>editable: axiom-memory-mcp/src]:::wt
    end

    SHARED -.outside any worktree → falls back here.-> WS
    WS -.- AX
    classDef shared fill:#1e3a5f,color:#e8f0fa,stroke:#4a7ab8
    classDef wt fill:#3d2e5a,color:#f0e8fa,stroke:#7a5fb0
    style WS fill:#0d1a2a,color:#e8f0fa,stroke:#4a7ab8
    style AX fill:#1a0d2a,color:#f0e8fa,stroke:#7a5fb0
```

Key choices:

- **direnv `.envrc` per worktree.** `.envrc` does `layout python` (or `source .venv/bin/activate`) so cd'ing into a worktree activates that worktree's venv automatically. Already in use for `Neutron_OS/`.
- **The bare repo / main worktree gets a venv too.** Today `axiom/` (a bare repo with a checkout at `feat/tidy-drift-dashboard`) shares the workspace `.venv` — that's S2's broken case. Each worktree, including the "main" one, gets its own.
- **Sibling consumer repos (Neutron_OS, dendra) keep using the workspace `.venv`.** They depend on a *pinned* axi version, not editable; the shared venv is correct for them.
- **`claude mcp add` registration uses the venv's python that was active when run.** No change to the registration logic; it's always anchored to one venv. Per-worktree venvs make this explicit instead of accidental.
- **`axi dr` self-heal becomes per-venv.** Each worktree's `.envrc` activates a venv, and `axi dr` checks (and fixes) the registration for *that* venv's python path. If the user has multiple venvs and registered from one, the others' `axi dr` flag the entry as stale (already implemented in `check_axiom_memory_mcp_registered` via `expected_command=sys.executable`).

---

## What this means for the auto-registration we just shipped

`axi memory register-mcp` writes the entry to `~/.claude.json` keyed on `axiom-memory`. Only one entry per server name is supported by Claude Code. So:

- If you have multiple worktree venvs and run `axi memory register-mcp` from each, the **last one wins**. That's the same MCP server name; only one python path can be registered at a time.
- This is *fine* in practice because every worktree's `axi memory` reads the same `~/.axi/memory/` ledger — the data is shared. The MCP server just needs to be importable from *some* live venv on the system, and any of them work.
- The `stale` warning in `axi dr` flags worktrees whose venv isn't the currently-registered one. Operator can either re-register (`axi memory register-mcp`) or accept that another worktree's venv is the canonical MCP source.

If we ever want **per-worktree-named MCP servers** (`axiom-memory-cr`, `axiom-memory-twin`), that's a separate decision. For now, one `axiom-memory` registered against whichever venv last ran `register-mcp` is the simplest correct shape.

---

## Implementation steps (when we commit to this)

1. **Per-worktree `.envrc`** — template `.envrc` that creates `<worktree>/.venv` if missing, activates it, and runs `pip install -e <worktree>` (and any sibling editable deps the worktree needs). One command for new-worktree onboarding.
2. **`axi tidy worktree-venv`** — TIDY-domain command that scans for active worktrees missing a `.venv` and offers to create one. Aligns with `feedback_tidy_owns_background_task_hygiene.md`.
3. **Documentation in CLAUDE.md** — replace the current "Venv: .venv at workspace root" line with the layered guidance: shared workspace venv for sibling repos + per-worktree venv for active axi-development worktrees.
4. **`axi dr` cross-venv awareness** — current `axiom-memory MCP registered` check warns when `sys.executable != registered command`; consider extending to "venv I'm running in vs venv that owns the registration" with an explicit hint about which worktree's venv is canonical right now.

None of this blocks the post-Prague queue we just shipped. The current workspace-shared `.venv` keeps working; the layered model improves it.

---

## Open questions

- **Should `axi install` set up a per-worktree venv automatically?** Today it's TOML-step-driven and venv-agnostic. Probably worth a `venv_setup` step type, opt-in.
- **What does `axi chat` do when called from outside any worktree?** Falls through to the workspace `.venv`'s axi, which uses whatever editable install pth points at. Acceptable; document it.
- **direnv-on-Windows?** Many Windows users won't have direnv. Need a fallback (PowerShell hook? `axi shell init`?). Out of scope for now; macOS/Linux first.
- **Federation impact?** A federation peer running multiple axi versions across worktrees could route differently than expected. Needs thought before federation directory MVP ships.

---

This note is design-only. No code changes here. Surface it for discussion before implementing the per-worktree-venv migration.
