Skip to content

feat(agent-actions): configurable per-contributor open PR/issue cap with auto-close over the limit #2270

Description

@JSONbored

Parent: #1936

Motivation

There is currently no per-contributor limit on open PRs or open issues. A single contributor can open many PRs simultaneously (observed: 5 at once), which burns review-queue capacity and creates disproportionate review pressure from one person. There's precedent for removing a cruder version of this: the disposition planner's own code comment states "submission volume is NOT a hold reason... anti-farming-by-manual-hold removed" — that was a silent quality-based hold, which is a different and worse mechanism than what's being requested here. This feature should be the opposite: an explicit, contributor-visible cap with a clear close reason, not a silent hold — closer in spirit to the existing contributor-blacklist feature than to the removed heuristic.

Requirements

  • Config-driven per this project's convention: default disabled, enableable globally (env var) or per-repo (.gittensory.yml / repository_settings, following the yml > DB > defaults precedence used everywhere else).
  • Two independently configurable numeric caps: max open PRs per contributor, max open issues per contributor.
  • When a contributor is at or over a cap and opens another item, the newest item(s) above the cap are auto-closed with a clear, deterministic reason — the oldest N stay open. (Scope: per-repo, matching how every other contributor-facing control in this codebase — blacklist, autonomy, gate config — is repo-scoped, not fleet-wide across an operator's installed repos.)
  • Owner and automation-bot authors are exempt, matching every other auto-close mechanism in the codebase.
  • Must not be defeat-able by a burst (opening several PRs in the same second) — the enforcement point can't only run on a slow periodic sweep.

Deliverables

  • Type + settings: add contributorOpenPrCap?: number | null and contributorOpenIssueCap?: number | null to RepositorySettings (src/types.ts), null/absent = disabled (matches the nullable-numeric-threshold pattern already used for other opt-in caps).
  • Config-as-code: document and parse settings.contributorOpenPrCap / settings.contributorOpenIssueCap in .gittensory.yml (add to FocusManifestSettings's field list and parseSettingsOverride, following the exact template used for every other settings field — see the sibling issue on commandAuthorization for a worked example of this exact wiring gap to avoid repeating).
  • DB: migration adding the two nullable integer columns to repository_settings; thread through resolveEffectiveSettings/getRepositorySettings the same way every other settings field is threaded.
  • Global env var override: e.g. CONTRIBUTOR_OPEN_PR_CAP_DEFAULT / CONTRIBUTOR_OPEN_ISSUE_CAP_DEFAULT, so an operator can enable this fleet-wide without touching every repo's config, with per-repo config able to override (including explicitly disabling) the global default.
  • Enforcement — PR path: on a PR opened webhook (and as a backstop, the existing maintenance sweep for anything the webhook path missed), count the author's current open PRs on the repo; if opening this PR pushes them over contributorOpenPrCap, plan a label + close action mirroring the blacklist short-circuit in src/settings/agent-actions.ts (blacklistContributor/blacklistMatch block) — add a new closeKind: "contributor_cap" (parallel to the existing "blacklist" and "linked-issue-hard-rule" deterministic kinds), so it's immune to the close-precision breaker the way those already are.
  • Enforcement — issue path: mirror the same short-circuit for issue opened events (issues currently have no auto-close path in processGitHubWebhook at all — see the sibling issue on issue-side label edits never re-triggering PR hard-rule evaluation, which found the same gap; this feature would need to add the first eventName === "issues" actuation branch, so land that plumbing carefully and reuse it if the label-edit issue is picked up around the same time).
  • Which items close: order the contributor's open items oldest-first; anything beyond the cap count closes, newest first. On the enforcement webhook, this is naturally just "the incoming item, if it pushes the count over" — the sweep backstop needs the explicit oldest-first ordering for backfill (e.g. a contributor who was under the cap when they opened 5 PRs, then the cap gets lowered by the maintainer afterward).
  • Close comment: static, deterministic template (mirroring blacklistCloseMessage()) stating the cap and current count, e.g. "Gittensory closed this because @login has N open pull requests, above this repo's configured limit of {cap}. Close or merge an existing one to open a new one."
  • Label: apply a configurable label (default something like "over-contributor-limit") via the existing label action class, same as blacklist's label + close pair.
  • Add regression tests: cap disabled (default) → no enforcement; cap enabled, contributor at cap opens one more → the new one closes, existing ones stay open; owner/bot authors exempt; global env default + per-repo override precedence.

Acceptance criteria

  • A repo with no cap configured (the default) sees zero behavior change.
  • A repo with contributorOpenPrCap: 2 set: a contributor's 3rd simultaneously-open PR is auto-closed with a clear reason; their first 2 remain open and reviewable.
  • The same for issues via contributorOpenIssueCap.
  • Owner and automation-bot PRs/issues are never auto-closed by this mechanism.
  • The cap can be enabled fleet-wide via an env var default, and overridden (including disabled) per repo.

Expected outcome

A maintainer can cap how much simultaneous review pressure a single contributor can create, with a transparent and immediate (not sweep-delayed) enforcement, using the same deterministic-close machinery and trust model the contributor-blacklist feature already established.

Metadata

Metadata

Assignees

Labels

maintainer-onlyOwner-only work — yields no Gittensor points.

Projects

Status
Done

Relationships

None yet

Development

No branches or pull requests

Issue actions