explorbot 0.1.32-beta.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/README.md +14 -14
  2. package/bin/explorbot-cli.ts +4 -3
  3. package/boat/doc-collector/src/ai/documentarian.ts +22 -27
  4. package/boat/doc-collector/src/ai/tools.ts +67 -10
  5. package/boat/doc-collector/src/docbot.ts +28 -7
  6. package/boat/doc-collector/src/docs-renderer.ts +19 -24
  7. package/boat/doc-collector/src/screenshots.ts +33 -1
  8. package/boat/doc-collector/src/state-diagram.ts +281 -0
  9. package/dist/bin/explorbot-cli.js +3 -3
  10. package/dist/boat/doc-collector/src/ai/documentarian.js +16 -6
  11. package/dist/boat/doc-collector/src/ai/tools.js +47 -10
  12. package/dist/boat/doc-collector/src/docbot.js +25 -6
  13. package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
  14. package/dist/boat/doc-collector/src/screenshots.js +29 -0
  15. package/dist/boat/doc-collector/src/state-diagram.js +200 -0
  16. package/dist/package.json +20 -3
  17. package/dist/src/action-result.d.ts +8 -10
  18. package/dist/src/action-result.js +72 -164
  19. package/dist/src/action.d.ts +3 -10
  20. package/dist/src/action.js +18 -111
  21. package/dist/src/ai/captain/web-mode.js +0 -7
  22. package/dist/src/ai/captain.d.ts +1 -3
  23. package/dist/src/ai/captain.js +4 -19
  24. package/dist/src/ai/conversation.d.ts +2 -5
  25. package/dist/src/ai/conversation.js +9 -28
  26. package/dist/src/ai/driller.d.ts +0 -4
  27. package/dist/src/ai/driller.js +3 -12
  28. package/dist/src/ai/experience-compactor.d.ts +2 -2
  29. package/dist/src/ai/experience-compactor.js +16 -33
  30. package/dist/src/ai/fisherman-tools.js +1 -1
  31. package/dist/src/ai/fisherman.d.ts +0 -1
  32. package/dist/src/ai/fisherman.js +0 -3
  33. package/dist/src/ai/historian/codeceptjs.d.ts +2 -0
  34. package/dist/src/ai/historian/codeceptjs.js +3 -2
  35. package/dist/src/ai/historian/playwright.d.ts +2 -0
  36. package/dist/src/ai/historian/playwright.js +3 -2
  37. package/dist/src/ai/historian/utils.d.ts +0 -1
  38. package/dist/src/ai/historian/utils.js +0 -1
  39. package/dist/src/ai/historian.d.ts +5 -4
  40. package/dist/src/ai/historian.js +1 -5
  41. package/dist/src/ai/navigator.d.ts +3 -5
  42. package/dist/src/ai/navigator.js +11 -46
  43. package/dist/src/ai/pilot.d.ts +1 -3
  44. package/dist/src/ai/pilot.js +6 -17
  45. package/dist/src/ai/planner.d.ts +3 -3
  46. package/dist/src/ai/planner.js +49 -37
  47. package/dist/src/ai/provider.d.ts +4 -1
  48. package/dist/src/ai/provider.js +77 -136
  49. package/dist/src/ai/quartermaster.d.ts +21 -22
  50. package/dist/src/ai/quartermaster.js +4 -10
  51. package/dist/src/ai/rerunner.d.ts +1 -2
  52. package/dist/src/ai/rerunner.js +14 -22
  53. package/dist/src/ai/researcher/cache.js +11 -18
  54. package/dist/src/ai/researcher/deep-analysis.js +3 -6
  55. package/dist/src/ai/researcher/parser.js +1 -1
  56. package/dist/src/ai/researcher/research-result.d.ts +0 -1
  57. package/dist/src/ai/researcher/research-result.js +0 -19
  58. package/dist/src/ai/researcher/sections.js +5 -1
  59. package/dist/src/ai/researcher.js +12 -53
  60. package/dist/src/ai/rules.d.ts +1 -0
  61. package/dist/src/ai/rules.js +22 -15
  62. package/dist/src/ai/session-analyst.js +8 -5
  63. package/dist/src/ai/task-agent.d.ts +1 -1
  64. package/dist/src/ai/task-agent.js +2 -27
  65. package/dist/src/ai/tester.d.ts +0 -2
  66. package/dist/src/ai/tester.js +10 -61
  67. package/dist/src/ai/tools.d.ts +18 -7
  68. package/dist/src/ai/tools.js +119 -101
  69. package/dist/src/commands/compact-command.d.ts +1 -1
  70. package/dist/src/commands/compact-command.js +6 -6
  71. package/dist/src/commands/context-command.js +1 -1
  72. package/dist/src/commands/context-knowledge-command.js +1 -1
  73. package/dist/src/commands/experience-command.js +1 -1
  74. package/dist/src/commands/knows-command.js +1 -1
  75. package/dist/src/commands/learn-command.js +3 -1
  76. package/dist/src/components/AddKnowledge.d.ts +2 -0
  77. package/dist/src/components/AddKnowledge.js +3 -7
  78. package/dist/src/components/App.js +2 -2
  79. package/dist/src/config.d.ts +2 -0
  80. package/dist/src/config.js +12 -0
  81. package/dist/src/experience-tracker.d.ts +11 -6
  82. package/dist/src/experience-tracker.js +81 -154
  83. package/dist/src/explorbot.d.ts +4 -5
  84. package/dist/src/explorbot.js +17 -51
  85. package/dist/src/explorer.d.ts +4 -15
  86. package/dist/src/explorer.js +18 -68
  87. package/dist/src/knowledge-tracker.d.ts +2 -2
  88. package/dist/src/knowledge-tracker.js +38 -45
  89. package/dist/src/state-manager.d.ts +6 -31
  90. package/dist/src/state-manager.js +10 -129
  91. package/dist/src/utils/aria.d.ts +6 -1
  92. package/dist/src/utils/aria.js +4 -1
  93. package/dist/src/utils/cache.d.ts +15 -0
  94. package/dist/src/utils/cache.js +34 -0
  95. package/dist/src/utils/context-formatter.d.ts +6 -0
  96. package/dist/src/utils/context-formatter.js +15 -27
  97. package/dist/src/utils/html.d.ts +5 -0
  98. package/dist/src/utils/html.js +71 -0
  99. package/dist/src/utils/markdown-files.d.ts +10 -0
  100. package/dist/src/utils/markdown-files.js +21 -0
  101. package/dist/src/utils/markdown-query.d.ts +6 -0
  102. package/dist/src/utils/markdown-query.js +14 -1
  103. package/dist/src/utils/page-readiness.d.ts +1 -0
  104. package/dist/src/utils/page-readiness.js +1 -1
  105. package/dist/src/utils/secrets.js +2 -2
  106. package/dist/src/utils/strings.d.ts +2 -0
  107. package/dist/src/utils/strings.js +10 -0
  108. package/dist/src/utils/web-element.d.ts +2 -1
  109. package/dist/src/utils/web-element.js +3 -0
  110. package/dist/src/utils/web-sandbox.d.ts +4 -0
  111. package/dist/src/utils/web-sandbox.js +37 -0
  112. package/package.json +20 -3
  113. package/src/action-result.ts +70 -173
  114. package/src/action.ts +16 -130
  115. package/src/ai/captain/web-mode.ts +0 -7
  116. package/src/ai/captain.ts +5 -17
  117. package/src/ai/conversation.ts +9 -32
  118. package/src/ai/driller.ts +3 -16
  119. package/src/ai/experience-compactor.ts +16 -35
  120. package/src/ai/fisherman-tools.ts +1 -1
  121. package/src/ai/fisherman.ts +0 -4
  122. package/src/ai/historian/codeceptjs.ts +4 -2
  123. package/src/ai/historian/playwright.ts +4 -2
  124. package/src/ai/historian/utils.ts +0 -1
  125. package/src/ai/historian.ts +4 -7
  126. package/src/ai/navigator.ts +13 -49
  127. package/src/ai/pilot.ts +6 -16
  128. package/src/ai/planner.ts +56 -44
  129. package/src/ai/provider.ts +85 -145
  130. package/src/ai/quartermaster.ts +34 -41
  131. package/src/ai/rerunner.ts +17 -21
  132. package/src/ai/researcher/cache.ts +10 -16
  133. package/src/ai/researcher/deep-analysis.ts +3 -6
  134. package/src/ai/researcher/parser.ts +1 -1
  135. package/src/ai/researcher/research-result.ts +0 -18
  136. package/src/ai/researcher/sections.ts +5 -1
  137. package/src/ai/researcher.ts +11 -59
  138. package/src/ai/rules.ts +24 -13
  139. package/src/ai/session-analyst.ts +8 -5
  140. package/src/ai/task-agent.ts +3 -30
  141. package/src/ai/tester.ts +12 -73
  142. package/src/ai/tools.ts +119 -106
  143. package/src/commands/compact-command.ts +7 -8
  144. package/src/commands/context-command.ts +1 -1
  145. package/src/commands/context-knowledge-command.ts +1 -1
  146. package/src/commands/experience-command.ts +1 -1
  147. package/src/commands/knows-command.ts +1 -1
  148. package/src/commands/learn-command.ts +3 -1
  149. package/src/components/AddKnowledge.tsx +5 -7
  150. package/src/components/App.tsx +2 -2
  151. package/src/config.ts +12 -0
  152. package/src/experience-tracker.ts +88 -151
  153. package/src/explorbot.ts +17 -51
  154. package/src/explorer.ts +20 -72
  155. package/src/knowledge-tracker.ts +42 -49
  156. package/src/state-manager.ts +15 -156
  157. package/src/utils/aria.ts +11 -2
  158. package/src/utils/cache.ts +40 -0
  159. package/src/utils/context-formatter.ts +12 -15
  160. package/src/utils/html.ts +79 -0
  161. package/src/utils/markdown-files.ts +30 -0
  162. package/src/utils/markdown-query.ts +15 -1
  163. package/src/utils/page-readiness.ts +1 -1
  164. package/src/utils/secrets.ts +2 -3
  165. package/src/utils/strings.ts +12 -0
  166. package/src/utils/web-element.ts +4 -0
  167. package/src/utils/web-sandbox.ts +43 -0
  168. package/dist/boat/api-tester/example/apibot.config.js +0 -30
