@asrunjs/asrun 0.10.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 (39) hide show
  1. package/.claude/skills/exploring-cases/SKILL.md +45 -58
  2. package/.claude/skills/exploring-cases/references/notes.md +87 -0
  3. package/.claude/skills/exploring-cases/references/readiness.md +3 -2
  4. package/.claude/skills/exploring-cases/references/tools.md +34 -0
  5. package/.claude/skills/exploring-cases/references/user-handoff.md +1 -1
  6. package/.claude/skills/generating-specs/SKILL.md +44 -37
  7. package/AGENTS.md +10 -9
  8. package/README.md +6 -5
  9. package/bin/asrun.mjs +1 -0
  10. package/dist/lib/explore-notes.d.ts +74 -0
  11. package/dist/lib/explore-notes.js +242 -0
  12. package/dist/lib/paths.d.ts +2 -0
  13. package/dist/lib/paths.js +2 -0
  14. package/dist/scripts/cli.js +9 -2
  15. package/dist/scripts/explore-notes.d.ts +1 -0
  16. package/dist/scripts/explore-notes.js +131 -0
  17. package/docs/agents/case-format.md +1 -0
  18. package/docs/agents/framework-dev.md +4 -0
  19. package/docs/ai-workflow.md +9 -1
  20. package/docs/cases.md +4 -4
  21. package/docs/commands.md +11 -0
  22. package/docs/theme-guide.html +56 -0
  23. package/docs/web-ui.md +30 -9
  24. package/package.json +2 -1
  25. package/templates/package.json +1 -0
  26. package/ui/dist/assets/{dist-BmWaNjqO.js → dist--BXJInTq.js} +1 -1
  27. package/ui/dist/assets/{dist-2msWI-5M.js → dist-BMmNFIUH.js} +1 -1
  28. package/ui/dist/assets/{dist-sfZKBSMm.js → dist-BXNaYHeh.js} +1 -1
  29. package/ui/dist/assets/{dist-BVvHVJ3N.js → dist-ByLbEOlF.js} +1 -1
  30. package/ui/dist/assets/{dist-e-KhTWHJ.js → dist-C9ra6_BD.js} +1 -1
  31. package/ui/dist/assets/{dist-4U_gPHXf.js → dist-CK8mow8u.js} +1 -1
  32. package/ui/dist/assets/{dist-BwNz_xW2.js → dist-CQS0So-i.js} +1 -1
  33. package/ui/dist/assets/{dist-J36xayOv.js → dist-CTrwRc3Q.js} +1 -1
  34. package/ui/dist/assets/{dist-BMfJatHo.js → dist-DTYj2vUD.js} +1 -1
  35. package/ui/dist/assets/{dist-wjB7g6OJ.js → dist-mV0_nQZe.js} +1 -1
  36. package/ui/dist/assets/{index-CGbRJT3b.js → index-BzMqkmQ9.js} +41 -41
  37. package/ui/dist/assets/index-DcJR-8Mt.css +2 -0
  38. package/ui/dist/index.html +2 -2
  39. package/ui/dist/assets/index-CI0i0LWc.css +0 -2
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: exploring-cases
3
- description: Explores the real pages for one case document (cases/<id>.md) before its spec is generated, for every logged-in profile or only the ones named with --profiles. Checks the case against profiles.yaml and the pages, fixes wording mistakes, records what the pages show, turns unknowns into answerable TODO blocks, and, in an attended run, lets the person do the steps that write data or need a choice while it waits and records a general recipe under that step's `- Recipe:` lines. Ends with RESULT ready or needs-input. Use when asked to explore, review, check, clean up or narrow down a case, or for /explore-case. Its open-question rules are shared with generating-specs.
3
+ description: Explores the real pages for one case document (cases/<id>.md) before its spec is generated, for every logged-in profile or only the ones named with --profiles. Checks the case against profiles.yaml and the pages, fixes wording mistakes, records what the pages show, turns unknowns into answerable TODO blocks, and, in an attended run, lets the person do the steps that write data or need a choice while it waits and records a general recipe under that step's `- Recipe:` lines. Also writes a local exploration note (locators and data-call paths) under `.explore/` for `/gen-spec` to verify instead of rediscovering. Ends with RESULT ready or needs-input. Use when asked to explore, review, check, clean up or narrow down a case, or for /explore-case. Its open-question rules are shared with generating-specs.
4
4
  ---
5
5
 
6
6
  # Exploring a case document
@@ -9,8 +9,10 @@ Read the workspace `AGENTS.md` (hard rules and project rules), `cases/_template.
9
9
  (how a case's `## Rules` are read, applied and maintained), `references/open-questions.md` (how
