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
@@ -98,7 +98,6 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
98
98
  }
99
99
 
100
100
  const diff = await current.diff(previous);
101
- await diff.calculate();
102
101
 
103
102
  if (!diff.ariaChanged && diff.htmlParts.length === 0) {
104
103
  debugLog(`No diff between current and previous state for overlay "${focusArea.name}"`);
@@ -359,13 +358,12 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
359
358
  const isCoordinateClick = el.commands[0].startsWith('I.clickXY(');
360
359
  if (!isCoordinateClick) {
361
360
  const hoverCmd = el.commands[0].replace('I.click(', 'I.moveCursorTo(');
362
- await this.explorer.attemptAction(hoverCmd, undefined, false);
361
+ await this.explorer.attemptAction(hoverCmd, undefined);
363
362
  await new Promise((r) => setTimeout(r, 500));
364
363
 
365
364
  await this.explorer.capturePageState();
366
365
  const hoverAR = ActionResult.fromState(this.stateManager.getCurrentState()!);
367
366
  const hoverDiff = await hoverAR.diff(previousState);
368
- await hoverDiff.calculate();
369
367
  const hoverHtmlSize = hoverDiff.htmlParts.reduce((sum, p) => sum + p.subtree.length, 0);
370
368
  const hoverRevealed = hoverDiff.ariaChanged && hoverHtmlSize > 500;
371
369
 
@@ -405,7 +403,7 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
405
403
  let clickCode: string | null = null;
406
404
  const action = this.explorer.createAction();
407
405
  for (const cmd of commands) {
408
- if (await action.attempt(cmd, undefined, false)) {
406
+ if (await action.attempt(cmd, undefined)) {
409
407
  clickCode = cmd;
410
408
  break;
411
409
  }
@@ -422,7 +420,6 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
422
420
  await this.explorer.createAction().capturePageState();
423
421
  const currAR = ActionResult.fromState(this.stateManager.getCurrentState()!);
424
422
  diff = await currAR.diff(previousState);
425
- await diff.calculate();
426
423
  } catch (err) {
427
424
  tag('warning').log(`State capture failed after click: ${err instanceof Error ? err.message : err}`);
428
425
  await this._restorePageState(state.url, originalAria);
@@ -453,7 +450,7 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
453
450
  await (this as any).cancelInUi();
454
451
  await this.explorer.capturePageState();
455
452
  const currentAria = this.stateManager.getCurrentState()?.ariaSnapshot || '';
456
- if (!diffAriaSnapshots(originalAria, currentAria)) return;
453
+ if (!diffAriaSnapshots(originalAria, currentAria).text) return;
457
454
  } catch (err) {
458
455
  tag('warning').log(`State capture failed after cancelInUi: ${err instanceof Error ? err.message : err}`);
459
456
  }
@@ -94,7 +94,7 @@ export function extractContainerFromBlockquote(sectionMarkdown: string): string
94
94
  }
95
95
 
96
96
  export function parseResearchSections(markdown: string): ResearchSection[] {
97
- const hasExtendedResearch = markdown.includes('\n# Extended Research') || markdown.startsWith('# Extended Research');
97
+ const hasExtendedResearch = mdq(markdown).query('section1(~"Extended Research")').count() > 0;
98
98
 
99
99
  return parseSections(markdown)
100
100
  .filter((s) => !SKIP_SECTIONS.has(s.name.toLowerCase()) && !s.name.toLowerCase().includes('data:'))
@@ -46,24 +46,6 @@ export class ResearchResult {
46
46
  }));
47
47
  }
48
48
 
