feat(review): hold PRs that solve an unlinked open issue - #3513
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
gittensory-ui | 5f37fb26 | Commit Preview URL Branch Preview URL |
Jul 05 2026, 04:56 PM |
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
|
Tip 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 ✅ Gittensory review result - approve/merge recommendedReview updated: 2026-07-05 16:59:05 UTC
✅ Suggested Action - Approve/Merge
Review summary Blockers
Nits — 6 non-blocking
Review context
Contributor next steps
Signal definitions
🟩 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 Gittensory, a quiet PR intelligence layer for OSS maintainers.
|
06d6734 to
8b2cd59
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3513 +/- ##
==========================================
+ Coverage 93.11% 93.15% +0.03%
==========================================
Files 303 307 +4
Lines 31517 31667 +150
Branches 11505 11567 +62
==========================================
+ Hits 29346 29498 +152
Misses 1517 1517
+ Partials 654 652 -2
🚀 New features to boost your workflow:
|
Adds an opt-in, off-by-default guardrail: when a contributor PR links no issue, a cheap deterministic pre-filter looks for open issues whose title/body or changed-file paths overlap it, and any candidate is verified by the free/self-host AI reviewer (never BYOK) before the PR is held for manual review. Never auto-closes and never blocks a PR that has nothing to match against -- only confirmed, high-confidence matches route to a hold, so a maintainer can catch a contributor slicing an issue across unlinked PRs to farm merge-ratio credibility without the scrutiny a linked issue would invite.
…k gate (#3514) pr.linkedIssues is populated by a pure body-text regex match and never checked whether the cited issue is actually open, so linkedIssueGateMode: "block" could be satisfied by citing an already-closed or fabricated issue number. The missing_linked_issue blocker now also fires when every citation resolves to confirmed-closed or not-found, fail-open on any ambiguous fetch so a transient GitHub error never wrongly blocks a legitimately linked PR.
…lose A first confirmed match still only holds the PR for manual review. A second confirmed match by the SAME contributor (tracked via the existing audit_events ledger, hasRecentAuditEvent/recordAuditEvent) is no longer a coincidence worth the benefit of the doubt, so it closes the PR one-shot instead. The close stays tagged closeKind: heuristic (not concrete evidence) since the underlying signal is an AI semantic-match verdict, keeping it subject to the close-precision breaker even after it repeats.
…when close is disabled heldForManualReview didn't account for unlinkedIssueMatchClose, so a confirmed repeat could still satisfy canMerge with close autonomy off. Also closes the remaining codecov/patch branch gaps: the linked-issue-hard-rule reason fallback and the unlinkedIssueGuardrail minConfidence-only sparse override.
8b2cd59 to
5f37fb2
Compare
Closes #3555
Summary
pre-filter (
src/signals/unlinked-issue-candidates.ts) checks the repo's open issues for atitle/body/changed-path overlap with the PR. Any candidate is then verified by the free/self-host AI
reviewer only (
src/review/unlinked-issue-match.ts— never BYOK, mirroringai-review.ts's ownblock-mode rule that BYOK must never affect who gets held/blocked).
src/review/unlinked-issue-guardrail.tsorchestrates all three steps, wired into
src/settings/agent-actions.ts'sheldForManualReviewexactlylike the existing
migrationCollisionHoldpattern) — never auto-closes, never fires when nothingplausibly matches.
tracked via the existing
audit_eventsledger (hasRecentAuditEvent/recordAuditEventindb/repositories.ts— the same general-purpose actor/event-type mechanism already used for thereview-nag cooldown and decision-pack debounce), scoped to the contributor across all repos within a
90-day window (mirroring
submitter-reputation.ts'sREPUTATION_WINDOW_DAYS). The escalated close staystagged
closeKind: "heuristic"(deliberately NOTcloseConcreteEvidence) since the underlying signal isan AI semantic-match verdict — a systematically-wrong match must stay subject to the close-precision
breaker even after it repeats.
.gittensory.yml settings.unlinkedIssueGuardrail: { mode, minConfidence },defaulting fully off. Wired the same way
linkedIssueHardRulesalready is: a normalizer(
src/review/unlinked-issue-guardrail-config.ts), a sparse-partial manifest override insrc/signals/focus-manifest.ts, and a documented (but DB-less, yml-only) OpenAPI shape.avoid the scrutiny a linked issue invites, while still accumulating merge-ratio credibility upstream. This
does not touch that upstream scoring (out of scope for this repo) — it only gives a maintainer visibility
into the specific, narrow pattern (a direct, verified solve of an existing open issue with no link) before
the PR merges, without penalizing the common, legitimate case of a standalone fix with nothing to link,
and without treating one coincidental match as grounds for a close.
canMergewhen thecloseautonomy class is disabled:heldForManualReviewnow also gates oninput.unlinkedIssueMatchClose !== undefined && !acting("close"),falling back to the same manual-review hold a first match gets, with the label/reason/comment surfaced
the same way.
Scope
type(scope): short summaryConventional Commit format.apps/gittensory-ui/public/openapi.jsondiff is thegenerated byproduct of the new OpenAPI schema field, not a UI change).
CONTRIBUTING.md; nosite//CNAME/VitePress changes.Closes #3555.Validation
git diff --checknpm run actionlint— not run; no workflow files touched.npm run typecheck(clean at every stage, including after each rebase)npm run test:coverage(full/unsharded) — not run locally; ran the specific new/changed test filesinstead (
unlinked-issue-guardrail-config,unlinked-issue-candidates,unlinked-issue-match,unlinked-issue-guardrail,focus-manifest,agent-actions,rules,linked-issue-hard-rules, andthe full
queue.test.ts, 574+ tests) — all green.codecov/patchpreviously landed at 98.08%(3 partial branches:
agent-actions.tsline 858'sreason ?? "ineligible linked issue"fallback,and
focus-manifest.ts's minConfidence-only sparse-override branch pair) — closed with 2 new targetedtests, then re-confirmed 100% on every new/changed line and branch across all 9 touched
src/**filesby cross-referencing the exact diff hunk ranges against
coverage-final.jsondirectly.npm run test:workers/npm run build:mcp/npm run test:mcp-pack— not run; nothing in thosesurfaces touched.
npm run ui:openapi:check— not run directly, butnpm run ui:openapiwas run (including after therebase conflict) and its output committed, so check should pass.
npm run ui:lint/ui:typecheck/ui:build— not run; noapps/gittensory-ui/**source touched.npm audit --audit-level=moderate— not run; no dependency changes.paths, the config on/off gate, the already-linked-issue skip, the repeat-detection escalation (first
match holds, second match by the same contributor closes, different contributors/unknown authors never
cross-escalate, the ledger is scoped across repos not per-repo), and the processors.ts wiring. The
escalation integration test in particular caught a real bug during development (a missing
closeRequiresCiStatefield silently made the close action a no-op against green CI) that the unittests alone did not catch — worth noting since it's exactly the kind of gap an end-to-end test exists
to close.
If any required check was skipped, explain why:
test:ci/test:coverage/npm auditpass, since GitHub CI runs the complete gate on push and re-runningthe whole suite by hand for every change is redundant.
Safety
claims.
linkedIssueHardRules-styledocumentation pattern; no new endpoint was added.
UI Evidencesection included.Notes
entrius/gittensor); this guardrail onlyever produces a HOLD (first offense) or a CLOSE (confirmed repeat) for a specific, verified pattern — a
standalone PR with nothing to match against is completely unaffected, and one coincidental AI match is
never enough to close anything.
the
linkedIssueGateMode: blockhard gate previously accepted a citation of an already-CLOSED orfabricated issue number as satisfying "has a linked issue" — that's now verified against the issue's
actual live state.
canMergewhencloseautonomy wasdisabled, since
heldForManualReviewdidn't account forunlinkedIssueMatchClose. Fixed per thesuggested predicate, plus surfaced the same reason/label/comment through the manual-review fallback path
(mirroring the existing
migrationCollisionHoldshape) with 3 new tests covering: hold with onlymergeauto, hold without duplicating the label when
review_state_labelis also acting, and theexplicit-
null-manualReviewLabel case where the PR must never merge at all.