explorbot 0.4.0 → 0.4.2
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/bin/explorbot-cli.ts +13 -6
- package/boat/api-tester/src/ai/chief.ts +7 -1
- package/boat/api-tester/src/ai/curler-tools.ts +1 -1
- package/boat/api-tester/src/ai/curler.ts +7 -1
- package/boat/api-tester/src/apibot.ts +10 -4
- package/boat/api-tester/src/cli.ts +15 -2
- package/boat/api-tester/src/config.ts +28 -8
- package/boat/doc-collector/bin/doc-collector-cli.ts +3 -2
- package/boat/doc-collector/src/ai/documentarian.ts +57 -5
- package/boat/doc-collector/src/ai/tools.ts +1 -1
- package/boat/doc-collector/src/cli.ts +21 -3
- package/boat/doc-collector/src/config.ts +3 -0
- package/boat/doc-collector/src/docbot.ts +57 -12
- package/boat/doc-collector/src/docs-renderer.ts +79 -55
- package/boat/doc-collector/src/interaction-screenshots.ts +19 -10
- package/boat/doc-collector/src/screenshots.ts +106 -3
- package/boat/doc-collector/src/template-dedup.ts +31 -0
- package/boat/prima/bin/prima-cli.ts +2 -0
- package/boat/prima/src/cli.ts +5 -0
- package/boat/prima/src/prima.ts +3 -4
- package/dist/bin/explorbot-cli.js +14 -6
- package/dist/boat/api-tester/bin/apibot-cli.js +3 -2
- package/dist/boat/api-tester/src/ai/chief.js +6 -1
- package/dist/boat/api-tester/src/ai/curler-tools.js +1 -1
- package/dist/boat/api-tester/src/ai/curler.js +6 -1
- package/dist/boat/api-tester/src/apibot.js +7 -3
- package/dist/boat/api-tester/src/cli.js +14 -2
- package/dist/boat/api-tester/src/config.js +31 -8
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +3 -2
- package/dist/boat/doc-collector/src/ai/documentarian.js +62 -4
- package/dist/boat/doc-collector/src/ai/tools.js +1 -1
- package/dist/boat/doc-collector/src/cli.js +18 -3
- package/dist/boat/doc-collector/src/config.js +1 -0
- package/dist/boat/doc-collector/src/docbot.js +53 -11
- package/dist/boat/doc-collector/src/docs-renderer.js +69 -46
- package/dist/boat/doc-collector/src/interaction-screenshots.js +18 -11
- package/dist/boat/doc-collector/src/screenshots.js +104 -1
- package/dist/boat/doc-collector/src/template-dedup.js +27 -0
- package/dist/boat/prima/bin/prima-cli.js +2 -0
- package/dist/boat/prima/src/cli.js +5 -0
- package/dist/boat/prima/src/prima.js +3 -4
- package/dist/package.json +1 -1
- package/dist/rules/planner/styles/normal.md +1 -1
- package/dist/src/action-result.d.ts +2 -2
- package/dist/src/action-result.js +15 -17
- package/dist/src/action.d.ts +1 -1
- package/dist/src/action.js +15 -15
- package/dist/src/ai/captain.js +1 -1
- package/dist/src/ai/fisherman/request-haul.d.ts +11 -0
- package/dist/src/ai/fisherman/request-haul.js +28 -0
- package/dist/src/ai/fisherman-tools.d.ts +10 -1
- package/dist/src/ai/fisherman-tools.js +35 -31
- package/dist/src/ai/fisherman.d.ts +3 -2
- package/dist/src/ai/fisherman.js +8 -6
- package/dist/src/ai/navigator.js +1 -1
- package/dist/src/ai/pilot.js +21 -16
- package/dist/src/ai/planner.js +9 -7
- package/dist/src/ai/provider.js +8 -14
- package/dist/src/ai/researcher/coordinates.d.ts +2 -0
- package/dist/src/ai/researcher/coordinates.js +13 -1
- package/dist/src/ai/researcher/deep-analysis.js +9 -9
- package/dist/src/ai/researcher/fingerprint-worker.js +1 -11
- package/dist/src/ai/researcher.js +3 -1
- package/dist/src/ai/rules.js +11 -3
- package/dist/src/ai/tester.js +20 -20
- package/dist/src/ai/tools.d.ts +1 -1
- package/dist/src/ai/tools.js +18 -74
- package/dist/src/api/request-store.d.ts +1 -5
- package/dist/src/api/request-store.js +12 -22
- package/dist/src/api/spec-reader.js +1 -1
- package/dist/src/commands/config-command.js +3 -10
- package/dist/src/commands/drill-command.js +1 -1
- package/dist/src/commands/explore-command.js +12 -1
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/init-command.js +3 -7
- package/dist/src/commands/options/base-option.d.ts +8 -0
- package/dist/src/commands/options/base-option.js +12 -0
- package/dist/src/commands/options/index.d.ts +5 -0
- package/dist/src/commands/options/index.js +5 -0
- package/dist/src/commands/options/knowledge-option.d.ts +7 -0
- package/dist/src/commands/options/knowledge-option.js +12 -0
- package/dist/src/commands/options/ws-option.d.ts +7 -0
- package/dist/src/commands/options/ws-option.js +21 -0
- package/dist/src/commands/plans-command.d.ts +4 -1
- package/dist/src/commands/plans-command.js +4 -4
- package/dist/src/commands/recommended-models-command.d.ts +9 -0
- package/dist/src/commands/recommended-models-command.js +85 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.js +18 -2
- package/dist/src/experience-tracker.js +1 -1
- package/dist/src/explorbot.js +4 -4
- package/dist/src/global-config.d.ts +2 -0
- package/dist/src/global-config.js +6 -0
- package/dist/src/knowledge-tracker.d.ts +20 -7
- package/dist/src/knowledge-tracker.js +69 -31
- package/dist/src/remote.d.ts +0 -3
- package/dist/src/remote.js +0 -18
- package/dist/src/state-manager.d.ts +3 -3
- package/dist/src/state-manager.js +8 -8
- package/dist/src/test-plan.d.ts +1 -0
- package/dist/src/test-plan.js +13 -9
- package/dist/src/utils/aria.d.ts +1 -1
- package/dist/src/utils/aria.js +23 -2
- package/dist/src/utils/cli-section.d.ts +1 -0
- package/dist/src/utils/cli-section.js +12 -0
- package/dist/src/utils/html-diff.d.ts +1 -0
- package/dist/src/utils/html-diff.js +5 -5
- package/dist/src/utils/html.d.ts +2 -0
- package/dist/src/utils/html.js +5 -4
- package/dist/src/utils/overlay.d.ts +19 -55
- package/dist/src/utils/overlay.js +94 -148
- package/dist/src/utils/region.d.ts +41 -0
- package/dist/src/utils/region.js +99 -0
- package/dist/src/utils/similarity.d.ts +1 -0
- package/dist/src/utils/similarity.js +11 -0
- package/dist/src/utils/url-matcher.d.ts +1 -1
- package/dist/src/utils/url-matcher.js +4 -4
- package/docs/api-testing/basics.md +15 -0
- package/docs/api-testing/planning.md +10 -1
- package/docs/doc-collection/basics.md +11 -5
- package/docs/doc-collection/crawling.md +21 -1
- package/docs/reference/commands.md +27 -4
- package/docs/superpowers/plans/2026-09-03-model-resolves-click-ambiguity.md +509 -0
- package/docs/workflow/agentic-usage.md +11 -2
- package/docs/workflow/application-spec.md +4 -0
- package/docs/workflow/knowledge.md +46 -2
- package/package.json +1 -1
- package/rules/planner/styles/normal.md +1 -1
- package/src/action-result.ts +15 -16
- package/src/action.ts +16 -15
- package/src/ai/captain.ts +1 -1
- package/src/ai/fisherman/request-haul.ts +32 -0
- package/src/ai/fisherman-tools.ts +38 -32
- package/src/ai/fisherman.ts +8 -7
- package/src/ai/navigator.ts +1 -1
- package/src/ai/pilot.ts +19 -14
- package/src/ai/planner.ts +9 -8
- package/src/ai/provider.ts +8 -12
- package/src/ai/researcher/coordinates.ts +13 -1
- package/src/ai/researcher/deep-analysis.ts +9 -9
- package/src/ai/researcher/fingerprint-worker.ts +1 -10
- package/src/ai/researcher.ts +2 -1
- package/src/ai/rules.ts +11 -3
- package/src/ai/tester.ts +18 -18
- package/src/ai/tools.ts +18 -86
- package/src/api/request-store.ts +13 -27
- package/src/api/spec-reader.ts +1 -1
- package/src/commands/config-command.ts +3 -8
- package/src/commands/drill-command.ts +1 -1
- package/src/commands/explore-command.ts +12 -1
- package/src/commands/index.ts +2 -0
- package/src/commands/init-command.ts +4 -11
- package/src/commands/options/base-option.ts +18 -0
- package/src/commands/options/index.ts +7 -0
- package/src/commands/options/knowledge-option.ts +14 -0
- package/src/commands/options/ws-option.ts +24 -0
- package/src/commands/plans-command.ts +5 -5
- package/src/commands/recommended-models-command.ts +92 -0
- package/src/config.ts +20 -2
- package/src/experience-tracker.ts +1 -1
- package/src/explorbot.ts +4 -4
- package/src/global-config.ts +8 -0
- package/src/knowledge-tracker.ts +94 -36
- package/src/remote.ts +0 -20
- package/src/state-manager.ts +10 -9
- package/src/test-plan.ts +12 -10
- package/src/utils/aria.ts +26 -3
- package/src/utils/cli-section.ts +12 -0
- package/src/utils/html-diff.ts +6 -6
- package/src/utils/html.ts +6 -4
- package/src/utils/overlay.ts +105 -192
- package/src/utils/region.ts +132 -0
- package/src/utils/similarity.ts +9 -0
- package/src/utils/url-matcher.ts +3 -3
- package/dist/src/utils/request-map.d.ts +0 -7
- package/dist/src/utils/request-map.js +0 -16
- package/src/utils/request-map.ts +0 -19
|
@@ -33,7 +33,7 @@ Inside the TUI, use the matching slash command: `/explore`, `/research`, `/plan`
|
|
|
33
33
|
| Generate test plan | `npx explorbot plan <path>` | `/plan [--focus <feature>]` | Writes plan markdown |
|
|
34
34
|
| List saved plans | `npx explorbot plans [plan]` | `/plans [plan]` | Show plans and their tests |
|
|
35
35
|
| Navigate to a URL | `npx explorbot navigate <url>` | `/navigate <target>` | Reachability probe + session capture |
|
|
36
|
-
| Drill page components | `npx explorbot drill <url>` | `/drill [--knowledge <path>] [--max-components <n>]` | Learn interactions |
|
|
36
|
+
| Drill page components | `npx explorbot drill <url>` | `/drill [--save-knowledge <path>] [--max-components <n>]` | Learn interactions |
|
|
37
37
|
| Execute plan tests | `npx explorbot test <planfile> [index]` | `/test [scenario\|number\|*]` | Run scenarios |
|
|
38
38
|
| Re-run generated tests | `npx explorbot rerun <file> [index]` | `/rerun <file> [index]` | With AI auto-healing |
|
|
39
39
|
| List generated tests | `npx explorbot runs [file]` | `/runs [file]` | Index + dry-run |
|
|
@@ -52,6 +52,7 @@ Inside the TUI, use the matching slash command: `/explore`, `/research`, `/plan`
|
|
|
52
52
|
| Initialize project | `npx explorbot init` | — | Generates `explorbot.config.*`, or `~/.explorbot` with `--global` |
|
|
53
53
|
| List registered sites | `npx explorbot sites` | — | Sites stored in the global installation |
|
|
54
54
|
| Show resolved configuration | `npx explorbot config [url] [--json]` | `/config` | Models, config file, paths and `EXPLORBOT_*` in effect |
|
|
55
|
+
| Show recommended models | `npx explorbot recommended-models [--json]` | `/recommended-models` | Models this version recommends per provider |
|
|
55
56
|
| Clean generated files | `npx explorbot clean [target]` | `/clean [target]` | Same targets both ways |
|
|
56
57
|
|
|
57
58
|
## Common CLI Options
|
|
@@ -69,6 +70,16 @@ Every CLI command that drives a browser accepts these options (`start`, `explore
|
|
|
69
70
|
| `--incognito` | Run without recording experiences |
|
|
70
71
|
| `--session [file]` | Save/restore browser session (cookies, localStorage) from file |
|
|
71
72
|
|
|
73
|
+
### `--knowledge`
|
|
74
|
+
|
|
75
|
+
Passes facts to the run without creating a file in `knowledge/`. Plain text applies everywhere; add frontmatter to scope it to a page or an API endpoint. Repeat the flag for several facts. See [Knowledge](../workflow/knowledge.md#per-session-knowledge).
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx explorbot explore /pay --knowledge 'Test card 4111 1111 1111 1111, any future expiry'
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Like `--ws`, it is a program-level option rather than a per-command one: it works on every command — including `api`, `docs` and `prima` — and can go anywhere on the line. It is listed under `npx explorbot --help` rather than in each command's own help.
|
|
82
|
+
|
|
72
83
|
### `--session`
|
|
73
84
|
|
|
74
85
|
Saves browser state (cookies, localStorage, sessionStorage) to a JSON file. The next run restores the session, so you skip login and setup steps.
|
|
@@ -104,6 +115,7 @@ EXPLORBOT_AI_PROVIDER=openrouter \
|
|
|
104
115
|
| `EXPLORBOT_EPHEMERAL` | Keep no state between runs — output goes to a fresh temp directory instead of the site dir |
|
|
105
116
|
| `EXPLORBOT_KNOWLEDGE` | Inline knowledge text, applied to every page |
|
|
106
117
|
| `EXPLORBOT_KNOWLEDGE_FILE` | Path to a knowledge markdown file |
|
|
118
|
+
| `EXPLORBOT_SPEC` | Docbot application spec directory or index.md, used as page knowledge |
|
|
107
119
|
| `EXPLORBOT_API_SPEC` | OpenAPI spec path for the API boat |
|
|
108
120
|
| `EXPLORBOT_NO_BANNER` | Suppress the startup banner, for machine-readable output |
|
|
109
121
|
| `EXPLORBOT_MAX_DURATION` | Wall-clock budget in minutes for an explore run; same as --max-duration |
|
|
@@ -111,6 +123,8 @@ EXPLORBOT_AI_PROVIDER=openrouter \
|
|
|
111
123
|
|
|
112
124
|
`npx explorbot config` prints the values a run actually uses — models per role, the config file behind them, the output, knowledge and experience directories, and every `EXPLORBOT_*` variable currently set. The boats answer for their own configuration the same way: `npx explorbot api config`, `npx explorbot docs config`, `npx explorbot prima config`. Add `--json` on any of them to get the same values as an object a script can read.
|
|
113
125
|
|
|
126
|
+
`npx explorbot recommended-models` prints, per provider, the model this version recommends for each role, and the two ways to select it. Both need the provider's API key exported. Set `EXPLORBOT_AI_PROVIDER=<name>` and every role takes that provider's recommendation; leave it out and pin the roles yourself with `EXPLORBOT_AI_MODEL`, `EXPLORBOT_VISION_MODEL` and `EXPLORBOT_AGENTIC_MODEL`, each written as `provider/model-id` — the command prints those three lines filled in, ready to paste. A role a provider does not serve is named as such, so you know to pair it with another. It closes with the model variables and provider keys currently exported, and a ready-to-run OpenRouter one-liner. It reads nothing but the bundled recommendations, so it answers before any configuration exists and every CLI carries it: `npx explorbot api recommended-models`, `npx explorbot docs recommended-models`, `npx prima recommended-models`. `--json` prints the bundled recommendations as an object.
|
|
127
|
+
|
|
114
128
|
Explorbot resolves its configuration in this order: the path given to `--config`, then `explorbot.config.*` in the working directory, then the `EXPLORBOT_*` variables, and finally `~/.explorbot/config.*` from the global installation. A bare provider name fills every model role from the recommendations in [Providers](../basics/providers.md); a `provider/model-id` spec pins one model and splits on the first slash, so `openrouter/openai/gpt-oss-120b:nitro` selects OpenRouter with model `openai/gpt-oss-120b:nitro`. Supported providers: `openai`, `anthropic`, `google`, `groq`, `mistral`, `openrouter`, `sambanova`.
|
|
115
129
|
|
|
116
130
|
In this mode output goes to `~/.explorbot/sites/<host>/output/` (or `EXPLORBOT_OUTPUT`, or a temp directory with `EXPLORBOT_EPHEMERAL=1`), experience is kept beside it unless the run is ephemeral, and the Historian is off, so no generated test files appear. See [Agentic Usage](../workflow/agentic-usage.md) for the full picture.
|
|
@@ -444,18 +458,18 @@ Drill all components on a page to learn interactions.
|
|
|
444
458
|
# CLI
|
|
445
459
|
npx explorbot drill /components
|
|
446
460
|
npx explorbot drill /components --max-components 10
|
|
447
|
-
npx explorbot drill /login --knowledge /login
|
|
461
|
+
npx explorbot drill /login --save-knowledge /login
|
|
448
462
|
```
|
|
449
463
|
|
|
450
464
|
```
|
|
451
465
|
# TUI
|
|
452
466
|
/drill
|
|
453
|
-
/drill --knowledge /login --max-components 10
|
|
467
|
+
/drill --save-knowledge /login --max-components 10
|
|
454
468
|
```
|
|
455
469
|
|
|
456
470
|
| Option | Description |
|
|
457
471
|
|---|---|
|
|
458
|
-
| `--knowledge <path>` | Save learned interactions to a knowledge file at this URL path |
|
|
472
|
+
| `--save-knowledge <path>` | Save learned interactions to a knowledge file at this URL path |
|
|
459
473
|
| `--max-components <count>` | Maximum number of components to drill |
|
|
460
474
|
|
|
461
475
|
## Test Rerun
|
|
@@ -593,9 +607,15 @@ Crawl pages and generate a documentation spec with `Purpose`, `User Can`, and `U
|
|
|
593
607
|
```bash
|
|
594
608
|
npx explorbot docs collect /users/sign_in
|
|
595
609
|
npx explorbot docs collect /docs/openapi#tag/project-analytics-tags --max-pages 20
|
|
610
|
+
npx explorbot docs collect /dashboard --url https://app.example.com
|
|
596
611
|
npx explorbot docs collect https://teleportal.ua/ua/serials/stb/kod --path explorbot-testing --show --session --max-pages 20
|
|
597
612
|
```
|
|
598
613
|
|
|
614
|
+
| Option | Description |
|
|
615
|
+
|---|---|
|
|
616
|
+
| `--url <url>` | Base URL of the site, for a relative path argument. Same as `EXPLORBOT_URL`; an absolute path argument carries its own |
|
|
617
|
+
| `--max-pages <count>` | Stop after documenting this many pages |
|
|
618
|
+
|
|
599
619
|
Output is written to:
|
|
600
620
|
|
|
601
621
|
- `output/docs/spec.md`
|
|
@@ -718,6 +738,7 @@ Every command takes these:
|
|
|
718
738
|
| `-i, --instance <name>` | Which prima-owned browser to talk to; parallel work needs one each |
|
|
719
739
|
| `--session [file]` | Cookies and storage persisted across processes; ignored while attached, since the attached session keeps its own |
|
|
720
740
|
| `--url <url>` | Page to open when the session has no page yet |
|
|
741
|
+
| `--spec <path>` | A Docbot application spec directory or its `index.md`, read as page knowledge. Same as `EXPLORBOT_SPEC` / `PRIMA_CLI_SPEC` |
|
|
721
742
|
| `--ephemeral` | Keep no state between runs. Applies to config-free runs only — with a config file the output directory comes from the config |
|
|
722
743
|
| `--framework <name>` | Parsed but not active yet; reported code is CodeceptJS whatever you pass |
|
|
723
744
|
| `-c, --config <path>`, `-p, --path <path>` | As on every other Explorbot command |
|
|
@@ -767,6 +788,8 @@ Prima follows the same [configuration ladder](#environment-variables) as every o
|
|
|
767
788
|
EXPLORBOT_AI_PROVIDER=groq npx explorbot prima go https://app.example.com
|
|
768
789
|
```
|
|
769
790
|
|
|
791
|
+
The three inputs a run needs beyond the model come from flags or the environment, so no file has to exist: `--url` / `PRIMA_CLI_URL` for the site, `--spec` / `PRIMA_CLI_SPEC` for collected documentation, and `--knowledge` / `PRIMA_CLI_KNOWLEDGE` for facts such as credentials. Every `EXPLORBOT_*` variable has a `PRIMA_CLI_*` twin that prima reads first.
|
|
792
|
+
|
|
770
793
|
`pw` still works when no model is usable at all; commands that need one say so and point at the fallback.
|
|
771
794
|
|
|
772
795
|
## Plan Management
|
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
# Model-Resolved Click Ambiguity Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Make `MultipleElementsFound` a real zero-click failure that hands the model the matched elements and lets it pick one by position, instead of an internal AI call that silently retries and clicks.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Delete the in-tool disambiguation retry (`disambiguateElements`) from both `click()` and `form()`. The existing failure path in `failedToolResult` already returns `multipleElementsDetected`, a numbered `elements` list, and a suggestion — that becomes the only path. Then document CodeceptJS's `step.opts({ elementIndex: N })` in the shared action rules so the model can act on that list; the sandbox already exposes `step`, so no runtime work is needed.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Bun, TypeScript, CodeceptJS 4 (Playwright helper), Biome, `bun:test`.
|
|
10
|
+
|
|
11
|
+
**Spec:** This document — see Background. Primary evidence: Langfuse trace `5172fb3e976aa1e3889f149dd25a039b` (session `ThoughtlessActualViolet630`, 2026-09-01).
|
|
12
|
+
|
|
13
|
+
## Background
|
|
14
|
+
|
|
15
|
+
`src/ai/tools.ts` catches `MultipleElementsFound`, runs a separate AI call to choose a position, appends `step.opts({ elementIndex: N })` to the failing command, retries, and returns `success: true` — while leaving the original `MultipleElementsFound` text in `attempts`.
|
|
16
|
+
|
|
17
|
+
Both readers of that result treat it as a failure. The Tester sees the error in the result it just received; Pilot's `formatActions` (`src/ai/pilot.ts:1038`) lifts the first `attempts[].error` into its evidence line and fires its `MultipleElementsFound → xpathCheck() then precise locator` rule (`src/ai/pilot.ts:1145`). So both send another click. Every one of those calls really clicks, because the internal retry runs inside each call.
|
|
18
|
+
|
|
19
|
+
In the reference trace a `button[role="switch"]` was clicked four times in 26 seconds — twice from the Tester's own locators, twice on Pilot's advice — each call reporting `success: true, disambiguated: true` with `code` ending in `step.opts({ elementIndex: 1 })` and an ariaDiff alternating `added switch [checked]` / `removed switch [checked]`. The plan was saved with the setting off and the test reported PASS.
|
|
20
|
+
|
|
21
|
+
The disambiguator picked position 1 correctly all four times, so it was not buying accuracy — it was buying one round trip and charging a page mutation for it. After this change an ambiguous locator never mutates the page, so a wrong guess is free.
|
|
22
|
+
|
|
23
|
+
`step.opts({ elementIndex })` already works end to end and needs no implementation: `src/utils/web-sandbox.ts:9` puts `step` in the sandbox's argument names, `node_modules/codeceptjs/lib/step/record.js:14` strips a `StepConfig` from the last argument position, and `node_modules/codeceptjs/lib/helper/Playwright.js:4285` honours `elementIndex`. It is 1-based in document order, accepts negatives counting from the end, and accepts `'first'`/`'last'`. It appears in no prompt or rule today, which is why the model could only guess new locators.
|
|
24
|
+
|
|
25
|
+
## Global Constraints
|
|
26
|
+
|
|
27
|
+
- Runtime is **Bun only**. Never invoke `node`.
|
|
28
|
+
- Prompt text must be **general, never example-driven**: no locator, class name, or scenario taken from the reference trace may appear in any prompt, rule, or tool description. Illustrate the shape of correct usage, not the bug.
|
|
29
|
+
- Prompts stay **concise** — 1-3 lines per bullet, telegraph style.
|
|
30
|
+
- Never hardcode site-specific locators anywhere.
|
|
31
|
+
- Use `dedent` for multi-line prompt blocks.
|
|
32
|
+
- Prefer early exit over `if`/`else`. No ternaries. No `...(cond ? {k:v} : {})` spreads.
|
|
33
|
+
- Run `bun run format` after each code change; `bun run lint` must stay clean.
|
|
34
|
+
- Do **not** add or re-add the `regression` label, and do not trigger `.github/workflows/regression.yml`.
|
|
35
|
+
- End every commit message with:
|
|
36
|
+
`Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>` and
|
|
37
|
+
`Claude-Session: https://claude.ai/code/session_017DuWpxPTjAacCgowWAT631`
|
|
38
|
+
|
|
39
|
+
## File Structure
|
|
40
|
+
|
|
41
|
+
| File | Responsibility after this plan |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `src/ai/tools.ts` | `click()` and `form()` report ambiguity as a plain failure; keeps `extractWebElements` / `formatElementList` / `formatMatchedElements` for the element list; no longer holds an AI disambiguator |
|
|
44
|
+
| `src/ai/rules.ts` | `actionRule` documents `step.opts({ elementIndex })` under `### I.click`, reaching Tester, Navigator, Captain web-mode and Rerunner through the existing imports |
|
|
45
|
+
| `src/ai/pilot.ts` | Its `MultipleElementsFound` diagnostic line points at `elementIndex` instead of a sharper locator |
|
|
46
|
+
| `tests/unit/click-ambiguity.test.ts` | New. Pins zero clicks on ambiguity, and that the element list survives later fallback failures |
|
|
47
|
+
| `tests/unit/matched-elements.test.ts` | Unchanged. Already covers `formatMatchedElements` |
|
|
48
|
+
| `CHANGELOG.md` | User-facing entry |
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
### Task 1: Ambiguous click fails without clicking
|
|
53
|
+
|
|
54
|
+
**Files:**
|
|
55
|
+
- Modify: `src/ai/tools.ts:95-145` (the `click` tool's command loop, the disambiguation block, the failure return)
|
|
56
|
+
- Test: `tests/unit/click-ambiguity.test.ts` (create)
|
|
57
|
+
|
|
58
|
+
**Interfaces:**
|
|
59
|
+
- Consumes: `failedToolResult(action, message, data, error)` — already async, already sets `multipleElementsDetected: true` and `elements` when any text in `message` or `data.attempts[].error` contains `'multiple elements'`, using the `error` argument to build the list.
|
|
60
|
+
- Produces: the `click` tool result on ambiguity — `{ success: false, action: 'click', message: 'All click commands failed', attempts, suggestion, multipleElementsDetected: true, elements }`. No `disambiguated` key exists any more.
|
|
61
|
+
|
|
62
|
+
- [ ] **Step 1: Write the failing test**
|
|
63
|
+
|
|
64
|
+
Create `tests/unit/click-ambiguity.test.ts`:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { beforeEach, describe, expect, it } from 'bun:test';
|
|
68
|
+
import { createCodeceptJSTools } from '../../src/ai/tools.ts';
|
|
69
|
+
import { ConfigParser } from '../../src/config.ts';
|
|
70
|
+
|
|
71
|
+
function multipleElementsError(): Error {
|
|
72
|
+
const element = (xpath: string, text: string) => ({
|
|
73
|
+
toAbsoluteXPath: async () => xpath,
|
|
74
|
+
toOuterHTML: async () => '<button role="switch" type="button"></button>',
|
|
75
|
+
getText: async () => text,
|
|
76
|
+
});
|
|
77
|
+
return Object.assign(new Error('Multiple elements (2) found for "{role: switch}" in strict mode'), {
|
|
78
|
+
name: 'MultipleElementsFound',
|
|
79
|
+
webElements: [element('/html/body/div/button[1]', 'First control'), element('/html/body/div/button[2]', 'Second control')],
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function notFoundError(): Error {
|
|
84
|
+
return Object.assign(new Error('element (.missing) was not found by text|CSS|XPath'), { name: 'ElementNotFound' });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fakeDeps(errorFor: (command: string) => Error) {
|
|
88
|
+
const state = { url: '/settings', html: '<html><body></body></html>', ariaSnapshot: '- switch', id: 'unchanged' };
|
|
89
|
+
const action: any = {
|
|
90
|
+
lastError: null,
|
|
91
|
+
executedSteps: [],
|
|
92
|
+
ran: [] as string[],
|
|
93
|
+
saveScreenshot: async () => undefined,
|
|
94
|
+
attempt: async (command: string) => {
|
|
95
|
+
action.ran.push(command);
|
|
96
|
+
action.lastError = errorFor(command);
|
|
97
|
+
return false;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
const deps: any = {
|
|
101
|
+
explorer: { action: () => action },
|
|
102
|
+
stateManager: { getCurrentState: () => state },
|
|
103
|
+
ai: { getModelForAgent: () => ({}), generateObject: async () => ({ object: { position: 1 } }) },
|
|
104
|
+
};
|
|
105
|
+
return { deps, action };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function fakeTask(): any {
|
|
109
|
+
return { startNote: () => ({ commit: () => {}, screenshot: undefined }) };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
describe('click on an ambiguous locator', () => {
|
|
113
|
+
beforeEach(() => {
|
|
114
|
+
ConfigParser.resetForTesting();
|
|
115
|
+
ConfigParser.setupTestConfig();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('clicks nothing and hands the matched elements back to the model', async () => {
|
|
119
|
+
const { deps, action } = fakeDeps(() => multipleElementsError());
|
|
120
|
+
const tools = createCodeceptJSTools(deps, fakeTask());
|
|
121
|
+
|
|
122
|
+
const result = await tools.click.execute({ commands: [`I.click({"role":"switch"})`], explanation: 'Toggle the control' }, {} as any);
|
|
123
|
+
|
|
124
|
+
expect(result.success).toBe(false);
|
|
125
|
+
expect(action.ran).toEqual([`I.click({"role":"switch"})`]);
|
|
126
|
+
expect(result.disambiguated).toBeUndefined();
|
|
127
|
+
expect(result.multipleElementsDetected).toBe(true);
|
|
128
|
+
expect(result.elements).toContain('Element 1:');
|
|
129
|
+
expect(result.elements).toContain('Element 2:');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('keeps the ambiguous match when a later fallback command failed differently', async () => {
|
|
133
|
+
const { deps } = fakeDeps((command) => {
|
|
134
|
+
if (command.includes('role')) return multipleElementsError();
|
|
135
|
+
return notFoundError();
|
|
136
|
+
});
|
|
137
|
+
const tools = createCodeceptJSTools(deps, fakeTask());
|
|
138
|
+
|
|
139
|
+
const result = await tools.click.execute({ commands: [`I.click({"role":"switch"})`, `I.click('.missing')`], explanation: 'Toggle the control' }, {} as any);
|
|
140
|
+
|
|
141
|
+
expect(result.multipleElementsDetected).toBe(true);
|
|
142
|
+
expect(result.elements).toContain('Element 2:');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
- [ ] **Step 2: Run the test to verify it fails**
|
|
148
|
+
|
|
149
|
+
Run: `bun test tests/unit/click-ambiguity.test.ts`
|
|
150
|
+
|
|
151
|
+
Expected: both tests FAIL.
|
|
152
|
+
|
|
153
|
+
The first fails on `action.ran`: the old code disambiguates and retries, so `ran` holds three commands — the original, the original plus `step.opts({ elementIndex: 1 })`, and the picked XPath. The fake `ai` must answer `generateObject`; with an empty `ai` object the disambiguator throws, is swallowed by its own `catch`, returns `null`, and the test would pass against unfixed code for the wrong reason.
|
|
154
|
+
|
|
155
|
+
The second fails on `elements`, which reads `Could not fetch element details.` — at return time `action.lastError` is the not-found error from the second command, not the ambiguity error from the first.
|
|
156
|
+
|
|
157
|
+
- [ ] **Step 3: Capture the ambiguity error in the command loop**
|
|
158
|
+
|
|
159
|
+
In `src/ai/tools.ts`, in the `click` tool, declare the holder next to `attempts` and set it inside the loop. Replace:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const attempts: Array<{ command: string; success: boolean; error?: string }> = [];
|
|
163
|
+
|
|
164
|
+
for (let i = 0; i < commands.length; i++) {
|
|
165
|
+
const command = transformContainsCommand(commands[i]);
|
|
166
|
+
const success = await action.attempt(command, explanation);
|
|
167
|
+
|
|
168
|
+
const attempt: { command: string; success: boolean; error?: string } = { command, success };
|
|
169
|
+
if (action.lastError) attempt.error = errorText(action.lastError);
|
|
170
|
+
attempts.push(attempt);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
with:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
const attempts: Array<{ command: string; success: boolean; error?: string }> = [];
|
|
177
|
+
let ambiguityError: Error | null = null;
|
|
178
|
+
|
|
179
|
+
for (let i = 0; i < commands.length; i++) {
|
|
180
|
+
const command = transformContainsCommand(commands[i]);
|
|
181
|
+
const success = await action.attempt(command, explanation);
|
|
182
|
+
|
|
183
|
+
const attempt: { command: string; success: boolean; error?: string } = { command, success };
|
|
184
|
+
if (action.lastError) attempt.error = errorText(action.lastError);
|
|
185
|
+
attempts.push(attempt);
|
|
186
|
+
|
|
187
|
+
if (!ambiguityError && action.lastError?.name === 'MultipleElementsFound') ambiguityError = action.lastError;
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- [ ] **Step 4: Delete the disambiguation retry block**
|
|
191
|
+
|
|
192
|
+
Still in the `click` tool, delete this entire block (it sits between the command loop and the final `const toolResult = …`):
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
let disambiguated = null;
|
|
196
|
+
if (attempts.some((a) => a.error?.toLowerCase().includes(MULTIPLE_ELEMENTS_PATTERN))) {
|
|
197
|
+
disambiguated = await disambiguateElements(action.lastError, explanation, ai);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (disambiguated) {
|
|
201
|
+
debugLog('Disambiguation picked element %d', disambiguated.position);
|
|
202
|
+
const failedCommand = attempts.find((a) => a.error?.toLowerCase().includes(MULTIPLE_ELEMENTS_PATTERN))?.command;
|
|
203
|
+
const retryCommands = [];
|
|
204
|
+
if (failedCommand) {
|
|
205
|
+
retryCommands.push(failedCommand.replace(/\)$/, `, step.opts({ elementIndex: ${disambiguated.position} }))`));
|
|
206
|
+
}
|
|
207
|
+
retryCommands.push(`I.click('${disambiguated.xpath.replace(/'/g, "\\'")}')`);
|
|
208
|
+
|
|
209
|
+
for (const retryCmd of retryCommands) {
|
|
210
|
+
if (!(await action.attempt(retryCmd, explanation))) {
|
|
211
|
+
attempts.push({ command: retryCmd, success: false, error: errorText(action.lastError) });
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const toolResult = await ActionResult.fromState(stateManager.getCurrentState()!).toToolResult(previousState, retryCmd);
|
|
215
|
+
await commitNote(activeNote, TestResult.PASSED, toolResult, action);
|
|
216
|
+
return successToolResult('click', { ...toolResult, attempts, code: retryCmd, disambiguated: true }, action);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
- [ ] **Step 5: Pass the ambiguity error to the failure result**
|
|
222
|
+
|
|
223
|
+
In the same tool, change the final return's `error` argument. Replace:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
return failedToolResult(
|
|
227
|
+
'click',
|
|
228
|
+
'All click commands failed',
|
|
229
|
+
{
|
|
230
|
+
...toolResult,
|
|
231
|
+
attempts,
|
|
232
|
+
suggestion,
|
|
233
|
+
},
|
|
234
|
+
action.lastError
|
|
235
|
+
);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
with:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
return failedToolResult(
|
|
242
|
+
'click',
|
|
243
|
+
'All click commands failed',
|
|
244
|
+
{
|
|
245
|
+
...toolResult,
|
|
246
|
+
attempts,
|
|
247
|
+
suggestion,
|
|
248
|
+
},
|
|
249
|
+
ambiguityError || action.lastError
|
|
250
|
+
);
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
- [ ] **Step 6: Run the test to verify it passes**
|
|
254
|
+
|
|
255
|
+
Run: `bun test tests/unit/click-ambiguity.test.ts`
|
|
256
|
+
|
|
257
|
+
Expected: 2 pass.
|
|
258
|
+
|
|
259
|
+
- [ ] **Step 7: Commit**
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
git add src/ai/tools.ts tests/unit/click-ambiguity.test.ts
|
|
263
|
+
git commit -m "Let an ambiguous click fail without clicking"
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
### Task 2: Remove the AI disambiguator
|
|
269
|
+
|
|
270
|
+
**Files:**
|
|
271
|
+
- Modify: `src/ai/tools.ts:34` (drop the now-unused `ai` binding), `src/ai/tools.ts:18` (drop the `AIProvider` import), `src/ai/tools.ts:412-422` (the `form` tool's ambiguity branch), `src/ai/tools.ts:1370-1410` (delete `disambiguateElements`)
|
|
272
|
+
- Test: `tests/unit/tools.test.ts`, `tests/unit/matched-elements.test.ts` (both existing, must keep passing)
|
|
273
|
+
|
|
274
|
+
**Interfaces:**
|
|
275
|
+
- Consumes: nothing new.
|
|
276
|
+
- Produces: `createCodeceptJSTools({ explorer, stateManager }: ToolDeps, task: Task)` — the `ai` member stays on the `ToolDeps` type because `createAgentTools` uses it; only this function stops destructuring it. `disambiguateElements` no longer exists. `extractWebElements`, `formatElementList`, `formatMatchedElements`, `MAX_DISAMBIGUATE_ELEMENTS`, `MAX_DISAMBIGUATE_TEXT`, `MAX_DISAMBIGUATE_HTML` and `MULTIPLE_ELEMENTS_PATTERN` all stay — they build the element list the model now reads.
|
|
277
|
+
|
|
278
|
+
- [ ] **Step 1: Simplify the form tool's failure suggestion**
|
|
279
|
+
|
|
280
|
+
In `src/ai/tools.ts`, in the `form` tool, replace:
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
let formSuggestion = 'Commands after the failing one never ran. Retry only those, using click() or form().';
|
|
284
|
+
if (message.toLowerCase().includes(MULTIPLE_ELEMENTS_PATTERN)) {
|
|
285
|
+
const disambiguated = await disambiguateElements(action.lastError, explanation, ai);
|
|
286
|
+
if (disambiguated) {
|
|
287
|
+
formSuggestion = `Multiple elements matched. Add step.opts({ elementIndex: ${disambiguated.position} }) to the failing command. Fallback locator: ${disambiguated.xpath}`;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
with:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
const formSuggestion = 'Commands after the failing one never ran. Retry only those, using click() or form().';
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
`failedToolResult` already receives `action.lastError` here and replaces the suggestion with the ambiguity text plus the `elements` list whenever the message is ambiguous, so nothing is lost.
|
|
299
|
+
|
|
300
|
+
- [ ] **Step 2: Delete the disambiguator function**
|
|
301
|
+
|
|
302
|
+
Delete the whole `disambiguateElements` function from `src/ai/tools.ts` — it starts with:
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
async function disambiguateElements(error: Error | null | undefined, explanation: string, provider: AIProvider): Promise<{ position: number; xpath: string } | null> {
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
and ends with the closing brace after its `catch` block. Do not touch `extractWebElements`, `formatElementList` or `formatMatchedElements` directly above it.
|
|
309
|
+
|
|
310
|
+
- [ ] **Step 3: Drop the now-unused bindings**
|
|
311
|
+
|
|
312
|
+
At `src/ai/tools.ts:34`, change:
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
export function createCodeceptJSTools({ explorer, stateManager, ai }: ToolDeps, task: Task) {
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
to:
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
export function createCodeceptJSTools({ explorer, stateManager }: ToolDeps, task: Task) {
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
At `src/ai/tools.ts:18`, delete the import line:
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
import type { AIProvider } from './provider.ts';
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
- [ ] **Step 4: Verify nothing else referenced them**
|
|
331
|
+
|
|
332
|
+
Run: `grep -rn "disambiguateElements\|AIProvider" src/ai/tools.ts`
|
|
333
|
+
|
|
334
|
+
Expected: no output.
|
|
335
|
+
|
|
336
|
+
Run: `bun run format && bun run lint`
|
|
337
|
+
|
|
338
|
+
Expected: formatting applied, `bun run lint` reports no errors.
|
|
339
|
+
|
|
340
|
+
- [ ] **Step 5: Run the surrounding suites**
|
|
341
|
+
|
|
342
|
+
Run: `bun test tests/unit/tools.test.ts tests/unit/matched-elements.test.ts tests/unit/click-ambiguity.test.ts tests/unit/click-failure-suggestion.test.ts`
|
|
343
|
+
|
|
344
|
+
Expected: all pass.
|
|
345
|
+
|
|
346
|
+
- [ ] **Step 6: Commit**
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
git add src/ai/tools.ts
|
|
350
|
+
git commit -m "Drop the AI disambiguator from the click and form tools"
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
### Task 3: Teach the model to pick a match by position
|
|
356
|
+
|
|
357
|
+
**Files:**
|
|
358
|
+
- Modify: `src/ai/rules.ts:309-338` (the `### I.click` section of `actionRule`)
|
|
359
|
+
- Modify: `src/ai/tools.ts:55-65` (the `click` tool's `commands` schema description) and `src/ai/tools.ts:1285-1295` (`getMultipleElementsSuggestion`)
|
|
360
|
+
- Modify: `src/ai/pilot.ts:1145` (the `MultipleElementsFound` diagnostic line)
|
|
361
|
+
- Test: `tests/unit/click-ambiguity.test.ts` (existing, from Task 1)
|
|
362
|
+
|
|
363
|
+
**Interfaces:**
|
|
364
|
+
- Consumes: `actionRule` and `locatorRule` are already imported by `src/ai/tester.ts:823-825`, `src/ai/navigator.ts:414`, `src/ai/captain/web-mode.ts:146-148` and `src/ai/rerunner.ts:448-450`. Editing `actionRule` reaches all four; no import changes.
|
|
365
|
+
- Produces: `getMultipleElementsSuggestion(): string` — same signature, new text naming `elementIndex`.
|
|
366
|
+
|
|
367
|
+
- [ ] **Step 1: Document `elementIndex` in the shared action rule**
|
|
368
|
+
|
|
369
|
+
In `src/ai/rules.ts`, inside `actionRule`'s `### I.click` section, insert after the line `If locator doesn't work, try CSS or XPath locators.`:
|
|
370
|
+
|
|
371
|
+
```
|
|
372
|
+
When one locator matches several elements, select among them by position instead of inventing another locator.
|
|
373
|
+
Pass step.opts({ elementIndex: N }) as the LAST argument. N is 1-based, in document order.
|
|
374
|
+
|
|
375
|
+
<example>
|
|
376
|
+
I.click('Remove', step.opts({ elementIndex: 2 }));
|
|
377
|
+
I.click({ role: 'link', text: 'Details' }, '.panel', step.opts({ elementIndex: 1 }));
|
|
378
|
+
</example>
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
- [ ] **Step 2: Rewrite the ambiguity suggestion**
|
|
382
|
+
|
|
383
|
+
In `src/ai/tools.ts`, replace the body of `getMultipleElementsSuggestion`:
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
function getMultipleElementsSuggestion(): string {
|
|
387
|
+
return dedent`
|
|
388
|
+
Multiple elements matched your locator, so NOTHING was clicked and the page is unchanged.
|
|
389
|
+
Read the numbered elements list and click the one you meant by its number:
|
|
390
|
+
reuse the same locator with step.opts({ elementIndex: N }) as the last argument.
|
|
391
|
+
If none of them is the element you want, narrow the locator with a container or its full unique text.
|
|
392
|
+
If the list is missing, call xpathCheck() to see what the locator matches.
|
|
393
|
+
`;
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
- [ ] **Step 3: Point the click tool's schema at the same escape hatch**
|
|
398
|
+
|
|
399
|
+
In `src/ai/tools.ts`, in the `click` tool's `commands` description, append one line after `5. I.clickXY(x, y) - coordinates fallback`:
|
|
400
|
+
|
|
401
|
+
```
|
|
402
|
+
After a result reporting multiple matches, reuse that locator with step.opts({ elementIndex: N }) as the last argument.
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
- [ ] **Step 4: Update Pilot's diagnostic line**
|
|
406
|
+
|
|
407
|
+
In `src/ai/pilot.ts`, replace:
|
|
408
|
+
|
|
409
|
+
```
|
|
410
|
+
- MultipleElementsFound → xpathCheck() to identify the right one, then precise locator or visualClick().
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
with:
|
|
414
|
+
|
|
415
|
+
```
|
|
416
|
+
- MultipleElementsFound → nothing was clicked. Tell Tester to reuse the same locator with step.opts({ elementIndex: N }) from the numbered elements list.
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
- [ ] **Step 5: Assert the model is told how to resolve it**
|
|
420
|
+
|
|
421
|
+
In `tests/unit/click-ambiguity.test.ts`, add one assertion to the end of the first test, after `expect(result.elements).toContain('Element 2:');`:
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
expect(result.suggestion).toContain('elementIndex');
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
- [ ] **Step 6: Run the tests**
|
|
428
|
+
|
|
429
|
+
Run: `bun test tests/unit/click-ambiguity.test.ts tests/unit/rules.test.ts`
|
|
430
|
+
|
|
431
|
+
Expected: all pass.
|
|
432
|
+
|
|
433
|
+
- [ ] **Step 7: Commit**
|
|
434
|
+
|
|
435
|
+
```bash
|
|
436
|
+
git add src/ai/rules.ts src/ai/tools.ts src/ai/pilot.ts tests/unit/click-ambiguity.test.ts
|
|
437
|
+
git commit -m "Tell the model to pick an ambiguous match by position"
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
### Task 4: Full suites, changelog, pull request
|
|
443
|
+
|
|
444
|
+
**Files:**
|
|
445
|
+
- Modify: `CHANGELOG.md`
|
|
446
|
+
|
|
447
|
+
**Interfaces:**
|
|
448
|
+
- Consumes: everything from Tasks 1-3.
|
|
449
|
+
- Produces: nothing consumed by later tasks.
|
|
450
|
+
|
|
451
|
+
- [ ] **Step 1: Run the whole unit suite**
|
|
452
|
+
|
|
453
|
+
Run: `bun test tests/unit/`
|
|
454
|
+
|
|
455
|
+
Expected: all pass. Baseline before this plan was 1247 pass / 0 fail; expect that plus the two new tests, minus none.
|
|
456
|
+
|
|
457
|
+
- [ ] **Step 2: Run the integration suite**
|
|
458
|
+
|
|
459
|
+
Run: `bun test tests/integration/`
|
|
460
|
+
|
|
461
|
+
Expected: all pass. Baseline was 92 pass / 0 fail. These exercise real prompts through the aimock server, so a prompt edit that breaks a journal assertion surfaces here.
|
|
462
|
+
|
|
463
|
+
- [ ] **Step 3: Format and lint**
|
|
464
|
+
|
|
465
|
+
Run: `bun run format && bun run lint`
|
|
466
|
+
|
|
467
|
+
Expected: clean.
|
|
468
|
+
|
|
469
|
+
- [ ] **Step 4: Add the changelog entry**
|
|
470
|
+
|
|
471
|
+
Add to `CHANGELOG.md` under a `## 2026-09-03` heading with a `### Changes` section (create the date heading directly under `# Changelog` if it does not exist yet; keep one blank line between the header and the first entry, and between entries):
|
|
472
|
+
|
|
473
|
+
```markdown
|
|
474
|
+
- Click tool: A locator that matches several elements is now reported as a failure that clicked nothing,
|
|
475
|
+
together with the numbered list of what matched. Explorbot no longer guesses which one you meant and
|
|
476
|
+
clicks it — a guess used to land a real click, so a control that toggles could be switched back by a
|
|
477
|
+
retry the model thought had failed. The AI now picks a match by its number instead.
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
- [ ] **Step 5: Commit and push**
|
|
481
|
+
|
|
482
|
+
```bash
|
|
483
|
+
git add CHANGELOG.md
|
|
484
|
+
git commit -m "Add changelog entry for model-resolved click ambiguity"
|
|
485
|
+
git push -u origin fix/model-resolves-click-ambiguity
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
- [ ] **Step 6: Open the pull request**
|
|
489
|
+
|
|
490
|
+
Open a PR against `main` titled `Let the model resolve an ambiguous click by position`. The body must state: ambiguity now clicks nothing; the disambiguating AI call is gone; `step.opts({ elementIndex })` is documented for the model; evidence is Langfuse trace `5172fb3e976aa1e3889f149dd25a039b`. End it with the 🤖 Claude Code footer and the session link. Do **not** apply the `regression` label.
|
|
491
|
+
|
|
492
|
+
- [ ] **Step 7: Close the superseded pull request**
|
|
493
|
+
|
|
494
|
+
PR #179 (`fix/click-disambiguation-not-a-failure`) implements the rejected approach — it kept the auto-retry and only made its reporting honest. Ask the user before closing it, then close with a comment naming the replacement PR and remove its worktree:
|
|
495
|
+
|
|
496
|
+
```bash
|
|
497
|
+
bunosh worktree:delete fix/click-disambiguation-not-a-failure
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
## Notes for the executor
|
|
503
|
+
|
|
504
|
+
- `MULTIPLE_ELEMENTS_PATTERN` is `'multiple elements'`, matched case-insensitively against error text. Task 1 additionally matches `error.name === 'MultipleElementsFound'` to pick the right error object out of several failed attempts — the string check stays where it is for the suggestion routing, because a failure can reach `failedToolResult` with only the text and no error object.
|
|
505
|
+
- `formatElementList` numbers entries `Element 1:`, `Element 2:` … 1-based, which is exactly the numbering `elementIndex` expects. Do not renumber either side.
|
|
506
|
+
- The `MAX_DISAMBIGUATE_*` constants keep their names. They cap the list the model reads; renaming them is churn with no user-visible effect.
|
|
507
|
+
- `elementIndex` also accepts a negative index counting from the end, and the strings `'first'`/`'last'` (`node_modules/codeceptjs/lib/helper/extras/elementSelection.js`, shared by the Playwright and WebDriver helpers). That is left out of the prompt on purpose: the element list is numbered 1..N, so positive integers are all the model needs. Do not "complete" the rule by adding them.
|
|
508
|
+
- `grep -rn "disambiguat|elementIndex|multiple elements" docs/` was run against this tree. Nothing in `docs/` claims Explorbot picks the matching element itself, so no user documentation needs updating. `docs/superpowers/specs/2026-08-01-actor-boat-design.md:73` mentions cheap-model disambiguation, but that is a historical design doc for the actor boat and no boat implements it — leave it alone.
|
|
509
|
+
- Do not add a knowledge file, a locator table, or any site-specific selector. The reference trace's app has two unlabelled `button[role="switch"]` controls; that is an accessibility gap in the application under test, not something to encode here.
|
|
@@ -56,6 +56,7 @@ No `init`, no config file, no project directory, no model IDs to look up. These
|
|
|
56
56
|
| `EXPLORBOT_EPHEMERAL` | no | Keep no state between runs — output goes to a fresh temp directory instead of the site dir |
|
|
57
57
|
| `EXPLORBOT_KNOWLEDGE` | no | Inline knowledge text, applied to every page |
|
|
58
58
|
| `EXPLORBOT_KNOWLEDGE_FILE` | no | Path to a knowledge markdown file |
|
|
59
|
+
| `EXPLORBOT_SPEC` | no | Docbot application spec directory or index.md, used as page knowledge |
|
|
59
60
|
| `EXPLORBOT_API_SPEC` | no | OpenAPI spec path for the API boat |
|
|
60
61
|
| `EXPLORBOT_NO_BANNER` | no | Suppress the startup banner, for machine-readable output |
|
|
61
62
|
| `EXPLORBOT_MAX_DURATION` | no | Wall-clock budget in minutes for an explore run; same as --max-duration |
|
|
@@ -124,6 +125,12 @@ EXPLORBOT_AI_PROVIDER=openrouter \
|
|
|
124
125
|
EXPLORBOT_KNOWLEDGE_FILE=./checkout-knowledge.md npx explorbot explore /checkout
|
|
125
126
|
```
|
|
126
127
|
|
|
128
|
+
Both variables work in config-free runs and in runs on the global configuration, where what they carry is written into the site's knowledge directory for that run — rewritten on the next run, and removed by a run that sets neither variable. Facts worth keeping belong in `learn` or `know`. The `--knowledge` flag does the same thing as an argument, works with a project config as well, and writes nothing, so prefer it when one command needs one fact:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
npx explorbot explore /checkout --knowledge 'Use the sandbox card 4111 1111 1111 1111'
|
|
132
|
+
```
|
|
133
|
+
|
|
127
134
|
### What this mode changes
|
|
128
135
|
|
|
129
136
|
Config-free runs leave no trace in the working directory:
|
|
@@ -213,9 +220,11 @@ The same variables drive API testing and doc collection.
|
|
|
213
220
|
EXPLORBOT_URL=https://api.example.com \
|
|
214
221
|
EXPLORBOT_API_SPEC=./openapi.yaml \
|
|
215
222
|
EXPLORBOT_AI_PROVIDER=openrouter \
|
|
216
|
-
npx explorbot api explore
|
|
223
|
+
npx explorbot api explore /users
|
|
217
224
|
```
|
|
218
225
|
|
|
226
|
+
The API boat also takes those two as flags, so one line carries the whole run: `npx explorbot api explore /users --endpoint https://api.example.com --spec ./openapi.yaml`.
|
|
227
|
+
|
|
219
228
|
```bash
|
|
220
229
|
EXPLORBOT_AI_PROVIDER=openrouter \
|
|
221
230
|
npx explorbot docs collect https://app.example.com/dashboard --max-pages 20
|
|
@@ -223,7 +232,7 @@ EXPLORBOT_AI_PROVIDER=openrouter \
|
|
|
223
232
|
|
|
224
233
|
`docs collect` takes its base URL from the absolute path argument, so `EXPLORBOT_URL` is optional there.
|
|
225
234
|
|
|
226
|
-
Knowledge written by `EXPLORBOT_KNOWLEDGE` carries `endpoint: '*'` frontmatter alongside `url: '*'`, matching the convention `api init` and `api know` use
|
|
235
|
+
Knowledge written by `EXPLORBOT_KNOWLEDGE` carries `endpoint: '*'` frontmatter alongside `url: '*'`, matching the convention `api init` and `api know` use, so one variable reaches both boats.
|
|
227
236
|
|
|
228
237
|
## See Also
|
|
229
238
|
|