Skip to content

fix(gate): stop two subsystem failures from becoming wrong auto-verdicts (#9460, #9462) - #9501

Merged
JSONbored merged 3 commits into
mainfrom
fix/gate-integrity-verdict-bypasses
Jul 28, 2026
Merged

fix(gate): stop two subsystem failures from becoming wrong auto-verdicts (#9460, #9462)#9501
JSONbored merged 3 commits into
mainfrom
fix/gate-integrity-verdict-bypasses

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

Two verified paths by which a subsystem failure became a verdict instead of a "cannot determine". Both are from the 2026-07-27 adversarial audit, and both are live on this deployment's current configuration.

The codebase already models the correct shape in pre-merge-checks.ts:9-13,38-49 — an unresolvable enforced check yields PRE_MERGE_CHECK_UNRESOLVED_CODE → NEUTRAL → held, documented explicitly as "never silently skipping a hard requirement (auto-merge bypass)". Neither of these two followed it.

Closes #9460
Closes #9462

#9460 — an AI blocker silently collapsed to a clean pass (false merge)

synthesizeDefect returns null for two situations that must not share an outcome: nobody named a real blocker (a genuine clean pass), and a real blocker whose title toPublicSafe refuses to publish. The single and synthesis strategies collapsed both to {defect: null, split: false, inconclusive: false} — indistinguishable from "the reviewer found nothing" — so the PR auto-merged with the defect unreported, and the row was cached durably.

single is this deployment's live strategy (AI_PROVIDER=claude-code,ollamaresolveAiReviewerPlan, src/selfhost/ai.ts:1596-1613). consensus already failed closed via its own split arm and is untouched.

Both strategies now resolve to inconclusive — deliberately, rather than split: the situation genuinely is "no usable public verdict for this head", which is ai_review_inconclusive's own semantics and copy, and routes to a neutral gate → human hold. ai_review_split's copy instead asserts a disagreement between two reviewers that never happened, which would be actively misleading in single-reviewer mode.

A blank-only blockers array is still not a flag (realBlockersOf filters it) — that stays a clean pass, unchanged.

One correction to the issue as filed: its example (computeScore divides by zero) would not actually be filtered — BARE_SCORE_TERM_PATTERN is /\bscore\w*\b/i and computeScore has no word boundary before Score. What genuinely trips it is reward/rewards/payout/farming/ranking/cohort/reviewability (plain .includes()) and a bare score/scores/scored/scorer. All are now pinned as regression tests, and the camelCase non-match is pinned too so any future widening of the pattern is a deliberate, visible decision. Also worth noting: synthesizeDefect calls toPublicSafe with no options, so the gate-bearing title is filtered even on a repo in LOOPOVER_PUBLIC_SCORE_TERMS_ALLOWED_REPOS — the allowlist never rescued this path.

#9462 — a stuck capture marker silently disabled the screenshot gate (false merge)

Two compounding defects:

The marker outlived its retries. scheduleVisualCaptureRetry's budget-exhausted early return only skipped writing visualCaptureRetryPendingSha again — the previous attempt's marker stayed. The only other clear (markPullRequestVisualCaptureSatisfied) requires a successful capture, which is by definition the thing not happening. Verified by exhaustive grep: exactly one clear and one setter across src/. So a head whose capture never succeeded stayed marked forever, and the doc comment claiming the gate "falls through to its normal (accurate) evaluation" was false in code.

The deferral skipped instead of holding. With the marker set, screenshotTableMatch was left undefined and the plan fell through to ordinary merit/CI evaluation — so a green PR planned a merge, with no finding, hold, or blocker representing "screenshot evidence unresolved". That is a disposition inversion: a PR the gate would have closed merged instead.

The gate is configured action: close for all three repos in the private server-side config, and was re-enabled two commits before the audit (73d4e94, #9455).

Scope note — #9464 deliberately excluded

The audit also found a false-close sibling (browserless render failures being swallowed). I implemented it, discovered it was wrong, and backed it out rather than ship it.

Inferring the failure downstream from "routes resolved but zero rendered pairs" cannot distinguish a browserless blip from a capture that legitimately found nothing renderable — and it broke the existing #4110 test covering exactly that legitimate case, where the gate should close. #9464's real deliverable is to surface an explicit renderFailed signal from capture.ts where the failure is actually known. That needs its own change and its own tests, so it stays open for a follow-up.

Validation

  • npx tsc --noEmit -p tsconfig.json — clean
  • test/unit/ai-review.test.ts, test/unit/agent-actions.test.ts, test/unit/queue-3.test.ts740 passed

New regression tests:

  • combineReviews: unpublishable blocker holds in single, synthesis/either, and synthesis/both; four parameterised real-vocabulary titles; publishable blocker still yields a defect; camelCase identifier still publishable; blank-only blockers still pass.
  • Planner: unresolved evidence plans neither merge nor close; holds even with a green gate and merge autonomy (the exact bypass); does not hold for owner/admin/automation-bot; an explicit false or absent flag leaves the close path untouched.

One test changed rather than added: "synthesized defect drops a blocker whose only finding is blank or unsafe (fail-safe...)" asserted inconclusive: false for the unsafe case — i.e. it pinned the bug as correct behaviour, using the fixture literal "Boost your reward payout". It has been split so the blank case keeps its old assertion and the unsafe case asserts the hold.

Coverage gap I want to flag honestly

The marker-clearing path (clearPullRequestVisualCaptureRetryPending and its call site) has no direct test. The existing #9030 exhaustion test starts from a PR that never had a marker written, so it exercises the early return but not the clear. The test that would cover it is the marker-SET-then-budget-EXHAUSTED sequence; I drafted it, found it needed a fetch-stub helper that does not exist (the neighbouring tests inline their stubs), and removed it rather than commit a broken file. Happy to add it in this PR before merge — flagging rather than letting codecov/patch surface it.

…cts (#9460, #9462)

An unpublishable AI blocker and a stuck visual-capture marker each let a subsystem failure
resolve as a verdict rather than as "cannot determine". pre-merge-checks already models the
correct shape (an unresolvable enforced check -> NEUTRAL -> hold, never a silent auto-merge
bypass); neither of these followed it.

combineReviews (#9460): synthesizeDefect returns null both when nobody flagged anything and
when a real blocker's title is unpublishable, and single/synthesis collapsed both to a clean
pass -- so a blocker phrased with ordinary review vocabulary (reward/ranking/cohort, or a bare
score term) auto-MERGED the change it blocked. single is this deployment's live strategy. Both
strategies now hold; consensus already failed closed and is untouched. Note synthesizeDefect
calls toPublicSafe with no options, so the gate-bearing title is filtered even on a repo in
LOOPOVER_PUBLIC_SCORE_TERMS_ALLOWED_REPOS -- the allowlist never rescued this path.

Visual capture (#9462): scheduleVisualCaptureRetry's budget-exhausted early return only
skipped re-writing visualCaptureRetryPendingSha, leaving the previous attempt's marker standing
forever (the sole other clear needs a successful capture, which by definition never comes). A
permanently-marked head silently disabled the screenshot gate's close -- a permanent bypass out
of what was designed as a temporary deferral. The marker is now cleared on exhaustion, and the
unresolved state is threaded into the planner so the deferral holds the PR rather than falling
through to a merge.
@loopover-orb

loopover-orb Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Caution

🛑 LoopOver review result - fixes required

Review updated: 2026-07-28 00:36:48 UTC

8 files · 1 AI reviewer · no blockers · CI failing · unstable

🛑 Suggested Action - Manual Review

  • Required AI review was skipped by a submitter-reputation downgrade: This repository requires blocking AI review, and the submitter's recent-submission signal downgraded this PR to deterministic-only checks. Those checks do not read code semantics, so the gate is held for human review instead of passing automatically.

Review summary
This PR closes two real fail-open gate bugs: (1) `synthesizeDefect` returning `null` both for "nobody flagged anything" and "a real blocker whose title got redacted by `toPublicSafe`" — the fix in `defectOrHold` distinguishes them by checking `realBlockersOf` first, then routing an unpublishable-but-real blocker to `inconclusive` instead of a silent clean pass; (2) `scheduleVisualCaptureRetry` deferring a screenshot-table CLOSE for a pending capture retry but leaving `screenshotTableMatch` undefined, which let the plan fall through to a MERGE — fixed by threading `screenshotTableEvidenceUnresolved` through `AgentActionPlanInput` to force a hold. Both fixes are traced end-to-end (call sites, live strategy config, planner) and are backed by targeted regression tests covering the exact previously-broken paths (single/synthesis strategies, contributor vs owner/admin/bot standing, green-CI auto-merge scenario). The new `clearPullRequestVisualCaptureRetryPending` repository function correctly scopes its `WHERE` on `visualCaptureRetryPendingSha` (not headSha) per its own doc comment, closing the marker-permanence leak once the retry budget is exhausted.

Nits — 7 non-blocking
  • The size-smell flags on `src/db/repositories.ts` and `src/queue/processors.ts` reflect pre-existing file size, not something this diff introduces, so they're not actionable here.
  • src/queue/processors.ts: the new best-effort clear on retry-budget exhaustion uses `console.log` for its failure path — consistent with the file's existing pattern elsewhere, but worth confirming that's still the house convention rather than a structured logger.
  • The two-issue title/description bundling (gate: AI blocker silently collapses to a clean pass when its text trips the public-safe filter (false merge, live config) #9460 and gate(visual): capture-retry marker never cleared + pending marker skips instead of holds → screenshot gate silently disabled (false merge) #9462) touches four production files plus their schema/settings-plumbing counterparts in one PR; each fix is independently reviewable and could have shipped as separate PRs for a tighter blast radius, though the coupling (both are "subsystem failure → wrong auto-verdict" fixes) is a reasonable rationale for bundling.
  • Consider a follow-up test asserting `clearPullRequestVisualCaptureRetryPending`'s scoping behavior directly against the repository layer (a row whose head has since moved on) to pin the intentional non-headSha-scoped WHERE clause described in its comment.
  • The PR states both issues are 'live on this deployment's current configuration' (single-strategy AI_PROVIDER) — worth double-checking that CI's FAILED `validate`/`validate-tests` checks (no detail provided) aren't masking something in this exact area before merge, since a rebase/retry would clarify whether it's environmental or content-related.
  • PR author also opened the linked issue — Link an issue that was opened by a different contributor, or provide a rationale for why this self-authored issue represents genuine discovery work.
  • Required AI review was skipped by a submitter-reputation downgrade — Review this PR manually, or run AI review with a trusted override, before merging.

CI checks failing

  • codecov/patch — 85.71% of diff hit (target 99.00%)

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ⚠️ Gate result — Not blocking (Advisory; not blocking this PR.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #9460, #9462
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (2 linked issues).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 14 registered-repo PR(s), 13 merged, 346 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 14 PR(s), 346 issue(s).
Improvement ✅ Minor risk: clean · value: minor
Linked issue satisfaction

Addressed
The diff adds a defectOrHold helper used by both single and synthesis branches that converts a real-but-unpublishable blocker into an inconclusive hold (leaving consensus untouched), adds a counter increment at the drop site, updates the doc comment, and includes tests covering single/synthesis(either/both)/consensus-regression/publishable-blocker cases as required.

Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: not available
  • Official Gittensor activity: 14 PR(s), 346 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Treat this as maintainer-lane context rather than normal contributor-lane activity.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask <question> answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat <question> answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

Decision record
  • action: merge · clause: success
  • config: f7691d178efac6ee89bb51b01b9b2836f1ab43239fc6e3d83ff0a804c6777123 · pack: oss-anti-slop · ci: passed
  • record: 1ba40e73427a2ea19b4c8a4abc0faaad664c073a153e729adb7ee85241915477 (schema v5, head 88486aa)

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Logic backtest

Replayed 0 historical case(s) for linked_issue_scope_mismatch through the base (6571a1b) and head (88486aa) versions of its detection logic (corpus checksum 4f53cda18c2b).

Backtest comparison: linked_issue_scope_mismatch

Verdict: unchanged — no comparable axis moved.

Advisory only — this check never blocks merge (#8105).

@JSONbored JSONbored self-assigned this Jul 27, 2026
@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.71429% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.62%. Comparing base (6571a1b) to head (88486aa).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/queue/processors.ts 62.50% 1 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9501      +/-   ##
==========================================
+ Coverage   89.55%   93.62%   +4.06%     
==========================================
  Files         843      741     -102     
  Lines      110073    60428   -49645     
  Branches    26194    21300    -4894     
==========================================
- Hits        98573    56574   -41999     
+ Misses      10238     2901    -7337     
+ Partials     1262      953     -309     
Flag Coverage Δ
backend 93.62% <85.71%> (-1.65%) ⬇️
control-plane ?
engine ?
rees ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/db/repositories.ts 96.79% <100.00%> (+<0.01%) ⬆️
src/selfhost/metrics.ts 100.00% <ø> (ø)
src/services/ai-review.ts 97.01% <100.00%> (+0.01%) ⬆️
src/settings/agent-actions.ts 98.58% <100.00%> (+0.01%) ⬆️
src/queue/processors.ts 94.74% <62.50%> (-0.08%) ⬇️

... and 275 files with indirect coverage changes

@loopover-orb loopover-orb Bot added gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. manual-review Gittensor contributor context labels Jul 27, 2026
…ETRIC_META (#9460)

The drift guard in test/unit/selfhost-metrics.test.ts requires every literal metric name
emitted from src/ to carry a registered meta entry; the counter added alongside the
combineReviews hold had none, failing the backend coverage suite.
…#9462)

The existing #9030 exhaustion test starts from a PR that never had a marker written, so it
exercises the early return but not the defect. This drives the real failing sequence -- attempt
0 errors and writes the marker, then the final attempt exhausts the budget with the pipeline
still failing -- and asserts the marker does not outlive the retries that justified it.

Verified to fail against the unfixed code (expected 'vis62' to be null), so it pins the fix
rather than merely passing alongside it. Closes the codecov/patch gap flagged on the PR.
@JSONbored

Copy link
Copy Markdown
Owner Author

Coverage gap closed. The marker-clearing path now has a direct regression test (`screenshot-table gate (#9462): a marker written by an earlier attempt is CLEARED once the budget is exhausted`).

It drives the real failing sequence the existing #9030 test could not reach — attempt 0 errors and writes the marker, then the final attempt exhausts the budget with the pipeline still failing — rather than starting from a PR that never had a marker written.

Verified it actually pins the fix: with the clear neutralised it fails with expected 'vis62' to be null, and passes with the fix in place. 158 tests green in that file.

@JSONbored
JSONbored merged commit a0d8e2e into main Jul 28, 2026
8 of 9 checks passed
@JSONbored
JSONbored deleted the fix/gate-integrity-verdict-bypasses branch July 28, 2026 00:46
JSONbored added a commit that referenced this pull request Jul 28, 2026
…9504/#9505 merges (#9509)

* test(gate): cover the fail-safe marker-clear path (#9462)

Mirrors the sibling visual-capture-satisfied fail-safe test: a rejecting clear must not throw
out of the pass, since a failed clear only means the gate stays deferred until the head moves.

(cherry picked from commit efa6472)

* refactor(gate): drop an unreachable spread arm and annotate the defensive head-SHA guard (#9462)

screenshotTableEvidenceUnresolved is computed as a plain boolean, so the undefined arm of its
conditional spread was dead code -- pass it directly. The head-SHA guard in the exhausted branch
is genuinely defensive (a recapture-preview job is only minted for a PR that had one), matching
the sibling mark write, so it carries a reason rather than a contrived test.

(cherry picked from commit 4f59499)

* test(ai): exclude two unreachable best-effort cleanup arms from coverage (#9479)

rm with force:true does not throw for a missing path, so the catch needs a filesystem-level
failure no unit test can portably induce; the unset-cwd guard is only reachable when mkdtemp
itself threw, i.e. no directory was ever created. Both annotated with their reason, matching
the file's existing convention.

(cherry picked from commit bf3efe6)

* test(ai): drive the real subprocess EPIPE path and the non-Error stall guard (#9476, #9479)

The EPIPE regression uses the existing real-subprocess harness rather than a stubbed spawn, so
the stdin error listener is genuinely exercised: a fake CLI exits immediately without draining
stdin, and the test asserts no uncaughtException reaches the process. Verified to fail without
the listener (it captures an uncaught EPIPE), which is precisely the crash that took down every
in-flight queue job in the container.

Also covers isStalledNoOutput rejecting a non-Error throw -- a provider adapter can reject with
a string, and misclassifying that as a deadline signal would silently skip the retry budget.

(cherry picked from commit c770cc1)

* test(rate-limit): cover the inline-retry break for an over-budget Retry-After (#9494)

Asserts the attempt COUNT, not just the classification: a 60s Retry-After yields exactly one
attempt instead of four, and a short one still uses the full inline budget so the break stays
narrow.

(cherry picked from commit 92813bf)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. manual-review Gittensor contributor context

Projects

None yet

1 participant