Skip to content

feat(review): hold PRs that solve an unlinked open issue - #3513

Merged
JSONbored merged 4 commits into
mainfrom
claude/zealous-herschel-f6bfba
Jul 5, 2026
Merged

feat(review): hold PRs that solve an unlinked open issue#3513
JSONbored merged 4 commits into
mainfrom
claude/zealous-herschel-f6bfba

Conversation

@JSONbored

@JSONbored JSONbored commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Closes #3555

Summary

  • New opt-in, off-by-default guardrail: when a contributor PR links no issue, a cheap deterministic
    pre-filter (src/signals/unlinked-issue-candidates.ts) checks the repo's open issues for a
    title/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, mirroring ai-review.ts's own
    block-mode rule that BYOK must never affect who gets held/blocked).
  • A FIRST confirmed match holds the PR for manual review (src/review/unlinked-issue-guardrail.ts
    orchestrates all three steps, wired into src/settings/agent-actions.ts's heldForManualReview exactly
    like the existing migrationCollisionHold pattern) — never auto-closes, never fires when nothing
    plausibly matches.
  • A CONFIRMED REPEAT by the SAME contributor escalates to an actual CLOSE instead of another hold. This is
    tracked via the existing audit_events ledger (hasRecentAuditEvent/recordAuditEvent in
    db/repositories.ts — the same general-purpose actor/event-type mechanism already used for the
    review-nag cooldown and decision-pack debounce), scoped to the contributor across all repos within a
    90-day window (mirroring submitter-reputation.ts's REPUTATION_WINDOW_DAYS). The escalated close stays
    tagged closeKind: "heuristic" (deliberately NOT closeConcreteEvidence) since the underlying signal is
    an AI semantic-match verdict — a systematically-wrong match must stay subject to the close-precision
    breaker even after it repeats.
  • New per-repo config-as-code knob, .gittensory.yml settings.unlinkedIssueGuardrail: { mode, minConfidence },
    defaulting fully off. Wired the same way linkedIssueHardRules already is: a normalizer
    (src/review/unlinked-issue-guardrail-config.ts), a sparse-partial manifest override in
    src/signals/focus-manifest.ts, and a documented (but DB-less, yml-only) OpenAPI shape.
  • Motivation: a contributor can currently slice one tracked issue across several small, unlinked PRs to
    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.
  • A confirmed repeat can no longer silently reach canMerge when the close autonomy class is disabled:
    heldForManualReview now also gates on input.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

  • The PR title follows type(scope): short summary Conventional Commit format.
  • This PR is focused (backend/engine only — the apps/gittensory-ui/public/openapi.json diff is the
    generated byproduct of the new OpenAPI schema field, not a UI change).
  • Follows CONTRIBUTING.md; no site//CNAME/VitePress changes.
  • Linked issue: Closes #3555.

Validation

  • git diff --check
  • npm 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 files
    instead (unlinked-issue-guardrail-config, unlinked-issue-candidates, unlinked-issue-match,
    unlinked-issue-guardrail, focus-manifest, agent-actions, rules, linked-issue-hard-rules, and
    the full queue.test.ts, 574+ tests) — all green. codecov/patch previously landed at 98.08%
    (3 partial branches: agent-actions.ts line 858's reason ?? "ineligible linked issue" fallback,
    and focus-manifest.ts's minConfidence-only sparse-override branch pair) — closed with 2 new targeted
    tests, then re-confirmed 100% on every new/changed line and branch across all 9 touched src/** files
    by cross-referencing the exact diff hunk ranges against coverage-final.json directly.
  • npm run test:workers / npm run build:mcp / npm run test:mcp-pack — not run; nothing in those
    surfaces touched.
  • npm run ui:openapi:check — not run directly, but npm run ui:openapi was run (including after the
    rebase conflict) and its output committed, so check should pass.
  • npm run ui:lint / ui:typecheck / ui:build — not run; no apps/gittensory-ui/** source touched.
  • npm audit --audit-level=moderate — not run; no dependency changes.
  • New/changed behavior has unit + integration tests for every new branch, including the fail-closed AI
    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
    closeRequiresCiState field silently made the close action a no-op against green CI) that the unit
    tests 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:

  • Local validation here was typecheck + the specific affected test files rather than a full local
    test:ci/test:coverage/npm audit pass, since GitHub CI runs the complete gate on push and re-running
    the whole suite by hand for every change is redundant.

Safety

  • No secrets, wallets, hotkeys, coldkeys, trust scores, or private scoring values are touched or exposed.
  • Public-facing text (the hold/close comments) stays sanitized and factual — no compensation/optimization
    claims.
  • Not an auth/CORS/session change; no negative-path tests needed for that reason.
  • API/OpenAPI updated (new schema field) and covered by the existing linkedIssueHardRules-style
    documentation pattern; no new endpoint was added.
  • Not a UI change (N/A) — no UI Evidence section included.
  • No changelog edit.

Notes

  • The actual credibility/reward number stays computed upstream (entrius/gittensor); this guardrail only
    ever 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.
  • A related, independent fix (fix(review): require a genuinely open issue for the linked-issue block gate #3514, already merged into this branch) closed a separate pre-existing gap:
    the linkedIssueGateMode: block hard gate previously accepted a citation of an already-CLOSED or
    fabricated issue number as satisfying "has a linked issue" — that's now verified against the issue's
    actual live state.
  • Addressed prior review feedback: a confirmed repeat could satisfy canMerge when close autonomy was
    disabled, since heldForManualReview didn't account for unlinkedIssueMatchClose. Fixed per the
    suggested predicate, plus surfaced the same reason/label/comment through the manual-review fallback path
    (mirroring the existing migrationCollisionHold shape) with 3 new tests covering: hold with only merge
    auto, hold without duplicating the label when review_state_label is also acting, and the
    explicit-null-manualReviewLabel case where the PR must never merge at all.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 5, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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-security

Copy link
Copy Markdown
Contributor

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

@loopover-orb loopover-orb Bot added the gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. label Jul 5, 2026
@loopover-orb

loopover-orb Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Tip

🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩

✅ Gittensory review result - approve/merge recommended

Review updated: 2026-07-05 16:59:05 UTC

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

✅ Suggested Action - Approve/Merge

  • safe to merge

Review summary
This PR adds an opt-in unlinked-issue guardrail with deterministic candidate filtering, AI verification, planner holds/closes, and config/OpenAPI wiring. The core flow is mostly coherent and defaults off, but the PR does not clearly close or link an eligible open issue for this new feature surface, which violates this repo's contributor scope rule. I also see one planner edge case where a confirmed repeat can fall through to merge when close autonomy is disabled instead of at least being held.

Blockers

  • PR description: this feature PR does not close or clearly link an eligible open issue in this repo; add an explicit `Closes #NNN` / `Fixes #NNN` reference to the maintainer-authorized issue or explain the existing issue authorization before this can be considered merge-ready.
  • src/settings/agent-actions.ts:710: `unlinkedIssueMatchViolated` is gated on `acting("close")`, but `heldForManualReview` at src/settings/agent-actions.ts:687 ignores `input.unlinkedIssueMatchClose`, so a confirmed repeat can still satisfy `canMerge` when close autonomy is disabled; change the hold predicate to include the close disposition, e.g. `const heldForManualReview = guardrailHit || input.migrationCollisionHold !== undefined || input.unlinkedIssueMatchHold !== undefined || (input.unlinkedIssueMatchClose !== undefined && !acting("close"));` and use that reason in the manual-review label path.
Nits — 6 non-blocking
  • src/review/unlinked-issue-match.ts:42 uses a Unicode ellipsis in the truncated diff marker while this repo generally keeps source ASCII; use `... (diff truncated)` unless there is a reason this needs a non-ASCII character.
  • src/signals/unlinked-issue-candidates.ts:52 repeats `"would"` in `STOPWORDS`, which is harmless but makes the curated list look less intentional.
  • src/signals/unlinked-issue-candidates.ts:67 matches changed-path basenames with raw `includes`, so `foo.ts` can qualify on unrelated prose or longer filenames; consider token/boundary matching for basenames while keeping full-path matching exact.
  • src/review/linked-issue-hard-rules.ts:229 fans out one live issue fetch per linked number with `Promise.all`; this is acceptable for normal PR bodies, but consider reusing the existing overflow/bound semantics or documenting the expected cap so a noisy body cannot create surprise API pressure.
  • src/settings/agent-actions.ts:684 should treat an unlinked repeat as non-mergeable even when close is disabled, falling back to the same manual-review path as a first match.
  • 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.
Signal Result Evidence
Code review ✅ No blockers 1 reviewer
Linked issue ✅ Linked #3555
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 (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 56 registered-repo PR(s), 46 merged, 521 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 56 PR(s), 521 issue(s).
Gate result ✅ Passing No configured blocker found.
Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository registration is not available in the local Gittensory cache.
  • Public profile languages: Python, TypeScript, JavaScript, Ruby, Go, Kotlin, MDX, Shell
  • Official Gittensor activity: 56 PR(s), 521 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • No action.
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.

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

  • Re-run Gittensory review

@codecov

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.15%. Comparing base (eba476d) to head (5f37fb2).
✅ All tests successful. No failed tests found.

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     
Files with missing lines Coverage Δ
src/openapi/schemas.ts 100.00% <ø> (ø)
src/queue/processors.ts 93.57% <100.00%> (+0.02%) ⬆️
src/review/linked-issue-hard-rules.ts 98.07% <100.00%> (+0.40%) ⬆️
src/review/unlinked-issue-guardrail-config.ts 100.00% <100.00%> (ø)
src/review/unlinked-issue-guardrail.ts 100.00% <100.00%> (ø)
src/review/unlinked-issue-match.ts 100.00% <100.00%> (ø)
src/rules/advisory.ts 97.27% <100.00%> (+0.02%) ⬆️
src/settings/agent-actions.ts 97.47% <100.00%> (+1.32%) ⬆️
src/signals/focus-manifest.ts 98.98% <100.00%> (+0.01%) ⬆️
src/signals/unlinked-issue-candidates.ts 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

JSONbored added 4 commits July 5, 2026 09:53
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.
@JSONbored
JSONbored force-pushed the claude/zealous-herschel-f6bfba branch from 8b2cd59 to 5f37fb2 Compare July 5, 2026 16:54
@JSONbored JSONbored removed the manual-review Gittensor contributor context label Jul 5, 2026
@JSONbored
JSONbored merged commit 9d321f6 into main Jul 5, 2026
12 checks passed
@JSONbored
JSONbored deleted the claude/zealous-herschel-f6bfba branch July 5, 2026 17:35
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.

Development

Successfully merging this pull request may close these issues.

feat(review): guard against credibility gate farming via unlinked-issue PRs

1 participant