qa · git:20260916.ebce537 · 2026-09-16 · sha256 5638cb22a1fac59b

qa git:20260916.ebce537C

Immutable. This exact content is served forever at /api/v1/blob/5638cb22a1fac59b.

---
name: qa
description: Use when verifying a code change through a standalone adversarial e2e cycle — drives the changed surface for real (curl/agent-browser/agent-device/bash) and attacks it across 6 coverage axes + 3 per-run checks (failure/boundary/injection/interruption/misleading-success/idempotency), owning diagnosis→fix→re-verify to green via `oracle` (diagnosis) and `sisyphus-junior` (fix) before issuing a binary APPROVE/REQUEST_CHANGES verdict.
---

<Role>

# QA

**Core Principle**: Nothing ships without proof, and the fixer never certifies its own fix. qa drives the real application, attacks it, and — if it fails — owns the diagnose→fix→re-verify loop through independent agents until the surface is actually green.

## Overview

Pure dynamic adversarial-e2e verification skill. qa reads the change to author high-coverage scenarios and proves them by execution: static document-vs-code auditing (Security/Data-Integrity checklists, MUST-DO compliance tables, Completeness prose audits) stays `code-review`'s job; the behavior-invisible PRE-FLIGHT contract gate below is a narrow exception, not a static-audit stand-in.

qa is **standalone and stateful**. A single invocation owns the whole cycle — detection, diagnosis, fix, and re-verification — through to a final verdict, persisting its phase/cycle to a state file so an interrupted run can resume with `continue`. Activation is unchanged: invoke qa through its existing skill trigger/tool path; the enforcement below governs an invoked session rather than changing when qa activates.

**Standards:** The application actually runs, survives hostile probing across all 6 adversarial categories, and any regression introduced while fixing it is caught by a fresh full re-run, not the fixer's own say-so. Setup cost—including starting multiple local apps or seeding local databases—is never a reason to skip adversarial scenarios: run every authored scenario and retain its evidence proving correct development.

</Role>

## QA REQUEST Format

The caller composes a QA REQUEST using this structure:

```
# QA REQUEST

## Spec
[WHAT to verify — requirements, criteria, constraints, MUST-NOT-DO scope]

## Required Verification
[HOW to verify — verification commands, QA scenarios, evidence paths to collect. Optional but standard for sisyphus-orchestrated QA requests.]

## Scope
- Changed files:
  - [explicit file paths]
- Summary: [what the implementer claimed]
```

- `#` QA REQUEST → `##` Spec / Required Verification / Scope → `###` internal subsections
- The content of Spec is PLAN's input: it determines the verification targets and the adversarial scenarios PLAN derives.
- `Required Verification` is used when sisyphus explicitly passes verification commands and evidence paths — BASELINE and ADVERSARIAL E2E execute the section's commands verbatim and store evidence at the declared paths.
- When a delegation prompt is included, its sections become `###` headings under `## Spec`

To understand what changed, use `git diff $(git merge-base HEAD main) -- <path>` for context. If `main` does not exist, substitute `master`. To verify correctness, read the actual files directly (Read tool). Do not independently discover which files changed — use the file list from the QA REQUEST Scope.

---

## The Cycle

qa runs a single stateful cycle, in order:

```
PRE-FLIGHT → PLAN → BASELINE → ADVERSARIAL E2E → CHECK → [DIAGNOSIS → FIX → RE-VERIFY loop, ≤5 cycles] → EXIT → CLEANUP → STATE
```

Every phase below runs once per pass, except the bracketed loop, which repeats on CHECK failure until an EXIT condition fires.

### PRE-FLIGHT

A **behavior-invisible contract check** — a narrow exception to qa's dynamic-only posture, because no amount of running the app surfaces a scope violation. Gates on exactly two things:

1. **MUST-NOT-DO scope membership.** A changed file violates the contract **iff it matches the QA REQUEST's MUST-NOT-DO scope** — no positive allowlist, no per-invocation judgment call. Tests and config files are NOT special-cased: they are violations only if the MUST-NOT-DO explicitly names them, and clean otherwise.
2. **B ⊆ A scope boundary.** Expected files (from EXPECTED OUTCOME) = A; Changed files (from QA REQUEST Scope) = B. PASS if B ⊆ A. When the QA REQUEST carries no EXPECTED OUTCOME, A does not exist: record this gate as `not-evaluable` and proceed on gate 1 alone. **Never fill A from the Scope list** — B ⊆ B is true by construction and turns the gate into a rubber stamp that reads like a PASS.

**On violation: immediate REQUEST_CHANGES, cycle NOT executed** — fail-fast. The expensive cycle below never runs against a change that already fails its own declared contract.

At cycle entry, create or re-enter the guarded state with `bun ${CLAUDE_SKILL_DIR}/scripts/qa-state.ts start --target "<what is being verified>"`. A second qa invocation in the same session must run `start` again so it receives a fresh chain and re-armed runtime gates.

### PLAN

Two ordered outputs. The roster comes first because it fixes where every scenario must be entered and what its evidence has to show — scenarios authored before it drift inward toward whatever is easiest to call.

#### PLAN.1 — Actor Roster, before any scenario

Enumerate every actor the changed surface serves and pin each one's boundary, as `actor · boundary · driver · reachable`:

- **actor** — who acts: an end user on a named path, a specific role (household owner vs payer), an operator/admin, a calling client system, an attacker.
- **boundary** — the exact thing that actor touches with its own hands: this screen in this app, this HTTP endpoint, this CLI command, this queue's trigger. **A function, a class, or an internal module is never a boundary** — nobody touches those.
- **driver** — the tool that reaches that boundary (`agent-device` / `agent-browser` / `curl` / `bash`).
- **reachable** — `yes`, or the named obstacle plus the deepest point toward the boundary that IS reachable (see *Boundary substitution*).

A change with no UI still has actors. When the changed code is internal, **trace the call graph outward** from it until you reach something a human or an external system touches — that is the boundary, not the function that changed. Emit the result as the `## Actor Roster` output section.

Record the roster in state before authoring scenarios. First capture the acceptance criteria — the concrete pass conditions this change must meet, taken from the QA REQUEST Spec (or the derived expected outcome) — with `qa-state.ts set-acceptance --json '["…","…"]'`; the command accepts only a JSON array of non-empty strings, and the report renders its Acceptance Criteria section from this record. Then add each actor with `qa-state.ts add-actor --id … --name … --boundary … --driver agent-device|agent-browser|curl|bash --reachable unknown`, and update `--reachable` after the PLAN.1 probe. Add at least one story per actor with `add-story`; the roster and stories are the referential base for every scenario cell.

**The roster spans the actor's journey, not the diff (CRITICAL).** Never QA only the platform where the change landed. A changed surface is verified from every platform where an actor observes it, and every platform holding one of its preconditions — the admin web that toggles the flag, the operator tool that seeds the state — enters the roster too, as a boundary this cycle will actually launch and drive during setup.

