@asrunjs/asrun 0.9.0 → 0.11.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 (78) 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 +84 -63
  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/notes.md +87 -0
  10. package/.claude/skills/exploring-cases/references/open-questions.md +58 -6
  11. package/.claude/skills/exploring-cases/references/readiness.md +7 -5
  12. package/.claude/skills/exploring-cases/references/tools.md +151 -0
  13. package/.claude/skills/exploring-cases/references/user-handoff.md +47 -18
  14. package/.claude/skills/generating-specs/SKILL.md +99 -54
  15. package/.claude/skills/generating-specs/references/conventions.md +4 -0
  16. package/.claude/skills/making-cases/SKILL.md +14 -7
  17. package/.claude/skills/making-cases/references/gaps.md +7 -4
  18. package/.claude/skills/making-cases/references/normalizing.md +1 -1
  19. package/AGENTS.md +61 -20
  20. package/README.md +10 -8
  21. package/bin/asrun.mjs +2 -0
  22. package/dist/lib/case-doc.d.ts +41 -4
  23. package/dist/lib/case-doc.js +109 -17
  24. package/dist/lib/explore-notes.d.ts +74 -0
  25. package/dist/lib/explore-notes.js +242 -0
  26. package/dist/lib/paths.d.ts +2 -0
  27. package/dist/lib/paths.js +2 -0
  28. package/dist/scripts/check-cases.js +89 -1
  29. package/dist/scripts/cli.js +9 -2
  30. package/dist/scripts/doctor.js +50 -3
  31. package/dist/scripts/explore-notes.d.ts +1 -0
  32. package/dist/scripts/explore-notes.js +131 -0
  33. package/dist/scripts/markers-confirm.d.ts +1 -0
  34. package/dist/scripts/markers-confirm.js +139 -0
  35. package/dist/ui/server/actions.js +50 -16
  36. package/dist/ui/server/app.js +7 -2
  37. package/dist/ui/server/job-store.d.ts +60 -0
  38. package/dist/ui/server/job-store.js +198 -0
  39. package/dist/ui/server/jobs.d.ts +46 -4
  40. package/dist/ui/server/jobs.js +361 -59
  41. package/dist/ui/server/process-info.d.ts +17 -0
  42. package/dist/ui/server/process-info.js +126 -0
  43. package/dist/ui/server/result-state.d.ts +10 -0
  44. package/dist/ui/server/result-state.js +106 -0
  45. package/dist/ui/shared/ai-commands.d.ts +4 -1
  46. package/dist/ui/shared/ai-commands.js +14 -4
  47. package/dist/ui/shared/types.d.ts +17 -2
  48. package/docs/agents/case-format.md +40 -5
  49. package/docs/agents/framework-dev.md +4 -0
  50. package/docs/ai-workflow.md +56 -16
  51. package/docs/cases.md +32 -8
  52. package/docs/commands.md +17 -1
  53. package/docs/theme-guide.html +56 -0
  54. package/docs/troubleshooting.md +40 -0
  55. package/docs/web-ui.md +59 -15
  56. package/package.json +3 -1
  57. package/templates/.claude/commands/explore-case.md +11 -6
  58. package/templates/.claude/commands/gen-spec.md +10 -5
  59. package/templates/.claude/commands/heal-spec.md +11 -7
  60. package/templates/.claude/commands/make-case.md +1 -1
  61. package/templates/AGENTS.md +9 -6
  62. package/templates/cases/_template.md +7 -0
  63. package/templates/package.json +2 -0
  64. package/ui/dist/assets/{dist-WvvCZeeu.js → dist--BXJInTq.js} +1 -1
  65. package/ui/dist/assets/{dist-B7HeglDd.js → dist-BMmNFIUH.js} +1 -1
  66. package/ui/dist/assets/{dist-DevASegc.js → dist-BXNaYHeh.js} +1 -1
  67. package/ui/dist/assets/{dist-Bb36rNUw.js → dist-ByLbEOlF.js} +1 -1
  68. package/ui/dist/assets/{dist-Dqc_tfzC.js → dist-C9ra6_BD.js} +1 -1
  69. package/ui/dist/assets/{dist-DfBpjgZT.js → dist-CK8mow8u.js} +1 -1
  70. package/ui/dist/assets/{dist-G9KNeh6u.js → dist-CQS0So-i.js} +1 -1
  71. package/ui/dist/assets/{dist-GvRUDL-2.js → dist-CTrwRc3Q.js} +1 -1
  72. package/ui/dist/assets/{dist-B9_GXR2G.js → dist-DTYj2vUD.js} +1 -1
  73. package/ui/dist/assets/{dist-Dq4Vnw5N.js → dist-mV0_nQZe.js} +1 -1
  74. package/ui/dist/assets/index-BzMqkmQ9.js +348 -0
  75. package/ui/dist/assets/index-DcJR-8Mt.css +2 -0
  76. package/ui/dist/index.html +2 -2
  77. package/ui/dist/assets/index-BJ5D2mFK.js +0 -346
  78. package/ui/dist/assets/index-BXY-eKBn.css +0 -2
