onevcs
Version control and its remote host behind one host-neutral vocabulary, for agent workflows.
The review unit is a change request — GitHub maps it to a pull request, and a
later host maps it to whatever it calls the same thing. Vcs owns the repository
side (identities, sessions over an isolated worktree, preserved work);
RemoteHost owns the host side (opening a change, reading its checks, merging
it); a rules file decides, per repository, how a change publishes and what
verifies it. Everything a run does is emitted as an NDJSON event stream.
The public surface is the approved contract — docs/contract.md
— compiled, and it is implemented: the registry with its lazy migration, the rules
engine, sessions over borrowing clones, bounded git, the FIFO merge queue, both
publication strategies, recovery and provenance, the merge train, and the event
stream.
What one change looks like
onevcs register ~/projects/widgets # once per checkout
onevcs rules check widgets # which policy, and why
token=$(onevcs session open widgets --branch feature/thing | jq -r .token)
# …work in the worktree the session printed…
onevcs publish "$token" # verify, then land it
onevcs events "$token" # everything it did, as NDJSON
onevcs release status "$token" # …and whether a release carries it
onevcs rules check REPO is the second line of that, on its own: it resolves the
checkout to an identity, says which rule of the rules file matched it, and gives the
publication and approvals that rule decided with the source of each — and, beside them,
whether the draft lifecycle below is on, what a green change does under that policy, and
whether an early lift warns.
onevcs repos --audit-gates reads the same decision back for every registered
identity at once, and beside each one what actually verifies a publication there —
the host's required checks, and whether anything on the merge path runs a gate:
A session can also open its change request as a draft it holds while its work is still being made, and finish the description off once it has evidence to put in it:
onevcs publish "$token" --draft # open it as a draft the session holds
# …trigger a CI run, put up a demonstration change, gather what the reviewer needs…
onevcs change describe "$token" --body-file pr.md # replace the description with the evidence
onevcs change show "$token" --json # what the host holds: url, id, base, draft, title, body
onevcs change ready "$token" # lift the draft without landing anything
onevcs publish "$token" # …or publish it: its checks decide the lift
Each of those addresses the session's own change request — the one open from its
branch into its base — so nothing names a URL. The library forms are
session_change, describe_change, and ready_change.
Every change request starts as a draft while its required checks run. A plain
onevcs publish under change-auto, change-direct or change-open opens (or adopts)
the change request as a draft and watches the pushed commit's required checks. Green lifts
it to ready — and only then arms or performs the merge, since a host merges no draft —
except under change-open with approvals: required, where it stays a draft for its own
user's review and the publication ends change-review-draft. Red, or the bound, leaves it
a draft. A repository whose CI skips drafts is lifted after a grace window
(ONEVCS_DRAFT_CHECKS_GRACE_SECONDS) so its checks can run, with one warning line. Lifting is one-way, and drafts: {disabled: true} in the rules file opens
change requests ready, as before:
default: {publication: change-open, approvals: required, drafts: {warn_on_early_lift: false}}
A branch that outlived the session that cut it is landed by name instead, under
that same rules-resolved policy: onevcs publish-branch feature/thing --repo ~/projects/widgets for work that finished, and onevcs recover for a step that
stopped half way, which publishes it with the attestation that verification
cleared it. Whichever of the three refuses a branch names the one that takes it.
A branch that is not ready to land and must not be lost is preserved instead:
onevcs preserve feature/thing --repo ~/projects/widgets puts it on that
identity's origin under its own name and publishes nothing — no change request, no
merge path, no base touched, nothing forced, and no provenance marker cleared. It
is what a host with running work reaches for before it shuts down, so the commits
its dispatches just made survive the machine going away; the branch is exactly as
recoverable afterwards as it was before, and onevcs recoverable still names the
verb that lands it, with the origin beside it. The library form is preserve,
answering Pushed, AlreadyOnOrigin, or NoRemote, and recoverable is the
enumeration beside it.
A change that depends on several changes still in review is drafted against a stack
base: onevcs stack-base widgets --branch stack-base/plan/node --root main --parent feature/a --parent feature/c cuts the branch from main, merges each parent into it in
that order, and pushes it with no change request. Run again, it merges the root's new
head and each parent's new head forward — never a rebase, never a force — so every
draft based on it can merge it in turn; a conflict is left in a session on the branch
(exit 3) for whoever resolves it, and the same command then finishes the job. The
library form is assemble_stack_base.
A branch that provably holds nothing beyond its base is retired — deleted from
every checkout, pool slot clone, run clone and the origin that holds it, with a
branch-retired record left saying why — and that happens on its own: session close retires the branch of a session whose landing is recorded, onevcs sweep
retires every such branch on the host, and onevcs retire-finished is the same pass
by name. What counts as proof is a merged change request whose head carries the
branch's work, a recorded landing followed by nothing but commits that change no
content, or every path the branch changed reading the same on the base. onevcs retire BRANCH asks for one by name and refuses anything else, printing why it is
kept; a pool slot is returned rather than removed. A branch a retry superseded,
recorded with onevcs supersede, that still differs from the base is listed by
recoverable with a Reclaim: line and only onevcs reclaim removes it.
onevcs status REF answers what became of a piece of work, asked by whichever
name you hold — a change request's URL, a session token, a branch, or a commit. It
reports the identity's resolved policy, the session, every checkout and per-run
clone holding the branch, whether the change landed and what says so, the
host's checks, the last thing its merge path said about it, and the command that
advances it. Landing is
decided from the base's own history — a recorded landing, the change request's
number in the base's log, or a landing trailer — so a change that squash-merged
reads as landed rather than as unpublished however far the base has moved since,
and one that history cannot decide reads as unknown rather than as work nobody
published. A host that cannot be reached leaves its section unavailable instead of
failing the command.
A branch is often worked on by more than one session — a run stops and the next
one continues the name — so the older session's record names the one that
continued it, and every answer about a session or a branch follows that chain to
its newest record before it reports a landing. A copy of the branch that was
superseded is still reported as holding it and no longer decides anything about
it. A chain this host cannot follow — a session record removed underneath one, a
link into another repository, a cycle — reports unknown and says why, rather
than falling back to whichever record still read: a wrong no there reads as an
instruction to publish work the base already carries.
onevcs release answers what happens after a change lands, so an upgrade can
be sequenced behind the release that carries it rather than behind the merge.
onevcs release targets REPO lists what a repository releases and whether it
adopts fast (the work is enough) or published (the release is what is depended
on); onevcs release latest REPO [--target NAME] says what is out right now; and
onevcs release status REF [--target NAME] says whether the release carrying one
landed change has happened yet, asked by the same four names onevcs status takes —
released: crate 1.5.0 (automated, probed), not released, or not answered.
A target's style decides its shape. An automated target carries a probe — a
script the repository carries, or a one-liner run through sh — and is answered by
running it under a bounded timeout; a human-step target carries no probe at all,
because the release happens when a person does something, and is answered by
onevcs release acknowledge REF --target NAME --version VERSION after they have
done it. Recording the same version again is a no-op; a different one is refused
until --supersede replaces it, which keeps the version it replaced.
Two answers stay apart everywhere, and a consumer routes on the difference: "not
released" is a probe that answered, and "not answered" is a probe that did
not — a timeout, a non-zero exit, or output that is not one usable version. A
landing whose probe never answered is "not answered" for ever rather than being
compared against a reading taken later, because the release carrying that very
change may already be in it. Configure targets in $ONEVCS_HOME/releases.yml; a
host with none behaves exactly as it did before there was one.
onevcs import BRANCH --repo PATH [--from SOURCE] [--as NAME] makes a branch
reachable from an identity's registered checkouts, so a later run's clone can see
work a stopped run left in its own. It writes refs and nothing else — no checkout,
no working tree — and refuses a non-fast-forward overwrite by naming the commits
it would lose.
onevcs session open places a session on a pool of warm worktree slots when the
host keeps one for the repository — $ONEVCS_HOME/workspaces.yml, per identity,
sizes the pool and the overflow past it, and names the paths to delete on
every return. An idle slot is taken first, one is cut while the pool is under its
size, a session past the pool is placed under runs/ exactly as before until the
overflow is spent, and then session open refuses with exit code 4 rather than
waiting. Closing a session on a slot returns it — detached onto the base, reset,
cleaned of untracked files but not of ignored ones — so the next session on it finds
target/, node_modules/ or .venv/ still there. onevcs pool status REPO [--json] reports the capacity and every slot; onevcs pool prune REPO [--json]
removes the idle ones whose clone retains no branch. A host that writes no file keeps
every session fresh, as it always has; --pool N and --overflow N|unlimited on one
open override its resolution for that session alone.
A warm slot that is never swept grows without bound, so the workspaces file may also
name what maintenance is per identity — maintain: {command: ["cargo", "sweep", "--time", "7"], timeout: 30m} — and onevcs pool maintain [REPO] [--older-than SPAN] [--json] runs it in each idle slot's worktree, one slot at a time, claiming the slot
so a session open goes elsewhere rather than waiting, and records the attempt on
the slot. onevcs holds no schedule: when to maintain is the caller's — a driver's
idle tick, a cron, a person — and --older-than is what makes every caller cheap, by
skipping a slot maintained within that span. It exits 0 when nothing ran or every
command succeeded, 1 when any command failed or timed out, naming the artifact that
holds what it wrote.
onevcs sweep [--dry-run] [--min-age-hours HOURS] [--format text|json] reclaims the workspaces those
landings leave behind. Every branch published by name cuts a run root — a clone, a
worktree, and the merge path's preserved logs — under the state root, and until this rule
existed nothing removed one; thirty-one of them filled a host's disk twice in a
single run. The rule runs whether or not anybody asks: each landing enforces it
over its own family before cutting the next run root, and this verb is the same
judgement asked deliberately and over every family at once.
A workspace is removed only where this tool can prove it is finished: its merge
path recorded a verdict, no live session holds its occupancy lease, it was last written
outside the age floor --min-age-hours sets, and removing it is something this host
can do at all. Everything else is retained and reported with the reason — a
workspace somebody is still publishing in is never removed, and neither is one
belonging to another manager on a shared state root — and the per-run lifecycle
clones a session keeps as recovery history, the pool of warm slots beside them, and
the unpublished branches sessions left behind are outside it entirely, each named in
the report with the verb that reaches it. --format json writes the same report as
one object a consumer reads by field name.
Two things a proven-finished workspace still gets to keep. Its preserved logs last at least as long as the age floor, because they are what an operator reads after a publication failed; and a clone still holding work that never reached the origin is kept past the floor too, until enough newer ones stand in front of it — under the same bound the per-run lifecycle clones a session keeps are kept under. Reclaiming a workspace also stops the processes that publication left running — a publication runs the repository's own verification and verifications start daemons, and unlinking files a live process holds open frees none of their blocks.
A landing releases its own build output as it ends, so most of that never waits
for the floor at all. Completed, refused by its merge path, or failed, publish-branch
and recover remove the clone and the worktree they built in and stop what is still
running there before they return — by the same proofs, and while the branch still reads
as held by the publication. What stays is the evidence: the preserved logs, at the path
every push event names, which the sweep then reaps once they are past the floor. A
workspace one of those proofs does not cover — somebody else inside it, or a clone
holding work that neither an origin nor the checkout the branch was read out of
carries — is kept whole, and the verb says why on stderr.
Private work may inform public repositories, and public output never names it.
Every write to a public destination — publish, publish-branch, recover,
preserve, integrate --push, change describe — is checked before it reaches the
remote: its added lines, new paths, every outgoing commit's message against every
parent, the branch, and the change request's title and body are matched against the
terms of the private repositories this host has registered, and a hit refuses the
write, naming only where it was found. A term is a private repository's
owner/name (anywhere, which covers its URLs), its name and the package names its
committed manifests declare (whole words — a common word, or a public repository's
name, narrows to owner/name), and its owner where that owner has no public
repository; a repository may commit a private-terms.toml adding terms and narrowing
or dropping derived ones. A cleanup may remove what its destination already carries
and nothing is exempt from the rest. Which repositories are public is GitHub's answer,
recorded in the registry and refreshed at every write, or a rule's visibility: —
unknown is private, and a hosted destination whose visibility cannot be confirmed is
screened rather than written to unchecked. --term-scope IDENTITY (repeatable) narrows the check to the
repositories a piece of work names, and --term-scope-empty to none.
onevcs export --from REPO --branch REF --directory DIR --to REPO --target-directory DIR --branch-name NAME turns one directory of private work into one new commit on a
public repository's base — fixed neutral subject and author, nothing of the source's
history, names or paths — on a local branch, after refusing anything outside that
directory, a link, a submodule, a binary blob, or a private term. It pushes nothing.
onevcs boundary check --destination public --input - puts any caller's output to the
same check (exit 0, 1 refused, 2 unavailable), onevcs boundary inspect --input - --json answers a repository's visibility, and onevcs boundary schema --json prints
both commands' versioned schemas. onevcs rules apply --base FILE --overlay FILE...
composes a tracked rules file with overlays kept outside the checkout and installs the
result where the registry reads rules.
Everything durable lives under one state root — ONEVCS_HOME, otherwise
~/.onevcs.
The library forms of the pool are workspace_capacity(&request), which says how many
more opens an identity admits and whether one request would be placed now,
pool_status, pool_prune and pool_maintain(scope, older_than); Span is the one
duration grammar of the host files (7d, 36h, 90m, 600s), and first_matching
matches a RuleMatch list against a repository by exactly the matcher the three host
files use.
GitHub is reached through gh, so whatever gh auth status reports is the
credential. A fine-grained personal access token needs Actions: Read on the
repository beside its contents and pull-requests access: GitHub will not let that
credential class resolve a check run at all — there is no Checks permission to
grant one — so a change request's checks are read from its workflow runs, and
change_checks says so in the sources it answers with. Anything a third-party
integration posted as a check run or a commit status is invisible to such a token,
and a credential that can read neither source is refused rather than reported as
having no checks.
Install
cargo install onevcs # crates.io
pip install onevcs-cli # a prebuilt wheel, no Rust toolchain
npm install -g onevcs-cli # a prebuilt binary, no Rust toolchain
All three install the same onevcs binary. Prebuilt binaries exist for Linux
(x64, arm64), macOS (x64, arm64), and Windows (x64); every release also attaches
the archives and their .sha256 checksums for a direct download.
Use
onevcs --help
--help is the command surface, and publish reserves its own exit codes for a
verification that failed, a request that was invalid, and a base that moved under
it.
Every picture in this README is a real capture of this CLI: the release binary driven against a scratch host of real origins, clones and hooks, with no network and no credential, rendered by
just screenshots. The digest of each still is committed, so CI refuses the moment the output it shows stops matching what the binary prints; the animation at the top is a rendering of one such run's own output, regenerated withjust screenshots-gif.
Embed it
A command answers a process: an exit code and a line of prose. A caller embedding the crate wants the decision, so the same operations answer values.
let published = onevcs::publish(&providers, &token, &PublishRequest::default())?;
match published.outcome {
PublishOutcome::Merged(sha) => journal.landed(sha),
PublishOutcome::ChangeOpen(url) | PublishOutcome::Queued(url) => journal.awaiting(url),
// Green, and kept a draft for the person who dispatched the work to review.
PublishOutcome::ChangeReviewDraft(url) => journal.for_review(url),
// A draft cannot land while it stands; `ready_change` lifts it, and so does a
// publication with no `draft` once its checks are green. Whether it awaits a
// dependency's release or is held by the session still making it is the
// `DraftReason` the request carried.
PublishOutcome::ChangeDraft(url) => journal.held_back(url),
PublishOutcome::NothingToPublish => journal.nothing(),
PublishOutcome::Failed { kind, reason, retained } => journal.failed(kind, reason, retained),
}
close_session and session are the same for the rest of a session's life, and
EventStream::open(&token) reads its events as Envelopes, each attributed to
the session that wrote it — so a caller following several publications at once can
tell them apart. The command line is a rendering of these rather than a second
path through them.
A consumer that wants some of what a session writes reads it through the filter
grammar the three producing libraries share — EventStream::open_filtered(&token, filter), or onevcs events TOKEN --filter SPEC with the spec inline as JSON or in
a file. An envelope passes when it matches any include matcher (or include is
absent) and no exclude matcher, matching source by family, phase and the
reserved label keys exactly, and kind by glob (change-*):
include:
- {source: vcs, kind: "gate-*"}
exclude:
- {kind: lock-wait}
Every envelope carries the phase of a change's life its producer stamped it
with — development (the work being made, including a push of the session's own
branch), integrate (the merge queue, the merge, a sync conflict, and a push of
any other branch), review (the change request opened, checked, and merged), and
release (a probe, an acknowledgement, an observation). review is also where a
draft is opened, described, and lifted. Naming a phase is how a
consumer asks for "the review of this change" without listing the kinds in it, so
a kind added to that phase later arrives in the read that already wanted it.
EventStream takes the phases the session can actually produce: development and
integrate always, review only where the resolved merge policy is not
local-direct, and release only where $ONEVCS_HOME/releases.yml configures
targets for that repository. Naming a phase a session does not have is refused by
name, because a filter answered with silence and a session that did nothing look
alike; naming none takes what there is. And where release is one of them, the
read also hands back that repository's release-observed and
release-acknowledged events whose landing commit is this session's own — the
correlation onevcs can already make, so a consumer never has to find the
repository's release stream. That set grows after the session closes, because a
release happens when it happens.
Before a caller has a token, session_holders(repo) answers who is in a
repository: one SessionHolder per recorded session, carrying the token the calls
above take, the branch and worktree it holds, and whether its owner is still
running (Liveness::Live) or the session is the remains of a run that stopped.
Test against it, without a real GitHub
Embedding the crate, run_with(&cli, providers) takes the two implementations a
run reaches Vcs and RemoteHost through; run is that with Git and GitHub,
and every entry point above goes through the same seam.
onevcs-testing ships in-memory and file-backed
implementations of both, so a consumer's suite drives a real onevcs through a
real journey against a host it seeded:
cargo add --dev onevcs-testing
They emit the events the real implementations emit — a claim this repository's own suite checks by running one publication journey on both backends and holding the two event streams to each other.
Develop
just bootstrap # from a clean clone
just check # the deterministic gate: format, clippy, tests, coverage, docs
just gate # check, plus the diff-scoped LLM-judge tier — the pre-push bar
just --list is the full index.
License
MIT. See LICENSE.
Metadata
Release files for onevcs-cli 0.46.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| onevcs_cli-0.46.0-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details |
| onevcs_cli-0.46.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | Python 3 | none | Linux glibc 2.17+ x86-64 | Details |
| onevcs_cli-0.46.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | Python 3 | none | Linux glibc 2.17+ ARM64 | Details |
| onevcs_cli-0.46.0-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details |
| onevcs_cli-0.46.0-py3-none-macosx_10_12_x86_64.whl | Python 3 | none | macOS 10.12+ x86-64 | Details |
Total release size: 20.1 MB
Release files / onevcs_cli-0.46.0-py3-none-win_amd64.whl
| Download URL | onevcs_cli-0.46.0-py3-none-win_amd64.whl |
|---|---|
| Size | 4.3 MB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
7ca8a635c571381397d807569add610a59105ea073ca4b39ffffb29e12a67e94
|
|
BLAKE2b-256 checksum How to use checksums |
cad8c350ee43db2a8a04b1f088cdac13a2d118d8b90d333afe8561ce87018bed
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / onevcs_cli-0.46.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | onevcs_cli-0.46.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 4.1 MB |
| Tags | Linux glibc 2.17+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
9212d8a47617297b1ebb4a2edfc2749a93e25c883dbef1bc74dca86eca537927
|
|
BLAKE2b-256 checksum How to use checksums |
84fb86619dd0eb132a1b3f95a7cbce9bf5a59f54ab2f18e88d995da3db434de8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / onevcs_cli-0.46.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | onevcs_cli-0.46.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 3.9 MB |
| Tags | Linux glibc 2.17+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
24dcea24923d68d62dce92b716a1753939d37408cee0771c1792da5e35713bf0
|
|
BLAKE2b-256 checksum How to use checksums |
e5c57b89795ea9119f30002bfd991042d5f6bfcf9da50ac6fed8bda806c53c54
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / onevcs_cli-0.46.0-py3-none-macosx_11_0_arm64.whl
| Download URL | onevcs_cli-0.46.0-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 3.8 MB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
69014ee53c8a33cc406974b703c0e10ef2a90f66cde2c2dd37cf7b6600b3551b
|
|
BLAKE2b-256 checksum How to use checksums |
91222971d9020047413812c18b77e5b2a1ea547bccc48159689dfe10131f3c4f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / onevcs_cli-0.46.0-py3-none-macosx_10_12_x86_64.whl
| Download URL | onevcs_cli-0.46.0-py3-none-macosx_10_12_x86_64.whl |
|---|---|
| Size | 4.0 MB |
| Tags | Python 3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
60b6d829ebb52e96af63ed23580a41c423eb4e3d612e683aeb2c044fdf8edc3d
|
|
BLAKE2b-256 checksum How to use checksums |
c0a28dad4dd36eb4bac92c0d8b53974b2525cb702bac1b0148d6be92de609e14
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|