explorbot 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +1 -1
  2. package/bin/explorbot-cli.ts +26 -8
  3. package/boat/api-tester/src/cli.ts +17 -0
  4. package/boat/api-tester/src/config.ts +4 -2
  5. package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
  6. package/boat/doc-collector/src/ai/documentarian.ts +61 -31
  7. package/boat/doc-collector/src/cli.ts +14 -1
  8. package/boat/doc-collector/src/config.ts +4 -2
  9. package/boat/prima/bin/prima-cli.ts +0 -0
  10. package/boat/prima/src/activity-line.ts +33 -0
  11. package/boat/prima/src/cli.ts +127 -86
  12. package/boat/prima/src/envelope.ts +102 -52
  13. package/boat/prima/src/prima.ts +567 -128
  14. package/boat/prima/src/pw-parser.ts +11 -1
  15. package/boat/prima/src/pw-registry.ts +4 -5
  16. package/boat/prima/src/session-log.ts +126 -0
  17. package/dist/bin/explorbot-cli.js +26 -8
  18. package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
  19. package/dist/boat/api-tester/src/cli.js +17 -0
  20. package/dist/boat/api-tester/src/config.js +4 -2
  21. package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
  22. package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
  23. package/dist/boat/doc-collector/src/cli.js +14 -1
  24. package/dist/boat/doc-collector/src/config.js +4 -2
  25. package/dist/boat/prima/src/activity-line.js +30 -0
  26. package/dist/boat/prima/src/cli.js +109 -77
  27. package/dist/boat/prima/src/envelope.js +94 -44
  28. package/dist/boat/prima/src/prima.js +533 -119
  29. package/dist/boat/prima/src/pw-parser.js +13 -1
  30. package/dist/boat/prima/src/pw-registry.js +4 -5
  31. package/dist/boat/prima/src/session-log.js +108 -0
  32. package/dist/package.json +3 -2
  33. package/dist/rules/navigator/verification-actions.md +20 -0
  34. package/dist/src/action-result.d.ts +7 -0
  35. package/dist/src/action-result.js +4 -0
  36. package/dist/src/action.d.ts +2 -0
  37. package/dist/src/action.js +41 -2
  38. package/dist/src/ai/captain/web-mode.js +6 -3
  39. package/dist/src/ai/captain.js +2 -0
  40. package/dist/src/ai/navigator.d.ts +34 -0
  41. package/dist/src/ai/navigator.js +237 -181
  42. package/dist/src/ai/pilot.d.ts +7 -0
  43. package/dist/src/ai/pilot.js +90 -2
  44. package/dist/src/ai/provider.d.ts +2 -2
  45. package/dist/src/ai/provider.js +14 -23
  46. package/dist/src/ai/rerunner.js +2 -1
  47. package/dist/src/ai/researcher/cache.d.ts +2 -0
  48. package/dist/src/ai/researcher/cache.js +10 -2
  49. package/dist/src/ai/researcher.js +3 -2
  50. package/dist/src/ai/rules.js +17 -10
  51. package/dist/src/ai/session-analyst.js +2 -0
  52. package/dist/src/ai/task-agent.js +4 -1
  53. package/dist/src/ai/tester.d.ts +6 -3
  54. package/dist/src/ai/tester.js +50 -46
  55. package/dist/src/ai/tools.d.ts +14 -0
  56. package/dist/src/ai/tools.js +117 -37
  57. package/dist/src/commands/config-command.d.ts +51 -0
  58. package/dist/src/commands/config-command.js +117 -0
  59. package/dist/src/commands/index.js +2 -0
  60. package/dist/src/config.d.ts +9 -1
  61. package/dist/src/config.js +53 -4
  62. package/dist/src/execution-controller.d.ts +2 -0
  63. package/dist/src/execution-controller.js +6 -0
  64. package/dist/src/explorbot.d.ts +2 -1
  65. package/dist/src/explorbot.js +7 -2
  66. package/dist/src/explorer.js +2 -3
  67. package/dist/src/playwright-recorder.js +30 -0
  68. package/dist/src/remote.d.ts +55 -0
  69. package/dist/src/remote.js +235 -0
  70. package/dist/src/reporter.d.ts +1 -0
  71. package/dist/src/reporter.js +7 -1
  72. package/dist/src/state-manager.d.ts +2 -1
  73. package/dist/src/state-manager.js +3 -1
  74. package/dist/src/stats.d.ts +1 -0
  75. package/dist/src/stats.js +1 -0
  76. package/dist/src/test-plan.d.ts +3 -0
  77. package/dist/src/test-plan.js +26 -0
  78. package/dist/src/utils/aria.d.ts +2 -8
  79. package/dist/src/utils/aria.js +69 -40
  80. package/dist/src/utils/html.js +1 -0
  81. package/dist/src/utils/logger.d.ts +7 -1
  82. package/dist/src/utils/logger.js +32 -0
  83. package/dist/src/utils/page-readiness.js +18 -1
  84. package/dist/src/utils/url-matcher.js +3 -0
  85. package/dist/src/utils/web-element.d.ts +2 -0
  86. package/dist/src/utils/web-element.js +8 -0
  87. package/dist/src/utils/web-sandbox.d.ts +1 -1
  88. package/dist/src/utils/web-sandbox.js +2 -3
  89. package/docs/api-testing/basics.md +90 -0
  90. package/docs/api-testing/planning.md +57 -0
  91. package/docs/api-testing/running-tests.md +55 -0
  92. package/docs/assets/cloud-report.png +0 -0
  93. package/docs/assets/html-report.png +0 -0
  94. package/docs/assets/langfuse-trace.png +0 -0
  95. package/docs/assets/successful-explore-run.png +0 -0
  96. package/docs/basics/getting-started.md +140 -0
  97. package/docs/basics/prerequisites.md +63 -0
  98. package/docs/basics/providers.md +362 -0
  99. package/docs/basics/running.md +78 -0
  100. package/docs/contributing/ai-integration-tests.md +57 -0
  101. package/docs/contributing/contributing.md +90 -0
  102. package/docs/contributing/demo-videos.md +36 -0
  103. package/docs/contributing/npm-package.md +138 -0
  104. package/docs/contributing/observability.md +227 -0
  105. package/docs/contributing/regression-tests.md +103 -0
  106. package/docs/contributing/testing.md +95 -0
  107. package/docs/doc-collection/basics.md +128 -0
  108. package/docs/doc-collection/crawling.md +67 -0
  109. package/docs/doc-collection/interactive-mode.md +99 -0
  110. package/docs/index.json +87 -0
  111. package/docs/reference/commands.md +997 -0
  112. package/docs/reference/configuration.md +569 -0
  113. package/docs/reference/scripting.md +303 -0
  114. package/docs/reference/websocket.md +50 -0
  115. package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
  116. package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
  117. package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
  118. package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
  119. package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
  120. package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
  121. package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
  122. package/docs/superpowers/specs/2026-08-18-prima-false-verdicts.md +159 -0
  123. package/docs/web-testing/agents.md +158 -0
  124. package/docs/web-testing/automated-tests.md +134 -0
  125. package/docs/web-testing/basics.md +91 -0
  126. package/docs/web-testing/customization.md +131 -0
  127. package/docs/web-testing/hooks.md +238 -0
  128. package/docs/web-testing/page-interaction.md +84 -0
  129. package/docs/web-testing/planner.md +122 -0
  130. package/docs/web-testing/rerun.md +164 -0
  131. package/docs/web-testing/researcher.md +380 -0
  132. package/docs/workflow/agentic-usage.md +233 -0
  133. package/docs/workflow/application-spec.md +73 -0
  134. package/docs/workflow/ci.md +202 -0
  135. package/docs/workflow/knowledge.md +310 -0
  136. package/docs/workflow/planning-styles.md +67 -0
  137. package/docs/workflow/reporting.md +133 -0
  138. package/docs/workflow/test-plans.md +90 -0
  139. package/package.json +3 -2
  140. package/rules/navigator/verification-actions.md +20 -0
  141. package/src/action-result.ts +11 -0
  142. package/src/action.ts +43 -3
  143. package/src/ai/captain/web-mode.ts +6 -3
  144. package/src/ai/captain.ts +3 -0
  145. package/src/ai/navigator.ts +255 -186
  146. package/src/ai/pilot.ts +104 -2
  147. package/src/ai/provider.ts +14 -24
  148. package/src/ai/rerunner.ts +2 -1
  149. package/src/ai/researcher/cache.ts +12 -2
  150. package/src/ai/researcher.ts +3 -2
  151. package/src/ai/rules.ts +17 -10
  152. package/src/ai/session-analyst.ts +2 -0
  153. package/src/ai/task-agent.ts +3 -1
  154. package/src/ai/tester.ts +52 -45
  155. package/src/ai/tools.ts +136 -37
  156. package/src/commands/config-command.ts +146 -0
  157. package/src/commands/index.ts +2 -0
  158. package/src/config.ts +60 -5
  159. package/src/execution-controller.ts +8 -0
  160. package/src/explorbot.ts +7 -3
  161. package/src/explorer.ts +2 -2
  162. package/src/playwright-recorder.ts +23 -0
  163. package/src/remote.ts +244 -0
  164. package/src/reporter.ts +7 -1
  165. package/src/state-manager.ts +6 -2
  166. package/src/stats.ts +1 -0
  167. package/src/test-plan.ts +29 -0
  168. package/src/utils/aria.ts +65 -45
  169. package/src/utils/html.ts +1 -0
  170. package/src/utils/logger.ts +33 -2
  171. package/src/utils/page-readiness.ts +24 -1
  172. package/src/utils/url-matcher.ts +3 -0
  173. package/src/utils/web-element.ts +9 -0
  174. package/src/utils/web-sandbox.ts +3 -4