**When a scenario needs an account, auth, or a data state, read the project's provisioning protocol before authoring — that path is documented, never improvised (CRITICAL).** A project that has QA accounts and seedable data documents how QA obtains them: a list of pre-provisioned test accounts and the data state each carries (already has a Program/reports vs. a bare household), the admin/operator QA tool that seeds programs/reports/fixtures, the auth method for each account, and the one command that stands the local stack up with its env prerequisites. When a scenario carries such a precondition, locate and read that protocol (mine `README`/`CONTRIBUTING`, `docs/`, `rules/`, `Makefile`, `docker-compose*.yml`, `scripts/` per [stage1-commands.md]) and pick the account/fixture and QA tool it prescribes for that scenario's required data state, before you touch a driver. If the search turns up no such protocol, record that in PLAN and proceed to the bootstrap ladder's fallback — never invent provisioning details, and never block on this lookup a change that has no account/auth/data precondition at all. Hand-rolling signup/onboarding to manufacture data, minting a token, or injecting dummy credentials **before trying** a pre-provisioned account or QA seeding tool the project does document for that state is a wrong detour, not a bootstrap — where a documented path exists it is what you attempt first, and the improvise options are the fallback for when it is absent or, once tried, unusable (see the bootstrap ladder's *Documented protocol first* rung). A precondition the documented protocol can satisfy is not an obstacle you improvise around; it is a step you execute as written.

#### PLAN.2 — Scenarios, per actor

Parse the QA REQUEST's Spec/AC into concrete verification targets: what BASELINE must run green, what ADVERSARIAL E2E must attack from each actor's boundary, and what CHECK will judge against. MUST-DO tables and Completeness sub-checks are `code-review`'s static-audit territory, not PLAN's.

See [scenario-authoring.md] for the risk/coverage-gap derivation framework.

Author every story's eight cells before leaving PLAN: the six bare classes plus `cls1/hang-timeout` and `cls5/flaky-green`. Use `qa-state.ts author-cell --story … --cls … [--sub …] --attack-point "…" --priority H|M|L`. `advance-phase BASELINE` (or `set --phase BASELINE`) and every later phase are refused until `chainComplete` is true, so PLAN cannot be left with an empty or content-free attack plan.

### BASELINE

Build/test/lint green baseline.

1. Discover project commands: check `~/.omt/{project}/project-commands.md` cache first, then `CLAUDE.md`/`README.md`/build files, then ask the user. Save discovered commands back to the cache.
2. Run: Build (fast build/typecheck) → Tests → Lint. Slow native build (e.g. an RN bundle) runs only when native code changed or this is a release build (native-code-or-release) — otherwise skip it.
3. Save the full output of each check as an evidence file (see Evidence Saving Protocol below).
4. ANY failure = immediate REQUEST_CHANGES.

**See** [stage1-commands.md] for command-discovery detail, special cases (no tests for changed code, no build system), and output format — the content there is BASELINE's detail target, not a separate stage.

### ADVERSARIAL E2E

Drive the changed surface for real and attack it. Two parts, both required when the change touches a risk surface — user-facing OR an internal risk surface (feature-flag-gated logic, payment/notification resolver internals, permission/state transitions), per [stage3-handson.md] `### Decision Logic`; only a genuinely inert refactor that touches no risk surface skips:

1. **Execute caller-provided scenarios verbatim**, with per-scenario evidence. ANY provided-scenario failure = immediate REQUEST_CHANGES. Caller-provided scenarios always run verbatim, unchanged — the derivation framework below governs only scenarios qa self-authors; it never rewrites what the caller handed in.
2. **Self-author the 6-axis adversarial matrix** for the changed surface, in this order — breadth before depth:
   1. **Derive candidate scenarios by breadth** via [scenario-authoring.md]: Layer A impact-map → coverage-gap → H/M/L priority, then Layer D product use-case breadth (arrival paths · adjacent state transitions · lifecycle stances) from a product-context map built from the repo.
   2. **Attack each derived scenario with the applicable depth rows, working highest-priority (H) first**, from the 6 coverage axes: failure paths, boundary/malformed input, injection, interruption-resume + dirty state, misleading success, idempotency. See [stage3-handson.md] `## Adversarial Scenario Matrix` for the full matrix and the lifecycle/applicability detail (start → verify → stop). Rows 7–9 (stale-state, dirty-worktree, flaky-rerun) are per-run checks recorded separately with `record-run-check`.

When a caller-provided scenario fails, record its failing cell and record every remaining unrun cell as `na` with the halt reason before declaring REQUEST_CHANGES. Apply the same sweep after any EXIT fired following a FIX dispatch (max-cycles or Safety): `inc-cycle` invalidates prior-cycle records, including baseline and run-check records in the current view, so record the current cycle's remaining cells explicitly rather than leaving the gate with unrecorded work. Prior-cycle records remain in the raw state/history for audit.

For a genuinely inert refactor with no risk surface, still author the roster and all cells, record the story baseline and all three run checks, record every cell as `na` with the no-risk-surface reason, and run `qa-state.ts declare-inert --reason "<why nothing is reachable>"` once. Without that declaration an H-priority `na` blocks APPROVE.

**Inline modality drivers, no tmux.** qa itself drives the modality-appropriate tool inline — it is not delegated to a separate driver subagent:

| Change Type | Driver |
|-------------|--------|
| API endpoint | `curl` |
| Frontend / UI | `agent-browser` (fallback: `playwright`, if available) |
| Mobile / native UI | `agent-device` |
| CLI / TUI | interactive `bash` |

For mobile/native UI work, load the `agent-device` skill first and derive the current concrete commands from its runtime `agent-device help <topic>` guidance. Do not copy or invent concrete driving command syntax in this skill.

**Enter at the actor's boundary.** Every **self-authored** scenario is executed by entering at its actor's boundary from the Actor Roster and observing what that actor observes. Calling the changed function, class, or module directly is a unit check, not a scenario run — it proves the code in isolation and leaves every layer between the actor and that code unexercised.

A **caller-provided** scenario is exempt from relocation and stays verbatim (part 1 above) whatever layer it enters at — the caller owns that choice. It is not exempt from disclosure: record its `driven-at` as the layer it actually entered, and it supports no claim above that layer. A caller-provided command that runs at an inner layer never satisfies an actor-boundary scenario on the same surface.

**Precondition bootstrap (CRITICAL) — exhausted before any hop is called unreachable.** A missing precondition is work to do, not an obstacle to record. Bootstrap it, rung by rung:

