@asrunjs/asrun 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/.claude/commands/explore-case.md +11 -6
  2. package/.claude/commands/gen-spec.md +10 -6
  3. package/.claude/commands/heal-spec.md +13 -9
  4. package/.claude/commands/make-case.md +1 -1
  5. package/.claude/skills/exploring-cases/SKILL.md +65 -31
  6. package/.claude/skills/exploring-cases/references/assertion-targets.md +8 -4
  7. package/.claude/skills/exploring-cases/references/case-rules.md +110 -0
  8. package/.claude/skills/exploring-cases/references/handoff-card.js +73 -11
  9. package/.claude/skills/exploring-cases/references/open-questions.md +58 -6
  10. package/.claude/skills/exploring-cases/references/readiness.md +4 -3
  11. package/.claude/skills/exploring-cases/references/tools.md +117 -0
  12. package/.claude/skills/exploring-cases/references/user-handoff.md +46 -17
  13. package/.claude/skills/generating-specs/SKILL.md +71 -33
  14. package/.claude/skills/generating-specs/references/conventions.md +4 -0
  15. package/.claude/skills/making-cases/SKILL.md +14 -7
  16. package/.claude/skills/making-cases/references/gaps.md +7 -4
  17. package/.claude/skills/making-cases/references/normalizing.md +1 -1
  18. package/AGENTS.md +51 -11
  19. package/README.md +4 -3
  20. package/bin/asrun.mjs +1 -0
  21. package/dist/lib/case-doc.d.ts +41 -4
  22. package/dist/lib/case-doc.js +109 -17
  23. package/dist/scripts/check-cases.js +89 -1
  24. package/dist/scripts/doctor.js +50 -3
  25. package/dist/scripts/markers-confirm.d.ts +1 -0
  26. package/dist/scripts/markers-confirm.js +139 -0
  27. package/dist/ui/server/actions.js +50 -16
  28. package/dist/ui/server/app.js +7 -2
  29. package/dist/ui/server/job-store.d.ts +60 -0
  30. package/dist/ui/server/job-store.js +198 -0
  31. package/dist/ui/server/jobs.d.ts +46 -4
  32. package/dist/ui/server/jobs.js +361 -59
  33. package/dist/ui/server/process-info.d.ts +17 -0
  34. package/dist/ui/server/process-info.js +126 -0
  35. package/dist/ui/server/result-state.d.ts +10 -0
  36. package/dist/ui/server/result-state.js +106 -0
  37. package/dist/ui/shared/ai-commands.d.ts +4 -1
  38. package/dist/ui/shared/ai-commands.js +14 -4
  39. package/dist/ui/shared/types.d.ts +17 -2
  40. package/docs/agents/case-format.md +39 -5
  41. package/docs/ai-workflow.md +47 -15
  42. package/docs/cases.md +28 -4
  43. package/docs/commands.md +6 -1
  44. package/docs/troubleshooting.md +40 -0
  45. package/docs/web-ui.md +29 -6
  46. package/package.json +2 -1
  47. package/templates/.claude/commands/explore-case.md +11 -6
  48. package/templates/.claude/commands/gen-spec.md +10 -5
  49. package/templates/.claude/commands/heal-spec.md +11 -7
  50. package/templates/.claude/commands/make-case.md +1 -1
  51. package/templates/AGENTS.md +9 -6
  52. package/templates/cases/_template.md +7 -0
  53. package/templates/package.json +1 -0
  54. package/ui/dist/assets/{dist-B7HeglDd.js → dist-2msWI-5M.js} +1 -1
  55. package/ui/dist/assets/{dist-DfBpjgZT.js → dist-4U_gPHXf.js} +1 -1
  56. package/ui/dist/assets/{dist-B9_GXR2G.js → dist-BMfJatHo.js} +1 -1
  57. package/ui/dist/assets/{dist-Bb36rNUw.js → dist-BVvHVJ3N.js} +1 -1
  58. package/ui/dist/assets/{dist-WvvCZeeu.js → dist-BmWaNjqO.js} +1 -1
  59. package/ui/dist/assets/{dist-G9KNeh6u.js → dist-BwNz_xW2.js} +1 -1
  60. package/ui/dist/assets/{dist-GvRUDL-2.js → dist-J36xayOv.js} +1 -1
  61. package/ui/dist/assets/{dist-Dqc_tfzC.js → dist-e-KhTWHJ.js} +1 -1
  62. package/ui/dist/assets/{dist-DevASegc.js → dist-sfZKBSMm.js} +1 -1
  63. package/ui/dist/assets/{dist-Dq4Vnw5N.js → dist-wjB7g6OJ.js} +1 -1
  64. package/ui/dist/assets/{index-BJ5D2mFK.js → index-CGbRJT3b.js} +134 -132
  65. package/ui/dist/assets/index-CI0i0LWc.css +2 -0
  66. package/ui/dist/index.html +2 -2
  67. package/ui/dist/assets/index-BXY-eKBn.css +0 -2
