@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.
Files changed (71) hide show
  1. package/HOW_TO_USE.md +25 -9
  2. package/README.md +30 -1
  3. package/browserstack.example.yaml +14 -0
  4. package/docs/security-data-flow.html +154 -86
  5. package/docs/user-guide.html +329 -135
  6. package/evals/prompts/brain/capability-question.yaml +12 -0
  7. package/evals/prompts/brain/failed-runs-cause.yaml +12 -0
  8. package/evals/prompts/brain/release-route.yaml +14 -0
  9. package/evals/prompts/brain/report-request.yaml +13 -0
  10. package/evals/prompts/brain/secret-request.yaml +9 -0
  11. package/evals/prompts/explore-charters/lockout.yaml +18 -0
  12. package/evals/prompts/explore-charters/transfer-limit.yaml +16 -0
  13. package/evals/prompts/refine/current-release.yaml +26 -0
  14. package/evals/prompts/refine/missing-item.yaml +10 -0
  15. package/evals/prompts/refine/question-stays-question.yaml +13 -0
  16. package/evals/prompts/refine/report-request.yaml +19 -0
  17. package/forgeqe.example.yaml +51 -0
  18. package/package.json +5 -2
  19. package/prompts/README.md +28 -7
  20. package/prompts/api-scenarios.md +56 -24
  21. package/prompts/api-tests.md +67 -51
  22. package/prompts/brain.md +277 -268
  23. package/prompts/browser-capture.md +8 -7
  24. package/prompts/browser-case-report.md +13 -12
  25. package/prompts/browser-checks.md +17 -18
  26. package/prompts/browser-efficiency.md +4 -1
  27. package/prompts/browser-error-recovery.md +5 -4
  28. package/prompts/browser-extra.md +1 -0
  29. package/prompts/browser-goal-verification.md +8 -7
  30. package/prompts/browser-skill-select.md +28 -1
  31. package/prompts/conventions.md +10 -0
  32. package/prompts/domain-rule.md +17 -14
  33. package/prompts/eval-judge.md +36 -13
  34. package/prompts/explore-charters.md +53 -22
  35. package/prompts/find-tests.md +45 -30
  36. package/prompts/heal.md +42 -32
  37. package/prompts/pr-review.md +44 -28
  38. package/prompts/refine.md +73 -40
  39. package/prompts/skill-rule.md +4 -1
  40. package/prompts/test-agent.md +61 -45
  41. package/prompts/test-reviewer.md +64 -37
  42. package/prompts/test-rules.md +59 -56
  43. package/py/forgeqe_engine/browser.py +111 -1
  44. package/py/forgeqe_engine/capture.py +8 -1
  45. package/py/forgeqe_engine/checks.py +20 -8
  46. package/py/forgeqe_engine/prompts.py +9 -0
  47. package/py/forgeqe_engine/report.py +15 -8
  48. package/py/forgeqe_engine/service.py +50 -27
  49. package/py/forgeqe_engine/skills.py +31 -7
  50. package/src/auth/signin.ts +30 -1
  51. package/src/bi.ts +914 -0
  52. package/src/cache.ts +92 -0
  53. package/src/capabilities/guide.ts +322 -140
  54. package/src/capabilities/registry.ts +3 -3
  55. package/src/childEnv.ts +4 -0
  56. package/src/cli/help.ts +18 -4
  57. package/src/cli/index.ts +149 -15
  58. package/src/config.ts +272 -0
  59. package/src/engine/bridge.ts +32 -2
  60. package/src/evals/prompts.ts +288 -0
  61. package/src/heal/loop.ts +3 -1
  62. package/src/init.ts +18 -0
  63. package/src/mcp/server.ts +8 -0
  64. package/src/mcp/tools.ts +30 -4
  65. package/src/monitor/run.ts +3 -1
  66. package/src/prompts.ts +6 -3
  67. package/src/promptstyle.ts +159 -0
  68. package/src/run.ts +4 -1
  69. package/src/ste.ts +8 -2
  70. package/src/suite/run.ts +3 -1
  71. package/testmu.example.yaml +14 -0
