explorbot 0.5.9 → 0.6.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 (167) hide show
  1. package/README.md +1 -1
  2. package/bin/explorbot-cli.ts +495 -482
  3. package/boat/api-tester/src/ai/curler.ts +1 -1
  4. package/boat/api-tester/src/cli.ts +4 -4
  5. package/boat/api-tester/src/commands/init-command.ts +2 -0
  6. package/boat/doc-collector/bin/doc-collector-cli.ts +2 -1
  7. package/boat/doc-collector/src/screenshots.ts +1 -1
  8. package/boat/prima/bin/prima-cli.ts +2 -1
  9. package/dist/bin/explorbot-cli.js +456 -444
  10. package/dist/boat/api-tester/bin/apibot-cli.js +2 -1
  11. package/dist/boat/api-tester/src/ai/curler.js +1 -1
  12. package/dist/boat/api-tester/src/cli.js +4 -4
  13. package/dist/boat/api-tester/src/commands/init-command.js +2 -0
  14. package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -1
  15. package/dist/boat/prima/bin/prima-cli.js +2 -1
  16. package/dist/models.json +1 -1
  17. package/dist/package.json +2 -1
  18. package/dist/rules/navigator/output.md +2 -8
  19. package/dist/rules/navigator/verification-actions.md +6 -2
  20. package/dist/rules/planner/ignored-pages.md +1 -0
  21. package/dist/src/action-result.js +1 -1
  22. package/dist/src/action.d.ts +0 -1
  23. package/dist/src/action.js +0 -4
  24. package/dist/src/ai/captain/idle-mode.js +42 -26
  25. package/dist/src/ai/captain/mixin.d.ts +2 -0
  26. package/dist/src/ai/captain/web-mode.js +11 -0
  27. package/dist/src/ai/captain.d.ts +6 -4
  28. package/dist/src/ai/captain.js +25 -1
  29. package/dist/src/ai/driller.js +1 -1
  30. package/dist/src/ai/experience-compactor.js +2 -2
  31. package/dist/src/ai/judge.d.ts +7 -0
  32. package/dist/src/ai/judge.js +8 -1
  33. package/dist/src/ai/navigator.d.ts +1 -0
  34. package/dist/src/ai/navigator.js +11 -7
  35. package/dist/src/ai/planner/subpages.js +1 -1
  36. package/dist/src/ai/planner.d.ts +12 -5
  37. package/dist/src/ai/planner.js +138 -86
  38. package/dist/src/ai/provider.d.ts +1 -1
  39. package/dist/src/ai/provider.js +16 -5
  40. package/dist/src/ai/rerunner.js +1 -1
  41. package/dist/src/ai/researcher/cache.js +14 -6
  42. package/dist/src/ai/researcher/deep-analysis.d.ts +2 -1
  43. package/dist/src/ai/researcher/deep-analysis.js +7 -2
  44. package/dist/src/ai/researcher/fingerprint-worker.js +4 -4
  45. package/dist/src/ai/researcher.js +44 -24
  46. package/dist/src/ai/rules.js +6 -7
  47. package/dist/src/ai/scout.d.ts +12 -2
  48. package/dist/src/ai/scout.js +103 -8
  49. package/dist/src/ai/tester.d.ts +9 -5
  50. package/dist/src/ai/tester.js +22 -42
  51. package/dist/src/ai/tools.js +10 -8
  52. package/dist/src/application-spec.d.ts +2 -0
  53. package/dist/src/application-spec.js +17 -12
  54. package/dist/src/commands/context-experience-command.js +0 -1
  55. package/dist/src/commands/context-knowledge-command.js +0 -1
  56. package/dist/src/commands/explore-command.d.ts +3 -2
  57. package/dist/src/commands/explore-command.js +35 -23
  58. package/dist/src/commands/help-json-command.js +3 -1
  59. package/dist/src/commands/init-command.js +8 -4
  60. package/dist/src/commands/learn-command.js +0 -2
  61. package/dist/src/commands/options/auth-option.d.ts +6 -0
  62. package/dist/src/commands/options/auth-option.js +14 -0
  63. package/dist/src/commands/options/index.d.ts +2 -0
  64. package/dist/src/commands/options/index.js +2 -0
  65. package/dist/src/commands/plan-command.d.ts +3 -0
  66. package/dist/src/commands/plan-command.js +43 -17
  67. package/dist/src/commands/plan-save-command.js +6 -17
  68. package/dist/src/commands/plans-command.js +1 -1
  69. package/dist/src/commands/test-command.js +12 -2
  70. package/dist/src/components/App.d.ts +1 -2
  71. package/dist/src/components/App.js +1 -3
  72. package/dist/src/components/InputPane.js +0 -5
  73. package/dist/src/config.d.ts +3 -2
  74. package/dist/src/config.js +3 -12
  75. package/dist/src/explorbot.d.ts +3 -2
  76. package/dist/src/explorbot.js +32 -10
  77. package/dist/src/explorer.js +6 -7
  78. package/dist/src/knowledge-tracker.d.ts +2 -2
  79. package/dist/src/knowledge-tracker.js +18 -6
  80. package/dist/src/playwright-recorder.d.ts +0 -1
  81. package/dist/src/playwright-recorder.js +0 -2
  82. package/dist/src/reporter.d.ts +1 -1
  83. package/dist/src/reporter.js +3 -3
  84. package/dist/src/test-plan.js +1 -1
  85. package/dist/src/utils/error-page.d.ts +17 -0
  86. package/dist/src/utils/error-page.js +53 -2
  87. package/dist/src/utils/html-diff.d.ts +1 -2
  88. package/dist/src/utils/html-diff.js +4 -44
  89. package/dist/src/utils/logger.js +27 -10
  90. package/dist/src/utils/markdown-terminal.js +1 -2
  91. package/dist/src/utils/next-steps.js +20 -16
  92. package/dist/src/utils/page-readiness.d.ts +2 -1
  93. package/dist/src/utils/page-readiness.js +24 -5
  94. package/dist/src/utils/test-plan-markdown.d.ts +3 -0
  95. package/dist/src/utils/test-plan-markdown.js +35 -4
  96. package/dist/src/utils/url-matcher.d.ts +1 -0
  97. package/dist/src/utils/url-matcher.js +5 -0
  98. package/docs/basics/providers.md +1 -1
  99. package/docs/contributing/regression-tests.md +19 -4
  100. package/docs/reference/commands.md +18 -2
  101. package/docs/reference/configuration.md +1 -1
  102. package/docs/web-testing/agents.md +8 -0
  103. package/docs/web-testing/customization.md +2 -0
  104. package/docs/web-testing/researcher.md +1 -1
  105. package/docs/workflow/agentic-usage.md +7 -0
  106. package/docs/workflow/knowledge.md +6 -0
  107. package/models.json +1 -1
  108. package/package.json +2 -1
  109. package/rules/navigator/output.md +2 -8
  110. package/rules/navigator/verification-actions.md +6 -2
  111. package/rules/planner/ignored-pages.md +1 -0
  112. package/src/action-result.ts +1 -2
  113. package/src/action.ts +0 -5
  114. package/src/ai/captain/idle-mode.ts +38 -26
  115. package/src/ai/captain/mixin.ts +2 -1
  116. package/src/ai/captain/web-mode.ts +12 -0
  117. package/src/ai/captain.ts +25 -7
  118. package/src/ai/driller.ts +1 -2
  119. package/src/ai/experience-compactor.ts +2 -2
  120. package/src/ai/judge.ts +10 -1
  121. package/src/ai/navigator.ts +12 -7
  122. package/src/ai/planner/subpages.ts +1 -2
  123. package/src/ai/planner.ts +140 -92
  124. package/src/ai/provider.ts +15 -6
  125. package/src/ai/rerunner.ts +1 -2
  126. package/src/ai/researcher/cache.ts +16 -7
  127. package/src/ai/researcher/deep-analysis.ts +7 -2
  128. package/src/ai/researcher/fingerprint-worker.ts +5 -5
  129. package/src/ai/researcher.ts +44 -28
  130. package/src/ai/rules.ts +6 -7
  131. package/src/ai/scout.ts +98 -10
  132. package/src/ai/tester.ts +29 -47
  133. package/src/ai/tools.ts +11 -9
  134. package/src/application-spec.ts +19 -13
  135. package/src/commands/clean-command.ts +1 -1
  136. package/src/commands/context-command.ts +0 -1
  137. package/src/commands/context-experience-command.ts +0 -1
  138. package/src/commands/context-knowledge-command.ts +0 -1
  139. package/src/commands/explore-command.ts +36 -23
  140. package/src/commands/help-json-command.ts +8 -6
  141. package/src/commands/init-command.ts +10 -3
  142. package/src/commands/learn-command.ts +0 -2
  143. package/src/commands/options/auth-option.ts +15 -0
  144. package/src/commands/options/index.ts +2 -0
  145. package/src/commands/plan-command.ts +44 -17
  146. package/src/commands/plan-save-command.ts +6 -17
  147. package/src/commands/plans-command.ts +1 -1
  148. package/src/commands/test-command.ts +11 -2
  149. package/src/components/App.tsx +2 -6
  150. package/src/components/InputPane.tsx +0 -5
  151. package/src/config.ts +5 -16
  152. package/src/experience-tracker.ts +1 -1
  153. package/src/explorbot.ts +31 -11
  154. package/src/explorer.ts +6 -7
  155. package/src/knowledge-tracker.ts +18 -7
  156. package/src/playwright-recorder.ts +0 -2
  157. package/src/reporter.ts +3 -4
  158. package/src/test-plan.ts +1 -2
  159. package/src/utils/error-page.ts +64 -2
  160. package/src/utils/html-diff.ts +4 -56
  161. package/src/utils/html.ts +0 -1
  162. package/src/utils/logger.ts +30 -12
  163. package/src/utils/markdown-terminal.ts +1 -2
  164. package/src/utils/next-steps.ts +19 -17
  165. package/src/utils/page-readiness.ts +33 -7
  166. package/src/utils/test-plan-markdown.ts +36 -4
  167. package/src/utils/url-matcher.ts +6 -0
