explorbot 0.1.32-beta.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/README.md +14 -14
  2. package/bin/explorbot-cli.ts +4 -3
  3. package/boat/doc-collector/src/ai/documentarian.ts +22 -27
  4. package/boat/doc-collector/src/ai/tools.ts +67 -10
  5. package/boat/doc-collector/src/docbot.ts +28 -7
  6. package/boat/doc-collector/src/docs-renderer.ts +19 -24
  7. package/boat/doc-collector/src/screenshots.ts +33 -1
  8. package/boat/doc-collector/src/state-diagram.ts +281 -0
  9. package/dist/bin/explorbot-cli.js +3 -3
  10. package/dist/boat/doc-collector/src/ai/documentarian.js +16 -6
  11. package/dist/boat/doc-collector/src/ai/tools.js +47 -10
  12. package/dist/boat/doc-collector/src/docbot.js +25 -6
  13. package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
  14. package/dist/boat/doc-collector/src/screenshots.js +29 -0
  15. package/dist/boat/doc-collector/src/state-diagram.js +200 -0
  16. package/dist/package.json +20 -3
  17. package/dist/src/action-result.d.ts +8 -10
  18. package/dist/src/action-result.js +72 -164
  19. package/dist/src/action.d.ts +3 -10
  20. package/dist/src/action.js +18 -111
  21. package/dist/src/ai/captain/web-mode.js +0 -7
  22. package/dist/src/ai/captain.d.ts +1 -3
  23. package/dist/src/ai/captain.js +4 -19
  24. package/dist/src/ai/conversation.d.ts +2 -5
  25. package/dist/src/ai/conversation.js +9 -28
  26. package/dist/src/ai/driller.d.ts +0 -4
  27. package/dist/src/ai/driller.js +3 -12
  28. package/dist/src/ai/experience-compactor.d.ts +2 -2
  29. package/dist/src/ai/experience-compactor.js +16 -33
  30. package/dist/src/ai/fisherman-tools.js +1 -1
  31. package/dist/src/ai/fisherman.d.ts +0 -1
  32. package/dist/src/ai/fisherman.js +0 -3
  33. package/dist/src/ai/historian/codeceptjs.d.ts +2 -0
  34. package/dist/src/ai/historian/codeceptjs.js +3 -2
  35. package/dist/src/ai/historian/playwright.d.ts +2 -0
  36. package/dist/src/ai/historian/playwright.js +3 -2
  37. package/dist/src/ai/historian/utils.d.ts +0 -1
  38. package/dist/src/ai/historian/utils.js +0 -1
  39. package/dist/src/ai/historian.d.ts +5 -4
  40. package/dist/src/ai/historian.js +1 -5
  41. package/dist/src/ai/navigator.d.ts +3 -5
  42. package/dist/src/ai/navigator.js +11 -46
  43. package/dist/src/ai/pilot.d.ts +1 -3
  44. package/dist/src/ai/pilot.js +6 -17
  45. package/dist/src/ai/planner.d.ts +3 -3
  46. package/dist/src/ai/planner.js +49 -37
  47. package/dist/src/ai/provider.d.ts +4 -1
  48. package/dist/src/ai/provider.js +77 -136
  49. package/dist/src/ai/quartermaster.d.ts +21 -22
  50. package/dist/src/ai/quartermaster.js +4 -10
  51. package/dist/src/ai/rerunner.d.ts +1 -2
  52. package/dist/src/ai/rerunner.js +14 -22
  53. package/dist/src/ai/researcher/cache.js +11 -18
  54. package/dist/src/ai/researcher/deep-analysis.js +3 -6
  55. package/dist/src/ai/researcher/parser.js +1 -1
  56. package/dist/src/ai/researcher/research-result.d.ts +0 -1
  57. package/dist/src/ai/researcher/research-result.js +0 -19
  58. package/dist/src/ai/researcher/sections.js +5 -1
  59. package/dist/src/ai/researcher.js +12 -53
  60. package/dist/src/ai/rules.d.ts +1 -0
  61. package/dist/src/ai/rules.js +22 -15
  62. package/dist/src/ai/session-analyst.js +8 -5
  63. package/dist/src/ai/task-agent.d.ts +1 -1
  64. package/dist/src/ai/task-agent.js +2 -27
  65. package/dist/src/ai/tester.d.ts +0 -2
  66. package/dist/src/ai/tester.js +10 -61
  67. package/dist/src/ai/tools.d.ts +18 -7
  68. package/dist/src/ai/tools.js +119 -101
  69. package/dist/src/commands/compact-command.d.ts +1 -1
  70. package/dist/src/commands/compact-command.js +6 -6
  71. package/dist/src/commands/context-command.js +1 -1
  72. package/dist/src/commands/context-knowledge-command.js +1 -1
  73. package/dist/src/commands/experience-command.js +1 -1
  74. package/dist/src/commands/knows-command.js +1 -1
  75. package/dist/src/commands/learn-command.js +3 -1
  76. package/dist/src/components/AddKnowledge.d.ts +2 -0
  77. package/dist/src/components/AddKnowledge.js +3 -7
  78. package/dist/src/components/App.js +2 -2
  79. package/dist/src/config.d.ts +2 -0
  80. package/dist/src/config.js +12 -0
  81. package/dist/src/experience-tracker.d.ts +11 -6
  82. package/dist/src/experience-tracker.js +81 -154
  83. package/dist/src/explorbot.d.ts +4 -5
  84. package/dist/src/explorbot.js +17 -51
  85. package/dist/src/explorer.d.ts +4 -15
  86. package/dist/src/explorer.js +18 -68
  87. package/dist/src/knowledge-tracker.d.ts +2 -2
  88. package/dist/src/knowledge-tracker.js +38 -45
  89. package/dist/src/state-manager.d.ts +6 -31
  90. package/dist/src/state-manager.js +10 -129
  91. package/dist/src/utils/aria.d.ts +6 -1
  92. package/dist/src/utils/aria.js +4 -1
  93. package/dist/src/utils/cache.d.ts +15 -0
  94. package/dist/src/utils/cache.js +34 -0
  95. package/dist/src/utils/context-formatter.d.ts +6 -0
  96. package/dist/src/utils/context-formatter.js +15 -27
  97. package/dist/src/utils/html.d.ts +5 -0
  98. package/dist/src/utils/html.js +71 -0
  99. package/dist/src/utils/markdown-files.d.ts +10 -0
  100. package/dist/src/utils/markdown-files.js +21 -0
  101. package/dist/src/utils/markdown-query.d.ts +6 -0
  102. package/dist/src/utils/markdown-query.js +14 -1
  103. package/dist/src/utils/page-readiness.d.ts +1 -0
  104. package/dist/src/utils/page-readiness.js +1 -1
  105. package/dist/src/utils/secrets.js +2 -2
  106. package/dist/src/utils/strings.d.ts +2 -0
  107. package/dist/src/utils/strings.js +10 -0
  108. package/dist/src/utils/web-element.d.ts +2 -1
  109. package/dist/src/utils/web-element.js +3 -0
  110. package/dist/src/utils/web-sandbox.d.ts +4 -0
  111. package/dist/src/utils/web-sandbox.js +37 -0
  112. package/package.json +20 -3
  113. package/src/action-result.ts +70 -173
  114. package/src/action.ts +16 -130
  115. package/src/ai/captain/web-mode.ts +0 -7
  116. package/src/ai/captain.ts +5 -17
  117. package/src/ai/conversation.ts +9 -32
  118. package/src/ai/driller.ts +3 -16
  119. package/src/ai/experience-compactor.ts +16 -35
  120. package/src/ai/fisherman-tools.ts +1 -1
  121. package/src/ai/fisherman.ts +0 -4
  122. package/src/ai/historian/codeceptjs.ts +4 -2
  123. package/src/ai/historian/playwright.ts +4 -2
  124. package/src/ai/historian/utils.ts +0 -1
  125. package/src/ai/historian.ts +4 -7
  126. package/src/ai/navigator.ts +13 -49
  127. package/src/ai/pilot.ts +6 -16
  128. package/src/ai/planner.ts +56 -44
  129. package/src/ai/provider.ts +85 -145
  130. package/src/ai/quartermaster.ts +34 -41
  131. package/src/ai/rerunner.ts +17 -21
  132. package/src/ai/researcher/cache.ts +10 -16
  133. package/src/ai/researcher/deep-analysis.ts +3 -6
  134. package/src/ai/researcher/parser.ts +1 -1
  135. package/src/ai/researcher/research-result.ts +0 -18
  136. package/src/ai/researcher/sections.ts +5 -1
  137. package/src/ai/researcher.ts +11 -59
  138. package/src/ai/rules.ts +24 -13
  139. package/src/ai/session-analyst.ts +8 -5
  140. package/src/ai/task-agent.ts +3 -30
  141. package/src/ai/tester.ts +12 -73
  142. package/src/ai/tools.ts +119 -106
  143. package/src/commands/compact-command.ts +7 -8
  144. package/src/commands/context-command.ts +1 -1
  145. package/src/commands/context-knowledge-command.ts +1 -1
  146. package/src/commands/experience-command.ts +1 -1
  147. package/src/commands/knows-command.ts +1 -1
  148. package/src/commands/learn-command.ts +3 -1
  149. package/src/components/AddKnowledge.tsx +5 -7
  150. package/src/components/App.tsx +2 -2
  151. package/src/config.ts +12 -0
  152. package/src/experience-tracker.ts +88 -151
  153. package/src/explorbot.ts +17 -51
  154. package/src/explorer.ts +20 -72
  155. package/src/knowledge-tracker.ts +42 -49
  156. package/src/state-manager.ts +15 -156
  157. package/src/utils/aria.ts +11 -2
  158. package/src/utils/cache.ts +40 -0
  159. package/src/utils/context-formatter.ts +12 -15
  160. package/src/utils/html.ts +79 -0
  161. package/src/utils/markdown-files.ts +30 -0
  162. package/src/utils/markdown-query.ts +15 -1
  163. package/src/utils/page-readiness.ts +1 -1
  164. package/src/utils/secrets.ts +2 -3
  165. package/src/utils/strings.ts +12 -0
  166. package/src/utils/web-element.ts +4 -0
  167. package/src/utils/web-sandbox.ts +43 -0
  168. package/dist/boat/api-tester/example/apibot.config.js +0 -30
