When a hive agent opens a PR on a repository that has an external review bot
installed — Copilot code review, chatgpt-codex-connector[bot], CodeRabbit —
the bot reviews it a few minutes later and leaves inline threads. On a
repository with "require conversation resolution before merging" the PR cannot
merge until every thread is resolved. Before
#7360 nothing in the hive
noticed: the PR sat open until a human addressed the comments and resolved the
conversations by hand.
The hive now reconciles those threads itself, for every PR it opened, with no per-agent prompt rule. Three pieces, and one config key that turns them on.
A PR is the hive's to follow up on — hive-mediated — when either of two things is true:
- its author is a hive login: the App bot, or
project.ai_authorwhen configured. These are the PRs the PR-request watcher opens; or - its body carries the
— hive:attribution trailer (github.HasAttributionTrailer). These are the PRs a hive agent opened on a person's credentials — a contributor relay, or an operator running agents under their own GitHub auth. GitHub shows the person as the author, so the login rule alone can never recognise them.
The second rule exists because of #7638: the reconciler originally keyed on the author login only, and every relay-run PR was dropped before its threads were even fetched. Those are precisely the PRs external review bots review — Codex skips bot-authored PRs — so the only PRs getting Codex threads were the ones the hive could not follow up on. The trailer is the same rule the task-list sweep already uses to decide an issue is the hive's to close.
Do not fix this by setting project.ai_author to the operator's own
login: that would make every PR they write by hand "hive-authored" too, and
agents would be told to fix and resolve review threads on the operator's own
work. ai_author is for a dedicated bot login.
A relay-run PR's trailer usually has no agent= (the relay does not know a
hive-side agent name), and there is no App-bot audit entry for a PR the hive
did not open itself, so it is listed with an empty agent and reaches the
scanner, exactly as any other unattributed PR does.
On spoofing. The trailer is a line of text; anyone can paste — hive:
into a hand-written PR and have the hive answer that PR's review-bot threads.
This is accepted, as it was for the task-list sweep closing issues, because
the consequence is bounded: the hive will only ever reply in and resolve
threads that a configured review bot opened on that PR — the watcher-side
guard (§3) still refuses a human's thread, and it keys on the thread's first
author, never on the PR's author — which is something the PR's own author
could already do by hand. Nothing about the trailer lets a PR merge, be
approved, or have its human conversations touched.
classification:
review_bots:
logins:
- "chatgpt-codex-connector[bot]"
- "Copilot"
min_priority: P1 # optional: only P0/P1 plus unknown badges
max_attempts_per_thread: 1 # default 1
resolve_after_fix: true # default trueThe key lives in hive-project.yaml next to its sibling monitor's config
(classification.copilot_check), and the Go binary also accepts the identical
block in hive.yaml; when both name a login, hive.yaml wins. A
hive.yaml min_priority (the field the dashboard edits) also overrides the
project file's threshold on its own. It is the one
key the Go side reads from the project file
(config.LoadProjectReviewBots, path HIVE_PROJECT_YAML or
/etc/hive/hive-project.yaml).
- Absent, or empty
logins: the feature is off. The monitor writes an emptyreview-threads.jsonand the watcher denies every thread request. loginsare matched case-insensitively against the first comment's author, ignoring surrounding whitespace and a trailing[bot]suffix. Both REST/web (chatgpt-codex-connector[bot]) and GraphQL (chatgpt-codex-connector) spellings match either configured form. Do not list a human here: it would let agents resolve that person's threads.max_attempts_per_threadis how many times the hive replies in one thread before leaving it for a human. The counter is the thread itself — replies authored by the App bot — so there is no state file to drift.resolve_after_fix: falsemakes the agent reply but leave the thread open.min_priorityoptionally limits routing toP0throughP3(P0 is most urgent).P1includes P0/P1 and excludes P2/P3. When neitherhive.yamlnorhive-project.yamlsets it,review.severity.block_atsupplies the threshold (see review-swarm.md). With no threshold anywhere, or an unrecognised value, every finding is routed. Case and surrounding whitespace in the threshold are ignored. Only Codex's Markdown badge format,![P0 Badge]through![P3 Badge], is recognised today, using the first matching badge in the first comment. Unknown formats (including unbadged Copilot/CodeRabbit findings) always remain included. Filtered threads stay open for humans: they are omitted from the FIX-BEFORE-NEW work list and are not auto-resolved. Each PR inreview-threads.jsonreportsexcluded_by_priority, counting otherwise actionable threads excluded solely by this threshold (not resolved, outdated, human-authored, or attempt-capped threads). The reviewer's jq filter applies the same badge threshold, so it need not answer the lower-priority threads in its review.
The dashboard's Settings → Features → Review Gate card displays the
effective review-bot settings, including defaults (one attempt per thread and
resolve after fix enabled). The governor settings bundle at
GET /api/config/governor exposes them in review_bots — logins,
min_priority (P0–P3, or empty when every finding is routed),
max_attempts_per_thread, resolve_after_fix, and enabled; load_error
reports an unreadable or malformed project file rather than hiding it as an
unconfigured feature. Logins, attempts, and resolve-after-fix are read-only
there: logins stay config-file-only because adding one grants agents
thread-resolution rights.
min_priority is editable from the same card (Minimum bot-finding
priority: All, P0, P1, P2, P3). The owner-only PUT /api/config/review
accepts it as {"review_bots": {"min_priority": "P1"}}; an absent key leaves
it untouched, an empty string clears the dashboard override, all routes
every finding, and any other value is rejected with 400. The value is written
to hive.yaml's classification.review_bots.min_priority and overrides the
project file's threshold even when hive.yaml names no login — the project
file's logins are never copied into hive.yaml. The change applies to the
thread reconciler and the reviewer without a restart.
copilot_check / bin/copilot-comment-checker.sh are untouched: they cover
merged PRs; this covers open ones.
The governor's eval tick (writeReviewThreads, throttled to once per five
minutes) runs Client.CollectReviewThreads over the enumerated actionable
PRs. It keeps only hive-mediated PRs (see Which PRs qualify:
a hive login as author, or the — hive: trailer in the body — the trailer
test is computed from the PR list payload at enumeration time, so it costs no
extra call), skips drafts and anything carrying a hold / do-not-merge /
exempt label (the same exclusions as every other kick input), and for each
remaining PR runs one GraphQL pullRequest.reviewThreads query. A thread is listed when it is
unresolved, not outdated, its first comment is from a configured
bot, and it has fewer than max_attempts_per_thread replies from the
hive. Threads a human opened never appear.
{
"generated_at": "2026-09-17T12:00:00Z",
"enabled": true,
"total_threads": 1,
"prs": [
{
"repo": "org/repo",
"number": 123,
"title": "fix: nil deref in x",
"head_ref": "hive/fix-123",
"agent": "scanner",
"threads": [
{"thread_id": "PRRT_kwDO…", "path": "src/x.go", "line": 42,
"author": "chatgpt-codex-connector[bot]", "body": "x may be nil here",
"comment_id": 2211, "hive_replies": 0}
]
}
]
}agent is the hive agent whose relay request opened the PR, resolved from the
audit trail exactly as ci-failing.json does; empty means unattributed and the
kick builder defaults it to scanner (a PR opened on a person's credentials
is always in this state — see Which PRs qualify). escalated: true marks a PR the
escalation sweep has handed to a human. A PR whose bot threads are all
resolved (or all at the attempt cap) is listed with an empty threads array,
so the file shows the reconciler has nothing left to do there rather than
silently dropping the PR.
Scheduler.addReviewThreadFixFirst is a sibling of the red-CI
addRedPRFixFirst, called at the same post-resolution seam so no kick
template can omit it. For a PR-capable agent it prepends a block listing that
agent's own non-escalated PRs with actionable threads — path, line, and a
bounded excerpt of each finding (redPRFixMaxDetailed PRs,
redPRFixExcerptRunes per excerpt) — and the instructions: check out
head_ref, address each thread, git commit -s, push to the same branch, then
for each thread reply in-thread with one line and (if resolve_after_fix)
resolve it. One pass per kick; a thread already answered is filtered out by
the monitor, and the agent is told to skip it if it still appears.
ReviewRequest gained a thread_id field and a resolve_thread event:
hive-review <number> --repo <owner/repo> --comment --thread <PRRT_id> --body "<one line>"
hive-review <number> --repo <owner/repo> --resolve-thread <PRRT_id>A comment with --thread is an in-thread reply (GraphQL
addPullRequestReviewThreadReply), not a PR-level review; --resolve-thread
runs resolveReviewThread. Both execute with the App token, go through the
same per-agent authorizer and UID forge-resistance as every other review
request, are audited as agent_pr_reviewed with state=thread_replied /
state=thread_resolved and thread=<id>, and write the usual
<name>.result.json.
The guard is in the watcher, not the prompt. Before acting it re-fetches
the thread by id and quarantines the request as .denied (reason in the
result file, nothing touched on the forge) when:
| Condition | Reason recorded |
|---|---|
review_bots.logins is empty |
classification.review_bots is not configured |
| the id resolves to nothing this token can see | thread … not found |
| the thread is on a different PR than the request names | thread … belongs to o/r#N, not the requested PR |
| the thread's first comment is not from a configured bot | … opened by <login>, which is not a configured review bot; human threads are never resolved by the hive |
| the thread is already resolved | thread … is already resolved |
(reply only) the hive already replied max_attempts_per_thread times |
… already has N hive replies (max_attempts_per_thread=N); leaving it for a human |
Malformed requests (resolve_thread without thread_id, thread_id on an
approve) are quarantined as .bad before authorization, like any other
shape error. A transient forge failure on the re-fetch or the mutation keeps
the request for the same exponential-backoff retry and give-up horizon as a
failed review.
The three pieces above act only on hive-mediated PRs. The review swarm reads the same threads on every PR it reviews (#9360): before that, a reviewer could post "Confidence: 5/5 (safe)" minutes after Codex left a correct P1 finding on the same commit, without mentioning it.
When review_bots.logins names any bot, each review kick
(buildReviewBotFindingsInstruction, pkg/review/prompts.go) carries a
gh api graphql read of the PR's reviewThreads, filtered to unresolved
threads whose first comment is from a configured bot. Logins are compared
lower-cased with the [bot] suffix removed, because GraphQL reports an App's
login without it. The reviewer must then:
- answer every returned thread in its review: agree, disagree (reason
plus the refuting
file:line), or cannot verify; - verify each finding against the code at head and classify it with
review_scope, like one of its own; - treat a confirmed, in-scope, severe finding (the bot's P0/P1) as a blocker: the owning perspective may not approve, and the review may not call the PR clean or safe. A refuted or out-of-scope finding is still answered but does not lower the verdict.
The reviewer stays comment-only. It never replies in, reacts to, or resolves a bot thread; that remains the reconciler's job on hive-mediated PRs. With no logins configured, the kick has no bot section.
- A PR with two bot threads gets, within one kick, a push, two in-thread replies from the App bot saying what changed, and two resolved threads.
- If the bot comments again on the fix, the new thread appears once, gets one more attempt, and then stops: an open bot thread with one hive reply explaining what it tried.
- A human's thread on the same PR is never listed, never replied to by this path, and never resolved — including on a PR whose author is a human because a hive agent opened it on their credentials: the guard looks at who opened the thread, not who opened the PR.