@@ -2,7 +2,7 @@ import { mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { ActionResult } from '../../action-result.ts';
4
4
  import { ConfigParser } from '../../config.ts';
5
- import { KnowledgeTracker } from '../../knowledge-tracker.ts';
5
+ import type { StateManager } from '../../state-manager.ts';
6
6
  import { type PlaywrightRecorder, type TraceCall, renderAssertion, renderCall } from '../../playwright-recorder.ts';
7
7
  import type { Plan } from '../../test-plan.ts';
8
8
  import { tag } from '../../utils/logger.ts';
@@ -25,6 +25,7 @@ export function WithPlaywright<T extends Constructor>(Base: T) {
25
25
  return class extends Base {
26
26
  declare playwright: { recorder: PlaywrightRecorder; helper: any } | undefined;
27
27
  declare savedFiles: Set<string>;
28
+ declare stateManager?: StateManager;
28
29
 
29
30
  async toPlaywrightCode(conversation: Conversation, scenario: string): Promise<string> {
30
31
  const toolExecutions = conversation.getToolExecutions();
@@ -146,7 +147,8 @@ export function WithPlaywright<T extends Constructor>(Base: T) {
146
147
  }
147
148
 
148
149
  private getPlaywrightKnowledgeLines(url: string, indent = ' '): string[] {
149
- const knowledgeTracker = new KnowledgeTracker();
150
+ const knowledgeTracker = this.stateManager?.getKnowledgeTracker();
151
+ if (!knowledgeTracker) return [];
150
152
  const state = new ActionResult({ url });
151
153
  const { wait, waitForElement } = knowledgeTracker.getStateParameters(state, ['wait', 'waitForElement']);
152
154
 
@@ -1,5 +1,4 @@
1
1
  import type { ToolExecution } from '../conversation.ts';
2
- export { isNonReusableCode, stripComments } from '../../utils/step-analyzer.ts';
3
2
 
4
3
  export function escapeString(str: string): string {
5
4
  return str.replace(/'/g, "\\'").replace(/\n/g, ' ');
@@ -1,6 +1,6 @@
1
1
  import { readFileSync, writeFileSync } from 'node:fs';
2
2
  import type { ExplorbotConfig } from '../config.ts';
3
- import { ExperienceTracker } from '../experience-tracker.ts';
3
+ import type { ExperienceTracker } from '../experience-tracker.ts';
4
4
  import type { PlaywrightRecorder } from '../playwright-recorder.ts';
5
5
  import type { Reporter } from '../reporter.ts';
6
6
  import type { StateManager } from '../state-manager.ts';
@@ -13,8 +13,6 @@ import { type PlaywrightMethods, WithPlaywright } from './historian/playwright.t
13
13
  import { type ScreencastMethods, WithScreencast } from './historian/screencast.ts';
14
14
  import type { Provider } from './provider.ts';
15
15
 
16
- export { isNonReusableCode } from '../utils/step-analyzer.ts';
17
-
18
16
  const HistorianBase = WithScreencast(WithPlaywright(WithCodeceptJS(WithExperience(Object as unknown as new (...args: any[]) => object))));
19
17
 
20
18
  export interface Historian extends ExperienceMethods, CodeceptJSMethods, PlaywrightMethods, ScreencastMethods {}
@@ -28,10 +26,10 @@ export class Historian extends HistorianBase {
28
26
  declare playwright: { recorder: PlaywrightRecorder; helper: any } | undefined;
29
27
  declare savedFiles: Set<string>;
30
28
 
31
- constructor(provider: Provider, experienceTracker?: ExperienceTracker, reporter?: Reporter, stateManager?: StateManager, config?: ExplorbotConfig, playwright?: { recorder: PlaywrightRecorder; helper: any }) {
29
+ constructor(provider: Provider, experienceTracker: ExperienceTracker, reporter?: Reporter, stateManager?: StateManager, config?: ExplorbotConfig, playwright?: { recorder: PlaywrightRecorder; helper: any }) {
32
30
  super();
33
31
  this.provider = provider;
34
- this.experienceTracker = experienceTracker || new ExperienceTracker();
32
+ this.experienceTracker = experienceTracker;
35
33
  this.reporter = reporter;
36
34
  this.stateManager = stateManager;
37
35
  this.config = config;
@@ -52,11 +50,10 @@ export class Historian extends HistorianBase {
52
50
  return this.isPlaywrightFramework() ? this.savePlaywrightPlanToFile(plan) : this.saveCodeceptPlanToFile(plan);
53
51
  }
54
52
 
55
- rewriteScenarioInFile(filePath: string, healedSteps: Array<{ test: string; original: string; healed: string }>): void {
53
+ rewriteScenarioInFile(filePath: string, healedSteps: Array<{ original: string; healed: string }>): void {
56
54
  let content = readFileSync(filePath, 'utf-8');
57
55
 
58
56
  for (const step of healedSteps) {
59
- if (!content.includes(step.original)) continue;
60
57
  content = content.replace(step.original, step.healed);
61
58
  }
62
59
 
@@ -3,10 +3,10 @@ import dedent from 'dedent';
3
3
  import { z } from 'zod';
4
4
  import { ActionResult } from '../action-result.js';
5
5
  import type Action from '../action.ts';
6
- import { ExperienceTracker, renderExperienceToc } from '../experience-tracker.js';
6
+ import type { ExperienceTracker } from '../experience-tracker.js';
7
7
  import Explorer from '../explorer.ts';
8
- import { KnowledgeTracker } from '../knowledge-tracker.js';
9
- import { type WebPageState, normalizeUrl } from '../state-manager.js';
8
+ import type { KnowledgeTracker } from '../knowledge-tracker.js';
9
+ import { normalizeUrl } from '../state-manager.js';
10
10
  import { extractCodeBlocks } from '../utils/code-extractor.js';
11
11
  import { HooksRunner } from '../utils/hooks-runner.ts';
12
12
  import { createDebug, pluralize, tag } from '../utils/logger.js';
@@ -15,19 +15,17 @@ import { RulesLoader } from '../utils/rules-loader.ts';
15
15
  import { extractStatePath } from '../utils/url-matcher.js';
16
16
  import type { Agent } from './agent.js';
17
17
  import type { Conversation } from './conversation.js';
18
- import { ExperienceCompactor } from './experience-compactor.js';
19
18
  import type { Provider } from './provider.js';
20
19
  import { Researcher } from './researcher.ts';
21
20
  import { actionRule, locatorRule, unexpectedPopupRule } from './rules.js';
22
21
  import { isInteractive } from './task-agent.js';
23
- import { createAgentTools } from './tools.ts';
22
+ import { createLearnExperienceTool } from './tools.ts';
24
23
 
25
24
  const debugLog = createDebug('explorbot:navigator');
26
25
 
27
26
  class Navigator implements Agent {
28
27
  emoji = '🧭';
29
28
  private provider: Provider;
30
- private experienceCompactor: ExperienceCompactor;
31
29
  private knowledgeTracker: KnowledgeTracker;
32
30
  private experienceTracker: ExperienceTracker;
33
31
  private hooksRunner: HooksRunner;
@@ -73,12 +71,11 @@ class Navigator implements Agent {
73
71
  `;
74
72
  private explorer: Explorer;
75
73
 
76
- constructor(explorer: Explorer, provider: Provider, experienceCompactor: ExperienceCompactor, experienceTracker?: ExperienceTracker) {
74
+ constructor(explorer: Explorer, provider: Provider) {
77
75
  this.provider = provider;
78
76
  this.explorer = explorer;
79
- this.experienceCompactor = experienceCompactor;
80
- this.knowledgeTracker = new KnowledgeTracker();
81
- this.experienceTracker = experienceTracker || new ExperienceTracker();
77
+ this.knowledgeTracker = explorer.getKnowledgeTracker();
78
+ this.experienceTracker = explorer.getStateManager().getExperienceTracker();
82
79
  this.hooksRunner = new HooksRunner(explorer, explorer.getConfig());
83
80
  }
84
81
 
@@ -194,19 +191,9 @@ class Navigator implements Agent {
194
191
  const action = opts?.action ?? this.explorer.createAction();
195
192
  const expectedUrl = opts?.expectedUrl;
196
193
 
197
- let knowledge = '';
194
+ const knowledge = this.knowledgeTracker.renderRelevantKnowledge(actionResult);
198
195
  let experience = '';
199
196
 
200
- const relevantKnowledge = this.knowledgeTracker.getRelevantKnowledge(actionResult);
201
- if (relevantKnowledge.length > 0) {
202
- const knowledgeContent = relevantKnowledge.map((k) => k.content).join('\n\n');
203
- knowledge = `
204
- <hint>
205
- Here is relevant knowledge for this page:
206
- ${knowledgeContent}
207
- </hint>`;
208
- }
209
-
210
197
  if (!actionResult.isInsideIframe) {
211
198
  const successful = this.experienceTracker.getSuccessfulExperience(actionResult);
212
199
  if (successful.length > 0) {
@@ -397,7 +384,6 @@ class Navigator implements Agent {
397
384
  if (freshState.getStateHash() !== prevHash) {
398
385
  try {
399
386
  const diff = await freshState.diff(prevActionResult);
400
- await diff.calculate();
401
387
  ariaChanges = diff.ariaChanged;
402
388
  } catch (err) {
403
389
  debugLog('Failed to compute pageDiff for failed URL verification:', err);
@@ -486,20 +472,13 @@ class Navigator implements Agent {
486
472
  }
487
473
 
488
474
  private buildExperienceTools(): { learnExperience: unknown } | undefined {
475
+ if (!this.experienceTracker) return undefined;
489
476
  const stateManager = this.explorer.getStateManager();
490
477
  const getState = () => {
491
478
  const s = stateManager.getCurrentState();
492
479
  return s ? ActionResult.fromState(s) : null;
493
480
  };
494
- const { learnExperience } = createAgentTools({
495
- explorer: this.explorer,
496
- researcher: null as unknown as Researcher,
497
- navigator: this,
498
- experienceTracker: this.experienceTracker,
499
- getState,
500
- });
501
- if (!learnExperience) return undefined;
502
- return { learnExperience };
481
+ return { learnExperience: createLearnExperienceTool({ getExperienceTracker: () => this.experienceTracker, getState }) };
503
482
  }
504
483
 
505
484
  async freeSail(opts?: { strategy?: 'deep' | 'shallow'; scope?: string; visitedUrls?: Set<string> }, actionResult?: ActionResult): Promise<{ target: string; reason: string } | null> {
@@ -641,26 +620,11 @@ class Navigator implements Agent {
641
620
  return { verified: cachedVerification, successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
642
621
  }
643
622
 
644
- let knowledge = '';
623
+ const knowledge = this.knowledgeTracker.renderRelevantKnowledge(actionResult);
645
624
  let experience = '';
646
625
 
647
- const relevantKnowledge = this.knowledgeTracker.getRelevantKnowledge(actionResult);
648
- if (relevantKnowledge.length > 0) {
649
- const knowledgeContent = relevantKnowledge.map((k) => k.content).join('\n\n');
650
- knowledge = `
651
- <hint>
652
- Here is relevant knowledge for this page:
653
- ${knowledgeContent}
654
- </hint>`;
655
- }
656
-
657
626
  if (!actionResult.isInsideIframe) {
658
- const toc = this.experienceTracker.getExperienceTableOfContents(actionResult);
659
- if (toc.length > 0) {
660
- const totalSections = toc.reduce((sum, entry) => sum + entry.sections.length, 0);
661
- tag('operation').log(`Found ${toc.length} experience ${pluralize(toc.length, 'file')} (${totalSections} sections) for: ${actionResult.url}`);
662
- experience = renderExperienceToc(toc);
663
- }
627
+ experience = this.experienceTracker.renderExperienceTocFor(actionResult);
664
628
  }
665
629
 
666
630
  const priorVerifications = Object.entries(actionResult.verifications ?? {});
@@ -764,7 +728,7 @@ class Navigator implements Agent {
764
728
 
765
729
  await this.explorer.switchToMainFrame();
766
730
 
767
- const verified = await action.attempt(codeBlock, message, false);
731
+ const verified = await action.attempt(codeBlock, message);
768
732
 
769
733
  if (verified) {
770
734
  tag('success').log('Verification passed');
package/src/ai/pilot.ts CHANGED
@@ -3,7 +3,6 @@ import dedent from 'dedent';
3
3
  import { z } from 'zod';
4
4
  import { ActionResult } from '../action-result.ts';
5
5
  import { ConfigParser } from '../config.ts';
6
- import { type ExperienceTracker, renderExperienceToc } from '../experience-tracker.ts';
7
6
  import type Explorer from '../explorer.ts';
8
7
  import { type Test, TestResult } from '../test-plan.ts';
9
8
  import { collectInteractiveNodes, detectFocusArea, extractFocusedElement } from '../utils/aria.ts';
@@ -32,14 +31,12 @@ export class Pilot implements Agent {
32
31
  private researcher: Researcher;
33
32
  private explorer: Explorer;
34
33
  private fisherman: Fisherman | null = null;
35
- private experienceTracker: ExperienceTracker | null;
36
34
 
37
- constructor(provider: Provider, agentTools: any, researcher: Researcher, explorer: Explorer, experienceTracker?: ExperienceTracker) {
35
+ constructor(provider: Provider, agentTools: any, researcher: Researcher, explorer: Explorer) {
38
36
  this.provider = provider;
39
37
  this.agentTools = agentTools;
40
38
  this.researcher = researcher;
41
39
  this.explorer = explorer;
42
- this.experienceTracker = experienceTracker || null;
43
40
  }
44
41
 
45
42
  setFisherman(fisherman: Fisherman): void {
@@ -111,7 +108,7 @@ export class Pilot implements Agent {
111
108
  .string()
112
109
  .nullable()
113
110
  .describe(
114
- 'REQUIRED whenever decision is "pass" — a one-sentence natural-language claim about the current page that, if true, proves the scenario goal (e.g., "New test suite \\"Foo\\" is visible in the suites list"). NOT code: do not write I.*, expect(), .then(), grabTitle, or any JavaScript. Navigator translates the claim into CodeceptJS assertions and runs them; passing assertions are saved to the generated test file. Also use when evidence is insufficient before deciding pass/fail. Leave null for "continue", "fail", or "skipped".'
111
+ 'REQUIRED whenever decision is "pass" — a one-sentence natural-language claim about the current page that, if true, proves the scenario goal (e.g., "New item \\"Foo\\" is visible in the items list"). NOT code: do not write I.*, expect(), .then(), grabTitle, or any JavaScript. Navigator translates the claim into CodeceptJS assertions and runs them; passing assertions are saved to the generated test file. Also use when evidence is insufficient before deciding pass/fail. Leave null for "continue", "fail", or "skipped".'
115
112
  ),
116
113
  });
117
114
 
@@ -147,7 +144,7 @@ export class Pilot implements Agent {
147
144
  - Mixed evidence + final state shows success → pass. Mixed + final state unclear → continue with guidance.
148
145
 
149
146
  When deciding "pass", you MUST also set requestVerification to a one-sentence natural-language
150
- claim about the current page (e.g., "New test suite Foo is visible in the suites list"). NOT
147
+ claim about the current page (e.g., "New item Foo is visible in the items list"). NOT
151
148
  code — do not write I.*, expect(), .then(), or any JavaScript. Choose the strongest single
152
149
  piece of evidence (a unique element/text that exists ONLY because the scenario succeeded).
153
150
  Navigator translates the claim into CodeceptJS assertions; without it the generated test has
@@ -429,10 +426,6 @@ export class Pilot implements Agent {
429
426
 
430
427
  FIRST: Decide if precondition() is needed.
431
428
 
432
- ${capabilityGroundingRule}
433
-
434
- ${dataProtectionRules}
435
-
436
429
  Call precondition() WHEN:
437
430
  - The scenario edits/deletes/modifies an item, and you want a DISPOSABLE item to act on safely
438
431
  - The scenario needs specific data clearly NOT on the current page (e.g., items with specific statuses for filtering)
@@ -603,12 +596,9 @@ export class Pilot implements Agent {
603
596
  }
604
597
 
605
598
  private getExperienceToc(): string {
606
- if (!this.experienceTracker) return '';
607
599
  const state = this.explorer.getStateManager().getCurrentState();
608
600
  if (!state) return '';
609
- const actionResult = ActionResult.fromState(state);
610
- const toc = this.experienceTracker.getExperienceTableOfContents(actionResult);
611
- return renderExperienceToc(toc);
601
+ return this.explorer.getStateManager().getExperienceTracker().renderExperienceTocFor(ActionResult.fromState(state));
612
602
  }
613
603
 
614
604
  private pickPlanningTools() {
@@ -1056,8 +1046,8 @@ export class Pilot implements Agent {
1056
1046
 
1057
1047
  ${dataProtectionRules}
1058
1048
 
1059
- Describe WHAT to create, not what exists. RIGHT: precondition("1 test"). WRONG:
1060
- precondition("1 test suite named Updated Suite with existing tests"). Keep descriptions short.
1049
+ Describe WHAT to create, not what exists. RIGHT: precondition("1 post"). WRONG:
1050
+ precondition("1 post named Updated Post with existing comments"). Keep descriptions short.
1061
1051
 
1062
1052
  Response format:
1063
1053
  PROGRESS: <1 sentence assessment>
package/src/ai/planner.ts CHANGED
@@ -3,7 +3,7 @@ import { z } from 'zod';
3
3
  import { ActionResult } from '../action-result.ts';
4
4
  import { setActivity } from '../activity.ts';
5
5
  import { ConfigParser } from '../config.ts';
6
- import { ExperienceTracker } from '../experience-tracker.ts';
6
+ import type { ExperienceTracker } from '../experience-tracker.ts';
7
7
  import type Explorer from '../explorer.ts';
8
8
  import { Observability } from '../observability.ts';
9
9
  import type { StateManager } from '../state-manager.js';
@@ -18,10 +18,10 @@ import type { Agent } from './agent.js';
18
18
  import { Conversation } from './conversation.ts';
19
19
  import type { Fisherman } from './fisherman.ts';
20
20
  import { WithSessionDedup } from './planner/session-dedup.ts';
21
- import { getActiveStyle, getStyles } from './planner/styles.ts';
21
+ import { getActiveStyle } from './planner/styles.ts';
22
22
  import { WithSubPages, getPlannedByStateHash, getRegisteredPlan, registerPlan } from './planner/subpages.ts';
23
23
  import type { Provider } from './provider.js';
24
- import { POSSIBLE_SECTIONS, Researcher } from './researcher.ts';
24
+ import { POSSIBLE_SECTIONS, type Researcher } from './researcher.ts';
25
25
  import { findSimilarStateHash } from './researcher/cache.ts';
26
26
  import { hasFocusedSection } from './researcher/focus.ts';
27
27
  import { capabilityGroundingRule, dataProtectionRules, fileUploadRule } from './rules.ts';
@@ -63,13 +63,13 @@ export class Planner extends PlannerBase implements Agent {
63
63
  researcher: Researcher;
64
64
  private fisherman: Fisherman | null = null;
65
65
 
66
- constructor(explorer: Explorer, provider: Provider) {
66
+ constructor(explorer: Explorer, provider: Provider, researcher: Researcher) {
67
67
  super();
68
68
  this.explorer = explorer;
69
69
  this.provider = provider;
70
- this.researcher = new Researcher(explorer, provider);
70
+ this.researcher = researcher;
71
71
  this.stateManager = explorer.getStateManager();
72
- this.experienceTracker = new ExperienceTracker();
72
+ this.experienceTracker = explorer.getStateManager().getExperienceTracker();
73
73
  }
74
74
 
75
75
  setFisherman(fisherman: Fisherman): void {
@@ -98,7 +98,7 @@ export class Planner extends PlannerBase implements Agent {
98
98
  You are ISTQB certified senior manual QA planning exploratory testing session of a web application.
99
99
  Each test scenario must complete a meaningful user workflow — not just open a UI element and verify it exists.
100
100
  Bad: "Open the Help panel" — just opens something.
101
- Good: "Create a new suite and verify it appears in the list" — completes a business action.
101
+ Good: "Create a new post and verify it appears in the list" — completes a business action.
102
102
  </role>
103
103
  <task>
104
104
  List possible testing scenarios for the web page.
@@ -171,6 +171,9 @@ export class Planner extends PlannerBase implements Agent {
171
171
 
172
172
  const tags = ['planner'];
173
173
  if (style) tags.push(style);
174
+
175
+ const existingTestScenarios = this.getExistingTestFileScenarios(state.url);
176
+
174
177
  const result = await Observability.run(`planner: ${state.url}`, { tags, sessionId: state.url }, async () => {
175
178
  const actionResult = ActionResult.fromState(state);
176
179
  const conversation = await this.buildConversation(actionResult, style, parentPlan, feature);
@@ -211,7 +214,6 @@ export class Planner extends PlannerBase implements Agent {
211
214
  const defaultStartUrl = this.getDefaultStartUrl(state);
212
215
  if (parentPlan) this.currentPlan.parentPlan = parentPlan;
213
216
  const allPreviousScenarios = this.getPreviousSessionScenarios();
214
- const existingTestScenarios = this.getExistingTestFileScenarios(state.url);
215
217
  for (const s of existingTestScenarios) allPreviousScenarios.add(s);
216
218
  for (const t of tests) {
217
219
  if (allPreviousScenarios.has(t.scenario.toLowerCase())) continue;
@@ -266,6 +268,7 @@ export class Planner extends PlannerBase implements Agent {
266
268
  }
267
269
 
268
270
  private getExistingTestFileScenarios(currentUrl?: string): Set<string> {
271
+ this.lastSuite = null;
269
272
  if (!currentUrl) return new Set<string>();
270
273
  try {
271
274
  this.lastSuite = new Suite(currentUrl);
@@ -277,29 +280,36 @@ export class Planner extends PlannerBase implements Agent {
277
280
  }
278
281
 
279
282
  private cleanExperienceFlows(text: string): string | null {
280
- const seenTitles = new Set<string>();
281
283
  let result = text;
284
+ const seenTitles = new Set<string>();
285
+ for (const selector of ['section2', 'section3']) {
286
+ result = mdq(result)
287
+ .query(selector)
288
+ .replaceEach((section) => {
289
+ const heading = section.query('heading').text().trim();
290
+ const withoutHeadings = mdq(section.text()).query('heading').replace('');
291
+ const body = mdq(withoutHeadings).query('hr').replace('').trim();
292
+ if (body && !seenTitles.has(heading)) {
293
+ seenTitles.add(heading);
294
+ return section.text();
295
+ }
296
+ return '';
297
+ });
298
+ }
282
299
 
283
- for (const section of [...mdq(text).query('section2').each(), ...mdq(text).query('section3').each()]) {
284
- const heading = section.query('heading').text().trim();
285
- const body = mdq(section.text())
286
- .query('heading')
287
- .replace('')
288
- .replace(/^---\s*$/gm, '')
289
- .trim();
290
-
291
- if (!body || seenTitles.has(heading)) {
292
- result = result.replace(section.text(), '');
293
- continue;
294
- }
295
- seenTitles.add(heading);
296
-
297
- const blockquotes = section.query('blockquote').each();
298
- if (blockquotes.length <= 10) continue;
299
- for (const bq of blockquotes.slice(10)) {
300
- result = result.replace(bq.text(), '');
301
- }
302
- result = result.replace(section.text().trim(), `${section.text().trim()}\n> ... and ${blockquotes.length - 10} more discoveries`);
300
+ const trimmedTitles = new Set<string>();
301
+ for (const selector of ['section2', 'section3']) {
302
+ result = mdq(result)
303
+ .query(selector)
304
+ .replaceEach((section) => {
305
+ const heading = section.query('heading').text().trim();
306
+ if (trimmedTitles.has(heading)) return section.text();
307
+ const count = section.query('blockquote').count();
308
+ if (count <= 10) return section.text();
309
+ const kept = mdq(section.text()).query('blockquote[10:]').replace('');
310
+ trimmedTitles.add(heading);
311
+ return `${kept.trimEnd()}\n> ... and ${count - 10} more discoveries\n`;
312
+ });
303
313
  }
304
314
 
305
315
  return result.trim() || null;
@@ -338,6 +348,7 @@ export class Planner extends PlannerBase implements Agent {
338
348
  If there are subpages (pages with same URL path) plan testing of those subpages as well
339
349
  If you plan to test CRUD operations, plan them in correct order: create, read, update.
340
350
  Do not invent specific route names, success messages, validation texts, badge counts, or welcome messages unless they are visible in research, visited pages, or prior observed flows.
351
+ When validation placement or wording was not observed, require feedback associated with the invalid input without inventing a specific location or message.
341
352
  If exact wording is unknown, describe the expected result generically, for example "an authentication error is shown" or "the user stays on the login page" instead of guessing the literal text.
342
353
  If exact redirect destination is unknown, describe the destination by visible page identity, for example "the dashboard page opens" or "the current workspace home page opens" instead of inventing a URL slug.
343
354
  Only propose scenarios whose prerequisites are evident from page research, visited pages, or API data preparation context.
@@ -378,16 +389,17 @@ export class Planner extends PlannerBase implements Agent {
378
389
  deep: true,
379
390
  });
380
391
  let plannerResearch = mdq(research).query('code').replace('');
381
- for (const table of mdq(plannerResearch).query('table').each()) {
382
- const rawTable = table.text();
383
- const rows = table.toJson();
384
- if (rows.length === 0 || !rows[0].Element) continue;
385
- const elementWithType = rows.map((r) => ({
386
- Element: r.Element,
387
- Type: r.Type || '',
388
- }));
389
- plannerResearch = plannerResearch.replace(rawTable, jsonToTable(elementWithType, ['Element', 'Type']));
390
- }
392
+ plannerResearch = mdq(plannerResearch)
393
+ .query('table')
394
+ .replaceEach((table) => {
395
+ const rows = table.toJson();
396
+ if (rows.length === 0 || !rows[0].Element) return table.text();
397
+ const elementWithType = rows.map((r) => ({
398
+ Element: r.Element,
399
+ Type: r.Type || '',
400
+ }));
401
+ return jsonToTable(elementWithType, ['Element', 'Type']);
402
+ });
391
403
 
392
404
  const hasFocusedOverlay = hasFocusedSection(plannerResearch);
393
405
  const focusNote = hasFocusedOverlay ? "IMPORTANT: One section is marked as **Focused** — this is the user's current focus area. Concentrate testing on the Focused section FIRST — test all interactions inside it before planning tests for the rest of the page." : '';
@@ -576,17 +588,17 @@ export class Planner extends PlannerBase implements Agent {
576
588
 
577
589
  STEPS - actionable commands:
578
590
  - Each step should be a specific action to perform
579
- - Good: "Click Create Suite button", "Enter 'My New Suite' as suite name", "Click Save button"
580
- - Good: "Click Star icon on 'Template API Testing' suite"
591
+ - Good: "Click Create Post button", "Enter 'My New Post' as post title", "Click Save button"
592
+ - Good: "Click Star icon on 'Quarterly Report' item"
581
593
  - Bad: "Open the dropdown menu" (opening is not a goal, it's a means)
582
594
  - Bad: "Verify modal appears" (verification belongs in expected outcomes, not steps)
583
595
  - Steps should form a complete workflow, not stop at opening a UI element
584
596
  - NEVER split a workflow across two tests. One test = one complete action + its verification
585
597
 
586
598
  EXPECTED OUTCOMES - verifiable results:
587
- - Good: "New suite 'My New Suite' appears in the suite list"
588
- - Good: "Suite appears under Starred filter tab"
589
- - Good: "Success message 'Suite created' is displayed"
599
+ - Good: "New post 'My New Post' appears in the posts list"
600
+ - Good: "Item appears under Starred filter tab"
601
+ - Good: "Success message 'Post created' is displayed"
590
602
  - Good when wording is unknown: "An authentication error is displayed"
591
603
  - Good when route is unknown: "The workspace home page is displayed"
592
604
  - Bad: "Modal is displayed" (just verifying existence, no business value)