Skip to content

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

Merged
JSONbored merged 5 commits into
feat/contributor-cap-enforcementfrom
feat/contributor-cap-issue-enforcement
Jul 2, 2026
Merged

feat(agent-actions): auto-close a contributor's issue over the open-issue cap (#2270)#2493
JSONbored merged 5 commits into
feat/contributor-cap-enforcementfrom
feat/contributor-cap-issue-enforcement

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

  • Stacked on feat(agent-actions): auto-close a contributor's PR over the open-PR cap (#2270) #2479 (base branch is that PR's branch — this PR's diff is only the issue-side delta; it will retarget to main once feat(settings): add contributorOpenPrCap/contributorOpenIssueCap config (#2270) #2467feat(agent-actions): auto-close a contributor's PR over the open-PR cap (#2270) #2479 → this merge in order).
  • Adds the first eventName === "issues" actuation branch in processGitHubWebhook — issues previously had zero auto-close path at all. maybeCloseIssueOverContributorCap mirrors the PR-path cap exactly: same oldest-stays/newest-closes ranking by item number, same owner/admin/automation-bot exemption, and it 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, not a parallel reimplementation.
  • Fixes a bug this reuse exposed: the close-comment message was hardcoded to say "pull requests" even when closing an issue (caught by my own local integration test, not by the gate this time). contributorCapMatch now carries an explicit itemKind: "pull requests" | "issues" so each caller states its own noun — no default, so a future caller can't silently mislabel the other kind.
  • New executeIssueMaintenanceActions is deliberately narrower than the PR executor: no freshness/live-CI-re-verification/pull_requests:write gates (none of those PR concepts apply to a plain issue — no head SHA, no CI, and a close needs issues: write, a different permission). auto_with_approval is 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.
  • closeIssue is a new GitHub REST primitive (PATCH /issues/{n}) — closePullRequest hits the Pulls API, which a plain issue number is not valid against.

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 "at cap, not over" and "matched but no acting autonomy" branches for the issue path).
  • 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 — direct executor tests in test/unit/agent-action-executor.test.ts (label/close, paused, autonomy-denied, dry-run, auto_with_approval denial, unsupported-class defensive denial, error path), a closeIssue primitive test in test/unit/github-pr-actions.test.ts, planner itemKind regression test in test/unit/agent-actions.test.ts, and full webhook-pipeline integration tests in test/unit/queue.test.ts (over-cap close, at-cap no-close, disabled-by-default no-op, no-acting-autonomy no-op, owner exemption).

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 — same sanitized close-comment construction as the PR path.
  • 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 — no new request/response fields (all config already shipped in feat(settings): add contributorOpenPrCap/contributorOpenIssueCap config (#2270) #2467).
  • 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 — no doc changes needed (already covered in feat(settings): add contributorOpenPrCap/contributorOpenIssueCap config (#2270) #2467); CHANGELOG.md untouched.

Notes

@dosubot dosubot Bot added the size:L label Jul 2, 2026
@JSONbored JSONbored force-pushed the feat/contributor-cap-issue-enforcement branch from 7745dc1 to b7e841c Compare July 2, 2026 06:09
@gittensory-orb

gittensory-orb Bot commented Jul 2, 2026

Copy link
Copy Markdown

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

@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 self-assigned this Jul 2, 2026
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (feat/contributor-cap-enforcement@a39c458). Learn more about missing BASE report.
✅ All tests successful. No failed tests found.

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           
Files with missing lines Coverage Δ
src/github/pr-actions.ts 100.00% <100.00%> (ø)
src/queue/processors.ts 91.97% <100.00%> (ø)
src/services/agent-action-executor.ts 94.36% <100.00%> (ø)
src/settings/agent-actions.ts 93.38% <100.00%> (ø)
🚀 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
…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 JSONbored force-pushed the feat/contributor-cap-issue-enforcement branch from b7e841c to 15ceaa8 Compare July 2, 2026 07:11
@JSONbored JSONbored force-pushed the feat/contributor-cap-enforcement branch from 3cf66ef to a39c458 Compare July 2, 2026 07:12
JSONbored added 5 commits July 2, 2026 00:15
…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.
@JSONbored JSONbored force-pushed the feat/contributor-cap-issue-enforcement branch from 15ceaa8 to 4984a2a Compare July 2, 2026 07:27
@JSONbored JSONbored merged commit 3ae65af into feat/contributor-cap-enforcement Jul 2, 2026
2 checks passed
@JSONbored JSONbored deleted the feat/contributor-cap-issue-enforcement branch July 2, 2026 07:29
@github-project-automation github-project-automation Bot moved this from Todo to Done in gittensory - v1 roadmap Jul 2, 2026
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.
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