diff --git a/src/settings/agent-actions.ts b/src/settings/agent-actions.ts index 17624d0eaa..0eba0c53af 100644 --- a/src/settings/agent-actions.ts +++ b/src/settings/agent-actions.ts @@ -751,6 +751,10 @@ export function planAgentMaintenanceActions(input: AgentActionPlanInput): Planne closeReasons: ["over the per-contributor open-item cap"], closeComment: sanitizePublicComment(contributorCapCloseMessage(authorLogin, openCount, cap, itemKind, scope)), closeKind: "contributor_cap", + // Pin like blacklist/review_nag/copycat/screenshot_table above (#2452): without this an auto_with_approval + // stage persists params.expectedHeadSha as undefined, so decidePendingAgentAction's isUnpinnedRatifyingAction + // guard unconditionally rejects the maintainer's accept before the close ever executes. + ...(input.pr.headSha ? { expectedHeadSha: input.pr.headSha } : {}), }); } if (acting("close") && label !== null) actions.push({ actionClass: "label", autonomyClass: "close", closeKind: "contributor_cap", requiresApproval: approval("close"), reason: "over the per-contributor open-item cap", label, labelOp: "add" }); diff --git a/test/unit/agent-actions.test.ts b/test/unit/agent-actions.test.ts index 2c2db57ec3..be5debb3b6 100644 --- a/test/unit/agent-actions.test.ts +++ b/test/unit/agent-actions.test.ts @@ -2052,6 +2052,16 @@ describe("per-contributor open-item cap short-circuit (#2270)", () => { expect(plan[1]).toMatchObject({ actionClass: "label", label: DEFAULT_CONTRIBUTOR_CAP_LABEL, labelOp: "add", closeKind: "contributor_cap" }); }); + it("pins the contributor_cap close to the reviewed head, mirroring blacklist/review_nag/copycat/screenshot_table (regression: was previously unpinned, which made decidePendingAgentAction reject every auto_with_approval accept via isUnpinnedRatifyingAction)", () => { + const plan = planAgentMaintenanceActions(overCap({ pr: { labels: [], headSha: "h-reviewed" } })); + expect(plan.find((a) => a.actionClass === "close")).toMatchObject({ closeKind: "contributor_cap", expectedHeadSha: "h-reviewed" }); + }); + + it("omits expectedHeadSha on the contributor_cap close when the PR record has no headSha (defensive fallback)", () => { + const plan = planAgentMaintenanceActions(overCap()); + expect(plan.find((a) => a.actionClass === "close")?.expectedHeadSha).toBeUndefined(); + }); + it("interpolates the (public) login/count/cap into the close comment — unlike blacklist's static-only comment", () => { const plan = planAgentMaintenanceActions(overCap()); expect(plan[0]?.closeComment).toContain("@farmer99");