@@ -6,8 +6,8 @@ Load the `exploring-cases` skill and explore: $ARGUMENTS
6
6
  In this repository the workspace is `examples/demo` (set `ASRUN_ROOT=examples/demo`); the framework is `.`.
7
7
  Rules combine in this order, and a later one wins where they conflict: the framework baseline `AGENTS.md`, the
8
8
  framework skill, `.claude/skills/exploring-cases.md` if it exists, then the workspace `AGENTS.md` §2 in `examples/demo`
9
- (Project rules). F1–F6 always win.
10
- Arguments: `<case-id> <env> [--attended] [--profiles a,b]`. The case id is the path below cases/ without `.md` (it may include a group
9
+ (Project rules). F1–F6 and R2 always win.
10
+ Arguments: `<case-id> <env> [--attended] [--profiles a,b] [--hints <file>]`. The case id is the path below cases/ without `.md` (it may include a group
11
11
  folder, e.g. documents/landing-page) and is required. `<env>` is `staging` or `production`; it is required and there is no
12
12
  default (without one, stop and say so). With `--attended` I am at the machine and the browser window is visible: when a step
13
13
  writes data or needs a choice, hand it to me as the skill describes, wait for me, and record what I did as a general
@@ -15,8 +15,13 @@ recipe under the step it belongs to (`- Recipe:` lines). Without `--attended`, n
15
15
  `--profiles a,b` explores only those profiles (comma-separated ids from profiles.yaml); without it, at least one
16
16
  logged-in profile per domain. A profile the list leaves out is not opened and nothing is written about it: runs add up,
17
17
  so a later run covers the rest.
18
- Follow `assertion-targets.md` (list the candidates, never pick when in doubt, ask for the exact image/text/place/UI)
19
- and `readiness.md` (wait for the key requests and the content before you read anything) of the skill.
18
+ `--hints <file>` (or free text after the arguments) is what I know or want checked; read it first. After the work, a
19
+ reusable instruction goes into `## Rules` by the gate in `references/case-rules.md`; a one-off clue stays in the report.
20
+ Read `## Rules` before the steps and apply it. Follow `case-rules.md` (a rule says how to run the case, never what it
21
+ proves; never delete or rewrite one), `assertion-targets.md` (list the candidates, never pick when in doubt, ask for the
22
+ exact image/text/place/UI) and `readiness.md` (wait for the key requests and the content before you read anything) of
23
+ the skill.
20
24
  This command edits cases/<case-id>.md only (plus screenshots under `.explore/targets/`). Do not ask me questions in the chat: write open questions into the case
21
- file as the skill describes, and end with the `RESULT:` line. If you are not allowed to write files at all (a plan-mode
22
- run, a read-only sandbox), say what blocked you and end with `RESULT: needs-input`.
25
+ file as the skill describes, and end with the `RESULT:` line as plain text on the last line, never in a code fence or in
26
+ emphasis. If you are not allowed to write files at all (a plan-mode run, a read-only sandbox), say what blocked you and
27
+ end with `RESULT: needs-input`.
@@ -6,14 +6,18 @@ Load the `generating-specs` skill and generate the spec for: $ARGUMENTS
6
6
  In this repository the workspace is `examples/demo` (set `ASRUN_ROOT=examples/demo`); the framework is `.`.
7
7
  Rules combine in this order, and a later one wins where they conflict: the framework baseline `AGENTS.md`, the
8
8
  framework skill, `.claude/skills/generating-specs.md` if it exists, then the workspace `AGENTS.md` §2 in `examples/demo`
9
- (Project rules). F1–F6 always win.
10
- Arguments: `<case-id> [<env>] [--force]`. The case file is cases/<case-id>.md (the id may include one group folder, e.g.
9
+ (Project rules). F1–F6 and R2 always win.
10
+ Arguments: `<case-id> [<env>] [--force] [--hints <file>]`. The case file is cases/<case-id>.md (the id may include one group folder, e.g.
11
11
  documents/landing-page). `<env>` is the environment to explore in (`staging` or `production`); there is no default. If the spec already exists, it is
12
- left alone unless `--force` is given; with `--force` it is regenerated and overwritten.
12
+ left alone unless `--force` is given; with `--force` it is regenerated and overwritten. `--hints <file>` (or free text
13
+ after the arguments) is what I know or want changed; read it first. A reusable instruction goes into `## Rules` by the
14
+ gate in `references/case-rules.md`; a one-off clue stays in the report.
13
15
  There is no profile option: the spec is generated for every profile the case holds information for (its `## Values`
14
16
  keys, its target cards and its observations), not for a chosen subset and not only for the profiles that are logged in.
15
- Follow the skill's procedure exactly, including `assertion-targets.md` and `readiness.md`; run `pnpm spec:lint <case-id>`. Only generate the spec: do not run `pnpm asrun` (running specs is a separate
17
+ Follow the skill's procedure exactly, including `case-rules.md` (read `## Rules` before the steps; a rule says how to
18
+ run the case, never what it proves), `assertion-targets.md` and `readiness.md`; run `pnpm spec:lint <case-id>`. Only generate the spec: do not run `pnpm asrun` (running specs is a separate
16
19
  process), and do not create baselines.
17
20
  Do not ask me questions: resolve ambiguity as the skill describes (explore the page, use observed values marked for QA,
18
- write the remaining questions into the case file), and end with the `RESULT:` line. If you are not allowed to write
19
- files at all (a plan-mode run, a read-only sandbox), say what blocked you and end with `RESULT: needs-input`.
21
+ write the remaining questions into the case file), and end with the `RESULT:` line as plain text on the last line,
22
+ never in a code fence or in emphasis. If you are not allowed to write files at all (a plan-mode run, a read-only
23
+ sandbox), say what blocked you and end with `RESULT: needs-input`.
@@ -6,7 +6,7 @@ Load the `generating-specs` skill, section "Healing and refining a spec", and fo
6
6
  In this repository the workspace is `examples/demo` (set `ASRUN_ROOT=examples/demo`); the framework is `.`.
