feat(agent-actions): auto-close a contributor's PR over the open-PR cap (#2270)#2479
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
gittensory-ui | 6b5f728 | Commit Preview URL Branch Preview URL |
Jul 02 2026, 07:57 AM |
|
Warning 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨 ⏸️ Gittensory review result - manual review recommendedReview updated: 2026-07-02 08:06:33 UTC
⏸️ Suggested Action - Manual Review
Review summary Nits — 7 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.
|
ed351e5 to
4bf4d36
Compare
…ed (#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.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2479 +/- ##
========================================
Coverage 95.95% 95.95%
========================================
Files 226 228 +2
Lines 25387 25677 +290
Branches 9234 9335 +101
========================================
+ Hits 24359 24639 +280
- Misses 417 426 +9
- Partials 611 612 +1
🚀 New features to boost your workflow:
|
… cap (#2479) Gate finding on this PR: contributorCapMatch was computed only from otherOpenPullRequests, so webhook delivery order (not guaranteed to match PR creation order) could let a sibling's own webhook process before this PR existed in the DB — that sibling would wrongly conclude it's within the cap, and since nothing else ever re-evaluates it, the cap would be permanently bypassed for that PR. Once a delivery has the complete picture, it now wakes any other still- open sibling that's also in the over-cap set via the existing agent-regate-pr sweep-unit job — the same "wake and fully re-evaluate" entry point the linked-issue-wake feature (#2259) uses for an identical class of problem, so the sibling gets its own live-head/CI-freshness re-check rather than acting on a shortcut. Coalesced per sibling PR to avoid an O(N^2) job storm from a burst of over-cap siblings.
…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.
…ed (#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.
3cf66ef to
a39c458
Compare
…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.
…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.
…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 (#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).
…ed (#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.
…very (#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.
…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.
…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).
3ae65af to
6b5f728
Compare
…bling 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
main— this PR's diff is only the enforcement delta; it will retarget tomainonce feat(settings): add contributorOpenPrCap/contributorOpenIssueCap config (#2270) #2467 merges).closeKind: "contributor_cap"short-circuit inplanAgentMaintenanceActions, mirroring the existing contributor-blacklist block exactly (fires ahead of merit/CI/AI analysis, exempt from the close-precision breaker via the same!== "heuristic"check, owner/admin/automation-bot never touched).otherOpenPullRequests, already-fetched live-DB-backed data — no new query added) plus the incoming PR, ranked by PR number (GitHub's own creation order, not webhook-arrival order) so a burst of near-simultaneous opens still ranks deterministically. Only the PR(s) pushing the count over the cap close; older ones stay open.contributorCapLabelsetting (default"over-contributor-limit") through the full config-as-code chain (migration0090, schema, DB round-trip,.gittensory.ymlparsing + precedence, OpenAPI, docs) since the label needed to be configurable per feat(agent-actions): configurable per-contributor open PR/issue cap with auto-close over the limit #2270's own text, and it's tightly coupled to this enforcement action rather than the PR1 contract.Known scope limit (documented, not a gap I missed): this is live webhook-time enforcement only. If a maintainer lowers the cap after a contributor already has more PRs open than the new cap allows, nothing retroactively re-evaluates those pre-existing PRs in this PR — that's a periodic-sweep backstop, tracked as follow-up work on #2270. It isn't required for the core ask (stopping a new PR that pushes someone over the limit).
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 "contributor exactly at cap, not over" branch).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-actions.test.ts(label/close/comment shape, owner/admin/bot exemption, autonomy gating, interaction with a simultaneous blacklist match), full webhook-pipeline integration tests intest/unit/queue.test.ts(over-cap close, at-cap no-close, disabled-by-default no-op), DB round-trip + manifest-parsing tests forcontributorCapLabel.Safety
sanitizePublicComment.RepositorySettingsOpenAPI schema regenerated (npm run ui:openapi).UI Evidencesection. — N/A, no visible UI change..gittensory.yml.exampleupdated;CHANGELOG.mduntouched.Notes
eventName === "issues"actuation branch — issues currently have zero auto-close path), then the global env-var default + 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.