explorbot 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/bin/explorbot-cli.ts +5 -3
  2. package/boat/api-tester/src/ai/chief.ts +7 -1
  3. package/boat/api-tester/src/ai/curler.ts +7 -1
  4. package/boat/api-tester/src/apibot.ts +10 -4
  5. package/boat/api-tester/src/cli.ts +12 -2
  6. package/boat/api-tester/src/config.ts +28 -8
  7. package/boat/doc-collector/bin/doc-collector-cli.ts +3 -2
  8. package/boat/doc-collector/src/cli.ts +3 -1
  9. package/boat/doc-collector/src/docbot.ts +1 -1
  10. package/boat/prima/bin/prima-cli.ts +2 -0
  11. package/boat/prima/src/cli.ts +2 -0
  12. package/dist/bin/explorbot-cli.js +5 -3
  13. package/dist/boat/api-tester/bin/apibot-cli.js +3 -2
  14. package/dist/boat/api-tester/src/ai/chief.js +6 -1
  15. package/dist/boat/api-tester/src/ai/curler.js +6 -1
  16. package/dist/boat/api-tester/src/apibot.js +7 -3
  17. package/dist/boat/api-tester/src/cli.js +12 -2
  18. package/dist/boat/api-tester/src/config.js +31 -8
  19. package/dist/boat/doc-collector/bin/doc-collector-cli.js +3 -2
  20. package/dist/boat/doc-collector/src/cli.js +3 -1
  21. package/dist/boat/doc-collector/src/docbot.js +1 -1
  22. package/dist/boat/prima/bin/prima-cli.js +2 -0
  23. package/dist/boat/prima/src/cli.js +3 -0
  24. package/dist/package.json +1 -1
  25. package/dist/rules/planner/styles/normal.md +1 -1
  26. package/dist/src/ai/captain.js +1 -1
  27. package/dist/src/ai/navigator.js +1 -1
  28. package/dist/src/ai/planner.js +9 -7
  29. package/dist/src/ai/researcher.js +2 -0
  30. package/dist/src/ai/rules.js +3 -3
  31. package/dist/src/api/spec-reader.js +1 -1
  32. package/dist/src/commands/config-command.js +1 -1
  33. package/dist/src/commands/drill-command.js +1 -1
  34. package/dist/src/commands/explore-command.js +12 -1
  35. package/dist/src/commands/options/base-option.d.ts +8 -0
  36. package/dist/src/commands/options/base-option.js +12 -0
  37. package/dist/src/commands/options/index.d.ts +5 -0
  38. package/dist/src/commands/options/index.js +5 -0
  39. package/dist/src/commands/options/knowledge-option.d.ts +7 -0
  40. package/dist/src/commands/options/knowledge-option.js +12 -0
  41. package/dist/src/commands/options/ws-option.d.ts +7 -0
  42. package/dist/src/commands/options/ws-option.js +21 -0
  43. package/dist/src/config.d.ts +1 -0
  44. package/dist/src/config.js +11 -0
  45. package/dist/src/explorbot.js +1 -1
  46. package/dist/src/knowledge-tracker.d.ts +20 -7
  47. package/dist/src/knowledge-tracker.js +69 -31
  48. package/dist/src/remote.d.ts +0 -3
  49. package/dist/src/remote.js +0 -18
  50. package/docs/api-testing/basics.md +15 -0
  51. package/docs/api-testing/planning.md +10 -1
  52. package/docs/reference/commands.md +24 -4
  53. package/docs/workflow/agentic-usage.md +11 -2
  54. package/docs/workflow/knowledge.md +46 -2
  55. package/package.json +1 -1
  56. package/rules/planner/styles/normal.md +1 -1
  57. package/src/ai/captain.ts +1 -1
  58. package/src/ai/navigator.ts +1 -1
  59. package/src/ai/planner.ts +9 -8
  60. package/src/ai/researcher.ts +1 -0
  61. package/src/ai/rules.ts +3 -3
  62. package/src/api/spec-reader.ts +1 -1
  63. package/src/commands/config-command.ts +1 -1
  64. package/src/commands/drill-command.ts +1 -1
  65. package/src/commands/explore-command.ts +12 -1
  66. package/src/commands/options/base-option.ts +18 -0
  67. package/src/commands/options/index.ts +7 -0
  68. package/src/commands/options/knowledge-option.ts +14 -0
  69. package/src/commands/options/ws-option.ts +24 -0
  70. package/src/config.ts +11 -0
  71. package/src/explorbot.ts +1 -1
  72. package/src/knowledge-tracker.ts +94 -36
  73. package/src/remote.ts +0 -20
