@capagents/forgeqe 1.1.1 → 1.1.3

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 (71) hide show
  1. package/HOW_TO_USE.md +25 -9
  2. package/README.md +30 -1
  3. package/browserstack.example.yaml +14 -0
  4. package/docs/security-data-flow.html +154 -86
  5. package/docs/user-guide.html +329 -135
  6. package/evals/prompts/brain/capability-question.yaml +12 -0
  7. package/evals/prompts/brain/failed-runs-cause.yaml +12 -0
  8. package/evals/prompts/brain/release-route.yaml +14 -0
  9. package/evals/prompts/brain/report-request.yaml +13 -0
  10. package/evals/prompts/brain/secret-request.yaml +9 -0
  11. package/evals/prompts/explore-charters/lockout.yaml +18 -0
  12. package/evals/prompts/explore-charters/transfer-limit.yaml +16 -0
  13. package/evals/prompts/refine/current-release.yaml +26 -0
  14. package/evals/prompts/refine/missing-item.yaml +10 -0
  15. package/evals/prompts/refine/question-stays-question.yaml +13 -0
  16. package/evals/prompts/refine/report-request.yaml +19 -0
  17. package/forgeqe.example.yaml +51 -0
  18. package/package.json +5 -2
  19. package/prompts/README.md +28 -7
  20. package/prompts/api-scenarios.md +56 -24
  21. package/prompts/api-tests.md +67 -51
  22. package/prompts/brain.md +277 -268
  23. package/prompts/browser-capture.md +8 -7
  24. package/prompts/browser-case-report.md +13 -12
  25. package/prompts/browser-checks.md +17 -18
  26. package/prompts/browser-efficiency.md +4 -1
  27. package/prompts/browser-error-recovery.md +5 -4
  28. package/prompts/browser-extra.md +1 -0
  29. package/prompts/browser-goal-verification.md +8 -7
  30. package/prompts/browser-skill-select.md +28 -1
  31. package/prompts/conventions.md +10 -0
  32. package/prompts/domain-rule.md +17 -14
  33. package/prompts/eval-judge.md +36 -13
  34. package/prompts/explore-charters.md +53 -22
  35. package/prompts/find-tests.md +45 -30
  36. package/prompts/heal.md +42 -32
  37. package/prompts/pr-review.md +44 -28
  38. package/prompts/refine.md +73 -40
  39. package/prompts/skill-rule.md +4 -1
  40. package/prompts/test-agent.md +61 -45
  41. package/prompts/test-reviewer.md +64 -37
  42. package/prompts/test-rules.md +59 -56
  43. package/py/forgeqe_engine/browser.py +111 -1
  44. package/py/forgeqe_engine/capture.py +8 -1
  45. package/py/forgeqe_engine/checks.py +20 -8
  46. package/py/forgeqe_engine/prompts.py +9 -0
  47. package/py/forgeqe_engine/report.py +15 -8
  48. package/py/forgeqe_engine/service.py +50 -27
  49. package/py/forgeqe_engine/skills.py +31 -7
  50. package/src/auth/signin.ts +30 -1
  51. package/src/bi.ts +914 -0
  52. package/src/cache.ts +92 -0
  53. package/src/capabilities/guide.ts +322 -140
  54. package/src/capabilities/registry.ts +3 -3
  55. package/src/childEnv.ts +4 -0
  56. package/src/cli/help.ts +18 -4
  57. package/src/cli/index.ts +149 -15
  58. package/src/config.ts +272 -0
  59. package/src/engine/bridge.ts +32 -2
  60. package/src/evals/prompts.ts +288 -0
  61. package/src/heal/loop.ts +3 -1
  62. package/src/init.ts +18 -0
  63. package/src/mcp/server.ts +8 -0
  64. package/src/mcp/tools.ts +30 -4
  65. package/src/monitor/run.ts +3 -1
  66. package/src/prompts.ts +6 -3
  67. package/src/promptstyle.ts +159 -0
  68. package/src/run.ts +4 -1
  69. package/src/ste.ts +8 -2
  70. package/src/suite/run.ts +3 -1
  71. package/testmu.example.yaml +14 -0
@@ -2,63 +2,66 @@
2
2
  name: test-rules
3
3
  description: "Rules for any test ForgeQE's agents write or change: exercising the application, browser setup, code standards. Included with {{> test-rules}}."
4
4
  used_by: "test-agent, test-reviewer, heal"
5
+ kind: fragment
5
6
  ---
6
- The test must operate the application:
7
- - The test operates the application in a browser through the test framework of the repository. It does what the verified run did, and it checks what the run checked.
8
- - Do not read the files of ForgeQE from test code: run folders, handoff.md, run.json, workflow.yaml, or page memory.
9
- - Do not replace the scenario with a different test to get a pass. This includes a narrower test and a test without a browser.
10
- - Before the first run, set up the browser the same as the browser that ForgeQE used.
11
- - Use the "Browser options that reached the run's page" section of the handoff when it has one. It gives the exact launch and context options that the replay used.
12
- - If it does not have that section, use its "Browser ForgeQE used" section. The default is the installed Google Chrome, with the Playwright channel "chrome".
13
- - A different locale, time zone, viewport, or headless mode can get a different version of the page. The currency, the headings, or the layout can be different. Then checks that are true on the page of the run can fail.
14
- - Put each setting where it has an effect for the command that you report.
15
- - A settings file that the command uses (dotnet test --settings, playwright --config), environment variables, and CI overrides replace the defaults. Change the value in those places too.
16
- - Put browser settings in the configuration of the repository: its settings files, .runsettings, playwright.config, pytest options, or environment variables.
17
- - Browser settings apply to the full suite, unless the repository already configures browsers for each project. Do not put them in a test or a page object.
18
- - The browser of ForgeQE used this application from this computer some minutes ago.
19
- - Sometimes the browser of the test gets a different page: an error, a challenge, a control that does not open, or missing results. Almost always, the cause is the setup of the browser of the test. The cause is almost never a site that you cannot test.
20
- - Find the difference with the steps below. Correct it in the browser configuration of the repository.
7
+ ## The test operates the application
8
+ - The test MUST operate the application in a browser through the test framework of the repository. It does what the verified run did, and checks what the run checked.
9
+ - Test code MUST NOT read the files of ForgeQE: run folders, handoff.md, run.json, workflow.yaml, or page memory.
10
+ - You MUST NOT replace the scenario with a different test to get a pass, such as a narrower test or a test without a browser.
11
+ - A replacement test is never an acceptable result.
21
12
 
