@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,153 @@
1
+ import type { SourcesConfig } from "../config.ts"
2
+ import { envSecret, htmlToText, http } from "./http.ts"
3
+ import { resultText } from "./jira.ts"
4
+ import type { CaseResult, CaseStep, TestCase, WriteBack } from "./types.ts"
5
+
6
+ type ZCase = { key: string; name: string; objective?: string | null; precondition?: string | null; labels?: string[]; folder?: { id?: number } | null }
7
+ type ZStep = { inline?: { description?: string | null; testData?: string | null; expectedResult?: string | null } | null; testCase?: { testCaseKey?: string } | null }
8
+ type Paged<T> = { values?: T[]; isLast?: boolean; total?: number }
9
+
10
+ export function zephyrApi(cfg: SourcesConfig): { api: string; auth: string; projectKey?: string } {
11
+ const z = cfg.zephyr
12
+ if (!z) throw new Error("Zephyr Scale is not set up. Add sources.zephyr to forgeqe.yaml, or set ZEPHYR_API_TOKEN.")
13
+ return { api: z.url, auth: `Bearer ${envSecret(z.tokenEnv, "Zephyr Scale")}`, projectKey: z.projectKey }
14
+ }
15
+
16
+ /** "SHOP-T12" → "SHOP": Zephyr Scale keys are the Jira project key, "-T", and a number. */
17
+ export const zephyrProject = (key: string) => key.slice(0, key.lastIndexOf("-T"))
18
+
19
+ async function paged<T>(url: string, auth: string): Promise<T[]> {
20
+ const out: T[] = []
21
+ for (let start = 0; ; start += 100) {
22
+ const { data } = await http<Paged<T>>(`${url}${url.includes("?") ? "&" : "?"}maxResults=100&startAt=${start}`, { auth })
23
+ const page = data?.values || []
24
+ out.push(...page)
25
+ if (!page.length || data?.isLast !== false) break
26
+ }
27
+ return out
28
+ }
29
+
30
+ async function steps(key: string, api: string, auth: string, depth = 0): Promise<CaseStep[]> {
31
+ const out: CaseStep[] = []
32
+ for (const s of await paged<ZStep>(`${api}/testcases/${encodeURIComponent(key)}/teststeps`, auth)) {
33
+ if (s.testCase?.testCaseKey && depth < 2) out.push(...(await steps(s.testCase.testCaseKey, api, auth, depth + 1)))
34
+ const action = htmlToText(s.inline?.description || "")
35
+ if (action) out.push({ action, data: htmlToText(s.inline?.testData || "") || undefined, expected: htmlToText(s.inline?.expectedResult || "") || undefined })
36
+ }
37
+ return out
38
+ }
39
+
40
+ const folderNames = new Map<string, Promise<string | undefined>>()
41
+
42
+ /** A folder's name, for grouping cases in reports; undefined when it cannot be read. */
43
+ function folderName(id: number | undefined, api: string, auth: string): Promise<string | undefined> {
44
+ if (id === undefined) return Promise.resolve(undefined)
45
+ const key = `${api}|${id}`
46
+ if (!folderNames.has(key)) {
47
+ folderNames.set(key, http<{ name?: string }>(`${api}/folders/${id}`, { auth }).then((r) => r.data?.name || undefined, () => undefined))
48
+ }
49
+ return folderNames.get(key)!
50
+ }
51
+
52
+ async function toCase(c: ZCase, api: string, auth: string, cycle?: string): Promise<TestCase> {
53
+ return {
54
+ id: c.key,
55
+ title: c.name,
56
+ source: "zephyr",
57
+ description: htmlToText(c.objective || "") || undefined,
58
+ preconditions: htmlToText(c.precondition || "") ? [htmlToText(c.precondition || "")] : undefined,
59
+ steps: await steps(c.key, api, auth),
60
+ tags: c.labels?.length ? c.labels : undefined,
61
+ group: await folderName(c.folder?.id ?? undefined, api, auth),
62
+ ref: { testCase: c.key, ...(cycle ? { cycle } : {}) },
63
+ }
64
+ }
65
+
66
+ /** The test case key in a reference like https://api.zephyrscale.smartbear.com/v2/testcases/SHOP-T1/versions/1. */
67
+ function keyFromSelf(self: string | undefined): string | undefined {
68
+ const parts = (self || "").split("/")
69
+ const i = parts.indexOf("testcases")
70
+ return i >= 0 ? decodeURIComponent(parts[i + 1] || "") || undefined : undefined
71
+ }
72
+
73
+ /** `zephyr:SHOP-T1,SHOP-T2`, `zephyr:cycle=SHOP-R5` (results go to that cycle), or `zephyr:folder=123` (needs project_key). */
74
+ export async function zephyrCases(ref: string, cfg: SourcesConfig): Promise<TestCase[]> {
75
+ const z = zephyrApi(cfg)
76
+ const cycle = ref.match(/^cycle=(.+)$/i)?.[1]?.trim().toUpperCase()
77
+ const folder = ref.match(/^folder=(\d+)$/i)?.[1]
78
+ const get = async (key: string) => (await http<ZCase>(`${z.api}/testcases/${encodeURIComponent(key)}`, { auth: z.auth })).data
79
+ const out: TestCase[] = []
80
+ if (cycle) {
81
+ const project = cycle.includes("-R") ? cycle.slice(0, cycle.lastIndexOf("-R")) : z.projectKey
82
+ const executions = await paged<{ testCase?: { self?: string } }>(
83
+ `${z.api}/testexecutions?testCycle=${encodeURIComponent(cycle)}${project ? `&projectKey=${encodeURIComponent(project)}` : ""}`,
84
+ z.auth,
85
+ )
86
+ const keys = [...new Set(executions.map((e) => keyFromSelf(e.testCase?.self)).filter((k): k is string => Boolean(k)))]
87
+ for (const key of keys) out.push(await toCase(await get(key), z.api, z.auth, cycle))
88
+ if (!out.length) throw new Error(`No test cases in Zephyr Scale cycle ${cycle}.`)
89
+ return out
90
+ }
91
+ if (folder) {
92
+ if (!z.projectKey) throw new Error("Zephyr Scale folders need sources.zephyr.project_key in forgeqe.yaml.")
93
+ for (const c of await paged<ZCase>(`${z.api}/testcases?projectKey=${encodeURIComponent(z.projectKey)}&folderId=${folder}`, z.auth)) out.push(await toCase(c, z.api, z.auth))
94
+ if (!out.length) throw new Error(`No test cases in Zephyr Scale folder ${folder}.`)
95
+ return out
96
+ }
97
+ const keys = ref.split(/[,\s]+/).map((k) => k.trim().toUpperCase()).filter(Boolean)
98
+ if (!keys.length || keys.some((k) => !k.includes("-T"))) throw new Error(`Zephyr Scale reference "${ref}": use zephyr:SHOP-T1,SHOP-T2, zephyr:cycle=SHOP-R5, or zephyr:folder=123.`)
99
+ for (const key of keys) out.push(await toCase(await get(key), z.api, z.auth))
100
+ return out
101
+ }
102
+
103
+ const STATUS = { passed: "Pass", failed: "Fail", error: "Blocked" } as const
104
+ const STEP_STATUS = { passed: "Pass", failed: "Fail", not_run: "Not Executed" } as const
105
+
106
+ /** One test execution per case, in the cycle the cases came from or in a new cycle per project. */
107
+ export async function zephyrWriteBack(results: CaseResult[], cfg: SourcesConfig, runName: string): Promise<WriteBack[]> {
108
+ const z = zephyrApi(cfg)
109
+ const out: WriteBack[] = []
110
+ const newCycles = new Map<string, string>()
111
+ const recorded = new Map<string, number>()
112
+ for (const r of results) {
113
+ const key = String(r.case.ref?.testCase || r.case.id)
114
+ const projectKey = zephyrProject(key)
115
+ try {
116
+ let cycle = r.case.ref?.cycle ? String(r.case.ref.cycle) : newCycles.get(projectKey)
117
+ if (!cycle) {
118
+ const { data } = await http<{ key: string }>(`${z.api}/testcycles`, { auth: z.auth, body: { projectKey, name: runName } })
119
+ cycle = data.key
120
+ newCycles.set(projectKey, cycle)
121
+ }
122
+ const verdicts = new Map((r.stepResults || []).map((v) => [v.step, v]))
123
+ const body: Record<string, unknown> = {
124
+ projectKey,
125
+ testCaseKey: key,
126
+ testCycleKey: cycle,
127
+ statusName: STATUS[r.status as keyof typeof STATUS] ?? "Blocked",
128
+ executionTime: r.ms,
129
+ actualEndDate: r.finishedAt,
130
+ comment: resultText(r).replace(/\n/g, "<br>"),
131
+ }
132
+ const scripted = verdicts.size
133
+ ? {
134
+ ...body,
135
+ testScriptResults: r.case.steps.map((_, i) => {
136
+ const v = verdicts.get(i + 1)
137
+ return { statusName: v ? STEP_STATUS[v.status] : "Not Executed", actualEndDate: r.finishedAt, actualResult: v?.actual || "" }
138
+ }),
139
+ }
140
+ : body
141
+ await http(`${z.api}/testexecutions`, { auth: z.auth, body: scripted }).catch((err) =>
142
+ scripted === body ? Promise.reject(err) : http(`${z.api}/testexecutions`, { auth: z.auth, body }),
143
+ )
144
+ recorded.set(cycle, (recorded.get(cycle) || 0) + 1)
145
+ } catch (err) {
146
+ out.push({ source: "zephyr", caseId: r.case.id, text: `${key}: result not recorded`, error: (err as Error).message })
147
+ }
148
+ }
149
+ for (const [cycle, n] of recorded) {
150
+ out.push({ source: "zephyr", text: `Zephyr Scale cycle ${cycle}${[...newCycles.values()].includes(cycle) ? " (new)" : ""}: ${n} execution${n === 1 ? "" : "s"}` })
151
+ }
152
+ return out
153
+ }
@@ -0,0 +1,86 @@
1
+ import { savedSecrets } from "./auth/store.ts"
2
+ import type { AppConfig } from "./config.ts"
3
+ import { NO_TELEMETRY_ENV, withoutTelemetry } from "./privacy.ts"
4
+
5
+ /**
6
+ * What each process ForgeQE starts may see of its environment. The engine gets an allow-list (system settings, its own
7
+ * tuning, the active model key, and saved-login variables); the user's test commands and the agent runtime get everything
8
+ * except the secrets ForgeQE itself holds that their job does not need.
9
+ */
10
+
11
+ type Env = Record<string, string | undefined>
12
+
13
+ /** System settings a browser, Python, or a package manager needs: locale, display, temp folders, proxies, certificates. */
14
+ const SYSTEM_NAMES = new Set([
15
+ "PATH", "HOME", "USER", "LOGNAME", "SHELL", "TMPDIR", "TMP", "TEMP", "LANG", "LANGUAGE", "TZ", "TERM", "COLORTERM",
16
+ "DISPLAY", "WAYLAND_DISPLAY", "XAUTHORITY", "DBUS_SESSION_BUS_ADDRESS",
17
+ "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY",
18
+ "SSL_CERT_FILE", "SSL_CERT_DIR", "REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE", "NODE_EXTRA_CA_CERTS",
19
+ "VIRTUAL_ENV", "FORGEQE_HOME", "FORGEQE_PYTHON",
20
+ // Windows
21
+ "SYSTEMROOT", "SYSTEMDRIVE", "WINDIR", "COMSPEC", "PATHEXT", "APPDATA", "LOCALAPPDATA", "PROGRAMDATA", "PROGRAMFILES",
22
+ "PROGRAMFILES(X86)", "PROGRAMW6432", "COMMONPROGRAMFILES", "USERPROFILE", "USERNAME", "USERDOMAIN", "HOMEDRIVE", "HOMEPATH",
23
+ "NUMBER_OF_PROCESSORS", "PROCESSOR_ARCHITECTURE", "OS",
24
+ ])
25
+ const SYSTEM_PREFIXES = ["LC_", "XDG_", "BROWSER_USE_", "FORGEQE_ENGINE_", "UV_", "PIP_", "PYTHON", "PYENV", "CONDA", "CHROME", "PLAYWRIGHT_"]
26
+
27
+ function isSystem(name: string): boolean {
28
+ const n = name.toUpperCase()
29
+ return SYSTEM_NAMES.has(n) || SYSTEM_PREFIXES.some((p) => n.startsWith(p))
30
+ }
31
+
32
+ /** `env` with telemetry off, keeping only system settings and `names`. */
33
+ export function allowListEnv(env: Env, names: Iterable<string>): Record<string, string> {
34
+ const keep = new Set([...names].filter(Boolean))
35
+ const out: Record<string, string> = {}
36
+ for (const [k, v] of Object.entries(withoutTelemetry(env))) if (isSystem(k) || keep.has(k)) out[k] = v
37
+ return { ...out, ...NO_TELEMETRY_ENV }
38
+ }
39
+
40
+ /** `env` with telemetry off and `names` removed. */
41
+ export function withoutNames(env: Env, names: Iterable<string>): Record<string, string> {
42
+ const drop = new Set(names)
43
+ const out = withoutTelemetry(env)
44
+ for (const k of Object.keys(out)) if (drop.has(k)) delete out[k]
45
+ return out
46
+ }
47
+
48
+ const ENV_REF = /\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g
49
+
50
+ /** The variable names `{env:NAME}` references in `value` (any JSON-like value). */
51
+ export function envRefs(value: unknown): string[] {
52
+ return [...new Set([...JSON.stringify(value ?? null).matchAll(ENV_REF)].map((m) => m[1]!))]
53
+ }
54
+
55
+ /** Login variables (usernameEnv, passwordEnv, totpEnv): test credentials that the tests themselves read. */
56
+ const LOGIN_KEYS = new Set(["usernameEnv", "passwordEnv", "totpEnv"])
57
+
58
+ function collectEnvNames(value: unknown, out: Set<string>, inLogins: boolean): void {
59
+ if (Array.isArray(value)) {
60
+ for (const v of value) collectEnvNames(v, out, inLogins)
61
+ return
62
+ }
63
+ if (!value || typeof value !== "object") return
64
+ for (const [k, v] of Object.entries(value)) {
65
+ if (typeof v === "string" && /Env$/.test(k) && v && !(inLogins && LOGIN_KEYS.has(k))) out.add(v)
66
+ else collectEnvNames(v, out, inLogins || k === "logins")
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Secrets ForgeQE holds for itself: every model profile's key variable, test case system and alert credentials
72
+ * (`*Env` settings), and variables `mcp:` servers reference. Saved-login variables are left out: tests read them.
73
+ */
74
+ export function privateEnvNames(cfg: AppConfig, extra: Iterable<string> = []): string[] {
75
+ const out = new Set<string>(extra)
76
+ collectEnvNames({ ...cfg, logins: undefined }, out, false)
77
+ for (const name of envRefs(cfg.mcp)) out.add(name)
78
+ // Secrets a saved sign-in put into the environment, including those saved by name (forgeqe auth login NAME).
79
+ for (const name of savedSecrets) out.add(name)
80
+ return [...out]
81
+ }
82
+
83
+ /** Variables the saved logins read: the engine needs them to sign in, tests to log in. */
84
+ export function loginEnvNames(logins: { usernameEnv?: string; passwordEnv?: string; totpEnv?: string }[]): string[] {
85
+ return logins.flatMap((l) => [l.usernameEnv, l.passwordEnv, l.totpEnv]).filter((n): n is string => Boolean(n))
86
+ }
@@ -0,0 +1,114 @@
1
+ import path from "node:path"
2
+
3
+ import { replayMode } from "../config.ts"
4
+
5
+ /** Command-line arguments: flags, switches, and the values commands read from them. */
6
+
7
+ export type Flags = Map<string, string | boolean>
8
+
9
+ /** Flags that never take the next word as their value, so `run --direct "<goal>"` keeps the goal. `--headed=false` still works. */
10
+ export const SWITCHES = new Set([
11
+ "all", "check", "debug", "direct", "force", "global", "guide", "headed", "help", "locators", "no-code", "no-memory", "no-write-back",
12
+ "once", "open", "plain", "shots", "steps", "test-alerts", "user", "verbose", "yes",
13
+ ])
14
+
15
+ export function parseArgs(argv: string[]): { positionals: string[]; flags: Flags } {
16
+ const positionals: string[] = []
17
+ const flags: Flags = new Map()
18
+ const alias: Record<string, string> = {
19
+ p: "profile",
20
+ u: "url",
21
+ g: "goal",
22
+ o: "out",
23
+ d: "dir",
24
+ c: "config",
25
+ h: "help",
26
+ }
27
+ for (let i = 0; i < argv.length; i++) {
28
+ const token = argv[i]
29
+ if (token === "--") {
30
+ positionals.push(...argv.slice(i + 1))
31
+ break
32
+ }
33
+ if (token.startsWith("--")) {
34
+ const body = token.slice(2)
35
+ const eq = body.indexOf("=")
36
+ // --skill repeats: each value is added to the list.
37
+ const set = (key: string, value: string | boolean) => {
38
+ const before = flags.get(key)
39
+ flags.set(key, key === "skill" && typeof before === "string" && typeof value === "string" ? `${before},${value}` : value)
40
+ }
41
+ if (eq >= 0) {
42
+ set(body.slice(0, eq), body.slice(eq + 1))
43
+ continue
44
+ }
45
+ const next = argv[i + 1]
46
+ if (next === undefined || next.startsWith("-") || SWITCHES.has(body)) set(body, true)
47
+ else {
48
+ set(body, next)
49
+ i++
50
+ }
51
+ continue
52
+ }
53
+ if (token.startsWith("-") && token.length === 2) {
54
+ const key = alias[token.slice(1)] || token.slice(1)
55
+ const next = argv[i + 1]
56
+ if (next === undefined || next.startsWith("-") || SWITCHES.has(key)) flags.set(key, true)
57
+ else {
58
+ flags.set(key, next)
59
+ i++
60
+ }
61
+ continue
62
+ }
63
+ positionals.push(token)
64
+ }
65
+ return { positionals, flags }
66
+ }
67
+
68
+ export function strFlag(flags: Flags, name: string): string | undefined {
69
+ const value = flags.get(name)
70
+ return typeof value === "string" ? value : undefined
71
+ }
72
+
73
+ export function boolFlag(flags: Flags, name: string): boolean {
74
+ const value = flags.get(name)
75
+ if (value === undefined || value === false) return false
76
+ if (value === true) return true
77
+ return !/^(0|false|no|off)$/i.test(value)
78
+ }
79
+
80
+ /** --skill NAME (repeatable, or comma-separated): browser skills for every run; `--skill none` runs without any. Unset: the engine picks. */
81
+ export function skillFlags(flags: Flags): string[] | undefined {
82
+ const raw = strFlag(flags, "skill")
83
+ if (raw === undefined) return undefined
84
+ const names = raw.split(",").map((n) => n.trim()).filter(Boolean)
85
+ return names.includes("none") ? [] : names
86
+ }
87
+
88
+ export function configOpts(flags: Flags) {
89
+ return {
90
+ configPath: strFlag(flags, "config"),
91
+ profile: strFlag(flags, "profile"),
92
+ }
93
+ }
94
+
95
+ export function memoryFlags(flags: Flags) {
96
+ const replay = strFlag(flags, "replay")
97
+ return {
98
+ replay: replay ? replayMode(replay) : undefined,
99
+ memory: boolFlag(flags, "no-memory") ? false : undefined,
100
+ }
101
+ }
102
+
103
+ export function maxStepsFlag(flags: Flags): number | undefined {
104
+ const raw = strFlag(flags, "max-steps")
105
+ if (raw && !(Number(raw) >= 1)) throw new Error("--max-steps must be a positive number")
106
+ return raw ? Number(raw) : undefined
107
+ }
108
+
109
+ /** --repo DIR, null for --no-code, undefined for code.repo from forgeqe.yaml. */
110
+ export function repoFlag(flags: Flags): string | null | undefined {
111
+ if (boolFlag(flags, "no-code")) return null
112
+ const dir = strFlag(flags, "repo")
113
+ return dir ? path.resolve(dir) : undefined
114
+ }
@@ -0,0 +1,167 @@
1
+ import { HELP } from "../commands.ts"
2
+
3
+ /** `forgeqe --help`. */
4
+
5
+ export const HELP_TEXT = `ForgeQE — browser agent CLI
6
+
7
+ Live runs use the ForgeQE engine (Python) with an OpenTUI shell. Tests are written by Scribe, the coding agent.
8
+ No FastAPI or database. LLM profiles live in llms.json, settings in forgeqe.yaml.
9
+
10
+ Usage:
11
+ forgeqe init [--dir .] [--global] [--force] [--profile name]
12
+ forgeqe setup [--force]
13
+ forgeqe models [--config forgeqe.yaml]
14
+ forgeqe features [--config forgeqe.yaml] what ForgeQE can do, and what each capability still needs here
15
+ forgeqe docs [--guide|--open] [--copy DIR] the guides and the architecture page (--guide: the user guide in the browser; --open: the architecture page)
16
+ forgeqe start [--profile name] [--url URL] [--goal TEXT] [--headed] [--replay MODE] [--no-memory]
17
+ [--repo DIR] [--no-code]
18
+ forgeqe run "<request>" [--profile name] [--url URL] [--headed] [--plain] [--max-steps N]
19
+ [--repo DIR] [--no-code] [--replay MODE] [--no-memory] [--attempts N] [--code-model provider/model]
20
+ ForgeQE's agent works out what the request needs: a browser goal,
21
+ test cases from Jira, Azure DevOps, Xray or files, a test, a pipeline
22
+ forgeqe run --direct "<goal>" [--out DIR] [--skill NAME] ... one browser goal, no agent
23
+ forgeqe suite <source...> [--parallel N] [--repo DIR] [--no-code] [--no-write-back] [--out DIR]
24
+ [--profile name] [--url URL] [--headed] [--plain] [--attempts N] [--skill NAME]
25
+ run test cases the same way every time (what pipelines call)
26
+ forgeqe cases <source...> [--goal] list the test cases a source gives (--goal: the goal each runs)
27
+ forgeqe code [run-dir] [--repo DIR] [--attempts N] [--code-model provider/model]
28
+ write a test for a run (default: the latest) with Scribe
29
+ forgeqe heal ["<test command>"] [--repo DIR] [--attempts N] [--code-model provider/model] [--headed] [--plain]
30
+ run the repo's tests and heal the ones the app has outgrown
31
+ forgeqe monitor [source...] [--every 15m] [--once] [--replay MODE] [--parallel N] [--out DIR] [--verbose] [--skill NAME]
32
+ check flows on a schedule and alert Teams or email when one breaks
33
+ forgeqe monitor --test-alerts send a sample alert to check the Teams and email settings
34
+ forgeqe mcp [--repo DIR] [--profile name] [--url URL] [--headed]
35
+ ForgeQE's tools as an MCP server on stdio, for other agents
36
+ forgeqe mcp list the Azure DevOps, Jira and other MCP servers ForgeQE's agent gets
37
+ forgeqe pr-tests [pr] [--base BRANCH] [--agent] [--run] [--post]
38
+ the tests a pull request (or the commits since BRANCH) can break; --run runs
39
+ them and exits 1 when one fails (a CI gate), --post puts the results on the PR
40
+ forgeqe quality [run-dir] a run's WCAG rules passed and failed, Core Web Vitals, and budgets
41
+ forgeqe quality trends [page] accessibility and timings per page over time: what got worse or better
42
+ forgeqe notify test [channel] send a test card to the Teams and email notification channels
43
+ forgeqe eval domain [--only id,id] [--min 0.8] does the agent apply domain and regulatory knowledge? (with the domain declared and without)
44
+ forgeqe prompts [list] every agent prompt and where it comes from (ForgeQE's or your edited copy)
45
+ forgeqe prompts show|diff|reset <name> read one, see how yours differs from ForgeQE's, or go back to ForgeQE's
46
+ forgeqe visual [run-dir] each page of a run against its approved screenshot: same, changed (with a diff), new
47
+ forgeqe visual approve [run-dir] [--page a,b] [--yes]
48
+ make the run's screenshots the new baselines (an intended change)
49
+ forgeqe explore <story> [--url URL] [--charters N] [--roles a,b] [--focus TEXT] [--yes] [--file-bugs]
50
+ exploratory testing of a story (ado:123, SHOP-12, a link, or its text):
51
+ charters beyond the scripted cases, defects confirmed by a second run,
52
+ proposed test cases, bugs for confirmed defects; exits 1 on a defect
53
+ forgeqe api contracts --spec <link|file> API contracts from OpenAPI/Swagger, WSDL, GraphQL, .proto, or grpc://host:port
54
+ forgeqe api contracts --run [RUN] API contracts from the calls a browser run saw the app make (latest run)
55
+ forgeqe api run [SET] [--base-url URL] [--only a,b] [--allow-writes]
56
+ run contracts against the API (no browser); exits 1 when one fails
57
+ forgeqe api tests [SET] [--repo DIR] [--include-writes]
58
+ the coding agent writes them as API tests in the repository's framework
59
+ forgeqe api scenarios --run [RUN] an end-to-end API scenario from a browser run: values chained call to call
60
+ forgeqe api scenarios [SET] [--spec <link|file>] [--no-negative]
61
+ lifecycle, browse, linked, and negative scenarios from an API description
62
+ forgeqe api scenarios --cases <ado:2238,...> | --text "<scenario>" [--spec <link|file>]
63
+ scenarios planned from test cases or plain English
64
+ forgeqe api run-scenarios [SET] [--allow-writes] [--base-url URL]
65
+ run them against the API; exits 1 when one fails
66
+ forgeqe api tests [SET] --scenarios[=a,b] --repo DIR
67
+ scenario tests on the repository's existing API helpers
68
+ forgeqe api list saved contract sets
69
+ forgeqe auth login [system] [--no-browser] sign in to azure_devops, github, jira, confluence, testrail, zephyr, qtest
70
+ or xray in the browser, and save the token in the system's credential store
71
+ forgeqe auth login <ENV_NAME> save one secret (an LLM API key, a webhook URL) in the credential store
72
+ forgeqe auth [status] which systems are signed in, and how
73
+ forgeqe auth logout <system|ENV_NAME> forget a saved sign-in
74
+ forgeqe login <name> [--url URL] sign in once by hand in a browser; runs reuse the saved session
75
+ forgeqe logins [clear <name>|--all] saved logins (logins: in forgeqe.yaml) and their sessions
76
+ forgeqe memory [list] hosts, pages, elements and flows in page memory
77
+ forgeqe memory show <host|url> [--locators] [--all]
78
+ forgeqe memory flows [--steps]
79
+ forgeqe memory clear [host] --yes
80
+ forgeqe skills [list|check] the skills each agent can use, where they come from, and problems
81
+ forgeqe skills show <name> one skill's instructions
82
+ forgeqe skills new <name> [--user] [--agent browser|scribe|forgeqe] [--description TEXT]
83
+ a new skill in .forgeqe/skills (--user: ~/.forgeqe/skills)
84
+ forgeqe skills enable|disable <name> turn a skill on or off for this project (skills.disable)
85
+ forgeqe logs recent session logs (one per forgeqe session, in .forgeqe/logs)
86
+ forgeqe logs bundle [log] [--shots] one archive to share for debugging: the session log, its run
87
+ folders (--shots: with screenshots), settings without secrets
88
+ forgeqe update [version] [--check] [--wait MIN]
89
+ update every forgeqe on PATH (bun, npm and pip installs) to the
90
+ latest release, waiting while the registries catch up (default 20)
91
+ forgeqe --version
92
+ --debug (or FORGEQE_DEBUG=1) on any command also log the browser engine's debug output and keep long values whole
93
+ --verbose (or FORGEQE_VERBOSE=1) show everything the agents do while they work: the browser agent's
94
+ reasoning, page memory, engine logs, every file the coding agent reads
95
+ (default: progress, step results, files written, test results, problems)
96
+
97
+ Page memory (memory: in forgeqe.yaml; default .forgeqe/memory next to it, or ~/.forgeqe/memory) keeps every element seen on each
98
+ page with ranked locators and a confidence %, plus the steps of each goal's last successful run.
99
+ --replay verify replay a known goal without the LLM, then one LLM step checks the result (default)
100
+ --replay trust replay and finish without the LLM when every step works
101
+ --replay off never replay; still record pages
102
+
103
+ ForgeQE's agent (forgeqe start, forgeqe run) runs on the agent runtime with the active profile's model (or code.model) and
104
+ calls ForgeQE's tools: browser runs, reading and running test cases, writing tests and running them, page memory, pushing
105
+ a branch, and storing CI secrets. It reads the request and decides; there are no commands to pick a source.
106
+ forgeqe run "Search hotels in Mumbai on booking.com for next weekend"
107
+ forgeqe run "Run SHOP-12 and SHOP-14" (Jira keys, Azure DevOps ids, Xray tests, files, or pasted cases)
108
+ forgeqe run "Add an Azure DevOps pipeline that runs test plan 12 every night"
109
+ Pipelines: the agent writes or updates the workflow, validates it, pushes it to a new branch (never the default
110
+ branch; git push itself is blocked), stores the keys it needs as secrets without seeing them, triggers it with gh or
111
+ az, watches it, and fixes it until it passes. The pipeline itself calls forgeqe suite.
112
+ It needs the agent runtime (downloaded automatically) and a live profile. Without them the request runs as one browser goal.
113
+ Exit codes for run: 0 done, 1 failed, 2 stopped, 3 the agent needs more information.
114
+
115
+ Tests (code: in forgeqe.yaml, or --repo): after a run passes, the agent gets the run's steps and page memory,
116
+ studies the repo's existing tests, page objects and fixtures, and writes a test. ForgeQE runs that test itself;
117
+ the test counts only when it passes, within --attempts (default 5) runs.
118
+
119
+ Test case sources for forgeqe suite and cases (sources: in forgeqe.yaml): a file (.txt/.md split on --- lines,
120
+ .csv, .yaml, .json, .feature), ado:123,456 ado:plan=12/suite=34 jira:SHOP-1 jira:jql=<query> xray:SHOP-1
121
+ xray:plan=SHOP-100 testrail:C12,C13 testrail:run=45 testrail:plan=7 zephyr:SHOP-T1 zephyr:cycle=SHOP-R5
122
+ qtest:1234 qtest:cycle=88, or quoted text. Each case runs as one goal; its expected results are the checks, and
123
+ the agent gives a verdict for every step. Results go back to Azure DevOps (test run or comment), Jira (comment),
124
+ Xray (Test Execution), TestRail (run), Zephyr Scale (test cycle) or qTest (test run logs), with step results.
125
+ With sources.bugs, each failed case files a bug in Jira or Azure DevOps, or comments on the open one.
126
+ The report is report.html (step verdicts, timeline with screenshots, timings, environment) + junit.xml in
127
+ out/suites/<time>/; every run folder also has its own report.html.
128
+
129
+ Healing (forgeqe heal): ForgeQE runs the tests (the command given, code.test_command, or one the agent finds).
130
+ For each failing test, Scribe re-runs its flow in the real browser: when the flow still works, the test is out
131
+ of date and it updates the steps and locators from page memory; when the browser fails at the same step, it is an
132
+ app bug and the test is left alone. ForgeQE re-runs every healed test and then the whole command; only those runs
133
+ count. Quote the command, or put it after --: forgeqe heal -- npx playwright test --project=chromium
134
+ The report is heal.md in out/heal/<time>/, next to the browser runs.
135
+
136
+ Monitoring (monitor: in forgeqe.yaml): each round runs the checks (any source forgeqe suite takes) replaying page
137
+ memory, so a known flow costs one model call to confirm the outcome (or none with --replay trust). A check alerts
138
+ after monitor.fail_after failing rounds, again every monitor.remind_every while it fails, and once when it recovers,
139
+ through a Teams webhook and email (SMTP). --once runs one round for cron or a scheduled pipeline (keep
140
+ out/monitor/state.json between runs so alerts fire on changes only). Rounds and their reports are in out/monitor/.
141
+
142
+ Skills (skills: in forgeqe.yaml): folders with a SKILL.md (name, description, optional agents: [forgeqe, scribe,
143
+ browser]) that teach the agents a repo's conventions, a framework or an app. ForgeQE ships some; yours go in
144
+ .forgeqe/skills next to forgeqe.yaml (or the repo's) and ~/.forgeqe/skills, and a project skill replaces a built-in
145
+ one with the same name. ForgeQE's agent and Scribe load the ones that fit the job; for browser runs the agent passes
146
+ them, or the engine picks them from the goal. --skill NAME (repeatable) fixes the browser skills; --skill none uses none.
147
+
148
+ Saved logins (logins: in forgeqe.yaml): the browser agent signs in with credentials from the environment (it only
149
+ sees placeholders), fills authenticator codes from a TOTP secret, and keeps the session for the next run.
150
+ Generated specs read the same variables. forgeqe login <name> saves a session you sign in to by hand (SSO).
151
+
152
+ Shell (forgeqe start):
153
+ ${HELP.split("\n").map((l) => ` ${l}`).join("\n")}
154
+
155
+ MCP servers for ForgeQE's agent come from forgeqe.yaml: Microsoft's Azure DevOps server (npx @azure-devops/mcp)
156
+ for sources.azure_devops, mcp-atlassian (uvx) or Atlassian's hosted server for sources.jira and sources.confluence,
157
+ and anything under mcp:.
158
+ Org, project, site URL and token names come from the same settings; tokens stay in the environment. A system
159
+ without a connected server (Xray has none) goes through ForgeQE's own API tools, including query_api.
160
+
161
+ MCP (forgeqe mcp): ForgeQE's tools for Claude Code, Cursor, Codex or any MCP client, e.g.
162
+ {"mcpServers": {"forgeqe": {"command": "forgeqe", "args": ["mcp"]}}}
163
+
164
+ OpenTUI needs Bun 1.3+ (Node 22 cannot load the native renderer).
165
+ The first live run installs the engine into ~/.forgeqe (uv or Python 3.11+); forgeqe setup does it up front.
166
+ Each run gets its own folder under .forgeqe/reports/<run-id>/ (report, steps, screenshots).
167
+ `