Operate local deploy trains for coding-agent worktrees with approval-gated deploys.
io.github.yongjip/mergetrain (MCP Server)
The io.github.yongjip/mergetrain MCP server operates local deploy trains for coding-agent worktrees. It supports approval-gated deploys, where deploy actions require approval before proceeding. This is intended to manage deploy workflows tied to coding-agent workspaces and ensure controlled releases.
π οΈ Key Features
Local deploy train operation
Coding-agent worktree deploy management
Approval-gated deploys
π Use Cases
Managing deployments originating from coding-agent worktrees
Enforcing approval steps before deploy changes are released
Running local deploy sequences for agent-driven development
β‘ Developer Benefits
Clear deploy gating via approvals
Safer deploy workflow for worktree-based agent coding
Local, train-style deploy execution for iterative development
Let agents code in parallel. Let one train prove and ship the result.
mergetrain is a local-first integration runtime for coding-agent worktrees.
Worktrees give each agent an independent lane, but the repository still has one
ordered history: without a coordinated landing boundary, parallel coding turns
back into manual rebases, merge races, and repeated test runs. mergetrain keeps
that missing integration spine on your machine. Coding agents commit and
enqueue; one local runner serializes their branches, validates the exact train,
and pushes only after explicit approval. No hosted merge-queue service or CI
provider is required.
Four guarantees shape the design: an exact validated train identity (the
train you approved is the train that ships, byte for byte), a lease-fenced
single runner (two processes can never race a push), an atomic multi-ref
push, and crash-safe, exactly-once deploys (after any crash, recovery
reconciles the local queue against the remote, so a landed train is never
re-pushed and a lost one is never mislabeled as shipped).
Local-first, not local-only. Queue state, locking, train assembly, and
gates stay local. Configured Git remotes and post-deploy verification may
still use external services.
Status: current release (v1.2.0). The machine contract is frozen β
additive-only within contract major 1 β and the real-repo soak gate is
complete. Built to scratch my own itch first β published in case it scratches
yours too.
The problem
One repo, three coding-agent sessions running at once β Codex, Claude, whatever
β each in its own git worktree on its own branch: one adds a health check,
one refactors config loading, one fixes a flaky test. All three finish within
the same hour. Now what?
Without a queue, the ending is always one of these:
You become the merge coordinator. Deciding landing order, rebasing each
branch onto the last landing, rerunning the tests after every one β serially,
by hand. The time you saved by running three agents in parallel is spent
again integrating their results, and while you're integrating, you're not
reviewing or directing.
Agents that push, race. Two sessions push the same deploy branch within
seconds: non-fast-forward rejects, a retry with --force that quietly
clobbers the other session's landing, or an unreviewed merge combination
shipping because whoever pushed last "won".
Green branches, red main. Each branch passes its own tests, but two of
them touched the same config loader in incompatible ways. No per-branch
check can see that β only testing the combined result before it ships can.
Judgment calls land on an LLM. Stale lock or live runner? Duplicate
enqueue or a legitimate retry? Is unattended deploy actually approved for
this job? These are exactly the calls you don't want an agent guessing at
from fuzzy shell output.
What mergetrain does about each
Each of those failure modes maps to a specific mechanism β this is the design,
not a feature list:
Without a queue
With mergetrain
You order, rebase, and re-test every landing by hand
Agents enqueue and stop; one runner assembles the FIFO train in a throwaway worktree and lands it
Sessions race git push; a retry with --force clobbers
Agents never touch deploy refs; a lease-fenced single runner pushes atomically, exactly once
Green branches, red main
Gates run over the exact combined train before push; when a combination fails, the runner bisects it and names the conflicting pair (conflict_with) instead of shipping around the breakage
Stale locks, duplicate enqueues, "may I deploy?" become LLM guesses
Every state is JSON with an explicit next_action; deploys require explicit intent (--deploy), and unattended runs touch only pre-approved --auto jobs
The laptop dies mid-push
A write-ahead marker and pin ref let recovery ask the remote what landed β never re-pushed, never mislabeled
That mapping is also the honest origin story: mergetrain exists because I was
running several coding-agent sessions on one repo and spending the hours they
saved me re-serializing their branches by hand. The queue was built to get
those hours back; the rest β validated train identity, crash recovery, the
multi-repo hub β followed from operating it every day. The parallelism you
paid for stays parallel.
When to reach for it
Several agents, one project β the core case, and the one it was built
for: parallel worktree sessions on a single repo, landing their results all
day without you serializing them by hand.
Unattended batches β pre-approved --auto jobs land via the daemon
while you're away; everything else waits for a human.
Agents across several repos β the hub registers each repo and runs the
same policy machine-wide, one repo's gates at a time.
One agent, one branch at a time? You don't need this β git push is fine.
And if your team is PR-first on a hosted forge with remote CI, use the
forge's native queue (see alternatives).
PR-first or mergetrain?
A fully parallel coding-agent workflow needs two things: parallel execution
lanes and a serialized integration spine. Worktrees provide the lanes;
mergetrain provides the spine. That integration layer is what preserves the
parallelism after several agents finish at nearly the same time.
A pull request is primarily a human review unit. A mergetrain job is an
execution and integration unit for a committed agent branch. Neither model
is universally better; the useful question is whether every agent branch needs
its own review conversation or whether several trusted branches should be
validated and landed as one train.
Dimension
PR-first workflow
mergetrain
Strongest advantage
Human review, approvals, audit history, and distributed collaboration
Low-ceremony integration of many local agent branches
Validation
Usually per PR; a forge-native merge queue can add merge-group CI
Gates run over the exact assembled train before one atomic push
Latency and CI use
Each branch enters a remote PR/CI lifecycle
Local gates can validate a batch once; failures may trigger isolation runs
Platform model
Forge, webhooks, branch rules, and hosted CI
Local SQLite, Git worktrees, shell gates, and any Git remote
Main trade-off
Repeated PR ceremony and changing merge bases for small agent tasks
No code-review UI; you own the runner, credentials, and branch policy
The two can coexist: use mergetrain for direct integration where that is the
policy, push a validated train to a review branch and open one PR, or reserve
individual PRs for changes that need human discussion. See the
PR-first comparison and decision guide for the full
pros, cons, and hybrid patterns.
Hosted merge queues (GitHub Merge Queue, GitLab Merge Trains, Mergify, Aviator, bors) solve a related problem, but they are PR-first, remote-CI-first, and platform-first. mergetrain is for the other workflow: local-agent, worktree-first, deploy-branch-first.
How it works
code
agent A ββ
agent B ββΌββΆ mergetrain queue (SQLite) ββΆ one runner (lock)
agent C ββ β
βΌ
fresh integration worktree @ origin/main
merge A β B β C (the train)
β
gates (diff-check, tests, scansβ¦)
β
git push --atomic β configured refs
β
post-push verify hooks
Agents commit their work and enqueue a branch. They never push deploy refs themselves. A single runner (or unattended daemon) claims the queue, builds an isolated integration worktree on top of your integration branch, merges the queued branches in FIFO order, runs your gates once over the whole train, and only then pushes β atomically β to your deploy refs. Validation worktrees are disposable by default; path-sensitive build caches can opt into a runner-locked stable validation path while deploy worktrees remain disposable. Every important state is readable as JSON so an agent can follow the result instead of inferring it.
Quickstart
A terminal recording of mergetrain creating four agent branches, attributing a semantic conflict between two of them, and atomically deploying the compatible survivor train.
bash
# See the whole workflow in a disposable local sandbox (~1 minute)
uvx mergetrain demo
# Install the public alpha (zero runtime dependencies)
uv tool install mergetrain # or: pipx install mergetrain
brew install yongjip/tap/mergetrain # macOS, no Python needed# No install at all? Try it first: uvx mergetrain --help# 1. Scaffold config + agent docs in your repo
mergetrain init --project my-app --write
# 2. An agent finishes work, commits, and enqueues its branch
mergetrain enqueue --task "add health check" --branch agent/health --capture-sha
# 3. See the queue and lock state (machine-readable)
mergetrain status --json
# 4. Watch the queue and runner locally (read-only)
mergetrain dashboard
# 5. Validate the whole train without shipping
mergetrain run-batch --validate-only
# 6. Ship β explicit, never implicit
mergetrain run-batch --deploy
Here deploy is the backward-compatible name for the atomic Git ref update,
not necessarily a provider release. Repositories that reserve βdeployβ for
TestFlight, Play, App Store, Kubernetes, or another downstream system can set
terminology.git_operation: integrate and use run-batch --integrate. This
changes human CLI, dashboard, wrapper, and generated-agent wording only;
SQLite/JSON keep the stable deployed status and deploy_sha field.
For an unreleased source checkout, use python -m pip install -e . instead.
mergetrain demo creates a throwaway repository and local bare remote, then
drives four real agent branches through FIFO enqueue β ordered merge β a
combined-train gate failure that no single branch causes β bisection to the
minimal failing pair (conflict_with) β exact-train deploy of the two
survivors. Two of the four branches touch no common file, so Git merges them
cleanly and each is green alone: only the combined tree is red, which is the
case a merge queue exists for and per-branch CI cannot see. It uses no network
and isolates Git configuration from your account. Add --keep to inspect the
final queue or open mergetrain dashboard --preview; add --pause for a
narrated presentation.
The dashboard is served at http://127.0.0.1:8765/. It streams structured
runner phases, heartbeat freshness, job order, blocked reasons, recent activity,
the exact current gate and command template, history-derived ETA and gate
timing, and the next safe action. On phone-sized screens it collapses to the
three facts needed for a remote glance: state, next action, and attention.
CONNECTED describes the browser's data stream; RUNNER ACTIVE separately
describes the process that owns the train. It has no mutation endpoints or
deploy controls.
Running agents across several repos? Register each one and serve every queue
on a single board:
sh
mergetrain hub add ~/projects/app # once per repo
mergetrain hub # one dashboard for the whole machine
The hub is the same read-only UI in multi-repo mode β repo cards with queue
counts, runner state, and the next safe action, each drilling down into the
full single-repo view. It owns no queue state: every repo entry is read from
that repo's own SQLite database, opened read-only, so observing a repo never
creates or migrates anything inside it. mergetrain hub daemon runs your
--auto work across those repos too, with a machine-wide concurrency cap
(default: one repo's gates at a time β parallel agents, but never parallel
engine builds). See hub.
Non-interactive callers can observe the same runner without starting a browser:
The event stream is resumable by persisted event ID and emits separate heartbeat
and terminal frames. Raw command output stays in the explicit local logs
command, not structured events.
Validation records an exact train identity, including every task HEAD and the
integration base used for the check. The later deploy reassembles that same
train on the current integration ref, reruns all gates, and refuses changed
task branches. Newly queued work is not silently added to the approved train.
Expensive gates may be reused only through an explicit validated-reuse policy or
--reuse-validated; a non-deploying --preview --json reports the exact reused
SHA or why the full safe path will run, plus identity facts and a
coverage-qualified savings estimate that never grants authorization. Independent
gates can opt into resource-bounded parallel groups while the default remains
strictly sequential.
If the intended contents change after validation, supersede atomically retires
the old train and captures clean replacement HEADs as a new queued train. The
audit relationship is preserved, but validation, reuse identity, and deploy
approval are never inherited.
Every agent-facing command is non-interactive and requires explicit intent: --validate-only or --deploy, never a bare run-batch.
Core concepts
Job β one task branch waiting in the queue, with the SHAs captured at enqueue time.
Validated train β an exact, deployable group of jobs that passed gates together and is waiting for explicit deploy approval.
Runner lock β gives every claim a unique lease token, heartbeats through long-running commands, and prevents a stale runner from overwriting a newer owner.
Run event β a persisted, secret-conscious phase transition with an integer resume cursor; follow mode adds ephemeral heartbeat and terminal frames.
Integration worktree β an isolated, detached Git worktree built on your integration ref. It is disposable by default; validation may opt into a stable path that preserves only declared ignored caches. Deploy worktrees remain disposable, so agents never checkout or push the deploy branch.
Gate β a verification command (diff-check, tests, secret-scanβ¦) run once over the assembled train before push. A gate failure means nothing ships.
Verify hook β a command run after push to confirm the deploy is live.
Auto job β a job enqueued with --auto, the only kind the unattended daemon will touch. Manual jobs are left for a human-initiated runner.
Also local-first: lands queued branches one at a time (rebase β check β push), built around Claude Code's worktree hooks
Batched validated trains with an exact approved identity, SQLite-durable state, remote-reconciled crash recovery, and harness-agnostic operation (Codex, Claude, anything that can run a CLI)
If your team is PR-first on a hosted forge with remote CI, use the native
queue β that is exactly what it is for. mergetrain is for the other workflow:
local coding agents in worktrees, shipping to a deploy branch, with or
before any PR.
The crash story, specifically
A hosted queue's crash recovery is its vendor's uptime page. A local queue
runs on a laptop β which loses power, sleeps mid-push, and gets its terminal
killed. mergetrain treats that as the normal case, not the exception: every
push is preceded by a durable write-ahead marker and a
refs/mergetrain/pending/<id> pin ref, so mergetrain recover can ask the
remote what actually happened. A train is marked deployed only when a
push ref carries its SHA, a landed train is never pushed twice, and deploys
are refused while any job still needs reconciling. As far as we can tell, no
other merge queue β hosted or local β documents an exactly-once push contract
at all; the full failure catalogue is in
failure modes.
mergetrain is not a general-purpose job queue (it won't replace Celery/RQ/Sidekiq), a CI provider, or a deploy provider. The core is provider-neutral: your push targets, test commands, and deploy checks live in config, not in mergetrain.
Configuration
A single .mergetrain.yaml at your repo root holds all policy. The core stays neutral; you bring the commands.
yaml
project:name:my-appgit:remote:originintegration_branch:mainpush_refs: [main] # atomic push targets on deployterminology:git_operation:integrate# deploy (default), integrate, or pushqueue:lock_ttl_minutes:30heartbeat_interval_seconds:10command_timeout_seconds:3600gates:-name:diff-checkrun:gitdiff--check${integration_ref}..HEAD-name:testsrun:python-mpytestpaths:-src/**-tests/**-pyproject.tomldeploy:verify:-name:live-healthrun:curl-fsShttps://example.invalid/health
See the config reference for the full schema, placeholders, and environment variables.
For AI agents
mergetrain is designed so an agent can operate it from a short contract and JSON output, without guessing:
Work on a task-specific branch in its own worktree.
Commit before enqueuing.
Never push deploy refs directly.
Read mergetrain doctor --json / status --json before acting.
Use --auto only after explicit human approval for unattended deploys.
Let one runner or daemon own merge β test β push β verify.
Fix blocked/failed work on the owning branch and enqueue a fresh clean job.
When doctor --json says wait_for_runner, use inspect --json or a scoped
events --follow --jsonl stream instead of probing the OS process tree.
mergetrain init writes AGENTS.mergetrain.md / CLAUDE.mergetrain.md so your agents pick this up automatically.
Claude Code can install the same contract plus the human-gated MCP deploy tool
as a plugin. Install the CLI with its MCP extra first, then add this repository
as a marketplace:
The plugin contributes /mergetrain:mergetrain for normal queue work and the
manual-only /mergetrain:deploy flow. A deploy still requires the MCP client's
attributable human confirmation; installing the plugin does not enable
unattended deployment or validated-gate reuse.
v1.2.0 is the current release. The machine contract is additive-only within
contract major 1: a key may be added, never removed or renamed, and a golden
key-set fingerprint over 25 payload surfaces plus the JSONL frames fails CI on
any un-versioned shape change (see
contract).
The core β queue, runner lock, merge train, gates (with bisected joint-failure
isolation and semantic-conflict reporting), atomic push, crash-safe
reconciliation/recovery
(reconcile/recover/unlock/verify/dismiss/supersede),
auto-only daemon, resumable CLI events/inspection/log following, the local
read-only dashboard, the multi-repo hub, and an
MCP server whose
deploy tool requires a human accept β is implemented and tested on macOS, Linux,
and Windows, including a fault-injection matrix that SIGKILLs a real
git push --atomic with the remote's refs applied and without, to prove the queue
never lies about what shipped.
The 1.0 evidence gate is now met: a dedicated GitHub repository completed 20
landed trains at a 100% land rate, including planned gate and conflict recovery
and one real git push --atomic SIGKILL whose recovered queue verdict matched
the remote. Built for my own multi-agent workflow first; issues and ideas
welcome. Review your config trust boundary, gate commands, and secret handling
before enabling unattended deploys β see
security.