feat(settings): add contributorOpenPrCap/contributorOpenIssueCap config (#2270)#2467
Conversation
…ig (#2270) Adds the config-as-code contract for per-contributor open PR/issue caps: DB migration, RepositorySettings type, .gittensory.yml settings: parsing with precedence over the DB, OpenAPI response fields, and docs. No enforcement behavior yet (a repo with no cap configured sees zero change) — auto-close over the cap lands in a follow-up PR.
|
Warning 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨 ⏸️ Gittensory review result - manual review recommendedReview updated: 2026-07-02 05:35:21 UTC
⏸️ Suggested Action - Manual Review Review summary 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.
|
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
gittensory-ui | ad44ba3 | Commit Preview URL Branch Preview URL |
Jul 02 2026, 05:15 AM |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2467 +/- ##
=======================================
Coverage 95.93% 95.93%
=======================================
Files 225 225
Lines 25338 25348 +10
Branches 9218 9223 +5
=======================================
+ Hits 24308 24318 +10
Misses 417 417
Partials 613 613
🚀 New features to boost your workflow:
|
…ap (#2467) parseSettingsOverride collapsed an explicit `settings.contributorOpenPrCap: null` in .gittensory.yml to the same outcome as omitting the key, so a maintainer had no way to force a DB-configured cap back to disabled via config-as-code, contradicting the documented yml > DB > null precedence. Distinguish the three states (omitted / explicit null / invalid) before normalizing. Also corrects .gittensory.yml.example, which described these fields as already closing over-cap PRs; enforcement lands in a follow-up PR.
…ap (#2467) parseSettingsOverride collapsed an explicit `settings.contributorOpenPrCap: null` in .gittensory.yml to the same outcome as omitting the key, so a maintainer had no way to force a DB-configured cap back to disabled via config-as-code, contradicting the documented yml > DB > null precedence. Distinguish the three states (omitted / explicit null / invalid) before normalizing. Also corrects .gittensory.yml.example, which described these fields as already closing over-cap PRs; enforcement lands in a follow-up PR.
…ssue cap (#2270) (#2493) * feat(settings): add contributorOpenPrCap/contributorOpenIssueCap config (#2270) Adds the config-as-code contract for per-contributor open PR/issue caps: DB migration, RepositorySettings type, .gittensory.yml settings: parsing with precedence over the DB, OpenAPI response fields, and docs. No enforcement behavior yet (a repo with no cap configured sees zero change) — auto-close over the cap lands in a follow-up PR. * fix(settings): preserve an explicit yml null clearing a contributor cap (#2467) parseSettingsOverride collapsed an explicit `settings.contributorOpenPrCap: null` in .gittensory.yml to the same outcome as omitting the key, so a maintainer had no way to force a DB-configured cap back to disabled via config-as-code, contradicting the documented yml > DB > null precedence. Distinguish the three states (omitted / explicit null / invalid) before normalizing. Also corrects .gittensory.yml.example, which described these fields as already closing over-cap PRs; enforcement lands in a follow-up PR. * feat(agent-actions): auto-close a contributor's issue over the open-issue cap (#2270) Adds the first eventName === "issues" actuation branch in processGitHubWebhook — issues previously had zero auto-close path. maybeCloseIssueOverContributorCap mirrors the PR-path cap exactly (same oldest-stays/newest-closes ranking by item number, same owner/admin/bot exemption) and reuses planAgentMaintenanceActions's contributor_cap short-circuit to build the label+close plan, so the label/message/closeKind construction is identical between the PR and issue paths. Fixes a bug introduced by that reuse: the close-comment message was hardcoded to say "pull requests" even when closing an issue. contributorCapMatch now carries an itemKind ("pull requests" | "issues") so each caller states its own noun explicitly. New executeIssueMaintenanceActions is deliberately narrower than the PR executor: no freshness/live-CI/pull_requests:write gates (none of those PR concepts apply to a plain issue), and auto_with_approval is denied rather than staged — the pending-action queue is PR-shaped (pullNumber-typed staging + a /pull/{n} deeplink); extending it to issues is a documented follow-up, not silently bypassed here. closeIssue is a new GitHub REST primitive (PATCH /issues/{n}), since closePullRequest hits the Pulls API, which a plain issue number isn't valid against. * fix(agent-actions): close every over-cap sibling issue, not just the incoming one (#2493) Same delivery-order gap as #2479, mirrored for issues: closing only "the incoming issue, if it's over cap" let an older sibling's stale verdict (computed before a newer sibling existed in the DB) stand forever, since nothing else ever re-evaluates an issue. Now closes every number in the over-cap set discovered by the current delivery. Unlike the PR path, issues have no live-head/CI staleness risk to guard against and no issue-side "regate" job type to reuse, so acting directly on the already-fetched snapshot is safe. Also fixes the codecov/patch gap the gate flagged on this PR: adds coverage for the label ?? "" fallback, the no-slash repoFullName guard, and an author-less (ghost) sibling issue. * test(queue): cover the sibling-wake enqueue-failure path (#2270) env.JOBS.send can reject (queue backpressure/outage) when waking a missed over-cap sibling; pin the fail-safe behavior — swallow the error and skip claiming the coalescing key so a later discovery can retry the enqueue.
…ssue cap (#2270) (#2493) * feat(settings): add contributorOpenPrCap/contributorOpenIssueCap config (#2270) Adds the config-as-code contract for per-contributor open PR/issue caps: DB migration, RepositorySettings type, .gittensory.yml settings: parsing with precedence over the DB, OpenAPI response fields, and docs. No enforcement behavior yet (a repo with no cap configured sees zero change) — auto-close over the cap lands in a follow-up PR. * fix(settings): preserve an explicit yml null clearing a contributor cap (#2467) parseSettingsOverride collapsed an explicit `settings.contributorOpenPrCap: null` in .gittensory.yml to the same outcome as omitting the key, so a maintainer had no way to force a DB-configured cap back to disabled via config-as-code, contradicting the documented yml > DB > null precedence. Distinguish the three states (omitted / explicit null / invalid) before normalizing. Also corrects .gittensory.yml.example, which described these fields as already closing over-cap PRs; enforcement lands in a follow-up PR. * feat(agent-actions): auto-close a contributor's issue over the open-issue cap (#2270) Adds the first eventName === "issues" actuation branch in processGitHubWebhook — issues previously had zero auto-close path. maybeCloseIssueOverContributorCap mirrors the PR-path cap exactly (same oldest-stays/newest-closes ranking by item number, same owner/admin/bot exemption) and reuses planAgentMaintenanceActions's contributor_cap short-circuit to build the label+close plan, so the label/message/closeKind construction is identical between the PR and issue paths. Fixes a bug introduced by that reuse: the close-comment message was hardcoded to say "pull requests" even when closing an issue. contributorCapMatch now carries an itemKind ("pull requests" | "issues") so each caller states its own noun explicitly. New executeIssueMaintenanceActions is deliberately narrower than the PR executor: no freshness/live-CI/pull_requests:write gates (none of those PR concepts apply to a plain issue), and auto_with_approval is denied rather than staged — the pending-action queue is PR-shaped (pullNumber-typed staging + a /pull/{n} deeplink); extending it to issues is a documented follow-up, not silently bypassed here. closeIssue is a new GitHub REST primitive (PATCH /issues/{n}), since closePullRequest hits the Pulls API, which a plain issue number isn't valid against. * fix(agent-actions): close every over-cap sibling issue, not just the incoming one (#2493) Same delivery-order gap as #2479, mirrored for issues: closing only "the incoming issue, if it's over cap" let an older sibling's stale verdict (computed before a newer sibling existed in the DB) stand forever, since nothing else ever re-evaluates an issue. Now closes every number in the over-cap set discovered by the current delivery. Unlike the PR path, issues have no live-head/CI staleness risk to guard against and no issue-side "regate" job type to reuse, so acting directly on the already-fetched snapshot is safe. Also fixes the codecov/patch gap the gate flagged on this PR: adds coverage for the label ?? "" fallback, the no-slash repoFullName guard, and an author-less (ghost) sibling issue. * test(queue): cover the sibling-wake enqueue-failure path (#2270) env.JOBS.send can reject (queue backpressure/outage) when waking a missed over-cap sibling; pin the fail-safe behavior — swallow the error and skip claiming the coalescing key so a later discovery can retry the enqueue.
…ap (#2270) (#2479) * feat(settings): add contributorOpenPrCap/contributorOpenIssueCap config (#2270) Adds the config-as-code contract for per-contributor open PR/issue caps: DB migration, RepositorySettings type, .gittensory.yml settings: parsing with precedence over the DB, OpenAPI response fields, and docs. No enforcement behavior yet (a repo with no cap configured sees zero change) — auto-close over the cap lands in a follow-up PR. * feat(agent-actions): auto-close a contributor's PR over the open-PR cap (#2270) Adds the enforcement half of #2270: a new closeKind: "contributor_cap" short-circuit in planAgentMaintenanceActions, mirroring the existing contributor-blacklist block exactly (fires ahead of merit/CI/AI analysis, exempt from the close-precision breaker, owner/admin/bot never touched). The webhook-time check counts the author's other open PRs on the repo (already-fetched, live-DB-backed data — no new query) plus the incoming one, ranked by PR number so a burst of near-simultaneous opens still ranks deterministically; only the PR(s) pushing the count over the cap close, oldest ones stay open. Adds the paired contributorCapLabel setting (default "over-contributor-limit") through the full config-as-code chain. Known scope limit: this covers live webhook-time enforcement only. A maintainer lowering the cap after a contributor already has more PRs open than the new cap allows is not retroactively enforced by a sweep in this PR — tracked as follow-up work on #2270, not required for the core ask (closing a NEW PR that pushes someone over the limit). * fix(docs): correct .gittensory.yml.example — the PR cap is now enforced (#2479) Gate finding on this PR: the example file still said contributorOpenPrCap was inert with enforcement "landing in a follow-up PR" — but this very PR IS that follow-up, so the doc was stale the moment it merged. Splits the two fields' status: contributorOpenPrCap is enforced; contributorOpenIssueCap remains inert until its own follow-up. Also adds a webhook-level (not just planner-level) owner-exemption test, per the gate's non-blocking suggestion. * fix(queue): wake missed siblings on out-of-order contributor-cap delivery (#2270) Webhook delivery order is not guaranteed to match PR creation order, so a sibling PR's webhook can process before a later-created PR exists in the DB and wrongly conclude the author is within the open-PR cap. Nothing else would ever re-evaluate that sibling, permanently bypassing the cap. Now that a later delivery has the complete picture, wake any other still-open over-cap sibling so its own next pass self-corrects. * feat(agent-actions): auto-close a contributor's issue over the open-issue cap (#2270) (#2493) * feat(settings): add contributorOpenPrCap/contributorOpenIssueCap config (#2270) Adds the config-as-code contract for per-contributor open PR/issue caps: DB migration, RepositorySettings type, .gittensory.yml settings: parsing with precedence over the DB, OpenAPI response fields, and docs. No enforcement behavior yet (a repo with no cap configured sees zero change) — auto-close over the cap lands in a follow-up PR. * fix(settings): preserve an explicit yml null clearing a contributor cap (#2467) parseSettingsOverride collapsed an explicit `settings.contributorOpenPrCap: null` in .gittensory.yml to the same outcome as omitting the key, so a maintainer had no way to force a DB-configured cap back to disabled via config-as-code, contradicting the documented yml > DB > null precedence. Distinguish the three states (omitted / explicit null / invalid) before normalizing. Also corrects .gittensory.yml.example, which described these fields as already closing over-cap PRs; enforcement lands in a follow-up PR. * feat(agent-actions): auto-close a contributor's issue over the open-issue cap (#2270) Adds the first eventName === "issues" actuation branch in processGitHubWebhook — issues previously had zero auto-close path. maybeCloseIssueOverContributorCap mirrors the PR-path cap exactly (same oldest-stays/newest-closes ranking by item number, same owner/admin/bot exemption) and reuses planAgentMaintenanceActions's contributor_cap short-circuit to build the label+close plan, so the label/message/closeKind construction is identical between the PR and issue paths. Fixes a bug introduced by that reuse: the close-comment message was hardcoded to say "pull requests" even when closing an issue. contributorCapMatch now carries an itemKind ("pull requests" | "issues") so each caller states its own noun explicitly. New executeIssueMaintenanceActions is deliberately narrower than the PR executor: no freshness/live-CI/pull_requests:write gates (none of those PR concepts apply to a plain issue), and auto_with_approval is denied rather than staged — the pending-action queue is PR-shaped (pullNumber-typed staging + a /pull/{n} deeplink); extending it to issues is a documented follow-up, not silently bypassed here. closeIssue is a new GitHub REST primitive (PATCH /issues/{n}), since closePullRequest hits the Pulls API, which a plain issue number isn't valid against. * fix(agent-actions): close every over-cap sibling issue, not just the incoming one (#2493) Same delivery-order gap as #2479, mirrored for issues: closing only "the incoming issue, if it's over cap" let an older sibling's stale verdict (computed before a newer sibling existed in the DB) stand forever, since nothing else ever re-evaluates an issue. Now closes every number in the over-cap set discovered by the current delivery. Unlike the PR path, issues have no live-head/CI staleness risk to guard against and no issue-side "regate" job type to reuse, so acting directly on the already-fetched snapshot is safe. Also fixes the codecov/patch gap the gate flagged on this PR: adds coverage for the label ?? "" fallback, the no-slash repoFullName guard, and an author-less (ghost) sibling issue. * test(queue): cover the sibling-wake enqueue-failure path (#2270) env.JOBS.send can reject (queue backpressure/outage) when waking a missed over-cap sibling; pin the fail-safe behavior — swallow the error and skip claiming the coalescing key so a later discovery can retry the enqueue. * fix(queue): resolve base conflict and 2 gate findings on contributor-cap enforcement Gate findings on this PR: - .gittensory.yml.example still documented contributorOpenIssueCap as inert, but the issue-cap enforcement from #2493 already landed on this branch — the doc was stale. Both caps are now documented as enforced. - maybeCloseIssueOverContributorCap counted siblings from listOpenIssues (a DB read), with no live verification. A stale "open" row for a sibling already closed on GitHub could inflate the count and wrongly close a newly opened issue within the real cap. Live-verify each counted sibling before trusting it, mirroring reconcileLiveDuplicateSiblings' fail-open contract (only drop a sibling on a positive "not open" confirmation). Also resolves a base conflict from a rebase onto main (closeKind's "contributor_cap" variant combined with the closeRequiresCiState tri-state added by #2478, both touching the same close-action types). * fix(queue): fail SAFE, not fail-open, on an unverifiable issue-cap sibling The live-verify guard added for the first gate finding on this PR still counted an unreadable sibling toward the cap by default (mirroring reconcileLiveDuplicateSiblings' fail-open-to-stored contract). The gate flagged that this compounds with a stale "open" DB row: a transient fetch failure could still let a stale-closed sibling inflate the count and wrongly close a newly opened issue within the real cap. Unlike reconcileLiveDuplicateSiblings (which only re-ranks a non-final duplicate-cluster winner), this count gates an irreversible close, so the safe default flips: only a POSITIVE "open" confirmation counts a sibling toward the cap. An unreadable check now excludes it instead of including it.
Summary
contributorOpenPrCap/contributorOpenIssueCaponRepositorySettings, DB migration0089_contributor_open_caps.sql,.gittensory.ymlsettings:parsing withyml > DB > nullprecedence (mirrors the existingcontributorBlacklist/qualityGateMinScorefields end-to-end), OpenAPI response fields, and.gittensory.yml.exampledocs.closeKind: "contributor_cap"short-circuit), split out per this repo's small-PR convention (contract PR first, activation PR next).Scope
type(scope): short summaryConventional Commit format.CONTRIBUTING.mdand does not reintroduce GitHub Pages, VitePress,site/, orCNAME.Validation
git diff --checknpm run actionlintnpm run typechecknpm run test:coveragelocally — 100% line/branch on every changed line (verified via the v8 report's uncovered-line list against the diff).npm run test:workersnpm run build:mcpnpm run test:mcp-packnpm run ui:openapi:checknpm run ui:lintnpm run ui:typechecknpm run ui:buildnpm audit --audit-level=moderatetest/unit/data-spine.test.ts, manifest parse + precedence + invalid-value rejection intest/unit/focus-manifest.test.ts.Safety
RepositorySettingsOpenAPI schema regenerated (npm run ui:openapi).UI Evidencesection. — N/A, no visible UI change (config/type/schema only)..gittensory.yml.exampleupdated;CHANGELOG.mduntouched.Notes