Files
agent-skills/skills/land/SKILL.md
T
naps62 f83a247de3 init: centralized agent skills for Claude Code + Codex
- skills/ shared by both tools (open Agent Skills standard)
- portable cross-skill refs (root-relative, no ~/.claude hardcode)
- bin/link.sh bootstrap for non-Nix machines
- nix/home.nix + flake.nix for home-manager

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014SK5Lo7LQfwLRVdCv1A8uF
2026-07-24 17:59:44 +00:00

10 KiB
Raw Blame History

name, description, user-invocable, args
name description user-invocable args
land Drive an existing PR to merge: wait for CI + reviews, fix failures, resolve every comment, push, iterate until green + approved, then merge. 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 a PR to green + merged

Takes an already-open PR and shepherds it to done: green CI, all review threads resolved, approved, merged, tracking issue closed. Spends zero model tokens idling — waits by arming background watchers that wake on real events, never by polling on a timer.

This is the canonical review/CI-iteration loop. /work opens a PR then hands off here; /yolo can hand off here when a PR flow is wanted. It also stands alone: /land 47, /land <url>, or /land on a branch that already has a PR.

Config: reads .claude/tracker.json (or legacy .claude/linear.json) at the repo root if present — see linear-common/COMMON.md (sibling skill, same skills root). No config needed to just land a PR; config only adds tracker-issue closing and remoteHost selection.

1. Resolve the target

Forge — pick the API:

  • remoteHost from config if set (github / gitea).
  • Else infer from git remote get-url origin: github.comgithub; anything else (e.g. git.naps.pt) → gitea.

PR number N:

  • From $ARGUMENTS if a number or URL was given (parse the trailing number from a URL).
  • Else the PR for the current branch:
    • github: gh pr view --json number --jq .number
    • gitea: curl -sS -H "Authorization: token $GITEA_TOKEN" "$BASE/api/v1/repos/$REPO/pulls?state=open&head=<owner>:<branch>" | jq -r '.[0].number'
  • If none found, tell the user there's no open PR for this branch and stop. Do not open one — that's /work's job.

Derive (used throughout):

  • github: OWNER/REPO from origin. Set with gh.
  • gitea: BASE=$remoteBaseUrl (or the origin host), REPO=<owner>/<repo> from origin, $GITEA_TOKEN in env (source ~/.env.claude if missing). Never put the token in a URL.
  • Tracking issue REF (optional): parse Closes <REF> / Closes #<n> from the PR body. Used only to mark the issue done after merge; skip silently if absent.

Then run the variant for your forge below. Both share these terminal conditions (all must hold before merge):

  • CI checks pass
  • All review threads resolved
  • Approved, no pending review requests