10
10
  questions are written, resolved and reported), `references/assertion-targets.md` (which exact image, text, file or
11
11
  place a check looks at, and when to ask instead of picking), `references/readiness.md` (wait for the content before
12
- you read it) and `references/user-handoff.md` (how a person works in the browser while the agent waits, and how it is
13
- recorded). All but the last are binding for `/gen-spec` and healing too.
12
+ you read it), `references/tools.md` (the browser tools this MCP can run, and the substitutes for the ones it cannot),
13
+ `references/notes.md` (the local exploration note `/gen-spec` reads) and `references/user-handoff.md` (how a person works
14
+ in the browser while the agent waits, and how it is recorded). All but the last are binding for `/gen-spec` and healing
15
+ too.
14
16
 
15
17
  ## Before you start
16
18
  Read the workspace `AGENTS.md`: its §2 holds the project rules, and it points at the framework baseline. If
@@ -23,14 +25,14 @@ Three steps, each learns one thing:
23
25
  1. `/make-case` (making-cases): the **intent**. Turns raw notes into case documents. No browser.
24
26
  2. `/explore-case` (this skill): the **facts and the inputs**. What the pages really show, and what a person has to
25
27
  do for the steps that write or need a choice. The only step where a person and the agent work together.
26
- 3. `/gen-spec` (generating-specs): the **implementation**. Replays the case and its step recipes to find locators and
27
- writes the spec. It does not judge the case again.
28
+ 3. `/gen-spec` (generating-specs): the **implementation**. Replays the case, its step recipes and the exploration note to
29
+ find locators and writes the spec. It does not judge the case again.
28
30
 
29
31
  The purpose of this skill is to leave a case that can be replayed without anybody: no ambiguity, every fact marked
30
32
  as observed, every input step described as a general recipe.
31
33
 
32
- This skill edits `cases/<id>.md` only. It never edits `profiles.yaml`, `tests/`, the framework folder or anything
33
- under `.auth/`.
34
+ This skill edits `cases/<id>.md` and writes its local exploration note under `.explore/notes/`. It never edits
35
+ `profiles.yaml`, `tests/`, the framework folder or anything under `.auth/`.
34
36
  **The agent never writes data**, in staging or in production: a person does every step that writes or needs a
35
37
  choice (see `references/user-handoff.md`).
36
38
 
@@ -77,22 +79,14 @@ one exception is a handoff in an attended run, where waiting for the person is t
77
79
  3. **Repair the wording** (edit the file). Fix typos and grammar, make steps one action or one check each, put
78
80
  assumptions in `## Notes` (not in `## Expected`), remove duplicates and template placeholders, make paths relative
79
81
  to the app. Do not add checks the case did not ask for and do not drop any. Never edit the `## Source` table.
80
- 4. **Explore**. The exploration runs through the `playwright-test` MCP only. Read `references/tools.md` before the
81
- first browser call: that MCP lists many tools it cannot run, and a call that answers `Tool "<name>" not found`
82
- means exactly that — the server is up, use the substitute in that file, do not stop and do not report a missing
83
- MCP server. If the tools are not registered in this session at all (no `browser_*` / `generator_*` tool) or the
84
- server reports that it failed to start, do not fall back to ad-hoc Playwright scripts: they see a different browser
85
- and their network view does not count as `browser_network_requests` (A2). Stop, tell the user which MCP server is
86
- missing and to run `pnpm asrun doctor` (it names the broken `.mcp.json` / `opencode.jsonc` and the fix), make no
87
- observations, and end with `RESULT: needs-input`.
88
- Run `pnpm status --env <env>`. If it cannot run (a sandbox error such as
89
- `listen EPERM` or a Chromium start failure), do not give up: open the profile's page through the MCP, and a page
90
- that lands on the login page means the profile is not logged in. Explore each profile `--profiles` names that is
91
- logged in, and, without `--profiles`, at least one logged-in profile per domain, with the `playwright-test` MCP
92
- (`generator_setup_page` with `project=<profile-id>`, `seedFile=tests/seed.spec.ts` and `plan=""`). Never explore a
93
- profile the selection leaves out, and never add a profile to fill
94
- a gap. Profiles that are selected but not logged in are skipped and named in the report; a human runs
95
- `pnpm signin <profile-id>`.
82
+ 4. **Explore**. The exploration runs through the `playwright-test` MCP only: read `references/tools.md` before the
83
+ first browser call and follow its "Starting a session" and "Short calls" sections. If the tools are not registered
84
+ in this session at all (no `browser_*` / `generator_*` tool) or the server reports that it failed to start, do not
85
+ fall back to ad-hoc Playwright scripts: stop, tell the user which MCP server is missing and to run `pnpm asrun
86
+ doctor`, make no observations, and end with `RESULT: needs-input`.
87
+ Explore each profile `--profiles` names that is logged in, and, without `--profiles`, at least one logged-in profile
88
+ per domain. Never explore a profile the selection leaves out, and never add a profile to fill a gap. Profiles that
89
+ are selected but not logged in are skipped and named in the report; a human runs `pnpm signin <profile-id>`.
96
90
  - Do the read-only steps yourself: open, look, read, hover, switch tabs, open and close dialogs and menus.
