About qa-gate
qa-gate is a skill for validating completed features against their specifications through public user-facing interfaces. It guides agents through a resumable QA run, evidence capture, typed blockers, and a final report verdict.
Key features
- 01Public-interface acceptance checks
- 02Resumable, row-by-row reporting
- 03Evidence capture and validation
- 04Typed blocker and verdict handling
- 05MCP tools and connected servers use
Use cases
- 01Validating completed feature requirements
- 02Resuming an interrupted QA run
- 03Recording blocked acceptance checks
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ marcioaltoe/ skills --skill qa-gateAfter installing, type/qa-gatein Claude Code to confirm it loaded.
Details
From the project READMEQA Gate
Validate the assembled feature against the promises in its spec by exercising the public interfaces a real user reaches. The Daemon runs this skill from the unique authored terminal qa Task named by _tasks.md frontmatter; it is part of the Task Graph, not a per-run request. That node depends on every non-QA leaf and therefore becomes runnable only after the graph it closes settles. This skill owns the complete gate: plan, execution, evidence, findings, report, and final verdict. Use the agent's tools and connected servers directly; do not load another QA skill or create a separate living QA tree.
Non-negotiables
- Real user seat. Enter through the same frontend, API, CLI, data workflow, or documented operational path as the intended actor. Internal helpers and code inspection can diagnose a failure, but cannot prove a user story passes.
- Proof beyond optimistic state. A pass requires the expected observable, an independent confirmation through a fresh load or another public read path, persistence across refresh/restart when relevant, and captured evidence.
- Resumable evidence. Create the dated report with every row
pendingbefore the first check. Update it after each row so an interrupted run resumes from disk instead of repeating completed work. - One honest verdict. The seeded report starts pending. Keep its seeded front matter as the report's only front matter while filling in that block and the Results rows. Every planned row ends as
pass,fail,blocked, orskipped; the report closes with zeropendingrows. A report that records no QA row never settles the gate, and a report whose front matter is empty or duplicated never settles the gate. - Typed blocked causes. Record a row that is unreachable for a proved
environmental cause as
blocked (environment: <cause>)and count it inrows_blocked_environment; record a row stopped by a finding asblocked (finding: <id> — waits on <named failure>)and count it inrows_blocked_finding; record a row that is unreachable for the reason in a matching, pre-run Spec declaration asblocked (declared: <criterion>)and count it inrows_blocked_declared. Keep the three causes separate: never fold one into another to make the report or verdict look cleaner. - Binary identities. The auditor fields are Daemon-owned: keep the seeded
auditing_binaryandauditor_stalenesslines unchanged. Record the--versionline of the binary that ran every public-CLI row asuser_flow_binary. In a Roundfix self-audit, build that binary from the audited Run Worktree withmake build, run public-CLI rows only with./bin/roundfix, and never use aroundfixfound on PATH.
Runnable evidence
Store runnable evidence under a non-compiled extension such as .go.txt,
never as a source file the repository gate compiles or formats. A later
pass's import skips compiled source and records each skipped path.
Evidence files are text or images
Keep files under qa/evidence/ as text or images. Screenshots use PNG, JPEG
or SVG. The Daemon accepts text (including SVG), PNG and JPEG by content,
regardless of the file name, and refuses to commit a non-image binary there.
Record a video, a trace archive, a PDF, a database or a build output as text
or keep it outside qa/evidence/; a refusal settles the QA Task failed
and leaves its report and evidence uncommitted in the Run Worktree.
Evidence carries no secret
record environment variable names, never their values, and record whether a
variable is set as <NAME> set or <NAME> not set. Never persist the output
of a command that prints the environment, including env, printenv, set,
export -p, declare -x, make -p and make --print-data-base. Never write
a secret-shaped sample into evidence. Record a Secret Match by path, line and
kind only, and never reload a shell profile to obtain a key. The Daemon
refuses to commit a QA Report whose files match a Secret Pattern.
QA settlement
The same outcome settles the authored qa Task and determines what the archive leaves:
| Outcome | Settles | Archives |
|---|---|---|
pass | Settles the QA Task as completed and makes the Spec archive-eligible when the report has no disallowed blocked rows. | The Archive Record, which names the QA Report and verdict; the Spec, its QA report and evidence stay in Git at the record's source_revision. |
qualifying declared partial | Settles the QA Task as completed when no row failed, was skipped or is finding-blocked, every declared-blocked row is covered by a matching ## Unreachable Acceptance declaration, and every environment-blocked row is the pre-PR Pull Request row, recorded as blocked (environment: no open Pull Request) with the Pull Request row named in its provenance, or an outside-evidence row the Run sandbox could not reach, recorded as blocked (environment: network denied: <host>) with the outside-evidence row named in its provenance. Neither row needs an Unreachable Acceptance declaration, and a partial whose only unmet rows are such rows qualifies. | The Archive Record, which names the QA Report and verdict and carries the declarations' satisfied-by record as unproven; the Spec, its QA report and evidence stay in Git at the record's source_revision. |
environment-blocked | Leaves the row blocked; the report can still settle as pass when equivalent evidence satisfies the environment policy. | Nothing by itself; a qualifying report can archive the Spec. |
failed | Leaves the QA Task unresolved and refuses archive unless an authorized override applies. | Nothing. |
missing | Leaves the QA Task unresolved and refuses archive unless an authorized override applies. | Nothing. |
override | Does not change the QA Task status or report verdict; settles archive as explicitly authorized despite failed or missing QA. | The Archive Record with qa_override, qa_override_approval, qa_override_reason, qa_override_qa_outcome, qa_override_qa_task_status when the QA Task is incomplete, and qa_override_revision; the QA Task and Reports stay unchanged in Git at the record's source_revision. |
Rows the Spec's authorization keeps offline
The qualifying declared partial above also accepts an outside-evidence row
the gate did not look up because the Spec's own authorization forbids that
lookup, recorded as blocked (environment: <subject> prohibited by Spec authorization)
with the outside-evidence row named in its provenance. The subject names what
the authorization forbids, in its words: network for a network lookup, or
another repository; it is 1 to 80 characters and holds no parenthesis. Like
the network-denied row, it never decides a qualifying partial and needs no
Unreachable Acceptance declaration, a row counts toward one exempt kind at
most, and settlement, roundfix qa-report accept and roundfix archive read
it through the same policy (ADR-0276, ADR-0335).
1. Resolve scope and preconditions
Resolve docs/specs/<slug>/, then read _tasks.md, _prd.md, every
task_NN.md, and prior files under qa/. Confirm that the manifest's qa:
field names the current type: qa Task and that this node is terminal and
depends on every non-QA leaf. A missing or mismatched authored node is a graph
defect; do not run the gate outside that node.
Surface Transcripts at the gate
For every Surface Transcript named by the TechSpec, add one QA matrix row. The
row reproduces the transcript's command through the built product and compares
standard output, standard error, and exit code using the TechSpec matching
rules: a line ... matches zero or more consecutive lines, text in angle
brackets matches one or more characters within the same line, and everything
else matches exactly. Compare standard output and standard error separately;
the exit code always matches exactly. Name the transcript in the row's
provenance and preserve the observed output as evidence.
-
A clean authoring check is a precondition of the gate, not a substitute for it. For every active, non-legacy Spec, run:
roundfix spec check <slug> --strictThe gate keeps this non-probing form because the probe asks whether a command already passes before its work exists, while the gate runs after every implementation Task is complete. The probe leaves completed Tasks unprobed and reports their commands
not probed, so it has nothing left to ask of them. The three authoring skills use--run-verificationwhile the work is still being authored, when that question is answerable.Stop before building the matrix when the command fails, and write the refusal before you stop. A gate that stops here measured no requirement, so its whole report is the refusal:
--- verdict: fail build: "<audited-commit>" rows_blocked_precondition: 1 auditing_binary: "<version-and-build-identity>" auditor_staleness: "<state>: <reason>" rows_blocked_environment: 0 rows_blocked_finding: 0 rows_blocked_declared: 0 precondition_check: "roundfix spec check <slug> --strict" precondition_reason: "<every refusing code and the sentence beside it>" --- # QA Report ## Results | # | Status | Provenance | | - | --- | --- | | 0 | blocked | precondition | ## Precondition refusal - check: roundfix spec check <slug> --strict - reason: <every refusing code and the sentence beside it>Row
0is the entire matrix:blockedbecause nothing ran, provenancepreconditionbecause the stop is a refusal and not a measurement. Name every refusingSC-*code inprecondition_reason, code first and its sentence after, joined with;and collapsed onto one line, so a maintainer reading the report knows whether to fix the Spec or the tree. Justify the row in prose as a list, never as a second table under## Results. An empty Results table is not an option: it refuses every later run on this report instead of on the Spec, and the prescribed repair — materialize every planned row — is one a run that never built a matrix cannot perform.A gate that stopped here writes nothing else: no matrix, and no rows for the checks that never executed.
A refusal records
buildas a full report does: the commit the gate audited, quoted, so a reader can tell this refusal from one an earlier Run left on the branch. A refusal the Daemon wrote already carries it; keep its value.When the command passes, record the clean result in the report's scope and environment, but do not create QA matrix rows that repeat the authoring rules in the table below. A completed or archived legacy Spec is exempt from forced constraint backfill; keep every legacy artifact byte-identical and retain any governance row that has no clean authoring result to replace it. Absence of a Project Constraints section by itself is not proof of legacy status.
| Authoring rule removed from the QA matrix | spec check equivalent |
|---|---|
| The authored Task Graph parses, maps PRD promises to Tasks, and resolves declared repository paths. | The Task Graph load required by SC-COVERAGE-UNTASKED, plus SC-COVERAGE-UNTASKED and SC-REF-UNRESOLVED |
| PRD promises map into the TechSpec. | SC-COVERAGE-UNMAPPED |
| Every required Project Constraint row exists. | SC-CONSTRAINT-MISSING |
| A declared Project Constraint applicability has a reason. | SC-CONSTRAINT-UNREASONED |
| A Project Constraint row cites an operative source. | SC-CONSTRAINT-SOURCE |
| A cited tooling authorization names the Spec. | SC-TOOLING-UNAUTHORIZED |
| Applicable tooling authority declares bounded files. | SC-TOOLING-UNBOUNDED |
| A tooling authorization record states its grant in readable fields. | SC-TOOLING-UNTYPED |
| Every pending non-QA Task declaration of a Governed Path appears in the authorization record and each present Tooling authority row. | SC-TOOLING-UNDECLARED |
| Every pending non-QA Task that names a CLI surface also names a guide in that Task or its transitive dependencies. | SC-CLI-UNDOCUMENTED |
| Active ADR obligations are listed, related decisions are accounted for, and attributed claims match the cited record. | SC-ADR-UNLISTED, SC-ADR-RELATED, and SC-CITATION-UNSUPPORTED |
| Task requirements do not contradict each other, rehearsals are declared, and Verification can distinguish Task work from no work. | SC-REQUIREMENT-CONTRADICTORY, SC-REHEARSAL-UNDECLARED, and SC-VERIFY-WORK-INDEPENDENT |
| Emitted vocabulary is documented through the TechSpec's Vocabulary Contract. | SC-VOCABULARY-UNDOCUMENTED |
| Repository loop declarations and Finding, Rollup, archive, and promoted Backlog lifecycles agree. | SC-LOOP-ORDER-DIVERGENT, SC-FINDING-LIFECYCLE, SC-ROLLUP-MEMBER, SC-ARCHIVE-LICENSE, and SC-BACKLOG-UNMOVED |
A named detector in the checker's skipped list did not run. It is not an equivalent, so retain the corresponding QA row. Do not extend this mapping by analogy: applicability that the checker did not parse, missing tooling authorization, outside evidence, Non-Goals, the report contract, current Task status, and live control or chronology behavior stay in the gate unless a named checker rule decides them.
- Keep the commit-dependent tooling audit as matrix rows. Identify every Task
that actually creates, edits, renames, moves, or deletes repository-tooling
configuration, scripts, ignore files, plugin declarations, or version pins.
The mechanical stage audits every Task commit between the delivery base and
the audited head, and its authorization audit table names each audited
commit. Audit by command only a Task commit in that range that the table does
not list and that changes a repository-tooling path. When the report's
mechanical skips name
Task commits of earlier Runs, audit every Task commit by command. Execute each command row against the actual paths from the Daemon-owned Task commit and any current worktree delta, then resolve the authorization, prerequisite-fix, and consequent-fix commits in chronological ancestry. Usegit diff-tree --no-commit-id --name-only -r <commit>for every committed change rather than trusting a reported file list. - Report every post-commit authorization-shape problem together in the same failed command row: missing, late, or untraceable authorization; authorization or a prerequisite fix folded into the Task commit; a consequent fix folded into or ordered before the change that caused it; a claimed derived pin without reproducible evidence from the sanctioned regeneration command; and every path outside the exact bounded list plus the assigned Task file. Do not stop at the first problem and defer the rest to another QA rerun. Once the matrix exists, a finding blocks only the rows that depend on it.
- QA writes only its report and evidence. It never changes Task status or Task Graph dependencies.
- Require every dependency of the authored
qaTask to becompleted. If any dependency remains incomplete, stop and list it. The Daemon owns the current gate Task's status and settles it from the final verdict. Run a partial gate only when the authored QA Task explicitly limits its scope; mark its scope and final verdictpartial. The mechanical stage's withholding of the Agent Session before a matrix exists is unchanged: a blocking machine fact still stops the gate before it builds a matrix. A declared matrix is a full gate and can reachpass; declaring a matrix does not make the run partial.partialstays reserved for aqaTask that explicitly limits its scope or for a planned row the gate did not run. - Identify the PRD's declared surfaces, user stories, core features, acceptance criteria, user-experience states, and Non-Goals.
- Read every entry under the PRD's
## Unreachable Acceptancesection before the run. Treat each declaration as an author's claim to test, not as proof and not as permission to omit the row. The gate may match a blocked row only to one of these pre-run declarations; it must never declare unreachability itself or infer a declaration from a failed attempt. - Read each task's
## Result. Credit a task-level criterion only when it points to named, reproducible evidence and the static checks it depends on pass. An unrelated static failure does not invalidate that evidence. Spend live QA effort on assembled user journeys, cross-task seams, persistence, failure behavior, and scope creep. - On a rerun, keep every row the Daemon seeded as
carried (established by: …; head: …): it counts as passed and is not executed again. Execute every other row, starting with the rows the seeded## Row carry-forwardsection lists asre-run.
The scope is complete when coverage is complete and closed.
When a Requirement audits a Task's changed files against its declarations, a
path the Task file lists under ## Recorded paths counts as declared and is
named in that row. A Governed Path still needs its authorization.
2. Build the QA matrix and open the report
Create a collision-safe report path before execution:
docs/specs/<slug>/qa/qa-report-YYYY-MM-DD.md for the day's first report, then
qa-report-YYYY-MM-DD-NN.md with a suffix one above the highest existing suffix
of that date for same-day reruns; an earlier gap is never filled. Numeric
same-day suffixes are the only allowed suffixes; never use a scope or build
slug. Resume an existing status: in-progress report only when it is for the
same build; otherwise create the next numeric sibling and preserve older
reports as history.
Read the Pull Request fact in the Roundfix QA prompt before planning Pull Request journeys:
The Pull Request row uses the equivalent-evidence path by default. Under ADR-0088, the authored gate runs before an Open Pull Request exists, so Specs must not pay to rediscover that arrangement. Keep the row in the matrix and record equivalent observed or supervised evidence for every control the Pull Request would have exercised, alongside the row's environment cause:
- approval — who accepted the change, or that no acceptance was obtained;
- checks and status — the repository Verification and any required check, run locally, with its result;
- unresolved review threads — that none stands, or which do;
- Merge-Ready acceptance — the Review Source evidence for the expected head, or that none exists yet;
- review-artifact ancestry — the head the evidence belongs to, and that it is the head the row is claiming for.
Never silently pass or omit the row. A control with no equivalent evidence keeps the row blocked; ADR-0080 refuses to credit a journey without evidence, and an enumerated control left unanswered is exactly that.
- When the fact names an Open Pull Request, those journeys are runnable.
Observe that Pull Request read-only through the existing
ghand Review Source boundaries. Check approval, checks and status evidence, unresolved review threads, Merge-Ready acceptance, and the Daemon's separate review-artifact commit ancestry where the Spec requires them. Never mutate the Pull Request, resolve threads, commit, or push. - When the fact says no Pull Request is open, record every Pull Request journey
as
blocked (environment: no open Pull Request)and count it inrows_blocked_environment. The pre-PR Pull Request row never decides a qualifying partial and needs no Unreachable Acceptance declaration; name the Pull Request row in its provenance. The Pull Request row's provenance item may carry a note afterPull Request row. When the fact says the Pull Request could not be resolved, the absence is unproven: record the cause asblocked (environment: Pull Request unresolved)and never write it up as a confirmed absence. Do not try to resolve a Pull Request from the Run Worktree branch: that per-Run branch is never pushed and has no Pull Request of its own.
Coverage sources
Each numbered qa Task Requirement that starts with MUST verify or MUST run is a declared verification Requirement. Every other Requirement constrains how the gate runs.
When a qa Task has at least one declared verification Requirement, those
Requirements declare the matrix's coverage sources. A declared Requirement that verifies every Acceptance Criterion of named Tasks makes each of those criteria its own source.
Every matrix, declared or default, covers these non-waivable sources:
- the outside-evidence row;
- the Pull Request row, on its equivalent-evidence path;
- the repository Verification;
- each declaration under the PRD's
## Unreachable Acceptancesection; - the frontend sweep, when
frontendis declared.
A qa Task without a declared verification Requirement gets the bounded default.
Its coverage sources are:
- the PRD user stories and Goals;
- each non-QA Task's Acceptance Criteria;
- each declared intentional break;
- the non-waivable sources listed above.
A Non-Goal becomes a source only when it names an observable the Spec could ship. Its row reads only the paths the Spec changed and the surfaces the Non-Goal names.
Coverage and provenance
Coverage is complete, closed and named:
- Every row names, in its provenance, each coverage source it covers.
- Every coverage source appears in at least one row's provenance.
- no row's provenance names a source outside the coverage sources.
- Rows may group several sources that one observation settles.
- No row's result is computed from other rows.
- No row re-checks a Mechanical Refusal Code.
For each row record the sources it covers in its provenance, the actor, entry
point, surface, steps, expected observable, independent confirmation,
persistence check, evidence path, and status pending. Order rows by user
impact and blast radius. Behavior probes for high-risk journeys stay in the
bounded default only. For each high-risk journey in that default, select 2-5
relevant probes: double submit, refresh or back navigation mid-action,
deep-link/reopen, invalid or out-of-order input, session expiry,
offline/reconnect, concurrent tabs, or locale/accessibility changes. Choose
probes that fit the feature; unrelated probes create noise.
Use the qa Task's own identifiers in provenance, such as "Requirement 3",
"Task 01 criterion 2" and "repository Verification".
Row input declaration
Every row the Agent executes declares a non-empty, typed inputs: list in its
detailed evidence block. The row's inputs are fixed when it is planned as
pending, bounded to what the row reads. Place the block under a ### <row-id>
heading whose row identifier matches the row's # cell in the Results table,
and use one fenced yaml block per row; a block under any other heading is
ignored and the row is never carried. Each entry has a kind and a ref:
inputs:
- kind: repository_path
ref: <repository-relative path or glob>
Use one entry for every input the row's truth depends on:
| Kind | Use when |
|---|---|
repository_path | The evidence depends on content at a literal path or glob in the current repository. |
external_repository | The evidence depends on content or state in another repository. The row is never carriable. |
live_service | The evidence depends on state observed from a live service. The row is never carriable. |
elapsed_time | The evidence depends on elapsed time, age, duration, or a time window. The row is never carriable. |
commit_range | The row reads Task commits, their authorization, or their changed-file scope. The row is never carriable. |
Declare conservatively: include every source the row reads, including every
source a built binary compiles from and the module manifest. Declare every
applicable kind. A mixed list containing any non-repository input is never
carriable. A row with no inputs: declaration or an empty list is invalid for
execution and must be re-observed, so carry-forward remains opt-in and
fail-closed.
A report without inputs: behaves exactly as it does today; existing rows,
counts, statuses, verdict rules, and report naming do not change. When a future
round closes, a pass row whose declaration carries only repository inputs,
whose ancestry is proven, and whose evidence is byte-identical at the later
head is materialized as carried (established by: <report>; head: <sha>) and
is not re-observed.
A row whose inputs grow after it ran is not carriable. Do not add inputs after execution to make carry-forward eligible.
A gate reopened over a report written before rows declared inputs re-executes
every row that report recorded without inputs:; such a row is never
carriable. A row executed again in a new pass declares its inputs for that pass
when the pass plans it as pending. That is a new observation with its own
inputs, not inputs added after execution; never add inputs: to the earlier
report's rows.
The Agent never writes evidence_snapshots; the Daemon records them after the
turn. Rows naming the repository Verification or the Pull Request row are
always observed, and a row declaring commit_range is always observed.
One row is the Spec's outside-evidence row: the acceptance row that rests on
evidence originating outside the Spec's own artifacts — a repository the Spec
did not build, a measurement it did not design, or published literature. Record
in that row where its evidence came from, named precisely enough for a later
reader to reach the same source, so the result cannot be read as a rehearsal of
the Spec's own premise. When the Run sandbox denies network access to the
lookup, record the row as blocked (environment: network denied: <host>), naming
the host the lookup tried, and keep an outside-evidence row item in its
provenance. This records that the source was not reached and never decides a
qualifying partial. When the Spec's own _authorization.md forbids that
lookup, do not attempt it: record the row as
blocked (environment: <subject> prohibited by Spec authorization), where
<subject> names what the authorization forbids in its words, such as
network for a network lookup, is 1 to 80 characters, and holds no
parenthesis. Keep an outside-evidence row item in its provenance and name in
the report the authorization text that forbids it. It never decides a
qualifying partial either (ADR-0276, ADR-0335). A source the row can check
offline, such as a passage the
PRD quotes or a document under docs/references/, is checked, never
blocked. Any other blocked outside-evidence row still blocks Pull
Request preparation. Count every blocked outside-evidence row in
rows_blocked_environment; never drop the row, and never satisfy it with
evidence the Spec authored. Task authoring never stalls on it — decomposition
records the blocked row and proceeds — so the obligation lands
here, at the gate, where the Spec is asked to account for it. See ADR-0104.
The plan is complete when every coverage source appears in at least one row's provenance, no row's provenance names a source outside the coverage sources, every chosen default probe has a reason, and the report contains the full pending matrix.
3. Run static gates first
Glossary at the gate
The strict precondition reports every Glossary Gap. The report records, for each term the Glossary Declaration adds or changes, whether the glossary defines it after the work.
In a Daemon-assigned gate, the Daemon already ran the repository Verification and gives its result in the prompt; record that result and do not run the repository Verification again. In a standalone gate, run the repository's selected Verification. If the selected Verification cannot run at all, record the verification gate as blocked. A formatting, test, or build check that runs and fails is a fail, not a block — classify it with the code-caused and environment-caused distinction below before recording anything.
A timeout or intermittent failure of the repository Verification is recorded as a failure, never an environment block. A claimed contention, a non-reproducible run, or the same failure on the unchanged delivery target is not a proved environmental cause. Only a cause that stops the command from running at all is environmental.
When a qa Task names an analyzer beyond the repository Verification, run it
over the changed packages. A diagnostic identical on the delivery target is
recorded as observed, naming the Spec that owns it, and does not fail the Spec.
A repository Verification failure stays blocking whether or not it is
pre-existing.
When a command fails, diagnose its source before continuing:
- Code-caused: Once the matrix exists, a finding blocks only the rows that depend on it. Record a finding and trace the failed check to the matrix
rows whose entry point, observable, or evidence depends on what failed.
Block only those implicated rows as
blocked (finding: <id> — waits on <named failing check>); continue every unimplicated row whose result remains valid. A failure may block the whole matrix only when every row genuinely depends on it, such as a build failure that leaves no runnable artifact; record that dependency on each row instead of asserting it once for the matrix. - Environment-caused: for checks other than the repository Verification,
prove the constraint with the error or an unchanged-base reproduction, record
affected rows as
blocked (environment: <cause>), continue checks that remain valid, and apply the typed blocked-cause verdict rule in section 6.
The static gate is complete when it passes or every failure is classified, every implicated row names the check it waits on, every environment-caused block has concrete proof and an explicit unblocking requirement, and the remaining rows are identified as safe to run.
4. Exercise real flows
Start the application through its production-like entry point. Record the build or commit, commands, URL or binary, data setup, credentials profile, and every parity deviation.
For each matrix row, repeat this loop:
- Enter as the named actor through a real entry point.
- Act through the public surface and observe the product's user-facing response.
- Confirm the result through a fresh load, restart, deep link, second public endpoint/command, or another user-visible read path.
- Capture evidence at the goal state and every divergence. Store screenshots and focused text or image artifacts under
qa/evidence/<YYYY-MM-DD>-<run-slug>/; keep exact commands and concise outputs in the report. - Update the row immediately. A stall, dead control, infinite spinner, or vanished state is a finding, not a reason to work around the product.
When a row cannot reach its expected observable, compare the row and the
proved cause with the pre-run declarations. Use blocked (declared: <criterion>) only when the row matches a declared criterion and the observed
constraint matches that declaration's reason. A different circumstance stays
environment-blocked or finding-blocked under its actual cause. A blocked row
with no matching declaration keeps its existing cause and remains blocking
under the existing verdict rules.
If the gate can reach a declared row, run it normally and report the
declaration as a wrongly-declared-row finding. Do not accept, ignore, or remove
the declaration merely because the journey passes: the reachable journey is
evidence that the Spec's claim was wrong, and that finding prevents pass.
Surface protocol
| Surface | Exercise | Minimum evidence |
|---|---|---|
frontend | Drive a real browser through each actor's journey; re-read interactive state after navigation or DOM changes. | Key-state and failure screenshots, independent confirmation after refresh/deep-link, console errors/warnings, failed network requests. |
backend | Send real requests to the running documented API, including named failure modes. | Exact request, status, payload, and a public read that confirms side effects. |
cli | Run the installed or built command exactly as an operator would. | Command, exit code, output, and a second command/read confirming state. |
data | Apply the real migration or sync to a scratch database and rerun it. | Shape/row checks, preserved distinctions, side effects, and idempotency evidence. |
infra | Apply or inspect through a safe scratch/dry-run path that matches deployment. | Command output, resulting configuration/state, and parity gaps. |
docs | Follow the documentation as its intended reader without undocumented knowledge. | Commands attempted, result, and any missing or misleading step. |
Frontend sweep
When frontend is declared, cover all of these in the matrix:
- Viewport and input: use named breakpoints or mobile 375×812, tablet 768×1024, and desktop 1440×900. Complete the primary flow at each size; check clipping, overlap, horizontal scrolling, keyboard-only operation, visible focus, and 200% zoom on key screens.
- States and recovery: exercise the PRD's empty, loading, success, validation, and error states. Async actions show prompt feedback, failures preserve input, and recovery offers a concrete next step.
- Accessibility and language: inputs and controls have accessible names, heading/focus order is coherent, color is not the only signal, images have appropriate alternatives, and product copy uses the spec's vocabulary.
- Runtime health: exercised flows leave no unexplained console errors/warnings or failed requests. Run a focused accessibility or Lighthouse audit when available; disclose when unavailable.
For a TUI, map the same sweep to small/large/degenerate terminal sizes, keyboard flows, focus/follow behavior, color-independent readability, aligned output, and clean interrupt handling.
Browser driver inside a sandbox
When a daemon-backed browser tool such as agent-browser cannot start inside the
Run sandbox, the gate drives the browser through the chrome-devtools MCP server
when the session exposes it, because that server runs outside the Agent
sandbox. Record which driver ran each browser row. Without either driver the
row is blocked (environment: <error>), never retried in a loop.
5. Classify results and findings
Assign each row exactly one terminal status:
pass: the expected observable, independent confirmation, persistence check, and evidence all exist;fail: observed product behavior contradicts the spec or a Non-Goal shipped;blocked (environment: <cause>): the gate environment makes the journey unreachable; include the proof, exact unblocking action, and equivalent observed or supervised evidence when available;blocked (finding: <id> — waits on <named failure>): a finding prevents the journey from reaching the expected observable; name the failed check or behavior the row waits on, then link the finding and affected evidence;blocked (declared: <criterion>): the row is unreachable for the reason in the matching pre-run Spec declaration; link the declaration, the proof that the gate could not reach it, and the named human action that would satisfy it;skipped: allowed only for an explicitly partial gate or a risk-based cut; state why and what remains unverified.
Classify each finding by user impact:
Blocks-Completion: the actor cannot finish a value-delivering journey;Data-Loss: user data is destroyed, corrupted, or silently discarded;Trust-Damage: contradictory, inaccessible, or unrecoverable behavior undermines confidence;Friction: the journey completes with avoidable confusion, delay, or repetition;Cosmetic: presentation differs without affecting completion or trust.
For every failure, record the actor, journey step, expected and actual behavior, reproduction from the public entry point, evidence, impact tier, and affected rows. Search previous reports before naming a finding as new; mark a repeated or regressed symptom instead of splitting its history.
Finish the planned sweep before fixing code. A changed build invalidates remaining comparisons. After repairs, start a new run or clearly version the build, rerun the static gate, the failed flow, and one adjacent canary.
6. Close the report
Use this structure:
---
spec: <slug>
date: YYYY-MM-DD
build: <audited-commit-or-artifact>
auditing_binary: "<version-and-build-identity>"
auditor_staleness: "<state>: <reason>" # state is current|stale|unknown; the reason names the signal that answered, such as commit ancestry or the declared tree version
user_flow_binary: "<the --version line of the binary that ran public-CLI rows>"
status: in-progress # in-progress | closed
verdict: pending # pending | pass | fail | partial
rows_blocked_environment: 0
rows_blocked_finding: 0
rows_blocked_declared: 0
surfaces: [frontend, backend]
---
# QA report — <feature>
## Scope and environment
<!-- Full or partial scope, app start commands, build, tools, data, actors, parity gaps. -->
## Static gate
<!-- Exact commands and results. -->
## Results
| # | Status | Provenance |
| - | --- | --- |
For each row, put the story or criterion, actor, surface, steps, and evidence
in a block below the table.
## Findings
<!-- One block per finding: impact, expected/actual, reproduction, evidence, affected rows. -->
## Blocked and skipped
<!-- Proof, unblocking action, and uncovered scope. Omit only when empty. -->
## Coverage
<!-- Stories and criteria passed/total; rows_blocked_environment, rows_blocked_finding, and rows_blocked_declared; probes and frontend sweeps attempted; Non-Goals checked. -->
## Final verdict
<!-- Written last: one actionable sentence, plus counts by status and impact tier. -->
Close with status: closed and apply the verdict mechanically:
failwhen any row failed or the gate found a wrongly declared row;partialwhen none failed and no declaration was wrong, but any row is declared-blocked, finding-blocked, or skipped, the authored QA Task defines a partial run, or an environment-blocked row lacks equivalent observed or supervised evidence;passwhen every runnable row passed, every environment-blocked row records its cause and equivalent observed or supervised evidence, no row is declared-blocked, finding-blocked, or skipped, no declaration was wrong, and all evidence paths resolve. A nonzerorows_blocked_environmentcount does not by itself preventpass; a nonzerorows_blocked_findingorrows_blocked_declaredcount does.
Set rows_blocked_environment, rows_blocked_finding, and
rows_blocked_declared to the exact number of rows with each blocked cause.
Keep all three keys in every closed report, including when any count is zero.
A report that carries the precondition refusal row from section 1 carries
rows_blocked_precondition as well, set to the exact number of blocked rows
whose provenance is precondition, plus precondition_check and
precondition_reason. A gate that reached its matrix writes none of these three
keys: the refusal is an added shape, not a fourth count every report owes.
The gate permits Pull Request preparation on pass, or on a qualifying declared partial as the QA settlement table defines. On other outcomes, state what must change or be verified before rerunning. In a daemon-assigned Roundfix QA step, write the report but never commit or push; the daemon owns the QA report commit. Daemon-assigned steps may also run sandboxed: when an operation outside the workspace fails with a permission error (writes to $HOME, nested tool state), classify it immediately as environment-caused, mark the affected row blocked (environment: <error>), and move on — never retry-loop a sandbox denial — noting in the environment record which checks need a full-access session. For an outside-evidence lookup denied by the Run sandbox's network policy, write blocked (environment: network denied: <host>) with the attempted host and the outside-evidence row provenance item; record that the source was not reached, and apply the one-rule verdict described above: a Pull Request row with equivalent evidence for every control still allows pass, and without it the report closes partial, which qualifies when no other unmet row remains.
Decision examples
- A UI says "Saved", but the record disappears after refresh:
fail,Data-Loss; the optimistic message is not proof. - The full suite passes, but real OAuth needs a human-controlled account and no equivalent supervised evidence exists: affected rows
blocked (environment: human-controlled account), overallpartial, with exact human verification steps. - The Spec declares that publishing a real release is unreachable, and the
gate proves that its hermetic environment cannot perform that irreversible
action: the matching row is
blocked (declared: publish the real release),rows_blocked_declaredis incremented, and the verdict ispartial. - The Spec declares a journey unreachable, but the gate can exercise it: run
the journey, record a wrongly-declared-row finding, and return
faileven if the journey's product behavior passes. - A governance check fails but does not affect the runnable application's behavior: block only the governance rows that wait on that named check and continue the functional journeys.
- The outside-evidence row names repositories this environment does not hold:
record
blocked (environment: network denied: <host>)with the attempted host and anoutside-evidence rowprovenance item when the Run sandbox denied network access; this records that the source was not reached and never decides a qualifying partial. When the Spec's own authorization forbids that lookup, recordblocked (environment: <subject> prohibited by Spec authorization);<subject>names what the authorization forbids in its words, such asnetworkfor a network lookup, is 1 to 80 characters, and holds no parenthesis. Keep the same provenance item and name the authorization text in the report; it never decides a qualifying partial either. Any other blocked outside-evidence row still blocks Pull Request preparation. Count it inrows_blocked_environment— never substitute a rehearsal the Spec authored. - The prompt names an Open Pull Request and read-only observation proves approval, Merge-Ready acceptance, and review-artifact ancestry: pass those Pull Request journeys without commit, push, or Pull Request mutation authority.
- A task Result names a passing unit test, while the assembled browser journey also persists after refresh with screenshots: credit the task criterion and pass the user-story row from live evidence.
Outcome for the Archive Record
Write one ## Outcome paragraph in the QA Report describing the result that
the Archive Record will carry. In a Delivery Queue Run, use
roundfix archive <slug> --plan --offline to list the cut and its candidates.
A QA Task never contacts the judge. Review candidates for reusable knowledge
and copy confirmed knowledge to docs/references/ in the QA commit with
--promote <path> when archiving.
Anti-patterns
- Declaring pass from tests, source inspection, route rendering, or optimistic UI alone.
- Writing the report only after the run, losing resumability and uncovered rows.
- Skipping failure states, behavior probes, or responsive checks because the happy path passed.
- Capturing hundreds of screenshots instead of goal states and divergences.
- Treating an environment denial as a code failure, or crediting it without a recorded cause and equivalent evidence.
- Softening a failure into a note, leaving rows pending, or claiming coverage without a matrix row.
- Fixing during the sweep and continuing as though every row ran against one build.