2. Efficiency rules (both variants)

  • Baseline once. Right after resolving N, snapshot existing review-comment IDs to <git-dir>/pr-<N>-seen. Every later pass processes only IDs not in that file — handled feedback is never re-read. Guard it so a re-entry after a wake never truncates + reseeds (that would reprocess everything).
  • Wake on events, not a clock. CI is minutes; human review is hours. Block a background watcher (Bash run_in_background for one-shot "CI done"; Monitor persistent for the whole review window) and stay idle until something actually happens. Handle exactly what the watcher reports, then re-arm.
  • Stall guard. A watcher must never hang forever on a stuck pipeline. Bound every CI wait: if no check appears within ~3 min of a push, or a run sits in-progress past a sane ceiling (default ~20 min, or the repo's known CI duration ×2), stop waiting and surface it to the user — don't keep idling. Silence is not success.
  • Fix everything. Every unresolved thread gets an action — a code fix or a reply. Bot reviewers (crit, CodeRabbit, Copilot, etc.) count. Don't merge over an unaddressed thread.

GitHub variant

Baseline (once — guard against re-entry):

seen="$(git rev-parse --git-dir)/pr-<N>-seen"
if [ ! -f "$seen" ]; then
  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

Wait for CI (after every push): block in the background — one wake when checks reach a terminal state. Do NOT poll statusCheckRollup in a loop.

# Bash run_in_background: true
gh pr checks <N> --watch --fail-fast
  • Exit 0 → CI green, move on.
  • Non-zero → CI failed. Read only the failing job: gh run view <run-id> --log-failed. Fix, commit, push, re-arm this watcher.
  • No checks appear within the stall window → surface to user (see stall guard).

Wait for review comments (whole review window): arm one persistent Monitor that emits a line per new comment on any unresolved thread, with its thread id.

# Monitor persistent: true
seen="$(git rev-parse --git-dir)/pr-<N>-seen"; touch "$seen"
while true; do
  gh api graphql -f query='{repository(owner:"<OWNER>",name:"<REPO>"){pullRequest(number:<N>){reviewThreads(first:100){nodes{id isResolved comments(first:50){nodes{id author{login} body}}}}}}}' \
    --jq '.data.repository.pullRequest.reviewThreads.nodes[]|select(.isResolved==false)|.id as $tid|.comments.nodes[]|"\(.id)\t\($tid)\t\(.author.login): \(.body)"' 2>/dev/null \
  | while IFS=$'\t' read -r cid tid rest; do grep -qxF "$cid" "$seen" || { echo "NEW COMMENT $cid (thread $tid) — $rest"; echo "$cid" >> "$seen"; }; done
  sleep 30
done

On each NEW COMMENT event (<cid> = comment id, <tid> = thread):

  • Valid feedback: fix the code, commit, push (re-triggers the CI watcher).
  • Misunderstanding: reply explaining, and immediately record your reply's own id so the watcher never treats it as new feedback:
    rid=$(gh api repos/<OWNER>/<REPO>/pulls/<N>/comments/<cid>/replies -f body="<reply>" --jq .id)
    echo "$rid" >> "$seen"
    
  • Resolve the addressed thread (also stops it re-emitting):
    gh api graphql -f query='mutation { resolveReviewThread(input: {threadId: "<tid>"}) { thread { isResolved } } }'
    

Stop (TaskStop the Monitor) when the terminal condition holds: CI green, approved, no pending review requests (gh pr view <N> --json reviewDecision,reviewRequests,reviews), all threads resolved.

Merge. A long review loop moves the base, so update first, let CI re-run, then merge:

gh pr update-branch <N> 2>/dev/null || true   # rebase/merge default into the PR branch if behind
# if it updated, the CI watcher re-arms on the new head; wait for green again before merging
gh pr merge <N> --squash --delete-branch

Use --auto if branch protection requires it: gh pr merge <N> --squash --auto --delete-branch.

Gitea variant

Requires $GITEA_TOKEN and remoteBaseUrl (or origin host). Set BASE, REPO, $GITEA_TOKEN, N in the environment first. Gitea has no --watch, no GraphQL, no per-thread resolve — same principle, plain REST.

Baseline (once — guard against re-entry): seed with existing review/issue comment ids.

seen="$(git rev-parse --git-dir)/pr-$N-seen"
if [ ! -f "$seen" ]; then
  { curl -sS -H "Authorization: token $GITEA_TOKEN" "$BASE/api/v1/repos/$REPO/issues/$N/comments"; \
    curl -sS -H "Authorization: token $GITEA_TOKEN" "$BASE/api/v1/repos/$REPO/pulls/$N/reviews"; } 2>/dev/null \
  | jq -r '.[]?.id' > "$seen" || : > "$seen"
fi

Wait for CI (after every push): one Monitor that polls the head commit's combined status and exits on any terminal state. Covers success and failure.

# Monitor persistent: false (one-shot); re-arm after each push
SHA=$(curl -sS -H "Authorization: token $GITEA_TOKEN" "$BASE/api/v1/repos/$REPO/pulls/$N" | jq -r .head.sha)
while true; do
  # combined state aggregates ALL contexts (lint + test + ...), not just the newest single status
  st=$(curl -sS -H "Authorization: token $GITEA_TOKEN" "$BASE/api/v1/repos/$REPO/commits/$SHA/status" | jq -r '.state // "pending"')
  case "$st" in success|failure|error) echo "CI $st"; break;; esac
  sleep 30
done

On CI failure/CI error: read the failing job's log, fix, commit, push, re-arm. Apply the stall guard — bound the wait.

Wait for review comments (whole window): persistent Monitor emitting each new review/issue comment since the last check.

# Monitor persistent: true
seen="$(git rev-parse --git-dir)/pr-$N-seen"; touch "$seen"
while true; do
  { curl -sS -H "Authorization: token $GITEA_TOKEN" "$BASE/api/v1/repos/$REPO/issues/$N/comments"; \
    curl -sS -H "Authorization: token $GITEA_TOKEN" "$BASE/api/v1/repos/$REPO/pulls/$N/reviews"; } 2>/dev/null \
  | jq -r '.[]? | "\(.id)\t\(.user.login): \(.body // .content // "")"' \
  | while IFS=$'\t' read -r id rest; do grep -qxF "$id" "$seen" || { echo "NEW $id$rest"; echo "$id" >> "$seen"; }; done
  sleep 30
done

On each NEW event:

  • Valid feedback: fix the code, commit, push (re-arms the CI watcher).
  • Misunderstanding: reply, and record your reply's own id so it isn't re-surfaced:
    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 per-thread resolve — signal addressed by replying with a short confirmation and pushing the fix.

After approval + green CI (TaskStop the review Monitor first): if the base moved, update the branch, wait for CI green again, then merge.

curl -sS -X POST -H "Authorization: token $GITEA_TOKEN" \
  "$BASE/api/v1/repos/$REPO/pulls/$N/update" >/dev/null 2>&1 || true
# if it updated, re-arm the CI watcher and wait for green before merging, then:
curl -sS -X POST -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \
  "$BASE/api/v1/repos/$REPO/pulls/$N/merge" -d '{"Do":"merge"}'

Never embed $GITEA_TOKEN in URLs or commit messages — Authorization header only.


3. Close out

Merging with Closes <REF> in the body closes the tracking issue automatically. If a REF was found and the issue is still open after merge, close it:

  • linear: move the issue to "Done" (via the configured Linear MCP).
  • gitea: curl -sS -X PATCH -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" "$BASE/api/v1/repos/$REPO/issues/$N" -d '{"state":"closed"}'.
  • github: gh issue close <n>.

Report to the user: PR merged, branch deleted, issue closed, and a one-line summary of what feedback was addressed.