explorbot 0.2.2 → 0.2.4

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 (214) hide show
  1. package/README.md +1 -1
  2. package/bin/explorbot-cli.ts +59 -38
  3. package/boat/api-tester/src/apibot.ts +4 -2
  4. package/boat/api-tester/src/cli.ts +2 -2
  5. package/boat/api-tester/src/config.ts +43 -10
  6. package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
  7. package/boat/doc-collector/src/ai/documentarian.ts +61 -31
  8. package/boat/doc-collector/src/cli.ts +1 -0
  9. package/boat/doc-collector/src/config.ts +4 -2
  10. package/boat/doc-collector/src/docs-renderer.ts +18 -4
  11. package/boat/doc-collector/src/state-diagram.ts +61 -14
  12. package/boat/prima/bin/prima-cli.ts +5 -0
  13. package/boat/prima/package.json +16 -0
  14. package/boat/prima/src/activity-line.ts +33 -0
  15. package/boat/prima/src/cli.ts +251 -0
  16. package/boat/prima/src/envelope.ts +169 -0
  17. package/boat/prima/src/prima.ts +1124 -0
  18. package/boat/prima/src/pw-parser.ts +27 -0
  19. package/boat/prima/src/pw-registry.ts +74 -0
  20. package/boat/prima/src/session-log.ts +126 -0
  21. package/dist/bin/explorbot-cli.js +51 -32
  22. package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
  23. package/dist/boat/api-tester/src/apibot.js +3 -2
  24. package/dist/boat/api-tester/src/cli.js +2 -2
  25. package/dist/boat/api-tester/src/config.js +40 -10
  26. package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
  27. package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
  28. package/dist/boat/doc-collector/src/cli.js +1 -0
  29. package/dist/boat/doc-collector/src/config.js +4 -2
  30. package/dist/boat/doc-collector/src/docs-renderer.js +17 -3
  31. package/dist/boat/doc-collector/src/state-diagram.js +57 -13
  32. package/dist/boat/prima/bin/prima-cli.js +4 -0
  33. package/dist/boat/prima/src/activity-line.js +30 -0
  34. package/dist/boat/prima/src/cli.js +220 -0
  35. package/dist/boat/prima/src/envelope.js +142 -0
  36. package/dist/boat/prima/src/prima.js +1031 -0
  37. package/dist/boat/prima/src/pw-parser.js +30 -0
  38. package/dist/boat/prima/src/pw-registry.js +65 -0
  39. package/dist/boat/prima/src/session-log.js +108 -0
  40. package/dist/models.json +3 -0
  41. package/dist/package.json +8 -3
  42. package/dist/rules/navigator/verification-actions.md +20 -0
  43. package/dist/src/action-result.d.ts +7 -0
  44. package/dist/src/action-result.js +4 -0
  45. package/dist/src/action.d.ts +7 -2
  46. package/dist/src/action.js +41 -6
  47. package/dist/src/ai/captain/mixin.js +3 -4
  48. package/dist/src/ai/captain/web-mode.js +7 -4
  49. package/dist/src/ai/captain.js +2 -0
  50. package/dist/src/ai/navigator.d.ts +11 -0
  51. package/dist/src/ai/navigator.js +21 -12
  52. package/dist/src/ai/pilot.d.ts +4 -0
  53. package/dist/src/ai/pilot.js +48 -2
  54. package/dist/src/ai/planner.d.ts +1 -0
  55. package/dist/src/ai/planner.js +6 -0
  56. package/dist/src/ai/provider.js +2 -2
  57. package/dist/src/ai/rerunner.js +2 -1
  58. package/dist/src/ai/researcher.js +2 -2
  59. package/dist/src/ai/rules.js +17 -10
  60. package/dist/src/ai/task-agent.js +5 -2
  61. package/dist/src/ai/tester.d.ts +1 -0
  62. package/dist/src/ai/tester.js +33 -20
  63. package/dist/src/ai/tools.d.ts +14 -0
  64. package/dist/src/ai/tools.js +113 -36
  65. package/dist/src/application-spec-contract.d.ts +8 -0
  66. package/dist/src/application-spec-contract.js +8 -0
  67. package/dist/src/application-spec.d.ts +15 -0
  68. package/dist/src/application-spec.js +71 -0
  69. package/dist/src/browser-server.d.ts +12 -6
  70. package/dist/src/browser-server.js +74 -19
  71. package/dist/src/commands/clean-command.js +2 -7
  72. package/dist/src/commands/init-command.d.ts +5 -0
  73. package/dist/src/commands/init-command.js +119 -1
  74. package/dist/src/commands/navigate-command.js +1 -1
  75. package/dist/src/commands/research-command.js +1 -1
  76. package/dist/src/commands/sites-command.d.ts +6 -0
  77. package/dist/src/commands/sites-command.js +23 -0
  78. package/dist/src/components/InitWizard.d.ts +10 -0
  79. package/dist/src/components/InitWizard.js +133 -0
  80. package/dist/src/components/InputReadline.d.ts +1 -0
  81. package/dist/src/components/InputReadline.js +7 -4
  82. package/dist/src/config.d.ts +25 -5
  83. package/dist/src/config.js +158 -40
  84. package/dist/src/execution-controller.d.ts +2 -0
  85. package/dist/src/execution-controller.js +6 -0
  86. package/dist/src/explorbot.d.ts +11 -1
  87. package/dist/src/explorbot.js +27 -6
  88. package/dist/src/explorer.d.ts +4 -1
  89. package/dist/src/explorer.js +42 -9
  90. package/dist/src/global-config.d.ts +22 -0
  91. package/dist/src/global-config.js +117 -0
  92. package/dist/src/knowledge-tracker.d.ts +5 -1
  93. package/dist/src/knowledge-tracker.js +14 -1
  94. package/dist/src/playwright-recorder.js +30 -0
  95. package/dist/src/remote.d.ts +54 -0
  96. package/dist/src/remote.js +229 -0
  97. package/dist/src/reporter.d.ts +1 -0
  98. package/dist/src/reporter.js +7 -1
  99. package/dist/src/state-manager.d.ts +2 -1
  100. package/dist/src/stats.d.ts +1 -0
  101. package/dist/src/stats.js +1 -0
  102. package/dist/src/test-plan.d.ts +2 -0
  103. package/dist/src/test-plan.js +7 -0
  104. package/dist/src/utils/aria.d.ts +2 -8
  105. package/dist/src/utils/aria.js +69 -40
  106. package/dist/src/utils/cli-name.js +6 -2
  107. package/dist/src/utils/html.js +1 -0
  108. package/dist/src/utils/logger.d.ts +6 -0
  109. package/dist/src/utils/logger.js +24 -0
  110. package/dist/src/utils/page-readiness.js +18 -1
  111. package/dist/src/utils/test-files.js +1 -2
  112. package/dist/src/utils/url-matcher.d.ts +1 -0
  113. package/dist/src/utils/url-matcher.js +12 -0
  114. package/dist/src/utils/web-element.d.ts +2 -0
  115. package/dist/src/utils/web-element.js +8 -0
  116. package/dist/src/utils/web-sandbox.d.ts +1 -1
  117. package/dist/src/utils/web-sandbox.js +2 -3
  118. package/docs/api-testing/basics.md +90 -0
  119. package/docs/api-testing/planning.md +57 -0
  120. package/docs/api-testing/running-tests.md +55 -0
  121. package/docs/assets/cloud-report.png +0 -0
  122. package/docs/assets/html-report.png +0 -0
  123. package/docs/assets/langfuse-trace.png +0 -0
  124. package/docs/assets/successful-explore-run.png +0 -0
  125. package/docs/basics/getting-started.md +140 -0
  126. package/docs/basics/prerequisites.md +63 -0
  127. package/docs/basics/providers.md +362 -0
  128. package/docs/basics/running.md +78 -0
  129. package/docs/contributing/ai-integration-tests.md +57 -0
  130. package/docs/contributing/contributing.md +90 -0
  131. package/docs/contributing/demo-videos.md +36 -0
  132. package/docs/contributing/npm-package.md +138 -0
  133. package/docs/contributing/observability.md +227 -0
  134. package/docs/contributing/regression-tests.md +103 -0
  135. package/docs/contributing/testing.md +95 -0
  136. package/docs/doc-collection/basics.md +128 -0
  137. package/docs/doc-collection/crawling.md +67 -0
  138. package/docs/doc-collection/interactive-mode.md +99 -0
  139. package/docs/index.json +86 -0
  140. package/docs/reference/commands.md +994 -0
  141. package/docs/reference/configuration.md +569 -0
  142. package/docs/reference/scripting.md +303 -0
  143. package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
  144. package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
  145. package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
  146. package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
  147. package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
  148. package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
  149. package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
  150. package/docs/web-testing/agents.md +158 -0
  151. package/docs/web-testing/automated-tests.md +134 -0
  152. package/docs/web-testing/basics.md +91 -0
  153. package/docs/web-testing/customization.md +131 -0
  154. package/docs/web-testing/hooks.md +238 -0
  155. package/docs/web-testing/page-interaction.md +84 -0
  156. package/docs/web-testing/planner.md +122 -0
  157. package/docs/web-testing/rerun.md +164 -0
  158. package/docs/web-testing/researcher.md +380 -0
  159. package/docs/workflow/agentic-usage.md +233 -0
  160. package/docs/workflow/application-spec.md +73 -0
  161. package/docs/workflow/ci.md +202 -0
  162. package/docs/workflow/knowledge.md +310 -0
  163. package/docs/workflow/planning-styles.md +67 -0
  164. package/docs/workflow/reporting.md +133 -0
  165. package/docs/workflow/test-plans.md +90 -0
  166. package/models.json +3 -0
  167. package/package.json +8 -3
  168. package/rules/navigator/verification-actions.md +20 -0
  169. package/src/action-result.ts +11 -0
  170. package/src/action.ts +47 -7
  171. package/src/ai/captain/mixin.ts +3 -3
  172. package/src/ai/captain/web-mode.ts +7 -4
  173. package/src/ai/captain.ts +3 -0
  174. package/src/ai/navigator.ts +26 -15
  175. package/src/ai/pilot.ts +53 -2
  176. package/src/ai/planner.ts +7 -0
  177. package/src/ai/provider.ts +2 -4
  178. package/src/ai/rerunner.ts +2 -1
  179. package/src/ai/researcher.ts +2 -2
  180. package/src/ai/rules.ts +17 -10
  181. package/src/ai/task-agent.ts +4 -2
  182. package/src/ai/tester.ts +32 -18
  183. package/src/ai/tools.ts +132 -36
  184. package/src/application-spec-contract.ts +10 -0
  185. package/src/application-spec.ts +87 -0
  186. package/src/browser-server.ts +74 -19
  187. package/src/commands/clean-command.ts +1 -6
  188. package/src/commands/init-command.ts +146 -1
  189. package/src/commands/navigate-command.ts +1 -1
  190. package/src/commands/research-command.ts +1 -1
  191. package/src/commands/sites-command.ts +27 -0
  192. package/src/components/InitWizard.tsx +166 -0
  193. package/src/components/InputReadline.tsx +8 -4
  194. package/src/config.ts +177 -43
  195. package/src/execution-controller.ts +8 -0
  196. package/src/explorbot.ts +34 -7
  197. package/src/explorer.ts +47 -9
  198. package/src/global-config.ts +148 -0
  199. package/src/knowledge-tracker.ts +17 -1
  200. package/src/playwright-recorder.ts +23 -0
  201. package/src/remote.ts +238 -0
  202. package/src/reporter.ts +7 -1
  203. package/src/state-manager.ts +2 -1
  204. package/src/stats.ts +1 -0
  205. package/src/test-plan.ts +9 -0
  206. package/src/utils/aria.ts +65 -45
  207. package/src/utils/cli-name.ts +5 -2
  208. package/src/utils/html.ts +1 -0
  209. package/src/utils/logger.ts +24 -1
  210. package/src/utils/page-readiness.ts +24 -1
  211. package/src/utils/test-files.ts +1 -2
  212. package/src/utils/url-matcher.ts +13 -0
  213. package/src/utils/web-element.ts +9 -0
  214. package/src/utils/web-sandbox.ts +3 -4
