Interactive PR Feedback Walkthrough
Cheaper and more controllable than a batch pass: the feedback is inventoried once, then handled one point at a time, with the user in the loop at every branch. Only the code a given point touches is read, and only when that point comes up.
This skill is allowed to edit, commit, push, post replies and resolve threads — but never without the user's explicit validation for that specific point. Output language: French (the user's working language).
A PR is rarely walked in one sitting — the same PR typically comes back over several sessions and several days. Everything that survives a session lives in the state file (Step 2d), never only in context.
Sibling skill: /analyze-pr-feedback is the read-only overview. Use this one when
the user actually wants the feedback handled.
Step 1 — Identify the PR
If a PR number was passed as argument, use it. Otherwise detect from the branch:
gh pr view --json number,url,headRefName,title 2>/dev/null
If no PR exists for the branch, say so and stop. If detection is ambiguous, confirm the number with the user before continuing.
Get owner/repo for the GraphQL calls:
gh repo view --json owner,name
Also confirm the working tree is clean (git status --short). If it isn't, show the
diff stat and ask the user whether to continue on top of it or stop — uncommitted
work will otherwise get swept into a per-point commit.
Step 2 — Fetch all the feedback (once)
Four sources, one pass. This is the only network-heavy step.
2a. Inline review threads (GraphQL)
The query must carry the thread id and each comment's databaseId — they are
needed later for the in-thread reply, the resolve mutation and the state file key.
gh api graphql -f query='
query($owner: String!, $repo: String!, $pr: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pr) {
reviewThreads(first: 100) {
nodes {
id
isResolved
isOutdated
path
line
originalLine
diffSide
comments(first: 20) {
nodes { id databaseId body author { login } createdAt }
}
}
}
}
}
}' -f owner=<owner> -f repo=<repo> -F pr=<PR#>
Keep only isResolved == false. Keep isOutdated == true threads too — flag them
as "diff obsolète" (the line may have moved) but never drop them.
2b. Review bodies
gh pr view <PR#> --json reviews
Keep reviews with a non-empty body — reviewers put real asks in the summary.
Ignore APPROVED reviews with an empty body.
2c. General PR comments
gh pr view <PR#> --json comments
Skip bot noise and CI chatter unless it raises a real concern.
De-duplicate: the same point raised inline and in a review body is one item.
2d. The companion PR in the sibling repo
A front ticket usually has a back PR, and its review often lands on the front. Look
for a PR carrying the same ticket prefix (PROD-XXXX / #N) in the sibling repo
(agorize-front ↔ agorize-core):
gh pr list --repo <org>/<sibling-repo> --state open --search "<PROD-XXXX or #N>" \
--json number,title,url,headRefName
If one exists, ask once whether to fold its unresolved threads into the same inventory (rerun 2a–2c against it). Points from the companion PR are tagged with their repo in the table.
Any point requiring a back change is handled in the agorize-core worktree of this
same ticket — reuse it if git worktree list already shows one, create it
otherwise. Never work in an unrelated agorize-core worktree, and never in the main
checkout.
2e. Persist the state — keyed on the PR, not on the session
The scratchpad is session-scoped: a state written there is lost the moment the user comes back tomorrow. Write instead to a stable, PR-keyed directory:
~/.claude/pr-feedback/<owner>-<repo>-<PR#>/state.json
~/.claude/pr-feedback/<owner>-<repo>-<PR#>/reply-<databaseId>.md
The directory is keyed on the PR the skill was invoked from, and a companion pass
(2d) writes its points into that same directory, whatever worktree you happen to be
in — otherwise the cross-repo points lose their status on the next pass. Create it
once with mkdir -p before the first write. state.json holds one entry per point:
{
"key": "<thread node id, or comment databaseId for a review body / PR comment>",
"databaseId": 123456789,
"source": "thread | review | comment",
"repo": "agorize-front",
"path": "src/foo.vue", "line": 42, "author": "reviewer",
"gist": "une phrase",
"body": "corps intégral de chaque commentaire du thread, non tronqué",
"status": "open | done | parked | answered | skipped",
"commit": "a1b2c3d", "reply": "none | drafted | posted", "note": "en attente du PO",
"aiAuthored": true
}
aiAuthored is set at fetch time: the reviewer's comment starts with 🤖 and its
author isn't the PR author. Step 5 reads it to decide the Jira transition — don't
recompute it later from a thread that has since gained one of your own 🤖 replies.
Alongside the points, the file carries two top-level keys: reviewers (the distinct
author.login of every point, minus the PR author) and closeout (set at the end of
Step 5). Both survive the passes where a fetched thread no longer appears.
If the file already exists, load it and merge on key, never on the index — a
thread resolved or a comment added since the previous pass shifts every number. A
point absent from the fresh fetch (resolved on GitHub in the meantime) keeps its
entry for the final report but leaves the loop.
Rewrite state.json after every status change: a validated fix, a parked point,
an approved draft, a posted reply. Step 4a re-quotes the reviewer from body, so a
long loop can't lose the exact wording, and the fetch never has to run twice.
If nothing is unresolved across the sources, report it and stop.
Step 3 — Print the inventory, then start point 1 in the same turn
Before reading a single line of code, output a numbered table. This is the map for the whole session and what makes "reprends au point 4" possible.
## Retours à traiter — PR #<N> (<title>)
<X> points, dont <Y> déjà traités lors d'un passage précédent.
| # | Emplacement | Auteur | En une phrase | Statut |
|---|-------------|--------|---------------|--------|
| 1 | `src/foo.vue:42` | @reviewer | Passer `mapState` en `mapStores` | ✅ commit `a1b2c3d` |
| 2 | Review (résumé) | @reviewer | Le cas liste vide ne serait pas géré | à traiter |
| 3 | core · `app/x.rb:8` | @reviewer | Question sur le nommage du prop | ⏸ en attente du PO |
The Statut column comes from state.json. It is what makes the walk resumable
without the user having to say "ne traite que le 2, les autres sont déjà faits".
No verdicts, no code reading, no opinions at this stage — a faithful one-line gist per point is all that belongs here. The full comment body is re-printed when the point comes up in 4a, so nothing is lost by keeping this table terse.
Then go straight into 4a for the first point still open, in the same turn. Do
not end the turn on "on y va ?" — the table already shows what remains, and the user
interrupts if they want to restrict the run. If a start index was passed as argument
(/walk-pr-feedback 3217 4), start at that line of the freshly printed table.
Step 4 — The loop: one point at a time
For each point N, in order. Never batch two points into one turn.
4a. Restate the reviewer's comment in full, then ground it in the code
Open every point by re-printing the feedback verbatim and in full — the inventory only carried a one-line gist, so by the time point N comes up neither the user nor you still has the actual wording in view. Don't summarize, don't trim, don't paraphrase: the reviewer's exact words are what's being judged, and a trimmed quote is how a misreading slips in.
### Point <N>/<X> — `src/foo.vue:42` — @reviewer · <date>
> <corps intégral du commentaire, tel quel — code blocks, suggestions et liens compris>
If the thread already has several comments (reviewer's follow-up, a previous answer), quote all of them in order, each attributed to its author — the discussion so far is often what makes the ask intelligible. Same for a point that came from a review body or a general PR comment: the whole relevant passage, not an extract.
The quote must also be inside every AskUserQuestion of the point (4c, 4d, 4e,
4f gates). The question dialog hides the text printed before it, so a gate that only
says "on valide ?" leaves the user deciding without the comment in view. Build the
question field as: Point <N>/<X> — <path:line> — @reviewer : + the reviewer's
comment in full (for a very long comment, the whole ask with only digressions cut,
marked […]) + the verdict and what was done in a few lines + the actual question.
Then read the file at path:line (originalLine if line is null) and enough context
around it to actually judge the point. For a .vue / .ts file whose comment
concerns behavior, also open the matching .spec.ts.
Re-read the file fresh every time, even if a previous point already opened it — an earlier fix may have moved lines.
For a point that names no file, grep for the symbol or component mentioned.
Every claim about how the code behaves must cite path:line. If a point can't be
settled from the code (product or design question), mark it
non vérifiable dans le code rather than guessing. If it depends on what the API can
actually return, check it in the agorize-core worktree of the ticket — never assume a
payload shape.
4b. Classify into one of five branches
| Branche | Quand | Ce qui se passe |
|---|---|---|
| ✅ D'accord | Le reviewer a raison et la correction est évidente | 4c — corriger puis faire valider |
| 🔀 Plusieurs solutions | D'accord sur le fond, mais ≥ 2 implémentations défendables | 4d — faire choisir |
| 🤔 À discuter | Point légitime mais arbitrage produit / convention / trade-off | 4e — demander la position de l'utilisateur |
| ❌ Pas d'accord | Point erroné, déjà traité, ou hors scope — preuve dans le code à l'appui | 4f — brouillon de réponse |
| ⏸ En attente | Dépend d'un tiers (PO, design, back) que l'utilisateur doit consulter | 4g — parquer, passer au suivant |
State the verdict and the path:line reasoning in one short block before acting.
Disagreeing is a normal outcome, not a failure mode — but the burden of proof is on
you when you do.
4c. ✅ D'accord — corriger, puis faire valider
- Check the scope before editing. Touch only files already in the PR's diff. A file outside it, a shared utility, a serializer, or more than three files → stop and confirm the scope with the user. Before deleting an i18n key, a constant or a helper, grep its other usages — dropping it because this component no longer uses it is the classic miss.
- Apply the fix, following the project conventions (CLAUDE.md + memory):
mapStoresnotmapState/mapActions, Options API, BEM classes mirroring the DOM, blank line between sibling template elements / none between script option blocks, boolean prop shorthand, no!non-null assertion, explicitelse, positive condition first. - Update the matching
.spec.tsin the same change if behavior moved. Most review feedback is about tests, and these are the misses that come back every time:- a literal used twice in the same
it()→ a const; recount after every added assertion — that's how a second occurrence appears unnoticed const expected*: Typeextracted beforetoEqual/toHaveBeenCalledWithas soon as the value is a constructed object or arraydisplays, neverrenders; nowhenin anit()label; theit()names the observable action, not the implementation- no optional chaining in assertions: extract,
toBeTruthy(), then guard the payload check inside anif - no vacuous test: a non-conditional
aria-label, tooltip or pass-through - explicit type annotation on every object literal
- a single
mount(/shallowMount(per file, insidebuildComponent - mocks imported from
tests/helpers/mocks/, never built in the spec; a test that needsascasts is the sign the shared mock should be enriched instead
- a literal used twice in the same
- Only if the point can break something, run the spec files touched by this
point:
npx vitest run <path-to-spec>. Skip it for a change that cannot alter behaviour — renaming a local variable or anit()label, reordering, a comment, extracting a literal into a const. Run it as soon as logic, a conditional template branch, a component contract or a spec's setup/assertions change. Grep for the path if it isn't obvious:grep -rl "ComponentName" tests/ --include="*.spec.ts". Never the full suite, never--maxWorkers— a single review point doesn't justify rerunning the whole project. ReportPASS/FAIL, and grep the output for[Vue warn]— any warning is a bug. If it fails, try one fix iteration; if it still fails, revert and hand the point back to the user asneeds-manual-review. - Show
git difffor the touched files. For a rendering point (CSS, layout, responsive, visual state) a diff proves nothing: screenshot it yourself (headless Chrome, fresh profile) or ask the user for a capture before gating.
Then gate — always, no exception. A fix is never committed without this
AskUserQuestion, however small it looks:
- Valider et commiter — go to 4h
- Corriger d'abord — the user says what to change; apply, re-run the spec if step 4 calls for it, re-show the diff, ask again
- Annuler ce point — mark the point skipped, next N. Revert with
git restore --source=HEAD <files>only in commit-per-point mode, where HEAD already carries the earlier fixes. In single-commit mode (see 4h) a checkout would wipe a previous point's uncommitted fix to the same file — undo by re-editing.
4d. 🔀 Plusieurs solutions — faire choisir
Don't write code yet. Put the candidates in an AskUserQuestion, one option each,
using the option preview field to show the actual snippet side by side. Two to
three candidates, each with its trade-off in the description. Put your own
recommendation first, suffixed (Recommandé).
Once chosen, drop into 4c from step 1 with that solution.
4e. 🤔 À discuter — demander la position
Present the trade-off in three or four lines with the path:line evidence, then ask
via AskUserQuestion what the user wants:
- adopt the reviewer's proposal → 4c
- keep the current code and explain why → 4f
- a third path the user describes → 4c with that path
Whatever the outcome, the thread gets an answer — the user's position becomes the substance of the reply drafted in 4f.
4f. ❌ Pas d'accord — brouillon de réponse
Draft the reply without posting it. It must:
- open with
🤖(required marker for AI-authored PR content) - be in the language the reviewer used
- cite the
path:linethat proves the point - stay short and non-defensive — one paragraph, no lecture
Show the draft, then gate with AskUserQuestion:
- Valider ce brouillon — persist it, next N
- Reformuler — the user says how; redraft and ask again
- Finalement corriger — go to 4c
Nothing is posted at this stage. Replies leave in one batch at Step 5.
Write every approved draft the moment it's approved, into
~/.claude/pr-feedback/<owner>-<repo>-<PR#>/ — reply-<databaseId>.md for an inline
thread, reply-review-<reviewId>.md or reply-comment-<id>.md for a point coming
from a review body or a general PR comment (those are posted with gh pr comment, so
they need a name of their own). Set the point to answered / reply: drafted in
state.json. Step 5 posts from those files; a long interactive loop is exactly the
situation where a draft held only in context gets lost.
4g. ⏸ En attente — parquer le point
When the user says the point depends on someone else ("j'ai posé la question au PO",
"à voir avec le design", "Marine doit trancher"), don't force a verdict and don't
guess. Set the point to parked in state.json with a one-line note saying what is
awaited, say it in one line, and move on to the next point in the same turn.
A parked point is never silently dropped: it shows as ⏸ en attente in the inventory
of the next pass and in the final report, and its thread stays open. When the user
comes back with the answer ("pour le point 8, le PO veut X"), re-enter at 4c with it.
4h. Commit the point, then chain to the next one
One commit per validated point — it keeps the loop resumable and makes each review point traceable. (If the user asks for a single commit at the end, hold the changes and commit once at Step 5 instead.)
- Format only the files this point touched, never the whole project:
bunx eslint --fix <files>for.ts/.js/.vue,bunx stylelint --fix <files>for.scss, thenbunx prettier --write <files>. Then stage explicitly:git add <file1> <file2>— nevergit add -A/git add .. - Prefix from the branch name:
PROD-XXXX-...→PROD-XXXX,<N>-...→#<N>, otherwise no prefix. - Message in English, conventional style (
fix(scope): ...,update(scope): ...), focused on the why. ThePreToolUsehook checks prettier/eslint/stylelint on the staged files and blocks the commit on failure — it does not format. - Write the reply draft for this thread (🤖 + what changed + the
path:line) to~/.claude/pr-feedback/<owner>-<repo>-<PR#>/reply-<databaseId>.md. - Update
state.json:status: done, the commit sha,reply: drafted. - Chain straight into 4a of the next point, in the same turn. Announce
→ Point N+1 sur Xand carry on — never end a turn on the announcement or on a "j'y vais ?". The validation being asked for is on the fix (the 4c gate), never on whether to keep going.
Never push inside the loop — the pre-push lint gate would run on every point.
When the last point is done, go to Step 5.
Step 5 — Close out: push once, then reply and resolve
Normally reached at the end of the loop — but the user can trigger it mid-loop
("réponds et résous ce qui est déjà traité, et push"). In that case, close out only
the points marked done / answered, leave the rest untouched in state.json, and
resume the loop at the next open point afterwards.
Steps 1–3 below run only if at least one commit was made. If the pass produced nothing but replies, skip straight to the posting part.
- Pre-push check:
bun run typecheck(fall back to the repo's package manager), only if a commit of this pass changed a signature, an interface, a type or an exported symbol — or touched.ts/.vuecode in a way the specs don't cover. Skip it when the pass only changed test labels, literals, CSS or comments. Neverbun run lint: the commit hook already linted each commit's staged files, and the localpre-pushhook runs the full lint on push. Not the test suite either — the specs were handled at 4c. If the typecheck fails, report and stop — don't push. - Ask before pushing. Unlike
/ship, invoking this skill is not authorization to push — the user gated commits and replies, never push.AskUserQuestion: pousser maintenant / je pousse moi-même / rester local. On anything but the first, skip the push, hold the replies (they reference pushed fixes), and jump to Step 6 listing what's left to do. git push(add-u origin <branch>if there's no upstream). Verify the branch name first; never push to master. If thepre-pushlint hook blocks the push, report the failure and stop.
Then post the replies validated during the loop, in-thread, never top-level, from the files written during the loop:
gh api repos/<owner>/<repo>/pulls/<PR#>/comments \
-f body="$(cat ~/.claude/pr-feedback/<owner>-<repo>-<PR#>/reply-<databaseId>.md)" \
-F in_reply_to=<comment databaseId>
Use --body-file / "$(cat …)" rather than inline escapes — backticks and $ in a
reply break an inline -f body=. Every reply starts with 🤖. Mark the point
reply: posted in state.json as each one lands.
For points raised in a review body or a general PR comment (no thread), reply with
gh pr comment — still prefixed 🤖.
Then resolve only the threads whose fix was pushed:
gh api graphql -f query='mutation($threadId: ID!) {
resolveReviewThread(input: {threadId: $threadId}) { thread { isResolved } }
}' -f threadId=<thread id>
Threads answered with a disagreement, a discussion reply or a parked point stay open — the reviewer closes those. Resolve one anyway only if the user explicitly asks. If the user says a thread was resolved by mistake, reopen it:
gh api graphql -f query='mutation($threadId: ID!) {
unresolveReviewThread(input: {threadId: $threadId}) { thread { isResolved } }
}' -f threadId=<thread id>
Relancer la review — et le ticket si la review venait d'une IA
Once the threads are resolved, two closing actions, in this order. Both run only on a
final close-out — every point in state.json is done, answered or skipped,
nothing left open or parked, and this pass pushed or posted something. Three cases
skip them, each announced in one line so the user knows they're deferred, not lost:
- a mid-loop close-out (Step 5 triggered with points still to walk) — post the replies, resolve the threads, then go back to the loop
- points still
parked— the PR isn't ready for re-review while one waits on the PO - the user chose je pousse moi-même / rester local — nothing is on the remote yet
Once run, set "closeout": "<ISO date>" at the top level of state.json; a later pass
on the same PR that adds no new point must not re-fire the transition.
1. Re-request the review on GitHub — always, whoever the reviewer was. Resolving a thread doesn't put the PR back in anyone's queue; the review has to be asked for again. For every reviewer whose feedback was handled in this pass:
gh api repos/<owner>/<repo>/pulls/<PR#>/requested_reviewers \
-X POST -f "reviewers[]=<login>"
That REST endpoint is the re-request primitive — it puts back a reviewer who has
already submitted a review, which is exactly the case here.
gh pr edit <PR#> --add-reviewer <login> is the fallback if it errors; it takes a
comma-separated list but doesn't accept @me / @copilot.
The logins come from reviewers in state.json (2e), not from the fresh fetch — a
thread resolved in an earlier pass is no longer returned by 2a. Skip the PR author's
own login (GitHub rejects a self-request) and bot accounts (<name>[bot]), which that
endpoint can't re-request.
Read the result back rather than trusting the exit code — reporting « review redemandée » when nothing reached the queue is worse than failing loudly:
gh pr view <PR#> --json reviewRequests
If a login is missing from it, say so instead of claiming the re-request landed. A failure (no write access, reviewer removed from the repo) is reported in one line and never blocks the close-out. A companion PR walked at 2d gets its own re-request.
2. Transition the Jira ticket to "Final Review" — only for an AI review. Two conditions, both required:
- the branch carries a
PROD-XXXXprefix (no Jira ticket → nothing to transition), and - at least one handled point carries
aiAuthored: trueinstate.json— a reviewer comment starting with🤖, authored by someone other than the PR author.
The second check is the one to get right: the user's own replies posted by this very
skill also start with 🤖, which is why the flag is set at fetch time (2e) and read
here rather than recomputed. No aiAuthored point → the review was human: leave the
ticket where it is.
When both hold, list the transitions and apply "Final Review":
curl -s -u "$JIRA_EMAIL:$JIRA_API_TOKEN" \
-X GET \
"https://agorize.atlassian.net/rest/api/3/issue/<PROD-XXXX>/transitions" \
| jq '.transitions[] | {id, name}'
curl -s -u "$JIRA_EMAIL:$JIRA_API_TOKEN" \
-X POST \
-H "Content-Type: application/json" \
-d '{"transition":{"id":"<ID>"}}' \
"https://agorize.atlassian.net/rest/api/3/issue/<PROD-XXXX>/transitions"
The Jira credentials come from the environment: JIRA_EMAIL (the Atlassian account
email) and JIRA_API_TOKEN. If either is unset, say so and skip the transition — never
guess the email from git config, the Jira account can differ from the commit identity.
If "Final Review" isn't in the
available transitions (the ticket isn't in a state that allows it), say which
transitions were offered and leave the ticket untouched — don't pick a neighbouring
status.
Finally, if the fixes changed what the PR actually does — a behaviour added or dropped, a renamed component, a removed access right — the description is now stale. Say so in one line and offer to update it (both PRs if a companion one was walked); don't rewrite it unasked.
Step 6 — Final report
## PR #<N> — <X> points traités
| # | Emplacement | Verdict | Action | Tests | Commit | Réponse |
|---|-------------|---------|--------|-------|--------|---------|
| 1 | `src/foo.vue:42` | ✅ | `mapStores` | PASS | `a1b2c3d` | postée + résolu |
| 2 | Review (résumé) | ❌ | aucune | — | — | postée, thread ouvert |
| 3 | `src/baz.vue:88` | 🤔 → ✅ | prop renommé | PASS | `e4f5g6h` | postée + résolu |
| 4 | `src/old.vue:5` | ⏸ | en attente du PO | — | — | thread ouvert |
| 5 | `tests/foo.spec.ts:12` | ✅ | libellé `it()` | non lancés | `c3d4e5f` | postée + résolu |
Threads laissés ouverts : #2 (en attente du reviewer), #4 (en attente du PO).
Review redemandée à @reviewer · Jira PROD-7705 passé en « Final Review » (review IA).
The last line states both close-out actions: who the review was re-requested from, and whether the ticket moved to "Final Review" — or why it didn't (pas de ticket Jira, review humaine, transition indisponible, rien poussé).
State state.json is up to date and name the points still open or parked — the
next pass resumes from there without the user having to renumber anything.
Rules
- Allowed after validation of that specific point: file edits, commits, push,
posted replies, thread resolution. This is the mutating skill of the pair — don't
inherit
/analyze-pr-feedback's read-only caution. - Never post a reply the user hasn't validated, resolve a thread whose reply wasn't approved, push mid-loop, or push at all without asking first — invoking this skill authorizes the loop, not the push.
- Re-requesting the review and the "Final Review" transition are the exception: they
are automatic once the threads are resolved, no
AskUserQuestion. The transition fires only for aPROD-XXXXbranch whose reviewer comments are🤖-prefixed — the PR author's own🤖replies don't count as an AI review. - One point per turn — but a turn ends on a validation gate, never on "on continue ?". Don't read ahead and don't pre-fix point N+1.
- Every point opens with the reviewer's comment quoted in full. Summarizing it is
the one economy not to make: it's the text being judged. Repeat it inside the
questionof every gate — the dialog hides the text printed before it. - Don't assert code behavior you haven't read — cite
path:lineor saynon vérifiable dans le code. - Stay inside the PR's diff. A shared utility, a serializer, a file outside the diff, or more than three files → confirm the scope with the user before editing.
- Never run the full test suite. Only the specs touched by the current point.
- Stay on the current working tree — the user is already on the right branch. No
worktree, no
git checkout <branch>. A back change goes to the agorize-core worktree of the same ticket.