feat(api): add untriaged filter to /api/issues for grooming intake#394
Conversation
Add ?untriaged=true query parameter to GET /api/issues to surface open issues with no status/* label. This provides a grooming intake path via the main issues API, complementing the existing dedicated /api/issues/untriaged endpoint. - Add buildNoStatusWhere() helper in issue-filters.ts - Wire untriaged param into GET /api/issues route - Add tests for buildNoStatusWhere and combined filter behavior
There was a problem hiding this comment.
AI Automated Review
Full PR review.
Analysis engine: MiniMax-M2.7@https://litellm.jory.dev/v1 (anthropic) — routed smart (risk match: public_route_changes)
Recommendation: Approve
This PR adds a ?untriaged=true query parameter to GET /api/issues to surface open issues with no status/* label for grooming intake. The implementation is clean, well-tested, and correctly addresses the linked issue PR 390.
Change-by-Change Findings
src/lib/issue-filters.ts
Added buildNoStatusWhere() helper that returns a Prisma { hasNone: STATUS_LABELS } filter when includeUntriaged is true. The STATUS_LABELS constant is imported from @/types and includes all five status labels (status/backlog, status/ready, status/in-progress, status/in-review, status/done). The helper correctly returns undefined when includeUntriaged is false, allowing the caller to skip applying the filter.
src/app/api/issues/route.ts
Wired the untriaged query parameter into the main issues GET endpoint. The filter is applied AFTER building the visibleIssueWhere (which includes repository.enabled: true and excludes closed issues by default), and AFTER applying excluded labels. This layering is correct: visible issues → repository filter → decomposed filter → label filters → excluded labels → no-status filter.
Tests
Added unit tests in issue-filters.test.ts for buildNoStatusWhere() (false returns undefined; true returns hasNone with all five status labels) and integration tests in route.test.ts for the untriaged parameter (filters for untriaged when true, does not add hasNone when false, combines correctly with agent filter).
Must-Check Items
✅ verify route access controls are in place
The /api/issues route does not implement its own auth checks. According to the repository documentation in src/middleware.ts and AGENTS.md:
- Legacy mode (no
DISPATCH_AUTH_MODEset): No middleware enforcement; routes handle their own auth. API routes in legacy mode are effectively open. - Basic mode (
DISPATCH_AUTH_MODE="basic"): UI pages require Basic Auth; API routes also acceptDISPATCH_AGENT_TOKENbearer auth. - OIDC mode: UI pages require NextAuth session; API routes are authorized by route handlers.
This is the documented, existing behavior for /api/issues — this PR does not change the access control posture. The route was already publicly accessible in legacy mode. Adding a query parameter filter does not introduce a new security boundary.
✅ check for unintended public endpoints
This PR does not add a new endpoint — it extends the existing /api/issues endpoint with an optional query parameter. The untriaged filter does not expose any data that was not already accessible via the same endpoint without the filter; it merely narrows the result set. There are no new routes, no removed authentication, and no exposure of previously protected data.
Standards Compliance
- Error handling: Uses
error instanceof Errorpattern in the try/catch block (✅) - API routes return appropriate HTTP status codes: Returns 500 on error with JSON body (✅)
- Validation: Input is coerced with
untriaged === "true"before passing to the filter builder (✅) - No agent-specific names: Uses generic
agent/*patterns (✅)
Linked Issue Fit (PR 390)
All acceptance criteria from issue PR 390 are addressed:
- ✅ "An open issue with no
status/*label appears in grooming intake via both/api/issues/untriagedand/api/issues?untriaged=true" — The PR adds exactly this capability - ✅ "The same issue does not appear in normal/escalated worker queues by default (existing behavior preserved)" — The change is additive to the main issues API; agent queue endpoints (
/api/agents/{agentName}/queue) are unaffected - ✅ "Grooming can promote a no-status issue to
status/readyorstatus/backlog" — This is out of scope for the API change; the groom route already exists - ✅ "Renovate Dashboard issues do not get promoted accidentally" — The
buildVisibleIssueWherefilter is applied before the untriaged filter, ensuring closed issues are excluded - ✅ "Tests cover no-status grooming intake separately from worker queues" — Unit and integration tests added
- ✅ "Existing worker queue behavior from PR 291 remains intact" — No changes to agent queue endpoints; commit history confirms PR 291 fixed unlabelled orphan exclusion from queues
Evidence Provider Findings
No evidence providers were configured for this PR.
Tool Harness Findings
- Read
src/app/api/issues/route.ts— Confirmed the route implementation with proper filter ordering and error handling - Read
src/types/index.ts— ConfirmedSTATUS_LABELSis defined as a typed constant - Git grep for
middleware|auth|NextAuth|authorize— Confirmed auth architecture documented in middleware.ts and README - Git grep for
middleware— Confirmed legacy mode behavior and API route bearer auth pattern
Unknowns / Needs Verification
None. The PR is straightforward and fully verified from the diff, test coverage, and documentation.
Fixes #390
Add
?untriaged=truequery parameter toGET /api/issuesto surface open issues with nostatus/*label. This provides a grooming intake path via the main issues API, complementing the existing dedicated/api/issues/untriagedendpoint.Changes
src/lib/issue-filters.ts: AddbuildNoStatusWhere()helper that returns a Prisma{ hasNone: STATUS_LABELS }filter for untriaged issuessrc/app/api/issues/route.ts: Wire theuntriagedquery parameter into the main issues GET endpointbuildNoStatusWhere()and combined filter behavior (untriaged + agent filters)Acceptance criteria addressed
status/*label appears in grooming intake via both/api/issues/untriagedand/api/issues?untriaged=truestatus/readyorstatus/backlog(existing groom route)