97
91
  - **A step that writes or needs a choice is done by the person**, per `references/user-handoff.md`: announce it,
98
92
  name the finish condition, run the wait loop (the card records what the person does), then write down what
@@ -109,38 +103,23 @@ one exception is a handoff in an attended run, where waiting for the person is t
109
103
  avoid is followed on every profile. Re-check each rule against the page: one that no longer holds gets a TODO
110
104
  with evidence under it, never silent deletion or rewriting. A rule never lets you write data or do a person's
111
105
  step.
112
- - **Ready before you read** (`references/readiness.md`): note the highest number of
113
- `browser_network_requests` (`static: false`) before an action that loads something, then the action, then wait for
114
- its key requests (the numbers above that mark) and for the content signal, and only then take the snapshot.
115
- `browser_network_clear` is not runnable in this MCP: the marker replaces it (`references/tools.md`). When the
116
- first read is empty or missing something, wait and read again before you record or ask anything. Record the
117
- signal as a plain note, `(seen on <profile>, after <signal>)`: the spec waits for it, so no person has to confirm
118
- it.
119
- - **Targets, not guesses** (`references/assertion-targets.md`): for every check on a logo, image, icon, file, PDF,
120
- screenshot or repeated text, list ALL candidates, outline the one that is meant (`locator.highlight()` through
121
- `browser_run_code_unsafe`, see `references/tools.md`), take the screenshot to `.explore/targets/<case-id>/`,
122
- **open that screenshot and look at it**, then clear it with `page.hideHighlight()`, and write the card under
123
- `## Targets`. If there are several candidates, only a position to tell them apart, a generic locator, dynamic
124
- data in the area, or the page differs from the case: write a `[blocking]` TODO that names the exact image, text,
125
- place or UI wanted and lists the candidates. In an attended run let the person name the element in the handoff
126
- card, then outline what they named (`browser_annotate` is not runnable here).
127
- - **Downloads and email composers are opened for real** (the composer is closed with "Discard"). A download is
128
- verified in five parts: starts, finishes, file exists, file valid, and a failure or timeout fails the check
129
- (see `references/readiness.md`). Record the file name, size and kind as observed. A downloaded PDF is rendered
130
- (`pnpm pdf:render`) and its page images are opened and looked at: record which pages show the target, where, and
131
- the text. Which pages the check covers follows the case: every page unless the case says ONLY (strong wording).
106
+ - **Ready before you read**: follow `references/readiness.md`, "Exploring". Record the signal it proves as a plain
107
+ note, `(seen on <profile>, after <signal>)`: the spec waits for it, so no person has to confirm it. When the first
108
+ read is empty or missing something, wait and read again before you record or ask anything.
109
+ - **Targets, not guesses**: follow `references/assertion-targets.md`, "How a card is made (exploring)". List ALL
110
+ candidates, outline the one that is meant, take the screenshot to `.explore/targets/<case-id>/`, **open it and look
111
+ at it**, clear the highlight and write the card under `## Targets`. When candidates cannot be told apart, or only a
112
+ position, a generic locator or dynamic data would decide it, write a `[blocking]` TODO that names the exact image,
113
+ text, place or UI wanted and lists the candidates. In an attended run let the person name the element in the
114
+ handoff card, then outline what they named (`browser_annotate` is not runnable here).
115
+ - **Downloads and email composers are opened for real** (the composer is closed with "Discard"). Verify a download
116
+ in the five parts of `references/readiness.md`, "Downloads", and record the file name, size and kind. Render a
117
+ downloaded PDF (`pnpm pdf:render`), open its page images and record which pages show the target, where, and the
118
+ text; the pages follow the case (every page unless the case says ONLY).
132
119
  - Compare every step with what is really there.
133
-
134
- **Keep every browser call short, so nothing hangs** (`generating-specs` follows the same rules):
135
- - One `browser_run_code_unsafe` call does one thing and should finish within about 60 seconds. Do not close a tab,
136
- click, wait for a dialog and read it in one call: split it, and read the page (`browser_snapshot`) between the
137
- steps. Only the handoff wait loop may take minutes.
138
- - The browser times out an action or a lookup after 15 s (the config sets it). Pass a shorter `{timeout: 5000}` when
139
- you wait for something that may not appear, and use `.catch()` or `isVisible()` for a one-time look while exploring only.
140
- `isVisible()` answers for this instant and ignores any `timeout` passed to it, so after an action take a snapshot or
141
- wait for a signal first. Never copy this pattern into a spec (see `references/readiness.md`, "Optional confirmations").
142
- - After a click that should open something, take a snapshot and see what opened before you wait for it by role. A
143
- dialog that did not appear is a finding (what did open instead?), not something to wait for or retry.
120
+ - **Record the exploration note** for this profile as you go, in the shape of `references/notes.md`: the locator of
121
+ each step's main control, the data-call paths of each loading action, and each target it confirmed. It is local
122
+ cache under `.explore/`, never case content, and `/gen-spec` reads it; step 8 writes the file.
144
123
  - A step that fails or times out twice is not tried a third time: write what you saw, add a `TODO [blocking]` under
