PROP-001: Git-backed registry for vibe-registry
01Milestone: M1.1 (ROADMAP.md).
02Status: accepted 2026-04-22, shipped 2026-04-22. Partially superseded by PROP-002 (2026-04-24). See the "Superseded parts" block below.
03Supersedes: nothing.
04Related: spec://org.vibevm.core/vibevm/common/PROP-000#registry, VIBEVM-SPEC.md §8, PROP-002.
Superseded parts (by PROP-002)
05The following decisions in this PROP were revised by PROP-002 when the registry model moved from monorepo-as-registry to decentralized per-package repos. Use PROP-002 as the authoritative source for these:
- 06§2.3
Registrytrait — the single-registry trait is extended by aMultiRegistryResolvercoordinating several[[registry]]entries, each wrapped as aGitPackageRegistry. The monorepo-eraGitRegistryis retired. - §2.4 Cache layout —
~/.vibe/registries/<hash>/clone/(one clone per registry URL) is replaced by~/.vibe/registries/<canonical-url-hash>/packages/<kind>-<name>/{clone,meta.toml}(one clone per package; the package directory was later re-keyed by identity to<group>.<name>— PROP-002 §2.6). - §2.6 Lockfile
source_uriformat —git+<transport>://<host>/<path>.git#<kind>/<name>/v<ver>(path-in-monorepo) is replaced by full lockfile fields:registry,source_url,source_ref,resolved_commit,content_hash;#fragmentis no longer used.
07What is not superseded (and remains authoritative here): §2.1 (shell-out-to-git backend choice), §2.2 (GitBackend trait), §2.5 (1-hour freshness TTL), §2.7 (Windows UX and stderr classification).
- 08Additionally, the size-footprint argument in §2.1 is pruned by PROP-000 §15 (dependency weight is not a decision factor).
- The remaining arguments against
git2(Windows SSH-auth lottery, diagnostic clarity of shell-out error messages) still carry the decision for M1 — but the argument tree is narrower now. - Revisit when a concrete reason arises, e.g. programmatic object reads that shell-out can't do cheaply.
1. Motivation
09M0 shipped with a local-directory registry only.
10M1 makes the registry a git
repository hosted on GitVerse (default
git@gitverse.ru:anarchic/vibespecs.git). The implementation must:
- 11Clone the registry into
~/.vibe/registries/<hash>/on first use andgit pullon subsequent use (VIBEVM-SPEC.md§8.3). - Preserve the existing
LocalRegistrycode path so tests and the--registry <path>override keep working. - Authenticate against GitVerse using the SSH identity the user has
already configured (see
vibevm/vibespecs/boot/90-user.xml). - Run on Windows, macOS, and Linux with no per-platform build hoops.
- Carry its operational weight on constrained dev machines without
bloating the
vibebinary or adding a C toolchain requirement.
12This PROP records the architectural decisions. The mechanics
(Registry trait surface, error variants, wire-level command lines)
live in the crate's module documentation — lib.rs doc comments and the error
strings that cite spec:// anchors — not in a README; crates/vibe-registry
has none.
2. Decisions
2.1 Backend: shell out to git, not git2
13Decision: vibe-registry performs all git operations by spawning the
system git binary via std::process::Command. We do not link
against libgit2 (via the git2 crate) in v1.
14Why:
- 15SSH on Windows is the killer. GitVerse authenticates via SSH.
Git for Windows ships OpenSSH and a working
ssh-agent; the user's identity (olegchir@UNIT-2040) is already loaded and the push togitverse.ruis proven (seevibevm/vibespecs/boot/90-user.xml).libgit2useslibssh2for SSH, which talks tossh-agentthrough a named-pipe protocol that is fragile on Windows and routinely requiresSSH_AUTH_SOCKjuggling or explicit key paths. Cargo itself falls back to the systemgiton auth failure for this exact reason. Shell-out makes that fallback the primary path and retires the class of bug.
- 16Dependency footprint.
git2pullslibgit2-sys,libssh2-sys,libz-sys, andopenssl-sys(or a vendored alternative). Non-vendored builds demand a working C toolchain on every developer and CI machine; vendored builds add 3–8 MB to the release binary. Shell-out adds zero bytes and zero build-time native dependencies.
- 17Feature parity and debuggability. The user's
gitis by definition current. Errors surface with the full native message;tracinglogs the exact argv so a user can re-run the failing command by hand.libgit2's error strings (ERROR class=Net (12): unexpected http status code: 401) are harder to diagnose.
- 18We do not need programmatic git. The v1 operations are
git clone,git fetch,git pull --ff-only, andgit --versionfor preflight. No partial clone, no in-memory object reads, no custom refspecs, no progress UI. Shell-out handles this trivially.
- 19Licensing.
gitis GPL v2, but shell-out isexecnot linkage — the GNU FAQ explicitly separates these. Our binary stays unambiguously permissive.libgit2is GPL v2 with a Linking Exception (permissive for our purposes), but shell-out leaves the entire conversation at the door.
20Risks accepted:
- 21Runtime dependency on
gitinPATH. Acceptable: our target audience is developers who already have git installed. We perform a preflightgit --versioncheck and emit an actionable error (with a pointer tohttps://git-scm.com/downloads) if it is missing. - stderr parsing for fine-grained error classification. We
mitigate by running git with
LC_ALL=Cand keying off exit code + substring markers (fatal:prefix,Permission denied (publickey),Repository not found). See §2.7.
22When to revisit: if and when we need one of:
- 23partial/sparse clone with custom filters,
- programmatic object reads (e.g. to fetch a
latestmarker file without a working-tree checkout), - OS-credential-store integration that can't be delegated to
git, - running on a platform where bundling
gitis easier than requiring it.
24At that point, add a libgit2 feature behind the GitBackend trait
(§2.2). The trait is designed so the switch costs one impl block
and one line in the factory, and nothing else in the codebase moves.
2.2 GitBackend trait
25Decision: vibe-registry::git_backend::GitBackend is the single
interface through which the registry layer touches git. It has exactly
the operations we use:
26pub trait GitBackend: Send + Sync {
/// Clone `url` (checked out at `refname`) into `dest`.
/// Caller guarantees `dest` is either empty or absent.
fn bootstrap(&self, url: &str, refname: &str, dest: &Path) -> Result<(), GitError>;
/// Fast-forward `dest` to the tip of `refname` on origin.
/// No-op if already up to date.
fn update(&self, dest: &Path, refname: &str) -> Result<(), GitError>;
}
27Method-name note. The "make a fresh clone" operation is called
bootstrap rather than the obvious clone or clone_into because
the backend is held as Arc<dyn GitBackend> at its call sites and
both of those names collide with blanket-impl methods from the
standard library (std::clone::Clone::clone,
std::borrow::ToOwned::clone_into), forcing ugly <T as
GitBackend>::… disambiguations at every call. bootstrap is
semantically accurate — it's how we initialise the registry cache
from empty state — and has no std-library namesake.
28Why narrow. The narrower the trait, the cheaper the backend swap.
If we need ls_remote or fetch_ref later, we add a method — that
addition is a visible, deliberate change, not a quiet interface drift.
29Implementations:
- 30
ShellGit— default, built fromstd::process::Command. See §2.7. LibGit2— reserved. Not implemented in M1; the trait is the entry point for a future feature-gated addition.
31The vibe-registry crate does not expose a mock implementation.
Tests use ShellGit against a bare git repository created in a
tempdir — exercising the production code path end-to-end.
2.3 Registry trait
32Decision: introduce a vibe-registry::Registry trait that both
LocalRegistry and GitRegistry implement:
33pub trait Registry {
fn list_versions(&self, kind: PackageKind, name: &str)
-> Result<Vec<semver::Version>, RegistryError>;
fn resolve(&self, pkgref: &PackageRef)
-> Result<ResolvedPackage, RegistryError>;
fn fetch(&self, resolved: &ResolvedPackage, cache_root: &Path)
-> Result<CachedPackage, RegistryError>;
}
34vibe-install and vibe-cli continue to consume ResolvedPackage /
CachedPackage exactly as in M0; the only change is that the concrete
type is chosen at CLI-arg-parse time.
35Selection rule. CLI precedence stays as defined in VIBEVM-SPEC.md
§9.1: --registry <path> (explicit, always a local directory) wins
over the [registry] section in vibe.toml (a URL — git or
file://).
2.4 Cache layout
36Decision: the on-disk layout under ~/.vibe/registries/ is:
37~/.vibe/registries/
└── <hash>/
├── clone/ ← the git working tree
└── meta.toml ← { url, ref, last_pulled_at }
- 38
<hash>= lowercase hex of the first 16 bytes ofsha256(normalized_url). 16 hex chars is enough to avoid realistic collisions while keeping the directory name tab-completable (same trick Cargo uses for its git cache). The full hash lives inmeta.tomlfor audit. normalized_urlstrips a trailing.gitand lowercases the scheme + host sogit@gitverse.ru:anarchic/vibespecs.gitandssh://git@gitverse.ru/anarchic/vibespecshash to the same registry.meta.tomlis written after each successful clone or update. It carries the url (for debugging), the ref, and the UTC RFC3339 timestamp of the last successful fetch.- The
clone/subdirectory is the registry working tree.GitRegistryinternally wraps aLocalRegistry::new(clone_dir)and delegatesresolve/list_versions/fetchto it — the packaged layout (<kind>/<name>/v<ver>/…) is identical in both worlds.
39Per-project package cache (<project>/.vibe/cache/<kind>/<name>/<ver>/)
is unchanged from M0.
2.5 Freshness policy
40Decision: the default freshness TTL is 1 hour, checked against
meta.toml.last_pulled_at. An install whose registry cache is older
than the TTL triggers an implicit update. An install whose cache is
younger skips the pull. vibe registry sync forces an update
regardless of age.
41Why 1 hour: short enough to pick up new package versions within one working session, long enough to amortise network round-trips over a burst of installs. Revisit once real usage arrives.
42Superseded — --offline shipped. This recorded the M1 state: a
network failure during an implicit update failed the install with a clear
message. Offline resolution landed with PROP-002 §2.2.2.1
(url_is_local) and the PROP-030 flag, and is live in vibe install --help.
2.6 Lockfile source_uri format
43Decision: when a package originates from a git registry, the lockfile records its source as
44git+ssh://git@gitverse.ru/anarchic/vibespecs.git#<kind>/<name>/v<ver>
45The #fragment names the package directory inside the registry
relative to the registry root. The scheme prefix (git+ssh /
git+https / git+file) encodes the transport. Local-directory
registries continue to produce file://… URIs as in M0.
46Why a scheme prefix. pip and Cargo both use git+… prefixes to
disambiguate a git source from a plain URL; it reads obviously in
the lockfile.
2.7 Windows UX and stderr parsing
47Decision: on Windows, every git subprocess is spawned with the
CREATE_NO_WINDOW creation flag (0x08000000) via
std::os::windows::process::CommandExt::creation_flags.
48Why. If vibe ever runs inside a process without a console of its
own (a GUI launcher, IDE plugin, Windows service), a child with
CREATE_CONSOLE semantics would flash a separate black window. The
flag costs nothing in the console-attached case (stdio still
inherits), and covers the hypothetical hostless case for free.
49Decision: every git invocation runs with LC_ALL=C and
LANG=C in the environment so error strings are stable across user
locales. We key error classification off:
- 50exit code (zero vs non-zero),
- stderr substrings:
fatal: repository … not found,Permission denied (publickey),Could not resolve host,Repository .* is empty,unable to access.
51Anything unmatched is reported as a generic "git command failed" with the raw stderr attached. Stable classification covers the diagnoses we hand-hold the user through; the catch-all covers the rest without hiding information.
3. Rejected alternatives
3.1 git2 crate as the primary backend
52Rejected for M1. See §2.1. The decision is reversible via
GitBackend (§2.2).
3.2 Hybrid git2 + shell-out fallback
53Cargo does this. Rejected for v1 because it doubles the surface area
(two backends under one implementation), makes error messages
conditional on which path fired, and provides zero benefit on our
target matrix. Revisit only if we ever take the libgit2 branch and
need auth fallback to system git.
3.3 Sparse / partial clone in M1
54Rejected: vibespecs is tiny. Optimisation is M2. The GitBackend
trait is narrow enough that adding a clone_sparse method later is a
one-line extension.
3.4 Hosting the registry cache under the project
55Rejected: cache-per-project duplicates the same git clone across every
project on the same machine. VIBEVM-SPEC.md §8.3 already pins the
cache at ~/.vibe/registries/<hash>/ for this reason.
3.5 Vendoring git with the vibe binary
56Rejected: vendoring a full git is the antithesis of "single Rust
binary". If we ever want zero runtime dependencies, the answer is
libgit2, not a bundled git.
4. Out of scope for M1.1
- 57Authentication for HTTPS registries with token / PAT (M2, PROP later).
vibe publish(VIBEVM-SPEC.md§8.4 pins this to v2+).- LLM-based install review (
VIBEVM-SPEC.md§8.5, M2). - Progress UI for long clones.
- Multiple registries per project.
--offlineflag.
5. Acceptance (for M1.1 implementation)
58Code-complete and live on 2026-04-22. Every box below ticks; the milestone is shippable.
- 59[x]
vibe-registryexposes aRegistrytrait and two implementations (LocalRegistry,GitRegistry). - [x]
GitBackendtrait +ShellGitimplementation land invibe-registry::git_backend. - [x]
ShellGitpreflight (git --version) runs once per instance (cached viaOnceLock) and emitsGitError::NotInstalledwith an actionable message if absent. - [x]
ShellGit::bootstrapandShellGit::updatesucceed against a bare fixture repo in an integration test. - [x] Cache lives at
~/.vibe/registries/<hash>/{clone,meta.toml}. - [x]
meta.tomlgains a well-formedlast_pulled_atafter each fetch. - [x] Freshness policy: ≤1h skips pull; >1h pulls;
vibe registry syncalways pulls (TTL=0 uses>=so same-second wallclock still triggers). - [x] End-to-end install against a
git+file://…registry seeded with the canonicalflow:wal@0.1.0fixture succeeds; the lockfile records agit+…#flow/wal/v0.1.0source URI. - [x] Manual smoke-test against the real
git@gitverse.ru:anarchic/vibespecs.git(commit98e51fc) ran 2026-04-22 on Windows / Git Bash; every step matched the expected output, including thegit+ssh://git@gitverse.ru/anarchic/vibespecs.git#flow/wal/v0.1.0lockfile source URI. Procedure and last-pass metadata live inmanual-tests/M1.1-git-registry-smoke.md. - [x]
vibe registry sync(no args) force-pulls the configured registry. - [x] Windows: every spawned git carries
CREATE_NO_WINDOW; no stray console windows from a hostless parent. - [x]
cargo test --workspacegreen (77 tests). - [x]
cargo clippy --workspace --all-targets -- -D warningsclean.
6. Open questions
60None blocking. Parking lot:
- 61Resolved — shipped as proposed. The
VIBE_GIT_BINARYPATH override lives ingit_backend/shell.rsand its comment cites §6 of this PROP; the env-var form was chosen over a CLI flag exactly to keep the CLI surface stable. - Does the registry cache need a lock file against concurrent
vibeinvocations? Probably yes for M2; a crash mid-clone leaves a half-populatedclone/. For M1, document the behaviour ("if a clone fails, delete the cache dir and retry") rather than mechanising it.