@capagents/forgeqe 1.1.6 → 1.1.8

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 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 release status across projects, open defects, or pipeline health, as a Markdown file and an HTML page in the reports folder, with a summary in the chat and links to open them. | `write_report` | [docs](#reports-from-the-agent) |
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: "create a release report for all projects", "write up the open defects", "summarise this sprint's test results as a report". It gathers the facts with its tools (Azure DevOps, Jira, the runs it made), then saves the report with `write_report` as a Markdown file and an HTML page in the reports folder (`export.out_dir`, `.forgeqe/reports/` by default):
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/release-status-all-projects-20261009-001512/
397
- report.md # to edit, paste into a wiki, or attach
398
- report.html # to open or share; light and dark, tables scroll on a phone
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 chat gets a short summary and a card with both paths as links, and **Open the report** appears as a next step. The report holds only what the agent read in that request. 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`).
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
 
@@ -463,8 +473,8 @@ The chat gets a short summary and a card with both paths as links, and **Open th
463
473
  The Activities column shows what the agents do as they do it:
464
474
 
465
475
  - the status line: who is working, a progress bar for the plan or a suite's cases, for how long, and **■ Stop**; under it, the whole width, what is happening now (*Agent: Azure DevOps API · QI-Payments/_apis/build/builds…*);
466
- - **Plan**: the agent's own task list as soon as it writes one, then each browser step or suite case: ○ pending, ◐ in progress, ✓ done, ✗ failed and why. ForgeQE's agent writes a plan for any request of three steps or more and updates it as it goes;
467
- - **Activity**: **Now** (the task in progress) and **Next** (the next task), then every tool call, browser step, and note. A call shows ◐ the moment it starts and becomes ✓ or ✗ in the same row when it ends, so a long API query or test run is never invisible. Reads and searches that the conversation leaves out at the brief detail level are listed here. A call takes one row and a note up to three; focus the panel to scroll through everything or filter it.
476
+ - **Plan**: the agent's own task list as soon as it writes one, then each browser step or suite case: ○ pending, a spinner while in progress, ✓ done, ✗ failed and why. ForgeQE's agent writes a plan for any request of three steps or more and updates it as it goes;
477
+ - **Activity**: **Now** (the task in progress) and **Next** (the next task), then every tool call, browser step, and note. A call shows a spinner the moment it starts and becomes ✓ or ✗ in the same row when it ends, so a long API query or test run is never invisible. Reads and searches that the conversation leaves out at the brief detail level are listed here. A call takes one row and a note up to three; focus the panel to scroll through everything or filter it. Each request starts under a line with its time and what you asked (*── 14:02 can you generate a defect report… ──*) and ends with how it went (*✓ Done in 1m 12s · 16 activities*, or ✗ Failed, ■ Stopped, ? Waiting for your answer), so one request's activities are never read as another's; earlier requests stay above, to scroll back to. The Plan shows the current request only.
468
478
 
469
479
  Over the conversation: **Files** (each file read ◉, written ✚, or edited ✎, newest first), **Context** (how full the model's context window is, and the skills, product knowledge, domain, and MCP servers in it), and **Tokens** (total, input, output, cached). When that half is under 80 columns they fold into one line: *Files 4 (1 changed) · Context 26% · Skills 2 · 52.9K tokens*.
470
480
 
@@ -719,8 +719,8 @@ Add a GitHub Actions workflow that runs tests/cases.yaml on every PR, and make i
719
719
  <p>The Activities column shows what the agents do as they do it:</p>
720
720
  <ul>
721
721
  <li>the status line: who is working, a progress bar for the plan or a suite's cases, for how long, and <strong>■ Stop</strong>; under it, the whole width, what is happening now (<em>Agent: Azure DevOps API · QI-Payments/_apis/build/builds…</em>);</li>
722
- <li><strong>Plan</strong>: the agent's own task list as soon as it writes one, then each browser step or suite case: ○ pending, ◐ in progress, ✓ done, ✗ failed and why. ForgeQE's agent writes a plan for any request of three steps or more and updates it as it goes;</li>
723
- <li><strong>Activity</strong>: <strong>Now</strong> (the task in progress) and <strong>Next</strong> (the next task), then every tool call, browser step, and note. A call shows ◐ the moment it starts and becomes ✓ or ✗ in the same row when it ends, so a long API query or test run is never invisible. Reads and searches that the conversation leaves out at the brief detail level are listed here. A call takes one row and a note up to three; focus the panel to scroll through everything or filter it.</li>
722
+ <li><strong>Plan</strong>: the agent's own task list as soon as it writes one, then each browser step or suite case: ○ pending, a spinner while in progress, ✓ done, ✗ failed and why. ForgeQE's agent writes a plan for any request of three steps or more and updates it as it goes;</li>
723
+ <li><strong>Activity</strong>: <strong>Now</strong> (the task in progress) and <strong>Next</strong> (the next task), then every tool call, browser step, and note. A call shows a spinner the moment it starts and becomes ✓ or ✗ in the same row when it ends, so a long API query or test run is never invisible. Reads and searches that the conversation leaves out at the brief detail level are listed here. A call takes one row and a note up to three; focus the panel to scroll through everything or filter it. Each request starts under a line with its time and what you asked (<em>── 14:02 can you generate a defect report… ──</em>) and ends with how it went (<em>✓ Done in 1m 12s · 16 activities</em>, or ✗ Failed, ■ Stopped, ? Waiting for your answer), so one request's activities are never read as another's; earlier requests stay above, to scroll back to. The Plan shows the current request only.</li>
724
724
  </ul>
725
725
  <p>Over the conversation: <strong>Files</strong> (each file read ◉, written ✚, or edited ✎, newest first), <strong>Context</strong> (how full the model's context window is, and the skills, product knowledge, domain, and MCP servers in it), and <strong>Tokens</strong> (total, input, output, cached). When that half is under 80 columns they fold into one line: <em>Files 4 (1 changed) · Context 26% · Skills 2 · 52.9K tokens</em>.</p>
726
726
  <p><strong>Buttons.</strong> Under the input: <strong>✦ Refine</strong> (Ctrl-R; it fills in once there is text to refine), <strong>⌕ Search</strong> (Ctrl-P, the command palette), and <strong>▦ Panels</strong> (Ctrl-O); while a request runs, <strong>■ Stop</strong> (Esc) takes Refine's place. With the panels open, <strong>▦ Focus panels</strong> (Shift-Tab) and <strong>✕ Close</strong> (Ctrl-O) are in their status line. On a narrow terminal, <strong>◧ Details</strong> (Ctrl-O) and <strong>⇄ Profile</strong> (Tab) are under the input. Each is clickable.</p>
@@ -1804,22 +1804,31 @@ forgeqe visual approve --page /home # make this run's /home the baseline (asks
1804
1804
  </article>
1805
1805
  <article class="spec feat" id="cap-reports">
1806
1806
  <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 release status across projects, open defects, or pipeline health, as a Markdown file and an HTML page in the reports folder, with a summary in the chat and links to open them.</p>
1807
+ <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
1808
  <div class="facts">
1809
1809
  <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
1810
  <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>The report holds what the agent read with its tools in this request; it does not invent figures, and it does not publish the page anywhere unless the reports folder is served (notify.report_url).</li></ul></div>
1811
+ <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
1812
  <div class="fact"><span class="mono">Tools</span><p class="tools"><code>write_report</code></p></div>
1813
1813
  </div>
1814
1814
  <div class="detail"><span class="mono">In detail</span>
1815
1815
  <section class="doc" id="doc-how_to_use-reports-from-the-agent">
1816
1816
  <h4>Reports from the agent</h4>
1817
- <div class="prose"><p>Ask for a report and ForgeQE's agent writes one you can share: &quot;create a release report for all projects&quot;, &quot;write up the open defects&quot;, &quot;summarise this sprint's test results as a report&quot;. It gathers the facts with its tools (Azure DevOps, Jira, the runs it made), then saves the report with <code>write_report</code> as a Markdown file and an HTML page in the reports folder (<code>export.out_dir</code>, <code>.forgeqe/reports/</code> by default):</p>
1818
- <pre><code>.forgeqe/reports/release-status-all-projects-20261009-001512/
1819
- report.md # to edit, paste into a wiki, or attach
1820
- report.html # to open or share; light and dark, tables scroll on a phone
1821
- </code></pre>
1822
- <p>The chat gets a short summary and a card with both paths as links, and <strong>Open the report</strong> appears as a next step. The report holds only what the agent read in that request. To post it, ask for it (&quot;post the report to the release channel&quot;) once <code>notify.channels</code> is set up; the reports folder is linked when it is served (<code>notify.report_url</code>).</p>
1817
+ <div class="prose"><p>Ask for a report and ForgeQE's agent writes one you can share: &quot;generate a defect report for all open defects&quot;, &quot;create a release report for all projects&quot;, &quot;summarise this sprint's test results as a report&quot;. Every table in it is built from <strong>evidence</strong>, not typed by the model:</p>
1818
+ <ul>
1819
+ <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>
1820
+ <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 &quot;all&quot; of something.</li>
1821
+ <li>The agent writes only the summary paragraph.</li>
1822
+ </ul>
1823
+ <p>The report goes to the reports folder (<code>export.out_dir</code>, <code>.forgeqe/reports/</code> by default):</p>
1824
+ <pre><code>.forgeqe/reports/open-defects-all-projects-20261013-101512/
1825
+ report.html # one file: summary, filters, charts, sortable table, Sources
1826
+ report.md # the same, to edit, paste into a wiki, or attach
1827
+ open-defects.csv # each dataset's rows
1828
+ evidence/E1.json # the raw result of each tool call the report used
1829
+ </code></pre>
1830
+ <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>
1831
+ <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 (&quot;post the report to the release channel&quot;) once <code>notify.channels</code> is set up; the reports folder is linked when it is served (<code>notify.report_url</code>).</p>
1823
1832
  </div>
1824
1833
  </section>
1825
1834
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capagents/forgeqe",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
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 and an HTML page in the reports folder.
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 collect the facts with your tools and save the report with write_report.
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.
@@ -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 release status across projects, open defects, or pipeline health, as a Markdown file and an HTML page in the reports folder, with a summary in the chat and links to open them.",
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: ["The report holds what the agent read with its tools in this request; it does not invent figures, and it does not publish the page anywhere unless the reports folder is served (notify.report_url)."],
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 WIQL_TOP are read back with their main fields in one batch read.
45
+ * the first `top` are read back with their main fields in batch reads.
24
46
  */
25
- async function adoWiql(cfg: SourcesConfig, wiql: string, project?: string): Promise<unknown> {
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=${WIQL_TOP}`,
53
+ `${base}/_apis/wit/wiql?api-version=7.1&$top=${top}`,
32
54
  { auth, body: { query: wiql } },
33
55
  )
