Operant Studio
OPER-QA-16

[OPER-QA-16] AgentWorker as QaTestProjectMember (agent + human fanout)

Suggested
Justin Cooke

Body

## Context

`OPER-127`'s `resolveAssignmentsForProject()` correctly materializes `TestAssignment` rows by walking `QaTestProjectMember` -> phases -> cases. Today `QaTestProjectMember` only accepts `userId` (humans). To mass-assign the same TestProject to a fleet of agent workers, `QaTestProjectMember` needs to accept `agentWorkerId` alongside `userId`, with a `XOR` constraint so a member row targets exactly one of the two.

This is a natural extension of the existing resolver — it does not change the resolver's shape, just what rows it fans across. Machine execution then flows through the same `TestAssignment` schema, the same `TestRun` lifecycle (OPER-QA-6 state machine), and the same evidence writeback path.

Reversible: `ALTER TABLE "QaTestProjectMember" DROP COLUMN "agentWorkerId"; DROP CHECK ...` if we regret it.

## Goal

`QaTestProjectMember` supports both `userId` and `agentWorkerId` (XOR). `resolveAssignmentsForProject()` correctly materializes one `TestAssignment` per (member, case) pair regardless of member kind. Mass-assign to N agents becomes: create N `QaTestProjectMember` rows with `agentWorkerId`, add the case rows to a phase, transition phase to `IN_PROGRESS`.

## Acceptance Criteria

- Migration `YYYYMMDD_oper_qa_16_agent_members` adds nullable `"agentWorkerId" TEXT` FK to `AgentWorker(id)`, adds `CHECK` constraint `(("userId" IS NOT NULL) != ("agentWorkerId" IS NOT NULL))` enforcing XOR.
- Prisma `qa.prisma` updates `QaTestProjectMember` with `agentWorkerId String?` + `agentWorker AgentWorker? @relation(...)`; marks `userId` as `String?` (was required).
- `resolveAssignmentsForProject()` in `packages/qa/src/projects.ts` unchanged in signature; internal fanout logic handles both member kinds identically.
- `TestAssignment` schema unchanged (already references `member` polymorphically via `QaTestProjectMember.id`).
- Vitest `packages/qa/src/__tests__/projects.agent-members.test.ts` proves:
  - A project with 2 human + 3 agent members and 4 cases materializes 20 assignments (5 * 4)
  - XOR constraint rejects a member row with both `userId` and `agentWorkerId` set
  - XOR constraint rejects a member row with neither set
  - RLS: agent members are team-scoped identically to user members
- No changes to POR-QA-1 BFF contract (agent members surface as generic `member` shapes to the Portico UI).

## Why this matters

Unblocks agent-side execution of the QA fleet at scale. Today we can mass-assign to humans only; after this a phase transition can fan out to any mix of humans + agents in one operation. Depends on OPER-127 (DONE) and OPER-PHI-1 (agent registry).

blocked_by: [OPER-127]


---
<!-- 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…