# QA Agent API

The agent integration uses Supabase Edge Functions as a gateway. Agents never
receive a user's Supabase session token. The agent requests an activation URL,
the user signs in at the app and activates the claim, then the agent exchanges
its one-time request secret for a scoped `qa_agent_...` token.

Public app URL: `https://qa.felipeotarola.com`
Public agent docs: `https://qa.felipeotarola.com/qa-agent-api.md`

## Index

Start here:

- [Flow](#flow) - access approval and token exchange overview.
- [Recommended Agent Loop](#recommended-agent-loop) - the full project-first
  loop agents should follow for real QA work.

Access:

- [Request Access](#request-access) - create an activation claim.
- [Exchange Approval](#exchange-approval) - exchange an approved request for a
  scoped `qa_agent_...` token.

Projects and workspace memory:

- [List Projects](#list-projects)
- [Create Project](#create-project)
- [Project Brief](#project-brief) - one-call project intelligence for Sladdis
  before planning a run.
- [QA Workspace](#qa-workspace) - durable project context, safe test data,
  and retest queue.
- [Read QA Workspace](#read-qa-workspace)
- [Upsert Project Context](#upsert-project-context)
- [Create Test Data And Retest Items](#create-test-data-and-retest-items)
- [Create Agent Field Notes](#create-agent-field-notes) - visible notes in
  `/agent-notes`.
- [QA Work Board](#qa-work-board)
- [List Work Items](#list-work-items)
- [Update Work Item QA State](#update-work-item-qa-state)
- [Automation Packages V1](#automation-packages-v1)
- [List Automation Packages](#list-automation-packages)
- [Create Automation Package](#create-automation-package)
- [Create Automation Package Version](#create-automation-package-version)
- [Link Automation Assertion to Test Case](#link-automation-assertion-to-test-case)
- [List Automation Runs](#list-automation-runs)
- [Queue Automation Run](#queue-automation-run)
- [API Testing V1](#api-testing-v1)
- [Create API Target](#create-api-target)
- [Create API Auth Profile](#create-api-auth-profile)
- [Create API Test Case](#create-api-test-case)
- [Run API Test Case](#run-api-test-case)
- [Mobile Testing V1](#mobile-testing-v1)
- [Create Mobile Device](#create-mobile-device)
- [Create Mobile Test Case](#create-mobile-test-case)
- [Run Mobile Test Case](#run-mobile-test-case)

Product model:

- [Application Map](#application-map)
- [List Application Map](#list-application-map)
- [Upsert Application Map Node](#upsert-application-map-node)
- [Upsert Application Map Edge](#upsert-application-map-edge)
- [Link Test Case To Application Map Node](#link-test-case-to-application-map-node)
- [Generate Application Map From Test Cases](#generate-application-map-from-test-cases)

Test cases and evidence:

- [Test Suites](#test-suites)
- [List Test Cases](#list-test-cases)
- [Create Test Case](#create-test-case)
- [Update Test Case](#update-test-case)
- [Upload Test Case Screenshot](#upload-test-case-screenshot)

Runs and results:

- [List Test Runs](#list-test-runs)
- [Run Plan](#run-plan)
- [Create Run](#create-run)
- [Update Run](#update-run)
- [Add Result](#add-result)
- [Create Triage Action](#create-triage-action)

## Flow

1. Agent requests access with `agent-request-access`.
2. User opens the returned `activationUrl`, for example `/activate?claim=...`.
3. User signs in when needed and the activation page approves the claim.
4. Agent polls `agent-exchange-approval` with `requestId` and `requestSecret`.
5. Agent uses the returned token to find or create the QAA project first, then
   read workspace context, read test cases/runs, claim queued work requests,
   write QA runs/results, and report triage decisions.

## Request Access

```bash
curl -X POST "$SUPABASE_URL/functions/v1/agent-request-access" \
  -H "Content-Type: application/json" \
  -d '{
    "agentName": "Playwright QA Agent",
    "userEmail": "user@example.com",
    "appOrigin": "https://qa.felipeotarola.com",
    "scopes": ["projects:read", "projects:write", "test_cases:write", "test_runs:write", "test_results:write", "qa_manager:write"]
  }'
```

The response contains `requestId`, `requestSecret`, `claim`, and
`activationUrl`. The agent should show the app-domain activation URL to the
user. A `projectId` may still be provided to request access to only one project;
when omitted, the approved token can list available QA projects.

## Exchange Approval

```bash
curl -X POST "$SUPABASE_URL/functions/v1/agent-exchange-approval" \
  -H "Content-Type: application/json" \
  -d '{
    "requestId": "REQUEST_UUID",
    "requestSecret": "qa_req_..."
  }'
```

Pending approvals return `202`. Approved requests return `agentToken`.

## List Projects

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-list-projects" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json"
```

Requires `projects:read`. Returns active projects unless the token is scoped to
one project, in which case only that project is returned.

## Create Project

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-create-project" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Storefront",
    "key": "acme-storefront",
    "publicSlug": "acme-storefront",
    "description": "Public storefront regression suite",
    "baseUrl": "https://acme.example.com",
    "status": "Active"
  }'
```

Requires `projects:write`. Project-scoped agent tokens cannot create projects;
the token must be workspace-wide. If `key` is omitted, the endpoint creates a
slug from `name`. If `publicSlug` is omitted, the endpoint uses the project key.
`status` defaults to `Active`. The endpoint also accepts `slug`, `public_slug`,
`publicUrlSlug`, `public_url_slug`, and `publicProjectSlug` as public slug
aliases.

Response shape:

```json
{
  "project": {
    "id": "PROJECT_UUID",
    "name": "Acme Storefront",
    "key": "acme-storefront",
    "description": "Public storefront regression suite",
    "base_url": "https://acme.example.com",
    "status": "Active",
    "visibility": "private",
    "public_slug": "acme-storefront",
    "public_share_title": "Acme Storefront QA Report",
    "public_share_description": "Read-only QA report for Acme Storefront.",
    "public_allow_indexing": false,
    "created_at": "2026-06-16T17:00:00.000Z",
    "updated_at": "2026-06-16T17:00:00.000Z"
  }
}
```

Use this endpoint as the first durable setup step when the agent is working on
a project that does not already exist in QAA. Creating the project first gives
the agent a stable `projectId` for workspace context, test data, application
map nodes, test cases, runs, results, defects, and field notes. For existing
projects, call `qa-list-projects` first and reuse the matching `id`.

## Project Brief

Use `qa-agent-project-brief` immediately after selecting or creating the QAA
project. This is the project-first read model for Sladdis: one call returns the
QAA state needed to decide whether the project is safe to release, what needs
attention, and what should be tested next.

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-agent-project-brief" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID"
  }'
```

Requires `projects:read`. If the token is scoped to one project, `projectId` is
optional and any other project id is rejected.

The signed-in app route is also available:

```bash
curl "https://qa.felipeotarola.com/api/qa-project-brief?projectId=PROJECT_UUID" \
  -H "Cookie: authenticated_app_session=..."
```

Response shape:

```json
{
  "brief": {
    "projectId": "PROJECT_UUID",
    "project": {
      "id": "PROJECT_UUID",
      "name": "Acme Storefront",
      "key": "acme-storefront"
    },
    "decision": {
      "releaseState": "blocked",
      "testConfidence": 78,
      "confidenceDelta": null,
      "blockingCount": 2,
      "advisoryCount": 1,
      "dataQualityWarningCount": 0,
      "lastVerifiedAt": "2026-06-16T17:00:00.000Z",
      "freshnessState": "fresh",
      "ageHours": 2
    },
    "attention": [
      {
        "id": "coverage-checkout",
        "type": "coverage_gap",
        "impact": "release_blocking",
        "priority": "critical",
        "title": "Checkout",
        "reason": "critical gap · 1 linked tests",
        "href": "/application-map"
      }
    ],
    "context": {
      "product_profile": "Public storefront for trial signups.",
      "key_flows": ["Homepage", "Signup", "Pricing"],
      "risk_areas": ["Lead capture", "Mobile navigation"]
    },
    "readiness": {
      "safeTestDataCount": 1,
      "openRetestCount": 2,
      "contextSignalCount": 7
    },
    "coverage": {
      "counts": {
        "covered_passing": 4,
        "covered_failing": 1,
        "partial": 2,
        "untested": 3,
        "blocked": 1,
        "critical_gap": 1
      },
      "gaps": [
        {
          "label": "Checkout",
          "coverageState": "covered_failing",
          "priority": "high",
          "failedCount": 2,
          "testCaseIds": ["ACME-CHECKOUT-001"]
        }
      ]
    },
    "tests": {
      "total": 27,
      "statusCounts": {
        "Passed": 18,
        "Failed": 7,
        "Blocked": 1,
        "Not run": 1
      },
      "top": []
    },
    "runs": {
      "health": {
        "releaseHealth": "failing",
        "totals": {
          "runs": 7,
          "failedRuns": 5,
          "blockedTests": 1,
          "failingTests": 9
        }
      },
      "recentFailures": []
    },
    "work": {
      "activeItems": [],
      "activeDefects": []
    },
    "retests": {
      "open": [],
      "totalOpen": 2,
      "totalRetesting": 0
    },
    "runPlans": {
      "active": [],
      "recentCompleted": []
    },
    "notes": {
      "recent": [],
      "memory": [],
      "nextTest": []
    }
  }
}
```

The brief intentionally composes existing QAA primitives instead of creating a
parallel intelligence store. Do not create duplicate "project intelligence",
"coverage heatmap", or "memory" tables before this brief shows a real schema
gap. Current source-of-truth mapping:

- Project/product facts: `qa_project_context`.
- Test data and safe rules: `qa_test_data` and `qa_project_context`.
- Coverage heatmap: `qa_app_map_nodes`, `qa_test_case_app_nodes`, and latest
  test status.
- Execution health: `qa_test_runs` and `qa_test_run_results`.
- Reproducible evidence: `qa_test_evidence`, `qa_api_run_evidence`, and
  `qa_mobile_run_evidence`.
- Bugs/action handoff: `qa_work_items` with `item_type = bug`.
- Retests: `qa_retest_items`.
- Saved pre-run intent and plan-vs-actual comparison: `qa_run_plans`.
- Narrative, memory, and next-test notes: `qa_agent_field_notes`.
- Triage/audit decisions: `qa_triage_actions` and `qa_agent_audit_logs`.

The `/qa-overview` app page should become Project Intelligence rather than a
separate overview dashboard. It should be built from this brief and show the
product profile, key flows, risk areas, known weak spots, coverage map, active
defects, retest queue, active run plans, and confidence score per area. Keep
QAA as the data layer and review surface; Sladdis should do the reasoning and
write the resulting plan, evidence, and notes back into these primitives.

The recommended rollout is:

1. Use this brief as Sladdis' first read after project selection.
2. Build coverage heatmap UI from `brief.coverage`.
3. Use Retest Queue V2 links on `qa_retest_items`: `test_case_id`, `run_id`,
   `result_id`, `work_item_id`, `resolved_by_run_id`, and `resolved_at`.
4. Save pre-run intent in `qa_run_plans` before execution and compare it with
   the linked `qa_test_runs` actual outcome after execution.
5. Generate next-best-test recommendations from the brief first; persist them
   later only if history/review becomes useful.

## QA Workspace

QAA is the system of record for project Quality Assurance. If an agent has QAA
access, it should read project workspace context before testing and persist
workspace updates after testing. Do not only report findings in chat when a QAA
project is available.

The workspace contains durable project context, safe test data, agent
instructions, and retest queue items. This is what lets Sladdis act as the QA
owner for a project instead of a one-off URL checker.

Use `qa-create-agent-note` in [Create Agent Field Notes](#create-agent-field-notes)
for all QA narrative, observations, reasoning, and memory that should appear in
`/agent-notes`.

### Read QA Workspace

```bash
curl -X GET "$SUPABASE_URL/functions/v1/qa-workspace?projectId=PROJECT_UUID" \
  -H "Authorization: Bearer qa_agent_..."
```

Requires `projects:read`. Returns `context`, `testData`, and `retestItems`.
Agents should call this before planning a run.

Response shape:

```json
{
  "projectId": "PROJECT_UUID",
  "context": {
    "project_id": "PROJECT_UUID",
    "product_profile": "Public storefront for trial signups.",
    "key_flows": ["Homepage", "Signup", "Pricing", "Contact"],
    "risk_areas": ["Auth", "Lead capture", "Mobile navigation"],
    "testing_rules": [
      "Do not submit real orders",
      "Stop before payment capture"
    ],
    "agent_instructions": "Run safe smoke first, persist evidence, queue retests for failures.",
    "updated_by": "Sladdis"
  },
  "testData": [
    {
      "id": "TEST_DATA_UUID",
      "label": "Login test user",
      "data_type": "credential",
      "sensitivity": "sensitive",
      "usage_notes": "Use only for login smoke testing when explicitly requested.",
      "status": "active"
    }
  ],
  "retestItems": [
    {
      "id": "RETEST_UUID",
      "title": "Retest checkout validation after fix",
      "status": "open",
      "priority": "high",
      "source_type": "result",
      "source_id": "RUN_RESULT_UUID"
    }
  ]
}
```

The app-session route is also available for the signed-in UI:

```bash
curl "https://qa.felipeotarola.com/api/qa-workspace?projectId=PROJECT_UUID" \
  -H "Cookie: authenticated_app_session=..."
```

### Upsert Project Context

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-workspace" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "action": "upsert-context",
    "projectId": "PROJECT_UUID",
    "productProfile": "Public storefront for trial signups.",
    "keyFlows": "Homepage\nSignup\nPricing\nContact",
    "riskAreas": "Auth\nLead capture\nMobile navigation",
    "testingRules": "Do not submit real orders\nStop before payment capture",
    "agentInstructions": "Run safe smoke first, persist evidence, queue retests for failures."
  }'
```

Requires `test_cases:write`.

### Create Test Data And Retest Items

Use the same endpoint with `action` set to `create-test-data`,
`create-retest-item`, or `update-retest-status`. Test data must be classified as
`safe`, `sensitive`, `temporary`, or `do_not_reuse`. Retest status values are
`open`, `retesting`, `resolved`, and `ignored`.

For notes, observations, reasoning, and project memory, use the separate
`qa-create-agent-note` Edge Function so the content appears in `/agent-notes`.

Retest Queue V2:

- Link every retest to the failed evidence when possible: `testCaseId`,
  `runId`, `resultId`, and `workItemId`.
- Use `sourceType: "result"` and `sourceId: "RUN_RESULT_UUID"` when the retest
  comes from a failed or blocked result.
- When a retest passes, call `update-retest-status` with `status: "resolved"`
  and `resolvedByRunId`. QAA stores `resolved_at` and can show "fixed after run
  X".
- Keep `qa_work_items` as the bug/action handoff and `qa_retest_items` as the
  verification queue. Do not create a second retest model in Agent Notes or
  triage actions.

Example: saving credentials or other user-provided test data:

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-workspace" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "action": "create-test-data",
    "projectId": "PROJECT_UUID",
    "label": "Login test user",
    "dataType": "credential",
    "value": "email=test-user@example.com\npassword=EXAMPLE_ONLY_DO_NOT_USE",
    "sensitivity": "sensitive",
    "usageNotes": "Example only. Store real credentials only when the user explicitly provides test credentials and the project rules allow authenticated checks."
  }'
```

Example: creating a linked retest from a failed result:

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-workspace" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "action": "create-retest-item",
    "projectId": "PROJECT_UUID",
    "title": "Retest checkout validation after fix",
    "reason": "Checkout failed at step 2 with validation error.",
    "priority": "critical",
    "sourceType": "result",
    "sourceId": "RUN_RESULT_UUID",
    "testCaseId": "ACME-CHECKOUT-001",
    "runId": "run-checkout-regression",
    "resultId": "RUN_RESULT_UUID",
    "workItemId": "WORK_ITEM_UUID"
  }'
```

Example: resolving a retest after a passing rerun:

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-workspace" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "action": "update-retest-status",
    "projectId": "PROJECT_UUID",
    "id": "RETEST_UUID",
    "status": "resolved",
    "resolvedByRunId": "run-checkout-rerun",
    "resolutionNotes": "Checkout validation passed after deploy."
  }'
```

When a user tells the agent "this is test data", the agent should save it here
before or after the run, classify sensitivity conservatively, and reference it
in future project QA planning.

Agent rules for test data:

- Default to `sensitive` for credentials, tokens, personal data, customer data,
  production identifiers, or anything that could grant access.
- Use `temporary` for values the agent creates during a single run and may
  delete or ignore after the run.
- Use `do_not_reuse` for one-time codes, expired links, destructive data, or
  user-provided values that should not be tried again.
- Do not paste sensitive test data into Slack, public reports, screenshots,
  defect titles, or run summaries. Reference the test data by `label` or `id`
  instead.
- If the user says "this is test data", save it to the workspace before or
  immediately after the run, unless doing so would store secrets outside the
  agreed QAA boundary.
- Never invent real credentials. Generated credentials must be clearly marked as
  fake, temporary, or example-only.

## QA Work Board

The QA Work Board is the human-to-agent delivery board in QAA. It is separate
from durable test coverage: humans own the product scope, title, description,
priority, due date, and acceptance criteria; the agent reads those fields,
chooses relevant QAA tests, runs them, and writes back QA state.

Board statuses:

- `backlog`
- `in_progress`
- `ready_for_qa`
- `testing`
- `tested`
- `done`

Agents should normally start from `ready_for_qa`, move a card to `testing`
while execution is running, then move it to `tested` or back to `in_progress`
with a concise QA summary and linked run id. When QA finds a bug, the ticket is
not complete: set `qaStatus` to `failed`, write the bug fields, and move
`status` back to `in_progress`. If the agent cannot test because of a blocker,
set `qaStatus` to `blocked`, explain the blocker in `qaSummary`, and move or
leave `status` as `in_progress`. `done` is the final accepted state after the
work has passed QA and been handed off.

Agent-writable fields:

- `status`
- `qaStatus`
- `qaRunId`
- `qaSummary`
- `qaRisk`
- `qaAgentNotes`
- `agentPrompt`
- `bugTitle`
- `bugDescription`
- `bugSeverity`
- `releaseImpact`
- `releaseImpactReason`

Do not edit title, description, acceptance criteria, priority, owner, due date,
or product scope from the agent.

### List Work Items

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-list-work-items" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID",
    "status": "ready_for_qa",
    "limit": 50
  }'
```

Requires `projects:read`. If the token is scoped to one project, `projectId`
is optional and any other project id is rejected. Supported filters:
`projectId`, `status`, `qaStatus`, `itemType`, `search`, and `limit`.

Response shape:

```json
{
  "workItems": [
    {
      "id": "WORK_ITEM_UUID",
      "project_id": "PROJECT_UUID",
      "title": "Mobile login smoke reaches welcome screen",
      "description": "Run Android real-device smoke for package com.example.admin.",
      "status": "ready_for_qa",
      "priority": "high",
      "item_type": "qa",
      "acceptance_criteria": [
        "App launches on a safe Android device",
        "Welcome text appears after login"
      ],
      "target_url": "com.example.admin",
      "app_area": "Mobile Auth",
      "linked_test_case_ids": [],
      "qa_status": "not_tested",
      "qa_run_id": null,
      "qa_summary": "",
      "qa_risk": "medium",
      "release_impact": "informational",
      "release_impact_reason": "",
      "qa_agent_notes": "",
      "agent_prompt": ""
    }
  ]
}
```

### Update Work Item QA State

```bash
curl -X PATCH "$SUPABASE_URL/functions/v1/qa-update-work-item" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "id": "WORK_ITEM_UUID",
    "projectId": "PROJECT_UUID",
    "status": "in_progress",
    "qaStatus": "failed",
    "qaRunId": "run-mobile-login",
    "qaSummary": "Login smoke failed because Welcome text never appeared.",
    "qaRisk": "high",
    "qaAgentNotes": "Reproduced on Android staging device. Screenshot evidence is attached to the run result.",
    "agentPrompt": "You are fixing a QA failure. Reproduce the Android login smoke, inspect the auth flow, implement the smallest fix, add regression coverage, rerun QAA, and update this ticket.",
    "bugTitle": "Android login does not reach Welcome screen",
    "bugDescription": "After tapping Login with approved test data, the app remains on the login screen without a crash signal.",
    "bugSeverity": "P2",
    "releaseImpact": "non_blocking",
    "releaseImpactReason": "The defect needs repair but does not invalidate the current release-critical flows."
  }'
```

Requires `test_results:write`. Allowed agent transitions are intentionally
limited to QA handoff movement:

- `backlog -> in_progress | ready_for_qa`
- `in_progress -> ready_for_qa`
- `ready_for_qa -> testing | in_progress`
- `testing -> ready_for_qa | in_progress | tested`
- `tested -> ready_for_qa | testing | done`
- `done -> ready_for_qa | testing`

Use `qaRunId` whenever the update is based on a QAA run. If a bug is found,
move the card back to `in_progress`, set `qaStatus` to `failed`, and write a
short `bugTitle`, reproducible `bugDescription`, and severity `P1`, `P2`, `P3`,
or `P4`. `agentPrompt` is the repair prompt for a coding agent: include the
failed behavior, reproduction target, expected outcome, and required checks,
but do not include secrets, raw logs, or sensitive screenshot contents. Attach
redacted evidence to the run instead. If the test is blocked, use
`status: "in_progress"` with `qaStatus: "blocked"` rather than a board column.
Set `releaseImpact` explicitly to `release_blocking`, `non_blocking`,
`data_quality`, or `informational`, and explain the decision in
`releaseImpactReason`. Do not infer release impact from severity alone when the
product context provides a more precise answer.

Every work item update creates an append-only `qa_work_item_events` history row
with the changed fields, previous values, new values, actor, and source. Do not
erase older bug feedback to describe a new retest. Update the current summary
fields with the latest state; QAA preserves prior developer fixes, failed
retests, severity changes, and handoff movement in ticket history.

## Automation Packages V1

Automation packages are versioned, project-linked source assets owned by QAA.
V1 supports QAA-managed Playwright drafts, code inspection, and zip export.
The web runner capability is `configured` when the isolated host worker can
claim validation jobs. Agents must still use the persisted run result rather
than treating a queued or running draft as executed.

Core rules:

- Match or create the QAA project before creating a package.
- Use `playwright` for QAA-managed V1 packages.
- Keep package paths relative and bounded; never include secrets.
- Begin at `draft`. Validation and execution are separate capabilities.
- Treat queued/running packages as `not-run`; only a persisted terminal run is evidence.
- Treat browser load checks as bounded capacity probes, not unrestricted stress
  tests. Use a separate package, read-only navigation, one queued run initially,
  no authenticated or mutating flows, and explicit concurrency/request limits.
- Record the runner container limits with the result. Do not present a
  container-limited run as whole-host capacity evidence.
- The default ceiling for an initial capacity probe is four concurrent pages,
  25 total navigations, 30 seconds per test, and one run. Raising any ceiling
  requires reviewing target ownership, worker headroom, and stop conditions.
- Stop escalation on timeouts, HTTP 429/5xx responses, runner blocking, OOM,
  swap growth, or sustained host pressure.
- Requires `projects:read` to list and `test_cases:write` to create.
- Sladdis owns package selection, queueing, and interpretation. The isolated
  host worker only claims and executes the persisted job.
- GitHub export is a reviewed external mutation. A project owner/admin connects
  a repository token in QAA, where it is encrypted in Supabase Vault and never
  returned to an agent or browser. Sladdis may prepare the immutable package
  version and target path, but a signed-in project member must explicitly
  approve creation of the branch and pull request in QAA.
- QAA remains the system of record for QA history and evidence. After an
  exported pull request is merged, Git is the source of truth for that
  repository-managed test source.
- PR, deployment, and schema triggers are persisted disabled by default. An
  owner/admin receives the signing secret once, configures the provider
  webhook, and can activate only after the latest three terminal package runs
  all passed. Webhooks are HMAC-SHA256 verified and delivery-id deduplicated.
  Agents must not activate triggers or handle their signing secrets.

### List Automation Packages

```bash
curl "$SUPABASE_URL/functions/v1/qa-automation-packages?projectId=PROJECT_UUID" \
  -H "Authorization: Bearer qa_agent_..."
```

Pass `id=PACKAGE_UUID` to read one package including its latest version and
source files.

### Create Automation Package

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-automation-packages" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID",
    "name": "Public site smoke",
    "description": "Stable read-only checks",
    "targetUrl": "https://example.com",
    "framework": "playwright",
    "files": [
      {
        "path": "playwright.config.ts",
        "mediaType": "text/typescript",
        "content": "import { defineConfig } from \"@playwright/test\"; export default defineConfig({ workers: 1 });"
      },
      {
        "path": "tests/homepage.spec.ts",
        "mediaType": "text/typescript",
        "content": "import { test, expect } from \"@playwright/test\"; test(\"home\", async ({ page }) => { await page.goto(\"https://example.com\"); await expect(page).toHaveTitle(/Example/); });"
      }
    ]
  }'
```

The response returns the immutable draft version and
`runnerCapability: "configured"`. Human users can inspect and download the
same package from `/automation-packages`.

### Create Automation Package Version

Create an immutable next revision of an existing QAA-managed package:

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-automation-packages" \
  -H "Authorization: Bearer $QAA_AGENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "create-version",
    "projectId": "PROJECT_UUID",
    "packageId": "PACKAGE_UUID",
    "baseVersionId": "CURRENT_VERSION_UUID",
    "changelog": "Add navigation regression coverage",
    "files": [{"path":"tests/navigation.spec.ts","mediaType":"text/typescript","content":"..."}]
  }'
```

`baseVersionId` is an optimistic concurrency guard. QAA rejects stale edits and
no-op revisions. Success creates `vN+1`; it never modifies earlier source,
hashes, runs, or artifacts. New runs pin the latest version while historical
runs remain pinned to the version they executed.

### Link Automation Assertion to Test Case

Link an exact Playwright `file::title` key to an existing QAA test case:

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-automation-packages" \
  -H "Authorization: Bearer $QAA_AGENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "link-test-case",
    "projectId": "PROJECT_UUID",
    "packageId": "PACKAGE_UUID",
    "testCaseId": "WEB-HOME-001",
    "testKey": "tests/homepage.spec.ts::homepage responds"
  }'
```

The package and test case must belong to the same project. `*` is supported
only as a single-test package fallback. A terminal run materializes linked
assertions into the normal QAA test run and result history. A failure creates
one deduplicated Work Board finding plus an open retest; repeated failures
update that handoff, and a later pass resolves both the retest and finding.

### List Automation Runs

```bash
curl "$SUPABASE_URL/functions/v1/qa-automation-runs?projectId=PROJECT_UUID&packageId=PACKAGE_UUID" \
  -H "Authorization: Bearer qa_agent_..."
```

Requires `projects:read`. Treat `queued` and `running` as `not-run`. Only
`passed`, `failed`, or `blocked` is terminal evidence. A `blocked` run means
the execution engine or policy could not produce a valid Playwright result; it
must not be reported as a product failure.

### Queue Automation Run

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-automation-runs" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "action": "queue",
    "packageId": "PACKAGE_UUID"
  }'
```

Requires `test_results:write`. The runner selects the newest runnable immutable
version, applies exact-origin and resource policy, and persists status,
statistics, a bounded log excerpt, package version, target, and runner version.
It uploads private screenshots, traces, failure videos, JSON reports, and HTML
reports when Playwright produces them. Every artifact is project/run scoped,
size and type limited, SHA-256 verified after upload, inventoried in
`qa_automation_run_artifacts`, and available to signed-in project members from
`/automation-packages`. Do not claim an artifact exists unless it appears on
the persisted terminal run.

## API Testing V1

API testing is a first-class test type beside web testing. Agents must not use
free-form fetch access for project APIs. API requests should be represented as
API targets, auth profiles, API test cases, and controlled runner executions so
QAA owns safety policy, auth injection, redaction, and evidence persistence.

Core rules:

- Create an API target before creating API cases.
- Store only secret references in QAA. Do not write secret values into test
  data, notes, case bodies, or reports.
- Production mutation policy should be `read_only` or `approval_required`.
- Runner evidence is redacted before persistence.
- Public reports only show redacted API summaries.

Mutation policies:

- `read_only`: only `GET`, `HEAD`, and `OPTIONS`.
- `safe_mutations`: mutations only when the target is marked safe.
- `approval_required`: mutations require explicit human approval.
- `blocked`: mutations are never allowed.

Response body storage:

- `none`: store no body.
- `excerpt`: store a redacted excerpt.
- `full_redacted`: store full redacted body only when the target is marked as a
  safe environment; otherwise the runner downgrades to `excerpt`.

### Create API Target

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-workspace" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "action": "create-api-target",
    "projectId": "PROJECT_UUID",
    "name": "Customer API",
    "environment": "staging",
    "baseUrl": "https://api.example.com/v1",
    "mutationPolicy": "approval_required",
    "allowedMethods": "GET\nHEAD\nOPTIONS",
    "forbiddenPaths": "/admin\n/billing",
    "storeResponseBody": "excerpt",
    "timeoutMs": "15000",
    "isSafeEnvironment": "false"
  }'
```

### Create API Auth Profile

Auth profiles store metadata and secret references only. Secret values are
resolved by the runner environment.

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-workspace" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "action": "create-api-auth-profile",
    "projectId": "PROJECT_UUID",
    "name": "Staging bearer token",
    "authType": "bearer",
    "secretRef": "staging_api_token",
    "placement": "header",
    "headerName": "Authorization"
  }'
```

### Create API Test Case

Creates a normal QAA test case with `test_type = api` and the linked
`qa_api_test_cases` runner details.

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-workspace" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "action": "create-api-test-case",
    "projectId": "PROJECT_UUID",
    "targetId": "API_TARGET_UUID",
    "title": "Customer API returns current profile",
    "area": "Customers API",
    "method": "GET",
    "path": "/customers/me",
    "expectedStatus": "200",
    "authMode": "target_default",
    "queryTemplate": "{}",
    "headersTemplate": "{\"accept\":\"application/json\"}",
    "assertions": "[{\"type\":\"status_equals\",\"value\":200},{\"type\":\"json_path_exists\",\"path\":\"data.id\"}]",
    "steps": [
      {
        "action": "Send GET request to /customers/me.",
        "expected": "The request is executed against the configured API target policy."
      },
      {
        "action": "Validate the HTTP response status and response payload.",
        "expected": "Status is 200 and JSON path data.id exists."
      }
    ]
  }'
```

If `steps` is omitted, QAA creates a small default API procedure from the
stored method, path, expected status, and assertions. Prefer sending explicit
steps when the case documents business meaning beyond a raw endpoint check.

### Run API Test Case

The Edge Function runs the controlled API runner, applies QAA target policy,
persists the run result, and stores redacted API evidence. Agents must not send
URL, method, headers, body, or auth secrets to this endpoint. The runner loads
those details from the saved QAA API target and API test case.

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-run-api-test-case" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID",
    "testCaseId": "API-CUSTOMER-...",
    "runId": "RUN_ID",
    "approvedMutation": false
  }'
```

Requires `test_results:write`. Project-scoped tokens can only run API test
cases for their own project. Failed API runner results create the same linked
QA Work Board bug ticket and `agent_prompt` as `qa-add-result`. The response
includes a compact runner summary:

```json
{
  "result": { "status": "Passed" },
  "workItem": null,
  "workItemError": null,
  "autoTicket": {
    "functionVersion": "qa-run-api-test-case:auto-ticket-v2",
    "normalizedStatus": "Passed",
    "attempted": false,
    "createdOrReused": false,
    "error": null
  },
  "runner": {
    "request": {
      "method": "GET",
      "url": "https://api.example.com/customers/me?api_key=%5BREDACTED%5D"
    },
    "response": { "status": 200, "bodyStorage": "excerpt" },
    "assertions": [
      { "type": "status_equals", "passed": true, "message": "Status was 200." }
    ]
  }
}
```

The signed-in app route may still exist as a UI-only/internal runner surface,
but agent automation should use the Edge Function above.

Runner safety checks:

- Method whitelist is strict.
- Paths must be relative to the target base URL and cannot be absolute or
  protocol-relative.
- Redirects are not followed automatically.
- Bodies are not sent for `GET` or `HEAD`.
- Assertions run even when the expected status is non-2xx.
- Headers, query values, request bodies, response headers, and response bodies
  are redacted before persistence.

## Mobile Testing V1

Mobile testing is a first-class test type beside web and API testing. QAA owns
the project configuration, safe device policy, mobile test case metadata, run
status, and redacted evidence. The ADB execution engine lives in the harness or
agent runtime, not in the app UI.

Agents must not run arbitrary ADB commands from QAA. A mobile run is limited to
the stored mobile test case contract and the configured device policy.

Core rules:

- Create a mobile device before creating mobile cases.
- Only Android is supported in V1.
- Do not require Appium or Playwright for the first mobile path.
- Devices must be `enabled` and `is_safe_device = true` before execution.
- App packages must match the device `allowed_packages` list when configured.
- Raw screenshots, UIAutomator dumps, and logcat output belong in the harness
  first. QAA stores only paths and redacted excerpts.
- Harness-provided mobile screenshot URLs are mirrored into the same
  `qa_test_evidence` gallery used by web runs when they are public-renderable
  `http(s)` artifact URLs.
- Public reports show only a redacted mobile summary by default. Do not expose
  logcat, UI dumps, secrets, or unapproved screenshots publicly.

Supported mobile steps:

```json
[
  { "type": "tap", "target": "text=Login" },
  { "type": "tap", "x": 120, "y": 600 },
  { "type": "text", "valueRef": "test_user_email" },
  { "type": "swipe", "direction": "up" },
  { "type": "press", "key": "BACK" },
  { "type": "wait_for_text", "value": "Welcome" },
  { "type": "screenshot" }
]
```

Harness implementations should prefer UIAutomator selectors from `text`,
`resource-id`, and `content-desc` before falling back to coordinates.

Supported mobile assertions:

```json
[
  { "type": "text_visible", "value": "Welcome" },
  { "type": "text_not_visible", "value": "Crash" },
  { "type": "element_exists", "selector": "resource-id=com.app:id/login" },
  { "type": "current_package_equals", "value": "com.example.app" },
  { "type": "no_crash_in_logcat" },
  { "type": "screen_changed" }
]
```

### Create Mobile Device

Use the workspace endpoint to register a safe Android test device and policy.

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-workspace" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "action": "create-mobile-device",
    "projectId": "PROJECT_UUID",
    "name": "Pixel staging device",
    "deviceSerial": "emulator-5554",
    "model": "Pixel 8",
    "osVersion": "Android 15",
    "appPackage": "com.example.app",
    "environment": "staging",
    "isSafeDevice": "true",
    "allowedPackages": "com.example.app\ncom.example.app.staging",
    "forbiddenActions": "",
    "notes": "Dedicated staging Android device. Do not use against production accounts."
  }'
```

### Create Mobile Test Case

Creates a normal QAA test case with `test_type = mobile` and the linked
`qa_mobile_test_cases` runner details.

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-workspace" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "action": "create-mobile-test-case",
    "projectId": "PROJECT_UUID",
    "deviceId": "DEVICE_UUID",
    "title": "Android login smoke reaches welcome screen",
    "area": "Mobile Auth",
    "appPackage": "com.example.app",
    "launchMode": "package",
    "preconditions": "{\"account\":\"test_user_email\"}",
    "steps": "[{\"type\":\"tap\",\"target\":\"text=Login\"},{\"type\":\"text\",\"valueRef\":\"test_user_email\"},{\"type\":\"tap\",\"target\":\"text=Continue\"},{\"type\":\"wait_for_text\",\"value\":\"Welcome\"}]",
    "assertions": "[{\"type\":\"text_visible\",\"value\":\"Welcome\"},{\"type\":\"no_crash_in_logcat\"}]",
    "safetyLevel": "safe_interaction",
    "timeoutMs": "60000"
  }'
```

Safety levels:

- `read`: capture screen/UI tree/log signals only.
- `safe_interaction`: taps, text input using approved test data, swipes, back,
  waits, and screenshots.
- `destructive`: requires explicit human approval and should normally be
  avoided in shared environments.

### Run Mobile Test Case

The signed-in app exposes a runner contract at:

```bash
curl -X POST "$APP_URL/api/qa-mobile-runner" \
  -H "Cookie: authenticated_app_session=..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID",
    "testCaseId": "MOB-LOGIN-...",
    "deviceId": "DEVICE_UUID",
    "approvedInteraction": false
  }'
```

The app validates QAA policy and then calls the mobile harness when
`QA_MOBILE_HARNESS_URL` is configured. Without a configured harness, the runner
blocks safely and persists redacted evidence explaining that no ADB execution
occurred.

Harness input shape:

```json
{
  "deviceSerial": "emulator-5554",
  "appPackage": "com.example.app",
  "launch": { "mode": "package", "value": null },
  "safetyLevel": "safe_interaction",
  "timeoutMs": 60000,
  "steps": [],
  "assertions": []
}
```

Harness response shape:

```json
{
  "status": "passed",
  "durationMs": 12340,
  "assertionResults": [
    {
      "type": "text_visible",
      "passed": true,
      "message": "Welcome was visible."
    }
  ],
  "screenshotBeforePath": "https://...",
  "screenshotAfterPath": "https://...",
  "uiDumpExcerpt": "redacted UIAutomator excerpt",
  "logcatExcerpt": "redacted crash/log signal excerpt",
  "blockedReason": null,
  "errorMessage": null
}
```

Agents should treat mobile screenshots and logcat as sensitive. Store only
redacted excerpts and stable artifact paths returned by the harness. Use
Application Map screenshots for stable product surfaces, test case screenshots
for expected reference states, and run evidence screenshots for what happened
during a specific execution.

## Application Map

The Application Map is the diagram workspace for how the system behaves, where tests
attach, and where risk lives. It is separate from executable test cases. Use it
to describe pages, components, customer flows, API calls, repo/service
dependencies, external systems, and important UI states.

Agents should write durable nodes and edges first. The UI can render those
records as a heatmap, Mermaid-style diagrams, app/customer/API/repo flows, and
coverage-shape charts. Link test cases to map nodes only when a case actually
covers or validates that surface. Stakeholders use this map to see product
structure, missing coverage, risky dependencies, and visual context.

Recommended node/edge modeling:

- Build structured graph records first. Mermaid, React Flow, and other diagrams
  are renderers/export views, not the source of truth.
- App behavior layer (`layer: "application"`): page/state/component/flow nodes
  connected by `navigation`, `redirect`, `modal`, or `contains` edges.
- API layer (`layer: "api"`): `api_endpoint`, `api_operation`, `service`, and
  `external_service` nodes connected by `calls`, `api`, or `dependency` edges.
  Include auth requirements, methods, status families, mutation policy, and
  redaction notes in `metadata`.
- Data layer (`layer: "data"`): `data_entity`, `data_store`, and
  `database_table` nodes connected by `reads`, `writes`, `owns`, or
  `dependency` edges. Include sensitive fields, ownership, source, and evidence
  in `metadata`.
- Coverage layer (`layer: "coverage"`): `test_case` and `coverage_area` nodes
  connected to application/API/data nodes conceptually through `covers`,
  `validates`, `blocks`, or `related` edges. Continue using
  `qa-link-test-case-to-app-node` for the durable test-to-surface link.
- Test attachment: link test cases to the node they prove with
  `qa-link-test-case-to-app-node`.
- Risk: set `riskLevel` and `coverageStatus` from latest results, known
  defects, blockers, missing evidence, and untested critical paths.
- Discovery expectation: agents should populate these layers from multiple
  observations when available: browser walkthroughs, public routes, network
  calls, API docs/OpenAPI, saved QAA API targets, database/schema metadata,
  screenshots, prior runs, work items, and retest queue history. Do not only
  mirror the same app map in a different format.

### List Application Map

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-list-app-map" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID"
  }'
```

Requires `projects:read`. Returns `nodes`, `edges`, and `links`. The UI derives
heatmaps, Mermaid-style source, risk paths, and coverage-shape metrics from
these records.

### Upsert Application Map Node

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-upsert-app-map-node" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID",
    "layer": "application",
    "key": "login-page",
    "label": "Login page",
    "kind": "page",
    "route": "/login",
    "url": "https://example.com/login",
    "description": "Authentication entry point for returning users.",
    "riskLevel": "critical",
    "coverageStatus": "partial",
    "position": { "x": 120, "y": 80 },
    "screenshots": [
      {
        "url": "https://...public.blob.vercel-storage.com/qa/project/login.png",
        "label": "Login page",
        "targetUrl": "https://example.com/login",
        "kind": "page",
        "capturedAt": "2026-06-18T10:20:00.000Z",
        "viewport": { "width": 1440, "height": 900, "device": "desktop" }
      }
    ],
    "metadata": {
      "authRequired": false,
      "discoveredBy": "playwright"
    }
  }'
```

Requires `test_cases:write`. `key` must be stable within the project layer. If a
node with the same `projectId`, `layer`, and `key` exists, it is updated.
Supported `layer` values: `application`, `api`, `data`, and `coverage`.
Supported `kind` values: `page`, `component`, `flow`, `service`, `external`,
`state`, `api_endpoint`, `api_operation`, `data_entity`, `data_store`,
`database_table`, `external_service`, `test_case`, and `coverage_area`.
Supported `riskLevel` values: `low`, `medium`, `high`, and `critical`.
Supported `coverageStatus` values: `unknown`, `missing`, `partial`, `covered`,
and `failing`.

The endpoint also accepts `"nodes": [...]` for bulk upsert. To attach visual
context, first upload a screenshot with `/api/qa-screenshots`, then copy the
returned screenshot object into `screenshots`.

### Upsert Application Map Edge

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-upsert-app-map-edge" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID",
    "layer": "application",
    "sourceKey": "login-page",
    "targetKey": "dashboard",
    "kind": "navigation",
    "label": "Successful login",
    "metadata": {
      "trigger": "submit valid credentials"
    }
  }'
```

Requires `test_cases:write`. Use `sourceKey`/`targetKey` for stable agent
writes, or `sourceNodeId`/`targetNodeId` if ids are already known. Edges default
to resolving keys inside their own `layer`; use `sourceLayer` or `targetLayer`
when linking across layers. Supported `kind` values: `navigation`, `redirect`,
`dependency`, `api`, `modal`, `contains`, `related`, `calls`, `reads`, `writes`,
`owns`, `covers`, `validates`, and `blocks`.

Use edge kinds consistently:

- `navigation`: user can move from one surface to another.
- `redirect`: the system sends the user to another surface.
- `modal`: one surface opens or controls a transient state.
- `contains`: parent flow/page owns a component or state.
- `api`: frontend or service calls another service or endpoint.
- `calls`: one API operation calls another API/service operation.
- `reads`: a UI/API/service reads a data entity, store, or table.
- `writes`: a UI/API/service mutates a data entity, store, or table.
- `owns`: a service or domain owns a data entity or table.
- `covers`: a test or coverage area exercises a surface.
- `validates`: a test validates a surface, contract, or invariant.
- `blocks`: a failure, defect, or dependency blocks confidence in a surface.
- `dependency`: repo/service/component depends on another unit.
- `related`: weak relationship when context exists but a hard flow is unknown.

### Link Test Case To Application Map Node

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-link-test-case-to-app-node" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID",
    "testCaseId": "POOLIO-AUTH-002",
    "nodeKey": "login-page",
    "relationship": "covers"
  }'
```

Requires `test_cases:write`. Supported relationships: `covers`, `validates`,
`blocks`, and `mentions`. Prefer `covers` for normal regression coverage.

### Generate Application Map From Test Cases

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-generate-app-map-from-test-cases" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID"
  }'
```

Requires `test_cases:write`. This creates or updates one starter map node per
test-case area, links existing cases to those nodes, and infers coverage/risk
from current test statuses and priorities. Use this only as a bootstrap step;
after exploration, replace area-level nodes with concrete pages, flows,
components, services, and states.

## Test Suites

Test suites are reusable, named collections of durable test cases. A test case
may belong to several suites. Suites are not execution history: a run plan
resolves the selected suites to a deduplicated test-case snapshot, and the
later run/result records remain unchanged if suite membership changes.

List the active suites for a project:

```bash
curl "$SUPABASE_URL/functions/v1/qa-test-suites?projectId=PROJECT_UUID" \
  -H "Authorization: Bearer qa_agent_..."
```

Create a suite:

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-test-suites" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID",
    "name": "Authentication smoke",
    "description": "Fast release-critical checks for sign-in and sign-out.",
    "purpose": "smoke",
    "tags": ["authentication", "release-critical"],
    "testCaseIds": ["AUTH-001", "AUTH-002", "AUTH-005"]
  }'
```

Update the suite or replace its ordered membership:

```bash
curl -X PATCH "$SUPABASE_URL/functions/v1/qa-test-suites" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID",
    "id": "SUITE_UUID",
    "name": "Authentication smoke",
    "purpose": "smoke",
    "testCaseIds": ["AUTH-001", "AUTH-002", "AUTH-005", "AUTH-008"]
  }'
```

Archive a suite without changing historic runs:

```bash
curl -X DELETE "$SUPABASE_URL/functions/v1/qa-test-suites?projectId=PROJECT_UUID&id=SUITE_UUID" \
  -H "Authorization: Bearer qa_agent_..."
```

Reading requires `projects:read`; creating, updating, and archiving requires
`test_cases:write`. Supported purposes are `smoke`, `regression`, `feature`,
`release`, and `custom`.

Agent suite rules:

- Reuse an existing relevant suite instead of creating one for every run.
- Create a suite only when the collection is expected to be reused.
- A case may belong to several suites; deduplicate cases before execution.
- Leave a case outside suites when membership is unclear instead of guessing.
- Never treat a suite as a result or a run. Suite membership is organization;
  results belong to a specific run.
- Never rewrite historic runs after a suite changes.

## List Test Cases

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-list-test-cases" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID",
    "status": "Failed",
    "priority": "Critical",
    "includeSteps": true,
    "includeResults": true,
    "limit": 50
  }'
```

Requires `projects:read`. If the token is scoped to one project, `projectId` is
optional and any other project id is rejected. The endpoint also accepts `GET`
query parameters, for example:

```bash
curl "$SUPABASE_URL/functions/v1/qa-list-test-cases?projectId=PROJECT_UUID&area=Checkout&includeResults=true" \
  -H "Authorization: Bearer qa_agent_..."
```

Supported filters: `projectId`, `id`, `status`, `priority`, `area`, `release`,
`search`, and `limit`. `includeSteps` defaults to `true`; `includeResults`
defaults to `false`. Test cases with `project_id = null` are treated as legacy
global/demo cases and are hidden by default; pass `includeGlobal: true` or
`includeGlobal=true` to include them.

Response shape:

```json
{
  "testCases": [
    {
      "id": "LYS-CONTACT-006",
      "project_id": "PROJECT_UUID",
      "title": "Contact form accepts a qualified lead",
      "description": "Validates lead capture submission.",
      "area": "Lead capture",
      "status": "Failed",
      "priority": "Critical",
      "preconditions": ["Preview deployment is available"],
      "expected_result": "The lead is submitted and confirmed.",
      "steps": [
        {
          "position": 1,
          "action": "Open the contact page.",
          "expected": "The form is visible."
        }
      ],
      "results": [
        {
          "status": "Failed",
          "environment": "Production",
          "notes": "Submission returned validation error."
        }
      ]
    }
  ]
}
```

Use this endpoint when the agent needs to answer questions such as:

- "Which critical test cases are currently failing?"
- "Summarize the checkout test coverage and preconditions."
- "What steps should be executed for this test case?"
- "Which tests are blocked for the current release?"

## Create Test Case

If the agent has captured a page or component image, upload it first with
`/api/qa-screenshots` and pass the returned object in `screenshots`. Screenshots
belong on test cases when they describe the intended page/component/state that
the case covers. Save execution failure evidence on run results later.

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-create-test-case" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID",
    "id": "LYS-CHECKOUT-009",
    "title": "Checkout confirms successful card payment",
    "description": "Verifies that a shopper can complete payment and reach the receipt page.",
    "area": "Checkout",
    "priority": "Critical",
    "browser": "Chrome",
    "device": "Desktop",
    "status": "Not run",
    "suiteType": "regression",
    "qualityDimensions": ["functional"],
    "agentGenerated": true,
    "strategyNotes": "Durable regression coverage created from checkout release-risk analysis.",
    "tags": ["checkout", "payment", "release-critical"],
    "screenshots": [
      {
        "url": "https://...public.blob.vercel-storage.com/qa/project/case/checkout.png",
        "label": "Checkout payment form",
        "targetUrl": "https://example.com/checkout",
        "kind": "page",
        "capturedAt": "2026-06-18T10:20:00.000Z",
        "viewport": {
          "width": 1440,
          "height": 900,
          "device": "desktop"
        }
      }
    ],
    "owner": "QA Agent",
    "release": "RC-2026.06",
    "preconditions": ["Cart contains one product", "Test payment gateway is available"],
    "expectedResult": "The order is confirmed and the receipt is visible.",
    "testUrl": "https://example.com/checkout",
    "steps": [
      {
        "action": "Open checkout with a product in cart.",
        "expected": "Checkout form is visible."
      },
      {
        "action": "Submit valid payment details.",
        "expected": "Payment succeeds and receipt page opens."
      }
    ]
  }'
```

Requires `test_cases:write`. If the token is scoped to one project, `projectId`
is optional and any other project id is rejected. If `id` is omitted, the
endpoint generates a `TC-...` id. Prefer stable human-readable ids when the
agent is creating durable catalog tests.

The endpoint can also create several cases at once:

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-create-test-case" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID",
    "testCases": [
      {
        "id": "LYS-A11Y-010",
        "title": "Header navigation exposes visible focus states",
        "area": "Accessibility",
        "priority": "High",
        "expectedResult": "Keyboard focus is visible on every header control.",
        "steps": [
          {
            "action": "Tab through the header navigation.",
            "expected": "Focus follows visual order and remains visible."
          }
        ]
      },
      {
        "id": "LYS-SEO-011",
        "title": "Product pages expose canonical metadata",
        "area": "SEO",
        "priority": "Medium",
        "expectedResult": "Each product page has one canonical URL."
      }
    ]
  }'
```

Supported fields: `projectId`, `id`, `title`, `description`, `area`, `browser`,
`device`, `status`, `priority`, `suiteType` (legacy classification), `agentGenerated`,
`strategyNotes`, `qualityDimensions`, `tags`, `screenshots`, `owner`, `durationSeconds`,
`lastRunLabel`, `release`, `preconditions`, `expectedResult`, `testUrl`,
`sortOrder`, and `steps`. Each step requires `action` and `expected`.
When `steps` is sent to `qa-update-test-case`, it replaces the full stored
step list for that test case.

Use `status: "Not run"` for newly planned or enriched test cases. Do not use
`Running` when only creating or updating coverage metadata; `Running` should be
reserved for actual execution state written from a run/result workflow.

## Update Test Case

Use this endpoint when the agent needs to attach screenshots or update metadata
on an existing durable test case. This is the correct follow-up after
`/api/qa-screenshots`; uploading a screenshot stores the file in Blob, while
this endpoint writes the returned metadata onto the test case.

```bash
curl -X PATCH "$SUPABASE_URL/functions/v1/qa-update-test-case" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID",
    "testCaseId": "POOLIO-AUTH-002",
    "screenshots": [
      {
        "url": "https://...public.blob.vercel-storage.com/qa/project/case/login.png",
        "label": "Poolio login page",
        "targetUrl": "https://poolio.example.com/login",
        "kind": "page",
        "capturedAt": "2026-06-18T10:20:00.000Z",
        "viewport": {
          "width": 1440,
          "height": 900,
          "device": "desktop"
        }
      }
    ]
  }'
```

Requires `test_cases:write`. By default, incoming screenshots are appended to
the current `screenshots` array and deduplicated by `url`. Send
`"replaceScreenshots": true` to replace the screenshot list instead.

Supported update fields: `projectId`, `id`, `testCaseId`, `title`,
`description`, `area`, `browser`, `device`, `status`, `priority`, `suiteType` (legacy classification),
`agentGenerated`, `strategyNotes`, `qualityDimensions`, `tags`, `screenshots`,
`replaceScreenshots`, `owner`, `durationSeconds`, `lastRunLabel`, `release`,
`preconditions`, `expectedResult`, `testUrl`, and `sortOrder`.

## Upload QA Screenshot

Use this endpoint when the agent captures a screenshot from any supported test
surface and needs to attach it to QAA. The screenshot surface can be web,
mobile, API-adjacent visual evidence, or a future device/runtime technique. The
endpoint stores the image in Vercel Blob and returns metadata that can be copied
into `qa-create-test-case.screenshots`, `qa-update-test-case.screenshots`, or
Application Map node screenshots. The same endpoint also returns an `evidence`
object. Use that object when a screenshot is evidence for a specific run result
or failed step.

Screenshots are a shared QAA primitive:

- Application Map screenshots describe stable product surfaces.
- Test case screenshots describe expected reference states.
- Run evidence screenshots describe what happened in one execution.
- Mobile screenshots should use the same endpoint when the harness has a safe
  image file to upload. Set metadata such as
  `{"source":"mobile","platform":"android","deviceSerial":"..."}`.

```bash
curl -X POST "$APP_URL/api/qa-screenshots" \
  -H "Authorization: Bearer qa_agent_..." \
  -F "projectId=PROJECT_UUID" \
  -F "testCaseId=LYS-CHECKOUT-009" \
  -F "runId=run-checkout-regression" \
  -F "stepPosition=2" \
  -F "label=Checkout payment form" \
  -F "targetUrl=https://example.com/checkout" \
  -F "kind=failure" \
  -F "notes=Payment form shows validation error after submit." \
  -F 'viewport={"width":1440,"height":900,"device":"desktop"}' \
  -F 'metadata={"source":"web","technique":"playwright"}' \
  -F "file=@checkout-payment-form.png;type=image/png"
```

Mobile example:

```bash
curl -X POST "$APP_URL/api/qa-screenshots" \
  -H "Authorization: Bearer qa_agent_..." \
  -F "projectId=PROJECT_UUID" \
  -F "testCaseId=MOB-LOGIN-001" \
  -F "runId=run-mobile-login" \
  -F "label=Android login failure" \
  -F "targetUrl=com.example.app" \
  -F "kind=failure" \
  -F "notes=Welcome text did not appear after login." \
  -F 'viewport={"width":1080,"height":2400,"device":"Pixel 8","platform":"android"}' \
  -F 'metadata={"source":"mobile","platform":"android","technique":"adb-screencap"}' \
  -F "file=@android-login-failure.png;type=image/png"
```

Requires an agent token with `test_cases:write`. If the token is scoped to one
project, any different `projectId` is rejected. Maximum file size is 8 MB.

Response shape:

```json
{
  "screenshot": {
    "url": "https://...public.blob.vercel-storage.com/qa/project/case/file.png",
    "label": "Checkout payment form",
    "targetUrl": "https://example.com/checkout",
    "kind": "failure",
    "capturedAt": "2026-06-18T10:20:00.000Z",
    "viewport": {
      "width": 1440,
      "height": 900,
      "device": "desktop"
    },
    "metadata": {
      "source": "web",
      "technique": "playwright"
    }
  },
  "evidence": {
    "url": "https://...public.blob.vercel-storage.com/qa/project/case/file.png",
    "label": "Checkout payment form",
    "targetUrl": "https://example.com/checkout",
    "kind": "failure",
    "capturedAt": "2026-06-18T10:20:00.000Z",
    "runId": "run-checkout-regression",
    "resultId": null,
    "testCaseId": "LYS-CHECKOUT-009",
    "stepPosition": 2,
    "notes": "Payment form shows validation error after submit.",
    "viewport": {
      "width": 1440,
      "height": 900,
      "device": "desktop"
    },
    "metadata": {
      "source": "web",
      "technique": "playwright"
    }
  }
}
```

Recommended screenshot types:

- For durable test-case screenshots: `page`, `component`, or `state`.
- For run result evidence: `before`, `after`, `failure`, or `reference`.

When a step fails, prefer uploading a screenshot immediately after the failed
assertion and passing the returned `evidence` object to `qa-add-result`. Always
include `stepPosition` for step-level evidence when a specific step failed.
Public reports render non-sensitive evidence next to the failed step and in the
recent result/run result rows. Mobile evidence is treated as sensitive by
default and should only be shown publicly when a project/report policy explicitly
allows it.

Response shape:

```json
{
  "testCases": [
    {
      "id": "LYS-CHECKOUT-009",
      "project_id": "PROJECT_UUID",
      "title": "Checkout confirms successful card payment",
      "status": "Running",
      "priority": "Critical"
    }
  ],
  "steps": [
    {
      "test_case_id": "LYS-CHECKOUT-009",
      "position": 1,
      "action": "Open checkout with a product in cart.",
      "expected": "Checkout form is visible."
    }
  ]
}
```

Use this endpoint when the agent discovers missing test coverage, converts a
bug report into a repeatable regression test, or creates planned cases before
running `qa-create-run` and `qa-add-result`.

## List Test Runs

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-list-test-runs" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID",
    "status": "Failed",
    "includeResults": true,
    "limit": 20
  }'
```

Requires `projects:read`. If the token is scoped to one project, `projectId` is
optional and any other project id is rejected. The endpoint also accepts `GET`
query parameters, for example:

```bash
curl "$SUPABASE_URL/functions/v1/qa-list-test-runs?projectId=PROJECT_UUID&triggerType=AI%20Agent&includeResults=true" \
  -H "Authorization: Bearer qa_agent_..."
```

Supported filters: `projectId`, `runId`, `id`, `status`, `environment`,
`triggerType`, `search`, and `limit`. `includeResults` defaults to `true`.

Response shape:

```json
{
  "runs": [
    {
      "id": "run-lysande-public-web",
      "project_id": "PROJECT_UUID",
      "name": "Lysande public web regression",
      "environment": "Production",
      "status": "Failed",
      "trigger_type": "AI Agent",
      "passed_count": 2,
      "failed_count": 1,
      "blocked_count": 1,
      "total_count": 5,
      "scope": ["Homepage", "Lead capture", "SEO"],
      "notes": "Contact form validation failed.",
      "results": [
        {
          "test_case_id": "LYS-CONTACT-006",
          "test_case_title": "Contact form accepts a qualified lead",
          "status": "Failed",
          "notes": "Submission returned validation error."
        }
      ]
    }
  ]
}
```

Use this endpoint when the agent needs to answer questions such as:

- "Summarize the latest failed run."
- "What failed, passed, or was blocked in the last production run?"
- "Compare recent AI-agent runs for this project."
- "Which test cases repeatedly fail in recent runs?"

## Run Plan

Create a run plan before execution. The plan is Sladdis' saved intent: scope,
selected suites/tests, risks, reason, and expected evidence. The endpoint
resolves selected suites and individually selected cases into a deduplicated
`selected_test_case_ids` snapshot. The later test run links back to this plan
so QAA can compare plan vs actual even when suite membership changes later.

When Sladdis delegates to subagents, include `tracks`. Tracks are parallel
worker scopes inside the same run plan. Sladdis remains the owner of the final
run, result normalization, dedupe, and QAA persistence.

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-run-plan" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID",
    "title": "Checkout release readiness",
    "runType": "regression",
    "environment": "Preview",
    "scope": ["Checkout", "Authentication", "Payment confirmation"],
    "notInScope": ["Refund admin flows"],
    "riskAreas": ["Payment validation", "Session expiry"],
    "selectedTestSuiteIds": ["CHECKOUT_REGRESSION_SUITE_UUID"],
    "selectedTestCaseIds": ["LYS-CHECKOUT-001", "LYS-AUTH-003"],
    "reason": "Recent checkout changes touched payment and auth handoff.",
    "expectedEvidence": ["Screenshots for failed steps", "Console/network errors when present"],
    "sourcePrompt": "Validate checkout release readiness before deploy.",
    "confidenceBefore": 62,
    "tracks": [
      {
        "name": "UX/UI sweep",
        "agentName": "Sladdis UX Subagent",
        "focusArea": "Navigation, responsive layout, copy, friction",
        "status": "planned",
        "scope": ["Desktop first-pass", "Mobile viewport smoke"],
        "riskAreas": ["Responsive layout", "Primary CTA clarity"],
        "expectedEvidence": ["Screenshots for layout defects"]
      },
      {
        "name": "Accessibility smoke",
        "agentName": "Sladdis A11y Subagent",
        "focusArea": "Keyboard, labels, focus, landmark smoke",
        "status": "planned",
        "scope": ["Keyboard tab order", "Form labels", "Accessible names"],
        "riskAreas": ["Keyboard traps", "Missing labels"],
        "expectedEvidence": ["Failed selector or screenshot evidence"]
      }
    ]
  }'
```

Requires `test_runs:write`. The response contains `runPlan.id` and
`runPlan.tracks[]` when tracks were saved. Pass the plan id to `qa-create-run`
as `runPlanId`.

List recent plans:

```bash
curl "$SUPABASE_URL/functions/v1/qa-run-plan?projectId=PROJECT_UUID&status=planned" \
  -H "Authorization: Bearer qa_agent_..."
```

Update a plan manually when needed:

```bash
curl -X PATCH "$SUPABASE_URL/functions/v1/qa-run-plan" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID",
    "id": "RUN_PLAN_UUID",
    "status": "completed",
    "actualRunId": "run-...",
    "actualSummary": {
      "status": "Failed",
      "total": 5,
      "passed": 2,
      "failed": 2,
      "blocked": 1,
      "durationSeconds": 420
    }
  }'
```

Delete an incorrect or obsolete plan draft when it should no longer appear in
Overview. This removes the run plan and its tracks. Existing test runs and test
cases are not deleted; linked runs lose the plan reference.

```bash
curl -X DELETE "$SUPABASE_URL/functions/v1/qa-run-plan?projectId=PROJECT_UUID&id=RUN_PLAN_UUID" \
  -H "Authorization: Bearer qa_agent_..."
```

Update one or more subagent tracks as workers finish:

```bash
curl -X PATCH "$SUPABASE_URL/functions/v1/qa-run-plan" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID",
    "id": "RUN_PLAN_UUID",
    "tracks": [
      {
        "id": "TRACK_UUID",
        "status": "completed",
        "actualRunId": "run-...",
        "summary": "Desktop navigation passed; mobile CTA wraps awkwardly on 390px.",
        "findings": [
          {
            "severity": "Medium",
            "title": "Mobile CTA wraps into two uneven lines",
            "recommendation": "Shorten the label or widen the button container."
          }
        ],
        "coverage": {
          "viewports": ["1440x900", "390x844"],
          "areas": ["home", "pricing"]
        },
        "confidenceAfter": 76
      }
    ]
  }'
```

Normally `qa-create-run` marks the linked plan `in_progress`, and
`qa-update-run` marks it `completed` with an `actualSummary` when the run
finishes as `Passed`, `Failed`, or `Blocked`.

## Create Run

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-create-run" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID",
    "name": "Checkout regression",
    "environment": "Preview",
    "status": "Running",
    "runPlanId": "RUN_PLAN_UUID",
    "runType": "regression",
    "strategy": "Focus on release-critical checkout and authentication paths first.",
    "requestedBy": "agent",
    "sourcePrompt": "Validate checkout release readiness.",
    "startedAt": "2026-06-22T09:16:49.195Z",
    "scope": ["Checkout", "Authentication"]
  }'
```

Set `startedAt` to the moment the execution actually begins. The app uses the
run `started_at` timestamp as the displayed "last run" time for each test case
in that run. If omitted, the API uses the server receive time.

## Update Run

```bash
curl -X PATCH "$SUPABASE_URL/functions/v1/qa-update-run" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "runId": "run-...",
    "status": "Failed",
    "finishedAt": "2026-06-16T14:00:00.000Z",
    "durationSeconds": 420,
    "notes": "Checkout failed on payment validation."
  }'
```

## Add Result

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-add-result" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "runId": "run-...",
    "testCaseId": "LYS-CHECKOUT-001",
    "testCaseTitle": "Checkout accepts valid card details",
    "status": "Failed",
    "durationSeconds": 84,
    "failedStepPosition": 2,
    "failureMessage": "Payment form returned a validation error.",
    "notes": "Payment submit returned validation error.",
    "evidence": [
      {
        "url": "https://...public.blob.vercel-storage.com/qa/project/case/file.png",
        "label": "Payment validation failure",
        "targetUrl": "https://example.com/checkout",
        "kind": "failure",
        "capturedAt": "2026-06-18T10:20:00.000Z",
        "stepPosition": 2,
        "notes": "Validation banner remained visible after submitting valid card details.",
        "viewport": {
          "width": 1440,
          "height": 900,
          "device": "desktop"
        }
      }
    ]
  }'
```

Requires `test_results:write`. The endpoint writes to the run result log and
also mirrors the result into the test case history used by `qa-list-test-cases`
and the app's "Recent results" section. If `browser` or `device` is omitted,
the endpoint copies those values from the test case. If `environment` is
omitted, it uses the run environment.

When `status` is `Failed`, QAA automatically creates a linked QA Work Board bug
ticket unless one already exists for the same run and test case. The ticket is
created in `backlog`, has `item_type: "bug"`, `qa_status: "failed"`, links the
test case in `linked_test_case_ids`, stores the run in `qa_run_id`, and includes
an `agent_prompt` repair prompt. The prompt is meant to be copied into a coding
agent: it summarizes the failing test, environment, target, failed step,
evidence labels, reproduction task, and definition of done. Do not create a
second work item for the same failed result unless the first ticket was closed
and the failure is genuinely new.

When a result fails on a specific test step, send `failedStepPosition` using
the 1-based step number from the test case. Send `failureMessage` when there is
a concise step-level failure reason. The app uses these fields to highlight the
failed step in the test case detail view and to show the failed step in Recent
results. For credible reports, include `evidence` screenshots for failed or
blocked results whenever possible. Evidence rows are stored separately from the
durable test case screenshots, so each run can show what happened during that
specific execution.

Response shape:

```json
{
  "result": {
    "id": "RUN_RESULT_UUID",
    "status": "Failed"
  },
  "workItem": {
    "id": "WORK_ITEM_UUID"
  },
  "workItemError": null,
  "autoTicket": {
    "functionVersion": "qa-add-result:auto-ticket-v2",
    "requestedStatus": "Failed",
    "normalizedStatus": "Failed",
    "attempted": true,
    "createdOrReused": true,
    "error": null
  }
}
```

`workItem` is `null` for passed results. If a matching failure ticket already
exists for the same run and test case, or an open bug already exists for the
same test case, `workItem.id` is the existing ticket id.

If the result is saved but QAA cannot create the Work Board ticket, the endpoint
still returns `201` for the saved result with `workItem: null` and a non-empty
`workItemError`. Treat that as an infrastructure issue: record the error in the
run notes or a triage action, then report it to the QAA owner. A common rollout
case is a missing `agent_prompt` column; QAA will try a fallback ticket without
that column and return a `workItemError` telling the operator to apply
`202606260001_add_work_item_agent_prompt.sql` and reload the PostgREST schema.

Use `autoTicket.functionVersion` to confirm the deployed Edge Function version.
If the field is missing, the endpoint is running an older deploy. If
`attempted` is `false`, check the submitted status and `normalizedStatus`.

When `status` is `Failed` or `Blocked`, QAA also creates or reuses an open
Retest Queue V2 item. The retest is linked to the `runId`, saved result id,
`testCaseId`, and the auto-created/reused work item when available. The
response includes `retestItem` and `retestError`. Treat this automatic path as
the default. Create a retest item manually only when `retestItem` is `null`,
`retestError` is non-empty, or the unresolved risk did not originate from a
saved Failed or Blocked result.

## Create Triage Action

```bash
curl -X POST "$SUPABASE_URL/functions/v1/qa-create-triage-action" \
  -H "Authorization: Bearer qa_agent_..." \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID",
    "runId": "run-checkout-regression",
    "testCaseId": "LYS-CHECKOUT-001",
    "action": "create_defect",
    "defectTitle": "Checkout rejects valid card details",
    "defectSeverity": "P1",
    "note": "Agent reproduced failure on step 2 with payment validation error."
  }'
```

Requires `test_results:write`. Supported actions:

- `reviewed`
- `assign_owner`
- `mark_flaky`
- `request_rerun`
- `create_defect`

Use triage actions when the agent has enough evidence to recommend a human
workflow decision. The Work Board bug created or reused by `qa-add-result` is
the primary failure handoff. Reuse that work item for normal failed-result
follow-up. Use `create_defect` only when a separate formal defect record is
explicitly needed; it inserts a row in `qa_defects` and must not be used to
duplicate the existing Work Board bug.

## Operational Decision Contract

Use this contract before the detailed loop below:

1. Start from the project brief and choose one QA outcome: verify a handoff,
   investigate a failure, close a retest, measure coverage, or support a
   release decision. Do not collect unrelated checks into one run.
2. Reuse an existing test case, open defect, work item, and retest whenever they
   describe the same behavior. Update the durable record instead of creating a
   parallel version of the same problem.
3. A multi-case, coverage, regression, or release run requires a saved run
   plan. A single-case linked retest may run without a new plan. It still gets
   a new run and result, but keeps traceability to the original failure through
   the retest item's source `runId`, `resultId`, and `testCaseId`; omit
   `runPlanId` rather than creating an empty plan.
4. A failed or blocked result requires evidence or an explicit explanation of
   why evidence could not be captured. Never present an unsupported failure as
   release-blocking certainty.
5. Assess `releaseImpact` separately from severity. A severe bug can be outside
   the active release scope, and a visually small defect can block a critical
   flow.
6. Close loops. A passing retest must resolve its retest item and update the
   linked Work Board card. A repeated failure must update the existing records
   with new evidence and history.
7. Save `memory` notes only for durable facts. Execution narrative belongs on
   the run, actionable failures belong on work items, and future verification
   belongs in the retest queue or a `next_test` note.
8. Finish every run with its decision, unresolved risk, saved QAA links, and one
   recommended next QA action.

## Recommended Agent Loop

1. Establish the QAA project before doing any durable QA work. Call
   `qa-list-projects`, match the user's URL/request to an existing project,
   and reuse its `id`.
2. If no project matches and the token is workspace-wide, create the project
   with `qa-create-project` immediately. If the token cannot create projects,
   ask the user which existing project to use before saving test cases, runs,
   defects, workspace data, or agent field notes.
3. Treat the selected or created project `id` as the anchor for the rest of the
   run. Every workspace update, Application Map node, test case, run, result,
   triage action, and agent field note should reference this project.
4. Read the project brief with `qa-agent-project-brief`. Use it as the primary
   planning input for release health, coverage gaps, recent failures, active
   defects, retests, work items, memory notes, and next-test notes.
5. Read or update the QA workspace with `qa-workspace` when the brief shows
   missing context, missing safe test data, or retest items that need changes.
   Use `context`, `testData`,
   and `retestItems` as planning input before choosing what to run.
6. Read the QA Work Board with `qa-list-work-items`. If the user asks the
   agent to test queued product work, start from `ready_for_qa` cards and move
   the chosen card to `testing` with `qa-update-work-item`.
7. Interpret the user's chat intent and choose a strategy such as `smoke`,
   `regression`, `investigation`, `coverage`, or `rerun`.
8. Read current product structure with `qa-list-app-map` when the brief's map
   summary is not enough for the task.
9. Read current coverage with `qa-list-test-cases` and reusable collections
   with `qa-test-suites` when the brief's top tests and coverage gaps are not
   enough for the task.
10. If mobile testing is requested, configure safe Android devices and mobile
    cases in QAA first. Do not execute arbitrary ADB commands outside the mobile
    runner contract.
11. If this is a new or sparse project, bootstrap the project workspace:
    upsert project context from the walkthrough, create initial Application Map
    application/API/data/coverage layer nodes, create starter smoke test cases,
    and save any user-provided test data with the correct sensitivity
    classification.
12. During exploration, create or update Application Map nodes and edges with
    `qa-upsert-app-map-node` and `qa-upsert-app-map-edge`. Use concrete surfaces
    such as login page, dashboard, scan form, history table, settings modal,
    upload API, database table, customer entity, or payment provider rather than
    generic test-case categories.
13. When visual context helps human review, capture a browser or mobile
    harness screenshot and upload it with `/api/qa-screenshots`.
14. Attach relevant screenshots to Application Map nodes and/or durable test
    cases. Map screenshots explain what a surface is; test case screenshots
    explain the intended page/component/state for that check.
15. Create missing durable coverage with `qa-create-test-case`, including
    quality-oriented tags, `strategyNotes`, `agentGenerated`, and uploaded
    `screenshots`. Treat `suiteType` as legacy compatibility metadata.
16. Update existing durable coverage with `qa-update-test-case` when screenshots
    or metadata need to be attached after creation. Use `qa-test-suites` to add
    cases to reusable named suites; do not encode membership in `suiteType`.
17. Link each relevant test case to the Application Map with
    `qa-link-test-case-to-app-node`.
18. Before a multi-case, coverage, regression, or release execution, create a
    durable run plan with `qa-run-plan`. Save the selected scope, out-of-scope
    areas, selected suite ids and/or individual test case ids, risk areas,
    reason, expected evidence,
    confidence before the run, and `tracks` when subagents will execute parallel
    UX, accessibility, performance, functional, content, API, or mobile slices.
    A single-case linked retest may reuse its existing retest and source-run
    context without creating an otherwise empty plan.
19. Start execution with `qa-create-run`, including `runType`, `strategy`,
    `requestedBy`, and `sourcePrompt`. Include `runPlanId` when the execution
    uses a saved plan; omit it for the single-case linked-retest exception. A
    linked plan is marked `in_progress` automatically.
20. For failed or blocked web steps, capture a Playwright screenshot, upload it with
    `/api/qa-screenshots`, and keep the returned `evidence` object.
21. For mobile runs, use the mobile runner/harness contract and persist only
    redacted mobile evidence. Never paste raw logcat, UI dumps, secrets, or
    sensitive screenshots into reports.
22. Add each result with `qa-add-result`, including failed step metadata and
    any uploaded `evidence`. As subagents finish, patch the matching run-plan
    track with status, summary, findings, coverage, and confidence after.
23. Finish the run with `qa-update-run`. A linked plan is marked `completed`
    and receives an `actualSummary` when the run finishes as `Passed`, `Failed`,
    or `Blocked`.
24. Update the originating Work Board card with `qa-update-work-item`: use
    `tested`/`passed` when accepted, `in_progress`/`failed` when a bug is found,
    or `in_progress`/`blocked` with a clear blocked reason when the agent
    cannot proceed. `done` is reserved for accepted/completed work.
25. Create triage actions for failures that need review, defect creation, rerun,
    or flaky classification.
26. Persist workspace learnings after the run: update project context when the
    product understanding changed, save reusable test data, create retest items
    only for unresolved failures that `qa-add-result` did not already create or
    reuse, update resolved retest statuses, and use `qa-create-agent-note` for
    agent field notes that should appear in `/agent-notes`. Use field notes for
    test narrative, reasoning, and project knowledge that is not yet a test
    case or finding.
27. Reply in chat with the run summary, saved QAA links, unresolved risks, and
    the recommended next test. Do not stop at a chat-only report when QAA access
    and a matching project are available.

QAA is the platform where the agent can work with project context, approved
tools, durable QA state, evidence, and triage. Humans talk to the agent, then
watch realtime notifications and the activity timeline, inspect failed runs,
and triage the agent's findings in QAA.

## Create Agent Field Notes

Use `qa-create-agent-note` when Sladdis has useful test narrative that should
appear in `/agent-notes` and should not overwrite a ticket. Tickets are actions.
Agent field notes preserve observations, reasoning, recommendations, memory,
and next-test ideas. Notes can stand alone or link to a run and/or Work Board
ticket.

`qa-create-agent-note` creates Agent Field Notes shown in `/agent-notes`.

```bash
curl -X POST "$SUPABASE_FUNCTIONS_URL/qa-create-agent-note" \
  -H "Authorization: Bearer $QAA_AGENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID",
    "source": "telegram",
    "noteType": "recommendation",
    "title": "Pure Consulting smoke pass narrative",
    "body": "Summary: tested public website metadata and core navigation.\nObserved duplicate social metadata and broken touch icon.\nNext test: rerun metadata crawler after favicon fix.",
    "tags": ["metadata", "smoke", "public-web"],
    "relatedRunId": "run-...",
    "relatedTicketId": "WORK_ITEM_UUID"
  }'
```

`source` must be one of `telegram`, `testbench`, `qaa_run`, `manual`, or
`edge_function`. `noteType` must be one of `observation`, `reasoning`,
`recommendation`, `memory`, `next_test`, or `reply`.

Write notes for scanning and later retrieval:

- Use a short, outcome-oriented title and keep one idea per note.
- Use `observation` for evidence-backed findings, `reasoning` for a decision
  trace, `recommendation` for a proposed action, `next_test` for a concrete
  future check, and `memory` only for durable project facts.
- Link `relatedRunId` and/or `relatedTicketId` whenever the note comes from
  executable QA work. This keeps the narrative connected to its evidence.
- Prefer three to six stable tags over sentence-like or one-off tags.
- Create replies with `noteType: "reply"` and the root note's `parentNoteId` so
  follow-up context remains in the same thread.

## QA Manager artifacts (QM0)

Use `qa-manager-artifacts` for structured strategy, risk, coverage, readiness,
gate, metric, and sign-off proposals. This endpoint requires a project-scoped
token with `qa_manager:write`. Sladdis may write only `draft`, `needs_input`, or
`ready_for_review`; owner/admin approval remains in QAA.

```bash
curl -X POST "$SUPABASE_FUNCTIONS_URL/qa-manager-artifacts" \
  -H "Authorization: Bearer $QAA_AGENT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_UUID",
    "artifactType": "test_strategy",
    "state": "ready_for_review",
    "idempotencyKey": "strategy-build-abc123-v1",
    "content": {
      "objective": "Protect the release decision",
      "scope": {"in": ["application flow"], "out": []},
      "qualityGoals": ["No critical-path regression"],
      "exitCriteria": ["All release blockers resolved"]
    },
    "sources": [{"kind": "project_brief", "ref": "current"}],
    "evidenceRefs": ["run-abc123"]
  }'
```

`GET` accepts `projectId` and optional `artifactType`. `PATCH` requires `id`,
`projectId`, and `expectedRevision`. A stale update returns `409`; re-read and
reconcile it. Reusing an idempotency key with a different payload also returns
`409`. The full schema and responsibility boundary are documented in
`docs/qa-manager-contract.md`.

Screenshots uploaded through `/api/qa-screenshots` are signature-validated and
kept in private project-scoped storage. Persist the returned proxy URL and
metadata; never replace it with a raw storage URL.
