# merge_pr — Initiate a Rebase-Style Merge (with Automerge) on a PR

**Script name:** `merge_pr`

**Script location:** [`scripts/merge_pr.ts`](../../../scripts/merge_pr.ts)

**Invoke:**
```bash
npx --yes tsx .opencode/skills/auto-agents-system/scripts/merge_pr.ts \
  --url URL --pat TOKEN --owner OWNER --repo REPO --pr NUMBER \
  [--dep error|delete|reverse]
```

**CRITICAL:** Always invoke the script using bash and the format noted above. **Never** try to use the skill as a task, or invoke it through any other method than by directly calling bash with a command of the form listed above under "Invoke:".

**CRITICAL:** All attempts to merge pull requests **must** use this script.

## Synopsis

```
merge_pr.ts --url URL --pat TOKEN --owner OWNER --repo REPO --pr NUMBER
            [--dep error|delete|reverse]
```

Calls the Forgejo REST API to merge a pull request using the `rebase` merge
style with `merge_when_checks_succeed=true`. If all CI checks and review
requirements are already satisfied, the PR merges immediately. Otherwise,
Forgejo queues it for automatic merging once those conditions are met — the
script does not wait; it returns as soon as the merge request is accepted.

The script also detects and handles **open blocking issue dependencies** before
calling the merge endpoint, since Forgejo silently ignores merge requests when
a PR has open dependencies (returns HTTP 200 but does nothing).

## Arguments

### Required

| Flag | Type | Description |
|------|------|-------------|
| `--url URL` | string | Forgejo base URL, **no trailing slash** (e.g. `https://git.example.com`) |
| `--pat TOKEN` | string | Personal Access Token with **merge permission** on the repository |
| `--owner OWNER` | string | Repository owner or organisation name |
| `--repo REPO` | string | Repository name |
| `--pr NUMBER` | positive integer | Pull request number to merge |

### Optional

| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--dep STRATEGY` | string | `error` | How to handle open blocking issue dependencies (see below) |
| `-h`, `--help` | — | — | Print usage to stderr and exit 0 |

### Dependency Strategy (`--dep`)

Forgejo PRs can declare issue dependencies. When a PR has **open** (unresolved)
dependencies, Forgejo silently ignores merge requests — it returns HTTP 200 but
the PR is never actually merged. This script detects open dependencies first
and handles them according to `--dep`:

| Value | Behaviour |
|-------|-----------|
| `error` | **(default)** Abort with exit code 1 and list all open blocking dependencies. State is never modified. Safe for read-before-write workflows. |
| `delete` | Delete each open dependency link from the PR, then proceed with the merge. The dependent issues are unaffected; only the link is removed. |
| `reverse` | Delete each open dependency link, then create a new reversed link (the formerly-blocking issue now depends on the PR instead of the other way around). Useful when the intent is "merge this PR, then finish the related issue". |

### Argument Validity Rules

- `--pr` must be a positive integer. Floats, zero, and negatives are rejected
  with exit code 1.
- `--dep` must be exactly `error`, `delete`, or `reverse`. Any other value
  causes exit code 1.
- All five named arguments are required (except `--dep` which defaults to
  `error`). Any missing required argument causes exit code 1.
- Unknown flags cause exit code 1.

## What the Script Does

The script executes these steps in order:

1. **Fetch the PR** — verifies it is open and not already merged.
2. **Fetch all dependencies** (exhaustively paginated) — filters to open ones.
3. **Handle open dependencies** per `--dep` strategy (error / delete / reverse).
4. **POST to the merge endpoint**:
   ```
   POST /api/v1/repos/{owner}/{repo}/pulls/{index}/merge
   {
     "Do": "rebase",
     "merge_when_checks_succeed": true,
     "head_commit_id": "<current HEAD SHA>"
   }
   ```
   The `head_commit_id` acts as an **optimistic lock** — if someone pushed to
   the branch between the script reading the PR and calling merge, Forgejo
   returns HTTP 409, preventing a stale-read merge.
5. **Re-fetch the PR** — inspects `merged` flag to determine the outcome.
6. **Return JSON result** with `status: "merged"` or `status: "auto_merge_scheduled"`.

## Output

Written to **stdout** on success as a pretty-printed JSON object:

```json
{
  "status": "merged" | "auto_merge_scheduled",
  "pr_number": 42,
  "pr_title": "feat: add new feature",
  "head_sha": "abc123...",
  "merge_commit_sha": "<sha>" | null,
  "merged_at": "2026-04-17T12:00:00Z" | null,
  "deps_handled": [
    {
      "action": "deleted" | "reversed",
      "dep_number": 10,
      "dep_title": "Related issue title",
      "dep_repo": "owner/repo"
    }
  ],
  "message": "PR #42 merged immediately using rebase style."
}
```

| Field | Type | Description |
|-------|------|-------------|
| `status` | string | `merged` — PR merged immediately; `auto_merge_scheduled` — Forgejo accepted the request and will merge when checks pass |
| `pr_number` | integer | The PR number that was processed |
| `pr_title` | string | The PR title at the time of the call |
| `head_sha` | string | The head commit SHA that was submitted for merge |
| `merge_commit_sha` | string \| null | Non-null only when `status == "merged"` |
| `merged_at` | string \| null | ISO 8601 timestamp, non-null only when `status == "merged"` |
| `deps_handled` | array | Zero or more dependency actions taken (empty if no open deps existed) |
| `message` | string | Human-readable summary of the outcome |

## Diagnostics / stderr

All progress information is written to stderr:

```
Fetching PR #42 from https://git.example.com/org/repo...
PR #42: "feat: add new feature"
  State:     open
  Head SHA:  abc123def456...
  Mergeable: true

