---
name: agent-merge
description: Manage the full lifecycle of a pull request through automated check-ins. Use when the "agent merge" feature is enabled on a workspace to rebase, address review comments, fix CI failures, and merge the PR. Triggers on automated agent merge ticks.
---

# Agent Merge — Automated PR Lifecycle

Drive a pull request to merge. This is not a status check — actively work through every requirement until the PR is merged or you hit something that requires human intervention.

## Merge conditions

A PR is ready to merge when **all three** of these conditions are satisfied simultaneously:

| Condition | What it means | How to check |
|-----------|--------------|--------------|
| **Reviews — threads handled** | Every unresolved review thread has been addressed on its merits, replied to on GitHub, and resolved. Code changes alone do **not** satisfy this — a thread is handled when the reply+resolve step succeeds without error. | GraphQL `reviewThreads` query (see below) |
| **Checks — CI is green** | All required status checks and CI jobs have passed on the current HEAD. Cloud Code Review only keeps Checks pending when the status script reports an active current-head CCR request/run; optional/unrequested CCR is informational and must not block merge. | `bash src-tauri/app-skills/agent-merge/check-merge-conditions.sh <owner> <repo> <pr_number>` |
| **Mergeable — no conflicts, current** | The branch is mergeable with no conflicts. If the repo requires the branch to be up to date, it must not be behind base. | `GH_PAGER="" gh pr view <number> --repo <repo> --json mergeable,mergeStateStatus` |

Top-level PR comments are still important feedback: read them and action anything useful, but do **not** treat them as a strict merge gate because GitHub does not provide a resolve state for them.

Your job is to drive all three conditions to "satisfied" as efficiently as possible — working on them concurrently, not sequentially, while still reading non-gating top-level feedback when it is useful.

**Status script** — use `src-tauri/app-skills/agent-merge/check-merge-conditions.sh` to check all three conditions in a single call:

```bash
bash src-tauri/app-skills/agent-merge/check-merge-conditions.sh <owner> <repo> <pr_number>
```

It outputs a structured report (✅/❌/⏳ per condition) and exits **0** when all deterministic/actionable merge conditions are met, **1** when one or more of those conditions are not yet satisfied (but all checks ran successfully), and **2** if the script itself fails (for example usage error, missing/invalid auth, or GitHub API failure). Treat exit code **2** as an error/unknown state — surface it or retry later, and do **not** treat it as "not ready to merge."

Important nuance: the script also prints GitHub's **final merge signal** separately. Treat `mergeStateStatus: CLEAN` as the definitive signal that GitHub currently allows merge. `reviewDecision` is informational: it may still say `REVIEW_REQUIRED` even when GitHub will accept `gh pr merge`. If `mergeStateStatus` is `CLEAN`, attempt the merge and let GitHub enforce branch protection. Only treat approval as a stopping point when `mergeStateStatus` is `BLOCKED` (or an actual merge command fails with an approval-related error).

Use this script whenever you need to assess merge readiness — it is cheaper and more reliable than querying each condition manually.

## Rules