145
124
  the step and go on with the next one.
146
125
  5. **Write the `## Targets` section and the ready signals.** One card per target, in the format of
@@ -162,12 +141,18 @@ one exception is a handoff in an attended run, where waiting for the person is t
162
141
  the rules: apply what holds, move what belongs in another section, and leave a question under a rule that conflicts
163
142
  or is obsolete. Then apply the `--hints` gate of `references/case-rules.md`: a reusable, general instruction goes
164
143
  into `## Rules` (with a marker only when it changes what is asserted); a one-off clue stays in the report.
165
- 8. **Check**: run `pnpm case:check` (it fails on an incomplete target card, a rule that names a missing step, and
166
- warns on a check that has none) and fix every FAIL. If it fails with a sandbox error (`listen EPERM`), run
144
+ 8. **Write the exploration note, then check.** After the case edits are done (so `caseSha256` is the case's final hash;
145
+ `pnpm asrun explore:notes <case-id> --hash`), write `.explore/notes/<case-id>/<profile-id>.json` for every profile
146
+ explored in this run, in the shape of `references/notes.md` and with only what was seen. Then run `pnpm case:check`
147
+ (it fails on an incomplete target card, a rule that names a missing step, and warns on a check that has none) and
148
+ `pnpm asrun explore:notes <case-id> --check` (it fails on a malformed note and warns on a stale one; the summary
149
+ line of every profile you explored must say `fresh`), and fix every FAIL. If
150
+ `case:check` fails with a sandbox error (`listen EPERM`), run
167
151
  `node --import tsx <framework>/scripts/check-cases.ts` instead; it checks the same thing.
168
152
  9. **Report** in the user's language: what was changed (wording fixes, observations added, TODOs closed or narrowed),
169
153
  what was explored (profile ids), which profiles `--profiles` left out (and that a run with them covers the rest),
170
- which steps the person did and what was recorded for them, what was skipped and
154
+ which steps the person did and what was recorded for them, what the exploration note covers (and what it does not),
155
+ what was skipped and
171
156
  why, the target cards written (and which ones are still questions), the steps whose first read was too early,
172
157
  the steps that could not be observed (and why), the questions that remain
173
158
  with the sentence that deleting each block would confirm, the markers with their reason and whether each changes an
@@ -177,7 +162,9 @@ one exception is a handoff in an attended run, where waiting for the person is t
177
162
  requires: plain text on the last line, never inside a code fence or in emphasis.
178
163
 
179
164
  ## Hard rules
180
- - Edit `cases/<id>.md` only. Do not touch the `## Source` table.
165
+ - Edit `cases/<id>.md` only. Do not touch the `## Source` table. The exploration note under `.explore/notes/` is the
166
+ other file this run writes, and a locator never moves from there into the case.
167
+ - Never put a cookie, a token, a header or a URL with a query string into a note (`references/notes.md`).
181
168
  - A `## Rules` bullet is a person's instruction: never delete it or change what it says, and never treat it as
182
169
  permission to write data, send email or do a person's step (`R1`–`R4`, `references/case-rules.md`).
183
170
  - The agent never writes data (see above). Never add `--allow-writes`, never set `ASRUN_ALLOW_WRITES`.
@@ -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.
@@ -63,8 +63,9 @@ await expectImageLoaded(logo);
63
63
 
64
64
  - Register the response wait **before** the action (`actAndWait` does).
65
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
66
- signal. The request URL patterns come from `browser_network_requests` during exploration, not from guesses; if none was
67
- 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.
68
69
  - Never `waitForTimeout`, never `networkidle`. They wait for time or for silence, not for the content. `pnpm spec:lint` fails both.
69
70
  - Never `if (await x.isVisible()) ...` to decide something that may not have loaded yet. Wait for a ready signal, then branch.
70
71
  Where an element is genuinely optional, say what proves the page finished (`// ready: ...`) before looking for it.
@@ -23,6 +23,37 @@ substitute below instead, and say in the report which substitute you used.
23
23
  `pnpm asrun doctor` prints how many tools the installed Playwright actually runs and warns when this file names one
24
24
  it cannot (it happens when Playwright changes the filter; the file then needs the same edit).
25
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
+
26
57
  ## Runnable
27
58
 
28
59
  Navigation and reading:
@@ -115,3 +146,6 @@ the card what the screenshot shows.
115
146
  were started by the action. `browser_network_request({index})` gives one request's detail and status.
116
147
 
