agents/openai.yaml
interface: display_name: 'RHDH Pull Request Review' short_description: 'Review RHDH pull request code'
redhat-developer/rhdh-skills · GitHub
>-
프로젝트 폴더에서 아래 명령어를 실행하고, 설치할 에이전트를 선택하세요.
npx skills add redhat-developer/rhdh-skills --skill rhdh-pr-review설치 명령을 직접 실행해야 적용됩니다. 지원 에이전트와 필요한 권한·라이선스는 제작자의 안내를 확인하세요.
agents/openai.yamlinterface: display_name: 'RHDH Pull Request Review' short_description: 'Review RHDH pull request code'
references/review-perspectives.md# Code Review Perspectives Adversarial is always dispatched from `workflows/review-code.md` on a `/code-review` run. This file holds that prompt and the optional extra lenses. Spec coverage lives in `/code-review`; do not run a third Requirements pass. Specialist domain knowledge lives in whatever skill the user already named. Do not invent a default specialist list. ## Common perspectives | Perspective | Focus | Prompt guidance | |-------------|-------|-----------------| | **Adversarial** | Abuse of the change: hostile input, confused deputy, path or auth bypass, a new script, hook, or parser | "Break the new surface. Assume hostile input." | | **Correctness** | Logic bugs, edge cases, error handling, off-by-ones, null/undefined paths | "Find bugs that would reach production. Ignore style." | | **Security** | Injection vectors, auth/authz gaps, secrets exposure, input validation | "Flag vulnerabilities with severity ratings." | | **Architecture** | Module boundaries, coupling, abstraction levels, extensibility | "Evaluate structural impact. Is this change in the right place?" | | **Performance** | Hot paths, query patterns, algorithmic complexity, caching | "Flag measurable performance risks." | | **Compatibility** | Public API surface, breaking changes, deprecations | "Determine if changed symbols are public-facing before flagging." | ## Signals that suggest an extra perspective Use these as hints for lenses **other than Adversarial**. A PR may need a perspective not listed here, or may not need one that signal-matches. | Signal | Suggests | Example | |--------|----------|---------| | Changes span 2+ modules/packages | Architecture | `src/api/` + `src/worker/` | | New files created | Architecture | New module, new component | | Changed paths match DB/query patterns | Performance | `**/model*`, `**/migration*`, `**/schema*` | | Keywords in title/body | Performance | `optimization`, `latency`, `cache`, `slow` | | Changed paths match API surface | Compatibility | `**/api/**`, `**/proto/**`, `**/openapi*` | | Package version changes | Compatibility | `package.json`, `pyproject.toml` version bumps | | Labels | Varies | `refactor` → Architecture, `breaking` → Compatibility | ## Choosing extras Adversarial is already running; do not add a second Adversarial pass. Add another row when you recommend it from these signals, or when the user named it.
scripts/fetch_pr_context.py#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.9"
# dependencies = []
# ///
"""Fetch PR context from GitHub and print it as one JSON object.
Runs gh CLI commands to collect PR metadata, diff, linked issues,
existing review comments, and CI status. Output is consumed by
the review-code.md workflow.
Examples:
uv run scripts/fetch_pr_context.py https://github.com/redhat-developer/rhdh-operator/pull/123
uv run scripts/fetch_pr_context.py 123
uv run scripts/fetch_pr_context.py 123 --repo redhat-developer/rhdh-operator
"""
import argparse
import json
import os
import re
import subprocess
import sys
# Progress on stderr, machine-readable failure on stdout: this script writes one
# JSON document to stdout and nothing else. Bundled here so the script runs
# installed alone.
def log(msg):
"""Write progress to stderr, keeping stdout clean for JSON output.
Silent when stderr is redirected or when NO_COLOR is set.
"""
if sys.stderr.isatty() and os.environ.get("NO_COLOR") is None:
print(msg, file=sys.stderr)
def error_exit(error_key, detail=None, extra=None):
"""Print a JSON error object to stdout and exit 1.
``error_key`` is the stable machine-readable reason. ``detail`` is a human
string; ``extra`` merges additional diagnostic keys.
"""
result = {"error": error_key}
if detail:
result["detail"] = detail
if extra:
result.update(extra)
json.dump(result, sys.stdout, indent=2)
print()
sys.exit(1)
def run_gh(args, check=True):
"""Run a gh CLI command and return stdout. Exits on failure if check=True."""
cmd = ["gh"] + args
try:
result = subprocess.run(
cmd, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=60
)
except FileNotFoundError:
error_exit("gh_not_found", "gh CLI is not installed or not on PATH")
except subprocess.TimeoutExpired:
error_exit("gh_timeout", f"Command timed out: {' '.join(cmd)}")
if check and result.returncode != 0:
stderr = result.stderr.strip()
error_exit("gh_error", f"{' '.join(cmd)}: {stderr}")
return result.stdout
def run_gh_json(args):
"""Run a gh CLI command and parse stdout as JSON."""
raw = run_gh(args)
try:
return json.loads(raw)
except json.JSONDecodeError:
error_exit("gh_json_parse", f"Failed to parse JSON from: {' '.join(['gh'] + args)}")
def parse_pr_input(pr_input):
"""Parse a PR URL or number into (repo, number). Returns (None, number) if no repo in input."""
# Full URL: https://github.com/owner/repo/pull/123
url_match = re.match(r"https?://github\.com/([^/]+/[^/]+)/pull/(\d+)", pr_input)
if url_match:
return url_match.group(1), int(url_match.group(2))
# Plain number
if pr_input.isdigit():
return None, int(pr_input)
# owner/repo#123
ref_match = re.match(r"([^/]+/[^#]+)#(\d+)", pr_input)
if ref_match:
return ref_match.group(1), int(ref_match.group(2))
error_exit("invalid_input", f"Cannot parse PR reference: {pr_input}")
def detect_repo():
"""Detect repo from current git remote."""
raw = run_gh(["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"])
repo = raw.strip()
if not repo:
error_exit("no_repo", "Could not detect repo. Pass --repo or use a full PR URL.")
return repo
def extract_issue_refs(body):
"""Extract GitHub issue references and Jira keys from PR body text."""
if not body:
return [], []
# GitHub: Fixes #123, Closes #456, Resolves #789, refs #101
gh_pattern = r"(?:fix(?:es|ed)?|clos(?:es|ed)?|resolv(?:es|ed)?|refs?)\s+#(\d+)"
gh_issues = [int(m) for m in re.findall(gh_pattern, body, re.IGNORECASE)]
# Also catch bare #N references not already captured.
# This is intentionally broad — may catch "step #1" or cross-repo refs.
# Cross-repo refs (org/repo#N) will fail gh issue view against the wrong
# repo, so we filter those out below.
bare_pattern = r"(?<!\w)(?<![/\w])#(\d+)"
bare_issues = [int(m) for m in re.findall(bare_pattern, body)]
gh_issues = sorted(set(gh_issues + bare_issues))
# Jira keys: RHIDP-1234, RHDHBUGS-567, etc.
jira_keys = re.findall(r"[A-Z][A-Z0-9]+-\d+", body)
jira_keys = sorted(set(jira_keys))
return gh_issues, jira_keys
def fetch_linked_issues(repo, issue_numbers):
"""Fetch GitHub issue details for each linked issue number."""
issues = []
for num in issue_numbers:
log(f" Fetching issue #{num}...")
data = run_gh_json(
["issue", "view", str(num), "--repo", repo, "--json", "number,title,body,labels,state"]
)
issues.append(
{
"number": data.get("number", num),
"title": data.get("title", ""),
"body": data.get("body", ""),
"labels": [label.get("name", "") for label in data.get("labels", [])],
"state": data.get("state", ""),
}
)
return issues
def fetch_review_comments(repo, pr_number):
"""Fetch existing inline review comments."""
raw = run_gh(
[
"api",
f"repos/{repo}/pulls/{pr_number}/comments",
"--paginate",
"-q",
".[] | {user: .user.login, path: .path, line: .line, body: .body, createdAt: .created_at}",
],
check=False,
)
if not raw.strip():
return []
comments = []
for line in raw.strip().split("\n"):
line = line.strip()
if not line:
continue
try:
comments.append(json.loads(line))
except json.JSONDecodeError:
continue
return comments
def fetch_reviews(repo, pr_number):
"""Fetch top-level review comments (review bodies, not inline)."""
raw = run_gh(
[
"api",
f"repos/{repo}/pulls/{pr_number}/reviews",
"--paginate",
"-q",
".[] | {user: .user.login, state: .state, body: .body}",
],
check=False,
)
if not raw.strip():
return []
reviews = []
for line in raw.strip().split("\n"):
line = line.strip()
if not line:
continue
try:
reviews.append(json.loads(line))
except json.JSONDecodeError:
continue
return reviews
def fetch_ci_status(repo, pr_number):
"""Fetch CI check status. Returns 'pass', 'fail', 'pending', or 'unknown'."""
raw = run_gh(
["pr", "checks", str(pr_number), "--repo", repo, "--json", "name,state,conclusion"],
check=False,
)
if not raw.strip():
return "unknown"
try:
checks = json.loads(raw)
except json.JSONDecodeError:
return "unknown"
if not checks:
return "unknown"
states = [c.get("conclusion", c.get("state", "")) for c in checks]
if any(s in ("FAILURE", "failure", "ERROR", "error") for s in states):
return "fail"
if any(s in ("PENDING", "pending", "IN_PROGRESS", "in_progress", "") for s in states):
return "pending"
return "pass"
def main():
parser = argparse.ArgumentParser(
description="Fetch GitHub PR context and output a structured JSON artifact."
)
parser.add_argument(
"pr",
help="PR number, URL (https://github.com/owner/repo/pull/123), or owner/repo#123",
)
parser.add_argument(
"--repo",
help="Repository (owner/repo). Auto-detected from git remote if omitted.",
)
parser.add_argument(
"--no-diff",
action="store_true",
help="Skip fetching the diff (useful for metadata-only queries).",
)
parser.add_argument(
"--no-comments",
action="store_true",
help="Skip fetching existing review comments.",
)
parser.add_argument(
"--no-issues",
action="store_true",
help="Skip fetching linked GitHub issues.",
)
args = parser.parse_args()
# Parse input
parsed_repo, pr_number = parse_pr_input(args.pr)
repo = args.repo or parsed_repo
if not repo:
log("No repo specified, detecting from git remote...")
repo = detect_repo()
log(f"Fetching PR #{pr_number} from {repo}...")
# Step 1: PR metadata
log(" Fetching metadata...")
pr_data = run_gh_json(
[
"pr",
"view",
str(pr_number),
"--repo",
repo,
"--json",
"number,title,body,state,author,labels,headRefName,baseRefName,"
"headRefOid,files,additions,deletions,url,commits",
]
)
# Step 2: Diff
diff = ""
if not args.no_diff:
log(" Fetching diff...")
diff = run_gh(["pr", "diff", str(pr_number), "--repo", repo])
# Step 3: Linked issues
gh_issue_nums, jira_keys = extract_issue_refs(pr_data.get("body", ""))
linked_issues = []
if not args.no_issues and gh_issue_nums:
log(f" Found {len(gh_issue_nums)} linked GitHub issue(s)...")
linked_issues = fetch_linked_issues(repo, gh_issue_nums)
# Step 4: Existing comments
existing_comments = []
existing_reviews = []
if not args.no_comments:
log(" Fetching existing review comments...")
existing_comments = fetch_review_comments(repo, pr_number)
existing_reviews = fetch_reviews(repo, pr_number)
# Step 5: CI status
log(" Checking CI status...")
ci_status = fetch_ci_status(repo, pr_number)
# Assemble the PR context document
files = []
for f in pr_data.get("files", []):
files.append(
{
"path": f.get("path", ""),
"additions": f.get("additions", 0),
"deletions": f.get("deletions", 0),
}
)
labels = [label.get("name", "") for label in pr_data.get("labels", [])]
head_sha = pr_data.get("headRefOid", "")
context = {
"repository": repo,
"changeRequest": {
"forge": "github",
"number": pr_number,
"headSha": head_sha,
"baseRef": pr_data.get("baseRefName", ""),
"headRef": pr_data.get("headRefName", ""),
"title": pr_data.get("title", ""),
"body": pr_data.get("body", ""),
"author": pr_data.get("author", {}).get("login", ""),
"state": pr_data.get("state", ""),
"url": pr_data.get("url", ""),
"labels": labels,
},
"files": files,
"totalAdditions": pr_data.get("additions", 0),
"totalDeletions": pr_data.get("deletions", 0),
"diff": diff,
"linkedIssues": linked_issues,
"jiraKeys": jira_keys,
"existingComments": existing_comments,
"existingReviews": existing_reviews,
"ciStatus": ci_status,
}
# Output
if sys.stdout.isatty():
json.dump(context, sys.stdout, indent=2)
else:
json.dump(context, sys.stdout)
print()
log(
f"Done. {len(files)} files, {len(linked_issues)} linked issues, "
f"{len(existing_comments)} comments, CI: {ci_status}"
)
if __name__ == "__main__":
main()
SKILL.md--- name: rhdh-pr-review description: >- Reviews Red Hat Developer Hub pull request code on GitHub in rhdh, rhdh-operator, rhdh-plugins, or community-plugins. Use for a GitHub PR URL or number, "review this PR", analysis-only review, or posting inline comments. For label and merge-readiness triage of the overlay PR backlog, use /rhdh-overlay. compatibility: "GitHub CLI and Python 3. Requires the external /code-review skill — analysis is blocked without it." --- # RHDH Pull Request Review Keep forge I/O at the edges: fetch produces the PR context, analysis works from that context and checked-out code alone, and posting sends only findings already verified against the head SHA. ## Route by outcome | Outcome | Workflow sequence | |---|---| | Code review and post | `workflows/fetch-github.md` → `workflows/review-code.md` → `workflows/post-to-github.md` | | Analysis only | `workflows/fetch-github.md` → `workflows/review-code.md`; stop after the edited draft | A bare PR URL or number defaults to code review and post. ## Review invariants - `/code-review` is required on every draft-review path, including analysis-only. If it is missing, stop, say that `code-review` is missing, name `/setup-rhdh-skills install`, and do not substitute a local two-axis review. - Every `/code-review` run also dispatches Adversarial. Team, worktree, and draft steps live in `workflows/review-code.md`. - Present the complete edited draft before stating any post operation. An explicit request to post is intent, not approval of the exact write. ## Write gate Fetch and analysis are read-only. Posting a GitHub review is an external write: invoke the named skill `mutation-gate` and follow the gate it owns rather than restating it here. Creating or removing a local git worktree is not that gate. A review operation's target pins the head SHA. An earlier confirmation of findings approves no write. Report each outcome with the review URL, the verification done, and any recovery still owed. ## What each stage carries forward Every stage passes its result to the next in conversation. The field names are defined once, where they are produced: | Stage | Result | Defined in | |---|---|---| | Fetch | PR context: repository, changeRequest, files, diff, linkedIssues, jiraKeys, existingComments, existingReviews, ciStatus, specSource | `workflows/fetch-github.md` | | Analysis | Review draft: changeRequest, summary, verdict, findings, edited, worktreePath | `workflows/review-code.md` | ## Completion Complete when the report names the head SHA reviewed, has presented the `/code-review` Standards and Spec reports, and presents the edited draft. On the post route, also give the outcome of every approved write with its target.
workflows/fetch-github.md# Workflow: Fetch GitHub PR Context
Fetch PR metadata, diff, linked issues, existing comments, and CI status from
GitHub. Produces the PR context that `review-code.md` analyzes.
## Script
Run the fetch script to collect all PR context in one call:
```bash
uv run scripts/fetch_pr_context.py <PR_URL_OR_NUMBER> [--repo owner/repo]
```
The path is relative to the skill directory.
The script accepts:
- A full URL: `https://github.com/owner/repo/pull/123`
- A number (detects repo from git remote): `123`
- A shorthand: `owner/repo#123`
Optional flags:
- `--repo owner/repo` — override repo detection
- `--no-diff` — skip diff (metadata-only queries)
- `--no-comments` — skip existing review comments
- `--no-issues` — skip fetching linked GitHub issues
Consume the full JSON output. Do not pipe through `head`, `tail`, or `grep`.
## PR context fields
The script prints one JSON object and nothing else. There is no envelope: these
fields are the whole document.
```
repository: "owner/repo"
changeRequest: {forge, number, headSha, baseRef, headRef, title, body, author, state, url, labels}
files: [{path, additions, deletions}, ...]
totalAdditions, totalDeletions
diff: "full unified diff text"
linkedIssues: [{number, title, body, labels, state}, ...]
jiraKeys: ["RHIDP-1234", ...]
existingComments: [{user, path, line, body, createdAt}, ...]
existingReviews: [{user, state, body}, ...]
ciStatus: "pass" | "fail" | "pending" | "unknown"
```
## Linked issues
`linkedIssues` carries the title, body, labels, and state of each GitHub issue
the PR body references — enough for most reviews. When a review needs the full
issue detail, including its comment thread and resolved workspace, invoke
`/rhdh-forge` by name with the issue reference. Do not add issue parsing to this
workflow.
When `linkedIssues` is empty, ask once whether there is a spec or issue to
judge against. If the author points at none, the PR body is the contract. Carry
that choice forward as `specSource` — the linked issue bodies, or the PR body —
into `/code-review` Spec. Spec still runs; the review does not wait on an issue.
## Jira keys
The script extracts Jira keys (for example, `RHIDP-1234`) from the PR body but
does not fetch them. When Jira detail affects the review, invoke `/rhdh-jira-api`
by name with the keys and use what it returns. Otherwise retain the keys and
continue. Do not select a Jira transport or inspect Jira credentials from this
workflow.
## CI status
`ciStatus` comes from `gh pr checks`. When it is `unknown`, invoke `/rhdh-forge`
to confirm against `gh run list --branch` before treating CI as missing or
failed. `/rhdh-forge` owns failed-log reads.
## After fetching
Proceed to `review-code.md`. Carry the complete context forward. Downstream
workflows read these fields by name, including `specSource`.
workflows/post-to-github.md# Workflow: Post Review to GitHub
Takes the review draft from `review-code.md` and posts it as an inline review via the GitHub API. This workflow is GitHub-specific.
## Prerequisites
- `gh` CLI authenticated with write access to the target repo
- A review draft carrying `changeRequest`, `summary`, `verdict`, `edited: true`, and `findings[]`
## Step 1: Finalize the draft
If the findings have not been shown yet, present the full edited draft first:
```
## Review for PR #<number>
**Event:** COMMENT / APPROVE / REQUEST_CHANGES
**Summary:** <top-level text>
### Inline comments (<count>)
1. `<path>:<line>` [<type>] — <body preview>
2. ...
```
Resolve requested edits and freeze the review event, head SHA, summary, and
inline bodies. Approval of the prose is not approval to post it.
## Step 2: Find exact line numbers
GitHub's review API needs line numbers in the file at HEAD, not diff-relative positions. For each finding, grep the file at HEAD for the target string:
```bash
gh api repos/<repo>/contents/<path>?ref=<head_sha> \
-H "Accept: application/vnd.github.raw+json" | grep -n "<target string>"
```
Comment on the line that contains the claim. A folded YAML or wrapped prose
sentence (`description: >-`) can start on one line and land the claim on the
next.
Set `start_line` when the suggestion replaces a block, not only when the comment
is multi-line prose. A suggestion must be the full replacement for the commented
range. If the fix spans a later line, extend the range or drop the fence and
leave guidance.
Update `line` (and `start_line` when the range is a block) to match the file at
the frozen SHA.
## Step 3: Build the payload
Write the review payload to a temp file — avoids shell escaping issues with suggestion blocks and markdown.
**Single-line comment:**
```json
{
"path": "src/file.ts",
"line": 42,
"side": "RIGHT",
"body": "Comment text\n\n```suggestion\nreplacement code\n```"
}
```
**Multi-line comment** (use `start_line` when the suggestion replaces a block):
```json
{
"path": "src/file.ts",
"start_line": 10,
"line": 12,
"start_side": "RIGHT",
"side": "RIGHT",
"body": "Multi-line suggestion\n\n```suggestion\nreplacement for lines 10-12\n```"
}
```
**Full payload:**
```json
{
"commit_id": "<head_sha>",
"body": "<summary text>",
"event": "COMMENT",
"comments": [ ... ]
}
```
Write to a temp file (use a platform-appropriate temp directory):
```bash
REVIEW_FILE=$(mktemp)
cat > "$REVIEW_FILE" << 'REVIEW_EOF'
<payload JSON>
REVIEW_EOF
```
Scan that payload file through `/mutation-gate` before showing the plan.
Describe leftover credential-shaped fields without quoting an example value.
## Step 4: State and post the review
Invoke `/mutation-gate` and follow it. State one operation: the target repo and
PR number, the exact `gh api` command below, the complete JSON payload as the
preview, the frozen head SHA as a precondition, and — on failure — deleting the
partial review or following up manually. State it only once the payload file is
final and has been scanned.
Run the command below only after the user approves that stated operation. A prior
request to review or post, or approval of the prose draft, does not open this
gate.
Immediately before posting, re-read the live head:
```bash
gh api repos/<repo>/pulls/<number> --jq .head.sha
```
If it differs from the frozen `commit_id`, stop. Do not post a stale review.
```bash
gh api repos/<repo>/pulls/<number>/reviews \
--input "$REVIEW_FILE"
```
## Step 5: Clean up
```bash
rm -f "$REVIEW_FILE"
```
Remove the worktree when this run created one (`worktreePath` from
`review-code.md`).
Report the outcome: review URL, number of comments posted, event type, API
status, and whether it verified against the current head SHA.
## Common mistakes
| Mistake | Fix |
|---------|-----|
| Using diff line numbers for the API | Grep the actual file at HEAD for correct line numbers |
| Commenting the fold opener of wrapped YAML or prose | Comment the line that contains the claim |
| Shell-escaping suggestion blocks in `gh api` | Write JSON to a temp file, use `--input` |
| Posting after prose approval but before gate approval | State the exact operation and wait for approval of it |
| Incomplete suggestion for the commented range | Extend `start_line`…`line` to cover the whole fix, or drop the fence |
| Posting after a push landed during prose approval | Re-read live `head.sha` immediately before `gh api`; stop on drift |
| Credential-shaped leftovers in a comment body | Describe the leftover field; scan the payload via `/mutation-gate` |
| Including `start_line` when the comment is one line with no block replacement | Omit `start_line` for a single-line comment |
workflows/review-code.md# Workflow: Review Code
Platform-agnostic code analysis. Reads the PR context from `fetch-github.md`
and produces the review draft a posting workflow sends.
Work from that context. The one exception is reading full file contents at HEAD
to verify findings (see Reading source at HEAD).
## Mindset
You are a senior team member reviewing a contribution. Your goal is to help the author ship confidently, not demonstrate expertise. Every comment should either prevent a real problem or teach something useful — if it does neither, don't leave it.
## Step 0: `/code-review` prerequisite
`/code-review` is required on every run, including analysis-only. If the named skill is absent, stop. Say that `code-review` is missing, name `/setup-rhdh-skills install`, and do not substitute a local two-axis review.
## Step 1: Team
`/code-review`'s Standards and Spec agents run on every draft-review path.
Dispatch **Adversarial** on every `/code-review` run. Load
`../references/review-perspectives.md` for that prompt and for any further lens.
Specialists named in the original request join that set. Add another perspective
from that reference when you recommend it from the diff, or when the user named
it. Do not re-ask.
Use `specSource` from fetch as the Spec contract for `/code-review`. Spec still runs.
## Step 2: Worktree, then `/code-review`
If `git rev-parse HEAD` is not `changeRequest.headSha`, create a git worktree at
that SHA. For an RHDH repository, `/rhdh-context` locates the checkout to branch
from. Pass the worktree path into `/code-review` and any other subagents. Remove
the worktree after the GitHub post, or after the analysis-only draft.
Invoke `/code-review` with the PR base as the fixed point and `specSource` as the spec. Present the Standards and Spec reports as their own reports. Do not paste them as the GitHub review. Draft later from verified findings.
When dispatching Adversarial or extra reviewers, each receives:
- The worktree path when one exists
- The diff from `diff`
- `files[]`
- `specSource`
- Their focus area
They verify against HEAD. They do not write GitHub review prose.
### Reading source at HEAD
When the diff alone is insufficient to judge a finding, read the full file at HEAD. Prefer the worktree when one exists. Otherwise use `repository` and `changeRequest.headSha` from the fetched context:
- **GitHub**: `gh api repos/{repo}/contents/{path}?ref={head_sha} -H "Accept: application/vnd.github.raw+json"`
Prefer the diff when it is enough.
## Step 3: Verify every finding (critical)
Reviewers will produce false positives. Verify each finding against actual code at HEAD.
**Drop any finding that:**
- References code that doesn't exist at HEAD
- References files that are not in the PR's changed files list (check `files[]` — don't assume a file exists in the PR just because it exists on the branch)
- Was already raised and resolved in `existingComments` or `existingReviews`
- Misreads what the code actually does
- Matches existing codebase conventions (the PR follows the project's style, not the reviewer's preference)
**For each finding `/code-review` Spec reported**, verify it against code at
HEAD. Note anything from the issue's scope that is missing; the author may be
intentionally splitting work — note, don't block.
Present a **finding inventory** to the user before drafting: `file:line`, category (`question` / `observation` / `fix`), and a one-line label only. This is a triage list for what to include — **not** review prose and **not** the GitHub draft. Do not write full comment bodies here. Skip the inventory only when the user already said to proceed to a draft.
## Step 4: Draft the review
The posted review should read like a person wrote it, not a report generator. Step 3 only decides which findings to keep; this step writes the actual comments.
Prefer **inline comments** for findings. Put substance on the line; do not duplicate inline content in the top-level comment.
### Top-level comment
Reserved for **important issues to resolve before merge** — not a summary or roll-up of the inlines. Do not restate what is already inline. A brief thanks is fine when needed. No performative praise.
If `existingReviews` shows you've already left a top-level comment on this PR, a new one is often unnecessary — consider posting only the inline findings. A follow-up top-level is still warranted if there are new merge-blocking issues or the prior review was on a different revision.
**If nothing significant survives verification:** draft a short approving top-level (thanks is enough). Don't manufacture issues.
### Inline comments
One inline per merge-shaped problem or lasting rule. Group nits into one comment or a single top-level "also" paragraph. A finding that neither prevents a wrong write nor teaches something that will still be true next month does not get its own inline.
Write each comment as natural prose — a short paragraph explaining the issue and why it matters. Avoid bullet lists, bold headers, and over-structured formatting.
**Guide, don't dictate.** Assume deliberate choices. When the design intent is unclear, ask why before proposing alternatives. Explain reasoning only when the fix isn't obvious. Finding `type: "fix"` means "propose a direction," not "paste a patch" — still guide unless a GitHub `suggestion` block applies below.
A `suggestion` fence is the full replacement for the commented range, or there is no fence.
### Edit before show-user
After drafting top-level + inlines, invoke `/prose-editing` on the whole draft — top-level comment and every inline body — in the **flavored** register. A review is a document, not a procedure, and the caller names the register so the editor does not have to guess it.
Preserve technical meaning, severity, file paths, line numbers, `suggestion` fences, and the review event. Present only what comes back. Never show the unedited prose as the review draft. Applies to posting and analysis-only routes.
## Step 5: Choose event type
Present the **edited** draft to the user. For posting routes, ask which event type to use:
| Event | When |
|-------|------|
| `COMMENT` | Default. Feedback without a verdict. |
| `APPROVE` | No issues, or only minor nits. |
| `REQUEST_CHANGES` | Critical issues that must be fixed. Use sparingly. |
For analysis-only (route 2), present the edited draft and stop — no event type, no post. Remove the worktree if this run created one.
## What this workflow hands on
Carry the finished review forward as:
```
changeRequest: {repository: "owner/repo", number: 123, headSha: "abc123..."}
summary: "top-level review text"
verdict: "COMMENT" | "APPROVE" | "REQUEST_CHANGES"
edited: true
worktreePath: null (or the path this run created)
findings[]
├── path: "src/file.ts"
├── line: 42
├── startLine: null (or number for a block the suggestion replaces)
├── type: "question" | "observation" | "fix"
└── body: "comment text, optionally with ```suggestion block when allowed"
```
`type` is the finding kind for triage. A GitHub `suggestion` fence inside `body`
is separate (see Step 4).
**Do not post the review.** If the router selected a posting workflow, hand that draft to it. If analysis-only, stop after presenting the edited draft (Step 5).