@capagents/forgeqe 2.3.5 → 2.3.6
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 +1 -1
- package/dist/forgeqe.js +2 -2
- package/docs/user-guide.html +1 -1
- package/forgeqe.example.yaml +8 -4
- package/package.json +1 -1
- package/py/forgeqe_desktop/__main__.py +7 -2
- package/py/forgeqe_engine/__main__.py +6 -2
- package/py/forgeqe_engine/stdio.py +38 -0
- package/py/forgeqe_mobile/__main__.py +7 -2
package/HOW_TO_USE.md
CHANGED
|
@@ -351,7 +351,7 @@ Which part uses what:
|
|
|
351
351
|
|
|
352
352
|
Copilot runs one SDK session per model request, with none of Copilot's own tools and your request's system prompt in place of Copilot's. The agent's tools are offered to the model as the session's only tools; when it calls one, the gateway ends the turn and hands the call back to ForgeQE's agent runtime, which runs it (with the same permissions as on any other model) and sends the result with the next request. The gateway itself runs nothing.
|
|
353
353
|
|
|
354
|
-
**With Copilot and a key-based model both in `llms.json`**, Copilot comes first: a sign-in makes it the default, and without a `"default"` it is the active profile. Before a model is used, ForgeQE checks that it works (its sign-in or key, and one live call, kept for a day once it passes). If Copilot does not work (not signed in, the Copilot SDK missing, the model not on your seat), the other model is used and ForgeQE says why and how to fix Copilot; if a key-based model you made the default does not work, Copilot is tried first. A model that fails its check is never used, and when none works the error lists each one with what to do. `--profile` turns
|
|
354
|
+
**With Copilot and a key-based model both in `llms.json`**, Copilot comes first: a sign-in makes it the default, and without a `"default"` it is the active profile. Before a model is used, ForgeQE checks that it works (its sign-in or key, and one live call, kept for a day once it passes). If Copilot does not work (not signed in, the Copilot SDK missing, the model not on your seat), the other model is used and ForgeQE says why and how to fix Copilot; if a key-based model you made the default does not work, Copilot is tried first. A model that fails its check is never used, and when none works the error lists each one with what to do. This happens with `llm.routing` off too: routing picks a model for each kind of work, the fallback only replaces one that does not work. `--profile` turns it off for one run, and `llm.fallback: false` in `forgeqe.yaml` for good: the model you chose is used as it is, and its own error is shown. Each request is billed to the seat (GitHub AI credits or premium requests, by your plan), and GitHub may rate-limit frequent automated requests. If a model is refused, check that your organization's Copilot policies allow it. ForgeQE shows no cost for a `github_copilot` profile unless you give it a `price`.
|
|
355
355
|
|
|
356
356
|
**In pipelines.** `forgeqe suite ... --no-code` uses only the browser engine. Set `COPILOT_GITHUB_TOKEN` to a fine-grained token with the Copilot Requests permission, stored as a pipeline secret. GitHub also documents the workflow's own `GITHUB_TOKEN` with `permissions: copilot-requests: write` for organization repositories whose Copilot policy allows it ([server tokens](https://docs.github.com/en/copilot/how-tos/copilot-sdk/auth/server-to-server-tokens)). A Copilot seat belongs to one person: prefer an organization-billed token over signing a shared pipeline in to someone's seat.
|
|
357
357
|
|
package/dist/forgeqe.js
CHANGED
|
@@ -179,7 +179,7 @@ ${r}`}function BH(e,t){let{text:n,dumps:r}=XPe(e),s,o;if(t)try{if(JPe(pte.dirnam
|
|
|
179
179
|
`).replace(/\x1b\[[0-9;]*m/g,"")),S=!a&&w===0;return{ok:S,exitCode:a?null:w,...BH(v,S?void 0:e.save),ms:Date.now()-n,browsers:[...d]}}var FH=8000,UH=4000,YPe;var Ey=ee(()=>{Qr();Rj();sp();YPe=["--remote-debugging-pipe","--remote-debugging-port","-juggler-pipe","--inspector-pipe","-marionette","--marionette"]});import{randomBytes as tDe}from"crypto";import fte from"path";function rDe(e,t){return["run","--rm","--name",t.name,"--label","forgeqe=1","--label",`forgeqe.project=${t.key}`,"--label","forgeqe.kind=tests","--security-opt","no-new-privileges","--pids-limit","4096","--network",gh(t.key),...t.uid?["--user",t.uid]:[],...[t.repo,...t.mounts].flatMap((n)=>["--volume",`${Fk(n)}:${n}`]),...t.masks?Nj(t.masks):[],...t.home?["--volume",`${t.home}:${Kb}`]:[],"--workdir",t.cwd,...Object.entries({CI:"1",NO_COLOR:"1",FORCE_COLOR:"0",PLAYWRIGHT_HTML_OPEN:"never",HOME:t.home?Kb:"/tmp",...e.env}).flatMap(([n,r])=>["--env",`${n}=${r}`]),...t.byName.flatMap((n)=>["--env",n]),e.image,"sh","-c",t.command]}function sDe(e,t){return e.name==="podman"?["top",t,"pid","ppid","args"]:["top",t,"-eo","pid,ppid,args"]}function oDe(e){return e.split(/\r?\n/).slice(1).flatMap((t)=>{let n=/^\s*\d+\s+\d+\s+(.*)$/.exec(t),r=n?HH(n[1]):null;return r?[r]:[]})}function qj(e,t,n=[],r={}){let s=r.image||Kr(e.cfg).tests;if(!s)throw Error("containers.tests is not set in forgeqe.yaml: it names the image the tests run in.");if(process.platform==="win32")throw Error("Tests in a container need a Linux or macOS machine (the repository is mounted at its own path).");return async(o)=>{let a=Date.now(),l=wf(Kr(e.cfg).root);await Vb(e,l);let c=e.env||process.env,d=new Set([...Nn(e.cfg),...o.hide||[]]),f=YT({name:"containers.tests",secrets:s.secrets},e.cfg,c),m=$y(),h=o.env||{},w={...Object.fromEntries(Object.entries(c).filter(([de,Re])=>nDe.test(de)&&Re!==void 0)),...m,...h,...f};for(let de of Object.keys(w))if(d.has(de))delete w[de];let v=`forgeqe-${l}-tests-${tDe(3).toString("hex")}`,S=typeof process.getuid==="function"?`${process.getuid()}:${process.getgid?.()??process.getuid()}`:void 0,R=[...new Set(n.map((de)=>fte.resolve(de)))].filter((de)=>fte.relative(t,de).startsWith("..")),j=r.masks||Oj(t),O=rDe(s,{key:l,name:v,repo:t,cwd:o.cwd,mounts:R,command:o.command,byName:Object.keys(w),uid:S,...r.home?{home:r.home}:{},masks:j}),D={...sy(c,["DOCKER_HOST","DOCKER_CONTEXT","DOCKER_CONFIG","DOCKER_CERT_PATH","DOCKER_TLS_VERIFY","CONTAINER_HOST","CONTAINER_CONNECTION"]),...w},M=o.timeoutSeconds*1000,W,Z=new Set,G=!0,re=(async()=>{while(G){let de=await e.exec(e.runtime.bin,sDe(e.runtime,v),{env:D,timeoutMs:1e4}).catch(()=>null);if(de?.code===0)for(let Re of oDe(de.stdout))Z.add(Re);if(G)await Bun.sleep(1000)}})();try{W=await e.exec(e.runtime.bin,O,{env:D,timeoutMs:M,...o.signal?{signal:o.signal}:{}})}finally{G=!1,await re,await e.exec(e.runtime.bin,["rm","--force","--volumes",v],{env:D,timeoutMs:30000}).catch(()=>{return})}let me=Date.now()-a>=M,ie=[...Object.values(f),...Object.values(m),...[...d].map((de)=>c[de]||"")],pe=Uk(Va([W.stdout,W.stderr].filter((de)=>de.trim()).join(`
|
|
180
180
|
`).replace(/\x1b\[[0-9;]*m/g,"")),ie),Ae=!me&&W.code===0;return{ok:Ae,exitCode:me?null:W.code,...BH(pe,Ae?void 0:o.save),ms:Date.now()-a,browsers:[...Z]}}}var nDe,Kb="/forgeqe-home";var zH=ee(()=>{Ey();DH();sp();rp();bf();Qr();nDe=/^FORGEQE_(ACTIVE_ENV$|APP_|VAR_)/});import{randomBytes as iDe}from"crypto";import{chmodSync as aDe,existsSync as gte,mkdtempSync as lDe,realpathSync as cDe,rmSync as uDe,writeFileSync as dDe}from"fs";import pDe from"os";import WH from"path";function mDe(e,t){return["run","--detach","--init","--name",t.name,"--label","forgeqe=1","--label",`forgeqe.project=${t.key}`,"--label","forgeqe.kind=workspace","--security-opt","no-new-privileges","--pids-limit","4096","--network",gh(t.key),...t.uid?["--user",t.uid]:[],"--volume",`${Fk(t.repo)}:${t.repo}`,...Nj(t.masks),"--volume",`${t.home}:${Kb}`,"--workdir",t.repo,...Object.entries({CI:"1",NO_COLOR:"1",FORCE_COLOR:"0",PLAYWRIGHT_HTML_OPEN:"never",HOME:Kb,...e.env}).flatMap(([n,r])=>["--env",`${n}=${r}`]),...t.byName.flatMap((n)=>["--env",n]),"--entrypoint","sleep",e.image,"infinity"]}function hDe(e){return["#!/bin/sh","# ForgeQE: the agent's shell commands run in the workspace container (docs/CONTAINERS.md).",'if [ "$#" -ne 2 ] || [ "$1" != "-c" ]; then echo "forgeqe workspace: expected -c <command>" >&2; exit 2; fi','case "$PWD/" in',` ${Fj(e.repo+"/")}*) ;;`,` *) echo "forgeqe workspace: $PWD is outside the repository; commands run in the workspace container, which has only "${Fj(e.repo)} >&2; exit 2 ;;`,"esac",`exec ${Fj(e.bin)} exec --workdir "$PWD" ${e.names.map((t)=>`--env ${t}`).join(" ")} ${Fj(e.container)} sh -c "$2"`.replace(/ {2,}/g," "),""].join(`
|
|
181
181
|
`)}function gDe(e,t){return["## Where your commands run",`Your shell commands run in a Linux container (${e}), ForgeQE's workspace for this project, not on the user's machine. The repository is mounted at the same path (${t}), so file paths are the same in your file tools and your commands; files outside it do not exist there. Build, restore and run with the tools the image has, run browsers headless, and do not install system packages: if a tool is missing, say so in your notes. ForgeQE runs the tests in the same image.`].join(`
|
|
182
|
-
`)}class Uj{cfg;repo;w;name=null;starting=null;dir=null;constructor(e,t,n){this.cfg=e;this.repo=t;this.w=n}get image(){return this.w.image}get settings(){return JSON.stringify(this.w)}update(e){this.cfg=e}async stop(){let e=this.name;if(this.name=null,this.starting=null,this.close(),!e)return;let t=Vc(this.cfg);if(t)await GH(t.bin,["rm","--force",e],{env:Wo({cfg:this.cfg,runtime:t,exec:GH}),timeoutMs:30000}).catch(()=>{return});ZT(e)}ctx(e){let t=Vc(this.cfg);if(!t)throw Error(`containers.workspace is set, but ${Kr(this.cfg).runtime||"Docker or Podman"} is not on PATH. Install one, or remove containers.workspace to work on this machine.`);return{cfg:this.cfg,runtime:t,exec:GH,...e?{log:e}:{}}}async ensure(e){let t=this.ctx(e);if(this.name){let n=await t.exec(t.runtime.bin,["inspect","--format","{{.State.Running}}",this.name],{env:Wo(t),timeoutMs:15000});if(n.code===0&&n.stdout.trim()==="true")return this.name;ZT(this.name),this.name=null,this.starting=null}return this.starting||=this.start(t).catch((n)=>{throw this.starting=null,n}),this.name=await this.starting}async start(e){let t=wf(Kr(this.cfg).root),n=Wo(e);if(await Vb(e,t),this.w.services.length){let d=await Ij(e,Sy(Kr(this.cfg).services,this.w.services));if(d.failed)throw Error(`${d.failed.service} did not start for the workspace: ${d.failed.error}`)}let r=await this.ensureHome(e,t),s=YT({name:"containers.workspace",secrets:this.w.secrets},this.cfg,process.env),o=`forgeqe-${t}-workspace-${iDe(3).toString("hex")}`;e.log?.(`Starting the workspace container (${this.w.image})`);let a=hte(),l=mDe(this.w,{key:t,name:o,repo:this.repo,home:r,masks:Oj(this.repo),byName:Object.keys(s),...a?{uid:a}:{}}),c=await e.exec(e.runtime.bin,l,{env:{...n,...s},timeoutMs:900000});if(c.code!==0)throw Error(`The workspace container did not start: ${Uk((c.stderr||c.stdout).trim(),Object.values(s)).slice(-1500)}`);return Mj(e,o),o}async ensureHome(e,t){let n=mte(t),r=Wo(e);if((await e.exec(e.runtime.bin,["volume","inspect",n],{env:r,timeoutMs:15000})).code===0)return n;let o=await e.exec(e.runtime.bin,["volume","create","--label","forgeqe=1","--label",`forgeqe.project=${t}`,n],{env:r,timeoutMs:30000});if(o.code!==0)throw Error(`Could not create the volume ${n}: ${o.stderr.trim().slice(0,300)}`);let a=hte();if(a){let l=await e.exec(e.runtime.bin,["run","--rm","--user","0:0","--network","none","--volume",`${n}:${Kb}`,"--entrypoint","chown",this.w.image,a,Kb],{env:r,timeoutMs:600000});if(l.code!==0)throw Error(`Could not prepare the volume ${n}: ${l.stderr.trim().slice(0,300)}`)}return n}async shell(e){let t=await this.ensure(e),n=this.ctx(e);this.dir||=lDe(WH.join(pDe.tmpdir(),"forgeqe-workspace-"));let r=WH.join(this.dir,`sh-${t}`);if(!gte(r)){let s=[...new Set([...Object.keys(process.env).filter((o)=>fDe.test(o)),...Object.keys($y())])].filter((o)=>/^[A-Za-z_][A-Za-z0-9_]*$/.test(o)).sort();dDe(r,hDe({bin:n.runtime.bin,container:t,repo:this.repo,names:s}),{mode:448}),aDe(r,448)}return{path:r,note:gDe(this.w.image,this.repo)}}runTest=async(e)=>{let t=this.ctx();await this.ensure();let n=wf(Kr(this.cfg).root);return qj({...t,...e.signal?{signal:e.signal}:{}},this.repo,[],{image:this.w,home:mte(n)})(e)};close(){if(this.dir)uDe(this.dir,{recursive:!0,force:!0});this.dir=null}}function yDe(e){let t=e.code?.repo||Kr(e).root;return gte(t)?cDe(t):WH.resolve(t)}function yte(e){if(process.platform==="win32")return;let t=yDe(e),n=e.containers?.workspace,r=qk(t);if(n&&r instanceof Uj&&r.repo===t&&r.settings===JSON.stringify(n))return r.update(e);if(r instanceof Uj&&r.repo===t)r.stop();tte(t,n?new Uj(e,t,n):null)}var fDe,mte=(e)=>`forgeqe-${e}-home`,hte=()=>typeof process.getuid==="function"?`${process.getuid()}:${process.getgid?.()??process.getuid()}`:void 0,Fj=(e)=>`'${e.replace(/'/g,"'\\''")}'`,GH;var wte=ee(()=>{sp();Rj();rp();DH();bf();zH();fDe=/^FORGEQE_(ACTIVE_ENV$|APP_|VAR_)/;GH=xy});import bte from"path";function xi(e,t){if(e===void 0||e===null||e==="")return;if(typeof e!=="string"&&typeof e!=="number")throw Error(`${t} must be text`);return String(e).trim()||void 0}function QH(e,t){let n=xi(e,t);if(n!==void 0&&!bDe.test(n))throw Error(`${t} must name an environment variable (letters, digits and _), not hold its value`);return n}function SDe(e,t,n,r){if(!wDe.test(e))throw Error(`${n}: database names are letters, digits and _ . -`);if(!t||typeof t!=="object"||Array.isArray(t))throw Error(`${n} must be a mapping`);let s=t;for(let R of Object.keys(s))if(_De.includes(R.toLowerCase()))throw Error(`${n}.${R}: no secrets in config files. Put it in an environment variable and name it with ${R.startsWith("p")?"password_env":"url_env"} (save it with /auth NAME).`);let o=xi(s.engine,`${n}.engine`)?.toLowerCase();if(o!==void 0&&!VH.includes(o))throw Error(`${n}.engine must be one of ${VH.join(", ")}`);let a=(xi(s.auth,`${n}.auth`)||"password").toLowerCase();if(!["password","azure","aws_iam","none"].includes(a))throw Error(`${n}.auth must be password, azure, aws_iam or none`);let l=xi(s.ssl,`${n}.ssl`)?.toLowerCase();if(l!==void 0&&!["verify","require","disable"].includes(l))throw Error(`${n}.ssl must be verify, require or disable`);let c=xi(s.host,`${n}.host`);if(c!==void 0&&!(o==="kafka"?/^[A-Za-z0-9.-]{1,253}(:\d{1,5})?(,[A-Za-z0-9.-]{1,253}(:\d{1,5})?){0,20}$/:vDe).test(c))throw Error(`${n}.host must be a host name or address`);let d=s.port===void 0||s.port===null?void 0:Number(s.port);if(d!==void 0&&!(Number.isInteger(d)&&d>0&&d<65536))throw Error(`${n}.port must be a port number`);let f=QH(s.url_env,`${n}.url_env`);if(!f&&!o)throw Error(`${n}: set engine (${VH.join(", ")}), or url_env with a connection string`);let m=xi(s.path,`${n}.path`);if((o==="sqlite"||o==="duckdb")&&!m&&!f)throw Error(`${n}.path: the ${o} file`);let h=s.timeout_seconds===void 0?30:Number(s.timeout_seconds);if(!(Number.isInteger(h)&&h>0&&h<=3600))throw Error(`${n}.timeout_seconds must be 1 to 3600`);let y=s.sample_rows===void 0?0:Number(s.sample_rows);if(!(Number.isInteger(y)&&y>=0&&y<=5))throw Error(`${n}.sample_rows must be 0 to 5`);let w=s.mask===void 0||s.mask===null?[]:s.mask;if(!Array.isArray(w)||!w.every((R)=>typeof R==="string"))throw Error(`${n}.mask must be a list of column name patterns`);for(let R of w)try{new RegExp(R)}catch{throw Error(`${n}.mask: "${R}" is not a valid pattern`)}let v={},S=s.options===void 0||s.options===null?{}:s.options;if(typeof S!=="object"||Array.isArray(S))throw Error(`${n}.options must be a mapping`);for(let[R,j]of Object.entries(S)){if(!vte.has(R))throw Error(`${n}.options.${R} is not a known option (${[...vte].join(", ")})`);v[R]=String(xi(j,`${n}.options.${R}`)||"")}return{name:e,...o?{engine:o}:{},...xi(s.description,`${n}.description`)?{description:xi(s.description,`${n}.description`)}:{},...f?{urlEnv:f}:{},...c?{host:c}:{},...d?{port:d}:{},...xi(s.database,`${n}.database`)?{database:xi(s.database,`${n}.database`)}:{},...xi(s.user,`${n}.user`)?{user:xi(s.user,`${n}.user`)}:{},...QH(s.password_env,`${n}.password_env`)?{passwordEnv:QH(s.password_env,`${n}.password_env`)}:{},...m?{path:bte.resolve(r,m)}:{},...xi(s.schema,`${n}.schema`)?{schema:xi(s.schema,`${n}.schema`)}:{},auth:a,...xi(s.region,`${n}.region`)?{region:xi(s.region,`${n}.region`)}:{},...l?{ssl:l}:{},...xi(s.ssl_ca,`${n}.ssl_ca`)?{sslCa:bte.resolve(r,xi(s.ssl_ca,`${n}.ssl_ca`))}:{},writable:s.writable===!0,...s.vector===!0?{vector:!0}:{},timeoutSeconds:h,sampleRows:y,mask:w,options:v}}function Bj(e,t,n){if(e===void 0||e===null)return{};if(typeof e!=="object"||Array.isArray(e))throw Error(`${t} must be a mapping of database names to settings`);let r={};for(let[s,o]of Object.entries(e))r[s]=SDe(s,o,`${t}.${s}`,n);return r}function va(e){let t=e.environments,n=t?.active?t.all[t.active]:void 0;return{...e.databases||{},...n?.databases||{}}}function Vo(e,t){let n=va(e),r=Object.keys(n);if(!r.length)throw Error("No databases are set up: add one under databases: in forgeqe.yaml or the environment in env.yaml (HOW_TO_USE.md, Databases).");let s=String(t||"").trim();if(!s){if(r.length===1)return n[r[0]];throw Error(`Name the database: ${r.join(", ")}.`)}let o=n[s]||Object.values(n).find((a)=>a.name.toLowerCase()===s.toLowerCase());if(!o)throw Error(`No database "${s}" (databases: ${r.join(", ")}).`);return o}function Hj(e){return{name:e.name,...e.engine?{engine:e.engine}:{},...e.urlEnv?{url_env:e.urlEnv}:{},...e.host?{host:e.host}:{},...e.port?{port:e.port}:{},...e.database?{database:e.database}:{},...e.user?{user:e.user}:{},...e.passwordEnv?{password_env:e.passwordEnv}:{},...e.path?{path:e.path}:{},...e.schema?{schema:e.schema}:{},auth:e.auth,...e.region?{region:e.region}:{},...e.ssl?{ssl:e.ssl}:{},...e.sslCa?{ssl_ca:e.sslCa}:{},writable:e.writable,timeout_seconds:e.timeoutSeconds,options:e.options}}var VH,wDe,bDe,vDe,_De,vte;var Qc=ee(()=>{VH=["postgres","cockroachdb","redshift","mysql","mariadb","mssql","oracle","sqlite","duckdb","snowflake","mongodb","kafka"],wDe=/^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/,bDe=/^[A-Za-z_][A-Za-z0-9_]{0,63}$/,vDe=/^[A-Za-z0-9.-]{1,253}$/,_De=["password","pwd","url","uri","connection_string","connectionstring","token","secret"],vte=new Set(["warehouse","role","odbc_driver","security_protocol","sasl_mechanism"])});import{existsSync as kDe,readFileSync as xDe}from"fs";import EDe from"path";function zj(e){let t=EDe.join(e,".env");if(!kDe(t))return;let n=xDe(t,"utf8");for(let r of n.split(/\r?\n/)){let s=r.trim();if(!s||s.startsWith("#"))continue;let o=s.indexOf("=");if(o<=0)continue;let a=s.slice(0,o).trim(),l=s.slice(o+1).trim();if(l.startsWith('"')&&l.endsWith('"')||l.startsWith("'")&&l.endsWith("'"))l=l.slice(1,-1);if(process.env[a]===void 0)process.env[a]=l}}var _te=()=>{};function JH(e){return Lr.find((t)=>t.id===e)}var KH,Lr,Gj;var Kc=ee(()=>{KH=["docs/DESKTOP.md","docs/DESKTOP-CI.md","docs/MOBILE.md","docs/CONTAINERS.md","docs/MODEL_ROUTING.md"],Lr=[{id:"browser-goals",area:"Browser",title:"Run any goal in a real browser",summary:"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.",tools:["browser_run","run_details","page_memory","browser_devices"],keywords:["browser","website","web site","web app","page","url","go to","open the","click","search","chrome","edge","firefox","device","testmu","browserstack","sign up","checkout","site","form","cart"],examples:["Go to booking.com and search hotels in Mumbai for next weekend","What does ForgeQE know about the checkout page?","Add Chrome on Windows 11 and Chrome 128 on Windows 10 as TestMu devices, then run SHOP-12 on both"],requires:[{kind:"live_model"}],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."],docs:[{doc:"HOW_TO_USE.md",heading:"4. Without the shell"},{doc:"HOW_TO_USE.md",heading:"5. Page memory"}]},{id:"logins",area:"Browser",title:"Saved logins for the application under test",summary:"Signs the browser agent in with saved credentials (including TOTP) and reuses the session; a user can also sign in by hand once in a browser.",tools:["logins"],keywords:["login","log in","sign in","signed in","session","credential","password","saved login","forget the saved"],examples:["Sign in to the admin site and keep the session","Forget the saved session for shop.example.com"],requires:[],docs:[{doc:"HOW_TO_USE.md",heading:"Signing in"}]},{id:"record-web",area:"Browser",title:"Record a web test by hand",summary:"Opens a browser with the Recorder panel docked in the page while the user does the steps of a web test themself, and saves their clicks, typing, choices and keys in order as a run, with the checks, kept values, test data and notes they add; the coding agent then turns it into a test in the repository's framework.",tools:["web_record"],keywords:["record","recording","recorder","show you the steps","i'll show you","do the steps myself","do it myself","my steps","codegen"],examples:["I'll show you the checkout steps in the shop: record them","Record my steps on the cart page and make a test of it"],requires:[{kind:"coding_agent",optional:!0}],limits:["Local Chrome or Chromium only, one recording at a time; drag and drop and steps inside iframes are not recorded yet"],docs:[{doc:"HOW_TO_USE.md",heading:"Recording a web test by hand"}]},{id:"run-test-cases",area:"Test cases",title:"Run manual test cases in browsers",summary:"Reads test cases from Azure DevOps, Jira, Xray, TestRail, Zephyr Scale, qTest, a file, or pasted text, runs each in a real browser with a verdict per step, writes report.html and junit.xml, posts results back, and files or updates bugs for failures.",tools:["find_test_cases","run_test_cases"],keywords:["test case","cases","test plan","suite","manual test","run case","execute case","run the case"],examples:["Run SHOP-12 and SHOP-14 from Jira, two browsers at once","Run test plan 12 suite 34 from Azure DevOps and don't post results"],requires:[{kind:"live_model"},{kind:"system",anyOf:["azure_devops","jira","xray","testrail","zephyr","qtest"],optional:!0,why:"to read cases from a test management system and post results back (files and pasted text work without one)"},{kind:"bugs",optional:!0}],docs:[{doc:"HOW_TO_USE.md",heading:"7. Test cases and suites"},{doc:"HOW_TO_USE.md",heading:"Where cases come from"},{doc:"HOW_TO_USE.md",heading:"Running"},{doc:"HOW_TO_USE.md",heading:"Results back in the source"},{doc:"HOW_TO_USE.md",heading:"Bugs for failures"}]},{id:"test-case-quality",area:"Test cases",title:"Check and improve test cases",summary:"Checks test cases before they run (vague or missing expected results, missing test data, placeholders, duplicates) against the story, feature, and epic they test, then rewrites Azure DevOps or Jira cases in place once the user agrees.",tools:["check_test_cases","update_test_case"],keywords:["good enough","quality","improve","fix the expected","expected result","check the cases","check cases","review the cases","rewrite"],examples:["Are the cases in test plan 12 good enough to run?","Fix the expected results of case 4512"],requires:[{kind:"system",anyOf:["azure_devops","jira"],optional:!0,why:"to rewrite cases in place"}],docs:[{doc:"HOW_TO_USE.md",heading:"Checking test cases before they run"}]},{id:"write-test-cases",area:"Test cases",title:"Write test cases from a story",summary:"Writes test cases from a story's acceptance criteria and saves them in Azure DevOps or Jira, linked to the story. Reads the cases already linked first, so asking again adds only what is missing and updates cases in place instead of duplicating them; plans end-to-end journeys from the existing cases.",tools:["story_test_cases","save_test_cases","e2e_journey"],keywords:["write test cases","test cases for","story","acceptance criteria","journey","end-to-end","generate cases","save them"],examples:["Write test cases for story 1234 and save them","Build an end-to-end journey from story 1234's cases"],requires:[{kind:"system",anyOf:["azure_devops","jira"]}],docs:[{doc:"HOW_TO_USE.md",heading:"Where cases come from"}],playbook:[{heading:"Testing from requirements",when:"write test cases from a story, a Confluence page, or a specification"},{heading:"End-to-end journeys from test cases",when:"an end-to-end case or flow from test cases that exist"}]},{id:"domain-compliance",area:"Test cases",title:"Domain and regulatory test design",summary:"Every agent works as an expert in the product's industry would, automatically: the risks the industry is known for and the obligations of its regulations and standards (for example DORA, PSD2, GDPR, ISO 27001), declared in forgeqe.yaml or worked out from the story and the application. It applies to analysing requirements, writing and checking test cases, exploration, writing, reviewing, and healing test code, API tests, finding tests, and pull request reviews, labelled so coverage can be traced and citing only the control ids in the team's compliance file. forgeqe eval domain measures how well the agent does it.",tools:[],keywords:["gdpr","dora","psd2","hipaa","pci","sox","iso 27001","compliance","regulation","obligation"],examples:["Write test cases for story 1234, covering our DORA and PSD2 obligations","Which GDPR obligations do these cases miss?"],requires:[{kind:"domain",optional:!0}],limits:["Designs and checks tests for regulatory obligations; it does not certify compliance or replace an audit.","Cites control or article ids only from the team's compliance file, never from memory."],docs:[{doc:"HOW_TO_USE.md",heading:"16. Domain and regulations"}]},{id:"exploratory-testing",area:"Test cases",title:"Exploratory testing of a story",summary:"Explores a story in real browsers beyond its scripted cases (edge cases, invalid input, boundaries, roles, state), confirms each defect by reproducing it, proposes test cases, and files bugs once the user agrees.",tools:["explore_story"],keywords:["explore","exploratory","find bugs","beyond the test cases","charter"],examples:["Explore story 1234 and find bugs","Test this feature beyond the test cases"],requires:[{kind:"live_model"},{kind:"system",anyOf:["azure_devops","jira"],optional:!0,why:"to read the story and file bugs"}],docs:[{doc:"HOW_TO_USE.md",heading:"Exploratory testing from a story"}],playbook:[{heading:"Exploratory testing",when:"explore a story or feature past its scripted cases, or find bugs in it"}]},{id:"test-apps",area:"Setup",title:"Test environments and their apps",summary:"Knows the test environments in env.yaml (QA, UAT, \u2026) and each one's apps: web apps and APIs by URL, Windows desktop apps, and mobile apps, with what each is for. Every agent works from the active one; switches environment on request, picks the apps a scenario needs, and carries values such as an order number from one app to the next.",tools:["test_apps","use_environment"],keywords:["environment","env","qa","uat","staging","apps","which apps"],examples:["Place an order in the shop, then check its invoice in Billing, in UAT","Which apps can you test in QA?","From now on, use the UAT environment"],requires:[{kind:"input",what:"env.yaml with the environments and apps (forgeqe init writes one)"}],limits:[],docs:[{doc:"HOW_TO_USE.md",heading:"Environments and apps"}],playbook:[{heading:"Apps and environments",when:"a request names an app or environment, or a scenario spans several apps"}]},{id:"desktop-apps",area:"Desktop",title:"Automate Windows desktop apps",summary:"Carries out a plain-language goal in a Windows thick-client app with UFO\xB2 and saves it as a workflow with checks of the outcome, or records the steps a tester does by hand, with no model; replays saved workflows with no model, healing a step whose control moved, and fixes a workflow from chat (a timeout, a wait, a dialog to dismiss) after showing the change. Apps and the secrets they may type come from env.yaml or forgeqe.yaml desktop:.",tools:["desktop_explore","desktop_record","desktop_replay","desktop_edit_workflow"],keywords:["desktop","windows app","thick client","thick-client",".exe","record","replay","workflow","ufo","notepad","win32","wpf","winforms"],examples:["Create an invoice for Contoso in the Billing app and save it as a test","I'll show you the steps in Billing: record them","Replay the billing workflow with customer Fabrikam","Step 4 timed out while the list loaded: fix the workflow"],requires:[{kind:"live_model",optional:!0},{kind:"apps",app:"desktop"},{kind:"input",what:"a Windows machine, and the app in forgeqe.yaml desktop.apps"}],limits:["Windows only, one run at a time per desktop","Apps whose controls UI Automation cannot see (Citrix, some Java and custom-drawn apps) are not supported yet"],docs:[{doc:"HOW_TO_USE.md",heading:"Desktop apps (Windows)"}],guide:[{doc:"docs/DESKTOP.md",heading:"ForgeQE desktop: thick-client automation"},{doc:"docs/DESKTOP.md",heading:"Use"},{doc:"docs/DESKTOP.md",heading:"How a run flows"},{doc:"docs/DESKTOP.md",heading:"One model config: `llms.json` \u2192 UFO"},{doc:"docs/DESKTOP.md",heading:"Workflow YAML (`forgeqe-desktop-v1`)"},{doc:"docs/DESKTOP.md",heading:"Recording by hand"},{doc:"docs/DESKTOP.md",heading:"Fixing a workflow from chat"},{doc:"docs/DESKTOP.md",heading:"Waits, retries and dialogs"},{doc:"docs/DESKTOP.md",heading:"Visual checks (`expect_image`)"},{doc:"docs/DESKTOP.md",heading:"Unattended runs"},{doc:"docs/DESKTOP.md",heading:"Updating UFO\xB2"},{doc:"docs/DESKTOP.md",heading:"Checks of the outcome"},{doc:"docs/DESKTOP.md",heading:"Recording"},{doc:"docs/DESKTOP.md",heading:"Security"},{doc:"docs/DESKTOP.md",heading:"Before first use on Windows"},{doc:"docs/DESKTOP.md",heading:"Next steps"},{doc:"docs/DESKTOP-CI.md",heading:"Running desktop workflows unattended (nightly CI)"},{doc:"docs/DESKTOP-CI.md",heading:"What breaks a desktop run, and how ForgeQE tells you"},{doc:"docs/DESKTOP-CI.md",heading:"Set up the test machine"},{doc:"docs/DESKTOP-CI.md",heading:"The nightly job"}],playbook:[{heading:"Desktop apps",when:"automate or test a Windows desktop (thick-client) app"}]},{id:"mobile-apps",area:"Mobile",title:"Automate Android and iOS apps",summary:"Carries out a plain-language goal in an Android or iOS app through Appium and saves it as a mobile workflow with checks of the outcome; replays workflows with no model on Appium (healing a moved control) or Maestro; runs the team's own Maestro flows after checking them against an allowlist; uploads an app build to TestMu AI and saves its lt:// id as the app's build. Apps come from env.yaml (kind: mobile) or forgeqe.yaml mobile:.",tools:["mobile_explore","mobile_replay","mobile_maestro","mobile_upload"],keywords:["mobile","android","ios","iphone","ipad","phone","tablet","apk","ipa","maestro","appium","emulator","simulator","app store","play store","upload","build","testmu","lambdatest"],examples:["Sign in to the courier app and accept the first delivery, and save it as a test","Replay the courier login on Maestro","Run our Maestro flows in .maestro/","Upload builds/courier.apk to TestMu for the courier app"],requires:[{kind:"live_model",optional:!0},{kind:"apps",app:"mobile"},{kind:"input",what:"an Appium server with a device or emulator (or a device cloud), or Maestro installed; the app in env.yaml or forgeqe.yaml mobile.apps"}],limits:["Explore runs on Appium only","Maestro replays do not heal moved controls or compare images (expect_image)","On TestMu AI, Maestro (through HyperExecute) refuses workflows that type secrets; replay those there with --driver appium"],docs:[{doc:"HOW_TO_USE.md",heading:"Mobile apps (Android, iOS)"}],guide:[{doc:"docs/MOBILE.md",heading:"ForgeQE mobile: Android and iOS apps"},{doc:"docs/MOBILE.md",heading:"Use"},{doc:"docs/MOBILE.md",heading:"Apps and settings"},{doc:"docs/MOBILE.md",heading:"Workflow YAML (`forgeqe-mobile-v1`)"},{doc:"docs/MOBILE.md",heading:"Explore"},{doc:"docs/MOBILE.md",heading:"Maestro"},{doc:"docs/MOBILE.md",heading:"TestMu AI (formerly LambdaTest)"},{doc:"docs/MOBILE.md",heading:"Security"}],playbook:[{heading:"Mobile apps",when:"automate or test an Android or iOS app"}]},{id:"containers",area:"Operations",title:"Services, mocks, workspace and browser in Docker or Podman",summary:"Starts the services a test run needs (a database, a queue, the API under test) in Docker or Podman, as forgeqe.yaml declares them, and waits until each is ready; stops them, and shows their status and logs. Starts mocks of an API on WireMock, from the calls a browser run recorded or from an OpenAPI description, and can point an API app of the environment at the mock for the session. With containers.workspace, the coding agents' shell commands (build, restore, run) and every test ForgeQE runs go to a container of the project's image; with browser.provider docker, the browser agent, the replay and the coding agent's live browser use Chromium in a container.",tools:["services","mock_service"],keywords:["docker","podman","container","containers","compose","mock","mocks","mock service","mock server","mock api","stub","stubs","wiremock","testcontainers","service up","services up","workspace","docker browser","browser in docker","run tests in docker","build in docker"],examples:["Start the database and the queue in Docker","Start the services from our docker-compose file","Mock the payments API from the last run","Create a mock service from openapi.yaml and point order-api at it","Show the logs of the db container"],requires:[{kind:"containers"}],limits:["Starts only services declared in forgeqe.yaml containers.services; it never runs an image, flag or mount from a request","No privileged containers, host network, or access to the container runtime's socket; ports on 127.0.0.1 only; mounts read-only from inside the project","Mocks answer REST calls (WireMock); GraphQL, SOAP and gRPC are not mocked","Reads docker-compose files only for their services' image, ports, environment, bind mounts, command and depends_on; refuses build, privileged, network_mode, cap_add, devices and env_file; leaves named volumes out","The workspace mounts the repository with its credential files (.env, .npmrc, keys) covered; the agents and the model key stay on this machine; ForgeQE's own agent keeps this machine's shell","A browser in a container reaches this machine's apps as host.docker.internal, not localhost; it is headless, or visible on a virtual display, never on this screen"],docs:[{doc:"HOW_TO_USE.md",heading:"Containers and mock services"}],guide:[{doc:"docs/CONTAINERS.md",heading:"ForgeQE containers: services and mocks in Docker or Podman"},{doc:"docs/CONTAINERS.md",heading:"Use"},{doc:"docs/CONTAINERS.md",heading:"Declaring services"},{doc:"docs/CONTAINERS.md",heading:"Compose files"},{doc:"docs/CONTAINERS.md",heading:"The project network"},{doc:"docs/CONTAINERS.md",heading:"Tests in a container"},{doc:"docs/CONTAINERS.md",heading:"The workspace"},{doc:"docs/CONTAINERS.md",heading:"The browser in a container"},{doc:"docs/CONTAINERS.md",heading:"Testcontainers (Scribe)"},{doc:"docs/CONTAINERS.md",heading:"Mocks"},{doc:"docs/CONTAINERS.md",heading:"Lifetime"},{doc:"docs/CONTAINERS.md",heading:"Security"}],playbook:[{heading:"Containers and mocks",when:"start, stop or inspect the project's services in Docker or Podman, or mock an API"}]},{id:"databases",area:"Data",title:"Ask a database in plain language",summary:"Connects to the project's databases (PostgreSQL, MySQL and MariaDB, SQL Server and Azure SQL, Oracle, Snowflake, Redshift, CockroachDB, SQLite, DuckDB, and MongoDB) from a connection string or host and user, signing in with a password, Microsoft Entra ID or AWS IAM. Reads the schema through DB-GPT's connectors and turns questions into SQL that is checked before it runs: reads in a read-only session that is rolled back, writes only on a database marked writable, after you confirm them.",tools:["databases","database_schema","run_sql"],keywords:["sql","database","db","schema","text to sql","text-to-sql","postgres","postgresql","mysql","mariadb","sql server","mssql","azure sql","oracle","snowflake","redshift","cockroachdb","sqlite","duckdb","mongodb","mongo","collection","select statement","query the database","rows in"],examples:["Which customers in India have paid orders? Ask the orders database","Show me the tables in the shop database","Write the SQL for revenue per country last month and run it","Check the connection to the reporting database"],requires:[{kind:"databases"}],limits:["One statement at a time: SELECT (with CTEs and set operations), SHOW, DESCRIBE and EXPLAIN SELECT anywhere; INSERT, and UPDATE or DELETE with a WHERE clause, only on databases marked writable and after you confirm","Never DDL (CREATE, ALTER, DROP, TRUNCATE), procedures, or functions that read files, sleep or reach other servers","MongoDB is queried with find, aggregate, count and distinct (JSON operations); its test data is not made yet; other non-SQL stores are not supported"],docs:[{doc:"HOW_TO_USE.md",heading:"Databases"}],playbook:[{heading:"Databases",when:"answer a question from a database, write or run SQL, or check what a database holds"}]},{id:"test-data",area:"Data",title:"Test data on demand",summary:"Makes realistic test data from a spec that fits the database's schema (types, sizes, NOT NULL, unique keys, CHECK lists, enums), parents before children, the same rows for the same seed. Or takes a masked subset of a real database: the rows you pick and every row they need, with names, emails, phones, addresses and other personal columns replaced the same way every time. Shows a preview, writes CSV, JSON or SQL files for automation, or inserts the rows into a database marked writable after you confirm, as a batch that can be removed again. Specs live in the project, so tests and pipelines make the same data with forgeqe data gen.",tools:["generate_test_data","test_data_batches"],keywords:["test data","seed data","fixture","fixtures","synthetic data","fake data","dummy data","sample data","generate data","populate","seed the database","test data batch","clean up test data","faker","subset","masked","mask","anonymize","anonymise","copy of production","production data"],examples:["Generate test data: 20 customers with 1 to 3 orders each in the shop database","Make test data for the orders tables as CSV files for our automation","Insert 50 test users into the fixtures database","Copy 100 paid orders from prod into fixtures as a masked subset","Remove the test data batch we inserted yesterday"],requires:[{kind:"databases"}],limits:["Inserts only into databases marked writable, after you confirm; removes only rows of a batch it inserted","A subset reads its source in read-only sessions; its start condition is a filter of one table, and personal columns are found by name (mask: adds others)","Simple CHECK constraints (a list of values, bounds) are read; other rules the database enforces may refuse rows, and then nothing is kept"],docs:[{doc:"HOW_TO_USE.md",heading:"Test data"}],playbook:[{heading:"Test data",when:"make test data, fixtures or seed rows, write them to files, insert them, or remove a batch"}]},{id:"data-flows",area:"Data",title:"Data flows between systems",summary:"Checks the data that passes between your systems' databases (the digital channel, CRM, billing, a warehouse), by batch ETL or in real time: each hop's rows arrived, complete, with every mapped column right after its rule, on time for its SLA, without duplicates. Follows one record end to end, says what a change to a table or column reaches downstream, and writes the flows from your mapping documents wherever they are, or suggests them from the schemas.",tools:["data_flows","check_flows","trace_record","read_mapping_file"],keywords:["data flow","data flows","upstream","downstream","lineage","etl","reconcile","reconciliation","mapping document","source to target","source-to-target","cdc","sync","integration","propagate","crm","trace the record","impact","kafka","topic","event stream"],examples:["Check that customers from the digital database reach the CRM correctly","Trace customer 42 from digital through CRM to billing","What does a change to digital.customers.email affect downstream?","Write the data flows from mapping.xlsx","Reconcile yesterday's orders between the ERP and the warehouse"],requires:[{kind:"databases"}],limits:["Hops between SQL tables, Kafka topics, and APIs (a target by key, a source by its list; GET or POST only); MongoDB is not checked","At most 200,000 source rows a flow per check, compared by key","Reads only; flows are written into flows.yaml, and nothing in the databases changes"],docs:[{doc:"HOW_TO_USE.md",heading:"Data flows"}],playbook:[{heading:"Data flows",when:"check data passing between databases, follow a record across systems, the impact of a change downstream, or write flows from mapping documents"}]},{id:"write-tests",area:"Automated tests",title:"Turn a passed run into an automated test",summary:"Scribe, the coding agent, writes a test for a passed browser run in the repository's own framework, page objects, and helpers; ForgeQE runs it until it passes and a reviewer approves it.",tools:["write_test","run_test"],keywords:["automated test","automation","automate","playwright","cypress","selenium","webdriverio","write a test","test code","spec","turn it into a test","turn that run into","scribe"],examples:["Write a Playwright test for that run in ../app"],requires:[{kind:"coding_agent"},{kind:"input",what:"a repository to write the test in (default: the working directory)"}],docs:[{doc:"HOW_TO_USE.md",heading:"6. Tests"}],guide:[{doc:"HOW_TO_USE.md",heading:"How Scribe writes a test"},{doc:"HOW_TO_USE.md",heading:"Generated code"}],playbook:[{heading:"Writing tests",when:"write an automated test from a run, or a session where passed runs become tests"}],alsoAfter:["browser_run","run_test_cases"]},{id:"heal-tests",area:"Automated tests",title:"Heal failing tests",summary:"Runs a repository's tests and updates the ones the application has outgrown against the live app, or reports a real application bug instead.",tools:["heal_tests"],keywords:["heal","failing test","broken test","fix the tests","fix tests","repair"],examples:["Heal the failing tests in ../app"],requires:[{kind:"coding_agent"},{kind:"input",what:"a repository with tests"}],docs:[{doc:"HOW_TO_USE.md",heading:"Fixing broken tests"}],playbook:[{heading:"Repairing broken tests",when:"automated tests that fail, broke after a release, or need healing"}]},{id:"find-run-tests",area:"Automated tests",title:"Find and run existing tests",summary:"Finds the automated tests that match a feature, tags, ids, a folder, or a sprint, confirms the selection, and runs it once the user agrees, with a verdict per test.",tools:["find_tests","run_tests","list_sprints"],keywords:["run the tests","run tests","run the","existing tests","tests in","test suite","npm test","pytest","added in sprint","checkout tests","tests added"],examples:["Run the checkout tests","Run the tests added in sprint 12"],requires:[{kind:"coding_agent"},{kind:"input",what:"a repository with tests"},{kind:"system",anyOf:["azure_devops","jira"],optional:!0,why:"to pick tests by sprint"}],docs:[{doc:"HOW_TO_USE.md",heading:"Finding and running existing tests"},{doc:"HOW_TO_USE.md",heading:"Reports for tests run outside ForgeQE"}],playbook:[{heading:"Repository tests",when:"find, list, or run the automated tests of the repository"}]},{id:"regression-packs",area:"Automated tests",title:"Regression packs for a release",summary:"Builds a regression pack from what goes into a release (an Azure DevOps sprint or work items, a commit range, pull requests, or changes read from any connected tool): the tests those changes can break, each with why, plus risk-area and smoke tests, and the changes no test covers. Runs it locally, on TestMu AI, or in a pipeline, compares each run with the last, and triages each failure: flaky, environment, broken test code, heal, or an AI browser re-run to tell a test the app outgrew from an app bug.",tools:["build_regression_pack","run_regression_pack","list_regression_packs"],keywords:["regression","pack","which tests should","release candidate","merged this week","triage","heal or rerun","why did the pack fail"],examples:["Build a regression pack for sprint 12 and run it on TestMu","Which tests should we run for the PRs merged this week?"],requires:[{kind:"input",what:"a repository with tests"},{kind:"system",anyOf:["azure_devops","jira"],optional:!0,why:"to read a sprint's or release's work items"}],docs:[{doc:"HOW_TO_USE.md",heading:"Regression packs for a release"}],playbook:[{heading:"Regression packs",when:"a regression pack, or which tests to run for a release, sprint, or pull requests"}]},{id:"flaky-tests",area:"Automated tests",title:"Flaky test detection and quarantine",summary:"Finds tests that flip between passing and failing in the repository's run history, with their likely cause, and quarantines them once the user agrees.",tools:["flaky_tests","quarantine_tests"],keywords:["flaky","quarantine","intermittent","unstable","flakes"],examples:["Which tests are flaky?","Quarantine the flaky login test"],requires:[{kind:"input",what:"a repository whose tests ForgeQE has run"}],docs:[{doc:"HOW_TO_USE.md",heading:"Flaky tests"}]},{id:"pr-review",area:"Pull requests",title:"Review pull requests",summary:"Reviews an Azure DevOps or GitHub pull request from the local clone for bugs, security, requirements, and tests, and posts the review once the user agrees.",tools:["review_pull_request","post_pr_review"],keywords:["review pr","review the pr","review pull request","pull request","code review","pr review","review the change"],examples:["Review PR 42 and focus on error handling"],requires:[{kind:"coding_agent"},{kind:"input",what:"a local clone of the repository"},{kind:"either",of:[{kind:"system",anyOf:["azure_devops"],why:"for Azure DevOps pull requests"},{kind:"command",command:"gh",why:"for GitHub pull requests"}]}],docs:[{doc:"HOW_TO_USE.md",heading:"Reviewing pull requests"}],playbook:[{heading:"Pull requests",when:"review a pull request, its impact, or the tests it can break"}],alsoAfter:["analyze_pr_impact","select_pr_tests","post_pr_test_results","comment_on_pr"]},{id:"pr-impact",area:"Pull requests",title:"Pull request impact and test selection",summary:"Works out which product areas, pages, requirements, and tests a pull request touches, picks the tests it can break, runs them, and posts the impact or results on the pull request once the user agrees.",tools:["analyze_pr_impact","select_pr_tests","post_pr_test_results","comment_on_pr"],keywords:["impact","pr ","pull request","affected","can break","what does pr","changed files"],examples:["What does PR 42 impact?","Run the tests PR 42 can break"],requires:[{kind:"input",what:"a local clone of the repository"},{kind:"either",of:[{kind:"system",anyOf:["azure_devops"],why:"for Azure DevOps pull requests and their linked work items"},{kind:"command",command:"gh",why:"for GitHub pull requests"}]}],docs:[{doc:"HOW_TO_USE.md",heading:"What a pull request impacts in the product"},{doc:"HOW_TO_USE.md",heading:"Tests for a pull request"}]},{id:"api-testing",area:"API",title:"API and contract testing",summary:"Builds API contracts (status, schema, key fields) from the calls a browser run saw or from an API description (OpenAPI, Swagger, WSDL, GraphQL, .proto, gRPC reflection), and end-to-end API scenarios from runs, descriptions, test cases, or plain English. Runs them straight against the API without a browser, or writes them as tests in the repository. REST, GraphQL, SOAP, and gRPC.",tools:["api_contracts","run_api_contracts","api_scenarios","run_api_scenarios","write_api_tests"],keywords:["api","endpoint","openapi","swagger","contract","rest","graphql","postman","request","response"],examples:["Turn the last run's API calls into tests","Test the API in openapi.yaml","Write API scenario tests for the order lifecycle"],requires:[{kind:"coding_agent",optional:!0}],limits:["Functional API testing, not load or stress testing."],docs:[{doc:"HOW_TO_USE.md",heading:"API and contract tests"},{doc:"HOW_TO_USE.md",heading:"End-to-end API scenarios"}],playbook:[{heading:"API testing",when:"API contracts, API tests, or end-to-end API scenarios (REST, GraphQL, SOAP, gRPC)"}]},{id:"accessibility-performance",area:"Quality",title:"Accessibility and page performance",summary:"Checks every page a run visits against WCAG rules (axe-core) and measures its Core Web Vitals, against budgets, with trends over time per page.",tools:["quality_report","quality_trends"],keywords:["accessible","accessibility","a11y","wcag","performance","lighthouse","slow","page speed","web vitals","load time"],examples:["Is the checkout page accessible?","Did page performance get worse this week?"],requires:[{kind:"input",what:"a browser run (from browser_run or run_test_cases); new runs need a live model"}],limits:["Per-page Core Web Vitals, not load testing under many users."],docs:[{doc:"HOW_TO_USE.md",heading:"Accessibility and performance budgets"}],playbook:[{heading:"Accessibility, performance, and visual changes",when:"accessibility, Core Web Vitals, performance budgets, or visual changes of pages"}],alsoAfter:["visual_compare","visual_approve"]},{id:"visual-comparison",area:"Quality",title:"Visual comparison with baselines",summary:"Compares each page a run captures with its approved baseline and shows what changed; intended changes become the new baseline once the user agrees.",tools:["visual_compare","visual_approve"],keywords:["visual","visually","baseline","looks different","approve the new","pixel","screenshot diff","layout"],examples:["What changed visually since the last run?","Approve the new header"],requires:[{kind:"input",what:"a browser run (from browser_run or run_test_cases); new runs need a live model"}],docs:[{doc:"HOW_TO_USE.md",heading:"Visual comparison"}]},{id:"defect-intelligence",area:"Quality",title:"Defect intelligence: trends and prediction",summary:"Groups failures into defects, keeps their history across suites (new, known, gone, back again), and gives defect trends and where a change is likely to cause defects, with the cases to run first.",tools:["defect_trends","predict_defects"],keywords:["defect","bugs keep","trend","predict","coming back","root cause","rca","hotspot","since main","changes since","can the changes"],examples:["Which defects keep coming back?","What can the changes since main break?"],requires:[{kind:"input",what:"suites run into one folder (suite.out_dir)"},{kind:"command",command:"git",optional:!0,why:"for predictions from the product's repository"}],limits:["Prediction is a heuristic from the saved suites and the change, not a trained model; its accuracy shows once suites have scored it."],docs:[{doc:"HOW_TO_USE.md",heading:"Bugs for failures"},{doc:"HOW_TO_USE.md",heading:"Defect trends and prediction"}]},{id:"release-readiness",area:"Release",title:"Requirement coverage and release readiness",summary:"Gives requirement coverage and a GO / GO WITH RISKS / NO-GO verdict for a sprint, area, epic, or stories, from test cases, results, bugs, and flaky tests; can publish it to the wiki.",tools:["release_readiness"],keywords:["release","ready to release","coverage","go/no-go","no-go","readiness","requirement coverage","ship"],examples:["Are we ready to release?","Coverage of sprint 12"],requires:[{kind:"system",anyOf:["azure_devops","jira"]}],docs:[{doc:"HOW_TO_USE.md",heading:"Requirement coverage and release readiness"}]},{id:"monitoring",area:"Operations",title:"Scheduled monitoring with alerts",summary:"Runs checks on a schedule and alerts Teams or email when one keeps failing, and again when it recovers.",tools:["monitor"],keywords:["monitor","every minute","every hour","every day","every night","every 5 minutes","every 15 minutes","every 30 minutes","schedule","uptime","alert","cron","minutes","hourly","nightly"],examples:["Check the login flow every 15 minutes and alert Teams"],requires:[{kind:"live_model"},{kind:"monitor_alerts",optional:!0}],docs:[{doc:"HOW_TO_USE.md",heading:"9. Monitoring"}],playbook:[{heading:"Monitoring",when:"checks on a schedule, with alerts"}]},{id:"jobs",area:"Operations",title:"Background jobs on a schedule",summary:"Runs ForgeQE commands (suites, monitor rounds, regression packs, healing, pull-request tests, browser goals) on a schedule or once in the jobs service, apart from the conversation and after it closes, and watches an existing GitHub Actions or Azure DevOps pipeline (read-only). A failed pipeline run or pack run is investigated: the healing agent works on each failure on a local branch of its own (a fix for review, or an app bug), never pushing. Says how each job goes when asked, shows runs as they end, and sends failures, recoveries and findings to Teams or email.",tools:["jobs"],keywords:["job","jobs","background","in the background","every weekday","every night at","keep running","while i work","scheduled job","run it nightly","watch the pipeline","pipeline health","investigate failures"],examples:["Run the release-2.3 pack every weekday at 2am in the background","Watch the pipeline in the background and investigate failures","How are my jobs doing?"],requires:[{kind:"notify_channels",optional:!0}],docs:[{doc:"HOW_TO_USE.md",heading:"The jobs service: scheduled and long jobs apart from the shell"}],playbook:[{heading:"Background jobs",when:"work on a schedule or in the background, or how a job is going"}]},{id:"notifications",area:"Operations",title:"Teams and email notifications",summary:"Tells the team in Microsoft Teams channels or by email when a suite fails or passes, when a release readiness verdict is in, and when the domain evaluation falls below its gate, with the failing cases, their bugs, and links to the report and the CI build. Each channel picks its events; the agent also posts a summary to a channel when asked.",tools:["notify"],keywords:["teams","email","notify","post the results","channel","tell the","send the"],examples:["Post the results to the QA channel","Tell the release channel the verdict is NO-GO"],requires:[{kind:"notify_channels"}],limits:["Automatic notifications come from runs nobody is watching (forgeqe suite, forgeqe run, pipelines, the MCP server), not from the interactive shell, where the agent posts only when asked."],docs:[{doc:"HOW_TO_USE.md",heading:"17. Notifications"}]},{id:"reports",area:"Release",title:"Reports you can share",summary:"Writes up what the Orchestrator found, such as open defects across projects, release status, or pipeline health, as a page with filters (project, state, priority, assignee, \u2026), charts, and a sortable table, plus Markdown and CSV. Every table is built from evidence: the results of the tool calls the agent made, or a read-only query ForgeQE runs itself, listed on the page's Sources with each raw result kept beside the report.",tools:["write_report"],keywords:["report","write up","write-up","summary","share","pdf","html report"],examples:["Create a release report for all projects","Write up the open defects as a report I can share"],requires:[{kind:"live_model"}],limits:["Rows come only from tool results in the session or a query ForgeQE runs; the agent writes only the summary, which is not checked figure by figure.","A WIQL query reads up to 1000 work items; the page says when a query matched more.","The page is not published anywhere unless the reports folder is served (notify.report_url)."],docs:[{doc:"HOW_TO_USE.md",heading:"Reports from the agent"}],guide:[{doc:"HOW_TO_USE.md",heading:"Team report"},{doc:"README.md",heading:"Static BI"},{doc:"README.md",heading:"One report for the whole team"},{doc:"HOW_TO_USE.md",heading:"Model prices and usage"},{doc:"HOW_TO_USE.md",heading:"Spending budgets"}]},{id:"pipelines",area:"Operations",title:"CI pipelines",summary:"Writes a GitHub Actions or Azure Pipelines workflow that runs ForgeQE, stores the secrets it needs, pushes a branch, and makes the pipeline green.",tools:["push_branch","set_ci_secret"],keywords:["pipeline","github actions","azure pipelines","ci","gitlab","jenkins","make it green","ci secret","workflow file","on every pr"],examples:["Add a GitHub Actions workflow that runs tests/cases.yaml on every PR, and make it green"],requires:[{kind:"command",command:"git"},{kind:"either",of:[{kind:"command",command:"gh",why:"for GitHub Actions"},{kind:"command",command:"az",why:"for Azure Pipelines"}]}],docs:[{doc:"HOW_TO_USE.md",heading:"8. Pipelines"}],playbook:[{heading:"CI pipelines",when:"a GitHub Actions or Azure DevOps pipeline that runs ForgeQE or the tests"}]},{id:"systems",area:"Systems",title:"Work with Azure DevOps, Jira, GitHub, and Confluence",summary:"Reads anything in the configured systems (work items, issues, queries, boards, pages), makes changes the user asks for, puts run evidence (screenshots, reports) on work items and issues as attachments or comments, and builds Azure DevOps dashboards and wiki pages.",tools:["read_confluence","create_ado_dashboard","write_ado_wiki_page","attach_to_work_item"],keywords:["jira","azure devops","ado","confluence","system","github issue","work item","bug","sprint","dashboard","wiki","comment","backlog","issue","ticket","requirement"],examples:["Show the open bugs in this sprint","Read the checkout requirements from Confluence","Make a dashboard of this sprint's test results","Add the last screenshot of that run to ADO 256 as a comment","Run the GitHub issues labelled login as test cases"],requires:[{kind:"system",anyOf:["azure_devops","jira","xray","confluence"]}],docs:[{doc:"README.md",heading:"Azure DevOps, Jira, and other systems"}],playbook:[{heading:"Releases and pipelines in Azure DevOps",when:"releases, deployments, environments, or pipeline runs in Azure DevOps"},{heading:"Dashboards and wiki pages in Azure DevOps",when:"an Azure DevOps dashboard or wiki page"}]},{id:"system-data",area:"Systems",title:"Read any data in Azure DevOps, Jira, Xray, and Confluence",summary:"Reads through a system's REST API what its MCP tools do not cover: pull requests and repositories, test plans and suites, classic releases, and anything else in Azure DevOps, Jira, Xray, or Confluence. Given on every turn once a system is set up, so the agent never turns to the browser or the shell to read it.",tools:["query_api"],keywords:["pull request","pull requests","prs","open pr","repos","repository","branch","commit","test plan","test plans","release"],examples:["Are there open pull requests in any repository of QI-Claims?","List the test plans in QI-MobileBanking with their suites"],requires:[{kind:"system",anyOf:["azure_devops","jira","xray","confluence"]}],docs:[{doc:"README.md",heading:"Azure DevOps, Jira, and other systems"}]},{id:"generated-code",area:"Setup",title:"What ForgeQE wrote",summary:"Says how much of the code ForgeQE wrote is still in the repository, and gives the trailer and footer that mark a commit message or pull request description covering code it wrote. Works with any repository, whatever system the team uses.",tools:["generated_attribution"],keywords:["commit message","pull request description","pr description","attribution","generated code","how much code","code forgeqe wrote","fq:","provenance"],examples:["How much of the code ForgeQE wrote is still there?","Write the commit message for these tests"],requires:[],docs:[{doc:"HOW_TO_USE.md",heading:"Generated code"}]},{id:"forgeqe-commands",area:"Setup",title:"Every forgeqe command from the shell",summary:"Runs the forgeqe commands no other tool covers when the user asks in words: the context budget, guardrails, prompts (show, diff, check, reset), the domain eval, skills on or off, session logs, saved API sets and packs, pack pipelines, the BI site, a test notification, desktop and mobile doctor and validate, and updating ForgeQE. Reading commands run at once; the others are shown and asked first.",tools:["forgeqe_command"],keywords:["context budget","guardrail","prompt","prompts","eval domain","domain eval","enable skill","disable skill","session log","logs","api list","contract sets","pack show","pack pipeline","bi","evidence","dashboard site","test notification","notify test","doctor","validate","update forgeqe","upgrade","new version","forgeqe command","run forgeqe"],examples:["Show me the context budget","Add a guardrail: never delete test data in prod","Diff the brain prompt against ForgeQE's","Is there a new version of ForgeQE?","Run the mobile doctor","Build the BI site"],requires:[],docs:[{doc:"HOW_TO_USE.md",heading:"15. Commands cheat sheet"},{doc:"HOW_TO_USE.md",heading:"Guardrails"}],guide:[{doc:"HOW_TO_USE.md",heading:"Your prompts, skills, and product knowledge"}]},{id:"setup-help",area:"Setup",title:"Setup, skills, and documentation",summary:"Shows the model profiles and MCP servers, installs or repairs the browser engine, creates a forgeqe.yaml, manages skills, and answers any question about ForgeQE from its documentation, this list, and its own code (the prompts, skills, and source it runs).",tools:["forgeqe_setup","capabilities","playbook","use_capabilities"],keywords:["help","what can you","how do i","how does forgeqe","set up","setup","configure","model","profile","skill","install","icons","agents","version","internals"],examples:["What can you do?","Which model am I using?","How do I set up email alerts?","Show me the icons of all agents","Which version of ForgeQE is this?"],requires:[],docs:[{doc:"HOW_TO_USE.md",heading:"2. Scaffold config"},{doc:"HOW_TO_USE.md",heading:"11. Skills"},{doc:"HOW_TO_USE.md",heading:"Model routing"}],guide:[{doc:"README.md",heading:"Privacy"},{doc:"docs/MODEL_ROUTING.md",heading:"Model routing: the right configured model for each task"},{doc:"docs/MODEL_ROUTING.md",heading:"Turn it on"},{doc:"docs/MODEL_ROUTING.md",heading:"The areas of work"},{doc:"docs/MODEL_ROUTING.md",heading:"How a model is picked"},{doc:"docs/MODEL_ROUTING.md",heading:"How ForgeQE learns the models"},{doc:"docs/MODEL_ROUTING.md",heading:"Limit what an area may use"},{doc:"docs/MODEL_ROUTING.md",heading:"What it does not do"}]}],Gj=[{what:"Load, stress, or soak testing with many simultaneous users",instead:"per-page Core Web Vitals with budgets"},{what:"Penetration testing or dynamic security scanning",instead:"security findings in pull request reviews"}]});function Ste(e,t){let n=e.routing,r=n===void 0||n===null||n===""||n===!1?"off":n===!0?"auto":String(n).toLowerCase();if(!["off","shadow","auto"].includes(r))throw Error(`llm.routing in forgeqe.yaml is "${String(n)}". Use off, shadow or auto.`);let s={},o=e.routes;if(o!==void 0&&o!==null){if(typeof o!=="object"||Array.isArray(o))throw Error(`llm.routes in forgeqe.yaml must map areas (${Ty.join(", ")}) to auto or profile names.`);for(let[a,l]of Object.entries(o)){if(!Ty.includes(a))throw Error(`llm.routes.${a} in forgeqe.yaml is not an area. The areas are ${Ty.join(", ")}.`);if(l==="auto"||l===void 0||l===null){s[a]="auto";continue}let c=(Array.isArray(l)?l:[l]).map((f)=>String(f).trim()).filter(Boolean),d=c.filter((f)=>!t.includes(f));if(d.length)throw Error(`llm.routes.${a} in forgeqe.yaml names ${d.map((f)=>`"${f}"`).join(", ")}, not a profile in llms.json. Run: forgeqe models`);if(!c.length)throw Error(`llm.routes.${a} in forgeqe.yaml is empty. Give auto or profile names.`);s[a]=[...new Set(c)]}}return{mode:r,routes:s,...n===void 0||n===null||n===""?{unset:!0}:{}}}var Cy,Ty;var tC=ee(()=>{Cy={agent:{title:"Orchestrator: the shell, forgeqe run and forgeqe mcp",bar:4,tools:!0},browser:{title:"Driving a browser, desktop app or mobile app from screenshots",bar:4,vision:!0,tools:!1},coding:{title:"Writing, healing and fixing tests in a repository",bar:5,tools:!0},review:{title:"Reviewing tests and pull requests, judging results",bar:3,tools:!0},analysis:{title:"Root cause of defects and failures",bar:5,tools:!0},planning:{title:"Refining requests, finding tests, planning scenarios and charters",bar:3,tools:!0}},Ty=Object.keys(Cy)});import{existsSync as kte,readdirSync as $De,readFileSync as xte}from"fs";import Vj from"path";function Jb(e,t){if(e===void 0||e===null)return{};if(typeof e!=="object"||Array.isArray(e))throw Error(`${t} must be a mapping`);return e}function cc(e,t){if(e===void 0||e===null||e==="")return;if(typeof e!=="string"&&typeof e!=="number")throw Error(`${t} must be text`);return String(e)}function $te(e,t){let n;try{n=new URL(e)}catch{throw Error(`${t} must be a full http(s) URL, e.g. https://qa.shop.example.com`)}if(n.protocol!=="https:"&&n.protocol!=="http:")throw Error(`${t} must be an http(s) URL, not ${n.protocol}`);if(n.username||n.password)throw Error(`${t} must not carry a user name or password; use logins: in forgeqe.yaml`);return n.href}function TDe(e,t,n){if(!rz.test(e))throw Error(`${n}: app names are letters, digits and _ . -`);let r=Jb(t,n),s=String(r.kind||"");if(s!=="web"&&s!=="desktop"&&s!=="mobile"&&s!=="api")throw Error(`${n}.kind must be web, desktop, mobile or api`);let o={name:e,kind:s,...cc(r.description,`${n}.description`)?{description:cc(r.description,`${n}.description`)}:{}},a=cc(r.login,`${n}.login`);if(a)o.login=a;if(s==="web"||s==="api"){let l=cc(r.url,`${n}.url`);if(!l)throw Error(`${n}.url is required for a${s==="api"?"n api":" web"} app`);o.url=$te(l,`${n}.url`)}else if(s==="desktop"){let l=cc(r.path,`${n}.path`);if(!l)throw Error(`${n}.path is required for a desktop app (an absolute path to its .exe)`);if(!/^([A-Za-z]:[\\/]|\\\\|\/)/.test(l)||!/\.exe$/i.test(l)||/[\x00-\x1f]/.test(l))throw Error(`${n}.path must be an absolute path to an .exe; .bat, .cmd and scripts run through a shell`);o.path=l;let c=r.args??[];if(!Array.isArray(c)||!c.every((f)=>typeof f==="string"||typeof f==="number"))throw Error(`${n}.args must be a list of arguments, not one command string`);o.args=c.map(String);let d=cc(r.window,`${n}.window`);if(d)o.window=d}else{let l=String(r.platform||"");if(l!=="android"&&l!=="ios")throw Error(`${n}.platform must be android or ios`);let c=cc(r.app_id??r.appId,`${n}.app_id`);if(!c||!/^[A-Za-z0-9_.-]+$/.test(c))throw Error(`${n}.app_id must be the app's package or bundle id`);o.platform=l,o.appId=c;let d=cc(r.activity,`${n}.activity`);if(d){if(l!=="android"||!/^[A-Za-z0-9_.$]{1,200}$/.test(d))throw Error(`${n}.activity must be an Android activity name such as .MainActivity`);o.activity=d}let f=cc(r.app??r.app_file,`${n}.app`);if(f){if(/[\x00-\x1f]/.test(f)||!(/^https:\/\//i.test(f)||/^(bs|lt):\/\/[A-Za-z0-9._-]+$/.test(f)||/^storage:filename=[A-Za-z0-9._-]+$/.test(f)||Vj.isAbsolute(f)&&/\.(apk|aab|ipa|app|zip)$/i.test(f)))throw Error(`${n}.app must be an absolute path to an .apk, .aab, .ipa, .app or .zip, an https URL, or a device cloud upload (bs://\u2026)`);o.appFile=f}}return o}function CDe(e,t){let n=`env.yaml environments.${e}`;if(!rz.test(e))throw Error(`${n}: environment names are letters, digits and _ . -`);let r=Jb(t,n),s={};for(let[f,m]of Object.entries(Jb(r.apps,`${n}.apps`)))s[f]=TDe(f,m,`${n}.apps.${f}`);let o={};for(let[f,m]of Object.entries(Jb(r.variables,`${n}.variables`))){if(!ZH.test(f))throw Error(`${n}.variables: "${f}" must be letters, digits and _`);let h=cc(m,`${n}.variables.${f}`);o[f]=h??""}let a=r.secrets??[];if(!Array.isArray(a)||!a.every((f)=>typeof f==="string"&&ZH.test(f)))throw Error(`${n}.secrets must be a list of environment variable names (letters, digits and _); env.yaml holds no secret values`);let l=(f,m)=>{let h=cc(r[f],`${n}.${f}`);if(h){if(s[h]?.kind!==m)throw Error(`${n}.${f} must name one of its ${m} apps`);return h}let y=Object.values(s).filter((w)=>w.kind===m);return y.length===1?y[0].name:void 0},c=l("start_app","web"),d=l("api_app","api");return{name:e,...cc(r.description,`${n}.description`)?{description:cc(r.description,`${n}.description`)}:{},apps:s,...c?{startApp:c}:{},...d?{apiApp:d}:{},variables:o,secrets:[...new Set(a)]}}function Cte(e,t){let n={...e};for(let[r,s]of Object.entries(t)){let o=n[r];n[r]=o&&s&&typeof o==="object"&&typeof s==="object"&&!Array.isArray(o)&&!Array.isArray(s)?Cte(o,s):s}return n}function tz(e,t=""){if(e&&typeof e==="object"&&!Array.isArray(e))return Object.fromEntries(Object.entries(e).map(([n,r])=>[n,tz(r,n)]));if(Array.isArray(e))return e.map((n)=>tz(n,t));if(typeof e==="string"){if(Qj.test(t))return Tte.test(e)?e.trim():"<secret, not shown>";if(ez.test(e))return e.replace(ez,(n)=>n.startsWith("//")?"//<user>:<secret>@":`${n.split("=")[0]}=<secret>`)}return e}function ODe(e,t){let n={},r={},s=new Set,o=[],a,l=(y)=>{let w=Ete(y.name)||y.kind;for(let v=2;n[w];v++)w=`${Ete(y.name)||y.kind}${v}`;return n[w]={...y,name:w},w},c=(y,w)=>{if(y&&typeof y==="object"&&!Array.isArray(y)){let v=y;for(let[S,R]of Object.entries(v)){if(/^(description|desc)$/i.test(S)&&w.length===0)continue;let j=[...w,S];if(typeof R==="string"){let O=R.trim();if(Qj.test(S)){let M=Tte.exec(O),W=M&&(M[1]||M[2]||M[3]||M[4]||M[5]);if(W)s.add(W);else if(O)o.push(j.join("."));continue}if(ez.test(O)){o.push(j.join("."));continue}let D=(M,W)=>(M.test(S)?w[w.length-1]||"app":S.replace(W,""))||S;if(/^https?:\/\//i.test(O)){try{let M=$te(O,j.join(".")),W=D(YH,/[_-]?(base[_-]?)?(url|uri|endpoint|host)$/i),Z=/api/i.test(j.join("."))||/\/api(\/|$)/i.test(new URL(M).pathname)?"api":"web",G=typeof v.description==="string"?v.description:void 0,re=l({name:W,kind:Z,url:M,...YH.test(S)&&G?{description:G}:{}});if(w.length===0&&YH.test(S)&&Z==="web"&&!a)a=re}catch{}continue}if(/^([A-Za-z]:[\\/]|\\\\|\/).*\.exe$/i.test(O)&&!/[\x00-\x1f]/.test(O)){let M=Array.isArray(v.args)?v.args:Array.isArray(v.arguments)?v.arguments:[];l({name:D(RDe,/[_-]?(path|exe)$/i),kind:"desktop",path:O,args:M.filter((W)=>typeof W==="string"||typeof W==="number").map(String),...typeof v.description==="string"?{description:v.description}:{}});continue}if(O.length<=500&&!/[\r\n]/.test(O))r[XH(j.join("_"))]=O}else if(typeof R==="number"||typeof R==="boolean"){if(!Qj.test(S))r[XH(j.join("_"))]=String(R)}else if(Array.isArray(R)){if(/^(args|arguments)$/i.test(S))continue;if(/^secrets?$/i.test(S)){for(let O of R){if(typeof O!=="string"||!ZH.test(O))throw Error(`env.yaml ${e}: ${[...j].join(".")} must be a list of environment variable names (letters, digits and _); env.yaml holds no secret values`);s.add(O)}continue}if(!Qj.test(S)&&R.every((O)=>typeof O==="string"||typeof O==="number")&&R.length<=50)r[XH(j.join("_"))]=R.join(", ")}else if(R&&typeof R==="object")c(R,j)}}};c(t,[]);let d=_n.stringify(tz(t)).trim(),f={name:e,...typeof t.description==="string"?{description:t.description}:{},apps:n,variables:r,secrets:[...s],details:d.length>6000?`${d.slice(0,6000)}
|
|
182
|
+
`)}class Uj{cfg;repo;w;name=null;starting=null;dir=null;constructor(e,t,n){this.cfg=e;this.repo=t;this.w=n}get image(){return this.w.image}get settings(){return JSON.stringify(this.w)}update(e){this.cfg=e}async stop(){let e=this.name;if(this.name=null,this.starting=null,this.close(),!e)return;let t=Vc(this.cfg);if(t)await GH(t.bin,["rm","--force",e],{env:Wo({cfg:this.cfg,runtime:t,exec:GH}),timeoutMs:30000}).catch(()=>{return});ZT(e)}ctx(e){let t=Vc(this.cfg);if(!t)throw Error(`containers.workspace is set, but ${Kr(this.cfg).runtime||"Docker or Podman"} is not on PATH. Install one, or remove containers.workspace to work on this machine.`);return{cfg:this.cfg,runtime:t,exec:GH,...e?{log:e}:{}}}async ensure(e){let t=this.ctx(e);if(this.name){let n=await t.exec(t.runtime.bin,["inspect","--format","{{.State.Running}}",this.name],{env:Wo(t),timeoutMs:15000});if(n.code===0&&n.stdout.trim()==="true")return this.name;ZT(this.name),this.name=null,this.starting=null}return this.starting||=this.start(t).catch((n)=>{throw this.starting=null,n}),this.name=await this.starting}async start(e){let t=wf(Kr(this.cfg).root),n=Wo(e);if(await Vb(e,t),this.w.services.length){let d=await Ij(e,Sy(Kr(this.cfg).services,this.w.services));if(d.failed)throw Error(`${d.failed.service} did not start for the workspace: ${d.failed.error}`)}let r=await this.ensureHome(e,t),s=YT({name:"containers.workspace",secrets:this.w.secrets},this.cfg,process.env),o=`forgeqe-${t}-workspace-${iDe(3).toString("hex")}`;e.log?.(`Starting the workspace container (${this.w.image})`);let a=hte(),l=mDe(this.w,{key:t,name:o,repo:this.repo,home:r,masks:Oj(this.repo),byName:Object.keys(s),...a?{uid:a}:{}}),c=await e.exec(e.runtime.bin,l,{env:{...n,...s},timeoutMs:900000});if(c.code!==0)throw Error(`The workspace container did not start: ${Uk((c.stderr||c.stdout).trim(),Object.values(s)).slice(-1500)}`);return Mj(e,o),o}async ensureHome(e,t){let n=mte(t),r=Wo(e);if((await e.exec(e.runtime.bin,["volume","inspect",n],{env:r,timeoutMs:15000})).code===0)return n;let o=await e.exec(e.runtime.bin,["volume","create","--label","forgeqe=1","--label",`forgeqe.project=${t}`,n],{env:r,timeoutMs:30000});if(o.code!==0)throw Error(`Could not create the volume ${n}: ${o.stderr.trim().slice(0,300)}`);let a=hte();if(a){let l=await e.exec(e.runtime.bin,["run","--rm","--user","0:0","--network","none","--volume",`${n}:${Kb}`,"--entrypoint","chown",this.w.image,a,Kb],{env:r,timeoutMs:600000});if(l.code!==0)throw Error(`Could not prepare the volume ${n}: ${l.stderr.trim().slice(0,300)}`)}return n}async shell(e){let t=await this.ensure(e),n=this.ctx(e);this.dir||=lDe(WH.join(pDe.tmpdir(),"forgeqe-workspace-"));let r=WH.join(this.dir,`sh-${t}`);if(!gte(r)){let s=[...new Set([...Object.keys(process.env).filter((o)=>fDe.test(o)),...Object.keys($y())])].filter((o)=>/^[A-Za-z_][A-Za-z0-9_]*$/.test(o)).sort();dDe(r,hDe({bin:n.runtime.bin,container:t,repo:this.repo,names:s}),{mode:448}),aDe(r,448)}return{path:r,note:gDe(this.w.image,this.repo)}}runTest=async(e)=>{let t=this.ctx();await this.ensure();let n=wf(Kr(this.cfg).root);return qj({...t,...e.signal?{signal:e.signal}:{}},this.repo,[],{image:this.w,home:mte(n)})(e)};close(){if(this.dir)uDe(this.dir,{recursive:!0,force:!0});this.dir=null}}function yDe(e){let t=e.code?.repo||Kr(e).root;return gte(t)?cDe(t):WH.resolve(t)}function yte(e){if(process.platform==="win32")return;let t=yDe(e),n=e.containers?.workspace,r=qk(t);if(n&&r instanceof Uj&&r.repo===t&&r.settings===JSON.stringify(n))return r.update(e);if(r instanceof Uj&&r.repo===t)r.stop();tte(t,n?new Uj(e,t,n):null)}var fDe,mte=(e)=>`forgeqe-${e}-home`,hte=()=>typeof process.getuid==="function"?`${process.getuid()}:${process.getgid?.()??process.getuid()}`:void 0,Fj=(e)=>`'${e.replace(/'/g,"'\\''")}'`,GH;var wte=ee(()=>{sp();Rj();rp();DH();bf();zH();fDe=/^FORGEQE_(ACTIVE_ENV$|APP_|VAR_)/;GH=xy});import bte from"path";function xi(e,t){if(e===void 0||e===null||e==="")return;if(typeof e!=="string"&&typeof e!=="number")throw Error(`${t} must be text`);return String(e).trim()||void 0}function QH(e,t){let n=xi(e,t);if(n!==void 0&&!bDe.test(n))throw Error(`${t} must name an environment variable (letters, digits and _), not hold its value`);return n}function SDe(e,t,n,r){if(!wDe.test(e))throw Error(`${n}: database names are letters, digits and _ . -`);if(!t||typeof t!=="object"||Array.isArray(t))throw Error(`${n} must be a mapping`);let s=t;for(let R of Object.keys(s))if(_De.includes(R.toLowerCase()))throw Error(`${n}.${R}: no secrets in config files. Put it in an environment variable and name it with ${R.startsWith("p")?"password_env":"url_env"} (save it with /auth NAME).`);let o=xi(s.engine,`${n}.engine`)?.toLowerCase();if(o!==void 0&&!VH.includes(o))throw Error(`${n}.engine must be one of ${VH.join(", ")}`);let a=(xi(s.auth,`${n}.auth`)||"password").toLowerCase();if(!["password","azure","aws_iam","none"].includes(a))throw Error(`${n}.auth must be password, azure, aws_iam or none`);let l=xi(s.ssl,`${n}.ssl`)?.toLowerCase();if(l!==void 0&&!["verify","require","disable"].includes(l))throw Error(`${n}.ssl must be verify, require or disable`);let c=xi(s.host,`${n}.host`);if(c!==void 0&&!(o==="kafka"?/^[A-Za-z0-9.-]{1,253}(:\d{1,5})?(,[A-Za-z0-9.-]{1,253}(:\d{1,5})?){0,20}$/:vDe).test(c))throw Error(`${n}.host must be a host name or address`);let d=s.port===void 0||s.port===null?void 0:Number(s.port);if(d!==void 0&&!(Number.isInteger(d)&&d>0&&d<65536))throw Error(`${n}.port must be a port number`);let f=QH(s.url_env,`${n}.url_env`);if(!f&&!o)throw Error(`${n}: set engine (${VH.join(", ")}), or url_env with a connection string`);let m=xi(s.path,`${n}.path`);if((o==="sqlite"||o==="duckdb")&&!m&&!f)throw Error(`${n}.path: the ${o} file`);let h=s.timeout_seconds===void 0?30:Number(s.timeout_seconds);if(!(Number.isInteger(h)&&h>0&&h<=3600))throw Error(`${n}.timeout_seconds must be 1 to 3600`);let y=s.sample_rows===void 0?0:Number(s.sample_rows);if(!(Number.isInteger(y)&&y>=0&&y<=5))throw Error(`${n}.sample_rows must be 0 to 5`);let w=s.mask===void 0||s.mask===null?[]:s.mask;if(!Array.isArray(w)||!w.every((R)=>typeof R==="string"))throw Error(`${n}.mask must be a list of column name patterns`);for(let R of w)try{new RegExp(R)}catch{throw Error(`${n}.mask: "${R}" is not a valid pattern`)}let v={},S=s.options===void 0||s.options===null?{}:s.options;if(typeof S!=="object"||Array.isArray(S))throw Error(`${n}.options must be a mapping`);for(let[R,j]of Object.entries(S)){if(!vte.has(R))throw Error(`${n}.options.${R} is not a known option (${[...vte].join(", ")})`);v[R]=String(xi(j,`${n}.options.${R}`)||"")}return{name:e,...o?{engine:o}:{},...xi(s.description,`${n}.description`)?{description:xi(s.description,`${n}.description`)}:{},...f?{urlEnv:f}:{},...c?{host:c}:{},...d?{port:d}:{},...xi(s.database,`${n}.database`)?{database:xi(s.database,`${n}.database`)}:{},...xi(s.user,`${n}.user`)?{user:xi(s.user,`${n}.user`)}:{},...QH(s.password_env,`${n}.password_env`)?{passwordEnv:QH(s.password_env,`${n}.password_env`)}:{},...m?{path:bte.resolve(r,m)}:{},...xi(s.schema,`${n}.schema`)?{schema:xi(s.schema,`${n}.schema`)}:{},auth:a,...xi(s.region,`${n}.region`)?{region:xi(s.region,`${n}.region`)}:{},...l?{ssl:l}:{},...xi(s.ssl_ca,`${n}.ssl_ca`)?{sslCa:bte.resolve(r,xi(s.ssl_ca,`${n}.ssl_ca`))}:{},writable:s.writable===!0,...s.vector===!0?{vector:!0}:{},timeoutSeconds:h,sampleRows:y,mask:w,options:v}}function Bj(e,t,n){if(e===void 0||e===null)return{};if(typeof e!=="object"||Array.isArray(e))throw Error(`${t} must be a mapping of database names to settings`);let r={};for(let[s,o]of Object.entries(e))r[s]=SDe(s,o,`${t}.${s}`,n);return r}function va(e){let t=e.environments,n=t?.active?t.all[t.active]:void 0;return{...e.databases||{},...n?.databases||{}}}function Vo(e,t){let n=va(e),r=Object.keys(n);if(!r.length)throw Error("No databases are set up: add one under databases: in forgeqe.yaml or the environment in env.yaml (HOW_TO_USE.md, Databases).");let s=String(t||"").trim();if(!s){if(r.length===1)return n[r[0]];throw Error(`Name the database: ${r.join(", ")}.`)}let o=n[s]||Object.values(n).find((a)=>a.name.toLowerCase()===s.toLowerCase());if(!o)throw Error(`No database "${s}" (databases: ${r.join(", ")}).`);return o}function Hj(e){return{name:e.name,...e.engine?{engine:e.engine}:{},...e.urlEnv?{url_env:e.urlEnv}:{},...e.host?{host:e.host}:{},...e.port?{port:e.port}:{},...e.database?{database:e.database}:{},...e.user?{user:e.user}:{},...e.passwordEnv?{password_env:e.passwordEnv}:{},...e.path?{path:e.path}:{},...e.schema?{schema:e.schema}:{},auth:e.auth,...e.region?{region:e.region}:{},...e.ssl?{ssl:e.ssl}:{},...e.sslCa?{ssl_ca:e.sslCa}:{},writable:e.writable,timeout_seconds:e.timeoutSeconds,options:e.options}}var VH,wDe,bDe,vDe,_De,vte;var Qc=ee(()=>{VH=["postgres","cockroachdb","redshift","mysql","mariadb","mssql","oracle","sqlite","duckdb","snowflake","mongodb","kafka"],wDe=/^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/,bDe=/^[A-Za-z_][A-Za-z0-9_]{0,63}$/,vDe=/^[A-Za-z0-9.-]{1,253}$/,_De=["password","pwd","url","uri","connection_string","connectionstring","token","secret"],vte=new Set(["warehouse","role","odbc_driver","security_protocol","sasl_mechanism"])});import{existsSync as kDe,readFileSync as xDe}from"fs";import EDe from"path";function zj(e){let t=EDe.join(e,".env");if(!kDe(t))return;let n=xDe(t,"utf8");for(let r of n.split(/\r?\n/)){let s=r.trim();if(!s||s.startsWith("#"))continue;let o=s.indexOf("=");if(o<=0)continue;let a=s.slice(0,o).trim(),l=s.slice(o+1).trim();if(l.startsWith('"')&&l.endsWith('"')||l.startsWith("'")&&l.endsWith("'"))l=l.slice(1,-1);if(process.env[a]===void 0)process.env[a]=l}}var _te=()=>{};function JH(e){return Lr.find((t)=>t.id===e)}var KH,Lr,Gj;var Kc=ee(()=>{KH=["docs/DESKTOP.md","docs/DESKTOP-CI.md","docs/MOBILE.md","docs/CONTAINERS.md","docs/MODEL_ROUTING.md"],Lr=[{id:"browser-goals",area:"Browser",title:"Run any goal in a real browser",summary:"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.",tools:["browser_run","run_details","page_memory","browser_devices"],keywords:["browser","website","web site","web app","page","url","go to","open the","click","search","chrome","edge","firefox","device","testmu","browserstack","sign up","checkout","site","form","cart"],examples:["Go to booking.com and search hotels in Mumbai for next weekend","What does ForgeQE know about the checkout page?","Add Chrome on Windows 11 and Chrome 128 on Windows 10 as TestMu devices, then run SHOP-12 on both"],requires:[{kind:"live_model"}],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."],docs:[{doc:"HOW_TO_USE.md",heading:"4. Without the shell"},{doc:"HOW_TO_USE.md",heading:"5. Page memory"}]},{id:"logins",area:"Browser",title:"Saved logins for the application under test",summary:"Signs the browser agent in with saved credentials (including TOTP) and reuses the session; a user can also sign in by hand once in a browser.",tools:["logins"],keywords:["login","log in","sign in","signed in","session","credential","password","saved login","forget the saved"],examples:["Sign in to the admin site and keep the session","Forget the saved session for shop.example.com"],requires:[],docs:[{doc:"HOW_TO_USE.md",heading:"Signing in"}]},{id:"record-web",area:"Browser",title:"Record a web test by hand",summary:"Opens a browser with the Recorder panel docked in the page while the user does the steps of a web test themself, and saves their clicks, typing, choices and keys in order as a run, with the checks, kept values, test data and notes they add; the coding agent then turns it into a test in the repository's framework.",tools:["web_record"],keywords:["record","recording","recorder","show you the steps","i'll show you","do the steps myself","do it myself","my steps","codegen"],examples:["I'll show you the checkout steps in the shop: record them","Record my steps on the cart page and make a test of it"],requires:[{kind:"coding_agent",optional:!0}],limits:["Local Chrome or Chromium only, one recording at a time; drag and drop and steps inside iframes are not recorded yet"],docs:[{doc:"HOW_TO_USE.md",heading:"Recording a web test by hand"}]},{id:"run-test-cases",area:"Test cases",title:"Run manual test cases in browsers",summary:"Reads test cases from Azure DevOps, Jira, Xray, TestRail, Zephyr Scale, qTest, a file, or pasted text, runs each in a real browser with a verdict per step, writes report.html and junit.xml, posts results back, and files or updates bugs for failures.",tools:["find_test_cases","run_test_cases"],keywords:["test case","cases","test plan","suite","manual test","run case","execute case","run the case"],examples:["Run SHOP-12 and SHOP-14 from Jira, two browsers at once","Run test plan 12 suite 34 from Azure DevOps and don't post results"],requires:[{kind:"live_model"},{kind:"system",anyOf:["azure_devops","jira","xray","testrail","zephyr","qtest"],optional:!0,why:"to read cases from a test management system and post results back (files and pasted text work without one)"},{kind:"bugs",optional:!0}],docs:[{doc:"HOW_TO_USE.md",heading:"7. Test cases and suites"},{doc:"HOW_TO_USE.md",heading:"Where cases come from"},{doc:"HOW_TO_USE.md",heading:"Running"},{doc:"HOW_TO_USE.md",heading:"Results back in the source"},{doc:"HOW_TO_USE.md",heading:"Bugs for failures"}]},{id:"test-case-quality",area:"Test cases",title:"Check and improve test cases",summary:"Checks test cases before they run (vague or missing expected results, missing test data, placeholders, duplicates) against the story, feature, and epic they test, then rewrites Azure DevOps or Jira cases in place once the user agrees.",tools:["check_test_cases","update_test_case"],keywords:["good enough","quality","improve","fix the expected","expected result","check the cases","check cases","review the cases","rewrite"],examples:["Are the cases in test plan 12 good enough to run?","Fix the expected results of case 4512"],requires:[{kind:"system",anyOf:["azure_devops","jira"],optional:!0,why:"to rewrite cases in place"}],docs:[{doc:"HOW_TO_USE.md",heading:"Checking test cases before they run"}]},{id:"write-test-cases",area:"Test cases",title:"Write test cases from a story",summary:"Writes test cases from a story's acceptance criteria and saves them in Azure DevOps or Jira, linked to the story. Reads the cases already linked first, so asking again adds only what is missing and updates cases in place instead of duplicating them; plans end-to-end journeys from the existing cases.",tools:["story_test_cases","save_test_cases","e2e_journey"],keywords:["write test cases","test cases for","story","acceptance criteria","journey","end-to-end","generate cases","save them"],examples:["Write test cases for story 1234 and save them","Build an end-to-end journey from story 1234's cases"],requires:[{kind:"system",anyOf:["azure_devops","jira"]}],docs:[{doc:"HOW_TO_USE.md",heading:"Where cases come from"}],playbook:[{heading:"Testing from requirements",when:"write test cases from a story, a Confluence page, or a specification"},{heading:"End-to-end journeys from test cases",when:"an end-to-end case or flow from test cases that exist"}]},{id:"domain-compliance",area:"Test cases",title:"Domain and regulatory test design",summary:"Every agent works as an expert in the product's industry would, automatically: the risks the industry is known for and the obligations of its regulations and standards (for example DORA, PSD2, GDPR, ISO 27001), declared in forgeqe.yaml or worked out from the story and the application. It applies to analysing requirements, writing and checking test cases, exploration, writing, reviewing, and healing test code, API tests, finding tests, and pull request reviews, labelled so coverage can be traced and citing only the control ids in the team's compliance file. forgeqe eval domain measures how well the agent does it.",tools:[],keywords:["gdpr","dora","psd2","hipaa","pci","sox","iso 27001","compliance","regulation","obligation"],examples:["Write test cases for story 1234, covering our DORA and PSD2 obligations","Which GDPR obligations do these cases miss?"],requires:[{kind:"domain",optional:!0}],limits:["Designs and checks tests for regulatory obligations; it does not certify compliance or replace an audit.","Cites control or article ids only from the team's compliance file, never from memory."],docs:[{doc:"HOW_TO_USE.md",heading:"16. Domain and regulations"}]},{id:"exploratory-testing",area:"Test cases",title:"Exploratory testing of a story",summary:"Explores a story in real browsers beyond its scripted cases (edge cases, invalid input, boundaries, roles, state), confirms each defect by reproducing it, proposes test cases, and files bugs once the user agrees.",tools:["explore_story"],keywords:["explore","exploratory","find bugs","beyond the test cases","charter"],examples:["Explore story 1234 and find bugs","Test this feature beyond the test cases"],requires:[{kind:"live_model"},{kind:"system",anyOf:["azure_devops","jira"],optional:!0,why:"to read the story and file bugs"}],docs:[{doc:"HOW_TO_USE.md",heading:"Exploratory testing from a story"}],playbook:[{heading:"Exploratory testing",when:"explore a story or feature past its scripted cases, or find bugs in it"}]},{id:"test-apps",area:"Setup",title:"Test environments and their apps",summary:"Knows the test environments in env.yaml (QA, UAT, \u2026) and each one's apps: web apps and APIs by URL, Windows desktop apps, and mobile apps, with what each is for. Every agent works from the active one; switches environment on request, picks the apps a scenario needs, and carries values such as an order number from one app to the next.",tools:["test_apps","use_environment"],keywords:["environment","env","qa","uat","staging","apps","which apps"],examples:["Place an order in the shop, then check its invoice in Billing, in UAT","Which apps can you test in QA?","From now on, use the UAT environment"],requires:[{kind:"input",what:"env.yaml with the environments and apps (forgeqe init writes one)"}],limits:[],docs:[{doc:"HOW_TO_USE.md",heading:"Environments and apps"}],playbook:[{heading:"Apps and environments",when:"a request names an app or environment, or a scenario spans several apps"}]},{id:"desktop-apps",area:"Desktop",title:"Automate Windows desktop apps",summary:"Carries out a plain-language goal in a Windows thick-client app with UFO\xB2 and saves it as a workflow with checks of the outcome, or records the steps a tester does by hand, with no model; replays saved workflows with no model, healing a step whose control moved, and fixes a workflow from chat (a timeout, a wait, a dialog to dismiss) after showing the change. Apps and the secrets they may type come from env.yaml or forgeqe.yaml desktop:.",tools:["desktop_explore","desktop_record","desktop_replay","desktop_edit_workflow"],keywords:["desktop","windows app","thick client","thick-client",".exe","record","replay","workflow","ufo","notepad","win32","wpf","winforms"],examples:["Create an invoice for Contoso in the Billing app and save it as a test","I'll show you the steps in Billing: record them","Replay the billing workflow with customer Fabrikam","Step 4 timed out while the list loaded: fix the workflow"],requires:[{kind:"live_model",optional:!0},{kind:"apps",app:"desktop"},{kind:"input",what:"a Windows machine, and the app in forgeqe.yaml desktop.apps"}],limits:["Windows only, one run at a time per desktop","Apps whose controls UI Automation cannot see (Citrix, some Java and custom-drawn apps) are not supported yet"],docs:[{doc:"HOW_TO_USE.md",heading:"Desktop apps (Windows)"}],guide:[{doc:"docs/DESKTOP.md",heading:"ForgeQE desktop: thick-client automation"},{doc:"docs/DESKTOP.md",heading:"Use"},{doc:"docs/DESKTOP.md",heading:"How a run flows"},{doc:"docs/DESKTOP.md",heading:"One model config: `llms.json` \u2192 UFO"},{doc:"docs/DESKTOP.md",heading:"Workflow YAML (`forgeqe-desktop-v1`)"},{doc:"docs/DESKTOP.md",heading:"Recording by hand"},{doc:"docs/DESKTOP.md",heading:"Fixing a workflow from chat"},{doc:"docs/DESKTOP.md",heading:"Waits, retries and dialogs"},{doc:"docs/DESKTOP.md",heading:"Visual checks (`expect_image`)"},{doc:"docs/DESKTOP.md",heading:"Unattended runs"},{doc:"docs/DESKTOP.md",heading:"Updating UFO\xB2"},{doc:"docs/DESKTOP.md",heading:"Checks of the outcome"},{doc:"docs/DESKTOP.md",heading:"Recording"},{doc:"docs/DESKTOP.md",heading:"Security"},{doc:"docs/DESKTOP.md",heading:"Before first use on Windows"},{doc:"docs/DESKTOP.md",heading:"Next steps"},{doc:"docs/DESKTOP-CI.md",heading:"Running desktop workflows unattended (nightly CI)"},{doc:"docs/DESKTOP-CI.md",heading:"What breaks a desktop run, and how ForgeQE tells you"},{doc:"docs/DESKTOP-CI.md",heading:"Set up the test machine"},{doc:"docs/DESKTOP-CI.md",heading:"The nightly job"}],playbook:[{heading:"Desktop apps",when:"automate or test a Windows desktop (thick-client) app"}]},{id:"mobile-apps",area:"Mobile",title:"Automate Android and iOS apps",summary:"Carries out a plain-language goal in an Android or iOS app through Appium and saves it as a mobile workflow with checks of the outcome; replays workflows with no model on Appium (healing a moved control) or Maestro; runs the team's own Maestro flows after checking them against an allowlist; uploads an app build to TestMu AI and saves its lt:// id as the app's build. Apps come from env.yaml (kind: mobile) or forgeqe.yaml mobile:.",tools:["mobile_explore","mobile_replay","mobile_maestro","mobile_upload"],keywords:["mobile","android","ios","iphone","ipad","phone","tablet","apk","ipa","maestro","appium","emulator","simulator","app store","play store","upload","build","testmu","lambdatest"],examples:["Sign in to the courier app and accept the first delivery, and save it as a test","Replay the courier login on Maestro","Run our Maestro flows in .maestro/","Upload builds/courier.apk to TestMu for the courier app"],requires:[{kind:"live_model",optional:!0},{kind:"apps",app:"mobile"},{kind:"input",what:"an Appium server with a device or emulator (or a device cloud), or Maestro installed; the app in env.yaml or forgeqe.yaml mobile.apps"}],limits:["Explore runs on Appium only","Maestro replays do not heal moved controls or compare images (expect_image)","On TestMu AI, Maestro (through HyperExecute) refuses workflows that type secrets; replay those there with --driver appium"],docs:[{doc:"HOW_TO_USE.md",heading:"Mobile apps (Android, iOS)"}],guide:[{doc:"docs/MOBILE.md",heading:"ForgeQE mobile: Android and iOS apps"},{doc:"docs/MOBILE.md",heading:"Use"},{doc:"docs/MOBILE.md",heading:"Apps and settings"},{doc:"docs/MOBILE.md",heading:"Workflow YAML (`forgeqe-mobile-v1`)"},{doc:"docs/MOBILE.md",heading:"Explore"},{doc:"docs/MOBILE.md",heading:"Maestro"},{doc:"docs/MOBILE.md",heading:"TestMu AI (formerly LambdaTest)"},{doc:"docs/MOBILE.md",heading:"Security"}],playbook:[{heading:"Mobile apps",when:"automate or test an Android or iOS app"}]},{id:"containers",area:"Operations",title:"Services, mocks, workspace and browser in Docker or Podman",summary:"Starts the services a test run needs (a database, a queue, the API under test) in Docker or Podman, as forgeqe.yaml declares them, and waits until each is ready; stops them, and shows their status and logs. Starts mocks of an API on WireMock, from the calls a browser run recorded or from an OpenAPI description, and can point an API app of the environment at the mock for the session. With containers.workspace, the coding agents' shell commands (build, restore, run) and every test ForgeQE runs go to a container of the project's image; with browser.provider docker, the browser agent, the replay and the coding agent's live browser use Chromium in a container.",tools:["services","mock_service"],keywords:["docker","podman","container","containers","compose","mock","mocks","mock service","mock server","mock api","stub","stubs","wiremock","testcontainers","service up","services up","workspace","docker browser","browser in docker","run tests in docker","build in docker"],examples:["Start the database and the queue in Docker","Start the services from our docker-compose file","Mock the payments API from the last run","Create a mock service from openapi.yaml and point order-api at it","Show the logs of the db container"],requires:[{kind:"containers"}],limits:["Starts only services declared in forgeqe.yaml containers.services; it never runs an image, flag or mount from a request","No privileged containers, host network, or access to the container runtime's socket; ports on 127.0.0.1 only; mounts read-only from inside the project","Mocks answer REST calls (WireMock); GraphQL, SOAP and gRPC are not mocked","Reads docker-compose files only for their services' image, ports, environment, bind mounts, command and depends_on; refuses build, privileged, network_mode, cap_add, devices and env_file; leaves named volumes out","The workspace mounts the repository with its credential files (.env, .npmrc, keys) covered; the agents and the model key stay on this machine; ForgeQE's own agent keeps this machine's shell","A browser in a container reaches this machine's apps as host.docker.internal, not localhost; it is headless, or visible on a virtual display, never on this screen"],docs:[{doc:"HOW_TO_USE.md",heading:"Containers and mock services"}],guide:[{doc:"docs/CONTAINERS.md",heading:"ForgeQE containers: services and mocks in Docker or Podman"},{doc:"docs/CONTAINERS.md",heading:"Use"},{doc:"docs/CONTAINERS.md",heading:"Declaring services"},{doc:"docs/CONTAINERS.md",heading:"Compose files"},{doc:"docs/CONTAINERS.md",heading:"The project network"},{doc:"docs/CONTAINERS.md",heading:"Tests in a container"},{doc:"docs/CONTAINERS.md",heading:"The workspace"},{doc:"docs/CONTAINERS.md",heading:"The browser in a container"},{doc:"docs/CONTAINERS.md",heading:"Testcontainers (Scribe)"},{doc:"docs/CONTAINERS.md",heading:"Mocks"},{doc:"docs/CONTAINERS.md",heading:"Lifetime"},{doc:"docs/CONTAINERS.md",heading:"Security"}],playbook:[{heading:"Containers and mocks",when:"start, stop or inspect the project's services in Docker or Podman, or mock an API"}]},{id:"databases",area:"Data",title:"Ask a database in plain language",summary:"Connects to the project's databases (PostgreSQL, MySQL and MariaDB, SQL Server and Azure SQL, Oracle, Snowflake, Redshift, CockroachDB, SQLite, DuckDB, and MongoDB) from a connection string or host and user, signing in with a password, Microsoft Entra ID or AWS IAM. Reads the schema through DB-GPT's connectors and turns questions into SQL that is checked before it runs: reads in a read-only session that is rolled back, writes only on a database marked writable, after you confirm them.",tools:["databases","database_schema","run_sql"],keywords:["sql","database","db","schema","text to sql","text-to-sql","postgres","postgresql","mysql","mariadb","sql server","mssql","azure sql","oracle","snowflake","redshift","cockroachdb","sqlite","duckdb","mongodb","mongo","collection","select statement","query the database","rows in"],examples:["Which customers in India have paid orders? Ask the orders database","Show me the tables in the shop database","Write the SQL for revenue per country last month and run it","Check the connection to the reporting database"],requires:[{kind:"databases"}],limits:["One statement at a time: SELECT (with CTEs and set operations), SHOW, DESCRIBE and EXPLAIN SELECT anywhere; INSERT, and UPDATE or DELETE with a WHERE clause, only on databases marked writable and after you confirm","Never DDL (CREATE, ALTER, DROP, TRUNCATE), procedures, or functions that read files, sleep or reach other servers","MongoDB is queried with find, aggregate, count and distinct (JSON operations); its test data is not made yet; other non-SQL stores are not supported"],docs:[{doc:"HOW_TO_USE.md",heading:"Databases"}],playbook:[{heading:"Databases",when:"answer a question from a database, write or run SQL, or check what a database holds"}]},{id:"test-data",area:"Data",title:"Test data on demand",summary:"Makes realistic test data from a spec that fits the database's schema (types, sizes, NOT NULL, unique keys, CHECK lists, enums), parents before children, the same rows for the same seed. Or takes a masked subset of a real database: the rows you pick and every row they need, with names, emails, phones, addresses and other personal columns replaced the same way every time. Shows a preview, writes CSV, JSON or SQL files for automation, or inserts the rows into a database marked writable after you confirm, as a batch that can be removed again. Specs live in the project, so tests and pipelines make the same data with forgeqe data gen.",tools:["generate_test_data","test_data_batches"],keywords:["test data","seed data","fixture","fixtures","synthetic data","fake data","dummy data","sample data","generate data","populate","seed the database","test data batch","clean up test data","faker","subset","masked","mask","anonymize","anonymise","copy of production","production data"],examples:["Generate test data: 20 customers with 1 to 3 orders each in the shop database","Make test data for the orders tables as CSV files for our automation","Insert 50 test users into the fixtures database","Copy 100 paid orders from prod into fixtures as a masked subset","Remove the test data batch we inserted yesterday"],requires:[{kind:"databases"}],limits:["Inserts only into databases marked writable, after you confirm; removes only rows of a batch it inserted","A subset reads its source in read-only sessions; its start condition is a filter of one table, and personal columns are found by name (mask: adds others)","Simple CHECK constraints (a list of values, bounds) are read; other rules the database enforces may refuse rows, and then nothing is kept"],docs:[{doc:"HOW_TO_USE.md",heading:"Test data"}],playbook:[{heading:"Test data",when:"make test data, fixtures or seed rows, write them to files, insert them, or remove a batch"}]},{id:"data-flows",area:"Data",title:"Data flows between systems",summary:"Checks the data that passes between your systems' databases (the digital channel, CRM, billing, a warehouse), by batch ETL or in real time: each hop's rows arrived, complete, with every mapped column right after its rule, on time for its SLA, without duplicates. Follows one record end to end, says what a change to a table or column reaches downstream, and writes the flows from your mapping documents wherever they are, or suggests them from the schemas.",tools:["data_flows","check_flows","trace_record","read_mapping_file"],keywords:["data flow","data flows","upstream","downstream","lineage","etl","reconcile","reconciliation","mapping document","source to target","source-to-target","cdc","sync","integration","propagate","crm","trace the record","impact","kafka","topic","event stream"],examples:["Check that customers from the digital database reach the CRM correctly","Trace customer 42 from digital through CRM to billing","What does a change to digital.customers.email affect downstream?","Write the data flows from mapping.xlsx","Reconcile yesterday's orders between the ERP and the warehouse"],requires:[{kind:"databases"}],limits:["Hops between SQL tables, Kafka topics, and APIs (a target by key, a source by its list; GET or POST only); MongoDB is not checked","At most 200,000 source rows a flow per check, compared by key","Reads only; flows are written into flows.yaml, and nothing in the databases changes"],docs:[{doc:"HOW_TO_USE.md",heading:"Data flows"}],playbook:[{heading:"Data flows",when:"check data passing between databases, follow a record across systems, the impact of a change downstream, or write flows from mapping documents"}]},{id:"write-tests",area:"Automated tests",title:"Turn a passed run into an automated test",summary:"Scribe, the coding agent, writes a test for a passed browser run in the repository's own framework, page objects, and helpers; ForgeQE runs it until it passes and a reviewer approves it.",tools:["write_test","run_test"],keywords:["automated test","automation","automate","playwright","cypress","selenium","webdriverio","write a test","test code","spec","turn it into a test","turn that run into","scribe"],examples:["Write a Playwright test for that run in ../app"],requires:[{kind:"coding_agent"},{kind:"input",what:"a repository to write the test in (default: the working directory)"}],docs:[{doc:"HOW_TO_USE.md",heading:"6. Tests"}],guide:[{doc:"HOW_TO_USE.md",heading:"How Scribe writes a test"},{doc:"HOW_TO_USE.md",heading:"Generated code"}],playbook:[{heading:"Writing tests",when:"write an automated test from a run, or a session where passed runs become tests"}],alsoAfter:["browser_run","run_test_cases"]},{id:"heal-tests",area:"Automated tests",title:"Heal failing tests",summary:"Runs a repository's tests and updates the ones the application has outgrown against the live app, or reports a real application bug instead.",tools:["heal_tests"],keywords:["heal","failing test","broken test","fix the tests","fix tests","repair"],examples:["Heal the failing tests in ../app"],requires:[{kind:"coding_agent"},{kind:"input",what:"a repository with tests"}],docs:[{doc:"HOW_TO_USE.md",heading:"Fixing broken tests"}],playbook:[{heading:"Repairing broken tests",when:"automated tests that fail, broke after a release, or need healing"}]},{id:"find-run-tests",area:"Automated tests",title:"Find and run existing tests",summary:"Finds the automated tests that match a feature, tags, ids, a folder, or a sprint, confirms the selection, and runs it once the user agrees, with a verdict per test.",tools:["find_tests","run_tests","list_sprints"],keywords:["run the tests","run tests","run the","existing tests","tests in","test suite","npm test","pytest","added in sprint","checkout tests","tests added"],examples:["Run the checkout tests","Run the tests added in sprint 12"],requires:[{kind:"coding_agent"},{kind:"input",what:"a repository with tests"},{kind:"system",anyOf:["azure_devops","jira"],optional:!0,why:"to pick tests by sprint"}],docs:[{doc:"HOW_TO_USE.md",heading:"Finding and running existing tests"},{doc:"HOW_TO_USE.md",heading:"Reports for tests run outside ForgeQE"}],playbook:[{heading:"Repository tests",when:"find, list, or run the automated tests of the repository"}]},{id:"regression-packs",area:"Automated tests",title:"Regression packs for a release",summary:"Builds a regression pack from what goes into a release (an Azure DevOps sprint or work items, a commit range, pull requests, or changes read from any connected tool): the tests those changes can break, each with why, plus risk-area and smoke tests, and the changes no test covers. Runs it locally, on TestMu AI, or in a pipeline, compares each run with the last, and triages each failure: flaky, environment, broken test code, heal, or an AI browser re-run to tell a test the app outgrew from an app bug.",tools:["build_regression_pack","run_regression_pack","list_regression_packs"],keywords:["regression","pack","which tests should","release candidate","merged this week","triage","heal or rerun","why did the pack fail"],examples:["Build a regression pack for sprint 12 and run it on TestMu","Which tests should we run for the PRs merged this week?"],requires:[{kind:"input",what:"a repository with tests"},{kind:"system",anyOf:["azure_devops","jira"],optional:!0,why:"to read a sprint's or release's work items"}],docs:[{doc:"HOW_TO_USE.md",heading:"Regression packs for a release"}],playbook:[{heading:"Regression packs",when:"a regression pack, or which tests to run for a release, sprint, or pull requests"}]},{id:"flaky-tests",area:"Automated tests",title:"Flaky test detection and quarantine",summary:"Finds tests that flip between passing and failing in the repository's run history, with their likely cause, and quarantines them once the user agrees.",tools:["flaky_tests","quarantine_tests"],keywords:["flaky","quarantine","intermittent","unstable","flakes"],examples:["Which tests are flaky?","Quarantine the flaky login test"],requires:[{kind:"input",what:"a repository whose tests ForgeQE has run"}],docs:[{doc:"HOW_TO_USE.md",heading:"Flaky tests"}]},{id:"pr-review",area:"Pull requests",title:"Review pull requests",summary:"Reviews an Azure DevOps or GitHub pull request from the local clone for bugs, security, requirements, and tests, and posts the review once the user agrees.",tools:["review_pull_request","post_pr_review"],keywords:["review pr","review the pr","review pull request","pull request","code review","pr review","review the change"],examples:["Review PR 42 and focus on error handling"],requires:[{kind:"coding_agent"},{kind:"input",what:"a local clone of the repository"},{kind:"either",of:[{kind:"system",anyOf:["azure_devops"],why:"for Azure DevOps pull requests"},{kind:"command",command:"gh",why:"for GitHub pull requests"}]}],docs:[{doc:"HOW_TO_USE.md",heading:"Reviewing pull requests"}],playbook:[{heading:"Pull requests",when:"review a pull request, its impact, or the tests it can break"}],alsoAfter:["analyze_pr_impact","select_pr_tests","post_pr_test_results","comment_on_pr"]},{id:"pr-impact",area:"Pull requests",title:"Pull request impact and test selection",summary:"Works out which product areas, pages, requirements, and tests a pull request touches, picks the tests it can break, runs them, and posts the impact or results on the pull request once the user agrees.",tools:["analyze_pr_impact","select_pr_tests","post_pr_test_results","comment_on_pr"],keywords:["impact","pr ","pull request","affected","can break","what does pr","changed files"],examples:["What does PR 42 impact?","Run the tests PR 42 can break"],requires:[{kind:"input",what:"a local clone of the repository"},{kind:"either",of:[{kind:"system",anyOf:["azure_devops"],why:"for Azure DevOps pull requests and their linked work items"},{kind:"command",command:"gh",why:"for GitHub pull requests"}]}],docs:[{doc:"HOW_TO_USE.md",heading:"What a pull request impacts in the product"},{doc:"HOW_TO_USE.md",heading:"Tests for a pull request"}]},{id:"api-testing",area:"API",title:"API and contract testing",summary:"Builds API contracts (status, schema, key fields) from the calls a browser run saw or from an API description (OpenAPI, Swagger, WSDL, GraphQL, .proto, gRPC reflection), and end-to-end API scenarios from runs, descriptions, test cases, or plain English. Runs them straight against the API without a browser, or writes them as tests in the repository. REST, GraphQL, SOAP, and gRPC.",tools:["api_contracts","run_api_contracts","api_scenarios","run_api_scenarios","write_api_tests"],keywords:["api","endpoint","openapi","swagger","contract","rest","graphql","postman","request","response"],examples:["Turn the last run's API calls into tests","Test the API in openapi.yaml","Write API scenario tests for the order lifecycle"],requires:[{kind:"coding_agent",optional:!0}],limits:["Functional API testing, not load or stress testing."],docs:[{doc:"HOW_TO_USE.md",heading:"API and contract tests"},{doc:"HOW_TO_USE.md",heading:"End-to-end API scenarios"}],playbook:[{heading:"API testing",when:"API contracts, API tests, or end-to-end API scenarios (REST, GraphQL, SOAP, gRPC)"}]},{id:"accessibility-performance",area:"Quality",title:"Accessibility and page performance",summary:"Checks every page a run visits against WCAG rules (axe-core) and measures its Core Web Vitals, against budgets, with trends over time per page.",tools:["quality_report","quality_trends"],keywords:["accessible","accessibility","a11y","wcag","performance","lighthouse","slow","page speed","web vitals","load time"],examples:["Is the checkout page accessible?","Did page performance get worse this week?"],requires:[{kind:"input",what:"a browser run (from browser_run or run_test_cases); new runs need a live model"}],limits:["Per-page Core Web Vitals, not load testing under many users."],docs:[{doc:"HOW_TO_USE.md",heading:"Accessibility and performance budgets"}],playbook:[{heading:"Accessibility, performance, and visual changes",when:"accessibility, Core Web Vitals, performance budgets, or visual changes of pages"}],alsoAfter:["visual_compare","visual_approve"]},{id:"visual-comparison",area:"Quality",title:"Visual comparison with baselines",summary:"Compares each page a run captures with its approved baseline and shows what changed; intended changes become the new baseline once the user agrees.",tools:["visual_compare","visual_approve"],keywords:["visual","visually","baseline","looks different","approve the new","pixel","screenshot diff","layout"],examples:["What changed visually since the last run?","Approve the new header"],requires:[{kind:"input",what:"a browser run (from browser_run or run_test_cases); new runs need a live model"}],docs:[{doc:"HOW_TO_USE.md",heading:"Visual comparison"}]},{id:"defect-intelligence",area:"Quality",title:"Defect intelligence: trends and prediction",summary:"Groups failures into defects, keeps their history across suites (new, known, gone, back again), and gives defect trends and where a change is likely to cause defects, with the cases to run first.",tools:["defect_trends","predict_defects"],keywords:["defect","bugs keep","trend","predict","coming back","root cause","rca","hotspot","since main","changes since","can the changes"],examples:["Which defects keep coming back?","What can the changes since main break?"],requires:[{kind:"input",what:"suites run into one folder (suite.out_dir)"},{kind:"command",command:"git",optional:!0,why:"for predictions from the product's repository"}],limits:["Prediction is a heuristic from the saved suites and the change, not a trained model; its accuracy shows once suites have scored it."],docs:[{doc:"HOW_TO_USE.md",heading:"Bugs for failures"},{doc:"HOW_TO_USE.md",heading:"Defect trends and prediction"}]},{id:"release-readiness",area:"Release",title:"Requirement coverage and release readiness",summary:"Gives requirement coverage and a GO / GO WITH RISKS / NO-GO verdict for a sprint, area, epic, or stories, from test cases, results, bugs, and flaky tests; can publish it to the wiki.",tools:["release_readiness"],keywords:["release","ready to release","coverage","go/no-go","no-go","readiness","requirement coverage","ship"],examples:["Are we ready to release?","Coverage of sprint 12"],requires:[{kind:"system",anyOf:["azure_devops","jira"]}],docs:[{doc:"HOW_TO_USE.md",heading:"Requirement coverage and release readiness"}]},{id:"monitoring",area:"Operations",title:"Scheduled monitoring with alerts",summary:"Runs checks on a schedule and alerts Teams or email when one keeps failing, and again when it recovers.",tools:["monitor"],keywords:["monitor","every minute","every hour","every day","every night","every 5 minutes","every 15 minutes","every 30 minutes","schedule","uptime","alert","cron","minutes","hourly","nightly"],examples:["Check the login flow every 15 minutes and alert Teams"],requires:[{kind:"live_model"},{kind:"monitor_alerts",optional:!0}],docs:[{doc:"HOW_TO_USE.md",heading:"9. Monitoring"}],playbook:[{heading:"Monitoring",when:"checks on a schedule, with alerts"}]},{id:"jobs",area:"Operations",title:"Background jobs on a schedule",summary:"Runs ForgeQE commands (suites, monitor rounds, regression packs, healing, pull-request tests, browser goals) on a schedule or once in the jobs service, apart from the conversation and after it closes, and watches an existing GitHub Actions or Azure DevOps pipeline (read-only). A failed pipeline run or pack run is investigated: the healing agent works on each failure on a local branch of its own (a fix for review, or an app bug), never pushing. Says how each job goes when asked, shows runs as they end, and sends failures, recoveries and findings to Teams or email.",tools:["jobs"],keywords:["job","jobs","background","in the background","every weekday","every night at","keep running","while i work","scheduled job","run it nightly","watch the pipeline","pipeline health","investigate failures"],examples:["Run the release-2.3 pack every weekday at 2am in the background","Watch the pipeline in the background and investigate failures","How are my jobs doing?"],requires:[{kind:"notify_channels",optional:!0}],docs:[{doc:"HOW_TO_USE.md",heading:"The jobs service: scheduled and long jobs apart from the shell"}],playbook:[{heading:"Background jobs",when:"work on a schedule or in the background, or how a job is going"}]},{id:"notifications",area:"Operations",title:"Teams and email notifications",summary:"Tells the team in Microsoft Teams channels or by email when a suite fails or passes, when a release readiness verdict is in, and when the domain evaluation falls below its gate, with the failing cases, their bugs, and links to the report and the CI build. Each channel picks its events; the agent also posts a summary to a channel when asked.",tools:["notify"],keywords:["teams","email","notify","post the results","channel","tell the","send the"],examples:["Post the results to the QA channel","Tell the release channel the verdict is NO-GO"],requires:[{kind:"notify_channels"}],limits:["Automatic notifications come from runs nobody is watching (forgeqe suite, forgeqe run, pipelines, the MCP server), not from the interactive shell, where the agent posts only when asked."],docs:[{doc:"HOW_TO_USE.md",heading:"17. Notifications"}]},{id:"reports",area:"Release",title:"Reports you can share",summary:"Writes up what the Orchestrator found, such as open defects across projects, release status, or pipeline health, as a page with filters (project, state, priority, assignee, \u2026), charts, and a sortable table, plus Markdown and CSV. Every table is built from evidence: the results of the tool calls the agent made, or a read-only query ForgeQE runs itself, listed on the page's Sources with each raw result kept beside the report.",tools:["write_report"],keywords:["report","write up","write-up","summary","share","pdf","html report"],examples:["Create a release report for all projects","Write up the open defects as a report I can share"],requires:[{kind:"live_model"}],limits:["Rows come only from tool results in the session or a query ForgeQE runs; the agent writes only the summary, which is not checked figure by figure.","A WIQL query reads up to 1000 work items; the page says when a query matched more.","The page is not published anywhere unless the reports folder is served (notify.report_url)."],docs:[{doc:"HOW_TO_USE.md",heading:"Reports from the agent"}],guide:[{doc:"HOW_TO_USE.md",heading:"Team report"},{doc:"README.md",heading:"Static BI"},{doc:"README.md",heading:"One report for the whole team"},{doc:"HOW_TO_USE.md",heading:"Model prices and usage"},{doc:"HOW_TO_USE.md",heading:"Spending budgets"}]},{id:"pipelines",area:"Operations",title:"CI pipelines",summary:"Writes a GitHub Actions or Azure Pipelines workflow that runs ForgeQE, stores the secrets it needs, pushes a branch, and makes the pipeline green.",tools:["push_branch","set_ci_secret"],keywords:["pipeline","github actions","azure pipelines","ci","gitlab","jenkins","make it green","ci secret","workflow file","on every pr"],examples:["Add a GitHub Actions workflow that runs tests/cases.yaml on every PR, and make it green"],requires:[{kind:"command",command:"git"},{kind:"either",of:[{kind:"command",command:"gh",why:"for GitHub Actions"},{kind:"command",command:"az",why:"for Azure Pipelines"}]}],docs:[{doc:"HOW_TO_USE.md",heading:"8. Pipelines"}],playbook:[{heading:"CI pipelines",when:"a GitHub Actions or Azure DevOps pipeline that runs ForgeQE or the tests"}]},{id:"systems",area:"Systems",title:"Work with Azure DevOps, Jira, GitHub, and Confluence",summary:"Reads anything in the configured systems (work items, issues, queries, boards, pages), makes changes the user asks for, puts run evidence (screenshots, reports) on work items and issues as attachments or comments, and builds Azure DevOps dashboards and wiki pages.",tools:["read_confluence","create_ado_dashboard","write_ado_wiki_page","attach_to_work_item"],keywords:["jira","azure devops","ado","confluence","system","github issue","work item","bug","sprint","dashboard","wiki","comment","backlog","issue","ticket","requirement"],examples:["Show the open bugs in this sprint","Read the checkout requirements from Confluence","Make a dashboard of this sprint's test results","Add the last screenshot of that run to ADO 256 as a comment","Run the GitHub issues labelled login as test cases"],requires:[{kind:"system",anyOf:["azure_devops","jira","xray","confluence"]}],docs:[{doc:"README.md",heading:"Azure DevOps, Jira, and other systems"}],playbook:[{heading:"Releases and pipelines in Azure DevOps",when:"releases, deployments, environments, or pipeline runs in Azure DevOps"},{heading:"Dashboards and wiki pages in Azure DevOps",when:"an Azure DevOps dashboard or wiki page"}]},{id:"system-data",area:"Systems",title:"Read any data in Azure DevOps, Jira, Xray, and Confluence",summary:"Reads through a system's REST API what its MCP tools do not cover: pull requests and repositories, test plans and suites, classic releases, and anything else in Azure DevOps, Jira, Xray, or Confluence. Given on every turn once a system is set up, so the agent never turns to the browser or the shell to read it.",tools:["query_api"],keywords:["pull request","pull requests","prs","open pr","repos","repository","branch","commit","test plan","test plans","release"],examples:["Are there open pull requests in any repository of QI-Claims?","List the test plans in QI-MobileBanking with their suites"],requires:[{kind:"system",anyOf:["azure_devops","jira","xray","confluence"]}],docs:[{doc:"README.md",heading:"Azure DevOps, Jira, and other systems"}]},{id:"generated-code",area:"Setup",title:"What ForgeQE wrote",summary:"Says how much of the code ForgeQE wrote is still in the repository, and gives the trailer and footer that mark a commit message or pull request description covering code it wrote. Works with any repository, whatever system the team uses.",tools:["generated_attribution"],keywords:["commit message","pull request description","pr description","attribution","generated code","how much code","code forgeqe wrote","fq:","provenance"],examples:["How much of the code ForgeQE wrote is still there?","Write the commit message for these tests"],requires:[],docs:[{doc:"HOW_TO_USE.md",heading:"Generated code"}]},{id:"forgeqe-commands",area:"Setup",title:"Every forgeqe command from the shell",summary:"Runs the forgeqe commands no other tool covers when the user asks in words: the context budget, guardrails, prompts (show, diff, check, reset), the domain eval, skills on or off, session logs, saved API sets and packs, pack pipelines, the BI site, a test notification, desktop and mobile doctor and validate, and updating ForgeQE. Reading commands run at once; the others are shown and asked first.",tools:["forgeqe_command"],keywords:["context budget","guardrail","prompt","prompts","eval domain","domain eval","enable skill","disable skill","session log","logs","api list","contract sets","pack show","pack pipeline","bi","evidence","dashboard site","test notification","notify test","doctor","validate","update forgeqe","upgrade","new version","forgeqe command","run forgeqe"],examples:["Show me the context budget","Add a guardrail: never delete test data in prod","Diff the brain prompt against ForgeQE's","Is there a new version of ForgeQE?","Run the mobile doctor","Build the BI site"],requires:[],docs:[{doc:"HOW_TO_USE.md",heading:"15. Commands cheat sheet"},{doc:"HOW_TO_USE.md",heading:"Guardrails"}],guide:[{doc:"HOW_TO_USE.md",heading:"Your prompts, skills, and product knowledge"}]},{id:"setup-help",area:"Setup",title:"Setup, skills, and documentation",summary:"Shows the model profiles and MCP servers, installs or repairs the browser engine, creates a forgeqe.yaml, manages skills, and answers any question about ForgeQE from its documentation, this list, and its own code (the prompts, skills, and source it runs).",tools:["forgeqe_setup","capabilities","playbook","use_capabilities"],keywords:["help","what can you","how do i","how does forgeqe","set up","setup","configure","model","profile","skill","install","icons","agents","version","internals"],examples:["What can you do?","Which model am I using?","How do I set up email alerts?","Show me the icons of all agents","Which version of ForgeQE is this?"],requires:[],docs:[{doc:"HOW_TO_USE.md",heading:"2. Scaffold config"},{doc:"HOW_TO_USE.md",heading:"11. Skills"},{doc:"HOW_TO_USE.md",heading:"Model routing"}],guide:[{doc:"README.md",heading:"Privacy"},{doc:"docs/MODEL_ROUTING.md",heading:"Model routing: the right configured model for each task"},{doc:"docs/MODEL_ROUTING.md",heading:"Turn it on"},{doc:"docs/MODEL_ROUTING.md",heading:"The areas of work"},{doc:"docs/MODEL_ROUTING.md",heading:"How a model is picked"},{doc:"docs/MODEL_ROUTING.md",heading:"How ForgeQE learns the models"},{doc:"docs/MODEL_ROUTING.md",heading:"Limit what an area may use"},{doc:"docs/MODEL_ROUTING.md",heading:"What it does not do"}]}],Gj=[{what:"Load, stress, or soak testing with many simultaneous users",instead:"per-page Core Web Vitals with budgets"},{what:"Penetration testing or dynamic security scanning",instead:"security findings in pull request reviews"}]});function Ste(e,t){let n=e.routing,r=n===void 0||n===null||n===""||n===!1?"off":n===!0?"auto":String(n).toLowerCase();if(!["off","shadow","auto"].includes(r))throw Error(`llm.routing in forgeqe.yaml is "${String(n)}". Use off, shadow or auto.`);let s={},o=e.routes;if(o!==void 0&&o!==null){if(typeof o!=="object"||Array.isArray(o))throw Error(`llm.routes in forgeqe.yaml must map areas (${Ty.join(", ")}) to auto or profile names.`);for(let[l,c]of Object.entries(o)){if(!Ty.includes(l))throw Error(`llm.routes.${l} in forgeqe.yaml is not an area. The areas are ${Ty.join(", ")}.`);if(c==="auto"||c===void 0||c===null){s[l]="auto";continue}let d=(Array.isArray(c)?c:[c]).map((m)=>String(m).trim()).filter(Boolean),f=d.filter((m)=>!t.includes(m));if(f.length)throw Error(`llm.routes.${l} in forgeqe.yaml names ${f.map((m)=>`"${m}"`).join(", ")}, not a profile in llms.json. Run: forgeqe models`);if(!d.length)throw Error(`llm.routes.${l} in forgeqe.yaml is empty. Give auto or profile names.`);s[l]=[...new Set(d)]}}let a=e.fallback;if(a!==void 0&&a!==null&&typeof a!=="boolean")throw Error(`llm.fallback in forgeqe.yaml is "${String(a)}". Use true (the default: a model that does not work is replaced by one that does) or false.`);return{mode:r,routes:s,...n===void 0||n===null||n===""?{unset:!0}:{},...a===!1?{fallback:!1}:{}}}var Cy,Ty;var tC=ee(()=>{Cy={agent:{title:"Orchestrator: the shell, forgeqe run and forgeqe mcp",bar:4,tools:!0},browser:{title:"Driving a browser, desktop app or mobile app from screenshots",bar:4,vision:!0,tools:!1},coding:{title:"Writing, healing and fixing tests in a repository",bar:5,tools:!0},review:{title:"Reviewing tests and pull requests, judging results",bar:3,tools:!0},analysis:{title:"Root cause of defects and failures",bar:5,tools:!0},planning:{title:"Refining requests, finding tests, planning scenarios and charters",bar:3,tools:!0}},Ty=Object.keys(Cy)});import{existsSync as kte,readdirSync as $De,readFileSync as xte}from"fs";import Vj from"path";function Jb(e,t){if(e===void 0||e===null)return{};if(typeof e!=="object"||Array.isArray(e))throw Error(`${t} must be a mapping`);return e}function cc(e,t){if(e===void 0||e===null||e==="")return;if(typeof e!=="string"&&typeof e!=="number")throw Error(`${t} must be text`);return String(e)}function $te(e,t){let n;try{n=new URL(e)}catch{throw Error(`${t} must be a full http(s) URL, e.g. https://qa.shop.example.com`)}if(n.protocol!=="https:"&&n.protocol!=="http:")throw Error(`${t} must be an http(s) URL, not ${n.protocol}`);if(n.username||n.password)throw Error(`${t} must not carry a user name or password; use logins: in forgeqe.yaml`);return n.href}function TDe(e,t,n){if(!rz.test(e))throw Error(`${n}: app names are letters, digits and _ . -`);let r=Jb(t,n),s=String(r.kind||"");if(s!=="web"&&s!=="desktop"&&s!=="mobile"&&s!=="api")throw Error(`${n}.kind must be web, desktop, mobile or api`);let o={name:e,kind:s,...cc(r.description,`${n}.description`)?{description:cc(r.description,`${n}.description`)}:{}},a=cc(r.login,`${n}.login`);if(a)o.login=a;if(s==="web"||s==="api"){let l=cc(r.url,`${n}.url`);if(!l)throw Error(`${n}.url is required for a${s==="api"?"n api":" web"} app`);o.url=$te(l,`${n}.url`)}else if(s==="desktop"){let l=cc(r.path,`${n}.path`);if(!l)throw Error(`${n}.path is required for a desktop app (an absolute path to its .exe)`);if(!/^([A-Za-z]:[\\/]|\\\\|\/)/.test(l)||!/\.exe$/i.test(l)||/[\x00-\x1f]/.test(l))throw Error(`${n}.path must be an absolute path to an .exe; .bat, .cmd and scripts run through a shell`);o.path=l;let c=r.args??[];if(!Array.isArray(c)||!c.every((f)=>typeof f==="string"||typeof f==="number"))throw Error(`${n}.args must be a list of arguments, not one command string`);o.args=c.map(String);let d=cc(r.window,`${n}.window`);if(d)o.window=d}else{let l=String(r.platform||"");if(l!=="android"&&l!=="ios")throw Error(`${n}.platform must be android or ios`);let c=cc(r.app_id??r.appId,`${n}.app_id`);if(!c||!/^[A-Za-z0-9_.-]+$/.test(c))throw Error(`${n}.app_id must be the app's package or bundle id`);o.platform=l,o.appId=c;let d=cc(r.activity,`${n}.activity`);if(d){if(l!=="android"||!/^[A-Za-z0-9_.$]{1,200}$/.test(d))throw Error(`${n}.activity must be an Android activity name such as .MainActivity`);o.activity=d}let f=cc(r.app??r.app_file,`${n}.app`);if(f){if(/[\x00-\x1f]/.test(f)||!(/^https:\/\//i.test(f)||/^(bs|lt):\/\/[A-Za-z0-9._-]+$/.test(f)||/^storage:filename=[A-Za-z0-9._-]+$/.test(f)||Vj.isAbsolute(f)&&/\.(apk|aab|ipa|app|zip)$/i.test(f)))throw Error(`${n}.app must be an absolute path to an .apk, .aab, .ipa, .app or .zip, an https URL, or a device cloud upload (bs://\u2026)`);o.appFile=f}}return o}function CDe(e,t){let n=`env.yaml environments.${e}`;if(!rz.test(e))throw Error(`${n}: environment names are letters, digits and _ . -`);let r=Jb(t,n),s={};for(let[f,m]of Object.entries(Jb(r.apps,`${n}.apps`)))s[f]=TDe(f,m,`${n}.apps.${f}`);let o={};for(let[f,m]of Object.entries(Jb(r.variables,`${n}.variables`))){if(!ZH.test(f))throw Error(`${n}.variables: "${f}" must be letters, digits and _`);let h=cc(m,`${n}.variables.${f}`);o[f]=h??""}let a=r.secrets??[];if(!Array.isArray(a)||!a.every((f)=>typeof f==="string"&&ZH.test(f)))throw Error(`${n}.secrets must be a list of environment variable names (letters, digits and _); env.yaml holds no secret values`);let l=(f,m)=>{let h=cc(r[f],`${n}.${f}`);if(h){if(s[h]?.kind!==m)throw Error(`${n}.${f} must name one of its ${m} apps`);return h}let y=Object.values(s).filter((w)=>w.kind===m);return y.length===1?y[0].name:void 0},c=l("start_app","web"),d=l("api_app","api");return{name:e,...cc(r.description,`${n}.description`)?{description:cc(r.description,`${n}.description`)}:{},apps:s,...c?{startApp:c}:{},...d?{apiApp:d}:{},variables:o,secrets:[...new Set(a)]}}function Cte(e,t){let n={...e};for(let[r,s]of Object.entries(t)){let o=n[r];n[r]=o&&s&&typeof o==="object"&&typeof s==="object"&&!Array.isArray(o)&&!Array.isArray(s)?Cte(o,s):s}return n}function tz(e,t=""){if(e&&typeof e==="object"&&!Array.isArray(e))return Object.fromEntries(Object.entries(e).map(([n,r])=>[n,tz(r,n)]));if(Array.isArray(e))return e.map((n)=>tz(n,t));if(typeof e==="string"){if(Qj.test(t))return Tte.test(e)?e.trim():"<secret, not shown>";if(ez.test(e))return e.replace(ez,(n)=>n.startsWith("//")?"//<user>:<secret>@":`${n.split("=")[0]}=<secret>`)}return e}function ODe(e,t){let n={},r={},s=new Set,o=[],a,l=(y)=>{let w=Ete(y.name)||y.kind;for(let v=2;n[w];v++)w=`${Ete(y.name)||y.kind}${v}`;return n[w]={...y,name:w},w},c=(y,w)=>{if(y&&typeof y==="object"&&!Array.isArray(y)){let v=y;for(let[S,R]of Object.entries(v)){if(/^(description|desc)$/i.test(S)&&w.length===0)continue;let j=[...w,S];if(typeof R==="string"){let O=R.trim();if(Qj.test(S)){let M=Tte.exec(O),W=M&&(M[1]||M[2]||M[3]||M[4]||M[5]);if(W)s.add(W);else if(O)o.push(j.join("."));continue}if(ez.test(O)){o.push(j.join("."));continue}let D=(M,W)=>(M.test(S)?w[w.length-1]||"app":S.replace(W,""))||S;if(/^https?:\/\//i.test(O)){try{let M=$te(O,j.join(".")),W=D(YH,/[_-]?(base[_-]?)?(url|uri|endpoint|host)$/i),Z=/api/i.test(j.join("."))||/\/api(\/|$)/i.test(new URL(M).pathname)?"api":"web",G=typeof v.description==="string"?v.description:void 0,re=l({name:W,kind:Z,url:M,...YH.test(S)&&G?{description:G}:{}});if(w.length===0&&YH.test(S)&&Z==="web"&&!a)a=re}catch{}continue}if(/^([A-Za-z]:[\\/]|\\\\|\/).*\.exe$/i.test(O)&&!/[\x00-\x1f]/.test(O)){let M=Array.isArray(v.args)?v.args:Array.isArray(v.arguments)?v.arguments:[];l({name:D(RDe,/[_-]?(path|exe)$/i),kind:"desktop",path:O,args:M.filter((W)=>typeof W==="string"||typeof W==="number").map(String),...typeof v.description==="string"?{description:v.description}:{}});continue}if(O.length<=500&&!/[\r\n]/.test(O))r[XH(j.join("_"))]=O}else if(typeof R==="number"||typeof R==="boolean"){if(!Qj.test(S))r[XH(j.join("_"))]=String(R)}else if(Array.isArray(R)){if(/^(args|arguments)$/i.test(S))continue;if(/^secrets?$/i.test(S)){for(let O of R){if(typeof O!=="string"||!ZH.test(O))throw Error(`env.yaml ${e}: ${[...j].join(".")} must be a list of environment variable names (letters, digits and _); env.yaml holds no secret values`);s.add(O)}continue}if(!Qj.test(S)&&R.every((O)=>typeof O==="string"||typeof O==="number")&&R.length<=50)r[XH(j.join("_"))]=R.join(", ")}else if(R&&typeof R==="object")c(R,j)}}};c(t,[]);let d=_n.stringify(tz(t)).trim(),f={name:e,...typeof t.description==="string"?{description:t.description}:{},apps:n,variables:r,secrets:[...s],details:d.length>6000?`${d.slice(0,6000)}
|
|
183
183
|
# \u2026`:d,...o.length?{literalSecrets:o}:{}},m=Object.values(n).filter((y)=>y.kind==="web"),h=Object.values(n).filter((y)=>y.kind==="api");if(a)f.startApp=a;else if(m.length===1)f.startApp=m[0].name;if(h.length===1)f.apiApp=h[0].name;return f}function NDe(e){let t=e.apps;return Boolean(t&&typeof t==="object"&&!Array.isArray(t)&&Object.values(t).length&&Object.values(t).every((n)=>n&&typeof n==="object"&&("kind"in n)))}function nz(e,t,n){if(!rz.test(e))throw Error(`env.yaml: "${e}": environment names are letters, digits and _ . -`);let r=Cte(n,Jb(t,`env.yaml ${e}`)),s=NDe(r)?CDe(e,r):ODe(e,r),o=Bj(r.databases,`env.yaml ${e}.databases`,process.cwd());return Object.keys(o).length?{...s,databases:o}:s}function jDe(e){let t=[e.environments,e.envs].find((a)=>Array.isArray(a));if(t){let a={};for(let l of t){let c=Jb(l,"env.yaml environments[]"),d=String(c.name??c.env??c.id??"");if(!d)throw Error("env.yaml: each environment in the list needs a name");let{name:f,env:m,id:h,...y}=c;a[d]=nz(d,y,{})}return a}let n=[e.environments,e.envs,e.environment].find((a)=>a&&typeof a==="object"&&!Array.isArray(a)),r=n||e,s=[r.common,r.defaults,r.shared,r.global,e.common,e.defaults,e.shared].find((a)=>a&&typeof a==="object"&&!Array.isArray(a)),o={};for(let[a,l]of Object.entries(r)){if(ADe.has(a.toLowerCase())||!l||typeof l!=="object"||Array.isArray(l))continue;if(!n&&["environments","envs","environment"].includes(a))continue;o[a]=nz(a,l,s||{})}return o}function Rte(e,t,n,r,s=[],o={}){let a=e.file===void 0?void 0:String(e.file||""),l=e.dir===void 0?void 0:String(e.dir||"");if(a===void 0&&l===void 0&&!r)return;let c,d={},f;if(l!==void 0){if(c=Vj.resolve(t,l||"."),!kte(c))throw Error(`environments.dir in forgeqe.yaml names ${c}, which does not exist.`);for(let w of $De(c).filter((v)=>/\.(ya?ml|json)$/i.test(v)).sort()){let v=_n.parse(xte(Vj.join(c,w),"utf8"))??{};d[w.replace(/\.(ya?ml|json)$/i,"")]=nz(w.replace(/\.(ya?ml|json)$/i,""),v,{})}}else{if(c=Vj.resolve(t,a||"./env.yaml"),!kte(c)){if(a===void 0)throw Error(`--env ${r}: there is no env.yaml here. Add environments: { file: ./env.yaml } to forgeqe.yaml.`);throw Error(`environments.file in forgeqe.yaml names ${c}, which does not exist. Run forgeqe init to create one.`)}let w=Jb(_n.parse(xte(c,"utf8"))??{},"env.yaml");d=jDe(w),f=IDe(w)}let m=Object.keys(d),h=cc(e.default,"environments.default")||f||(m.length===1?m[0]:void 0),y=r||h;if(y&&!d[y])throw Error(`No environment "${y}" in ${c} (environments: ${m.join(", ")||"none"}).`);return{file:c,...y?{active:y}:{},all:d,baseDesktop:n,...o.mobile?{baseMobile:o.mobile}:{},baseHosts:s,...o.startUrl?{baseStartUrl:o.startUrl}:{},...o.apiUrl?{baseApiUrl:o.apiUrl}:{}}}function PDe(e){let t={};for(let n of Object.values(e.apps))if(n.kind==="desktop")t[n.name]={path:n.path,args:n.args||[],...n.window?{window:n.window}:{}};return t}function DDe(e){let t={};for(let n of Object.values(e.apps))if(n.kind==="mobile")t[n.name]={platform:n.platform,app_id:n.appId,...n.activity?{activity:n.activity}:{},...n.appFile?{app:n.appFile}:{}};return t}function ks(e,t){let n=e.environments;if(!n){if(t)throw Error(`No environments are configured, so "${t}" cannot be used. Add environments: { file: ./env.yaml } to forgeqe.yaml.`);return e}let r=t||n.active;if(!r)return e;let s=n.all[r];if(!s)throw Error(`No environment "${r}" in ${n.file} (environments: ${Object.keys(n.all).join(", ")||"none"}).`);let o=n.baseDesktop,a=Object.values(s.apps).flatMap((f)=>(f.kind==="web"||f.kind==="api")&&f.url?[new URL(f.url).hostname]:[]),l=n.baseHosts.length?[...new Set([...n.baseHosts,...a])]:n.baseHosts,c=s.startApp?s.apps[s.startApp].url:n.baseStartUrl,d=s.apiApp?s.apps[s.apiApp].url:n.baseApiUrl;return{...e,browser:{...e.browser,allowedHosts:l,startUrl:c},...e.api?{api:{...e.api,baseUrl:d}}:{},environments:{...n,active:r},desktop:{...o,apps:{...o.apps,...PDe(s)},secrets:[...new Set([...o.secrets,...s.secrets])]},...n.baseMobile?{mobile:{...n.baseMobile,apps:{...n.baseMobile.apps,...DDe(s)},secrets:[...new Set([...n.baseMobile.secrets,...s.secrets])]}}:{}}}function Ry(e){let t=e.environments;return t?.active?t.all[t.active]:void 0}function Kj(e,t,n){let r=ks(e,n),s=Ry(r),o=s?.apps[t];if(!o)throw Error(`No app "${t}" in ${s?`environment ${s.name}`:"env.yaml (no environment is active)"}${s?` (apps: ${Object.keys(s.apps).join(", ")||"none"})`:""}.`);if(o.kind!=="web"||!o.url)throw Error(`App "${t}" is a ${o.kind} app, not a web app: use the ${o.kind} tools for it.`);return o.url}function zk(e){let t=Ry(e);if(!t)return{};let n={FORGEQE_ACTIVE_ENV:t.name};for(let r of Object.values(t.apps)){if(r.url)n[`FORGEQE_APP_${Wj(r.name)}_URL`]=r.url;if(r.path)n[`FORGEQE_APP_${Wj(r.name)}_PATH`]=r.path;if(r.appId)n[`FORGEQE_APP_${Wj(r.name)}_ID`]=r.appId}for(let[r,s]of Object.entries(t.variables))n[`FORGEQE_VAR_${Wj(r)}`]=s;for(let[r,s]of Object.entries(n))if(/[\r\n]/.test(s))delete n[r];return n}function op(e,t=process.env){for(let n of Object.keys(t))if(MDe.test(n))delete t[n];Object.assign(t,zk(e))}function Jc(e,t){let n=Ry(e);if(!n)return"";let r=Object.values(n.apps),s=(l)=>{let c=l.url||l.path||(l.appId?`${l.platform} ${l.appId}`:"");return`- ${l.name} (${l.kind}${c?`: ${c}`:""})${l.description?`: ${l.description}`:""}`},o=Object.entries(n.variables);if(t==="browser"){let l=r.filter((c)=>c.kind==="web"||c.kind==="api");if(!l.length&&!o.length)return"";return[`Test environment: ${n.name}${n.description?` (${n.description})`:""}.`,...l.length?["Its web apps, where a goal that names one of them means:",...l.map(s),"Stay on this environment's hosts; never use another environment's URL for these apps."]:[],...o.length?["Values of this environment, for goals that refer to them:",...o.map(([c,d])=>`- ${c}: ${d}`)]:[]].join(`
|
|
184
184
|
`)}if(t==="code"){let l=zk(e),c=Object.entries(l).filter(([d])=>/_URL$/.test(d));return[`The tests run against test environment ${n.name}; others are ${Object.keys(e.environments.all).filter((d)=>d!==n.name).join(", ")||"none yet"}. A test must run unchanged in every environment:`,"- Never write an environment's URL, host or app path into a test. Read it where the repository already keeps its base URL or settings; if it keeps none, read these variables, which ForgeQE sets whenever it runs the tests, with the repository's own default as fallback:",...c.map(([d,f])=>` ${d} (now ${f})`),"- FORGEQE_ACTIVE_ENV names the environment, and FORGEQE_VAR_<NAME> holds its values ("+(o.map(([d])=>d).join(", ")||"none")+"). Secrets are never in these variables."].join(`
|
|
185
185
|
`)}let a=Object.values(e.environments.all).filter((l)=>l.name!==n.name);return[`Test environment in use: ${n.name}${n.description?` (${n.description})`:""}, from ${e.environments.file}.${a.length?` Others: ${a.map((l)=>l.name).join(", ")} (use_environment switches; tools also take env for one call).`:""}`,r.length?["Its apps (pick by description; never invent a URL or path):",...r.map(s)].join(`
|
|
@@ -834,7 +834,7 @@ CREATE INDEX IF NOT EXISTS runs_started ON runs(started_at);
|
|
|
834
834
|
`),OGe(a,r)}catch{}}function Rce(e,t,n){let r=$ce(e,t);mR.set(r,n),n.then((s)=>s.ok&&PGe(e,r,s))}async function Yx(e,t,n={}){let r=e.profiles[t];if(!r)return"is not a profile in llms.json";let s=Ih(t,r).unusable;if(s)return s;let o=$ce(e,t);if(!mR.has(o)){let l=Cce(e)[o];if(!n.profiler&&l&&Date.now()-Date.parse(l.at)<Ece)mR.set(o,Promise.resolve({ok:!0,...l.ms!==void 0?{ms:l.ms}:{}}));else Rce(e,t,(n.profiler||xce(e)).probe(t))}let a=await mR.get(o);if(!a.ok&&r.provider!=="github_copilot"&&/engine is not set up/.test(a.error||""))return null;return a.ok?null:`failed its live check (${xG(a.error)})`}function kce(e,t){let n=e.profiles[t];return n?Ih(t,n).unusable||null:"is not a profile in llms.json"}function Ph(e,t,n){let r=e.profiles[t],s=n.toLowerCase();if(/engine is not set up|forgeqe setup|no module named|copilot sdk/.test(s))return"run forgeqe setup (it installs the ForgeQE engine and GitHub's Copilot SDK)";if(r?.provider==="github_copilot"){if(/refused the|authori[sz]ation error|credentials may be expired|bad credentials/.test(s))return DGe(r);if(/not signed in|did not accept the token|classic tokens|401|auth/.test(s))return"run forgeqe auth login copilot (it opens GitHub in your browser), or set COPILOT_GITHUB_TOKEN to a fine-grained token with the Copilot Requests permission";if(/no answer in/.test(s))return"try again: Copilot's first start on a machine downloads its runtime, which can take minutes; if it keeps timing out, forgeqe doctor checks the network to GitHub";if(/conflicts with a built-in tool/.test(s))return"update ForgeQE (forgeqe update): this version names its tools in a way this Copilot does not take";if(/\bmodel\b/.test(s))return`run forgeqe auth login copilot to see the models your seat can use, and put one in llms.json as ${t}'s "model"`;return"run forgeqe doctor to check the network to GitHub, then forgeqe auth login copilot"}if(/github models/.test(s))return`remove ${t} from llms.json: GitHub Copilot replaces it (forgeqe auth login copilot sets it up)`;if(/example endpoint/.test(s))return`put your own endpoint in llms.json for ${t}`;if(/api_key_env|api key|not set in this terminal|401|invalid_api_key|unauthorized/.test(s))return r?.apiKeyEnv?`set ${r.apiKeyEnv} to ${t}'s key (in this terminal or a .env next to forgeqe.yaml)`:`set api_key_env for ${t} in llms.json`;if(/notfound|404|deployment/.test(s))return`check ${t}'s endpoint, deployment and model in llms.json`;return`check ${t} in llms.json, then forgeqe models --routes --profile-models`}function DGe(e){let n=process.env,r=e.apiKeyEnv&&n[e.apiKeyEnv]?.trim()?e.apiKeyEnv:n[sc]?.trim()?sc:null;if(r)return`Copilot refused the token in ${r}: make a new fine-grained token with the Copilot Requests permission, or remove ${r} and run forgeqe auth login copilot to sign in in the browser; if it still fails and your Copilot seat comes from your company, ask a GitHub admin to allow Copilot CLI in the Copilot policies (the Copilot SDK runs on it)`;if(pk(e))return"Copilot refused the token saved by an earlier sign-in: run forgeqe auth login copilot and choose the browser, which replaces it; if it still fails and your Copilot seat comes from your company, ask a GitHub admin to allow Copilot CLI in the Copilot policies (the Copilot SDK runs on it)";return"run forgeqe auth login copilot again (the sign-in has expired or was revoked); if it still fails and your Copilot seat comes from your company, ask a GitHub admin to allow Copilot CLI in the Copilot policies (the Copilot SDK runs on it)"}async function MGe(e,t,n,r){let s=Cy[t],o=Object.keys(e.profiles).filter((v)=>v!==n&&!["mock","none"].includes(e.profiles[v].provider)),a=o.some((v)=>!kce(e,v)),l=a?await Yx(e,n,r):kce(e,n),c=e.profiles[n]&&Ih(n,e.profiles[n]),d=!l&&c?Jx(c,t,r.tokens):null;if(!l&&!d)return null;if(!a&&!l)return null;let f=[`${n} ${l||d}${l?`. Fix: ${Ph(e,n,l)}`:""}`],m=(v)=>e.profiles[v].provider==="github_copilot"?0:1,h=o.map((v)=>Ih(v,e.profiles[v])),y=h.filter((v)=>!Jx(v,t,r.tokens)||v.unusable),w=[...y].sort((v,S)=>m(v.name)-m(S.name)||Number(S.vision===!0)-Number(v.vision===!0)||S.window-v.window||v.name.localeCompare(S.name));for(let v of w){let S=await Yx(e,v.name,r);if(!S){let R=`${n} ${l||d}${l?` (fix: ${Ph(e,n,l)})`:""}`;return{area:t,profile:v.name,reason:R}}f.push(`${v.name} ${S}. Fix: ${Ph(e,v.name,S)}`)}for(let v of h.filter((S)=>!y.includes(S)))f.push(`${v.name} ${Jx(v,t,r.tokens)}`);throw Error(`No model in llms.json can ${s.title.charAt(0).toLowerCase()}${s.title.slice(1)} right now:
|
|
835
835
|
`+f.map((v)=>` - ${v}`).join(`
|
|
836
836
|
`)+`
|
|
837
|
-
forgeqe doctor checks everything ForgeQE needs; forgeqe models --routes shows which model each area gets.`)}async function LGe(e,t,n={}){let r=await qGe(e,t,n),s=r.config;if(s.profilePinned||t!=="browser"&&s.code?.model||s.llm.provider==="mock"||s.llm.provider==="none")return r;if(!s.profiles?.[s.activeProfile])return r;let o=await MGe(s,t,s.activeProfile,n);if(!o)return r;qn("model.route",o);let a=`${t}:${o.profile}`;if(!hR.has(a))hR.add(a),n.say?.(`Model for ${t}: ${o.profile}, because ${o.reason}`);return{config:OG(s,o),decision:o}}async function qGe(e,t,n){if(RG(e,t))return{config:e,decision:null};let{facts:r,profile:s}=await AG(e,{profiler:n.profiler,say:n.say}),o=r.filter((d)=>mD(e,t).includes(d.name)),a=CG(e,t,o,s?.models||null,{tokens:n.tokens});if(!a){let d=r.find((m)=>m.name===e.activeProfile),f=d&&Jx(d,t,n.tokens);if(f){let m=CG({...e,routing:{...e.routing,routes:{}}},t,r,s?.models||null,{tokens:n.tokens});a=m&&{...m,reason:`${e.activeProfile} ${f}${f.startsWith("failed")?` (fix: ${Ph(e,e.activeProfile,f)})`:""}; ${m.reason}`}}}if(!a)return qn("model.route",{area:t,profile:e.activeProfile,reason:"no configured model passes the checks; active profile kept"}),{config:e,decision:null};let l=Nv(e)==="shadow";if(qn("model.route",{...a,...l?{shadow:!0}:{}}),l||a.profile===e.activeProfile)return{config:e,decision:{...a,...l?{shadow:l}:{}}};let c=`${t}:${a.profile}`;if(!hR.has(c))hR.add(c),n.say?.(`Model for ${t}: ${a.profile} (${a.reason})`);return{config:OG(e,a),decision:a}}async function Ir(e,t,n={}){return(await LGe(e,t,n)).config}function OG(e,t){return{...e,activeProfile:t.profile,llm:e.profiles[t.profile]}}async function Ace(e,t,n,r={}){if(RG(e,t)||Nv(e)!=="auto"||e[Sce])return null;let{facts:s,profile:o}=await AG(e,{profiler:r.profiler});if(!o)return null;let a=FGe(e.activeProfile,t,s.filter((l)=>mD(e,t).includes(l.name)),o.models,r.tokens);if(!a)return null;return qn("model.escalate",{area:t,from:e.activeProfile,to:a.profile,why:n}),r.say?.(`${n}: trying again on ${a.profile}, rated higher for ${t}.`),Object.assign(OG(e,a),{[Sce]:!0})}function FGe(e,t,n,r,s){let o=(f)=>r[f]?.ratings[t]??0,a=n.find((f)=>f.name===e),l=a?Nf(a):Number.POSITIVE_INFINITY,d=n.filter((f)=>f.name!==e&&!Jx(f,t,s)).filter((f)=>o(f.name)>o(e)||o(f.name)===o(e)&&Number.isFinite(Nf(f))&&Nf(f)>l).sort((f,m)=>o(m.name)-o(f.name)||Nf(m)-Nf(f)||f.name.localeCompare(m.name))[0];return d?{area:t,profile:d.name,rating:o(d.name),reason:`rated ${o(d.name)} for ${t}, against ${o(e)}`}:null}async function Oce(e,t={}){if(t.force)IGe();let{facts:n,profile:r}=await AG(e,{force:t.force,profiler:t.profiler,say:t.say});return Object.keys(Cy).map((s)=>{let o=RG({...e,routing:{...e.routing||{routes:{}},mode:Nv(e)==="off"?"shadow":Nv(e)}},s);if(o)return{area:s,decision:{area:s,profile:e.activeProfile,reason:o}};let a=n.filter((l)=>mD(e,s).includes(l.name));return{area:s,decision:CG(e,s,a,r?.models||null)}})}var Sce,Nf=(e)=>e.provider==="github_copilot"?0:e.price?e.price.input+e.price.output/4:Number.POSITIVE_INFINITY,fD,hR,mR,Ece=86400000,$ce=(e,t)=>fR(e,[t]),oo=(e)=>(t)=>e?.({type:"code",kind:"info",text:t});var Ci=ee(()=>{Zd();oC();Xc();tC();gce();$G();Sce=Symbol("escalated");fD=new Map,hR=new Set;mR=new Map});import{existsSync as NG,mkdirSync as Nce,readFileSync as UGe,writeFileSync as BGe}from"fs";import{createRequire as HGe}from"module";import gp from"path";function zGe(e,t){let n=[gp.resolve(t),gp.resolve(e,t)].find((s)=>NG(gp.join(s,"resume.json")));if(!n)throw Error(`No steps to resume in ${t}. Only a run that failed or was cut off part of the way leaves resume.json; run the goal without resume_from.`);let r=JSON.parse(UGe(gp.join(n,"resume.json"),"utf8"));if(!Array.isArray(r.steps)||!r.steps.length)throw Error(`${gp.join(n,"resume.json")} has no steps.`);return r}function Gs(e,t){if(t)return gp.resolve(t);let n=new Date,r=(o)=>String(o).padStart(2,"0"),s=`${n.getFullYear()}${r(n.getMonth()+1)}${r(n.getDate())}-${r(n.getHours())}${r(n.getMinutes())}${r(n.getSeconds())}`;Nce(e,{recursive:!0});for(let o=1;;o++){let a=gp.join(e,o===1?s:`${s}-${o}`);try{return Nce(a),a}catch(l){if(l.code!=="EEXIST")throw l}}}function jG(e){return(2*e+300)*1000}function GGe(){try{let e=aN("axe.min.js",()=>HGe(import.meta.url).resolve("axe-core/axe.min.js"));return NG(e)?e:null}catch{return null}}function WGe(e,t,n,r){try{let s=e.quality||Ek,o=s.visual.enabled?X7(t,s.visual,s.visual.baselineDir||gp.join(gp.dirname(e.memory?.dir||e.export.outDir),"visual-baselines"),s.budgets.visual):null,a=IN(t,s)||(o?aX(s):null);if(!a)return;if(o){a.visual=o,a.budgets.visual=s.budgets.visual!==void 0||Object.values(s.visual.pages).some((l)=>l.maxDiffPercent!==void 0);for(let l of o.pages.filter((c)=>c.over))a.breaches.push({kind:"visual",page:l.url,text:`${l.diffPercent}% of the page differs from its baseline (budget ${l.limit}%)${l.resized?`; size ${l.resized}`:""}`})}if(n.quality=a,BGe(gp.join(t,"quality.json"),`${JSON.stringify(a,null,2)}
|
|
837
|
+
forgeqe doctor checks everything ForgeQE needs; forgeqe models --routes shows which model each area gets.`)}async function LGe(e,t,n={}){let r=await qGe(e,t,n),s=r.config;if(s.profilePinned||s.routing?.fallback===!1||t!=="browser"&&s.code?.model||s.llm.provider==="mock"||s.llm.provider==="none")return r;if(!s.profiles?.[s.activeProfile])return r;let o=await MGe(s,t,s.activeProfile,n);if(!o)return r;qn("model.route",o);let a=`${t}:${o.profile}`;if(!hR.has(a))hR.add(a),n.say?.(`Model for ${t}: ${o.profile}, because ${o.reason}`);return{config:OG(s,o),decision:o}}async function qGe(e,t,n){if(RG(e,t))return{config:e,decision:null};let{facts:r,profile:s}=await AG(e,{profiler:n.profiler,say:n.say}),o=r.filter((d)=>mD(e,t).includes(d.name)),a=CG(e,t,o,s?.models||null,{tokens:n.tokens});if(!a){let d=r.find((m)=>m.name===e.activeProfile),f=d&&Jx(d,t,n.tokens);if(f){let m=CG({...e,routing:{...e.routing,routes:{}}},t,r,s?.models||null,{tokens:n.tokens});a=m&&{...m,reason:`${e.activeProfile} ${f}${f.startsWith("failed")?` (fix: ${Ph(e,e.activeProfile,f)})`:""}; ${m.reason}`}}}if(!a)return qn("model.route",{area:t,profile:e.activeProfile,reason:"no configured model passes the checks; active profile kept"}),{config:e,decision:null};let l=Nv(e)==="shadow";if(qn("model.route",{...a,...l?{shadow:!0}:{}}),l||a.profile===e.activeProfile)return{config:e,decision:{...a,...l?{shadow:l}:{}}};let c=`${t}:${a.profile}`;if(!hR.has(c))hR.add(c),n.say?.(`Model for ${t}: ${a.profile} (${a.reason})`);return{config:OG(e,a),decision:a}}async function Ir(e,t,n={}){return(await LGe(e,t,n)).config}function OG(e,t){return{...e,activeProfile:t.profile,llm:e.profiles[t.profile]}}async function Ace(e,t,n,r={}){if(RG(e,t)||Nv(e)!=="auto"||e[Sce])return null;let{facts:s,profile:o}=await AG(e,{profiler:r.profiler});if(!o)return null;let a=FGe(e.activeProfile,t,s.filter((l)=>mD(e,t).includes(l.name)),o.models,r.tokens);if(!a)return null;return qn("model.escalate",{area:t,from:e.activeProfile,to:a.profile,why:n}),r.say?.(`${n}: trying again on ${a.profile}, rated higher for ${t}.`),Object.assign(OG(e,a),{[Sce]:!0})}function FGe(e,t,n,r,s){let o=(f)=>r[f]?.ratings[t]??0,a=n.find((f)=>f.name===e),l=a?Nf(a):Number.POSITIVE_INFINITY,d=n.filter((f)=>f.name!==e&&!Jx(f,t,s)).filter((f)=>o(f.name)>o(e)||o(f.name)===o(e)&&Number.isFinite(Nf(f))&&Nf(f)>l).sort((f,m)=>o(m.name)-o(f.name)||Nf(m)-Nf(f)||f.name.localeCompare(m.name))[0];return d?{area:t,profile:d.name,rating:o(d.name),reason:`rated ${o(d.name)} for ${t}, against ${o(e)}`}:null}async function Oce(e,t={}){if(t.force)IGe();let{facts:n,profile:r}=await AG(e,{force:t.force,profiler:t.profiler,say:t.say});return Object.keys(Cy).map((s)=>{let o=RG({...e,routing:{...e.routing||{routes:{}},mode:Nv(e)==="off"?"shadow":Nv(e)}},s);if(o)return{area:s,decision:{area:s,profile:e.activeProfile,reason:o}};let a=n.filter((l)=>mD(e,s).includes(l.name));return{area:s,decision:CG(e,s,a,r?.models||null)}})}var Sce,Nf=(e)=>e.provider==="github_copilot"?0:e.price?e.price.input+e.price.output/4:Number.POSITIVE_INFINITY,fD,hR,mR,Ece=86400000,$ce=(e,t)=>fR(e,[t]),oo=(e)=>(t)=>e?.({type:"code",kind:"info",text:t});var Ci=ee(()=>{Zd();oC();Xc();tC();gce();$G();Sce=Symbol("escalated");fD=new Map,hR=new Set;mR=new Map});import{existsSync as NG,mkdirSync as Nce,readFileSync as UGe,writeFileSync as BGe}from"fs";import{createRequire as HGe}from"module";import gp from"path";function zGe(e,t){let n=[gp.resolve(t),gp.resolve(e,t)].find((s)=>NG(gp.join(s,"resume.json")));if(!n)throw Error(`No steps to resume in ${t}. Only a run that failed or was cut off part of the way leaves resume.json; run the goal without resume_from.`);let r=JSON.parse(UGe(gp.join(n,"resume.json"),"utf8"));if(!Array.isArray(r.steps)||!r.steps.length)throw Error(`${gp.join(n,"resume.json")} has no steps.`);return r}function Gs(e,t){if(t)return gp.resolve(t);let n=new Date,r=(o)=>String(o).padStart(2,"0"),s=`${n.getFullYear()}${r(n.getMonth()+1)}${r(n.getDate())}-${r(n.getHours())}${r(n.getMinutes())}${r(n.getSeconds())}`;Nce(e,{recursive:!0});for(let o=1;;o++){let a=gp.join(e,o===1?s:`${s}-${o}`);try{return Nce(a),a}catch(l){if(l.code!=="EEXIST")throw l}}}function jG(e){return(2*e+300)*1000}function GGe(){try{let e=aN("axe.min.js",()=>HGe(import.meta.url).resolve("axe-core/axe.min.js"));return NG(e)?e:null}catch{return null}}function WGe(e,t,n,r){try{let s=e.quality||Ek,o=s.visual.enabled?X7(t,s.visual,s.visual.baselineDir||gp.join(gp.dirname(e.memory?.dir||e.export.outDir),"visual-baselines"),s.budgets.visual):null,a=IN(t,s)||(o?aX(s):null);if(!a)return;if(o){a.visual=o,a.budgets.visual=s.budgets.visual!==void 0||Object.values(s.visual.pages).some((l)=>l.maxDiffPercent!==void 0);for(let l of o.pages.filter((c)=>c.over))a.breaches.push({kind:"visual",page:l.url,text:`${l.diffPercent}% of the page differs from its baseline (budget ${l.limit}%)${l.resized?`; size ${l.resized}`:""}`})}if(n.quality=a,BGe(gp.join(t,"quality.json"),`${JSON.stringify(a,null,2)}
|
|
838
838
|
`),Kle(nD(e.export.outDir),a,yB(t),gp.basename(t)),a.breaches.length)n.budgetExceeded=a.breaches.map((l)=>`${VGe(l.page)}: ${l.text}`),r?.({type:"log",level:"warn",text:`Budget exceeded: ${n.budgetExceeded.join("; ")}`})}catch(s){r?.({type:"log",level:"warn",text:`Accessibility and performance not judged: ${s.message}`})}}function VGe(e){try{let t=new URL(e);return`${t.host}${t.pathname}`}catch{return e}}function QGe(e){let t=e.config.memory;if(!t.enabled||e.memory===!1)return;return{dir:t.dir,replay:e.replay||t.replay,...Qx(e.config)==="on"?{checks_first:!0}:{}}}function jce(e,t){if(!e.config.memory.enabled||e.memory===!1||e.config.browser.rulebook===!1||!t)return null;try{return tce(e.config.memory.dir,new URL(t).host)}catch{return null}}function KGe(e,t){let n=jce(e,t);return n?[Hx(iD(n),"on this site"),hr("site-lessons")].filter(Boolean).join(`
|
|
839
839
|
|
|
840
840
|
`):""}function JGe(e,t,n,r){let s=(r.summary||"").split(`
|
package/docs/user-guide.html
CHANGED
|
@@ -596,7 +596,7 @@ bun src/index.ts init --global --profile azure-gpt4o
|
|
|
596
596
|
</tbody>
|
|
597
597
|
</table></div>
|
|
598
598
|
<p>Copilot runs one SDK session per model request, with none of Copilot's own tools and your request's system prompt in place of Copilot's. The agent's tools are offered to the model as the session's only tools; when it calls one, the gateway ends the turn and hands the call back to ForgeQE's agent runtime, which runs it (with the same permissions as on any other model) and sends the result with the next request. The gateway itself runs nothing.</p>
|
|
599
|
-
<p><strong>With Copilot and a key-based model both in <code>llms.json</code></strong>, Copilot comes first: a sign-in makes it the default, and without a <code>"default"</code> it is the active profile. Before a model is used, ForgeQE checks that it works (its sign-in or key, and one live call, kept for a day once it passes). If Copilot does not work (not signed in, the Copilot SDK missing, the model not on your seat), the other model is used and ForgeQE says why and how to fix Copilot; if a key-based model you made the default does not work, Copilot is tried first. A model that fails its check is never used, and when none works the error lists each one with what to do. <code>--profile</code> turns
|
|
599
|
+
<p><strong>With Copilot and a key-based model both in <code>llms.json</code></strong>, Copilot comes first: a sign-in makes it the default, and without a <code>"default"</code> it is the active profile. Before a model is used, ForgeQE checks that it works (its sign-in or key, and one live call, kept for a day once it passes). If Copilot does not work (not signed in, the Copilot SDK missing, the model not on your seat), the other model is used and ForgeQE says why and how to fix Copilot; if a key-based model you made the default does not work, Copilot is tried first. A model that fails its check is never used, and when none works the error lists each one with what to do. This happens with <code>llm.routing</code> off too: routing picks a model for each kind of work, the fallback only replaces one that does not work. <code>--profile</code> turns it off for one run, and <code>llm.fallback: false</code> in <code>forgeqe.yaml</code> for good: the model you chose is used as it is, and its own error is shown. Each request is billed to the seat (GitHub AI credits or premium requests, by your plan), and GitHub may rate-limit frequent automated requests. If a model is refused, check that your organization's Copilot policies allow it. ForgeQE shows no cost for a <code>github_copilot</code> profile unless you give it a <code>price</code>.</p>
|
|
600
600
|
<p><strong>In pipelines.</strong> <code>forgeqe suite ... --no-code</code> uses only the browser engine. Set <code>COPILOT_GITHUB_TOKEN</code> to a fine-grained token with the Copilot Requests permission, stored as a pipeline secret. GitHub also documents the workflow's own <code>GITHUB_TOKEN</code> with <code>permissions: copilot-requests: write</code> for organization repositories whose Copilot policy allows it (<a href="https://docs.github.com/en/copilot/how-tos/copilot-sdk/auth/server-to-server-tokens" rel="noopener">server tokens</a>). A Copilot seat belongs to one person: prefer an organization-billed token over signing a shared pipeline in to someone's seat.</p>
|
|
601
601
|
<p>GitHub Models, which ForgeQE used for the engine before, was <a href="https://github.blog/changelog/2026-07-30-github-models-is-now-retired/" rel="noopener">retired by GitHub</a> on July 30, 2026. A profile that still has <code>engine_model: "openai/..."</code> or <code>github_org</code> needs updating: drop <code>github_org</code>, and use a Copilot model id. A profile whose endpoint is GitHub Models (<code>models.github.ai</code>) is never called: remove it from <code>llms.json</code>. Copilot needs no <code>GITHUB_TOKEN</code> or <code>GH_TOKEN</code>; ForgeQE does not pass them to Copilot.</p>
|
|
602
602
|
<p><strong>When something goes wrong.</strong></p>
|
package/forgeqe.example.yaml
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
llm:
|
|
2
2
|
models_file: ./llms.json
|
|
3
3
|
# active: azure-gpt4o # profile to use; default: "default" in llms.json (Tab switches in the shell)
|
|
4
|
-
# The model router
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
|
|
4
|
+
# The model router. off (the default): the active profile for everything. auto: each area of work gets the cheapest
|
|
5
|
+
# profile in llms.json that is good enough for it; ForgeQE rates the models once with a few model calls
|
|
6
|
+
# (.forgeqe/model-profile.json), worth it with several models of different price. shadow: decide and log only.
|
|
7
|
+
# forgeqe models --routes shows the picks. --profile always overrides it.
|
|
8
|
+
routing: off
|
|
9
|
+
# A model that does not work (no key, a refused sign-in, a failed check) is replaced by one that does, Copilot
|
|
10
|
+
# first, and ForgeQE says why. false: keep the model and show its own error.
|
|
11
|
+
# fallback: true
|
|
8
12
|
# routes: # optional: the profiles an area may use (one pins it); auto: any of them
|
|
9
13
|
# browser: [azure-gpt4o]
|
|
10
14
|
# coding: auto
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capagents/forgeqe",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.6",
|
|
4
4
|
"description": "ForgeQE: a browser agent CLI that runs goals and test cases in local Chrome or Chromium, or BrowserStack and TestMu AI/LambdaTest when configured, and turns passing runs into tests in your repo",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"repository": {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
from forgeqe_engine.
|
|
1
|
+
from forgeqe_engine.stdio import take_stdin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# Before the service module binds sys.stdin as a default argument.
|
|
4
|
+
take_stdin()
|
|
5
|
+
|
|
6
|
+
from forgeqe_engine.systemcerts import use_system_certificates # noqa: E402
|
|
7
|
+
|
|
8
|
+
from .cli import main # noqa: E402
|
|
4
9
|
|
|
5
10
|
use_system_certificates()
|
|
6
11
|
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
from forgeqe_engine.
|
|
1
|
+
from forgeqe_engine.stdio import take_stdin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
take_stdin()
|
|
4
|
+
|
|
5
|
+
from forgeqe_engine.systemcerts import use_system_certificates # noqa: E402
|
|
6
|
+
|
|
7
|
+
from .cli import main # noqa: E402
|
|
4
8
|
|
|
5
9
|
use_system_certificates()
|
|
6
10
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The pipe ForgeQE writes to (a request, then "stop"), kept for this process alone.
|
|
3
|
+
|
|
4
|
+
A run reads that pipe on a thread while it starts other programs: Chrome, the browser's installer, Copilot's runtime,
|
|
5
|
+
an app under test. Each of them inherited the pipe as its stdin. On Windows a run's browser then did not start until a
|
|
6
|
+
line arrived on the pipe, so it opened only when the user pressed Stop. The programs a process starts now get an empty
|
|
7
|
+
stdin (the null device), and the process reads its own copy of the pipe.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
STD_INPUT_HANDLE = -10
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def take_stdin() -> None:
|
|
18
|
+
"""Moves stdin to a private copy (sys.stdin reads it) and makes the null device the stdin children inherit.
|
|
19
|
+
|
|
20
|
+
Call it first in an entry point, before a module binds sys.stdin (a default argument, say)."""
|
|
21
|
+
try:
|
|
22
|
+
fd = sys.stdin.fileno()
|
|
23
|
+
except (AttributeError, OSError, ValueError):
|
|
24
|
+
return # no stdin (pythonw, a closed stdin): nothing for a child to inherit
|
|
25
|
+
encoding = getattr(sys.stdin, "encoding", None) or "utf-8"
|
|
26
|
+
private = os.dup(fd) # not inheritable (PEP 446): no child gets this one
|
|
27
|
+
nul = os.open(os.devnull, os.O_RDONLY)
|
|
28
|
+
try:
|
|
29
|
+
os.dup2(nul, fd)
|
|
30
|
+
finally:
|
|
31
|
+
os.close(nul)
|
|
32
|
+
if os.name == "nt":
|
|
33
|
+
# Windows children take their stdin from the standard handle, which dup2 does not always move.
|
|
34
|
+
import ctypes
|
|
35
|
+
import msvcrt
|
|
36
|
+
|
|
37
|
+
ctypes.windll.kernel32.SetStdHandle(STD_INPUT_HANDLE, msvcrt.get_osfhandle(fd))
|
|
38
|
+
sys.stdin = os.fdopen(private, "r", encoding=encoding, errors="replace")
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
from forgeqe_engine.
|
|
1
|
+
from forgeqe_engine.stdio import take_stdin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# Before the service module binds sys.stdin as a default argument.
|
|
4
|
+
take_stdin()
|
|
5
|
+
|
|
6
|
+
from forgeqe_engine.systemcerts import use_system_certificates # noqa: E402
|
|
7
|
+
|
|
8
|
+
from .cli import main # noqa: E402
|
|
4
9
|
|
|
5
10
|
use_system_certificates()
|
|
6
11
|
|