QA-Pilot for Claude Code

A verdict you can open, not a claim.

QA-Pilot runs feature tests against a deployed environment and publishes a verdict only after a script has checked its evidence. Every executed case leaves a Playwright trace and a deploy SHA read from the running environment, so QA reviews evidence instead of re-running the tests.

1. Add the marketplace

claude plugin marketplace add https://github.com/SandeepTakasi/qa-pilot

2. Install

claude plugin install qa-pilot

Read the docs How production runs stay safe

report.json (excerpt)
run_id: 20261001T092200Z-checkout-a1f9c3e
env_name: staging
commit_sha: a1f9c3e
sha_before: a1f9c3e
sha_after: a1f9c3e
mutation_policy: scoped-write
mutation_prefix: qa-e2e-
cases:
  - id: CHECKOUT-ORDER-001
    verdict: pass
    trace: test-results/checkout-order-001/trace.zip
  - id: CHECKOUT-ORDER-002
    verdict: flaky
    retries: 1
    trace: test-results/checkout-order-002/trace.zip
  - id: CHECKOUT-ORDER-007
    verdict: blocked
    trace: null
summary: { pass: 21, fail: 0, flaky: 1, blocked: 2 }
writes.json (CHECKOUT-ORDER-001, one attempt, excerpt)
{
  "policy": "scoped-write",
  "prefix": "qa-e2e-",
  "write_signatures": 2,
  "routed_requests": 41,
  "blocked": 0,
  "observed": 1,
  "events": [
    {
      "action": "observe",
      "reason": "POST matches write_signatures[1]; ...",
      "url": "https://staging.example.com/api/orders"
    }
  ]
}

The problem

Looked fine is not evidence

An agent can click through an app and report that it looked fine. That report is a sentence. It names no build, holds no recording, and gives a second person nothing to open or to run again next week.

“Claude clicked through it and it looked fine”

QA-Pilot treats that sentence as the failure it is built to remove. An agentic browser session can help write a test, but it produces no trace and no machine-readable result, so it has nothing to publish with. What QA-Pilot leaves behind instead:

The pipeline

Six commands, with QA between design and execution

Each command is a Claude Code skill. The deterministic work, such as parsing the report and validating it, sits in zero-dependency Node scripts that a model does not get to reinterpret.

  1. /qa-pilot:qa-init

    Describe the project

    Scans the repo, asks for what it cannot discover (environment URLs, where the deploy SHA is served, the tracker), and writes a schema-valid qa-pilot.config.yaml. Once per project.

  2. /qa-pilot:setup-profiles

    Save a login once

    Gives you one command that opens a real browser for a single sign-in per account, then saves cookies, localStorage and IndexedDB so specs reuse the session. Once per developer.

  3. /qa-pilot:generate-tests

    Author the cases

    Reads the tracker task and the code, then writes cases.yaml: at most 25 cases, a required scenario mix, and an assertion that could become a Playwright check in every case. Per feature.

  4. QA approves the scenario matrix, the write policy and the cases
  5. /qa-pilot:run-tests

    Run against a deployed build

    Turns approved cases into committed Playwright specs and runs them with a trace per case, reading the deploy SHA before and after. A feature that may not write freely runs under the write guard.

  6. /qa-pilot:publish-results

    Validate, then publish

    Builds report.json from the Playwright report and runs the gate. Only a report that passes reaches the tracker.

  7. /qa-pilot:qa-review

    Review as QA

    Sorts the queue with failures first, tracks the sampling quotas, and records approve, reject with a reason, or retest. Every confirmed defect becomes a linked bug.

What it refuses to publish

Six things it will not publish

The publish step is a script, not a judgement call. It rejects each of these outright. The lines below are the gate’s own output, as printed under REFUSED. This report cannot be published.

  1. A verdict without evidence

    Every executed case needs a Playwright trace and a deploy SHA read from the running environment. A missing, empty or unreadable artifact fails the report.

    cases[CHECKOUT-ORDER-001].trace: required for a pass verdict. An unevidenced verdict is exactly the unverifiable claim this pipeline exists to prevent.
  2. A verdict from a local or sandbox environment

    Mock backends are seeded and always succeed. Runs against them are useful for stabilising specs and are stamped as such, but they cannot carry a verdict.

    api_mode: "mocks" is this host's sandbox mode. Sandbox backends are seeded and always succeed, so their results are structurally false passes and cannot be published.
  3. A pass that was really a retry

    Pass-on-retry is recorded flaky, always, derived from the attempt results and not from Playwright’s own status field, so no config change can turn it green. A failure in any browser is a failure, never a flake. The gate recomputes every verdict from results.json.

    rule 1b: cases[CHECKOUT-ORDER-002].verdict says pass but results.json gives flaky
  4. A verdict for a case QA never approved

    Checked against the case’s recorded status at the time of the run, not against the case map. Every generated case is in the map from the moment it is created, so membership proves nothing about approval.

    cases[CHECKOUT-ORDER-004]: was "Case Review" (case_review) when the run happened, which is not an approved state. Only cases QA approved may be published.
  5. A run that lost its ground truth

    If the deployed build changes mid-run, every case is blocked, because half tested one build and half another. Past 10% blocked the run halts: the environment failed, not the feature.

    sha_mismatch: the deployed build changed mid-run (a1f9c3e -> 7d02be4) but 12 case(s) still carry a verdict; every case must be blocked
  6. A run that broke its write policy

    Each feature declares whether its specs may write: read-only, scoped-write with a name prefix, or unrestricted. The write guard records every write control and write request it saw. A read-only run that recorded one, or a run whose guard never reported in, does not publish.

    rule 3: read-only run recorded 2 write(s) in cases[CHECKOUT-ORDER-003]