117
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,
@@ -9,8 +9,10 @@ Read `references/conventions.md` first. It defines the spec shape, fixtures, ass
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
11
  `../exploring-cases/references/case-rules.md` (how a case's `## Rules` are read, applied and maintained),
12
- `../exploring-cases/references/assertion-targets.md` (the exact thing each check looks at) and
13
- `../exploring-cases/references/readiness.md` (wait for the content, verify downloads in five parts). All four are binding here.
12
+ `../exploring-cases/references/assertion-targets.md` (the exact thing each check looks at),
13
+ `../exploring-cases/references/readiness.md` (wait for the content, verify downloads in five parts),
14
+ `../exploring-cases/references/notes.md` (the local exploration note this run reads first) and
15
+ `../exploring-cases/references/tools.md` (the browser tools this MCP can run). All six are binding here.
14
16
 
15
17
  This skill never stops to ask the user in the chat, because it may run without anybody present (a one-shot
16
18
  `claude -p` / `opencode run`). This holds whatever mode or agent the run is in: plan mode changes what you may write,
@@ -116,42 +118,42 @@ Decide this first, before exploring anything. The spec path is `tests/<id>.spec.
116
118
  never for what the check asserts about it: that goes to `test.fixme` with the reason, or `needs-input`.
117
119
  - Do not invent expected values. If the case's `## Values` and the spec disagree, the case wins: fix the spec and
118
120
  mention the change in the report.
119
- 3. **Check sessions** if `login: required`: run `pnpm status --env <env>`. If a profile needs login, tell the
120
- user to run `pnpm signin <profile-id>` and continue with the profiles that are ready. A profile the case holds
121
- information for is never dropped because it is not logged in: write its part from the case and mark it
122
- `not verified on the page` in the report. If `pnpm status` cannot run (a sandbox error such as `listen EPERM` or a
123
- Chromium start failure), judge by the page instead: a page that lands on the login page means the profile is not
124
- logged in.
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`.
131
- Call `generator_setup_page` with
132
- `project=<profile-id>` (for example `example-staging`), `seedFile=tests/seed.spec.ts` and `plan=""`, then perform the
133
- steps in the environment you were given (never ask for it; there is no default) to learn the locators.
121
+ 3. **Check sessions** if `login: required`: follow `../exploring-cases/references/tools.md`, "Starting a session". A
122
+ profile that needs login is named with `pnpm signin <profile-id>`, and the run continues with the profiles that are
123
+ ready. A profile the case holds information for is never dropped because it is not logged in: write its part from
124
+ the case and mark it `not verified on the page` in the report.
125
+ 4. **Replay the case on the real page** with the `playwright-test` MCP, following
126
+ `../exploring-cases/references/tools.md` ("Starting a session", "Short calls") and `readiness.md` ("Exploring"). If
127
+ its tools are not registered in this session at all (no `browser_*` / `generator_*` tool) or it reports that it
128
+ failed to start, do not fall back to ad-hoc Playwright scripts: stop, tell the user which MCP server is missing and
129
+ to run `pnpm asrun doctor`, write no spec, and end with `RESULT: needs-input`.
130
+ **Read each profile's exploration note first** (`../exploring-cases/references/notes.md`): run
131
+ `pnpm asrun explore:notes <case-id> --json`. A fresh note (its `caseSha256` matches the case) supplies the locator,
132
+ the request pattern and the target's match count for a step; confirm each on the page instead of rediscovering it,
133
+ and skip what it does not hold. A missing or stale note means the plain replay below. After confirming an entry of
134
+ a fresh note, you may write the verified locator or request back into that file (`updatedAt` gets the time); never
135
+ add a step or a target, and never touch a stale note. Say in the report what came from a note and what was
136
+ refreshed. Perform the steps in the environment you were given (never ask for it; there is no default).
134
137
  Do the read-only steps as written, and follow the `## Rules` that say how to prepare, wait or which control to
135
138
  avoid. **Never perform a step that writes or needs a choice**: the agent does not write
136
139
  data, in staging or in production, and it does not hand off to a person here or wait for one. A rule never
137
140
  authorizes one of those steps. Write those steps
138
141
  of the spec from the `- Recipe:` lines under the step (positions, `[asrun]` names, control labels and roles that
139
142
  `/explore-case` recorded). Pages that only exist after such a step cannot be seen from here: write their locators
140
- from what the recipe and the exploration notes say, mark them in the report as "not verified on the page", and
141
- leave the first run of the spec (or `/heal-spec`) to confirm them. Do this for at least one logged-in profile
142
- per domain (example.com and exampleshop.com differ in markup). A profile the case holds information for that is
143
- not logged in is skipped and named in the report; its part is written from the case and marked `not verified` in
144
- the assertion table. Use this exploration to answer the open points of step 2, and read each profile's value from
145
- the case's `## Values`: open that profile's page only when a value or a behavior still has to be resolved for it.
146
- Keep every browser call short (one action, about 60 s at most, a snapshot between steps, `{timeout: 5000}` when
147
- something may not appear, no third retry): see "Keep every browser call short" in `../exploring-cases/SKILL.md`.
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.
151
- **Check every target on the page:** for each target card, find its `Identified by` locator, confirm that it matches
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()`.
143
+ from a fresh note first, then from the recipe, and mark them in the report as "observed by explore on <profile>" or
144
+ "not verified on the page"; leave the first run of the spec (or `/heal-spec`) to confirm the latter. Do this for at
145
+ least one logged-in profile per domain (example.com and exampleshop.com differ in markup). A profile the case holds
146
+ information for that is not logged in is skipped and named in the report; its part is written from the case and
147
+ marked `not verified` in the assertion table. Use this exploration to answer the open points of step 2, and read each
148
+ profile's value from the case's `## Values`: open that profile's page only when a value or a behavior still has to be
149
+ resolved for it.
150
+ **Ready before you read, here too**: follow `readiness.md`. The request URL patterns for `actAndWait` come from a
151
+ fresh note or from `browser_network_requests` during this replay, never from a guess; the `Ready when` signal comes
152
+ from the case.
153
+ **Check every target on the page:** follow `../exploring-cases/references/assertion-targets.md`, "What the spec does
154
+ with a card (generating)". Confirm that the card's `Identified by` matches exactly one element and that this element
155
+ is what `What` says (outline it with `locator.highlight()` and look at the screenshot, see `tools.md`). A locator that
156
+ matches several elements or the wrong one is fixed or becomes a question, never `.first()`.
155
157
  Prefer `getByTestId` (the project uses `data-tn`), then role/label locators. No CSS chains or XPath.
156
158
  Update the case file with what you learned: fold observations into the right section, marked `(seen on <profile>)`
157
159
  or, for a literal the spec asserts, `(observed on <profile>, 🔍 to be confirmed: <what to decide>)`; remove the
@@ -179,7 +181,8 @@ Decide this first, before exploring anything. The spec path is `tests/<id>.spec.
179
181
  never decided by one `isVisible()` look (its `timeout` option is ignored): use `clickOptionalUntilHidden`
180
182
  (`@asrunjs/asrun/ready`) or a waiting `expect(...).toBeVisible({timeout})`; see `readiness.md`, "Optional confirmations".
181
183
  7. **Check the files**: run `pnpm typecheck`, `pnpm spec:lint <id>` and `pnpm case:check` (or `node --import tsx <framework>/scripts/check-cases.ts` if
182
- it fails with a sandbox error such as `listen EPERM`). Fix every error, including every `FAIL` of the spec lint (a lint failure is never silenced: fix the spec or turn the check
184
+ it fails with a sandbox error such as `listen EPERM`). When you wrote an exploration note back, also run
185
+ `pnpm asrun explore:notes <id> --check --profile <profile-id>` for each profile you refreshed. Fix every error, including every `FAIL` of the spec lint (a lint failure is never silenced: fix the spec or turn the check
183
186
  into a question). Every lint `WARN` is handled too: fix it, or name it in the report with the reason it is acceptable.
184
187
  Then do the **self-review**: for each assertion answer in writing "if the product were broken, would this fail?"
185
188
  and "if the product is right, could this fail by timing?" (a check that cannot fail, or that races the page, is rewritten).
@@ -188,7 +191,8 @@ Decide this first, before exploring anything. The spec path is `tests/<id>.spec.
188
191
  Do NOT run `pnpm asrun`, do not use
189
192
  `--update-snapshots` and do not create baselines: the first run of the spec does that, and a person reviews them.
190
193
  8. **Report** in the user's language: spec path, **an assertion table** (step, what is asserted, target card, ready signal, and
191
- `observed on the page` or `not verified`; no row may be missing), profiles explored, profiles skipped (and why),
194
+ `observed on the page` or `not verified`; no row may be missing), profiles explored, which steps were written from a
195
+ fresh exploration note and which notes were refreshed (or that there were none), profiles skipped (and why),
192
196
  the profiles of `profiles.yaml` the case holds no information for (they run on `default` alone), whether the case file was