7
7
  Rules combine in this order, and a later one wins where they conflict: the framework baseline `AGENTS.md`, the
8
8
  framework skill, `.claude/skills/generating-specs.md` if it exists, then the workspace `AGENTS.md` §2 in `examples/demo`
9
- (Project rules). F1–F6 always win.
9
+ (Project rules). F1–F6 and R2 always win.
10
10
  Arguments: $ARGUMENTS
11
11
  Format: `<case-id> <env> [--profiles a,b] [--report <run-id>] [--hints <file>]`, with optional free text after them.
12
12
  - `<case-id>` and `<env>` (`staging` or `production`) are required. There is no default environment. Without one, stop
@@ -14,22 +14,26 @@ Format: `<case-id> <env> [--profiles a,b] [--report <run-id>] [--hints <file>]`,
14
14
  - `--profiles`: the profiles to run when evidence is needed. Without it, every profile that is logged in in `<env>`.
15
15
  - `--report`: the report (folder name under `reports/`) to read first. Without it, the newest report of this case.
16
16
  - `--hints <file>`: a file with what the user wants repaired or improved, with any evidence. Read it first. Any free
17
- text after the arguments is the same kind of request. Both are optional.
17
+ text after the arguments is the same kind of request. Both are optional. Read `## Rules` before the steps and apply
18
+ it; after the work, a reusable method instruction goes into `## Rules` by the gate in `references/case-rules.md`,
19
+ while a one-off clue stays in the report.
18
20
  A passing run is not a reason to refuse a request: healing also refines a case and its spec. Never lower the bar for a
19
21
  product finding to make a test pass.
20
22
 
21
23
  Do not ask me questions in the chat: this may run unattended. Resolve what the report, the case and the page can
22
- resolve, and write what still needs a person into the case as a `[blocking]` question. End with the `RESULT:` line:
23
- `ready` when the healing or the refinement is finished, `needs-input` when something is still open. If you could not
24
- write files at all (a plan-mode run, a read-only sandbox, a refused permission), say in one sentence what blocked you
25
- and end with `RESULT: needs-input`: never answer with a plan and a question instead.
24
+ resolve, and write what still needs a person into the case as a `[blocking]` question. End with the `RESULT:` line as
25
+ plain text on the last line, never in a code fence or in emphasis: `ready` when the healing or the refinement is
26
+ finished, `needs-input` when something is still open. If you could not write files at all (a plan-mode run, a read-only
27
+ sandbox, a refused permission), say in one sentence what blocked you and end with `RESULT: needs-input`: never answer
28
+ with a plan and a question instead.
26
29
 
27
30
  Before you decide "product or test", audit the assertion itself (target card, baseline content, readiness): see step 5
28
31
  of the skill's healing section, `assertion-targets.md` and `readiness.md`. Run `pnpm spec:lint <case-id>`.
29
32
  Read the existing report first (`pnpm heal:evidence --case <case-id> --env <env>`), and run the case again only when the
