--- 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 `, `/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=:`. 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--seen" if [ ! -f "$seen" ]; then # github gh api graphql -f query='{repository(owner:"",name:""){pullRequest(number:){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 --json reviews,reviewRequests --jq '.. | .login? // empty' | grep -qi copilot; then gh api -X POST repos///pulls//requested_reviewers \ -f 'reviewers[]=copilot-pull-request-reviewer[bot]' >/dev/null 2>&1 || true fi ``` **Write the state file** — `/pr--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///pulls//comments//replies -f body="" --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 "" '{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: ""}) { 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 ` for the state, `gh run view --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/ --no-edit # resolve, commit, push ``` `gh pr update-branch ` (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 ` 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.