explorbot 0.4.4 → 0.4.6

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 (86) hide show
  1. package/boat/api-tester/src/apibot.ts +18 -2
  2. package/boat/api-tester/src/cli.ts +85 -274
  3. package/boat/api-tester/src/commands/api-command.ts +10 -0
  4. package/boat/api-tester/src/commands/explore-command.ts +52 -0
  5. package/boat/api-tester/src/commands/init-command.ts +119 -0
  6. package/boat/api-tester/src/commands/know-command.ts +44 -0
  7. package/boat/api-tester/src/commands/plan-command.ts +42 -0
  8. package/boat/api-tester/src/commands/test-command.ts +54 -0
  9. package/boat/prima/src/prima.ts +8 -3
  10. package/dist/boat/api-tester/src/apibot.js +14 -1
  11. package/dist/boat/api-tester/src/cli.js +87 -243
  12. package/dist/boat/api-tester/src/commands/api-command.js +7 -0
  13. package/dist/boat/api-tester/src/commands/explore-command.js +41 -0
  14. package/dist/boat/api-tester/src/commands/init-command.js +88 -0
  15. package/dist/boat/api-tester/src/commands/know-command.js +39 -0
  16. package/dist/boat/api-tester/src/commands/plan-command.js +37 -0
  17. package/dist/boat/api-tester/src/commands/test-command.js +45 -0
  18. package/dist/boat/prima/src/prima.js +10 -3
  19. package/dist/package.json +4 -4
  20. package/dist/src/ai/fisherman/tools.js +7 -1
  21. package/dist/src/ai/fisherman.js +2 -1
  22. package/dist/src/ai/pilot.d.ts +0 -1
  23. package/dist/src/ai/pilot.js +8 -24
  24. package/dist/src/ai/planner.d.ts +4 -0
  25. package/dist/src/ai/planner.js +28 -0
  26. package/dist/src/ai/provider.js +3 -1
  27. package/dist/src/ai/researcher/deep-analysis.d.ts +1 -1
  28. package/dist/src/ai/researcher/deep-analysis.js +14 -6
  29. package/dist/src/ai/rules.js +8 -7
  30. package/dist/src/ai/scout/tools.d.ts +17 -0
  31. package/dist/src/ai/scout/tools.js +130 -0
  32. package/dist/src/ai/scout.d.ts +21 -0
  33. package/dist/src/ai/scout.js +150 -0
  34. package/dist/src/ai/tools.d.ts +1 -1
  35. package/dist/src/ai/tools.js +62 -31
  36. package/dist/src/api/spec-reader.d.ts +1 -0
  37. package/dist/src/api/spec-reader.js +93 -1
  38. package/dist/src/application-spec.d.ts +3 -0
  39. package/dist/src/application-spec.js +21 -5
  40. package/dist/src/commands/base-command.d.ts +3 -3
  41. package/dist/src/commands/init-command.d.ts +3 -0
  42. package/dist/src/commands/init-command.js +6 -3
  43. package/dist/src/config.d.ts +6 -1
  44. package/dist/src/explorbot.d.ts +3 -0
  45. package/dist/src/explorbot.js +33 -0
  46. package/dist/src/explorer.d.ts +1 -1
  47. package/dist/src/explorer.js +1 -1
  48. package/dist/src/knowledge-tracker.d.ts +1 -0
  49. package/dist/src/knowledge-tracker.js +3 -0
  50. package/dist/src/utils/aria-ref.d.ts +16 -0
  51. package/dist/src/utils/aria-ref.js +47 -0
  52. package/dist/src/utils/aria.js +3 -3
  53. package/dist/src/utils/html-diff.js +4 -1
  54. package/dist/src/utils/web-annotate.js +3 -15
  55. package/dist/src/utils/web-element.d.ts +0 -2
  56. package/dist/src/utils/web-element.js +0 -8
  57. package/docs/api-testing/basics.md +26 -2
  58. package/docs/reference/configuration.md +28 -1
  59. package/docs/superpowers/specs/2026-09-09-pagination-rule-design.md +317 -0
  60. package/docs/web-testing/agents.md +9 -1
  61. package/docs/web-testing/planner.md +5 -0
  62. package/docs/workflow/application-spec.md +4 -0
  63. package/package.json +4 -4
  64. package/src/ai/fisherman/tools.ts +8 -1
  65. package/src/ai/fisherman.ts +2 -1
  66. package/src/ai/pilot.ts +8 -25
  67. package/src/ai/planner.ts +33 -0
  68. package/src/ai/provider.ts +2 -1
  69. package/src/ai/researcher/deep-analysis.ts +13 -6
  70. package/src/ai/rules.ts +8 -7
  71. package/src/ai/scout/tools.ts +150 -0
  72. package/src/ai/scout.ts +173 -0
  73. package/src/ai/tools.ts +75 -38
  74. package/src/api/spec-reader.ts +106 -1
  75. package/src/application-spec.ts +22 -4
  76. package/src/commands/base-command.ts +3 -3
  77. package/src/commands/init-command.ts +6 -3
  78. package/src/config.ts +7 -0
  79. package/src/explorbot.ts +36 -0
  80. package/src/explorer.ts +1 -1
  81. package/src/knowledge-tracker.ts +4 -0
  82. package/src/utils/aria-ref.ts +61 -0
  83. package/src/utils/aria.ts +3 -3
  84. package/src/utils/html-diff.ts +3 -1
  85. package/src/utils/web-annotate.ts +3 -15
  86. package/src/utils/web-element.ts +0 -9
