@qaping/cli 0.1.1 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/qaping CHANGED
@@ -43,8 +43,9 @@ function resolveKitDir() {
43
43
  const RULE_BODY = `This machine has qaping: automatic QA for games — real human playtesters play the developer's builds on their own hardware and report back, and the coding agent runs the whole loop.
44
44
  Whenever the user asks to set up QA for a game, run QA on a patch, check whether a change broke anything, or playtest a game or build, load the qaping skill and follow it — it owns authoring/maintaining QA-PLAN.md in the game repo and the per-patch run.
45
45
  Builds ship with \`qaping publish-build <game.zip> --platform windows|macos\`; the printed /b/<slug> URL is what a round is filed against (store-delivered games file with their Steam/TestFlight/App Store URL instead).
46
- The MCP tools are qaping_playtest (file a round with real human playtesters), qaping_results (fetch a finished round, free) and qaping_wait (continue a pending round — whenever a filing or wait returns pending, call qaping_wait again immediately; never report pending as the answer).
47
- Playtests are duration-billed at 2 credits per minute of play per playtester — state the estimated cost before filing. Windows rounds return a recording + questionnaire and no transcript; never promise one.
46
+ The MCP tools are qaping_playtest (file a round with real human playtesters), qaping_results (fetch a finished round, free) and qaping_wait (continue a pending round — on pending, spawn a background task that loops qaping_wait and keep working; foreground polling is the fallback, and never report pending as the answer or end with an unbacked promise to check later).
47
+ Playtests are duration-billed at 2 credits per minute of play per playtester — state the estimated cost before filing. Windows rounds return a recording and no transcript; web rounds return answers only (no recording, no transcript) — never promise evidence a platform does not ship.
48
+ QA memory: whenever the developer mentions — in ANY conversation, not just QA runs — something that should always be tested, a fragile area, or a QA lesson learned, offer to record it as a check (or a note on an existing check) in QA-PLAN.md so it is tested from then on.
48
49
  `;
49
50
 
50
51
  // The wrapper object harness/setup.js's opts.wrapper API takes; also the one
@@ -12,15 +12,16 @@ YAML at the very top of the file:
12
12
  ---
13
13
  game: Solar Drift
14
14
  build_command: ./scripts/package.sh --release # the ONE command that produces a shippable build
15
- platforms: [windows] # windows | macos | ios
15
+ platforms: [windows] # windows | macos | ios | web
16
16
  input: XInput gamepad # optional; default keyboard+mouse
17
17
  ---
18
18
  ```
19
19
 
20
20
  `build_command` is recorded once at setup so every future run can build without
21
- asking. `input` declares the hardware the first-15-minutes check (and any check
22
- that needs it) is played on rounds must ask playtesters to confirm what they
23
- actually used.
21
+ asking. `platforms: [web]` means a browser-playable game its "build" is a
22
+ static directory, not a zip. `input` declares the hardware the
23
+ first-15-minutes check (and any check that needs it) is played on — rounds
24
+ must ask playtesters to confirm what they actually used.
24
25
 
25
26
  ## Checks
26
27
 
@@ -30,19 +31,41 @@ opens with a fenced yaml block, then the steps as prose:
30
31
  ```yaml
31
32
  id: save-loads # stable slug — never reused, never renamed
32
33
  rung: human # human | code
34
+ state: unverified # unverified | passed | ambiguous | failing
33
35
  origin: authored # "authored", or the round id that minted/last changed it
34
36
  last_verified: 2026-08-25 # date or build tag of the last passing verification
37
+ setup: "?save=campaign-mid" # optional: the link/flag that forces this check's precondition
35
38
  ```
36
39
 
37
40
  - **Steps are prose for a person**: numbered, imperative, written so a
38
- playtester who has never seen the game can follow them they become the
39
- round's instructions verbatim.
41
+ playtester who has never seen the game can follow them. At filing time each
42
+ check becomes ONE id-tagged step with typed verdict options, so end the
43
+ steps in an observation ask ("describe what you saw"), never a
44
+ confirmation ("confirm it appears").
45
+ - **`state` is the check's verdict history**: `unverified` (never verified on
46
+ the current behavior), `passed` (last verified answer was Pass),
47
+ `ambiguous` (a fail that could be dirty state or tester noise — refiles
48
+ alone as a targeted round), `failing` (a fail backed by hard evidence or a
49
+ targeted round). Blocked/VOID outcomes change nothing here.
50
+ - **`setup` names the test affordance** that forces the check's precondition —
51
+ a URL param (`?fresh=1`, `?state=<checkpoint>`), a flag, a bundled-fixture
52
+ load control. At filing time the hook rides the ROUND's `url` — the review
53
+ interface loads it itself; testers cannot open links or type URLs — and
54
+ the filed step states only the player-visible expectation, never the
55
+ mechanism. A check
56
+ whose precondition can be neither forced nor verified is testimony-only —
57
+ say so in its prose.
58
+ - **Fixtures are provisioned by the dev's repo**: a previous-version save
59
+ ships inside the build (or seeds via URL param on web) and is named in the
60
+ check's steps — an anonymous playtester in a fresh browser cannot possess
61
+ it.
40
62
  - **Order the file critical-path first**: boots to menu, previous-version save
41
63
  loads, first 15 minutes on the declared input hardware, core loop, settings
42
64
  persist — then game-specific checks.
43
65
  - A `rung: code` check keeps its prose (it documents intent) and adds one
44
66
  `test:` line naming the repo test that now asserts it, e.g.
45
- `` test: `tests/save_compat.test.ts` ``.
67
+ `` test: `tests/save_compat.test.ts` ``. Mechanically-assertable checks
68
+ enter at `rung: code` on day one — human rounds are for judgment.
46
69
 
47
70
  ## Example section
48
71
 
@@ -52,20 +75,51 @@ last_verified: 2026-08-25 # date or build tag of the last passing verification
52
75
  ```yaml
53
76
  id: save-loads
54
77
  rung: human
78
+ state: unverified
55
79
  origin: authored
56
- last_verified: 2026-08-25
80
+ last_verified: never
81
+ setup: "?save=campaign-mid"
57
82
  ```
58
83
 
59
- 1. From the main menu, choose Continue.
60
- 2. Load the provided save "campaign-mid.sav" (made on the previous release).
61
- 3. Confirm the game resumes in the desert outpost with the inventory intact.
62
- 4. Play for one minute; note anything missing, corrupted, or visually wrong.
84
+ 1. The game opens already loaded into the save "campaign-mid.sav" (made on
85
+ the previous release; the round's link forces it no action needed).
86
+ 2. Play for one minute in the desert outpost.
87
+ 3. Describe what you saw: where you resumed, what the inventory held, and
88
+ anything missing, corrupted, or visually wrong.
63
89
  ````
64
90
 
91
+ ## The round ledger — qa-rounds.jsonl
92
+
93
+ Beside the plan, at the game repo root, sits `qa-rounds.jsonl`: one JSON line
94
+ appended per round, committed alongside the plan updates —
95
+
96
+ ```json
97
+ {"round_id": "…", "report_url": "…", "build": "<sha or hosted url>",
98
+ "evidence_grade": "…", "checks": [{"id": "…", "outcome": "…"}],
99
+ "verdict": "…", "credits": 20, "follow_ups": ["…"]}
100
+ ```
101
+
102
+ Outcomes are `Pass | Fail | Blocked | NOT-ASKED` — a VOID (precondition
103
+ unverified/disproven) is recorded as `Blocked` with a "VOID: …" note in the
104
+ report, never its own enum value. `report_url` is the round's service report
105
+ page (what the results tool returns), never a PR or issue link. There is no
106
+ evidence-link field, deliberately: signed recording URLs expire in days.
107
+ The ledger is the machine truth of what was asked and answered, per check,
108
+ per round; the plan's `last_verified` and `state` are derived from it and
109
+ stay the dev-readable contract. Open `follow_ups` are what the next filing
110
+ must carry forward.
111
+
65
112
  ## Maintenance rules
66
113
 
67
- - Every run updates the run checks' `last_verified`; findings-driven edits
68
- record the round id as `origin`.
69
- - New checks start `rung: human`, `origin: authored`.
114
+ - Every run updates the run checks' `state` and `last_verified` from typed
115
+ outcomes (Pass stamps both; Blocked/VOID/NOT-ASKED touch neither);
116
+ findings-driven edits record the round id as `origin`.
117
+ - New checks start `rung: human` (unless mechanically assertable — then
118
+ `rung: code` with a repo test written at entry), `state: unverified`,
119
+ `origin: authored`.
70
120
  - Promotion (human → code) and any deletion/demotion happen only with the
71
121
  developer's explicit agreement.
122
+ - **QA memory**: the plan accretes from conversation, not just QA runs — when
123
+ the developer mentions something that should always be tested, a fragile
124
+ area, or a lesson learned, the agent offers to record it as a check (or a
125
+ note on an existing check) right then.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qaping/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "description": "qaping: automatic QA for your game. Your coding agent authors and maintains the QA plan, ships each patch's build to real human playtesters, and reports what broke.",
5
5
  "keywords": [
6
6
  "qaping",
@@ -6,31 +6,92 @@ description: Run automatic QA for a game repo through qaping — real human play
6
6
  # qaping — the QA loop for a game repo
7
7
 
8
8
  Two jobs, one file. SETUP authors `QA-PLAN.md` — the committed list of checks
9
- this game must pass. RUN executes it per patch: pick the affected checks, build,
10
- ship the build to real human playtesters, report what they found. Over patches,
11
- checks migrate from human rounds to repo code tests, so QA gets cheaper. All
12
- service contact goes through three MCP tools `qaping_playtest`,
13
- `qaping_results`, `qaping_wait` plus `qaping publish-build` for hosting
14
- the build.
9
+ this game must pass. RUN executes it per patch: pick the affected checks,
10
+ build, ship the build to real human playtesters, and report ONE typed outcome
11
+ per check never a vibe. The check is the unit of value: its id rides from
12
+ the plan onto the wire, into the report, and into the round ledger. Over
13
+ patches, checks migrate from human rounds to repo code tests, so QA gets
14
+ cheaper. All service contact goes through three MCP tools —
15
+ `qaping_playtest`, `qaping_results`, `qaping_wait` — plus
16
+ `qaping publish-build` for hosting the build.
15
17
 
16
18
  ## Ground rules (read before filing anything)
17
19
 
18
- - **Playtesters are real people.** A session takes real minutes to be claimed
19
- and played. File, then `qaping_wait`; whenever a filing or wait returns
20
- pending, immediately call `qaping_wait` again with the same ping_id. Never
21
- report pending as the final answer, never file a duplicate round.
20
+ - **Playtesters are real people, and waiting is background work.** A session
21
+ takes real minutes to be claimed and played, and the wait chain matters: each
22
+ `qaping_wait` renews the lease that keeps the round visible to new claimants.
23
+ The moment a filing returns pending, SPAWN A BACKGROUND TASK that loops
24
+ `qaping_wait` with the ping_id until results arrive, and keep working on
25
+ other things — that is the intended shape, the same as every pingfusi round.
26
+ Only if your harness has no background tasks: keep polling in the foreground.
27
+ Only in a one-shot print mode where neither is possible: say plainly that the
28
+ round is pending, give the ping_id, and ask to be re-invoked to continue —
29
+ never promise polling you cannot do (an ended session cannot poll; a round
30
+ went invisible and expired unclaimed exactly this way, live). Never report
31
+ pending as the final answer, never file a duplicate round.
22
32
  - **Costs are duration-billed**: 2 credits per minute of play, per playtester
23
- (`est_minutes × 2 × players`). State the estimate to the user BEFORE filing.
24
- - **Windows rounds return a recording + questionnaire and NO transcript**
25
- (`transcript_status:'unavailable'`)never promise one; plan to watch the
26
- recording for timestamps. macOS and iOS rounds include an inline [mm:ss]
27
- think-aloud transcript.
28
- - **Machine replay is not self-serve yet.** Never claim an automated replay ran.
29
- The only automated rung you can run today is a `rung: code` test in the
30
- dev's own repo/CI.
33
+ (`est_minutes × 2 × players`). State the estimate to the user BEFORE filing,
34
+ and set the wait expectation honestly in the same message: claim time is
35
+ unbounded on a small pool "push now or wait for QA" is their call, made
36
+ informed.
37
+ - **Evidence varies by platform — never promise what a platform does not
38
+ ship.** macOS and iOS rounds return a screen+voice recording plus an inline
39
+ [mm:ss] think-aloud transcript. Windows rounds return a recording and NO
40
+ transcript (`transcript_status:'unavailable'`) never promise one; plan to
41
+ watch the recording for timestamps. WEB rounds return answers only — no
42
+ recording, no transcript: the tester's option picks and free text are the
43
+ entire deliverable, which is why every check rides an option step.
44
+ - **Pay is flat per session, never per bug.** Never offer or imply a bounty.
45
+ Never steer a tester's OS-security decision (stopping at a warning is their
46
+ penalty-free call); steering their PLAY — steps, links, forced states — is
47
+ the job.
48
+ - **Machine replay is not self-serve yet.** Never claim an automated replay
49
+ ran. The only automated rung you can run today is a `rung: code` test in
50
+ the dev's own repo/CI.
31
51
  - **QA-PLAN.md is the dev's file.** Commit it only with their approval; never
32
52
  delete or demote a human check without them agreeing.
33
53
 
54
+ ## The review interface (know your tester)
55
+
56
+ <!-- Distilled from QAPING_REVIEWER_INTERFACE.md (monorepo root, INTERNAL,
57
+ code-cited). Re-derive there when a reviewer surface changes. -->
58
+
59
+ Binding, derived from the review interfaces' code. The person who claims a
60
+ web round is on their phone, scrolling a feed. They read `instructions`
61
+ BEFORE claiming — front-load hardware and requirements there so the wrong
62
+ tester never claims. On claim the game auto-loads into an embedded browser
63
+ view with your steps beside it on the same small screen, with zero context;
64
+ attention is highest in the first minutes; setup friction is unpaid time.
65
+
66
+ The tester CAN: play the already-loaded game (touch, scroll, type); go
67
+ Back; Reload — which KEEPS storage, so a reload is never a fresh state; see
68
+ all steps at once and answer in any order; tap ONE option per option step;
69
+ type a note on any question step; pin a comment to a page element and draw
70
+ on it; pick one verdict; bail out with a reason.
71
+
72
+ The tester CANNOT:
73
+ - open tabs or windows — window.open and target=_blank links are dead
74
+ INSIDE the game too, so a game whose Play button opens a new window is
75
+ broken-for-review: flag it to the dev instead of filing;
76
+ - type or edit a URL: the round's `url` is the only page-load there is, and
77
+ links in step text are inert on the phone — never put links in steps;
78
+ - clear storage or go incognito: fresh state comes ONLY from a setup URL
79
+ param the round's `url` already carries;
80
+ - use devtools, attach files, or record — web rounds return answers only;
81
+ - use a second device;
82
+ - see `instructions` or `expected` once inside the review — EVERYTHING the
83
+ tester needs at answer time lives in the step text (≤300 chars each).
84
+
85
+ **Steps speak player language (MUST).** Things to tap and see in the game —
86
+ never flags, URL parameters, storage, or code concepts. A test hook is
87
+ exercised by the ROUND's `url` carrying it; the step describes only the
88
+ player-visible expectation ("the How to play option should be offered
89
+ first"), never the mechanism. Never ask to close/reopen anything or "start
90
+ fresh" — that state is a separate round whose `url` forces it. And as
91
+ always: one action per step, exact UI labels (the button says "Continue",
92
+ not "the resume option"), the most important check first, and never a step
93
+ that assumes they read your repo.
94
+
34
95
  ## SETUP — author QA-PLAN.md (first run in a repo)
35
96
 
36
97
  1. Read the game repo: engine, how a shippable build is produced, save system,
@@ -39,92 +100,261 @@ the build.
39
100
  shippable build) and which platforms they ship; record both in the plan's
40
101
  frontmatter so every future run can build without asking again.
41
102
  3. Author `QA-PLAN.md` at the repo root in EXACTLY this format (self-contained
42
- here — do not go looking for other docs). Plan frontmatter (YAML at the very
43
- top): `game`, `build_command` (the ONE shippable-build command), `platforms:
44
- [windows|macos|ios|web]` (`web` = a browser-playable game — its "build" is a
45
- static directory, not a zip), optional `input` (hardware; default
46
- keyboard+mouse, or touch for a mobile-web game).
103
+ here — do not go looking for other docs). Plan frontmatter (YAML at the
104
+ very top): `game`, `build_command` (the ONE shippable-build command),
105
+ `platforms: [windows|macos|ios|web]` (`web` = a browser-playable game — its
106
+ "build" is a static directory, not a zip), optional `input` (hardware;
107
+ default keyboard+mouse, or touch for a mobile-web game).
47
108
  Then one `##` section per check — the heading is the check's short name —
48
- opening with a fenced yaml block of exactly `id` (stable slug, never
49
- renamed), `rung: human | code`, `origin` ("authored" or the round id that
50
- last changed it), `last_verified` (date or build tag), followed by the steps
51
- as numbered prose a playtester who has never seen the game can follow (they
52
- become the round's instructions verbatim). A `rung: code` check keeps its
53
- prose and adds one `test:` line naming the repo test that asserts it. Order
54
- checks critical-path first, then game-specific ones:
109
+ opening with a fenced yaml block of `id` (stable slug, never renamed),
110
+ `rung: human | code`, `state` (`unverified | passed | ambiguous | failing`
111
+ new checks start `unverified`), `origin` ("authored" or the round id that
112
+ last changed it), `last_verified` (date or build tag), and optionally
113
+ `setup` (the link or flag that forces this check's precondition — see
114
+ "Make the build testable"), followed by the steps as numbered prose a
115
+ playtester who has never seen the game can follow. Author each check to
116
+ end in an observation ask, with its precondition stated as a verifiable
117
+ step-zero fact (forced by `setup`, else verified by an opening question).
118
+ A `rung: code` check keeps its prose and adds one `test:` line naming the
119
+ repo test that asserts it. Order checks critical-path first, then
120
+ game-specific ones:
55
121
  1. the game boots to the main menu
56
122
  2. a previous-version save loads
57
123
  3. the first 15 minutes play clean on the declared input hardware
58
124
  4. the core loop works (name it concretely — the thing this game is)
59
125
  5. settings persist across a restart
60
- Every check carries `{id, rung, origin, last_verified}`. New checks start
61
- `rung: human`, `origin: authored`.
62
- 4. Tell the dev how runs get triggered, honestly: for now the loop runs from
126
+ 4. **Enter each check at the right rung.** A check that is mechanically
127
+ assertable a save file loads, a config parses, an asset path resolves —
128
+ enters at `rung: code` from day one: write the repo test then and there,
129
+ never "start it human and promote later". Human rounds are reserved for
130
+ what only a human can judge. Where a save must still be human-tested, the
131
+ dev provides the fixture: the previous release's save ships inside the
132
+ build (or seeds via URL param on web), named in the check's steps — a
133
+ stranger in a fresh browser cannot possess your old save.
134
+ 5. **Gyms**: for a mechanic worth isolating, you may build a tiny
135
+ deterministic test scene or page in the dev's repo — one mechanic,
136
+ machine-readable pass/fail. Ordinary dev work; offer it, never force it.
137
+ 6. Tell the dev how runs get triggered, honestly: for now the loop runs from
63
138
  their own machine or agent — after a patch lands, they say "run QA on this
64
- patch" and the RUN half below executes. Do NOT offer to wire a CI workflow
65
- file: a plain CI runner has no qaping MCP connection and no login, so a
66
- committed workflow could not run this loop today. A CI-triggered recipe is
67
- a documented follow-up; until it ships, per-patch QA is an ask-your-agent
68
- step.
69
- 5. Show the dev the plan and commit only on their approval.
139
+ patch". Do NOT offer to wire a CI workflow file: a plain CI runner has no
140
+ qaping MCP connection and no login, so a committed workflow could not run
141
+ this loop today; a CI-triggered recipe is a documented follow-up. Until it
142
+ ships, per-patch QA is an ask-your-agent step.
143
+ 7. Show the dev the plan and commit only on their approval.
144
+
145
+ ## Make the build testable
146
+
147
+ Before publishing a test build, you SHOULD add test affordances to the dev's
148
+ own code — ordinary dev work that turns self-reported state into forced,
149
+ verifiable state:
150
+
151
+ - URL params or flags that force states: `?fresh=1` (a true first visit),
152
+ `?difficulty=medium`, `?state=<checkpoint>` (start at a checkpoint).
153
+ - Bundled save fixtures plus a load control (the previous release's save,
154
+ shipped in the build).
155
+ - Seeded randomness, so a reported oddity reproduces.
156
+ - A corner build stamp (build hash + first-run/save-state flag; a `?qa=1`
157
+ overlay on web) with a step asking the tester to read it back — a read-back
158
+ fact is verified; self-report is not.
159
+
160
+ Record each check's hook in its `setup` field; at filing time the hook
161
+ rides the ROUND's `url` — the game loads it itself, so the tester never
162
+ sees or types it — and the filed step states only the player-visible
163
+ expectation. The unreviewed-developer-build disclosure
164
+ already covers shipping these; the no-steering rule forbids steering SAFETY
165
+ decisions only. A store-delivered build that cannot surface state keeps its
166
+ checks testimony-only — mark that in the plan and report at that grade.
70
167
 
71
168
  ## RUN — per patch
72
169
 
73
170
  1. **Select.** Read the diff (or PR). Pick the checks it can plausibly affect,
74
- plus the always-run criticals (the critical-path block above). Tell the user
75
- which checks run and why.
76
- 2. **Build** via the recorded build command; zip the result (native) or keep
77
- the built static directory (web).
78
- 3. **Ship**, by delivery: NATIVE uploads `qaping publish-build <game.zip>
171
+ plus the always-run criticals (the critical-path block above) and every
172
+ check the ledger left NOT-ASKED or Blocked last round. Tell the user which
173
+ checks run and why.
174
+ 2. **Compose sessions.** Partition the selected checks by required state —
175
+ fresh-install / progression / settings-mutatingand put state-mutating
176
+ checks LAST within their session. A session has ONE state: its round
177
+ `url` (carrying the forcing setup param) is the only state control that
178
+ exists — testers cannot clear storage or start fresh themselves — so
179
+ checks needing different forced states never share a session. Budget play content: sum the checks'
180
+ directed play time; if it exceeds est_minutes, split into parallel
181
+ 5-minute rounds (credit-neutral: two focused 5-minute rounds cost one
182
+ diluted 10-minute round) or raise est_minutes within the cap, and state
183
+ the trade beside the cost estimate. Depth beyond the budget rotates across
184
+ patches; note in the plan which slice ran last.
185
+ 3. **Build** via the recorded build command, with each selected check's
186
+ `setup` affordance in place; zip the result (native) or keep the built
187
+ static directory (web).
188
+ 4. **Ship**, by delivery: NATIVE uploads — `qaping publish-build <game.zip>
79
189
  --platform windows|macos` → prints a `/b/<slug>` URL (temporary hosting;
80
190
  filing a playtest extends the build through the round; each publish mints a
81
191
  NEW URL). Store-delivered games file with their Steam store page, TestFlight
82
192
  public link, or App Store page as `url`. WEB games — host the built output
83
193
  at a public URL: the dev's own hosting if they have it, else
84
194
  `qaping publish <built-dir>` prints a hosted URL. Localhost never works —
85
- players open the link on their own devices.
86
- When filing a WEB game: send `url` + `est_minutes` and OMIT `platform`
87
- entirely (platform values are for native builds; a web round is claimed by
88
- players in their own browser, phones included).
89
- 4. **File** ONE `qaping_playtest` per batch of human checks a single session
90
- can cover, `est_minutes` an honest sum of the steps the service accepts
91
- 5–30 minutes (10 is the standard session; outside that range the filing is
92
- refused, not clamped), and the clock is play time: download/install happens
93
- before it starts. The authored
94
- `instructions` + `steps` must embed:
95
- - the selected checks' steps, in play order (the plan's prose, verbatim
96
- where possible);
97
- - the previous round's findings as context "last round reported X —
98
- recheck" so regressions get re-examined on every patch;
99
- - the hardware-verification convention: when the game needs more than
100
- keyboard+mouse, say so up front in `instructions` (playtesters self-select
101
- before claiming) AND add an explicit step asking which input device was
102
- actually used, with options the answer comes back verified in
103
- `steps_result`.
104
- For a regression batch that knows exactly what it is testing, send
105
- `questionnaire:'none'` so your steps are the only questions; leave the
106
- standard questionnaire on for a first baseline round.
107
- 5. **Wait:** `qaping_wait` until results arrive (see ground rules).
108
- 6. **Report** from `qaping_results`:
109
- - PR comment via `gh pr comment`: ONE verdict sentence first, then one line
110
- per check ✅/❌ with a recording timestamp ([mm:ss] from the transcript
111
- where present; from watching the recording on Windows) then credits
112
- spent.
113
- - One `gh issue create` per CONFIRMED bug: title, repro steps rebuilt from
114
- the playtester's answers, a timestamp link into the recording. Issues are
115
- drafts for the dev's own triage never near-duplicates, never one issue
116
- per symptom of the same bug.
117
- - Update `QA-PLAN.md`: each run check's `last_verified` becomes this
118
- build/date; a check changed because of this round records the round id as
119
- `origin`.
195
+ players open the link on their own devices. When filing a WEB game: send
196
+ `url` + `est_minutes` and OMIT `platform` entirely — a web round is claimed
197
+ by players in their own browser, phones included.
198
+ 5. **File** ONE `qaping_playtest` per composed session, always with
199
+ `questionnaire:'none'` on a QA-PLAN round your checks are the questions;
200
+ the standard instrument is a separate experience round (below), never a
201
+ default. A WEB session additionally files `require_evidence:'none'`
202
+ (MUST) left at its default, the finish silently demands a pinned
203
+ screenshot-comment no step asked for. est_minutes is an
204
+ honest sum of the steps' directed play — the service
205
+ accepts 5–30 minutes (10 is the standard session; outside that
206
+ range the filing is refused, not clamped), and the clock is play time:
207
+ download/install happens before it starts. The authored steps MUST be:
208
+ - **one step per selected check**, its text opening with the id token —
209
+ `[spin-readable] …` the plan prose rewritten as an observation
210
+ question ("describe what the start menu showed and what you tapped
211
+ first", never "confirm the menu appears"), with options exactly
212
+ ["Pass — as described", "Faildescribe exactly what you saw",
213
+ "Couldn't test — say why", "Didn't reach this part"] — the pick comes
214
+ back verified in steps_result. EXCEPTION: when the check asks the
215
+ tester to DISCRIMINATE between named states ("which difficulty did the
216
+ bot play like?", "which screen did you land on?"), the options are the
217
+ NAMED candidates themselves ("rookie-level" / "harder than that") plus
218
+ "Couldn't tell say why" — generic Pass/Fail on a discrimination
219
+ yields contradictions instead of a signal (measured live);
220
+ - option labels ≤40 chars, 2–4 options per step; never author a final
221
+ step whose options duplicate the verdict optionsthe phone hides it
222
+ as a duplicate of the verdict control;
223
+ - a check with a `setup` hook is forced by filing the session's `url`
224
+ WITH the hook the game loads it itself; the step states only the
225
+ player-visible expectation. One whose precondition cannot be forced
226
+ opens with a verifying option step ("Before this session: had you ever
227
+ played this game before?" yes / no / not sure);
228
+ - a recheck of a prior finding is a clearly-labeled step at its natural
229
+ play-order point (first when its required state demands it, last
230
+ otherwise) — "VERIFY FIX: last round saw X — does it still happen?" —
231
+ and prior findings stay OUT of `instructions` entirely, so the rest of
232
+ the pass is an independent look;
233
+ - a closing **break-it slice** (~2–3 minutes) authored from the diff:
234
+ "this patch changed X — try to break it: <concrete abuse ideas from the
235
+ diff>. Report the weirdest thing you saw AND list what you tried."
236
+ Abuse ideas target PLAYER-VISIBLE behaviors the diff changed — things a
237
+ player can tap, see, or provoke — never internal mechanisms (a flag, a
238
+ cache, a storage key means nothing to a player).
239
+ Finding nothing is a fine answer when we know what was attempted; frame
240
+ it in `instructions` as "these checks are the territory — break anything
241
+ near them";
242
+ - hardware verification where it matters: needs beyond keyboard+mouse go
243
+ up front in `instructions` AND an explicit step asks which input device
244
+ was actually used, with options.
245
+ **Filing gate (MUST):** print `this round covers: <ids>` per session and
246
+ assert the ids — unioned across every session this run files — equal the
247
+ Select step's list BEFORE calling qaping_playtest. The wire keeps only
248
+ the FIRST 20 steps_result rows and a play directive is prepended, so with
249
+ `questionnaire:'none'` budget at most 19 authored steps per session (8 if
250
+ the standard questionnaire rides) — rows past 20 are answered by the
251
+ tester and silently dropped at finish, which is why this gate exists. A
252
+ mismatch (the budget forcing cuts) fails loudly PRE-SPEND: batch another
253
+ session or state the deferral out loud — never silently truncate.
254
+ 6. **Wait, in the background:** spawn a background task looping `qaping_wait`
255
+ and keep working (see ground rules) — foreground polling is the fallback,
256
+ and a promise to check later is never a wait.
257
+ 7. **Report** from `qaping_results`:
258
+ - **Evidence line first (MUST).** Every report opens with its grade:
259
+ `Evidence: recording + transcript` (macOS/iOS) / `Evidence: recording
260
+ only` (Windows) / `Evidence: testimony only (web round)`.
261
+ - **Computed headline (MUST).** Derive the one verdict sentence from the
262
+ per-check outcomes, never from the tester's own verdict: `PASS` only
263
+ when every selected check returned Pass; `PASS — N unverified (<ids>)`
264
+ when nothing failed but any check is Blocked or NOT-ASKED; `FAIL: <id>`
265
+ on any Fail. Quote the tester's global verdict beneath it as one input —
266
+ it is never the headline. (The iterate rule already says unresolved
267
+ findings mean fix and refile — unresolved never headlines as PASS.)
268
+ - **One row per SELECTED check (MUST)**, keyed by id, outcome from
269
+ {Pass, Fail, Blocked, NOT-ASKED}. "Couldn't test" and "Didn't reach this
270
+ part" are Blocked, with the why. A check that reached the wire as more
271
+ than one row (a precondition step, a VERIFY FIX pair) takes its outcome
272
+ from the WORST row: any Fail → Fail; else any Blocked or unanswered row
273
+ leaves the check unverified and the headline counts it ("PASS — N
274
+ unverified") — never plain Pass over a Blocked sub-step. A check whose precondition came back
275
+ unverified or disproven is VOID — report it as Blocked ("VOID:
276
+ precondition unverified"), never as a pass or fail, and leave
277
+ `last_verified` untouched. A selected check that never made the wire or
278
+ never came back is NOT-ASKED and carries forward into the next filing
279
+ automatically. Cite [mm:ss] where a transcript or recording exists; on a
280
+ web round cite the step answers — there are no timestamps to cite.
281
+ - PR comment via `gh pr comment`: evidence line, the computed verdict
282
+ sentence first, then the per-check rows, then credits spent.
283
+ - **Ledger append (MUST).** Append ONE JSON line to `qa-rounds.jsonl` at
284
+ the game repo root, committed alongside the plan updates:
285
+ `{"round_id": …, "report_url": …, "build": "<sha or hosted url>",
286
+ "evidence_grade": …, "checks": [{"id": …, "outcome": …}], "verdict": …,
287
+ "credits": …, "follow_ups": […]}`. `report_url` is the round's service
288
+ report page (the `report_url` the results tool returns — never a PR or
289
+ issue link). No evidence-link field, deliberately: signed recording URLs
290
+ expire in days, so durable evidence pointers live in the report/issues,
291
+ not the ledger. The ledger is machine truth;
292
+ `last_verified` stays in QA-PLAN.md as the dev-readable contract derived
293
+ from it, and "carry findings forward" is a read of the last line's
294
+ follow_ups, not prose archaeology.
295
+ - Update `QA-PLAN.md` from the outcomes: Pass → `state: passed` and
296
+ `last_verified` this build/date; a Fail with hard evidence → `failing`;
297
+ a Fail that could be dirty state or tester noise → `ambiguous`;
298
+ Blocked/NOT-ASKED touch nothing. A check changed because of this round
299
+ records the round id as `origin`.
300
+ - **QA memory (standing rule, not just here)**: whenever the developer
301
+ mentions — in any conversation — something that should always be tested,
302
+ a fragile area, or a QA lesson learned, offer to record it as a check
303
+ (or a note on an existing check) in `QA-PLAN.md` right then, so the plan
304
+ accretes knowledge instead of losing it to chat scrollback.
305
+ - Issues: one `gh issue create` per finding — drafts for the dev's own
306
+ triage, never near-duplicates, never one issue per symptom of the same
307
+ bug. Before reproduction the body carries "observed once, unreproduced"
308
+ plus a proposed severity from consequence (blocks progress / degrades /
309
+ cosmetic). The label CONFIRMED is reserved for a targeted round or
310
+ unambiguous in-round evidence; a CONFIRMED issue carries repro steps
311
+ rebuilt from the tester's answers and a timestamp link into the
312
+ recording where one exists (web rounds have none — cite answers).
313
+
314
+ ## The ambiguity ladder
315
+
316
+ An `ambiguous` or first-time-failing check never re-runs inside the next
317
+ batch — it refiles ALONE as a targeted ~5-minute round: one check, its
318
+ precondition forced via `setup` (or verified by the opening option step),
319
+ plus the disambiguation steps. That round settles it: Pass → `passed` (note
320
+ the fluke); Fail → `failing`, and the issue upgrades to CONFIRMED. Never
321
+ resolve an ambiguity by re-buying the whole batch, and never label a
322
+ once-seen fail CONFIRMED without that round or hard evidence.
323
+
324
+ ## Paired rounds (two-state comparisons)
325
+
326
+ A feel/physics/balance regression judged absolute buys little — a stranger
327
+ answers "did spin feel right" in a vacuum. But a comparison never rides one
328
+ round: never two links in one round — links in step text are inert on the
329
+ phone and `instructions` go unread in-review, so a second URL simply never
330
+ gets opened. When a check needs TWO states compared (with/without a hook,
331
+ old build vs new), file TWO paired rounds with the SAME steps, one per
332
+ state, each state forced by its own round `url` (the ledger's `build` field
333
+ keeps the old build recoverable), and compare the answers yourself in the
334
+ report. Caveat it: different testers played each side, and a tester told to
335
+ hunt changes reports noise as change. Native builds pair the same way, one
336
+ build per round — re-publish the previous zip if its /b/<slug> expired.
337
+
338
+ ## The experience round (opt-in, never a default)
339
+
340
+ The standard 11-item player-experience questionnaire never rides a QA-PLAN
341
+ round. Offer it separately, at milestones, as an experience round: no checks
342
+ attached, questionnaire left on, n_target up to 3 players, priced
343
+ transparently (est_minutes × 2 × players — state it first). Single-tester
344
+ scores are trend data — meaningful only after 3+ rounds on the same game,
345
+ never compared across testers or builds at n=1. If 8 or more of the 11 scale
346
+ answers are identical, report "low-differentiation response — treat scores as
347
+ unreliable" and never average them into a comparison.
120
348
 
121
349
  ## PROMOTE — make the suite cheaper
122
350
 
123
351
  When a check has passed and is mechanically assertable — a save file loads, a
124
352
  config parses, an asset path resolves — offer to write a normal test in the
125
- repo's own test suite. On the dev's yes: write the test, flip the check's
353
+ repo's own test suite; the ledger's per-check pass history is the evidence
354
+ that justifies the offer. On the dev's yes: write the test, flip the check's
126
355
  `rung` to `code`, keep the prose (it documents intent) and add a `test:` line
127
356
  naming the test file. `rung: code` checks then run in the dev's own CI for
128
- free — that is the point: every check starts human and gets cheaper over time.
129
- Checks about feel, difficulty, or ambiguous visuals stay `rung: human`
130
- permanently. Never delete or demote a human check without the dev agreeing.
357
+ free — that is the point: every check starts as human as it must and gets
358
+ cheaper over time. Checks about feel, difficulty, or ambiguous visuals stay
359
+ `rung: human` permanently. Never delete or demote a human check without the
360
+ dev agreeing.