- **Only use `ask_user` for true blockers** — conflicts you cannot resolve, ambiguous review feedback you cannot interpret, or persistent CI failures you cannot fix. Do NOT use `ask_user` for expected waiting states like "awaiting PR approval." Just summarize and stop.
- `ask_user` is only available in the main context (not in sub-agents). Sub-agents are fine for self-contained work like resolving conflicts, investigating CI failures, or running `gh` commands.
- Be efficient — do not re-do work that is already complete. Review your conversation history.
- **Do not proactively comment on the PR.** Never post stand-alone status comments, ask for review, ping reviewers, or mention CODEOWNERS/users on your own. Assume the user will manage sharing the PR and getting reviews. Only comment when directly replying to an existing review thread or top-level PR comment that needs a response.
- **Do not stop just because something is "pending."** If CI is running, keep working on other conditions while you wait.
- **Only Checks pending is a wait state, not a stopping point.** If Reviews and Mergeable are satisfied and Checks are still pending, summarize the current status and end the turn. The next agent-merge tick will re-run the status script.
- **Translate status into plain language.** Do not summarize with bare labels like "Checks pending." Say what is actually true, for example: "Review threads are handled. Required checks passed. Cloud Code Review was not requested on this PR for the current head, so it isn't blocking merge." The Reviews/Checks/Mergeable labels are anchors for your own reasoning, not the user-facing summary.
- **Always disable pagers** when running `gh` commands: set `GH_PAGER=""` (e.g. `GH_PAGER="" gh pr view ...`) or pipe output through `cat`. Without this, commands hang in non-interactive shells.
- **Reviews are not complete until reply + resolve succeeds.** Do not treat "I made the code change" as equivalent to "thread handled." After touching a review thread, you must post a reply on that exact thread and resolve it.
- **Do not work around CI by disabling shared tooling.** A failing CI step is a signal, not a nuisance. Do not "fix" failures by stripping environment variables that configure shared build/test tooling, deleting or bypassing caches, skipping tests, lowering coverage thresholds, removing assertions, marking required checks optional, silencing linter rules, or otherwise neutering tooling that other PRs and builds on the repo rely on. These changes degrade CI for everyone on the project and hide the underlying problem.
- **For flaky infrastructure** (build caches, package registries, runners, third-party services): prefer a narrowly-scoped fix — a targeted retry, a higher timeout, or a pre-flight check that verifies the dependency is healthy before the failing step runs. If you cannot make a narrow fix in one or two attempts, escalate via `ask_user` instead of broadening the workaround.
- **Anti-pattern test for any CI "fix":** if your change would make the failure invisible on future PRs *without* solving it — for example, conditionally skipping a step on one platform, removing an env var only in CI, special-casing a single OS, marking a required check optional, or replacing a strict assertion with a permissive one — stop and escalate. That is a workaround, not a fix. If a test is genuinely wrong about product behavior, fix the test's *logic* (assertions, fixtures, setup); do not just loosen it until it passes.
- **No urgency exception. No "temporary" disables.** Agent-merge runs unsupervised, so urgency is the easiest rationalization for a workaround — and the one no human is there to push back on. The merge can wait; a degraded shared toolchain cannot be un-degraded by the next agent. If you cannot land a real fix in one or two attempts, escalate via `ask_user`. Do **not** disable a failing step "for now" with a TODO to re-enable later — "temporary" disables outlive the PR that added them and quietly become permanent.
- **Do not bundle a workaround with a real fix.** Belt-and-suspenders is not a virtue here: once the real fix lands, the "fallback" disable decays into a silent permanent regression that no one notices. Ship one real fix, or escalate. Never combine them in the same PR.
- **Verify before declaring shared tooling broken on a platform.** Before concluding "this tool doesn't work on macOS / Windows / this runner," reproduce the failure, check the tool's version and configuration, and look for transient causes — timeouts, network blips, package-registry hiccups, runner state. Most "X is broken on this platform" reports turn out to be "X timed out once on this runner": a transient failure of healthy tooling, not a real platform incompatibility. Treat removing/disabling the tool on a platform as a load-bearing claim about the platform that needs evidence, not a diagnostic shortcut.
- **Never use `bash sleep` (or any blocking in-tool wait) to pause for CI.** Long blocking waits inside a tool call lock the agent out of new user input — the user cannot interrupt to ask a question or steer mid-wait. When you hit a wait state, summarize the current status and **end the turn**. The next agent-merge tick (or the user's next invocation) will re-run you. Do **not** chain `sleep 300 && bash check-merge-conditions.sh ...`, **and do not use blocking watch commands like `gh run watch` or `gh pr checks --watch`** — those are the same anti-pattern, often worse because CI can block them for 30+ minutes. End the turn; the tick is the watch loop.

## Workflow

### 1. Triage — check PR state and all merge conditions

Fetch the full PR state:

```bash
GH_PAGER="" gh pr view <number> --repo <repo> --json state,isDraft,reviewDecision,mergeStateStatus,mergeable,statusCheckRollup
GH_PAGER="" gh pr checks <number> --repo <repo>
```

If the PR is already merged or closed, report that and stop.

Assess each merge condition (Reviews, Checks, Mergeable) from the response. Note which conditions are already satisfied and which need work. This is your starting checklist for the rest of the run.

### 2. First pass — address everything visible now

Work through all unsatisfied conditions before entering the monitoring loop. The goal is to push any needed code changes so that CI runs against the final state.

**a) Resolve conflicts (Mergeable)**