Evidence

The evidence is one file

One artifact per case per run: trace.zip. It carries the video byte-for-byte, the console output, the screenshot film-strip, DOM snapshots and the network log, so there is no separate video and no separate console log to keep in step.

Open it locally with npx playwright show-trace, which is the safe way for a trace that carries a credential; trace.playwright.dev also opens one in the browser, entirely client-side. No second storage system, and no extra credentials.

With ClickUp, the case’s task gets the verdict fields, the failure text, and the trace’s path and sha256, and the trace stays on the machine that ran it by default; attaching it is an opt-in per environment. Without ClickUp, set tracker: none and everything stays in local files.

One run directory
testing/checkout/runs/20261001T092200Z-checkout-a1f9c3e/
  meta.json
  spec-shas.json
  results.json
  statuses.json
  report.json
  fixtures/
  test-results/
    checkout-order-001/
      trace.zip
      writes.json
    checkout-order-002/
      trace.zip
      writes.json

Production

Production, without the damage

An environment declares what it is: kind: qa | staging | production, never guessed from the hostname. Production is allowed, under rules that are refusals, not advice. The first is a restricted account: a production environment must name its test_account, the account the runs sign in as and what restricts it (its own tenant, no admin rights, no billing). It is an attestation nothing verifies, so QA reviews it.

  • Evidence stays local. A trace carries the session, which can include a long-lived refresh token, and every request body it touched, so a production trace is never attached to the tracker, never uploaded by CI, and never committed. /qa-pilot:run-tests refuses unless the run directory is gitignored.
  • Full capture. Every case keeps a trace. On production a pass is the claim most worth checking, so evidence.capture must be always.
  • A declared write policy and a live guard. A production feature must be read-only or scoped-write, and the host profile says what a write looks like on that host. The guard judges clicks in the page before the app sees them, and requests at the network layer. It is the second layer under the restricted account, never the boundary.

Production, in full

What the tracker receives from a production run (excerpt)
{
  "mode": "local",
  "cases": [
    {
      "case_id": "CHECKOUT-ORDER-001",
      "verdict": "pass",
      "target_status": "Under Review",
      "env_name": "production",
      "build_id": "a1f9c3e",
      "trace_path": "test-results/checkout-order-001/trace.zip",
      "trace_sha256": "26f9dc66c7273...a1b2"
    }
  ],
  "summary": {
    "counts": { "pass": 1, "fail": 0, "flaky": 0, "blocked": 0 },
    "blocked_pct": 0,
    "confidence": { "score": 0, "label": "Not Ready" },
    "ready": false
  }
}

Portability

One profile per project, owned by QA

The plugin ships generic. Environment URLs, auth mechanics, selector policy, assertion constraints and evidence requirements live in qa-pilot.config.yaml, committed in the consuming repo and reviewed by QA.

A second project needs no plugin-code changes. Its profile, including the write signatures that say what counts as a write on that host, is the work, and /qa-pilot:qa-init starts it. Every other skill refuses to run without a schema-valid profile.

The host profile reference

qa-pilot.config.yaml (excerpt, mutation omitted)
project: shop
tracker: clickup

environments:
  staging:
    kind: staging
    apps:
      storefront: https://staging.example.com
    sha_source:
      url: https://staging.example.com/version.json
      json_path: commit
  production:
    kind: production
    test_account: qa-runner@example.com, its own tenant, no admin rights, no billing
    apps:
      storefront: https://app.example.com
    sha_source:
      url: https://app.example.com/version.json
      json_path: commit

evidence:
  capture: always

selectors:
  testid_attribute: data-testid

Honest limits

How strong these gates actually are

The checks are deterministic scripts, so a rushed developer cannot skip them by accident, and that is the failure this system is built for. What they verify is that a report is internally consistent and that its evidence exists on disk.

They do not cryptographically bind a report to a Playwright run that happened. Someone determined to fabricate a green result could hand-write the inputs, and nothing here stops that. QA-Pilot makes the evidenced path the path of least resistance, not the only conceivable one. The environment name in a report is taken on trust, like the deploy SHA, and the write guard’s ceilings are listed in its source.

Sampling does not close that gap. QA opens a share of passing evidence (all of P0, at least 30% of P1 and 10% of P2) and every failure, which checks that the evidence shows what the case claims. A pass that rests on a weak assertion looks green in its trace. The spec lint catches a missing assertion, a fixed sleep or a skipped test, and the case lint rejects vague expected outcomes; how strong an assertion is stays with QA’s design review, not sampling.

Install

Two commands

claude plugin marketplace add https://github.com/SandeepTakasi/qa-pilot
claude plugin install qa-pilot

Requires Node 20 or newer (the scripts are zero-dependency ESM) and, in the host repo, Playwright 1.51 or newer. That is the release where storageState({ indexedDB: true }) landed, and below it IndexedDB-persisted auth such as Firebase silently fails to restore.

Setting up a project QA-Pilot on GitHub