Skip to main content

vand

Vand materializes and pins external source instances, delegates lifecycle actions to their native tools, and records provenance independently of the source backend.

Works with any git remote — GitHub, GitLab, Bitbucket, self-hosted, file:// paths, and local folder clones — not just github.com. Git is the first source driver; the protocol is source-agnostic (source.yml manifests + origins.lock provenance ledger).

Manual CLI only — no background scheduler. For general cron/timer automation, a separate desktop scheduler project is planned later.

Why not submodules / myrepos / hawser?

Tool What it does Gap
git submodules Pins repos inside one repo Detached HEADs, nested checkout pain
myrepos (mr update) Update many repos No exact SHA lockfile for replication
hawser (haw sync) Multi-repo lockfile + verify Rust stack, heavier scope
Repo Family Control repo + lock No personal catalog curation
vand Catalog + origins.lock + replicate Small Python stdlib CLI

Per-app library pins inside one project are separate. vand pins whole source instances across a clone root on your machine.

Three artifacts

Artifact Location Job
Manifest source.yml inside each source tree Quotient map: install / update / verify / deinstall → shell commands
Provenance ledger origins.lock clone root Completed source facts: origin + pinned revision + target path (no hooks)
Execution log ~/.vand/logs/ Attempts, timestamps, exit codes — never in source trees

Do not confuse lock and log. A failed install must not produce a success-like ledger entry.

Install

Python 3.9+, Git, and pip. From a clone:

python -m pip install -e .
vand init --root <clone-root>

Or from a fresh clone, skip the separate pip — init runs pip install -e itself:

python vand.py init --root <clone-root>

That puts vand on PATH (Windows: the Python Scripts folder). Editable (-e) keeps the command pointed at this checkout, so self-update / git pull still work.

Without installing, you can still run python vand.py or vand.cmd from this directory.

Coding agents: follow AGENTS.md. That file is the install spec. This README is the map. First-run is python vand.py init --root <clone-root> (catalog, pip -e, skills, adopt self, examples/origins.lock). Usage skill: skills/vand/SKILL.md.

Help that machines can read

Human --help is a wall of text. Agents and other tools should not scrape it.

All machine-readable help is generated from argparse at runtime — there is no committed man page or JSON spec in the repo. If you add a flag or command, --help-json and man update automatically.

vand --help              # people
vand --help-json         # full spec: commands, flags, args, defaults
vand --help-json replicate
vand help --json
vand help replicate --json
vand man                 # roff on stdout (generated; same source as --help-json)
vand man --write FILE    # optional local copy, e.g. man/vand.1 for groff

On Unix: vand man | groff -man -Tutf8 | less. Optional: vand man --write man/vand.1 then MANPATH=man man vand.

Do not commit man/ or in-repo .cursor/ skill copies — both are gitignored local output. Source of truth: vand.py (argparse) and skills/vand/SKILL.md.

Quick start

# 1. First-run (catalog + PATH + skills + adopt this clone + origins.lock)
vand init --root <clone-root>

# 2. See existing clones not yet tracked
vand scan

# 3. Register an existing folder (reads source.yml if present)
vand adopt <folder>
vand adopt <folder> --install "make install"  # optional override

# 4. Check state
vand status
vand status --fetch

# 5. Export shareable ledger + human provenance log
vand export
# -> <clone-root>/origins.lock
# -> <clone-root>/VAND.md

<clone-root> is whatever directory you keep checkouts in. Catalog state lives in ~/.vand/ on this machine only.

Shared stack (Lolaplex)

This repo publishes a starter lock for the tools we share. It does not pin vand itself (the lock lives in this repo).

cd <clone-root>
git clone https://github.com/Lolaplex/vand.git
python vand/vand.py init --root .

init creates the catalog, pip install -e this clone, installs the agent skill, adopts vand, replicates examples/origins.lock, and adopts those repos. If you run init inside the vand folder with default --root ., the clone root becomes the parent.

Daily sync (pull when the other person pushed):

vand update agent-memory
vand push agent-memory   # after your own commits

update fast-forwards a clean tree and re-runs install hooks when the commit changes. Dirty or diverged trees are left alone — consolidate if you both edited.

On another machine

Copy origins.lock (and optionally VAND.md), then:

vand replicate origins.lock --root <clone-root>

If you omit --root, clones land next to the lockfile. Absolute root values from another computer are ignored.

Clones missing targets, checks out exact revisions, runs each repo's install hook from its manifest.

Dry run first:

vand replicate origins.lock --root <clone-root> --dry-run

Daily workflow

vand update          # fetch + fast-forward clean repos; re-run install on commit change
vand consolidate     # fetch + merge when ff-only fails; lists conflicts to fix
vand consolidate --continue NAME   # after fixing conflict markers
vand consolidate --abort NAME      # abort stuck merge/rebase
vand push            # push tracking branches; re-pin HEAD
vand pin             # pin catalog to current HEAD after local commits
vand pin --export    # pin + write origins.lock
vand hook-sync       # once: install git hooks so plain git keeps catalog pinned
vand verify          # exit 1 if any clone != ledger (CI gate); runs verify hooks
vand deinstall NAME  # remove from catalog + purge target (default)