193
197
  translated, what was added to the case file as `(seen on <profile>)` and which lines still need a person
194
198
  (`🔍 to be confirmed`, with the reason), checks generated from observations, visual checks that need a baseline on
@@ -250,12 +254,15 @@ Heal from evidence, not from a habit of rerunning. A report that already exists
250
254
  leaves out the profiles that are not logged in instead of failing them. A profile that is not logged in is never healed:
251
255
  name it in the report with `pnpm signin <profile-id>`.
252
256
  4. **Look at the pages when the change depends on them.** Reading a page is not a run and is always allowed, also for an
253
- improvement request with no failure behind it. Use the `playwright-test` MCP exactly as `/gen-spec` step 4 describes
254
- (`generator_setup_page` with `project=<profile-id>`, one action per call, ready before you read). Do the read-only
257
+ improvement request with no failure behind it. Use the `playwright-test` MCP exactly as `/gen-spec` step 4 describes,
258
+ following `../exploring-cases/references/tools.md` ("Starting a session", "Short calls"), `readiness.md` and the
259
+ exploration note (`../exploring-cases/references/notes.md`). Do the read-only
255
260
  steps only: **never perform a step that writes or needs a choice**, in any environment. What you see goes into the
256
261
  case by the gate in `open-questions.md` ("Which sentences carry a marker"): `(seen on <profile>)` for a locator, a
