@asrunjs/asrun 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/.claude/commands/explore-case.md +8 -3
  2. package/.claude/skills/exploring-cases/SKILL.md +72 -14
  3. package/.claude/skills/exploring-cases/references/handoff-card.js +81 -7
  4. package/.claude/skills/exploring-cases/references/notes.md +42 -4
  5. package/.claude/skills/exploring-cases/references/open-questions.md +2 -2
  6. package/.claude/skills/exploring-cases/references/readiness.md +6 -0
  7. package/.claude/skills/exploring-cases/references/user-handoff.md +60 -27
  8. package/.claude/skills/generating-specs/SKILL.md +10 -3
  9. package/AGENTS.md +12 -4
  10. package/README.md +14 -2
  11. package/dist/lib/desktop-notify.d.ts +38 -0
  12. package/dist/lib/desktop-notify.js +158 -0
  13. package/dist/lib/explore-notes.d.ts +15 -0
  14. package/dist/lib/explore-notes.js +34 -1
  15. package/dist/lib/migrations.d.ts +43 -4
  16. package/dist/lib/migrations.js +39 -7
  17. package/dist/lib/paths.d.ts +9 -2
  18. package/dist/lib/paths.js +32 -5
  19. package/dist/lib/pdf.js +23 -3
  20. package/dist/lib/scaffold.d.ts +87 -0
  21. package/dist/lib/scaffold.js +220 -0
  22. package/dist/lib/term.d.ts +36 -0
  23. package/dist/lib/term.js +77 -0
  24. package/dist/lib/update-check.d.ts +1 -7
  25. package/dist/lib/update-check.js +3 -15
  26. package/dist/lib/upgrade-prompt.d.ts +84 -0
  27. package/dist/lib/upgrade-prompt.js +233 -0
  28. package/dist/migrations/index.d.ts +12 -4
  29. package/dist/migrations/index.js +40 -5
  30. package/dist/migrations/manifest.json +27 -1
  31. package/dist/scripts/build.js +4 -1
  32. package/dist/scripts/check-cases.js +3 -3
  33. package/dist/scripts/cli.js +11 -3
  34. package/dist/scripts/doctor.js +73 -4
  35. package/dist/scripts/explore-notes.js +19 -2
  36. package/dist/scripts/init.d.ts +6 -26
  37. package/dist/scripts/init.js +82 -101
  38. package/dist/scripts/migrate.js +84 -21
  39. package/dist/scripts/migrations-check.d.ts +1 -0
  40. package/dist/scripts/migrations-check.js +272 -0
  41. package/dist/scripts/migrations-freeze.d.ts +1 -0
  42. package/dist/scripts/migrations-freeze.js +126 -0
  43. package/dist/scripts/notify-test.d.ts +1 -0
  44. package/dist/scripts/notify-test.js +48 -0
  45. package/dist/scripts/upgrade.js +53 -10
  46. package/dist/ui/server/actions.js +4 -2
  47. package/dist/ui/server/ai-stream.d.ts +7 -1
  48. package/dist/ui/server/ai-stream.js +26 -7
  49. package/dist/ui/server/app.js +4 -0
  50. package/dist/ui/server/jobs.d.ts +10 -0
  51. package/dist/ui/server/jobs.js +41 -1
  52. package/dist/ui/shared/ai-commands.js +4 -2
  53. package/dist/ui/shared/types.d.ts +8 -0
  54. package/docs/agents/case-format.md +8 -1
  55. package/docs/agents/framework-dev.md +44 -5
  56. package/docs/ai-workflow.md +27 -10
  57. package/docs/cases.md +5 -5
  58. package/docs/commands.md +73 -15
  59. package/docs/install.md +18 -5
  60. package/docs/troubleshooting.md +17 -0
  61. package/docs/web-ui.md +23 -8
  62. package/package.json +5 -2
  63. package/templates/.claude/commands/explore-case.md +9 -3
  64. package/templates/.claude/commands/upgrade-workspace.md +21 -0
  65. package/templates/AGENTS.md +1 -1
  66. package/templates/package.json +1 -1
  67. package/ui/dist/assets/{dist-C46EmZuI.js → dist-B4pM62Hc.js} +1 -1
  68. package/ui/dist/assets/{dist-DGzg2gyA.js → dist-BFADGd4p.js} +1 -1
  69. package/ui/dist/assets/{dist-P9roUi1O.js → dist-Bd-TmWTD.js} +1 -1
  70. package/ui/dist/assets/{dist-DMiOs9Qj.js → dist-Bt053MSo.js} +1 -1
  71. package/ui/dist/assets/{dist-DrFzCwMc.js → dist-CD9RixNE.js} +1 -1
  72. package/ui/dist/assets/{dist-_0g6iC9K.js → dist-ChACtLiA.js} +1 -1
  73. package/ui/dist/assets/{dist-D9GmSkjm.js → dist-CiW9sEPF.js} +1 -1
  74. package/ui/dist/assets/{dist-Db_XhDui.js → dist-CxEpzoZU.js} +1 -1
  75. package/ui/dist/assets/{dist-BB2cF3XN.js → dist-Dh7um7By.js} +1 -1
  76. package/ui/dist/assets/{dist-Dy45nXf8.js → dist-gHwKkZ6g.js} +1 -1
  77. package/ui/dist/assets/{index-BN4ucmmn.js → index-Ckax27uf.js} +158 -158
  78. package/ui/dist/index.html +1 -1
@@ -8,14 +8,19 @@ Rules combine in this order, and a later one wins where they conflict: the frame
8
8
  framework skill, `.claude/skills/exploring-cases.md` if it exists, then the workspace `AGENTS.md` §2 in `examples/demo`
9
9
  (Project rules). F1–F6 and R2 always win. The effective set is what `pnpm asrun rules` prints; §2 is applied as
10
10
  written and is never turned into a question (a difference from a framework default is not a conflict).
11
- 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
+ Arguments: `<case-id> <env> [--attended] [--profiles a,b] [--lead <profile-id>] [--hints <file>]`. The case id is the path below cases/ without `.md` (it may include a group
12
12
  folder, e.g. documents/landing-page) and is required. `<env>` is `staging` or `production`; it is required and there is no
13
13
  default (without one, stop and say so). With `--attended` I am at the machine and the browser window is visible: when a step
14
14
  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
15
  recipe under the step it belongs to (`- Recipe:` lines). Without `--attended`, never wait for me.