Dirty or diverged repos are never force-reset. Use consolidate when update stops at diverged/ff-only failures, then pin.

Lock-step with plain git pull / git push

update, push, pin, and consolidate write HEAD into ~/.vand/catalog.json. Your Agent, GitHub Desktop, and raw git do not.

Two different "hooks"

Command What it syncs Where
sync-hooks Manifest install/update shell commands from each repo's source.yml into the catalog catalog.json fields
hook-sync Git hooks that re-pin the catalog after commit / pull / rebase / checkout <clone>/.git/hooks/

Do not confuse them. sync-hooks does not install pin hooks. hook-sync does not read manifests.

Pin hooks (hook-sync)

vand hook-sync                 # all catalog repos
vand hook-sync agent-memory    # one repo
vand pin --here                # pin the catalog row for cwd (manual test)
vand pin --here --quiet        # same, used inside git hooks

adopt and add run hook-sync on the new clone. Existing catalog entries: run hook-sync once after upgrading vand.

Git hook When it runs
post-commit Local commit
post-merge git pull that fast-forwards or merges
post-rewrite Rebase / amend
post-checkout Branch switch, only when HEAD actually changed

Each hook runs vand pin --here --quiet. That looks up the catalog row by this clone's path and sets commit (and branch) to HEAD. Failures append to ~/.vand/logs/hook-pin.log and never fail the git command (|| true).

