QA Run
Verification is browser-only. Confirm a scenario by what a user observes
in the browser / admin UI / email sandbox — never mark a step passed because a
DB query, GraphQL response, or log line says so. If a step can only be
confirmed via terminal/DB/GraphQL, that's a plan-defect: log it, don't fake the
pass. Root-causing is different: once a step fails, a dev runner should read
the source to explain why and cite file:line in the Finding — that's an
asset, not a violation.
Step 0: Resolve tracker + locate the plan
Parse $ARGUMENTS. Strip a trailing --tracker linear|github|local into
TRACKER; the remainder is the plan reference. Without the flag, infer from
the reference's shape:
| plan-ref |
TRACKER |
^[A-Z]+-\d+$ or contains linear.app/ |
linear |
^#?\d+$ or github.com/…/issues/N |
github |
| a path, or no arg |
local |
No arg + local → most recent tasks/*/qa-plan.md by mtime. None found → abort.
Validate the precondition before doing any work:
| TRACKER |
Precondition |
linear |
linear CLI installed; delegate all Linear calls to the linear-cli skill |
github |
gh repo view --json nameWithOwner succeeds |
local |
none |
Find the plan issue / file
- linear: the ref is a Linear key/URL. If the issue title doesn't start
with
QA:, it's the parent — list sub-issues and pick the one titled
QA: … (zero → abort telling the user to run /qa-plan --linear; multiple →
ask). Persist its description to tasks/<SUBISSUE_KEY>/qa-plan.md (ask
before overwriting local edits). TASK = SUBISSUE_KEY.
- github: the ref is an issue number/URL.
gh issue view <n> --json title,body.
If the title doesn't start with QA:, treat <n> as the parent and find its
QA: … sub-issue via subIssues (GraphQL, see Step 2). Persist the body to
tasks/pr-<n>/qa-plan.md. TASK = pr-<n>.
- local: use the file directly.
TASK = its parent folder name.
Step 1: Load prior context
Each invocation is a fresh full pass — it re-verifies every scenario. Prior
runs are read only to focus the pass, never to skip scenarios.
- linear: list the plan issue's children via
linear-cli, take the newest
3 titled QA Run *, read their descriptions.
- github:
gh api graphql for issue(number:N){subIssues(last:3){nodes{number,title,body}}}.
- local: glob
tasks/<TASK>/qa-results-*.md, read all matches.
From those bodies build KNOWN_ISSUES: for each §X.Y, its latest status
and any finding still open (logged in one run, not marked fixed in a newer
one). Record recent passes too, so a fresh failure can be flagged a regression.
Echo the focus banner (only if KNOWN_ISSUES is non-empty): N open findings from prior runs to re-verify: §1.2 (<title>), …. Every scenario still
runs — the banner directs attention, it doesn't narrow scope.
Load the playbook:
- Site level: read
~/.claude/qa-playbook/<repo>.md (<repo> =
basename "$(git rev-parse --show-toplevel)"). Missing = empty, not an error.
- Scenario level: read the
## QA Playbook comment on the plan issue
(linear/github) or tasks/<TASK>/qa-playbook.md (local). Missing = empty.
Step 2: Create the run record
Created in Todo, assigned to the runner, so the tracker shows every run's
lifecycle end to end — Todo → In Progress (Step 3) → closed (Step 4).
The body/file starts from the template in Report format below, with one
[ ] status line per scenario (parsed from the plan's ### N.M headings).
Resolve $T once (pre-flight, shared by every scenario): chrome-debug.sh,
then cdp.mjs list, confirming the top target's title/URL is actually the app
under test — a fixture that collides or short-circuits burns a whole pass,
catch it here as a plan-defect rather than mark passes against it.
Step 3: Iterate scenarios
Move the run to In Progress before the first scenario:
linear issue update <RUN_KEY> --state started (github/local: no-op —
the issue is already open / the file already exists).
For each scenario in plan order — all of them, nothing is skipped, unless the
user stops the run early after a blocker (see Step 4):
- Announce: §X.Y title, preconditions. If in
KNOWN_ISSUES, frame as
fix-verification: §X.Y previously failed (Finding: <title>, <prior-ts>) — verify whether it's fixed.
- Spawn one sub-agent (sequential — never parallel) following
references/scenario-agent.md: it replays the
scenario's playbook block if one exists, falls back to interactive
exploration otherwise, and returns the structured payload described there.
- Upload the evidence (
--tracker linear only — github/local keep the
local paths as-is): for the payload's proof, run
URL=$(node scripts/upload-image.mjs <proof>). Use $URL — never the local
path — everywhere the run doc cites that proof. If the upload exits non-zero,
cite the local path and note (upload failed); never abort the run for it.
debug shots are not uploaded here — they go to the finding comment in Step 5.
- Decide outcome, annotate with prior-run relation, append to the run doc:
- Pass →
[x] §X.Y — passed (<note>, [📷 §X.Y](<proof URL or path>)). If it was a known
failure: (fixed: was <finding-ref> @ <prior-ts>).
- Fail →
[!] §X.Y — failed → Finding #<n> + a Finding entry (dev runner
fills Root cause with file:line; PM/QA leaves it blank). If it was a
known failure: (still failing — was <finding-ref> @ <prior-ts>); if it
passed previously: (regression — passed @ <prior-ts>).
- Skip →
[~] §X.Y — skipped (<reason>), Finding tagged plan-defect if the
plan itself is the problem.
- Rewrite the run body/file now (never batch):
linear issue update <RUN_KEY> --description-file $TMP / gh issue edit <n> --body-file $TMP /
overwrite the local file. If the push fails (auth, network), print the
error, write the doc to tasks/<TASK>/qa-results-<ts>.md as a crash file,
and continue the run — a 40-minute pass must not die to a token expiry.
- Accumulate the playbook delta from the sub-agent's payload (don't write
yet — merged once at end of run, Step 4).
Step 4: End-of-run summary, close-out + playbook merge
This step runs on every exit path — all scenarios completed, or the user
stopped the run early after a blocker finding — so the run issue never sits
abandoned in "In Progress". Run it against whatever scenarios actually
executed.
- Totals: passed / failed / skipped / pending (pending = not reached, only
possible on an early stop).
- Fix-verification rollup (only if
KNOWN_ISSUES was non-empty): Prior issues: 2 fixed, 1 still open (§3.1), 1 regression (§1.2).
- Triage table grouped by severity:
blocker, improvement, nit, plan-defect.
- Next step: blockers → fix, re-run; plan-defects → fix the plan, re-run;
nits only → ready for review.
Close the run: linear issue update <RUN_KEY> --state completed /
gh issue close <n> (local: nothing to close). This applies whether the run
finished normally or was stopped early on a blocking finding — the run itself
completed its job (it produced a result), so it closes rather than staying open.
Attach the debug shots for each finding (--tracker linear): post the
finding's gallery with linear issue comment add <RUN_KEY> --attach shot.png
(repeatable) — --attach always appends the images at the end of the comment,
which is exactly what a gallery wants. Under github/local the run doc's
local tasks/<TASK>/evidence/ paths are the record. See
references/linear-evidence.md for the
mechanics and the inline-image alternative.
Merge the playbook (once, here — not per scenario):
- Site: append each returned
playbook.site line to
~/.claude/qa-playbook/<repo>.md under its section, skipping near-duplicates.
- Scenario: splice each returned
playbook.scenario block into the single
## QA Playbook comment/file, applying the staleness rule from
references/playbook.md: a stale:* replay
with a corrected block replaces it (Stale: 0); a stale:* replay with no fix
keeps the old block and bumps Stale: 1; a block already at Stale: 1 that
goes stale again is deleted. Write back with a single
comment update / issue edit / file overwrite.
Report format (run body / qa-results-.md)
# QA Results — <plan title>
**Plan:** <plan issue URL or tasks/<TASK>/qa-plan.md>
**Run:** <ts>
## Status
- [ ] §1.1 — <title>
- [ ] §1.2 — <title>
<!-- proof links are Linear assetUrls under --tracker linear, local paths otherwise -->
## Findings
### Finding #1 — <title> — <blocker|improvement|nit|plan-defect>
**Scenario:** §X.Y
**Prior:** <finding-ref @ prior-ts if recurring; omit for new findings>
**Repro:** <steps observed>
**Expected:** <from plan>
**Observed:** <what happened>
**Root cause:** <dev only — file:line; omit if not investigated>
**Evidence:** proof: <assetUrl under linear, else path>; debug: <path>, <path>
## Notes
- <gotcha not worth a playbook entry>
Rules
- Plan is immutable. Never edit the plan file or issue description. The
## QA Playbook comment on the same issue is mutable and not part of the
plan — it's expected to change every run. If the plan itself is wrong, log
a plan-defect finding and tell the user to fix it separately.
- The tracker is the record. Under
linear/github, the run sub-issue is
canonical; under local, the file is. Never re-derive state from anything else.
- Never edit a previous run's issue/file. Each invocation owns one run
record; within the current run, rewrite the whole body on each scenario
(Step 3.4) rather than appending — the body is a live document, not a log.
- Status lines are one line. Detail goes in Findings.
- Verify in the browser, root-cause in the source. A pass rests only on
something observed in the UI/email — never a DB/GraphQL peek. Once something
fails, reading the source to explain why (with
file:line) is encouraged.
- Browser CDP is OK (via
chrome-cdp --isolated) for clicking faster than a
human, not for bypassing audience constraints. No evaluate() shortcuts to
read the DB, no GraphQL fetches, no localStorage poking beyond what a
normal user does, unless the user explicitly allows it to verify a tricky state.
- One scenario at a time, sequential sub-agents. Each finishes and is
logged before the next is spawned.
- A failing scenario never contributes a
Verify fast path — see
references/playbook.md. It can still
contribute Reach (the precondition) plus site notes.
- The run issue's lifecycle is Todo → In Progress → closed, every time.
Assigned to the runner at creation (Step 2). Never leave it "In Progress"
after the run stops — Step 4 runs, and the issue closes, on any exit path.
1---2name: qa-run3description: Execute a qa-plan interactively, autonomously driving Chrome via chrome-cdp by default. Each run is posted as its own sub-issue/file under the plan (--tracker linear|github|local), re-verifies every scenario, and surfaces prior runs' open findings. A persisted playbook (site knowledge + per-scenario fast paths) lets later runs replay known scenarios in one batched command instead of re-exploring. This skill should be used to run a manual QA pass against a plan produced by /qa-plan, whether the runner is the dev (dogfooding before handoff) or the PM/QA tester.4---56# QA Run78**Verification is browser-only.** Confirm a scenario by what a user observes9in the browser / admin UI / email sandbox — never mark a step passed because a10DB query, GraphQL response, or log line says so. If a step can *only* be11confirmed via terminal/DB/GraphQL, that's a plan-defect: log it, don't fake the12pass. Root-causing is different: once a step fails, a dev runner should read13the source to explain why and cite `file:line` in the Finding — that's an14asset, not a violation.1516## Step 0: Resolve tracker + locate the plan1718Parse `$ARGUMENTS`. Strip a trailing `--tracker linear|github|local` into19`TRACKER`; the remainder is the plan reference. Without the flag, infer from20the reference's shape:2122| plan-ref | TRACKER |23|---|---|24| `^[A-Z]+-\d+$` or contains `linear.app/` | `linear` |25| `^#?\d+$` or `github.com/…/issues/N` | `github` |26| a path, or no arg | `local` |2728No arg + `local` → most recent `tasks/*/qa-plan.md` by mtime. None found → abort.2930Validate the precondition **before** doing any work:3132| TRACKER | Precondition |33|---|---|34| `linear` | `linear` CLI installed; delegate all Linear calls to the **`linear-cli`** skill |35| `github` | `gh repo view --json nameWithOwner` succeeds |36| `local` | none |3738### Find the plan issue / file3940- **linear**: the ref is a Linear key/URL. If the issue title doesn't start41 with `QA:`, it's the parent — list sub-issues and pick the one titled42 `QA: …` (zero → abort telling the user to run `/qa-plan --linear`; multiple →43 ask). Persist its description to `tasks/<SUBISSUE_KEY>/qa-plan.md` (ask44 before overwriting local edits). `TASK = SUBISSUE_KEY`.45- **github**: the ref is an issue number/URL. `gh issue view <n> --json title,body`.46 If the title doesn't start with `QA:`, treat `<n>` as the parent and find its47 `QA: …` sub-issue via `subIssues` (GraphQL, see Step 2). Persist the body to48 `tasks/pr-<n>/qa-plan.md`. `TASK = pr-<n>`.49- **local**: use the file directly. `TASK` = its parent folder name.5051## Step 1: Load prior context5253Each invocation is a fresh full pass — it re-verifies every scenario. Prior54runs are read only to *focus* the pass, never to skip scenarios.5556- **linear**: list the plan issue's children via `linear-cli`, take the newest57 3 titled `QA Run *`, read their descriptions.58- **github**: `gh api graphql` for `issue(number:N){subIssues(last:3){nodes{number,title,body}}}`.59- **local**: glob `tasks/<TASK>/qa-results-*.md`, read all matches.6061From those bodies build **`KNOWN_ISSUES`**: for each `§X.Y`, its latest status62and any finding still open (logged in one run, not marked fixed in a newer63one). Record recent passes too, so a fresh failure can be flagged a regression.6465**Echo the focus banner** (only if `KNOWN_ISSUES` is non-empty): `N open66findings from prior runs to re-verify: §1.2 (<title>), …`. Every scenario still67runs — the banner directs attention, it doesn't narrow scope.6869**Load the playbook:**70- Site level: read `~/.claude/qa-playbook/<repo>.md` (`<repo>` =71 `basename "$(git rev-parse --show-toplevel)"`). Missing = empty, not an error.72- Scenario level: read the `## QA Playbook` comment on the plan issue73 (`linear`/`github`) or `tasks/<TASK>/qa-playbook.md` (`local`). Missing = empty.7475## Step 2: Create the run record7677Created in **Todo**, assigned to the runner, so the tracker shows every run's78lifecycle end to end — Todo → In Progress (Step 3) → closed (Step 4).7980- **linear**: `linear issue create --parent "$QA_KEY" --title "QA Run <ts>" --assignee self --state unstarted --description-file $TMP` (`<ts>` = `date +%Y%m%d-%H%M%S`; `unstarted` is the state *type* — Todo in Linear's default template — not a team-specific state name, so this holds regardless of the team's actual label).81- **github**: `gh label create qa-run --color 5319E7 2>/dev/null || true`, then82 `gh issue create --title "QA Run <ts>" --label qa-run --assignee @me --body-file $TMP`83 (GitHub issues have no Todo/In-Progress distinction without a Projects board,84 out of scope here — the issue is simply open, assigned, for the run's85 duration, then closed in Step 4), then nest it under the plan issue:86 ```bash87 gh api graphql -f query='mutation($p:ID!,$c:ID!){addSubIssue(input:{issueId:$p,subIssueId:$c}){clientMutationId}}' -F p="$PARENT_NODE_ID" -F c="$RUN_NODE_ID"88 ```89 (node ids via `gh api graphql -f query='{repository(owner:"O",name:"R"){issue(number:N){id}}}'`).90 If `addSubIssue` fails (org without sub-issues enabled), fall back to a91 comment on the plan issue linking the run issue and say so.92- **local**: write `tasks/<TASK>/qa-results-<ts>.md` from the template below.9394The body/file starts from the template in **Report format** below, with one95`[ ]` status line per scenario (parsed from the plan's `### N.M` headings).9697**Resolve `$T` once** (pre-flight, shared by every scenario): `chrome-debug.sh`,98then `cdp.mjs list`, confirming the top target's title/URL is actually the app99under test — a fixture that collides or short-circuits burns a whole pass,100catch it here as a plan-defect rather than mark passes against it.101102## Step 3: Iterate scenarios103104**Move the run to In Progress** before the first scenario:105`linear issue update <RUN_KEY> --state started` (`github`/`local`: no-op —106the issue is already open / the file already exists).107108For each scenario in plan order — all of them, nothing is skipped, unless the109user stops the run early after a blocker (see Step 4):1101111. **Announce**: §X.Y title, preconditions. If in `KNOWN_ISSUES`, frame as112 fix-verification: `§X.Y previously failed (Finding: <title>, <prior-ts>) — verify whether it's fixed.`1132. **Spawn one sub-agent** (sequential — never parallel) following114 [references/scenario-agent.md](references/scenario-agent.md): it replays the115 scenario's playbook block if one exists, falls back to interactive116 exploration otherwise, and returns the structured payload described there.1173. **Upload the evidence** (`--tracker linear` only — `github`/`local` keep the118 local paths as-is): for the payload's `proof`, run119 `URL=$(node scripts/upload-image.mjs <proof>)`. Use `$URL` — never the local120 path — everywhere the run doc cites that proof. If the upload exits non-zero,121 cite the local path and note `(upload failed)`; never abort the run for it.122 `debug` shots are not uploaded here — they go to the finding comment in Step 5.1234. **Decide outcome, annotate with prior-run relation, append to the run doc:**124 - Pass → `[x] §X.Y — passed (<note>, [📷 §X.Y](<proof URL or path>))`. If it was a known125 failure: `(fixed: was <finding-ref> @ <prior-ts>)`.126 - Fail → `[!] §X.Y — failed → Finding #<n>` + a Finding entry (dev runner127 fills **Root cause** with `file:line`; PM/QA leaves it blank). If it was a128 known failure: `(still failing — was <finding-ref> @ <prior-ts>)`; if it129 passed previously: `(regression — passed @ <prior-ts>)`.130 - Skip → `[~] §X.Y — skipped (<reason>)`, Finding tagged `plan-defect` if the131 plan itself is the problem.1325. **Rewrite the run body/file now** (never batch): `linear issue update133 <RUN_KEY> --description-file $TMP` / `gh issue edit <n> --body-file $TMP` /134 overwrite the local file. **If the push fails** (auth, network), print the135 error, write the doc to `tasks/<TASK>/qa-results-<ts>.md` as a crash file,136 and continue the run — a 40-minute pass must not die to a token expiry.1376. **Accumulate the playbook delta** from the sub-agent's payload (don't write138 yet — merged once at end of run, Step 4).139140## Step 4: End-of-run summary, close-out + playbook merge141142This step runs on **every** exit path — all scenarios completed, or the user143stopped the run early after a blocker finding — so the run issue never sits144abandoned in "In Progress". Run it against whatever scenarios actually145executed.146147- Totals: passed / failed / skipped / pending (pending = not reached, only148 possible on an early stop).149- **Fix-verification rollup** (only if `KNOWN_ISSUES` was non-empty): `Prior150 issues: 2 fixed, 1 still open (§3.1), 1 regression (§1.2).`151- Triage table grouped by severity: `blocker`, `improvement`, `nit`, `plan-defect`.152- Next step: blockers → fix, re-run; plan-defects → fix the plan, re-run;153 nits only → ready for review.154155**Close the run:** `linear issue update <RUN_KEY> --state completed` /156`gh issue close <n>` (`local`: nothing to close). This applies whether the run157finished normally or was stopped early on a blocking finding — the run itself158completed its job (it produced a result), so it closes rather than staying open.159160**Attach the debug shots** for each finding (`--tracker linear`): post the161finding's gallery with `linear issue comment add <RUN_KEY> --attach shot.png`162(repeatable) — `--attach` always appends the images at the end of the comment,163which is exactly what a gallery wants. Under `github`/`local` the run doc's164local `tasks/<TASK>/evidence/` paths are the record. See165[references/linear-evidence.md](references/linear-evidence.md) for the166mechanics and the inline-image alternative.167168**Merge the playbook** (once, here — not per scenario):169- Site: append each returned `playbook.site` line to170 `~/.claude/qa-playbook/<repo>.md` under its section, skipping near-duplicates.171- Scenario: splice each returned `playbook.scenario` block into the single172 `## QA Playbook` comment/file, applying the staleness rule from173 [references/playbook.md](references/playbook.md#learning): a `stale:*` replay174 with a corrected block replaces it (`Stale: 0`); a `stale:*` replay with no fix175 keeps the old block and bumps `Stale: 1`; a block already at `Stale: 1` that176 goes stale again is **deleted**. Write back with a single177 `comment update` / `issue edit` / file overwrite.178179## Report format (run body / qa-results-<ts>.md)180181```markdown182# QA Results — <plan title>183184**Plan:** <plan issue URL or tasks/<TASK>/qa-plan.md>185**Run:** <ts>186187## Status188- [ ] §1.1 — <title>189- [ ] §1.2 — <title>190191<!-- proof links are Linear assetUrls under --tracker linear, local paths otherwise -->192193## Findings194195### Finding #1 — <title> — <blocker|improvement|nit|plan-defect>196**Scenario:** §X.Y197**Prior:** <finding-ref @ prior-ts if recurring; omit for new findings>198**Repro:** <steps observed>199**Expected:** <from plan>200**Observed:** <what happened>201**Root cause:** <dev only — file:line; omit if not investigated>202**Evidence:** proof: <assetUrl under linear, else path>; debug: <path>, <path>203204## Notes205- <gotcha not worth a playbook entry>206```207208## Rules209210- **Plan is immutable.** Never edit the plan file or issue description. The211 `## QA Playbook` comment on the same issue is **mutable and not part of the212 plan** — it's expected to change every run. If the plan itself is wrong, log213 a `plan-defect` finding and tell the user to fix it separately.214- **The tracker is the record.** Under `linear`/`github`, the run sub-issue is215 canonical; under `local`, the file is. Never re-derive state from anything else.216- **Never edit a previous run's issue/file.** Each invocation owns one run217 record; within the current run, rewrite the whole body on each scenario218 (Step 3.4) rather than appending — the body is a live document, not a log.219- **Status lines are one line.** Detail goes in Findings.220- **Verify in the browser, root-cause in the source.** A pass rests only on221 something observed in the UI/email — never a DB/GraphQL peek. Once something222 fails, reading the source to explain why (with `file:line`) is encouraged.223- **Browser CDP is OK** (via `chrome-cdp --isolated`) for clicking faster than a224 human, not for bypassing audience constraints. No `evaluate()` shortcuts to225 read the DB, no GraphQL fetches, no `localStorage` poking beyond what a226 normal user does, unless the user explicitly allows it to verify a tricky state.227- **One scenario at a time, sequential sub-agents.** Each finishes and is228 logged before the next is spawned.229- **A failing scenario never contributes a `Verify` fast path** — see230 [references/playbook.md](references/playbook.md#learning). It can still231 contribute `Reach` (the precondition) plus site notes.232- **The run issue's lifecycle is Todo → In Progress → closed, every time.**233 Assigned to the runner at creation (Step 2). Never leave it "In Progress"234 after the run stops — Step 4 runs, and the issue closes, on any exit path.