49
- updateSection(sectionName: string, locators: Locator[]): void {
50
- const sections = parseResearchSections(this.text);
51
- const section = sections.find((s) => s.name === sectionName);
52
- if (!section) return;
53
-
54
- for (const el of section.elements) {
55
- const elLocators = locators.filter((l) => l.element === el.name);
56
- for (const loc of elLocators) {
57
- const value = loc.valid === false ? null : loc.locator || null;
58
- if (loc.type === 'css') el.css = value;
59
- if (loc.type === 'xpath') el.xpath = value;
60
- if (loc.type === 'aria') el.aria = value ? parseAriaLocator(value) : null;
61
- }
62
- }
63
-
64
- this.rebuildSectionInText(section);
65
- }
66
-
67
49
  rebuildSectionInText(section: ResearchSection): void {
68
50
  if (section.elements.length === 0) return;
69
51
  const newTable = rebuildSectionMarkdown(section);
@@ -105,9 +105,13 @@ export function WithSections<T extends Constructor>(Base: T) {
105
105
  </section_format>
106
106
 
107
107
  <rules>
108
- - Every element with eidx MUST appear in the table.
108
+ - List only elements physically inside this section's declared container.
109
+ - Do not copy global toolbar, navigation, list, or detail elements into this section unless they are descendants of this section container.
110
+ - Every element with eidx inside this section's container MUST appear in the table.
109
111
  - Every row needs CSS; ARIA may be "-" for icon-only buttons.
110
112
  - ARIA locator JSON uses keys "role" and "text" (NOT "name").
113
+ - Elements marked data-explorbot-hit="covered" or "offscreen" are not directly actionable; describe the covering or focused UI first.
114
+ - In split-pane pages, entity detail panels are active detail context; include close/back/pin controls in the detail panel section when present.
111
115
  </rules>
112
116
 
113
117
  ${generalLocatorRuleText}
@@ -1,4 +1,3 @@
1
- import { join } from 'node:path';
2
1
  import dedent from 'dedent';
3
2
  import { ActionResult } from '../action-result.js';
4
3
  import { setActivity } from '../activity.ts';
@@ -15,7 +14,7 @@ import { diffAriaSnapshots } from '../utils/aria.ts';
15
14
  import { ErrorPageError, detectPageCondition } from '../utils/error-page.ts';
16
15
  import { HooksRunner } from '../utils/hooks-runner.ts';
17
16
  import { isBodyEmpty } from '../utils/html.ts';
18
- import { createDebug, pluralize, tag } from '../utils/logger.js';
17
+ import { createDebug, tag } from '../utils/logger.js';
19
18
  import { mdq } from '../utils/markdown-query.ts';
20
19
  import { RulesLoader } from '../utils/rules-loader.ts';
21
20
  import type { Agent } from './agent.js';
@@ -315,11 +314,9 @@ export class Researcher extends ResearcherBase implements Agent {
315
314
  researchFile = saveResearch(stateHash, result.text, combinedHtml);
316
315
  }
317
316
 
318
- const summaryMatch = result.text.match(/## Summary\s*\n+([\s\S]*?)(?=\n##|$)/i);
319
- if (summaryMatch) {
320
- const summaryLine = summaryMatch[1].trim().split('\n')[0].trim().slice(0, 200);
321
- if (summaryLine) this.experienceTracker.updateSummary(this.actionResult!, summaryLine);
322
- }
317
+ const summaryText = mdq(result.text).query('section2(/^summary/)').query('paragraph[0]').text().trim();
318
+ const summaryLine = summaryText.split('\n')[0]?.trim().slice(0, 200);
319
+ if (summaryLine) this.experienceTracker.updateSummary(this.actionResult!, summaryLine);
323
320
 
324
321
  tag('multiline').log(formatResearchSummary(result.text, { visionUsed: this.hasScreenshotToAnalyze }));
325
322
  tag('success').log('Research complete');
@@ -428,7 +425,10 @@ export class Researcher extends ResearcherBase implements Agent {
428
425
  - Research all menus and navigational areas.
429
426
  - Ignore decorative sidebars, footer-only links, and external links.
430
427
  - Detect layout patterns: list/detail split, 2-pane, or 3-pane layouts.
431
- - Every element with an eidx attribute MUST appear in the UI map describe icon-only buttons by their visual role.
428
+ - 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.
429
+ - 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.
430
+ - 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.
431
+ - Every element with an eidx attribute MUST appear in exactly one matching UI map section — describe icon-only buttons by their visual role.
432
432
  - Every UI map row needs a CSS selector; ARIA may be "-" for icon-only buttons, CSS must never be "-".
433
433
  - ARIA locator JSON uses keys "role" and "text" (NOT "name").
434
434
  - Mark elements with likely hover interactions (title, aria-describedby, menu items with submenus) as "(hover)".
@@ -445,6 +445,7 @@ export class Researcher extends ResearcherBase implements Agent {
445
445
  .join('\n')}
446
446
 
447
447
  - Sections can overlap; prefer more detailed sections over broader ones.
448
+ - 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.
448
449
  - Never name a section "Focus" or "Focused" — use what it contains (Detail, Modal, Form, Content, List).
449
450
  - Omit sections that are not present or not relevant.
450
451
  - Each section needs a container CSS locator; UI map CSS locators are relative to it.
@@ -466,23 +467,7 @@ export class Researcher extends ResearcherBase implements Agent {
466
467
  if (!this.actionResult) throw new Error('actionResult is not set');
467
468
 
468
469
  const html = await this.actionResult.combinedHtml();
469
- const knowledgeFiles = this.stateManager.getRelevantKnowledge();
470
-
471
- let knowledge = '';
472
- if (knowledgeFiles.length > 0) {
473
- const knowledgeContent = knowledgeFiles
474
- .map((k) => k.content)
475
- .filter((k) => !!k)
476
- .join('\n\n');
477
-
478
- tag('operation').log(`Found ${knowledgeFiles.length} relevant knowledge ${pluralize(knowledgeFiles.length, 'file')} for: ${this.actionResult.url}`);
479
- knowledge = `
480
- <hint>
481
- Here is relevant knowledge for this page:
482
-
483
- ${knowledgeContent}
484
- </hint>`;
485
- }
470
+ const knowledge = this.explorer.getKnowledgeTracker().renderRelevantKnowledge(this.actionResult);
486
471
 
487
472
  const ariaSnapshot = this.actionResult.getCompactARIA();
488
473
 
@@ -540,39 +525,6 @@ export class Researcher extends ResearcherBase implements Agent {
540
525
  `;
541
526
  }
542
527
 
543
- async textContent(state: WebPageState): Promise<string> {
544
- const actionResult = ActionResult.fromState(state);
545
- const html = await actionResult.combinedHtml();
546
-
547
- const prompt = dedent`
548
- Transform into markdown.
549
- Identify headers, footers, asides, special application parts and main contant.
550
- Content should be in markdown format. If it is content: tables must be tables, lists must be lists.
551
- Navigation elements should be represented as standalone blocks after the content.
552
- Do not summarize content, just transform it into markdown.
553
- It is important to list all the content text
554
- If it is link it must be linked
555
- You can summarize footers/navigation/aside elements.
556
- But main conteint should be kept as text and formatted as markdown based on its current markup.
557
- Links to external web sites should be avoided in output.
558
-
559
- Break down into sections:
560
-
561
- ## Content Area
562
-
563
- ## Navigation Area
564
-
565
- <page_html>
566
- ${html}
567
- </page_html>
568
- `;
569
-
570
- const model = this.provider.getModelForAgent('researcher');
571
- const r = await this.provider.chat([{ role: 'user', content: prompt }], model, { agentName: 'researcher', telemetryFunctionId: 'researcher.textContent' });
572
-
573
- return r.text;
574
- }
575
-
576
528
  private getScreenshotFromState(state: WebPageState): { actionResult: ActionResult; image: Buffer } | null {
577
529
  const actionResult = ActionResult.fromState(state);
578
530
  const image = actionResult.screenshot;
@@ -777,7 +729,7 @@ export class Researcher extends ResearcherBase implements Agent {
777
729
  const beforeAria = this.stateManager.getCurrentState()?.ariaSnapshot || null;
778
730
 
779
731
  await this.explorer.executeAction('I.clickXY(0, 0)');
780
- if (diffAriaSnapshots(beforeAria, this.stateManager.getCurrentState()?.ariaSnapshot || null)) return;
732
+ if (diffAriaSnapshots(beforeAria, this.stateManager.getCurrentState()?.ariaSnapshot || null).text) return;
781
733
 
782
734
  await this.explorer.executeAction(`I.pressKey('Escape')`);
783
735
  }
package/src/ai/rules.ts CHANGED
@@ -2,7 +2,7 @@ import dedent from 'dedent';
2
2
 
3
3
  export const recommendedCodeceptCommands = ['I.click', 'I.type', 'I.fillField', 'I.see', 'I.seeElement'] as const;
4
4
 
5
- export const locatorRule = dedent`
5
+ const locatorPriorityRule = dedent`
6
6
  <locator_priority>
7
7
  Use the following priority when selecting locators:
8
8
 
@@ -19,10 +19,12 @@ export const locatorRule = dedent`
19
19
  Example: '#login-btn', '[data-testid="submit"]', 'form#login input[name="email"]'
20
20
 
21
21
  4. XPath (last resort) - for complex hierarchy or when CSS can't express the path
22
- Always start with //, never use positional indices like [1], [2]
22
+ Always start with //. Avoid positional indices like [1], [2] except as a last-resort disambiguator
23
23
  Example: '//form[@id="login"]//input[@name="email"]'
24
24
  </locator_priority>
25
+ `;
25
26
 
27
+ const contextSimplificationRule = dedent`
26
28
  <context_simplification>
27
29
  When container is available from UI map sections:
28
30
  - Text + container is simplest and PREFERRED: I.click('Save', '.modal')
@@ -30,7 +32,9 @@ export const locatorRule = dedent`
30
32
  - ALWAYS use context parameter unless locator is XPath or unique ID
31
33
  - No need for complex ARIA when container narrows scope sufficiently
32
34
  </context_simplification>
35
+ `;
33
36
 
37
+ const locatorStrategyRule = dedent`
34
38
  <disambiguation>
35
39
  When multiple elements could match the request, select based on intent:
36
40
  1. Match the context of recent actions - if filling a form, use elements in that same form
@@ -48,8 +52,9 @@ export const locatorRule = dedent`
48
52
  - Use aria-label value if present: { "role": "button", "text": "Close" } (from aria-label="Close")
49
53
  - Use title attribute if present: { "role": "button", "text": "Settings" } (from title="Settings")
50
54
  - If no accessible name exists, mark ARIA as "-" and use CSS/XPath:
51
- * CSS: use partial href a[href*="settings"] or SVG icon class a:has(svg.md-icon-cog)
52
- * XPath: use contains(@href,"settings") or SVG class //a[.//svg[contains(@class,"md-icon-cog")]]
55
+ * CSS: use partial href a[href*="settings"] or SVG icon class a:has(svg.icon-settings)
56
+ * XPath: use contains(@href,"settings") or SVG class //a[.//svg[contains(@class,"icon-settings")]]
57
+ - 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.
53
58
  - NEVER use empty text: { "role": "button", "text": "" } is INVALID and useless
54
59
 
55
60
  <good_aria_locator_example>
@@ -80,9 +85,9 @@ export const locatorRule = dedent`
80
85
  - Vue: data-v-* attributes
81
86
  Avoid locators that seem to have generated ids or class names (long random numbers, uuids, hashes, etc)
82
87
  Prefer text or ARIA locators over href-based ones. But for icon-only links with no accessible name, use:
83
- - Partial href match: a[href*="settings"], a[href*="requirements"] (use path segments, not full URLs)
84
- - SVG icon class: a:has(svg.md-icon-cog), button:has(svg.md-icon-plus) (target the SVG class inside the link/button)
85
- Avoid full absolute href like a[href="/projects/imr_manual12/settings"] — use generic path segments instead
88
+ - Partial href match: a[href*="settings"], a[href*="reports"] (use path segments, not full URLs)
89
+ - SVG icon class: a:has(svg.icon-settings), button:has(svg.icon-add) (target the SVG class inside the link/button)
90
+ Avoid full absolute href like a[href="/items/12345/settings"] — use generic path segments instead
86
91
  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)
87
92
  Prefer semantic class names, roles, data attributes, or element hierarchy for containers
88
93
 
@@ -94,7 +99,7 @@ export const locatorRule = dedent`
94
99
 
95
100
  <xpath_rules>
96
101
  XPath locators must start with //.
97
- XPath should use positional indices [1], [2], [3] and contains(., "text") for disambiguation.
102
+ XPath may use positional indices [1], [2] and contains(., "text") as a last-resort disambiguator when attribute/text strategies are exhausted.
98
103
  XPath should rely less on class names — prefer element hierarchy, position, and text content.
99
104
  XPath and CSS MUST provide different strategies for finding the same element.
100
105
  </xpath_rules>
@@ -106,22 +111,26 @@ export const locatorRule = dedent`
106
111
  '#content-top #user_name'
107
112
  '#content-top form input[name="name"]'
108
113
  'a.nav-item[href*="settings"]' // icon-only link matched by partial href
109
- 'a.nav-item:has(svg.md-icon-cog)' // icon-only link matched by SVG icon class
114
+ 'a.nav-item:has(svg.icon-settings)' // icon-only link matched by SVG icon class
110
115
  '//nav//a[contains(@href,"settings")]' // XPath for icon-only nav link
111
116
  </good locator example>
112
117
 
113
118
  <bad locator example>
114
- 'a.filter-tab:nth-of-type(1)' // WRONG: positional in CSS, use :has-text("Manual") instead
115
- '//a[contains(@class,"filter-tab") and contains(@class,"active")]' // WRONG: XPath repeats CSS approach, use positional //a[contains(@class,"filter-tab")][1]
119
+ 'a.filter-tab:nth-of-type(1)' // WRONG: positional in CSS, use :has-text("Active") instead
120
+ '//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]
116
121
  '//table//tbody/tr[1]//button[contains(@onclick,'fn()')]' // onclick is not semantic attribute
117
122
  '//html/body/vue-button-123' // vue-framework specific locator
118
- 'link "New Template"' // WRONG: malformed string, use {"role":"link","text":"New Template"}
119
- 'a[href="/projects/imr_manual12/settings"]' // WRONG: full absolute href, use a[href*="settings"] instead
123
+ 'link "New Item"' // WRONG: malformed string, use {"role":"link","text":"New Item"}
124
+ 'a[href="/items/12345/settings"]' // WRONG: full absolute href, use a[href*="settings"] instead
120
125
  </bad locator example>
121
126
 
122
127
  HTML locators must be valid JS strings
123
128
  `;
124
129
 
130
+ export const locatorRule = [locatorPriorityRule, contextSimplificationRule, locatorStrategyRule].join('\n\n');
131
+
132
+ export const drillLocatorRule = [locatorPriorityRule, locatorStrategyRule].join('\n\n');
133
+
125
134
  export const fileUploadRule = dedent`
126
135
  <file_upload>
127
136
  Explorbot CAN upload files using I.attachFile() via form() tool.
@@ -307,6 +316,7 @@ export const actionRule = dedent`
307
316
  Use context parameter (second argument) to narrow click area when:
308
317
  - The same text/button appears multiple times on page
309
318
  - You need to click inside a specific form, modal, or section
319
+ - You need an icon-only confirm/save control next to a field in an inline create/edit row
310
320
  Context should be a CSS selector pointing to a unique container.
311
321
 
312
322
  <example>
@@ -319,6 +329,7 @@ export const actionRule = dedent`
319
329
  </example>
320
330
 
321
331
  Prefer text/ARIA locators with context over complex CSS/XPath selectors.
332
+ 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.
322
333
  If locator doesn't work, try CSS or XPath locators.
323
334
  If nothing works, use I.clickXY(x, y) as last resort.
324
335
 
@@ -49,6 +49,9 @@ export class SessionAnalyst implements Agent {
49
49
  ## Severity (defects only)
50
50
  [High] blocks a core flow · [Medium] degrades a flow but workaround exists · [Low] cosmetic / edge case
51
51
 
52
+ ## Naming tests
53
+ 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\`.
54
+
52
55
  ## Format
53
56
 
54
57
  # Session Analysis
@@ -60,22 +63,22 @@ export class SessionAnalyst implements Agent {
60
63
  - Features: <capabilities>
61
64
 
62
65
  ## What works
63
- - **<feature>** — #2, #7, #8
66
+ - **<feature>** — \`ET-2 <title>\`, \`ET-7 <title>\`
64
67
 
65
68
  ## Defects
66
69
 
67
70
  ### [Medium] <plain-English bug title>
68
- Affects: #3, #5
71
+ Affects: \`ET-3 <title>\`, \`ET-5 <title>\`
69
72
  Reproduce:
70
73
  1. <concrete UI step>
71
74
  2. <next>
72
75
  Evidence: <one short observation>
73
76
 
74
77
  ## UX issues
75
- - **<feature>** — <what's confusing> (#7)
78
+ - **<feature>** — <what's confusing> (\`ET-7 <title>\`)
76
79
 
77
80
  ## Execution Issues
78
- - **#2 <scenario>** — <≤10 words, what was unreliable>
81
+ - \`ET-2 <title>\` — <≤10 words, what was unreliable>
79
82
 
80
83
  ## Brevity rules
81
84
 
@@ -143,7 +146,7 @@ export class SessionAnalyst implements Agent {
143
146
  : 'verification_status: none';
144
147
 
145
148
  return dedent`
146
- <test ref="#${ref}">
149
+ <test ref="ET-${ref}">
147
150
  url: ${test.startUrl || '/'}
148
151
  scenario: ${test.scenario}
149
152
  result: ${test.result || 'unknown'}
@@ -1,10 +1,6 @@
1
- import dedent from 'dedent';
2
1
  import type { ActionResult } from '../action-result.js';
3
- import { type ExperienceTracker, renderExperienceToc } from '../experience-tracker.js';
2
+ import type { ExperienceTracker } from '../experience-tracker.js';
4
3
  import type { KnowledgeTracker } from '../knowledge-tracker.js';
5
- import { createDebug, pluralize, tag } from '../utils/logger.js';
6
-
7
- const debugLog = createDebug('explorbot:task-agent');
8
4
  import { Historian } from './historian.js';
9
5
  import type { Navigator } from './navigator.js';
10
6
  import type { Provider } from './provider.js';
@@ -35,34 +31,11 @@ export abstract class TaskAgent {
35
31
  protected abstract getProvider(): Provider;
36
32
 
37
33
  protected getKnowledge(actionResult: ActionResult): string {
38
- const knowledgeFiles = this.getKnowledgeTracker().getRelevantKnowledge(actionResult);
39
-
40
- if (knowledgeFiles.length === 0) return '';
41
-
42
- const knowledgeContent = knowledgeFiles
43
- .map((k) => k.content)
44
- .filter((k) => !!k)
45
- .join('\n\n');
46
-
47
- tag('operation').log(`Found ${knowledgeFiles.length} relevant knowledge ${pluralize(knowledgeFiles.length, 'file')}`);
48
- return dedent`
49
- <knowledge>
50
- Here is relevant knowledge for this page:
51
-
52
- ${knowledgeContent}
53
- </knowledge>
54
- `;
34
+ return this.getKnowledgeTracker().renderRelevantKnowledge(actionResult);
55
35
  }
56
36
 
57
37
  protected getExperience(actionResult: ActionResult): string {
58
- const tracker = this.getExperienceTracker();
59
- const toc = tracker.getExperienceTableOfContents(actionResult);
60
- if (toc.length === 0) return '';
61
-
62
- const totalSections = toc.reduce((sum, entry) => sum + entry.sections.length, 0);
63
- debugLog(`injecting experience TOC (${toc.length} files, ${totalSections} sections)`);
64
- tag('operation').log(`Found ${toc.length} experience ${pluralize(toc.length, 'file')} (${totalSections} sections)`);
65
- return renderExperienceToc(toc);
38
+ return this.getExperienceTracker().renderExperienceTocFor(actionResult);
66
39
  }
67
40
 
68
41
  setHistorian(historian: Historian): void {
package/src/ai/tester.ts CHANGED
@@ -5,17 +5,15 @@ import dedent from 'dedent';
5
5
  import { z } from 'zod';
6
6
  import { ActionResult } from '../action-result.ts';
7
7
  import { clearActivity, setActivity } from '../activity.ts';
8
- import { ConfigParser } from '../config.ts';
9
8
  import type { ExperienceTracker } from '../experience-tracker.ts';
10
9
  import type Explorer from '../explorer.ts';
11
10
  import { Observability } from '../observability.ts';
12
- import type { StateTransition, WebPageState } from '../state-manager.ts';
11
+ import type { StateTransition } from '../state-manager.ts';
13
12
  import { Stats } from '../stats.ts';
14
- import { type Note, type Test, TestResult, type TestResultType } from '../test-plan.ts';
13
+ import { type Test, TestResult, type TestResultType } from '../test-plan.ts';
15
14
  import { detectFocusArea, extractFocusedElement } from '../utils/aria.ts';
16
15
  import { ErrorPageError, isErrorPage } from '../utils/error-page.ts';
17
16
  import { HooksRunner } from '../utils/hooks-runner.ts';
18
- import { codeToMarkdown } from '../utils/html.ts';
19
17
  import { createDebug, tag } from '../utils/logger.ts';
20
18
  import { loop } from '../utils/loop.ts';
21
19
  import type { Agent } from './agent.ts';
@@ -27,7 +25,7 @@ import { Provider } from './provider.ts';
27
25
  import { Researcher } from './researcher.ts';
28
26
  import { actionRule, capabilityGroundingRule, dataProtectionRules, focusedElementRule, formRequirementsRule, locatorRule, multipleTabsRule, sectionContextRule } from './rules.ts';
29
27
  import { TaskAgent } from './task-agent.ts';
30
- import { createCodeceptJSTools, createSpecialContextTools } from './tools.ts';
28
+ import { createCodeceptJSTools, createIframeTools } from './tools.ts';
31
29
 
32
30
  const debugLog = createDebug('explorbot:tester');
33
31
 
@@ -58,7 +56,6 @@ export class Tester extends TaskAgent implements Agent {
58
56
  researcher: Researcher;
59
57
  navigator: Navigator;
60
58
  agentTools: any;
61
- executionLogFile: string | null = null;
62
59
  private previousUrl: string | null = null;
63
60
  private previousStateHash: string | null = null;
64
61
  private pageStateHash: string | null = null;
@@ -151,10 +148,6 @@ export class Tester extends TaskAgent implements Agent {
151
148
  conversation.markLastMessageCacheable();
152
149
  this.currentConversation = conversation;
153
150
 
154
- const outputDir = ConfigParser.getInstance().getOutputDir();
155
- this.executionLogFile = join(outputDir, `tester_${task.sessionName}.md`);
156
- // Note: Markdown saving functionality removed from Conversation class
157
-
158
151
  const scenarioBlock = this.buildScenarioBlock(task, initialState);
159
152
  conversation.addUserText(scenarioBlock);
160
153
  conversation.markLastMessageCacheable();
@@ -261,10 +254,12 @@ export class Tester extends TaskAgent implements Agent {
261
254
 
262
255
  const tools = {
263
256
  ...codeceptjsTools,
264
- ...(currentState.isInsideIframe ? createSpecialContextTools(this.explorer, 'iframe') : {}),
265
257
  ...this.createTestFlowTools(task, currentState, conversation),
266
258
  ...this.agentTools,
267
259
  };
260
+ if (currentState.isInsideIframe) {
261
+ Object.assign(tools, createIframeTools(this.explorer));
262
+ }
268
263
 
269
264
  debugLog(`Test ${task.scenario} iteration ${iteration}`);
270
265
 
@@ -526,7 +521,6 @@ export class Tester extends TaskAgent implements Agent {
526
521
  const currentStateHash = currentState.hash;
527
522
 
528
523
  const isNewUrl = this.previousUrl !== currentUrl;
529
- const isStateChanged = !isNewUrl && this.previousStateHash !== currentStateHash;
530
524
 
531
525
  this.previousUrl = currentUrl;
532
526
  this.previousStateHash = currentStateHash;
@@ -641,27 +635,6 @@ export class Tester extends TaskAgent implements Agent {
641
635
  }
642
636
  }
643
637
 
644
- // if (isStateChanged) {
645
- // const combinedHtml = await currentState.combinedHtml();
646
- // context += dedent`
647
- // Context (state changed):
648
-
649
- // <page>
650
- // CURRENT URL: ${currentState.url}
651
- // CURRENT TITLE: ${currentState.title}
652
- // </page>
653
-
654
- // <page_html>
655
- // ${combinedHtml}
656
- // </page_html>
657
-
658
- // <page_aria>
659
- // ${currentState.ariaSnapshot}
660
- // </page_aria>
661
- // `;
662
- // return context;
663
- // }
664
-
665
638
  if (context) return context;
666
639
 
667
640
  if (iteration % 5) return '';
@@ -680,38 +653,6 @@ export class Tester extends TaskAgent implements Agent {
680
653
  `;
681
654
  }
682
655
 
683
- private async promptLogStep(task: Test): Promise<string> {
684
- let logPrompt = dedent`
685
- <task>
686
- Add a note explaining what you achieved with previous action.
687
- Use tools to interact with the page to achieve the scenario goal or expected outcomes.
688
- Call record tool to explain the last action
689
- Format: record([<action performed>, <what has changed>, <what you expect to do next>])
690
- </task>
691
- `;
692
-
693
- if (task.getPrintableNotes()) {
694
- logPrompt = dedent`
695
- Your interaction log notes:
696
- <notes>
697
- ${task.getPrintableNotes()}
698
- </notes>
699
-
700
- <rules>
701
- Use your previous interaction notes to guide your next actions.
702
- Do not perform the same checks.
703
- </rules>
704
- `;
705
- }
706
-
707
- const remaining = task.getRemainingExpectations();
708
- if (remaining.length > 0) {
709
- logPrompt += `\nExpected steps to check: ${remaining.join(', ')}`;
710
- }
711
-
712
- return logPrompt;
713
- }
714
-
715
656
  private finishTest(task: Test): void {
716
657
  if (!task.hasFinished) {
717
658
  task.finish(TestResult.FAILED);
@@ -810,11 +751,13 @@ export class Tester extends TaskAgent implements Agent {
810
751
  - Before retrying your actions check maybe they already achived expected results. Use see() tool for that
811
752
  - 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.
812
753
  - 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.
813
- - 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.
814
- - 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.
754
+ - 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.
755
+ - 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.
815
756
  - 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.
816
- - 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.
817
- - 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.
757
+ - 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.
758
+ - 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.
759
+ - 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.
760
+ - 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.
818
761
  - When filling complex form with lot of actions performed, use see() to look which fields were filled and which are not
819
762
  - When verify() fails, use see() to visually confirm the result — visual confirmation is equally valid evidence
820
763
  - For visual state verification (active tabs, selected items, counts, colors), prefer see() over DOM-based verify()
@@ -875,10 +818,6 @@ export class Tester extends TaskAgent implements Agent {
875
818
  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)
876
819
  Initial page URL: ${actionResult.url}
877
820
 
878
- ${capabilityGroundingRule}
879
-
880
- ${dataProtectionRules}
881
-
882
821
  ${this.buildDeletionScope(task)}
883
822
 
884
823
  ${this.buildAvailableFiles()}