@@ -61,11 +61,11 @@ export const CAPABILITIES: Capability[] = [
61
61
  area: "Browser",
62
62
  title: "Run any goal in a real browser",
63
63
  summary:
64
- "Carries out a plain-language goal in Chrome one step at a time and verifies the outcome, with screenshots, a report, and the steps it took. Page memory replays steps that already worked on a site, so repeat runs are faster and cheaper.",
64
+ "Carries out a plain-language goal in a real browser one step at a time and verifies the outcome, with screenshots, a report, and the steps it took. The browser comes from forgeqe.yaml: local Chrome or Chromium by default, or BrowserStack or TestMu AI/LambdaTest when configured. Page memory replays steps that already worked on a site, so repeat runs are faster and cheaper.",
65
65
  tools: ["browser_run", "run_details", "page_memory"],
66
66
  examples: ["Go to booking.com and search hotels in Mumbai for next weekend", "What does ForgeQE know about the checkout page?"],
67
67
  requires: [{ kind: "live_model" }],
68
- limits: ["Web applications in Chrome or Chromium only: not native mobile or desktop applications."],
68
+ limits: ["Web applications only: local Chrome or Chromium, or configured remote web grids such as BrowserStack and TestMu AI/LambdaTest; not native mobile or desktop applications."],
69
69
  docs: [
70
70
  { doc: "HOW_TO_USE.md", heading: "4. Without the shell" },
71
71
  { doc: "HOW_TO_USE.md", heading: "5. Page memory" },
@@ -345,7 +345,7 @@ export const CAPABILITIES: Capability[] = [
345
345
  * Only add what is certain.
346
346
  */
347
347
  export const OUT_OF_SCOPE: { what: string; instead?: string }[] = [
348
- { what: "Native mobile apps (iOS, Android) and desktop applications", instead: "web applications in Chrome or Chromium" },
348
+ { what: "Native mobile apps (iOS, Android) and desktop applications", instead: "web applications in local Chrome or Chromium, or configured remote web grids such as BrowserStack and TestMu AI/LambdaTest" },
349
349
  { what: "Load, stress, or soak testing with many simultaneous users", instead: "per-page Core Web Vitals with budgets" },
350
350
  { what: "Penetration testing or dynamic security scanning", instead: "security findings in pull request reviews" },
351
351
  ]
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
@@ -17,23 +17,31 @@ Usage:
17
17
  [--repo DIR] [--no-code]
18
18
  forgeqe run "<request>" [--profile name] [--url URL] [--headed] [--plain] [--max-steps N]
19
19
  [--repo DIR] [--no-code] [--replay MODE] [--no-memory] [--attempts N] [--code-model provider/model]
20
+ [--browser-provider local|browserstack|testmu]
20
21
  ForgeQE's agent works out what the request needs: a browser goal,
21
22
  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 run --direct "<goal>" [--out DIR] [--skill NAME] [--browser-provider local|browserstack|testmu] ... one browser goal, no agent
23
24
  forgeqe suite <source...> [--parallel N] [--repo DIR] [--no-code] [--no-write-back] [--out DIR]
24
25
  [--profile name] [--url URL] [--headed] [--plain] [--attempts N] [--skill NAME]
26
+ [--browser-provider local|browserstack|testmu]
25
27
  run test cases the same way every time (what pipelines call)
26
28
  forgeqe cases <source...> [--goal] list the test cases a source gives (--goal: the goal each runs)
27
29
  forgeqe code [run-dir] [--repo DIR] [--attempts N] [--code-model provider/model]
28
30
  write a test for a run (default: the latest) with Scribe
29
31
  forgeqe heal ["<test command>"] [--repo DIR] [--attempts N] [--code-model provider/model] [--headed] [--plain]
32
+ [--browser-provider local|browserstack|testmu]
30
33
  run the repo's tests and heal the ones the app has outgrown
31
34
  forgeqe monitor [source...] [--every 15m] [--once] [--replay MODE] [--parallel N] [--out DIR] [--verbose] [--skill NAME]
35
+ [--browser-provider local|browserstack|testmu]
32
36
  check flows on a schedule and alert Teams or email when one breaks
33
37
  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]
38
+ forgeqe mcp [--repo DIR] [--profile name] [--url URL] [--headed] [--browser-provider local|browserstack|testmu]
35
39
  ForgeQE's tools as an MCP server on stdio, for other agents
36
40
  forgeqe mcp list the Azure DevOps, Jira and other MCP servers ForgeQE's agent gets
41
+ forgeqe bi [build] [--evidence-dir DIR] [--data-dir DIR] [--build-dir DIR]
42
+ export ForgeQE + ADO/Jira datasets into an Evidence project and build static BI HTML
43
+ forgeqe bi init scaffold or refresh the managed Evidence project files
44
+ forgeqe bi sync refresh the CSV datasets and generated Evidence pages, without building
37
45
  forgeqe pr-tests [pr] [--base BRANCH] [--agent] [--run] [--post]
38
46
  the tests a pull request (or the commits since BRANCH) can break; --run runs
39
47
  them and exits 1 when one fails (a CI gate), --post puts the results on the PR
@@ -48,6 +56,7 @@ Usage:
48
56
  forgeqe visual approve [run-dir] [--page a,b] [--yes]
49
57
  make the run's screenshots the new baselines (an intended change)
50
58
  forgeqe explore <story> [--url URL] [--charters N] [--roles a,b] [--focus TEXT] [--yes] [--file-bugs]
59
+ [--browser-provider local|browserstack|testmu]
51
60
  exploratory testing of a story (ado:123, SHOP-12, a link, or its text):
52
61
  charters beyond the scripted cases, defects confirmed by a second run,
53
62
  proposed test cases, bugs for confirmed defects; exits 1 on a defect
@@ -67,8 +76,8 @@ Usage:
67
76
  forgeqe api tests [SET] --scenarios[=a,b] --repo DIR
68
77
  scenario tests on the repository's existing API helpers
69
78
  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 xray in the browser, and save the token in the system's credential store
79
+ forgeqe auth login [system] [--no-browser] sign in to azure_devops, github, jira, confluence, testrail, zephyr, qtest,
80
+ xray, browserstack, or testmu in the browser, and save the token in the system's credential store
72
81
  forgeqe auth login <ENV_NAME> save one secret (an LLM API key, a webhook URL) in the credential store
73
82
  forgeqe auth [status] which systems are signed in, and how
74
83
  forgeqe auth logout <system|ENV_NAME> forget a saved sign-in
@@ -100,6 +109,7 @@ page with ranked locators and a confidence %, plus the steps of each goal's last
100
109
  --replay verify replay a known goal without the LLM, then one LLM step checks the result (default)
101
110
  --replay trust replay and finish without the LLM when every step works
102
111
  --replay off never replay; still record pages
112
+ --browser-provider pick which configured browser provider runs this command's browser work
103
113
 
104
114
  ForgeQE's agent (forgeqe start, forgeqe run) runs on the agent runtime with the active profile's model (or code.model) and
105
115
  calls ForgeQE's tools: browser runs, reading and running test cases, writing tests and running them, page memory, pushing
@@ -127,6 +137,10 @@ With sources.bugs, each failed case files a bug in Jira or Azure DevOps, or comm
127
137
  The report is report.html (step verdicts, timeline with screenshots, timings, environment) + junit.xml in
128
138
  out/suites/<time>/; every run folder also has its own report.html.
129
139
 
140
+ Static BI (bi: in forgeqe.yaml): \`forgeqe bi\` writes curated CSV datasets from ForgeQE's own suite and readiness
141
+ reports plus optional Azure DevOps WIQL and Jira JQL datasets, scaffolds an Evidence project, and runs
142
+ \`evidence build\` to produce static HTML in the build directory. No server stays up between runs.
143
+
130
144
  Healing (forgeqe heal): ForgeQE runs the tests (the command given, code.test_command, or one the agent finds).
131
145
  For each failing test, Scribe re-runs its flow in the real browser: when the flow still works, the test is out
132
146
  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
@@ -10,7 +10,7 @@ import { goalFor, loadCases } from "../cases/index.ts"
10
10
  import { codeSkipReason, latestRunDir, runCodeAgent } from "../code/index.ts"
11
11
  import { detailFrom } from "../detail.ts"
12
12
  import { ensureOpencode, INSTALL_HINT } from "../code/runtime.ts"
13
- import { loadConfig, parseDuration, replayMode, selectProfile, userConfigDir, type AppConfig } from "../config.ts"
13
+ import { browserProvider, loadConfig, parseDuration, replayMode, selectProfile, userConfigDir, type AppConfig, type BrowserProvider } from "../config.ts"
14
14
  import { runHeal } from "../heal/loop.ts"
15
15
  import { formatHeal } from "../heal/report.ts"
16
16
  import { backgroundMonitors, stopBackgroundMonitors } from "../monitor/background.ts"
@@ -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,13 +54,19 @@ 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"))
61
64
 
65
+ function browserProviderFlag(flags: Flags): BrowserProvider | undefined {
66
+ const value = strFlag(flags, "browser-provider")
67
+ return value ? browserProvider(value) : undefined
68
+ }
69
+
62
70
  function debugRequested(flags: Flags): boolean {
63
71
  return boolFlag(flags, "debug") || /^(1|true|yes|on)$/i.test(process.env.FORGEQE_DEBUG || "")
64
72
  }
@@ -184,8 +192,10 @@ export async function main(argv: string[]): Promise<number> {
184
192
  return await withUsageLine(() => cmdApi(positionals.slice(1), flags))
185
193
  case "explore":
186
194
  return await withUsageLine(() => cmdExplore(positionals.slice(1), flags))
195
+ case "bi":
196
+ return await cmdBi(positionals.slice(1), flags)
187
197
  case "prompts":
188
- return cmdPrompts(positionals.slice(1), flags)
198
+ return positionals[1] === "eval" ? await withUsageLine(() => cmdPromptEval(positionals.slice(2), flags)) : cmdPrompts(positionals.slice(1), flags)
189
199
  case "eval":
190
200
  return await withUsageLine(() => cmdEval(positionals.slice(1), flags))
191
201
  case "notify":
@@ -207,7 +217,7 @@ export async function main(argv: string[]): Promise<number> {
207
217
  const cfg = loadConfig(configOpts(flags))
208
218
  const trends = positionals[1] === "trends"
209
219
  const color = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR
210
- const ctx: ToolContext = { config: cfg, cwd: process.cwd(), session: "once", emit: toolPrinter(color, (line) => console.log(line), detailOf(flags)) }
220
+ const ctx: ToolContext = { config: cfg, cwd: process.cwd(), session: "once", emit: toolPrinter(color, (line) => console.log(line), detailOf(flags)) }
211
221
  const out = (await TOOLS.find((t) => t.name === (trends ? "quality_trends" : "quality_report"))!.run(trends ? { page: positionals[2] } : { run_dir: positionals[1] }, ctx)) as { budget_exceeded?: unknown[]; pages?: { slower: unknown[]; newly_failing_rules: unknown[] }[] }
212
222
  return out.budget_exceeded?.length || out.pages?.some((p) => p.slower.length || p.newly_failing_rules.length) ? 1 : 0
213
223
  }
@@ -372,7 +382,7 @@ async function cmdApi(args: string[], flags: Flags): Promise<number> {
372
382
  const cfg = loadConfig(configOpts(flags))
373
383
  const action = args[0] || "list"
374
384
  const color = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR
375
- const ctx: ToolContext = { config: cfg, cwd: process.cwd(), session: "once", emit: toolPrinter(color, (line) => console.log(line), detailOf(flags)) }
385
+ const ctx: ToolContext = { config: cfg, cwd: process.cwd(), session: "once", emit: toolPrinter(color, (line) => console.log(line), detailOf(flags)) }
376
386
  const tool = (name: string) => TOOLS.find((t) => t.name === name)!
377
387
  const csv = (name: string) => (strFlag(flags, name) || "").split(",").map((s) => s.trim()).filter(Boolean)
378
388
  switch (action) {
@@ -449,7 +459,17 @@ async function cmdExplore(args: string[], flags: Flags): Promise<number> {
449
459
  process.once("SIGINT", onSigint)
450
460
  try {
451
461
  const ask = process.stdin.isTTY && !boolFlag(flags, "yes") ? terminalAsk(controller.signal) : undefined
452
- const ctx: ToolContext = { config: cfg, cwd: process.cwd(), session: "once", signal: controller.signal, headed: boolFlag(flags, "headed"), maxSteps: maxStepsFlag(flags), emit: toolPrinter(color, (line) => console.log(line), detailOf(flags)), ...(ask ? { ask } : {}) }
462
+ const ctx: ToolContext = {
463
+ config: cfg,
464
+ cwd: process.cwd(),
465
+ session: "once",
466
+ signal: controller.signal,
467
+ headed: boolFlag(flags, "headed"),
468
+ browserProvider: browserProviderFlag(flags),
469
+ maxSteps: maxStepsFlag(flags),
470
+ emit: toolPrinter(color, (line) => console.log(line), detailOf(flags)),
471
+ ...(ask ? { ask } : {}),
472
+ }
453
473
  const roles = (strFlag(flags, "roles") || "").split(",").map((s) => s.trim()).filter(Boolean)
454
474
  const out = (await TOOLS.find((t) => t.name === "explore_story")!.run(
455
475
  { story, url: strFlag(flags, "url"), charters: Number(strFlag(flags, "charters")) || undefined, ...(roles.length ? { roles } : {}), focus: strFlag(flags, "focus"), file_bugs: boolFlag(flags, "file-bugs") },
@@ -461,6 +481,50 @@ async function cmdExplore(args: string[], flags: Flags): Promise<number> {
461
481
  }
462
482
  }
463
483
 
484
+ async function cmdBi(args: string[], flags: Flags): Promise<number> {
485
+ const cfg = loadConfig(configOpts(flags))
486
+ const action = args[0] || "build"
487
+ const overrides = {
488
+ evidenceDir: strFlag(flags, "evidence-dir") || strFlag(flags, "dir"),
489
+ dataDir: strFlag(flags, "data-dir"),
490
+ buildDir: strFlag(flags, "build-dir") || strFlag(flags, "out"),
491
+ }
492
+ switch (action) {
493
+ case "init": {
494
+ const out = await initBiProject(cfg, overrides)
495
+ console.log(`Evidence project ${out.evidenceDir}`)
496
+ for (const file of out.files) console.log(`generated ${path.relative(process.cwd(), file) || file}`)
497
+ return 0
498
+ }
499
+ case "sync": {
500
+ const out = await syncBiData(cfg, overrides)
501
+ console.log(`Evidence project ${out.evidenceDir}`)
502
+ console.log(`Data export ${out.dataDir}`)
503
+ console.log(`Build output ${out.buildDir}`)
504
+ console.log(`Suite runs ${out.suites}`)
505
+ console.log(`Case results ${out.caseResults}`)
506
+ console.log(`Readiness reports ${out.readinessReports}`)
507
+ console.log(`External datasets ${out.externalDatasets}`)
508
+ console.log(`Summary ${path.relative(process.cwd(), out.summaryFile) || out.summaryFile}`)
509
+ return 0
510
+ }
511
+ case "build": {
512
+ const out = await buildBi(cfg, overrides)
513
+ console.log(`Evidence project ${out.evidenceDir}`)
514
+ console.log(`Static site ${out.buildDir}`)
515
+ console.log(`Index ${path.relative(process.cwd(), out.indexFile) || out.indexFile}`)
516
+ console.log(`Summary ${path.relative(process.cwd(), out.summaryFile) || out.summaryFile}`)
517
+ console.log(`Suite runs ${out.suites}`)
518
+ console.log(`Case results ${out.caseResults}`)
519
+ console.log(`Readiness reports ${out.readinessReports}`)
520
+ console.log(`External datasets ${out.externalDatasets}`)
521
+ return 0
522
+ }
523
+ default:
524
+ throw new Error("Usage: forgeqe bi [init|sync|build] [--evidence-dir DIR] [--data-dir DIR] [--build-dir DIR]")
525
+ }
526
+ }
527
+
464
528
  /** forgeqe notify test [channel]: a test card to every notification channel (or one), to check each arrives. */
465
529
  async function cmdNotify(args: string[], flags: Flags): Promise<number> {
466
530
  if (args[0] !== "test") throw new Error("Usage: forgeqe notify test [channel]")
@@ -535,6 +599,51 @@ async function cmdEval(args: string[], flags: Flags): Promise<number> {
535
599
  * from (ForgeQE's, or an edited copy in .forgeqe/prompts), how an edited one differs from ForgeQE's, and how closely each
536
600
  * follows ASD-STE100.
537
601
  */
602
+ /**
603
+ * forgeqe prompts eval [prompt] [--case <id>] [--runs <n>]: runs the evaluation cases of the prompts (ForgeQE's, and
604
+ * the project's in .forgeqe/evals/prompts), with checks decided by code, and compares each pass rate with the last run.
605
+ */
606
+ async function cmdPromptEval(args: string[], flags: Flags): Promise<number> {
607
+ const config = loadConfig(configOpts(flags))
608
+ const runs = Math.max(1, Number(strFlag(flags, "runs")) || 1)
609
+ const dirs = [BUILTIN_CASES, ...(config.configPath ? [path.join(path.dirname(config.configPath), ".forgeqe", "evals", "prompts")] : [])]
610
+ const cases = loadPromptCases(dirs, { prompt: args[0], case: strFlag(flags, "case") })
611
+ if (!cases.length) throw new Error(`No evaluation cases${args[0] ? ` for ${args[0]}` : ""}. They are in ${dirs.join(" and ")}, as <prompt>/<case>.yaml.`)
612
+ const before = previousRates(config)
613
+ const results: CaseResult[] = []
614
+ let failed = 0
615
+ for (const c of cases) {
616
+ process.stdout.write(`${c.prompt}/${c.id} … `)
617
+ const r = await runCase(c, config, process.cwd(), runs)
618
+ results.push(r)
619
+ if (r.skipped) {
620
+ console.log(`skipped (${r.skipped})`)
621
+ continue
622
+ }
623
+ const passed = r.runs.filter((x) => x.pass).length
624
+ if (passed < r.runs.length) failed++
625
+ const was = before.get(`${c.prompt}/${c.id}`)
626
+ const tokens = Math.round(r.runs.reduce((a, x) => a + x.tokens, 0) / r.runs.length / 100) / 10
627
+ console.log(`${passed === r.runs.length ? "✓" : "✗"} ${passed}/${r.runs.length}${was ? ` (was ${was[0]}/${was[1]})` : ""} ${tokens}K tokens a run ${c.title}`)
628
+ // Each check that failed in any run, once, with what was seen.
629
+ const seen = new Set<string>()
630
+ for (const run of r.runs) {
631
+ if (run.error) console.log(` error: ${run.error}`)
632
+ for (const ch of run.checks.filter((x) => !x.pass && !seen.has(x.id))) {
633
+ seen.add(ch.id)
634
+ console.log(` ✗ ${ch.id}: ${ch.detail}`)
635
+ }
636
+ }
637
+ const seenText = r.runs.find((x) => x.seen)?.seen
638
+ if (seenText) console.log(` output: ${seenText}`)
639
+ }
640
+ const file = saveResults(config, results)
641
+ const ran = results.filter((r) => !r.skipped)
642
+ const runsAll = ran.flatMap((r) => r.runs)
643
+ 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}`)
644
+ return failed ? 1 : 0
645
+ }
646
+
538
647
  function cmdPrompts(args: string[], flags: Flags): number {
539
648
  loadConfig(configOpts(flags))
540
649
  const action = args[0] || "list"
@@ -547,20 +656,30 @@ function cmdPrompts(args: string[], flags: Flags): number {
547
656
  return 0
548
657
  }
549
658
  if (action === "check") {
550
- // ASD-STE100: each prompt in use (ForgeQE's or the edited copy) scored, and with a name, the sentences to fix.
659
+ // The prompting standard (src/promptstyle.ts) for each prompt in use, ForgeQE's or the edited copy; with a name,
660
+ // every issue to fix.
551
661
  const all = listPrompts().filter((p) => !name || p.name === name)
552
662
  if (name && !all.length) throw new Error(`No prompt named "${name}". forgeqe prompts lists them.`)
553
663
  const width = Math.max(...all.map((p) => p.name.length))
554
- let below = 0
664
+ let failing = 0
555
665
  for (const p of all) {
556
666
  const text = readFileSync(p.file, "utf8").replace(/^---\n[\s\S]*?\n---\n/, "")
557
- const s = steScore(text)
558
- if (s.score < STE_TARGET) below++
559
- console.log(`${s.score >= STE_TARGET ? "✓" : "✗"} ${p.name.padEnd(width)} ${String(Math.round(s.score * 100)).padStart(3)}% ${s.compliant} of ${s.sentences} sentences${p.source === "built-in" ? "" : ` (${p.source})`}`)
560
- if (name) for (const i of s.issues) console.log(` - ${i.issues.map((x) => x.detail).join("; ")}\n ${i.text}`)
667
+ const kind = promptKind(p.kind)
668
+ const r = kind ? checkStyle(text, kind) : undefined
669
+ const issues = r ? r.issues : [{ rule: "kind", detail: `no kind in the header; use one of ${PROMPT_KINDS.join(", ")}` }]
670
+ if (issues.length) failing++
671
+ const pct = (n: number) => `${String(Math.round(n * 100)).padStart(3)}%`
672
+ const facts = r && r.kind !== "user" ? `STE ${pct(r.ste)} RFC 2119 ${r.ruleLines ? pct(r.rfc) : " –"}${r.kind === "structured" ? ` examples ${r.examples}` : ""}` : ""
673
+ 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})`}`)
674
+ if (name) {
675
+ for (const i of issues) console.log(` - ${i.rule}: ${i.detail}`)
676
+ for (const i of steScore(text).issues) console.log(` - ste: ${i.issues.map((x) => x.detail).join("; ")}\n ${i.text}`)
677
+ }
561
678
  }