30
- skill says it is needed. Always run with `--logged-in`. If the change leaves an open question or an unconfirmed marker
31
- in the case, stop there, tell me to review the case and run `/explore-case`, and end with `RESULT: needs-input`.
32
- Otherwise tell me whether it is a test problem or a product finding.
33
+ skill says it is needed. Always run with `--logged-in`. If the change leaves an open question or a `🔍 to be confirmed` marker
34
+ in the case, stop there, tell me to review the case and run `/explore-case` (or `pnpm markers:confirm` after a passing
35
+ run), and end with `RESULT: needs-input`. A marker inside `## Rules` does not stop the spec unless the rule covers a
36
+ literal the spec asserts. Otherwise tell me whether it is a test problem or a product finding.
33
37
 
34
38
  Run `pnpm asrun ...`, `pnpm heal:evidence ...`, `pnpm spec:lint ...`, `pnpm baselines:review ...` and `pnpm status ...` as plain commands, optionally piped to `tail` or
35
39
  `head`, and do not chain other commands with them (`;` or `&&`). Only these exact forms run outside Claude Code's
@@ -6,6 +6,6 @@ Load the `making-cases` skill and turn this input into case documents: $ARGUMENT
6
6
  In this repository the workspace is `examples/demo` (set `ASRUN_ROOT=examples/demo`); the framework is `.`.
7
7
  Rules combine in this order, and a later one wins where they conflict: the framework baseline `AGENTS.md`, the
8
8
  framework skill, `.claude/skills/making-cases.md` if it exists, then the workspace `AGENTS.md` §2 in `examples/demo`
9
- (Project rules). F1–F6 always win.
9
+ (Project rules). F1–F6 and R2 always win.
10
10
  The input is either text or a path to a file with the notes. Follow the skill's procedure exactly.
11
11
  Ask me the open questions before writing any case file. Do not generate specs; that is `/gen-spec`.
@@ -5,7 +5,8 @@ description: Explores the real pages for one case document (cases/<id>.md) befor
5
5
 
6
6
  # Exploring a case document
7
7
 
