Repository navigation
Description
Summary
#365 taught the verifier headline to name the worst blocker rather than only count it. Its own commit message states why:
That leading line also names the worst blocker instead of only counting it — a count reads the same whether the agent is missing a docstring or can move funds with no enforced control.
That leading line is _report_primary_headline, and _verifier_headline reaches it from two branches only — the newly-introduced-manifest branch and the self-approval branch. Every other run returns report.agent_summary.headline directly:
# cli/verify/orchestrator.py::_verifier_headline
if manifest_introduced and not pure_adoption_review and report is not None:
...
return _compose_with_reserved_suffix(_report_lead(report, suffix), suffix) # <- named
note = _self_approval_note(...)
if note is not None:
if report is not None and _blockers_outrank_governance(report.release_decision):
return _compose_with_reserved_suffix(_report_lead(report, note), note) # <- named
return _compose_with_reserved_suffix(..., note)
if report is not None and report.agent_summary is not None:
return _lead(_one_clause(report.agent_summary.headline)) # <- countedSo the case #365 was written for — a PR that blocks release on critical findings — gets the named cause only if it also touches the trust root or introduces a manifest. An ordinary blocked PR, which is the common one, still gets the count.
Reproduction
Two commits in an MCP repository whose manifest declares delete_repository; the head commit adds one unrelated tool and touches nothing else. verify --base HEAD~1, CLI at 16261d99.
The blockers are there, both critical, and the picker finds them:
blockers: [('critical', 'SHIP-POLICY-APPROVAL-MISSING',
'delete_repository lacks a declared approval policy'),
('critical', 'SHIP-ACTION-DESTRUCTIVE-ROLLBACK-MISSING',
'delete_repository has destructive capability without required controls')]
_worst_blocker(...).title
-> 'delete_repository has destructive capability without required controls'
_report_primary_headline(report)
-> '2 active finding(s) block release; 3 review item(s) accepted as debt.
Most severe: delete_repository has destructive capability without required controls.'
And the line that actually ships:
verifier.headline
-> '2 active finding(s) block release; 3 review item(s) accepted as debt. ...'
"Most severe" in verifier.headline -> False
_report_primary_headline computed exactly the right sentence and nothing published it. Two critical blockers, and the one line that reaches the PR comment, control.reason, control.next_action.why and any triage list names neither.
Why this is worth fixing rather than leaving
The ranking half of #365 shipped and works: when a governance notice competes with a critical blocker, the blocker leads. The naming half shipped only on the routes where that competition exists. A reviewer's exposure to the improvement is therefore inversely related to how ordinary their PR is — the repository that has already adopted, changes no policy, and simply blocks on a destructive tool is the one that still reads a count.
It also makes the surrounding line harder to interpret. On #433's reproduction the headline now carries Not fully analysed: find_duplicate [github_mcp] — … next to 14 active finding(s) block release, and a reader has no way to tell whether the 14 are about the named subject or about something else entirely. Most severe: is exactly the clause that would answer that.
Suggested direction
Route the plain branch through _report_primary_headline as well — it already falls back to agent_summary.headline when there is no blocker, so the no-blocker case is unchanged by construction:
if report is not None and report.agent_summary is not None:
return _lead(_report_primary_headline(report))Two things to settle deliberately rather than by accident:
- Budget.
_leadfits its context with_fit_sentencesagainstMAX_ENVELOPE_PROSE_BYTES - len(primary), andprimarygrows by up to_HEADLINE_TITLE_MAX_CHARS(120) here. On the plain branch there is no reserved suffix, so nothing is at risk of being cut — but the evidence-gap context will yield more often. That is the documented priority order (verdict, then cause, then context), so it is probably right; it should be stated rather than discovered. - Blast radius. Every blocked run's
verifier.headline,control.reasonandcontrol.next_action.whygains a clause. It is prose, not a documented identifier, but it is worth a## Migration Noteand a check of the PR-comment goldens.
Acceptance
- The reproduction above puts
Most severe: delete_repository has destructive capability without required controls.inverifier.headline,control.reasonandcontrol.next_action.why. - A blocked run that touches no trust root and introduces no manifest names its worst blocker — the case with no existing test.
- A run with no blockers gains no clause (
_report_primary_headlinealready returns the summary unchanged; the point is to pin it). - The picker stays deterministic across the branches: severity, then check id, then title, same row on the plain path as on the governance paths.
Found 2026-08-26 while reviewing #434 (#433). Noted there as out of scope: it is a severity-attribution question rather than a naming one, and it moves every blocked headline.
Activity