@asrunjs/asrun 0.8.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 (70) hide show
  1. package/.claude/commands/explore-case.md +14 -5
  2. package/.claude/commands/gen-spec.md +12 -5
  3. package/.claude/commands/heal-spec.md +23 -8
  4. package/.claude/commands/make-case.md +1 -1
  5. package/.claude/skills/exploring-cases/SKILL.md +82 -37
  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 +60 -8
  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 +148 -55
  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 +68 -20
  19. package/README.md +8 -6
  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 +57 -18
  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 +9 -3
  38. package/dist/ui/shared/ai-commands.js +25 -11
  39. package/dist/ui/shared/types.d.ts +18 -3
  40. package/docs/agents/case-format.md +39 -5
  41. package/docs/ai-workflow.md +80 -21
  42. package/docs/cases.md +28 -4
  43. package/docs/commands.md +6 -1
  44. package/docs/theme-guide.html +93 -5
  45. package/docs/troubleshooting.md +47 -3
  46. package/docs/web-ui.md +55 -10
  47. package/package.json +2 -1
  48. package/templates/.claude/commands/explore-case.md +14 -4
  49. package/templates/.claude/commands/gen-spec.md +12 -4
  50. package/templates/.claude/commands/heal-spec.md +27 -13
  51. package/templates/.claude/commands/make-case.md +1 -1
  52. package/templates/AGENTS.md +9 -6
  53. package/templates/cases/_template.md +7 -0
  54. package/templates/package.json +1 -0
  55. package/ui/dist/assets/{dist-ChBHX4rh.js → dist-2msWI-5M.js} +1 -1
  56. package/ui/dist/assets/{dist-DoChb6QQ.js → dist-4U_gPHXf.js} +1 -1
  57. package/ui/dist/assets/{dist-CJiwxr1r.js → dist-BMfJatHo.js} +1 -1
  58. package/ui/dist/assets/{dist-N-o4QIaZ.js → dist-BVvHVJ3N.js} +1 -1
  59. package/ui/dist/assets/{dist-BC_P1Gnf.js → dist-BmWaNjqO.js} +1 -1
  60. package/ui/dist/assets/{dist-Do_MhHKF.js → dist-BwNz_xW2.js} +1 -1
  61. package/ui/dist/assets/{dist-D6jmnDr-.js → dist-J36xayOv.js} +1 -1
  62. package/ui/dist/assets/{dist-CavmZIv7.js → dist-e-KhTWHJ.js} +1 -1
  63. package/ui/dist/assets/{dist-BpkTvixI.js → dist-sfZKBSMm.js} +1 -1
  64. package/ui/dist/assets/{dist-BgPbx2GZ.js → dist-wjB7g6OJ.js} +1 -1
  65. package/ui/dist/assets/index-CGbRJT3b.js +348 -0
  66. package/ui/dist/assets/index-CI0i0LWc.css +2 -0
  67. package/ui/dist/help/case-explore.png +0 -0
  68. package/ui/dist/index.html +2 -2
  69. package/ui/dist/assets/index-Cy2l91qV.css +0 -2
  70. package/ui/dist/assets/index-DGQ5logJ.js +0 -338
@@ -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,16 +147,22 @@ 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
- or the work could not be done (no environment, nobody logged in). Say what the user should edit in the case file,
114
- then re-run.
165
+ or the work could not be done (no environment, nobody logged in, or no permission to write files: a plan-mode run,
166
+ a read-only sandbox). Say what the user should edit in the case file, then re-run.
115
167
 
116
168
  Do not write anything after the `RESULT:` line.
@@ -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.