package/src/ai/pilot.ts CHANGED
@@ -7,8 +7,9 @@ import { ConfigParser } from '../config.ts';
7
7
  import type Explorer from '../explorer.ts';
8
8
  import type { PlaywrightRecorder } from '../playwright-recorder.ts';
9
9
  import type { StateManager } from '../state-manager.ts';
10
+ import { Stats } from '../stats.ts';
10
11
  import { type Test, TestResult } from '../test-plan.ts';
11
- import { collectInteractiveNodes, detectFocusArea, extractFocusedElement } from '../utils/aria.ts';
12
+ import { collectInteractiveNodes, detectFocusArea } from '../utils/aria.ts';
12
13
  import { ErrorPageError } from '../utils/error-page.ts';
13
14
  import { createDebug, tag } from '../utils/logger.ts';
14
15
 
@@ -22,6 +23,7 @@ import type { Provider } from './provider.ts';
22
23
  import type { Researcher } from './researcher.ts';
23
24
  import { capabilityGroundingRule, dataProtectionRules } from './rules.ts';
24
25
  import { isInteractive } from './task-agent.ts';
26
+ import { withdrawVisionTools } from './tools.ts';
25
27
 
26
28
  const CHECK_TOOLS = ['verify', 'see', 'research', 'context'];
27
29
  const META_TOOLS = ['record', 'reset', 'stop', 'finish'];
