vmn
A version number that can rebuild your repo.
Language-agnostic semantic versioning where a version is a restorable state, not just a label.
Designed for AI-assisted development. Versions live in git tags — no database, no server, no lock-in.
Works with any language in a git repository — vmn versions the repo, not the build system.
pip install vmn
vmn stamp -r patch my_app # => 0.0.1 (auto-initializes, no setup)
# ...six months and 400 commits later, prod is broken on 0.0.1
vmn goto -v 0.0.1 my_app # your repo AND every dependency repo,
# exactly as they were when 0.0.1 shipped
Other versioning tools produce a string. vmn produces a state you can return to — across every repository your product spans, not just the one you run it in.
The same property is what makes vmn a natural fit for AI-assisted development: give each agent an isolated workspace, capture what it produced, and roll back what didn't work.
All state is stored as plain YAML in git annotated tag messages. Remove vmn and the tags remain fully readable.
vmn has been in continuous development since 2019 and versions its own releases. It is in daily production use by teams shipping multi-repository products, where recovering from a bad release means restoring every repo to a known state, not just one.
Install · The idea · AI agents · Why vmn · Commands · Config · Experiments · Web UI · Islands · CI · Help · Migrate
📦 Install
pip install vmn # or: pipx install vmn / uvx vmn
pip install "vmn[ui]" # + the web dashboard
Requirements: Python 3.8+, Git 2.10+ (2.17+ recommended). Linux, macOS, Windows/WSL. Nothing platform-specific to configure.
vmn is a Python CLI, but the project it versions can be in any language — it operates on git tags, not on your build system.
Try it in 30 seconds (copy-paste, no existing repo needed)
mkdir remote && cd remote && git init --bare && cd ..
git clone ./remote ./local && cd local
echo a >> ./a.txt && git add ./a.txt && git commit -m "first commit" && git push origin master
vmn stamp -r patch my_app # => 0.0.1
echo b >> ./a.txt && git add ./a.txt && git commit -m "feat: add b" && git push origin master
vmn stamp -r patch my_app # => 0.0.2
git tag -n1 my_app_0.0.2 # the version metadata is right there in the tag
No vmn init needed — the first vmn stamp initializes the repo and the app. Works in CI, in shallow clones, and fully offline.
Shell completion (bash / zsh / fish / tcsh)
vmn --completion-install # auto-detects your shell, idempotent
vmn --completion # or just print the script, change nothing
vmn --completion-uninstall # remove it again
All three accept an explicit shell name if auto-detection picks the wrong one. After installing, restart your shell. vmn <TAB> lists commands, vmn stamp <TAB> suggests your tracked app names, and vmn stamp -r <TAB> offers the release modes.
💡 The idea
Most tools treat a version as a name for a moment. vmn treats it as a handle on a state — and once you have that, the same primitive answers four different problems.
| Granularity | Command | What gets captured |
|---|---|---|
| Released state | vmn stamp → vmn goto |
committed code + the exact commit of every dependency repo |
| Working state | vmn snapshot |
↑ plus uncommitted changes, unpushed commits, untracked files |
| Measured state | vmn exp |
↑ plus metrics, params, artifacts, and a run log |
| Parallel state | vmn worktrees |
any of the above, checked out as a separate tree instead of replacing your current one |
Each of the first three rows extends the one above it; the fourth materializes any of them as a separate tree. These are not four separate features: vmn exp is built on the snapshot primitive, and snapshots share the version grammar of stamps. That is why vmn goto -v 1.2.0-dev.a1b2c3d.e4f5g6h works — a snapshot is a version.
What this enables
State recovery across repos. If your product spans five git repos, vmn stamp records every dependency's commit hash into the tag. vmn goto restores all of them, in parallel, cloning any that are missing. Reproducing a six-month-old bug becomes one command instead of an afternoon of digging through CI logs.
Uncommitted work becomes addressable. git stash is unnamed, local, and single-repo. A WIP commit pollutes history. A snapshot turns your exact working state — modified files, local commits, untracked files, across every dependency — into a version string you can name, diff, share, and restore.
Experiment tracking with no server. An experiment is a snapshot plus an append-only metrics log. That's the whole design. No tracking server, no database, no cloud account — and unlike every dedicated tracker, the code state is captured, not just the numbers.
Microservice topology. Version services independently under one root app. Each service keeps its own semver; the root gets a monotonic integer that ticks on every child stamp — one number for "what changed last" across the whole platform.
Version formats vmn understands
1.6.0 # release
1.6.0-rc.23 # prerelease
1.6.7.4 # hotfix — an optional 4th segment
1.6.0-rc.23+build01.Info # build metadata
1.6.0-dev.a1b2c3d.e4f5g6h # dev snapshot (commit hash + diff hash)
Standard Semver 2.0, plus two additions. The hotfix segment lets you ship an emergency fix without consuming a patch number, keeping the release train on schedule. The dev snapshot is content-addressed: identical code always produces the identical version string, so re-snapshotting an unchanged tree returns the same version instead of creating a duplicate.
🤖 Built for AI-assisted development
Coding agents are fast, parallel, and occasionally destructive. Each of those properties creates a state problem — and state is what vmn manages.
| Problem | vmn's answer |
|---|---|
| The agent doesn't know your versioning conventions | vmn skill --install — writes vmn's usage instructions into the agent's instruction file |
| Multiple agents editing one working tree overwrite each other | vmn worktrees — one isolated island each, dependencies included |
| An agent corrupted the tree and you need the last good state | vmn snapshot / vmn goto |
| An agent shouldn't be cutting releases | --no-stamp islands, where stamping is refused |
| Identifying which of many runs produced the good result | vmn exp — every metric anchored to the tree that produced it |
The tool documents itself to the agent. vmn skill --install writes vmn usage instructions into your agent's instruction file — .claude/skills/vmn/SKILL.md, .cursorrules, or AGENTS.md. The agent learns your versioning workflow from the tool that implements it rather than inferring it from the repo. The cursor and agents targets write a marker-delimited block, so re-running updates vmn's section and leaves the rest of your instructions untouched.
vmn skill --install # Claude Agent Skill
vmn skill --install --target cursor # .cursorrules
vmn skill --install --target agents # AGENTS.md
Every agent gets its own island. One command creates a git worktree for your repo and every dependency repo, pinned to a known-good state, alongside your work rather than in place of it. Agents working in separate islands cannot touch each other's files.
vmn worktrees create my_app --island-name agent-auth
vmn worktrees create my_app --island-name agent-perf
Each island includes an island.json manifest — paths, branches, dependency hashes — so an agent can orient itself without being told the layout. Use --no-stamp for a read-only island when the agent shouldn't create versions at all.
Recoverable by default. Agents leave behind uncommitted edits, half-finished refactors, and untracked scratch files — state that git stash and WIP commits handle poorly. vmn snapshot create captures all of it, across every dependency repo, as a named version you can return to. Restore operations snapshot whatever is currently dirty before overwriting it, so uncommitted work is never silently destroyed.
Keep the good run reproducible. When an agent iterates on something measurable — a prompt, a heuristic, a model — vmn exp run records the metrics and the exact tree that produced them. Identifying run 14 as the best one is still useful a month later, because run 14's code remains addressable.
The full text of the instructions vmn installs is in docs/agent-skill.md.
⚡ Why vmn
| Capability | vmn | semantic-release | release-please | changesets |
|---|---|---|---|---|
| Language-agnostic | ✅ | JS-centric | JS-centric | JS only |
| Git-tag source of truth | ✅ | ✅ | ✅ | ❌ |
| Conventional commits + changelog | ✅ | ✅ | ✅ | partial |
| GitHub Release creation | ✅ | ✅ | ✅ | ❌ |
| Auto-embed version into project files | ✅ | per-plugin | ❌ | JS only |
| Multi-repo dependency tracking | ✅ | ❌ | ❌ | ❌ |
State recovery (vmn goto) | ✅ | ❌ | ❌ | ❌ |
| Microservice / root-app topology | ✅ | ❌ | ❌ | monorepo |
| 4-segment hotfix versioning | ✅ | ❌ | ❌ | ❌ |
| Zero-config start (auto-init) | ✅ | ❌ | ❌ | ❌ |
| Offline / air-gapped | ✅ | ❌ | ❌ | ❌ * |
| Uncommitted-state capture | ✅ | ❌ | ❌ | ❌ |
| ML experiment tracking | ✅ | ❌ | ❌ | ❌ |
Ships agent instructions (vmn skill) | ✅ | ❌ | ❌ | ❌ |
| Parallel agent isolation (islands) | ✅ | ❌ | ❌ | ❌ |
Bold rows are things only vmn does. * changesets authors offline but needs GitHub/npm to publish.
vs. experiment trackers (MLflow, W&B, DVC, Neptune)
| Capability | vmn | MLflow | W&B | DVC | Neptune |
|---|---|---|---|---|---|
| No server required | ✅ | ❌ * | ❌ | ✅ | ❌ |
| No cloud account | ✅ | ✅ self-hosted | ❌ | ✅ | ❌ |
| Free & open source | ✅ | ✅ | free tier | ✅ | free tier |
| Metrics + live curves | ✅ | ✅ | ✅ | ❌ | ✅ |
| Web UI | ✅ one command | server + DB | cloud | ❌ | cloud |
| Full code-state capture | ✅ | ❌ | ❌ | partial ** | ❌ |
| Uncommitted changes captured | ✅ | ❌ | ❌ | ❌ | ❌ |
| One-command state restore | ✅ | ❌ | ❌ | ❌ | ❌ |
| Stamp-tree / version DAG view | ✅ | ❌ | ❌ | ❌ | ❌ |
| Built-in version management | ✅ | ❌ | ❌ | ❌ | ❌ |
| Works offline / air-gapped | ✅ | self-hosted | ❌ | partial | ❌ |
| Install | pip install vmn |
server + DB | account + pip | pip + git config | account + pip |
| Lock-in | none (git tags + files) | MLflow format | W&B cloud | DVC format | Neptune cloud |
* MLflow can log to local files, but the comparison UI needs mlflow server. ** DVC versions data/model files via git but captures no uncommitted code.
Use vmn when you want a CLI-first, local-first workflow, you work offline or air-gapped, you want versioning and experiments in one tool, or you don't want to run infrastructure to track training runs.
Use MLflow / W&B when you need hosted dashboards, team collaboration features, reports, or sweep orchestration.
Is vmn for me?
| Any language — Python, Rust, Go, C++, Java, JS, anything in a git repo | Microservices — independent versions per service, one root counter |
| Multi-repo — reproducible state recovery across repositories | Zero config — no plugins, no pipelines, no ecosystem buy-in |
| Offline / air-gapped — works with no network at all | Zero lock-in — versions are plain git tags |
| ML / research — reproducible snapshots with metrics, no tracking server | CI — handles shallow clones automatically |
| AI-assisted development — per-agent isolation, generated agent instructions, state rollback | Fast-moving teams — capture and roll back state without ceremony |
🔧 Commands
| Command | What it does | Example |
|---|---|---|
stamp |
Create a new version | vmn stamp -r patch my_app |
release |
Promote a prerelease to final | vmn release my_app |
show |
Display version info | vmn show my_app |
goto |
Restore repo + deps to a version | vmn goto -v 1.2.3 my_app |
snapshot |
Capture uncommitted working state | vmn snapshot create my_app |
experiment |
Track runs with metrics (alias exp) |
vmn exp run my_model -- python train.py |
worktrees |
Isolated parallel dev islands | vmn worktrees create my_app |
ui |
Serve the web dashboard | vmn ui |
gen |
Render a file from a Jinja2 template | vmn gen -t ver.j2 -o ver.txt my_app |
add |
Attach build metadata to a version | vmn add -v 1.0.0 --bm build42 my_app |
config |
Edit app config (TUI or scriptable) | vmn config my_app |
skill |
Emit AI-agent instructions for vmn | vmn skill --install |
init / init-app |
Explicit init — rarely needed, stamp auto-inits |
vmn init-app -v 1.4.2 my_app |
Global flags: --debug, --version, --completion[-install|-uninstall] [SHELL]
stamp
vmn stamp -r patch my_app # => 0.0.1
vmn stamp -r minor my_app # => 0.1.0
vmn stamp -r patch --pr rc my_app # => 0.1.1-rc.1
vmn stamp my_app # no -r needed with conventional_commits
vmn stamp --dry-run -r patch my_app # preview, commit nothing
vmn stamp --pull -r patch my_app # pull first, retry on conflict
Idempotent — it won't re-stamp a commit that already has a version. Auto-initializes the repo and app on first run.
Conventional commits, changelogs, GitHub Releases
With conventional_commits enabled, -r becomes optional. Commit prefixes map to release modes: fix: → patch, feat: → minor, BREAKING CHANGE or ! after the type → major.
git commit -m "feat: add search endpoint"
vmn stamp my_app # => 0.2.0, minor inferred
conf:
conventional_commits: true
default_release_mode: optional # or "strict"
changelog:
path: "CHANGELOG.md"
github_release:
draft: false
Changelog generation requires conventional_commits. GitHub Releases need the gh CLI and GITHUB_TOKEN.
-r vs --orm, and every stamp flag
Without -r: works during an in-progress prerelease sequence, or always if conventional_commits is on. Otherwise it errors on a release commit.
| Flag | Behavior |
|---|---|
-r patch |
Strict — always advances. 0.0.1 → 0.0.2; 0.0.2-rc.3 → 0.0.3. |
--orm patch |
Optional — advances only if no prerelease already exists at the target. |
| Flag | Description |
|---|---|
-r, --release-mode |
major, minor, patch, hotfix, micro |
--orm, --optional-release-mode |
major, minor, patch, hotfix |
--pr, --prerelease |
Create a prerelease (--pr rc → X.Y.Z-rc.N) |
--pull |
Pull before stamping; retries on conflict |
--dry-run |
Preview without committing or tagging |
-e, --extra-commit-message |
Append text to the stamp commit message |
--ov, --override-version |
Force a specific version string |
--orv, --override-root-version |
Force a specific root-app version |
--dont-check-vmn-version |
Skip the vmn compatibility check |
--git-push-user / --git-push-token |
Push credentials (see below) |
Push credentials. For checkouts with no credentials of their own (CI runners, containers), --git-push-user / --git-push-token — or VMN_GIT_PUSH_USER / VMN_GIT_PUSH_TOKEN — make vmn rewrite the remote to an authenticated HTTPS URL for that single push only, leaving your git remote config untouched. ssh:// and git@host: remotes are converted to HTTPS. Both values must be supplied together; a lone one is ignored with a warning. vmn release accepts them too.
vmn init-app flags: -v/--version (initial version, default 0.0.0), --dry-run, --orm/--default-release-mode (optional | strict).
release
vmn release my_app # auto-detect from the current commit
vmn release -v 0.0.1-rc.1 my_app # explicit version — tag only
vmn release --stamp my_app # full stamp flow: commit + tag + push
Promotes a prerelease to final. Idempotent. -v and --stamp are mutually exclusive.
Iterating on release candidates
vmn stamp -r major --pr alpha my_app # 2.0.0-alpha.1
vmn stamp --pr alpha my_app # 2.0.0-alpha.2
vmn stamp --pr mybeta my_app # 2.0.0-mybeta.1
vmn release my_app # 2.0.0
show
vmn show my_app # current version
vmn show --verbose my_app # full YAML metadata
vmn show --dev my_app # dev version (commit + diff hash)
vmn show --type my_app # release / prerelease / metadata
vmn show -u my_app # unique ID (version + commit hash)
vmn show --root my_platform # root-app version (an integer)
Remaining show flags
| Flag | Description |
|---|---|
-v, --version |
Show info for a specific version |
-t, --template |
Render with an ad-hoc template |
--raw |
Skip template formatting |
--conf |
Print the effective app configuration |
--from-file |
Read local state instead of git tags |
--ignore-dirty |
Don't fail on a dirty working tree |
goto
vmn goto -v 1.2.3 my_app # repo + all deps
vmn goto my_app # latest on the current branch
vmn goto -v 1.2.3 --deps-only my_app # dependencies only
vmn goto -v 5 --root my_platform # by root-app version
vmn goto -v 1.2.0-dev.a1b2c3d.e4f5g6h my_model # restore a dev snapshot
vmn goto -v 1.2.3 --pull my_app # fetch first if not found locally
Missing dependency repos are cloned automatically, in parallel. Restoring a dev version checks out the base commit, replays local commits, then applies the working-tree patch.
snapshot
Capture your exact working state — uncommitted changes, unpushed commits, untracked files, across every dependency — as a deterministic version you can restore. No WIP commits, no stash management.
Use it when: you're hours into a refactor that half-works and want to try a different approach without losing this one. Committing pollutes history with work you may discard;
git stashgives you an unnamed entry with no record of dependency state. A snapshot gives you a version string — try the other approach, and if it's worse, restore.Also useful for: sharing a bug that only reproduces with your local debug changes; saving your state before an agent edits the same files.
vmn snapshot create my_app --note "promising results"
# => 1.2.0-dev.a1b2c3d.e4f5g6h
vmn snapshot list my_app
vmn snapshot diff my_app -v 1.2.0-dev.a1b # second side defaults to your working tree
vmn snapshot restore my_app --latest # dirty work is auto-saved first
Snapshot vs. experiment — which do I want?
An experiment is a snapshot plus an append-only metrics log. Use a plain snapshot to save or restore code state; use an experiment when you want to track and compare runs.
vmn snapshot |
vmn exp |
|
|---|---|---|
| Captures code state (tracked + untracked + deps) | ✅ | ✅ |
| Metrics / params / notes | one note | ✅ append-only log |
| Run a command, record its outcome | ❌ | ✅ (exp run) |
| Compare across runs | diff only | diff + metric deltas + compare |
| Typical use | saving WIP before a risky change | tracking and comparing runs |
What's inside a snapshot
.vmn/{app}/snapshots/{version}/
metadata.yml # version, branch, timestamp, note, dirty states
working_tree.patch # uncommitted changes (git diff HEAD)
local_commits.patch # commits not yet pushed
untracked_files.tar.gz # untracked files
deps/{dep_name}/... # the same three, per dependency repo
artifacts/{filename} # attached files
Dependency state feeds into the content hash, so two snapshots differing only inside a dep get different version strings.
Note: unlike
vmn goto, snapshot restore does not clone a missing dependency — it warns and skips it. Deps are expected to be on disk already.
All snapshot flags
Actions: create (default), list, show, note, diff, export, restore. Version-taking actions default to the latest and accept a full version, a unique prefix, --latest, or @N.
| Flag | Description |
|---|---|
-v, --version |
Target a specific snapshot |
--latest |
Use the most recent snapshot |
--last N |
Show only the N most recent (for list) |
--note |
Attach or update a note |
--to |
Second version for diff (default: current, your working tree) |
--tool |
External diff tool; falls back to git config diff.tool |
-o, --output |
Export destination |
--meta / --meta-file |
Extra metadata, repeatable key=value or a YAML file |
--filter |
Filter list by key=value, repeatable |
--verbose |
Full ISO timestamps |
--backend / --bucket / --endpoint-url / --prefix |
local (default) or s3; S3 works with MinIO, Spaces, etc. |
gen
vmn gen -t version.j2 -o version.txt my_app
vmn gen -t version.j2 -o version.txt -c custom.yml my_app
Template variables: version, base_version, name, release_mode, prerelease, previous_version, stamped_on_branch, release_notes, changesets, root_name, root_version, root_services.
add
vmn add -v 0.0.1 --bm build42 my_app
vmn add -v 0.0.1 --bm build42 --vmp ./build.yml --vmu https://ci/build/42 my_app
Attaches build metadata to an existing tag (0.0.1+build42). --vmp records a path to a YAML metadata file; --vmu an associated URL.
config
vmn config # list all managed apps
vmn config my_app # interactive TUI
vmn config my_app --vim # open in $EDITOR
vmn config --branch my_app # override for the current branch
vmn config --root my_platform # root-app config
vmn config --global # repo-level .vmn/conf.yml
Non-interactive (config gen) — for CI and scripting
Creates a config file with no TTY. Never overwrites an existing one.
vmn config gen my_app # .vmn/my_app/conf.yml
vmn config gen --branch my_app # branch config, seeded from the effective conf
vmn config gen --branch --root my_platform # branch config for a root app
vmn config gen --branch --sync-dep-branches my_app # pin each branch-tracked dep to its current branch
--sync-dep-branches is only valid with config gen --branch.
skill
Emits vmn's own usage instructions for AI coding agents — see Built for AI-assisted development.
vmn skill --install # .claude/skills/vmn/SKILL.md (default)
vmn skill --install --target cursor # .cursorrules
vmn skill --install --target agents # AGENTS.md
vmn skill --install --methodology # + opinionated TDD / worktree rules
vmn skill --install --force # overwrite an existing Claude SKILL.md
vmn skill # just print it
--install resolves the managed repo root even from a nested directory. The Claude target refuses to overwrite an existing skill without --force; the Cursor and AGENTS targets rewrite only vmn's marker block and preserve the rest of the file. Full text: docs/agent-skill.md.
Using vmn as a Python library
from version_stamp.cli.entry import vmn_run
ret, ctx = vmn_run(["show", "my_app"])
vmn_run takes an argument list and returns (exit_code, context). It prints to stdout/stderr, so wrap calls in contextlib.redirect_stdout / redirect_stderr to capture output.
Environment variables
Read by vmn:
| Variable | Description |
|---|---|
VMN_WORKING_DIR |
Override the working directory |
VMN_LOCK_FILE_PATH |
Custom lock file path (default .vmn/vmn.lock) |
GITHUB_TOKEN / GH_TOKEN |
Required for GitHub Releases |
VMN_GIT_PUSH_USER / VMN_GIT_PUSH_TOKEN |
Fallbacks for the --git-push-* flags |
VMN_UI_TOKEN |
Fallback for vmn ui --token |
Set by vmn for the child process of vmn exp run:
| Variable | Description |
|---|---|
VMN_EXPERIMENT_ID |
The verstr of the running experiment |
VMN_APP_NAME |
The app name |
VMN_METRICS_FILE |
Path your command appends key=value metrics to |
⚙️ Configuration
vmn writes .vmn/<app>/conf.yml when an app is first stamped. Edit it directly or via vmn config.
Full conf.yml reference
conf:
template: '[{major}][.{minor}][.{patch}][.{hotfix}][-{prerelease}][.{rcn}][-dev.{dev_commit}.{dev_diff_hash}][+{buildmetadata}]'
hide_zero_hotfix: true
extra_info: false
create_snapshots: false
conventional_commits: true
default_release_mode: optional # "optional" (--orm) or "strict" (-r). Top-level, not nested.
changelog:
path: "CHANGELOG.md"
github_release:
draft: false
deps:
../:
other_repo:
vcs_type: git
version_backends:
npm:
path: "package.json"
policies:
whitelist_release_branches: ["main"]
snapshot_storage:
backend: local
bucket: my-bucket
prefix: vmn-snapshots
endpoint_url: https://...
experiment:
metrics:
loss: { goal: min, primary: true }
acc: { goal: max }
storage: # same shape as snapshot_storage; CLI flags override
backend: local
create_verinfo_fileswas renamed tocreate_snapshots. The old key still works but warns.
Auto-embedding the version into project files
vmn stamp can write the version straight into your project files:
| Backend | File | Field |
|---|---|---|
npm |
package.json |
version |
cargo |
Cargo.toml |
version |
poetry |
pyproject.toml |
[tool.poetry].version |
pep621 |
pyproject.toml |
[project].version |
version_backends:
npm:
path: "relative/path/to/package.json"
Regex find-and-replace in any file:
version_backends:
generic_selectors:
- paths_section:
- input_file_path: in.txt
output_file_path: in.txt
selectors_section:
- regex_selector: '(version: )(\d+\.\d+\.\d+)'
regex_sub: \1{{version}}
{{VMN_VERSION_REGEX}} matches any vmn version string (playground).
Jinja2 rendering:
version_backends:
generic_jinja:
- input_file_path: f1.jinja2
output_file_path: jinja_out.txt
custom_keys_path: custom.yml
Same variables as vmn gen.
Or skip file injection entirely — with hatch-vcs, read the version from the tag at build time:
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[tool.hatch.version]
source = "vcs"
tag-pattern = "my_app_(?P<version>.*)"
Per-branch configuration
A branch can override the app config. The canonical location:
.vmn/<app>/branch_conf/<branch>/conf.yml # slashes in the branch name
.vmn/<app>/branch_conf/<branch>/root_conf.yml # become real directories
Create one with vmn config --branch <app> or vmn config gen --branch <app> — both seed it from the currently effective config. vmn resolves a branch config first and falls back to conf.yml.
Two older layouts are still read: flat <branch-with-dashes>_conf.yml and nested <branch>/conf.yml beside conf.yml. Precedence is canonical > flat > legacy, and legacy files are auto-migrated to canonical on the next vmn stamp. Stale branch configs from other branches are cleaned up on stamp.
🧬 Experiments
Local-first experiment tracking for any versioned app. Experiments are plain files under .vmn/{app}/experiments/ — git-ignored, never committed or pushed. No server, no database, no account.
Use it when: you're tuning something over days — hyperparameters, a cache policy, a retrieval prompt, compiler flags — editing code between runs. Two weeks later, "what did we change to get 0.91?" is unanswerable, because the tree that produced it was overwritten twenty runs ago. An experiment pins each result to the exact code that produced it, so
exp diffshows the metric delta and the source change side by side.Also useful for: benchmark and load-test runs where the configuration is the variable; agent-driven iteration loops; any workflow where many attempts produce one keeper.
# Capture code state, run the command, record metrics + exit code + duration
vmn exp run my_model --note "baseline CNN" -- python train.py
# => 0.1.0-dev.a1b2c3d.e4f5g6h
# Change the model, run again — a distinct experiment, even on the same commit
vmn exp run my_model --note "with dropout" -- python train.py
# Leaderboard, best loss first
vmn exp list my_model --sort loss --top 3
# Metric delta AND a real source diff between two runs
vmn exp diff my_model
# Winner — restore that exact state (dirty work auto-saved first)
vmn exp restore my_model --latest
No training script required. An experiment is a tree snapshot plus a metrics log — ML training is one use case; config sweeps, benchmarks, and load tests work identically.
Your command reports metrics by appending key=value lines to $VMN_METRICS_FILE. Prefix a line with step=N to build a per-step series — vmn tails the file during the run, so training curves appear live in the web UI.
Subcommands at a glance
| Command | What it does |
|---|---|
exp run |
Capture state, run a command, record metrics + exit code + duration |
exp create |
Capture a snapshot with metrics/params/notes, no command |
exp add |
Append metrics, notes, or artifacts to an existing experiment |
exp list |
List experiments, filter and sort by any metric |
exp show |
Full detail for one experiment, including its log |
exp diff |
Metric delta + real source diff between two experiments |
exp compare |
Side-by-side metric table across N experiments |
exp restore |
Restore the exact code state (dirty work auto-saved) |
exp export |
Package an experiment as a directory or tarball |
exp prune |
Clean up by count (--keep N) or age (--older-than 30d) |
Version-taking actions default to the latest experiment and accept a full version, a unique prefix, --latest, or @N (the row index from exp list).
Re-running over an identical code state starts a new run (.r2, .r3, …) rather than overwriting — "same code, different seed" never replaces a previous run.
Metrics schema — teaching vmn which direction is better
experiment:
metrics:
loss: {goal: min, primary: true} # lower is better; default sort key
val_loss: {goal: min}
acc: {goal: max}
goal: min sorts best-first ascending, goal: max descending. primary: true sets the default sort for list and the web-UI leaderboard.
📖 Full experiments guide → — the metrics protocol, structured params, addressing, S3 storage, and the no-script workflow.
🖥️ Web UI
A dashboard over data you already have. It reads git tags and .vmn/ files directly; the whole SPA ships inside the wheel.
pip install "vmn[ui]"
vmn ui # http://127.0.0.1:8265, opens your browser
- Leaderboard — sortable, goal-aware metric columns;
@Nindices matching the CLI. - Run detail — params vs. metrics, live training curves, full log, copy-paste reproduce commands.
- Compare — pick two runs for a metric-delta table plus a color-coded code diff.
- Stamp tree — your version history as a DAG, colored by release mode, with root-app topology and cross-repo dependency pins. No experiment tracker has this — it falls out of vmn's git-tag model.
- Actions — run
stamp/restore/goto/release/prunefrom the browser. Each runs as a realvmnsubprocess, so it takes the repo lock correctly and streams its log live.
Team / remote deployment
vmn ui --host 0.0.0.0 --port 8265 \
--token "$VMN_UI_TOKEN" \
--data-dir /srv/vmn-ui \
--repo /srv/checkouts/model-a --repo /srv/checkouts/model-b
- Workspaces — one server hosts many isolated checkouts. Several can be clones of the same repo (one per branch or user); a stamp in one never touches another. S3 buckets register as read-only experiment sources with no local repo at all.
- Auth — a shared bearer token; put TLS and user management behind a reverse proxy.
--read-onlydisables every mutation endpoint.--data-dir(default~/.vmn-ui) holds the workspace registry and a derived SQLite cache that keeps leaderboards instant;--no-indexreads sources directly.
The whole /api/v1/... surface is documented at /api/docs.
🏝️ Islands (parallel worktrees)
An island is a set of git worktrees — your main repo plus every dependency — pinned to a known-good state, sitting beside your work instead of on top of it.
Use it when: you want three agents working on three features at once in a product that spans four repos. Doing that by hand means a
git worktree addper repo per feature, with every dependency checked out at the exact hash the last good version recorded — twelve checkouts, each re-derived from tag metadata. Onevmn worktrees createper feature does it, and agents in separate islands cannot touch each other's files.Also useful for: reproducing a customer bug on 2.1.0 while your current work stays where it is —
vmn gotowould move your checkout, an island provides that state alongside it. Or a disposable--no-stampisland for a risky experiment.
| Problem | Without islands | With vmn worktrees |
|---|---|---|
| Parallel features | git worktree + manually clone each dep at the right hash |
one command |
| Dependency alignment | vmn goto mutates your checkout |
islands are non-destructive copies |
| Agent isolation | agents collide in one tree | each agent gets its own island |
| Reproducibility | "works on my machine" | island.json records the exact state |
vmn worktrees create my_app # from current HEAD, auto-named
vmn worktrees my_app # 'create' is the default action
vmn worktrees create my_app --island-name feat-auth -fv 2.1.0 # from a version
vmn worktrees create my_app --island-name feat-perf -fb develop # from a branch
vmn worktrees create my_app --island-name ci-test --no-stamp # read-only: stamping disabled
vmn worktrees list
vmn worktrees remove feat-auth # removes worktrees and branches
Layout, branch model, and the manifest
../vmn-islands/ # configurable with --base-path
feat-auth/
my_project/ # main repo — git worktree on a new branch
auth_service/ # dep — git worktree, detached HEAD
payment_gateway/ # dep — git worktree, detached HEAD
island.json # machine-readable manifest
- Main repo gets a new branch
island/{name}/{original-branch}— ready for commits and PRs. - Dependencies are detached HEAD at the exact hash recorded when the source version was stamped. Detaching sidesteps git's rule that the same branch can't be checked out in two worktrees.
--editable-dep <name>gives that dep its ownisland/{name}/{dep-branch}for cross-repo work.
island.json records name, creation time, app, version, source ref, the main repo's path/branch/remote, and every dep's path/hash/branch/remote — so an agent can orient itself without being told the layout.
--shallow-deps is a fallback, not a speed knob: if a dependency repo is already on disk, vmn always makes a worktree from it. The flag only applies when a dep is missing locally, permitting a --depth 1 clone from its remote. Without it, a missing dep is a hard error.
Stamping inside an island
vmn stamp works inside islands by default. The version commit stays on the local island branch and vmn pushes only the tag, so it never assigns the island branch to origin/main or publishes the branch. --pull fetches remote version state without merging another branch into the island.
Use --no-stamp for islands meant for CI, testing, review, or agents that shouldn't create versions.
🔄 CI
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # required — vmn reads tags and history
- uses: progovoy/vmn-action@latest
with:
app-name: my_app
do-stamp: true
stamp-mode: patch
env:
GITHUB_TOKEN: ${{ github.token }}
fetch-depth: 0 is not optional — vmn computes the next version from git history and tags.
🔍 Troubleshooting
vmn can't find tags / reports the wrong version
Most CI systems shallow-clone by default. vmn needs full history:
- uses: actions/checkout@v4
with:
fetch-depth: 0
Or manually: git fetch --tags --unshallow
"Another vmn process is running" / lock file error
vmn takes a per-repo lock so concurrent stamps can't interleave. If a previous run crashed:
rm .vmn/vmn.lock # default location
# or, if VMN_LOCK_FILE_PATH is set:
rm "$VMN_LOCK_FILE_PATH"
Tag name collision
vmn tags are {app_name}_{version} (slashes in app names become -). If your repo already has tags matching that pattern, rename the app or clean up the conflicting tags before the first stamp.
"Dirty" state warnings on stamp
vmn refuses to stamp over uncommitted changes or unpushed commits. Commit or stash first — or capture the state with vmn snapshot create so you can return to it. vmn show --verbose prints the exact flags (pending, outgoing, detached).
App name rejected
App names cannot contain - or start with /. Use _, or / to express root-app topology (my_platform/auth).
🔀 Coming from another tool?
| From | Guide |
|---|---|
| semantic-release | migration guide |
| release-please | migration guide |
| setuptools-scm | migration guide |
| standard-version (archived 2023) | migration guide |
| bump2version | migration guide |
Your existing tags keep working — vmn uses its own {app}_{version} format and won't collide with v1.2.3-style tags.
Every version, restorable.
pip install vmn
If vmn is useful to you, consider starring the repo. Found a problem? File an issue.
Add the badge to your project:
[](https://github.com/progovoy/vmn)
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vmn-0.9.4-py3-none-any.whl.
File metadata
- Download URL: vmn-0.9.4-py3-none-any.whl
- Upload date:
- Size: 366.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
16c8a3a3d52c1ea55c7371a5e7945b461f5ee62abf0e92f4e2e54277fa479b88
|
|
| MD5 |
7d843cfd5ec4131e8fb5e44b1d4ca45d
|
|
| BLAKE2b-256 |
5bfdd212fad917c93caac02afe3c3c340adf1e8d35ea2c17e729b152488dd61a
|