8
- Read the workspace `AGENTS.md` (hard rules and project rules), `cases/_template.md` (the target format), `references/open-questions.md` (how
8
+ Read the workspace `AGENTS.md` (hard rules and project rules), `cases/_template.md` (the target format), `references/case-rules.md`
9
+ (how a case's `## Rules` are read, applied and maintained), `references/open-questions.md` (how
9
10
  questions are written, resolved and reported), `references/assertion-targets.md` (which exact image, text, file or
10
11
  place a check looks at, and when to ask instead of picking), `references/readiness.md` (wait for the content before
11
12
  you read it) and `references/user-handoff.md` (how a person works in the browser while the agent waits, and how it is
@@ -14,8 +15,8 @@ recorded). All but the last are binding for `/gen-spec` and healing too.
14
15
  ## Before you start
15
16
  Read the workspace `AGENTS.md`: its §2 holds the project rules, and it points at the framework baseline. If
16
17
  `.claude/skills/exploring-cases.md` exists in the workspace, read it and apply it after this skill. Then apply
17
- `AGENTS.md` §2, which wins over both (the baseline, "Precedence"). F1–F6 always win. This is not a stop condition:
18
- continue with the procedure below.
18
+ `AGENTS.md` §2, which wins over both (the baseline, "Precedence"). F1–F6 and R2 always win. This is not a stop
19
+ condition: continue with the procedure below.
19
20
 
20
21
  ## Where this skill sits
21
22
  Three steps, each learns one thing:
@@ -50,10 +51,15 @@ one exception is a handoff in an attended run, where waiting for the person is t
50
51
  least one logged-in profile per domain. The selection is never widened: a profile it leaves out is not opened and
51
52
  nothing about it is written into the case. Runs add up: a later run with other profiles adds their observations and
52
53
  `## Values` entries next to the ones recorded before.
54
+ - Optionally `--hints <file>` (or free text after the arguments): what the person knows or wants checked. Read it
55
+ before the static review. It guides this run; after the work, a reusable instruction is written into `## Rules` by
56
+ the gate in `references/case-rules.md`, and a one-off clue stays in the report.
53
57
 
54
58
  ## Procedure
55
- 1. **Read** the case, `profiles.yaml` and `cases/_template.md`. Apply uncommitted user edits as they are: they are the
56
- newest wording. Use targeted edits, never rewrite a whole file.
59
+ 1. **Read** the case, `profiles.yaml` and `cases/_template.md`. Read `## Rules` before the steps: it says how to run
60
+ this case. Apply the rule pass of `references/case-rules.md` (apply what holds, move what belongs in another
61
+ section, turn a conflict into a `[blocking]` question, never delete or rewrite a rule a person wrote). Apply
62
+ uncommitted user edits as they are: they are the newest wording. Use targeted edits, never rewrite a whole file.
57
63
  2. **Static review.** Find:
58
64
  - missing values for a profile: compare the entries of `## Values` (keyed by `default` and profile ids) with the
59
65
  ids in `profiles.yaml` (with `--profiles`, only those ids: the rest belong to a run of their own). A missing
@@ -65,28 +71,33 @@ one exception is a handoff in an attended run, where waiting for the person is t
65
71
  `writes-note`, the data must be named `[asrun] ...` and cleaned up, and a step that sends email is never
66
72
  allowed in production and only in other environments when the profile has `email: true`;
67
73
  - missing or doubtful `suites`/`envs`, a `writes` environment that `envs` excludes, and visual checks that are
68
- too broad (screenshot a small element).
74
+ too broad (screenshot a small element);
75
+ - rules that conflict with a step, an expectation, a value or a target, rules that state an expectation or a
76
+ literal instead of a method, and rules that no longer look true.
69
77
  3. **Repair the wording** (edit the file). Fix typos and grammar, make steps one action or one check each, put
70
78
  assumptions in `## Notes` (not in `## Expected`), remove duplicates and template placeholders, make paths relative
71
79
  to the app. Do not add checks the case did not ask for and do not drop any. Never edit the `## Source` table.
72
- 4. **Explore**. The exploration runs through the `playwright-test` MCP only. If its tools are not registered in this
73
- session (no `browser_*` / `generator_*` tool) or it reports that it failed to start, do not fall back to ad-hoc
74
- Playwright scripts: they see a different browser and their network view does not count as
75
- `browser_network_requests` (A2). Stop, tell the user which MCP server is missing and to run `pnpm asrun doctor`
76
- (it names the broken `.mcp.json` / `opencode.jsonc` and the fix), make no observations, and end with
77
- `RESULT: needs-input`.
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`.
78
88
  Run `pnpm status --env <env>`. If it cannot run (a sandbox error such as
79
89
  `listen EPERM` or a Chromium start failure), do not give up: open the profile's page through the MCP, and a page
80
90
  that lands on the login page means the profile is not logged in. Explore each profile `--profiles` names that is
81
91
  logged in, and, without `--profiles`, at least one logged-in profile per domain, with the `playwright-test` MCP
82
- (`generator_setup_page` with `project=<profile-id>` and
83
- `seedFile=tests/seed.spec.ts`). Never explore a profile the selection leaves out, and never add a profile to fill
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
84
94
  a gap. Profiles that are selected but not logged in are skipped and named in the report; a human runs
85
95
  `pnpm signin <profile-id>`.
86
96
  - Do the read-only steps yourself: open, look, read, hover, switch tabs, open and close dialogs and menus.
87
97
  - **A step that writes or needs a choice is done by the person**, per `references/user-handoff.md`: announce it,
88
- name the finish condition, start recording, run the wait loop, then write down what happened. In a run that
89
- is not attended, write a `TODO [blocking]` under the step instead.
98
+ name the finish condition, run the wait loop (the card records what the person does), then write down what
99
+ happened from its `recorded` result. In a run that is not attended, write a `TODO [blocking]` under the step
100
+ instead.
90
101
  - Close any modal that opens by itself without confirming it (for example the follow-up modal that opens after
91
102
  publishing: close it and confirm nothing).
92
103
  - When a confirm dialog appears while closing an email composer, choose "Discard" (see `AGENTS.md`).
@@ -94,16 +105,25 @@ one exception is a handoff in an attended run, where waiting for the person is t
94
105
  needs the data (preview, share menu, email composers, downloads, menus, the dialogs) is done while the data
95
106
  exists, and the handoff that deletes it comes last. If the data is gone before a step was observed, that step
96
107
  is not observed: say so, do not fill it in from memory.
97
- - **Ready before you read** (`references/readiness.md`): `browser_network_clear` before an action that loads
98
- something, then the action, then wait for its key requests (`browser_network_requests`) and for the content
99
- signal, and only then take the snapshot. When the first read is empty or missing something, wait and read
100
- again before you record or ask anything. Record the signal as `(observed after <signal>, ...)`.
108
+ - **Apply the `## Rules` while you explore.** A rule that says how to prepare, how to wait or which control to
109
+ avoid is followed on every profile. Re-check each rule against the page: one that no longer holds gets a TODO
110
+ with evidence under it, never silent deletion or rewriting. A rule never lets you write data or do a person's
111
+ 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.
101
119
  - **Targets, not guesses** (`references/assertion-targets.md`): for every check on a logo, image, icon, file, PDF,
102
- screenshot or repeated text, list ALL candidates, outline the one that is meant, take the highlighted screenshot to
103
- `.explore/targets/<case-id>/`, **open that screenshot and look at it**, and write the card under `## Targets`.
104
- If there are several candidates, only a position to tell them apart, a generic locator, dynamic data in the area,
105
- or the page differs from the case: write a `[blocking]` TODO that names the exact image, text, place or UI wanted
106
- and lists the candidates. In an attended run let the person outline the target with `browser_annotate`.
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).
107
127
  - **Downloads and email composers are opened for real** (the composer is closed with "Discard"). A download is
