fix(agent-actions): scope live CI close recheck - #2478
Conversation
|
Warning 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨 ⏸️ Gittensory review result - manual review recommendedReview updated: 2026-07-02 07:37:35 UTC
⏸️ Suggested Action - Manual Review
Review summary Nits — 5 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 | ab246a0 | Commit Preview URL Branch Preview URL |
Jul 02 2026, 05:43 AM |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2478 +/- ##
==========================================
+ Coverage 95.94% 95.96% +0.01%
==========================================
Files 226 226
Lines 25361 25473 +112
Branches 9229 9266 +37
==========================================
+ Hits 24333 24445 +112
Misses 417 417
Partials 611 611
🚀 New features to boost your workflow:
|
…oses
The narrowed live-CI recheck only fired for closeRequiresCiState ===
"failed", but a heuristic close staged before this field existed has no
closeRequiresCiState at all -- indistinguishable in storage from a fresh,
genuinely non-CI-driven close. Accepting such a legacy row skipped the
recheck entirely, so a stale CI-driven close could still execute after
CI recovered (flagged by the gate's own review).
Make the planner always tag a heuristic close explicitly ("failed" or
"not_required", never omitted) so an absent value can only mean a
legacy row. The executor now re-checks CI for that ambiguous case too,
falling back to the old pre-fix requirement (CI must still be failed).
5d8f2fb to
82b7bbf
Compare
…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).
…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.
Motivation
closeaction taggedcloseKind: "heuristic", which incorrectly required live CI to still befailedfor all heuristic closes and thus blocked non-CI-driven closes (gate failures, slop/duplicate findings, conflicts) when CI had become passing.Description
closeRequiresCiState?: "failed"to the planned action payload (PlannedAgentAction) and the persisted approval params (AgentPendingActionParams) so CI-driven closes can carry an explicit CI dependency.closeRequiresCiState: "failed"in the planner only when the computed close reason is actually a CI failure (ciFailed).mergeactions and only tocloseactions that carrycloseRequiresCiState === "failed", while keeping the broadercloseKind: "heuristic"value for the close-precision breaker.closeRequiresCiStatethroughactionParams()andpendingActionToPlanned()so queued/accepted closes retain the CI dependency; update comments and type docs accordingly.Testing
vitestfor the changed suites:test/unit/agent-action-executor.test.ts,test/unit/agent-actions.test.ts, andtest/unit/agent-approval-queue.test.ts, and they all passed locally.tsc --noEmitcompleted successfully.git diff --checkwith no issues reported.npm run test:cicould not complete in this environment due to network/setup limits affectingactionlintand the repository's self-hosted runner label, andnpm audit --audit-level=moderatecould not complete because the audit endpoint returned403 Forbidden.Codex Task