@capagents/forgeqe 1.1.1 → 1.1.3
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 +25 -9
- package/README.md +30 -1
- package/browserstack.example.yaml +14 -0
- package/docs/security-data-flow.html +154 -86
- package/docs/user-guide.html +329 -135
- 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 +51 -0
- package/package.json +5 -2
- 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 -27
- package/py/forgeqe_engine/skills.py +31 -7
- package/src/auth/signin.ts +30 -1
- package/src/bi.ts +914 -0
- package/src/cache.ts +92 -0
- package/src/capabilities/guide.ts +322 -140
- package/src/capabilities/registry.ts +3 -3
- package/src/childEnv.ts +4 -0
- package/src/cli/help.ts +18 -4
- package/src/cli/index.ts +149 -15
- package/src/config.ts +272 -0
- package/src/engine/bridge.ts +32 -2
- package/src/evals/prompts.ts +288 -0
- package/src/heal/loop.ts +3 -1
- package/src/init.ts +18 -0
- package/src/mcp/server.ts +8 -0
- package/src/mcp/tools.ts +30 -4
- package/src/monitor/run.ts +3 -1
- package/src/prompts.ts +6 -3
- package/src/promptstyle.ts +159 -0
- package/src/run.ts +4 -1
- package/src/ste.ts +8 -2
- package/src/suite/run.ts +3 -1
- package/testmu.example.yaml +14 -0
package/src/config.ts
CHANGED
|
@@ -16,7 +16,30 @@ 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
|
+
export type BrowserRemoteConfigs = Partial<Record<RemoteBrowserProvider, BrowserRemoteConfig>>
|
|
22
|
+
export type BrowserProviderFiles = Partial<Record<RemoteBrowserProvider, string>>
|
|
23
|
+
|
|
24
|
+
export type BrowserRemoteConfig = {
|
|
25
|
+
provider: RemoteBrowserProvider
|
|
26
|
+
usernameEnv: string
|
|
27
|
+
accessKeyEnv: string
|
|
28
|
+
endpoint: string
|
|
29
|
+
browser: string
|
|
30
|
+
browserVersion: string
|
|
31
|
+
os?: string
|
|
32
|
+
osVersion?: string
|
|
33
|
+
platform?: string
|
|
34
|
+
project?: string
|
|
35
|
+
build?: string
|
|
36
|
+
name?: string
|
|
37
|
+
local: boolean
|
|
38
|
+
extra?: Record<string, unknown>
|
|
39
|
+
}
|
|
40
|
+
|
|
19
41
|
export type BrowserConfig = {
|
|
42
|
+
provider: BrowserProvider
|
|
20
43
|
headless: boolean
|
|
21
44
|
width: number
|
|
22
45
|
height: number
|
|
@@ -25,6 +48,9 @@ export type BrowserConfig = {
|
|
|
25
48
|
visionDetail: "low" | "high"
|
|
26
49
|
allowedHosts: string[]
|
|
27
50
|
startUrl?: string
|
|
51
|
+
remote?: BrowserRemoteConfig
|
|
52
|
+
remotes: BrowserRemoteConfigs
|
|
53
|
+
providerFiles: BrowserProviderFiles
|
|
28
54
|
/** ForgeQE overlay in a visible browser: edge glow, a bar with the current step, the person's input locked. */
|
|
29
55
|
overlay: boolean
|
|
30
56
|
/** A second model call checks the finished run against the goal, and its verdict can fail a run the agent completed. */
|
|
@@ -37,6 +63,20 @@ export type MemoryConfig = {
|
|
|
37
63
|
replay: ReplayMode
|
|
38
64
|
}
|
|
39
65
|
|
|
66
|
+
export type CacheConfig = {
|
|
67
|
+
enabled: boolean
|
|
68
|
+
/** Redis connection string, e.g. redis://127.0.0.1:6379. */
|
|
69
|
+
redisUrl: string
|
|
70
|
+
/** Prefix for every Redis key this ForgeQE workspace writes. */
|
|
71
|
+
keyPrefix: string
|
|
72
|
+
/** Default TTL for cached JSON payloads, in seconds. */
|
|
73
|
+
defaultTtlSeconds: number
|
|
74
|
+
/** TTL for BI external dataset exports, in seconds. */
|
|
75
|
+
biTtlSeconds: number
|
|
76
|
+
/** Socket connect timeout for Redis, in milliseconds. */
|
|
77
|
+
connectTimeoutMs: number
|
|
78
|
+
}
|
|
79
|
+
|
|
40
80
|
function runtimeBin(value: unknown, base: string): string | undefined {
|
|
41
81
|
if (!value) return undefined
|
|
42
82
|
const s = String(value)
|
|
@@ -176,6 +216,34 @@ export type ApiConfig = {
|
|
|
176
216
|
timeoutSeconds: number
|
|
177
217
|
}
|
|
178
218
|
|
|
219
|
+
export type BiDatasetConfig =
|
|
220
|
+
| {
|
|
221
|
+
id: string
|
|
222
|
+
title: string
|
|
223
|
+
kind: "ado_wiql"
|
|
224
|
+
wiql: string
|
|
225
|
+
project?: string
|
|
226
|
+
}
|
|
227
|
+
| {
|
|
228
|
+
id: string
|
|
229
|
+
title: string
|
|
230
|
+
kind: "jira_jql"
|
|
231
|
+
jql: string
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export type BiConfig = {
|
|
235
|
+
/** Title of the generated Evidence project and its landing page. */
|
|
236
|
+
title: string
|
|
237
|
+
/** Evidence project root, generated and managed by ForgeQE. */
|
|
238
|
+
evidenceDir: string
|
|
239
|
+
/** Exported CSV datasets the Evidence project reads. */
|
|
240
|
+
dataDir: string
|
|
241
|
+
/** Static HTML output from `forgeqe bi build`. */
|
|
242
|
+
buildDir: string
|
|
243
|
+
/** Declarative external datasets to fetch on each sync/build. */
|
|
244
|
+
datasets: BiDatasetConfig[]
|
|
245
|
+
}
|
|
246
|
+
|
|
179
247
|
export type SuiteConfig = {
|
|
180
248
|
/** Browser runs at the same time. Coding runs one at a time. */
|
|
181
249
|
parallel: number
|
|
@@ -280,6 +348,8 @@ export type AppConfig = {
|
|
|
280
348
|
configPath?: string
|
|
281
349
|
/** `mcp:` in forgeqe.yaml: more MCP servers for ForgeQE's agent, by name. */
|
|
282
350
|
mcp?: Record<string, ExtraMcpServer>
|
|
351
|
+
/** Optional Redis-backed cache for deterministic external data fetches. */
|
|
352
|
+
cache?: CacheConfig
|
|
283
353
|
activeProfile: string
|
|
284
354
|
profiles: Record<string, LlmProfile>
|
|
285
355
|
llm: LlmProfile
|
|
@@ -312,6 +382,8 @@ export type AppConfig = {
|
|
|
312
382
|
release?: ReleaseThresholds
|
|
313
383
|
/** API and contract tests (api: in forgeqe.yaml): see src/api/. */
|
|
314
384
|
api?: ApiConfig
|
|
385
|
+
/** Offline BI export and the Evidence project that renders it (bi: in forgeqe.yaml). */
|
|
386
|
+
bi?: BiConfig
|
|
315
387
|
/** Accessibility standard and accessibility/performance budgets (quality: in forgeqe.yaml): see src/quality/. */
|
|
316
388
|
quality?: QualityConfig
|
|
317
389
|
/** The product's industry and the regulations in scope (domain: in forgeqe.yaml): see src/domain.ts. */
|
|
@@ -338,6 +410,22 @@ function domainConfig(raw: unknown): DomainConfig | undefined {
|
|
|
338
410
|
return { industry, regulations, ...(d.notes ? { notes: String(d.notes).trim() } : {}) }
|
|
339
411
|
}
|
|
340
412
|
|
|
413
|
+
function cacheConfig(raw: unknown): CacheConfig | undefined {
|
|
414
|
+
const c = asRecord(raw)
|
|
415
|
+
const asked = raw !== undefined || Boolean(process.env.FORGEQE_REDIS_URL)
|
|
416
|
+
if (!asked || c.enabled === false) return undefined
|
|
417
|
+
const redisUrl = String(c.redis_url || c.url || process.env.FORGEQE_REDIS_URL || "").trim()
|
|
418
|
+
if (!redisUrl) throw new Error('cache.redis_url is required when cache is enabled. Example: cache: { redis_url: "redis://127.0.0.1:6379" }')
|
|
419
|
+
return {
|
|
420
|
+
enabled: true,
|
|
421
|
+
redisUrl,
|
|
422
|
+
keyPrefix: String(c.key_prefix || "forgeqe").trim() || "forgeqe",
|
|
423
|
+
defaultTtlSeconds: positiveInt(c.default_ttl_seconds, 3600, "cache.default_ttl_seconds"),
|
|
424
|
+
biTtlSeconds: positiveInt(c.bi_ttl_seconds, 900, "cache.bi_ttl_seconds"),
|
|
425
|
+
connectTimeoutMs: positiveInt(c.connect_timeout_ms, 3000, "cache.connect_timeout_ms"),
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
341
429
|
export function replayMode(raw: unknown): ReplayMode {
|
|
342
430
|
const mode = String(raw || "verify").toLowerCase()
|
|
343
431
|
if (mode === "trust" || mode === "off") return mode
|
|
@@ -371,6 +459,150 @@ function visionDetail(raw: unknown): BrowserConfig["visionDetail"] {
|
|
|
371
459
|
return String(raw || "low").toLowerCase() === "high" ? "high" : "low"
|
|
372
460
|
}
|
|
373
461
|
|
|
462
|
+
export function browserProvider(raw: unknown): BrowserProvider {
|
|
463
|
+
const provider = String(raw || "local").trim().toLowerCase().replace(/[\s-]/g, "_")
|
|
464
|
+
if (!provider || provider === "local") return "local"
|
|
465
|
+
if (provider === "browserstack") return "browserstack"
|
|
466
|
+
if (provider === "testmu" || provider === "lambdatest" || provider === "lambda_test") return "testmu"
|
|
467
|
+
throw new Error(`browser.provider must be local, browserstack, or testmu; got "${String(raw)}".`)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const REMOTE_BROWSER_DEFAULTS: Record<RemoteBrowserProvider, Pick<BrowserRemoteConfig, "usernameEnv" | "accessKeyEnv" | "endpoint">> = {
|
|
471
|
+
browserstack: {
|
|
472
|
+
usernameEnv: "BROWSERSTACK_USERNAME",
|
|
473
|
+
accessKeyEnv: "BROWSERSTACK_ACCESS_KEY",
|
|
474
|
+
endpoint: "wss://cdp.browserstack.com/playwright",
|
|
475
|
+
},
|
|
476
|
+
testmu: {
|
|
477
|
+
usernameEnv: "LT_USERNAME",
|
|
478
|
+
accessKeyEnv: "LT_ACCESS_KEY",
|
|
479
|
+
endpoint: "wss://cdp.lambdatest.com/playwright",
|
|
480
|
+
},
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const REMOTE_BROWSER_PROVIDERS = Object.keys(REMOTE_BROWSER_DEFAULTS) as RemoteBrowserProvider[]
|
|
484
|
+
const REMOTE_BROWSER_KEYS = new Set([
|
|
485
|
+
"browser",
|
|
486
|
+
"browser_version",
|
|
487
|
+
"browserVersion",
|
|
488
|
+
"os",
|
|
489
|
+
"os_version",
|
|
490
|
+
"osVersion",
|
|
491
|
+
"platform",
|
|
492
|
+
"username_env",
|
|
493
|
+
"usernameEnv",
|
|
494
|
+
"access_key_env",
|
|
495
|
+
"accessKeyEnv",
|
|
496
|
+
"endpoint",
|
|
497
|
+
"project",
|
|
498
|
+
"build",
|
|
499
|
+
"name",
|
|
500
|
+
"local",
|
|
501
|
+
"local_testing",
|
|
502
|
+
"extra",
|
|
503
|
+
"capabilities",
|
|
504
|
+
])
|
|
505
|
+
|
|
506
|
+
function objectMap(raw: unknown, where: string): Record<string, unknown> | undefined {
|
|
507
|
+
if (raw === undefined || raw === null) return undefined
|
|
508
|
+
if (typeof raw !== "object" || Array.isArray(raw)) throw new Error(`${where} must be a map of names to values.`)
|
|
509
|
+
return { ...(raw as Record<string, unknown>) }
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function browserProviderFiles(raw: Record<string, unknown>, base: string): BrowserProviderFiles {
|
|
513
|
+
const files = objectMap(raw.providers, "browser.providers")
|
|
514
|
+
const out: BrowserProviderFiles = {}
|
|
515
|
+
for (const provider of REMOTE_BROWSER_PROVIDERS) {
|
|
516
|
+
const file = files?.[provider]
|
|
517
|
+
if (file === undefined || file === null || file === false || String(file).trim() === "") continue
|
|
518
|
+
out[provider] = path.resolve(base, String(file))
|
|
519
|
+
}
|
|
520
|
+
return out
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function browserProviderFileSection(file: string, provider: RemoteBrowserProvider): Record<string, unknown> {
|
|
524
|
+
if (!existsSync(file)) throw new Error(`browser.providers.${provider} not found: ${file}`)
|
|
525
|
+
const doc = readYaml(file)
|
|
526
|
+
const section = Object.keys(asRecord(doc.browser)).length ? { ...asRecord(doc.browser) } : { ...doc }
|
|
527
|
+
if (!section.provider && doc.provider !== undefined) section.provider = doc.provider
|
|
528
|
+
if (!Object.keys(asRecord(section.remote)).length) {
|
|
529
|
+
const rootRemote = asRecord(doc.remote)
|
|
530
|
+
if (Object.keys(rootRemote).length) section.remote = rootRemote
|
|
531
|
+
else {
|
|
532
|
+
const directRemote = Object.fromEntries(Object.entries(section).filter(([k]) => REMOTE_BROWSER_KEYS.has(k)))
|
|
533
|
+
if (Object.keys(directRemote).length) section.remote = directRemote
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (section.provider !== undefined && browserProvider(section.provider) !== provider) {
|
|
537
|
+
throw new Error(`browser.providers.${provider} points to ${file}, but that file declares provider ${String(section.provider)}.`)
|
|
538
|
+
}
|
|
539
|
+
return section
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function browserRemoteConfig(raw: Record<string, unknown>, provider: BrowserProvider, where = "browser"): BrowserRemoteConfig | undefined {
|
|
543
|
+
if (provider === "local") return undefined
|
|
544
|
+
const remote = asRecord(raw.remote)
|
|
545
|
+
const defaults = REMOTE_BROWSER_DEFAULTS[provider]
|
|
546
|
+
const browser = String(remote.browser || "").trim()
|
|
547
|
+
if (!browser) throw new Error(`${where}.remote.browser is required when browser.provider is ${provider}.`)
|
|
548
|
+
const browserVersion = String(remote.browser_version || remote.browserVersion || "latest").trim() || "latest"
|
|
549
|
+
const os = String(remote.os || "").trim() || undefined
|
|
550
|
+
const osVersion = String(remote.os_version || remote.osVersion || "").trim() || undefined
|
|
551
|
+
const platform = String(remote.platform || "").trim() || undefined
|
|
552
|
+
if (provider === "browserstack" && (!os || !osVersion)) {
|
|
553
|
+
throw new Error(`${where}.remote.os and ${where}.remote.os_version are required when browser.provider is browserstack.`)
|
|
554
|
+
}
|
|
555
|
+
if (provider === "testmu" && !platform && !os) {
|
|
556
|
+
throw new Error(`${where}.remote.platform is required when browser.provider is testmu, or set ${where}.remote.os (and optionally os_version).`)
|
|
557
|
+
}
|
|
558
|
+
const extra = objectMap(remote.extra ?? remote.capabilities, `${where}.remote.extra`)
|
|
559
|
+
return {
|
|
560
|
+
provider,
|
|
561
|
+
usernameEnv: String(remote.username_env || remote.usernameEnv || defaults.usernameEnv),
|
|
562
|
+
accessKeyEnv: String(remote.access_key_env || remote.accessKeyEnv || defaults.accessKeyEnv),
|
|
563
|
+
endpoint: String(remote.endpoint || defaults.endpoint).trim().replace(/\/+$/, ""),
|
|
564
|
+
browser,
|
|
565
|
+
browserVersion,
|
|
566
|
+
...(os ? { os } : {}),
|
|
567
|
+
...(osVersion ? { osVersion } : {}),
|
|
568
|
+
...(platform ? { platform } : {}),
|
|
569
|
+
...(remote.project ? { project: String(remote.project).trim() } : {}),
|
|
570
|
+
...(remote.build ? { build: String(remote.build).trim() } : {}),
|
|
571
|
+
...(remote.name ? { name: String(remote.name).trim() } : {}),
|
|
572
|
+
local: remote.local === true || remote.local_testing === true,
|
|
573
|
+
...(extra && Object.keys(extra).length ? { extra } : {}),
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function browserRemoteConfigs(browser: Record<string, unknown>, provider: BrowserProvider, base: string): { remotes: BrowserRemoteConfigs; providerFiles: BrowserProviderFiles } {
|
|
578
|
+
const providerFiles = browserProviderFiles(browser, base)
|
|
579
|
+
const remotes: BrowserRemoteConfigs = {}
|
|
580
|
+
for (const remoteProvider of REMOTE_BROWSER_PROVIDERS) {
|
|
581
|
+
const fromFile = providerFiles[remoteProvider] ? browserProviderFileSection(providerFiles[remoteProvider]!, remoteProvider) : undefined
|
|
582
|
+
const inline = provider === remoteProvider ? browser : undefined
|
|
583
|
+
if (!fromFile && !inline) continue
|
|
584
|
+
const where = fromFile ? `browser.providers.${remoteProvider}` : "browser"
|
|
585
|
+
remotes[remoteProvider] = browserRemoteConfig(
|
|
586
|
+
{
|
|
587
|
+
...(fromFile || {}),
|
|
588
|
+
...(inline || {}),
|
|
589
|
+
remote: { ...asRecord(fromFile?.remote), ...asRecord(inline?.remote) },
|
|
590
|
+
},
|
|
591
|
+
remoteProvider,
|
|
592
|
+
where,
|
|
593
|
+
)
|
|
594
|
+
}
|
|
595
|
+
return { remotes, providerFiles }
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export function browserConfigFor(config: Pick<AppConfig, "browser">, provider = config.browser.provider): Pick<BrowserConfig, "provider" | "remote"> {
|
|
599
|
+
const chosen = provider || config.browser.provider || "local"
|
|
600
|
+
if (chosen === "local") return { provider: chosen }
|
|
601
|
+
const remote = config.browser.remotes?.[chosen] || ((config.browser.provider === chosen || !config.browser.provider) ? config.browser.remote : undefined)
|
|
602
|
+
if (remote) return { provider: chosen, remote }
|
|
603
|
+
throw new Error(`Browser provider ${chosen} is not configured. Add browser.providers.${chosen} to forgeqe.yaml, or set browser.provider: ${chosen} with browser.remote.`)
|
|
604
|
+
}
|
|
605
|
+
|
|
374
606
|
function profileFromRaw(name: string, raw: Record<string, unknown>): LlmProfile {
|
|
375
607
|
const provider = String(raw.provider || "openai") as LlmProfile["provider"]
|
|
376
608
|
return {
|
|
@@ -542,13 +774,17 @@ export function loadConfig(opts?: { configPath?: string; profile?: string }): Ap
|
|
|
542
774
|
: []
|
|
543
775
|
|
|
544
776
|
const sessionsDir = configPath ? path.join(projectDir(base), "sessions") : path.join(forgeqeHome(), "sessions")
|
|
777
|
+
const provider = browserProvider(browser.provider)
|
|
778
|
+
const { remotes, providerFiles } = browserRemoteConfigs(browser, provider, base)
|
|
545
779
|
const config: AppConfig = {
|
|
546
780
|
configPath,
|
|
547
781
|
mcp: extraMcpServers(yaml.mcp, base),
|
|
782
|
+
cache: cacheConfig(yaml.cache),
|
|
548
783
|
activeProfile,
|
|
549
784
|
profiles,
|
|
550
785
|
llm: profiles[activeProfile] || { ...NO_PROFILE },
|
|
551
786
|
browser: {
|
|
787
|
+
provider,
|
|
552
788
|
headless: browser.headless !== false,
|
|
553
789
|
width: Number(viewport.width || 1280),
|
|
554
790
|
height: Number(viewport.height || 800),
|
|
@@ -557,6 +793,9 @@ export function loadConfig(opts?: { configPath?: string; profile?: string }): Ap
|
|
|
557
793
|
visionDetail: visionDetail(browser.vision_detail),
|
|
558
794
|
allowedHosts: hosts,
|
|
559
795
|
startUrl: browser.start_url ? String(browser.start_url) : undefined,
|
|
796
|
+
remote: provider === "local" ? undefined : remotes[provider],
|
|
797
|
+
remotes,
|
|
798
|
+
providerFiles,
|
|
560
799
|
overlay: browser.overlay !== false,
|
|
561
800
|
judge: browser.judge === true,
|
|
562
801
|
},
|
|
@@ -600,6 +839,7 @@ export function loadConfig(opts?: { configPath?: string; profile?: string }): Ap
|
|
|
600
839
|
flaky: flakyConfig(asRecord(yaml.flaky)),
|
|
601
840
|
release: releaseThresholds(yaml.release),
|
|
602
841
|
api: apiConfig(asRecord(yaml.api)),
|
|
842
|
+
bi: biConfig(asRecord(yaml.bi), base, outDir, configPath),
|
|
603
843
|
quality: withBaselineDir(qualityConfig(yaml.quality), base, configPath),
|
|
604
844
|
domain: domainConfig(yaml.domain),
|
|
605
845
|
notify: notifyConfig(yaml.notify),
|
|
@@ -836,6 +1076,38 @@ function apiConfig(a: Record<string, unknown>): ApiConfig {
|
|
|
836
1076
|
}
|
|
837
1077
|
}
|
|
838
1078
|
|
|
1079
|
+
function biConfig(b: Record<string, unknown>, base: string, outDir: string, configPath: string | undefined): BiConfig {
|
|
1080
|
+
const evidenceDir = path.resolve(base, String(b.evidence_dir || path.join(".", "evidence")))
|
|
1081
|
+
const defaultDataDir = path.join(evidenceDir, "sources", "forgeqe", "data")
|
|
1082
|
+
const title = String(b.title || "ForgeQE BI").trim() || "ForgeQE BI"
|
|
1083
|
+
const datasets = Object.entries(asRecord(b.datasets)).map(([id, value]) => biDataset(id, asRecord(value)))
|
|
1084
|
+
return {
|
|
1085
|
+
title,
|
|
1086
|
+
evidenceDir,
|
|
1087
|
+
dataDir: path.resolve(base, String(b.data_dir || defaultDataDir)),
|
|
1088
|
+
buildDir: path.resolve(base, String(b.build_dir || path.join(outDir, "bi"))),
|
|
1089
|
+
datasets,
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function biDataset(id: string, raw: Record<string, unknown>): BiDatasetConfig {
|
|
1094
|
+
if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new Error(`bi.datasets.${id}: ids may only use letters, digits, - and _.`)
|
|
1095
|
+
const title = String(raw.title || id.replace(/[-_]+/g, " ").trim()).trim()
|
|
1096
|
+
const kind = String(raw.kind || "").trim().toLowerCase()
|
|
1097
|
+
if (kind === "ado_wiql") {
|
|
1098
|
+
const wiql = String(raw.wiql || "").trim()
|
|
1099
|
+
if (!wiql) throw new Error(`bi.datasets.${id}.wiql is required for kind ado_wiql.`)
|
|
1100
|
+
const project = String(raw.project || "").trim()
|
|
1101
|
+
return { id, title: title || id, kind, wiql, ...(project ? { project } : {}) }
|
|
1102
|
+
}
|
|
1103
|
+
if (kind === "jira_jql") {
|
|
1104
|
+
const jql = String(raw.jql || "").trim()
|
|
1105
|
+
if (!jql) throw new Error(`bi.datasets.${id}.jql is required for kind jira_jql.`)
|
|
1106
|
+
return { id, title: title || id, kind, jql }
|
|
1107
|
+
}
|
|
1108
|
+
throw new Error(`bi.datasets.${id}.kind must be ado_wiql or jira_jql; got "${String(raw.kind || "")}".`)
|
|
1109
|
+
}
|
|
1110
|
+
|
|
839
1111
|
function optional<K extends string, V>(key: K, value: V | undefined): Partial<Record<K, V>> {
|
|
840
1112
|
return value === undefined ? {} : ({ [key]: value } as Record<K, V>)
|
|
841
1113
|
}
|
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),
|