Repository navigation
Labels
plugin:ghThe gh pluginThe gh plugin
Description
Why
The shepherd spends CI cycles on pull requests that cannot merge yet, because they have no approval. Seen on a /gh:shepherd run on another repository, 2026-10-19:
- The pull request had auto-merge on, a current head (compare
behind_by0), all checks green, and merge stateBLOCKED. - It had no review at all. The base branch's ruleset has a
pull_requestrule withrequire_code_owner_review: true, and CODEOWNERS names one team for every path. reviewDecisioncame back null or empty, soshepherd-statusand the worker could not see the requirement. The worker rebased it twice (19:55 and 21:22 UTC), at about 19 minutes of CI each.- Every pull request that merged in that run had an APPROVED review.
The shepherd reads only reviewDecision for review state: settle treats REVIEW_REQUIRED and CHANGES_REQUESTED as blocking (BLOCKING_REVIEW_DECISIONS, plugins/gh/src/subcommands/shepherd-pr.ts). A requirement that comes from a ruleset, not from classic branch protection, can leave reviewDecision empty.
The rule to hold: a pull request with no approval is not worth working on yet. It stays out of the queue. The person can ask for every auto-merge pull request.
Scope
- Default: approved pull requests only.
shepherd-statusmakes a pull request a target only when it has an approval. That means at least one APPROVED review that is still current. When the base branch's ruleset asks for more, the approvals must meet that rule too.- The rule comes from the
pull_requestrule inrepos/{owner}/{repo}/rules/branches/{branch}:required_approving_review_countandrequire_code_owner_review.requiredContextsalready reads this endpoint (lib/gh.ts:2808). - If a code owner approval cannot be proven cheaply, "at least one APPROVED review" is the rule.
- The rule comes from the
--allon/gh:shepherdandshepherd-statusincludes every open, non-draft auto-merge pull request, as today. It works together with the label argument and--dry-run. Update the skill'sargument-hint.- A pull request left out for lack of an approval is named in the report, with the reason, so the person knows it needs a review.
--dry-runshows it too. - The worker also checks this in
state, because a review can be dismissed during a run. Without--all, a pull request that lost its approval is skipped before anyrebaseorwait. - Tests for each script change, with the capture or divergence record they need, and
gh-docs.test.tspins for the skill and agent rules. - Out of scope: requesting a review, or any write to a review.
Acceptance
- Without
--all, a pull request with no approval gets norebaseand nowait. The report says it needs an approval. - Without
--all, a pull request whose approvals do not meet the branch's ruleset is left out the same way. - With
--all, every open, non-draft auto-merge pull request is a target, as before this change. - An approved pull request is shepherded as today.
References
Metadata
Metadata
Assignees
Labels
plugin:ghThe gh pluginThe gh plugin
Activity
[-]fix(gh): shepherd skips a PR that lacks a required approval[/-][+]fix(gh): shepherd works only on approved PRs, unless asked for all[/+]brianespinosa commentedon Oct 19, 2026
Sequencing (sweep)
Unblocked set: #465 only. No blockers, nothing blocked. No open pull request touches these files. #463, #464 and #466 touch the same plugin but are in no round.
The defect holds on
main(b01a286):statusDocument(plugins/gh/src/shepherd.ts, after:480) makes a target from two tests only: not a draft, and auto-merge on. No review state is read.PULL_REQUEST_FIELDS(lib/gh.ts:956) carriesreviewDecision, and no review list.Hidden dependencies, checked against the code:
gh pr list --jsonofferslatestReviews(one review per author, with its state). Adding it toPULL_REQUEST_FIELDSchanges whatshepherd-statusand everyshepherd-praction read. A per-PR GraphQL read is the other way.requiredContexts(lib/gh.ts:2807) readsrules/branches/{branch}, andrequiredContextsOf(:1937) keeps onlyrequired_status_checks. Thepull_requestrule (required_approving_review_count,require_code_owner_review) needs a parser of its own, or a wider result. The read is per base branch.baseRefNameis in the field list since fix(gh): shepherd rebases a behind PR before it waits on CI #462.~/Code/@bje/CLAUDE.md, Enterprise rulesets). Under "approved only" as the default, a solo-maintained repo (Dependabot PRs, no reviewer) has no targets without--all. This is a question for the question phase: an approval always, or only when the branch rules ask for one.shepherd-statusparses its own argv. It refuses more than two arguments as "one label per run" (shepherd-status.ts:124).--allneeds a flag parse that keeps that refusal for a second label.tests/plugins/gh/fixtures/shepherd-status-output.jsonand the shepherd capture rules (README-shepherd.md). A new key for the left-out pull requests is a capture edit or a divergence row.lib/gh.tschanges go throughnode scripts/sync-lib.tsto the three copies. A newGhClientmethod needsharness/gh.tssupport.File map (one stream):
lib/gh.ts(+ 3 synced copies)PULL_REQUEST_FIELDS; the rules readpull_requestruleplugins/gh/src/shepherd.tsstatusDocument,summary,PullSummaryplugins/gh/src/subcommands/shepherd-status.tsHELP--all; base-branch rulesplugins/gh/src/subcommands/shepherd-pr.tsstateplugins/gh/skills/shepherd/SKILL.mdargument-hint, steps 2, 3 and 5--all; report the left-out PRsplugins/gh/agents/shepherd-prs.md--allplugins/gh/README.md:152),shepherd-statusreference (:237)docs/flows/gh/shepherd/_skill-flow.md,shepherd-status-flow.md,shepherd-pr-flow.mdharness/gh.ts,harness/README.mdtests/lib/gh.test.ts,tests/plugins/gh/shepherd.test.ts,tests/plugins/gh/subcommands/shepherd-status.test.ts,shepherd-pr.actions.test.ts,.test-support.tstests/plugins/gh/fixtures/shepherd-status-output.json,shepherd-pr-output.json,divergences-shepherd.json,README-shepherd.mdtests/repo/gh-docs.test.tsplugins/gh/.claude-plugin/plugin.jsonComments only:
plugins/workstreams/src/subcommands/pr-state.ts(it also callsrequiredContexts), if the rules read changes shape.Shape: one pull request. One issue. The skill's
--alland the script's filter cannot merge apart.Size:
Unlocks: nothing in the tracker.
Recorded by
workstreams:sweep2026-10-19.brianespinosa commentedon Oct 19, 2026
Rulings (question phase)
Asked
The branch rules decide. Without
--all, a pull request is left out only when its base branch requires an approval and the pull request does not have one. "Requires" means any of these:pull_requestrule hasrequired_approving_review_countabove 0, orrequire_code_owner_reviewtrue;reviewDecisionisREVIEW_REQUIREDorCHANGES_REQUESTED.A repository that requires no review is shepherded as today, with no
--all. This replaces the issue's line "at least one APPROVED review" as the default.Ruled (each one is cheap to reverse)
When an approval is met.
required_approving_review_count.reviewDecisionAPPROVEDis met.CHANGES_REQUESTED, fromreviewDecisionor from a current review, is never met.For: one read per base branch, no CODEOWNERS parser. Against: an approval from a person who is not a code owner counts. GitHub then still blocks the merge, and the worker skips it on
BLOCKED, as today.Reviews come from
latestReviewsinPULL_REQUEST_FIELDS(lib/gh.ts:956), one entry per author. For: no extra call per pull request. Against: every shepherd read carries the field.The rule comes from a new
GhClientmethod. It readsrules/branches/{branch}and returns thepull_requestrule's two parameters.requiredContextsand its callerpr-statestay unchanged.shepherd-statusreads it once for each distinct base branch. For: no change to a contractworkstreamsuses. Against: a second parser over one endpoint.A rules read that fails does not leave a pull request out. It falls back to
reviewDecisionalone, and stderr says why. A plan-limit answer means no ruleset, asrequiredContextsalready treats it (lib/gh.ts:2810). For: a failed read never empties the queue. Against: a PR that needs an approval can get through on a failed read. That is today's behaviour, no worse.The output.
shepherd-statusgains a key,needs_approval: the pull requests left out, each withnumber,title,authorand the reason.prinshepherd-prgainsapproval:met,missingornot_required, withnullwhen unread.--allis the flag name, on/gh:shepherdand onshepherd-status. It works with one label and with--dry-run. Any other flag is refused, and a second label is still refused.The worker gets the flag. SKILL.md step 3 tells the agent whether
--allis on. Without it,statewithapproval: missingis a skip ("needs an approval") before anyrebaseorwait.Shape: one layer, one pull request. It bumps the
ghplugin from 0.12.0 to 0.13.0.Recorded by
workstreams:question2026-10-19.brianespinosa commentedon Oct 19, 2026
Round 24 plan, approved 2026-10-19T21:59Z.
Issues: #465. Sweep: issuecomment-6278294598. Rulings: issuecomment-6278313158.
Layer 1: #465
wktr-465-shepherd-approved, basemainopus. Review model:opus;code-reviewer,silent-failure-hunter,pr-test-analyzer,comment-analyzer,type-design-analyzerplugins/gh/.claude-plugin/plugin.json(0.12.0 to 0.13.0)lib/gh.ts(latestReviewsinPULL_REQUEST_FIELDS; a new rules method for thepull_requestrule) and its synced copies inplugins/{gh,audit,workstreams}/src/lib/gh.tsplugins/gh/src/shepherd.ts(the approval verdict,PullSummary.approval,needs_approval)plugins/gh/src/subcommands/shepherd-status.ts(--all, one rules read for each base, the fallback,HELP)plugins/gh/src/subcommands/shepherd-pr.ts(statereportsapproval,HELP)plugins/gh/skills/shepherd/SKILL.md,plugins/gh/agents/shepherd-prs.md,plugins/gh/README.mddocs/flows/gh/shepherd/_skill-flow.md,shepherd-status-flow.md,shepherd-pr-flow.mdharness/gh.ts,harness/README.md,tests/harness/gh.test.tstests/lib/gh.test.ts,tests/plugins/gh/shepherd.test.ts,tests/plugins/gh/subcommands/shepherd-status.test.ts,tests/plugins/gh/subcommands/shepherd-pr.*.test.ts,shepherd-pr.test-support.ts,tests/repo/gh-docs.test.tstests/plugins/gh/fixtures/shepherd-status-output.json,shepherd-pr-output.json,README-shepherd.mdplugins/gh/.claude-plugin/plugin.jsonDone
pr-stategivesdonefor the layer.Review shape
The review state machine in
skills/review/SKILL.md, with the model and reviewers above.Estimate
Recorded by
workstreams:question2026-10-19.brianespinosa commentedon Oct 19, 2026
Mid-round ruling 10 (Round 24, answered by the person):
Field evidence: sm-incubator/beta-recognition#2816 was current, green, approved and on auto-merge, and still
BLOCKED. The base branch'spull_requestrule hadrequired_review_thread_resolution: true, and the pull request had 5 unresolved review threads.reviewDecisionwas empty. It merged within 20 s of the threads being resolved.Chosen: #465's check covers every
pull_requestrule parameter that a green pull request can still fail. That is approvals, code owner review, review thread resolution, and approval of the last push.required_review_thread_resolutiontrue: a pull request with an unresolved review thread is left out ("has unresolved review threads"). Count the threads with the GraphQL read thatviewPullRequestDetailalready does, or one like it, and only when the rule asks for it.require_last_push_approvaltrue: the approval must be on the current head.latestReviewscarries each review's commit. An approval on an older commit ismissing.not_ready, notneeds_approval. Each entry carries its reason or reasons.prinshepherd-prcarriesapprovalandthreads(resolved,unresolved,not_required, ornullwhen unread). Without--all, the agent skips a pull request that has either blocker, with that reason.For: one rules read, one exclusion list, one review. Against: a larger pull request, and one GraphQL read for each pull request whose rule asks for thread resolution.
Recorded by
workstreams:start2026-10-19.brianespinosa commentedon Oct 19, 2026
Mid-round ruling 11 and 12 (Round 24, cheap to reverse):
PULL_REQUEST_FIELDScarriesreviews, notlatestReviews. This revises ruling 3. gh'slatestReviewsJSON givescommit.oidas empty on every entry (chore(deps): bump the codeql-actions group with 3 updates cli/cli#14751, #14750, gh 2.102.0). The last-push rule (ruling 10) needs each review's commit. For: the real oid, with no extra call. Against: every review, with its body, rides on each shepherd read. To reverse: a per-PR GraphQL read for only the PRs whose rule asks for approval of the last push.reviewDecisionREVIEW_REQUIREDgivesmissing, even when the count of approvals is met. This narrows ruling 2. For: it is GitHub's own verdict, and without it, "REVIEW_REQUIRED and no rule" would count 0 of 0 as met. Against: a stalereviewDecisioncan hold back a PR that the ruleset would pass. To reverse: let the count win when a ruleset was read.Also passed to review round 1: the build return names a gap in the agent prose. The
changedandok:falsepaths re-readstateand go back to step 2, so the step 1 approval and threads check is skipped on a re-read.Recorded by
workstreams:start2026-10-19.brianespinosa commentedon Oct 19, 2026
Mid-round ruling 13 (Round 24, cheap to reverse):
Review round 1 found that rulings 2 and 10 conflict in one case:
reviewDecisionisAPPROVED, the ruleset asks for approval of the last push, and the only approval is on an older commit.Chosen: ruling 2 holds, and a decision of
APPROVEDis met. #468 already behaves this way, with a doc comment and a pin. For: GitHub computed that verdict, and it is the mirror of ruling 12, which letsREVIEW_REQUIREDwin. A ruleset-only requirement usually leavesreviewDecisionempty, so the count path decides there. Against: if GitHub reportsAPPROVEDand the ruleset still blocks on the last push, the shepherd spends CI on the pull request, andsettlethen reports itblocked. To reverse: let the count path decide whenever a ruleset was read withlastPushtrue. That is one branch inapprovalOfand one test.Recorded by
workstreams:start2026-10-19.brianespinosa commentedon Oct 19, 2026
Round 24 end, 2026-10-19.
1. State
doneatfba0281. CI is green, and no thread is open. It is 0 commits behindmain, and a merge withmainis clean. It is still a draft.2. Decisions the model made
The person confirmed all of these.
reviews, notlatestReviews.REVIEW_REQUIREDgivesmissing.APPROVEDis met under a last-push rule.--alltoo, and only when the rule asks for it.stateread follows a rebase.settlecatches an approval that the rebase made stale.3. Defects from before the round: not opened. The person declined the issue for the unpaginated
rules/branchesread.4. Scope changes for other issues: none.
5. Other items: the person marks #468 ready and merges it. Then
/gh:syncremoves the worktree and the branch.6. Cost
Facts for the record
BLOCKEDonrequired_review_thread_resolution.reviewsshape was checked against COMMENTED reviews on fix(gh): shepherd rebases a behind PR before it waits on CI #462 and fix(gh): prove stacked, rebased and moved merges in sync #451.not_readypath has no live evidence yet, because this repository has no open auto-merge pull request.Result
Recorded by
workstreams:start2026-10-19.feat(shepherd): read the merge queue and report it in shepherd-status (…