Get started
Setting up a project
Scans your repo, asks for what it cannot discover, and writes a schema-valid qa-pilot.config.yaml plus a gap report. Run it once per project, before any other command.
You are generating the host profile, the one file carrying everything project-specific. Every other skill reads it. Discover what you can, ask about what you cannot, and never invent a value.
0. Preflight #
Resolve the profile path: userConfig profile_path, default qa-pilot.config.yaml, relative to the repo root.
If a profile exists there, validate it with node "${CLAUDE_PLUGIN_ROOT}/scripts/lib/profile.mjs" <path>, then ask whether to update it (keep valid values, re-interview only gaps) or abort. Never silently overwrite it.
1. Discovery scan (read-only; do not ask about anything you can find) #
Multi-app repos and sibling repos are the norm, so do not stop at the first app.
Never open .env* files or a deployed runtime configuration (such as a served runtime-config.js), in this repo or any other. They hold secrets; discovery needs only variable names, which the code that reads them shows. You may list which .env* files exist by name.
| Look for | How | Fills |
|---|---|---|
| Apps + frameworks | package.json at root and in workspace/app dirs; dependencies such as vue, react, vuetify, @mui/material, next, svelte |
apps.<name>.framework |
| Playwright version | @playwright/test / playwright in devDependencies; npx playwright --version |
auth.playwright_min |
| Existing spec dirs | playwright.config.*, e2e/, tests/e2e/, cypress/ |
apps.<name>.spec_dir |
| Sandbox / mock mode | variable names only: grep vite.config.*, package.json scripts and source for import.meta.env.<NAME> and process.env.<NAME> named like API_MODE, VITE_API_MODE, MOCK, USE_MOCKS; then ask the user for the value that selects mock mode |
sandbox.mode |
| Documentation sources | design-system or product-model docs (docs/, a component library's docs), and any CLI or script that prints the model or API reference |
context.sources candidates |
| Selector convention | count data-testid, data-test, data-cy: grep -ro 'data-testid' --include=*.vue --include=*.tsx --include=*.jsx . | wc -l |
selectors.testid_attribute |
| House rules / style docs | CLAUDE.md, CONTRIBUTING.md, UI_GUIDELINES.md, docs/ |
selectors.policy_doc |
| Sibling app repos | if a second frontend is mentioned, check sibling dirs for its package.json |
second apps entry + repo |
Adopt what exists, never replace it: an existing Playwright config, spec directory or CI workflow is a fact about this host.
Show the scan results in a short table before asking questions.
2. Interview (only for what discovery cannot know) #
Use AskUserQuestion. Ask in this order, batching related questions:
Tracker: ClickUp (
tracker: clickup, the default) or local files with no tracker (tracker: none)? Undernone, skip items 7 and 8, write noclickupblock, and tell the user approvals live intesting/<feature>/statuses.jsonand all evidence stays local.Environments: names (typically
qa,staging), each app's base URL in each, and each one'skind:qa,stagingorproduction. Askkindexplicitly for every environment, never infer it from the hostname. They must be deployed; localhost is not verdict-eligible.For each environment ask where evidence goes (
evidence_upload; unset takes the default,localfor production,referenceotherwise):reference: traces stay on the executor's machine; the tracker gets the same fields and failure text plus each trace's path and sha256.tracker: traces attached to the tracker. Opt-in, only after Private Attachment Links is on and runs use a restricted account, because a trace carries the session and can carry a refresh token.local: traces stay on the machine; the tracker gets no application data, only path and sha256.
If any environment is
production, say what that commits the host to before going further:- its evidence is
local(trackerandreferenceare refused); evidence.capturemust bealways;- it needs a
test_account; - a
mutationblock with at least one write signature is required (item 9); testing/*/runs/must be gitignored, which/qa-pilot:run-testschecks before every run whose evidence stays on the machine (step 5 below);- the CI template never runs against it.
For every
productionenvironment, ask for itstest_account: the account the runs sign in as and what restricts it (own tenant, no admin rights, no billing). Write it into that environment, at least 20 characters after trimming or the profile is invalid, e.g.qa-runner@example.com, its own tenant, no admin rights, no billing. Say it is an attestation nothing verifies against the saved login, and the write guard is a second layer under that account, never the boundary. If the host has no restricted account, put that in the gap report and leave the profile invalid. Never write atest_accountthe user did not give.Deploy-SHA source: the URL that reports the running build's commit (e.g.
https://qa.example.com/api/version) and how to extract it: a dot path into the JSON response (build.commit) or a regex with one capture group. No such endpoint is a Phase-0 blocker: record it in the gap report and leave the profile invalid, because without a readable SHA no run can be published.Auth model:
dev-handoff(each dev logs in once through a headed browser),role-accounts(dedicated per-role accounts) ormixed(both; the usual answer when permission cases are in scope).Assertions: are application operations visible as requests in the browser Network tab? Engine-dispatched, worker-dispatched or WebSocket-multiplexed apps answer "no" →
network_events: forbidden+style: ui-state. When forbidden, ask whether the app emits structured console logs usable as failure evidence →evidence.extra: [console_log].Approved generation models: which model IDs may author cases; default to the current strongest available. It is a warning, not a gate:
validate-cases.mjswarns on cases stamped with any other model, and QA adds the model here once it trusts it.Cross-app propagation (multi-app only): if a change in app A reaches app B only after a delay (polling bridge, queue, cache), ask for the worst-case window in seconds and set
propagation_window_sto about 3x that, as a ceiling forexpect.poll.ClickUp space and folder (ClickUp only): read
references/clickup.mdand ask there. Recordclickup.spaceandclickup.folder.Status names (ClickUp only): in
references/clickup.md; writeclickup.statuses.What counts as a write (
mutation; required when any environment is production, recommended otherwise). Ask which browser requests change data and record each as awrite_signaturesentry{ method, url, body?, note }: an uppercase HTTP verb or*, a URL regex and, where one URL carries both reads and writes (GraphQL, RPC-stylePOST), abodyregex telling them apart. Record read-only lookalikes asallow_signatures. Put host-specific labels and icon classes of write controls underdeny_controls; the generic defaults (save, delete, publish, ...) stay in force. Confirm patterns against real requests in the Network tab: a missing signature is a write the guard cannot see.Stabilization environment: can new specs earn their three green runs in the sandbox, or do some flows need a deployed environment? If so, which, as
stabilization.env; never production.Documentation sources (
context.sources): confirm the scan candidates, each with aname, adescriptionof what it holds and which features it covers, and exactly one ofpath(file, directory or glob; never.env*) orcommand(run from the repo root; its output is the context). Tell the user everycommandis shown and confirmed before its first run in a session.
3. Emit the profile #
Write the profile to the resolved path, following ${CLAUDE_PLUGIN_ROOT}/schemas/qa-pilot.config.schema.md. Add brief comments for values a reader would question (the propagation window, forbidden network assertions). Then loop until this passes:
node "${CLAUDE_PLUGIN_ROOT}/scripts/lib/profile.mjs" <path>Fix every error; report every warning to the user rather than silencing it. A production environment without a test_account stays an error and goes in the gap report.
4. Gap report #
Print it (do not write a file). For each gap, name the skill it blocks:
| Gap | Blocks |
|---|---|
| No deploy-SHA endpoint | /run-tests and /publish-results: every run, permanently |
| No test-id attributes in the codebase | spec durability; specs bind to fragile selectors |
| Playwright < 1.51 | /setup-profiles: IndexedDB-persisted auth (Firebase) will not restore |
| No sandbox/mock mode | the whole profile: sandbox.mode (the env var and value that switch the app to mocks) is required and has no default. Never invent one; the profile stays invalid until the host names it |
| No role accounts yet | permission-type cases in /generate-tests |
| Deployed builds strip console logs | failure evidence where console output is the required evidence |
testing/*/runs/ not gitignored |
/run-tests on every environment whose evidence stays on the machine (reference, the default, and local), which refuses until ignored |
| No restricted account for a production environment | the whole profile: test_account is required there |
| No write signatures confirmed against real requests | the write guard on production: at least one is required, and an incomplete list is a write it cannot see |
With tracker: clickup, read references/clickup.md for the extra gap rows and the ClickUp setup the user must do. Under tracker: none there is no ClickUp setup; tell the user QA approves cases by editing testing/<feature>/statuses.json.
5. Hand off #
Recommend committing the profile: otherwise each developer tests against a different definition of the truth. Other skills warn rather than refuse when it is uncommitted, so a host trialling the pipeline can gitignore it; say it must be committed before the team relies on it.
Gitignoring QA artifacts is legitimate, but each entry turns off a capability. Say what it costs:
| Path | What stops working when ignored |
|---|---|
testing/<feature>/runs/ |
nothing; ignore it. Required unless every environment sets evidence_upload: tracker |
qa-pilot.config.yaml |
one shared definition of the truth |
<spec_dir>/ (the specs) |
CI, and anyone else reproducing a verdict |
testing/<feature>/cases.yaml |
anyone else running the feature |
testing/<feature>/specs.json |
CI finding the approved specs |
testing/<feature>/verdicts/ |
recording approvals and filing bugs from any machine but the one that ran |
testing/<feature>/approved.json |
approval carry-forward; CI can select nothing |
testing/<feature>/bugs.json |
bug deduplication: every run refiles every open bug |
Nothing refuses to run because of these; the skills warn when it matters. Two modes:
- Local-only: ignore the specs and all of
testing/. You keep approved cases and evidenced verdicts (and a ClickUp record undertracker: clickup) but lose CI, cross-machine reproduction and approval carry-forward, so every run returns the feature to review. Undertracker: noneapprovals also stay on one machine, so it suits a single reviewer. - Committed (recommended): ignore only
testing/*/runs/.
State what Local-only costs rather than arguing. Offer the entries either way:
# Committed mode (recommended)
testing/*/runs/
.playwright/
.qa-pilot/
test-results/
playwright-report/
# Local-only mode adds:
# <spec_dir>/
# testing/
# qa-pilot.config.yamlWhatever mode was chosen, check the run directories are ignored and say the result:
git check-ignore -q testing/x/runs/x && echo "runs are ignored" || echo "runs are NOT ignored"If not ignored, this is not a suggestion: /qa-pilot:run-tests refuses every run whose evidence stays on the machine, which is every production run and, by default, every other one.
Then state the next step: /qa-pilot:setup-profiles to save auth profiles, then /qa-pilot:generate-tests <feature> for the pilot feature.