explorbot 0.2.3 → 0.2.5

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 (174) hide show
  1. package/README.md +1 -1
  2. package/bin/explorbot-cli.ts +26 -8
  3. package/boat/api-tester/src/cli.ts +17 -0
  4. package/boat/api-tester/src/config.ts +4 -2
  5. package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
  6. package/boat/doc-collector/src/ai/documentarian.ts +61 -31
  7. package/boat/doc-collector/src/cli.ts +14 -1
  8. package/boat/doc-collector/src/config.ts +4 -2
  9. package/boat/prima/bin/prima-cli.ts +0 -0
  10. package/boat/prima/src/activity-line.ts +33 -0
  11. package/boat/prima/src/cli.ts +127 -86
  12. package/boat/prima/src/envelope.ts +102 -52
  13. package/boat/prima/src/prima.ts +567 -128
  14. package/boat/prima/src/pw-parser.ts +11 -1
  15. package/boat/prima/src/pw-registry.ts +4 -5
  16. package/boat/prima/src/session-log.ts +126 -0
  17. package/dist/bin/explorbot-cli.js +26 -8
  18. package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
  19. package/dist/boat/api-tester/src/cli.js +17 -0
  20. package/dist/boat/api-tester/src/config.js +4 -2
  21. package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
  22. package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
  23. package/dist/boat/doc-collector/src/cli.js +14 -1
  24. package/dist/boat/doc-collector/src/config.js +4 -2
  25. package/dist/boat/prima/src/activity-line.js +30 -0
  26. package/dist/boat/prima/src/cli.js +109 -77
  27. package/dist/boat/prima/src/envelope.js +94 -44
  28. package/dist/boat/prima/src/prima.js +533 -119
  29. package/dist/boat/prima/src/pw-parser.js +13 -1
  30. package/dist/boat/prima/src/pw-registry.js +4 -5
  31. package/dist/boat/prima/src/session-log.js +108 -0
  32. package/dist/package.json +3 -2
  33. package/dist/rules/navigator/verification-actions.md +20 -0
  34. package/dist/src/action-result.d.ts +7 -0
  35. package/dist/src/action-result.js +4 -0
  36. package/dist/src/action.d.ts +2 -0
  37. package/dist/src/action.js +41 -2
  38. package/dist/src/ai/captain/web-mode.js +6 -3
  39. package/dist/src/ai/captain.js +2 -0
  40. package/dist/src/ai/navigator.d.ts +34 -0
  41. package/dist/src/ai/navigator.js +237 -181
  42. package/dist/src/ai/pilot.d.ts +7 -0
  43. package/dist/src/ai/pilot.js +90 -2
  44. package/dist/src/ai/provider.d.ts +2 -2
  45. package/dist/src/ai/provider.js +14 -23
  46. package/dist/src/ai/rerunner.js +2 -1
  47. package/dist/src/ai/researcher/cache.d.ts +2 -0
  48. package/dist/src/ai/researcher/cache.js +10 -2
  49. package/dist/src/ai/researcher.js +3 -2
  50. package/dist/src/ai/rules.js +17 -10
  51. package/dist/src/ai/session-analyst.js +2 -0
  52. package/dist/src/ai/task-agent.js +4 -1
  53. package/dist/src/ai/tester.d.ts +6 -3
  54. package/dist/src/ai/tester.js +50 -46
  55. package/dist/src/ai/tools.d.ts +14 -0
  56. package/dist/src/ai/tools.js +117 -37
  57. package/dist/src/commands/config-command.d.ts +51 -0
  58. package/dist/src/commands/config-command.js +117 -0
  59. package/dist/src/commands/index.js +2 -0
  60. package/dist/src/config.d.ts +9 -1
  61. package/dist/src/config.js +53 -4
  62. package/dist/src/execution-controller.d.ts +2 -0
  63. package/dist/src/execution-controller.js +6 -0
  64. package/dist/src/explorbot.d.ts +2 -1
  65. package/dist/src/explorbot.js +7 -2
  66. package/dist/src/explorer.js +2 -3
  67. package/dist/src/playwright-recorder.js +30 -0
  68. package/dist/src/remote.d.ts +55 -0
  69. package/dist/src/remote.js +235 -0
  70. package/dist/src/reporter.d.ts +1 -0
  71. package/dist/src/reporter.js +7 -1
  72. package/dist/src/state-manager.d.ts +2 -1
  73. package/dist/src/state-manager.js +3 -1
  74. package/dist/src/stats.d.ts +1 -0
  75. package/dist/src/stats.js +1 -0
  76. package/dist/src/test-plan.d.ts +3 -0
  77. package/dist/src/test-plan.js +26 -0
  78. package/dist/src/utils/aria.d.ts +2 -8
  79. package/dist/src/utils/aria.js +69 -40
  80. package/dist/src/utils/html.js +1 -0
  81. package/dist/src/utils/logger.d.ts +7 -1
  82. package/dist/src/utils/logger.js +32 -0
  83. package/dist/src/utils/page-readiness.js +18 -1
  84. package/dist/src/utils/url-matcher.js +3 -0
  85. package/dist/src/utils/web-element.d.ts +2 -0
  86. package/dist/src/utils/web-element.js +8 -0
  87. package/dist/src/utils/web-sandbox.d.ts +1 -1
  88. package/dist/src/utils/web-sandbox.js +2 -3
  89. package/docs/api-testing/basics.md +90 -0
  90. package/docs/api-testing/planning.md +57 -0
  91. package/docs/api-testing/running-tests.md +55 -0
  92. package/docs/assets/cloud-report.png +0 -0
  93. package/docs/assets/html-report.png +0 -0
  94. package/docs/assets/langfuse-trace.png +0 -0
  95. package/docs/assets/successful-explore-run.png +0 -0
  96. package/docs/basics/getting-started.md +140 -0
  97. package/docs/basics/prerequisites.md +63 -0
  98. package/docs/basics/providers.md +362 -0
  99. package/docs/basics/running.md +78 -0
  100. package/docs/contributing/ai-integration-tests.md +57 -0
  101. package/docs/contributing/contributing.md +90 -0
  102. package/docs/contributing/demo-videos.md +36 -0
  103. package/docs/contributing/npm-package.md +138 -0
  104. package/docs/contributing/observability.md +227 -0
  105. package/docs/contributing/regression-tests.md +103 -0
  106. package/docs/contributing/testing.md +95 -0
  107. package/docs/doc-collection/basics.md +128 -0
  108. package/docs/doc-collection/crawling.md +67 -0
  109. package/docs/doc-collection/interactive-mode.md +99 -0
  110. package/docs/index.json +87 -0
  111. package/docs/reference/commands.md +997 -0
  112. package/docs/reference/configuration.md +569 -0
  113. package/docs/reference/scripting.md +303 -0
  114. package/docs/reference/websocket.md +50 -0
  115. package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
  116. package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
  117. package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
  118. package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
  119. package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
  120. package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
  121. package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
  122. package/docs/superpowers/specs/2026-08-18-prima-false-verdicts.md +159 -0
  123. package/docs/web-testing/agents.md +158 -0
  124. package/docs/web-testing/automated-tests.md +134 -0
  125. package/docs/web-testing/basics.md +91 -0
  126. package/docs/web-testing/customization.md +131 -0
  127. package/docs/web-testing/hooks.md +238 -0
  128. package/docs/web-testing/page-interaction.md +84 -0
  129. package/docs/web-testing/planner.md +122 -0
  130. package/docs/web-testing/rerun.md +164 -0
  131. package/docs/web-testing/researcher.md +380 -0
  132. package/docs/workflow/agentic-usage.md +233 -0
  133. package/docs/workflow/application-spec.md +73 -0
  134. package/docs/workflow/ci.md +202 -0
  135. package/docs/workflow/knowledge.md +310 -0
  136. package/docs/workflow/planning-styles.md +67 -0
  137. package/docs/workflow/reporting.md +133 -0
  138. package/docs/workflow/test-plans.md +90 -0
  139. package/package.json +3 -2
  140. package/rules/navigator/verification-actions.md +20 -0
  141. package/src/action-result.ts +11 -0
  142. package/src/action.ts +43 -3
  143. package/src/ai/captain/web-mode.ts +6 -3
  144. package/src/ai/captain.ts +3 -0
  145. package/src/ai/navigator.ts +255 -186
  146. package/src/ai/pilot.ts +104 -2
  147. package/src/ai/provider.ts +14 -24
  148. package/src/ai/rerunner.ts +2 -1
  149. package/src/ai/researcher/cache.ts +12 -2
  150. package/src/ai/researcher.ts +3 -2
  151. package/src/ai/rules.ts +17 -10
  152. package/src/ai/session-analyst.ts +2 -0
  153. package/src/ai/task-agent.ts +3 -1
  154. package/src/ai/tester.ts +52 -45
  155. package/src/ai/tools.ts +136 -37
  156. package/src/commands/config-command.ts +146 -0
  157. package/src/commands/index.ts +2 -0
  158. package/src/config.ts +60 -5
  159. package/src/execution-controller.ts +8 -0
  160. package/src/explorbot.ts +7 -3
  161. package/src/explorer.ts +2 -2
  162. package/src/playwright-recorder.ts +23 -0
  163. package/src/remote.ts +244 -0
  164. package/src/reporter.ts +7 -1
  165. package/src/state-manager.ts +6 -2
  166. package/src/stats.ts +1 -0
  167. package/src/test-plan.ts +29 -0
  168. package/src/utils/aria.ts +65 -45
  169. package/src/utils/html.ts +1 -0
  170. package/src/utils/logger.ts +33 -2
  171. package/src/utils/page-readiness.ts +24 -1
  172. package/src/utils/url-matcher.ts +3 -0
  173. package/src/utils/web-element.ts +9 -0
  174. package/src/utils/web-sandbox.ts +3 -4