@@ -3,12 +3,13 @@ import dedent from 'dedent';
3
3
  import { z } from 'zod';
4
4
  import { ActionResult } from '../action-result.js';
5
5
  import { normalizeUrl } from '../state-manager.js';
6
+ import { renderAssertion } from "../playwright-recorder.js";
6
7
  import { extractCodeBlocks } from '../utils/code-extractor.js';
7
8
  import { HooksRunner } from "../utils/hooks-runner.js";
8
9
  import { createDebug, pluralize, tag } from '../utils/logger.js';
9
10
  import { loop, pause } from '../utils/loop.js';
10
11
  import { RulesLoader } from "../utils/rules-loader.js";
11
- import { extractStatePath } from '../utils/url-matcher.js';
12
+ import { extractStatePath, matchesNavigationUrl } from '../utils/url-matcher.js';
12
13
  import { Researcher } from "./researcher.js";
13
14
  import { actionRule, locatorRule, unexpectedPopupRule } from './rules.js';
14
15
  import { isInteractive } from './task-agent.js';
@@ -118,7 +119,7 @@ class Navigator {
118
119
  return false;
119
120
  }
120
121
  const currentUrl = this.getComparableCurrentUrl(stateManager, expectedUrl);
121
- return normalizeUrl(currentUrl) === normalizeUrl(expectedUrl);
122
+ return matchesNavigationUrl(expectedUrl, currentUrl);
122
123
  }