562
- console.log(`\nASD-STE100 writing rules: 20 words or fewer in an instruction, 25 in a description, one instruction per sentence, the active voice in instructions, and approved words. Target: ${Math.round(STE_TARGET * 100)}% of sentences.${name ? "" : " forgeqe prompts check <name> lists the sentences to fix."}`)
563
- return below ? 1 : 0
679
+ console.log(
680
+ `\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."}`,
681
+ )
682
+ return failing ? 1 : 0
564
683
  }
565
684
  if (!name) throw new Error(`Usage: forgeqe prompts ${action} <name> (forgeqe prompts lists the names)`)
566
685
  const where = promptFile(name)
@@ -809,6 +928,7 @@ async function runAgent(cfg: AppConfig, flags: Flags, text: string, maxSteps: nu
809
928
  signal: controller.signal,
810
929
  context: () => ({
811
930
  headed: boolFlag(flags, "headed"),
931
+ browserProvider: browserProviderFlag(flags),
812
932
  maxSteps,
813
933
  url: strFlag(flags, "url"),
814
934
  ...memoryFlags(flags),
@@ -869,7 +989,17 @@ async function cmdMcp(args: string[], flags: Flags): Promise<number> {
869
989
  const repo = repoFlag(flags)
870
990
  const cwd = repo || process.cwd()
871
991
  const log = toolPrinter(false, (line) => process.stderr.write(`${line}\n`), detailOf(flags))
872
- const context = (): ToolContext => ({ config: cfg, cwd, headed: boolFlag(flags, "headed"), maxSteps: maxStepsFlag(flags), url: strFlag(flags, "url"), ...memoryFlags(flags), emit: log, session: "server" })
992
+ const context = (): ToolContext => ({
993
+ config: cfg,
994
+ cwd,
995
+ headed: boolFlag(flags, "headed"),
996
+ browserProvider: browserProviderFlag(flags),
997
+ maxSteps: maxStepsFlag(flags),
998
+ url: strFlag(flags, "url"),
999
+ ...memoryFlags(flags),
1000
+ emit: log,
1001
+ session: "server",
1002
+ })
873
1003
  const server = createMcpServer({ tools: TOOLS, context, instructions: () => environmentNotes(context()), version: packageVersion() })
874
1004
  process.stderr.write(`ForgeQE MCP server on stdio (${TOOLS.length} tools, working in ${cwd})\n`)
875
1005
  await serveStdio(server)
@@ -884,6 +1014,7 @@ async function runGoal(cfg: AppConfig, flags: Flags, goal: string, maxSteps: num
884
1014
  const { result, files } = await executeRun({
885
1015
  config: cfg,
886
1016
  goal,
1017
+ browserProvider: browserProviderFlag(flags),
887
1018
  url: strFlag(flags, "url"),
888
1019
  headed: boolFlag(flags, "headed"),
889
1020
  maxSteps,
@@ -1025,6 +1156,7 @@ async function cmdHeal(args: string[], flags: Flags): Promise<number> {
1025
1156
  repo,
1026
1157
  command,
1027
1158
  outDir: strFlag(flags, "out"),
1159
+ browserProvider: browserProviderFlag(flags),
1028
1160
  headed: boolFlag(flags, "headed"),
1029
1161
  maxSteps: maxStepsFlag(flags),
1030
1162
  url: strFlag(flags, "url"),
@@ -1096,6 +1228,7 @@ async function cmdMonitor(refs: string[], flags: Flags): Promise<number> {
1096
1228
  config: cfg,
1097
1229
  checks,
1098
1230
  once,
1231
+ browserProvider: browserProviderFlag(flags),
1099
1232
  headed: boolFlag(flags, "headed"),
1100
1233
  url: strFlag(flags, "url"),
1101
1234
  skills: skillFlags(flags),
@@ -1134,6 +1267,7 @@ async function cmdSuite(refs: string[], flags: Flags): Promise<number> {
1134
1267
  config: cfg,
1135
1268
  cases,
1136
1269
  repo,
1270
+ browserProvider: browserProviderFlag(flags),
1137
1271
  parallel,
1138
1272
  outDir: strFlag(flags, "out"),
1139
1273
  writeBack: boolFlag(flags, "no-write-back") ? false : undefined,