@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.
- package/HOW_TO_USE.md +1639 -0
- package/README.md +338 -2
- package/bin/forgeqe.d.ts +3 -0
- package/bin/forgeqe.js +188 -0
- package/docs/ARCHITECTURE.md +408 -0
- package/docs/security-data-flow.html +1870 -0
- package/docs/user-guide.html +2094 -0
- package/evals/domain/access-role-change.yaml +37 -0
- package/evals/domain/banking-payment-transfer.yaml +41 -0
- package/evals/domain/gdpr-account-erasure.yaml +30 -0
- package/evals/domain/insurance-claim-fnol.yaml +30 -0
- package/evals/domain/insurance-policy-renewal.yaml +37 -0
- package/evals/domain/lending-loan-application.yaml +36 -0
- package/forgeqe-architecture.html +718 -0
- package/forgeqe.example.yaml +203 -0
- package/knowledge/README.md +18 -0
- package/knowledge/business-rules.md +10 -0
- package/knowledge/compliance.md +16 -0
- package/knowledge/environments.md +5 -0
- package/knowledge/glossary.md +5 -0
- package/knowledge/product.md +16 -0
- package/knowledge/roles.md +9 -0
- package/knowledge/test-data.md +7 -0
- package/llms.example.json +51 -0
- package/package.json +63 -4
- package/prompts/README.md +34 -0
- package/prompts/api-scenarios.md +21 -0
- package/prompts/api-tests.md +39 -0
- package/prompts/brain.md +166 -0
- package/prompts/browser-capture.md +7 -0
- package/prompts/browser-case-report.md +7 -0
- package/prompts/browser-checks.md +14 -0
- package/prompts/browser-efficiency.md +7 -0
- package/prompts/browser-error-recovery.md +15 -0
- package/prompts/browser-extra.md +5 -0
- package/prompts/browser-goal-verification.md +13 -0
- package/prompts/browser-skill-select.md +6 -0
- package/prompts/domain-rule.md +13 -0
- package/prompts/eval-judge.md +19 -0
- package/prompts/explore-charters.md +27 -0
- package/prompts/find-tests.md +28 -0
- package/prompts/heal.md +40 -0
- package/prompts/pr-review.md +29 -0
- package/prompts/skill-rule.md +6 -0
- package/prompts/test-agent.md +34 -0
- package/prompts/test-reviewer.md +30 -0
- package/prompts/test-rules.md +28 -0
- package/py/forgeqe_engine/__init__.py +3 -0
- package/py/forgeqe_engine/__main__.py +3 -0
- package/py/forgeqe_engine/brand.py +453 -0
- package/py/forgeqe_engine/brand_assets.py +6 -0
- package/py/forgeqe_engine/browser.py +196 -0
- package/py/forgeqe_engine/capture.py +538 -0
- package/py/forgeqe_engine/checks.py +98 -0
- package/py/forgeqe_engine/cli.py +188 -0
- package/py/forgeqe_engine/diagnostics.py +609 -0
- package/py/forgeqe_engine/llm.py +195 -0
- package/py/forgeqe_engine/logins.py +251 -0
- package/py/forgeqe_engine/loops.py +224 -0
- package/py/forgeqe_engine/memory.py +1004 -0
- package/py/forgeqe_engine/network.py +244 -0
- package/py/forgeqe_engine/prompts.py +20 -0
- package/py/forgeqe_engine/replay.py +240 -0
- package/py/forgeqe_engine/report.py +173 -0
- package/py/forgeqe_engine/service.py +1335 -0
- package/py/forgeqe_engine/settings.py +93 -0
- package/py/forgeqe_engine/skills.py +88 -0
- package/py/forgeqe_engine/summary.py +239 -0
- package/py/forgeqe_engine/urls.py +81 -0
- package/py/forgeqe_engine/workflow_yaml.py +689 -0
- package/skills/dotnet-nunit-playwright/SKILL.md +34 -0
- package/skills/dynamics-365/SKILL.md +21 -0
- package/skills/playwright-typescript/SKILL.md +35 -0
- package/skills/writing-skills/SKILL.md +30 -0
- package/src/api/plan.ts +246 -0
- package/src/api/run.ts +204 -0
- package/src/api/scenario.ts +502 -0
- package/src/api/schema.ts +170 -0
- package/src/api/spec.ts +624 -0
- package/src/api/store.ts +232 -0
- package/src/api/traffic.ts +281 -0
- package/src/api/types.ts +163 -0
- package/src/api/write.ts +272 -0
- package/src/auth/signin.ts +494 -0
- package/src/auth/store.ts +170 -0
- package/src/brain/index.ts +189 -0
- package/src/brain/prompt.ts +19 -0
- package/src/capabilities/docs.ts +101 -0
- package/src/capabilities/guide.ts +455 -0
- package/src/capabilities/registry.ts +343 -0
- package/src/capabilities/render.ts +106 -0
- package/src/capabilities/status.ts +137 -0
- package/src/card.ts +11 -0
- package/src/cases/ado.ts +325 -0
- package/src/cases/api.ts +117 -0
- package/src/cases/bugs.ts +223 -0
- package/src/cases/confluence.ts +111 -0
- package/src/cases/context.ts +223 -0
- package/src/cases/dashboards.ts +137 -0
- package/src/cases/failure.ts +97 -0
- package/src/cases/files.ts +295 -0
- package/src/cases/goal.ts +53 -0
- package/src/cases/http.ts +111 -0
- package/src/cases/index.ts +131 -0
- package/src/cases/jira.ts +122 -0
- package/src/cases/qtest.ts +108 -0
- package/src/cases/quality.ts +190 -0
- package/src/cases/rag.ts +100 -0
- package/src/cases/readiness.ts +284 -0
- package/src/cases/save.ts +149 -0
- package/src/cases/sprints.ts +148 -0
- package/src/cases/testrail.ts +176 -0
- package/src/cases/types.ts +107 -0
- package/src/cases/wiki.ts +105 -0
- package/src/cases/xray.ts +206 -0
- package/src/cases/zephyr.ts +153 -0
- package/src/childEnv.ts +86 -0
- package/src/cli/args.ts +114 -0
- package/src/cli/help.ts +167 -0
- package/src/cli/index.ts +1142 -0
- package/src/cli/terminal.ts +187 -0
- package/src/code/areas.ts +337 -0
- package/src/code/attention.ts +214 -0
- package/src/code/find.ts +540 -0
- package/src/code/handoff.ts +541 -0
- package/src/code/history.ts +170 -0
- package/src/code/impact.ts +231 -0
- package/src/code/index.ts +34 -0
- package/src/code/livebrowser.ts +67 -0
- package/src/code/loop.ts +603 -0
- package/src/code/notes.ts +75 -0
- package/src/code/profile.ts +226 -0
- package/src/code/prompt.ts +303 -0
- package/src/code/prreview.ts +649 -0
- package/src/code/prtests.ts +103 -0
- package/src/code/replay.ts +772 -0
- package/src/code/review.ts +202 -0
- package/src/code/runtime.ts +781 -0
- package/src/code/slices.ts +159 -0
- package/src/code/testrun.ts +187 -0
- package/src/code/traceability.ts +100 -0
- package/src/code/worktree.ts +153 -0
- package/src/commands.ts +101 -0
- package/src/config.ts +970 -0
- package/src/detail.ts +65 -0
- package/src/domain.ts +19 -0
- package/src/engine/bridge.ts +359 -0
- package/src/engine/mock.ts +102 -0
- package/src/engine/setup.ts +298 -0
- package/src/entra.ts +154 -0
- package/src/env.ts +24 -0
- package/src/evals/domain.ts +338 -0
- package/src/explore/charters.ts +151 -0
- package/src/explore/run.ts +280 -0
- package/src/explore/story.ts +106 -0
- package/src/guard.ts +51 -0
- package/src/heal/loop.ts +357 -0
- package/src/heal/prompt.ts +143 -0
- package/src/heal/report.ts +95 -0
- package/src/index.ts +12 -0
- package/src/init.ts +102 -0
- package/src/knowledge.ts +114 -0
- package/src/legacyEnv.ts +9 -0
- package/src/llm.ts +61 -0
- package/src/locale.ts +25 -0
- package/src/logins.ts +148 -0
- package/src/managed.ts +101 -0
- package/src/mcp/apitools.ts +335 -0
- package/src/mcp/captools.ts +25 -0
- package/src/mcp/casetools.ts +223 -0
- package/src/mcp/commands.ts +421 -0
- package/src/mcp/exploretools.ts +129 -0
- package/src/mcp/external.ts +172 -0
- package/src/mcp/git.ts +78 -0
- package/src/mcp/notifytools.ts +49 -0
- package/src/mcp/prtools.ts +261 -0
- package/src/mcp/qualitytools.ts +154 -0
- package/src/mcp/repotests.ts +286 -0
- package/src/mcp/server.ts +234 -0
- package/src/mcp/tools.ts +810 -0
- package/src/memory.ts +250 -0
- package/src/money.ts +39 -0
- package/src/monitor/alerts.ts +94 -0
- package/src/monitor/background.ts +68 -0
- package/src/monitor/run.ts +263 -0
- package/src/monitor/state.ts +105 -0
- package/src/notify/events.ts +144 -0
- package/src/notify/notice.ts +177 -0
- package/src/prices.ts +175 -0
- package/src/print.ts +54 -0
- package/src/privacy.ts +35 -0
- package/src/prompts.ts +165 -0
- package/src/quality/budgets.ts +249 -0
- package/src/quality/history.ts +149 -0
- package/src/quality/visual.ts +201 -0
- package/src/report/data.ts +475 -0
- package/src/report/html.ts +106 -0
- package/src/report/template.html +84 -0
- package/src/run.ts +243 -0
- package/src/sessionLog.ts +216 -0
- package/src/sessionLogins.ts +94 -0
- package/src/skills/registry.ts +217 -0
- package/src/skills/scaffold.ts +65 -0
- package/src/skills/select.ts +37 -0
- package/src/skills/view.ts +44 -0
- package/src/suite/report.ts +135 -0
- package/src/suite/run.ts +303 -0
- package/src/summary.ts +188 -0
- package/src/tools.ts +130 -0
- package/src/tui/App.tsx +983 -0
- package/src/tui/clipboard.ts +36 -0
- package/src/tui/components.tsx +263 -0
- package/src/tui/dialogs.ts +65 -0
- package/src/tui/main.tsx +20 -0
- package/src/tui/text.ts +28 -0
- package/src/tui/theme.ts +104 -0
- package/src/tui/transcript.tsx +344 -0
- package/src/types.ts +146 -0
- package/src/update.ts +311 -0
- 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
|
+
}
|
package/src/childEnv.ts
ADDED
|
@@ -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
|
+
}
|
package/src/cli/args.ts
ADDED
|
@@ -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
|
+
}
|
package/src/cli/help.ts
ADDED
|
@@ -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
|
+
`
|