@asrunjs/asrun 0.9.0 → 0.10.0

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.
Files changed (67) hide show
  1. package/.claude/commands/explore-case.md +11 -6
  2. package/.claude/commands/gen-spec.md +10 -6
  3. package/.claude/commands/heal-spec.md +13 -9
  4. package/.claude/commands/make-case.md +1 -1
  5. package/.claude/skills/exploring-cases/SKILL.md +65 -31
  6. package/.claude/skills/exploring-cases/references/assertion-targets.md +8 -4
  7. package/.claude/skills/exploring-cases/references/case-rules.md +110 -0
  8. package/.claude/skills/exploring-cases/references/handoff-card.js +73 -11
  9. package/.claude/skills/exploring-cases/references/open-questions.md +58 -6
  10. package/.claude/skills/exploring-cases/references/readiness.md +4 -3
  11. package/.claude/skills/exploring-cases/references/tools.md +117 -0
  12. package/.claude/skills/exploring-cases/references/user-handoff.md +46 -17
  13. package/.claude/skills/generating-specs/SKILL.md +71 -33
  14. package/.claude/skills/generating-specs/references/conventions.md +4 -0
  15. package/.claude/skills/making-cases/SKILL.md +14 -7
  16. package/.claude/skills/making-cases/references/gaps.md +7 -4
  17. package/.claude/skills/making-cases/references/normalizing.md +1 -1
  18. package/AGENTS.md +51 -11
  19. package/README.md +4 -3
  20. package/bin/asrun.mjs +1 -0
  21. package/dist/lib/case-doc.d.ts +41 -4
  22. package/dist/lib/case-doc.js +109 -17
  23. package/dist/scripts/check-cases.js +89 -1
  24. package/dist/scripts/doctor.js +50 -3
  25. package/dist/scripts/markers-confirm.d.ts +1 -0
  26. package/dist/scripts/markers-confirm.js +139 -0
  27. package/dist/ui/server/actions.js +50 -16
  28. package/dist/ui/server/app.js +7 -2
  29. package/dist/ui/server/job-store.d.ts +60 -0
  30. package/dist/ui/server/job-store.js +198 -0
  31. package/dist/ui/server/jobs.d.ts +46 -4
  32. package/dist/ui/server/jobs.js +361 -59
  33. package/dist/ui/server/process-info.d.ts +17 -0
  34. package/dist/ui/server/process-info.js +126 -0
  35. package/dist/ui/server/result-state.d.ts +10 -0
  36. package/dist/ui/server/result-state.js +106 -0
  37. package/dist/ui/shared/ai-commands.d.ts +4 -1
  38. package/dist/ui/shared/ai-commands.js +14 -4
  39. package/dist/ui/shared/types.d.ts +17 -2
  40. package/docs/agents/case-format.md +39 -5
  41. package/docs/ai-workflow.md +47 -15
  42. package/docs/cases.md +28 -4
  43. package/docs/commands.md +6 -1
  44. package/docs/troubleshooting.md +40 -0
  45. package/docs/web-ui.md +29 -6
  46. package/package.json +2 -1
  47. package/templates/.claude/commands/explore-case.md +11 -6
  48. package/templates/.claude/commands/gen-spec.md +10 -5
  49. package/templates/.claude/commands/heal-spec.md +11 -7
  50. package/templates/.claude/commands/make-case.md +1 -1
  51. package/templates/AGENTS.md +9 -6
  52. package/templates/cases/_template.md +7 -0
  53. package/templates/package.json +1 -0
  54. package/ui/dist/assets/{dist-B7HeglDd.js → dist-2msWI-5M.js} +1 -1
  55. package/ui/dist/assets/{dist-DfBpjgZT.js → dist-4U_gPHXf.js} +1 -1
  56. package/ui/dist/assets/{dist-B9_GXR2G.js → dist-BMfJatHo.js} +1 -1
  57. package/ui/dist/assets/{dist-Bb36rNUw.js → dist-BVvHVJ3N.js} +1 -1
  58. package/ui/dist/assets/{dist-WvvCZeeu.js → dist-BmWaNjqO.js} +1 -1
  59. package/ui/dist/assets/{dist-G9KNeh6u.js → dist-BwNz_xW2.js} +1 -1
  60. package/ui/dist/assets/{dist-GvRUDL-2.js → dist-J36xayOv.js} +1 -1
  61. package/ui/dist/assets/{dist-Dqc_tfzC.js → dist-e-KhTWHJ.js} +1 -1
  62. package/ui/dist/assets/{dist-DevASegc.js → dist-sfZKBSMm.js} +1 -1
  63. package/ui/dist/assets/{dist-Dq4Vnw5N.js → dist-wjB7g6OJ.js} +1 -1
  64. package/ui/dist/assets/{index-BJ5D2mFK.js → index-CGbRJT3b.js} +134 -132
  65. package/ui/dist/assets/index-CI0i0LWc.css +2 -0
  66. package/ui/dist/index.html +2 -2
  67. package/ui/dist/assets/index-BXY-eKBn.css +0 -2
@@ -5,7 +5,7 @@ without anybody present (a one-shot `claude -p` / `opencode run`). The case file
5
5
  talk to each other, so nothing is lost between runs, tools or sessions.
6
6
 
7
7
  ## How a question is written in the case file
8
- Every marker starts with its emoji, which is part of the marker: `🚫 TODO [blocking]`, `❓ TODO [non-blocking]`, `✍️ Answer:`, `🔍 to be confirmed by QA` and `👁️ not observed`. The web UI colors the emoji with the marker and offers an X button that deletes it.
8
+ Every marker starts with its emoji, which is part of the marker: `🚫 TODO [blocking]`, `❓ TODO [non-blocking]`, `✍️ Answer:`, `🔍 to be confirmed` and `👁️ not observed`. The web UI colors the emoji with the marker and offers an X button that deletes it. `(seen on <profile>)` is not a marker: it is a plain note, shown as ordinary text.
9
9
  Every question is a small block that hangs under a normal sentence. The sentence is the agent's suggestion, written
10
10
  exactly as it should read in the final case. The block holds the question and an empty answer slot:
11
11
 
@@ -15,7 +15,7 @@ exactly as it should read in the final case. The block holds the question and an
15
15
  - ✍️ Answer:
16
16
  ```
17
17
 
18
- The same shape is used under a step, under a bullet of "Expected" and under the yaml block of "Values":
18
+ The same shape is used under a step, under a bullet of "Expected" and on the prose line above the `## Values` block:
19
19
 
