Operant Studio
OPER-QA-17

[OPER-QA-17] QaAgentCapability catalog (test-kind filter for resolver)

Suggested
Justin Cooke

Body

## Context

`AgentWorker` has a `capabilities` array today (used by dispatcher for PHI-eligibility gate per OPER-PHI-1). For QA mass-assignment (OPER-QA-16), the resolver needs to know which `TestCase.kind` each agent can execute so it doesn't materialize impossible assignments (e.g., a "PHI reviewer" agent shouldn't get UI E2E cases).

Add a small catalog table joining `AgentWorker` <-> `TestCase.kind` enum values. The resolver filters cases by member capabilities before creating assignments. Humans continue to be assigned all cases in scope (they self-triage).

## Goal

`QaAgentCapability` table exists, seeded from each `AgentWorker`'s declared `qaKinds` field. `resolveAssignmentsForProject()` skips cases whose `kind` isn't in the agent member's capability set. Materialized count is correct.

## Acceptance Criteria

- Migration `YYYYMMDD_oper_qa_17_agent_capability_catalog`:
  - Adds `"QaAgentCapability" (id, agentWorkerId, testKind, createdAt)` with unique index on `(agentWorkerId, testKind)`
  - Adds `testKind` FK-like enum values matching `TestCase.kind` (ui, e2e, api, unit, agent_eval, rls, migration, compliance)
  - Backfills from `AgentWorker.capabilities` where any capability matches a `testKind` name
- Prisma `qa.prisma` adds `QaAgentCapability` model + `agentWorker.qaCapabilities QaAgentCapability[]` back-relation.
- `resolveAssignmentsForProject()` fetches `qaCapabilities` for each agent member, filters cases by `case.kind IN capabilities` before materializing assignments.
- Vitest `packages/qa/src/__tests__/projects.capability-filter.test.ts`:
  - Agent with `[rls, migration]` capabilities + 4 cases (1 rls, 1 migration, 1 ui, 1 e2e) produces 2 assignments
  - Agent with no capabilities produces 0 assignments (idempotent, doesn't error)
  - Human members receive all cases regardless of capability catalog
- Small admin endpoint `POST /api/studio/[team]/qa/agent-workers/[id]/capabilities` for LEAD role to grant/revoke capabilities.

## Why this matters

Prevents wasted dispatcher runs on cases an agent can't execute. Makes the mass-assign UX honest: LEAD picks phase composition, resolver correctly fans out. Depends on OPER-QA-16.

blocked_by: [OPER-QA-16]


---
<!-- admission-rejected -->
**Admission rejected (missing_feature_id):** ticket body is missing `feature_id: FEAT-...` frontmatter — declare which feature this ticket implements

Attachments

Loading attachments…

Comments

Loading comments…