Skip to content

fix(gate): lift the bot's own manual-review hold, and stop drafts wedging the merge train - #9942

Merged
loopover-orb[bot] merged 1 commit into
mainfrom
fix/manual-review-label-provenance
Jul 30, 2026
Merged

fix(gate): lift the bot's own manual-review hold, and stop drafts wedging the merge train#9942
loopover-orb[bot] merged 1 commit into
mainfrom
fix/manual-review-label-provenance

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Closes #9939

Two independent reasons an autonomous queue stalls with no human in it. Both found on live PRs.

1. The manual-review label was a one-way latch

Seen on #9935: the label went on when a finding fired against a stale head, a rebase removed the cause, a fresh escalated review returned zero findings, and the PR was MERGEABLE / CLEAN / fully green. It still would not merge:

agent.action.merge   | denied | manual-review label "manual-review" is present on the live PR — merge not executed
agent.action.approve | denied | manual-review label "manual-review" is present on the live PR — approve not executed

It took a human removing the label by hand — in the mode that is supposed to have no human in it.

The sibling-label cleanup deliberately refuses to touch this label, because the same string is also a maintainer’s manual freeze and the planner had no way to tell them apart. That caution is correct and stays: silently undoing a human’s deliberate hold is far worse than leaving a stale one. The missing piece was never the caution — it was the provenance.

The planner now records the head SHA and reason when it applies the label, and may lift it once manualHoldReason === null — i.e. no reason wants a hold this pass, which is what makes it impossible to lift a label applied for reason A because reason B cleared. A label with no provenance is never touched. That asymmetry is the entire safety property, and it is asserted directly.

Deliberately not scoped to the recorded head on removal: a rebase resolving the cause is the single most common way a hold goes stale, and refusing to lift it there would leave the exact #9935 case unfixed.

2. A draft sibling wedged the merge train

The train already evicts conflicted and manual-review-held siblings, on the principle (#9039) that waiting for a PR not trying to reach merge buys nothing the 24h cap does not already bound far worse.

A draft is the strongest form of that signal — GitHub itself refuses to merge one, so it is not one merge away from landing, it is one author action away from being eligible at all.

The production shape you hit: a maintainer PR with red CI cannot be auto-closed (maintainers are exempt on purpose, so they can iterate), so it stays open and overlapping for as long as the fix takes — holding every newer overlapping PR behind it, green and approved, for up to the full 24 hours. Marking it draft is the author saying "skip me", and the train now understands that.

Eviction removes one PR from the queue; it does not disable the gate. A test pins that a real older sibling behind the draft still holds the line.

Also evicts on GitHub’s own mergeableState === "draft", for any caller that only resolved that field.

Tests

Provenance: lifts a bot-applied hold; never touches a label with no provenance; does not lift while a reason still wants it; no-op when the label is not on the PR; lifting rides the same merge autonomy class as the add it reverses. Repository writers: head-scoped write, both fields cleared together (so a later human-applied label cannot inherit the bot’s provenance), absent reads as null. Executor: writes only for the manual-review label, clears on removal, never writes when the repo disabled the label.

Draft eviction: draft does not block; mergeableState: "draft" also evicts; false/absent unchanged; the next-oldest viable sibling still blocks; draft + manual-review compose.

Changed-line coverage measured at zero uncovered statements and zero uncovered branches. Two unreachable defensive arms were removed rather than pragma-ignored (an "add already planned" guard that manualHoldReason === null already excludes, and a ?? "" on a required field).

…ging the merge train

Two independent reasons an autonomous queue stalls with no human in it, both
found on live PRs.

1) The manual-review label was a ONE-WAY LATCH.

The sibling-label cleanup deliberately refuses to touch it, because the same
label string is also a maintainer's manual freeze and the planner had no way to
tell them apart -- and silently undoing a human's freeze is far worse than
leaving a stale hold. That caution was right; the missing piece was provenance.