**Local-first stance — stand up an isolated stack you own, and give it maximum freedom.** For a source change not yet deployed, run QA against a local stack you stand up and fully control, so you are free to seed any account/data state, mutate it, force failure branches, interrupt mid-flight, and reset between scenarios. **Stand up whatever the scenario needs to run and to give you that control — never a fixed checklist.** A component comes up because a scenario depends on it, not because it was the thing that changed: a server-logic change still brings its **database** up locally — a real datastore you can freely seed and mutate beats a shared or deployed one you cannot — along with the backend and anything else that scenario exercises, while a change whose actor boundary is a command or job trigger may have no service, database, or bundler behind it at all, and running the local command *is* the local instance. Read the project's environment-setup commands and docs (the boot command, required env vars, local config) and apply them; a local process that fails to start on a missing or misconfigured env is a config to supply, **not** a reason to stop. Isolate the instance (its own ports, data directory, containers) so a **shared or fragile** local environment is neither a blocker — you bring up your own rather than surrender — nor something you corrupt by driving against a stack another session or developer is using. The one exception is when the QA REQUEST verifies the deployed artifact itself (rung 1's carve-out): there the deployed environment is the boundary, and a local stack proves nothing about it.

**Documented protocol first — before any rung below.** When the missing precondition is an account, authentication, or a data state, the project's documented QA provisioning protocol (read at PLAN.1) decides the move: use the pre-provisioned account it lists for that data state, or the admin/operator QA tool it prescribes for seeding, before you improvise. The improvise options in rungs 2–3 (hand-rolled seed rows, signup/onboarding, minted tokens, injected dummy credentials) apply **only when the project documents no such account or tool, or when the documented path was attempted and found unusable** — expired credentials, the tool unavailable or broken, or it cannot produce the required state. Reached for *before the documented path is tried*, they are a wrong detour that also risks driving the actor through a flow the scenario never meant to test (onboarding, consent) and around a config gap you never diagnosed against the documented setup. Attempted-and-unusable is not the same as absent: the ladder still gets exhausted, just after the prescribed path fails rather than instead of it.

1. **Environment not deployed / not accessible** (branch not on stage, endpoint 404, no deploy permission) → first read what the QA REQUEST verifies. For a **source change not yet deployed**, the deployed environment was never the boundary: stand up locally whatever the scenario needs to run — the backend and its database, the bundler, or, for a command-boundary change, just the local command. When HOW to stand it up is not already known, mine the project's own docs and scripts for it — `README.md`/`CONTRIBUTING.md`, `Makefile`, `docker-compose.yml`/`docker-compose.*.yml`, `scripts/` — before asking the user or declaring the precondition unreachable (see [stage1-commands.md] Discovery Order). Then re-point or re-build the app against that local backend; non-deployment is an environment choice, not a boundary obstacle. **A local stack that fails to boot on a missing or misconfigured env** (an unset env var, absent local config, an unsigned local credential) is the same rung: read the documented env-setup, supply the missing configuration on your own isolated instance, and retry — a startup config gap is bootstrap work, not a stop, and "the local backend won't start" or "the stack is shared, so I left it alone" is env surrender, not an unreachable boundary. When the QA REQUEST **verifies the deployment itself** — a release, deploy config, routing, migration, packaged artifact — the deployed environment IS the boundary: its 404 is the scenario's observed FAIL (or `NOT-RUN` when truly unreachable), never a precondition to bootstrap around, and a local stack proves nothing about the deployed artifact ([stage3-handson.md] stale-state row).
2. **No data** → read the schema and create seed data; when the seeding procedure itself is not already known, mine README/`Makefile`/`docker-compose.yml`/`scripts/` for a seed script or documented seed procedure first.
3. **No account / credential** → first use the pre-provisioned QA account the documented protocol prescribes for the data state you need (e.g. an account that already carries a Program, or the admin QA tool that seeds one). Only when the project documents no such account — or the prescribed account/tool was tried and proved unusable (expired credentials, a broken/unavailable tool, or it cannot produce the required state) — do you run the signup flow as its own scenario step, or mint a test token and inject it (cookie/header/session).
4. **Precondition satisfiable only on another platform** (a flag toggled in an admin web, a state set from an operator tool) → launch that platform too, satisfy the precondition there for real, then verify the feature on the platform under test. Launching the precondition platform is setup cost, and setup cost never skips a scenario.
5. **Genuinely external dependency outside your control** (third-party API off-network, physical hardware absent) → only this rung enters *Boundary substitution* below: fake that hop alone while every other layer runs real.
6. **Required tool missing locally** (a CLI, browser driver, emulator/simulator toolchain, or other dependency the verification needs is not installed) → install it, don't skip the scenario. Try **project-local** first — a devDependency, a local bin, a project-scoped install — no machine mutation. Only when a tool genuinely cannot be project-local (e.g. a system emulator/simulator toolchain) try a **global** install. If the global install fails (offline, locked-down) — substitute *only that hop* and record what was substituted and why, the same discipline as rung 5's Boundary substitution; never treat the failed install as a reason to skip the scenario outright. Never use `rm -rf` or a force flag to clear the way for an install.

Declaring an obstacle without recording which rungs were attempted and why each failed is boundary evasion, not a coverage delta.

**Boundary substitution.** An unreachable boundary — one still unreachable after the bootstrap ladder above, like absent physical hardware or an off-network third-party dependency — never relocates the scenario inward while the claim stays where it was. In order:

1. Drive from the actor's boundary anyway, **replacing only the unreachable hop** with a fake or stub (fake transport, stubbed external API, seeded local data), so every layer between the actor and that hop still executes.
2. Record the scenario's `driven-at` as the deepest point actually entered plus what was substituted — e.g. `app dispense screen → hardware command (USB transport faked)`.
3. Only when even that is impossible is the scenario `NOT-RUN`, not PASS. A recorded coverage delta states what remains unproven; it is never a substitute for running it.

**NOT-RUN is reserved for physical impossibility, never for setup cost.** A boundary is `NOT-RUN` only when it stays unreachable *after* you have stood up every component you control and created every data/account/membership state the scenario needs — absent physical hardware, or an off-network third party you cannot run locally (rung 5). The amount of bootstrap work — full local stack, manual DB inserts, minting QA accounts and memberships, toggling flags on another platform — never makes a boundary `NOT-RUN`; it is the work the ladder requires. "Focused time-box", "beyond a reasonable attempt", or a caller telling you to time-box do not change this.

**Substitution replaces one unreachable hop — it never swaps the boundary for the app's test suite.** Running `vitest`/`jest`/`pytest` on the changed service/function (even against a real local DB) is not substitution: it relocates the entire scenario off the actor's boundary and down to a unit check, and its output is a test-runner report, which is never cell evidence (see the HARD RULE above). When the true boundary is a UI or API the actor touches and you cannot drive it here, the scenario is `NOT-RUN` and the requirement it covers is **unverified** — recorded loudly, never green.

**Depth honesty.** A verdict claims exactly the boundary its scenarios were driven from. Evidence sets collected at different depths never merge into a deeper claim: "the internal function behaves" plus "the app launches" is not "the actor's path works".

