Inspiration

Resolving merge conflicts is a headache. I love working with teammates, but I hate stopping to untangle conflict markers and guess what someone else meant. The research backs up the feeling: a study of 143 open source projects found that 75.23% of merge conflicts needed a developer to reason about program logic, and code tied to a merge conflict was 2 times as likely to have a bug (Brindescu, Ahmed, Jensen and Sarma, Empirical Software Engineering, 2019).

AI merge tools already exist, but most of them write the model's answer straight into your file and leave the checking to you. A merge that compiles but quietly drops a teammate's change looks exactly like a good one. I wanted a tool where the AI proposes and something I can see decides.

What it does

Merge Desk is a web app (desktop and phone) for GitHub pull requests that can't merge. Anyone can sign in with GitHub, install the Merge Desk GitHub App on repositories they can push to (public or private), and use it on those pull requests. Judges can also replay real recorded runs at /demo with no account.

  1. See the conflict as two intentions. Each side is summarized in plain language ("ours renames fetchUser", "theirs adds a retry on 429") next to a side-by-side diff.
  2. Pick how to resolve it. Gemini offers options (keep ours, combine both, keep theirs), each saying what it keeps, what it drops and why. Dropping someone's work needs a press-and-hold to confirm.
  3. Watch the checks run. The proposed merge is written into a fresh cloud sandbox with its network switched off, and three checks run live on screen.
  4. HELD or VERIFIED. If any check fails, the merge is HELD, the reason is shown and nothing is pushed. There is no Land button. You can steer Gemini with one plain sentence and retry.
  5. Land. Only a VERIFIED merge can land. Land re-checks that nobody pushed since the run, then adds one merge commit to the pull request's own branch: never to main, never a force push. Every decision is recorded on the pull request.

The rule fits in one line:

$$\text{VERIFIED} = \text{it parses} \;\land\; \text{the chosen option was honored} \;\land\; \text{your tests pass}$$

The model never decides. Its answer is validated against a schema, then it has to survive the same checks a person's merge would.

How we built it

  • Next.js 16 and React 19 on Vercel, TypeScript, Tailwind CSS 4, shadcn/ui on Radix, TanStack Query and Pierre Diffs for the code views. One responsive layout: a two-pane desk on desktop, a full-height sheet with the action pinned in thumb reach on a phone.
  • Gemini (gemini-3.5-flash-lite through the Google GenAI SDK) reads both sides, writes the intents and options, and proposes the merge as structured output that is validated with Zod before use.
  • Vercel Sandbox runs each merge in a fresh microVM: clone at the exact commits, network set to deny-all before any candidate code runs, tests run as a separate non-root user, and the sandbox is stopped afterwards.
  • A deterministic "choice honored" check compares the merge line by line against the option you picked, so a merge that silently drops a side is caught even when it compiles.
  • A GitHub App (Octokit, contents and pull requests only) reads pull requests and writes the merge commit. On every request, GitHub is asked whether the signed-in account can push to that repository; the session holds the account's numeric GitHub id, so a renamed login can't be reused. The branch update only succeeds on the exact head that was checked, so it can only move forward.
  • Other people's repositories: the sandbox installs the pull request's dependencies with npm ci --ignore-scripts while the network is still open, cuts the network, then runs the repository's own npm test as a non-root user. A merge that edits package.json or the lockfile, a missing lockfile or test script, or a pretest/posttest script means HELD, never VERIFIED. Private repositories are fetched with a short-lived, read-only token for that one repository.
  • Proof you can check without an account: /demo replays real recorded runs, /judge is a short guided tour, and /api/health is public.
  • Engineering guardrails: GitHub Actions CI (format, lint, types, unit tests, build, secret and em-dash scans), Vitest unit tests, Playwright and axe end-to-end checks on desktop and phone, a scheduled production probe, and a daily usage throttle on live runs (overall and per person), counted from the sandboxes themselves.
  • Built with AI coding agents (Claude Code and Codex), planned with the Devpost Learn skill pack, with a rule that a different model reviews every change that writes to GitHub.

Challenges we ran into

  • A missing exit code read as a pass. The sandbox SDK reported a killed test command's missing exit code as 0. Merge Desk now reads the raw exit code, and a missing one means HELD.
  • A stale base hid a real conflict. I pointed Merge Desk at two of its own conflicting pull requests and it said there was no conflict. GitHub's base.sha on a pull request is where the base was when the PR was opened, not where it is now. Every decision now reads the base branch's current tip, and the next run found the conflict.
  • GitHub comments have no compare-and-swap. Two writes to the decision record could race, so writes take a lock stored as a Git ref per pull request. A write GitHub doesn't confirm keeps its lock for a human instead of guessing.
  • Running the app's own tests safely. A review round caught that npm run would read a .npmrc or node_modules planted in a pull request and could fake a pass, so the trusted test runner now runs by absolute path from a trusted dependency snapshot, never through npm.
  • The model I planned on was overloaded. It answered "high demand" errors to analysis-sized requests, so I measured alternatives and moved to gemini-3.5-flash-lite.
  • Opening it to everyone. A second-model review of the change found that a burst of requests could all pass the daily limit before any sandbox existed, so a run now boots and recounts its sandbox before Gemini is asked. It also found that the base branch could move while Land was writing. I tested GitHub's atomic multi-ref update on scratch branches: it can lock the base, but GitHub refuses it on protected branches like most mains. So Land tells you when the base moved and to run again instead of blocking.

Accomplishments that we're proud of

  • The whole loop works on real pull requests in my own repository: analyze, run, HELD or VERIFIED, steer, and a real Land.
  • 15 recorded runs from real sandboxes and real Gemini answers: 3 VERIFIED, 12 HELD (6 of them steered retries) and 3 real Lands, each taking between 10.3 and 12.2 seconds from start to verdict. Anyone can replay them at /demo with no sign-in.
  • Merge Desk resolved a real conflict in its own repository, with its own unit tests passing inside the sandbox.
  • Nothing ever shows as success unless GitHub confirmed it: an unconfirmed write shows UNKNOWN, and a refusal says why.

What we learned

  • An AI merge is easy to produce and hard to trust. The valuable part is the evidence around it, shown before anything lands.
  • Planning documents written through flipped interaction (the agent interviews me, I decide) kept a fast build pointed at one clear kernel.
  • Agents build best inside rules they can't talk their way around: numbers only from a measured file, claims only for what production health reports, and a second model reviewing anything that writes.

What's next for Merge Desk

  • Test runners beyond Node and npm, so live mode can check merges in any repository (today another repository's tests run only for Node projects with a package-lock.json).
  • A git merge driver for the terminal that runs the same checks.
  • A replay test over real historical merges to measure how often an unchecked AI merge drops a side.
  • An installable app for phone and desktop.

Judge Note

Judge's guide located at https://merge-desk-swart.vercel.app/judge

Built With

Share this project:

Updates

Submission history