On 2026-10-21 our agent pipeline sent a coding agent back to "fix" four pull requests that had nothing wrong with them. Each PR was opened as a draft. The automated reviewer on that repo skips drafts, so it posted no score. The pipeline treated "no score" the same as "a score below 5" and told the agent to bring the PR up to 5 out of 5. Nothing was there to fix. The rule we took from it: no review yet means wait, not iterate. More generally, automation has to tell an absent result apart from a negative one.
Key facts
- GitHub says draft pull requests cannot be merged and code owners are not automatically asked to review them.
- Our reviewer skipped drafts, so a draft PR never got a score.
- The pipeline mapped "no score" to "not 5/5" and spent a worker pass on it, four times in one day.
- Fix: if there is no score, wait; if the PR is a draft, mark it ready first.
- The habit: every check has at least three states, passed, failed and not there yet.
What happened?
The setup is a pipeline that takes a task, has a coding agent build it on a branch, and opens a pull request in a client's monorepo. An automated reviewer reads each PR and posts a confidence score out of 5. The pipeline merges at 5/5 with the required checks green. Below 5, it passes the reviewer's findings back to the agent for another round, up to six rounds.
Two choices that were each reasonable met. The agent opens PRs with gh pr create --draft, so nobody merges half-done work. The reviewer on that repo skips draft PRs, so it does not spend effort on work in progress. Together they mean a draft is never scored, and the pipeline only marked a PR ready once it was at 5/5.
The log for the day, times local:
| PR | What the pipeline did | What was really true |
|---|---|---|
| #229, 20:26 | Unscored, agent asked to iterate, pass 1 of 6 | Draft, never reviewed; marked ready by hand, merged 20:28 |
| #230, 21:28 | Unscored, agent asked to iterate | Same; marked ready by hand |
| #231, 22:24 | Unscored, pass 1 of 6 | Marked ready by hand, then reviewed 3/5, then 4/5, merged 22:36 |
| #232, 22:37 | Unscored, pass 1 of 6 | Same pattern, the fourth time that day |
Each false round costs a worker turn: the agent reads the prompt, finds no findings, polls the PR for a score that will not come, and waits out the round. It also costs a person, who has to notice and press "Ready for review" by hand. #231 shows the real loop working once the reviewer could see the PR: two genuine rounds, then a merge.
What was the bug?
The function that reads the review returned the score, or nothing when there was no score. The loop only asked one question:
# before
score = review_score(pr) # 0..5, or None when there is no review
if score == 5:
merge(pr)
else:
ask_agent_to_iterate(pr, score) # None lands here: "scored not yet/5"
# after
score = review_score(pr)
if score is None:
if is_draft(pr):
mark_ready(pr) # the reviewer skips drafts
wait_for_review(pr) # no review yet means wait
elif score == 5:
merge(pr)
else:
ask_agent_to_iterate(pr, score)
The prompt even said so: "scored not yet/5". The text knew the review was absent; the branch did not. There was a quieter version of the same bug too. A review written for an older commit is also treated as no score for the current one, which is right, but it is a third meaning of "nothing" that the loop must not mistake for a failure.
As of this post the rule is agreed and written down; the change to the pipeline has not shipped yet. Until it does, PRs are marked ready by hand.
Why is "absent" so easy to confuse with "negative"?
Because most code collapses it. A function returns a number or None, and the next line compares against the good value. Anything that is not good falls into the else branch, and the else branch is where the retry lives. It reads well and it is wrong whenever "not good" has more than one cause.
The same shape turns up everywhere in automation:
- A check run that never started. A required check that was filtered out by a path rule is not a failing check. Treat "missing" as its own state.
- A workflow that did not trigger. GitHub's docs say a
pull_requestworkflow runs by default only for theopened,synchronizeandreopenedactivity types. A workflow that should run when a draft becomes ready needsready_for_reviewin its types. - An empty API result. Zero rows can mean nothing matched, or the query hit the wrong account. Log which.
- A timeout. A test that timed out did not fail its assertion. Retrying a timeout is often right; retrying an assertion failure rarely is.
What is the rule?
Give every check three states at least, and handle each on purpose:
- Passed: move on.
- Failed: act on the reason, with the reason in hand.
- Not there yet: find out why (draft, stale, filtered, still running), fix the cause if it is yours, and wait. Do not spend a retry.
Then add a limit to the waiting, so "not there yet" cannot hang forever, and make that limit raise a flag for a person instead of a round for the agent. An agent asked to fix a problem it cannot see will find something to change; that is worse than doing nothing.
Common questions
Can a draft pull request be merged?
No. GitHub's docs say draft pull requests cannot be merged until they are marked ready for review.
Does marking a PR ready request reviews?
GitHub's docs say marking a pull request as ready for review requests reviews from any code owners. Bots and workflows depend on how they are set up.
Should an agent open PRs as drafts at all?
It is a fine default when people review by hand. When an automated reviewer gates the merge, either open the PR ready or have the pipeline mark it ready before it waits for a score.
Melta is free; join the waitlist.