A review evidence bundle is one self-contained JSON document per pull request head commit. It records what Hive reviewed, under which policy, what the reviewers found, what CI reported, who acted on the PR, and how it was merged. A bundle is evidence, not certification: it lets your own control owners decide whether Hive's automated review satisfies your change-management policy. See SOC 2 control mapping for how to use it, and general technical review for what Hive does and does not claim.
Status. Live review fields are populated today (prospective
control_windowassignment is not yet enabled; see Control arm): the review relay writes bundles (verdicts, posted reviews and the policy snapshot), the sentinel sweep adds its findings, and every merge path adds the CI check-run summary, human actions and the merge event and posts the evidence pointer comment (see Where it lives). Bundles are hashed, optionally signed (pkg/evidence), and can be read throughGET /api/review/evidence, the dashboard Evidence action andhivectl review evidence(see Downloading). Tracked in epic #11058.
- One bundle per
owner/repo#number@headSHA(itsid). - Append-only per head SHA. A new push does not edit the existing bundle: it starts a new bundle for the new head whose
previous_bundle_idpoints at the old one, so the chain of heads for a PR stays traceable. - Carries a content
hashand, when a signing key is configured, a detached Ed25519signature, so it can be checked offline.
The machine-readable schema is pkg/evidence/schema/v1.json; the Go types live in pkg/evidence. Timestamps are RFC 3339.
| Field | Meaning |
|---|---|
schema_version |
Always v1 for this schema. |
id |
owner/repo#number@headSHA. |
repo, number |
The repository (owner/name) and PR number. |
author |
login and kind (human, agent or bot). An agent author also carries agent, backend and model: the launch metadata the hive recorded when that agent opened the PR (the same values as the PR's attribution trailer); a field the hive did not know is omitted (#12065). |
base_sha, head_sha |
The commits the PR compares. The bundle is about head_sha. |
previous_bundle_id |
The bundle for the previous head of this PR, if any. |
policy |
Snapshot of the policy in force (below). |
verdicts |
Every reviewer verdict (below). |
posted_reviews |
Reviews Hive posted on the PR: url, head, at. |
ci |
CI for the head: status (the merge gate's verdict, success, failure or pending; absent on bundles written before it was captured), checks[] (name, conclusion, url) and captured_at. |
sentinel |
Why the sentinel sweep flagged the PR, if it did: rule, summary, paths. |
human_actions |
Approvals, label changes, hold lifts: actor, kind, detail, at. |
merge |
Present once merged: actor, method, at, sha, and opened_at (when the PR was opened on GitHub, so at − opened_at is its time to merge; absent on bundles recorded before it was captured — such a merge is left out of time-to-merge, never counted as zero). Absent before merge. |
revert_of, followup_of |
Bundle ids of earlier merged PRs this PR reverts or follows up with a fix (see Change-failure links). |
reverted_by, followed_up_by |
Bundle ids of later PRs that reverted or followed up this one. |
issue_closures |
Confirmed closing-keyword issue closures captured at hive merge: issue repo, issue, closure at, and clarity_gate (marker present in the issue body or comments). Missing closure records mean unknown clarity status, not ungated. |
issue_reopens |
Issues this PR closed that a person reopened (legacy bundles capture only within 14 days of the merge): issue, repo (only when it differs from the PR's), at, source (claim-ledger). A change-failure signal for the Outcome card (#13241). |
shadow |
Optional. What shadow-mode gates would have done, recorded only: review_gate (would_have_held, plus lane — self-merge, trusted-bot or trusted-author when the self-merge review gate recorded it on that sweep lane, absent for the merge-eligible lane) and intent (would_have_blocked), each with reason, tier, confidence and evaluated_at. Absent unless a shadow gate ran; it never affects a merge (#12103). |
control_window |
Optional prospective dispatch assignment at PR open time: on or off. Absent means unknown/unassigned, not on. Reporting foundation only; runtime collection is not yet enabled. See Control arm. |
backfilled, backfill |
Optional. Present only on bundles reconstructed from forge history by hivectl evidence backfill rather than recorded live: backfilled: true and a backfill block with source, at, state (open, closed, merged), closed_at, author_kind_rule and the pre-registration exclusion flags (placeholder_hive, hive_on_hive, under_min_merged). Such a bundle has no verdicts, ci, sentinel or shadow; see Backfilled bundles (#12371). |
outcome |
Optional. What the forge said became of a PR this hive reviewed but did not merge, stamped by the governor's review-outcome ledger (see Where outcomes come from): source (review-outcome-ledger), state (merged or closed), at, and merged_by / merged_at or closed_at. A merged outcome comes with a merge block whose actor is the merger GitHub reported and whose method is unrecorded. |
created_at, updated_at |
When the bundle was first written and last completed. |
signed |
true only when a valid signing key produced signature. |
hash |
Hex SHA-256 of the canonical encoding (see below). |
signature |
Base64 Ed25519 signature over the hash string. Absent when signed is false. |
acmm_level, perspectives, require_approval, min_priority, human_merge_paths, and sentinel_config_hash (a hash of the sentinel configuration, so a later config change is detectable without copying the config).
perspective, model, backend, verdict, confidence (0 to 1), optional confidence_raw and confidence_derived, recorded_at, and findings[] of path, line, severity, summary.
- Marshal the bundle, drop the
hashandsignaturefields, then re-encode with object keys sorted lexically, no insignificant whitespace, and no HTML escaping. This is the canonical JSON. Omitted optional fields stay omitted; do not addnullor empty arrays. hashis the lowercase hex SHA-256 of those bytes. Thesignedflag is inside the hashed content.signatureis the base64 Ed25519 signature of the ASCIIhashstring.
Any change to any covered field changes the hash and fails verification.
Bundles are written under the hive data directory, not /var/run, so they survive restarts:
/data/evidence/<owner>/<repo>/<number>/<head_sha>.json
The review relay writes them whenever it records a validated verdict (with or without posting a comment) or posts a review:
- The first write for a head fetches the PR once for
authorandbase_sha, snapshotspolicy(ACMM level,review.require_approval, the repo'sauto_merge.human_merge_paths, the perspective set, and a hash of thesentinelblock), and setsprevious_bundle_idto the newest bundle for an earlier head of the same PR. - Later writes append to
verdictsandposted_reviews. A verdict identical to one already recorded (ignoringrecorded_at), or a review URL already listed, is not added again, and a pass that adds nothing leaves the file untouched, so re-running the relay never duplicates entries. A changed verdict from the same perspective is kept beside the earlier one. - Every write recomputes
hash, and signs whenevidence.signing_key_fileis set. Files are written then renamed, so a reader never sees a partial bundle. An existing bundle that cannot be parsed is left untouched rather than replaced. - When the sentinel sweep flags a PR, its findings (
rule,summary, sortedpaths) are added tosentinelin the bundle for the flagged head. If the relay has not written that head yet, the sweep starts the bundle from the PR it already listed. A finding already recorded is not added again. - Each time Hive evaluates a head's merge eligibility, it stores the CI gate's verdict as
ci.statuswith the latest check runs andcaptured_at. If review has not started the head's bundle yet, the CI snapshot starts it from the PR metadata already fetched for the eligibility check. The bundle is rewritten only when the verdict or a check name or conclusion changed (check URLs alone do not count), so an unchanged verdict does not churn the hash; a merged bundle is never touched. When the PR merges, before the merge event is sealed in, Hive refreshescifrom GitHub, keeping the eligibility-timestatus: the latest check run per check name for the bundle's head, with its conclusion (or its status, such asin_progress, if it had not finished) and URL. It also captureshuman_actionsfrom the PR's reviews and issue events:approval(an approving review of this head, or any approval since the bundle was created;detailis the review URL),label_added,label_removed, andhold_lift(a hold label removed);detailis the label name. Label changes count only from the bundle'screated_at. Bot and App accounts, including Hive's own, are left out; their writes are in the hive audit log. Each capture is best-effort: if GitHub cannot be read, a warning is logged, that field stays empty, and the merge is still recorded. A replayed merge changes nothing. - When the merged PR's title or body references an earlier PR of the same repo, the two bundles are linked; see Change-failure links.
- A finding reported against the PR as a whole, with no file, is recorded with
path(pr). A verdict'sconfidenceis the reviewer's self-report snapped to the nearest rubric bin (0.55 / 0.65 / 0.75 / 0.85 / 0.95), with midpoint ties rounded up and the original number retained asconfidence_raw. Reports omitting confidence retain the historical aggregate score divided by 5. Neither calibration score changes the aggregate 0–5 merge gate. evidence.enabled: falsestops new writes. Failures are logged and never fail or retry the review itself.
Nothing prunes bundles automatically; they stay until an operator removes them. Reads follow the same retention contract as /api/runs/audit: a bundle Hive can prove existed (its PR directory is still there, or a newer bundle's previous_bundle_id names it) is reported with an explicit expired marker, never as silent absence.
The self-report prompt requires a bin for each perspective independently of its verdict:
| Bin | Rubric |
|---|---|
| 0.95 | Claim verified directly in the diff and tests |
| 0.85 | Verified in the diff; tests not checked or absent |
| 0.75 | Inferred from context, not directly verified |
| 0.65 | Plausible but uncertain |
| 0.55 | Mostly a guess / outside this perspective’s expertise |
review.DerivedConfidence is an evidence-strength heuristic, frozen when a new verdict is recorded. All weights and thresholds are named constants in pkg/review/calibration_confidence.go:
- Start at 0.55; add up to 0.10 for the fraction of findings with a real path and positive line. No findings adds nothing.
- Add 0.10 when the pinned base/head comparison touches conventional test paths. This says tests changed, not that they ran.
- Diff size (added + deleted lines): add 0.10 for ≤100, 0.05 for 101–500, or subtract 0.05 for >500. Failed fetches and comparisons reaching GitHub’s 300-file cap contribute neither size nor test signals.
- Add 0.05 for an already-recorded successful CI snapshot, subtract 0.05 for failure; pending or unknown contributes nothing. No future CI or merge outcome is read back into the score.
- Add
0.05 × (2 × agreement fraction − 1)across the latest verdict from each other perspective already available for the same repo/PR/head, including the current combined batch. No peers contributes nothing; repeated reports from one perspective count once. - Clamp the result to [0.55, 0.95]. The card groups the continuous score into the nearest rubric bin.
Retries preserve the original score. These optional schema-v1 fields leave old bundles’ canonical hashes unchanged when absent. This score measures observable support, not a calibrated probability, and does not feed enforce/advisory decisions.
The Evidence card and /api/evidence/summary (issue_reopen_rates_30d)
report distinct issues closed by hive-merged PRs in the trailing 30 days,
partitioned by the <!-- hive:clarity-gate --> marker captured in the issue
body or question comments at closure. This is a fixed rolling window by
issue closure time, independent of the selected PR-open-time window and
author filter; the repository selector applies to the issue repository,
including cross-repository closing links.
At merge, explicit closing-keyword references are fetched and recorded only when the issue is closed and GitHub's closure timestamp is within one minute after the PR merge. Issue/comment fetch failures, older bundles without closure capture, manual closures, and merges not recorded by this hive are unknown and excluded, not treated as an ungated control arm. Collection uses the existing bounded post-merge evidence context and is report-only.
/metrics exports these gauges, each labelled
hive_id, repo, clarity_gate="true|false":
| Series | Meaning |
|---|---|
hive_issue_closures_30d |
Distinct issues in the closure cohort (denominator). |
hive_issue_reopens_30d |
Those issues subsequently observed reopened by webhook or polling (numerator). |
hive_issue_reopen_rate_30d |
Numerator / denominator, in [0, 1]. Omitted for an empty cohort (API rate: null, UI —), never a misleading zero. |
Repeated bundle heads and repeated reopen observations do not inflate counts. An issue closed more than once uses its latest captured closure in the window; only a reopen after that closure counts. Both clarity cohorts are emitted for repositories with captured closures, including zero counts. Historical status is not reconstructed from today's marker. Recent closures have less time to reopen, and an unobserved reopen is missed: this is a comparison, not proof that the clarity gate caused an improvement. The existing 14-day PR-level change-failure proxy is unchanged.
The Evidence card's Issue opens : PR merges panel reports how fast the hive files issues against how fast it lands PRs, per repository and fleet-wide over the selected window, read against the governor's current cadence mode rather than against a flat 1:1 (pkg/evidence/opens_merges.go, /api/evidence/summary → issue_opens_pr_merges, server card cards["opens-merges"]).
Definitions. A hive-opened issue is an issue-create action in the hive's own audit log (agent_issue_created, hive_issue_created): an issue created through the hive's GitHub App identity, either on behalf of an agent or by hive automation. Issues humans or third-party bots open directly on GitHub never pass through the hive and are not counted — this is the hive's filing pace, not the repository's. A hive-merged PR follows the Outcome by author kind card: a folded evidence-bundle PR with a recorded merge where either this hive performed the merge (any author kind) or the author kind is agent (a merge the hive only observed through the review-outcome ledger counts when an agent wrote the PR). A human or bot PR merged outside the hive is not counted, and PRs the hive never saw are missing.
Expected band by mode. The governor's mode is read at render time: with a ?repo= filter, the card reads that repo's own cadence mode (GetState().RepoModes, populated under cadence_scope: per_repo) and falls back to the fleet mode when the governor has none recorded for it; with no repo filter it reads the fleet mode (GetState().Mode). Either way the mode is mapped to the band the ratio opens ÷ merges should fall in. Thresholds are named constants in pkg/evidence/opens_merges.go and mirrored as EV_OM_* in index.html for the explainer; change both together.
| Mode | Expects opens per merge | Why |
|---|---|---|
surge, busy |
≤ 1 (OpensMergesDrainMax) |
drain: with a full queue the hive should land more than it files |
quiet, idle |
1–2 (OpensMergesScoutMin–OpensMergesScoutMax) |
scout: with room to spare the hive may file ahead, but bounded so idle does not manufacture backlog |
| unknown (no governor) | 0.75–1.25 (parity ± OpensMergesParityTolerance) |
no cadence to lean on; expect parity |
The headline states the ratio, the mode, the band and a verdict: ✓ inside the band; ⚠ backlog growing above it (also when there are opens and no merges); ⚠ scouting below cadence under the quiet/idle floor. Deviation is shown against the band, not against 1.0, so a 0.8 in surge mode is a tick and a 2.7 in quiet mode is a warning.
Rolling 30 days. Independently of the window pills, the panel buckets the last 30 UTC days (rolling_30d_days, OpensMergesRollingDays) and reports the 30-day ratio against the parity band (rolling_30d_expected, rolling_30d_status): day to day the mode may push the ratio either way, but over a month it should regress toward 1:1. The sparkline plots a trailing 7-day ratio per day with the parity band shaded and a dashed 1:1 line; days with no hive merges in their trailing week are not plotted.
Grade. Needs GradeMinN (20) hive opens + merges in the window. Score = 1 − how far the ratio lies outside the band, relative to the nearer edge: inside the band A+; 10% out A-; 20% B-; ≥ 30% out, or opens with no merges, C-. Repeated audit entries for the same issue count once per entry (the audit log writes one per create); nothing is weighted by size, and no open is linked to the merge that closed it. Descriptive, not causal.
A proxy for the change failure rate, not a verdict on any change (#12067). When a PR merges, Hive scans its title and body for a keyword followed by #N or owner/repo#N (optionally PR #N / pull request #N) in the same repository:
- Revert:
revert,reverts,reverting,revert of(GitHub's Revert button writesReverts owner/repo#N). - Follow-up fix:
fix,fixes,fixed,follow-up to|of|for(alsofollowup,follow up),regression from|in|of.
A PR named both ways counts as a revert. A referenced number is linked only when its newest merged bundle records a merge in the 14 days before this one, so issue numbers and PRs Hive never reviewed link nothing. The merging bundle gets revert_of / followup_of (bundle ids) before it is sealed; then each linked bundle gets reverted_by / followed_up_by naming it and is re-sealed (and re-signed), so its hash differs from the one in its own merge comment. Commit messages are not scanned. A replayed merge adds nothing, and failures are logged and never affect the merge.
Reverts the hive did not merge (#13241): a merge the hive only observes — the pull_request closed webhook with merged: true, the PR state fetched for the review-outcome ledger, or the merged-PR backfill listing — is also checked for revert text, from the title and body already in hand and with no further GitHub call. Revert "…" titles (including GitHub's generated Revert "Original title (#N)"), Reverts #N references and git's This reverts commit <sha> line (7+ hex characters, matched against each candidate bundle's merge commit and head SHA) resolve to merged bundles in the same 14-day window and receive a reverted_by back-link naming the reverting PR as owner/repo#N@<head> (or @merged when the head is unknown). A revert whose text names neither a PR nor a commit is missed; the card says so.
Issue re-opens: when the duplicate-PR claim ledger voids a merged claim because a person reopened the issue it closed (the issues reopened webhook, or an enumeration listing the issue with state_reason: reopened), it hands the claim to Client.RecordReviewEvidenceIssueReopen, which stamps the closing PR's merged bundle with an issue_reopens entry, then re-seals it. Bundles with a matching captured issue_closures record retain later reopens too; legacy bundles retain the 14-day limit. The existing change-failure rate still counts only reopens within 14 days. Each issue is recorded once per bundle. Only issues linked by a closing keyword are tracked — the ledger never saw an issue closed by hand.
The two opt-in experimental arms of the study (RFC #12062) read these records: shadow-gate observations (shadow) and per-verdict reviewer model/backend. The run plan — consenting hives, 30-day periods, exact review.shadow / intent.shadow / review_models.exclude_author_family diffs, rollback — is research/ablation-plan-2026-10.md. Auto-merge is unchanged everywhere in it.
hivectl evidence backfill --repo <owner/repo> --since <date> [--until <date>] [--root <dir>] [--dry-run] reconstructs bundles for PRs that predate Hive's recorder, so the study in research/preregistration-2026-10.md has a baseline (#12371; run plan in research/backfill.md). It writes what GitHub history still knows and nothing else:
author.loginandauthor.kindby a documented heuristic, recorded inbackfill.author_kind_rule: Hive's— hive:attribution trailer in the body (attribution_trailer, carryingagent/backend/model), a login passed with--agent-login(agent_login), Hive's 🐝 identity footer (identity_footer), a GitHubBotaccount or[bot]suffix (bot_account), elsedefault_human.merge(actorfrom the merger GitHub reports, elseunrecorded;methodalwaysunrecorded), orbackfill.state: closedwithclosed_at, oropen.created_atis the PR's creation time, so backfilled bundles fall into the right historical window;updated_atandbackfill.atare when the backfill ran.revert_of/followup_ofand the reverse links, under exactly the change-failure link rules (pkg/evidence.ChangeRefs, 14-day window), against bundles built in the same run and bundles already on disk.
verdicts, ci (left with a zero captured_at), sentinel and shadow are absent, never fabricated; policy.acmm_level is 0 unless --acmm-level stamps a level known to be in force for the window. Every bundle carries backfilled: true so each chart and export can exclude or annotate it. Population exclusions from the pre-registration are applied as backfill.flags, never by dropping rows: hive_on_hive is added for hivecommons/hive, under_min_merged when fewer than --min-merged (default 10) PRs merged in the window, and placeholder_hive by --flag. Existing bundles for a head are skipped, so re-running is safe; the run pauses on GitHub rate limits and --dry-run prints counts only. Like everything in the evidence store, the backfill is record-only: nothing reads it to decide a merge.
A PR's outcome (pkg/evidence.PR.OutcomeKnown) has three sources, folded per PR by evidence.FoldPRs. Live merge: the hive's own merge paths (relay, sweep, queue) complete the merged head's bundle with merge via RecordPRMergedAudit, and a bundle started after GitHub already reports a merge records it on creation. Review-outcome ledger: every governor cycle folds the open-PR list into /data/review-outcomes.json (pkg/review/outcomes.go) and resolves PRs that left the list with one GetPRState each (capped at 40 per cycle); the same pass now stamps the PR's newest bundle with outcome — merged by someone other than this hive (a human by hand, prow/tide, another bot) plus an unrecorded merge naming the merger, or closed without merge — so no extra GitHub call is spent (Client.RecordReviewEvidenceOutcome). A bundle that already states an outcome is never touched, and a PR still open is not stamped. At read time, GET /api/evidence/summary also applies the ledger's rows to PRs whose records state no outcome (evidence.ApplyOutcomeHints): a PR that merged or closed before the ledger could write to its bundle, and a PR the ledger still sees open, both count as known. Backfill: hivectl evidence backfill reconstructs backfill.state from forge history for PRs that predate the recorder. The summary reports merged_elsewhere per author kind (a subset of merged), missing.prs_outcome_from_ledger (PRs whose outcome is known only from the ledger) and missing.prs_outcome_unknown (PRs with none of the three: typically opened and reviewed minutes before the ledger's first look, or in a repository the governor does not enumerate).
-
API:
GET /api/review/evidence?repo=<owner/repo>&number=<n>[&head=<sha>][&format=json|zip]for owner/merger roles. The default is the bundle for the PR's latest head;headtakes a full SHA or a unique prefix of at least 7 characters (409 if it matches more than one). Before either format is served, Hive verifies a signed bundle with the public half ofevidence.signing_key_file; when signing is not configured, it verifies the unsigned bundle's content hash. A failed or unavailable required signature is a 500 and no bundle bytes are returned.format=jsonreturns the verified bytes exactly as stored; adddownload=1for an attachment.format=zipreturnsbundle.json,verdicts.json(the matching verdict reports),review-links.json(the PR's review-links slice) andmanifest.json(SHA-256 of each file, plus the signing public key when the bundle is signed). A missing bundle is a 404 whose body hasstatus: "expired"andexpired: truewhen retention removed it, otherwisestatus: "not_found".GET /api/review/evidence/list?repo=&number=lists every retained bundle id and head for the PR after applying the same verification. -
Aggregate:
GET /api/evidence/summaryreports read-only aggregates over stored bundles, including per-tier proxywrite_counters({attempted, blocked, blocked_by_reason}, omitted when the counter file is absent) per-tierintent_counters(below), andcomplexity_by_repo(the newest 12 monthly complexity snapshots per repo, omitted when none exist); see API reference. Bundles whose hash or configured signature does not verify are excluded and counted inmissing.records_unreadable. -
Window: the summary takes
hours=(default 24,0= all time, capped at 90 days) or a custom rangefrom=/to=as RFC3339 orYYYY-MM-DD(a date-onlytois inclusive;todefaults to now).tobeforefrom, a span over 3 × 366 days, ortowithoutfromis a400. A custom range echoesfrom/toin the response and keeps PRs opened in[from, to). A PR is placed in a window by its open time, never by when a bundle was written: every bundle of the same PR is folded into one row — the newest record supplies verdicts, CI, sentinel and shadow observations, the earliestcreated_at(ormerge.opened_at) is the open time, and the merge event, the change-failure links (revert_of,followup_of,reverted_by,followed_up_by) and a backfilledbackfill.state: closedare taken from whichever record knows them. A live bundle is created when review starts and gains a merge event when the hive itself merged, or — when the bundle is started after the PR has already merged (automerge ran before the first review wrote one, or someone else merged) — carries the merge GitHub reports at creation (method: unrecorded, since the API says who and when, not how); a merge by another actor after the bundle exists is still unobserved until a backfill; a backfilled bundle (#12371) is stamped with the PR's open time and the forge's outcome. Without the fold, a hive that reviews but does not merge (Bluefin) showed every reviewed PR as open and every approval as a false approve, and a review sweep over old PRs pulled them into the 24h window. The fold isevidence.FoldPRsinpkg/evidence; the hub heartbeat evidence block andhivectl evidence exportuse the same one, so the three agree per PR (#12516). The response also carriesavailable_windows({"24", "168", "720", "all"}→ PR count under the samerepo/author_kindfilter) so the card can say "window too short — switch to 7d or 30d" instead of a bare "Not enough data". The Evidence card offers 24h · 7d · 30d · custom pills (custom accepts backfilled history, up to 3 × 366 days) and remembers the choice per browser (hive.evidence.window); its header reads "Evidence · last 30d" or "Evidence · Oct 1 – Oct 15". -
Hub revert rates by author kind: heartbeat evidence includes
by_author_kind, a fixedhuman/agent/botmap of merged PR counts (n) and linked revert counts (bad) over the newest bundle per PR in the 90-day creation window. Follow-up links do not count as reverts. The owner-only hub evidence drill-down pools these counts intorevert_rate_by_author_kind, displaying the sample size and 95% Wilson interval (null rate below 10 merges). Older spokes without the map contribute no author-kind samples. Revert links are best-effort, within 14 days of merge; recent merges may not yet have a full observation window. These rates are descriptive, not merge gates. Each hive row'scounts.by_author_kindcarries the same buckets so a hive or ACMM-level selection re-pools them in the browser. -
Outcome by author kind (panel 1):
outcomes_by_author_kindin the summary counts, over the folded record per PR in the window,n,merged,closed,open,unknown,reverted_14d(merged and then linked byreverted_bywithin 14 days),issue_reopened_14d(merged and then an issue it closed was reopened within 14 days, fromissue_reopens),change_failures(the two added; a PR both reverted and re-opened counts twice) andchange_failure_rate(change failures ÷ merged,nullunder 1 merge) forhuman,agentandbotauthors (#13241). The Evidence sub-panel draws it as the Outcome by author kind panel: one grouped bar per kind for merged / closed / undone (reverted or re-opened) plus a hollow bar for outcome unknown, withn=, the merged rate and the split of undone in the tooltip. Denominator policy: merged rate = merged ÷ PRs with a known outcome; change-failure rate = (reverted + re-opened) ÷ merged. This is the change-failure proxy for "a PR being accepted is not the same as the project being better off": it asks whether agent-written work gets undone more often than human work. Both signals are detected from data already fetched (see Change-failure links); reverts are only linked since 2026-10-15 and re-opens only since 2026-10-20.merged_elsewhereis the subset ofmergedthat someone other than this hive performed, learned from the review-outcome ledger;closedandopencome from a backfilled record of the PR or from the ledger (see Where outcomes come from). A PR with no merge event, no backfilled record and no ledger row has no recorded outcome — it isunknown(pkg/evidence.PR.OutcomeKnown), counted and shown, and excluded from every rate rather than counted as open or as not merged. The summary'smissing.prs_outcome_unknowngives the window total andmissing.prs_outcome_from_ledgerhow many the ledger resolved. A revert only counts once the reverting PR is linked. Reporting only (#12364). -
Maintainability trend (panel 7): the Evidence sub-panel draws
complexity_by_repoas three per-repo lanes on separate scales — median Go cyclomatic complexity, functions over the threshold, and warnings by tool (dashed, one dash pattern per tool) — with the first and last month on the axis. Snapshots whose month falls in the selected window are kept, boundary months included, andn=is the number of repos with at least two snapshots in that window; a repo with one snapshot is drawn as points. Missing months and missing tool measurements stay gaps, never zero. Vertical dotted ACMM markers come fromacmm_changes: for each repo and level, the month of the earliest stored bundle whosepolicy.acmm_levelis that level (1 …acmmadvisor.MaxLevel). Unlike the other aggregates they are computed over all stored bundles for the repo — not the latest bundle per PR, not theauthor_kindfilter, not thehours/from/towindow — because policy maturity is a repo property, and the chart clips them to the months it shows. A marker is the first observation, not the moment the setting changed, and a return to an earlier level is not shown. The panel is deliberately not graded: its explainer ends in a Not graded badge (grade keyungraded, "report-only; no threshold to grade against") rather than a Strong/Fair/Weak/Not enough data rating, because complexity rising or falling is an observation with no causal claim about agents, so there is no threshold to hold it to. It shows an empty state while no repo has a monthly snapshot. Reporting only (#12369). -
Confidence series: the self-reported / derived toggle selects
reviewer_calibrationorreviewer_calibration_derived, with a separate n for each. Old bundles withoutconfidence_derivedare excluded from the derived series;reviewer_calibration_legacydiscloses self-series verdicts still using the historical fallback. The plain summary compares the pooled endpoint rise in merged share per bin, only across bins with at least five known outcomes and at least two such bins per series. A falling intermediate bucket disqualifies monotonicity; sparse data produces no winner. The comparison is descriptive, uses each series’ available cohort, and does not establish causation or correctness. -
Reviewer calibration and accuracy (panels 2–3):
reviewer_calibrationbuckets each verdict by its stated confidence per perspective withn,merged,blocksandunknown(verdicts whose PR has no recorded outcome). Denominator policy: merged rate = merged ÷ (n−unknown); block rate = blocks ÷n(a block is known at verdict time). The card draws a bucket with fewer thanEV_CALIB_MIN_BUCKET_N(5) known-outcome verdicts hollow and leaves it out of the joined line, because one or two verdicts invert a line without meaning anything.reviewer_accuracy_by_modelcarriesapprovals,approved_not_merged,approved_unknown,blocks,merged_after_blockandblocked_unknownper model family × author kind, with blocks split by the bundle'spolicy.require_approvalintoenforced_blocks(merged_after_enforced_block,enforced_blocked_unknown) andadvisory_blocks(dissent_before_merge,advisory_blocked_unknown) — no row is dropped and the unsplit totals stay (#12595): false-approve rate = approvals on PRs known not to have merged (closed without merge, or a backfilled record still open) ÷ approvals on PRs with a known outcome; false-block rate = enforced blocks on PRs that merged ÷ enforced blocks on PRs with a known outcome; an advisory block on a PR that merged is dissent before merge, shown as a count and a share of advisory blocks and never as an error rate (when every block in the window was advisory the card reads "Perspective verdicts were advisory on this hive (review.require_approval off); n blocks preceded a merge."); the unknown counts are shown beside each rate. Nothing is dropped silently and nothing unknown is counted as a failure. One bias is stated on the card because it cannot be removed by policy: on a live-only hive the only way an outcome becomes known is a merge this hive performed (or, since the merge-at-start fix, a merge GitHub already reported when the bundle began), so until a backfill covers the window "merged ÷ known" is an upper bound and "false-approve ÷ known" a lower bound; the unknown count is the honest measure of what is unresolved. -
Boundary enforcement (panel 4): the headline is blocked ÷ every agent GitHub write the hive can see in the window, from
write_activityin the summary:total_writes=mediated.n(hive-mediated writes the audit log records — agent comments, issues, PRs, reviews, labels, issue closes, branch pushes, review requests) +direct.attempted(writes agents sent straight to GitHub through the proxy, summed from the proxy's weekly counter buckets that overlap the window, per tier underdirect.by_tier);blocked= proxy refusals (direct.blocked, split bydirect.blocked_by_reason) + audited refusals of mediated writes (mediated.refused). The panel prints "B of T agent writes blocked · x%" with both absolute counts, one row per intent tier (0ADVISORY… 3ISSUES_PRS_MERGE) showing blocked ÷ direct writes for that tier, one row for hive-mediated writes, the top block reasons in the legend, andsentinel_hits_per_weekas a sparkline;n=istotal_writes. Denominator policy: every agent write counts once; would-block counts from intent verification (intent_counters) are shown separately in the explainer and never added to blocked. The proxy'swrite-counters.jsonand the intentintent-counters.jsonnow live under/data/hive-metrics/(the PVC) so they survive container restarts — before this they sat in container-local/var/run/hive-metricsand were lost on every restart, which is why a hive could show "16 attempted" after thousands of writes; a file left at the old path is folded in once on startup. The counter file recordsrecorded_since(the Monday of its earliest week) anddirect.since/direct.partial_coverage/direct.recordedtell the card when a window reaches back before direct-write counting began: the card then says "direct-write counters recorded since " and the total for earlier weeks holds hive-mediated writes only — stated, never back-filled. Writes that bypass both the hive and the proxy are invisible.write_counters(all-time totals per tier) is still emitted for compatibility. Reporting only (#12368). -
Intent counters: every intent evaluation — the merge-eligible lane's
writeIntentVerdictspass and the App self-merge sweep's gate, whether or notintent.enforceorintent.shadowis on — increments an in-memory counter keyed by tier (tier0..tier3) and writes one audit-log line, actionintent_evaluated, detaillane=<merge-eligible|self-merge> tier=<tierN> would_block=<bool> enforce=<bool> [reason=<code>](#12362).would_blockis whatBlocksMerge(true)reports, so with enforcement off it is the shadow answer; reason codes aretier1_needs_issue,tier2_needs_plan,tier3_needs_approval,alignment_misaligned,evidence_unavailableorunknown. The counters flush once a minute (and on shutdown) into/data/hive-metrics/intent-counters.json, beside the proxy'swrite-counters.jsonand in the same shape: cumulativetotalsper tier (attempted= evaluations,blocked= would-block,blocked_by_reason) andweekskeyed by the UTC Monday of each ISO week. The summary API exposestotalsasintent_counters({attempted, blocked}per tier), omitted when the file is absent. Record-only: the counters are never read back into eligibility. -
Shadow gates (panel 8):
shadow_gatein the summary counts, over the latest bundle per PR in the window, the PRs with any shadow observation (n),would_have_held(review gate) andwould_have_blocked(intent), and for each gate (review_gate,intent) splits the evaluated PRs intoheldandpassed, each with how they actually turned out:merged,open,reverted(merged and later linked byreverted_by),closed(from a backfilled record of the same PR; a live-only bundle does not record a close) andunknown(no recorded outcome; excluded from the false-alarm rate, shown as a count).review_gate_by_lanesplits the review gate's counts intomerge_eligibleandself_merge(the self-merge ReviewGate, #12592);review_gatestays the sum of both. The Throughput section's Evidence sub-panel draws it as the Shadow gates panel (one review-gate row per lane once the self-merge lane has observations), with a zero state while shadow mode is off. Reporting only; nothing reads it to decide a merge (#12068). -
Claim vs verdict (panel 5):
claim_vs_verdictin the summary groups the latest bundle per agent-authored PR (author.kind == "agent") by the ISO week it was created and countsclaimed(every such PR: opening or marking it ready is the agent saying "done"),ci_not_green,verdict_not_approve(any perspective verdict other thanapprove) andboth, plusn,ci_pendingandci_captured, the CI column's denominator. CI state is classified byevidenceCIState: the bundle'sci.status(success/failure/pending) wins when set; otherwise any check with a failing conclusion (failure,cancelled,timed_out,action_required,startup_failure,stale) is not green, checks that all finishedsuccess/skipped/neutralare green (the same reading the merge gate applies — a skipped job is not a red one), and anything stillqueued/in_progressis pending. Denominator policy: CI not green rate = not green ÷ (ci_captured−ci_pending); a record with no captured checks is disclosed undermissing.records_empty_ci, one still running undermissing.records_ci_pending, and neither is counted as agreement or as failure. Before this classification everyskippedcheck read as "not green", which flagged almost every captured record on hives that skip path-filtered jobs. The Evidence sub-panel draws it as the Claim vs verdict panel with grouped weekly bars. Reporting only; nothing reads it to decide a merge (#12365). Read as the CI gate — integrity + convergence (ci_recoveryin the summary; the panel is titled CI gate): this hive hands off to CI by policy — agents open the PR as soon as they believe it is done and do not wait for CI; a red check holds the PR until the agent pushes a fix (auto_merge.self_authoredon,auto_merge.required_checksif set; there is no knob that makes agents wait for CI, and the summary echoes the policy underci_recovery.policy). CI red at claim is therefore by design: disclosed, not graded. Two grades are: Grade A, gate integrity (safety) —gatecovers every PR opened in the window that merged, any author, by the check state of the head that merged. The merged head is the backfilled record'shead_sha(GitHub's view) when one exists, else the newest live head — a review-time record often predates the final push, so the newest reviewed head is frequently not the merged head. The state is GitHub's check-run conclusions on that head from the commit-trail rollup where the collector has reached the PR (authoritative), else the newest live snapshot for that head. What counts as red is what the hive's own merge gate counts:PRCIRecoveryTrailresolves the PR's base branch's required set as it is now (branch protection viaRequiredStatusCheckContexts, else the configuredauto_merge.required_checks;required_known,required_checkson the trail) and judges each head on those contexts only — green when every required context succeeded (a required DCO context is satisfied once any DCO context did), red when one failed (required_red), pending when one is still running or has never reported (missing_required). A failed check outside the required set — a docs link check, a labeller, a cancelled optional build — never blocked the merge and is not a breach; it is carried asnon_required_redon the head and disclosed. Without a resolvable required set the head is judged on every gating check (the pre-existing reading) and counted underrequired_unknown. The trail also recordsmerged_by.merged_n,with_checks,green(green_from_bundlesof them rest on a snapshot — a re-run cannot turn a green head red, so they count;non_required_redof them merged with a red non-required check),breaches(confirmed only: GitHub reports a required check red on the merged head under a resolved required set; each listed inbreach_prswith repo, number, head, author kind,state,red(the check names),merged_at,merged_by,source— a breach is a bypass by whoever merged, not a fault of the author, and a check made required after an older merge reads red for that merge, which is why the date is shown),required_unknown,suspected/suspected_prs(three readings that cannot be confirmed: a review-time snapshot that was red on the merged head — a check can be re-run to green on the same head after it; a required check GitHub shows no run for (statemissing_required,missing) — the merge gate accepts a path-filtered skip and this panel cannot verify one; a red read under an unresolved required set — it may be an optional check. Not breaches; listed; the collector enriches these first),pending(merged while a check was still running),unknown(no snapshot on the merged head, no trail). Denominator policy: gate integrity = (with_checks−breaches) ÷with_checkswherewith_checks=green+breaches+pending;suspectedandunknownare counts outside the rate — a merged PR nobody snapshotted is not green and not a breach, and a stale red snapshot is a question for GitHub, not a verdict. Secondary gate latency:median_green_to_merge_hoursovergreen_to_merge_nmerged PRs, first green on the merged head → merge. Grade B, convergence (efficiency) —convergencecovers agent-authored PRs with a live record (n): a pass is one CI evaluation of a distinct head SHA;passesbuckets settled PRs by passes until the first green head:"1","2","3","4+","never"(closed without merge and without a green). Passes come from the GitHub commit trail where the collector has reached the PR (from_github:PRCIRecoveryTrailscans the PR's commits from the first, reading check-run conclusions through the app token, until the first green orPRCIConvergenceCap= 4 evaluated heads —evaluated,passes_to_green,scan_capped) and from the hive's own live snapshots otherwise (from_bundles; bundles can miss heads nobody reviewed, so this is a stand-in until the trail arrives).settled= Σpasses;in_flight(open, not yet green) andunknown(no CI captured, or merged without an observed green) are counts outside every denominator.median_passesis over PRs that went green, counting4+as 4.by_weekrepeats the read per ISO week of PR open time. Denominator policy: abandoned share =never÷settled; first-pass share ="1"÷ (settled−never) — the RFC's C1 number, drawn as a neutral line, not graded. The collector enriches ≤ciRecoveryMaxPerPass= 40 PRs per hourly pass — red-at-claim, suspected and merged-with-unknown-state PRs first, then the newest agent PRs without a trail — into the durable rollup/data/hive-metrics/ci-recovery.json. Disclosed facts (no grade):at_claim{green, red, pending, none}over agent claims (the claim is the first live agent bundle with captured checks), swarm non-approve verdicts, and the recovery readredfor red-at-claim PRs:recovered_merged,green_open,abandoned,pending,outcome_unknown,enriched/not_enriched,median_pushes_to_green,median_hours_to_green,same_head_recovered; recovery rate =recovered_merged÷ (recovered_merged+abandoned). Grading constants (static/index.html): Grade A — 0 breaches Excellent, breaches ÷with_checks≤ 1% Good (EV_GATE_BREACH_GOOD), ≤ 3% Fair (EV_GATE_BREACH_FAIR), above Weak. Grade B needs both conditions: median passes ≤ 1.5 and abandoned < 5% Excellent (EV_CONV_PASSES_EXCELLENT,EV_CONV_ABANDON_EXCELLENT); ≤ 2.5 and < 10% Good (EV_CONV_PASSES_GOOD,EV_CONV_ABANDON_GOOD); ≤ 4 and < 20% Fair (EV_CONV_PASSES_FAIR,EV_CONV_ABANDON_FAIR); otherwise Weak. Panel grade = the lower of A and B (evLowerGrade); both needEV_GRADE_MIN_Nsamples in their denominator. The weekly bars keep the claim-time facts (claimed, CI not green at claim, swarm not approve, both). -
Work balance (panel 6): answers "is the queue keeping up?" from GitHub truth rather than from the hive's own audit log alone.
work_balancein the summary has one row per ISO week of the window withopened(issues, splithuman/agent/bot),closed,prs_opened(same split),prs_merged,prs_closed(closed without merge),queue_issues/queue_prs(open at week end),issue_sourceandthrottled;work_balance_sourcesnames where each series came from. Sources: issue opens and closes come from GitHub's issues API (state=all,since), every author included, pulled by the metrics collector hourly into the durable rollup/data/hive-metrics/issue-activity.json(42-day initial lookback, 180-day retention;issues_github_sinceis the first date it covers,issues_fetched_atthe last refresh); author kind isagentwhen the body carries an agent attribution trailer,botfor GitHubBotaccounts, elsehuman, and an audited agent-created issue overrides the kind toagent. Weeks whose Monday predatesissues_github_sinceread the backfilled issue series at<evidence-root>/<owner>/<repo>/issues/<YYYY-Www>.jsonwhen its coverage contains the requested portion of that week. Event timestamps keep partial date ranges exact; zero-event files establish observed zero counts. The collector stays authoritative for weeks whose Monday is at or afterissues_github_since, including exact equality. Missing, malformed, or insufficiently covered series files retain the audit fallback per repository (issue_source: "audit", counted inaudit_only_weeks); mixed repository rows also disclose audit coverage. Backfilled rows useissue_source: "github";issue_series_weekscounts the repository-weeks supplied by the series. Series events carry no issue number, so the audited agent-created override above cannot be applied to series weeks: an agent-opened issue without the attribution trailer or 🐝 footer keeps the kind the writer recorded (botfor aBotaccount) in series weeks while the collector reclassifies the same issue asagentin rollup weeks. The audit log sees only hive-mediated writes, so the human column is a floor for audit weeks.hivectl evidence backfill-issueswrites the series (see CLI reference). When the collector starts midweek, backfill through the following Monday (exclusive--until) so the entire boundary week has series coverage; stopping at the collector timestamp leaves that week on audit fallback. PR opens, merges and closes come from the folded evidence bundles (live and backfilled) for PRs opened in the window, so PRs this hive never saw are missing. Week-end queue depth is the governor's last state snapshot of the week (/data/nous/snapshots, actionable open issues / PRs — the same numbers as the dashboard queue section, held items excluded;queue_samples,queue_retention_days30). Denominator policy: issue ratio =closed÷openedand PR ratio =prs_merged÷prs_openedover the whole window; nothing is dropped, PRs closed without merge are drawn as a tick but never counted as merged, and an unmatched close (an issue opened before the window) still counts as closed because the question is throughput, not per-issue lifecycle. Grading (EV_THROUGHPUT_*,EV_QUEUE_GROWTH_MAX): issues and PRs are graded separately and the card takes the lower — ratio ≥ 0.95 Excellent, ≥ 0.85 Good, ≥ 0.70 Fair, below Weak, and any grade is Weak when the week-end queue grew more than 10% from the previous week. Raw volume is never graded: a hive that opens 400 issues a week and closes 400 is Excellent. The Evidence sub-panel draws two charts (issues, PRs): opened stacked by kind, a closed / merged line, a closed-unmerged tick and a dashed week-end depth line on the right axis, under a headline "Queue at end of : N open issues · M open PRs".throttledstays false because this hive reports no governor issue-creation throttle windows (missing.throttle_windows_unavailable). Reporting only (#12366). -
Dashboard: owners get an Evidence button on Review Queue rows and Pipeline cards. It opens a modal with the bundle id, head, hash, whether it is signed, the verdict count and merge status, and Download JSON / Download ZIP buttons.
-
CLI:
hivectl review evidence <owner/repo#n> [--head <sha>] [-o FILE] [--zip]prints or saves the bundle, andhivectl review evidence verify FILE [--pubkey KEY]checks it offline (see hivectl). -
Export:
hivectl evidence export [--since <RFC3339|duration>] [--format csv|jsonl] [--repo owner/name] [--out FILE] [--root DIR]dumps one row per PR straight from the bundles on disk; see Exporting. -
On the PR: when a PR merges through any Hive merge path, the merge event is added to the head's bundle (re-signed) and Hive posts one comment:
<!-- hive-review-evidence --> Evidence bundle `<id>` sha256:<hash> — download from the dashboard or `hivectl review evidence <owner/repo#n>`.The marker makes it idempotent: a PR gets one such comment, however many sweeps see the merge.
Operators and the study need the raw per-PR evidence off a spoke without scraping /data/evidence. hivectl evidence export reads the schema v1 bundles under --root (default /data/evidence, layout <org>/<repo>/<pr>/<sha>.json) from the local filesystem — no hive session, no network — and emits one row per pull request, folding every bundle of the PR exactly as GET /api/evidence/summary does (see Window under Downloading), so exported counts reconcile with the dashboard Evidence charts (#12367, #12516, RFC #12062). Identity, verdict, CI, sentinel, shadow and created_at/updated_at columns come from the newest bundle; outcome, merged_at, the four link columns and opened_at come from whichever bundle of the PR recorded them.
hivectl evidence export --since 168h --format csv --out evidence.csv # last 7 days
hivectl evidence export --since 2026-10-01T00:00:00Z --repo hivecommons/hive # JSON Lines on stdout--sincetakes an RFC3339 timestamp or a Go duration and keeps PRs whose open time (opened_at) is at or after it, as the summary API does; a PR opened before the cutoff stays out even when a newer head was reviewed after it.--formatisjsonl(default, one JSON object per line) orcsv(header row first). Lists join with;in CSV and each verdict isperspective:verdict:model:backend:confidence.- Columns:
repo,pr,head_sha,bundle_id,author_login,author_kind,author_model,acmm_level,perspectives,reviewer_models(distinct, sorted),verdicts(perspective, verdict, model, backend, confidence; sorted by perspective),ci_conclusion(ci.status),ci_captured_at,outcome(merged,closedwhen a backfilled bundle records a close without merge, elseopen),merged_at,revert_of,reverted_by,followup_of,followed_up_by(bundle ids),shadow_review_would_have_held,shadow_intent_would_have_blocked(empty /nullwhen the gate was not evaluated),sentinel_hits,signed,created_at,updated_at(of the newest bundle),opened_at(PR open time). - Rows are ordered by repo (case-insensitive) then PR number, so two runs over the same files are byte-identical. Files that do not parse or are not schema v1 are skipped and counted on stderr; nothing is fabricated.
- Reporting only: the export reads bundles and never influences a merge.
Set evidence.signing_key_file in hive.yaml (see the operator reference). The file holds an Ed25519 private key as a 32-byte seed or 64-byte key, hex or base64 encoded. If the path is empty, bundles are written unsigned with "signed": false; they still carry a hash, which detects accidental change but not tampering by someone who can recompute it. A missing or unusable configured key also makes writers fall back to unsigned bundles, but dashboard readers fail closed until that configured key is restored, so an unavailable key cannot silently downgrade signed evidence. Keep the private key off shared hosts and publish only the public key.
You need the bundle JSON and the operator's Ed25519 public key (32 bytes, hex). No Hive access is required.
- Check
"signed": trueand thatsignatureis present. If not, the bundle is unsigned and cannot be verified beyond its hash. - Build the canonical JSON as described above.
- Compute SHA-256 of it and compare, as lowercase hex, with
hash. A mismatch means the content changed. - Base64-decode
signatureand verify it as an Ed25519 signature of the ASCIIhashstring under the public key.
A reference check in Python (needs pip install cryptography):
import base64, hashlib, json, sys
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
bundle = json.load(open(sys.argv[1]))
pub = Ed25519PublicKey.from_public_bytes(bytes.fromhex(sys.argv[2]))
sig = base64.b64decode(bundle.pop("signature"))
claimed = bundle.pop("hash")
canon = json.dumps(bundle, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
assert hashlib.sha256(canon).hexdigest() == claimed, "content changed"
pub.verify(sig, claimed.encode()) # raises InvalidSignature on failure
print("ok", bundle["id"])Go users can call evidence.Verify(&b, pub) from pkg/evidence. The Python sketch assumes numbers re-encode identically (integers and short decimals such as 0.9); if a number does not round-trip, use the Go verifier.
Every panel on the Throughput section's Evidence card shares one layout (#13204): a header row — title · ⓘ · n=… · letter grade — then one plain-English sentence, then the chart and a compact legend. The letter grade and the sentence are computed on the server (pkg/evidence/grade.go) from the same /api/evidence/summary numbers the chart draws, and shipped as cards[<panel>] = {grade: {letter, band, reason, enough_data, rubric}, plain_summary}. Nothing inside a panel renders larger than the header row, and nothing in it is read by anything that decides a merge.
The ⓘ button opens a popover over the chart (never inline) with the plain-language explainer — What is measured, Why it matters, How it is measured, What good looks like, What ours shows, Details (the denominators, policy and coverage text that used to sit under the chart), What this does not show — followed by the grade line: the letter, the one-line reason, and the rubric it was computed by. The "ours" and "details" sections are computed from the summary for the selected window, so they change with the 24h / 7d / 30d / custom pills. The button is keyboard reachable (aria-expanded, aria-controls); Esc closes an open explainer.
Boundary enforcement is deliberately not graded, including in the legacy client fallback. Its status is Enforcement demonstrated when at least one actual refusal is observed, even below 20 writes, and Enforcement not observed otherwise. This is positive evidence of enforcement on those attempts, not proof that bypasses or false positives are absent. Refusal frequency describes attempted-agent compliance, not enforcement effectiveness; increasing prohibited attempts is not a goal. Sentinel detections are informational, never counted as proven blocked writes. Counts and denominator coverage remain visible; missing direct counters and partial coverage remain explicit.
A grade is a letter from A+ to C- (A band green, B band amber, C band red) or — (grey, dashed) when the panel does not have enough data to be graded. Hovering the pill shows the reason ("86% of 22 PRs with a known outcome merged; 1 of 19 merged was reverted"); the popover adds the rubric. Where a rubric produces a 0–1 score the letter is: A+ ≥ 0.97, A ≥ 0.93, A- ≥ 0.90, B+ ≥ 0.87, B ≥ 0.83, B- ≥ 0.80, C+ ≥ 0.77, C ≥ 0.73, otherwise C-. Every rubric needs at least GradeMinN (20) samples of its denominator, else — with a reason that names what is missing. Rubrics are pure functions in pkg/evidence/grade.go, each table-tested; change the table and the code together.
| Panel | Needs | Rubric |
|---|---|---|
| Outcome by author kind | 20 PRs with a known outcome (merged, closed or open) | score = merged ÷ known × (1 − (reverted + issue re-opened within 14 d) ÷ merged, capped at 1); letter from the score table |
| Reviewer calibration | 20 verdicts with a known outcome in the selected series’ 0.95 bucket | score = merged share in the top bucket, minus 0.10 when the merged share is not non-decreasing across buckets that hold ≥ 5 known outcomes |
| Reviewer accuracy × model family | the rework-based outcome ledger (pkg/review/accuracy.go) with ≥ 20 samples |
score = 1 − max(false-approve rate, false-block rate) summed over reviewer models; — ("needs the reviewer outcome ledger") until the ledger exists — merge-vs-close is never used as ground truth |
| Maintainability trend | one repo with ≥ 2 monthly snapshots in the window | first→last change of median cyclomatic and of functions over threshold, summed across repos: neither worse → A- / A / A+ by how many improved (0 / 1 / 2); one worse → B (B+ if the other improved); both worse → C |
| Boundary enforcement | No minimum sample; observational, not graded | Any observed block → Enforcement demonstrated on those attempts; zero → Enforcement not observed, never failure or A-. Counts and blocked ÷ observed writes remain context, not a score |
| CI gate — integrity + convergence | 20 merged PRs with a known merge-time check state | confirmed breaches (required check red or missing on the merged head, as the merge gate judges): 0 → A+; ≤ 1% C+, ≤ 3% C, above C-. Convergence and CI red at claim are disclosed in the card and popover, not graded |
| Work balance | 20 issues opened in the window | issues opened ÷ closed: ≤ 0.8 A+, ≤ 0.9 A, ≤ 1.0 A-, ≤ 1.15 B+, ≤ 1.3 B, ≤ 1.5 B-, ≤ 1.75 C+, ≤ 2.0 C, above (or nothing closed) C- |
| Issue opens : PR merges | 20 hive issue opens + hive PR merges in the window | score = 1 − relative distance of opens ÷ merges outside the current governor mode's band (surge/busy ≤ 1; quiet/idle 1–2; unknown 0.75–1.25); inside the band A+, 10% out A-, 20% B-, ≥ 30% or no merges C-. The rolling 30 d is judged against parity and disclosed, not graded |
| Shadow gates | 20 PRs observed by the shadow gates | would-have-held ÷ observed: ≤ 5% A+, ≤ 10% A, ≤ 15% A-, ≤ 20% B+, ≤ 30% B, ≤ 40% B-, ≤ 50% C+, ≤ 65% C, above C- (nothing was actually held; the share says how often the gates would have intervened) |
The plain sentence under the header is at most 20 words and is templated from the numbers, for example "Agents: 418 merged, 1 reverted, 0 re-opened (0.2%). Humans: 346 merged, 0 undone. 3 outcomes not recorded yet." (a kind with no change failures is shortened to "0 undone" to stay within the word budget) or "All 58 merged PRs with a known state had green required checks. Agent PRs needed a median 1 CI pass." Zero-data windows get an explicit sentence ("No PRs with a recorded outcome in this window.") rather than a blank.
The legacy client-side Strong / Fair / Weak bands below are still computed inside the explainers (they feed the "What good looks like" wording) and remain the fallback grade line when a hive does not yet ship cards.
Terms used on first sight: a confidence bucket groups verdicts by the reviewer's stated confidence (0.55, 0.65, 0.75, 0.85, 0.95); calibration asks whether that confidence matches how often the verdict turned out right; a false approve is an approval on a PR that did not merge; a false block is a block an enforcing gate was overridden on (the PR merged anyway while review.require_approval was on); a block recorded while perspective verdicts were advisory (policy.require_approval false) that preceded a merge is dissent before merge — a count and a share of advisory blocks, never an error rate (#12595).
Grades are Strong, Fair, Weak or Not enough data — Work balance uses the four-band Excellent / Good / Fair / Weak scale the throughput ratio calls for. They describe the data in the window and are never read by anything that decides a merge. Thresholds are named constants in index.html and listed here; change both together.
| Panel | Graded on | Strong | Fair | Weak |
|---|---|---|---|---|
| Outcome by author kind | (reverts + issue re-opens) within 14 d ÷ merged (EV_REVERT_*) |
≤ 5% | ≤ 15% | above |
| Reviewer calibration | merged rate in the selected series’ 0.95 bucket (EV_CALIB_*) |
≥ 80% | ≥ 60% | below |
| Reviewer accuracy × model family | worse of false-approve and false-block rate (EV_ERR_*); the false-block rate counts enforced blocks only, so advisory dissent before merge never paints the panel red |
≤ 10% | ≤ 25% | above |
| Boundary enforcement | Not graded: observed blocked writes demonstrate exercised enforcement, independent of refusal rate or sample size | — | — | — |
| Shadow gates | would-have-stopped PRs that merged without a revert ÷ stopped PRs that merged (EV_SHADOW_FALSE_ALARM_*) |
≤ 25% | ≤ 50% | above |
| CI gate — A, integrity | breaches (merged with a required check red or missing, as the merge gate judges; red non-required checks are disclosed, not breaches) ÷ merged PRs with a known merge-time state (EV_GATE_BREACH_*); unknown, suspected and pending are counts outside the rate |
0 breaches Excellent; ≤ 1% Good | ≤ 3% | above |
| CI gate — B, convergence | median CI passes to first green over agent PRs that went green AND abandoned (never green, closed unmerged) ÷ settled (EV_CONV_PASSES_*, EV_CONV_ABANDON_*); panel grade = lower of A and B. CI red at claim is disclosed, not graded: this hive hands off to CI by policy |
≤ 1.5 passes & < 5% Excellent; ≤ 2.5 & < 10% Good | ≤ 4 & < 20% | beyond |
| Work balance | issues closed ÷ opened and PRs merged ÷ opened, graded separately, card takes the lower (EV_THROUGHPUT_EXCELLENT 0.95 / EV_THROUGHPUT_GOOD 0.85 / EV_THROUGHPUT_FAIR 0.70); Weak regardless when week-end queue depth grew > 10% week over week (EV_QUEUE_GROWTH_MAX) |
Excellent ≥ 95% · Good ≥ 85% | ≥ 70% | below, or queue growing |
Maintainability trend is not in the table on purpose: its explainer ends in a Not graded badge ("report-only; no threshold to grade against") rather than one of the rated grades, because complexity trends are observations with no causal claim. Not graded is distinct from Not enough data: more snapshots would not produce a grade, and its "What ours shows" section carries no early-read caveat.
A panel is Not enough data when the graded denominator is below EV_GRADE_MIN_N (20); its reason line then names the denominator ("needs at least 20 merged PRs") and, when the summary's available_windows shows a longer preset holding more PRs, adds "window too short: 7d 509, 30d 2553, all time 8731 PRs; switch to 7d or 30d". The card header shows the same hint when the window holds no PRs at all. Windows of EV_SHORT_WINDOW_HOURS (48) or less add an "early read" caveat. Every explainer states its denominator policy and repeats the honest limits: merged is not the same as correct, a PR with no recorded outcome is reported as unknown n=X and left out of every rate (never counted as "not merged" or as a false approve), live evidence (verdicts, CI, sentinel) has only been recorded since EV_DATA_SINCE (2026-10-08) reverts only since EV_REVERTS_SINCE (2026-10-15) and issue re-opens only since EV_REOPENS_SINCE (2026-10-20); backfilled records reach further back but carry only author, outcome and dates.
A push changes head_sha, so earlier verdicts no longer describe the code. Hive starts a new bundle for the new head with previous_bundle_id set, and re-reviews. The old bundle is left untouched and remains verifiable.
Hub admins reach the fleet evidence view from the Fleet quadrant card on
the hub dashboard (click the chart or its 🔬 Evidence button). It is
GET /api/hub/evidence rendered findings-first: a What this says block
of three to six plain sentences generated from the data, then one table per
section — each with a one-line plain finding under its title, a sticky title
and column heads — a window selector (24h · 7d · 30d · 90d · custom
date range — hives report fixed windows in their heartbeat, so a custom range
is served from the smallest window that covers it and the view says so), hive
and autonomy-level filters, a 📄 Report / ⬇ Markdown pair (see
Downloadable report below) and an ⓘ button per section that opens a
plain-language explainer with four headings: What is measured · Why it
matters · How it is measured · How to read our numbers. The last one is
computed from the numbers on screen and ends with a grade.
Every sentence in What this says, and every one-line finding under a
section title, is generated from the response fields (findings[] on the
API; regenerated client-side by the same rules when a filter is active) and
never typed in. Each has an explicit fallback when the data is not there
("No autonomy level has 10 merged PRs yet, so revert rates cannot be
reported") so no number is ever invented. Sections no selected hive reports
are not drawn as empty tables; they are listed once under Waiting on
hives at the bottom, in user-facing words ("hives report this once upgraded
to the Oct 16 build or later"). The technical pointer — which heartbeat field,
which issue — lives only in that item's ⓘ explainer and in this document.
Visible modal copy never cites an issue number
(TestEvidenceModalNoIssueNumbers).
| On screen | Means |
|---|---|
| All selected hives | the pooled row — every hive the filter currently selects (the whole fleet with no filter). When exactly one hive is selected its own row reads (same hive as above) instead of repeating the numbers. |
| Merged PRs | the number of pull requests GitHub says were merged in the last 90 days (the denominator for coverage). |
| X% of N | the rate and how many cases it is computed from, in one cell. |
| margin lo – hi (muted line under a rate) | the margin (95%): with this many cases, the true value is very likely inside this range. Fewer cases, wider range. |
| — | below the minimum sample of 10 cases a rate prints as a dash, never 0%, because a percentage from so few cases is mostly luck; the count is still shown. |
| Reviewer confidence | how sure the AI reviewer said it was, as one of four ranges (below). |
| False approve | the reviewer approved, but the PR was never merged or was reverted. |
| False block | the reviewer blocked while an enforcing gate was on (review.require_approval), but the PR merged anyway after a human override. |
| Dissent before merge | the reviewer blocked while verdicts were advisory (review.require_approval off) and the PR merged; reported as a count and a share of advisory blocks, not as an error. |
| Autonomy level | the ACMM level the hive runs at, L1 (advice only) to L6 (merges on green CI without a human). |
| Median time-to-merge (median of hive medians) | column of the Autonomy level × outcome table: time from PR opened to merged. See below. |
Each spoke reports, per heartbeat window, the median time-to-merge
(merge.at − merge.opened_at, via evidence.MedianTimeToMerge) of the
merged PRs opened in that window that recorded merge.opened_at, plus that
sample size, as time_to_merge (median_s, n) in its heartbeat evidence
block. Medians cannot be averaged or summed, and the heartbeat carries one
median per hive rather than a distribution, so the hub reports, per ACMM
level, the median of the per-hive medians (by_acmm_level[].time_to_merge:
median_of_hive_medians_s, hives, n; with an even number of hives, the
mean of the two middle medians, rounded down to the second). Each hive counts
once however many PRs it merged; n is the total PRs behind the medians and
is shown under the value. The column header says median of hive medians so
it is not read as a pooled median. A level where no hive reports it — older
builds, or only PRs merged before opened_at was captured — shows
not reported, never 0, and the item stays under Waiting on hives.
The client re-pools a hive / level selection by the same rule
(newEvidenceLevelTimeToMerge in pkg/hub/evidence.go, mirrored by
evidenceLevelTimeToMerge in dashboard.html). Reporting only: nothing reads
it to decide a merge.
The Per-axis criteria table sits below the findings and the evidence
tables. For each selected hive it lists the four quadrant axes (Trust ·
Productivity · Satisfaction · Efficiency) and sorts each axis's criteria into
met, unmet and not reported. It is derived hub-side from the same
criteria the Fleet quadrant card scores (pkg/hub/quadrant_axes.go), so
spokes send nothing new. It is served as hives[].criteria[] on
GET /api/hub/evidence: one entry per axis (axis, label, scored) with
its criteria[] (name, label, status, and display / score when
measured, reason when not). Its rules:
- A measured criterion is met when its 0–100 score is at least 50, that
is, at least halfway from its documented worst value to its documented best
(
evidenceCriterionMetScoreinpkg/hub/evidence_criteria.go; the curves are inpkg/hub/quadrant_curve.go). Below 50 it is unmet. - A criterion with no input is not reported, with the reason. It is never unmet.
- An offline hive, a hive that has not sent an evidence summary, or a hive with no scored axis shows every criterion as not reported.
The table reflects each hive's latest heartbeat, not the selected window. Reporting only: nothing reads it to decide a merge.
Spokes report calibration as six equal-width heartbeat buckets
(int(confidence × 6), 0–5). The spoke's own Evidence card shows five nearest-bin confidence groups and can switch between self-reported and derived scores; heartbeat ranges are unchanged.
The hub view, the findings and the report fold the six buckets into those
four ranges and label each with both the name and the numeric range:
| Hub label | Heartbeat buckets | Spoke card range |
|---|---|---|
| Not confident (0.0–0.5) | 0, 1, 2 | 0.0–0.5 |
| Somewhat confident (0.5–0.67) | 3 | 0.5–0.7 |
| Confident (0.67–0.83) | 4 | 0.7–0.9 |
| Very confident (0.83–1.0) | 5 | 0.9–1.0 |
The bucket edges are sixths, so the hub's 0.67 / 0.83 boundaries approximate
the spoke card's 0.7 / 0.9; the mapping is in evidenceConfidenceRanges
(pkg/hub/evidence_findings.go) and its JS mirror EVIDENCE_CONFIDENCE_RANGES.
Pooled buckets carry bad (wrong calls) as well as n so the hub can
regroup them without re-deriving counts from a rounded rate.
The 📄 Report link in the window row opens the current view — same window,
same hive / level filter — as a standalone plain-language page in a new tab;
⬇ Markdown downloads the same report as a .md file. Both are
GET /api/hub/evidence/report (hub admin / owner only, like
/api/hub/evidence): ?hive=<id>[,<id>…], ?level=<n>, the same
hours= / from= / to= window parameters, format=md (default) or
format=html, and download=1 to receive it as an attachment named
hive-evidence-<hive|fleet>-<from>-<to>.md|html. It is rendered server-side in
Go from the same EvidenceView the modal uses, so every figure reconciles with
what is on screen. Sections, in order: Summary (the What this says
sentences) · What we measured and why · The numbers (each section's
table with the plain labels) · How to read these (margin of error, merged
≠ correct, open PRs count as not merged, recording since 2026-10-08, backfilled
bundles carry no verdicts) · What is not reported yet and why · Grade per
section with the thresholds spelled out · a footer linking back here. The
HTML variant is a single page with inline CSS and no scripts or external
assets; hive names are escaped. Golden files live in
pkg/hub/testdata/evidence_report_*.golden.
Grades and their thresholds (named constants in dashboard.html and mirrored
in pkg/hub/evidence_report.go; Not enough data whenever the graded rate is
below the minimum sample, Not graded where there is no "good" value):
| Section | Graded on | Strong | Fair | Constants |
|---|---|---|---|---|
| Evidence coverage | share of merged PRs (90d) with a record, higher is better | ≥ 80% | ≥ 40% | EVIDENCE_COVERAGE_STRONG/FAIR |
| Reviewer confidence | wrong-call rate in the most-confident measurable range, lower is better (needs ≥ 2 measurable ranges) | ≤ 10% | ≤ 25% | EVIDENCE_ERR_STRONG/FAIR |
| Outcome by author kind | revert rate of agent authors (human if no agents), lower is better | ≤ 5% | ≤ 15% | EVIDENCE_REVERT_STRONG/FAIR |
| Autonomy level × outcome | revert rate of the highest level with enough data, lower is better | ≤ 5% | ≤ 15% | EVIDENCE_REVERT_STRONG/FAIR |
| Boundary enforcement | — (not graded: no block rate is "good" on its own) | |||
| Same- vs cross-family review | false-approve rate of the mix with most verdicts, lower is better | ≤ 10% | ≤ 25% | EVIDENCE_ERR_STRONG/FAIR |
| Per-hive evidence | share of hives reporting for the window, higher is better (no minimum sample) | ≥ 80% | ≥ 40% | EVIDENCE_REPORTING_STRONG/FAIR |
| Shadow gates × outcome | false-alarm rate (would-have-held PRs that merged and stuck) of the gate with most held PRs, lower is better | ≤ 25% | ≤ 50% | EVIDENCE_SHADOW_FALSE_ALARM_STRONG/FAIR |
Limits every explainer repeats: merged is not the same as correct; open PRs
are not failures; closed-without-merge is not recorded and counts as not
merged; reverts are only linked since 2026-10-15 (EVIDENCE_REVERTS_SINCE)
and evidence records only exist since 2026-10-08 (EVIDENCE_DATA_SINCE).
Nothing in this view or the report is read by any merge decision.
A bundle shows what Hive recorded. It does not prove who a reviewer's identity is, does not make the operator's policy accept automated review, and is not a compliance claim of any kind.
pkg/evidence/complexity writes one maintainability snapshot per repo per
month to /data/evidence/<org>/<repo>/complexity/<YYYY-MM>.json: Go
cyclomatic complexity (total, mean, median, p90, max, functions over a
threshold, top hotspots) plus static-analysis finding counts per tool. An
existing month is never overwritten. It is report-only and never blocks a
merge (RFC #12062, #12072, #12363); it feeds the dashboard maintainability
panel and the study.
- Schedule: the hive runs the snapshot itself (spoke side; nothing is
added to the managed repos' CI). A background loop checks hourly which
managed repos have no file for the current month and snapshots only those,
so a pass in a month that is already covered is a
statper repo. The first check runs two minutes after boot. The loop stops with the process and a shutdown waits for an in-flight pass. - Source: the repo's default branch, fetched as a GitHub tarball through
the App (no
giton the spoke, no clone cache). The archive is extracted into a temporary directory that is removed after measuring;refin the snapshot is the default-branch name. - Defaults:
evidence.complexity.enabledis on (it needsevidence.enabledtoo) because a monthly archive download plus an AST pass is cheap.evidence.complexity.static_analysisis off becausego vet/staticcheckcompile the repo and download its modules; turn it on where that cost is acceptable. Analyzer failures are logged and the snapshot is still written without that tool's count. - Retention: the newest 24 monthly files are kept per repo; older ones are
removed after a new month is written.
GET /api/evidence/summaryreturns the newest 12 per repo ascomplexity_by_repo(hotspot lists are left on disk). The review-bundle walkers skip thecomplexity/directory. - Failures: a repo whose snapshot fails (no GitHub client yet, archive error, invalid repo name) is logged and retried on the next hourly check; other repos are unaffected.