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