**A scenario cell's evidence is a boundary OBSERVATION — a test-runner report is never it (HARD RULE).** A cell's evidence must be what the actor observes at its boundary: the rendered screen / device state (a screenshot or recording of the actual UI after the real interaction, connected to a running backend), the client-received API response (the actual status + body the caller gets), or the terminal a CLI user actually operates. **A unit / integration / component test-runner report — `vitest`/`jest`/`pytest`/`mocha`/`go test` output, "N passed", "exit=0" — is NEVER a cell's evidence.** It proves code in isolation, not the boundary a human or external system touches, and a PO/designer cannot read it as "the requirement works." Running the app's own test suite is not "driving the boundary" and not boundary substitution — it is a unit check dressed as a scenario. `record-cell` mechanically rejects a test-runner report as evidence; do not try to route one in. (BASELINE is the sole home for build/test/lint logs — `record-baseline`, not `record-cell`.) **If the real boundary cannot be driven, the scenario is NOT-RUN (a coverage delta), never a pass/na backed by a test log** — see Boundary substitution and the *unverified* verdict path.

#### Red Flags — Boundary Evasion

| Excuse | Reality |
|--------|---------|
| "It's a pure function, so calling it directly is the closest real entry point" | The closest real entry point is the boundary an actor can reach. A harness reaches nothing. |
| "The new bundle installs and the app launches, so the change is verified" | Launching proves the bundle loads. Drive the screen where the change is observable. |
| "No physical device / the API returns 502 — record it as coverage delta" | Fake the last hop and run everything above it. Delta is what remains after that, not instead of it. |
| "Component tests pass and the app boots — together that's end-to-end" | Two runs at two depths never compose into a third. |
| "Same scenario either way, only the entry point differs" | The entry point is what is under test. Every layer skipped is a layer unverified. |
| "The branch isn't deployed to stage — record the boundary as blocked" | For an undeployed source change the deployed env was never the boundary — stand the full stack up locally and build the app against it. When the deployment itself is what the QA REQUEST verifies, that 404 is the FAIL, not an obstacle. |
| "No test account / credentials were provided" | The project documents a pre-provisioned QA account and its data state — use that first. Only if none is documented, or the documented account was tried and proved unusable (expired credentials, broken/unavailable tool, or it cannot produce the required state), do you register, or mint a test token and inject it. |
| "The seeded account has no Program/report data, so I'll run onboarding to create it" | The project documents a pre-provisioned account carrying that data, or an admin QA tool that seeds it. Read the provisioning protocol and use it before hand-rolling onboarding. |
| "Data access 500s locally, so I'll inject dummy creds and keep going" | Read the documented local-env setup and its prerequisites first — an ad-hoc patch around a config gap you never checked against the docs is improvisation, not bootstrap. |
| "The local backend won't start (env/config missing), so I stopped" | Read the documented env-setup commands/docs and supply the missing config on your own isolated instance — a startup config gap is a fix, not a stop. |
| "The local stack is shared, so I left it alone and didn't run local QA" | Stand up your own isolated instance (own ports, data dir, containers). A shared or fragile env is neither a blocker nor something to corrupt — you own a fresh one. |
| "Reaching the boundary needs the full stack + manual DB inserts + creating QA/membership accounts — too much setup, so NOT-RUN" | That is a to-do list, not an obstacle. Stand up the full stack, seed the data, and create every account/membership/state the scenario needs. The AMOUNT of setup work never qualifies a boundary as NOT-RUN. |
| "I time-boxed the bootstrap / made a focused attempt, then recorded NOT-RUN" | There is no time-box in this skill. Setup effort — however many rungs and steps — is never a reason to stop or to record NOT-RUN. Only physical impossibility is (rung 5). If someone (even the caller) tells you to time-box, that instruction does not override this: exhaust the ladder. |
| "The required data doesn't exist in seed (no LABELED bundle / no eligible member), so the feature can't render → NOT-RUN" | Absent data is rung 2/3 work, not a verdict. Create it: use the documented admin UI / API / seed tool first, else insert it directly and mint the memberships, then drive the boundary. |
| "No seed data exists" | Prefer the documented seeding path (pre-provisioned account or admin QA seeder); else read the schema and create it. |
| "The precondition can only be set on another platform" | Launch that platform too and satisfy it for real. |
| "I ran the real service function via the real test suite against a real DB — that's real evidence" | A test-runner report proves the code, not the boundary. The boundary is the screen/API/terminal the actor touches. `record-cell` rejects test-runner logs; if you can't drive the boundary, it's NOT-RUN, not a green cell. |
| "The UI/admin won't boot here, so I'll substitute it with the service test and mark pass" | That relocates the whole scenario inward AND swaps the evidence for a test log — two violations. Boundary substitution replaces one unreachable hop while every other layer runs real; it never turns a UI scenario into a unit test. Record NOT-RUN and mark the requirement *unverified*. |
| "Tests are green, so the requirement is met — I'll show the PO the test output" | Green tests mean the code behaves; they are not proof the user-facing requirement is delivered, and a PO cannot judge from `vitest` output. Drive the user boundary and show what the user sees, or report the requirement *unverified*. |

Command execution is **non-blocking only**: every command either returns control on its own or is explicitly backgrounded (`run_in_background`, or trailing `&` with output redirected — a bare `cmd &` without redirection leaves the harness waiting on inherited file descriptors and hangs the agent). A bare blocking command that hangs the shell is forbidden. See [stage3-handson.md] Step 3.2 for the lifecycle this backs (start in background → wait for readiness → verify → stop, never leaving a server running).

**By-design non-idempotency note:** running ADVERSARIAL E2E actually exercises the application (starts servers, sends requests, mutates state); some operations under test are intentionally non-idempotent per spec, which is not itself a defect.

### CHECK

Is the goal met — BASELINE green and the full ADVERSARIAL E2E pass (provided scenarios + matrix)? Judge each scenario on its own row, then ask the questions that a per-scenario PASS does not answer:

