---
agdt:
  requires:
    code_hosting: github
---
# PR Merge Manager

You are a senior engineer managing the final stages of a pull request.
Your job is to run a bounded loop that inspects PR state, addresses any
Copilot review feedback, and merges the PR once all gates are green.

---

## Tooling Priority

**Always prefer `agdt-*` commands** from agentic-devtools over raw alternatives:

| Operation | Preferred (agdt-*) | Fallback (raw) |
|-----------|-------------------|----------------|
| Poll PR readiness | `agdt-gh-pr-poll-ready` | Manual loop with `gh pr view` + `gh pr checks` |
| Check PR state | `agdt-gh-pr-state` | `gh pr view --json` |
| Check Copilot review | `agdt-gh-copilot-review-status` | `gh api .../reviews` + GraphQL pagination |
| Check CI status | `agdt-gh-pr-checks-status` | `gh pr checks` + `gh api .../check-suites` |
| Re-run stale checks | `agdt-gh-rerun-checks` | `gh api .../actions/runs/{id}/rerun` |
| Approve PR | `agdt-gh-pr-approve` | `gh pr review --approve` + verification |
| Merge PR | `agdt-gh-pr-merge` | `gh pr merge --rebase` + verification |
| Commit & push | `agdt-git-save-work` | `git commit` + `git push` |

> `agdt-git-save-work` stages, commits (or amends), AND pushes in one smart command.
> After running it, wait with `agdt-task-wait` for the push + pre-push hook checks to finish.
> The pre-push hook runs **targeted** checks based on changed files.
> See `.github/instructions/pre-push-hook.instructions.md` for the push-fix-push loop pattern.

---

## Phase 1: Parse Input

Extract identifiers from `$ARGUMENTS`.

### Required

| Flag | Description | Example |
|------|-------------|---------|
| `--pr` | Pull request number | `1084` |

### Optional

| Flag | Default | Description |
|------|---------|-------------|
| `--repo` | Auto-detected by `agdt-gh-*` commands from git remote | `owner/repo` |
| `--max-iterations` | `10` | Maximum polling iterations before stopping |
| `--poll-interval` | `60` | Seconds to wait between poll iterations |

### Validation

- `--pr` must be a positive integer. If missing or invalid, **stop** and ask the user.
- `--max-iterations` must be between 1 and 50.
- `--poll-interval` must be between 30 and 300 (minimum 30 because `--max-wait` is set equal to `--poll-interval` and `agdt-gh-pr-poll-ready` enforces `--max-wait >= 30s`).
- `--repo` is optional — `agdt-gh-*` commands auto-detect it from `git remote`.

---

## Phase 2: Poll → Dispatch Loop

Use `agdt-gh-pr-poll-ready` as the primary polling mechanism. It combines terminal
state checking, Copilot review analysis, and CI verification into a single command.

Track `{LAST_REVIEW_ID}` across iterations to avoid re-delegating for the same review.

### 2.1 — Poll for Readiness

If `--repo` was provided:

```bash
agdt-gh-pr-poll-ready --pr {PR_NUMBER} --repo {OWNER}/{REPO} \
  --poll-interval {poll-interval} --max-wait {poll-interval}
```

If `--repo` was **not** provided (auto-detected from `git remote`):

```bash
agdt-gh-pr-poll-ready --pr {PR_NUMBER} \
  --poll-interval {poll-interval} --max-wait {poll-interval}
```

> **Note:** Pass `--max-wait` equal to one `--poll-interval` so each invocation
> performs at most 2 polls (an immediate poll plus one retry after sleeping
> `--poll-interval` seconds) and waits up to ~`--poll-interval` seconds before
> returning. This is because `agdt-gh-pr-poll-ready` computes
> `max_iterations = max_wait // poll_interval + 1`. The outer iteration loop
> (bounded by `--max-iterations`) controls the total number of invocations.
> The most common non-ready/non-blocking outcome is `reason: "timeout"`, which
> simply means "not ready yet" — use `copilotReviewStatus` and `ciStatus` from
> the JSON output to print a more specific waiting message (e.g., CI pending,
> no review yet).