Seen on #9935: the label went on when a finding fired against a stale head, a
rebase removed the cause, a fresh escalated review returned ZERO findings, and
the PR was mergeable, green and clean -- and still would not merge:

  agent.action.merge | denied | manual-review label "manual-review" is present
                               on the live PR -- merge not executed

It took a human removing the label by hand, in the mode that is supposed to
have no human in it.

The planner now records the head SHA and reason when IT applies the label, and
may lift it once manualHoldReason is null (no reason wants a hold this pass, so
a label applied for reason A can never be lifted by reason B clearing). A label
with NO provenance -- a human's, or one predating this -- is never touched.
That asymmetry is the whole safety property.

2) A draft sibling wedged the merge train.

The train already evicts conflicted and manual-review-held siblings on the
principle that waiting for a PR which is not trying to reach merge buys nothing
the 24h cap does not already bound far worse. A DRAFT is the strongest form of
that signal: GitHub itself refuses to merge one, so it is not one merge away
from landing, it is one author action away from being eligible at all.

The production shape: a maintainer PR with red CI cannot be auto-closed
(maintainers are exempt on purpose, so they can iterate), so it stays open and
overlapping for as long as the fix takes -- holding every newer overlapping PR
behind it, green and approved, for up to the full 24 hours. Marking it draft is
the author saying "skip me", and the train now understands that.

Eviction removes ONE PR from the queue; it does not disable the gate. A real
older sibling behind the draft still holds the line.
@loopover-orb

loopover-orb Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Tip

✅ LoopOver review result - approve/merge recommended

Review updated: 2026-07-30 19:20:52 UTC

12 files · 1 AI reviewer · no blockers · readiness 88/100 · CI green · clean

✅ Suggested Action - Approve/Merge

  • safe to merge

Review summary
This adds provenance tracking (headSha + reason) for the bot-applied manual-review label so the planner can lift its own stale holds without ever touching a maintainer's manual freeze, plus evicts draft siblings from the merge train's wait logic. The schema change is correctly paired with a contiguous, D1-safe migration (two plain ADD COLUMN statements, no temp objects/pragmas/transactions), and the safety property (null provenance = never touched) is asserted directly in tests. The removal-path guard in agent-action-executor.ts only fires when `ctx.manualReviewLabel && action.label === ctx.manualReviewLabel`, which correctly scopes the provenance clear to the manual-review label specifically, matching the intended invariant.

