@capagents/forgeqe 1.1.7 → 1.1.9
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 +32 -7
- package/docs/user-guide.html +32 -13
- package/package.json +1 -1
- package/prompts/brain.md +4 -2
- package/src/brain/index.ts +4 -0
- package/src/capabilities/registry.ts +6 -2
- package/src/cases/api.ts +75 -22
- package/src/code/exchange.ts +53 -0
- package/src/code/loop.ts +15 -3
- package/src/code/runtime.ts +3 -0
- package/src/detail.ts +2 -1
- package/src/mcp/reporttools.ts +205 -14
- package/src/mcp/server.ts +8 -3
- package/src/mcp/tools.ts +4 -1
- package/src/print.ts +9 -0
- package/src/report/evidence.ts +313 -0
- package/src/report/html.ts +7 -3
- package/src/report/template.html +46 -45
- package/src/tui/App.tsx +23 -6
- package/src/tui/components.tsx +4 -3
- package/src/tui/text.ts +16 -0
- package/src/tui/theme.ts +25 -26
- package/src/tui/transcript.tsx +22 -9
- package/src/types.ts +6 -0
package/HOW_TO_USE.md
CHANGED
|
@@ -67,7 +67,7 @@ Everything ForgeQE can do, by area. ForgeQE's agent answers "can you…?" questi
|
|
|
67
67
|
| Release | **Requirement coverage and release readiness**: Gives requirement coverage and a GO / GO WITH RISKS / NO-GO verdict for a sprint, area, epic, or stories, from test cases, results, bugs, and flaky tests; can publish it to the wiki. | `release_readiness` | [docs](#requirement-coverage-and-release-readiness) |
|
|
68
68
|
| Operations | **Scheduled monitoring with alerts**: Runs checks on a schedule and alerts Teams or email when one keeps failing, and again when it recovers. | `monitor` | [docs](#9-monitoring) |
|
|
69
69
|
| Operations | **Teams and email notifications**: Tells the team in Microsoft Teams channels or by email when a suite fails or passes, when a release readiness verdict is in, and when the domain evaluation falls below its gate, with the failing cases, their bugs, and links to the report and the CI build. Each channel picks its events; the agent also posts a summary to a channel when asked. | `notify` | [docs](#17-notifications) |
|
|
70
|
-
| Release | **Reports you can share**: Writes up what ForgeQE's agent found, such as
|
|
70
|
+
| Release | **Reports you can share**: Writes up what ForgeQE's agent found, such as open defects across projects, release status, or pipeline health, as a page with filters (project, state, priority, assignee, …), charts, and a sortable table, plus Markdown and CSV. Every table is built from evidence: the results of the tool calls the agent made, or a read-only query ForgeQE runs itself, listed on the page's Sources with each raw result kept beside the report. | `write_report` | [docs](#reports-from-the-agent) |
|
|
71
71
|
| Operations | **CI pipelines**: Writes a GitHub Actions or Azure Pipelines workflow that runs ForgeQE, stores the secrets it needs, pushes a branch, and makes the pipeline green. | `push_branch`, `set_ci_secret` | [docs](#8-pipelines) |
|
|
72
72
|
| Systems | **Work with Azure DevOps, Jira, GitHub, and Confluence**: Reads anything in the configured systems (work items, issues, queries, boards, pages), makes changes the user asks for, puts run evidence (screenshots, reports) on work items and issues as attachments or comments, and builds Azure DevOps dashboards and wiki pages. | `query_api`, `read_confluence`, `create_ado_dashboard`, `write_ado_wiki_page`, `attach_to_work_item` | [docs](README.md#azure-devops-jira-and-other-systems) |
|
|
73
73
|
| Setup | **Setup, skills, and documentation**: Shows the model profiles and MCP servers, installs or repairs the browser engine, creates a forgeqe.yaml, manages skills, and answers questions about ForgeQE from its documentation and this list. | `forgeqe_setup`, `capabilities` | [docs](#2-scaffold-config) |
|
|
@@ -390,15 +390,25 @@ The text is the agent's own result. Failed runs show `✗ Task failed` with the
|
|
|
390
390
|
|
|
391
391
|
### Reports from the agent
|
|
392
392
|
|
|
393
|
-
Ask for a report and ForgeQE's agent writes one you can share: "
|
|
393
|
+
Ask for a report and ForgeQE's agent writes one you can share: "generate a defect report for all open defects", "create a release report for all projects", "summarise this sprint's test results as a report". Every table in it is built from **evidence**, not typed by the model:
|
|
394
|
+
|
|
395
|
+
- ForgeQE keeps the result of every tool call in the session (its own tools, and the Azure DevOps and Jira MCP servers). A dataset of the report names the calls it comes from, and ForgeQE takes the rows from their results.
|
|
396
|
+
- Or the dataset is a read-only query that ForgeQE runs itself when it writes the report: a WIQL query reads up to 1000 work items with their project, type, state, priority, severity, assignee, area, iteration, dates, and a link. The agent uses this for "all" of something.
|
|
397
|
+
- The agent writes only the summary paragraph.
|
|
398
|
+
|
|
399
|
+
The report goes to the reports folder (`export.out_dir`, `.forgeqe/reports/` by default):
|
|
394
400
|
|
|
395
401
|
```
|
|
396
|
-
.forgeqe/reports/
|
|
397
|
-
report.
|
|
398
|
-
report.
|
|
402
|
+
.forgeqe/reports/open-defects-all-projects-20261013-101512/
|
|
403
|
+
report.html # one file: summary, filters, charts, sortable table, Sources
|
|
404
|
+
report.md # the same, to edit, paste into a wiki, or attach
|
|
405
|
+
open-defects.csv # each dataset's rows
|
|
406
|
+
evidence/E1.json # the raw result of each tool call the report used
|
|
399
407
|
```
|
|
400
408
|
|
|
401
|
-
The
|
|
409
|
+
The page adds filters for the columns that are in the data (Project, State, Type, Priority, Severity, Assigned To, Area, Iteration, …, and other columns with a short list of values). The counts, the charts (click a bar to filter by it), and the table follow the filters, and **Export CSV** saves the rows you filtered. **Sources** lists each tool call, when it ran, and how many rows it gave, with a link to its raw result.
|
|
410
|
+
|
|
411
|
+
The chat gets a short summary and a card with the paths as links, and **Open the report** appears as a next step. To post it, ask for it ("post the report to the release channel") once `notify.channels` is set up; the reports folder is linked when it is served (`notify.report_url`).
|
|
402
412
|
|
|
403
413
|
### The screen
|
|
404
414
|
|
|
@@ -637,7 +647,22 @@ What happens after the run passes:
|
|
|
637
647
|
4. It writes the test from the handoff and the replay script: steps that worked are ported with their locators, steps that failed are fixed first. Every locator, including those for assertions, comes from the replay script, the page snapshots, page memory, or a check on the page; steps where the browser agent ran JavaScript to look around are left out. It does not walk the flow again or build tests to explore. Once a step fails (in the replay, or in its test), ForgeQE switches on its live browser: a Chrome that Playwright drives, set up like ForgeQE's, where it looks at the failing step's page one tool call per action (open, snapshot, click, type, wait, screenshot, run JavaScript, console errors, failed requests). Until then the live browser tools are off, so no tokens go into looking at pages that work. The live browser needs Node.js (`npx`), and Microsoft's Playwright MCP server is downloaded on first use; with `live_browser: false`, or without `npx`, it looks with a small throwaway script instead. When its browser gets a different page than ForgeQE's did (an error, a challenge, a control that will not open), it compares the two browsers (user agent, `navigator.webdriver`, headless, failed requests and CORS errors, headers the framework adds, console errors) and fixes the difference in the repo's browser configuration. It never judges the site with `curl` or `webfetch`.
|
|
638
648
|
5. Scribe does not run the test itself: it may build or compile, then reports the test file and the command that runs only that test, and ForgeQE runs it. If one command it runs fails `stall_after` times in a row within its work (the same build again and again, say), ForgeQE stops that turn and sends the command and its last output back, asking it to change approach. If the same command stalls again, the job stops as a failure with that output in the notes.
|
|
639
649
|
6. ForgeQE runs that command (with `CI=1`) and checks the exit code, and it watches the processes the command starts for a browser launched by Playwright, Puppeteer, Selenium, or Cypress. On a failure it sends the command, exit code, and output back to Scribe. The output is kept short so the error is never cut off: the page dump Playwright puts into a failed assertion ("Aria snapshot:", often thousands of lines) is taken out and saved as `<run>/test-runs/attempt-N-page.aria.yml`, then the start (where the first error and its stack trace are) and the end (the summary) are sent; the full output is in `<run>/test-runs/attempt-N.log`, and the prompt names both files. A command that exits 0 without starting a browser also goes back: the test must drive the app in a browser. If the repo's browser runs on another machine (Selenium Grid, a cloud service, a container), the agent says where and the check is skipped.
|
|
640
|
-
7. A passing test then goes to a separate reviewer agent, which can read files and `git diff` but not change anything. It checks the change against the goal and the test rules: every part of the goal done through the page and asserted, generic page objects with data in the test, no values or URLs copied from the run, browser settings in the repo's configuration, no leftover files. The changed files come inline in its prompt, so it reads nothing again, and it does not ask to replace locators or texts that came from the replay or the checks, since those matched the live site. It judges only what this job changed: ForgeQE records the working tree before the job, so your uncommitted edits, your untracked files, and ForgeQE's own files (`.forgeqe/`, `forgeqe.yaml`, `llms.json`) are never part of the review. It checks the diff of every file that existed before the job: removing or rewriting an existing test, even a commented-out one, is a correctness problem. Credentials come from the environment, except a public demo login that the application itself shows to anyone (such as the logins on saucedemo.com's sign-in page), which may stay in the repo's test data. It gets the outlines of the classes the changed code uses, the repo's browser settings, and the same `code_*` tools, so it checks reuse and settings without reading those files. The code graph also tells it what the job added that it can check in the code: a new member that locates an element an existing member already locates (to reuse instead), and new XPath or position-based locators. If it finds problems, they go back to Scribe as the next attempt.
|
|
650
|
+
7. A passing test then goes to a separate reviewer agent, which can read files and `git diff` but not change anything. It checks the change against the goal and the test rules: every part of the goal done through the page and asserted, generic page objects with data in the test, no values or URLs copied from the run, browser settings in the repo's configuration, no leftover files. The changed files come inline in its prompt, so it reads nothing again, and it does not ask to replace locators or texts that came from the replay or the checks, since those matched the live site. It judges only what this job changed: ForgeQE records the working tree before the job, so your uncommitted edits, your untracked files, and ForgeQE's own files (`.forgeqe/`, `forgeqe.yaml`, `llms.json`) are never part of the review. It checks the diff of every file that existed before the job: removing or rewriting an existing test, even a commented-out one, is a correctness problem. Credentials come from the environment, except a public demo login that the application itself shows to anyone (such as the logins on saucedemo.com's sign-in page), which may stay in the repo's test data. It gets the outlines of the classes the changed code uses, the repo's browser settings, and the same `code_*` tools, so it checks reuse and settings without reading those files. The code graph also tells it what the job added that it can check in the code: a new member that locates an element an existing member already locates (to reuse instead), and new XPath or position-based locators. If it finds problems, they go back to Scribe as the next attempt. The terminal shows the exchange as it happens, in order: the reviewer's verdict with its reason and each issue (✗ correctness, ~ coding standard) with the file and the fix it asks for; then why Scribe tries again and what it is fixing (the reviewer's issues, or the test's first error); then what Scribe says it did and which files it changed; then the next verdict:
|
|
651
|
+
|
|
652
|
+
```
|
|
653
|
+
⏺ Reviewer asked for changes attempt 1 of 5 · 1 to fix, 1 coding-standard
|
|
654
|
+
⎿ The test never checks that the pie chart is shown.
|
|
655
|
+
✗ Correctness · tests/PracticeTests.cs: No assertion on the SVG pie chart.
|
|
656
|
+
Fix: Assert the chart's heading and SVG are visible.
|
|
657
|
+
~ Coding standard · tests/PracticeTests.cs: Test data is inline.
|
|
658
|
+
Fix: Move it to the page object.
|
|
659
|
+
⏺ Scribe tries again · attempt 2 of 5 the reviewer asked for changes
|
|
660
|
+
⎿ → tests/PracticeTests.cs: No assertion on the SVG pie chart. — fix: Assert the chart's heading and SVG are visible.
|
|
661
|
+
⎿ Scribe (attempt 2): Added the pie chart assertion · changed tests/PracticeTests.cs, Pages/PracticePage.cs
|
|
662
|
+
⏺ Reviewer approved the test attempt 2 of 5
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
The Activities panel has one line for each verdict and attempt, and `forgeqe` commands outside the terminal UI print the same blocks.
|
|
641
666
|
|
|
642
667
|
Scribe cannot edit ForgeQE's files or run folders, and cannot run `git clean`, `git reset --hard`, `git stash`, `git restore`, or `git checkout -- <path>`. 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.
|
|
643
668
|
8. The job is done when ForgeQE's run passes with a browser and the reviewer approves. It stops as a failure after `max_attempts` attempts, when the job passes `code.max_tokens`, 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.
|
package/docs/user-guide.html
CHANGED
|
@@ -1302,13 +1302,23 @@ Acceptance criteria:
|
|
|
1302
1302
|
<p>ForgeQE runs that command (with <code>CI=1</code>) and checks the exit code, and it watches the processes the command starts for a browser launched by Playwright, Puppeteer, Selenium, or Cypress. On a failure it sends the command, exit code, and output back to Scribe. The output is kept short so the error is never cut off: the page dump Playwright puts into a failed assertion ("Aria snapshot:", often thousands of lines) is taken out and saved as <code><run>/test-runs/attempt-N-page.aria.yml</code>, then the start (where the first error and its stack trace are) and the end (the summary) are sent; the full output is in <code><run>/test-runs/attempt-N.log</code>, and the prompt names both files. A command that exits 0 without starting a browser also goes back: the test must drive the app in a browser. If the repo's browser runs on another machine (Selenium Grid, a cloud service, a container), the agent says where and the check is skipped.</p>
|
|
1303
1303
|
</li>
|
|
1304
1304
|
<li>
|
|
1305
|
-
<p>A passing test then goes to a separate reviewer agent, which can read files and <code>git diff</code> but not change anything. It checks the change against the goal and the test rules: every part of the goal done through the page and asserted, generic page objects with data in the test, no values or URLs copied from the run, browser settings in the repo's configuration, no leftover files. The changed files come inline in its prompt, so it reads nothing again, and it does not ask to replace locators or texts that came from the replay or the checks, since those matched the live site. It judges only what this job changed: ForgeQE records the working tree before the job, so your uncommitted edits, your untracked files, and ForgeQE's own files (<code>.forgeqe/</code>, <code>forgeqe.yaml</code>, <code>llms.json</code>) are never part of the review. It checks the diff of every file that existed before the job: removing or rewriting an existing test, even a commented-out one, is a correctness problem. Credentials come from the environment, except a public demo login that the application itself shows to anyone (such as the logins on saucedemo.com's sign-in page), which may stay in the repo's test data. It gets the outlines of the classes the changed code uses, the repo's browser settings, and the same <code>code_*</code> tools, so it checks reuse and settings without reading those files. The code graph also tells it what the job added that it can check in the code: a new member that locates an element an existing member already locates (to reuse instead), and new XPath or position-based locators. If it finds problems, they go back to Scribe as the next attempt
|
|
1306
|
-
<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.</p>
|
|
1307
|
-
</li>
|
|
1308
|
-
<li>
|
|
1309
|
-
<p>The job is done when ForgeQE's run passes with a browser and the reviewer approves. 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>
|
|
1305
|
+
<p>A passing test then goes to a separate reviewer agent, which can read files and <code>git diff</code> but not change anything. It checks the change against the goal and the test rules: every part of the goal done through the page and asserted, generic page objects with data in the test, no values or URLs copied from the run, browser settings in the repo's configuration, no leftover files. The changed files come inline in its prompt, so it reads nothing again, and it does not ask to replace locators or texts that came from the replay or the checks, since those matched the live site. It judges only what this job changed: ForgeQE records the working tree before the job, so your uncommitted edits, your untracked files, and ForgeQE's own files (<code>.forgeqe/</code>, <code>forgeqe.yaml</code>, <code>llms.json</code>) are never part of the review. It checks the diff of every file that existed before the job: removing or rewriting an existing test, even a commented-out one, is a correctness problem. Credentials come from the environment, except a public demo login that the application itself shows to anyone (such as the logins on saucedemo.com's sign-in page), which may stay in the repo's test data. It gets the outlines of the classes the changed code uses, the repo's browser settings, and the same <code>code_*</code> tools, so it checks reuse and settings without reading those files. The code graph also tells it what the job added that it can check in the code: a new member that locates an element an existing member already locates (to reuse instead), and new XPath or position-based locators. If it finds problems, they go back to Scribe as the next attempt. The terminal shows the exchange as it happens, in order: the reviewer's verdict with its reason and each issue (✗ correctness, ~ coding standard) with the file and the fix it asks for; then why Scribe tries again and what it is fixing (the reviewer's issues, or the test's first error); then what Scribe says it did and which files it changed; then the next verdict:</p>
|
|
1310
1306
|
</li>
|
|
1311
1307
|
</ol>
|
|
1308
|
+
<pre><code>⏺ Reviewer asked for changes attempt 1 of 5 · 1 to fix, 1 coding-standard
|
|
1309
|
+
⎿ The test never checks that the pie chart is shown.
|
|
1310
|
+
✗ Correctness · tests/PracticeTests.cs: No assertion on the SVG pie chart.
|
|
1311
|
+
Fix: Assert the chart's heading and SVG are visible.
|
|
1312
|
+
~ Coding standard · tests/PracticeTests.cs: Test data is inline.
|
|
1313
|
+
Fix: Move it to the page object.
|
|
1314
|
+
⏺ Scribe tries again · attempt 2 of 5 the reviewer asked for changes
|
|
1315
|
+
⎿ → tests/PracticeTests.cs: No assertion on the SVG pie chart. — fix: Assert the chart's heading and SVG are visible.
|
|
1316
|
+
⎿ Scribe (attempt 2): Added the pie chart assertion · changed tests/PracticeTests.cs, Pages/PracticePage.cs
|
|
1317
|
+
⏺ Reviewer approved the test attempt 2 of 5
|
|
1318
|
+
</code></pre>
|
|
1319
|
+
<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>
|
|
1320
|
+
<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.
|
|
1321
|
+
8. The job is done when ForgeQE's run passes with a browser and the reviewer approves. 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>
|
|
1312
1322
|
<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>
|
|
1313
1323
|
<p>The transcript shows the handover and Scribe's work as it goes; files it writes or edits are marked ✎:</p>
|
|
1314
1324
|
<pre><code>┃
|
|
@@ -1804,22 +1814,31 @@ forgeqe visual approve --page /home # make this run's /home the baseline (asks
|
|
|
1804
1814
|
</article>
|
|
1805
1815
|
<article class="spec feat" id="cap-reports">
|
|
1806
1816
|
<div class="hd"><span class="mono">Release</span><h3>Reports you can share</h3></div>
|
|
1807
|
-
<p class="ds">Writes up what ForgeQE's agent found, such as
|
|
1817
|
+
<p class="ds">Writes up what ForgeQE's agent found, such as open defects across projects, release status, or pipeline health, as a page with filters (project, state, priority, assignee, …), charts, and a sortable table, plus Markdown and CSV. Every table is built from evidence: the results of the tool calls the agent made, or a read-only query ForgeQE runs itself, listed on the page's Sources with each raw result kept beside the report.</p>
|
|
1808
1818
|
<div class="facts">
|
|
1809
1819
|
<div class="fact"><span class="mono">Ask it</span><ul class="asks"><li>Create a release report for all projects</li><li>Write up the open defects as a report I can share</li></ul></div>
|
|
1810
1820
|
<div class="fact"><span class="mono">What it needs</span><ul class="needs"><li><span class="need req">Required</span><span>A live model profile in llms.json (an API key or Microsoft Entra ID sign-in)</span></li></ul></div>
|
|
1811
|
-
<div class="fact"><span class="mono">Limits</span><ul class="plainlist"><li>
|
|
1821
|
+
<div class="fact"><span class="mono">Limits</span><ul class="plainlist"><li>Rows come only from tool results in the session or a query ForgeQE runs; the agent writes only the summary, which is not checked figure by figure.</li><li>A WIQL query reads up to 1000 work items; the page says when a query matched more.</li><li>The page is not published anywhere unless the reports folder is served (notify.report_url).</li></ul></div>
|
|
1812
1822
|
<div class="fact"><span class="mono">Tools</span><p class="tools"><code>write_report</code></p></div>
|
|
1813
1823
|
</div>
|
|
1814
1824
|
<div class="detail"><span class="mono">In detail</span>
|
|
1815
1825
|
<section class="doc" id="doc-how_to_use-reports-from-the-agent">
|
|
1816
1826
|
<h4>Reports from the agent</h4>
|
|
1817
|
-
<div class="prose"><p>Ask for a report and ForgeQE's agent writes one you can share: "
|
|
1818
|
-
<
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1827
|
+
<div class="prose"><p>Ask for a report and ForgeQE's agent writes one you can share: "generate a defect report for all open defects", "create a release report for all projects", "summarise this sprint's test results as a report". Every table in it is built from <strong>evidence</strong>, not typed by the model:</p>
|
|
1828
|
+
<ul>
|
|
1829
|
+
<li>ForgeQE keeps the result of every tool call in the session (its own tools, and the Azure DevOps and Jira MCP servers). A dataset of the report names the calls it comes from, and ForgeQE takes the rows from their results.</li>
|
|
1830
|
+
<li>Or the dataset is a read-only query that ForgeQE runs itself when it writes the report: a WIQL query reads up to 1000 work items with their project, type, state, priority, severity, assignee, area, iteration, dates, and a link. The agent uses this for "all" of something.</li>
|
|
1831
|
+
<li>The agent writes only the summary paragraph.</li>
|
|
1832
|
+
</ul>
|
|
1833
|
+
<p>The report goes to the reports folder (<code>export.out_dir</code>, <code>.forgeqe/reports/</code> by default):</p>
|
|
1834
|
+
<pre><code>.forgeqe/reports/open-defects-all-projects-20261013-101512/
|
|
1835
|
+
report.html # one file: summary, filters, charts, sortable table, Sources
|
|
1836
|
+
report.md # the same, to edit, paste into a wiki, or attach
|
|
1837
|
+
open-defects.csv # each dataset's rows
|
|
1838
|
+
evidence/E1.json # the raw result of each tool call the report used
|
|
1839
|
+
</code></pre>
|
|
1840
|
+
<p>The page adds filters for the columns that are in the data (Project, State, Type, Priority, Severity, Assigned To, Area, Iteration, …, and other columns with a short list of values). The counts, the charts (click a bar to filter by it), and the table follow the filters, and <strong>Export CSV</strong> saves the rows you filtered. <strong>Sources</strong> lists each tool call, when it ran, and how many rows it gave, with a link to its raw result.</p>
|
|
1841
|
+
<p>The chat gets a short summary and a card with the paths as links, and <strong>Open the report</strong> appears as a next step. To post it, ask for it ("post the report to the release channel") once <code>notify.channels</code> is set up; the reports folder is linked when it is served (<code>notify.report_url</code>).</p>
|
|
1823
1842
|
</div>
|
|
1824
1843
|
</section>
|
|
1825
1844
|
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capagents/forgeqe",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.9",
|
|
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": "MIT",
|
|
6
6
|
"type": "module",
|
package/prompts/brain.md
CHANGED
|
@@ -63,7 +63,7 @@ A message can also be a task specification with the parts Role, Objective, Conte
|
|
|
63
63
|
- logins: manages saved logins. It lists them. It lets the user sign in by hand in a browser (sign_in). It removes a session.
|
|
64
64
|
- page_memory: gives what ForgeQE knows about the pages, locators, and recorded flows of a site. Clear it only when the user asks.
|
|
65
65
|
- notify: posts a message to the Teams or email channels of the team. Use it only when the user asks. Suites, release verdicts, and failed evaluations go to their channels automatically from runs outside the shell.
|
|
66
|
-
- write_report: saves a report that the user asks for as a Markdown file
|
|
66
|
+
- write_report: saves a report that the user asks for as a page (filters, charts, the sources of its data) and a Markdown file. ForgeQE builds its tables from evidence: the results of tool calls in this session, or a read-only query that it runs itself. You write only the summary.
|
|
67
67
|
- capabilities: tells what ForgeQE can do, and whether each capability works in this setup. It gives what to set up, example requests, limits, and the documentation.
|
|
68
68
|
- forgeqe_setup: gives model profiles, the MCP servers that ForgeQE sets up, and the documentation of ForgeQE (docs). It installs or repairs the browser engine. It creates a forgeqe.yaml (init).
|
|
69
69
|
- push_branch: the only way to push. It refuses the default branch, and it never forces.
|
|
@@ -335,7 +335,9 @@ The shell shows replies as markdown in a narrow chat pane. Write them so that th
|
|
|
335
335
|
- You MUST use bold only for the one word or number that is important in a line, such as a status or a count.
|
|
336
336
|
- You SHOULD use a table only for two or three short columns.
|
|
337
337
|
- You MUST keep your own comments short. You MUST give in full the test cases, tests, and reviews that the user asked for.
|
|
338
|
-
- When the user asks for a report, a write-up, or a summary to share, you MUST
|
|
338
|
+
- When the user asks for a report, a write-up, or a summary to share, you MUST save the report with write_report, with its data as datasets. You MUST NOT type rows, counts, or tables into the report yourself.
|
|
339
|
+
- For "all" of something (all open defects, every project), you SHOULD give the dataset a query (a WIQL query or a REST path), so that ForgeQE reads every item itself. Otherwise, name the tool calls that you made in this session (from: the tool name or the evidence id).
|
|
340
|
+
- The summary MUST hold only facts that are in the datasets. After write_report, give the counts that it returned.
|
|
339
341
|
- You MUST then reply with three to five lines of summary. The shell shows the paths of the report as links, so you MUST NOT repeat them or paste the full report.
|
|
340
342
|
- You MUST NOT end a reply with "If you want, I can…" options. Put the follow-ups that the user will probably want in next, 3 or fewer, each with a short label and the request as the user would type it.
|
|
341
343
|
- You MUST end each turn with the structured answer: reply, outcome (done, failed, or needs_input), and next.
|
package/src/brain/index.ts
CHANGED
|
@@ -159,6 +159,10 @@ export async function openBrain(opts: BrainOptions): Promise<Brain> {
|
|
|
159
159
|
[MCP_NAME]: tools.entry,
|
|
160
160
|
},
|
|
161
161
|
hideTool: (tool) => tool.startsWith(`${MCP_NAME}_`),
|
|
162
|
+
// ForgeQE's own tools record their results themselves; this adds the other MCP servers' (Azure DevOps, Jira, …).
|
|
163
|
+
onToolResult: (r) => {
|
|
164
|
+
if (r.ok && !r.tool.startsWith(`${MCP_NAME}_`) && r.tool.includes("_")) tools.server.evidence.record(r.tool, r.input, r.output)
|
|
165
|
+
},
|
|
162
166
|
title: "ForgeQE",
|
|
163
167
|
})
|
|
164
168
|
} catch (err) {
|
|
@@ -317,11 +317,15 @@ export const CAPABILITIES: Capability[] = [
|
|
|
317
317
|
area: "Release",
|
|
318
318
|
title: "Reports you can share",
|
|
319
319
|
summary:
|
|
320
|
-
"Writes up what ForgeQE's agent found, such as
|
|
320
|
+
"Writes up what ForgeQE's agent found, such as open defects across projects, release status, or pipeline health, as a page with filters (project, state, priority, assignee, …), charts, and a sortable table, plus Markdown and CSV. Every table is built from evidence: the results of the tool calls the agent made, or a read-only query ForgeQE runs itself, listed on the page's Sources with each raw result kept beside the report.",
|
|
321
321
|
tools: ["write_report"],
|
|
322
322
|
examples: ["Create a release report for all projects", "Write up the open defects as a report I can share"],
|
|
323
323
|
requires: [{ kind: "live_model" }],
|
|
324
|
-
limits: [
|
|
324
|
+
limits: [
|
|
325
|
+
"Rows come only from tool results in the session or a query ForgeQE runs; the agent writes only the summary, which is not checked figure by figure.",
|
|
326
|
+
"A WIQL query reads up to 1000 work items; the page says when a query matched more.",
|
|
327
|
+
"The page is not published anywhere unless the reports folder is served (notify.report_url).",
|
|
328
|
+
],
|
|
325
329
|
docs: [{ doc: "HOW_TO_USE.md", heading: "Reports from the agent" }],
|
|
326
330
|
},
|
|
327
331
|
{
|
package/src/cases/api.ts
CHANGED
|
@@ -16,37 +16,88 @@ export type ApiQuery = {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const WIQL_TOP = 50
|
|
19
|
+
/** What a report may read in one WIQL search. */
|
|
20
|
+
export const REPORT_WIQL_TOP = 1000
|
|
19
21
|
const WIQL_FIELDS = ["System.Id", "System.WorkItemType", "System.Title", "System.State", "System.AssignedTo", "System.ChangedDate", "System.IterationPath"]
|
|
22
|
+
/** Read too when the organization has them; a process without one of them falls back to WIQL_FIELDS. */
|
|
23
|
+
const WIQL_MORE = ["System.TeamProject", "Microsoft.VSTS.Common.Priority", "Microsoft.VSTS.Common.Severity", "System.AreaPath", "System.CreatedDate", "System.Tags"]
|
|
24
|
+
const BATCH = 200
|
|
25
|
+
|
|
26
|
+
export type WorkItemRow = {
|
|
27
|
+
id: number
|
|
28
|
+
project?: string
|
|
29
|
+
type?: unknown
|
|
30
|
+
title?: unknown
|
|
31
|
+
state?: unknown
|
|
32
|
+
priority?: unknown
|
|
33
|
+
severity?: unknown
|
|
34
|
+
assignedTo?: string
|
|
35
|
+
area?: unknown
|
|
36
|
+
iteration?: unknown
|
|
37
|
+
tags?: unknown
|
|
38
|
+
created?: unknown
|
|
39
|
+
changed?: unknown
|
|
40
|
+
url: string
|
|
41
|
+
}
|
|
20
42
|
|
|
21
43
|
/**
|
|
22
44
|
* A WIQL search: Azure DevOps answers it with ids only (a POST, but WIQL has no statement that changes anything), so
|
|
23
|
-
* the first
|
|
45
|
+
* the first `top` are read back with their main fields in batch reads.
|
|
24
46
|
*/
|
|
25
|
-
async function
|
|
47
|
+
export async function adoWorkItems(cfg: SourcesConfig, wiql: string, project?: string, top = WIQL_TOP): Promise<{ count: number; total: number; items: WorkItemRow[] }> {
|
|
26
48
|
const { ado, auth } = adoSettings(cfg)
|
|
27
49
|
const org = ado.orgUrl.replace(/\/+$/, "")
|
|
28
50
|
const scope = project || ado.project
|
|
29
51
|
const base = scope ? `${org}/${encodeURIComponent(scope)}` : org
|
|
30
52
|
const { data } = await http<{ workItems?: { id: number }[]; workItemRelations?: { source?: { id: number } | null; target?: { id: number } }[] }>(
|
|
31
|
-
`${base}/_apis/wit/wiql?api-version=7.1&$top=${
|
|
53
|
+
`${base}/_apis/wit/wiql?api-version=7.1&$top=${top}`,
|
|
32
54
|
{ auth, body: { query: wiql } },
|
|
33
55
|
)
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const
|
|
56
|
+
const all = [...new Set([...(data.workItems || []).map((w) => w.id), ...(data.workItemRelations || []).flatMap((r) => [r.source?.id, r.target?.id])])].filter((id): id is number => typeof id === "number")
|
|
57
|
+
const ids = all.slice(0, top)
|
|
58
|
+
const read = async (fields: string[]) => {
|
|
59
|
+
const out: { id: number; fields: Record<string, unknown> }[] = []
|
|
60
|
+
for (let i = 0; i < ids.length; i += BATCH) {
|
|
61
|
+
const batch = await http<{ value: { id: number; fields: Record<string, unknown> }[] }>(`${org}/_apis/wit/workitemsbatch?api-version=7.1`, { auth, body: { ids: ids.slice(i, i + BATCH), fields } })
|
|
62
|
+
out.push(...batch.data.value)
|
|
63
|
+
}
|
|
64
|
+
return out
|
|
65
|
+
}
|
|
66
|
+
const values = ids.length ? await read([...WIQL_FIELDS, ...WIQL_MORE]).catch(() => read(WIQL_FIELDS)) : []
|
|
37
67
|
const name = (v: unknown) => (v && typeof v === "object" ? String((v as { displayName?: string }).displayName || "") : String(v || ""))
|
|
38
68
|
return {
|
|
39
69
|
count: ids.length,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
70
|
+
total: all.length,
|
|
71
|
+
items: values.map((w) => {
|
|
72
|
+
const f = w.fields
|
|
73
|
+
const itemProject = String(f["System.TeamProject"] || scope || "")
|
|
74
|
+
return {
|
|
75
|
+
id: w.id,
|
|
76
|
+
...(f["System.TeamProject"] ? { project: String(f["System.TeamProject"]) } : {}),
|
|
77
|
+
type: f["System.WorkItemType"],
|
|
78
|
+
title: f["System.Title"],
|
|
79
|
+
state: f["System.State"],
|
|
80
|
+
...(f["Microsoft.VSTS.Common.Priority"] !== undefined ? { priority: f["Microsoft.VSTS.Common.Priority"] } : {}),
|
|
81
|
+
...(f["Microsoft.VSTS.Common.Severity"] !== undefined ? { severity: f["Microsoft.VSTS.Common.Severity"] } : {}),
|
|
82
|
+
assignedTo: name(f["System.AssignedTo"]) || undefined,
|
|
83
|
+
...(f["System.AreaPath"] ? { area: f["System.AreaPath"] } : {}),
|
|
84
|
+
iteration: f["System.IterationPath"],
|
|
85
|
+
...(f["System.Tags"] ? { tags: f["System.Tags"] } : {}),
|
|
86
|
+
...(f["System.CreatedDate"] ? { created: f["System.CreatedDate"] } : {}),
|
|
87
|
+
changed: f["System.ChangedDate"],
|
|
88
|
+
url: `${org}/${itemProject ? `${encodeURIComponent(itemProject)}/` : ""}_workitems/edit/${w.id}`,
|
|
89
|
+
}
|
|
90
|
+
}),
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function adoWiql(cfg: SourcesConfig, wiql: string, project?: string): Promise<unknown> {
|
|
95
|
+
const { count, items } = await adoWorkItems(cfg, wiql, project, WIQL_TOP)
|
|
96
|
+
if (!count) return { count: 0, items: [] }
|
|
97
|
+
return {
|
|
98
|
+
count,
|
|
99
|
+
items,
|
|
100
|
+
note: `Read each one in full with path <project>/_apis/wit/workitems/<id>?$expand=all.${count === WIQL_TOP ? ` Only the first ${WIQL_TOP} are listed; narrow the query for the rest, or pass the query to write_report, which reads up to ${REPORT_WIQL_TOP}.` : ""}`,
|
|
50
101
|
}
|
|
51
102
|
}
|
|
52
103
|
|
|
@@ -85,31 +136,33 @@ function clipJson(data: unknown): unknown {
|
|
|
85
136
|
* Read-only requests with the credentials from forgeqe.yaml and the environment: GET and WIQL searches for Azure
|
|
86
137
|
* DevOps, GET for Jira (Xray Server/Data Center is under Jira's /rest/raven), GraphQL queries for Xray Cloud.
|
|
87
138
|
*/
|
|
88
|
-
export async function queryApi(cfg: SourcesConfig, q: ApiQuery): Promise<unknown> {
|
|
139
|
+
export async function queryApi(cfg: SourcesConfig, q: ApiQuery, opts: { full?: boolean } = {}): Promise<unknown> {
|
|
140
|
+
// A report reads the whole answer; the agent gets it clipped, so it fits its context.
|
|
141
|
+
const clip = opts.full ? (d: unknown) => d : clipJson
|
|
89
142
|
switch (q.system) {
|
|
90
143
|
case "azure_devops": {
|
|
91
|
-
if (q.wiql?.trim()) return clipJson(await adoWiql(cfg, q.wiql, q.project))
|
|
144
|
+
if (q.wiql?.trim()) return opts.full ? adoWorkItems(cfg, q.wiql, q.project, REPORT_WIQL_TOP) : clipJson(await adoWiql(cfg, q.wiql, q.project))
|
|
92
145
|
if (!q.path) throw new Error("path or wiql is required for azure_devops")
|
|
93
146
|
const { ado, auth } = adoSettings(cfg)
|
|
94
147
|
let url = resolve(q.path, adoBases(ado.orgUrl), "Azure DevOps")
|
|
95
148
|
if (!/[?&]api-version=/.test(url)) url += `${url.includes("?") ? "&" : "?"}api-version=7.1`
|
|
96
|
-
return
|
|
149
|
+
return clip((await http(url, { auth })).data)
|
|
97
150
|
}
|
|
98
151
|
case "jira": {
|
|
99
152
|
if (!q.path) throw new Error("path is required for jira")
|
|
100
153
|
const j = jiraApi(cfg)
|
|
101
|
-
return
|
|
154
|
+
return clip((await http(resolve(q.path, [j.url], "Jira"), { auth: j.auth })).data)
|
|
102
155
|
}
|
|
103
156
|
case "confluence": {
|
|
104
157
|
if (!q.path) throw new Error("path is required for confluence")
|
|
105
158
|
const c = confluenceApi(cfg)
|
|
106
|
-
return
|
|
159
|
+
return clip((await http(resolve(q.path, [c.url], "Confluence"), { auth: c.auth })).data)
|
|
107
160
|
}
|
|
108
161
|
case "xray": {
|
|
109
162
|
if (cfg.jira && !cfg.jira.cloud) throw new Error("Xray Server/Data Center is part of Jira: use system jira with a /rest/raven/1.0/... path.")
|
|
110
163
|
if (!q.query) throw new Error("query (GraphQL) is required for xray")
|
|
111
164
|
if (isMutation(q.query)) throw new Error("query_api only reads; GraphQL mutations are not allowed.")
|
|
112
|
-
return
|
|
165
|
+
return clip(await xrayGraphql(cfg, await xrayCloudToken(cfg), q.query, q.variables || {}))
|
|
113
166
|
}
|
|
114
167
|
default:
|
|
115
168
|
throw new Error(`Unknown system "${String(q.system)}". Use azure_devops, jira, xray, or confluence.`)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Card } from "../card.ts"
|
|
2
|
+
import type { CodeEvent } from "../types.ts"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The exchange between Scribe and the reviewer, as the shell and the CLI show it: what the reviewer found and why,
|
|
6
|
+
* why Scribe tries again and what it is fixing, and what Scribe says it did. One place, so both say the same.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
type Review = Extract<CodeEvent, { kind: "review" }>
|
|
10
|
+
type Attempt = Extract<CodeEvent, { kind: "attempt" }>
|
|
11
|
+
type Answer = Extract<CodeEvent, { kind: "answer" }>
|
|
12
|
+
|
|
13
|
+
const clip = (t: string, n: number) => {
|
|
14
|
+
const s = t.replace(/\s+/g, " ").trim()
|
|
15
|
+
return s.length <= n ? s : `${s.slice(0, n - 1)}…`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** "✗ Does not test the goal · tests/Practice.cs: the pie chart is never checked", then the fix under it. */
|
|
19
|
+
export function reviewCard(e: Review): Card {
|
|
20
|
+
const correctness = e.issues.filter((i) => i.kind !== "standards").length
|
|
21
|
+
const standards = e.issues.length - correctness
|
|
22
|
+
const count = [correctness ? `${correctness} to fix` : "", standards ? `${standards} coding-standard` : ""].filter(Boolean).join(", ")
|
|
23
|
+
return {
|
|
24
|
+
status: e.approved ? "ok" : "fail",
|
|
25
|
+
title: e.approved ? (standards ? "Reviewer approved the test, with coding-standard notes" : "Reviewer approved the test") : "Reviewer asked for changes",
|
|
26
|
+
stats: [`attempt ${e.attempt} of ${e.max}`, count].filter(Boolean).join(" · "),
|
|
27
|
+
body: [
|
|
28
|
+
...(e.summary ? [clip(e.summary, 400)] : []),
|
|
29
|
+
...(e.issues.length ? [""] : []),
|
|
30
|
+
...e.issues.flatMap((i) => [
|
|
31
|
+
// ✗ marks what makes the test wrong, ~ how it is written: the colours follow the mark.
|
|
32
|
+
`${i.kind === "standards" ? "~ Coding standard" : "✗ Correctness"}${i.file ? ` · ${i.file}` : ""}: ${clip(i.problem, 300)}`,
|
|
33
|
+
...(i.fix ? [` Fix: ${clip(i.fix, 300)}`] : []),
|
|
34
|
+
]),
|
|
35
|
+
],
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** "Scribe · attempt 2 of 5 · the reviewer asked for changes", then each thing it was asked to fix. */
|
|
40
|
+
export function attemptCard(e: Attempt): Card {
|
|
41
|
+
return {
|
|
42
|
+
status: "info",
|
|
43
|
+
title: `Scribe tries again · attempt ${e.attempt} of ${e.max}`,
|
|
44
|
+
...(e.why ? { stats: e.why } : {}),
|
|
45
|
+
body: e.fixing.map((f) => `→ ${clip(f, 300)}`),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** "Scribe (attempt 2): Moved the values into PracticePage · changed tests/Practice.cs, pages/PracticePage.cs". */
|
|
50
|
+
export function answerText(e: Answer): string {
|
|
51
|
+
const files = e.changed.length ? `changed ${e.changed.slice(0, 4).join(", ")}${e.changed.length > 4 ? ` +${e.changed.length - 4} more` : ""}` : ""
|
|
52
|
+
return `Scribe (attempt ${e.attempt}): ${[e.notes ? clip(e.notes, 300) : "", files].filter(Boolean).join(" · ")}`
|
|
53
|
+
}
|
package/src/code/loop.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
standardsFixPrompt,
|
|
39
39
|
type AgentReport,
|
|
40
40
|
type ExistingTest,
|
|
41
|
+
type ReviewIssue,
|
|
41
42
|
} from "./prompt.ts"
|
|
42
43
|
import { openReviewer, type Reviewer } from "./review.ts"
|
|
43
44
|
import { runTestCommand } from "./testrun.ts"
|
|
@@ -254,6 +255,9 @@ export async function runCodeAgent(opts: {
|
|
|
254
255
|
let spent: Usage = { input: 0, output: 0 }
|
|
255
256
|
const attemptLog: string[] = []
|
|
256
257
|
let nextTask: string | null = null
|
|
258
|
+
/** Why the next attempt runs and what it is to fix, as the shell shows it: "the reviewer asked for changes", its issues. */
|
|
259
|
+
let next: { why: string; fixing: string[] } = { why: "", fixing: [] }
|
|
260
|
+
const issueText = (i: ReviewIssue) => `${i.file ? `${i.file}: ` : ""}${i.problem}${i.fix ? ` — fix: ${i.fix}` : ""}`
|
|
257
261
|
let notesText: string | undefined = known ? notesSection(known) : undefined
|
|
258
262
|
// The repository's test setup, read by ForgeQE without a model; Scribe starts from it instead of a reading turn.
|
|
259
263
|
const repoProfile = buildProfile(repo)
|
|
@@ -374,7 +378,7 @@ export async function runCodeAgent(opts: {
|
|
|
374
378
|
session = await (opts.open || openSession)(sessionOptions())
|
|
375
379
|
result.sessionId = session.sessionId
|
|
376
380
|
saveSession()
|
|
377
|
-
emit({ type: "code", kind: "
|
|
381
|
+
emit({ type: "code", kind: "attempt", attempt, max: allowed, why: next.why, fixing: next.fixing })
|
|
378
382
|
prompt = nextAttemptPrompt({
|
|
379
383
|
handoff,
|
|
380
384
|
notes: [profileText, notesText].filter(Boolean).join("\n\n") || undefined,
|
|
@@ -417,6 +421,8 @@ export async function runCodeAgent(opts: {
|
|
|
417
421
|
break
|
|
418
422
|
}
|
|
419
423
|
if (reply.report) report = { ...report, ...reply.report }
|
|
424
|
+
// What Scribe says it did this time, so a retry is not only "Editing …" lines.
|
|
425
|
+
if (reply.report && (reply.report.notes || reply.report.changed?.length)) emit({ type: "code", kind: "answer", attempt, ...(reply.report.notes ? { notes: reply.report.notes } : {}), changed: reply.report.changed || [] })
|
|
420
426
|
result.notes = report.notes || reply.text.slice(0, 500)
|
|
421
427
|
if (reply.error && FATAL_ERRORS.some((name) => reply.error!.startsWith(name))) {
|
|
422
428
|
result.notes = reply.error
|
|
@@ -477,6 +483,7 @@ export async function runCodeAgent(opts: {
|
|
|
477
483
|
emit({ type: "code", kind: "test", ok: false, attempt, text: `${report.test_file} exited 0 in ${secs}s but opened no browser (attempt ${attempt}/${maxAttempts})` })
|
|
478
484
|
attemptLog.push(attemptLine(attempt, "exited 0 but opened no browser", "", report.notes))
|
|
479
485
|
nextTask = noBrowserPrompt(report.command)
|
|
486
|
+
next = { why: "the test passed without opening a browser", fixing: ["Make the test drive a real browser, as the run did"] }
|
|
480
487
|
continue
|
|
481
488
|
}
|
|
482
489
|
if (run.ok) {
|
|
@@ -501,6 +508,8 @@ export async function runCodeAgent(opts: {
|
|
|
501
508
|
result.review = verdict.review
|
|
502
509
|
const correctness = verdict.review.issues.filter((i) => i.kind !== "standards")
|
|
503
510
|
const standards = verdict.review.issues.filter((i) => i.kind === "standards")
|
|
511
|
+
// The verdict as the reviewer gave it: with only coding-standard issues the test is approved and gets a tidy-up.
|
|
512
|
+
emit({ type: "code", kind: "review", attempt, max: allowed, approved: verdict.review.approved || !correctness.length, summary: verdict.review.summary, issues: verdict.review.issues })
|
|
504
513
|
// Coding standards alone never fail a test that works and tests the goal.
|
|
505
514
|
if (tidying && correctness.length) {
|
|
506
515
|
// The tidy-up changed what the test does: back to the version that passed.
|
|
@@ -515,6 +524,7 @@ export async function runCodeAgent(opts: {
|
|
|
515
524
|
emit({ type: "code", kind: "test", ok: true, attempt, text: `${report.test_file} passed and tests the goal; the reviewer found ${standards.length} coding-standard ${standards.length === 1 ? "issue" : "issues"}, so the agent fixes ${standards.length === 1 ? "it" : "them"} in one more round (the passing version is kept if that breaks it)` })
|
|
516
525
|
attemptLog.push(attemptLine(attempt, "passed and tests the goal; coding-standard issues to fix", standards.map((i) => i.problem).join(" "), report.notes))
|
|
517
526
|
nextTask = standardsFixPrompt(standards)
|
|
527
|
+
next = { why: "the reviewer approved the test; one round for its coding-standard issues", fixing: standards.map(issueText) }
|
|
518
528
|
continue
|
|
519
529
|
}
|
|
520
530
|
result.status = "passed"
|
|
@@ -526,8 +536,7 @@ export async function runCodeAgent(opts: {
|
|
|
526
536
|
emit({ type: "code", kind: "test", ok: true, attempt, text: `${report.test_file} passed and was approved (attempt ${attempt}/${allowed})${standards.length ? `; ${standards.length} coding-standard ${standards.length === 1 ? "issue" : "issues"} left in the notes` : ""}` })
|
|
527
537
|
break
|
|
528
538
|
}
|
|
529
|
-
|
|
530
|
-
emit({ type: "code", kind: "test", ok: false, attempt, text: `review found ${n} ${n === 1 ? "issue" : "issues"}: ${verdict.review.summary} (attempt ${attempt}/${maxAttempts})` })
|
|
539
|
+
// The review event above says what was found; the rejection itself is its verdict.
|
|
531
540
|
// A second rejection usually means the goal and the page disagree (a test case the page cannot satisfy), not a
|
|
532
541
|
// fix the next attempt will find: stop and report the issues instead of spending the remaining attempts.
|
|
533
542
|
if (++rejections >= MAX_REJECTIONS) {
|
|
@@ -537,6 +546,7 @@ export async function runCodeAgent(opts: {
|
|
|
537
546
|
}
|
|
538
547
|
attemptLog.push(attemptLine(attempt, "passed; the reviewer asked for changes", verdict.review.summary, report.notes))
|
|
539
548
|
nextTask = reviewFixPrompt(verdict.review.summary, verdict.review.issues)
|
|
549
|
+
next = { why: "the reviewer asked for changes", fixing: verdict.review.issues.map(issueText) }
|
|
540
550
|
continue
|
|
541
551
|
}
|
|
542
552
|
if (tidying) {
|
|
@@ -558,11 +568,13 @@ export async function runCodeAgent(opts: {
|
|
|
558
568
|
emit({ type: "code", kind: "test", ok: false, attempt, text: `${report.test_file} failed the same way as the last attempt, ${why} (attempt ${attempt}/${maxAttempts})` })
|
|
559
569
|
attemptLog.push(attemptLine(attempt, `failed, ${why}, the same way as before`, firstError(run.output), report.notes))
|
|
560
570
|
nextTask = repeatedFailurePrompt(sameFailures + 1, report.command, run.exitCode, run.output, files) + mismatch + switchLiveOn()
|
|
571
|
+
next = { why: `the test failed the same way again (${why}); Scribe looks at the failing step first`, fixing: [firstError(run.output)].filter(Boolean) }
|
|
561
572
|
continue
|
|
562
573
|
}
|
|
563
574
|
emit({ type: "code", kind: "test", ok: false, attempt, text: `${report.test_file} failed, ${why} (attempt ${attempt}/${maxAttempts})` })
|
|
564
575
|
attemptLog.push(attemptLine(attempt, `failed, ${why}`, firstError(run.output), report.notes))
|
|
565
576
|
nextTask = fixPrompt(attempt, report.command, run.exitCode, run.output, files) + mismatch
|
|
577
|
+
next = { why: `the test failed (${why})`, fixing: [firstError(run.output)].filter(Boolean) }
|
|
566
578
|
}
|
|
567
579
|
keepSession = result.status === "stopped" || runtimeFailed
|
|
568
580
|
if (report.test_name) result.testName = report.test_name
|
package/src/code/runtime.ts
CHANGED
|
@@ -539,6 +539,8 @@ export async function openSession<R = AgentReport>(opts: {
|
|
|
539
539
|
mcp?: Record<string, unknown>
|
|
540
540
|
/** Tool calls not to show as ⚙ lines (the caller shows them itself). */
|
|
541
541
|
hideTool?: (tool: string) => boolean
|
|
542
|
+
/** Every finished tool call with what it returned, shown or not: the evidence reports are built from. */
|
|
543
|
+
onToolResult?: (r: { tool: string; input: Record<string, unknown>; output: string; ok: boolean }) => void
|
|
542
544
|
title?: string
|
|
543
545
|
protect?: Protect
|
|
544
546
|
/** Stop the turn when one shell command has failed this many times since the last resetStall. */
|
|
@@ -672,6 +674,7 @@ export async function openSession<R = AgentReport>(opts: {
|
|
|
672
674
|
emit({ type: "code", kind: "start", tool: part.tool, id: part.id, text: toolLine(opts.repo, part.tool, part.state.input || {}, part.state.title) })
|
|
673
675
|
}
|
|
674
676
|
if (part?.type === "tool" && (part.state?.status === "completed" || part.state?.status === "error")) {
|
|
677
|
+
opts.onToolResult?.({ tool: part.tool, input: part.state.input || {}, output: String(part.state.output ?? ""), ok: part.state.status === "completed" })
|
|
675
678
|
if (part.tool === "StructuredOutput" && part.state.status === "completed" && !turn.structured) {
|
|
676
679
|
turn.structured = part.state.input || null
|
|
677
680
|
}
|
package/src/detail.ts
CHANGED
|
@@ -27,7 +27,8 @@ export function shows(event: AgentEvent | CodeEvent, detail: Detail): boolean {
|
|
|
27
27
|
case "memory":
|
|
28
28
|
return false
|
|
29
29
|
case "code":
|
|
30
|
-
|
|
30
|
+
// Milestones, and the exchange with the reviewer: why a test was rejected and what Scribe fixes.
|
|
31
|
+
if (event.kind === "info" || event.kind === "test" || event.kind === "review" || event.kind === "attempt" || event.kind === "answer") return true
|
|
31
32
|
if (event.kind === "tool") return !event.ok || WRITES_FILE.test(event.text)
|
|
32
33
|
return false
|
|
33
34
|
default:
|