34
- const ids = [...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").slice(0, WIQL_TOP)
35
- if (!ids.length) return { count: 0, items: [] }
36
- const batch = await http<{ value: { id: number; fields: Record<string, unknown> }[] }>(`${org}/_apis/wit/workitemsbatch?api-version=7.1`, { auth, body: { ids, fields: WIQL_FIELDS } })
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
- items: batch.data.value.map((w) => ({
41
- id: w.id,
42
- type: w.fields["System.WorkItemType"],
43
- title: w.fields["System.Title"],
44
- state: w.fields["System.State"],
45
- assignedTo: name(w.fields["System.AssignedTo"]) || undefined,
46
- changed: w.fields["System.ChangedDate"],
47
- iteration: w.fields["System.IterationPath"],
48
- })),
49
- note: `Read each one in full with path <project>/_apis/wit/workitems/<id>?$expand=all.${ids.length === WIQL_TOP ? ` Only the first ${WIQL_TOP} are listed; narrow the query for the rest.` : ""}`,
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 clipJson((await http(url, { auth })).data)
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 clipJson((await http(resolve(q.path, [j.url], "Jira"), { auth: j.auth })).data)
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 clipJson((await http(resolve(q.path, [c.url], "Confluence"), { auth: c.auth })).data)
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 clipJson(await xrayGraphql(cfg, await xrayCloudToken(cfg), q.query, q.variables || {}))
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.`)
@@ -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
  }