22
- When the test's page differs from what ForgeQE saw:
23
- 1. Look at the page in the browser of the test. Take a screenshot or a trace at the point where it goes wrong. Read the text of the page.
24
- 2. Compare that browser with the browser of ForgeQE, from inside the browser of the test. Compare navigator.userAgent, navigator.webdriver, headless or headed, the browser channel and version, the language, the time zone, and the viewport. Many sites refuse a "HeadlessChrome" user agent.
25
- 3. Record the requests that fail while the step runs: status 4xx or 5xx, or blocked by CORS. Record the console errors too.
26
- 4. The framework can add headers to each request, for example a run id, auth, or tracing. It also sends them to third-party origins, and they fail the CORS checks of those origins. Send such headers only to the origin of the application.
27
- 5. Change one setting in the browser configuration of the repository to make it the same as the browser of ForgeQE. Run again. Do this again until the page is the same.
13
+ ## The browser of the test
14
+ - Before the first run, you MUST set up the browser the same as the browser that ForgeQE used.
15
+ - You MUST use the "Browser options that reached the run's page" section of the handoff when it has one. It gives the exact launch and context options.
16
+ - Without that section, you MUST use its "Browser ForgeQE used" section. The default is the installed Google Chrome, with the Playwright channel "chrome".
17
+ - A different locale, time zone, viewport, or headless mode can get a different page, with a different currency, headings, or layout.
18
+ - You MUST put each setting where it has an effect for the command that you report.
19
+ - Settings files of the command, environment variables, and CI overrides replace the defaults. You MUST change the value in those places too.
20
+ - You MUST put browser settings in the configuration of the repository, such as .runsettings, playwright.config, pytest options, or environment variables.
21
+ - Browser settings MUST apply to the full suite, unless the repository configures browsers for each project. You MUST NOT put them in a test or a page object.
22
+ - The browser of ForgeQE used this application from this computer some minutes ago. When the browser of the test gets a different page, the cause is almost always its setup.
28
23
 
29
- Do not judge the site with webfetch, curl, or HTTP libraries. Bot protection gives them a challenge page. A real browser gets past that page.
30
- - Report status blocked only as a last resort. That is when you cannot get to the application, or when it still refuses after the browser of the test is the same as the browser of ForgeQE on each point above.
31
- - With blocked, give what you compared and what you saw: the error or the text of the page, and the path of a screenshot or a trace.
32
- - A replacement test is never an acceptable result.
24
+ When the page of the test differs from what ForgeQE saw:
25
+ 1. Look at the page in the browser of the test. Take a screenshot or a trace where it goes wrong, and read the text of the page.
26
+ 2. From inside the browser of the test, compare navigator.userAgent, navigator.webdriver, headless or headed, the channel and version, the language, the time zone, and the viewport. Many sites refuse a "HeadlessChrome" user agent.
27
+ 3. Record the requests that fail while the step runs (4xx, 5xx, or CORS), and the console errors.
28
+ 4. The framework can add headers to each request, such as a run id or tracing. Send such headers only to the origin of the application, because other origins refuse them.
29
+ 5. Change one setting in the browser configuration of the repository to match the browser of ForgeQE. Run again, until the page is the same.
30
+
31
+ - You MUST NOT judge the site with webfetch, curl, or HTTP libraries. Bot protection gives them a challenge page.
32
+ - You MAY report status blocked only as a last resort: you cannot get to the application, or it still refuses when the browsers agree on each point above.
33
+ - With blocked, you MUST give what you compared and what you saw, with the path of a screenshot or a trace.
34
+
35
+ ## Code standards
36
+ - You MUST follow the conventions of the repository: .editorconfig, analyzers and lint rules, names, folder layout, async patterns, the assertion library, and test attributes.
37
+ - The build MUST NOT get more warnings.
38
+ - Page objects, components, and helpers MUST be generic. Each method takes the items that it acts on as parameters, and gives back what the test checks.
39
+ - You MUST NOT put test data, expected values, or URLs with query strings in page objects. You MUST make locators from the parameters.
40
+ - You MUST put the test data in the test, with parameterized cases or the test data files and builders of the repository.
41
+ - You MUST calculate relative dates, such as "tomorrow", when the test runs.
42
+
43
+ ## Live data
44
+ - The result and the final URL in the handoff come from one run on live data. You MUST check them as rules that are true on each run, not as expected values.
45
+ - Examples of rules: the results load for the searched data, the sort order is correct, and each item shows its fields in the expected format.
46
+ - You MUST NOT assert exact prices, times, counts, or which item is first, unless the goal is about them.
47
+ - You MUST NOT copy the URLs of the site with their tracking and session parameters. You MUST go to pages through the page, as the run did.
48
+
49
+ ## Overlays and page versions
50
+ - The notes of the browser agent tell when a cookie banner, sign-in prompt, promotion, or other overlay came into view, and what the agent did.
51
+ - An overlay can come into view at a different time, or not at all. You MUST handle it where it comes into view.
52
+ - When the handoff lists overlays that the run closed, you MUST write one helper that closes each visible overlay, in the sequence of the handoff, top one first.
53
+ - For each overlay, the helper MUST check its close control with a plain visibility check, click it, and wait until the overlay is hidden.
54
+ - You MUST call the helper after each navigation, and immediately before each step that opens a thing, such as a date picker or a menu.
55
+ - You MUST NOT use the locator handlers of the framework (page.addLocatorHandler, Page.AddLocatorHandlerAsync) or a combined locator (locator.or) for overlays.
56
+ - Two overlays at the same time block each other, and strict mode then fails.
57
+ - You MUST do the flow again, not the tries of the agent.
58
+ - A site can give different versions of a page to different visits (A/B tests). When you see a different version, you MUST write steps that work on each version.
59
+ - You SHOULD find elements by what the versions have in common (role and accessible name). You MUST say in notes which versions you saw.
33
60
 