257
262
  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.
263
+ literal the spec asserts, so the person settles that in the review gate below. If the note was stale, edit the case
264
+ as usual; if it was fresh, a verified locator may be written back
265
+ (`../exploring-cases/references/notes.md`).
259
266
  5. **Audit the assertion before you blame anyone.** Read the `assertion-targets.md` and `readiness.md` sections on healing.
260
267
  - Is the locator still what the target card says (`What`, `Identified by`)? Open the failure screenshot and the
261
268
  baseline (`pnpm baselines:review <case-id>`) and look at each: a baseline that shows something other than the card
package/AGENTS.md CHANGED
@@ -184,15 +184,16 @@ Three steps, each learns one thing: the intent, the facts and inputs, the implem
184
184
  1. `making-cases` (or `/make-case <notes>`) turns raw notes into case documents. It writes `cases/*.md` only and
185
185
  never opens a browser. Check them with `pnpm case:check`.
186
186
  2. `exploring-cases` (or `/explore-case <case-id> <env> [--attended] [--profiles a,b]`) explores the real pages for one
187
- case. It edits that case file only: wording, observations, TODO blocks, and, with `--attended`, the `- Recipe:` lines
188
- of the steps that write or need a choice. `--profiles` narrows the run to those profiles; runs add up, so the case
189
- can be explored in passes.
190
- 3. `generating-specs` (or `/gen-spec <case-id> <env>`) turns the case into a spec. It replays the case and its
191
- step recipes and `## Values`, writes the spec (values stay in the case document), but never runs the tests, never
192
- writes data and never creates baselines. There is no profile selection here: it covers every profile the case names,
193
- which is why a case explored in passes generates one spec for all of them. A tagged step with no `- Recipe:` line
194
- (an old `## Inputs` line counts until it is moved) stops with `needs-input`: run `/explore-case` first. Running specs
195
- is a separate step (`pnpm asrun`, or the Run page).
187
+ case. It edits that case file: wording, observations, TODO blocks, and, with `--attended`, the `- Recipe:` lines
188
+ of the steps that write or need a choice, and it writes a local note under `.explore/notes/` with the locators and
189
+ data-call paths it saw. `--profiles` narrows the run to those profiles; runs add up, so the case can be explored in
190
+ passes.
191
+ 3. `generating-specs` (or `/gen-spec <case-id> <env>`) turns the case into a spec. It replays the case, its
192
+ step recipes and the exploration note, and `## Values`, writes the spec (values stay in the case document), but never
193
+ runs the tests, never writes data and never creates baselines. There is no profile selection here: it covers every
194
+ profile the case names, which is why a case explored in passes generates one spec for all of them. A tagged step with
195
+ no `- Recipe:` line (an old `## Inputs` line counts until it is moved) stops with `needs-input`: run
196
+ `/explore-case` first. Running specs is a separate step (`pnpm asrun`, or the Run page).
196
197
  Workspace maintenance is separate from the AI steps: `pnpm asrun upgrade` installs the newest framework, refreshes the
197
198
  generated files and applies the release's workspace migrations (it checks the target release for breaking changes
198
199
  before installing), `pnpm asrun init [--update] [--force]` writes or refreshes the workspace files, `pnpm asrun
package/README.md CHANGED
@@ -99,11 +99,12 @@ exempts `@asrunjs/asrun` from pnpm's 24-hour release age, so a fresh release ins
99
99
  3. **Write a case.** `pnpm case:new header-logo` creates `cases/header-logo.md` from the template; or start from raw
100
100
  notes with `/make-case notes.md`. Fill in the steps, what is expected and the exact values.
101
101
  4. **Check it against the real pages.** `/explore-case header-logo staging` opens the pages (with your saved session),
102
- writes what they show into the case and turns what it cannot know into questions. Answer them in the case file, or
103
- in the UI's **Answer** sheet, and run it again. With many profiles, `--profiles a,b` opens only those; a later run
104
- with the others adds theirs, and the case keeps both.
105
- 5. **Generate, run, read.** `/gen-spec header-logo staging` writes `tests/header-logo.spec.ts` and fills the case's
106
- `## Values`. Then:
102
+ writes what they show into the case and turns what it cannot know into questions. It also saves a local note of the
103
+ locators and data calls it saw. Answer the questions in the case file, or in the UI's **Answer** sheet, and run it
104
+ again. With many profiles, `--profiles a,b` opens only those; a later run with the others adds theirs, and the case
105
+ keeps both.
106
+ 5. **Generate, run, read.** `/gen-spec header-logo staging` reads those notes, writes `tests/header-logo.spec.ts` and
107
+ fills the case's `## Values`. Then:
107
108
 
