explorbot 0.1.21 → 0.1.22

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.
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explorbot",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
@@ -82,7 +82,7 @@
82
82
  "@opentelemetry/sdk-trace-base": "^2.2.0",
83
83
  "@opentelemetry/semantic-conventions": "^1.38.0",
84
84
  "@scalar/openapi-parser": "^0.25.6",
85
- "@testomatio/reporter": "^2.7.9-beta.3-markdown",
85
+ "@testomatio/reporter": "^2.8.4",
86
86
  "ai": "^6.0.6",
87
87
  "axe-core": "^4.11.1",
88
88
  "bash-tool": "^1.3.15",
@@ -108,7 +108,7 @@
108
108
  "micromatch": "^4.0.8",
109
109
  "ora-classic": "^5.4.2",
110
110
  "parse5": "^8.0.0",
111
- "playwright": "^1.59.0",
111
+ "playwright": "^1.60",
112
112
  "react": "^19.1.1",
113
113
  "strip-ansi": "^7.1.2",
114
114
  "turndown": "^7.2.1",
@@ -151,6 +151,18 @@ export class ActionResult {
151
151
  this.verifications ??= {};
152
152
  this.verifications[assertion] = passed;
153
153
  }
154
+ getVerification(message) {
155
+ if (!this.verifications)
156
+ return null;
157
+ if (typeof message === 'string') {
158
+ return this.verifications[message] ?? null;
159
+ }
160
+ for (const [assertion, passed] of Object.entries(this.verifications)) {
161
+ if (message.test(assertion))
162
+ return passed;
163
+ }
164
+ return null;
165
+ }
154
166
  isSameUrl(state) {
155
167
  if (!this.url || this.url === '') {
156
168
  return false;
@@ -70,6 +70,12 @@ class Navigator {
70
70
  this.experienceTracker = experienceTracker || new ExperienceTracker();
71
71
  this.hooksRunner = new HooksRunner(explorer, explorer.getConfig());
72
72
  }
73
+ get verifyAttempts() {
74
+ return this.explorer.getConfig().ai?.agents?.navigator?.verifyAttempts ?? 3;
75
+ }
76
+ get verifyTimeout() {
77
+ return this.explorer.getConfig().ai?.agents?.navigator?.verifyTimeout ?? 1500;
78
+ }
73
79
  getBaseOrigin() {
74
80
  const baseUrl = this.explorer.getConfig().playwright.url;
75
81
  try {
@@ -565,6 +571,11 @@ class Navigator {
565
571
  async verifyState(message, actionResult) {
566
572
  tag('info').log('AI Navigator verifying state at', actionResult.url);
567
573
  debugLog('Verification message:', message);
574
+ const cachedVerification = actionResult.getVerification(message);
575
+ if (cachedVerification !== null) {
576
+ tag('substep').log(`Reusing cached verification: ${cachedVerification ? 'PASS' : 'FAIL'}`);
577
+ return { verified: cachedVerification, successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
578
+ }
568
579
  let knowledge = '';
569
580
  let experience = '';
570
581
  const relevantKnowledge = this.knowledgeTracker.getRelevantKnowledge(actionResult);
@@ -584,6 +595,20 @@ class Navigator {
584
595
  experience = renderExperienceToc(toc);
585
596
  }
586
597
  }
598
+ const priorVerifications = Object.entries(actionResult.verifications ?? {});
599
+ let verificationContext = '';
600
+ if (priorVerifications.length > 0) {
601
+ const lines = priorVerifications.map(([claim, passed]) => `- "${claim}" → ${passed ? 'passed' : 'failed'}`).join('\n');
602
+ verificationContext = dedent `
603
+ <already_verified>
604
+ These claims were already checked on this page:
605
+ ${lines}
606
+
607
+ If the claim to verify has the same meaning as one above (even if worded differently), do NOT write any assertion code.
608
+ Respond with a single line and nothing else: ALREADY_VERIFIED: <exact text of the matching claim>
609
+ </already_verified>
610
+ `;
611
+ }
587
612
  const prompt = dedent `
588
613
  <message>
589
614
  ${message}
@@ -597,11 +622,13 @@ class Navigator {
597
622
  </page_html>
598
623
  </page>
599
624
 
625
+ ${verificationContext}
626
+
600
627
  <task>
601
628
  Identify what assertion the user wants to verify on the page.
602
- Propose different CodeceptJS assertion code blocks to verify the expected state.
629
+ Propose 2-3 strong, distinct CodeceptJS assertion code blocks that each directly prove the claim.
603
630
  Use only data from the <page> context to plan the verification.
604
- Try various locators and approaches to verify the assertion.
631
+ Prefer the fewest, most specific assertions over many variants of the same locator.
605
632
 
606
633
  IMPORTANT: Each code block must verify the SPECIFIC claim in the message, not just a generic aspect of it.
607
634
  Bad: I.seeElement({"role":"button","aria-pressed":"true"}) — matches ANY button, not the specific one
@@ -620,50 +647,83 @@ class Navigator {
620
647
  tag('debug').log('Prompt:', prompt);
621
648
  const conversation = this.provider.startConversation(this.systemPrompt, 'navigator');
622
649
  conversation.addUserText(prompt);
650
+ let alreadyVerified = false;
623
651
  const tools = this.buildExperienceTools();
624
652
  let codeBlocks = [];
625
653
  const successfulCodes = [];
626
654
  const assertionSteps = [];
627
655
  const action = this.explorer.createAction();
628
- await loop(async ({ stop, iteration }) => {
629
- if (codeBlocks.length === 0) {
630
- const result = await this.provider.invokeConversation(conversation, tools);
631
- if (!result)
656
+ let failures = 0;
657
+ const page = this.explorer.playwrightHelper?.page;
658
+ const originalTimeout = this.explorer.playwrightHelper?.options?.timeout ?? 3000;
659
+ page?.setDefaultTimeout(this.verifyTimeout);
660
+ try {
661
+ await loop(async ({ stop, iteration }) => {
662
+ if (codeBlocks.length === 0) {
663
+ const result = await this.provider.invokeConversation(conversation, tools);
664
+ if (!result)
665
+ return;
666
+ const aiResponse = result?.response?.text ?? '';
667
+ debugLog('Received AI response:', aiResponse.length, 'characters');
668
+ tag('step').log('Verifying assertion...');
669
+ if (this.checkAlreadyVerified(aiResponse, actionResult)) {
670
+ alreadyVerified = true;
671
+ stop();
672
+ return;
673
+ }
674
+ codeBlocks = extractCodeBlocks(aiResponse);
675
+ }
676
+ if (codeBlocks.length === 0) {
632
677
  return;
633
- const aiResponse = result?.response?.text;
634
- debugLog('Received AI response:', aiResponse?.length ?? 0, 'characters');
635
- tag('step').log('Verifying assertion...');
636
- codeBlocks = extractCodeBlocks(aiResponse ?? '');
637
- }
638
- if (codeBlocks.length === 0) {
639
- return;
640
- }
641
- const codeBlock = codeBlocks[iteration - 1];
642
- if (!codeBlock) {
643
- stop();
644
- return;
645
- }
646
- await this.explorer.switchToMainFrame();
647
- const verified = await action.attempt(codeBlock, message, false);
648
- if (verified) {
649
- tag('success').log('Verification passed');
650
- successfulCodes.push(codeBlock);
651
- assertionSteps.push(...action.assertionSteps);
652
- }
653
- }, {
654
- maxAttempts: this.MAX_ATTEMPTS,
655
- observability: {
656
- agent: 'navigator',
657
- },
658
- catch: async (error) => {
659
- debugLog(error);
660
- },
661
- });
662
- const totalAttempted = Math.min(codeBlocks.length, this.MAX_ATTEMPTS);
663
- const verified = totalAttempted <= 1 ? successfulCodes.length > 0 : successfulCodes.length > totalAttempted / 2;
678
+ }
679
+ const codeBlock = codeBlocks[iteration - 1];
680
+ if (!codeBlock) {
681
+ stop();
682
+ return;
683
+ }
684
+ await this.explorer.switchToMainFrame();
685
+ const verified = await action.attempt(codeBlock, message, false);
686
+ if (verified) {
687
+ tag('success').log('Verification passed');
688
+ successfulCodes.push(codeBlock);
689
+ assertionSteps.push(...action.assertionSteps);
690
+ }
691
+ else {
692
+ failures++;
693
+ }
694
+ const target = Math.min(codeBlocks.length, this.verifyAttempts);
695
+ const majorityNeeded = Math.floor(target / 2) + 1;
696
+ if (successfulCodes.length >= majorityNeeded || failures > target - majorityNeeded) {
697
+ stop();
698
+ }
699
+ }, {
700
+ maxAttempts: this.verifyAttempts,
701
+ observability: {
702
+ agent: 'navigator',
703
+ },
704
+ catch: async (error) => {
705
+ debugLog(error);
706
+ },
707
+ });
708
+ }
709
+ finally {
710
+ page?.setDefaultTimeout(originalTimeout);
711
+ }
712
+ const totalAttempted = Math.min(codeBlocks.length, this.verifyAttempts);
713
+ const majorityNeeded = Math.floor(totalAttempted / 2) + 1;
714
+ let verified = successfulCodes.length >= majorityNeeded;
715
+ if (alreadyVerified)
716
+ verified = true;
664
717
  actionResult.addVerification(message, verified);
665
718
  this.explorer.getStateManager().updateState(actionResult);
666
719
  return { verified, successfulCodes, assertionSteps, totalAttempted };
667
720
  }
721
+ checkAlreadyVerified(aiResponse, actionResult) {
722
+ const verifiedMatch = aiResponse.match(/ALREADY_VERIFIED:\s*(.+)/i);
723
+ if (!verifiedMatch)
724
+ return false;
725
+ const claim = verifiedMatch[1].trim().replace(/^["']|["']$/g, '');
726
+ return actionResult.getVerification(claim) === true;
727
+ }
668
728
  }
669
729
  export { Navigator };
@@ -279,6 +279,12 @@ export class Pilot {
279
279
  - "Delete X" → X must be gone. Clicking delete is NOT enough.
280
280
  - "Edit X" → updated value must be persisted (visible in list/detail). Opening edit is NOT enough; redirect after save with the new value visible IS enough.
281
281
  - Negative tests ("without a name", "invalid", "duplicate", "unauthorized") → success means the system PREVENTED the action with validation/error.
282
+ - Navigation-prefixed titles ("Access/Open/Go to X to <do Y>") → the goal is <do Y>; reaching X is
283
+ only a milestone. A satisfied milestone (tab active, panel/prompt visible, list shown) is NEVER a pass
284
+ if <do Y> did not occur this run.
285
+ - If the page reveals the goal cannot be performed here — required control absent, integration not
286
+ connected, or only a setup/connect/empty-state prompt is shown — vote "skipped" (prerequisites unmet),
287
+ never "pass".
282
288
 
283
289
  PROVENANCE: the entity you cite as proof must appear by name in <notes> or
284
290
  <session_log> tool inputs for THIS run. Name absent from tester activity = stale
@@ -29,6 +29,15 @@ const config = {
29
29
  // agentic model for decision making
30
30
  agenticModel: openrouter('minimax/minimax-m2.5:nitro'),
31
31
  },
32
+
33
+ reporter: {
34
+ // Save a local HTML report after each run.
35
+ html: true,
36
+ // Save a local markdown report after each run.
37
+ markdown: true,
38
+ // Group runs by title in Testomat.io / HTML reports. Defaults to today's date — customize or remove.
39
+ runGroup: new Date().toISOString().slice(0, 10),
40
+ },
32
41
  };
33
42
 
34
43
  export default config;
@@ -11,7 +11,7 @@ export class Reporter {
11
11
  constructor(config, stateManager) {
12
12
  this.reporterEnabled = Reporter.resolveEnabled(config);
13
13
  this.stateManager = stateManager;
14
- if (this.reporterEnabled && (!process.env.TESTOMATIO || config?.html)) {
14
+ if (this.reporterEnabled && config?.html) {
15
15
  this.configureHtmlPipe();
16
16
  }
17
17
  if (this.reporterEnabled && config?.markdown) {
@@ -45,6 +45,8 @@ export class Reporter {
45
45
  return true;
46
46
  if (config?.enabled === false)
47
47
  return false;
48
+ if (config?.html || config?.markdown)
49
+ return true;
48
50
  return Boolean(process.env.TESTOMATIO);
49
51
  }
50
52
  configureHtmlPipe() {
@@ -68,13 +70,9 @@ export class Reporter {
68
70
  configureRunGroup(runGroup) {
69
71
  if (process.env.TESTOMATIO_RUNGROUP_TITLE)
70
72
  return;
71
- if (runGroup === null)
72
- return;
73
- if (runGroup) {
74
- process.env.TESTOMATIO_RUNGROUP_TITLE = runGroup;
73
+ if (!runGroup)
75
74
  return;
76
- }
77
- process.env.TESTOMATIO_RUNGROUP_TITLE = `Explorbot ${new Date().toISOString().slice(0, 10)}`;
75
+ process.env.TESTOMATIO_RUNGROUP_TITLE = runGroup;
78
76
  }
79
77
  async startRun() {
80
78
  if (this.isRunStarted) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explorbot",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
@@ -82,7 +82,7 @@
82
82
  "@opentelemetry/sdk-trace-base": "^2.2.0",
83
83
  "@opentelemetry/semantic-conventions": "^1.38.0",
84
84
  "@scalar/openapi-parser": "^0.25.6",
85
- "@testomatio/reporter": "^2.7.9-beta.3-markdown",
85
+ "@testomatio/reporter": "^2.8.4",
86
86
  "ai": "^6.0.6",
87
87
  "axe-core": "^4.11.1",
88
88
  "bash-tool": "^1.3.15",
@@ -108,7 +108,7 @@
108
108
  "micromatch": "^4.0.8",
109
109
  "ora-classic": "^5.4.2",
110
110
  "parse5": "^8.0.0",
111
- "playwright": "^1.59.0",
111
+ "playwright": "^1.60",
112
112
  "react": "^19.1.1",
113
113
  "strip-ansi": "^7.1.2",
114
114
  "turndown": "^7.2.1",
@@ -207,6 +207,17 @@ export class ActionResult implements ActionResultData {
207
207
  this.verifications[assertion] = passed;
208
208
  }
209
209
 
210
+ getVerification(message: string | RegExp): boolean | null {
211
+ if (!this.verifications) return null;
212
+ if (typeof message === 'string') {
213
+ return this.verifications[message] ?? null;
214
+ }
215
+ for (const [assertion, passed] of Object.entries(this.verifications)) {
216
+ if (message.test(assertion)) return passed;
217
+ }
218
+ return null;
219
+ }
220
+
210
221
  isSameUrl(state: WebPageState): boolean {
211
222
  if (!this.url || this.url === '') {
212
223
  return false;
@@ -82,6 +82,14 @@ class Navigator implements Agent {
82
82
  this.hooksRunner = new HooksRunner(explorer, explorer.getConfig());
83
83
  }
84
84
 
85
+ private get verifyAttempts(): number {
86
+ return this.explorer.getConfig().ai?.agents?.navigator?.verifyAttempts ?? 3;
87
+ }
88
+
89
+ private get verifyTimeout(): number {
90
+ return this.explorer.getConfig().ai?.agents?.navigator?.verifyTimeout ?? 1500;
91
+ }
92
+
85
93
  private getBaseOrigin(): string | null {
86
94
  const baseUrl = this.explorer.getConfig().playwright.url;
87
95
  try {
@@ -623,6 +631,12 @@ class Navigator implements Agent {
623
631
  tag('info').log('AI Navigator verifying state at', actionResult.url);
624
632
  debugLog('Verification message:', message);
625
633
 
634
+ const cachedVerification = actionResult.getVerification(message);
635
+ if (cachedVerification !== null) {
636
+ tag('substep').log(`Reusing cached verification: ${cachedVerification ? 'PASS' : 'FAIL'}`);
637
+ return { verified: cachedVerification, successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
638
+ }
639
+
626
640
  let knowledge = '';
627
641
  let experience = '';
628
642
 
@@ -645,6 +659,21 @@ class Navigator implements Agent {
645
659
  }
646
660
  }
647
661
 
662
+ const priorVerifications = Object.entries(actionResult.verifications ?? {});
663
+ let verificationContext = '';
664
+ if (priorVerifications.length > 0) {
665
+ const lines = priorVerifications.map(([claim, passed]) => `- "${claim}" → ${passed ? 'passed' : 'failed'}`).join('\n');
666
+ verificationContext = dedent`
667
+ <already_verified>
668
+ These claims were already checked on this page:
669
+ ${lines}
670
+
671
+ If the claim to verify has the same meaning as one above (even if worded differently), do NOT write any assertion code.
672
+ Respond with a single line and nothing else: ALREADY_VERIFIED: <exact text of the matching claim>
673
+ </already_verified>
674
+ `;
675
+ }
676
+
648
677
  const prompt = dedent`
649
678
  <message>
650
679
  ${message}
@@ -658,11 +687,13 @@ class Navigator implements Agent {
658
687
  </page_html>
659
688
  </page>
660
689
 
690
+ ${verificationContext}
691
+
661
692
  <task>
662
693
  Identify what assertion the user wants to verify on the page.
663
- Propose different CodeceptJS assertion code blocks to verify the expected state.
694
+ Propose 2-3 strong, distinct CodeceptJS assertion code blocks that each directly prove the claim.
664
695
  Use only data from the <page> context to plan the verification.
665
- Try various locators and approaches to verify the assertion.
696
+ Prefer the fewest, most specific assertions over many variants of the same locator.
666
697
 
667
698
  IMPORTANT: Each code block must verify the SPECIFIC claim in the message, not just a generic aspect of it.
668
699
  Bad: I.seeElement({"role":"button","aria-pressed":"true"}) — matches ANY button, not the specific one
@@ -684,6 +715,7 @@ class Navigator implements Agent {
684
715
  const conversation = this.provider.startConversation(this.systemPrompt, 'navigator');
685
716
  conversation.addUserText(prompt);
686
717
 
718
+ let alreadyVerified = false;
687
719
  const tools = this.buildExperienceTools();
688
720
 
689
721
  let codeBlocks: string[] = [];
@@ -691,57 +723,90 @@ class Navigator implements Agent {
691
723
  const assertionSteps: Array<{ name: string; args: any[] }> = [];
692
724
 
693
725
  const action = this.explorer.createAction();
726
+ let failures = 0;
694
727
 
695
- await loop(
696
- async ({ stop, iteration }) => {
697
- if (codeBlocks.length === 0) {
698
- const result = await this.provider.invokeConversation(conversation, tools);
699
- if (!result) return;
700
- const aiResponse = result?.response?.text;
701
- debugLog('Received AI response:', aiResponse?.length ?? 0, 'characters');
702
- tag('step').log('Verifying assertion...');
703
- codeBlocks = extractCodeBlocks(aiResponse ?? '');
704
- }
728
+ const page = this.explorer.playwrightHelper?.page;
729
+ const originalTimeout = this.explorer.playwrightHelper?.options?.timeout ?? 3000;
730
+ page?.setDefaultTimeout(this.verifyTimeout);
705
731
 
706
- if (codeBlocks.length === 0) {
707
- return;
708
- }
732
+ try {
733
+ await loop(
734
+ async ({ stop, iteration }) => {
735
+ if (codeBlocks.length === 0) {
736
+ const result = await this.provider.invokeConversation(conversation, tools);
737
+ if (!result) return;
738
+ const aiResponse = result?.response?.text ?? '';
739
+ debugLog('Received AI response:', aiResponse.length, 'characters');
740
+ tag('step').log('Verifying assertion...');
741
+
742
+ if (this.checkAlreadyVerified(aiResponse, actionResult)) {
743
+ alreadyVerified = true;
744
+ stop();
745
+ return;
746
+ }
709
747
 
710
- const codeBlock = codeBlocks[iteration - 1];
711
- if (!codeBlock) {
712
- stop();
713
- return;
714
- }
748
+ codeBlocks = extractCodeBlocks(aiResponse);
749
+ }
715
750
 
716
- await this.explorer.switchToMainFrame();
751
+ if (codeBlocks.length === 0) {
752
+ return;
753
+ }
717
754
 
718
- const verified = await action.attempt(codeBlock, message, false);
755
+ const codeBlock = codeBlocks[iteration - 1];
756
+ if (!codeBlock) {
757
+ stop();
758
+ return;
759
+ }
719
760
 
720
- if (verified) {
721
- tag('success').log('Verification passed');
722
- successfulCodes.push(codeBlock);
723
- assertionSteps.push(...action.assertionSteps);
724
- }
725
- },
726
- {
727
- maxAttempts: this.MAX_ATTEMPTS,
728
- observability: {
729
- agent: 'navigator',
730
- },
731
- catch: async (error) => {
732
- debugLog(error);
761
+ await this.explorer.switchToMainFrame();
762
+
763
+ const verified = await action.attempt(codeBlock, message, false);
764
+
765
+ if (verified) {
766
+ tag('success').log('Verification passed');
767
+ successfulCodes.push(codeBlock);
768
+ assertionSteps.push(...action.assertionSteps);
769
+ } else {
770
+ failures++;
771
+ }
772
+
773
+ const target = Math.min(codeBlocks.length, this.verifyAttempts);
774
+ const majorityNeeded = Math.floor(target / 2) + 1;
775
+ if (successfulCodes.length >= majorityNeeded || failures > target - majorityNeeded) {
776
+ stop();
777
+ }
733
778
  },
734
- }
735
- );
779
+ {
780
+ maxAttempts: this.verifyAttempts,
781
+ observability: {
782
+ agent: 'navigator',
783
+ },
784
+ catch: async (error) => {
785
+ debugLog(error);
786
+ },
787
+ }
788
+ );
789
+ } finally {
790
+ page?.setDefaultTimeout(originalTimeout);
791
+ }
736
792
 
737
- const totalAttempted = Math.min(codeBlocks.length, this.MAX_ATTEMPTS);
738
- const verified = totalAttempted <= 1 ? successfulCodes.length > 0 : successfulCodes.length > totalAttempted / 2;
793
+ const totalAttempted = Math.min(codeBlocks.length, this.verifyAttempts);
794
+ const majorityNeeded = Math.floor(totalAttempted / 2) + 1;
795
+ let verified = successfulCodes.length >= majorityNeeded;
796
+ if (alreadyVerified) verified = true;
739
797
 
740
798
  actionResult.addVerification(message, verified);
741
799
  this.explorer.getStateManager().updateState(actionResult);
742
800
 
743
801
  return { verified, successfulCodes, assertionSteps, totalAttempted };
744
802
  }
803
+
804
+ private checkAlreadyVerified(aiResponse: string, actionResult: ActionResult): boolean {
805
+ const verifiedMatch = aiResponse.match(/ALREADY_VERIFIED:\s*(.+)/i);
806
+ if (!verifiedMatch) return false;
807
+ const claim = verifiedMatch[1].trim().replace(/^["']|["']$/g, '');
808
+ return actionResult.getVerification(claim) === true;
809
+ }
745
810
  }
746
811
 
747
812
  export { Navigator };
package/src/ai/pilot.ts CHANGED
@@ -322,6 +322,12 @@ export class Pilot implements Agent {
322
322
  - "Delete X" → X must be gone. Clicking delete is NOT enough.
323
323
  - "Edit X" → updated value must be persisted (visible in list/detail). Opening edit is NOT enough; redirect after save with the new value visible IS enough.
324
324
  - Negative tests ("without a name", "invalid", "duplicate", "unauthorized") → success means the system PREVENTED the action with validation/error.
325
+ - Navigation-prefixed titles ("Access/Open/Go to X to <do Y>") → the goal is <do Y>; reaching X is
326
+ only a milestone. A satisfied milestone (tab active, panel/prompt visible, list shown) is NEVER a pass
327
+ if <do Y> did not occur this run.
328
+ - If the page reveals the goal cannot be performed here — required control absent, integration not
329
+ connected, or only a setup/connect/empty-state prompt is shown — vote "skipped" (prerequisites unmet),
330
+ never "pass".
325
331
 
326
332
  PROVENANCE: the entity you cite as proof must appear by name in <notes> or
327
333
  <session_log> tool inputs for THIS run. Name absent from tester activity = stale
@@ -30,6 +30,15 @@ const config = {
30
30
  // agentic model for decision making
31
31
  agenticModel: openrouter('minimax/minimax-m2.5:nitro'),
32
32
  },
33
+
34
+ reporter: {
35
+ // Save a local HTML report after each run.
36
+ html: true,
37
+ // Save a local markdown report after each run.
38
+ markdown: true,
39
+ // Group runs by title in Testomat.io / HTML reports. Defaults to today's date — customize or remove.
40
+ runGroup: new Date().toISOString().slice(0, 10),
41
+ },
33
42
  };
34
43
 
35
44
  export default config;
package/src/config.ts CHANGED
@@ -84,6 +84,8 @@ interface PilotAgentConfig extends AgentConfig {
84
84
  interface NavigatorAgentConfig extends AgentConfig {
85
85
  addHtmlOnTry?: number;
86
86
  maxAttempts?: number;
87
+ verifyAttempts?: number;
88
+ verifyTimeout?: number;
87
89
  }
88
90
 
89
91
  type HealFn = (ctx: { I: any }) => Promise<void> | void;
package/src/reporter.ts CHANGED
@@ -29,7 +29,7 @@ export class Reporter {
29
29
  this.reporterEnabled = Reporter.resolveEnabled(config);
30
30
  this.stateManager = stateManager;
31
31
 
32
- if (this.reporterEnabled && (!process.env.TESTOMATIO || config?.html)) {
32
+ if (this.reporterEnabled && config?.html) {
33
33
  this.configureHtmlPipe();
34
34
  }
35
35
 
@@ -63,6 +63,7 @@ export class Reporter {
63
63
  static resolveEnabled(config?: ReporterConfig): boolean {
64
64
  if (config?.enabled === true) return true;
65
65
  if (config?.enabled === false) return false;
66
+ if (config?.html || config?.markdown) return true;
66
67
  return Boolean(process.env.TESTOMATIO);
67
68
  }
68
69
 
@@ -88,12 +89,8 @@ export class Reporter {
88
89
 
89
90
  private configureRunGroup(runGroup: string | null | undefined): void {
90
91
  if (process.env.TESTOMATIO_RUNGROUP_TITLE) return;
91
- if (runGroup === null) return;
92
- if (runGroup) {
93
- process.env.TESTOMATIO_RUNGROUP_TITLE = runGroup;
94
- return;
95
- }
96
- process.env.TESTOMATIO_RUNGROUP_TITLE = `Explorbot ${new Date().toISOString().slice(0, 10)}`;
92
+ if (!runGroup) return;
93
+ process.env.TESTOMATIO_RUNGROUP_TITLE = runGroup;
97
94
  }
98
95
 
99
96
  async startRun(): Promise<void> {