The review job can fail AFTER posting its review — the CLI exiting
silently, a cleanup step dying — and both fallback sites then announce
that review as one that could not be posted, retry instruction attached.
Measured on PR #9342: the review posted at 11:56:34Z, review-pr failed
at 12:00:53Z ("Qwen review completed but produced no output"), and the
comment landed at 12:01:00Z saying the pipeline "failed before a review
could be posted. … retry with @qwen-code /review" — a fresh ~3-hour
review, asked for beside the review that had just landed. The autofix
takeover loop reads the same feed a human does.
Both sites now check, before composing a body, whether a review this run
posted is already on the PR. The check is scoped three ways so a stale
review can never buy silence on a genuinely dead pipeline: the bot's own
account, the head this run reviewed, and a submission at or after this
run started. Where the proof is unavailable — no start time, no head, a
failed listing — the guard declines to fire and the comment posts, the
same call the head-moved guard already makes.
The job-level step now reads state and headRefOid in one `gh pr view`
(the in-job step already did), which is where its head value comes from.
Tests run the steps' real bash over review fixtures, because the guard
IS a filter: silence when this run posted the review, and posting for
each near-miss on its own — an earlier run's review at the same head,
another account's, one of a different head, a PENDING one, none at all,
an unavailable start time, and a failed reviews listing. One existing
assertion tightened: "no `gh run view`" was the proxy for "no head
comparison on comment runs", and the new guard asks that same command
for startedAt on every event, so it now pins the head lookups
themselves. The stub's state,headRefOid branch learned the pr_closed
scenario its state-only sibling already knew.