Skip to content

fix(shepherd): model a review-body reviewer and three verdicts (#41 items 3-5) - #46

Merged
bawoodruff merged 3 commits into
mainfrom
fix/shepherd-review-body-model
Aug 29, 2026
Merged

bawoodruff merged 3 commits into
mainfrom
fix/shepherd-review-body-model

Conversation

@bawoodruff

Copy link
Copy Markdown
Contributor

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_COUNT from reviewThreads, but gh pr review --body-file submits one top-level body and cannot attach line comments. THREAD_COUNT was 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 APPROVED body 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_FAILING now excludes it by name via REVIEW_GATE_CHECK — overridable, which #30's consumer repos 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 × 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: 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

Case Old New
#42 — COMMENTED, 0 threads, 4,161-char body Route E → poll ~45 min → "stuck" Route B → fix findings
#39 / #40 / #43 — APPROVED with a body Route A (done) Route A (done) — unchanged
verdict gate FAILURE, all builds green CI_FAILING=1 → "investigate CI" CI_FAILING=0 → routes to B
genuine build failure alongside the gate caught still caught — no regression
verdict gate PENDING pending CI_PENDING=1 — still waits

The 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.md carries 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

@github-actions

github-actions Bot commented Aug 29, 2026 •

Copy link
Copy Markdown
Contributor

Agent QA — reviewer audit score

Defect Technique Result
D1 diff parallel implementations FOUND
D2 falsify doc comments FOUND
D3 what would this test still pass with FOUND
D4 language traps (float equality) FOUND
D5 language traps / semantic (truncation before filter) FOUND
D6 state/failure paths (error after commit + retry) FOUND

SCORE: 6/6 (gate threshold: 4; recorded baseline: 6/6 (100%))

Full review output: workflow run artifact agent-qa-review-output

Planted-defect harness tests/agent-qa/; policy: docs/adr/011-agent-qa-in-ci.md.

@cortexa-llc-reviewer cortexa-llc-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_CHECK being overridable for consumer repos.
  • Reusing $VERDICT_RAW for both VERDICT and REVIEW_BODY in SKILL.md is the right design — one call, one consistent snapshot.
  • The 40-char body threshold for BODY_FINDINGS is 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.

bawoodruff and others added 2 commits August 29, 2026 15:58
…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
@bawoodruff
bawoodruff force-pushed the fix/shepherd-review-body-model branch from 615d63b to 4b084ef Compare August 29, 2026 22:58
@bawoodruff

Copy link
Copy Markdown
Contributor Author

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 MAX_WAIT_ITER before escalating as "reviewer appears stuck". Item 5's exact behavior, left intact in half the stack, under a claim that it wasn't.

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:

State Result
green, no findings, COMMENTED exit — reviewed, not approved
green, no findings, CHANGES_REQUESTED exit
green, body findings, COMMENTED continue → fix
green, threads open, COMMENTED continue → fix
green, no findings, PENDING continue → wait
CI not green, no findings, COMMENTED continue
green, no findings, APPROVED never reaches the guard — the done branch catches it

Minor (double fetch) — fixed. Worth noting the race you identified is the more serious half: a review landing between the two calls leaves VERDICT and REVIEW_BODY describing different reviews, so BODY_FINDINGS is computed from mismatched state. One VERDICT_RAW snapshot now feeds both, matching SKILL.md.

Minor (stale BODY_FINDINGS) — fixed rather than documented as debt. The Done Check re-derives verdict and body from one fresh snapshot.

Minor (CI_PENDING wording) — fixed in both places. You're right that on FAILURE the gate falls out of both counts. The comment now says so and explains why that's deliberate: a check whose only meaning is "the reviewer said no" must not also read as a broken build.

@cortexa-llc-reviewer cortexa-llc-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 both VERDICT and REVIEW_BODY from 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 ACTIONABLE abstraction cleanly unifies thread and body findings across all routing branches; the inline comments explaining why THREAD_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>
@bawoodruff

Copy link
Copy Markdown
Contributor Author

Shepherd round 1 — disposition for review on 4b084ef4

All four findings accepted and fixed in 1db77a2e. No disputes this round.

Critical — Route G can never fire for CHANGES_REQUESTED (pr-shepherd.md:344–345, 413)

Fixed. Confirmed on the merits, and this PR's own CI was the reproduction: gh pr checks 46 on 4b084ef4 reported Claude verdict fail with every other check green, against a CHANGES_REQUESTED verdict.

Empirical check of the two expressions against that exact payload:

$ echo '[{"name":"Claude verdict","state":"FAILURE"},{"name":"Docs site builds","state":"SUCCESS"}]' \
  | jq -r '(length > 0) and ([.[].state] | all(. == "SUCCESS" or . == "SKIPPED" or . == "NEUTRAL" or . == "STALE" or . == "CANCELLED"))'
false        # old — ALL_OK pinned false, Route G guard unreachable

$ ... | jq -r --arg gate "Claude verdict" '(length > 0) and ([.[] | select(.name != $gate) | .state] | all(...))'
true         # new — Route G fires

Regression cases both hold: [] still yields false (the vacuous-true guard survives the rewrite), and Claude verdict=FAILURE alongside Docs site builds=FAILURE still yields false, so a genuine build failure is not masked.

Major — item 4's exclusion absent from the agent's Step 2 (pr-shepherd.md:170–171)

Fixed. FAILURES now carries --arg gate / select(.name != $gate). Verified the gate drops out while a real failure survives: the query over [Claude verdict=FAILURE, Docs site builds=FAILURE, gate=SUCCESS] returns exactly Docs site builds.

REVIEW_GATE_CHECK is defined once in the startup variable block (defaulting to Claude verdict, overridable per #30) and re-defaulted idempotently at each use, since Step 7 can run without Step 2 having executed.

Minor — CHANGELOG parity claim false for item 4 (CHANGELOG.md:33)

Fixed. The claim is now true, and rather than leave it as a bare assertion a future reviewer has to cross-check, the entry spells out both queries the agent was missing and why the ALL_OK one was the load-bearing failure.

Minor — DISMISSED falls through all routes (SKILL.md:~307)

Fixed, and in both files rather than only the skill — the agent's terminal-verdict exit had the identical enumeration. Route G's guard is now the complement of the two handled states (not APPROVED and not PENDING) instead of a COMMENTED/CHANGES_REQUESTED list. APPROVED exits at Route A, PENDING waits at Route E, so every other value — DISMISSED or anything unrecognised — is terminal-with-nothing-to-do and now reports and stops. A catch-all was the alternative; the complement is strictly less to keep in sync and removes the failure mode rather than adding a branch for it.

Validation

No workflow YAML changed, so actionlint was not applicable. The edited shell constructs pass bash -n, and the jq expressions were exercised against both live PR data and synthetic edge cases as shown above. Versions stay consistent — the new notes land under the existing [3.4.0] section; VERSION and plugin.json are untouched at 3.4.0.

@cortexa-llc-reviewer cortexa-llc-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 both VERDICT and REVIEW_BODY from one call, documented with the race-window rationale) is the right design and consistent across both variants.
  • REVIEW_GATE_CHECK is overridable at each use site, matching the consumer-repo requirement from #30.
  • Route G uses the complement of {APPROVED, PENDING} rather than enumerating COMMENTED/CHANGES_REQUESTED, correctly catching DISMISSED and 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.

@bawoodruff
bawoodruff merged commit ce1c3cb into main Aug 29, 2026
10 checks passed
@bawoodruff
bawoodruff deleted the fix/shepherd-review-body-model branch August 29, 2026 23:19
bawoodruff added a commit that referenced this pull request Aug 30, 2026
…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
bawoodruff added a commit that referenced this pull request Aug 31, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant