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: >-4 Execute a qa-plan interactively, autonomously driving Chrome via chrome-cdp5 by default. Each run is posted as its own sub-issue/file under the plan6 (--tracker linear|github|local), re-verifies every scenario, and surfaces7 prior runs' open findings. A persisted playbook (site knowledge + per-scenario8 fast paths) lets later runs replay known scenarios in one batched command9 instead of re-exploring. This skill should be used to run a manual QA pass10 against a plan produced by /qa-plan, whether the runner is the dev11 (dogfooding before handoff) or the PM/QA tester.12argument-hint: "[plan-ref] [--tracker linear|github|local]"13---1415# QA Run1617**Verification is browser-only.** Confirm a scenario by what a user observes18in the browser / admin UI / email sandbox — never mark a step passed because a19DB query, GraphQL response, or log line says so. If a step can *only* be20confirmed via terminal/DB/GraphQL, that's a plan-defect: log it, don't fake the21pass. Root-causing is different: once a step fails, a dev runner should read22the source to explain why and cite `file:line` in the Finding — that's an23asset, not a violation.2425## Step 0: Resolve tracker + locate the plan2627Parse `$ARGUMENTS`. Strip a trailing `--tracker linear|github|local` into28`TRACKER`; the remainder is the plan reference. Without the flag, infer from29the reference's shape:3031| plan-ref | TRACKER |32|---|---|33| `^[A-Z]+-\d+$` or contains `linear.app/` | `linear` |34| `^#?\d+$` or `github.com/…/issues/N` | `github` |35| a path, or no arg | `local` |3637No arg + `local` → most recent `tasks/*/qa-plan.md` by mtime. None found → abort.3839Validate the precondition **before** doing any work:4041| TRACKER | Precondition |42|---|---|43| `linear` | `linear` CLI installed; delegate all Linear calls to the **`linear-cli`** skill |44| `github` | `gh repo view --json nameWithOwner` succeeds |45| `local` | none |4647### Find the plan issue / file4849- **linear**: the ref is a Linear key/URL. If the issue title doesn't start50 with `QA:`, it's the parent — list sub-issues and pick the one titled51 `QA: …` (zero → abort telling the user to run `/qa-plan --linear`; multiple →52 ask). Persist its description to `tasks/<SUBISSUE_KEY>/qa-plan.md` (ask53 before overwriting local edits). `TASK = SUBISSUE_KEY`.54- **github**: the ref is an issue number/URL. `gh issue view <n> --json title,body`.55 If the title doesn't start with `QA:`, treat `<n>` as the parent and find its56 `QA: …` sub-issue via `subIssues` (GraphQL, see Step 2). Persist the body to57 `tasks/pr-<n>/qa-plan.md`. `TASK = pr-<n>`.58- **local**: use the file directly. `TASK` = its parent folder name.5960## Step 1: Load prior context6162Each invocation is a fresh full pass — it re-verifies every scenario. Prior63runs are read only to *focus* the pass, never to skip scenarios.6465- **linear**: list the plan issue's children via `linear-cli`, take the newest66 3 titled `QA Run *`, read their descriptions.67- **github**: `gh api graphql` for `issue(number:N){subIssues(last:3){nodes{number,title,body}}}`.68- **local**: glob `tasks/<TASK>/qa-results-*.md`, read all matches.6970From those bodies build **`KNOWN_ISSUES`**: for each `§X.Y`, its latest status71and any finding still open (logged in one run, not marked fixed in a newer72one). Record recent passes too, so a fresh failure can be flagged a regression.7374**Echo the focus banner** (only if `KNOWN_ISSUES` is non-empty): `N open75findings from prior runs to re-verify: §1.2 (<title>), …`. Every scenario still76runs — the banner directs attention, it doesn't narrow scope.7778**Load the playbook:**79- Site level: read `~/.claude/qa-playbook/<repo>.md` (`<repo>` =80 `basename "$(git rev-parse --show-toplevel)"`). Missing = empty, not an error.81- Scenario level: read the `## QA Playbook` comment on the plan issue82 (`linear`/`github`) or `tasks/<TASK>/qa-playbook.md` (`local`). Missing = empty.8384## Step 2: Create the run record8586Created in **Todo**, assigned to the runner, so the tracker shows every run's87lifecycle end to end — Todo → In Progress (Step 3) → closed (Step 4).8889- **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).90- **github**: `gh label create qa-run --color 5319E7 2>/dev/null || true`, then91 `gh issue create --title "QA Run <ts>" --label qa-run --assignee @me --body-file $TMP`92 (GitHub issues have no Todo/In-Progress distinction without a Projects board,93 out of scope here — the issue is simply open, assigned, for the run's94 duration, then closed in Step 4), then nest it under the plan issue:95 ```bash96 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"97 ```98 (node ids via `gh api graphql -f query='{repository(owner:"O",name:"R"){issue(number:N){id}}}'`).99 If `addSubIssue` fails (org without sub-issues enabled), fall back to a100 comment on the plan issue linking the run issue and say so.101- **local**: write `tasks/<TASK>/qa-results-<ts>.md` from the template below.102103The body/file starts from the template in **Report format** below, with one104`[ ]` status line per scenario (parsed from the plan's `### N.M` headings).105106**Resolve `$T` once** (pre-flight, shared by every scenario): `chrome-debug.sh`,107then `cdp.mjs list`, confirming the top target's title/URL is actually the app108under test — a fixture that collides or short-circuits burns a whole pass,109catch it here as a plan-defect rather than mark passes against it.110111## Step 3: Iterate scenarios112113**Move the run to In Progress** before the first scenario:114`linear issue update <RUN_KEY> --state started` (`github`/`local`: no-op —115the issue is already open / the file already exists).116117For each scenario in plan order — all of them, nothing is skipped, unless the118user stops the run early after a blocker (see Step 4):1191201. **Announce**: §X.Y title, preconditions. If in `KNOWN_ISSUES`, frame as121 fix-verification: `§X.Y previously failed (Finding: <title>, <prior-ts>) — verify whether it's fixed.`1222. **Spawn one sub-agent** (sequential — never parallel) following123 [references/scenario-agent.md](references/scenario-agent.md): it replays the124 scenario's playbook block if one exists, falls back to interactive125 exploration otherwise, and returns the structured payload described there.1263. **Upload the evidence** (`--tracker linear` only — `github`/`local` keep the127 local paths as-is): for the payload's `proof`, run128 `URL=$(node scripts/upload-image.mjs <proof>)`. Use `$URL` — never the local129 path — everywhere the run doc cites that proof. If the upload exits non-zero,130 cite the local path and note `(upload failed)`; never abort the run for it.131 `debug` shots are not uploaded here — they go to the finding comment in Step 5.1324. **Decide outcome, annotate with prior-run relation, append to the run doc:**133 - Pass → `[x] §X.Y — passed (<note>, [📷 §X.Y](<proof URL or path>))`. If it was a known134 failure: `(fixed: was <finding-ref> @ <prior-ts>)`.135 - Fail → `[!] §X.Y — failed → Finding #<n>` + a Finding entry (dev runner136 fills **Root cause** with `file:line`; PM/QA leaves it blank). If it was a137 known failure: `(still failing — was <finding-ref> @ <prior-ts>)`; if it138 passed previously: `(regression — passed @ <prior-ts>)`.139 - Skip → `[~] §X.Y — skipped (<reason>)`, Finding tagged `plan-defect` if the140 plan itself is the problem.1415. **Rewrite the run body/file now** (never batch): `linear issue update142 <RUN_KEY> --description-file $TMP` / `gh issue edit <n> --body-file $TMP` /143 overwrite the local file. **If the push fails** (auth, network), print the144 error, write the doc to `tasks/<TASK>/qa-results-<ts>.md` as a crash file,145 and continue the run — a 40-minute pass must not die to a token expiry.1466. **Accumulate the playbook delta** from the sub-agent's payload (don't write147 yet — merged once at end of run, Step 4).148149## Step 4: End-of-run summary, close-out + playbook merge150151This step runs on **every** exit path — all scenarios completed, or the user152stopped the run early after a blocker finding — so the run issue never sits153abandoned in "In Progress". Run it against whatever scenarios actually154executed.155156- Totals: passed / failed / skipped / pending (pending = not reached, only157 possible on an early stop).158- **Fix-verification rollup** (only if `KNOWN_ISSUES` was non-empty): `Prior159 issues: 2 fixed, 1 still open (§3.1), 1 regression (§1.2).`160- Triage table grouped by severity: `blocker`, `improvement`, `nit`, `plan-defect`.161- Next step: blockers → fix, re-run; plan-defects → fix the plan, re-run;162 nits only → ready for review.163164**Close the run:** `linear issue update <RUN_KEY> --state completed` /165`gh issue close <n>` (`local`: nothing to close). This applies whether the run166finished normally or was stopped early on a blocking finding — the run itself167completed its job (it produced a result), so it closes rather than staying open.168169**Attach the debug shots** for each finding (`--tracker linear`): post the170finding's gallery with `linear issue comment add <RUN_KEY> --attach shot.png`171(repeatable) — `--attach` always appends the images at the end of the comment,172which is exactly what a gallery wants. Under `github`/`local` the run doc's173local `tasks/<TASK>/evidence/` paths are the record. See174[references/linear-evidence.md](references/linear-evidence.md) for the175mechanics and the inline-image alternative.176177**Merge the playbook** (once, here — not per scenario):178- Site: append each returned `playbook.site` line to179 `~/.claude/qa-playbook/<repo>.md` under its section, skipping near-duplicates.180- Scenario: splice each returned `playbook.scenario` block into the single181 `## QA Playbook` comment/file, applying the staleness rule from182 [references/playbook.md](references/playbook.md#learning): a `stale:*` replay183 with a corrected block replaces it (`Stale: 0`); a `stale:*` replay with no fix184 keeps the old block and bumps `Stale: 1`; a block already at `Stale: 1` that185 goes stale again is **deleted**. Write back with a single186 `comment update` / `issue edit` / file overwrite.187188## Report format (run body / qa-results-<ts>.md)189190```markdown191# QA Results — <plan title>192193**Plan:** <plan issue URL or tasks/<TASK>/qa-plan.md>194**Run:** <ts>195196## Status197- [ ] §1.1 — <title>198- [ ] §1.2 — <title>199200<!-- proof links are Linear assetUrls under --tracker linear, local paths otherwise -->201202## Findings203204### Finding #1 — <title> — <blocker|improvement|nit|plan-defect>205**Scenario:** §X.Y206**Prior:** <finding-ref @ prior-ts if recurring; omit for new findings>207**Repro:** <steps observed>208**Expected:** <from plan>209**Observed:** <what happened>210**Root cause:** <dev only — file:line; omit if not investigated>211**Evidence:** proof: <assetUrl under linear, else path>; debug: <path>, <path>212213## Notes214- <gotcha not worth a playbook entry>215```216217## Rules218219- **Plan is immutable.** Never edit the plan file or issue description. The220 `## QA Playbook` comment on the same issue is **mutable and not part of the221 plan** — it's expected to change every run. If the plan itself is wrong, log222 a `plan-defect` finding and tell the user to fix it separately.223- **The tracker is the record.** Under `linear`/`github`, the run sub-issue is224 canonical; under `local`, the file is. Never re-derive state from anything else.225- **Never edit a previous run's issue/file.** Each invocation owns one run226 record; within the current run, rewrite the whole body on each scenario227 (Step 3.4) rather than appending — the body is a live document, not a log.228- **Status lines are one line.** Detail goes in Findings.229- **Verify in the browser, root-cause in the source.** A pass rests only on230 something observed in the UI/email — never a DB/GraphQL peek. Once something231 fails, reading the source to explain why (with `file:line`) is encouraged.232- **Browser CDP is OK** (via `chrome-cdp --isolated`) for clicking faster than a233 human, not for bypassing audience constraints. No `evaluate()` shortcuts to234 read the DB, no GraphQL fetches, no `localStorage` poking beyond what a235 normal user does, unless the user explicitly allows it to verify a tricky state.236- **One scenario at a time, sequential sub-agents.** Each finishes and is237 logged before the next is spawned.238- **A failing scenario never contributes a `Verify` fast path** — see239 [references/playbook.md](references/playbook.md#learning). It can still240 contribute `Reach` (the precondition) plus site notes.241- **The run issue's lifecycle is Todo → In Progress → closed, every time.**242 Assigned to the runner at creation (Step 2). Never leave it "In Progress"243 after the run stops — Step 4 runs, and the issue closes, on any exit path.