MergeLoop is a merge queue for GitHub. Add a label to a pull request, and MergeLoop rebases it onto its base branch if it has to, waits for the required checks, and merges it.
Status: early. Expect rough edges, and tell us what breaks.
This README is also the website's docs, at mergeloop.ai/docs. DOCS in crates/web/src/docs.rs picks its ## sections by heading, and a test fails when one is renamed.
MergeLoop was previously called Mergepatrol. A repo set up before the rename keeps working as
it is: MergeLoop still reads its config file at .mergepatrol/mergepatrol.yml, where the label
the file leaves out stays mergepatrol, and updating the repo's skill moves it from
skills/mergepatrol to skills/mergeloop. Self-hosting still reads the MERGEPATROL_*
environment variables. The source repo is now agent-labs-dev/mergeloop; GitHub redirects the old name.
The hosted MergeLoop at https://mergeloop.ai runs the public GitHub App Merge Loop (merge-loop). You bring your own model API key.
- Sign in with GitHub at https://mergeloop.ai.
- Install the App on your organization or account, and choose its repos. GitHub sends you back to the dashboard.
- In Settings, pick a repo and Set up. MergeLoop opens a PR that adds its config file,
.mergeloop/mergeloop.yml. Merge it, and the repo queues. - In Settings, add an Anthropic or OpenAI API key. Create one for MergeLoop alone, with a spend limit.
- Add the
mergelooplabel to a PR on GitHub. The dashboard's Queue page shows each queue in the order it merges, each PR's stage, and what holds it; a PR opens on GitHub.
Public repos are free. Private repos cost $15 a seat a month, after a free trial: see Pricing. To run your own, see Run it.
A labeled PR gets ready on its own first, and no PR waits for another's: the agent resolves a conflict with its base, its checks run, and the agent fixes what fails. Then the agent reviews it. A PR that this cannot settle goes on to the queue as it is, which ejects it. A stack gets ready as one, and a PR from a fork skips this. To run all of it in the queue instead, one PR at a time, a self-hosted MergeLoop sets MERGEPATROL_PREP=false.
A labeled PR keeps its place in line, on hold, until GitHub would merge it and the queue can take it:
- It is not a draft.
- It has the reviews its branch protection or rulesets require, and no requested changes from a required reviewer. MergeLoop reads GitHub's review decision, so a repo that requires no review needs none.
- It targets the default branch, or a branch in
branchesof the config file. - With Automatic review on, the agent's review holds nothing, and every review thread on the PR is resolved: its author fixed or answered each comment, and a human answered anything the review asked.
- On the hosted MergeLoop, a private repo has a subscription, and a public repo's workspace is under the fair-use limit.
Then it runs alone or tested in a batch, and merges once its checks pass. Alone, it is rebased unless GitHub can already merge it as it is: its checks passed on its head, it has no conflict, it is not a stack, and the branch is not strict about being up to date. That skips a run of CI, at the cost of merging a PR that was not tested with the branch's newest commits. Turn on "Require branches to be up to date" to always rebase. Only the base branch's required checks count. A branch with no required checks counts every check on the commit, so turn on required checks for CI that must pass. The merge uses the repo's default merge method on GitHub. The PR's comment and the dashboard say what holds a PR.
When a rebase has conflicts, an agent resolves them. Claude is the default. A plain git rebase runs first, so the agent runs only on real conflicts. A conflict found in the queue goes back to getting ready, keeping its place, so the agent holds up no other PR.
A PR that keeps up with its base branch by merging, as when its author never force-pushes, is brought up to date the same way: MergeLoop merges the base branch into it and pushes that merge commit without force. A rebase would replay each of its commits and conflict again in files its merges already resolve. A PR whose rebase conflicts while a merge of the base branch is clean is merged too. A conflict in the merge goes to the agent once, on the merge, and the same checks hold: no file outside the PR, no conflict markers left. A repo that merges PRs by rebasing them always gets a rebase, as GitHub would replay the merge's commits too. A rebase that runs out of time merges the base branch in instead; the time running out is never a reason to eject the PR.
When a PR's required checks fail, MergeLoop first runs their failed GitHub Actions jobs again, once for each head, since a failure such as a dropped network request may pass on the next run. That needs the App's actions: write permission: an App created before this permission existed must be given it on GitHub, and until then the checks go straight to the fix. A check that is not a GitHub Actions job is not run again. If the checks still fail, an agent fixes them. It reads what GitHub shows of the failure, commits a fix on top of the PR, and the checks run again. After three fixes that do not make the checks pass, the PR is ejected. A PR gets three fixes in all, and a push from its author earns new ones. When the agent gives up because the failure is not the PR's to fix, as with a flaky test, an outage or a title since changed, an empty commit runs the checks once more, and a failure then ejects the PR. When what fails is a check of the PR's title, the agent sets a title that meets it, and the checks run the same way. A failure found in the queue goes back to getting ready, keeping its place, so the agent holds up no other PR. Without a model API key in the workspace's settings, no agent can fix the checks: a PR whose checks still fail after they run again is ejected, and its comment says to add a key.
Automatic review is on for every workspace with a model key: an agent reviews every queued PR before it takes its place in line. It reads the change and the code around it, and reports each thing it looked at with how sure it is.
The reviewer holds a PR to your repo's own instructions. It reads AGENTS.md and CLAUDE.md, and the docs they point to, and reports code that breaks a convention they state, citing the line, and a change that leaves a line in them out of date. A REVIEW.md tells it what matters in a review of your repo: what failure looks like there, paths to skip, how many small findings are worth reporting. It reads each of these files at the repo's root and in every directory the PR changes, a deeper one adding to those above it, as they are on the PR's base: a PR that edits REVIEW.md is reviewed by the old one. MergeLoop's own agent loads no other copy. REVIEW.md steers what the reviewer reports; it cannot lift the evidence rule below.
The reviewer must show a check that actually fails before sending a non-security change back, unless the repo's Review depth is Deep. For every non-security finding marked change, the reviewer names a runnable evidence command and, when needed, writes a focused test. MergeLoop then runs that command itself on the PR's head, with only the files the finding names. Only a failure MergeLoop saw in its own run counts; the reviewer's claim that it ran something does not. A passing command, a timeout or a command that could not start does not qualify.
Without that failure, the finding is cleared as a note and holds nothing; at Deep, a panel votes on it instead, as below for a security change. Cleared findings remain visible in MergeLoop's review. When a review is posted on GitHub, they appear under What it cleared. A review that clears everything posts no summary on GitHub. Nothing is silently dropped.
A security change with no failing command goes back only if fresh agents agree it holds. A hole often shows in the code alone, with no test to fail, so MergeLoop has three fresh agents check each such finding, at once, each in a checkout of the PR's head of its own. Each sees only what the finding claims and the code, not how the reviewer got there, nor the others' answers, and votes on whether it holds. It goes back to the author only when two of the three say it holds. Otherwise it is cleared as a note that says how the vote went. The votes, and why each agent cast its own, show on the finding in MergeLoop's review and in its thread on GitHub.
A change marked needs_human still goes to a human, as do kinds configured to always ask, such as migrations. Those paths do not require a failing command or a vote.
A change that meets these rules and its confidence bar goes back to the PR's author, often the agent that built it, never to a fix of MergeLoop's own: the author decides what to make of each finding. It comes as a GitHub review that requests changes, with a thread on its line for each finding, saying what to change, so GitHub too holds the PR until the review clears. A review that only asks a human is a comment. Shows it gives the command MergeLoop ran, what it printed and any test patch; a test too large for the GitHub comment is available in full from MergeLoop's review. The author works each thread: fix it and push, or reply with why it doesn't apply, then resolve it. Once every thread is resolved, MergeLoop dismisses its request for changes and the PR goes on to the merge queue with no human approval.
A push gets a new review, which checks each finding sent back, with the author's replies in front of it. It reads only the commits pushed since the head it sent them back on, and reports nothing about the rest of the PR unless those commits break it; after a rebase, or a merge of the base branch into the PR, it reads the whole PR again. A finding still there gets a reply in its own thread, which opens again, never a new comment. A thread whose finding is fixed, or whose reply holds up in the code, is resolved, and a review that sends nothing back dismisses the request for changes of the last. After three rounds that still send findings back, the next review asks a human instead. An author can also reply to a finding in its thread, mentioning the App: the agent reads the code again, answers in the thread, and withdraws the finding, resolving its thread, if the reply holds up.
A human is asked only about what the author cannot settle from the PR: a call on scope, product or design, access it lacks, a change outside the repo, or a kind the workspace always asks about. The agent then requests a review from the code owners of the lines in question, and puts each such finding in a thread on its line, with why it needs a human and what the agent already checked. Someone who can push to the repo answers each in its thread, on GitHub: resolving it says it is fine as it is, and a reply asks for the change it says. The PR's author's own reply is not an answer, as the change is theirs to push. Approving the PR answers every ask as fine, and requesting changes sends them all to the author. A reply that mentions the App, @merge-loop on the hosted MergeLoop, asks the agent to dig further instead: it reads the code again and answers in the thread. Once every ask is answered, the PR goes on, or, with a change asked for, keeps its place while the author pushes it, and the agent reviews it again knowing what was answered. The agent never approves on GitHub itself.
Each kind of change has a bar: how sure the agent must be to send a change back to the author. A lower bar sends back more. The defaults: 60% for security, 70% for dependencies and for public APIs, 80% for everything else, and migrations always go to a human. A change under the bar shows as a note and holds nothing. Confidence alone never replaces the evidence requirement, nor a panel's vote.
Review depth sets how hard the review looks, and so what it spends. Standard, the default, is one reviewer, as above. Quick is one reviewer that reports only what could be wrong, leaving out what it checked and found sound. Deep is for code where one missed bug costs more than the extra review: three reviewers look at once, each fresh, in a checkout of the head of its own, and their findings merge, one per issue: two of the same kind about the same place, a few lines apart, are one. Then a panel of three fresh agents votes on every change, of any kind, that no failing command shows, as it does on a security change at any depth: it goes back to the author when two agree it holds. A deep review spends several times what a standard one does, which shows in what the dashboard's History says the agents spent. A repo sets it in its config file as review.depth.
Require resolved comments, on by default, makes the gate wait for every review thread on the PR to be resolved, whoever started it: the agent's, a human's or a bot's. It is the same rule as GitHub's "Require conversation resolution before merging", so a repo can turn that on in its branch protection too, to hold merges made outside the queue. A repo sets it in its config file as review.require_resolved. The App hears of a resolved thread from the pull_request_review_thread event: an App created before the event was added must subscribe to it on GitHub, or the queue sees the thread resolved only on its next read of the PR. Settings shows how the agent's confidence has held up against the humans' answers, to set each bar by. A workspace can turn automatic review off. It does not review PRs from forks: a human reviews those. A clean rebase by the queue keeps the approval, and so does a push that changes the same lines the same way as the approved head did, as a rebase onto a newer base does. A fix by the queue's agent, or a conflict it resolved, does not: the PR merges once the agent has reviewed that change, on its own.
Required checks that have not finished 3 hours after a rebase or a fix also eject the PR, with no fix, so a check that never finishes does not hold the queue forever. A required check that never reports at all goes sooner: once every check that did report has finished, the PR waits 30 minutes for the rest, then is ejected with a comment naming them. That is a PR that renames a required check, or a workflow that no longer runs it. A head on which no check has reported at all, as when CI has not started, waits out the 3 hours instead. Before it counts a check as never reported, the queue reads the head's GitHub Actions workflow runs: the 30 minutes do not run while one is still queued, waiting for an approval, or running. In a batch, either counts as a failure.
While a PR waits for its checks, PRs labeled meanwhile take their places behind it within a minute, as waiting or held, in the dashboard and in a PR's state. During a rebase or an agent's fix, they join once it is done.
An error outside the PR's job does not eject it. That covers GitHub being down, or slow to answer, as well as a rate limit. The PR keeps its place at the head of the queue, and its job runs again. Only after 30 minutes of such errors is it ejected.
The default branch has a queue, and so does each branch that branches in .mergeloop/mergeloop.yml lists, release say. A labeled PR joins the queue of the branch it targets, and is rebased onto that branch. A PR into any other branch is held, as is a PR stacked by hand on another PR's branch: a stack made with gh stack joins the queue of the branch the whole stack merges into. The queues of a repo take turns, one job each, so a busy branch does not hold another up; they share the repo's git cache, so two never run a job at once. A batch holds PRs into one branch only.
A PR retargeted to another branch leaves its queue, and joins the other branch's at the back. Deleting a branch closes the PRs into it, which leave its queue. A branch taken off the list holds its PRs. The settings in the file, on the default branch, apply to every branch's queue.
A queue that runs CI once per PR merges a few PRs an hour. So when several queued PRs have passed their own checks, MergeLoop tests them together:
- It merges them, in queue order, onto a branch
mergeloop/batch-<PRs>at the base branch, and opens a draft PR from it, so the repo'spull_requestCI runs on the combination. The draft's title is the first PR's title, plus a note, so a PR title check passes. - When the required checks pass, it merges the PRs one by one, as they are: no rebase, and no second CI run. Each merge must leave the base branch at the tree that the batch tested, else it stops.
- It deletes the branch, which closes the draft PR.
When a batch fails, MergeLoop tests half of it next, and so on down to one PR, which runs alone: rebased, with its own checks. That PR is ejected; the rest merge.
A PR runs alone when its own checks have not passed, when it conflicts with the PRs ahead of it, and on a base branch that requires PRs to be up to date before they merge. MergeLoop reads that setting from the rulesets, and from classic branch protection with the App's administration: read permission. Without the permission, every base branch counts as requiring it. An App created before this permission existed must be given it on GitHub.
A stack of PRs, made with gh stack, is one item of the queue: the label on any of its PRs queues the whole stack, and MergeLoop merges it all at once, with GitHub's asynchronous merge. Either every PR of the stack merges, or none does. GitHub merges a stack only when the required checks of each of its PRs passed.
- The stack gets ready as one item, off the queue, like a single PR: a conflict on any layer rebases the whole stack, and it waits for each layer's checks. A failed check is fixed on the lowest layer it failed on, and the layers above it are rebased onto the fix. The layers share one fix budget.
- The stack waits while any of its PRs is a draft or waits for a review.
- The queue page lists a stack's PRs, top first, each with its title, checks and diff, and marks the one that holds it.
- The agent reviews each layer on its own: what it changes over the layer below. A layer that a rebase or a push below moves, with its own changes the same, keeps its approval and is not reviewed again.
- In a batch, the stack is one step: its top PR holds every layer below it.
- Alone, MergeLoop rebases each layer onto the one below it, the bottom onto the base branch, and waits for each layer's checks. A layer that keeps up by merging, over a layer below that did too, has the layer below merged into it instead. Over a rebased layer, a layer is rebased. A failed check is fixed on the layer it failed on, and the layers above it are rebased onto the fix.
- A failed stack is ejected whole.
Every merge goes through the asynchronous merge API, stacked or not. A merge still pending when MergeLoop restarts is waited for, not requested again.
A maintainer queues a PR from a fork with the label, as any other. Its author is not trusted as the repo's own are, so the queue treats it differently:
- It merges the PR only as it was when the label went on. MergeLoop reads that head from the
labeledwebhook, or, without webhooks, from when it first sees the PR. If the author pushes after that, the PR leaves the queue, and a maintainer adds the label again to queue the new commits. - It never tests the PR in a batch, whose draft PR runs CI with the repo's secrets. The PR runs alone, with the fork's own CI.
- It skips getting ready. No agent runs on it, and running alone, it is rebased and its checks run again in the queue anyway.
- It runs no agent on the PR: a plain
git rebase, or merge of the base branch, only. A conflict, or a failed required check, ejects the PR, for its author to fix. - It pushes the rebase to the fork, which GitHub allows while the author allows edits by maintainers. Without that, a PR that needs a rebase waits on hold until they allow it. GitHub does not let the App push to a fork that an organization owns, nor push changes to workflow files to a fork: such a PR that needs a rebase is ejected, for its author to rebase.
- While GitHub waits for a maintainer to approve the fork's workflow runs, the PR waits on hold.
A stack with a PR from a fork is not supported yet.
The agent's review is answered in its threads on GitHub, which MergeLoop hears of from the pull_request_review_comment, pull_request_review_thread and issue_comment events. An App created before those events were added must subscribe to them on GitHub; until then, only an approval or a request for changes answers the review.
The hosted MergeLoop is free on public repos. Private repos cost $15 a month per seat, after a free trial of 14 days that needs no card. Either way, the agent runs on your own model API key.
- Public repos have a fair-use limit across a workspace: 100 PRs joining a queue, and 50 jobs (rebases and fixes), an hour. Over it, waiting PRs go on hold, keep their place, and go on once the last hour is under the limit. A PR already running finishes.
- Private repos need a subscription, started from the dashboard's Settings. Without one, their waiting PRs go on hold, with a comment that links there.
- A seat is a developer whose PR joined a private repo's queue. They take it with their first such PR, and keep it until removed in Settings. A seat added mid-month is charged for the rest of it; a removed one drops off the next bill. A PR authored by an App, such as Dependabot, takes no seat.
A self-hosted MergeLoop has no limits, and bills no one.
GitHub webhooks wake a queue as soon as something changes, but GitHub only delivers them to a public https URL. On localhost, MergeLoop polls.
-
Generate the secret key. It encrypts the GitHub App and the model API keys in the database. Keep it: without it, you must set MergeLoop up again.
export MERGEPATROL_SECRET_KEY=$(openssl rand -base64 32)
-
Start MergeLoop:
docker compose up --build -d docker compose logs mergeloop | grep setup_tokenOr without Docker for MergeLoop itself, with Rust (
rust-toolchain.tomlpicks the version) and Node 24 with pnpm:docker compose -f compose.dev.yml up -d # or any Postgres 17 (cd ui && pnpm install && pnpm build) # the dashboard, which the binary embeds export MERGEPATROL_DATABASE_URL=postgres://mergepatrol:mergepatrol@127.0.0.1:54329/mergepatrol cargo run -p mergepatrol # prints the setup token
Jobs then run as local processes, without a sandbox. If port 8080 is taken, set
MERGEPATROL_BIND=127.0.0.1:<port>andMERGEPATROL_PUBLIC_URL=http://localhost:<port>. -
Open http://localhost:8080 and do the setup in the dashboard:
- Enter the setup token from the logs.
- Create the GitHub App. GitHub creates it with the permissions MergeLoop needs. If the button does nothing, the name is taken, even by an App you deleted: change it.
- Install the App on your organization or account, and choose its repos. GitHub sends you back to sign in.
- In Repos, set up the repos to queue. Each gets a PR that adds its config file: see Repo settings. Merge it, and the repo queues.
- In Settings, add your Anthropic or OpenAI API key. MergeLoop checks it with the provider, then stores it encrypted. Create a key for MergeLoop alone, with a spend limit (for Anthropic, in a workspace with one), so a leaked key costs at most the limit. For OpenAI, you can Sign in with ChatGPT instead, and Codex runs on your ChatGPT plan. MergeLoop signs in the way the Codex CLI does, which OpenAI does not support for other apps and could stop at any time: keep an API key to switch back to.
-
Add the
mergelooplabel to a PR to queue it. MergeLoop creates the queue labels on each repo it queues.
Anyone with a GitHub account can sign in. Each installation of the App is a workspace, with its own repos and its own model API keys. A user sees the workspaces that GitHub lets them access. A session reads them from GitHub again every hour, so a new installation shows up, and a lost one goes, within the hour. A session lasts 30 days from its last use, and ends when GitHub stops taking the user's token: when they revoke it, or after 6 months, when GitHub's refresh token expires.
If you reach MergeLoop at another address than http://localhost:8080, set MERGEPATROL_PUBLIC_URL before you create the App: GitHub sends users back to it, and delivers webhooks there when it is https. To get webhooks on your machine, run a tunnel such as cloudflared tunnel --url http://localhost:8080 and use its URL.
| Variable | Default | What it does |
|---|---|---|
MERGEPATROL_DATABASE_URL |
(required) | Postgres. Compose runs one |
MERGEPATROL_SECRET_KEY |
(required) | Encrypts the stored secrets. 32 bytes, base64. To rotate it, see Rotate the secret key |
MERGEPATROL_SECRET_KEY_FILE |
A file that holds the secret key, instead of MERGEPATROL_SECRET_KEY. Compose uses one |
|
MERGEPATROL_PUBLIC_URL |
http://localhost:8080 |
Where users reach the dashboard |
MERGEPATROL_LABEL |
mergeloop |
The default trigger label. <label>:priority jumps the queue, and ejected PRs get <label>:failed |
MERGEPATROL_POLL_SECS |
60 |
How often to poll GitHub, in case a webhook is lost |
MERGEPATROL_BATCH_SIZE |
8 |
The default batch size: the most PRs tested together. 1 merges one PR at a time |
MERGEPATROL_MAX_JOBS |
4 |
The most rebase and fix jobs that run at once, across every queue |
MERGEPATROL_DATA_DIR |
./data |
The repo caches and the job outputs, with their transcripts. A job's output is kept 7 days |
MERGEPATROL_BIND |
127.0.0.1:8080 |
The dashboard address |
MERGEPATROL_PROVIDER |
anthropic |
The agent's default provider: anthropic or openai |
MERGEPATROL_MODEL |
claude-opus-5-5 |
The agent's default model. Required when the default provider is not anthropic |
MERGEPATROL_EFFORT |
medium |
The agent's default effort |
The hosted MergeLoop uses one public GitHub App, which we create by hand on GitHub, rather than with the setup in the dashboard. A self-hosted MergeLoop can do the same. Set the App's credentials, and the setup token and the App creation are skipped:
| Variable | What it is |
|---|---|
MERGEPATROL_GITHUB_APP_ID |
The App ID |
MERGEPATROL_GITHUB_APP_PRIVATE_KEY |
The private key, PEM |
MERGEPATROL_GITHUB_CLIENT_ID |
The OAuth client ID |
MERGEPATROL_GITHUB_CLIENT_SECRET |
The OAuth client secret |
MERGEPATROL_GITHUB_WEBHOOK_SECRET |
The webhook secret. Optional: without it, MergeLoop only polls |
The App needs the permissions in crates/github/src/setup.rs (manifest), each with the reason it is there, the callback URL <public URL>/auth/callback, the setup URL <public URL>/setup/installed, and the webhook URL <public URL>/webhooks/github, subscribed to the events in WEBHOOK_EVENTS in the same file. An App created before workflows: write joined the manifest must be given it on GitHub, or the push of a rebase or fix of a PR that changes .github/workflows is refused. An App created before the repository event joined WEBHOOK_EVENTS must be subscribed to it on GitHub (the App's settings, Permissions & events, Repository). Until then, a change to a repo's merge settings reaches MergeLoop at its next merge or within 15 minutes.
Generate a new key, and set it in front of the old one, separated by a comma: MERGEPATROL_SECRET_KEY=<new>,<old>. Restart MergeLoop. At startup, it re-encrypts every stored secret with the new key, and logs how many. Then set only the new key, and restart again. Each session's GitHub token is re-encrypted at its next hourly check; a session idle through the whole rotation signs out.
On GCP, a deploy runs the old revision beside the new one for a while, and each must read what the other writes. Add each step as a version of the mergepatrol-secret-key secret, and deploy it:
<old>,<new>: every revision can read both keys, and still encrypts with the old one.<new>,<old>: the new revision re-encrypts with the new key.<new>,<old>again, once the revision from step 1 has stopped: it re-encrypts what that one wrote with the old key meanwhile.<new>.
Each repo sets its queue in .mergeloop/mergeloop.yml on its default branch. MergeLoop reads the file from there, so a PR cannot change its own settings. A setting the file leaves out follows the workspace, so an empty file is valid, and inherits everything. Settings sets the workspace's defaults for the label, batch size, model, effort, task models and CI fixes. They apply at once, with no PR, to every repo that follows them. A workspace that has set none follows the instance: on a self-hosted MergeLoop, the MERGEPATROL_* variables in Environment variables.
version: 1
label: mergeloop
batch_size: 8
agent: # the default model, which every task runs unless it overrides it
provider: anthropic # or openai
model: claude-opus-5-5
effort: medium # low, medium, high, xhigh or max
overrides: # optional: a task's own model, over the default
rebase: # resolving a rebase's conflicts
provider: openai
model: gpt-6.1-sol
fix: # fixing failed checks
model: claude-sonnet-5-5
review: # reviewing a PR, and answering questions about the review
model: claude-fable-5-1
verify:
- cargo check
fix_ci:
enabled: true # an agent fixes failed required checks
attempts: 3 # 1 to 5 fixes of its checks before the PR is ejected
branches: # queues besides the default branch's: exact names
- release
review: # over the workspace's automatic review settings
enabled: true
bars: # how sure the agent must be to send a change back to the author: 50 to 100, or always_ask
security: always_ask
dependencies: 95
require_resolved: true # wait for every review thread to be resolved
depth: deep # quick, standard or deep: how hard the review looksThe agent runs Claude Code for Anthropic's models, and Codex for OpenAI's, with the workspace's key for the provider from Settings. A workspace signed in to ChatGPT runs Codex on its plan instead of the OpenAI key. A file that sets another provider than the instance's default must set its model too.
The agent has three tasks: resolving a rebase's conflicts, fixing failed checks, and reviewing. Each runs the default model unless agent.overrides gives it its own. A task that sets another provider than the default's must set its model too. The dashboard offers each provider's current models by name; the file takes the model's API id, and may name any model the provider serves. A file that still says resolver:, the section's old name, reads as agent:.
The verify commands declared in .mergeloop/mergeloop.yml run after an agent resolution or fix, before the publish. They must pass for that resolution or fix to land.
A repo with no verify commands, whether the setting is omitted or verify: [], gets commands derived from the run steps of its .github/workflows/*.yml and *.yaml workflows that run on pull_request. MergeLoop reads those files from its own checkout, and gives the commands to the reviewer and fixer. The reviewer uses them to build a failing check, and the fixer runs them after its change. The review evidence requirement still applies: MergeLoop must see the evidence command fail itself before sending a non-security change back.
Derived commands guide the agents; they never eject a PR. Only commands declared in verify gate a resolution or fix, so set that list to pin what must pass. GitHub's required checks still gate merging as usual. A workflow step that needs the workflow's context, a secret or another host is left out: the job has the toolchains, not your CI's services. If no usable commands remain, the reviewer follows the repo's docs to build a check; it still needs a failure MergeLoop can reproduce.
A fix runs in the sandbox, like a rebase, while the PR keeps its place at the head of the queue. It may change any file except those under .github/ and .mergeloop/. The agent gets each failed check's output and annotations, and for a GitHub Actions job, the end of its log. The log needs the App's actions: read permission: an App created before this permission existed must be given it on GitHub, or the agent works from the output alone.
A repo queues once its default branch has the file. MergeLoop looks for it in every repo every 5 minutes, and at once when a push webhook adds or removes it. Turn on and Turn off in Settings override the file, until you choose Follow the config file.
The dashboard edits the file through PRs. Set up opens a PR that adds a file with nothing but its version, and the mergeloop agent skill at .agents/skills/mergeloop/SKILL.md and .claude/skills/mergeloop/SKILL.md, so coding agents in the repo queue their PRs rather than merge them. A change to a repo's settings opens one that writes only what differs from the workspace. Both come from the branch mergeloop/config: a second change updates the open PR. A repo that queues gets the PR queued too, as does the PR that updates its skill, so it goes through review and checks like any other PR. The queue uses the new settings once the PR merges.
Update all in Settings brings every queuing repo up to date at once, and says what it did to each. For a repo behind, it opens a PR that rewrites the config file the way MergeLoop writes it now, keeping only what differs from the workspace, and a PR that writes the current skill, then queues them, so they go through the repo's review and checks like any other PR. It skips a repo with no config file, or one that does not parse. It leaves alone an open settings PR that changes the settings or would lose comments, and opens but does not queue a new PR that would drop comments from the file. Run it again as often as you like: a repo up to date gets no PR.
When the file does not parse, the queue pauses, and Settings shows why, until the file is fixed.
MergeLoop serves an MCP server at /mcp, so a coding agent can do anything the dashboard does: queue a PR, take it out, read where it is and what it needs next, read the agent's review and the queue's history, change a repo's settings. Reviews, approvals and comments happen on GitHub, with gh. Every call acts as a GitHub user or bot: MergeLoop puts the labels on, comments, and reviews with their token, so GitHub checks they may.
The hosted endpoint is https://mergeloop.ai/mcp. It uses sessionless streamable HTTP: each POST carries one JSON-RPC message, with JSON responses to requests. GET returns 405 Method Not Allowed. Use the endpoint directly: following a redirect from the old host can break POST requests.
An MCP client that supports OAuth signs in with GitHub in the browser, as the dashboard does:
claude mcp add --transport http mergeloop https://mergeloop.ai/mcpThe client gets a MergeLoop session, and the GitHub token stays on the server. MergeLoop sends the sign-in back only to a loopback address, where native clients such as Claude Code, Codex and Cursor listen.
An agent without a browser, such as one in CI, passes a GitHub token instead: one from gh auth token, a personal access token, or a workflow's GITHUB_TOKEN. A fine-grained token also needs write access to issues or pull requests. MergeLoop never stores the token. It remembers its hash, in memory for 5 minutes, so calls in a row do not each ask GitHub again.
Send the header Authorization: Bearer <token>; no OAuth flow or MergeLoop sign-in is needed. Accepted token prefixes are ghp_, gho_, ghu_, ghs_ and github_pat_.
claude mcp add --transport http mergeloop https://mergeloop.ai/mcp \
--header "Authorization: Bearer $(gh auth token)"An agent with a shell lands a PR with the mergeloop command, which calls the MCP server:
mkdir -p ~/.local/bin && gh release download cli-latest -R agent-labs-dev/mergeloop \
-p "mergeloop-$(uname -s)-$(uname -m).tar.gz" -O - | tar -xz -C ~/.local/bin
mergeloop # the current branch's PR; or 123, owner/name#123, or its URLEach push to main that changes it rebuilds it, for macOS and Linux on Apple silicon, Intel and Arm, into the cli-latest prerelease. Or build it: cargo install --locked --git https://github.com/agent-labs-dev/mergeloop mergeloop-cli.
mergeloop queues the PR, waits until it needs something of the agent, prints that need as JSON, and exits. brief says what to do, such as working the review's threads or fixing what got the PR ejected, and then is the command to run once done, which waits for the next need. The exit code says what kind it is: 0 work for the agent, 10 merged, 11 ejected, 12 waiting on a human, 2 an error. An agent runs it in the background, so it hears of the next need as soon as there is one, and nothing runs between needs. mergeloop call <tool> '<json>' runs any other tool, as a brief names it.
It acts as the GitHub token from gh auth token, or MERGELOOP_TOKEN, against https://mergeloop.ai, or MERGELOOP_URL. The next_need tool is the same loop, for an agent over MCP alone.
A GitHub token reaches only the repos GitHub lets it read: MergeLoop cannot tell which of its workspaces such a token belongs to. The tools marked Sign-in work across a workspace, so they need the sign-in, and show only the workspace's repos you can read. Each tool takes the repo as repo, owner/name, or the workspace as workspace, its installation ID. get_session lists those IDs for a sign-in; with a GitHub token it answers status: "signed_out", with no workspaces, so a token's calls name a repo.
| Tool | Does | Sign-in |
|---|---|---|
get_changelog |
MergeLoop's newest changelog entries | |
get_session |
Who a sign-in is for, and its workspaces: signed_out for a GitHub token |
|
list_queue |
The queues, in the order they take PRs: a repo's, or the workspace's | Without repo |
list_events |
A repo's queue events, newest first | |
list_history |
The PRs that left the queue: merged, ejected, or removed | Yes |
get_history_stats |
The queue's numbers over a window | Yes |
get_review_stats |
How the agent's review did over a window: what became of each change it asked for, reviews to pass, time asked of people | Yes |
list_repos |
The workspace's repos, and whether each queues | Yes |
set_repo |
Turns a repo's queue on or off, or back to following its config file | Or a token with admin on the repo |
pull_status |
Where a PR is, as /api/v1 below says, and its latest events |
|
next_need |
What a PR needs next from the agent that queued it, with what to do: waits for a newer one with after and wait |
|
queue_pull |
Queues a PR, or its stack. priority puts it at the front. Says when it goes on hold whatever the PR, or the agent has no model key |
|
dequeue_pull |
Takes a PR, and its stack, out of the queue | |
get_agent_review |
The agent's review of a queued PR: what it found, what it sent back to the author, and what it asks a human | |
get_repo_config |
A repo's queue settings | |
propose_repo_config |
Opens a PR that changes a repo's queue settings | Yes |
get_repo_skill |
Whether a repo has the current mergeloop agent skill |
|
update_repo_skill |
Opens a PR that updates a repo's mergeloop agent skill |
Yes |
update_repo |
Opens and queues the PRs that bring a queuing repo up to the latest settings and agent skill | Yes |
get_repo_defaults |
What the workspace's repos follow where their config files say nothing | Yes |
set_repo_defaults |
Sets what the workspace's repos follow, at once and with no PR | Yes |
get_settings |
Which model API keys and ChatGPT login the workspace has | Yes |
set_api_key |
Sets a model provider's API key | Yes |
delete_api_key |
Removes a model provider's API key | Yes |
start_chatgpt_login |
Starts a ChatGPT sign-in: a code for the user to enter | Yes |
delete_chatgpt_login |
Removes the ChatGPT login | Yes |
poll_chatgpt_login |
Finishes the ChatGPT sign-in once the user entered the code | Yes |
get_review_settings |
The agent's review settings, and its track record | Yes |
set_review_settings |
Turns the agent's review on or off, and sets how hard it looks and how sure it must be | Yes |
get_billing |
The workspace's plan and seats | Yes |
start_checkout |
A Stripe page to subscribe | Yes |
open_billing_portal |
A Stripe page to manage the subscription | Yes |
free_seat |
Frees a seat | Yes |
Each tool calls a route of the JSON API under /api, which the dashboard uses. A script can call the API itself with the same Authorization: Bearer header:
curl -H "Authorization: Bearer $(gh auth token)" \
https://mergeloop.ai/api/w/<installation ID>/repos/<owner>/<name>/pulls/<number>/statusAn app that shows PRs, such as a PR list in an editor, reads what MergeLoop knows of them from /api/v1, instead of working it out from labels and the config file. It authenticates as the API above does: Authorization: Bearer with the viewer's GitHub token (OAuth, personal access token, or an App installation token) or a MergeLoop session. GitHub decides what the token can read. A repo it cannot read is a 404, the same as a repo that does not exist. A repo it can read is a 200, also when MergeLoop does not run there.
curl -H "Authorization: Bearer $(gh auth token)" \
https://mergeloop.ai/api/v1/repos/acme/widgets/pulls/42
curl -H "Authorization: Bearer $(gh auth token)" \
"https://mergeloop.ai/api/v1/repos/acme/widgets/pulls?numbers=42,43,44"The second form reads up to 100 PRs of one repo, as { "pulls": [...] } in the order asked. Each PR reads:
{
"repo": "acme/widgets",
"number": 42,
"url": "https://mergeloop.ai/acme/widgets/pull/42",
"mergeloop": {
"state": "on",
"installed": true,
"has_config": true,
"override": null,
"config_error": null,
"hold": null,
"missing_key": null
},
"queue": {
"trunk": "main",
"state": "held",
"hold": {
"reason": "review_required",
"layer": null,
"message": "waiting for a required review"
},
"priority": false,
"position": 3,
"stack": null,
"batch_pr": null
},
"merged": null,
"ejected": null
}| Field | Says |
|---|---|
url |
Where to open the PR: its lane in the dashboard while queued, the repo's settings while MergeLoop is off there, and the PR on GitHub otherwise |
mergeloop.state |
on, off (no App on the repo, no config file, or turned off), or paused (the config file does not parse: config_error says why) |
mergeloop.override |
on or off when turned so in the dashboard, whatever the config file says. null follows the file |
mergeloop.hold |
Why any PR queued now goes on hold, before it is queued: { reason, layer, message } as in queue.hold, with reason no_subscription (a private repo on the hosted MergeLoop, without a subscription) or fair_use_limit. null when nothing about the repo holds it |
mergeloop.missing_key |
The agent's tasks on this repo that the workspace has no model key for: { tasks, message, settings_url }, each task { task, provider } with task rebase (resolving a rebase's conflicts), fix (fixing failed checks) or review. Without the key, a conflict or a failed check ejects the PR. settings_url is where to add the key. null when the workspace has a key for each task |
queue |
null unless queued. state is waiting, running or held. position counts from 1 in the order the queue takes PRs. stack is { number, layers } for a stack. batch_pr is the draft PR testing it, while it runs in a batch |
queue.hold.reason |
draft, review_required, changes_requested, workflow_approval (a fork PR's runs wait for approval), fork_edits_off (a fork PR behind its trunk that does not allow edits by maintainers), no_queue (it targets a branch without a queue), no_subscription, fair_use_limit, reviewing (the agent's automatic review runs), review_asks (that review waits for a human's answers) review_changes (it sent findings back to the author, or a human answering it asked for changes, and it waits for the author) or unresolved_threads (review threads on it are unresolved, and the gate waits for them all). layer is the stack's PR that holds it |
merged, ejected |
How it last left the queue: { at, sha } or { at, reason }. Both null while it is queued, and when it left otherwise |
Every response has an ETag: send it back as If-None-Match for a 304 when nothing changed. Each caller, by its token, has 5,000 calls an hour, a 304 included, which x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-used and x-ratelimit-reset (Unix seconds) report. Past the limit, a 429 has a retry-after, in seconds. So does a 503 when GitHub's own rate limit is used up.
A rebase job runs the repo's code (the verify commands) and an agent with a shell. MergeLoop treats it as untrusted:
- The orchestrator holds the GitHub App key and the queue. It never runs repo code.
- Each job runs
mergepatrol jobin its own container, from the job image. It gets a GitHub token that can only read the one repo, and revokes it once it has fetched the repo. It returns a git bundle. - The workspace's model API keys and ChatGPT login stay in the orchestrator, which renews the login. A job calls its provider's API through the orchestrator's proxy, at
/anthropicfor Claude Code or/openaifor Codex, with a token that works only while the job runs, and for a set number of requests. - The orchestrator checks the bundle before it publishes: the result must sit on the base branch, and change no file that the PR did not change.
In production the job containers use gVisor (runsc) on a network that cannot reach the cloud metadata server, private ranges, or the host. deploy/sandbox-host.sh sets that up on a Linux VM. Then set:
| Variable | Default | What it does |
|---|---|---|
MERGEPATROL_EXECUTOR |
local |
docker runs each job in a container. agent-sandbox runs it in a GKE sandbox: see On GCP. local runs it as a process, without isolation, for development |
MERGEPATROL_JOB_IMAGE |
mergepatrol-job:latest |
The job image (docker build --target job) |
MERGEPATROL_JOB_RUNTIME |
Docker's default | runsc for gVisor |
MERGEPATROL_JOB_NETWORK |
Docker's default | mergepatrol-jobs, from deploy/sandbox-host.sh |
MERGEPATROL_JOB_RESOLV_CONF |
Docker's default | A file on the Docker host, mounted as the jobs' /etc/resolv.conf. /etc/mergepatrol/job-resolv.conf on the sandbox network |
MERGEPATROL_JOB_PROXY_URL |
MERGEPATROL_PUBLIC_URL |
Where jobs reach the orchestrator: its model API proxy, and a GKE sandbox's link. http://10.213.0.1:8081 on the sandbox network |
MERGEPATROL_JOB_PROXY_BIND |
None | A second listener that serves only what jobs use, for jobs that cannot reach the dashboard's. 0.0.0.0:8081 in compose.yml |
MERGEPATROL_JOB_MEMORY |
4g |
Memory limit per job |
MERGEPATROL_JOB_CPUS |
2 |
CPU limit per job |
The job image has git, the Claude and Codex CLIs, Rust, Go, Python with uv, and Node with pnpm and Bun, for the verify commands and for the agent, which regenerates a generated file that conflicts with the repo's own tools.
No release has been published yet, so for now build both images from source with docker compose up --build. Once releases start, each one publishes both images, tagged with its version and latest: ghcr.io/agent-labs-dev/mergeloop, the orchestrator, and ghcr.io/agent-labs-dev/mergeloop-job. To run a release instead of building, set MERGEPATROL_JOB_IMAGE=ghcr.io/agent-labs-dev/mergeloop-job:<version>.
deploy/gcp/bootstrap.sh sets up one environment in a GCP project with billing linked: the GKE Autopilot cluster mergepatrol, Cloud SQL Postgres, and a load balancer for the public URL. It deploys with deploy/gcp/cloudbuild.yaml, which runs deploy/gcp/deploy.sh. The manifests are under deploy/gcp/k8s.
The orchestrator runs as the Deployment mergepatrol, one instance, with Cloud SQL through a proxy sidecar and its secrets from Secret Manager. Only the instance that holds a lock in Postgres runs the queues, so the new instance of a deploy waits until the old one has stopped. On SIGTERM, an instance lets go of the lock at once.
Each job runs in its own gVisor sandbox, through Agent Sandbox: the orchestrator claims one from the warm pool mergepatrol-job, which keeps one sandbox ready, so a job starts in about a second. Jobs past the first start cold, on a new node, in 1 to 2.5 minutes. A sandbox has no service account and no DNS. Its network policy lets it reach two things: the orchestrator's job port, and the egress proxy mergepatrol-egress. It can't reach the metadata server, the rest of the cluster, the VPC, or the internet directly. The egress proxy is Squid, at 34.118.224.100:3128, set as the sandbox's HTTPS_PROXY. It opens tunnels to port 443 of the hosts in deploy/gcp/k8s/squid.conf, and refuses every other host:
- GitHub, to fetch the repo and revoke the job's token.
- The package registries the verify commands use: npm and Yarn, PyPI, the Go module proxy and its bucket, crates.io, and rustup's toolchains.
A job that needs another host fails until the host is added there. The proxy logs every tunnel it opens or refuses (kubectl -n mergepatrol-jobs logs -l app=mergepatrol-egress). The allowlist goes by host name. So it doesn't stop a job that brings its own credentials from pushing to GitHub or publishing to a registry. It also doesn't stop a job from writing to a bucket that anyone can write to on storage.googleapis.com, where the Go proxy keeps large modules.
The orchestrator hands the sandbox its link, http://<orchestrator pod IP>:8081/jobs/<token>, over HTTP, since a claim that sets env would skip the warm pool. With that link, the job fetches its spec once, uploads its result in chunks, and reports that it is done.
This is the hosted MergeLoop at https://mergeloop.ai. It runs the public App Merge Loop, which any account can install; each workspace brings its own Anthropic or OpenAI API key. deploy/gcp/hosted-app.sh creates the App once and stores its credentials in Secret Manager.
| Variable | What it is |
|---|---|
MERGEPATROL_SANDBOX_NAMESPACE |
Where the jobs' sandboxes run, with MERGEPATROL_EXECUTOR=agent-sandbox. mergepatrol-jobs by default |
MERGEPATROL_SANDBOX_WARM_POOL |
The SandboxWarmPool that sandboxes are claimed from. mergepatrol-job by default |
MERGEPATROL_POSTHOG_KEY |
The PostHog project key. Turns on product analytics (below) |
MERGEPATROL_POSTHOG_HOST |
The PostHog host. https://us.i.posthog.com by default |
Billing runs through Stripe, and is off unless the three MERGEPATROL_STRIPE_* variables below are set. In Stripe, create:
- A product, with a recurring price of $15 a month per unit:
MERGEPATROL_STRIPE_PRICE. MergeLoop keeps each subscription's quantity at the workspace's seats. - A webhook endpoint at
<public URL>/webhooks/stripe, forcheckout.session.completedandcustomer.subscription.created,.updatedand.deleted: its signing secret isMERGEPATROL_STRIPE_WEBHOOK_SECRET. - The customer portal's settings, where a customer adds a card, sees invoices, or cancels.
A workspace's first subscription starts with Stripe's trial, without a card. One that has no card when the trial ends is cancelled.
| Variable | What it is |
|---|---|
MERGEPATROL_STRIPE_SECRET_KEY |
A secret or restricted key: Checkout sessions, customer portal sessions, and subscriptions (write) |
MERGEPATROL_STRIPE_WEBHOOK_SECRET |
The webhook endpoint's signing secret, whsec_… |
MERGEPATROL_STRIPE_PRICE |
The price, price_… |
With MERGEPATROL_POSTHOG_KEY, the hosted MergeLoop sends product analytics to PostHog. Without it, as when self-hosted, it sends nothing, and the dashboard does not load PostHog.
- A person is a GitHub login. The dashboard identifies the signed-in user, and the server sends
signed in. - A
workspacegroup is an installation of the App, keyed by its installation ID, with the account's name. The dashboard groups what the user does under the workspace in view. - Each queue event goes to its PR's author and workspace as
pr <kind>, such aspr mergedorpr ejected, with the repo, trunk and PR number. When a PR leaves the queue, the event also hasqueue_seconds, the seconds in each step (waiting_seconds,held_seconds,rebasing_seconds,checks_seconds,fixing_seconds), and, forpr ejected, thereason:fork,rebase-failed,checks-failed,fix-failed,base-moved,merge-rejected,checks-timed-outorchecks-missing. - Installing the App sends
app installed, for the person who installed it, and uninstalling itapp uninstalled. - The dashboard sends
api key setandapi key removed, with theprovider, andrepo config proposed. - Before sign-in, the landing page and the
/blogarticles send PostHog's$pageviewand autocaptured clicks under an anonymous id, with no person profile. The articles load PostHog's script themselves, as they are plain HTML outside the dashboard. A reader keeps the same id from an article to the landing page, and signing in joins it to their login.
| Crate | What it does |
|---|---|
core |
The queue and its state machine. Pure logic, no I/O |
github |
GitHub App auth, the REST and GraphQL calls, and publishing commits through the Git Data API |
git |
Local git: fetch, worktrees, rebase |
resolver |
The Resolver trait, and its implementations: Claude Code for Anthropic, Codex for OpenAI |
executor |
Runs a rebase job: as a local process, or in a container. The job protocol |
store |
The Store trait, Postgres, and the encrypted secrets. Tests need the dev Postgres from compose.dev.yml |
web |
The HTTP server: JSON API and the dashboard |
analytics |
Product analytics in PostHog, for the hosted MergeLoop |
mergepatrol |
The binary: the orchestrator, and mergepatrol job for the sandbox |
cli |
mergeloop, the command an agent lands a PR with: a client of the MCP server |
GitHub's merge queue also tests PRs together before they merge. What MergeLoop adds:
| MergeLoop | |
|---|---|
| A PR that conflicts with its base | An agent resolves the conflict, and the PR stays in the queue |
| A required check that fails | An agent commits a fix on the PR, up to fix_ci.attempts times, before the PR is ejected |
| A batch that fails | Half of it is tested next, and so on down to the one PR that fails. The rest merge |
| Stacked PRs | A gh stack stack queues and merges as one item |
| CI | Runs on the repo's pull_request workflows, on a draft PR per batch. No merge_group trigger to add |
| Queueing a PR | A label, so people and coding agents queue the same way |
For Mergify, Graphite, Aviator and Trunk as well, see mergeloop.ai/compare.
See CONTRIBUTING.md. Report vulnerabilities privately, as SECURITY.md describes.
Apache-2.0