16
16
  `--profiles a,b` explores only those profiles (comma-separated ids from profiles.yaml); without it, at least one
17
- logged-in profile per domain. A profile the list leaves out is not opened and nothing is written about it: runs add up,
18
- so a later run covers the rest.
17
+ logged-in profile per domain. Every id in that list is opened in its own session (`generator_setup_page` with
18
+ `project=<id>`); two different hosts are two domains, even for the same product. A profile the list leaves out is not
19
+ opened and nothing is written about it: runs add up, so a later run covers the rest.
20
+ `--lead <profile-id>` explores the selection asymmetrically: that profile in full, every other selected profile only
21
+ where it can differ (its own `## Values` entries, its target screenshots and the steps that reach them), as the skill's
22
+ "Profile passes" says; a profile whose page or flow differs is explored in full and the report says so. Keep lead and
23
+ diff profiles in one run, because the notes are written at its end.
19
24
  `--hints <file>` (or free text after the arguments) is what I know or want checked; read it first. After the work, a
20
25
  reusable instruction goes into `## Rules` by the gate in `references/case-rules.md`; a one-off clue stays in the report.
21
26
  Read `## Rules` before the steps and apply it. Follow `case-rules.md` (a rule says how to run the case, never what it
@@ -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. 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.
3
+ description: Explores the real pages for one case document (cases/<id>.md) before its spec is generated, for the profiles named with --profiles or, without it, for at least one logged-in profile per domain. With --lead <id> one profile is explored in full and the others only where they can differ. 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
@@ -54,10 +54,17 @@ one exception is a handoff in an attended run, where waiting for the person is t
54
54
  before exploring and report `RESULT: needs-input` ("pass the environment").
55
55
  - Optionally `--attended`: a person is at the machine and the browser window is visible, so handoffs may happen.
56
56
  Without it nothing waits, and a step that needs a person becomes a `[blocking]` TODO.
57
- - Optionally `--profiles a,b`: explore only these profiles (ids from `profiles.yaml`, comma-separated). Without it, at
58
- least one logged-in profile per domain. The selection is never widened: a profile it leaves out is not opened and
59
- nothing about it is written into the case. Runs add up: a later run with other profiles adds their observations and
60
- `## Values` entries next to the ones recorded before.
57
+ - Optionally `--profiles a,b`: explore only these profiles (ids from `profiles.yaml`, comma-separated): **every id in
58
+ the list**. Without it, at least one logged-in profile per domain. A different host is a different domain, even when
59
+ two profiles serve the same product on another address, so the list is never collapsed into one profile. The
60
+ selection is never widened: a profile it leaves out is not opened and nothing about it is written into the case.
61
+ Runs add up: a later run with other profiles adds their observations and `## Values` entries next to the ones
62
+ recorded before.
63
+ - Optionally `--lead <profile-id>`: explore the profiles of this run asymmetrically. The lead profile (one of the ids
64
+ this run explores, normally the first) is explored **in full**; every other selected profile gets the **diff pass**
65
+ of step 4: only what can differ on it, plus what reaches it. Without the flag every profile is explored in full.
66
+ Lead and diff profiles belong in **one run**: the case edits of a diff pass would make a note written before them
67
+ stale, so a split run falls back to a replay instead of the notes.
61
68
  - Optionally `--hints <file>` (or free text after the arguments): what the person knows or wants checked. Read it
62
69
  before the static review. It guides this run; after the work, a reusable instruction is written into `## Rules` by
63
70
  the gate in `references/case-rules.md`, and a one-off clue stays in the report.
@@ -90,14 +97,46 @@ one exception is a handoff in an attended run, where waiting for the person is t
90
97
  in this session at all (no `browser_*` / `generator_*` tool) or the server reports that it failed to start, do not
91
98
  fall back to ad-hoc Playwright scripts: stop, tell the user which MCP server is missing and to run `pnpm asrun
92
99
  doctor`, make no observations, and end with `RESULT: needs-input`.
93
- Explore each profile `--profiles` names that is logged in, and, without `--profiles`, at least one logged-in profile
94
- per domain. Never explore a profile the selection leaves out, and never add a profile to fill a gap. Profiles that
100
+ Explore each profile `--profiles` names that is logged in, and, without `--profiles`, one logged-in profile per
101
+ domain. Never explore a profile the selection leaves out, and never add a profile to fill a gap. Profiles that
95
102
  are selected but not logged in are skipped and named in the report; a human runs `pnpm signin <profile-id>`.
103
+ **One profile at a time, one session each.** Open it with
104
+ `generator_setup_page({project: '<profile-id>', seedFile: 'tests/seed.spec.ts', plan: ''})`; never call it without
105
+ `project` (the MCP then opens whichever project it likes). Before the first read, check that `page.url()` starts
106
+ with that profile's `baseURL` in `profiles.yaml`: if it does not, the wrong project is open, so open the right one
107
+ and record nothing from the wrong page. A different host is a different domain even when both profiles serve the
108
+ same product, so two named profiles mean two sessions, never one. Finish every selected profile before you start
109
+ writing the case.
110
+ **Profile passes (`--lead`).** Without the flag every selected profile is explored in full. With `--lead <id>` the
111
+ lead profile is explored in full (every step, every handoff, every recipe, every target, a note with every step) and
112
+ each other selected profile gets the **diff pass**:
113
+ - **Confirm the structure first.** Open the profile and check that it is the same application as the lead's: the same
114
+ header, navigation, page layout and components around each differing check. A different host is no reason to skip
115
+ this profile, and no reason to assume it matches: it is the same app with another address, style or dataset. A page,
116
+ a control or a step that differs from the lead's, and any profile whose flow needs another step, makes that
117
+ profile a **full pass** — explore it as if `--lead` had not been given, and say so in the report.
118
+ - **Do only the differing steps** and the read-only navigation and `Ready when` signals that reach them. A step
119
+ differs when (a) a value it asserts comes from that profile's own `## Values` entry, (b) it reads a `[T<n>]` whose
120
+ card needs this profile's `Reference <profile-id>:`, (c) a `## Rules` bullet tied to that step says so, or (d) its
121
+ page differs from the lead's. When in doubt whether a step can differ, run it: the diff pass never assumes.
122
+ - **A write step that a differing check needs is still a handoff** on that profile: data of a test account or a
123
+ tenant is usually private to it. Hand off only what reaches the check (create, observe, clean up, in that order),
124
+ and record the observation from the card's `recorded` result as on any profile.
125
+ - **Record the difference, not the flow**: one observation per differing check, one `Reference <profile-id>:`
126
+ screenshot for each target card it confirms, and that profile's own `## Values` entries. Every other step stays
127
+ as the lead observed it and is reported as `observed on <lead>, not verified on <profile>`; do not write a second
128
+ copy of the steps, a marker or a TODO about them.
129
+ - **What it cannot reach is a gap, not an assumption**: a differing check whose page cannot be opened gets a
130
+ `TODO [blocking]` under the step that needs it, never "same as the lead".
131
+ - **A step whose recipe would differ per profile** (another editor, another dialog, an extra step) is explored in
132
+ full on that profile, and reported: the case's `- Recipe:` lines are per step, not per profile, so a single recipe
133
+ cannot describe both. When the difference is real, put a `❓ TODO [non-blocking]` under the step asking whether the
134
+ case should be split; never rewrite the lead's recipe to fit one profile.
96
135
  - Do the read-only steps yourself: open, look, read, hover, switch tabs, open and close dialogs and menus.
97
- - **A step that writes or needs a choice is done by the person**, per `references/user-handoff.md`: announce it,
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.
136
+ - **A step that writes or needs a choice is done by the person**, per `references/user-handoff.md`: announce it
137
+ with one sentence plus one action per entry of `steps` and the finish condition in `finish`, run the wait loop
138
+ (the card records what the person does), then write down what happened from its `recorded` result. In a run that
139
+ is not attended, write a `TODO [blocking]` under the step instead.
101
140
  - Close any modal that opens by itself without confirming it (for example the follow-up modal that opens after
102
141
  publishing: close it and confirm nothing).
103
142
  - When a confirm dialog appears while closing an email composer, choose "Discard" (see `AGENTS.md`).
@@ -122,7 +161,8 @@ one exception is a handoff in an attended run, where waiting for the person is t
122
161
  in the five parts of `references/readiness.md`, "Downloads", and record the file name, size and kind. Render a
123
162
  downloaded PDF (`pnpm pdf:render`), open its page images and record which pages show the target, where, and the
124
163
  text; the pages follow the case (every page unless the case says ONLY).
125
- - Compare every step with what is really there.
164
+ - Compare every step with what is really there. On a diff pass, that means the steps the pass opened: the rest stay
165
+ as the lead observed them.
126
166
  - **Record the exploration note** for this profile as you go, in the shape of `references/notes.md`: the locator of
127
167
  each step's main control, the data-call paths of each loading action, and each target it confirmed. It is local
128
168
  cache under `.explore/`, never case content, and `/gen-spec` reads it; step 8 writes the file.
@@ -138,7 +178,8 @@ one exception is a handoff in an attended run, where waiting for the person is t
138
178
  (`testDataName`); the recipe records how to build it, never the stamp itself. A step that was not handed off gets
139
179
  no recipe line and a `[blocking]` TODO. A case
140
180
  that still has the old `## Inputs` section: move the lines of the steps you touch under those steps as
141
- `- Recipe:`; `pnpm case:check` names the rest.
181
+ `- Recipe:`; `pnpm case:check` names the rest. A diff pass normally adds no recipe line: the step's recipe from the
182
+ lead pass still describes it. When the pass had to write and the step has no recipe yet, write one from that handoff.
142
183
  7. **Resolve every open point** in the order of `references/open-questions.md`: answer it from the page, else apply
143
184
  the safe default as the sentence above a `[non-blocking]` TODO block, else keep a `[blocking]` block. Write the
144
185
  outcome into the case, every block with an empty `Answer:` line. Then mark the facts you wrote: a fact the run
@@ -157,8 +198,20 @@ one exception is a handoff in an attended run, where waiting for the person is t
157
198
  line of every profile you explored must say `fresh`), and fix every FAIL. If
