@qaping/cli 0.1.2 → 0.1.5

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
@@ -22,6 +22,11 @@ const { spawnSync } = require("child_process");
22
22
 
23
23
  const QPKG = path.resolve(__dirname, "..");
24
24
 
25
+ // The name npm actually serves. The bare `qaping` is refused by the registry's
26
+ // similarity rule (petition open, QAPING_PLAN.md §8), so every global install —
27
+ // the wrapper's own and the one the kit's setup performs — must use this.
28
+ const NPM_PACKAGE = "@qaping/cli";
29
+
25
30
  // The kit is a normal npm dependency when qaping is installed, and a workspace
26
31
  // sibling in the monorepo checkout (where node_modules may not be linked yet)
27
32
  // — resolve the package, never a hardcoded install path.
@@ -43,7 +48,7 @@ function resolveKitDir() {
43
48
  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
49
  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
50
  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).
51
+ 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
52
  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
53
  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.
49
54
  `;
@@ -55,7 +60,7 @@ function qapingWrapper() {
55
60
  brand: "qaping",
56
61
  // npm registry name ≠ bin name: the similarity rule blocks bare "qaping"
57
62
  // (petition open, QAPING_PLAN §8), so the global install pulls @qaping/cli.
58
- installPackage: "@qaping/cli",
63
+ installPackage: NPM_PACKAGE,
59
64
  appUrl: process.env.QAPING_APP_URL || undefined,
60
65
  mcpPath: "/api/mcp/qaping",
61
66
  serverKey: "qaping",
@@ -122,6 +127,32 @@ function defaultIO(kitDir) {
122
127
 
123
128
  const VALID_CLIENTS = ["claude-desktop", "claude-code", "cursor", "codex"];
124
129
 
130
+ // The kit's setup step 2 installs the global bin as `npm i -g <brand>` unless
131
+ // it is handed an installPackage — and a PUBLISHED kit older than that option
132
+ // ignores it, so it runs `npm i -g qaping`, a name npm's similarity rule
133
+ // refuses to host (404). Hit live on a fresh machine: setup configured every
134
+ // MCP client and reused the login, then reported "Setup incomplete" over that
135
+ // one 404.
136
+ //
137
+ // So the wrapper installs its OWN bin first, under the name npm serves. Once
138
+ // `qaping` is on PATH the kit's step finds it and logs "already installed
139
+ // globally", whatever kit version is underneath. npx runs from a cache dir
140
+ // that is NOT a persistent install — exactly the case that step exists for —
141
+ // so a cache/tmp path does not count as already installed.
142
+ function ensureGlobalBin(io) {
143
+ if (isSourceCheckout()) return;
144
+ const existing = io.which("qaping");
145
+ const persistent =
146
+ existing && !/[\\/](_npx|_cacache|npm-cache|Temp|tmp)[\\/]/i.test(existing);
147
+ if (persistent) return;
148
+ io.log(`Installing the qaping command (npm i -g ${NPM_PACKAGE})…`);
149
+ const r = io.run("npm", ["i", "-g", NPM_PACKAGE]);
150
+ if (!r || r.error || r.status !== 0) {
151
+ io.log(`⚠ could not install the qaping command — run it yourself: npm i -g ${NPM_PACKAGE}`);
152
+ io.log(" (setup continues; the MCP tools your agent calls do not need it, but `qaping publish-build` does)");
153
+ }
154
+ }
155
+
125
156
  async function cmdSetup(argv) {
126
157
  const kitDir = resolveKitDir();
127
158
  const { setup } = require(path.join(kitDir, "harness", "setup.js"));
@@ -129,7 +160,9 @@ async function cmdSetup(argv) {
129
160
  // accepts both the positional form (`setup cursor`) and `setup --client cursor`
130
161
  const args = argv.filter((a) => a !== "--force");
131
162
  const client = ((args[1] === "--client" ? args[2] : args[1]) || "").toLowerCase();
132
- const r = await setup(defaultIO(kitDir), {
163
+ const io = defaultIO(kitDir);
164
+ ensureGlobalBin(io);
165
+ const r = await setup(io, {
133
166
  home: os.homedir(),
134
167
  sourceCheckout: isSourceCheckout(),
135
168
  resolveToken,
@@ -49,7 +49,10 @@ setup: "?save=campaign-mid" # optional: the link/flag that forces this check's
49
49
  targeted round). Blocked/VOID outcomes change nothing here.
50
50
  - **`setup` names the test affordance** that forces the check's precondition —
51
51
  a URL param (`?fresh=1`, `?state=<checkpoint>`), a flag, a bundled-fixture
52
- load control. The filed step then starts with "open <exact link>". A check
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
53
56
  whose precondition can be neither forced nor verified is testimony-only —
54
57
  say so in its prose.
55
58
  - **Fixtures are provisioned by the dev's repo**: a previous-version save
@@ -78,8 +81,8 @@ last_verified: never
78
81
  setup: "?save=campaign-mid"
79
82
  ```
