@capagents/forgeqe 0.0.0-stage → 1.0.1

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 (220) hide show
  1. package/HOW_TO_USE.md +1639 -0
  2. package/README.md +338 -2
  3. package/bin/forgeqe.d.ts +3 -0
  4. package/bin/forgeqe.js +188 -0
  5. package/docs/ARCHITECTURE.md +408 -0
  6. package/docs/security-data-flow.html +1870 -0
  7. package/docs/user-guide.html +2094 -0
  8. package/evals/domain/access-role-change.yaml +37 -0
  9. package/evals/domain/banking-payment-transfer.yaml +41 -0
  10. package/evals/domain/gdpr-account-erasure.yaml +30 -0
  11. package/evals/domain/insurance-claim-fnol.yaml +30 -0
  12. package/evals/domain/insurance-policy-renewal.yaml +37 -0
  13. package/evals/domain/lending-loan-application.yaml +36 -0
  14. package/forgeqe-architecture.html +718 -0
  15. package/forgeqe.example.yaml +203 -0
  16. package/knowledge/README.md +18 -0
  17. package/knowledge/business-rules.md +10 -0
  18. package/knowledge/compliance.md +16 -0
  19. package/knowledge/environments.md +5 -0
  20. package/knowledge/glossary.md +5 -0
  21. package/knowledge/product.md +16 -0
  22. package/knowledge/roles.md +9 -0
  23. package/knowledge/test-data.md +7 -0
  24. package/llms.example.json +51 -0
  25. package/package.json +63 -4
  26. package/prompts/README.md +34 -0
  27. package/prompts/api-scenarios.md +21 -0
  28. package/prompts/api-tests.md +39 -0
  29. package/prompts/brain.md +166 -0
  30. package/prompts/browser-capture.md +7 -0
  31. package/prompts/browser-case-report.md +7 -0
  32. package/prompts/browser-checks.md +14 -0
  33. package/prompts/browser-efficiency.md +7 -0
  34. package/prompts/browser-error-recovery.md +15 -0
  35. package/prompts/browser-extra.md +5 -0
  36. package/prompts/browser-goal-verification.md +13 -0
  37. package/prompts/browser-skill-select.md +6 -0
  38. package/prompts/domain-rule.md +13 -0
  39. package/prompts/eval-judge.md +19 -0
  40. package/prompts/explore-charters.md +27 -0
  41. package/prompts/find-tests.md +28 -0
  42. package/prompts/heal.md +40 -0
  43. package/prompts/pr-review.md +29 -0
  44. package/prompts/skill-rule.md +6 -0
  45. package/prompts/test-agent.md +34 -0
  46. package/prompts/test-reviewer.md +30 -0
  47. package/prompts/test-rules.md +28 -0
  48. package/py/forgeqe_engine/__init__.py +3 -0
  49. package/py/forgeqe_engine/__main__.py +3 -0
  50. package/py/forgeqe_engine/brand.py +453 -0
  51. package/py/forgeqe_engine/brand_assets.py +6 -0
  52. package/py/forgeqe_engine/browser.py +196 -0
  53. package/py/forgeqe_engine/capture.py +538 -0
  54. package/py/forgeqe_engine/checks.py +98 -0
  55. package/py/forgeqe_engine/cli.py +188 -0
  56. package/py/forgeqe_engine/diagnostics.py +609 -0
  57. package/py/forgeqe_engine/llm.py +195 -0
  58. package/py/forgeqe_engine/logins.py +251 -0
  59. package/py/forgeqe_engine/loops.py +224 -0
  60. package/py/forgeqe_engine/memory.py +1004 -0
  61. package/py/forgeqe_engine/network.py +244 -0
  62. package/py/forgeqe_engine/prompts.py +20 -0
  63. package/py/forgeqe_engine/replay.py +240 -0
  64. package/py/forgeqe_engine/report.py +173 -0
  65. package/py/forgeqe_engine/service.py +1335 -0
  66. package/py/forgeqe_engine/settings.py +93 -0
  67. package/py/forgeqe_engine/skills.py +88 -0
  68. package/py/forgeqe_engine/summary.py +239 -0
  69. package/py/forgeqe_engine/urls.py +81 -0
  70. package/py/forgeqe_engine/workflow_yaml.py +689 -0
  71. package/skills/dotnet-nunit-playwright/SKILL.md +34 -0
  72. package/skills/dynamics-365/SKILL.md +21 -0
  73. package/skills/playwright-typescript/SKILL.md +35 -0
  74. package/skills/writing-skills/SKILL.md +30 -0
  75. package/src/api/plan.ts +246 -0
  76. package/src/api/run.ts +204 -0
  77. package/src/api/scenario.ts +502 -0
  78. package/src/api/schema.ts +170 -0
  79. package/src/api/spec.ts +624 -0
  80. package/src/api/store.ts +232 -0
  81. package/src/api/traffic.ts +281 -0
  82. package/src/api/types.ts +163 -0
  83. package/src/api/write.ts +272 -0
  84. package/src/auth/signin.ts +494 -0
  85. package/src/auth/store.ts +170 -0
  86. package/src/brain/index.ts +189 -0
  87. package/src/brain/prompt.ts +19 -0
  88. package/src/capabilities/docs.ts +101 -0
  89. package/src/capabilities/guide.ts +455 -0
  90. package/src/capabilities/registry.ts +343 -0
  91. package/src/capabilities/render.ts +106 -0
  92. package/src/capabilities/status.ts +137 -0
  93. package/src/card.ts +11 -0
  94. package/src/cases/ado.ts +325 -0
  95. package/src/cases/api.ts +117 -0
  96. package/src/cases/bugs.ts +223 -0
  97. package/src/cases/confluence.ts +111 -0
  98. package/src/cases/context.ts +223 -0
  99. package/src/cases/dashboards.ts +137 -0
  100. package/src/cases/failure.ts +97 -0
  101. package/src/cases/files.ts +295 -0
  102. package/src/cases/goal.ts +53 -0
  103. package/src/cases/http.ts +111 -0
  104. package/src/cases/index.ts +131 -0
  105. package/src/cases/jira.ts +122 -0
  106. package/src/cases/qtest.ts +108 -0
  107. package/src/cases/quality.ts +190 -0
  108. package/src/cases/rag.ts +100 -0
  109. package/src/cases/readiness.ts +284 -0
  110. package/src/cases/save.ts +149 -0
  111. package/src/cases/sprints.ts +148 -0
  112. package/src/cases/testrail.ts +176 -0
  113. package/src/cases/types.ts +107 -0
  114. package/src/cases/wiki.ts +105 -0
  115. package/src/cases/xray.ts +206 -0
  116. package/src/cases/zephyr.ts +153 -0
  117. package/src/childEnv.ts +86 -0
  118. package/src/cli/args.ts +114 -0
  119. package/src/cli/help.ts +167 -0
  120. package/src/cli/index.ts +1142 -0
  121. package/src/cli/terminal.ts +187 -0
  122. package/src/code/areas.ts +337 -0
  123. package/src/code/attention.ts +214 -0
  124. package/src/code/find.ts +540 -0
  125. package/src/code/handoff.ts +541 -0
  126. package/src/code/history.ts +170 -0
  127. package/src/code/impact.ts +231 -0
  128. package/src/code/index.ts +34 -0
  129. package/src/code/livebrowser.ts +67 -0
  130. package/src/code/loop.ts +603 -0
  131. package/src/code/notes.ts +75 -0
  132. package/src/code/profile.ts +226 -0
  133. package/src/code/prompt.ts +303 -0
  134. package/src/code/prreview.ts +649 -0
  135. package/src/code/prtests.ts +103 -0
  136. package/src/code/replay.ts +772 -0
  137. package/src/code/review.ts +202 -0
  138. package/src/code/runtime.ts +781 -0
  139. package/src/code/slices.ts +159 -0
  140. package/src/code/testrun.ts +187 -0
  141. package/src/code/traceability.ts +100 -0
  142. package/src/code/worktree.ts +153 -0
  143. package/src/commands.ts +101 -0
  144. package/src/config.ts +970 -0
  145. package/src/detail.ts +65 -0
  146. package/src/domain.ts +19 -0
  147. package/src/engine/bridge.ts +359 -0
  148. package/src/engine/mock.ts +102 -0
  149. package/src/engine/setup.ts +298 -0
  150. package/src/entra.ts +154 -0
  151. package/src/env.ts +24 -0
  152. package/src/evals/domain.ts +338 -0
  153. package/src/explore/charters.ts +151 -0
  154. package/src/explore/run.ts +280 -0
  155. package/src/explore/story.ts +106 -0
  156. package/src/guard.ts +51 -0
  157. package/src/heal/loop.ts +357 -0
  158. package/src/heal/prompt.ts +143 -0
  159. package/src/heal/report.ts +95 -0
  160. package/src/index.ts +12 -0
  161. package/src/init.ts +102 -0
  162. package/src/knowledge.ts +114 -0
  163. package/src/legacyEnv.ts +9 -0
  164. package/src/llm.ts +61 -0
  165. package/src/locale.ts +25 -0
  166. package/src/logins.ts +148 -0
  167. package/src/managed.ts +101 -0
  168. package/src/mcp/apitools.ts +335 -0
  169. package/src/mcp/captools.ts +25 -0
  170. package/src/mcp/casetools.ts +223 -0
  171. package/src/mcp/commands.ts +421 -0
  172. package/src/mcp/exploretools.ts +129 -0
  173. package/src/mcp/external.ts +172 -0
  174. package/src/mcp/git.ts +78 -0
  175. package/src/mcp/notifytools.ts +49 -0
  176. package/src/mcp/prtools.ts +261 -0
  177. package/src/mcp/qualitytools.ts +154 -0
  178. package/src/mcp/repotests.ts +286 -0
  179. package/src/mcp/server.ts +234 -0
  180. package/src/mcp/tools.ts +810 -0
  181. package/src/memory.ts +250 -0
  182. package/src/money.ts +39 -0
  183. package/src/monitor/alerts.ts +94 -0
  184. package/src/monitor/background.ts +68 -0
  185. package/src/monitor/run.ts +263 -0
  186. package/src/monitor/state.ts +105 -0
  187. package/src/notify/events.ts +144 -0
  188. package/src/notify/notice.ts +177 -0
  189. package/src/prices.ts +175 -0
  190. package/src/print.ts +54 -0
  191. package/src/privacy.ts +35 -0
  192. package/src/prompts.ts +165 -0
  193. package/src/quality/budgets.ts +249 -0
  194. package/src/quality/history.ts +149 -0
  195. package/src/quality/visual.ts +201 -0
  196. package/src/report/data.ts +475 -0
  197. package/src/report/html.ts +106 -0
  198. package/src/report/template.html +84 -0
  199. package/src/run.ts +243 -0
  200. package/src/sessionLog.ts +216 -0
  201. package/src/sessionLogins.ts +94 -0
  202. package/src/skills/registry.ts +217 -0
  203. package/src/skills/scaffold.ts +65 -0
  204. package/src/skills/select.ts +37 -0
  205. package/src/skills/view.ts +44 -0
  206. package/src/suite/report.ts +135 -0
  207. package/src/suite/run.ts +303 -0
  208. package/src/summary.ts +188 -0
  209. package/src/tools.ts +130 -0
  210. package/src/tui/App.tsx +983 -0
  211. package/src/tui/clipboard.ts +36 -0
  212. package/src/tui/components.tsx +263 -0
  213. package/src/tui/dialogs.ts +65 -0
  214. package/src/tui/main.tsx +20 -0
  215. package/src/tui/text.ts +28 -0
  216. package/src/tui/theme.ts +104 -0
  217. package/src/tui/transcript.tsx +344 -0
  218. package/src/types.ts +146 -0
  219. package/src/update.ts +311 -0
  220. package/src/usage.ts +158 -0