@@ -3,14 +3,16 @@ import dedent from 'dedent';
3
3
  import { z } from 'zod';
4
4
  import { ActionResult } from "../action-result.js";
5
5
  import { ConfigParser } from "../config.js";
6
+ import { Stats } from "../stats.js";
6
7
  import { TestResult } from "../test-plan.js";
7
- import { collectInteractiveNodes, detectFocusArea, extractFocusedElement } from "../utils/aria.js";
8
+ import { collectInteractiveNodes, detectFocusArea } from "../utils/aria.js";
8
9
  import { ErrorPageError } from "../utils/error-page.js";
9
10
  import { createDebug, tag } from "../utils/logger.js";
10
11
  const debugLog = createDebug('explorbot:pilot');
11
12
  import { truncateJson } from "../utils/strings.js";
12
13
  import { capabilityGroundingRule, dataProtectionRules } from "./rules.js";
13
14
  import { isInteractive } from "./task-agent.js";
15
+ import { withdrawVisionTools } from "./tools.js";
14
16
  const CHECK_TOOLS = ['verify', 'see', 'research', 'context'];
15
17
  const META_TOOLS = ['record', 'reset', 'stop', 'finish'];
16
18
  export class Pilot {
@@ -484,6 +486,91 @@ export class Pilot {
484
486
  }
485
487
  return text;
486
488
  }