80
83
 
81
- 1. Open the game with the provided link (it loads the save "campaign-mid.sav",
82
- made on the previous release, from the build's bundled fixtures).
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).
83
86
  2. Play for one minute in the desert outpost.
84
87
  3. Describe what you saw: where you resumed, what the inventory held, and
85
88
  anything missing, corrupted, or visually wrong.
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@qaping/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
+ "homepage": "https://qaping.dev",
4
5
  "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
6
  "keywords": [
6
7
  "qaping",
@@ -17,11 +17,18 @@ cheaper. All service contact goes through three MCP tools —
17
17
 
18
18
  ## Ground rules (read before filing anything)
19
19
 
20
- - **Playtesters are real people.** A session takes real minutes to be claimed
21
- and played. File, then `qaping_wait`; whenever a filing or wait returns
22
- pending, immediately call `qaping_wait` again with the same ping_id keep
23
- polling, or the round goes invisible to new claimants when the lease lapses.
24
- Never 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.
25
32
  - **Costs are duration-billed**: 2 credits per minute of play, per playtester
26
33
  (`est_minutes × 2 × players`). State the estimate to the user BEFORE filing,
27
34
  and set the wait expectation honestly in the same message: claim time is
@@ -44,17 +51,46 @@ cheaper. All service contact goes through three MCP tools —
44
51
  - **QA-PLAN.md is the dev's file.** Commit it only with their approval; never
45
52
  delete or demote a human check without them agreeing.
46
53
 
47
- ## Know your tester
54
+ ## The review interface (know your tester)
48
55
 
49
- The person who claims your round is on their phone, scrolling a feed. They
50
- read `instructions` BEFORE claiming front-load hardware and requirements
51
- there so the wrong tester never claims. They play with your steps visible on
52
- the same small screen, with zero context and no dev tools; attention is
53
- highest in the first minutes; setup friction is unpaid time. So: one action
54
- per step, exact UI labels (the button says "Continue", not "the resume
55
- option"), the most important check first, state-sensitive checks forced by a
56
- link (see "Make the build testable"), and never a step that assumes they read
57
- your repo.
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.
58
94
 
59
95
  ## SETUP — author QA-PLAN.md (first run in a repo)
60
96
 
@@ -121,8 +157,10 @@ verifiable state:
121
157
  overlay on web) with a step asking the tester to read it back — a read-back
122
158
  fact is verified; self-report is not.
123
159
 
124
- Record each check's hook in its `setup` field; that check's filed step then
125
- STARTS with "open <exact link>". The unreviewed-developer-build disclosure
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
126
164
  already covers shipping these; the no-steering rule forbids steering SAFETY
127
165
  decisions only. A store-delivered build that cannot surface state keeps its
128
166
  checks testimony-only — mark that in the plan and report at that grade.
@@ -135,7 +173,10 @@ checks testimony-only — mark that in the plan and report at that grade.
135
173
  checks run and why.
136
174
  2. **Compose sessions.** Partition the selected checks by required state —
137
175
  fresh-install / progression / settings-mutating — and put state-mutating
138
- checks LAST within their session. Budget play content: sum the checks'
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'
139
180
  directed play time; if it exceeds est_minutes, split into parallel
140
181
  5-minute rounds (credit-neutral: two focused 5-minute rounds cost one
141
182
  diluted 10-minute round) or raise est_minutes within the cap, and state
@@ -157,8 +198,11 @@ checks testimony-only — mark that in the plan and report at that grade.
157
198
  5. **File** ONE `qaping_playtest` per composed session, always with
158
199
  `questionnaire:'none'` — on a QA-PLAN round your checks are the questions;
159
200
  the standard instrument is a separate experience round (below), never a
160
- default. est_minutes is an honest sum of the steps' directed play — the
161
- service accepts 5–30 minutes (10 is the standard session; outside that
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
162
206
  range the filing is refused, not clamped), and the clock is play time:
163
207
  download/install happens before it starts. The authored steps MUST be:
164
208
  - **one step per selected check**, its text opening with the id token —
@@ -167,11 +211,20 @@ checks testimony-only — mark that in the plan and report at that grade.
167
211
  first", never "confirm the menu appears"), with options exactly
168
212
  ["Pass — as described", "Fail — describe exactly what you saw",
169
213
  "Couldn't test — say why", "Didn't reach this part"] — the pick comes
170
- back verified in steps_result;
171
- - a check with a `setup` hook opens "open <exact link>"; one whose
172
- precondition cannot be forced opens with a verifying option step
173
- ("Before opening: had you ever played this game before?" — yes / no /
174
- not sure);
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 options — the 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);
175
228
  - a recheck of a prior finding is a clearly-labeled step at its natural