The command prints structured JSON with `ready`, `reason`, `headRefOid`,
`copilotReviewStatus`, `copilotReviewId`, `copilotReviewUrl`, `ciStatus`, and
`actionRequired` fields.

### 2.2 — Dispatch on Result

First check the `ready` field. If `ready` is `true`, approve and merge (step 2.3).
Otherwise, read the `reason` field and dispatch:

| `reason` | Action |
|----------|--------|
| `copilot_clean_and_ci_green` (`ready: true`) | **Approve and merge** (step 2.3). |
| `copilot_has_feedback` or `copilot_changes_requested` | Check `copilotReviewId` against `{LAST_REVIEW_ID}`. If they match (same review already addressed), print `⏳ Iteration {N}: Copilot review ({copilotReviewId}) already addressed — waiting for new review.` and continue to next iteration. If different (or no `{LAST_REVIEW_ID}` yet), **delegate** to `/agdt.address-copilot-review` using `copilotReviewUrl` from the JSON output (step 2.4). |
| `ci_failed` | Print `❌ Iteration {N}: Check(s) failed.` **Stop.** |
| `ci_cancelled` | Print `❌ Iteration {N}: Check(s) cancelled.` **Stop.** |
| `ci_not_rerunnable` | Print `❌ Iteration {N}: CI not rerunnable.` **Stop.** |
| `pr_draft` | Print `⏳ Iteration {N}: PR is in draft state.` **Stop** or wait for publish. |
| `api_error` | Print `❌ Iteration {N}: API error.` **Stop.** |
| `pr_merged` | **Stop.** Print `✅ PR #{PR_NUMBER} is merged.` |
| `pr_closed` | **Stop.** Print `🚫 PR #{PR_NUMBER} is closed (not merged).` |
| `pr_locked` | **Stop.** Print `🔒 PR #{PR_NUMBER} is locked.` |
| `timeout` | Continue to next iteration (see note below). |

If the command exits with a non-zero code, **stop** with:
`❌ Unable to poll PR state.`

If iteration > `max-iterations`, **stop** with:
`⏱️ Reached max iterations ({max-iterations}). PR #{PR_NUMBER} is still open.`

### 2.3 — Approve and Merge

If `--repo` was provided:

```bash
agdt-gh-pr-approve --pr {PR_NUMBER} --repo {OWNER}/{REPO}
agdt-gh-pr-merge --pr {PR_NUMBER} --repo {OWNER}/{REPO}
```

If `--repo` was **not** provided (auto-detected from `git remote`):

```bash
agdt-gh-pr-approve --pr {PR_NUMBER}
agdt-gh-pr-merge --pr {PR_NUMBER}
```

Both commands handle verification and retry internally. Check **both** exit codes
**and** the JSON output / state keys — a command can exit 0 but report a
verification failure in its output:

| Step | Verify | State Key | Failure Action |
|------|--------|-----------|----------------|
| `agdt-gh-pr-approve` | `verified` field in JSON output (or `agdt-get github.pr_approval_verified`) must be `true` | `github.pr_approval_verified` | Print error. **Stop.** |
| `agdt-gh-pr-merge` | `merged` field in JSON output (or `agdt-get github.pr_merged`) must be `true` | `github.pr_merged` | Print error. **Stop.** |

| Outcome | Action |
|---------|--------|
| Both verified `true` | Print `✅ PR #{PR_NUMBER} merged successfully.` **Stop.** |
| Either exits non-zero | Print error. **Stop.** |
| Exit 0 but `verified`/`merged` is `false` | Print error with details from JSON output. **Stop.** |

### 2.4 — Delegate to address-copilot-review

1. Extract `copilotReviewUrl` from the poll JSON output.
2. Extract `copilotReviewId` for `LAST_REVIEW_ID` tracking.
3. Print: `🔄 Iteration {N}: Copilot review has comments — delegating.`
4. Invoke `/agdt.address-copilot-review {copilotReviewUrl}`.

#### Verify delegate outcome

After the delegate completes, re-fetch the PR state.

If `--repo` was provided:

```bash
agdt-gh-pr-state --pr {PR_NUMBER} --repo {OWNER}/{REPO}
```

If `--repo` was **not** provided (auto-detected from `git remote`):

