5d1a81ec48
The gitea posting snippet used the issue-comment endpoint, which has no path or line, so findings named `path:line` in prose instead of landing on the code. Both forges now post one COMMENT review carrying anchored findings in comments[] and loose ones in the body. land's gitea baseline missed review-comment ids, which would replay every code comment on the first hint. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
200 lines
7.6 KiB
Markdown
200 lines
7.6 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. Never merges. 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 button: 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*.
|
|
|
|
**Never merge.** The final click is the user's — every repo, every
|
|
forge. No `gh pr merge`, no merge API call, no `--auto`.
|
|
|
|
**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 `linear-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.
|
|
|
|
### `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. Record
|
|
the review id and its comment ids.
|
|
|
|
- **Resolve the thread** (github only — gitea has no per-thread
|
|
resolve, so a short confirming reply plus the pushed fix is the
|
|
signal):
|
|
|
|
```bash
|
|
gh api graphql -f query='mutation { resolveReviewThread(input: {threadId: "<tid>"}) { thread { isResolved } } }'
|
|
```
|
|
|
|
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.
|
|
|
|
Update the branch if the base moved (above), let CI re-run, and wait for
|
|
the resulting hint. The user's click should be the only step left.
|
|
|
|
## 4. Close out
|
|
|
|
Report: PR ready to merge with its link, CI green, approved, threads
|
|
resolved, and one line on what feedback was addressed. 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.
|
|
|
|
`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.
|