Repository navigation
fix(shepherd): model a review-body reviewer and three verdicts (#41 items 3-5) - #46
Conversation
Agent QA — reviewer audit score
SCORE: 6/6 (gate threshold: 4; recorded baseline: 6/6 (100%)) Full review output: workflow run artifact Planted-defect harness |
There was a problem hiding this comment.
Review Summary
- Files reviewed:
plugin/agents/pr-shepherd.md,plugin/skills/shepherd-pr/SKILL.md,CHANGELOG.md,VERSION,plugin/.claude-plugin/plugin.json - Overall verdict: COMMENT — one major correctness gap; the SKILL variant is solid
Major Issues (should fix)
plugin/agents/pr-shepherd.md:391 — Route G is absent from the agent; the CHANGELOG's parity claim is false
SKILL.md adds explicit Route G:
IF ACTIONABLE == 0 AND … AND VERDICT is COMMENTED or CHANGES_REQUESTED
→ print REVIEWED-NOT-APPROVED REPORT, stop (no ScheduleWakeup)
The agent has no equivalent. Its Step 7 else-branch adds a comment ("COMMENTED/CHANGES_REQUESTED is TERMINAL — never wait for it to change on its own") but then says "Act on its findings" — when ACTIONABLE is 0 there are none, so the instruction gives no exit signal, and the loop continues to Step 1 and repeats. That cycle runs until MAX_WAIT_ITER before escalating as "reviewer appears stuck": the exact bad behaviour item 5 was written to fix, still present in the agent variant.
The CHANGELOG's claim — "plugin/agents/pr-shepherd.md carries the same corrections, so the agent and skill variants cannot drift apart on what counts as done" — is demonstrably false for this specific fix.
Fix: add an explicit guard in the pr-shepherd.md loop structure (after the Done Check block) mirroring Route G's exit condition: when ACTIONABLE is 0 and VERDICT is COMMENTED or CHANGES_REQUESTED, write the Reviewed-Not-Approved report and exit without looping.
Minor Issues (optional)
plugin/agents/pr-shepherd.md:257 — redundant gh api call for REVIEW_BODY
Lines 248–250 fetch the reviews payload to extract VERDICT; lines 257–259 make a second, identical gh api "repos/${REPO}/pulls/${PR}/reviews" --paginate call for REVIEW_BODY. SKILL.md correctly captures this in $VERDICT_RAW and derives both values from the same snapshot. The double call wastes a round-trip and opens a race window: if a new review lands between the two calls, VERDICT and REVIEW_BODY reflect different reviews and BODY_FINDINGS can be computed against mismatched state. Fix: capture the payload in VERDICT_RAW and derive both variables from it, as SKILL.md does.
plugin/agents/pr-shepherd.md:370 — Done Check uses stale $BODY_FINDINGS from Step 3
Step 7 re-fetches OPEN_COUNT and VERDICT live (correct), but evaluates the $BODY_FINDINGS value set in Step 3 — before the push. If the push triggers a fast automated approval between Steps 5–6 and the Done Check, $BODY_FINDINGS still reflects the pre-fix CHANGES_REQUESTED state and fails the [ "$BODY_FINDINGS" -eq 0 ] guard despite the reviewer having approved. The consequence is one wasted extra loop rather than a wrong decision, but it's a silent inconsistency. Fix: re-derive $BODY_FINDINGS from the freshly-fetched $VERDICT and $REVIEW_BODY at the top of Step 7 (or accept the extra iteration as acceptable debt, and document it).
plugin/skills/shepherd-pr/SKILL.md:158 — changelog and code comment overstate when the gate check appears in CI_PENDING
The comment (and the CHANGELOG entry for item 4) says the gate check "still counts toward CI_PENDING, where 'the review is running' is exactly what it means." That is true only while the gate is IN_PROGRESS. When the reviewer posts CHANGES_REQUESTED, the gate check transitions to FAILURE — which the CI_PENDING query excludes by state, so it is counted in neither CI_FAILING nor CI_PENDING. The behavior is correct and harmless (VERDICT carries the semantics), but the comment will mislead someone debugging a FAILURE gate. Fix: tighten the wording — "counts toward CI_PENDING while it is running (IN_PROGRESS/QUEUED); once it reaches FAILURE the gate's signal is carried entirely by VERDICT."
Security Findings
None.
Positive Observations
- The SKILL.md routing logic is clean and complete: Route G correctly terminates the loop on terminal verdicts with nothing actionable, and the CI_FAILING exclusion is well-guarded with
$REVIEW_GATE_CHECKbeing overridable for consumer repos. - Reusing
$VERDICT_RAWfor bothVERDICTandREVIEW_BODYin SKILL.md is the right design — one call, one consistent snapshot. - The 40-char body threshold for
BODY_FINDINGSis conservative and the decision to leave APPROVED bodies non-actionable (avoiding the "treadmill" re-open) is well-reasoned. - The PR's verification table (test cases against real PRs #39, #40, #42, #43) is exactly the right evidence to accompany a routing change.
…tems 3-5) The shepherd assumed a reviewer that posts inline threads and answers APPROVED-or-nothing. This repo's reviewer posts a single review body and answers with three verdicts, so three failures compounded. Item 3 — findings were invisible. The state machine keyed entirely on THREAD_COUNT from reviewThreads, but `gh pr review --body-file` submits one top-level body and CANNOT attach line comments. THREAD_COUNT was therefore always 0, the fix route never fired, and Steps 5-8 were dead code against this reviewer. Step 2 now also reads the review body from the same REST payload already fetched for the verdict, and routing keys on ACTIONABLE — threads, a body, or both. An APPROVED body is deliberately not actionable: approval is terminal success, and reopening it restarts the treadmill. Item 4 — a by-design failure was reported as broken CI. The review gate check fails deliberately on changes-requested, but it counted toward CI_FAILING, so Route D fired "CI is failing — investigate CI failures" on a PR whose builds were all green and whose real signal was a review finding. CI_FAILING now excludes the gate by name via REVIEW_GATE_CHECK (overridable, which consumer repos in #30 will need). It still counts toward CI_PENDING, where "the review is running" is exactly what it means. Item 5 — a concluded review was polled as if it might change. Route A terminated only on APPROVED, so a COMMENTED head fell to the wait route and polled 30 x 90s ~ 45 minutes before escalating as "reviewer appears stuck" — with the findings sitting unread in the body. Route E now waits only on PENDING; COMMENTED and CHANGES_REQUESTED are terminal, routing to the fix path when they carry findings and to a new Route G ("Reviewed, Not Approved") when they do not. Steps 5/7/8 no longer assume every finding lives in a resolvable thread: a body-posting reviewer gets one disposition comment per round, listing declined findings with reasoning, since it offers nothing to reply to or resolve. plugin/agents/pr-shepherd.md carries the same corrections so the two variants cannot drift on what counts as done. Verified against this repo's real PRs and the observed CI shape: PR #42 verdict=COMMENTED, 0 threads, 4161-char body OLD -> Route E (poll ~45 min, then "stuck") NEW -> Route B (fix findings) PR #39/#40/#43 verdict=APPROVED with a body OLD -> Route A (done) NEW -> Route A (done, unchanged) verdict gate FAILURE + all builds green OLD CI_FAILING=1 -> Route D "investigate CI" NEW CI_FAILING=0 -> routes to B genuine build failure alongside the gate NEW CI_FAILING=1 (no regression) verdict gate PENDING CI_PENDING=1 (still waits while the review runs) Refs #41 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WDPKxAK1dJvyn3gXaGfsFc
…state Addresses the review on PR #46. Major — my CHANGELOG claimed the agent "carries the same corrections" and that the two variants "cannot drift apart on what counts as done". That was false for the fix that mattered most. SKILL.md gained Route G's terminal exit; the agent got only a comment saying the verdict is terminal, then "act on its findings" — which, when there are none, gives no exit signal at all. It looped back to Step 1, re-read the same finished review, and ran to MAX_WAIT_ITER before escalating as "reviewer appears stuck": precisely the item-5 behavior this PR exists to remove, left intact in half the stack. The agent now has an explicit guard mirroring Route G — green CI, no threads, no body findings, and a COMMENTED or CHANGES_REQUESTED verdict writes the Reviewed-Not-Approved report and exits without looping. The CHANGELOG says what is actually true now. Minor — the agent fetched the reviews payload twice, once for VERDICT and again for REVIEW_BODY. Besides the wasted round-trip, a review landing between the two calls leaves them describing different reviews and BODY_FINDINGS computed from mismatched state. One VERDICT_RAW snapshot now feeds both, matching SKILL.md. Minor — the Done Check evaluated BODY_FINDINGS from Step 3, i.e. pre-push. A push that triggered a fast approval would still fail the guard on stale state and cost a wasted loop. It now re-derives verdict and body from one fresh snapshot. Minor — the CI_PENDING comment and CHANGELOG overstated the gate check's membership. It counts as pending only while running; on FAILURE it falls out of both counts and VERDICT carries the signal alone. Reworded in both places, including why that is deliberate. Verified the new guard as real shell across seven states: exits on COMMENTED and CHANGES_REQUESTED with nothing outstanding; continues when body findings exist, when threads are open, when the verdict is still PENDING, and when CI is not green. APPROVED never reaches the guard — the done branch catches it first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WDPKxAK1dJvyn3gXaGfsFc
615d63b to
4b084ef
Compare
|
Fixed in 4b084ef (rebased onto main now that #45 has merged). Major — correct, and the sharper point is that my CHANGELOG asserted the opposite. I claimed the agent "carries the same corrections" and that the two variants "cannot drift apart on what counts as done." That was false for the one fix that mattered most: SKILL.md got Route G's terminal exit, the agent got only a comment saying the verdict is terminal followed by "act on its findings" — which, when there are none, gives no exit signal at all. It looped to Step 1, re-read the same finished review, and ran to The agent now has an explicit guard mirroring Route G, and the CHANGELOG says what is actually true — including why the agent needed it. Exercised as real shell across seven states:
Minor (double fetch) — fixed. Worth noting the race you identified is the more serious half: a review landing between the two calls leaves Minor (stale Minor ( |
There was a problem hiding this comment.
Review Summary
- Files reviewed:
plugin/agents/pr-shepherd.md,plugin/skills/shepherd-pr/SKILL.md,CHANGELOG.md,VERSION,plugin/.claude-plugin/plugin.json - Overall verdict: REQUEST CHANGES — Route G is structurally non-functional in the agent for its primary target verdict; item 4's fix was applied to the skill but not ported to the agent, and the two gaps compound.
Critical Issues (must fix before merge)
plugin/agents/pr-shepherd.md:344–345, 413 — Route G can never fire for CHANGES_REQUESTED
ALL_OK is computed by checking that every CI check is SUCCESS/SKIPPED/NEUTRAL/STALE/CANCELLED. When VERDICT=CHANGES_REQUESTED the "Claude verdict" gate check is in FAILURE state (that is the gate's designed behaviour), so ALL_OK=false. Route G's guard is [ "$ALL_OK" = "true" ], which therefore fails for every CHANGES_REQUESTED verdict. Route G — the fix for item 5, "concluded review was polled as if it might change" — cannot fire for the primary verdict it was written to handle; the agent falls through to the KG write-and-loop path and exhausts MAX_ITER before escalating, reproducing the original bug.
The gate check exclusion (REVIEW_GATE_CHECK / select(.name != $gate)) was added to SKILL.md's CI_FAILING computation but was not applied to the agent's ALL_OK query in Step 7. Fix: mirror the SKILL.md exclusion in Step 7's ALL_OK jq expression — exclude the gate check by name before the all(…) test, e.g.:
REVIEW_GATE_CHECK="${REVIEW_GATE_CHECK:-Claude verdict}"
ALL_OK=$(echo "$CHECKS_JSON" | jq -r --arg gate "$REVIEW_GATE_CHECK" \
'(length > 0) and ([.[] | select(.name != $gate) | .state]
| all(. == "SUCCESS" or . == "SKIPPED" or . == "NEUTRAL" or . == "STALE" or . == "CANCELLED"))')Major Issues (should fix)
plugin/agents/pr-shepherd.md:170–171 — item 4's fix (gate check exclusion from FAILURES) is absent from the agent
SKILL.md adds --arg gate "$REVIEW_GATE_CHECK" and select(.name != $gate) to the CI_FAILING query so the gate check's designed FAILURE is not treated as broken CI. The agent's Step 2 FAILURES query has no equivalent filter. When the reviewer requests changes, "Claude verdict" appears in FAILURES and Step 2 attempts to diagnose and fix it as a CI failure — consuming an iteration on a check that is working correctly. The CHANGELOG's claim that "plugin/agents/pr-shepherd.md carries all three corrections" is false for item 4. Fix: add the name filter to the Step 2 FAILURES query, matching the SKILL.md pattern.
Minor Issues (optional)
CHANGELOG.md:33 — parity claim is false for item 4
"plugin/agents/pr-shepherd.md carries all three corrections" — items 3 and 5 are present; item 4 is not. Update after the code is fixed to avoid a future reviewer trusting the claim to skip a cross-check.
plugin/skills/shepherd-pr/SKILL.md:~307 — DISMISSED and other non-standard verdicts fall through all routes
Route E now matches only PENDING; Route G matches only COMMENTED or CHANGES_REQUESTED. A DISMISSED verdict (or any API response not in that set) matches no route, exits with no wakeup and no report. Document the intended behaviour or add a final catch-all for unknown verdicts.
Positive Observations
- The single-fetch pattern for
VERDICT_RAW(derive bothVERDICTandREVIEW_BODYfrom one API call) is the correct fix for the race window; consistently applied across both Step 3 and the Step 7 re-derive in both files. - SKILL.md's
ACTIONABLEabstraction cleanly unifies thread and body findings across all routing branches; the inline comments explaining whyTHREAD_COUNT-only routing was blind to body-posting reviewers are clear and correct. - Route G's report template and the "do not schedule a wakeup" directive are both present and correctly placed in SKILL.md.
…th queries The agent variant never received item 4's gate-check exclusion, and the omission compounded into a Critical defect. Step 7 computed ALL_OK across every check, including "Claude verdict" — the gate that FAILS BY DESIGN when the reviewer requests changes. On any CHANGES_REQUESTED head that pinned ALL_OK=false, and since the terminal-verdict exit is guarded on ALL_OK=true, that exit could never fire for the primary verdict it was written to handle. The agent fell through to the KG write-and-loop path and burned MAX_ITER re-reading a finished review — exactly the bug item 5 set out to remove. Step 2's FAILURES query had the same gap, spending an iteration diagnosing a check that was working correctly. Both queries now exclude the gate by name via REVIEW_GATE_CHECK (default "Claude verdict", overridable per issue #30), matching the skill. Verified against PR #46's live check payload: the old ALL_OK returns false and the new one returns true on a CHANGES_REQUESTED head, while an empty array still yields false (the vacuous-true guard survives) and a genuine build failure alongside the gate is still caught. Both variants also now test the terminal verdict as the complement of APPROVED and PENDING rather than enumerating COMMENTED/CHANGES_REQUESTED, so DISMISSED and any unrecognised state report and stop instead of matching no route and exiting silently with neither a wakeup nor a report. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shepherd round 1 — disposition for review on
|
There was a problem hiding this comment.
Review Summary
- Files reviewed:
plugin/agents/pr-shepherd.md,plugin/skills/shepherd-pr/SKILL.md,CHANGELOG.md,VERSION,plugin/.claude-plugin/plugin.json - Overall verdict: APPROVE — all previously-raised Critical and Major issues are fixed; no new critical or major defects found.
Previously-Raised Issues — Verified Fixed
Critical (last review): Route G could never fire for CHANGES_REQUESTED because ALL_OK counted the gate check (which is in FAILURE by design) and was always false.
Fixed. ALL_OK in Step 7 of the agent now excludes $REVIEW_GATE_CHECK via select(.name != $gate) before the all() test. With the gate excluded, a CHANGES_REQUESTED head where all real builds pass produces ALL_OK=true, and Route G can fire as intended.
Major (last review): Item 4's gate-check exclusion was applied to SKILL.md's CI_FAILING but not to the agent's FAILURES query in Step 2.
Fixed. The agent's Step 2 now passes --arg gate "$REVIEW_GATE_CHECK" and prepends select(.name != $gate) before the failure-state filter, matching the skill.
Major (last review, COMMENTED): Route G was absent from the agent entirely.
Fixed. Lines 431–438 of the agent add the explicit Route G guard (ALL_OK=true && OPEN_COUNT==0 && BODY_FINDINGS==0 && VERDICT != APPROVED && VERDICT != PENDING → exit 0), mirroring SKILL.md Route G.
Minor (last review, COMMENTED): Redundant gh api call for REVIEW_BODY.
Fixed. Both Step 3 and the Done Check (Step 7) now fetch VERDICT_RAW once and derive both VERDICT and REVIEW_BODY from a single snapshot, eliminating the race window.
Minor Observations (no action required)
plugin/agents/pr-shepherd.md — triple declaration of REVIEW_GATE_CHECK
REVIEW_GATE_CHECK="${REVIEW_GATE_CHECK:-Claude verdict}" appears at initialisation (~line 48), in Step 2 (~line 175), and in Step 7 (~line 356). The :- default syntax makes each subsequent declaration a no-op when the variable is already set, so this is harmless — and the per-site comments make each block self-documenting. Purely cosmetic.
plugin/agents/pr-shepherd.md:358 — vacuous-truth comment now slightly misleading
The (length > 0) and … guard was originally written to catch an entirely empty checks array. After the select(.name != $gate) filter, the downstream all() may operate on an empty array even when checks exist (if the gate is the only check), and that is intentional behaviour. The comment still says "Guard against vacuous-true when checks array is empty" — a one-line note that the vacuous case also covers "only the gate remains after filtering" would clarify the intent for future editors.
Positive Observations
- The three bug fixes are tightly scoped and address exactly the failure modes documented in #41.
- The single-snapshot pattern for
VERDICT_RAW(derive bothVERDICTandREVIEW_BODYfrom one call, documented with the race-window rationale) is the right design and consistent across both variants. REVIEW_GATE_CHECKis overridable at each use site, matching the consumer-repo requirement from #30.- Route G uses the complement of
{APPROVED, PENDING}rather than enumeratingCOMMENTED/CHANGES_REQUESTED, correctly catchingDISMISSEDand any future unrecognised verdict without a silent exit. - The verification table in the PR description covers the most dangerous regressions (approvals still terminate, real build failures still surface, running reviews still count as pending) — useful evidence that the exclusion is scoped correctly.
…cked script Addresses the review on PR #48. Major — the workflow's round prompt carried "/Users/bryanw/Projects/Vibe/ai-pack" and issued its review fetch against a literal "Cortexa-LLC/ai-pack". Both survived into a file this PR makes version-controlled and public. Two distinct problems: the absolute path exists on exactly one machine, so every round agent everywhere else is told it is somewhere it is not; and the hardcoded slug directly defeats the copy-this-file guidance in the very doc this PR adds — a consumer's copy would fetch reviews from THIS repository. Fixed: the path is gone entirely (the agent works in whatever checkout the harness gives it), and the slug becomes an optional `repo` arg. Optional rather than required so the documented {pr, branch} call shape keeps working; when it is omitted the round agent resolves the slug from its own checkout with `gh repo view --json nameWithOwner`. meta.whenToUse, which is what surfaces in the skill listing, now names the argument, and SHEPHERDING.md states the portability rule for anyone copying the file. Minor — noProgress was not reset when a round agent failed. Since the null branch `continue`s without recording history, the next successful round compared itself against the last round BEFORE the crash. Two rounds that independently reported the same findings, with a real fix attempt between them that happened to crash, could therefore trip the "stuck" exit as though nothing had been tried. A crashed round is an interrupted attempt, not evidence of no progress; it now resets. Minor — the prompt asserted that "reviews in this PR's history have already produced false positives" and cited two examples that came from PR #46. On a fresh PR that claim is simply untrue, and it primed the round agent to dispute findings by pattern-matching against out-of-scope history. Reworded to say past reviews in this repo, not this PR. Also pruned the stale worktree the completed run left behind. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WDPKxAK1dJvyn3gXaGfsFc
…ow (#48) * docs(shepherd): answer the distribution question and track the workflow Closes the two open deliverables on #29. DISTRIBUTION — plugins cannot ship workflows. Determined empirically rather than inferred: added a workflows/ dir to plugin/, installed it with make update-plugin, confirmed the file present in the installed cache copy, then invoked it by name and got Workflow "plugin-workflow-probe" not found. Available: deep-research, shepherd-pr so it ships but never registers. Corroborating: across the 207 plugins in the marketplace catalog the only component types that appear are agents, commands, hooks, lspServers, mcpServers, and skills — there is no workflows component, and no installed plugin ships such a directory. The probe was removed and the installed plugin restored. Consequence: workflow-backed orchestration is repo-local by necessity, which is why the agent and skill variants are kept rather than collapsed into the workflow. Recorded at the script itself and in the new doc, with the probe written down so it costs one command to re-confirm if Claude Code adds a workflow component type. VERSION CONTROL — a gap found while writing this up: .claude/ is gitignored, so .claude/workflows/shepherd-pr.js was tracked by exactly nothing. The script #29 asks for existed only on one machine — unreviewable, unshareable, and one `rm` from gone. Since plugins cannot distribute workflows, .claude/workflows/ is the only place it can live AND run, so it needs to be version-controlled in place rather than copied somewhere tracked (a second copy is how the skill and agent variants have drifted twice already). The gitignore change uses `.claude/*` rather than `.claude/`: git does not descend into an excluded DIRECTORY, so the negation would otherwise be unreachable. Verified all three ways — the workflow is no longer ignored, while settings.local.json and worktrees/ still are. DOCS — docs/SHEPHERDING.md covers which of the three variants to use when, what the workflow run on PR #46 did and did NOT prove (it exited at round 1, so MIN_ROUNDS, the non-convergence check and the hard cap remain unexercised), and the parity discipline the skill/agent pair needs, including the two CHANGELOG claims that turned out false. README points at it. No plugin/ changes, so no version bump. Fixes #29 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WDPKxAK1dJvyn3gXaGfsFc * fix(workflow): remove a personal path and hardcoded slug from the tracked script Addresses the review on PR #48. Major — the workflow's round prompt carried "/Users/bryanw/Projects/Vibe/ai-pack" and issued its review fetch against a literal "Cortexa-LLC/ai-pack". Both survived into a file this PR makes version-controlled and public. Two distinct problems: the absolute path exists on exactly one machine, so every round agent everywhere else is told it is somewhere it is not; and the hardcoded slug directly defeats the copy-this-file guidance in the very doc this PR adds — a consumer's copy would fetch reviews from THIS repository. Fixed: the path is gone entirely (the agent works in whatever checkout the harness gives it), and the slug becomes an optional `repo` arg. Optional rather than required so the documented {pr, branch} call shape keeps working; when it is omitted the round agent resolves the slug from its own checkout with `gh repo view --json nameWithOwner`. meta.whenToUse, which is what surfaces in the skill listing, now names the argument, and SHEPHERDING.md states the portability rule for anyone copying the file. Minor — noProgress was not reset when a round agent failed. Since the null branch `continue`s without recording history, the next successful round compared itself against the last round BEFORE the crash. Two rounds that independently reported the same findings, with a real fix attempt between them that happened to crash, could therefore trip the "stuck" exit as though nothing had been tried. A crashed round is an interrupted attempt, not evidence of no progress; it now resets. Minor — the prompt asserted that "reviews in this PR's history have already produced false positives" and cited two examples that came from PR #46. On a fresh PR that claim is simply untrue, and it primed the round agent to dispute findings by pattern-matching against out-of-scope history. Reworded to say past reviews in this repo, not this PR. Also pruned the stale worktree the completed run left behind. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WDPKxAK1dJvyn3gXaGfsFc * fix(workflow): make the convergence guard order-insensitive Addresses round 2 on PR #48. Major — `prev.titles.join('||') === r.findingTitles.join('||')` compared two model-generated arrays positionally. Model output ordering is not stable, so two rounds raising the SAME Critical/Major findings in a different order read as progress: noProgress resets, and the stuck exit becomes unreachable. A genuinely stuck PR would then grind all 12 rounds — each blocking synchronously on a full CI run — before falling out at cap-reached. That is the convergence guard the whole workflow exists to provide, not working. Fixed by sorting both sides. Used JSON.stringify rather than join('||') as the reviewer's parenthetical suggested, because the collision runs both ways: ["a||b"] and ["a","b"] join to the same string, which is a false MATCH — it would declare "stuck" on two rounds whose findings differ. Verified across six cases: identical/same order and identical/reordered both compare equal; one finding resolved, a new finding appearing, and the pipe collision all compare unequal; empty/empty equal. Minor — docs listed the cap exit as `cap` while the code returns `cap-reached`, so anything pattern-matching the outcome string would have missed it. All five outcome strings now match between code and docs, including `error`, which the docs omitted entirely. Minor — the MIN_ROUNDS comment read as though the policy floored every exit. It guards only the `stuck` exit; a clean verdict or an owner blocker still returns at round 1. Comment now says so, and the docs repeat it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WDPKxAK1dJvyn3gXaGfsFc --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses items 3, 4, and 5 of #41 — one theme, as the reporter grouped them. Items 1–2 are in #45.
The shepherd assumed a reviewer that posts inline threads and answers APPROVED-or-nothing. This repo's reviewer posts a single review body and answers with three verdicts. Three failures compounded from that one mismatch.
Item 3 — findings were invisible
The state machine keyed entirely on
THREAD_COUNTfromreviewThreads, butgh pr review --body-filesubmits one top-level body and cannot attach line comments.THREAD_COUNTwas permanently 0, the fix route never fired, and Steps 5–8 were dead code against this reviewer.Step 2 now also reads the review body — from the REST payload it already fetches for the verdict, so no extra call — and routing keys on
ACTIONABLE: threads, a body, or both.An
APPROVEDbody is deliberately not actionable. Approval is terminal success; reopening it restarts the treadmill the skill already warns about.Item 4 — a by-design failure was reported as broken CI
The review gate check fails deliberately when the reviewer requests changes. It counted toward
CI_FAILING, so Route D fired "CI is failing — investigate CI failures" on a PR whose builds were all green and whose real signal was a review finding.CI_FAILINGnow excludes it by name viaREVIEW_GATE_CHECK— overridable, which #30's consumer repos will need. It still counts towardCI_PENDING, where "the review is running" is exactly what it means.Item 5 — a concluded review was polled as if it might change
Route A terminated only on
APPROVED, so aCOMMENTEDhead fell to the wait route and polled 30 × 90s ≈ 45 minutes before escalating as "reviewer appears stuck" — with the findings sitting unread in the body.Route E now waits only on
PENDING.COMMENTEDandCHANGES_REQUESTEDare terminal: they route to the fix path when they carry findings, and to a new Route G ("Reviewed, Not Approved") when they don't — which reports the verdict, quotes the body, and stops without a wakeup, because no further automated event is coming.Verified against this repo's own PRs
COMMENTED, 0 threads, 4,161-char bodyAPPROVEDwith a bodyFAILURE, all builds greenCI_FAILING=1→ "investigate CI"CI_FAILING=0→ routes to BPENDINGCI_PENDING=1— still waitsThe middle three rows are the regressions I most needed to rule out: that approvals still terminate, that a real build failure isn't masked by the exclusion, and that a running review still counts as pending.
Also
Steps 5/7/8 no longer assume every finding lives in a resolvable thread — a body-posting reviewer gets one disposition comment per round, listing declined findings with reasoning, since it offers nothing to reply to or resolve.
plugin/agents/pr-shepherd.mdcarries the same corrections so the agent and skill variants can't drift on what counts as done.Bumped to 3.4.0 with a CHANGELOG entry (behavior change under
plugin/).🤖 Generated with Claude Code
https://claude.ai/code/session_01WDPKxAK1dJvyn3gXaGfsFc