@@ -0,0 +1,408 @@
1
+ # ForgeQE CLI architecture
2
+
3
+ Three processes. The Bun CLI owns the shell, config, profiles, and ForgeQE's tools. ForgeQE's agent runs on the agent runtime, reads each request, and calls those tools over MCP. Scribe, the coding agent, runs on the same runtime and writes, heals, and reviews tests. A Python engine drives Chrome. There is no FastAPI server and no database.
4
+
5
+ The single-view map of subagents, tools, and capabilities is [`forgeqe-architecture.html`](../forgeqe-architecture.html) (`forgeqe docs --open`). Suite `report.html` includes the browser agent's token counts, and a cost in the machine's currency when the profile has a price.
6
+
7
+ ```
8
+ ┌───────────────────────────────────────────────┐ ┌──────────────────────────────┐
9
+ │ OpenTUI shell or forgeqe run --plain │ MCP │ Agent runtime │
10
+ │ config, llms.json profile, /stop, transcript │◄──────►│ "forgeqe" agent: decides, │
11
+ │ ForgeQE tools (src/mcp), 127.0.0.1 + token │ HTTP │ edits files, runs commands │
12
+ │ write_test / heal_tests open Scribe sessions │ │ Scribe: writes, heals, and │
13
+ │ │ │ reviews tests in your repo │
14
+ └─────────────────────┬─────────────────────────┘ └──────────────────────────────┘
15
+ stdin: request JSON, later "stop"
16
+ stdout: JSON events (log, memory, step, result, usage, done)
17
+ ┌─────────────────────▼─────────────────────────┐
18
+ │ python -m forgeqe_engine run │ ~/.forgeqe/engine/.venv
19
+ │ Engine agent + browser (Chrome) │ (py/forgeqe_engine)
20
+ │ nexus recovery prompt, tuning, loop breaker │
21
+ └─────────────────────┬─────────────────────────┘
22
+ │
23
+ .forgeqe/reports/<run-id>/: run.json workflow.yaml trace.md steps/ failure.png
24
+ ```
25
+
26
+ ## Engine (`py/forgeqe_engine`)
27
+
28
+ Copied from test-agent-nexus and trimmed to what a CLI run needs:
29
+
30
+ | Module | From nexus | Contents |
31
+ |--------|------------|----------|
32
+ | `service.py` | nexus browser service | `RunRequest`, `run_browser_task` (memory replay, Agent construction fallbacks, the step callbacks, timeout and stop handling, one `_save` for every way a run ends), error-recovery system prompt, `MemoryRun`, artifact writing |
33
+ | `llm.py` | nexus browser service | LLM construction (azure, openai, openai_compatible, ollama), model-call timing, token usage |
34
+ | `browser.py` | nexus browser service | Chrome launch kwargs, waiting for a page to settle, browser and agent shutdown, the browser profile recorded in `run.json` |
35
+ | `settings.py` | nexus browser service | Fast-mode tuning, headless mode, the default-login block |
36
+ | `urls.py` | nexus browser service | Start URL normalisation and the allowed-hosts checks |
37
+ | `loops.py` | nexus browser service | Progress checks, the repeated-control nudge then stop, and the resume point after a failure |
38
+ | `capture.py` | new | Live capture for the test the run becomes: what each action changed on the page (`install_effects` wraps the agent's `tools.act`), the control a user acts on (`actionable_node`), locators checked on the live page (`verify_locators`: role and name through Chrome's accessibility tree, the rest in the page), the element and section behind each check (`locate_check`), and `click_selector`, a real mouse click on a CSS-selected element |
39
+ | `summary.py` | nexus browser service | Result markdown (the agent's own final answer and the checks it recorded; no filtering of what it read), fallback summaries, failure trace and screenshot, the judge's verdict |
40
+ | `workflow_yaml.py` | `browser_workflow_yaml.py` | `forgeqe-v2-compact` steps from history (or the live trace after a stop), semantic locators, execution metadata |
41
+ | `cli.py` | new | The stdin/stdout protocol above |
42
+ | `memory.py` | new | Page memory: element facts from the engine library's DOM, ranked locators with confidence, element identity across visits, recorded flows, prompt hints |
43
+ | `replay.py` | new | Replays a recorded flow through the engine library's `Tools` and writes the hand-off task for the agent |
44
+ | `logins.py` | new | Saved logins: the engine library's `sensitive_data` per domain, TOTP codes, session files (load, split by host, save when changed), and the interactive `login` command |
45
+ | `report.py` | new | `CaseReport` (per-step verdicts through the engine library's `output_model_schema`) and `steps.json` with step screenshots |
46
+ | `checks.py` | new | `record_check`, the browser agent's action for each result the goal asks to verify: `expected` and the exact `text` on the page, refused when the page (visible text or form values, over CDP) does not show it; `service.py` registers it on the agent's `Tools`, adds `CHECKS_SYSTEM_MESSAGE`, and on success passes the checks to `run.json` and `workflow.yaml` `checks` |
47
+ | `diagnostics.py` | new | Console errors, uncaught exceptions, failed and slow requests, and accessibility checks per page, over CDP |
48
+ | `skills.py` | new | Browser skills: `choose_skills` (one structured model call picks at most 3 by name and description; never raises) and `skills_block` (the chosen skills' text under a header, within `skills_max_chars`). `run_browser_task` appends the block to the extended system prompt and records what it decided in `run.json` `skills`; the choice's tokens are added to the run's usage |
49
+ | `brand.py`, `brand_assets.py` | new | ForgeQE start page and overlay in a visible browser; the icon made by `scripts/brand.py` from `assets/forgeqe-icon.png` |
50
+
51
+ **Saved logins.** The request carries each login's name, domains, and environment variable names; values cross stdin only for logins remembered this session (`values`), and are never logged. `use_logins` builds the engine library's domain-scoped `sensitive_data` (`{domain: {shop_username: …, shop_password: …, shop_bu_2fa_code: <TOTP secret>}}`), so the model sees placeholders, the engine fills values only on matching domains, masks them in page text, and turns `bu_2fa_code` into a fresh pyotp code when typed. Session files for the run's logins are merged into `Browser(storage_state=…)`. Because the agent closes the browser when `run()` returns, `_on_step_end` saves each login's cookies and origins (`_cdp_get_storage_state`, split by host) whenever they change, mode 0600, with a `.gitignore` in the folder. Page memory records `text_env` / `totp_env` instead of values, so replays and the handoff to the coding agent (`process.env.X`) use the current secrets.
52
+
53
+ **Session logins** (`src/sessionLogins.ts`). Credentials the user gives in a message or test case become a login through the `logins` tool's `remember` action: the site and its sign-in hosts as domains, `FORGEQE_<NAME>_USERNAME` / `_PASSWORD` / `_TOTP` as env names, a session file in `.forgeqe/sessions/`, and the values held in ForgeQE's process only. `executeRun` sends `allLogins(config)` (forgeqe.yaml's plus these, a remembered one replacing a configured one of the same name) and passes the goal through `redactSecrets`, which replaces each remembered password or secret with "(the password of saved login NAME)". `runTestCommand` puts the values under their env names for ForgeQE's own test runs and redacts them from the output. The brain's notes list remembered logins; its prompt says to remember credentials before the first run and never put them in goals.
54
+
55
+ **Step budget.** `max_steps` is a checkpoint, not a wall. The library's `run()` gets `3 × max_steps`, so its budget warning (at 75%) and forced last-step `done` come at the cap. At each multiple of `max_steps`, `_on_step_end` looks at the last 10 steps (`step_outcomes`: the step's actions ran without an error and the agent's own evaluation at the next step did not call it a failure) and carries on when at least 3 worked (`making_progress`), logging "Reached N steps and still making progress"; otherwise it stops the agent and the summary starts "Stopped at N steps: the last 10 steps made no progress." **Page loading.** `_on_step_start` calls `settle_page` before every step after the first: it reads `location.href`, `document.readyState`, and the body's text length over CDP every 0.25 s until the page is `complete`, or `interactive` with content, for up to 8 s (3 s while the tab stays `about:blank`), and logs a debug line when it waited a second or more. The library switches to a tab a click opened 50 ms after the click and waits at most 0.3 s before reading the page, so a heavy page used to be read blank and cost a step. The agent's own note actions (`write_file`, `replace_file`, `read_file`) are in `READ_ONLY_ACTIONS`, so they are neither recorded nor stop a replay.
56
+
57
+ **Repeated controls.** `_control_loop_exceeded` counts the same click, input, search, or select (by element index or text) in the last 8 steps. The first time one reaches 4, the agent is not stopped: `_install_nudges` wraps the library's `_inject_loop_detection_nudge`, which runs after the library clears its per-step context, and the next step's prompt gets "you have done `click "One way"` 4 times… work out why the earlier attempts did not stick… take a different approach". Counting restarts after the nudge; reaching 4 again stops the run ("…after the agent had already been told to change approach"). With a library that has no such hook, the first loop stops the run as before.
58
+
59
+ **Resuming a failed run.** Each recorded step keeps the agent step it came from (`MemoryRun.step_of`; 0 for replayed steps and the opening navigate). On a failure other than a user stop, `resume_point` picks the step where the run went wrong: where a repeated-control loop began, where the last progress window of a stalled run began, or the trailing streak of failed steps. `resume_steps` keeps the recorded steps from before it (not only navigation and waits). They go to `resume.json` in the run folder and under `resume` in the task's flow file; a later success removes them. Replay then picks, in order: the request's `resume` (from `browser_run`'s `resume_from`), the task's successful flow, the task's `resume`. `handoff_task(kind="resume")` tells the agent where the earlier run went wrong and to take a different approach there. **Checkpoints.** After each step `_on_step_end` calls `MemoryRun.checkpoint()`: the steps so far (`resume_steps` with no cut-off) go to `checkpoints/<flow file name>` in the memory folder (a separate folder, so `recent_flows` never lists them) with the run id and `interrupted: true`, the same data goes to the run folder's `resume.json`, and page memory is flushed. `MemoryRun.finish` removes the checkpoint when this run or the run it picked up wrote it, except for a stopped run; `_write_artifacts` removes the checkpoint `resume.json` from a run that did not fail and was not stopped. The lookup takes the checkpoint after the task's successful flow and before its saved `resume`, as `kind="interrupted"`, and `handoff_task` then says the earlier run was cut off and to carry on without repeating the steps. Stopped runs report tokens from the library's token counter, since `history.usage` is only filled when `run()` returns. Session-login values are also set in the engine process's environment (one process per run), so a replayed sign-in step finds them by env name.
60
+
61
+ **Session log** (`src/sessionLog.ts`). `main()` opens one log per session for the commands that run something (`startSessionLog`, in the logs folder next to `sessions`). Lines are `<ISO time> <kind> <JSON>`: `session` (versions, argv, profile, browser and memory settings, login names), `agent.ask` / `agent.turn` (written after the turn, so credentials the message gave are remembered and redacted), `agent.event` (the agent runtime's events), `tool.call` / `tool.result` (from `createMcpServer`, with duration), `tool.event` (what a tool reports while it runs: the coding and healing agents' reads, commands, edits, and test runs, suites, cards; browser run events are left to `engine.event`), `engine.request` / `engine.event` / `engine.log` (every stderr line) / `engine.exit` (from `runEngine`), `run.done` (from `executeRun`), and `error`. `serialize` hides fields named like `api_key`, `password`, `token`, or `values`, clips strings at 8,000 characters (200,000 with `--debug`), and `redactForLog` replaces remembered login values, secret-looking environment variables (name parts KEY, TOKEN, SECRET, PASSWORD, PAT, CREDENTIALS, values of 8+ characters), and API keys from `llms.json`. `--debug` / `FORGEQE_DEBUG` also sets the engine's logging level to debug; debug-level engine events go to the log only. `bundleLogs` (`forgeqe logs bundle`, `/logs`) stages the log, the run folders named by `out_dir` / `runDir` / `run_dir` in it (text files redacted, screenshots only with `shots`), `forgeqe.yaml`, profiles without keys, and versions, then `tar -czf`s them.
62
+
63
+ **Updating** (`src/update.ts`, `forgeqe update`). `findInstalls` walks PATH for `forgeqe` executables, resolves pyenv shims with `pyenv which`, and classifies each by what it resolves to: a package folder named `@capagents/forgeqe` under `.bun` (bun) or elsewhere (npm), or a script whose shebang is a Python that imports `forgeqe_cli` (pip). `latestVersion` reads the registry's `latest` tag with a cache-busting query; `npmReady` needs the version document and a HEAD of the tarball to answer 200, `pypiReady` the PyPI version JSON, because both registries accept an upload minutes before they serve it. `update` waits for them (`waitUntil`, every 15 s, 20 minutes by default), then runs `bun pm cache rm` and `bun add -g …@version --no-cache`, `npm install -g …@version --prefer-online`, or `<python> -m pip install --upgrade --no-cache-dir forgeqe-cli==version`, and reads the installs again to confirm. `scripts/publish.sh` waits the same way after uploading before it prints `Done`.
64
+
65
+ **Step verdicts.** When the goal came from a test case with steps, `CASE_REPORT_INSTRUCTION` goes into the system prompt and `output_model_schema=CaseReport` makes the engine library's `done` action return `{summary, steps: [{step, status, actual}]}`; `history.structured_output` becomes `case_report` in the result. If the Agent rejects the schema, the run continues without it.
66
+
67
+ **Finishing and the judge.** `_GOAL_VERIFICATION_SYSTEM_MESSAGE` is added to every agent's system prompt: `done` with success only when every field holds exactly what the goal asked for (pre-filled or extra values in multi-value fields removed), every setting the goal names is applied through the page's own controls and shown by the page, and findings are read from the page, items a list already shows are read from it in one step, and a tab that looks blank right after opening is still loading. The engine library's judge (`use_judge`) is off by default: `browser.judge: true` (passed to the engine as its judge setting) or `FORGEQE_ENGINE_USE_JUDGE=1` turns it on, and it then makes one model call over the steps and screenshots. It was turned off because it failed completed runs whose answer came from one screenshot (five result scores) instead of one step per item, and ForgeQE's agent then retried with ever stricter goals. When the agent says success and the judge's verdict is false, the run fails with the judge's `failure_reason` ahead of the agent's report; the verdict goes into `run.json` as `judge`. A judge call that errors leaves the agent's result as it was.
68
+
69
+ **Browser profile.** At the end of the first step (or before writing a trust-mode replay), `browser_profile` reads how the browser was set up: the engine's driver, channel and executable, headless, the default switches removed, extra HTTP headers (none), `Browser.getVersion`, and from the page `navigator.userAgent`, `navigator.webdriver`, language, time zone, and viewport. It goes into `run.json` as `browser`, and the handoff shows it so the coding agent can compare its test's browser with it.
70
+
71
+ **Timeline.** `build_timeline` writes `steps.json` from the history (and the replayed steps): URL, title, the model's evaluation, memory and next goal, each action with its error, start time (`t0`) and duration, the part of it spent waiting for the model (`model_ms`, from wrapping `llm.ainvoke` and overlapping the calls with the step's `step_start_time`/`step_end_time`), and a JPEG of the page (max 1280 wide) under `steps/`, subject to `report.screenshots`. Each action on an element carries `box`: the element's `absolute_position` from the DOM snapshot the model saw, minus the page's scroll, as fractions of the viewport, so the report can outline it on that step's screenshot. Timeouts, stops, and crashes still write it from the partial history.
72
+
73
+ **Diagnostics.** `Diagnostics` registers `Runtime.consoleAPICalled`, `Runtime.exceptionThrown` and `Log.entryAdded` on the engine library's CDP client (cdp-use allows one handler per event, and the engine library's own watchdogs don't use these three; `Network.responseReceived` belongs to the downloads watchdog, so failed and slow requests come from the page's resource timing entries, `responseStatus` and duration, at each sample). It samples at every step start and end and after each replayed step. Each new page (by URL without the hash, up to 25) is checked once: `Accessibility.getFullAXTree` for unnamed buttons, links, fields and images, heading order and the document title, plus a page script for `lang`, text contrast (WCAG ratios, skipping text over images) and zoom-blocking viewports, with up to five examples each from `DOM.getOuterHTML`. A request counts as slow from 3 s. `assign_problems` attaches each event to the step it happened in (by `t0`), at most 25 per step; `run.json` gets the totals as `diagnostics`, and the pages go to `accessibility.json`. `report.accessibility: false` skips the page checks.
74
+
75
+ **Overlay.** With a visible browser and `browser.overlay` on, `brand.Overlay` puts a `<forgeqe-overlay>` element on each page: a shadow root (page CSS can't reach it) with an edge glow, a bottom bar (icon, the step from `step_text`: its intent from `next_goal`, or the "next goal" sentence in the model's memory, and its actions in words from `plain_action`, never raw tool parameters; a Thinking / Acting / From memory chip), and a transparent blocker. Window capture listeners, added before the page's scripts, drop trusted mouse, wheel and key events while it is locked. It locks in `_on_step_end` and replay's gaps between steps, and unlocks in `_on_new_step` (which the engine library calls after the model answers and before the actions run) and around each replayed action, so the agent's own CDP input always gets through. Each page gets it from `Page.addScriptToEvaluateOnNewDocument` on its own target (the engine library's `_cdp_add_init_script` covers only the focused one), with the current text baked in and the lock off, since a page can load halfway through a step's actions. The host carries the library's DOM-exclude attribute (left out of the DOM the model reads), `aria-hidden`, and attached outside `<body>`, so the accessibility checks skip it. `brand.install()` wraps `ScreenshotWatchdog.on_ScreenshotEvent` to hide it for every screenshot (the model's and the report's) and replaces the about:blank watchdog's animation, which loads a logo from the library's CDN, with a start page. The status bar's icon is raw pixels on a canvas, so a page's `img-src` policy can't block it or log an error; the start page only appears on about:blank, which has no policy. Headless runs skip all of it.
76
+
77
+ Left out: Mongo persistence, LangGraph, Scrum Studio, and video recording. Nexus's Mongo site memory and compact replay are replaced by the file-based page memory below.
78
+
79
+ The model decides everything about navigation. With no pinned URL the browser starts blank and the agent opens the site itself, even when the goal names one. Nexus's `_infer_initial_url_from_goal` regex is removed, and the engine library's own URL guessing from the task (`directly_open_url`) is turned off. Only a pinned start URL (`--url`, `start_url`, or the `start_url` argument the agent passes when the user named a page) is opened before the agent's first step.
80
+
81
+ `fd 1` inside the engine is redirected to stderr at start-up, so library prints cannot corrupt the event stream.
82
+
83
+ ## The path a test follows (`capture.py`)
84
+
85
+ A passed run becomes a test, so the run records the path that worked, checked while the page is still there, not every attempt.
86
+
87
+ **What changed.** `install_effects` wraps the agent's `tools.act`. Around each click, key press, script, and `click_selector` it takes a fingerprint of what a user could notice (address, visible text, element count, open menus and dialogs, form values, selected and pressed states) and polls for up to 1.5 s for a change. `_record_memory_results` leaves an action that changed nothing out of the flow (a click on a decorative node, a retry that missed, a script that only read the page) and lists it in `run.json` `memory.left_out`; fills and selects always stay. A script that did change the page stays, marked `scripted`, and the handoff asks for the page's own control instead.
88
+
89
+ **The control, and locators checked on the live page.** Before the step's actions run (the library's new-step callback comes before `multi_act`), `_point_at_controls` re-points each element action from the node under the pointer to the control a user acts on (an icon inside a label or button becomes the label or button) and `verify_locators` checks each candidate on the live page: role and name through `Accessibility.queryAXTree` (the accessible names Playwright's `getByRole` uses), ids, CSS, attributes, text, and XPath in the page. A locator is `verified` when it finds exactly that element (or something inside it). Verified locators come first, so `best_locator` is one that worked.
90
+
91
+ **Real clicks, not scripts.** The agent's prompt (`CAPTURE_SYSTEM_MESSAGE`) asks it to act only through browser actions, and `click_selector` gives it a real mouse click (scroll into view, refuse when covered, `Input.dispatchMouseEvent`) on an element it found by CSS, captured like any click.
92
+
93
+ **Checks with their element.** `record_check` finds the smallest visible element showing the text and the section it sits in, each with verified locators (`element`, `container` in `workflow.yaml` `checks`), and takes `dynamic` for live data (today's date, a price, a count).
94
+
95
+ The handoff lists the path with ✓ on verified locators, says how many actions were left out, gives each check its locator and section, and leaves out page memory when every step and check is verified. Scribe is told to use ✓ locators as given and not to look for those elements again.
96
+
97
+ ## Page memory (`memory.py`, `replay.py`)
98
+
99
+ Files under `memory.dir` (default `.forgeqe/memory` next to `forgeqe.yaml`, or `~/.forgeqe/memory` when there is no config file), written atomically at the end of a run:
100
+
101
+ - `pages/<host>/<path>.json`: one per page template. The key is host (plus port) and path; segments that are numbers or look generated become `*`, and the query string is dropped. Each element stores tag, role, accessible name, text, stable attributes (never `value`), its locators, `best`, `confidence`, visit and use counts, and `first_seen` / `last_seen` / `last_used` / `last_updated`. Pages keep at most 400 elements; past that, elements never used and least recently seen go first.
102
+ - `flows/<goal>-<hash>.json`: keyed by the normalised goal and start URL. It holds the steps of the last successful run, each with the element's top locators and a fingerprint, plus pass, fail, and replay counts.
103
+ - `scribe-notes.json`: written by the coding side (`src/code/notes.ts`), not the engine: per repository, Scribe's notes from its reading turn and the tests jobs wrote there.
104
+
105
+ **Observe.** Before each agent step, `_on_new_step` passes the engine library's `selector_map` (`EnhancedDOMTreeNode`s) to `MemoryStore.observe`. For every element it builds the locator candidates, counts how many elements on the page each one matches, and merges them into the page file. An element keeps its identity across visits through a strong locator (test id, id, name, role, label, placeholder, href), then its XPath, then a hash of tag and anchor text. An XPath match counts only when the role and accessible name (or text) agree too, because sites reuse one popup container for different widgets; an element whose label changed becomes a new entry. When replay resolves a step on a different entry than the one recorded, `record_stale` marks the recorded entry's dead locators as missed and failed, and the flow is saved against the new entry. `flow_step` lists the locators that matched on the page at that moment ahead of those only seen on earlier visits.
106
+
107
+ **Confidence** is `100 × base × uniqueness × stability × reliability`, clamped to 1–99:
108
+
109
+ | Factor | Value |
110
+ |--------|-------|
111
+ | base | by kind: test id .96, id .90, role .88, name .86, label .84, placeholder .82, alt .78, href .74, title .72, text .70, CSS .50, XPath .30; ×0.4 when the value looks generated |
112
+ | uniqueness | 1 when it matches one element, else 0.5 / (matches − 1) |
113
+ | stability | (0.75 + 0.25 × min(seen, 4) / 4) × seen / (seen + missed) |
114
+ | reliability | (successes + 1) / (successes + failures + 1) |
115
+
116
+ **Record.** `_on_step_end` pairs each action's result with the element it targeted. It records success or failure against the element and the locator, and adds non-read-only actions to the run's flow. At the end, a complete run replaces the flow's steps, a failed run only counts a failure, and a stopped run leaves the flow alone.
117
+
118
+ **Replay.** Before the agent is built, `replay_flow` runs the stored steps with `Tools().act`, the same actions the agent would call. Only this task's own steps are replayed: the exact goal's flow, a failed run's `resume` steps, or a cut-off run's checkpoint. A different goal replays nothing; `prompt_hints` gives the agent the steps of up to 2 recent flows of other tasks on the host and the controls that worked there, and the agent decides which apply (no keyword rules). Element steps take a fresh snapshot (retrying briefly while the page loads) and resolve the element by the store's current best locator, then the step's own locators, then the fingerprint. Read-only actions (`extract`, `find_text`, `done`, …) are skipped. Replay stops at a step it cannot resolve or restore: password fields, file uploads, tab switches, `evaluate`. `handoff_task` then tells the agent what was already done: check and finish (`verify`), or continue from the stopped step. `trust` returns success with no agent only when every step of this same task ran. Replayed steps are emitted as `step` events with `"source": "memory"`, before any agent steps. A replayed step whose recorded locators no longer match anything on the page, but that still resolved through another locator, is recorded as a heal (`{step, line, label, stale, used}`); when replay stops and the agent then finishes the case, `run.json` notes `repaired` with the step and reason. Both go under `memory` in `run.json` for the report.
119
+
120
+ **Parallel runs.** `flush` and `save_flow_result` hold an `fcntl` lock on `<memory>/.lock`. Each page file has a `revision` counter. If the file on disk has moved on since this run loaded it, the run merges instead of overwriting: every visit, match, miss, success, and failure counter becomes the saved value plus this run's own change, elements this run never saw keep the saved copy, and the revision becomes one past the higher of the two. Two runs on the same page (a suite with `parallel` above 1) therefore both count.
121
+
122
+ **Hints.** `prompt_hints` adds the last successful steps on the same host, and the controls those runs actually used, to the agent's extended system prompt, capped at 2,500 characters, and tells the agent to act on them in one turn.
123
+
124
+ **In-run history.** Fast mode passes `max_history_items=8` and turns message compaction off. Each later step of an exploration keeps the first step and the most recent ones, instead of resending the whole transcript or pausing for a summary model call. `FORGEQE_ENGINE_MAX_HISTORY_ITEMS` overrides the cap (minimum 6, which is the engine library's own floor).
125
+
126
+ **Workflow steps.** `compact_step` gives each workflow step its `best_locator` and ranked `locators` (with `css`/`attr` where the locator needs them), `evaluate` steps their `code`, and every step the browser agent's `note`, which the handoff passes to the coding agent. `_on_step_end` passes `agent_note(model_output)` (the step's evaluation, memory, and next goal, each once) to `_record_memory_results`, which stores it on each flow step that step recorded; flows keep it, so replayed steps carry the note of the run that first took them. The engine writes no test script: the coding agent writes the test in the repository's language, and ForgeQE runs it.
127
+
128
+ ## Bridge (`src/engine`)
129
+
130
+ The launchers (`bin/forgeqe.js` for npm, `py/forgeqe_cli/launcher.py` for pip) use a Bun 1.3+ from `FORGEQE_BUN`, PATH or `~/.bun/bin`, else download the pinned `BUN_VERSION` from Bun's GitHub releases into `~/.forgeqe/tools/bun-<version>` after checking it against `SHASUMS256.txt`. Both pin the same version (a test checks this). The pip launcher runs the checkout's `src/index.ts` when it sits in the repo, else `bun x @capagents/forgeqe@<its version>`.
131
+
132
+ `src/tools.ts` downloads a release archive into `~/.forgeqe/tools/<name>-<version>`, checks its SHA-256 (from the release's checksum file, or the digest GitHub's release API records), unpacks it with `tar` (System32's bsdtar on Windows, which reads zip), and removes older versions. uv and the agent runtime use it; the launchers carry their own copy for Bun because they run before any TypeScript. `code/runtime.ts` uses the runtime from `code.runtime`, PATH, or an existing install, else downloads the pinned runtime, whose version matches the agent runtime SDK in `package.json` (a test checks this). `forgeqe setup` calls it, and so does every command that can start the agent.
133
+
134
+ `setup.ts` finds or creates the engine environment: `FORGEQE_PYTHON` when that Python passes the engine check (otherwise it logs why and skips it), else `~/.forgeqe/engine/.venv`, which is rebuilt when its engine check fails (created with `uv venv --python ">=3.11"`, or `python3.11+ -m venv` when no uv can be found or downloaded). uv is the system one, else the pinned `UV_VERSION` downloaded from uv's GitHub releases into `~/.forgeqe/tools/uv-<version>` (checked against its `.sha256`, unpacked with `tar`, which on Windows is System32's bsdtar). A downloaded uv keeps the Pythons it fetches in `~/.forgeqe/tools/python`. It installs the engine's pinned Python libraries and records the version in `installed.json`, so later runs skip setup. When Google Chrome is missing it installs Chromium through Playwright.
135
+
136
+ `bridge.ts` spawns the engine with `PYTHONPATH` pointing at the packaged `py/`, sends the request (goal, profile, start URL, headed, max steps, allowed hosts, extra system prompt, browser skills with `skills_pick` and `skills_max_chars`), and maps events:
137
+
138
+ | Engine event | Transcript |
139
+ |--------------|------------|
140
+ | `log` | grey line |
141
+ | `memory` | `↻` line, e.g. `Replaying 4 remembered steps` |
142
+ | `step` | step counter, the model's next goal; `(memory)` for replayed steps |
143
+ | `result` | ✓ / ✗ per action, e.g. `click "Search": element not found` |
144
+ | `usage` | token counter |
145
+ | `done` | final status, summary, artifact list, memory counts (`Memory` row in the summary) |
146
+
147
+ Esc or `/stop` writes `stop`; the engine stops the agent and still writes artifacts from the live trace. An engine still running `STOP_GRACE_MS` (30 s) later gets SIGTERM, then SIGKILL `KILL_AFTER_MS` (10 s) after that, and the run reports stopped with a note that its artifacts may be incomplete. `executeRun` also gives the bridge a hard deadline (`engineDeadlineMs`: twice `browser.timeout_seconds` plus five minutes), because the engine's own timeout covers only the agent, not browser start-up, skill choice (itself capped at 60 s), or memory replay; a run past it is stopped the same way and reported as failed.
148
+
149
+ **Environment.** The engine is spawned with an allow-list (`allowListEnv` in `src/childEnv.ts`): system settings (PATH, HOME, locale, display, temp folders, proxies, certificates, Windows system variables), `BROWSER_USE_*`, `FORGEQE_ENGINE_*`, Python and uv settings, the active profile's key variable, and the saved logins' variables. Other secrets in the terminal, such as tracker tokens, never reach the browser agent.
150
+
151
+ ## ForgeQE's agent (`src/brain`, `src/mcp`)
152
+
153
+ Every message in the shell and every `forgeqe run "<request>"` goes to one agent runtime session running the `forgeqe` agent. Nothing is parsed before it: the agent decides whether the text is a browser goal, test case references, a test to write, or a pipeline task, and asks (outcome `needs_input`) when it cannot tell. The only slash commands left are session controls (`src/commands.ts`: `/stop`, `/headed`, `/profile`, `/logs`, `/skills`, `/clear`, `/help`, `/exit`); any other line, including one starting with `/`, is text for the agent.
154
+
155
+ ```
156
+ shell / forgeqe run ──► openBrain ──► agent runtime session (forgeqe agent, BRAIN_PROMPT + environment notes)
157
+ ▲ │ │ forgeqe_* tool calls (MCP, Streamable HTTP, bearer token)
158
+ │ ToolEvents └── createMcpServer ◄┘
159
+ └──────────────────────── TOOLS: browser_run, run_test_cases, … (same process)
160
+ ```
161
+
162
+ | Module | Contents |
163
+ |--------|----------|
164
+ | `brain/prompt.ts` | `BRAIN_PROMPT`: the tools (every CLI command is one, so the agent uses the tool instead of telling the user to run a command), how to choose a source, find vs run, signing in, writing tests (`write_test`; small hand edits verified with `run_test`), the CI pipeline procedure, fixing broken tests (`heal_tests`), running existing repo tests (`find_tests` then `run_tests`, never without the user's confirmation; sprint dates only from `sprint`), monitoring, ForgeQE's own docs and setup, and the rules (no secret values, push only with `push_branch`, stay in the working directory). `BRAIN_SCHEMA`: `{reply, outcome: done \| failed \| needs_input}` |
165
+ | `brain/index.ts` | `openBrain` starts the MCP server on 127.0.0.1 with a random token (`localToolServer`), then an agent runtime session with the prompt, the session notes, the schema, and the server as a remote MCP entry (6 h tool timeout). `ask` returns the reply and outcome, `abort` stops the session and every running tool call. `brainUnavailable` is why it cannot run (no model set up and no `code.model`, or no agent runtime); callers then run the text as one browser goal. `sessionSetup` picks the working directory and whether passed runs become tests from `--repo`, `--no-code`, and `code.repo` (default: tests in the current folder) |
166
+ | `mcp/tools.ts` | `TOOLS` and `environmentNotes` (working directory, run and memory folders, configured test case systems, suite defaults, timeouts, the profile, and the names, never values, of keys set in the environment). Each tool reports `ToolEvent`s (run-start/event/done, suite-start/event/done, info, `agent` for a nested coding or healing session's events, `card` for a result panel) to whoever shows them. `ToolContext` carries `session` (`shell`, `once` for `forgeqe run`, `server` for `forgeqe mcp`), `waitForUser` and `ask` when a person can answer, and `openSession` so tests can script the nested agent runtime sessions |
167
+ | `cases/save.ts` | `save_test_cases`: `saveAdoCases` creates Test Case work items (`adoStepsXml` writes `Microsoft.VSTS.TCM.Steps` the way `adoCases` reads it) linked to the story with `TestedBy-Reverse` in its area and iteration; `saveJiraCases` creates issues (type Test by default) with the steps in the description and an issue link. Both reuse a case already linked to the story under the same title. On unless `sources.save_cases: false`; `environmentNotes` tells the agent which |
168
+ | `mcp/commands.ts` | `COMMAND_TOOLS`, one tool per CLI command, appended to `TOOLS`. `write_test` runs the coding loop (`runCodeAgent`) and `heal_tests` runs `runHeal`, forwarding their events as `agent` events and ending with a card; each opens its own agent runtime server on port 0, so it nests inside the brain's session. `monitor`: `round` (one `runMonitor --once`, refused while a background monitor uses the folder), `start`/`stop`/`status` (background monitors, only in `shell` and `server` sessions), `test_alerts`. `browser_run` takes `purpose` (`test` or `task`); `taskReason` treats a `task`, or a run that starts on, ends on, or names a configured source system's host (Azure DevOps, Jira, Confluence, TestRail, Zephyr, qTest, Xray), as a task: it writes `task.json` (`TASK_FILE`) in the run folder, returns no `write_test` next step, and `write_test` refuses that folder. `browser_run` also takes `resume_from` (a failed run's folder or name, read by `loadResume` in `run.ts`) and, after a failure with resume steps, returns them with a `next_step` naming the folder; the brain's prompt says to retry with it and say what to do differently. `logins`: `remember` (session logins), list (configured and remembered), clear (a saved session, and a remembered login's credentials and session file), and `sign_in` through `signIn` and `ctx.waitForUser` (refused without it). `forgeqe_setup`: profiles, MCP servers, `docsAbout` (README and HOW_TO_USE split at headings and scored against the topic, since the agent's `external_directory` rule keeps it out of the package folder), `install_engine`, `init`, `skills` (`op` list, show, or new: `newSkill` into the project's or the user's skills folder) |
169
+ | `card.ts` | `Card` (`status`, `title`, `stats`, `body`) and `cardText`. `codeCard`, `healCard`, and `roundCard` build cards from the same lines as the terminal summaries |
170
+ | `mcp/server.ts` | JSON-RPC for MCP by hand: `initialize` (protocol 2025-06-18, 2025-03-26, 2024-11-05, with `instructions`), `ping`, `tools/list`, `tools/call`, `notifications/cancelled`. `serveHttp`: Streamable HTTP on 127.0.0.1, checking the bearer token in constant time (`tokenMatches`) and refusing requests whose `Origin` is not this machine (`originAllowed`), so a web page cannot call the tools; tool calls answer as an event stream with a progress notification every 10 s and a keep-alive every 15 s, so the agent runtime's per-call timeout (which resets on progress) never fires during a long suite. `serveStdio`: newline-delimited JSON-RPC for `forgeqe mcp`; closing stdin aborts running calls. `localToolServer`: a server over HTTP with a fresh token and the agent runtime's MCP entry for it, shared by the brain and the healing session |
171
+ | `mcp/git.ts` | `pushBranch`: finds the default branch (`symbolic-ref refs/remotes/<remote>/HEAD`, else `ls-remote --symref`), refuses it and `main`/`master`, checks the name with `git check-ref-format`, and runs `git push --set-upstream <remote> HEAD:refs/heads/<name>` without force. `setCiSecret`: `gh secret set NAME` with the value on stdin, or `az pipelines variable create\|update --secret true` with the value in `AZURE_DEVOPS_EXT_PIPELINE_VAR_<NAME>`; the value is scrubbed from any error |
172
+ | `mcp/external.ts` | `externalMcp` turns `sources:` and `mcp:` into agent runtime MCP entries: `azure_devops` (`npx -y @azure-devops/mcp <org> -a pat\|azcli -d <domains>`), `atlassian` (`uvx mcp-atlassian` with `JIRA_URL` and `JIRA_USERNAME` + `JIRA_API_TOKEN` or `JIRA_PERSONAL_TOKEN`, plus `CONFLUENCE_*` when `sources.confluence` is set, `TOOLSETS=default`; or the hosted Rovo server at `mcp.atlassian.com/v2/mcp` with Basic auth, which also covers Confluence on the same site), `confluence` (`uvx mcp-atlassian` for Confluence alone when Jira's server doesn't cover it), and every `mcp:` server, which replaces a built-in one of the same name. Secrets are `{env:NAME}` references that the agent runtime substitutes; values that must be derived (base64 of `forgeqe:<PAT>` for Azure DevOps, of `email:token` for Rovo) are put in `FORGEQE_*` variables of ForgeQE's own environment before the agent runtime server starts. A system whose server can't start here (no npx or uvx, missing token, Azure DevOps Server, Rovo on Server/DC) is listed with the reason. The servers get a 120 s timeout, which also covers the first `npx`/`uvx` download |
173
+ | `cases/dashboards.ts` | `createAdoDashboard` for the `create_ado_dashboard` tool, since Microsoft's Azure DevOps MCP server has no dashboard or saved-query tools: runs each widget's WIQL once (`$top=1`) so a bad query fails before anything is written, saves it under `Shared Queries/ForgeQE` (creating the folder, PATCHing a query of the same name), then POSTs the dashboard (`7.1-preview.3`, team-scoped when a team is given) with count tiles (`QueryScalarWidget`, `{queryId, queryName}`) across the top, then work item lists (`WitViewWidget`, `{query: {queryId, queryName}}`) and markdown two to a row. Widget `settings` are JSON strings, as the API requires |
174
+ | `cases/wiki.ts` | `writeAdoWikiPage` for the `write_ado_wiki_page` tool, since the MCP server's wiki tools need an existing wiki: picks the named wiki or the project wiki, creating `<Project>.wiki` (`type: projectWiki`, project id from `_apis/projects`) when the project has none, PUTs missing parent pages empty, then PUTs the page with `If-Match` set to its ETag when it already exists. Code wikis get `versionDescriptor` for their first published branch |
175
+ | `cases/api.ts` | `queryApi` for the `query_api` tool: GET under the Azure DevOps organization (and its `vsrm`/`vssps`/`almsearch` hosts, with `api-version=7.1` added) or the Jira site, and Xray Cloud GraphQL queries (mutations refused). Absolute URLs on other hosts are refused, so credentials only go to their own system. Answers over 40,000 characters are cut |
176
+
177
+ After the session opens, `openBrain` reads `client.mcp.status()` and reports each external server as connected, failed (with the error), or needing a browser sign-in; skipped systems are reported with their reason. The agent's session notes list the servers and say that a server whose tools are missing did not connect, so the agent falls back to `query_api`.
178
+
179
+ Tools take structured arguments. `run_test_cases` and `find_test_cases` get a list of `CaseSelector`s (`{source: azure_devops \| jira \| xray \| testrail \| zephyr \| qtest \| file \| text, ids, plan, suite, run, cycle, folder, jql, paths, text}`) that `casesFrom` reads directly, so the model's choice of source is never re-parsed.
180
+
181
+ **Permissions.** The agent's bash is allowed except `git push*`, `git * push*` (so `git -C . push` is caught too), and `gh repo delete*`, each also with a leading `*` so a push wrapped in another command (`env git push`, `sh -c 'git push'`) is caught; the agent runtime matches each command of a chain on its own and applies the last matching rule. These rules are guard rails, not a sandbox. `forgeqe_*` tools are allowed. `external_directory` allows only the run and memory folders. Tool calls to `forgeqe_*` are hidden from the `⚙` lines because their `ToolEvent`s already render richer output (browser steps, suite cards).
182
+
183
+ **Callers.** The TUI opens the brain on the first message and keeps it for the session, so follow-up answers and "write a test for that run" have context; `/profile` (or Tab) closes it and keeps the turns so far: the next agent session's first prompt is `carryOver(turns, text)`, the last 20 turns with remembered secrets redacted, so switching models does not lose the conversation. It renders `card` events as result panels and `agent` events as `⚙` and test lines with their own token counter, and its `waitForUser` pushes the prompt into the transcript and opens a **Your turn** dialog (Done, continue / Cancel); opening another dialog or Esc settles it as cancelled. `forgeqe run` opens the brain for one request, prints tool events (cards as `cardText`) and the reply as plain lines, waits for Enter in `waitForUser` when stdin is a terminal, and exits 0 done, 1 failed, 2 stopped, 3 needs input. `forgeqe run --direct` and a missing agent runtime run `browser_run` directly. `forgeqe mcp` serves the same `TOOLS` on stdio and logs `ToolEvent`s to stderr; it has no `waitForUser`. `forgeqe start` and `forgeqe mcp` stop background monitors when they return, because `src/index.ts` calls `process.exit`.
184
+
185
+ ## Scribe, the coding agent (`src/code`)
186
+
187
+ The test-writing loop used by `forgeqe code`, by `forgeqe suite` with a repo, and by the `write_test` tool, which is how ForgeQE's agent writes tests in the shell and `forgeqe run`. It uses the agent runtime SDK and the runtime binary that `code/runtime.ts` finds.
188
+
189
+ ```
190
+ run dir ──► handoff.ts ──► handoff.md ──► Scribe session (forgeqe-test agent) ─► test file + command
191
+ ▲ │
192
+ ├──── fix prompt: exit code + output ◄── testrun.ts (ForgeQE runs it, watches for a browser)
193
+ │ │ exit 0 with a browser
194
+ └──── review issues ◄──────────────────── review.ts (forgeqe-review agent, read-only)
195
+ ```
196
+
197
+ | Module | Contents |
198
+ |--------|----------|
199
+ | `handoff.ts` | Builds `handoff.md` from `run.json`, `workflow.yaml`, and the page-memory files for every page the run visited: the result and final URL labelled as one run's live data, the judge's reasoning, the browser the run used (`run.json` `browser`), steps in order with values and the JavaScript the agent ran, each group led by the browser agent's note from that step (`process.env.NAME` for secrets), best and ranked locators with confidence (at most `MAX_LOCATORS` = 4, under `MIN_CONFIDENCE` = 30% dropped unless nothing else is left), and a table of each page's other elements (up to `MAX_ELEMENTS` = 8 of `MAX_PAGES` = 4 pages; elements whose best locator a step used are left out, the rest need an interaction or 70% confidence; no table when nothing is left; the table is skipped when a replay pass has page snapshots), and the browser agent's checks (`workflow.yaml` `checks`), each with the locator the replay matched on the last page when the counts agree |
200
+ | `prompt.ts` | `TEST_RULES` (shared with healing: browser set up like ForgeQE's, the debugging steps when the test's page differs, generic page objects, data in tests, rules not values, pop-ups handled wherever they appear, every A/B version of a page handled), the test agent's system prompt (start from the notes, port the replay script, assert the handoff's checks and nothing else, never guess locators, reuse and extend page objects, do not run the test: ForgeQE runs it; look at the live page with the `live_browser_*` tools once a step has failed), the result JSON schema (`status`, `blocked_reason`, `remote_browser`, `test_file`, `command`, `reused`, `changed`, `notes`), and the prepare prompt (notes of at most 25 lines), the first (with the repo notes, naming the live browser's output folder when it is on), fix (the short output plus the saved log and page snapshot), missing-command, no-browser, blocked-challenge, stall, and review-fix prompts |
201
+ | `review.ts` | The `forgeqe-review` agent: prompt, `REVIEW_SCHEMA` (`approved`, `summary`, `issues[{file, problem, fix}]`), and `openReviewer`, a second, read-only Scribe session opened on the first passing run and kept for later attempts. `reviewPrompt` inlines the checks from the handoff and the changed files (`changedFiles`: up to 12 KB each, 40 KB in all), so the reviewer reads nothing again. It may load Scribe's skills and judges the test against their conventions too |
202
+ | `runtime.ts` | Maps the `llms.json` profile to an agent runtime provider, builds the config, starts the agent runtime server, and drives one session over the SDK. With `resume`, `openSession` reopens that session with `session.get` (the runtime keeps sessions on disk, so their messages survive a restart) and sets `resumed`; when it is gone, a new one is created. `FailedCommands` counts each exact shell command the agent runs that exits non-zero; at `stallAfter` failures of one command the turn is aborted and `prompt()` returns `stalled` (`command`, `count`, the last 40 lines of output). `resetStall()` clears the counts `READ_AGENT` (`forgeqe-read`) has the test agent's instructions with `edit` and `live_*` denied; the config builder takes it in `agents`, and a prompt picks it with `agent`. `PromptOptions` also carries `structured` (off: no JSON answer) and `tools` (per-prompt switches, used only for `live_*`: the agent runtime keeps them for later prompts, and `true` would override the agent's path rules for file tools) |
203
+ | `livebrowser.ts` | `liveBrowser(runDir)`: the `live` MCP server for the agent, Microsoft's Playwright MCP (`npx -y @playwright/mcp@0.0.83 --config …`), with a config written to `<run>/live-browser/config.json` from `run.json` `browser` (channel or executable, headless, viewport, locale, time zone, user agent) and snapshots and screenshots saved in that folder. `null` without `npx`. The loop switches its tools (`live_*`) on per prompt once a step fails |
204
+ | `replay.ts` | The replay check. `replayScript(steps)` turns `workflow.yaml` into `flow.mjs`, one `step(n, action, run)` per recorded step, with each locator rebuilt from its type and value (`locatorExpr`), text typed with `pressSequentially`, secrets read with `env(NAME)` and one-time codes made with `totp(NAME)`; actions with no Playwright equivalent are marked "not replayed". `alternatives.mjs` holds each step's other locators. `ensurePlaywright` installs `playwright-core` (`PLAYWRIGHT_VERSION`) with Bun into `~/.forgeqe/tools` and removes older versions. `replayRun` writes a runner that launches the run's browser (`replayOptions`: channel or executable, headless, viewport, locale, time zone, user agent, extra headers), runs each step with a 10 s timeout, keeps an aria snapshot of each page after each step that worked (by host and path, the latest one; the final page's taken once two snapshots a second apart agree, up to 6 s, since a page can change its URL at once and re-render later) and writes them as `<setup>-page-N.aria.yml` in `ReplayPass.pages`, and on a failure saves a screenshot and an aria snapshot, counts the other locators' matches, and records console errors, failed requests, and HTTP errors; three failures in a row stop it. It runs under Bun with the session logins' env, 180 s per pass. The first pass uses Playwright's default launch; if it does not reach the run's final page (host and path), a second pass leaves out `--enable-automation` and adds `--disable-blink-features=AutomationControlled`. `replaySection` renders the handoff section (the page snapshots listed; a pass whose failed steps were followed by steps that all worked and that reached the final page, `reachedPastFailures`, reads as a working script with those steps to judge, and `replayFailed` is false for it so the live browser stays off): each pass, the failed steps with Playwright's reason, the page, the open dialogs from the snapshot, the other locators' matches, and the saved files, and the launch settings to copy when only the second pass got through. After the last step the runner looks up each recorded check on the page (`findCheck`: a visible exact heading, then exact text, then a looser heading or text, then a form field holding the value) and returns its locator, match count, and up to 3 containers in `ReplayPass.checks`; for a heading match, `sectionOf` adds `section` (the element `aria-labelledby` the heading, else the `section`/`article`/region it heads, else the element after it or after its only-child wrapper) with a stable locator (test id, id, or `aria-label`, else empty) and its visible link and list item counts |
205
+ | `testrun.ts` | Runs the reported command with `sh -c` in the repo (`CI=1`, `PLAYWRIGHT_HTML_OPEN=never`, no colour), with a timeout. `promptOutput` makes the prompt's copy of a failed run: `withoutPageDumps` takes out each "Aria snapshot:" block Playwright puts in a failed assertion, then `headAndTail` keeps the first 8 KB and the last 4 KB; the full output goes to `<run>/test-runs/attempt-N.log` and the first page dump to `attempt-N-page.aria.yml`. While it runs it samples the process tree (`ps -A -o pid=,ppid=,args=` every 250 ms; `Win32_Process` every 1.5 s on Windows) for browsers at or below the command that carry an automation switch (`--remote-debugging-pipe`, `--remote-debugging-port`, `-juggler-pipe`, `--inspector-pipe`, `-marionette`) and returns them as `browsers` |
206
+ | `notes.ts` | `scribe-notes.json` in `memory.dir`: per repository, the reading turn's notes (`saveNotes`), when they were written, and up to 15 tests jobs wrote there (`addWrittenTest` on approval). `readNotes` returns nothing past 30 days; `notesSection` renders them for the first prompt |
207
+ | `worktree.ts` | The working tree before the job (`git status --porcelain -uall` with a hash of each file, and a copy of each file up to 5 MB, plus ForgeQE's `forgeqe.yaml` and `llms.json`); the paths the job changed since then; putting back files the job deleted or uncommitted edits it reverted to the committed version; and the protected paths in the form the agent runtime matches them |
208
+ | `loop.ts` | The attempt loop, stop handling, the replay (`code.replay`, on by default in `forgeqe.yaml`: `replayRun` runs while Scribe reads the repo, then the handoff is rebuilt with the replay and the result goes to `code.json` `replay`; skipped when `canReplay` finds no step beyond navigation and no checks), the reading turn (`READ_AGENT`, no structured answer, with or without a replay; skipped when `readNotes` has notes for the repo, which go into the first prompt instead; its reply is saved as the notes), the live browser (`code.live_browser`, on by default in `forgeqe.yaml`; its tools are off until the replay or a test run fails, a stall, a no-browser run, or a blocked report, and the prompt that switches them on says so), the stall guard (`code.stall_after`, default 5; counts reset after each ForgeQE test run), `code.json` (the run folder is recreated if it is gone, and a failed write is reported without changing the result), and resuming: `scribe-session.json` in the run folder (session id, repo, stage `reading`/`writing`, whether the live browser is on) is written when the session opens and updated as the job goes, and removed when the job ends with a result; after a stop, a crash, or a `ProviderAuthError`/`APIError` it stays, and a rerun for the same run and repo passes it as `openSession`'s `resume`. A reopened session skips the reading turn, takes `savedReplay` when the replay finished, and starts with `carryOnPrompt` (or `RESUMED_NOTE` plus the first prompt when it was still reading) |
209
+
210
+ **Config.** The whole agent runtime config is passed through the environment, so nothing is written into the repo. Keys are `{env:NAME}` references, never values. Profile mapping:
211
+
212
+ | Profile | Agent runtime provider | Model |
213
+ |---------|-------------------|-------|
214
+ | `azure` | `azure`, `baseURL: <endpoint origin>/openai/v1` (Responses API, `api-key` header) | `azure/<deployment>` |
215
+ | `openai` | `openai`, `baseURL` when set | `openai/<model>` |
216
+ | `openai_compatible`, `ollama` | `forgeqe` via `@ai-sdk/openai-compatible` and the profile's `base_url` | `forgeqe/<model>` |
217
+
218
+ `code.model` skips the mapping and uses the agent runtime's own provider setup. The `forgeqe-test` agent is a primary agent with its own prompt and permissions:
219
+
220
+ - `edit`: allowed, except ForgeQE's files and folders (`.forgeqe/`, `forgeqe.yaml`, `llms.json`, the run and memory directories). The agent runtime matches edit patterns against the path relative to the git worktree root (or the filesystem root outside git), so the patterns are built the same way
221
+ - `bash`: allowed, except `git push*`, `git * push*`, `gh repo delete*`, commands that name ForgeQE's paths, and commands that throw away uncommitted work (`git clean*`, `git reset --hard*`, `git stash*`, `git restore*`, `git checkout -- *`, `git checkout .*`), each also when wrapped in another command. Shell rules cannot catch every spelling, so after each attempt `restoreRemoved` also puts back ForgeQE's files (`forgeqe.yaml`, `llms.json`) when the job changed them, not only when it deleted them; healing does the same at the end of its job, with the same edit and shell rules
222
+ - `webfetch`: allowed
223
+ - `question`: denied
224
+ - `external_directory`: allowed for the run and memory directories, rejected elsewhere
225
+ - `doom_loop`: rejected, which tells the model to change approach
226
+
227
+ The repo path is resolved with `realpath` (macOS `/var` is `/private/var`), and both forms are allowed.
228
+
229
+ **Events.** `event.subscribe` streams the session. Tool parts become `⚙` lines with repo-relative paths (`edit tests/login.spec.ts`, `bash npx playwright test …`). Finished text parts become the agent's thoughts, and `step-finish` tokens feed the token counter. Permission requests the config does not settle are declined straight away (`declineMessage` tells the model why), so the session never waits for a person and nothing outside the config is granted: not paths outside the repository, not repeated identical calls, and not the runtime's own "ask" defaults such as reading `.env` files.
230
+
231
+ **Environment and secrets.** `openSession` starts `opencode serve` itself (`startServer`) with an explicit environment (`serverEnv`), never by changing `process.env`: the terminal's environment without ForgeQE's own secrets (`privateEnvNames`: every profile's key variable, tracker and alert credentials, variables `mcp:` servers reference) except those its config references as `{env:NAME}`, plus the values ForgeQE derives for MCP servers (`secrets`, e.g. base64 of email:token). A runtime plugin (`hideEnvPlugin`, the runtime's `shell.env` hook) then blanks all of those, and `OPENCODE_CONFIG_CONTENT`, in every shell command the agents run, so a prompt injected by a page under test cannot read the model key or tracker tokens. Saved-login variables stay visible: the tests read them.
232
+
233
+ **Result.** Each prompt sends `format: {type: "json_schema"}`, and the model answers through the agent runtime's `StructuredOutput` tool. The result is read from the `message.updated` event (`info.structured`, or `info.error`), not from `session.messages`: in the pinned agent runtime that endpoint returns BadRequest for any session that used `format`.
234
+
235
+ **Loop.** ForgeQE does not trust the model's claim that the test passes. Scribe does not run the test; it reports the command and ForgeQE runs it. A non-zero exit or a timeout sends a fix prompt with the command, exit code, the short output (`promptOutput`), and the paths of the full log and the page snapshot. An exit 0 with no browser under the command sends the no-browser prompt, unless the agent reported `remote_browser`. A missing command sends a reminder. A failure whose output matches the previous one apart from numbers (`failureKey`) gets the repeated-failure prompt (look at the failing step before changing code); a third one in a row ends the loop as `failed` with the output in the notes. A stalled turn (one command failed `stall_after` times inside it) gets the stall prompt with the command and its output once; a second stall ends the loop as `failed` with that output in the notes. A passing run with a browser goes to the reviewer, with the paths this job changed and the paths the user already had uncommitted, so it never asks for the user's work or ForgeQE's files to be removed; its issues go back as the next prompt. After every agent turn, files that existed before the job and are now gone or reverted are put back, `handoff.md` is rewritten if missing, and the next prompt says what was restored. Each of these counts as an attempt. The first `blocked` report is answered with the blocked-challenge prompt (compare the test's browser with the handoff's "Browser ForgeQE used" and fix the difference); a second one ends the loop as `blocked`. The loop ends when a passing run is approved (`passed`), after `max_attempts` (`failed`), when the reviewer gives no verdict (`failed`), on `ProviderAuthError`, `APIError`, `ContextOverflowError`, or `ContentFilterError` (`failed`), or on Esc (`stopped`, which aborts the session). Both servers are closed in every case, and the reviewer's tokens are added to the result's usage.
236
+
237
+ The `forgeqe-review` agent overrides the permissions key by key (`AgentSpec.permission`): `edit`, `webfetch`, and `task` denied, and `bash` denied except `git diff*`, `git status*`, `git log*`, `git show*`, and `ls*`.
238
+
239
+ ## Skills (`src/skills`)
240
+
241
+ Agent Skills folders (`<name>/SKILL.md`: YAML frontmatter with `name`, `description`, and ForgeQE's `agents`, then Markdown) that ForgeQE's agent, Scribe, and the browser agent use. `skills:` in `forgeqe.yaml` becomes `SkillsConfig` (`enabled`, `projectDir`, `userDir`, `dirs`, `disable`, `external`, `browserMaxChars`).
242
+
243
+ | Module | Contents |
244
+ |--------|----------|
245
+ | `registry.ts` | `skillRoots` in precedence order, lowest first: prebuilt (`skills/` in the package), external (only with `external`: `~/.claude/skills`, `~/.agents/skills`, and the repo's `.claude`/`.agents` skills), user (`$FORGEQE_HOME/skills`), project (`.forgeqe/skills` next to `forgeqe.yaml`, and the repo's own), then each `dirs` folder. `loadSkills` reads every `<root>/<name>/SKILL.md`, validates it (name lowercase words joined by hyphens, at most 64 characters, the same as the folder; description present, at most 1,024 characters; `agents` from `forgeqe`/`scribe`/`browser`, default `forgeqe` and `scribe`; file at most 100 KB), and reports a broken one in `problems` instead of failing. A later source replaces a skill of the same name and records `overrides`; `disable` then moves skills to `disabled`. `skillsFor(reg, agent)` filters by audience. `linkSkills` makes a temporary folder of links (junctions on Windows) to the chosen skill folders, one per name; `remove` unlinks them and never follows them |
246
+ | `select.ts` | `browserSkills(cfg, goal, names?)`: with names (ForgeQE's agent, `--skill`), `skills_pick: "given"` with only those browser skills (unknown names returned for a warning; `[]` means none); without, `"choose"` with every browser skill's name, description, and body. `rememberChoice` keeps the engine's choice per goal for the life of the process, so monitor rounds and repeated cases send `"given"`; it is kept only when the engine actually chose (`run.json` has `skills`) |
247
+ | `scaffold.ts` | `newSkill` writes a `SKILL.md` template (the body hints at the agent it is for) and refuses an existing one; `setSkillDisabled` edits `skills.disable` through a YAML `Document`, so comments survive, and drops the key when the list is empty; `ignoredNote` warns when git would not commit a new project skill |
248
+ | `view.ts` | `skillsOverview` (per agent: name, source and what it replaces, description; disabled; problems; folders read) and `skillDetail`, shared by `forgeqe skills` and `/skills` |
249
+
250
+ **Agent runtime.** `openSession(…, skills)` keeps the skills whose `agents` include an audience of the session's agents (`AgentSpec.skills`: `forgeqe` for the brain, `scribe` for `forgeqe-test`, `forgeqe-read`, `forgeqe-review`, `forgeqe-find`, and `forgeqe-heal`), links them with `linkSkills`, and passes the folder as `skills.paths`. Each agent's `skill` permission is `{"*": "deny", <name>: "allow", …}` for its audience, or `"deny"` with none, so the runtime's built-in skills and a repo's `.opencode/skills` stay unloadable and out of the listing the model chooses from. `OPENCODE_DISABLE_EXTERNAL_SKILLS=1` and `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS=1` turn off the runtime's own discovery of `~/.claude/skills`, `~/.agents/skills`, and the project's. The links folder and each skill folder (and their real paths) are added to `external_directory` and made read-only: added to the session's protected paths, or, for a session without them, passed as `readOnly` (edit and bash denied for those paths only). ForgeQE's agent keeps write access to user and project skills, so it can change one when asked; built-in and global skills stay read-only for it too. Completed `skill` tool calls show as "Using skill NAME" and are collected in `skillsUsed`, which `runCodeAgent` and `runHeal` put on their results. `SKILL_RULE` in each agent's prompt says to load a skill whose description fits the job and that the prompt wins where they disagree.
251
+
252
+ **Browser runs.** `environmentNotes` lists the browser skills for ForgeQE's agent, and `browser_run` and `run_test_cases` take `skills` (names). `executeRun` calls `browserSkills` and sends the result in the engine request; the engine chooses (`choose_skills`) or uses them, adds the text within `skills_max_chars`, logs "Skills for this run: …" (and the ones left out for size), and writes `skills` to `run.json` only when it decided, so a replay that finished without the agent leaves the choice open. `run.done` in the session log, `RunResult.skills`, and the report's case header carry them; a case's Scribe skills go to `CaseResult.code.skills`.
253
+
254
+ ## Healing (`src/heal`)
255
+
256
+ `forgeqe heal` repairs a repo's existing tests after the app changed, and reports app bugs instead of hiding them. Like the coding loop, ForgeQE decides what passed by running the commands itself.
257
+
258
+ ```
259
+ test command ──► baseline run (testrun.ts) ── passes ──► clean, no model
260
+ │ fails
261
+ ▼
262
+ Scribe session (forgeqe-heal agent) ◄─── MCP: browser_run, run_details, run_test, page_memory
263
+ │ tests[]: healed | app_bug | not_healed, each with its own command
264
+ ▼
265
+ ForgeQE runs each healed test ── fails ──► still-failing prompt ─┐
266
+ │ all pass │
267
+ ▼ │
268
+ ForgeQE runs the whole command ── fails ──► remaining prompt ─────┤
269
+ │ passes │
270
+ ▼ ▼
271
+ passed next attempt (up to code.max_attempts)
272
+ ```
273
+
274
+ | Module | Contents |
275
+ |--------|----------|
276
+ | `prompt.ts` | `HEAL_BRIEF`, the heal agent's procedure: failing tests only; re-run each one's flow with `browser_run` as a plain-language goal; a passing run means the test is out of date, so read `run_details` and update the locators where they live (page objects); a run that fails at the same step means an app bug, and the test is left alone; never delete, skip, loosen, or slow down a test; verify with `run_test`. `HEAL_SCHEMA`: `{tests: [{name, file, status, command, reason, run_dir}], changed, notes, suite_command}`. The find-command, first, still-failing, remaining, and missing-tests prompts |
277
+ | `loop.ts` | `runHeal`. The command is the argument, `code.test_command`, or `suite_command` from a first prompt that asks the agent to find it. The heal session gets its own `localToolServer` with only the four tools above; browser runs are written inside the heal folder (`export.out_dir` is pointed there) and their folders are recorded from `run-done` events. Tests are merged by name across replies. A healed test counts only after ForgeQE runs its command; a healed test with no command is sent back. The whole command runs after every attempt whose tests all pass, and once more at the end if tests were confirmed since. The loop ends when the whole command passes (`passed`), when a reply heals nothing new (`failed` if the command still fails), after `max_attempts`, on the same fatal errors as the coding loop, or on stop. `heal.json` and `heal.md` are written in every case |
278
+ | `report.ts` | `healMarkdown` (verdict table, files changed, browser runs, test output before and after) and `formatHeal`, the terminal summary (✓ confirmed, ? healed but not confirmed, ! app bug, ✗ not healed) |
279
+
280
+ In the shell and `forgeqe run`, ForgeQE's agent calls the `heal_tests` tool, which runs `runHeal`, so "the login tests broke, fix them" goes through the same loop and the same checks as `forgeqe heal`. `tools.ts` and `heal/loop.ts` import each other; that is safe because `runHeal` only reads `TOOLS` when it runs.
281
+
282
+ ## Finding and running existing tests (`src/code/find.ts`, `src/mcp/repotests.ts`, `src/cases/sprints.ts`)
283
+
284
+ "Run all tests related to login" and "run the tests added this sprint" select tests that already exist in the repo. Scribe decides which tests match by reading them; ForgeQE confirms the selection with the framework's own list command, and nothing runs until the user agrees.
285
+
286
+ ```
287
+ request (+ since/until, or a sprint picked from Azure DevOps / Jira via ctx.ask)
288
+ │
289
+ ▼
290
+ Scribe session (forgeqe-find agent, read-only) ── tests[], related[], list_command, run_command
291
+ │
292
+ ▼
293
+ ForgeQE runs list_command ──► listing back to Scribe ── changed ──► lists again (up to 3 rounds)
294
+ │ list_matches, unchanged
295
+ ▼
296
+ selection.json + card ──► run_tests: ctx.ask "Run N tests?" ── Run ──► ForgeQE runs run_command
297
+ │
298
+ JUnit XML / TRX ─┴─► results.json + per-test card
299
+ ```
300
+
301
+ | Module | Contents |
302
+ |--------|----------|
303
+ | `code/find.ts` | `FIND_AGENT` (`forgeqe-find`): `edit`, `webfetch`, and `task` denied, `bash` denied except `git log/show/diff/blame/ls-files/rev-parse/status` and `ls`. Its prompt: work out the frameworks, decide each test from its body and the helpers it calls (a name alone neither includes nor excludes it), a time range means tests added in it (git history, including new functions in existing files; only-modified tests go under `related`), one test per entry, and the two commands. `FIND_SCHEMA`: `{framework, tests: [{name, file, reason, added}], related, list_command, run_command, results_file, list_matches, notes}`. `findTests` opens the session (readDirs: the repo only), gives it today's date, the shell, the range, and the JUnit and TRX paths inside a new `<export.out_dir>/tests/<time>/` folder; runs `list_command` with `runTestCommand` (180s) and feeds the output back until Scribe confirms an unchanged selection (`listed`), and writes `selection.json`. `runSelection` deletes stale result files, runs `run_command` with `code.test_timeout_seconds`, collects the fresh `.xml`/`.trx` files (the given file, or a folder one level deep), and parses them with `parseResults` (JUnit `testcase` at any depth with `failure`/`error`/`skipped`; TRX `UnitTestResult` outcomes and `hh:mm:ss` durations); without results the exit code decides. `selectionCard` and `resultsCard` |
304
+ | `mcp/repotests.ts` | `REPO_TEST_TOOLS`, appended to `TOOLS`. `list_sprints`. `find_tests` validates dates, resolves `sprint` (a name through `findSprint`; `ask` offers up to eight `recentSprints` through `ctx.ask`, or returns them as `needs_choice` when there is no `ask`), runs `findTests`, and emits the selection card. `run_tests` loads the selection and asks **Run N tests? / Don't run** through `ctx.ask`; without `ask` (`forgeqe mcp`, or `forgeqe run` without a terminal) it returns `needs_confirmation` until called with `confirmed: true`. Failures come back with a `heal_tests` suggestion |
305
+ | `cases/sprints.ts` | Azure DevOps `work/teamsettings/iterations` (the project's default team, or `team`) and Jira agile `board?type=scrum` (by project or board, up to five boards) then `board/{id}/sprint`, paged. Both become `Sprint {system, name, start, end, state: current \| past \| future, where}`; a system that fails is reported in `errors`. `recentSprints` keeps each team's or board's current, three latest past, and next sprint |
306
+
307
+ `ctx.ask` is how a tool puts a choice to the user. The TUI opens an **ask** dialog (options stacked vertically when there are more than three or they do not fit; ids prefixed `ask:` so they never collide with the stop and close choices; Esc, another dialog, or Ctrl+C resolve `null`) and pushes the question's `lines` into the transcript. `forgeqe run` prints numbered options when stdin is a terminal. The brain prompt routes repo tests to `find_tests` and test cases in test management systems to `run_test_cases`, and forbids running a selection any other way or without that confirmation.
308
+
309
+ ## Test cases and suites (`src/cases`, `src/suite`)
310
+
311
+ Every source becomes the same `TestCase`: id, title, source, link, description, preconditions, steps (`action`, `data`, `expected`), start URL, tags, and a `ref` with what write-back needs (work item, test point, plan, issue key).
312
+
313
+ | Module | Reads | Writes back |
314
+ |--------|-------|-------------|
315
+ | `files.ts` | text (`---` blocks), CSV (RFC 4180; step rows continue a case), YAML/JSON, Gherkin (Background, Outline × Examples, Then → expected) | nothing |
316
+ | `ado.ts` | `workitemsbatch` for ids; `testplan` suites and test points with `x-ms-continuationtoken` paging; the Steps XML (fast-xml-parser, order kept), shared steps through `compref`, parameter rows from `LocalDataSource` | a test run per plan: create with the point ids, set each result's outcome and comment, complete the run. Cases read by id: a work-item comment |
317
+ | `jira.ts` | Cloud: `/rest/api/3/search/jql` (`nextPageToken`), ADF descriptions. Server/DC: `/rest/api/2/search` (`startAt`), bearer PAT | an issue comment (ADF on Cloud) |
318
+ | `xray.ts` | Cloud: token from `/api/v2/authenticate`, then GraphQL `getTests` / `getTestPlans`. Server/DC: `/rest/raven/1.0/api/test` and `testPlanTests` JQL | an import per plan: a Test Execution with statuses, step statuses and actual results, comments, and `failure.png` as evidence |
319
+ | `testrail.ts` | `get_case`, `get_tests/<run>`, `get_plan`, `get_cases/<project>&suite_id=` (paged by `_links.next`); separated steps or the text template | `add_results_for_cases` on the source run, or `add_run` per suite; `custom_step_results`; `add_attachment_to_result` for `failure.png` |
320
+ | `zephyr.ts` | Zephyr Scale Cloud v2: `testcases/<key>`, `teststeps` (call-to-test inlined to depth 2), `testexecutions?testCycle=`, `testcases?folderId=` | `testexecutions` per case with `testScriptResults`, in the source cycle or a new `testcycles` per project |
321
+ | `qtest.ts` | `test-cases/<id>?expandSteps=true`, `test-runs?parentId=&parentType=test-cycle\|test-suite` | `test-runs/<id>/auto-test-logs` with `test_step_logs`; cases read by id get a new test cycle and test runs |
322
+ | `confluence.ts` | Cloud: `api/v2/pages/<id>?body-format=storage`, CQL through `rest/api/search`. Server/DC: `rest/api/content`. Storage format to text with table cells kept apart | nothing; for the `read_confluence` tool |
323
+ | `bugs.ts` | nothing | with `sources.bugs`: per failed case, find an open bug by the case's fingerprint label/tag (JQL `labels = … AND statusCategory != Done`, or WIQL `Tags CONTAINS … AND State NOT IN (…)`); comment "Still failing" on it, or create one (ADF or ReproSteps HTML, labels/tags, area path, screenshot attachment, "Relates" link or `TestedBy-Forward` relation to the test case). A passed case notes on its open bug that it passes again |
324
+
325
+ Every write-back includes the step verdicts, and retries once without the step details when a system rejects them. Each `WriteBack` carries the `caseId` it is about, so the report shows it with that case.
326
+
327
+ `http.ts` is the one HTTP helper. Tokens come from the environment variables `sources:` names, and errors show the method, host, path, and status, never a credential. For `forgeqe suite` and `forgeqe cases`, `refKind` picks a source from the reference's prefix, then an existing file, then plain text; the agent's tools skip this and pass structured selectors to `casesFrom`. `loadCases` adds `~2`, `~3` to repeated ids.
328
+
329
+ `goalFor` turns a case into the agent's goal: numbered steps, each with its data and "Expected: …", and an instruction to check every expected result and fail the task if any is not met. `caseMarkdown` goes into the coding handoff (from `testcase.json` in the run folder) and asks Scribe to name the test `<id> <title>`.
330
+
331
+ `runSuite` gives each case a run folder `NN-<id>` and runs `executeRun` with a pool of `parallel` workers. With a repo, a passed case is queued for the coding agent as soon as its browser run ends and its worker awaits that job before taking the next case, so each case is explored, handed off, and coded before the next one starts; the queue is one promise, so coding jobs never overlap. A case that is not coded records `codeSkipped` with the reason. Write-back and `fileBugs` run after every case finishes and skip stopped cases. `report.ts` writes `junit.xml` (failed → `<failure>`, error → `<error>`, stopped → `<skipped>`, step verdicts and posted links in `system-out`), `report.html` (`src/report/data.ts` turns the results into `ReportData`: step verdicts, the `steps.json` timeline with screenshots as data URIs up to a 40 MB budget, failed cases first, small text files such as the trace and workflow, posted records, each run folder's `run.json` (page memory with heals and repairs, browser diagnostics) and `accessibility.json`, the case's group and requirements, usage and cost, and history from the sibling `.forgeqe/reports/suites/*/suite.json`. From that history it also finds each failed case's last passing run folder (next to its `suite.json`, else where it was written) for the "last good vs now" screenshot of the same URL path, and each memory-replayed case's last run with no replay, as the baseline for page memory savings; `src/report/html.ts` injects it as `window.__FORGEQE_REPORT__` into `src/report/template.html`, a React app from `report-ui/` built into one file by `bun run build:report`; every single run writes one too), `suite.json`, the terminal summary, and the GitHub job summary (appended to `GITHUB_STEP_SUMMARY` when set). The `run_test_cases` tool calls the same `runSuite`, passing the working directory and `runCodeAgent` as the coder when the session writes tests (so the agent never calls `write_test` for those cases afterwards), and the shell renders a suite card. There is no pipeline template: the agent writes pipelines to fit each repository, and they call `forgeqe suite`.
332
+
333
+ ## Monitoring (`src/monitor`)
334
+
335
+ `forgeqe monitor` is `runSuite` on a timer, plus memory between rounds of which checks are failing, plus alerts.
336
+
337
+ ```
338
+ round: loadCases(monitor.checks) ──► runSuite (out_dir: .forgeqe/reports/monitor, code off, replay verify, write-back off)
339
+ │ │ case results (+ forgeqe-monitor: passed)
340
+ │ throws (source down, no cases) ▼
341
+ └──► forgeqe-monitor: error ──► decide(state.json, outcomes, now, fail_after, remind_every)
342
+ │ alerts: failing | still_failing | recovered
343
+ ▼
344
+ sendAlerts ──► Teams webhook (Adaptive Card) and SMTP (nodemailer)
345
+ │
346
+ state.json, <round>/alerts.json, prune rounds beyond keep
347
+ ```
348
+
349
+ | Module | Contents |
350
+ |--------|----------|
351
+ | `state.ts` | `decide` is pure: a failure starts or extends a failing streak (`since`, `failures`); the first alert goes out when the streak reaches `fail_after`, a `still_failing` reminder every `remind_every` after the last alert, and `recovered` on a pass after an alert (a streak that never alerted recovers silently). Stopped checks keep their state. A round that could not run reports only the `forgeqe-monitor` pseudo-check and keeps the others (`keepOthers`). `state.json` is written through a temp file and a rename |
352
+ | `alerts.ts` | One `AlertMessage` per round. `teamsPayload`: a `message` with one Adaptive Card 1.4 attachment (full width, a line per check with its failing step, a test case link, an Open report action), which classic incoming webhooks and Workflows webhooks both accept. `emailContent`: subject `[ForgeQE] <title>`, text and HTML, and the failing checks' `failure.png` (at most 5). `smtpTransport`: nodemailer with `secure` for port 465, STARTTLS when the server offers it, auth only when the username variable is set, and 20–30 s timeouts. `sendAlerts` sends to each channel independently and removes the webhook URL and the SMTP password from errors |
353
+ | `run.ts` | `monitorRound` (load, run, decide, save, alert, prune), `runMonitor` (a lock file with the pid, `.monitor.lock`, refuses a second live monitor on the folder; rounds start `every` apart, or at once after an overrun; `--once` returns 0/1/2), `pruneRounds`, `formatRound`, `roundCard`, and `sendTestAlert` |
354
+ | `background.ts` | Monitors started by the `monitor` tool: `runMonitor` with its own `AbortController` inside the process, one per `out_dir` (a second in the same process is refused, and `lockMonitor` is checked first so another process's monitor is too). Each round is emitted as a card, and a monitor that ends on an error emits a "Monitor stopped" card. `stopBackgroundMonitors` aborts and waits for them |
355
+
356
+ Rounds are ordinary suite folders, so `report.html` gets trends and flaky checks from the sibling rounds' `suite.json`. `runDirectory` creates each run or suite folder itself and adds `-2`, `-3` when a folder for that second exists, so back-to-back rounds and parallel tool calls never share one. From the shell the brain can run a round, start a background monitor that lives as long as the shell, set up `monitor:` in `forgeqe.yaml`, and test the alerts. Monitoring that outlives the shell is a service, cron, or a scheduled pipeline running `forgeqe monitor --once` with `state.json` cached.
357
+
358
+ **Signing in.** `signIn` in `src/logins.ts` drives the engine's `login` command: when the engine says the browser is open it calls `wait`, and sends `save` (true) or `stop` (false, or on abort). `forgeqe login` wraps it with readline and Ctrl-C; the `logins` tool passes `ctx.waitForUser`.
359
+
360
+ ## Telemetry (`src/privacy.ts`)
361
+
362
+ `withoutTelemetry(env)` is part of every child's environment: the engine and `forgeqe setup` (through `allowListEnv`), the test command and replay (through `withoutNames`, which also drops ForgeQE's own secrets), and the agent runtime server (through `serverEnv`). It drops `OTEL_EXPORTER_OTLP_*` and forces the opt-outs on:
363
+
364
+ | Process | Variables | Effect |
365
+ |---------|-----------|--------|
366
+ | Engine | `ANONYMIZED_TELEMETRY=false`, and the library's cloud-sync and cost-calculation switches off | No PostHog client, no cloud sync, no LiteLLM price download |
367
+ | Agent runtime | Its auto-update, share, and model-catalogue switches off, and `experimental.openTelemetry: false` in the config | No update check, no share links, bundled model catalog, no AI SDK spans |
368
+ | test command | `DO_NOT_TRACK=1`, `OTEL_SDK_DISABLED=true`, and the Next.js, Nuxt, Astro, Gatsby, Storybook, Turborepo, Angular, .NET, and Cypress opt-outs | Framework CLIs a test may start stay quiet |
369
+
370
+ `cli.py` maps each `FORGEQE_ENGINE_<NAME>` to the setting the engine reads and sets the telemetry variables again with `os.environ.update`, so an engine started by hand behaves the same. `brand.install()` replaces the about:blank watchdog's animation, which loads a logo from the library's CDN, with ForgeQE's start page (which loads nothing). Chrome runs with the engine library's defaults (`--disable-background-networking`, `--metrics-recording-only`, `--no-pings`, `--disable-sync`, …).
371
+
372
+ ## Profiles and the offline test double
373
+
374
+ `loadConfig` reads the profiles in `llms.json` and picks `--profile`, then `WP_LLM_PROFILE`, then `llm.active`, then `default`, then the first profile. With none, the active profile is `""` and its provider `none`: `missingKey` and `noAgentModel` return `NO_MODEL_SET_UP`, which the shell, `forgeqe run`, and the tools show instead of running. Entries with provider `mock`, `default: mock`, and `llm.active: mock` that older templates wrote are ignored.
375
+
376
+ `mock` is a test double, not a profile users see: it exists only when asked for by name (`--profile mock` or `WP_LLM_PROFILE=mock`). It never starts the Python engine: `src/engine/mock.ts` speaks the engine's stdin/stdout protocol (it opens the pinned URL and finishes, or fails when there is none) and `runEngine` runs it in its place, so offline runs and the test suite go through the same bridge, events, and reports as live runs without Python, a browser, or an API key. Its result has `driver: "synthetic"`, so it never becomes a test.
377
+
378
+ ## Artifacts
379
+
380
+ | File | Contents |
381
+ |------|----------|
382
+ | `run.json` | Result, summary, usage, visited URLs, page memory (replayed, heals, repaired), browser diagnostics, the judge's verdict (`judge`), how the browser was set up (`browser`), and the browser skills added to the prompt (`skills`) |
383
+ | `accessibility.json` | Accessibility checks for each page visited |
384
+ | `workflow.yaml` | `forgeqe-v2-compact` steps for replay or audit |
385
+ | `trace.md` | Task, results, failure trace |
386
+ | `failure.png` | Last screenshot of a failed run |
387
+ | `steps.json`, `steps/NN.jpg` | The step timeline and a screenshot per step |
388
+ | `report.html` | The run's own report |
389
+ | `handoff.md` | What the coding agent was given (only with a repo) |
390
+ | `code.json` | Coding result: status, attempts, test file, command, reused, changed, last test output, tokens |
391
+ | `scribe-session.json` | Scribe's session for a job that stopped before it finished, reopened by a rerun |
392
+ | `testcase.json` | The test case a suite ran in this folder |
393
+ | `junit.xml`, `report.html`, `suite.json` | Suite reports, in `.forgeqe/reports/suites/<time>/` above the case folders (monitoring rounds: `.forgeqe/reports/monitor/<time>/`) |
394
+ | `heal.json`, `heal.md` | Healing result and report, in `.forgeqe/reports/heal/<time>/` with the browser runs it made |
395
+ | `state.json`, `alerts.json` | Monitoring: which checks are failing and since when (`.forgeqe/reports/monitor/`), and what a round alerted and whether it was sent (in the round folder) |
396
+
397
+ ## Tests
398
+
399
+ - `bun test`: config, commands, TUI, mock loop, bridge (with stub engines), memory readers and `forgeqe memory`, and `tests/engine.e2e.test.ts`, which runs the real engine and Chrome against a local site with a local OpenAI-compatible model server. Its second run replays the first from memory and checks the model is called once. The e2e test is skipped when the engine is not installed. Its last test passes that real run to the real agent runtime.
400
+ - `tests/code.test.ts`: provider mapping, permissions, handoff contents, and the loop with a scripted session (fail then pass, missing command, a stalled command asked once to change approach and then stopping the job, the live browser's MCP server and config from `run.json`, the replay running while Scribe reads the repo as the read-only agent, the reading turn and its notes with the replay off, a check's section in the handoff, the live browser switched on by a failed replay or a failed test, the same failure stopping the job on its third time, fatal error, stop), `FailedCommands`, the read-only agent's permissions, and the trimmed handoff (weak locators dropped, step elements left out of the page tables). `tests/code.e2e.test.ts` also checks, against the real agent runtime, that the reading turn has no write or structured-answer tools. `tests/replay.test.ts`: the script and locators built from a workflow, the two launch setups, and against a local site in Chrome, a click a cookie dialog covers (Playwright's reason, the dialog, the other locators' matches, the rest of the flow still run) and a control hidden from `navigator.webdriver` (the second launch gets through and the handoff says the setup is the difference), and a run that only opened a page whose heading checks come back with their sections (by the element after the heading, the section it heads, and `aria-labelledby`). `tests/code.e2e.test.ts` runs the real agent runtime against a local OpenAI-compatible model that writes a failing test, gets ForgeQE's failure output, and fixes it. It checks the key arrives through `{env:}`. Both tests are skipped when the agent runtime is not installed.
401
+ - `tests/cases.test.ts`: every file format, goals, the handoff, and Azure DevOps, Jira (Cloud and Server), and Xray (Cloud and Server) against local fake APIs that check paging, auth headers, and the write-back calls. `tests/suite.test.ts`: the worker pool, serial coding, reports, exit codes, stop, write-back, and the job summary. The engine e2e test also runs a two-case suite in two real browsers and checks page memory counted both.
402
+ - `tests/sources.test.ts`: TestRail, Zephyr Scale, and qTest reading and write-back (paging, step results, retries without them, new runs and cycles), bugs in Jira and Azure DevOps (create with screenshot and links, comment while failing, note on pass), Confluence reading and its MCP wiring, parsing of `logins:`, `report:` and the new sources, the login commands, session notes without secret values, and `report.html`. The engine e2e test also signs in with a saved login and TOTP (the model never sees the values), checks the session file, that `workflow.yaml` holds `text_env` / `totp_env` instead of values, and that the next run starts signed in.
403
+ - `tests/integrations.test.ts`: `sources:` MCP switches and `mcp:` parsing, the Azure DevOps and Jira server entries for each auth mode and every reason a server is skipped (secret values never in the config), overrides, and `query_api` against fake APIs (auth headers, api-version, host restriction, mutation refusal, cutting). `brain.e2e.test.ts` also starts a local stdio server, a remote HTTP server that checks its bearer token, and a broken server through the real agent runtime: the `{env:}` values arrive, both tools are callable, the broken server is reported, and no secret reaches the model.
404
+ - `tests/mcp.test.ts`: the MCP server over HTTP (auth, initialize, list, progress, cancel, errors) and stdio, environment notes without secret values, `pushBranch` against a bare remote whose default branch is `trunk`, and `setCiSecret` with fake `gh` and `az`. `tests/brain.e2e.test.ts` runs the real agent runtime against a scripted local model: the agent chooses `forgeqe_browser_run` over MCP, both forms of `git push` are refused, and a follow-up message runs a suite. `tests/cli.test.ts` drives `forgeqe run` through the same real agent runtime (tool lines, reply, exit 3 for needs input) and `forgeqe mcp` over stdio with the mock profile. The agent runtime tests are skipped when the agent runtime is not installed.
405
+ - `tests/heal.test.ts`: the healing loop with a scripted session that calls ForgeQE's tools over the real MCP server (only the four heal tools are listed; the browser run lands in the heal folder): nothing to heal, healed and confirmed, an app bug left failing, a healed test that still fails or has no command, finding the command, `code.test_command`, a missing report, a fatal error, and stop. `tests/monitor.test.ts`: `decide` sequences (fail_after, flaky, reminders, recovery, stopped, a round that could not run), duration and `monitor:` parsing, rounds through the real `runSuite` with state carried over, report links, attachments, `alerts.json`, pruning, the loop and `--once` exit codes, the lock against a live pid, the Teams card against a fake webhook, email through nodemailer to a local fake SMTP server (login, recipients, subject, screenshot), and secrets scrubbed from errors. `tests/cli.test.ts` runs `forgeqe monitor --test-alerts` and `--once` with the mock profile against a fake Teams webhook.
406
+ - `tests/commands.test.ts`: the command tools called as the agent calls them: `write_test` with a scripted session (agent events, the card, the mock profile refused), `heal_tests` clean and healed, `monitor` round and status, `start` refused in a `once` session, start/status/stop in a `shell` session (a round card, a second start and a one-off round refused, the lock released), `test_alerts` against a fake webhook, `logins` list/clear and `sign_in` without a person, `page_memory` flows and clear, and `forgeqe_setup` profiles, MCP servers, docs by topic, and init. `tests/repotests.test.ts`: `parseResults` on JUnit and TRX; `find_tests` with a scripted Scribe (the read-only agent, the listing fed back, a corrected list command listed again, nothing found, mock profile and bad dates refused); `run_tests` declined (nothing runs), confirmed (per-test card from JUnit, the heal suggestion), and without `ask` (`needs_confirmation`, then `confirmed: true`, exit code only); sprints from fake Azure DevOps and Jira APIs (paging, the pick through `ask`, cancel, `needs_choice`, a name matched). `tests/engine/test_step_notes.py`: the agent's note built from its own fields and stored on the flow steps a step recorded, `evaluate` steps with their code. `tests/code.test.ts` (handoff): notes before each group of steps, once per step, and the JavaScript shown. `tests/update.test.ts`: bun and pip installs found in PATH order; waiting while the registry 404s, then the cache-skipping commands per install; `--check` running nothing; a release never served failing with a retry hint. `tests/sessionLog.test.ts`: the session header and events with remembered passwords, secret environment variables, and inline API keys replaced; tool calls and results; the bundle's contents without screenshots or keys; `/logs`. `tests/engine/test_page_settle.py`: the judge off by default and on by setting, `page_settled`, `settle_page` waiting for a new tab and giving up on one that stays blank, and note actions as read-only. `tests/resume.test.ts`: resume steps in the run result, `loadResume` by folder or run name, and `browser_run` returning the retry and passing `resume_from`. `tests/engine/test_resume.py`: `resume_point`, `resume_steps`, the loop start and nudge text, `_install_nudges` draining after the library's hook, resume steps in the flow file, the resume hand-off, checkpoints (kept apart from flows, cleared only by their own run, kept by a stopped run), and the hand-off after a cut-off run. `tests/code.test.ts` (resuming): a stopped job keeps `scribe-session.json` and a finished one removes it, a rerun reopens the session with the saved replay and a carry-on prompt, and a gone session starts fresh; `tests/code.e2e.test.ts` reopens a session by id after the runtime restarted and finds its earlier messages. `tests/engine.e2e.test.ts` runs a page where clicking "One way" resets the date: a model that takes the nudge finishes; one that ignores it is stopped with its resume steps saved, and a retry with `resume_from` replays the cookie click and needs two model calls. `tests/sessionLogins.test.ts`: remembered logins (domains from a URL, hosts, and wildcards; the engine payload with values; no process env), redaction next to punctuation and in carried-over turns, a remembered login replacing a configured one in the notes, ForgeQE's test runs getting the env names with the output redacted, and the `logins` tool's remember/list/clear without the password in any result. `tests/engine/test_session_logins.py`: remembered values as placeholders on every sign-in domain, env values still working, `step_outcomes`, and `making_progress`. `tests/tui.test.tsx` renders a monitor round's card in the transcript and checks the **Your turn** dialog holds a tool until **Done, continue**.
407
+ - `tests/skills.test.ts`: `skills:` parsing; the prebuilt skills loading cleanly; precedence and `overrides` across user, project, the repo's own folder, and `dirs`; the repo's `.claude` skills only with `external`; the default and opt-in `agents`; `disable` and unknown names; every validation problem (including the unquoted `: ` hint); the per-agent `skill` permission and `skills.paths` in the runtime config; links readable through and removed without touching the skill; `browserSkills` given and choose, unknown names, and the remembered choice; the engine request through a stub engine and `skills` back on the result; the skills catalogue in the environment notes; `newSkill`, `setSkillDisabled` keeping comments, and `ignoredNote`; the overview and `/skills`; and `forgeqe skills new/show/disable/enable/check` end to end. `tests/code.e2e.test.ts` checks, against the real agent runtime, that Scribe's skill listing has only the registry's skills for it, an allowed skill's text arrives, and a disabled skill, the repo's `.opencode` and `.claude` skills, the runtime's built-in skill, and `~/.claude/skills` stay out unless `external` adds them through the registry. `tests/engine/test_skills.py`: `choose_skills` order, the cap of 3, errors and an empty catalogue without a model call; `skills_block` within `skills_max_chars`; the request fields.
408
+ - `<engine-python> -m pytest tests/engine`: the engine directly, with a scripted model, including `test_memory.py` (locators, confidence, identity, flows, secrets) and replay, trust mode, and self-healing against real Chrome. `test_brand.py` checks the overlay in real Chrome: the start page, trusted CDP clicks and keys dropped while locked and let through while acting, the unlocked state and step text on a page loaded mid-step, and a clean screenshot and DOM for the model. `WP_E2E_HEADED=1` runs the bun e2e test in a visible browser, with the overlay on.