489
+ async settleExpectations(task, finalState) {
490
+ let image = null;
491
+ if (finalState?.screenshot && this.provider.hasVision())
492
+ image = `data:image/png;base64,${finalState.screenshot.toString('base64')}`;
493
+ const decided = (text) => {
494
+ if (task.hasAchievedAny() && !task.getRemainingExpectations().includes(text))
495
+ return 'passed';
496
+ return 'failed';
497
+ };
498
+ let undecided = task.expected.filter((text) => !task.getCheckedExpectations().includes(text));
499
+ if (image)
500
+ undecided = task.expected;
501
+ if (!undecided.length)
502
+ return task.expected.map((text) => ({ text, status: decided(text) }));
503
+ const schema = z.object({
504
+ outcomes: z.array(z.object({
505
+ expectation: z.string().describe('The expected outcome, repeated exactly as it was given'),
506
+ status: z.enum(['passed', 'failed', 'unverified', 'contradiction']).describe('passed = the evidence shows it happened, failed = the evidence shows it did not, unverified = the run never established either way, contradiction = the picture and the run disagree'),
507
+ evidence: z.string().nullable().describe('What settled it. For a contradiction, what each side shows. Null when there is nothing to add'),
508
+ })),
509
+ });
510
+ let pageEvidence = '';
511
+ if (image) {
512
+ pageEvidence = dedent `
513
+ A screenshot of the whole page as the run left it is attached. It is the proof: an outcome is satisfied
514
+ when the page shows it to somebody looking at it. The log only says what the run did.
515
+
516
+ Not finding something in the picture is not by itself a disagreement. Report "contradiction" only when
517
+ the picture shows something incompatible with what the run claims — a list visibly empty, an error where
518
+ a result was expected, the old value still displayed, a control visibly disabled. When you simply cannot
519
+ make it out, say "unverified" and name what you could not find.
520
+
521
+ The picture covers the full page, but not the inside of a region that scrolls on its own, and not the
522
+ state of the page before the run ended. An outcome established earlier stays established even when the
523
+ page has moved past it, and that is not a contradiction.
524
+ `;
525
+ }
526
+ const userContent = dedent `
527
+ A test run has finished. Decide, for each expected outcome, what the run established about it.
528
+
529
+ <expected_outcomes>
530
+ ${undecided.map((text) => `- ${text}`).join('\n')}
531
+ </expected_outcomes>
532
+
533
+ <run_log>
534
+ ${task.notesToString() || 'No steps recorded.'}
535
+ </run_log>
536
+
537
+ ${pageEvidence}
538
+
539
+ The log is written in the tester's own words, so an outcome can be satisfied by a step that describes it
540
+ differently. Judge by what the steps show happened, not by whether the wording matches.
541
+ Choose "unverified" only when the evidence neither shows the outcome happening nor shows it failing —
542
+ that is a statement about the run, not about the application.
543
+ `;
544
+ const settle = (content, model) => this.provider
545
+ .generateObject([{ role: 'user', content }], schema, model, {
546
+ agentName: 'pilot',
547
+ telemetry: { functionId: 'pilot.settleExpectations' },
548
+ })
549
+ .catch(() => null);
550
+ let response = null;
551
+ if (image) {
552
+ const seen = [
553
+ { type: 'text', text: userContent },
554
+ { type: 'file', mediaType: 'image/png', data: image },
555
+ ];
556
+ response = await settle(seen, this.provider.getVisionModel());
557
+ if (!response) {
558
+ Stats.visionDisabled = true;
559
+ tag('warning').log('⚠️ Vision model could not judge the outcomes. Settling them from the run log instead.');
560
+ }
561
+ }
562
+ if (!response)
563
+ response = await settle(userContent, this.provider.getAgenticModel('pilot'));
564
+ const judged = new Map((response?.object?.outcomes || []).map((outcome) => [outcome.expectation, outcome]));
565
+ return task.expected.map((text) => {
566
+ if (!undecided.includes(text))
567
+ return { text, status: decided(text) };
568
+ const outcome = judged.get(text);
569
+ if (!outcome)
570
+ return { text, status: 'unverified' };
571
+ return { text, status: outcome.status || 'unverified', evidence: outcome.evidence };
572
+ });
573
+ }
487
574
  formatExpectations(task) {
488
575
  const checked = task.getCheckedExpectations();
489
576
  const remaining = task.getRemainingExpectations();
@@ -547,6 +634,7 @@ export class Pilot {
547
634
  planning.learnExperience = learnExperience;
548
635
  if (askUser)
549
636
  planning.askUser = askUser;
637
+ withdrawVisionTools(planning);
550
638
  return planning;
551
639
  }
552
640
  buildPreconditionTool(task) {
@@ -624,7 +712,7 @@ export class Pilot {
624
712
  const lines = [];
625
713
  lines.push(`url: ${state.url}`);
626
714
  lines.push(`title: ${state.title || 'unknown'}`);
627
- const focused = extractFocusedElement(state.ariaSnapshot);
715
+ const focused = state.focusedElement;
628
716
  if (focused) {
629
717
  const valuePart = focused.value ? ` (value: "${focused.value}")` : '';
630
718
  lines.push(`focused: ${focused.role} "${focused.name}"${valuePart}`);
@@ -1,6 +1,6 @@
1
1
  import { NodeSDK } from '@opentelemetry/sdk-node';
2
2
  import type { ModelMessage } from 'ai';
3
- import type { AIConfig } from '../config.js';
3
+ import { type AIConfig } from '../config.js';
4
4
  import { type RetryOptions } from '../utils/retry.js';
5
5
  import { Conversation } from './conversation.js';
6
6
  declare class AiError extends Error {
@@ -14,10 +14,10 @@ export declare class Provider {
14
14
  defaultRetryOptions: RetryOptions;
15
15
  lastConversation: Conversation | null;
16
16
  constructor(config: AIConfig);
17
- getModelName(model: any): string;
18
17
  validateConnection(): Promise<void>;
19
18
  getModelForAgent(agentName?: string): any;
20
19
  getAgenticModel(agentName?: string): any;
20
+ getVisionModel(): any;
21
21
  getConfiguredModels(): Record<string, string>;
22
22
  getSystemPromptForAgent(agentName: string, currentUrl?: string): string | undefined;
23
23
  getProviderOptionsForAgent(agentName: string): Record<string, any> | undefined;
@@ -3,6 +3,7 @@ import { LangfuseSpanProcessor } from '@langfuse/otel';
3
3
  import { NodeSDK } from '@opentelemetry/sdk-node';
4
4
  import { generateObject, generateText, isStepCount, registerTelemetry } from 'ai';
5
5
  import { clearActivity, setActivity } from "../activity.js";
6
+ import { configuredModels, modelName as getModelName } from '../config.js';
6
7
  import { executionController } from "../execution-controller.js";
7
8
  import { Observability } from "../observability.js";
8
9
  import { Stats } from "../stats.js";
@@ -79,9 +80,6 @@ export class Provider {
79
80
  this.config = config;
80
81
  this.initLangfuse();
81
82
  }
82
- getModelName(model) {
83
- return model?.modelId || model?.model || 'unknown';
84
- }
85
83
  async validateConnection() {
86
84
  try {
87
85
  await generateText({
@@ -108,16 +106,13 @@ export class Provider {
108
106
  }
109
107
  return this.config.agenticModel || this.config.model;
110
108
  }
109
+ getVisionModel() {
110
+ return this.config.visionModel;
111
+ }
111
112
  getConfiguredModels() {
112
- const models = { model: this.getModelName(this.config.model) };
113
- if (this.config.agenticModel)
114
- models.agenticModel = this.getModelName(this.config.agenticModel);
115
- if (this.config.visionModel)
116
- models.visionModel = this.getModelName(this.config.visionModel);
117
- for (const [agent, agentConfig] of Object.entries(this.config.agents || {})) {
118
- if (agentConfig?.model)
119
- models[agent] = this.getModelName(agentConfig.model);
120
- }
113
+ const models = {};
114
+ for (const [role, model] of Object.entries(configuredModels(this.config)))
115
+ models[role] = model.name;
121
116
  return models;
122
117
  }
123
118
  getSystemPromptForAgent(agentName, currentUrl) {
@@ -207,11 +202,7 @@ export class Provider {
207
202
  return retry(reduced.messages, { ...options, _contextRetryLevel: reduced.nextLevel });
208
203
  }
209
204
  initLangfuse() {
210
- const langfuseConfig = this.config.langfuse;
211
- const publicKey = langfuseConfig?.publicKey || process.env.LANGFUSE_PUBLIC_KEY;
212
- const secretKey = langfuseConfig?.secretKey || process.env.LANGFUSE_SECRET_KEY;
213
- const baseUrl = langfuseConfig?.baseUrl || process.env.LANGFUSE_BASE_URL || process.env.LANGFUSE_HOST;
214
- const enabled = langfuseConfig?.enabled ?? Boolean(publicKey && secretKey);
205
+ const { enabled, publicKey, secretKey, baseUrl } = this.config.langfuse || {};
215
206
  if (!enabled || !publicKey || !secretKey) {
216
207
  return;
217
208
  }
@@ -282,7 +273,7 @@ export class Provider {
282
273
  return { conversation, response, toolExecutions };
283
274
  }
284
275
  async chat(messages, model, options = {}) {
285
- const modelName = this.getModelName(model);
276
+ const modelName = getModelName(model);
286
277
  setActivity(`🤖 Asking ${modelName}`, 'ai');
287
278
  promptLog(`Using model: ${modelName}`);
288
279
  const config = this.buildGenerateConfig({ maxOutputTokens: 16384 }, { model, abortSignal: executionController.getAbortSignal() }, options);
@@ -290,6 +281,7 @@ export class Provider {
290
281
  try {
291
282
  const response = await withRetry(async () => {
292
283
  const result = await generateText({ messages, ...config });
284
+ this.recordUsage(options.agentName || 'unknown', modelName, result.usage);
293
285
  if (!result.text) {
294
286
  debugLog(result);
295
287
  if (result.finishReason === 'length') {
@@ -304,7 +296,6 @@ export class Provider {
304
296
  }, this.getRetryOptions(options));
305
297
  clearActivity();
306
298
  responseLog(response.text);
307
- this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
308
299
  return response;
309
300
  }
310
301
  catch (error) {
@@ -324,7 +315,7 @@ export class Provider {
324
315
  }
325
316
  }
326
317
  async generateWithTools(messages, model, tools, options = {}) {
327
- const modelName = this.getModelName(model);
318
+ const modelName = getModelName(model);
328
319
  setActivity(`🤖 Asking ${modelName} with dynamic tools`, 'ai');
329
320
  promptLog(`Using model: ${modelName}`);
330
321
  const toolNames = Object.keys(tools || {});
@@ -340,6 +331,7 @@ export class Provider {
340
331
  try {
341
332
  const response = await withRetry(async () => {
342
333
  const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages, ...config, abortSignal: signal }), config.timeout || 30000));
334
+ this.recordUsage(options.agentName || 'unknown', modelName, result.usage);
343
335
  const hasToolCall = (result.toolCalls?.length || 0) > 0;
344
336
  if (!result.text && !hasToolCall && result.finishReason === 'length') {
345
337
  throw new ContextLengthError('AI response empty: output truncated at maxTokens. Increase maxOutputTokens in config or use a model with higher output capacity.');
@@ -355,7 +347,6 @@ export class Provider {
355
347
  });
356
348
  }
357
349
  responseLog(response.text);
358
- this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
359
350
  return response;
360
351
  }
361
352
  catch (error) {
@@ -379,7 +370,7 @@ export class Provider {
379
370
  }
380
371
  async generateObject(messages, schema, model, options = {}) {
381
372
  const modelToUse = model || this.config.model;
382
- const modelName = this.getModelName(modelToUse);
373
+ const modelName = getModelName(modelToUse);
383
374
  setActivity(`🤖 Asking ${modelName} for structured output`, 'ai');
384
375
  promptLog(`Using model: ${modelName}`);
385
376
  const config = this.buildGenerateConfig({ schema }, { model: modelToUse }, options);
@@ -557,7 +548,7 @@ export class Provider {
557
548
  }, this.getRetryOptions());
558
549
  clearActivity();
559
550
  responseLog(response.text);
560
- this.recordUsage('vision', this.getModelName(this.config.visionModel), response.usage);
551
+ this.recordUsage('vision', getModelName(this.config.visionModel), response.usage);
561
552
  return response;
562
553
  }
563
554
  catch (error) {
@@ -21,7 +21,7 @@ import { RulesLoader } from "../utils/rules-loader.js";
21
21
  import { toolExecutionLabel } from "./conversation.js";
22
22
  import { actionRule, locatorRule, sectionContextRule } from "./rules.js";
23
23
  import { TaskAgent } from "./task-agent.js";
24
- import { createCodeceptJSTools } from "./tools.js";
24
+ import { createCodeceptJSTools, withdrawVisionTools } from "./tools.js";
25
25
  const debugLog = createDebug('explorbot:rerunner');
26
26
  export class Rerunner extends TaskAgent {
27
27
  ACTION_TOOLS = ['click', 'pressKey', 'form'];
@@ -322,6 +322,7 @@ export class Rerunner extends TaskAgent {
322
322
  },
323
323
  }),
324
324
  };
325
+ withdrawVisionTools(tools);
325
326
  const conversation = this.provider.startConversation(this.getHealSystemPrompt(), 'rerunner');
326
327
  conversation.addUserText(this.getHealUserPrompt(failedCode, error));
327
328
  await loop(async ({ stop }) => {
@@ -1,3 +1,5 @@
1
+ export declare function researchPath(hash: string): string;
2
+ export declare function reportResearch(hash: string, text: string): void;
1
3
  export declare function clearResearchCache(): void;
2
4
  export declare function getCachedResearch(hash: string): string;
3
5
  export declare function getPreviousResearch(hash: string): string;
@@ -4,6 +4,7 @@ import { Worker } from 'node:worker_threads';
4
4
  import { outputPath } from "../../config.js";
5
5
  import { TTLCache } from "../../utils/cache.js";
6
6
  import { computeHtmlFingerprint } from "../../utils/html-diff.js";
7
+ import { tag } from "../../utils/logger.js";
7
8
  import { debugLog } from "./mixin.js";
8
9
  const CACHE_TTL_MS = 6 * 60 * 60 * 1000; // 6 hours
9
10
  const FINGERPRINT_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
@@ -11,6 +12,12 @@ const FINGERPRINT_WORKER_TIMEOUT_MS = 10_000;
11
12
  const SIMILARITY_THRESHOLD = 90;
12
13
  const memoryCache = new TTLCache(CACHE_TTL_MS);
13
14
  let fingerprintWorker = null;
15
+ export function researchPath(hash) {
16
+ return outputPath('research', `${hash}.md`);
17
+ }
18
+ export function reportResearch(hash, text) {
19
+ tag('data').log('research', { path: researchPath(hash), hash, content: text });
20
+ }
14
21
  function getStatesDir() {
15
22
  return outputPath('states');
16
23
  }
@@ -30,7 +37,7 @@ export function getCachedResearch(hash) {
30
37
  const cached = memoryCache.get(hash);
31
38
  if (cached !== undefined)
32
39
  return cached;
33
- const researchFile = outputPath('research', `${hash}.md`);
40
+ const researchFile = researchPath(hash);
34
41
  if (!existsSync(researchFile))
35
42
  return '';
36
43
  const stats = statSync(researchFile);
@@ -43,7 +50,7 @@ export function getCachedResearch(hash) {
43
50
  export function getPreviousResearch(hash) {
44
51
  if (!hash)
45
52
  return '';
46
- const researchFile = outputPath('research', `${hash}.md`);
53
+ const researchFile = researchPath(hash);
47
54
  if (!existsSync(researchFile))
48
55
  return '';
49
56
  return readFileSync(researchFile, 'utf8');
@@ -55,6 +62,7 @@ export function saveResearch(hash, text, combinedHtml) {
55
62
  mkdirSync(researchDir, { recursive: true });
56
63
  writeFileSync(researchFile, text);
57
64
  memoryCache.set(hash, text);
65
+ reportResearch(hash, text);
58
66
  debugLog(`Research saved to ${researchFile}`);
59
67
  if (combinedHtml) {
60
68
  const statesDir = getStatesDir();
@@ -13,7 +13,7 @@ import { mdq } from "../utils/markdown-query.js";
13
13
  import { RulesLoader } from "../utils/rules-loader.js";
14
14
  import { annotatePageElements } from "../utils/web-annotate.js";
15
15
  import { ContextLengthError } from './provider.js';
16
- import { findSimilarResearch, getCachedResearch, saveResearch } from "./researcher/cache.js";
16
+ import { findSimilarResearch, getCachedResearch, reportResearch, saveResearch } from "./researcher/cache.js";
17
17
  import { WithCoordinates } from "./researcher/coordinates.js";
18
18
  import { WithDeepAnalysis } from "./researcher/deep-analysis.js";
19
19
  import { detectFocusedSection, hasFocusedSection, markSectionAsFocused, pickDefaultFocusedSection } from "./researcher/focus.js";
@@ -76,7 +76,8 @@ export class Researcher extends ResearcherBase {
76
76
  const cached = getCachedResearch(stateHash);
77
77
  if (cached) {
78
78
  debugLog('Previous research result found');
79
- return `!! UI MAP IS CACHED AND MAY NOT REPRESENT CURRENT STATE; REFRESH RESEARCH IF YOU NOTICE ISSUES !!\n\n${cached}`;
79
+ reportResearch(stateHash, cached);
80
+ return cached;
80
81
  }
81
82
  }
82
83
  Stats.researches++;
@@ -2,13 +2,18 @@ import dedent from 'dedent';
2
2
  export const recommendedCodeceptCommands = ['I.click', 'I.type', 'I.fillField', 'I.see', 'I.seeElement'];
3
3
  const locatorPriorityRule = dedent `
4
4
  <locator_priority>
5
+ When the page context shows the element a ref, such as [ref=e14], there is no locator to select: click it with clickRef
6
+ and that ref. A ref names one exact element, so it never matches the wrong one and never has to be narrowed. Everything
7
+ below is for elements the context gives no ref for.
8
+
5
9
  Use the following priority when selecting locators:
6
10
 
7
11
  1. ARIA locators (first choice) - target browser's accessibility tree, most reliable
8
12
  Use JSON format: { "role": "button", "text": "Login" }
9
- Best for: buttons, links, inputs, form controls, dropdowns, checkboxes, radio buttons
13
+ Copy role and text VERBATIM from the ARIA snapshot or UI map — never guess the pair.
14
+ If the element is absent from the snapshot, do not invent one; use text or CSS instead.
10
15
 
11
- 2. Text locators (second choice) - use only when text is unique on the page
16
+ 2. Text locators (second choice) - exact visible text, use only when unique on the page
12
17
  Example: 'Login', 'Submit', 'Username'
13
18
  Skip if the same text appears multiple times on the page
14
19
 
@@ -23,10 +28,10 @@ const locatorPriorityRule = dedent `
23
28
  `;
24
29
  const contextSimplificationRule = dedent `
25
30
  <context_simplification>
26
- When container is available from UI map sections:
27
- - Text + container is simplest and PREFERRED: I.click('Save', '.modal')
28
- - ARIA + container for disambiguation: I.click({"role":"button","text":"Save"}, '.modal')
29
- - ALWAYS use context parameter unless locator is XPath or unique ID
31
+ - Add a container when the target may match several elements, or the UI map gives a verified
32
+ section container: I.click('Save', '.modal')
33
+ - Skip the container when the locator is already unique (XPath, unique ID, unique text)
34
+ - A wrong or stale container guarantees failure: always add one fallback command WITHOUT a container
30
35
  - No need for complex ARIA when container narrows scope sufficiently
31
36
  </context_simplification>
32
37
  `;
@@ -55,7 +60,7 @@ const locatorStrategyRule = dedent `
55
60
 
56
61
  <good_aria_locator_example>
57
62
  { "role": "button", "text": "Login" },
58
- { "role": "input", "text": "Name" },
63
+ { "role": "textbox", "text": "Name" },
59
64
  { "role": "link", "text": "Forgot your password?" },
60
65
  { "role": "link", "text": "Sign Up" },
61
66
  { "role": "button", "text": "Sign In" },
@@ -101,7 +106,7 @@ const locatorStrategyRule = dedent `
101
106
  </xpath_rules>
102
107
 
103
108
  <good locator example>
104
- 'div[role=input][placeholder="Name"]'
109
+ 'input[placeholder="Name"]'
105
110
  '[aria-label="Name"]'
106
111
  'form#user_form input[name="name"]'
107
112
  '#content-top #user_name'
@@ -231,7 +236,8 @@ export const unexpectedPopupRule = dedent `
231
236
  `;
232
237
  export const sectionContextRule = dedent `
233
238
  <section_context_rule>
234
- Context parameter is DEFAULT for all interactions. ALWAYS use container from UI map sections unless locator is XPath or unique ID.
239
+ Use a container from UI map sections when the target may match several elements. A container that is
240
+ wrong or stale guarantees failure, so prefer a verified one and keep a containerless fallback.
235
241
 
236
242
  1. Identify which section contains the target element
237
243
  2. Get the Context Locator from that section in the UI map
@@ -249,9 +255,10 @@ export const sectionContextRule = dedent `
249
255
  For CSS locators - prepend section context:
250
256
  - I.click('.main button.submit') // instead of I.click('button.submit')
251
257
 
252
- Only omit context when:
258
+ Omit context when the locator already resolves to one element:
253
259
  - Locator is XPath (already includes path context)
254
260
  - Locator is a unique ID (#specific-element)
261
+ - Text or ARIA that appears only once on the page
255
262
  </section_context_rule>
256
263
 
257
264
  ${unexpectedPopupRule}
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import dedent from 'dedent';
4
4
  import { outputPath } from "../config.js";
5
5
  import { Stats } from "../stats.js";
6
+ import { tag } from "../utils/logger.js";
6
7
  export class SessionAnalyst {
7
8
  emoji = '🧐';
8
9
  provider;
@@ -103,6 +104,7 @@ export class SessionAnalyst {
103
104
  if (!existsSync(dir))
104
105
  mkdirSync(dir, { recursive: true });
105
106
  writeFileSync(filePath, markdown);
107
+ tag('data').log('report', { path: filePath, content: markdown });
106
108
  return filePath;
107
109
  }
108
110
  serializeTest(test, ref) {
@@ -1,6 +1,9 @@
1
+ import { executionController } from "../execution-controller.js";
1
2
  import { HooksRunner } from "../utils/hooks-runner.js";
2
3
  export function isInteractive() {
3
- return process.env.INK_RUNNING === 'true';
4
+ if (process.env.INK_RUNNING === 'true')
5
+ return true;
6
+ return executionController.hasInputCallback();
4
7
  }
5
8
  function createNullProxy() {
6
9
  return new Proxy({}, {
@@ -31,7 +31,6 @@ export declare class Tester extends TaskAgent implements Agent {
31
31
  seenUiMapUrls: Set<string>;
32
32
  lastAnalyzedStateHash: string | null;
33
33
  stalledIterations: number;
34
- hasSuccessfulAssertion: boolean;
35
34
  readonly MAX_STALLED_ITERATIONS = 3;
36
35
  constructor(deps: AgentDeps, researcher: Researcher, navigator: Navigator, agentTools?: any);
37
36
  getNavigator(): Navigator;
@@ -40,16 +39,17 @@ export declare class Tester extends TaskAgent implements Agent {
40
39
  getCurrentState(): ActionResult;
41
40
  get progressCheckInterval(): number;
42
41
  getConversation(): Conversation | null;
43
- test(task: Test): Promise<{
42
+ test(task: Test, opts?: TestOptions): Promise<{
44
43
  success: boolean;
45
44
  }>;
46
- runTestSession(task: Test, initialState: ActionResult, conversation: Conversation, handlers: TestSessionHandlers): Promise<{
45
+ runTestSession(task: Test, initialState: ActionResult, conversation: Conversation, handlers: TestSessionHandlers, opts: TestOptions): Promise<{
47
46
  success: boolean;
48
47
  }>;
49
48
  shouldAnalyzeProgress(iteration: number, currentState: ActionResult): boolean;
50
49
  shouldStopForStalledExecution(task: Test, previousState: ActionResult, toolExecutions: any[]): boolean;
51
50
  prepareInstructionsForNextStep(task: Test): Promise<string>;
52
51
  reinjectContextIfNeeded(iteration: number, currentState: ActionResult): Promise<string>;
52
+ interactiveAriaWithRefs(state: ActionResult): Promise<string>;
53
53
  finishTest(task: Test): void;
54
54
  abortStartedTestOnErrorPage(task: Test, actionResult: ActionResult): Promise<{
55
55
  success: boolean;
@@ -93,4 +93,7 @@ export declare class Tester extends TaskAgent implements Agent {
93
93
  interface TestSessionHandlers {
94
94
  offFailedRequest?: () => void;
95
95
  }
96
+ export interface TestOptions {
97
+ startOnCurrentPage?: boolean;
98
+ }
96
99
  export {};