@capagents/forgeqe 1.1.1 → 1.1.2
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 +20 -4
- package/README.md +27 -1
- package/docs/user-guide.html +24 -4
- package/evals/prompts/brain/capability-question.yaml +12 -0
- package/evals/prompts/brain/failed-runs-cause.yaml +12 -0
- package/evals/prompts/brain/release-route.yaml +14 -0
- package/evals/prompts/brain/report-request.yaml +13 -0
- package/evals/prompts/brain/secret-request.yaml +9 -0
- package/evals/prompts/explore-charters/lockout.yaml +18 -0
- package/evals/prompts/explore-charters/transfer-limit.yaml +16 -0
- package/evals/prompts/refine/current-release.yaml +26 -0
- package/evals/prompts/refine/missing-item.yaml +10 -0
- package/evals/prompts/refine/question-stays-question.yaml +13 -0
- package/evals/prompts/refine/report-request.yaml +19 -0
- package/forgeqe.example.yaml +39 -0
- package/package.json +1 -1
- package/prompts/README.md +28 -7
- package/prompts/api-scenarios.md +56 -24
- package/prompts/api-tests.md +67 -51
- package/prompts/brain.md +277 -268
- package/prompts/browser-capture.md +8 -7
- package/prompts/browser-case-report.md +13 -12
- package/prompts/browser-checks.md +17 -18
- package/prompts/browser-efficiency.md +4 -1
- package/prompts/browser-error-recovery.md +5 -4
- package/prompts/browser-extra.md +1 -0
- package/prompts/browser-goal-verification.md +8 -7
- package/prompts/browser-skill-select.md +28 -1
- package/prompts/conventions.md +10 -0
- package/prompts/domain-rule.md +17 -14
- package/prompts/eval-judge.md +36 -13
- package/prompts/explore-charters.md +53 -22
- package/prompts/find-tests.md +45 -30
- package/prompts/heal.md +42 -32
- package/prompts/pr-review.md +44 -28
- package/prompts/refine.md +73 -40
- package/prompts/skill-rule.md +4 -1
- package/prompts/test-agent.md +61 -45
- package/prompts/test-reviewer.md +64 -37
- package/prompts/test-rules.md +59 -56
- package/py/forgeqe_engine/browser.py +111 -1
- package/py/forgeqe_engine/capture.py +8 -1
- package/py/forgeqe_engine/checks.py +20 -8
- package/py/forgeqe_engine/prompts.py +9 -0
- package/py/forgeqe_engine/report.py +15 -8
- package/py/forgeqe_engine/service.py +50 -28
- package/py/forgeqe_engine/skills.py +31 -7
- package/src/auth/signin.ts +30 -1
- package/src/bi.ts +899 -0
- package/src/childEnv.ts +4 -0
- package/src/cli/help.ts +10 -2
- package/src/cli/index.ts +114 -10
- package/src/config.ts +149 -0
- package/src/engine/bridge.ts +32 -2
- package/src/evals/prompts.ts +288 -0
- package/src/mcp/server.ts +8 -0
- package/src/mcp/tools.ts +4 -0
- package/src/prompts.ts +6 -3
- package/src/promptstyle.ts +159 -0
- package/src/run.ts +1 -0
- package/src/ste.ts +8 -2
package/src/childEnv.ts
CHANGED
|
@@ -84,3 +84,7 @@ export function privateEnvNames(cfg: AppConfig, extra: Iterable<string> = []): s
|
|
|
84
84
|
export function loginEnvNames(logins: { usernameEnv?: string; passwordEnv?: string; totpEnv?: string }[]): string[] {
|
|
85
85
|
return logins.flatMap((l) => [l.usernameEnv, l.passwordEnv, l.totpEnv]).filter((n): n is string => Boolean(n))
|
|
86
86
|
}
|
|
87
|
+
|
|
88
|
+
export function browserProviderEnvNames(browser: Pick<AppConfig["browser"], "provider" | "remote">): string[] {
|
|
89
|
+
return browser.provider === "local" || !browser.remote ? [] : [browser.remote.usernameEnv, browser.remote.accessKeyEnv].filter(Boolean)
|
|
90
|
+
}
|
package/src/cli/help.ts
CHANGED
|
@@ -34,6 +34,10 @@ Usage:
|
|
|
34
34
|
forgeqe mcp [--repo DIR] [--profile name] [--url URL] [--headed]
|
|
35
35
|
ForgeQE's tools as an MCP server on stdio, for other agents
|
|
36
36
|
forgeqe mcp list the Azure DevOps, Jira and other MCP servers ForgeQE's agent gets
|
|
37
|
+
forgeqe bi [build] [--evidence-dir DIR] [--data-dir DIR] [--build-dir DIR]
|
|
38
|
+
export ForgeQE + ADO/Jira datasets into an Evidence project and build static BI HTML
|
|
39
|
+
forgeqe bi init scaffold or refresh the managed Evidence project files
|
|
40
|
+
forgeqe bi sync refresh the CSV datasets and generated Evidence pages, without building
|
|
37
41
|
forgeqe pr-tests [pr] [--base BRANCH] [--agent] [--run] [--post]
|
|
38
42
|
the tests a pull request (or the commits since BRANCH) can break; --run runs
|
|
39
43
|
them and exits 1 when one fails (a CI gate), --post puts the results on the PR
|
|
@@ -67,8 +71,8 @@ Usage:
|
|
|
67
71
|
forgeqe api tests [SET] --scenarios[=a,b] --repo DIR
|
|
68
72
|
scenario tests on the repository's existing API helpers
|
|
69
73
|
forgeqe api list saved contract sets
|
|
70
|
-
forgeqe auth login [system] [--no-browser] sign in to azure_devops, github, jira, confluence, testrail, zephyr, qtest
|
|
71
|
-
or
|
|
74
|
+
forgeqe auth login [system] [--no-browser] sign in to azure_devops, github, jira, confluence, testrail, zephyr, qtest,
|
|
75
|
+
xray, browserstack, or testmu in the browser, and save the token in the system's credential store
|
|
72
76
|
forgeqe auth login <ENV_NAME> save one secret (an LLM API key, a webhook URL) in the credential store
|
|
73
77
|
forgeqe auth [status] which systems are signed in, and how
|
|
74
78
|
forgeqe auth logout <system|ENV_NAME> forget a saved sign-in
|
|
@@ -127,6 +131,10 @@ With sources.bugs, each failed case files a bug in Jira or Azure DevOps, or comm
|
|
|
127
131
|
The report is report.html (step verdicts, timeline with screenshots, timings, environment) + junit.xml in
|
|
128
132
|
out/suites/<time>/; every run folder also has its own report.html.
|
|
129
133
|
|
|
134
|
+
Static BI (bi: in forgeqe.yaml): \`forgeqe bi\` writes curated CSV datasets from ForgeQE's own suite and readiness
|
|
135
|
+
reports plus optional Azure DevOps WIQL and Jira JQL datasets, scaffolds an Evidence project, and runs
|
|
136
|
+
\`evidence build\` to produce static HTML in the build directory. No server stays up between runs.
|
|
137
|
+
|
|
130
138
|
Healing (forgeqe heal): ForgeQE runs the tests (the command given, code.test_command, or one the agent finds).
|
|
131
139
|
For each failing test, Scribe re-runs its flow in the real browser: when the flow still works, the test is out
|
|
132
140
|
of date and it updates the steps and locators from page memory; when the browser fails at the same step, it is an
|
package/src/cli/index.ts
CHANGED
|
@@ -44,6 +44,8 @@ import { resultsCard, runSelection, selectionCard } from "../code/find.ts"
|
|
|
44
44
|
import { listSets as listApiSets } from "../api/store.ts"
|
|
45
45
|
import { builtinPrompt, lineDiff, listPrompts, prompt, promptDirs, promptFile, promptWarnings } from "../prompts.ts"
|
|
46
46
|
import { steScore, STE_TARGET } from "../ste.ts"
|
|
47
|
+
import { checkStyle, promptKind, PROMPT_KINDS, RFC_TARGET } from "../promptstyle.ts"
|
|
48
|
+
import { BUILTIN_CASES, loadPromptCases, previousRates, runCase, saveResults, type CaseResult } from "../evals/prompts.ts"
|
|
47
49
|
import { resultsMarkdown, selectPrTests } from "../code/prtests.ts"
|
|
48
50
|
import { postPrComment, resolvePr } from "../code/prreview.ts"
|
|
49
51
|
import { privateEnvNames } from "../childEnv.ts"
|
|
@@ -52,9 +54,10 @@ import { featuresText } from "../capabilities/render.ts"
|
|
|
52
54
|
import { DOMAIN_CASES, gate, loadDomainCases, reportMarkdown, runDomainEval, saveReport, VARIANTS, type Variant } from "../evals/domain.ts"
|
|
53
55
|
import { evalNotice, notifiedLine, notify, notifySuite } from "../notify/events.ts"
|
|
54
56
|
import { sendNotice, type Notice } from "../notify/notice.ts"
|
|
57
|
+
import { buildBi, initBiProject, syncBiData } from "../bi.ts"
|
|
55
58
|
|
|
56
59
|
const AGENT_COMMANDS = new Set(["start", "run", "code", "suite", "heal", "monitor", "explore"])
|
|
57
|
-
const LOGGED_COMMANDS = new Set([...AGENT_COMMANDS, "mcp", "login", "setup"])
|
|
60
|
+
const LOGGED_COMMANDS = new Set([...AGENT_COMMANDS, "mcp", "login", "setup", "bi"])
|
|
58
61
|
|
|
59
62
|
/** How much of the agents' work to print: brief unless --verbose. */
|
|
60
63
|
const detailOf = (flags: Flags) => detailFrom(boolFlag(flags, "verbose"))
|
|
@@ -184,8 +187,10 @@ export async function main(argv: string[]): Promise<number> {
|
|
|
184
187
|
return await withUsageLine(() => cmdApi(positionals.slice(1), flags))
|
|
185
188
|
case "explore":
|
|
186
189
|
return await withUsageLine(() => cmdExplore(positionals.slice(1), flags))
|
|
190
|
+
case "bi":
|
|
191
|
+
return await cmdBi(positionals.slice(1), flags)
|
|
187
192
|
case "prompts":
|
|
188
|
-
return cmdPrompts(positionals.slice(1), flags)
|
|
193
|
+
return positionals[1] === "eval" ? await withUsageLine(() => cmdPromptEval(positionals.slice(2), flags)) : cmdPrompts(positionals.slice(1), flags)
|
|
189
194
|
case "eval":
|
|
190
195
|
return await withUsageLine(() => cmdEval(positionals.slice(1), flags))
|
|
191
196
|
case "notify":
|
|
@@ -461,6 +466,50 @@ async function cmdExplore(args: string[], flags: Flags): Promise<number> {
|
|
|
461
466
|
}
|
|
462
467
|
}
|
|
463
468
|
|
|
469
|
+
async function cmdBi(args: string[], flags: Flags): Promise<number> {
|
|
470
|
+
const cfg = loadConfig(configOpts(flags))
|
|
471
|
+
const action = args[0] || "build"
|
|
472
|
+
const overrides = {
|
|
473
|
+
evidenceDir: strFlag(flags, "evidence-dir") || strFlag(flags, "dir"),
|
|
474
|
+
dataDir: strFlag(flags, "data-dir"),
|
|
475
|
+
buildDir: strFlag(flags, "build-dir") || strFlag(flags, "out"),
|
|
476
|
+
}
|
|
477
|
+
switch (action) {
|
|
478
|
+
case "init": {
|
|
479
|
+
const out = await initBiProject(cfg, overrides)
|
|
480
|
+
console.log(`Evidence project ${out.evidenceDir}`)
|
|
481
|
+
for (const file of out.files) console.log(`generated ${path.relative(process.cwd(), file) || file}`)
|
|
482
|
+
return 0
|
|
483
|
+
}
|
|
484
|
+
case "sync": {
|
|
485
|
+
const out = await syncBiData(cfg, overrides)
|
|
486
|
+
console.log(`Evidence project ${out.evidenceDir}`)
|
|
487
|
+
console.log(`Data export ${out.dataDir}`)
|
|
488
|
+
console.log(`Build output ${out.buildDir}`)
|
|
489
|
+
console.log(`Suite runs ${out.suites}`)
|
|
490
|
+
console.log(`Case results ${out.caseResults}`)
|
|
491
|
+
console.log(`Readiness reports ${out.readinessReports}`)
|
|
492
|
+
console.log(`External datasets ${out.externalDatasets}`)
|
|
493
|
+
console.log(`Summary ${path.relative(process.cwd(), out.summaryFile) || out.summaryFile}`)
|
|
494
|
+
return 0
|
|
495
|
+
}
|
|
496
|
+
case "build": {
|
|
497
|
+
const out = await buildBi(cfg, overrides)
|
|
498
|
+
console.log(`Evidence project ${out.evidenceDir}`)
|
|
499
|
+
console.log(`Static site ${out.buildDir}`)
|
|
500
|
+
console.log(`Index ${path.relative(process.cwd(), out.indexFile) || out.indexFile}`)
|
|
501
|
+
console.log(`Summary ${path.relative(process.cwd(), out.summaryFile) || out.summaryFile}`)
|
|
502
|
+
console.log(`Suite runs ${out.suites}`)
|
|
503
|
+
console.log(`Case results ${out.caseResults}`)
|
|
504
|
+
console.log(`Readiness reports ${out.readinessReports}`)
|
|
505
|
+
console.log(`External datasets ${out.externalDatasets}`)
|
|
506
|
+
return 0
|
|
507
|
+
}
|
|
508
|
+
default:
|
|
509
|
+
throw new Error("Usage: forgeqe bi [init|sync|build] [--evidence-dir DIR] [--data-dir DIR] [--build-dir DIR]")
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
464
513
|
/** forgeqe notify test [channel]: a test card to every notification channel (or one), to check each arrives. */
|
|
465
514
|
async function cmdNotify(args: string[], flags: Flags): Promise<number> {
|
|
466
515
|
if (args[0] !== "test") throw new Error("Usage: forgeqe notify test [channel]")
|
|
@@ -535,6 +584,51 @@ async function cmdEval(args: string[], flags: Flags): Promise<number> {
|
|
|
535
584
|
* from (ForgeQE's, or an edited copy in .forgeqe/prompts), how an edited one differs from ForgeQE's, and how closely each
|
|
536
585
|
* follows ASD-STE100.
|
|
537
586
|
*/
|
|
587
|
+
/**
|
|
588
|
+
* forgeqe prompts eval [prompt] [--case <id>] [--runs <n>]: runs the evaluation cases of the prompts (ForgeQE's, and
|
|
589
|
+
* the project's in .forgeqe/evals/prompts), with checks decided by code, and compares each pass rate with the last run.
|
|
590
|
+
*/
|
|
591
|
+
async function cmdPromptEval(args: string[], flags: Flags): Promise<number> {
|
|
592
|
+
const config = loadConfig(configOpts(flags))
|
|
593
|
+
const runs = Math.max(1, Number(strFlag(flags, "runs")) || 1)
|
|
594
|
+
const dirs = [BUILTIN_CASES, ...(config.configPath ? [path.join(path.dirname(config.configPath), ".forgeqe", "evals", "prompts")] : [])]
|
|
595
|
+
const cases = loadPromptCases(dirs, { prompt: args[0], case: strFlag(flags, "case") })
|
|
596
|
+
if (!cases.length) throw new Error(`No evaluation cases${args[0] ? ` for ${args[0]}` : ""}. They are in ${dirs.join(" and ")}, as <prompt>/<case>.yaml.`)
|
|
597
|
+
const before = previousRates(config)
|
|
598
|
+
const results: CaseResult[] = []
|
|
599
|
+
let failed = 0
|
|
600
|
+
for (const c of cases) {
|
|
601
|
+
process.stdout.write(`${c.prompt}/${c.id} … `)
|
|
602
|
+
const r = await runCase(c, config, process.cwd(), runs)
|
|
603
|
+
results.push(r)
|
|
604
|
+
if (r.skipped) {
|
|
605
|
+
console.log(`skipped (${r.skipped})`)
|
|
606
|
+
continue
|
|
607
|
+
}
|
|
608
|
+
const passed = r.runs.filter((x) => x.pass).length
|
|
609
|
+
if (passed < r.runs.length) failed++
|
|
610
|
+
const was = before.get(`${c.prompt}/${c.id}`)
|
|
611
|
+
const tokens = Math.round(r.runs.reduce((a, x) => a + x.tokens, 0) / r.runs.length / 100) / 10
|
|
612
|
+
console.log(`${passed === r.runs.length ? "✓" : "✗"} ${passed}/${r.runs.length}${was ? ` (was ${was[0]}/${was[1]})` : ""} ${tokens}K tokens a run ${c.title}`)
|
|
613
|
+
// Each check that failed in any run, once, with what was seen.
|
|
614
|
+
const seen = new Set<string>()
|
|
615
|
+
for (const run of r.runs) {
|
|
616
|
+
if (run.error) console.log(` error: ${run.error}`)
|
|
617
|
+
for (const ch of run.checks.filter((x) => !x.pass && !seen.has(x.id))) {
|
|
618
|
+
seen.add(ch.id)
|
|
619
|
+
console.log(` ✗ ${ch.id}: ${ch.detail}`)
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
const seenText = r.runs.find((x) => x.seen)?.seen
|
|
623
|
+
if (seenText) console.log(` output: ${seenText}`)
|
|
624
|
+
}
|
|
625
|
+
const file = saveResults(config, results)
|
|
626
|
+
const ran = results.filter((r) => !r.skipped)
|
|
627
|
+
const runsAll = ran.flatMap((r) => r.runs)
|
|
628
|
+
console.log(`\n${runsAll.filter((x) => x.pass).length} of ${runsAll.length} runs passed in ${ran.length} cases${results.length > ran.length ? `; ${results.length - ran.length} skipped` : ""}. Results: ${path.relative(process.cwd(), file) || file}`)
|
|
629
|
+
return failed ? 1 : 0
|
|
630
|
+
}
|
|
631
|
+
|
|
538
632
|
function cmdPrompts(args: string[], flags: Flags): number {
|
|
539
633
|
loadConfig(configOpts(flags))
|
|
540
634
|
const action = args[0] || "list"
|
|
@@ -547,20 +641,30 @@ function cmdPrompts(args: string[], flags: Flags): number {
|
|
|
547
641
|
return 0
|
|
548
642
|
}
|
|
549
643
|
if (action === "check") {
|
|
550
|
-
//
|
|
644
|
+
// The prompting standard (src/promptstyle.ts) for each prompt in use, ForgeQE's or the edited copy; with a name,
|
|
645
|
+
// every issue to fix.
|
|
551
646
|
const all = listPrompts().filter((p) => !name || p.name === name)
|
|
552
647
|
if (name && !all.length) throw new Error(`No prompt named "${name}". forgeqe prompts lists them.`)
|
|
553
648
|
const width = Math.max(...all.map((p) => p.name.length))
|
|
554
|
-
let
|
|
649
|
+
let failing = 0
|
|
555
650
|
for (const p of all) {
|
|
556
651
|
const text = readFileSync(p.file, "utf8").replace(/^---\n[\s\S]*?\n---\n/, "")
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
if (
|
|
652
|
+
const kind = promptKind(p.kind)
|
|
653
|
+
const r = kind ? checkStyle(text, kind) : undefined
|
|
654
|
+
const issues = r ? r.issues : [{ rule: "kind", detail: `no kind in the header; use one of ${PROMPT_KINDS.join(", ")}` }]
|
|
655
|
+
if (issues.length) failing++
|
|
656
|
+
const pct = (n: number) => `${String(Math.round(n * 100)).padStart(3)}%`
|
|
657
|
+
const facts = r && r.kind !== "user" ? `STE ${pct(r.ste)} RFC 2119 ${r.ruleLines ? pct(r.rfc) : " –"}${r.kind === "structured" ? ` examples ${r.examples}` : ""}` : ""
|
|
658
|
+
console.log(`${issues.length ? "✗" : "✓"} ${p.name.padEnd(width)} ${(kind || "?").padEnd(10)} ${facts}${issues.length ? ` ${issues.length} issue${issues.length === 1 ? "" : "s"}` : ""}${p.source === "built-in" ? "" : ` (${p.source})`}`)
|
|
659
|
+
if (name) {
|
|
660
|
+
for (const i of issues) console.log(` - ${i.rule}: ${i.detail}`)
|
|
661
|
+
for (const i of steScore(text).issues) console.log(` - ste: ${i.issues.map((x) => x.detail).join("; ")}\n ${i.text}`)
|
|
662
|
+
}
|
|
561
663
|
}
|
|
562
|
-
console.log(
|
|
563
|
-
|
|
664
|
+
console.log(
|
|
665
|
+
`\nThe standard: ASD-STE100 (${Math.round(STE_TARGET * 100)}% of sentences), RFC 2119 key words for rules (${Math.round(RFC_TARGET * 100)}% of rule lines, no lower-case must/should), the sections of each kind, worked examples for structured prompts, and an execution contract for agents.${name ? "" : " forgeqe prompts check <name> lists what to fix."}`,
|
|
666
|
+
)
|
|
667
|
+
return failing ? 1 : 0
|
|
564
668
|
}
|
|
565
669
|
if (!name) throw new Error(`Usage: forgeqe prompts ${action} <name> (forgeqe prompts lists the names)`)
|
|
566
670
|
const where = promptFile(name)
|
package/src/config.ts
CHANGED
|
@@ -16,7 +16,28 @@ import { currencyCode, currencyForLocale } from "./money.ts"
|
|
|
16
16
|
import { listPrice, pricesDate, type DeploymentType, type ListPrice } from "./prices.ts"
|
|
17
17
|
import { ragConfig, type RagConfig } from "./cases/rag.ts"
|
|
18
18
|
|
|
19
|
+
export type BrowserProvider = "local" | "browserstack" | "testmu"
|
|
20
|
+
export type RemoteBrowserProvider = Exclude<BrowserProvider, "local">
|
|
21
|
+
|
|
22
|
+
export type BrowserRemoteConfig = {
|
|
23
|
+
provider: RemoteBrowserProvider
|
|
24
|
+
usernameEnv: string
|
|
25
|
+
accessKeyEnv: string
|
|
26
|
+
endpoint: string
|
|
27
|
+
browser: string
|
|
28
|
+
browserVersion: string
|
|
29
|
+
os?: string
|
|
30
|
+
osVersion?: string
|
|
31
|
+
platform?: string
|
|
32
|
+
project?: string
|
|
33
|
+
build?: string
|
|
34
|
+
name?: string
|
|
35
|
+
local: boolean
|
|
36
|
+
extra?: Record<string, unknown>
|
|
37
|
+
}
|
|
38
|
+
|
|
19
39
|
export type BrowserConfig = {
|
|
40
|
+
provider: BrowserProvider
|
|
20
41
|
headless: boolean
|
|
21
42
|
width: number
|
|
22
43
|
height: number
|
|
@@ -25,6 +46,7 @@ export type BrowserConfig = {
|
|
|
25
46
|
visionDetail: "low" | "high"
|
|
26
47
|
allowedHosts: string[]
|
|
27
48
|
startUrl?: string
|
|
49
|
+
remote?: BrowserRemoteConfig
|
|
28
50
|
/** ForgeQE overlay in a visible browser: edge glow, a bar with the current step, the person's input locked. */
|
|
29
51
|
overlay: boolean
|
|
30
52
|
/** A second model call checks the finished run against the goal, and its verdict can fail a run the agent completed. */
|
|
@@ -176,6 +198,34 @@ export type ApiConfig = {
|
|
|
176
198
|
timeoutSeconds: number
|
|
177
199
|
}
|
|
178
200
|
|
|
201
|
+
export type BiDatasetConfig =
|
|
202
|
+
| {
|
|
203
|
+
id: string
|
|
204
|
+
title: string
|
|
205
|
+
kind: "ado_wiql"
|
|
206
|
+
wiql: string
|
|
207
|
+
project?: string
|
|
208
|
+
}
|
|
209
|
+
| {
|
|
210
|
+
id: string
|
|
211
|
+
title: string
|
|
212
|
+
kind: "jira_jql"
|
|
213
|
+
jql: string
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export type BiConfig = {
|
|
217
|
+
/** Title of the generated Evidence project and its landing page. */
|
|
218
|
+
title: string
|
|
219
|
+
/** Evidence project root, generated and managed by ForgeQE. */
|
|
220
|
+
evidenceDir: string
|
|
221
|
+
/** Exported CSV datasets the Evidence project reads. */
|
|
222
|
+
dataDir: string
|
|
223
|
+
/** Static HTML output from `forgeqe bi build`. */
|
|
224
|
+
buildDir: string
|
|
225
|
+
/** Declarative external datasets to fetch on each sync/build. */
|
|
226
|
+
datasets: BiDatasetConfig[]
|
|
227
|
+
}
|
|
228
|
+
|
|
179
229
|
export type SuiteConfig = {
|
|
180
230
|
/** Browser runs at the same time. Coding runs one at a time. */
|
|
181
231
|
parallel: number
|
|
@@ -312,6 +362,8 @@ export type AppConfig = {
|
|
|
312
362
|
release?: ReleaseThresholds
|
|
313
363
|
/** API and contract tests (api: in forgeqe.yaml): see src/api/. */
|
|
314
364
|
api?: ApiConfig
|
|
365
|
+
/** Offline BI export and the Evidence project that renders it (bi: in forgeqe.yaml). */
|
|
366
|
+
bi?: BiConfig
|
|
315
367
|
/** Accessibility standard and accessibility/performance budgets (quality: in forgeqe.yaml): see src/quality/. */
|
|
316
368
|
quality?: QualityConfig
|
|
317
369
|
/** The product's industry and the regulations in scope (domain: in forgeqe.yaml): see src/domain.ts. */
|
|
@@ -371,6 +423,68 @@ function visionDetail(raw: unknown): BrowserConfig["visionDetail"] {
|
|
|
371
423
|
return String(raw || "low").toLowerCase() === "high" ? "high" : "low"
|
|
372
424
|
}
|
|
373
425
|
|
|
426
|
+
export function browserProvider(raw: unknown): BrowserProvider {
|
|
427
|
+
const provider = String(raw || "local").trim().toLowerCase().replace(/[\s-]/g, "_")
|
|
428
|
+
if (!provider || provider === "local") return "local"
|
|
429
|
+
if (provider === "browserstack") return "browserstack"
|
|
430
|
+
if (provider === "testmu" || provider === "lambdatest" || provider === "lambda_test") return "testmu"
|
|
431
|
+
throw new Error(`browser.provider must be local, browserstack, or testmu; got "${String(raw)}".`)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const REMOTE_BROWSER_DEFAULTS: Record<RemoteBrowserProvider, Pick<BrowserRemoteConfig, "usernameEnv" | "accessKeyEnv" | "endpoint">> = {
|
|
435
|
+
browserstack: {
|
|
436
|
+
usernameEnv: "BROWSERSTACK_USERNAME",
|
|
437
|
+
accessKeyEnv: "BROWSERSTACK_ACCESS_KEY",
|
|
438
|
+
endpoint: "wss://cdp.browserstack.com/playwright",
|
|
439
|
+
},
|
|
440
|
+
testmu: {
|
|
441
|
+
usernameEnv: "LT_USERNAME",
|
|
442
|
+
accessKeyEnv: "LT_ACCESS_KEY",
|
|
443
|
+
endpoint: "wss://cdp.lambdatest.com/playwright",
|
|
444
|
+
},
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function objectMap(raw: unknown, where: string): Record<string, unknown> | undefined {
|
|
448
|
+
if (raw === undefined || raw === null) return undefined
|
|
449
|
+
if (typeof raw !== "object" || Array.isArray(raw)) throw new Error(`${where} must be a map of names to values.`)
|
|
450
|
+
return { ...(raw as Record<string, unknown>) }
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function browserRemoteConfig(raw: Record<string, unknown>, provider: BrowserProvider): BrowserRemoteConfig | undefined {
|
|
454
|
+
if (provider === "local") return undefined
|
|
455
|
+
const remote = asRecord(raw.remote)
|
|
456
|
+
const defaults = REMOTE_BROWSER_DEFAULTS[provider]
|
|
457
|
+
const browser = String(remote.browser || "").trim()
|
|
458
|
+
if (!browser) throw new Error(`browser.remote.browser is required when browser.provider is ${provider}.`)
|
|
459
|
+
const browserVersion = String(remote.browser_version || remote.browserVersion || "latest").trim() || "latest"
|
|
460
|
+
const os = String(remote.os || "").trim() || undefined
|
|
461
|
+
const osVersion = String(remote.os_version || remote.osVersion || "").trim() || undefined
|
|
462
|
+
const platform = String(remote.platform || "").trim() || undefined
|
|
463
|
+
if (provider === "browserstack" && (!os || !osVersion)) {
|
|
464
|
+
throw new Error("browser.remote.os and browser.remote.os_version are required when browser.provider is browserstack.")
|
|
465
|
+
}
|
|
466
|
+
if (provider === "testmu" && !platform && !os) {
|
|
467
|
+
throw new Error("browser.remote.platform is required when browser.provider is testmu, or set browser.remote.os (and optionally os_version).")
|
|
468
|
+
}
|
|
469
|
+
const extra = objectMap(remote.extra ?? remote.capabilities, "browser.remote.extra")
|
|
470
|
+
return {
|
|
471
|
+
provider,
|
|
472
|
+
usernameEnv: String(remote.username_env || remote.usernameEnv || defaults.usernameEnv),
|
|
473
|
+
accessKeyEnv: String(remote.access_key_env || remote.accessKeyEnv || defaults.accessKeyEnv),
|
|
474
|
+
endpoint: String(remote.endpoint || defaults.endpoint).trim().replace(/\/+$/, ""),
|
|
475
|
+
browser,
|
|
476
|
+
browserVersion,
|
|
477
|
+
...(os ? { os } : {}),
|
|
478
|
+
...(osVersion ? { osVersion } : {}),
|
|
479
|
+
...(platform ? { platform } : {}),
|
|
480
|
+
...(remote.project ? { project: String(remote.project).trim() } : {}),
|
|
481
|
+
...(remote.build ? { build: String(remote.build).trim() } : {}),
|
|
482
|
+
...(remote.name ? { name: String(remote.name).trim() } : {}),
|
|
483
|
+
local: remote.local === true || remote.local_testing === true,
|
|
484
|
+
...(extra && Object.keys(extra).length ? { extra } : {}),
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
374
488
|
function profileFromRaw(name: string, raw: Record<string, unknown>): LlmProfile {
|
|
375
489
|
const provider = String(raw.provider || "openai") as LlmProfile["provider"]
|
|
376
490
|
return {
|
|
@@ -549,6 +663,7 @@ export function loadConfig(opts?: { configPath?: string; profile?: string }): Ap
|
|
|
549
663
|
profiles,
|
|
550
664
|
llm: profiles[activeProfile] || { ...NO_PROFILE },
|
|
551
665
|
browser: {
|
|
666
|
+
provider: browserProvider(browser.provider),
|
|
552
667
|
headless: browser.headless !== false,
|
|
553
668
|
width: Number(viewport.width || 1280),
|
|
554
669
|
height: Number(viewport.height || 800),
|
|
@@ -557,6 +672,7 @@ export function loadConfig(opts?: { configPath?: string; profile?: string }): Ap
|
|
|
557
672
|
visionDetail: visionDetail(browser.vision_detail),
|
|
558
673
|
allowedHosts: hosts,
|
|
559
674
|
startUrl: browser.start_url ? String(browser.start_url) : undefined,
|
|
675
|
+
remote: browserRemoteConfig(browser, browserProvider(browser.provider)),
|
|
560
676
|
overlay: browser.overlay !== false,
|
|
561
677
|
judge: browser.judge === true,
|
|
562
678
|
},
|
|
@@ -600,6 +716,7 @@ export function loadConfig(opts?: { configPath?: string; profile?: string }): Ap
|
|
|
600
716
|
flaky: flakyConfig(asRecord(yaml.flaky)),
|
|
601
717
|
release: releaseThresholds(yaml.release),
|
|
602
718
|
api: apiConfig(asRecord(yaml.api)),
|
|
719
|
+
bi: biConfig(asRecord(yaml.bi), base, outDir, configPath),
|
|
603
720
|
quality: withBaselineDir(qualityConfig(yaml.quality), base, configPath),
|
|
604
721
|
domain: domainConfig(yaml.domain),
|
|
605
722
|
notify: notifyConfig(yaml.notify),
|
|
@@ -836,6 +953,38 @@ function apiConfig(a: Record<string, unknown>): ApiConfig {
|
|
|
836
953
|
}
|
|
837
954
|
}
|
|
838
955
|
|
|
956
|
+
function biConfig(b: Record<string, unknown>, base: string, outDir: string, configPath: string | undefined): BiConfig {
|
|
957
|
+
const evidenceDir = path.resolve(base, String(b.evidence_dir || path.join(".", "evidence")))
|
|
958
|
+
const defaultDataDir = path.join(evidenceDir, "sources", "forgeqe", "data")
|
|
959
|
+
const title = String(b.title || "ForgeQE BI").trim() || "ForgeQE BI"
|
|
960
|
+
const datasets = Object.entries(asRecord(b.datasets)).map(([id, value]) => biDataset(id, asRecord(value)))
|
|
961
|
+
return {
|
|
962
|
+
title,
|
|
963
|
+
evidenceDir,
|
|
964
|
+
dataDir: path.resolve(base, String(b.data_dir || defaultDataDir)),
|
|
965
|
+
buildDir: path.resolve(base, String(b.build_dir || path.join(outDir, "bi"))),
|
|
966
|
+
datasets,
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function biDataset(id: string, raw: Record<string, unknown>): BiDatasetConfig {
|
|
971
|
+
if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new Error(`bi.datasets.${id}: ids may only use letters, digits, - and _.`)
|
|
972
|
+
const title = String(raw.title || id.replace(/[-_]+/g, " ").trim()).trim()
|
|
973
|
+
const kind = String(raw.kind || "").trim().toLowerCase()
|
|
974
|
+
if (kind === "ado_wiql") {
|
|
975
|
+
const wiql = String(raw.wiql || "").trim()
|
|
976
|
+
if (!wiql) throw new Error(`bi.datasets.${id}.wiql is required for kind ado_wiql.`)
|
|
977
|
+
const project = String(raw.project || "").trim()
|
|
978
|
+
return { id, title: title || id, kind, wiql, ...(project ? { project } : {}) }
|
|
979
|
+
}
|
|
980
|
+
if (kind === "jira_jql") {
|
|
981
|
+
const jql = String(raw.jql || "").trim()
|
|
982
|
+
if (!jql) throw new Error(`bi.datasets.${id}.jql is required for kind jira_jql.`)
|
|
983
|
+
return { id, title: title || id, kind, jql }
|
|
984
|
+
}
|
|
985
|
+
throw new Error(`bi.datasets.${id}.kind must be ado_wiql or jira_jql; got "${String(raw.kind || "")}".`)
|
|
986
|
+
}
|
|
987
|
+
|
|
839
988
|
function optional<K extends string, V>(key: K, value: V | undefined): Partial<Record<K, V>> {
|
|
840
989
|
return value === undefined ? {} : ({ [key]: value } as Record<K, V>)
|
|
841
990
|
}
|
package/src/engine/bridge.ts
CHANGED
|
@@ -3,9 +3,9 @@ import { createInterface } from "node:readline"
|
|
|
3
3
|
|
|
4
4
|
import type { LlmProfile } from "../llm.ts"
|
|
5
5
|
import type { AgentEvent, CaseReport, MemoryInfo, ReplayMode, RunResult } from "../types.ts"
|
|
6
|
-
import { loginEnvNames } from "../childEnv.ts"
|
|
6
|
+
import { browserProviderEnvNames, loginEnvNames } from "../childEnv.ts"
|
|
7
7
|
import { recordUsage } from "../usage.ts"
|
|
8
|
-
import type { LoginConfig } from "../config.ts"
|
|
8
|
+
import type { BrowserProvider, BrowserRemoteConfig, LoginConfig } from "../config.ts"
|
|
9
9
|
import { debugLogging, logEvent } from "../sessionLog.ts"
|
|
10
10
|
import { engineEnv } from "./setup.ts"
|
|
11
11
|
import { AZURE_IDENTITY_ENV, entraPayload } from "../entra.ts"
|
|
@@ -18,6 +18,7 @@ export type EngineRequest = {
|
|
|
18
18
|
headed: boolean
|
|
19
19
|
maxSteps: number
|
|
20
20
|
allowedHosts: string[]
|
|
21
|
+
browser?: { provider: BrowserProvider; remote?: BrowserRemoteConfig }
|
|
21
22
|
extendSystemMessage?: string
|
|
22
23
|
/** Page memory folder and replay mode; omit to run without memory. */
|
|
23
24
|
memory?: { dir: string; replay: ReplayMode }
|
|
@@ -148,6 +149,33 @@ function llmPayload(p: LlmProfile): Record<string, unknown> {
|
|
|
148
149
|
return out
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
function browserPayload(browser: EngineRequest["browser"]): Record<string, unknown> | null {
|
|
153
|
+
if (!browser) return null
|
|
154
|
+
return {
|
|
155
|
+
provider: browser.provider,
|
|
156
|
+
...(browser.remote
|
|
157
|
+
? {
|
|
158
|
+
remote: {
|
|
159
|
+
provider: browser.remote.provider,
|
|
160
|
+
username_env: browser.remote.usernameEnv,
|
|
161
|
+
access_key_env: browser.remote.accessKeyEnv,
|
|
162
|
+
endpoint: browser.remote.endpoint,
|
|
163
|
+
browser: browser.remote.browser,
|
|
164
|
+
browser_version: browser.remote.browserVersion,
|
|
165
|
+
...(browser.remote.os ? { os: browser.remote.os } : {}),
|
|
166
|
+
...(browser.remote.osVersion ? { os_version: browser.remote.osVersion } : {}),
|
|
167
|
+
...(browser.remote.platform ? { platform: browser.remote.platform } : {}),
|
|
168
|
+
...(browser.remote.project ? { project: browser.remote.project } : {}),
|
|
169
|
+
...(browser.remote.build ? { build: browser.remote.build } : {}),
|
|
170
|
+
...(browser.remote.name ? { name: browser.remote.name } : {}),
|
|
171
|
+
local: browser.remote.local,
|
|
172
|
+
...(browser.remote.extra ? { extra: browser.remote.extra } : {}),
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
: {}),
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
151
179
|
export function toRunResult(goal: string, startUrl: string, r: EngineResult): RunResult {
|
|
152
180
|
// The agent's evidence wins over its verdict: a check that does not hold, or a failed case step, fails the run even
|
|
153
181
|
// when the agent called done with success (engines before this rule did not apply it themselves).
|
|
@@ -198,6 +226,7 @@ export async function runEngine(opts: {
|
|
|
198
226
|
const child = spawn(bin!, args, {
|
|
199
227
|
env: engineEnv(debugLogging() ? { ...req.env, BROWSER_USE_LOGGING_LEVEL: "debug" } : req.env, [
|
|
200
228
|
...(req.llm.auth === "entra" ? AZURE_IDENTITY_ENV : req.llm.apiKeyEnv ? [req.llm.apiKeyEnv] : []),
|
|
229
|
+
...browserProviderEnvNames(req.browser || { provider: "local" }),
|
|
201
230
|
...loginEnvNames(req.logins || []),
|
|
202
231
|
]),
|
|
203
232
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -222,6 +251,7 @@ export async function runEngine(opts: {
|
|
|
222
251
|
headed: req.headed,
|
|
223
252
|
max_steps: req.maxSteps,
|
|
224
253
|
allowed_hosts: req.allowedHosts,
|
|
254
|
+
browser: browserPayload(req.browser),
|
|
225
255
|
extend_system_message: req.extendSystemMessage || null,
|
|
226
256
|
memory: req.memory || null,
|
|
227
257
|
logins: loginPayload(req.logins),
|