@@ -26,15 +26,6 @@ export class Remote {
26
26
  asks = new Map();
27
27
  askCounter = 0;
28
28
  lastActivity = null;
29
- registerOption(program) {
30
- program.option('--ws <url>', 'Stream this run to a remote UI over WebSocket');
31
- program.hook('preAction', (_thisCommand, actionCommand) => {
32
- const url = actionCommand.optsWithGlobals().ws || process.env.EXPLORBOT_WS_URL;
33
- if (!url)
34
- return;
35
- this.attach(String(url), this.commandPath(actionCommand));
36
- });
37
- }
38
29
  attach(url, command) {
39
30
  if (this.url)
40
31
  return;
@@ -222,14 +213,5 @@ export class Remote {
222
213
  return failure.message;
223
214
  return undefined;
224
215
  }
225
- commandPath(command) {
226
- const parts = [];
227
- let node = command;
228
- while (node) {
229
- parts.unshift(node.name());
230
- node = node.parent;
231
- }
232
- return parts.slice(1).join(' ') || parts.join(' ');
233
- }
234
216
  }
235
217
  export const remote = new Remote();
@@ -56,6 +56,21 @@ api: {
56
56
 
57
57
  A matching `teardown` hook runs after all tests finish — use it to clean up data.
58
58
 
59
+ ### Without a config file
60
+
61
+ Chief and Curler need three things: where the API is, what its spec says, and how to authenticate. Pass all three on the command line and no config file is needed:
62
+
63
+ ```bash
64
+ npx explorbot api plan /users \
65
+ --endpoint https://api.example.com/v1 \
66
+ --spec ./openapi.yaml \
67
+ --knowledge 'Send X-Api-Key: ${env.API_KEY} on every request'
68
+ ```
69
+
70
+ `--endpoint` and `--spec` each have an environment twin — `EXPLORBOT_URL` and `EXPLORBOT_API_SPEC` — and the flag wins when both are set. `--knowledge` adds to the facts `EXPLORBOT_KNOWLEDGE` and `EXPLORBOT_KNOWLEDGE_FILE` bring in rather than replacing them. Configure your models once with `npx explorbot init --global` and every run stores its plans and requests per host under `~/.explorbot/sites/<host>/`, so a later `api test` against the same API picks up where the last one left off. Knowledge given on the command line lasts for the run; `api know` is what writes it down.
71
+
72
+ `--endpoint` keeps its path prefix: given `https://api.example.com/v1`, steps stay relative (`/users`) and Curler sends them to `https://api.example.com/v1/users`. `api test`, which takes a plan file rather than an endpoint, reads it from the flag or the variable.
73
+
59
74
  ### A dedicated API project
60
75
 
61
76
  If you don't have a web `explorbot.config.js`, run `npx explorbot api init`. It asks for your base endpoint, spec, and a one-line description of the API, then writes a standalone `apibot.config.ts` (with an `ai` and `api` section) plus `output/` and `knowledge/` directories. When both files exist, `apibot.config.*` takes precedence over `explorbot.config.*`.
@@ -21,7 +21,16 @@ endpoint: "/users"
21
21
  CRUD for users. Admin role required for writes. IDs are UUIDs.
22
22
  ```
23
23
 
24
- Chief loads knowledge matching the endpoint it's planning. Running `know` again on the same endpoint appends to the file. See [knowledge](../workflow/knowledge.md) for how matching and files work.
24
+ Chief loads knowledge matching the endpoint it's planning, and Curler loads it again for the endpoint it's testing, so auth headers and payload rules reach the requests themselves. Running `know` again on the same endpoint appends to the file. See [knowledge](../workflow/knowledge.md) for how matching and files work.
25
+
26
+ For a fact that should not be stored — a token, a one-off fixture — pass `--knowledge` instead. It applies to the run only:
27
+
28
+ ```bash
29
+ npx explorbot api explore /users --knowledge '---
30
+ endpoint: /users/*
31
+ ---
32
+ Send X-Api-Key: ${env.API_KEY} on every request'
33
+ ```
25
34
 
26
35
  ## Choose a planning style
27
36
 
@@ -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 |
@@ -69,6 +69,16 @@ Every CLI command that drives a browser accepts these options (`start`, `explore
69
69
  | `--incognito` | Run without recording experiences |
70
70
  | `--session [file]` | Save/restore browser session (cookies, localStorage) from file |
71
71
 
72
+ ### `--knowledge`
73
+
74
+ 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).
75
+
76
+ ```bash
77
+ npx explorbot explore /pay --knowledge 'Test card 4111 1111 1111 1111, any future expiry'
78
+ ```
79
+
80
+ 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.
81
+
72
82
  ### `--session`
73
83
 
74
84
  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 +114,7 @@ EXPLORBOT_AI_PROVIDER=openrouter \
104
114
  | `EXPLORBOT_EPHEMERAL` | Keep no state between runs — output goes to a fresh temp directory instead of the site dir |
105
115
  | `EXPLORBOT_KNOWLEDGE` | Inline knowledge text, applied to every page |
106
116
  | `EXPLORBOT_KNOWLEDGE_FILE` | Path to a knowledge markdown file |
117
+ | `EXPLORBOT_SPEC` | Docbot application spec directory or index.md, used as page knowledge |
107
118
  | `EXPLORBOT_API_SPEC` | OpenAPI spec path for the API boat |
108
119
  | `EXPLORBOT_NO_BANNER` | Suppress the startup banner, for machine-readable output |
109
120
  | `EXPLORBOT_MAX_DURATION` | Wall-clock budget in minutes for an explore run; same as --max-duration |
@@ -444,18 +455,18 @@ Drill all components on a page to learn interactions.
444
455
  # CLI
445
456
  npx explorbot drill /components
446
457
  npx explorbot drill /components --max-components 10
447
- npx explorbot drill /login --knowledge /login
458
+ npx explorbot drill /login --save-knowledge /login
448
459
  ```
449
460
 
450
461
  ```
451
462
  # TUI
452
463
  /drill
453
- /drill --knowledge /login --max-components 10
464
+ /drill --save-knowledge /login --max-components 10
454
465
  ```
455
466
 
456
467
  | Option | Description |
457
468
  |---|---|
458
- | `--knowledge <path>` | Save learned interactions to a knowledge file at this URL path |
469
+ | `--save-knowledge <path>` | Save learned interactions to a knowledge file at this URL path |
459
470
  | `--max-components <count>` | Maximum number of components to drill |
460
471
 
461
472
  ## Test Rerun
@@ -593,9 +604,15 @@ Crawl pages and generate a documentation spec with `Purpose`, `User Can`, and `U
593
604
  ```bash
594
605
  npx explorbot docs collect /users/sign_in
595
606
  npx explorbot docs collect /docs/openapi#tag/project-analytics-tags --max-pages 20
607
+ npx explorbot docs collect /dashboard --url https://app.example.com
596
608
  npx explorbot docs collect https://teleportal.ua/ua/serials/stb/kod --path explorbot-testing --show --session --max-pages 20
597
609
  ```
598
610
 
611
+ | Option | Description |
612
+ |---|---|
613
+ | `--url <url>` | Base URL of the site, for a relative path argument. Same as `EXPLORBOT_URL`; an absolute path argument carries its own |
614
+ | `--max-pages <count>` | Stop after documenting this many pages |
615
+
599
616
  Output is written to:
600
617
 
601
618
  - `output/docs/spec.md`
@@ -718,6 +735,7 @@ Every command takes these:
718
735
  | `-i, --instance <name>` | Which prima-owned browser to talk to; parallel work needs one each |
719
736
  | `--session [file]` | Cookies and storage persisted across processes; ignored while attached, since the attached session keeps its own |
720
737
  | `--url <url>` | Page to open when the session has no page yet |
738
+ | `--spec <path>` | A Docbot application spec directory or its `index.md`, read as page knowledge. Same as `EXPLORBOT_SPEC` / `PRIMA_CLI_SPEC` |
721
739
  | `--ephemeral` | Keep no state between runs. Applies to config-free runs only — with a config file the output directory comes from the config |
722
740
  | `--framework <name>` | Parsed but not active yet; reported code is CodeceptJS whatever you pass |
723
741
  | `-c, --config <path>`, `-p, --path <path>` | As on every other Explorbot command |
@@ -767,6 +785,8 @@ Prima follows the same [configuration ladder](#environment-variables) as every o
767
785
  EXPLORBOT_AI_PROVIDER=groq npx explorbot prima go https://app.example.com
768
786
  ```
769
787
 
788
+ 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.
789
+
770
790
  `pw` still works when no model is usable at all; commands that need one say so and point at the fallback.
771
791
 
772
792
  ## Plan Management
@@ -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. The API boat does not read knowledge at runtime yet; the frontmatter is there for when it does, and the web side ignores it.
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
 
@@ -45,7 +45,48 @@ While exploring, use the `/learn` command.
45
45
 
46
46
  ### API Testing
47
47
 
48
- [API testing](../api-testing/basics.md) shares the same `knowledge/` directory. `npx explorbot api know <endpoint> "<description>"` adds endpoint-scoped notes, stored with an `endpoint:` frontmatter field instead of `url:`.
48
+ [API testing](../api-testing/basics.md) shares the same `knowledge/` directory. `npx explorbot api know <endpoint> "<description>"` adds endpoint-scoped notes, stored with an `endpoint:` frontmatter field instead of `url:`. Chief reads them when planning an endpoint and Curler reads them when running its tests, so auth headers and payload rules reach both.
49
+
50
+ ## Per-Session Knowledge
51
+
52
+ `--knowledge` passes facts to a single run. Nothing is written to `knowledge/`, so credentials and one-off test data stay out of the repository.
53
+
54
+ ```bash
55
+ npx explorbot explore /pay --knowledge 'My credit card is 4111 1111 1111 1111'
56
+ ```
57
+
58
+ Plain text applies to every page. Add frontmatter to scope it, with the same URL patterns knowledge files use:
59
+
60
+ ```bash
61
+ npx explorbot explore / --knowledge '---
62
+ url: /pay
63
+ ---
64
+ Use the sandbox card 4111 1111 1111 1111 with any future expiry'
65
+ ```
66
+
67
+ Repeat the flag for several facts:
68
+
69
+ ```bash
70
+ npx explorbot explore / \
71
+ --knowledge '---
72
+ url: /login
73
+ ---
74
+ Log in as admin@example.com / secret123' \
75
+ --knowledge 'Dismiss the cookie banner before anything else'
76
+ ```
77
+
78
+ Everything a knowledge file supports works here: `${env.VAR}` interpolation, and page automation fields such as `wait` and `waitForElement`.
79
+
80
+ The flag works on every command of `explorbot`, `explorbot api`, `explorbot docs` and `prima`, and can go anywhere on the line. Scope API knowledge with `endpoint:` instead of `url:`:
81
+
82
+ ```bash
83
+ npx explorbot api explore /orders --knowledge '---
84
+ endpoint: /orders/*
85
+ ---
86
+ Send X-Api-Key: ${env.API_KEY} on every request'
87
+ ```
88
+
89
+ For runs driven from the environment — config-free, or on the global configuration — `EXPLORBOT_KNOWLEDGE` does the same job for the length of one run. See [Agentic usage](./agentic-usage.md).
49
90
 
50
91
  ## URL Patterns
51
92
 
@@ -81,10 +122,13 @@ Notes:
81
122
 
82
123
  | Field | Purpose |
83
124
  |-------|---------|
84
- | `url` | URL pattern to match (optional, defaults to `*`) |
125
+ | `url` | Page URL pattern to match |
126
+ | `endpoint` | API endpoint pattern to match |
85
127
  | `title` | Human-readable title (optional) |
86
128
  | Custom fields | Any additional metadata for agents |
87
129
 
130
+ `url` scopes a file to browser pages and `endpoint` scopes it to API endpoints. A file with neither applies to both.
131
+
88
132
  ## Variables
89
133
 
90
134
  Knowledge files support variable interpolation with `${namespace.key}` syntax. Explorbot resolves variables when it loads the knowledge.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explorbot",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
@@ -1,7 +1,7 @@
1
1
  Study the page and figure out its business purpose. What is this page FOR? What would a user come here to do?
2
2
 
3
3
  Based on the page type, propose tests for COMPLETE user workflows:
4
- - If this is a data page (lists, tables): test CRUD operations end-to-end (create item → verify in list, edit item verify changes saved, delete item verify removed)
4
+ - If this is a data page (lists, tables): test create, edit and delete as separate tests, each preparing the item it acts on
5
5
  - If the page has inputs to fill in: test the full commit flow, not just that the controls render
6
6
  - If this has filters and search: test filtering AND verify results change, not just "filter tab clicked"
7
7
  - If this has modals/dropdowns: test the ACTION inside them, not just opening/closing them
package/src/ai/captain.ts CHANGED
@@ -160,7 +160,7 @@ export class Captain extends CaptainBase implements Agent {
160
160
  const headingsBlock = headingLines.join('\n');
161
161
 
162
162
  let pageSummary = '';
163
- const cachedResearch = Researcher.getCachedResearch(state);
163
+ const cachedResearch = Researcher.getCachedResearch(actionResult);
164
164
  if (cachedResearch) {
165
165
  pageSummary = `<page_summary>\n${this.explorBot.agentResearcher().extractBrief(cachedResearch)}\n</page_summary>`;
166
166
  }
@@ -575,7 +575,7 @@ class Navigator implements Agent {
575
575
  }
576
576
 
577
577
  const currentActionResult = actionResult || ActionResult.fromState(state);
578
- const research = Researcher.getCachedResearch(state) || '';
578
+ const research = Researcher.getCachedResearch(currentActionResult) || '';
579
579
  const combinedHtml = await currentActionResult.combinedHtml();
580
580
 
581
581
  const history = stateManager.getStateHistory();
package/src/ai/planner.ts CHANGED
@@ -114,11 +114,15 @@ export class Planner extends PlannerBase implements Agent {
114
114
  Tests must be relevant to the page
115
115
  Tests must be achievable from UI
116
116
  Tests must be verifiable from UI
117
- NEVER split one workflow into multiple tests. Each test must be a complete end-to-end flow.
117
+ One test verifies ONE business operation: the steps that reach it, the action itself, and its verification.
118
+ Steps that only reach the action — opening a form, expanding a panel, creating or locating the item to act on — belong to that test. A second operation with its own verification does not.
118
119
  Bad: "Open delete dropdown" + "Confirm deletion" — these are ONE test, not two.
119
120
  Bad: "Search for X" + "Verify search results" — searching and verifying is ONE test.
120
121
  Bad: "Leave field empty" + "Click submit" — that's one negative test, not two.
121
- If two scenarios cannot run independently (one requires the other to run first), merge them into one.${featureDirective}${focusExistingDataDirective}
122
+ Bad: "Create a record, rename it, delete it" three verified operations, so THREE tests, not one.
123
+ Good: "Rename existing record and verify the new title" — ONE test; creating is skipped, we assume record already exists, only the rename is verified.
124
+ You may rely on another test having run first in case we deal with empty state and no relevant data was created yet and we expect another our test creates it
125
+ When the page reports a record is missing or unavailable, it is not a testable surface — plan list-level or recovery behavior instead of operations on that record.${featureDirective}${focusExistingDataDirective}
122
126
  </task>
123
127
 
124
128
  ${customPrompt || ''}
@@ -195,10 +199,6 @@ export class Planner extends PlannerBase implements Agent {
195
199
  throw new Error('No tasks were created successfully');
196
200
  }
197
201
 
198
- if (aiResult.object.scenarios.length === 0 && !this.currentPlan) {
199
- throw new Error('No tasks were created successfully');
200
- }
201
-
202
202
  const defaultStartUrl = this.getDefaultStartUrl(state);
203
203
  const fromPlanning = aiResult.object.scenarios.map((s: any) => new Test(s.scenario, s.priority, s.expectedOutcomes, s.startUrl || defaultStartUrl, s.steps || []));
204
204
 
@@ -335,6 +335,7 @@ export class Planner extends PlannerBase implements Agent {
335
335
  <task>
336
336
  Based on the page research, create ${this.MIN_TASKS}-${this.MAX_TASKS} exploratory testing scenarios.
337
337
  For each scenario provide specific steps and expected outcomes.
338
+ Exception: if the page reports the requested resource is missing, shows a failure state, or holds no content and no controls, return an empty scenarios list. Never invent tests for a page with nothing to exercise.
338
339
  </task>
339
340
 
340
341
  <rules>
@@ -348,13 +349,13 @@ export class Planner extends PlannerBase implements Agent {
348
349
  Focus on error or success messages as outcome.
349
350
  Focus on URL page change or data persistency after page reload.
350
351
  If there are subpages (pages with same URL path) plan testing of those subpages as well
351
- If you plan to test CRUD operations, plan them in correct order: create, read, update.
352
+ Plan CRUD operations in order: create, read, update, delete.
352
353
  Do not invent specific route names, success messages, validation texts, badge counts, or welcome messages unless they are visible in research, visited pages, or prior observed flows.
353
354
  When validation placement or wording was not observed, require feedback associated with the invalid input without inventing a specific location or message.
354
355
  If exact wording is unknown, describe the expected result generically, for example "an authentication error is shown" or "the user stays on the login page" instead of guessing the literal text.
355
356
  If exact redirect destination is unknown, describe the destination by visible page identity, for example "the dashboard page opens" or "the current workspace home page opens" instead of inventing a URL slug.
356
357
  Only propose scenarios whose prerequisites are evident from page research, visited pages, or API data preparation context.
357
- If a scenario needs existing records, recipients, results, notifications, or other target data, propose it only when that data is visible or API preconditions can create it.
358
+ If a scenario needs existing records, recipients, results, notifications, or other target data, propose it only when that data is visible, API preconditions can create it, or the scenario itself creates the record as its setup.
358
359
  If the page appears read-only, degraded, demo-limited, maintenance-like, or lacks write controls, prefer read-only scenarios such as opening panels, inspecting visible lists, filtering, searching, or verifying current state.
359
360
  Do not assume hidden data exists just because a control is present.
360
361
  For scenarios that act on existing items or search/filter by existing values, use only item names or values visible in research, visited pages, or prior observed flows.
@@ -76,6 +76,7 @@ export class Researcher extends ResearcherBase implements Agent {
76
76
  }
77
77
 
78
78
  static getCachedResearch(state: WebPageState): string {
79
+ if (state instanceof ActionResult) return getCachedResearch(state.baseHash);
79
80
  return getCachedResearch(ActionResult.fromState(state).baseHash);
80
81
  }
81
82
 
package/src/ai/rules.ts CHANGED
@@ -154,11 +154,11 @@ export const protectionRule = dedent`
154
154
 
155
155
  Pre-existing data on the page belongs to the application, not the test.
156
156
  Items that were not created inside the current test scenario must not be deleted, removed, emptied, reset, archived, or otherwise destroyed.
157
- If a scenario needs to verify destructive behaviour, the same scenario must first create a disposable target and then destroy that specific target — never operate on data that was already there when the test started.
157
+ If a scenario needs to verify destructive behaviour, the same scenario must first create its own target and then destroy that specific target — never operate on data that was already there when the test started.
158
158
 
159
159
  The resource that the current page URL represents is "under test".
160
160
  The test must not destroy the resource it is running against — doing so invalidates every subsequent scenario that starts on the same URL.
161
- Do not propose or perform delete/remove/archive actions on the entity that owns the current URL; propose such actions only on disposable children created within the scenario itself.
161
+ Do not propose or perform delete/remove/archive actions on the entity that owns the current URL; propose such actions only on children created within the scenario itself.
162
162
  </important>
163
163
  `;
164
164
 
@@ -174,7 +174,7 @@ export const dataProtectionRules = dedent`
174
174
  filter, tab, or list-inspection constraint. Use visible existing data when it is available.
175
175
  If no suitable data exists, report the missing precondition instead of creating data.
176
176
 
177
- Destructive actions are allowed only against disposable data created by the current scenario
177
+ Destructive actions are allowed only against data created by the current scenario
178
178
  or prepared for that scenario by Fisherman/API preconditions. Existing application data must
179
179
  remain unchanged.
180
180
  </data_protection_rules>
@@ -6,7 +6,7 @@ import { tag } from '../utils/logger.ts';
6
6
 
7
7
  export function validateSpecs(specs?: string[]): void {
8
8
  if (!specs?.length) {
9
- throw new Error('API spec is required. Set api.spec in your config file.');
9
+ throw new Error('API spec is required. Pass --spec, set EXPLORBOT_API_SPEC, or set api.spec in your config file.');
10
10
  }
11
11
  }
12
12
 
@@ -37,7 +37,7 @@ export class ConfigCommand extends BaseCommand {
37
37
  const dirs: Record<string, string> = {};
38
38
  if (options.root) {
39
39
  for (const [name, dir] of Object.entries({ output: 'output', ...config.dirs })) {
40
- dirs[name] = path.join(options.root, dir);
40
+ dirs[name] = path.resolve(options.root, dir);
41
41
  }
42
42
  }
43
43
 
@@ -26,7 +26,7 @@ export class DrillCommand extends BaseCommand {
26
26
  }
27
27
 
28
28
  private parseKnowledgeArg(args: string): string | undefined {
29
- const match = args.match(/--knowledge\s+(\S+)/);
29
+ const match = args.match(/--save-knowledge\s+(\S+)/);
30
30
  return match ? match[1] : undefined;
31
31
  }
32
32
 
@@ -261,9 +261,15 @@ export class ExploreCommand extends BaseCommand {
261
261
  tag('info').log(`Exploring sub-page: ${pick.url} (${pick.reason})`);
262
262
  try {
263
263
  await this.explorBot.visit(pick.url);
264
+ const errorPage = getStateErrorPageError(this.explorBot.stateManager().getCurrentState());
265
+ if (errorPage) {
266
+ tag('warning').log(`Skipping sub-page: ${errorPage.message}`);
267
+ this.failedSubPages.add(normalizeUrl(pick.url));
268
+ continue;
269
+ }
264
270
  await this.runAllStyles(pick.url, undefined, mainPlan, this.completedPlans, styles);
265
271
  const subPlan = this.explorBot.getCurrentPlan();
266
- if (subPlan && !this.completedPlans.includes(subPlan)) {
272
+ if (subPlan?.tests.length && !this.completedPlans.includes(subPlan)) {
267
273
  this.completedPlans.push(subPlan);
268
274
  }
269
275
  knownUrls.add(normalizeUrl(pick.url));
@@ -298,6 +304,11 @@ export class ExploreCommand extends BaseCommand {
298
304
  if (fresh && parentPlan) opts.extend = parentPlan;
299
305
  if (this.dryRun) opts.noSave = true;
300
306
  await this.planWithRetry(feature, opts, pageUrl);
307
+ const plan = this.explorBot.getCurrentPlan();
308
+ if (plan && plan.tests.length === 0) {
309
+ tag('warning').log('Nothing to test on this page, moving on');
310
+ return;
311
+ }
301
312
  await this.runPendingTests();
302
313
  this.rememberCurrentPlan();
303
314
  fresh = false;
@@ -0,0 +1,18 @@
1
+ import type { Command } from 'commander';
2
+
3
+ export abstract class BaseOption {
4
+ abstract flags: string;
5
+ abstract description: string;
6
+ collect?: (value: string, previous: any) => any;
7
+
8
+ register(program: Command): void {
9
+ if (this.collect) program.option(this.flags, this.description, this.collect);
10
+ if (!this.collect) program.option(this.flags, this.description);
11
+
12
+ program.hook('preAction', (_thisCommand, actionCommand) => {
13
+ this.apply(actionCommand.optsWithGlobals(), actionCommand);
14
+ });
15
+ }
16
+
17
+ protected abstract apply(options: Record<string, any>, command: Command): void;
18
+ }
@@ -0,0 +1,7 @@
1
+ import { KnowledgeOption } from './knowledge-option.js';
2
+ import { WsOption } from './ws-option.js';
3
+
4
+ export { BaseOption } from './base-option.js';
5
+
6
+ export const knowledgeOption = new KnowledgeOption();
7
+ export const wsOption = new WsOption();
@@ -0,0 +1,14 @@
1
+ import { KnowledgeTracker } from '../../knowledge-tracker.js';
2
+ import { BaseOption } from './base-option.js';
3
+
4
+ export class KnowledgeOption extends BaseOption {
5
+ flags = '--knowledge <text>';
6
+ description = 'Knowledge for this run only, not saved to disk. Markdown text; add url: or endpoint: frontmatter to scope it, otherwise it applies everywhere. Repeatable';
7
+ collect = (value: string, previous: string[] = []) => [...previous, value];
8
+
9
+ protected apply(options: Record<string, any>): void {
10
+ for (const text of options.knowledge || []) {
11
+ KnowledgeTracker.appendSessionKnowledge(text);
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,24 @@
1
+ import type { Command } from 'commander';
2
+ import { remote } from '../../remote.js';
3
+ import { BaseOption } from './base-option.js';
4
+
5
+ export class WsOption extends BaseOption {
6
+ flags = '--ws <url>';
7
+ description = 'Stream this run to a remote UI over WebSocket';
8
+
9
+ protected apply(options: Record<string, any>, command: Command): void {
10
+ const url = options.ws || process.env.EXPLORBOT_WS_URL;
11
+ if (!url) return;
12
+ remote.attach(String(url), commandPath(command));
13
+ }
14
+ }
15
+
16
+ function commandPath(command: Command): string {
17
+ const parts: string[] = [];
18
+ let node: Command | null = command;
19
+ while (node) {
20
+ parts.unshift(node.name());
21
+ node = node.parent;
22
+ }
23
+ return parts.slice(1).join(' ') || parts.join(' ');
24
+ }
package/src/config.ts CHANGED
@@ -271,6 +271,7 @@ export const EXPLORBOT_ENV_VARS: EnvVar[] = [
271
271
  { name: 'EXPLORBOT_EPHEMERAL', description: 'Keep no state between runs — output goes to a fresh temp directory instead of the site dir' },
272
272
  { name: 'EXPLORBOT_KNOWLEDGE', description: 'Inline knowledge text, applied to every page' },
273
273
  { name: 'EXPLORBOT_KNOWLEDGE_FILE', description: 'Path to a knowledge markdown file' },
274
+ { name: 'EXPLORBOT_SPEC', description: 'Docbot application spec directory or index.md, used as page knowledge' },
274
275
  { name: 'EXPLORBOT_API_SPEC', description: 'OpenAPI spec path for the API boat' },
275
276
  { name: 'EXPLORBOT_NO_BANNER', description: 'Suppress the startup banner, for machine-readable output' },
276
277
  { name: 'EXPLORBOT_MAX_DURATION', description: 'Wall-clock budget in minutes for an explore run; same as --max-duration' },
@@ -405,6 +406,8 @@ export class ConfigParser {
405
406
  this.enterGlobalMode(this.config, target);
406
407
  }
407
408
 
409
+ this.applyEnvSpec(this.config);
410
+
408
411
  // Restore original directory after successful config load
409
412
  if (options?.path && originalCwd !== process.cwd()) {
410
413
  process.chdir(originalCwd);
@@ -555,10 +558,18 @@ export class ConfigParser {
555
558
 
556
559
  config.dirs = { knowledge: 'knowledge', experience: 'experience', output: 'output' };
557
560
  config.playwright = { ...config.playwright, browser: config.playwright?.browser || 'chromium', url: site.baseUrl };
561
+ materializeKnowledge(this.site.dir);
558
562
 
559
563
  log(`Global mode: ${site.baseUrl} stored in ${this.site.dir}`);
560
564
  }
561
565
 
566
+ private applyEnvSpec(config: ExplorbotConfig): void {
567
+ const spec = process.env.EXPLORBOT_SPEC;
568
+ if (!spec) return;
569
+ if (!config.dirs) config.dirs = { knowledge: 'knowledge', experience: 'experience', output: 'output' };
570
+ config.dirs.spec = spec;
571
+ }
572
+
562
573
  private async buildEnvConfig(baseUrl: string | undefined, outputRoot: string): Promise<ExplorbotConfig> {
563
574
  const provider = process.env.EXPLORBOT_AI_PROVIDER;
564
575
  const modelSpec = process.env.EXPLORBOT_AI_MODEL;
package/src/explorbot.ts CHANGED
@@ -166,7 +166,7 @@ export class ExplorBot {
166
166
  }
167
167
 
168
168
  knowledgeTracker(): KnowledgeTracker {
169
- return (this._knowledgeTracker ||= new KnowledgeTracker(this.options.applicationSpec));
169
+ return (this._knowledgeTracker ||= new KnowledgeTracker({ applicationSpec: this.options.applicationSpec }));
170
170
  }
171
171
 
172
172
  experienceTracker(): ExperienceTracker {