@capagents/forgeqe 1.3.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/HOW_TO_USE.md +373 -9
  2. package/README.md +55 -4
  3. package/docs/ARCHITECTURE.md +62 -1
  4. package/docs/CONTAINERS.md +227 -0
  5. package/docs/COST_ARCHITECTURE.md +32 -0
  6. package/docs/DESKTOP-CI.md +87 -0
  7. package/docs/DESKTOP.md +387 -0
  8. package/docs/MOBILE.md +228 -0
  9. package/docs/feature-test-prompts.md +25 -0
  10. package/docs/security-data-flow.html +440 -3
  11. package/docs/user-guide.html +797 -415
  12. package/env.example.yaml +67 -0
  13. package/forgeqe.example.yaml +56 -1
  14. package/llms.example.json +7 -0
  15. package/package.json +4 -1
  16. package/prompts/brain.md +31 -0
  17. package/prompts/find-tests.md +1 -0
  18. package/py/forgeqe_desktop/__init__.py +1 -0
  19. package/py/forgeqe_desktop/__main__.py +3 -0
  20. package/py/forgeqe_desktop/apps.py +80 -0
  21. package/py/forgeqe_desktop/checks.py +168 -0
  22. package/py/forgeqe_desktop/cli.py +90 -0
  23. package/py/forgeqe_desktop/handhooks.py +293 -0
  24. package/py/forgeqe_desktop/handrecord.py +239 -0
  25. package/py/forgeqe_desktop/heal.py +107 -0
  26. package/py/forgeqe_desktop/models.py +208 -0
  27. package/py/forgeqe_desktop/pywinauto_driver.py +217 -0
  28. package/py/forgeqe_desktop/record.py +150 -0
  29. package/py/forgeqe_desktop/replay.py +345 -0
  30. package/py/forgeqe_desktop/service.py +356 -0
  31. package/py/forgeqe_desktop/session.py +60 -0
  32. package/py/forgeqe_desktop/ufo_bridge.py +86 -0
  33. package/py/forgeqe_desktop/ufo_runner.py +305 -0
  34. package/py/forgeqe_desktop/visual.py +112 -0
  35. package/py/forgeqe_desktop/workflow.py +338 -0
  36. package/py/forgeqe_engine/llm.py +54 -4
  37. package/py/forgeqe_engine/model_params.py +169 -0
  38. package/py/forgeqe_mobile/__init__.py +1 -0
  39. package/py/forgeqe_mobile/__main__.py +3 -0
  40. package/py/forgeqe_mobile/appium_driver.py +342 -0
  41. package/py/forgeqe_mobile/apps.py +84 -0
  42. package/py/forgeqe_mobile/cli.py +70 -0
  43. package/py/forgeqe_mobile/explore.py +203 -0
  44. package/py/forgeqe_mobile/hyperexecute.py +261 -0
  45. package/py/forgeqe_mobile/maestro.py +321 -0
  46. package/py/forgeqe_mobile/service.py +332 -0
  47. package/skills/testcontainers/SKILL.md +42 -0
  48. package/src/agents.ts +11 -2
  49. package/src/api/plan.ts +2 -0
  50. package/src/api/write.ts +2 -0
  51. package/src/auth/copilot.ts +130 -0
  52. package/src/auth/signin.ts +54 -1
  53. package/src/brain/index.ts +26 -4
  54. package/src/capabilities/guide.ts +2 -0
  55. package/src/capabilities/registry.ts +110 -2
  56. package/src/capabilities/status.ts +15 -0
  57. package/src/cases/rca.ts +2 -0
  58. package/src/childEnv.ts +4 -1
  59. package/src/cli/args.ts +3 -2
  60. package/src/cli/help.ts +24 -3
  61. package/src/cli/index.ts +47 -3
  62. package/src/code/find.ts +26 -0
  63. package/src/code/loop.ts +4 -1
  64. package/src/code/prompt.ts +2 -1
  65. package/src/code/prreview.ts +2 -0
  66. package/src/code/review.ts +6 -2
  67. package/src/code/runtime.ts +34 -3
  68. package/src/commands.ts +6 -1
  69. package/src/config.ts +159 -3
  70. package/src/containers/command.ts +68 -0
  71. package/src/containers/compose.ts +173 -0
  72. package/src/containers/config.ts +283 -0
  73. package/src/containers/mock.ts +156 -0
  74. package/src/containers/run.ts +362 -0
  75. package/src/containers/tests.ts +80 -0
  76. package/src/context.ts +33 -6
  77. package/src/copilot.ts +155 -0
  78. package/src/desktop/bridge.ts +229 -0
  79. package/src/desktop/command.ts +350 -0
  80. package/src/desktop/edit.ts +217 -0
  81. package/src/desktop/junit.ts +35 -0
  82. package/src/desktop/report.ts +155 -0
  83. package/src/desktop/run.ts +252 -0
  84. package/src/desktop/setup.ts +134 -0
  85. package/src/engine/bridge.ts +12 -5
  86. package/src/engine/setup.ts +3 -3
  87. package/src/environments.ts +566 -0
  88. package/src/evals/domain.ts +1 -0
  89. package/src/explore/charters.ts +2 -0
  90. package/src/guardrails.ts +145 -0
  91. package/src/heal/loop.ts +4 -1
  92. package/src/heal/prompt.ts +2 -1
  93. package/src/init.ts +5 -2
  94. package/src/llm.ts +23 -2
  95. package/src/mcp/attachtools.ts +16 -4
  96. package/src/mcp/captools.ts +27 -0
  97. package/src/mcp/containertools.ts +212 -0
  98. package/src/mcp/desktoptools.ts +334 -0
  99. package/src/mcp/mobiletools.ts +132 -0
  100. package/src/mcp/reporttools.ts +17 -2
  101. package/src/mcp/repotests.ts +22 -3
  102. package/src/mcp/routing.ts +11 -5
  103. package/src/mcp/scope.ts +210 -0
  104. package/src/mcp/server.ts +80 -5
  105. package/src/mcp/tools.ts +38 -1
  106. package/src/mobile/command.ts +245 -0
  107. package/src/mobile/run.ts +226 -0
  108. package/src/pack/pipeline.ts +44 -2
  109. package/src/refine.ts +3 -0
  110. package/src/report/html.ts +3 -0
  111. package/src/report/template.html +48 -48
  112. package/src/run.ts +7 -3
  113. package/src/tokenomics/ledger.ts +3 -1
  114. package/src/tui/App.tsx +23 -0
  115. package/src/usage.ts +1 -1
@@ -30,13 +30,17 @@ export type Requirement =
30
30
  | { kind: "command"; command: "gh" | "az" | "git" | "npx"; optional?: boolean; why?: string }
31
31
  /** Something only the request can supply (a repository, a pull request); stated, never checked. */
32
32
  | { kind: "input"; what: string }
33
+ /** Apps of this kind in the active environment (env.yaml) or forgeqe.yaml (desktop: or mobile:). */
34
+ | { kind: "apps"; app: "desktop" | "mobile"; optional?: boolean }
35
+ /** Docker or Podman on PATH (containers.runtime picks one). */
36
+ | { kind: "containers"; optional?: boolean }
33
37
  /** Alternatives: met when any one of them is (GitHub through gh, or Azure DevOps). */
