diff --git a/src/app/api/agents/[agentName]/next-task/route.ts b/src/app/api/agents/[agentName]/next-task/route.ts index 44ff2ba..c877218 100644 --- a/src/app/api/agents/[agentName]/next-task/route.ts +++ b/src/app/api/agents/[agentName]/next-task/route.ts @@ -10,7 +10,11 @@ import { import { isBacklogLane, getBacklogLane } from "@/lib/lane-config"; import { isRenovateIssue } from "@/lib/agent-queue"; import { fetchAgentQueueData } from "@/lib/agent-queue-fetch"; -import { applyRenovateIssueExclusion } from "@/lib/issue-filters"; +import { + applyRenovateIssueExclusion, + applyUmbrellaIssueExclusion, + buildGroomingStateExclusionWhere, +} from "@/lib/issue-filters"; export async function GET( request: Request, @@ -37,6 +41,20 @@ export async function GET( repository: { enabled: true }, }; applyRenovateIssueExclusion(issueWhere); + applyUmbrellaIssueExclusion(issueWhere); + + // Exclude issues already groomed recently or currently blocked/not-ready + const groomingStateWhere = buildGroomingStateExclusionWhere(24); + if (groomingStateWhere.AND) { + const existing = issueWhere.AND; + if (Array.isArray(existing)) { + existing.push(...groomingStateWhere.AND); + } else if (existing) { + issueWhere.AND = [existing, ...groomingStateWhere.AND]; + } else { + issueWhere.AND = groomingStateWhere.AND; + } + } const issues = await prisma.issue.findMany({ where: issueWhere, @@ -47,6 +65,9 @@ export async function GET( url: true, labels: true, currentLane: true, + groomedAt: true, + notReadyReason: true, + blockedReason: true, repository: { select: { fullName: true } }, }, orderBy: { number: "asc" }, diff --git a/src/lib/issue-filters.ts b/src/lib/issue-filters.ts index 75b9e9c..39a29c9 100644 --- a/src/lib/issue-filters.ts +++ b/src/lib/issue-filters.ts @@ -111,6 +111,45 @@ export function applyRenovateIssueExclusion(where: Record): voi appendIssueWhere(where, buildRenovateIssueExclusionWhere()); } +/** + * Build a Prisma where clause that excludes umbrella issues (issues with the + * "umbrella" label or titles starting with "Weekly tech debt audit:"). + */ +export function buildUmbrellaIssueExclusionWhere() { + return { + NOT: { + OR: [ + { labels: { has: "umbrella" } }, + { title: { startsWith: "Weekly tech debt audit:", mode: "insensitive" } }, + ], + }, + }; +} + +export function applyUmbrellaIssueExclusion(where: Record): void { + appendIssueWhere(where, buildUmbrellaIssueExclusionWhere()); +} + +/** + * Build a Prisma where clause that excludes issues already groomed within the + * given cooldown (default 24h) or currently blocked/not-ready. + */ +export function buildGroomingStateExclusionWhere(cooldownHours: number = 24) { + const cutoff = new Date(); + cutoff.setHours(cutoff.getHours() - cooldownHours); + + return { + AND: [ + // Not recently groomed (or never groomed) + { OR: [{ groomedAt: null }, { groomedAt: { lt: cutoff } }] }, + // Not currently blocked + { blockedReason: null }, + // Not currently marked not-ready + { notReadyReason: null }, + ], + }; +} + export const DEFAULT_DONE_RETENTION_DAYS = 7; export function getDoneRetentionDays(): number {