pr-post-comments
Turn review findings or review replies into tagged, human-sounding PR comments and post them paced, one at a time, through a bundled driver. Applies the [r|o|f|c] tag scheme, dedupes against bot revie
SKILL.mdskills/pr-post-comments/SKILL.md
GitHubPost PR Comments
Findings already exist. This skill is only about getting them onto the PR: correctly tagged, in a human voice, deduped, and paced.
The driver lives next to this file:
POST=~/.claude/skills/pr-post-comments/scripts/post_pr_comments.py # adjust if installed elsewhere
Hard rules
- Every inline comment carries a tag.
[r][o][f][c], first characters of the body. The tag is how the author knows whether they must act. Thread replies take no tag. - Draft first. Post only on an explicit go. Show the full set as a table plus bodies, wait for the user to say go. "Create the comments" is not "post the comments" - ask.
- Never post a comment the bots already made. Fetch existing comments first.
- Never approve, request-changes, or merge. This skill posts comments.
- Only the driver posts. No
gh api ... -X POST, nogh pr comment, no loop, no bulk review. Anything else skips the pacing and the lint.
Step 1 - Read what is already on the PR
Bot reviewers get there first. Re-raising their findings makes the review look automated and wastes the author's time.
gh api "repos/<owner>/<repo>/pulls/<PR>/comments?per_page=100" \
--jq '.[] | "\(.id)\t\(.user.login)\t\(.path):\(.line)\t\(.body[0:120])"'
Drop any finding a bot (Copilot, code-quality, CodeQL) already made at the same location. Keep a finding that corrects a bot, but write it as a reply on their thread, not a fresh comment. Include your own comments from earlier rounds - do not re-raise a resolved thread.
Step 2 - Tag every finding
| Tag | Means | Test |
|---|---|---|
[r] |
must change before merge | would you block on it? |
[o] |
author may take or skip | you'd accept either answer |
[f] |
follow-up PR or ticket | real, but not this PR's job |
[c] |
question, no action | you want to understand, not change |
Post every finding in one round; the tags already say what blocks. Split rounds
only when the PR misses its own acceptance criteria (post the [r] set and stop)
or a [r] redesign would make the line-level comments pointless.
Step 3 - Voice
If the user has a personal voice skill installed (for example voice-of-er),
load it and run every body through it. Either way, these rules apply, and the
driver's --check enforces the mechanical ones:
- Lead with the finding. No "I noticed that", no "It appears that".
- Contractions. Fragments where a fragment is how you'd say it.
- Uneven paragraphs. Never three sentences of the same length in a row.
- Name the file, the function, the number. "
execute_toolbuilds deps with db, storage, mission_id... no sandbox" beats "the dependency dict is incomplete". - Refer to the code, never the author. Never "you wrote", "your function".
- No process narration. Nobody needs "I checked the base branch and found".
- Length follows the finding. A one-line gap gets one line.
Enforced by --check (code spans exempt): no em or en dashes; no sentence opening
with So / Okay / Well / Yeah; none of furthermore, moreover, additionally,
consequently, utilize, facilitate, robust, seamless, synergy, deep dive, circle
back, touch base, nuanced, that said, it's important to note, in terms of,
"I checked / verified".
Step 4 - Derive line numbers
Anchors are new-file line numbers, right side of the diff. Verify against the real file rather than counting diff output by hand:
git show <head-sha>:<path> | rg -n "<the symbol you are anchoring to>"
A comment on the wrong line is worse than no comment - it sends the author hunting.
Step 5 - Build the comments file
A JSON list in the scratchpad, one object per comment, in posting order. Build it
with python or jq -n --arg so quoting, backticks and newlines survive:
[{"path": "app/a.py", "line": 28, "body": "[r] ..."},
{"path": "app/a.py", "start_line": 40, "line": 44, "body": "[o] ..."},
{"in_reply_to": 3992873113, "body": "..."},
{"body": "PR-level note"}]
in_reply_to is the databaseId of the thread's first comment. Inline entries
need the head SHA as --commit. Dry-run first:
SHA=$(gh api repos/<owner>/<repo>/pulls/<PR> --jq '.head.sha')
python3 "$POST" <owner>/<repo> <PR> comments.json --commit "$SHA" --check
One failing body stops the whole batch, so fix it and re-check.
Step 6 - Post paced
The same command without --check, as a background Bash task
(run_in_background: true).
A flat interval is its own tell. Nobody spends the same minute on a two-line nit
and on a four-paragraph architecture finding, so the driver waits before each
comment as long as writing it would take: 15-35 s of reading plus a second per
six characters, swung +/-25%, capped near 260 s. A 120-char [o] waits 30-60 s,
a 400-char finding 65-120 s. Nine mixed comments come out around 12-16 minutes.
The clock counts from the last post of any run (~/.claude/state, override with
PR_COMMENTS_STATE_DIR), and a lock there keeps two runs from interleaving.
Step 7 - Verify and report
Read the log, confirm one html_url per entry, report what landed and what did
not. If the run was killed midway, say exactly which numbers posted.
Stop a run: pkill -f post_pr_comments.py, confirm with pgrep -f post_pr_comments.py.
Fix a body after posting:
gh api repos/<owner>/<repo>/pulls/comments/<comment_id> -X PATCH -f body='...'
A 404 there means the comment is gone, or gh is logged in as an account without
access. Check gh api user --jq .login before assuming anything else.
Boundaries
- Comments only. No approve, no request-changes, no merge, no push.
- Do not invent findings to round out a set.
- A
[c]about the PR as a whole belongs in a PR-level comment, not inline. - Max 3 code snippets across a round. Past that, describe the change in prose.