diff --git a/server/src/__tests__/issue-execution-policy.test.ts b/server/src/__tests__/issue-execution-policy.test.ts index 8b9d560191..516b887142 100644 --- a/server/src/__tests__/issue-execution-policy.test.ts +++ b/server/src/__tests__/issue-execution-policy.test.ts @@ -773,6 +773,49 @@ describe("issue execution policy transitions", () => { // No error — just no patch modifications expect(result.patch).toEqual({}); }); + + it("board operator can reconcile a stuck active review stage back to the return assignee", () => { + const result = applyIssueExecutionPolicyTransition({ + issue: { + status: "in_review", + assigneeAgentId: qaAgentId, + assigneeUserId: null, + executionPolicy: policy, + executionState: { + status: "pending", + currentStageId: reviewStageId, + currentStageIndex: 0, + currentStageType: "review", + currentParticipant: { type: "agent", agentId: qaAgentId }, + returnAssignee: { type: "agent", agentId: coderAgentId }, + completedStageIds: [], + lastDecisionId: null, + lastDecisionOutcome: null, + }, + }, + policy, + requestedStatus: "in_progress", + requestedAssigneePatch: { assigneeAgentId: coderAgentId }, + actor: { userId: boardUserId }, + commentBody: "Operator reconciliation: reviewer already requested changes but the runtime failed to patch state.", + }); + + expect(result.patch.status).toBe("in_progress"); + expect(result.patch.assigneeAgentId).toBe(coderAgentId); + expect(result.patch.executionState).toMatchObject({ + status: "changes_requested", + currentStageId: reviewStageId, + currentStageType: "review", + currentParticipant: { type: "agent", agentId: qaAgentId }, + returnAssignee: { type: "agent", agentId: coderAgentId }, + lastDecisionOutcome: "changes_requested", + }); + expect(result.decision).toMatchObject({ + stageId: reviewStageId, + stageType: "review", + outcome: "changes_requested", + }); + }); }); describe("comment requirements", () => { diff --git a/server/src/services/issue-execution-policy.ts b/server/src/services/issue-execution-policy.ts index 859dc5fe1e..66823757ed 100644 --- a/server/src/services/issue-execution-policy.ts +++ b/server/src/services/issue-execution-policy.ts @@ -76,6 +76,10 @@ const MONITOR_BOUNDS_EXHAUSTED_MESSAGE = "Monitor bounds are already exhausted"; const STAGE_DECISION_COMMENT_HINT = "Include the decision comment in the same PATCH request; prior comments are not considered."; export const REDACTED_ISSUE_MONITOR_EXTERNAL_REF = "[redacted]"; +function isBoardActor(actor: ActorLike) { + return Boolean(actor.userId) && !actor.agentId; +} + function normalizeMonitorNotes(notes: string | null | undefined) { if (typeof notes !== "string") return null; const trimmed = notes.trim(); @@ -783,7 +787,7 @@ function applyIssueExecutionStageTransition(input: TransitionInput): TransitionR }; } - if (principalsEqual(currentParticipant, actor)) { + if ((actor && principalsEqual(currentParticipant, actor)) || isBoardActor(input.actor)) { if (requestedStatus === "done") { if (!input.commentBody?.trim()) { throw unprocessable(`Approving a review or approval stage requires a comment. ${STAGE_DECISION_COMMENT_HINT}`);