Skip to content

feat(agent-actions): auto-close a contributor's PR over the open-PR cap (#2270)#2479

Merged
JSONbored merged 7 commits into
mainfrom
feat/contributor-cap-enforcement
Jul 2, 2026
Merged

feat(agent-actions): auto-close a contributor's PR over the open-PR cap (#2270)#2479
JSONbored merged 7 commits into
mainfrom
feat/contributor-cap-enforcement

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

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

Validation

  • git diff --check
  • npm run actionlint
  • npm run typecheck
  • npm run test:coverage locally — 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:workers
  • npm run build:mcp
  • npm run test:mcp-pack
  • npm run ui:openapi:check
  • npm run ui:lint
  • npm run ui:typecheck
  • npm run ui:build
  • npm audit --audit-level=moderate
  • New or changed behavior has unit/integration tests for new branches, fallback paths, and sanitizer boundaries — planner-level short-circuit tests in test/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 in test/unit/queue.test.ts (over-cap close, at-cap no-close, disabled-by-default no-op), DB round-trip + manifest-parsing tests for contributorCapLabel.

Safety

  • No secrets, wallet details, hotkeys, coldkeys, user PATs, private keys, raw trust scores, private rankings, or private maintainer evidence are exposed.
  • Public GitHub text stays sanitized, low-noise, and does not imply compensation guarantees or optimization tactics — the close comment interpolates only the author's own public login and their own public open-PR count, run through sanitizePublicComment.
  • Auth, cookie, CORS, GitHub App, Cloudflare, or session changes include negative-path tests. — N/A, no auth/session/CORS surface touched.
  • API/OpenAPI/MCP behavior is updated and tested where needed — RepositorySettings OpenAPI schema regenerated (npm run ui:openapi).
  • UI changes use live API data or real empty/error/loading states, not production mock/demo fallbacks. — N/A, no UI change.
  • Visible UI changes include a UI Evidence section. — N/A, no visible UI change.
  • Public docs/changelogs are updated where needed; changelogs are only edited for release-prep PRs — .gittensory.yml.example updated; CHANGELOG.md untouched.

Notes

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 2, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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

@gittensory-orb

gittensory-orb Bot commented Jul 2, 2026

Copy link
Copy Markdown

Warning

🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨

⏸️ Gittensory review result - manual review recommended

Review updated: 2026-07-02 08:06:33 UTC

19 files · 1 AI reviewer · no blockers · readiness 73/100 · CI pending · unknown

⏸️ Suggested Action - Manual Review

  • Touches a guarded path — held for manual review

Review summary
The diff wires contributor open-item cap enforcement through the planner, PR processor, issue webhook path, settings schema, migration, OpenAPI, docs, and focused regression tests. The core enforcement is deterministic and correctly short-circuits before merit/CI/AI paths, with owner/admin/bot exemptions preserved in the planner and issue path. I do not see a reachable breaking defect in the provided diff; the main remaining concerns are maintainability and small defensive gaps around label validation/default drift.

Nits — 7 non-blocking
  • nit: src/github/backfill.ts:2591 says fetchLiveIssueState's caller fails open to stored state, but maybeCloseIssueOverContributorCap excludes undefined live checks from the count, so the comment should match the actual fail-safe behavior.
  • nit: src/queue/processors.ts:1889 still computes and enqueues over-cap sibling wakes for owner/admin/automation authors even though the planner later exempts them, which can create avoidable regate jobs for exempt authors.
  • nit: src/signals/focus-manifest.ts:817 accepts any string for settings.contributorCapLabel; an empty string or whitespace-only label will make the label action fail at GitHub even though the close can still proceed.
  • nit: src/db/repositories.ts:503 and src/db/repositories.ts:629 duplicate the contributor-cap default string instead of reusing DEFAULT_CONTRIBUTOR_CAP_LABEL, increasing drift risk with src/settings/agent-actions.ts.
  • Update src/github/backfill.ts:2591 to document that fetchLiveIssueState returns undefined on errors and that this cap caller treats undefined as not counted.
  • Readiness score is below the configured threshold — Use the readiness panel as advisory maintainer context; the score does not block this PR.
  • Touches a guarded path — held for manual review — A maintainer must review and merge this change.
Signal Result Evidence
Code review ✅ No blockers 1 reviewer
Linked issue ⚠️ Missing No linked issue or no-issue rationale found.
Related work ⚠️ 2 scoped overlaps Top overlaps are listed below; lower-confidence bulk is hidden.
Change scope ❌ 8/20 High review scope from cached public metadata (size label size:L; no linked issue context).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 65 registered-repo PR(s), 55 merged, 548 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 65 PR(s), 548 issue(s).
Gate result ⚠️ Not blocking Advisory; not blocking this PR.
Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository registration is not available in the local Gittensory cache.
  • Public profile languages: not available
  • Official Gittensor activity: 65 PR(s), 548 issue(s).
  • Related work: Titles/paths share 9 meaningful terms. (issue #2270)
  • Related work: Titles/paths share 8 meaningful terms. (issue #2462, issue #2270)
Contributor next steps
  • Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • Explain no-issue PR.
  • Review top overlaps.
  • Add a concise scope and risk note.
  • No action.
  • Link the issue being solved, or explicitly explain why this is a no-issue PR.
  • Check active issues and PRs before submitting.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.

🟩 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.

  • Re-run Gittensory review

@gittensory-orb gittensory-orb Bot added gittensor Gittensor contributor context gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 1.25x multiplier. labels Jul 2, 2026
@JSONbored JSONbored force-pushed the feat/contributor-cap-enforcement branch from ed351e5 to 4bf4d36 Compare July 2, 2026 05:37
@JSONbored JSONbored self-assigned this Jul 2, 2026
JSONbored added a commit that referenced this pull request Jul 2, 2026
…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.
@dosubot dosubot Bot added size:L and removed size:M labels Jul 2, 2026
Base automatically changed from feat/contributor-cap-settings to main July 2, 2026 06:15
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.07407% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 95.95%. Comparing base (cdef1c5) to head (c48ee80).
⚠️ Report is 32 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/queue/processors.ts 98.24% 0 Missing and 1 partial ⚠️
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     
Files with missing lines Coverage Δ
src/db/repositories.ts 96.52% <100.00%> (+<0.01%) ⬆️
src/db/schema.ts 69.46% <ø> (ø)
src/github/backfill.ts 96.49% <100.00%> (+<0.01%) ⬆️
src/github/pr-actions.ts 100.00% <100.00%> (ø)
src/openapi/schemas.ts 100.00% <ø> (ø)
src/services/agent-action-executor.ts 94.77% <100.00%> (+1.97%) ⬆️
src/settings/agent-actions.ts 93.60% <100.00%> (+0.80%) ⬆️
src/signals/focus-manifest.ts 99.25% <100.00%> (+0.01%) ⬆️
src/queue/processors.ts 91.96% <98.24%> (+0.21%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

JSONbored added a commit that referenced this pull request Jul 2, 2026
… 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.
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.
JSONbored added a commit that referenced this pull request Jul 2, 2026
…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.
@JSONbored JSONbored force-pushed the feat/contributor-cap-enforcement branch from 3cf66ef to a39c458 Compare July 2, 2026 07:12
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.
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 6 commits July 2, 2026 00:44
…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).
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 1.25x multiplier. gittensor Gittensor contributor context

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

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

1 participant