> ## Documentation Index
> Fetch the complete documentation index at: https://panopticon-cli.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Auto-Merge

> Opt-in scheduled auto-merge for solo Overdeck workflows

# Auto-Merge

When the Flywheel orchestrator is trusted to ship work without a human clicking
MERGE, you can flip a single toggle and Overdeck will schedule eligible PRs
for merge with a short cooldown window. During the cooldown an audible
announcement fires, a cancellable banner appears in the dashboard, and the
merge runs through the exact same code path as the MERGE button when the
cooldown expires.

<Warning>
  **Solo Overdeck workflows only.** Auto-merge moves the merge-consent decision
  from per-PR click to a configuration toggle. That trade is appropriate when the
  operator is the only human in the loop. In a shared Overdeck instance it
  shifts consent away from the person who would normally click MERGE — not
  appropriate for team workflows.
</Warning>

## Enabling

Auto-merge is gated by the global `require_uat_before_merge` setting in the
dashboard's `app_settings` store. Set it to `false` to turn auto-merge on. The
merge train must be on as well (`merge_train_enabled: true`): while it is off,
`POST /api/merge-train/auto-merge/schedule` refuses with
`412 Merge train is disabled` and the executor skips its tick.

There is no CLI for these settings. Change them in the dashboard or through
`/api/merge-train/config`:

```bash theme={null}
# inspect current state
curl http://localhost:3011/api/merge-train/config

# turn auto-merge on (a POST needs the dashboard's internal token)
curl -X POST http://localhost:3011/api/merge-train/config \
  -H 'content-type: application/json' \
  -H "x-overdeck-internal-token: $(cat ~/.overdeck/internal-token)" \
  -d '{"require_uat_before_merge": false}'

# turn it back off
curl -X POST http://localhost:3011/api/merge-train/config \
  -H 'content-type: application/json' \
  -H "x-overdeck-internal-token: $(cat ~/.overdeck/internal-token)" \
  -d '{"require_uat_before_merge": true}'
```

`GET` returns all three settings: `auto_pickup_backlog`,
`require_uat_before_merge`, and `merge_train_enabled`. `POST` changes only the
keys in the body, and each value must be a boolean.

The same toggle is exposed in the dashboard's Flywheel header as a checkbox.
The default is `true` (auto-merge OFF — operator UAT required before merge).

When the toggle is `false`, the dashboard schedules auto-merge for any issue
that satisfies the eligibility predicate. No flywheel run is needed: every
60 seconds, on the UAT batch-train reconciler tick, the primary dashboard walks
each project whose merge train is on and takes its open PRs with green checks
on a `feature/<id>` branch. A strike PR (`strike/<id>`) is never scheduled: the
operator merges it (PAN-3973). For each one it checks, in order:

1. the issue is open in the tracker (the cached tracker row) and opted in to
   auto-merge: its `auto-merge` / `hold-for-uat` label, then the project
   default, then this toggle (no forge read);
2. the merge gate (`evaluateIssueMergeGate`) says the PR is ready. Approval is
   a trusted `overdeck-verdict` marker comment whose `sha=` is the PR head, or
   a trusted reviewer's standing GitHub review approving that exact commit, so
   a repo without branch protection still qualifies. "Trusted" is the marker
   rule: `OWNER`, `MEMBER` or `COLLABORATOR`, or the identity Overdeck posts
   as. The App's bot is the installed App's own (`<slug>[bot]`, from the
   `app-slug` file beside its credentials in `~/.overdeck/github-app/`, else
   `GET /app`), matched as a GitHub `Bot` account, never by login alone. Its
   reviews arrive as `CONTRIBUTOR` and count through that match. With the
   GitHub App configured, an approving marker counts only from the App's bot:
   agents hold the operator's `gh` credentials and could post one as the
   owner. If the App's slug cannot be read, no bot approval counts. A review from any other account never counts, and each reviewer's
   latest review stands: a later "Request changes" or a dismissal withdraws an
   approval. An approval of an older commit, or a marker without `sha=`, never
   counts. A GitLab MR needs a named approver in `approved_by` (what
   `glab mr approve` records); a green, conflict-free MR nobody approved is not
   approved, even where the project requires zero approvals;
3. the issue's latest auto-merge entry allows a new one.

It schedules through the same door as `POST /api/merge-train/auto-merge/schedule`.
GitLab projects are skipped, with one log line. `OVERDECK_DISABLE_AUTO_MERGE=1`
stops the scheduler as well as the executor.