158
199
  `case:check` fails with a sandbox error (`listen EPERM`), run
159
200
  `node --import tsx <framework>/scripts/check-cases.ts` instead; it checks the same thing.
201
+ **A selected profile that is logged in gets its own note.** The note, the target card's `Reference
202
+ <profile-id>` and at least one observation marked with that profile id are the proof: before you report, compare the
203
+ selection with `.explore/notes/<case-id>/` and with the profile ids the case now names. A profile with no note that
204
+ was not skipped for its login was not explored: write a `TODO [blocking]` under the first step it affects (a person
205
+ re-runs `/explore-case` with that profile) and say so in the report; never report a run as complete for a profile
206
+ that was not opened. A profile skipped because it is not logged in gets no note and is named as skipped instead.
207
+ **A full note covers every step; a diff note covers the steps its pass opened** and names the lead in `depth: "diff"`
208
+ and `basedOn: "<lead profile-id>"` (`references/notes.md`, "Coverage"). A diff note is complete as it is: do not add
209
+ the lead's steps to it, and do not report the profile as unexplored. `pnpm asrun explore:notes <case-id> --check`
210
+ warns when a diff note's lead has no note of its own.
160
211
  9. **Report** in the user's language: what was changed (wording fixes, observations added, TODOs closed or narrowed),
161
- what was explored (profile ids), which profiles `--profiles` left out (and that a run with them covers the rest),
212
+ what was explored (profile ids), which profile was explored in full and which in a diff pass (with the steps each
213
+ diff pass opened, which checks stay `observed on <lead>, not verified on <profile>`, and any profile that turned out
214
+ to need a full pass and why), which profiles `--profiles` left out (and that a run with them covers the rest),
162
215
  which steps the person did and what was recorded for them, what the exploration note covers (and what it does not),
163
216
  what was skipped and
164
217
  why, the target cards written (and which ones are still questions), the steps whose first read was too early,
@@ -181,6 +234,11 @@ one exception is a handoff in an attended run, where waiting for the person is t
181
234
  check asserts are written into the case's `## Values` under the profile id (or `default` when they hold for every
182
235
  profile), and never into `profiles.yaml`.
183
236
  - Never explore a profile that `--profiles` leaves out, and never widen the selection to cover a gap in the case.
237
+ - A diff pass never assumes: a page, a control or a step that differs from the lead's makes that profile a full pass,
238
+ and a differing check it cannot reach is a `TODO [blocking]`, never "same as the lead".
239
+ - The diff pass covers values, targets and the pages that reach them. A step whose recipe differs per profile is
240
+ explored in full on that profile and reported (a `- Recipe:` belongs to a step, not to a profile), never folded into
241
+ the lead's recipe.
184
242
  - Never pick a target when in doubt, and never record a fact that was read before the content finished loading.
