VibeVM
Contents
On this page
en
Publisher
org.vibevm.core
Version
1.0.0latest
Audiences
user
Reading time
26 min
Rendered
Read aloud
never

PROP-019 — VibeVM Version Manager (VVM)

01Status: IMPLEMENTED (specified 2026-06-17 in an owner-requested design session and verified against the tree 2026-07-25 by the spec-actualization campaign — the vvm ships across the whole verb set, with dense specmap implements / verifies coverage); revised to v2 the same day after the owner found two architectural flaws in v1 (see §9): (a) making $VIBEVM_HOME the single source of truth forced a console reload on every switch/reinstall, and (b) replacing the running distribution locks its files (the .exe, and any future DLLs). v2 keeps the v1 command surface but reworks the internals: a live current pointer file + current_exe() ground truth (env demoted to advisory), the whole distribution directory as the immutable unit of install/switch, content- cheap diff-copy between instances, and a new vibe vars reconciliation command. v3 landed 2026-09-11: source builds still hold developer/managed checkouts by reference, while a binary release owns the exact clean source snapshot that produced it; vibe + essential vibe-index ship and activate together; mutable remote version labels create immutable local #N generations. §9 records the resulting decisions.

02Related: PROP-018 (VVM is a second standalone-mode citizen after vibe skill — pure algorithm, no LLM; §2.1), PROP-016 (the source mirrors VVM clones from when run outside a source tree), PROP-000 §7 and PROP-000 §20 (the publish token VVM never touches), VIBEVM-SPEC.md (CLI-first posture), and the repo's rust-toolchain.toml (the pin VVM honours when building).

1. Motivation

1.1 The problem — vibevm cannot install itself

  • 03Every other capability of vibevm assumes a vibe binary already exists.
  • Getting it there is, today, an unspecified manual act: clone, cargo build, find the artifact, put it on PATH.
  • There is no story for which version you built, switching between versions, reclaiming the disk a Rust build tree eats, or doing any of it on a clean machine.

04Two further forces shaped v2 (§9):

  • 05the owner iterates fast and must not have to reload the console after each self install/use;
  • and a distribution is more than one file (vibe, vibe-index, exact source, manifest and future assets), any executable member of which may lock while running.

1.2 What VVM is — a self-distribution manager

  • 06VVM is a command group, vibe self, described as "VibeVM Version Manager / VVM".
  • It either builds a selected version from git or installs a verified native release, places the whole vibe + vibe-index distribution under a managed prefix, exposes both commands through stable shims, tracks the exact source tree, and lets the user update, switch, inspect, roll back, garbage-collect, and remove immutable local instances.
  • It runs on Windows, macOS, and Linux, across the shells those platforms use.

1.3 What this is NOT — not vibe install

07req r1

  • 08vibe install is the package manager (PROP-003 / PROP-017): it resolves packages a project depends on into that project.
  • VVM manages the vibevm tool itself on the machine — a user-global prefix, not a project's vibedeps/.
  • The two never share code paths; vibe self is its own command group so the package verbs stay uncontaminated.

2. Decisions

2.1 VVM is a standalone, algorithmic capability

09req r1

  • 10VVM needs no LLM and no host agent.
  • In PROP-018's terms it is a standalone-mode capability — the second after vibe skill — behaving identically with or without an agent.
  • It is fully scriptable: every interactive prompt has a non-interactive flag equivalent, so VVM works from a bare terminal, CI, or an agent transcript.

2.2 Command surface — vibe self (+ vibe vars)

11req r5