@@ -74,7 +74,6 @@ export function WithDeepAnalysis(Base) {
74
74
  return null;
75
75
  }
76
76
  const diff = await current.diff(previous);
77
- await diff.calculate();
78
77
  if (!diff.ariaChanged && diff.htmlParts.length === 0) {
79
78
  debugLog(`No diff between current and previous state for overlay "${focusArea.name}"`);
80
79
  return null;
@@ -312,12 +311,11 @@ export function WithDeepAnalysis(Base) {
312
311
  const isCoordinateClick = el.commands[0].startsWith('I.clickXY(');
313
312
  if (!isCoordinateClick) {
314
313
  const hoverCmd = el.commands[0].replace('I.click(', 'I.moveCursorTo(');
315
- await this.explorer.attemptAction(hoverCmd, undefined, false);
314
+ await this.explorer.attemptAction(hoverCmd, undefined);
316
315
  await new Promise((r) => setTimeout(r, 500));
317
316
  await this.explorer.capturePageState();
318
317
  const hoverAR = ActionResult.fromState(this.stateManager.getCurrentState());
319
318
  const hoverDiff = await hoverAR.diff(previousState);
320
- await hoverDiff.calculate();
321
319
  const hoverHtmlSize = hoverDiff.htmlParts.reduce((sum, p) => sum + p.subtree.length, 0);
322
320
  const hoverRevealed = hoverDiff.ariaChanged && hoverHtmlSize > 500;
323
321
  if (hoverRevealed) {
@@ -355,7 +353,7 @@ export function WithDeepAnalysis(Base) {
355
353
  let clickCode = null;
356
354
  const action = this.explorer.createAction();
357
355
  for (const cmd of commands) {
358
- if (await action.attempt(cmd, undefined, false)) {
356
+ if (await action.attempt(cmd, undefined)) {
359
357
  clickCode = cmd;
360
358
  break;
361
359
  }
@@ -370,7 +368,6 @@ export function WithDeepAnalysis(Base) {
370
368
  await this.explorer.createAction().capturePageState();
371
369
  const currAR = ActionResult.fromState(this.stateManager.getCurrentState());
372
370
  diff = await currAR.diff(previousState);
373
- await diff.calculate();
374
371
  }
375
372
  catch (err) {
376
373
  tag('warning').log(`State capture failed after click: ${err instanceof Error ? err.message : err}`);
@@ -399,7 +396,7 @@ export function WithDeepAnalysis(Base) {
399
396
  await this.cancelInUi();
400
397
  await this.explorer.capturePageState();
401
398
  const currentAria = this.stateManager.getCurrentState()?.ariaSnapshot || '';
402
- if (!diffAriaSnapshots(originalAria, currentAria))
399
+ if (!diffAriaSnapshots(originalAria, currentAria).text)
403
400
  return;
404
401
  }
405
402
  catch (err) {
@@ -69,7 +69,7 @@ export function extractContainerFromBlockquote(sectionMarkdown) {
69
69
  return css;
70
70
  }
71
71
  export function parseResearchSections(markdown) {
72
- const hasExtendedResearch = markdown.includes('\n# Extended Research') || markdown.startsWith('# Extended Research');
72
+ const hasExtendedResearch = mdq(markdown).query('section1(~"Extended Research")').count() > 0;
73
73
  return parseSections(markdown)
74
74
  .filter((s) => !SKIP_SECTIONS.has(s.name.toLowerCase()) && !s.name.toLowerCase().includes('data:'))
75
75
  .map((section) => {
@@ -9,7 +9,6 @@ export declare class ResearchResult {
9
9
  parseLocators(): void;
10
10
  get containers(): string[];
11
11
  get containerLocators(): Locator[];
12
- updateSection(sectionName: string, locators: Locator[]): void;
13
12
  rebuildSectionInText(section: ResearchSection): void;
14
13
  cleanup(): void;
15
14
  reorderColumns(columns: string[]): string[];
@@ -42,25 +42,6 @@ export class ResearchResult {
42
42
  pwLocator: null,
43
43
  }));
44
44
  }
45
- updateSection(sectionName, locators) {
46
- const sections = parseResearchSections(this.text);
47
- const section = sections.find((s) => s.name === sectionName);
48
- if (!section)
49
- return;
50
- for (const el of section.elements) {
51
- const elLocators = locators.filter((l) => l.element === el.name);
52
- for (const loc of elLocators) {
53
- const value = loc.valid === false ? null : loc.locator || null;
54
- if (loc.type === 'css')
55
- el.css = value;
56
- if (loc.type === 'xpath')
57
- el.xpath = value;
58
- if (loc.type === 'aria')
59
- el.aria = value ? parseAriaLocator(value) : null;
60
- }
61
- }
62
- this.rebuildSectionInText(section);
63
- }
64
45
  rebuildSectionInText(section) {
65
46
  if (section.elements.length === 0)
66
47
  return;
@@ -89,9 +89,13 @@ export function WithSections(Base) {
89
89
  </section_format>
90
90
 
91
91
  <rules>
92
- - Every element with eidx MUST appear in the table.
92
+ - List only elements physically inside this section's declared container.
93
+ - Do not copy global toolbar, navigation, list, or detail elements into this section unless they are descendants of this section container.
94
+ - Every element with eidx inside this section's container MUST appear in the table.
93
95
  - Every row needs CSS; ARIA may be "-" for icon-only buttons.
94
96
  - ARIA locator JSON uses keys "role" and "text" (NOT "name").
97
+ - Elements marked data-explorbot-hit="covered" or "offscreen" are not directly actionable; describe the covering or focused UI first.
98
+ - In split-pane pages, entity detail panels are active detail context; include close/back/pin controls in the detail panel section when present.
95
99
  </rules>
96
100
 
97
101
  ${generalLocatorRuleText}
@@ -9,7 +9,7 @@ import { diffAriaSnapshots } from "../utils/aria.js";
9
9
  import { ErrorPageError, detectPageCondition } from "../utils/error-page.js";
10
10
  import { HooksRunner } from "../utils/hooks-runner.js";
11
11
  import { isBodyEmpty } from "../utils/html.js";
12
- import { createDebug, pluralize, tag } from '../utils/logger.js';
12
+ import { createDebug, tag } from '../utils/logger.js';
13
13
  import { mdq } from "../utils/markdown-query.js";
14
14
  import { RulesLoader } from "../utils/rules-loader.js";
15
15
  import { ContextLengthError } from './provider.js';
@@ -261,12 +261,10 @@ export class Researcher extends ResearcherBase {
261
261
  if (stateHash) {
262
262
  researchFile = saveResearch(stateHash, result.text, combinedHtml);
263
263
  }
264
- const summaryMatch = result.text.match(/## Summary\s*\n+([\s\S]*?)(?=\n##|$)/i);
265
- if (summaryMatch) {
266
- const summaryLine = summaryMatch[1].trim().split('\n')[0].trim().slice(0, 200);
267
- if (summaryLine)
268
- this.experienceTracker.updateSummary(this.actionResult, summaryLine);
269
- }
264
+ const summaryText = mdq(result.text).query('section2(/^summary/)').query('paragraph[0]').text().trim();
265
+ const summaryLine = summaryText.split('\n')[0]?.trim().slice(0, 200);
266
+ if (summaryLine)
267
+ this.experienceTracker.updateSummary(this.actionResult, summaryLine);
270
268
  tag('multiline').log(formatResearchSummary(result.text, { visionUsed: this.hasScreenshotToAnalyze }));
271
269
  tag('success').log('Research complete');
272
270
  if (researchFile)
@@ -365,7 +363,10 @@ export class Researcher extends ResearcherBase {
365
363
  - Research all menus and navigational areas.
366
364
  - Ignore decorative sidebars, footer-only links, and external links.
367
365
  - Detect layout patterns: list/detail split, 2-pane, or 3-pane layouts.
368
- - Every element with an eidx attribute MUST appear in the UI map describe icon-only buttons by their visual role.
366
+ - In split-pane layouts, keep global toolbar, list pane, and detail pane as separate sections. Do not copy toolbar/list elements into the detail section unless they are physically inside that detail container.
367
+ - If the URL or page state opens an entity detail panel, describe that panel as the active detail context and include its close/back/pin controls when present.
368
+ - If an element has data-explorbot-hit="covered" or "offscreen", do not present it as directly actionable. Prefer the overlay, drawer, dialog, or focused section covering it, and mention what must be dismissed or revealed first.
369
+ - Every element with an eidx attribute MUST appear in exactly one matching UI map section — describe icon-only buttons by their visual role.
369
370
  - Every UI map row needs a CSS selector; ARIA may be "-" for icon-only buttons, CSS must never be "-".
370
371
  - ARIA locator JSON uses keys "role" and "text" (NOT "name").
371
372
  - Mark elements with likely hover interactions (title, aria-describedby, menu items with submenus) as "(hover)".
@@ -382,6 +383,7 @@ export class Researcher extends ResearcherBase {
382
383
  .join('\n')}
383
384
 
384
385
  - Sections can overlap; prefer more detailed sections over broader ones.
386
+ - Section tables must list only elements physically inside the declared container. If a control belongs to a global toolbar, list it only in the toolbar section.
385
387
  - Never name a section "Focus" or "Focused" — use what it contains (Detail, Modal, Form, Content, List).
386
388
  - Omit sections that are not present or not relevant.
387
389
  - Each section needs a container CSS locator; UI map CSS locators are relative to it.
@@ -402,21 +404,7 @@ export class Researcher extends ResearcherBase {
402
404
  if (!this.actionResult)
403
405
  throw new Error('actionResult is not set');
404
406
  const html = await this.actionResult.combinedHtml();
405
- const knowledgeFiles = this.stateManager.getRelevantKnowledge();
406
- let knowledge = '';
407
- if (knowledgeFiles.length > 0) {
408
- const knowledgeContent = knowledgeFiles
409
- .map((k) => k.content)
410
- .filter((k) => !!k)
411
- .join('\n\n');
412
- tag('operation').log(`Found ${knowledgeFiles.length} relevant knowledge ${pluralize(knowledgeFiles.length, 'file')} for: ${this.actionResult.url}`);
413
- knowledge = `
414
- <hint>
415
- Here is relevant knowledge for this page:
416
-
417
- ${knowledgeContent}
418
- </hint>`;
419
- }
407
+ const knowledge = this.explorer.getKnowledgeTracker().renderRelevantKnowledge(this.actionResult);
420
408
  const ariaSnapshot = this.actionResult.getCompactARIA();
421
409
  return dedent `
422
410
  Analyze this web page and provide a comprehensive research report in markdown format.
@@ -471,35 +459,6 @@ export class Researcher extends ResearcherBase {
471
459
 
472
460
  `;
473
461
  }
474
- async textContent(state) {
475
- const actionResult = ActionResult.fromState(state);
476
- const html = await actionResult.combinedHtml();
477
- const prompt = dedent `
478
- Transform into markdown.
479
- Identify headers, footers, asides, special application parts and main contant.
480
- Content should be in markdown format. If it is content: tables must be tables, lists must be lists.
481
- Navigation elements should be represented as standalone blocks after the content.
482
- Do not summarize content, just transform it into markdown.
483
- It is important to list all the content text
484
- If it is link it must be linked
485
- You can summarize footers/navigation/aside elements.
486
- But main conteint should be kept as text and formatted as markdown based on its current markup.
487
- Links to external web sites should be avoided in output.
488
-
489
- Break down into sections:
490
-
491
- ## Content Area
492
-
493
- ## Navigation Area
494
-
495
- <page_html>
496
- ${html}
497
- </page_html>
498
- `;
499
- const model = this.provider.getModelForAgent('researcher');
500
- const r = await this.provider.chat([{ role: 'user', content: prompt }], model, { agentName: 'researcher', telemetryFunctionId: 'researcher.textContent' });
501
- return r.text;
502
- }
503
462
  getScreenshotFromState(state) {
504
463
  const actionResult = ActionResult.fromState(state);
505
464
  const image = actionResult.screenshot;
@@ -693,7 +652,7 @@ export class Researcher extends ResearcherBase {
693
652
  async cancelInUi() {
694
653
  const beforeAria = this.stateManager.getCurrentState()?.ariaSnapshot || null;
695
654
  await this.explorer.executeAction('I.clickXY(0, 0)');
696
- if (diffAriaSnapshots(beforeAria, this.stateManager.getCurrentState()?.ariaSnapshot || null))
655
+ if (diffAriaSnapshots(beforeAria, this.stateManager.getCurrentState()?.ariaSnapshot || null).text)
697
656
  return;
698
657
  await this.explorer.executeAction(`I.pressKey('Escape')`);
699
658
  }
@@ -1,5 +1,6 @@
1
1
  export declare const recommendedCodeceptCommands: readonly ["I.click", "I.type", "I.fillField", "I.see", "I.seeElement"];
2
2
  export declare const locatorRule: string;
3
+ export declare const drillLocatorRule: string;
3
4
  export declare const fileUploadRule: string;
4
5
  export declare const formRequirementsRule: string;
5
6
  export declare const protectionRule: string;
@@ -1,6 +1,6 @@
1
1
  import dedent from 'dedent';
2
2
  export const recommendedCodeceptCommands = ['I.click', 'I.type', 'I.fillField', 'I.see', 'I.seeElement'];
3
- export const locatorRule = dedent `
3
+ const locatorPriorityRule = dedent `
4
4
  <locator_priority>
5
5
  Use the following priority when selecting locators:
6
6
 
@@ -17,10 +17,11 @@ export const locatorRule = dedent `
17
17
  Example: '#login-btn', '[data-testid="submit"]', 'form#login input[name="email"]'
18
18
 
19
19
  4. XPath (last resort) - for complex hierarchy or when CSS can't express the path
20
- Always start with //, never use positional indices like [1], [2]
20
+ Always start with //. Avoid positional indices like [1], [2] except as a last-resort disambiguator
21
21
  Example: '//form[@id="login"]//input[@name="email"]'
22
22
  </locator_priority>
23
-
23
+ `;
24
+ const contextSimplificationRule = dedent `
24
25
  <context_simplification>
25
26
  When container is available from UI map sections:
26
27
  - Text + container is simplest and PREFERRED: I.click('Save', '.modal')
@@ -28,7 +29,8 @@ export const locatorRule = dedent `
28
29
  - ALWAYS use context parameter unless locator is XPath or unique ID
29
30
  - No need for complex ARIA when container narrows scope sufficiently
30
31
  </context_simplification>
31
-
32
+ `;
33
+ const locatorStrategyRule = dedent `
32
34
  <disambiguation>
33
35
  When multiple elements could match the request, select based on intent:
34
36
  1. Match the context of recent actions - if filling a form, use elements in that same form
@@ -46,8 +48,9 @@ export const locatorRule = dedent `
46
48
  - Use aria-label value if present: { "role": "button", "text": "Close" } (from aria-label="Close")
47
49
  - Use title attribute if present: { "role": "button", "text": "Settings" } (from title="Settings")
48
50
  - If no accessible name exists, mark ARIA as "-" and use CSS/XPath:
49
- * CSS: use partial href a[href*="settings"] or SVG icon class a:has(svg.md-icon-cog)
50
- * XPath: use contains(@href,"settings") or SVG class //a[.//svg[contains(@class,"md-icon-cog")]]
51
+ * CSS: use partial href a[href*="settings"] or SVG icon class a:has(svg.icon-settings)
52
+ * XPath: use contains(@href,"settings") or SVG class //a[.//svg[contains(@class,"icon-settings")]]
53
+ - In inline create/edit rows, confirmation can be an icon-only control near the edited field instead of a text Save button. Anchor the locator to the same row/form as the field and target the adjacent confirm icon/control.
51
54
  - NEVER use empty text: { "role": "button", "text": "" } is INVALID and useless
52
55
 
53
56
  <good_aria_locator_example>
@@ -78,9 +81,9 @@ export const locatorRule = dedent `
78
81
  - Vue: data-v-* attributes
79
82
  Avoid locators that seem to have generated ids or class names (long random numbers, uuids, hashes, etc)
80
83
  Prefer text or ARIA locators over href-based ones. But for icon-only links with no accessible name, use:
81
- - Partial href match: a[href*="settings"], a[href*="requirements"] (use path segments, not full URLs)
82
- - SVG icon class: a:has(svg.md-icon-cog), button:has(svg.md-icon-plus) (target the SVG class inside the link/button)
83
- Avoid full absolute href like a[href="/projects/imr_manual12/settings"] — use generic path segments instead
84
+ - Partial href match: a[href*="settings"], a[href*="reports"] (use path segments, not full URLs)
85
+ - SVG icon class: a:has(svg.icon-settings), button:has(svg.icon-add) (target the SVG class inside the link/button)
86
+ Avoid full absolute href like a[href="/items/12345/settings"] — use generic path segments instead
84
87
  Avoid CSS framework utility classes as containers (Tailwind: flex, grid, space-x-*, justify-*, items-*, w-*, h-*, p-*, m-*, etc; Bootstrap: col-*, row, d-flex, etc)
85
88
  Prefer semantic class names, roles, data attributes, or element hierarchy for containers
86
89
 
@@ -92,7 +95,7 @@ export const locatorRule = dedent `
92
95
 
93
96
  <xpath_rules>
94
97
  XPath locators must start with //.
95
- XPath should use positional indices [1], [2], [3] and contains(., "text") for disambiguation.
98
+ XPath may use positional indices [1], [2] and contains(., "text") as a last-resort disambiguator when attribute/text strategies are exhausted.
96
99
  XPath should rely less on class names — prefer element hierarchy, position, and text content.
97
100
  XPath and CSS MUST provide different strategies for finding the same element.
98
101
  </xpath_rules>
@@ -104,21 +107,23 @@ export const locatorRule = dedent `
104
107
  '#content-top #user_name'
105
108
  '#content-top form input[name="name"]'
106
109
  'a.nav-item[href*="settings"]' // icon-only link matched by partial href
107
- 'a.nav-item:has(svg.md-icon-cog)' // icon-only link matched by SVG icon class
110
+ 'a.nav-item:has(svg.icon-settings)' // icon-only link matched by SVG icon class
108
111
  '//nav//a[contains(@href,"settings")]' // XPath for icon-only nav link
109
112
  </good locator example>
110
113
 
111
114
  <bad locator example>
112
- 'a.filter-tab:nth-of-type(1)' // WRONG: positional in CSS, use :has-text("Manual") instead
113
- '//a[contains(@class,"filter-tab") and contains(@class,"active")]' // WRONG: XPath repeats CSS approach, use positional //a[contains(@class,"filter-tab")][1]
115
+ 'a.filter-tab:nth-of-type(1)' // WRONG: positional in CSS, use :has-text("Active") instead
116
+ '//a[contains(@class,"filter-tab") and contains(@class,"active")]' // WRONG: XPath repeats CSS approach, provide a different strategy e.g. //a[contains(@class,"filter-tab")][1]
114
117
  '//table//tbody/tr[1]//button[contains(@onclick,'fn()')]' // onclick is not semantic attribute
115
118
  '//html/body/vue-button-123' // vue-framework specific locator
116
- 'link "New Template"' // WRONG: malformed string, use {"role":"link","text":"New Template"}
117
- 'a[href="/projects/imr_manual12/settings"]' // WRONG: full absolute href, use a[href*="settings"] instead
119
+ 'link "New Item"' // WRONG: malformed string, use {"role":"link","text":"New Item"}
120
+ 'a[href="/items/12345/settings"]' // WRONG: full absolute href, use a[href*="settings"] instead
118
121
  </bad locator example>
119
122
 
120
123
  HTML locators must be valid JS strings
121
124
  `;
125
+ export const locatorRule = [locatorPriorityRule, contextSimplificationRule, locatorStrategyRule].join('\n\n');
126
+ export const drillLocatorRule = [locatorPriorityRule, locatorStrategyRule].join('\n\n');
122
127
  export const fileUploadRule = dedent `
123
128
  <file_upload>
124
129
  Explorbot CAN upload files using I.attachFile() via form() tool.
@@ -294,6 +299,7 @@ export const actionRule = dedent `
294
299
  Use context parameter (second argument) to narrow click area when:
295
300
  - The same text/button appears multiple times on page
296
301
  - You need to click inside a specific form, modal, or section
302
+ - You need an icon-only confirm/save control next to a field in an inline create/edit row
297
303
  Context should be a CSS selector pointing to a unique container.
298
304
 
299
305
  <example>
@@ -306,6 +312,7 @@ export const actionRule = dedent `
306
312
  </example>
307
313
 
308
314
  Prefer text/ARIA locators with context over complex CSS/XPath selectors.
315
+ For inline create/edit flows, after filling a field verify it contains the value, then confirm using the nearest explicit button/link, an adjacent icon-only confirm control in the same row/form, or Enter if the field remains focused.
309
316
  If locator doesn't work, try CSS or XPath locators.
310
317
  If nothing works, use I.clickXY(x, y) as last resort.
311
318
 
@@ -42,6 +42,9 @@ export class SessionAnalyst {
42
42
  ## Severity (defects only)
43
43
  [High] blocks a core flow · [Medium] degrades a flow but workaround exists · [Low] cosmetic / edge case
44
44
 
45
+ ## Naming tests
46
+ Reference every test by its full label wrapped in backticks: \`\`\`ET-<number> <test title>\`\`\` — the \`ref\` and \`scenario\` from its \`<test>\` block. Backticks only, never bold. Never write a bare \`#2\`.
47
+
45
48
  ## Format
46
49
 
47
50
  # Session Analysis
@@ -53,22 +56,22 @@ export class SessionAnalyst {
53
56
  - Features: <capabilities>
54
57
 
55
58
  ## What works
56
- - **<feature>** — #2, #7, #8
59
+ - **<feature>** — \`ET-2 <title>\`, \`ET-7 <title>\`
57
60
 
58
61
  ## Defects
59
62
 
60
63
  ### [Medium] <plain-English bug title>
61
- Affects: #3, #5
64
+ Affects: \`ET-3 <title>\`, \`ET-5 <title>\`
62
65
  Reproduce:
63
66
  1. <concrete UI step>
64
67
  2. <next>
65
68
  Evidence: <one short observation>
66
69
 
67
70
  ## UX issues
68
- - **<feature>** — <what's confusing> (#7)
71
+ - **<feature>** — <what's confusing> (\`ET-7 <title>\`)
69
72
 
70
73
  ## Execution Issues
71
- - **#2 <scenario>** — <≤10 words, what was unreliable>
74
+ - \`ET-2 <title>\` — <≤10 words, what was unreliable>
72
75
 
73
76
  ## Brevity rules
74
77
 
@@ -127,7 +130,7 @@ export class SessionAnalyst {
127
130
  `
128
131
  : 'verification_status: none';
129
132
  return dedent `
130
- <test ref="#${ref}">
133
+ <test ref="ET-${ref}">
131
134
  url: ${test.startUrl || '/'}
132
135
  scenario: ${test.scenario}
133
136
  result: ${test.result || 'unknown'}
@@ -1,5 +1,5 @@
1
1
  import type { ActionResult } from '../action-result.js';
2
- import { type ExperienceTracker } from '../experience-tracker.js';
2
+ import type { ExperienceTracker } from '../experience-tracker.js';
3
3
  import type { KnowledgeTracker } from '../knowledge-tracker.js';
4
4
  import { Historian } from './historian.js';
5
5
  import type { Navigator } from './navigator.js';
@@ -1,7 +1,3 @@
1
- import dedent from 'dedent';
2
- import { renderExperienceToc } from '../experience-tracker.js';
3
- import { createDebug, pluralize, tag } from '../utils/logger.js';
4
- const debugLog = createDebug('explorbot:task-agent');
5
1
  export function isInteractive() {
6
2
  return process.env.INK_RUNNING === 'true';
7
3
  }
@@ -18,31 +14,10 @@ export class TaskAgent {
18
14
  _historian = null;
19
15
  _quartermaster = null;
20
16
  getKnowledge(actionResult) {
21
- const knowledgeFiles = this.getKnowledgeTracker().getRelevantKnowledge(actionResult);
22
- if (knowledgeFiles.length === 0)
23
- return '';
24
- const knowledgeContent = knowledgeFiles
25
- .map((k) => k.content)
26
- .filter((k) => !!k)
27
- .join('\n\n');
28
- tag('operation').log(`Found ${knowledgeFiles.length} relevant knowledge ${pluralize(knowledgeFiles.length, 'file')}`);
29
- return dedent `
30
- <knowledge>
31
- Here is relevant knowledge for this page:
32
-
33
- ${knowledgeContent}
34
- </knowledge>
35
- `;
17
+ return this.getKnowledgeTracker().renderRelevantKnowledge(actionResult);
36
18
  }
37
19
  getExperience(actionResult) {
38
- const tracker = this.getExperienceTracker();
39
- const toc = tracker.getExperienceTableOfContents(actionResult);
40
- if (toc.length === 0)
41
- return '';
42
- const totalSections = toc.reduce((sum, entry) => sum + entry.sections.length, 0);
43
- debugLog(`injecting experience TOC (${toc.length} files, ${totalSections} sections)`);
44
- tag('operation').log(`Found ${toc.length} experience ${pluralize(toc.length, 'file')} (${totalSections} sections)`);
45
- return renderExperienceToc(toc);
20
+ return this.getExperienceTracker().renderExperienceTocFor(actionResult);
46
21
  }
47
22
  setHistorian(historian) {
48
23
  this._historian = historian;
@@ -26,7 +26,6 @@ export declare class Tester extends TaskAgent implements Agent {
26
26
  researcher: Researcher;
27
27
  navigator: Navigator;
28
28
  agentTools: any;
29
- executionLogFile: string | null;
30
29
  previousUrl: string | null;
31
30
  previousStateHash: string | null;
32
31
  pageStateHash: string | null;
@@ -56,7 +55,6 @@ export declare class Tester extends TaskAgent implements Agent {
56
55
  shouldStopForStalledExecution(task: Test, previousState: ActionResult, toolExecutions: any[]): boolean;
57
56
  prepareInstructionsForNextStep(task: Test): Promise<string>;
58
57
  reinjectContextIfNeeded(iteration: number, currentState: ActionResult): Promise<string>;
59
- promptLogStep(task: Test): Promise<string>;
60
58
  finishTest(task: Test): void;
61
59
  abortStartedTestOnErrorPage(task: Test, actionResult: ActionResult): Promise<{
62
60
  success: boolean;
@@ -5,7 +5,6 @@ import dedent from 'dedent';
5
5
  import { z } from 'zod';
6
6
  import { ActionResult } from "../action-result.js";
7
7
  import { clearActivity, setActivity } from "../activity.js";
8
- import { ConfigParser } from "../config.js";
9
8
  import { Observability } from "../observability.js";
10
9
  import { Stats } from "../stats.js";
11
10
  import { TestResult } from "../test-plan.js";
@@ -16,7 +15,7 @@ import { createDebug, tag } from "../utils/logger.js";
16
15
  import { loop } from "../utils/loop.js";
17
16
  import { actionRule, capabilityGroundingRule, dataProtectionRules, focusedElementRule, formRequirementsRule, locatorRule, multipleTabsRule, sectionContextRule } from "./rules.js";
18
17
  import { TaskAgent } from "./task-agent.js";
19
- import { createCodeceptJSTools, createSpecialContextTools } from "./tools.js";
18
+ import { createCodeceptJSTools, createIframeTools } from "./tools.js";
20
19
  const debugLog = createDebug('explorbot:tester');
21
20
  const SAMPLE_FILES_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '../../assets/sample-files');
22
21
  const SAMPLE_FILES = {
@@ -43,7 +42,6 @@ export class Tester extends TaskAgent {
43
42
  researcher;
44
43
  navigator;
45
44
  agentTools;
46
- executionLogFile = null;
47
45
  previousUrl = null;
48
46
  previousStateHash = null;
49
47
  pageStateHash = null;
@@ -120,9 +118,6 @@ export class Tester extends TaskAgent {
120
118
  const conversation = this.provider.startConversation(this.getSystemMessage(), 'tester');
121
119
  conversation.markLastMessageCacheable();
122
120
  this.currentConversation = conversation;
123
- const outputDir = ConfigParser.getInstance().getOutputDir();
124
- this.executionLogFile = join(outputDir, `tester_${task.sessionName}.md`);
125
- // Note: Markdown saving functionality removed from Conversation class
126
121
  const scenarioBlock = this.buildScenarioBlock(task, initialState);
127
122
  conversation.addUserText(scenarioBlock);
128
123
  conversation.markLastMessageCacheable();
@@ -217,10 +212,12 @@ export class Tester extends TaskAgent {
217
212
  const currentState = this.getCurrentState();
218
213
  const tools = {
219
214
  ...codeceptjsTools,
220
- ...(currentState.isInsideIframe ? createSpecialContextTools(this.explorer, 'iframe') : {}),
221
215
  ...this.createTestFlowTools(task, currentState, conversation),
222
216
  ...this.agentTools,
223
217
  };
218
+ if (currentState.isInsideIframe) {
219
+ Object.assign(tools, createIframeTools(this.explorer));
220
+ }
224
221
  debugLog(`Test ${task.scenario} iteration ${iteration}`);
225
222
  if (this.explorer.getStateManager().isInDeadLoop()) {
226
223
  task.addNote('Dead loop detected. Stopped');
@@ -456,7 +453,6 @@ export class Tester extends TaskAgent {
456
453
  const currentUrl = currentState.url;
457
454
  const currentStateHash = currentState.hash;
458
455
  const isNewUrl = this.previousUrl !== currentUrl;
459
- const isStateChanged = !isNewUrl && this.previousStateHash !== currentStateHash;
460
456
  this.previousUrl = currentUrl;
461
457
  this.previousStateHash = currentStateHash;
462
458
  let context = '';
@@ -564,23 +560,6 @@ export class Tester extends TaskAgent {
564
560
  `;
565
561
  }
566
562
  }
567
- // if (isStateChanged) {
568
- // const combinedHtml = await currentState.combinedHtml();
569
- // context += dedent`
570
- // Context (state changed):
571
- // <page>
572
- // CURRENT URL: ${currentState.url}
573
- // CURRENT TITLE: ${currentState.title}
574
- // </page>
575
- // <page_html>
576
- // ${combinedHtml}
577
- // </page_html>
578
- // <page_aria>
579
- // ${currentState.ariaSnapshot}
580
- // </page_aria>
581
- // `;
582
- // return context;
583
- // }
584
563
  if (context)
585
564
  return context;
586
565
  if (iteration % 5)
@@ -598,34 +577,6 @@ export class Tester extends TaskAgent {
598
577
  </page_aria>
599
578
  `;
600
579
  }
601
- async promptLogStep(task) {
602
- let logPrompt = dedent `
603
- <task>
604
- Add a note explaining what you achieved with previous action.
605
- Use tools to interact with the page to achieve the scenario goal or expected outcomes.
606
- Call record tool to explain the last action
607
- Format: record([<action performed>, <what has changed>, <what you expect to do next>])
608
- </task>
609
- `;
610
- if (task.getPrintableNotes()) {
611
- logPrompt = dedent `
612
- Your interaction log notes:
613
- <notes>
614
- ${task.getPrintableNotes()}
615
- </notes>
616
-
617
- <rules>
618
- Use your previous interaction notes to guide your next actions.
619
- Do not perform the same checks.
620
- </rules>
621
- `;
622
- }
623
- const remaining = task.getRemainingExpectations();
624
- if (remaining.length > 0) {
625
- logPrompt += `\nExpected steps to check: ${remaining.join(', ')}`;
626
- }
627
- return logPrompt;
628
- }
629
580
  finishTest(task) {
630
581
  if (!task.hasFinished) {
631
582
  task.finish(TestResult.FAILED);
@@ -725,11 +676,13 @@ export class Tester extends TaskAgent {
725
676
  - Before retrying your actions check maybe they already achived expected results. Use see() tool for that
726
677
  - If the current URL is already a create/edit/new form and the scenario is about creating/editing that entity, fill and submit that form. Do not click the list-page "New" button again from inside the form.
727
678
  - If the scenario is about search/filter/sort/tabs/list inspection and the current URL is a create/edit/new form, go back or reset to the stable list page before interacting with list controls.
728
- - When selecting related entities from a list, do not choose rows/options/cards marked as "0 items", "0 tests", or otherwise empty if the scenario requires selecting real content.
729
- - In selection pickers, counters such as "Selected 0", "Matched tests 0", or disabled Save/Apply mean the selection did not register. Choose a non-empty item or change filters before submitting.
679
+ - When selecting related entities from a list, do not choose rows/options/cards marked as "0 items", "0 results", or otherwise empty if the scenario requires selecting real content.
680
+ - In selection pickers, counters such as "Selected 0", "Matched 0", or disabled Save/Apply mean the selection did not register. Choose a non-empty item or change filters before submitting.
730
681
  - A passed form/click command only means the command executed. If a required field remains empty, submit stays disabled, or the expected text is not visible, treat the action as not completed and correct the missing field/state.
731
- - For filter/tab scenarios, success requires BOTH: the requested filter/tab is visibly active/selected AND the list content matches that filter. Do not finish from only one of these signals.
732
- - Empty-state text such as "No matched items" only proves a filter when the requested filter/tab is active and the empty state belongs to the filtered list.
682
+ - For filter/tab scenarios, success requires BOTH: the requested state is evidenced by a selected control, URL/query, or another explicit state indicator AND the list content matches that state. Do not finish from only one of these signals.
683
+ - Once the requested control state and matching content are both visible, finish the scenario instead of repeating the interaction. Do not require an aggregate count change unless a baseline was observed immediately before the action.
684
+ - Empty-state text such as "No matched items" only proves a filter when the requested filter state is explicit and the empty state belongs to the filtered list.
685
+ - Associate validation feedback with a field only through explicit evidence such as the field label in the message, an accessibility relationship, focus on the invalid control, or visual confirmation. Do not infer the affected field from DOM order or proximity alone.
733
686
  - When filling complex form with lot of actions performed, use see() to look which fields were filled and which are not
734
687
  - When verify() fails, use see() to visually confirm the result — visual confirmation is equally valid evidence
735
688
  - For visual state verification (active tabs, selected items, counts, colors), prefer see() over DOM-based verify()
@@ -788,10 +741,6 @@ export class Tester extends TaskAgent {
788
741
  When creating or editing items via form() or type() you should include ${task.sessionName} in the value (if it is not restricted by the application logic)
789
742
  Initial page URL: ${actionResult.url}
790
743
 
791
- ${capabilityGroundingRule}
792
-
793
- ${dataProtectionRules}
794
-
795
744
  ${this.buildDeletionScope(task)}
796
745
 
797
746
  ${this.buildAvailableFiles()}
@@ -1,4 +1,4 @@
1
- import { ActionResult } from '../action-result.js';
1
+ import { ActionResult, type PageDiff } from '../action-result.js';
2
2
  import type { ExperienceTracker } from '../experience-tracker.js';
3
3
  import type Explorer from '../explorer.js';
4
4
  import { type Task } from '../test-plan.js';
@@ -24,18 +24,29 @@ export declare function createCodeceptJSTools(explorer: Explorer, task: Task): {
24
24
  explanation: any;
25
25
  }, Record<string, any>, import("@ai-sdk/provider-utils").Context>>;
26
26
  };
27
- export declare function createSpecialContextTools(explorer: Explorer, context: 'iframe'): {
28
- exitIframe?: undefined;
29
- } | {
27
+ export declare function createIframeTools(explorer: Explorer): {
30
28
  exitIframe: import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<{
31
29
  reason?: string;
32
30
  }, Record<string, any>, import("@ai-sdk/provider-utils").Context>>;
33
31
  };
34
- export declare function createAgentTools({ explorer, researcher, navigator, experienceTracker, getState, supervisor, }: {
32
+ export declare function createLearnExperienceTool({ getExperienceTracker, getState }: {
33
+ getExperienceTracker: () => ExperienceTracker;
34
+ getState: () => ActionResult | null;
35
+ }): import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<{
36
+ fileTag: any;
37
+ sectionIndex: any;
38
+ }, {
39
+ title: string;
40
+ url: string;
41
+ content: string;
42
+ } | {
43
+ error: string;
44
+ }, import("@ai-sdk/provider-utils").Context>>;
45
+ export declare function createAgentTools({ explorer, researcher, navigator, supervisor, withExperience, }: {
35
46
  explorer: Explorer;
36
47
  researcher: Researcher;
37
48
  navigator: Navigator;
38
- experienceTracker?: ExperienceTracker;
39
- getState?: () => ActionResult | null;
40
49
  supervisor?: boolean;
50
+ withExperience?: boolean;
41
51
  }): any;
52
+ export declare function isMajorPageChange(pageDiff: PageDiff): boolean;