Validate a Forgejo pull request — checks, conventions, conflicts — and merge it once the human confirms in free text. Resolves merge conflicts (grilling on semantic ones), then closes the linked issue and cleans up. Use when the user says "land", "land-pr", "validate a PR", "review and merge", "merge this PR", "/land-pr", "/land-pr <N>", or wants to ship an open pull request.
Installs into .claude/skills of the current project.
Are you the author of Land Pr?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/cloonar-land-pr)
---
name: land-pr
description: Validate a Forgejo pull request — checks, conventions, conflicts — and merge it once the human confirms in free text. Resolves merge conflicts (grilling on semantic ones), then closes the linked issue and cleans up. Use when the user says "land", "land-pr", "validate a PR", "review and merge", "merge this PR", "/land-pr", "/land-pr <N>", or wants to ship an open pull request.
---
# land-pr
Take a pull request on the current repo's Forgejo tracker from "open" to "merged": **validate** it, **resolve** any merge conflict (grilling the human only when the resolution is a real judgement call), and **merge** it — but only on explicit free-text confirmation.
This skill is global: it operates on whatever repo it is invoked in, the same way `triage` does. It learns each repo's conventions at runtime rather than hard-coding them — see [Learn the repo's rules](#learn-the-repos-rules). The definition of **landable** it validates against lives in `validation-core.md` in this skill's directory — read it; it is the single source both this skill and the autoland lander apply.
The whole tracker surface is `labctl`: `LAB_URL` and `LAB_TOKEN` are already in the session environment, it targets the run's repo from the run token, and it needs no login or `--login`/`--repo` flags.
Every tracker write this skill makes (a `pr approve` body, a `pr comment`) **must** start with this disclaimer:
```
> *This was generated by AI while landing a PR.*
```
## Non-negotiable rules
- **Merging needs explicit free-text approval, per PR.** A merge is a write to the default branch. Do **not** use `AskUserQuestion` for it — the auto-mode classifier does not treat its answers as authorisation. And `labctl pr merge` is an API call, not a `git push`, so the classifier will not prompt for it on its own: *this skill is the gate*. Validate, approve, present the verdict, ask the human in plain text, and run `labctl pr merge` **only** on an explicit free-text go-ahead for **that one PR**. An `approve` is **not** a merge — it records that validation passed; only free text merges. Never offer a "merge everything green" batch.
- **Never force-push, never push to the base branch.** Conflict-resolution pushes go to the PR's **head branch** only, with an ordinary push. If the head branch is one the human does not own (a fork / external contributor), do **not** push and do **not** delete it — stop and hand back. (In practice every PR here is authored as `dominik`, AFK or manual.)
- **Require a clean working tree before checking out a PR.** Checking out the head switches the working tree. If `git status` is not clean, stop — never risk clobbering uncommitted work. Restore the prior branch when done.
- **Prefer a signal that already vouches over re-running anything.** Re-running an expensive gate the repo already enforced is waste. Only run the project's own checks yourself when nothing already vouches (see [Learn the repo's rules](#learn-the-repos-rules)).
## Preconditions
`labctl` reaches the tracker over the lab API — no forge login, no `--login`/`--repo` flags, and it works from anywhere in the worktree. Two things must hold:
- **`LAB_URL` and `LAB_TOKEN` are present** — without them `labctl` can't reach the tracker. (Every lab session has them.)
- **A git worktree with an `origin` remote** — conflict resolution fetches, checks out, and pushes the PR head over `origin`.
If either is missing, stop and say so.
## Invocation
The human invokes `/land-pr` and may name a PR or describe intent in natural language:
- `/land-pr` (no argument), or a **plural-intent** phrasing like "land the afk PRs" → **list, pick, loop**: cheaply list the open PRs and let the human pick which to land — deferring all validation until a PR is picked — or, when exactly one open non-draft PR exists, run the full flow on it directly. See [No argument: list, pick, loop](#no-argument-list-pick-loop).
- `/land-pr 42` → drive PR **#42** through the full flow to a (free-text-gated) merge.
- "is #42 ready?", "merge #42" → interpret and act: a single named PR drives the full flow directly.
## The state machine
Verdicts are verbs; do **not** invent labels.
```
open ──validate──► [blockers?] ──yes──► report findings in session (no forge write)
│ ▲
│ no └── author fixes, re-invoke ──┐
▼ │
PASS ──labctl pr approve──► (free-text go-ahead) ──► merge ──► closed ─────┘
```
- **Blockers** (`FAIL` or blocking `CONCERNS`) → report the findings **in the session**, file-and-line, and stop. The interactive skill makes **no forge write on a failed validation** — the human you are talking to is the audience. An operator who wants autoland to pick the rejection up posts their **own** *changes requested* review in the forge UI (a human review feeds the autoland loop directly); re-invoke this skill after the author has fixed.
- **Clean** (`PASS`) → `labctl pr approve <N>` with a short summary body — CONCERNS, when present, go in that body. This records the forge-observable "validated, awaiting confirm" state — but it is **not** a merge: the merge still waits on the human's free-text go-ahead.
- **The approve body is the audit record** on a `PASS` — what was checked, which signal vouched, any CONCERNS (disclaimer line first). Extra progress notes (e.g. a conflict you resolved) go as `labctl pr comment <N> "<body>"`. `labctl pr view <N>` prints submitted human reviews after the body, so an outstanding *changes requested* from a human reviewer is visible there.
## Learn the repo's rules
Discover how to verify and merge *this* repo, first hit wins:
1. **PR CI / check-runs**, if the repo has them — cheapest and most authoritative. Read them with `labctl pr checks --wait <N>`: it prints the aggregate (`success | failure | pending | none`) plus one tab-separated row per check, waits out an in-flight run itself (~10s poll, ~5 min cap, all inside one call), and exits `0` on green/none · `2` on red · `3` if still pending at the cap. Green = a vouching signal; red = a blocker. *(This repo runs Forgejo Actions CI — its combined status is the signal to read; don't re-run what it already ran.)*
2. **The repo's `CLAUDE.md`** — the norm across the fleet. It declares the build/test/lint commands, the gate model, and commit conventions. If it names a gate that runs before or at commit time, that gate already vouches — read it, don't re-run it.
3. **Ecosystem auto-detect**, when there's no `CLAUDE.md`: `go.mod` → `go test ./... && go build ./...`; `composer.json` → `composer test` / `phpunit`; `package.json` → the test/build/lint scripts it declares; `flake.nix` / `*.nix` → `nix build` / eval; `Makefile` → `make test`.
## Validate a PR
Read `validation-core.md` in this skill's directory — it is the single definition of **landable** (the checks aggregate, the conventions, the conflict policy, the `PASS`/`CONCERNS`/`FAIL` rubric). This section drives that definition; it does not restate it.
1. **Fetch & gate.** `labctl pr view <N>` — read state, head, base, title, body, and any submitted reviews (printed after the body). Confirm it's open and not a draft. Skip drafts with a note.
2. **Checks.** Run `labctl pr checks --wait <N>` and read the aggregate per `validation-core.md`:
- **Exit `0`, `state: success`** — a signal vouches. **Do not re-run it** — say so in the verdict.
- **Exit `0`, `state: none`** — nothing vouches (no CI configured, or a commit-time gate the checks call can't see). Run the project's own checks now.
- **Exit `2`** (aggregate `failure`) — a blocker: report the failing check rows and stop for the human. The fix belongs to the PR author, not the lander.
- **Exit `3`** (still pending at the cap) — say so, and either re-run the wait or stop. Never merge on a pending signal.
3. **Conventions.** Title is Conventional Commits (`feat:`, `fix(scope):`, …). The body carries a working `Closes #<issue>` — a **blocker** if missing (the AFK contract in `validation-core.md` says why). Apply any extra rules the repo's `CLAUDE.md` states.
4. **Review the diff.** Read the full diff and reason about correctness against the PR's stated intent and its linked issue. For a deeper pass, run `/code-review`. Flag anything that diverges from the issue's intent; if a divergence is ambiguous, **grill** (see below).
5. **Verdict.** Emit `PASS` / `CONCERNS` / `FAIL` per the rubric in `validation-core.md`, with concrete file-and-line findings, naming the signal you relied on. Then route:
- **`PASS`** → `labctl pr approve <N>` with a short summary body — disclaimer line first, non-blocking CONCERNS included in the body — then proceed toward the merge gate:
```
labctl pr approve <N> "> *This was generated by AI while landing a PR.*
<summary; signal relied on; CONCERNS if any>"
```
- **`FAIL` / blocking `CONCERNS`** → report the findings in the session and stop; **no forge write**. The fix belongs to the PR author; an operator who wants autoland to take the rejection up posts their own *changes requested* review in the forge UI.
## Resolve a merge conflict
When merging the PR would conflict with its base. You don't pre-reason the forge's mergeability (branch protection, required checks — that's the backend's call, surfaced verbatim when you attempt the merge); a *textual* conflict you resolve on the head yourself, and you'll see it the moment you merge the base into the head locally. Resolve it before the merge gate so the human's single go-ahead covers the resolved tree:
1. **Working tree must be clean** (else stop — see the non-negotiable rules). Head branch comes from `labctl pr view <N>`.
2. **Fetch & check out the head:** `git fetch origin <head>` then `git checkout <head>`.
3. **Bring the base in** with a merge, never a rebase: `git merge origin/<base>`. (A rebase needs a force-push that rewrites the branch and disrupts the open PR.)
4. **Resolve** per the conflict policy in `validation-core.md`: auto-resolve silently only when the resolution is deterministic and behaviour-preserving; the moment it's a *semantic choice*, **grill** the human — inline and targeted, not a full interview — showing both sides and asking the single question you need (*"PR sets the timeout to 30s, `main` changed it to 60s — which wins, or is there a combined intent?"*), then apply the answer. For a genuinely tangled, multi-point resolution, offer to escalate to a `/grill-me` session.
5. **Commit** the merge and **push** to the head branch (ordinary push, never force, never to base).
6. **Re-verify — mandatory.** The resolution commit is one *no gate has seen*, so the "a signal already vouches" shortcut no longer applies: run the project's own checks on the resolved tree now.
7. **Show the resolution diff** to the human before the merge gate; optionally record it as `labctl pr comment <N> "<summary>"` (disclaimer line first) for the audit trail.
The same principle generalises: act autonomously while the call is safe; **grill the moment a judgement is needed** — a semantic conflict or a validation ambiguity — and only then.
## Merge
Only after a clean verdict (`PASS`, approved) and, if there was a conflict, a re-verified resolution the human has seen:
1. **Free-text gate.** Tell the human the PR is validated and approved, and ask them to confirm the merge in plain text. Proceed only on an explicit go-ahead for this PR. (No `AskUserQuestion`.)
2. **Merge.** `labctl pr merge <N>`. The merge method is the **server's** choice — there is no `--style`/`--method` flag. Mergeability is the backend's call: attempt it and read the result — if the forge refuses (required check red, protected base, or a conflict), it surfaces the refusal's *own words* verbatim and exits non-zero. Don't pre-reason it. A refusal for a **conflict** routes to [Resolve a merge conflict](#resolve-a-merge-conflict), then re-present and re-attempt. Re-merging an already-merged PR is a no-op **success**.
3. **Close the loop.** The merge normally auto-closes the linked issue via its `Closes #<issue>` link. Confirm it closed; if it didn't, comment the reason then `labctl issue close <n>`. There is no claim label to clear — an AFK run claims via its `afk/<N>` branch, not a label, and the merge closing the issue is what releases the claim.
4. **Clean up.** Lab never auto-deletes a head branch on merge — branch lifecycle is guarded teardown's job, not the merge's. Once the PR is merged, delete the head branch yourself: `git push origin --delete <head>` (head from `labctl pr view`), then delete any local copy and restore the working tree to the base branch. Do **not** delete a branch the human doesn't own — leave a fork / external contributor's head alone.
5. **Report, don't babysit.** If the merge triggers the repo's CD, say so and stop — do not block-watch the deploy. The skill's job ends at a clean merge.
## No argument: list, pick, loop
`/land-pr` with no argument — and plural-intent phrasings like "land the afk PRs" — is a **lazy, list-first picker**, not a batch validator. It lists cheaply and defers *all* validation until a PR is picked. There is no batch runner: plural intent is served by the loop below, and every merge keeps its own free-text gate.
1. **List cheaply.** `labctl pr list` (it lists PRs across all states — filter to the open, non-draft ones yourself), plus at most **one** cheap `labctl pr view <N>` per PR for a one-line gloss. Each row shows: PR number, title, a one-line plain-language gloss of what the PR is about, author / head branch, and a status tag only where relevant (`draft`, `changes-requested`). **Read no diff, run no checks, probe no conflicts, and show no PASS/FAIL or mergeable status** — computing those *is* the deferred validation this path exists to avoid.
2. **Route on the count of open, non-draft PRs:**
- **Exactly one** → skip the picker and run the full flow on it directly: [Validate a PR](#validate-a-pr) → (resolve any conflict) → [Merge](#merge). The merge still needs explicit free-text approval.
- **Only draft(s)** → list them tagged `draft`, note none are landable, and stop.
- **Zero** → say so and stop.
- **Two or more** → present the list and wait for the human to pick.
3. **Pick by PR number** in free text. The picked PR runs the full existing flow — [Validate a PR](#validate-a-pr), conflict resolution if needed, [Merge](#merge) — unchanged.
4. **Loop.** After the picked PR reaches a terminal state — merged, failed validation, or merge declined — re-list the remaining open PRs and ask which to do next. A PR that failed validation stays open (its findings were reported in session); one carrying a human's *changes requested* review reappears tagged `changes-requested`. The loop ends when the list is empty or the human says they are done.
No auto-advance: every PR is an explicit pick, and there is no batch-merge path — one free-text go-ahead lands one PR.