12vibe self — named after rustup's self (a tool that manages its own versions), and unambiguous where man collided with the Unix manual page:

  • 13self install <selector> — from a source execution, build the selected ref; from a managed binary execution, an explicit X.Y.Z installs that native release. Flags: --release / --profile <debug|release> (source lane); --mirror <gitverse|github> (force the source lane); --force (always allocate a fresh immutable #N).
  • self update follows the running origin, and moves versions FORWARD (owner ruling 2026-09-14, replacing the v1 rule that a binary execution refreshes its own number and never jumps). A source/worktree execution rebuilds its exact source root at latest. A binary execution reads the release channel's newest-release aggregate manifest and goes to the newest published release: newer than the running version installs and activates it by the same verified path an explicit X.Y.Z takes. The SAME version is not a no-op — a release may be rebuilt under its own number, so the manifest's bundle digest decides: changed bytes install a fresh immutable #N, unchanged bytes reuse the instance already held and cost no bundle download at all. A newest release OLDER than the running one (withdrawn on the far side) never walks the machine backwards. --force installs a fresh #N whatever the digest says. self install stable enters this same path, since stable IS the newest release (§2.3).
  • self reinstall refreshes the version that is RUNNING, without changing which version that is: a binary execution refetches its own release's manifest and bundle, a source/worktree execution rebuilds its exact source root — either way landing a fresh immutable #N. It takes no --force, because a fresh instance is the whole verb; --profile / --release steer the source lane exactly as on update. The pair is deliberate (owner ruling 2026-09-14): refetching the current version and moving to a new release are two acts, so they carry two names.
  • Every self verb honours the offline posture the process resolved — --offline, VIBE_OFFLINE or [net] offline, layered as PROP-010 ##OFFLINE-LAYERING says. Under it a release-lane verb is refused before its first request, naming the verb and the address it would have needed; a source-lane rebuild runs without fetching when the checkout and the crates are already local, and is refused the same way otherwise. The domain never reads the environment for this: the composition root hands the resolved posture in.
  • self import <PATH> --tag <X.Y.Z> — put a legacy READY-BUILT local vibe executable into the inventory as an immutable tag: instance, reusing the store/placer/lock/state machinery (built 2026-08-20 for the 1.0.0 distributive; the install.ps1 of the zip calls it instead of duplicating state.toml). Local by construction: no network, no signature machinery — the file is streamed through SHA-256 once, and that digest drives idempotence: same tag + same digest reuses the instance; same mutable tag + different digest creates a new local #N beside the preserved old one. --replace-candidate remains compatibility spelling. Optional --commit, --profile (default release); inactive by default — --use runs the same complete shim/PATH/pointer/advisory-HOME activation as every other lane.
  • self bootstrap --manifest DISTRIBUTIONS.json --version X.Y.Z --release-base URL is the narrow machine-facing cold-start verb. It verifies the temporary bootstrap against the aggregate, downloads one bounded native bundle anonymously, verifies/extracts it, and activates it; --force makes another generation.
  • self use <selector> — make a version active by repointing the live current file — no console reload (§2.5). --eval prints the shell line for an integrity-checked shell-local override instead of touching the durable environment.
  • self rollback atomically swaps to the immediate previous instance; repeating it toggles the two pointers without deleting either generation.
  • self ls (alias list) prints copyable <kind>:<id>#N selectors and provenance. * marks the active pointer; > independently marks the actually running executable, including a direct developer-worktree binary.
  • self current shows active and running identities separately. self which [vibe|vibe-index|source] prints that member's absolute path; self source prints exactly the running/active source root for shell composition.
  • self remove <selector> (aliases rm, del, uninstall) — safe by default (§2.9).
  • self gc — reclaim disk (§2.10).
  • self doctor (+ --fix) — verify the install and environment (§2.11).
  • self env — print safely quoted VIBEVM_SHELL_HOME + advisory VIBEVM_HOME activation for a shell.
  • self relocate <path> — repoint source provenance to a moved checkout and clear the instances built from the abandoned tree (§2.17). Flags: --from <old-path> (override the inferred old location); -y/--yes (non-interactive); --dry-run.

14Top-level vibe vars (§2.14) prints the runtime variable context — the values vibevm actually uses (derived from current_exe) versus what the environment says — so scripts never break on a stale $VIBEVM_HOME.

2.3 Version selectors and resolution

15req r2

16A selector names what to install or use; resolution is deterministic:

  • 17latest → tip of branch main.
  • stable → highest semantic-version git tag (the newest release).
  • X.Y.Z → a tag; tries X.Y.Z then vX.Y.Z, always peeling an annotated tag to its exact commit before recording/building.
  • a hex commit-ish → a commit.
  • the canonical <kind>:<id> form (as self ls prints) → that exact id.
  • <kind>:<id>#N → one exact immutable local generation; the terminal #N disambiguates repeated content under the same mutable remote label.
  • any other bare name → branch, then tag, then commit (hex commits and X.Y.Z tags are classified before this point).
  • 18--tag / --branch / --commit force interpretation, mapping to fully-qualified git refs so a name that is both never resolves by accident.
  • No selector: installlatest; remove opens an interactive picker (or refuses without a TTY); use requires an explicit selector.

2.4 On-disk layout — instances, the current pointer, manifests

19req r2

  • 20The unit of install and switch is a whole distribution directory (an instance: the binary plus future DLLs/assets), not a single file (§9.3).
  • A version has a canonical id <kind>:<id> (kind ∈ {tag, branch, commit}); each id may have several instances (one per install).
21$VIBEVM_INSTALL_ROOT/            install base — default: ~/.vibe
└─ opt/
   ├─ bin/                       ← on PATH; stable shims
   │   ├─ vibe, vibe-index       POSIX shims
   │   └─ vibe.cmd, vibe-index.cmd   Windows shims
   └─ vibevm/
       ├─ current                ← live pointer: the active instance dir
       ├─ previous               ← immediate rollback pointer
       ├─ state.toml             inventory: every instance + its metadata
       ├─ versions/<kind>/<id>/<instance>/   immutable distribution dirs
       │       bin/{vibe[.exe],vibe-index[.exe]}
       │       source/                 binary lane: extracted exact source tree
       │       vibevm-source.zip       binary lane: retained verification source
       │       DISTRIBUTION.json       binary lane: component/source identity
       │       .vvm-manifest.toml      source-build diff-copy inventory
       ├─ build/                 shared cargo --target-dir (gc-able)
       └─ src/.mirror/           shared managed source clone; external developer
                                 worktrees stay outside and are referenced
  • 22$VIBEVM_INSTALL_ROOT (the install BASE) defaults to ~/.vibe → root ~/.vibe/opt in normal use (owner ruling 2026-08-20, the release-1.0 install path; until then the default base was the bare home dir → ~/opt). An explicit override keeps its meaning — <override>/opt — and one env var still relocates everything; tests pin it to a temp dir. A managed binary keeps recognising its own root by shape (the trailing opt component), so both generations of store keep working.
  • <instance> is a monotonic counter (§9.4) — never a hash of the payload (§9.2).
  • The shim dir is stable; switching repoints current, never the shim.
  • Development/managed checkouts are held by reference and their untracked caches are never copied. A binary instance instead owns a clean tracked-file snapshot under its own source/, because shipped skills may require the matching vibevm source even on a machine without a checkout (§2.16).

2.5 Activation — live current file + current_exe truth

23req r2

24Switching must not reload the console and must not overwrite a running file. (v1's "env is truth" violated the first; see §9.1.) The model has four layers:

  1. 25current_exe() → the running process's truth. A modern managed vibe lives at …/opt/vibevm/versions/<kind>/<id>/<instance>/bin/vibe[.exe] (legacy root-level binaries remain readable), so it derives its own version id, VIBEVM_HOME (= its instance dir), and VIBEVM_INSTALL_ROOT (walk up to opt) from its own path — no env var needed. Outside a managed location (dev cargo run, a bare copy), it falls back to env, then defaults.
  2. VIBEVM_SHELL_HOME → explicit shell-local selection. self use --eval and self env emit it together with advisory HOME; stable shims prioritize this non-durable override so evaluation really switches both vibe and vibe-index even while durable current names another instance.
  3. current file → the live active instance. The shim reads $shimdir/../vibevm/current on every launch and execs that instance. self use rewrites current → the next vibe in the same shell uses it. No reload (the shim reads a file, not the shell's frozen env).
  4. $VIBEVM_HOME / $VIBEVM_INSTALL_ROOT (env) → advisory. Still set durably for external JAVA_HOME-style tools, but no longer the source of truth. They may lag (new shells only); vibe vars (§2.14) reconciles, and a managed vibe whose current_exe-derived home disagrees with the env prints a one-line stderr warning at startup (suppressed outside a managed run).
  • 26The four shims (bin/{vibe,vibe.cmd,vibe-index,vibe-index.cmd}) are minimal: use explicit shell override, else resolve current, then fall back to $VIBEVM_HOME; exec the corresponding member or print "no active vibevm — run vibe self use <selector>".
  • Both POSIX and .cmd forms exist for both commands (Git Bash won't resolve .cmd; cmd/PowerShell won't run an extensionless script).

2.6 PATH and durable environment management

27req r2

28VVM detects OS and shell and manages durable settings under strict rules:

  • 29The shim dir on PATH (stable; set once).
  • VIBEVM_HOME / VIBEVM_INSTALL_ROOT as advisory env (§2.5) — repointed on self use for external tools; truth lives in current + current_exe.

30Rules:

  • 31idempotent (a marker guards the edit; no duplicate lines/entries),
  • never clobber (only our entry is added; the rest of PATH is preserved),
  • OS/shell-aware (Windows: raw HKCU\Environment — read WITHOUT variable expansion, the value's registry kind preserved (REG_SZ stays REG_SZ, REG_EXPAND_SZ stays REG_EXPAND_SZ, so a PATH carrying %USERPROFILE%\… survives untouched), entries compared normalised (expand+full-path+case-insensitive) for dedup while unrelated raw entries are preserved byte-for-byte, and a WM_SETTINGCHANGE broadcast after the write; the earlier [Environment] API form silently expanded references and lost the kind. POSIX: a marked block in the detected shell's rc — bash/zsh/fish/.profile),
  • and consent + honesty (interactive maintenance uses confirm / -y / self doctor --fix; executing the native bootstrap one-liner is itself the explicit install consent). Output says whether PATH was already live or only durable for new shells. Activation writes stable PATH first, flips current second, and updates advisory VIBEVM_HOME last, so a failure cannot point the durable version variable ahead of the live selector.
  • The durable writer is an injectable seam so tests exercise the POSIX rc path in a temp file and never mutate the real machine.

2.7 Install pipelines — source builds and verified native bundles

32req r2

33Both lanes end identically: stage a complete vibe + vibe-index distribution, record a fresh immutable local instance, then flip current. The source lane builds from a referenced checkout; the binary lane verifies a bounded published bundle and installs its owned clean source snapshot.

  • 34Locate source (§2.16). Source builds have two referenced origins:
  • externalself install run inside a committer's own checkout (outside the install root): build it in place, never touch its git state, and record its canonical absolute path as provenance so a later self install <id> can rebuild from the remembered location (a linked source).
  • managed — a shared clone vibevm owns under src/.mirror: created once, then updated incrementally with git fetch/checkout (or pull; stash first if dirty), never re-cloned, so a full rebuild (hours, in a large future) is avoided.
  • Resolve. The selector (§2.3) → a concrete commit, recorded.
  • Build. cargo build [--release] -p vibe-cli -p vibe-index into the shared build/ target dir (§9.3 — never the source tree's target/; load- bearing on Windows and keeps the dev tree clean), honouring rust-toolchain.toml.
  • Build the two essential binaries only. vibe-index is part of VibeVM's own distribution because on-premises registry owners require it. The terminal products (vibeterm, vibeframe) and the GUI launchers (vibe-launcher) used to be packaged into the instance alongside vibe; they have moved to a separate products repo (vibevm-term) and now publish themselves to PATH through their own version-manager (<product> self install). The install pipeline here builds vibe + vibe-indexvibe term / vibe frame resolve the terminal apps through $VIBEVM_<APP> → the active instance's packaged <app>/ (back-compat) → PATH, with an in-place fallback for vibe tree when no terminal app is available (spec://term-common/PROP-vvm#root is the products-repo twin).
  • Place a source build by diff-copy (§2.15). Both binaries are placed under instance/bin/; unchanged files may be hardlinked from the prior source instance. The distribution is placed into a fresh instance dir, copying only files that changed versus the previous instance and hardlinking the rest — so a 2 GB distribution where only vibe.exe changed costs one file copy, never a full re-copy or a payload hash (§9.2). If nothing changed, no new instance is made.
  • Install a binary bundle. Select one of the four fixed targets from DISTRIBUTIONS.json; enforce independent size limits; verify outer bundle, embedded manifest, both binaries, and vibevm-source.zip by SHA-256; reject traversal, collisions, symlinks, special files, extras, and decompression overflow; cap source files/materialized nodes/depth/path at 200,000/400,000/256/4096; retain the source ZIP and extract it under source/. The inventory retains the authenticated manifest digest that anchors later doctor/reuse checks.
  • Remote labels are mutable; local instances are not. A republished v1.0.0 may have new bytes, but it becomes tag:1.0.0#N+1. A non-forced identical bundle reuses an instance only after re-verifying its manifest, binaries, retained source ZIP, and extracted source file set/content.
  • Record + activate. state.toml gets the instance (id, instance, commit, toolchain, profile, time, origin, source_path). Every activating lane then uses one failure-ordered transaction: publish the stable shims, persist their generic PATH, journal/flip current + previous, and update advisory VIBEVM_HOME last.

35Because every install writes a new instance dir and switching is a pointer flip, no in-use file is ever overwritten — the running process keeps its instance dir intact; no lock, no reload, for the .exe or any DLL, on any OS (§9.3).

2.8 Required toolchain — a single source of truth

36req r2

  • 37A from-source build needs git, a Rust toolchain (rustc + cargo, stable ≥ 1.93, edition 2024 — via rustup so the pin resolves), and a system linker / C toolchain (Windows: VS Build Tools; macOS: Xcode CLT; Linux: build-essential). A native binary install needs none of those; its bootstrap uses only the platform download/TLS facilities.
  • OpenSSL is deliberately not required (rustls).
  • The list lives once as a REQUIRED_TOOLS table — (name, min_version, check_command, help_url) — read by self doctor (§2.11) and asserted by a test; it is the runnable form of "how to update the stack" (§7).
  • The publish token is never in this set (§2.13).

2.9 Removal — safe by default

38req r2

39self remove never silently wipes everything:

  • 40self remove <selector> — remove that version or exact #N. For binary bundles, --bin removes only bin/, --src only the owned source/ + retained source ZIP, and the default removes the whole instance. A scoped removal reports the deliberately partial, non-activatable result; external worktrees are never deleted and shared managed-source effects are reported separately.
  • self remove with no selector — an interactive picker; a non- interactive context errors with a hint, never a wipe.
  • self remove --all — every version, behind the flag and a re-confirm.
  • The active version and the running instance are protected: removing an active but non-running instance needs --force; the actually running instance is never removable, even with that flag.

41External sources (committer trees) are never removed — VVM only forgets their provenance record; the managed src/.mirror clone is VVM's to drop.

2.10 Garbage collection — self gc

42req r2

43self gc reclaims disk:

  • 44--build — clean the shared Rust build cache (build/); forces a rebuild next install but touches no installed instance.
  • --prune-others — remove every instance except the active and its immediate rollback target (plus referenced source), behind a re-confirm.
  • 45Instances are pruned best-effort: a dir still locked by a running process is skipped and collected on a later run (on POSIX the unlink succeeds and the inode lives until the process exits).
  • Hardlinked files are refcount-safe — removing one instance never corrupts another that shares inodes (§2.15).
  • No install silently erases rollback history. Binary and source generations remain until explicit remove or gc --prune-others; the immediate previous target survives pruning.
  • self gc operates only inside the install root and never touches the shared ~/.cargo caches.

2.11 Introspection — doctor, ls, current, which, env

46req r2

  • 47self doctor verifies end to end: both generated stable shims have their expected content/mode and their directory is on PATH; every source-built active/running instance still matches its placement manifest; source origins have their required build tools; and a modern binary origin does not require Rust/git but must pass the authenticated manifest-rooted full bundle/source integrity check. Source paths must still resolve to an actual vibevm root. A legacy single-binary import is labeled partial with an update recipe but its payload is still verified. It exits non-zero while problems remain; --fix repairs shims/PATH with consent and recomputes the result.
  • self ls / current / which read the current file for the active selection and current_exe() for the independently running selection. A direct worktree executable wins over cwd and over an unrelated active binary.
  • self env prints shell-specific activation lines.

2.12 Cold-start (bootstrap)

48req r2

  • 49Stable endpoints provide one-line native cold start: Bash for Linux/macOS/WSL and PowerShell for Windows. Each bounded script downloads DISTRIBUTIONS.json, verifies a small raw vibe bootstrap by size/SHA-256, then that Rust binary verifies and atomically installs the full bundle.
  • The scripts are shipped release assets install.sh and install.ps1 and are exposed by stable https://vibevm.org/install.* redirects. Homebrew, WinGet, and CMD installers remain outside this slice.

2.13 Security and trust

50req r2

51Building an arbitrary ref is arbitrary code execution — inherent to a build tool the user invokes deliberately, and accepted.

52Constraints:

  • 53host-key (SSH) / TLS verification never disabled on clone;
  • binary install/update reads public releases anonymously; the publish token is never read by VVM nor shown by vibe vars;
  • VVM operates only inside the install root and the declared, consented environment edits;
  • every store mutation is lexically contained and rejects symlink/reparse or special-file ancestors; current, previous, and recovery-journal paths must name exact inventoried instances before they can be applied;
  • the committer's own source tree is never mutated (§2.7, §2.16).

2.14 vibe vars — reconciling actual vs environment

54req r1

  • 55Scripts must know the real runtime context even when $VIBEVM_HOME is stale (§9.1).
  • vibe vars prints the project's env-configurable variables — VIBEVM_INSTALL_ROOT, VIBEVM_HOME (whose actual values are derived from current_exe, §2.5), plus VIBE_INVOKED_BY, VIBE_UNATTENDED, VIBE_LOG — in NAME=VALUE form. The publish token is deliberately excluded.
  • 56vibe varsactual values, one NAME=VALUE per line.
  • vibe vars diffNAME=VALUE [ENV_VALUE]; the bracket appears only when the environment differs from the actual.
  • vibe vars full — two tables, # ACTUAL then # ENVIRONMENT.
  • vibe vars full diff — both tables, differing names marked NAME=VALUE [*].

57"actual" for the VVM vars is the current_exe-derived value (falling back to env/default outside a managed run); "environment" is the raw env. A script reads vibe vars and knows exactly the context it runs in.

2.15 Distribution instances and diff-copy

58req r2

59Source-built instances use the existing diff-copy optimization. Binary bundles take the verification-first lane: their publisher digests are checked on download and again before an identical local instance is reused.

  • 60Each instance carries .vvm-manifest.toml: per dist file (rel, size, mtime, hash?). The build dir is persistent (shared --target-dir), so cargo preserves the mtime of unchanged outputs across builds.
  • On install, for each dist file: compare to the previous instance's manifest entry — by cheap content hash for small files (≤ a threshold) and by (size, mtime) for large files (stat only, never read). Unchanged → hardlink the previous instance's file into the new one (zero copy). Changed/new → copy from the build output. Hardlink failure (cross-volume / unsupported) → copy.
  • If every file is unchanged, no new instance is made — current stays ("already up to date"). --force always makes a fresh instance.
  • The complete new instance is staged then atomically renamed; stable PATH is ensured, current flips, and only then advisory VIBEVM_HOME updates.
  • gc is refcount-safe (§2.10); instances are immutable after publish.
  • 61This scales to a multi-GB distribution: an 80 GB asset that did not change is shared by hardlink; only the changed vibe.exe is copied (§9.2, §9.6).
  • The vibeterm/ subtree (~220 MB, ~3-4 k files) participates in the same diff-copy: small files hashed by the ≤16 MiB rule, the Electron binary by (size, mtime); an unchanged vibeterm is hardlinked file-by-file free, and a rebuild that changed nothing dedup-skips the whole instance.

2.16 Source provenance and linked sources

62req r2

63A developer checkout's caches are never copied. Source builds reference a checkout; binary bundles carry a clean tracked snapshot. Each instance records its origin, source path, commit, and payload identity:

  • 64managed — the VVM-owned clone at src/.mirror (VVM updates it via git and may drop it on remove/gc).
  • external — a committer's own checkout, identified by its canonical absolute path (source_path); VVM never modifies or removes it, only remembers where it is.
  • binary — a prebuilt artifact. Two roads enter this origin: the legacy LOCAL import (self import, shipped 2026-08-20) hashes the supplied file itself — SHA-256 computed on this machine, stored as payload_sha256, driving reuse/refusal. The fetched native release path is keyed by the publisher's bundle/component/source digests and owns its extracted clean source under the immutable instance. The two must not be conflated: local import verifies nothing about authorship, only integrity-identity of the bytes it was handed.
  • 65The remembered source_path makes an external source a linked source: self install <id> can rebuild from the recorded location from anywhere, without being in the checkout and without copying it.
  • Every installed binary bundle is self-contained with its matching source because packaged skills may need it. self source returns instance/source; for source/worktree executions it returns the canonical external/managed git root. A missing owned source is a diagnosed broken bundle, not a cwd fallback.

2.17 Relocate — repointing provenance after a checkout move

66req r2

  • 67A committer's checkout is not pinned in place: it is cloned, moved, renamed, re-organised on disk. When it moves, every external instance's remembered source_path (§2.16) goes stale — a later linked-source rebuild would miss — and the pile of instances built from the abandoned tree clutters self ls.
  • self relocate <new-path> is the maintenance verb for that move.
  • 68Validate the new location. <new-path> must resolve to a real vibevm source tree (the find_source_root shape — workspace Cargo.toml + crates/vibe-cli); a path that is not a checkout is refused before anything mutates. The new path is canonicalised and \\?\-stripped exactly as install records it (§2.16), so the rewritten source_path matches the form every other record carries.
  • Infer the old location. With no --from, the old path is the source provenance already recorded on the installed external instances (the common value when one checkout moved). --from <old-path> states it explicitly for an ambiguous inventory. There is nothing to relocate when no external instance records a source tree — the command says so and exits, never invents a move.
  • Repoint, then prune. Two effects, in one atomic state.toml rewrite: (a) every external instance whose source_path is the old location is repointed to the new one — so linked-source rebuilds (§2.16) resolve to the live tree; (b) the built instance directories sourced from the old tree are removed — they are provenance-stale artifacts of the abandoned checkout, and their records are forgotten.
  • Active, actually running, and immediate rollback instances are never deleted. Their directories remain, their matching provenance is repointed, and current/previous are repaired as one journaled transaction before any stale instance removal. Removing a protected version is self remove's job (§2.9).
  • Consent and scriptability. Removing instances is irreversible, so the default is an interactive warning that lists what is repointed and what is removed, behind a confirm. -y/--yes (or --unattended) skips it for scripts and CI; a non-TTY run without --yes errors rather than silently applying (the same contract as self remove/gc, §2.9, §2.10). --dry-run prints the plan and changes nothing. --json emits the plan and the applied result. A no-op (old already equals new) is reported, not an error.

69Relocate touches only state.toml and the install root's own versions/ instance dirs. It never touches a committer's source tree (external sources are held by reference, §2.16), never the shared build/ cache (that is self gc, §2.10), and never ~/.cargo.

2.18 Native release production — four hosts, one verified release

70req r1

  • 71The closed native matrix is x86_64-pc-windows-msvc, x86_64-unknown-linux-musl, x86_64-apple-darwin, and aarch64-apple-darwin. Each host builds only its own native target; no successful cross-compile is passed off as host proof.
  • Four dependency-light wrappers under distribution/release/ drive cargo xtask dist build. Construction runs without tests by default; --checks, --tests, and --self-check are explicit opt-ins, and --upload remains the manual build-plus-upload convenience. The Linux wrapper installs the Rust musl target through rustup and fails early with an actionable message when the required musl-gcc is absent; it never invokes a package manager or sudo.
  • The producer resolves one immutable commit OID, reads every identity/file from that OID, and materializes it offline from the local Git object store. Raw git archive supplies committed bytes only; a deterministic ZIP encoder normalizes ordering, timestamp and modes. The nested source ZIP uses STORED entries so its bytes have no architecture-dependent compressor input; the outer platform ZIP still applies DEFLATE for transport. A fail-closed portability gate rejects tracked paths that cannot have one identity on all four targets (including non-ASCII/case/reserved-name collisions), and the same 200,000-file / 400,000-node / 256-component / 4096-byte path limits are enforced before emission and extraction. Optional gates run in a disposable source tree and separate Cargo target cache; release compilation starts from a fresh expansion. The user's checkout, untracked files, and caches are never cleaned, reset, or copied.
  • Every platform ZIP embeds the same byte-identical vibevm-source.zip, its source tree OID and SHA-256, plus vibe, vibe-index, DISTRIBUTION.json, LICENSE.md, and README.md. The raw bootstrap asset is byte-identical to the bundle's vibe component.
  • Every version label is mutable by default. dist prepare --version V deletes any existing draft or published release for V, moves or creates vV at the exact source commit, and creates one fresh draft. Four hosts may independently replace their ZIP/bootstrap/fragment triplets. The exact direct refs/tags/vV → commit binding is verified after prepare and again immediately before publication. dist finalize requires and remotely verifies all twelve platform assets, publishes DISTRIBUTIONS.json + both installers, then publishes with GitHub make_latest=legacy so rebuilding an older SemVer cannot demote a newer one.
  • Compilation, build scripts, tests, self-check, Git, and built-binary probes receive no publish credential. Host wrappers disable command tracing before retaining an upload credential. CI builds xtask without credentials and runs that already-built trusted binary only in narrow prepare/upload/finalize steps carrying GITHUB_TOKEN.

3. Architecture — seams and cells

72req r2

73VVM is built from testable seams so the slow, machine-mutating parts are mockable and unit tests never clone, build, or edit the real environment:

  • 74VersionStore — the install-root layout (§2.4), instances, current, state.toml, manifests.
  • SourceProvider — git: resolve a selector to a commit; clone/update a managed source; record external provenance.
  • Builder — runs cargo for a profile/toolchain; mocked in tests.
  • Placer — the diff-copy of a distribution into a new instance (§2.15).
  • EnvPersister — the durable PATH/env edits (§2.6), injectable.
  • release_manifest + the VVM bundle reader — strict aggregate/fragment/bundle wire contracts, bounded anonymous download, safe ZIP extraction, and byte-level reusable-instance verification.
  • ToolDoctor — the REQUIRED_TOOLS table and checks (§2.8).
  • vars — the actual-vs-environment resolver (§2.14), current_exe-aware.
  • 75A managed vibe resolves its root/active from current_exe + the current file; env is the fallback.
  • The command lives as cli/vvm.rs + cli for vibe vars, with logic under commands/vvm/ (split across module-grain files to hold the file-length budget). conform and specmap stay green.

4. MVP scope

  • 76The full verb set on all three platforms: self install (external in-place + managed clone paths, debug + release, diff-copy into instances), self use (live current, no reload), self ls/current/which, self remove (safe + --all), self gc (build cache + prune), self doctor (+ --fix), self env, self relocate (§2.17), self import (legacy local ready-built payload), self bootstrap (native bundle), self rollback, self source, component-aware self which, and vibe vars.
  • Selector resolution per §2.3; durable PATH/advisory-env per §2.6 across Windows (cmd/PowerShell/Git Bash), macOS (zsh/bash), Linux (bash/zsh/fish).
  • diff-copy with hardlink sharing is in scope (§2.15).
  • Linked sources (§2.16) are in scope (the source_path record + rebuild-from-remembered).

5. Out of scope (now)

  • 77Retired from out-of-scope: fetched, publisher-digest-bound native bundles ship through self bootstrap, binary self update, and explicit SemVer self install (§2.7, §2.12).
  • offline / vendored builds;
  • cryptographic signature verification;
  • reflink/CoW placement (hardlink is the portable choice).
  • These are §6.

6. Far backlog

  • 78Shipped, no longer backlog: binary refresh/install is origin-directed rather than a separate --binary switch; remote labels stay mutable and local counter instances immutable.
  • Shipped, no longer backlog: bounded Bash and PowerShell one-line bootstrap assets (§2.12).
  • Offline builds via vendoring or a registry mirror.
  • Reflink/CoW placement where the filesystem supports it (§2.15).
  • Signature/provenance verification of the resolved ref.

7. Maintenance & evolution — updating the stack

79req r1

80Knowledge is runnable, so updates are mechanical:

  • 81the required tools are the REQUIRED_TOOLS table (§2.8, asserted by a test);
  • the default profile is one constant (§2.2);
  • the Rust pin is rust-toolchain.toml (read, not hard-coded);
  • the clone mirrors are PROP-016's mirrors.toml.

8. Acceptance

82req r2

  • 83From a fresh clone, the source lane builds both essential binaries. From a clean machine, the Bash/PowerShell bootstrap installs the verified native pair plus source and durably exposes stable shims; output states whether the current shell already sees PATH.
  • self use switches the active version and the next vibe in the same shell is the new one — no reload (current file).
  • Reinstalling the running version replaces no in-use file (new instance + pointer flip); the running process is unharmed.
  • A source-built distribution where only one essential binary changed copies that file; unchanged source-build members may be hardlinked. The binary lane instead verifies every bounded payload member and source before reuse.
  • vibe vars reports actual vs environment; vibe vars diff/full/full diff per §2.14; the publish token never appears.
  • self remove never wipes without --all + reconfirm; self gc never touches ~/.cargo; external sources are never modified or removed.
  • self relocate <new> repoints external source_path records and removes the stale instance dirs built from the old tree, keeping the active instance; the active's source is repointed, not deleted. --dry-run changes nothing; a non-TTY run without --yes errors.
  • self import is inactive by default (--use alone activates); importing the same tag with the same payload SHA-256 reuses the instance and allocates nothing; a different payload under the same mutable tag creates a new #N and preserves the previous instance; --replace-candidate remains compatibility spelling.
  • A native bootstrap installs both essential binaries and the matching exact source tree under one immutable instance; self source returns its absolute path, normal same-digest refresh is a verified no-op, --force allocates the next #N, and rollback restores the previous generation.
  • With two source worktrees and one binary install, self ls/current/which distinguish active * from running >; a directly executed worktree reports its own executable/root/commit and never borrows cwd or active binary provenance.
  • Each fixed host wrapper defaults to build-only/no tests; explicit gates are opt-in. All four fragments must bind the same commit/tree/source archive before mutable release finalization; build subprocesses receive no publish credentials.
  • Full self-check.sh green; conform 0/0/0; specmap clean.

9. Design rationale & questions explored

84The decisions above were reached by working through several sharp questions; recording them so a cold reader sees why, not just what.

9.1 Why current file + current_exe, not $VIBEVM_HOME (v1)

  • 85v1 made $VIBEVM_HOME the single source of truth for the active version. Environment variables are inherited at process start, so a shell's $VIBEVM_HOME is frozen until the shell is reloaded — every self use/ reinstall forced "open a new terminal".
  • The fix: the shim reads a live current file each launch (filesystem is live → instant switch in the same shell).
  • A running vibe derives its own identity from current_exe() (it is the binary, so it knows its path).
  • $VIBEVM_HOME stays only as an advisory/compat env for external tools, reconciled by vibe vars (§2.14) and a startup divergence warning.
  • This reverses v1's decision deliberately; env-as-truth was the cause of the reload friction.

9.2 Why not content-hash the distribution

  • 86A natural instance key is a content hash of the built distribution (dedup + self-describing).
  • It does not scale: a distribution may grow to gigabytes and ship as binaries (merged projects), and hashing 2 GB+ on every install would be prohibitive.
  • So the instance key never reads the payload (§9.4), and change detection for diff-copy hashes only small files, trusting (size, mtime) for large ones (§2.15).
  • Fetched binary artifacts are keyed by the publisher's digest (computed once at publish), verified during download/extraction, and their installed manifest, binaries, retained source ZIP and extracted source are rechecked before reuse. The local self import path is deliberately outside this rule: it has no publisher, so it hashes the handed file once on entry — that digest is its identity, not a verification of anyone's claim.

9.3 Why whole-directory instances, not in-place file replace

  • 87The first idea for "reinstall over the running binary" was the Windows rename-aside trick (rename the running .exe, write the new one; empirically verified to work).
  • It handles one file; a distribution is many (exe + DLLs + assets), all locked while running.
  • So the unit of install and switch became the whole immutable instance directory: each install writes a new dir and switching is a pointer flip, so nothing in use is ever overwritten — no lock for any file on any OS, and no reload.
  • rename-aside was dropped as unnecessary.

9.4 Why a monotonic counter for the instance key

  • 88With content-hash rejected (§9.2), the instance key is a monotonic counter: always unique, O(1), independent of distribution size.
  • "Did anything change" is answered cheaply by the diff-copy manifest (§2.15), which also yields the dedup-skip (no new instance when every file is unchanged) without hashing the payload. --force bypasses the skip.

9.5 Why explicit prune preserves rollback

  • 89Install never silently prunes either origin: a same-label previous payload is the rollback evidence for mutable releases. Disk reclamation is an explicit remove / gc --prune-others decision.
  • Pruning preserves the active and immediate previous instances. Source builds retain referenced checkout context; binary instances retain their exact source snapshot until explicitly pruned.

9.6 Why developer sources are referenced but release source is copied

  • 90Copying a checkout into the install root is untenable — a working tree's cargo target/ is already tens of GB.
  • So managed sources are git clones VVM updates incrementally, and external (committer) sources are referenced by their absolute path and built in place, never touched.
  • This also gives linked sources (§2.16): rebuild from a remembered location without being in it.
  • A developer working tree is never copied because its ignored build/cache surface may be enormous. A binary release deliberately embeds only the committed clean tree—normalized offline from one Git OID—because skills may need source and no external checkout exists. Its installed source/ is part of the immutable distribution, not a development clone.

9.7 Why the binary, not the shim, emits the divergence warning

  • 91When current and $VIBEVM_HOME disagree, the warning is emitted by the vibe binary at startup, not by the sh/cmd shim: the binary has current_exe ground truth and the vibe vars formatter, and keeps the shims trivial.
  • The warning is suppressed outside a managed run (a dev cargo run has no managed location and should not be nagged).

For an agent

This page has a machine mirror. The citation carries the version rather than latest, so what an agent quotes does not move under it.

spec://org.vibevm.core/vibevm@1.0.0/common/PROP-019-version-manager

.md.xmlllms.txt