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
@@ -14,5 +14,17 @@ export function isFunctionExpression(expr) {
14
14
  return { valid: true };
15
15
  }
16
16
  export function toCodeceptWrapper(expr) {
17
- return `I.usePlaywrightTo('pw', async (playwright) => (${expr.trim()})(playwright))`;
17
+ return `return I.usePlaywrightTo('pw', async (playwright) => (${expr.trim()})(playwright))`;
18
+ }
19
+ export function takePwValue(held) {
20
+ if (held === undefined || held === null)
21
+ return undefined;
22
+ if (typeof held === 'string')
23
+ return held;
24
+ try {
25
+ return JSON.stringify(held, null, 2);
26
+ }
27
+ catch {
28
+ return String(held);
29
+ }
18
30
  }
@@ -19,9 +19,8 @@ export function readDescriptors(dir = registryDir()) {
19
19
  }
20
20
  return descriptors;
21
21
  }
22
- export function selectDescriptor(descriptors, opts) {
23
- const workspaceDir = path.resolve(opts.workspaceDir);
24
- const candidates = descriptors.filter((descriptor) => path.resolve(descriptor.workspaceDir) === workspaceDir);
22
+ export function selectDescriptor(descriptors, opts = {}) {
23
+ const candidates = descriptors;
25
24
  if (!candidates.length)
26
25
  return { candidates };
27
26
  if (opts.title) {
@@ -53,13 +52,13 @@ function parseDescriptor(file) {
53
52
  catch {
54
53
  return null;
55
54
  }
56
- if (!data?.endpoint || !data?.title || !data?.workspaceDir)
55
+ if (!data?.endpoint || !data?.title)
57
56
  return null;
58
57
  return {
59
58
  file,
60
59
  title: data.title,
61
60
  endpoint: data.endpoint,
62
- workspaceDir: data.workspaceDir,
61
+ workspaceDir: data.workspaceDir || '',
63
62
  browserName: data.browser?.browserName || DEFAULT_BROWSER,
64
63
  playwrightLib: data.playwrightLib || '',
65
64
  };
@@ -0,0 +1,108 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { outputPath } from "../../../src/config.js";
4
+ import { safeFilename } from "../../../src/utils/strings.js";
5
+ import { renderEnvelope } from "./envelope.js";
6
+ const EXPECTATION_STATUS = { passed: 'passed', failed: 'failed', unverified: 'skipped' };
7
+ export function sessionsDir() {
8
+ return outputPath('prima', 'sessions');
9
+ }
10
+ export function sessionFile(key) {
11
+ return path.join(sessionsDir(), `${safeFilename(key)}.jsonl`);
12
+ }
13
+ export function latestSessionFile() {
14
+ const dir = sessionsDir();
15
+ if (!existsSync(dir))
16
+ return null;
17
+ const files = readdirSync(dir)
18
+ .filter((name) => name.endsWith('.jsonl'))
19
+ .map((name) => path.join(dir, name))
20
+ .sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
21
+ return files[0] || null;
22
+ }
23
+ export function recordCommand(file, session, envelope, durationMs) {
24
+ mkdirSync(path.dirname(file), { recursive: true });
25
+ if (recordedEndpoint(file) !== session.endpoint)
26
+ writeFileSync(file, jsonLine({ action: 'createRun', endpoint: session.endpoint, params: { title: session.title } }), 'utf-8');
27
+ appendFileSync(file, jsonLine({ action: 'addTest', testId: commandTest(envelope, durationMs) }), 'utf-8');
28
+ }
29
+ export function readSession(file) {
30
+ const entries = readEntries(file);
31
+ const run = entries.find((entry) => entry.action === 'createRun');
32
+ const tests = entries.filter((entry) => entry.action === 'addTest').map((entry) => entry.testId);
33
+ return { title: run?.params?.title || 'prima session', tests };
34
+ }
35
+ function commandTest(envelope, durationMs) {
36
+ let status = 'failed';
37
+ if (envelope.ok)
38
+ status = 'passed';
39
+ const test = {
40
+ rid: envelope.status,
41
+ title: envelope.command,
42
+ suite_title: 'Prima session',
43
+ status,
44
+ file: '<prima>',
45
+ description: [`page: ${envelope.page.url}`, envelope.stepFiles && `artifacts: ${envelope.stepFiles}`].filter(Boolean).join('\n'),
46
+ code: (envelope.used || []).join('\n'),
47
+ steps: commandSteps(envelope),
48
+ logs: renderEnvelope(envelope),
49
+ time: Math.round(durationMs),
50
+ };
51
+ if (envelope.failure)
52
+ test.error = { message: envelope.failure.error };
53
+ return test;
54
+ }
55
+ function commandSteps(envelope) {
56
+ const steps = [];
57
+ for (const step of envelope.steps || []) {
58
+ const entry = { category: 'user', title: step.label, status: 'passed', duration: 0, log: step.proof };
59
+ if (!step.ok) {
60
+ entry.status = 'failed';
61
+ entry.error = step.proof;
62
+ }
63
+ steps.push(entry);
64
+ }
65
+ for (const expectation of envelope.expectations || []) {
66
+ const entry = { category: 'user', title: `expected: ${expectation.text}`, status: EXPECTATION_STATUS[expectation.status], duration: 0 };
67
+ if (expectation.status === 'failed')
68
+ entry.error = 'the run did not reach this outcome';
69
+ steps.push(entry);
70
+ }
71
+ for (const assertion of envelope.assertions || []) {
72
+ const entry = { category: 'user', title: assertion.code, status: 'passed', duration: 0, log: assertion.proof.join('\n') };
73
+ if (!assertion.passed) {
74
+ entry.status = 'failed';
75
+ entry.error = 'the assertion did not hold on the page';
76
+ }
77
+ steps.push(entry);
78
+ }
79
+ return steps;
80
+ }
81
+ function recordedEndpoint(file) {
82
+ const [first] = readEntries(file);
83
+ return first?.endpoint || null;
84
+ }
85
+ function readEntries(file) {
86
+ if (!existsSync(file))
87
+ return [];
88
+ const entries = [];
89
+ for (const line of readFileSync(file, 'utf-8').split('\n')) {
90
+ if (!line.trim())
91
+ continue;
92
+ const entry = parseLine(line);
93
+ if (entry)
94
+ entries.push(entry);
95
+ }
96
+ return entries;
97
+ }
98
+ function parseLine(line) {
99
+ try {
100
+ return JSON.parse(line);
101
+ }
102
+ catch {
103
+ return null;
104
+ }
105
+ }
106
+ function jsonLine(entry) {
107
+ return `${JSON.stringify(entry)}\n`;
108
+ }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explorbot",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
@@ -19,6 +19,7 @@
19
19
  },
20
20
  "files": [
21
21
  "dist/",
22
+ "docs/",
22
23
  "src/**/*.ts",
23
24
  "src/**/*.tsx",
24
25
  "bin/**/*.ts",
@@ -119,7 +120,7 @@
119
120
  "ora-classic": "^5.4.2",
120
121
  "parse5": "^8.0.0",
121
122
  "pixelmatch": "^7.2.0",
122
- "playwright": "^1.60",
123
+ "playwright": "^1.62",
123
124
  "pngjs": "^7.0.0",
124
125
  "react": "^19.1.1",
125
126
  "sambanova-ai-provider": "^1.2.2",
@@ -105,9 +105,29 @@ Checks that page source does NOT contain expected text.
105
105
  I.dontSeeInSource('error-class');
106
106
  </example>
107
107
 
108
+ ### Asserting the state of a control
109
+
110
+ State means disabled, checked, readonly, required, selected, expanded. A presence assertion says
111
+ nothing about it, so assert it with an attribute selector:
112
+
113
+ <example>
114
+ I.seeElement('button[aria-label="Submit"][disabled]');
115
+ I.dontSeeElement('button[aria-label="Submit"][disabled]');
116
+ I.seeElement('input[name="accept"][checked]');
117
+ I.seeElement('button[aria-label="Details"][aria-expanded="true"]');
118
+ </example>
119
+
120
+ Use I.seeElement for the state you expect and I.dontSeeElement for the state you expect to be
121
+ absent — that pair expresses both directions.
122
+
123
+ I.seeAttributesOnElements(<locator>, { disabled: true }) also exists, but only takes a state that
124
+ must be PRESENT and does not resolve reliably against a role/text locator. Prefer the selector form above.
125
+
108
126
  <verification_rules>
109
127
  Be strict in assertions to avoid false positives.
110
128
  Prefer I.seeElement() with ARIA locators - most reliable.
129
+ For a claim about a control's state, assert it with an attribute selector — presence of the element is not evidence of its state.
130
+ If no assertion above can express the claim, say so instead of proposing an assertion that checks something weaker.
111
131
  I.see() and I.dontSee() MUST include context parameter.
112
132
  For input field values, ALWAYS use I.seeInField() — never check value via CSS attribute selectors or I.seeInSource.
113
133
  Prefer text locators (label, name, placeholder) for form fields: I.seeInField('Search', 'value') over I.seeInField('input[name="search"]', 'value').
@@ -24,6 +24,7 @@ interface ActionResultData extends WebPageState {
24
24
  }>;
25
25
  ariaSnapshot?: string | null;
26
26
  ariaSnapshotFile?: string;
27
+ focusedElement?: FocusedElement | null;
27
28
  iframeURL?: string;
28
29
  links?: Link[];
29
30
  }
@@ -69,6 +70,7 @@ export declare class ActionResult implements ActionResultData {
69
70
  snapshotCache: TTLCache<string>;
70
71
  readonly logFile: string | undefined;
71
72
  readonly ariaSnapshotFile: string | undefined;
73
+ readonly focusedElement: FocusedElement | null;
72
74
  _ariaSnapshot: string | null | undefined;
73
75
  _lastExtractedHtml: string | undefined;
74
76
  notes: string[];
@@ -128,4 +130,9 @@ export declare class Diff {
128
130
  get htmlDiff(): HtmlDiffResult | null;
129
131
  calculate(): Promise<void>;
130
132
  }
133
+ export interface FocusedElement {
134
+ role: string;
135
+ name: string;
136
+ value?: string;
137
+ }
131
138
  export {};
@@ -30,6 +30,7 @@ export class ActionResult {
30
30
  snapshotCache = new TTLCache();
31
31
  logFile = undefined;
32
32
  ariaSnapshotFile = undefined;
33
+ focusedElement = null;
33
34
  _ariaSnapshot = undefined;
34
35
  _lastExtractedHtml = undefined;
35
36
  notes = [];
@@ -61,6 +62,9 @@ export class ActionResult {
61
62
  if (data.ariaSnapshotFile !== undefined) {
62
63
  this.ariaSnapshotFile = data.ariaSnapshotFile;
63
64
  }
65
+ if (data.focusedElement !== undefined) {
66
+ this.focusedElement = data.focusedElement;
67
+ }
64
68
  // Store HTML in a private property if provided
65
69
  if (data.html !== undefined) {
66
70
  this._html = data.html;
@@ -15,6 +15,7 @@ declare class Action {
15
15
  name: string;
16
16
  args: any[];
17
17
  }>;
18
+ lastValue: unknown;
18
19
  recorder?: PlaywrightRecorder;
19
20
  recovery: RecoveryRunner;
20
21
  mainDocumentStatus: number | undefined;
@@ -45,6 +46,7 @@ declare class Action {
45
46
  exitIframe(): Promise<void>;
46
47
  getActor(): CodeceptJS.I;
47
48
  getActionResult(): ActionResult | null;
49
+ restorePageTimeout(): void;
48
50
  waitForPageReadiness(page: any): Promise<void>;
49
51
  }
50
52
  export default Action;
@@ -15,6 +15,8 @@ import { safeFilename } from "./utils/strings.js";
15
15
  import { codeceptJSSandbox, hasPlaywrightCommands, playwrightSandbox, sanitizeCodeBlock } from "./utils/web-sandbox.js";
16
16
  const debugLog = createDebug('explorbot:action');
17
17
  const CAPTURE_NAVIGATION_TRANSITION_ATTEMPTS = 3;
18
+ const DEFAULT_ACTION_TIMEOUT = 3000;
19
+ const DEFAULT_PAGE_TIMEOUT = 3000;
18
20
  class Action {
19
21
  actor;
20
22
  stateManager;
@@ -26,6 +28,7 @@ class Action {
26
28
  playwrightHelper;
27
29
  playwrightGroupId = null;
28
30
  assertionSteps = [];
31
+ lastValue;
29
32
  recorder;
30
33
  recovery;
31
34
  mainDocumentStatus = undefined;
@@ -47,6 +50,7 @@ class Action {
47
50
  await this.actor.saveScreenshot(filename);
48
51
  if (currentState)
49
52
  currentState.screenshotFile = filename;
53
+ tag('data').log('screenshot', { path: outputPath('states', filename) });
50
54
  return filename;
51
55
  }
52
56
  catch (err) {
@@ -94,7 +98,10 @@ class Action {
94
98
  const screenshotPath = join(statesDir, filename);
95
99
  screenshotFile = await page
96
100
  ?.screenshot({ path: screenshotPath, fullPage: true })
97
- .then(() => filename)
101
+ .then(() => {
102
+ tag('data').log('screenshot', { path: screenshotPath });
103
+ return filename;
104
+ })
98
105
  .catch((err) => {
99
106
  debugLog('Screenshot failed, continuing without it:', err);
100
107
  return undefined;
@@ -120,9 +127,11 @@ class Action {
120
127
  const iframeSnapshots = await this.captureIframeSnapshots(html);
121
128
  let ariaSnapshot = null;
122
129
  let ariaSnapshotFile = undefined;
130
+ let focusedElement = null;
123
131
  try {
124
132
  const page = this.playwrightHelper.page;
125
133
  ariaSnapshot = await page.locator('body').ariaSnapshot();
134
+ focusedElement = await page.evaluate(readFocusedElement);
126
135
  }
127
136
  catch (err) {
128
137
  debugLog('ARIA snapshot failed:', err instanceof Error ? `${err.message}\n${err.stack}` : err);
@@ -145,6 +154,7 @@ class Action {
145
154
  iframeSnapshots,
146
155
  ariaSnapshot,
147
156
  ariaSnapshotFile,
157
+ focusedElement,
148
158
  iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
149
159
  });
150
160
  this.stateManager.updateState(result, codeBlock);
@@ -272,16 +282,19 @@ class Action {
272
282
  if (!sanitizedCode) {
273
283
  throw new Error('No valid I.* or page.* commands found in code block');
274
284
  }
285
+ this.playwrightHelper?.page?.setDefaultTimeout(this.config.action?.timeout ?? DEFAULT_ACTION_TIMEOUT);
275
286
  if (isPlaywright) {
276
287
  const page = this.playwrightHelper.page;
277
288
  await playwrightSandbox(page, sanitizedCode);
278
289
  await sleep(this.config.action?.delay || 500);
279
290
  }
280
291
  else {
281
- codeceptJSSandbox(this.actor, sanitizedCode);
292
+ const returned = codeceptJSSandbox(this.actor, sanitizedCode);
282
293
  await recorder.add(() => sleep(this.config.action?.delay || 500));
283
294
  await recorder.promise();
295
+ this.lastValue = await returned;
284
296
  }
297
+ this.restorePageTimeout();
285
298
  if (executedSteps.length > 0) {
286
299
  codeString = executedSteps.join('\n');
287
300
  }
@@ -300,6 +313,7 @@ class Action {
300
313
  throw err;
301
314
  }
302
315
  finally {
316
+ this.restorePageTimeout();
303
317
  detachMainDocumentResponse();
304
318
  if (groupId)
305
319
  await this.recorder.endAction();
@@ -346,6 +360,9 @@ class Action {
346
360
  getActionResult() {
347
361
  return this.actionResult;
348
362
  }
363
+ restorePageTimeout() {
364
+ this.playwrightHelper?.page?.setDefaultTimeout(this.config.playwright.timeout ?? DEFAULT_PAGE_TIMEOUT);
365
+ }
349
366
  async waitForPageReadiness(page) {
350
367
  await waitForPageReadiness(page, {
351
368
  timeout: this.config.playwright.waitForTimeout,
@@ -408,3 +425,25 @@ const detachStepLogger = (listener) => {
408
425
  codeceptjs.event.dispatcher.off(codeceptjs.event.step.passed, listener);
409
426
  codeceptjs.event.dispatcher.off(codeceptjs.event.step.failed, listener);
410
427
  };
428
+ const readFocusedElement = () => {
429
+ const el = document.activeElement;
430
+ if (!el || el === document.body)
431
+ return null;
432
+ const tag = el.tagName.toLowerCase();
433
+ const textish = new Set(['text', 'search', 'email', 'password', 'url', 'tel', 'number']);
434
+ let role = el.getAttribute('role') || tag;
435
+ if (tag === 'textarea' || el.isContentEditable)
436
+ role = 'textbox';
437
+ if (tag === 'input' && textish.has(el.type))
438
+ role = 'textbox';
439
+ if (tag === 'select')
440
+ role = 'combobox';
441
+ if (tag === 'a')
442
+ role = 'link';
443
+ const label = el.getAttribute('aria-label') || el.getAttribute('placeholder') || el.labels?.[0]?.textContent || el.textContent || '';
444
+ const focused = { role, name: label.trim().slice(0, 80) };
445
+ const value = el.value ?? el.textContent;
446
+ if (typeof value === 'string' && value)
447
+ focused.value = value.slice(0, 200);
448
+ return focused;
449
+ };
@@ -16,7 +16,7 @@ export function WithWebMode(Base) {
16
16
  navigator: ctx.explorBot.agentNavigator(),
17
17
  });
18
18
  const { see, context, visualClick, learnExperience } = agentTools;
19
- return {
19
+ const tools = {
20
20
  navigate: tool({
21
21
  description: 'Navigate to a URL or page description using AI-powered navigation.',
22
22
  inputSchema: z.object({
@@ -114,11 +114,14 @@ export function WithWebMode(Base) {
114
114
  },
115
115
  }),
116
116
  ...codeceptTools,
117
- see,
118
117
  context,
119
- visualClick,
120
118
  learnExperience,
121
119
  };
120
+ if (see)
121
+ tools.see = see;
122
+ if (visualClick)
123
+ tools.visualClick = visualClick;
124
+ return tools;
122
125
  }
123
126
  webModePrompt() {
124
127
  return dedent `
@@ -15,6 +15,7 @@ import { WithWebMode } from "./captain/web-mode.js";
15
15
  import { toolExecutionLabel } from './conversation.js';
16
16
  import { Researcher } from "./researcher.js";
17
17
  import { TaskAgent } from "./task-agent.js";
18
+ import { withdrawVisionTools } from "./tools.js";
18
19
  const MAX_STEPS = 15;
19
20
  const CaptainBase = WithTestMode(WithWebMode(WithIdleMode(TaskAgent)));
20
21
  export class Captain extends CaptainBase {
@@ -389,6 +390,7 @@ export class Captain extends CaptainBase {
389
390
  stop();
390
391
  return;
391
392
  }
393
+ withdrawVisionTools(tools);
392
394
  const currentState = stateManager.getCurrentState();
393
395
  if (!currentState && this.getMode() !== 'idle') {
394
396
  stop();
@@ -15,6 +15,7 @@ declare class Navigator implements Agent {
15
15
  experienceTracker: ExperienceTracker;
16
16
  hooksRunner: HooksRunner;
17
17
  MAX_ATTEMPTS: number;
18
+ lastFailureReason: string | null;
18
19
  systemPrompt: string;
19
20
  freeSailSystemPrompt: string;
20
21
  explorer: Explorer;
@@ -25,10 +26,16 @@ declare class Navigator implements Agent {
25
26
  get verifyTimeout(): number;
26
27
  getBaseOrigin(): string | null;
27
28
  getComparableCurrentUrl(stateManager: any, expectedUrl: string): string;
29
+ comparableUrl(state: {
30
+ url?: string;
31
+ fullUrl?: string;
32
+ }, expectedUrl: string): string;
28
33
  isSameExpectedOrigin(expectedUrl: string, stateManager: any): boolean;
29
34
  isOnExpectedPage(expectedUrl: string, stateManager: any): boolean;
30
35
  visit(url: string): Promise<void>;
31
36
  visitOnce(url: string): Promise<void>;
37
+ navigationError(url: string, fallback: string): Error;
38
+ failureReason(stopReason: string | null, knowledge: string, url: string): string | null;
32
39
  resolveState(message: string, actionResult: ActionResult, opts?: {
33
40
  action?: Action;
34
41
  expectedUrl?: string;
@@ -37,6 +44,20 @@ declare class Navigator implements Agent {
37
44
  error?: string;
38
45
  }) => void;
39
46
  }): Promise<boolean>;
47
+ buildResolutionPrompt(message: string, actionResult: ActionResult): Promise<string>;
48
+ buildRetryFeedback(failures: BatchFailure[], includeHtml: boolean, actionResult: ActionResult): Promise<string>;
49
+ executeAttempt(action: Action, codeBlock: string, message: string): Promise<{
50
+ ok: boolean;
51
+ error?: string;
52
+ }>;
53
+ verifyNavigation(action: Action, expectedUrl: string): Promise<{
54
+ freshState: ActionResult;
55
+ urlMatches: boolean;
56
+ }>;
57
+ ariaDiff(freshState: ActionResult, previous: ActionResult): Promise<string | null>;
58
+ saveFlow(message: string, expectedUrl: string | undefined, actionResult: ActionResult, progressBlocks: string[]): void;
59
+ rescueDelayedRedirect(action: Action, expectedUrl: string): Promise<boolean>;
60
+ askUserToResolve(action: Action, message: string, expectedUrl: string | undefined, stopReason: string | null): Promise<boolean>;
40
61
  buildExperienceTools(): {
41
62
  learnExperience: unknown;
42
63
  } | undefined;
@@ -50,6 +71,8 @@ declare class Navigator implements Agent {
50
71
  } | null>;
51
72
  verifyState(message: string, actionResult: ActionResult): Promise<{
52
73
  verified: boolean;
74
+ inexpressible: boolean;
75
+ results: AssertionResult[];
53
76
  successfulCodes: string[];
54
77
  assertionSteps: Array<{
55
78
  name: string;
@@ -59,4 +82,15 @@ declare class Navigator implements Agent {
59
82
  }>;
60
83
  checkAlreadyVerified(aiResponse: string, actionResult: ActionResult): boolean;
61
84
  }
85
+ type BatchFailure = {
86
+ code: string;
87
+ error: string;
88
+ ariaChanges?: string | null;
89
+ urlAfter?: string;
90
+ };
91
+ export type AssertionResult = {
92
+ code: string;
93
+ passed: boolean;
94
+ proof: string[];
95
+ };
62
96
  export { Navigator };