```bash
GH_PAGER="" gh pr view <number> --repo <repo> --json mergeable
```

- `mergeable: MERGEABLE` → no conflicts.
- `mergeable: CONFLICTING` → resolve the conflicts. Use whatever strategy is appropriate for the repository and branch protection settings (rebase, merge commit, etc.). Check repo conventions if unsure. Resolve and push.
- Only use `ask_user` if you cannot resolve the conflicts correctly on your own — e.g., both sides made intentional changes to the same logic and you cannot determine the correct resolution.

**b) Address review comments (Reviews)**

There are three kinds of review feedback you should read:

1. **Inline review threads** — these are the strict merge gate because they can remain unresolved.
2. **Top-level review bodies** — the summary/body attached to a review (for example a `COMMENTED` or `CHANGES_REQUESTED` review). These cannot be resolved, but may contain useful actionable feedback.
3. **Top-level PR comments** — issue-style PR comments. These also cannot be resolved, but may contain useful actionable feedback.

**Review threads** (inline code comments) — use GraphQL to fetch unresolved threads:

```bash
gh api graphql -f query='
  { repository(owner: "<owner>", name: "<repo>") {
    pullRequest(number: <number>) {
      reviewThreads(first: 100) { nodes {
        isResolved id path line
        comments(last: 1) { nodes { databaseId body author { login } } }
      } }
    }
  } }' --jq '[.data.repository.pullRequest.reviewThreads.nodes[]
    | select(.isResolved == false)
    | {
        threadId: .id,
        path,
        line,
        latestCommentDatabaseId: (.comments.nodes[0].databaseId),
        latestCommentAuthor: (.comments.nodes[0].author.login),
        latestCommentBody: (.comments.nodes[0].body)
      }]'
```

For each unresolved thread, **read the comment carefully and consider it on its merits**. Evaluate whether the suggestion improves correctness, security, performance, or consistency — then act accordingly:

- **Actionable feedback** (bug fix, improvement, style consistency): Make the change, commit, push. Reply confirming what you changed, then resolve the thread.
- **Valid point but you disagree with the approach**: Reply explaining your reasoning. Make an alternative fix if appropriate.
- **Genuinely inapplicable** (comment about deleted code, already addressed, factually wrong): Reply explaining why, then resolve.
- **Needs human judgment** (architectural decision, product question): Use `ask_user` to escalate.

For every unresolved review thread you handle, complete this checklist in order. A thread is not done until all three steps succeed:

1. Make the code change if needed, then commit and push.
2. Write a 1-2 sentence reply explaining what you changed or why you are not changing it. Then use the latest comment's `databaseId` from the query above and run the helper script:

```bash
reply_root="${TMPDIR:-${TEMP:-${TMP:-/tmp}}}"
reply_md="$reply_root/reply-<comment_database_id>.md"
cat > "$reply_md" <<'EOF'
<your reply>
EOF
bash src-tauri/app-skills/agent-merge/reply-and-resolve-thread.sh \
  <owner> \
  <repo> \
  <pr_number> \
  <thread_node_id> \
  <comment_database_id> \
  "$reply_md"
```

3. If the helper script exits successfully, treat the thread as handled and continue. If it fails, treat the thread as still unhandled and keep working it.

**Top-level review bodies** (cannot be resolved) — fetch with:

```bash
GH_PAGER="" gh pr view <number> --repo <owner>/<repo> --json reviews --jq '.reviews'
```

Read the review bodies and decide whether they contain useful feedback you should act on. Ignore empty/boilerplate review bodies that add no actionable information beyond the thread comments you are already handling.