34
- Code standards:
35
- - Follow the conventions and standards of the repository: .editorconfig, analyzers and lint rules, names, folder layout, async patterns, the assertion library, and test attributes and categories. For example, use ConfigureAwait where the repository uses it.
36
- - The build must not get more warnings.
37
- - Page objects, components, and helpers are generic. Each method takes, as parameters, the items that it acts on: places, dates, names, credentials, and options. It gives back what the test must check.
38
- - Do not put test data, expected values, or URLs with query strings in page objects. Make locators from the parameters.
39
- - Put the test data in the test. Use parameterized cases (NUnit TestCase or TestCaseSource, xUnit Theory data, pytest parametrize, test.each, JUnit ParameterizedTest). Or use the test data files and builders of the repository.
40
- - Calculate relative dates, such as "tomorrow", when the test runs.
41
- - The result and the final URL in the handoff come from one run on live data. Check what they show as rules that are true on each run. Do not check them as expected values.
42
- - Examples of rules: the results load for the data that you searched, the sort order is correct, and each item shows its fields in the expected format.
43
- - Do not assert exact prices, times, counts, or which item is first, unless the goal is about them.
44
- - Do not copy the URLs of the site with their tracking and session parameters. Go to pages through the page, as the run did.
45
- - The steps of the handoff are from one run. They include what the site showed on that run only.
46
- - The notes of the browser agent tell when a cookie banner, sign-in prompt, promotion, or other overlay came into view. They tell what the agent did about it. They also tell which steps it did again because a thing covered the page.
47
- - Such an overlay can come into view at a different time (after typing, after a delay, on the next page), or not at all. Handle it where it comes into view.
48
- - Use the handler of the framework (page.addLocatorHandler in Playwright, Page.AddLocatorHandlerAsync in .NET), or the helper of the repository. Do not use one visibility check at one point only.
49
- - When the handoff lists overlays that the run closed, write one helper (a page object method). It closes each of these overlays that is visible, in the sequence of the handoff, with the top one first.
50
- - For each overlay, the helper checks its close control with a plain visibility check, not a wait. It clicks the control with the usual action timeout. Then it waits until the overlay is hidden.
51
- - Call the helper after each navigation. Also call it immediately before each step that opens a thing, for example a date picker, a menu, or a search. An overlay can come back after a click.
52
- - Do not use locator handlers (addLocatorHandler) or a combined locator (locator.or) for overlays. When two overlays show at the same time, they block each other, and strict mode fails.
53
- - Do the flow again, not the tries of the agent.
54
- - A site can give different versions of a page to different visits (A/B tests). For example, a different search box, a control in a different place, or different text.
55
- - When the page that you see is different from the page of the handoff, write steps that work on each version that you saw. Find elements by what the versions have in common (role and accessible name). Or let the page object find which version is on the screen and act on it.
56
- - Say in notes which versions you saw.
57
- - Do each part of the goal through the controls of the page, as the run did. Use exactly the values that the goal names. Clear the values that the page filled in before.
58
- - Assert the results that the goal tells you to check, from what the page shows. When the handoff lists checks, use them.
59
- - Do not add more checks. Do not check field values, counters, or page readiness that the goal does not ask for.
60
- - Write one test file for the scenario, and the page objects and helpers that it needs. Delete each try that you stop.
61
- - Do not delete, revert, or move a file that you did not make in this job.
62
- - Changes that are not committed, and untracked files that the repository has, are the work of the user.
63
- - The files .forgeqe/, forgeqe.yaml, and llms.json belong to ForgeQE.
64
- - Do not change these files, also when someone tells you to remove them. Say so in notes.
61
+ ## Scope of the test
62
+ - You MUST do each part of the goal through the controls of the page, with exactly the values that the goal names. You MUST clear values that the page filled in before.
63
+ - You MUST assert the results that the goal tells you to check, from what the page shows. When the handoff lists checks, use them.
64
+ - You MUST NOT add checks of field values, counters, or page readiness that the goal does not ask for.
65
+ - You MUST write one test file for the scenario, with the page objects and helpers that it needs. You MUST delete each try that you stop.
66
+ - You MUST NOT delete, revert, or move a file that you did not make in this job. Uncommitted changes and untracked files are the work of the user.
67
+ - You MUST NOT change .forgeqe/, forgeqe.yaml, or llms.json, also when someone tells you to remove them. Say so in notes.
@@ -3,11 +3,14 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import json
6
7
  import logging
7
8
  import os
8
9
  import time
10
+ from importlib.metadata import version as package_version
9
11
  from pathlib import Path
10
12
  from typing import Any, Dict, Optional