20
20
  ```
21
21
  11. Delete the document with the "Delete" option of its "..." menu on the list page. [writes]
@@ -40,8 +40,52 @@ Rules for writing it:
40
40
  QA should confirm it later but nothing waits for it.
41
41
  - One question per block; narrow broad questions down instead of copying them ("which profile elements?" becomes
42
42
  "is the logo the only per-profile element in the left side bar?").
43
- - A value read from a page is not a question: write it as a statement marked `(observed, 🔍 to be confirmed by QA)`.
44
- It needs no TODO block; the reviewer confirms it by deleting the marker.
43
+ - A fact read from a page is not a question: either say it as a plain note `(seen on <profile>)`, or mark it
44
+ `(observed on <profile>, 🔍 to be confirmed: <what a person must decide>)` when a wrong reading would still pass.
45
+ It needs no TODO block. See "Marker or question, never both" below.
46
+
47
+ ## Where the block hangs
48
+ The parent line is the sentence the question is about: the panel shows it on its own, next to "The text above this
49
+ question", so it has to make sense without the rest of the document.
50
+ - **The immediate line, not an ancestor.** Hang the block under the one sentence that makes the claim. Under a
51
+ nested bullet, indent it under that bullet, not under the top-level one above it; when the claim only makes sense
52
+ with an umbrella sentence, write the claim itself as a top-level bullet and hang the block there.
53
+ - **One claim, one block.** Several bullets raising one question get one block, under the closest of them, with the
54
+ others' evidence in that block. Never end a long list with a single question that covers all of it.
55
+ - **A sentence that stands alone.** No "see above", no pronoun whose referent is in a sibling bullet.
56
+ - **Never inside the `## Values` yaml block.** A marker or a question there becomes part of the expected value: put
57
+ it on the prose line above the block.
58
+
59
+ ## Marker or question, never both
60
+ A sentence carries at most one "not settled yet" signal, and the two marks are alternatives:
61
+ - `🚫/❓ TODO`: there **is** a question. The parent sentence is a suggestion, the block says what to decide.
62
+ - `(observed on <profile>, 🔍 to be confirmed: <what a person must decide>)`: there is **no** question, only a check
63
+ somebody has to make later. The part after the colon is required: "to be confirmed by QA" alone tells a person
64
+ nothing, and the panel shows that part as "why it needs you".
65
+ - `(observed on <profile>, evidence: .explore/observations/<case-id>/<image>.png)`: the same, with a screenshot the
66
+ reviewer opens instead of going back to the page. Save it while exploring.
67
+ - `(seen on <profile>)`: a plain note. The run checks it, so nobody has to; it gets no colour and no panel row.
68
+
69
+ - Never put a marker inside a TODO block: not in the question, not in the evidence, not in a wrapped line. The words
70
+ "to be confirmed", "TBC", "TBD" and "not observed" are marker words even in prose there, and `pnpm case:check` fails
71
+ on them.
72
+ - Never put a marker on the sentence a block hangs under: keep the question and drop the marker.
73
+ - `👁️ not observed` is for an expectation that came from the notes and was never seen on a page. It is never the
74
+ evidence of a question.
75
+
76
+ ## Which sentences carry a marker
77
+ A marker is for exactly one thing: a fact a person must settle because a wrong reading would still pass. Ask yourself:
78
+ **if this line were read wrong, would the generated spec fail?**
79
+ - **It would fail** → no marker. Locators, the flow of a step, navigation, request URLs, readiness signals, whether an
80
+ element exists. The run is the confirmation. Leave `(seen on <profile>)` when where it was seen matters.
81
+ - **It would pass anyway** → marker, with the reason. The literals a check asserts (a `## Values` entry, the wording of
82
+ an `## Expected` bullet) and the identity of a `[T<n>]` target. This is the class that hides a wrong expectation
83
+ forever; `assertion-targets.md` records two real incidents of it.
84
+ - A `## Rules` bullet the agent inferred from a page carries a marker only when the rule changes what is asserted;
85
+ otherwise it is `(seen on <profile>)`. A rule a person stated never carries one.
86
+
87
+ After a passing run, `pnpm markers:confirm --case <id> --env <env>` settles the markers whose line the spec checks
88
+ (`// unconfirmed: <the sentence>`), so a person does not have to open the page again to see whether it is still true.
45
89
 
46
90
  ## How the user answers
47
91
  Two ways, and the agent must understand both on the next run:
@@ -69,7 +113,9 @@ answer, adds `(page shows <x> on <profile-id>)` and reports it as a finding. It
69
113
  ## Resolution order (no question goes to the user while a better step exists)
70
114
  1. **The page answers it.** Facts such as exact labels, which controls exist, where the logo is, what a menu holds
71
115
  are answered by exploration (`/explore-case`; needs an environment and a logged-in profile; the agent only reads, a person does the steps that write). Rewrite the parent sentence
72
- with the fact, mark it `(observed, 🔍 to be confirmed by QA)` and remove the block.
116
+ with the fact and remove the block. Then decide the mark by "Which sentences carry a marker": a fact the run checks
117
+ gets `(seen on <profile>)`, a fact that would pass while wrong gets
118
+ `(observed on <profile>, 🔍 to be confirmed: <what a person must decide>)`.
73
119
  - A check with no expected value ("check X", "can see Y") becomes: the observed behaviour, marked as observed.
74
120
  A spec may be generated from it.
75
121
  - Values that a check asserts (logo name, label, option list) are observed per profile and written into the case's
@@ -101,13 +147,19 @@ answer, adds `(page shows <x> on <profile-id>)` and reports it as a finding. It
101
147
 
102
148
  ## Final report
103
149
  Report in the user's language. List every open block with its question and the parent sentence it would confirm, so
104
- the user can see what deleting it accepts. The last line of the final message is exactly one of:
150
+ the user can see what deleting it accepts. List the markers separately, each with its reason and whether it changes an
151
+ assertion: that is the short list a person actually has to look at. The last line of the final message is plain text
152
+ and nothing else, exactly one of:
105
153
 
106
154
  ```
107
155
  RESULT: ready
108
156
  RESULT: needs-input
109
157
  ```
110
158
 
159
+ Never put that line in a code fence or inline code, never make it bold, a heading or a list item, and put no label,
160
+ number or emoji in front of it: the panel reads it to show how the run ended. (The block above only lists the two
161
+ tokens; write the one that applies as the last line of your message, not the block.)
162
+
111
163
  - `ready`: no `[blocking]` question remains. Non-blocking ones may remain; list them so QA can confirm them.