Nits — 6 non-blocking
  • The `300` char bound in `markPullRequestManualReviewLabelApplied` (src/db/repositories.ts:4747) is an unexplained magic number; consider a named constant matching the convention of other bounded-string limits in this file.
  • `test/unit/agent-action-executor.test.ts:88` uses `getGlobalContributorBlacklist`/`blacklist` terminology inherited from existing code, not introduced by this diff, so it's pre-existing rather than a new issue.
  • The `manualReviewLabelAppliedSha` lift condition in agent-actions.ts is not scoped to the recorded head, which is called out explicitly in the comments as deliberate — worth confirming reviewers agree a rebase-driven false-clear can't reintroduce a different real hold silently (the reason-based guard via `manualHoldReason === null` should cover this, but it's a subtle invariant worth a second pair of eyes).
  • Consider extracting the `300` literal in repositories.ts:4747 into a named constant like `MANUAL_REVIEW_LABEL_REASON_MAX_CHARS` for consistency with `MAX_STORED_BODY_CHARS` etc.
  • The PR title references 'the bot's own manual-review hold' and 'drafts wedging the merge train' — both are covered by the diff and tied to issue The manual-review label is a one-way latch: nothing lifts it when the hold that applied it clears #9939, so scope matches intent well.
  • 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.

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ✅ Gate result — Passing (No configured blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #9939
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ❌ 8/20 High review scope from cached public metadata (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 9 registered-repo PR(s), 8 merged, 280 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 9 PR(s), 280 issue(s).
Improvement ✅ Minor risk: clean · value: minor · LLM: significant
Linked issue satisfaction

Addressed
The PR implements exactly the fix requested: it adds provenance fields (manualReviewLabelAppliedSha/Reason) recorded only when the planner itself applies the label, adds a new planner rule that lifts the label only when provenance exists AND no hold reason remains this pass (scoped to reason, not head), and leaves labels without provenance untouched, matching the issue's stated safety property and

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: Python, TypeScript, Ruby, Go, MDX, Shell, Solidity, JavaScript
  • Official Gittensor activity: 9 PR(s), 280 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • Then work through the remaining 2 steps in the Signals table above.
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.

🟩 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.

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

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.87%. Comparing base (7e0897c) to head (8d7d184).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #9942   +/-   ##
=======================================
  Coverage   91.87%   91.87%           
=======================================
  Files         923      923           
  Lines      113461   113474   +13     
  Branches    27343    27348    +5     
=======================================
+ Hits       104244   104257   +13     
  Misses       7928     7928           
  Partials     1289     1289           
Flag Coverage Δ
backend 95.68% <100.00%> (+<0.01%) ⬆️

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.78% <100.00%> (+<0.01%) ⬆️
src/db/schema.ts 74.87% <ø> (ø)
src/queue/processors.ts 94.60% <ø> (ø)
src/review/merge-train.ts 100.00% <100.00%> (ø)
src/services/agent-action-executor.ts 96.90% <100.00%> (+0.04%) ⬆️
src/settings/agent-actions.ts 98.28% <100.00%> (+0.01%) ⬆️
src/types.ts 100.00% <ø> (ø)

@loopover-orb loopover-orb Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoopOver approves — the gate is satisfied and CI is green.

@loopover-orb
loopover-orb Bot merged commit 35fceed into main Jul 30, 2026
10 checks passed
@loopover-orb
loopover-orb Bot deleted the fix/manual-review-label-provenance branch July 30, 2026 19:21
JSONbored added a commit that referenced this pull request Jul 30, 2026
#9942 added the release guarded on `manualHoldReason === null`, and stated that
no add site could be live at that point. That ternary covers only the guardrail
hit, unverified CI, an action_required conclusion and a not-review-good verdict.

Every would-MERGE hold -- migration collision, unlinked-issue match, priority
eligibility, unlinked-issue close, unstable merge state -- fires on `reviewGood`
with a SUCCESS conclusion, which is precisely when manualHoldReason is null. So
the release stripped the label while the hold was still standing, and that label
is exactly what the executor checks to deny merge and approve: removing it
removes the enforcement for a hold nothing has resolved.

Verified against the merged planner before changing it -- all five holds planned
a removal.

The condition is now one named predicate listing every reason that would ADD the
label, so a hold added later cannot silently fail to suppress the release. The
counterweight is tested too: with nothing holding, the label is still released,
so the one-way latch #9942 fixed does not come back.
JSONbored added a commit that referenced this pull request Jul 30, 2026
…ive (#9943)

#9942 added the release guarded on `manualHoldReason === null`, and stated that
no add site could be live at that point. That ternary covers only the guardrail
hit, unverified CI, an action_required conclusion and a not-review-good verdict.

Every would-MERGE hold -- migration collision, unlinked-issue match, priority
eligibility, unlinked-issue close, unstable merge state -- fires on `reviewGood`
with a SUCCESS conclusion, which is precisely when manualHoldReason is null. So
the release stripped the label while the hold was still standing, and that label
is exactly what the executor checks to deny merge and approve: removing it
removes the enforcement for a hold nothing has resolved.

Verified against the merged planner before changing it -- all five holds planned
a removal.

The condition is now one named predicate listing every reason that would ADD the
label, so a hold added later cannot silently fail to suppress the release. The
counterweight is tested too: with nothing holding, the label is still released,
so the one-way latch #9942 fixed does not come back.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The manual-review label is a one-way latch: nothing lifts it when the hold that applied it clears

1 participant