@@ -13,7 +13,7 @@ export class ApplicationSpec {
13
13
  this.load();
14
14
  }
15
15
  renderFor(state) {
16
- const relevant = this.pages.filter((page) => state.isMatchedBy({ url: page.url }));
16
+ const relevant = this.relevantPages(state);
17
17
  if (relevant.length === 0)
18
18
  return '';
19
19
  tag('operation').log(`Found application specification for ${state.url}`);
@@ -25,6 +25,9 @@ export class ApplicationSpec {
25
25
  </application_spec>
26
26
  `;
27
27
  }
28
+ matchedUrls(state) {
29
+ return this.relevantPages(state).map((page) => page.url);
30
+ }
28
31
  get pageCount() {
29
32
  return this.pages.length;
30
33
  }
@@ -63,9 +66,22 @@ export class ApplicationSpec {
63
66
  }
64
67
  }
65
68
  resolveSourcePath(sourcePath) {
66
- if (path.isAbsolute(sourcePath))
67
- return path.resolve(sourcePath);
68
- const configParser = ConfigParser.getInstance();
69
- return path.resolve(configParser.resolveProjectDir(sourcePath));
69
+ return resolveSpecSource(sourcePath);
70
+ }
71
+ relevantPages(state) {
72
+ return this.pages.filter((page) => state.isMatchedBy({ url: page.url }));
70
73
  }
71
74
  }
75
+ export function resolveSpecBundlePath(sourcePath) {
76
+ const resolved = resolveSpecSource(sourcePath);
77
+ if (!existsSync(resolved))
78
+ return null;
79
+ if (statSync(resolved).isDirectory())
80
+ return resolved;
81
+ return path.dirname(resolved);
82
+ }
83
+ function resolveSpecSource(sourcePath) {
84
+ if (path.isAbsolute(sourcePath))
85
+ return path.resolve(sourcePath);
86
+ return path.resolve(ConfigParser.getInstance().resolveProjectDir(sourcePath));
87
+ }
@@ -7,15 +7,15 @@ export interface Suggestion {
7
7
  command?: string;
8
8
  hint: string;
9
9
  }
10
- export declare abstract class BaseCommand {
10
+ export declare abstract class BaseCommand<T = ExplorBot> {
11
11
  abstract name: string;
12
12
  abstract description: string;
13
13
  aliases: string[];
14
14
  options: CommandOption[];
15
15
  tuiEnabled: boolean;
16
16
  suggestions: Suggestion[];
17
- explorBot: ExplorBot;
18
- constructor(explorBot: ExplorBot);
17
+ explorBot: T;
18
+ constructor(explorBot: T);
19
19
  abstract execute(args: string): Promise<void>;
20
20
  matches(commandName: string): boolean;
21
21
  printSuggestions(): void;
@@ -1,6 +1,9 @@
1
+ import { type ModelRole } from '../config.js';
2
+ export declare function envTemplate(provider: string): string;
1
3
  export declare function runInit(options: InitCommandOptions): Promise<void>;
2
4
  export declare function writeGlobalConfig(provider: string, apiKey?: string): void;
3
5
  export declare function runInitCommand(options: InitCommandOptions): void;
6
+ export declare function modelLines(provider: string, only?: ModelRole[]): string;
4
7
  type InitCommandOptions = {
5
8
  configPath?: string;
6
9
  force?: boolean;
@@ -37,7 +37,7 @@ ${modelLines(provider)}
37
37
  ${moduleExport}
38
38
  `;
39
39
  }
40
- function envTemplate(provider) {
40
+ export function envTemplate(provider) {
41
41
  const keyLines = Object.entries(PROVIDERS).map(([name, { envKey }]) => {
42
42
  if (name === provider)
43
43
  return `${envKey}=`;
@@ -219,14 +219,17 @@ async function renderLocalProviderWizard() {
219
219
  }), { exitOnCtrlC: false, patchConsole: false });
220
220
  });
221
221
  }