Fetching dependencies for PR #42...
  Total dependencies: 2 (1 open / blocking)
  BLOCKING: #10 [owner/repo] "Related issue"

Handling 1 open dependency(ies) with strategy 'delete'...
  DELETE dependency: PR #42 → #10 (owner/repo)
    Deleted OK (HTTP 200)

POST https://git.example.com/api/v1/repos/owner/repo/pulls/42/merge
  Do=rebase  merge_when_checks_succeed=true  head_commit_id=abc123def456…
Response: HTTP 200

Merge request accepted. Re-fetching PR to determine state...
  merged=false  state=open
PR #42 not yet merged — auto-merge is pending checks/approvals.
```

## Exit Codes

| Code | Meaning |
|------|---------|
| 0 | Merge initiated — `status` is `merged` or `auto_merge_scheduled` |
| 1 | Any failure — descriptive error on stderr with machine-readable error code |

## Errors the Script May Throw

| Error Code | HTTP Status | Condition |
|------------|-------------|-----------|
| `PR_MERGED` | — | PR is already merged; nothing to do |
| `PR_NOT_OPEN` | — | PR is closed (not open) |
| `OPEN_DEPENDENCIES` | — | Open deps found and `--dep=error` (default); lists all blockers |
| `DEP_DELETE_FAILED` | varies | DELETE of a dependency link returned non-200 |
| `DEP_REVERSE_FAILED` | varies | POST of the reversed dependency returned non-201 |
| `MERGE_FORBIDDEN` | 403 | The PAT lacks merge access to the repository |
| `MERGE_NOT_ALLOWED` | 405 | Branch protection disallows the `rebase` merge style, or automerge is disabled |
| `MERGE_CONFLICT` | 409 | Merge conflicts present, or `head_commit_id` mismatch (concurrent push between read and merge call); re-run to retry with fresh HEAD |
| `MERGE_UNPROCESSABLE` | 422 | PR is a draft, already merged, or otherwise not mergeable |
| `HTTP_NNN` | NNN | Any other unexpected HTTP response from the merge endpoint |

A special case: HTTP 409 with the message `"already scheduled to auto merge"`
is treated as **success** (not an error). This happens when the script is run
twice on the same PR; the second run detects the already-scheduled automerge
and returns `status: "auto_merge_scheduled"`.

## Important Behaviours and Caveats

### Automerge State Is Not Directly Readable
Forgejo provides no read-only endpoint to confirm automerge is scheduled. The
script infers `status: "auto_merge_scheduled"` from the combination of a 200
response from the merge endpoint plus `merged=false` on the re-fetched PR.
This is the authoritative signal — it means Forgejo has accepted the request.

### Optimistic Locking
The `head_commit_id` field in the merge request body is an optimistic lock. If
someone pushes to the PR branch between when the script fetches the PR (step 1)
and when it calls the merge endpoint (step 4), Forgejo returns HTTP 409
(`MERGE_CONFLICT`). Simply re-running the script resolves this.

### No Waiting for CI
The script returns immediately after Forgejo accepts the merge request. It does
**not** poll for the merge to complete. When `status: "auto_merge_scheduled"`,
the caller should expect the PR to eventually close on its own once CI passes.
This is intentional — agents should never block waiting for CI.

### Rebase Merge Style
The `Do: "rebase"` merge style rewrites the PR's commits linearly on top of
the base branch HEAD. No merge commit is created. This produces a clean,
linear history. Other merge styles (`merge`, `squash`, `rebase-merge`) are
not used by this script.

## When to Use This Script

Use `merge_pr` whenever you want to **initiate a merge on a specific PR**:

- After [`rebase_pr`](../rebase_pr/) has brought a stale PR current.
- After manual conflict resolution and force-push on a conflicted PR.
- Directly on a PR returned by [`list_prs_ready_to_merge`](../list_prs_ready_to_merge/).
- Any time you need to queue a PR for automerge once its CI finishes.

Do **not** use this script if the PR is still stale — call
[`rebase_pr`](../rebase_pr/) first (if `stale_no_conflicts`) or resolve
conflicts locally (if `stale_with_conflicts`).

## Usage Examples

```bash
# Merge PR #42 (default: abort if open dependencies exist)
npx --yes tsx /app/.opencode/skills/auto-agents-system/scripts/merge_pr.ts \
  --url https://git.example.com \
  --pat ghp_XXXX \
  --owner my-org \
  --repo my-repo \
  --pr 42