123
124
  async visit(url) {
124
125
  return this.visitOnce(url);
@@ -167,11 +168,13 @@ class Navigator {
167
168
  }
168
169
  }
169
170
  async resolveState(message, actionResult, opts) {
171
+ if (!this.provider)
172
+ throw new Error('AI-assisted recovery is unavailable: no AI model is configured.');
170
173
  tag('info').log('AI Navigator resolving state at', actionResult.url);
171
174
  debugLog('Resolution message:', message);
172
175
  const action = opts?.action ?? this.explorer.action();
173
176
  const expectedUrl = opts?.expectedUrl;
174
- const knowledge = this.knowledgeTracker.renderRelevantKnowledge(actionResult);
177
+ const knowledge = this.knowledgeTracker.renderRelevantContext(actionResult);
175
178
  let experience = '';
176
179
  if (!actionResult.isInsideIframe) {
177
180
  const successful = this.experienceTracker.getSuccessfulExperience(actionResult);
@@ -328,16 +331,19 @@ class Navigator {
328
331
  // Navigation did not reach 'load' state within timeout; continue and verify URL
329
332
  }
330
333
  }
334
+ if (attemptOk)
335
+ opts?.onAttempt?.({ code: codeBlock });
331
336
  if (!attemptOk) {
332
337
  const raw = action.lastError?.message || 'attempt failed';
333
338
  const firstMeaningful = raw.split('\n').find((l) => l.trim() && !l.trim().startsWith('at ')) || raw;
334
339
  const shortErr = firstMeaningful.replace(/\s+/g, ' ').trim().slice(0, 220);
335
340
  batchFailures.push({ code: codeBlock, error: shortErr });
341
+ opts?.onAttempt?.({ code: codeBlock, error: shortErr });
336
342
  }
337
343
  if (expectedUrl) {
338
344
  if (page) {
339
345
  try {
340
- await page.waitForURL((url) => normalizeUrl(url.pathname) === normalizeUrl(expectedUrl), { timeout: 5000 });
346
+ await page.waitForURL((url) => matchesNavigationUrl(expectedUrl, `${url.pathname}${url.search}${url.hash}`), { timeout: 5000 });
341
347
  }
342
348
  catch {
343
349
  // URL did not transition to expectedUrl within timeout
@@ -345,7 +351,7 @@ class Navigator {
345
351
  }
346
352
  const freshState = await this.explorer.capture();
347
353
  const currentUrl = /^https?:\/\//i.test(expectedUrl) ? freshState.fullUrl || freshState.url || '' : freshState.url || '';
348
- const urlMatches = this.isSameExpectedOrigin(expectedUrl, action.stateManager) && normalizeUrl(currentUrl) === normalizeUrl(expectedUrl);
354
+ const urlMatches = this.isSameExpectedOrigin(expectedUrl, action.stateManager) && matchesNavigationUrl(expectedUrl, currentUrl);
349
355
  const stateChanged = freshState.getStateHash() !== actionResult.getStateHash();
350
356
  resolved = urlMatches && stateChanged;
351
357
  if (!resolved && attemptOk) {
@@ -562,7 +568,7 @@ class Navigator {
562
568
  tag('operation').log(`Reusing cached verification: ${cachedVerification ? 'PASS' : 'FAIL'}`);
563
569
  return { verified: cachedVerification, successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
564
570
  }
565
- const knowledge = this.knowledgeTracker.renderRelevantKnowledge(actionResult);
571
+ const knowledge = this.knowledgeTracker.renderRelevantContext(actionResult);
566
572
  let experience = '';
567
573
  if (!actionResult.isInsideIframe) {
568
574
  experience = this.experienceTracker.renderExperienceTocFor(actionResult);
@@ -623,6 +629,7 @@ class Navigator {
623
629
  const tools = this.buildExperienceTools();
624
630
  let codeBlocks = [];
625
631
  const successfulCodes = [];
632
+ const results = [];
626
633
  const assertionSteps = [];
627
634
  const action = this.explorer.action();
628
635
  let failures = 0;
@@ -655,6 +662,8 @@ class Navigator {
655
662
  }
656
663
  await action.exitIframe();
657
664
  const verified = await action.attempt(codeBlock, message);
665
+ const proof = action.assertionSteps.map(renderAssertion).filter(Boolean);
666
+ results.push({ code: codeBlock, passed: verified, proof });
658
667
  if (verified) {
659
668
  tag('success').log('Verification passed');
660
669
  successfulCodes.push(codeBlock);
@@ -663,11 +672,6 @@ class Navigator {
663
672
  else {
664
673
  failures++;
665
674
  }
666
- const target = Math.min(codeBlocks.length, this.verifyAttempts);
667
- const majorityNeeded = Math.floor(target / 2) + 1;
668
- if (successfulCodes.length >= majorityNeeded || failures > target - majorityNeeded) {
669
- stop();
670
- }
671
675
  }, {
672
676
  maxAttempts: this.verifyAttempts,
673
677
  observability: {
@@ -686,9 +690,14 @@ class Navigator {
686
690
  let verified = successfulCodes.length >= majorityNeeded;
687
691
  if (alreadyVerified)
688
692
  verified = true;
693
+ const inexpressible = !alreadyVerified && totalAttempted === 0;
694
+ if (inexpressible) {
695
+ tag('warning').log('No assertion could express this claim');
696
+ return { verified: false, inexpressible, results, successfulCodes, assertionSteps, totalAttempted };
697
+ }
689
698
  actionResult.addVerification(message, verified);
690
699
  this.stateManager.updateState(actionResult);
691
- return { verified, successfulCodes, assertionSteps, totalAttempted };
700
+ return { verified, inexpressible, results, successfulCodes, assertionSteps, totalAttempted };
692
701
  }
693
702
  checkAlreadyVerified(aiResponse, actionResult) {
694
703
  const verifiedMatch = aiResponse.match(/ALREADY_VERIFIED:\s*(.+)/i);
@@ -40,6 +40,10 @@ export declare class Pilot implements Agent {
40
40
  planTest(task: Test, currentState: ActionResult): Promise<string>;
41
41
  reviewNewPage(task: Test, currentState: ActionResult, testerConversation: Conversation): Promise<string>;
42
42
  analyzeProgress(task: Test, currentState: ActionResult, testerConversation: Conversation): Promise<string>;
43
+ settleExpectations(task: Test): Promise<Array<{
44
+ text: string;
45
+ status: 'passed' | 'failed' | 'unverified';
46
+ }>>;
43
47
  formatExpectations(task: Test): string;
44
48
  sendToPilot(userText: string, functionId: string, opts: {
45
49
  tools?: boolean;
@@ -4,13 +4,14 @@ import { z } from 'zod';
4
4
  import { ActionResult } from "../action-result.js";
5
5
  import { ConfigParser } from "../config.js";
6
6
  import { TestResult } from "../test-plan.js";
7
- import { collectInteractiveNodes, detectFocusArea, extractFocusedElement } from "../utils/aria.js";
7
+ import { collectInteractiveNodes, detectFocusArea } from "../utils/aria.js";
8
8
  import { ErrorPageError } from "../utils/error-page.js";
9
9
  import { createDebug, tag } from "../utils/logger.js";
10
10
  const debugLog = createDebug('explorbot:pilot');
11
11
  import { truncateJson } from "../utils/strings.js";
12
12
  import { capabilityGroundingRule, dataProtectionRules } from "./rules.js";
13
13
  import { isInteractive } from "./task-agent.js";
14
+ import { withdrawVisionTools } from "./tools.js";
14
15
  const CHECK_TOOLS = ['verify', 'see', 'research', 'context'];
15
16
  const META_TOOLS = ['record', 'reset', 'stop', 'finish'];
16
17
  export class Pilot {
@@ -484,6 +485,50 @@ export class Pilot {
484
485
  }
485
486
  return text;
486
487
  }
488
+ async settleExpectations(task) {
489
+ const undecided = task.expected.filter((text) => !task.getCheckedExpectations().includes(text));
490
+ const decided = (text) => {
491
+ if (task.hasAchievedAny() && !task.getRemainingExpectations().includes(text))
492
+ return 'passed';
493
+ return 'failed';
494
+ };
495
+ if (!undecided.length)
496
+ return task.expected.map((text) => ({ text, status: decided(text) }));
497
+ const schema = z.object({
498
+ outcomes: z.array(z.object({
499
+ expectation: z.string().describe('The expected outcome, repeated exactly as it was given'),
500
+ status: z.enum(['passed', 'failed', 'unverified']).describe('passed = the log shows it happened, failed = the log shows it did not, unverified = the run never established either way'),
501
+ })),
502
+ });
503
+ const userContent = dedent `
504
+ A test run has finished. Decide, for each expected outcome, what the run established about it.
505
+
506
+ <expected_outcomes>
507
+ ${undecided.map((text) => `- ${text}`).join('\n')}
508
+ </expected_outcomes>
509
+
510
+ <run_log>
511
+ ${task.notesToString() || 'No steps recorded.'}
512
+ </run_log>
513
+
514
+ The log is written in the tester's own words, so an outcome can be satisfied by a step that describes it
515
+ differently. Judge by what the steps show happened, not by whether the wording matches.
516
+ Choose "unverified" only when the log neither shows the outcome happening nor shows it failing —
517
+ that is a statement about the run, not about the application.
518
+ `;
519
+ const response = await this.provider
520
+ .generateObject([{ role: 'user', content: userContent }], schema, this.provider.getAgenticModel('pilot'), {
521
+ agentName: 'pilot',
522
+ telemetry: { functionId: 'pilot.settleExpectations' },
523
+ })
524
+ .catch(() => null);
525
+ const judged = new Map((response?.object?.outcomes || []).map((outcome) => [outcome.expectation, outcome.status]));
526
+ return task.expected.map((text) => {
527
+ if (!undecided.includes(text))
528
+ return { text, status: decided(text) };
529
+ return { text, status: judged.get(text) || 'unverified' };
530
+ });
531
+ }
487
532
  formatExpectations(task) {
488
533
  const checked = task.getCheckedExpectations();
489
534
  const remaining = task.getRemainingExpectations();
@@ -547,6 +592,7 @@ export class Pilot {
547
592
  planning.learnExperience = learnExperience;
548
593
  if (askUser)
549
594
  planning.askUser = askUser;
595
+ withdrawVisionTools(planning);
550
596
  return planning;
551
597
  }
552
598
  buildPreconditionTool(task) {
@@ -624,7 +670,7 @@ export class Pilot {
624
670
  const lines = [];
625
671
  lines.push(`url: ${state.url}`);
626
672
  lines.push(`title: ${state.title || 'unknown'}`);
627
- const focused = extractFocusedElement(state.ariaSnapshot);
673
+ const focused = state.focusedElement;
628
674
  if (focused) {
629
675
  const valuePart = focused.value ? ` (value: "${focused.value}")` : '';
630
676
  lines.push(`focused: ${focused.role} "${focused.name}"${valuePart}`);
@@ -50,6 +50,7 @@ export declare class Planner extends PlannerBase implements Agent {
50
50
  provider: Provider;
51
51
  stateManager: StateManager;
52
52
  experienceTracker: ExperienceTracker;
53
+ knowledgeTracker: AgentDeps['knowledgeTracker'];
53
54
  MIN_TASKS: number;
54
55
  MAX_TASKS: number;
55
56
  currentPlan: Plan | null;
@@ -41,6 +41,7 @@ export class Planner extends PlannerBase {
41
41
  provider;
42
42
  stateManager;
43
43
  experienceTracker;
44
+ knowledgeTracker;
44
45
  MIN_TASKS = 3;
45
46
  MAX_TASKS = 12;
46
47
  currentPlan = null;
@@ -56,6 +57,7 @@ export class Planner extends PlannerBase {
56
57
  this.researcher = researcher;
57
58
  this.stateManager = deps.stateManager;
58
59
  this.experienceTracker = deps.stateManager.getExperienceTracker();
60
+ this.knowledgeTracker = deps.knowledgeTracker;
59
61
  }
60
62
  setFisherman(fisherman) {
61
63
  this.fisherman = fisherman;
@@ -368,6 +370,10 @@ export class Planner extends PlannerBase {
368
370
  ${plannerResearch}
369
371
  </page_research>
370
372
  `);
373
+ const applicationContext = this.knowledgeTracker.renderApplicationSpec(state);
374
+ if (applicationContext) {
375
+ conversation.addUserText(applicationContext);
376
+ }
371
377
  conversation.addUserText(dedent `
372
378
  ${this.buildApproach(style)}
373
379
 
@@ -290,6 +290,7 @@ export class Provider {
290
290
  try {
291
291
  const response = await withRetry(async () => {
292
292
  const result = await generateText({ messages, ...config });
293
+ this.recordUsage(options.agentName || 'unknown', modelName, result.usage);
293
294
  if (!result.text) {
294
295
  debugLog(result);
295
296
  if (result.finishReason === 'length') {
@@ -304,7 +305,6 @@ export class Provider {
304
305
  }, this.getRetryOptions(options));
305
306
  clearActivity();
306
307
  responseLog(response.text);
307
- this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
308
308
  return response;
309
309
  }
310
310
  catch (error) {
@@ -340,6 +340,7 @@ export class Provider {
340
340
  try {
341
341
  const response = await withRetry(async () => {
342
342
  const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages, ...config, abortSignal: signal }), config.timeout || 30000));
343
+ this.recordUsage(options.agentName || 'unknown', modelName, result.usage);
343
344
  const hasToolCall = (result.toolCalls?.length || 0) > 0;
344
345
  if (!result.text && !hasToolCall && result.finishReason === 'length') {
345
346
  throw new ContextLengthError('AI response empty: output truncated at maxTokens. Increase maxOutputTokens in config or use a model with higher output capacity.');
@@ -355,7 +356,6 @@ export class Provider {
355
356
  });
356
357
  }
357
358
  responseLog(response.text);
358
- this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
359
359
  return response;
360
360
  }
361
361
  catch (error) {
@@ -21,7 +21,7 @@ import { RulesLoader } from "../utils/rules-loader.js";
21
21
  import { toolExecutionLabel } from "./conversation.js";
22
22
  import { actionRule, locatorRule, sectionContextRule } from "./rules.js";
23
23
  import { TaskAgent } from "./task-agent.js";
24
- import { createCodeceptJSTools } from "./tools.js";
24
+ import { createCodeceptJSTools, withdrawVisionTools } from "./tools.js";
25
25
  const debugLog = createDebug('explorbot:rerunner');
26
26
  export class Rerunner extends TaskAgent {
27
27
  ACTION_TOOLS = ['click', 'pressKey', 'form'];
@@ -322,6 +322,7 @@ export class Rerunner extends TaskAgent {
322
322
  },
323
323
  }),
324
324
  };
325
+ withdrawVisionTools(tools);
325
326
  const conversation = this.provider.startConversation(this.getHealSystemPrompt(), 'rerunner');
326
327
  conversation.addUserText(this.getHealUserPrompt(failedCode, error));
327
328
  await loop(async ({ stop }) => {
@@ -76,7 +76,7 @@ export class Researcher extends ResearcherBase {
76
76
  const cached = getCachedResearch(stateHash);
77
77
  if (cached) {
78
78
  debugLog('Previous research result found');
79
- return `!! UI MAP IS CACHED AND MAY NOT REPRESENT CURRENT STATE; REFRESH RESEARCH IF YOU NOTICE ISSUES !!\n\n${cached}`;
79
+ return cached;
80
80
  }
81
81
  }
82
82
  Stats.researches++;
@@ -379,7 +379,7 @@ export class Researcher extends ResearcherBase {
379
379
  if (!this.actionResult)
380
380
  throw new Error('actionResult is not set');
381
381
  const html = await this.actionResult.combinedHtml();
382
- const knowledge = this.knowledgeTracker.renderRelevantKnowledge(this.actionResult);
382
+ const knowledge = this.knowledgeTracker.renderRelevantContext(this.actionResult);
383
383
  const ariaSnapshot = this.actionResult.getCompactARIA();
384
384
  return dedent `
385
385
  Analyze this web page and provide a comprehensive research report in markdown format.
@@ -2,13 +2,18 @@ import dedent from 'dedent';
2
2
  export const recommendedCodeceptCommands = ['I.click', 'I.type', 'I.fillField', 'I.see', 'I.seeElement'];
3
3
  const locatorPriorityRule = dedent `
4
4
  <locator_priority>
5
+ When the page context shows the element a ref, such as [ref=e14], there is no locator to select: click it with clickRef
6
+ and that ref. A ref names one exact element, so it never matches the wrong one and never has to be narrowed. Everything
7
+ below is for elements the context gives no ref for.
8
+
5
9
  Use the following priority when selecting locators:
6
10
 
7
11
  1. ARIA locators (first choice) - target browser's accessibility tree, most reliable
8
12
  Use JSON format: { "role": "button", "text": "Login" }
9
- Best for: buttons, links, inputs, form controls, dropdowns, checkboxes, radio buttons
13
+ Copy role and text VERBATIM from the ARIA snapshot or UI map — never guess the pair.
14
+ If the element is absent from the snapshot, do not invent one; use text or CSS instead.
10
15
 
11
- 2. Text locators (second choice) - use only when text is unique on the page
16
+ 2. Text locators (second choice) - exact visible text, use only when unique on the page
12
17
  Example: 'Login', 'Submit', 'Username'
13
18
  Skip if the same text appears multiple times on the page
14
19
 
@@ -23,10 +28,10 @@ const locatorPriorityRule = dedent `
23
28
  `;
24
29
  const contextSimplificationRule = dedent `
25
30
  <context_simplification>
26
- When container is available from UI map sections:
27
- - Text + container is simplest and PREFERRED: I.click('Save', '.modal')
28
- - ARIA + container for disambiguation: I.click({"role":"button","text":"Save"}, '.modal')
29
- - ALWAYS use context parameter unless locator is XPath or unique ID
31
+ - Add a container when the target may match several elements, or the UI map gives a verified
32
+ section container: I.click('Save', '.modal')
33
+ - Skip the container when the locator is already unique (XPath, unique ID, unique text)
34
+ - A wrong or stale container guarantees failure: always add one fallback command WITHOUT a container
30
35
  - No need for complex ARIA when container narrows scope sufficiently
31
36
  </context_simplification>
32
37
  `;
@@ -55,7 +60,7 @@ const locatorStrategyRule = dedent `
55
60
 
56
61
  <good_aria_locator_example>
57
62
  { "role": "button", "text": "Login" },
58
- { "role": "input", "text": "Name" },
63
+ { "role": "textbox", "text": "Name" },
59
64
  { "role": "link", "text": "Forgot your password?" },
60
65
  { "role": "link", "text": "Sign Up" },
61
66
  { "role": "button", "text": "Sign In" },
@@ -101,7 +106,7 @@ const locatorStrategyRule = dedent `
101
106
  </xpath_rules>
102
107
 
103
108
  <good locator example>
104
- 'div[role=input][placeholder="Name"]'
109
+ 'input[placeholder="Name"]'
105
110
  '[aria-label="Name"]'
106
111
  'form#user_form input[name="name"]'
107
112
  '#content-top #user_name'
@@ -231,7 +236,8 @@ export const unexpectedPopupRule = dedent `
231
236
  `;
232
237
  export const sectionContextRule = dedent `
233
238
  <section_context_rule>
234
- Context parameter is DEFAULT for all interactions. ALWAYS use container from UI map sections unless locator is XPath or unique ID.
239
+ Use a container from UI map sections when the target may match several elements. A container that is
240
+ wrong or stale guarantees failure, so prefer a verified one and keep a containerless fallback.
235
241
 
236
242
  1. Identify which section contains the target element
237
243
  2. Get the Context Locator from that section in the UI map
@@ -249,9 +255,10 @@ export const sectionContextRule = dedent `
249
255
  For CSS locators - prepend section context:
250
256
  - I.click('.main button.submit') // instead of I.click('button.submit')
251
257
 
252
- Only omit context when:
258
+ Omit context when the locator already resolves to one element:
253
259
  - Locator is XPath (already includes path context)
254
260
  - Locator is a unique ID (#specific-element)
261
+ - Text or ARIA that appears only once on the page
255
262
  </section_context_rule>
256
263
 
257
264
  ${unexpectedPopupRule}
@@ -1,6 +1,9 @@
1
+ import { executionController } from "../execution-controller.js";
1
2
  import { HooksRunner } from "../utils/hooks-runner.js";
2
3
  export function isInteractive() {
3
- return process.env.INK_RUNNING === 'true';
4
+ if (process.env.INK_RUNNING === 'true')
5
+ return true;
6
+ return executionController.hasInputCallback();
4
7
  }
5
8
  function createNullProxy() {
6
9
  return new Proxy({}, {
@@ -49,7 +52,7 @@ export class TaskAgent {
49
52
  return this.provider;
50
53
  }
51
54
  getKnowledge(actionResult) {
52
- return this.getKnowledgeTracker().renderRelevantKnowledge(actionResult);
55
+ return this.getKnowledgeTracker().renderRelevantContext(actionResult);
53
56
  }
54
57
  getExperience(actionResult) {
55
58
  return this.getExperienceTracker().renderExperienceTocFor(actionResult);
@@ -49,6 +49,7 @@ export declare class Tester extends TaskAgent implements Agent {
49
49
  shouldStopForStalledExecution(task: Test, previousState: ActionResult, toolExecutions: any[]): boolean;
50
50
  prepareInstructionsForNextStep(task: Test): Promise<string>;
51
51
  reinjectContextIfNeeded(iteration: number, currentState: ActionResult): Promise<string>;
52
+ interactiveAriaWithRefs(state: ActionResult): Promise<string>;
52
53
  finishTest(task: Test): void;
53
54
  abortStartedTestOnErrorPage(task: Test, actionResult: ActionResult): Promise<{
54
55
  success: boolean;
@@ -8,13 +8,13 @@ import { clearActivity, setActivity } from "../activity.js";
8
8
  import { Observability } from "../observability.js";
9
9
  import { Stats } from "../stats.js";
10
10
  import { TestResult } from "../test-plan.js";
11
- import { detectFocusArea, extractFocusedElement } from "../utils/aria.js";
11
+ import { compactAriaSnapshot, detectFocusArea } from "../utils/aria.js";
12
12
  import { ErrorPageError, isErrorPage } from "../utils/error-page.js";
13
13
  import { createDebug, tag } from "../utils/logger.js";
14
14
  import { loop } from "../utils/loop.js";
15
15
  import { actionRule, capabilityGroundingRule, dataProtectionRules, focusedElementRule, formRequirementsRule, locatorRule, multipleTabsRule, sectionContextRule } from "./rules.js";
16
16
  import { TaskAgent } from "./task-agent.js";
17
- import { createCodeceptJSTools, createIframeTools } from "./tools.js";
17
+ import { createCodeceptJSTools, createIframeTools, withdrawVisionTools } from "./tools.js";
18
18
  const debugLog = createDebug('explorbot:tester');
19
19
  const SAMPLE_FILES_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '../../assets/sample-files');
20
20
  const SAMPLE_FILES = {
@@ -93,7 +93,7 @@ export class Tester extends TaskAgent {
93
93
  const requestStore = this.requestStore;
94
94
  requestStore.clear();
95
95
  const offFailedRequest = requestStore.onFailedRequest((r) => {
96
- task.addNote(`Network error: ${r.method} ${r.path} → ${r.status}`, TestResult.FAILED);
96
+ task.addObservation(`Network error: ${r.method} ${r.path} → ${r.status}`);
97
97
  });
98
98
  const initialState = ActionResult.fromState(state);
99
99
  if (isErrorPage(initialState)) {
@@ -206,6 +206,7 @@ export class Tester extends TaskAgent {
206
206
  if (currentState.isInsideIframe) {
207
207
  Object.assign(tools, createIframeTools(this.toolDeps));
208
208
  }
209
+ withdrawVisionTools(tools);
209
210
  debugLog(`Test ${task.scenario} iteration ${iteration}`);
210
211
  if (this.stateManager.isInDeadLoop()) {
211
212
  task.addNote('Dead loop detected. Stopped');
@@ -351,6 +352,7 @@ export class Tester extends TaskAgent {
351
352
  if (extensions >= this.MAX_EXTENSIONS)
352
353
  break;
353
354
  extensions++;
355
+ this.stalledIterations = 0;
354
356
  tag('info').log(`Pilot extending test (${extensions}/${this.MAX_EXTENSIONS})`);
355
357
  conversation.cleanupTag('page_aria', '...trimmed...', 1);
356
358
  conversation.cleanupTag('page_html', '...trimmed...', 0);
@@ -405,8 +407,7 @@ export class Tester extends TaskAgent {
405
407
  this.stalledIterations++;
406
408
  if (this.stalledIterations < this.MAX_STALLED_ITERATIONS)
407
409
  return false;
408
- task.addNote('No browser progress after repeated attempts on unchanged page', TestResult.FAILED);
409
- task.finish(TestResult.FAILED);
410
+ task.addNote('No further browser progress on unchanged page; requesting final review');
410
411
  return true;
411
412
  }
412
413
  async prepareInstructionsForNextStep(task) {
@@ -424,7 +425,8 @@ export class Tester extends TaskAgent {
424
425
  </rules>
425
426
  `;
426
427
  if (task.getPrintableNotes()) {
427
- outcomeStatus = dedent `
428
+ outcomeStatus += dedent `
429
+
428
430
  Your current log:
429
431
  <notes>
430
432
  ${task.notesToString()}
@@ -445,7 +447,7 @@ export class Tester extends TaskAgent {
445
447
  this.previousStateHash = currentStateHash;
446
448
  let context = '';
447
449
  const focusArea = detectFocusArea(currentState.ariaSnapshot);
448
- const focusedElement = extractFocusedElement(currentState.ariaSnapshot);
450
+ const focusedElement = currentState.focusedElement;
449
451
  if (focusedElement) {
450
452
  const isTextInput = ['textbox', 'combobox', 'searchbox'].includes(focusedElement.role);
451
453
  context += dedent `
@@ -526,7 +528,7 @@ export class Tester extends TaskAgent {
526
528
  </page>
527
529
 
528
530
  <page_aria>
529
- ${currentState.getInteractiveARIA()}
531
+ ${await this.interactiveAriaWithRefs(currentState)}
530
532
  </page_aria>
531
533
  ${uiMapSection}
532
534
 
@@ -561,26 +563,32 @@ export class Tester extends TaskAgent {
561
563
  </page>
562
564
 
563
565
  <page_aria>
564
- ${currentState.getInteractiveARIA()}
566
+ ${await this.interactiveAriaWithRefs(currentState)}
565
567
  </page_aria>
566
568
  `;
567
569
  }
570
+ async interactiveAriaWithRefs(state) {
571
+ const withRefs = await Promise.resolve(this.explorer?.withPage?.((page) => page.locator('body').ariaSnapshot({ mode: 'ai' }))).catch(() => null);
572
+ if (!withRefs)
573
+ return state.getInteractiveARIA();
574
+ return compactAriaSnapshot(withRefs, false);
575
+ }
568
576
  finishTest(task) {
569
- if (!task.hasFinished) {
570
- task.finish(TestResult.FAILED);
577
+ if (!task.result) {
578
+ if (task.hasAchievedAll())
579
+ task.finish(TestResult.PASSED);
580
+ else
581
+ task.finish(TestResult.FAILED);
571
582
  }
572
583
  if (task.isSuccessful) {
573
584
  tag('success').log(`Successful test: ${task.scenario}`);
585
+ return;
574
586
  }
575
- else if (task.isSkipped) {
587
+ if (task.isSkipped) {
576
588
  tag('warning').log(`Skipped test: ${task.scenario}`);
589
+ return;
577
590
  }
578
- else if (task.hasFailed) {
579
- tag('error').log(`Failed test: ${task.scenario}`);
580
- }
581
- else {
582
- tag('warning').log(`Test with no result: ${task.scenario}`);
583
- }
591
+ tag('error').log(`Failed test: ${task.scenario}`);
584
592
  }
585
593
  async abortStartedTestOnErrorPage(task, actionResult) {
586
594
  const error = new ErrorPageError(actionResult.url || task.startUrl || '', actionResult.title, actionResult.httpStatus);
@@ -721,6 +729,9 @@ export class Tester extends TaskAgent {
721
729
  ${task.expected.map((e) => `- ${e}`).join('\n')}
722
730
  </expected_results>
723
731
 
732
+ An expected result counts as settled only when you record it back word for word as it is written above.
733
+ A note in your own wording is a general note and leaves that result unsettled.
734
+
724
735
  Your goal is to perform actions on the web page and verify the expected outcomes.
725
736
  Try to achieve as many goals as possible.
726
737
  If goal is not achievable, log that and skip to next one.
@@ -955,6 +966,9 @@ export class Tester extends TaskAgent {
955
966
  - You unsuccessfully tried multiple iterations and failed
956
967
  - If the expected result was expected to fail, use status="success" instead
957
968
 
969
+ When a note settles one of the expected results, that note must repeat the expected result word
970
+ for word. Paraphrasing it leaves the expected result unsettled and it is reported as unverified.
971
+
958
972
  Example:
959
973
  - record({ notes: ["clicked login button", "login form appeared", "fill credentials"], status: "success" })
960
974
  `,
@@ -1032,8 +1046,7 @@ export class Tester extends TaskAgent {
1032
1046
  this.stalledIterations++;
1033
1047
  if (this.stalledIterations < this.MAX_STALLED_ITERATIONS)
1034
1048
  return false;
1035
- task.addNote('No browser progress after repeated execution errors', TestResult.FAILED);
1036
- task.finish(TestResult.FAILED);
1049
+ task.addNote('No browser progress after repeated execution errors; requesting final review');
1037
1050
  return true;
1038
1051
  }
1039
1052
  async cleanupStartedTest(task) {
@@ -16,6 +16,10 @@ export declare function createCodeceptJSTools({ explorer, stateManager, ai }: To
16
16
  commands: any;
17
17
  explanation: any;
18
18
  }, Record<string, any>, import("@ai-sdk/provider-utils").Context>>;
19
+ clickRef: import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<{
20
+ ref: any;
21
+ element: any;
22
+ }, Record<string, any>, import("@ai-sdk/provider-utils").Context>>;
19
23
  hover: import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<{
20
24
  commands: any;
21
25
  explanation: any;
@@ -49,5 +53,15 @@ export declare function createLearnExperienceTool({ getExperienceTracker, getSta
49
53
  error: string;
50
54
  }, import("@ai-sdk/provider-utils").Context>>;
51
55
  export declare function createAgentTools({ explorer, stateManager, ai, researcher, navigator, supervisor, withExperience }: AgentToolDeps): any;
56
+ export declare function commitNote(activeNote: any, result: TestResult, toolResult: any, action: any): Promise<void>;
57
+ export declare function successToolResult(action: string, data?: Record<string, any>, source?: {
58
+ playwrightGroupId?: string | null;
59
+ assertionSteps?: any[];
60
+ }): Record<string, any>;
52
61
  export declare function isMajorPageChange(pageDiff: PageDiff): boolean;
62
+ export declare function failedToolResult(action: string, message: string, data?: Record<string, any>, error?: Error | null): Promise<Record<string, any>>;
63
+ export declare function withdrawVisionTools(tools: Record<string, any>): void;
64
+ export declare function clickFailureSuggestion(attempts: Array<{
65
+ error?: string;
66
+ }>): string;
53
67
  export {};