explorbot 0.1.0 → 0.1.1

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 (69) hide show
  1. package/bin/explorbot-cli.ts +93 -36
  2. package/dist/bin/explorbot-cli.js +71 -16
  3. package/dist/rules/rerunner/healing-approach.md +19 -0
  4. package/dist/src/action.js +8 -10
  5. package/dist/src/ai/historian.js +34 -3
  6. package/dist/src/ai/navigator.js +35 -28
  7. package/dist/src/ai/pilot.js +33 -9
  8. package/dist/src/ai/planner.js +29 -10
  9. package/dist/src/ai/rerunner.js +472 -0
  10. package/dist/src/ai/researcher.js +3 -4
  11. package/dist/src/ai/rules.js +2 -2
  12. package/dist/src/ai/tools.js +2 -2
  13. package/dist/src/commands/add-rule-command.js +1 -2
  14. package/dist/src/commands/base-command.js +12 -0
  15. package/dist/src/commands/context-command.js +12 -5
  16. package/dist/src/commands/drill-command.js +0 -1
  17. package/dist/src/commands/explore-command.js +20 -5
  18. package/dist/src/commands/freesail-command.js +8 -22
  19. package/dist/src/commands/index.js +4 -0
  20. package/dist/src/commands/init-command.js +3 -3
  21. package/dist/src/commands/path-command.js +2 -1
  22. package/dist/src/commands/plan-command.js +37 -15
  23. package/dist/src/commands/rerun-command.js +42 -0
  24. package/dist/src/commands/research-command.js +10 -4
  25. package/dist/src/commands/runs-command.js +22 -0
  26. package/dist/src/commands/start-command.js +0 -1
  27. package/dist/src/commands/test-command.js +3 -3
  28. package/dist/src/components/App.js +8 -0
  29. package/dist/src/config.js +3 -0
  30. package/dist/src/explorbot.js +19 -0
  31. package/dist/src/explorer.js +2 -1
  32. package/dist/src/suite.js +115 -0
  33. package/dist/src/utils/html.js +2 -5
  34. package/dist/src/utils/rules-loader.js +33 -17
  35. package/dist/src/utils/test-files.js +103 -0
  36. package/package.json +2 -1
  37. package/rules/rerunner/healing-approach.md +19 -0
  38. package/src/action.ts +7 -9
  39. package/src/ai/historian.ts +37 -3
  40. package/src/ai/navigator.ts +35 -28
  41. package/src/ai/pilot.ts +33 -9
  42. package/src/ai/planner.ts +28 -9
  43. package/src/ai/rerunner.ts +532 -0
  44. package/src/ai/researcher.ts +3 -4
  45. package/src/ai/rules.ts +2 -2
  46. package/src/ai/tools.ts +2 -2
  47. package/src/commands/add-rule-command.ts +1 -2
  48. package/src/commands/base-command.ts +13 -0
  49. package/src/commands/context-command.ts +12 -5
  50. package/src/commands/drill-command.ts +0 -1
  51. package/src/commands/explore-command.ts +21 -5
  52. package/src/commands/freesail-command.ts +6 -23
  53. package/src/commands/index.ts +4 -0
  54. package/src/commands/init-command.ts +3 -3
  55. package/src/commands/path-command.ts +2 -1
  56. package/src/commands/plan-command.ts +45 -16
  57. package/src/commands/rerun-command.ts +46 -0
  58. package/src/commands/research-command.ts +10 -4
  59. package/src/commands/runs-command.ts +27 -0
  60. package/src/commands/start-command.ts +0 -1
  61. package/src/commands/test-command.ts +3 -3
  62. package/src/components/App.tsx +8 -0
  63. package/src/config.ts +23 -0
  64. package/src/explorbot.ts +21 -0
  65. package/src/explorer.ts +3 -2
  66. package/src/suite.ts +135 -0
  67. package/src/utils/html.ts +1 -5
  68. package/src/utils/rules-loader.ts +35 -17
  69. package/src/utils/test-files.ts +122 -0
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env bun
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import chalk from 'chalk';
4
5
  import { Command } from 'commander';
6
+ import figureSet from 'figures';
5
7
  import { render } from 'ink';
6
8
  import React from 'react';
7
9
  import { App } from '../src/components/App.js';