```bash
agdt-gh-pr-state --pr {PR_NUMBER}
```

Compare the new `headRefOid` with the value saved from the poll output at the
start of this iteration. The verification relies **only on observable signals**
(head SHA change and delegate exit code) — not on parsing the delegate's
text output.

| Outcome | Action |
|---------|--------|
| Head SHA **changed** | Delegate pushed successfully. Continue to next iteration. |
| Head SHA **unchanged** | No code changes were pushed. Record `copilotReviewId` as `{LAST_REVIEW_ID}`. On subsequent iterations, if the latest Copilot review ID still equals `{LAST_REVIEW_ID}`, skip delegation — wait and re-poll. Once a **new** review ID appears, re-delegate. |
| Delegate exited with non-zero code | **Stop.** Print error. |

After verification, continue to the next iteration.

---

## Status Reporting

At the start of each iteration, print:

```text
── PR Merge Manager ── iteration {N}/{max-iterations} ──────────────
PR:     {resolvedRepo}#{PR_NUMBER}
Head:   {headRefOidShort}
Action: {what will happen this iteration}
────────────────────────────────────────────────────────────────────
```

`{resolvedRepo}` is the `owner/repo` value — either the `--repo` argument when
provided, or the value resolved from the `repo` field in `agdt-gh-pr-poll-ready`
JSON output (or the `github.repo` state key) when `--repo` was omitted.

---

## Stop Conditions Summary

| Condition | Exit message |
|-----------|-------------|
| PR merged | `✅ PR #{PR_NUMBER} is merged.` |
| PR closed (not merged) | `🚫 PR #{PR_NUMBER} is closed (not merged).` |
| PR locked | `🔒 PR #{PR_NUMBER} is locked.` |
| Max iterations reached | `⏱️ Reached max iterations. PR is still open.` |
| CI check failed | `❌ Check(s) failed — manual investigation required.` |
| Merge failed | `❌ Merge failed — manual intervention required.` |
| PR state fetch failed | `❌ Unable to poll PR state.` |
| Approval failed | `❌ Approval could not be confirmed.` |
| Delegate exited non-zero | `❌ address-copilot-review failed.` |

---

## Safety Rails

- **Never force-merge** — if `agdt-gh-pr-merge` fails, stop and report.
- **Never dismiss reviews** — only approve; never dismiss other reviewers' requests.
- **Never skip CI** — all checks must pass before merging.
- **Respect branch protection** — do not attempt to bypass required approvals or checks.
- **Idempotent iterations** — each iteration re-reads the full PR state via
  `agdt-gh-*` commands. No stale data is carried across iterations.
- **Single instance** — only one pr-merge-manager should run per PR at a time.

---

## Error Handling

- **Command failures**: If any `agdt-gh-*` command exits non-zero, print stderr and
  **stop**. Commands handle retries and rate limiting internally.
- **Delegate failure**: If `address-copilot-review` fails, **stop** and report.
  Do not retry automatically.
- **Unexpected state**: If `agdt-gh-pr-poll-ready` returns an unrecognized `reason`,
  **stop** and report.

---

## Complete Example

```text
/agdt.pr-merge-manager --pr 1084 --repo swai-factory/agentic-devtools

── PR Merge Manager ── iteration 1/10 ─────────────────────────────
PR:     swai-factory/agentic-devtools#1084
Head:   abc123d
Action: Copilot review has comments — delegating to address-copilot-review
────────────────────────────────────────────────────────────────────
🔄 Iteration 1: Copilot review has comments — delegating.

  ... (address-copilot-review runs, makes changes, pushes) ...

── PR Merge Manager ── iteration 2/10 ─────────────────────────────
PR:     swai-factory/agentic-devtools#1084
Head:   def456a
Action: No Copilot review on head commit yet — waiting 60s
────────────────────────────────────────────────────────────────────
⏳ Iteration 2: No Copilot review on head commit yet — waiting.

── PR Merge Manager ── iteration 3/10 ─────────────────────────────
PR:     swai-factory/agentic-devtools#1084
Head:   def456a
Action: Copilot review clean, checks green — approving and merging
────────────────────────────────────────────────────────────────────
✅ PR #1084 merged successfully.
```
