explorbot 0.1.10 → 0.1.12
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/README.md +37 -1
- package/bin/explorbot-cli.ts +27 -18
- package/dist/bin/explorbot-cli.js +26 -18
- package/dist/package.json +3 -3
- package/dist/rules/navigator/output.md +9 -0
- package/dist/rules/navigator/verification-actions.md +2 -0
- package/dist/src/action-result.js +23 -1
- package/dist/src/action.js +51 -42
- package/dist/src/ai/bosun.js +11 -1
- package/dist/src/ai/conversation.js +39 -0
- package/dist/src/ai/historian/codeceptjs.js +109 -0
- package/dist/src/ai/historian/experience.js +321 -0
- package/dist/src/ai/historian/mixin.js +2 -0
- package/dist/src/ai/historian/playwright.js +145 -0
- package/dist/src/ai/historian/screencast.js +121 -0
- package/dist/src/ai/historian/utils.js +18 -0
- package/dist/src/ai/historian.js +21 -405
- package/dist/src/ai/navigator.js +82 -29
- package/dist/src/ai/pilot.js +232 -13
- package/dist/src/ai/planner.js +29 -9
- package/dist/src/ai/provider.js +54 -17
- package/dist/src/ai/researcher.js +41 -32
- package/dist/src/ai/rules.js +26 -14
- package/dist/src/ai/tester.js +90 -26
- package/dist/src/ai/tools.js +13 -7
- package/dist/src/browser-server.js +16 -3
- package/dist/src/commands/add-rule-command.js +11 -8
- package/dist/src/commands/clean-command.js +2 -1
- package/dist/src/commands/explore-command.js +43 -15
- package/dist/src/commands/init-command.js +9 -8
- package/dist/src/commands/plan-command.js +32 -0
- package/dist/src/commands/plan-save-command.js +19 -7
- package/dist/src/commands/rerun-command.js +4 -0
- package/dist/src/components/App.js +15 -5
- package/dist/src/execution-controller.js +13 -2
- package/dist/src/experience-tracker.js +20 -64
- package/dist/src/explorbot.js +8 -8
- package/dist/src/explorer.js +11 -3
- package/dist/src/observability.js +50 -99
- package/dist/src/playwright-recorder.js +309 -0
- package/dist/src/reporter.js +4 -1
- package/dist/src/test-plan.js +12 -0
- package/dist/src/utils/aria.js +37 -1
- package/dist/src/utils/error-page.js +20 -7
- package/dist/src/utils/next-steps.js +37 -0
- package/dist/src/utils/strings.js +15 -0
- package/package.json +3 -3
- package/rules/navigator/output.md +9 -0
- package/rules/navigator/verification-actions.md +2 -0
- package/src/action-result.ts +26 -1
- package/src/action.ts +49 -41
- package/src/ai/bosun.ts +11 -1
- package/src/ai/conversation.ts +37 -0
- package/src/ai/historian/codeceptjs.ts +130 -0
- package/src/ai/historian/experience.ts +384 -0
- package/src/ai/historian/mixin.ts +4 -0
- package/src/ai/historian/playwright.ts +169 -0
- package/src/ai/historian/screencast.ts +133 -0
- package/src/ai/historian/utils.ts +23 -0
- package/src/ai/historian.ts +37 -473
- package/src/ai/navigator.ts +82 -29
- package/src/ai/pilot.ts +237 -14
- package/src/ai/planner.ts +29 -9
- package/src/ai/provider.ts +51 -17
- package/src/ai/researcher.ts +45 -33
- package/src/ai/rules.ts +27 -14
- package/src/ai/tester.ts +94 -26
- package/src/ai/tools.ts +47 -25
- package/src/browser-server.ts +17 -3
- package/src/commands/add-rule-command.ts +11 -7
- package/src/commands/clean-command.ts +2 -1
- package/src/commands/explore-command.ts +46 -14
- package/src/commands/init-command.ts +9 -8
- package/src/commands/plan-command.ts +35 -0
- package/src/commands/plan-save-command.ts +18 -7
- package/src/commands/rerun-command.ts +5 -0
- package/src/components/App.tsx +16 -5
- package/src/config.ts +12 -1
- package/src/execution-controller.ts +14 -3
- package/src/experience-tracker.ts +21 -72
- package/src/explorbot.ts +8 -8
- package/src/explorer.ts +13 -3
- package/src/observability.ts +50 -109
- package/src/playwright-recorder.ts +305 -0
- package/src/reporter.ts +4 -1
- package/src/test-plan.ts +12 -0
- package/src/utils/aria.ts +38 -1
- package/src/utils/error-page.ts +22 -7
- package/src/utils/next-steps.ts +51 -0
- package/src/utils/strings.ts +17 -0
package/src/browser-server.ts
CHANGED
|
@@ -2,7 +2,9 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from '
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { chromium, firefox, webkit } from 'playwright-core';
|
|
4
4
|
import { ConfigParser } from './config.js';
|
|
5
|
-
import {
|
|
5
|
+
import { getCliName } from './utils/cli-name.ts';
|
|
6
|
+
import { log } from './utils/logger.js';
|
|
7
|
+
import { type NextStepSection, printNextSteps } from './utils/next-steps.ts';
|
|
6
8
|
|
|
7
9
|
const ENDPOINT_FILENAME = '.browser-endpoint';
|
|
8
10
|
|
|
@@ -57,8 +59,20 @@ async function launchServer(opts: { browser?: string; show?: boolean }): Promise
|
|
|
57
59
|
writeEndpoint(wsEndpoint);
|
|
58
60
|
|
|
59
61
|
log(`Browser server started: ${browserName} (${opts.show ? 'headed' : 'headless'})`);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
|
|
63
|
+
const cli = getCliName();
|
|
64
|
+
const sections: NextStepSection[] = [
|
|
65
|
+
{
|
|
66
|
+
label: 'Browser server',
|
|
67
|
+
path: getEndpointFilePath(),
|
|
68
|
+
commands: [
|
|
69
|
+
{ label: 'Endpoint', command: wsEndpoint },
|
|
70
|
+
{ label: 'Status', command: `${cli} browser status` },
|
|
71
|
+
{ label: 'Stop', command: `${cli} browser stop` },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
printNextSteps(sections);
|
|
62
76
|
|
|
63
77
|
return server;
|
|
64
78
|
}
|
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { render } from 'ink';
|
|
4
4
|
import React from 'react';
|
|
5
5
|
import { tag } from '../utils/logger.js';
|
|
6
|
+
import { type NextStepSection, printNextSteps, relativeToCwd } from '../utils/next-steps.ts';
|
|
6
7
|
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
7
8
|
|
|
8
9
|
export class AddRuleCommand extends BaseCommand {
|
|
@@ -43,19 +44,22 @@ export class AddRuleCommand extends BaseCommand {
|
|
|
43
44
|
|
|
44
45
|
const filePath = join(rulesDir, `${ruleName}.md`);
|
|
45
46
|
if (existsSync(filePath)) {
|
|
46
|
-
tag('warning').log(`Rule file already exists: ${filePath}`);
|
|
47
|
+
tag('warning').log(`Rule file already exists: ${relativeToCwd(filePath)}`);
|
|
47
48
|
return null;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
const content = opts?.content || `Instructions for ${agentName} agent.`;
|
|
51
52
|
writeFileSync(filePath, `${content.trim()}\n`);
|
|
52
|
-
tag('success').log(`Rule created: ${filePath}`);
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
const configLine = opts?.urlPattern ? `ai.agents.${agentName}.rules: [{ '${opts.urlPattern}': '${ruleName}' }]` : `ai.agents.${agentName}.rules: ['${ruleName}']`;
|
|
55
|
+
const sections: NextStepSection[] = [
|
|
56
|
+
{
|
|
57
|
+
label: 'Rule',
|
|
58
|
+
path: filePath,
|
|
59
|
+
commands: [{ label: 'Add to config', command: configLine }],
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
printNextSteps(sections);
|
|
59
63
|
|
|
60
64
|
return filePath;
|
|
61
65
|
}
|
|
@@ -8,6 +8,7 @@ export const CLEAN_TARGETS: Record<string, { description: string; getDir: () =>
|
|
|
8
8
|
states: { description: 'page states', getDir: () => outputPath('states') },
|
|
9
9
|
research: { description: 'research cache', getDir: () => outputPath('research') },
|
|
10
10
|
plans: { description: 'test plans', getDir: () => outputPath('plans') },
|
|
11
|
+
tests: { description: 'generated tests', getDir: () => outputPath('tests') },
|
|
11
12
|
experiences: { description: 'experience files', getDir: () => getExperienceDir() },
|
|
12
13
|
output: { description: 'all output files', getDir: () => outputPath() },
|
|
13
14
|
};
|
|
@@ -40,7 +41,7 @@ function cleanDirectoryContents(dirPath: string): number {
|
|
|
40
41
|
|
|
41
42
|
export class CleanCommand extends BaseCommand {
|
|
42
43
|
name = 'clean';
|
|
43
|
-
description = 'Clean files: clean [states|research|plans|experiences|output]';
|
|
44
|
+
description = 'Clean files: clean [states|research|plans|tests|experiences|output]';
|
|
44
45
|
suggestions: Suggestion[] = Object.entries(CLEAN_TARGETS).map(([name, target]) => ({ command: `clean ${name}`, hint: `clean ${target.description}` }));
|
|
45
46
|
|
|
46
47
|
async execute(args: string): Promise<void> {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import figureSet from 'figures';
|
|
3
2
|
import { getStyles } from '../ai/planner/styles.js';
|
|
3
|
+
import { outputPath } from '../config.js';
|
|
4
4
|
import { Stats } from '../stats.js';
|
|
5
5
|
import type { Plan } from '../test-plan.js';
|
|
6
6
|
import { getCliName } from '../utils/cli-name.ts';
|
|
7
7
|
import { ErrorPageError } from '../utils/error-page.ts';
|
|
8
8
|
import { tag } from '../utils/logger.js';
|
|
9
9
|
import { jsonToTable } from '../utils/markdown-parser.js';
|
|
10
|
+
import { type NextStepSection, printNextSteps, relativeToCwd } from '../utils/next-steps.ts';
|
|
11
|
+
import { safeFilename } from '../utils/strings.ts';
|
|
10
12
|
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
11
13
|
|
|
12
14
|
export class ExploreCommand extends BaseCommand {
|
|
@@ -70,8 +72,8 @@ export class ExploreCommand extends BaseCommand {
|
|
|
70
72
|
this.explorBot.setCurrentPlan(mainPlan);
|
|
71
73
|
if (mainUrl) await this.explorBot.visit(mainUrl);
|
|
72
74
|
const savedPath = this.explorBot.savePlans(this.completedPlans);
|
|
73
|
-
this.printResults(
|
|
74
|
-
this.
|
|
75
|
+
this.printResults();
|
|
76
|
+
this.printNextSteps(savedPath);
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
private async runAllStyles(pageUrl?: string, feature?: string, parentPlan?: Plan, completedPlans?: Plan[]): Promise<void> {
|
|
@@ -103,7 +105,7 @@ export class ExploreCommand extends BaseCommand {
|
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
107
|
|
|
106
|
-
private printResults(
|
|
108
|
+
private printResults(): void {
|
|
107
109
|
const allTests = this.completedPlans.flatMap((plan) => plan.tests.filter((t) => t.startTime != null).map((test) => ({ test, planTitle: plan.title })));
|
|
108
110
|
|
|
109
111
|
if (allTests.length === 0) return;
|
|
@@ -132,22 +134,52 @@ export class ExploreCommand extends BaseCommand {
|
|
|
132
134
|
if (hasSubPages) columns.push('Plan');
|
|
133
135
|
tag('multiline').log(jsonToTable(rows, columns));
|
|
134
136
|
tag('info').log(`${figureSet.tick} ${allTests.length} tests completed`);
|
|
137
|
+
}
|
|
135
138
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
private printNextSteps(savedPlanPath?: string | null): void {
|
|
140
|
+
const cli = getCliName();
|
|
141
|
+
const sections: NextStepSection[] = [];
|
|
142
|
+
|
|
143
|
+
if (savedPlanPath) {
|
|
144
|
+
const relPlan = relativeToCwd(savedPlanPath);
|
|
145
|
+
sections.push({
|
|
146
|
+
label: 'Plan',
|
|
147
|
+
path: savedPlanPath,
|
|
148
|
+
commands: [
|
|
149
|
+
{ label: 'Re-run', command: `${cli} test ${relPlan} 1` },
|
|
150
|
+
{ label: 'Run all', command: `${cli} test ${relPlan} *` },
|
|
151
|
+
{ label: 'Run range', command: `${cli} test ${relPlan} 1-3` },
|
|
152
|
+
],
|
|
153
|
+
});
|
|
139
154
|
}
|
|
140
|
-
}
|
|
141
155
|
|
|
142
|
-
private printRerunSuggestions(): void {
|
|
143
156
|
const savedFiles = this.explorBot.agentHistorian().getSavedFiles();
|
|
144
|
-
|
|
157
|
+
const screencasts = savedFiles.filter((f) => f.endsWith('.webm'));
|
|
158
|
+
const testFiles = savedFiles.filter((f) => !f.endsWith('.webm'));
|
|
159
|
+
|
|
160
|
+
if (testFiles.length > 0) {
|
|
161
|
+
const commands = testFiles.map((f) => ({ label: '', command: `${cli} rerun ${relativeToCwd(f)}` }));
|
|
162
|
+
commands.push({ label: 'List tests', command: `${cli} runs` });
|
|
163
|
+
sections.push({
|
|
164
|
+
label: `Generated tests (${testFiles.length})`,
|
|
165
|
+
commands,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
145
168
|
|
|
146
|
-
|
|
147
|
-
|
|
169
|
+
if (screencasts.length > 0) {
|
|
170
|
+
const commands = screencasts.map((f) => ({ label: '', command: relativeToCwd(f) }));
|
|
171
|
+
const screencastDir = relativeToCwd(outputPath('screencasts'));
|
|
172
|
+
const planSlugs = [...new Set(this.completedPlans.map((p) => safeFilename(p.title)).filter(Boolean))];
|
|
173
|
+
for (const slug of planSlugs) {
|
|
174
|
+
commands.push({ label: 'Browse plan', command: `ls ${screencastDir}/${slug}-*` });
|
|
175
|
+
}
|
|
176
|
+
sections.push({
|
|
177
|
+
label: `Screencasts (${screencasts.length})`,
|
|
178
|
+
commands,
|
|
179
|
+
});
|
|
148
180
|
}
|
|
149
|
-
|
|
150
|
-
|
|
181
|
+
|
|
182
|
+
printNextSteps(sections);
|
|
151
183
|
}
|
|
152
184
|
|
|
153
185
|
private isLimitReached(): boolean {
|
|
@@ -4,6 +4,7 @@ import chalk from 'chalk';
|
|
|
4
4
|
import dedent from 'dedent';
|
|
5
5
|
import { getCliName } from '../utils/cli-name.ts';
|
|
6
6
|
import { log, tag } from '../utils/logger.js';
|
|
7
|
+
import { relativeToCwd } from '../utils/next-steps.ts';
|
|
7
8
|
|
|
8
9
|
const DEFAULT_CONFIG_TEMPLATE = `import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
|
9
10
|
// import { '<your provider here>' } from '<your provider package here>';
|
|
@@ -61,10 +62,10 @@ export function runInitCommand(options: InitCommandOptions): void {
|
|
|
61
62
|
const dir = resolve(customPath);
|
|
62
63
|
if (!existsSync(dir)) {
|
|
63
64
|
mkdirSync(dir, { recursive: true });
|
|
64
|
-
log(`Created directory: ${dir}`);
|
|
65
|
+
log(`Created directory: ${relativeToCwd(dir)}`);
|
|
65
66
|
}
|
|
66
67
|
process.chdir(dir);
|
|
67
|
-
log(`Working in directory: ${dir}`);
|
|
68
|
+
log(`Working in directory: ${relativeToCwd(dir)}`);
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
try {
|
|
@@ -78,24 +79,24 @@ export function runInitCommand(options: InitCommandOptions): void {
|
|
|
78
79
|
const dir = dirname(outPath);
|
|
79
80
|
if (!existsSync(dir)) {
|
|
80
81
|
mkdirSync(dir, { recursive: true });
|
|
81
|
-
log(`Created directory: ${dir}`);
|
|
82
|
+
log(`Created directory: ${relativeToCwd(dir)}`);
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
if (existsSync(outPath) && !force) {
|
|
85
|
-
log(`Config file already exists: ${outPath}`);
|
|
86
|
+
log(`Config file already exists: ${relativeToCwd(outPath)}`);
|
|
86
87
|
log('Use --force to overwrite existing file');
|
|
87
88
|
process.exit(1);
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
writeFileSync(outPath, DEFAULT_CONFIG_TEMPLATE, 'utf8');
|
|
91
|
-
log(`Created config file: ${outPath}`);
|
|
92
|
+
log(`Created config file: ${relativeToCwd(outPath)}`);
|
|
92
93
|
|
|
93
94
|
const envPath = resolve(process.cwd(), '.env');
|
|
94
95
|
if (!existsSync(envPath)) {
|
|
95
96
|
writeFileSync(envPath, `${DEFAULT_ENV_TEMPLATE}\n`, 'utf8');
|
|
96
|
-
log(`Created env file: ${envPath}`);
|
|
97
|
+
log(`Created env file: ${relativeToCwd(envPath)}`);
|
|
97
98
|
} else {
|
|
98
|
-
log(`Env file already exists: ${envPath}`);
|
|
99
|
+
log(`Env file already exists: ${relativeToCwd(envPath)}`);
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
log('');
|
|
@@ -112,7 +113,7 @@ export function runInitCommand(options: InitCommandOptions): void {
|
|
|
112
113
|
|
|
113
114
|
if (!existsSync('./output')) {
|
|
114
115
|
mkdirSync('./output', { recursive: true });
|
|
115
|
-
log('Created directory:
|
|
116
|
+
log('Created directory: output');
|
|
116
117
|
}
|
|
117
118
|
} catch (error) {
|
|
118
119
|
log('Failed to create config file:', error);
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import figureSet from 'figures';
|
|
4
|
+
import { getCliName } from '../utils/cli-name.ts';
|
|
4
5
|
import { tag } from '../utils/logger.js';
|
|
6
|
+
import { type NextStepSection, printNextSteps, relativeToCwd } from '../utils/next-steps.ts';
|
|
5
7
|
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
6
8
|
|
|
7
9
|
export class PlanCommand extends BaseCommand {
|
|
@@ -41,6 +43,39 @@ export class PlanCommand extends BaseCommand {
|
|
|
41
43
|
|
|
42
44
|
this.printPlanSummary();
|
|
43
45
|
this.updateSuggestions();
|
|
46
|
+
this.printNextSteps();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private printNextSteps(): void {
|
|
50
|
+
const savedPath = this.explorBot.lastSavedPlanPath;
|
|
51
|
+
if (!savedPath) return;
|
|
52
|
+
|
|
53
|
+
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
|
+
];
|
|
67
|
+
|
|
68
|
+
const suite = this.explorBot.getSuite();
|
|
69
|
+
const files = suite && suite.automatedTestCount > 0 ? suite.getAutomatedTestFiles() : [];
|
|
70
|
+
if (files.length > 0) {
|
|
71
|
+
const commands = files.map((f) => ({ label: '', command: `${cli} rerun ${relativeToCwd(f)}` }));
|
|
72
|
+
sections.push({
|
|
73
|
+
label: `Automated tests (${files.length})`,
|
|
74
|
+
commands,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
printNextSteps(sections);
|
|
44
79
|
}
|
|
45
80
|
|
|
46
81
|
private printPlanSummary(): void {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { getCliName } from '../utils/cli-name.ts';
|
|
2
|
+
import { type NextStepSection, printNextSteps, relativeToCwd } from '../utils/next-steps.ts';
|
|
3
3
|
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
4
4
|
|
|
5
5
|
export class PlanSaveCommand extends BaseCommand {
|
|
@@ -15,11 +15,22 @@ export class PlanSaveCommand extends BaseCommand {
|
|
|
15
15
|
|
|
16
16
|
const filename = args.trim() || undefined;
|
|
17
17
|
const savedPath = this.explorBot.savePlan(filename);
|
|
18
|
+
if (!savedPath) return;
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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);
|
|
24
35
|
}
|
|
25
36
|
}
|
|
@@ -24,6 +24,11 @@ export class RerunCommand extends BaseCommand {
|
|
|
24
24
|
filePath = resolve(ConfigParser.getInstance().getTestsDir(), filename);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
if (filePath.endsWith('.spec.ts') || filePath.endsWith('.spec.js')) {
|
|
28
|
+
tag('error').log(`Rerun does not support Playwright tests. Run them with: npx playwright test ${filePath}`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
const testIndices = indexArg ? parseTestIndices(indexArg) : undefined;
|
|
28
33
|
await this.explorBot.agentRerunner().rerun(filePath, { testIndices });
|
|
29
34
|
}
|
package/src/components/App.tsx
CHANGED
|
@@ -99,7 +99,11 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
99
99
|
setShowInput(true);
|
|
100
100
|
|
|
101
101
|
return new Promise<string | null>((resolve) => {
|
|
102
|
-
interruptResolveRef.current =
|
|
102
|
+
interruptResolveRef.current = (value) => {
|
|
103
|
+
interruptResolveRef.current = null;
|
|
104
|
+
setInterruptPrompt(null);
|
|
105
|
+
resolve(value);
|
|
106
|
+
};
|
|
103
107
|
});
|
|
104
108
|
});
|
|
105
109
|
|
|
@@ -107,11 +111,19 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
107
111
|
setShowInput(true);
|
|
108
112
|
};
|
|
109
113
|
|
|
114
|
+
const handleInterrupt = () => {
|
|
115
|
+
if (interruptResolveRef.current) {
|
|
116
|
+
interruptResolveRef.current(null);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
110
120
|
executionController.on('idle', handleIdle);
|
|
121
|
+
executionController.on('interrupt', handleInterrupt);
|
|
111
122
|
setInputCallbackReady(true);
|
|
112
123
|
|
|
113
124
|
return () => {
|
|
114
125
|
executionController.off('idle', handleIdle);
|
|
126
|
+
executionController.off('interrupt', handleInterrupt);
|
|
115
127
|
executionController.reset();
|
|
116
128
|
};
|
|
117
129
|
}, []);
|
|
@@ -284,9 +296,10 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
284
296
|
}
|
|
285
297
|
|
|
286
298
|
if (isCommand) {
|
|
287
|
-
|
|
299
|
+
if (interruptResolveRef.current) {
|
|
300
|
+
interruptResolveRef.current(null);
|
|
301
|
+
}
|
|
288
302
|
setShowInput(false);
|
|
289
|
-
interruptResolveRef.current = null;
|
|
290
303
|
executionController.startExecution();
|
|
291
304
|
try {
|
|
292
305
|
await commandHandler.executeCommand(trimmed);
|
|
@@ -303,8 +316,6 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
303
316
|
|
|
304
317
|
if (interruptResolveRef.current) {
|
|
305
318
|
interruptResolveRef.current(input);
|
|
306
|
-
interruptResolveRef.current = null;
|
|
307
|
-
setInterruptPrompt(null);
|
|
308
319
|
setShowInput(false);
|
|
309
320
|
return;
|
|
310
321
|
}
|
package/src/config.ts
CHANGED
|
@@ -107,6 +107,16 @@ interface PlannerAgentConfig extends AgentConfig {
|
|
|
107
107
|
stylesDir?: string;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
interface ScreencastConfig {
|
|
111
|
+
size?: { width: number; height: number };
|
|
112
|
+
quality?: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface HistorianAgentConfig extends AgentConfig {
|
|
116
|
+
framework?: 'codeceptjs' | 'playwright';
|
|
117
|
+
screencast?: boolean | ScreencastConfig;
|
|
118
|
+
}
|
|
119
|
+
|
|
110
120
|
interface AgentsConfig {
|
|
111
121
|
tester?: TesterAgentConfig;
|
|
112
122
|
navigator?: NavigatorAgentConfig;
|
|
@@ -116,7 +126,7 @@ interface AgentsConfig {
|
|
|
116
126
|
'experience-compactor'?: AgentConfig;
|
|
117
127
|
captain?: AgentConfig;
|
|
118
128
|
quartermaster?: AgentConfig;
|
|
119
|
-
historian?:
|
|
129
|
+
historian?: HistorianAgentConfig;
|
|
120
130
|
fisherman?: AgentConfig;
|
|
121
131
|
chief?: AgentConfig;
|
|
122
132
|
curler?: AgentConfig;
|
|
@@ -229,6 +239,7 @@ export type {
|
|
|
229
239
|
ActionConfig,
|
|
230
240
|
AgentConfig,
|
|
231
241
|
AgentsConfig,
|
|
242
|
+
HistorianAgentConfig,
|
|
232
243
|
ResearcherAgentConfig,
|
|
233
244
|
NavigatorAgentConfig,
|
|
234
245
|
PlannerAgentConfig,
|
|
@@ -10,6 +10,7 @@ export class ExecutionController extends EventEmitter {
|
|
|
10
10
|
private inputCallback: InputCallback | null = null;
|
|
11
11
|
private interruptResolvers: Array<() => void> = [];
|
|
12
12
|
private abortController: AbortController | null = null;
|
|
13
|
+
private awaitingInput = false;
|
|
13
14
|
|
|
14
15
|
private constructor() {
|
|
15
16
|
super();
|
|
@@ -48,6 +49,10 @@ export class ExecutionController extends EventEmitter {
|
|
|
48
49
|
this.emit('idle');
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
isAwaitingInput(): boolean {
|
|
53
|
+
return this.awaitingInput;
|
|
54
|
+
}
|
|
55
|
+
|
|
51
56
|
isInterrupted(): boolean {
|
|
52
57
|
return this.interrupted;
|
|
53
58
|
}
|
|
@@ -77,11 +82,16 @@ export class ExecutionController extends EventEmitter {
|
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
async requestInput(prompt: string): Promise<string | null> {
|
|
80
|
-
if (this.inputCallback) {
|
|
81
|
-
return await this.
|
|
85
|
+
if (!this.inputCallback) {
|
|
86
|
+
return await this.readlineInput(prompt);
|
|
82
87
|
}
|
|
83
88
|
|
|
84
|
-
|
|
89
|
+
this.awaitingInput = true;
|
|
90
|
+
try {
|
|
91
|
+
return await this.inputCallback(prompt);
|
|
92
|
+
} finally {
|
|
93
|
+
this.awaitingInput = false;
|
|
94
|
+
}
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
private async readlineInput(prompt: string): Promise<string | null> {
|
|
@@ -103,6 +113,7 @@ export class ExecutionController extends EventEmitter {
|
|
|
103
113
|
this.interrupted = false;
|
|
104
114
|
this.interruptResolvers = [];
|
|
105
115
|
this.abortController = null;
|
|
116
|
+
this.awaitingInput = false;
|
|
106
117
|
}
|
|
107
118
|
}
|
|
108
119
|
|
|
@@ -18,15 +18,17 @@ export const RECENT_WINDOW_DAYS = 30;
|
|
|
18
18
|
/**
|
|
19
19
|
* Stores and reads per-page experience files (`./experience/<stateHash>.md`).
|
|
20
20
|
*
|
|
21
|
-
*
|
|
21
|
+
* Two writers, two contracts:
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* writeFlow(state, body, relatedUrls?) — caller hands in a fully-formatted
|
|
24
|
+
* `## FLOW: <imperative title>` block (multi-step,
|
|
25
|
+
* `*` bullets + optional ```js``` + `>` discovery,
|
|
26
|
+
* ends with `---`). Tracker dedups + prepends.
|
|
27
|
+
* writeAction(state, ActionInput) — `## ACTION: <imperative title>`, single-step,
|
|
28
|
+
* optional `Solution:` line + one ```js``` code block.
|
|
29
|
+
* Title normalized via normalizeTitle().
|
|
25
30
|
*
|
|
26
31
|
* - Always h2. Never h3 for FLOW/ACTION.
|
|
27
|
-
* - Title is an imperative verb phrase, lowercase-first, no trailing punctuation.
|
|
28
|
-
* Writers normalize automatically (strip own `FLOW:` / `ACTION:` prefix if the caller included it,
|
|
29
|
-
* lowercase first char, trim trailing `.!?,;:`).
|
|
30
32
|
* - On read (getSuccessfulExperience), headings are rendered as
|
|
31
33
|
* `## HOW to <title> (multi-step|single-step)` so prompts get natural phrasing.
|
|
32
34
|
*/
|
|
@@ -184,27 +186,27 @@ export class ExperienceTracker {
|
|
|
184
186
|
tag('substep').log(` Added ACTION to: ${stateHash}.md`);
|
|
185
187
|
}
|
|
186
188
|
|
|
187
|
-
writeFlow(state: ActionResult,
|
|
189
|
+
writeFlow(state: ActionResult, body: string, relatedUrls?: string[]): void {
|
|
188
190
|
if (this.disabled || this.isWritingDisabled(state)) return;
|
|
189
|
-
if (!
|
|
191
|
+
if (!body?.trim()) return;
|
|
190
192
|
|
|
191
193
|
this.ensureExperienceFile(state);
|
|
192
194
|
const stateHash = state.getStateHash();
|
|
193
195
|
const { content, data } = this.readExperienceFile(stateHash);
|
|
194
196
|
|
|
195
|
-
if (
|
|
197
|
+
if (content.includes(body)) {
|
|
198
|
+
debugLog('Skipping duplicate flow body');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (relatedUrls?.length) {
|
|
196
203
|
const currentPath = extractStatePath(state.url || '');
|
|
197
204
|
const existingRelated = Array.isArray(data.related) ? data.related : [];
|
|
198
|
-
const allRelated = [...new Set([...existingRelated, ...
|
|
205
|
+
const allRelated = [...new Set([...existingRelated, ...relatedUrls])];
|
|
199
206
|
data.related = allRelated.filter((url) => url !== currentPath);
|
|
200
207
|
}
|
|
201
208
|
|
|
202
|
-
const
|
|
203
|
-
if (!title) return;
|
|
204
|
-
|
|
205
|
-
const sessionContent = this.trimSessionContent(generateFlowContent(title, flow.steps));
|
|
206
|
-
if (!sessionContent) return;
|
|
207
|
-
const updatedContent = `${sessionContent}\n${content}`;
|
|
209
|
+
const updatedContent = `${body}\n${content}`;
|
|
208
210
|
this.writeExperienceFile(stateHash, updatedContent, data);
|
|
209
211
|
|
|
210
212
|
tag('substep').log(`Added FLOW to: ${stateHash}.md`);
|
|
@@ -277,35 +279,6 @@ export class ExperienceTracker {
|
|
|
277
279
|
// The actual files will be cleaned up by test cleanup
|
|
278
280
|
}
|
|
279
281
|
|
|
280
|
-
private trimSessionContent(content: string): string | null {
|
|
281
|
-
const q = mdq(content);
|
|
282
|
-
if (q.query('heading').count() === 0) return null;
|
|
283
|
-
if (q.query('code').count() === 0) return null;
|
|
284
|
-
|
|
285
|
-
let result = content;
|
|
286
|
-
const codeBlocks = q.query('code').each();
|
|
287
|
-
if (codeBlocks.length > 2) {
|
|
288
|
-
for (const block of codeBlocks.slice(2)) {
|
|
289
|
-
result = result.replace(block.text(), '');
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const blockquotes = mdq(result).query('blockquote').each();
|
|
294
|
-
if (blockquotes.length > 5) {
|
|
295
|
-
for (const bq of blockquotes.slice(5)) {
|
|
296
|
-
result = result.replace(bq.text(), '');
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const lines = result.split('\n');
|
|
301
|
-
if (lines.length > 40) {
|
|
302
|
-
result = lines.slice(0, 40).join('\n');
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (!result.trim()) return null;
|
|
306
|
-
return result;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
282
|
getSuccessfulExperience(state: ActionResult, options?: { includeDescendants?: boolean; stripCode?: boolean }): string[] {
|
|
310
283
|
const records = this.getRelevantExperience(state, {
|
|
311
284
|
includeDescendantExperience: options?.includeDescendants,
|
|
@@ -513,7 +486,9 @@ export function renderExperienceToc(toc: ExperienceTocEntry[]): string {
|
|
|
513
486
|
|
|
514
487
|
const lines: string[] = [];
|
|
515
488
|
lines.push('<experience>');
|
|
516
|
-
lines.push('Past experience for this page —
|
|
489
|
+
lines.push('Past experience for this page — recipes recorded from prior successful runs.');
|
|
490
|
+
lines.push('Locators and step ordering worked then; the page may have changed since.');
|
|
491
|
+
lines.push('Treat as a starting hypothesis, not ground truth. If a step fails, fall back to ARIA/UI-map.');
|
|
517
492
|
lines.push('FLOW: = multi-step recipe (bullets + code + discovery). ACTION: = single-step snippet (one code block).');
|
|
518
493
|
lines.push('Call learn_experience({ fileTag, sectionIndex }) to read a section when it looks relevant to the current step.');
|
|
519
494
|
lines.push('');
|
|
@@ -559,26 +534,6 @@ function generateActionContent(title: string, code: string, explanation?: string
|
|
|
559
534
|
return lines.join('\n');
|
|
560
535
|
}
|
|
561
536
|
|
|
562
|
-
function generateFlowContent(title: string, steps: SessionStep[]): string {
|
|
563
|
-
let content = `## FLOW: ${title}\n\n`;
|
|
564
|
-
for (const step of steps) {
|
|
565
|
-
content += `* ${step.message}\n\n`;
|
|
566
|
-
if (step.code) {
|
|
567
|
-
content += '```js\n';
|
|
568
|
-
content += `${step.code}\n`;
|
|
569
|
-
content += '```\n\n';
|
|
570
|
-
}
|
|
571
|
-
if (step.discovery) {
|
|
572
|
-
const discoveries = step.discovery.split('\n').filter((d) => d.trim());
|
|
573
|
-
for (const discovery of discoveries) {
|
|
574
|
-
content += `> ${discovery.trim()}\n\n`;
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
content += '---\n';
|
|
579
|
-
return content;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
537
|
function renderAsHowTo(content: string): string {
|
|
583
538
|
const tokens = marked.lexer(content);
|
|
584
539
|
let result = '';
|
|
@@ -606,12 +561,6 @@ export interface ExperienceFile {
|
|
|
606
561
|
mtime: Date;
|
|
607
562
|
}
|
|
608
563
|
|
|
609
|
-
export interface FlowInput {
|
|
610
|
-
scenario: string;
|
|
611
|
-
steps: SessionStep[];
|
|
612
|
-
relatedUrls?: string[];
|
|
613
|
-
}
|
|
614
|
-
|
|
615
564
|
export interface ActionInput {
|
|
616
565
|
title: string;
|
|
617
566
|
code: string;
|