- Keep these in the same **session-local tracking note** as other non-gating feedback, using a stable identifier such as the review ID plus a disposition: `acted_on`, `replied`, `ignored`, or `needs_human`.
- If a review body contains useful actionable feedback, make the change and record that you acted on it.
- If it is not useful or not actionable, record that you intentionally ignored it.
- Do **not** post a stand-alone PR comment just to acknowledge a review body.

**Top-level PR comments** (cannot be resolved) — fetch with:

```bash
GH_PAGER="" gh pr view <number> --repo <owner>/<repo> --json comments --jq '.comments'
```

Read top-level PR comments and decide whether they contain useful feedback you should act on. They are not a strict merge gate, but they may still identify bugs, improvements, or context worth incorporating.

- Keep a small **session-local tracking note** (for example a session workspace doc or other scratch artifact that is **not committed**) listing non-gating feedback items and your disposition for each one. Include the item type (`review` or `comment`), its ID, and a disposition: `acted_on`, `replied`, `ignored`, or `needs_human`.
- Use that tracking note to avoid re-processing the same top-level review bodies and PR comments on every check-in. Treat items not in the note, or items whose state is still unresolved in your note, as needing review.
- If a top-level comment contains useful actionable feedback, make the change and reply if a reply adds value.
- If it is not useful or not actionable, do not feel obligated to reply just to acknowledge it.
- Record the outcome in your tracking note after you act on or intentionally ignore the comment.
- Check your conversation history — if you have already replied to a comment in a previous check-in, skip it.
- When replying, quote the original comment (using `> ` markdown quoting) to make it clear which comment you're responding to.
- Do **not** post a stand-alone PR comment for status updates, to ask for review, or to ping reviewers/CODEOWNERS.

**c) Update branch if required (Mergeable)**

```bash
GH_PAGER="" gh pr view <number> --repo <repo> --json mergeStateStatus
```

