@capagents/forgeqe 2.4.0 → 2.4.2
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/HOW_TO_USE.md +28 -7
- package/README.md +4 -2
- package/dist/forgeqe.js +1041 -1007
- package/dist/web/assets/ApiTests-B-0nk720.js +2 -0
- package/dist/web/assets/Chat-Be6J6POH.js +3 -0
- package/dist/web/assets/{Data-Qolfhsd7.js → Data-BtcD7_T2.js} +4 -4
- package/dist/web/assets/Features-DHIGL43K.js +1 -0
- package/dist/web/assets/Jobs-C-cEyrrH.js +3 -0
- package/dist/web/assets/Quality-C6hKykoO.js +1 -0
- package/dist/web/assets/Record-B5395d_b.js +1 -0
- package/dist/web/assets/Regression-BS5g4T3v.js +2 -0
- package/dist/web/assets/Reports-CHqfhNR8.js +2 -0
- package/dist/web/assets/Response-P2oku3_B.js +8 -0
- package/dist/web/assets/Runs-DPmd5QPL.js +2 -0
- package/dist/web/assets/chevron-right-Cwpv0kJi.js +1 -0
- package/dist/web/assets/circle-dashed-COka5oNi.js +1 -0
- package/dist/web/assets/external-link-B9T-hxHX.js +1 -0
- package/dist/web/assets/file-chart-column-increasing-Ch7H145u.js +1 -0
- package/dist/web/assets/filters-C5e4Y0To.js +1 -0
- package/dist/web/assets/index-BI_5kNwE.css +2 -0
- package/dist/web/assets/index-KUNDUjo0.js +26 -0
- package/dist/web/assets/{markdown-GKy06efH.js → markdown-DclrGj2e.js} +7 -7
- package/dist/web/assets/rotate-ccw-BizUsxuR.js +1 -0
- package/dist/web/assets/sticky-note-CFIxnO7K.js +1 -0
- package/dist/web/assets/terminal-BnhGgg6d.js +1 -0
- package/dist/web/assets/trash-CtWVl2Xk.js +1 -0
- package/dist/web/assets/trending-up-BnzWZEhg.js +1 -0
- package/dist/web/assets/when-CHnpLdYw.js +1 -0
- package/dist/web/index.html +2 -3
- package/docs/ARCHITECTURE.md +5 -2
- package/docs/user-guide.html +32 -13
- package/evals/prompts/brain/browser-test-intent.yaml +30 -0
- package/forgeqe.example.yaml +4 -3
- package/package.json +1 -1
- package/prompts/brain.md +4 -1
- package/py/forgeqe_engine/flowmatch.py +72 -1
- package/py/forgeqe_engine/logins.py +17 -5
- package/py/forgeqe_engine/loops.py +26 -3
- package/py/forgeqe_engine/memory.py +11 -0
- package/py/forgeqe_engine/service.py +29 -1
- package/dist/web/assets/ApiTests-CSSed9hU.js +0 -2
- package/dist/web/assets/Chat-CFfiv7wI.js +0 -2
- package/dist/web/assets/Features-BM48eq6x.js +0 -1
- package/dist/web/assets/Jobs-BjVNQUaK.js +0 -3
- package/dist/web/assets/Quality-F0sKErYg.js +0 -1
- package/dist/web/assets/Record-CkwV9gN8.js +0 -1
- package/dist/web/assets/Regression-DKaNfFu1.js +0 -2
- package/dist/web/assets/Reports-o_82URX6.js +0 -3
- package/dist/web/assets/Response-YL9AIB8R.js +0 -8
- package/dist/web/assets/Runs-Dp-DzyW1.js +0 -2
- package/dist/web/assets/circle-dashed-DbpKfamm.js +0 -1
- package/dist/web/assets/external-link-pvVHJFAZ.js +0 -1
- package/dist/web/assets/file-chart-column-increasing-b-Yh6rmz.js +0 -1
- package/dist/web/assets/index-Bkx3fVbr.js +0 -21
- package/dist/web/assets/index-DSEYuBEd.css +0 -2
- package/dist/web/assets/rotate-ccw-BL6YOAPy.js +0 -1
- package/dist/web/assets/search-YLg3uzAx.js +0 -1
- package/dist/web/assets/status-CL_clA0J.js +0 -3
- package/dist/web/assets/sticky-note-DIQ2FWMw.js +0 -1
- package/dist/web/assets/terminal-BdTFlGZR.js +0 -1
- package/dist/web/assets/timer-NLZ0XmVl.js +0 -1
- package/dist/web/assets/trash-DIUJbRpw.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Pt as e}from"./index-KUNDUjo0.js";var t={name:`arrow-left`,size:24,node:[[`path`,{d:`m12 19-7-7 7-7`,key:`1l729n`}],[`path`,{d:`M19 12H5`,key:`x3x0zl`}]]};t.node;var n=e(t),r={name:`rotate-ccw`,size:24,node:[[`path`,{d:`M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8`,key:`1357e3`}],[`path`,{d:`M3 3v5h5`,key:`1xhq8a`}]]};r.node;var i=e(r);export{n,i as t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Pt as e}from"./index-KUNDUjo0.js";var t={name:`sticky-note`,size:24,node:[[`path`,{d:`M21 9a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 15 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2z`,key:`1dfntj`}],[`path`,{d:`M15 3v5a1 1 0 0 0 1 1h5`,key:`6s6qgf`}]]};t.node;var n=e(t);export{n as t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Pt as e}from"./index-KUNDUjo0.js";var t={name:`terminal`,size:24,node:[[`path`,{d:`M12 19h8`,key:`baeox8`}],[`path`,{d:`m4 17 6-6-6-6`,key:`1yngyt`}]]};t.node;var n=e(t);export{n as t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Pt as e}from"./index-KUNDUjo0.js";var t={name:`trash`,size:24,node:[[`path`,{d:`M10 11v6`,key:`nco0om`}],[`path`,{d:`M14 11v6`,key:`outv1u`}],[`path`,{d:`M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6`,key:`miytrc`}],[`path`,{d:`M3 6h18`,key:`d0wm0j`}],[`path`,{d:`M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2`,key:`e791ji`}]],aliases:[`trash-2`]};t.node;var n=e(t);export{n as t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Pt as e}from"./index-KUNDUjo0.js";var t={name:`trending-down`,size:24,node:[[`path`,{d:`M16 17h6v-6`,key:`t6n2it`}],[`path`,{d:`m22 17-8.5-8.5-5 5L2 7`,key:`x473p`}]]};t.node;var n=e(t),r={name:`trending-up`,size:24,node:[[`path`,{d:`M16 7h6v6`,key:`box55l`}],[`path`,{d:`m22 7-8.5 8.5-5-5L2 17`,key:`1t1m79`}]]};r.node;var i=e(r);export{n,i as t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e){let t=e?Date.parse(e):NaN;if(Number.isNaN(t))return``;let n=Math.max(0,(Date.now()-t)/1e3);if(n<60)return`just now`;if(n<3600)return`${Math.floor(n/60)} min ago`;if(n<21600)return`${Math.floor(n/3600)} h ago`;let r=new Date(t);return r.toDateString()===new Date().toDateString()?r.toLocaleTimeString([],{hour:`2-digit`,minute:`2-digit`}):n<518400?r.toLocaleString([],{weekday:`short`,hour:`2-digit`,minute:`2-digit`}):r.toLocaleDateString([],{day:`numeric`,month:`short`})}function t(e,t=new Date){let n=e?new Date(e):null;if(!n||Number.isNaN(n.getTime()))return`Older`;let r=new Date(t.getFullYear(),t.getMonth(),t.getDate()).getTime();return n.getTime()>=r?`Today`:n.getTime()>=r-864e5?`Yesterday`:n.getTime()>=r-5184e5?`Earlier this week`:`Older`}export{t as n,e as t};
|
package/dist/web/index.html
CHANGED
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<meta name="referrer" content="no-referrer" />
|
|
7
7
|
<title>ForgeQE</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-DSEYuBEd.css">
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-KUNDUjo0.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BI_5kNwE.css">
|
|
11
10
|
</head>
|
|
12
11
|
<body>
|
|
13
12
|
<div id="root"></div>
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -172,8 +172,9 @@ shell / forgeqe run ──► openBrain ──► agent runtime session (forgeqe
|
|
|
172
172
|
| `card.ts` | `Card` (`status`, `title`, `stats`, `body`) and `cardText`. `codeCard`, `healCard`, and `roundCard` build cards from the same lines as the terminal summaries |
|
|
173
173
|
| `mcp/server.ts` | JSON-RPC for MCP by hand: `initialize` (protocol 2025-06-18, 2025-03-26, 2024-11-05, with `instructions`), `ping`, `tools/list`, `tools/call`, `notifications/cancelled`. `serveHttp`: Streamable HTTP on 127.0.0.1, checking the bearer token in constant time (`tokenMatches`) and refusing requests whose `Origin` is not this machine (`originAllowed`), so a web page cannot call the tools; tool calls answer as an event stream with a progress notification every 10 s and a keep-alive every 15 s, so the agent runtime's per-call timeout (which resets on progress) never fires during a long suite. `serveStdio`: newline-delimited JSON-RPC for `forgeqe mcp`; closing stdin aborts running calls. `localToolServer`: a server over HTTP with a fresh token and the agent runtime's MCP entry for it, shared by the brain and the healing session |
|
|
174
174
|
| `mcp/git.ts` | `pushBranch`: finds the default branch (`symbolic-ref refs/remotes/<remote>/HEAD`, else `ls-remote --symref`), refuses it and `main`/`master`, checks the name with `git check-ref-format`, and runs `git push --set-upstream <remote> HEAD:refs/heads/<name>` without force. `setCiSecret`: `gh secret set NAME` with the value on stdin, or `az pipelines variable create\|update --secret true` with the value in `AZURE_DEVOPS_EXT_PIPELINE_VAR_<NAME>`; the value is scrubbed from any error |
|
|
175
|
-
| `mcp/external.ts` | `externalMcp` turns `sources:` and `mcp:` into agent runtime MCP entries: `azure_devops` (`npx -y @azure-devops/mcp <org> -a pat\|azcli -d <
|
|
175
|
+
| `mcp/external.ts` | `externalMcp` turns `sources:` and `mcp:` into agent runtime MCP entries: `azure_devops` (`npx -y @azure-devops/mcp <org> -a pat\|azcli -d <every group in ADO_GROUPS>`, with `hints` from `adoHints`: each prefix's words and keywords, and the `mcp_domains` prefixes on every turn), `atlassian` (`uvx mcp-atlassian` with `JIRA_URL` and `JIRA_USERNAME` + `JIRA_API_TOKEN` or `JIRA_PERSONAL_TOKEN`, plus `CONFLUENCE_*` when `sources.confluence` is set, `TOOLSETS=all`; or the hosted Rovo server at `mcp.atlassian.com/v2/mcp` with Basic auth, which also covers Confluence on the same site), `confluence` (`uvx mcp-atlassian` for Confluence alone when Jira's server doesn't cover it), and every `mcp:` server, which replaces a built-in one of the same name. Secrets are `{env:NAME}` references that the agent runtime substitutes; values that must be derived (base64 of `forgeqe:<PAT>` for Azure DevOps, of `email:token` for Rovo) are put in `FORGEQE_*` variables of ForgeQE's own environment before the agent runtime server starts. A system whose server can't start here (no npx or uvx, missing token, Azure DevOps Server, Rovo on Server/DC) is listed with the reason. The servers get a 120 s timeout, which also covers the first `npx`/`uvx` download |
|
|
176
176
|
| `cases/dashboards.ts` | `createAdoDashboard` for the `create_ado_dashboard` tool, since Microsoft's Azure DevOps MCP server has no dashboard or saved-query tools: runs each widget's WIQL once (`$top=1`) so a bad query fails before anything is written, saves it under `Shared Queries/ForgeQE` (creating the folder, PATCHing a query of the same name), then POSTs the dashboard (`7.1-preview.3`, team-scoped when a team is given) with count tiles (`QueryScalarWidget`, `{queryId, queryName}`) across the top, then work item lists (`WitViewWidget`, `{query: {queryId, queryName}}`) and markdown two to a row. Widget `settings` are JSON strings, as the API requires |
|
|
177
|
+
| `cases/adopr.ts` | `createAdoPullRequest` for the `create_ado_pull_request` tool, since the default MCP tool groups leave out `repositories` and `query_api` only reads: `branchRef` checks each branch against git's ref-name rules before anything is sent, the repository is read by name or id, both refs must exist (`refs?filter=heads/…`), an active pull request between the same refs is returned (`existing: true`) instead of a second one, then it POSTs `pullrequests` with a default title of "Merge <source> into <target>" |
|
|
177
178
|
| `cases/wiki.ts` | `writeAdoWikiPage` for the `write_ado_wiki_page` tool, since the MCP server's wiki tools need an existing wiki: picks the named wiki or the project wiki, creating `<Project>.wiki` (`type: projectWiki`, project id from `_apis/projects`) when the project has none, PUTs missing parent pages empty, then PUTs the page with `If-Match` set to its ETag when it already exists. Code wikis get `versionDescriptor` for their first published branch |
|
|
178
179
|
| `cases/api.ts` | `queryApi` for the `query_api` tool: GET under the Azure DevOps organization (and its `vsrm`/`vssps`/`almsearch` hosts, with `api-version=7.1` added) or the Jira site, and Xray Cloud GraphQL queries (mutations refused). Absolute URLs on other hosts are refused, so credentials only go to their own system. Answers over 40,000 characters are cut |
|
|
179
180
|
|
|
@@ -191,7 +192,9 @@ Every model call carries the definitions of the tools the agent has, so the agen
|
|
|
191
192
|
|
|
192
193
|
| Module | Contents |
|
|
193
194
|
|--------|----------|
|
|
194
|
-
| `mcp/scope.ts` | `CapabilityScope`, one per agent session. `beginTurn(text)` picks the turn's capabilities and returns the per-prompt `tools` switches (`forgeqe_<tool>: true\|false`). The picks are: the core (`setup-help`, `test-apps`), `agent.capabilities.always`, the ones `use_capabilities` asked for, those used in the last `STICKY_TURNS` (3) turns, and up to `MAX_PICKED` (4) from `scoreRequest`. `scoreRequest` makes no model call: it matches keywords and tool names, URLs (the browser), issue keys (test cases), and the names of the active environment's apps (by kind). A capability whose required apps or system are missing (`usableCapabilities`) is not picked from words alone, and nothing is picked from `agent.capabilities.off`. When nothing matches, `browser-goals` is the fallback. `request` (from `use_capabilities`) refuses unknown and switched-off ids and queues the added ones in `takePending`. `listedTools` is the list for MCP clients. `capabilityIndex` is the short list of capabilities in the agent's prompt |
|
|
195
|
+
| `mcp/scope.ts` | `CapabilityScope`, one per agent session. `beginTurn(text)` picks the turn's capabilities and returns the per-prompt `tools` switches (`forgeqe_<tool>: true\|false`). The picks are: the core (`setup-help`, `test-apps`), `agent.capabilities.always`, the ones `use_capabilities` asked for, those used in the last `STICKY_TURNS` (3) turns, and up to `MAX_PICKED` (4) from `scoreRequest`. `scoreRequest` makes no model call: it matches keywords and tool names, URLs (the browser), issue keys (test cases), and the names of the active environment's apps (by kind). A capability whose required apps or system are missing (`usableCapabilities`) is not picked from words alone, and nothing is picked from `agent.capabilities.off`. When nothing matches, `browser-goals` is the fallback. `request` (from `use_capabilities`) refuses unknown and switched-off ids and queues the added ones in `takePending`. `listedTools` is the list for MCP clients. `capabilityIndex` is the short list of capabilities in the agent's prompt. Other MCP servers' `ToolGroup`s are switched the same way: `always` ones every turn, a group whose keywords match the request as whole words (they do not count against `MAX_PICKED`; when the request names a system, by a group's `system` words such as "github" or "azure devops", only that system's groups), the ones `use_capabilities` asked for, and one whose tool ran in the last turns (`used` takes the runtime name, from `onToolResult`). `toggles` adds a switch per tool of each group; opencode applies a prompt's `tools` to MCP tools by name, so a switched-off group's definitions are not sent (tests/brain-groups.e2e.test.ts) |
|
|
196
|
+
| `mcp/discover.ts` | `discoverTools` asks every server for its tools (initialize, tools/list) as the runtime starts it: a local command through `spawnPlan` (an argument list; cmd.exe quoting on Windows) with its `{env:}` values filled in and ForgeQE's private variables left out, or a remote address (streamable HTTP, JSON or SSE). The list is kept in `~/.forgeqe/cache/mcp-tools/<hash>.json` (mode 0600) for `LIST_TTL_MS` (a week); the hash is of what the server runs and the names of its variables, never a value or the server's name. `serverGroups` groups each list; a server that does not answer is left out, so its tools stay on |
|
|
197
|
+
| `mcp/toolgroups.ts` | `groupTools` makes a server's groups from its tool names alone. Words of a name are split (snake and camel case), made singular, and verbs and fillers dropped; two words that only ever come together are one ("pull request"). A server whose tools mostly share a first word is split by it; otherwise, and for a prefix group over `MAX_GROUP` tools or `MAX_GROUP_TOKENS`, each tool goes to the rarest word at least 3 of them share, then the rest to words 2 share, the remainder to a catch-all whose keywords are its members' words. Keywords are the group's word or phrase, phrases from its names, plurals, and `SPELLED` (wit: work item); broad words (`WEAK`) are never one alone. A server under `SMALL_SERVER_TOKENS` is one group, always on. `ServerHints` only add titles, keywords and the always-on groups (`mcp/adogroups.ts` for Azure DevOps) |
|
|
195
198
|
| `mcp/captools.ts` | `use_capabilities`: adds capabilities for the rest of the session |
|
|
196
199
|
| `mcp/routing.ts` | `createRouter` passes every ForgeQE tool that runs to `scope.used` |
|
|
197
200
|
| `brain/index.ts` | `ask` starts each turn with `beginTurn` and prompts with its switches. When `use_capabilities` added capabilities during the turn, it continues the same request with them, at most `MAX_SCOPE_CONTINUATIONS` (2) times. `FORGEQE_TOOL_SCOPE=off` turns the scope off |
|
package/docs/user-guide.html
CHANGED
|
@@ -396,7 +396,7 @@ forgeqe docs --copy ./docs # copy the guide, the architecture page, and the re
|
|
|
396
396
|
<div class="area"><a class="area-name" href="#area-api"><span class="mono">API</span></a><ul class="tiles"><li><a class="tile" href="#cap-api-testing"><b>API and contract testing</b><span>Turn the last run's API calls into tests</span></a></li></ul></div>
|
|
397
397
|
<div class="area"><a class="area-name" href="#area-quality"><span class="mono">Quality</span></a><ul class="tiles"><li><a class="tile" href="#cap-accessibility-performance"><b>Accessibility and page performance</b><span>Is the checkout page accessible?</span></a></li><li><a class="tile" href="#cap-visual-comparison"><b>Visual comparison with baselines</b><span>What changed visually since the last run?</span></a></li><li><a class="tile" href="#cap-defect-intelligence"><b>Defect intelligence: trends and prediction</b><span>Which defects keep coming back?</span></a></li></ul></div>
|
|
398
398
|
<div class="area"><a class="area-name" href="#area-release"><span class="mono">Release</span></a><ul class="tiles"><li><a class="tile" href="#cap-release-readiness"><b>Requirement coverage and release readiness</b><span>Are we ready to release?</span></a></li><li><a class="tile" href="#cap-reports"><b>Reports you can share</b><span>Create a release report for all projects</span></a></li></ul></div>
|
|
399
|
-
<div class="area"><a class="area-name" href="#area-systems"><span class="mono">Systems</span></a><ul class="tiles"><li><a class="tile" href="#cap-systems"><b>Work with Azure DevOps, Jira, GitHub, and Confluence</b><span>Show the open bugs in this sprint</span></a></li><li><a class="tile" href="#cap-system-data"><b>Read any data in Azure DevOps, Jira, Xray, and Confluence</b><span>Are there open pull requests in any repository of QI-Claims?</span></a></li></ul></div>
|
|
399
|
+
<div class="area"><a class="area-name" href="#area-systems"><span class="mono">Systems</span></a><ul class="tiles"><li><a class="tile" href="#cap-systems"><b>Work with Azure DevOps, Jira, GitHub, and Confluence</b><span>Show the open bugs in this sprint</span></a></li><li><a class="tile" href="#cap-system-data"><b>Read any data in Azure DevOps, Jira, Xray, and Confluence, and open Azure DevOps pull requests</b><span>Are there open pull requests in any repository of QI-Claims?</span></a></li></ul></div>
|
|
400
400
|
<h3 class="band-title">Not supported</h3>
|
|
401
401
|
<ul class="plainlist"><li><b>Load, stress, or soak testing with many simultaneous users</b>. Instead: per-page Core Web Vitals with budgets.</li><li><b>Penetration testing or dynamic security scanning</b>. Instead: security findings in pull request reviews.</li></ul>
|
|
402
402
|
</section>
|
|
@@ -916,6 +916,7 @@ forgeqe run "Run the cases in smoke.yaml and don't post results" --pla
|
|
|
916
916
|
<tr><td><code>answer</code></td><td><code>id</code> (from an <code>ask</code>), <code>choice</code> (an option id, or <code>null</code> to cancel)</td><td>Answers the pending question</td></tr>
|
|
917
917
|
<tr><td><code>continue</code></td><td><code>id</code> (from a <code>wait</code>), <code>ok</code> (<code>true</code> = Done, continue; <code>false</code> = Cancel)</td><td>Ends the pending wait</td></tr>
|
|
918
918
|
<tr><td><code>stop</code></td><td></td><td>Stops the running turn (Esc or <code>/stop</code> in the shell). Nothing when idle</td></tr>
|
|
919
|
+
<tr><td><code>settings</code></td><td><code>headed</code> and/or <code>writeTests</code> (each <code>true</code> or <code>false</code>, nothing else)</td><td>Changes the chat's choices from now on: whether browser runs are shown (as <code>--headed</code>), and whether passed runs become tests (as <code>--no-code</code>, off). The next browser run uses them; the agent is told of a change with your next message. Answered with <code>settings</code></td></tr>
|
|
919
920
|
<tr><td><code>close</code></td><td></td><td>Stops any turn, closes the agent, stops monitors and the containers it started, writes <code>closed</code>, exits 0</td></tr>
|
|
920
921
|
</tbody>
|
|
921
922
|
</table></div>
|
|
@@ -926,8 +927,9 @@ forgeqe run "Run the cases in smoke.yaml and don't post results" --pla
|
|
|
926
927
|
<tr><th><code>type</code></th><th>Fields</th><th>When</th></tr>
|
|
927
928
|
</thead>
|
|
928
929
|
<tbody>
|
|
929
|
-
<tr><td><code>ready</code></td><td><code>protocol</code> (1), <code>version</code>, <code>cwd</code>, <code>writeTests</code>, <code>profile</code> (<code>name</code> and <code>provider</code>, never a key)</td><td>Once, after the config is loaded and checked, before stdin is read</td></tr>
|
|
930
|
+
<tr><td><code>ready</code></td><td><code>protocol</code> (1), <code>version</code>, <code>cwd</code>, <code>writeTests</code>, <code>headed</code>, <code>profile</code> (<code>name</code> and <code>provider</code>, never a key)</td><td>Once, after the config is loaded and checked, before stdin is read</td></tr>
|
|
930
931
|
<tr><td><code>turn_start</code></td><td><code>id</code></td><td>A message was accepted</td></tr>
|
|
932
|
+
<tr><td><code>settings</code></td><td><code>headed</code>, <code>writeTests</code></td><td>After a <code>settings</code> line: the choices now in force</td></tr>
|
|
931
933
|
<tr><td><code>agent</code></td><td><code>id</code>, <code>event</code>: a <code>CodeEvent</code> (<code>stream</code>, <code>say</code>, <code>start</code>, <code>tool</code>, <code>plan</code>, <code>usage</code>, <code>context</code>, <code>test</code>, <code>info</code>, …)</td><td>While the turn runs</td></tr>
|
|
932
934
|
<tr><td><code>tool</code></td><td><code>id</code>, <code>event</code>: a <code>ToolEvent</code> (<code>run-start</code>, <code>run-event</code>, <code>run-done</code>, <code>suite-*</code>, <code>card</code>, <code>info</code>, <code>agent</code>, <code>code-*</code>, …)</td><td>While the turn runs; <code>id</code> is <code>null</code> for a background monitor between turns</td></tr>
|
|
933
935
|
<tr><td><code>ask</code></td><td><code>id</code> (<code>ask-N</code>), <code>turn</code>, <code>question</code> (<code>title</code>, <code>detail</code>, <code>lines</code>, <code>options[{id, label}]</code>)</td><td>A tool asks you to choose</td></tr>
|
|
@@ -947,7 +949,7 @@ forgeqe run "Run the cases in smoke.yaml and don't post results" --pla
|
|
|
947
949
|
<li>Exit codes: 0 after <code>closed</code>, 1 after <code>fatal</code>, 2 after SIGINT or SIGTERM (the turn is stopped and the agent closed first).</li>
|
|
948
950
|
</ul>
|
|
949
951
|
<p>A complete exchange (<code>→</code> stdout, <code>←</code> stdin; <code>t</code> left out):</p>
|
|
950
|
-
<pre><code class="language-text">→ {"v":1,"seq":1,"type":"ready","protocol":1,"version":"2.3.11","cwd":"/srv/studio/users/ana/workspace","writeTests":true,"profile":{"name":"copilot","provider":"github_copilot"}}
|
|
952
|
+
<pre><code class="language-text">→ {"v":1,"seq":1,"type":"ready","protocol":1,"version":"2.3.11","cwd":"/srv/studio/users/ana/workspace","writeTests":true,"headed":false,"profile":{"name":"copilot","provider":"github_copilot"}}
|
|
951
953
|
← {"type":"message","id":"m1","text":"Run SHOP-12 on staging"}
|
|
952
954
|
→ {"v":1,"seq":2,"type":"turn_start","id":"m1"}
|
|
953
955
|
→ {"v":1,"seq":3,"type":"agent","id":"m1","event":{"type":"code","kind":"stream","id":"p1","text":"I'll run SHOP-12"}}
|
|
@@ -1791,6 +1793,15 @@ Signing in stays <code>/auth</code> and <code>forgeqe login</code>; <code>forgeq
|
|
|
1791
1793
|
</table></div>
|
|
1792
1794
|
<p><strong>Your copy or ForgeQE's.</strong> <code>.forgeqe-manifest.json</code> in each folder records what was copied. A copy you have not changed still counts as ForgeQE's, so it keeps getting ForgeQE's improvements when you update; once you edit it, yours is used. When ForgeQE's version changes after you edited yours, ForgeQE says so at start-up. Running <code>init</code> again adds what is missing and never overwrites an edited file (with <code>--force</code> it refreshes only unchanged copies).</p>
|
|
1793
1795
|
<p><strong>Prompts.</strong> Each file has a short header (<code>name</code>, <code>description</code>, <code>used_by</code>, <code>kind</code>) and the prompt; <code>{{> name}}</code> includes another prompt (<code>test-rules</code> is included in the test, reviewer, and healing prompts, so one edit changes all three). ForgeQE looks in <code>prompts.dir</code>, then <code>.forgeqe/prompts/</code>, then <code>~/.forgeqe/prompts/</code> (for every project), then its own. Changes take effect on the next run or session.</p>
|
|
1796
|
+
<p><strong>Your team's prompts.</strong> Instructions of your own for chosen agents go in <code>.forgeqe/prompts/team/<name>.md</code> (or <code>~/.forgeqe/prompts/team/</code> for every project; a name in the project wins). The header has <code>name</code>, <code>description</code>, and <code>for</code>: the agents' prompts it joins (<code>brain</code> for ForgeQE's agent, <code>test-agent</code> for Scribe, <code>browser-extra</code> for the browser agent, and so on). Its text is added at the end of those prompts, under "Your team's instructions", as written: it is not a template and includes nothing. The web app's Prompts page adds, edits and removes them, and shows each agent's whole prompt with them included; <code>forgeqe prompts show <prompt></code> shows it in the terminal.</p>
|
|
1797
|
+
<pre><code class="language-markdown">---
|
|
1798
|
+
name: tag-work-items
|
|
1799
|
+
description: "Tests carry the work item they cover"
|
|
1800
|
+
for: test-agent, browser-extra
|
|
1801
|
+
---
|
|
1802
|
+
Tag every test with the Azure DevOps work item it covers.
|
|
1803
|
+
If the request names no work item, ask for it.
|
|
1804
|
+
</code></pre>
|
|
1794
1805
|
<pre><code class="language-bash">forgeqe prompts # every prompt, what it is for, and where it comes from
|
|
1795
1806
|
forgeqe prompts show test-agent # the prompt as the agent gets it (includes expanded; --raw for the file)
|
|
1796
1807
|
forgeqe prompts diff test-rules # how your copy differs from ForgeQE's
|
|
@@ -3889,7 +3900,7 @@ tokens and the like) show as <code>•••</code> in results. A check compares
|
|
|
3889
3900
|
</code></pre>
|
|
3890
3901
|
<p>The Activities panel has one line for each verdict and attempt, and <code>forgeqe</code> commands outside the terminal UI print the same blocks.</p>
|
|
3891
3902
|
<p>Scribe cannot edit ForgeQE's files or run folders, and cannot run <code>git clean</code>, <code>git reset --hard</code>, <code>git stash</code>, <code>git restore</code>, or <code>git checkout -- <path></code>. If it still removes a file that existed before the job, or reverts your uncommitted edits, ForgeQE puts the file back after that attempt and tells the agent.
|
|
3892
|
-
8. The job is done when ForgeQE's run passes with a browser and the
|
|
3903
|
+
8. The job is done when ForgeQE's run passes with a browser, the reviewer approves, and the test passes again unchanged until it has passed <code>code.confirm_runs</code> times in a row (default 3; <code>1</code> runs it once). A test that passes and then fails is flaky: the next attempt is told which run failed and why, and to make that step wait for the page. It stops as a failure after <code>max_attempts</code> attempts, when the job passes <code>code.max_tokens</code>, when the reviewer rejects a passing test twice, if the review cannot finish, when the test fails the same way three times in a row (the same output apart from numbers such as timings and line numbers; the second time, Scribe is told its change did not change the failure and to look at the failing step before changing code), or straight away on an auth, API, or context-limit error from the model.</p>
|
|
3893
3904
|
<p><code>blocked</code> is a last resort. The first time the agent reports that the app blocks it, ForgeQE sends it back to compare its browser with ForgeQE's and fix the difference; only a second blocked report, with what it compared, ends the job as blocked.</p>
|
|
3894
3905
|
<p>The transcript shows the handover and Scribe's work as it goes; files it writes or edits are marked ✎:</p>
|
|
3895
3906
|
<pre><code>┃
|
|
@@ -3967,7 +3978,7 @@ forgeqe run "..." --no-code # no tests
|
|
|
3967
3978
|
</ul>
|
|
3968
3979
|
<p><strong>When the flow cannot be reproduced.</strong> If ForgeQE's own replay of the run could not reach the run's final page and Scribe's test fails too, Scribe is told where the replay stopped and what was open on the page, since that is usually a pop-up, consent banner or ad the browser agent closed, or state its browser already had (signed in, items in a cart), and gets another attempt to handle it. The job stops, saying which replayed step failed and why, only when the test then fails the same way again, instead of spending its remaining attempts on a flow no script can repeat as recorded.</p>
|
|
3969
3980
|
<p><strong>Why a test failed.</strong> When ForgeQE's run of Scribe's test fails, the ✗ line and the next attempt say why in the test's own words: dotnet test's Error Message, pytest's <code>E</code> lines, or the error with its Expected and Received values (Playwright, Jest, JUnit), not the runner's summary line.</p>
|
|
3970
|
-
<p><strong>Keeping Scribe's cost down.</strong> ForgeQE reads the repository's test setup itself before Scribe starts (stack, settings files and their browser values, the test id attribute, base classes, page objects and their methods, one test command) and hands it over, so Scribe does not discover it with dozens of file reads. Each attempt after the first starts a fresh session from a summary of the job (handoff, files changed, what each attempt did, the latest failure), so later attempts cost about what the first did. The summary has a "Tried already, and did not work" list: for each failed attempt, what Scribe said it did (it reports each change in <code>tried</code>), how it failed, the locator the test was waiting for and the element that covered it and took the click, both read from the test's output, and whether an earlier attempt already failed on that locator. Scribe must not try those again in the same form and must say what it does differently. <code>forgeqe heal</code> keeps the same list across its attempts. <code>code.max_attempts</code> (default 3) and <code>code.max_tokens</code> (default 1,500,000; <code>0</code> turns it off) cap a job, and the token budget is checked during an attempt too, so one long attempt is stopped when the total passes it. The reviewer sorts its issues into correctness (the test does not test what the goal says, or not reliably) and coding standards (test data or credentials in a page object, naming, hard waits, leftovers). Coding standards never fail a test that works: it is accepted, and the coding agent gets one more round (not counted in <code>max_attempts</code>) to fix them; if that round breaks the test, ForgeQE puts back the version that passed and lists the standards issues in the notes (<code>code.tidy: false</code> skips the round). A test the reviewer rejects twice for correctness stops the job with its issues, since that usually means the test case and the page disagree. When a test passed and the reviewer sent it back, and the changes for the review then break it (or the attempts or the token budget run out first), ForgeQE puts back the version that passed: the result is passed but not approved, with the reviewer's issues in the notes, so you get a working test to finish instead of none. Under each step, the handoff shows that step's part of the page (its section and neighbours, from the replay's page snapshot) instead of whole snapshots. When a test fails, ForgeQE shows the failing element on the test's page at the failure and on the replay's page, so most failures are fixed without opening a browser. Scribe's live browser is switched on only when the same failure repeats, and it does not send a page snapshot after every click or keystroke. <code>code.max_steps</code> (default 50) caps the tool calls in one of Scribe's turns. <code>code.review_profile</code> runs the reviewer on a cheaper llms.json profile.</p>
|
|
3981
|
+
<p><strong>Keeping Scribe's cost down.</strong> ForgeQE reads the repository's test setup itself before Scribe starts (stack, settings files and their browser values, the test id attribute, base classes, page objects and their methods, one test command) and hands it over, so Scribe does not discover it with dozens of file reads. Each attempt after the first starts a fresh session from a summary of the job (handoff, files changed, what each attempt did, the latest failure), so later attempts cost about what the first did. The summary has a "Tried already, and did not work" list: for each failed attempt, what Scribe said it did (it reports each change in <code>tried</code>), how it failed, the locator the test was waiting for and the element that covered it and took the click, both read from the test's output, and whether an earlier attempt already failed on that locator. Scribe must not try those again in the same form and must say what it does differently. <code>forgeqe heal</code> keeps the same list across its attempts. <code>code.max_attempts</code> (default 3) and <code>code.max_tokens</code> (default 1,500,000; <code>0</code> turns it off) cap a job, and the token budget is checked during an attempt too, so one long attempt is stopped when the total passes it. Cached input counts at its share of the profile's input price (a session re-reads its own context, and that is most of its input), and in full when no cached price is known, such as with another <code>code.model</code>. The reviewer sorts its issues into correctness (the test does not test what the goal says, or not reliably) and coding standards (test data or credentials in a page object, naming, hard waits, leftovers). Coding standards never fail a test that works: it is accepted, and the coding agent gets one more round (not counted in <code>max_attempts</code>) to fix them; if that round breaks the test, ForgeQE puts back the version that passed and lists the standards issues in the notes (<code>code.tidy: false</code> skips the round). A test the reviewer rejects twice for correctness stops the job with its issues, since that usually means the test case and the page disagree. When a test passed and the reviewer sent it back, and the changes for the review then break it (or the attempts or the token budget run out first), ForgeQE puts back the version that passed: the result is passed but not approved, with the reviewer's issues in the notes, so you get a working test to finish instead of none. Under each step, the handoff shows that step's part of the page (its section and neighbours, from the replay's page snapshot) instead of whole snapshots. When a test fails, ForgeQE shows the failing element on the test's page at the failure and on the replay's page, so most failures are fixed without opening a browser. Scribe's live browser is switched on only when the same failure repeats, and it does not send a page snapshot after every click or keystroke. <code>code.max_steps</code> (default 50) caps the tool calls in one of Scribe's turns. <code>code.review_profile</code> runs the reviewer on a cheaper llms.json profile.</p>
|
|
3971
3982
|
<p><strong>Scribe's rulebook: the same mistake once.</strong> When a job passes after a failed test run or a reviewer's change, Scribe's report gives up to three one-line rules that would have avoided the mistake ("Wait for the cart badge with toHaveText before clicking Checkout; it updates after a request"). ForgeQE keeps them in <code>.forgeqe/scribe-rules.json</code> in the repository, and every attempt of later jobs, and every healing session, starts with them. A lesson from a job that did not pass, or that passed at once, is not kept: only a fix that worked becomes a rule. The book stays small, since it is sent with each model call: at most 15 rules of one line each; a lesson learned again is merged into its rule (shown as "learned 3 times") instead of added; when it is full, the rule confirmed least and longest ago goes, and a rule not learned again for 180 days expires. The rules are the coding agent's own words, so a rule with a link, something like a token, a destructive command, or wording that tells an agent to drop its instructions is refused, and the agents are told they are hints that never override their instructions or the team's guardrails. Commit the file to share the rules with the team and see each change in review; edit or delete a rule there (the agents cannot change <code>.forgeqe</code>). ForgeQE says each new rule as it learns it (<code>Learned for next time: …</code>). <code>code.rulebook: false</code> turns it off.</p>
|
|
3972
3983
|
</div>
|
|
3973
3984
|
</section>
|
|
@@ -4620,10 +4631,10 @@ forgeqe bi build --from //share/qa-reports # this machine's runs plus every bund
|
|
|
4620
4631
|
<tr><th>System</th><th>MCP server</th><th>Needs</th></tr>
|
|
4621
4632
|
</thead>
|
|
4622
4633
|
<tbody>
|
|
4623
|
-
<tr><td>Azure DevOps Services</td><td>Microsoft's <a href="https://github.com/microsoft/azure-devops-mcp" rel="noopener">Azure DevOps MCP server</a> (<code>npx @azure-devops/mcp</code>), with all its tools: work items, boards, test plans, pipelines, repos, wiki pages, search, and security alerts</td><td>Node.js, and <code>ADO_PAT</code> or <code>az login</code></td></tr>
|
|
4634
|
+
<tr><td>Azure DevOps Services</td><td>Microsoft's <a href="https://github.com/microsoft/azure-devops-mcp" rel="noopener">Azure DevOps MCP server</a> (<code>npx @azure-devops/mcp</code>), with all its tools, each turn getting the groups it needs: work items, boards, test plans, pipelines, repos, wiki pages, search, and security alerts</td><td>Node.js, and <code>ADO_PAT</code> or <code>az login</code></td></tr>
|
|
4624
4635
|
<tr><td>Jira Cloud, Server/Data Center</td><td><a href="https://github.com/sooperset/mcp-atlassian" rel="noopener">mcp-atlassian</a> (<code>uvx</code>), with issues, JQL search, comments, and transitions. <code>mcp: rovo</code> uses <a href="https://github.com/atlassian/atlassian-mcp-server" rel="noopener">Atlassian's hosted server</a> instead (Cloud, and an admin must allow API tokens)</td><td>uv; <code>JIRA_EMAIL</code> and <code>JIRA_API_TOKEN</code> (Cloud) or a personal access token (Server/DC)</td></tr>
|
|
4625
4636
|
<tr><td>Confluence</td><td>mcp-atlassian, shared with Jira (or its own when Jira isn't set up); Atlassian's hosted server covers it on the same site</td><td><code>confluence: true</code> on a Jira Cloud site, or <code>confluence.url</code> + tokens</td></tr>
|
|
4626
|
-
<tr><td>GitHub</td><td>GitHub's <a href="https://github.com/github/github-mcp-server" rel="noopener">hosted MCP server</a> with
|
|
4637
|
+
<tr><td>GitHub</td><td>GitHub's <a href="https://github.com/github/github-mcp-server" rel="noopener">hosted MCP server</a> with every toolset, each turn getting the groups of tools it needs (<code>sources.github.toolsets</code> limits them), on in a repository with a GitHub remote or when <code>sources.github</code> is set. GitHub issues are also a test case source (<code>github:#12</code>, <code>github:acme/shop#12,#13</code>, <code>github:label=login</code>, <code>github:q=login in:title</code>): results go back as issue comments, and <code>sources.bugs.system: github</code> files bugs as issues. GitHub has no API to upload a screenshot to an issue, so a bug names the run folder instead</td><td><code>GITHUB_TOKEN</code>, or the GitHub CLI's sign-in (<code>forgeqe auth login github</code>)</td></tr>
|
|
4627
4638
|
<tr><td>Xray, TestRail, Zephyr Scale, qTest</td><td>none; ForgeQE's API</td><td><code>XRAY_CLIENT_ID</code> / <code>XRAY_CLIENT_SECRET</code>, <code>TESTRAIL_EMAIL</code> / <code>TESTRAIL_API_KEY</code>, <code>ZEPHYR_API_TOKEN</code>, <code>QTEST_TOKEN</code></td></tr>
|
|
4628
4639
|
<tr><td>Anything else</td><td>servers you add under <code>mcp:</code></td><td></td></tr>
|
|
4629
4640
|
</tbody>
|
|
@@ -4640,6 +4651,7 @@ mcp:
|
|
|
4640
4651
|
<p>Whatever has no MCP server, or whose server didn't connect, goes through ForgeQE's own tools, which call the REST APIs directly: <code>find_test_cases</code> and <code>run_test_cases</code> for test cases and posting results back, <code>read_confluence</code> for Confluence pages, and <code>query_api</code> to read anything else from Azure DevOps, Jira, Xray, or Confluence. <code>query_api</code> only reads (GET requests and Xray GraphQL queries) and only reaches the configured hosts. <code>sources.azure_devops.mcp: false</code> or <code>sources.jira.mcp: false</code> uses the API only.</p>
|
|
4641
4652
|
<p>Microsoft's Azure DevOps server can't create dashboards, so "create a dashboard in QI-Payments with all defects and this sprint's stories" goes through ForgeQE's <code>create_ado_dashboard</code>: one shared query per widget under <code>Shared Queries/ForgeQE</code>, then a dashboard with a work item list or count tile for each. Your <code>ADO_PAT</code> needs Work Items (read and write) and permission to edit dashboards. Chart widgets aren't made through the API; for those the agent uses the browser, signed in to <code>dev.azure.com</code> with a saved login.</p>
|
|
4642
4653
|
<p>Its wiki tools can only write into a wiki that already exists, so wiki pages go through ForgeQE's <code>write_ado_wiki_page</code>. If the project has no wiki yet, that tool creates the project wiki first, along with any missing parent pages. The PAT needs Wiki (read and write).</p>
|
|
4654
|
+
<p>Pull requests are opened with ForgeQE's <code>create_ado_pull_request</code> ("create a PR from feature/cart to main in the shop repository"), since the <code>repositories</code> tool group is not loaded by default. It checks that both branches exist, and returns an active pull request between the same branches instead of opening a second one. The PAT needs Code (read and write).</p>
|
|
4643
4655
|
<p>Requirements work too: "write and run test cases for the checkout rules page in Confluence" makes the agent read the page, write one case per rule or acceptance criterion (with the main negative paths), show them, and run them. It saves them into a test system only when you ask.</p>
|
|
4644
4656
|
<p>The agent creates or changes items (a bug, a comment, a status change) only when you ask it to; bugs for failed cases come from <code>sources.bugs</code>, not from the agent.</p>
|
|
4645
4657
|
</div>
|
|
@@ -4647,12 +4659,12 @@ mcp:
|
|
|
4647
4659
|
</div>
|
|
4648
4660
|
</article>
|
|
4649
4661
|
<article class="spec feat" id="cap-system-data">
|
|
4650
|
-
<div class="hd"><span class="mono">Systems</span><h3>Read any data in Azure DevOps, Jira, Xray, and Confluence</h3></div>
|
|
4651
|
-
<p class="ds">Reads through a system's REST API what its MCP tools do not cover: pull requests and repositories, test plans and suites, classic releases, and anything else in Azure DevOps, Jira, Xray, or Confluence. Given on every turn once a system is set up, so the agent never turns to the browser or the shell
|
|
4662
|
+
<div class="hd"><span class="mono">Systems</span><h3>Read any data in Azure DevOps, Jira, Xray, and Confluence, and open Azure DevOps pull requests</h3></div>
|
|
4663
|
+
<p class="ds">Reads through a system's REST API what its MCP tools do not cover: pull requests and repositories, test plans and suites, classic releases, and anything else in Azure DevOps, Jira, Xray, or Confluence. Opens Azure DevOps pull requests, which the API reads cannot. Given on every turn once a system is set up, so the agent never turns to the browser or the shell for them.</p>
|
|
4652
4664
|
<div class="facts">
|
|
4653
|
-
<div class="fact"><span class="mono">Ask it</span><ul class="asks"><li>Are there open pull requests in any repository of QI-Claims?</li><li>List the test plans in QI-MobileBanking with their suites</li></ul></div>
|
|
4665
|
+
<div class="fact"><span class="mono">Ask it</span><ul class="asks"><li>Are there open pull requests in any repository of QI-Claims?</li><li>List the test plans in QI-MobileBanking with their suites</li><li>Create a pull request from demo_mai_qiclaims to main in the QI-Claims Typescript repository</li></ul></div>
|
|
4654
4666
|
<div class="fact"><span class="mono">What it needs</span><ul class="needs"><li><span class="need req">Required</span><span>Azure DevOps or Jira or Xray or Confluence: set up under sources: in forgeqe.yaml, or sign in with /auth</span></li></ul></div>
|
|
4655
|
-
<div class="fact"><span class="mono">Tools</span><p class="tools"><code>query_api</code></p></div>
|
|
4667
|
+
<div class="fact"><span class="mono">Tools</span><p class="tools"><code>query_api</code> <code>create_ado_pull_request</code></p></div>
|
|
4656
4668
|
</div>
|
|
4657
4669
|
<div class="detail"><span class="mono">In detail</span>
|
|
4658
4670
|
<p class="see">See <a href="#doc-readme-azure-devops-jira-and-other-systems">Azure DevOps, Jira, and other systems</a> (from the README).</p>
|
|
@@ -4665,14 +4677,21 @@ mcp:
|
|
|
4665
4677
|
<h3>MCP</h3>
|
|
4666
4678
|
<div class="prose"></div>
|
|
4667
4679
|
<section class="doc sub" id="doc-how_to_use-servers-forgeqes-agent-connects-to"><h4>Servers ForgeQE's agent connects to</h4><div class="prose"><p>When ForgeQE's agent starts, it connects to an MCP server for each system set up under <code>sources:</code>. The server settings come from the same place as everything else: the organization and project, the site URL, and the names of the environment variables that hold the tokens. You don't write MCP config for these.</p>
|
|
4668
|
-
<p><strong>
|
|
4680
|
+
<p><strong>Every tool, without paying for every tool.</strong> Each server is started with everything it has: Azure DevOps with all its tool groups, Jira and Confluence with <code>TOOLSETS=all</code>, GitHub with every toolset. Every tool given to the model is sent with each of its requests (GitHub's are about 79K tokens, Jira's and Confluence's about 29K, Azure DevOps' about 21K), so a turn gets only the groups of tools its request needs, the way it gets ForgeQE's own capabilities:</p>
|
|
4681
|
+
<ul>
|
|
4682
|
+
<li>ForgeQE asks each server for its tools when the session starts and keeps the list for a week (<code>~/.forgeqe/cache/mcp-tools</code>), then groups them by their names. No server needs a table of its own: tools named by prefix (<code>repo_…</code>, <code>wit_…</code>, <code>jira_…</code>) are grouped by prefix, and a large prefix again by what its tools are about (Jira's sprints, boards, versions); tools named verb first (<code>list_pull_requests</code>) by what they act on (pull requests, issues, Dependabot alerts). A group is named <code><server>:<group></code>, such as <code>github:pull-request</code> or <code>atlassian:jira-sprint</code>.</li>
|
|
4683
|
+
<li>A turn gets a group when its request names what the group is about ("create a PR", "the current sprint", "dependabot alerts"), when its tools ran in the last turns, or when the agent asks for it with <code>use_capabilities</code>: every group is in the list of capabilities the agent has, so none is out of reach and the browser is never the way round a missing tool. A request that names a system ("the GitHub repository", "in Azure DevOps") gets that system's groups from its words, not another's.</li>
|
|
4684
|
+
<li>A small server (under about 3K tokens) is one group, on every turn. A server ForgeQE could not ask keeps all its tools on every turn, as before.</li>
|
|
4685
|
+
<li><code>agent.capabilities.always</code> and <code>off</code> take group ids too: <code>always: [github:issue]</code>, <code>off: [github:copilot]</code>.</li>
|
|
4686
|
+
</ul>
|
|
4687
|
+
<p><strong>Azure DevOps.</strong> Microsoft's <a href="https://github.com/microsoft/azure-devops-mcp" rel="noopener">Azure DevOps MCP server</a>, run with every tool group it has: projects and teams (<code>azure-devops:core</code>), iterations and boards (<code>azure-devops:work</code>), work items (<code>azure-devops:wit</code>), pipelines (<code>azure-devops:pipeline</code>), repositories and pull requests (<code>azure-devops:repo</code>), wiki (<code>azure-devops:wiki</code>), test plans (<code>azure-devops:testplan</code>), search (<code>azure-devops:search</code>), and security alerts (<code>azure-devops:advsec</code>). The first four are on every turn (about 21 tools, roughly 10.7K tokens); the others come as described above. ForgeQE's own tools cover dashboards, wiki pages in a project without a wiki, and opening pull requests (<code>create_ado_pull_request</code>) and reviewing them, and <code>query_api</code> reads anything else, such as classic releases. The organization comes from <code>org_url</code> (<code>https://dev.azure.com/acme</code> or <code>https://acme.visualstudio.com</code>). ForgeQE passes the PAT from <code>token_env</code> (default <code>ADO_PAT</code>) the way the server wants it; without a PAT it uses your <code>az login</code> session. Its tools are <code>azure_devops_*</code>. <code>mcp_domains</code> says which groups are on every turn (default <code>[core, work, work-items, pipelines]</code>; <code>[all]</code> for every group, at about 21K tokens a request); <code>agent.capabilities.off</code> switches a group off for good (<code>[azure-devops:advsec]</code>). It needs Node.js. Azure DevOps Server (on-premises) isn't supported by Microsoft's server, so it uses ForgeQE's API.</p>
|
|
4669
4688
|
<pre><code class="language-yaml">sources:
|
|
4670
4689
|
azure_devops:
|
|
4671
4690
|
org_url: https://dev.azure.com/acme # the MCP server takes the organization, not a project
|
|
4672
4691
|
# project: Shop # optional: limit ForgeQE's own API calls to one project
|
|
4673
4692
|
token_env: ADO_PAT
|
|
4674
4693
|
mcp: true # false: API only
|
|
4675
|
-
mcp_domains: [core, work, work-items, pipelines, repositories] # optional: the
|
|
4694
|
+
mcp_domains: [core, work, work-items, pipelines, repositories] # optional: groups on every turn; the others come when needed
|
|
4676
4695
|
</code></pre>
|
|
4677
4696
|
<p>Microsoft's server has no dashboard tools and can't save queries. Ask for a dashboard ("create a dashboard in QI-Payments with all defects in every state and the current sprint's stories") and ForgeQE's own <code>create_ado_dashboard</code> tool does it through the REST API with <code>ADO_PAT</code>: it runs each WIQL query once to check it, saves it as a shared query under <code>Shared Queries/ForgeQE</code> (updating one of the same name), and creates a team or project dashboard with a work item list or count tile per query, plus optional markdown text. The PAT needs Work Items (read and write) and permission to edit dashboards. Chart widgets are not made through the API; for those the agent uses the browser, which has to be signed in to <code>dev.azure.com</code> (<code>forgeqe login <name></code> with a login for it under <code>logins:</code>).</p>
|
|
4678
4697
|
<p>Microsoft's wiki tools (<code>azure_devops_wiki</code>, <code>azure_devops_wiki_upsert_page</code>) only work in a wiki that already exists. In a project without one they answer "No wiki found". Ask for a wiki page and ForgeQE's <code>write_ado_wiki_page</code> tool writes it through the REST API instead. If the project has no wiki, the tool creates the project wiki (<code><Project>.wiki</code>) first, then any missing parent pages (empty), then the page itself. A page that already exists is replaced. Code wikis are written on their published branch. The PAT needs Wiki (read and write). A login added to <code>forgeqe.yaml</code> while ForgeQE is running is picked up the next time the agent lists logins or signs in.</p>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
id: browser-test-intent
|
|
2
|
+
title: A flow the user asks to test is a test, and its goal asks only for what the page can show
|
|
3
|
+
# The Booking.com log: sent as a task (so no test was written), with a per-night price the result cards never show,
|
|
4
|
+
# and a guest count the user never gave.
|
|
5
|
+
input: |
|
|
6
|
+
Role: A browser testing agent who searches travel sites.
|
|
7
|
+
|
|
8
|
+
Objective: Search Booking.com for hotels near Heathrow Airport, with check-in tomorrow, check-out five days later, and daily cost under £100.
|
|
9
|
+
|
|
10
|
+
Context:
|
|
11
|
+
- Site: Booking.com.
|
|
12
|
+
- Location: Heathrow Airport.
|
|
13
|
+
- Dates: check-in tomorrow; check-out five days later.
|
|
14
|
+
- Price limit: less than £100 per day.
|
|
15
|
+
|
|
16
|
+
Output format: A short list of matching hotels, with the hotel name, daily price, total price, distance note, and booking link.
|
|
17
|
+
needs: []
|
|
18
|
+
# browser_run is not allowed: the call is refused, and still observed with its arguments (no browser starts).
|
|
19
|
+
allow: [capabilities, tool_help]
|
|
20
|
+
checks:
|
|
21
|
+
- id: runs-as-test
|
|
22
|
+
tools_any: ["forgeqe_browser_run:test"]
|
|
23
|
+
- id: never-a-task
|
|
24
|
+
tools_none: ["forgeqe_browser_run:task"]
|
|
25
|
+
- id: gives-the-total-too
|
|
26
|
+
field: browser_goals
|
|
27
|
+
matches: "500"
|
|
28
|
+
- id: adds-no-guest-count
|
|
29
|
+
field: browser_goals
|
|
30
|
+
not_matches: "\\b(1|one) adult"
|
package/forgeqe.example.yaml
CHANGED
|
@@ -180,9 +180,10 @@ code:
|
|
|
180
180
|
# test_command: npx playwright test # the repo's test command, for forgeqe heal; default: the agent finds it
|
|
181
181
|
max_attempts: 3 # test runs before giving up (also healing attempts); each attempt after the first starts a fresh session from a summary
|
|
182
182
|
# rulebook: false # Scribe keeps rules learned from its own fixed mistakes in .forgeqe/scribe-rules.json (default on)
|
|
183
|
-
# max_tokens: 1500000 # stop a coding job past this many tokens
|
|
183
|
+
# max_tokens: 1500000 # stop a coding job past this many tokens and report how far it got; cached input counts at its share of the input price
|
|
184
184
|
# review_profile: azure-mini # llms.json profile for the reviewer, which only reads and judges: a cheaper model is enough
|
|
185
185
|
test_timeout_seconds: 600 # per test run
|
|
186
|
+
confirm_runs: 3 # passes in a row a new test needs before it counts as passing (catches a flaky test before it is handed over); 1 runs it once
|
|
186
187
|
stall_after: 5 # stop the coding agent when one command it runs has failed this many times
|
|
187
188
|
replay: true # before the coding agent starts, run the steps as a Playwright script and hand over what worked and where it failed
|
|
188
189
|
live_browser: true # once a step fails, the coding agent looks at that page in a Playwright browser instead of building tests to look
|
|
@@ -260,7 +261,7 @@ sources:
|
|
|
260
261
|
# # project: Shop # optional: limit ForgeQE's own test, sprint, and bug calls to one project. Or ADO_PROJECT. * or all means every project.
|
|
261
262
|
# token_env: ADO_PAT # PAT with Test Management + Work Items (read & write)
|
|
262
263
|
# mcp: true # Microsoft's Azure DevOps MCP server (needs npx); false: API only
|
|
263
|
-
# # mcp_domains: [core, work, work-items, pipelines] # the default;
|
|
264
|
+
# # mcp_domains: [core, work, work-items, pipelines] # groups on every turn (the default); repositories, wiki, test-plans, search and advanced-security come when a request needs them. [all]: every group on every turn
|
|
264
265
|
# jira:
|
|
265
266
|
# url: https://acme.atlassian.net # or JIRA_URL; Server/Data Center URLs work too
|
|
266
267
|
# email_env: JIRA_EMAIL # Cloud only
|
|
@@ -292,7 +293,7 @@ sources:
|
|
|
292
293
|
# github: # GitHub issues as test cases (github:#12, github:label=login) and bugs;
|
|
293
294
|
# repo: acme/shop # also turns on GitHub's MCP server (on anyway in a repo with a GitHub remote)
|
|
294
295
|
# token_env: GITHUB_TOKEN # default; without it, the GitHub CLI's sign-in (forgeqe auth login github)
|
|
295
|
-
# toolsets: [repos, issues, pull_requests, actions] # the MCP server's
|
|
296
|
+
# toolsets: [repos, issues, pull_requests, actions] # limit the MCP server's toolsets (default: all, each turn getting the groups it needs); mcp: false turns it off
|
|
296
297
|
# bugs: # a bug per defect; still failing: a comment on the open bug
|
|
297
298
|
# system: jira # jira | azure_devops | github
|
|
298
299
|
# project: SHOP # Jira project key; GitHub owner/repo (Azure DevOps uses azure_devops.project)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capagents/forgeqe",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.2",
|
|
4
4
|
"description": "ForgeQE: a browser agent CLI that runs goals and test cases in local Chrome or Chromium, or BrowserStack and TestMu AI/LambdaTest when configured, and turns passing runs into tests in your repo",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"repository": {
|
package/prompts/brain.md
CHANGED
|
@@ -26,7 +26,7 @@ A message can also be a task specification with the parts Role, Objective, Conte
|
|
|
26
26
|
**Tools** (ForgeQE tools; the client adds the forgeqe_ prefix):
|
|
27
27
|
- Each ForgeQE tool comes with its own description. Read it to select the tool.
|
|
28
28
|
- Some playbooks are not in this prompt. The index under "Playbooks" names them. The first result of their tools gives each one, and you MUST obey it.
|
|
29
|
-
- browser_run: set purpose to test for a flow of the application under test. Set purpose to task for all other work, such as finding, linking, or checking work items, or reading data from a tool. A task never becomes an automated test.
|
|
29
|
+
- browser_run: set purpose to test for a flow of the application under test, and for any flow on a website that the user wants tested or automated: the message says test, verify, automate, or script, or its role is a testing agent. That is so also when it asks for the data the flow finds, such as a list of results. Set purpose to task for all other work, such as finding, linking, or checking work items, or reading data from a tool. A task never becomes an automated test.
|
|
30
30
|
- attach_to_work_item: the only way to put a file from a run on a work item or an issue. The Azure DevOps MCP server cannot upload files.
|
|
31
31
|
- A test passes only when run_test, write_test, or heal_tests says so.
|
|
32
32
|
- query_api only reads. set_ci_secret takes the name of a variable, never its value.
|
|
@@ -112,6 +112,7 @@ Read data from Azure DevOps, Jira, Xray, Confluence, and other systems in this s
|
|
|
112
112
|
- The browser MUST be signed in to dev.azure.com. When no saved login covers it, use logins with sign_in first, and add the login to forgeqe.yaml when it is missing.
|
|
113
113
|
- For a wiki page, you MUST use write_ado_wiki_page, not the browser. It works also when the project has no wiki.
|
|
114
114
|
- The MCP wiki tools answer "No wiki found" for a project without a wiki. This answer is not a reason to use the browser.
|
|
115
|
+
- For a pull request in Azure Repos, you MUST use create_ado_pull_request, not the browser. query_api is read-only for Azure DevOps; that is not a reason to use the browser.
|
|
115
116
|
|
|
116
117
|
### Test cases
|
|
117
118
|
- To run test cases from these systems, files, or text, you MUST use run_test_cases. It turns each case into a browser goal, writes the reports, and sends the results back.
|
|
@@ -179,6 +180,8 @@ For "an end-to-end test for story 1234", "combine these cases into one flow", or
|
|
|
179
180
|
### Browser runs
|
|
180
181
|
- A browser_run goal is the complete instruction that the browser agent follows. It MUST give each value and each setting that the user named (dates, places, filters, sort order), and what to check.
|
|
181
182
|
- The goal MUST NOT have your questions, alternatives, or reasoning. You MUST remove all doubt before you call it.
|
|
183
|
+
- The goal MUST NOT add values that the user did not give, such as a number of guests or rooms. Leave the site's default.
|
|
184
|
+
- The goal MUST ask only for what the page can show. When the user gives a limit per unit (per night, per item, per month) and the site can show the total for the whole stay or order, give both in the goal: the limit per unit and the total that it means (5 nights under £100 a night is a total under £500). The agent then uses the one that the page shows.
|
|
182
185
|
- A run with outcome assertion_failed did all the steps and found an expected result that is not true. failed_expectations gives what was expected and what the page shows.
|
|
183
186
|
- That is a correct result: a failed test case. You MUST report it as a failed test, with the expected and the actual result.
|
|
184
187
|
- You MUST NOT run it again, look for the value in other places, or change the expectation. Run again only the runs that could not do their steps.
|
|
@@ -55,6 +55,65 @@ def _norm(text: Any) -> str:
|
|
|
55
55
|
return " ".join(str(text or "").split()).lower()
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
_MONTHS = {
|
|
59
|
+
m: i
|
|
60
|
+
for i, names in enumerate(
|
|
61
|
+
(("jan", "january"), ("feb", "february"), ("mar", "march"), ("apr", "april"), ("may",), ("jun", "june"), ("jul", "july"),
|
|
62
|
+
("aug", "august"), ("sep", "sept", "september"), ("oct", "october"), ("nov", "november"), ("dec", "december")),
|
|
63
|
+
1,
|
|
64
|
+
)
|
|
65
|
+
for m in names
|
|
66
|
+
}
|
|
67
|
+
_MONTH = r"(jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|june?|july?|aug(?:ust)?|sept?(?:ember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)"
|
|
68
|
+
_ISO_DATE = re.compile(r"\b(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})\b")
|
|
69
|
+
_NUM_DATE = re.compile(r"\b(\d{1,2})[/.](\d{1,2})[/.](\d{2,4})\b")
|
|
70
|
+
_DAY_MONTH = re.compile(rf"\b(\d{{1,2}})(?:st|nd|rd|th)?\s+(?:of\s+)?{_MONTH}\b")
|
|
71
|
+
_MONTH_DAY = re.compile(rf"\b{_MONTH}\.?\s+(\d{{1,2}})(?:st|nd|rd|th)?\b")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def dates(text: Any) -> Set[Tuple[int, int]]:
|
|
75
|
+
"""
|
|
76
|
+
The calendar days a text names, as (month, day): "2026-10-29", "Thursday, 29 October 2026", "Oct 29", "29/10/2026"
|
|
77
|
+
(read both ways). Years are left out: a calendar's label and a task can write them differently, or not at all.
|
|
78
|
+
"""
|
|
79
|
+
s = str(text or "").lower()
|
|
80
|
+
out: Set[Tuple[int, int]] = set()
|
|
81
|
+
|
|
82
|
+
def add(month: int, day: int) -> None:
|
|
83
|
+
if 1 <= month <= 12 and 1 <= day <= 31:
|
|
84
|
+
out.add((month, day))
|
|
85
|
+
|
|
86
|
+
for _, m, d in _ISO_DATE.findall(s):
|
|
87
|
+
add(int(m), int(d))
|
|
88
|
+
for a, b, _ in _NUM_DATE.findall(s):
|
|
89
|
+
add(int(b), int(a))
|
|
90
|
+
add(int(a), int(b))
|
|
91
|
+
for d, m in _DAY_MONTH.findall(s):
|
|
92
|
+
add(_MONTHS[m], int(d))
|
|
93
|
+
for m, d in _MONTH_DAY.findall(s):
|
|
94
|
+
add(_MONTHS[m], int(d))
|
|
95
|
+
return out
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _superseded(steps: Sequence[Dict[str, Any]], describe: Describe) -> Set[int]:
|
|
99
|
+
"""
|
|
100
|
+
Indexes of steps that type into a control the same flow types into again later with another value: the recording
|
|
101
|
+
changed its mind there ("London", later "Heathrow Airport"), so the first value was never the task's.
|
|
102
|
+
"""
|
|
103
|
+
last: Dict[str, str] = {}
|
|
104
|
+
out: Set[int] = set()
|
|
105
|
+
infos = [describe(s) for s in steps]
|
|
106
|
+
for i in range(len(infos) - 1, -1, -1):
|
|
107
|
+
info = infos[i]
|
|
108
|
+
if info.get("kind") != "input" or info.get("secret"):
|
|
109
|
+
continue
|
|
110
|
+
field_key, value = _norm(info.get("label")), _norm(info.get("value"))
|
|
111
|
+
if field_key in last and last[field_key] != value:
|
|
112
|
+
out.add(i)
|
|
113
|
+
last.setdefault(field_key, value)
|
|
114
|
+
return out
|
|
115
|
+
|
|
116
|
+
|
|
58
117
|
def step_key(info: Dict[str, Any]) -> str:
|
|
59
118
|
"""The same step across recorded tasks: what it does, on which control, with which value."""
|
|
60
119
|
return "|".join((str(info.get("kind") or ""), _norm(info.get("label")), _norm(info.get("value"))))
|
|
@@ -76,10 +135,16 @@ def borrowable(
|
|
|
76
135
|
A step other tasks also took is borrowed only while the flow is still in its opening (before the first step this
|
|
77
136
|
task names), and only when it types no value: a banner or a menu is common, the data a task enters is its own.
|
|
78
137
|
A step marked `never` (a page script, an upload, an API call) ends what is borrowed.
|
|
138
|
+
|
|
139
|
+
A step that names a date (a calendar day, a typed date) is borrowed only when the task names that date too, and a
|
|
140
|
+
value the flow later typed over in the same control is not borrowed: both are the other task's data.
|
|
79
141
|
"""
|
|
80
142
|
goal_words = words(goal)
|
|
143
|
+
goal_named = {w for w in goal_words if not w.isdigit()}
|
|
144
|
+
goal_dates = dates(goal)
|
|
81
145
|
goal_text = _norm(goal)
|
|
82
146
|
common = common or Counter()
|
|
147
|
+
superseded = _superseded(steps, describe)
|
|
83
148
|
taken = 0
|
|
84
149
|
useful = 0
|
|
85
150
|
opening = True
|
|
@@ -89,6 +154,11 @@ def borrowable(
|
|
|
89
154
|
kind = str(info.get("kind") or "")
|
|
90
155
|
if info.get("never"):
|
|
91
156
|
break
|
|
157
|
+
if n - 1 in superseded:
|
|
158
|
+
break
|
|
159
|
+
step_dates = dates(info.get("label")) | dates(info.get("value"))
|
|
160
|
+
if step_dates and not step_dates <= goal_dates:
|
|
161
|
+
break
|
|
92
162
|
if info.get("neutral"):
|
|
93
163
|
taken = n if useful else taken
|
|
94
164
|
continue
|
|
@@ -106,7 +176,8 @@ def borrowable(
|
|
|
106
176
|
signing_in = bool(info.get("secret"))
|
|
107
177
|
if not ok:
|
|
108
178
|
named_value = bool(value_words) and value_words <= goal_words
|
|
109
|
-
|
|
179
|
+
# A number alone (a year, a day) does not name a control: "Monday, 19 October 2026" is not named by "2026-10-29".
|
|
180
|
+
named_label = bool(label_words & goal_named) or bool(dates(info.get("label")))
|
|
110
181
|
ok = (named_value and (named_label or not label_words or kind != "input")) if value_words else named_label
|
|
111
182
|
if ok:
|
|
112
183
|
opening = False
|
|
@@ -111,6 +111,7 @@ def totp_code(secret: str) -> str:
|
|
|
111
111
|
|
|
112
112
|
def prompt_block(logins: List[Login]) -> Optional[str]:
|
|
113
113
|
lines = []
|
|
114
|
+
sessions = []
|
|
114
115
|
for login in logins:
|
|
115
116
|
parts = [
|
|
116
117
|
f"{label} <secret>{login.placeholder(field)}</secret>"
|
|
@@ -119,17 +120,28 @@ def prompt_block(logins: List[Login]) -> Optional[str]:
|
|
|
119
120
|
]
|
|
120
121
|
if parts:
|
|
121
122
|
lines.append(f"- {login.name} on {', '.join(login.domains)}: {', '.join(parts)}")
|
|
122
|
-
|
|
123
|
+
elif login.session_file:
|
|
124
|
+
# A login with only a saved session (SSO, captcha): the browser starts signed in, and there is nothing to type.
|
|
125
|
+
# Without this line the agent was told "sign in with the saved login X" and typed X as the email address.
|
|
126
|
+
sessions.append(f"- {login.name} on {', '.join(login.domains)}")
|
|
127
|
+
if not lines and not sessions:
|
|
123
128
|
return None
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
129
|
+
out = ["## Saved logins"]
|
|
130
|
+
if lines:
|
|
131
|
+
out += [
|
|
127
132
|
"Use these only when the site asks you to sign in (a login form, or a page asking for an authenticator / 2FA / one-time code) and the task did not give other credentials. "
|
|
128
133
|
"Type the placeholder exactly as written, including the <secret> tags; the real value is filled in for you. "
|
|
129
134
|
"The one-time code placeholder produces a fresh code each time you type it. If the page already shows you signed in, carry on without signing in again.",
|
|
130
135
|
*lines,
|
|
131
136
|
]
|
|
132
|
-
|
|
137
|
+
if sessions:
|
|
138
|
+
out += [
|
|
139
|
+
"These logins are a saved browser session only: there is no username or password for them. "
|
|
140
|
+
"If one of these sites shows a sign-in page, do not type the login's name, an email address, or anything else into it: "
|
|
141
|
+
"stop and report that the site asked to sign in and the saved session for that login did not cover it, so it must be saved again with forgeqe login <name>.",
|
|
142
|
+
*sessions,
|
|
143
|
+
]
|
|
144
|
+
return "\n".join(out)
|
|
133
145
|
|
|
134
146
|
|
|
135
147
|
def _host_matches(host: str, login_hosts: List[str]) -> bool:
|
|
@@ -72,6 +72,17 @@ def resume_steps(steps: List[Dict[str, Any]], step_of: List[int], failed_at: int
|
|
|
72
72
|
_LOOP_PRONE_ACTION_KEYS = frozenset({"input", "click", "search", "type", "select"})
|
|
73
73
|
|
|
74
74
|
|
|
75
|
+
# Actions that only read the page, and the parameter that says what they read. The same read again finds the same
|
|
76
|
+
# thing: the Booking.com log searched one page for the same price pattern six times in a row until the run timed out.
|
|
77
|
+
_READ_ACTION_PARAMS = {
|
|
78
|
+
"search_page": "pattern",
|
|
79
|
+
"find_text": "text",
|
|
80
|
+
"find_elements": "selector",
|
|
81
|
+
"extract": "query",
|
|
82
|
+
"evaluate": "code",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
75
86
|
_CONTROL_LOOP_GOAL_KEYWORDS = ("search", "select", "outlet", "autocomplete", "combobox", "filter", "lookup", "pick", "choose")
|
|
76
87
|
|
|
77
88
|
|
|
@@ -94,10 +105,13 @@ def _normalize_snippet(s: str) -> str:
|
|
|
94
105
|
|
|
95
106
|
|
|
96
107
|
def _action_fingerprint(action_name: str, params: Any) -> Optional[str]:
|
|
97
|
-
if action_name not in _LOOP_PRONE_ACTION_KEYS:
|
|
98
|
-
return None
|
|
99
108
|
if not isinstance(params, dict):
|
|
100
109
|
params = {}
|
|
110
|
+
if action_name in _READ_ACTION_PARAMS:
|
|
111
|
+
what = _normalize_snippet(str(params.get(_READ_ACTION_PARAMS[action_name]) or ""))
|
|
112
|
+
return f"read|{action_name}|{what[:200]}"
|
|
113
|
+
if action_name not in _LOOP_PRONE_ACTION_KEYS:
|
|
114
|
+
return None
|
|
101
115
|
if action_name in ("input", "type"):
|
|
102
116
|
text = str(params.get("text", "")).strip().lower()
|
|
103
117
|
return f"input|{params.get('index')}|{text}"
|
|
@@ -148,8 +162,15 @@ def _loop_start(collected_steps: List[Dict[str, Any]], fingerprint: str, window:
|
|
|
148
162
|
return first, line
|
|
149
163
|
|
|
150
164
|
|
|
151
|
-
def _loop_nudge_message(*, line: str, count: int, window: int) -> str:
|
|
165
|
+
def _loop_nudge_message(*, line: str, count: int, window: int, fingerprint: str = "") -> str:
|
|
152
166
|
what = f"`{line}`" if line else "the same action"
|
|
167
|
+
if fingerprint.startswith("read|"):
|
|
168
|
+
return (
|
|
169
|
+
f"ForgeQE: you have done {what} {count} times in the last {window} steps. Reading the page the same way again "
|
|
170
|
+
"finds the same thing. Use what the earlier reads found: if the page does not show what you are looking for, "
|
|
171
|
+
"it may show it another way (a total instead of a per-night price, a range instead of a value), so work from "
|
|
172
|
+
"what is there, or read another part of the page. If you have what the task asks for, call done now."
|
|
173
|
+
)
|
|
153
174
|
return (
|
|
154
175
|
f"ForgeQE: you have done {what} {count} times in the last {window} steps and the page is still not where you want it. "
|
|
155
176
|
"Doing it again will not help. Before acting, look at the current page and work out why the earlier attempts did not stick: "
|
|
@@ -185,6 +206,8 @@ def _control_loop_break_message(*, count: int, max_retries: int, fingerprint: Op
|
|
|
185
206
|
detail = f" (repeated input '{parts[2]}')" if len(parts) >= 3 and parts[2] else " (repeated input on the same field)"
|
|
186
207
|
elif fingerprint.startswith("search|"):
|
|
187
208
|
detail = " (repeated search)"
|
|
209
|
+
elif fingerprint.startswith("read|"):
|
|
210
|
+
detail = " (repeated the same read of the page)"
|
|
188
211
|
elif fingerprint.startswith("select|"):
|
|
189
212
|
detail = " (repeated select)"
|
|
190
213
|
elif fingerprint.startswith("goal|"):
|