Install
openclaw skills install @arunjeetsingh/address-review-commentsFix the latest reviewer batch, self-audit, commit, and reply
openclaw skills install @arunjeetsingh/address-review-commentsTake the PR number from the user's request (or $ARGUMENTS on hosts that substitute it).
If it is not explicit, ask which PR before doing anything.
Invoking this skill authorizes it to, without pausing for a per-step confirmation: edit
files on the PR branch, make one commit per review round, push that commit, and post one
GitHub PR reply comment per round through your gh credentials. It decides which round
to act on by matching PR comment CONTENT against a marker pattern (see Step 2), not by
authenticating who posted it beyond GitHub's own login attribution — anyone who can
comment on the PR (including, on some repos, the PR author replying to their own
automated review) can shape which comment this skill treats as "the latest reviewer
batch." Run it only on repositories where you trust everyone able to comment on a PR not
to forge a batch marker, and review the round's diff and reply before treating it as
final — this skill never merges or deploys, and a human is expected to do both. When it
ends a round by arming a wait for the next batch, that watch keeps running GitHub reads
(and, on a real batch, another edit/commit/push/comment round) after this invocation
ends, per whichever host mechanism ../WAITING.md selects for your runtime; see that
file's host-mapping table for how to list, pause, or permanently stop it.
Every command block below opens with this preamble, and that is deliberate. Each
shell call may run in a fresh process — variables do not reliably survive from one block
to the next on every host — so a block that assumes $PR from an earlier one can run with
it empty. That is not a loud failure: gh pr view "" silently answers about the current
branch's PR, and a redirect built from an empty $SCRATCH targets /, which is
read-only. So every block re-derives $PR and re-validates it — a guard that runs
only in the illustrative preamble guards nothing, because the preamble is not what runs.
Re-derive, never carry over:
set -euo pipefail
PR="$ARGUMENTS" # substituted by the skill host, or set from the request
case "$PR" in ''|*[!0-9]*) echo "PR must be numeric, got '$PR'" >&2; exit 1 ;; esac
REPO=$(git rev-parse --show-toplevel) # the checkout that invoked this skill, not a path
SCRATCH="${TMPDIR:-/tmp}/pr${PR}-round"; mkdir -p "$SCRATCH"
$SCRATCH is derived from $PR rather than mktemp -d precisely so that re-deriving it
in a later block lands on the same directory. Worktrees for this repo's branches live
alongside it as $REPO/../wt-*.
A long adversarial review can diverge: each fix adds machinery, the machinery has its own failure modes, and the document grows faster than findings close. Apply these rules to any PR whose review has passed ~3 rounds:
C1…Cn) indexed at the top of its section. Every other
section — especially delivery-plan rows — REFERENCES the label and
never restates the content. A restatement is itself a defect.When replying, state explicitly which category each finding falls into. If a finding proposes new machinery on frozen architecture, say so and propose the tightening instead.
gh pr merge.git add/commit in one shell
command. A failed edit must not let a commit of partial state through.set -euo pipefail
PR="$ARGUMENTS"
case "$PR" in ''|*[!0-9]*) echo "PR must be numeric, got '$PR'" >&2; exit 1 ;; esac
REPO=$(git rev-parse --show-toplevel)
# gh already derives the repo from the working directory; there is no flag for it.
gh pr view "$PR" --json headRefName,title,state --jq '"head=\(.headRefName) state=\(.state)"'
git -C "$REPO" worktree list
Work in the existing worktree that has the PR's head branch checked out. If none exists,
create it ANCHORED TO $REPO — git worktree add "$REPO/../wt-<short-name>" <headRefName>. A bare ../wt-<name> is relative to the working directory, so from a
subdirectory it lands inside the repo. Confirm the
worktree is clean before starting; if it has uncommitted changes, stop and
report — do not stack a new round on top of unexplained state.
Dump the full comment history — and all of it means all three streams, every page. You need it for the regression lens, and Step 2's whole decision is "is there a reviewer batch newer than my last reply", which a short dump answers wrongly and silently.
gh pr view --json comments cannot do this, and its failure mode is the bad one. Verified
against the installed binary: it issues comments(first: 100) and returns them
oldest-first, so on a PR with 101 top-level comments the 100 you get are the 100 you
already know about and the newest reviewer batch is the one missing. Step 2 then reports
"waiting on reviewer" while a batch sits unaddressed. It also returns issue comments only,
never reviews or review replies, so the "all feedback" claim was never true of it.
set -euo pipefail
PR="$ARGUMENTS"
case "$PR" in ''|*[!0-9]*) echo "PR must be numeric, got '$PR'" >&2; exit 1 ;; esac
REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner)
SCRATCH="${TMPDIR:-/tmp}/pr${PR}-round"; mkdir -p "$SCRATCH"
# Brace the variable: `pr$PR_all_comments.txt` expands to `pr.txt` — one undefined name,
# silently the wrong file.
OUT="$SCRATCH/pr${PR}_all_comments.txt"
# Write to a temp and move only on success. A failed page must not leave a SHORT history
# file behind, because a short file is exactly what this block exists to prevent and
# nothing downstream can tell one from a quiet PR.
{
for ep in "issues/$PR/comments:comment" "pulls/$PR/reviews:review" "pulls/$PR/comments:reply"; do
gh api --paginate --slurp "repos/$REPO/${ep%%:*}" \
| jq --arg k "${ep##*:}" '[ .[][] | {kind:$k, who:.user.login, at:.created_at, body:(.body // "")} ]'
done
} | jq -s -r '[.[][]] | sort_by(.at) | .[] |
"=== \(.kind) by \(.who) @ \(.at) ===\n\(if .body == "" then "(no body — inline comments only)" else .body end)\n"' \
> "$OUT.part"
mv "$OUT.part" "$OUT"
echo "dumped $(grep -c '^=== ' "$OUT") item(s) to $OUT"
--slurp yields an array of pages per endpoint, so .[][] flattens one endpoint and the
outer jq -s merges the three before sorting. --slurp cannot be combined with --jq,
which is why the filter is a separate jq.
The regression check, because "it looked fine on a short PR" is how this shipped broken. Run it after any change to the block above; it stubs a second page and asserts the newest batch survives:
set -euo pipefail
STUB=$(mktemp -d)
cat > "$STUB/gh" <<'GH'
#!/bin/sh
case "$*" in
*"repo view"*) echo "o/r" ;;
*"issues/236/comments"*)
{ printf '[['
i=1; while [ $i -le 100 ]; do
[ $i -gt 1 ] && printf ','
printf '{"user":{"login":"a"},"created_at":"2026-01-01T00:00:%02dZ","body":"old %d"}' $((i%60)) $i
i=$((i+1))
done
printf '],[{"user":{"login":"a"},"created_at":"2026-09-10T05:11:44Z","body":"batch_id=NEWEST"}]]' ; } ;;
*"pulls/236/reviews"*) echo '[[]]' ;;
*"pulls/236/comments"*) echo '[[]]' ;;
*) echo "stub: unexpected: $*" >&2; exit 1 ;;
esac
GH
chmod +x "$STUB/gh"
PATH="$STUB:$PATH"; export PATH
PR=236; REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner)
{
for ep in "issues/$PR/comments:comment" "pulls/$PR/reviews:review" "pulls/$PR/comments:reply"; do
gh api --paginate --slurp "repos/$REPO/${ep%%:*}" \
| jq --arg k "${ep##*:}" '[ .[][] | {kind:$k, who:.user.login, at:.created_at, body:(.body // "")} ]'
done
} | jq -s -r '[.[][]] | sort_by(.at) | .[] | "=== \(.kind) by \(.who) @ \(.at) ===\n\(.body)\n"' \
> "$STUB/dump.txt"
n=$(grep -c '^=== ' "$STUB/dump.txt")
[ "$n" -eq 101 ] || { echo "expected 101 items, got $n" >&2; exit 1; }
grep -q 'batch_id=NEWEST' "$STUB/dump.txt" \
|| { echo "the page-2 batch was dropped — the dump is not paginated" >&2; exit 1; }
echo "history pagination OK: 101 items, newest batch present and last"
rm -rf "$STUB"
It discriminates: on the same corpus, the first page alone yields 100 items with the newest batch absent, and the paginated read yields 101 with it present and sorted last.
Identify the latest REVIEWER comment and the latest comment authored by us (the reply posted last round). Then branch:
Do not distinguish them by author. The automated reviewer may post under the SAME
GitHub account as our replies, so .user.login cannot tell them apart. Distinguish by
content: reviewer batches carry a <!-- review-pr-state … cycle=N … --> metadata block;
our replies carry codex-addressed:<batch_id>.
If this round ends by waiting for the next batch, wait the way ../WAITING.md
prescribes for your host — a background watch that costs little to nothing while the PR is
quiet, rather than a scheduler that re-runs this skill on a timer. That file holds the
loop, the host mapping, the filter-on-content rule and the filter-verification step; do
not restate them. Arm it with MARKER='<!-- review-pr-state' — the HTML comment the
reviewer writes, not the bare word, which appears in any reply discussing it, your own
included — and no EXCLUDE, since reviewer batches are what this direction waits for.
Bind PR and REPO into the command/prompt, since a watch may inherit nothing from this
session, and keep whatever id your host returns for stopping later.
../WAITING.md and stop; do NOT schedule a
plain timed wakeup on top of it. A timed wakeup with no predicate re-runs this
whole skill to discover nothing, which is the cost that file exists to remove.Judge the all-clear by substance, not keywords: a comment that closes old items but opens ANY new finding is state 3. A comment that only asks a question for the repo owner (not a doc change) should be surfaced, not "addressed" by editing.
Enumerate every finding in the latest reviewer comment. For each one, locate
the exact current text (grep -n first — never edit from memory of the
file), then apply the edit.
Hard-won rules that exist because each was violated once and burned a round:
\ must
be last char), no inline # comments on continued command lines,
${VAR:+...} is NOT an assignment in command position, arrays for
conditional env/args. Paste the block into zsh -n (or your target
shell's syntax-check mode) if there is any doubt.Before committing anything, run a four-lens audit of the FULL revised document/diff: one auditor per lens, each finding then adversarially verified by a separate agent that defaults to "not real" when uncertain.
Lens 1 — internal consistency: contradictions introduced by this round's edits; every grammar/contract (naming schemes, numeric budgets, actor assignments, config keys) checked against every usage site; shell blocks re-linted.
Lens 2 — regression vs reviewer history: read the FULL comment dump from Step 2. For each previously-resolved finding, verify this revision did not reintroduce it or weaken its resolution.
Lens 3 — staleness + incompleteness: leftover references to concepts this round changed; promised tests missing from workstream rows; any new mechanism named without who-runs-it / when / what-failure-it-produces.
Lens 4 — platform fact-check (give this lens web access): every claim the revision makes about a third-party platform — API endpoint, state/enum name, allowed state transition, CLI subcommand, UI control label, documented precondition — verified against the vendor's own current docs, and the verifier told to research independently rather than trust the auditor's citation.
This lens exists because it catches what the other three structurally cannot. Prose review confirms a document is self-consistent; it cannot tell you the platform doesn't work that way. Invented-platform-behaviour blockers (a nonexistent CLI verb, a state transition that's actually unreachable from the claimed precondition, a UI control mislabelled) have cost real rounds on real PRs, sometimes ones that would have deadlocked a lane in production while reading perfectly coherently.
Rules that follow from that: a wrong platform fact is a BLOCKER, not P2 — it cannot work as written. Never assert store/tool behaviour from memory; if you cannot verify it, say so in the document and route it to P2 rather than stating it. When a fact-check forces you to reduce claimed automation (an action that exists only in a vendor's web UI, not its API), reduce it — a design that promises a step which cannot run is worse than one that names the manual step honestly. And re-check your own corrections: over-correcting an already-wrong fact is a distinct failure.
Fix every CONFIRMED finding (same per-edit rules as Step 3), then run residual greps for the vocabulary the round changed (old names, old numeric values, old actor assignments) to catch stragglers the agents missed.
Re-audit until the findings stop being consequential, not until zero. If a pass produced blocker-level fixes, audit the result again — the fix text is the newest and least-reviewed text in the document, and in practice it carries its own defects. Long-running review docs have needed six or more passes in a single round, where each later pass found a real blocker created or left behind by the previous pass's fix (a stale third copy of a rule, a deadlock introduced by a new constraint, an invented platform fact, a clause that re-attached to the wrong branch when a sentence was split).
Two rules keep that from becoming an infinite loop:
Watch for these two self-inflicted patterns specifically, because both recur: inserting a clause mid-sentence and orphaning the trailing clauses onto the wrong subject (re-read every edited sentence start-to-finish), and fixing a rule in one place while an older copy survives elsewhere (grep the whole document for the rule's distinctive vocabulary, not just the section you edited).
Single commit on the PR branch. Message: one line summarizing the round
(e.g. E2E design: address review round N (6 findings)), then push.
Then post ONE reply comment — write the body to a file first, because these are long and shell-quoting them corrupts them:
set -euo pipefail
PR="$ARGUMENTS"
case "$PR" in ''|*[!0-9]*) echo "PR must be numeric, got '$PR'" >&2; exit 1 ;; esac
SCRATCH="${TMPDIR:-/tmp}/pr${PR}-round"
gh pr comment "$PR" --body-file "$SCRATCH/reply.md"
The reply contains:
State which round this was, the findings addressed, what the self-audit caught, the commit SHA, and that the reply is posted. If anything needs a decision (open questions, scope calls), list it explicitly.