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.
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
.gittensory.yml/repository_settings, following theyml > DB > defaultsprecedence used everywhere else).Deliverables
contributorOpenPrCap?: number | nullandcontributorOpenIssueCap?: number | nulltoRepositorySettings(src/types.ts), null/absent = disabled (matches the nullable-numeric-threshold pattern already used for other opt-in caps).settings.contributorOpenPrCap/settings.contributorOpenIssueCapin.gittensory.yml(add toFocusManifestSettings's field list andparseSettingsOverride, following the exact template used for every other settings field — see the sibling issue oncommandAuthorizationfor a worked example of this exact wiring gap to avoid repeating).repository_settings; thread throughresolveEffectiveSettings/getRepositorySettingsthe same way every other settings field is threaded.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.openedwebhook (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 overcontributorOpenPrCap, plan alabel+closeaction mirroring the blacklist short-circuit insrc/settings/agent-actions.ts(blacklistContributor/blacklistMatchblock) — add a newcloseKind: "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.openedevents (issues currently have no auto-close path inprocessGitHubWebhookat 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 firsteventName === "issues"actuation branch, so land that plumbing carefully and reuse it if the label-edit issue is picked up around the same time).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.""over-contributor-limit") via the existinglabelaction class, same as blacklist'slabel+closepair.Acceptance criteria
contributorOpenPrCap: 2set: a contributor's 3rd simultaneously-open PR is auto-closed with a clear reason; their first 2 remain open and reviewable.contributorOpenIssueCap.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.