@@ -104,14 +106,14 @@ async function showStatsAndExit(code: number): Promise<never> {
104
106
  process.exit(code);
105
107
  }
106
108
 
107
- addCommonOptions(program.command('start [path]').alias('sail').description('Start web exploration')).action(async (startPath, options) => {
109
+ addCommonOptions(program.command('start [path]').description('Start web exploration')).action(async (startPath, options) => {
108
110
  setPreserveConsoleLogs(false);
109
111
  const explorBot = new ExplorBot(buildExplorBotOptions(startPath, options));
110
112
  await explorBot.start();
111
113
  await startTUI(explorBot);
112
114
  });
113
115
 
114
- addCommonOptions(program.command('explore <path>').description('Start web exploration (legacy command)').option('--max-tests <count>', 'Maximum number of tests to run')).action(async (explorePath, options) => {
116
+ addCommonOptions(program.command('explore <path>').description('Explore a page autonomously and run invented scenarios').option('--max-tests <count>', 'Maximum number of tests to run')).action(async (explorePath, options) => {
115
117
  try {
116
118
  const explorBot = new ExplorBot(buildExplorBotOptions(explorePath, options));
117
119
  await explorBot.start();
@@ -159,6 +161,23 @@ addCommonOptions(program.command('plan <path>').description('Generate test plan
159
161
  await showStatsAndExit(1);
160
162
  }
161
163
 
164
+ const suite = explorBot.getSuite();
165
+ if (suite && suite.automatedTestCount > 0) {
166
+ const names = suite.getAutomatedTestNames();
167
+ console.log(`\n${chalk.bold.cyan(`Already implemented (${names.length} tests)`)}`);
168
+ for (let i = 0; i < names.length; i++) {
169
+ console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${names[i]}`);
170
+ }
171
+ }
172
+
173
+ if (plan?.tests.length) {
174
+ console.log(`\n${chalk.bold.cyan(`New test scenarios (${plan.tests.length})`)}`);
175
+ for (let i = 0; i < plan.tests.length; i++) {
176
+ const t = plan.tests[i];
177
+ console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${t.scenario} ${chalk.dim(`[${t.priority}]`)}`);
178
+ }
179
+ }
180
+
162
181
  const savedPath = explorBot.savePlan();
163
182
  const planFile = savedPath ? path.basename(savedPath) : 'plan.md';
164
183
 
@@ -166,10 +185,14 @@ addCommonOptions(program.command('plan <path>').description('Generate test plan
166
185
  const cliSuffix = cliFlags ? ` ${cliFlags}` : '';
167
186
 
168
187
  const lines: string[] = [];
169
- lines.push('Run tests:');
170
- lines.push(`\`${cli} test ${planFile} 1${cliSuffix}\` → run first test`);
171
- lines.push(`\`${cli} test ${planFile} 1-3${cliSuffix}\` → run tests 1 to 3`);
172
- lines.push(`\`${cli} test ${planFile} *${cliSuffix}\` run all tests`);
188
+ lines.push('Run commands:');
189
+ lines.push(`\`${cli} test ${planFile} 1${cliSuffix}\` → run first new test`);
190
+ lines.push(`\`${cli} test ${planFile} *${cliSuffix}\` → run all new tests`);
191
+ if (suite && suite.automatedTestCount > 0) {
192
+ for (const f of suite.getAutomatedTestFiles()) {
193
+ lines.push(`\`${cli} rerun ${path.relative(process.cwd(), f)}${cliSuffix}\` → re-run automated tests`);
194
+ }
195
+ }
173
196
 
174
197
  log(parseMarkdownToTerminal(lines.join('\n')));
175
198
 
@@ -281,6 +304,42 @@ addCommonOptions(program.command('test <planfile> [index]').description('Execute
281
304
  }
282
305
  });
283
306
 
307
+ program
308
+ .command('runs [file]')
309
+ .description('List generated test files, or show steps for a specific file')
310
+ .option('-p, --path <path>', 'Working directory path')
311
+ .option('-c, --config <path>', 'Path to configuration file')
312
+ .action(async (file, options) => {
313
+ try {
314
+ await ConfigParser.getInstance().loadConfig({
315
+ config: options.config,
316
+ path: options.path || process.cwd(),
317
+ });
318
+ const explorBot = new ExplorBot({ path: options.path });
319
+ const { RunsCommand } = await import('../src/commands/runs-command.js');
320
+ await new RunsCommand(explorBot).execute(file || '');
321
+ } catch (error) {
322
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
323
+ process.exit(1);
324
+ }
325
+ });
326
+
327
+ addCommonOptions(program.command('rerun <filename> [index]').description('Re-run generated tests with AI auto-healing')).action(async (filename, index, options) => {
328
+ try {
329
+ const explorBot = new ExplorBot(buildExplorBotOptions(undefined, options));
330
+ await explorBot.start();
331
+ const { RerunCommand } = await import('../src/commands/rerun-command.js');
332
+ const cmd = new RerunCommand(explorBot);
333
+ const args = index ? `${filename} ${index}` : filename;
334
+ await cmd.execute(args);
335
+ await explorBot.stop();
336
+ await showStatsAndExit(0);
337
+ } catch (error) {
338
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
339
+ await showStatsAndExit(1);
340
+ }
341
+ });
342
+
284
343
  addCommonOptions(
285
344
  program
286
345
  .command('freesail [startUrl]')
@@ -377,7 +436,6 @@ program
377
436
 
378
437
  program
379
438
  .command('learn [url] [description]')
380
- .alias('add-knowledge')
381
439
  .description('Add knowledge for URLs')
382
440
  .option('-p, --path <path>', 'Working directory path')
383
441
  .action(async (url, description, options) => {
@@ -448,32 +506,32 @@ addCommonOptions(program.command('research <url>').description('Research a page
448
506
  }
449
507
  );
450
508
 
451
- addCommonOptions(
452
- program.command('drill <url>').alias('bosun').description('Drill all components on a page to learn interactions').option('--knowledge <path>', 'Save learned interactions to knowledge file at this URL path').option('--max <count>', 'Maximum number of components to drill', '20')
453
- ).action(async (url, options) => {
454
- try {
455
- const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
456
- await explorBot.start();
509
+ addCommonOptions(program.command('drill <url>').description('Drill all components on a page to learn interactions').option('--knowledge <path>', 'Save learned interactions to knowledge file at this URL path').option('--max <count>', 'Maximum number of components to drill', '20')).action(
510
+ async (url, options) => {
511
+ try {
512
+ const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
513
+ await explorBot.start();
457
514
 
458
- await explorBot.visit(url);
515
+ await explorBot.visit(url);
459
516
 
460
- const plan = await explorBot.agentBosun().drill({
461
- knowledgePath: options.knowledge,
462
- maxComponents: Number.parseInt(options.max, 10),
463
- interactive: false,
464
- });
517
+ const plan = await explorBot.agentBosun().drill({
518
+ knowledgePath: options.knowledge,
519
+ maxComponents: Number.parseInt(options.max, 10),
520
+ interactive: false,
521
+ });
465
522
 
466
- console.log(`\nDrill completed: ${plan.tests.length} components`);
467
- console.log(`Successful: ${plan.tests.filter((t) => t.isSuccessful).length}`);
468
- console.log(`Failed: ${plan.tests.filter((t) => t.hasFailed).length}`);
523
+ console.log(`\nDrill completed: ${plan.tests.length} components`);
524
+ console.log(`Successful: ${plan.tests.filter((t) => t.isSuccessful).length}`);
525
+ console.log(`Failed: ${plan.tests.filter((t) => t.hasFailed).length}`);
469
526
 
470
- await explorBot.stop();
471
- await showStatsAndExit(0);
472
- } catch (error) {
473
- console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
474
- await showStatsAndExit(1);
527
+ await explorBot.stop();
528
+ await showStatsAndExit(0);
529
+ } catch (error) {
530
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
531
+ await showStatsAndExit(1);
532
+ }
475
533
  }
476
- });
534
+ );
477
535
 
478
536
  program
479
537
  .command('context <url>')
@@ -625,18 +683,18 @@ browserCmd
625
683
  });
626
684
 
627
685
  program
628
- .command('extract-styles <agent>')
629
- .description('Extract built-in planning styles to a directory for customization')
630
- .option('-d, --dir <path>', 'Target directory (default: ./rules/<agent>/styles)')
686
+ .command('extract-rules <agent>')
687
+ .description('Extract built-in rules (including planning styles) for an agent to a directory for customization')
688
+ .option('-d, --dir <path>', 'Target directory (default: ./rules/<agent>)')
631
689
  .action(async (agent, options) => {
632
690
  try {
633
691
  const { RulesLoader } = await import('../src/utils/rules-loader.js');
634
- const targetDir = options.dir || path.resolve(`./rules/${agent}/styles`);
635
- const extracted = RulesLoader.extractStyles(agent, targetDir);
692
+ const targetDir = options.dir || path.resolve(`./rules/${agent}`);
693
+ const extracted = RulesLoader.extractRules(agent, targetDir);
636
694
  if (extracted.length === 0) {
637
- console.log('All style files already exist in target directory.');
695
+ console.log('All rule files already exist in target directory.');
638
696
  } else {
639
- console.log(`\nExtracted ${extracted.length} style files to ${targetDir}`);
697
+ console.log(`\nExtracted ${extracted.length} rule files to ${targetDir}`);
640
698
  }
641
699
  } catch (error) {
642
700
  console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
@@ -646,7 +704,6 @@ program
646
704
 
647
705
  program
648
706
  .command('add-rule [agent] [name]')
649
- .alias('rules:add')
650
707
  .description('Create a rule file for an agent')
651
708
  .option('--url <pattern>', 'URL pattern for this rule')
652
709
  .option('-p, --path <path>', 'Working directory path')
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import chalk from 'chalk';
4
5
  import { Command } from 'commander';
6
+ import figureSet from 'figures';
5
7
  import { render } from 'ink';
6
8
  import React from 'react';
7
9
  import { App } from '../src/components/App.js';
@@ -78,13 +80,13 @@ async function showStatsAndExit(code) {
78
80
  }
79
81
  process.exit(code);
80
82
  }
81
- addCommonOptions(program.command('start [path]').alias('sail').description('Start web exploration')).action(async (startPath, options) => {
83
+ addCommonOptions(program.command('start [path]').description('Start web exploration')).action(async (startPath, options) => {
82
84
  setPreserveConsoleLogs(false);
83
85
  const explorBot = new ExplorBot(buildExplorBotOptions(startPath, options));
84
86
  await explorBot.start();
85
87
  await startTUI(explorBot);
86
88
  });
87
- addCommonOptions(program.command('explore <path>').description('Start web exploration (legacy command)').option('--max-tests <count>', 'Maximum number of tests to run')).action(async (explorePath, options) => {
89
+ addCommonOptions(program.command('explore <path>').description('Explore a page autonomously and run invented scenarios').option('--max-tests <count>', 'Maximum number of tests to run')).action(async (explorePath, options) => {
88
90
  try {
89
91
  const explorBot = new ExplorBot(buildExplorBotOptions(explorePath, options));
90
92
  await explorBot.start();
@@ -128,15 +130,34 @@ addCommonOptions(program.command('plan <path>').description('Generate test plan
128
130
  await explorBot.stop();
129
131
  await showStatsAndExit(1);
130
132
  }
133
+ const suite = explorBot.getSuite();
134
+ if (suite && suite.automatedTestCount > 0) {
135
+ const names = suite.getAutomatedTestNames();
136
+ console.log(`\n${chalk.bold.cyan(`Already implemented (${names.length} tests)`)}`);
137
+ for (let i = 0; i < names.length; i++) {
138
+ console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${names[i]}`);
139
+ }
140
+ }
141
+ if (plan?.tests.length) {
142
+ console.log(`\n${chalk.bold.cyan(`New test scenarios (${plan.tests.length})`)}`);
143
+ for (let i = 0; i < plan.tests.length; i++) {
144
+ const t = plan.tests[i];
145
+ console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${t.scenario} ${chalk.dim(`[${t.priority}]`)}`);
146
+ }
147
+ }
131
148
  const savedPath = explorBot.savePlan();
132
149
  const planFile = savedPath ? path.basename(savedPath) : 'plan.md';
133
150
  const cliFlags = [options.path ? `--path ${options.path}` : '', options.session ? '--session' : ''].filter(Boolean).join(' ');
134
151
  const cliSuffix = cliFlags ? ` ${cliFlags}` : '';
135
152
  const lines = [];
136
- lines.push('Run tests:');
137
- lines.push(`\`${cli} test ${planFile} 1${cliSuffix}\` → run first test`);
138
- lines.push(`\`${cli} test ${planFile} 1-3${cliSuffix}\` → run tests 1 to 3`);
139
- lines.push(`\`${cli} test ${planFile} *${cliSuffix}\` run all tests`);
153
+ lines.push('Run commands:');
154
+ lines.push(`\`${cli} test ${planFile} 1${cliSuffix}\` → run first new test`);
155
+ lines.push(`\`${cli} test ${planFile} *${cliSuffix}\` → run all new tests`);
156
+ if (suite && suite.automatedTestCount > 0) {
157
+ for (const f of suite.getAutomatedTestFiles()) {
158
+ lines.push(`\`${cli} rerun ${path.relative(process.cwd(), f)}${cliSuffix}\` → re-run automated tests`);
159
+ }
160
+ }
140
161
  log(parseMarkdownToTerminal(lines.join('\n')));
141
162
  await explorBot.stop();
142
163
  await showStatsAndExit(0);
@@ -240,6 +261,42 @@ addCommonOptions(program.command('test <planfile> [index]').description('Execute
240
261
  await showStatsAndExit(1);
241
262
  }
242
263
  });
264
+ program
265
+ .command('runs [file]')
266
+ .description('List generated test files, or show steps for a specific file')
267
+ .option('-p, --path <path>', 'Working directory path')
268
+ .option('-c, --config <path>', 'Path to configuration file')
269
+ .action(async (file, options) => {
270
+ try {
271
+ await ConfigParser.getInstance().loadConfig({
272
+ config: options.config,
273
+ path: options.path || process.cwd(),
274
+ });
275
+ const explorBot = new ExplorBot({ path: options.path });
276
+ const { RunsCommand } = await import('../src/commands/runs-command.js');
277
+ await new RunsCommand(explorBot).execute(file || '');
278
+ }
279
+ catch (error) {
280
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
281
+ process.exit(1);
282
+ }
283
+ });
284
+ addCommonOptions(program.command('rerun <filename> [index]').description('Re-run generated tests with AI auto-healing')).action(async (filename, index, options) => {
285
+ try {
286
+ const explorBot = new ExplorBot(buildExplorBotOptions(undefined, options));
287
+ await explorBot.start();
288
+ const { RerunCommand } = await import('../src/commands/rerun-command.js');
289
+ const cmd = new RerunCommand(explorBot);
290
+ const args = index ? `${filename} ${index}` : filename;
291
+ await cmd.execute(args);
292
+ await explorBot.stop();
293
+ await showStatsAndExit(0);
294
+ }
295
+ catch (error) {
296
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
297
+ await showStatsAndExit(1);
298
+ }
299
+ });
243
300
  addCommonOptions(program
244
301
  .command('freesail [startUrl]')
245
302
  .description('Continuously explore and navigate to new pages autonomously')
@@ -328,7 +385,6 @@ program
328
385
  });
329
386
  program
330
387
  .command('learn [url] [description]')
331
- .alias('add-knowledge')
332
388
  .description('Add knowledge for URLs')
333
389
  .option('-p, --path <path>', 'Working directory path')
334
390
  .action(async (url, description, options) => {
@@ -395,7 +451,7 @@ addCommonOptions(program.command('research <url>').description('Research a page
395
451
  await showStatsAndExit(1);
396
452
  }
397
453
  });
398
- addCommonOptions(program.command('drill <url>').alias('bosun').description('Drill all components on a page to learn interactions').option('--knowledge <path>', 'Save learned interactions to knowledge file at this URL path').option('--max <count>', 'Maximum number of components to drill', '20')).action(async (url, options) => {
454
+ addCommonOptions(program.command('drill <url>').description('Drill all components on a page to learn interactions').option('--knowledge <path>', 'Save learned interactions to knowledge file at this URL path').option('--max <count>', 'Maximum number of components to drill', '20')).action(async (url, options) => {
399
455
  try {
400
456
  const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
401
457
  await explorBot.start();
@@ -555,19 +611,19 @@ browserCmd
555
611
  }
556
612
  });
557
613
  program
558
- .command('extract-styles <agent>')
559
- .description('Extract built-in planning styles to a directory for customization')
560
- .option('-d, --dir <path>', 'Target directory (default: ./rules/<agent>/styles)')
614
+ .command('extract-rules <agent>')
615
+ .description('Extract built-in rules (including planning styles) for an agent to a directory for customization')
616
+ .option('-d, --dir <path>', 'Target directory (default: ./rules/<agent>)')
561
617
  .action(async (agent, options) => {
562
618
  try {
563
619
  const { RulesLoader } = await import('../src/utils/rules-loader.js');
564
- const targetDir = options.dir || path.resolve(`./rules/${agent}/styles`);
565
- const extracted = RulesLoader.extractStyles(agent, targetDir);
620
+ const targetDir = options.dir || path.resolve(`./rules/${agent}`);
621
+ const extracted = RulesLoader.extractRules(agent, targetDir);
566
622
  if (extracted.length === 0) {
567
- console.log('All style files already exist in target directory.');
623
+ console.log('All rule files already exist in target directory.');
568
624
  }
569
625
  else {
570
- console.log(`\nExtracted ${extracted.length} style files to ${targetDir}`);
626
+ console.log(`\nExtracted ${extracted.length} rule files to ${targetDir}`);
571
627
  }
572
628
  }
573
629
  catch (error) {
@@ -577,7 +633,6 @@ program
577
633
  });
578
634
  program
579
635
  .command('add-rule [agent] [name]')
580
- .alias('rules:add')
581
636
  .description('Create a rule file for an agent')
582
637
  .option('--url <pattern>', 'URL pattern for this rule')
583
638
  .option('-p, --path <path>', 'Working directory path')
@@ -0,0 +1,19 @@
1
+ <healing_approach>
2
+ The failed step was NOT performed. You MUST execute a replacement action.
3
+ Just waiting or diagnosing is NOT enough — you must perform the click/fill/press that was intended.
4
+
5
+ 1. FIRST: Check the page URL and ARIA — are you on the right page?
6
+ - If URL or ARIA shows login/error/404 page → call giveUp immediately
7
+ 2. If ARIA is empty/minimal → page may still be loading:
8
+ - Use xpathCheck() to detect spinners, loaders, or loading indicators on the page
9
+ - Use wait() to let the page load — it returns fresh ARIA automatically
10
+ - Then execute the replacement action with a working locator
11
+ 3. If the target element is visible in ARIA:
12
+ - Use click() with multiple fallback locators (ARIA, CSS, XPath)
13
+ 4. If element is NOT in ARIA but page is correct:
14
+ - Use xpathCheck() to search the full HTML
15
+ - Use research() to get a semantic UI map of the page if needed
16
+ - If found → click it
17
+ - If not → bash to check console logs → giveUp
18
+ 5. Call done() with the command that replaced the failed step
19
+ </healing_approach>
@@ -50,7 +50,7 @@ class Action {
50
50
  return undefined;
51
51
  }
52
52
  }
53
- async capturePageState({ includeScreenshot = false, ariaSnapshot: preCapuredAria } = {}) {
53
+ async capturePageState({ includeScreenshot = false } = {}) {
54
54
  try {
55
55
  const currentState = this.stateManager.getCurrentState();
56
56
  const stateHash = currentState?.hash || 'screenshot';
@@ -90,16 +90,14 @@ class Action {
90
90
  debugLog('Page:', { url, title, size: html.length, html: html.substring(0, 100) });
91
91
  // Capture iframe HTML snapshots
92
92
  const iframeSnapshots = await this.captureIframeSnapshots(html);
93
- let ariaSnapshot = preCapuredAria || null;
93
+ let ariaSnapshot = null;
94
94
  let ariaSnapshotFile = undefined;
95
- if (!ariaSnapshot) {
96
- try {
97
- const page = this.playwrightHelper.page;
98
- ariaSnapshot = await page.locator('body').ariaSnapshot();
99
- }
100
- catch (err) {
101
- debugLog('ARIA snapshot failed:', err instanceof Error ? `${err.message}\n${err.stack}` : err);
102
- }
95
+ try {
96
+ const page = this.playwrightHelper.page;
97
+ ariaSnapshot = await page.locator('body').ariaSnapshot();
98
+ }
99
+ catch (err) {
100
+ debugLog('ARIA snapshot failed:', err instanceof Error ? `${err.message}\n${err.stack}` : err);
103
101
  }
104
102
  if (ariaSnapshot) {
105
103
  const ariaFileName = `${stateHash}_${timestamp}.aria.yaml`;
@@ -1,9 +1,10 @@
1
- import { mkdirSync, writeFileSync } from 'node:fs';
1
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import dedent from 'dedent';
4
4
  import { z } from 'zod';
5
5
  import { ActionResult } from "../action-result.js";
6
6
  import { ConfigParser } from "../config.js";
7
+ import { KnowledgeTracker } from "../knowledge-tracker.js";
7
8
  import { ExperienceTracker } from "../experience-tracker.js";
8
9
  import { Test } from "../test-plan.js";
9
10
  import { createDebug, tag } from "../utils/logger.js";
@@ -329,6 +330,7 @@ export class Historian {
329
330
  if (startUrl) {
330
331
  lines.push('Before(({ I }) => {');
331
332
  lines.push(` I.amOnPage('${this.escapeString(startUrl)}');`);
333
+ lines.push(...this.getKnowledgeLines(startUrl));
332
334
  lines.push('});');
333
335
  lines.push('');
334
336
  }
@@ -356,8 +358,7 @@ export class Historian {
356
358
  lines.push('});');
357
359
  lines.push('');
358
360
  }
359
- const outputDir = ConfigParser.getInstance().getOutputDir();
360
- const testsDir = join(outputDir, 'tests');
361
+ const testsDir = ConfigParser.getInstance().getTestsDir();
361
362
  mkdirSync(testsDir, { recursive: true });
362
363
  const filename = plan.title.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase();
363
364
  const filePath = join(testsDir, `${filename}.js`);
@@ -365,12 +366,42 @@ export class Historian {
365
366
  tag('substep').log(`Saved plan tests to: ${filePath}`);
366
367
  return filePath;
367
368
  }
369
+ rewriteScenarioInFile(filePath, healedSteps) {
370
+ let content = readFileSync(filePath, 'utf-8');
371
+ for (const step of healedSteps) {
372
+ if (!content.includes(step.original))
373
+ continue;
374
+ content = content.replace(step.original, step.healed);
375
+ }
376
+ writeFileSync(filePath, content);
377
+ tag('substep').log(`Updated test file with healed steps: ${filePath}`);
378
+ }
368
379
  getExecutionLabel(exec, fallback) {
369
380
  return exec.input?.explanation || exec.input?.assertion || exec.input?.note || fallback || '';
370
381
  }
371
382
  escapeString(str) {
372
383
  return str.replace(/'/g, "\\'").replace(/\n/g, ' ');
373
384
  }
385
+ getKnowledgeLines(url, indent = ' ') {
386
+ const knowledgeTracker = new KnowledgeTracker();
387
+ const state = new ActionResult({ url });
388
+ const { wait, waitForElement, code } = knowledgeTracker.getStateParameters(state, ['wait', 'waitForElement', 'code']);
389
+ const lines = [];
390
+ if (wait !== undefined) {
391
+ lines.push(`${indent}I.wait(${wait});`);
392
+ }
393
+ if (waitForElement) {
394
+ lines.push(`${indent}I.waitForElement(${JSON.stringify(waitForElement)});`);
395
+ }
396
+ if (code) {
397
+ for (const codeLine of code.split('\n')) {
398
+ const trimmed = codeLine.trim();
399
+ if (trimmed)
400
+ lines.push(`${indent}${trimmed}`);
401
+ }
402
+ }
403
+ return lines;
404
+ }
374
405
  stripComments(code) {
375
406
  return code
376
407
  .split('\n')
@@ -31,6 +31,18 @@ class Navigator {
31
31
  You are given the web page and a message from user.
32
32
  You need to resolve the state of the page based on the message.
33
33
  </task>
34
+
35
+ ${locatorRule}
36
+
37
+ <constraints>
38
+ NEVER navigate away from the base URL domain. Stay on the same origin at all times.
39
+ NEVER attempt to rewrite, replace, mock, or spoof the URL via JavaScript, history API, location assignment, or any client-side trick.
40
+ NEVER use executeScript, executeAsyncScript, or any JS evaluation to change the URL, bypass redirects, or fake the page state.
41
+ If the target URL redirects to an authentication/login page, DO NOT try to force the original URL. Instead:
42
+ 1. Look for credentials in the provided knowledge/hint context and perform a real login through the form.
43
+ 2. If no credentials are available, ask the user for credentials or ask the user to log in manually.
44
+ A redirect to /login, /sign_in, /auth, or similar is a signal that authentication is required — treat it as such, never as an obstacle to bypass.
45
+ </constraints>
34
46
  `;
35
47
  freeSailSystemPrompt = dedent `
36
48
  <role>
@@ -145,6 +157,14 @@ class Navigator {
145
157
  ${message}
146
158
  </message>
147
159
 
160
+ <page>
161
+ ${actionResult.toAiContext()}
162
+
163
+ <page_html>
164
+ ${await actionResult.combinedHtml()}
165
+ </page_html>
166
+ </page>
167
+
148
168
  <task>
149
169
  Identify the actual request of the user.
150
170
  Identify what is expected by user.
@@ -155,25 +175,13 @@ class Navigator {
155
175
  Try various ways to achieve the result
156
176
  </task>
157
177
 
158
-
159
- <page>
160
- ${actionResult.toAiContext()}
161
-
162
- <page_html>
163
- ${await actionResult.simplifiedHtml()}
164
- </page_html>
165
- </page>
166
-
167
-
168
- ${knowledge}
169
-
170
178
  ${actionRule}
171
179
 
172
- ${experience}
180
+ ${RulesLoader.loadRules('navigator', ['multiple-locator', 'output'], actionResult.url || '').replace('{{maxAttempts}}', String(this.MAX_ATTEMPTS))}
173
181
 
174
- ${locatorRule}
182
+ ${experience}
175
183
 
176
- ${RulesLoader.loadRules('navigator', ['multiple-locator', 'output'], actionResult.url || '').replace('{{maxAttempts}}', String(this.MAX_ATTEMPTS))}
184
+ ${knowledge}
177
185
  `;
178
186
  const conversation = this.provider.startConversation(this.systemPrompt, 'navigator');
179
187
  conversation.addUserText(prompt);
@@ -206,7 +214,7 @@ class Navigator {
206
214
  Previous solutions did not work. Here is the full HTML context:
207
215
 
208
216
  <page_html>
209
- ${await actionResult.simplifiedHtml()}
217
+ ${await actionResult.combinedHtml()}
210
218
  </page_html>
211
219
 
212
220
  Please suggest new solutions based on this additional context.
@@ -234,6 +242,7 @@ class Navigator {
234
242
  }
235
243
  if (resolved) {
236
244
  tag('success').log('Navigation resolved successfully');
245
+ await this.experienceTracker.saveSuccessfulResolution(actionResult, message, codeBlock);
237
246
  stop();
238
247
  return;
239
248
  }
@@ -414,6 +423,14 @@ class Navigator {
414
423
  ${message}
415
424
  </message>
416
425
 
426
+ <page>
427
+ ${actionResult.toAiContext()}
428
+
429
+ <page_html>
430
+ ${await actionResult.combinedHtml()}
431
+ </page_html>
432
+ </page>
433
+
417
434
  <task>
418
435
  Identify what assertion the user wants to verify on the page.
419
436
  Propose different CodeceptJS assertion code blocks to verify the expected state.
@@ -427,21 +444,11 @@ class Navigator {
427
444
  Do not generate assertions that would pass even if the specific claim is false.
428
445
  </task>
429
446
 
430
- <page>
431
- ${actionResult.toAiContext()}
432
-
433
- <page_html>
434
- ${await actionResult.simplifiedHtml()}
435
- </page_html>
436
- </page>
437
-
438
- ${knowledge}
439
-
440
447
  ${RulesLoader.loadRules('navigator', ['verification-actions'], actionResult.url || '')}
441
448
 
442
- ${locatorRule}
443
-
444
449
  ${experience}
450
+
451
+ ${knowledge}
445
452
  `;
446
453
  debugLog('Sending verification prompt to AI provider');
447
454
  tag('debug').log('Prompt:', prompt);