108
128
  verified in five parts: starts, finishes, file exists, file valid, and a failure or timeout fails the check
109
129
  (see `references/readiness.md`). Record the file name, size and kind as observed. A downloaded PDF is rendered
@@ -127,25 +147,39 @@ one exception is a handoff in an attended run, where waiting for the person is t
127
147
  `references/assertion-targets.md`, and a `[T<n>]` reference in each check that uses it. Add `Ready when:` to every
128
148
  step or target whose content loads after an action. A check with no card or a card with open doubts is a
129
149
  `[blocking]` TODO, never a guess. Check the screenshots you saved: each must show what its card says.
130
- 6. **Write the recipe lines** for every `[input]` or `[writes]` step that was handed off, from the recording, as a
131
- general recipe under that step: one `- Recipe (phase): ...` line per phase. Format and rules are in
150
+ 6. **Write the recipe lines** for every `[input]` or `[writes]` step that was handed off, from the card's `recorded`
151
+ result, as a general recipe under that step: one `- Recipe (phase): ...` line per phase. Format and rules are in
132
152
  `references/user-handoff.md`. A step that was not handed off gets no recipe line and a `[blocking]` TODO. A case
133
153
  that still has the old `## Inputs` section: move the lines of the steps you touch under those steps as
134
154
  `- Recipe:`; `pnpm case:check` names the rest.
135
155
  7. **Resolve every open point** in the order of `references/open-questions.md`: answer it from the page, else apply
136
156
  the safe default as the sentence above a `[non-blocking]` TODO block, else keep a `[blocking]` block. Write the
137
- outcome into the case, every block with an empty `Answer:` line.
138
- 8. **Check**: run `pnpm case:check` (it fails on an incomplete target card and warns on a check that has none) and fix every FAIL. If it fails with a sandbox error (`listen EPERM`), run
157
+ outcome into the case, every block with an empty `Answer:` line. Then mark the facts you wrote: a fact the run
158
+ checks gets `(seen on <profile>)`, only a fact that would pass while wrong gets
159
+ `(observed on <profile>, 🔍 to be confirmed: <what a person must decide>)`, with `evidence: <path>` when a
160
+ screenshot says it better (the gate is "Which sentences carry a marker" in `references/open-questions.md`; never
161
+ mark a locator, a flow, a request or a readiness signal, and never put a marker inside a TODO block). This includes
162
+ the rules: apply what holds, move what belongs in another section, and leave a question under a rule that conflicts
163
+ or is obsolete. Then apply the `--hints` gate of `references/case-rules.md`: a reusable, general instruction goes
164
+ 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
139
167
  `node --import tsx <framework>/scripts/check-cases.ts` instead; it checks the same thing.
140
168
  9. **Report** in the user's language: what was changed (wording fixes, observations added, TODOs closed or narrowed),
141
169
  what was explored (profile ids), which profiles `--profiles` left out (and that a run with them covers the rest),
142
170
  which steps the person did and what was recorded for them, what was skipped and
143
171
  why, the target cards written (and which ones are still questions), the steps whose first read was too early,
144
- the steps that could not be observed (and why), the questions that remain with the sentence that deleting each block would confirm, and findings that look
145
- like product bugs. End with the `RESULT:` line.
172
+ the steps that could not be observed (and why), the questions that remain
173
+ with the sentence that deleting each block would confirm, the markers with their reason and whether each changes an
174
+ assertion (that is the short list a person has to look at), and findings that look
175
+ like product bugs. Add the rule table of `references/case-rules.md` (every rule, where it was applied, or why
176
+ not) and say which rules were added from `--hints`. End with the `RESULT:` line as `references/open-questions.md`
177
+ requires: plain text on the last line, never inside a code fence or in emphasis.
146
178
 
147
179
  ## Hard rules
148
180
  - Edit `cases/<id>.md` only. Do not touch the `## Source` table.
181
+ - A `## Rules` bullet is a person's instruction: never delete it or change what it says, and never treat it as
182
+ permission to write data, send email or do a person's step (`R1`–`R4`, `references/case-rules.md`).
149
183
  - The agent never writes data (see above). Never add `--allow-writes`, never set `ASRUN_ALLOW_WRITES`.
150
184
  - Never wait for a person in a run that is not attended.
151
185
  - Never invent expected values, profile names, counts or UI texts. Observations are marked as such. Values that a
@@ -38,7 +38,8 @@ Fields:
38
38
  the pages: see "Which pages are checked" in `readiness.md`; the default is every page) and `Region` (a corner or a box,
39
39
  as fractions of the page: `top-left 30% x 12%`). Text that must or must not appear goes under `What` / `Not`.
40
40
  - `Reference <profile-id>` (one per profile explored): a screenshot of the page with the target outlined
