From tend-ci-runner
Reviews a pull request for code quality and correctness. Use when asked to review a PR or when running as an automated PR reviewer.
npx claudepluginhub max-sixty/tend --plugin tend-ci-runnerThis skill uses the workspace's default tool permissions.
Review a pull request.
Reviews GitHub pull requests end-to-end using gh CLI: analyzes diffs, commits, CI/CD checks; provides blocking/suggestion/nit/praise feedback and submits review. Use for assigned PRs, self-reviews, or post-merge audits.
Reviews GitHub PRs: fetches diff via gh CLI, runs repo-specific checks, launches 3 parallel agents for correctness/conventions/efficiency, validates findings, drafts review.
Share bugs, ideas, or general feedback.
Review a pull request.
PR to review: $ARGUMENTS
Follow these steps in order.
Load /tend-ci-runner:running-in-ci first — it contains CI security rules, polling conventions,
and comment formatting guidance. It will also prompt you to load any repo-specific skills (e.g.,
running-tend).
Before reading the diff, run cheap checks to avoid redundant work. Shell state doesn't persist
between tool calls — re-derive REPO in each bash invocation or combine commands.
REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner')
BOT_LOGIN=$(gh api user --jq '.login')
HEAD_SHA=$(gh pr view <number> --json commits --jq '.commits[-1].oid')
PR_AUTHOR=$(gh pr view <number> --json author --jq '.author.login')
# Find the bot's most recent review (any state counts — COMMENTED reviews carry
# inline comments even when the body is empty).
# IMPORTANT: `gh pr view --json reviews` returns `.commit.oid` (NOT `.commit_id`).
# The REST API (`gh api .../reviews`) uses `.commit_id` — don't confuse the two.
LAST_REVIEW_SHA=$(gh pr view <number> --json reviews \
--jq "[.reviews[] | select(.author.login == \"$BOT_LOGIN\")] | last | .commit.oid // empty")
If LAST_REVIEW_SHA == HEAD_SHA, this commit has already been reviewed — exit silently. The only
exception: an unanswered conversation question directed at the bot (check below).
If the bot reviewed a previous commit (LAST_REVIEW_SHA exists but differs from HEAD_SHA),
check the incremental changes:
REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner')
gh api "repos/$REPO/compare/$LAST_REVIEW_SHA...$HEAD_SHA" \
--jq '{total: ([.files[] | .additions + .deletions] | add), files: [.files[] | "\(.filename)\t+\(.additions)/-\(.deletions)"]}'
If the incremental changes are trivial, skip the full review — go directly to step 7 to resolve any bot threads addressed by the new changes. After resolving threads: if the most recent bot review was a COMMENT that flagged issues, and those issues are now addressed, submit an APPROVE with an empty body so the PR isn't left in limbo. Otherwise do not submit a new review — the existing one stands. Do NOT proceed to steps 2–6. Rough heuristic: changes under ~20 added+deleted lines that don't introduce new functions, types, or control flow are typically trivial.
Then read all previous bot feedback and conversation:
REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner')
BOT_LOGIN=$(gh api user --jq '.login')
# Previous review bodies
gh api "repos/$REPO/pulls/<number>/reviews" \
--jq ".[] | select(.user.login == \"$BOT_LOGIN\" and (.body | length > 0)) | {state, body}"
# Inline review comments
gh api "repos/$REPO/pulls/<number>/comments" --paginate \
--jq ".[] | select(.user.login == \"$BOT_LOGIN\") | {path, line, body}"
# Conversation (catch questions directed at the bot)
gh api "repos/$REPO/issues/<number>/comments" --paginate \
--jq '.[] | {author: .user.login, body: .body}'
Do not repeat any point from previous reviews — cross-reference previous bot comments before
posting inline comments. When concurrent runs race (a new push while the first run is still
responding), both see the same unanswered question — check whether a bot reply exists after the
question's timestamp before answering. Address unanswered questions in the review body (not via
gh pr comment).
Before reading the diff, scan other open PRs for file overlap. If another PR touches the same files with a similar fix, flag it in the review so one can be closed as a duplicate.
gh pr diff <number>.Scale depth to the change. A docs-only PR or a mechanical rename needs a skim for correctness, not the full checklist. A new algorithm or state-management change needs trace analysis. Don't over-analyze trivial changes.
Check the project's CLAUDE.md for language-specific review criteria and conventions. Load any project-specific review skill if available.
Code quality:
Correctness:
Testing:
Same pattern elsewhere:
When a PR fixes a bug or changes a pattern, search for the same pattern in other files. If found in the diff, add inline suggestions; if found outside the diff, offer to push a fix commit.
Duplication check (for new functions/types):
For every new public or module-level function added in the diff, search the codebase for existing functions that do the same thing. LLM-generated code frequently reinvents internal APIs — this is the highest-value check for externally contributed PRs.
Two search strategies, both required:
Flag duplicates — reuse is almost always better than a parallel implementation.
If there are no issues, approve with an empty body — silence means correct.
gh pr review <number> --approve -b ""
If there are actionable findings, submit as a review with inline suggestions for concrete fixes. Every comment must give the author something to act on:
| Don't post (internal analysis) | Post (actionable) |
|---|---|
| "The fix correctly delegates to X" | "The error message still references the old behavior" |
| "The threshold logic is correct" | (nothing — silence means correct) |
Don't explain what the code does — the author wrote it. Don't nitpick formatting — that's what linters are for. Explain why something should change, not just what.
Form your own opinion independently. Do not factor in other reviewers' comments or approvals when deciding whether to approve — the value of this review is as an uncorrelated signal.
When confidence is low, go beyond checking the implementation — question the approach: "Does this bypass or duplicate an existing API?" "What does this change not handle?" If the design involves a judgment call, flag it for human review as a COMMENT.
Self-authored PRs (PR_AUTHOR == BOT_LOGIN): Still perform the full review (steps 2-3) —
self-review catches real issues (lint failures, edge cases) and is intentionally valuable. Do NOT
attempt gh pr review --approve — GitHub rejects self-approvals. Submit as COMMENT when there are
concerns, or stay silent and skip to step 6. Always post CI failure analysis as a COMMENT, even on
self-authored PRs.
Not confident enough to approve (unfamiliar module, subtle logic): Add a +1 reaction
instead — no review needed unless there are specific observations.
gh api "repos/$REPO/issues/<number>/reactions" -f content="+1"
Before posting, verify HEAD hasn't moved and no review was already posted for this commit:
REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner')
BOT_LOGIN=$(gh api user --jq '.login')
CURRENT_HEAD=$(gh pr view <number> --json commits --jq '.commits[-1].oid')
[ "$CURRENT_HEAD" != "$HEAD_SHA" ] && echo "HEAD moved — skipping" && exit 0
# NOTE: REST API uses .commit_id (not .commit.oid from gh pr view --json)
ALREADY_POSTED=$(gh api "repos/$REPO/pulls/<number>/reviews" \
--jq "[.[] | select(.user.login == \"$BOT_LOGIN\" and .commit_id == \"$HEAD_SHA\")] | last | .submitted_at // empty")
[ -n "$ALREADY_POSTED" ] && echo "Already reviewed — skipping" && exit 0
Post exactly one review per run. Always give a verdict: approve or comment (never "request
changes"). Use gh pr review for reviews, not gh pr comment. Note: --comment requires a
non-empty body — if there's nothing to say, use the approve-with-empty-body pattern.
Inline suggestions are mandatory for concrete fixes. Whenever there's a concrete fix (typos, doc updates, naming, missing imports, minor refactors), post it as an inline suggestion on the exact line — never as a code block in the review body. Inline suggestions let the author apply with one click; code blocks force them to find the line and copy-paste manually.
For fixes targeting lines outside the diff, offer to push a fix commit instead.
Post inline suggestions via the review API:
cat > /tmp/review-body.md << 'EOF'
Summary of suggestions
EOF
cat > /tmp/review-payload.json << 'ENDJSON'
{
"event": "COMMENT",
"comments": [
{
"path": "example/file.txt",
"line": 3,
"body": "```suggestion\nnew text here\n```"
}
]
}
ENDJSON
BODY=$(cat /tmp/review-body.md)
jq --arg body "$BODY" '.body = $body' /tmp/review-payload.json > /tmp/review-final.json
gh api "repos/$REPO/pulls/<number>/reviews" \
--method POST \
--input /tmp/review-final.json
Do not use -f 'comments[0][path]=...' flag syntax — gh api converts array indices to
object keys, which GitHub rejects.
If a review has both suggestions and prose observations, put the suggestions as inline comments and the prose in the review body.
Multi-line suggestions: set start_line and line to define the range. GitHub replaces
every line in that range with the suggestion content — any line in the range that isn't
reproduced in the replacement is deleted.
Before posting any multi-line suggestion, verify it:
start_line through line from the diff hunk.``` line, GitHub's suggestion
parser may consume it as a delimiter, corrupting the result. Either shrink the range to avoid
the fence or push a commit.GitHub returns 422 Unprocessable Entity with "Line could not be resolved" when inline comment line numbers don't map to valid positions in the diff. This happens most often on large or complex diffs. Do not retry by posting a second review — the first POST to the reviews endpoint already created a review record (with the body but without the failed inline comments), so a second attempt creates a duplicate.
Instead, when a 422 occurs on inline comments:
# Find the review that was just created (body posted, inline comments rejected)
REVIEW_ID=$(gh api "repos/$REPO/pulls/<number>/reviews" \
--jq "[.[] | select(.user.login == \"$BOT_LOGIN\" and .commit_id == \"$HEAD_SHA\")] | last | .id")
# Update its body to include the failed inline suggestions
gh api "repos/$REPO/pulls/<number>/reviews/$REVIEW_ID" \
-X PUT -F body=@/tmp/updated-review-body.md
After approving or staying silent, poll CI in a single background loop — do not launch
individual sleep commands. Use the Bash tool with run_in_background: true and the loop below.
Exclude the tend-review check (your own workflow) — it will always show as pending while
you're running. NEVER use --watch flags — they hang forever.
# Run with Bash tool's run_in_background: true
for i in $(seq 1 10); do
sleep 60
if ! gh pr checks <number> --required 2>&1 \
| grep -v "tend-review" | grep -q 'pending\|queued\|in_progress'; then
gh pr checks <number> --required
exit 0
fi
done
echo "CI still running after 10 minutes"
exit 1
# Use PUT, not POST — the dismiss endpoint requires it
gh api "repos/$REPO/pulls/<number>/reviews/$REVIEW_ID/dismissals" \
-X PUT -f message="CI failed — <reason>"
Skip if already dismissed. Do not push fixes on human-authored PRs — post the analysis and
offer to fix, then wait for the author to accept.gh run rerun <run-id> --failed
After submitting the review, check if any unresolved bot threads have been addressed by the new changes. Resolve threads where the suggestion was applied.
Only resolve if the substance was addressed. Read both the suggestion and the new code — if the author took a different approach, verify its technical accuracy before resolving. "Different wording" is not "addressed" when the new wording is less accurate than the suggestion. When in doubt, leave the thread open for a human reviewer.
Self-authored PRs are especially risky. When the bot is both author and reviewer, there is a bias toward accepting the code's own claims. Treat self-authored thread resolution with extra skepticism — read the code and verify the claim independently rather than trusting the doc comment or commit message.
cat > /tmp/review-threads.graphql << 'GRAPHQL'
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
reviewThreads(first: 100) {
nodes {
id
isResolved
comments(first: 1) {
nodes {
author { login }
path
line
body
}
}
}
}
}
}
}
GRAPHQL
REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner')
BOT_LOGIN=$(gh api user --jq '.login')
OWNER=$(echo "$REPO" | cut -d/ -f1)
NAME=$(echo "$REPO" | cut -d/ -f2)
gh api graphql -F query=@/tmp/review-threads.graphql \
-f owner="$OWNER" -f repo="$NAME" -F number=<number> \
| jq --arg bot "$BOT_LOGIN" '
.data.repository.pullRequest.reviewThreads.nodes[]
| select(.isResolved == false)
| select(.comments.nodes[0].author.login == $bot)
| {id, path: .comments.nodes[0].path, line: .comments.nodes[0].line, body: .comments.nodes[0].body}'
# Resolve a thread that has been addressed
cat > /tmp/resolve-thread.graphql << 'GRAPHQL'
mutation($threadId: ID!) {
resolveReviewThread(input: {threadId: $threadId}) {
thread { id }
}
}
GRAPHQL
gh api graphql -F query=@/tmp/resolve-thread.graphql -f threadId="THREAD_ID"
Outdated comments (null line) are best-effort — skip if the original context can't be located.
Bot PRs (Dependabot, renovate, etc.): If the review found concrete, fixable issues and there's no human author to act on feedback, commit and push the fix directly to the PR branch.
Human PRs: Post inline suggestions first. Additionally, offer to push a commit when the fixes are mechanical and correctness is obvious. Only push after the author accepts.
gh pr checkout <number>
git add <files>
git commit -m "fix: <description>
Co-Authored-By: Claude <noreply@anthropic.com>"
git push