explorbot 0.5.10 → 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.
- package/bin/explorbot-cli.ts +494 -482
- package/boat/api-tester/src/ai/curler.ts +1 -1
- package/boat/api-tester/src/cli.ts +4 -4
- package/boat/doc-collector/src/screenshots.ts +1 -1
- package/dist/bin/explorbot-cli.js +455 -444
- package/dist/boat/api-tester/src/ai/curler.js +1 -1
- package/dist/boat/api-tester/src/cli.js +4 -4
- package/dist/models.json +1 -1
- package/dist/package.json +2 -1
- package/dist/rules/navigator/output.md +2 -8
- package/dist/rules/navigator/verification-actions.md +6 -2
- package/dist/rules/planner/ignored-pages.md +1 -0
- package/dist/src/action-result.js +1 -1
- package/dist/src/action.d.ts +0 -1
- package/dist/src/action.js +0 -4
- package/dist/src/ai/captain/idle-mode.js +42 -26
- package/dist/src/ai/captain/mixin.d.ts +2 -0
- package/dist/src/ai/captain/web-mode.js +11 -0
- package/dist/src/ai/captain.d.ts +6 -4
- package/dist/src/ai/captain.js +25 -1
- package/dist/src/ai/driller.js +1 -1
- package/dist/src/ai/experience-compactor.js +2 -2
- package/dist/src/ai/judge.d.ts +7 -0
- package/dist/src/ai/judge.js +8 -1
- package/dist/src/ai/navigator.d.ts +1 -0
- package/dist/src/ai/navigator.js +11 -7
- package/dist/src/ai/planner/subpages.js +1 -1
- package/dist/src/ai/planner.d.ts +12 -5
- package/dist/src/ai/planner.js +138 -86
- package/dist/src/ai/provider.d.ts +1 -1
- package/dist/src/ai/provider.js +16 -5
- package/dist/src/ai/rerunner.js +1 -1
- package/dist/src/ai/researcher/cache.js +14 -6
- package/dist/src/ai/researcher/deep-analysis.d.ts +2 -1
- package/dist/src/ai/researcher/deep-analysis.js +7 -2
- package/dist/src/ai/researcher/fingerprint-worker.js +4 -4
- package/dist/src/ai/researcher.js +44 -24
- package/dist/src/ai/rules.js +6 -7
- package/dist/src/ai/scout.d.ts +12 -2
- package/dist/src/ai/scout.js +103 -8
- package/dist/src/ai/tester.d.ts +9 -5
- package/dist/src/ai/tester.js +22 -42
- package/dist/src/ai/tools.js +10 -8
- package/dist/src/application-spec.d.ts +2 -0
- package/dist/src/application-spec.js +17 -12
- package/dist/src/commands/context-experience-command.js +0 -1
- package/dist/src/commands/context-knowledge-command.js +0 -1
- package/dist/src/commands/explore-command.d.ts +3 -2
- package/dist/src/commands/explore-command.js +35 -23
- package/dist/src/commands/help-json-command.js +3 -1
- package/dist/src/commands/init-command.js +4 -4
- package/dist/src/commands/learn-command.js +0 -2
- package/dist/src/commands/plan-command.d.ts +3 -0
- package/dist/src/commands/plan-command.js +43 -17
- package/dist/src/commands/plan-save-command.js +6 -17
- package/dist/src/commands/plans-command.js +1 -1
- package/dist/src/commands/test-command.js +12 -2
- package/dist/src/components/App.d.ts +1 -2
- package/dist/src/components/App.js +1 -3
- package/dist/src/components/InputPane.js +0 -5
- package/dist/src/config.d.ts +3 -2
- package/dist/src/config.js +2 -12
- package/dist/src/explorbot.d.ts +3 -2
- package/dist/src/explorbot.js +32 -10
- package/dist/src/explorer.js +6 -7
- package/dist/src/knowledge-tracker.d.ts +2 -2
- package/dist/src/knowledge-tracker.js +18 -6
- package/dist/src/playwright-recorder.d.ts +0 -1
- package/dist/src/playwright-recorder.js +0 -2
- package/dist/src/reporter.d.ts +1 -1
- package/dist/src/reporter.js +3 -3
- package/dist/src/test-plan.js +1 -1
- package/dist/src/utils/error-page.d.ts +17 -0
- package/dist/src/utils/error-page.js +53 -2
- package/dist/src/utils/html-diff.d.ts +1 -2
- package/dist/src/utils/html-diff.js +4 -44
- package/dist/src/utils/logger.js +27 -10
- package/dist/src/utils/markdown-terminal.js +1 -2
- package/dist/src/utils/next-steps.js +20 -16
- package/dist/src/utils/page-readiness.d.ts +2 -1
- package/dist/src/utils/page-readiness.js +24 -5
- package/dist/src/utils/test-plan-markdown.d.ts +3 -0
- package/dist/src/utils/test-plan-markdown.js +35 -4
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +5 -0
- package/docs/basics/providers.md +1 -1
- package/docs/contributing/regression-tests.md +19 -4
- package/docs/reference/commands.md +6 -2
- package/docs/reference/configuration.md +1 -1
- package/docs/web-testing/agents.md +8 -0
- package/docs/web-testing/customization.md +2 -0
- package/docs/web-testing/researcher.md +1 -1
- package/models.json +1 -1
- package/package.json +2 -1
- package/rules/navigator/output.md +2 -8
- package/rules/navigator/verification-actions.md +6 -2
- package/rules/planner/ignored-pages.md +1 -0
- package/src/action-result.ts +1 -2
- package/src/action.ts +0 -5
- package/src/ai/captain/idle-mode.ts +38 -26
- package/src/ai/captain/mixin.ts +2 -1
- package/src/ai/captain/web-mode.ts +12 -0
- package/src/ai/captain.ts +25 -7
- package/src/ai/driller.ts +1 -2
- package/src/ai/experience-compactor.ts +2 -2
- package/src/ai/judge.ts +10 -1
- package/src/ai/navigator.ts +12 -7
- package/src/ai/planner/subpages.ts +1 -2
- package/src/ai/planner.ts +140 -92
- package/src/ai/provider.ts +15 -6
- package/src/ai/rerunner.ts +1 -2
- package/src/ai/researcher/cache.ts +16 -7
- package/src/ai/researcher/deep-analysis.ts +7 -2
- package/src/ai/researcher/fingerprint-worker.ts +5 -5
- package/src/ai/researcher.ts +44 -28
- package/src/ai/rules.ts +6 -7
- package/src/ai/scout.ts +98 -10
- package/src/ai/tester.ts +29 -47
- package/src/ai/tools.ts +11 -9
- package/src/application-spec.ts +19 -13
- package/src/commands/clean-command.ts +1 -1
- package/src/commands/context-command.ts +0 -1
- package/src/commands/context-experience-command.ts +0 -1
- package/src/commands/context-knowledge-command.ts +0 -1
- package/src/commands/explore-command.ts +36 -23
- package/src/commands/help-json-command.ts +8 -6
- package/src/commands/init-command.ts +5 -3
- package/src/commands/learn-command.ts +0 -2
- package/src/commands/plan-command.ts +44 -17
- package/src/commands/plan-save-command.ts +6 -17
- package/src/commands/plans-command.ts +1 -1
- package/src/commands/test-command.ts +11 -2
- package/src/components/App.tsx +2 -6
- package/src/components/InputPane.tsx +0 -5
- package/src/config.ts +4 -16
- package/src/experience-tracker.ts +1 -1
- package/src/explorbot.ts +31 -11
- package/src/explorer.ts +6 -7
- package/src/knowledge-tracker.ts +18 -7
- package/src/playwright-recorder.ts +0 -2
- package/src/reporter.ts +3 -4
- package/src/test-plan.ts +1 -2
- package/src/utils/error-page.ts +64 -2
- package/src/utils/html-diff.ts +4 -56
- package/src/utils/html.ts +0 -1
- package/src/utils/logger.ts +30 -12
- package/src/utils/markdown-terminal.ts +1 -2
- package/src/utils/next-steps.ts +19 -17
- package/src/utils/page-readiness.ts +33 -7
- package/src/utils/test-plan-markdown.ts +36 -4
- package/src/utils/url-matcher.ts +6 -0
|
@@ -21,10 +21,9 @@ if (renderer) {
|
|
|
21
21
|
const originalListitem = renderer.listitem;
|
|
22
22
|
renderer.listitem = function (text) {
|
|
23
23
|
if (typeof text === 'object') {
|
|
24
|
-
const item = text;
|
|
25
24
|
const parser = this.parser;
|
|
26
25
|
const originalParse = parser.parse.bind(parser);
|
|
27
|
-
parser.parse = (tokens
|
|
26
|
+
parser.parse = (tokens) => parser.parseInline(tokens);
|
|
28
27
|
const result = originalListitem.call(this, text);
|
|
29
28
|
parser.parse = originalParse;
|
|
30
29
|
return result;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import figures from 'figures';
|
|
2
4
|
import { tag } from './logger.js';
|
|
3
5
|
export function relativeToCwd(absPath) {
|
|
4
6
|
const rel = path.relative(process.cwd(), absPath);
|
|
@@ -7,25 +9,27 @@ export function relativeToCwd(absPath) {
|
|
|
7
9
|
export function printNextSteps(sections) {
|
|
8
10
|
if (sections.length === 0)
|
|
9
11
|
return;
|
|
10
|
-
const
|
|
12
|
+
const lines = [''];
|
|
13
|
+
let isDefault = true;
|
|
11
14
|
for (const section of sections) {
|
|
12
|
-
const lines = [];
|
|
13
15
|
const headerPath = section.path ? relativeToCwd(section.path) : '';
|
|
14
|
-
lines.push(headerPath ? `${section.label}
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const padded = `${cmd.label}:`.padEnd(maxLabel + 2);
|
|
25
|
-
lines.push(` ${padded} ${cmd.command}`);
|
|
16
|
+
lines.push(headerPath ? `${chalk.bold(section.label)} ${chalk.gray(headerPath)}` : `${chalk.bold(section.label)}`);
|
|
17
|
+
for (const cmd of section.commands || []) {
|
|
18
|
+
if (cmd.label)
|
|
19
|
+
lines.push(BLANK);
|
|
20
|
+
if (isDefault) {
|
|
21
|
+
if (cmd.label)
|
|
22
|
+
lines.push(chalk.bold.yellow(`${figures.pointer} ${cmd.label}`));
|
|
23
|
+
lines.push(chalk.bold(` ${cmd.command}`));
|
|
24
|
+
isDefault = false;
|
|
25
|
+
continue;
|
|
26
26
|
}
|
|
27
|
+
if (cmd.label)
|
|
28
|
+
lines.push(chalk.dim(` ${cmd.label}`));
|
|
29
|
+
lines.push(` ${cmd.command}`);
|
|
27
30
|
}
|
|
28
|
-
|
|
31
|
+
lines.push(BLANK);
|
|
29
32
|
}
|
|
30
|
-
tag('
|
|
33
|
+
tag('info').log(lines.join('\n'));
|
|
31
34
|
}
|
|
35
|
+
const BLANK = ' ';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export declare function waitForPageReadiness(page: any, options?: PageReadinessOptions): Promise<
|
|
1
|
+
export declare function waitForPageReadiness(page: any, options?: PageReadinessOptions): Promise<boolean>;
|
|
2
|
+
export declare function waitForDomChange(page: any, timeout: number): Promise<boolean>;
|
|
2
3
|
export declare function sleep(ms: number): Promise<void>;
|
|
3
4
|
interface PageReadinessOptions {
|
|
4
5
|
timeout?: number;
|
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
export async function waitForPageReadiness(page, options = {}) {
|
|
2
2
|
if (!page)
|
|
3
|
-
return;
|
|
3
|
+
return false;
|
|
4
4
|
const timeout = options.timeout ?? 6000;
|
|
5
5
|
await page.waitForLoadState?.('domcontentloaded', { timeout })?.catch(() => { });
|
|
6
6
|
await Promise.race([waitForNetworkIdle(page, timeout), waitForDomQuiet(page, timeout), waitForVisibleSpinnersHidden(page, options.spinnerSelectors || [], timeout), sleep(timeout)]).catch(() => { });
|
|
7
|
-
|
|
7
|
+
return waitForPageBodyContent(page, timeout);
|
|
8
|
+
}
|
|
9
|
+
export function waitForDomChange(page, timeout) {
|
|
10
|
+
if (!page?.evaluate)
|
|
11
|
+
return Promise.resolve(false);
|
|
12
|
+
return page
|
|
13
|
+
.evaluate((ms) => new Promise((resolve) => {
|
|
14
|
+
const observer = new MutationObserver(() => {
|
|
15
|
+
observer.disconnect();
|
|
16
|
+
resolve(true);
|
|
17
|
+
});
|
|
18
|
+
observer.observe(document, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
19
|
+
setTimeout(() => {
|
|
20
|
+
observer.disconnect();
|
|
21
|
+
resolve(false);
|
|
22
|
+
}, ms);
|
|
23
|
+
}), timeout)
|
|
24
|
+
.catch(() => true);
|
|
8
25
|
}
|
|
9
26
|
const DOM_QUIET_MS = 350;
|
|
10
27
|
function waitForDomQuiet(page, timeout) {
|
|
@@ -50,15 +67,17 @@ async function waitForVisibleSpinnersHidden(page, selectors, timeout) {
|
|
|
50
67
|
}
|
|
51
68
|
function waitForPageBodyContent(page, timeout) {
|
|
52
69
|
if (!page?.waitForFunction)
|
|
53
|
-
return Promise.resolve();
|
|
70
|
+
return Promise.resolve(true);
|
|
54
71
|
return page
|
|
55
72
|
.waitForFunction(() => {
|
|
56
73
|
const body = document.body;
|
|
57
74
|
if (!body)
|
|
58
75
|
return false;
|
|
59
|
-
|
|
76
|
+
if (body.innerText.trim().length > 0)
|
|
77
|
+
return true;
|
|
78
|
+
return !!body.querySelector('input, button, select, textarea, img, svg, canvas, iframe, video');
|
|
60
79
|
}, undefined, { timeout })
|
|
61
|
-
.
|
|
80
|
+
.then(() => true, () => false);
|
|
62
81
|
}
|
|
63
82
|
export function sleep(ms) {
|
|
64
83
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -2,6 +2,9 @@ import { Plan } from '../test-plan.js';
|
|
|
2
2
|
export declare function planToCompactAiContext(plan: Plan): string;
|
|
3
3
|
export declare function parsePlanFromMarkdown(filePath: string): Plan;
|
|
4
4
|
export declare function parsePlansFromMarkdown(filePath: string): Plan[];
|
|
5
|
+
export declare function parsePlansFromMarkdownText(content: string): Plan[];
|
|
6
|
+
export declare function findPlanMarkdownProblems(markdown: string): string[];
|
|
7
|
+
export declare function readPlanMarkdownComment(markdown: string, key: string): string | null;
|
|
5
8
|
export declare function savePlanToMarkdown(plan: Plan, filePath: string): void;
|
|
6
9
|
export declare function savePlansToMarkdown(plans: Plan[], filePath: string): void;
|
|
7
10
|
export declare function planToAiContext(plan: Plan, options?: {
|
|
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { Plan, Test } from "../test-plan.js";
|
|
3
3
|
import { mdq } from "./markdown-query.js";
|
|
4
4
|
const NOISE_PREFIXES = ['Test started', 'Finish requested:', 'Session name:'];
|
|
5
|
+
const CURRENT_PAGE = 'Current page';
|
|
5
6
|
function isNoiseNote(message) {
|
|
6
7
|
return NOISE_PREFIXES.some((prefix) => message.startsWith(prefix));
|
|
7
8
|
}
|
|
@@ -118,7 +119,9 @@ export function parsePlanFromMarkdown(filePath) {
|
|
|
118
119
|
return main;
|
|
119
120
|
}
|
|
120
121
|
export function parsePlansFromMarkdown(filePath) {
|
|
121
|
-
|
|
122
|
+
return parsePlansFromMarkdownText(readFileSync(filePath, 'utf-8'));
|
|
123
|
+
}
|
|
124
|
+
export function parsePlansFromMarkdownText(content) {
|
|
122
125
|
const lines = content.split('\n');
|
|
123
126
|
const plans = [];
|
|
124
127
|
let currentPlan = null;
|
|
@@ -184,7 +187,8 @@ export function parsePlansFromMarkdown(filePath) {
|
|
|
184
187
|
continue;
|
|
185
188
|
}
|
|
186
189
|
if (currentTest && inRequirements && line && !line.startsWith('##')) {
|
|
187
|
-
|
|
190
|
+
if (line !== CURRENT_PAGE)
|
|
191
|
+
currentTest.startUrl = line;
|
|
188
192
|
continue;
|
|
189
193
|
}
|
|
190
194
|
if (currentTest && inSteps && line.startsWith('* ')) {
|
|
@@ -207,7 +211,7 @@ export function parsePlansFromMarkdown(filePath) {
|
|
|
207
211
|
continue;
|
|
208
212
|
const nextSuite = content.indexOf('<!-- suite -->', suiteStart + 1);
|
|
209
213
|
const suiteContent = nextSuite === -1 ? content.slice(suiteStart) : content.slice(suiteStart, nextSuite);
|
|
210
|
-
const firstItem = mdq(suiteContent).query('section[0] item[0]').
|
|
214
|
+
const firstItem = mdq(suiteContent).query('section[0] item[0]').nodes()[0]?.text.trim() || '';
|
|
211
215
|
const urlMatch = firstItem.match(/^URL:\s*(.+)/);
|
|
212
216
|
if (urlMatch) {
|
|
213
217
|
plan.url = urlMatch[1].replace(/\*\*|`|\*|_|~~?/g, '').trim();
|
|
@@ -219,6 +223,33 @@ export function parsePlansFromMarkdown(filePath) {
|
|
|
219
223
|
}
|
|
220
224
|
return plans;
|
|
221
225
|
}
|
|
226
|
+
export function findPlanMarkdownProblems(markdown) {
|
|
227
|
+
const doc = mdq(markdown);
|
|
228
|
+
const problems = [];
|
|
229
|
+
const headings = doc.query('h1').nodes();
|
|
230
|
+
if (!doc.query('comment(/^suite/)').exists() || headings.length === 0) {
|
|
231
|
+
problems.push('Missing <!-- suite --> comment followed by the plan name heading');
|
|
232
|
+
}
|
|
233
|
+
const scenarioCount = Math.max(headings.length - 1, 0);
|
|
234
|
+
const testComments = doc.query('comment(/^test/)').count();
|
|
235
|
+
if (testComments !== scenarioCount) {
|
|
236
|
+
problems.push(`Found ${scenarioCount} scenario headings but ${testComments} <!-- test --> comments`);
|
|
237
|
+
}
|
|
238
|
+
for (const section of doc.query('section1').each().slice(1)) {
|
|
239
|
+
const title = section.query('heading').nodes()[0]?.text;
|
|
240
|
+
if (!section.query('section2("Steps") item').exists())
|
|
241
|
+
problems.push(`Scenario "${title}" has no ## Steps items`);
|
|
242
|
+
if (!section.query('section2("Expected") item').exists())
|
|
243
|
+
problems.push(`Scenario "${title}" has no ## Expected items`);
|
|
244
|
+
}
|
|
245
|
+
return problems;
|
|
246
|
+
}
|
|
247
|
+
export function readPlanMarkdownComment(markdown, key) {
|
|
248
|
+
const body = mdq(markdown).query(`comment(/^${key}:/)`).nodes()[0]?.text;
|
|
249
|
+
if (!body)
|
|
250
|
+
return null;
|
|
251
|
+
return body.slice(key.length + 1).trim() || null;
|
|
252
|
+
}
|
|
222
253
|
function formatPlanSuite(plan) {
|
|
223
254
|
let content = `<!-- suite -->\n# ${plan.title}\n\n`;
|
|
224
255
|
if (plan.url) {
|
|
@@ -231,7 +262,7 @@ function formatPlanSuite(plan) {
|
|
|
231
262
|
content += `<!-- test\npriority: ${test.priority}\n-->\n`;
|
|
232
263
|
content += `# ${test.scenario}\n\n`;
|
|
233
264
|
content += '## Requirements\n';
|
|
234
|
-
content += `${test.startUrl ||
|
|
265
|
+
content += `${test.startUrl || CURRENT_PAGE}\n\n`;
|
|
235
266
|
if (test.plannedSteps.length > 0) {
|
|
236
267
|
content += '## Steps\n';
|
|
237
268
|
for (const step of test.plannedSteps) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare function isDynamicSegment(segment: string): boolean;
|
|
2
2
|
export declare function hasDynamicUrlSegment(url: string): boolean;
|
|
3
3
|
export declare function isSamePageFamily(urlA: string, urlB: string): boolean;
|
|
4
|
+
export declare function isSamePageIgnoringQuery(urlA: string, urlB: string): boolean;
|
|
4
5
|
export declare function generalizeSegment(segment: string): string;
|
|
5
6
|
export declare function generalizeUrl(url: string, replaceSegment?: (segment: string) => string): string;
|
|
6
7
|
export declare function matchesUrl(pattern: string, path: string): boolean;
|
|
@@ -51,6 +51,11 @@ export function isSamePageFamily(urlA, urlB) {
|
|
|
51
51
|
}
|
|
52
52
|
return true;
|
|
53
53
|
}
|
|
54
|
+
export function isSamePageIgnoringQuery(urlA, urlB) {
|
|
55
|
+
const a = new URL(urlA, 'http://localhost');
|
|
56
|
+
const b = new URL(urlB, 'http://localhost');
|
|
57
|
+
return a.pathname === b.pathname && a.hash === b.hash;
|
|
58
|
+
}
|
|
54
59
|
export function generalizeSegment(segment) {
|
|
55
60
|
if (/^\d+$/.test(segment))
|
|
56
61
|
return '\\d+';
|
package/docs/basics/providers.md
CHANGED
|
@@ -59,7 +59,7 @@ export default {
|
|
|
59
59
|
model: openrouter('openai/gpt-oss-20b:nitro'),
|
|
60
60
|
visionModel: openrouter('openai/gpt-5.6-luna'),
|
|
61
61
|
agenticModel: openrouter('anthropic/claude-haiku-5.5'),
|
|
62
|
-
decisionModel: { provider: 'openrouter', model: 'typesafe/jev-latest' },
|
|
62
|
+
decisionModel: { provider: 'openrouter', model: '~typesafe/jev-latest' },
|
|
63
63
|
},
|
|
64
64
|
};
|
|
65
65
|
```
|
|
@@ -28,6 +28,14 @@ The plan (`tests/regression/seeds/vault-plan.md`) describes *what* to do but nev
|
|
|
28
28
|
|
|
29
29
|
> The code is carried in a knowledge file, not only experience, on purpose. Knowledge is Explorbot's prominent, deterministic channel for page-specific data — the same mechanism scenario A uses for login credentials. Empirically, cheap models followed an experience-only code recipe only occasionally, so an experience-only gate was a coin-flip; knowledge makes the gate reliable while the experience file still exercises recipe reuse.
|
|
30
30
|
|
|
31
|
+
### Scenario C — late-rendering start page (`regression:slow`)
|
|
32
|
+
|
|
33
|
+
Runs scenario A again with the fixture's slow start on: on the first visit of a session `/issues` arrives as an empty shell and renders client-side 12 seconds later, longer than the page-readiness wait. Scenario A's assertions must still hold, which proves the Researcher waited for the real page instead of researching the blank shell. It executes one test instead of five (`--max-tests 1`, at least one passing): research and planning are what it checks, and scenario A already covers running the plan.
|
|
34
|
+
|
|
35
|
+
### Scenario D — sign-in form at the start URL (`regression:gated`)
|
|
36
|
+
|
|
37
|
+
Runs scenario A's assertions against `explore /workspace`. Signed out, `/workspace` shows the sign-in form at its own URL instead of redirecting to `/login`, and signing in there keeps you on `/workspace`. The planner reports the page blocked by a sign-in form, so explore must hand the report to Captain, sign in with the credentials in `seeds/knowledge/workspace.md` and plan the page again. Like scenario C, it executes one test.
|
|
38
|
+
|
|
31
39
|
## The fixture app — "Trackly"
|
|
32
40
|
|
|
33
41
|
A self-contained issue tracker served in-process by `tests/regression/fixture/server.ts` (`Bun.serve`, ephemeral port, fresh in-memory store per attempt). No external network, no database.
|
|
@@ -40,6 +48,7 @@ A self-contained issue tracker served in-process by `tests/regression/fixture/se
|
|
|
40
48
|
| `/issues/:id` | yes | detail, change-status menu, comment form, delete modal |
|
|
41
49
|
| `/settings` | yes | Profile/Preferences tabs + an activity iframe |
|
|
42
50
|
| `/vault` | no | scenario B access-code gate |
|
|
51
|
+
| `/workspace` | no / yes | scenario D: the sign-in form when signed out, the issue list when signed in |
|
|
43
52
|
| `/api/*` | cookie | REST API over the same store, `+ /api/openapi.json` |
|
|
44
53
|
|
|
45
54
|
The REST API mirrors the store and is served for a future API-testing scenario; the current scenarios do not exercise it.
|
|
@@ -68,7 +77,9 @@ bunx bunosh regression:serve
|
|
|
68
77
|
export OPENROUTER_API_KEY=sk-...
|
|
69
78
|
bunx bunosh regression:experience --retries 0 # cheapest AI path
|
|
70
79
|
bunx bunosh regression:basic --retries 0
|
|
71
|
-
bunx bunosh regression:
|
|
80
|
+
bunx bunosh regression:slow --retries 0
|
|
81
|
+
bunx bunosh regression:gated --retries 0
|
|
82
|
+
bunx bunosh regression:all # all scenarios, default retries
|
|
72
83
|
```
|
|
73
84
|
|
|
74
85
|
Each attempt runs in an isolated throwaway directory under `tests/regression/.runs/` (gitignored). Inspect `tests/regression/.runs/<scenario>-<variant>-a<n>/output/` for the research, plans, and reporter markdown a run produced, and `tests/regression/.runs/report.md` for the summary.
|
|
@@ -83,9 +94,13 @@ bunx bunosh regression:variants --scenario basic --variants native,aria,plain
|
|
|
83
94
|
|
|
84
95
|
## CI and the approval gate
|
|
85
96
|
|
|
86
|
-
`.github/workflows/regression.yml` runs on `pull_request` and `workflow_dispatch`. The job uses the `regression` GitHub environment, which holds `OPENROUTER_API_KEY` behind a **required reviewer**. Every run — including fork PRs — pauses at the environment gate before the secret is exposed. The job runs the PR's own code, so the reviewer must read the PR diff (workflow, Bunoshfile, fixture, lib) before approving; that review is the security boundary. The workflow never uses `pull_request_target`.
|
|
97
|
+
`.github/workflows/regression.yml` runs on `pull_request` and `workflow_dispatch`. `regression:all` runs the scenarios in parallel inside one job, so a run lasts as long as its slowest scenario and needs one approval. The job uses the `regression` GitHub environment, which holds `OPENROUTER_API_KEY` behind a **required reviewer**. Every run — including fork PRs — pauses at the environment gate before the secret is exposed. The job runs the PR's own code, so the reviewer must read the PR diff (workflow, Bunoshfile, fixture, lib) before approving; that review is the security boundary. The workflow never uses `pull_request_target`.
|
|
98
|
+
|
|
99
|
+
Every attempt writes a `record.json` into its run directory. `regression:report` merges those records and posts the report as a sticky PR comment (on `pull_request`) or a `Regression Reports` GitHub Discussion (on `workflow_dispatch`). Fork PRs get a read-only token, so comment posting may fail; the report is also written to the step summary.
|
|
100
|
+
|
|
101
|
+
### Speed comparison
|
|
87
102
|
|
|
88
|
-
The report is
|
|
103
|
+
The report's `Time` column is the duration of the passing attempt. `Previous median` is the median of that scenario's passing time over the last 5 runs, and `Change` flags a difference of 20% or more. The job uploads the run's records as the `regression-summary` artifact (the run directories go to `regression-runs`) (kept 90 days), and the next run downloads the latest five to compare against. Which tests the planner picks changes a run's time, so read one flagged run as a hint and a trend over several as a regression.
|
|
89
104
|
|
|
90
105
|
The report also embeds the **Session Analysis** from the fresh-explore run — Explorbot's Analyst agent writes a prose summary of what works, defects, UX issues, and execution issues, which the harness reads from `output/reports/<label>.md` and appends to the comment. The Analyst is enabled in the config template (`ai.agents.analyst.enabled: true`); it adds one AI call per basic run.
|
|
91
106
|
|
|
@@ -93,7 +108,7 @@ The report also embeds the **Session Analysis** from the fresh-explore run — E
|
|
|
93
108
|
|
|
94
109
|
- **Thresholds** (`MIN_PLANNED_TESTS`, `MIN_PASSED`, `MIN_KEYWORD_HITS`, `MIN_FEATURE_GROUPS`) live at the top of `tests/regression/lib/assertions.ts`.
|
|
95
110
|
- **Models** live in `tests/regression/fixture/explorbot.config.js`. If cheap models are too flaky, promote a stronger model to `ai.model` or lower `MIN_PASSED`.
|
|
96
|
-
- **Run size** — adjust
|
|
111
|
+
- **Run size** — adjust `maxTests` / `minPassed` on the scenario constants and the per-CLI `timeout` values in `Bunoshfile.js`.
|
|
97
112
|
|
|
98
113
|
## Owner prerequisites (one-time)
|
|
99
114
|
|
|
@@ -34,7 +34,7 @@ Inside the TUI, use the matching slash command: `/explore`, `/research`, `/plan`
|
|
|
34
34
|
| List saved plans | `npx explorbot plans [plan]` | `/plans [plan]` | Show plans and their tests |
|
|
35
35
|
| Navigate to a URL | `npx explorbot navigate <url>` | `/navigate <target>` | Reachability probe + session capture |
|
|
36
36
|
| Drill page components | `npx explorbot drill <url>` | `/drill [--save-knowledge <path>] [--max-components <n>]` | Learn interactions |
|
|
37
|
-
| Execute plan tests | `npx explorbot test <planfile> [index]` | `/test [scenario\|number\|*]` | Run scenarios |
|
|
37
|
+
| Execute plan tests | `npx explorbot test <planfile> [index]` | `/test [planfile] [scenario\|number\|*]` | Run scenarios |
|
|
38
38
|
| Re-run generated tests | `npx explorbot rerun <file> [index]` | `/rerun <file> [index]` | With AI auto-healing |
|
|
39
39
|
| List generated tests | `npx explorbot runs [file]` | `/runs [file]` | Index + dry-run |
|
|
40
40
|
| Store domain knowledge | `npx explorbot learn [url] [note]` | `/learn [note]` | Persisted to `knowledge/` |
|
|
@@ -454,6 +454,8 @@ Execute test scenarios using the Tester agent.
|
|
|
454
454
|
```bash
|
|
455
455
|
# CLI
|
|
456
456
|
npx explorbot test output/plans/login.md # run all enabled tests
|
|
457
|
+
npx explorbot test login # same, plan looked up in output/plans
|
|
458
|
+
npx explorbot test login --path example # plan looked up in example/output/plans
|
|
457
459
|
npx explorbot test output/plans/login.md 3 # run test #3
|
|
458
460
|
npx explorbot test output/plans/login.md 1-5 # range
|
|
459
461
|
npx explorbot test output/plans/login.md 1,3,7 # selection
|
|
@@ -463,9 +465,11 @@ npx explorbot test 3 --from-plan output/plans/login.md # index first, plan via
|
|
|
463
465
|
|
|
464
466
|
```
|
|
465
467
|
# TUI
|
|
466
|
-
/test # Run next pending test
|
|
468
|
+
/test # Run next pending test of the current plan
|
|
467
469
|
/test * # Run all pending tests
|
|
468
470
|
/test 2 # Run test #2 from plan
|
|
471
|
+
/test login.md # Load plan file and run all its tests
|
|
472
|
+
/test login.md 2 # Load plan file and run test #2
|
|
469
473
|
/test login # Run tests matching "login"
|
|
470
474
|
/test User can logout successfully # Create and run ad-hoc test
|
|
471
475
|
```
|
|
@@ -218,7 +218,7 @@ The Researcher takes all standard agent options plus options that control intera
|
|
|
218
218
|
| Option | Type | Description |
|
|
219
219
|
|--------|------|-------------|
|
|
220
220
|
| `maxExpandableClicks` | `number` | Maximum expandable elements clicked during deep analysis (default: 10) |
|
|
221
|
-
| `errorPageTimeout` | `number` | Seconds to wait for a
|
|
221
|
+
| `errorPageTimeout` | `number` | Seconds to wait for a blank or barely rendered page to finish loading before research; uses the same readiness checks as every action, and the judge when `ai.decisionModel` is set (default: 10, `0` disables the wait) |
|
|
222
222
|
| `focusSections` | `string[]` | CSS selectors that narrow research to a matching element when present (e.g. an open modal or drawer). First match wins. Applies only to the per-section fallback used after a truncated research response. |
|
|
223
223
|
|
|
224
224
|
```javascript
|
|
@@ -125,6 +125,14 @@ The Captain steps in when a slash command isn't enough — answering questions a
|
|
|
125
125
|
|
|
126
126
|
When a test hits a fatal browser error, the Captain tries to recover — reload, restart the browser, open a fresh tab, or close extra tabs — before the test is stopped, then tells the Tester how to continue. It runs on explicit TUI requests and on test interrupts (stop, pass, skip, or redirect a running test).
|
|
127
127
|
|
|
128
|
+
Wherever Explorbot opens a page, plans a page or starts a test, including in `explore` and `freesail`, the Captain also takes over when another agent reports it is blocked:
|
|
129
|
+
|
|
130
|
+
- the Planner can't plan a page, for example because it shows a sign-in form
|
|
131
|
+
- the Navigator can't reach a page after all its attempts
|
|
132
|
+
- the page open at a test's start isn't where that test can start; this check needs [`ai.decisionModel`](../reference/configuration.md)
|
|
133
|
+
|
|
134
|
+
The Captain gets what the agent was asked to do, where it stopped and why, and what was already tried. It works out from the browser session how to get past the blocker. Then the planning, the page or the test continues. If the Captain can't resolve it, the page or test is skipped, and the same blocker isn't handed to it again in that run.
|
|
135
|
+
|
|
128
136
|
## Per-Agent Model Configuration
|
|
129
137
|
|
|
130
138
|
Use different models for different agents to control cost:
|
|
@@ -27,6 +27,8 @@ Log in with these credentials:
|
|
|
27
27
|
|
|
28
28
|
Explorbot reads this when it opens the login page and signs in on its own. See [Knowledge](../workflow/knowledge.md).
|
|
29
29
|
|
|
30
|
+
Some apps show the sign-in form at the page you asked for instead of redirecting to `/login`. When the planner reports that the page is blocked by a sign-in form, `explore` hands that report to Captain. Captain looks at the browser session, decides how to get past the form with this knowledge, and re-plans the page itself. If Captain adds no tests, the page is skipped with the planner's reason. Set the knowledge file's `url` to that page so the credentials load there.
|
|
31
|
+
|
|
30
32
|
## Stay logged in between runs
|
|
31
33
|
|
|
32
34
|
Logging in on every run is slow. Use `--session` to save cookies and local storage, then restore them next time:
|
|
@@ -39,7 +39,7 @@ ai: {
|
|
|
39
39
|
| `sections` | `string[]` | all sections | Page sections to identify (order = priority) |
|
|
40
40
|
| `focusSections` | `string[]` | `[]` | CSS selectors used in the truncated-response fallback, when research is split into per-section requests (first match wins). The matching element becomes the container for the focused section — see [Handling Truncated Responses](#handling-truncated-responses). |
|
|
41
41
|
| `maxExpandableClicks` | `number` | `10` | Max expandable elements clicked during deep research |
|
|
42
|
-
| `errorPageTimeout` | `number` | `10` | Seconds to wait for
|
|
42
|
+
| `errorPageTimeout` | `number` | `10` | Seconds to wait for a blank or barely rendered page to settle before research. Uses the same readiness checks as every action (network idle, quiet DOM, `playwright.spinnerSelectors`, visible content), so a small page that has rendered stops the wait early. With `ai.decisionModel` set, the judge decides whether a page that still looks unfinished is loading, an error page or loaded, and Explorbot waits for the page to change before asking again. Error pages detected during this wait abort research. Set `0` to skip the wait |
|
|
43
43
|
| `retries` | `number` | `2` | Retries when most locators are broken in Stage 2 |
|
|
44
44
|
| `reasoning` | `string` | `'low'` | AI SDK v7 reasoning effort: `'none'`, `'minimal'`, `'low'`, `'medium'`, `'high'`, `'xhigh'`, `'provider-default'` |
|
|
45
45
|
| `providerOptions` | `object` | - | Provider-specific options. Reasoning keys here take precedence over `reasoning`. |
|
package/models.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "CLI app built with React Ink, CodeceptJS, and Playwright",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -131,6 +131,7 @@
|
|
|
131
131
|
"sambanova-ai-provider": "^1.2.2",
|
|
132
132
|
"strip-ansi": "^7.1.2",
|
|
133
133
|
"turndown": "^7.2.1",
|
|
134
|
+
"wrap-ansi": "^9.0.2",
|
|
134
135
|
"unique-names-generator": "^4.7.1",
|
|
135
136
|
"yaml": "^2.8.3",
|
|
136
137
|
"yargs": "^17.7.2",
|
|
@@ -65,14 +65,8 @@ Trying to fill the form on the page
|
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
```js
|
|
68
|
-
I.fillField('
|
|
68
|
+
I.fillField('//html/body//form//input[@name="name"]', 'Value');
|
|
69
69
|
```
|
|
70
70
|
</example_output>
|
|
71
71
|
|
|
72
|
-
If
|
|
73
|
-
|
|
74
|
-
<example_output>
|
|
75
|
-
```js
|
|
76
|
-
throw new Error('No resolution');
|
|
77
|
-
```
|
|
78
|
-
</example_output>
|
|
72
|
+
If no solution is possible, call stop() with the reason instead of proposing code.
|
|
@@ -26,6 +26,10 @@ Prefer ARIA locators for reliable element detection.
|
|
|
26
26
|
I.seeElement('.success-message');
|
|
27
27
|
</example>
|
|
28
28
|
|
|
29
|
+
To assert text inside element via `has-text` CSS, prefer using I.see() instead:
|
|
30
|
+
|
|
31
|
+
Prefer `I.see('Collection', '.users')` over `I.seeElement(".users div:has-text("Collection")")`
|
|
32
|
+
|
|
29
33
|
### I.seeInField
|
|
30
34
|
|
|
31
35
|
I.seeInField(<locator>, <value>, <context>)
|
|
@@ -141,8 +145,8 @@ NEVER use `:has-text(...)` inside a seeElement/dontSeeElement locator. Checking
|
|
|
141
145
|
NEVER emit two assertions that check the same fact with different shapes. `I.see(text, locator)` and `I.seeElement("<locator>:has-text('text')")` verify the same thing — pick one (prefer I.see). One claim, one assertion.
|
|
142
146
|
</verification_rules>
|
|
143
147
|
|
|
144
|
-
[
|
|
148
|
+
[Prefer the commands listed here; use other CodeceptJS I.* commands when none of these fits]
|
|
145
149
|
[INTERACT ONLY WITH ELEMENTS THAT ARE ON THE PAGE HTML OR ARIA SNAPSHOT]
|
|
146
150
|
[DO NOT USE WAIT FUNCTIONS]
|
|
147
151
|
|
|
148
|
-
</actions>
|
|
152
|
+
</actions>
|
package/src/action-result.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
2
|
import { ConfigParser, type HtmlConfig, outputPath } from './config.ts';
|
|
4
3
|
import type { Link, WebPageState } from './state-manager.ts';
|
|
5
4
|
import { LARGE_ARIA_CHANGE_THRESHOLD, compactAriaSnapshot, diffAriaSnapshots } from './utils/aria.ts';
|
|
@@ -742,7 +741,7 @@ export class Diff {
|
|
|
742
741
|
async calculate(): Promise<void> {
|
|
743
742
|
if (!this.previous) return;
|
|
744
743
|
|
|
745
|
-
this._htmlDiffResult = await htmlDiff(this.previous.html, this.current.html
|
|
744
|
+
this._htmlDiffResult = await htmlDiff(this.previous.html, this.current.html);
|
|
746
745
|
|
|
747
746
|
if (!this._isSameUrl) {
|
|
748
747
|
this._messages = liveRegionMessages(this.previous.html, this.current.html);
|
package/src/action.ts
CHANGED
|
@@ -34,8 +34,6 @@ class Action {
|
|
|
34
34
|
public actionResult: ActionResult | null = null;
|
|
35
35
|
private config: ExplorbotConfig;
|
|
36
36
|
|
|
37
|
-
// action info
|
|
38
|
-
private action: string | null = null;
|
|
39
37
|
public lastError: Error | null = null;
|
|
40
38
|
public playwrightHelper: any;
|
|
41
39
|
public playwrightGroupId: string | null = null;
|
|
@@ -389,8 +387,6 @@ class Action {
|
|
|
389
387
|
}
|
|
390
388
|
|
|
391
389
|
private async executeOnce(code: string, verbatim = false): Promise<Action> {
|
|
392
|
-
let error: Error | null = null;
|
|
393
|
-
|
|
394
390
|
setActivity('🔎 Browsing...', 'action');
|
|
395
391
|
|
|
396
392
|
let codeString = code.replace(/^\(I\) => /, '').trim();
|
|
@@ -441,7 +437,6 @@ class Action {
|
|
|
441
437
|
this.assertionSteps = assertionSteps;
|
|
442
438
|
} catch (err) {
|
|
443
439
|
debugLog('Action error', errorToString(err));
|
|
444
|
-
error = err as Error;
|
|
445
440
|
if (!isPlaywright) {
|
|
446
441
|
await recorder.reset();
|
|
447
442
|
await recorder.start();
|
|
@@ -3,7 +3,9 @@ import { createBashTool } from 'bash-tool';
|
|
|
3
3
|
import dedent from 'dedent';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { ConfigParser } from '../../config.ts';
|
|
6
|
-
import { Test } from '../../test-plan.ts';
|
|
6
|
+
import { Plan, Test } from '../../test-plan.ts';
|
|
7
|
+
import { getStyles } from '../planner/styles.ts';
|
|
8
|
+
import { PlanningBlockedError } from '../../utils/error-page.ts';
|
|
7
9
|
import { listRecentArtifacts, readCaptainFile } from './file-tools.ts';
|
|
8
10
|
import { type Constructor, type ModeContext, resolveProjectRoot } from './mixin.ts';
|
|
9
11
|
|
|
@@ -36,11 +38,18 @@ export function WithIdleMode<T extends Constructor>(Base: T) {
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
const tools: Record<string, any> = {
|
|
39
|
-
|
|
40
|
-
description:
|
|
41
|
+
plan: tool({
|
|
42
|
+
description: dedent`
|
|
43
|
+
Plan tests for the current page. Adds them to the current plan when it is for this page, otherwise starts a new plan.
|
|
44
|
+
Without tests, the Planner writes the tests. With tests, exactly those tests are added and the Planner does not run: use it for tests the user dictates.
|
|
45
|
+
Returns how many tests were added, or why planning failed.
|
|
46
|
+
`,
|
|
41
47
|
inputSchema: z.object({
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
feature: z.string().optional().describe('Focus area for the Planner'),
|
|
49
|
+
style: z
|
|
50
|
+
.enum(Object.keys(getStyles()) as [string, ...string[]])
|
|
51
|
+
.optional()
|
|
52
|
+
.describe('Planning style for the Planner; omit to let it choose'),
|
|
44
53
|
tests: z
|
|
45
54
|
.array(
|
|
46
55
|
z.object({
|
|
@@ -49,33 +58,36 @@ export function WithIdleMode<T extends Constructor>(Base: T) {
|
|
|
49
58
|
expected: z.array(z.string()).optional(),
|
|
50
59
|
})
|
|
51
60
|
)
|
|
52
|
-
.optional()
|
|
61
|
+
.optional()
|
|
62
|
+
.describe('Tests to add as written, without running the Planner'),
|
|
63
|
+
replace: z.boolean().optional().describe('Remove the tests already in this page plan first'),
|
|
64
|
+
title: z.string().optional().describe('New plan title'),
|
|
53
65
|
}),
|
|
54
|
-
execute: async ({
|
|
66
|
+
execute: async ({ feature, style, tests, replace, title }) => {
|
|
67
|
+
if (ctx.blocker instanceof PlanningBlockedError) {
|
|
68
|
+
feature = ctx.blocker.feature;
|
|
69
|
+
style = ctx.blocker.style;
|
|
70
|
+
}
|
|
71
|
+
const url = ctx.explorBot.stateManager().getCurrentState()?.url || '';
|
|
55
72
|
let plan = ctx.explorBot.getCurrentPlan();
|
|
56
|
-
if (
|
|
57
|
-
|
|
73
|
+
if (plan?.url && plan.url !== url) plan = undefined;
|
|
74
|
+
if (replace) plan?.tests.splice(0);
|
|
75
|
+
const before = new Set(plan?.tests ?? []);
|
|
76
|
+
if (!tests?.length) {
|
|
77
|
+
plan = await ctx.explorBot.plan(feature, { style });
|
|
78
|
+
const error = ctx.explorBot.lastPlanError;
|
|
79
|
+
if (error) return { success: false, message: error.message };
|
|
58
80
|
}
|
|
59
81
|
if (!plan) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (title) {
|
|
63
|
-
plan.title = title;
|
|
82
|
+
plan = new Plan(url);
|
|
83
|
+
ctx.explorBot.setCurrentPlan(plan);
|
|
64
84
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
plan.tests.length = 0;
|
|
68
|
-
}
|
|
69
|
-
const currentUrl = ctx.explorBot.stateManager().getCurrentState()?.url || '';
|
|
70
|
-
for (const testInput of tests) {
|
|
71
|
-
const priority = testInput.priority || 'normal';
|
|
72
|
-
const expected = testInput.expected?.length ? testInput.expected : [];
|
|
73
|
-
const test = new Test(testInput.scenario, priority, expected, currentUrl);
|
|
74
|
-
plan.addTest(test);
|
|
75
|
-
}
|
|
85
|
+
for (const t of tests ?? []) {
|
|
86
|
+
plan.addTest(new Test(t.scenario, t.priority || 'normal', t.expected ?? [], url));
|
|
76
87
|
}
|
|
88
|
+
if (title) plan.title = title;
|
|
77
89
|
plan.updateStatus();
|
|
78
|
-
return { success: true, tests: plan.tests.length };
|
|
90
|
+
return { success: true, testsAdded: plan.tests.filter((t) => !before.has(t)).length, tests: plan.tests.length };
|
|
79
91
|
},
|
|
80
92
|
}),
|
|
81
93
|
project: tool({
|
|
@@ -144,7 +156,7 @@ export function WithIdleMode<T extends Constructor>(Base: T) {
|
|
|
144
156
|
|
|
145
157
|
return dedent`
|
|
146
158
|
<idle_capabilities>
|
|
147
|
-
- Plan management:
|
|
159
|
+
- Plan management: plan() — run the Planner on the current page, or add tests you write yourself
|
|
148
160
|
- readFile() — read specific report, plan, log, generated test, knowledge, or experience file content
|
|
149
161
|
- bash() — discover files and inspect file metadata
|
|
150
162
|
- READ from: ${knowledgeDir}/, ${experienceDir}/, ${outputDir}/
|
package/src/ai/captain/mixin.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { dirname } from 'node:path';
|
|
2
1
|
import { ConfigParser } from '../../config.ts';
|
|
3
2
|
import type { ExplorBot } from '../../explorbot.ts';
|
|
4
3
|
import type { Task } from '../../test-plan.ts';
|
|
4
|
+
import type { BlockedError } from '../../utils/error-page.ts';
|
|
5
5
|
import { createDebug } from '../../utils/logger.js';
|
|
6
6
|
|
|
7
7
|
export type Constructor<T = object> = new (...args: any[]) => T;
|
|
@@ -13,6 +13,7 @@ export type CaptainMode = 'idle' | 'web' | 'test' | 'heal';
|
|
|
13
13
|
export interface ModeContext {
|
|
14
14
|
explorBot: ExplorBot;
|
|
15
15
|
task: Task;
|
|
16
|
+
blocker: BlockedError | null;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export function resolveProjectRoot(): string | null {
|