237 lines
8.9 KiB
Markdown
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.
|