Each entry records the PR head it was scheduled for. A pending or merging entry
is left alone. A cancelled entry stops the issue for good, whatever it is
pushed next: only re-scheduling it through the endpoint starts it again. A
failed entry (the merge was attempted, including the retry circuit breaker)
holds for that PR head only: a new push re-arms the issue. A blocked entry (the
executor refused it before merging, for example because checks went red or the
head moved during the cooldown) re-arms as soon as the merge gate passes again.
An entry written before heads were recorded never re-arms by itself. The insert
re-checks the latest entry, so a cancel that lands while the scheduler is
reading the forge sticks.

When the cooldown ends, the executor reads the tracker live and blocks the
entry if the issue was closed, and blocks it if the PR head moved. An entry
with no recorded head is blocked, never merged unpinned. The merge is bound to
the approved head and to the PR the gate judged:

* the final gate check reads the `feature/<id>` PR fresh (no facts cache), and
  the merge refuses when the PR it would land is not that PR;
* a clean PR merges directly only when its live head is the approved head,
  pinned to it (`gh pr merge --match-head-commit`, the App merge's `sha`,
  `glab mr merge --sha`);
* a PR that needs a rebase is rebased by the server only, and only when the
  worktree and `origin/feature/<id>` are both at the approved head. The server
  rebases exactly that commit in its own detached worktree, created at the
  approved head under a temporary path the work agent never uses and removed
  afterwards, so nothing the agent commits in its worktree meanwhile can
  become the rebased head. It moves the work agent's worktree to the result
  for local verification (only while that worktree is still at the approved
  head), pushes the result by sha with a lease on the approved head, and pins
  the merge to it. It never hands the rebase to the work agent, whose push
  would merge unreviewed. The pushed head is new, so if verification or the
  merge then fails, the PR needs a fresh review before it can merge
  automatically again;
* polyrepo merge sets and branches that track `.planning/` files are left to
  the operator: an automatic merge refuses them;
* a git read that fails while checking where the merge may start (a `fetch`
  or `rev-parse` failure) is retried within the merge retry budget instead of
  failing that head for good. A head that was read and differs fails it.

A commit made or pushed after the approval therefore fails the merge instead
of landing code nobody approved.

## Per-project default

Every registered project's HOME tab includes a **Project settings** panel, even
when the project has no in-flight pipeline issues. Its auto-merge control offers
**⚡ Auto**, **🔒 Hold for UAT**, and **Global default**, and the collapsed row
shows the current auto-merge and swarming values at a glance. The project default
applies only to issues without an explicit per-issue auto-merge setting.

Changes are saved through `POST /api/projects/:key/auto-merge-default` and stored
in `~/.overdeck/projects.yaml`.

<Note>
  **Auto-merge vs. UAT batch trains.** This toggle controls *per-issue* scheduled
  auto-merge — Overdeck merging one eligible PR at a time without a human click.
  The separate `merge_train_enabled` setting enables [UAT batch
  trains](/features/merge-batches), where ready features are assembled into one
  tested batch you promote together. Auto-merge ships individual PRs on a
  cooldown; batch trains assemble and promote a tested bundle. Auto-merge also
  needs the merge train on: with `merge_train_enabled` off, nothing is scheduled
  and nothing is merged.
  Leave `require_uat_before_merge` at its default (`true`, auto-merge OFF) when you
  want a human to UAT each batch before promoting it.
</Note>

## Per-issue override

An issue can carry one of two tracker labels that take precedence over both the
project default and the global setting:

* `auto-merge`: this issue auto-merges when eligible, whatever the project or
  global setting says.
* `hold-for-uat`: this issue waits for a human batch review, whatever the
  project or global setting says.

Add or remove the label in the tracker (for GitHub, `gh issue edit <n> --add-label auto-merge` or `--remove-label auto-merge`). The dashboard reads it
from its cached tracker issues, so a change shows up after the next tracker
refresh.

Only you set these labels. Overdeck agents and the Flywheel cannot add or
remove `auto-merge`, `hold-for-uat` or `released`: their `gh` refuses the write
and tells them to ask you.

The dashboard's auto-merge indicator (the ⚡ Auto / 🔒 Hold control on an
in-flight issue) shows the result of this resolution: issue label, then project
default, then global setting. It is read-only. Change the policy at the tier you
mean, not by clicking the indicator.

## Eligibility

