Files
agent-skills/skills/land/SKILL.md
T

183 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: land
description: "Drive an existing PR to ready-to-merge: wait for CI + reviews, fix failures, resolve every comment, push, iterate until green + approved, then hand the merge click to the user. Never merges. 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 a PR to green + ready-to-merge
Takes an **already-open PR** and shepherds it to the merge button: green CI, all review threads resolved, approved, branch up to date. Spends **zero model tokens idling** — waits by arming background watchers that wake on real events, never by polling on a timer.
**Never merge.** The final merge click is always the user's — on every repo, every forge. Public repos with other contributors and client repos need a human gate, and a single click on private repos is cheap. No `gh pr merge`, no merge API call, no `--auto`.
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.com`**github**; 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 for the Linear follow-up note in close-out; skip silently if absent.
Then run the variant for your forge below. Both share these **terminal conditions** (all must hold before handing off):
- 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 declare ready over an unaddressed thread.
---
## GitHub variant
**Baseline** (once — guard against re-entry):
```bash
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
```
**Copilot review** (once, right after baselining): if Copilot has neither reviewed nor been requested, request it. Its comments then flow through the normal comment watcher like any other bot reviewer.
```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 # repo may not have Copilot review enabled
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.
```bash
# 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:
```bash
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.
**Ready.** A long review loop moves the base, so update the branch and let CI re-run — the user's click should be the only step left:
```bash
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
```
Then go to step 3 (close out). Do **not** run `gh pr merge` in any form.
## 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.
```bash
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.
```bash
# 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.
```bash
# 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:
```bash
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 and wait for CI green again — the user's click should be the only step left.
```bash
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
```
Then go to step 3 (close out). Do **not** call the merge API.
Never embed `$GITEA_TOKEN` in URLs or commit messages — `Authorization` header only.
---
## 3. Close out
Report to the user: PR ready to merge (link it), CI green, approved, all threads resolved, and a one-line summary of what feedback was addressed. The merge — and the branch delete + tracking-issue close that follow it — is theirs.
The review window is often hours; the user may be away when the PR goes green. Push a notification so the one click can happen from their phone: `mcp__ha-mcp__ha_call_service` with `domain: "notify"`, service `mobile_app_pixel_7_naps` (or `blitz.notifyService` from config), message "PR #<N> ready to merge" + the PR URL.
`Closes <REF>` in the PR body closes the tracking issue automatically on merge (GitHub/Gitea). Only Linear needs follow-up: if config points at Linear, tell the user the issue must be moved to Done after they merge, or move it yourself if you're still around post-merge.