package/src/ai/tools.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { tool } from 'ai';
2
2
  import dedent from 'dedent';
3
3
  import { z } from 'zod';
4
- import { ActionResult, type PageDiff, type ToolResultMetadata } from '../action-result.ts';
4
+ import { ActionResult, type PageDiff } from '../action-result.ts';
5
5
  import type { ExecutedStep } from '../action.ts';
6
6
  import { type ExperienceTracker, renderExperienceRecipes } from '../experience-tracker.ts';
7
7
  import { Stats } from '../stats.ts';
@@ -65,8 +65,8 @@ export function createCodeceptJSTools({ explorer, stateManager, judge }: ToolDep
65
65
  Never mix different elements — use separate click() calls instead.
66
66
  REQUIRED: include at least one command WITHOUT a container — a wrong or stale container always fails.
67
67
  Order by reliability:
68
- 1. I.click(text, container) - when the container is verified - e.g. I.click("Save", ".modal")
69
- 2. I.click(ARIA, container) - e.g. I.click({"role":"button","text":"Save"}, ".modal")
68
+ 1. I.click(ARIA, container) - e.g. I.click({"role":"button","text":"Save"}, ".modal")
69
+ 2. I.click(text, container) - when the container is verified - e.g. I.click("Save", ".modal")
70
70
  3. I.click(CSS, container) - e.g. I.click("#btn", ".modal")
71
71
  4. I.click(CSS) or I.click(XPath) - when locator already includes context (ID, XPath)
72
72
  After a result reporting multiple matches, reuse that locator with step.opts({ elementIndex: N }) as the last argument.
@@ -292,7 +292,7 @@ export function createCodeceptJSTools({ explorer, stateManager, judge }: ToolDep
292
292
  const normalizedKey = key.toLowerCase();
293
293
  const matchingStandardKey = Array.from(standardKeys).find((sk) => sk.toLowerCase() === normalizedKey);
294
294
  const isStandardKey = !!matchingStandardKey;
295
- const keyToUse = matchingStandardKey || key;
295
+ const keyToUse = KEY_ALIASES[matchingStandardKey || ''] || matchingStandardKey || key;
296
296
 
297
297
  if (!isSingleChar && !isStandardKey) {
298
298
  const previousState = ActionResult.fromState(stateManager.getCurrentState()!);
@@ -480,7 +480,7 @@ export function createCodeceptJSTools({ explorer, stateManager, judge }: ToolDep
480
480
  commandsExecuted: lines.length,
481
481
  attempts: action.executedSteps,
482
482
  code: codeBlock,
483
- suggestion: 'Verify the form was filled in correctly using see() tool. If needed to submit: try click() tool or form() with I.pressKey("Enter").',
483
+ suggestion: 'Check the filled values with verify() before submitting, then submit with click().',
484
484
  },
485
485
  action
486
486
  );
@@ -678,7 +678,7 @@ export function createAgentTools({ explorer, stateManager, ai, judge, researcher
678
678
  inputSchema: z.object({
679
679
  reason: z.string().describe('Why do you need fresh context? Required to prevent overuse.'),
680
680
  }),
681
- execute: async ({ reason }) => {
681
+ execute: async () => {
682
682
  try {
683
683
  const currentState = stateManager.getCurrentState();
684
684
 
@@ -760,6 +760,7 @@ export function createAgentTools({ explorer, stateManager, ai, judge, researcher
760
760
  }
761
761
 
762
762
  return failedToolResult('verify', `Verification failed: ${assertion}`, {
763
+ attempts: result.results.map(({ code, passed, error }) => ({ code, passed, error })),
763
764
  suggestion: 'The assertion could not be verified. Check if the condition is actually present on the page or try a different assertion.',
764
765
  });
765
766
  } catch (error) {
@@ -793,7 +794,7 @@ export function createAgentTools({ explorer, stateManager, ai, judge, researcher
793
794
  inputSchema: z.object({
794
795
  reason: z.string().describe('Why do you need research? What information is missing from existing context?'),
795
796
  }),
796
- execute: async ({ reason }) => {
797
+ execute: async () => {
797
798
  try {
798
799
  const currentState = stateManager.getCurrentState();
799
800
 
@@ -907,7 +908,7 @@ export function createAgentTools({ explorer, stateManager, ai, judge, researcher
907
908
  element: z.string().describe('Visual description of element to click (e.g., "blue Submit button in the modal footer", "Settings gear icon in top right")'),
908
909
  context: z.string().describe('What you already tried and why it failed - helps with accurate identification'),
909
910
  }),
910
- execute: async ({ element, context }) => {
911
+ execute: async ({ element }) => {
911
912
  if (Stats.visionDisabled) {
912
913
  return failedToolResult('visualClick', 'Vision tools are disabled for this session. Use xpathCheck() to find the element, then click() with the discovered locator.');
913
914
  }
@@ -1058,7 +1059,7 @@ export function createAgentTools({ explorer, stateManager, ai, judge, researcher
1058
1059
  xpath: z.string().describe('XPath expression — use broad patterns to avoid missing the element'),
1059
1060
  reason: z.string().describe('What element you are looking for and why'),
1060
1061
  }),
1061
- execute: async ({ xpath, reason }) => {
1062
+ execute: async ({ xpath }) => {
1062
1063
  const currentState = stateManager.getCurrentState();
1063
1064
 
1064
1065
  if (!currentState) {
@@ -1202,6 +1203,7 @@ const ARIA_OUTPUT_CAP = 4000;
1202
1203
  const HTML_OUTPUT_CAP = 6000;
1203
1204
  const ANALYSIS_OUTPUT_CAP = 2000;
1204
1205
  const JUDGE_RECENT_ACTIONS_LIMIT = 8;
1206
+ const KEY_ALIASES: Record<string, string> = { Esc: 'Escape', Del: 'Delete' };
1205
1207
 
1206
1208
  function cap(text: string | undefined | null, max: number): string {
1207
1209
  if (!text) return '';
@@ -10,6 +10,7 @@ import { loadMarkdownFiles } from './utils/markdown-files.ts';
10
10
  export class ApplicationSpec {
11
11
  private pages: ApplicationSpecPage[] = [];
12
12
  readonly sourcePath: string;
13
+ private totalMarkdownFiles = 0;
13
14
 
14
15
  constructor(sourcePath: string) {
15
16
  this.sourcePath = this.resolveSourcePath(sourcePath);
@@ -38,28 +39,27 @@ export class ApplicationSpec {
38
39
  return this.pages.length;
39
40
  }
40
41
 
42
+ get markdownFileCount(): number {
43
+ return this.totalMarkdownFiles;
44
+ }
45
+
41
46
  private load(): void {
42
47
  if (!existsSync(this.sourcePath)) {
43
48
  throw new Error(`Application spec not found: ${this.sourcePath}`);
44
49
  }
45
50
 
46
51
  const isDirectory = statSync(this.sourcePath).isDirectory();
47
- if (!isDirectory && path.basename(this.sourcePath).toLowerCase() !== 'index.md') {
48
- throw new Error(`Application spec file must be index.md: ${this.sourcePath}`);
49
- }
50
-
51
52
  const bundlePath = isDirectory ? this.sourcePath : path.dirname(this.sourcePath);
53
+
54
+ const allFiles = loadMarkdownFiles(bundlePath, { recursive: true });
52
55
  const indexPath = path.join(bundlePath, 'index.md');
53
- if (!existsSync(indexPath)) {
54
- throw new Error(`Application spec index not found: ${indexPath}`);
55
- }
56
+ this.totalMarkdownFiles = allFiles.filter((file) => path.resolve(file.filePath) !== path.resolve(indexPath)).length;
56
57
 
57
- const pagesPath = path.join(bundlePath, 'pages');
58
- if (!existsSync(pagesPath)) {
59
- throw new Error(`Application spec pages directory not found: ${pagesPath}`);
60
- }
58
+ for (const file of allFiles) {
59
+ if (path.resolve(file.filePath) === path.resolve(indexPath)) continue;
60
+
61
+ if (!file.data.url && file.data.format === undefined && file.data.version === undefined) continue;
61
62
 
62
- for (const file of loadMarkdownFiles(pagesPath, { recursive: true })) {
63
63
  const parsed = APPLICATION_SPEC_PAGE_SCHEMA.safeParse(file.data);
64
64
  if (!parsed.success && parsed.error.issues.some((issue) => issue.path[0] === 'format')) {
65
65
  throw new Error(`Invalid application spec format in ${file.filePath}`);
@@ -73,8 +73,14 @@ export class ApplicationSpec {
73
73
  this.pages.push({ url: parsed.data.url, content: file.content.trim() });
74
74
  }
75
75
 
76
+ if (this.totalMarkdownFiles === 0) {
77
+ tag('warning').log(`No markdown files found in spec: ${bundlePath}`);
78
+ return;
79
+ }
80
+
76
81
  if (this.pages.length === 0) {
77
- throw new Error(`Application spec contains no documented pages: ${pagesPath}`);
82
+ tag('debug').log(`${this.totalMarkdownFiles} spec files found in ${bundlePath}`);
83
+ return;
78
84
  }
79
85
  }
80
86
 
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readdirSync, rmSync, statSync, unlinkSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
2
+ import { join } from 'node:path';
3
3
  import { ConfigParser, outputPath } from '../config.js';
4
4
  import { tag } from '../utils/logger.js';
5
5
  import { BaseCommand, type Suggestion } from './base-command.js';
@@ -1,4 +1,3 @@
1
- import { ActionResult } from '../action-result.js';
2
1
  import { Researcher } from '../ai/researcher.js';
3
2
  import { visuallyAnnotateContainers } from '../ai/researcher/coordinates.js';
4
3
  import { outputPath } from '../config.js';
@@ -9,7 +9,6 @@ export class ContextExperienceCommand extends BaseCommand {
9
9
  description = 'Print all matching experience for current page';
10
10
 
11
11
  async execute(_args: string): Promise<void> {
12
- const explorer = this.explorBot.getExplorer();
13
12
  const state = this.explorBot.stateManager().getCurrentState();
14
13
 
15
14
  if (!state) {
@@ -9,7 +9,6 @@ export class ContextKnowledgeCommand extends BaseCommand {
9
9
  description = 'Print all matching knowledge for current page';
10
10
 
11
11
  async execute(_args: string): Promise<void> {
12
- const explorer = this.explorBot.getExplorer();
13
12
  const state = this.explorBot.stateManager().getCurrentState();
14
13
 
15
14
  if (!state) {
@@ -6,7 +6,7 @@ import { Stats } from '../stats.js';
6
6
  import { type Plan, type Test, TestResult } from '../test-plan.js';
7
7
  import { browserErrorMessage } from '../utils/browser-errors.ts';
8
8
  import { getCliName } from '../utils/cli-name.ts';
9
- import { ErrorPageError, getStateErrorPageError } from '../utils/error-page.ts';
9
+ import { BlockedError, ErrorPageError, getStateErrorPageError } from '../utils/error-page.ts';
10
10
  import { tag } from '../utils/logger.js';
11
11
  import { type NextStepSection, printNextSteps, relativeToCwd } from '../utils/next-steps.ts';
12
12
  import { keepSiteUrls, readSitemap } from '../utils/sitemap.ts';
@@ -14,6 +14,7 @@ import { parseSpecPairs } from '../utils/spec.ts';
14
14
  import { safeFilename } from '../utils/strings.ts';
15
15
  import { UrlFilter } from '../utils/url-filter.ts';
16
16
  import { type ArgumentCompletion, BaseCommand, type Suggestion } from './base-command.js';
17
+ import { PlanCommand } from './plan-command.js';
17
18
 
18
19
  const MAX_SUB_PAGE_ATTEMPTS = 30;
19
20
  const PRIORITY_ORDER: Record<string, number> = { critical: 0, important: 1, high: 2, normal: 3, low: 4 };
@@ -136,7 +137,7 @@ export class ExploreCommand extends BaseCommand {
136
137
  }
137
138
 
138
139
  private async runFreshMode(mainUrl: string | undefined, feature: string | undefined, styles?: string[]): Promise<void> {
139
- await this.runAllStyles(mainUrl, feature, undefined, undefined, styles);
140
+ await this.runAllStyles(mainUrl, feature, undefined, styles);
140
141
  this.rememberCurrentPlan();
141
142
  const mainPlan = this.explorBot.getCurrentPlan();
142
143
  if (!mainPlan) return;
@@ -320,7 +321,7 @@ export class ExploreCommand extends BaseCommand {
320
321
  tag('warning').log(`Skipping page: ${errorPage.message}`);
321
322
  return false;
322
323
  }
323
- await this.runAllStyles(url, feature, parentPlan, this.completedPlans, styles);
324
+ await this.runAllStyles(url, feature, parentPlan, styles);
324
325
  const plan = this.explorBot.getCurrentPlan();
325
326
  if (plan?.tests.length && !this.completedPlans.includes(plan)) {
326
327
  this.completedPlans.push(plan);
@@ -333,14 +334,14 @@ export class ExploreCommand extends BaseCommand {
333
334
  for (const style of styleList) {
334
335
  if (this.isLimitReached()) break;
335
336
  this.explorBot.setCurrentPlan(existingPlan);
336
- const opts: { fresh: boolean; style: string; completedPlans?: Plan[]; noSave?: boolean } = { fresh: false, style, completedPlans: this.completedPlans };
337
+ const opts: { fresh: boolean; style: string; noSave?: boolean } = { fresh: false, style };
337
338
  if (this.dryRun) opts.noSave = true;
338
339
  await this.planWithRetry(feature, opts, pageUrl);
339
340
  await this.runPendingTests();
340
341
  }
341
342
  }
342
343
 
343
- private async runAllStyles(pageUrl?: string, feature?: string, parentPlan?: Plan, completedPlans?: Plan[], styles?: string[]): Promise<void> {
344
+ private async runAllStyles(pageUrl?: string, feature?: string, parentPlan?: Plan, styles?: string[]): Promise<void> {
344
345
  const styleList = styles ?? Object.keys(getStyles());
345
346
  let fresh = true;
346
347
  for (const style of styleList) {
@@ -348,7 +349,7 @@ export class ExploreCommand extends BaseCommand {
348
349
  if (!fresh && pageUrl && !this.dryRun) {
349
350
  await this.explorBot.visit(pageUrl);
350
351
  }
351
- const opts: { fresh: boolean; style: string; extend?: Plan; completedPlans?: Plan[]; noSave?: boolean } = { fresh, style, completedPlans };
352
+ const opts: { fresh: boolean; style: string; extend?: Plan; noSave?: boolean } = { fresh, style };
352
353
  if (fresh && parentPlan) opts.extend = parentPlan;
353
354
  if (this.dryRun) opts.noSave = true;
354
355
  await this.planWithRetry(feature, opts, pageUrl);
@@ -371,10 +372,14 @@ export class ExploreCommand extends BaseCommand {
371
372
  this.completedPlans.push(plan);
372
373
  }
373
374
 
374
- private async planWithRetry(feature: string | undefined, opts: { fresh: boolean; style: string; extend?: Plan; completedPlans?: Plan[]; noSave?: boolean }, pageUrl?: string): Promise<void> {
375
+ private async planWithRetry(feature: string | undefined, opts: { fresh: boolean; style: string; extend?: Plan; noSave?: boolean }, pageUrl?: string): Promise<void> {
375
376
  const before = new Set(this.explorBot.getCurrentPlan()?.tests ?? []);
376
377
 
377
378
  await this.explorBot.plan(feature, opts);
379
+ if (this.explorBot.lastPlanError instanceof BlockedError) {
380
+ tag('warning').log(`Skipping page: ${this.explorBot.lastPlanError.message}`);
381
+ return;
382
+ }
378
383
  if (this.explorBot.lastPlanError) {
379
384
  if (this.explorBot.lastPlanError instanceof ErrorPageError) {
380
385
  throw this.explorBot.lastPlanError;
@@ -388,12 +393,15 @@ export class ExploreCommand extends BaseCommand {
388
393
  }
389
394
  }
390
395
 
391
- const planAfter = this.explorBot.getCurrentPlan();
392
- if (!planAfter) return;
393
- const added = planAfter.tests.filter((t) => !before.has(t));
394
- if (added.length === 0) return;
396
+ this.printAddedTests(before, opts.style, pageUrl);
397
+ }
398
+
399
+ private printAddedTests(before: Set<Test>, style: string, pageUrl?: string): number {
400
+ const added = this.explorBot.getCurrentPlan()?.tests.filter((t) => !before.has(t)) ?? [];
401
+ if (added.length === 0) return 0;
395
402
  const urlNote = pageUrl ? ` for ${pageUrl}` : '';
396
- this.printPreview(`Planner added ${added.length} new test(s) [style=${opts.style}]${urlNote}:`, added);
403
+ this.printPreview(`Planner added ${added.length} new test(s) [style=${style}]${urlNote}:`, added);
404
+ return added.length;
397
405
  }
398
406
 
399
407
  private parseConfigure(raw: string | undefined): ConfigureSpec {
@@ -545,16 +553,7 @@ export class ExploreCommand extends BaseCommand {
545
553
  const sections: NextStepSection[] = [];
546
554
 
547
555
  if (savedPlanPath) {
548
- const relPlan = relativeToCwd(savedPlanPath);
549
- sections.push({
550
- label: 'Plan',
551
- path: savedPlanPath,
552
- commands: [
553
- { label: 'Re-run', command: `${cli} test ${relPlan} 1` },
554
- { label: 'Run all', command: `${cli} test ${relPlan} *` },
555
- { label: 'Run range', command: `${cli} test ${relPlan} 1-3` },
556
- ],
557
- });
556
+ sections.push(PlanCommand.planSection(savedPlanPath));
558
557
  }
559
558
 
560
559
  const savedFiles = this.explorBot.agentHistorian().getSavedFiles();
@@ -594,6 +593,14 @@ export class ExploreCommand extends BaseCommand {
594
593
  printNextSteps(sections);
595
594
  }
596
595
 
596
+ private skipBlockedTest(test: Test, error: unknown): void {
597
+ if (!(error instanceof BlockedError)) throw error;
598
+ tag('warning').log(`Skipping test: ${error.message}`);
599
+ test.start();
600
+ test.addNote(error.message, TestResult.SKIPPED);
601
+ test.finish(TestResult.SKIPPED);
602
+ }
603
+
597
604
  private isLimitReached(): boolean {
598
605
  if (this.maxTests != null && this.testsRun >= this.maxTests) return true;
599
606
  return this.isDeadlineReached();
@@ -628,7 +635,13 @@ export class ExploreCommand extends BaseCommand {
628
635
  test.start();
629
636
  test.finish(TestResult.SKIPPED);
630
637
  } else {
631
- await this.explorBot.agentTester().test(test, { deadline: this.hardDeadlineAt });
638
+ const run = () => this.explorBot.agentTester().test(test, { deadline: this.hardDeadlineAt });
639
+ await run()
640
+ .catch(async (err) => {
641
+ if (!(await this.explorBot.agentCaptain().resolveBlocker(err, () => this.explorBot.agentNavigator().isOn(test.startUrl)))) throw err;
642
+ return run();
643
+ })
644
+ .catch((err) => this.skipBlockedTest(test, err));
632
645
  }
633
646
  this.testsRun++;
634
647
  }
@@ -31,12 +31,14 @@ export class HelpJsonCommand {
31
31
  required: argument.required,
32
32
  variadic: argument.variadic,
33
33
  })),
34
- options: helper.visibleOptions(cmd).map((option) => ({
35
- flags: option.flags,
36
- description: option.description,
37
- default: option.defaultValue,
38
- choices: option.argChoices,
39
- })),
34
+ options: cmd.options
35
+ .filter((option) => !option.hidden)
36
+ .map((option) => ({
37
+ flags: option.flags,
38
+ description: option.description,
39
+ default: option.defaultValue,
40
+ choices: option.argChoices,
41
+ })),
40
42
  commands: helper.visibleCommands(cmd).map((sub) => HelpJsonCommand.data(sub)),
41
43
  };
42
44
 
@@ -327,6 +327,11 @@ function printInitNextSteps(options: { url?: string; siteCommands?: NextStepComm
327
327
 
328
328
  if (options.siteCommands) sections.push({ label: 'Explore any site', commands: options.siteCommands });
329
329
 
330
+ sections.push({
331
+ label: 'Log in for one run (kept in memory, never saved)',
332
+ commands: [{ command: `${cli} explore /dashboard --auth 'login: admin@example.com password: secret123'` }],
333
+ });
334
+
330
335
  sections.push(
331
336
  {
332
337
  label: 'Run it from VS Code',
@@ -338,11 +343,13 @@ function printInitNextSteps(options: { url?: string; siteCommands?: NextStepComm
338
343
  }
339
344
  );
340
345
 
346
+ sections.push({
347
+ label: 'Add knowledge (logins, credentials) — created on first use in ./knowledge',
348
+ commands: [{ command: `${cli} learn '*' 'to authorize use these credentials: admin@example.com / secret123'` }],
349
+ });
350
+
341
351
  printNextSteps(sections);
342
352
 
343
- log('');
344
- log('Add knowledge (logins, credentials) — created on first use in ./knowledge:');
345
- tag('substep').log(chalk.yellow(`${cli} learn '*' 'to authorize use these credentials: admin@example.com / secret123'`));
346
353
  tag('substep').log('You can use ${env.LOGIN} and ${env.PASSWORD} to reference environment variables.');
347
354
  }
348
355
 
@@ -13,7 +13,6 @@ export class LearnCommand extends BaseCommand {
13
13
 
14
14
  if (!note) {
15
15
  const AddKnowledge = (await import('../components/AddKnowledge.js')).default;
16
- const explorer = this.explorBot.getExplorer();
17
16
  const state = this.explorBot.stateManager().getCurrentState();
18
17
  const initialUrl = state?.url || '';
19
18
 
@@ -32,7 +31,6 @@ export class LearnCommand extends BaseCommand {
32
31
  return;
33
32
  }
34
33
 
35
- const explorer = this.explorBot.getExplorer();
36
34
  const state = this.explorBot.stateManager().getCurrentState();
37
35
 
38
36
  if (!state) {
@@ -0,0 +1,15 @@
1
+ import { KnowledgeTracker } from '../../knowledge-tracker.js';
2
+ import { registerSecret } from '../../utils/secrets.js';
3
+ import { BaseOption } from './base-option.js';
4
+
5
+ export class AuthOption extends BaseOption {
6
+ flags = '--auth <text>';
7
+ description = 'Auth details for this run only, not saved to disk, in free form (e.g. "login: admin password: secret" or "Bearer: <token>"). Falls back to EXPLORBOT_AUTH';
8
+
9
+ protected apply(options: Record<string, any>): void {
10
+ const auth = options.auth || process.env.EXPLORBOT_AUTH;
11
+ if (!auth) return;
12
+ registerSecret(auth);
13
+ KnowledgeTracker.appendSessionKnowledge(auth);
14
+ }
15
+ }
@@ -1,9 +1,11 @@
1
+ import { AuthOption } from './auth-option.js';
1
2
  import { DecisionModelOption } from './decision-model-option.js';
2
3
  import { KnowledgeOption } from './knowledge-option.js';
3
4
  import { WsOption } from './ws-option.js';
4
5
 
5
6
  export { BaseOption } from './base-option.js';
6
7
 
8
+ export const authOption = new AuthOption();
7
9
  export const decisionModelOption = new DecisionModelOption();
8
10
  export const knowledgeOption = new KnowledgeOption();
9
11
  export const wsOption = new WsOption();
@@ -1,3 +1,4 @@
1
+ import { existsSync } from 'node:fs';
1
2
  import path from 'node:path';
2
3
  import chalk from 'chalk';
3
4
  import figureSet from 'figures';
@@ -17,8 +18,12 @@ export class PlanCommand extends BaseCommand {
17
18
  options = [
18
19
  { flags: '--fresh', description: 'Regenerate plan from scratch' },
19
20
  { flags: '--clear', description: 'Clear plan before regenerating' },
21
+ { flags: '--append', description: 'Add tests to existing plan file' },
20
22
  { flags: '--style <name>', description: 'Planning style (normal, curious, psycho, performer)' },
21
23
  { flags: '--focus <feature>', description: 'Focus area for test planning' },
24
+ { flags: '--tests <count>', description: 'Exact number of tests to generate' },
25
+ { flags: '--min-tests <count>', description: 'Minimum number of tests to generate' },
26
+ { flags: '--max-tests <count>', description: 'Maximum number of tests to generate' },
22
27
  ];
23
28
 
24
29
  async execute(args: string): Promise<void> {
@@ -30,11 +35,20 @@ export class PlanCommand extends BaseCommand {
30
35
  tag('success').log('Plan cleared');
31
36
  }
32
37
 
38
+ if (opts.append) {
39
+ const existingPlanPath = path.join(this.explorBot.getPlansDir(), this.explorBot.generatePlanFilename());
40
+ if (existsSync(existingPlanPath)) this.explorBot.loadPlan(existingPlanPath);
41
+ }
42
+
33
43
  if (focus) {
34
44
  tag('info').log(`Planning focus: ${focus}`);
35
45
  }
36
46
 
37
- await this.explorBot.plan(focus, { fresh: !!(opts.fresh || opts.clear), style: opts.style as string });
47
+ const testsCount = opts.tests ? Number.parseInt(opts.tests as string, 10) : undefined;
48
+ const minTests = testsCount ?? (opts.minTests ? Number.parseInt(opts.minTests as string, 10) : undefined);
49
+ const maxTests = testsCount ?? (opts.maxTests ? Number.parseInt(opts.maxTests as string, 10) : undefined);
50
+
51
+ await this.explorBot.plan(focus, { fresh: !!(opts.fresh || opts.clear), style: opts.style as string, minTests, maxTests });
38
52
 
39
53
  const plan = this.explorBot.getCurrentPlan();
40
54
  if (!plan?.tests.length) {
@@ -46,29 +60,34 @@ export class PlanCommand extends BaseCommand {
46
60
  this.printNextSteps();
47
61
  }
48
62
 
63
+ static planSection(planPath: string, cliSuffix = ''): NextStepSection {
64
+ const cli = getCliName();
65
+ const planName = path.basename(planPath, '.md');
66
+ return {
67
+ label: 'Plan',
68
+ path: planPath,
69
+ commands: [
70
+ { label: 'Run all tests', command: `${cli} test ${planName}${cliSuffix}` },
71
+ { label: 'Run one test', command: `${cli} test ${planName} 1${cliSuffix}` },
72
+ { label: 'Run a range', command: `${cli} test ${planName} 1-3${cliSuffix}` },
73
+ ],
74
+ };
75
+ }
76
+
49
77
  private printNextSteps(): void {
50
78
  const savedPath = this.explorBot.lastSavedPlanPath;
51
79
  if (!savedPath) return;
52
80
 
53
81
  const cli = getCliName();
54
- const relPlan = relativeToCwd(savedPath);
55
- const sections: NextStepSection[] = [
56
- {
57
- label: 'Plan',
58
- path: savedPath,
59
- commands: [
60
- { label: 'Re-run', command: `${cli} test ${relPlan} 1` },
61
- { label: 'Run all', command: `${cli} test ${relPlan} *` },
62
- { label: 'Run range', command: `${cli} test ${relPlan} 1-3` },
63
- { label: 'Reload', command: `/plan:load ${relPlan}` },
64
- ],
65
- },
66
- ];
82
+ const cliSuffix = this.cliSuffix();
83
+ const section = PlanCommand.planSection(savedPath, cliSuffix);
84
+ section.commands?.push({ label: 'Reload', command: `/plan:load ${path.basename(savedPath, '.md')}` });
85
+ const sections: NextStepSection[] = [section];
67
86
 
68
87
  const suite = this.explorBot.getSuite();
69
88
  const files = suite && suite.automatedTestCount > 0 ? suite.getAutomatedTestFiles() : [];
70
89
  if (files.length > 0) {
71
- const commands = files.map((f) => ({ label: '', command: `${cli} rerun ${relativeToCwd(f)}` }));
90
+ const commands = files.map((f) => ({ label: '', command: `${cli} rerun ${relativeToCwd(f)}${cliSuffix}` }));
72
91
  sections.push({
73
92
  label: `Automated tests (${files.length})`,
74
93
  commands,
@@ -78,6 +97,14 @@ export class PlanCommand extends BaseCommand {
78
97
  printNextSteps(sections);
79
98
  }
80
99
 
100
+ private cliSuffix(): string {
101
+ const options = this.explorBot.getOptions();
102
+ let suffix = '';
103
+ if (options.path) suffix += ` --path ${options.path}`;
104
+ if (options.session) suffix += ' --session';
105
+ return suffix;
106
+ }
107
+
81
108
  private printPlanSummary(): void {
82
109
  const suite = this.explorBot.getSuite();
83
110
  const plan = this.explorBot.getCurrentPlan();
@@ -91,10 +118,10 @@ export class PlanCommand extends BaseCommand {
91
118
  }
92
119
 
93
120
  if (plan?.tests.length) {
94
- console.log(`\n${chalk.bold.cyan(`New test scenarios (${plan.tests.length})`)}`);
121
+ console.log(`\n${chalk.bold.cyan(`New test scenarios (${plan.tests.length})`)}\n`);
95
122
  for (let i = 0; i < plan.tests.length; i++) {
96
123
  const t = plan.tests[i];
97
- console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${t.scenario} ${chalk.dim(`[${t.priority}]`)}`);
124
+ console.log(` ${chalk.cyan(`${i + 1}.`)} ${t.scenario}`);
98
125
  }
99
126
  }
100
127
  }
@@ -1,5 +1,6 @@
1
- import { getCliName } from '../utils/cli-name.ts';
2
- import { type NextStepSection, printNextSteps, relativeToCwd } from '../utils/next-steps.ts';
1
+ import path from 'node:path';
2
+ import { printNextSteps } from '../utils/next-steps.ts';
3
+ import { PlanCommand } from './plan-command.js';
3
4
  import { BaseCommand, type Suggestion } from './base-command.js';
4
5
 
5
6
  export class PlanSaveCommand extends BaseCommand {
@@ -17,20 +18,8 @@ export class PlanSaveCommand extends BaseCommand {
17
18
  const savedPath = this.explorBot.savePlan(filename);
18
19
  if (!savedPath) return;
19
20
 
20
- const cli = getCliName();
21
- const relPlan = relativeToCwd(savedPath);
22
- const sections: NextStepSection[] = [
23
- {
24
- label: 'Plan',
25
- path: savedPath,
26
- commands: [
27
- { label: 'Re-run', command: `${cli} test ${relPlan} 1` },
28
- { label: 'Run all', command: `${cli} test ${relPlan} *` },
29
- { label: 'Run range', command: `${cli} test ${relPlan} 1-3` },
30
- { label: 'Reload', command: `/plan:load ${relPlan}` },
31
- ],
32
- },
33
- ];
34
- printNextSteps(sections);
21
+ const section = PlanCommand.planSection(savedPath);
22
+ section.commands?.push({ label: 'Reload', command: `/plan:load ${path.basename(savedPath, '.md')}` });
23
+ printNextSteps([section]);
35
24
  }
36
25
  }
@@ -52,7 +52,7 @@ export class PlansCommand extends BaseCommand {
52
52
  }
53
53
  tag('info').log('');
54
54
  tag('info').log('Run test from this plan as:');
55
- tag('info').log(`${getCliName()} test 1 --from-plan ${file.name}`);
55
+ tag('info').log(`${getCliName()} test ${file.name} 1`);
56
56
  }
57
57
 
58
58
  private resolvePlanFile(target: string, files: PlanFile[]): { plan: Plan; file: PlanFile } {
@@ -1,5 +1,6 @@
1
1
  import { Stats } from '../stats.js';
2
2
  import { Plan, Test } from '../test-plan.js';
3
+ import { BlockedError } from '../utils/error-page.ts';
3
4
  import { tag } from '../utils/logger.js';
4
5
  import { type ArgumentCompletion, BaseCommand, type Suggestion } from './base-command.js';
5
6
 
@@ -18,6 +19,11 @@ export class TestCommand extends BaseCommand {
18
19
  this.explorBot.loadPlan(String(opts.fromPlan));
19
20
  }
20
21
 
22
+ if (!opts.fromPlan && remaining[0] && Plan.resolveFile(remaining[0], this.explorBot.getPlansDir())) {
23
+ this.explorBot.loadPlan(remaining.shift()!);
24
+ if (remaining.length === 0) remaining.push('*');
25
+ }
26
+
21
27
  const plan = this.explorBot.getCurrentPlan();
22
28
  const selector = remaining.join(' ');
23
29
  Stats.mode = 'test';
@@ -71,13 +77,16 @@ export class TestCommand extends BaseCommand {
71
77
  const tester = this.explorBot.agentTester();
72
78
  for (const [index, test] of toExecute.entries()) {
73
79
  tag('info').log(`Starting test ${index + 1}/${toExecute.length}: ${test.scenario}`);
74
- await tester.test(test);
80
+ await tester.test(test).catch((err) => {
81
+ if (!(err instanceof BlockedError)) throw err;
82
+ tag('warning').log(`Skipping test: ${err.message}`);
83
+ });
75
84
  }
76
85
  tag('success').log('Test execution finished');
77
86
  }
78
87
 
79
88
  completeArguments(): ArgumentCompletion[] {
80
- return Plan.listFiles(this.explorBot.getPlansDir()).map((file) => ({ value: `--from-plan ${file.name}`, display: file.label }));
89
+ return Plan.listFiles(this.explorBot.getPlansDir()).map((file) => ({ value: file.name, display: file.label }));
81
90
  }
82
91
  }
83
92