34
38
  | { kind: "either"; of: Requirement[]; optional?: boolean }
35
39
 
36
40
  /** A heading in README.md or HOW_TO_USE.md, by its exact text (### headings without their parent). */
37
41
  export type DocRef = { doc: "README.md" | "HOW_TO_USE.md"; heading: string }
38
42
 
39
- export type Area = "Browser" | "Test cases" | "Automated tests" | "Pull requests" | "API" | "Quality" | "Release" | "Operations" | "Systems" | "Setup"
43
+ export type Area = "Browser" | "Desktop" | "Mobile" | "Test cases" | "Automated tests" | "Pull requests" | "API" | "Quality" | "Release" | "Operations" | "Systems" | "Setup"
40
44
 
41
45
  export type Capability = {
42
46
  /** Stable id the agent asks for details by. */
@@ -47,6 +51,11 @@ export type Capability = {
47
51
  summary: string
48
52
  /** The tools that carry it out (forgeqe_ prefix left off). */
49
53
  tools: string[]
54
+ /**
55
+ * Words and phrases in a request that mean this capability (lower case). ForgeQE's agent gets a capability's tools
56
+ * for a turn when the request matches them (src/mcp/scope.ts); its examples must match, a test checks it.
57
+ */
58
+ keywords: string[]
50
59
  /** How a user might ask for it, in their words. */
51
60
  examples: string[]
52
61
  requires: Requirement[]
@@ -70,6 +79,7 @@ export const CAPABILITIES: Capability[] = [
70
79
  summary:
71
80
  "Carries out a plain-language goal in a real browser one step at a time and verifies the outcome, with screenshots, a report, and the steps it took. The browser comes from forgeqe.yaml: local Chrome or Chromium by default, or BrowserStack or TestMu AI/LambdaTest when configured. Page memory replays steps that already worked on a site, so repeat runs are faster and cheaper.",
72
81
  tools: ["browser_run", "run_details", "page_memory", "browser_devices"],
82
+ keywords: ["browser", "website", "web site", "web app", "page", "url", "go to", "open the", "click", "search", "chrome", "edge", "firefox", "device", "testmu", "browserstack", "sign up", "checkout", "site", "form", "cart"],
73
83
  examples: [
74
84
  "Go to booking.com and search hotels in Mumbai for next weekend",
75
85
  "What does ForgeQE know about the checkout page?",
@@ -88,6 +98,7 @@ export const CAPABILITIES: Capability[] = [
88
98
  title: "Saved logins for the application under test",
89
99
  summary: "Signs the browser agent in with saved credentials (including TOTP) and reuses the session; a user can also sign in by hand once in a browser.",
90
100
  tools: ["logins"],
101
+ keywords: ["login", "log in", "sign in", "signed in", "session", "credential", "password", "saved login", "forget the saved"],
91
102
  examples: ["Sign in to the admin site and keep the session", "Forget the saved session for shop.example.com"],
92
103
  requires: [],
93
104
  docs: [{ doc: "HOW_TO_USE.md", heading: "Signing in" }],
@@ -99,6 +110,7 @@ export const CAPABILITIES: Capability[] = [
99
110
  summary:
100
111
  "Reads test cases from Azure DevOps, Jira, Xray, TestRail, Zephyr Scale, qTest, a file, or pasted text, runs each in a real browser with a verdict per step, writes report.html and junit.xml, posts results back, and files or updates bugs for failures.",
101
112
  tools: ["find_test_cases", "run_test_cases"],
113
+ keywords: ["test case", "cases", "test plan", "suite", "manual test", "run case", "execute case", "run the case"],
102
114
  examples: ["Run SHOP-12 and SHOP-14 from Jira, two browsers at once", "Run test plan 12 suite 34 from Azure DevOps and don't post results"],
103
115
  requires: [
104
116
  { kind: "live_model" },
@@ -120,6 +132,7 @@ export const CAPABILITIES: Capability[] = [
120
132
  summary:
121
133
  "Checks test cases before they run (vague or missing expected results, missing test data, placeholders, duplicates) against the story, feature, and epic they test, then rewrites Azure DevOps or Jira cases in place once the user agrees.",
122
134
  tools: ["check_test_cases", "update_test_case"],
135
+ keywords: ["good enough", "quality", "improve", "fix the expected", "expected result", "check the cases", "check cases", "review the cases", "rewrite"],
123
136
  examples: ["Are the cases in test plan 12 good enough to run?", "Fix the expected results of case 4512"],
124
137
  requires: [{ kind: "system", anyOf: ["azure_devops", "jira"], optional: true, why: "to rewrite cases in place" }],
125
138
  docs: [{ doc: "HOW_TO_USE.md", heading: "Checking test cases before they run" }],
@@ -131,6 +144,7 @@ export const CAPABILITIES: Capability[] = [
131
144
  summary:
132
145
  "Writes test cases from a story's acceptance criteria and saves them in Azure DevOps or Jira, linked to the story. Reads the cases already linked first, so asking again adds only what is missing and updates cases in place instead of duplicating them; plans end-to-end journeys from the existing cases.",
133
146
  tools: ["story_test_cases", "save_test_cases", "e2e_journey"],
147
+ keywords: ["write test cases", "test cases for", "story", "acceptance criteria", "journey", "end-to-end", "generate cases", "save them"],
134
148
  examples: ["Write test cases for story 1234 and save them", "Build an end-to-end journey from story 1234's cases"],
135
149
  requires: [{ kind: "system", anyOf: ["azure_devops", "jira"] }],
136
150
  docs: [{ doc: "HOW_TO_USE.md", heading: "Where cases come from" }],
@@ -146,6 +160,7 @@ export const CAPABILITIES: Capability[] = [
146
160
  summary:
147
161
  "Every agent works as an expert in the product's industry would, automatically: the risks the industry is known for and the obligations of its regulations and standards (for example DORA, PSD2, GDPR, ISO 27001), declared in forgeqe.yaml or worked out from the story and the application. It applies to analysing requirements, writing and checking test cases, exploration, writing, reviewing, and healing test code, API tests, finding tests, and pull request reviews, labelled so coverage can be traced and citing only the control ids in the team's compliance file. forgeqe eval domain measures how well the agent does it.",
148
162
  tools: [],
163
+ keywords: ["gdpr", "dora", "psd2", "hipaa", "pci", "sox", "iso 27001", "compliance", "regulation", "obligation"],
149
164
  examples: ["Write test cases for story 1234, covering our DORA and PSD2 obligations", "Which GDPR obligations do these cases miss?"],
150
165
  requires: [{ kind: "domain", optional: true }],
151
166
  limits: ["Designs and checks tests for regulatory obligations; it does not certify compliance or replace an audit.", "Cites control or article ids only from the team's compliance file, never from memory."],
@@ -158,11 +173,86 @@ export const CAPABILITIES: Capability[] = [
158
173
  summary:
159
174
  "Explores a story in real browsers beyond its scripted cases (edge cases, invalid input, boundaries, roles, state), confirms each defect by reproducing it, proposes test cases, and files bugs once the user agrees.",
160
175
  tools: ["explore_story"],
176
+ keywords: ["explore", "exploratory", "find bugs", "beyond the test cases", "charter"],
161
177
  examples: ["Explore story 1234 and find bugs", "Test this feature beyond the test cases"],
162
178
  requires: [{ kind: "live_model" }, { kind: "system", anyOf: ["azure_devops", "jira"], optional: true, why: "to read the story and file bugs" }],
163
179
  docs: [{ doc: "HOW_TO_USE.md", heading: "Exploratory testing from a story" }],
164
180
  playbook: [{ heading: "Exploratory testing", when: "explore a story or feature past its scripted cases, or find bugs in it" }],
165
181
  },
182
+ {
183
+ id: "test-apps",
184
+ area: "Setup",
185
+ title: "Test environments and their apps",
186
+ summary:
187
+ "Knows the test environments in env.yaml (QA, UAT, …) and each one's apps: web apps and APIs by URL, Windows desktop apps, and mobile apps, with what each is for. Every agent works from the active one; switches environment on request, picks the apps a scenario needs, and carries values such as an order number from one app to the next.",
188
+ tools: ["test_apps", "use_environment"],
189
+ keywords: ["environment", "env", "qa", "uat", "staging", "apps", "which apps"],
190
+ examples: ["Place an order in the shop, then check its invoice in Billing, in UAT", "Which apps can you test in QA?", "From now on, use the UAT environment"],
191
+ requires: [{ kind: "input", what: "env.yaml with the environments and apps (forgeqe init writes one)" }],
192
+ limits: [],
193
+ docs: [{ doc: "HOW_TO_USE.md", heading: "Environments and apps" }],
194
+ playbook: [{ heading: "Apps and environments", when: "a request names an app or environment, or a scenario spans several apps" }],
195
+ },
196
+ {
197
+ id: "desktop-apps",
198
+ area: "Desktop",
199
+ title: "Automate Windows desktop apps",
200
+ summary:
201
+ "Carries out a plain-language goal in a Windows thick-client app with UFO² and saves it as a workflow with checks of the outcome, or records the steps a tester does by hand, with no model; replays saved workflows with no model, healing a step whose control moved, and fixes a workflow from chat (a timeout, a wait, a dialog to dismiss) after showing the change. Apps and the secrets they may type come from env.yaml or forgeqe.yaml desktop:.",
202
+ tools: ["desktop_explore", "desktop_record", "desktop_replay", "desktop_edit_workflow"],
203
+ keywords: ["desktop", "windows app", "thick client", "thick-client", ".exe", "record", "replay", "workflow", "ufo", "notepad", "win32", "wpf", "winforms"],
204
+ examples: [
205
+ "Create an invoice for Contoso in the Billing app and save it as a test",
206
+ "I'll show you the steps in Billing: record them",
207
+ "Replay the billing workflow with customer Fabrikam",
208
+ "Step 4 timed out while the list loaded: fix the workflow",
209
+ ],
210
+ requires: [
211
+ { kind: "live_model", optional: true },
212
+ { kind: "apps", app: "desktop" },
213
+ { kind: "input", what: "a Windows machine, and the app in forgeqe.yaml desktop.apps" },
214
+ ],
215
+ limits: ["Windows only, one run at a time per desktop", "Apps whose controls UI Automation cannot see (Citrix, some Java and custom-drawn apps) are not supported yet"],
216
+ docs: [{ doc: "HOW_TO_USE.md", heading: "Desktop apps (Windows)" }],
217
+ playbook: [{ heading: "Desktop apps", when: "automate or test a Windows desktop (thick-client) app" }],
218
+ },
219
+ {
220
+ id: "mobile-apps",
221
+ area: "Mobile",
222
+ title: "Automate Android and iOS apps",
223
+ summary:
224
+ "Carries out a plain-language goal in an Android or iOS app through Appium and saves it as a mobile workflow with checks of the outcome; replays workflows with no model on Appium (healing a moved control) or Maestro; runs the team's own Maestro flows after checking them against an allowlist. Apps come from env.yaml (kind: mobile) or forgeqe.yaml mobile:.",
225
+ tools: ["mobile_explore", "mobile_replay", "mobile_maestro"],
226
+ keywords: ["mobile", "android", "ios", "iphone", "ipad", "phone", "tablet", "apk", "ipa", "maestro", "appium", "emulator", "simulator", "app store", "play store"],
227
+ examples: ["Sign in to the courier app and accept the first delivery, and save it as a test", "Replay the courier login on Maestro", "Run our Maestro flows in .maestro/"],
228
+ requires: [
229
+ { kind: "live_model", optional: true },
230
+ { kind: "apps", app: "mobile" },
231
+ { kind: "input", what: "an Appium server with a device or emulator (or a device cloud), or Maestro installed; the app in env.yaml or forgeqe.yaml mobile.apps" },
232
+ ],
233
+ limits: ["Explore runs on Appium only", "Maestro replays do not heal moved controls or compare images (expect_image)", "On TestMu AI, Maestro (through HyperExecute) refuses workflows that type secrets; replay those there with --driver appium"],
234
+ docs: [{ doc: "HOW_TO_USE.md", heading: "Mobile apps (Android, iOS)" }],
235
+ playbook: [{ heading: "Mobile apps", when: "automate or test an Android or iOS app" }],
236
+ },
237
+ {
238
+ id: "containers",
239
+ area: "Operations",
240
+ title: "Services and mocks in Docker or Podman",
241
+ summary:
242
+ "Starts the services a test run needs (a database, a queue, the API under test) in Docker or Podman, as forgeqe.yaml declares them, and waits until each is ready; stops them, and shows their status and logs. Starts mocks of an API on WireMock, from the calls a browser run recorded or from an OpenAPI description, and can point an API app of the environment at the mock for the session.",
243
+ tools: ["services", "mock_service"],
244
+ keywords: ["docker", "podman", "container", "containers", "compose", "mock", "mocks", "mock service", "mock server", "mock api", "stub", "stubs", "wiremock", "testcontainers", "service up", "services up"],
245
+ examples: ["Start the database and the queue in Docker", "Start the services from our docker-compose file", "Mock the payments API from the last run", "Create a mock service from openapi.yaml and point order-api at it", "Show the logs of the db container"],
246
+ requires: [{ kind: "containers" }],
247
+ limits: [
248
+ "Starts only services declared in forgeqe.yaml containers.services; it never runs an image, flag or mount from a request",
249
+ "No privileged containers, host network, or access to the container runtime's socket; ports on 127.0.0.1 only; mounts read-only from inside the project",
250
+ "Mocks answer REST calls (WireMock); GraphQL, SOAP and gRPC are not mocked",
251
+ "Reads docker-compose files only for their services' image, ports, environment, bind mounts, command and depends_on; refuses build, privileged, network_mode, cap_add, devices and env_file; leaves named volumes out",
252
+ ],
253
+ docs: [{ doc: "HOW_TO_USE.md", heading: "Containers and mock services" }],
254
+ playbook: [{ heading: "Containers and mocks", when: "start, stop or inspect the project's services in Docker or Podman, or mock an API" }],
255
+ },
166
256
  {
167
257
  id: "write-tests",
168
258
  area: "Automated tests",
@@ -170,6 +260,7 @@ export const CAPABILITIES: Capability[] = [
170
260
  summary:
171
261
  "Scribe, the coding agent, writes a test for a passed browser run in the repository's own framework, page objects, and helpers; ForgeQE runs it until it passes and a reviewer approves it.",
172
262
  tools: ["write_test", "run_test"],
263
+ keywords: ["automated test", "automation", "automate", "playwright", "cypress", "selenium", "webdriverio", "write a test", "test code", "spec", "turn it into a test", "turn that run into", "scribe"],
173
264
  examples: ["Write a Playwright test for that run in ../app"],
174
265
  requires: [{ kind: "coding_agent" }, { kind: "input", what: "a repository to write the test in (default: the working directory)" }],
175
266
  docs: [{ doc: "HOW_TO_USE.md", heading: "6. Tests" }],
@@ -182,6 +273,7 @@ export const CAPABILITIES: Capability[] = [
182
273
  title: "Heal failing tests",
183
274
  summary: "Runs a repository's tests and updates the ones the application has outgrown against the live app, or reports a real application bug instead.",
184
275
  tools: ["heal_tests"],
276
+ keywords: ["heal", "failing test", "broken test", "fix the tests", "fix tests", "repair"],
185
277
  examples: ["Heal the failing tests in ../app"],
186
278
  requires: [{ kind: "coding_agent" }, { kind: "input", what: "a repository with tests" }],
187
279
  docs: [{ doc: "HOW_TO_USE.md", heading: "Fixing broken tests" }],
@@ -193,6 +285,7 @@ export const CAPABILITIES: Capability[] = [
193
285
  title: "Find and run existing tests",
194
286
  summary: "Finds the automated tests that match a feature, tags, ids, a folder, or a sprint, confirms the selection, and runs it once the user agrees, with a verdict per test.",
195
287
  tools: ["find_tests", "run_tests", "list_sprints"],
288
+ keywords: ["run the tests", "run tests", "run the", "existing tests", "tests in", "test suite", "npm test", "pytest", "added in sprint", "checkout tests", "tests added"],
196
289
  examples: ["Run the checkout tests", "Run the tests added in sprint 12"],
197
290
  requires: [
198
291
  { kind: "coding_agent" },
@@ -208,6 +301,7 @@ export const CAPABILITIES: Capability[] = [
208
301
  title: "Regression packs for a release",
209
302
  summary: "Builds a regression pack from what goes into a release (an Azure DevOps sprint or work items, a commit range, pull requests, or changes read from any connected tool): the tests those changes can break, each with why, plus risk-area and smoke tests, and the changes no test covers. Runs it locally, on TestMu AI, or in a pipeline, and compares each run with the last.",
210
303
  tools: ["build_regression_pack", "run_regression_pack", "list_regression_packs"],
304
+ keywords: ["regression", "pack", "which tests should", "release candidate", "merged this week"],
211
305
  examples: ["Build a regression pack for sprint 12 and run it on TestMu", "Which tests should we run for the PRs merged this week?"],
212
306
  requires: [
213
307
  { kind: "input", what: "a repository with tests" },
@@ -222,6 +316,7 @@ export const CAPABILITIES: Capability[] = [
222
316
  title: "Flaky test detection and quarantine",
223
317
  summary: "Finds tests that flip between passing and failing in the repository's run history, with their likely cause, and quarantines them once the user agrees.",
224
318
  tools: ["flaky_tests", "quarantine_tests"],
319
+ keywords: ["flaky", "quarantine", "intermittent", "unstable", "flakes"],
225
320
  examples: ["Which tests are flaky?", "Quarantine the flaky login test"],
226
321
  requires: [{ kind: "input", what: "a repository whose tests ForgeQE has run" }],
227
322
  docs: [{ doc: "HOW_TO_USE.md", heading: "Flaky tests" }],
@@ -232,6 +327,7 @@ export const CAPABILITIES: Capability[] = [
232
327
  title: "Review pull requests",
233
328
  summary: "Reviews an Azure DevOps or GitHub pull request from the local clone for bugs, security, requirements, and tests, and posts the review once the user agrees.",
234
329
  tools: ["review_pull_request", "post_pr_review"],
330
+ keywords: ["review pr", "review the pr", "review pull request", "pull request", "code review", "pr review", "review the change"],
235
331
  examples: ["Review PR 42 and focus on error handling"],
236
332
  requires: [
237
333
  { kind: "coding_agent" },
@@ -249,6 +345,7 @@ export const CAPABILITIES: Capability[] = [
249
345
  summary:
250
346
  "Works out which product areas, pages, requirements, and tests a pull request touches, picks the tests it can break, runs them, and posts the impact or results on the pull request once the user agrees.",
251
347
  tools: ["analyze_pr_impact", "select_pr_tests", "post_pr_test_results", "comment_on_pr"],
348
+ keywords: ["impact", "pr ", "pull request", "affected", "can break", "what does pr", "changed files"],
252
349
  examples: ["What does PR 42 impact?", "Run the tests PR 42 can break"],
253
350
  requires: [
254
351
  { kind: "input", what: "a local clone of the repository" },
@@ -266,6 +363,7 @@ export const CAPABILITIES: Capability[] = [
266
363
  summary:
267
364
  "Builds API contracts (status, schema, key fields) from the calls a browser run saw or from an API description (OpenAPI, Swagger, WSDL, GraphQL, .proto, gRPC reflection), and end-to-end API scenarios from runs, descriptions, test cases, or plain English. Runs them straight against the API without a browser, or writes them as tests in the repository. REST, GraphQL, SOAP, and gRPC.",
268
365
  tools: ["api_contracts", "run_api_contracts", "api_scenarios", "run_api_scenarios", "write_api_tests"],
366
+ keywords: ["api", "endpoint", "openapi", "swagger", "contract", "rest", "graphql", "postman", "request", "response"],
269
367
  examples: ["Turn the last run's API calls into tests", "Test the API in openapi.yaml", "Write API scenario tests for the order lifecycle"],
270
368
  requires: [{ kind: "coding_agent", optional: true }],
271
369
  limits: ["Functional API testing, not load or stress testing."],
@@ -281,6 +379,7 @@ export const CAPABILITIES: Capability[] = [
281
379
  title: "Accessibility and page performance",
282
380
  summary: "Checks every page a run visits against WCAG rules (axe-core) and measures its Core Web Vitals, against budgets, with trends over time per page.",
283
381
  tools: ["quality_report", "quality_trends"],
382
+ keywords: ["accessible", "accessibility", "a11y", "wcag", "performance", "lighthouse", "slow", "page speed", "web vitals", "load time"],
284
383
  examples: ["Is the checkout page accessible?", "Did page performance get worse this week?"],
285
384
  requires: [{ kind: "input", what: "a browser run (from browser_run or run_test_cases); new runs need a live model" }],
286
385
  limits: ["Per-page Core Web Vitals, not load testing under many users."],
@@ -294,6 +393,7 @@ export const CAPABILITIES: Capability[] = [
294
393
  title: "Visual comparison with baselines",
295
394
  summary: "Compares each page a run captures with its approved baseline and shows what changed; intended changes become the new baseline once the user agrees.",
296
395
  tools: ["visual_compare", "visual_approve"],
396
+ keywords: ["visual", "visually", "baseline", "looks different", "approve the new", "pixel", "screenshot diff", "layout"],
297
397
  examples: ["What changed visually since the last run?", "Approve the new header"],
298
398
  requires: [{ kind: "input", what: "a browser run (from browser_run or run_test_cases); new runs need a live model" }],
299
399
  docs: [{ doc: "HOW_TO_USE.md", heading: "Visual comparison" }],
@@ -305,6 +405,7 @@ export const CAPABILITIES: Capability[] = [
305
405
  summary:
306
406
  "Groups failures into defects, keeps their history across suites (new, known, gone, back again), and gives defect trends and where a change is likely to cause defects, with the cases to run first.",
307
407
  tools: ["defect_trends", "predict_defects"],
408
+ keywords: ["defect", "bugs keep", "trend", "predict", "coming back", "root cause", "rca", "hotspot", "since main", "changes since", "can the changes"],
308
409
  examples: ["Which defects keep coming back?", "What can the changes since main break?"],
309
410
  requires: [
310
411
  { kind: "input", what: "suites run into one folder (suite.out_dir)" },
@@ -322,6 +423,7 @@ export const CAPABILITIES: Capability[] = [
322
423
  title: "Requirement coverage and release readiness",
323
424
  summary: "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.",
324
425
  tools: ["release_readiness"],
426
+ keywords: ["release", "ready to release", "coverage", "go/no-go", "no-go", "readiness", "requirement coverage", "ship"],
325
427
  examples: ["Are we ready to release?", "Coverage of sprint 12"],
326
428
  requires: [{ kind: "system", anyOf: ["azure_devops", "jira"] }],
327
429
  docs: [{ doc: "HOW_TO_USE.md", heading: "Requirement coverage and release readiness" }],
@@ -332,6 +434,7 @@ export const CAPABILITIES: Capability[] = [
332
434
  title: "Scheduled monitoring with alerts",
333
435
  summary: "Runs checks on a schedule and alerts Teams or email when one keeps failing, and again when it recovers.",
334
436
  tools: ["monitor"],
437
+ keywords: ["monitor", "every minute", "every hour", "every day", "every night", "every 5 minutes", "every 15 minutes", "every 30 minutes", "schedule", "uptime", "alert", "cron", "minutes", "hourly", "nightly"],
335
438
  examples: ["Check the login flow every 15 minutes and alert Teams"],
336
439
  requires: [{ kind: "live_model" }, { kind: "monitor_alerts", optional: true }],
337
440
  docs: [{ doc: "HOW_TO_USE.md", heading: "9. Monitoring" }],
@@ -344,6 +447,7 @@ export const CAPABILITIES: Capability[] = [
344
447
  summary:
345
448
  "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.",
346
449
  tools: ["notify"],
450
+ keywords: ["teams", "email", "notify", "post the results", "channel", "tell the", "send the"],
347
451
  examples: ["Post the results to the QA channel", "Tell the release channel the verdict is NO-GO"],
348
452
  requires: [{ kind: "notify_channels" }],
349
453
  limits: ["Automatic notifications come from runs nobody is watching (forgeqe suite, forgeqe run, pipelines, the MCP server), not from the interactive shell, where the agent posts only when asked."],
@@ -356,6 +460,7 @@ export const CAPABILITIES: Capability[] = [
356
460
  summary:
357
461
  "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.",
358
462
  tools: ["write_report"],
463
+ keywords: ["report", "write up", "write-up", "summary", "share", "pdf", "html report"],
359
464
  examples: ["Create a release report for all projects", "Write up the open defects as a report I can share"],
360
465
  requires: [{ kind: "live_model" }],
361
466
  limits: [
@@ -371,6 +476,7 @@ export const CAPABILITIES: Capability[] = [
371
476
  title: "CI pipelines",
372
477
  summary: "Writes a GitHub Actions or Azure Pipelines workflow that runs ForgeQE, stores the secrets it needs, pushes a branch, and makes the pipeline green.",
373
478
  tools: ["push_branch", "set_ci_secret"],
479
+ keywords: ["pipeline", "github actions", "azure pipelines", "ci", "gitlab", "jenkins", "make it green", "ci secret", "workflow file", "on every pr"],
374
480
  examples: ["Add a GitHub Actions workflow that runs tests/cases.yaml on every PR, and make it green"],
375
481
  requires: [
376
482
  { kind: "command", command: "git" },
@@ -385,6 +491,7 @@ export const CAPABILITIES: Capability[] = [
385
491
  title: "Work with Azure DevOps, Jira, GitHub, and Confluence",
386
492
  summary: "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.",
387
493
  tools: ["query_api", "read_confluence", "create_ado_dashboard", "write_ado_wiki_page", "attach_to_work_item"],
494
+ keywords: ["jira", "azure devops", "ado", "confluence", "system", "github issue", "work item", "bug", "sprint", "dashboard", "wiki", "comment", "backlog", "issue", "ticket", "requirement"],
388
495
  examples: [
389
496
  "Show the open bugs in this sprint",
390
497
  "Read the checkout requirements from Confluence",
@@ -404,7 +511,8 @@ export const CAPABILITIES: Capability[] = [
404
511
  area: "Setup",
405
512
  title: "Setup, skills, and documentation",
406
513
  summary: "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.",
407
- tools: ["forgeqe_setup", "capabilities", "playbook"],
514
+ tools: ["forgeqe_setup", "capabilities", "playbook", "use_capabilities"],
515
+ keywords: ["help", "what can you", "how do i", "set up", "setup", "configure", "model", "profile", "skill", "install"],
408
516
  examples: ["What can you do?", "Which model am I using?", "How do I set up email alerts?"],
409
517
  requires: [],
410
518
  docs: [
@@ -1,6 +1,7 @@
1
1
  import type { AppConfig } from "../config.ts"
2
2
  import { findOpencode } from "../code/runtime.ts"
3
3
  import { noAgentModel } from "../llm.ts"
4
+ import { findRuntime } from "../containers/run.ts"
4
5
  import { CAPABILITIES, type Capability, type Requirement, type System } from "./registry.ts"
5
6
 
6
7
  /**
@@ -87,6 +88,16 @@ export function requirementGap(r: Requirement, cfg: AppConfig, probes: Probes =
87
88
  return cfg.domain ? null : { missing: "the product's industry and regulations", fix: "set domain: in forgeqe.yaml, and list your applicable controls in .forgeqe/knowledge/compliance.md" }
88
89
  case "command":
89
90
  return probes.which(r.command) ? null : { missing: `${r.command} on PATH${r.why ? ` (${r.why})` : ""}`, fix: `install ${r.command}` }
91
+ case "apps": {
92
+ const apps = r.app === "desktop" ? cfg.desktop?.apps : cfg.mobile?.apps
93
+ return Object.keys(apps || {}).length
94
+ ? null
95
+ : { missing: `a ${r.app} app to test`, fix: `add it to env.yaml (kind: ${r.app}) or to ${r.app}.apps in forgeqe.yaml` }
96
+ }
97
+ case "containers": {
98
+ const which = (c: string) => (probes.which(c) ? c : null)
99
+ return findRuntime(cfg, which) ? null : { missing: `${cfg.containers?.runtime || "Docker or Podman"} on PATH`, fix: "install Docker Desktop, Docker Engine or Podman (containers.runtime in forgeqe.yaml picks one)" }
100
+ }
90
101
  case "input":
91
102
  return null
92
103
  case "either": {
@@ -130,6 +141,10 @@ export function describeRequirement(r: Requirement): string {
130
141
  return "The product's industry and regulations: domain: in forgeqe.yaml, with your applicable controls in .forgeqe/knowledge/compliance.md"
131
142
  case "command":
132
143
  return `${r.command} on PATH${r.why ? `, ${r.why}` : ""}`
144
+ case "apps":
145
+ return `${r.app === "desktop" ? "Windows desktop" : "Mobile"} apps: env.yaml (kind: ${r.app}) or ${r.app}.apps in forgeqe.yaml`
146
+ case "containers":
147
+ return "Docker or Podman on PATH; the services to start in containers.services in forgeqe.yaml (mocks need none)"
133
148
  case "input":
134
149
  return `From your request: ${r.what}`
135
150
  case "either":
package/src/cases/rca.ts CHANGED
@@ -13,6 +13,7 @@ import { lastPassed } from "./defectHistory.ts"
13
13
  import { routeOf, type DefectCluster } from "./defects.ts"
14
14
  import { reproText } from "./repro.ts"
15
15
  import type { CaseResult } from "./types.ts"
16
+ import { guardrailsNotes } from "../guardrails.ts"
16
17
 
17
18
  /**
18
19
  * Defect intelligence, part three (b): why a defect may happen. Always a hypothesis, never a verdict.
@@ -219,6 +220,7 @@ export async function analyzeCause(
219
220
  repo,
220
221
  profile: config.llm,
221
222
  domain: domainNotes(config),
223
+ guardrails: guardrailsNotes(config),
222
224
  model: config.code.model,
223
225
  bin: config.code.opencode,
224
226
  readDirs: [repo],
package/src/childEnv.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { savedSecrets } from "./auth/store.ts"
2
2
  import type { AppConfig } from "./config.ts"
3
+ import { COPILOT_AUTH_ENV, MODELS_TOKEN_ENV, RUNTIME_AUTH_ENV } from "./copilot.ts"
3
4
  import { NO_TELEMETRY_ENV, withoutTelemetry } from "./privacy.ts"
4
5
 
5
6
  /**
@@ -72,7 +73,9 @@ function collectEnvNames(value: unknown, out: Set<string>, inLogins: boolean): v
72
73
  * (`*Env` settings), and variables `mcp:` servers reference. Saved-login variables are left out: tests read them.
73
74
  */
74
75
  export function privateEnvNames(cfg: AppConfig, extra: Iterable<string> = []): string[] {
75
- const out = new Set<string>(extra)
76
+ const out = new Set<string>([...extra, COPILOT_AUTH_ENV, RUNTIME_AUTH_ENV, MODELS_TOKEN_ENV])
77
+ // A github_copilot profile without api_key_env reaches GitHub Models with GITHUB_TOKEN: a model key then.
78
+ if (cfg.llm?.provider === "github_copilot" && !cfg.llm.apiKeyEnv) out.add("GITHUB_TOKEN")
76
79
  collectEnvNames({ ...cfg, logins: undefined }, out, false)
77
80
  for (const name of envRefs(cfg.mcp)) out.add(name)
78
81
  // Secrets a saved sign-in put into the environment, including those saved by name (forgeqe auth login NAME).
package/src/cli/args.ts CHANGED
@@ -8,8 +8,8 @@ export type Flags = Map<string, string | boolean>
8
8
 
9
9
  /** Flags that never take the next word as their value, so `run --direct "<goal>"` keeps the goal. `--headed=false` still works. */
10
10
  export const SWITCHES = new Set([
11
- "all", "check", "debug", "direct", "force", "global", "guide", "headed", "help", "locators", "no-code", "no-memory", "no-write-back",
12
- "once", "open", "plain", "shots", "steps", "test-alerts", "user", "verbose", "yes",
11
+ "accept-heals", "all", "all-tools", "check", "debug", "direct", "force", "global", "guide", "headed", "help", "locators", "no-code", "no-heal", "no-memory", "no-write-back",
12
+ "once", "open", "plain", "shots", "steps", "test-alerts", "ufo", "update-baselines", "user", "verbose", "virtual", "yes",
13
13
  ])
14
14
 
15
15
  export function parseArgs(argv: string[]): { positionals: string[]; flags: Flags } {
@@ -89,6 +89,7 @@ export function configOpts(flags: Flags) {
89
89
  return {
90
90
  configPath: strFlag(flags, "config"),
91
91
  profile: strFlag(flags, "profile"),
92
+ env: strFlag(flags, "env"),
92
93
  }
93
94
  }
94
95
 
package/src/cli/help.ts CHANGED
@@ -13,6 +13,7 @@ Usage:
13
13
  forgeqe models [--config forgeqe.yaml]
14
14
  forgeqe features [--config forgeqe.yaml] what ForgeQE can do, and what each capability still needs here
15
15
  forgeqe context what each agent loads on every model call, against its budget (exits 1 over budget)
16
+ forgeqe guardrails [add "rule"] the team's rules every agent must follow (.forgeqe/guardrails), or add one
16
17
  forgeqe docs [--guide|--open] [--copy DIR] the guides and the architecture page (--guide: the user guide in the browser; --open: the architecture page)
17
18
  forgeqe start [--profile name] [--url URL] [--goal TEXT] [--headed] [--replay MODE] [--no-memory]
18
19
  [--repo DIR] [--no-code] [--browser-provider local|browserstack|testmu] [--devices a,b]
@@ -26,6 +27,26 @@ Usage:
26
27
  [--profile name] [--url URL] [--headed] [--plain] [--attempts N] [--skill NAME]
27
28
  [--browser-provider local|browserstack|testmu] [--devices a,b]
28
29
  run test cases the same way every time (what pipelines call)
30
+ forgeqe apps [--env NAME] the test environments in env.yaml and the apps (web, desktop, mobile) of one;
31
+ every command takes --env NAME (or FORGEQE_ENV) to pick the environment
32
+ forgeqe desktop explore "<goal>" --app NAME [--name TEXT] [--out DIR] [--yes]
33
+ Windows thick clients: UFO² does the goal on the live app and the run is
34
+ saved as a YAML workflow (docs/DESKTOP.md)
35
+ forgeqe desktop record --app NAME [--name TEXT] do the steps in a Windows app yourself; they are saved as a workflow
36
+ forgeqe desktop replay <workflow.yaml|folder> ... [name=value ...] [--no-heal] [--accept-heals] [--junit FILE]
37
+ replay desktop workflows with no model; moved controls are healed for review
38
+ forgeqe desktop setup [--ufo] | apps | doctor | validate <workflow.yaml>
39
+ forgeqe mobile explore "<goal>" --app NAME Android and iOS apps through Appium: the model does the goal and the run
40
+ is saved as a mobile workflow (docs/MOBILE.md)
41
+ forgeqe mobile replay <workflow.yaml|folder> [--driver appium|maestro] [--junit FILE]
42
+ replay mobile workflows with no model, on Appium or Maestro
43
+ forgeqe mobile maestro <flow.yaml|folder> run your team's Maestro flows (checked against an allowlist first)
44
+ forgeqe mobile upload <app> [--virtual] upload a build to TestMu AI; prints the lt:// id for the app's app:
45
+ forgeqe mobile apps | doctor | flow <workflow.yaml> | validate <workflow.yaml>
46
+ forgeqe services [status|up|down|logs] [name ...]
47
+ the services forgeqe.yaml (containers.services) or its compose files
48
+ declare, in Docker or Podman: start in order and wait until ready,
49
+ stop, logs (docs/CONTAINERS.md)
29
50
  forgeqe cases <source...> [--goal] list the test cases a source gives (--goal: the goal each runs)
30
51
  forgeqe code [run-dir] [--repo DIR] [--attempts N] [--code-model provider/model]
31
52
  write a test for a run (default: the latest) with Scribe
@@ -36,7 +57,7 @@ Usage:
36
57
  [--browser-provider local|browserstack|testmu]
37
58
  check flows on a schedule and alert Teams or email when one breaks
38
59
  forgeqe monitor --test-alerts send a sample alert to check the Teams and email settings
39
- forgeqe mcp [--repo DIR] [--profile name] [--url URL] [--headed] [--browser-provider local|browserstack|testmu]
60
+ forgeqe mcp [--all-tools] [--repo DIR] [--profile name] [--url URL] [--headed] [--browser-provider local|browserstack|testmu]
40
61
  ForgeQE's tools as an MCP server on stdio, for other agents
41
62
  forgeqe mcp list the Azure DevOps, Jira and other MCP servers ForgeQE's agent gets
42
63
  forgeqe bi [build] [--evidence-dir DIR] [--data-dir DIR] [--build-dir DIR] [--static | --evidence]
@@ -90,8 +111,8 @@ Usage:
90
111
  forgeqe api tests [SET] --scenarios[=a,b] --repo DIR
91
112
  scenario tests on the repository's existing API helpers
92
113
  forgeqe api list saved contract sets
93
- forgeqe auth login [system] [--no-browser] sign in to azure_devops, github, jira, confluence, testrail, zephyr, qtest,
94
- xray, browserstack, or testmu in the browser, and save the token in the system's credential store
114
+ forgeqe auth login [system] [--no-browser] sign in to azure_devops, github, copilot, jira, confluence, testrail, zephyr,
115
+ qtest, xray, browserstack, or testmu in the browser, and save the token in the system's credential store
95
116
  forgeqe auth login <ENV_NAME> save one secret (an LLM API key, a webhook URL) in the credential store
96
117
  forgeqe auth [status] which systems are signed in, and how
97
118
  forgeqe auth logout <system|ENV_NAME> forget a saved sign-in
package/src/cli/index.ts CHANGED
@@ -56,9 +56,15 @@ import { DOMAIN_CASES, gate, loadDomainCases, reportMarkdown, runDomainEval, sav
56
56
  import { evalNotice, notifiedLine, notify, notifySuite } from "../notify/events.ts"
57
57
  import { sendNotice, type Notice } from "../notify/notice.ts"
58
58
  import { buildBi, initBiProject, syncBiData } from "../bi.ts"
59
+ import { cmdDesktop } from "../desktop/command.ts"
60
+ import { cmdMobile } from "../mobile/command.ts"
61
+ import { cmdServices } from "../containers/command.ts"
62
+ import { startedContainers, stopStartedContainers } from "../containers/run.ts"
63
+ import { CapabilityScope } from "../mcp/scope.ts"
64
+ import { appsText } from "../environments.ts"
59
65
 
60
66
  const AGENT_COMMANDS = new Set(["start", "run", "code", "suite", "heal", "monitor", "explore"])
61
- const LOGGED_COMMANDS = new Set([...AGENT_COMMANDS, "mcp", "login", "setup", "bi"])
67
+ const LOGGED_COMMANDS = new Set([...AGENT_COMMANDS, "mcp", "login", "setup", "bi", "desktop", "mobile", "services"])
62
68
 
63
69
  /** How much of the agents' work to print: brief unless --verbose. */
64
70
  const detailOf = (flags: Flags) => detailFrom(boolFlag(flags, "verbose"))
@@ -151,6 +157,9 @@ export async function main(argv: string[]): Promise<number> {
151
157
  return 0
152
158
  }
153
159
 
160
+ // --env picks the test environment (env.yaml) for everything this process loads: commands, the shell, MCP, suites.
161
+ const envFlag = strFlag(flags, "env")
162
+ if (envFlag) process.env.FORGEQE_ENV = envFlag
154
163
  // Sign-ins saved with forgeqe auth login, under the names the config reads; the terminal's own variables win.
155
164
  await applySavedCredentials().catch(() => [])
156
165
  // Models' list prices for costs: waited for only the first time, refreshed daily in the background after that.
@@ -176,8 +185,26 @@ export async function main(argv: string[]): Promise<number> {
176
185
  const { agentContexts, contextReport } = await import("../context.ts")
177
186
  const rows = agentContexts()
178
187
  console.log(contextReport(rows).join("\n"))
188
+ // The team's guardrails ride on top of every agent's fixed context.
189
+ const { guardrailsNotes, readGuardrails } = await import("../guardrails.ts")
190
+ const gcfg = loadConfig(configOpts(flags))
191
+ const block = guardrailsNotes(gcfg)
192
+ if (block) console.log(`\nGuardrails: ${readGuardrails(gcfg).length} rules add ~${Math.ceil(block.length / 4)} tokens to every agent above (forgeqe guardrails lists them).`)
179
193
  return rows.some((r) => r.over) ? 1 : 0
180
194
  }
195
+ case "guardrails": {
196
+ // The team's rules for every agent: list them, or add one (src/guardrails.ts).
197
+ const { addGuardrail, guardrailsText } = await import("../guardrails.ts")
198
+ const cfg = loadConfig(configOpts(flags))
199
+ if (positionals[1] === "add") {
200
+ const { added, file } = addGuardrail(cfg, positionals.slice(2).join(" "))
201
+ console.log(added ? `Added to ${file}. Every agent started from now on follows it (restart a running ForgeQE shell for its own agent).` : `That guardrail is already in force (${file}).`)
202
+ return 0
203
+ }
204
+ if (positionals[1]) throw new Error(`Unknown guardrails action "${positionals[1]}". Use: forgeqe guardrails [add "rule"]`)
205
+ console.log(guardrailsText(cfg))
206
+ return 0
207
+ }
181
208
  case "features":
182
209
  // What ForgeQE can do, and whether each capability works with this config and machine.
183
210
  console.log(featuresText(loadConfig(configOpts(flags))))
@@ -212,6 +239,15 @@ export async function main(argv: string[]): Promise<number> {
212
239
  return await withUsageLine(() => cmdExplore(positionals.slice(1), flags))
213
240
  case "bi":
214
241
  return await cmdBi(positionals.slice(1), flags)
242
+ case "desktop":
243
+ return await withUsageLine(() => cmdDesktop(positionals.slice(1), flags))
244
+ case "mobile":
245
+ return await withUsageLine(() => cmdMobile(positionals.slice(1), flags))
246
+ case "services":
247
+ return await withUsageLine(() => cmdServices(positionals.slice(1), flags))
248
+ case "apps":
249
+ console.log(appsText(loadConfig(configOpts(flags))))
250
+ return 0
215
251
  case "prompts":
216
252
  return positionals[1] === "eval" ? await withUsageLine(() => cmdPromptEval(positionals.slice(2), flags)) : cmdPrompts(positionals.slice(1), flags)
217
253
  case "eval":
@@ -897,6 +933,11 @@ async function cmdStart(flags: Flags): Promise<number> {
897
933
 
898
934
  /** Background monitors started by the agent live as long as this process; wind their rounds down before exiting. */
899
935
  async function stopMonitorsOnExit(log: (line: string) => void) {
936
+ // Services and mocks the agent started in this session end with it (forgeqe services up keeps its own).
937
+ if (startedContainers().length) {
938
+ log(`Stopping the containers this session started (${startedContainers().join(", ")})…`)
939
+ await stopStartedContainers()
940
+ }
900
941
  const running = backgroundMonitors()
901
942
  if (!running.length) return
902
943
  log(`Stopping ${running.length === 1 ? "the monitor" : `${running.length} monitors`}…`)
@@ -1062,6 +1103,7 @@ async function runAgent(cfg: AppConfig, flags: Flags, text: string, maxSteps: nu
1062
1103
  } finally {
1063
1104
  process.off("SIGINT", onSigint)
1064
1105
  await brain?.close()
1106
+ if (startedContainers().length) await stopStartedContainers()
1065
1107
  }
1066
1108
  }
1067
1109
 
@@ -1105,8 +1147,10 @@ async function cmdMcp(args: string[], flags: Flags): Promise<number> {
1105
1147
  emit: log,
1106
1148
  session: "server",
1107
1149
  })
1108
- const server = createMcpServer({ tools: TOOLS, context, instructions: () => environmentNotes(context()), version: packageVersion() })
1109
- process.stderr.write(`ForgeQE MCP server on stdio (${TOOLS.length} tools, working in ${cwd})\n`)
1150
+ // The client lists the core and the capabilities this project can use; use_capabilities adds more (--all-tools: every tool).
1151
+ const scope = boolFlag(flags, "all-tools") ? undefined : new CapabilityScope(cfg, TOOLS.map((t) => t.name))
1152
+ const server = createMcpServer({ tools: TOOLS, context, instructions: () => environmentNotes(context()), version: packageVersion(), ...(scope ? { scope } : {}) })
1153
+ process.stderr.write(`ForgeQE MCP server on stdio (${scope ? `${scope.listedTools().length} of ${TOOLS.length}` : TOOLS.length} tools, working in ${cwd})\n`)
1110
1154
  await serveStdio(server)
1111
1155
  await stopMonitorsOnExit((line) => process.stderr.write(`${line}\n`))
1112
1156
  return 0
package/src/code/find.ts CHANGED
@@ -13,11 +13,32 @@ import { CAUSE_TEXT, flakyReport, quarantined, recordResults, RELEASE_AFTER, set
13
13
  import { loadSkills } from "../skills/registry.ts"
14
14
  import { prompt } from "../prompts.ts"
15
15
  import { domainNotes } from "../domain.ts"
16
+ import { guardrailsNotes } from "../guardrails.ts"
16
17
 
17
18
  const LIST_ROUNDS = 3
18
19
  const LIST_TIMEOUT_SECONDS = 180
19
20
  const OUTPUT_CHARS = 6_000
20
21
 
22
+ /**
23
+ * ForgeQE runs Scribe's list command without asking, so it takes only a test framework's own listing: Scribe reads the
24
+ * repository, and a file in it could talk Scribe into any command. The run command is shown to the user first instead.
25
+ */
26
+ const LIST_RUNNER =
27
+ /^(?:(?:(?:npx(?:\s+--no-install)?|pnpm(?:\s+exec)?|yarn|bunx|bun\s+x)\s+)?(?:playwright\s+test|jest|vitest|mocha)|(?:(?:python3?|py)\s+-m\s+|(?:uv|poetry)\s+run\s+)?pytest|dotnet\s+test)(?=\s|$)/i
28
+ const LIST_FLAG = /(?:^|\s)(?:--list|--listTests|--list-tests|--collect-only|--co|--dry-run)(?=[\s=]|$)/
29
+ /** Characters that chain, substitute, or redirect in sh or cmd.exe. */
30
+ const SHELL_SYNTAX = /[;&|`$<>()\r\n%]/
31
+
32
+ /** Whether ForgeQE may run `command` unasked as a list command: one test runner, a listing flag, no shell syntax. */
33
+ export function listCommandAllowed(command: string): boolean {
34
+ const c = command.trim()
35
+ if (!c || SHELL_SYNTAX.test(c)) return false
36
+ const runner = LIST_RUNNER.exec(c)
37
+ if (!runner) return false
38
+ const rest = c.slice(runner[0].length)
39
+ return LIST_FLAG.test(rest) || (/vitest$/i.test(runner[0]) && /^\s+list(?=\s|$)/.test(rest))
40
+ }
41
+
21
42
  export const findAgentPrompt = () => prompt("find-tests")
22
43
 
23
44
  export const FIND_AGENT: AgentSpec = {
@@ -176,6 +197,7 @@ export async function findTests(opts: {
176
197
  repo,
177
198
  profile: opts.config.llm,
178
199
  domain: domainNotes(opts.config),
200
+ guardrails: guardrailsNotes(opts.config),
179
201
  model: opts.config.code.model,
180
202
  bin: opts.config.code.opencode,
181
203
  readDirs: [repo],
@@ -195,6 +217,10 @@ export async function findTests(opts: {
195
217
  let report = reply.report
196
218
  let listed = false
197
219
  for (let round = 1; round <= LIST_ROUNDS && clean(report.tests).length && report.list_command?.trim() && !opts.signal?.aborted; round++) {
220
+ if (!listCommandAllowed(report.list_command!)) {
221
+ emit({ type: "code", kind: "info", text: `ForgeQE does not run this list command, which is not a test framework's own listing: ${report.list_command}` })
222
+ break
223
+ }
198
224
  emit({ type: "code", kind: "info", text: `ForgeQE lists the selected tests: ${report.list_command}` })
199
225
  const run = await runTest({ command: report.list_command!, cwd: repo, timeoutSeconds: LIST_TIMEOUT_SECONDS, signal: opts.signal, hide: privateEnvNames(opts.config) })
200
226
  if (opts.signal?.aborted) break