explorbot 0.5.0 → 0.5.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.
@@ -12,7 +12,7 @@ import { HelpJsonCommand } from '../src/commands/help-json-command.js';
12
12
  import { RecommendedModelsCommand } from '../src/commands/recommended-models-command.js';
13
13
  import { App } from '../src/components/App.js';
14
14
  import { StatusPane } from '../src/components/StatusPane.js';
15
- import { knowledgeOption, wsOption } from '../src/commands/options/index.js';
15
+ import { decisionModelOption, knowledgeOption, wsOption } from '../src/commands/options/index.js';
16
16
  import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
17
17
  import { ExplorBot, type ExplorBotOptions } from '../src/explorbot.js';
18
18
  import { remote } from '../src/remote.js';
@@ -33,6 +33,7 @@ const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version as stri
33
33
  program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
34
34
  wsOption.register(program);
35
35
  knowledgeOption.register(program);
36
+ decisionModelOption.register(program);
36
37
 
37
38
  process.on('uncaughtException', async (error) => {
38
39
  tag('error').log(`Uncaught exception: ${error instanceof Error ? `${error.message}\n${error.stack}` : String(error)}`);
@@ -61,22 +61,49 @@ prima-cli browser stop
61
61
 
62
62
  Prima needs a model, and takes it from the environment. There is no `init` to run and nothing is written for you.
63
63
 
64
+ The setup we recommend runs everything through OpenRouter with one key: GPT-OSS 120B served by Groq as the main model, and TypeSafe's Jev as the [decision model](https://github.com/testomatio/explorbot/blob/main/docs/web-testing/decisions.md), which answers narrow yes/no and pick-one questions so the main model is called less often.
65
+
64
66
  ```bash
65
- export PRIMA_CLI_AI_MODEL=openrouter/openai/gpt-oss-120b
66
- export OPENROUTER_API_KEY=your-key
67
+ PRIMA_CLI_AI_MODEL=openrouter/openai/gpt-oss-120b:nitro
68
+ PRIMA_CLI_DECISION_MODEL=openrouter/typesafe/jev-1.13
69
+ OPENROUTER_API_KEY=your-key
67
70
  ```
68
71
 
69
- The provider comes from the model name, so that is the whole setup. Fish uses `set -gx` instead of `export`. Since prima is usually run by a coding agent, the better home is the agent's own config — in `~/.claude/settings.json`, an `env` block reaches every command the agent runs:
72
+ The provider comes from each model name, and `:nitro` asks OpenRouter for its fastest host, which is Groq for this model. Jev is optional: leave `PRIMA_CLI_DECISION_MODEL` unset and prima never calls it, or pass `--decision-model` for a single run.
73
+
74
+ Prima is usually run by a coding agent, so the best home for these variables is the agent's own config, where they reach every command the agent runs.
75
+
76
+ Claude Code, in `~/.claude/settings.json`:
70
77
 
71
78
  ```json
72
79
  {
73
80
  "env": {
74
- "PRIMA_CLI_AI_MODEL": "openrouter/openai/gpt-oss-120b",
81
+ "PRIMA_CLI_AI_MODEL": "openrouter/openai/gpt-oss-120b:nitro",
82
+ "PRIMA_CLI_DECISION_MODEL": "openrouter/typesafe/jev-1.13",
75
83
  "OPENROUTER_API_KEY": "your-key"
76
84
  }
77
85
  }
78
86
  ```
79
87
 
88
+ Codex, in `~/.codex/config.toml`:
89
+
90
+ ```toml
91
+ [shell_environment_policy]
92
+ set = { PRIMA_CLI_AI_MODEL = "openrouter/openai/gpt-oss-120b:nitro", PRIMA_CLI_DECISION_MODEL = "openrouter/typesafe/jev-1.13", OPENROUTER_API_KEY = "your-key" }
93
+ ```
94
+
95
+ `set` is applied after Codex filters the inherited environment, so the key survives even where names containing `KEY` are stripped.
96
+
97
+ To run prima yourself, or from any agent that inherits your shell, export them in `~/.bashrc` or `~/.zshrc`:
98
+
99
+ ```bash
100
+ export PRIMA_CLI_AI_MODEL=openrouter/openai/gpt-oss-120b:nitro
101
+ export PRIMA_CLI_DECISION_MODEL=openrouter/typesafe/jev-1.13
102
+ export OPENROUTER_API_KEY=your-key
103
+ ```
104
+
105
+ Fish uses `set -gx` in `~/.config/fish/config.fish` instead of `export`.
106
+
80
107
  Screenshot analysis needs its own model — one is never guessed from the main model. Set `PRIMA_CLI_VISION_MODEL`, or pass `--vision-model` for a single run, as `--model` does for the main one. Without it prima still runs, but settles `check` outcomes from the run log rather than from the page as seen.
81
108
 
82
109
  Every `PRIMA_CLI_*` variable mirrors the `EXPLORBOT_*` one of the same name and wins over it, so prima can be pointed at a different provider, model or URL without disturbing an explorbot setup on the same machine. `PRIMA_CLI_AI_MODEL`, `PRIMA_CLI_URL`, `PRIMA_CLI_VISION_MODEL` and the rest all work this way.
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env bun
2
- import { knowledgeOption } from '../../../src/commands/options/index.ts';
2
+ import { decisionModelOption, knowledgeOption } from '../../../src/commands/options/index.ts';
3
3
  import { createPrimaCommands } from '../src/cli.ts';
4
4
 
5
5
  const program = createPrimaCommands('prima');
6
6
  knowledgeOption.register(program);
7
+ decisionModelOption.register(program);
7
8
  program.parse();
@@ -12,7 +12,7 @@ import { HelpJsonCommand } from '../src/commands/help-json-command.js';
12
12
  import { RecommendedModelsCommand } from '../src/commands/recommended-models-command.js';
13
13
  import { App } from '../src/components/App.js';
14
14
  import { StatusPane } from '../src/components/StatusPane.js';
15
- import { knowledgeOption, wsOption } from '../src/commands/options/index.js';
15
+ import { decisionModelOption, knowledgeOption, wsOption } from '../src/commands/options/index.js';
16
16
  import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
17
17
  import { ExplorBot } from '../src/explorbot.js';
18
18
  import { remote } from '../src/remote.js';
@@ -30,6 +30,7 @@ const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
30
30
  program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
31
31
  wsOption.register(program);
32
32
  knowledgeOption.register(program);
33
+ decisionModelOption.register(program);
33
34
  process.on('uncaughtException', async (error) => {
34
35
  tag('error').log(`Uncaught exception: ${error instanceof Error ? `${error.message}\n${error.stack}` : String(error)}`);
35
36
  await flushTelemetry();
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { knowledgeOption } from "../../../src/commands/options/index.js";
2
+ import { decisionModelOption, knowledgeOption } from "../../../src/commands/options/index.js";
3
3
  import { createPrimaCommands } from "../src/cli.js";
4
4
  const program = createPrimaCommands('prima');
5
5
  knowledgeOption.register(program);
6
+ decisionModelOption.register(program);
6
7
  program.parse();
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explorbot",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
@@ -102,7 +102,7 @@
102
102
  "bash-tool": "^1.3.15",
103
103
  "chalk": "^5.6.2",
104
104
  "cli-highlight": "^2.1.11",
105
- "codeceptjs": "^4.2.0-beta.3",
105
+ "codeceptjs": "^4.2.0",
106
106
  "commander": "^14.0.1",
107
107
  "debug": "^4.4.3",
108
108
  "dedent": "^1.6.0",
@@ -615,7 +615,7 @@ export class Provider {
615
615
  if (!this.config.visionModel) {
616
616
  throw new Error(`Vision model not configured. Set ai.visionModel in your config; ${getCliName()} recommended-models lists the providers that serve one`);
617
617
  }
618
- setActivity(`🤖 Processing image with ${this.config.visionModel}`, 'ai');
618
+ setActivity(`🤖 Processing image with ${getModelName(this.config.visionModel)}`, 'ai');
619
619
  const messages = [
620
620
  {
621
621
  role: 'user',
@@ -480,6 +480,19 @@ export const actionRule = dedent `
480
480
 
481
481
  After hovering, use see() or context() to check what appeared.
482
482
 
483
+ ### I.seeInClipboard / I.seeClipboardEquals / I.clearClipboard
484
+
485
+ Check what a copy control put into the system clipboard. Run via form(), not click().
486
+ I.seeInClipboard asserts the clipboard contains the text, I.seeClipboardEquals asserts an exact match,
487
+ I.clearClipboard empties it — call it before copying so a stale value can't pass the check.
488
+
489
+ <example>
490
+ I.clearClipboard();
491
+ I.click({ role: 'button', text: 'Copy link' });
492
+ I.seeInClipboard('/invite/');
493
+ I.seeClipboardEquals('ABC-123');
494
+ </example>
495
+
483
496
  [DO NEVER USE OTHER CODECEPTJS COMMANDS THAN PROPOSED HERE]
484
497
  [INTERACT ONLY WITH ELEMENTS THAT ARE ON THE PAGE HTML]
485
498
  [DO NOT USE WAIT FUNCTIONS]
@@ -0,0 +1,6 @@
1
+ import { BaseOption } from './base-option.js';
2
+ export declare class DecisionModelOption extends BaseOption {
3
+ flags: string;
4
+ description: string;
5
+ apply(options: Record<string, any>): void;
6
+ }
@@ -0,0 +1,10 @@
1
+ import { BaseOption } from './base-option.js';
2
+ export class DecisionModelOption extends BaseOption {
3
+ flags = '--decision-model <model>';
4
+ description = 'Turn on the decision model for this run, as provider/model-id (e.g. openrouter/typesafe/jev-1.13)';
5
+ apply(options) {
6
+ if (!options.decisionModel)
7
+ return;
8
+ process.env.EXPLORBOT_DECISION_MODEL = options.decisionModel;
9
+ }
10
+ }
@@ -1,5 +1,7 @@
1
+ import { DecisionModelOption } from './decision-model-option.js';
1
2
  import { KnowledgeOption } from './knowledge-option.js';
2
3
  import { WsOption } from './ws-option.js';
3
4
  export { BaseOption } from './base-option.js';
5
+ export declare const decisionModelOption: DecisionModelOption;
4
6
  export declare const knowledgeOption: KnowledgeOption;
5
7
  export declare const wsOption: WsOption;
@@ -1,5 +1,7 @@
1
+ import { DecisionModelOption } from './decision-model-option.js';
1
2
  import { KnowledgeOption } from './knowledge-option.js';
2
3
  import { WsOption } from './ws-option.js';
3
4
  export { BaseOption } from './base-option.js';
5
+ export const decisionModelOption = new DecisionModelOption();
4
6
  export const knowledgeOption = new KnowledgeOption();
5
7
  export const wsOption = new WsOption();
@@ -270,6 +270,7 @@ export declare class ConfigParser {
270
270
  static cleanupAllTestDirectories(): void;
271
271
  enterGlobalMode(config: ExplorbotConfig, target: string | null): Promise<ExplorbotConfig>;
272
272
  applyEnvSpec(config: ExplorbotConfig): void;
273
+ applyEnvDecisionModel(config: ExplorbotConfig): void;
273
274
  buildEnvConfig(baseUrl: string | undefined, outputRoot: string): Promise<ExplorbotConfig>;
274
275
  findConfigFile(): string | null;
275
276
  loadConfigModule(configPath: string): Promise<any>;
@@ -45,6 +45,7 @@ export const EXPLORBOT_ENV_VARS = [
45
45
  { name: 'EXPLORBOT_URL', required: true, description: 'Base URL to test; the API boat reads it as the base endpoint' },
46
46
  { name: 'EXPLORBOT_VISION_MODEL', description: 'Screenshot analysis; overrides the provider recommendation' },
47
47
  { name: 'EXPLORBOT_AGENTIC_MODEL', description: 'Captain and Pilot decisions; overrides the provider recommendation' },
48
+ { name: 'EXPLORBOT_DECISION_MODEL', description: 'Turns on the decision model, as provider/model-id (openrouter/typesafe/jev-1.13 or typesafe/jev-latest); overrides ai.decisionModel' },
48
49
  { name: 'EXPLORBOT_OUTPUT', description: 'Output root for states, plans, research, and reports. Defaults to the site dir under ~/.explorbot/sites' },
49
50
  { name: 'EXPLORBOT_EPHEMERAL', description: 'Keep no state between runs — output goes to a fresh temp directory instead of the site dir' },
50
51
  { name: 'EXPLORBOT_KNOWLEDGE', description: 'Inline knowledge text, applied to every page' },
@@ -138,6 +139,7 @@ export class ConfigParser {
138
139
  this.runtimeTarget = target;
139
140
  this.configPath = sourcePath;
140
141
  this.applyEnvSpec(this.config);
142
+ this.applyEnvDecisionModel(this.config);
141
143
  // Restore original directory after successful config load
142
144
  if (options?.path && originalCwd !== process.cwd()) {
143
145
  process.chdir(originalCwd);
@@ -300,6 +302,15 @@ export class ConfigParser {
300
302
  config.dirs = { knowledge: 'knowledge', experience: 'experience', output: 'output' };
301
303
  config.dirs.spec = spec;
302
304
  }
305
+ applyEnvDecisionModel(config) {
306
+ const spec = process.env.EXPLORBOT_DECISION_MODEL;
307
+ if (!spec)
308
+ return;
309
+ const separator = spec.indexOf('/');
310
+ if (separator < 1)
311
+ throw new Error('EXPLORBOT_DECISION_MODEL must be written as "provider/model-id", e.g. openrouter/typesafe/jev-1.13');
312
+ config.ai.decisionModel = { ...config.ai.decisionModel, provider: spec.slice(0, separator), model: spec.slice(separator + 1) };
313
+ }
303
314
  async buildEnvConfig(baseUrl, outputRoot) {
304
315
  const provider = process.env.EXPLORBOT_AI_PROVIDER;
305
316
  const modelSpec = process.env.EXPLORBOT_AI_MODEL;
@@ -22,6 +22,8 @@ Explorbot uses three roles:
22
22
 
23
23
  Pick a fast, cheap model for the first two and a stronger one for the third. When a provider has no recommended model for one of these roles, combine it with another provider for that role.
24
24
 
25
+ An optional [decision model](#decision-model) can answer narrow yes/no and pick-one questions so the other models are called less often.
26
+
25
27
  ### OpenRouter
26
28
 
27
29
  Start with OpenRouter. One key reaches [many providers and models](https://openrouter.ai/models).
@@ -308,6 +310,40 @@ Laguna XS is an agentic coding model — fast, cheap, and reliable at tool calli
308
310
 
309
311
  Keep `agenticModel` on another provider. Poolside's endpoint accepts `response_format: json_schema` but does not enforce it, so structured-output calls depend on the model volunteering valid JSON. Laguna XS usually does; Laguna S answers in prose instead, which makes it unusable for the Planner, Pilot, and Captain. Laguna S is also slow enough under page-sized prompts to hit Explorbot's request timeouts, so it is not a substitute for Laguna XS in the `model` role either.
310
312
 
313
+ ## Decision Model
314
+
315
+ A decision model is an optional fourth role. It does not generate text or call tools. It receives the current state and one closed question, and returns an answer with a probability. Explorbot uses it for narrow choices that would otherwise take a call to a larger model or a guess.
316
+
317
+ Explorbot supports TypeSafe's Jev, reached through OpenRouter or through TypeSafe's API directly:
318
+
319
+ ```javascript
320
+ export default {
321
+ ai: {
322
+ // ...your model, visionModel, agenticModel...
323
+ decisionModel: { provider: 'openrouter', model: 'typesafe/jev-1.13' },
324
+ // or: decisionModel: { provider: 'typesafe', model: 'jev-latest' },
325
+ },
326
+ };
327
+ ```
328
+
329
+ | Option | Type | Description |
330
+ |--------|------|-------------|
331
+ | `provider` | `'openrouter' \| 'typesafe'` | Authenticates with `OPENROUTER_API_KEY` or `TYPESAFE_API_KEY` |
332
+ | `model` | `string` | Model ID at that provider |
333
+ | `tool` | `boolean` | Give Tester and Pilot a `judge` tool to ask their own questions. Default: `true` |
334
+ | `direct` | `boolean` | Consult the model at Explorbot's built-in decision points. Default: `true` |
335
+
336
+ Without a config file, or to turn it on for one run, set `EXPLORBOT_DECISION_MODEL` or pass `--decision-model` as `provider/model-id`. It overrides `provider` and `model` in the config and keeps `tool` and `direct`:
337
+
338
+ ```bash
339
+ EXPLORBOT_DECISION_MODEL=openrouter/typesafe/jev-1.13 npx explorbot explore
340
+ npx prima-cli go "Settings" --decision-model typesafe/jev-latest
341
+ ```
342
+
343
+ Leave `decisionModel` unset and Explorbot never calls it. An unknown provider or a missing API key stops Explorbot at startup.
344
+
345
+ Explorbot acts on an answer only when it is more than 70% sure, and otherwise does what it would have done without the decision model. See [Decisions](../web-testing/decisions.md) for where it is consulted, the `judge` tool, and what happens when an answer is rejected.
346
+
311
347
  ## Multi-Provider Configuration
312
348
 
313
349
  Mix clients the same way you assign `model`, `visionModel`, and `agenticModel`. Each field can use a different provider instance — a fast provider does the token-heavy reading while a stronger one makes the decisions:
@@ -359,4 +395,5 @@ export OPENAI_API_KEY=your-key-here
359
395
  export ANTHROPIC_API_KEY=your-key-here
360
396
  export GOOGLE_API_KEY=your-key-here
361
397
  export MISTRAL_API_KEY=your-key-here
398
+ export TYPESAFE_API_KEY=your-key-here
362
399
  ```
package/docs/index.json CHANGED
@@ -22,6 +22,7 @@
22
22
  { "title": "Planner", "file": "web-testing/planner.md", "description": "Tune what gets tested" },
23
23
  { "title": "Researcher", "file": "web-testing/researcher.md", "description": "How pages are analyzed, and how to tune it" },
24
24
  { "title": "Page interaction", "file": "web-testing/page-interaction.md", "description": "How agents read and act on a page" },
25
+ { "title": "Decisions", "file": "web-testing/decisions.md", "description": "The optional decision model: built-in decision points, the judge tool, fallbacks" },
25
26
  { "title": "Automated tests", "file": "web-testing/automated-tests.md", "description": "The runnable test files you keep" },
26
27
  { "title": "Rerun", "file": "web-testing/rerun.md", "description": "Re-run generated tests with AI healing" },
27
28
  { "title": "Agents", "file": "web-testing/agents.md", "description": "What each agent does under the hood" },
@@ -112,6 +112,7 @@ EXPLORBOT_AI_PROVIDER=openrouter \
112
112
  | `EXPLORBOT_URL` | Base URL to test; the API boat reads it as the base endpoint |
113
113
  | `EXPLORBOT_VISION_MODEL` | Screenshot analysis; overrides the provider recommendation |
114
114
  | `EXPLORBOT_AGENTIC_MODEL` | Captain and Pilot decisions; overrides the provider recommendation |
115
+ | `EXPLORBOT_DECISION_MODEL` | Turns on the decision model, as provider/model-id (openrouter/typesafe/jev-1.13 or typesafe/jev-latest); overrides ai.decisionModel |
115
116
  | `EXPLORBOT_OUTPUT` | Output root for states, plans, research, and reports. Defaults to the site dir under ~/.explorbot/sites |
116
117
  | `EXPLORBOT_EPHEMERAL` | Keep no state between runs — output goes to a fresh temp directory instead of the site dir |
117
118
  | `EXPLORBOT_KNOWLEDGE` | Inline knowledge text, applied to every page |
@@ -537,6 +537,7 @@ export default {
537
537
  ai: {
538
538
  model: groq('openai/gpt-oss-20b'), // Default model instance (required)
539
539
  visionModel: groq('meta-llama/llama-4-scout-17b-16e-instruct'), // Model for screenshot analysis; setting it enables vision features
540
+ decisionModel: { provider: 'openrouter', model: 'typesafe/jev-1.13' }, // Optional; see providers docs
540
541
  config: {}, // Additional provider config
541
542
  langfuse: { // Observability settings
542
543
  enabled: true,
@@ -624,6 +625,7 @@ export default {
624
625
 
625
626
  - [API testing](../api-testing/basics.md) — API testing setup and commands
626
627
  - [AI providers](../basics/providers.md) — provider setup examples
628
+ - [Decisions](../web-testing/decisions.md) — the optional decision model and its fallbacks
627
629
  - [Agents](../web-testing/agents.md) — agent descriptions and workflows
628
630
  - [Agent hooks](../web-testing/hooks.md) — custom code before and after an agent runs
629
631
  - [Researcher agent](../web-testing/researcher.md) — Researcher configuration and usage
@@ -0,0 +1,108 @@
1
+ # Decisions
2
+
3
+ A decision model answers one closed question about the current state: a yes/no statement, or a pick from a list of options. It returns the answer together with its probability. Explorbot uses it to settle narrow choices quickly and cheaply that would otherwise need a call to a larger model or a guess.
4
+
5
+ The decision model is optional. Without it Explorbot behaves exactly as described elsewhere in these docs. Setup is covered in [AI providers](../basics/providers.md#decision-model).
6
+
7
+ ## Decision rules
8
+
9
+ Every question has one of two shapes:
10
+
11
+ | Shape | Asked as | Answer |
12
+ |---|---|---|
13
+ | **Statement** | "The page shows that this claim is true." | Probability that the statement holds |
14
+ | **Choice** | "Which listed element does the intent name?" plus numbered options | The chosen option and its probability |
15
+
16
+ A choice always includes an extra *undecided* option, so the model can say that none of the options fits.
17
+
18
+ Explorbot acts on an answer only when it is **approved**:
19
+
20
+ - a statement's probability is above **70%**, or
21
+ - a choice picked a real option, not *undecided*, with a probability above **70%**.
22
+
23
+ Anything else counts as **rejected**, including:
24
+
25
+ - a confident "no", or any probability of 70% or lower
26
+ - *undecided*
27
+ - a choice offered with fewer than two options
28
+ - a request timeout (15 seconds) or a failed request
29
+ - `direct: false` in the config, for built-in decision points
30
+
31
+ A rejected answer never means "confidently no". It means Explorbot has no usable answer, so it takes its usual path. Every question is worded so that approving it lets Explorbot take a shortcut and rejecting it keeps the usual path. A wrong or unsure answer can therefore cost a shortcut, but never a check.
32
+
33
+ ## Built-in decision points
34
+
35
+ These are fixed places in the code where Explorbot asks the decision model before doing the usual, more expensive work. Setting `direct: false` turns them off.
36
+
37
+ ### Tester: locator matches several elements
38
+
39
+ When a click or fill fails because the locator matched more than one element, Explorbot lists the matches and asks which one the step meant.
40
+
41
+ - **Approved:** the Tester is told which element to use and how to target it with `elementIndex`.
42
+ - **Fallback:** the Tester receives the numbered list of matches and picks one itself, narrows the locator, or uses a visual click.
43
+
44
+ ### Pilot: progress review
45
+
46
+ Every few steps, Pilot reviews the Tester's recent actions with the `agenticModel`. First it asks whether the run is moving toward its goal and can continue unsupervised for now.
47
+
48
+ - **Approved:** the review is skipped and the Tester continues. This saves one `agenticModel` call.
49
+ - **Fallback:** Pilot runs its full review as usual.
50
+
51
+ ### Navigator: claim already verified
52
+
53
+ Before Navigator writes assertions for a claim, it asks whether a claim it has already verified on this page means the same thing, even if it is worded differently.
54
+
55
+ - **Approved:** the claim counts as verified and no assertions are generated or run.
56
+ - **Fallback:** Navigator verifies the claim with assertions as usual.
57
+
58
+ The new claim is not added to the page's verified claims. Only a claim backed by assertions that actually ran is recorded as verified.
59
+
60
+ ### Navigator: claim no assertion can express
61
+
62
+ Some claims cannot be turned into an assertion. When that happens, Navigator asks whether the page shows the claim is true.
63
+
64
+ - **Approved:** `verify` reports that the page appears to confirm the claim. The result is marked as a judgement, kept apart from assertions that ran, and the Tester is asked to restate the claim so it can be asserted.
65
+ - **Fallback:** `verify` reports that no assertion could express the claim, and suggests restating it or checking it with `see`.
66
+
67
+ In both cases `verify` returns a failure, because nothing ran in the browser.
68
+
69
+ ### Prima: `go` to a described page
70
+
71
+ When `prima go` gets a description of a page instead of a URL, it asks which control on the current page leads there.
72
+
73
+ - **Approved:** Prima clicks that control. It then asks a second question: whether the new page shows the target. Only when that is approved too does Prima report success.
74
+ - **Fallback:** if the choice is rejected, or the click fails, or the page doesn't change, or arrival is not confirmed, Prima hands off to the Navigator as usual.
75
+
76
+ ### Prima: `check` expected outcomes
77
+
78
+ When `prima check` settles the expected outcomes of a finished run without a screenshot, it asks what the run established about each outcome: that it happened, or that it did not.
79
+
80
+ - **Approved:** that outcome is settled as passed or failed without calling the `agenticModel`.
81
+ - **Fallback:** the remaining outcomes are settled by the `agenticModel` as usual. When a screenshot is available, all outcomes go to the vision model and the decision model is not asked.
82
+
83
+ ## The `judge` tool
84
+
85
+ With `tool: true`, Tester and Pilot get a `judge` tool. They use it when a step depends on reading the page rather than running a command. The tool takes:
86
+
87
+ | Argument | Description |
88
+ |---|---|
89
+ | `question` | A statement to confirm, or the question a list of options answers |
90
+ | `options` | Possible answers. Leave it out to confirm a statement |
91
+ | `context` | Anything the page itself doesn't show |
92
+
93
+ The model supplies only the question. The tool gathers the rest of the state itself: the current scenario, the compact ARIA snapshot of the page (up to 12,000 characters), and the last eight test steps.
94
+
95
+ - **Approved:** the tool returns the answer and its confidence.
96
+ - **Rejected:** the tool returns "Not confirmed" and tells the agent to gather more context or try another route. "Not confirmed" means the page doesn't settle the question. It does not mean the statement is false.
97
+
98
+ `tool` and `direct` are independent. For example, `tool: false` keeps the built-in decision points but stops the agents from asking questions of their own.
99
+
100
+ ## Where it is not used
101
+
102
+ - **Pilot's verdict on a test.** Whether a scenario passed, failed or was impossible is always decided by the `agenticModel`.
103
+ - **Filtering experience.** One question per stored experience block, on every prompt, would cost more than it saves.
104
+ - **Generation, screenshots and tool calling.** The decision model reads text and answers questions. It does not write, look at images or act on the page.
105
+
106
+ ## Tracing
107
+
108
+ Each question is recorded as a `judge.decide` span with the question, the answer and its confidence. A failed request also records the reason. The TUI shows `⚖️ Asking judge...` while a question is in flight. See [Observability](../contributing/observability.md) for setting up tracing.
@@ -52,6 +52,7 @@ No `init`, no config file, no project directory, no model IDs to look up. These
52
52
  | `EXPLORBOT_URL` | yes | Base URL to test; the API boat reads it as the base endpoint |
53
53
  | `EXPLORBOT_VISION_MODEL` | no | Screenshot analysis; overrides the provider recommendation |
54
54
  | `EXPLORBOT_AGENTIC_MODEL` | no | Captain and Pilot decisions; overrides the provider recommendation |
55
+ | `EXPLORBOT_DECISION_MODEL` | no | Turns on the decision model, as provider/model-id (openrouter/typesafe/jev-1.13 or typesafe/jev-latest); overrides ai.decisionModel |
55
56
  | `EXPLORBOT_OUTPUT` | no | Output root for states, plans, research, and reports. Defaults to the site dir under ~/.explorbot/sites |
56
57
  | `EXPLORBOT_EPHEMERAL` | no | Keep no state between runs — output goes to a fresh temp directory instead of the site dir |
57
58
  | `EXPLORBOT_KNOWLEDGE` | no | Inline knowledge text, applied to every page |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explorbot",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
@@ -102,7 +102,7 @@
102
102
  "bash-tool": "^1.3.15",
103
103
  "chalk": "^5.6.2",
104
104
  "cli-highlight": "^2.1.11",
105
- "codeceptjs": "^4.2.0-beta.3",
105
+ "codeceptjs": "^4.2.0",
106
106
  "commander": "^14.0.1",
107
107
  "debug": "^4.4.3",
108
108
  "dedent": "^1.6.0",
@@ -683,7 +683,7 @@ export class Provider {
683
683
  throw new Error(`Vision model not configured. Set ai.visionModel in your config; ${getCliName()} recommended-models lists the providers that serve one`);
684
684
  }
685
685
 
686
- setActivity(`🤖 Processing image with ${this.config.visionModel}`, 'ai');
686
+ setActivity(`🤖 Processing image with ${getModelName(this.config.visionModel)}`, 'ai');
687
687
 
688
688
  const messages: ModelMessage[] = [
689
689
  {
package/src/ai/rules.ts CHANGED
@@ -497,6 +497,19 @@ export const actionRule = dedent`
497
497
 
498
498
  After hovering, use see() or context() to check what appeared.
499
499
 
500
+ ### I.seeInClipboard / I.seeClipboardEquals / I.clearClipboard
501
+
502
+ Check what a copy control put into the system clipboard. Run via form(), not click().
503
+ I.seeInClipboard asserts the clipboard contains the text, I.seeClipboardEquals asserts an exact match,
504
+ I.clearClipboard empties it — call it before copying so a stale value can't pass the check.
505
+
506
+ <example>
507
+ I.clearClipboard();
508
+ I.click({ role: 'button', text: 'Copy link' });
509
+ I.seeInClipboard('/invite/');
510
+ I.seeClipboardEquals('ABC-123');
511
+ </example>
512
+
500
513
  [DO NEVER USE OTHER CODECEPTJS COMMANDS THAN PROPOSED HERE]
501
514
  [INTERACT ONLY WITH ELEMENTS THAT ARE ON THE PAGE HTML]
502
515
  [DO NOT USE WAIT FUNCTIONS]
@@ -0,0 +1,11 @@
1
+ import { BaseOption } from './base-option.js';
2
+
3
+ export class DecisionModelOption extends BaseOption {
4
+ flags = '--decision-model <model>';
5
+ description = 'Turn on the decision model for this run, as provider/model-id (e.g. openrouter/typesafe/jev-1.13)';
6
+
7
+ protected apply(options: Record<string, any>): void {
8
+ if (!options.decisionModel) return;
9
+ process.env.EXPLORBOT_DECISION_MODEL = options.decisionModel;
10
+ }
11
+ }
@@ -1,7 +1,9 @@
1
+ import { DecisionModelOption } from './decision-model-option.js';
1
2
  import { KnowledgeOption } from './knowledge-option.js';
2
3
  import { WsOption } from './ws-option.js';
3
4
 
4
5
  export { BaseOption } from './base-option.js';
5
6
 
7
+ export const decisionModelOption = new DecisionModelOption();
6
8
  export const knowledgeOption = new KnowledgeOption();
7
9
  export const wsOption = new WsOption();
package/src/config.ts CHANGED
@@ -278,6 +278,7 @@ export const EXPLORBOT_ENV_VARS: EnvVar[] = [
278
278
  { name: 'EXPLORBOT_URL', required: true, description: 'Base URL to test; the API boat reads it as the base endpoint' },
279
279
  { name: 'EXPLORBOT_VISION_MODEL', description: 'Screenshot analysis; overrides the provider recommendation' },
280
280
  { name: 'EXPLORBOT_AGENTIC_MODEL', description: 'Captain and Pilot decisions; overrides the provider recommendation' },
281
+ { name: 'EXPLORBOT_DECISION_MODEL', description: 'Turns on the decision model, as provider/model-id (openrouter/typesafe/jev-1.13 or typesafe/jev-latest); overrides ai.decisionModel' },
281
282
  { name: 'EXPLORBOT_OUTPUT', description: 'Output root for states, plans, research, and reports. Defaults to the site dir under ~/.explorbot/sites' },
282
283
  { name: 'EXPLORBOT_EPHEMERAL', description: 'Keep no state between runs — output goes to a fresh temp directory instead of the site dir' },
283
284
  { name: 'EXPLORBOT_KNOWLEDGE', description: 'Inline knowledge text, applied to every page' },
@@ -423,6 +424,7 @@ export class ConfigParser {
423
424
  this.runtimeTarget = target;
424
425
  this.configPath = sourcePath;
425
426
  this.applyEnvSpec(this.config);
427
+ this.applyEnvDecisionModel(this.config);
426
428
 
427
429
  // Restore original directory after successful config load
428
430
  if (options?.path && originalCwd !== process.cwd()) {
@@ -600,6 +602,14 @@ export class ConfigParser {
600
602
  config.dirs.spec = spec;
601
603
  }
602
604
 
605
+ private applyEnvDecisionModel(config: ExplorbotConfig): void {
606
+ const spec = process.env.EXPLORBOT_DECISION_MODEL;
607
+ if (!spec) return;
608
+ const separator = spec.indexOf('/');
609
+ if (separator < 1) throw new Error('EXPLORBOT_DECISION_MODEL must be written as "provider/model-id", e.g. openrouter/typesafe/jev-1.13');
610
+ config.ai.decisionModel = { ...config.ai.decisionModel, provider: spec.slice(0, separator), model: spec.slice(separator + 1) };
611
+ }
612
+
603
613
  private async buildEnvConfig(baseUrl: string | undefined, outputRoot: string): Promise<ExplorbotConfig> {
604
614
  const provider = process.env.EXPLORBOT_AI_PROVIDER;
605
615
  const modelSpec = process.env.EXPLORBOT_AI_MODEL;