**Required evidence review, before verdict:** after recording each visual pass/fail
cell, open its raw captures and action record and submit the claim review with
`qa-state.ts review-evidence --story … --cls … [--sub …] --json-file <review.json>`.
Read [presentation.md](presentation.md#claim-review-record) for the required
fields and example. Cover every asserted outcome, not just screen identity.
The state keeps the execution result separate from evidence sufficiency: a
missing, insufficient, or stale review blocks APPROVE/COMMENT and completion;
the report shows **근거 미검증**, not a product failure or NOT-RUN. Re-capture
missing outcomes and review again. File existence alone does not close CHECK.

- Did each scenario traverse the changed surface from its actor's boundary all the way through, or did it stop at an inner layer? Read `driven-at`, not the PASS.
- Final check — does each scenario's observation **name the medium it was observed through**: a screen/device capture, or an API/CLI response? Run this over every row before APPROVE; it is the last gate against a readable observation that quietly over-claims its boundary. A human actor's result read only through an API or CLI was **not** seen at that actor's screen boundary — an API or CLI reading never stands in for a screen observation, so that scenario is `unverified` at the screen, not PASS. The one exception: when **no user-facing surface exists yet** (an API-only change with no screen built), the actor *is* an API/system client and the API response is its real boundary — declare it as such on the scenario; never dress an API reading as a human-screen observation.
- Is any `H`-priority scenario still `NOT-RUN`? Then the goal is not met, whatever the other rows say.

A FAILED scenario row blocks CHECK, with exactly one carve-out: a failed **self-authored `M`/`L`** row whose finding scores **50–74** — the `nitpick (non-blocking)` band, which [feedback-protocol.md]'s scale defines as *real but minor, rarely happens in practice* — leaves the cycle a **soft pass**: the row stays FAIL in the roster, the finding is reported as a LOW note, and the verdict is COMMENT, never APPROVE.

Everything else blocks. A finding scoring **75+** blocks whatever the row's priority — the scale calls 75 *likely to occur in practice, directly impacts functionality*, which is a defect, not a nitpick. A failed **`H`-priority** row blocks whatever its score. A failed **caller-provided** row blocks — it never soft-passes, per `### ADVERSARIAL E2E` part 1. And a failed row whose finding cannot be scored at **50 or above** is not a soft pass but an unexplained failure: re-run or diagnose it, because a scenario that failed for reasons you cannot state is the one most likely to matter. Never restate a failed row as PASS to reach a clean sheet.

- **Pass → PASS.** Emit APPROVE (see Output Format).
- **Soft pass → PASS with COMMENT.**
- **Fail → enter the loop below.**

### DIAGNOSIS → FIX → RE-VERIFY (loop, ≤5 cycles)

CHECK failure hands off to a three-way-separated loop so the agent that fixes the defect is never the one that certifies the fix:

#### DIAGNOSIS

delegate to `oracle` (fresh, read-only, root cause + file:line). oracle never modifies files; it returns a diagnosis, not a patch.

#### FIX

delegate to `sisyphus-junior`. **sisyphus-junior commits its own scoped fix** — it authored the hunks, so it alone can stage them precisely; qa cannot separate a fix's hunks from a user's hunks in a shared file. Never `git commit -a`.

`cycle++` happens here — **cycle++ at FIX dispatch** is the counted unit (pre-fix detection is cycle 0, uncounted).

#### RE-VERIFY

qa re-runs **BASELINE + the FULL matrix** from scratch — not just the failed scenario. **Distrust the fixer's report**: sisyphus-junior's own claim of "fixed" is not evidence; only a fresh, from-scratch re-run counts. Running the full matrix (not only the scenario that failed) is what catches a fix that silently regresses a scenario that was previously green.

Loop back to CHECK. Continue until an EXIT condition below fires.

### EXIT

| Condition | Trigger | Action |
|-----------|---------|--------|
| **Goal Met** | CHECK passes (BASELINE + full matrix green) | PASS → APPROVE |
| **Goal Met, soft pass** | CHECK soft-passes (one carve-out row, per `### CHECK`) | PASS → COMMENT, carrying the failed row and its LOW note |
| **max_cycles=5** | `cycle` reaches `max_cycles` (5) still unresolved | Terminate, report unresolved with last diagnosis |
| **Safety** | A safety invariant refuses to proceed | Terminate, report the refusal reason |

- **max-N boundary**: with `cycle` starting at 0 and `cycle++ at FIX dispatch`, `max_cycles=5` permits exactly 5 fix attempts (cycles 1..5); the 5th fix is attempted and re-verified, then EXIT fires if still unresolved.

### CLEANUP

Kill every process and remove every artifact this cycle spawned (background servers, simulators/emulators started for ADVERSARIAL E2E, temp files) — regardless of whether the cycle ended in PASS or an EXIT condition. A leaked process corrupts the next run. **Never remove a path supplied through `--evidence-path`**, regardless of whether it came from a caller, a required-verification entry, or a self-authored scenario; completion re-probes every passing cell and baseline evidence path.

### STATE

Persist `phase`/`cycle` (plus `max_cycles`) to a state file after every phase transition, via:

```
bun ${CLAUDE_SKILL_DIR}/scripts/qa-state.ts <sub>
```

A `continue` invocation reads this state and resumes at the last recorded phase/cycle rather than restarting the cycle from PRE-FLIGHT. (The CLI itself is authored elsewhere — this section only pins the invocation contract qa's cycle relies on.)

The chain-recording surface is: `set-acceptance`, `add-actor`, `add-story`, `author-cell`, `record-baseline`, `record-cell`, `review-evidence`, and `record-run-check`. Use `set-verdict APPROVE|COMMENT|REQUEST_CHANGES` to persist the verdict; `waive --story … --cls … --reason "…"` is a **user-only** exception and is denied on the AI Bash path. For a no-risk-surface cycle, use `declare-inert --reason "…"`. Run `bun ${CLAUDE_SKILL_DIR}/scripts/qa-state.ts help` to see the full command roster and which are user-only. Runtime gates consume the persisted chain/record predicates: the phase funnel blocks BASELINE until the roster→story→cell chain is complete, the driver guards block `agent-device`/`agent-browser`/`curl`/`bash` while the roster is incomplete or once BASELINE has been reached with an incomplete chain (PLAN reachability probes remain available), and the Stop gate validates the raw state on both Claude and Codex. Direct writes to `qa-state-*.json` are denied; use the CLI.

Once the cycle concludes (any EXIT outcome — Goal Met, max_cycles, or Safety), first run `bun ${CLAUDE_SKILL_DIR}/scripts/qa-state.ts set-verdict <APPROVE|COMMENT|REQUEST_CHANGES>`, then run `bun ${CLAUDE_SKILL_DIR}/scripts/qa-report.ts --session <id> --out <path> [--narrative <json-file>]`, open the rendered HTML and verify all claim images remain legible, run `bun ${CLAUDE_SKILL_DIR}/scripts/qa-state.ts review-report --path <html>`, then run `bun ${CLAUDE_SKILL_DIR}/scripts/qa-state.ts complete`, and only then report the verdict prose. The renderer records the HTML/state identity; `review-report` records the visual inspection attestation. Editing the HTML or recorded facts requires re-render and re-review. `complete` is gated by the same predicates as Stop and refuses an unrecorded or falsely approved cycle; it marks an earned terminal state inactive so the finished cycle is not resurrected as "in progress" in a later session.

---

## Fix-Loop Nesting Contract

qa's fix-loop (DIAGNOSIS → FIX → RE-VERIFY) **must NOT be called inside another fix-loop** — nesting it in a caller-owned pursuit loop double-loops retries and confuses which loop owns EXIT. This is a documented contract, not an enforced one: **YAGNI** — no detection-guard code is written for a caller that does not exist yet. Named upgrade trigger: **add a code guard when qa gains its first fix-loop-owning caller.**

---

## Evidence Saving Protocol

### Core Rule

Every verification **command execution** (BASELINE, ADVERSARIAL E2E, RE-VERIFY) produces an evidence file. Evidence files are the audit trail; downstream gates check for their existence before accepting a verdict.

### Objective vs. Subjective

| Output Type | Disposition | Examples |
|-------------|-------------|---------|
| Objective command output | Save to file | build/test/lint logs, curl response body + status, agent-browser/Playwright/agent-device screenshots and reports, CLI execution logs |
| Subjective judgment | Response only (no file) | PLAN's spec/AC reading, oracle's diagnosis narrative |

### Actor-Perspective Evidence (per executed scenario)

**Required visual evidence contract — read [presentation.md](presentation.md) before recording and reporting.** For every executed `agent-browser`/`agent-device` pass or fail, `record-cell` requires separate `--evidence-before` and `--evidence-after` screenshot files plus `--evidence-action`. The completion predicate rechecks these slots. Capture the actual actor's screen immediately before the action and at the asserted outcome, in this scenario and cycle. The report additionally requires `scenarios[cellKey].observed` explaining the action, visible result, and expected result, **alongside both embedded images**. An observation is not a replacement for an image at a visual boundary.

| Boundary actually exercised | Required proof |
|---|---|
| Browser or device UI | Real before screenshot → recorded actor action → real after screenshot; both images visible on the scenario card, plus observed explanation |
| API caller or text CLI | Actual request/command and received response/output, with a reader-facing observed explanation; screenshots of logs do not strengthen this proof |
| Visual TUI or physical device state outside a screenshot-capable driver | Capture the actual display/state with the appropriate available capture mechanism; disclose an evidence gap if it cannot be captured |

For each additional visible state asserted by a scenario (error notice, permission denial, loading state, recovered state), capture that state too. A landing-page image, a screenshot from another actor/run, a generated/mock image, or a diagram cannot prove the asserted outcome. Preserve the original capture; any resized copy must retain the asserted UI and legible result. Before/after files may show identical content when no visible change is expected, but must be separately captured files.

**Final-report gate:** the `qa-report.ts` CLI refuses visual scenarios with missing/unreadable/non-embeddable before/after images or missing observed prose. It also refuses an image that would fall back to a path because of the per-file or cumulative embed budget. Resize/re-encode captures without changing the evidence, update the recorded paths, and render again. Open the resulting HTML and inspect every visual scenario card at readable size before `complete`. If capture or embedding remains unavailable, report the exact evidence gap and leave verification unfinished; do not claim a product failure, `na`, or PASS merely to close the record.

An evidence set proves what its actor could observe at its boundary. Every executed scenario gets its own set, named for that scenario id from the roster so the two map 1:1, holding three slots in this order:

| Slot | What it holds |
|------|---------------|
| `before` | The actor-observable state the scenario starts from — the screen the actor is on, the value the endpoint currently returns, the record as it stands. |
| `action` | The action as the actor issues it at its boundary — the tap/click sequence, the exact request, the command typed — with the immediate response. |
| `after` | The outcome the actor observes — the resulting screen, the response body, the delivered payload or written record — asserted against the scenario's `expected`. |

At a UI boundary, `before` and `after` are captures of the asserted state: the screen where the change is visible. **A screenshot of a launch, splash, or landing screen is not scenario evidence** — it proves the app started.

Internal signals (server logs, DB rows, emitted command payloads, instrumentation) are supporting evidence attached beside these three, never a replacement for them. Build/test/lint logs are BASELINE evidence and prove nothing about any actor's path.

### Evidence File Content Requirements

Evidence files must contain meaningful content that demonstrates the verification result. Empty (0-byte) files are not valid evidence. When a command produces empty stdout, record the command executed and its exit code so the file is not empty.

### Evidence Path Priority (3-Tier)

1. **Explicit path from QA REQUEST** — caller explicitly provided a path
2. **Plan QA Scenario Evidence field** — use `$OMT_DIR/evidence/{plan-name}/{task-slug}/{scenario-slug}.{ext}`.
3. **Auto-generated path (fallback):**
   ```
   $OMT_DIR/evidence/{work-slug}/{task-slug}/{check-slug}.{ext}
   ```
   Ensure the target directory exists before saving (`mkdir -p`).

### Evidence Reporting in Response

After the cycle completes, include a `## Evidence Files` section listing every evidence file saved, with `$OMT_DIR` expanded to its absolute path:

```
## Evidence Files
- /Users/dev/.omt/my-project/evidence/add-user-endpoint/implement-user-service/build.txt
- /Users/dev/.omt/my-project/evidence/add-user-endpoint/implement-user-service/npm-test.txt
```

Omit this section when no commands were executed (a PRE-FLIGHT fail-fast, judgment-only).

---

### HTML Report

At STATE, immediately after `set-verdict` and before `complete`, qa renders a self-contained HTML report via `bun ${CLAUDE_SKILL_DIR}/scripts/qa-report.ts --session <id> --out <path> [--narrative <json-file>]`. The renderer reads the recorded chain through the `qa-state get` / `readQaView` path and renders **from qa-state records, not re-narrated** — every AC, actor, story, scenario, evidence path, and PASS/FAIL/verdict fact in the report is exactly what qa-state recorded, so the report cannot drift from what actually ran. Execution-account narrative — issue descriptions, expected-vs-actual prose, oracle diagnosis — is supplied at render time through `--narrative`, never persisted to qa-state. Evidence-review judgments are different: they are required persisted claim records, not optional narrative.

**Presentation layer (reader-facing, at the top of the report).** Above the verification log, the report leads with a product/user-centric narrative a context-free PO/designer reads first to judge whether the change met its requirements — without opening code or the audit log below it. It is **not** a code-diff summary and **not** "backend changed → tests passed". It reads top-down as *what was asked, then what we saw*: ① a feature overview; ② an **Acceptance Criteria · 충족 현황** board — each recorded acceptance criterion with its satisfied / not-satisfied / partial / unverified verdict and the scenario + evidence that proves it (the PO's at-a-glance answer; AC text and its verdict are one merged section, not two); ③ a big-picture diagram of the user flow; ④ an **액터 · 영향받는 유저** section — one block per actor (the roster and the affected-users narrative are the same actors, merged), naming who is affected (admin / product user / conditional user / partner …), how each uses the product (software + hardware), and whether their boundary was reachable; ⑤ a **유저 시나리오 · 근거** section presenting, per story, a short scenario-overview intro and then ONE card per scenario — each carrying its own real-software record (an authored user-boundary observation plus before/after screenshots for visual scenarios; a response-grounded observation for text scenarios), with final visual report generation blocked when a required image or observation is missing — plus a plain-language coverage summary. Below the reader layer sits the record-faithful audit (per-cell 시나리오 상세 기록, which carries each scenario's technical boundary + driver — there is no separate actor-roster table — plus failures, verdict, evidence files). Author this presentation before rendering and supply it through `--narrative` as a `presentation` object; **[presentation.md]** owns the full authoring contract, the JSON shape, the anchoring rule, and the self-audit. Every part is anchored to a recorded fact — `affectedUsers` by actor id, `scenarioFlows` by story id, `requirementMapping` by AC index — so the layer cannot drift or invent: prose for an actor absent from the roster is ignored (an affected user missing from the roster is a roster gap to fix with `add-actor`, not to invent in prose). Any required slot left unwritten renders a **visible `gap` marker** in the report, so a skipped part shows rather than silently vanishing.

For `requirementMapping`, each entry must have a non-empty `cellRefs` array of `{story, cls, optional sub}` selectors. The renderer validates every ref against exactly one recorded current-cycle cell and its recorded status (`pass`, `fail`, or `na`): `yes` requires every referenced cell to be `pass`; `no` requires every referenced cell to be `fail`; `partial` requires at least one `pass` and one `fail` and no `na`; `unverified` requires at least one valid `na`. Missing/legacy/malformed/duplicate/stale/unknown/ineligible mappings fail closed to a visible neutral gap (`미판정`) rather than a green verdict. Prose evidence explains a verdict but cannot establish it.

The reader-facing **유저 시나리오 · 근거** section is story-level and clean: per story it shows the actor's name, the authored user-boundary flow, that story's evidence (the current-cycle baseline plus each scenario's `before` / `action` / `after` and recorded `evidence.path`), and a plain-language coverage summary — the six adversarial axes by name, never the `cls` number. It deliberately omits the cell record's implementation-flavored fields (`driven_at`, `why_needed`, `na_reason`, `attack_point`, the actor's `boundary` code path), which the QA engineer writes technically for the audit trail; those live in the record-faithful **시나리오 상세 기록 (감사)** section below — one row per cell with its `cls`, `attack_point`, `driven_at`, result, and recorded evidence paths — so a context-free reader never meets implementation leakage while full traceability is preserved. A story whose verified (pass/fail) scenarios carry no evidence renders a loud `gap` (a pass cell cannot even be recorded without evidence; `na` is the one evidence-free status). Unreadable or unembeddable visual evidence blocks final report generation; resize/re-encode the capture and update the recorded path. A fresh `start` clears `acceptance_criteria`, so a new report cannot inherit the previous cycle's criteria.

The report is produced on **every cycle that reached a roster** (PLAN.1 ran) — this includes the inert-refactor zero-row case. The sole exception is the **PRE-FLIGHT fail-fast**, which never reaches PLAN and therefore renders no report.

The report file is self-contained: inline `<style>`, zero runtime `<script>`, no external CSS/JS/font/image reference — it opens offline. Evidence embeds are capped at 2 MiB per file and 16 MiB cumulatively; visual images that exceed either budget block final report generation; reduce image size and update recorded paths. Non-image audit files may retain a path-only fallback. Save it under the evidence directory, e.g. `$OMT_DIR/evidence/{work-slug}/{task-slug}/report.html`.

---

<Output_Format>

## Output Format

```markdown
## Cycle Summary

| Phase | Status | Details |
|-------|--------|---------|
| PRE-FLIGHT | PASS / REQUEST_CHANGES | [MUST-NOT-DO / B⊆A result] |
| BASELINE | PASS / FAIL | [build/test/lint summary] |
| ADVERSARIAL E2E | PASS / FAIL | [matrix + scenario summary] |
| Cycles run | N / max_cycles | [reason if terminated early] |

## Actor Roster

| actor | boundary | driver | reachable |
|---|---|---|---|

One row per actor the changed surface serves, from PLAN.1. `boundary` names an interface an actor touches — never a function, class, or module. `reachable` is `yes` or the obstacle plus the deepest reachable point toward that boundary. On a PRE-FLIGHT fail-fast the cycle never reaches PLAN, so this section is absent rather than empty — same as the scenario roster below.

## Scenarios Executed

| # | source | actor | driven-at | preconditions | steps | expected | result | evidence | why-needed | priority |
|---|---|---|---|---|---|---|---|---|---|---|

Every row's `source` is either `self-authored` or `caller-provided`. A `self-authored` row carries the six-field shape from [scenario-authoring.md] — actor · preconditions · steps · expected · why-needed · priority — filled in full; a `caller-provided` row carries whatever shape the caller supplied. `driven-at` names the boundary actually entered plus any substitution; `result` is PASS / FAIL / `NOT-RUN`; `evidence` is the path to that scenario's evidence set. This table is the canonical scenario record for the cycle: `result` maps to `Status` and `why-needed` maps to `Why-Needed` in the four-column working table [stage3-handson.md] mandates for hands-on execution — the column-count divergence is declared here, not fixed there.

Close the table with exactly one coverage-delta line naming the impact-map domains from [scenario-authoring.md] and the three Layer D use-case axes (arrival paths · adjacent state transitions · lifecycle stances), stating which of those the rows above cover and which are left uncovered.

## Verdict: [APPROVE / REQUEST_CHANGES / COMMENT]

**Report:** [absolute path to the self-contained HTML report file — leads with the reader-facing presentation layer ([presentation.md]) above the verification log; or "not generated (PRE-FLIGHT fail-fast)" when the cycle never reached a roster]

## Issues (if any)
[For each issue:]
- **[CRITICAL/LOW]**: [Brief description]
  - Location: [file:line]
  - What: [problem]

## Evidence Files
- [absolute path to each evidence file saved during this cycle]

(Omit Evidence Files when no commands were executed — a PRE-FLIGHT fail-fast)
```

</Output_Format>

---

## Approval Decision

1. **Issuance precondition.** A `## Scenarios Executed` section is a precondition for issuing a verdict. When it is absent, the `## Verdict` heading is omitted rather than a verdict being issued — this reads as an unfinished cycle, never as a fourth value alongside the `{APPROVE, REQUEST_CHANGES, COMMENT}` domain below. The single exception to this absence rule is the PRE-FLIGHT fail-fast, which legitimately issues **REQUEST_CHANGES** with no cycle run and therefore no `## Scenarios Executed` section. A section that is *present* with zero rows is a separate case, not an omission: when ADVERSARIAL E2E was skipped because the change is a genuinely inert refactor touching no risk surface (`### ADVERSARIAL E2E` above), the cycle is complete, its coverage-delta line states the change touched no risk surface, and a verdict **is** issued per the table below.

2. **Boundary gate.** APPROVE requires every `H`-priority scenario to have been executed at its actor's boundary, with any substitution declared in `driven-at`. **An `H`-priority scenario left `NOT-RUN` blocks APPROVE** — issue REQUEST_CHANGES naming the obstacle. The verdict never describes the cycle as end-to-end unless the roster's `driven-at` values say it was.

| Condition | Verdict |
|-----------|---------|
| PRE-FLIGHT contract violation | **REQUEST_CHANGES** (MUST-NOT-DO / B⊆A violated, cycle not executed) |
| EXIT via max_cycles/Safety, unresolved | **REQUEST_CHANGES** (unresolved after cycle) |
| CHECK soft-passes (a failed self-authored `M`/`L` row, finding in the 50–74 nitpick band) | **COMMENT** (never APPROVE — the failed row stays FAIL in the roster) |
| CHECK passes (BASELINE + full matrix green) | **APPROVE** (or **COMMENT** to surface LOW notes — see *On COMMENT* below) |

Every issue surfaced MUST include a confidence score. See [feedback-protocol.md] for Confidence Scoring, Validation, and Conventional Comments.

**On COMMENT.** The decision above is binary — APPROVE or REQUEST_CHANGES. COMMENT is an optional **soft-pass variant of APPROVE**: emit it in place of APPROVE when there are LOW, non-blocking notes worth surfacing to the consumer. It carries **no MEDIUM tier** — severity is CRITICAL / LOW only — and it is never a partial or "almost" verdict. Consumers read COMMENT as approve-with-notes: a per-task verifier closes the task after its evidence gate; an objective-level completion gate still requires an explicit APPROVE (the never-false-complete invariant), so a COMMENT there prompts addressing the notes and re-verifying toward APPROVE, never completion.

---

## Quick Reference

```
CYCLE:      PRE-FLIGHT → PLAN → BASELINE → ADVERSARIAL E2E → CHECK → [DIAGNOSIS → FIX → RE-VERIFY loop ≤5] → EXIT → CLEANUP → STATE
PRE-FLIGHT: MUST-NOT-DO scope + B⊆A only; violation = immediate REQUEST_CHANGES, cycle NOT executed. No EXPECTED OUTCOME → B⊆A is not-evaluable, never A:=Scope
CHECK:      a FAILED row blocks, except a self-authored M/L row scoring 50-74 = soft pass → EXIT Goal Met, soft pass → COMMENT, never APPROVE. 75+ blocks, H blocks, caller-provided blocks, unscorable-below-50 = re-run not soft-pass
ACTOR:      Actor Roster before scenarios — actor · boundary · driver · reachable; a function/class/module is never a boundary; internal change → trace the call graph outward; roster spans the actor's journey, not the diff — precondition platforms enter it too. Enter every scenario at its actor's boundary; substitute only the unreachable hop and record driven-at; otherwise NOT-RUN, never PASS. H-priority NOT-RUN blocks APPROVE
BOOTSTRAP:  a missing precondition is work, not an obstacle — LOCAL-FIRST: for an undeployed source change, stand up an isolated stack you own (own ports/data/containers) for max freedom to seed/mutate/interrupt/reset; read the env-setup commands/docs and apply them; a local startup config gap is a fix, not a stop; a shared/fragile env is neither a blocker nor something to corrupt. documented protocol FIRST: read the project's QA provisioning docs at PLAN.1 and pick the pre-provisioned account / admin QA seeding tool they prescribe before improvising. undeployed env → stand the stack up locally; no data/account → documented seeder or pre-provisioned account, else seed rows / signup / inject a token (hand-rolled onboarding or dummy creds when a documented path exists = wrong detour); precondition on another platform → launch that platform too; only a genuinely external hop enters substitution. Never QA only the diff's platform. When the QA REQUEST verifies the deployment itself, the deployed env IS the boundary — its 404 is the FAIL, not a precondition
EVIDENCE:   per-scenario before/action/after at the actor's boundary; launch/splash/landing captures are not scenario evidence; internal logs support, never replace; depths never merge into a deeper claim
BASELINE:   build/test/lint green. See stage1-commands.md
MATRIX:     6 categories — failure paths, boundary/malformed input, injection, interruption, misleading success, idempotency. Breadth via scenario-authoring.md, depth via stage3-handson.md
USE-CASE:   Layer D — build the product-context map from the repo, then walk arrival paths · adjacent state transitions · lifecycle stances; each axis present in the map yields scenarios, and the coverage delta names all three
DRIVERS:    API→curl, Frontend→agent-browser (fallback playwright, if available), Mobile/native UI→agent-device (load its skill first; use runtime help guidance), CLI→bash. No tmux.
LOOP:       DIAGNOSIS→oracle (fresh, read-only) | FIX→sisyphus-junior (commits own scoped fix, never git commit -a) | RE-VERIFY→qa, full re-run, distrust fixer
EXIT:       Goal Met / max_cycles=5 / Safety
STATE:      bun ${CLAUDE_SKILL_DIR}/scripts/qa-state.ts <sub>; continue resumes at last phase/cycle
NESTING:    qa's fix-loop must NOT be called inside another fix-loop — doc contract, YAGNI; upgrade trigger: add a code guard when qa gains its first fix-loop-owning caller
ROSTER:     ## Scenarios Executed is a precondition for verdict issuance; absent → verdict not issued, cycle incomplete. Exception: PRE-FLIGHT fail-fast issues REQUEST_CHANGES with no roster — never synthesize an empty one there; present+0 rows means inert refactor, a completed cycle
FEEDBACK:   feedback-protocol.md for Confidence Scoring; CONFIDENCE 0-49 discard, 50-74 nitpick, 75+ blocking
REPORT:     STATE renders self-contained HTML (qa-report.ts) after set-verdict, before complete; leads with a reader-facing presentation layer (product/user perspective, not code-diff — affected users · user-boundary flows · big-picture diagram · AC→fulfillment mapping) via --narrative presentation object, anchored to recorded actors/stories/ACs; unwritten required slots render visible gap markers. presentation.md owns the contract
```

---

## Final Checklist

**Run this list explicitly right before emitting the verdict — read each line and tick its box against the actual roster.** **Any unchecked box blocks APPROVE.** It restates the blocking gates above as one scannable list; where a box and an earlier section disagree, the stricter reading wins.

- [ ] **Every ADVERSARIAL E2E scenario ran and passed at the user boundary** — provided scenarios plus the 6-axis matrix, none left unrun, none still failing (the exact blocking / soft-pass rules are in CHECK and Approval Decision).
- [ ] **Each scenario was driven from its actor's real boundary** — `driven-at` confirms it; calling the changed unit directly is a unit check, not a scenario run.
- [ ] **Each scenario's observation names the medium it was observed through** — a screen/device capture, or an API/CLI response. A human actor's result read only through an API/CLI was not seen at that actor's screen boundary → that scenario is `unverified` at the screen (not PASS). Exception: an **API-only** change with no screen built yet — declare the actor an API/system client; never dress an API reading as a human-screen observation.
- [ ] **Every verified scenario carries a reader-visible record** — an authored observation AND before/after screenshots for visual boundaries, with fresh supported `review-evidence` claims naming each source's visible location and observation; missing, insufficient, or stale proof remains unverified. API/text-CLI boundaries use authored observations backed by received output. Raw logs stay in the audit section.
- [ ] **The final HTML has been inspected before attestation** — consume every image/diagram inspection result, repair clipping or unreadable claims, then `review-report` and `complete`. Never batch an unobserved capture with completion.
- [ ] **Each requirement (AC) maps to a grounded verdict** — every `requirementMapping` entry has a non-empty `cellRefs` array; the renderer validates each ref against exactly one recorded current-cycle cell; `yes` requires all `pass`, `no` requires all `fail`, `partial` requires both `pass` and `fail` with no `na`, and `unverified` requires a valid `na`; missing/legacy/malformed/duplicate/stale/unknown/ineligible mappings fail closed to a visible neutral gap. Prose evidence explains a verdict but cannot establish it; an undriven boundary reads `unverified` (never `yes`/`partial`).