222
- function modelLines(provider) {
222
+ export function modelLines(provider, only) {
223
223
  const recommended = ConfigParser.recommendedModels()[provider] || {};
224
224
  const roles = [
225
225
  ['model', 'fast model with tool calling capabilities'],
226
226
  ['visionModel', 'vision model for screenshot analysis'],
227
227
  ['agenticModel', 'agentic model for decision making'],
228
228
  ];
229
- return roles.map(([role, comment]) => ` // ${comment}\n ${role}: '${provider}/${recommended[role] || '<model-id>'}',`).join('\n');
229
+ let selected = roles;
230
+ if (only)
231
+ selected = roles.filter(([role]) => only.includes(role));
232
+ return selected.map(([role, comment]) => ` // ${comment}\n ${role}: '${provider}/${recommended[role] || '<model-id>'}',`).join('\n');
230
233
  }
231
234
  function globalConfigTemplate(provider) {
232
235
  const { envKey } = PROVIDERS[provider];
@@ -105,6 +105,10 @@ interface RerunnerAgentConfig extends AgentConfig {
105
105
  interface PlannerAgentConfig extends AgentConfig {
106
106
  styles?: string[];
107
107
  stylesDir?: string;
108
+ docsWeight?: number;
109
+ }
110
+ interface ScoutAgentConfig extends AgentConfig {
111
+ dirs?: string[];
108
112
  }
109
113
  interface ScreencastConfig {
110
114
  size?: {
@@ -129,6 +133,7 @@ interface AgentsConfig {
129
133
  quartermaster?: AgentConfig;
130
134
  historian?: HistorianAgentConfig;
131
135
  fisherman?: AgentConfig;
136
+ scout?: ScoutAgentConfig;
132
137
  chief?: AgentConfig;
133
138
  curler?: AgentConfig;
134
139
  rerunner?: RerunnerAgentConfig;
@@ -221,7 +226,7 @@ interface ExplorbotConfig {
221
226
  type RuleEntry = string | Record<string, string>;
222
227
  export declare const EXPLORBOT_CONFIG_PATHS: string[];
223
228
  export declare const EXPLORBOT_ENV_VARS: EnvVar[];
224
- export type { ExplorbotConfig, PlaywrightConfig, AIConfig, HtmlConfig, ActionConfig, AgentConfig, AgentsConfig, HistorianAgentConfig, ResearcherAgentConfig, NavigatorAgentConfig, PlannerAgentConfig, RerunnerAgentConfig, HealRecipe, Hook, HookConfig, HooksConfig, PlaywrightHook, CodeceptJSHook, HookPatternMap, RuleEntry, ReporterConfig, ApiConfig, WebConfig, ApiHookFn, };
229
+ export type { ExplorbotConfig, PlaywrightConfig, AIConfig, HtmlConfig, ActionConfig, AgentConfig, AgentsConfig, HistorianAgentConfig, ResearcherAgentConfig, NavigatorAgentConfig, PlannerAgentConfig, ScoutAgentConfig, RerunnerAgentConfig, HealRecipe, Hook, HookConfig, HooksConfig, PlaywrightHook, CodeceptJSHook, HookPatternMap, RuleEntry, ReporterConfig, ApiConfig, WebConfig, ApiHookFn, };
225
230
  export declare class ConfigParser {
226
231
  static instance: ConfigParser;
227
232
  static recommended: Record<string, Record<string, string>> | null;
@@ -12,6 +12,7 @@ import { AIProvider } from './ai/provider.js';
12
12
  import { Quartermaster } from './ai/quartermaster.js';
13
13
  import { Rerunner } from './ai/rerunner.js';
14
14
  import { Researcher } from './ai/researcher.js';
15
+ import { Scout } from './ai/scout.js';
15
16
  import { SessionAnalyst } from './ai/session-analyst.js';
16
17
  import { Tester } from './ai/tester.js';
17
18
  import { RequestStore } from './api/request-store.js';
@@ -99,6 +100,8 @@ export declare class ExplorBot {
99
100
  agentDriller(): Driller;
100
101
  agentSessionAnalyst(): SessionAnalyst;
101
102
  agentFisherman(): Fisherman | null;
103
+ agentScout(): Scout | null;
104
+ scoutCorpusDirs(): string[];
102
105
  getCurrentPlan(): Plan | undefined;
103
106
  getSuite(): Suite | null;
104
107
  getPlanFeature(): string | undefined;
@@ -12,12 +12,15 @@ import { AIProvider } from "./ai/provider.js";
12
12
  import { Quartermaster } from "./ai/quartermaster.js";
13
13
  import { Rerunner } from "./ai/rerunner.js";
14
14
  import { Researcher } from "./ai/researcher.js";
15
+ import { Scout } from "./ai/scout.js";
16
+ import { loadScoutCorpus } from "./ai/scout/tools.js";
15
17
  import { SessionAnalyst } from "./ai/session-analyst.js";
16
18
  import { Tester } from "./ai/tester.js";
17
19
  import { createAgentTools } from "./ai/tools.js";
18
20
  import { ApiClient } from "./api/api-client.js";
19
21
  import { RequestStore } from "./api/request-store.js";
20
22
  import { loadSpec } from "./api/spec-reader.js";
23
+ import { resolveSpecBundlePath } from "./application-spec.js";
21
24
  import { ConfigParser } from "./config.js";
22
25
  import { ExperienceTracker } from "./experience-tracker.js";
23
26
  import Explorer from "./explorer.js";
@@ -184,6 +187,9 @@ export class ExplorBot {
184
187
  const fisherman = this.agentFisherman();
185
188
  if (fisherman)
186
189
  this.agents.planner.setFisherman(fisherman);
190
+ const scout = this.agentScout();
191
+ if (scout)
192
+ this.agents.planner.setScout(scout);
187
193
  }
188
194
  return this.agents.planner;
189
195
  }
@@ -310,6 +316,33 @@ export class ExplorBot {
310
316
  }
311
317
  return this.agents.fisherman;
312
318
  }
319
+ agentScout() {
320
+ const scoutConfig = this.config.ai?.agents?.scout;
321
+ if (scoutConfig?.enabled !== true)
322
+ return null;
323
+ const dirs = this.scoutCorpusDirs();
324
+ if (dirs.length === 0) {
325
+ tag('warning').log('Scout enabled but no documentation found — set --spec or ai.agents.scout.dirs');
326
+ return null;
327
+ }
328
+ return (this.agents.scout ||= this.createAgent(({ ai }) => new Scout(ai, loadScoutCorpus(dirs))));
329
+ }
330
+ scoutCorpusDirs() {
331
+ const dirs = [];
332
+ const specPath = this.options.applicationSpec || this.config.dirs?.spec;
333
+ if (specPath) {
334
+ const bundle = resolveSpecBundlePath(specPath);
335
+ const pagesDir = bundle && path.join(bundle, 'pages');
336
+ if (pagesDir && existsSync(pagesDir))
337
+ dirs.push(pagesDir);
338
+ }
339
+ for (const dir of this.config.ai?.agents?.scout?.dirs || []) {
340
+ const resolved = this.configParser.resolveProjectDir(dir);
341
+ if (existsSync(resolved))
342
+ dirs.push(resolved);
343
+ }
344
+ return [...new Set(dirs)];
345
+ }
313
346
  getCurrentPlan() {
314
347
  return this.currentPlan;
315
348
  }
@@ -39,7 +39,7 @@ declare class Explorer {
39
39
  testPageErrorHandler: ((error: Error) => void) | null;
40
40
  testConsoleHandler: ((message: any) => void) | null;
41
41
  testDialogHandler: ((dialog: any) => void) | null;
42
- eventDispatcher: any;
42
+ eventDispatcher: NodeJS.EventEmitter<[never]>;
43
43
  constructor(config: ExplorbotConfig, options: ExplorerOptions | undefined, deps: ExplorerDeps);
44
44
  get actor(): CodeceptJS.I;
45
45
  get page(): Page | null;
@@ -208,7 +208,7 @@ class Explorer {
208
208
  codeceptjs.container.create(this.convertToCodeceptConfig(this.config), {});
209
209
  }
210
210
  convertToCodeceptConfig(config) {
211
- const playwrightConfig = { ...config.playwright };
211
+ const playwrightConfig = { visibleLocator: true, ...config.playwright };
212
212
  if (this.options?.show !== undefined) {
213
213
  playwrightConfig.show = this.options.show;
214
214
  }
@@ -16,6 +16,7 @@ export declare class KnowledgeTracker {
16
16
  renderEndpointKnowledge(endpoint: string): string;
17
17
  renderRelevantContext(state: ActionResult): string;
18
18
  renderApplicationSpec(state: ActionResult): string;
19
+ applicationSpecUrls(state: ActionResult): string[];
19
20
  addKnowledge(urlPattern: string, description: string, opts?: {
20
21
  replace?: boolean;
21
22
  }): {
@@ -79,6 +79,9 @@ export class KnowledgeTracker {
79
79
  renderApplicationSpec(state) {
80
80
  return this.applicationSpec?.renderFor(state) || '';
81
81
  }
82
+ applicationSpecUrls(state) {
83
+ return this.applicationSpec?.matchedUrls(state) || [];
84
+ }
82
85
  addKnowledge(urlPattern, description, opts) {
83
86
  const configParser = ConfigParser.getInstance();
84
87
  const configPath = configParser.getConfigPath();
@@ -0,0 +1,16 @@
1
+ import { WebElement } from './web-element.js';
2
+ export declare function ariaRefSelector(ref: string): string;
3
+ export declare function isAriaRef(ref: string): boolean;
4
+ export declare function ariaRefSnapshot(page: any): Promise<string>;
5
+ export declare function parseAriaRefs(ariaSnapshot: string): AriaRefEntry[];
6
+ export declare function elementFromAriaRef(page: any, ref: string): Promise<WebElement | null>;
7
+ export declare function refIsGone(explorer: any, ref: string): Promise<boolean>;
8
+ export declare function describeRef(explorer: any, ref: string): Promise<{
9
+ role: string;
10
+ text: string;
11
+ } | null>;
12
+ export interface AriaRefEntry {
13
+ role: string;
14
+ name: string;
15
+ ref: string;
16
+ }
@@ -0,0 +1,47 @@
1
+ import { WebElement } from "./web-element.js";
2
+ const REF_LINE_PATTERN = /^(\s*)-\s+(\w+)\s*(?:"([^"]*)")?.*?\[ref=(e\d+)\]/;
3
+ const ARIA_REF_PATTERN = /^(f\d+)?e\d+$/i;
4
+ const REF_ROLES = { a: 'link', button: 'button', select: 'combobox', textarea: 'textbox' };
5
+ export function ariaRefSelector(ref) {
6
+ return `aria-ref=${ref}`;
7
+ }
8
+ export function isAriaRef(ref) {
9
+ return ARIA_REF_PATTERN.test(ref);
10
+ }
11
+ export function ariaRefSnapshot(page) {
12
+ return page.locator('body').ariaSnapshot({ mode: 'ai' });
13
+ }
14
+ export function parseAriaRefs(ariaSnapshot) {
15
+ const entries = [];
16
+ for (const line of ariaSnapshot.split('\n')) {
17
+ const match = line.match(REF_LINE_PATTERN);
18
+ if (!match)
19
+ continue;
20
+ entries.push({ role: match[2], name: match[3] || '', ref: match[4] });
21
+ }
22
+ return entries;
23
+ }
24
+ export async function elementFromAriaRef(page, ref) {
25
+ if (!isAriaRef(ref))
26
+ return null;
27
+ return WebElement.fromPlaywrightLocator(page.locator(ariaRefSelector(ref)));
28
+ }
29
+ export async function refIsGone(explorer, ref) {
30
+ const count = () => Promise.resolve(explorer?.withPage?.((page) => page.locator(ariaRefSelector(ref)).count())).catch(() => undefined);
31
+ if ((await count()) !== 0)
32
+ return false;
33
+ await Promise.resolve(explorer?.withPage?.(ariaRefSnapshot)).catch(() => null);
34
+ return (await count()) === 0;
35
+ }
36
+ export async function describeRef(explorer, ref) {
37
+ return Promise.resolve(explorer?.withPage?.((page) => page.locator(ariaRefSelector(ref)).evaluate((el, roles) => {
38
+ const tag = el.tagName.toLowerCase();
39
+ const role = el.getAttribute('role') || roles[tag];
40
+ if (!role)
41
+ return null;
42
+ const text = (el.getAttribute('aria-label') || el.innerText || el.value || '').trim().split('\n')[0];
43
+ if (!text)
44
+ return null;
45
+ return { role, text };
46
+ }, REF_ROLES))).catch(() => null);
47
+ }
@@ -434,9 +434,9 @@ const formatDiffSection = (label, items) => {
434
434
  const summary = countBy(items);
435
435
  if (summary.size === 0)
436
436
  return [` ${label}: []`];
437
- const sorted = Array.from(summary.entries()).sort(([aItem, aCount], [bItem, bCount]) => bCount - aCount || aItem.localeCompare(bItem));
438
- const top = sorted.slice(0, TOP_DIFF_ITEMS);
439
- const rest = sorted.slice(TOP_DIFF_ITEMS);
437
+ const ordered = Array.from(summary.entries());
438
+ const top = ordered.slice(0, TOP_DIFF_ITEMS);
439
+ const rest = ordered.slice(TOP_DIFF_ITEMS);
440
440
  const lines = [` ${label}:`];
441
441
  for (const [item, count] of top) {
442
442
  let suffix = '';
@@ -4,7 +4,7 @@ import { isDynamicId, isGenericClass } from "./xpath.js";
4
4
  const IGNORED_PATHS = new Set(['html[1]', 'html[1]/head[1]', 'html[1]/body[1]']);
5
5
  const SHELL_RATIO = 0.8;
6
6
  const ROOT_CONTENT_RATIO = 0.8;
7
- const LIVE_REGION_ROLES = new Set(['alert', 'alertdialog', 'status', 'log']);
7
+ const LIVE_REGION_ROLES = new Set(['alert', 'alertdialog', 'status', 'log', 'tooltip']);
8
8
  const TEXT_LINE_PREFIX = 'TEXT:';
9
9
  const MESSAGE_MAX_LENGTH = 200;
10
10
  const MESSAGE_LIMIT = 8;
@@ -447,6 +447,9 @@ function semanticSelectorFor(element, allElements) {
447
447
  const selector = buildContainerSelector(current, allElements);
448
448
  if (selector)
449
449
  return selector;
450
+ const classAttr = (current.attrs ?? []).find((a) => a.name === 'class')?.value;
451
+ if (classAttr && filterContainerClasses(classAttr.split(/\s+/).filter(Boolean)).length > 0)
452
+ return undefined;
450
453
  current = dominantChild(current);
451
454
  }
452
455
  return undefined;
@@ -1,24 +1,12 @@
1
+ import { ariaRefSnapshot, parseAriaRefs } from "./aria-ref.js";
1
2
  import { ELEMENT_EXTRACTION_CONFIG, getElementDataExtractorSource } from "./html.js";
2
3
  import { createDebug } from './logger.js';
3
4
  import { WebElement } from "./web-element.js";
4
5
  const debugLog = createDebug('explorbot:web-annotate');
5
- const REF_LINE_PATTERN = /^(\s*)-\s+(\w+)\s*(?:"([^"]*)")?.*?\[ref=(e\d+)\]/;
6
6
  const ANNOTATABLE_ROLES = new Set(['button', 'link', 'textbox', 'searchbox', 'checkbox', 'radio', 'switch', 'combobox', 'tab', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'slider', 'spinbutton', 'treeitem']);
7
- function parseAriaRefs(ariaSnapshot) {
8
- const entries = [];
9
- for (const line of ariaSnapshot.split('\n')) {
10
- const match = line.match(REF_LINE_PATTERN);
11
- if (!match)
12
- continue;
13
- if (!ANNOTATABLE_ROLES.has(match[2]))
14
- continue;
15
- entries.push({ role: match[2], name: match[3] || '', ref: match[4] });
16
- }
17
- return entries;
18
- }
19
7
  export async function annotatePageElements(page) {
20
- const ariaSnapshot = await page.locator('body').ariaSnapshot({ mode: 'ai' });
21
- const refEntries = parseAriaRefs(ariaSnapshot);
8
+ const ariaSnapshot = await ariaRefSnapshot(page);
9
+ const refEntries = parseAriaRefs(ariaSnapshot).filter((entry) => ANNOTATABLE_ROLES.has(entry.role));
22
10
  const byRole = new Map();
23
11
  for (const { role, name, ref } of refEntries) {
24
12
  let list = byRole.get(role);
@@ -36,8 +36,6 @@ export declare class WebElement {
36
36
  static fromXPathMatch(m: XPathMatch): WebElement;
37
37
  static fromPlaywrightLocator(locator: any): Promise<WebElement | null>;
38
38
  static fromEidx(page: any, eidx: string): Promise<WebElement | null>;
39
- static isAriaRef(ref: string): boolean;
40
- static fromAriaRef(page: any, ref: string): Promise<WebElement | null>;
41
39
  static fromEidxList(page: any, eidxList: string[]): Promise<WebElement[]>;
42
40
  static commonAncestor(page: any, eidxList: string[]): Promise<WebElement | null>;
43
41
  static findByXPath(html: string, xpath: string): Promise<{
@@ -111,14 +111,6 @@ export class WebElement {
111
111
  static async fromEidx(page, eidx) {
112
112
  return WebElement.fromPlaywrightLocator(page.locator(`[${EXPLORBOT_ATTRS.eidx}="${eidx}"]`));
113
113
  }
114
- static isAriaRef(ref) {
115
- return /^(f\d+)?e\d+$/i.test(ref);
116
- }
117
- static async fromAriaRef(page, ref) {
118
- if (!WebElement.isAriaRef(ref))
119
- return null;
120
- return WebElement.fromPlaywrightLocator(page.locator(`aria-ref=${ref}`));
121
- }
122
114
  static async fromEidxList(page, eidxList) {
123
115
  const validEidxList = eidxList.filter((eidx) => /^e\d+$/i.test(eidx));
124
116
  if (validEidxList.length === 0)
@@ -66,7 +66,7 @@ npx explorbot api explore https://api.example.com/v1 \
66
66
  -H "Authorization: Bearer $TOKEN"
67
67
  ```
68
68
 
69
- `api explore` takes the base endpoint as its argument, so one line covers the whole run: it plans in every style, executes each plan, and reports the totals. The other commands take a path within the API and read the base from `--endpoint`:
69
+ `api explore` takes an endpoint or a pattern as its argument, so one line covers the whole run: it plans, executes each plan, and reports the totals. Passing the base endpoint covers every collection the spec describes. The other commands take a path within the API and read the base from `--endpoint`:
70
70
 
71
71
  ```bash
72
72
  npx explorbot api plan /users \
@@ -81,7 +81,7 @@ The base endpoint keeps its path prefix: given `https://api.example.com/v1`, ste
81
81
 
82
82
  ### A dedicated API project
83
83
 
84
- If you don't have a web `explorbot.config.js`, run `npx explorbot api init`. It asks for your base endpoint, spec, and a one-line description of the API, then writes a standalone `apibot.config.ts` (with an `ai` and `api` section) plus `output/` and `knowledge/` directories. When both files exist, `apibot.config.*` takes precedence over `explorbot.config.*`.
84
+ If you don't have a web `explorbot.config.js`, run `npx explorbot api init`. It asks for your base endpoint, spec, and a one-line description of the API, then writes a standalone `apibot.config.js` (with an `ai` and `api` section) plus `output/` and `knowledge/` directories. When both files exist, `apibot.config.*` takes precedence over `explorbot.config.*`.
85
85
 
86
86
  ## Your first run
87
87
 
@@ -99,6 +99,30 @@ npx explorbot api test output/plans/users.md
99
99
 
100
100
  Curler runs the scenarios and prints how many passed and failed.
101
101
 
102
+ ### Covering many endpoints
103
+
104
+ `api explore` runs the whole loop for you: plan, test, re-plan. Given one endpoint it plans in every style.
105
+
106
+ ```bash
107
+ npx explorbot api explore /users
108
+ ```
109
+
110
+ The endpoint may be a pattern. `*` stands for one path segment, and a pattern also covers the paths below it, so `/users` and `/users/*` both cover `/users/{id}`. Quote it, or your shell will try to expand it first.
111
+
112
+ ```bash
113
+ npx explorbot api explore '/projects/acme/*'
114
+ ```
115
+
116
+ Explorbot explores collections, not raw paths: `/users/{id}` and `/users/{id}/posts` fold into `/users`, whose spec lookup brings them along anyway. When a pattern matches several collections the planning styles spread across them, one style per collection, so covering a whole API stays one plan per collection rather than one per style.
117
+
118
+ Pass `/` to take every collection in the spec. The path parameters have to come from somewhere, so put them in the base endpoint.
119
+
120
+ ```bash
121
+ npx explorbot api explore / --endpoint https://api.example.com/v2/acme
122
+ ```
123
+
124
+ If a parameter is left with no value the run stops and names it, rather than sending requests to a literal `{project_id}`. Collections whose own parameters no pattern can fill, like `/analytics/stats/{kind}`, are listed and skipped.
125
+
102
126
  ## Output files
103
127
 
104
128
  | Output | Location | What it is |
@@ -179,6 +179,7 @@ Each agent takes its own model and system prompt.
179
179
  | `rerunner` | Heals failing steps when re-running generated tests |
180
180
  | `analyst` | Writes the end-of-session markdown report |
181
181
  | `fisherman` | Prepares test data through API requests |
182
+ | `scout` | Retrieves relevant documentation for the Planner |
182
183
  | `chief` | API test planning |
183
184
  | `curler` | API test execution |
184
185
 
@@ -206,7 +207,7 @@ agents: {
206
207
  | `beforeHook` | `Hook \| HookPatternMap` | Code to run before agent execution |
207
208
  | `afterHook` | `Hook \| HookPatternMap` | Code to run after agent execution |
208
209
 
209
- Some agents take extra options: `pilot` accepts `stepsToReview` (recent steps reviewed per check, default 5); `planner` accepts `styles` (see [Planning styles](#planning-styles)); `rerunner` accepts `healLimit` (max heal attempts, default 3) and `recipes` (custom heal recipes, see [Rerunning Tests](../web-testing/rerun.md)). Researcher and Historian options are documented below.
210
+ Some agents take extra options: `pilot` accepts `stepsToReview` (recent steps reviewed per check, default 5); `planner` accepts `styles` (see [Planning styles](#planning-styles)) and `docsWeight` (share of scenarios grounded in documentation when Scout is enabled, default 70); `scout` accepts `dirs` (see [Scout agent](#scout-agent)); `rerunner` accepts `healLimit` (max heal attempts, default 3) and `recipes` (custom heal recipes, see [Rerunning Tests](../web-testing/rerun.md)). Researcher and Historian options are documented below.
210
211
 
211
212
  See [Agent hooks](../web-testing/hooks.md) for hook configuration.
212
213
 
@@ -257,6 +258,31 @@ See [AI providers](../basics/providers.md) for recommended models and provider s
257
258
 
258
259
  Fisherman prepares test data over the API before a scenario runs, and can also answer questions about data that already exists without creating or changing anything. Pilot reaches this read-only capability through its `askApi(question)` tool, calling it to check whether suitable data is already there — or to get the exact name or id of an existing record — before deciding whether to create anything through `precondition()`. In replicate mode, where Fisherman learns the API by watching browser traffic instead of reading a spec, the read endpoints it can query come from successful GET requests observed in the browser, alongside the write endpoints already captured from XHR traffic. The endpoint list shown to the model names only the path and its query-parameter names, never their values; the underlying capture on disk holds the full request URL and headers — what write captures already hold — but no response body.
259
260
 
261
+ ### Scout agent
262
+
263
+ Scout retrieves documentation relevant to the page being planned and hands it to the Planner as a `<docs_context>` block, so scenarios can be grounded in what the application documents say. It is opt-in and needs documentation collected beforehand:
264
+
265
+ ```javascript
266
+ ai: {
267
+ agents: {
268
+ scout: {
269
+ enabled: true, // Opt in — Scout never runs without this
270
+ dirs: ['docs'], // Extra markdown directories to search, beyond the spec bundle
271
+ },
272
+ planner: {
273
+ docsWeight: 70, // Roughly 70% of scenarios exercise documented behavior, the rest explore beyond it
274
+ },
275
+ },
276
+ },
277
+ ```
278
+
279
+ | Option | Type | Description |
280
+ |--------|------|-------------|
281
+ | `enabled` | `boolean` | Turn Scout on. Default: off. |
282
+ | `dirs` | `string[]` | Markdown directories added to the corpus, resolved relative to the project |
283
+
284
+ The corpus combines the [application spec](../workflow/application-spec.md) bundle (`--spec` / `EXPLORBOT_SPEC` / `dirs.spec`, set by `explorbot docs collect`) with the `dirs` above. Scout scans it with the same `bash` + `readFile` tools Captain uses: the corpus is loaded into an in-memory sandbox and the model itself runs `rg` (or `grep` — whichever is installed) to explore it. One of the two must be on PATH — Scout fails loudly when neither is found. Pages already injected for the current URL as `<application_spec>` are excluded from the Scout corpus, so the two blocks never duplicate each other. Files under `dirs` that carry no page URL are listed as hand-written notes for Scout to inspect when they are relevant.
285
+
260
286
  ## Playwright settings
261
287
 
262
288
  ### Browser selection
@@ -512,6 +538,7 @@ export default {
512
538
  quartermaster: { /* ... */ },
513
539
  historian: { /* ... */ },
514
540
  fisherman: { /* ... */ },
541
+ scout: { enabled: true, dirs: ['docs'] }, // Documentation retrieval for the Planner
515
542
  rerunner: { /* ... */ },
516
543
  analyst: { /* ... */ },
517
544
  },