Prerequisites: clone must be in the catalog (adopt / add / init). vand must be on PATH (or the hook falls back to py -3 / python + this checkout's vand.py).

git push does not move HEAD. Commit/pull already pinned the SHA; status --fetch is enough to see whether origin is caught up.

Do not set global core.hooksPath (Git replaces per-repo hooks instead of chaining). Do not alias git. Do not auto-export origins.lock from hooks (pin --export / export stay explicit). Foreign hook files are left alone unless --force (appends the pin block after the existing script).

desktop-commander is the later scheduler clock (vand update at 09:00). Pin hooks are the residual patch for ad-hoc git in the working tree.

Remote URLs

Form Example
GitHub shorthand owner/repo
HTTPS / SSH https://gitlab.com/group/project.git, git@host:org/repo.git
Local path a folder on disk, or file:// URL

adopt reads origin (push target). Extra remotes are stored as mirrors only if they already contain the pinned SHA. origins.lock stores source origin + pinned revision + target. Catalog may also cache hooks and git mirrors for fetch. Read aliases: vendor.lock, vand.lock, shared.lock. v1 locks are not loaded — re-export after upgrade.

Pins are SHAs. Klix927/agent-memory and Lolaplex/agent-memory are different remotes. They are fetch sources for a pin only when that exact commit exists there. replicate / install fetch url plus listed mirrors until the lock SHA is present, then check out that SHA. update / push still follow origin. A GitHub repo name match is not identity.

For repo authors — source.yml

Stop writing install guides only in README. Add a machine-readable manifest at the repo root (version: 1 required):

# source.yml
version: 1
install: npm ci && npm run build
update: npm ci
verify: npm test
deinstall: optional prelude before vand deinstall purge

Also read via aliases: vand.yml, vand.yaml, vend.ini, json variants.

Key When it runs
install After add, install, replicate
update After update / consolidate when the revision changed (defaults to install)
verify vand verify
deinstall Optional prelude before vand deinstall purge

Commands can be a string, a list (run in sequence with &&), or { run: scripts/setup.sh, shell: bash }.

If no manifest exists, vand tries conservative heuristics (Makefile install, package.json, requirements.txt, composer.json, go.mod, uv.lock).

vand scan          # shows [manifest file] next to repos that declare hooks
vand sync-hooks    # refresh catalog from on-disk manifests

The manifest is in the repo — it travels with the code and works on every machine after replicate. Hooks are not stored in origins.lock.

See source.schema.json for the JSON shape.

Where files live

File Purpose
~/.vand/catalog.json Your curated repo list (private to this machine, includes local clone root)
~/.vand/logs/ Timestamped logs from update/install/replicate
~/.vand/logs/hook-pin.log Quiet pin failures from git hooks
<clone>/.git/hooks/ Pin hooks installed by hook-sync (not in the clone's tree)
<clone-root>/origins.lock Shareable provenance ledger (relative targets only)
<clone-root>/VAND.md Human-readable provenance table
<repo>/source.yml Quotient manifest (install/update/verify/deinstall)

Commands

Command Description
init [--root PATH] First-run: catalog, pip -e, skills, adopt self, replicate examples/origins.lock
scan Git folders under root not in catalog
add owner/repo [--install CMD] Clone + register
adopt FOLDER [--install CMD] Register existing clone (auto-reads manifest; runs hook-sync)

Most commands take one or more repo names inline: vand update agents-docs agents-memory vand. With no names they apply to the whole catalog. | sync-hooks [NAME...] | Refresh catalog install/update commands from repo manifests (not git hooks) | | rm NAME | Remove from catalog (keeps folder; same as deinstall --keep) | | deinstall NAME [--keep] | Remove from catalog; default purges target directory | | status [NAME...] [--fetch] | pinned / behind / ahead / dirty / diverged / missing | | update [NAME...] | Fetch; ff-only if clean | | consolidate [NAME...] [--rebase] | Merge/rebase when update cannot ff-only; rebase refused when it would rewrite local history across upstream commits | | consolidate --continue [NAME...] | Finish merge/rebase after fixing conflicts | | consolidate --abort [NAME...] | Abort in-progress merge/rebase | | push [NAME...] | Push branch; refuses when diverged from origin (reconcile with consolidate first) | | pin [NAME...] [--export] | Pin named repo (or all) to HEAD | | pin --here [--quiet] | Pin catalog row for cwd; --quiet for git hooks (logs on failure) | | hook-sync [NAME...] [--force] | Install git pin hooks in .git/hooks/ | | install [NAME...] | Clone missing + checkout pin + install hooks | | export [--out PATH] | Write origins.lock + VAND.md | | replicate LOCK [--root PATH] [--dry-run] | Bootstrap from ledger | | verify [--lock PATH] | Drift check + verify hooks | | self-check [--fetch] [--json] | Check if vand itself is up to date | | self-update | Fast-forward this checkout (only refs that are descendants of HEAD) + reinstall | | install-skills | Copy agent skill to ~/.cursor/skills and ~/.agents/skills | | help [CMD] [--json] | Human or JSON help (JSON generated from argparse) | | man [--write FILE] | Print / write roff man page (generated from argparse; not in repo) |

Global: --no-self-check skips the 24h residual self-update. --help-json prints the CLI spec and exits.

origins.lock format (v2)

Portable provenance only — no hooks, no branch. target is relative to the clone root you pass to replicate.

{
  "version": 2,
  "sources": [
    {
      "name": "example-app",
      "source": {
        "kind": "vcs",
        "scheme": "git",
        "origin": "https://github.com/acme/example-app.git",
        "revision": "b6dfd52…"
      },
      "target": "example-app"
    }
  ]
}

Automation (future)

Scheduled vand update belongs in a separate desktop scheduler project (general cron/timer for any command). vand v1 is intentionally manual with visible terminal output and logs under ~/.vand/logs/.

Example future job:

- name: sync-repos
  command: vand update && vand export
  cwd: /path/to/vand
  schedule: "0 9 * * *"

Tests

python -m unittest discover -s tests -v

Self-update

vand keeps itself current from this checkout's remotes (personal origin + org mirror both count, same as other repos):

vand self-check              # uses 24h cache when offline-friendly
vand self-check --fetch        # force fresh compare
vand self-update               # ff-only + `python -m pip install -e .`

After other commands, vand observes remotes at most once per 24h. If that observe shows a clean fast-forward residual, it patches (ff-only + reinstall). Within the TTL it only prints a hint from cache. Dirty or diverged trees are left alone. Disable with --no-self-check or VAND_SKIP_SELF_CHECK=1. self-update always observes and patches.

Detection order:

  1. If vand.py lives in a git clone: compare HEAD to configured remotes. Catalog update follows origin. self-update fast-forwards a remote tip only when HEAD is an ancestor of that tip.
  2. Else if origin is a GitHub owner/repo URL: GitHub API
  3. Otherwise: unknown (no hardcoded upstream)

License

MIT

Metadata

Release files for vand 1.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for vand 1.0.1
File Size Uploaded
vand-1.0.1.tar.gz 47.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for vand 1.0.1
File Interpreter ABI Platform
vand-1.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 81.7 kB

Release files / vand-1.0.1.tar.gz

Download URL vand-1.0.1.tar.gz
Size 47.8 kB
Tags Source
SHA-256 checksum
How to use checksums
1395f3543d7c1618b97ad2acecb739178c0e28cb81d32dc54b5037012a6cf61b
BLAKE2b-256 checksum
How to use checksums
a79cf91c7b59d53cf625db43c1828bbc927d1ad66be59b2df03a3312ff13cafa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.9

Release files / vand-1.0.1-py3-none-any.whl

Download URL vand-1.0.1-py3-none-any.whl
Size 33.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
68a2bcc7ed54a43ff159242cd141d4b5e0719e3165f05d6854661c6b675b4d47
BLAKE2b-256 checksum
How to use checksums
b317b2f481a67bf7bb4d10541f0da640ec2faa93007a3f31c1231a83f05bf45f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.9

Release history Release notifications | RSS feed

This release

1.0.1 This release

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page