코드 변경의 테스트와 기존 기능에 미치는 영향을 확인하고 저장소 템플릿에 맞춰 PR을 생성하며, 리뷰 요청부터 지적 사항 대응까지 진행합니다.
- 지정된 템플릿으로 PR 생성
- 기존 동작과 새 기능의 테스트 확인
- 로컬 검증이 어려울 때 봇 리뷰 요청
PR 검토와 수정을 반복하며 미해결 의견, CI 실패, 병합 충돌을 확인합니다. 늦게 도착하는 댓글도 기다려 PR을 병합할 수 있는 상태로 정리합니다.
Alternate /pr-review and /pr-address on a PR until the PR is truly mergeable — no new review findings, zero unresolved inline threads, zero unaddressed top-level reviews or issue comments, all CI checks green, and two consecutive quiet polls after CI settles. Use when the user wants a PR polished to merge-ready without setting a fixed number of rounds.
실행 점수 미검증
설치 방법 보기/pr-review와 /pr-address를 번갈아 실행해 PR을 병합 가능한 상태로 정리합니다. 새 지적이 없어질 때까지 검토와 수정을 반복하며, 코드 줄별 논의 해결 여부와 검토 및 댓글에 대한 응답을 확인합니다. 자동 검사인 CI의 결과와 병합 충돌도 점검합니다. 각 단계의 요약만으로 완료를 판단하지 않고 GitHub의 실제 상태를 조회합니다.
검토 한 번으로 끝내지 않고 수정 후 재확인까지 맡기고 싶을 때 유용합니다. CI 종료 후에도 60초 이상 간격으로 두 번 연속 새 의견이 없는지 확인해 늦게 도착하는 검토에 대응합니다. 요청한 봇 검토가 아직 도착하지 않았다면 대기 한도 안에서 응답을 기다립니다.
/pr-review, /pr-address, GitHub CLI의 gh, jq 등을 사용합니다. 수정 사항의 커밋과 푸시, 댓글 응답을 수행합니다. 최대 10회 반복하며 요청한 봇 검토는 최대 60분 기다립니다. 실제 병합은 완료 조건에 포함되지 않습니다.
이 소개문은 공개된 SKILL.md를 바탕으로 AI(OpenAI Codex)가 작성했습니다. 정확한 사양은 아래 원문을 확인하세요.
AI가 SKILL.md의 내용에서 주의가 필요한 작업과 도입 전제를 확인했습니다. 점수는 실제로 실행한 결과(실행 검증)만으로 매깁니다.
점수에는 넣지 않았습니다. 2026년 10월 23일 기준으로 AI(OpenAI Codex)가 판정했습니다. 참고용이므로 설치 전에 원문도 확인해 주세요.
설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.
Goal. Drive a PR to merge-ready by alternating /pr-review and /pr-address until all of the following hold:
/pr-review produces zero new findings (no new inline comments, no new top-level reviews with a non-empty body).isResolved: true.conclusion: "success" or "skipped" / "neutral" — none "failure" or still pending./review comment was posted on the PR and the review bot has not replied yet, quiet polls do not count toward condition 6 — keep polling until the review lands or the wait budget expires. See Waiting on a requested bot review.Do not stop at a fixed number of rounds. If round N introduces new comments, round N+1 is required. Cap at _MAX_ROUNDS = 10 as a safety valve, but expect 2–5 in practice.
Before starting, write two todos so the user can see the loop progression:
Round {current}: /pr-review + /pr-address on PR #{N} — current iteration.Final polish polling: 2 consecutive clean polls, CI green, 0 unresolved — runs after the last non-empty review round.Update the current round counter at the start of each iteration; mark completed only when the round's address step finishes (all new threads addressed + resolved).
ARG_PR="${ARG:-}"
# Normalize URL → numeric ID if the skill arg is a pull-request URL.
if [[ "$ARG_PR" =~ ^https?://github\.com/[^/]+/[^/]+/pull/([0-9]+) ]]; then
ARG_PR="${BASH_REMATCH[1]}"
fi
PR="${ARG_PR:-$(gh pr list --head "$(git branch --show-current)" --repo Significant-Gravitas/AutoGPT --json number --jq '.[0].number')}"
if [ -z "$PR" ] || [ "$PR" = "null" ]; then
echo "No PR found for current branch. Provide a PR number or URL as the skill arg."
exit 1
fi
echo "Polishing PR #$PR"
round = 0
while round < _MAX_ROUNDS:
round += 1
baseline = snapshot_state(PR) # see "Snapshotting state" below
invoke_skill("pr-review", PR) # posts findings as inline comments / top-level review
findings = diff_state(PR, baseline)
if findings.total == 0:
break # no new findings → go to polish polling
invoke_skill("pr-address", PR) # resolves every unresolved thread + CI failure
# Post-loop: polish polling (see below).
polish_polling(PR)
invoke_skill("pr-review", PR) is the agent's own review and always runs every round. In addition to it, the repo's review bot can be summoned on demand by commenting /review on the PR (see the open-pr skill):
gh pr comment "${PR}" --body "/review"
This is an option, not a required step, and it is not a substitute for the /pr-review round — use it when you want a second opinion the outer loop cannot produce itself, or when the PR carries no bot findings at all yet. Bot findings land as inline threads and top-level reviews, exactly like the agent's own findings, so both feed the same invoke_skill("pr-address", PR) step — no separate handling is needed. If you do request one, the exit conditions must wait for it; see Waiting on a requested bot review.
Before each /pr-review, capture a baseline so the diff after the review reflects only what the review just added (not pre-existing threads):
# Inline threads — total count + latest databaseId per thread
gh api graphql -f query="
{
repository(owner: \"Significant-Gravitas\", name: \"AutoGPT\") {
pullRequest(number: ${PR}) {
reviewThreads(first: 100) {
totalCount
nodes {
id
isResolved
comments(last: 1) { nodes { databaseId } }
}
}
}
}
}" > /tmp/baseline_threads.json
# Top-level reviews — count + latest id per non-empty review
gh api "repos/Significant-Gravitas/AutoGPT/pulls/${PR}/reviews" --paginate \
--jq '[.[] | select((.body // "") != "") | {id, user: .user.login, state, submitted_at}]' \
> /tmp/baseline_reviews.json
# Issue comments — count + latest id per non-bot, non-author comment.
# Bots are filtered by User.type == "Bot" (GitHub sets this for app/bot
# accounts like coderabbitai, github-actions, sentry-io). The author is
# filtered by comparing login to the PR author — export it so jq can see it.
AUTHOR=$(gh api "repos/Significant-Gravitas/AutoGPT/pulls/${PR}" --jq '.user.login')
# Slash-command triggers are excluded here too: a `/review` posted by a
# maintainer is non-bot and non-author, so without this it enters the baseline
# and later registers as a "new finding", forcing an address round over a
# comment with nothing to address.
gh api "repos/Significant-Gravitas/AutoGPT/issues/${PR}/comments" --paginate --slurp \
| jq --arg author "$AUTHOR" \
'[.[][] | select(.user.type != "Bot" and .user.login != $author)
| select((.body // "") | test("^\\s*/[a-z-]+\\s*$") | not)
| {id, user: .user.login, created_at}]' \
> /tmp/baseline_issue_comments.json
After /pr-review runs, any of these counting as "new findings" means another address round is needed:
id not in the baseline.databaseId is higher than the baseline's (new reply on an old thread).id with a non-empty body.id from a non-bot, non-author user.If any of the four buckets is non-empty → not done; invoke /pr-address and loop.
Once /pr-review produces zero new findings, do not exit yet. Bots (coderabbitai, sentry, autogpt-pr-reviewer) commonly post late reviews after CI settles — 30–90 seconds after the final push. Poll at 60-second intervals:
NON_SUCCESS_TERMINAL = {"failure", "cancelled", "timed_out", "action_required", "startup_failure"}
clean_polls = 0
required_clean = 2
while clean_polls < required_clean:
# 1. CI gate — any terminal non-success conclusion (not just "failure")
# must trigger /pr-address. "success", "skipped", "neutral" are clean;
# anything else (including cancelled, timed_out, action_required) is a
# blocker that won't self-resolve.
ci = fetch_check_runs(PR)
if any ci.conclusion in NON_SUCCESS_TERMINAL:
invoke_skill("pr-address", PR) # address failures + any new comments
baseline = snapshot_state(PR) # reset — push during address invalidates old baseline
clean_polls = 0
continue
if any ci.conclusion is None (still in_progress):
sleep 60; continue # wait without counting this as clean
# 2. Comment / thread gate
threads = fetch_unresolved_threads(PR)
new_issue_comments = diff_against_baseline(issue_comments)
new_reviews = diff_against_baseline(reviews)
if threads or new_issue_comments or new_reviews:
invoke_skill("pr-address", PR)
baseline = snapshot_state(PR) # reset — the address loop just dealt with these,
# otherwise they stay "new" relative to the old baseline forever
clean_polls = 0
continue
# 3. Mergeability gate
mergeable = gh api repos/.../pulls/${PR} --jq '.mergeable'
if mergeable == false (CONFLICTING):
resolve_conflicts(PR) # see pr-address skill
clean_polls = 0
continue
if mergeable is null (UNKNOWN):
sleep 60; continue
# 4. Pending-review gate — a /review with no bot answer yet.
# Quiet is exactly what a bot still thinking looks like, so don't bank
# a clean poll on it. review_requested_but_unanswered() already returns
# false once REVIEW_WAIT_BUDGET is spent, so the loop cannot hang here.
if review_requested_but_unanswered(PR):
sleep 60; continue
clean_polls += 1
sleep 60
Only after clean_polls == 2 do you report ORCHESTRATOR:DONE.
gh pr checks text columns)The fetch_check_runs(PR) step above must use --json, not the default text output. Job names can contain spaces and parentheses (e.g. test (3.11), Analyze (python)), so gh pr checks $PR | awk '{print $2}' extracts (3.11) instead of the status — leading to a clean-poll firing while jobs are still pending.
# Reliable: use --json so columns are unambiguous.
ci_json=$(gh pr checks $PR --repo Significant-Gravitas/AutoGPT --json name,state,bucket)
pending=$(echo "$ci_json" | jq '[.[] | select(.bucket == "pending")] | length')
failed=$(echo "$ci_json" | jq '[.[] | select(.bucket == "fail" or .bucket == "cancel")] | length')
# Buckets are: pass | fail | pending | cancel | skipping
# (NOTE: gh pr checks does NOT expose `conclusion` as a JSON field —
# only `bucket`. Don't confuse with the GitHub REST API's check_runs
# endpoint, which DOES use conclusion.)
Map back to the pseudocode above: bucket == "pending" is ci.conclusion is None (still in_progress); bucket in {"fail", "cancel"} is ci.conclusion in NON_SUCCESS_TERMINAL; bucket in {"pass", "skipping"} is clean.
autogpt-pr-reviewer[bot] typically takes ~30 minutes to answer a /review — far longer than the 60-second poll interval, so without this gate two clean polls would report ORCHESTRATOR:DONE while the requested review is still in flight.
review_requested_but_unanswered(PR) is:
Recompute from the API on every poll — never track elapsed time in a loop
variable, which is how this gate becomes an infinite wait. --paginate
concatenates one array per page, so last picks the last item of the final
page rather than the newest overall; use --slurp and max_by. An answer is
either a top-level review or an inline review comment, whichever is later —
an inline-only reply is still an answer, and counting only top-level reviews
left the gate waiting after its threads had already been addressed.
# `--slurp` cannot be combined with `--jq`; pipe to standalone jq instead.
REQUESTED_AT=$(gh api "repos/Significant-Gravitas/AutoGPT/issues/${PR}/comments" --paginate --slurp \
| jq -r '[.[][] | select((.body // "") | test("^\\s*/review\\s*$"))] | max_by(.created_at) | .created_at // empty')
REVIEWED_AT=$(gh api "repos/Significant-Gravitas/AutoGPT/pulls/${PR}/reviews" --paginate --slurp \
| jq -r '[.[][] | select(.user.login == "autogpt-pr-reviewer[bot]")] | max_by(.submitted_at) | .submitted_at // empty')
COMMENTED_AT=$(gh api "repos/Significant-Gravitas/AutoGPT/pulls/${PR}/comments" --paginate --slurp \
| jq -r '[.[][] | select(.user.login == "autogpt-pr-reviewer[bot]")] | max_by(.created_at) | .created_at // empty')
ANSWERED_AT=$(printf '%s\n%s\n' "$REVIEWED_AT" "$COMMENTED_AT" | grep -v '^$' | sort | tail -1)
# Pending iff a request exists and no answer is strictly newer than it.
# Start every poll from `false`: a stale `true` from the previous poll would
# otherwise keep the gate waiting after the answer has already landed.
# `[ a \< b ]` is not portable (fails under zsh), so compare via sort.
pending=false
NEWEST=$(printf '%s\n%s\n' "$REQUESTED_AT" "$ANSWERED_AT" | grep -v '^$' | sort | tail -1)
if [ -n "$REQUESTED_AT" ] && { [ -z "$ANSWERED_AT" ] || [ "$NEWEST" = "$REQUESTED_AT" ]; }; then
pending=true
fi
REVIEW_WAIT_BUDGET = 60 minutes, measured as now - REQUESTED_AT:
# Only meaningful when a request exists: GNU date reads an empty string as
# "now", BSD date errors, and either way there is nothing to time out.
if [ -n "$REQUESTED_AT" ]; then
WAITED=$(( $(date +%s) - $(date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "$REQUESTED_AT" +%s 2>/dev/null || date -u -d "$REQUESTED_AT" +%s) ))
[ "$WAITED" -ge 3600 ] && pending=false # budget spent — stop waiting
fi
An observed reply on this repo took 46 minutes, so a 45-minute budget would have abandoned a review that was about to arrive. 60 gives headroom.
Past the budget, stop waiting, resume counting clean polls normally, and note in the final report that the requested bot review never arrived. The loop must never hang on a bot.
Never post a second /review while one is unanswered. Check
review_requested_but_unanswered(PR) before triggering, not just before
exiting — otherwise each round requests another review and the bot answers a
queue of duplicates.
A single green snapshot can be misleading — the final CI check often completes ~30s before a bot posts its delayed review. One quiet cycle does not prove the PR is stable; two consecutive cycles with no new threads, reviews, or issue comments arriving gives high confidence nothing else is incoming.
/pr-address polling inside a single round already re-checks its own comments, but /pr-polish sits a level above and must also catch:
dev.Delegate to existing skills with the Skill tool; do not re-implement the review or address logic inline. This keeps the polish loop focused on orchestration and lets the child skills evolve independently.
Skill(skill="pr-review", args=pr_url)
Skill(skill="pr-address", args=pr_url)
After each child invocation, re-query GitHub state directly — never trust a summary for the stop condition. The orchestrator's ORCHESTRATOR:DONE is verified against actual GraphQL / REST responses per the rules in pr-address's "Verify actual count before outputting ORCHESTRATOR:DONE" section.
/pr-polish is a single orchestration task — one invocation drives the PR all the way to merge-ready. When a child Skill() call returns control to you:
Skill() call or polling sleep.The child skill returning is a loop iteration boundary, not a conversation turn boundary. You are expected to keep going until one of the exit conditions in the opening section is met (2 consecutive clean polls, _MAX_ROUNDS hit, or an unrecoverable error).
If the user needs to approve a risky action mid-loop (e.g., a force-push or a destructive git operation), pause there — but not at the routine "round N finished, round N+1 needed" boundary. Those are silent transitions.
Spawning /pr-polish inside an Agent(subagent_type="general-purpose") background task does not work. Background agents don't inherit the parent's slash-command registry, so Skill(skill="pr-review") and Skill(skill="pr-address") calls aren't available — the agent has to manually replicate the child skills' logic, which is fragile and tends to stall on the first network or rate-limit hiccup. Symptom: the background task reports stalled: no progress for 600s mid-review.
Run /pr-polish inline in the foreground conversation. If the user asks for "/pr-polish + /pr-test in parallel", split them: foreground /pr-polish, and ONLY then can the test step go to a background agent (because /pr-test doesn't itself need to invoke skills).
Skill(pr-review) every round — even when bot reviews already existA common failure mode: CodeRabbit / autogpt-pr-reviewer / Sentry have already posted findings on the PR, and the orchestrator skips the Skill(pr-review) step on the assumption that "review has been done." That's wrong — the outer loop's purpose is to layer the agent's own review on top of the bot reviews, catching issues the bots miss (architecture, naming, cross-file invariants, hidden coupling). If the orchestrator only addresses bot findings without ever running its own review, the loop converges to "bot-clean" but not "agent-reviewed-clean," and the user reasonably asks "did /pr-polish even read the diff?"
Self-check before reporting ORCHESTRATOR:DONE: confirm at least one Skill(skill="pr-review") call appears in the current orchestration. If none, the loop is incomplete — go back and run one round.
This skill issues many GraphQL calls (one review-thread query per outer iteration plus per-poll queries inside polish polling). Expect the GraphQL budget to be tight on large PRs. When gh api rate_limit --jq .resources.graphql.remaining drops below ~200, back off:
/pulls/{N}/comments, /pulls/{N}/reviews, /issues/{N}/comments) per the pr-address skill's GraphQL-fallback section.sleep 5 between any batch of ≥20 writes to avoid secondary rate limits._MAX_ROUNDS = 10 — if review+address rounds exceed this, stop and escalate to the user with a summary of what's still unresolved. A PR that cannot converge in 10 rounds has systemic issues that need human judgment.poetry run format / pnpm format && pnpm lint && pnpm types per the target codebase's conventions. A failing format check is CI failure that will never self-resolve./pr-review round checks for duplicate concerns first (via pr-review's own "Fetch existing review comments" step) so the loop does not re-post the same finding that a prior round already resolved.When the skill finishes (either via two clean polls or hitting _MAX_ROUNDS), produce a compact summary:
PR #{N} polish complete ({rounds_completed} rounds):
- {X} inline threads opened and resolved
- {Y} CI failures fixed
- {Z} new commits pushed
Final state: CI green, {total} threads all resolved, mergeable.
If exiting via _MAX_ROUNDS, flag explicitly:
PR #{N} polish stopped at {_MAX_ROUNDS} rounds — NOT merge-ready:
- {N} threads still unresolved: {titles}
- CI status: {summary}
Needs human review.
Use when the user says any of:
Do not use when:
/pr-review)./pr-address).아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.
로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.
코드 변경의 테스트와 기존 기능에 미치는 영향을 확인하고 저장소 템플릿에 맞춰 PR을 생성하며, 리뷰 요청부터 지적 사항 대응까지 진행합니다.
별도 작업 폴더에서 여러 Claude Code를 실행하여 개발 작업 배정, 진행 상황 확인, 중단된 세션 복구 지원, PR 완료 확인을 함께 관리합니다.
Sentry 데이터로 변경 전후나 환경 간 속도와 실패율을 비교합니다. 통계적 변동을 고려해 빨라짐, 느려짐, 변화 감지 안 됨, 데이터 부족을 판정합니다.
PR 검토 의견을 모아 코드 수정, 커밋, 답변과 스레드 해결을 진행합니다. CI 실패, 충돌, 새 댓글도 계속 확인하며 대응이 끝날 때까지 추적합니다.
코드 변경 제안인 PR의 목적과 차이를 읽고 정확성, 보안, 설계, 테스트를 점검합니다. 기존 의견과 중복되지 않도록 심각도를 붙여 해당 코드 줄에 댓글을 게시합니다.
실행 중인 시스템에서 PR이나 브랜치 변경을 조작하고 화면, API, 저장 데이터의 변화를 대조하여 작업 전후 스크린샷과 함께 검증 결과를 보고합니다.