Overdeck stores no merge-ready flag. Eligibility is computed from the PR's live
facts each time it is checked (`isAutoMergeEligible` in
`src/lib/cloister/auto-merge-eligibility.ts`). A PR is eligible for auto-merge
only when **all** of these are true:

* The PR is open (not closed, not a draft, not already merged)
* The PR is approved on its exact head commit (a trusted reviewer's standing
  GitHub review of that commit, or a trusted verdict marker whose `sha=` names
  it, posted by the GitHub App's bot when the App is configured; on GitLab, a named approver in `approved_by`), and no review or marker
  requests changes
* The forge reports CI checks on the PR head, and all of them passed. Pending
  checks, failing checks, and no reported checks each make the PR ineligible.
  When the project runs `verification.tests: ci`, the CI test job must also
  have run and passed on that head.
* The forge reports the PR as `mergeable` (no merge conflicts, branch
  protection satisfied). A PR whose mergeability the forge has not computed
  yet is not eligible.
* The issue is not held for UAT (its label, then the project default, then the
  global setting; see above)
* None of the **blocker labels** are present on the issue:
  * `needs-design`
  * `needs-discussion`
  * `do-not-merge`

Eligibility is re-checked when the cooldown expires. A PR that became
ineligible during the cooldown (e.g. someone added a `do-not-merge` label
while the timer was running) gets surfaced as a blocked entry instead of
merged.

## Cooldown

Cooldown is fixed at **5 minutes** of wall-clock time from when the
scheduler schedules the merge to when the executor fires. The countdown is
persisted in SQLite, so it survives dashboard restarts — if the dashboard
sleeps through the deadline, the executor still picks the entry up on the
next tick and re-validates eligibility before firing.

The cooldown is not configurable today; it's a deliberate single-knob value
that gives the operator a predictable cancel window.

## Cancelling

Three ways to cancel an in-flight auto-merge during its cooldown:

1. The Pending auto-merges card on the dashboard's Flywheel page has a
   Cancel button.
2. `DELETE /api/merge-train/auto-merge/:id` against the dashboard. The route
   needs the dashboard's internal token (the `x-overdeck-internal-token`
   header) or a dashboard session.
3. The CLI:

   ```bash theme={null}
   pan merge cancel PAN-123
   ```

   This calls the DELETE endpoint, removes the entry from the pending list,
   drops any waiting entry for the issue from the project merge queue, and
   emits an `auto-merge cancelled for PAN-123` TTS announcement so the
   operator knows the abort took effect.

Cancellation only succeeds during the cooldown window, before the executor
has transitioned the row to `merging`. Once the executor has started running
the merge there is nothing to cancel — at that point it's the same code path
as clicking MERGE.

## Queue interaction

The dashboard runs at most one merge at a time per project. An automatic
merge never joins the project merge queue: when another merge holds the slot,
`triggerMerge()` defers it, and the entry is requeued with a short backoff
(`scheduledMergeAt` bumped by 60 seconds, status reverted to `pending`) without
using its retry budget. The next executor tick re-runs every check (the gate,
the head, the labels, the tracker) and tries again. A queued merge would start
later with none of them. Stale `merging` rows are prevented by this requeue path; the executor
never leaves a row stuck mid-flight when `triggerMerge()` returns a
non-terminal status.

## Inspecting state

```bash theme={null}
# Pending entries currently in cooldown
curl http://localhost:3011/api/merge-train/auto-merge/pending

# Rows that failed or were blocked during the executor tick
curl http://localhost:3011/api/merge-train/auto-merge/problems
```

The Pending auto-merges card on the dashboard's Flywheel page shows the pending
list with a countdown and a Cancel button. The dashboard does not show the
problems list; read it from the endpoint.

## Failure handling

When the executor fires and the merge fails (e.g. surface-level rebase
conflict raised at merge time), the row is moved to `failed` with the failure
reason captured, a TTS announcement plays, and the orchestrator surfaces the
entry as an `investigate` suggestion in its next tick snapshot. The operator
can then either fix the underlying problem and reschedule, or cancel the
failed entry to clear it.

## Related

* `GET` / `POST /api/merge-train/config`: read and write
  `require_uat_before_merge`, `merge_train_enabled`, and `auto_pickup_backlog`
  (no CLI)
* `pan merge cancel <id>` — cancel a pending auto-merge
* `roles/flywheel.md` — the orchestrator's contract; references the toggle
  when deciding whether to call the scheduler vs. emitting a `merge`
  suggestion for human action.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.