176
229
  play-order point (first when its required state demands it, last
177
230
  otherwise) — "VERIFY FIX: last round saw X — does it still happen?" —
@@ -180,6 +233,9 @@ checks testimony-only — mark that in the plan and report at that grade.
180
233
  - a closing **break-it slice** (~2–3 minutes) authored from the diff:
181
234
  "this patch changed X — try to break it: <concrete abuse ideas from the
182
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).
183
239
  Finding nothing is a fine answer when we know what was attempted; frame
184
240
  it in `instructions` as "these checks are the territory — break anything
185
241
  near them";
@@ -188,10 +244,16 @@ checks testimony-only — mark that in the plan and report at that grade.
188
244
  was actually used, with options.
189
245
  **Filing gate (MUST):** print `this round covers: <ids>` per session and
190
246
  assert the ids — unioned across every session this run files — equal the
191
- Select step's list BEFORE calling qaping_playtest. A mismatch (e.g. the
192
- 20-step cap forcing cuts) fails loudly PRE-SPEND: batch another session or
193
- state the deferral out loud never silently truncate.
194
- 6. **Wait:** `qaping_wait` until results arrive (see ground rules).
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.
195
257
  7. **Report** from `qaping_results`:
196
258
  - **Evidence line first (MUST).** Every report opens with its grade:
197
259
  `Evidence: recording + transcript` (macOS/iOS) / `Evidence: recording
@@ -205,7 +267,11 @@ checks testimony-only — mark that in the plan and report at that grade.
205
267
  findings mean fix and refile — unresolved never headlines as PASS.)
206
268
  - **One row per SELECTED check (MUST)**, keyed by id, outcome from
207
269
  {Pass, Fail, Blocked, NOT-ASKED}. "Couldn't test" and "Didn't reach this
208
- part" are Blocked, with the why. A check whose precondition came back
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
209
275
  unverified or disproven is VOID — report it as Blocked ("VOID:
210
276
  precondition unverified"), never as a pass or fail, and leave
211
277
  `last_verified` untouched. A selected check that never made the wire or
@@ -255,18 +321,19 @@ the fluke); Fail → `failing`, and the issue upgrades to CONFIRMED. Never
255
321
  resolve an ambiguity by re-buying the whole batch, and never label a
256
322
  once-seen fail CONFIRMED without that round or hard evidence.
257
323
 
258
- ## Differential rounds (web feel checks)
324
+ ## Paired rounds (two-state comparisons)
259
325
 
260
326
  A feel/physics/balance regression judged absolute buys little — a stranger
261
- answers "did spin feel right" in a vacuum. On WEB builds, where switching
262
- costs seconds, such a check MAY file as ONE session carrying BOTH builds:
263
- publish the previous build alongside the current one (the ledger's `build`
264
- field makes it recoverable), put the second URL in `instructions`, and author
265
- the steps as "play 2 minutes on build A, then build B name anything that
266
- changed and where". est_minutes honestly covers both halves. Caveat the
267
- report: a tester told to hunt changes reports noise as change, and order
268
- effects apply. Store-delivered and uploaded native builds stay single-build
269
- no second-URL surface exists for them.
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.
270
337
 
271
338
  ## The experience round (opt-in, never a default)
272
339