Git is very good at branching.
Git is not a coordinator.
That distinction does not matter when one person is working in one checkout.
It matters a lot when one machine is running five agents, ten worktrees, and a
protected local main that still needs to stay clean and pushable all day.
That is what mainline is for.
mainline is a repo-local control plane for parallel worktree development. It
keeps feature work in feature worktrees, serializes integrations onto protected
main, coalesces publishes so only the newest protected tip matters, and keeps
the whole queue durable so the machine can still explain itself after a crash.
This is not a new VCS. It is the missing piece between “Git has branches” and “many humans and agents are all trying to land code on the same box.”
The default workflow looks reasonable:
- branch off
main - work in parallel
- rebase occasionally
- merge or push when done
That works for one person.
It degrades fast under local parallelism:
- one worktree rebases while another keeps building on stale
main - conflicts get discovered on the protected branch instead of in the source worktree
- two publish attempts race even though only one protected tip actually matters
- an agent gets interrupted and nobody knows what was queued, blocked, retried, or cancelled
The failure mode is not dramatic. It is just expensive. main stops being
boring. Queue state lives in scrollback. Humans and agents start doing careful
Git surgery by hand.
mainline makes that coordination problem explicit and mechanical.
There is one rule that matters:
main is not where feature work happens.
Once that line is real, the rest follows:
- the repo root checkout stays as the canonical protected
main - feature work happens in topic worktrees
- submissions are recorded durably
- integration is serialized
- conflicts are resolved in the source worktree
- publish is a separate queue
- operators can see, retry, cancel, and stream what is happening
Git still owns refs, rebases, fast-forwards, pushes, and worktrees.
mainline.toml owns repo policy and protected-worktree configuration.
SQLite owns queue state, locks, and event history. mainline coordinates the
three.
Do not run environment-mutating helpers like npm skills from the protected
root checkout; run them in the topic worktree you are changing so local
lockfile drift does not block publish.
The short CLI is mq, because that is what it is: the main queue for one
machine.
The agent path should feel like this:
cd /path/to/topic-worktree
mq submit --check-only --json
mq submit --wait --for landed --timeout 30m --jsonImportant: plain mq submit --wait stops at integrated. In a repo with
[publish].Mode = 'manual', that means the commit is on local protected
main but not yet pushed to remote.
If the wrapper wants one blocking submit call that waits through auto-publish:
mq submit --wait --for landed --timeout 30m --jsonIf the caller wants a durable machine handle instead of a one-shot wait:
mq submit --json
mq wait --submission 42 --for landed --json --timeout 30mThat is the primary follow path. Use the returned submission_id and wait on
it. Do not use sleeps, branch-name polling, mq logs, mq events, or
mq watch as the normal way to decide whether your land finished.
If the wrapper expects remote landing as part of the same job, prefer:
mq land --json --timeout 30mIf publish loses a race because remote main moved first, rerun the failed
publish request instead of hand-repairing protected main:
mq retry --repo /path/to/protected-worktree --publish 4When the unpublished protected-branch commits can be replayed cleanly, mq
fetches upstream, rebases the protected branch onto the updated remote tip, and
retries the push automatically.
If a submission blocks because its topic branch is behind local protected
main, use mq rebase instead of hand-rolling the Git repair:
mq blocked --repo /path/to/main --json
mq rebase --repo /path/to/topic-worktree --submission 23 --json
mq retry --repo /path/to/topic-worktree --submission 23 --jsonIf mq rebase stops on conflicts, finish that in-progress Git rebase in the
source worktree before rerunning mq rebase; reruns do not abort and restart
an in-progress resolution.
For branch-oriented repair without a submission id:
mq rebase --repo /path/to/topic-worktree --branch feature/login --jsonIf you only need the next protected-main advance, use:
mq next --json
mq next commit --jsonFor agent-heavy and factory-style repos, set [publish].Mode = 'auto' in
mainline.toml unless there is a real operator reason to keep publish manual.
That file is the runtime config authority for the repo. The SQLite store keeps
queue identity and history; it should not be treated as the source of repo
policy truth.
If a repo needs cache/build preparation after integration and before push, use
[checks].PreparePublish in mainline.toml. This is the right place for
commands like repo-local pnpm install --frozen-lockfile or cache warmup steps
that make push-time validation reproducible.
Put read-only verification commands in [checks].ValidatePublish.
Prepare commands may warm ignored caches, but they must not leave tracked or
other non-ignored drift in protected main; mq fails publish if prepare
commands dirty the protected worktree. Legacy [checks].PrePublish still runs,
but it is now compatibility-only and should be migrated to PreparePublish or
ValidatePublish.
Once a repo configures explicit publish checks, mq treats those checks as the
authoritative publish gate and bypasses inherited local pre-push hooks for
the final git push. Keep publish-time preparation and validation in
mainline.toml, not in hook side effects on protected main.
mq status --json and mq repo show --json expose that effective publish
execution policy and, when publish is active, whether the worker is in
prepare, validate, or push.
If you want to prove that some other process, not submit, handled a specific change:
mq submit --queue-only --json
mq wait --submission 42 --for landed --json --timeout 30mThe controller path should feel like this:
mq land --json --timeout 30mThat is the product: one machine, one protected branch, many worktrees, one queue.
The demos are generated from source-controlled VHS tapes in
docs/demos/tapes/ and can be re-rendered with:
./docs/demos/scripts/render.shBecause the model is conservative in the right places.
- Git is still the source of truth
- the protected branch is treated as infrastructure, not as a workspace
- the queue survives crashes and restarts
- conflicts get pushed back to the worktree that owns them
- publish is coalesced instead of “whatever finished pushing last”
This is exactly what you want when the machine is running Codex, Claude,
humans, or all of them at once. mainline does not ask those actors to become
perfectly disciplined. It turns the safe path into the normal path.
mainline is already a real toolchain:
- standard repo and bare-clone-plus-worktree discovery
- durable SQLite queue state in shared Git storage
- serialized integration and publish workers
- branch submission from topic worktrees and detached SHAs
submit --check-only,submit --wait, and one-shotland- opportunistic submit-side draining when no worker already holds the lock
wait --submission <id> --for integrated|landedstatus,watch,logs,events,doctor, andconfidence- optional manual
mainlinedhelper mode for experiments or multi-repo hosting - retry and cancel as real operator controls
- policy checks, hook coordination, and repo-managed hooks
- repository-row consolidation when one protected worktree was accidentally registered under multiple canonical paths
- Homebrew, Nix, GitHub release archives, checksums, and release manifests
- GoReleaser-driven multi-platform binary releases for macOS, Linux, and Windows
Recent hardening coverage now explicitly exercises the adoption-critical paths:
- concurrent multi-worktree submit, integrate, and publish flows
- deleted, moved, and dirtied source worktrees after submit
- stale queued or blocked submissions whose source SHA is already reachable from
protected
main, including after the original worktree was dropped - queued branch head drift after submit
- external protected-branch advancement while queued work waits
- inherited
pre-pushhook success and failure-plus-retry publish paths - crash/restart recovery around rebase, fast-forward, and push boundaries
- JSON contract tests for
status, rawevents, lifecycleevents,watch, and host logs - bare-repo plus linked-worktree runs
This repo dogfoods that workflow. The repo-local worktree instructions live in .agents/skills/worktree/SKILL.md, and the repo-specific guardrails live in AGENTS.md. Machine-readable JSON contracts and their compatibility policy are documented in JSON_CONTRACTS.md. That document, not the internal Go structs, is the public automation contract.
Requires Go 1.25 or newer.
From source:
git clone git@github.com:recallnet/mainline.git
cd mainline
make buildWith go install:
go install github.com/recallnet/mainline/cmd/mainline@latest
go install github.com/recallnet/mainline/cmd/mq@latestmq is the default product path. mainlined is optional.
Homebrew and Nix details are in install.md.
There are two valid ways to be here:
- developing the
mainlinecodebase itself - using
mainlineto manage some other repo
Those are not the same thing.
In this source checkout:
- committed docs and policy files live here
mq repo root --repo . --jsontells you whether this checkout is the canonical protected root checkout- a fresh clone is not automatically an initialized managed repo just because
mainline.tomlexists in Git - queue/state commands like
mq status --repo . --jsoncan still fail until this checkout has been explicitly initialized on this machine
For initialized repos, the operator model should stay simple:
- humans
cdinto the normal repo path - that checkout is protected
main - it stays clean and boring
- feature work happens in linked worktrees
- bare Git storage, if present, stays an implementation detail
If you are onboarding as a contributor to mainline itself, start with:
make build
go test ./...
./bin/mq --help
./bin/mq repo show --repo . --json
./bin/mq repo root --repo . --jsonThe contributor-specific flow is also summarized in
CONTRIBUTING.md
and encoded for agents in
.agents/skills/onboarding/SKILL.md.
Use the onboarding skill before the worktree skill. Onboarding answers "what is
this repo, what works in this checkout, and what should I run first?" The
worktree skill answers "I already understand the setup, now how do I
contribute safely through mq?"
Recommended first-time repo setup after install:
cd /path/to/repo-root
mq repo init --repo .
git add mainline.toml
git commit -m "Initialize mainline repo policy"
./scripts/install-hooks.shThat init commit matters. It turns the repo’s queue policy into versioned,
reviewable state instead of one more local convention that agents have to infer.
For normal repos, the root checkout should be the canonical protected main.
Keep it clean and boring. Humans inspect that path, and the machine wrapper
builds mq from it. If it is dirty, the wrapper should refuse
to build rather than silently drift.
Run package-manager helpers and lockfile generators in topic worktrees, not in
the protected root checkout.
Use mq repo root --repo . --json to verify that the root checkout is still
trustworthy. Use mq repo root --repo . --adopt-root only when the root
checkout is already clean and on the protected branch.
Repo-local AstroChicken scaffolding under .astrochicken/ is treated as
tool-owned authoring surface, not protected-branch drift, so it does not by
itself make the checkout untrustworthy.
Setup:
mq repo init --repo /path/to/repo-root
mq repo root --repo /path/to/repo-root --json
mq repo audit --repo /path/to/repo-root --json
mq config edit --repo /path/to/repo-root
mq doctor --repo /path/to/repo-root --fix --jsonIf protected main is dirty, mq doctor is the takeover command: it tells you the queue is blocked, shows the dirty paths, and tells you to save, clean, or resolve the protected root checkout before retrying.
Submit and land:
cd /path/to/topic-worktree
mq submit --check-only --json
mq submit --wait --timeout 15m --json
mq submit --wait --for landed --timeout 30m --json
mq submit --allow-newer-head --wait --timeout 15m --json
mq submit --json
mq wait --submission 42 --for landed --json --timeout 30m
mq land --json --timeout 30mUse mq submit --wait when the caller only needs an integration result.
Use mq submit --wait --for landed or mq land when the job is not done until
remote main has moved.
Operate and observe:
mq status --repo /path/to/repo-root --json
mq repo audit --repo /path/to/repo-root --json
mq watch --repo /path/to/repo-root
mq events --repo /path/to/repo-root --follow --json --lifecycle
mq registry prune --json
mq retry --repo /path/to/repo-root --submission 17
mq retry --repo /path/to/protected-worktree --publish 4
mq cancel --repo /path/to/repo-root --publish 4The default model is queue-first commands that try to become the drainer
themselves and stay alive until the repo is quiescent, including sleeping
through scheduled publish retries. mainlined still exists as an optional
manual helper mode, but it is not part of the default machine setup.
mainline supports both:
- normal repos with
.git/inside the checked-out worktree - bare-clone storage with linked worktrees, such as
~/Projects/.bare/owner/repo.gitwith~/Projects/owner/repo
For bare-clone layouts, queue state and locks live with shared Git storage so
every worktree sees the same queue truth.
The bare storage path is not a human-facing checkout, so root_checkout.exists = false is expected there. Point mq repo init at a clean local checkout on
the protected branch, not at the bare storage directory itself.
The design is intentionally small.
go-githandles ordinary inspection and config work- native
gitis used where exact write-path behavior matters - Git answers topology and branch semantics
- SQLite answers ordering, durability, coordination, and audit history
More detail is in ARCHITECTURE.md.
Use mainline when:
- many worktrees share one machine
mainmust stay clean and pushable- humans and agents are landing in parallel
- you want deterministic branch landing instead of ad hoc Git rituals
- you want queue state to survive crashes and restarts
Do not use it if your workflow is one person, one branch, one push, and no parallelism. Plain Git is already excellent there.
make fmt
make test
make build
make install-hooks

