@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.
- package/.claude/commands/explore-case.md +8 -3
- package/.claude/skills/exploring-cases/SKILL.md +72 -14
- package/.claude/skills/exploring-cases/references/handoff-card.js +81 -7
- package/.claude/skills/exploring-cases/references/notes.md +42 -4
- package/.claude/skills/exploring-cases/references/open-questions.md +2 -2
- package/.claude/skills/exploring-cases/references/readiness.md +6 -0
- package/.claude/skills/exploring-cases/references/user-handoff.md +60 -27
- package/.claude/skills/generating-specs/SKILL.md +10 -3
- package/AGENTS.md +12 -4
- package/README.md +14 -2
- package/dist/lib/desktop-notify.d.ts +38 -0
- package/dist/lib/desktop-notify.js +158 -0
- package/dist/lib/explore-notes.d.ts +15 -0
- package/dist/lib/explore-notes.js +34 -1
- package/dist/lib/migrations.d.ts +43 -4
- package/dist/lib/migrations.js +39 -7
- package/dist/lib/paths.d.ts +9 -2
- package/dist/lib/paths.js +32 -5
- package/dist/lib/pdf.js +23 -3
- package/dist/lib/scaffold.d.ts +87 -0
- package/dist/lib/scaffold.js +220 -0
- package/dist/lib/term.d.ts +36 -0
- package/dist/lib/term.js +77 -0
- package/dist/lib/update-check.d.ts +1 -7
- package/dist/lib/update-check.js +3 -15
- package/dist/lib/upgrade-prompt.d.ts +84 -0
- package/dist/lib/upgrade-prompt.js +233 -0
- package/dist/migrations/index.d.ts +12 -4
- package/dist/migrations/index.js +40 -5
- package/dist/migrations/manifest.json +27 -1
- package/dist/scripts/build.js +4 -1
- package/dist/scripts/check-cases.js +3 -3
- package/dist/scripts/cli.js +11 -3
- package/dist/scripts/doctor.js +73 -4
- package/dist/scripts/explore-notes.js +19 -2
- package/dist/scripts/init.d.ts +6 -26
- package/dist/scripts/init.js +82 -101
- package/dist/scripts/migrate.js +84 -21
- package/dist/scripts/migrations-check.d.ts +1 -0
- package/dist/scripts/migrations-check.js +272 -0
- package/dist/scripts/migrations-freeze.d.ts +1 -0
- package/dist/scripts/migrations-freeze.js +126 -0
- package/dist/scripts/notify-test.d.ts +1 -0
- package/dist/scripts/notify-test.js +48 -0
- package/dist/scripts/upgrade.js +53 -10
- package/dist/ui/server/actions.js +4 -2
- package/dist/ui/server/ai-stream.d.ts +7 -1
- package/dist/ui/server/ai-stream.js +26 -7
- package/dist/ui/server/app.js +4 -0
- package/dist/ui/server/jobs.d.ts +10 -0
- package/dist/ui/server/jobs.js +41 -1
- package/dist/ui/shared/ai-commands.js +4 -2
- package/dist/ui/shared/types.d.ts +8 -0
- package/docs/agents/case-format.md +8 -1
- package/docs/agents/framework-dev.md +44 -5
- package/docs/ai-workflow.md +27 -10
- package/docs/cases.md +5 -5
- package/docs/commands.md +73 -15
- package/docs/install.md +18 -5
- package/docs/troubleshooting.md +17 -0
- package/docs/web-ui.md +23 -8
- package/package.json +5 -2
- package/templates/.claude/commands/explore-case.md +9 -3
- package/templates/.claude/commands/upgrade-workspace.md +21 -0
- package/templates/AGENTS.md +1 -1
- package/templates/package.json +1 -1
- package/ui/dist/assets/{dist-C46EmZuI.js → dist-B4pM62Hc.js} +1 -1
- package/ui/dist/assets/{dist-DGzg2gyA.js → dist-BFADGd4p.js} +1 -1
- package/ui/dist/assets/{dist-P9roUi1O.js → dist-Bd-TmWTD.js} +1 -1
- package/ui/dist/assets/{dist-DMiOs9Qj.js → dist-Bt053MSo.js} +1 -1
- package/ui/dist/assets/{dist-DrFzCwMc.js → dist-CD9RixNE.js} +1 -1
- package/ui/dist/assets/{dist-_0g6iC9K.js → dist-ChACtLiA.js} +1 -1
- package/ui/dist/assets/{dist-D9GmSkjm.js → dist-CiW9sEPF.js} +1 -1
- package/ui/dist/assets/{dist-Db_XhDui.js → dist-CxEpzoZU.js} +1 -1
- package/ui/dist/assets/{dist-BB2cF3XN.js → dist-Dh7um7By.js} +1 -1
- package/ui/dist/assets/{dist-Dy45nXf8.js → dist-gHwKkZ6g.js} +1 -1
- package/ui/dist/assets/{index-BN4ucmmn.js → index-Ckax27uf.js} +158 -158
- 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.
|
|
18
|
-
|
|
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
|
|
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)
|
|
58
|
-
least one logged-in profile per domain.
|
|
59
|
-
|
|
60
|
-
|
|
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`,
|
|
94
|
-
|
|
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
|
-
|
|
99
|
-
happened from its `recorded` result. In a run that
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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><
|
|
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 (
|
|
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
|
|
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,
|
|
49
|
-
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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:
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|