feat(agent-actions): auto-close a contributor's issue over the open-issue cap (#2270)#2493
Merged
JSONbored merged 5 commits intoJul 2, 2026
Conversation
7745dc1 to
b7e841c
Compare
|
Important 🟪🟪🟪🟪🟪🟪🟪🟪🟪🟪🟪🟪 🔍 Gittensory is reviewing…AI analysis is in progress. This comment will update when the review is complete. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed · 🟪 Reviewing |
24 tasks
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## feat/contributor-cap-enforcement #2493 +/- ##
===================================================================
Coverage ? 95.97%
===================================================================
Files ? 226
Lines ? 25521
Branches ? 9272
===================================================================
Hits ? 24493
Misses ? 417
Partials ? 611
🚀 New features to boost your workflow:
|
JSONbored
added a commit
that referenced
this pull request
Jul 2, 2026
…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.
b7e841c to
15ceaa8
Compare
3cf66ef to
a39c458
Compare
…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.
…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) 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.
…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.
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.
15ceaa8 to
4984a2a
Compare
JSONbored
added a commit
that referenced
this pull request
Jul 2, 2026
…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.
JSONbored
added a commit
that referenced
this pull request
Jul 2, 2026
…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).
JSONbored
added a commit
that referenced
this pull request
Jul 2, 2026
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
mainonce feat(settings): add contributorOpenPrCap/contributorOpenIssueCap config (#2270) #2467 → feat(agent-actions): auto-close a contributor's PR over the open-PR cap (#2270) #2479 → this merge in order).eventName === "issues"actuation branch inprocessGitHubWebhook— issues previously had zero auto-close path at all.maybeCloseIssueOverContributorCapmirrors the PR-path cap exactly: same oldest-stays/newest-closes ranking by item number, same owner/admin/automation-bot exemption, and it reusesplanAgentMaintenanceActions'scontributor_capshort-circuit to build the label+close plan — so the label/message/closeKindconstruction is identical between the PR and issue paths, not a parallel reimplementation.contributorCapMatchnow carries an explicititemKind: "pull requests" | "issues"so each caller states its own noun — no default, so a future caller can't silently mislabel the other kind.executeIssueMaintenanceActionsis deliberately narrower than the PR executor: no freshness/live-CI-re-verification/pull_requests:writegates (none of those PR concepts apply to a plain issue — no head SHA, no CI, and a close needsissues: write, a different permission).auto_with_approvalis denied, not staged — the pending-action queue is PR-shaped (pullNumber-typed staging + a/pull/{n}notification deeplink); extending it to issues is a documented follow-up, not silently bypassed.closeIssueis a new GitHub REST primitive (PATCH /issues/{n}) —closePullRequesthits the Pulls API, which a plain issue number is not valid against.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 coverage and full branch coverage on every changed line (verified via the v8/lcov report against the exact diff, including the "at cap, not over" and "matched but no acting autonomy" branches for the issue path).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/agent-action-executor.test.ts(label/close, paused, autonomy-denied, dry-run,auto_with_approvaldenial, unsupported-class defensive denial, error path), acloseIssueprimitive test intest/unit/github-pr-actions.test.ts, planneritemKindregression test intest/unit/agent-actions.test.ts, and full webhook-pipeline integration tests intest/unit/queue.test.ts(over-cap close, at-cap no-close, disabled-by-default no-op, no-acting-autonomy no-op, owner exemption).Safety
UI Evidencesection. — N/A, no visible UI change.CHANGELOG.mduntouched.Notes
CONTRIBUTOR_OPEN_PR_CAP_DEFAULT/CONTRIBUTOR_OPEN_ISSUE_CAP_DEFAULT) and the named-contributor whitelist noted in the comment on feat(agent-actions): configurable per-contributor open PR/issue cap with auto-close over the limit #2270.