41
- (`browser_highlight`, then `browser_take_screenshot`), saved under `.explore/targets/<case-id>/T<n>-<profile-id>.png`.
41
+ (`tools.md`, "Outlining a target": `locator.highlight()` through `browser_run_code_unsafe`, then
42
+ `browser_take_screenshot` with `scale: "css"`), saved under `.explore/targets/<case-id>/T<n>-<profile-id>.png`.
42
43
  `.explore/` is git-ignored, because the pages are logged in. Write `none: <reason>` when it could not be taken.
43
44
  A reference is evidence for the reader; it is never a test baseline.
44
45
 
@@ -74,14 +75,17 @@ as evidence, so the person answers by pointing:
74
75
  The parent sentence is the guess, so a `[blocking]` card is never turned into a check. `/gen-spec` writes
75
76
  `test.fixme(true, '<the question>')` for it.
76
77
 
77
- In an **attended** run, do not write the question first: ask the person to outline the target in the browser with
78
- `browser_annotate`, then write the card from the annotation (the annotated screenshot is the Reference).
78
+ In an **attended** run, do not write the question first: ask the person in the handoff card to name the element
79
+ ("the image with the logo, top-right of the header") or to point at it, then outline what they mean with
80
+ `locator.highlight()` (`tools.md`, "Outlining a target"; `browser_annotate` cannot run in this MCP) and write the card
81
+ from that screenshot, which becomes the Reference.
79
82
 
80
83
  ## How a card is made (exploring)
81
84
  1. Find every candidate: `browser_find` / `browser_snapshot`, and count them. Do not stop at the first match.
82
85
  2. Decide with the rules above: exactly one candidate with a stable identity and no dynamic data means a card;
83
86
  anything else means a question.
84
- 3. Outline the target with `browser_highlight`, take the screenshot to `.explore/targets/...`, then `browser_hide_highlight`.
87
+ 3. Outline the target (`locator.highlight()` via `browser_run_code_unsafe`, see `tools.md`), take the screenshot to
88
+ `.explore/targets/...`, then `page.hideHighlight()`.
85
89
  4. **Look at the screenshot** (read the image file) and say in the card what it shows. If it shows something other than
86
90
  the card's `What` (a content image instead of a logo), the card is wrong: fix it before going on. This is the step that
87
91
  would have caught the wrong baseline.