If `mergeStateStatus: BEHIND`, the repo requires the branch to be up to date before merging. Update it now so CI runs against the final state. (GitHub only reports `BEHIND` when branch protection enforces this — if the repo doesn't require it, the status will be `CLEAN` even when behind.)

### 3. Monitor and converge — wait for all conditions to be met

After the first pass, CI should be running against your latest push. **Do not block on CI.** Instead, use the companion script to cheaply poll all three conditions and only take action when something needs work.

**Use the status script** — `src-tauri/app-skills/agent-merge/check-merge-conditions.sh` checks the three deterministic merge conditions (Reviews, Checks, Mergeable) in a single call and reports which conditions are satisfied, pending, or failing. For Checks, trust its Copilot Cloud Code Review classification: active CCR for the PR's current head keeps Checks pending; no CCR signal is non-blocking. Run it instead of manually querying each condition:

```bash
bash src-tauri/app-skills/agent-merge/check-merge-conditions.sh <owner> <repo> <pr_number>
```

- **Exit code 0** → all deterministic/actionable conditions are met. Read the final GitHub merge signal in the output: if it reports `mergeStateStatus: CLEAN`, proceed to step 4 and attempt the merge even if `reviewDecision` is `REVIEW_REQUIRED`; if it reports a non-actionable blocker, summarize and stop.
- **Exit code 1** → one or more conditions unsatisfied. Read the output to see which.
- **Exit code 2** → the check failed (for example auth/API error or an indeterminate condition). Treat this as an error/unknown state, not as "ready" or "not ready."

**Loop logic:**

1. Run the status script.
2. If all deterministic/actionable conditions are met → exit the loop and proceed to step 4 for the final GitHub mergeability check.
3. If any condition needs action:
   - **Reviews unsatisfied** (unresolved review threads listed in output) → address them using the approach in step 2b. Stay on Reviews until each affected thread has gone through the helper script successfully. Commit and push any code changes.
   - **Checks failing** (failed checks listed in output) → investigate and fix the **root cause**:
     - Examine logs: `GH_PAGER="" gh run view <run_id> --repo <repo> --log-failed`
     - Identify what actually failed and why. Distinguish between a real product/test bug, an environmental/infra issue (build caches, package registries, runners), and a flaky third-party dependency.
     - Fix it at the source. **Do not disable, skip, or neuter shared tooling to make the failure go away** — see the "Do not work around CI..." rules above.
     - For infra/flake failures, prefer targeted retries, timeout bumps, or pre-flight verification of the failing dependency over removing the dependency from the build.
     - *Illustrative example:* if a build cache (sccache, ccache, a remote artifact cache, etc.) fails to start with a timeout, the right fix is usually to raise its startup timeout and add a pre-flight health check in the workflow. The wrong fix is to strip the cache's env vars or wrapper out of the build — that silently disables the cache for every future build on that platform.
     - If you've attempted a narrowly-scoped fix once or twice without progress, escalate via `ask_user` rather than broadening the change.
     - Once you have a real fix, commit and push.
   - **Mergeable unsatisfied** (conflicts or branch behind) → resolve conflicts or update branch, then push.
4. Re-check non-gating review feedback opportunistically:
   - Read top-level review bodies and top-level PR comments during the first pass.
   - Re-read them when you are already doing deeper PR inspection for another reason (for example because Reviews/Checks/Mergeable needs action), and once more just before merging.
   - Use your session-local tracking note to focus on new or still-untriaged review bodies/comments rather than re-reading the full set from scratch every time.
   - Do **not** poll top-level review bodies or top-level PR comments on every CI-only wait iteration.
5. If **only Checks are pending** and Reviews + Mergeable are satisfied → this is a **wait state, not a stopping point — but it is also not an in-tool wait.** Summarize the current status and **end the turn**; the next agent-merge tick (~5 minutes out) will re-invoke you. Do **not** `bash sleep` or otherwise block inside a tool call — that locks the user out of interrupting you. If the script reports Cloud Code Review pending and CCR is optional or non-blocking, do **not** treat that as a wait at all.
6. After pushing any fixes, CI will restart. You may do one immediate re-check inside the same turn to confirm the new run was picked up — no sleep needed, just `gh pr checks`. After that, end the turn and let the next tick handle the ~5-minute cadence.
7. If you've attempted the same CI fix multiple times without progress, or the failure is outside your control (flaky infra, external service), use `ask_user` to escalate.

The script uses a small number of focused API calls and already handles the Cloud Code Review edge cases, including the difference between optional/unrequested CCR and genuinely pending CCR. Prefer it over manually piecing together `gh` commands during this loop.

### 4. Merge

All three conditions should be satisfied. Verify once more and merge:

```bash
GH_PAGER="" gh pr view <number> --repo <repo> --json mergeStateStatus,mergeable,reviewDecision,statusCheckRollup
```

- `mergeStateStatus: CLEAN` → GitHub currently allows merge. **Always attempt the merge**, even if `reviewDecision` is `REVIEW_REQUIRED`. GitHub branch protection is the source of truth here: if merge is not actually allowed, `gh pr merge` will fail and tell you why. Do **not** stop just because `reviewDecision` looks like a blocker when `mergeStateStatus` is `CLEAN`.
  - Merge using the repository's supported/preferred method. Do **not** assume squash is always allowed.
  - If the repo or PR clearly indicates the intended method, use it.
  - Otherwise, first look for repository norms (project docs, contribution guidance, or a few recent merged PRs / default-branch history) to infer what is normal here.
  - Then check which merge methods are allowed:
    ```bash
    GH_PAGER="" gh repo view <repo> --json mergeCommitAllowed,rebaseMergeAllowed,squashMergeAllowed
    ```
  - Choose the method that best matches the repo norm among the allowed options (`--merge`, `--squash`, or `--rebase`). If the norm is still unclear, prefer the least surprising allowed option and avoid inventing a new convention.
  - **Never pass `--delete-branch`.** Defer to the repo's "Automatically delete head branches" setting (Settings → General). Forcing deletion auto-closes any downstream PR stacked on this branch, and recovery (push branch back → reopen → retarget, in that order) is painful.

  - **Handling auto-retargeted stacked PRs.** When a stacked sibling merges, GitHub retargets this PR and deletes the old base branch. Prompts, session metadata, and old messages may still name the deleted ref — treat GitHub as the source of truth.

    Detect and normalize:

    ```bash
    git fetch --prune origin
    GH_PAGER="" gh pr view <number> --json baseRefName --jq .baseRefName
    ```

    Strip a leading `origin/` from the prompt's base name before comparing. If it differs from the returned value, use the returned value and mention the retarget once in the summary (e.g. `Note: base auto-retargeted to main`).

    Enable `git rerere` (`git config rerere.enabled true`), then merge from `origin/<baseRefName>` — never the local branch, which may be stale or pruned. Two common conflict shapes:

    - **`ours` is strictly newer** (sibling merged a subset of this branch's work). Verify before blanket `--ours`:
      ```bash
      git show :2:<file> | grep -c "<expected-marker>"  # ours
      git show :3:<file> | grep -c "<expected-marker>"  # theirs
      git checkout --ours <file> && git add <file>
      ```
      Run a typecheck (`cargo check`, `bun run typecheck`) to catch semantic conflicts file-level resolution missed.
    - **`modify/delete` from a sub-split.** Find where the file went:
      ```bash
      git log --oneline --diff-filter=D origin/<baseRefName> -- <file>
      ```
      Usually accept the deletion with `git rm <file>` and adopt the new structure. Restoring is rare; call it out.

    Before validation, scan merged commits for tooling drift in dependency manifests, lock files, toolchains, or lint configs. Re-run the install step if manifests/lockfiles changed; flag toolchain or lint config changes so new errors read as drift, not regressions.

  ```bash
  GH_PAGER="" gh pr merge <number> --repo <repo> --merge
  # or --squash / --rebase when that is the appropriate supported method
  ```
  - If the merge command fails, treat the command output as the real blocker. If GitHub says approval is still required, summarize that and stop. If it fails for CI/conflicts/branch state, go back to step 3.
- `mergeStateStatus: BEHIND` → update the branch. This will restart CI — go back to step 3.
- `mergeStateStatus: BLOCKED` → diagnose what is unmet:
  - `reviewDecision`: needs approval or changes requested → if not actionable, stop and summarize. Do not post a PR comment to ask for review or approval.
  - `statusCheckRollup`: check for `FAILURE`, `PENDING`, or `ERROR` → go back to step 3.
  - Unresolved review threads → address them (step 2b), then go back to step 3.
  - If nothing actionable remains (for example `mergeStateStatus: BLOCKED` and GitHub is enforcing reviewer approval), stop and summarize — you cannot unblock this.

Before merging, read top-level review bodies and top-level PR comments one more time and action any useful feedback you have not already handled.

## Summary

Print a summary when you reach a stopping point. A stopping point is any of:
- **PR merged** — report success.
- **Nothing actionable remains** — only when GitHub is still blocking merge for a non-actionable reason (for example `mergeStateStatus: BLOCKED` and approval is genuinely required). **This does not include the case where only Checks are pending** — pending CI / explicitly requested Cloud Code Review must stay in the loop. If `mergeStateStatus` is `CLEAN`, attempt the merge first. When nothing actionable remains, summarize what you did and what is still needed. Do not use `ask_user` for this — just summarize and stop. Do not post a stand-alone PR comment for this summary.
- **Truly stuck** (unresolvable conflicts, ambiguous feedback, persistent CI failures) — use `ask_user` to escalate with context.

Include:
- What actions you took since the last check-in (if any)
- Current status of each merge condition (Reviews, Checks, Mergeable) — format as one bullet per condition, each starting with the status emoji from the script (✅ satisfied, ❌ failing/unsatisfied, ⏳ pending), followed by the label and a plain-language explanation. If the script exited 2 (error/unknown) and a condition's state is genuinely undetermined, use ❓ for that condition rather than ❌, and say so in the explanation — ❌ means "checked and failing," not "we couldn't check." Example:
  - ✅ Reviews — all threads handled and resolved.
  - ⏳ Checks — Copilot Cloud Code Review still in progress against the current head `abc1234`.
  - ❌ Mergeable — conflicts with base; needs manual resolution.
- What is still needed to merge (if not yet merged)
- Spell the status out in human language. Prefer "Required checks passed; Cloud Code Review was not requested for this head, so merge is not waiting on it" over bare labels like "Checks satisfied/optional."