108
109
  ```bash
109
110
  pnpm asrun --env staging --grep header-logo
package/bin/asrun.mjs CHANGED
@@ -23,6 +23,7 @@ const SCRIPTS = {
23
23
  status: 'status.js',
24
24
  report: 'report.js',
25
25
  explore: 'explore.js',
26
+ 'explore:notes': 'explore-notes.js',
26
27
  cleanup: 'cleanup.js',
27
28
  'case:new': 'new-case.js',
28
29
  'case:check': 'check-cases.js',
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The local exploration note: what `/explore-case` saw on the page, in a form `/gen-spec` can read.
3
+ *
4
+ * It lives under `.explore/notes/<case-id>/<profile-id>.json`, one file per profile, so a run with `--profiles` never
5
+ * has to merge with an earlier one. `.explore/` is git-ignored local state: a note is optional, may be deleted at any
6
+ * time, and a fresh checkout simply has none (generation then replays the case as before).
7
+ *
8
+ * A note holds only what the case does not: locators the agent confirmed on the page, the data-call paths of the
9
+ * actions that load something, and the match count of a target card's element. Expected values stay in the case's
10
+ * `## Values`, ready signals in the case's `Ready when:` lines, and assertions nowhere near this file.
11
+ */
12
+ export declare const NOTES_VERSION = 1;
13
+ export interface ExploreLocator {
14
+ /** What the locator is for, in words ("the header navigation link"). */
15
+ purpose: string;
16
+ /** A Playwright locator as `browser_generate_locator` returned it, for example `page.getByRole('link', {name: 'Home'})`. */
17
+ locator: string;
18
+ }
19
+ export interface ExploreNoteStep {
20
+ /** The path the step runs on, relative to `baseURL` (for example `/documents/`). */
21
+ url?: string;
22
+ locators?: ExploreLocator[];
23
+ /** Data-call paths the step's action starts, path only (`/api/articles`). */
24
+ requests?: string[];
25
+ }
26
+ export interface ExploreNoteTarget {
27
+ locator: string;
28
+ /** How many elements the locator matched when it was confirmed (1 for a target that is not a question). */
29
+ matches?: number;
30
+ }
31
+ export interface ExploreNote {
32
+ version: typeof NOTES_VERSION;
33
+ caseId: string;
34
+ profile: string;
35
+ env?: string;
36
+ /** sha256 of `cases/<case-id>.md` when the note was written; a different one makes the note stale. */
37
+ caseSha256: string;
38
+ capturedAt: string;
39
+ /** Set when a later `/gen-spec` refreshed an entry it confirmed on the page. */
40
+ updatedAt?: string;
41
+ steps: Record<string, ExploreNoteStep>;
42
+ targets?: Record<string, ExploreNoteTarget>;
43
+ }
44
+ export interface ExploreNoteError {
45
+ file: string;
46
+ problems: string[];
47
+ }
48
+ /** The folder that holds one case's notes; `<case-id>` may contain its group folder (`documents/landing-page`). */
49
+ export declare function exploreNotesDir(caseId: string): string;
50
+ /** The note of one profile: `.explore/notes/<case-id>/<profile-id>.json`. */
51
+ export declare function exploreNotesFile(caseId: string, profileId: string): string;
52
+ /** sha256 of the case document, or null when it does not exist. */
53
+ export declare function caseSha256(caseId: string): string | null;
54
+ export type NoteFreshness = 'fresh' | 'stale' | 'no-case';
55
+ /**
56
+ * Whether a note still describes the case. A stale note is not an error: `/gen-spec` replays the case and ignores it.
57
+ */
58
+ export declare function noteFreshness(note: ExploreNote): {
59
+ state: NoteFreshness;
60
+ reason?: string;
61
+ };
62
+ /** Every reason the note is not usable, in plain words. An empty list means it is valid. */
63
+ export declare function validateExploreNote(value: unknown, expected: {
64
+ caseId: string;
65
+ profile: string;
66
+ }): string[];
67
+ /**
68
+ * Reads every note of a case (or one profile's), parses and validates it. A malformed file is reported, never thrown:
69
+ * the caller decides whether it matters, and generation treats an unreadable note as a missing one.
70
+ */
71
+ export declare function readExploreNotes(caseId: string, profileId?: string): {
72
+ notes: ExploreNote[];
73
+ errors: ExploreNoteError[];
74
+ };