185
243
  - Never read or print anything under `.auth/`. Never automate login or 2FA.
186
244
  - Prose follows the artifact language of the workspace `AGENTS.md` §2 (English by default); structural tokens stay English.
@@ -4,8 +4,25 @@ async (page) => {
4
4
  const ctx = page.context();
5
5
  const cfg = ctx.__handoffCfg || {};
6
6
  const message = cfg.message || 'Do the step, then press Done';
7
+ // The steps are the person's actions, one per line and numbered by the card: a handoff that needs several things
8
+ // never becomes one sentence nobody can follow (see user-handoff.md, "Before the handoff").
9
+ const steps = (Array.isArray(cfg.steps) ? cfg.steps : [])
10
+ .filter((step) => typeof step === 'string' && step.trim())
11
+ .map((step) => step.trim());
7
12
  const copy = Array.isArray(cfg.copy) ? cfg.copy : [];
8
13
  const doneText = cfg.doneText || null;
14
+ // The finish condition gets its own block. `doneText` is the same promise as a text the card can watch for, so an
15
+ // older handoff that only set it still says when the step is done.
16
+ const finish = typeof cfg.finish === 'string' && cfg.finish.trim()
17
+ ? cfg.finish.trim()
18
+ : doneText ? `"${doneText}" appears` : null;
19
+ // The finish condition usually quotes the text it waits for ("Unfavorite Article"). A handoff that left `doneText`
20
+ // out gets that text as its signal, so the card ends by itself; the check below drops it when the page already shows
21
+ // it, because a condition that holds before the person acts must never end the handoff at once.
22
+ const quoted = finish
23
+ ? ((finish.match(/["“]([^"”]{2,})["”]/) || finish.match(/'([^']{2,})'/)) || [])[1]
24
+ : undefined;
25
+ let watchedText = doneText || (quoted ? quoted.trim() : null);
9
26
  const maxMs = cfg.maxMs || 600000;
10
27
  await ctx.grantPermissions(['clipboard-read', 'clipboard-write']).catch(() => {});
11
28
 
@@ -55,7 +72,18 @@ async (page) => {
55
72
  }
56
73
  }
57
74
 
58
- const paint = (p) => p.evaluate(({message, copy}) => {
75
+ /** True when the page's own text holds `text`: the card's text is never part of it (it quotes the condition). */
76
+ const pageHasText = (p, text) => p.evaluate((needle) => {
77
+ const lower = needle.toLowerCase();
78
+ if (!document.body) return false;
79
+ if (document.body.innerText.toLowerCase().includes(lower)) return true;
80
+ // Sites that render inside their own shadow roots are not in innerText.
81
+ return Array.from(document.querySelectorAll('*')).some((el) =>
82
+ el.id !== '__ASRUN_panel' && el.shadowRoot &&
83
+ el.shadowRoot.textContent.toLowerCase().includes(lower));
84
+ }, text).catch(() => false);
85
+
86
+ const paint = (p) => p.evaluate(({message, steps, finish, copy, autoEnd}) => {
59
87
  if (document.getElementById('__ASRUN_panel')) return;
60
88
  const KEY = '__ASRUN_panel_state';
61
89
  let state = {minimized: false, left: null, top: null};
@@ -74,24 +102,55 @@ async (page) => {
74
102
  host.style.cssText = 'all:initial;position:fixed;z-index:2147483647;left:16px;bottom:16px';
75
103
  const root = host.attachShadow({mode: 'open'});
76
104
  root.innerHTML = '<style>' +
77
- '.card{width:300px;max-width:calc(100vw - 32px);background:#1e3340;color:#fff;font:13px/1.4 system-ui,sans-serif;border-radius:8px;box-shadow:0 6px 20px rgba(0,0,0,.35);overflow:hidden}' +
105
+ '.card{width:340px;max-width:calc(100vw - 32px);background:#1e3340;color:#fff;font:13px/1.4 system-ui,sans-serif;border-radius:8px;box-shadow:0 6px 20px rgba(0,0,0,.35);overflow:hidden}' +
78
106
  '.card.min{width:auto}.card.ok{background:#15803d}' +
79
107
  '.bar{display:flex;align-items:center;gap:8px;padding:7px 8px 7px 12px;background:#b45309;cursor:grab;user-select:none;touch-action:none}' +
80
108
  '.ok .bar{background:#166534;align-items:flex-start;padding-top:9px}.bar b{flex:1;min-width:0;font-size:12px;letter-spacing:.04em;white-space:nowrap}.ok .bar b{white-space:normal;line-height:1.35}.grip{display:inline-flex;opacity:.8}' +
81
109
  'button{all:unset;box-sizing:border-box;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:4px;padding:3px 8px;border-radius:4px;background:rgba(255,255,255,.18);color:#fff;font:600 12px system-ui,sans-serif}' +
82
110
  'button:hover{background:rgba(255,255,255,.3)}' +
83
- '.body{padding:10px 12px 12px;display:flex;flex-direction:column;gap:8px}.min .body,.ok .body{display:none}' +
111
+ // The steps scroll on their own: a long list never pushes the finish condition or the buttons out of the card.
112
+ '.body{display:flex;flex-direction:column}.min .body,.ok .body{display:none}' +
113
+ '.content{display:flex;flex-direction:column;gap:9px;padding:10px 12px 0}' +
114
+ '.summary{font-weight:600;white-space:pre-wrap;overflow-wrap:anywhere}' +
115
+ '.steps{margin:0;padding:0 2px 0 1.6em;display:flex;flex-direction:column;gap:6px;max-height:min(30vh,240px);overflow:auto;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.35) transparent}' +
116
+ '.steps li{padding-left:2px;overflow-wrap:anywhere}.steps li::marker{color:#fbbf24;font-weight:700}' +
117
+ // A list longer than its box fades out at the bottom instead of cutting a line in half, so nobody reads it as the end.
118
+ '.steps.scrolly{-webkit-mask-image:linear-gradient(to bottom,#000 calc(100% - 16px),transparent);mask-image:linear-gradient(to bottom,#000 calc(100% - 16px),transparent)}' +
119
+ '.finish{margin:9px 12px 0;border-left:4px solid #22c55e;background:rgba(34,197,94,.18);border-radius:4px;padding:7px 10px;display:flex;flex-direction:column;gap:3px}' +
120
+ '.finish .label{font-size:10px;font-weight:700;letter-spacing:.1em;color:#86efac}' +
121
+ '.finish .cond{font-weight:600;line-height:1.35;overflow-wrap:anywhere}' +
122
+ '.finish .also{font-size:11px;opacity:.8}' +
84
123
  '.mini-done{display:none}.min .mini-done{display:inline-flex}' +
85
124
  '.chip{display:flex;align-items:center;gap:6px;background:rgba(0,0,0,.28);padding:4px 4px 4px 8px;border-radius:4px}' +
86
125
  '.chip span{flex:1;font:12px ui-monospace,Menlo,monospace;word-break:break-all;user-select:all}' +
87
- '.row{display:flex;gap:8px}.row .primary{background:#15803d}.row .primary:hover{background:#16a34a}' +
126
+ '.row{display:flex;gap:8px;padding:12px}.row .primary{background:#15803d}.row .primary:hover{background:#16a34a}' +
88
127
  '</style>' +
89
128
  '<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>' +
90
129
  '<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>' +
91
- '<div class="body"><div id="msg"></div><div id="chips"></div><div class="row">' +
130
+ '<div class="body"><div class="content"><div id="msg" class="summary"></div><ol id="steps" class="steps"></ol><div id="chips"></div></div>' +
131
+ '<div id="finish" class="finish" style="display:none"><div class="label">DONE WHEN</div><div id="cond" class="cond"></div>' +
132
+ '<div id="also" class="also"></div></div>' +
133
+ '<div class="row">' +
92
134
  '<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>';
93
135
  const card = root.getElementById('card'), bar = root.getElementById('bar'), toggle = root.getElementById('toggle');
136
+ // Every text goes in through `textContent`: the message comes from an agent, never from the site under test.
94
137
  root.getElementById('msg').textContent = message;
138
+ const list = root.getElementById('steps');
139
+ if (steps.length) {
140
+ for (const step of steps) {
141
+ const item = document.createElement('li');
142
+ item.textContent = step;
143
+ list.appendChild(item);
144
+ }
145
+ } else list.style.display = 'none';
146
+ if (finish) {
147
+ root.getElementById('cond').textContent = finish;
148
+ // Say which way the wait can end: a card that can never notice the condition must not suggest that it will.
149
+ root.getElementById('also').textContent = autoEnd
150
+ ? 'The card ends by itself when this shows. Done also works.'
151
+ : 'Press Done when you are finished.';
152
+ root.getElementById('finish').style.display = 'flex';
153
+ }
95
154
  const chips = root.getElementById('chips');
96
155
  chips.style.cssText = 'display:' + (copy.length ? 'flex' : 'none') + ';flex-direction:column;gap:6px';
97
156
  for (const text of copy) {
@@ -115,6 +174,10 @@ async (page) => {
115
174
  card.classList.toggle('min', state.minimized);
116
175
  toggle.innerHTML = state.minimized ? ICON.up : ICON.down;
117
176
  root.getElementById('title').textContent = state.minimized ? 'YOUR TURN' : 'YOUR TURN: control is yours';
177
+ // The minimized pill hides the steps and the finish condition: keep both one hover away, so nothing is missed.
178
+ const hidden = state.minimized ? [message, ...steps, ...(finish ? ['Done when: ' + finish] : [])].join('\n') : null;
179
+ bar.title = hidden || 'Drag to move';
180
+ bar.setAttribute('aria-label', hidden || 'AsRun handoff: drag to move');
118
181
  };
119
182
  const place = () => {
120
183
  if (state.left === null) return;
@@ -147,10 +210,21 @@ async (page) => {
147
210
  apply();
148
211
  document.documentElement.appendChild(host);
149
212
  place();
213
+ // Only a list longer than its box gets the fade; a short one must not look cut off. Measured after the card is in
214
+ // the document: before that every height is zero. The fade goes away at the end of the list, where nothing is left.
215
+ const markScroll = () =>
216
+ list.classList.toggle('scrolly', list.scrollHeight - list.clientHeight - list.scrollTop > 1);
217
+ list.addEventListener('scroll', markScroll);
218
+ markScroll();
150
219
  if (!document.title.startsWith('YOUR TURN | ')) document.title = 'YOUR TURN | ' + document.title;
151
- }, {message, copy}).catch(() => {});
220
+ }, {message, steps, finish, copy, autoEnd: Boolean(watchedText)}).catch(() => {});
152
221
 
153
222
  for (const p of ctx.pages()) await p.evaluate(() => { window.__handoff = null; }).catch(() => {});
223
+ // A text the handoff only implies (from its finish condition) is dropped when the page already shows it: it would
224
+ // end the handoff before the person does anything. An explicit `doneText` keeps its meaning as the agent gave it.
225
+ if (!doneText && watchedText) {
226
+ for (const p of ctx.pages()) if (await pageHasText(p, watchedText)) { watchedText = null; break; }
227
+ }
154
228
  const started = Date.now();
155
229
  let outcome = null;
156
230
  try {
@@ -159,7 +233,7 @@ async (page) => {
159
233
  await paint(p);
160
234
  const flag = await p.evaluate(() => window.__handoff || null).catch(() => null);
161
235
  if (flag) outcome = flag;
162
- else if (doneText && await p.getByText(doneText).first().isVisible().catch(() => false)) outcome = 'condition';
236
+ else if (watchedText && await pageHasText(p, watchedText)) outcome = 'condition';
163
237
  }
164
238
  if (!outcome) await page.waitForTimeout(500);
165
239
  }
@@ -2,7 +2,8 @@
2
2
 
3
3
  Local, git-ignored and optional. `exploring-cases` writes it; `generating-specs` (and healing) read it to **verify**
4
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.
5
+ `## Values`, behaviour in `## Expected`, ready signals in `Ready when:`, and assertions never appear here. A note is
6
+ **full** (every step of the case) or **diff** (only the steps a `--lead` diff pass opened); see "Coverage".
6
7
 
7
8
  ## Where it lives
8
9
 
@@ -36,13 +37,47 @@ that is a normal state: generation falls back to replaying the case.
36
37
  ```
37
38
 
38
39
  - 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.
40
+ - `steps` may be `{}` and `targets` may be omitted; every other key is required (`env`, `updatedAt`, `depth` and
41
+ `basedOn` are optional). No other field is accepted.
41
42
  - `caseSha256` is the 64-character hash `pnpm asrun explore:notes <case-id> --hash` prints (not the placeholder above).
42
43
  - `pnpm asrun explore:notes <case-id> --check` validates the files: it fails on a malformed note and warns on a note
43
44
  whose `caseSha256` no longer matches the case (the summary already prints it as `stale`; re-run the exploration, or
44
45
  delete the note and let the case be replayed). A file that fails is ignored by `/gen-spec`.
45
46
 
47
+ ## Coverage
48
+
49
+ A note is either **full** or **diff**:
50
+
51
+ - **full** (the default, and what a note without `depth` is): every step of the case is in `steps`. `/explore-case`
52
+ writes one for a profile it explored without `--lead`.
53
+ - **diff**: the note belongs to a profile that an `--lead` run explored only where it can differ. `depth` is
54
+ `"diff"` and `basedOn` names the lead profile whose note covers the steps this one did not open. `steps` holds only
55
+ the steps the diff pass opened; `targets` holds the cards it confirmed on that profile.
56
+
57
+ ```json
58
+ {
59
+ "version": 1,
60
+ "caseId": "conduit/header-logo",
61
+ "profile": "conduit-b",
62
+ "env": "staging",
63
+ "caseSha256": "<the same hash as the lead's note>",
64
+ "capturedAt": "2026-11-10T08:12:04Z",
65
+ "depth": "diff",
66
+ "basedOn": "conduit-a",
67
+ "steps": {"4": {"url": "/profile/asrun-b", "locators": [{"purpose": "the profile heading", "locator": "page.getByRole('heading', {name: 'asrun-b'})"}]}},
68
+ "targets": {"T1": {"locator": "page.getByRole('img', {name: 'Conduit B'})", "matches": 1}}
69
+ }
70
+ ```
71
+
72
+ Rules for a diff note:
73
+ - `basedOn` is required (`explore:notes --check` fails without it) and must be a profile, not a case or a URL.
74
+ - It is **complete as it is**. `/gen-spec` writes the steps the note does not hold from the lead's note, and reports
75
+ them as `observed on <lead>, not verified on <profile>`. Never pad it with steps copied from the lead's note.
76
+ - A diff note whose lead has no note of its own (or whose lead note is itself a diff note) is a warning, not a failure:
77
+ generation replays the steps neither note holds. A profile that a diff pass turned into a full pass gets a normal
78
+ full note.
79
+ - `caseSha256` is the same for every note of one run: they are all written after the last case edit.
80
+
46
81
  ## What goes in
47
82
 
48
83
  `steps.<n>`
@@ -53,6 +88,8 @@ that is a normal state: generation falls back to replaying the case.
53
88
  - `requests`: the data-call paths the step's action started, taken from `browser_network_requests` above the marker
54
89
  (`tools.md`, "Network requests without a clear"). Path only: `/api/articles`, never a host, a query string or a token.
55
90
  Images, fonts and analytics are not data calls.
91
+ - A diff note holds only the steps its pass opened; the steps it did not open come from `basedOn` and are not copied
92
+ here.
56
93
 
57
94
  `targets.T1`
58
95
  - `locator`: the card's `Identified by`, confirmed on the page.
@@ -76,7 +113,8 @@ the name of the item, because the data changes between runs. The same rule as a
76
113
  ## Freshness and the write-back
77
114
 
78
115
  - `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.
116
+ the note is **stale**: `/gen-spec` replays the case and does not use it. Every note of one run shares the hash, because
117
+ `/explore-case` writes them after its last case edit; a later run that edits the case makes them stale together.
80
118
  - `/gen-spec` confirms an entry of a fresh note on the page and may write the verified locator or request back
81
119
  (`updatedAt` gets the time). It never adds a step or a target, and it never touches a stale note.
82
120
  - Deleting `.explore/notes/` is always safe: generation then re-derives everything from the page.
@@ -45,8 +45,8 @@ Rules for writing it:
45
45
  It needs no TODO block. See "Marker or question, never both" below.
46
46
 
47
47
  ## Where the block hangs
48
- The parent line is the sentence the question is about: the panel shows it on its own, next to "The text above this
49
- question", so it has to make sense without the rest of the document.
48
+ The parent line is the sentence the question is about: the panel shows it on its own, under "The case says", so it
49
+ has to make sense without the rest of the document.
50
50
  - **The immediate line, not an ancestor.** Hang the block under the one sentence that makes the claim. Under a
51
51
  nested bullet, indent it under that bullet, not under the top-level one above it; when the claim only makes sense
52
52
  with an umbrella sentence, write the claim itself as a top-level bullet and hang the block there.
@@ -136,6 +136,12 @@ in the same `browser_run_code_unsafe` call that waits for the download), run
136
136
  images** and say in the card where the logo really is on each page, which pages have it and which do not, and what the
137
137
  text shows. The rendered pages are the card's `Reference`. If the logo is not where the case says, that is a question.
138
138
 
139
+ When `pnpm pdf:render` itself fails, the install is broken — a PDF check would fail the same way once the spec runs, so
140
+ there is nothing to explore: end with `needs-input` and name the error (`pnpm asrun doctor` says which part is
141
+ missing). **Never substitute another renderer** (Ghostscript, an image converter, a preview from the browser): the
142
+ baseline a spec compares against is produced by `expectPdf` and pdf.js, so a page drawn by anything else is not a
143
+ `Reference` and the card would describe a picture that never appears again.
144
+
139
145
  While exploring, do the same by hand: click, wait for the download event, see the file finish, read its size and its first
140
146
  bytes. Record "(observed: <file name>, <size>, valid <kind>)".
141
147
 
@@ -23,15 +23,19 @@ perform this step; run /explore-case with --attended") and goes on with the rest
23
23
  that nobody watches.
24
24
 
25
25
  ## Before the handoff
26
- 1. Say what the user has to do and when it is finished, in one or two sentences ("Fill in the form, pick two
27
- items, then press Done"). Name the finish condition first (below).
28
- 2. Put every text the user has to type into `copy` (the name of a document, a title), so it can be
26
+ 1. Say in one sentence what the handoff is about (`message`), and put what to do in `steps`, **one action per entry, in
27
+ the order the person does it**. The card numbers them. Never chain actions in one entry ("pick items, then press
28
+ Next"): a step is one thing to click, type or choose. More than about five steps is two handoffs, not one long card.
29
+ 2. Name the finish condition in `finish`, in the words of the page ("'Published' appears at the top of the page",
30
+ without a closing full stop), and keep it out of the steps: the card puts it in its own highlighted "DONE WHEN"
31
+ block. `doneText` is the same condition as a text the card can watch for (see "Finish condition").
32
+ 3. Put every text the user has to type into `copy` (the name of a document, a title), so it can be
29
33
  copied with one click. Build a created name with the project's test-data prefix (default `[asrun]`, printed by
30
34
  `pnpm asrun rules`) and a run stamp: `<prefix> <base> (<YYYY-MM-DD HH-mm-ss>, UTC)`, the same shape
31
35
  `testDataName()` builds. The stamp is chosen now, not recorded in the recipe.
32
- 3. Never ask the user to send email (production: never; other environments: only when `email` is `true` for the
36
+ 4. Never ask the user to send email (production: never; other environments: only when `email` is `true` for the
33
37
  profile). Never ask for login or 2FA. Name everything the user creates `<prefix> <something> (<run stamp>)`.
34
- 4. Do not start a recording yourself: the card starts and stops it (see "Recording"). Never call
38
+ 5. Do not start a recording yourself: the card starts and stops it (see "Recording"). Never call
35
39
  `browser_start_recording` or `browser_stop_recording`: the `playwright-test` MCP lists them but cannot run them
36
40
  (`devtools` tools; see `tools.md`).
37
41
 
@@ -39,9 +43,14 @@ that nobody watches.
39
43
  Run it with one `browser_run_code_unsafe` call. A single call may wait for minutes (verified: 150 s without a
40
44
  timeout), so no polling from outside is needed. The loop:
41
45
 
42
- - **Shows a small floating card, not a bar.** It sits in the bottom-left corner and only the card takes clicks, so
43
- nothing of the page is covered or blocked. The user can **drag** it by its title bar and **minimize** it to a small
44
- pill that still has a Done button. Position and minimized state survive a page reload (kept in `sessionStorage`).
46
+ - **Shows a small floating card, not a bar.** It reads top to bottom: one sentence about the handoff, the steps as a
47
+ numbered list, a line per text to copy, and the finish condition in its own green "DONE WHEN" block at the bottom,
48
+ with a line under it that says whether the card ends by itself. A list longer than its box scrolls (and fades at the
49
+ edge), while the finish condition and the buttons stay in view.
50
+ It sits in the bottom-left corner and only the card takes clicks, so nothing of the page is covered or blocked. The
51
+ user can **drag** it by its title bar and **minimize** it to a small pill that still has a Done button (its tooltip
52
+ keeps the steps and the finish condition). Position and minimized state survive a page reload (kept in
53
+ `sessionStorage`).
45
54
  - **Click to copy.** Every text in `copy` (for example the `<prefix> <name> (<run stamp>)` the user has to type) is shown on its
46
55
  own line with a copy icon; one click puts it on the clipboard and the icon shows "Copied". Pass only text that the
47
56
  case says to type; leave `copy` empty when there is none.
@@ -72,9 +81,14 @@ badge, no count in the tab title and no notification (a HINT in the job log says
72
81
  // browser_run_code_unsafe({intent: 'set the handoff card options', code: ...})
73
82
  async (page) => {
74
83
  page.context().__handoffCfg = {
75
- message: '<what the user does, one sentence>',
84
+ message: '<what this handoff is for, one sentence>',
85
+ steps: [ // one action per entry, in order: the card numbers them
86
+ '<first thing to do>',
87
+ '<next thing to do>',
88
+ ],
89
+ finish: '<what the page shows when it is done, in the page\'s own words, no full stop>',
76
90
  copy: [], // texts to type, e.g. ['[autotest] published document (2026-11-09 12-34-56)']
77
- doneText: null, // text that appears when finished, or null
91
+ doneText: '<the exact text that appears when it is done>', // so the card ends by itself (see below)
78
92
  maxMs: 600000,
79
93
  };
80
94
  return 'cfg set';
@@ -89,26 +103,44 @@ badge, no count in the tab title and no notification (a HINT in the job log says
89
103
  the recording was unavailable (`recording: false`; `recordingError` says why).
90
104
 
91
105
  Check once, after the card shows, that it looks right: take a screenshot and see a title bar with a move icon, a
92
- chevron button at its right and, when `copy` is not empty, a line per text with a copy icon. Playwright's own recording
93
- toolbar at the top of the page is expected and is not part of the card. If the card is not right, stop and report it;
94
- do not hand off with a broken card.
95
-
96
- Tell the user in the chat (the terminal or the UI job log) as well: "Your turn in the browser window: <task>. Press
97
- Done on the card (bottom-left; drag it if it is in the way) when you finish." When the run was started from the web
98
- UI, the UI notices the handoff by itself (it sees the `__handoffCfg` call and the `handoff-card.js` call, whether the
99
- browser tool is called directly or through the CLI's code mode, as OpenCode's `execute` tool does) and shows the
100
- "Your turn" badge, a count in the tab title and, if the person switched notifications on, a browser notification;
101
- this is why the message in `__handoffCfg` must be a plain string literal. A macOS notification is a bonus and may
102
- fail in a sandbox: `osascript -e 'display notification "<task>" with title "AsRun" sound name "Glass"'`. If it
103
- fails, go on.
106
+ chevron button at its right, the message as one line, the steps numbered, the finish condition in its own green
107
+ "DONE WHEN" block saying "The card ends by itself when this shows", and, when `copy` is not empty, a line per text
108
+ with a copy icon. The Done button must be visible without scrolling. Playwright's own recording toolbar at the top of
109
+ the page is expected and is not part of the card. If the card is not right (or says "Press Done when you are
110
+ finished"), fix `doneText` before you hand the step over.
111
+
112
+ Tell the user in the chat (the terminal or the UI job log) as well: "Your turn in the browser window: <task>. Done
113
+ when <finish condition>. Press Done on the card (bottom-left; drag it if it is in the way) when you finish." Name the
114
+ finish condition there too, not only on the card. When the run was started from the web UI, the UI notices the handoff
115
+ by itself (it sees the `__handoffCfg` call and the `handoff-card.js` call, whether the browser tool is called directly
116
+ or through the CLI's code mode, as OpenCode's `execute` tool does) and shows the "Your turn" badge, a count in the tab
117
+ title and a desktop notification saying what to do and when it is done — this is why the message and the finish
118
+ condition in `__handoffCfg` must be plain string literals. The UI server shows that notification itself; **never send
119
+ one yourself** (`osascript`, `notify-send`, a toast): a CLI that refuses a shell call leaves a failed call in the job
120
+ log, and the person gets nothing either way.
104
121
 
105
122
  ## Finish condition
106
- Pick one that the page can show, before the handoff, and write it into the message of the card:
123
+ Pick one that the page can show, before the handoff, and write it into `finish`, where the card shows it alone:
107
124
  - a text that appears (a "Saved" toast, the title of the next step) or one that goes away;
108
125
  - a URL that the page reaches;
109
126
  - an element that appears or disappears (an editor that closes).
110
127
 
111
- The Done button is always there (also on the minimized card) as the fallback, and "Cannot do this" lets the user give up a step.
128
+ **Also give `doneText`: the exact text that is not on the page yet and shows up when the step is done** (the new
129
+ button label, the toast, the heading of the next screen). Without it the card cannot notice the condition and waits for
130
+ the person to press Done, which is the one thing a handoff must not need every time. Only a condition that carries no
131
+ text at all (a URL, an empty editor) may leave it out.
132
+
133
+ - Both must agree: nobody should read "DONE WHEN ..." on the card while `doneText` watches for something else.
134
+ - Quote the *new* text, not the one that is already there: `doneText: 'Unfavorite Article'`, never `'Favorite Article'`
135
+ when the button already reads that.
136
+ - If `finish` quotes a text and `doneText` is missing, the card uses that quoted text as its signal — but only when the
137
+ page did not show it when the card appeared, so a condition that already held can never end the handoff at once.
138
+ Relying on that is a mistake: set `doneText` yourself.
139
+ - The block says which way the wait ends: "The card ends by itself when this shows. Done also works." when the card can
140
+ notice the condition, "Press Done when you are finished." when it cannot. Read that line on the screenshot: a
141
+ handoff that will never end by itself is a handoff to fix, not to run.
142
+ - The Done button is always there (also on the minimized card) as the fallback, and "Cannot do this" lets the user give
143
+ up a step.
112
144
 
113
145
  ## After the handoff
114
146
  - The card turns green and says control is back ("Done: control is back with Playwright. Do not touch the page.")
@@ -116,9 +148,10 @@ The Done button is always there (also on the minimized card) as the fallback, an
116
148
  - `outcome: 'timeout'` or `'skip'`: do not guess. Write a `TODO [blocking]` under the step and go on with what does
117
149
  not depend on it.
118
150
  - Take a snapshot, write down what the user did from the card's `recorded` (see "Recording"), then continue read-only.
119
- - At the end of the exploration hand off once more when the user created data: "Delete the test items you
120
- created (they start with <the project's prefix> and carry this run's timestamp), then press Done". The case must
121
- clean up after itself, and so must the exploration.
151
+ - At the end of the exploration hand off once more when the user created data, with the same shape as any other
152
+ handoff: `message` "Clean up the test items this run created", `steps` = "Open the list", "Delete the items that
153
+ start with <prefix> and carry this run's timestamp", `finish` = "the list holds no item of this run any more". The
154
+ case must clean up after itself, and so must the exploration.
122
155
 
123
156
  ## Recording
124
157
  The card records what the person does: it starts the recording when it shows and stops it on every way out (Done,
@@ -135,7 +135,11 @@ Decide this first, before exploring anything. The spec path is `tests/<id>.spec.
135
135
  **Read each profile's exploration note first** (`../exploring-cases/references/notes.md`): run
136
136
  `pnpm asrun explore:notes <case-id> --json`. A fresh note (its `caseSha256` matches the case) supplies the locator,
137
137
  the request pattern and the target's match count for a step; confirm each on the page instead of rediscovering it,
138
- and skip what it does not hold. A missing or stale note means the plain replay below. After confirming an entry of
138
+ and skip what it does not hold. A `depth: "diff"` note covers only the steps that profile's exploration opened: take
139
+ those, take the rest from the lead note named in `basedOn` when it is fresh, and report the rest as
140
+ `observed on <lead>, not verified on <profile>`. A diff note is a normal input, never a gap: do not stop on it, do not
141
+ ask for a full exploration of that profile, and never write the lead's steps into it. A missing or stale note means
142
+ the plain replay below. After confirming an entry of
139
143
  a fresh note, you may write the verified locator or request back into that file (`updatedAt` gets the time); never
140
144
  add a step or a target, and never touch a stale note. Say in the report what came from a note and what was
141
145
  refreshed. Perform the steps in the environment you were given (never ask for it; there is no default).
@@ -147,7 +151,9 @@ Decide this first, before exploring anything. The spec path is `tests/<id>.spec.
147
151
  `/explore-case` recorded). Pages that only exist after such a step cannot be seen from here: write their locators
148
152
  from a fresh note first, then from the recipe, and mark them in the report as "observed by explore on <profile>" or
149
153
  "not verified on the page"; leave the first run of the spec (or `/heal-spec`) to confirm the latter. Do this for at
150
- least one logged-in profile per domain (example.com and exampleshop.com differ in markup). A profile the case holds
154
+ least one logged-in profile per domain (example.com and exampleshop.com differ in markup). A profile with a fresh
155
+ diff note does not have to be opened again for the steps the note leaves to its lead: open it only to resolve a
156
+ value or a behavior, as below. A profile the case holds
151
157
  information for that is not logged in is skipped and named in the report; its part is written from the case and
152
158
  marked `not verified` in the assertion table. Use this exploration to answer the open points of step 2, and read each
153
159
  profile's value from the case's `## Values`: open that profile's page only when a value or a behavior still has to be
@@ -196,7 +202,8 @@ Decide this first, before exploring anything. The spec path is `tests/<id>.spec.
196
202
  Do NOT run `pnpm asrun`, do not use
197
203
  `--update-snapshots` and do not create baselines: the first run of the spec does that, and a person reviews them.
198
204
  8. **Report** in the user's language: spec path, **an assertion table** (step, what is asserted, target card, ready signal, and
199
- `observed on the page` or `not verified`; no row may be missing), profiles explored, which steps were written from a
205
+ `observed on the page` or `not verified`; no row may be missing), profiles explored (naming the profile whose note
206
+ each row came from, and `observed on <lead>, not verified on <profile>` for a diff note's rows), which steps were written from a
200
207
  fresh exploration note and which notes were refreshed (or that there were none), profiles skipped (and why),
201
208
  the profiles of `profiles.yaml` the case holds no information for (they run on `default` alone), whether the case file was
202
209
  translated, what was added to the case file as `(seen on <profile>)` and which lines still need a person