112
164
  - `needs-input`: at least one `[blocking]` question remains in the case file (list each with its parent sentence),
113
165
  or the work could not be done (no environment, nobody logged in, or no permission to write files: a plan-mode run,
@@ -22,9 +22,10 @@ A check for something **missing, empty or hidden** is only valid after a positiv
22
22
  means: the list finished loading and has rows, and none is X.
23
23
 
24
24
  ## Exploring
25
- - Know the key requests: after the action, call `browser_network_requests`, pick the data calls (XHR/fetch to the
26
- app's API, the ones that return the content). Wait until they are finished; use `browser_network_clear` before the
27
- action so the list only holds what the action started.
25
+ - Know the key requests: after the action, call `browser_network_requests` (`static: false`), pick the data calls
26
+ (XHR/fetch to the app's API, the ones that return the content). Wait until they are finished. `browser_network_clear`
27
+ is not runnable in this MCP, so take a marker instead: read the list before the action, keep the highest number, and
28
+ after the action only the entries above it belong to it (`tools.md`, "Network requests without a clear").
28
29
  - Then wait for the content signal (`browser_wait_for` with text, or `browser_find` that the content is there), and
29
30
  take the snapshot only then. A snapshot taken right after a click is a frame, not a fact.
30
31
  - Observations record the ready signal: `(observed after <signal>, ...)`. A fact with no signal is not recorded.
@@ -0,0 +1,117 @@
1
+ # The browser tools this MCP can run, and the ones it only lists
2
+
3
+ Shared by `exploring-cases`, `generating-specs` and healing. It is binding in all three: read it before the first
4
+ browser call of a run.
5
+
6
+ `playwright run-test-mcp-server` answers `tools/list` with **every** browser tool Playwright has, but a call runs
7
+ inside a paused test, and that backend installs one capability set:
8
+
9
+ ```js
10
+ const config = { capabilities: ["testing"] }; // playwright/lib/mcp/test/browserBackend.js
11
+ const toolList = tools.filteredTools(config); // core* + testing, minus `skillOnly`
12
+ ```
13
+
14
+ Everything outside that set is listed and still cannot run: it answers `Tool "<name>" not found`. Two groups:
15
+ - a capability other than `core`/`testing` (`devtools`, `storage`, `network`, `pdf`, `vision`, `config`);
16
+ - `skillOnly: true` tools, which the backend never installs (`browser_network_clear`, `browser_console_clear`).
17
+
18
+ **`Tool "<name>" not found` is not a broken MCP server.** The server is up and every other tool works. Never stop
19
+ with `RESULT: needs-input` for it, never report a missing MCP server, and never fall back to an ad-hoc Playwright
20
+ script (it sees a different browser and its network view is not `browser_network_requests`, see A2). Use the
21
+ substitute below instead, and say in the report which substitute you used.
22
+
23
+ `pnpm asrun doctor` prints how many tools the installed Playwright actually runs and warns when this file names one
24
+ it cannot (it happens when Playwright changes the filter; the file then needs the same edit).
25
+
26
+ ## Runnable
27
+
28
+ Navigation and reading:
29
+ `browser_navigate`, `browser_navigate_back`, `browser_snapshot`, `browser_find`, `browser_evaluate`,
30
+ `browser_run_code_unsafe`, `browser_tabs`, `browser_resize`, `browser_close`, `browser_handle_dialog`,
31
+ `browser_console_messages`, `browser_emulate_media`, `browser_wait_for`.
32
+
33
+ Interaction: `browser_click`, `browser_type`, `browser_fill_form`, `browser_press_key`, `browser_hover`,
34
+ `browser_drag`, `browser_drop`, `browser_select_option`, `browser_file_upload`.
35
+
36
+ Screenshots: `browser_take_screenshot`.
37
+
38
+ Network: `browser_network_requests`, `browser_network_request`.
39
+
40
+ Test helpers: `browser_generate_locator`, `browser_verify_element_visible`, `browser_verify_text_visible`,
41
+ `browser_verify_list_visible`, `browser_verify_value`.
42
+
43
+ `browser_run_code_unsafe` takes Playwright's own API (a `page`, `context`, `expect` are in scope), which is what
44
+ every substitute below uses.
45
+
46
+ ## Listed but not runnable
47
+
48
+ | Tool | Why | Do this instead |
49
+ |---|---|---|
50
+ | `browser_highlight`, `browser_hide_highlight` | `devtools` | [Outline a target](#outlining-a-target-without-browser_highlight) |
51
+ | `browser_network_clear` | `skillOnly` | [Network requests without a clear](#network-requests-without-a-clear) |
52
+ | `browser_console_clear` | `skillOnly` | read `browser_console_messages` and note the messages you already saw |
53
+ | `browser_start_recording`, `browser_stop_recording` | `devtools` | the handoff card (`user-handoff.md`, "Recording") |
54
+ | `browser_annotate` | `devtools` | let the person name the element in the handoff card, then outline it yourself |
55
+ | `browser_start/stop_tracing`, `browser_start/stop_video`, `browser_video_*`, `browser_resume` | `devtools` | not needed by a case |
56
+ | `browser_cookie_*`, `browser_localstorage_*`, `browser_sessionstorage_*`, `browser_storage_state`, `browser_set_storage_state` | `storage` | never: saved logins are passwords (F5) |
57
+ | `browser_route`, `browser_unroute`, `browser_route_list`, `browser_network_state_set` | `network` | never: a case checks the real page |
58
+ | `browser_pdf_save` | `pdf` | a page that serves a PDF is downloaded and rendered (`pnpm pdf:render`) |
59
+ | `browser_mouse_move_xy`, `browser_mouse_down`, `browser_mouse_up`, `browser_mouse_wheel`, `browser_mouse_click_xy`, `browser_mouse_drag_xy` | `vision` | `browser_click` with a locator |
60
+ | `browser_get_config` | `config` | `pnpm status --env <env>` |
61
+
62
+ ## Arguments the schema requires
63
+
64
+ A call that looks complete still fails when one of these is missing. Both come from the MCP's own schema, and both
65
+ answer `Missing key`:
66
+
67
+ 1. **`intent` on every action, input and assertion tool.** The test runner adds it so the recorded step has a name.
68
+ One short sentence, imperative, about what this call is for: `intent: "open the article preview"`. It belongs on
69
+ `browser_navigate`, `browser_navigate_back`, `browser_click`, `browser_type`, `browser_fill_form`,
70
+ `browser_press_key`, `browser_hover`, `browser_drag`, `browser_drop`, `browser_select_option`,
71
+ `browser_file_upload`, `browser_evaluate`, `browser_run_code_unsafe`, `browser_tabs`, `browser_close`,
72
+ `browser_resize`, `browser_handle_dialog`, `browser_emulate_media`, and on `browser_wait_for` and every
73
+ `browser_verify_*`. The read-only tools (`browser_snapshot`, `browser_find`, `browser_take_screenshot`,
74
+ `browser_console_messages`, `browser_network_requests`, `browser_network_request`, `browser_generate_locator`) take
75
+ none.
76
+ 2. **A field with a default is published as required:**
77
+ - `browser_take_screenshot` → `scale: "css"` (CSS pixels, the size baselines expect).
78
+ - `browser_network_requests` → `static: false` (data calls only).
79
+ - `browser_network_request` → `index` (from the numbered list).
80
+ - `browser_tabs` → `action` (`"list"`, `"new"`, `"select"` or `"close"`).
81
+ - `browser_console_messages` → `level` (`"error"` is enough).
82
+ - `browser_handle_dialog` → `accept` (`true` to accept, `false` to dismiss).
83
+ - `generator_setup_page` → `project`, `seedFile` and `plan` (`plan: ""` when there is no plan).
84
+
85
+ ## Outlining a target without `browser_highlight`
86
+
87
+ `browser_highlight` is a one-line wrapper over `locator.highlight()`, and that method is public Playwright API. The
88
+ overlay is the real one, not an approximation, and nothing of the page is changed.
89
+
90
+ 1. Outline (one call, one element, the card's `Identified by` as the locator):
91
+ ```js
92
+ // browser_run_code_unsafe({intent: 'outline the target for the reference screenshot', code: ...})
93
+ async (page) => {
94
+ const target = page.getByRole('img', {name: '<the Identified by value of the card>'});
95
+ await target.highlight({style: 'outline: 3px solid #e11d48; outline-offset: 2px'});
96
+ return await target.toString();
97
+ }
98
+ ```
99
+ 2. Screenshot the page (not the element) so the reader sees where it sits:
100
+ `browser_take_screenshot({scale: 'css', filename: '.explore/targets/<case-id>/T<n>-<profile-id>.png'})`
101
+ 3. Remove it before the next step: `browser_run_code_unsafe` with `intent: 'remove the highlight overlay'` and
102
+ `async (page) => { await page.hideHighlight(); }`.
103
+
104
+ A highlight overlay is not page state: it never appears in a snapshot, it is not part of what the case checks, and
105
+ `page.hideHighlight()` clears all of them. Do this for every candidate when several elements are compared, and say in
106
+ the card what the screenshot shows.
107
+
108
+ ## Network requests without a clear
109
+
110
+ `browser_network_requests` returns a numbered list that only grows while the page stays open, so a marker works:
111
+
112
+ 1. Before the action: `browser_network_requests({static: false})`, note the highest number in the list (`mark`).
113
+ 2. Do the action.
114
+ 3. Read `browser_network_requests({static: false})` again and take the entries above `mark`: those, and only those,
115
+ were started by the action. `browser_network_request({index})` gives one request's detail and status.
116
+
117
+ Record the marker as a plain note in the report (`after 12 new requests; the data call was 7`), never in the case.
@@ -29,7 +29,9 @@ that nobody watches.
29
29
  copied with one click.
30
30
  3. Never ask the user to send email (production: never; other environments: only when `email` is `true` for the
31
31
  profile). Never ask for login or 2FA. Name everything the user creates `[asrun] <something>`.
32
- 4. Start recording with `browser_start_recording` (see "Recording").
32
+ 4. Do not start a recording yourself: the card starts and stops it (see "Recording"). Never call
33
+ `browser_start_recording` or `browser_stop_recording`: the `playwright-test` MCP lists them but cannot run them
34
+ (`devtools` tools; see `tools.md`).
33
35
 
34
36
  ## The wait loop
35
37
  Run it with one `browser_run_code_unsafe` call. A single call may wait for minutes (verified: 150 s without a
@@ -63,8 +65,9 @@ inside the call that runs the card: OpenCode reports a tool call only when it en
63
65
  way the web UI can see that a person is needed while the card waits. With the card alone there is no "Your turn"
64
66
  badge, no count in the tab title and no notification (a HINT in the job log says so).
65
67
 
66
- 1. Set the options on the browser context (this call only sets data):
68
+ 1. Set the options on the browser context (this call only sets data; `browser_run_code_unsafe` needs `intent`):
67
69
  ```js
70
+ // browser_run_code_unsafe({intent: 'set the handoff card options', code: ...})
68
71
  async (page) => {
69
72
  page.context().__handoffCfg = {
70
73
  message: '<what the user does, one sentence>',
@@ -77,11 +80,16 @@ badge, no count in the tab title and no notification (a HINT in the job log says
77
80
  ```
78
81
  2. Run the card and wait: `browser_run_code_unsafe` with `filename` set to the absolute path of
79
82
  `<framework>/.claude/skills/exploring-cases/references/handoff-card.js` (use the absolute path of the repository, a
80
- relative one is resolved against the workspace root, which may be the parent folder). It returns `{outcome: 'done' | 'skip' | 'condition' | 'timeout', waitedMs}`.
83
+ relative one is resolved against the workspace root, which may be the parent folder) and an `intent` such as
84
+ `'hand off step 5 to the person'`. It returns
85
+ `{outcome: 'done' | 'skip' | 'condition' | 'timeout', waitedMs, recording, recorded}`: `recorded` holds the
86
+ actions the person performed, one Playwright line each, across every tab (`page`, `page1`, ...), and is empty when
87
+ the recording was unavailable (`recording: false`; `recordingError` says why).
81
88
 
82
89
  Check once, after the card shows, that it looks right: take a screenshot and see a title bar with a move icon, a
83
- chevron button at its right and, when `copy` is not empty, a line per text with a copy icon. If not, stop and report
84
- it; do not hand off with a broken card.
90
+ chevron button at its right and, when `copy` is not empty, a line per text with a copy icon. Playwright's own recording
91
+ toolbar at the top of the page is expected and is not part of the card. If the card is not right, stop and report it;
92
+ do not hand off with a broken card.
85
93
 
86
94
  Tell the user in the chat (the terminal or the UI job log) as well: "Your turn in the browser window: <task>. Press
87
95
  Done on the card (bottom-left; drag it if it is in the way) when you finish." When the run was started from the web
@@ -105,17 +113,21 @@ The Done button is always there (also on the minimized card) as the fallback, an
105
113
  for two seconds before it is removed, so the user knows when they may let go.
106
114
  - `outcome: 'timeout'` or `'skip'`: do not guess. Write a `TODO [blocking]` under the step and go on with what does
107
115
  not depend on it.
108
- - Take a snapshot and write down what the user did (see "Recording"), then continue read-only.
116
+ - Take a snapshot, write down what the user did from the card's `recorded` (see "Recording"), then continue read-only.
109
117
  - At the end of the exploration hand off once more when the user created data: "Delete the [asrun] items you
110
118
  created, then press Done". The case must clean up after itself, and so must the exploration.
111
119
 
112
120
  ## Recording
113
- Between the start and the end of a handoff the agent records what the user does (`browser_start_recording`,
114
- `browser_stop_recording`). The recorded Playwright code is **raw material, never saved as it is**. It is turned into
115
- a general recipe and written under the step as `- Recipe (phase): ...` lines (format below). The recipe holds what the
116
- user did in words, plus the **labels and roles of the controls** they used ("the textbox 'Title'", "the button
117
- 'Next'"), because `/gen-spec` cannot write data and so cannot see those controls itself. It never holds selectors,
118
- ids or the names of data that can change.
121
+ The card records what the person does: it starts the recording when it shows and stops it on every way out (Done,
122
+ "Cannot do this", the finish condition, the timeout, an error), in the same `browser_run_code_unsafe` call. The agent
123
+ neither starts nor stops it. The result is the card's `recorded`: one Playwright line per action, `page` for the first
124
+ tab that acted and `page1`, `page2`, ... for the others. The aliases follow the order in which actions first appear,
125
+ not the order in which the tabs were opened, so read the actions before you describe tabs.
126
+
127
+ The recorded Playwright code is **raw material, never saved as it is**. It is turned into a general recipe and written
128
+ under the step as `- Recipe (phase): ...` lines (format below). The recipe holds what the user did in words, plus the
129
+ **labels and roles of the controls** they used ("the textbox 'Title'", "the button 'Next'"), because `/gen-spec` cannot
130
+ write data and so cannot see those controls itself. It never holds selectors, ids or the names of data that can change.
119
131
 
120
132
  Make the recipe as general as possible, because lists and data change:
121
133
  - **A choice from a list that can change** (items, search results, a contact list): record the position, never the
@@ -123,14 +135,31 @@ Make the recipe as general as possible, because lists and data change:
123
135
  - **Random is allowed only when the user asks for it** ("pick any 3"). The default is the position, because it can be
124
136
  repeated. A random pick must be written into the test's annotations when the spec runs.
125
137
  - **Text the case creates**: `[asrun] <something>`.
126
- - **A value that has to exist** (an existing name, a search term): write what it must be like ("a name that
127
- returns a result") and the observed value as `(observed "<value>", 🔍 to be confirmed by QA)` plus a
128
- `[non-blocking]` TODO block. Do not put a brittle literal in as the only truth.
138
+ - **A value that has to exist** (an existing name, a search term): the recipe says what it must be like ("a name
139
+ that returns a result") and notes the observed value as plain text `(seen on <profile>: "<value>")`. Do not put a
140
+ brittle literal in as the only truth. One claim carries one signal: when a `[non-blocking]` TODO under the recipe
141
+ asks whether any such value is acceptable, the observed value gets **no** marker (`open-questions.md`, "Marker or
142
+ question, never both").
129
143
  - **Nothing that depends on the pick** may become an assertion (a total changes with the items). Say so in a
130
144
  TODO when a check would.
131
145
  - Settings that the user changed on purpose (a toggle, a template) are recorded as they were set, by label.
132
146
 
133
- If the recording tool fails or returns nothing, describe what the user did from before/after snapshots instead.
147
+ The recording is raw: merge and prune it before you write the recipe.
148
+ - **The same action can appear twice**, and clicking a checkbox or its label can give both a `click` and a `check`.
149
+ Write it once ("check 'Agree'").
150
+ - **Side products**: clicking a textbox to focus it, `Cmd+C`, `Scroll`, `Escape`. Keep only actions that change data
151
+ or the page's state.
152
+ - **The card's own buttons are filtered out** of `recorded`. A click on the card's text (not a button) can still show
153
+ up; ignore it.
154
+ - **Not recorded at all**: drag and drop, native file pickers, OS dialogs. Describe those from before/after snapshots.
155
+
156
+ If `recording` is `false` or `recorded` is empty, do not silently write a recipe from snapshots: write a
157
+ `🚫 TODO [blocking]` under the step that says the step was not recorded (and why, from `recordingError` when there is
158
+ one). A recipe from before/after snapshots may be added next to that block only, and must say that it came from
159
+ snapshots, not from a recording.
160
+
161
+ If Playwright's own recording toolbar shows at the top of the page while the card waits, it is not AsRun's: leave it
162
+ alone.
134
163
 
135
164
  ## Recipe lines under a step
136
165
  One or more `- Recipe (phase): ...` lines directly under the `[input]` or `[writes]` step they belong to, indented by
@@ -139,7 +168,7 @@ keep in sync.
139
168
 
140
169
  ```
141
170
  3. Select an existing document and pick 3 items. [input]
142
- - Recipe (start): search for an existing document and pick result #1 (observed search term "...", 🔍 to be confirmed by QA).
171
+ - Recipe (start): search for an existing document and pick result #1 (seen on example-staging: "quarterly report").
143
172
  - ❓ TODO [non-blocking]: Is any result that opens fine as the starting point? (seen on example-staging)
144
173
  - ✍️ Answer:
145
174
  - Recipe (items): select 3 items by position (#1 to #3) from the list.
@@ -8,8 +8,9 @@ description: Turns a human-written test case (cases/<id>.md, the id may include
8
8
  Read `references/conventions.md` first. It defines the spec shape, fixtures, assertion layers and
9
9
  the read-only rules. Read the workspace `AGENTS.md` for the hard rules and the project rules. Read `../exploring-cases/references/open-questions.md`: it
10
10
  defines how open questions are written into the case file, resolved and reported. Also read
11
+ `../exploring-cases/references/case-rules.md` (how a case's `## Rules` are read, applied and maintained),
11
12
  `../exploring-cases/references/assertion-targets.md` (the exact thing each check looks at) and
12
- `../exploring-cases/references/readiness.md` (wait for the content, verify downloads in five parts). All three are binding here.
13
+ `../exploring-cases/references/readiness.md` (wait for the content, verify downloads in five parts). All four are binding here.
13
14
 
14
15
  This skill never stops to ask the user in the chat, because it may run without anybody present (a one-shot
15
16
  `claude -p` / `opencode run`). This holds whatever mode or agent the run is in: plan mode changes what you may write,
@@ -36,15 +37,18 @@ and the spec, and runs only the profiles that are logged in, and only when the r
36
37
  ## Before you start
37
38
  Read the workspace `AGENTS.md`: its §2 holds the project rules, and it points at the framework baseline. If
38
39
  `.claude/skills/generating-specs.md` exists in the workspace, read it and apply it after this skill. Then apply
39
- `AGENTS.md` §2, which wins over both (the baseline, "Precedence"). F1–F6 always win. This is not a stop condition:
40
- continue with the procedure below.
40
+ `AGENTS.md` §2, which wins over both (the baseline, "Precedence"). F1–F6 and R2 always win. This is not a stop
41
+ condition: continue with the procedure below.
41
42
 
42
43
  ## Inputs
43
44
  - The case id and the environment to explore in (`staging` or `production`). There is no default. If the
44
45
  environment is missing, stop before exploring and report `RESULT: needs-input` ("pass the environment").
45
46
  - Optionally `--force`: regenerate the spec from scratch and overwrite the existing one. See "An existing spec".
47
+ - Optionally `--hints <file>` (or free text after the arguments): what the person knows, or a change they want. Read it
48
+ before validating the case. It guides this run; after the work, a reusable instruction is written into `## Rules`
49
+ by the gate in `../exploring-cases/references/case-rules.md`, and a one-off clue stays in the report.
46
50
  - No profile selection: the profile scope is the case's (see above). A case id and an environment are all this command
47
- takes besides `--force`.
51
+ takes besides `--force` and `--hints`.
48
52
 
49
53
  A case id is its path below `cases/` without `.md`: `hub-landing`, or `documents/landing-page` when the case is in a
50
54
  folder (one level only). The spec is at the same path below `tests/` (`tests/documents/landing-page.spec.ts`); both import
@@ -80,14 +84,21 @@ Decide this first, before exploring anything. The spec path is `tests/<id>.spec.
80
84
  2. **Validate the case.** Collect everything that is ambiguous, every check without an expected value and every
81
85
  missing per-profile value, including the `TODO` items already in the case. Resolve them in the order of
82
86
  `open-questions.md`; do not stop here:
87
+ - **Apply the rule pass (`R1`–`R4`).** Read `## Rules` before the steps. A rule that states an expectation, a
88
+ literal, a target or a real step belongs in that section: move it there (or ask). A rule that conflicts with the
89
+ case or the page is a `[blocking]` question, never an override. A rule that would write, send email or do a
90
+ person's step is never followed. Details: `case-rules.md`.
83
91
  - **Steps that write or need a choice (`[input]`, `[writes]`) need a recipe.** Each must have at least one
84
92
  `- Recipe (phase): ...` line under it in the case (written by `/explore-case`). An old case may still hold the
85
93
  same recipe in `## Inputs`: use that line until it is moved under the step. If one has none, the case was
86
94
  never explored with a person: stop before opening a browser, do not write the spec, and report `RESULT:
87
95
  needs-input` with exactly this: "step <n> has no recorded input; run `/explore-case <id> <env> --attended`
88
96
  first". Nothing else is written.
89
- - Facts the page can answer and that the case does not hold yet are read in step 4 and written into the case as
90
- `(observed, 🔍 to be confirmed by QA)`. A spec is generated from observed values.
97
+ - Facts the page can answer and that the case does not hold yet are read in step 4 and written into the case. Mark
98
+ them by the gate in `open-questions.md` ("Which sentences carry a marker"): a locator, a flow or a request is
99
+ `(seen on <profile>)`; a literal the spec asserts gets
100
+ `(observed on <profile>, 🔍 to be confirmed: <what a person must decide>)`, and that is the only thing a person
101
+ has to settle. A spec is generated from observed values.
91
102
  - Safe defaults are applied: the suggestion is the sentence above the TODO block, and the block stays in the case
92
103
  as `❓ TODO [non-blocking]` with an empty `✍️ Answer:`. A sentence with no TODO block under it is confirmed text.
93
104
  - A non-empty `Answer:` is the user's answer: it wins over the sentence above it. Fold it into the case.
@@ -111,15 +122,19 @@ Decide this first, before exploring anything. The spec path is `tests/<id>.spec.
111
122
  `not verified on the page` in the report. If `pnpm status` cannot run (a sandbox error such as `listen EPERM` or a
112
123
  Chromium start failure), judge by the page instead: a page that lands on the login page means the profile is not
113
124
  logged in.
114
- 4. **Replay the case on the real page** with the `playwright-test` MCP. If its tools are not registered in this
115
- session (no `browser_*` / `generator_*` tool) or it reports that it failed to start, do not fall back to ad-hoc
116
- Playwright scripts: stop, tell the user which MCP server is missing and to run `pnpm asrun doctor`, write no spec,
117
- and end with `RESULT: needs-input`.
125
+ 4. **Replay the case on the real page** with the `playwright-test` MCP. Read `../exploring-cases/references/tools.md`
126
+ first: that MCP lists tools it cannot run, and a call that answers `Tool "<name>" not found` means exactly that (the
127
+ server is up; use the substitute). If its tools are not registered in this session at all (no `browser_*` /
128
+ `generator_*` tool) or it reports that it failed to start, do not fall back to ad-hoc Playwright scripts: stop, tell
129
+ the user which MCP server is missing and to run `pnpm asrun doctor`, write no spec, and end with
130
+ `RESULT: needs-input`.
118
131
  Call `generator_setup_page` with
119
- `project=<profile-id>` (for example `example-staging`) and `seedFile=tests/seed.spec.ts`, then perform the
132
+ `project=<profile-id>` (for example `example-staging`), `seedFile=tests/seed.spec.ts` and `plan=""`, then perform the
120
133
  steps in the environment you were given (never ask for it; there is no default) to learn the locators.
121
- Do the read-only steps as written. **Never perform a step that writes or needs a choice**: the agent does not write
122
- data, in staging or in production, and it does not hand off to a person here or wait for one. Write those steps
134
+ Do the read-only steps as written, and follow the `## Rules` that say how to prepare, wait or which control to
135
+ avoid. **Never perform a step that writes or needs a choice**: the agent does not write
136
+ data, in staging or in production, and it does not hand off to a person here or wait for one. A rule never
137
+ authorizes one of those steps. Write those steps
123
138
  of the spec from the `- Recipe:` lines under the step (positions, `[asrun]` names, control labels and roles that
124
139
  `/explore-case` recorded). Pages that only exist after such a step cannot be seen from here: write their locators
125
140
  from what the recipe and the exploration notes say, mark them in the report as "not verified on the page", and
@@ -130,16 +145,18 @@ Decide this first, before exploring anything. The spec path is `tests/<id>.spec.
130
145
  the case's `## Values`: open that profile's page only when a value or a behavior still has to be resolved for it.
131
146
  Keep every browser call short (one action, about 60 s at most, a snapshot between steps, `{timeout: 5000}` when
132
147
  something may not appear, no third retry): see "Keep every browser call short" in `../exploring-cases/SKILL.md`.
133
- **Ready before you read, here too** (`readiness.md`): `browser_network_clear`, act, wait for the key requests
134
- (`browser_network_requests`) and the content signal, then snapshot. The request URL patterns for `actAndWait` and the
135
- `Ready when` signals of the case are taken from what you see here.
148
+ **Ready before you read, here too** (`readiness.md`): take a marker on `browser_network_requests`
149
+ (`static: false`), act, wait for the key requests above that mark and the content signal, then snapshot. The request
150
+ URL patterns for `actAndWait` and the `Ready when` signals of the case are taken from what you see here.
136
151
  **Check every target on the page:** for each target card, find its `Identified by` locator, confirm that it matches
137
- exactly one element and that this element is what `What` says (highlight it and read the screenshot). A locator that
138
- matches several elements or the wrong one is fixed or becomes a question, never `.first()`.
152
+ exactly one element and that this element is what `What` says (outline it with `locator.highlight()` and look at the
153
+ screenshot, see `tools.md`). A locator that matches several elements or the wrong one is fixed or becomes a
154
+ question, never `.first()`.
139
155
  Prefer `getByTestId` (the project uses `data-tn`), then role/label locators. No CSS chains or XPath.
140
- Update the case file with what you learned: fold observations into the right section marked
141
- `(observed, 🔍 to be confirmed by QA)`, remove the TODO blocks they answer (rewrite the sentence above the block),
142
- and narrow the ones that remain. Keep the shape of `open-questions.md` for every block. Edit the case
156
+ Update the case file with what you learned: fold observations into the right section, marked `(seen on <profile>)`
157
+ or, for a literal the spec asserts, `(observed on <profile>, 🔍 to be confirmed: <what to decide>)`; remove the
158
+ TODO blocks they answer (rewrite the sentence above the block), and narrow the ones that remain. Keep the shape of
159
+ `open-questions.md` for every block, and never put a marker inside one. Edit the case
143
160
  with targeted edits; never touch its `## Source` table.
144
161
  5. **Complete the case's `## Values`**: for every profile the case holds information for, add the entry with the
145
162
  literals the spec asserts (the same keys as `default`, only what differs); put a literal that holds for every profile
@@ -154,7 +171,10 @@ Decide this first, before exploring anything. The spec path is `tests/<id>.spec.
154
171
  (`@asrunjs/asrun/values`), which merges `default` with the running profile's entry.
155
172
  Every action that loads something goes through `actAndWait` (`@asrunjs/asrun/ready`) with the key requests and the ready
156
173
  signal; every download goes through `verifyDownload` (`@asrunjs/asrun/downloads`); every check on a target has
157
- `// target: T<n>` on the line before it; every absence check has a `// ready: <signal>` before it. Never
174
+ `// target: T<n>` on the line before it; every absence check has a `// ready: <signal>` before it; code that exists
175
+ only because of a `## Rules` bullet carries `// rule: <short quote>` before it; an assertion whose literal comes
176
+ from a `🔍 to be confirmed` line carries `// unconfirmed: <that case sentence>` before it, so
177
+ `pnpm markers:confirm` can settle it from a passing run. Never
158
178
  `waitForTimeout`, `networkidle` or `.first()` to pick an image. A confirmation or modal that may or may not appear is
159
179
  never decided by one `isVisible()` look (its `timeout` option is ignored): use `clickOptionalUntilHidden`
160
180
  (`@asrunjs/asrun/ready`) or a waiting `expect(...).toBeVisible({timeout})`; see `readiness.md`, "Optional confirmations".
@@ -170,12 +190,16 @@ Decide this first, before exploring anything. The spec path is `tests/<id>.spec.
170
190
  8. **Report** in the user's language: spec path, **an assertion table** (step, what is asserted, target card, ready signal, and
171
191
  `observed on the page` or `not verified`; no row may be missing), profiles explored, profiles skipped (and why),
172
192
  the profiles of `profiles.yaml` the case holds no information for (they run on `default` alone), whether the case file was
173
- translated, what was added to the case file as observed, checks generated from observations (QA must confirm
174
- them), visual checks that need a baseline on the first run, and the remaining TODO blocks (`[blocking]` ones with
175
- the sentence that deleting the block would confirm). State that the spec was not run and give the command to run it:
193
+ translated, what was added to the case file as `(seen on <profile>)` and which lines still need a person
194
+ (`🔍 to be confirmed`, with the reason), checks generated from observations, visual checks that need a baseline on
195
+ the first run, and the remaining TODO blocks (`[blocking]` ones with
196
+ the sentence that deleting the block would confirm). Add the rule table of `case-rules.md`: every `## Rules`
197
+ bullet, where it was applied or why it was not, and which rules were added from `--hints`. State that the spec was
198
+ not run and give the command to run it:
176
199
  `pnpm asrun --env <env> --profile <ids> --grep <id>` (with `--update-snapshots` once if the spec has visual
177
200
  checks, then run `pnpm baselines:review <id> --new` and open every new image: it must show what its target card says). The last line is `RESULT: ready` (no `[blocking]` question left, spec
178
- written and the checks above pass) or `RESULT: needs-input`, as defined in `open-questions.md`.
201
+ written and the checks above pass) or `RESULT: needs-input`, as defined in `open-questions.md`: plain text on the
202
+ last line, never inside a code fence or in emphasis.
179
203
 
180
204
  ## Cases that write data
181
205
  Only when the case front matter lists environments under `writes` (for example `writes: [staging]`). Never add
@@ -197,7 +221,8 @@ reason to do nothing when the user asked for something.
197
221
  Heal from evidence, not from a habit of rerunning. A report that already exists says what failed.
198
222
 
199
223
  1. **Read the user's request first** (the `--hints` file, or the free text). It says what to repair or improve and may
200
- hold evidence the report lacks. Decide which kind of work it is:
224
+ hold evidence the report lacks. Read the case's `## Rules` before the steps and apply the rule pass (`R1`–`R4`,
225
+ `../exploring-cases/references/case-rules.md`). Decide which kind of work it is:
201
226
  - a failure to repair (the last run failed, or the user names one);
202
227
  - an improvement, even when the last run passed: a check that is too loose, a step, expectation, value or target
203
228
  that no longer matches the page, wording a person cannot follow;
@@ -228,7 +253,9 @@ Heal from evidence, not from a habit of rerunning. A report that already exists
228
253
  improvement request with no failure behind it. Use the `playwright-test` MCP exactly as `/gen-spec` step 4 describes
229
254
  (`generator_setup_page` with `project=<profile-id>`, one action per call, ready before you read). Do the read-only
230
255
  steps only: **never perform a step that writes or needs a choice**, in any environment. What you see goes into the
231
- case as `(observed, 🔍 to be confirmed by QA)`, so the person can confirm it in the review gate below.
256
+ case by the gate in `open-questions.md` ("Which sentences carry a marker"): `(seen on <profile>)` for a locator, a
257
+ flow or a request, and `(observed on <profile>, 🔍 to be confirmed: <what a person must decide>)` only for a
258
+ literal the spec asserts, so the person settles that in the review gate below.
232
259
  5. **Audit the assertion before you blame anyone.** Read the `assertion-targets.md` and `readiness.md` sections on healing.
233
260
  - Is the locator still what the target card says (`What`, `Identified by`)? Open the failure screenshot and the
234
261
  baseline (`pnpm baselines:review <case-id>`) and look at each: a baseline that shows something other than the card
@@ -250,12 +277,22 @@ Heal from evidence, not from a habit of rerunning. A report that already exists
250
277
  more than a sentence (a new or removed step, different expected values, another target), regenerate the spec from
251
278
  the case as "## Procedure" steps 5–6 describes instead of patching it; otherwise edit it in place. Say in the
252
279
  report which of the two you did.
280
+ - A reusable instruction that is method, not an expectation: append it to `## Rules` under the four-question gate
281
+ of `case-rules.md`, in general terms, after checking that it is not already there. An agent-inferred rule
282
+ carries a marker only when it changes what is asserted; a method rule is `(seen on <profile>)` and a rule the
283
+ person stated carries nothing. A one-off request (this failure, this
284
+ account, this run) stays in the report. Never put a write, an email, a person's step, an expectation or a literal
285
+ into `## Rules`.
253
286
  7. **Stop at the review gate.** After editing the case, read it back. If **this run** left a `🚫 TODO [blocking]` or
254
- `❓ TODO [non-blocking]` block, or a `🔍 to be confirmed by QA` or `👁️ not observed` marker, do not edit or
287
+ `❓ TODO [non-blocking]` block, or a `🔍 to be confirmed` or `👁️ not observed` marker, do not edit or
255
288
  regenerate the spec any further: a case that still asks something is not a source for a spec. Report exactly what
256
289
  to review, that answering or deleting those blocks comes first, and that the next command is
257
- `/explore-case <case-id> <env>` (with `--attended` when a step writes data). End with `RESULT: needs-input`. Blocks
258
- and markers that were already in the case before this run and that it did not touch do not stop it.
290
+ `/explore-case <case-id> <env>` (with `--attended` when a step writes data), or
291
+ `pnpm markers:confirm --case <case-id> --env <env>` when a passing run already checks those lines. End with
292
+ `RESULT: needs-input`. Blocks and markers that were already in the case before this run and that it did not touch do
293
+ not stop it. A plain `(seen on <profile>)` note never stops it. One exception:
294
+ a `🔍 to be confirmed` marker inside `## Rules` does not stop the spec work unless the rule covers a literal the
295
+ spec asserts (a `🚫`/`❓` TODO block under a rule still does).
259
296
  8. **After editing the spec, run it once to confirm** (same command as in step 3, for the profiles you healed). That rerun
260
297
  is the only one that is always allowed. Run `pnpm typecheck`, `pnpm spec:lint <case-id>` and `pnpm case:check`, and
261
298
  fix every failure of the checks you touched. An improvement may make the run fail on purpose: a tightened check that
@@ -264,8 +301,9 @@ Heal from evidence, not from a habit of rerunning. A report that already exists
264
301
  whether the case was run again and why (or why not), what the request asked for and what came of it, what was
265
302
  changed in the case and in the spec (and whether the spec was regenerated), what is a product finding (with paths),
266
303
  which assertions were wrong or raced the page, which baselines are wrong and were deleted, what the user's request
267
- contributed, which profiles were left out because they are not logged in, and the result of the
268
- confirming run. The last line is `RESULT: ready` (nothing left for a person, the spec changed and its checks pass)
304
+ contributed, the rule table of `case-rules.md` (every `## Rules` bullet, where it was applied or why it was not,
305
+ and which rules this run added or changed), which profiles were left out because they are not logged in, and the
306
+ result of the confirming run. The last line is `RESULT: ready` (nothing left for a person, the spec changed and its checks pass)
269
307
  or `RESULT: needs-input` (the case waits for a review, as in step 7, or a person has to decide something). A run
270
308
  that could not write files at all (plan mode, a read-only sandbox, a refused permission) is `needs-input` too:
271
309
  never end with a plan and a question instead.
@@ -108,6 +108,10 @@ Binding rules are in `../exploring-cases/references/readiness.md` and `.../asser
108
108
  - A check on a logo, image, file or screenshot has `// target: T<n>` on the line before it (a card in the case's
109
109
  `## Targets`). The element is found by the card's `Identified by` (role and name, test id, alt/title), never `.first()` or
110
110
  a bare `img`/`svg`. Semantic assertion first (name/alt/src, where it is), pixels second, on that element only.
111
+ - An assertion whose literal comes from a `🔍 to be confirmed` line in the case carries
112
+ `// unconfirmed: <that case sentence>` before it (one line, the sentence without its marker). It is what
113
+ `pnpm markers:confirm --case <id> --env <env>` reads after a passing run to settle the marker, so the line must be
114
+ the case's own wording, not a paraphrase.
111
115
  - A check for absence ("no", "missing", "empty", "hidden") has `// ready: <signal>` in front of it and a positive wait
112
116
  before it. The page must have loaded before "nothing" means anything.
113
117
  - A download is `await verifyDownload(page, () => option.click(), {kind: 'pdf'})`. It checks, and fails the step on