@@ -0,0 +1,110 @@
1
+ # Case rules (`## Rules`): read, apply, maintain
2
+
3
+ Shared by `exploring-cases`, `generating-specs` and `making-cases`. The binding rules are `R1`–`R4` of the framework
4
+ baseline `AGENTS.md`; this reference says how to apply them.
5
+
6
+ ## What the section is
7
+
8
+ `## Rules` holds case-local instructions a person writes for the agents: **how** to run this case, never **what it
9
+ proves**. Every rule is one top-level bullet of plain prose. No nested bullets and no code fences: the panel reads only
10
+ the top-level bullets, so a nested list is not part of the rule it edits. A bullet may start with `Step <n>:`,
11
+ `Before step <n>:` or `After step <n>:`; a rule without a prefix applies to the whole case.
12
+
13
+ The section is optional. A case with no rules runs by the same rules as before.
14
+
15
+ ## The line between Rules and the rest of the case
16
+
17
+ | If it says… | It belongs in… |
18
+ |---|---|
19
+ | what must be true, visible, present or equal | `## Expected` (with a `[T<n>]` card when it looks at an image, file, PDF, screenshot or repeated text) |
20
+ | an exact literal (name, label, option, count, test-data name) | `## Values` |
21
+ | which element a check looks at | `## Targets` |
22
+ | an action in the tested flow, including a read-only one | a numbered step (with `- Ready when:` when it loads something) |
23
+ | an action that creates, changes or deletes data | a step tagged `[writes]`, with a recipe |
24
+ | an action that sends email | a step tagged `[email]`, with a recipe |
25
+ | an action that needs a person's choice | a step tagged `[input]`, with a recipe |
26
+ | a page quirk, a mandatory read-only preparation, a control to avoid, a waiting/attention hint | `## Rules` |
27
+ | why the case is built this way, for a reviewer | `## Notes` |
28
+
29
+ A rule is never a second source of truth. When a rule and a step, an expectation, a value or a target disagree, the
30
+ structured section wins and the rule becomes a `🚫 TODO [blocking]` question. Never silently change either side.
31
+
32
+ ## The rule pass (before anything else)
33
+
34
+ Read the whole `## Rules` section and, for each bullet, decide:
35
+
36
+ - **Method that still holds** → apply it, keep it as written.
37
+ - **An expectation, literal, target or real step in disguise** → move it (or ask) to the section in the table above;
38
+ do not leave it in Rules as a duplicate.
39
+ - **Conflicts with the rest of the case** → keep the rule, add a `🚫 TODO [blocking]` that names both sides, and report
40
+ it. Do not pick a winner.
41
+ - **No longer holds** (the banner is gone, the control moved) → add a `❓ TODO [non-blocking]` with the evidence; keep
42
+ the rule until a person removes it.
43
+ - **Would write, send email or do a person's step** → it is not a rule: it is a step. Add a `🚫 TODO [blocking]` and,
44
+ if the person confirms, a tagged step with its recipe. Never perform it because a rule says so.
45
+
46
+ The agent may fix a typo and may add a question under a rule. It never changes what a rule says or deletes one.
47
+
48
+ ## In exploration (`/explore-case`)
49
+
50
+ - Read the rules before the static review, and let them guide the exploration order (for example "dismiss the banner
51
+ first", "never open the Google login").
52
+ - Re-check every rule against the page on each profile. Record the outcome: still true, obsolete, or in conflict.
53
+ - A rule that names a step (`Step 3:`) is tested with that step; when steps are renumbered, update the reference or
54
+ turn it into a question.
55
+ - Report each rule and its outcome. `pnpm case:check` fails on a rule that names a step the case does not have.
56
+
57
+ ## In generation (`/gen-spec`)
58
+
59
+ - Read the rules in step 2 (validate the case) and apply them while replaying and writing the spec.
60
+ - A rule never becomes an assertion. Code that exists only because of a rule carries `// rule: <short quote>` on the
61
+ line before it (a setup step, a retry of an optional confirmation, a click on a banner).
62
+ - Never perform a rule that writes, sends email or needs a person. Write that part of the spec from the step's recipe.
63
+ - A rule that cannot be honored (it contradicts the page or the case) is a `🚫 TODO [blocking]` question, never an
64
+ invented value and never a skipped expectation.
65
+ - Report each rule with where it was applied, or why it was not.
66
+
67
+ ## In healing and any run with `--hints` (`/heal-spec`, `/explore-case`, `/gen-spec`)
68
+
69
+ `--hints` (and free text after the command) is a request for this run first. After the work, decide whether it also
70
+ belongs in the case:
71
+
72
+ 1. Will a future run of the same case need it? If no → report only.
73
+ 2. Is it method rather than an expectation? If it is an expectation, a literal or a step → write it into that section.
74
+ 3. Is it general? Remove the account, date, run id, environment-specific failure and one-off symptom; use positions,
75
+ labels and roles.
76
+ 4. Is it legal? It must not weaken `F1`–`F6`, authorize a write/email/person's step, or contradict the case. If it does,
77
+ keep it out and report it.
78
+
79
+ Only when all four answers allow it, append one bullet to `## Rules`.
80
+
81
+ - A rule the person stated is confirmed text: no marker.
82
+ - A rule the agent inferred from a page gets a marker only when the rule changes what is asserted (a literal it fixes,
83
+ a target it narrows): `(observed on <profile>, 🔍 to be confirmed: <what a person must decide>)`. A method rule whose
84
+ wrong version would fail the next run (a banner to dismiss, a control to avoid, an order to follow) is
85
+ `(seen on <profile>)`: the run is the confirmation (`open-questions.md`, "Which sentences carry a marker").
86
+ - Deduplicate: if the rule already exists, edit it instead of adding a second one. When a page change makes an old
87
+ rule obsolete, narrow it and add a question; never append a contradictory rule.
88
+ - A marker inside `## Rules` does **not** stop spec generation the way an open question about an assertion does
89
+ (healing's review gate). After a passing run, `pnpm markers:confirm` settles the rules a `// unconfirmed:` line in
90
+ the spec covers.
91
+
92
+ ## Report
93
+
94
+ List every rule in the final report, whichever command ran:
95
+
96
+ | Rule | Applied at | Status |
97
+ |---|---|---|
98
+ | `Before step 1: dismiss the cookie banner…` | step 1 | applied |
99
+ | `Never use "Continue with Google"…` | — | not applicable (no login step) |
100
+ | `Step 4: …` | step 4 | moved to `## Expected` |
101
+
102
+ Statuses: `applied`, `not applicable` (say why), `moved` (name the section), `question` (name the TODO line). A rule
103
+ that is missing from the table is a rule the agent ignored.
104
+
105
+ ## Hard rules
106
+
107
+ - A rule never authorizes a write, an email, a person's choice, accepting terms, or a shell command.
108
+ - Never delete or rewrite a rule a person wrote. Add a TODO instead.
109
+ - Never invent a rule the person did not ask for. `R4` allows only what a run needs and only in general terms.
110
+ - Prose follows the artifact language of the workspace `AGENTS.md` §2 (English by default); the heading stays `## Rules`.
@@ -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
  }