# rebase_pr — Trigger a Server-Side Rebase on a Stale, Conflict-Free PR

**Script name:** `rebase_pr`

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

**Invoke:**
```bash
npx --yes tsx .opencode/skills/auto-agents-system/scripts/rebase_pr.ts \
  --url URL --pat TOKEN --owner OWNER --repo REPO --pr NUMBER
```

**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:".

## Synopsis

```
rebase_pr.ts --url URL --pat TOKEN --owner OWNER --repo REPO --pr NUMBER
```

Calls the Forgejo REST API to rebase a pull request's head branch onto the
latest commit of its base branch — **entirely server-side, without a local
git clone**. The PR's commits are rewritten on top of the current base branch
HEAD, bringing the PR fully up-to-date.

The script enforces three strict pre-flight conditions before calling the
rebase endpoint. If any condition fails, the script exits with code 1 and a
descriptive error; it **never proceeds blindly**.


## Arguments

All five arguments are **required**. There are no optional arguments beyond
`--help`.

| 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 **write access** to the repository |
| `--owner OWNER` | string | Repository owner or organisation name |
| `--repo REPO` | string | Repository name |
| `--pr NUMBER` | positive integer | Pull request number to rebase |
| `-h`, `--help` | — | Print usage to stderr and exit 0 |

### Argument Validity Rules

- `--pr` must be a **positive integer**. Floats, zero, and negatives are
  rejected with exit code 1: `ERROR: --pr must be a positive integer`.
- All five arguments are required. Any missing argument causes exit code 1.
- Unknown flags cause exit code 1.

### Invalid Argument Combinations

There are no multi-flag combinations to validate — the five arguments are
independent. However the following are logically invalid and will be caught
by the pre-flight checks (not by argument parsing):

- Passing the number of an already-merged PR → `PR_MERGED` error
- Passing a PR that is not stale → `PR_NOT_STALE` error (informational, not a bug)
- Passing a PR with conflicts → `PR_HAS_CONFLICTS` error


## Pre-Flight Conditions

The script enforces **all three** of the following before calling the Forgejo
rebase endpoint. Failing any one causes an immediate exit 1:

| Condition | Check | Error Code if Failed |
|-----------|-------|---------------------|
| PR must be open | `PR.state == "open"` AND `PR.merged == false` | `PR_MERGED` / `PR_NOT_OPEN` |
| PR must be stale | `PR.merge_base != current base-branch HEAD SHA` | `PR_NOT_STALE` (informational — no rebase needed) |
| PR must have no conflicts | `PR.mergeable == true` | `MERGEABLE_UNKNOWN` (null) or `PR_HAS_CONFLICTS` (false) |

The staleness check requires one additional API call to fetch the current HEAD
SHA of the base branch. This is cached internally but requires the PAT to have
read access to branches.


## What the Script Does

1. **Fetch the PR** — reads `state`, `merged`, `mergeable`, `merge_base`,
   `base.ref`, `head.sha`.
2. **Guard: PR must be open and not merged.**
3. **Fetch the current base-branch HEAD SHA** via
   `GET /api/v1/repos/{owner}/{repo}/branches/{branch}`.
4. **Guard: PR must be stale** (`merge_base != base HEAD`).
5. **Guard: PR must have no conflicts** (`mergeable == true`; rejects `null` and `false`).
6. **POST to the rebase endpoint**:
   ```
   POST /api/v1/repos/{owner}/{repo}/pulls/{index}/update?style=rebase
   ```
   No request body; the `style=rebase` query parameter selects the rebase
   strategy (as opposed to `style=merge` which creates a merge commit).
7. **Re-fetch the PR** — captures the new head SHA to confirm the rebase applied.
8. **Return JSON result** with the old and new SHAs.


## Output

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

```json
{
  "success": true,
  "message": "PR #42 successfully rebased onto 'master'.",
  "pr_number": 42,
  "base_branch": "master",
  "old_merge_base": "abc123def456...",
  "new_head_sha": "xyz789abc012..."
}
```

| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | Always `true` on a successful exit |
| `message` | string | Human-readable summary |
| `pr_number` | integer | The PR number that was rebased |
| `base_branch` | string | The base branch the PR was rebased onto |
| `old_merge_base` | string | The `merge_base` SHA before the rebase (documents what changed) |
| `new_head_sha` | string | The PR's new head SHA after the rebase |


## 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
  Base:       master
  Head:       feature/my-feature
  Merge base: abc123def456...
  Mergeable:  true

Fetching current HEAD of base branch 'master'...
  Base HEAD: def789abc123...

PR is stale: merge_base (abc123def4…) != base HEAD (def789abc1…).
No conflicts detected (mergeable=true). Triggering server-side rebase...

POST https://git.example.com/api/v1/repos/org/repo/pulls/42/update?style=rebase
Response: HTTP 200

Rebase triggered. Fetching updated PR details...
  New head SHA:   xyz789abc012...
  New merge_base: (pending — Forgejo will compute shortly)
```


## Exit Codes

| Code | Meaning |
|------|---------|
| 0 | Rebase triggered successfully — JSON result written to stdout |
| 1 | Any failure — descriptive error on stderr with machine-readable error code |


## Errors the Script May Throw

| Error Code | Condition |
|------------|-----------|
| `PR_MERGED` | The PR is already merged; nothing to rebase |
| `PR_NOT_OPEN` | The PR is closed (not open) |
| `MERGE_BASE_MISSING` | `PR.merge_base` is null — Forgejo hasn't set it yet; retry shortly |
| `PR_NOT_STALE` | `PR.merge_base == base HEAD` — PR is already up-to-date; no rebase needed (informational, not a bug) |
| `MERGEABLE_UNKNOWN` | `PR.mergeable == null` — Forgejo hasn't finished computing the conflict status; retry in a few seconds |
| `PR_HAS_CONFLICTS` | `PR.mergeable == false` — PR has merge conflicts; server-side rebase cannot proceed; manual resolution required |
| `BASE_HEAD_UNKNOWN` | The branch API did not return a commit id for the base branch |
| `FORBIDDEN` | HTTP 403 — the PAT lacks write access to the head repository |
| `NOT_FOUND` | HTTP 404 — the PR or repository was not found |
| `REBASE_CONFLICT` | HTTP 409 — conflicts were detected during the rebase itself despite `mergeable=true` pre-flight check (race condition: concurrent push between pre-flight and rebase call); re-run |
| `ALREADY_UP_TO_DATE` | HTTP 422 — PR was already current by the time the rebase was called (race condition: another process rebased it first); treat as success and proceed |
| `HTTP_NNN` | Any other unexpected HTTP response from the update endpoint |

### Race Condition Notes

Two race conditions are possible between the pre-flight check and the rebase call:

1. **`REBASE_CONFLICT` (HTTP 409)**: Another agent pushed to the base branch
   between your staleness check (step 3) and the rebase call (step 6), causing
   newly introduced conflicts. Resolution: re-run the script; if conflicts
   persist, the PR needs manual resolution.

2. **`ALREADY_UP_TO_DATE` (HTTP 422)**: Another process rebased the PR first.
   This should be treated as a success — proceed to call
   [`merge_pr`](../merge_pr/).


## Important Behaviours and Caveats

### Server-Side Only
This script uses Forgejo's built-in rebase capability. It does **not**
clone the repository, run `git` locally, or require git to be installed.
This is only possible because `PR.mergeable == true` guarantees there are
no conflicts for the server to stumble on.

### `mergeable == null` Is Not Processed
Forgejo computes `PR.mergeable` asynchronously after a PR is created or
updated. If the value is `null`, the script exits with `MERGEABLE_UNKNOWN`
rather than guessing. Retry after a few seconds.

### After the Rebase
The re-fetched PR may show `merge_base` as still `null` or a stale value —
Forgejo computes the new `merge_base` asynchronously after the rebase. The
`new_head_sha` field in the output is the reliable indicator that the rebase
applied. The next call to a `list_prs_*` script will see the updated state
once Forgejo catches up (typically within seconds).

### This Script Does Not Merge
`rebase_pr` only brings the PR up-to-date. To actually merge it after rebasing,
call [`merge_pr`](../merge_pr/).


## When to Use This Script

Use `rebase_pr` when you have a PR whose `stale_state` is `stale_no_conflicts`
(i.e. [`list_prs_stale_clean`](../list_prs_stale_clean/) returns it). This is
the correct and only safe use of this script.

Do **not** use this script for:
- PRs with `stale_state == stale_with_conflicts` — use a local clone instead.
- PRs with `stale_state == stale_unknown` — wait for Forgejo to compute the
  conflict status, then retry.
- PRs with `stale_state == not_stale` — they are already current; call
  [`merge_pr`](../merge_pr/) directly.


## Usage Examples

```bash
# Rebase PR #42 onto its base branch
npx --yes tsx /app/.opencode/skills/auto-agents-system/scripts/rebase_pr.ts \
  --url https://git.example.com \
  --pat ghp_XXXX \
  --owner my-org \
  --repo my-repo \
  --pr 42

