7.9 KiB
name, description, user-invocable, args
| name | description | user-invocable | args | |||||||
|---|---|---|---|---|---|---|---|---|---|---|
| land | 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). | true |
|
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 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:
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:
{ 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.
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.
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:
# github rid=$(gh api repos/<OWNER>/<REPO>/pulls/<N>/comments/<cid>/replies -f body="<reply>" --jq .id) echo "$rid" >> "$seen"# 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 samepathandnew_position— gitea groups code comments by position into one conversation (new_position: 0for a file-level comment). Record the review id and its comment ids. -
Resolve the thread once addressed — fix pushed or reply posted:
# github gh api graphql -f query='mutation { resolveReviewThread(input: {threadId: "<tid>"}) { thread { isResolved } } }'# 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-failedfor 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.
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 # 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.