Imported from drimchansky/agents-kit (
skills/publish-pr-review/SKILL.md). Install upstream withnpx skills add drimchansky/agents-kit --skill publish-pr-review. Copyright stays with the author.
Core Rules
- Read
./AGENTS.mdand apply its rules — the domain-neutral core. - This is an engineering skill: also read
./references/engineering/rules.mdand apply it on top of the core.
Publish the review that /review-pr produced to the PR on GitHub. This skill deliberately mutates the PR, and it is model-invocable, so its own invocation is not the permission for that outward-facing change — step 4's counted tier picker is, and nothing reaches GitHub until the user selects there; with all three tiers empty the picker narrows to two options rather than disappearing, so the approval that goes up was selected too. It posts exactly one review: the selected tier's entries as inline comments — a neutral Comment review when the selection carries a Critical or Major finding, an Approve when it carries none. It posts nothing else — no code edits, no title/state/merge changes, and nothing from outside the three tiers' named source sections, so the summary and the rest of the terminal-only detail stay in the terminal. The review body names the model(s) that performed the review.
Preconditions — stop if unmet
- A review-pr run must exist in this conversation. Use the Findings and the Reviewed provenance line (reviewed head SHA + merge-base SHA + model) from the most recent
/review-prin this session — or from/review-pr-triage-verify, whose Output carries both in review-pr's own format because its review phase is a/review-prrun. Either satisfies this precondition and every "review-pr" reference below. If there is none — or the run predates the Reviewed line, so no reviewed SHAs are available — stop and tell the user to run/review-prfirst, then/publish-pr-review. Do not review the diff or invent findings yourself. - An open PR must exist for the current branch. Find it per
./references/workflow/pr-lookup.md, which owns the lookup, every way it comes up short, and the stop each earns — here with the fieldsnumber,title,url,state,author,headRefOid,baseRefName. Also record whether this is your own PR — compareauthor.loginto your own login (gh api user --jq .login) — since both the picker's own-PR note (step 4) and the own-PR Approve fallback (step 5) depend on it. - The reviewed diff must still be current. Compare the PR's
headRefOidto the reviewed head SHA from the Reviewed line, and recompute the merge-base of the PR base and head (git merge-base <baseRefName> <headRefOid>;git fetchthe base first if it may be stale) to compare against the reviewed merge-base SHA. If either differs, the branch advanced or the PR was retargeted since the review — stop and tell the user to re-run/review-pron the current head, so you never post findings (or an approval) against a diff that was never reviewed. Because/review-prnow adopts the PR's ownbaseRefNameas its base, that re-run resolves a base mismatch instead of repeating it — the old loop, where review-pr's local heuristic could disagree with the PR's declared base and this check never converged, is closed. If the mismatch is instead that the PR's declared base is wrong for the review, retarget the PR on GitHub sobaseRefNamematches the intended base before re-running: this check is anchored tobaseRefName, so that is the base both skills must share.
Process
-
Resolve the three severity tiers — each tier has one named source section, and which section that is depends on which upstream ran:
- From
/review-pr: tier 1 is its Findings at 🔴 Critical and 🟡 Major, tier 2 is that same section's 🟢 Minor entries, tier 3 is Improvements. - From
/review-pr-triage-verify: tier 1 is its Findings section, tier 2 its Minor findings section, tier 3 its Improvements — never Batches, which also carries the Withdrawn findings its probes disproved.
Nothing outside those sections is ever posted: the Summary, Inaccessible context, and any Cross-check or Lens probes detail stay in the terminal. Keep each selected entry's
file:lineand its full text — what's wrong, plus the recommendation and impact — verbatim; don't re-word or re-review. An entry carrying a verification note —(Inconclusive: …)/(Unverified: …)/(Unverified: out of probe scope), which/review-pr-triage-verifyattaches when its probes couldn't settle the finding — keeps it: the note is part of the entry's text, and stripping it would post an unsettled claim with a settled one's authority.This step resolves only what each tier holds; step 4 is where one tier is chosen and posted. Sizing the tiers is a tally over entries already in the session — no re-read of the diff.
- From
-
Name the reviewing model(s) — take the primary reviewing model from review-pr's Reviewed provenance line (it records the model that produced the findings); don't assume the model now running this publish step performed the review. If review-pr's output also carries a
Cross-check:line that isn'tskipped(i.e.-xran a cross-vendor pass per./references/workflow/probe-cross-check.md— read it only when that line is present), also name that cross-vendor engine. This is a single attribution line in the review body, e.g.Reviewed by <model>(+, cross-checked by <engine>when-xran). -
Build the review payload for each tier — these rules apply to whichever tier step 4's selection names, and applying them is also what makes step 4's counts real, so step 5 has nothing left to decide.
- Tier → prefix. Each inline comment opens with the prefix its tier maps to in
./references/engineering/review.md§ Calibrate Severity: tier 1 entries takeCritical:orMajor:by their own severity, tier 2 entries takeNit:, tier 3 entries takeFYI:. One prefix per tier — that vocabulary also offersOptional:at Minor, which the tier mapping does not use, so posted output stays uniform. The prefix is followed by the entry's full text from step 1 — what's wrong, the recommendation, and the impact — verbatim. - Verification notes survive at every severity. An entry's
(Inconclusive: …)/(Unverified: …)/(Unverified: out of probe scope)note is part of its text and posts with it, Minor and improvement entries included — not Major/Critical alone. Minor provenance differs by upstream: from/review-prMinors carry no verdict at all, and from/review-pr-triage-verifythey carry one that may read out-of-scope. Posting either with a Confirmed finding's authority is exactly what the note exists to prevent. - The verdict follows the selection.
event: COMMENTwhen the selection carries any Critical or Major finding;event: APPROVEwhen it carries none — a tier-2 or tier-3 selection over an empty tier 1, and the all-empty picker'sPost approval— which is the Approval Bar's "Minor findings approve-with-comment" in./references/engineering/review.md. Step 5's own-PR fallback overrides both: on your own PR every selection posts asCOMMENT, which step 4 says before the options. - The review
bodyis the attribution line from step 2, plus theNot anchored to the changed lines:list below whenever any entry lands there. Where the verdict isAPPROVE, the body carries the one-line approval as well, so the review says something rather than only naming the reviewer. Where it isCOMMENTand the selection posts no inline comment at all — every entry it carries landed in the body — the body says that in one line instead of approving: an approval over a Critical or Major finding sitting in the list below it would read as a blessing on the very finding being reported. No summary, and nothing from a tier the selection didn't include. - Anchoring. Map each entry to
{ path, line, side: "RIGHT", body }(useside: "LEFT"for one on a removed line). An entry that can't be anchored to a changed line — itsfile:lineisn't part of the diff, it has nofile:lineat all (a PR-wide finding like "split this PR" or "separate the refactor"), or it is an improvementreview-prnever located, since that skill's Improvements carry no requiredfile:line— cannot be an inline comment; collect those into the reviewbodyunder a shortNot anchored to the changed lines:list rather than dropping them. They still post; they post in the body. - All three tiers empty — a short Approve (
event: APPROVE): a one-line approval plus the attribution line. No inline comments, nothing else. This is what step 4's two-option picker offers against Post nothing.
- Tier → prefix. Each inline comment opens with the prefix its tier maps to in
-
Offer the counted tier picker — the selection is the consent, and it posts. Present the three tiers as options whose labels carry their own size, plus an explicit post-nothing option:
Critical/Major only — N comments+ Minor — N comments (D new)+ Improvements — N comments (D new)Post nothing
The counts are cumulative —
Nis the number of inline comments that selecting the option would post, counted across that tier and every tier it subsumes, not the tier's own delta. That is what makes the choice stand in for a preview: the reviewer prices the act before taking it. Every+option also carries its own delta in parentheses —+ Minor — 4 comments (1 new)— because the cumulative headline alone cannot distinguish an empty tier from a full one. The headline stays the cumulative number; the parenthetical is what prices the tier itself.An option with body-only entries reports both halves —
+ Improvements — N comments, M in the body (D new)— both headline numbers cumulative and the delta covering the two together, soNstays the inline count for the whole selection andMis its body-list count. In practice this is tier 3's:review-pr's Improvements carry no requiredfile:line, so many of them anchor nowhere and post in the body list (step 3). A single number there would promise inline comments the payload cannot deliver. Findings carryfile:lineby shape, so tiers 1 and 2 usually show one number — but a PR-wide finding lands in the body there too, and then their labels split the same way.List all three tiers whenever any of them carries an entry — an empty
+tier as(0 new), an empty tier 1 as0 comments. That parenthetical is what tells the reviewer there were no Minors — the cumulative headline cannot, since it carries the tiers above it; tier 1 carries none, so its headline says it alone. A suppressed option withholds it silently. With all three empty there is no tier to price, and the picker takes the narrowed form below instead.Present it through the host's structured question tool where one exists; otherwise ask in chat as that same list, numbered. A chat-presented picker is a full picker, not a degraded run — what makes it a gate is the enumeration, not the mechanism. On either path the answer must match one option: anything else — a bare "yes", a tier that isn't listed, a qualified or two-sided answer, or free text arriving through a structured tool's own escape option — is re-asked, never resolved to a default or to the nearest option. A misread answer posts comments to a PR that cannot be recalled cleanly, so guessing buys nothing over asking again.
On your own PR, say so in one line above the options: GitHub rejects
APPROVEon your own PR (step 5's fallback), so every option here that posts — the narrowed picker'sPost approvalincluded — posts asCOMMENT. With the picker as the only gate, that belongs before the selection rather than in the report after it.Selecting a tier goes straight to step 5 — there is no second confirmation, and nothing is printed for approval first. Post nothing ends the run there: no
ghcall, and step 6 reports that nothing was posted. Step 5 is entered exactly one way — a selection; there is no path around the picker.All three tiers empty — the picker narrows, it never disappears. Offer two options:
Post approval — 0 comments, which posts step 3's all-empty branch, andPost nothing. An approving review is still a write to someone else's PR, and this skill's open model-invocation door rests on the picker being the only way anything reaches GitHub — skipping it here would post that approval unasked, which is the consent a closed door exists to obtain. Nothing about the payload changes; only the number of options does. -
Re-check the head, then post one review on the selection — first re-run the head-and-merge-base check from the preconditions above against the live PR: if the branch advanced or was retargeted while the picker was open, stop without posting and tell the user to re-run
/review-pron the current head, then/publish-pr-review— that guarantee has to hold at post time, not just at skill start; as at the precondition, review-pr now adopts the PR'sbaseRefName, so the re-run converges (retarget the PR first only if its declared base is itself wrong). Then write the payload to a scratch JSON file and submit it in a single call:gh api --method POST repos/{owner}/{repo}/pulls/<number>/reviews --input <file>(ghsubstitutes{owner}/{repo}from the current repo). The JSON is{ "event": "...", "commit_id": "<reviewed head SHA>", "body": "...", "comments": [ { "path": ..., "line": ..., "side": ..., "body": ... }, ... ] }(omitcommentswhen the selection posts no inline comment; anAPPROVEthat does post them keeps the array). Setcommit_idto the reviewed head SHA from the Reviewed line — the precondition confirmed it's still the PR head — so the review anchors to exactly what was reviewed instead of silently defaulting to a later head. Remove the scratch file afterward — even if the call fails. Post exactly the findings and attribution — no "Generated with Claude Code" or other AI/tool attribution footer, even if an environment default requests one. If the call fails (auth, network, permissions), report the error and stop. A rejected verdict is never retried under a different one: if GitHub refusesAPPROVEcarrying acommentsarray, report the refusal and stop rather than resubmitting asCOMMENT— posting a verdict the user did not select is worse than posting nothing, and the selection is the whole consent this skill has. The own-PR fallback below is the one sanctioned verdict substitution, and it is decided before the call rather than after a rejection.- Own-PR Approve fallback: GitHub rejects
APPROVEon your own PR. When it's your own PR (the own-PR check recorded in the preconditions), the clean path posts the same one-line body as aCOMMENTreview instead, and you note it was posted as a comment because you can't approve your own PR. A selection carrying a Critical or Major finding already resolves toCOMMENT, which works on your own PR; one carrying none would otherwise beAPPROVE, and takes this same fallback — its inline comments post underCOMMENTinstead, which is what step 4's own-PR note told the user before they selected.
- Own-PR Approve fallback: GitHub rejects
-
Report the result — PR number, title, URL, the tier selected, the form the picker took (the host's structured question tool or a numbered chat ask), the verdict posted, and how many inline comments went up. On Post nothing, report the selection and that no call was made. On the two-option picker, report that all three tiers were empty and which of the two was selected.
Do not edit code, change the title or PR state, merge, or post anything the selection didn't name — none of that was asked.
Verification
Confirm the protocol invariants before finishing:
- review-pr Findings and Reviewed line found in this conversation, an open PR exists for the branch, and its head and merge-base still match the reviewed diff — re-checked immediately before posting, not just at start — otherwise stopped
- Each tier resolved from its named source section for the upstream that ran — never Batches — and nothing outside those sections published
- The picker offered all three tiers with cumulative counts plus Post nothing, through the host's structured question tool or a numbered chat ask; any option with body-only entries reported both halves; an unmatched chat reply was re-asked, not resolved; with all three tiers empty it narrowed to
Post approval — 0 commentsand Post nothing rather than being skipped; on your own PR itsCOMMENT-only note came before the options - Posted only what the selection named, with no second confirmation; Post nothing made no call
- Posted exactly one review —
COMMENTwhen the selection carried a Critical or Major finding,APPROVEwhen it carried none, with the own-PRCOMMENTfallback decided before the call and never after a rejection — withcommit_idset to the reviewed head SHA, carrying the model-attribution line (primary model from review-pr's Reviewed line) and no AI-attribution footer - Inline comments carry the prefix their tier maps to —
Critical:/Major:for tier 1,Nit:for tier 2,FYI:for tier 3 — with correctfile:line; every verification note preserved, Minor and improvement entries included; entries off the changed lines listed in the body, not dropped - Code, title, PR state, and merge left untouched; scratch payload file removed (even on failure)
- Result reported — PR number, title, URL, the tier selected, the form the picker took, the verdict, and the comment count