# Check the new head SHA after a successful rebase
npx --yes tsx /app/.opencode/skills/auto-agents-system/scripts/rebase_pr.ts \
  --url https://git.example.com \
  --pat ghp_XXXX \
  --owner my-org \
  --repo my-repo \
  --pr 42 \
  | jq -r '.new_head_sha'

# Rebase then immediately queue for merge (full pipeline for stale-clean PRs)
npx --yes tsx /app/.opencode/skills/auto-agents-system/scripts/rebase_pr.ts \
  --url https://git.example.com --pat ghp_XXXX \
  --owner my-org --repo my-repo --pr 42 \
  && \
  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

# Handle PR_NOT_STALE gracefully in a shell script
output=$(npx --yes tsx /app/.opencode/skills/auto-agents-system/scripts/rebase_pr.ts \
  --url https://git.example.com --pat ghp_XXXX \
  --owner my-org --repo my-repo --pr 42 2>&1)
exit_code=$?
if [ $exit_code -ne 0 ]; then
  if echo "$output" | grep -q "PR_NOT_STALE"; then
    echo "PR is already current — proceeding to merge"
    # call merge_pr
  else
    echo "Rebase failed: $output"
    exit 1
  fi
fi
```


## Agent Usage Examples

### pr-merge-worker

The worker is the **primary caller** of this script, specifically on the
`stale_no_conflicts` path:

```
# Step 1: Server-side rebase (stale_no_conflicts path)
npx --yes tsx /app/.opencode/skills/auto-agents-system/scripts/rebase_pr.ts \
  --url $FORGEJO_URL --pat $FORGEJO_PAT \
  --owner $REPO_OWNER --repo $REPO_NAME \
  --pr $PR_NUMBER

# Step 2: Immediately schedule the merge
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
```

The worker does **not** call `rebase_pr` for the `stale_with_conflicts` path —
that path requires a local clone with manual conflict resolution.

### pr-merge-pool-supervisor

The supervisor never calls this script directly. It dispatches `pr-merge-worker`
subagents (via the Task tool, blocking), which call this script as part of
the stale-clean processing path.

### system-watchdog-pool-supervisor

The watchdog would not normally call `rebase_pr`. However, it could be
used in an emergency intervention workflow to manually rebase a PR that the
merge supervisor has been unable to process.

### grooming-pool-supervisor

The grooming supervisor focuses on issue/PR metadata and would not normally
call `rebase_pr`. However, it might verify that `rebase_pr` is available
and runnable as part of a pipeline health check.


## Dependencies

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