Files
agent-skills/skills/land/SKILL.md
T
Miguel Palhas d3c3063f06
ci / nix (pull_request) Successful in 7s
ci / lint (pull_request) Successful in 10s
docs(land): gitea resolve API is 1.26+, not 1.23
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 16:34:36 +01:00

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
name description required
target PR number (e.g. 47), a PR URL, or omit to use the PR for the current branch 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 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 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:

    # 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-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.

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.