@@ -561,6 +563,97 @@ export class Pilot implements Agent {
561
563
  return text;
562
564
  }
563
565
 
566
+ async settleExpectations(task: Test, finalState?: ActionResult): Promise<SettledExpectation[]> {
567
+ let image: string | null = null;
568
+ if (finalState?.screenshot && this.provider.hasVision()) image = `data:image/png;base64,${finalState.screenshot.toString('base64')}`;
569
+
570
+ const decided = (text: string): 'passed' | 'failed' => {
571
+ if (task.hasAchievedAny() && !task.getRemainingExpectations().includes(text)) return 'passed';
572
+ return 'failed';
573
+ };
574
+
575
+ let undecided = task.expected.filter((text) => !task.getCheckedExpectations().includes(text));
576
+ if (image) undecided = task.expected;
577
+ if (!undecided.length) return task.expected.map((text) => ({ text, status: decided(text) }));
578
+
579
+ const schema = z.object({
580
+ outcomes: z.array(
581
+ z.object({
582
+ expectation: z.string().describe('The expected outcome, repeated exactly as it was given'),
583
+ status: z.enum(['passed', 'failed', 'unverified', 'contradiction']).describe('passed = the evidence shows it happened, failed = the evidence shows it did not, unverified = the run never established either way, contradiction = the picture and the run disagree'),
584
+ evidence: z.string().nullable().describe('What settled it. For a contradiction, what each side shows. Null when there is nothing to add'),
585
+ })
586
+ ),
587
+ });
588
+
589
+ let pageEvidence = '';
590
+ if (image) {
591
+ pageEvidence = dedent`
592
+ A screenshot of the whole page as the run left it is attached. It is the proof: an outcome is satisfied
593
+ when the page shows it to somebody looking at it. The log only says what the run did.
594
+
595
+ Not finding something in the picture is not by itself a disagreement. Report "contradiction" only when
596
+ the picture shows something incompatible with what the run claims — a list visibly empty, an error where
597
+ a result was expected, the old value still displayed, a control visibly disabled. When you simply cannot
598
+ make it out, say "unverified" and name what you could not find.
599
+
600
+ The picture covers the full page, but not the inside of a region that scrolls on its own, and not the
601
+ state of the page before the run ended. An outcome established earlier stays established even when the
602
+ page has moved past it, and that is not a contradiction.
603
+ `;
604
+ }
605
+
606
+ const userContent = dedent`
607
+ A test run has finished. Decide, for each expected outcome, what the run established about it.
608
+
609
+ <expected_outcomes>
610
+ ${undecided.map((text) => `- ${text}`).join('\n')}
611
+ </expected_outcomes>
612
+
613
+ <run_log>
614
+ ${task.notesToString() || 'No steps recorded.'}
615
+ </run_log>
616
+
617
+ ${pageEvidence}
618
+
619
+ The log is written in the tester's own words, so an outcome can be satisfied by a step that describes it
620
+ differently. Judge by what the steps show happened, not by whether the wording matches.
621
+ Choose "unverified" only when the evidence neither shows the outcome happening nor shows it failing —
622
+ that is a statement about the run, not about the application.
623
+ `;
624
+
625
+ const settle = (content: any, model: any) =>
626
+ this.provider
627
+ .generateObject([{ role: 'user' as const, content }], schema, model, {
628
+ agentName: 'pilot',
629
+ telemetry: { functionId: 'pilot.settleExpectations' },
630
+ })
631
+ .catch(() => null);
632
+
633
+ let response = null;
634
+ if (image) {
635
+ const seen = [
636
+ { type: 'text', text: userContent },
637
+ { type: 'file', mediaType: 'image/png', data: image },
638
+ ];
639
+ response = await settle(seen, this.provider.getVisionModel());
640
+ if (!response) {
641
+ Stats.visionDisabled = true;
642
+ tag('warning').log('⚠️ Vision model could not judge the outcomes. Settling them from the run log instead.');
643
+ }
644
+ }
645
+
646
+ if (!response) response = await settle(userContent, this.provider.getAgenticModel('pilot'));
647
+
648
+ const judged = new Map((response?.object?.outcomes || []).map((outcome: any) => [outcome.expectation, outcome]));
649
+ return task.expected.map((text) => {
650
+ if (!undecided.includes(text)) return { text, status: decided(text) };
651
+ const outcome = judged.get(text) as { status: SettledStatus; evidence?: string } | undefined;
652
+ if (!outcome) return { text, status: 'unverified' as SettledStatus };
653
+ return { text, status: outcome.status || 'unverified', evidence: outcome.evidence };
654
+ });
655
+ }
656
+
564
657
  private formatExpectations(task: Test): string {
565
658
  const checked = task.getCheckedExpectations();
566
659
  const remaining = task.getRemainingExpectations();
@@ -620,6 +713,7 @@ export class Pilot implements Agent {
620
713
  if (xpathCheck) planning.xpathCheck = xpathCheck;
621
714
  if (learnExperience) planning.learnExperience = learnExperience;
622
715
  if (askUser) planning.askUser = askUser;
716
+ withdrawVisionTools(planning);
623
717
  return planning;
624
718
  }
625
719
 
@@ -703,7 +797,7 @@ export class Pilot implements Agent {
703
797
  lines.push(`url: ${state.url}`);
704
798
  lines.push(`title: ${state.title || 'unknown'}`);
705
799
 
706
- const focused = extractFocusedElement(state.ariaSnapshot);
800
+ const focused = state.focusedElement;
707
801
  if (focused) {
708
802
  const valuePart = focused.value ? ` (value: "${focused.value}")` : '';
709
803
  lines.push(`focused: ${focused.role} "${focused.name}"${valuePart}`);
@@ -1065,3 +1159,11 @@ export class Pilot implements Agent {
1065
1159
  `;
1066
1160
  }
1067
1161
  }
1162
+
1163
+ export type SettledStatus = 'passed' | 'failed' | 'unverified' | 'contradiction';
1164
+
1165
+ export interface SettledExpectation {
1166
+ text: string;
1167
+ status: SettledStatus;
1168
+ evidence?: string;
1169
+ }
@@ -4,7 +4,7 @@ import { NodeSDK } from '@opentelemetry/sdk-node';
4
4
  import { generateObject, generateText, isStepCount, registerTelemetry } from 'ai';
5
5
  import type { ModelMessage } from 'ai';
6
6
  import { clearActivity, setActivity } from '../activity.ts';
7
- import type { AIConfig } from '../config.js';
7
+ import { type AIConfig, configuredModels, modelName as getModelName } from '../config.js';
8
8
  import { executionController } from '../execution-controller.ts';
9
9
  import { Observability } from '../observability.ts';
10
10
  import { Stats } from '../stats.ts';
@@ -88,10 +88,6 @@ export class Provider {
88
88
  this.initLangfuse();
89
89
  }
90
90
 
91
- private getModelName(model: any): string {
92
- return model?.modelId || model?.model || 'unknown';
93
- }
94
-
95
91
  async validateConnection(): Promise<void> {
96
92
  try {
97
93
  await generateText({
@@ -120,13 +116,13 @@ export class Provider {
120
116
  return this.config.agenticModel || this.config.model;
121
117
  }
122
118
 
119
+ getVisionModel(): any {
120
+ return this.config.visionModel;
121
+ }
122
+
123
123
  getConfiguredModels(): Record<string, string> {
124
- const models: Record<string, string> = { model: this.getModelName(this.config.model) };
125
- if (this.config.agenticModel) models.agenticModel = this.getModelName(this.config.agenticModel);
126
- if (this.config.visionModel) models.visionModel = this.getModelName(this.config.visionModel);
127
- for (const [agent, agentConfig] of Object.entries(this.config.agents || {})) {
128
- if (agentConfig?.model) models[agent] = this.getModelName(agentConfig.model);
129
- }
124
+ const models: Record<string, string> = {};
125
+ for (const [role, model] of Object.entries(configuredModels(this.config))) models[role] = model.name;
130
126
  return models;
131
127
  }
132
128
 
@@ -223,11 +219,7 @@ export class Provider {
223
219
  }
224
220
 
225
221
  private initLangfuse() {
226
- const langfuseConfig = this.config.langfuse;
227
- const publicKey = langfuseConfig?.publicKey || process.env.LANGFUSE_PUBLIC_KEY;
228
- const secretKey = langfuseConfig?.secretKey || process.env.LANGFUSE_SECRET_KEY;
229
- const baseUrl = langfuseConfig?.baseUrl || process.env.LANGFUSE_BASE_URL || process.env.LANGFUSE_HOST;
230
- const enabled = langfuseConfig?.enabled ?? Boolean(publicKey && secretKey);
222
+ const { enabled, publicKey, secretKey, baseUrl } = this.config.langfuse || {};
231
223
 
232
224
  if (!enabled || !publicKey || !secretKey) {
233
225
  return;
@@ -316,7 +308,7 @@ export class Provider {
316
308
  }
317
309
 
318
310
  async chat(messages: ModelMessage[], model: any, options: any = {}): Promise<any> {
319
- const modelName = this.getModelName(model);
311
+ const modelName = getModelName(model);
320
312
  setActivity(`🤖 Asking ${modelName}`, 'ai');
321
313
  promptLog(`Using model: ${modelName}`);
322
314
 
@@ -326,6 +318,7 @@ export class Provider {
326
318
  try {
327
319
  const response = await withRetry(async () => {
328
320
  const result = await generateText({ messages, ...config });
321
+ this.recordUsage(options.agentName || 'unknown', modelName, result.usage);
329
322
  if (!result.text) {
330
323
  debugLog(result);
331
324
  if (result.finishReason === 'length') {
@@ -342,8 +335,6 @@ export class Provider {
342
335
  clearActivity();
343
336
  responseLog(response.text);
344
337
 
345
- this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
346
-
347
338
  return response;
348
339
  } catch (error: any) {
349
340
  clearActivity();
@@ -361,7 +352,7 @@ export class Provider {
361
352
  }
362
353
 
363
354
  async generateWithTools(messages: ModelMessage[], model: any, tools: any, options: any = {}): Promise<any> {
364
- const modelName = this.getModelName(model);
355
+ const modelName = getModelName(model);
365
356
  setActivity(`🤖 Asking ${modelName} with dynamic tools`, 'ai');
366
357
  promptLog(`Using model: ${modelName}`);
367
358
 
@@ -378,6 +369,7 @@ export class Provider {
378
369
  try {
379
370
  const response = await withRetry(async () => {
380
371
  const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages, ...config, abortSignal: signal }), config.timeout || 30000)) as any;
372
+ this.recordUsage(options.agentName || 'unknown', modelName, result.usage);
381
373
  const hasToolCall = (result.toolCalls?.length || 0) > 0;
382
374
  if (!result.text && !hasToolCall && result.finishReason === 'length') {
383
375
  throw new ContextLengthError('AI response empty: output truncated at maxTokens. Increase maxOutputTokens in config or use a model with higher output capacity.');
@@ -397,8 +389,6 @@ export class Provider {
397
389
 
398
390
  responseLog(response.text);
399
391
 
400
- this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
401
-
402
392
  return response;
403
393
  } catch (error: any) {
404
394
  clearActivity();
@@ -420,7 +410,7 @@ export class Provider {
420
410
 
421
411
  async generateObject(messages: ModelMessage[], schema: any, model?: any, options: any = {}): Promise<any> {
422
412
  const modelToUse = model || this.config.model;
423
- const modelName = this.getModelName(modelToUse);
413
+ const modelName = getModelName(modelToUse);
424
414
  setActivity(`🤖 Asking ${modelName} for structured output`, 'ai');
425
415
  promptLog(`Using model: ${modelName}`);
426
416
 
@@ -622,7 +612,7 @@ export class Provider {
622
612
  clearActivity();
623
613
  responseLog(response.text);
624
614
 
625
- this.recordUsage('vision', this.getModelName(this.config.visionModel), response.usage);
615
+ this.recordUsage('vision', getModelName(this.config.visionModel), response.usage);
626
616
 
627
617
  return response;
628
618
  } catch (error: any) {
@@ -24,7 +24,7 @@ import type { Navigator } from './navigator.ts';
24
24
  import { Provider } from './provider.ts';
25
25
  import { actionRule, locatorRule, sectionContextRule } from './rules.ts';
26
26
  import { TaskAgent } from './task-agent.ts';
27
- import { createCodeceptJSTools } from './tools.ts';
27
+ import { createCodeceptJSTools, withdrawVisionTools } from './tools.ts';
28
28
 
29
29
  const debugLog = createDebug('explorbot:rerunner');
30
30
 
@@ -361,6 +361,7 @@ export class Rerunner extends TaskAgent implements Agent {
361
361
  },
362
362
  }),
363
363
  };
364
+ withdrawVisionTools(tools);
364
365
 
365
366
  const conversation = this.provider.startConversation(this.getHealSystemPrompt(), 'rerunner');
366
367
  conversation.addUserText(this.getHealUserPrompt(failedCode, error));
@@ -4,6 +4,7 @@ import { Worker } from 'node:worker_threads';
4
4
  import { outputPath } from '../../config.ts';
5
5
  import { TTLCache } from '../../utils/cache.ts';
6
6
  import { computeHtmlFingerprint } from '../../utils/html-diff.ts';
7
+ import { tag } from '../../utils/logger.ts';
7
8
  import { debugLog } from './mixin.ts';
8
9
 
9
10
  const CACHE_TTL_MS = 6 * 60 * 60 * 1000; // 6 hours
@@ -15,6 +16,14 @@ const memoryCache = new TTLCache<string>(CACHE_TTL_MS);
15
16
 
16
17
  let fingerprintWorker: Worker | null = null;
17
18
 
19
+ export function researchPath(hash: string): string {
20
+ return outputPath('research', `${hash}.md`);
21
+ }
22
+
23
+ export function reportResearch(hash: string, text: string): void {
24
+ tag('data').log('research', { path: researchPath(hash), hash, content: text });
25
+ }
26
+
18
27
  function getStatesDir(): string {
19
28
  return outputPath('states');
20
29
  }
@@ -35,7 +44,7 @@ export function getCachedResearch(hash: string): string {
35
44
  if (!hash) return '';
36
45
  const cached = memoryCache.get(hash);
37
46
  if (cached !== undefined) return cached;
38
- const researchFile = outputPath('research', `${hash}.md`);
47
+ const researchFile = researchPath(hash);
39
48
  if (!existsSync(researchFile)) return '';
40
49
  const stats = statSync(researchFile);
41
50
  if (Date.now() - stats.mtimeMs > CACHE_TTL_MS) return '';
@@ -46,7 +55,7 @@ export function getCachedResearch(hash: string): string {
46
55
 
47
56
  export function getPreviousResearch(hash: string): string {
48
57
  if (!hash) return '';
49
- const researchFile = outputPath('research', `${hash}.md`);
58
+ const researchFile = researchPath(hash);
50
59
  if (!existsSync(researchFile)) return '';
51
60
  return readFileSync(researchFile, 'utf8');
52
61
  }
@@ -57,6 +66,7 @@ export function saveResearch(hash: string, text: string, combinedHtml?: string):
57
66
  if (!existsSync(researchDir)) mkdirSync(researchDir, { recursive: true });
58
67
  writeFileSync(researchFile, text);
59
68
  memoryCache.set(hash, text);
69
+ reportResearch(hash, text);
60
70
  debugLog(`Research saved to ${researchFile}`);
61
71
 
62
72
  if (combinedHtml) {
@@ -19,7 +19,7 @@ import { annotatePageElements } from '../utils/web-annotate.ts';
19
19
  import type { Agent, AgentDeps } from './agent.js';
20
20
  import type { Navigator } from './navigator.ts';
21
21
  import { ContextLengthError, type Provider } from './provider.js';
22
- import { findSimilarResearch, getCachedResearch, saveResearch } from './researcher/cache.ts';
22
+ import { findSimilarResearch, getCachedResearch, reportResearch, saveResearch } from './researcher/cache.ts';
23
23
  import { type CoordinateMethods, WithCoordinates } from './researcher/coordinates.ts';
24
24
  import { type DeepAnalysisMethods, WithDeepAnalysis } from './researcher/deep-analysis.ts';
25
25
  import { detectFocusedSection, hasFocusedSection, markSectionAsFocused, pickDefaultFocusedSection } from './researcher/focus.ts';
@@ -102,7 +102,8 @@ export class Researcher extends ResearcherBase implements Agent {
102
102
  const cached = getCachedResearch(stateHash);
103
103
  if (cached) {
104
104
  debugLog('Previous research result found');
105
- return `!! UI MAP IS CACHED AND MAY NOT REPRESENT CURRENT STATE; REFRESH RESEARCH IF YOU NOTICE ISSUES !!\n\n${cached}`;
105
+ reportResearch(stateHash, cached);
106
+ return cached;
106
107
  }
107
108
  }
108
109
 
package/src/ai/rules.ts CHANGED
@@ -4,13 +4,18 @@ export const recommendedCodeceptCommands = ['I.click', 'I.type', 'I.fillField',
4
4
 
5
5
  const locatorPriorityRule = dedent`
6
6
  <locator_priority>
7
+ When the page context shows the element a ref, such as [ref=e14], there is no locator to select: click it with clickRef
8
+ and that ref. A ref names one exact element, so it never matches the wrong one and never has to be narrowed. Everything
9
+ below is for elements the context gives no ref for.
10
+
7
11
  Use the following priority when selecting locators:
8
12
 
9
13
  1. ARIA locators (first choice) - target browser's accessibility tree, most reliable
10
14
  Use JSON format: { "role": "button", "text": "Login" }
11
- Best for: buttons, links, inputs, form controls, dropdowns, checkboxes, radio buttons
15
+ Copy role and text VERBATIM from the ARIA snapshot or UI map — never guess the pair.
16
+ If the element is absent from the snapshot, do not invent one; use text or CSS instead.
12
17
 
13
- 2. Text locators (second choice) - use only when text is unique on the page
18
+ 2. Text locators (second choice) - exact visible text, use only when unique on the page
14
19
  Example: 'Login', 'Submit', 'Username'
15
20
  Skip if the same text appears multiple times on the page
16
21
 
@@ -26,10 +31,10 @@ const locatorPriorityRule = dedent`
26
31
 
27
32
  const contextSimplificationRule = dedent`
28
33
  <context_simplification>
29
- When container is available from UI map sections:
30
- - Text + container is simplest and PREFERRED: I.click('Save', '.modal')
31
- - ARIA + container for disambiguation: I.click({"role":"button","text":"Save"}, '.modal')
32
- - ALWAYS use context parameter unless locator is XPath or unique ID
34
+ - Add a container when the target may match several elements, or the UI map gives a verified
35
+ section container: I.click('Save', '.modal')
36
+ - Skip the container when the locator is already unique (XPath, unique ID, unique text)
37
+ - A wrong or stale container guarantees failure: always add one fallback command WITHOUT a container
33
38
  - No need for complex ARIA when container narrows scope sufficiently
34
39
  </context_simplification>
35
40
  `;
@@ -59,7 +64,7 @@ const locatorStrategyRule = dedent`
59
64
 
60
65
  <good_aria_locator_example>
61
66
  { "role": "button", "text": "Login" },
62
- { "role": "input", "text": "Name" },
67
+ { "role": "textbox", "text": "Name" },
63
68
  { "role": "link", "text": "Forgot your password?" },
64
69
  { "role": "link", "text": "Sign Up" },
65
70
  { "role": "button", "text": "Sign In" },
@@ -105,7 +110,7 @@ const locatorStrategyRule = dedent`
105
110
  </xpath_rules>
106
111
 
107
112
  <good locator example>
108
- 'div[role=input][placeholder="Name"]'
113
+ 'input[placeholder="Name"]'
109
114
  '[aria-label="Name"]'
110
115
  'form#user_form input[name="name"]'
111
116
  '#content-top #user_name'
@@ -245,7 +250,8 @@ export const unexpectedPopupRule = dedent`
245
250
 
246
251
  export const sectionContextRule = dedent`
247
252
  <section_context_rule>
248
- Context parameter is DEFAULT for all interactions. ALWAYS use container from UI map sections unless locator is XPath or unique ID.
253
+ Use a container from UI map sections when the target may match several elements. A container that is
254
+ wrong or stale guarantees failure, so prefer a verified one and keep a containerless fallback.
249
255
 
250
256
  1. Identify which section contains the target element
251
257
  2. Get the Context Locator from that section in the UI map
@@ -263,9 +269,10 @@ export const sectionContextRule = dedent`
263
269
  For CSS locators - prepend section context:
264
270
  - I.click('.main button.submit') // instead of I.click('button.submit')
265
271
 
266
- Only omit context when:
272
+ Omit context when the locator already resolves to one element:
267
273
  - Locator is XPath (already includes path context)
268
274
  - Locator is a unique ID (#specific-element)
275
+ - Text or ARIA that appears only once on the page
269
276
  </section_context_rule>
270
277
 
271
278
  ${unexpectedPopupRule}
@@ -4,6 +4,7 @@ import dedent from 'dedent';
4
4
  import { outputPath } from '../config.ts';
5
5
  import { Stats } from '../stats.ts';
6
6
  import type { Test } from '../test-plan.ts';
7
+ import { tag } from '../utils/logger.ts';
7
8
  import type { Agent } from './agent.ts';
8
9
  import type { Provider } from './provider.ts';
9
10
 
@@ -117,6 +118,7 @@ export class SessionAnalyst implements Agent {
117
118
  const dir = path.dirname(filePath);
118
119
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
119
120
  writeFileSync(filePath, markdown);
121
+ tag('data').log('report', { path: filePath, content: markdown });
120
122
  return filePath;
121
123
  }
122
124
 
@@ -1,5 +1,6 @@
1
1
  import type { ActionResult } from '../action-result.js';
2
2
  import type { ExplorbotConfig } from '../config.ts';
3
+ import { executionController } from '../execution-controller.ts';
3
4
  import type { ExperienceTracker } from '../experience-tracker.js';
4
5
  import type Explorer from '../explorer.ts';
5
6
  import type { KnowledgeTracker } from '../knowledge-tracker.js';
@@ -12,7 +13,8 @@ import type { Provider } from './provider.js';
12
13
  import { Quartermaster } from './quartermaster.js';
13
14
 
14
15
  export function isInteractive(): boolean {
15
- return process.env.INK_RUNNING === 'true';
16
+ if (process.env.INK_RUNNING === 'true') return true;
17
+ return executionController.hasInputCallback();
16
18
  }
17
19
 
18
20
  function createNullProxy<T extends object>(): T {