Reference
The config schema, how to add a stack, and what every agent and skill does. If you are looking for what this is and why, start at the overview.
qa.config.yml
One file at the project root. Every agent reads it before doing anything, so a wrong value here makes everything downstream confidently wrong.
| Key | Meaning |
|---|---|
project.name | Used in report headers and generated file names |
project.glossary | Optional. A markdown file of domain terms, so generated tests speak the business's language |
stacks.<platform>.profile | Folder name under profiles/. This is the line that picks your language and runner |
stacks.<platform>.root | Where the test code for this stack lives, relative to the project root |
stacks.<platform>.base_url_env | Name of the env var holding the base URL. Agents never hardcode a URL |
stacks.mobile.platforms | [android, ios] |
stacks.mobile.app | Path or build artifact URL, per platform |
ci.provider | github-actions, gitlab-ci, jenkins, circleci, none. Picks which template /qa:init copies out of the profile |
tracker.provider | jira, github-issues, linear, none. Where requirements come from and bugs go |
tracker.project_key | e.g. SHOP |
requirements.source | confluence, markdown, github-issues, jira |
requirements.path | Folder of requirement documents, when the source is markdown |
reporting.artifacts | Where the runner drops artifacts. The maintainer and auditor read these |
policies.test_id_attribute | The attribute the author agent prefers for a stable hook |
policies.max_test_runtime_s | A test slower than this gets flagged in an audit |
policies.forbid | House rules. Not advice, see below |
policies.forbid
Five rules are understood out of the box, and qa-policy checks all five. Anything else you put in the list is read by the agents but not machine checked.
| Rule | Catches | Check |
|---|---|---|
hard_sleeps | waitForTimeout, time.sleep, Thread.sleep, implicitly_wait, driver.pause, cy.wait(500) | exact |
absolute_xpath | //div[3]/span, anything rooted at /html | exact |
conditional_assertions | An assertion inside a branch, so the test can pass by skipping its own check | heuristic |
no_assertion | A test that drives the app and never checks anything | heuristic |
shared_mutable_state | describe.serial, module level mutables, enforced ordering | heuristic |
Profiles
A profile teaches the agents one stack. It holds no QA reasoning, just the mechanics.
profiles/<name>/
profile.yaml commands, layout, templates, selector priority
CONVENTIONS.md how to write a test here, with real code
templates/ skeleton files the author agent copies
ci/ one pipeline file per CI provider
What ships
| Profile | Language | Runner | Platform | State |
|---|---|---|---|---|
ts-playwright | TypeScript | Playwright | web | demo runs in CI |
py-pytest-playwright | Python | pytest + Playwright | web | demo runs in CI |
mobile-appium-wdio | TypeScript | WebdriverIO + Appium | mobile | typechecked in CI, run locally on a device |
_template | any | any | any | blank starting point |
Adding one
cp -r profiles/_template profiles/java-selenium-junit5
# fill in profile.yaml, name must match the folder
# write CONVENTIONS.md with real code, this is the file that decides
# whether generated tests look like they belong in your repo
python3 scripts/validate_profiles.py
Then point a project at it with stacks.web.profile: java-selenium-junit5. No agent changes.
Java plus Selenium plus JUnit 5 is deliberately not shipped. It is the obvious next profile and writing it from the template is the fastest way to find out whether this design holds up.
Who reads what
| File | Read by | For |
|---|---|---|
commands | maintainer, auditor | Running the suite, rerunning one test |
layout | author, auditor | Where a new file goes, which files are tests |
templates | author | Starting a test or page object |
selector_priority | author, exploratory guide | Picking a locator |
CONVENTIONS.md | author, maintainer | Everything the template cannot express |
ci/ | /qa:init | Dropping a pipeline into a new project |
Command placeholders: {file} for a test file path, {title} for a test name, {tag} for a tag or marker expression.
Agents
Three. An agent exists where the work reads a lot of files and needs no conversation. Everything interactive is a skill running in the main thread, because a subagent cannot ask you a question.
test-author
Writes tests and page objects in the active profile's style. Runs what it wrote, then breaks it on purpose to confirm it can fail. Reports which design rows it did not write and why.
test-maintainer
Fixes failing and flaky tests and refactors duplication. Triages through five causes before editing. Fixes the cause once rather than the symptom four times.
suite-auditor
Scores a suite against a nine area rubric with evidence, ranks findings by cost over effort, and changes nothing.
What they refuse to do
- Author: promote a unit row to e2e because e2e is the layer it can reach. Invent an expected result. Write a row the design said not to automate.
- Maintainer: add a sleep, widen an assertion to make it pass, add retries to hide a flake, or delete a test to clear a red build.
- Auditor: change anything, publish a total score, or report a finding without a path and a line behind it.
Those lists are the interesting part of each agent. The rest is method.
Skills
Workflows
| Skill | Does | Hands off to |
|---|---|---|
/qa:init | Works out the stack from the repo, writes qa.config.yml, drops in CI | inline |
/qa:profile | Reads an existing suite and drafts a profile describing what that repo actually does, into <project>/profiles/ | inline, it asks which pattern wins where the suite disagrees with itself |
/qa:refine | Ticket to acceptance criteria, ambiguities, risk. Asks the blocking questions | inline, it needs a conversation |
/qa:design | Risk scored coverage matrix, layer per row, plus what is not covered | inline |
/qa:explore | Session based exploratory testing through the browser or device MCP | inline, it needs a conversation |
/qa:generate | Writes the automatable rows | test-author past three rows |
/qa:fix | Triage then fix | test-maintainer past three failures |
/qa:audit | Score and rank | suite-auditor, always |
/qa:prevalidate | Runs the policy check against what is staged, same rules as the edit hook | inline, it only reports |
Reference skills
Model invoked, pulled in by the workflows and usable on their own.
| Skill | Holds |
|---|---|
qa-config | How to resolve the project setup and what to do when it fails. Every other skill leans on this |
test-design-techniques | Equivalence partitioning, boundary values, decision tables, state transitions, pairwise. Which technique for which shape of requirement, risk scoring, and choosing a layer |
flake-triage | The five causes of a red test in the order they are actually likely, and the rerun protocol (qa-rerun runs it) that tells a flake from a defect |
prevalidate | Runs qa-policy against what is staged. Standalone outside Claude Code too, see setup |
bug-report | A report shape that avoids the round trip, and how to file to the configured tracker |
/qa:profile is the brownfield entry point. A shipped profile knows how to run your tests. It does not know how you write them, and generated code that fights your reviewers is worse than no generated code. See the setup page.
page-object-authoring skill. That knowledge lives in each profile's CONVENTIONS.md, where it can be stack specific and correct, instead of in a skill where it would have to be generic and vague.
Tools and wiring
Three commands
All stdlib only Python, because they run on the critical path of every task and adding a pip install there would be a bad trade for a few fields of YAML. All three also run standalone, outside Claude Code, from a bare clone.
qa-context [stack] [--paths] resolve config + active profile + conventions
qa-policy <file>... | --stack check against policies.forbid
qa-policy --changed-only <file> only lines this working tree added or changed
qa-rerun <file> --title "..." run a test N times alone/suite/parallel, read the pattern
One hook
PostToolUse on Write and Edit runs qa-policy --hook. A test written with a forbidden pattern in it exits 2, which is the code that sends the message back to be fixed rather than only printing it. Outside a project with a config, it does nothing.
--hook implies --changed-only, and that is what makes it usable on a repo that already has a suite. It reports violations on lines the edit touched, not on the whole file. Dropping this into a codebase with 200 existing sleeps and then failing every edit to every file containing one is not a guardrail, it is a wall, and it gets the hook deleted on day two. Pre-existing violations are still real and still surface in an audit, where a decision about them can actually be made. A file git has never seen is checked in full, since there is nothing to compare it against.
MCP
| Server | For | State |
|---|---|---|
| Playwright MCP | Driving a real browser during exploratory sessions, and finding selectors before writing a test | ships enabled in .mcp.json |
| Appium or WebdriverIO MCP | The mobile equivalent | opt in, needs a local Appium server and a device |
| Atlassian | Pulling requirements out of Jira and Confluence, filing bugs back | opt in, per tracker.provider |
gh CLI | GitHub issues and PRs | no MCP needed |
Checks
python3 scripts/validate_profiles.py every profile.yaml and every qa.config.yml
python3 scripts/test_qa_context.py the config parsing
python3 scripts/test_qa_policy.py that qa-policy fires on what it claims to
python3 scripts/test_qa_rerun.py that qa-rerun resolves the right command and reads the pattern right
All three run in CI, along with both web demo suites against the live site and a typecheck of the mobile example.
The third one matters most. A policy checker that never fires reads as coverage and is worse than not having one, so its checks include the shapes it must catch and the shapes it must stay quiet on.