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
vibebinary already exists. - Getting it there is, today, an unspecified manual act: clone,
cargo build, find the artifact, put it onPATH. - 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-indexdistribution 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
- 08
vibe installis 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 selfis 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:
- 13
self install <selector>— from a source execution, build the selected ref; from a managed binary execution, an explicitX.Y.Zinstalls 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 updatefollows 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 atlatest. 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 explicitX.Y.Ztakes. 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.--forceinstalls a fresh#Nwhatever the digest says.self install stableenters this same path, sincestableIS the newest release (§2.3).self reinstallrefreshes 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/--releasesteer the source lane exactly as onupdate. 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
selfverb honours the offline posture the process resolved —--offline,VIBE_OFFLINEor[net] offline, layered as PROP-010##OFFLINE-LAYERINGsays. 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 localvibeexecutable into the inventory as an immutabletag: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 duplicatingstate.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#Nbeside the preserved old one.--replace-candidateremains compatibility spelling. Optional--commit,--profile(defaultrelease); inactive by default —--useruns the same complete shim/PATH/pointer/advisory-HOME activation as every other lane.self bootstrap --manifest DISTRIBUTIONS.json --version X.Y.Z --release-base URLis 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;--forcemakes another generation.self use <selector>— make a version active by repointing the livecurrentfile — no console reload (§2.5).--evalprints the shell line for an integrity-checked shell-local override instead of touching the durable environment.self rollbackatomically swaps to the immediatepreviousinstance; repeating it toggles the two pointers without deleting either generation.self ls(aliaslist) prints copyable<kind>:<id>#Nselectors and provenance.*marks the active pointer;>independently marks the actually running executable, including a direct developer-worktree binary.self currentshows active and running identities separately.self which [vibe|vibe-index|source]prints that member's absolute path;self sourceprints exactly the running/active source root for shell composition.self remove <selector>(aliasesrm,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 quotedVIBEVM_SHELL_HOME+ advisoryVIBEVM_HOMEactivation 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:
- 17
latest→ tip of branchmain. stable→ highest semantic-version git tag (the newest release).X.Y.Z→ a tag; triesX.Y.ZthenvX.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 (asself lsprints) → that exact id. <kind>:<id>#N→ one exact immutable local generation; the terminal#Ndisambiguates repeated content under the same mutable remote label.- any other bare name → branch, then tag, then commit (hex commits and
X.Y.Ztags are classified before this point).
- 18
--tag/--branch/--commitforce interpretation, mapping to fully-qualified git refs so a name that is both never resolves by accident. - No selector:
install→latest;removeopens an interactive picker (or refuses without a TTY);userequires 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/optin 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 trailingoptcomponent), 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:
- 25
current_exe()→ the running process's truth. A modern managedvibelives 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), andVIBEVM_INSTALL_ROOT(walk up toopt) from its own path — no env var needed. Outside a managed location (devcargo run, a bare copy), it falls back to env, then defaults. VIBEVM_SHELL_HOME→ explicit shell-local selection.self use --evalandself envemit it together with advisory HOME; stable shims prioritize this non-durable override so evaluation really switches bothvibeandvibe-indexeven while durablecurrentnames another instance.currentfile → the live active instance. The shim reads$shimdir/../vibevm/currenton every launch and execs that instance.self userewritescurrent→ the nextvibein the same shell uses it. No reload (the shim reads a file, not the shell's frozen env).$VIBEVM_HOME/$VIBEVM_INSTALL_ROOT(env) → advisory. Still set durably for externalJAVA_HOME-style tools, but no longer the source of truth. They may lag (new shells only);vibe vars(§2.14) reconciles, and a managedvibewhosecurrent_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 resolvecurrent, then fall back to$VIBEVM_HOME; exec the corresponding member or print "no active vibevm — runvibe self use <selector>". - Both POSIX and
.cmdforms 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_ROOTas advisory env (§2.5) — repointed onself usefor external tools; truth lives incurrent+current_exe.
30Rules:
- 31idempotent (a marker guards the edit; no duplicate lines/entries),
- never clobber (only our entry is added; the rest of
PATHis preserved), - OS/shell-aware (Windows: raw
HKCU\Environment— read WITHOUT variable expansion, the value's registry kind preserved (REG_SZstaysREG_SZ,REG_EXPAND_SZstaysREG_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 aWM_SETTINGCHANGEbroadcast 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, flipscurrentsecond, and updates advisoryVIBEVM_HOMElast, 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:
- external —
self installrun 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 laterself 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 withgit fetch/checkout(orpull; 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-indexinto the sharedbuild/target dir (§9.3 — never the source tree'starget/; load- bearing on Windows and keeps the dev tree clean), honouringrust-toolchain.toml. - Build the two essential binaries only.
vibe-indexis 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 alongsidevibe; they have moved to a separate products repo (vibevm-term) and now publish themselves toPATHthrough their own version-manager (<product> self install). The install pipeline here buildsvibe+vibe-index—vibe term/vibe frameresolve the terminal apps through$VIBEVM_<APP>→ the active instance's packaged<app>/(back-compat) →PATH, with an in-place fallback forvibe treewhen no terminal app is available (spec://term-common/PROP-vvm#rootis 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 onlyvibe.exechanged 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, andvibevm-source.zipby 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 undersource/. The inventory retains the authenticated manifest digest that anchors laterdoctor/reuse checks. - Remote labels are mutable; local instances are not. A republished
v1.0.0may have new bytes, but it becomestag: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.tomlgets 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/flipcurrent+previous, and update advisoryVIBEVM_HOMElast.
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_TOOLStable —(name, min_version, check_command, help_url)— read byself 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:
- 40
self remove <selector>— remove that version or exact#N. For binary bundles,--binremoves onlybin/,--srconly the ownedsource/+ 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 removewith 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
removeorgc --prune-others; the immediateprevioustarget survives pruning. self gcoperates only inside the install root and never touches the shared~/.cargocaches.
2.11 Introspection — doctor, ls, current, which, env
46req r2
- 47
self doctorverifies end to end: both generated stable shims have their expected content/mode and their directory is onPATH; 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;--fixrepairs shims/PATH with consent and recomputes the result. self ls/current/whichread thecurrentfile for the active selection andcurrent_exe()for the independently running selection. A direct worktree executable wins over cwd and over an unrelated active binary.self envprints 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 rawvibebootstrap by size/SHA-256, then that Rust binary verifies and atomically installs the full bundle. - The scripts are shipped release assets
install.shandinstall.ps1and are exposed by stablehttps://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_HOMEis stale (§9.1). vibe varsprints the project's env-configurable variables —VIBEVM_INSTALL_ROOT,VIBEVM_HOME(whose actual values are derived fromcurrent_exe, §2.5), plusVIBE_INVOKED_BY,VIBE_UNATTENDED,VIBE_LOG— inNAME=VALUEform. The publish token is deliberately excluded.
- 56
vibe vars— actual values, oneNAME=VALUEper line. vibe vars diff—NAME=VALUE [ENV_VALUE]; the bracket appears only when the environment differs from the actual.vibe vars full— two tables,# ACTUALthen# ENVIRONMENT.vibe vars full diff— both tables, differing names markedNAME=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 —
currentstays ("already up to date").--forcealways makes a fresh instance. - The complete new instance is staged then atomically renamed; stable PATH is
ensured,
currentflips, and only then advisoryVIBEVM_HOMEupdates. - 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.exeis 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:
- 64
managed— the VVM-owned clone atsrc/.mirror(VVM updates it via git and may drop it onremove/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 aspayload_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_pathmakes 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 sourcereturnsinstance/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 cluttersself 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 (thefind_source_rootshape — workspaceCargo.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 rewrittensource_pathmatches 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.tomlrewrite: (a) every external instance whosesource_pathis 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/previousare repaired as one journaled transaction before any stale instance removal. Removing a protected version isself 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--yeserrors rather than silently applying (the same contract asself remove/gc, §2.9, §2.10).--dry-runprints the plan and changes nothing.--jsonemits 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, andaarch64-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/drivecargo xtask dist build. Construction runs without tests by default;--checks,--tests, and--self-checkare explicit opt-ins, and--uploadremains 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 requiredmusl-gccis 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 archivesupplies 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, plusvibe,vibe-index,DISTRIBUTION.json,LICENSE.md, andREADME.md. The raw bootstrap asset is byte-identical to the bundle'svibecomponent. - Every version label is mutable by default.
dist prepare --version Vdeletes any existing draft or published release forV, moves or createsvVat the exact source commit, and creates one fresh draft. Four hosts may independently replace their ZIP/bootstrap/fragment triplets. The exact directrefs/tags/vV→ commit binding is verified after prepare and again immediately before publication.dist finalizerequires and remotely verifies all twelve platform assets, publishesDISTRIBUTIONS.json+ both installers, then publishes with GitHubmake_latest=legacyso 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
xtaskwithout credentials and runs that already-built trusted binary only in narrow prepare/upload/finalize steps carryingGITHUB_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:
- 74
VersionStore— 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— runscargofor a profile/toolchain; mocked in tests.Placer— the diff-copy of a distribution into a new instance (§2.15).EnvPersister— the durablePATH/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— theREQUIRED_TOOLStable and checks (§2.8).vars— the actual-vs-environment resolver (§2.14),current_exe-aware.
- 75A managed
viberesolves its root/active fromcurrent_exe+ thecurrentfile; env is the fallback. - The command lives as
cli/vvm.rs+cliforvibe vars, with logic undercommands/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(livecurrent, 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-awareself which, andvibe 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_pathrecord + rebuild-from-remembered).
5. Out of scope (now)
- 77Retired from out-of-scope: fetched, publisher-digest-bound native bundles
ship through
self bootstrap, binaryself update, and explicit SemVerself 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
--binaryswitch; 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_TOOLStable (§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 useswitches the active version and the nextvibein the same shell is the new one — no reload (currentfile).- 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 varsreports actual vs environment;vibe vars diff/full/full diffper §2.14; the publish token never appears.self removenever wipes without--all+ reconfirm;self gcnever touches~/.cargo; external sources are never modified or removed.self relocate <new>repoints externalsource_pathrecords and removes the stale instance dirs built from the old tree, keeping the active instance; the active's source is repointed, not deleted.--dry-runchanges nothing; a non-TTY run without--yeserrors.self importis inactive by default (--usealone 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#Nand preserves the previous instance;--replace-candidateremains compatibility spelling.- A native bootstrap installs both essential binaries and the matching exact
source tree under one immutable instance;
self sourcereturns its absolute path, normal same-digest refresh is a verified no-op,--forceallocates the next#N, and rollback restores the previous generation. - With two source worktrees and one binary install,
self ls/current/whichdistinguish 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.shgreen; 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_HOMEthe single source of truth for the active version. Environment variables are inherited at process start, so a shell's$VIBEVM_HOMEis frozen until the shell is reloaded — everyself use/ reinstall forced "open a new terminal". - The fix: the shim reads a live
currentfile each launch (filesystem is live → instant switch in the same shell). - A running
vibederives its own identity fromcurrent_exe()(it is the binary, so it knows its path). $VIBEVM_HOMEstays only as an advisory/compat env for external tools, reconciled byvibe 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 importpath 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.
--forcebypasses 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-othersdecision. - 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
currentand$VIBEVM_HOMEdisagree, the warning is emitted by thevibebinary at startup, not by the sh/cmd shim: the binary hascurrent_exeground truth and thevibe varsformatter, and keeps the shims trivial. - The warning is suppressed outside a managed run (a dev
cargo runhas no managed location and should not be nagged).