diff --git a/src/services/agent-action-executor.ts b/src/services/agent-action-executor.ts index 99990ab5b8..62950673d0 100644 --- a/src/services/agent-action-executor.ts +++ b/src/services/agent-action-executor.ts @@ -17,6 +17,7 @@ import { import { isAuthorBlacklisted } from "../settings/contributor-blacklist"; import { classifyMergeFailure, MERGE_RETRY_CAP } from "./merge-failure"; import { notifyActionToDiscord, notifyActionToSlack, type NotifyOutcome } from "./notify-discord"; +import { resolveDispositionReason } from "../review/outcomes-wire"; import { cancelInFlightWorkflowRunsForHeadSha, createInstallationToken, githubErrorStatus, isGitHubRateLimitedError } from "../github/app"; import { fetchLiveCiAggregate, fetchLivePullRequestMergeState, fetchLivePullRequestState, fetchLiveReviewThreadBlockers, refreshInstallationHealthForInstallation } from "../github/backfill"; import { githubRateLimitAdmissionKeyForToken } from "../github/client"; @@ -562,7 +563,12 @@ export async function executeAgentMaintenanceActions(env: Env, ctx: AgentActionE const notifyOutcome: NotifyOutcome | null = action.actionClass === "merge" ? "merged" : action.actionClass === "close" ? "closed" : action.actionClass === "request_changes" ? "manual" : null; if (notifyOutcome) { - const notifyParams = { repoFullName: ctx.repoFullName, pullNumber: ctx.pullNumber, outcome: notifyOutcome, summary: action.reason, submitter: ctx.authorLogin }; + // #6636: enrich the notification with the AI's actual gate-verdict reasoning (the latest recorded + // gate_decision summary for this PR) instead of only the plain disposition reason — resolveDispositionReason + // falls back to `action.reason` when no verdict is on record or the read fails, so this is byte-identical + // when there's nothing to enrich with. review_audit keys gate_decision rows by `${repoFullName}#${number}`. + const summary = await resolveDispositionReason(env, `${ctx.repoFullName}#${ctx.pullNumber}`, action.reason); + const notifyParams = { repoFullName: ctx.repoFullName, pullNumber: ctx.pullNumber, outcome: notifyOutcome, summary, submitter: ctx.authorLogin }; await notifyActionToDiscord(env, notifyParams).catch(() => undefined); await notifyActionToSlack(env, notifyParams).catch(() => undefined); } diff --git a/test/unit/agent-action-executor.test.ts b/test/unit/agent-action-executor.test.ts index 7be7add609..da7319404b 100644 --- a/test/unit/agent-action-executor.test.ts +++ b/test/unit/agent-action-executor.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import * as notifyDiscordModule from "../../src/services/notify-discord"; vi.mock("../../src/github/pr-actions", () => ({ createPullRequestReview: vi.fn(async () => ({ id: 1 })), @@ -223,6 +224,25 @@ describe("executeAgentMaintenanceActions (#778 gate stack)", () => { expect(outcomes[0]?.detail.endsWith("…")).toBe(false); }); + it("enriches the disposition notification with the recorded gate verdict, falling back to the plain reason when none is on record (#6636)", async () => { + const env = createTestEnv({}); + const notifySpy = vi.spyOn(notifyDiscordModule, "notifyActionToDiscord").mockResolvedValue(undefined); + + // PR #7 HAS a recorded gate_decision verdict → the notification carries the enriched (verdict) reason, not + // the plain disposition reason ("noise"). review_audit keys the row by `${repoFullName}#${pullNumber}`. + await env.DB.prepare( + "INSERT INTO review_audit (id, project, target_id, event_type, decision, source, head_sha, summary, created_at) VALUES (?,?,?,?,?,?,?,?,?)", + ) + .bind("gv7", "owner/repo", "owner/repo#7", "gate_decision", "close", "gittensory-native", "sha7", "An AI reviewer flagged a likely blocking defect", "2026-07-16T00:00:00.000Z") + .run(); + await executeAgentMaintenanceActions(env, ctx(), [close]); + expect(notifySpy).toHaveBeenCalledWith(env, expect.objectContaining({ pullNumber: 7, summary: "An AI reviewer flagged a likely blocking defect" })); + + // PR #8 has NO recorded verdict → resolveDispositionReason falls back to the plain disposition reason. + await executeAgentMaintenanceActions(env, ctx({ pullNumber: 8, headSha: "sha8" }), [close]); + expect(notifySpy).toHaveBeenCalledWith(env, expect.objectContaining({ pullNumber: 8, summary: "noise" })); + }); + it("records every structured close reason in audit metadata instead of only the flattened detail", async () => { const env = createTestEnv({}); const closeWithReasons: PlannedAgentAction = {