# Merge PR #42, deleting any open dependency links first
npx --yes tsx /app/.opencode/skills/auto-agents-system/scripts/merge_pr.ts \
  --url https://git.example.com \
  --pat ghp_XXXX \
  --owner my-org \
  --repo my-repo \
  --pr 42 \
  --dep delete

# Merge PR #42, reversing dependency links (issue now depends on PR)
npx --yes tsx /app/.opencode/skills/auto-agents-system/scripts/merge_pr.ts \
  --url https://git.example.com \
  --pat ghp_XXXX \
  --owner my-org \
  --repo my-repo \
  --pr 42 \
  --dep reverse

# Parse just the status from the output
npx --yes tsx /app/.opencode/skills/auto-agents-system/scripts/merge_pr.ts \
  --url https://git.example.com --pat ghp_XXXX \
  --owner my-org --repo my-repo --pr 42 \
  | jq -r '.status'

# Check if the merge was immediate or scheduled
result=$(npx --yes tsx /app/.opencode/skills/auto-agents-system/scripts/merge_pr.ts \
  --url https://git.example.com --pat ghp_XXXX \
  --owner my-org --repo my-repo --pr 42)
status=$(echo "$result" | jq -r '.status')
if [ "$status" = "merged" ]; then
  echo "Merged immediately"
else
  echo "Queued for automerge"
fi
```

## Agent Usage Examples

### pr-merge-worker

The worker is the **primary caller** of this script. It always calls
`merge_pr` as the final step after any necessary rebase or conflict resolution:

```
# After rebase_pr (stale_no_conflicts path):
npx --yes tsx /app/.opencode/skills/auto-agents-system/scripts/merge_pr.ts \
  --url $FORGEJO_URL --pat $FORGEJO_PAT \
  --owner $REPO_OWNER --repo $REPO_NAME \
  --pr $PR_NUMBER --dep delete

# After local conflict resolution (stale_with_conflicts path):
npx --yes tsx /app/.opencode/skills/auto-agents-system/scripts/merge_pr.ts \
  --url $FORGEJO_URL --pat $FORGEJO_PAT \
  --owner $REPO_OWNER --repo $REPO_NAME \
  --pr $PR_NUMBER --dep delete

# For a ready-to-merge PR (not stale, already approved):
npx --yes tsx /app/.opencode/skills/auto-agents-system/scripts/merge_pr.ts \
  --url $FORGEJO_URL --pat $FORGEJO_PAT \
  --owner $REPO_OWNER --repo $REPO_NAME \
  --pr $PR_NUMBER
```

The worker uses `--dep delete` by default for the stale buckets because those
PRs have already been verified to meet all quality gates; open dependency links
at that point are likely stale metadata rather than genuine blockers.

### pr-merge-pool-supervisor

The supervisor does **not** call this script directly. It dispatches
`pr-merge-worker` subagents, which call this script as part of their procedure.

### task-implementor

An implementation worker creating a PR would never call this script — that
would be outside its responsibility. However, it might call it after creating
a PR if the project's workflow involves self-merging after CI passes (unusual
but possible with `--dep error` for safety).

## Dependencies

- Node.js ≥ 18 (native `fetch` API)
- `npx tsx` for TypeScript execution
- Zero external npm packages