@@ -9,6 +9,52 @@ async (page) => {
9
9
  const maxMs = cfg.maxMs || 600000;
10
10
  await ctx.grantPermissions(['clipboard-read', 'clipboard-write']).catch(() => {});
11
11
 
12
+ // Record what the person does. The card owns both ends of the recording, so every way out stops it and no handoff
13
+ // leaks a recording into the next one (see user-handoff.md, "Recording"). Never call browser_start_recording: the
14
+ // playwright-test MCP lists it but cannot run it (devtools-only; see tools.md in this folder).
15
+ const CARD_CONTROL = /AsRun handoff:/i;
16
+ const entries = [];
17
+ const entry = (action, code) => ({
18
+ code,
19
+ card: CARD_CONTROL.test(String((action && action.selector) || '')),
20
+ });
21
+ const sink = {
22
+ actionAdded: (p, action, code) => entries.push(entry(action, code)),
23
+ actionUpdated: (p, action, code) => {
24
+ const next = entry(action, code);
25
+ if (entries.length) entries[entries.length - 1] = next;
26
+ else entries.push(next);
27
+ },
28
+ signalAdded: (p, signal, code) => {
29
+ const last = entries[entries.length - 1];
30
+ if (last && code && !last.card) last.code = code;
31
+ },
32
+ };
33
+ let recording = false;
34
+ let recordingError = null;
35
+ // Library-mode code (`page`, `page1`, ...) keeps the `newPage`/`close` lines of tabs the person opened by hand; the
36
+ // test-runner generator drops `openPage`/`closePage`.
37
+ const recordingOptions = {language: 'javascript'};
38
+ try {
39
+ if (typeof ctx._startRecording !== 'function' || typeof ctx._stopRecording !== 'function')
40
+ throw new Error('this Playwright has no browser-context recording');
41
+ await ctx._startRecording(recordingOptions, sink);
42
+ recording = true;
43
+ } catch (e) {
44
+ // A card that died before stopping left the context recording: close that one and try once more.
45
+ if (/already in progress/i.test(String(e && e.message))) {
46
+ await ctx._stopRecording().catch(() => {});
47
+ try {
48
+ await ctx._startRecording(recordingOptions, sink);
49
+ recording = true;
50
+ } catch (e2) {
51
+ recordingError = String((e2 && e2.message) || e2);
52
+ }
53
+ } else {
54
+ recordingError = String((e && e.message) || e);
55
+ }
56
+ }
57
+
12
58
  const paint = (p) => p.evaluate(({message, copy}) => {
13
59
  if (document.getElementById('__ASRUN_panel')) return;
14
60
  const KEY = '__ASRUN_panel_state';
@@ -41,9 +87,9 @@ async (page) => {
41
87
  '.row{display:flex;gap:8px}.row .primary{background:#15803d}.row .primary:hover{background:#16a34a}' +
42
88
  '</style>' +
43
89
  '<div class="card" id="card"><div class="bar" id="bar" title="Drag to move"><span class="grip">' + ICON.move + '</span><b id="title">YOUR TURN</b>' +
44
- '<button class="mini-done" data-a="done">Done</button><button id="toggle" title="Minimize or expand" aria-label="Minimize or expand"></button></div>' +
90
+ '<button class="mini-done" data-a="done" aria-label="AsRun handoff: done">Done</button><button id="toggle" title="Minimize or expand" aria-label="AsRun handoff: minimize or expand"></button></div>' +
45
91
  '<div class="body"><div id="msg"></div><div id="chips"></div><div class="row">' +
46
- '<button class="primary" data-a="done">Done</button><button data-a="skip">Cannot do this</button></div></div></div>';
92
+ '<button class="primary" data-a="done" aria-label="AsRun handoff: done">Done</button><button data-a="skip" aria-label="AsRun handoff: cannot do this">Cannot do this</button></div></div></div>';
47
93
  const card = root.getElementById('card'), bar = root.getElementById('bar'), toggle = root.getElementById('toggle');
48
94
  root.getElementById('msg').textContent = message;
49
95
  const chips = root.getElementById('chips');
@@ -51,7 +97,7 @@ async (page) => {
51
97
  for (const text of copy) {
52
98
  const chip = document.createElement('div');
53
99
  chip.className = 'chip';
54
- chip.innerHTML = '<span></span><button aria-label="Copy to clipboard" title="Copy">' + ICON.copy + '</button>';
100
+ chip.innerHTML = '<span></span><button aria-label="AsRun handoff: copy to clipboard" title="Copy">' + ICON.copy + '</button>';
55
101
  chip.firstChild.textContent = text;
56
102
  const btn = chip.lastChild;
57
103
  btn.addEventListener('click', async () => {
@@ -107,14 +153,18 @@ async (page) => {
107
153
  for (const p of ctx.pages()) await p.evaluate(() => { window.__handoff = null; }).catch(() => {});
108
154
  const started = Date.now();
109
155
  let outcome = null;
110
- while (!outcome && Date.now() - started < maxMs) {
111
- for (const p of ctx.pages()) {
112
- await paint(p);
113
- const flag = await p.evaluate(() => window.__handoff || null).catch(() => null);
114
- if (flag) outcome = flag;
115
- else if (doneText && await p.getByText(doneText).first().isVisible().catch(() => false)) outcome = 'condition';
156
+ try {
157
+ while (!outcome && Date.now() - started < maxMs) {
158
+ for (const p of ctx.pages()) {
159
+ await paint(p);
160
+ const flag = await p.evaluate(() => window.__handoff || null).catch(() => null);
161
+ if (flag) outcome = flag;
162
+ else if (doneText && await p.getByText(doneText).first().isVisible().catch(() => false)) outcome = 'condition';
163
+ }
164
+ if (!outcome) await page.waitForTimeout(500);
116
165
  }
117
- if (!outcome) await page.waitForTimeout(500);
166
+ } finally {
167
+ if (recording) await ctx._stopRecording().catch(() => {});
118
168
  }
119
169
  for (const p of ctx.pages()) {
120
170
  await p.evaluate(() => {
@@ -124,5 +174,17 @@ async (page) => {
124
174
  }
125
175
  await page.waitForTimeout(2300);
126
176
  for (const p of ctx.pages()) await p.evaluate(() => sessionStorage.removeItem('__ASRUN_panel_state')).catch(() => {});
127
- return {outcome: outcome || 'timeout', waitedMs: Date.now() - started};
177
+ const dedent = (text) => {
178
+ const lines = text.split('\n');
179
+ const indents = lines.filter((line) => line.trim()).map((line) => line.length - line.trimStart().length);
180
+ const indent = indents.length ? Math.min(...indents) : 0;
181
+ return lines.map((line) => line.substring(indent)).join('\n');
182
+ };
183
+ return {
184
+ outcome: outcome || 'timeout',
185
+ waitedMs: Date.now() - started,
186
+ recording,
187
+ recorded: entries.filter((e) => e.code && e.code.trim() && !e.card).map((e) => dedent(e.code)),
188
+ ...(recordingError ? {recordingError} : {}),
189
+ };
128
190
  }
@@ -0,0 +1,87 @@
1
+ # The exploration note: what a run saw, in a form `/gen-spec` can read
2
+
3
+ Local, git-ignored and optional. `exploring-cases` writes it; `generating-specs` (and healing) read it to **verify**
4
+ instead of rediscovering the page. It is not part of the case and never becomes part of it: expected values stay in
5
+ `## Values`, behaviour in `## Expected`, ready signals in `Ready when:`, and assertions never appear here.
6
+
7
+ ## Where it lives
8
+
9
+ ```
10
+ .explore/notes/<case-id>/<profile-id>.json
11
+ ```
12
+
13
+ One file per profile. A `--profiles a,b` run writes only `a.json` and `b.json`; a later run with `c` adds `c.json`
14
+ next to them, so nothing is merged and an earlier observation is never overwritten. A fresh checkout has no notes, and
15
+ that is a normal state: generation falls back to replaying the case.
16
+
17
+ ## The shape
18
+
19
+ ```json
20
+ {
21
+ "version": 1,
22
+ "caseId": "conduit/header-navigation",
23
+ "profile": "example-staging",
24
+ "env": "staging",
25
+ "caseSha256": "<pnpm asrun explore:notes <case-id> --hash>",
26
+ "capturedAt": "2026-11-09T10:20:31Z",
27
+ "steps": {
28
+ "2": {
29
+ "url": "/",
30
+ "locators": [{"purpose": "the header navigation link", "locator": "page.getByRole('link', {name: 'Home'})"}],
31
+ "requests": ["/api/articles"]
32
+ }
33
+ },
34
+ "targets": {"T1": {"locator": "page.getByRole('img', {name: 'Example'})", "matches": 1}}
35
+ }
36
+ ```
37
+
38
+ - Keys of `steps` are the case's step numbers, keys of `targets` are its card ids (`T1`).
39
+ - `steps` may be `{}` and `targets` may be omitted; every other key is required (`env` and `updatedAt` are optional).
40
+ No other field is accepted.
41
+ - `caseSha256` is the 64-character hash `pnpm asrun explore:notes <case-id> --hash` prints (not the placeholder above).
42
+ - `pnpm asrun explore:notes <case-id> --check` validates the files: it fails on a malformed note and warns on a note
43
+ whose `caseSha256` no longer matches the case (the summary already prints it as `stale`; re-run the exploration, or
44
+ delete the note and let the case be replayed). A file that fails is ignored by `/gen-spec`.
45
+
46
+ ## What goes in
47
+
48
+ `steps.<n>`
49
+ - `url`: the path the step runs on, relative to `baseURL` (`/documents/`), when it is not the current page.
50
+ - `locators`: the element the step acts on, not every element on the page. `purpose` says what it is in words; `locator`
51
+ is exactly what `browser_generate_locator` returned. Prefer the stable controls (`getByRole`, `getByTestId`,
52
+ `getByLabel`) and one entry per action.
53
+ - `requests`: the data-call paths the step's action started, taken from `browser_network_requests` above the marker
54
+ (`tools.md`, "Network requests without a clear"). Path only: `/api/articles`, never a host, a query string or a token.
55
+ Images, fonts and analytics are not data calls.
56
+
57
+ `targets.T1`
58
+ - `locator`: the card's `Identified by`, confirmed on the page.
59
+ - `matches`: how many elements it matched — `1` for a completed card. A card with open doubts gets no entry.
60
+
61
+ ## What never goes in
62
+
63
+ - Expected values, counts, texts a check asserts: they belong in the case's `## Values` and `## Expected`.
64
+ - Ready signals: they belong in `Ready when:` (the note holds the request paths, not the promise).
65
+ - Assertions, test steps or a copy of the spec.
66
+ - Cookies, headers, storage state, tokens or a URL with a query string. `.explore/` is local, but a note is still not a
67
+ place for a credential (`F5`).
68
+ - A selector or an id that a future run would copy into `cases/*.md`. The case stays implementation-free; the note is
69
+ the only place a locator lives.
70
+
71
+ ## Lists and changing data
72
+
73
+ A list, a row or a search result is recorded by its container and position (`page.getByRole('row').nth(0)`), never by
74
+ the name of the item, because the data changes between runs. The same rule as a recipe (`user-handoff.md`).
75
+
76
+ ## Freshness and the write-back
77
+
78
+ - `caseSha256` is the sha256 of `cases/<case-id>.md` at the moment the note was written. If the case changes after that,
79
+ the note is **stale**: `/gen-spec` replays the case and does not use it.
80
+ - `/gen-spec` confirms an entry of a fresh note on the page and may write the verified locator or request back
81
+ (`updatedAt` gets the time). It never adds a step or a target, and it never touches a stale note.
82
+ - Deleting `.explore/notes/` is always safe: generation then re-derives everything from the page.
83
+
84
+ ## Who reads it
85
+
86
+ Both skills read this file and `tools.md`, `readiness.md` and `assertion-targets.md`; the details of a browser call are
87
+ in `tools.md`, never repeated here.
@@ -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.
@@ -62,8 +63,9 @@ await expectImageLoaded(logo);
62
63
 
63
64
  - Register the response wait **before** the action (`actAndWait` does).
64
65
  - Every action that opens a dialog, a tab, a menu with data, a list or a preview waits for its key requests and its content
65
- signal. The request URL patterns come from `browser_network_requests` during exploration, not from guesses; if none was
66
- seen (the action was not run), say "not verified on the page" in the report.
66
+ signal. The request URL patterns come from the exploration note (`notes.md`) or, when it holds none, from
67
+ `browser_network_requests` during this replay; never from guesses. If none was seen (the action was not run), say
68
+ "not verified on the page" in the report.
67
69
  - Never `waitForTimeout`, never `networkidle`. They wait for time or for silence, not for the content. `pnpm spec:lint` fails both.
68
70
  - Never `if (await x.isVisible()) ...` to decide something that may not have loaded yet. Wait for a ready signal, then branch.
69
71
  Where an element is genuinely optional, say what proves the page finished (`// ready: ...`) before looking for it.
@@ -0,0 +1,151 @@
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
+ ## Starting a session
27
+
28
+ Both `exploring-cases` and `generating-specs` (and healing) drive the browser through this MCP, one profile at a time.
29
+
30
+ 1. Check the session with `pnpm status --env <env>` (or `pnpm status` when the environment is the profile's). It can fail
31
+ with a sandbox error (`listen EPERM`, a Chromium start failure): then open the profile's page and judge by the login
32
+ page instead.
33
+ 2. Start the session with `generator_setup_page` (`project=<profile-id>`, `seedFile=tests/seed.spec.ts`, `plan=""`).
34
+ Every later call belongs to that profile.
35
+ 3. A profile that is not logged in is skipped and named in the report; a human runs `pnpm signin <profile-id>`. Never
36
+ explore a profile a selection leaves out (`--profiles` in exploration), and never add one to fill a gap.
37
+
38
+ If no `browser_*` / `generator_*` tool is registered at all, or the server reports that it failed to start, stop: tell
39
+ the user which MCP server is missing and to run `pnpm asrun doctor`, write nothing, and end with `RESULT: needs-input`.
40
+
41
+ ## Short calls
42
+
43
+ A call that hangs is the normal failure mode of this MCP, so keep every call small.
44
+
45
+ - One `browser_run_code_unsafe` call does one thing and finishes within about 60 seconds. Do not close a tab, click,
46
+ wait for a dialog and read it in one call: split it and take a `browser_snapshot` between the steps. Only the handoff
47
+ wait loop may take minutes.
48
+ - The browser times out an action or a lookup after 15 s (the config sets it). Pass `{timeout: 5000}` when you wait for
49
+ something that may not appear, and use `.catch()` or `isVisible()` for a one-time look **while exploring only**.
50
+ `isVisible()` answers for this instant and ignores any `timeout` passed to it, so take a snapshot or wait for a signal
51
+ after an action first. Never copy this pattern into a spec (`readiness.md`, "Optional confirmations").
52
+ - After a click that should open something, take a snapshot and see what opened before you wait for it by role. A dialog
53
+ that did not appear is a finding (what did open instead?), not something to wait for or retry.
54
+ - A step that fails or times out twice is not tried a third time: record what you saw and go on (a `TODO [blocking]`
55
+ under the step in a case, a `needs-input` report when writing a spec).
56
+
57
+ ## Runnable
58
+
59
+ Navigation and reading:
60
+ `browser_navigate`, `browser_navigate_back`, `browser_snapshot`, `browser_find`, `browser_evaluate`,
61
+ `browser_run_code_unsafe`, `browser_tabs`, `browser_resize`, `browser_close`, `browser_handle_dialog`,
62
+ `browser_console_messages`, `browser_emulate_media`, `browser_wait_for`.
63
+
64
+ Interaction: `browser_click`, `browser_type`, `browser_fill_form`, `browser_press_key`, `browser_hover`,
65
+ `browser_drag`, `browser_drop`, `browser_select_option`, `browser_file_upload`.
66
+
67
+ Screenshots: `browser_take_screenshot`.
68
+
69
+ Network: `browser_network_requests`, `browser_network_request`.
70
+
71
+ Test helpers: `browser_generate_locator`, `browser_verify_element_visible`, `browser_verify_text_visible`,
72
+ `browser_verify_list_visible`, `browser_verify_value`.
73
+
74
+ `browser_run_code_unsafe` takes Playwright's own API (a `page`, `context`, `expect` are in scope), which is what
75
+ every substitute below uses.
76
+
77
+ ## Listed but not runnable
78
+
79
+ | Tool | Why | Do this instead |
80
+ |---|---|---|
81
+ | `browser_highlight`, `browser_hide_highlight` | `devtools` | [Outline a target](#outlining-a-target-without-browser_highlight) |
82
+ | `browser_network_clear` | `skillOnly` | [Network requests without a clear](#network-requests-without-a-clear) |
83
+ | `browser_console_clear` | `skillOnly` | read `browser_console_messages` and note the messages you already saw |
84
+ | `browser_start_recording`, `browser_stop_recording` | `devtools` | the handoff card (`user-handoff.md`, "Recording") |
85
+ | `browser_annotate` | `devtools` | let the person name the element in the handoff card, then outline it yourself |
86
+ | `browser_start/stop_tracing`, `browser_start/stop_video`, `browser_video_*`, `browser_resume` | `devtools` | not needed by a case |
87
+ | `browser_cookie_*`, `browser_localstorage_*`, `browser_sessionstorage_*`, `browser_storage_state`, `browser_set_storage_state` | `storage` | never: saved logins are passwords (F5) |
88
+ | `browser_route`, `browser_unroute`, `browser_route_list`, `browser_network_state_set` | `network` | never: a case checks the real page |
89
+ | `browser_pdf_save` | `pdf` | a page that serves a PDF is downloaded and rendered (`pnpm pdf:render`) |
90
+ | `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 |
91
+ | `browser_get_config` | `config` | `pnpm status --env <env>` |
92
+
93
+ ## Arguments the schema requires
94
+
95
+ A call that looks complete still fails when one of these is missing. Both come from the MCP's own schema, and both
96
+ answer `Missing key`:
97
+
98
+ 1. **`intent` on every action, input and assertion tool.** The test runner adds it so the recorded step has a name.
99
+ One short sentence, imperative, about what this call is for: `intent: "open the article preview"`. It belongs on
100
+ `browser_navigate`, `browser_navigate_back`, `browser_click`, `browser_type`, `browser_fill_form`,
101
+ `browser_press_key`, `browser_hover`, `browser_drag`, `browser_drop`, `browser_select_option`,
102
+ `browser_file_upload`, `browser_evaluate`, `browser_run_code_unsafe`, `browser_tabs`, `browser_close`,
103
+ `browser_resize`, `browser_handle_dialog`, `browser_emulate_media`, and on `browser_wait_for` and every
104
+ `browser_verify_*`. The read-only tools (`browser_snapshot`, `browser_find`, `browser_take_screenshot`,
105
+ `browser_console_messages`, `browser_network_requests`, `browser_network_request`, `browser_generate_locator`) take
106
+ none.
107
+ 2. **A field with a default is published as required:**
108
+ - `browser_take_screenshot` → `scale: "css"` (CSS pixels, the size baselines expect).
109
+ - `browser_network_requests` → `static: false` (data calls only).
110
+ - `browser_network_request` → `index` (from the numbered list).
111
+ - `browser_tabs` → `action` (`"list"`, `"new"`, `"select"` or `"close"`).
112
+ - `browser_console_messages` → `level` (`"error"` is enough).
113
+ - `browser_handle_dialog` → `accept` (`true` to accept, `false` to dismiss).
114
+ - `generator_setup_page` → `project`, `seedFile` and `plan` (`plan: ""` when there is no plan).
115
+
116
+ ## Outlining a target without `browser_highlight`
117
+
118
+ `browser_highlight` is a one-line wrapper over `locator.highlight()`, and that method is public Playwright API. The
119
+ overlay is the real one, not an approximation, and nothing of the page is changed.
120
+
121
+ 1. Outline (one call, one element, the card's `Identified by` as the locator):
122
+ ```js
123
+ // browser_run_code_unsafe({intent: 'outline the target for the reference screenshot', code: ...})
124
+ async (page) => {
125
+ const target = page.getByRole('img', {name: '<the Identified by value of the card>'});
126
+ await target.highlight({style: 'outline: 3px solid #e11d48; outline-offset: 2px'});
127
+ return await target.toString();
128
+ }
129
+ ```
130
+ 2. Screenshot the page (not the element) so the reader sees where it sits:
131
+ `browser_take_screenshot({scale: 'css', filename: '.explore/targets/<case-id>/T<n>-<profile-id>.png'})`
132
+ 3. Remove it before the next step: `browser_run_code_unsafe` with `intent: 'remove the highlight overlay'` and
133
+ `async (page) => { await page.hideHighlight(); }`.
134
+
135
+ A highlight overlay is not page state: it never appears in a snapshot, it is not part of what the case checks, and
136
+ `page.hideHighlight()` clears all of them. Do this for every candidate when several elements are compared, and say in
137
+ the card what the screenshot shows.
138
+
139
+ ## Network requests without a clear
140
+
141
+ `browser_network_requests` returns a numbered list that only grows while the page stays open, so a marker works:
142
+
143
+ 1. Before the action: `browser_network_requests({static: false})`, note the highest number in the list (`mark`).
144
+ 2. Do the action.
145
+ 3. Read `browser_network_requests({static: false})` again and take the entries above `mark`: those, and only those,
146
+ were started by the action. `browser_network_request({index})` gives one request's detail and status.
147
+
148
+ Record the marker as a plain note in the report (`after 12 new requests; the data call was 7`), never in the case.
149
+
150
+ 4. While exploring, write the data-call paths of the action into the profile's exploration note (`notes.md`): path only,
151
+ no host, no query string. `/gen-spec` turns them into the `responses` of `actAndWait`.
@@ -1,7 +1,7 @@
1
1
  # User handoff: a person works in the browser, the agent waits
2
2
 
3
3
  Used by `exploring-cases` (and by `making-cases` never: it does not open a browser). `generating-specs` never hands
4
- off; it replays the step recipes that an exploration recorded.
4
+ off; it replays the step recipes and the exploration note (`notes.md`) that an exploration recorded.
5
5
 
6
6
  ## The rule
7
7
  **An agent never writes data.** Not in staging, not in production. Every step that creates, edits, publishes, shares,
@@ -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.