basis is a small constellation build system, written in Rust. A
constellation is a group of related repositories. You build, clean, and
version these repositories together.
basis can do these tasks:
- Build a constellation. One YAML manifest controls all repositories.
Each repository has a command list for each action (
build,clean, or a custom action). - Track versions. basis reads the version of a Rust repository from
Cargo.toml. basis reads the version of a C++ repository from a.versionfile and/orCMakeLists.txt. - Synchronize versions. basis sets all repositories to one common version.
- Bump one component. basis increases the version of one repository. Then basis writes the new version into each repository that depends on it.
- Verify identity. basis makes sure that the
git config user.emailof each repository is on a permitted domain. basis does the same check for the e-mail of the GPG signing key. - Report status.
basis statusshows the git state and the version state of the full constellation.
cargo install --path .
# or run from the workspace:
cargo run -- <args>basis install prepares a full constellation from its manifest repository:
basis install acme/platform # -> https://github.com/acme/platform
basis install git@github.com:acme/platform.git --into platform --branch mainThe command does these steps:
- basis clones the manifest repository into a directory. The default
directory name is the repository name. Use
--into DIRto set a different name. - basis reads
basis.yamlfrom that directory. - basis clones each member repository that has a
url:into itspath, adjacent to the manifest:
platform/
basis.yaml # from acme/platform
core/ # cloned from its url:
engine/ # cloned from its url:
org/repo is a short form of a GitHub HTTPS URL. A full git URL (https://…,
git@…, file://…) is also permitted. basis does not change members that are
present. basis reports members that have no url:. After the installation, go
into the constellation directory. Then run basis status or basis build.
You can run basis install without an argument from a directory in an
existing constellation. (basis finds the manifest in a parent directory. All
commands do this search.) Then basis clones only the members that are missing.
Use this after you add a repository to the manifest, or to complete a partial
checkout. basis does not change members that are present.
basis install clones only the manifest repository. But a constellation
frequently also needs loose files: helper scripts for a _postclone hook, or
shared configuration files. Declare these files in the manifest. Then basis
downloads them explicitly, and the constellation does not depend on a full
clone of the manifest repository:
files:
- path: gen-dbg.sh
url: https://raw.githubusercontent.com/acme/constellation/main/gen-dbg.sh
executable: truebasis downloads these files with curl before it clones the members. Thus
a hook can use them. basis does not change files that are present.
A repository can have a special _postclone action. When basis install
clones that repository, basis runs the action automatically in the repository
directory. Use the hook to patch generated files or local development files.
An action with a name that starts with _ is a hook. Hooks run automatically.
The basis action list does not show hooks.
- name: release-generator
path: release-generator
url: https://github.com/acme/release-generator
lang: other
actions:
_postclone: ["../gen-dbg.sh"] # regenerate local-dev overrides on cloneconstellation: my-product
version: 1.2.0 # optional canonical version of the constellation
email_domain: corp.com # optional identity policy (see `basis verify`)
repos:
- name: core # unique name, used with --repo
path: core # path relative to the manifest
lang: rust # rust | cpp
url: https://github.com/acme/core # optional canonical git URL
actions:
build: [cargo build --release]
clean: [cargo clean]
- name: engine
path: engine
lang: cpp
url: git@github.com:acme/engine.git
provides: core # optional package name exposed to dependents
version_file: .version # optional, default: .version
cmake_file: CMakeLists.txt # optional, default: CMakeLists.txt
actions:
build:
- cmake -B build -S .
- cmake --build build
clean: [rm -rf build]- Each repository has a map of actions. Each action is a list of shell
commands, in order. The commands run in the repository directory through
sh -c. - You can select almost all action names. Run an action with
basis <action>. Only the reserved names are not permitted (see below).
basis # list all actions in the manifest
# A name that is not reserved runs the matching `action` across the constellation:
basis <action> [--repo NAME]... [-k] [-n] [--tmux|--no-tmux]
basis build # run the `build` action everywhere
basis run # run the `run` action (e.g. services)
basis test --repo core --tmux # run `test` in a tmux display
# Reserved subcommands (not permitted as action names):
basis install [org/repo] [--into DIR] [--branch B] # clone a constellation
# (no arg: clone missing
# members of current manifest)
basis update [--repo NAME]... # git pull --ff-only the cloned repos
basis status # git + version status of all repos
basis verify # check git/GPG e-mail domains
basis display [NAME] [--detached|--kill] # start a tmux dev dashboard
basis mcp # serve the constellation over MCP (stdio)
basis version # alias of `version show`
basis version show # list every repo's version
basis version set <X.Y.Z> # set an explicit version everywhere
basis version sync [--to <X.Y.Z>] # set all repos to one version
basis version bump <repo> [--major|--minor|--patch|--to X.Y.Z]build, clean, run, and test are not special names. They are action
names that basis finds in the manifest. The names install, update,
status, verify, display, version, and mcp are reserved. Only these
names are not permitted as action names.
Common flags:
-f, --file <PATH>— the manifest path. The default isbasis.yaml. If PATH is a bare filename, basis searches for it upward from the current directory. (git finds.gitin the same manner.) Thus you can runbasisfrom each subdirectory of the constellation. If PATH has a directory component (example:../basis.yaml), basis uses it without a search. Repository paths are relative to the directory that contains the manifest.-r, --repo <NAME>— operate only on the given repositories. You can give this flag more than one time.-k, --keep-going— if a command fails, continue with the other repositories.-n, --dry-run— show the commands, but do not run them.-t, --tmux— run the action in a per-task tmux display. Each repository gets one pane. The panes run in parallel. You can add--detachedand--layout <L>.
basis version sync selects its target version in this order:
- The
--to <X.Y.Z>value, if you give it. - If not, the top-level
version:of the manifest. - If not, the highest semver version in the repositories.
For a Rust repository, basis writes [package].version in Cargo.toml. basis
keeps the file format. For a C++ repository, basis writes the .version file.
If the CMake file has a project(... VERSION x.y.z ...) call, basis also
updates that call.
basis version bump <repo> increases the version of one component. The
default is --patch. You can give --major, --minor, or --to X.Y.Z. Then
basis updates each repository that depends on the component:
- Rust dependents — basis updates the
versionof the applicable entry in[dependencies],[dev-dependencies], or[build-dependencies]. basis keepspath, the features, andpackage =renames. - C++ dependents — basis sets the new version in
find_package(<name> <ver> ...)in the CMake file.
basis finds the dependents by the provided name of the component. This is
the provides: field, if set. If not, it is the Rust crate name
([package].name). If not, it is the repository name.
$ basis version bump core --minor
bumping core 1.0.0 -> 1.1.0 (provides 'core')
✓ core version set to 1.1.0
↳ app now requires core 1.1.0
↳ engine now requires core 1.1.0A task can name a display in the manifest. A display is the tmux session in which the task runs. basis creates the display only when the task runs: one pane for each repository, in parallel. basis creates nothing before that time.
Use a display for a long task that you monitor: services, or watchers. Do not
give a display to a short task (build, clean). A task without a display
runs in the current terminal:
tasks:
run:
display: services # run this task in the "services" display
layout: even-vertical
# build / clean: no display — they run inline in the current terminalWith this configuration, basis run does these steps:
- basis creates a tmux session with the name
services. - Each selected repository that has the action gets one pane. The pane starts in the repository directory. The pane runs the commands of the action.
- basis skips the repositories that do not have the action.
- basis applies the layout and attaches to the session.
The usual example is a long run task. It starts each service. Each service
shows its output live in its own pane:
tasks:
run:
display: services
layout: even-vertical # stacked logs, one per repo
repos:
- name: api
actions: { run: ["cargo run --bin api"] }
- name: worker
actions: { run: ["cargo run --bin worker"] }
- name: web
actions: { run: ["npm run dev"] }basis run # api / worker / web each get a pane in "services", logs livebasis sends the commands to a live shell. When you push Ctrl-C, the pane stays
open. You can start the process in the pane again. When you run basis run
again, basis attaches to the same session.
Close a display: basis binds Ctrl-q (without prefix) to kill-session
in each display it creates. When you push Ctrl+Q in an attached display,
tmux closes the display and stops the processes in it. Ctrl-b d only
detaches you. The display then continues in the background.
Restart a display: When you push Ctrl+R (without prefix), basis
restarts each pane. basis sends Ctrl-C to stop the process. Then basis runs
the initial pane command again. (basis keeps this command in the
@basis_restart pane option.) Use this after a code change: one key runs the
full task again. Note: the two key bindings are global in the tmux server.
In tmux, they replace the standard Ctrl+Q and Ctrl+R functions. Thus the
reverse-i-search of the shell is not available in the panes.
Overrides for one invocation:
basis run # uses the task's display: setting
basis run --tmux # force a display (named <constellation>-run)
basis run --no-tmux # force the current terminal for this run
basis run --detached # with tmux: create but don't attach
basis run --layout tiled # with tmux: override the layoutA display can also be a named tmux session in the manifest: a permanent development dashboard (servers, watchers, logs, a free shell):
displays:
dev:
session: myproj-dev # optional, default <constellation>-<display>
layout: tiled # tiled | even-horizontal | even-vertical | main-vertical | ...
panes:
- { repo: core, command: "cargo watch -x run" } # cmd in the repo dir
- { repo: engine, action: build } # reuse a repo action
- { name: logs, cwd: ., command: "tail -f log/dev.log" }
- { name: shell } # just a shell in base dirbasis display # list configured displays
basis display dev # create the session (if needed) and attach
basis display dev --detached # create but don't attach (prints attach hint)
basis display dev --kill # stop and remove the sessionEach pane starts in a directory. basis uses cwd, if given. If not, basis
uses the repo directory. If not, basis uses the manifest directory. Each
pane runs a command. basis uses command, if given. If not, basis uses the
given action of repo (basis connects its commands with &&). If not,
basis starts a plain shell. basis sends the commands to a live shell. When the
task stops, the pane stays open. You can run the task again. basis display NAME is idempotent: if the session exists, basis attaches to it and does not
create it again.
basis status shows each configured display. It also shows if the tmux
session of the display operates:
displays:
dev ● running 3 pane(s), tiled [demo-dev]
tests ○ stopped 2 pane(s), even-horizontal [demo-tests]
basis mcp serves the constellation through the
Model Context Protocol with the stdio
transport. Thus an AI agent can control the constellation. basis generates
the tool set from the manifest:
- One tool for each action (
build,clean,test, …). Each tool has areposfilter. The filter permits only the repositories that have the action. Each tool also has thekeep_goinganddry_runflags. Hooks (_postclone, …) do not become tools. If the task of an action has adisplay:(long services), the tool description has a LONG-RUNNING warning. Through MCP, such an action runs inline, and the call does not return until the processes stop. - Fixed tools:
basis_repos(the member list as structured data),basis_status,basis_update,basis_versions,basis_version_sync,basis_version_bump.
For each call, the server starts the basis binary again and collects the
output. Thus a tool call and the equal CLI command have the same behavior.
(Actions always run with --no-tmux.) The server reads the manifest again for
each request. Thus a change to basis.yaml shows in the subsequent
tools/list.
The JSON-RPC stream uses stdout. Thus the server writes its log to stderr: one start message (constellation, repository count, tool count), and one line for each request (tool name, arguments, ok or error, duration):
basis mcp: constellation 'example' — 2 repo(s), 9 tool(s); serving MCP on stdio
· initialize (claude-code) → ok [0ms]
· tools/list → ok [1ms]
· build {"dry_run":true,"repos":["core"]} → ok [12ms]
Add the server to an MCP client. basis status shows the applicable commands
for Claude Code and for Codex, with the full manifest path:
mcp: serve this constellation to AI agents (stdio):
claude mcp add my-product -- basis --file /path/to/constellation/basis.yaml mcp
codex mcp add my-product -- basis --file /path/to/constellation/basis.yaml mcp
A plain basis mcp command also operates when the work directory of the
client is in the constellation. basis then finds the manifest with the usual
upward search.
basis verify makes sure that contributors use a company identity. For each
repository that has an e-mail-domain policy, the command checks two items:
git config user.emailis on a permitted domain.- The OpenPGP signing key (
user.signingkey, or the key that matches the git e-mail) has a user ID with an e-mail on a permitted domain.
The domains come from email_domain (one value) and/or email_domains (a
list). A repository can replace the constellation policy with its own field.
A signature in SSH format (gpg.format=ssh) has no e-mail. basis reports it
as unverifiable, not as a failure. If a checked repository fails, the command
exits with a code that is not zero.
$ basis verify
==> core
allowed domains: corp.com
✓ git email: dev@corp.com
✓ gpg key: ABCD1234 [dev@corp.com]
✓ okbasis status does the same checks. It shows a short id ✓ / id ✗ / id !
column for each repository (— if no policy applies), plus a summary line.
basis status only gives information, and it always exits with code 0.
basis verify is the gate that stops errors. Use verify in CI or in a
pre-push hook.
$ basis status
core rust 1.0.0 id ✓ main clean origin✓
app rust 1.0.0 id ✗ main dirty origin✗
versions: all versions at 1.0.0
identity: 1 repo(s) fail (run `basis verify` for details)Each repository can give a canonical git url:. basis status compares it
with the local origin remote. The report shows one of these values:
origin✓—originand the canonical URL are the same.origin✗—originpoints to a different location. The report shows the expected value and the found value below the table.no-origin— the repository has nooriginremote.missing— the repository directory is not cloned.
basis normalizes the URLs before the comparison. Thus
git@github.com:acme/core.git and https://github.com/acme/core are the
same repository. basis ignores the scheme, the git@ userinfo, and a .git
ending.
The examples/ directory has an example that you can run. From
the repository root:
basis -f examples/basis.yaml status
basis -f examples/basis.yaml version sync
basis -f examples/basis.yaml build -n