Files
agent-skills/skills/land/SKILL.md
T
Miguel Palhas ca02107d42
ci / nix (push) Successful in 10s
ci / lint (push) Failing after 12s
fix(pr): pin agent-meta marker, add harness field
2026-08-26 10:34:52 +01:00

237 lines
8.9 KiB
Markdown

---
name: land
description: "Drive a PR you authored to ready-to-merge: fix CI failures, address every review comment, resolve conflicts, push, until green + approved. Merges it on Gitea; on GitHub it stops and leaves the click to the user. Event-driven — the PR daemon wakes it. Forge-agnostic (GitHub or Gitea)."
user-invocable: true
args:
- name: target
description: "PR number (e.g. 47), a PR URL, or omit to use the PR for the current branch"
required: false
---
# Land — drive your own PR to ready-to-merge
Takes a PR **you authored** and shepherds it to the merge: green CI,
every review thread addressed, approved, branch up to date. For PRs
someone else authored, use `review-pr` instead — it reads and comments
and never pushes.
Read `pr-common/COMMON.md` (sibling skill, same skills root) first. It
defines hints, the seen file, the state file, and forge resolution. This
document only covers what to *do*.
**Merging is forge-scoped.** On **GitHub**, never merge — the final
click is the user's. No `gh pr merge`, no `--auto`. On **Gitea**, merge
the PR yourself once section 3's conditions all hold; those are the
user's own self-hosted repos and the click adds nothing.
**Spend nothing while idle.** You do not wait, poll, or arm watchers.
The daemon wakes you with a hint when something changes. Do the work the
hint points at, then end the turn. The exception is the no-daemon
fallback in `COMMON.md`.
Entered three ways: a hint (`skill=land`), a handoff from `/work` or
`/yolo` right after the PR is opened, or by hand — `/land 47`, `/land
<url>`, `/land` on a branch with an open PR.
**Config:** `.claude/tracker.json` (or legacy `.claude/linear.json`) at
the repo root, if present — see `tracker-common/COMMON.md`. Only needed
for tracker-issue closing and `remoteHost`.
## 1. Setup pass
Runs once per PR, on first entry. Everything here is work no event will
ever trigger, which is why `/work` still calls this skill at PR-open
time instead of leaving it to the first hint.
**Resolve `N`:** from `$ARGUMENTS` if given (parse the trailing number
of a URL), else the PR for the current branch — `gh pr view --json
number --jq .number`, or on gitea
`GET /repos/$REPO/pulls?state=open&head=<owner>:<branch>`. No open PR:
say so and stop. Do not open one; that's `/work`'s job.
**Baseline the seen file**, guarded against re-entry:
```bash
seen="$(git rev-parse --git-dir)/pr-<N>-seen"
if [ ! -f "$seen" ]; then
# github
gh api graphql -f query='{repository(owner:"<OWNER>",name:"<REPO>"){pullRequest(number:<N>){reviewThreads(first:100){nodes{comments(first:50){nodes{id}}}}}}}' \
--jq '.data.repository.pullRequest.reviewThreads.nodes[].comments.nodes[].id' > "$seen" 2>/dev/null || : > "$seen"
fi
```
Gitea equivalent — issue comments plus reviews:
```bash
{ curl -sS -H "Authorization: token $GITEA_TOKEN" "$BASE/api/v1/repos/$REPO/issues/$N/comments" | jq -r '.[]?.id'
for r in $(curl -sS -H "Authorization: token $GITEA_TOKEN" "$BASE/api/v1/repos/$REPO/pulls/$N/reviews" | jq -r '.[]?.id'); do
echo "$r"
curl -sS -H "Authorization: token $GITEA_TOKEN" "$BASE/api/v1/repos/$REPO/pulls/$N/reviews/$r/comments" | jq -r '.[]?.id'
done; } 2>/dev/null > "$seen" || : > "$seen"
```
**Request the Copilot review** (github only, once). Its comments then
arrive as ordinary `reason=comments` hints. Re-request only if its last
review is 2+ days old.
```bash
if ! gh pr view <N> --json reviews,reviewRequests --jq '.. | .login? // empty' | grep -qi copilot; then
gh api -X POST repos/<OWNER>/<REPO>/pulls/<N>/requested_reviewers \
-f 'reviewers[]=copilot-pull-request-reviewer[bot]' >/dev/null 2>&1 || true
fi
```
**Write the state file**`<git-dir>/pr-<N>-state.md` with phase, head
SHA, and anything already outstanding. Then check whether CI is already
running and handle it as `reason=ci` below.
Then **end the turn**. Do not wait for anything.
## 2. Handling a hint
Each reason is one query. Nothing new: return silently, per
`COMMON.md`. Update the state file whenever the phase or head SHA
changes. Every body you post ends with the metadata marker from
`COMMON.md`, verbatim — the `agent-meta` JSON object, never an
invented tag:
```
<!-- agent-meta: {"model":"<model-id>","harness":"<harness>","session":"<sid>"} -->
```
### `reason=comments`
List review and issue comments, drop every id already in the seen file,
and act on what's left:
- **Valid feedback** — fix the code, commit, push. Record the id.
- **Misunderstanding** — reply, and record the reply's own id in the
same step:
```bash
# github
rid=$(gh api repos/<OWNER>/<REPO>/pulls/<N>/comments/<cid>/replies -f body="<reply>" --jq .id)
echo "$rid" >> "$seen"
```
```bash
# gitea
rid=$(curl -sS -X POST -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \
"$BASE/api/v1/repos/$REPO/issues/$N/comments" -d "$(jq -nc --arg body "<reply>" '{body:$body}')" | jq -r .id)
echo "$rid" >> "$seen"
```
Gitea has no reply endpoint, so that lands as a loose PR comment. To
answer a code comment inside its own thread, post a review instead
whose `comments[]` entry repeats the same `path` and `new_position` —
gitea groups code comments by position into one conversation
(`new_position: 0` for a file-level comment). Record the review id
and its comment ids.
- **Resolve the thread** once addressed — fix pushed or reply posted:
```bash
# github
gh api graphql -f query='mutation { resolveReviewThread(input: {threadId: "<tid>"}) { thread { isResolved } } }'
```
```bash
# gitea (1.26+; on 404 fall back to a confirming reply as the signal)
curl -sS -X POST -H "Authorization: token $GITEA_TOKEN" \
"$BASE/api/v1/repos/$REPO/pulls/comments/<cid>/resolve"
```
Every unresolved thread gets an action — a fix or a reply. Bot
reviewers (Copilot, CodeRabbit, crit) count. Never declare the PR ready
over an unaddressed thread.
### `reason=ci`
The head SHA moved, so checks are running or done.
- github: `gh pr checks <N>` for the state, `gh run view <run-id>
--log-failed` for a failure. Read only the failing job.
- gitea: `GET /repos/$REPO/commits/$SHA/status` — the combined state
aggregates every context, not just the newest.
Failing: fix, commit, push. That produces another `reason=ci` hint when
the new head lands, so don't wait for it.
Still pending: return silently. The next hint carries the result.
**No checks at all a few minutes after a push** is worth surfacing to
the user rather than assuming — silence is not success. You have no
watcher to time out, so judge it from the timestamps you can see.
### `reason=conflicts`
The base moved under the PR. Merge base into the branch — never rebase
and force-push mid-review, which detaches every existing review
comment.
```bash
git fetch origin && git merge origin/<base> --no-edit
# resolve, commit, push
```
`gh pr update-branch <N>` (github) or `POST /repos/$REPO/pulls/$N/update`
(gitea) does the same thing server-side when there's nothing to resolve
by hand.
### `reason=state`
Read the PR state. Merged or closed: write the outcome to the state
file and go to close-out. Draft flipped to ready: nothing to do beyond
noting it. Anything else — usually a label change — is the empty case:
return silently.
## 3. Ready
All of these must hold: CI green, every thread resolved, approved with
no pending review requests, branch not behind the base.
On gitea, a PR with no reviewer ever requested and no review posted
counts as approved — otherwise a solo PR waits forever for a review
that is never coming. A requested or posted review still has to land.
Update the branch if the base moved (above), let CI re-run, and wait for
the resulting hint.
**On GitHub**, that is where you stop — the user's click is the only
step left.
**On Gitea**, merge it. Squash, server-side so the PR reads "merged"
and not "closed":
```bash
curl -sS -X POST -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
"$BASE/api/v1/repos/$REPO/pulls/$N/merge" -d '{"Do":"squash"}'
```
Do not delete the branch or remove the worktree — the user handles
cleanup.
## 4. Close out
**Merged (gitea):** report the merge with the PR link, CI green,
threads resolved, and one line on what feedback was addressed.
**Ready but not merged (github):** report PR ready to merge with the
same detail. The merge, the branch delete, and the tracking-issue close
are the user's.
The review window is often hours and the user may be away. Push a
notification so the click can happen from a phone —
`mcp__ha-mcp__ha_call_service`, `domain: "notify"`, service
`mobile_app_pixel_7_naps` (or `blitz.notifyService` from config),
message "PR #<N> ready to merge" plus the URL. On gitea, the same
notification instead says the PR merged.
`Closes <REF>` in the PR body closes a GitHub or Gitea tracking issue on
merge. Only Linear needs follow-up: tell the user to move the issue to
Done after merging, or do it yourself if you're still around.
**Draft PRs on client repos** publish only on an explicit green light
from the user.