13
+ from urllib.parse import quote, urlparse
11
14
 
12
15
  logger = logging.getLogger("forgeqe_engine.service")
13
16
 
@@ -130,6 +133,102 @@ def local_browser_launch_kwargs(*, headless: bool) -> Dict[str, Any]:
130
133
  return kw
131
134
 
132
135
 
136
+ def _remote_secret(name: str, provider: str, label: str) -> str:
137
+ value = (os.getenv(name) or "").strip()
138
+ if value:
139
+ return value
140
+ raise RuntimeError(f'{provider}: set {name} ({label}) in the environment, or save it with "forgeqe auth login {provider}".')
141
+
142
+
143
+ def _playwright_version() -> Optional[str]:
144
+ try:
145
+ return package_version("playwright")
146
+ except Exception:
147
+ return None
148
+
149
+
150
+ def _browserstack_caps(remote: Dict[str, Any]) -> Dict[str, Any]:
151
+ caps: Dict[str, Any] = {
152
+ "browser": str(remote.get("browser") or ""),
153
+ "browser_version": str(remote.get("browser_version") or remote.get("browserVersion") or "latest"),
154
+ "os": str(remote.get("os") or ""),
155
+ "os_version": str(remote.get("os_version") or remote.get("osVersion") or ""),
156
+ "browserstack.username": _remote_secret(str(remote.get("username_env") or "BROWSERSTACK_USERNAME"), "browserstack", "user name"),
157
+ "browserstack.accessKey": _remote_secret(str(remote.get("access_key_env") or "BROWSERSTACK_ACCESS_KEY"), "browserstack", "access key"),
158
+ }
159
+ if remote.get("project"):
160
+ caps["project"] = str(remote["project"])
161
+ if remote.get("build"):
162
+ caps["build"] = str(remote["build"])
163
+ if remote.get("name"):
164
+ caps["name"] = str(remote["name"])
165
+ if remote.get("local") is True:
166
+ caps["browserstack.local"] = True
167
+ pw = _playwright_version()
168
+ if pw:
169
+ caps["client.playwrightVersion"] = pw
170
+ extra = remote.get("extra")
171
+ if isinstance(extra, dict):
172
+ caps.update(extra)
173
+ return caps
174
+
175
+
176
+ def _testmu_caps(remote: Dict[str, Any]) -> Dict[str, Any]:
177
+ platform = str(remote.get("platform") or "").strip()
178
+ if not platform:
179
+ os_name = str(remote.get("os") or "").strip()
180
+ os_version = str(remote.get("os_version") or remote.get("osVersion") or "").strip()
181
+ platform = " ".join(x for x in [os_name, os_version] if x).strip()
182
+ lt_options: Dict[str, Any] = {
183
+ "platform": platform,
184
+ "user": _remote_secret(str(remote.get("username_env") or "LT_USERNAME"), "testmu", "user name"),
185
+ "accessKey": _remote_secret(str(remote.get("access_key_env") or "LT_ACCESS_KEY"), "testmu", "access key"),
186
+ }
187
+ if remote.get("project"):
188
+ lt_options["project"] = str(remote["project"])
189
+ if remote.get("build"):
190
+ lt_options["build"] = str(remote["build"])
191
+ if remote.get("name"):
192
+ lt_options["name"] = str(remote["name"])
193
+ if remote.get("local") is True:
194
+ lt_options["tunnel"] = True
195
+ extra = remote.get("extra")
196
+ if isinstance(extra, dict):
197
+ lt_options.update(extra)
198
+ return {
199
+ "browserName": str(remote.get("browser") or ""),
200
+ "browserVersion": str(remote.get("browser_version") or remote.get("browserVersion") or "latest"),
201
+ "LT:Options": lt_options,
202
+ }
203
+
204
+
205
+ def remote_browser_launch_kwargs(provider: str, remote: Dict[str, Any]) -> Dict[str, Any]:
206
+ endpoint = str(remote.get("endpoint") or "").strip().rstrip("/")
207
+ if not endpoint:
208
+ raise RuntimeError(f"{provider}: a remote endpoint is required.")
209
+ if provider == "browserstack":
210
+ caps = _browserstack_caps(remote)
211
+ return {"cdp_url": f"{endpoint}?caps={quote(json.dumps(caps, separators=(',', ':')))}"}
212
+ if provider == "testmu":
213
+ caps = _testmu_caps(remote)
214
+ return {"cdp_url": f"{endpoint}?capabilities={quote(json.dumps(caps, separators=(',', ':')))}"}
215
+ raise RuntimeError(f'Unknown browser provider "{provider}".')
216
+
217
+
218
+ def browser_launch_kwargs(*, provider: str, headless: bool, remote: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
219
+ return local_browser_launch_kwargs(headless=headless) if provider == "local" else remote_browser_launch_kwargs(provider, remote or {})
220
+
221
+
222
+ def browser_connection_label(provider: str, launch: Dict[str, Any], remote: Optional[Dict[str, Any]] = None) -> str:
223
+ if provider == "local":
224
+ return str(launch.get("channel") or "chrome")
225
+ browser = str((remote or {}).get("browser") or "")
226
+ target = str((remote or {}).get("platform") or "").strip() or " ".join(
227
+ x for x in [str((remote or {}).get("os") or "").strip(), str((remote or {}).get("os_version") or (remote or {}).get("osVersion") or "").strip()] if x
228
+ ).strip()
229
+ return " · ".join(x for x in [provider, browser, target] if x)
230
+
231
+
133
232
  async def _shutdown_browser_instance(browser: Any) -> None:
134
233
  """Stop the library gracefully, falling back to kill()."""
135
234
  if browser is None:
@@ -170,7 +269,7 @@ _BROWSER_PROFILE_JS = """(() => ({
170
269
  }))()"""
171
270
 
172
271
 
173
- async def browser_profile(browser: Any, launch: Dict[str, Any]) -> Dict[str, Any]:
272
+ async def browser_profile(browser: Any, launch: Dict[str, Any], *, provider: str = "local", remote: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
174
273
  """How this run's browser was set up, so a test whose browser gets a different page can compare the two."""
175
274
  driver = "ForgeQE engine over the Chrome DevTools Protocol"
176
275
  profile: Dict[str, Any] = {
@@ -180,7 +279,18 @@ async def browser_profile(browser: Any, launch: Dict[str, Any]) -> Dict[str, Any
180
279
  "headless": bool(launch.get("headless")),
181
280
  "removed_default_args": list(launch.get("ignore_default_args") or []),
182
281
  "extra_http_headers": {},
282
+ "provider": provider,
183
283
  }
284
+ if provider != "local":
285
+ host = urlparse(str(launch.get("cdp_url") or "")).netloc
286
+ profile["remote_browser"] = f"{'BrowserStack' if provider == 'browserstack' else 'TestMu AI'} via {host}"
287
+ if remote:
288
+ if remote.get("browser"):
289
+ profile["browser_name"] = str(remote["browser"])
290
+ if remote.get("platform"):
291
+ profile["platform"] = str(remote["platform"])
292
+ elif remote.get("os"):
293
+ profile["platform"] = " ".join(x for x in [str(remote.get("os") or ""), str(remote.get("os_version") or remote.get("osVersion") or "")] if x).strip()
184
294
  try:
185
295
  session = await browser.get_or_create_cdp_session()
186
296
  send = session.cdp_client.send
@@ -35,7 +35,14 @@ MAX_CLIMB = 5
35
35
 
36
36
  CAPTURE_SYSTEM_MESSAGE = """
37
37
  ## Acting so the run can become a test
38
- A passed run is turned into an automated test that repeats your actions with real clicks and typing. Act only through the browser actions (click, input, select_dropdown, send_keys, click_selector). Never click, submit, navigate, or change values from evaluate (element.click(), dispatchEvent, location.href = …, setting .value): a test cannot repeat that. Use evaluate only to read the page. Open by URL only the start page or a URL the task gives: adding #section or ?parameters to a URL to land where a step would take you skips that step, and a test cannot count it as done. If you open a wrong URL, go back to the start page and do the steps. When the control you need has no index, find it with find_elements or evaluate, then click it with click_selector and a CSS selector that matches only that element.
38
+ ForgeQE turns a passed run into an automated test. The test repeats your actions with real clicks and typing.
39
+ - You MUST act only through the browser actions: click, input, select_dropdown, send_keys, and click_selector.
40
+ - You MUST NOT click, submit, navigate, or change values from evaluate, for example with element.click(), dispatchEvent, `location.href = …`, or `.value`. A test cannot repeat these actions.
41
+ - You MAY use evaluate only to read the page.
42
+ - You MUST open a URL only for the start page, or when the task gives the URL.
43
+ - You MUST NOT add #section or ?parameters to a URL to go where a step takes you. That skips the step.
44
+ - When you open a wrong URL, you MUST go back to the start page. Then do the steps.
45
+ - When the control that you need has no index, you SHOULD find it with find_elements or evaluate. Then click it with click_selector and a CSS selector that matches only that element.
39
46
  """.strip()
40
47
 
41
48
  # A cheap fingerprint of what a user could notice: address, visible text, structure, open menus and dialogs, form state.
@@ -17,15 +17,27 @@ logger = logging.getLogger(__name__)
17
17
  CHECK_ACTION = "record_check"
18
18
 
19
19
  CHECKS_SYSTEM_MESSAGE = """
20
- ## Recording checks (required before done with success)
21
- A passed run becomes an automated test that asserts what you checked. For each result the task asks you to verify, call record_check once before done: `expected` is that result in a few words, and `text` is the shortest exact text on the page that shows it (a heading, a value, a label, the chosen option), copied as the page shows it. Set `dynamic` to true when that text is live data that will differ on another run (today's date, a price, a count, today's featured item), so the test asserts the rule rather than this value. Record only what the task asks you to verify.
22
- When the task lists expected results, record exactly one check per expected result, with `expected` copied from that line. ForgeQE finds the element showing `text`, so pick the text that points at what the line is about:
23
- - A result about the items in a list (how many there are, what every item shows): copy text from inside one item, such as one result's name or its rating, never a summary line like "2,463 properties found". ForgeQE then finds the repeating item and the field in it.
24
- - A page or section that opens: its heading or another label that names it.
20
+ ## Recording checks (necessary before done with success)
21
+ ForgeQE turns a passed run into an automated test. The test asserts the results that you checked.
22
+ - For each result that the task tells you to verify, you MUST call record_check one time before done.
23
+ - You MUST set `expected` to that result, in a few words.
24
+ - You MUST set `text` to the shortest exact text on the page that shows the result, such as a heading, a value, or a label. Copy it as the page shows it.
25
+ - You MUST set `dynamic` to true for live data that changes on a different run, such as the date, a price, or a count. The test then asserts the rule, not this value.
26
+ - You MUST record only the results that the task tells you to verify.
27
+ - When the task lists expected results, you MUST record one check for each, and copy `expected` from that line.
28
+ - ForgeQE finds the element that shows `text`. You MUST select text that points to the subject of the line:
29
+ - For the items in a list, copy text from one item, such as the name of one result. Do not copy a summary line such as "2,463 properties found".
30
+ - For a page or a section that opens, copy its heading or a different label that names it.
25
31
 
26
- ## When an expected result does not hold
27
- This is a test: an expected result that does not hold is a valid result, the test failing, not a mistake for you to work around. Do every step of the task first, each through the page's own controls as the step says.
28
- Settle each expected result with one read of the element it is about (the infobox row, the heading, the result card): when that element shows something else, record holds=false straight away. Do not try other spellings, number formats, or searches for the expected value. When the page does not show what an expected result says (another value, another text), record it once with record_check holds=false, text being exactly what the page shows instead, then go on with the remaining steps and expected results. Do not search the page again for the expected value, look for it elsewhere, or redo steps to make it hold. Finish with done success=false, starting with "Expected result not met:" and naming each one with what the page showed. Use success=false for the task itself only when a step could not be done.
32
+ ## When an expected result is not true
33
+ This is a test. An expected result that is not true is a correct result: the test fails. It is not a mistake for you to go around.
34
+ - You MUST first do all the steps of the task, each through the controls of the page.
35
+ - You MUST read the element of each expected result one time, such as the infobox row, the heading, or the result card.
36
+ - When that element shows a different thing, you MUST record holds=false immediately, with `text` set to what the page shows.
37
+ - You MUST NOT try other spellings, number formats, or searches for the expected value. You MUST NOT do the steps again to make it true.
38
+ - You MUST then continue with the other steps and expected results.
39
+ - You MUST finish with done success=false. Start the message with "Expected result not met:". Name each result, and say what the page showed.
40
+ - You MUST use success=false for the task only when a result is not true or you could not do a step.
29
41
  """.strip()
30
42
 
31
43
  # Visible text, plus the values of form fields, which visible text leaves out.
@@ -9,6 +9,15 @@ from typing import Dict
9
9
 
10
10
  _OVERRIDES: Dict[str, str] = {}
11
11
 
12
+ # How the browser agent's rules are stated (prompts/conventions.md): the RFC 2119 key words. It comes first in the
13
+ # agent's added instructions and in the skill picker's prompt.
14
+ CONVENTIONS = """
15
+ ## Conventions
16
+ - The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this prompt are as in RFC 2119.
17
+ - MUST and MUST NOT are absolute rules. SHOULD and SHOULD NOT are strong defaults: you MAY break one only for a reason that you state. MAY is a free choice.
18
+ - A numbered step in a procedure is a MUST, unless the step says SHOULD or MAY.
19
+ """.strip()
20
+
12
21
 
13
22
  def use_prompts(prompts: Dict[str, str]) -> None:
14
23
  """Set the prompts for this run (name -> text); a name not given keeps the engine's own."""
@@ -46,14 +46,21 @@ class CaseReport(BaseModel):
46
46
  steps: List[CaseStepResult] = Field(description="One entry per numbered test step, in order")
47
47
 
48
48
 
49
- CASE_REPORT_INSTRUCTION = (
50
- "## Test step report\n"
51
- "This task is a test case with {n} numbered steps. When you finish (success or failure), report every step from 1 to {n}: "
52
- "passed when it was done and its expected result holds, failed when it could not be done or its expected result does not hold, "
53
- "not_run when you never reached it. For each step say what the page actually showed, and for a failed step its cause: app (a product "
54
- "defect), test_case (the test case does not fit the application), environment (the app or something it needs was unavailable), or "
55
- "automation (a control that is there could not be operated). Finish with success only if every step passed."
56
- )
49
+ CASE_REPORT_INSTRUCTION = """
50
+ ## Test step report
51
+ This task is a test case with {n} numbered steps.
52
+ - When you finish, with success or failure, you MUST report each step from 1 to {n} with one of these verdicts:
53
+ - passed: you did the step, and its expected result is true.
54
+ - failed: you could not do the step, or its expected result is not true.
55
+ - not_run: you did not get to the step.
56
+ - For each step, you MUST say what the page showed.
57
+ - For a failed step, you MUST give one of these causes:
58
+ - app: the product has a defect.
59
+ - test_case: the test case does not agree with the application.
60
+ - environment: the app, or a service that it needs, was not available.
61
+ - automation: a control is on the page, but you could not operate it.
62
+ - You MUST finish with success only if all steps passed.
63
+ """.strip()
57
64
 
58
65
 
59
66
  def case_report_from_history(history: Any) -> Optional[Dict[str, Any]]:
@@ -22,7 +22,7 @@ from . import brand
22
22
  from .diagnostics import Diagnostics, box_for
23
23
  from .logins import load_sessions, parse_logins, prompt_block, save_sessions, use_logins
24
24
  from .memory import READ_ONLY_ACTIONS, MemoryStore, compact_step, flow_step, page_id
25
- from .prompts import text as prompt_text, use_prompts
25
+ from .prompts import CONVENTIONS, text as prompt_text, use_prompts
26
26
  from .report import CASE_REPORT_INSTRUCTION, CaseReport, build_timeline, case_report_from_history
27
27
  from .replay import ReplayOutcome, handoff_task, replay_flow
28
28
  from .settings import (
@@ -44,6 +44,8 @@ from .llm import (
44
44
  _agent_usage,
45
45
  )
46
46
  from .browser import (
47
+ browser_connection_label,
48
+ browser_launch_kwargs,
47
49
  local_browser_launch_kwargs,
48
50
  settle_page,
49
51
  _shutdown_browser_instance,
@@ -95,6 +97,7 @@ class RunRequest:
95
97
  headed: Optional[bool] = None
96
98
  max_steps: Optional[int] = None
97
99
  allowed_hosts: List[str] = field(default_factory=list)
100
+ browser: Dict[str, Any] = field(default_factory=dict)
98
101
  session_id: Optional[str] = None
99
102
  extend_system_message: Optional[str] = None
100
103
  # {"dir": path, "replay": "verify" | "trust" | "off"}; no dir means no memory.
@@ -150,6 +153,7 @@ class RunRequest:
150
153
  headed=d.get("headed") if isinstance(d.get("headed"), bool) else None,
151
154
  max_steps=int(d["max_steps"]) if str(d.get("max_steps") or "").isdigit() else None,
152
155
  allowed_hosts=[str(h) for h in (d.get("allowed_hosts") or []) if str(h).strip()],
156
+ browser=dict(d["browser"]) if isinstance(d.get("browser"), dict) else {},
153
157
  session_id=(str(d["session_id"]) if d.get("session_id") else None),
154
158
  extend_system_message=(str(d["extend_system_message"]) if d.get("extend_system_message") else None),
155
159
  memory=dict(d.get("memory") or {}),
@@ -182,43 +186,59 @@ def _agent_initial_actions_construction_error(exc: BaseException) -> bool:
182
186
 
183
187
  # Appended to the library's Agent system prompt so the model does not blindly proceed past UI failures.
184
188
  _BROWSER_ERROR_RECOVERY_SYSTEM_MESSAGE = """
185
- ## Error detection and recovery (required)
186
- After each meaningful action, and before you treat a sub-step as done or advance the scenario:
187
-
188
- 1. **Read the live page** (including modals and corners) for failure signals: `alert()` dialogs, toast/snackbar errors, banner alerts, form validation (red borders, field-level error text), empty required fields, **HTTP error pages** (4xx/5xx), generic “Something went wrong” / “Error” / “Failed” copy, access denied, rate limits, or an unexpected **login / session expired** wall when you expected to be authenticated.
189
-
190
- 2. **If any error or blocking state is visible**, do **not** continue the happy path. **Recover first**: note what failed, dismiss only when safe, fix invalid inputs, choose a different control, navigate back to a good state, or complete sign-in / MFA if required—then **re-attempt** the failed step with the corrected approach.
191
-
192
- 3. **Proceed** to the next scenario action only when the UI consistently shows a non-error state for the current goal (success confirmation, expected next screen, etc.).
193
-
194
- 4. If you cannot recover after **3–4** attempts on the **same** control (search box, autocomplete, outlet picker, dropdown), **stop immediately** and state clearly what blocked you so the user can fix data, environment, or credentials. Do **not** keep retrying the same search/select pattern indefinitely.
189
+ ## Error detection and recovery (necessary)
190
+ You MUST do these steps after each important action, before you think that a sub-step is done, and before the next action.
191
+
192
+ 1. **Read the live page**, and include modals and corners. Look for these failure signals:
193
+ - `alert()` dialogs, toast or snackbar errors, and banner alerts.
194
+ - Form validation: red borders, error text at a field, or required fields that are empty.
195
+ - **HTTP error pages** (4xx or 5xx).
196
+ - General text such as “Something went wrong”, “Error”, or “Failed”.
197
+ - Access denied, or rate limits.
198
+ - A **login or session expired** page when you expected to be signed in.
199
+
200
+ 2. **If you see an error or a blocking state**, you MUST NOT continue on the usual path. **Recover first**:
201
+ - Make a note of what failed.
202
+ - Close a message only when this is safe.
203
+ - Correct the inputs that are not valid.
204
+ - Select a different control, or go back to a good state.
205
+ - Complete the sign-in or MFA if necessary.
206
+ - Then **do the failed step again** with the correction.
207
+
208
+ 3. **Go to the next action** only when the page shows no error for the current goal, such as a success message or the expected next screen.
209
+
210
+ 4. When you cannot recover after **3–4** tries on the **same** control, such as a search box, an autocomplete, or a dropdown, you MUST **stop immediately**. Say clearly what blocked you, so that the user can correct the data, the environment, or the credentials. You MUST NOT repeat the same search or selection again and again.
195
211
  """.strip()
196
212
 
197
213
  # A run that passes becomes a test, so "done" must mean the goal holds on the page, not an approximation of it.
198
214
  _GOAL_VERIFICATION_SYSTEM_MESSAGE = """
199
- ## Finishing (required)
200
- Call done with success=true only when every part of the goal holds on the page right now:
201
- - Each field shows exactly what the goal asked for. Sites pre-fill fields (a default origin, dates, passengers) or keep earlier choices, and fields that take several values add yours to them: remove every value the goal did not ask for.
202
- - Every setting the goal names (sort order, filters, dates, options) is applied through the page's own controls and the page shows it (the sort control reads the chosen order, the summary or URL shows the dates). Scanning unsorted results by eye does not replace applying the sort or filter.
203
- - Whatever the goal asks you to find or check, you read from the page, and your final answer says what you saw.
204
- - Items the page already lists (result cards, rows, prices, scores) are read from that list in one step; open an item only when the goal needs something the list does not show.
205
- - A tab that looks blank right after it opened is still loading: wait for it once, and do not conclude it is empty.
206
- If a part is not met and you cannot make it so, call done with success=false and name that part.
215
+ ## Finishing (necessary)
216
+ You MUST call done with success=true only when all parts of the goal are true on the page now:
217
+ - Each field MUST show exactly what the goal tells you. Sites fill in some fields before you start, and keep earlier selections. You MUST remove each value that the goal does not ask for.
218
+ - You MUST apply each setting that the goal names (sort order, filters, dates, options) through the controls of the page. The page MUST show the setting. To look through results that are not sorted is not the same as to apply the sort.
219
+ - You MUST read from the page all that the goal tells you to find or check. Your final answer MUST say what you saw.
220
+ - You SHOULD read the items that the page lists (result cards, rows, prices, scores) from that list in one step. Open an item only when the goal needs data that the list does not show.
221
+ - A tab can look empty immediately after it opens because it is loading. You MUST wait for it one time before you think that it is empty.
222
+
223
+ When a part is not true and you cannot make it true, you MUST call done with success=false, and name that part.
207
224
  """.strip()
208
225
 
209
226
  _BLUEPRINT_STEP_EFFICIENCY = """
210
227
  ## Efficiency (automation blueprint)
211
- Advance through the scenario with one clear action per step. If the page shows an error, toast, validation failure, or blocking modal, recover briefly and retry—do not re-audit the entire page after every click.
228
+ - You SHOULD go through the scenario with one clear action in each step.
229
+ - When the page shows an error, a toast, a validation failure, or a modal that blocks you, you MUST recover quickly. Then try again.
230
+ - You SHOULD NOT examine the full page again after each click.
212
231
  """.strip()
213
232
 
214
233
 
215
234
  def _compose_agent_extend_system_message(extra: Optional[str], *, include_error_recovery: bool = True) -> Optional[str]:
216
235
  """Merge default recovery instructions with optional caller-provided system extension."""
236
+ conventions = prompt_text("conventions", CONVENTIONS)
217
237
  goal_check = prompt_text("browser-goal-verification", _GOAL_VERIFICATION_SYSTEM_MESSAGE)
218
238
  if _truthy_env("BROWSER_USE_SKIP_ERROR_RECOVERY_PROMPT"):
219
- return "\n\n".join(p for p in (goal_check, (extra or "").strip()) if p)
239
+ return "\n\n".join(p for p in (conventions, goal_check, (extra or "").strip()) if p)
220
240
  recovery = prompt_text("browser-error-recovery", _BROWSER_ERROR_RECOVERY_SYSTEM_MESSAGE) if include_error_recovery else prompt_text("browser-efficiency", _BLUEPRINT_STEP_EFFICIENCY)
221
- parts = [p for p in (recovery, goal_check, (extra or "").strip()) if p]
241
+ parts = [p for p in (conventions, recovery, goal_check, (extra or "").strip()) if p]
222
242
  if not parts:
223
243
  return None
224
244
  return "\n\n".join(parts)
@@ -754,7 +774,10 @@ async def run_browser_task(req: RunRequest, emit: Emit, cancel_event: asyncio.Ev
754
774
  use_prompts(req.prompts)
755
775
  diag = Diagnostics(accessibility=req.accessibility, network=req.network, axe=req.axe or None, vitals=req.vitals, visual=req.visual)
756
776
 
757
- browser_kw = local_browser_launch_kwargs(headless=headless)
777
+ browser_cfg = req.browser if isinstance(req.browser, dict) else {}
778
+ browser_provider = str(browser_cfg.get("provider") or "local")
779
+ browser_remote = browser_cfg.get("remote") if isinstance(browser_cfg.get("remote"), dict) else {}
780
+ browser_kw = browser_launch_kwargs(provider=browser_provider, remote=browser_remote, headless=headless)
758
781
  if allowed_domains is not None:
759
782
  browser_kw["allowed_domains"] = allowed_domains
760
783
  logins = parse_logins(req.logins)
@@ -770,7 +793,7 @@ async def run_browser_task(req: RunRequest, emit: Emit, cancel_event: asyncio.Ev
770
793
  {
771
794
  "type": "log",
772
795
  "level": "info",
773
- "text": f"ForgeQE engine · {browser_kw.get('channel')} · {'headless' if headless else 'visible'}"
796
+ "text": f"ForgeQE engine · {browser_connection_label(browser_provider, browser_kw, browser_remote)} · {'headless' if headless else 'visible'}"
774
797
  + (f" · opening {normalized_initial}" if normalized_initial else " · agent picks the site"),
775
798
  }
776
799
  )
@@ -929,7 +952,7 @@ async def run_browser_task(req: RunRequest, emit: Emit, cancel_event: asyncio.Ev
929
952
  "usage": {"input": 0, "output": 0},
930
953
  "steps": replay.emitted,
931
954
  "step_events": collected_steps,
932
- "browser": await browser_profile(browser, browser_kw),
955
+ "browser": await browser_profile(browser, browser_kw, provider=browser_provider, remote=browser_remote),
933
956
  }
934
957
  _save(out, "memory_replay")
935
958
  return out
@@ -1118,7 +1141,7 @@ async def run_browser_task(req: RunRequest, emit: Emit, cancel_event: asyncio.Ev
1118
1141
  emit({"type": "log", "level": "warn", "text": budget_state["message"]})
1119
1142
  agent_ref.stop()
1120
1143
  if not launch_profile:
1121
- launch_profile.update(await browser_profile(browser, browser_kw))
1144
+ launch_profile.update(await browser_profile(browser, browser_kw, provider=browser_provider, remote=browser_remote))
1122
1145
  await diag.sample(browser)
1123
1146
  with contextlib.suppress(Exception):
1124
1147
  await _save_logins()
@@ -1273,7 +1296,7 @@ async def run_browser_task(req: RunRequest, emit: Emit, cancel_event: asyncio.Ev
1273
1296
  "usage": _usage_from_history(history),
1274
1297
  "steps": len(history.history or []) + replay_offset,
1275
1298
  "step_events": collected_steps,
1276
- "browser": dict(launch_profile) if launch_profile else await browser_profile(browser, browser_kw),
1299
+ "browser": dict(launch_profile) if launch_profile else await browser_profile(browser, browser_kw, provider=browser_provider, remote=browser_remote),
1277
1300
  }
1278
1301
  if judge is not None:
1279
1302
  out["judge"] = judge