saffron-ai 0.9.3 → 0.9.4

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/dist-pkg/cli.js CHANGED
@@ -1,103 +1,41 @@
1
1
  #!/usr/bin/env node
2
- import ae from"node:fs";import N from"node:path";import yh from"node:readline";import{Command as bh}from"commander";import xd from"node:fs";import vd from"node:path";import{AstBuilder as $d,GherkinClassicTokenMatcher as kd,Parser as Td}from"@cucumber/gherkin";import{IdGenerator as Rd,StepKeywordType as os}from"@cucumber/messages";import ei from"node:fs";import id from"node:path";import Ot from"node:fs";import st from"node:path";var ln=/\{data:([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+)\}/g,jt=st.resolve("data"),Fe,zn=new Map;function Me(e,t="data",n){jt=st.isAbsolute(t)?t:st.join(e,t),Fe=n?.trim()||void 0,zn.clear()}var De=()=>({dir:jt,env:Fe});function Qo(e,t){let n=s=>s!==null&&typeof s=="object"&&!Array.isArray(s);if(!n(e)||!n(t))return t;let r={...e};for(let[s,o]of Object.entries(t))r[s]=s in e?Qo(e[s],o):o;return r}function Zo(e,t){try{return JSON.parse(Ot.readFileSync(e,"utf8"))}catch(n){throw new Error(`data file ${t} is not valid JSON: ${n instanceof Error?n.message:String(n)}`)}}function sd(e){if(zn.has(e))return zn.get(e);let t=st.join(jt,`${e}.json`),n=Fe?st.join(jt,`${e}.${Fe}.json`):void 0,r=Ot.existsSync(t),s=n!==void 0&&Ot.existsSync(n);if(!r&&!s)throw new Error(`data file ${st.relative(process.cwd(),t)||t} does not exist${Fe?` (nor ${e}.${Fe}.json for the "${Fe}" environment)`:""}`);let o=r?Zo(t,`${e}.json`):void 0;if(s){let i=Zo(n,`${e}.${Fe}.json`);o=r?Qo(o,i):i}return zn.set(e,o),o}function ut(e){let[t,...n]=e.split("."),r=sd(t),s=[t];for(let o of n){let i=r,a=i&&typeof i=="object"?Array.isArray(i)?i[Number(o)]:i[o]:void 0;if(a===void 0)throw new Error(`"${o}" does not exist under ${s.join(".")} (in ${t}.json)`);r=a,s.push(o)}return r}function Nt(e){let t;try{t=ut(e)}catch(n){throw new Error(`{data:${e}}: ${n instanceof Error?n.message:String(n)}`)}if(t===null||typeof t=="object")throw new Error(`{data:${e}} is ${Array.isArray(t)?"a list":"an object"}, not a single value. Point at one entry, for example {data:${e}.${Array.isArray(t)?"0":Object.keys(t??{})[0]??"key"}}`);return String(t)}function ot(e){let t;try{t=ut(e)}catch(s){throw new Error(`{data:${e}}: ${s instanceof Error?s.message:String(s)}`)}let n=Array.isArray(t)?t:[t];if(n.length===0)throw new Error(`{data:${e}} is an empty list: there is nothing to check`);let r=n.find(s=>s===null||typeof s=="object");if(r!==void 0)throw new Error(`{data:${e}} must be a list of plain values (text, numbers); it contains ${Array.isArray(r)?"a list":"an object"}`);return n.map(String)}function qn(e){ot(e)}function Xo(e){return e.replace(ln,(t,n)=>{let r=ot(n);return Array.isArray(ut(n))?`${t} (a data list, currently: ${r.map(o=>`"${o}"`).join(", ")})`:r[0]})}function od(e){let t=[],n=[],r="",s=!1;for(let o=0;o<e.length;o++){let i=e[o];s?i==='"'&&e[o+1]==='"'?(r+='"',o++):i==='"'?s=!1:r+=i:i==='"'&&r===""?s=!0:i===","?(n.push(r),r=""):i===`
2
+ import re from"node:fs";import F from"node:path";import jh from"node:readline";import{Command as Nh}from"commander";import Dd from"node:fs";import Ld from"node:path";import{AstBuilder as Ud,GherkinClassicTokenMatcher as Bd,Parser as zd}from"@cucumber/gherkin";import{IdGenerator as Hd,StepKeywordType as us}from"@cucumber/messages";import fi from"node:fs";import $d from"node:path";import Ft from"node:fs";import it from"node:path";var un=/\{data:([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+)\}/g,Mt=it.resolve("data"),Me,Yn=new Map;function De(e,t="data",n){Mt=it.isAbsolute(t)?t:it.join(e,t),Me=n?.trim()||void 0,Yn.clear()}var Le=()=>({dir:Mt,env:Me});function di(e,t){let n=s=>s!==null&&typeof s=="object"&&!Array.isArray(s);if(!n(e)||!n(t))return t;let r={...e};for(let[s,o]of Object.entries(t))r[s]=s in e?di(e[s],o):o;return r}function li(e,t){try{return JSON.parse(Ft.readFileSync(e,"utf8"))}catch(n){throw new Error(`data file ${t} is not valid JSON: ${n instanceof Error?n.message:String(n)}`)}}function Sd(e){if(Yn.has(e))return Yn.get(e);let t=it.join(Mt,`${e}.json`),n=Me?it.join(Mt,`${e}.${Me}.json`):void 0,r=Ft.existsSync(t),s=n!==void 0&&Ft.existsSync(n);if(!r&&!s)throw new Error(`data file ${it.relative(process.cwd(),t)||t} does not exist${Me?` (nor ${e}.${Me}.json for the "${Me}" environment)`:""}`);let o=r?li(t,`${e}.json`):void 0;if(s){let i=li(n,`${e}.${Me}.json`);o=r?di(o,i):i}return Yn.set(e,o),o}function ht(e){let[t,...n]=e.split("."),r=Sd(t),s=[t];for(let o of n){let i=r,a=i&&typeof i=="object"?Array.isArray(i)?i[Number(o)]:i[o]:void 0;if(a===void 0)throw new Error(`"${o}" does not exist under ${s.join(".")} (in ${t}.json)`);r=a,s.push(o)}return r}function Dt(e){let t;try{t=ht(e)}catch(n){throw new Error(`{data:${e}}: ${n instanceof Error?n.message:String(n)}`)}if(t===null||typeof t=="object")throw new Error(`{data:${e}} is ${Array.isArray(t)?"a list":"an object"}, not a single value. Point at one entry, for example {data:${e}.${Array.isArray(t)?"0":Object.keys(t??{})[0]??"key"}}`);return String(t)}function at(e){let t;try{t=ht(e)}catch(s){throw new Error(`{data:${e}}: ${s instanceof Error?s.message:String(s)}`)}let n=Array.isArray(t)?t:[t];if(n.length===0)throw new Error(`{data:${e}} is an empty list: there is nothing to check`);let r=n.find(s=>s===null||typeof s=="object");if(r!==void 0)throw new Error(`{data:${e}} must be a list of plain values (text, numbers); it contains ${Array.isArray(r)?"a list":"an object"}`);return n.map(String)}function Qn(e){at(e)}function pi(e){return e.replace(un,(t,n)=>{let r=at(n);return Array.isArray(ht(n))?`${t} (a data list, currently: ${r.map(o=>`"${o}"`).join(", ")})`:r[0]})}function xd(e){let t=[],n=[],r="",s=!1;for(let o=0;o<e.length;o++){let i=e[o];s?i==='"'&&e[o+1]==='"'?(r+='"',o++):i==='"'?s=!1:r+=i:i==='"'&&r===""?s=!0:i===","?(n.push(r),r=""):i===`
3
3
  `||i==="\r"?(i==="\r"&&e[o+1]===`
4
- `&&o++,n.push(r),r="",n.some(a=>a!=="")&&t.push(n),n=[]):r+=i}return n.push(r),n.some(o=>o!=="")&&t.push(n),t.map(o=>o.map(i=>i.trim()))}function Hn(e){let t=a=>{throw new Error(`Examples: {data:${e}}: ${a}`)},n=Fe?st.join(jt,`${e}.${Fe}.csv`):void 0,r=n&&Ot.existsSync(n)?n:st.join(jt,`${e}.csv`);if(!e.includes(".")&&Ot.existsSync(r)){let[a,...c]=od(Ot.readFileSync(r,"utf8"));return(!a||c.length===0)&&t(`${e}.csv needs a header row and at least one data row`),{headers:a,rows:c.map(l=>Object.fromEntries(a.map((d,f)=>[d,l[f]??""])))}}let s;try{s=ut(e)}catch(a){return t(`${a instanceof Error?a.message:String(a)} (looked for ${e.split(".")[0]}.csv and ${e.split(".")[0]}.json in the data folder)`)}(!Array.isArray(s)||s.length===0||s.some(a=>a===null||typeof a!="object"||Array.isArray(a)))&&t('must be a non-empty list of records, like [{ "role": "Admin", "menu": "Users" }]');let o=s,i=[...new Set(o.flatMap(a=>Object.keys(a)))];return{headers:i,rows:o.map(a=>Object.fromEntries(i.map(c=>[c,a[c]===void 0||a[c]===null?"":String(a[c])])))}}function dn(e){return e.replace(ln,(t,n)=>Nt(n))}function gt(e){let t=new Set;for(let n of e)if(n)for(let r of n.matchAll(ln))t.add(r[1]);return t}function Jn(e){let t=new Map;for(let n of gt(e))try{t.set(n,Nt(n))}catch{}return t}function Zr(e,t,n=new Set){let r=new Map;for(let[i,a]of t)r.set(a,[...r.get(a)??[],i]);let s=[];for(let[i,a]of r){let c=a.length===1?a:a.filter(l=>n.has(l));c.length===1&&s.push([c[0],i])}s.sort((i,a)=>a[1].length-i[1].length);for(let[i,a]of s)if(a!==""&&e===a)return`{data:${i}}`;let o=e;for(let[i,a]of s)a.length<4||(o=o.split(a).join(`{data:${i}}`));return o}var Wn=/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g;function pn(e){return e.replace(Wn,(t,n)=>{let r=process.env[n];if(r===void 0)throw new Error(`environment variable ${n} is not set (referenced as {env:${n}}) \u2014 export it or add it to .env`);return r})}function Xr(e){let t=new Set;for(let n of e)if(n)for(let r of n.matchAll(Wn))t.add(r[1]);return t}function le(e){let t=[];for(let n of e.steps){t.push(n.text,n.resolvedText),n.docString&&t.push(n.docString),n.resolvedDocString&&t.push(n.resolvedDocString);for(let r of n.table??[])t.push(...r);for(let r of n.resolvedTable??[])t.push(...r)}return t}function es(e){let t=[];for(let n of gt(e))try{t.push(...ot(n))}catch{}return t}function Ye(e){let t=new Map,n=le(e);for(let r of Xr([...n,...es(n)])){let s=process.env[r];s!==void 0&&t.set(r,s)}return t}var ht=3,Qr=String.raw`\{(?:env:[A-Za-z_][A-Za-z0-9_]*|data:[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+|unique:[A-Za-z0-9_-]+(?::digits)?)\}`,Gn={'"':'\\"',"\\":"\\\\","\b":"\\b","\f":"\\f","\n":"\\n","\r":"\\r"," ":"\\t"};function ad(e,t,n,r){let s="";for(let o of e.split("")){let i=o.charCodeAt(0);if(t?.test(o)||i<32&&!Gn[o]){let a=i.toString(16).padStart(4,"0");s+="\\u"+(n?a.toUpperCase():a)}else Gn[o]?s+=Gn[o]:o==="/"&&r?s+="\\/":s+=o}return s}function cd(e){return[e,e.replace(/%[0-9A-F]{2}/g,t=>t.toLowerCase())]}function ti(e,t,n){return e.replace(/[&<>"'+]|[^\x00-\x7f]/gu,r=>t[r]??(n&&r>"\x7F"?n(r.codePointAt(0)):r))}function ld(e,t){let n="";for(let r of e){let s=r<=" "||r>="\x7F"||t.test(r);n+=s?[...Buffer.from(r,"utf8")].map(o=>"%"+o.toString(16).toUpperCase().padStart(2,"0")).join(""):r}return n}var dd=[/["#<>']/,/["#<>?^`{}]/,/["#<>?^`{|}]/,/["#<>?^`{}/:;=@[\\\]|]/,/["#<>?^`{}/:;=@[\\\]|']/];function pd(e){let t=/[^\x00-\x7f]/,n=new Set([e]);for(let[s,o,i]of[[void 0,!1,!1],[void 0,!1,!0],[t,!1,!1],[t,!0,!1],[t,!1,!0],[/[<>&\u2028\u2029]/,!1,!1],[/[^\x00-\x7f]|[<>&'+`"]/,!0,!1]])n.add(ad(e,s,o,i));let r=s=>{try{for(let o of cd(s()))n.add(o)}catch{}};r(()=>encodeURIComponent(e)),r(()=>encodeURIComponent(e).replaceAll("'","%27")),r(()=>encodeURI(e)),r(()=>new URLSearchParams([["",e]]).toString().slice(1));for(let s of dd)r(()=>ld(e,s));for(let s of[{"&":"&amp;","<":"&lt;",">":"&gt;"},{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"},{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;"},{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&apos;"},{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&#34;","'":"&#39;","+":"&#43;"}])n.add(ti(e,s));return n.add(ti(e,{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;","+":"&#x2B;"},s=>`&#x${s.toString(16).toUpperCase()};`)),[...n]}var fn=e=>e.map(t=>`{env:${t}}`).join(""),Ke=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),fd=e=>Buffer.from(e,"utf8").toString("latin1");function mt(e,t={}){let n=[...e.entries()].filter(([,p])=>p.length>=ht).sort((p,b)=>b[1].length-p[1].length),r=new Map,s=new Set;for(let[p,b]of n){t.standaloneBelow!==void 0&&b.length<t.standaloneBelow&&s.add(b);let v=t.encodings?pd(b):[b];for(let k of v){let w=t.bytes?[fd(k),.../^[\x00-\xff]*$/.test(k)?[k]:[]]:[k];for(let x of w)r.has(x)||r.set(x,p)}}let o=new Set(n.map(([,p])=>p)),i=new Set(["u"]);for(let[,p]of n)for(let b of p){let v=Gn[b]??(b==="/"?"\\/":void 0);v&&i.add(v.slice(1))}let a=new RegExp(`\\\\[${[...i].map(Ke).join("")}]`);if(r.size===0)return{empty:!0,mask:p=>p,matches:()=>[],occursIn:()=>!1,isValue:()=>!1,hiddenByEscape:a};let c=[...r.keys()].sort((p,b)=>b.length-p.length).map(p=>s.has(p)?`(?<![A-Za-z0-9])${Ke(p)}(?![A-Za-z0-9])`:Ke(p)).join("|"),l=new RegExp(`(${Qr})|${c}`,"g"),d=new RegExp(`(${Qr})|${c}`,"y"),f=new RegExp(c),m=p=>{let b=[];l.lastIndex=0;for(let v=l.exec(p);v!==null;v=l.exec(p)){if(v[1]!==void 0)continue;let k=v.index,w=k+v[0].length,x=[r.get(v[0])];for(let y=k+1;y<w;y++){d.lastIndex=y;let h=d.exec(p);h&&h[1]===void 0&&y+h[0].length>w&&(w=y+h[0].length,x.push(r.get(h[0])))}b.push({start:k,end:w,names:[...new Set(x)]}),l.lastIndex=w}return b};return{empty:!1,hiddenByEscape:a,occursIn:p=>f.test(p),isValue:p=>o.has(p),matches:m,mask(p){let b="",v=0;for(let k of m(p))b+=p.slice(v,k.start)+fn(k.names),v=k.end;return v===0?p:b+p.slice(v)}}}function ts(e,t,n=r=>r){let r=new Map;for(let i of e)for(let a of i.split(/[\s"'`]+/)){let c=a.replace(/^[.,;:!?()[\]]+|[.,;:!?()[\]]+$/g,""),l=[...c.matchAll(Wn)].map(m=>m[1]);if(l.length===0||l.some(m=>!t.has(m)))continue;let d=c.replace(Wn,(m,p)=>t.get(p));if(d===c||d.length<ht||c===`{env:${l[0]}}`)continue;let f=n(d);r.has(f)||r.set(f,c.split(/(\{env:[A-Za-z_][A-Za-z0-9_]*\})/).map((m,p)=>p%2?m:n(m)).join(""))}if(r.size===0)return i=>i;let s=[...r.keys()].sort((i,a)=>a.length-i.length).map(Ke).join("|"),o=new RegExp(`(${Qr})|${s}`,"g");return i=>i.replace(o,(a,c)=>c!==void 0?a:r.get(a))}var ni=new WeakMap;function Ze(e,t){let n=ni.get(t);return n||(n=mt(t,{encodings:!0}),ni.set(t,n)),n.mask(e)}function ri(e){let t=id.join(e,".env");if(ei.existsSync(t))for(let n of ei.readFileSync(t,"utf8").split(`
5
- `)){let r=n.trim();if(!r||r.startsWith("#"))continue;let s=r.indexOf("=");if(s<=0)continue;let o=r.slice(0,s).trim();if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(o))continue;let i=r.slice(s+1).trim();(i.startsWith('"')&&i.endsWith('"')||i.startsWith("'")&&i.endsWith("'"))&&(i=i.slice(1,-1)),process.env[o]===void 0&&(process.env[o]=i)}}function yt(e,t){return e.replace(/<([^<>]+)>/g,(n,r)=>r in t?t[r]:n)}function fe(e){if(!e||e.length===0)return{};let t={};if(e.every(n=>n.length===2)){for(let[n,r]of e)t[`table:${n}`]=r;return t}if(e.length>=2){let[n,...r]=e;if(n.some(s=>!s))return{};r.forEach((s,o)=>{n.forEach((i,a)=>{s[a]!==void 0&&(t[`table:${o+1}:${i}`]=s[a])})})}return t}function bt(e){return e!==void 0?{docstring:e}:{}}function it(e){let t=new Map;for(let r of e){let s={...fe(r.resolvedTable??r.table),...bt(r.resolvedDocString??r.docString)};for(let[o,i]of Object.entries(s)){let a=t.get(o);a||t.set(o,a=new Set),a.add(i)}}let n={};for(let[r,s]of t)s.size===1&&(n[r]=[...s][0]);return n}function ns(e){return e.replace(/\\.|["'`]|>>/g,t=>t[0]==="\\"?t:t===">>"?"\\>\\>":`\\${t}`)}var oe={quoted:e=>JSON.stringify(e).slice(1,-1),regex:e=>ns(Ke(e).replaceAll("/","\\/")),pattern:e=>Ke(e)};function Ft(e,t,n=r=>r){let r="",s=0;for(let o of e.matchAll(/"((?:[^"\\]|\\.)*)"|=\/((?:[^/\\]|\\.)*)\/([a-z]*)/g)){let[i,a,c,l]=o;a===void 0&&/[uv]/.test(l)||(r+=n(e.slice(s,o.index))+(a!==void 0?`"${t(a,"quoted")}"`:`=/${t(c,"regex")}/${l}`),s=o.index+i.length)}return r+n(e.slice(s))}import rs from"node:fs";import Vn from"node:path";import{AstBuilder as ud,GherkinClassicTokenMatcher as gd,Parser as hd}from"@cucumber/gherkin";import{IdGenerator as md}from"@cucumber/messages";var ie=class extends Error{},yd=/^(\s*)StepSet:\s*(.+?)\s*$/,bd=/^(\s*)StepSet\s+([^\s:].*?)\s*$/;function ss(e){let t=e.split(`
6
- `),n=new Map,r=new Map;return{source:t.map((o,i)=>{let a=o.match(yd);if(a)return n.set(i+1,a[2]),`${a[1]}Scenario: ${a[2]}`;let c=o.match(bd);return c?(r.set(i+1,c[2]),`${c[1]}* StepSet ${c[2]}`):o}).join(`
7
- `),definitions:n,invocations:r}}function wd(e){return new hd(new ud(md.uuid()),new gd).parse(e)}function si(e,t){for(let n of e)n.scenario?t(n.scenario):"rule"in n&&n.rule&&si(n.rule.children,t)}function Qe(e,t){let n=new Map;for(let r of e){let s=Vn.relative(t,r),o=ss(rs.readFileSync(r,"utf8"));if(o.definitions.size===0)continue;let i=wd(o.source);i.feature&&si(i.feature.children,a=>{let c=o.definitions.get(a.location.line);if(c===void 0)return;let l=n.get(c);if(l)throw new ie(`StepSet "${c}" is defined twice: ${l.file}:${l.line} and ${s}:${a.location.line}. Set names are unique across the project. If line ${a.location.line} was meant to INVOKE the set inside a scenario, remove the colon: "StepSet ${c}".`);if(a.examples.length>0)throw new ie(`StepSet "${c}" (${s}:${a.location.line}) has an Examples table \u2014 step sets take their values from the invoking scenario.`);for(let d of a.steps)if(o.invocations.has(d.location.line))throw new ie(`StepSet "${c}" (${s}:${a.location.line}) invokes another step set at line ${d.location.line} \u2014 step sets cannot be nested.`);n.set(c,{name:c,file:s,line:a.location.line,steps:[...a.steps]})})}return n}var Sd=[".feature",".saffron"];function Xe(e){return Sd.some(t=>e.endsWith(t))}function Re(e,t,n){let r=Vn.isAbsolute(t.features??"features")?t.features:Vn.join(e,t.features??"features"),s=new Set(wt(r));for(let o of n)o.endsWith(".saffron")&&s.add(o);return Qe([...s].sort(),e)}function wt(e){if(!rs.existsSync(e))return[];let t=[];for(let n of rs.readdirSync(e,{recursive:!0})){let r=String(n);r.endsWith(".saffron")&&t.push(Vn.join(e,r))}return t.sort()}function oi(e,t,n){return e.map(({step:r,source:s})=>{let o;switch(r.keywordType){case os.OUTCOME:o="assertion";break;case os.CONTEXT:case os.ACTION:o="action";break;default:o=n.value}n.value=o;let i=r.dataTable?r.dataTable.rows.map(c=>c.cells.map(l=>l.value)):void 0,a=r.docString?.content;return{keyword:r.keyword.trim(),kind:o,text:r.text,resolvedText:yt(r.text,t),table:i,resolvedTable:i?.map(c=>c.map(l=>yt(l,t))),docString:a,resolvedDocString:a!==void 0?yt(a,t):void 0,source:s}})}function ii(e,t,n,r,s){let o=[];for(let i of e){let a=r.get(i.location.line);if(a===void 0){o.push({step:i,source:{file:t,line:i.location.line,fromBackground:n}});continue}let c=s?.get(a);if(!c)throw new ie(`${t}:${i.location.line} invokes StepSet "${a}", which is not defined anywhere in the project.`);for(let l of c.steps)o.push({step:l,source:{file:c.file,line:l.location.line,fromStepSet:c.name,fromBackground:n}})}return o}function Ed(e,t,n,r,s,o=!1){let i=e.tags.map(c=>c.name),a=[];if(e.examples.length===0)a.push({params:{},label:""});else{let c=0;for(let l of e.examples){let d=l.name.trim().match(/^\{data:([A-Za-z0-9_.-]+)\}$/);if(d&&!l.tableHeader){let m=`${t}:${l.location.line}`;if(!o)throw new ie(`${m} reads Examples from a data file, which only .saffron files understand. Rename the file to ${t.replace(/\.feature$/,".saffron")}: nothing else needs to change.`);let p;try{p=Hn(d[1])}catch(k){throw new ie(`${m} ${k instanceof Error?k.message:String(k)}`)}let v=[...new Set([...r,...s].flatMap(k=>[k.step.text,k.step.docString?.content??"",...k.step.dataTable?.rows.flatMap(w=>w.cells.map(x=>x.value))??[]].flatMap(w=>[...w.matchAll(/<([^<>]+)>/g)].map(x=>x[1]))))].filter(k=>!p.headers.includes(k)&&!k.startsWith("table:")&&k!=="docstring");if(v.length>0)throw new ie(`${m} the scenario uses <${v.join(">, <")}>, but {data:${d[1]}} has the columns ${p.headers.map(k=>`"${k}"`).join(", ")}.`);for(let k of p.rows)c+=1,a.push({params:k,label:` (example ${c})`});continue}let f=l.tableHeader?.cells.map(m=>m.value)??[];for(let m of l.tableBody){c+=1;let p={};m.cells.forEach((b,v)=>{p[f[v]]=b.value}),a.push({params:p,label:` (example ${c})`})}}}return a.map(({params:c,label:l})=>{let d={value:"action"};return{featurePath:t,featureName:n,name:e.name,displayName:`${e.name}${l}`,tags:i,steps:[...oi(r,c,d),...oi(s,c,d)],params:c}})}function he(e,t,n){let r=xd.readFileSync(e,"utf8"),s=e.endsWith(".saffron"),o=s?ss(r):{source:r,definitions:new Map,invocations:new Map},i=new Td(new $d(Rd.uuid()),new kd),a=vd.relative(t,e),c;try{c=i.parse(o.source)}catch(m){let p=(m instanceof Error?m.message:String(m)).split(`
8
- `).filter(v=>/^\(\d+:\d+\)/.test(v)).map(v=>`${a}${v.replace(/^\((\d+):(\d+)\)/,":$1:$2")}`),b=!s&&/^\s*StepSet\b/m.test(r);throw new ie(b?`${a} uses StepSet, which only .saffron files understand. Rename it to ${a.replace(/\.feature$/,".saffron")}: nothing else needs to change (a .saffron file accepts everything a .feature file does).`:`${a} is not valid Gherkin:
4
+ `&&o++,n.push(r),r="",n.some(a=>a!=="")&&t.push(n),n=[]):r+=i}return n.push(r),n.some(o=>o!=="")&&t.push(n),t.map(o=>o.map(i=>i.trim()))}function Zn(e){let t=a=>{throw new Error(`Examples: {data:${e}}: ${a}`)},n=Me?it.join(Mt,`${e}.${Me}.csv`):void 0,r=n&&Ft.existsSync(n)?n:it.join(Mt,`${e}.csv`);if(!e.includes(".")&&Ft.existsSync(r)){let[a,...c]=xd(Ft.readFileSync(r,"utf8"));return(!a||c.length===0)&&t(`${e}.csv needs a header row and at least one data row`),{headers:a,rows:c.map(l=>Object.fromEntries(a.map((d,f)=>[d,l[f]??""])))}}let s;try{s=ht(e)}catch(a){return t(`${a instanceof Error?a.message:String(a)} (looked for ${e.split(".")[0]}.csv and ${e.split(".")[0]}.json in the data folder)`)}(!Array.isArray(s)||s.length===0||s.some(a=>a===null||typeof a!="object"||Array.isArray(a)))&&t('must be a non-empty list of records, like [{ "role": "Admin", "menu": "Users" }]');let o=s,i=[...new Set(o.flatMap(a=>Object.keys(a)))];return{headers:i,rows:o.map(a=>Object.fromEntries(i.map(c=>[c,a[c]===void 0||a[c]===null?"":String(a[c])])))}}function gn(e){return e.replace(un,(t,n)=>Dt(n))}function mt(e){let t=new Set;for(let n of e)if(n)for(let r of n.matchAll(un))t.add(r[1]);return t}function Xn(e){let t=new Map;for(let n of mt(e))try{t.set(n,Dt(n))}catch{}return t}function os(e,t,n=new Set){let r=new Map;for(let[i,a]of t)r.set(a,[...r.get(a)??[],i]);let s=[];for(let[i,a]of r){let c=a.length===1?a:a.filter(l=>n.has(l));c.length===1&&s.push([c[0],i])}s.sort((i,a)=>a[1].length-i[1].length);for(let[i,a]of s)if(a!==""&&e===a)return`{data:${i}}`;let o=e;for(let[i,a]of s)a.length<4||(o=o.split(a).join(`{data:${i}}`));return o}var tr=/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g;function hn(e){return e.replace(tr,(t,n)=>{let r=process.env[n];if(r===void 0)throw new Error(`environment variable ${n} is not set (referenced as {env:${n}}) \u2014 export it or add it to .env`);return r})}function as(e){let t=new Set;for(let n of e)if(n)for(let r of n.matchAll(tr))t.add(r[1]);return t}function de(e){let t=[];for(let n of e.steps){t.push(n.text,n.resolvedText),n.docString&&t.push(n.docString),n.resolvedDocString&&t.push(n.resolvedDocString);for(let r of n.table??[])t.push(...r);for(let r of n.resolvedTable??[])t.push(...r)}return t}function cs(e){let t=[];for(let n of mt(e))try{t.push(...at(n))}catch{}return t}function Ze(e){let t=new Map,n=de(e);for(let r of as([...n,...cs(n)])){let s=process.env[r];s!==void 0&&t.set(r,s)}return t}var yt=3,is=String.raw`\{(?:env:[A-Za-z_][A-Za-z0-9_]*|data:[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+|unique:[A-Za-z0-9_-]+(?::digits)?)\}`,er={'"':'\\"',"\\":"\\\\","\b":"\\b","\f":"\\f","\n":"\\n","\r":"\\r"," ":"\\t"};function kd(e,t,n,r){let s="";for(let o of e.split("")){let i=o.charCodeAt(0);if(t?.test(o)||i<32&&!er[o]){let a=i.toString(16).padStart(4,"0");s+="\\u"+(n?a.toUpperCase():a)}else er[o]?s+=er[o]:o==="/"&&r?s+="\\/":s+=o}return s}function Td(e){return[e,e.replace(/%[0-9A-F]{2}/g,t=>t.toLowerCase())]}function ui(e,t,n){return e.replace(/[&<>"'+]|[^\x00-\x7f]/gu,r=>t[r]??(n&&r>"\x7F"?n(r.codePointAt(0)):r))}function Rd(e,t){let n="";for(let r of e){let s=r<=" "||r>="\x7F"||t.test(r);n+=s?[...Buffer.from(r,"utf8")].map(o=>"%"+o.toString(16).toUpperCase().padStart(2,"0")).join(""):r}return n}var Ed=[/["#<>']/,/["#<>?^`{}]/,/["#<>?^`{|}]/,/["#<>?^`{}/:;=@[\\\]|]/,/["#<>?^`{}/:;=@[\\\]|']/];function Ad(e){let t=/[^\x00-\x7f]/,n=new Set([e]);for(let[s,o,i]of[[void 0,!1,!1],[void 0,!1,!0],[t,!1,!1],[t,!0,!1],[t,!1,!0],[/[<>&\u2028\u2029]/,!1,!1],[/[^\x00-\x7f]|[<>&'+`"]/,!0,!1]])n.add(kd(e,s,o,i));let r=s=>{try{for(let o of Td(s()))n.add(o)}catch{}};r(()=>encodeURIComponent(e)),r(()=>encodeURIComponent(e).replaceAll("'","%27")),r(()=>encodeURI(e)),r(()=>new URLSearchParams([["",e]]).toString().slice(1));for(let s of Ed)r(()=>Rd(e,s));for(let s of[{"&":"&amp;","<":"&lt;",">":"&gt;"},{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"},{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;"},{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&apos;"},{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&#34;","'":"&#39;","+":"&#43;"}])n.add(ui(e,s));return n.add(ui(e,{"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;","+":"&#x2B;"},s=>`&#x${s.toString(16).toUpperCase()};`)),[...n]}var mn=e=>e.map(t=>`{env:${t}}`).join(""),Qe=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),_d=e=>Buffer.from(e,"utf8").toString("latin1");function bt(e,t={}){let n=[...e.entries()].filter(([,p])=>p.length>=yt).sort((p,b)=>b[1].length-p[1].length),r=new Map,s=new Set;for(let[p,b]of n){t.standaloneBelow!==void 0&&b.length<t.standaloneBelow&&s.add(b);let $=t.encodings?Ad(b):[b];for(let k of $){let w=t.bytes?[_d(k),.../^[\x00-\xff]*$/.test(k)?[k]:[]]:[k];for(let x of w)r.has(x)||r.set(x,p)}}let o=new Set(n.map(([,p])=>p)),i=new Set(["u"]);for(let[,p]of n)for(let b of p){let $=er[b]??(b==="/"?"\\/":void 0);$&&i.add($.slice(1))}let a=new RegExp(`\\\\[${[...i].map(Qe).join("")}]`);if(r.size===0)return{empty:!0,mask:p=>p,matches:()=>[],occursIn:()=>!1,isValue:()=>!1,hiddenByEscape:a};let c=[...r.keys()].sort((p,b)=>b.length-p.length).map(p=>s.has(p)?`(?<![A-Za-z0-9])${Qe(p)}(?![A-Za-z0-9])`:Qe(p)).join("|"),l=new RegExp(`(${is})|${c}`,"g"),d=new RegExp(`(${is})|${c}`,"y"),f=new RegExp(c),m=p=>{let b=[];l.lastIndex=0;for(let $=l.exec(p);$!==null;$=l.exec(p)){if($[1]!==void 0)continue;let k=$.index,w=k+$[0].length,x=[r.get($[0])];for(let y=k+1;y<w;y++){d.lastIndex=y;let u=d.exec(p);u&&u[1]===void 0&&y+u[0].length>w&&(w=y+u[0].length,x.push(r.get(u[0])))}b.push({start:k,end:w,names:[...new Set(x)]}),l.lastIndex=w}return b};return{empty:!1,hiddenByEscape:a,occursIn:p=>f.test(p),isValue:p=>o.has(p),matches:m,mask(p){let b="",$=0;for(let k of m(p))b+=p.slice($,k.start)+mn(k.names),$=k.end;return $===0?p:b+p.slice($)}}}function ls(e,t,n=r=>r){let r=new Map;for(let i of e)for(let a of i.split(/[\s"'`]+/)){let c=a.replace(/^[.,;:!?()[\]]+|[.,;:!?()[\]]+$/g,""),l=[...c.matchAll(tr)].map(m=>m[1]);if(l.length===0||l.some(m=>!t.has(m)))continue;let d=c.replace(tr,(m,p)=>t.get(p));if(d===c||d.length<yt||c===`{env:${l[0]}}`)continue;let f=n(d);r.has(f)||r.set(f,c.split(/(\{env:[A-Za-z_][A-Za-z0-9_]*\})/).map((m,p)=>p%2?m:n(m)).join(""))}if(r.size===0)return i=>i;let s=[...r.keys()].sort((i,a)=>a.length-i.length).map(Qe).join("|"),o=new RegExp(`(${is})|${s}`,"g");return i=>i.replace(o,(a,c)=>c!==void 0?a:r.get(a))}var gi=new WeakMap;function Xe(e,t){let n=gi.get(t);return n||(n=bt(t,{encodings:!0}),gi.set(t,n)),n.mask(e)}function hi(e){let t=$d.join(e,".env");if(fi.existsSync(t))for(let n of fi.readFileSync(t,"utf8").split(`
5
+ `)){let r=n.trim();if(!r||r.startsWith("#"))continue;let s=r.indexOf("=");if(s<=0)continue;let o=r.slice(0,s).trim();if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(o))continue;let i=r.slice(s+1).trim();(i.startsWith('"')&&i.endsWith('"')||i.startsWith("'")&&i.endsWith("'"))&&(i=i.slice(1,-1)),process.env[o]===void 0&&(process.env[o]=i)}}function wt(e,t){return e.replace(/<([^<>]+)>/g,(n,r)=>r in t?t[r]:n)}function ue(e){if(!e||e.length===0)return{};let t={};if(e.every(n=>n.length===2)){for(let[n,r]of e)t[`table:${n}`]=r;return t}if(e.length>=2){let[n,...r]=e;if(n.some(s=>!s))return{};r.forEach((s,o)=>{n.forEach((i,a)=>{s[a]!==void 0&&(t[`table:${o+1}:${i}`]=s[a])})})}return t}function vt(e){return e!==void 0?{docstring:e}:{}}function ct(e){let t=new Map;for(let r of e){let s={...ue(r.resolvedTable??r.table),...vt(r.resolvedDocString??r.docString)};for(let[o,i]of Object.entries(s)){let a=t.get(o);a||t.set(o,a=new Set),a.add(i)}}let n={};for(let[r,s]of t)s.size===1&&(n[r]=[...s][0]);return n}function ds(e){return e.replace(/\\.|["'`]|>>/g,t=>t[0]==="\\"?t:t===">>"?"\\>\\>":`\\${t}`)}var ae={quoted:e=>JSON.stringify(e).slice(1,-1),regex:e=>ds(Qe(e).replaceAll("/","\\/")),pattern:e=>Qe(e)};function Lt(e,t,n=r=>r){let r="",s=0;for(let o of e.matchAll(/"((?:[^"\\]|\\.)*)"|=\/((?:[^/\\]|\\.)*)\/([a-z]*)/g)){let[i,a,c,l]=o;a===void 0&&/[uv]/.test(l)||(r+=n(e.slice(s,o.index))+(a!==void 0?`"${t(a,"quoted")}"`:`=/${t(c,"regex")}/${l}`),s=o.index+i.length)}return r+n(e.slice(s))}import ps from"node:fs";import nr from"node:path";import{AstBuilder as Id,GherkinClassicTokenMatcher as Pd,Parser as Cd}from"@cucumber/gherkin";import{IdGenerator as Od}from"@cucumber/messages";var ce=class extends Error{},jd=/^(\s*)StepSet:\s*(.+?)\s*$/,Nd=/^(\s*)StepSet\s+([^\s:].*?)\s*$/;function fs(e){let t=e.split(`
6
+ `),n=new Map,r=new Map;return{source:t.map((o,i)=>{let a=o.match(jd);if(a)return n.set(i+1,a[2]),`${a[1]}Scenario: ${a[2]}`;let c=o.match(Nd);return c?(r.set(i+1,c[2]),`${c[1]}* StepSet ${c[2]}`):o}).join(`
7
+ `),definitions:n,invocations:r}}function Fd(e){return new Cd(new Id(Od.uuid()),new Pd).parse(e)}function mi(e,t){for(let n of e)n.scenario?t(n.scenario):"rule"in n&&n.rule&&mi(n.rule.children,t)}function et(e,t){let n=new Map;for(let r of e){let s=nr.relative(t,r),o=fs(ps.readFileSync(r,"utf8"));if(o.definitions.size===0)continue;let i=Fd(o.source);i.feature&&mi(i.feature.children,a=>{let c=o.definitions.get(a.location.line);if(c===void 0)return;let l=n.get(c);if(l)throw new ce(`StepSet "${c}" is defined twice: ${l.file}:${l.line} and ${s}:${a.location.line}. Set names are unique across the project. If line ${a.location.line} was meant to INVOKE the set inside a scenario, remove the colon: "StepSet ${c}".`);if(a.examples.length>0)throw new ce(`StepSet "${c}" (${s}:${a.location.line}) has an Examples table \u2014 step sets take their values from the invoking scenario.`);for(let d of a.steps)if(o.invocations.has(d.location.line))throw new ce(`StepSet "${c}" (${s}:${a.location.line}) invokes another step set at line ${d.location.line} \u2014 step sets cannot be nested.`);n.set(c,{name:c,file:s,line:a.location.line,steps:[...a.steps]})})}return n}var Md=[".feature",".saffron"];function tt(e){return Md.some(t=>e.endsWith(t))}function Ee(e,t,n){let r=nr.isAbsolute(t.features??"features")?t.features:nr.join(e,t.features??"features"),s=new Set(St(r));for(let o of n)o.endsWith(".saffron")&&s.add(o);return et([...s].sort(),e)}function St(e){if(!ps.existsSync(e))return[];let t=[];for(let n of ps.readdirSync(e,{recursive:!0})){let r=String(n);r.endsWith(".saffron")&&t.push(nr.join(e,r))}return t.sort()}function yi(e,t,n){return e.map(({step:r,source:s})=>{let o;switch(r.keywordType){case us.OUTCOME:o="assertion";break;case us.CONTEXT:case us.ACTION:o="action";break;default:o=n.value}n.value=o;let i=r.dataTable?r.dataTable.rows.map(c=>c.cells.map(l=>l.value)):void 0,a=r.docString?.content;return{keyword:r.keyword.trim(),kind:o,text:r.text,resolvedText:wt(r.text,t),table:i,resolvedTable:i?.map(c=>c.map(l=>wt(l,t))),docString:a,resolvedDocString:a!==void 0?wt(a,t):void 0,source:s}})}function bi(e,t,n,r,s){let o=[];for(let i of e){let a=r.get(i.location.line);if(a===void 0){o.push({step:i,source:{file:t,line:i.location.line,fromBackground:n}});continue}let c=s?.get(a);if(!c)throw new ce(`${t}:${i.location.line} invokes StepSet "${a}", which is not defined anywhere in the project.`);for(let l of c.steps)o.push({step:l,source:{file:c.file,line:l.location.line,fromStepSet:c.name,fromBackground:n}})}return o}function qd(e,t,n,r,s,o=!1){let i=e.tags.map(c=>c.name),a=[];if(e.examples.length===0)a.push({params:{},label:""});else{let c=0;for(let l of e.examples){let d=l.name.trim().match(/^\{data:([A-Za-z0-9_.-]+)\}$/);if(d&&!l.tableHeader){let m=`${t}:${l.location.line}`;if(!o)throw new ce(`${m} reads Examples from a data file, which only .saffron files understand. Rename the file to ${t.replace(/\.feature$/,".saffron")}: nothing else needs to change.`);let p;try{p=Zn(d[1])}catch(k){throw new ce(`${m} ${k instanceof Error?k.message:String(k)}`)}let $=[...new Set([...r,...s].flatMap(k=>[k.step.text,k.step.docString?.content??"",...k.step.dataTable?.rows.flatMap(w=>w.cells.map(x=>x.value))??[]].flatMap(w=>[...w.matchAll(/<([^<>]+)>/g)].map(x=>x[1]))))].filter(k=>!p.headers.includes(k)&&!k.startsWith("table:")&&k!=="docstring");if($.length>0)throw new ce(`${m} the scenario uses <${$.join(">, <")}>, but {data:${d[1]}} has the columns ${p.headers.map(k=>`"${k}"`).join(", ")}.`);for(let k of p.rows)c+=1,a.push({params:k,label:` (example ${c})`});continue}let f=l.tableHeader?.cells.map(m=>m.value)??[];for(let m of l.tableBody){c+=1;let p={};m.cells.forEach((b,$)=>{p[f[$]]=b.value}),a.push({params:p,label:` (example ${c})`})}}}return a.map(({params:c,label:l})=>{let d={value:"action"};return{featurePath:t,featureName:n,name:e.name,displayName:`${e.name}${l}`,tags:i,steps:[...yi(r,c,d),...yi(s,c,d)],params:c}})}function me(e,t,n){let r=Dd.readFileSync(e,"utf8"),s=e.endsWith(".saffron"),o=s?fs(r):{source:r,definitions:new Map,invocations:new Map},i=new zd(new Ud(Hd.uuid()),new Bd),a=Ld.relative(t,e),c;try{c=i.parse(o.source)}catch(m){let p=(m instanceof Error?m.message:String(m)).split(`
8
+ `).filter($=>/^\(\d+:\d+\)/.test($)).map($=>`${a}${$.replace(/^\((\d+):(\d+)\)/,":$1:$2")}`),b=!s&&/^\s*StepSet\b/m.test(r);throw new ce(b?`${a} uses StepSet, which only .saffron files understand. Rename it to ${a.replace(/\.feature$/,".saffron")}: nothing else needs to change (a .saffron file accepts everything a .feature file does).`:`${a} is not valid Gherkin:
9
9
  ${p.join(`
10
- `)||(m instanceof Error?m.message:String(m))}`)}if(!c.feature)return[];let l=c.feature.name,d=[],f=(m,p)=>{let b=p;for(let v of m)if(v.background)b=[...p,...ii(v.background.steps,a,!0,o.invocations,n)];else if(v.scenario){if(o.definitions.has(v.scenario.location.line))continue;d.push(...Ed(v.scenario,a,l,b,ii(v.scenario.steps,a,!1,o.invocations,n),s))}else"rule"in v&&v.rule&&f(v.rule.children,b)};return f(c.feature.children,[]),d}import hi from"node:fs";import ls from"node:path";import di from"node:fs";import pi from"node:path";import{createHash as Fd}from"node:crypto";import Yn from"node:fs";import Mt from"node:path";var Se=new Set(["expectVisible","expectNotVisible","expectText","expectUrl","expectValue","expectDiffers","expectMatches","expectAttribute","expectResponse","expectEach"]);var Kn=1,Ad=new Set(["goto","click","fill","press","selectOption","check","uncheck","hover","waitFor","captureText","handleDialog","uploadFile","dragTo","waitForResponse","switchTab","closeTab","newTab",...Se]),_d=["role","name","nameRegex","label","text","testId","placeholder","frame"],Id=["value","url","saveAs","compareTo","each","pattern","attribute","promptText","urlPattern","method","status","pageFingerprint"],X=class extends Error{constructor(n,r){super(`${n}: ${r}`);this.file=n;this.problem=r}file;problem},Pe=e=>e!==null&&typeof e=="object"&&!Array.isArray(e);function z(e,t,n){if(!e)throw new X(t,n)}function ai(e,t,n){z(Pe(e),t,`${n} must be an object`);for(let r of _d)z(e[r]===void 0||typeof e[r]=="string",t,`${n}.${r} must be a string`);e.fallbackSelectors!==void 0&&z(Array.isArray(e.fallbackSelectors)&&e.fallbackSelectors.every(r=>typeof r=="string"),t,`${n}.fallbackSelectors must be a list of selectors`)}var Pd=e=>["role","label","text","testId","placeholder"].some(t=>typeof e[t]=="string"&&e[t]!=="")||Array.isArray(e.fallbackSelectors)&&e.fallbackSelectors.some(t=>typeof t=="string"&&t.trim().length>0),Cd={goto:["url"],click:["target"],fill:["target","value"],press:["value"],selectOption:["target","value"],check:["target"],uncheck:["target"],hover:["target"],waitFor:["targetOrValue"],captureText:["target","saveAs"],handleDialog:[],uploadFile:["value"],dragTo:["target","toTarget"],waitForResponse:["urlPattern"],switchTab:["value"],closeTab:[],newTab:[],expectVisible:["target"],expectNotVisible:["target"],expectText:["target","value"],expectUrl:["urlOrValue"],expectValue:["target","value"],expectDiffers:["compareTo","targetOrValue"],expectMatches:["target","pattern"],expectAttribute:["target","attribute","pattern"],expectResponse:["urlPattern"],expectEach:["target","each"]};function Od(e,t,n){let r=o=>typeof e[o]=="string",s=o=>Pe(e[o])&&Pd(e[o]);for(let o of Cd[e.action])switch(o){case"target":case"toTarget":z(s(o),t,`${n} (${e.action}) needs a ${o} with a role, name, label, text, test id, placeholder or selector`);break;case"urlOrValue":z(r("url")||r("value"),t,`${n} (${e.action}) needs a url`);break;case"targetOrValue":z(s("target")||r("value"),t,`${n} (${e.action}) needs a target or a value`);break;default:z(r(o),t,`${n} (${e.action}) needs ${o}`)}for(let o of["target","toTarget"])e[o]!==void 0&&z(s(o),t,`${n}.${o} has no usable locator`);if(e.action==="expectUrl"){let o=e.url??e.value;z(typeof o=="string"&&o.length>0,t,`${n} (expectUrl) needs a non-empty url or value`)}for(let o of["url","saveAs","compareTo","each","pattern","attribute","urlPattern"])e[o]!==void 0&&z(e[o].length>0,t,`${n}.${o} must not be empty`);["press","uploadFile","switchTab","expectText"].includes(e.action)&&z(typeof e.value=="string"&&e.value.length>0,t,`${n} (${e.action}) needs a non-empty value`)}function jd(e,t,n){z(Pe(e),t,`${n} must be an object`),z(typeof e.action=="string",t,`${n}.action is missing`),z(Ad.has(e.action),t,`${n}.action "${e.action}" is not an action this runner knows. Recorded by a newer saffron-ai? Update, or re-record with --rerecord`);for(let r of Id)z(e[r]===void 0||typeof e[r]=="string",t,`${n}.${r} must be a string`);z(e.opensTab===void 0||typeof e.opensTab=="boolean",t,`${n}.opensTab must be true or false`),e.target!==void 0&&ai(e.target,t,`${n}.target`),e.toTarget!==void 0&&ai(e.toTarget,t,`${n}.toTarget`),Od(e,t,n)}function Nd(e,t,n){z(Pe(e),t,`${n} must be an object`),z(typeof e.gherkin=="string"&&e.gherkin.length>0,t,`${n}.gherkin (the step text) is missing`),z(typeof e.keyword=="string",t,`${n}.keyword is missing`),z(e.kind==="action"||e.kind==="assertion",t,`${n}.kind must be "action" or "assertion"`),z(Array.isArray(e.actions),t,`${n}.actions must be a list (empty for a recorded no-op)`),e.actions.forEach((r,s)=>jd(r,t,`${n}.actions[${s}]`)),e.table!==void 0&&z(Array.isArray(e.table)&&e.table.every(r=>Array.isArray(r)&&r.every(s=>typeof s=="string")),t,`${n}.table must be rows of strings`),z(e.docString===void 0||typeof e.docString=="string",t,`${n}.docString must be a string`),z(e.fromStepSet===void 0||typeof e.fromStepSet=="string",t,`${n}.fromStepSet must be a string`)}function is(e,t){return z(Pe(e),t,"is not a JSON object"),z(e.version!==void 0,t,"has no version: not a Saffron recording"),z(e.version===Kn,t,typeof e.version=="number"&&e.version>Kn?`is schema version ${e.version}; this saffron-ai reads version ${Kn}. Update saffron-ai to the release that wrote it`:`has schema version ${JSON.stringify(e.version)}; this saffron-ai reads version ${Kn}`),z(typeof e.feature=="string"&&e.feature.length>0,t,"feature (the feature file path) is missing"),z(typeof e.scenario=="string"&&e.scenario.length>0,t,"scenario (the scenario name) is missing"),z(e.recordedAt===void 0||typeof e.recordedAt=="string",t,"recordedAt must be a string"),z(e.recordedBy===void 0||e.recordedBy==="agent"||e.recordedBy==="manual",t,'recordedBy must be "agent" or "manual"'),z(Array.isArray(e.steps),t,"steps must be a list"),e.steps.forEach((n,r)=>Nd(n,t,`steps[${r}]`)),e.data!==void 0&&z(Pe(e.data)&&Object.values(e.data).every(n=>typeof n=="string"),t,"data must map references to strings"),e}function ci(e,t){z(Pe(e),t,"is not a JSON object"),z(Pe(e.meta),t,"meta is missing: not a Saffron proposal");let n=e.meta;z(n.mode==="record"||n.mode==="heal",t,'meta.mode must be "record" or "heal"'),z(typeof n.narrative=="string",t,"meta.narrative must be a string"),z(Array.isArray(n.adaptations)&&n.adaptations.every(r=>typeof r=="string"),t,"meta.adaptations must be a list of strings"),z(n.verified===void 0||typeof n.verified=="boolean",t,"meta.verified must be true or false"),z(Pe(n.usage),t,"meta.usage is missing"),n.featureEdits!==void 0&&z(Array.isArray(n.featureEdits)&&n.featureEdits.every(r=>Pe(r)&&typeof r.index=="number"&&typeof r.text=="string"),t,"meta.featureEdits must be a list of { index, text }"),n.bound!==void 0&&z(Pe(n.bound)&&["scenario","committed","recording"].every(r=>typeof n.bound[r]=="string"),t,"meta.bound must hold the scenario, committed and recording digests"),is(e.cache,`${t} (cache)`)}var as=".saffron";function Zn(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"unnamed"}function cs(e){let t=e.split(/[\\/]/).filter(Boolean),n=t.pop()??"unnamed",r=t.slice(1),s=Zn([...r,Mt.basename(n,".feature")].join("-"));if(r.length===0)return s;let o=Fd("sha1").update(e.replace(/\\/g,"/")).digest("hex").slice(0,6);return`${s}-${o}`}function Y(e,t,n){return Mt.join(e,as,"cache",cs(t),`${Zn(n)}.json`)}function li(e,t){let n=new Map;for(let r of t){let s=Y(e,r.featurePath,r.name),o=n.get(s)??new Set;o.add(`${r.featurePath} \u203A ${r.name}`),n.set(s,o)}return[...n.entries()].filter(([,r])=>r.size>1).map(([r,s])=>`${[...s].join(" and ")} both map to ${Mt.relative(e,r)}. Rename one scenario or one file.`)}var Dt="owner.json";function Le(e,t,n){return Mt.join(e,as,"artifacts",cs(t),Zn(n))}function xe(e,t,n){return Mt.join(e,as,"proposals",cs(t),`${Zn(n)}.json`)}function Ue(e,t){try{return me(e)}catch(n){if(!(n instanceof X))throw n;t?.(e,n.problem);return}}function me(e){if(!Yn.existsSync(e))return;let t;try{t=JSON.parse(Yn.readFileSync(e,"utf8"))}catch(n){throw new X(e,`is not valid JSON: ${n instanceof Error?n.message:String(n)}`)}return is(t,e)}function Lt(e,t){Yn.mkdirSync(Mt.dirname(e),{recursive:!0}),Yn.writeFileSync(e,`${JSON.stringify(t,null,2)}
11
- `)}function un(e,t){let n=r=>typeof r=="string"?r.includes(t):Array.isArray(r)?r.some(n):r&&typeof r=="object"?Object.entries(r).some(([s,o])=>s!=="pageFingerprint"&&n(o)):!1;return e.some(n)}function Ut(e,t){if(t.trim()==="")return!1;let n=s=>(s??"").replace(/<[^<>]+>/g,""),r=new Set([t,...Object.values(oe).map(s=>s(t))]);return e.some(s=>[s.value,s.url,s.urlPattern,s.pattern,s.promptText,s.compareTo,...[s.target,s.toTarget].flatMap(o=>[o?.name,o?.nameRegex,o?.label,o?.text,o?.placeholder,o?.testId,o?.frame,...o?.fallbackSelectors??[]])].some(o=>[...r].some(i=>n(o).includes(i))))}function Qn(e){let t=[];return{pattern:e.replace(/"((?:[^"\\]|\\.)*)"/g,(r,s)=>(t.push(s),`"\xAB${t.length-1}\xBB"`)),args:t}}function Md(e,t){let n=r=>JSON.stringify(r.map(({pageFingerprint:s,...o})=>o));return n(e)===n(t)}function Dd(e){let t=new Set,n=r=>{if(r)for(let s of r.matchAll(/<([^<>]+)>/g))t.add(s[1])};for(let r of e.actions)n(r.value),n(r.url),n(r.pattern),n(r.target?.name),n(r.target?.nameRegex),n(r.target?.label),n(r.target?.text),n(r.target?.placeholder);return n(e.gherkin),t}function Be(e){let t=pi.join(e,".saffron","cache"),n=new Map,r=new Map,s=0;if(di.existsSync(t))for(let a of di.readdirSync(t,{recursive:!0})){let c=String(a);if(!c.endsWith(".json"))continue;let l=Ue(pi.join(t,c));if(l){s++;for(let d of l.steps){let f=d.gherkin,m=JSON.stringify(d.actions.map(({pageFingerprint:v,...k})=>k)),p=r.get(f)??[];p.includes(m)||(p.push(m),r.set(f,p));let b=n.get(f);!b||l.recordedAt>b.recordedAt?n.set(f,{gherkin:d.gherkin,table:d.table,docString:d.docString,actions:d.actions,sourceParams:it(l.steps),sourceData:l.data,feature:l.feature,scenario:l.scenario,recordedAt:l.recordedAt,variants:p.length}):b.variants=p.length}}}let o=[...r.entries()].filter(([,a])=>a.length>1).map(([a])=>a),i=new Map;for(let a of n.values()){let{pattern:c,args:l}=Qn(a.gherkin);if(l.length===0)continue;let d=i.get(c);(!d||a.recordedAt>d.recordedAt)&&i.set(c,{...a,args:l})}return{steps:n,patterns:i,divergent:o,scannedCaches:s}}function fi(e,t,n){if(!!e.table!=!!t.table)return!1;if(e.table&&t.table){let o=Object.keys(fe(e.table)),i=Object.keys(fe(t.table));if(o.length>0&&i.length>0){if(JSON.stringify(o)!==JSON.stringify(i))return!1}else if(JSON.stringify(e.table)!==JSON.stringify(t.table))return!1}if(e.docString!==void 0!=(t.docString!==void 0)||e.docString!==void 0&&t.docString!==void 0&&e.docString!==t.docString&&!e.actions.some(o=>[o.value,o.url,o.pattern].some(i=>i?.includes("<docstring>"))))return!1;if(e.table&&t.table){let o=fe(e.table),i=fe(t.table);for(let a of Object.keys(o))if(o[a]!==i[a]&&(!un(e.actions,`<${a}>`)||Ut(e.actions,o[a])))return!1}for(let[o,i]of Object.entries(e.sourceParams??{}))if(o in n&&n[o]!==i&&Ut(e.actions,i))return!1;for(let[o,i]of Object.entries(e.sourceData??{})){let a;try{a=Nt(o)}catch{continue}if(a!==i&&(Ut(e.actions,i)||e.gherkin.includes(`{data:${o}}`)&&!un(e.actions,`{data:${o}}`)))return!1}let r={...n,...fe(t.resolvedTable??t.table),...bt(t.resolvedDocString??t.docString)},s=Dd({gherkin:e.gherkin,keyword:"",kind:"action",actions:e.actions});for(let o of s)if(!(o in r))return!1;return!0}function ui(e,t,n){let r=e.steps.get(t.text);if(r)return fi(r,t,n)?r:void 0;let{pattern:s,args:o}=Qn(t.text);if(o.length===0)return;let i=e.patterns.get(s);if(!i)return;let a=new Map;for(let f=0;f<o.length;f++){let m=i.args[f],p=o[f];if(m===p)continue;let b=a.get(m);if(b!==void 0&&b!==p)return;a.set(m,p)}let c=f=>[f?.name,f?.nameRegex,f?.label,f?.text,f?.placeholder];for(let f of a.keys())if(!i.actions.some(p=>[p.value,p.url,p.pattern,...c(p.target)].includes(f)))return;let l=f=>f!==void 0&&a.has(f)?a.get(f):f,d={...i,gherkin:t.text,actions:i.actions.map(({pageFingerprint:f,...m})=>({...m,value:l(m.value),url:l(m.url),pattern:l(m.pattern),target:m.target?{...m.target,name:l(m.target.name),nameRegex:l(m.target.nameRegex),label:l(m.target.label),text:l(m.target.text),placeholder:l(m.target.placeholder)}:void 0}))};return fi(d,t,n)?d:void 0}function gi(e,t){let n=[];return e.forEach((r,s)=>{if(r.actions.length!==0&&!t.steps.has(r.gherkin)){for(let o of t.steps.values())if(!(o.variants>1)&&o.actions.length!==0&&Md(r.actions,o.actions)){n.push({index:s,from:r.gherkin,to:o.gherkin,feature:o.feature,scenario:o.scenario});break}}}),n}function ds(e){if(!hi.existsSync(e))return[];let t=[];for(let n of hi.readdirSync(e,{recursive:!0})){let r=String(n);Xe(r)&&t.push(ls.join(e,r))}return t.sort()}function ze(e,t,n,r){let s=ls.isAbsolute(t)?t:ls.join(e,t),o=ds(s),i=n??Qe(o.filter(p=>p.endsWith(".saffron")),e),a=new Map,c=new Map;for(let p of o)for(let b of r?.get(p)??he(p,e,i)){let v=`${b.featurePath}::${b.name}`;for(let k of b.steps){let w=a.get(k.text);if(w||(w={text:k.text,keywords:new Map,kind:k.kind,scenarios:new Set,files:new Set,fromStepSet:k.source?.fromStepSet},a.set(k.text,w)),w.keywords.set(k.keyword,(w.keywords.get(k.keyword)??0)+1),w.scenarios.add(v),k.source&&w.files.add(k.source.file),k.source?.fromStepSet){let x=c.get(k.source.fromStepSet);x||c.set(k.source.fromStepSet,x=new Set),x.add(v)}}}let l=Be(e),d=new Set(l.divergent),f=[...a.values()].map(p=>({text:p.text,keyword:[...p.keywords.entries()].sort((b,v)=>v[1]-b[1])[0][0],kind:p.kind,status:d.has(p.text)?"divergent":l.steps.has(p.text)?"recorded":"unrecorded",usage:p.scenarios.size,files:[...p.files].sort(),fromStepSet:p.fromStepSet})).sort((p,b)=>b.usage-p.usage||p.text.localeCompare(b.text)),m=[...i.values()].map(p=>({name:p.name,file:p.file,line:p.line,steps:p.steps.map(b=>b.text),usage:c.get(p.name)?.size??0})).sort((p,b)=>b.usage-p.usage||p.name.localeCompare(b.name));return{steps:f,stepSets:m,scannedFiles:o.length,scannedCaches:l.scannedCaches}}function Xn(e,t){let n=t.toLowerCase();return{...e,steps:e.steps.filter(r=>r.text.toLowerCase().includes(n)),stepSets:e.stepSets.filter(r=>r.name.toLowerCase().includes(n)||r.steps.some(s=>s.toLowerCase().includes(n)))}}function mi(e){let t={},n="saffron,feature,gherkin,cucumber";for(let r of e.steps)t[`step: ${r.text}`]={scope:n,prefix:r.text,body:[r.text.replace(/\$/g,"\\$")],description:`${r.status} \xB7 used in ${r.usage} scenario(s)`};for(let r of e.stepSets)t[`StepSet: ${r.name}`]={scope:n,prefix:`StepSet ${r.name}`,body:[`StepSet ${r.name}`],description:`step set (${r.steps.length} steps) \u2014 ${r.file}`};return JSON.stringify(t,null,2)+`
12
- `}import{Server as Zf}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as Qf}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as Xf,ListToolsRequestSchema as eu}from"@modelcontextprotocol/sdk/types.js";import _e from"node:fs";import Ge from"node:path";import yi from"node:fs";import Ld from"node:path";function ne(e){ri(e);let t=Ld.join(e,"saffron.config.json"),n=yi.existsSync(t)?JSON.parse(yi.readFileSync(t,"utf8")):{};return Me(e,n.dataDir,process.env.SAFFRON_ENV??n.env),n}var ps={provider:"claude",features:"features",dataDir:"data",actionTimeoutMs:5e3,pollIntervalMs:100,retries:1,screenshot:"viewport",maxTurns:100,strict:!1,verifyProposals:!0,snapshotMode:"none",reuseSteps:!0,browser:"chromium",workers:1,assertionPolicy:"strict",hookTimeoutMs:3e5,trace:"off"};function bi(e){return{...ps,...Object.fromEntries(Object.entries(e).filter(([,t])=>t!==void 0))}}import fs from"node:fs";import er from"node:path";import{fileURLToPath as Ud}from"node:url";function tr(){let e=er.dirname(Ud(import.meta.url));for(let t=0;t<6;t++){let n=er.join(e,"package.json");if(fs.existsSync(n))try{if(JSON.parse(fs.readFileSync(n,"utf8")).name==="saffron-ai")return e}catch{}e=er.dirname(e)}}function Ce(){let e=tr();if(!e)return"unknown";try{return JSON.parse(fs.readFileSync(er.join(e,"package.json"),"utf8")).version??"unknown"}catch{return"unknown"}}import ue from"node:fs";import re from"node:path";import de from"node:fs";import{createHash as qd}from"node:crypto";import wi from"node:os";import nr from"node:path";import et from"node:fs";import Bt from"node:path";import{createHash as Bd,randomBytes as zd}from"node:crypto";function at(e,t){try{if(!et.statSync(t).isFile())return!1;let n=Bt.relative(et.realpathSync(e),et.realpathSync(t));return n!==""&&n!==".."&&!n.startsWith(".."+Bt.sep)&&!Bt.isAbsolute(n)}catch{return!1}}function Oe(e){return Bd("sha256").update(e).digest("hex")}function Ee(e,t,n){et.mkdirSync(Bt.dirname(e),{recursive:!0});let r=`${e}.${process.pid}-${zd(4).toString("hex")}.partial`;try{et.writeFileSync(r,t),n!==void 0&&et.chmodSync(r,n),et.renameSync(r,e)}catch(s){throw et.rmSync(r,{force:!0}),s}}function zt(e,t){let n=Bt.join(e,t.file);if(at(Bt.join(e,".saffron","artifacts"),n))try{let r=et.readFileSync(n);return t.sha256&&Oe(r)!==t.sha256?void 0:r}catch{return}}function Si(e,t){de.mkdirSync(nr.dirname(e),{recursive:!0});let n=Buffer.from(`${JSON.stringify(t,null,2)}
13
- `);return qt(e,()=>Ee(e,n)),hs(n)}var Hd=6e4,Jd=5e3,Gd=60*6e4,Wd=new Int32Array(new SharedArrayBuffer(4)),St=class extends Error{};function qt(e,t,n=1e4){let r=`${e}.lock`,s=Date.now()+n,o;for(;;){o=JSON.stringify({pid:process.pid,host:wi.hostname(),at:new Date().toISOString()});try{de.writeFileSync(r,o,{flag:"wx"});break}catch(i){let a=i.code,c=a==="EPERM"&&process.platform==="win32";if(a!=="EEXIST"&&!c)throw i;if(Date.now()>=s)throw c?i:new St(`${e} is being decided by another process. Try again.`);Vd(r)||Atomics.wait(Wd,0,0,25)}}try{return t()}finally{us(r)===o&&de.rmSync(r,{force:!0})}}function us(e){try{return de.readFileSync(e,"utf8")}catch{return}}function Vd(e){let t=us(e);if(t===void 0)return!1;let n={};try{n=JSON.parse(t)||{}}catch{}let r=Date.parse(n.at??""),s=Hd;if(Number.isNaN(r)){try{r=de.statSync(e).mtimeMs}catch{return!1}s=Jd}let o=n.host===wi.hostname()&&typeof n.pid=="number";return o&&Kd(n.pid)&&Date.now()-r<Gd||!o&&Date.now()-r<s||us(e)!==t?!1:(de.rmSync(e,{force:!0}),!0)}function Kd(e){try{return process.kill(e,0),!0}catch(t){return t.code!=="ESRCH"}}function rr(e,t,n){return de.existsSync(e)?qt(e,()=>de.existsSync(e)?t():n()):n()}function gs(e){let t=/^(.+)#([0-9a-f]{64})$/i.exec(e);return t?{file:t[1],revision:t[2].toLowerCase()}:{file:e}}function qe(e){return hs(de.readFileSync(e))}var hs=e=>qd("sha256").update(e).digest("hex");function Ht(e){return sr(e).proposal}function sr(e){return ms(de.readFileSync(e),e)}function ms(e,t){let n;try{n=JSON.parse(e.toString("utf8"))}catch(r){throw new X(t,`is not valid JSON: ${r instanceof Error?r.message:String(r)}`)}return ci(n,t),{proposal:n,revision:hs(e),bytes:e}}function He(e,t){let n=nr.join(e,".saffron","proposals");if(!de.existsSync(n))return[];let r=[];for(let s of de.readdirSync(n)){let o=nr.join(n,s);if(de.statSync(o).isDirectory())for(let i of de.readdirSync(o)){if(!i.endsWith(".json"))continue;let a=nr.join(o,i);try{r.push({file:a,...sr(a)})}catch(c){if(!t||!(c instanceof X))throw c;t(a,c.problem)}}}return r}function or(e,t){let n=Ht(t),r=Y(e,n.cache.feature,n.cache.scenario);return Lt(r,n.cache),de.rmSync(t),r}function ir(e){de.rmSync(e)}var ys=(e,t)=>re.relative(e,t).split(re.sep).join("/");function xi(e){return ue.existsSync(e)?[...ue.readdirSync(e,{recursive:!0})].map(String).filter(t=>t.endsWith(".json")).map(t=>re.join(e,t)).sort():[]}function vi(e){try{let t=JSON.parse(ue.readFileSync(e,"utf8")),n="cache"in t&&t.cache?t.cache:t;return{feature:n.feature,scenario:n.scenario}}catch{return{}}}function $i(e){try{let t=JSON.parse(ue.readFileSync(re.join(e,Dt),"utf8"));return{feature:typeof t.feature=="string"?t.feature:void 0,scenario:typeof t.scenario=="string"?t.scenario:void 0}}catch{return{}}}function cr(e,t,n){let r=re.isAbsolute(t)?t:re.join(e,t),s=new Set(ue.existsSync(r)?[...ue.readdirSync(r,{recursive:!0})].map(String).filter(Xe).map(l=>re.join(r,l)):[]);for(let l of["cache","proposals"])for(let d of xi(re.join(e,".saffron",l))){let{feature:f}=vi(d);if(typeof f=="string"){let m=re.resolve(e,f);ue.existsSync(m)&&s.add(m)}}for(let l of ar(re.join(e,".saffron","artifacts")))for(let d of ar(l)){let f=$i(d).feature;if(f){let m=re.resolve(e,f);ue.existsSync(m)&&s.add(m)}}let o=[...n?.unreadable??[]],i=[...s].filter(l=>!n?.parsedFiles.has(l)),a=new Map;if(i.length>0)try{a=Qe([...new Set([...wt(r),...s].filter(l=>l.endsWith(".saffron")))],e)}catch(l){o.push(l instanceof Error?l.message:String(l))}let c=new Set(n?.owned??[]);for(let l of i){let d;try{d=he(l,e,a)}catch(f){o.push(`${ys(e,l)}: ${f instanceof Error?f.message:String(f)}`);continue}for(let f of d)c.add(Y(e,f.featurePath,f.name)),c.add(xe(e,f.featurePath,f.name)),c.add(Le(e,f.featurePath,f.displayName))}return{...Yd(e,c),unreadable:o}}function Yd(e,t){let n=[],r=0;for(let o of["cache","proposal"]){let i=re.join(e,".saffron",o==="cache"?"cache":"proposals");for(let a of xi(i)){if(t.has(a)){r++;continue}let{feature:c,scenario:l}=vi(a),d=c?re.join(e,c):void 0;n.push({file:ys(e,a),kind:o,feature:c,scenario:l,reason:c?ue.existsSync(d)?"scenario-gone":"feature-gone":"unknown"})}}let s=re.join(e,".saffron","artifacts");for(let o of ar(s))for(let i of ar(o)){if(t.has(i)){r++;continue}let{feature:a,scenario:c}=$i(i);n.push({file:ys(e,i),kind:"artifact",feature:a,scenario:c,reason:a?ue.existsSync(re.join(e,a))?"scenario-gone":"feature-gone":"unknown"})}return{orphans:n,kept:r}}function ar(e){try{return ue.readdirSync(e,{withFileTypes:!0}).filter(t=>t.isDirectory()).map(t=>re.join(e,t.name)).sort()}catch{return[]}}function ki(e,t){let n=[],r=new Set;for(let s of t){let o=re.join(e,s.file);try{s.kind==="proposal"?qt(o,()=>ue.rmSync(o)):ue.rmSync(o,{recursive:s.kind==="artifact"}),n.push(s.file),r.add(re.dirname(o))}catch{}}for(let s of r)try{ue.readdirSync(s).length===0&&ue.rmdirSync(s)}catch{}return n}function Ti(e){if(e.kind==="artifact")return`${e.file}/
10
+ `)||(m instanceof Error?m.message:String(m))}`)}if(!c.feature)return[];let l=c.feature.name,d=[],f=(m,p)=>{let b=p;for(let $ of m)if($.background)b=[...p,...bi($.background.steps,a,!0,o.invocations,n)];else if($.scenario){if(o.definitions.has($.scenario.location.line))continue;d.push(...qd($.scenario,a,l,b,bi($.scenario.steps,a,!1,o.invocations,n),s))}else"rule"in $&&$.rule&&f($.rule.children,b)};return f(c.feature.children,[]),d}import Ei from"node:fs";import ys from"node:path";import xi from"node:fs";import $i from"node:path";import{createHash as Xd}from"node:crypto";import sr from"node:fs";import Ut from"node:path";var Se=new Set(["expectVisible","expectNotVisible","expectText","expectUrl","expectValue","expectDiffers","expectMatches","expectAttribute","expectResponse","expectEach"]);var rr=1,Jd=new Set(["goto","click","fill","press","selectOption","check","uncheck","hover","waitFor","captureText","handleDialog","uploadFile","dragTo","waitForResponse","switchTab","closeTab","newTab",...Se]),Wd=["role","name","nameRegex","label","text","testId","placeholder","frame"],Gd=["value","url","saveAs","compareTo","each","pattern","attribute","promptText","urlPattern","method","status","pageFingerprint"],X=class extends Error{constructor(n,r){super(`${n}: ${r}`);this.file=n;this.problem=r}file;problem},Ce=e=>e!==null&&typeof e=="object"&&!Array.isArray(e);function B(e,t,n){if(!e)throw new X(t,n)}function wi(e,t,n){B(Ce(e),t,`${n} must be an object`);for(let r of Wd)B(e[r]===void 0||typeof e[r]=="string",t,`${n}.${r} must be a string`);e.fallbackSelectors!==void 0&&B(Array.isArray(e.fallbackSelectors)&&e.fallbackSelectors.every(r=>typeof r=="string"),t,`${n}.fallbackSelectors must be a list of selectors`)}var Vd=e=>["role","label","text","testId","placeholder"].some(t=>typeof e[t]=="string"&&e[t]!=="")||Array.isArray(e.fallbackSelectors)&&e.fallbackSelectors.some(t=>typeof t=="string"&&t.trim().length>0),Kd={goto:["url"],click:["target"],fill:["target","value"],press:["value"],selectOption:["target","value"],check:["target"],uncheck:["target"],hover:["target"],waitFor:["targetOrValue"],captureText:["target","saveAs"],handleDialog:[],uploadFile:["value"],dragTo:["target","toTarget"],waitForResponse:["urlPattern"],switchTab:["value"],closeTab:[],newTab:[],expectVisible:["target"],expectNotVisible:["target"],expectText:["target","value"],expectUrl:["urlOrValue"],expectValue:["target","value"],expectDiffers:["compareTo","targetOrValue"],expectMatches:["target","pattern"],expectAttribute:["target","attribute","pattern"],expectResponse:["urlPattern"],expectEach:["target","each"]};function Yd(e,t,n){let r=o=>typeof e[o]=="string",s=o=>Ce(e[o])&&Vd(e[o]);for(let o of Kd[e.action])switch(o){case"target":case"toTarget":B(s(o),t,`${n} (${e.action}) needs a ${o} with a role, name, label, text, test id, placeholder or selector`);break;case"urlOrValue":B(r("url")||r("value"),t,`${n} (${e.action}) needs a url`);break;case"targetOrValue":B(s("target")||r("value"),t,`${n} (${e.action}) needs a target or a value`);break;default:B(r(o),t,`${n} (${e.action}) needs ${o}`)}for(let o of["target","toTarget"])e[o]!==void 0&&B(s(o),t,`${n}.${o} has no usable locator`);if(e.action==="expectUrl"){let o=e.url??e.value;B(typeof o=="string"&&o.length>0,t,`${n} (expectUrl) needs a non-empty url or value`)}for(let o of["url","saveAs","compareTo","each","pattern","attribute","urlPattern"])e[o]!==void 0&&B(e[o].length>0,t,`${n}.${o} must not be empty`);["press","uploadFile","switchTab","expectText"].includes(e.action)&&B(typeof e.value=="string"&&e.value.length>0,t,`${n} (${e.action}) needs a non-empty value`)}function Qd(e,t,n){B(Ce(e),t,`${n} must be an object`),B(typeof e.action=="string",t,`${n}.action is missing`),B(Jd.has(e.action),t,`${n}.action "${e.action}" is not an action this runner knows. Recorded by a newer saffron-ai? Update, or re-record with --rerecord`);for(let r of Gd)B(e[r]===void 0||typeof e[r]=="string",t,`${n}.${r} must be a string`);B(e.opensTab===void 0||typeof e.opensTab=="boolean",t,`${n}.opensTab must be true or false`),e.target!==void 0&&wi(e.target,t,`${n}.target`),e.toTarget!==void 0&&wi(e.toTarget,t,`${n}.toTarget`),Yd(e,t,n)}function Zd(e,t,n){B(Ce(e),t,`${n} must be an object`),B(typeof e.gherkin=="string"&&e.gherkin.length>0,t,`${n}.gherkin (the step text) is missing`),B(typeof e.keyword=="string",t,`${n}.keyword is missing`),B(e.kind==="action"||e.kind==="assertion",t,`${n}.kind must be "action" or "assertion"`),B(Array.isArray(e.actions),t,`${n}.actions must be a list (empty for a recorded no-op)`),e.actions.forEach((r,s)=>Qd(r,t,`${n}.actions[${s}]`)),e.table!==void 0&&B(Array.isArray(e.table)&&e.table.every(r=>Array.isArray(r)&&r.every(s=>typeof s=="string")),t,`${n}.table must be rows of strings`),B(e.docString===void 0||typeof e.docString=="string",t,`${n}.docString must be a string`),B(e.fromStepSet===void 0||typeof e.fromStepSet=="string",t,`${n}.fromStepSet must be a string`)}function gs(e,t){return B(Ce(e),t,"is not a JSON object"),B(e.version!==void 0,t,"has no version: not a Saffron recording"),B(e.version===rr,t,typeof e.version=="number"&&e.version>rr?`is schema version ${e.version}; this saffron-ai reads version ${rr}. Update saffron-ai to the release that wrote it`:`has schema version ${JSON.stringify(e.version)}; this saffron-ai reads version ${rr}`),B(typeof e.feature=="string"&&e.feature.length>0,t,"feature (the feature file path) is missing"),B(typeof e.scenario=="string"&&e.scenario.length>0,t,"scenario (the scenario name) is missing"),B(e.recordedAt===void 0||typeof e.recordedAt=="string",t,"recordedAt must be a string"),B(e.recordedBy===void 0||e.recordedBy==="agent"||e.recordedBy==="manual",t,'recordedBy must be "agent" or "manual"'),B(Array.isArray(e.steps),t,"steps must be a list"),e.steps.forEach((n,r)=>Zd(n,t,`steps[${r}]`)),e.data!==void 0&&B(Ce(e.data)&&Object.values(e.data).every(n=>typeof n=="string"),t,"data must map references to strings"),e}function vi(e,t){B(Ce(e),t,"is not a JSON object"),B(Ce(e.meta),t,"meta is missing: not a Saffron proposal");let n=e.meta;B(n.mode==="record"||n.mode==="heal",t,'meta.mode must be "record" or "heal"'),B(typeof n.narrative=="string",t,"meta.narrative must be a string"),B(Array.isArray(n.adaptations)&&n.adaptations.every(r=>typeof r=="string"),t,"meta.adaptations must be a list of strings"),B(n.verified===void 0||typeof n.verified=="boolean",t,"meta.verified must be true or false"),B(Ce(n.usage),t,"meta.usage is missing"),n.featureEdits!==void 0&&B(Array.isArray(n.featureEdits)&&n.featureEdits.every(r=>Ce(r)&&typeof r.index=="number"&&typeof r.text=="string"),t,"meta.featureEdits must be a list of { index, text }"),n.bound!==void 0&&B(Ce(n.bound)&&["scenario","committed","recording"].every(r=>typeof n.bound[r]=="string"),t,"meta.bound must hold the scenario, committed and recording digests"),gs(e.cache,`${t} (cache)`)}var hs=".saffron";function or(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"unnamed"}function ms(e){let t=e.split(/[\\/]/).filter(Boolean),n=t.pop()??"unnamed",r=t.slice(1),s=or([...r,Ut.basename(n,".feature")].join("-"));if(r.length===0)return s;let o=Xd("sha1").update(e.replace(/\\/g,"/")).digest("hex").slice(0,6);return`${s}-${o}`}function Y(e,t,n){return Ut.join(e,hs,"cache",ms(t),`${or(n)}.json`)}function Si(e,t){let n=new Map;for(let r of t){let s=Y(e,r.featurePath,r.name),o=n.get(s)??new Set;o.add(`${r.featurePath} \u203A ${r.name}`),n.set(s,o)}return[...n.entries()].filter(([,r])=>r.size>1).map(([r,s])=>`${[...s].join(" and ")} both map to ${Ut.relative(e,r)}. Rename one scenario or one file.`)}var Bt="owner.json";function Ue(e,t,n){return Ut.join(e,hs,"artifacts",ms(t),or(n))}function xe(e,t,n){return Ut.join(e,hs,"proposals",ms(t),`${or(n)}.json`)}function Be(e,t){try{return ye(e)}catch(n){if(!(n instanceof X))throw n;t?.(e,n.problem);return}}function ye(e){if(!sr.existsSync(e))return;let t;try{t=JSON.parse(sr.readFileSync(e,"utf8"))}catch(n){throw new X(e,`is not valid JSON: ${n instanceof Error?n.message:String(n)}`)}return gs(t,e)}function zt(e,t){sr.mkdirSync(Ut.dirname(e),{recursive:!0}),sr.writeFileSync(e,`${JSON.stringify(t,null,2)}
11
+ `)}function yn(e,t){let n=r=>typeof r=="string"?r.includes(t):Array.isArray(r)?r.some(n):r&&typeof r=="object"?Object.entries(r).some(([s,o])=>s!=="pageFingerprint"&&n(o)):!1;return e.some(n)}function Ht(e,t){if(t.trim()==="")return!1;let n=s=>(s??"").replace(/<[^<>]+>/g,""),r=new Set([t,...Object.values(ae).map(s=>s(t))]);return e.some(s=>[s.value,s.url,s.urlPattern,s.pattern,s.promptText,s.compareTo,...[s.target,s.toTarget].flatMap(o=>[o?.name,o?.nameRegex,o?.label,o?.text,o?.placeholder,o?.testId,o?.frame,...o?.fallbackSelectors??[]])].some(o=>[...r].some(i=>n(o).includes(i))))}function ir(e){let t=[];return{pattern:e.replace(/"((?:[^"\\]|\\.)*)"/g,(r,s)=>(t.push(s),`"\xAB${t.length-1}\xBB"`)),args:t}}function ep(e,t){let n=r=>JSON.stringify(r.map(({pageFingerprint:s,...o})=>o));return n(e)===n(t)}function tp(e){let t=new Set,n=r=>{if(r)for(let s of r.matchAll(/<([^<>]+)>/g))t.add(s[1])};for(let r of e.actions)n(r.value),n(r.url),n(r.pattern),n(r.target?.name),n(r.target?.nameRegex),n(r.target?.label),n(r.target?.text),n(r.target?.placeholder);return n(e.gherkin),t}function ze(e){let t=$i.join(e,".saffron","cache"),n=new Map,r=new Map,s=0;if(xi.existsSync(t))for(let a of xi.readdirSync(t,{recursive:!0})){let c=String(a);if(!c.endsWith(".json"))continue;let l=Be($i.join(t,c));if(l){s++;for(let d of l.steps){let f=d.gherkin,m=JSON.stringify(d.actions.map(({pageFingerprint:$,...k})=>k)),p=r.get(f)??[];p.includes(m)||(p.push(m),r.set(f,p));let b=n.get(f);!b||l.recordedAt>b.recordedAt?n.set(f,{gherkin:d.gherkin,table:d.table,docString:d.docString,actions:d.actions,sourceParams:ct(l.steps),sourceData:l.data,feature:l.feature,scenario:l.scenario,recordedAt:l.recordedAt,variants:p.length}):b.variants=p.length}}}let o=[...r.entries()].filter(([,a])=>a.length>1).map(([a])=>a),i=new Map;for(let a of n.values()){let{pattern:c,args:l}=ir(a.gherkin);if(l.length===0)continue;let d=i.get(c);(!d||a.recordedAt>d.recordedAt)&&i.set(c,{...a,args:l})}return{steps:n,patterns:i,divergent:o,scannedCaches:s}}function ki(e,t,n){if(!!e.table!=!!t.table)return!1;if(e.table&&t.table){let o=Object.keys(ue(e.table)),i=Object.keys(ue(t.table));if(o.length>0&&i.length>0){if(JSON.stringify(o)!==JSON.stringify(i))return!1}else if(JSON.stringify(e.table)!==JSON.stringify(t.table))return!1}if(e.docString!==void 0!=(t.docString!==void 0)||e.docString!==void 0&&t.docString!==void 0&&e.docString!==t.docString&&!e.actions.some(o=>[o.value,o.url,o.pattern].some(i=>i?.includes("<docstring>"))))return!1;if(e.table&&t.table){let o=ue(e.table),i=ue(t.table);for(let a of Object.keys(o))if(o[a]!==i[a]&&(!yn(e.actions,`<${a}>`)||Ht(e.actions,o[a])))return!1}for(let[o,i]of Object.entries(e.sourceParams??{}))if(o in n&&n[o]!==i&&Ht(e.actions,i))return!1;for(let[o,i]of Object.entries(e.sourceData??{})){let a;try{a=Dt(o)}catch{continue}if(a!==i&&(Ht(e.actions,i)||e.gherkin.includes(`{data:${o}}`)&&!yn(e.actions,`{data:${o}}`)))return!1}let r={...n,...ue(t.resolvedTable??t.table),...vt(t.resolvedDocString??t.docString)},s=tp({gherkin:e.gherkin,keyword:"",kind:"action",actions:e.actions});for(let o of s)if(!(o in r))return!1;return!0}function Ti(e,t,n){let r=e.steps.get(t.text);if(r)return ki(r,t,n)?r:void 0;let{pattern:s,args:o}=ir(t.text);if(o.length===0)return;let i=e.patterns.get(s);if(!i)return;let a=new Map;for(let f=0;f<o.length;f++){let m=i.args[f],p=o[f];if(m===p)continue;let b=a.get(m);if(b!==void 0&&b!==p)return;a.set(m,p)}let c=f=>[f?.name,f?.nameRegex,f?.label,f?.text,f?.placeholder];for(let f of a.keys())if(!i.actions.some(p=>[p.value,p.url,p.pattern,...c(p.target)].includes(f)))return;let l=f=>f!==void 0&&a.has(f)?a.get(f):f,d={...i,gherkin:t.text,actions:i.actions.map(({pageFingerprint:f,...m})=>({...m,value:l(m.value),url:l(m.url),pattern:l(m.pattern),target:m.target?{...m.target,name:l(m.target.name),nameRegex:l(m.target.nameRegex),label:l(m.target.label),text:l(m.target.text),placeholder:l(m.target.placeholder)}:void 0}))};return ki(d,t,n)?d:void 0}function Ri(e,t){let n=[];return e.forEach((r,s)=>{if(r.actions.length!==0&&!t.steps.has(r.gherkin)){for(let o of t.steps.values())if(!(o.variants>1)&&o.actions.length!==0&&ep(r.actions,o.actions)){n.push({index:s,from:r.gherkin,to:o.gherkin,feature:o.feature,scenario:o.scenario});break}}}),n}function bs(e){if(!Ei.existsSync(e))return[];let t=[];for(let n of Ei.readdirSync(e,{recursive:!0})){let r=String(n);tt(r)&&t.push(ys.join(e,r))}return t.sort()}function He(e,t,n,r){let s=ys.isAbsolute(t)?t:ys.join(e,t),o=bs(s),i=n??et(o.filter(p=>p.endsWith(".saffron")),e),a=new Map,c=new Map;for(let p of o)for(let b of r?.get(p)??me(p,e,i)){let $=`${b.featurePath}::${b.name}`;for(let k of b.steps){let w=a.get(k.text);if(w||(w={text:k.text,keywords:new Map,kind:k.kind,scenarios:new Set,files:new Set,fromStepSet:k.source?.fromStepSet},a.set(k.text,w)),w.keywords.set(k.keyword,(w.keywords.get(k.keyword)??0)+1),w.scenarios.add($),k.source&&w.files.add(k.source.file),k.source?.fromStepSet){let x=c.get(k.source.fromStepSet);x||c.set(k.source.fromStepSet,x=new Set),x.add($)}}}let l=ze(e),d=new Set(l.divergent),f=[...a.values()].map(p=>({text:p.text,keyword:[...p.keywords.entries()].sort((b,$)=>$[1]-b[1])[0][0],kind:p.kind,status:d.has(p.text)?"divergent":l.steps.has(p.text)?"recorded":"unrecorded",usage:p.scenarios.size,files:[...p.files].sort(),fromStepSet:p.fromStepSet})).sort((p,b)=>b.usage-p.usage||p.text.localeCompare(b.text)),m=[...i.values()].map(p=>({name:p.name,file:p.file,line:p.line,steps:p.steps.map(b=>b.text),usage:c.get(p.name)?.size??0})).sort((p,b)=>b.usage-p.usage||p.name.localeCompare(b.name));return{steps:f,stepSets:m,scannedFiles:o.length,scannedCaches:l.scannedCaches}}function ar(e,t){let n=t.toLowerCase();return{...e,steps:e.steps.filter(r=>r.text.toLowerCase().includes(n)),stepSets:e.stepSets.filter(r=>r.name.toLowerCase().includes(n)||r.steps.some(s=>s.toLowerCase().includes(n)))}}function Ai(e){let t={},n="saffron,feature,gherkin,cucumber";for(let r of e.steps)t[`step: ${r.text}`]={scope:n,prefix:r.text,body:[r.text.replace(/\$/g,"\\$")],description:`${r.status} \xB7 used in ${r.usage} scenario(s)`};for(let r of e.stepSets)t[`StepSet: ${r.name}`]={scope:n,prefix:`StepSet ${r.name}`,body:[`StepSet ${r.name}`],description:`step set (${r.steps.length} steps) \u2014 ${r.file}`};return JSON.stringify(t,null,2)+`
12
+ `}import{Server as uu}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as gu}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as hu,ListToolsRequestSchema as mu}from"@modelcontextprotocol/sdk/types.js";import Ie from"node:fs";import Ve from"node:path";import _i from"node:fs";import np from"node:path";function ee(e){hi(e);let t=np.join(e,"saffron.config.json"),n=_i.existsSync(t)?JSON.parse(_i.readFileSync(t,"utf8")):{};return De(e,n.dataDir,process.env.SAFFRON_ENV??n.env),n}var ws={provider:"claude",features:"features",dataDir:"data",actionTimeoutMs:5e3,pollIntervalMs:100,retries:1,screenshot:"viewport",maxTurns:100,strict:!1,verifyProposals:!0,snapshotMode:"none",reuseSteps:!0,browser:"chromium",workers:1,assertionPolicy:"strict",hookTimeoutMs:3e5,trace:"off"};function Ii(e){return{...ws,...Object.fromEntries(Object.entries(e).filter(([,t])=>t!==void 0))}}import vs from"node:fs";import cr from"node:path";import{fileURLToPath as rp}from"node:url";function lr(){let e=cr.dirname(rp(import.meta.url));for(let t=0;t<6;t++){let n=cr.join(e,"package.json");if(vs.existsSync(n))try{if(JSON.parse(vs.readFileSync(n,"utf8")).name==="saffron-ai")return e}catch{}e=cr.dirname(e)}}function Oe(){let e=lr();if(!e)return"unknown";try{return JSON.parse(vs.readFileSync(cr.join(e,"package.json"),"utf8")).version??"unknown"}catch{return"unknown"}}import ge from"node:fs";import se from"node:path";import pe from"node:fs";import{createHash as ip}from"node:crypto";import Pi from"node:os";import dr from"node:path";import nt from"node:fs";import qt from"node:path";import{createHash as sp,randomBytes as op}from"node:crypto";function lt(e,t){try{if(!nt.statSync(t).isFile())return!1;let n=qt.relative(nt.realpathSync(e),nt.realpathSync(t));return n!==""&&n!==".."&&!n.startsWith(".."+qt.sep)&&!qt.isAbsolute(n)}catch{return!1}}function je(e){return sp("sha256").update(e).digest("hex")}function Ae(e,t,n){nt.mkdirSync(qt.dirname(e),{recursive:!0});let r=`${e}.${process.pid}-${op(4).toString("hex")}.partial`;try{nt.writeFileSync(r,t),n!==void 0&&nt.chmodSync(r,n),nt.renameSync(r,e)}catch(s){throw nt.rmSync(r,{force:!0}),s}}function Jt(e,t){let n=qt.join(e,t.file);if(lt(qt.join(e,".saffron","artifacts"),n))try{let r=nt.readFileSync(n);return t.sha256&&je(r)!==t.sha256?void 0:r}catch{return}}function Ci(e,t){pe.mkdirSync(dr.dirname(e),{recursive:!0});let n=Buffer.from(`${JSON.stringify(t,null,2)}
13
+ `);return Wt(e,()=>Ae(e,n)),$s(n)}var ap=6e4,cp=5e3,lp=60*6e4,dp=new Int32Array(new SharedArrayBuffer(4)),xt=class extends Error{};function Wt(e,t,n=1e4){let r=`${e}.lock`,s=Date.now()+n,o;for(;;){o=JSON.stringify({pid:process.pid,host:Pi.hostname(),at:new Date().toISOString()});try{pe.writeFileSync(r,o,{flag:"wx"});break}catch(i){let a=i.code,c=a==="EPERM"&&process.platform==="win32";if(a!=="EEXIST"&&!c)throw i;if(Date.now()>=s)throw c?i:new xt(`${e} is being decided by another process. Try again.`);pp(r)||Atomics.wait(dp,0,0,25)}}try{return t()}finally{Ss(r)===o&&pe.rmSync(r,{force:!0})}}function Ss(e){try{return pe.readFileSync(e,"utf8")}catch{return}}function pp(e){let t=Ss(e);if(t===void 0)return!1;let n={};try{n=JSON.parse(t)||{}}catch{}let r=Date.parse(n.at??""),s=ap;if(Number.isNaN(r)){try{r=pe.statSync(e).mtimeMs}catch{return!1}s=cp}let o=n.host===Pi.hostname()&&typeof n.pid=="number";return o&&fp(n.pid)&&Date.now()-r<lp||!o&&Date.now()-r<s||Ss(e)!==t?!1:(pe.rmSync(e,{force:!0}),!0)}function fp(e){try{return process.kill(e,0),!0}catch(t){return t.code!=="ESRCH"}}function pr(e,t,n){return pe.existsSync(e)?Wt(e,()=>pe.existsSync(e)?t():n()):n()}function xs(e){let t=/^(.+)#([0-9a-f]{64})$/i.exec(e);return t?{file:t[1],revision:t[2].toLowerCase()}:{file:e}}function qe(e){return $s(pe.readFileSync(e))}var $s=e=>ip("sha256").update(e).digest("hex");function Gt(e){return fr(e).proposal}function fr(e){return ks(pe.readFileSync(e),e)}function ks(e,t){let n;try{n=JSON.parse(e.toString("utf8"))}catch(r){throw new X(t,`is not valid JSON: ${r instanceof Error?r.message:String(r)}`)}return vi(n,t),{proposal:n,revision:$s(e),bytes:e}}function Je(e,t){let n=dr.join(e,".saffron","proposals");if(!pe.existsSync(n))return[];let r=[];for(let s of pe.readdirSync(n)){let o=dr.join(n,s);if(pe.statSync(o).isDirectory())for(let i of pe.readdirSync(o)){if(!i.endsWith(".json"))continue;let a=dr.join(o,i);try{r.push({file:a,...fr(a)})}catch(c){if(!t||!(c instanceof X))throw c;t(a,c.problem)}}}return r}function ur(e,t){let n=Gt(t),r=Y(e,n.cache.feature,n.cache.scenario);return zt(r,n.cache),pe.rmSync(t),r}function gr(e){pe.rmSync(e)}var Ts=(e,t)=>se.relative(e,t).split(se.sep).join("/");function Oi(e){return ge.existsSync(e)?[...ge.readdirSync(e,{recursive:!0})].map(String).filter(t=>t.endsWith(".json")).map(t=>se.join(e,t)).sort():[]}function ji(e){try{let t=JSON.parse(ge.readFileSync(e,"utf8")),n="cache"in t&&t.cache?t.cache:t;return{feature:n.feature,scenario:n.scenario}}catch{return{}}}function Ni(e){try{let t=JSON.parse(ge.readFileSync(se.join(e,Bt),"utf8"));return{feature:typeof t.feature=="string"?t.feature:void 0,scenario:typeof t.scenario=="string"?t.scenario:void 0}}catch{return{}}}function mr(e,t,n){let r=se.isAbsolute(t)?t:se.join(e,t),s=new Set(ge.existsSync(r)?[...ge.readdirSync(r,{recursive:!0})].map(String).filter(tt).map(l=>se.join(r,l)):[]);for(let l of["cache","proposals"])for(let d of Oi(se.join(e,".saffron",l))){let{feature:f}=ji(d);if(typeof f=="string"){let m=se.resolve(e,f);ge.existsSync(m)&&s.add(m)}}for(let l of hr(se.join(e,".saffron","artifacts")))for(let d of hr(l)){let f=Ni(d).feature;if(f){let m=se.resolve(e,f);ge.existsSync(m)&&s.add(m)}}let o=[...n?.unreadable??[]],i=[...s].filter(l=>!n?.parsedFiles.has(l)),a=new Map;if(i.length>0)try{a=et([...new Set([...St(r),...s].filter(l=>l.endsWith(".saffron")))],e)}catch(l){o.push(l instanceof Error?l.message:String(l))}let c=new Set(n?.owned??[]);for(let l of i){let d;try{d=me(l,e,a)}catch(f){o.push(`${Ts(e,l)}: ${f instanceof Error?f.message:String(f)}`);continue}for(let f of d)c.add(Y(e,f.featurePath,f.name)),c.add(xe(e,f.featurePath,f.name)),c.add(Ue(e,f.featurePath,f.displayName))}return{...up(e,c),unreadable:o}}function up(e,t){let n=[],r=0;for(let o of["cache","proposal"]){let i=se.join(e,".saffron",o==="cache"?"cache":"proposals");for(let a of Oi(i)){if(t.has(a)){r++;continue}let{feature:c,scenario:l}=ji(a),d=c?se.join(e,c):void 0;n.push({file:Ts(e,a),kind:o,feature:c,scenario:l,reason:c?ge.existsSync(d)?"scenario-gone":"feature-gone":"unknown"})}}let s=se.join(e,".saffron","artifacts");for(let o of hr(s))for(let i of hr(o)){if(t.has(i)){r++;continue}let{feature:a,scenario:c}=Ni(i);n.push({file:Ts(e,i),kind:"artifact",feature:a,scenario:c,reason:a?ge.existsSync(se.join(e,a))?"scenario-gone":"feature-gone":"unknown"})}return{orphans:n,kept:r}}function hr(e){try{return ge.readdirSync(e,{withFileTypes:!0}).filter(t=>t.isDirectory()).map(t=>se.join(e,t.name)).sort()}catch{return[]}}function Fi(e,t){let n=[],r=new Set;for(let s of t){let o=se.join(e,s.file);try{s.kind==="proposal"?Wt(o,()=>ge.rmSync(o)):ge.rmSync(o,{recursive:s.kind==="artifact"}),n.push(s.file),r.add(se.dirname(o))}catch{}}for(let s of r)try{ge.readdirSync(s).length===0&&ge.rmdirSync(s)}catch{}return n}function Mi(e){if(e.kind==="artifact")return`${e.file}/
14
14
  failure screenshots of a scenario that no longer exists`;let t=e.feature&&e.scenario?`${e.feature} \u203A ${e.scenario}`:"an unreadable recording",n=e.reason==="feature-gone"?"the feature file is gone":e.reason==="scenario-gone"?"the scenario is gone from that file":"it names no scenario";return`${e.file}
15
- ${e.kind==="proposal"?"pending proposal for ":""}${t}: ${n}`}import aa from"node:path";var Zd=/\{date([+-]\d+)?(?::([^}]+))?\}/g;function Qd(e,t){let n=String(e.getFullYear()),r=String(e.getMonth()+1).padStart(2,"0"),s=String(e.getDate()).padStart(2,"0");return t.replaceAll("YYYY",n).replaceAll("MM",r).replaceAll("DD",s).replaceAll("M",String(e.getMonth()+1)).replaceAll("D",String(e.getDate()))}function bs(e,t=new Date){return e.replace(Zd,(n,r,s)=>{let o=new Date(t);return o.setDate(o.getDate()+(r?parseInt(r,10):0)),Qd(o,s??"YYYY-MM-DD")})}var Xd=/\b(\d{4})-(\d{2})-(\d{2})\b/g,ep=400;function Ri(e,t=new Date){let n=new Date(t.getFullYear(),t.getMonth(),t.getDate());return e.replace(Xd,(r,s,o,i)=>{let a=new Date(Number(s),Number(o)-1,Number(i));if(Number.isNaN(a.getTime()))return r;let c=Math.round((a.getTime()-n.getTime())/864e5);return Math.abs(c)>ep?r:c===0?"{date}":`{date${c>0?"+":""}${c}}`})}import{createHash as tp,randomBytes as np}from"node:crypto";var Ei=/\{unique:([A-Za-z0-9_-]+)(?::(digits))?\}/g,Ae="__saffron_unique_seed",xt=()=>`${Date.now().toString(36)}${np(4).toString("hex")}`;function Ai(e,t,n=!1){let r=tp("sha1").update(`${e}:${t}`).digest();if(n){let o=r.readUInt32BE(0)*65536+r.readUInt16BE(4);return String(1e8+o%9e8)}let s=BigInt(`0x${r.subarray(0,8).toString("hex")}`).toString(36).padStart(9,"0").slice(-9);return`${"abcdefghijklmnopqrstuvwxyz"[r[8]%26]}${s}`}function Jt(e,t){return e.replace(Ei,(n,r,s)=>Ai(t,r,s==="digits"))}function Gt(e,t){let n=new Map;for(let r of e)if(r)for(let s of r.matchAll(Ei))n.set(s[0],Ai(t,s[1],s[2]==="digits"));return n}function _i(e,t){let n=e;for(let[r,s]of t)n=n.split(s).join(r);return n}import{createHash as rp}from"node:crypto";async function vt(e){let t;try{t=await e.locator("body").ariaSnapshot({timeout:3e3})}catch{return"sha256:unavailable"}let n=t.replace(/\d+/g,"#").replace(/\s+/g," ").trim();return`sha256:${rp("sha256").update(n).digest("hex")}`}var sp=/^[a-z][a-z\d+.-]*:|^\/\//i,Ii=e=>e.pathname.replace(/\/+$/,"");function gn(e,t){if(!t)return e;let n,r;try{n=new URL(e),r=new URL(t)}catch{return e}if(n.origin!==r.origin)return e;let s=Ii(r),o=n.pathname,i;if(o===r.pathname||o===s||o===`${s}/`)i="/";else if(o.startsWith(`${s}/`))i=o.slice(s.length);else return e;let a=n.search===r.search?"":n.search||"?",c=i!=="/"?n.hash:n.hash===r.hash?"":n.hash||(r.hash?"#":"");return`${i}${a}${c}`}function ws(e,t){if(sp.test(e))return e;if(!t)throw new Error(`relative navigation "${e}" needs a base URL (set baseURL or pass --base-url)`);let n=new URL(t),r=e.indexOf("#"),s=r>=0?e.slice(r):void 0,o=r>=0?e.slice(0,r):e,i=o.indexOf("?"),a=i>=0?o.slice(i):void 0,c=i>=0?o.slice(0,i):o,l=c===""||c==="/",d=new URL(n.href);return d.pathname=l?n.pathname:`${Ii(n)}${c.startsWith("/")?c:`/${c}`}`,d.search=a===void 0?n.search:a==="?"?"":a,d.hash=s==="#"?"":s!==void 0?s:l?n.hash:"",d.toString()}import Pi from"node:fs";import op from"node:os";import je from"node:path";import{Client as ip}from"@modelcontextprotocol/sdk/client/index.js";import{StdioClientTransport as ap}from"@modelcontextprotocol/sdk/client/stdio.js";import{McpServer as cp}from"@modelcontextprotocol/sdk/server/mcp.js";import{CallToolRequestSchema as lp,ListToolsRequestSchema as dp}from"@modelcontextprotocol/sdk/types.js";var Ss=6e4;function Ci(){return Pi.mkdtempSync(je.join(op.tmpdir(),"saffron-mcp-"))}var xs=1e8,Oi=new Set(["browser_navigate","browser_click","browser_type","browser_press_sequentially","browser_fill_form","browser_select_option","browser_check","browser_uncheck","browser_press_key","browser_hover","browser_drag","browser_file_upload","browser_handle_dialog","browser_tabs","browser_snapshot","browser_find","browser_wait_for","browser_take_screenshot","browser_console_messages","browser_network_requests","browser_network_request","browser_evaluate"]);function pp(e){return{...e,tools:e.tools.filter(t=>Oi.has(t.name))}}function fp(e){return Oi.has(e)?void 0:{isError:!0,content:[{type:"text",text:`${e} is not available in a Saffron session.`}]}}var up=e=>({isError:!0,content:[{type:"text",text:e}]}),gp=new Set(["http:","https:","about:","data:"]);function hn(e){if(typeof e!="string"||/^localhost(:\d+)?(\/|$)/i.test(e))return;let t;try{t=new URL(e).protocol}catch{return}return gp.has(t)?void 0:`${e} is not a web page; a Saffron session only opens http(s) URLs.`}function vs(e,t){if(!Array.isArray(e))return;let n=i=>{try{return Pi.realpathSync(i)}catch{return}},r=n(t)??je.resolve(t),s=i=>{let a=je.relative(r,i);return a.startsWith("..")||je.isAbsolute(a)?void 0:a},o=i=>i.split(je.sep).some(a=>a.startsWith(".")&&a!=="..");for(let i of e){if(typeof i!="string")continue;let a=je.resolve(i),c=n(a),l=n(je.dirname(a)),d=s(c??(l?je.join(l,je.basename(a)):a));if(d===void 0)return`${i} is outside the project; uploads come from files inside it.`;if(o(d)||o(je.relative(je.resolve(t),a)))return`${i} is a hidden file; a Saffron session does not upload dotfiles.`;if(!c)return`${i} does not exist; uploads come from files inside the project.`}}function hp(e,t,n=process.cwd()){if(!t)return{args:t};let r=e==="browser_navigate"||e==="browser_tabs"?hn(t.url):e==="browser_file_upload"?vs(t.paths,n):void 0;if(r)return{args:t,refused:up(r)};if("filename"in t){let{filename:s,...o}=t;return{args:o}}return{args:t}}function mp(e,t=Ss){if(!Array.isArray(e.content))return e;let n=!1,r=e.content.map(s=>{if(s.type!=="text"||typeof s.text!="string"||s.text.length<=t)return s;n=!0;let o=`
15
+ ${e.kind==="proposal"?"pending proposal for ":""}${t}: ${n}`}import wa from"node:path";var gp=/\{date([+-]\d+)?(?::([^}]+))?\}/g;function hp(e,t){let n=String(e.getFullYear()),r=String(e.getMonth()+1).padStart(2,"0"),s=String(e.getDate()).padStart(2,"0");return t.replaceAll("YYYY",n).replaceAll("MM",r).replaceAll("DD",s).replaceAll("M",String(e.getMonth()+1)).replaceAll("D",String(e.getDate()))}function Rs(e,t=new Date){return e.replace(gp,(n,r,s)=>{let o=new Date(t);return o.setDate(o.getDate()+(r?parseInt(r,10):0)),hp(o,s??"YYYY-MM-DD")})}var mp=/\b(\d{4})-(\d{2})-(\d{2})\b/g,yp=400;function Di(e,t=new Date){let n=new Date(t.getFullYear(),t.getMonth(),t.getDate());return e.replace(mp,(r,s,o,i)=>{let a=new Date(Number(s),Number(o)-1,Number(i));if(Number.isNaN(a.getTime()))return r;let c=Math.round((a.getTime()-n.getTime())/864e5);return Math.abs(c)>yp?r:c===0?"{date}":`{date${c>0?"+":""}${c}}`})}import{createHash as bp,randomBytes as wp}from"node:crypto";var Li=/\{unique:([A-Za-z0-9_-]+)(?::(digits))?\}/g,_e="__saffron_unique_seed",$t=()=>`${Date.now().toString(36)}${wp(4).toString("hex")}`;function Ui(e,t,n=!1){let r=bp("sha1").update(`${e}:${t}`).digest();if(n){let o=r.readUInt32BE(0)*65536+r.readUInt16BE(4);return String(1e8+o%9e8)}let s=BigInt(`0x${r.subarray(0,8).toString("hex")}`).toString(36).padStart(9,"0").slice(-9);return`${"abcdefghijklmnopqrstuvwxyz"[r[8]%26]}${s}`}function Vt(e,t){return e.replace(Li,(n,r,s)=>Ui(t,r,s==="digits"))}function Kt(e,t){let n=new Map;for(let r of e)if(r)for(let s of r.matchAll(Li))n.set(s[0],Ui(t,s[1],s[2]==="digits"));return n}function Bi(e,t){let n=e;for(let[r,s]of t)n=n.split(s).join(r);return n}import{createHash as vp}from"node:crypto";async function kt(e){let t;try{t=await e.locator("body").ariaSnapshot({timeout:3e3})}catch{return"sha256:unavailable"}let n=t.replace(/\d+/g,"#").replace(/\s+/g," ").trim();return`sha256:${vp("sha256").update(n).digest("hex")}`}var Sp=/^[a-z][a-z\d+.-]*:|^\/\//i,zi=e=>e.pathname.replace(/\/+$/,"");function bn(e,t){if(!t)return e;let n,r;try{n=new URL(e),r=new URL(t)}catch{return e}if(n.origin!==r.origin)return e;let s=zi(r),o=n.pathname,i;if(o===r.pathname||o===s||o===`${s}/`)i="/";else if(o.startsWith(`${s}/`))i=o.slice(s.length);else return e;let a=n.search===r.search?"":n.search||"?",c=i!=="/"?n.hash:n.hash===r.hash?"":n.hash||(r.hash?"#":"");return`${i}${a}${c}`}function Es(e,t){if(Sp.test(e))return e;if(!t)throw new Error(`relative navigation "${e}" needs a base URL (set baseURL or pass --base-url)`);let n=new URL(t),r=e.indexOf("#"),s=r>=0?e.slice(r):void 0,o=r>=0?e.slice(0,r):e,i=o.indexOf("?"),a=i>=0?o.slice(i):void 0,c=i>=0?o.slice(0,i):o,l=c===""||c==="/",d=new URL(n.href);return d.pathname=l?n.pathname:`${zi(n)}${c.startsWith("/")?c:`/${c}`}`,d.search=a===void 0?n.search:a==="?"?"":a,d.hash=s==="#"?"":s!==void 0?s:l?n.hash:"",d.toString()}import Hi from"node:fs";import xp from"node:os";import Ne from"node:path";import{Client as $p}from"@modelcontextprotocol/sdk/client/index.js";import{StdioClientTransport as kp}from"@modelcontextprotocol/sdk/client/stdio.js";import{McpServer as Tp}from"@modelcontextprotocol/sdk/server/mcp.js";import{CallToolRequestSchema as Rp,ListToolsRequestSchema as Ep}from"@modelcontextprotocol/sdk/types.js";var As=6e4;function qi(){return Hi.mkdtempSync(Ne.join(xp.tmpdir(),"saffron-mcp-"))}var _s=1e8,Ji=new Set(["browser_navigate","browser_click","browser_type","browser_press_sequentially","browser_fill_form","browser_select_option","browser_check","browser_uncheck","browser_press_key","browser_hover","browser_drag","browser_file_upload","browser_handle_dialog","browser_tabs","browser_snapshot","browser_find","browser_wait_for","browser_take_screenshot","browser_console_messages","browser_network_requests","browser_network_request","browser_evaluate"]);function Ap(e){return{...e,tools:e.tools.filter(t=>Ji.has(t.name))}}function _p(e){return Ji.has(e)?void 0:{isError:!0,content:[{type:"text",text:`${e} is not available in a Saffron session.`}]}}var Ip=e=>({isError:!0,content:[{type:"text",text:e}]}),Pp=new Set(["http:","https:","about:","data:"]);function wn(e){if(typeof e!="string"||/^localhost(:\d+)?(\/|$)/i.test(e))return;let t;try{t=new URL(e).protocol}catch{return}return Pp.has(t)?void 0:`${e} is not a web page; a Saffron session only opens http(s) URLs.`}function Is(e,t){if(!Array.isArray(e))return;let n=i=>{try{return Hi.realpathSync(i)}catch{return}},r=n(t)??Ne.resolve(t),s=i=>{let a=Ne.relative(r,i);return a.startsWith("..")||Ne.isAbsolute(a)?void 0:a},o=i=>i.split(Ne.sep).some(a=>a.startsWith(".")&&a!=="..");for(let i of e){if(typeof i!="string")continue;let a=Ne.resolve(i),c=n(a),l=n(Ne.dirname(a)),d=s(c??(l?Ne.join(l,Ne.basename(a)):a));if(d===void 0)return`${i} is outside the project; uploads come from files inside it.`;if(o(d)||o(Ne.relative(Ne.resolve(t),a)))return`${i} is a hidden file; a Saffron session does not upload dotfiles.`;if(!c)return`${i} does not exist; uploads come from files inside the project.`}}function Cp(e,t,n=process.cwd()){if(!t)return{args:t};let r=e==="browser_navigate"||e==="browser_tabs"?wn(t.url):e==="browser_file_upload"?Is(t.paths,n):void 0;if(r)return{args:t,refused:Ip(r)};if("filename"in t){let{filename:s,...o}=t;return{args:o}}return{args:t}}function Op(e,t=As){if(!Array.isArray(e.content))return e;let n=!1,r=e.content.map(s=>{if(s.type!=="text"||typeof s.text!="string"||s.text.length<=t)return s;n=!0;let o=`
16
16
 
17
- [Saffron cut this result at ${t} of ${s.text.length} characters: whole, it would not fit the agent's context. Ask for less: a snapshot of one region, a filtered request list, or a saffron_assert on the field you need instead of the whole body.]`,i=/[\uD800-\uDBFF]/.test(s.text[t-1])?t-1:t;return{...s,text:s.text.slice(0,i)+o}});return n?{...e,content:r}:e}async function ji(e,t){let n=new ip({name:"saffron-cap",version:"1"}),r=Object.fromEntries(Object.entries(process.env).filter(o=>o[1]!==void 0));await n.connect(new ap({...e,env:r,stderr:"ignore"}));let s=$s(n,void 0,t);return{config:{type:"sdk",name:"playwright",instance:s},close:async()=>{await Promise.allSettled([n.close(),s.close()])}}}function $s(e,t=(r,s,o)=>o(),n){let r=new cp({name:"playwright",version:"1"},{capabilities:{tools:{}}});return r.server.setRequestHandler(dp,async()=>pp(await e.listTools())),r.server.setRequestHandler(lp,async(s,o)=>{let i=fp(s.params.name);if(i)return i;let a=hp(s.params.name,s.params.arguments,n);if(a.refused)return a.refused;let c={...s.params,arguments:a.args};return mp(await t(c.name,c.arguments,()=>e.callTool(c,void 0,{timeout:xs,signal:o.signal})))}),r}import yn from"node:fs";import vp from"node:os";import ks from"node:path";import ye from"node:fs";import yp from"node:path";import{createHash as bp,randomBytes as wp}from"node:crypto";import dr from"node:zlib";var ct=4294967295,Fi=67324752,Mi=33639248,Di=101010256;function Li(e){return mn(e).map(t=>({name:t.name,data:t.read()}))}function Ui(e,t){let n=Math.max(0,t-22-65535),r=e(n,t-n),s=-1;for(let l=r.length-22;l>=0;l--)if(r.readUInt32LE(l)===Di){s=l;break}if(s<0)throw new Error("not a zip file: no central directory");let o=r.readUInt16LE(s+10),i=e(r.readUInt32LE(s+16),r.readUInt32LE(s+12)),a=0,c=[];for(let l=0;l<o;l++){if(i.readUInt32LE(a)!==Mi)throw new Error("not a zip file: bad central directory entry");let d=i.readUInt16LE(a+10),f=i.readUInt32LE(a+16),m=i.readUInt32LE(a+20),p=i.readUInt32LE(a+24),b=i.readUInt16LE(a+28),v=i.readUInt16LE(a+30),k=i.readUInt16LE(a+32),w=i.readUInt32LE(a+42),x=i.subarray(a+46,a+46+b).toString("utf8");a+=46+b+v+k;let y=e(w,30);if(y.length<30||y.readUInt32LE(0)!==Fi)throw new Error(`not a zip file: bad local header for ${x}`);let h=w+30+y.readUInt16LE(26)+y.readUInt16LE(28);if(!x.endsWith("/")){if(d!==0&&d!==8)throw new Error(`zip entry ${x} uses compression method ${d}, which is not supported`);c.push({name:x,method:d,crc:f,size:p,compressedSize:m,start:h})}}return c}function mn(e){return Ui((t,n)=>e.subarray(t,t+n),e.length).map(({name:t,method:n,crc:r,size:s,compressedSize:o,start:i})=>{let a=e.subarray(i,i+o);return{name:t,method:n,crc:r,size:s,raw:a,read:()=>n===0?Buffer.from(a):dr.inflateRawSync(a)}})}function Sp(e,t,n){let r=ye.openSync(e,"r");try{let s=Buffer.alloc(n),o=0;for(;o<n;){let i=ye.readSync(r,s,o,n-o,t+o);if(i===0)break;o+=i}return s.subarray(0,o)}finally{ye.closeSync(r)}}function Bi(e){let t=ye.statSync(e).size,n=(r,s)=>Sp(e,r,Math.max(0,Math.min(s,t-r)));return Ui(n,t).map(r=>({...r,raw:()=>n(r.start,r.compressedSize)}))}var xp=(()=>{let e=new Uint32Array(256);for(let t=0;t<256;t++){let n=t;for(let r=0;r<8;r++)n=n&1?3988292384^n>>>1:n>>>1;e[t]=n>>>0}return e})();function lr(e,t=0){let n=dr.crc32;if(n)return n(e,t)>>>0;let r=(t^4294967295)>>>0;for(let s of e)r=xp[(r^s)&255]^r>>>8;return(r^4294967295)>>>0}var Ni=1024*1024;function zi(e,t){let n=ye.openSync(e,"r");try{let r=Buffer.alloc(Ni);for(let s;(s=ye.readSync(n,r,0,Ni,null))>0;)t(r.subarray(0,s))}finally{ye.closeSync(n)}}function pr(e){let t=bp("sha256"),n=0,r=0;return zi(e,s=>{n=lr(s,n),r+=s.length,t.update(s)}),{crc:n,size:r,sha256:t.digest("hex")}}function qi(e,t,n,r,s,o){let i=Buffer.alloc(30);i.writeUInt32LE(Fi,0),i.writeUInt16LE(20,4),i.writeUInt16LE(2048,6),i.writeUInt16LE(t,8),i.writeUInt32LE(0,10),i.writeUInt32LE(n,14),i.writeUInt32LE(r,18),i.writeUInt32LE(s,22),i.writeUInt16LE(e.length,26),i.writeUInt16LE(0,28);let a=Buffer.alloc(46);return a.writeUInt32LE(Mi,0),a.writeUInt16LE(20,4),a.writeUInt16LE(20,6),a.writeUInt16LE(2048,8),a.writeUInt16LE(t,10),a.writeUInt32LE(0,12),a.writeUInt32LE(n,16),a.writeUInt32LE(r,20),a.writeUInt32LE(s,24),a.writeUInt16LE(e.length,28),a.writeUInt16LE(0,30),a.writeUInt16LE(0,32),a.writeUInt16LE(0,34),a.writeUInt16LE(0,36),a.writeUInt32LE(0,38),a.writeUInt32LE(o,42),{local:i,central:a}}function Hi(e,t,n){let r=Buffer.alloc(22);return r.writeUInt32LE(Di,0),r.writeUInt16LE(0,4),r.writeUInt16LE(0,6),r.writeUInt16LE(e,8),r.writeUInt16LE(e,10),r.writeUInt32LE(t,12),r.writeUInt32LE(n,16),r.writeUInt16LE(0,20),r}var Ji=e=>new Error(`cannot write a zip of ${e} files: one holds at most 65,535`),Gi=e=>new Error(`cannot write ${e} into a zip: a zip holds at most 4 GB, and this one would be larger`),Wi=()=>new Error("cannot write the zip: a zip holds at most 4 GB, and this one would be larger");function Vi(e){if(e.length>65535)throw Ji(e.length);let t=[],n=[],r=0;for(let o of e){let i="raw"in o,a=i?o.raw:dr.deflateRawSync(o.data),c=i?o.size:o.data.length,l=Buffer.from(o.name,"utf8");if(c>ct||r+30+l.length+a.length>ct)throw Gi(o.name);let{local:d,central:f}=qi(l,i?o.method:8,i?o.crc:lr(o.data),a.length,c,r);t.push(d,l,a),n.push(f,l),r+=d.length+l.length+a.length}let s=n.reduce((o,i)=>o+i.length,0);if(r+s>ct)throw Wi();return Buffer.concat([...t,...n,Hi(e.length,s,r)])}function Wt(e,t){for(let n=0;n<t.length;)n+=ye.writeSync(e,t,n,t.length-n)}function Ki(e,t){if(t.length>65535)throw Ji(t.length);ye.mkdirSync(yp.dirname(e),{recursive:!0});let n=`${e}.${process.pid}-${wp(4).toString("hex")}.partial`,r=ye.openSync(n,"w");try{let s=[],o=0;for(let a of t){let c=Buffer.from(a.name,"utf8"),l="file"in a,d=l&&(a.crc===void 0||a.size===void 0)?pr(a.file):void 0,f=l?void 0:dr.deflateRawSync(a.data),m=l?d?.crc??a.crc:lr(a.data),p=l?d?.size??a.size:a.data.length,b=f?.length??p;if(p>ct||o+30+c.length+b>ct)throw Gi(a.name);let{local:v,central:k}=qi(c,l?0:8,m,b,p,o);if(Wt(r,v),Wt(r,c),f)Wt(r,f);else{let w=0,x=0;if(zi(a.file,y=>{w+y.length>p||(Wt(r,y),w+=y.length,x=lr(y,x))}),w!==p||x!==m)throw new Error(`${a.name} changed while it was being packed`)}s.push(k,c),o+=v.length+c.length+b}let i=s.reduce((a,c)=>a+c.length,0);if(o+i>ct)throw Wi();for(let a of s)Wt(r,a);Wt(r,Hi(t.length,i,o)),ye.closeSync(r),ye.renameSync(n,e)}catch(s){try{ye.closeSync(r)}catch{}throw ye.rmSync(n,{force:!0}),s}}var Ts=["off","on","retain-on-failure"],Rs="trace.zip";async function Es(e,t){await e.tracing.start({screenshots:!0,snapshots:!0,sources:!1,title:t})}async function As(e,t){t&&yn.mkdirSync(ks.dirname(t),{recursive:!0}),await e.tracing.stop(t?{path:t}:void 0)}async function _s(e,t,n){let r;try{r=yn.mkdtempSync(ks.join(vp.tmpdir(),"saffron-trace-"));let s=ks.join(r,Rs);await e.tracing.stop({path:s});let o=jp(yn.readFileSync(s),n);return Ee(t,o),Oe(o)}catch{try{yn.rmSync(t,{force:!0})}catch{console.warn("Could not remove the execution trace artifact.")}console.warn("Could not save the execution trace; no trace was attached to this run.");return}finally{if(r)try{yn.rmSync(r,{recursive:!0,force:!0})}catch{console.warn("Could not remove the private temporary trace directory.")}}}async function Zi(e,t,n,r){if(!t)return r();let s=e.context().tracing;await s.group(n).catch(()=>{});try{return await r()}finally{await s.groupEnd().catch(()=>{})}}var $p=new Set(["callId","parentId","pageId","frameId","contextId","pageref","_frameref","snapshotName","beforeSnapshot","afterSnapshot","inputSnapshot","sha1","_sha1"]),kp=new Set(["before","action","after"]),Tp=/^\[saffron:(?:agent|replay|pending|action):-?\d+\] /,Rp=new Set(["type","class","method"]),Ep=new Set(["__playwright_bounding_rect__","__playwright_scroll_top_","__playwright_scroll_left_","__playwright_target__","__playwright_checked_","__playwright_selected_","__playwright_popover_open_","__playwright_dialog_open_","__playwright_shadow_root_","__playwright_custom_elements__"]),Ap=new Set(["style","__playwright_style_sheet_"]),_p=new Set(["srcset","imagesrcset"]),Ip=/^(?:image\/(?!svg)|font\/|audio\/|video\/)/i,Pp=/\.(?:jpe?g|png|gif|webp|avif|bmp|ico|woff2?|ttf|otf|eot|mp3|mp4|m4a|wav|ogg|webm)$/i,Cp=/^text\/css\b/i,Op=/\.css$/i;function jp(e,t){let n=mt(t,{encodings:!0});if(n.empty)return e;let r=mt(t,{encodings:!0,bytes:!0}),s=mn(e),o=new Set(s.filter(p=>p.name.includes("/")).map(p=>p.name));for(let p of s)p.name.startsWith("resources/")&&o.add(p.name.slice(10));let i={names:o,pieces:Jp(t)},a=new Set,c=new Set,l=p=>{for(let b of[p.snapshot?.response?.content,p.snapshot?.request?.postData]){if(!b)continue;let v=b._file??(b._sha1&&`resources/${b._sha1}`);if(!v)continue;let k=String(b.mimeType??"");Ip.test(k)?a.add(v):Cp.test(k)&&c.add(v)}},d=[...s],f=!1,m=(p,b)=>{b&&(d[p]={name:s[p].name,data:b},f=!0)};return s.forEach((p,b)=>{/\.(?:trace|network)$/.test(p.name)?m(b,Np(p.read(),n,i,p.name.endsWith(".network")?l:void 0)):p.name.endsWith(".stacks")&&m(b,Fp(p.read(),n,i))}),s.forEach((p,b)=>{/\.(?:trace|network|stacks)$/.test(p.name)||a.has(p.name)||Pp.test(p.name)||m(b,Mp(p.read(),n,r,c.has(p.name)||Op.test(p.name)))}),f?Vi(d):e}function Np(e,t,n,r){let s=e.toString("utf8").split(`
18
- `),o=!1;for(let i=0;i<s.length;i++){let a=s[i];if(!a.trim())continue;let c=JSON.parse(a);if(r?.(c),!t.occursIn(a)&&!t.hiddenByEscape.test(a))continue;let l=Ps(a,t,{keys:!1,numbers:!1,playwright:n});l!==a&&(s[i]=l,o=!0)}return o?Buffer.from(s.join(`
19
- `),"utf8"):void 0}function Fp(e,t,n){let r=e.toString("utf8");if(JSON.parse(r),!t.occursIn(r)&&!t.hiddenByEscape.test(r))return;let s=Ps(r,t,{keys:!1,numbers:!1,playwright:n});return s===r?void 0:Buffer.from(s,"utf8")}function Mp(e,t,n,r=!1){if(r){let d=e.toString("latin1"),f=ta(d,n);return f===d?void 0:Buffer.from(f,"latin1")}let s=e.toString("utf8"),o=s.startsWith("\uFEFF")?"\uFEFF":"",i=s.slice(o.length);if(!s.includes("\uFFFD")||Buffer.from(s,"utf8").equals(e)){let d=/^\s*[[{"]/.test(i)?Qi(i,t):void 0,f=d===void 0&&/^\s*(?:[[{"\d-]|true|false|null)/.test(i)&&i.trim().includes(`
20
- `)?Dp(i,t):void 0,m=d??f;if(m!==void 0)return m===i?void 0:Buffer.from(o+m,"utf8")}let c=e.toString("latin1"),l=n.mask(c);return l===c?void 0:Buffer.from(l,"latin1")}function Qi(e,t){try{JSON.parse(e)}catch(n){if(n instanceof SyntaxError)return;throw n}return!t.occursIn(e)&&!t.hiddenByEscape.test(e)?e:Ps(e,t,{keys:!0,numbers:!0})}function Dp(e,t){let n=e.split(`
21
- `),r=[];for(let s of n){if(!s.trim()){r.push(s);continue}let i=Qi(s,t);if(i===void 0)return;r.push(i)}return r.join(`
22
- `)}var Xi=/^([a-z][a-z0-9+.-]*:\/\/)([^/?#]*@)?([^/?#]*)/i;function ea(e,t=0,n=e.length){let r=Xi.exec(e.slice(t,n));if(!r)return;let[s,o,i=""]=r,a=t+o.length+i.length;return c=>c>=t&&c<t+o.length||c>=a-(i?1:0)&&c<=t+s.length&&c<n}function Lp(e,t,n){let r="",s=0;for(let o of t.matches(e)){let{token:i,kept:a=()=>!1}=n(o),c="";for(let l=o.start;l<o.end;){if(a(l)){c+=e[l++];continue}for(;l<o.end&&!a(l);)l++;c+=i(o.names)}c!==e.slice(o.start,o.end)&&(r+=e.slice(s,o.start)+c,s=o.end)}return s===0?e:r+e.slice(s)}var Up=e=>e.map(t=>`%7Benv:${t}%7D`).join(""),Bp=e=>e.map(t=>`\\{env\\:${t}\\}`).join("");function Is(e,t,n,r){let s=n.map(([i,a])=>({start:i,end:a,kept:ea(e,i,a)})),o=0;return Lp(e,t,i=>{for(;o<s.length&&s[o].end<i.end;)o++;let a=s[o];return a&&a.start<=i.start&&i.end<=a.end?{token:Up,kept:a.kept}:{token:r}})}function zp(e,t){return ea(e)?Is(e,t,[[0,e.length]],fn):t.mask(e)}var qp=/url\(\s*(?:"([^"]*)"|'([^']*)'|([^)\s'"]*))|@import\s+(?:"([^"]*)"|'([^']*)')/dgi;function ta(e,t){if(!t.occursIn(e))return e;let n=[...e.matchAll(qp)].map(r=>r.indices.slice(1).find(s=>s!==void 0));return Is(e,t,n,Bp)}function Hp(e,t){let n=[];for(let r=0;r<e.length;){for(;r<e.length&&/[\s,]/.test(e[r]);)r++;let s=r;for(;s<e.length&&!/\s/.test(e[s]);)s++;let o=s;for(;o>r&&e[o-1]===",";)o--;if(o>r&&n.push([r,o]),r=s,o===s)for(;r<e.length&&e[r]!==",";)r++}return Is(e,t,n,fn)}function Jp(e){let t=new Map;for(let[n,r]of e)if(!(r.length<ht||!Xi.test(r)||!URL.canParse(r)))for(let s of new URL(r).searchParams.values())s.length>=ht&&!t.has(s)&&t.set(s,n);return t}var Yi=e=>e===" "||e===" "||e===`
23
- `||e==="\r";function Ps(e,t,n){let r=[],s=0,o,i=[],a=c=>{let l=c-1;for(;l>=0&&Yi(e[l]);)l--;return e[l]};for(let c=0;c<e.length;c++){let l=e[c];if(l==="{"||l==="["){let d=i.at(-1),f=a(c)===":"?o:void 0;i.push({dom:(d?.dom??!1)||f==="html",logged:(d?.logged??!1)||f==="value"});continue}if(l==="}"||l==="]"){i.pop();continue}if(l==='"'){let d=c++,f=!1;for(;c<e.length&&e[c]!=='"';c++)e[c]==="\\"&&(f=!0,c++);let m=c+1,p=m;for(;p<e.length&&Yi(e[p]);)p++;let b=e[p]===":",v=f?JSON.parse(e.slice(d,m)):e.slice(d+1,c),k=i.at(-1),w=!b&&a(d)===":"?o:void 0,x=S=>t.mask(S);if(b){if(o=v,!n.keys)continue}else if(n.playwright){if(i.length===1&&w!==void 0&&Rp.has(w)||n.playwright.names.has(v))continue;let{pieces:S}=n.playwright;if(x=$=>S.has($)?fn([S.get($)]):zp($,t),k?.dom){if(a(d)==="["){k.tag=v.toUpperCase();continue}let $=w===void 0?k.tag:i.at(-2)?.tag;if(w!==void 0&&Ep.has(w)||w==="src"&&($==="IFRAME"||$==="FRAME"))continue;w===void 0&&$==="STYLE"||w!==void 0&&Ap.has(w)?x=u=>ta(u,t):w!==void 0&&_p.has(w)&&(x=u=>Hp(u,t))}else if(w!==void 0&&$p.has(w)||w==="phase"&&kp.has(v))continue}let y=n.playwright&&i.length===1&&w==="title"?Tp.exec(v)?.[0]??"":"",h=y+x(v.slice(y.length));h!==v&&(r.push(e.slice(s,d),JSON.stringify(h)),s=m)}else if(l==="-"||l>="0"&&l<="9"){let d=c+1;for(;d<e.length&&/[0-9eE.+-]/.test(e[d]);)d++;(n.numbers||(i.at(-1)?.logged??!1)||a(c)===":"&&o==="value")&&t.isValue(e.slice(c,d))&&(r.push(e.slice(s,c),JSON.stringify(t.mask(e.slice(c,d)))),s=d),c=d-1}}return r.length?r.join("")+e.slice(s):e}function wn(e){return Array.isArray(e)?e.map(wn):e&&typeof e=="object"?Object.fromEntries(Object.entries(e).filter(([t,n])=>n!==void 0&&t!=="pageFingerprint").sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>[t,wn(n)])):e}var Cs=e=>JSON.stringify(wn(e));function Os(e,t="",n={}){if(Array.isArray(e))n[t]=e.map(r=>String(r)).join(", ");else if(e&&typeof e=="object")for(let[r,s]of Object.entries(e))Os(s,t?`${t}.${r}`:r,n);else t&&(n[t]=String(e));return n}function Gp(e,t){let n=Os(wn(e)),r=Os(wn(t)),s=[...new Set([...Object.keys(n),...Object.keys(r)])].sort(),o=[];for(let i of s){if(i==="action"||n[i]===r[i])continue;let a=i.replace(/^target\./,"").replace(/^toTarget\./,"destination ");n[i]===void 0?o.push(`${a}: added "${r[i]}"`):r[i]===void 0?o.push(`${a}: removed "${n[i]}"`):o.push(`${a}: "${n[i]}" \u2192 "${r[i]}"`)}return o}function oa(e,t,n){let r=e.map(n),s=t.map(n),o=Array.from({length:r.length+1},()=>new Array(s.length+1).fill(0));for(let l=r.length-1;l>=0;l--)for(let d=s.length-1;d>=0;d--)o[l][d]=r[l]===s[d]?o[l+1][d+1]+1:Math.max(o[l+1][d],o[l][d+1]);let i=[],a=0,c=0;for(;a<r.length&&c<s.length;)r[a]===s[c]?(i.push({before:e[a],after:t[c]}),a++,c++):o[a+1][c]>=o[a][c+1]?(i.push({before:e[a]}),a++):(i.push({after:t[c]}),c++);for(;a<r.length;a++)i.push({before:e[a]});for(;c<s.length;c++)i.push({after:t[c]});return i}function na(e){if(!e)return"";let t=[];if(e.role){let n=e.name?` "${e.name}"`:e.nameRegex?` matching /${e.nameRegex}/`:"";t.push(`${e.role}${n}`)}else e.label?t.push(`label "${e.label}"`):e.text?t.push(`text "${e.text}"`):e.placeholder?t.push(`placeholder "${e.placeholder}"`):e.testId&&t.push(`test id ${e.testId}`);return e.fallbackSelectors?.length&&t.push(`[${e.fallbackSelectors.join(", ")}]`),e.frame&&t.push(`inside frame ${e.frame}`),t.join(" ")}var ra=e=>`${e.method??"any"} /${e.urlPattern}/ ${e.status??"2xx"}`+(e.pattern?` with a body matching /${e.pattern}/`:"");function ve(e){let t=na(e.target),n=e.value??"",r=(()=>{switch(e.action){case"goto":return`go to ${e.url}`;case"click":return`click ${t}`;case"fill":return`fill ${t} with "${n}"`;case"press":return t?`press ${n} on ${t}`:`press ${n}`;case"selectOption":return`select "${n}" in ${t}`;case"check":return`check ${t}`;case"uncheck":return`uncheck ${t}`;case"hover":return`hover ${t}`;case"waitFor":return t?`wait for ${t}`:`wait ${n}ms`;case"captureText":return`remember ${t} as "${e.saveAs}"`;case"handleDialog":return`${n} the dialog${e.promptText?` with "${e.promptText}"`:""}`;case"uploadFile":return`upload ${n}`;case"dragTo":return`drag ${t} onto ${na(e.toTarget)}`;case"waitForResponse":return`wait for ${ra(e)}`;case"switchTab":return`switch to tab ${n}`;case"closeTab":return`close tab ${n||"(the current one)"}`;case"newTab":return`open a new tab${e.url?` at ${e.url}`:""}`;case"expectVisible":return`expect ${t} to be visible`;case"expectNotVisible":return`expect ${t} to be gone`;case"expectText":return`expect ${t} to contain "${n}"`;case"expectUrl":return`expect the URL to contain "${e.url??n}"`;case"expectValue":return`expect ${t} to hold "${n}"`;case"expectDiffers":return`expect ${t||`"${n}"`} to differ from "${e.compareTo}"`;case"expectMatches":return`expect ${t} to match /${e.pattern}/`;case"expectAttribute":return`expect ${t} ${e.attribute} to match /${e.pattern}/`;case"expectResponse":return`expect a response ${ra(e)}`;case"expectEach":return`expect ${t} to show every value of ${e.each}`;default:return e.action}})();return e.opensTab?`${r}, opening a tab`:r}var bn=e=>Se.has(e.action);function Wp(e,t){let n=oa(e,t,Cs),r=[];for(let s=0;s<n.length;s++){let o=n[s];if(o.before&&o.after){r.push({change:"same",before:ve(o.before),after:ve(o.after),assertion:bn(o.after)});continue}let i=n[s+1];if(o.before&&i?.after&&!i.before){r.push({change:"changed",before:ve(o.before),after:ve(i.after),details:Gp(o.before,i.after),assertion:bn(o.before)||bn(i.after)}),s++;continue}o.before&&r.push({change:"removed",before:ve(o.before),assertion:bn(o.before)}),o.after&&r.push({change:"added",after:ve(o.after),assertion:bn(o.after)})}return r}var Vp=e=>`${e.kind}\0${e.gherkin}`;function lt(e,t){let n=oa(e?.steps??[],t.steps,Vp),r=[],s={added:0,removed:0,changed:0,assertions:0};for(let o of n){let i=o.after??o.before,a=Wp(o.before?.actions??[],o.after?.actions??[]);for(let l of a)l.change==="added"?s.added++:l.change==="removed"?s.removed++:l.change==="changed"&&s.changed++,l.change!=="same"&&l.assertion&&s.assertions++;let c=e&&o.before?o.after?a.some(l=>l.change!=="same")?"changed":"same":"removed":"added";r.push({gherkin:i.gherkin,kind:i.kind,fromStepSet:i.fromStepSet,change:c,actions:a})}return{feature:t.feature,scenario:t.scenario,hasCommitted:e!==void 0,steps:r,summary:s}}var sa=new Set(["goto","click","press","newTab","switchTab","closeTab"]);function ia(e,t){let n=[],r=s=>`${s} navigation action${s===1?"":"s"}`;for(let s=0;s<Math.min(e.length,t.length);s++){let o=e[s].actions.filter(f=>sa.has(f.action)),a=[...t[s].actions.filter(f=>sa.has(f.action))],c=[];for(let f of o){let m=a.findIndex(p=>Cs(p)===Cs(f));m>=0?a.splice(m,1):c.push(f)}let l=a;for(let f=c.length-1;f>=0;f--){let m=l.findIndex(p=>p.action===c[f].action);m>=0&&(l.splice(m,1),c.splice(f,1))}let d=`step ${s+1} ("${e[s].gherkin}")`;c.length>0&&n.push(`flow change: ${d} no longer performs ${r(c.length)} from the committed recording: ${c.map(ve).join("; ")}. A page or navigation step may have been removed or merged.`),l.length>0&&n.push(`flow change: ${d} now performs ${r(l.length)} the committed recording did not: ${l.map(ve).join("; ")}. A page or navigation step may have been added or reordered.`)}return n}function fr(e){let t=[];t.push(`${e.feature} \u203A ${e.scenario}`),t.push(e.hasCommitted?"Comparing the committed recording with the proposed one.":"First recording for this scenario: there is nothing to compare against."),t.push("");for(let i of e.steps){let a=i.change==="removed"?"- ":i.change==="added"&&e.hasCommitted?"+ ":" ";t.push(`${a}${i.kind==="assertion"?"[assertion] ":""}${i.gherkin}`+(i.fromStepSet?` (from step set "${i.fromStepSet}")`:""));for(let c of i.actions)if(c.change==="same")t.push(` ${c.before}`);else if(c.change==="added")t.push(` + ${c.after}`);else if(c.change==="removed")t.push(` - ${c.before}`);else{t.push(` - ${c.before}`),t.push(` + ${c.after}`);for(let l of c.details??[])t.push(` ${l}`)}t.push("")}let{added:n,removed:r,changed:s,assertions:o}=e.summary;return t.push(n+r+s===0?"No action changed.":`${s} changed, ${n} added, ${r} removed.`+(o>0?` ${o} of them ${o===1?"is an assertion":"are assertions"}: read those first.`:"")),t.join(`
24
- `)}var Ms=e=>new Promise(t=>setTimeout(t,e)),Ns=class{entries=[];seq=0;prevStepSeq=0;curStepSeq=0;listener;context;attach(t,n){this.context=t,this.listener=r=>{let s={seq:++this.seq,url:r.url(),method:r.request().method(),status:r.status()};if(this.entries.push(s),n){let o=r.headers()["content-type"]??"";/json|text|xml/i.test(o)&&r.text().then(i=>{s.body=i.length>262144?i.slice(0,262144):i}).catch(()=>{})}},t.on("response",this.listener)}detach(){this.context&&this.listener&&this.context.off("response",this.listener)}markStep(){this.prevStepSeq=this.curStepSeq,this.curStepSeq=this.seq}sincePreviousStep(){return this.entries.filter(t=>t.seq>this.prevStepSeq)}all(){return this.entries}};function Kp(e,t){if(!t)return e>=200&&e<300;let n=/^([1-5])xx$/i.exec(t.trim());if(n){let r=Number(n[1])*100;return e>=r&&e<r+100}return e===Number(t.trim())}function ca(e,t,n,r,s){return!(!t.test(e.url)||n&&e.method.toUpperCase()!==n.toUpperCase()||!Kp(e.status,r)||s&&!s.test(e.body??""))}var Yp=(e,t)=>`response matching ${e.method??"any-method"} /${t}/ status ${e.status??"2xx"}${e.pattern?` body /${e.pattern}/`:""}`,la=new Set(["click","fill","press","selectOption","check","uncheck","hover","dragTo","uploadFile"]),Zp=/(?<=(?:^|[^\\])(?:\\\\)*)(\\?)(<[^<>]+>|\{(?:env|data|date)[^{}]*\})/g;function Fs(e,t,n,r=!1){return e.replace(Zp,(s,o,i,a)=>{let c=e.slice(Math.max(0,a-2),a);if(i.startsWith("<")&&(c==="(?"||c==="\\k"))return s;let l=t(i);return l===i?s:(r?"":o)+n(l)})}function da(e,t,n,r=!1,s=!0){let o=c=>pn(dn(bs(yt(c,n)))),i=[],a=c=>{if(t.role){let l=t.nameRegex?new RegExp(Fs(t.nameRegex,o,oe.pattern,!0),"i"):t.name?o(t.name):void 0,d=t.role;l===void 0&&!!(t.label||t.placeholder||t.text||t.testId||t.fallbackSelectors?.length)?t.text&&i.push(c.getByRole(d).filter({hasText:o(t.text)})):typeof l=="string"?(i.push(c.getByRole(d,{name:l,exact:!0})),s&&i.push(c.getByRole(d,{name:l,exact:!1}))):i.push(c.getByRole(d,{name:l,exact:!1}))}t.label&&i.push(c.getByLabel(o(t.label))),t.placeholder&&i.push(c.getByPlaceholder(o(t.placeholder))),t.text&&!t.role&&i.push(c.getByText(o(t.text))),t.testId&&i.push(c.getByTestId(o(t.testId)));for(let l of t.fallbackSelectors??[])i.push(c.locator(Ft(l,(d,f)=>Fs(d,o,oe[f],f==="regex"),o)))};if(t.frame)a(e.frameLocator(o(t.frame)));else if(a(e),r)for(let c of e.frames())c!==e.mainFrame()&&a(c);if(i.length===0)throw new Error("Action target has neither role nor fallback selectors");return i}var fa=100;async function Qp(e,t,n,r){let s=Date.now()+t,o;for(;Date.now()<s;){try{if(await e())return}catch(i){o=i}await Ms(r)}throw new Error(`Timed out waiting for ${n}${o?` (${String(o)})`:""}`)}async function Je(e,t,n=!1){let r;for(let s of e)try{await t(s.first());return}catch(o){if(r=o,n&&await s.first().count().catch(()=>0)>0)throw o}throw r}var js=e=>e.replace(/\s+/g," ").trim(),Sn=new WeakMap;async function gr(e,t,n,r,s,o=new Map,i,a=fa){let c=(S,$,u)=>Qp(S,$,u,a),l=Sn.get(e);if(l&&la.has(t.action)&&t.action!=="uploadFile"){Sn.delete(e);let S,$=g=>{S??=g};e.on("filechooser",$);let u;try{if(u=await gr(e,t,n,r,s,o,i,a),S||await e.waitForEvent("filechooser",{timeout:r}).then($,()=>{}),!S)throw new Error(`uploadFile failed: the ${t.action} after it opened no file chooser within ${r} ms`);try{await S.setFiles(l,{timeout:r})}catch(g){throw new Error(`uploadFile failed: ${g instanceof Error?g.message:String(g)}`)}}catch(g){throw Sn.set(e,l),g}finally{e.off("filechooser",$)}return u}o.has(Ae)||o.set(Ae,xt());let d=o.get(Ae),f=S=>typeof S=="string"?Jt(S,d):Array.isArray(S)?S.map(f):S&&typeof S=="object"?Object.fromEntries(Object.entries(S).map(([$,u])=>[$,f(u)])):S;t=f(t),n=f(n);let m=S=>pn(dn(bs(yt(S,n)))),p=S=>Fs(S,m,oe.pattern,!0),b=t.value!==void 0?m(t.value):void 0,v=t.url!==void 0?m(t.url):void 0,k=t.action==="expectVisible"||t.action==="expectText"||t.action==="expectValue"||t.action==="expectMatches"||t.action==="expectAttribute"||t.action==="expectEach"||t.action==="captureText",w=t.target?da(e,t.target,n,k,!la.has(t.action)):[],x={timeout:r},y={timeout:Math.min(1e3,r)},h=S=>async()=>{for(let $ of w)try{if(await S($.first()))return!0}catch{}return!1};switch(t.action){case"goto":{if(!v)throw new Error("goto requires url");let S=hn(v);if(S)throw new Error(S);await e.goto(ws(v,s),x);return}case"click":return Je(w,S=>S.click(x),!0);case"fill":return Je(w,S=>S.fill(b??"",x),!0);case"press":return w.length>0?Je(w,S=>S.press(b??"",x),!0):e.keyboard.press(b??"");case"selectOption":return Je(w,async S=>{await S.selectOption(b??"",x)},!0);case"check":return Je(w,S=>S.check(x),!0);case"uncheck":return Je(w,S=>S.uncheck(x),!0);case"hover":return Je(w,S=>S.hover(x),!0);case"waitFor":return w.length>0?Je(w,S=>S.waitFor({state:"visible",timeout:r})):Ms(Math.min(Number(b??"0"),r));case"expectVisible":return c(h(S=>S.isVisible()),r,"target to be visible");case"expectNotVisible":return c(async()=>{for(let S of w){let $=await S.count();for(let u=0;u<$;u++)if(await S.nth(u).isVisible())return!1}return!0},r,`every match of every recorded locator to be hidden (${w.length} locator(s))`);case"expectText":return c(h(async S=>(await S.textContent(y)??"").includes(b??"")),r,`text "${b}"`);case"expectEach":{let S=t.each?.match(/^\{data:([^}]+)\}$/)?.[1];if(!S)throw new Error(`expectEach needs a {data:...} list reference, got ${JSON.stringify(t.each)}`);let $=ot(S),u=async R=>await R.isVisible()?new Set(await R.evaluate(E=>{let I=F=>(F??"").replace(/\s+/g," ").trim(),A=F=>typeof F.checkVisibility=="function"?F.checkVisibility({visibilityProperty:!0,contentVisibilityAuto:!0}):!!(F.offsetWidth||F.offsetHeight||F.getClientRects().length),P=F=>{let C=F.ownerDocument.createTreeWalker(F,4),L="";for(let T=C.nextNode();T;T=C.nextNode())T.parentElement&&A(T.parentElement)&&(L+=T.nodeValue??"");return L},_=new Set,j=F=>{for(let C of Array.from(F.options))C.hidden||_.add(I(C.textContent))};E.tagName==="SELECT"&&j(E);for(let F of Array.from(E.querySelectorAll("*")))A(F)&&(F.tagName==="SELECT"?j(F):F.tagName!=="OPTION"&&_.add(I(P(F))));_.add(I(P(E)));for(let F of String(E.innerText??"").split(`
25
- `))_.add(I(F));return[..._]},void 0,y)):new Set,g=$;return c(async()=>{for(let R of w)try{let E=await u(R.first()),I=$.filter(A=>!E.has(A.replace(/\s+/g," ").trim()));if(I.length<g.length&&(g=I),I.length===0)return!0}catch{}return!1},r,`every value of ${t.each} to be shown`).catch(()=>{throw new Error(`${g.map(R=>`"${R}"`).join(", ")} from ${t.each} ${g.length===1?"is":"are"} not shown in the target (${$.length-g.length} of ${$.length} found)`)})}case"expectValue":return c(h(async S=>await S.inputValue(y)===(b??"")),r,`input value "${b}"`);case"expectUrl":{let S=v??b??"";return c(async()=>e.url().includes(S),r,`URL containing "${S}" on the active page (${e.url()})`)}case"handleDialog":{let S=(b??"accept")!=="dismiss",$=t.promptText?m(t.promptText):void 0;e.once("dialog",u=>{S?u.accept($):u.dismiss()});return}case"uploadFile":{let S=(b??"").split(`
26
- `).filter(Boolean).map(u=>aa.isAbsolute(u)?u:aa.resolve(u));if(S.length===0)throw new Error("uploadFile requires a path");let $=vs(S,process.cwd());if($)throw new Error($);if(w.length>0)return Je(w,u=>u.setInputFiles(S,x),!0);Sn.set(e,S);return}case"dragTo":{if(!t.toTarget)throw new Error("dragTo requires toTarget");let S=da(e,t.toTarget,n,!1,!1);return Je(w,$=>$.dragTo(S[0].first(),x),!0)}case"captureText":{if(!t.saveAs)throw new Error("captureText requires saveAs");return c(h(async S=>{let $=js(await S.textContent(y)??"");return $===""?!1:(o.set(t.saveAs,$),!0)}),r,`non-empty text to capture as "${t.saveAs}"`)}case"expectDiffers":{if(!t.compareTo)throw new Error("expectDiffers requires compareTo");let S=o.get(t.compareTo);if(S===void 0)throw new Error(`expectDiffers: no captured value named "${t.compareTo}"`);if(w.length>0)return c(h(async g=>js(await g.textContent(y)??"")!==S),r,`target text to differ from "${t.compareTo}" (${S})`);let $=b??"",u=o.get($);if(u===void 0)throw new Error(`expectDiffers: no captured value named "${$}"`);if(u===S)throw new Error(`expectDiffers: "${$}" (${u}) equals "${t.compareTo}" (${S})`);return}case"expectAttribute":{if(!t.attribute)throw new Error("expectAttribute requires attribute");if(!t.pattern)throw new Error("expectAttribute requires pattern");let S=new RegExp(p(t.pattern),"i");return c(h(async $=>S.test(await $.getAttribute(t.attribute,y)??"")),r,`attribute ${t.attribute} to match /${t.pattern}/i`)}case"expectMatches":{if(!t.pattern)throw new Error("expectMatches requires pattern");let S=new RegExp(p(t.pattern),"i");return c(h(async $=>S.test(js(await $.textContent(y)??""))),r,`target text to match /${t.pattern}/i`)}case"switchTab":{let S=Number.parseInt(b??"",10),$=()=>e.context().pages().filter(g=>!g.isClosed());await c(async()=>$().length>S,r,`tab ${b} to exist (${$().length} open: ${$().map(g=>g.url()).join(", ")})`);let u=$()[S];return await u.bringToFront(),u}case"closeTab":{let S=e.context().pages().filter(g=>!g.isClosed()),$=b===void 0?e:S[Number.parseInt(b,10)];if(!$)throw new Error(`tab ${b} does not exist (${S.length} open)`);await $.close();let u=e.context().pages().filter(g=>!g.isClosed());if(u.length===0)throw new Error("closed the last tab; nothing left to drive");return u[u.length-1]}case"newTab":{let S=v?hn(v):void 0;if(S)throw new Error(S);let $=await e.context().newPage();return v&&await $.goto(ws(v,s),x),$}case"waitForResponse":case"expectResponse":{if(!t.urlPattern)throw new Error(`${t.action} requires urlPattern`);let S=new RegExp(p(t.urlPattern)),$=t.pattern?new RegExp(p(t.pattern)):void 0;if(!i){await e.waitForResponse(async g=>{let R={seq:0,url:g.url(),method:g.request().method(),status:g.status()};return ca(R,S,t.method,t.status,void 0)?$?$.test(await g.text().catch(()=>"")):!0:!1},x);return}let u=()=>t.action==="expectResponse"?i.all():i.sincePreviousStep();return c(async()=>u().some(g=>ca(g,S,t.method,t.status,$)),r,Yp(t,t.urlPattern))}default:throw new Error(`Unknown action type: ${t.action}`)}}var ur=(e,t)=>un(e.actions,t),pa=(e,t)=>Ut(e.actions,t);function Ds(e,t){if(t.steps.length!==e.steps.length)return!1;for(let[n,r]of Object.entries(t.data??{})){let s;try{s=Nt(n)}catch{continue}if(s===r)continue;let o=t.steps.some(a=>ur(a,`{data:${n}}`)),i=t.steps.some(a=>pa(a,r));if(!o||i)return!1}return t.steps.every((n,r)=>{let s=e.steps[r];if(n.gherkin!==s.text||n.kind!==s.kind)return!1;let o=n.table,i=s.table;if(!!o!=!!i)return!1;if(o&&i){let a=Object.keys(fe(o)),c=Object.keys(fe(i));if(a.length>0&&c.length>0){if(JSON.stringify(a)!==JSON.stringify(c))return!1;let l=fe(o),d=fe(i);for(let f of a){if(l[f]===d[f])continue;let m=`<${f}>`,p=t.steps.some(v=>ur(v,m)),b=t.steps.some(v=>pa(v,l[f]));if(!p||b)return!1}}else if(JSON.stringify(o)!==JSON.stringify(i))return!1}if(n.docString!==void 0!=(s.docString!==void 0))return!1;if(n.docString!==void 0&&s.docString!==void 0&&n.docString!==s.docString){let a=t.steps.some(l=>ur(l,"<docstring>")),c=t.steps.some(l=>ur(l,n.docString));if(!a||c)return!1}return!0})}function Ls(e,t){let n=e.steps[t],r=n?.resolvedTable??n?.table;return{...e.params,...it(e.steps),...fe(r),...bt(n?.resolvedDocString??n?.docString)}}async function xn(e,t,n,r={}){let s=r.actionTimeoutMs??5e3,o=r.retries??1,i=Math.max(10,r.pollIntervalMs??fa),a=[],c=r.vars??new Map;r.uniqueSeed&&c.set(Ae,r.uniqueSeed);for(let u of e.context().pages())Sn.delete(u);let l=()=>Object.fromEntries(c),d=n.steps.some(u=>u.actions.some(g=>(g.action==="waitForResponse"||g.action==="expectResponse")&&g.pattern!==void 0)),f=new Ns;f.attach(e.context(),d);let m=!1,p=()=>(m=r.keepResponseLog===!0,m?{responses:f}:{}),b=e,v=new Set(e.context().pages()),k=[],w=u=>k.push(u);e.context().on("page",w);let x=u=>new Promise(g=>{let R=()=>{clearTimeout(E),e.context().off("page",R),g()},E=setTimeout(R,u);e.context().on("page",R)}),y=[],S=!n.steps.some(u=>u.actions.some(g=>g.opensTab))&&n.steps.some(u=>u.actions.some(g=>g.action==="switchTab"||g.action==="closeTab"||g.action==="newTab")),$=()=>{let u=e.context().pages();for(let g of u)v.has(g)||(v.add(g),g.isClosed()||(b=g));if(b.isClosed()){let g=u.filter(R=>!R.isClosed());g.length>0&&(b=g[g.length-1])}};if(!Ds(t,n))return f.detach(),{status:"stale",stepResults:a,captured:l()};try{for(let u=0;u<n.steps.length;u++){let g=n.steps[u],R=Ls(t,u);f.markStep();let E=!1,I=0;r.trace&&await e.context().tracing.group(`[saffron:${r.tracePhase??"replay"}:${u+(r.traceStepOffset??0)}] ${g.keyword} ${g.gherkin}`).catch(()=>{});let A=async()=>{r.trace&&await e.context().tracing.groupEnd().catch(()=>{})};try{if(g.kind==="assertion"&&!g.actions.some(P=>Se.has(P.action))){let P=`Assertion step "${g.gherkin}" recorded ${g.actions.length===0?"nothing":"only setup actions"}, so nothing was checked. Re-record the scenario (saffron run --rerecord) so the agent records what must be true.`;a.push({gherkin:g.gherkin,fromStepSet:g.fromStepSet,kind:g.kind,status:"failed",drift:E,error:P});for(let _=u+1;_<n.steps.length;_++)a.push({gherkin:n.steps[_].gherkin,kind:n.steps[_].kind,status:"skipped"});return{status:"failed",failure:{stepIndex:u,actionIndex:0,stepText:g.gherkin,kind:g.kind,error:P},stepResults:a,captured:l(),...p()}}for(let P=0;P<g.actions.length;P++){let _=g.actions[P];$(),_.pageFingerprint&&await vt(b)!==_.pageFingerprint&&(E=!0);let j,F=!1;for(let C=0;C<=o&&!F;C++){C>0&&(I++,await Ms(250*C),$());try{let L=C>0?`${ve(_)} (attempt ${C+1})`:ve(_),T=await Zi(b,r.trace,`[saffron:action:${P}] ${L}`,()=>gr(b,_,R,s,r.baseURL,c,f,i));T&&(b=T,v.add(T));let O=_.action==="click"||_.action==="press";if(k.length===0&&_.opensTab?await e.context().waitForEvent("page",{timeout:s}).catch(()=>{}):k.length===0&&O&&S&&await x(150),_.opensTab&&k.length===0)throw new Error("this action opened a new tab when it was recorded, and none opened now; the flow has changed");for(k.length>0&&y.push({stepIndex:u,actionIndex:P});k.length>0;)await k.shift().waitForLoadState("domcontentloaded",{timeout:s}).catch(()=>{});F=!0}catch(L){j=L}}if(!F){let C=j instanceof Error?j.message:String(j);a.push({gherkin:g.gherkin,fromStepSet:g.fromStepSet,kind:g.kind,status:"failed",drift:E,...I?{retries:I}:{},failedActionIndex:P,error:C});for(let T=u+1;T<n.steps.length;T++)a.push({gherkin:n.steps[T].gherkin,kind:n.steps[T].kind,status:"skipped"});let L={stepIndex:u,actionIndex:P,stepText:g.gherkin,kind:g.kind,error:C,pageUrl:b.isClosed()?void 0:b.url()};return await r.onFailure?.(b,L).catch(()=>{}),{status:"failed",failure:L,stepResults:a,captured:l(),tabOpeners:y,activePage:b.isClosed()?void 0:b,...p()}}}a.push({gherkin:g.gherkin,fromStepSet:g.fromStepSet,kind:g.kind,status:"passed",drift:E,...I?{retries:I}:{}})}finally{await A()}}return{status:"passed",stepResults:a,captured:l(),tabOpeners:y,activePage:b.isClosed()?void 0:b}}finally{e.context().off("page",w),m||f.detach()}}import{createHash as Xp}from"node:crypto";import ef from"node:path";var Us=e=>Xp("sha1").update(JSON.stringify(e)).digest("hex").slice(0,16);function ua(e){return{steps:e.steps.map(t=>({...t,actions:t.actions.map(({pageFingerprint:n,...r})=>r)})),data:e.data??null}}var ga=e=>Us(e.map(t=>[t.kind,t.text,t.table??null,t.docString??null])),vn=e=>e?Us(ua(e)):"none",ha=(e,t)=>Us({contract:ua(e),featureEdits:t??null});function ma(e,t,n,r){return{scenario:ga(e.steps),committed:vn(t),recording:ha(n,r)}}function $e(e,t,n){let r=t.meta.bound;if(!r)return;let s=[],{feature:o,scenario:i}=t.cache,a;if(n&&!(n instanceof Map))a=n;else try{a=he(ef.join(e,o),e,n).find(l=>l.name===i)}catch(l){s.push(`${o} no longer parses: ${l instanceof Error?l.message.split(`
27
- `)[0]:String(l)}`)}s.length||(a?ga(a.steps)!==r.scenario&&s.push(tf(a,t.cache)):s.push(`scenario "${i}" is no longer in ${o}`));let c;try{c=me(Y(e,o,i))}catch(l){if(!(l instanceof X))throw l;return s.push(`the committed recording cannot be read (${l.problem})`),s.join("; ")}return vn(c)!==r.committed&&s.push(r.committed==="none"?"a recording was committed for this scenario after the proposal was filed, so the diff you would review is not the one that was made":c?"the committed recording changed after the proposal was filed (an accepted heal, a propagated locator fix or a pull), so the diff you would review is not the one that was made":"the committed recording the proposal was diffed against is gone"),ha(t.cache,t.meta.featureEdits)!==r.recording&&s.push("the proposal's recording or feature edits were edited after it was filed"),s.length>0?s.join("; "):void 0}function tf(e,t){let n=e.steps,r=t.steps;if(n.length!==r.length)return`the scenario has ${n.length} steps now and had ${r.length} when the proposal was filed`;let s=n.findIndex((o,i)=>o.text!==r[i].gherkin||o.kind!==r[i].kind);return s<0?"the scenario's steps changed since the proposal was filed":`step ${s+1} now reads "${n[s].text}" and was "${r[s].gherkin}" when the proposal was filed`}var hr="not bound: filed by a runner older than 0.8.5, so changes since cannot be detected";import $t from"node:fs";import wa from"node:path";function mr(e){return`$${e.toFixed(e>=1?2:4)}`}function Bs(e,t){let n=e===void 0?"?":`${Math.round(e)}%`,r=t===void 0?"?":`${Math.round(t)}%`;return`${n} \u2192 ${r}`}function ke(e){return e.costUsd>0||e.aiCalls===0}function zs(e){return ke(e)?e.plan?` \xB7 5-hour window ${Bs(e.plan.fiveHourBefore,e.plan.fiveHourAfter)} (${e.plan.subscriptionType} plan) \xB7 \u2248 ${mr(e.costUsd)} at API rates`:` \xB7 ${mr(e.costUsd)}`:" \xB7 cost not reported by the provider"}function ya(e,t){return e?t?`\u2248 ${mr(e)}`:mr(e):""}function ba(e){if(!e)return"";let t=new Date(e);return Number.isNaN(t.getTime())?"":`resets ${t.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}`}function qs(e){let t=e.costUsd===void 0?"":` \xB7 cost ${e.costUsd>=0?"+":"-"}$${Math.abs(e.costUsd).toFixed(4)}`;return`vs previous run: pass rate ${e.passRatePp>=0?"+":""}${e.passRatePp}pp${t} \xB7 AI calls ${e.aiCalls>=0?"+":""}${e.aiCalls}`}function Js(e){return wa.join(e,".saffron","history.jsonl")}function Gs(e,t,n,r){let s=e.map(o=>({feature:o.scenario.featurePath,scenario:o.scenario.displayName,tags:[...o.scenario.tags],status:o.status,source:o.source,durationMs:o.durationMs,verified:o.verified,seededSteps:o.seededSteps,adaptations:o.adaptations.length,driftSteps:o.stepResults.filter(i=>i.drift).length,aiCalls:o.usage.aiCalls,tokensInOut:o.usage.inputTokens+o.usage.outputTokens,cacheRead:o.usage.cacheReadTokens,cacheWrite:o.usage.cacheCreationTokens,costUsd:o.usage.costUsd??0,error:o.error?.split(`
28
- `)[0]}));return{runId:t.toISOString(),startedAt:t.toISOString(),finishedAt:new Date().toISOString(),baseURL:r,green:s.filter(o=>o.status==="green").length,yellow:s.filter(o=>o.status==="yellow").length,red:s.filter(o=>o.status==="red").length,aiCalls:s.reduce((o,i)=>o+i.aiCalls,0),costUsd:s.reduce((o,i)=>o+i.costUsd,0),divergentSteps:n,scenarios:s}}function Ws(e,t){let n=Js(e);$t.mkdirSync(wa.dirname(n),{recursive:!0}),$t.appendFileSync(n,`${JSON.stringify(t)}
29
- `)}function Sa(e,t){let n=Js(e);if(!$t.existsSync(n))return Ws(e,t);let r=$t.readFileSync(n,"utf8").split(`
30
- `).filter(i=>i.trim()),s=!1,o=r.map(i=>{try{if(JSON.parse(i).runId===t.runId)return s=!0,JSON.stringify(t)}catch{}return i});s||o.push(JSON.stringify(t)),$t.writeFileSync(n,`${o.join(`
17
+ [Saffron cut this result at ${t} of ${s.text.length} characters: whole, it would not fit the agent's context. Ask for less: a snapshot of one region, a filtered request list, or a saffron_assert on the field you need instead of the whole body.]`,i=/[\uD800-\uDBFF]/.test(s.text[t-1])?t-1:t;return{...s,text:s.text.slice(0,i)+o}});return n?{...e,content:r}:e}async function Wi(e,t){let n=new $p({name:"saffron-cap",version:"1"}),r=Object.fromEntries(Object.entries(process.env).filter(o=>o[1]!==void 0));await n.connect(new kp({...e,env:r,stderr:"ignore"}));let s=Ps(n,void 0,t);return{config:{type:"sdk",name:"playwright",instance:s},close:async()=>{await Promise.allSettled([n.close(),s.close()])}}}function Ps(e,t=(r,s,o)=>o(),n){let r=new Tp({name:"playwright",version:"1"},{capabilities:{tools:{}}});return r.server.setRequestHandler(Ep,async()=>Ap(await e.listTools())),r.server.setRequestHandler(Rp,async(s,o)=>{let i=_p(s.params.name);if(i)return i;let a=Cp(s.params.name,s.params.arguments,n);if(a.refused)return a.refused;let c={...s.params,arguments:a.args};return Op(await t(c.name,c.arguments,()=>e.callTool(c,void 0,{timeout:_s,signal:o.signal})))}),r}import Sn from"node:fs";import Lp from"node:os";import Cs from"node:path";import be from"node:fs";import jp from"node:path";import{createHash as Np,randomBytes as Fp}from"node:crypto";import br from"node:zlib";var dt=4294967295,Vi=67324752,Ki=33639248,Yi=101010256;function Qi(e){return vn(e).map(t=>({name:t.name,data:t.read()}))}function Zi(e,t){let n=Math.max(0,t-22-65535),r=e(n,t-n),s=-1;for(let l=r.length-22;l>=0;l--)if(r.readUInt32LE(l)===Yi){s=l;break}if(s<0)throw new Error("not a zip file: no central directory");let o=r.readUInt16LE(s+10),i=e(r.readUInt32LE(s+16),r.readUInt32LE(s+12)),a=0,c=[];for(let l=0;l<o;l++){if(i.readUInt32LE(a)!==Ki)throw new Error("not a zip file: bad central directory entry");let d=i.readUInt16LE(a+10),f=i.readUInt32LE(a+16),m=i.readUInt32LE(a+20),p=i.readUInt32LE(a+24),b=i.readUInt16LE(a+28),$=i.readUInt16LE(a+30),k=i.readUInt16LE(a+32),w=i.readUInt32LE(a+42),x=i.subarray(a+46,a+46+b).toString("utf8");a+=46+b+$+k;let y=e(w,30);if(y.length<30||y.readUInt32LE(0)!==Vi)throw new Error(`not a zip file: bad local header for ${x}`);let u=w+30+y.readUInt16LE(26)+y.readUInt16LE(28);if(!x.endsWith("/")){if(d!==0&&d!==8)throw new Error(`zip entry ${x} uses compression method ${d}, which is not supported`);c.push({name:x,method:d,crc:f,size:p,compressedSize:m,start:u})}}return c}function vn(e){return Zi((t,n)=>e.subarray(t,t+n),e.length).map(({name:t,method:n,crc:r,size:s,compressedSize:o,start:i})=>{let a=e.subarray(i,i+o);return{name:t,method:n,crc:r,size:s,raw:a,read:()=>n===0?Buffer.from(a):br.inflateRawSync(a)}})}function Mp(e,t,n){let r=be.openSync(e,"r");try{let s=Buffer.alloc(n),o=0;for(;o<n;){let i=be.readSync(r,s,o,n-o,t+o);if(i===0)break;o+=i}return s.subarray(0,o)}finally{be.closeSync(r)}}function Xi(e){let t=be.statSync(e).size,n=(r,s)=>Mp(e,r,Math.max(0,Math.min(s,t-r)));return Zi(n,t).map(r=>({...r,raw:()=>n(r.start,r.compressedSize)}))}var Dp=(()=>{let e=new Uint32Array(256);for(let t=0;t<256;t++){let n=t;for(let r=0;r<8;r++)n=n&1?3988292384^n>>>1:n>>>1;e[t]=n>>>0}return e})();function yr(e,t=0){let n=br.crc32;if(n)return n(e,t)>>>0;let r=(t^4294967295)>>>0;for(let s of e)r=Dp[(r^s)&255]^r>>>8;return(r^4294967295)>>>0}var Gi=1024*1024;function ea(e,t){let n=be.openSync(e,"r");try{let r=Buffer.alloc(Gi);for(let s;(s=be.readSync(n,r,0,Gi,null))>0;)t(r.subarray(0,s))}finally{be.closeSync(n)}}function wr(e){let t=Np("sha256"),n=0,r=0;return ea(e,s=>{n=yr(s,n),r+=s.length,t.update(s)}),{crc:n,size:r,sha256:t.digest("hex")}}function ta(e,t,n,r,s,o){let i=Buffer.alloc(30);i.writeUInt32LE(Vi,0),i.writeUInt16LE(20,4),i.writeUInt16LE(2048,6),i.writeUInt16LE(t,8),i.writeUInt32LE(0,10),i.writeUInt32LE(n,14),i.writeUInt32LE(r,18),i.writeUInt32LE(s,22),i.writeUInt16LE(e.length,26),i.writeUInt16LE(0,28);let a=Buffer.alloc(46);return a.writeUInt32LE(Ki,0),a.writeUInt16LE(20,4),a.writeUInt16LE(20,6),a.writeUInt16LE(2048,8),a.writeUInt16LE(t,10),a.writeUInt32LE(0,12),a.writeUInt32LE(n,16),a.writeUInt32LE(r,20),a.writeUInt32LE(s,24),a.writeUInt16LE(e.length,28),a.writeUInt16LE(0,30),a.writeUInt16LE(0,32),a.writeUInt16LE(0,34),a.writeUInt16LE(0,36),a.writeUInt32LE(0,38),a.writeUInt32LE(o,42),{local:i,central:a}}function na(e,t,n){let r=Buffer.alloc(22);return r.writeUInt32LE(Yi,0),r.writeUInt16LE(0,4),r.writeUInt16LE(0,6),r.writeUInt16LE(e,8),r.writeUInt16LE(e,10),r.writeUInt32LE(t,12),r.writeUInt32LE(n,16),r.writeUInt16LE(0,20),r}var ra=e=>new Error(`cannot write a zip of ${e} files: one holds at most 65,535`),sa=e=>new Error(`cannot write ${e} into a zip: a zip holds at most 4 GB, and this one would be larger`),oa=()=>new Error("cannot write the zip: a zip holds at most 4 GB, and this one would be larger");function ia(e){if(e.length>65535)throw ra(e.length);let t=[],n=[],r=0;for(let o of e){let i="raw"in o,a=i?o.raw:br.deflateRawSync(o.data),c=i?o.size:o.data.length,l=Buffer.from(o.name,"utf8");if(c>dt||r+30+l.length+a.length>dt)throw sa(o.name);let{local:d,central:f}=ta(l,i?o.method:8,i?o.crc:yr(o.data),a.length,c,r);t.push(d,l,a),n.push(f,l),r+=d.length+l.length+a.length}let s=n.reduce((o,i)=>o+i.length,0);if(r+s>dt)throw oa();return Buffer.concat([...t,...n,na(e.length,s,r)])}function Yt(e,t){for(let n=0;n<t.length;)n+=be.writeSync(e,t,n,t.length-n)}function aa(e,t){if(t.length>65535)throw ra(t.length);be.mkdirSync(jp.dirname(e),{recursive:!0});let n=`${e}.${process.pid}-${Fp(4).toString("hex")}.partial`,r=be.openSync(n,"w");try{let s=[],o=0;for(let a of t){let c=Buffer.from(a.name,"utf8"),l="file"in a,d=l&&(a.crc===void 0||a.size===void 0)?wr(a.file):void 0,f=l?void 0:br.deflateRawSync(a.data),m=l?d?.crc??a.crc:yr(a.data),p=l?d?.size??a.size:a.data.length,b=f?.length??p;if(p>dt||o+30+c.length+b>dt)throw sa(a.name);let{local:$,central:k}=ta(c,l?0:8,m,b,p,o);if(Yt(r,$),Yt(r,c),f)Yt(r,f);else{let w=0,x=0;if(ea(a.file,y=>{w+y.length>p||(Yt(r,y),w+=y.length,x=yr(y,x))}),w!==p||x!==m)throw new Error(`${a.name} changed while it was being packed`)}s.push(k,c),o+=$.length+c.length+b}let i=s.reduce((a,c)=>a+c.length,0);if(o+i>dt)throw oa();for(let a of s)Yt(r,a);Yt(r,na(t.length,i,o)),be.closeSync(r),be.renameSync(n,e)}catch(s){try{be.closeSync(r)}catch{}throw be.rmSync(n,{force:!0}),s}}var Os=["off","on","retain-on-failure"],js="trace.zip";async function Ns(e,t){await e.tracing.start({screenshots:!0,snapshots:!0,sources:!1,title:t})}async function Fs(e,t){t&&Sn.mkdirSync(Cs.dirname(t),{recursive:!0}),await e.tracing.stop(t?{path:t}:void 0)}async function Ms(e,t,n){let r;try{r=Sn.mkdtempSync(Cs.join(Lp.tmpdir(),"saffron-trace-"));let s=Cs.join(r,js);await e.tracing.stop({path:s});let o=Qp(Sn.readFileSync(s),n);return Ae(t,o),je(o)}catch{try{Sn.rmSync(t,{force:!0})}catch{console.warn("Could not remove the execution trace artifact.")}console.warn("Could not save the execution trace; no trace was attached to this run.");return}finally{if(r)try{Sn.rmSync(r,{recursive:!0,force:!0})}catch{console.warn("Could not remove the private temporary trace directory.")}}}async function la(e,t,n,r){if(!t)return r();let s=e.context().tracing;await s.group(n).catch(()=>{});try{return await r()}finally{await s.groupEnd().catch(()=>{})}}var Up=new Set(["callId","parentId","pageId","frameId","contextId","pageref","_frameref","snapshotName","beforeSnapshot","afterSnapshot","inputSnapshot","sha1","_sha1"]),Bp=new Set(["before","action","after"]),zp=/^\[saffron:(?:agent|replay|pending|action):-?\d+\] /,Hp=new Set(["type","class","method"]),qp=new Set(["__playwright_bounding_rect__","__playwright_scroll_top_","__playwright_scroll_left_","__playwright_target__","__playwright_checked_","__playwright_selected_","__playwright_popover_open_","__playwright_dialog_open_","__playwright_shadow_root_","__playwright_custom_elements__"]),Jp=new Set(["style","__playwright_style_sheet_"]),Wp=new Set(["srcset","imagesrcset"]),Gp=/^(?:image\/(?!svg)|font\/|audio\/|video\/)/i,Vp=/\.(?:jpe?g|png|gif|webp|avif|bmp|ico|woff2?|ttf|otf|eot|mp3|mp4|m4a|wav|ogg|webm)$/i,Kp=/^text\/css\b/i,Yp=/\.css$/i;function Qp(e,t){let n=bt(t,{encodings:!0});if(n.empty)return e;let r=bt(t,{encodings:!0,bytes:!0}),s=vn(e),o=new Set(s.filter(p=>p.name.includes("/")).map(p=>p.name));for(let p of s)p.name.startsWith("resources/")&&o.add(p.name.slice(10));let i={names:o,pieces:lf(t)},a=new Set,c=new Set,l=p=>{for(let b of[p.snapshot?.response?.content,p.snapshot?.request?.postData]){if(!b)continue;let $=b._file??(b._sha1&&`resources/${b._sha1}`);if(!$)continue;let k=String(b.mimeType??"");Gp.test(k)?a.add($):Kp.test(k)&&c.add($)}},d=[...s],f=!1,m=(p,b)=>{b&&(d[p]={name:s[p].name,data:b},f=!0)};return s.forEach((p,b)=>{/\.(?:trace|network)$/.test(p.name)?m(b,Zp(p.read(),n,i,p.name.endsWith(".network")?l:void 0)):p.name.endsWith(".stacks")&&m(b,Xp(p.read(),n,i))}),s.forEach((p,b)=>{/\.(?:trace|network|stacks)$/.test(p.name)||a.has(p.name)||Vp.test(p.name)||m(b,ef(p.read(),n,r,c.has(p.name)||Yp.test(p.name)))}),f?ia(d):e}function Zp(e,t,n,r){let s=e.toString("utf8").split(`
18
+ `),o=!1;for(let i=0;i<s.length;i++){let a=s[i];if(!a.trim())continue;let c=JSON.parse(a);if(r?.(c),!t.occursIn(a)&&!t.hiddenByEscape.test(a))continue;let l=Ls(a,t,{keys:!1,numbers:!1,playwright:n});l!==a&&(s[i]=l,o=!0)}return o?Buffer.from(s.join(`
19
+ `),"utf8"):void 0}function Xp(e,t,n){let r=e.toString("utf8");if(JSON.parse(r),!t.occursIn(r)&&!t.hiddenByEscape.test(r))return;let s=Ls(r,t,{keys:!1,numbers:!1,playwright:n});return s===r?void 0:Buffer.from(s,"utf8")}function ef(e,t,n,r=!1){if(r){let d=e.toString("latin1"),f=ua(d,n);return f===d?void 0:Buffer.from(f,"latin1")}let s=e.toString("utf8"),o=s.startsWith("\uFEFF")?"\uFEFF":"",i=s.slice(o.length);if(!s.includes("\uFFFD")||Buffer.from(s,"utf8").equals(e)){let d=/^\s*[[{"]/.test(i)?da(i,t):void 0,f=d===void 0&&/^\s*(?:[[{"\d-]|true|false|null)/.test(i)&&i.trim().includes(`
20
+ `)?tf(i,t):void 0,m=d??f;if(m!==void 0)return m===i?void 0:Buffer.from(o+m,"utf8")}let c=e.toString("latin1"),l=n.mask(c);return l===c?void 0:Buffer.from(l,"latin1")}function da(e,t){try{JSON.parse(e)}catch(n){if(n instanceof SyntaxError)return;throw n}return!t.occursIn(e)&&!t.hiddenByEscape.test(e)?e:Ls(e,t,{keys:!0,numbers:!0})}function tf(e,t){let n=e.split(`
21
+ `),r=[];for(let s of n){if(!s.trim()){r.push(s);continue}let i=da(s,t);if(i===void 0)return;r.push(i)}return r.join(`
22
+ `)}var pa=/^([a-z][a-z0-9+.-]*:\/\/)([^/?#]*@)?([^/?#]*)/i;function fa(e,t=0,n=e.length){let r=pa.exec(e.slice(t,n));if(!r)return;let[s,o,i=""]=r,a=t+o.length+i.length;return c=>c>=t&&c<t+o.length||c>=a-(i?1:0)&&c<=t+s.length&&c<n}function nf(e,t,n){let r="",s=0;for(let o of t.matches(e)){let{token:i,kept:a=()=>!1}=n(o),c="";for(let l=o.start;l<o.end;){if(a(l)){c+=e[l++];continue}for(;l<o.end&&!a(l);)l++;c+=i(o.names)}c!==e.slice(o.start,o.end)&&(r+=e.slice(s,o.start)+c,s=o.end)}return s===0?e:r+e.slice(s)}var rf=e=>e.map(t=>`%7Benv:${t}%7D`).join(""),sf=e=>e.map(t=>`\\{env\\:${t}\\}`).join("");function Ds(e,t,n,r){let s=n.map(([i,a])=>({start:i,end:a,kept:fa(e,i,a)})),o=0;return nf(e,t,i=>{for(;o<s.length&&s[o].end<i.end;)o++;let a=s[o];return a&&a.start<=i.start&&i.end<=a.end?{token:rf,kept:a.kept}:{token:r}})}function of(e,t){return fa(e)?Ds(e,t,[[0,e.length]],mn):t.mask(e)}var af=/url\(\s*(?:"([^"]*)"|'([^']*)'|([^)\s'"]*))|@import\s+(?:"([^"]*)"|'([^']*)')/dgi;function ua(e,t){if(!t.occursIn(e))return e;let n=[...e.matchAll(af)].map(r=>r.indices.slice(1).find(s=>s!==void 0));return Ds(e,t,n,sf)}function cf(e,t){let n=[];for(let r=0;r<e.length;){for(;r<e.length&&/[\s,]/.test(e[r]);)r++;let s=r;for(;s<e.length&&!/\s/.test(e[s]);)s++;let o=s;for(;o>r&&e[o-1]===",";)o--;if(o>r&&n.push([r,o]),r=s,o===s)for(;r<e.length&&e[r]!==",";)r++}return Ds(e,t,n,mn)}function lf(e){let t=new Map;for(let[n,r]of e)if(!(r.length<yt||!pa.test(r)||!URL.canParse(r)))for(let s of new URL(r).searchParams.values())s.length>=yt&&!t.has(s)&&t.set(s,n);return t}var ca=e=>e===" "||e===" "||e===`
23
+ `||e==="\r";function Ls(e,t,n){let r=[],s=0,o,i=[],a=c=>{let l=c-1;for(;l>=0&&ca(e[l]);)l--;return e[l]};for(let c=0;c<e.length;c++){let l=e[c];if(l==="{"||l==="["){let d=i.at(-1),f=a(c)===":"?o:void 0;i.push({dom:(d?.dom??!1)||f==="html",logged:(d?.logged??!1)||f==="value"});continue}if(l==="}"||l==="]"){i.pop();continue}if(l==='"'){let d=c++,f=!1;for(;c<e.length&&e[c]!=='"';c++)e[c]==="\\"&&(f=!0,c++);let m=c+1,p=m;for(;p<e.length&&ca(e[p]);)p++;let b=e[p]===":",$=f?JSON.parse(e.slice(d,m)):e.slice(d+1,c),k=i.at(-1),w=!b&&a(d)===":"?o:void 0,x=v=>t.mask(v);if(b){if(o=$,!n.keys)continue}else if(n.playwright){if(i.length===1&&w!==void 0&&Hp.has(w)||n.playwright.names.has($))continue;let{pieces:v}=n.playwright;if(x=S=>v.has(S)?mn([v.get(S)]):of(S,t),k?.dom){if(a(d)==="["){k.tag=$.toUpperCase();continue}let S=w===void 0?k.tag:i.at(-2)?.tag;if(w!==void 0&&qp.has(w)||w==="src"&&(S==="IFRAME"||S==="FRAME"))continue;w===void 0&&S==="STYLE"||w!==void 0&&Jp.has(w)?x=h=>ua(h,t):w!==void 0&&Wp.has(w)&&(x=h=>cf(h,t))}else if(w!==void 0&&Up.has(w)||w==="phase"&&Bp.has($))continue}let y=n.playwright&&i.length===1&&w==="title"?zp.exec($)?.[0]??"":"",u=y+x($.slice(y.length));u!==$&&(r.push(e.slice(s,d),JSON.stringify(u)),s=m)}else if(l==="-"||l>="0"&&l<="9"){let d=c+1;for(;d<e.length&&/[0-9eE.+-]/.test(e[d]);)d++;(n.numbers||(i.at(-1)?.logged??!1)||a(c)===":"&&o==="value")&&t.isValue(e.slice(c,d))&&(r.push(e.slice(s,c),JSON.stringify(t.mask(e.slice(c,d)))),s=d),c=d-1}}return r.length?r.join("")+e.slice(s):e}function $n(e){return Array.isArray(e)?e.map($n):e&&typeof e=="object"?Object.fromEntries(Object.entries(e).filter(([t,n])=>n!==void 0&&t!=="pageFingerprint").sort(([t],[n])=>t.localeCompare(n)).map(([t,n])=>[t,$n(n)])):e}var Us=e=>JSON.stringify($n(e));function Bs(e,t="",n={}){if(Array.isArray(e))n[t]=e.map(r=>String(r)).join(", ");else if(e&&typeof e=="object")for(let[r,s]of Object.entries(e))Bs(s,t?`${t}.${r}`:r,n);else t&&(n[t]=String(e));return n}function df(e,t){let n=Bs($n(e)),r=Bs($n(t)),s=[...new Set([...Object.keys(n),...Object.keys(r)])].sort(),o=[];for(let i of s){if(i==="action"||n[i]===r[i])continue;let a=i.replace(/^target\./,"").replace(/^toTarget\./,"destination ");n[i]===void 0?o.push(`${a}: added "${r[i]}"`):r[i]===void 0?o.push(`${a}: removed "${n[i]}"`):o.push(`${a}: "${n[i]}" \u2192 "${r[i]}"`)}return o}function ya(e,t,n){let r=e.map(n),s=t.map(n),o=Array.from({length:r.length+1},()=>new Array(s.length+1).fill(0));for(let l=r.length-1;l>=0;l--)for(let d=s.length-1;d>=0;d--)o[l][d]=r[l]===s[d]?o[l+1][d+1]+1:Math.max(o[l+1][d],o[l][d+1]);let i=[],a=0,c=0;for(;a<r.length&&c<s.length;)r[a]===s[c]?(i.push({before:e[a],after:t[c]}),a++,c++):o[a+1][c]>=o[a][c+1]?(i.push({before:e[a]}),a++):(i.push({after:t[c]}),c++);for(;a<r.length;a++)i.push({before:e[a]});for(;c<s.length;c++)i.push({after:t[c]});return i}function ga(e){if(!e)return"";let t=[];if(e.role){let n=e.name?` "${e.name}"`:e.nameRegex?` matching /${e.nameRegex}/`:"";t.push(`${e.role}${n}`)}else e.label?t.push(`label "${e.label}"`):e.text?t.push(`text "${e.text}"`):e.placeholder?t.push(`placeholder "${e.placeholder}"`):e.testId&&t.push(`test id ${e.testId}`);return e.fallbackSelectors?.length&&t.push(`[${e.fallbackSelectors.join(", ")}]`),e.frame&&t.push(`inside frame ${e.frame}`),t.join(" ")}var ha=e=>`${e.method??"any"} /${e.urlPattern}/ ${e.status??"2xx"}`+(e.pattern?` with a body matching /${e.pattern}/`:"");function $e(e){let t=ga(e.target),n=e.value??"",r=(()=>{switch(e.action){case"goto":return`go to ${e.url}`;case"click":return`click ${t}`;case"fill":return`fill ${t} with "${n}"`;case"press":return t?`press ${n} on ${t}`:`press ${n}`;case"selectOption":return`select "${n}" in ${t}`;case"check":return`check ${t}`;case"uncheck":return`uncheck ${t}`;case"hover":return`hover ${t}`;case"waitFor":return t?`wait for ${t}`:`wait ${n}ms`;case"captureText":return`remember ${t} as "${e.saveAs}"`;case"handleDialog":return`${n} the dialog${e.promptText?` with "${e.promptText}"`:""}`;case"uploadFile":return`upload ${n}`;case"dragTo":return`drag ${t} onto ${ga(e.toTarget)}`;case"waitForResponse":return`wait for ${ha(e)}`;case"switchTab":return`switch to tab ${n}`;case"closeTab":return`close tab ${n||"(the current one)"}`;case"newTab":return`open a new tab${e.url?` at ${e.url}`:""}`;case"expectVisible":return`expect ${t} to be visible`;case"expectNotVisible":return`expect ${t} to be gone`;case"expectText":return`expect ${t} to contain "${n}"`;case"expectUrl":return`expect the URL to contain "${e.url??n}"`;case"expectValue":return`expect ${t} to hold "${n}"`;case"expectDiffers":return`expect ${t||`"${n}"`} to differ from "${e.compareTo}"`;case"expectMatches":return`expect ${t} to match /${e.pattern}/`;case"expectAttribute":return`expect ${t} ${e.attribute} to match /${e.pattern}/`;case"expectResponse":return`expect a response ${ha(e)}`;case"expectEach":return`expect ${t} to show every value of ${e.each}`;default:return e.action}})();return e.opensTab?`${r}, opening a tab`:r}var xn=e=>Se.has(e.action);function pf(e,t){let n=ya(e,t,Us),r=[];for(let s=0;s<n.length;s++){let o=n[s];if(o.before&&o.after){r.push({change:"same",before:$e(o.before),after:$e(o.after),assertion:xn(o.after)});continue}let i=n[s+1];if(o.before&&i?.after&&!i.before){r.push({change:"changed",before:$e(o.before),after:$e(i.after),details:df(o.before,i.after),assertion:xn(o.before)||xn(i.after)}),s++;continue}o.before&&r.push({change:"removed",before:$e(o.before),assertion:xn(o.before)}),o.after&&r.push({change:"added",after:$e(o.after),assertion:xn(o.after)})}return r}var ff=e=>`${e.kind}\0${e.gherkin}`;function pt(e,t){let n=ya(e?.steps??[],t.steps,ff),r=[],s={added:0,removed:0,changed:0,assertions:0};for(let o of n){let i=o.after??o.before,a=pf(o.before?.actions??[],o.after?.actions??[]);for(let l of a)l.change==="added"?s.added++:l.change==="removed"?s.removed++:l.change==="changed"&&s.changed++,l.change!=="same"&&l.assertion&&s.assertions++;let c=e&&o.before?o.after?a.some(l=>l.change!=="same")?"changed":"same":"removed":"added";r.push({gherkin:i.gherkin,kind:i.kind,fromStepSet:i.fromStepSet,change:c,actions:a})}return{feature:t.feature,scenario:t.scenario,hasCommitted:e!==void 0,steps:r,summary:s}}var ma=new Set(["goto","click","press","newTab","switchTab","closeTab"]);function ba(e,t){let n=[],r=s=>`${s} navigation action${s===1?"":"s"}`;for(let s=0;s<Math.min(e.length,t.length);s++){let o=e[s].actions.filter(f=>ma.has(f.action)),a=[...t[s].actions.filter(f=>ma.has(f.action))],c=[];for(let f of o){let m=a.findIndex(p=>Us(p)===Us(f));m>=0?a.splice(m,1):c.push(f)}let l=a;for(let f=c.length-1;f>=0;f--){let m=l.findIndex(p=>p.action===c[f].action);m>=0&&(l.splice(m,1),c.splice(f,1))}let d=`step ${s+1} ("${e[s].gherkin}")`;c.length>0&&n.push(`flow change: ${d} no longer performs ${r(c.length)} from the committed recording: ${c.map($e).join("; ")}. A page or navigation step may have been removed or merged.`),l.length>0&&n.push(`flow change: ${d} now performs ${r(l.length)} the committed recording did not: ${l.map($e).join("; ")}. A page or navigation step may have been added or reordered.`)}return n}function vr(e){let t=[];t.push(`${e.feature} \u203A ${e.scenario}`),t.push(e.hasCommitted?"Comparing the committed recording with the proposed one.":"First recording for this scenario: there is nothing to compare against."),t.push("");for(let i of e.steps){let a=i.change==="removed"?"- ":i.change==="added"&&e.hasCommitted?"+ ":" ";t.push(`${a}${i.kind==="assertion"?"[assertion] ":""}${i.gherkin}`+(i.fromStepSet?` (from step set "${i.fromStepSet}")`:""));for(let c of i.actions)if(c.change==="same")t.push(` ${c.before}`);else if(c.change==="added")t.push(` + ${c.after}`);else if(c.change==="removed")t.push(` - ${c.before}`);else{t.push(` - ${c.before}`),t.push(` + ${c.after}`);for(let l of c.details??[])t.push(` ${l}`)}t.push("")}let{added:n,removed:r,changed:s,assertions:o}=e.summary;return t.push(n+r+s===0?"No action changed.":`${s} changed, ${n} added, ${r} removed.`+(o>0?` ${o} of them ${o===1?"is an assertion":"are assertions"}: read those first.`:"")),t.join(`
24
+ `)}var Js=e=>new Promise(t=>setTimeout(t,e)),Hs=class{entries=[];seq=0;prevStepSeq=0;curStepSeq=0;listener;context;attach(t,n){this.context=t,this.listener=r=>{let s={seq:++this.seq,url:r.url(),method:r.request().method(),status:r.status()};if(this.entries.push(s),n){let o=r.headers()["content-type"]??"";/json|text|xml/i.test(o)&&r.text().then(i=>{s.body=i.length>262144?i.slice(0,262144):i}).catch(()=>{})}},t.on("response",this.listener)}detach(){this.context&&this.listener&&this.context.off("response",this.listener)}markStep(){this.prevStepSeq=this.curStepSeq,this.curStepSeq=this.seq}sincePreviousStep(){return this.entries.filter(t=>t.seq>this.prevStepSeq)}all(){return this.entries}};function uf(e,t){if(!t)return e>=200&&e<300;let n=/^([1-5])xx$/i.exec(t.trim());if(n){let r=Number(n[1])*100;return e>=r&&e<r+100}return e===Number(t.trim())}function va(e,t,n,r,s){return!(!t.test(e.url)||n&&e.method.toUpperCase()!==n.toUpperCase()||!uf(e.status,r)||s&&!s.test(e.body??""))}var gf=(e,t)=>`response matching ${e.method??"any-method"} /${t}/ status ${e.status??"2xx"}${e.pattern?` body /${e.pattern}/`:""}`,Sa=new Set(["click","fill","press","selectOption","check","uncheck","hover","dragTo","uploadFile"]),hf=/(?<=(?:^|[^\\])(?:\\\\)*)(\\?)(<[^<>]+>|\{(?:env|data|date)[^{}]*\})/g;function qs(e,t,n,r=!1){return e.replace(hf,(s,o,i,a)=>{let c=e.slice(Math.max(0,a-2),a);if(i.startsWith("<")&&(c==="(?"||c==="\\k"))return s;let l=t(i);return l===i?s:(r?"":o)+n(l)})}function xa(e,t,n,r=!1,s=!0){let o=c=>hn(gn(Rs(wt(c,n)))),i=[],a=c=>{if(t.role){let l=t.nameRegex?new RegExp(qs(t.nameRegex,o,ae.pattern,!0),"i"):t.name?o(t.name):void 0,d=t.role;l===void 0&&!!(t.label||t.placeholder||t.text||t.testId||t.fallbackSelectors?.length)?t.text&&i.push(c.getByRole(d).filter({hasText:o(t.text)})):typeof l=="string"?(i.push(c.getByRole(d,{name:l,exact:!0})),s&&i.push(c.getByRole(d,{name:l,exact:!1}))):i.push(c.getByRole(d,{name:l,exact:!1}))}t.label&&i.push(c.getByLabel(o(t.label))),t.placeholder&&i.push(c.getByPlaceholder(o(t.placeholder))),t.text&&!t.role&&i.push(c.getByText(o(t.text))),t.testId&&i.push(c.getByTestId(o(t.testId)));for(let l of t.fallbackSelectors??[])i.push(c.locator(Lt(l,(d,f)=>qs(d,o,ae[f],f==="regex"),o)))};if(t.frame)a(e.frameLocator(o(t.frame)));else if(a(e),r)for(let c of e.frames())c!==e.mainFrame()&&a(c);if(i.length===0)throw new Error("Action target has neither role nor fallback selectors");return i}var ka=100;async function mf(e,t,n,r){let s=Date.now()+t,o;for(;Date.now()<s;){try{if(await e())return}catch(i){o=i}await Js(r)}throw new Error(`Timed out waiting for ${n}${o?` (${String(o)})`:""}`)}async function We(e,t,n=!1){let r;for(let s of e)try{await t(s.first());return}catch(o){if(r=o,n&&await s.first().count().catch(()=>0)>0)throw o}throw r}var zs=e=>e.replace(/\s+/g," ").trim(),kn=new WeakMap;async function xr(e,t,n,r,s,o=new Map,i,a=ka){let c=(v,S,h)=>mf(v,S,h,a),l=kn.get(e);if(l&&Sa.has(t.action)&&t.action!=="uploadFile"){kn.delete(e);let v,S=g=>{v??=g};e.on("filechooser",S);let h;try{if(h=await xr(e,t,n,r,s,o,i,a),v||await e.waitForEvent("filechooser",{timeout:r}).then(S,()=>{}),!v)throw new Error(`uploadFile failed: the ${t.action} after it opened no file chooser within ${r} ms`);try{await v.setFiles(l,{timeout:r})}catch(g){throw new Error(`uploadFile failed: ${g instanceof Error?g.message:String(g)}`)}}catch(g){throw kn.set(e,l),g}finally{e.off("filechooser",S)}return h}o.has(_e)||o.set(_e,$t());let d=o.get(_e),f=v=>typeof v=="string"?Vt(v,d):Array.isArray(v)?v.map(f):v&&typeof v=="object"?Object.fromEntries(Object.entries(v).map(([S,h])=>[S,f(h)])):v;t=f(t),n=f(n);let m=v=>hn(gn(Rs(wt(v,n)))),p=v=>qs(v,m,ae.pattern,!0),b=t.value!==void 0?m(t.value):void 0,$=t.url!==void 0?m(t.url):void 0,k=t.action==="expectVisible"||t.action==="expectText"||t.action==="expectValue"||t.action==="expectMatches"||t.action==="expectAttribute"||t.action==="expectEach"||t.action==="captureText",w=t.target?xa(e,t.target,n,k,!Sa.has(t.action)):[],x={timeout:r},y={timeout:Math.min(1e3,r)},u=v=>async()=>{for(let S of w)try{if(await v(S.first()))return!0}catch{}return!1};switch(t.action){case"goto":{if(!$)throw new Error("goto requires url");let v=wn($);if(v)throw new Error(v);await e.goto(Es($,s),x);return}case"click":return We(w,v=>v.click(x),!0);case"fill":return We(w,v=>v.fill(b??"",x),!0);case"press":return w.length>0?We(w,v=>v.press(b??"",x),!0):e.keyboard.press(b??"");case"selectOption":return We(w,async v=>{await v.selectOption(b??"",x)},!0);case"check":return We(w,v=>v.check(x),!0);case"uncheck":return We(w,v=>v.uncheck(x),!0);case"hover":return We(w,v=>v.hover(x),!0);case"waitFor":return w.length>0?We(w,v=>v.waitFor({state:"visible",timeout:r})):Js(Math.min(Number(b??"0"),r));case"expectVisible":return c(u(v=>v.isVisible()),r,"target to be visible");case"expectNotVisible":return c(async()=>{for(let v of w){let S=await v.count();for(let h=0;h<S;h++)if(await v.nth(h).isVisible())return!1}return!0},r,`every match of every recorded locator to be hidden (${w.length} locator(s))`);case"expectText":return c(u(async v=>(await v.textContent(y)??"").includes(b??"")),r,`text "${b}"`);case"expectEach":{let v=t.each?.match(/^\{data:([^}]+)\}$/)?.[1];if(!v)throw new Error(`expectEach needs a {data:...} list reference, got ${JSON.stringify(t.each)}`);let S=at(v),h=async T=>await T.isVisible()?new Set(await T.evaluate(E=>{let _=M=>(M??"").replace(/\s+/g," ").trim(),A=M=>typeof M.checkVisibility=="function"?M.checkVisibility({visibilityProperty:!0,contentVisibilityAuto:!0}):!!(M.offsetWidth||M.offsetHeight||M.getClientRects().length),C=M=>{let z=M.ownerDocument.createTreeWalker(M,4),P="";for(let R=z.nextNode();R;R=z.nextNode())R.parentElement&&A(R.parentElement)&&(P+=R.nodeValue??"");return P},I=new Set,j=M=>{for(let z of Array.from(M.options))z.hidden||I.add(_(z.textContent))};E.tagName==="SELECT"&&j(E);for(let M of Array.from(E.querySelectorAll("*")))A(M)&&(M.tagName==="SELECT"?j(M):M.tagName!=="OPTION"&&I.add(_(C(M))));I.add(_(C(E)));for(let M of String(E.innerText??"").split(`
25
+ `))I.add(_(M));return[...I]},void 0,y)):new Set,g=S;return c(async()=>{for(let T of w)try{let E=await h(T.first()),_=S.filter(A=>!E.has(A.replace(/\s+/g," ").trim()));if(_.length<g.length&&(g=_),_.length===0)return!0}catch{}return!1},r,`every value of ${t.each} to be shown`).catch(()=>{throw new Error(`${g.map(T=>`"${T}"`).join(", ")} from ${t.each} ${g.length===1?"is":"are"} not shown in the target (${S.length-g.length} of ${S.length} found)`)})}case"expectValue":return c(u(async v=>await v.inputValue(y)===(b??"")),r,`input value "${b}"`);case"expectUrl":{let v=$??b??"";return c(async()=>e.url().includes(v),r,`URL containing "${v}" on the active page (${e.url()})`)}case"handleDialog":{let v=(b??"accept")!=="dismiss",S=t.promptText?m(t.promptText):void 0;e.once("dialog",h=>{v?h.accept(S):h.dismiss()});return}case"uploadFile":{let v=(b??"").split(`
26
+ `).filter(Boolean).map(h=>wa.isAbsolute(h)?h:wa.resolve(h));if(v.length===0)throw new Error("uploadFile requires a path");let S=Is(v,process.cwd());if(S)throw new Error(S);if(w.length>0)return We(w,h=>h.setInputFiles(v,x),!0);kn.set(e,v);return}case"dragTo":{if(!t.toTarget)throw new Error("dragTo requires toTarget");let v=xa(e,t.toTarget,n,!1,!1);return We(w,S=>S.dragTo(v[0].first(),x),!0)}case"captureText":{if(!t.saveAs)throw new Error("captureText requires saveAs");return c(u(async v=>{let S=zs(await v.textContent(y)??"");return S===""?!1:(o.set(t.saveAs,S),!0)}),r,`non-empty text to capture as "${t.saveAs}"`)}case"expectDiffers":{if(!t.compareTo)throw new Error("expectDiffers requires compareTo");let v=o.get(t.compareTo);if(v===void 0)throw new Error(`expectDiffers: no captured value named "${t.compareTo}"`);if(w.length>0)return c(u(async g=>zs(await g.textContent(y)??"")!==v),r,`target text to differ from "${t.compareTo}" (${v})`);let S=b??"",h=o.get(S);if(h===void 0)throw new Error(`expectDiffers: no captured value named "${S}"`);if(h===v)throw new Error(`expectDiffers: "${S}" (${h}) equals "${t.compareTo}" (${v})`);return}case"expectAttribute":{if(!t.attribute)throw new Error("expectAttribute requires attribute");if(!t.pattern)throw new Error("expectAttribute requires pattern");let v=new RegExp(p(t.pattern),"i");return c(u(async S=>v.test(await S.getAttribute(t.attribute,y)??"")),r,`attribute ${t.attribute} to match /${t.pattern}/i`)}case"expectMatches":{if(!t.pattern)throw new Error("expectMatches requires pattern");let v=new RegExp(p(t.pattern),"i");return c(u(async S=>v.test(zs(await S.textContent(y)??""))),r,`target text to match /${t.pattern}/i`)}case"switchTab":{let v=Number.parseInt(b??"",10),S=()=>e.context().pages().filter(g=>!g.isClosed());await c(async()=>S().length>v,r,`tab ${b} to exist (${S().length} open: ${S().map(g=>g.url()).join(", ")})`);let h=S()[v];return await h.bringToFront(),h}case"closeTab":{let v=e.context().pages().filter(g=>!g.isClosed()),S=b===void 0?e:v[Number.parseInt(b,10)];if(!S)throw new Error(`tab ${b} does not exist (${v.length} open)`);await S.close();let h=e.context().pages().filter(g=>!g.isClosed());if(h.length===0)throw new Error("closed the last tab; nothing left to drive");return h[h.length-1]}case"newTab":{let v=$?wn($):void 0;if(v)throw new Error(v);let S=await e.context().newPage();return $&&await S.goto(Es($,s),x),S}case"waitForResponse":case"expectResponse":{if(!t.urlPattern)throw new Error(`${t.action} requires urlPattern`);let v=new RegExp(p(t.urlPattern)),S=t.pattern?new RegExp(p(t.pattern)):void 0;if(!i){await e.waitForResponse(async g=>{let T={seq:0,url:g.url(),method:g.request().method(),status:g.status()};return va(T,v,t.method,t.status,void 0)?S?S.test(await g.text().catch(()=>"")):!0:!1},x);return}let h=()=>t.action==="expectResponse"?i.all():i.sincePreviousStep();return c(async()=>h().some(g=>va(g,v,t.method,t.status,S)),r,gf(t,t.urlPattern))}default:throw new Error(`Unknown action type: ${t.action}`)}}var Sr=(e,t)=>yn(e.actions,t),$a=(e,t)=>Ht(e.actions,t);function Ws(e,t){if(t.steps.length!==e.steps.length)return!1;for(let[n,r]of Object.entries(t.data??{})){let s;try{s=Dt(n)}catch{continue}if(s===r)continue;let o=t.steps.some(a=>Sr(a,`{data:${n}}`)),i=t.steps.some(a=>$a(a,r));if(!o||i)return!1}return t.steps.every((n,r)=>{let s=e.steps[r];if(n.gherkin!==s.text||n.kind!==s.kind)return!1;let o=n.table,i=s.table;if(!!o!=!!i)return!1;if(o&&i){let a=Object.keys(ue(o)),c=Object.keys(ue(i));if(a.length>0&&c.length>0){if(JSON.stringify(a)!==JSON.stringify(c))return!1;let l=ue(o),d=ue(i);for(let f of a){if(l[f]===d[f])continue;let m=`<${f}>`,p=t.steps.some($=>Sr($,m)),b=t.steps.some($=>$a($,l[f]));if(!p||b)return!1}}else if(JSON.stringify(o)!==JSON.stringify(i))return!1}if(n.docString!==void 0!=(s.docString!==void 0))return!1;if(n.docString!==void 0&&s.docString!==void 0&&n.docString!==s.docString){let a=t.steps.some(l=>Sr(l,"<docstring>")),c=t.steps.some(l=>Sr(l,n.docString));if(!a||c)return!1}return!0})}function Gs(e,t){let n=e.steps[t],r=n?.resolvedTable??n?.table;return{...e.params,...ct(e.steps),...ue(r),...vt(n?.resolvedDocString??n?.docString)}}async function Tn(e,t,n,r={}){let s=r.actionTimeoutMs??5e3,o=r.retries??1,i=Math.max(10,r.pollIntervalMs??ka),a=[],c=r.vars??new Map;r.uniqueSeed&&c.set(_e,r.uniqueSeed);for(let h of e.context().pages())kn.delete(h);let l=()=>Object.fromEntries(c),d=n.steps.some(h=>h.actions.some(g=>(g.action==="waitForResponse"||g.action==="expectResponse")&&g.pattern!==void 0)),f=new Hs;f.attach(e.context(),d);let m=!1,p=()=>(m=r.keepResponseLog===!0,m?{responses:f}:{}),b=e,$=new Set(e.context().pages()),k=[],w=h=>k.push(h);e.context().on("page",w);let x=h=>new Promise(g=>{let T=()=>{clearTimeout(E),e.context().off("page",T),g()},E=setTimeout(T,h);e.context().on("page",T)}),y=[],v=!n.steps.some(h=>h.actions.some(g=>g.opensTab))&&n.steps.some(h=>h.actions.some(g=>g.action==="switchTab"||g.action==="closeTab"||g.action==="newTab")),S=()=>{let h=e.context().pages();for(let g of h)$.has(g)||($.add(g),g.isClosed()||(b=g));if(b.isClosed()){let g=h.filter(T=>!T.isClosed());g.length>0&&(b=g[g.length-1])}};if(!Ws(t,n))return f.detach(),{status:"stale",stepResults:a,captured:l()};try{for(let h=0;h<n.steps.length;h++){let g=n.steps[h],T=Gs(t,h);f.markStep();let E=!1,_=0;r.trace&&await e.context().tracing.group(`[saffron:${r.tracePhase??"replay"}:${h+(r.traceStepOffset??0)}] ${g.keyword} ${g.gherkin}`).catch(()=>{});let A=async()=>{r.trace&&await e.context().tracing.groupEnd().catch(()=>{})};try{if(g.kind==="assertion"&&!g.actions.some(C=>Se.has(C.action))){let C=`Assertion step "${g.gherkin}" recorded ${g.actions.length===0?"nothing":"only setup actions"}, so nothing was checked. Re-record the scenario (saffron run --rerecord) so the agent records what must be true.`;a.push({gherkin:g.gherkin,fromStepSet:g.fromStepSet,kind:g.kind,status:"failed",drift:E,error:C});for(let I=h+1;I<n.steps.length;I++)a.push({gherkin:n.steps[I].gherkin,kind:n.steps[I].kind,status:"skipped"});return{status:"failed",failure:{stepIndex:h,actionIndex:0,stepText:g.gherkin,kind:g.kind,error:C},stepResults:a,captured:l(),...p()}}for(let C=0;C<g.actions.length;C++){let I=g.actions[C];S(),I.pageFingerprint&&await kt(b)!==I.pageFingerprint&&(E=!0);let j,M=!1;for(let z=0;z<=o&&!M;z++){z>0&&(_++,await Js(250*z),S());try{let P=z>0?`${$e(I)} (attempt ${z+1})`:$e(I),R=await la(b,r.trace,`[saffron:action:${C}] ${P}`,()=>xr(b,I,T,s,r.baseURL,c,f,i));R&&(b=R,$.add(R));let O=I.action==="click"||I.action==="press";if(k.length===0&&I.opensTab?await e.context().waitForEvent("page",{timeout:s}).catch(()=>{}):k.length===0&&O&&v&&await x(150),I.opensTab&&k.length===0)throw new Error("this action opened a new tab when it was recorded, and none opened now; the flow has changed");for(k.length>0&&y.push({stepIndex:h,actionIndex:C});k.length>0;)await k.shift().waitForLoadState("domcontentloaded",{timeout:s}).catch(()=>{});M=!0}catch(P){j=P}}if(!M){let z=j instanceof Error?j.message:String(j);a.push({gherkin:g.gherkin,fromStepSet:g.fromStepSet,kind:g.kind,status:"failed",drift:E,..._?{retries:_}:{},failedActionIndex:C,error:z});for(let R=h+1;R<n.steps.length;R++)a.push({gherkin:n.steps[R].gherkin,kind:n.steps[R].kind,status:"skipped"});let P={stepIndex:h,actionIndex:C,stepText:g.gherkin,kind:g.kind,error:z,pageUrl:b.isClosed()?void 0:b.url()};return await r.onFailure?.(b,P).catch(()=>{}),{status:"failed",failure:P,stepResults:a,captured:l(),tabOpeners:y,activePage:b.isClosed()?void 0:b,...p()}}}a.push({gherkin:g.gherkin,fromStepSet:g.fromStepSet,kind:g.kind,status:"passed",drift:E,..._?{retries:_}:{}})}finally{await A()}}return{status:"passed",stepResults:a,captured:l(),tabOpeners:y,activePage:b.isClosed()?void 0:b}}finally{e.context().off("page",w),m||f.detach()}}import{createHash as yf}from"node:crypto";import bf from"node:path";var Vs=e=>yf("sha1").update(JSON.stringify(e)).digest("hex").slice(0,16);function Ta(e){return{steps:e.steps.map(t=>({...t,actions:t.actions.map(({pageFingerprint:n,...r})=>r)})),data:e.data??null}}var Ra=e=>Vs(e.map(t=>[t.kind,t.text,t.table??null,t.docString??null])),Rn=e=>e?Vs(Ta(e)):"none",Ea=(e,t)=>Vs({contract:Ta(e),featureEdits:t??null});function Aa(e,t,n,r){return{scenario:Ra(e.steps),committed:Rn(t),recording:Ea(n,r)}}function ke(e,t,n){let r=t.meta.bound;if(!r)return;let s=[],{feature:o,scenario:i}=t.cache,a;if(n&&!(n instanceof Map))a=n;else try{a=me(bf.join(e,o),e,n).find(l=>l.name===i)}catch(l){s.push(`${o} no longer parses: ${l instanceof Error?l.message.split(`
27
+ `)[0]:String(l)}`)}s.length||(a?Ra(a.steps)!==r.scenario&&s.push(wf(a,t.cache)):s.push(`scenario "${i}" is no longer in ${o}`));let c;try{c=ye(Y(e,o,i))}catch(l){if(!(l instanceof X))throw l;return s.push(`the committed recording cannot be read (${l.problem})`),s.join("; ")}return Rn(c)!==r.committed&&s.push(r.committed==="none"?"a recording was committed for this scenario after the proposal was filed, so the diff you would review is not the one that was made":c?"the committed recording changed after the proposal was filed (an accepted heal, a propagated locator fix or a pull), so the diff you would review is not the one that was made":"the committed recording the proposal was diffed against is gone"),Ea(t.cache,t.meta.featureEdits)!==r.recording&&s.push("the proposal's recording or feature edits were edited after it was filed"),s.length>0?s.join("; "):void 0}function wf(e,t){let n=e.steps,r=t.steps;if(n.length!==r.length)return`the scenario has ${n.length} steps now and had ${r.length} when the proposal was filed`;let s=n.findIndex((o,i)=>o.text!==r[i].gherkin||o.kind!==r[i].kind);return s<0?"the scenario's steps changed since the proposal was filed":`step ${s+1} now reads "${n[s].text}" and was "${r[s].gherkin}" when the proposal was filed`}var $r="not bound: filed by a runner older than 0.8.5, so changes since cannot be detected";import Tt from"node:fs";import Pa from"node:path";function kr(e){return`$${e.toFixed(e>=1?2:4)}`}function Ks(e,t){let n=e===void 0?"?":`${Math.round(e)}%`,r=t===void 0?"?":`${Math.round(t)}%`;return`${n} \u2192 ${r}`}function Te(e){return e.costUsd>0||e.aiCalls===0}function Ys(e){return Te(e)?e.plan?` \xB7 5-hour window ${Ks(e.plan.fiveHourBefore,e.plan.fiveHourAfter)} (${e.plan.subscriptionType} plan) \xB7 \u2248 ${kr(e.costUsd)} at API rates`:` \xB7 ${kr(e.costUsd)}`:" \xB7 cost not reported by the provider"}function _a(e,t){return e?t?`\u2248 ${kr(e)}`:kr(e):""}function Ia(e){if(!e)return"";let t=new Date(e);return Number.isNaN(t.getTime())?"":`resets ${t.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}`}function Qs(e){let t=e.costUsd===void 0?"":` \xB7 cost ${e.costUsd>=0?"+":"-"}$${Math.abs(e.costUsd).toFixed(4)}`;return`vs previous run: pass rate ${e.passRatePp>=0?"+":""}${e.passRatePp}pp${t} \xB7 AI calls ${e.aiCalls>=0?"+":""}${e.aiCalls}`}function Xs(e){return Pa.join(e,".saffron","history.jsonl")}function eo(e,t,n,r){let s=e.map(o=>({feature:o.scenario.featurePath,scenario:o.scenario.displayName,tags:[...o.scenario.tags],status:o.status,source:o.source,durationMs:o.durationMs,verified:o.verified,seededSteps:o.seededSteps,adaptations:o.adaptations.length,driftSteps:o.stepResults.filter(i=>i.drift).length,aiCalls:o.usage.aiCalls,tokensInOut:o.usage.inputTokens+o.usage.outputTokens,cacheRead:o.usage.cacheReadTokens,cacheWrite:o.usage.cacheCreationTokens,costUsd:o.usage.costUsd??0,error:o.error?.split(`
28
+ `)[0]}));return{runId:t.toISOString(),startedAt:t.toISOString(),finishedAt:new Date().toISOString(),baseURL:r,green:s.filter(o=>o.status==="green").length,yellow:s.filter(o=>o.status==="yellow").length,red:s.filter(o=>o.status==="red").length,aiCalls:s.reduce((o,i)=>o+i.aiCalls,0),costUsd:s.reduce((o,i)=>o+i.costUsd,0),divergentSteps:n,scenarios:s}}function to(e,t){let n=Xs(e);Tt.mkdirSync(Pa.dirname(n),{recursive:!0}),Tt.appendFileSync(n,`${JSON.stringify(t)}
29
+ `)}function Ca(e,t){let n=Xs(e);if(!Tt.existsSync(n))return to(e,t);let r=Tt.readFileSync(n,"utf8").split(`
30
+ `).filter(i=>i.trim()),s=!1,o=r.map(i=>{try{if(JSON.parse(i).runId===t.runId)return s=!0,JSON.stringify(t)}catch{}return i});s||o.push(JSON.stringify(t)),Tt.writeFileSync(n,`${o.join(`
31
31
  `)}
32
- `)}function $n(e,t=50){let n=Js(e);if(!$t.existsSync(n))return[];let r=$t.readFileSync(n,"utf8").trim().split(`
33
- `).filter(Boolean),s=[];for(let o of r.slice(-t))try{s.push(JSON.parse(o))}catch{}return s}var Hs=e=>{let t=e.green+e.yellow+e.red;return t===0?0:Math.round((e.green+e.yellow)/t*100)};function Vs(e,t){let n=[...e,t],r=n.slice(-20),s=n.slice(-10),o=e.at(-1),i=o?{passRatePp:Hs(t)-Hs(o),costUsd:ke(t)&&ke(o)?t.costUsd-o.costUsd:void 0,aiCalls:t.aiCalls-o.aiCalls,red:t.red-o.red}:void 0,a=new Map;for(let f of s)for(let m of f.scenarios){let p=`${m.feature}::${m.scenario}`,b=a.get(p)??{feature:m.feature,scenario:m.scenario,heals:0,reds:0,runs:0};b.runs++,m.source==="agent-heal"&&m.status==="yellow"&&b.heals++,m.status==="red"&&b.reds++,a.set(p,b)}let c=[...a.values()].filter(f=>f.heals>=2||f.reds>=3).sort((f,m)=>m.heals+m.reds-(f.heals+f.reds)),l=new Map;for(let f of r)for(let m of f.scenarios)m.status==="red"&&m.error&&l.set(m.error,(l.get(m.error)??0)+1);let d=[...l.entries()].sort((f,m)=>m[1]-f[1]).slice(0,3).map(([f,m])=>({message:f,count:m}));return{prev:i,series:{passRate:r.map(Hs),costUsd:r.map(f=>ke(f)?Number(f.costUsd.toFixed(4)):null),aiCalls:r.map(f=>f.aiCalls)},chronic:c,divergentSteps:t.divergentSteps,topIssues:d}}import{execFileSync as Sf}from"node:child_process";import{createHash as xf,randomBytes as io}from"node:crypto";import G from"node:fs";import V from"node:path";import vf from"node:zlib";var D=e=>e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;"),va="M11 14 L10 4.5 L17.5 9.5 L24 2.5 L30.5 9.5 L38 4.5 L37 14 Z",nf="M24.085 45.646L24.085 45.646Q20.719 45.646 18.407 44.575Q16.095 43.504 14.905 41.651Q13.715 39.798 13.715 37.384L13.715 37.384L19.121 37.384Q19.121 38.336 19.648 39.152Q20.175 39.968 21.263 40.461Q22.351 40.954 24.085 40.954L24.085 40.954Q25.649 40.954 26.703 40.546Q27.757 40.138 28.301 39.407Q28.845 38.676 28.845 37.724L28.845 37.724Q28.845 36.534 27.825 35.837Q26.805 35.140 24.527 34.936L24.527 34.936L22.657 34.766Q18.951 34.460 16.741 32.454Q14.531 30.448 14.531 27.150L14.531 27.150Q14.531 24.770 15.670 23.036Q16.809 21.302 18.849 20.367Q20.889 19.432 23.677 19.432L23.677 19.432Q26.601 19.432 28.692 20.435Q30.783 21.438 31.905 23.257Q33.027 25.076 33.027 27.524L33.027 27.524L27.587 27.524Q27.587 26.606 27.145 25.841Q26.703 25.076 25.836 24.600Q24.969 24.124 23.677 24.124L23.677 24.124Q22.453 24.124 21.620 24.532Q20.787 24.940 20.379 25.637Q19.971 26.334 19.971 27.150L19.971 27.150Q19.971 28.204 20.719 28.986Q21.467 29.768 23.167 29.904L23.167 29.904L25.071 30.074Q27.791 30.312 29.865 31.247Q31.939 32.182 33.112 33.797Q34.285 35.412 34.285 37.724L34.285 37.724Q34.285 40.104 33.044 41.889Q31.803 43.674 29.525 44.660Q27.247 45.646 24.085 45.646Z";function Ys(e){return`<svg viewBox="0 0 48 48" width="${e}" height="${e}" aria-hidden="true">
34
- <path d="${va}" fill="#F4A300"/>
35
- <path d="${nf}" fill="#E7E9EC"/>
36
- </svg>`}function rf(e){return`<svg viewBox="8 1 32 15" height="${e}" aria-hidden="true"><path d="${va}" fill="#F4A300"/></svg>`}function sf(e){let t=e.imported;if(!t)return"";let n=t.shards?` \xB7 shard${t.shards.imported.length===1?"":"s"} ${t.shards.imported.join(", ")} of ${t.shards.total}`:"",r=t.headCommit??t.commit,s=`imported from ${t.run?`run ${t.run}`:"another machine"}${t.branch?` \xB7 ${t.branch}`:""}${r?` @ ${r.slice(0,7)}`:""}${n}`;return t.url&&/^https?:\/\//i.test(t.url)?`<a class="chip" href="${D(t.url)}" target="_blank" rel="noopener noreferrer">${D(s)}</a>`:`<span class="chip">${D(s)}</span>`}var of={green:"passed",yellow:"pending review",red:"failed"};function af(e){return e.hooks?.length?`<div class="panel"><div class="panel-title">setup and teardown</div>${e.hooks.map(n=>{let r=n.exitCode===0,s=r?"ok":n.timedOut?"timed out":n.exitCode===null?"could not start":`exit ${n.exitCode}`;return`<div class="mono">${r?"\u2713":"\u2717"} ${D(String(n.phase))} \xB7 ${D(n.command)} \xB7 ${D(s)} \xB7 ${D(Tn(n.durationMs))}</div>`}).join("")}</div>`:""}function Tn(e){return e>=1e3?`${(e/1e3).toFixed(1)}s`:`${e}ms`}function kn(e){return e>=1e6?`${(e/1e6).toFixed(1)}M`:e>=1e4?`${(e/1e3).toFixed(1)}k`:e.toLocaleString()}function tt(e,t,n,r="",s="",o=""){return`<div class="kpi">
37
- <div class="kpi-label">${e}</div>
38
- <div class="kpi-value ${r}">${t}</div>
39
- <div class="kpi-sub ${s}">${n}</div>${o}
40
- </div>`}function Ks(e,t){let n=e.filter(l=>l!==null);if(n.length<2)return"";let r=108,s=22,o=Math.min(...n),a=Math.max(...n)-o||1,c=e.map((l,d)=>{if(l===null)return"";let f=`${(d/(e.length-1)*r).toFixed(1)},${(s-2-(l-o)/a*(s-4)).toFixed(1)}`;return d===0||e[d-1]===null?`M${f}h0`:`L${f}`}).join("");return`<svg class="spark" viewBox="0 0 ${r} ${s}" width="${r}" height="${s}" aria-hidden="true"><path d="${c}" fill="none" stroke="${t}" stroke-width="1.8" stroke-linejoin="round" stroke-linecap="round"/></svg>`}var xa=(e,t="",n=0)=>`${e>=0?"+":"\u2212"}${Math.abs(e).toFixed(n)}${t} vs prev`;function cf(e,t){let n=e.totals,r=e.trends,s=n.scenarios===0?0:Math.round((n.green+n.yellow)/n.scenarios*100),o=n.red>0?"red":n.yellow>0?"gold":"green",i=n.inputTokens+n.outputTokens,a=r?.prev?`${xa(r.prev.passRatePp,"pp")} \xB7 ${n.green+n.yellow} of ${n.scenarios}`:`${n.green+n.yellow} of ${n.scenarios} scenarios`,c=r?.prev?.costUsd!==void 0?`${xa(r.prev.costUsd,"",4).replace("+","+$").replace("\u2212","\u2212$")} \xB7 ${Tn(t)}`:`${Tn(t)} total runtime`;return`<section class="kpis">
41
- ${tt("pass rate",`${s}<span class="unit">%</span>`,a,o,r?.prev?r.prev.passRatePp>=0?"green":"red":"",Ks(r?.series.passRate??[],"var(--green)"))}
42
- ${tt("passed",String(n.green),"deterministic replay","green",n.green>0?"green":"")}
43
- ${tt("pending",String(n.yellow),n.yellow>0?"\u25B2 proposals to review":"nothing to review",n.yellow>0?"gold":"",n.yellow>0?"gold":"")}
44
- ${tt("failed",String(n.red),n.red>0?"\u25BC needs attention":"all clear",n.red>0?"red":"",n.red>0?"red":"green")}
45
- ${tt("ai calls",String(n.aiCalls),`${kn(i)} in+out tokens`,n.aiCalls>0?"":"green")}
46
- ${tt("cache traffic",kn(n.cacheReadTokens+n.cacheCreationTokens),n.cacheReadTokens+n.cacheCreationTokens>0?`${kn(n.cacheReadTokens)} read \xB7 ${kn(n.cacheCreationTokens)} written`:"zero (fully cached run)","",n.cacheReadTokens+n.cacheCreationTokens>0?"":"green")}
47
- ${n.plan?tt("plan usage",`${Bs(n.plan.fiveHourBefore,n.plan.fiveHourAfter)}`,[`5-hour window \xB7 ${n.plan.subscriptionType} plan`,ba(n.plan.resetsAt)].filter(Boolean).join(" \xB7 "),"gold","")+tt("api-equivalent",`<span class="unit">\u2248 $</span>${n.costUsd.toFixed(n.costUsd>=1?2:4)}`,`what this run costs on an API key \xB7 ${c}`,"","",Ks(r?.series.costUsd??[],"var(--gold)")):tt("ai cost",ke(n)?`<span class="unit">$</span>${n.costUsd.toFixed(n.costUsd>=1?2:4)}`:"n/a",ke(n)?c:"not reported by the provider","","",ke(n)?Ks(r?.series.costUsd??[],"var(--gold)"):"")}
48
- </section>`}function lf(e){let t=e.trends;if(!t)return"";let n=[];return t.chronic.length>0&&n.push(`<div class="panel accent-gold"><div class="panel-title gold">chronic scenarios: stop paying for heals</div><ul>${t.chronic.map(r=>`<li>${D(r.feature)} \u203A ${D(r.scenario)}: ${r.heals} heal(s), ${r.reds} red(s) in the last ${r.runs} run(s). Re-record it or fix the feature text.</li>`).join("")}</ul></div>`),t.divergentSteps>0&&n.push(`<div class="panel"><div class="panel-title">step divergence (level-2 signal)</div><p>${t.divergentSteps} step text(s) currently have divergent recordings across caches.</p></div>`),t.topIssues.length>0&&n.push(`<div class="panel accent-red"><div class="panel-title red">recurring failure themes (last 20 runs)</div><ul>${t.topIssues.map(r=>`<li>${r.count}\xD7: ${D(r.message)}</li>`).join("")}</ul></div>`),n.length>0?`<section class="trends">${n.join(`
49
- `)}</section>`:""}function df(e){return`<ol class="steps">${e.steps.map(n=>{let r=n.status==="passed"?'<span class="dot green"></span>':n.status==="failed"?'<span class="dot red"></span>':'<span class="dot idle"></span>';return`<li class="step ${n.status}">
50
- ${r}
51
- <div class="step-body">
52
- <span class="step-text">${D(n.gherkin)}</span>
53
- ${n.kind==="assertion"?'<span class="tag">assert</span>':""}
54
- ${n.fromStepSet?`<span class="tag indigo" title="inlined from a StepSet definition: edit once, every invoking scenario follows">from StepSet: ${D(n.fromStepSet)}</span>`:""}
55
- ${n.drift?'<span class="tag gold" title="page fingerprint no longer matches the recording">drift</span>':""}
56
- ${n.retries?`<span class="tag gold" title="an action failed and was re-attempted with backoff before the step settled">retried \xD7${n.retries}</span>`:""}
57
- ${n.error?`<div class="step-error">${D(n.error)}</div>`:""}
58
- </div>
59
- </li>`}).join(`
60
- `)}</ol>`}function pf(e){return e.featureEdits&&e.featureEdits.length>0?`<div class="panel">
61
- <div class="panel-title gold">suggested .feature edit</div>
62
- <div class="diff">${e.featureEdits.map(n=>{let r=e.steps[n.index]?.gherkin??`step ${n.index}`;return`<div class="diff-line del">\u2212 ${D(r)}</div><div class="diff-line add">+ ${D(n.text)}</div>`}).join("")}</div>
63
- <div class="panel-hint">apply with <code>saffron accept --with-feature-edit</code></div>
64
- </div>`:e.suggestedFeatureEdit?`<div class="panel"><div class="panel-title gold">suggested .feature edit</div><pre>${D(e.suggestedFeatureEdit)}</pre></div>`:""}function ff(e){let t=e.proposalDiff;if(!t)return"";let n=t.steps.filter(c=>c.change!=="same"||!t.hasCommitted).map(c=>{let l=`<div class="diff-step">${c.kind==="assertion"?'<span class="pill gold">assertion</span> ':""}${D(c.gherkin)}${c.fromStepSet?` <span class="mono">(step set "${D(c.fromStepSet)}")</span>`:""}</div>`,d=c.actions.flatMap(f=>f.change==="same"?[`<div class="diff-line"> ${D(f.before??"")}</div>`]:f.change==="added"?[`<div class="diff-line add">+ ${D(f.after??"")}</div>`]:f.change==="removed"?[`<div class="diff-line del">\u2212 ${D(f.before??"")}</div>`]:[`<div class="diff-line del">\u2212 ${D(f.before??"")}</div>`,`<div class="diff-line add">+ ${D(f.after??"")}</div>`,...(f.details??[]).map(m=>`<div class="diff-line detail">${D(m)}</div>`)]).join("");return`${l}<div class="diff">${d}</div>`}).join(""),{added:r,removed:s,changed:o,assertions:i}=t.summary,a=r+s+o===0?"No action changed.":`${o} changed, ${r} added, ${s} removed.${i>0?` ${i} touch the scenario's verdict.`:""}`;return`<div class="panel accent-gold"><div class="panel-title gold">what this proposal changes</div>${t.hasCommitted?"":'<p class="panel-hint">First recording: there is nothing to compare against.</p>'}${n}<div class="panel-hint">${D(a)}</div></div>`}function uf(e,t){let n=e.usage.inputTokens+e.usage.outputTokens;if(e.usage.aiCalls===0)return'<span class="pill green">0 tokens</span>';let r=ya(e.usage.costUsd,t),s=r?` \xB7 ${r}`:"";return`<span class="pill gold">${e.usage.aiCalls} ai \xB7 ${kn(n)} tok${s}</span>`}function gf(e,t,n){return`<details class="card" ${e.status!=="green"?"open":""}>
65
- <summary>
66
- <span class="pill ${e.status==="green"?"green":e.status==="yellow"?"gold":"red"}">${of[e.status]}</span>
67
- <span class="card-title">${D(e.scenario)}</span>
68
- <span class="card-meta">
69
- <span class="m">${D(e.feature)}</span>
70
- <span class="m-sep">\xB7</span>
71
- <span class="m">${D(e.source)}</span>
72
- <span class="m-sep">\xB7</span>
73
- <span class="m">${Tn(e.durationMs)}</span>
74
- ${uf(e,t)}
75
- </span>
76
- </summary>
77
- <div class="card-body">
78
- ${df(e)}
79
- ${e.narrative?`<div class="panel"><div class="panel-title">agent narrative</div><p>${D(e.narrative)}</p></div>`:""}
80
- ${e.adaptations.length>0?`<div class="panel accent-gold"><div class="panel-title gold">adaptations</div><ul>${e.adaptations.map(r=>`<li>${D(r)}</li>`).join("")}</ul></div>`:""}
81
- ${pf(e)}
82
- ${ff(e)}
83
- ${e.proposalFile?`<div class="panel"><div class="panel-title">cache proposal pending${e.verified===!0?' <span class="pill green">verified</span>':e.verified===!1?' <span class="pill red">unverified</span>':""}</div><p class="mono">${D(e.proposalFile)}</p>${e.verified===!1&&e.proofError?`<p class="mono" style="color:var(--red)">proof replay failed: ${D(e.proofError.split(`
84
- `)[0])}</p>`:""}<div class="panel-hint">review with <code>saffron accept</code> / <code>saffron reject</code></div></div>`:""}
85
- ${e.error&&e.status==="red"?`<div class="panel accent-red"><div class="panel-title red">failure</div><p>${D(e.error)}</p></div>`:""}
86
- ${e.uniques&&e.status!=="green"?`<div class="panel"><div class="panel-title">unique values in this run</div>${Object.entries(e.uniques).map(([r,s])=>`<div class="mono">${D(r)} = ${D(s)}</div>`).join("")}</div>`:""}
87
- ${e.proofUniques&&e.status!=="green"?`<div class="panel"><div class="panel-title">unique values in the proof replay</div>${Object.entries(e.proofUniques).map(([r,s])=>`<div class="mono">${D(r)} = ${D(s)}</div>`).join("")}</div>`:""}
88
- ${hf(e,n)}
89
- </div>
90
- </details>`}function hf(e,t){let n=(e.evidence??[]).find(o=>o.kind==="trace"),r=n?`<div class="panel accent-gold"><div class="panel-title gold">execution replay</div><div>Step through this run with the page inspectable at every action:</div><div class="mono" style="margin-top:6px">npx saffron trace ${D(mf(`${e.feature}:${e.scenario}`))}</div><div class="panel-hint">${D(n.file)}</div></div>`:"",s=(e.evidence??[]).flatMap(o=>{if(!t||o.kind!=="failure"&&o.kind!=="before-heal")return[];let i=zt(t,o);if(!i)return[];try{let a=i.toString("base64"),c=o.kind==="failure"?"the page when it failed":"the page when the cached step stopped working, before the agent took over";return[`<figure class="shot"><figcaption>${D(c)}${o.step?`: <span class="mono">${D(o.step)}</span>`:""}</figcaption><a href="data:image/jpeg;base64,${a}" target="_blank" rel="noopener"><img alt="${D(c)}" src="data:image/jpeg;base64,${a}"/></a><div class="panel-hint">${D(o.file)}</div></figure>`]}catch{return[]}});return s.length===0?r:`<div class="panel ${e.status==="red"?"accent-red":"accent-gold"}"><div class="panel-title ${e.status==="red"?"red":"gold"}">screenshot</div>${s.join("")}</div>${r}`}var mf=e=>/[^A-Za-z0-9_./:-]/.test(e)?`'${e.replace(/'/g,"'\\''")}'`:e;function Vt(e,t){let n=e.totals,r=new Date(e.startedAt),s=new Date(e.finishedAt).getTime()-r.getTime(),o=r.toLocaleString("en-GB",{day:"2-digit",month:"short",year:"numeric",hour:"2-digit",minute:"2-digit"});return`<!doctype html>
91
- <html lang="en">
92
- <head>
93
- <meta charset="utf-8"/>
94
- <meta name="viewport" content="width=device-width, initial-scale=1"/>
95
- <title>Saffron run report</title>
96
- <link rel="preconnect" href="https://fonts.googleapis.com">
32
+ `)}function En(e,t=50){let n=Xs(e);if(!Tt.existsSync(n))return[];let r=Tt.readFileSync(n,"utf8").trim().split(`
33
+ `).filter(Boolean),s=[];for(let o of r.slice(-t))try{s.push(JSON.parse(o))}catch{}return s}var Zs=e=>{let t=e.green+e.yellow+e.red;return t===0?0:Math.round((e.green+e.yellow)/t*100)};function no(e,t){let n=[...e,t],r=n.slice(-20),s=n.slice(-10),o=e.at(-1),i=o?{passRatePp:Zs(t)-Zs(o),costUsd:Te(t)&&Te(o)?t.costUsd-o.costUsd:void 0,aiCalls:t.aiCalls-o.aiCalls,red:t.red-o.red}:void 0,a=new Map;for(let f of s)for(let m of f.scenarios){let p=`${m.feature}::${m.scenario}`,b=a.get(p)??{feature:m.feature,scenario:m.scenario,heals:0,reds:0,runs:0};b.runs++,m.source==="agent-heal"&&m.status==="yellow"&&b.heals++,m.status==="red"&&b.reds++,a.set(p,b)}let c=[...a.values()].filter(f=>f.heals>=2||f.reds>=3).sort((f,m)=>m.heals+m.reds-(f.heals+f.reds)),l=new Map;for(let f of r)for(let m of f.scenarios)m.status==="red"&&m.error&&l.set(m.error,(l.get(m.error)??0)+1);let d=[...l.entries()].sort((f,m)=>m[1]-f[1]).slice(0,3).map(([f,m])=>({message:f,count:m}));return{prev:i,series:{passRate:r.map(Zs),costUsd:r.map(f=>Te(f)?Number(f.costUsd.toFixed(4)):null),aiCalls:r.map(f=>f.aiCalls)},chronic:c,divergentSteps:t.divergentSteps,topIssues:d}}var ro=e=>e.replace(/ \(example \d+\)$/,""),An=(e,t)=>`${e}\0${t}`,so=(e,t)=>t===0?0:Math.round(e/t*100),Oa=e=>Math.max(0,Date.parse(e.finishedAt)-Date.parse(e.startedAt))||0;function ja(e,t){let n=new Map;for(let S of e)for(let h of S.scenarios)n.set(An(S.path,h.name),{feature:S.path,scenario:h.name,tags:h.tags});let r=S=>n.has(An(S.feature,ro(S.scenario))),s=e.flatMap(S=>S.scenarios),o=t.slice(-20),i=t.at(-1),a=new Map;for(let S of o)for(let h of S.scenarios){if(!r(h))continue;let g=An(h.feature,h.scenario),T=a.get(g)??{ref:{feature:h.feature,scenario:h.scenario},results:[]};T.results.push({at:S.startedAt,s:h}),a.set(g,T)}let c=[],l=[],d=[];for(let{ref:S,results:h}of a.values()){let g=h.map(A=>A.s.status==="red"),T=0;for(let A=1;A<g.length;A++)g[A]!==g[A-1]&&T++;let E=g.filter(Boolean).length;T>=2&&c.push({...S,runs:h.length,reds:E,flips:T,last:h.at(-1).s.status});let _=h.filter(A=>A.s.source==="agent-heal"&&A.s.status==="yellow").length;_>=2&&l.push({...S,runs:h.length,heals:_}),d.push({...S,avgMs:Math.round(h.reduce((A,C)=>A+C.s.durationMs,0)/h.length),lastMs:h.at(-1).s.durationMs,runs:h.length})}c.sort((S,h)=>h.flips-S.flips||h.reds-S.reds),l.sort((S,h)=>h.heals-S.heals),d.sort((S,h)=>h.avgMs-S.avgMs);let f=[],m=new Set(t.slice(-10).map(S=>S.startedAt));for(let{ref:S,results:h}of a.values()){let g=h.findLast(_=>_.s.status==="red");if(!g||!m.has(g.at))continue;let T=h.at(-1).s.status==="red",E=0;for(let _=h.length-1;_>=0&&h[_].s.status==="red";_--)E++;f.push({...S,at:g.at,error:g.s.error,stillFailing:T,streak:E})}f.sort((S,h)=>Number(h.stillFailing)-Number(S.stillFailing)||h.at.localeCompare(S.at));let p={green:0,yellow:1,red:2},b=new Map;for(let S of t){let h=new Map;for(let g of S.scenarios){let T=An(g.feature,ro(g.scenario)),E=h.get(T);(!E||p[g.status]>p[E])&&h.set(T,g.status)}for(let[g,T]of h)b.set(g,T)}let $=new Set(b.keys()),k=new Set((i?.scenarios??[]).map(S=>An(S.feature,ro(S.scenario)))),w=[],x=[];for(let[S,h]of n){let g={feature:h.feature,scenario:h.scenario};$.has(S)?i&&!k.has(S)&&x.push(g):w.push(g)}let y=i?(()=>{let S=i.scenarios.length;return{startedAt:i.startedAt,durationMs:Oa(i),ran:S,green:i.green,yellow:i.yellow,red:i.red,passRate:so(i.green+i.yellow,S),replayRate:so(i.scenarios.filter(h=>h.source==="cache"&&h.status!=="red").length,S)}})():void 0,u={green:0,yellow:0,red:0,neverRun:0},v=new Map;for(let[S,h]of n){let g=b.get(S)??"neverRun";u[g]++;for(let T of h.tags){let E=v.get(T)??{tag:T,scenarios:0,green:0,yellow:0,red:0,neverRun:0};E.scenarios++,E[g]++,v.set(T,E)}}return{historyRuns:t.length,suite:{features:e.length,scenarios:s.length,tests:s.reduce((S,h)=>S+h.rows,0),recorded:s.filter(S=>S.cached).length,...u},lastRun:y,trend:o.map(S=>({startedAt:S.startedAt,passRate:so(S.green+S.yellow,S.green+S.yellow+S.red),durationMs:Oa(S),red:S.red,tests:S.green+S.yellow+S.red})),flaky:c,healing:l,recentFailures:f,notInLastRun:x,neverRun:w,slowest:d.slice(0,10),tags:[...v.values()].sort((S,h)=>h.red-S.red||h.scenarios-S.scenarios||S.tag.localeCompare(h.tag))}}import{execFileSync as Ff}from"node:child_process";import{createHash as Mf,randomBytes as bo}from"node:crypto";import G from"node:fs";import V from"node:path";import Df from"node:zlib";var N=e=>e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;"),Fa="M11 14 L10 4.5 L17.5 9.5 L24 2.5 L30.5 9.5 L38 4.5 L37 14 Z",vf="M24.085 45.646L24.085 45.646Q20.719 45.646 18.407 44.575Q16.095 43.504 14.905 41.651Q13.715 39.798 13.715 37.384L13.715 37.384L19.121 37.384Q19.121 38.336 19.648 39.152Q20.175 39.968 21.263 40.461Q22.351 40.954 24.085 40.954L24.085 40.954Q25.649 40.954 26.703 40.546Q27.757 40.138 28.301 39.407Q28.845 38.676 28.845 37.724L28.845 37.724Q28.845 36.534 27.825 35.837Q26.805 35.140 24.527 34.936L24.527 34.936L22.657 34.766Q18.951 34.460 16.741 32.454Q14.531 30.448 14.531 27.150L14.531 27.150Q14.531 24.770 15.670 23.036Q16.809 21.302 18.849 20.367Q20.889 19.432 23.677 19.432L23.677 19.432Q26.601 19.432 28.692 20.435Q30.783 21.438 31.905 23.257Q33.027 25.076 33.027 27.524L33.027 27.524L27.587 27.524Q27.587 26.606 27.145 25.841Q26.703 25.076 25.836 24.600Q24.969 24.124 23.677 24.124L23.677 24.124Q22.453 24.124 21.620 24.532Q20.787 24.940 20.379 25.637Q19.971 26.334 19.971 27.150L19.971 27.150Q19.971 28.204 20.719 28.986Q21.467 29.768 23.167 29.904L23.167 29.904L25.071 30.074Q27.791 30.312 29.865 31.247Q31.939 32.182 33.112 33.797Q34.285 35.412 34.285 37.724L34.285 37.724Q34.285 40.104 33.044 41.889Q31.803 43.674 29.525 44.660Q27.247 45.646 24.085 45.646Z";function In(e){return`<svg viewBox="0 0 48 48" width="${e}" height="${e}" aria-hidden="true">
34
+ <path d="${Fa}" fill="#F4A300"/>
35
+ <path d="${vf}" fill="#E7E9EC"/>
36
+ </svg>`}function oo(e){return`<svg viewBox="8 1 32 15" height="${e}" aria-hidden="true"><path d="${Fa}" fill="#F4A300"/></svg>`}function Sf(e){let t=e.imported;if(!t)return"";let n=t.shards?` \xB7 shard${t.shards.imported.length===1?"":"s"} ${t.shards.imported.join(", ")} of ${t.shards.total}`:"",r=t.headCommit??t.commit,s=`imported from ${t.run?`run ${t.run}`:"another machine"}${t.branch?` \xB7 ${t.branch}`:""}${r?` @ ${r.slice(0,7)}`:""}${n}`;return t.url&&/^https?:\/\//i.test(t.url)?`<a class="chip" href="${N(t.url)}" target="_blank" rel="noopener noreferrer">${N(s)}</a>`:`<span class="chip">${N(s)}</span>`}var io=`<link rel="preconnect" href="https://fonts.googleapis.com">
97
37
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
98
- <link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,600;12..96,700&family=Geist:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
99
- <style>
100
- :root {
38
+ <link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,600;12..96,700&family=Geist:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">`,ao=` :root {
101
39
  --bg: #131416;
102
40
  --card: #1A1B1E;
103
41
  --card-2: #16171A;
@@ -207,91 +145,153 @@ import ae from"node:fs";import N from"node:path";import yh from"node:readline";i
207
145
  code { font-family: var(--mono); font-size: 11px; background: var(--bg); border: 1px solid var(--border-2); padding: 1px 6px; border-radius: 4px; color: var(--text-dim); }
208
146
 
209
147
  footer { margin-top: 42px; display: flex; align-items: center; justify-content: center; gap: 8px; color: var(--faint); font-family: var(--mono); font-size: 10.5px; letter-spacing: .08em; }
210
- </style>
148
+ `,xf={green:"passed",yellow:"pending review",red:"failed"};function $f(e){return e.hooks?.length?`<div class="panel"><div class="panel-title">setup and teardown</div>${e.hooks.map(n=>{let r=n.exitCode===0,s=r?"ok":n.timedOut?"timed out":n.exitCode===null?"could not start":`exit ${n.exitCode}`;return`<div class="mono">${r?"\u2713":"\u2717"} ${N(String(n.phase))} \xB7 ${N(n.command)} \xB7 ${N(s)} \xB7 ${N(Ge(n.durationMs))}</div>`}).join("")}</div>`:""}function Ge(e){return e>=1e3?`${(e/1e3).toFixed(1)}s`:`${e}ms`}function _n(e){return e>=1e6?`${(e/1e6).toFixed(1)}M`:e>=1e4?`${(e/1e3).toFixed(1)}k`:e.toLocaleString()}function oe(e,t,n,r="",s="",o=""){return`<div class="kpi">
149
+ <div class="kpi-label">${e}</div>
150
+ <div class="kpi-value ${r}">${t}</div>
151
+ <div class="kpi-sub ${s}">${n}</div>${o}
152
+ </div>`}function Qt(e,t){let n=e.filter(l=>l!==null);if(n.length<2)return"";let r=108,s=22,o=Math.min(...n),a=Math.max(...n)-o||1,c=e.map((l,d)=>{if(l===null)return"";let f=`${(d/(e.length-1)*r).toFixed(1)},${(s-2-(l-o)/a*(s-4)).toFixed(1)}`;return d===0||e[d-1]===null?`M${f}h0`:`L${f}`}).join("");return`<svg class="spark" viewBox="0 0 ${r} ${s}" width="${r}" height="${s}" aria-hidden="true"><path d="${c}" fill="none" stroke="${t}" stroke-width="1.8" stroke-linejoin="round" stroke-linecap="round"/></svg>`}var Na=(e,t="",n=0)=>`${e>=0?"+":"\u2212"}${Math.abs(e).toFixed(n)}${t} vs prev`;function kf(e,t){let n=e.totals,r=e.trends,s=n.scenarios===0?0:Math.round((n.green+n.yellow)/n.scenarios*100),o=n.red>0?"red":n.yellow>0?"gold":"green",i=n.inputTokens+n.outputTokens,a=r?.prev?`${Na(r.prev.passRatePp,"pp")} \xB7 ${n.green+n.yellow} of ${n.scenarios}`:`${n.green+n.yellow} of ${n.scenarios} scenarios`,c=r?.prev?.costUsd!==void 0?`${Na(r.prev.costUsd,"",4).replace("+","+$").replace("\u2212","\u2212$")} \xB7 ${Ge(t)}`:`${Ge(t)} total runtime`;return`<section class="kpis">
153
+ ${oe("pass rate",`${s}<span class="unit">%</span>`,a,o,r?.prev?r.prev.passRatePp>=0?"green":"red":"",Qt(r?.series.passRate??[],"var(--green)"))}
154
+ ${oe("passed",String(n.green),"deterministic replay","green",n.green>0?"green":"")}
155
+ ${oe("pending",String(n.yellow),n.yellow>0?"\u25B2 proposals to review":"nothing to review",n.yellow>0?"gold":"",n.yellow>0?"gold":"")}
156
+ ${oe("failed",String(n.red),n.red>0?"\u25BC needs attention":"all clear",n.red>0?"red":"",n.red>0?"red":"green")}
157
+ ${oe("ai calls",String(n.aiCalls),`${_n(i)} in+out tokens`,n.aiCalls>0?"":"green")}
158
+ ${oe("cache traffic",_n(n.cacheReadTokens+n.cacheCreationTokens),n.cacheReadTokens+n.cacheCreationTokens>0?`${_n(n.cacheReadTokens)} read \xB7 ${_n(n.cacheCreationTokens)} written`:"zero (fully cached run)","",n.cacheReadTokens+n.cacheCreationTokens>0?"":"green")}
159
+ ${n.plan?oe("plan usage",`${Ks(n.plan.fiveHourBefore,n.plan.fiveHourAfter)}`,[`5-hour window \xB7 ${n.plan.subscriptionType} plan`,Ia(n.plan.resetsAt)].filter(Boolean).join(" \xB7 "),"gold","")+oe("api-equivalent",`<span class="unit">\u2248 $</span>${n.costUsd.toFixed(n.costUsd>=1?2:4)}`,`what this run costs on an API key \xB7 ${c}`,"","",Qt(r?.series.costUsd??[],"var(--gold)")):oe("ai cost",Te(n)?`<span class="unit">$</span>${n.costUsd.toFixed(n.costUsd>=1?2:4)}`:"n/a",Te(n)?c:"not reported by the provider","","",Te(n)?Qt(r?.series.costUsd??[],"var(--gold)"):"")}
160
+ </section>`}function Tf(e){let t=e.trends;if(!t)return"";let n=[];return t.chronic.length>0&&n.push(`<div class="panel accent-gold"><div class="panel-title gold">chronic scenarios: stop paying for heals</div><ul>${t.chronic.map(r=>`<li>${N(r.feature)} \u203A ${N(r.scenario)}: ${r.heals} heal(s), ${r.reds} red(s) in the last ${r.runs} run(s). Re-record it or fix the feature text.</li>`).join("")}</ul></div>`),t.divergentSteps>0&&n.push(`<div class="panel"><div class="panel-title">step divergence (level-2 signal)</div><p>${t.divergentSteps} step text(s) currently have divergent recordings across caches.</p></div>`),t.topIssues.length>0&&n.push(`<div class="panel accent-red"><div class="panel-title red">recurring failure themes (last 20 runs)</div><ul>${t.topIssues.map(r=>`<li>${r.count}\xD7: ${N(r.message)}</li>`).join("")}</ul></div>`),n.length>0?`<section class="trends">${n.join(`
161
+ `)}</section>`:""}function Rf(e){return`<ol class="steps">${e.steps.map(n=>{let r=n.status==="passed"?'<span class="dot green"></span>':n.status==="failed"?'<span class="dot red"></span>':'<span class="dot idle"></span>';return`<li class="step ${n.status}">
162
+ ${r}
163
+ <div class="step-body">
164
+ <span class="step-text">${N(n.gherkin)}</span>
165
+ ${n.kind==="assertion"?'<span class="tag">assert</span>':""}
166
+ ${n.fromStepSet?`<span class="tag indigo" title="inlined from a StepSet definition: edit once, every invoking scenario follows">from StepSet: ${N(n.fromStepSet)}</span>`:""}
167
+ ${n.drift?'<span class="tag gold" title="page fingerprint no longer matches the recording">drift</span>':""}
168
+ ${n.retries?`<span class="tag gold" title="an action failed and was re-attempted with backoff before the step settled">retried \xD7${n.retries}</span>`:""}
169
+ ${n.error?`<div class="step-error">${N(n.error)}</div>`:""}
170
+ </div>
171
+ </li>`}).join(`
172
+ `)}</ol>`}function Ef(e){return e.featureEdits&&e.featureEdits.length>0?`<div class="panel">
173
+ <div class="panel-title gold">suggested .feature edit</div>
174
+ <div class="diff">${e.featureEdits.map(n=>{let r=e.steps[n.index]?.gherkin??`step ${n.index}`;return`<div class="diff-line del">\u2212 ${N(r)}</div><div class="diff-line add">+ ${N(n.text)}</div>`}).join("")}</div>
175
+ <div class="panel-hint">apply with <code>saffron accept --with-feature-edit</code></div>
176
+ </div>`:e.suggestedFeatureEdit?`<div class="panel"><div class="panel-title gold">suggested .feature edit</div><pre>${N(e.suggestedFeatureEdit)}</pre></div>`:""}function Af(e){let t=e.proposalDiff;if(!t)return"";let n=t.steps.filter(c=>c.change!=="same"||!t.hasCommitted).map(c=>{let l=`<div class="diff-step">${c.kind==="assertion"?'<span class="pill gold">assertion</span> ':""}${N(c.gherkin)}${c.fromStepSet?` <span class="mono">(step set "${N(c.fromStepSet)}")</span>`:""}</div>`,d=c.actions.flatMap(f=>f.change==="same"?[`<div class="diff-line"> ${N(f.before??"")}</div>`]:f.change==="added"?[`<div class="diff-line add">+ ${N(f.after??"")}</div>`]:f.change==="removed"?[`<div class="diff-line del">\u2212 ${N(f.before??"")}</div>`]:[`<div class="diff-line del">\u2212 ${N(f.before??"")}</div>`,`<div class="diff-line add">+ ${N(f.after??"")}</div>`,...(f.details??[]).map(m=>`<div class="diff-line detail">${N(m)}</div>`)]).join("");return`${l}<div class="diff">${d}</div>`}).join(""),{added:r,removed:s,changed:o,assertions:i}=t.summary,a=r+s+o===0?"No action changed.":`${o} changed, ${r} added, ${s} removed.${i>0?` ${i} touch the scenario's verdict.`:""}`;return`<div class="panel accent-gold"><div class="panel-title gold">what this proposal changes</div>${t.hasCommitted?"":'<p class="panel-hint">First recording: there is nothing to compare against.</p>'}${n}<div class="panel-hint">${N(a)}</div></div>`}function _f(e,t){let n=e.usage.inputTokens+e.usage.outputTokens;if(e.usage.aiCalls===0)return'<span class="pill green">0 tokens</span>';let r=_a(e.usage.costUsd,t),s=r?` \xB7 ${r}`:"";return`<span class="pill gold">${e.usage.aiCalls} ai \xB7 ${_n(n)} tok${s}</span>`}function If(e,t,n){return`<details class="card" ${e.status!=="green"?"open":""}>
177
+ <summary>
178
+ <span class="pill ${e.status==="green"?"green":e.status==="yellow"?"gold":"red"}">${xf[e.status]}</span>
179
+ <span class="card-title">${N(e.scenario)}</span>
180
+ <span class="card-meta">
181
+ <span class="m">${N(e.feature)}</span>
182
+ <span class="m-sep">\xB7</span>
183
+ <span class="m">${N(e.source)}</span>
184
+ <span class="m-sep">\xB7</span>
185
+ <span class="m">${Ge(e.durationMs)}</span>
186
+ ${_f(e,t)}
187
+ </span>
188
+ </summary>
189
+ <div class="card-body">
190
+ ${Rf(e)}
191
+ ${e.narrative?`<div class="panel"><div class="panel-title">agent narrative</div><p>${N(e.narrative)}</p></div>`:""}
192
+ ${e.adaptations.length>0?`<div class="panel accent-gold"><div class="panel-title gold">adaptations</div><ul>${e.adaptations.map(r=>`<li>${N(r)}</li>`).join("")}</ul></div>`:""}
193
+ ${Ef(e)}
194
+ ${Af(e)}
195
+ ${e.proposalFile?`<div class="panel"><div class="panel-title">cache proposal pending${e.verified===!0?' <span class="pill green">verified</span>':e.verified===!1?' <span class="pill red">unverified</span>':""}</div><p class="mono">${N(e.proposalFile)}</p>${e.verified===!1&&e.proofError?`<p class="mono" style="color:var(--red)">proof replay failed: ${N(e.proofError.split(`
196
+ `)[0])}</p>`:""}<div class="panel-hint">review with <code>saffron accept</code> / <code>saffron reject</code></div></div>`:""}
197
+ ${e.error&&e.status==="red"?`<div class="panel accent-red"><div class="panel-title red">failure</div><p>${N(e.error)}</p></div>`:""}
198
+ ${e.uniques&&e.status!=="green"?`<div class="panel"><div class="panel-title">unique values in this run</div>${Object.entries(e.uniques).map(([r,s])=>`<div class="mono">${N(r)} = ${N(s)}</div>`).join("")}</div>`:""}
199
+ ${e.proofUniques&&e.status!=="green"?`<div class="panel"><div class="panel-title">unique values in the proof replay</div>${Object.entries(e.proofUniques).map(([r,s])=>`<div class="mono">${N(r)} = ${N(s)}</div>`).join("")}</div>`:""}
200
+ ${Pf(e,n)}
201
+ </div>
202
+ </details>`}function Pf(e,t){let n=(e.evidence??[]).find(o=>o.kind==="trace"),r=n?`<div class="panel accent-gold"><div class="panel-title gold">execution replay</div><div>Step through this run with the page inspectable at every action:</div><div class="mono" style="margin-top:6px">npx saffron trace ${N(Cf(`${e.feature}:${e.scenario}`))}</div><div class="panel-hint">${N(n.file)}</div></div>`:"",s=(e.evidence??[]).flatMap(o=>{if(!t||o.kind!=="failure"&&o.kind!=="before-heal")return[];let i=Jt(t,o);if(!i)return[];try{let a=i.toString("base64"),c=o.kind==="failure"?"the page when it failed":"the page when the cached step stopped working, before the agent took over";return[`<figure class="shot"><figcaption>${N(c)}${o.step?`: <span class="mono">${N(o.step)}</span>`:""}</figcaption><a href="data:image/jpeg;base64,${a}" target="_blank" rel="noopener"><img alt="${N(c)}" src="data:image/jpeg;base64,${a}"/></a><div class="panel-hint">${N(o.file)}</div></figure>`]}catch{return[]}});return s.length===0?r:`<div class="panel ${e.status==="red"?"accent-red":"accent-gold"}"><div class="panel-title ${e.status==="red"?"red":"gold"}">screenshot</div>${s.join("")}</div>${r}`}var Cf=e=>/[^A-Za-z0-9_./:-]/.test(e)?`'${e.replace(/'/g,"'\\''")}'`:e;function Zt(e,t){let n=e.totals,r=new Date(e.startedAt),s=new Date(e.finishedAt).getTime()-r.getTime(),o=r.toLocaleString("en-GB",{day:"2-digit",month:"short",year:"numeric",hour:"2-digit",minute:"2-digit"});return`<!doctype html>
203
+ <html lang="en">
204
+ <head>
205
+ <meta charset="utf-8"/>
206
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
207
+ <title>Saffron run report</title>
208
+ ${io}
209
+ <style>
210
+ ${ao}</style>
211
211
  </head>
212
212
  <body>
213
213
  <div class="topline"></div>
214
214
  <div class="wrap">
215
215
  <header class="top">
216
216
  <div class="brand">
217
- ${Ys(38)}
217
+ ${In(38)}
218
218
  <div>
219
219
  <div class="brand-name">saffron</div>
220
220
  <div class="brand-sub">test run report</div>
221
221
  </div>
222
222
  </div>
223
223
  <div class="run-meta">
224
- <span>${D(o)}</span>
224
+ <span>${N(o)}</span>
225
225
  <span>\xB7</span>
226
- <span>${Tn(s)}</span>
227
- ${e.baseURL?`<span class="chip base">${D(e.baseURL)}</span>`:""}
226
+ <span>${Ge(s)}</span>
227
+ ${e.baseURL?`<span class="chip base">${N(e.baseURL)}</span>`:""}
228
228
  ${e.shards?`<span class="chip">merged from ${e.shards} shards</span>`:""}
229
- ${sf(e)}
229
+ ${Sf(e)}
230
230
  </div>
231
231
  </header>
232
232
 
233
- ${cf(e,s)}
234
- ${lf(e)}
235
- ${af(e)}
233
+ ${kf(e,s)}
234
+ ${Tf(e)}
235
+ ${$f(e)}
236
236
 
237
237
  <div class="section-head">
238
238
  <span class="section-title">scenarios \xB7 ${n.scenarios}</span>
239
- <span class="section-title">saffron v${D(e.version)}</span>
239
+ <span class="section-title">saffron v${N(e.version)}</span>
240
240
  </div>
241
- ${e.scenarios.map(i=>gf(i,!!e.totals.plan,t)).join(`
241
+ ${e.scenarios.map(i=>If(i,!!e.totals.plan,t)).join(`
242
242
  `)}
243
243
 
244
- <footer>${rf(11)} generated by saffron v${D(e.version)} \xB7 ${D(new Date(e.finishedAt).toLocaleString("en-GB",{dateStyle:"medium",timeStyle:"medium"}))}</footer>
244
+ <footer>${oo(11)} generated by saffron v${N(e.version)} \xB7 ${N(new Date(e.finishedAt).toLocaleString("en-GB",{dateStyle:"medium",timeStyle:"medium"}))}</footer>
245
245
  </div>
246
246
  </body>
247
247
  </html>
248
- `}import Zs from"node:fs";import Qs from"node:path";function Xs(e){let t={scenarios:e.length,green:0,yellow:0,red:0,aiCalls:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreationTokens:0,costUsd:0},n=new Set;for(let r of e){for(let s of r.usage.models??[])n.add(s);t[r.status]+=1,t.aiCalls+=r.usage.aiCalls,t.inputTokens+=r.usage.inputTokens,t.outputTokens+=r.usage.outputTokens,t.cacheReadTokens+=r.usage.cacheReadTokens,t.cacheCreationTokens+=r.usage.cacheCreationTokens??0,t.costUsd+=r.usage.costUsd??0}return n.size>0&&(t.models=[...n].sort()),t}function eo(e,t,n){return{tool:"saffron",version:Ce(),startedAt:t.toISOString(),finishedAt:new Date().toISOString(),baseURL:n,totals:Xs(e),scenarios:e.map(r=>({feature:r.scenario.featurePath,scenario:r.scenario.displayName,tags:[...r.scenario.tags],status:r.status,source:r.source,proposalReplay:r.proposalReplay,durationMs:r.durationMs,usage:r.usage,steps:r.stepResults,narrative:r.narrative,adaptations:r.adaptations,suggestedFeatureEdit:r.suggestedFeatureEdit,featureEdits:r.featureEdits,verified:r.verified,proofError:r.proofError,proposalFile:r.proposalFile,proposalRevision:r.proposalRevision,proposalDiff:r.proposalDiff,error:r.error,evidence:r.evidence,uniques:r.uniques,proofUniques:r.proofUniques}))}}function Rn(e,t){let n=Qs.join(e,".saffron","reports");Zs.mkdirSync(n,{recursive:!0});let r=Qs.join(n,"latest.json"),s=Qs.join(n,"latest.html");return Zs.writeFileSync(r,`${JSON.stringify(t,null,2)}
249
- `),Zs.writeFileSync(s,Vt(t,e)),{json:r,html:s}}import yf from"node:crypto";import En from"node:fs";import yr from"node:path";var $a=["scenario","file"];function ka(e){let t=/^\s*(\d+)\s*\/\s*(\d+)\s*$/.exec(e),n=t?Number(t[1]):NaN,r=t?Number(t[2]):NaN;if(!t||r<1||n<1||n>r)throw new Error(`invalid --shard "${e}": use <index>/<total>, e.g. --shard 2/4 (index from 1 to total)`);return{index:n,total:r}}function Ta(e){if(!$a.includes(e))throw new Error(`invalid --shard-by "${e}": use ${$a.map(t=>`"${t}"`).join(" or ")}`);return e}function to(e){return`shard-${e.index}-of-${e.total}`}var bf=e=>`${e.featurePath}${e.displayName}`;function Ra(e,t){let n=t.by??"scenario",r=e.map(bf),s=new Set;if(n==="file"){let a=new Map;e.forEach((d,f)=>a.set(d.featurePath,[...a.get(d.featurePath)??[],f]));let c=new Array(t.total).fill(0),l=[...a].sort((d,f)=>f[1].length-d[1].length||(d[0]<f[0]?-1:d[0]>f[0]?1:0));for(let[,d]of l){let f=c.indexOf(Math.min(...c));if(c[f]+=d.length,f===t.index-1)for(let m of d)s.add(m)}}else e.map((c,l)=>l).sort((c,l)=>r[c]<r[l]?-1:r[c]>r[l]?1:0).forEach((c,l)=>{l%t.total===t.index-1&&s.add(c)});let o=e.map((a,c)=>c).filter(a=>s.has(a)),i=yf.createHash("sha256").update([...r].sort().join(`
250
- `)).digest("hex").slice(0,12);return{scenarios:o.map(a=>e[a]),info:{index:t.index,total:t.total,by:n,suite:i,suiteSize:e.length,positions:o}}}function br(e=process.env){return e.SAFFRON_RUN_ID||e.GITHUB_RUN_ID&&`github:${e.GITHUB_RUN_ID}`||e.CI_PIPELINE_ID&&`gitlab:${e.CI_PIPELINE_ID}`||e.BUILD_BUILDID&&`azure:${e.BUILD_BUILDID}`||e.CIRCLE_WORKFLOW_ID&&`circleci:${e.CIRCLE_WORKFLOW_ID}`||void 0}function no(e,t){let n=yr.join(e,".saffron","reports");En.mkdirSync(n,{recursive:!0});let r=yr.join(n,`${to(t.shard)}.json`);return En.writeFileSync(r,`${JSON.stringify(t,null,2)}
251
- `),r}var wr=e=>{let t=e;return!!t&&t.tool==="saffron"&&!!t.shard&&typeof t.shard.index=="number"&&typeof t.shard.total=="number"&&Array.isArray(t.shard.positions)&&Array.isArray(t.scenarios)&&!!t.history&&Array.isArray(t.history.scenarios)},wf=/^shard-\d+-of-\d+\.json$/;function Ea(e){let t=[],n=(r,s)=>{let o=En.statSync(r,{throwIfNoEntry:!1});if(!o)throw new Error(`not found: ${r}`);if(o.isDirectory())for(let i of En.readdirSync(r).sort())n(yr.join(r,i),!1);else(s||wf.test(yr.basename(r)))&&t.push(r)};for(let r of e)n(r,!0);return t.map(r=>{let s;try{s=JSON.parse(En.readFileSync(r,"utf8"))}catch(o){throw new Error(`cannot read shard report ${r}: ${o instanceof Error?o.message:String(o)}`)}if(!wr(s))throw new Error(`${r} is not a shard report (written by saffron run --shard)`);return{file:r,report:s}})}var Kt=class extends Error{};function Sr(e,t={}){if(e.length===0)throw new Kt("no shard reports found. Shards write .saffron/reports/shard-<i>-of-<n>.json");let n=e[0].report.shard,r=[],s=h=>h.map(S=>S.file).join(", "),o=new Set(e.map(h=>h.report.shard.total));o.size>1&&r.push(`shards disagree on the shard count (${[...o].sort().join(", ")}): ${s(e)}`),new Set(e.map(h=>`${h.report.shard.suite}/${h.report.shard.suiteSize}`)).size>1&&r.push("shards ran different suites (different scenarios or filters). Every shard must run the same paths and --filter");let a=new Set(e.map(h=>h.report.shard.by??"scenario"));a.size>1&&r.push(`shards split the suite in different ways (--shard-by ${[...a].sort().join(" and ")}): every shard must use the same`);let c=new Set(e.map(h=>h.report.shard.ciRun??""));c.size>1&&r.push(`shards come from different CI runs (${[...c].map(h=>h||"none recorded").join(", ")})`);let l=new Map;for(let h of e)l.set(h.report.shard.index,[...l.get(h.report.shard.index)??[],h]);for(let[h,S]of[...l].sort(($,u)=>$[0]-u[0]))S.length>1&&r.push(`shard ${h}/${n.total} appears ${S.length} times: ${s(S)}`);if(o.size===1&&!t.partial){let h=Array.from({length:n.total},(S,$)=>$+1).filter(S=>!l.has(S));h.length>0&&r.push(`missing ${h.length===1?"shard":"shards"} ${h.map(S=>`${S}/${n.total}`).join(", ")}. A merge without ${h.length===1?"it":"them"} would under-count the run`)}if(r.length===0){let h=new Set,S=0;for(let u of e){let{positions:g}=u.report.shard;(g.length!==u.report.scenarios.length||g.length!==u.report.history.scenarios.length)&&r.push(`${u.file} is inconsistent: ${g.length} positions for ${u.report.scenarios.length} scenarios`);for(let R of g)h.has(R)&&r.push(`two shards both ran scenario ${R+1} of the suite`),h.add(R);S+=g.length}let $=[...new Set(e.map(u=>u.report.baseURL??"(no base URL)"))];$.length>1&&r.push(`the shards ran against different base URLs: ${$.join(", ")}. Merge the shards of one environment at a time`),r.length===0&&!t.partial&&S!==n.suiteSize&&r.push(`the shards hold ${S} scenarios but the suite has ${n.suiteSize}`)}if(r.length>0)throw new Kt(r.join(`
252
- `));let d=e.sort((h,S)=>h.report.shard.index-S.report.shard.index).flatMap(({report:h})=>h.shard.positions.map((S,$)=>({position:S,scenario:h.scenarios[$],history:h.history.scenarios[$]}))).sort((h,S)=>h.position-S.position),f=e.map(h=>h.report),m=f.map(h=>h.startedAt).sort()[0],p=f.map(h=>h.finishedAt).sort().at(-1),b=d.map(h=>h.scenario),v=f.flatMap(h=>h.hooks??[]),k={tool:"saffron",version:f[0].version,startedAt:m,finishedAt:p,baseURL:f.find(h=>h.baseURL)?.baseURL,totals:Xs(b),scenarios:b,shards:e.length};v.length>0&&(k.hooks=v);let w=d.map(h=>h.history),y={runId:n.ciRun?`${n.ciRun}:${n.suite}:${k.baseURL??""}`:m,startedAt:m,finishedAt:p,baseURL:k.baseURL,green:w.filter(h=>h.status==="green").length,yellow:w.filter(h=>h.status==="yellow").length,red:w.filter(h=>h.status==="red").length,aiCalls:w.reduce((h,S)=>h+S.aiCalls,0),costUsd:w.reduce((h,S)=>h+S.costUsd,0),divergentSteps:Math.max(...f.map(h=>h.history.divergentSteps)),scenarios:w};return{report:k,history:y}}var xr=1,U=class extends Error{constructor(t){super(Te(t,1/0))}},kt=e=>e instanceof Error?e.message:String(e);function Te(e,t=200){let n=e.replace(/\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?|\x1b[@-_]?/g,"").replace(/[\x00-\x1f\x7f-\x9f]/g,"");return n.length>t?`${n.slice(0,t-3)}...`:n}var so=(e,t)=>V.relative(e,t).split(V.sep).join("/");function nt(e,t){try{return Sf("git",t,{cwd:e,encoding:"utf8",stdio:["ignore","pipe","ignore"],timeout:1e4}).trim()||void 0}catch{return}}function Fa(e){let t=e.trim();if(/^[a-z]:[\\/]/i.test(t))return;let n,r;if(/^[a-z][a-z0-9+.-]*:\/\//i.test(t)){let o;try{o=new URL(t)}catch{return}if(o.protocol==="file:")return;n=o.hostname,r=o.pathname}else{let o=/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/.exec(t);if(!o)return;n=o[1],r=o[2]}try{r=decodeURIComponent(r)}catch{}r=r.replace(/^\/+|\/+$/g,"").replace(/\.git$/i,""),n=n.toLowerCase();let s=/^v3\/([^/]+)\/([^/]+)\/([^/]+)$/.exec(r);return s?(n="dev.azure.com",r=`${s[1]}/${s[2]}/_git/${s[3]}`):n.endsWith(".visualstudio.com")&&(r=`${n.slice(0,-17)}/${r.replace(/^DefaultCollection\//i,"")}`,n="dev.azure.com"),r?`${n}/${r}`.toLowerCase():void 0}function $r(e){let n=(nt(e,["remote","-v"])?.split(`
253
- `)??[]).map(r=>r.split(/\s+/)[1]).filter(Boolean);return[...new Set(n.map(Fa).filter(r=>r!==void 0))]}var Aa=e=>e?.replace(/^refs\/heads\//,"")||void 0,Ma=e=>e.GITHUB_EVENT_NAME==="pull_request"||!!e.GITHUB_EVENT_NAME?.startsWith("pull_request_review");function $f(e){if(!e.GITHUB_EVENT_PATH||!Ma(e))return{};let t=n=>typeof n=="string"&&n?n:void 0;try{let n=JSON.parse(G.readFileSync(e.GITHUB_EVENT_PATH,"utf8"))?.pull_request?.head;return{sha:t(n?.sha),ref:t(n?.ref)}}catch{return{}}}function ao(e,t){return[...(nt(e,["cat-file","-p",t])??"").split(`
248
+ `}import co from"node:fs";import lo from"node:path";function po(e){let t={scenarios:e.length,green:0,yellow:0,red:0,aiCalls:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreationTokens:0,costUsd:0},n=new Set;for(let r of e){for(let s of r.usage.models??[])n.add(s);t[r.status]+=1,t.aiCalls+=r.usage.aiCalls,t.inputTokens+=r.usage.inputTokens,t.outputTokens+=r.usage.outputTokens,t.cacheReadTokens+=r.usage.cacheReadTokens,t.cacheCreationTokens+=r.usage.cacheCreationTokens??0,t.costUsd+=r.usage.costUsd??0}return n.size>0&&(t.models=[...n].sort()),t}function fo(e,t,n){return{tool:"saffron",version:Oe(),startedAt:t.toISOString(),finishedAt:new Date().toISOString(),baseURL:n,totals:po(e),scenarios:e.map(r=>({feature:r.scenario.featurePath,scenario:r.scenario.displayName,tags:[...r.scenario.tags],status:r.status,source:r.source,proposalReplay:r.proposalReplay,durationMs:r.durationMs,usage:r.usage,steps:r.stepResults,narrative:r.narrative,adaptations:r.adaptations,suggestedFeatureEdit:r.suggestedFeatureEdit,featureEdits:r.featureEdits,verified:r.verified,proofError:r.proofError,proposalFile:r.proposalFile,proposalRevision:r.proposalRevision,proposalDiff:r.proposalDiff,error:r.error,evidence:r.evidence,uniques:r.uniques,proofUniques:r.proofUniques}))}}function Pn(e,t){let n=lo.join(e,".saffron","reports");co.mkdirSync(n,{recursive:!0});let r=lo.join(n,"latest.json"),s=lo.join(n,"latest.html");return co.writeFileSync(r,`${JSON.stringify(t,null,2)}
249
+ `),co.writeFileSync(s,Zt(t,e)),{json:r,html:s}}import Of from"node:crypto";import Cn from"node:fs";import Tr from"node:path";var Ma=["scenario","file"];function Da(e){let t=/^\s*(\d+)\s*\/\s*(\d+)\s*$/.exec(e),n=t?Number(t[1]):NaN,r=t?Number(t[2]):NaN;if(!t||r<1||n<1||n>r)throw new Error(`invalid --shard "${e}": use <index>/<total>, e.g. --shard 2/4 (index from 1 to total)`);return{index:n,total:r}}function La(e){if(!Ma.includes(e))throw new Error(`invalid --shard-by "${e}": use ${Ma.map(t=>`"${t}"`).join(" or ")}`);return e}function uo(e){return`shard-${e.index}-of-${e.total}`}var jf=e=>`${e.featurePath}${e.displayName}`;function Ua(e,t){let n=t.by??"scenario",r=e.map(jf),s=new Set;if(n==="file"){let a=new Map;e.forEach((d,f)=>a.set(d.featurePath,[...a.get(d.featurePath)??[],f]));let c=new Array(t.total).fill(0),l=[...a].sort((d,f)=>f[1].length-d[1].length||(d[0]<f[0]?-1:d[0]>f[0]?1:0));for(let[,d]of l){let f=c.indexOf(Math.min(...c));if(c[f]+=d.length,f===t.index-1)for(let m of d)s.add(m)}}else e.map((c,l)=>l).sort((c,l)=>r[c]<r[l]?-1:r[c]>r[l]?1:0).forEach((c,l)=>{l%t.total===t.index-1&&s.add(c)});let o=e.map((a,c)=>c).filter(a=>s.has(a)),i=Of.createHash("sha256").update([...r].sort().join(`
250
+ `)).digest("hex").slice(0,12);return{scenarios:o.map(a=>e[a]),info:{index:t.index,total:t.total,by:n,suite:i,suiteSize:e.length,positions:o}}}function Rr(e=process.env){return e.SAFFRON_RUN_ID||e.GITHUB_RUN_ID&&`github:${e.GITHUB_RUN_ID}`||e.CI_PIPELINE_ID&&`gitlab:${e.CI_PIPELINE_ID}`||e.BUILD_BUILDID&&`azure:${e.BUILD_BUILDID}`||e.CIRCLE_WORKFLOW_ID&&`circleci:${e.CIRCLE_WORKFLOW_ID}`||void 0}function go(e,t){let n=Tr.join(e,".saffron","reports");Cn.mkdirSync(n,{recursive:!0});let r=Tr.join(n,`${uo(t.shard)}.json`);return Cn.writeFileSync(r,`${JSON.stringify(t,null,2)}
251
+ `),r}var Er=e=>{let t=e;return!!t&&t.tool==="saffron"&&!!t.shard&&typeof t.shard.index=="number"&&typeof t.shard.total=="number"&&Array.isArray(t.shard.positions)&&Array.isArray(t.scenarios)&&!!t.history&&Array.isArray(t.history.scenarios)},Nf=/^shard-\d+-of-\d+\.json$/;function Ba(e){let t=[],n=(r,s)=>{let o=Cn.statSync(r,{throwIfNoEntry:!1});if(!o)throw new Error(`not found: ${r}`);if(o.isDirectory())for(let i of Cn.readdirSync(r).sort())n(Tr.join(r,i),!1);else(s||Nf.test(Tr.basename(r)))&&t.push(r)};for(let r of e)n(r,!0);return t.map(r=>{let s;try{s=JSON.parse(Cn.readFileSync(r,"utf8"))}catch(o){throw new Error(`cannot read shard report ${r}: ${o instanceof Error?o.message:String(o)}`)}if(!Er(s))throw new Error(`${r} is not a shard report (written by saffron run --shard)`);return{file:r,report:s}})}var Xt=class extends Error{};function Ar(e,t={}){if(e.length===0)throw new Xt("no shard reports found. Shards write .saffron/reports/shard-<i>-of-<n>.json");let n=e[0].report.shard,r=[],s=u=>u.map(v=>v.file).join(", "),o=new Set(e.map(u=>u.report.shard.total));o.size>1&&r.push(`shards disagree on the shard count (${[...o].sort().join(", ")}): ${s(e)}`),new Set(e.map(u=>`${u.report.shard.suite}/${u.report.shard.suiteSize}`)).size>1&&r.push("shards ran different suites (different scenarios or filters). Every shard must run the same paths and --filter");let a=new Set(e.map(u=>u.report.shard.by??"scenario"));a.size>1&&r.push(`shards split the suite in different ways (--shard-by ${[...a].sort().join(" and ")}): every shard must use the same`);let c=new Set(e.map(u=>u.report.shard.ciRun??""));c.size>1&&r.push(`shards come from different CI runs (${[...c].map(u=>u||"none recorded").join(", ")})`);let l=new Map;for(let u of e)l.set(u.report.shard.index,[...l.get(u.report.shard.index)??[],u]);for(let[u,v]of[...l].sort((S,h)=>S[0]-h[0]))v.length>1&&r.push(`shard ${u}/${n.total} appears ${v.length} times: ${s(v)}`);if(o.size===1&&!t.partial){let u=Array.from({length:n.total},(v,S)=>S+1).filter(v=>!l.has(v));u.length>0&&r.push(`missing ${u.length===1?"shard":"shards"} ${u.map(v=>`${v}/${n.total}`).join(", ")}. A merge without ${u.length===1?"it":"them"} would under-count the run`)}if(r.length===0){let u=new Set,v=0;for(let h of e){let{positions:g}=h.report.shard;(g.length!==h.report.scenarios.length||g.length!==h.report.history.scenarios.length)&&r.push(`${h.file} is inconsistent: ${g.length} positions for ${h.report.scenarios.length} scenarios`);for(let T of g)u.has(T)&&r.push(`two shards both ran scenario ${T+1} of the suite`),u.add(T);v+=g.length}let S=[...new Set(e.map(h=>h.report.baseURL??"(no base URL)"))];S.length>1&&r.push(`the shards ran against different base URLs: ${S.join(", ")}. Merge the shards of one environment at a time`),r.length===0&&!t.partial&&v!==n.suiteSize&&r.push(`the shards hold ${v} scenarios but the suite has ${n.suiteSize}`)}if(r.length>0)throw new Xt(r.join(`
252
+ `));let d=e.sort((u,v)=>u.report.shard.index-v.report.shard.index).flatMap(({report:u})=>u.shard.positions.map((v,S)=>({position:v,scenario:u.scenarios[S],history:u.history.scenarios[S]}))).sort((u,v)=>u.position-v.position),f=e.map(u=>u.report),m=f.map(u=>u.startedAt).sort()[0],p=f.map(u=>u.finishedAt).sort().at(-1),b=d.map(u=>u.scenario),$=f.flatMap(u=>u.hooks??[]),k={tool:"saffron",version:f[0].version,startedAt:m,finishedAt:p,baseURL:f.find(u=>u.baseURL)?.baseURL,totals:po(b),scenarios:b,shards:e.length};$.length>0&&(k.hooks=$);let w=d.map(u=>u.history),y={runId:n.ciRun?`${n.ciRun}:${n.suite}:${k.baseURL??""}`:m,startedAt:m,finishedAt:p,baseURL:k.baseURL,green:w.filter(u=>u.status==="green").length,yellow:w.filter(u=>u.status==="yellow").length,red:w.filter(u=>u.status==="red").length,aiCalls:w.reduce((u,v)=>u+v.aiCalls,0),costUsd:w.reduce((u,v)=>u+v.costUsd,0),divergentSteps:Math.max(...f.map(u=>u.history.divergentSteps)),scenarios:w};return{report:k,history:y}}var _r=1,L=class extends Error{constructor(t){super(Re(t,1/0))}},Rt=e=>e instanceof Error?e.message:String(e);function Re(e,t=200){let n=e.replace(/\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?|\x1b[@-_]?/g,"").replace(/[\x00-\x1f\x7f-\x9f]/g,"");return n.length>t?`${n.slice(0,t-3)}...`:n}var mo=(e,t)=>V.relative(e,t).split(V.sep).join("/");function rt(e,t){try{return Ff("git",t,{cwd:e,encoding:"utf8",stdio:["ignore","pipe","ignore"],timeout:1e4}).trim()||void 0}catch{return}}function Ya(e){let t=e.trim();if(/^[a-z]:[\\/]/i.test(t))return;let n,r;if(/^[a-z][a-z0-9+.-]*:\/\//i.test(t)){let o;try{o=new URL(t)}catch{return}if(o.protocol==="file:")return;n=o.hostname,r=o.pathname}else{let o=/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/.exec(t);if(!o)return;n=o[1],r=o[2]}try{r=decodeURIComponent(r)}catch{}r=r.replace(/^\/+|\/+$/g,"").replace(/\.git$/i,""),n=n.toLowerCase();let s=/^v3\/([^/]+)\/([^/]+)\/([^/]+)$/.exec(r);return s?(n="dev.azure.com",r=`${s[1]}/${s[2]}/_git/${s[3]}`):n.endsWith(".visualstudio.com")&&(r=`${n.slice(0,-17)}/${r.replace(/^DefaultCollection\//i,"")}`,n="dev.azure.com"),r?`${n}/${r}`.toLowerCase():void 0}function Pr(e){let n=(rt(e,["remote","-v"])?.split(`
253
+ `)??[]).map(r=>r.split(/\s+/)[1]).filter(Boolean);return[...new Set(n.map(Ya).filter(r=>r!==void 0))]}var za=e=>e?.replace(/^refs\/heads\//,"")||void 0,Qa=e=>e.GITHUB_EVENT_NAME==="pull_request"||!!e.GITHUB_EVENT_NAME?.startsWith("pull_request_review");function Lf(e){if(!e.GITHUB_EVENT_PATH||!Qa(e))return{};let t=n=>typeof n=="string"&&n?n:void 0;try{let n=JSON.parse(G.readFileSync(e.GITHUB_EVENT_PATH,"utf8"))?.pull_request?.head;return{sha:t(n?.sha),ref:t(n?.ref)}}catch{return{}}}function wo(e,t){return[...(rt(e,["cat-file","-p",t])??"").split(`
254
254
 
255
- `)[0].matchAll(/^parent ([0-9a-f]+)$/gm)].map(r=>r[1])}function kf(e,t=process.env){let n=nt(e,["remote"])?.split(`
256
- `)??[],r=n.includes("origin")?"origin":n[0],s=t.GITHUB_SERVER_URL&&t.GITHUB_REPOSITORY?`${t.GITHUB_SERVER_URL}/${t.GITHUB_REPOSITORY}`:void 0,o=[r&&nt(e,["remote","get-url",r]),s,t.BUILD_REPOSITORY_URI].map(b=>b?Fa(b):void 0).find(Boolean),i=nt(e,["rev-parse","--abbrev-ref","HEAD"]),a=$f(t),c=(Ma(t)?t.GITHUB_HEAD_REF||a.ref:void 0)||(t.GITHUB_EVENT_NAME==="pull_request_target"?t.GITHUB_BASE_REF:void 0)||(t.GITHUB_REF_TYPE==="tag"?void 0:t.GITHUB_REF_NAME)||Aa(t.SYSTEM_PULLREQUEST_SOURCEBRANCH)||(t.BUILD_SOURCEBRANCH?.startsWith("refs/heads/")?Aa(t.BUILD_SOURCEBRANCH):void 0)||(i!=="HEAD"?i:void 0),l=nt(e,["rev-parse","HEAD"])||t.GITHUB_SHA||t.BUILD_SOURCEVERSION,d=a.sha||t.SYSTEM_PULLREQUEST_SOURCECOMMITID,f=d&&l&&ao(e,l)[1]===d?d:void 0,m=t.GITHUB_SERVER_URL&&t.GITHUB_REPOSITORY&&t.GITHUB_RUN_ID?`${t.GITHUB_SERVER_URL}/${t.GITHUB_REPOSITORY}/actions/runs/${t.GITHUB_RUN_ID}`:t.SYSTEM_COLLECTIONURI&&t.SYSTEM_TEAMPROJECT&&t.BUILD_BUILDID?`${t.SYSTEM_COLLECTIONURI.replace(/\/?$/,"/")}${encodeURIComponent(t.SYSTEM_TEAMPROJECT)}/_build/results?buildId=${t.BUILD_BUILDID}`:void 0,p={repository:o,branch:c,commit:l,headCommit:f&&f!==l?f:void 0,run:t.GITHUB_RUN_ID&&`github:${t.GITHUB_RUN_ID}`||t.BUILD_BUILDID&&`azure:${t.BUILD_BUILDID}`||br(t),attempt:t.GITHUB_RUN_ATTEMPT||t.SYSTEM_JOBATTEMPT||void 0,url:m};return Object.fromEntries(Object.entries(p).filter(([,b])=>b!==void 0))}function co(e){let t=e.headCommit??e.commit;return[e.run?`run ${e.run}${e.attempt&&e.attempt!=="1"?` (attempt ${e.attempt})`:""}`:"a local run",e.branch||t?`${e.branch??"detached"}${t?` @ ${t.slice(0,7)}`:""}`:void 0,e.repository].filter(Boolean).join(" \xB7 ")}var Tf=/^(latest|shard-\d+-of-\d+)\.json$/,Yt=e=>!!e&&typeof e=="object"&&!Array.isArray(e),_a=(e,t)=>t.find(n=>typeof e[n]!="number"),Rf=["scenarios","green","yellow","red","aiCalls","inputTokens","outputTokens","cacheReadTokens","costUsd"],Ef=["aiCalls","inputTokens","outputTokens","cacheReadTokens"],Ia=e=>typeof e=="string"&&/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(Z|[+-]\d\d:\d\d)$/.test(e)&&!Number.isNaN(Date.parse(e));function Da(e){if(!Yt(e)||e.tool!=="saffron")return"is not a Saffron run report";if(!Ia(e.startedAt)||!Ia(e.finishedAt))return"has no start or finish time";if(!Yt(e.totals))return"has no totals";let t=_a(e.totals,Rf);if(t)return`has totals.${t} that is not a number`;if(!Array.isArray(e.scenarios))return"has no list of scenarios";for(let[n,r]of e.scenarios.entries()){let s=`scenarios[${n}]`;if(!Yt(r)||typeof r.feature!="string"||typeof r.scenario!="string")return`has ${s} without a feature and scenario name`;if(!["green","yellow","red"].includes(r.status))return`has ${s}.status ${JSON.stringify(r.status)}, which this saffron-ai does not know`;if(!Yt(r.usage)||_a(r.usage,Ef))return`has ${s}.usage without its token counts`;if(!Array.isArray(r.steps))return`has ${s}.steps that is not a list`;let o=r.steps.findIndex(i=>!Yt(i)||typeof i.gherkin!="string"||typeof i.kind!="string"||typeof i.status!="string");if(o>=0)return`has ${s}.steps[${o}] without its gherkin, kind and status`;if(r.evidence!==void 0&&!(Array.isArray(r.evidence)&&r.evidence.every(i=>Yt(i)&&typeof i.file=="string"&&typeof i.kind=="string")))return`has ${s}.evidence that is not a list of files`}}var Af=e=>Da(e)===void 0;function _f(e){let t=V.join(e,".saffron","reports"),n;try{n=G.readdirSync(t).filter(s=>Tf.test(s)).sort()}catch{return}let r;for(let s of n)try{let o=G.readFileSync(V.join(t,s)),i=JSON.parse(o.toString("utf8"));Af(i)&&(!r||i.finishedAt>r.report.finishedAt)&&(r={bytes:o,report:i})}catch{}return r}function If(e,t){return V.join(e,".saffron","reports",t?`saffron-bundle-${to(t)}.zip`:"saffron-bundle.zip")}var Pa=e=>`${(e/1024**3).toFixed(1)} GB`,Ca='Keep fewer traces ("trace": "retain-on-failure" in saffron.config.json, or --trace retain-on-failure) and run again';function La(e,t={}){let n=[],r=[],s=[],o=[],i=V.join(e,".saffron","proposals");for(let{file:b,proposal:v,revision:k,bytes:w}of He(e,(x,y)=>n.push({file:so(e,x),problem:y}))){let x=`proposals/${so(i,b)}`;r.push({name:x,data:w}),s.push({entry:x,feature:v.cache.feature,scenario:v.cache.scenario,revision:k,bound:v.meta.bound}),o.push({file:b,proposal:v,bytes:w})}let a=_f(e),c=[],l=[],d=[];if(a){r.push({name:"report.json",data:a.bytes});let b=[],v=new Set;for(let w of a.report.scenarios)for(let x of w.evidence??[]){if(v.has(x.file))continue;v.add(x.file);let y=x.file.startsWith(".saffron/artifacts/")?G.statSync(V.join(e,x.file),{throwIfNoEntry:!1}):void 0;if(!y?.isFile()){l.push(x.file);continue}let h=w.status==="green"&&x.kind==="trace";if(y.size>oo){if(h){d.push(x.file);continue}throw new U(`${x.file} is ${Pa(y.size)}, over the 1 GB a bundle holds per file. ${Ca}`)}b.push({e:x,size:y.size,greenTrace:h})}let k=()=>r.reduce((w,x)=>w+("data"in x?x.data.length:x.size??0)+1024,0)+b.reduce((w,x)=>w+x.size+1024,0);if(k()>Oa)for(let w of b.filter(x=>x.greenTrace))b.splice(b.indexOf(w),1),d.push(w.e.file);if(k()>Oa)throw new U(`the bundle would hold ${Pa(k())}${d.length?" even without the traces of green scenarios":""}, over the 4 GB one bundle holds. ${Ca}, or split the run into shards (--shard)`);for(let{e:w}of b){let x=V.join(e,w.file),y;try{y=at(V.join(e,".saffron","artifacts"),x)?pr(x):void 0}catch{y=void 0}if(!y||w.sha256&&y.sha256!==w.sha256){l.push(w.file);continue}let h=w.file.slice(9);r.push({name:h,file:x,crc:y.crc,size:y.size}),c.push({entry:h,sha256:y.sha256})}}if(!a&&s.length===0)throw new U("nothing to export: no run report and no pending proposals under .saffron. Run `saffron run` first.");let f=a&&wr(a.report)?{index:a.report.shard.index,total:a.report.shard.total}:void 0,m={tool:"saffron",format:xr,runner:Ce(),createdAt:new Date().toISOString(),...kf(e,t.env),...f?{shard:f}:{},...a?{report:"report.json"}:{},proposals:s,evidence:c},p=t.out??If(e,f);if(G.existsSync(p)&&G.statSync(p).isDirectory())throw new U(`${p} is a folder: give --out a file name, such as ${V.join(p,"saffron-bundle.zip")}`);return Ki(p,[{name:"manifest.json",data:Buffer.from(`${JSON.stringify(m,null,2)}
257
- `)},...r]),{file:p,manifest:m,proposals:o,missing:l,dropped:d,unreadable:n}}var vr="[a-z0-9-]+",Ua=new RegExp(`^proposals/${vr}/${vr}\\.json$`),Pf=new RegExp(`^artifacts/${vr}/${vr}/[A-Za-z0-9][A-Za-z0-9._-]*$`),oo=1024**3,lo=8*1024**3,Cf="the bundles to import add up to more than 8 GB unzipped",Oa=Math.min(lo,ct),ja=100,Of=1024**2,jf=6e5;function Nf(e){try{return process.kill(e,0),!0}catch(t){return t.code==="EPERM"}}function po(e){let t=V.join(e,".saffron","reports");for(let o of G.existsSync(t)?G.readdirSync(t):[]){let i=Number(/^run-download-(\d+)-/.exec(o)?.[1]),a=V.join(t,o),c=i?G.statSync(a,{throwIfNoEntry:!1}):void 0;c&&Date.now()-c.mtimeMs>36e5&&!Nf(i)&&G.rmSync(a,{recursive:!0,force:!0})}let n=V.join(t,`run-download-${process.pid}-${io(4).toString("hex")}`),r=G.mkdirSync(n,{recursive:!0});return{dir:n,remove:()=>{G.rmSync(n,{recursive:!0,force:!0});for(let o=t;r&&o.startsWith(r);o=V.dirname(o))try{G.rmdirSync(o)}catch{break}}}}async function Ff(e,t){let n=r=>new U(r?.name==="TimeoutError"?`cannot download ${e}: it did not finish within 10 minutes`:`cannot download ${e}: ${kt(r)}`);try{let r=await fetch(e,{redirect:"follow",signal:AbortSignal.timeout(jf)});if(!r.ok||!r.body)throw new U(`cannot download ${e}: HTTP ${r.status}`);let s=G.openSync(t,"w");try{let o=0;for await(let i of r.body){if(o+=i.length,o>lo)throw new U(`cannot download ${e}: it is over 8 GB`);G.writeSync(s,i)}}finally{G.closeSync(s)}}catch(r){throw r instanceof U?r:n(r)}}async function Mf(e,t){if(/^https?:\/\//i.test(e)){let s=V.join(t(),`url-${io(4).toString("hex")}.zip`);return await Ff(e,s),[{source:e,file:s}]}let n=G.statSync(e,{throwIfNoEntry:!1});if(!n)throw new U(`not found: ${e}`);let r=n.isDirectory()?G.readdirSync(e,{recursive:!0}).map(String).filter(s=>/^saffron-bundle.*\.zip$/.test(V.basename(s))).sort().map(s=>V.join(e,s)):[e];if(r.length===0)throw new U(`no saffron-bundle*.zip under ${e}`);return r.map(s=>({source:s,file:s}))}function Df(e,t){try{return{source:e,file:t,entries:new Map(Bi(t).map(n=>[n.name,n]))}}catch(n){throw new U(`${e} is not a Saffron bundle: ${kt(n)}`)}}var Lf=1024*1024;function Na(e,t,n){let r=xf("sha256"),s=G.openSync(e,"r");try{let o=Buffer.alloc(Math.max(1,Math.min(Lf,t.size)));for(let i=0;i<t.size;){let a=G.readSync(s,o,0,Math.min(o.length,t.size-i),t.start+i);if(a===0)throw new Error("the file ends early");let c=o.subarray(0,a);r.update(c),n?.(c),i+=a}}finally{G.closeSync(s)}return r.digest("hex")}function Uf(e){try{return pr(e).sha256}catch{return}}function Bf({source:e,file:t,entries:n}){let r=p=>new U(`${e} ${p}`);for(let p of n.values()){if(p.size>oo||p.compressedSize>oo)throw r(`is damaged: ${Te(p.name)} is too large`);if(p.method!==0&&p.size>Of&&p.size>ja*p.compressedSize)throw r(`is damaged: ${Te(p.name)} claims to unzip to over ${ja} times its size`)}let s=p=>{let b=n.get(p);if(!b)throw r(`is damaged: ${p} is missing`);let v;try{let k=b.raw();if(k.length!==b.compressedSize)throw new Error("the file ends early");v=b.method===0?k:vf.inflateRawSync(k,{maxOutputLength:Math.max(b.size,1)})}catch(k){throw r(`is damaged: ${p} cannot be read (${kt(k)})`)}if(v.length!==b.size)throw r(`is damaged: ${p} is truncated`);return v},o=p=>{let b=n.get(p);if(!(!b||b.method!==0)){if(b.compressedSize!==b.size)throw r(`is damaged: ${p} is truncated`);return b}},i=p=>{let b=o(p);if(!b)return Oe(s(p));try{return Na(t,b)}catch(v){throw r(`is damaged: ${p} cannot be read (${kt(v)})`)}},a=(p,b,v)=>{let k=o(p),w=()=>new Error(Te(`${e} is damaged: ${p} changed while it was imported`,1/0));if(!k){let h=s(p);if(Oe(h)!==v)throw w();Ee(b,h);return}G.mkdirSync(V.dirname(b),{recursive:!0});let x=`${b}.${process.pid}-${io(4).toString("hex")}.partial`,y=G.openSync(x,"w");try{let h;try{h=Na(t,k,S=>{for(let $=0;$<S.length;)$+=G.writeSync(y,S,$,S.length-$)})}finally{G.closeSync(y)}if(h!==v)throw w();G.renameSync(x,b)}catch(h){throw G.rmSync(x,{force:!0}),h}},c=p=>{try{return JSON.parse(s(p).toString("utf8"))}catch(b){throw b instanceof U?b:r(`is damaged: ${p} is not JSON`)}};if(!n.has("manifest.json"))throw r("is not a Saffron bundle: it has no manifest.json");let l=c("manifest.json");if(!l||l.tool!=="saffron"||typeof l.format!="number")throw r("is not a Saffron bundle");for(let p of["runner","createdAt","repository","branch","commit","headCommit","run","attempt","url"])if(l[p]!==void 0){if(typeof l[p]!="string")throw r(`is damaged: its manifest's ${p} is not text`);if(l[p].length>2048)throw r(`is damaged: its manifest's ${p} is over 2048 characters`);l[p]=Te(l[p],2048)}if(l.format>xr)throw r(`is bundle format ${l.format}, made by saffron-ai ${l.runner}; this saffron-ai (${Ce()}) reads format ${xr}. Update saffron-ai to import it`);if(l.format!==xr)throw r(`has bundle format ${l.format}, which this saffron-ai does not read`);if(!Array.isArray(l.proposals)||!Array.isArray(l.evidence))throw r("is damaged: its manifest has no proposal or evidence list");let{shard:d}=l;if(d!==void 0&&!(Number.isInteger(d?.index)&&Number.isInteger(d?.total)&&d.index>=1&&d.index<=d.total))throw r("is damaged: its manifest's shard is not <index> of <total>");let f=new Map;for(let p of l.proposals){if(!p||typeof p.entry!="string"||!Ua.test(p.entry)||typeof p.revision!="string")throw r(`is damaged: its manifest lists a proposal at ${Te(JSON.stringify(p?.entry)??"nothing")}`);typeof p.feature=="string"&&(p.feature=Te(p.feature)),typeof p.scenario=="string"&&(p.scenario=Te(p.scenario));let b=s(p.entry);if(Oe(b)!==p.revision)throw r(`is damaged: ${p.entry} is not the proposal its manifest names`);f.set(p.entry,b)}for(let p of l.evidence){if(!p||typeof p.entry!="string"||!Pf.test(p.entry)||typeof p.sha256!="string")throw r(`is damaged: its manifest lists evidence at ${Te(JSON.stringify(p?.entry)??"nothing")}`);if(i(p.entry)!==p.sha256)throw r(`is damaged: ${p.entry} is not the file its manifest names`)}let m;if(l.report!==void 0){if(l.report!=="report.json")throw r(`is damaged: its manifest names the report ${JSON.stringify(l.report)}`);let p=c("report.json"),b=Da(p);if(b)throw r(`is damaged: report.json ${b}`);try{Vt(p)}catch(v){throw r(`is damaged: report.json cannot be shown (${kt(v)})`)}m=p}if(d&&!(m&&wr(m)))throw r("is damaged: it is a shard's bundle without the shard's report");return{source:e,manifest:l,report:m,files:f,read:s,write:a}}function zf(e,t){let n=t.headCommit??t.commit,r=nt(e,["rev-parse","HEAD"]);if(!n||!r||r===n)return;let s=`${n.slice(0,7)}${t.branch?` on ${t.branch}`:""}`;if(!/^[0-9a-f]{7,64}$/i.test(n)||!nt(e,["rev-parse","--verify","--quiet",`${n}^{commit}`]))return`this checkout is at ${r.slice(0,7)}, and the bundle was made at ${s}, which this clone does not have: fetch it to review against the same files`;let[o,i]=(nt(e,["rev-list","--left-right","--count",`HEAD...${n}`,"--"])??"").split(/\s+/).map(Number),a=[o?`${o} commit${o===1?"":"s"} ahead of`:"",i?`${i} commit${i===1?"":"s"} behind`:""].filter(Boolean).join(" and ");return`this checkout (${r.slice(0,7)}) is ${a||"not at"} the commit the bundle was made at, ${s}`}function ro(e){let{repository:t,branch:n,commit:r,headCommit:s,run:o,attempt:i,url:a}=e;return Object.fromEntries(Object.entries({repository:t,branch:n,commit:r,headCommit:s,run:o,attempt:i,url:a}).filter(([,c])=>typeof c=="string"))}var Ba=e=>V.join(e,".saffron","reports","imported.json");function An(e){let t;try{t=JSON.parse(G.readFileSync(Ba(e),"utf8"))}catch{return{}}return!t||typeof t!="object"||Array.isArray(t)?{}:Object.fromEntries(Object.entries(t).filter(([,n])=>!!n&&typeof n.revision=="string"&&!!n.from&&typeof n.from=="object"))}function qf(e,t){try{return G.readFileSync(e).equals(t)}catch{return!1}}function Hf(e,t,n,r,s,o){let i;try{i=ms(t,n)}catch(w){return{file:`.saffron/${n}`,feature:"",scenario:"",outcome:"refused",reason:`cannot be read: ${w instanceof X?w.problem:kt(w)}`}}let{proposal:a,revision:c}=i,{feature:l,scenario:d}=a.cache,f=xe(e,l,d),m={file:so(e,f),feature:Te(l),scenario:Te(d)},p=w=>({...m,outcome:"refused",reason:w});if(V.isAbsolute(l)||l.split(/[\\/]/).includes(".."))return p(`names a feature file outside the project: ${m.feature}`);if(!a.meta.bound)return p("carries no binding (a runner older than 0.8.5 filed it), so nothing can tell whether it fits this checkout");let b=o?`. ${o}`:"";if(!G.existsSync(V.join(e,l)))return p(`is for ${m.feature}, which is not in this checkout${b}`);let v=Ue(Y(e,l,d));if(v&&vn(v)===vn(a.cache))return{...m,outcome:"unchanged",reason:"already accepted here: the committed recording is this proposal's"};let k;try{k=$e(e,a,r)}catch(w){k=`cannot be checked: ${kt(w)}`}if(k)return p(`does not fit this checkout: ${Te(k,1e3)}${b}`);G.mkdirSync(V.dirname(f),{recursive:!0});try{return qt(f,()=>{if(G.existsSync(f)){if(qe(f)===c)return{...m,outcome:"unchanged"};if(!s)return p("differs from the proposal already pending here for this scenario: review that one first (saffron diff), or import with --overwrite to replace it")}return Ee(f,t),{...m,outcome:"imported"}})}catch(w){if(w instanceof St)return p("is being decided by another process. Try again.");throw w}}async function za(e,t,n,r={}){let s;try{let o=[];for(let i of t)for(let a of await Mf(i,()=>(s??=po(e)).dir))o.push(Df(r.describe?.(a.source)??a.source,a.file));if(o.reduce((i,a)=>i+[...a.entries.values()].reduce((c,l)=>c+l.size,0),0)>lo)throw new U(Cf);return Jf(e,o.map(Bf),n,r)}finally{s?.remove()}}function Jf(e,t,n,r){let s=t;if(s.length===0)throw new U("no bundle to import");let o=new Set,i=$r(e);for(let y of s){let h=y.manifest.repository;if(!h&&!r.allowUnknownRepository)throw new U(`${y.source} does not say which repository it comes from (it was made outside a git checkout). Import it with --allow-unknown-repository to check it by its proposals' bindings only`);if(!h)o.add(`${y.source} does not say which repository it comes from: it is checked by its proposals' bindings only`);else if(i.length===0)o.add(`this project has no git remote to compare ${h} with: the bundle is checked by its proposals' bindings only`);else if(!i.includes(h)){let S=u=>u.slice(u.indexOf("/")),$=i.find(u=>S(u)===S(h));if(!$)throw new U(`${y.source} comes from ${h}, and this checkout is ${i.join(", ")}. Import it into a checkout of ${h}`);o.add(`${s.length===1?"the bundle comes":"the bundles come"} from ${h}, and this checkout is ${$}: the same repository under another host name (an SSH host alias, or a mirror), so each proposal is checked by its binding`)}}if(s.length>1){let y=new Set(s.map(g=>g.manifest.run??""));if(y.size>1)throw new U(`these bundles come from different runs (${[...y].map(g=>g||"none recorded").join(", ")}). Import one run at a time`);let h=g=>Number(g.manifest.attempt)||0,S=new Map;for(let g of s){let R=g.manifest.shard?.index;if(R===void 0)continue;let E=S.get(R);(!E||h(g)>h(E))&&S.set(R,g)}let $=s.filter(g=>g.manifest.shard&&h(g)<h(S.get(g.manifest.shard.index)));for(let g of $){let R=S.get(g.manifest.shard.index);o.add(`${g.source} is shard ${g.manifest.shard.index} of attempt ${h(g)}, which attempt ${h(R)} ran again: left out`)}s=s.filter(g=>!$.includes(g));let u=s.filter(g=>!g.manifest.shard);if(u.length>0)throw new U(`${u.map(g=>g.source).join(", ")} ${u.length===1?"holds a whole run, not a shard of one":"hold whole runs, not shards of one"}: import one run at a time`)}let a,c=s.filter(y=>y.report);if(s.length===1&&c.length===1){let{shard:y,history:h,...S}=c[0].report;a=S}else if(c.length>0)try{a=Sr(c.map(y=>({file:y.source,report:y.report})),{partial:!0}).report}catch(y){throw y instanceof Kt?new U(`these shard bundles do not belong together: ${y.message}`):y}let l=s[0].manifest,d=zf(e,l);d&&o.add(d),l.headCommit&&l.commit&&o.add(`the run tested ${l.commit.slice(0,7)}, a pull request's merge commit, not the head of ${l.branch??"its branch"} (${l.headCommit.slice(0,7)}): each proposal is checked against this checkout by its binding`);let f=l.headCommit??l.commit,m=d&&f?`The bundle was made at ${f.slice(0,7)}${l.branch?` on ${l.branch}`:""}: check that out and import again`:void 0,p={written:0,unchanged:0},b=new Map;for(let y of a?.scenarios??[])for(let h of y.evidence??[])b.set(V.posix.dirname(h.file),{feature:y.feature,scenario:y.scenario});for(let y of s)for(let{entry:h,sha256:S}of y.manifest.evidence){let $=V.join(e,".saffron",...h.split("/"));if(Uf($)===S){p.unchanged++;continue}y.write(h,$,S),p.written++;let u=b.get(`.saffron/${V.posix.dirname(h)}`);u&&G.writeFileSync(V.join(V.dirname($),Dt),JSON.stringify(u,null,2))}let v;if(a){let y=s.flatMap(S=>S.manifest.shard?[S.manifest.shard]:[]);a.imported={...ro(l),...y.length>0?{shards:{imported:y.map(S=>S.index).sort((S,$)=>S-$),total:y[0].total}}:{}};for(let S of a.scenarios){let $=typeof S.proposalFile=="string"?S.proposalFile.split(/[\\/]/).slice(-2):[];Ua.test(`proposals/${$.join("/")}`)&&(S.proposalFile=V.join(e,".saffron","proposals",...$))}let h=V.join(e,".saffron","reports","latest.json");qf(h,Buffer.from(`${JSON.stringify(a,null,2)}
258
- `))?v="unchanged":(Rn(e,a),v="written")}let k=[],w=An(e),x=JSON.stringify(w);for(let y of s)for(let h of y.manifest.proposals){let S=Hf(e,y.files.get(h.entry),h.entry,n,r.overwrite===!0,m),$=S.reason===void 0?S:{...S,reason:Te(S.reason,1/0)};k.push($),($.outcome==="imported"||$.outcome==="unchanged"&&!$.reason)&&(w[$.file]={revision:h.revision,from:{...ro(y.manifest),...y.manifest.shard?{shard:y.manifest.shard}:{}}})}for(let[y,h]of Object.entries(w)){let S;try{S=qe(V.join(e,y))}catch{S=void 0}S!==h.revision&&delete w[y]}return JSON.stringify(w)!==x&&Ee(Ba(e),`${JSON.stringify(w,null,2)}
259
- `),{bundles:s.map(y=>{let{runner:h,createdAt:S,shard:$}=y.manifest;return{source:y.source,runner:h,createdAt:S,origin:ro(y.manifest),...$?{shard:{index:$.index,total:$.total}}:{}}}),warnings:[...o],proposals:k,evidence:p,...v?{report:v}:{}}}var Gf=/^\s*(?:Scenario(?: Outline| Template)?|Example):\s*(.+?)\s*$/,Wf=/^\s*StepSet:/;function dt(e,t){return Ge.relative(e,t).split(Ge.sep).join("/")}function Vf(e,t){let n={green:0,yellow:1,red:2};return!e||n[t]>n[e]?t:e}function qa(e,t){return`${e} :: ${t}`}function kr(e,t,n={}){let r=t.features??"features",s=Ge.isAbsolute(r)?r:Ge.join(e,r),o=Ge.join(e,".saffron","reports","latest.json"),i=Ge.join(e,".saffron","reports","latest.html"),a;try{a=JSON.parse(_e.readFileSync(o,"utf8"))}catch{a=void 0}let c=new Map;for(let u of a?.scenarios??[]){let g=u.scenario.replace(/ \(example \d+\)$/,""),R=qa(u.feature,g);c.set(R,Vf(c.get(R),u.status))}let l=new Map,d;try{l=Qe(wt(s),e)}catch(u){d=u instanceof Error?u.message:String(u)}let f=_e.existsSync(s)?[..._e.readdirSync(s,{recursive:!0})].map(String).filter(Xe).map(u=>Ge.join(s,u)).sort():[],m=[],p=new Map,b=new Map,v={parsedFiles:new Set,owned:new Set,unreadable:d?[d]:[]};for(let u of f){let g=dt(e,u),R="";try{R=_e.readFileSync(u,"utf8")}catch(C){m.push({path:g,name:g,scenarios:[],stepSets:0,error:String(C)}),v.unreadable.push(`${g}: ${String(C)}`);continue}let E=R.split(`
260
- `),I=new Map,A=0;E.forEach((C,L)=>{let T=C.match(Gf);T&&!I.has(T[1])?I.set(T[1],L+1):Wf.test(C)&&A++});let P=E.find(C=>/^\s*Feature:/.test(C)),_=P?P.replace(/^\s*Feature:\s*/,"").trim():g,j;try{j=he(u,e,l)}catch(C){m.push({path:g,name:_,scenarios:[],stepSets:A,error:d??(C instanceof Error?C.message:String(C))}),v.unreadable.push(`${g}: ${C instanceof Error?C.message:String(C)}`);continue}v.parsedFiles.add(u),b.set(u,j);let F=new Map;for(let C of j){v.owned.add(Le(e,C.featurePath,C.displayName));let L=F.get(C.name);if(L){L.rows+=1;continue}let T=Y(e,C.featurePath,C.name);v.owned.add(T),v.owned.add(xe(e,C.featurePath,C.name));let O="missing",M;if(_e.existsSync(T))try{let B=me(T);O=B&&Ds(C,B)?"valid":"stale"}catch(B){O="invalid",M=B instanceof X?B.problem:B instanceof Error?B.message:String(B)}F.set(C.name,{name:C.name,line:I.get(C.name),tags:[...C.tags],outline:Object.keys(C.params).length>0,rows:1,cached:O!=="missing",cacheState:O,...M?{cacheProblem:M}:{},proposal:_e.existsSync(xe(e,C.featurePath,C.name)),lastStatus:c.get(qa(C.featurePath,C.name))});for(let B of C.tags)p.set(B,(p.get(B)??0)+1)}m.push({path:g,name:_,scenarios:[...F.values()],stepSets:A})}let k=[],w=An(e),x=He(e,(u,g)=>k.push({file:dt(e,u),problem:g})).map(({file:u,proposal:g,revision:R})=>({file:dt(e,u),feature:g.cache.feature,scenario:g.cache.scenario,mode:g.meta.mode,createdAt:g.meta.createdAt,verified:g.meta.verified,proofError:g.meta.proofError,stale:g.meta.bound?$e(e,g,l):void 0,unbound:g.meta.bound?void 0:!0,revision:R,adaptations:g.meta.adaptations??[],narrative:g.meta.narrative,suggestedFeatureEdit:g.meta.suggestedFeatureEdit,recordedFor:g.meta.recordedFor,aiCalls:g.meta.usage?.aiCalls??0,costUsd:g.meta.usage&&ke({costUsd:g.meta.usage.costUsd??0,aiCalls:g.meta.usage.aiCalls})?g.meta.usage.costUsd:void 0,importedFrom:w[dt(e,u)]?.revision===R?w[dt(e,u)].from:void 0})).sort((u,g)=>u.file.localeCompare(g.file)),y;try{y=ze(e,r,l,b)}catch{y={steps:[],stepSets:[],scannedFiles:0,scannedCaches:0}}let h=Yf(e),S=$n(e,n.historyLimit??30).map(u=>({startedAt:u.startedAt,green:u.green,yellow:u.yellow,red:u.red,aiCalls:u.aiCalls,costUsd:ke(u)?u.costUsd:void 0})),$=Ge.join(e,"saffron.config.json");return{tool:"saffron",version:Ce(),projectRoot:e,packageInstalled:_e.existsSync(Ge.join(e,"node_modules","saffron-ai")),config:{file:_e.existsSync($)?"saffron.config.json":void 0,effective:bi(t)},features:m,tags:[...p.entries()].map(([u,g])=>({tag:u,scenarios:g})).sort((u,g)=>u.tag.localeCompare(g.tag)),proposals:x,unreadableProposals:k,lastRun:a?{startedAt:a.startedAt,finishedAt:a.finishedAt,totals:{...a.totals,costUsd:ke(a.totals)?a.totals.costUsd:void 0},reportHtml:_e.existsSync(i)?dt(e,i):void 0,attention:a.scenarios.filter(u=>u.status!=="green").map(u=>({feature:u.feature,scenario:u.scenario,baseScenario:u.scenario.replace(/ \(example \d+\)$/,""),status:u.status,error:u.status==="red"?u.error?.split(`
261
- `)[0]:void 0,failedStep:(u.steps??[]).find(g=>g.status==="failed")?.gherkin??u.evidence?.find(g=>g.step)?.step,evidence:(u.evidence??[]).filter(g=>g.kind==="trace"?_e.existsSync(Ge.join(e,g.file)):zt(e,g)!==void 0).map(g=>({kind:g.kind,file:g.file,...g.step?{step:g.step}:{},...g.sha256?{sha256:g.sha256}:{}}))})).sort((u,g)=>u.status===g.status?0:u.status==="red"?-1:1)}:void 0,history:S,vocabulary:{steps:y.steps.length,recorded:y.steps.filter(u=>u.status==="recorded").length,unrecorded:y.steps.filter(u=>u.status==="unrecorded").length,stepSets:y.stepSets.length,divergent:y.steps.filter(u=>u.status==="divergent").map(u=>u.text),duplicateWordings:h.slice(0,50)},data:(()=>{let u=De(),g=[],R=new Set;for(let[E,I]of b)for(let A of I)for(let P of gt(le(A)))if(!R.has(P)){R.add(P);try{qn(P)}catch(_){g.push({ref:P,reason:_ instanceof Error?_.message:String(_),feature:dt(e,E),scenario:A.name})}}return{dir:dt(e,u.dir),env:u.env,files:_e.existsSync(u.dir)?_e.readdirSync(u.dir).filter(E=>/\.(json|csv)$/.test(E)).sort():[],unresolved:g}})(),orphans:(()=>{let u=cr(e,r,v);return u.unreadable.length?[]:u.orphans})()}}function Ja(e){let t=e.features.reduce((o,i)=>o+i.scenarios.length,0),n=e.features.reduce((o,i)=>o+i.scenarios.filter(a=>a.cacheState==="valid").length,0),r=e.features.reduce((o,i)=>o+i.scenarios.filter(a=>a.cacheState==="stale"||a.cacheState==="invalid").length,0);console.log(`saffron ${e.version} \xB7 ${e.features.length} feature file(s) \xB7 ${t} scenario(s), ${n} replay from cache`+(r?`, ${r} stale (\u26A0) and will re-record`:""));for(let o of e.features){let i=o.scenarios.map(a=>a.proposal?"\u25D0":a.cacheState==="valid"?"\u25CF":a.cacheState==="missing"?"\u25CB":"\u26A0").join("");console.log(` ${o.path} ${i}${o.error?` \u2717 ${o.error.split(`
262
- `)[0]}`:""}`)}if(e.tags.length&&console.log(`tags: ${e.tags.map(o=>`${o.tag} (${o.scenarios})`).join(", ")}`),console.log(e.proposals.length?`${e.proposals.length} proposal(s) pending review: ${e.proposals.map(o=>o.file).join(", ")}`:"no proposals pending review",...e.unreadableProposals.map(o=>`\u26A0 proposal ${o.file} cannot be read: ${o.problem}`),...e.features.flatMap(o=>o.scenarios.filter(i=>i.cacheProblem).map(i=>`\u26A0 recording of ${o.path} \u203A ${i.name} cannot be read: ${i.cacheProblem}`))),e.lastRun){let o=e.lastRun.totals;console.log(`last run ${e.lastRun.finishedAt}: ${o.green} passed \xB7 ${o.yellow} pending review \xB7 ${o.red} failed \xB7 ${o.costUsd===void 0?"cost not reported":`$${o.costUsd.toFixed(2)}`}`)}let s=e.vocabulary;console.log(`vocabulary: ${s.steps} steps (${s.recorded} recorded, ${s.unrecorded} unrecorded), ${s.stepSets} step sets, ${s.divergent.length} divergent, ${s.duplicateWordings.length} duplicate wording group(s)`),e.orphans.length&&console.log(`${e.orphans.length} recording(s) belong to no scenario: run \`saffron prune\` to see them`),(e.data.files.length||e.data.unresolved.length)&&console.log(`test data: ${e.data.files.length} file(s) in ${e.data.dir}/${e.data.env?` \xB7 environment "${e.data.env}"`:""}`+(e.data.unresolved.length?` \xB7 ${e.data.unresolved.length} reference(s) do not resolve: ${e.data.unresolved.map(o=>`{data:${o.ref}}`).join(", ")}`:"")),console.log(`config: ${e.config.file??"(defaults, no saffron.config.json)"}`)}var Kf=/^(<[^>]+>|\{env:[^}]+\}|\{date[^}]*\})$/;function Ha(e){return e?e.role?`${e.role}${e.name?` "${e.name}"`:e.nameRegex?` /${e.nameRegex}/`:""}`:e.label?`label "${e.label}"`:e.text?`text "${e.text}"`:e.testId?`testid ${e.testId}`:e.placeholder?`placeholder "${e.placeholder}"`:e.fallbackSelectors?.length?e.fallbackSelectors[0]:"":""}function Yf(e){let t;try{t=Be(e)}catch{return[]}let n=new Map;for(let r of t.steps.values()){if(r.actions.length===0)continue;let s=Qn(r.gherkin).pattern,o=[...r.gherkin.matchAll(/"([^"]*)"/g)].map(f=>f[1]),i=f=>f!==void 0&&(o.includes(f)||Kf.test(f))?"\xABarg\xBB":f,a=r.actions.map(f=>{let{pageFingerprint:m,value:p,promptText:b,...v}=f;return{...v,value:i(p),promptText:i(b)}}),c=JSON.stringify(a),l=r.actions.map(f=>`${f.action}${Ha(f.target)?` ${Ha(f.target)}`:f.url?` ${f.url}`:""}`).join(" \xB7 "),d=n.get(c)??{steps:new Map,actions:l};d.steps.has(s)||d.steps.set(s,r.gherkin),n.set(c,d)}return[...n.values()].filter(r=>r.steps.size>1).map(r=>({steps:[...r.steps.values()].sort(),actions:r.actions})).sort((r,s)=>s.steps.length-r.steps.length||r.steps[0].localeCompare(s.steps[0]))}async function Ga(e,t){let n=new Zf({name:"saffron",version:Ce()},{capabilities:{tools:{}}});n.setRequestHandler(eu,async()=>({tools:[{name:"search_steps",description:"Search the project's step vocabulary. Returns known Gherkin steps with status (recorded = replays at zero tokens, divergent, unrecorded), usage counts, and source files. ALWAYS reuse an existing wording exactly instead of inventing a near-duplicate: exact step text is the replay-cache identity, so a new wording costs a fresh AI recording.",inputSchema:{type:"object",properties:{query:{type:"string",description:"Case-insensitive substring filter over step text; omit to list everything."}}}},{name:"list_step_sets",description:"List the project's StepSet definitions (named reusable step sequences from .saffron files) with their steps, defining file, and usage. Invoke one inside a scenario with a line reading exactly: StepSet <name>",inputSchema:{type:"object",properties:{}}},{name:"project_status",description:"The project overview (same data as `saffron status --json`): every feature file with its scenarios (header line, tags, outline rows, cached / proposal pending / last run status), the tag list, pending proposals with narrative and proof-replay verdict, the last run's totals, run history, vocabulary health (divergent steps, duplicate wordings proven by identical recordings) and the effective config. Call it before editing tests to see what is already recorded and what awaits review.",inputSchema:{type:"object",properties:{}}}]})),n.setRequestHandler(Xf,async r=>tu(r.params.name,r.params.arguments,e,t)),await n.connect(new Qf)}function tu(e,t,n,r){let s=o=>({content:[{type:"text",text:JSON.stringify(o,null,2)}]});if(e==="search_steps"){let o=ze(n,r),i=typeof t?.query=="string"?t.query:void 0,a=i?Xn(o,i):o;return s({steps:a.steps,scannedFiles:a.scannedFiles,scannedCaches:a.scannedCaches})}if(e==="list_step_sets"){let o=ze(n,r);return s({stepSets:o.stepSets})}return e==="project_status"?s(kr(n,ne(n))):{content:[{type:"text",text:`Unknown tool: ${e}`}],isError:!0}}import{query as nu}from"@anthropic-ai/claude-agent-sdk";function Wa(e,t){let n=t.steps.map(s=>`- [${s.status}] ${s.keyword} ${s.text}`).join(`
255
+ `)[0].matchAll(/^parent ([0-9a-f]+)$/gm)].map(r=>r[1])}function Uf(e,t=process.env){let n=rt(e,["remote"])?.split(`
256
+ `)??[],r=n.includes("origin")?"origin":n[0],s=t.GITHUB_SERVER_URL&&t.GITHUB_REPOSITORY?`${t.GITHUB_SERVER_URL}/${t.GITHUB_REPOSITORY}`:void 0,o=[r&&rt(e,["remote","get-url",r]),s,t.BUILD_REPOSITORY_URI].map(b=>b?Ya(b):void 0).find(Boolean),i=rt(e,["rev-parse","--abbrev-ref","HEAD"]),a=Lf(t),c=(Qa(t)?t.GITHUB_HEAD_REF||a.ref:void 0)||(t.GITHUB_EVENT_NAME==="pull_request_target"?t.GITHUB_BASE_REF:void 0)||(t.GITHUB_REF_TYPE==="tag"?void 0:t.GITHUB_REF_NAME)||za(t.SYSTEM_PULLREQUEST_SOURCEBRANCH)||(t.BUILD_SOURCEBRANCH?.startsWith("refs/heads/")?za(t.BUILD_SOURCEBRANCH):void 0)||(i!=="HEAD"?i:void 0),l=rt(e,["rev-parse","HEAD"])||t.GITHUB_SHA||t.BUILD_SOURCEVERSION,d=a.sha||t.SYSTEM_PULLREQUEST_SOURCECOMMITID,f=d&&l&&wo(e,l)[1]===d?d:void 0,m=t.GITHUB_SERVER_URL&&t.GITHUB_REPOSITORY&&t.GITHUB_RUN_ID?`${t.GITHUB_SERVER_URL}/${t.GITHUB_REPOSITORY}/actions/runs/${t.GITHUB_RUN_ID}`:t.SYSTEM_COLLECTIONURI&&t.SYSTEM_TEAMPROJECT&&t.BUILD_BUILDID?`${t.SYSTEM_COLLECTIONURI.replace(/\/?$/,"/")}${encodeURIComponent(t.SYSTEM_TEAMPROJECT)}/_build/results?buildId=${t.BUILD_BUILDID}`:void 0,p={repository:o,branch:c,commit:l,headCommit:f&&f!==l?f:void 0,run:t.GITHUB_RUN_ID&&`github:${t.GITHUB_RUN_ID}`||t.BUILD_BUILDID&&`azure:${t.BUILD_BUILDID}`||Rr(t),attempt:t.GITHUB_RUN_ATTEMPT||t.SYSTEM_JOBATTEMPT||void 0,url:m};return Object.fromEntries(Object.entries(p).filter(([,b])=>b!==void 0))}function vo(e){let t=e.headCommit??e.commit;return[e.run?`run ${e.run}${e.attempt&&e.attempt!=="1"?` (attempt ${e.attempt})`:""}`:"a local run",e.branch||t?`${e.branch??"detached"}${t?` @ ${t.slice(0,7)}`:""}`:void 0,e.repository].filter(Boolean).join(" \xB7 ")}var Bf=/^(latest|shard-\d+-of-\d+)\.json$/,en=e=>!!e&&typeof e=="object"&&!Array.isArray(e),Ha=(e,t)=>t.find(n=>typeof e[n]!="number"),zf=["scenarios","green","yellow","red","aiCalls","inputTokens","outputTokens","cacheReadTokens","costUsd"],Hf=["aiCalls","inputTokens","outputTokens","cacheReadTokens"],qa=e=>typeof e=="string"&&/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(Z|[+-]\d\d:\d\d)$/.test(e)&&!Number.isNaN(Date.parse(e));function Za(e){if(!en(e)||e.tool!=="saffron")return"is not a Saffron run report";if(!qa(e.startedAt)||!qa(e.finishedAt))return"has no start or finish time";if(!en(e.totals))return"has no totals";let t=Ha(e.totals,zf);if(t)return`has totals.${t} that is not a number`;if(!Array.isArray(e.scenarios))return"has no list of scenarios";for(let[n,r]of e.scenarios.entries()){let s=`scenarios[${n}]`;if(!en(r)||typeof r.feature!="string"||typeof r.scenario!="string")return`has ${s} without a feature and scenario name`;if(!["green","yellow","red"].includes(r.status))return`has ${s}.status ${JSON.stringify(r.status)}, which this saffron-ai does not know`;if(!en(r.usage)||Ha(r.usage,Hf))return`has ${s}.usage without its token counts`;if(!Array.isArray(r.steps))return`has ${s}.steps that is not a list`;let o=r.steps.findIndex(i=>!en(i)||typeof i.gherkin!="string"||typeof i.kind!="string"||typeof i.status!="string");if(o>=0)return`has ${s}.steps[${o}] without its gherkin, kind and status`;if(r.evidence!==void 0&&!(Array.isArray(r.evidence)&&r.evidence.every(i=>en(i)&&typeof i.file=="string"&&typeof i.kind=="string")))return`has ${s}.evidence that is not a list of files`}}var qf=e=>Za(e)===void 0;function Jf(e){let t=V.join(e,".saffron","reports"),n;try{n=G.readdirSync(t).filter(s=>Bf.test(s)).sort()}catch{return}let r;for(let s of n)try{let o=G.readFileSync(V.join(t,s)),i=JSON.parse(o.toString("utf8"));qf(i)&&(!r||i.finishedAt>r.report.finishedAt)&&(r={bytes:o,report:i})}catch{}return r}function Wf(e,t){return V.join(e,".saffron","reports",t?`saffron-bundle-${uo(t)}.zip`:"saffron-bundle.zip")}var Ja=e=>`${(e/1024**3).toFixed(1)} GB`,Wa='Keep fewer traces ("trace": "retain-on-failure" in saffron.config.json, or --trace retain-on-failure) and run again';function Xa(e,t={}){let n=[],r=[],s=[],o=[],i=V.join(e,".saffron","proposals");for(let{file:b,proposal:$,revision:k,bytes:w}of Je(e,(x,y)=>n.push({file:mo(e,x),problem:y}))){let x=`proposals/${mo(i,b)}`;r.push({name:x,data:w}),s.push({entry:x,feature:$.cache.feature,scenario:$.cache.scenario,revision:k,bound:$.meta.bound}),o.push({file:b,proposal:$,bytes:w})}let a=Jf(e),c=[],l=[],d=[];if(a){r.push({name:"report.json",data:a.bytes});let b=[],$=new Set;for(let w of a.report.scenarios)for(let x of w.evidence??[]){if($.has(x.file))continue;$.add(x.file);let y=x.file.startsWith(".saffron/artifacts/")?G.statSync(V.join(e,x.file),{throwIfNoEntry:!1}):void 0;if(!y?.isFile()){l.push(x.file);continue}let u=w.status==="green"&&x.kind==="trace";if(y.size>yo){if(u){d.push(x.file);continue}throw new L(`${x.file} is ${Ja(y.size)}, over the 1 GB a bundle holds per file. ${Wa}`)}b.push({e:x,size:y.size,greenTrace:u})}let k=()=>r.reduce((w,x)=>w+("data"in x?x.data.length:x.size??0)+1024,0)+b.reduce((w,x)=>w+x.size+1024,0);if(k()>Ga)for(let w of b.filter(x=>x.greenTrace))b.splice(b.indexOf(w),1),d.push(w.e.file);if(k()>Ga)throw new L(`the bundle would hold ${Ja(k())}${d.length?" even without the traces of green scenarios":""}, over the 4 GB one bundle holds. ${Wa}, or split the run into shards (--shard)`);for(let{e:w}of b){let x=V.join(e,w.file),y;try{y=lt(V.join(e,".saffron","artifacts"),x)?wr(x):void 0}catch{y=void 0}if(!y||w.sha256&&y.sha256!==w.sha256){l.push(w.file);continue}let u=w.file.slice(9);r.push({name:u,file:x,crc:y.crc,size:y.size}),c.push({entry:u,sha256:y.sha256})}}if(!a&&s.length===0)throw new L("nothing to export: no run report and no pending proposals under .saffron. Run `saffron run` first.");let f=a&&Er(a.report)?{index:a.report.shard.index,total:a.report.shard.total}:void 0,m={tool:"saffron",format:_r,runner:Oe(),createdAt:new Date().toISOString(),...Uf(e,t.env),...f?{shard:f}:{},...a?{report:"report.json"}:{},proposals:s,evidence:c},p=t.out??Wf(e,f);if(G.existsSync(p)&&G.statSync(p).isDirectory())throw new L(`${p} is a folder: give --out a file name, such as ${V.join(p,"saffron-bundle.zip")}`);return aa(p,[{name:"manifest.json",data:Buffer.from(`${JSON.stringify(m,null,2)}
257
+ `)},...r]),{file:p,manifest:m,proposals:o,missing:l,dropped:d,unreadable:n}}var Ir="[a-z0-9-]+",ec=new RegExp(`^proposals/${Ir}/${Ir}\\.json$`),Gf=new RegExp(`^artifacts/${Ir}/${Ir}/[A-Za-z0-9][A-Za-z0-9._-]*$`),yo=1024**3,So=8*1024**3,Vf="the bundles to import add up to more than 8 GB unzipped",Ga=Math.min(So,dt),Va=100,Kf=1024**2,Yf=6e5;function Qf(e){try{return process.kill(e,0),!0}catch(t){return t.code==="EPERM"}}function xo(e){let t=V.join(e,".saffron","reports");for(let o of G.existsSync(t)?G.readdirSync(t):[]){let i=Number(/^run-download-(\d+)-/.exec(o)?.[1]),a=V.join(t,o),c=i?G.statSync(a,{throwIfNoEntry:!1}):void 0;c&&Date.now()-c.mtimeMs>36e5&&!Qf(i)&&G.rmSync(a,{recursive:!0,force:!0})}let n=V.join(t,`run-download-${process.pid}-${bo(4).toString("hex")}`),r=G.mkdirSync(n,{recursive:!0});return{dir:n,remove:()=>{G.rmSync(n,{recursive:!0,force:!0});for(let o=t;r&&o.startsWith(r);o=V.dirname(o))try{G.rmdirSync(o)}catch{break}}}}async function Zf(e,t){let n=r=>new L(r?.name==="TimeoutError"?`cannot download ${e}: it did not finish within 10 minutes`:`cannot download ${e}: ${Rt(r)}`);try{let r=await fetch(e,{redirect:"follow",signal:AbortSignal.timeout(Yf)});if(!r.ok||!r.body)throw new L(`cannot download ${e}: HTTP ${r.status}`);let s=G.openSync(t,"w");try{let o=0;for await(let i of r.body){if(o+=i.length,o>So)throw new L(`cannot download ${e}: it is over 8 GB`);G.writeSync(s,i)}}finally{G.closeSync(s)}}catch(r){throw r instanceof L?r:n(r)}}async function Xf(e,t){if(/^https?:\/\//i.test(e)){let s=V.join(t(),`url-${bo(4).toString("hex")}.zip`);return await Zf(e,s),[{source:e,file:s}]}let n=G.statSync(e,{throwIfNoEntry:!1});if(!n)throw new L(`not found: ${e}`);let r=n.isDirectory()?G.readdirSync(e,{recursive:!0}).map(String).filter(s=>/^saffron-bundle.*\.zip$/.test(V.basename(s))).sort().map(s=>V.join(e,s)):[e];if(r.length===0)throw new L(`no saffron-bundle*.zip under ${e}`);return r.map(s=>({source:s,file:s}))}function eu(e,t){try{return{source:e,file:t,entries:new Map(Xi(t).map(n=>[n.name,n]))}}catch(n){throw new L(`${e} is not a Saffron bundle: ${Rt(n)}`)}}var tu=1024*1024;function Ka(e,t,n){let r=Mf("sha256"),s=G.openSync(e,"r");try{let o=Buffer.alloc(Math.max(1,Math.min(tu,t.size)));for(let i=0;i<t.size;){let a=G.readSync(s,o,0,Math.min(o.length,t.size-i),t.start+i);if(a===0)throw new Error("the file ends early");let c=o.subarray(0,a);r.update(c),n?.(c),i+=a}}finally{G.closeSync(s)}return r.digest("hex")}function nu(e){try{return wr(e).sha256}catch{return}}function ru({source:e,file:t,entries:n}){let r=p=>new L(`${e} ${p}`);for(let p of n.values()){if(p.size>yo||p.compressedSize>yo)throw r(`is damaged: ${Re(p.name)} is too large`);if(p.method!==0&&p.size>Kf&&p.size>Va*p.compressedSize)throw r(`is damaged: ${Re(p.name)} claims to unzip to over ${Va} times its size`)}let s=p=>{let b=n.get(p);if(!b)throw r(`is damaged: ${p} is missing`);let $;try{let k=b.raw();if(k.length!==b.compressedSize)throw new Error("the file ends early");$=b.method===0?k:Df.inflateRawSync(k,{maxOutputLength:Math.max(b.size,1)})}catch(k){throw r(`is damaged: ${p} cannot be read (${Rt(k)})`)}if($.length!==b.size)throw r(`is damaged: ${p} is truncated`);return $},o=p=>{let b=n.get(p);if(!(!b||b.method!==0)){if(b.compressedSize!==b.size)throw r(`is damaged: ${p} is truncated`);return b}},i=p=>{let b=o(p);if(!b)return je(s(p));try{return Ka(t,b)}catch($){throw r(`is damaged: ${p} cannot be read (${Rt($)})`)}},a=(p,b,$)=>{let k=o(p),w=()=>new Error(Re(`${e} is damaged: ${p} changed while it was imported`,1/0));if(!k){let u=s(p);if(je(u)!==$)throw w();Ae(b,u);return}G.mkdirSync(V.dirname(b),{recursive:!0});let x=`${b}.${process.pid}-${bo(4).toString("hex")}.partial`,y=G.openSync(x,"w");try{let u;try{u=Ka(t,k,v=>{for(let S=0;S<v.length;)S+=G.writeSync(y,v,S,v.length-S)})}finally{G.closeSync(y)}if(u!==$)throw w();G.renameSync(x,b)}catch(u){throw G.rmSync(x,{force:!0}),u}},c=p=>{try{return JSON.parse(s(p).toString("utf8"))}catch(b){throw b instanceof L?b:r(`is damaged: ${p} is not JSON`)}};if(!n.has("manifest.json"))throw r("is not a Saffron bundle: it has no manifest.json");let l=c("manifest.json");if(!l||l.tool!=="saffron"||typeof l.format!="number")throw r("is not a Saffron bundle");for(let p of["runner","createdAt","repository","branch","commit","headCommit","run","attempt","url"])if(l[p]!==void 0){if(typeof l[p]!="string")throw r(`is damaged: its manifest's ${p} is not text`);if(l[p].length>2048)throw r(`is damaged: its manifest's ${p} is over 2048 characters`);l[p]=Re(l[p],2048)}if(l.format>_r)throw r(`is bundle format ${l.format}, made by saffron-ai ${l.runner}; this saffron-ai (${Oe()}) reads format ${_r}. Update saffron-ai to import it`);if(l.format!==_r)throw r(`has bundle format ${l.format}, which this saffron-ai does not read`);if(!Array.isArray(l.proposals)||!Array.isArray(l.evidence))throw r("is damaged: its manifest has no proposal or evidence list");let{shard:d}=l;if(d!==void 0&&!(Number.isInteger(d?.index)&&Number.isInteger(d?.total)&&d.index>=1&&d.index<=d.total))throw r("is damaged: its manifest's shard is not <index> of <total>");let f=new Map;for(let p of l.proposals){if(!p||typeof p.entry!="string"||!ec.test(p.entry)||typeof p.revision!="string")throw r(`is damaged: its manifest lists a proposal at ${Re(JSON.stringify(p?.entry)??"nothing")}`);typeof p.feature=="string"&&(p.feature=Re(p.feature)),typeof p.scenario=="string"&&(p.scenario=Re(p.scenario));let b=s(p.entry);if(je(b)!==p.revision)throw r(`is damaged: ${p.entry} is not the proposal its manifest names`);f.set(p.entry,b)}for(let p of l.evidence){if(!p||typeof p.entry!="string"||!Gf.test(p.entry)||typeof p.sha256!="string")throw r(`is damaged: its manifest lists evidence at ${Re(JSON.stringify(p?.entry)??"nothing")}`);if(i(p.entry)!==p.sha256)throw r(`is damaged: ${p.entry} is not the file its manifest names`)}let m;if(l.report!==void 0){if(l.report!=="report.json")throw r(`is damaged: its manifest names the report ${JSON.stringify(l.report)}`);let p=c("report.json"),b=Za(p);if(b)throw r(`is damaged: report.json ${b}`);try{Zt(p)}catch($){throw r(`is damaged: report.json cannot be shown (${Rt($)})`)}m=p}if(d&&!(m&&Er(m)))throw r("is damaged: it is a shard's bundle without the shard's report");return{source:e,manifest:l,report:m,files:f,read:s,write:a}}function su(e,t){let n=t.headCommit??t.commit,r=rt(e,["rev-parse","HEAD"]);if(!n||!r||r===n)return;let s=`${n.slice(0,7)}${t.branch?` on ${t.branch}`:""}`;if(!/^[0-9a-f]{7,64}$/i.test(n)||!rt(e,["rev-parse","--verify","--quiet",`${n}^{commit}`]))return`this checkout is at ${r.slice(0,7)}, and the bundle was made at ${s}, which this clone does not have: fetch it to review against the same files`;let[o,i]=(rt(e,["rev-list","--left-right","--count",`HEAD...${n}`,"--"])??"").split(/\s+/).map(Number),a=[o?`${o} commit${o===1?"":"s"} ahead of`:"",i?`${i} commit${i===1?"":"s"} behind`:""].filter(Boolean).join(" and ");return`this checkout (${r.slice(0,7)}) is ${a||"not at"} the commit the bundle was made at, ${s}`}function ho(e){let{repository:t,branch:n,commit:r,headCommit:s,run:o,attempt:i,url:a}=e;return Object.fromEntries(Object.entries({repository:t,branch:n,commit:r,headCommit:s,run:o,attempt:i,url:a}).filter(([,c])=>typeof c=="string"))}var tc=e=>V.join(e,".saffron","reports","imported.json");function On(e){let t;try{t=JSON.parse(G.readFileSync(tc(e),"utf8"))}catch{return{}}return!t||typeof t!="object"||Array.isArray(t)?{}:Object.fromEntries(Object.entries(t).filter(([,n])=>!!n&&typeof n.revision=="string"&&!!n.from&&typeof n.from=="object"))}function ou(e,t){try{return G.readFileSync(e).equals(t)}catch{return!1}}function iu(e,t,n,r,s,o){let i;try{i=ks(t,n)}catch(w){return{file:`.saffron/${n}`,feature:"",scenario:"",outcome:"refused",reason:`cannot be read: ${w instanceof X?w.problem:Rt(w)}`}}let{proposal:a,revision:c}=i,{feature:l,scenario:d}=a.cache,f=xe(e,l,d),m={file:mo(e,f),feature:Re(l),scenario:Re(d)},p=w=>({...m,outcome:"refused",reason:w});if(V.isAbsolute(l)||l.split(/[\\/]/).includes(".."))return p(`names a feature file outside the project: ${m.feature}`);if(!a.meta.bound)return p("carries no binding (a runner older than 0.8.5 filed it), so nothing can tell whether it fits this checkout");let b=o?`. ${o}`:"";if(!G.existsSync(V.join(e,l)))return p(`is for ${m.feature}, which is not in this checkout${b}`);let $=Be(Y(e,l,d));if($&&Rn($)===Rn(a.cache))return{...m,outcome:"unchanged",reason:"already accepted here: the committed recording is this proposal's"};let k;try{k=ke(e,a,r)}catch(w){k=`cannot be checked: ${Rt(w)}`}if(k)return p(`does not fit this checkout: ${Re(k,1e3)}${b}`);G.mkdirSync(V.dirname(f),{recursive:!0});try{return Wt(f,()=>{if(G.existsSync(f)){if(qe(f)===c)return{...m,outcome:"unchanged"};if(!s)return p("differs from the proposal already pending here for this scenario: review that one first (saffron diff), or import with --overwrite to replace it")}return Ae(f,t),{...m,outcome:"imported"}})}catch(w){if(w instanceof xt)return p("is being decided by another process. Try again.");throw w}}async function nc(e,t,n,r={}){let s;try{let o=[];for(let i of t)for(let a of await Xf(i,()=>(s??=xo(e)).dir))o.push(eu(r.describe?.(a.source)??a.source,a.file));if(o.reduce((i,a)=>i+[...a.entries.values()].reduce((c,l)=>c+l.size,0),0)>So)throw new L(Vf);return au(e,o.map(ru),n,r)}finally{s?.remove()}}function au(e,t,n,r){let s=t;if(s.length===0)throw new L("no bundle to import");let o=new Set,i=Pr(e);for(let y of s){let u=y.manifest.repository;if(!u&&!r.allowUnknownRepository)throw new L(`${y.source} does not say which repository it comes from (it was made outside a git checkout). Import it with --allow-unknown-repository to check it by its proposals' bindings only`);if(!u)o.add(`${y.source} does not say which repository it comes from: it is checked by its proposals' bindings only`);else if(i.length===0)o.add(`this project has no git remote to compare ${u} with: the bundle is checked by its proposals' bindings only`);else if(!i.includes(u)){let v=h=>h.slice(h.indexOf("/")),S=i.find(h=>v(h)===v(u));if(!S)throw new L(`${y.source} comes from ${u}, and this checkout is ${i.join(", ")}. Import it into a checkout of ${u}`);o.add(`${s.length===1?"the bundle comes":"the bundles come"} from ${u}, and this checkout is ${S}: the same repository under another host name (an SSH host alias, or a mirror), so each proposal is checked by its binding`)}}if(s.length>1){let y=new Set(s.map(g=>g.manifest.run??""));if(y.size>1)throw new L(`these bundles come from different runs (${[...y].map(g=>g||"none recorded").join(", ")}). Import one run at a time`);let u=g=>Number(g.manifest.attempt)||0,v=new Map;for(let g of s){let T=g.manifest.shard?.index;if(T===void 0)continue;let E=v.get(T);(!E||u(g)>u(E))&&v.set(T,g)}let S=s.filter(g=>g.manifest.shard&&u(g)<u(v.get(g.manifest.shard.index)));for(let g of S){let T=v.get(g.manifest.shard.index);o.add(`${g.source} is shard ${g.manifest.shard.index} of attempt ${u(g)}, which attempt ${u(T)} ran again: left out`)}s=s.filter(g=>!S.includes(g));let h=s.filter(g=>!g.manifest.shard);if(h.length>0)throw new L(`${h.map(g=>g.source).join(", ")} ${h.length===1?"holds a whole run, not a shard of one":"hold whole runs, not shards of one"}: import one run at a time`)}let a,c=s.filter(y=>y.report);if(s.length===1&&c.length===1){let{shard:y,history:u,...v}=c[0].report;a=v}else if(c.length>0)try{a=Ar(c.map(y=>({file:y.source,report:y.report})),{partial:!0}).report}catch(y){throw y instanceof Xt?new L(`these shard bundles do not belong together: ${y.message}`):y}let l=s[0].manifest,d=su(e,l);d&&o.add(d),l.headCommit&&l.commit&&o.add(`the run tested ${l.commit.slice(0,7)}, a pull request's merge commit, not the head of ${l.branch??"its branch"} (${l.headCommit.slice(0,7)}): each proposal is checked against this checkout by its binding`);let f=l.headCommit??l.commit,m=d&&f?`The bundle was made at ${f.slice(0,7)}${l.branch?` on ${l.branch}`:""}: check that out and import again`:void 0,p={written:0,unchanged:0},b=new Map;for(let y of a?.scenarios??[])for(let u of y.evidence??[])b.set(V.posix.dirname(u.file),{feature:y.feature,scenario:y.scenario});for(let y of s)for(let{entry:u,sha256:v}of y.manifest.evidence){let S=V.join(e,".saffron",...u.split("/"));if(nu(S)===v){p.unchanged++;continue}y.write(u,S,v),p.written++;let h=b.get(`.saffron/${V.posix.dirname(u)}`);h&&G.writeFileSync(V.join(V.dirname(S),Bt),JSON.stringify(h,null,2))}let $;if(a){let y=s.flatMap(v=>v.manifest.shard?[v.manifest.shard]:[]);a.imported={...ho(l),...y.length>0?{shards:{imported:y.map(v=>v.index).sort((v,S)=>v-S),total:y[0].total}}:{}};for(let v of a.scenarios){let S=typeof v.proposalFile=="string"?v.proposalFile.split(/[\\/]/).slice(-2):[];ec.test(`proposals/${S.join("/")}`)&&(v.proposalFile=V.join(e,".saffron","proposals",...S))}let u=V.join(e,".saffron","reports","latest.json");ou(u,Buffer.from(`${JSON.stringify(a,null,2)}
258
+ `))?$="unchanged":(Pn(e,a),$="written")}let k=[],w=On(e),x=JSON.stringify(w);for(let y of s)for(let u of y.manifest.proposals){let v=iu(e,y.files.get(u.entry),u.entry,n,r.overwrite===!0,m),S=v.reason===void 0?v:{...v,reason:Re(v.reason,1/0)};k.push(S),(S.outcome==="imported"||S.outcome==="unchanged"&&!S.reason)&&(w[S.file]={revision:u.revision,from:{...ho(y.manifest),...y.manifest.shard?{shard:y.manifest.shard}:{}}})}for(let[y,u]of Object.entries(w)){let v;try{v=qe(V.join(e,y))}catch{v=void 0}v!==u.revision&&delete w[y]}return JSON.stringify(w)!==x&&Ae(tc(e),`${JSON.stringify(w,null,2)}
259
+ `),{bundles:s.map(y=>{let{runner:u,createdAt:v,shard:S}=y.manifest;return{source:y.source,runner:u,createdAt:v,origin:ho(y.manifest),...S?{shard:{index:S.index,total:S.total}}:{}}}),warnings:[...o],proposals:k,evidence:p,...$?{report:$}:{}}}var cu=/^\s*(?:Scenario(?: Outline| Template)?|Example):\s*(.+?)\s*$/,lu=/^\s*StepSet:/;function ft(e,t){return Ve.relative(e,t).split(Ve.sep).join("/")}function du(e,t){let n={green:0,yellow:1,red:2};return!e||n[t]>n[e]?t:e}function rc(e,t){return`${e} :: ${t}`}function jn(e,t,n={}){let r=t.features??"features",s=Ve.isAbsolute(r)?r:Ve.join(e,r),o=Ve.join(e,".saffron","reports","latest.json"),i=Ve.join(e,".saffron","reports","latest.html"),a;try{a=JSON.parse(Ie.readFileSync(o,"utf8"))}catch{a=void 0}let c=new Map;for(let g of a?.scenarios??[]){let T=g.scenario.replace(/ \(example \d+\)$/,""),E=rc(g.feature,T);c.set(E,du(c.get(E),g.status))}let l=new Map,d;try{l=et(St(s),e)}catch(g){d=g instanceof Error?g.message:String(g)}let f=Ie.existsSync(s)?[...Ie.readdirSync(s,{recursive:!0})].map(String).filter(tt).map(g=>Ve.join(s,g)).sort():[],m=[],p=new Map,b=new Map,$={parsedFiles:new Set,owned:new Set,unreadable:d?[d]:[]};for(let g of f){let T=ft(e,g),E="";try{E=Ie.readFileSync(g,"utf8")}catch(P){m.push({path:T,name:T,scenarios:[],stepSets:0,error:String(P)}),$.unreadable.push(`${T}: ${String(P)}`);continue}let _=E.split(`
260
+ `),A=new Map,C=0;_.forEach((P,R)=>{let O=P.match(cu);O&&!A.has(O[1])?A.set(O[1],R+1):lu.test(P)&&C++});let I=_.find(P=>/^\s*Feature:/.test(P)),j=I?I.replace(/^\s*Feature:\s*/,"").trim():T,M;try{M=me(g,e,l)}catch(P){m.push({path:T,name:j,scenarios:[],stepSets:C,error:d??(P instanceof Error?P.message:String(P))}),$.unreadable.push(`${T}: ${P instanceof Error?P.message:String(P)}`);continue}$.parsedFiles.add(g),b.set(g,M);let z=new Map;for(let P of M){$.owned.add(Ue(e,P.featurePath,P.displayName));let R=z.get(P.name);if(R){R.rows+=1;continue}let O=Y(e,P.featurePath,P.name);$.owned.add(O),$.owned.add(xe(e,P.featurePath,P.name));let D="missing",W;if(Ie.existsSync(O))try{let U=ye(O);D=U&&Ws(P,U)?"valid":"stale"}catch(U){D="invalid",W=U instanceof X?U.problem:U instanceof Error?U.message:String(U)}z.set(P.name,{name:P.name,line:A.get(P.name),tags:[...P.tags],outline:Object.keys(P.params).length>0,rows:1,cached:D!=="missing",cacheState:D,...W?{cacheProblem:W}:{},proposal:Ie.existsSync(xe(e,P.featurePath,P.name)),lastStatus:c.get(rc(P.featurePath,P.name))});for(let U of P.tags)p.set(U,(p.get(U)??0)+1)}m.push({path:T,name:j,scenarios:[...z.values()],stepSets:C})}let k=[],w=On(e),x=Je(e,(g,T)=>k.push({file:ft(e,g),problem:T})).map(({file:g,proposal:T,revision:E})=>({file:ft(e,g),feature:T.cache.feature,scenario:T.cache.scenario,mode:T.meta.mode,createdAt:T.meta.createdAt,verified:T.meta.verified,proofError:T.meta.proofError,stale:T.meta.bound?ke(e,T,l):void 0,unbound:T.meta.bound?void 0:!0,revision:E,adaptations:T.meta.adaptations??[],narrative:T.meta.narrative,suggestedFeatureEdit:T.meta.suggestedFeatureEdit,recordedFor:T.meta.recordedFor,aiCalls:T.meta.usage?.aiCalls??0,costUsd:T.meta.usage&&Te({costUsd:T.meta.usage.costUsd??0,aiCalls:T.meta.usage.aiCalls})?T.meta.usage.costUsd:void 0,importedFrom:w[ft(e,g)]?.revision===E?w[ft(e,g)].from:void 0})).sort((g,T)=>g.file.localeCompare(T.file)),y;try{y=He(e,r,l,b)}catch{y={steps:[],stepSets:[],scannedFiles:0,scannedCaches:0}}let u=fu(e),v=En(e,Math.max(100,n.historyLimit??30)),S=v.slice(-(n.historyLimit??30)).map(g=>({startedAt:g.startedAt,green:g.green,yellow:g.yellow,red:g.red,aiCalls:g.aiCalls,costUsd:Te(g)?g.costUsd:void 0})),h=Ve.join(e,"saffron.config.json");return{tool:"saffron",version:Oe(),projectRoot:e,packageInstalled:Ie.existsSync(Ve.join(e,"node_modules","saffron-ai")),config:{file:Ie.existsSync(h)?"saffron.config.json":void 0,effective:Ii(t)},features:m,tags:[...p.entries()].map(([g,T])=>({tag:g,scenarios:T})).sort((g,T)=>g.tag.localeCompare(T.tag)),proposals:x,unreadableProposals:k,lastRun:a?{startedAt:a.startedAt,finishedAt:a.finishedAt,totals:{...a.totals,costUsd:Te(a.totals)?a.totals.costUsd:void 0},reportHtml:Ie.existsSync(i)?ft(e,i):void 0,attention:a.scenarios.filter(g=>g.status!=="green").map(g=>({feature:g.feature,scenario:g.scenario,baseScenario:g.scenario.replace(/ \(example \d+\)$/,""),status:g.status,error:g.status==="red"?g.error?.split(`
261
+ `)[0]:void 0,failedStep:(g.steps??[]).find(T=>T.status==="failed")?.gherkin??g.evidence?.find(T=>T.step)?.step,evidence:(g.evidence??[]).filter(T=>T.kind==="trace"?Ie.existsSync(Ve.join(e,T.file)):Jt(e,T)!==void 0).map(T=>({kind:T.kind,file:T.file,...T.step?{step:T.step}:{},...T.sha256?{sha256:T.sha256}:{}}))})).sort((g,T)=>g.status===T.status?0:g.status==="red"?-1:1)}:void 0,history:S,quality:ja(m,v),vocabulary:{steps:y.steps.length,recorded:y.steps.filter(g=>g.status==="recorded").length,unrecorded:y.steps.filter(g=>g.status==="unrecorded").length,stepSets:y.stepSets.length,divergent:y.steps.filter(g=>g.status==="divergent").map(g=>g.text),duplicateWordings:u.slice(0,50)},data:(()=>{let g=Le(),T=[],E=new Set;for(let[_,A]of b)for(let C of A)for(let I of mt(de(C)))if(!E.has(I)){E.add(I);try{Qn(I)}catch(j){T.push({ref:I,reason:j instanceof Error?j.message:String(j),feature:ft(e,_),scenario:C.name})}}return{dir:ft(e,g.dir),env:g.env,files:Ie.existsSync(g.dir)?Ie.readdirSync(g.dir).filter(_=>/\.(json|csv)$/.test(_)).sort():[],unresolved:T}})(),orphans:(()=>{let g=mr(e,r,$);return g.unreadable.length?[]:g.orphans})()}}function oc(e){let t=e.features.reduce((o,i)=>o+i.scenarios.length,0),n=e.features.reduce((o,i)=>o+i.scenarios.filter(a=>a.cacheState==="valid").length,0),r=e.features.reduce((o,i)=>o+i.scenarios.filter(a=>a.cacheState==="stale"||a.cacheState==="invalid").length,0);console.log(`saffron ${e.version} \xB7 ${e.features.length} feature file(s) \xB7 ${t} scenario(s), ${n} replay from cache`+(r?`, ${r} stale (\u26A0) and will re-record`:""));for(let o of e.features){let i=o.scenarios.map(a=>a.proposal?"\u25D0":a.cacheState==="valid"?"\u25CF":a.cacheState==="missing"?"\u25CB":"\u26A0").join("");console.log(` ${o.path} ${i}${o.error?` \u2717 ${o.error.split(`
262
+ `)[0]}`:""}`)}if(e.tags.length&&console.log(`tags: ${e.tags.map(o=>`${o.tag} (${o.scenarios})`).join(", ")}`),console.log(e.proposals.length?`${e.proposals.length} proposal(s) pending review: ${e.proposals.map(o=>o.file).join(", ")}`:"no proposals pending review",...e.unreadableProposals.map(o=>`\u26A0 proposal ${o.file} cannot be read: ${o.problem}`),...e.features.flatMap(o=>o.scenarios.filter(i=>i.cacheProblem).map(i=>`\u26A0 recording of ${o.path} \u203A ${i.name} cannot be read: ${i.cacheProblem}`))),e.lastRun){let o=e.lastRun.totals;console.log(`last run ${e.lastRun.finishedAt}: ${o.green} passed \xB7 ${o.yellow} pending review \xB7 ${o.red} failed \xB7 ${o.costUsd===void 0?"cost not reported":`$${o.costUsd.toFixed(2)}`}`)}let s=e.vocabulary;console.log(`vocabulary: ${s.steps} steps (${s.recorded} recorded, ${s.unrecorded} unrecorded), ${s.stepSets} step sets, ${s.divergent.length} divergent, ${s.duplicateWordings.length} duplicate wording group(s)`),e.orphans.length&&console.log(`${e.orphans.length} recording(s) belong to no scenario: run \`saffron prune\` to see them`),(e.data.files.length||e.data.unresolved.length)&&console.log(`test data: ${e.data.files.length} file(s) in ${e.data.dir}/${e.data.env?` \xB7 environment "${e.data.env}"`:""}`+(e.data.unresolved.length?` \xB7 ${e.data.unresolved.length} reference(s) do not resolve: ${e.data.unresolved.map(o=>`{data:${o.ref}}`).join(", ")}`:"")),console.log(`config: ${e.config.file??"(defaults, no saffron.config.json)"}`)}var pu=/^(<[^>]+>|\{env:[^}]+\}|\{date[^}]*\})$/;function sc(e){return e?e.role?`${e.role}${e.name?` "${e.name}"`:e.nameRegex?` /${e.nameRegex}/`:""}`:e.label?`label "${e.label}"`:e.text?`text "${e.text}"`:e.testId?`testid ${e.testId}`:e.placeholder?`placeholder "${e.placeholder}"`:e.fallbackSelectors?.length?e.fallbackSelectors[0]:"":""}function fu(e){let t;try{t=ze(e)}catch{return[]}let n=new Map;for(let r of t.steps.values()){if(r.actions.length===0)continue;let s=ir(r.gherkin).pattern,o=[...r.gherkin.matchAll(/"([^"]*)"/g)].map(f=>f[1]),i=f=>f!==void 0&&(o.includes(f)||pu.test(f))?"\xABarg\xBB":f,a=r.actions.map(f=>{let{pageFingerprint:m,value:p,promptText:b,...$}=f;return{...$,value:i(p),promptText:i(b)}}),c=JSON.stringify(a),l=r.actions.map(f=>`${f.action}${sc(f.target)?` ${sc(f.target)}`:f.url?` ${f.url}`:""}`).join(" \xB7 "),d=n.get(c)??{steps:new Map,actions:l};d.steps.has(s)||d.steps.set(s,r.gherkin),n.set(c,d)}return[...n.values()].filter(r=>r.steps.size>1).map(r=>({steps:[...r.steps.values()].sort(),actions:r.actions})).sort((r,s)=>s.steps.length-r.steps.length||r.steps[0].localeCompare(s.steps[0]))}async function ic(e,t){let n=new uu({name:"saffron",version:Oe()},{capabilities:{tools:{}}});n.setRequestHandler(mu,async()=>({tools:[{name:"search_steps",description:"Search the project's step vocabulary. Returns known Gherkin steps with status (recorded = replays at zero tokens, divergent, unrecorded), usage counts, and source files. ALWAYS reuse an existing wording exactly instead of inventing a near-duplicate: exact step text is the replay-cache identity, so a new wording costs a fresh AI recording.",inputSchema:{type:"object",properties:{query:{type:"string",description:"Case-insensitive substring filter over step text; omit to list everything."}}}},{name:"list_step_sets",description:"List the project's StepSet definitions (named reusable step sequences from .saffron files) with their steps, defining file, and usage. Invoke one inside a scenario with a line reading exactly: StepSet <name>",inputSchema:{type:"object",properties:{}}},{name:"project_status",description:"The project overview (same data as `saffron status --json`): every feature file with its scenarios (header line, tags, outline rows, cached / proposal pending / last run status), the tag list, pending proposals with narrative and proof-replay verdict (and, for one imported from CI with `saffron import`, the run, branch and commit it came from, as importedFrom), the last run's totals, run history, vocabulary health (divergent steps, duplicate wordings proven by identical recordings) and the effective config. Call it before editing tests to see what is already recorded and what awaits review.",inputSchema:{type:"object",properties:{}}}]})),n.setRequestHandler(hu,async r=>yu(r.params.name,r.params.arguments,e,t)),await n.connect(new gu)}function yu(e,t,n,r){let s=o=>({content:[{type:"text",text:JSON.stringify(o,null,2)}]});if(e==="search_steps"){let o=He(n,r),i=typeof t?.query=="string"?t.query:void 0,a=i?ar(o,i):o;return s({steps:a.steps,scannedFiles:a.scannedFiles,scannedCaches:a.scannedCaches})}if(e==="list_step_sets"){let o=He(n,r);return s({stepSets:o.stepSets})}return e==="project_status"?s(jn(n,ee(n))):{content:[{type:"text",text:`Unknown tool: ${e}`}],isError:!0}}import{query as bu}from"@anthropic-ai/claude-agent-sdk";function ac(e,t){let n=t.steps.map(s=>`- [${s.status}] ${s.keyword} ${s.text}`).join(`
263
263
  `),r=t.stepSets.map(s=>`- StepSet ${s.name}
264
264
  ${s.steps.map(o=>` ${o}`).join(`
265
265
  `)}`).join(`
266
266
  `);return["Draft a Saffron feature file from the requirements below.","","EXISTING STEP VOCABULARY \u2014 reuse these wordings EXACTLY (character for character) wherever they express what a step needs to do. Steps marked [recorded] or [divergent] already have replay recordings: reusing them makes the new file replay almost for free. Only invent a new wording when nothing in the vocabulary expresses the intent, and then follow the same style:",n.length>0?n:"(none yet \u2014 this is the project's first file)","",t.stepSets.length>0?`EXISTING STEP SETS \u2014 invoke with a line reading exactly \`StepSet <name>\` instead of repeating their steps:
267
267
  ${r}`:void 0,"","RULES:","1. Output ONLY the feature file content \u2014 no prose, no code fences, no explanations.","2. Gherkin/.saffron syntax: Feature:, Scenario:, Scenario Outline: + Examples, Given/When/Then/And/But. Prefer concrete scenarios; use an Outline only when the requirements list variations of the same flow.","3. Then steps are assertions \u2014 state exactly what must be true, one condition per step. End every scenario with at least one Then.","4. If the same 3+ step sequence would appear in two or more scenarios, define it once as a StepSet (first step a guard, last step an exit assertion) and invoke it. Definition uses a colon (StepSet: Name), invocation does not (StepSet Name).",'5. Use 2-column data tables for key/value input, header tables for lists of records, and """ doc strings for free text.',"6. Steps are natural language for an AI browser agent \u2014 never CSS selectors or code.","","REQUIREMENTS:",e.trim()].filter(s=>s!==void 0).join(`
268
- `)}var Va="You are Saffron's feature-file author. You write clean, assertive Gherkin that reuses the project's exact step vocabulary. You output only file content.";async function Ya(e,t,n,r){let s={aiCalls:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreationTokens:0,costUsd:0},o="";if(r)return await r({systemPrompt:Va,prompt:Wa(e,t),model:n,maxTurns:1,mcpServers:{},mcpToken:"",usage:s,lastOutput:a=>{o=a},finished:()=>!1}),{content:Ka(o),usage:s};let i=nu({prompt:Wa(e,t),options:{model:n,maxTurns:1,permissionMode:"bypassPermissions",persistSession:!1,settingSources:[],strictMcpConfig:!0,settings:{disableClaudeAiConnectors:!0},allowedTools:[],systemPrompt:Va}});for await(let a of i)if(a.type==="assistant")for(let c of a.message.content)c.type==="text"&&(o+=c.text);else a.type==="result"&&(s.aiCalls=a.num_turns,s.costUsd=a.total_cost_usd,"usage"in a&&a.usage&&(s.inputTokens=a.usage.input_tokens??0,s.outputTokens=a.usage.output_tokens??0,s.cacheReadTokens=a.usage.cache_read_input_tokens??0,s.cacheCreationTokens=a.usage.cache_creation_input_tokens??0));return{content:Ka(o),usage:s}}function Ka(e){return e.replace(/^\s*```(?:gherkin|saffron)?\s*\n/,"").replace(/\n```\s*$/,"").trim()+`
269
- `}import Ne from"node:fs";import{fileURLToPath as Tt,pathToFileURL as fo}from"node:url";import ge from"node:path";import{createConnection as ru,ProposedFeatures as su,TextDocuments as ou,TextDocumentSyncKind as iu,DidChangeWatchedFilesNotification as au,WatchKind as cu,CompletionItemKind as pt,InsertTextFormat as Za,DiagnosticSeverity as In,MarkupKind as Qa}from"vscode-languageserver/node";import{TextDocument as lu}from"vscode-languageserver-textdocument";var Xa=/^(\s*)(Given|When|Then|And|But|\*)\s+(.+?)\s*$/,du=/^\s*StepSet:\s*(.+?)\s*$/,uo=/^(\s*)(StepSet)\s+([^\s:].*?)\s*$/,go={recorded:"\u25CF recorded \u2014 replays at zero tokens",divergent:"\u25CF divergent \u2014 same text, different recordings",unrecorded:"\u25CB written, not recorded yet"};function pu(e,t){let n=i=>new Set(i.toLowerCase().replace(/"[^"]*"/g,'"x"').split(/[^a-z0-9"<>]+/).filter(Boolean)),r=n(e),s=n(t);if(r.size===0||s.size===0)return 0;let o=0;for(let i of r)s.has(i)&&o++;return o/(r.size+s.size-o)}function ec(e){try{return JSON.parse(Ne.readFileSync(ge.join(e,"saffron.config.json"),"utf8"))}catch{return{}}}function tc(e){try{let t=JSON.parse(Ne.readFileSync(ge.join(e,"saffron.config.json"),"utf8"));return typeof t.features=="string"?t.features:void 0}catch{return}}function _n(e,t){let n=ge.dirname(e),r=ge.resolve(t);for(;;){if(Ne.existsSync(ge.join(n,"saffron.config.json")))return n;if(n===r||ge.dirname(n)===n)return t;n=ge.dirname(n)}}function oc(e,t,n={}){let r=e,s=t,o=e,i=!1,a=u=>{if(!u.startsWith("file:"))return;let g=_n(Tt(u),o);if(g===r)return;w(g);let R=v.get(g);if(!R||!i||k.has(g))return y();({vocab:m,registry:p,files:b}=R);for(let E of f.all())l(E)&&$(E)},c=u=>{if(!u.startsWith("file:"))return;let g=Tt(u),R=_n(g,o);v.get(R)?.files.has(g)||k.add(R)},l=u=>!u.uri.startsWith("file:")||_n(Tt(u.uri),o)===r,d=ru(su.all,process.stdin,process.stdout),f=new ou(lu),m={steps:[],stepSets:[],scannedFiles:0,scannedCaches:0},p=new Map,b=new Set,v=new Map,k=new Set,w=u=>{r=u,s=tc(u)??(Ne.existsSync(ge.join(u,"saffron.config.json"))?"features":t);try{let g=ec(u);Me(u,g.dataDir,process.env.SAFFRON_ENV??g.env)}catch(g){d.console.warn(`saffron vocabulary rebuild: ${String(g)}`)}},x,y=()=>{try{let u=ge.isAbsolute(s)?s:ge.join(r,s),g=ec(r);Me(r,g.dataDir,process.env.SAFFRON_ENV??g.env),p=Qe(wt(u),r),m=ze(r,s,p),b=new Set(ds(u)),v.set(r,{vocab:m,registry:p,files:b}),k.delete(r)}catch(u){d.console.warn(`saffron vocabulary rebuild: ${String(u)}`),{vocab:m,registry:p,files:b}=v.get(r)??{vocab:{steps:[],stepSets:[],scannedFiles:0,scannedCaches:0},registry:new Map,files:new Set}}for(let u of f.all())l(u)&&$(u)},h=()=>{clearTimeout(x),x=setTimeout(()=>{let u=r,g=new Set(f.all().filter(R=>R.uri.startsWith("file:")).map(R=>_n(Tt(R.uri),o)));for(let R of v.keys())!g.has(R)&&R!==u&&v.delete(R);g.delete(u);for(let R of[...g,u])w(R),y()},300)};function S(u){return m.steps.find(g=>g.text===u)?.status}function $(u){let g=u.getText().split(`
270
- `),R=gu(g);if(!u.uri.endsWith(".saffron")){d.sendDiagnostics({uri:u.uri,diagnostics:R});return}let E=new Map;for(let I=0;I<g.length;I++){let A=g[I],P={start:{line:I,character:0},end:{line:I,character:A.length}},_=A.match(uo);_&&!p.has(_[3])&&R.push({range:P,severity:In.Error,source:"saffron",message:`StepSet "${_[3]}" is not defined anywhere in the project.`});let j=A.match(du);if(j){let L=p.get(j[1]),T=ge.relative(r,Tt(u.uri)).replace(/\\/g,"/");(E.has(j[1])||L&&L.file.replace(/\\/g,"/")!==T)&&R.push({range:P,severity:In.Error,source:"saffron",message:`StepSet "${j[1]}" is defined twice \u2014 set names are project-wide. If this line was meant to INVOKE the set, remove the colon: "StepSet ${j[1]}".`}),E.set(j[1],I)}let F=A.trim();if(F&&!_&&!j){for(let[L]of p)if(F.toLowerCase()===L.toLowerCase()){R.push({range:P,severity:In.Warning,source:"saffron",message:`Did you mean "StepSet ${L}"? Invoking a step set needs the StepSet keyword, like a step needs Given/When/Then.`});break}}let C=A.match(Xa);if(C&&S(C[3])!=="recorded"){for(let L of m.steps)if(!(L.text===C[3]||L.status==="unrecorded")&&pu(L.text,C[3])>=.7){R.push({range:{start:{line:I,character:A.indexOf(C[3])},end:{line:I,character:A.length}},severity:In.Information,source:"saffron",message:`Similar to the ${L.status} step "${L.text}" (\xD7${L.usage}). Reusing the exact wording replays at zero tokens; a new wording costs a fresh AI recording.`});break}}}d.sendDiagnostics({uri:u.uri,diagnostics:R})}d.onInitialize(u=>{if(n.rootFromClient){let g=u.workspaceFolders?.[0]?.uri??u.rootUri??void 0;if(g&&g.startsWith("file:")){let R=Tt(g);Ne.existsSync(R)&&(o=r=R,s=tc(R)??t)}}return i=u.capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration===!0,y(),{capabilities:{textDocumentSync:iu.Incremental,completionProvider:{triggerCharacters:[" ",":","."]},definitionProvider:!0,hoverProvider:!0}}}),d.onInitialized(()=>{if(!i)return;let u=[...["**/*.saffron","**/*.feature","**/saffron.config.json","**/.saffron/cache/**","**/data/**"].map(g=>({globPattern:g})),{globPattern:"**",kind:cu.Delete}];d.client.register(au.type,{watchers:u}).catch(()=>{i=!1})}),d.onCompletion(u=>{let g=f.get(u.textDocument.uri);if(!g)return[];a(g.uri);let R=g.getText({start:{line:u.position.line,character:0},end:u.position}),E=hu(R);return E||mu(g.uri,R,u.position.line,m)}),d.onDefinition(u=>{let g=f.get(u.textDocument.uri);if(!g)return;a(g.uri);let R=g.getText({start:{line:u.position.line,character:0},end:{line:u.position.line+1,character:0}}).replace(/\n$/,""),E=nc(R,u.position.character);if(E){let j=uu(E.ref);return j?{uri:fo(j.file).href,range:{start:{line:j.line,character:0},end:{line:j.line,character:0}}}:void 0}let I=rc(R,u.position.character);if(I){let j=sc(r,I.name);return j?{uri:fo(j.file).href,range:{start:{line:j.line,character:0},end:{line:j.line,character:0}}}:void 0}let A=R.match(uo);if(!A)return;let P=p.get(A[3]);return P?{uri:fo(ge.join(r,P.file)).href,range:{start:{line:P.line-1,character:0},end:{line:P.line-1,character:0}}}:void 0}),d.onHover(u=>{let g=f.get(u.textDocument.uri);if(!g)return;a(g.uri);{let A=g.getText({start:{line:u.position.line,character:0},end:{line:u.position.line+1,character:0}}).replace(/\n$/,""),P=nc(A,u.position.character);if(P)return{contents:{kind:"markdown",value:ic(P.ref)}};let _=rc(A,u.position.character);if(_){let j=sc(r,_.name),F=j?`defined in ${ge.relative(r,j.file)} (line ${j.line+1}); the value is resolved at replay and never shown here`:"not found in .env or .env.example; export it in the shell or add it to a git-ignored .env";return{contents:{kind:"markdown",value:`**{env:${_.name}}** ${F}.`}}}}let R=g.getText({start:{line:u.position.line,character:0},end:{line:u.position.line+1,character:0}}).replace(/\n$/,""),E=R.match(uo);if(E){let A=m.stepSets.find(_=>_.name===E[3]);if(!A)return;let P=A.steps.map(_=>`- ${go[S(_)??"unrecorded"][0]} ${_}`).join(`
271
- `);return{contents:{kind:Qa.Markdown,value:`**StepSet ${A.name}** \u2014 expands to:
268
+ `)}var cc="You are Saffron's feature-file author. You write clean, assertive Gherkin that reuses the project's exact step vocabulary. You output only file content.";async function dc(e,t,n,r){let s={aiCalls:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreationTokens:0,costUsd:0},o="";if(r)return await r({systemPrompt:cc,prompt:ac(e,t),model:n,maxTurns:1,mcpServers:{},mcpToken:"",usage:s,lastOutput:a=>{o=a},finished:()=>!1}),{content:lc(o),usage:s};let i=bu({prompt:ac(e,t),options:{model:n,maxTurns:1,permissionMode:"bypassPermissions",persistSession:!1,settingSources:[],strictMcpConfig:!0,settings:{disableClaudeAiConnectors:!0},allowedTools:[],systemPrompt:cc}});for await(let a of i)if(a.type==="assistant")for(let c of a.message.content)c.type==="text"&&(o+=c.text);else a.type==="result"&&(s.aiCalls=a.num_turns,s.costUsd=a.total_cost_usd,"usage"in a&&a.usage&&(s.inputTokens=a.usage.input_tokens??0,s.outputTokens=a.usage.output_tokens??0,s.cacheReadTokens=a.usage.cache_read_input_tokens??0,s.cacheCreationTokens=a.usage.cache_creation_input_tokens??0));return{content:lc(o),usage:s}}function lc(e){return e.replace(/^\s*```(?:gherkin|saffron)?\s*\n/,"").replace(/\n```\s*$/,"").trim()+`
269
+ `}import Fe from"node:fs";import{fileURLToPath as Et,pathToFileURL as $o}from"node:url";import he from"node:path";import{createConnection as wu,ProposedFeatures as vu,TextDocuments as Su,TextDocumentSyncKind as xu,DidChangeWatchedFilesNotification as $u,WatchKind as ku,CompletionItemKind as ut,InsertTextFormat as pc,DiagnosticSeverity as Fn,MarkupKind as fc}from"vscode-languageserver/node";import{TextDocument as Tu}from"vscode-languageserver-textdocument";var uc=/^(\s*)(Given|When|Then|And|But|\*)\s+(.+?)\s*$/,Ru=/^\s*StepSet:\s*(.+?)\s*$/,ko=/^(\s*)(StepSet)\s+([^\s:].*?)\s*$/,To={recorded:"\u25CF recorded \u2014 replays at zero tokens",divergent:"\u25CF divergent \u2014 same text, different recordings",unrecorded:"\u25CB written, not recorded yet"};function Eu(e,t){let n=i=>new Set(i.toLowerCase().replace(/"[^"]*"/g,'"x"').split(/[^a-z0-9"<>]+/).filter(Boolean)),r=n(e),s=n(t);if(r.size===0||s.size===0)return 0;let o=0;for(let i of r)s.has(i)&&o++;return o/(r.size+s.size-o)}function gc(e){try{return JSON.parse(Fe.readFileSync(he.join(e,"saffron.config.json"),"utf8"))}catch{return{}}}function hc(e){try{let t=JSON.parse(Fe.readFileSync(he.join(e,"saffron.config.json"),"utf8"));return typeof t.features=="string"?t.features:void 0}catch{return}}function Nn(e,t){let n=he.dirname(e),r=he.resolve(t);for(;;){if(Fe.existsSync(he.join(n,"saffron.config.json")))return n;if(n===r||he.dirname(n)===n)return t;n=he.dirname(n)}}function wc(e,t,n={}){let r=e,s=t,o=e,i=!1,a=h=>{if(!h.startsWith("file:"))return;let g=Nn(Et(h),o);if(g===r)return;w(g);let T=$.get(g);if(!T||!i||k.has(g))return y();({vocab:m,registry:p,files:b}=T);for(let E of f.all())l(E)&&S(E)},c=h=>{if(!h.startsWith("file:"))return;let g=Et(h),T=Nn(g,o);$.get(T)?.files.has(g)||k.add(T)},l=h=>!h.uri.startsWith("file:")||Nn(Et(h.uri),o)===r,d=wu(vu.all,process.stdin,process.stdout),f=new Su(Tu),m={steps:[],stepSets:[],scannedFiles:0,scannedCaches:0},p=new Map,b=new Set,$=new Map,k=new Set,w=h=>{r=h,s=hc(h)??(Fe.existsSync(he.join(h,"saffron.config.json"))?"features":t);try{let g=gc(h);De(h,g.dataDir,process.env.SAFFRON_ENV??g.env)}catch(g){d.console.warn(`saffron vocabulary rebuild: ${String(g)}`)}},x,y=()=>{try{let h=he.isAbsolute(s)?s:he.join(r,s),g=gc(r);De(r,g.dataDir,process.env.SAFFRON_ENV??g.env),p=et(St(h),r),m=He(r,s,p),b=new Set(bs(h)),$.set(r,{vocab:m,registry:p,files:b}),k.delete(r)}catch(h){d.console.warn(`saffron vocabulary rebuild: ${String(h)}`),{vocab:m,registry:p,files:b}=$.get(r)??{vocab:{steps:[],stepSets:[],scannedFiles:0,scannedCaches:0},registry:new Map,files:new Set}}for(let h of f.all())l(h)&&S(h)},u=()=>{clearTimeout(x),x=setTimeout(()=>{let h=r,g=new Set(f.all().filter(T=>T.uri.startsWith("file:")).map(T=>Nn(Et(T.uri),o)));for(let T of $.keys())!g.has(T)&&T!==h&&$.delete(T);g.delete(h);for(let T of[...g,h])w(T),y()},300)};function v(h){return m.steps.find(g=>g.text===h)?.status}function S(h){let g=h.getText().split(`
270
+ `),T=Iu(g);if(!h.uri.endsWith(".saffron")){d.sendDiagnostics({uri:h.uri,diagnostics:T});return}let E=new Map;for(let _=0;_<g.length;_++){let A=g[_],C={start:{line:_,character:0},end:{line:_,character:A.length}},I=A.match(ko);I&&!p.has(I[3])&&T.push({range:C,severity:Fn.Error,source:"saffron",message:`StepSet "${I[3]}" is not defined anywhere in the project.`});let j=A.match(Ru);if(j){let P=p.get(j[1]),R=he.relative(r,Et(h.uri)).replace(/\\/g,"/");(E.has(j[1])||P&&P.file.replace(/\\/g,"/")!==R)&&T.push({range:C,severity:Fn.Error,source:"saffron",message:`StepSet "${j[1]}" is defined twice \u2014 set names are project-wide. If this line was meant to INVOKE the set, remove the colon: "StepSet ${j[1]}".`}),E.set(j[1],_)}let M=A.trim();if(M&&!I&&!j){for(let[P]of p)if(M.toLowerCase()===P.toLowerCase()){T.push({range:C,severity:Fn.Warning,source:"saffron",message:`Did you mean "StepSet ${P}"? Invoking a step set needs the StepSet keyword, like a step needs Given/When/Then.`});break}}let z=A.match(uc);if(z&&v(z[3])!=="recorded"){for(let P of m.steps)if(!(P.text===z[3]||P.status==="unrecorded")&&Eu(P.text,z[3])>=.7){T.push({range:{start:{line:_,character:A.indexOf(z[3])},end:{line:_,character:A.length}},severity:Fn.Information,source:"saffron",message:`Similar to the ${P.status} step "${P.text}" (\xD7${P.usage}). Reusing the exact wording replays at zero tokens; a new wording costs a fresh AI recording.`});break}}}d.sendDiagnostics({uri:h.uri,diagnostics:T})}d.onInitialize(h=>{if(n.rootFromClient){let g=h.workspaceFolders?.[0]?.uri??h.rootUri??void 0;if(g&&g.startsWith("file:")){let T=Et(g);Fe.existsSync(T)&&(o=r=T,s=hc(T)??t)}}return i=h.capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration===!0,y(),{capabilities:{textDocumentSync:xu.Incremental,completionProvider:{triggerCharacters:[" ",":","."]},definitionProvider:!0,hoverProvider:!0}}}),d.onInitialized(()=>{if(!i)return;let h=[...["**/*.saffron","**/*.feature","**/saffron.config.json","**/.saffron/cache/**","**/data/**"].map(g=>({globPattern:g})),{globPattern:"**",kind:ku.Delete}];d.client.register($u.type,{watchers:h}).catch(()=>{i=!1})}),d.onCompletion(h=>{let g=f.get(h.textDocument.uri);if(!g)return[];a(g.uri);let T=g.getText({start:{line:h.position.line,character:0},end:h.position}),E=Pu(T);return E||Cu(g.uri,T,h.position.line,m)}),d.onDefinition(h=>{let g=f.get(h.textDocument.uri);if(!g)return;a(g.uri);let T=g.getText({start:{line:h.position.line,character:0},end:{line:h.position.line+1,character:0}}).replace(/\n$/,""),E=mc(T,h.position.character);if(E){let j=_u(E.ref);return j?{uri:$o(j.file).href,range:{start:{line:j.line,character:0},end:{line:j.line,character:0}}}:void 0}let _=yc(T,h.position.character);if(_){let j=bc(r,_.name);return j?{uri:$o(j.file).href,range:{start:{line:j.line,character:0},end:{line:j.line,character:0}}}:void 0}let A=T.match(ko);if(!A)return;let C=p.get(A[3]);return C?{uri:$o(he.join(r,C.file)).href,range:{start:{line:C.line-1,character:0},end:{line:C.line-1,character:0}}}:void 0}),d.onHover(h=>{let g=f.get(h.textDocument.uri);if(!g)return;a(g.uri);{let A=g.getText({start:{line:h.position.line,character:0},end:{line:h.position.line+1,character:0}}).replace(/\n$/,""),C=mc(A,h.position.character);if(C)return{contents:{kind:"markdown",value:vc(C.ref)}};let I=yc(A,h.position.character);if(I){let j=bc(r,I.name),M=j?`defined in ${he.relative(r,j.file)} (line ${j.line+1}); the value is resolved at replay and never shown here`:"not found in .env or .env.example; export it in the shell or add it to a git-ignored .env";return{contents:{kind:"markdown",value:`**{env:${I.name}}** ${M}.`}}}}let T=g.getText({start:{line:h.position.line,character:0},end:{line:h.position.line+1,character:0}}).replace(/\n$/,""),E=T.match(ko);if(E){let A=m.stepSets.find(I=>I.name===E[3]);if(!A)return;let C=A.steps.map(I=>`- ${To[v(I)??"unrecorded"][0]} ${I}`).join(`
271
+ `);return{contents:{kind:fc.Markdown,value:`**StepSet ${A.name}** \u2014 expands to:
272
272
 
273
- ${P}`}}}let I=R.match(Xa);if(I){let A=m.steps.find(P=>P.text===I[3]);return A?{contents:{kind:Qa.Markdown,value:`${go[A.status]}
273
+ ${C}`}}}let _=T.match(uc);if(_){let A=m.steps.find(C=>C.text===_[3]);return A?{contents:{kind:fc.Markdown,value:`${To[A.status]}
274
274
 
275
275
  Used in ${A.usage} place(s): ${A.files.join(", ")}${A.fromStepSet?`
276
276
 
277
- Defined in StepSet **${A.fromStepSet}**`:""}`}}:void 0}}),f.onDidOpen(u=>{if(c(u.document.uri),u.document.uri.startsWith("file:")){let g=Tt(u.document.uri);_n(g,o)===r&&!b.has(g)&&h()}a(u.document.uri),$(u.document)}),f.onDidClose(u=>c(u.document.uri)),f.onDidChangeContent(u=>{a(u.document.uri),$(u.document)}),f.onDidSave(()=>h()),d.onDidChangeWatchedFiles(()=>h()),f.listen(d),d.listen()}function nc(e,t){let n=/\{data:([A-Za-z0-9_.-]+)\}/g,r;for(;r=n.exec(e);)if(t>=r.index&&t<=r.index+r[0].length)return{ref:r[1],start:r.index,end:r.index+r[0].length}}var fu=e=>/^\s*Examples:/.test(e);function ic(e,t=!1){let{env:n}=De(),r=n?` (environment "${n}")`:"";try{if(t||!e.includes(".")){let{headers:i,rows:a}=Hn(e);return`**{data:${e}}** ${a.length} Examples row(s), columns ${i.map(c=>`\`${c}\``).join(", ")}${r}.`}let s=ot(e);return Array.isArray(ut(e))?`**{data:${e}}** a list of ${s.length}: ${s.map(i=>`"${i}"`).join(", ")}${r}. Use it with "every": replay checks each value, so adding one needs no re-record.`:`**{data:${e}}** = "${s[0]}"${r}. Resolved at replay: change the file and the next run uses the new value at zero tokens.`}catch(s){return`**{data:${e}}** does not resolve: ${s instanceof Error?s.message:String(s)}`}}function uu(e){let{dir:t,env:n}=De(),[r,...s]=e.split("."),o=[n&&`${r}.${n}.json`,`${r}.json`,n&&`${r}.${n}.csv`,`${r}.csv`].filter(Boolean),i=[...s].reverse().find(c=>!/^\d+$/.test(c)),a;for(let c of o){let l=ge.join(t,c);if(!Ne.existsSync(l))continue;let d=Ne.readFileSync(l,"utf8").split(`
278
- `),f=i?d.findIndex(m=>m.includes(`"${i}"`)):0;if(f>=0)return{file:l,line:f};a??={file:l,line:0}}return a}function gu(e){let t=[];return e.forEach((n,r)=>{for(let s of n.matchAll(/\{data:([A-Za-z0-9_.-]+)\}/g)){let o=ic(s[1],fu(n));o.includes("does not resolve")&&t.push({range:{start:{line:r,character:s.index??0},end:{line:r,character:(s.index??0)+s[0].length}},severity:In.Error,source:"saffron",message:o.replace(/\*\*/g,"")})}}),t}function hu(e){let t=e.match(/\{data:([A-Za-z0-9_.-]*)$/);if(!t)return;let n=t[1],{dir:r}=De(),s=n.split(".");if(s.length===1)return(Ne.existsSync(r)?[...new Set(Ne.readdirSync(r).filter(i=>/\.(json|csv)$/.test(i)).map(i=>i.split(".")[0]))].sort():[]).map(i=>({label:i,kind:pt.File,detail:"data file",insertText:i}));try{let o=ut(s.slice(0,-1).join("."));return o===null||typeof o!="object"?[]:Object.entries(o).map(([i,a])=>({label:i,kind:a!==null&&typeof a=="object"?pt.Module:pt.Value,detail:Array.isArray(a)?`list of ${a.length}`:a!==null&&typeof a=="object"?"object":JSON.stringify(a),insertText:i}))}catch{return[]}}function rc(e,t){let n=/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g,r;for(;r=n.exec(e);)if(t>=r.index&&t<=r.index+r[0].length)return{name:r[1],start:r.index,end:r.index+r[0].length}}function sc(e,t){for(let n of[".env",".env.example"]){let r=ge.join(e,n);if(!Ne.existsSync(r))continue;let o=Ne.readFileSync(r,"utf8").split(`
279
- `).findIndex(i=>new RegExp(`^\\s*(?:export\\s+)?${t}\\s*=`).test(i));if(o!==-1)return{file:r,line:o}}}function mu(e,t,n,r){let s=e.endsWith(".saffron");if(/^\s*StepSet\s+/.test(t)&&s)return r.stepSets.map(a=>({label:a.name,kind:pt.Function,detail:`step set \xB7 ${a.steps.length} step(s) \xB7 \xD7${a.usage}`,documentation:a.steps.join(`
280
- `)}));if(/^\s*(Given|When|Then|And|But|\*)\s+/.test(t))return r.steps.map((a,c)=>({label:a.text,kind:pt.Text,detail:`${go[a.status]} \xB7 \xD7${a.usage}`,sortText:String(c).padStart(5,"0"),filterText:a.text}));let i=t.replace(/^\s*/,"").trimEnd().toLowerCase().replace(/\s+/g,"");if(i.length>0&&("stepset:".startsWith(i)||i.startsWith("stepset"))&&s){let a={start:{line:n,character:t.length-t.replace(/^\s*/,"").length},end:{line:n,character:t.length}},c=[{label:"StepSet: \u2026",kind:pt.Keyword,detail:"define a reusable step set",documentation:`StepSet: Name
277
+ Defined in StepSet **${A.fromStepSet}**`:""}`}}:void 0}}),f.onDidOpen(h=>{if(c(h.document.uri),h.document.uri.startsWith("file:")){let g=Et(h.document.uri);Nn(g,o)===r&&!b.has(g)&&u()}a(h.document.uri),S(h.document)}),f.onDidClose(h=>c(h.document.uri)),f.onDidChangeContent(h=>{a(h.document.uri),S(h.document)}),f.onDidSave(()=>u()),d.onDidChangeWatchedFiles(()=>u()),f.listen(d),d.listen()}function mc(e,t){let n=/\{data:([A-Za-z0-9_.-]+)\}/g,r;for(;r=n.exec(e);)if(t>=r.index&&t<=r.index+r[0].length)return{ref:r[1],start:r.index,end:r.index+r[0].length}}var Au=e=>/^\s*Examples:/.test(e);function vc(e,t=!1){let{env:n}=Le(),r=n?` (environment "${n}")`:"";try{if(t||!e.includes(".")){let{headers:i,rows:a}=Zn(e);return`**{data:${e}}** ${a.length} Examples row(s), columns ${i.map(c=>`\`${c}\``).join(", ")}${r}.`}let s=at(e);return Array.isArray(ht(e))?`**{data:${e}}** a list of ${s.length}: ${s.map(i=>`"${i}"`).join(", ")}${r}. Use it with "every": replay checks each value, so adding one needs no re-record.`:`**{data:${e}}** = "${s[0]}"${r}. Resolved at replay: change the file and the next run uses the new value at zero tokens.`}catch(s){return`**{data:${e}}** does not resolve: ${s instanceof Error?s.message:String(s)}`}}function _u(e){let{dir:t,env:n}=Le(),[r,...s]=e.split("."),o=[n&&`${r}.${n}.json`,`${r}.json`,n&&`${r}.${n}.csv`,`${r}.csv`].filter(Boolean),i=[...s].reverse().find(c=>!/^\d+$/.test(c)),a;for(let c of o){let l=he.join(t,c);if(!Fe.existsSync(l))continue;let d=Fe.readFileSync(l,"utf8").split(`
278
+ `),f=i?d.findIndex(m=>m.includes(`"${i}"`)):0;if(f>=0)return{file:l,line:f};a??={file:l,line:0}}return a}function Iu(e){let t=[];return e.forEach((n,r)=>{for(let s of n.matchAll(/\{data:([A-Za-z0-9_.-]+)\}/g)){let o=vc(s[1],Au(n));o.includes("does not resolve")&&t.push({range:{start:{line:r,character:s.index??0},end:{line:r,character:(s.index??0)+s[0].length}},severity:Fn.Error,source:"saffron",message:o.replace(/\*\*/g,"")})}}),t}function Pu(e){let t=e.match(/\{data:([A-Za-z0-9_.-]*)$/);if(!t)return;let n=t[1],{dir:r}=Le(),s=n.split(".");if(s.length===1)return(Fe.existsSync(r)?[...new Set(Fe.readdirSync(r).filter(i=>/\.(json|csv)$/.test(i)).map(i=>i.split(".")[0]))].sort():[]).map(i=>({label:i,kind:ut.File,detail:"data file",insertText:i}));try{let o=ht(s.slice(0,-1).join("."));return o===null||typeof o!="object"?[]:Object.entries(o).map(([i,a])=>({label:i,kind:a!==null&&typeof a=="object"?ut.Module:ut.Value,detail:Array.isArray(a)?`list of ${a.length}`:a!==null&&typeof a=="object"?"object":JSON.stringify(a),insertText:i}))}catch{return[]}}function yc(e,t){let n=/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g,r;for(;r=n.exec(e);)if(t>=r.index&&t<=r.index+r[0].length)return{name:r[1],start:r.index,end:r.index+r[0].length}}function bc(e,t){for(let n of[".env",".env.example"]){let r=he.join(e,n);if(!Fe.existsSync(r))continue;let o=Fe.readFileSync(r,"utf8").split(`
279
+ `).findIndex(i=>new RegExp(`^\\s*(?:export\\s+)?${t}\\s*=`).test(i));if(o!==-1)return{file:r,line:o}}}function Cu(e,t,n,r){let s=e.endsWith(".saffron");if(/^\s*StepSet\s+/.test(t)&&s)return r.stepSets.map(a=>({label:a.name,kind:ut.Function,detail:`step set \xB7 ${a.steps.length} step(s) \xB7 \xD7${a.usage}`,documentation:a.steps.join(`
280
+ `)}));if(/^\s*(Given|When|Then|And|But|\*)\s+/.test(t))return r.steps.map((a,c)=>({label:a.text,kind:ut.Text,detail:`${To[a.status]} \xB7 \xD7${a.usage}`,sortText:String(c).padStart(5,"0"),filterText:a.text}));let i=t.replace(/^\s*/,"").trimEnd().toLowerCase().replace(/\s+/g,"");if(i.length>0&&("stepset:".startsWith(i)||i.startsWith("stepset"))&&s){let a={start:{line:n,character:t.length-t.replace(/^\s*/,"").length},end:{line:n,character:t.length}},c=[{label:"StepSet: \u2026",kind:ut.Keyword,detail:"define a reusable step set",documentation:`StepSet: Name
281
281
  Given \u2026
282
282
  When \u2026
283
283
 
284
- Invoke it anywhere with: StepSet Name`,filterText:"step set stepset:",sortText:"0",insertTextFormat:Za.Snippet,textEdit:{range:a,newText:"StepSet: ${1:Name}\n "}}];r.stepSets.length===0&&c.push({label:"StepSet \u2026",kind:pt.Keyword,detail:"invoke a step set (none defined yet)",filterText:"step set stepset",sortText:"1",insertTextFormat:Za.Snippet,textEdit:{range:a,newText:"StepSet ${1:Name}"}});for(let l of r.stepSets)c.push({label:`StepSet ${l.name}`,kind:pt.Function,detail:`invoke step set \xB7 ${l.steps.length} step(s)`,documentation:l.steps.join(`
285
- `),filterText:`step set ${l.name}`,sortText:`1${l.name}`,textEdit:{range:a,newText:`StepSet ${l.name}`}});return c}return[]}import Zt from"node:fs";import Eu from"node:net";import be from"node:path";import{chromium as Au,firefox as _u,webkit as Iu}from"playwright";var yu=5e3,bu=new Set(["click","press","selectOption","check","uncheck","hover","dragTo"]);function dc(e){return e.mode==="heal"||e.mode==="refine"}function pc(e){return e.mode==="heal"||e.mode==="refine"?e.failedStep?.stepIndex??e.completedSteps?.length??0:e.recordOnly?e.recordOnly.from:e.completedSteps?.length??0}var ac=e=>`${e} action${e===1?"":"s"}`,cc=e=>e.map(ve).join("; "),Tr=class{firstStep;discardBeforeAnnounce;preAnnounce=[];discarded=[];carried;pendingTabOpen=!1;pendingTabFallback;lastOpener;lastOpenerAt=0;inFlight=new Set;steps;currentStepIndex=-1;done=!1;success=!1;narrative="";adaptations=[];suggestedFeatureEdit;featureEdits=[];announced=new Set;pendingFingerprint;constructor(t,n={}){this.firstStep=Math.max(n.firstStep??0,0),this.discardBeforeAnnounce=n.discardBeforeAnnounce??!1,this.steps=t.steps.map(r=>({gherkin:r.text,keyword:r.keyword,kind:r.kind,table:r.table,docString:r.docString,fromStepSet:r.source?.fromStepSet,actions:[]}))}addArmedAction(t){if(this.done)return;let n=this.destination();if(!n){this.discard([t],this.refusalReason());return}if(n.some(o=>o.action===t.action&&o.value===t.value&&o.promptText===t.promptText))return;let s=Math.max(n.length-1,0);n.splice(s,0,t)}destination(){if(this.currentStepIndex<0)return this.discardBeforeAnnounce?void 0:this.firstStep===0?this.steps[0]?.actions:this.preAnnounce;if(!(this.currentStepIndex<this.firstStep))return this.steps[this.currentStepIndex]?.actions}refusalReason(){let t=this.currentStepIndex;return t<0?`before announcing the failed step (step ${this.firstStep+1}); a heal records nothing until it announces that step, so this exploration was not filed under any step`:`under step ${t+1} ("${this.steps[t]?.gherkin??""}"), which had already run before the agent took over, so its recording was kept`}discard(t,n){if(t.length===0)return;let r=this.discarded.find(s=>s.why===n);r?r.actions.push(...t):this.discarded.push({actions:[...t],why:n})}announce(t,n,r){if(this.currentStepIndex=t,this.announced.add(t),r){let o=`Step ${t+1} (`;this.adaptations=this.adaptations.filter(i=>!i.startsWith(o)),this.adaptations.push(`${o}"${n}"): ${r}`)}let s=this.steps[t];this.preAnnounce.length>0&&t>=this.firstStep&&s&&(s.actions.unshift(...this.preAnnounce),this.carried={index:t,actions:this.preAnnounce},this.preAnnounce=[])}attributionNotes(){let t=[];if(this.carried){let{index:r,actions:s}=this.carried;t.push(`recording: ${ac(s.length)} the agent performed before announcing a step ${s.length===1?"was":"were"} filed under step ${r+1} ("${this.steps[r]?.gherkin??""}"), the first step it recorded: ${cc(s)}.`)}let n=this.preAnnounce.length>0?[{actions:this.preAnnounce,why:"before announcing a step this session records"}]:[];for(let{actions:r,why:s}of[...this.discarded,...n])t.push(`recording: discarded ${ac(r.length)} the agent performed ${s}: ${cc(r)}.`);return t}beginToolCall(t){this.inFlight.add(t)}endToolCall(t,n){this.inFlight.delete(t)&&(this.pendingTabOpen&&n&&(this.pendingTabFallback=void 0),this.inFlight.size===0&&this.pendingTabOpen&&(this.pendingTabFallback&&(this.pendingTabFallback.opensTab=!0),this.pendingTabOpen=!1,this.pendingTabFallback=void 0))}markTabOpened(){let t=this.inFlight.size===0,n=Date.now()-this.lastOpenerAt<=yu;if(t){this.lastOpener&&n&&(this.lastOpener.opensTab=!0);return}this.pendingTabOpen=!0,this.pendingTabFallback=n?this.lastOpener:void 0}addActions(t){if(this.done||t.length===0)return;if(this.currentStepIndex<0&&this.firstStep>0&&!this.discardBeforeAnnounce){let s=t.filter(o=>o.action==="switchTab");if(this.discard(s,"before announcing its first step (replay is already on the live page when the agent takes over)"),t=t.filter(o=>o.action!=="switchTab"),t.length===0)return}let n=this.destination();if(!n){this.pendingFingerprint=void 0,this.discard(t,this.refusalReason());return}this.pendingFingerprint&&t[0]&&(t[0].pageFingerprint=this.pendingFingerprint,this.pendingFingerprint=void 0),n.push(...t);let r=[...t].reverse().find(s=>bu.has(s.action));r&&(this.lastOpener=r,this.lastOpenerAt=Date.now(),this.pendingTabOpen&&(r.opensTab=!0,this.pendingTabOpen=!1,this.pendingTabFallback=void 0))}},We=e=>e.replace(/\\(['"\\])/g,"$1");function wu(e){let t=e.match(/'((?:[^'\\]|\\.)*)'/);return t?We(t[1]):void 0}function Su(e){let t=[],n=0;for(;n<e.length;){let r=/^\.?([A-Za-z]+)\(/.exec(e.slice(n));if(!r)return;let s=n+r[0].length,o=s,i=1,a;for(;s<e.length&&i>0;s++){let c=e[s];if(a)c==="\\"?s++:c===a&&(a=void 0);else if(c==="'"||c==='"'||c==="`")a=c;else if(c==="/"&&/[(,:]\s*$/.test(e.slice(o-1,s)))for(s++;s<e.length&&e[s]!=="/";s++)e[s]==="\\"&&s++;else c==="("||c==="{"||c==="["?i++:(c===")"||c==="}"||c==="]")&&i--}if(i!==0)return;t.push({method:r[1],args:e.slice(o,s-1)}),n=s}return t}function ho(e,t){let n=/^'((?:[^'\\]|\\.)*)'$/.exec(e.trim());if(n)return`${JSON.stringify(We(n[1]))}${t?"s":"i"}`;let r=/^\/((?:[^/\\]|\\.)*)\/([a-z]*)$/.exec(e.trim());if(!r)return;let s=ns(r[1]);return s!==r[1]&&/[uv]/.test(r[2])?void 0:`/${s}/${r[2]}`}var xu=new Set(["checked","disabled","selected","expanded","includeHidden","level","pressed","busy"]);function lc(e,t){let n=/exact:\s*true/.test(t),r=/^\s*('(?:[^'\\]|\\.)*'|\/(?:[^/\\]|\\.)*\/[a-z]*)/.exec(t)?.[1];switch(e){case"locator":{let s=/^\s*'((?:[^'\\]|\\.)*)'\s*$/.exec(t);return s?We(s[1]):void 0}case"getByRole":{let s=/^\s*'([^']+)'/.exec(t)?.[1];if(!s)return;let o="";for(let[,i,a]of t.matchAll(/(\w+):\s*('(?:[^'\\]|\\.)*'|\/(?:[^/\\]|\\.)*\/[a-z]*|[^,}\s]+)/g))if(i==="name"||i==="description"){let c=ho(a,n);if(c===void 0)return;o+=`[${i}=${c}]`}else if(xu.has(i))o+=`[${i==="includeHidden"?"include-hidden":i}=${a.replace(/^'(.*)'$/,"$1")}]`;else if(i!=="exact")return;return`internal:role=${s}${o}`}case"getByText":case"getByLabel":case"getByPlaceholder":case"getByAltText":case"getByTitle":{let s=r===void 0?void 0:ho(r,n);return s===void 0?void 0:e==="getByText"?`internal:text=${s}`:e==="getByLabel"?`internal:label=${s}`:`internal:attr=[${{getByPlaceholder:"placeholder",getByAltText:"alt",getByTitle:"title"}[e]}=${s}]`}case"getByTestId":{let s=r&&/^'/.test(r)?We(r.slice(1,-1)):void 0;return s===void 0?void 0:`internal:testid=[data-testid=${JSON.stringify(s)}s]`}case"first":return"nth=0";case"last":return"nth=-1";case"nth":return/^\s*-?\d+\s*$/.test(t)?`nth=${t.trim()}`:void 0;case"filter":{let s=/hasText:\s*('(?:[^'\\]|\\.)*'|\/(?:[^/\\]|\\.)*\/[a-z]*)/.exec(t);if(!s||!/^\s*\{\s*\}\s*$/.test(t.replace(s[0],"")))return;let o=ho(s[1],!1);return o===void 0?void 0:`internal:has-text=${o}`}default:return}}function vu({method:e,args:t}){let n=String.raw`'(?:[^'\\]|\\.)*'`;return e==="getByRole"?new RegExp(String.raw`^\s*'[^']+'\s*(?:,\s*\{\s*(?:name:\s*${n}\s*(?:,\s*exact:\s*true\s*)?)?\}\s*)?$`).test(t):["getByText","getByLabel","getByPlaceholder","getByTestId","locator"].includes(e)&&new RegExp(String.raw`^\s*${n}\s*$`).test(t)}function mo(e){let t={},n=e.match(/(?:frameLocator\('((?:[^'\\]|\\.)*)'\)|locator\('((?:[^'\\]|\\.)*)'\)\.contentFrame\(\))/);n&&(t.frame=We(n[1]??n[2]),e=e.slice((n.index??0)+n[0].length));let r=Su(e.replace(/^\./,"")),s=r?.filter((c,l)=>!(c.method==="first"&&l===r.length-1));if(s&&s.length>1){let c=s.map(l=>lc(l.method,l.args));return c.some(l=>l===void 0)?void 0:{...t,fallbackSelectors:[c.join(" >> ")]}}if(s?.length===1&&!vu(s[0])){let c=lc(s[0].method,s[0].args);return c===void 0?void 0:{...t,fallbackSelectors:[c]}}let o=e.match(/getByRole\('([^']+)'(?:,\s*\{([^}]*)\})?\)/);if(o){t.role=o[1];let c=(o[2]??"").match(/name:\s*'((?:[^'\\]|\\.)*)'/);c&&(t.name=We(c[1]))}let i=[[/getByLabel\('((?:[^'\\]|\\.)*)'\)/,"label"],[/getByText\('((?:[^'\\]|\\.)*)'\)/,"text"],[/getByTestId\('((?:[^'\\]|\\.)*)'\)/,"testId"],[/getByPlaceholder\('((?:[^'\\]|\\.)*)'\)/,"placeholder"]];for(let[c,l]of i){let d=e.match(c);d&&(t[l]=We(d[1]))}let a=e.match(/(?:^|\.)locator\('((?:[^'\\]|\\.)*)'\)/);return a&&(t.fallbackSelectors=[We(a[1])]),Object.keys(t).length>0?t:void 0}var $u={click:"click",dblclick:"click",fill:"fill",pressSequentially:"fill",type:"fill",press:"press",check:"check",uncheck:"uncheck",hover:"hover",selectOption:"selectOption"};function ku(e,t){let n=e.match(/page\.goto\('((?:[^'\\]|\\.)*)'\)/);if(n)return{action:"goto",url:gn(We(n[1]),t)};let r=e.match(/page\.keyboard\.press\('((?:[^'\\]|\\.)*)'\)/);if(r)return{action:"press",value:We(r[1])};let s=e.match(/await\s+page\.(.+)\.dragTo\(page\.(.+?)\);?\s*$/);if(s){let m=mo(s[1]),p=mo(s[2]);if(m&&p)return{action:"dragTo",target:m,toTarget:p}}let o=e.match(/await\s+page\.(.+)\.(click|dblclick|fill|pressSequentially|type|press|check|uncheck|hover|selectOption)\((.*)\);?\s*$/);if(!o)return;let[,i,a,c]=o,l=mo(i);if(!l)return;let d={action:$u[a],target:l},f=wu(c);return f!==void 0&&a!=="click"&&a!=="dblclick"&&(d.value=f),d}function fc(e){let t=Array.isArray(e)?e:e?.content,n=typeof e=="string"?[e]:Array.isArray(t)?t.flatMap(r=>typeof r?.text=="string"?[r.text]:[]):[];for(let r of n){let s=r.split(/^### Open tabs\r?$/m)[1]?.split(/^### /m)[0];if(!s)continue;let o=/^- (\d+): \(current\) /m.exec(s)?.[1];if(o!==void 0)return Number(o)}}function uc(e,t){let n=[],r=Array.isArray(e)?e:e?.content;if(Array.isArray(r))for(let o of r){let i=o?.text;typeof i=="string"&&n.push(i)}else typeof e=="string"&&n.push(e);let s=[];for(let o of n){let i=o.split(/^### Ran Playwright code[ \t\r]*$/m).slice(1).map(a=>a.split(/^### /m)[0]).join(`
284
+ Invoke it anywhere with: StepSet Name`,filterText:"step set stepset:",sortText:"0",insertTextFormat:pc.Snippet,textEdit:{range:a,newText:"StepSet: ${1:Name}\n "}}];r.stepSets.length===0&&c.push({label:"StepSet \u2026",kind:ut.Keyword,detail:"invoke a step set (none defined yet)",filterText:"step set stepset",sortText:"1",insertTextFormat:pc.Snippet,textEdit:{range:a,newText:"StepSet ${1:Name}"}});for(let l of r.stepSets)c.push({label:`StepSet ${l.name}`,kind:ut.Function,detail:`invoke step set \xB7 ${l.steps.length} step(s)`,documentation:l.steps.join(`
285
+ `),filterText:`step set ${l.name}`,sortText:`1${l.name}`,textEdit:{range:a,newText:`StepSet ${l.name}`}});return c}return[]}import tn from"node:fs";import Hu from"node:net";import we from"node:path";import{chromium as qu,firefox as Ju,webkit as Wu}from"playwright";var Ou=5e3,ju=new Set(["click","press","selectOption","check","uncheck","hover","dragTo"]);function kc(e){return e.mode==="heal"||e.mode==="refine"}function Tc(e){return e.mode==="heal"||e.mode==="refine"?e.failedStep?.stepIndex??e.completedSteps?.length??0:e.recordOnly?e.recordOnly.from:e.completedSteps?.length??0}var Sc=e=>`${e} action${e===1?"":"s"}`,xc=e=>e.map($e).join("; "),Cr=class{firstStep;discardBeforeAnnounce;preAnnounce=[];discarded=[];carried;pendingTabOpen=!1;pendingTabFallback;lastOpener;lastOpenerAt=0;inFlight=new Set;steps;currentStepIndex=-1;done=!1;success=!1;narrative="";adaptations=[];suggestedFeatureEdit;featureEdits=[];announced=new Set;pendingFingerprint;constructor(t,n={}){this.firstStep=Math.max(n.firstStep??0,0),this.discardBeforeAnnounce=n.discardBeforeAnnounce??!1,this.steps=t.steps.map(r=>({gherkin:r.text,keyword:r.keyword,kind:r.kind,table:r.table,docString:r.docString,fromStepSet:r.source?.fromStepSet,actions:[]}))}addArmedAction(t){if(this.done)return;let n=this.destination();if(!n){this.discard([t],this.refusalReason());return}if(n.some(o=>o.action===t.action&&o.value===t.value&&o.promptText===t.promptText))return;let s=Math.max(n.length-1,0);n.splice(s,0,t)}destination(){if(this.currentStepIndex<0)return this.discardBeforeAnnounce?void 0:this.firstStep===0?this.steps[0]?.actions:this.preAnnounce;if(!(this.currentStepIndex<this.firstStep))return this.steps[this.currentStepIndex]?.actions}refusalReason(){let t=this.currentStepIndex;return t<0?`before announcing the failed step (step ${this.firstStep+1}); a heal records nothing until it announces that step, so this exploration was not filed under any step`:`under step ${t+1} ("${this.steps[t]?.gherkin??""}"), which had already run before the agent took over, so its recording was kept`}discard(t,n){if(t.length===0)return;let r=this.discarded.find(s=>s.why===n);r?r.actions.push(...t):this.discarded.push({actions:[...t],why:n})}announce(t,n,r){if(this.currentStepIndex=t,this.announced.add(t),r){let o=`Step ${t+1} (`;this.adaptations=this.adaptations.filter(i=>!i.startsWith(o)),this.adaptations.push(`${o}"${n}"): ${r}`)}let s=this.steps[t];this.preAnnounce.length>0&&t>=this.firstStep&&s&&(s.actions.unshift(...this.preAnnounce),this.carried={index:t,actions:this.preAnnounce},this.preAnnounce=[])}attributionNotes(){let t=[];if(this.carried){let{index:r,actions:s}=this.carried;t.push(`recording: ${Sc(s.length)} the agent performed before announcing a step ${s.length===1?"was":"were"} filed under step ${r+1} ("${this.steps[r]?.gherkin??""}"), the first step it recorded: ${xc(s)}.`)}let n=this.preAnnounce.length>0?[{actions:this.preAnnounce,why:"before announcing a step this session records"}]:[];for(let{actions:r,why:s}of[...this.discarded,...n])t.push(`recording: discarded ${Sc(r.length)} the agent performed ${s}: ${xc(r)}.`);return t}beginToolCall(t){this.inFlight.add(t)}endToolCall(t,n){this.inFlight.delete(t)&&(this.pendingTabOpen&&n&&(this.pendingTabFallback=void 0),this.inFlight.size===0&&this.pendingTabOpen&&(this.pendingTabFallback&&(this.pendingTabFallback.opensTab=!0),this.pendingTabOpen=!1,this.pendingTabFallback=void 0))}markTabOpened(){let t=this.inFlight.size===0,n=Date.now()-this.lastOpenerAt<=Ou;if(t){this.lastOpener&&n&&(this.lastOpener.opensTab=!0);return}this.pendingTabOpen=!0,this.pendingTabFallback=n?this.lastOpener:void 0}addActions(t){if(this.done||t.length===0)return;if(this.currentStepIndex<0&&this.firstStep>0&&!this.discardBeforeAnnounce){let s=t.filter(o=>o.action==="switchTab");if(this.discard(s,"before announcing its first step (replay is already on the live page when the agent takes over)"),t=t.filter(o=>o.action!=="switchTab"),t.length===0)return}let n=this.destination();if(!n){this.pendingFingerprint=void 0,this.discard(t,this.refusalReason());return}this.pendingFingerprint&&t[0]&&(t[0].pageFingerprint=this.pendingFingerprint,this.pendingFingerprint=void 0),n.push(...t);let r=[...t].reverse().find(s=>ju.has(s.action));r&&(this.lastOpener=r,this.lastOpenerAt=Date.now(),this.pendingTabOpen&&(r.opensTab=!0,this.pendingTabOpen=!1,this.pendingTabFallback=void 0))}},Ke=e=>e.replace(/\\(['"\\])/g,"$1");function Nu(e){let t=e.match(/'((?:[^'\\]|\\.)*)'/);return t?Ke(t[1]):void 0}function Fu(e){let t=[],n=0;for(;n<e.length;){let r=/^\.?([A-Za-z]+)\(/.exec(e.slice(n));if(!r)return;let s=n+r[0].length,o=s,i=1,a;for(;s<e.length&&i>0;s++){let c=e[s];if(a)c==="\\"?s++:c===a&&(a=void 0);else if(c==="'"||c==='"'||c==="`")a=c;else if(c==="/"&&/[(,:]\s*$/.test(e.slice(o-1,s)))for(s++;s<e.length&&e[s]!=="/";s++)e[s]==="\\"&&s++;else c==="("||c==="{"||c==="["?i++:(c===")"||c==="}"||c==="]")&&i--}if(i!==0)return;t.push({method:r[1],args:e.slice(o,s-1)}),n=s}return t}function Ro(e,t){let n=/^'((?:[^'\\]|\\.)*)'$/.exec(e.trim());if(n)return`${JSON.stringify(Ke(n[1]))}${t?"s":"i"}`;let r=/^\/((?:[^/\\]|\\.)*)\/([a-z]*)$/.exec(e.trim());if(!r)return;let s=ds(r[1]);return s!==r[1]&&/[uv]/.test(r[2])?void 0:`/${s}/${r[2]}`}var Mu=new Set(["checked","disabled","selected","expanded","includeHidden","level","pressed","busy"]);function $c(e,t){let n=/exact:\s*true/.test(t),r=/^\s*('(?:[^'\\]|\\.)*'|\/(?:[^/\\]|\\.)*\/[a-z]*)/.exec(t)?.[1];switch(e){case"locator":{let s=/^\s*'((?:[^'\\]|\\.)*)'\s*$/.exec(t);return s?Ke(s[1]):void 0}case"getByRole":{let s=/^\s*'([^']+)'/.exec(t)?.[1];if(!s)return;let o="";for(let[,i,a]of t.matchAll(/(\w+):\s*('(?:[^'\\]|\\.)*'|\/(?:[^/\\]|\\.)*\/[a-z]*|[^,}\s]+)/g))if(i==="name"||i==="description"){let c=Ro(a,n);if(c===void 0)return;o+=`[${i}=${c}]`}else if(Mu.has(i))o+=`[${i==="includeHidden"?"include-hidden":i}=${a.replace(/^'(.*)'$/,"$1")}]`;else if(i!=="exact")return;return`internal:role=${s}${o}`}case"getByText":case"getByLabel":case"getByPlaceholder":case"getByAltText":case"getByTitle":{let s=r===void 0?void 0:Ro(r,n);return s===void 0?void 0:e==="getByText"?`internal:text=${s}`:e==="getByLabel"?`internal:label=${s}`:`internal:attr=[${{getByPlaceholder:"placeholder",getByAltText:"alt",getByTitle:"title"}[e]}=${s}]`}case"getByTestId":{let s=r&&/^'/.test(r)?Ke(r.slice(1,-1)):void 0;return s===void 0?void 0:`internal:testid=[data-testid=${JSON.stringify(s)}s]`}case"first":return"nth=0";case"last":return"nth=-1";case"nth":return/^\s*-?\d+\s*$/.test(t)?`nth=${t.trim()}`:void 0;case"filter":{let s=/hasText:\s*('(?:[^'\\]|\\.)*'|\/(?:[^/\\]|\\.)*\/[a-z]*)/.exec(t);if(!s||!/^\s*\{\s*\}\s*$/.test(t.replace(s[0],"")))return;let o=Ro(s[1],!1);return o===void 0?void 0:`internal:has-text=${o}`}default:return}}function Du({method:e,args:t}){let n=String.raw`'(?:[^'\\]|\\.)*'`;return e==="getByRole"?new RegExp(String.raw`^\s*'[^']+'\s*(?:,\s*\{\s*(?:name:\s*${n}\s*(?:,\s*exact:\s*true\s*)?)?\}\s*)?$`).test(t):["getByText","getByLabel","getByPlaceholder","getByTestId","locator"].includes(e)&&new RegExp(String.raw`^\s*${n}\s*$`).test(t)}function Eo(e){let t={},n=e.match(/(?:frameLocator\('((?:[^'\\]|\\.)*)'\)|locator\('((?:[^'\\]|\\.)*)'\)\.contentFrame\(\))/);n&&(t.frame=Ke(n[1]??n[2]),e=e.slice((n.index??0)+n[0].length));let r=Fu(e.replace(/^\./,"")),s=r?.filter((c,l)=>!(c.method==="first"&&l===r.length-1));if(s&&s.length>1){let c=s.map(l=>$c(l.method,l.args));return c.some(l=>l===void 0)?void 0:{...t,fallbackSelectors:[c.join(" >> ")]}}if(s?.length===1&&!Du(s[0])){let c=$c(s[0].method,s[0].args);return c===void 0?void 0:{...t,fallbackSelectors:[c]}}let o=e.match(/getByRole\('([^']+)'(?:,\s*\{([^}]*)\})?\)/);if(o){t.role=o[1];let c=(o[2]??"").match(/name:\s*'((?:[^'\\]|\\.)*)'/);c&&(t.name=Ke(c[1]))}let i=[[/getByLabel\('((?:[^'\\]|\\.)*)'\)/,"label"],[/getByText\('((?:[^'\\]|\\.)*)'\)/,"text"],[/getByTestId\('((?:[^'\\]|\\.)*)'\)/,"testId"],[/getByPlaceholder\('((?:[^'\\]|\\.)*)'\)/,"placeholder"]];for(let[c,l]of i){let d=e.match(c);d&&(t[l]=Ke(d[1]))}let a=e.match(/(?:^|\.)locator\('((?:[^'\\]|\\.)*)'\)/);return a&&(t.fallbackSelectors=[Ke(a[1])]),Object.keys(t).length>0?t:void 0}var Lu={click:"click",dblclick:"click",fill:"fill",pressSequentially:"fill",type:"fill",press:"press",check:"check",uncheck:"uncheck",hover:"hover",selectOption:"selectOption"};function Uu(e,t){let n=e.match(/page\.goto\('((?:[^'\\]|\\.)*)'\)/);if(n)return{action:"goto",url:bn(Ke(n[1]),t)};let r=e.match(/page\.keyboard\.press\('((?:[^'\\]|\\.)*)'\)/);if(r)return{action:"press",value:Ke(r[1])};let s=e.match(/await\s+page\.(.+)\.dragTo\(page\.(.+?)\);?\s*$/);if(s){let m=Eo(s[1]),p=Eo(s[2]);if(m&&p)return{action:"dragTo",target:m,toTarget:p}}let o=e.match(/await\s+page\.(.+)\.(click|dblclick|fill|pressSequentially|type|press|check|uncheck|hover|selectOption)\((.*)\);?\s*$/);if(!o)return;let[,i,a,c]=o,l=Eo(i);if(!l)return;let d={action:Lu[a],target:l},f=Nu(c);return f!==void 0&&a!=="click"&&a!=="dblclick"&&(d.value=f),d}function Rc(e){let t=Array.isArray(e)?e:e?.content,n=typeof e=="string"?[e]:Array.isArray(t)?t.flatMap(r=>typeof r?.text=="string"?[r.text]:[]):[];for(let r of n){let s=r.split(/^### Open tabs\r?$/m)[1]?.split(/^### /m)[0];if(!s)continue;let o=/^- (\d+): \(current\) /m.exec(s)?.[1];if(o!==void 0)return Number(o)}}function Ec(e,t){let n=[],r=Array.isArray(e)?e:e?.content;if(Array.isArray(r))for(let o of r){let i=o?.text;typeof i=="string"&&n.push(i)}else typeof e=="string"&&n.push(e);let s=[];for(let o of n){let i=o.split(/^### Ran Playwright code[ \t\r]*$/m).slice(1).map(a=>a.split(/^### /m)[0]).join(`
286
286
  `);for(let a of i.split(`
287
- `)){let c=ku(a.trim(),t);c&&s.push(c)}}return Tu(s)}function Tu(e){return e.filter((t,n)=>{let r=e[n+1];return!(t.action==="fill"&&(t.value??"")===""&&r?.action==="fill"&&JSON.stringify(r.target)===JSON.stringify(t.target))})}function gc(e){let t=[];for(let n of e){if(n.kind==="assertion"&&n.actions.length===0){t.push(`Step "${n.gherkin}" is an assertion but recorded no check, so nothing would be verified. Re-record with: saffron run --rerecord`);continue}if(/"[^"]+"|<[^<>]+>/.test(n.gherkin)){for(let s of n.actions)if(s.action==="fill"&&(s.value??"")===""){t.push(`Step "${n.gherkin}" recorded a fill with an empty value although the step passes one; the typed value was lost. Re-record with: saffron run --rerecord`);break}}}return t}function Rt(e,t,n=s=>s,r=!1){let s=e;for(let[o,i]of Object.entries(t).filter(([,a])=>a.length>=3).sort((a,c)=>c[1].length-a[1].length)){let a=n(i);if(!r){s=s.split(a).join(`<${o}>`);continue}if(i.length<5&&!/\p{N}/u.test(i)){s===a&&(s=`<${o}>`);continue}let c=a.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");s=s.replace(new RegExp(`(?<![\\p{L}\\p{N}])${c}(?![\\p{L}\\p{N}])`,"gu"),()=>`<${o}>`)}return s}function hc(e,t){let n=Object.entries(t).filter(([,l])=>l.length>=3);if(n.length===0)return e;let r=l=>l===void 0?void 0:Rt(l,t),s=[...n].sort((l,d)=>d[1].length-l[1].length),o=l=>{if(l!==void 0){for(let[d,f]of s)if(l.includes(f))return`<${d}>`;return l}},i=l=>l===void 0?void 0:Rt(l,t,oe.pattern),a=l=>l===void 0?void 0:Rt(l,t,void 0,!0),c=(l,d)=>l?{...l,name:d?o(l.name):r(l.name),nameRegex:i(l.nameRegex),label:r(l.label),text:d?o(l.text):r(l.text),placeholder:r(l.placeholder),testId:a(l.testId),frame:a(l.frame),fallbackSelectors:l.fallbackSelectors?.map(f=>Ft(f,(m,p)=>Rt(m,t,oe[p])))}:void 0;return e.map(l=>({...l,actions:l.actions.map(d=>{let f=Se.has(d.action)&&d.action!=="expectValue";return{...d,value:f?o(d.value):r(d.value),url:r(d.url),pattern:i(d.pattern),urlPattern:i(d.urlPattern),promptText:r(d.promptText),target:c(d.target,f),toTarget:c(d.toTarget,!1)}})}))}function mc(e,t,n=r=>r){let r=it(t.steps);return e.map((s,o)=>{let i=t.steps[o],a=i?.resolvedTable??i?.table,c=i?.resolvedDocString??i?.docString,l=Object.entries({...r,...fe(a),...bt(c)}).map(([m,p])=>[m,n(p)]).filter(([,m])=>m.length>=3).sort((m,p)=>p[1].length-m[1].length);if(l.length===0)return s;let d=(m,p=v=>v,b=!1)=>m===void 0?void 0:Rt(m,Object.fromEntries(l),p,b),f=m=>m?{...m,name:d(m.name),nameRegex:d(m.nameRegex,oe.pattern),label:d(m.label),text:d(m.text),placeholder:d(m.placeholder),testId:d(m.testId,void 0,!0),frame:d(m.frame,void 0,!0),fallbackSelectors:m.fallbackSelectors?.map(p=>Ft(p,(b,v)=>d(b,oe[v])??b))}:void 0;return{...s,actions:s.actions.map(m=>({...m,value:d(m.value),url:d(m.url),pattern:d(m.pattern,oe.pattern),urlPattern:d(m.urlPattern,oe.pattern),promptText:d(m.promptText),target:f(m.target),toTarget:f(m.toTarget)}))}})}function yc(e,t=new Date){return e.map(n=>({...n,actions:n.actions.map(r=>yo(r,s=>Ri(s,t)))}))}function yo(e,t,n){let r=(a,c,l=t)=>{let d={...a};for(let f of c)typeof a[f]=="string"&&(d[f]=l(a[f]));return d},s=a=>n?n(a,"pattern"):t(a),o=a=>{let c=r(r(a,["name","label","text","placeholder","testId","frame"]),["nameRegex"],s);return a.fallbackSelectors&&(c.fallbackSelectors=a.fallbackSelectors.map(l=>n?Ft(l,n,t):t(l))),c},i=r(r(e,["value","url","promptText"]),["pattern","urlPattern"],s);return e.target&&(i.target=o(e.target)),e.toTarget&&(i.toTarget=o(e.toTarget)),i}function Ru(e,t){if(t.length===0)return e;let n=new Map(t.map(([o,i])=>[i,o])),r=[...n.keys()].sort((o,i)=>i.length-o.length).map(Ke).join("|"),s=new RegExp(String.raw`(?<=^|[^A-Za-z0-9\\]|\\[A-Za-z])(?:${r})(?![A-Za-z0-9])`,"g");return e.split(/(\{(?:env|data|unique|date)[^{}]*\})/).map((o,i)=>i%2?o:o.replace(s,a=>`{env:${n.get(a)}}`)).join("")}function bc(e,t,n=[]){if(t.size===0)return e;let r=ts(n,t),s=mt(t,{standaloneBelow:6}),o=l=>s.mask(r(l)),i=[...t].filter(([,l])=>l.length>=ht),a=l=>{let d=ts(n,t,oe[l]),f=mt(new Map(i.filter(([,p])=>p.length>=6).map(([p,b])=>[p,oe[l](b)]))),m=i.filter(([,p])=>p.length<6).map(([p,b])=>[p,oe[l](b)]);return p=>o(Ru(f.mask(d(p)),m))},c={quoted:a("quoted"),regex:a("regex"),pattern:a("pattern")};return e.map(l=>({...l,actions:l.actions.map(d=>yo(d,o,(f,m)=>c[m](f)))}))}function wc(e,t){if(t.size===0)return e;let n=r=>typeof r=="string"?_i(r,t):Array.isArray(r)?r.map(n):r&&typeof r=="object"?Object.fromEntries(Object.entries(r).map(([s,o])=>[s,s==="pageFingerprint"?o:n(o)])):r;return e.map(r=>({...r,actions:r.actions.map(s=>n(s))}))}function Sc(e,t){return t.size===0?e:e.map(n=>{let r=new Set([...n.gherkin.matchAll(ln)].map(l=>l[1])),s=l=>Zr(l,t,r),o=l=>new Map([...t].filter(([,d])=>d.length>=4).map(([d,f])=>[d,oe[l](f)])),i={quoted:o("quoted"),regex:o("regex"),pattern:o("pattern")},a=l=>l.split(/(\{(?:env|data|unique|date)[^{}]*\})/).map((d,f)=>f%2?d:s(d)).join(""),c=(l,d)=>a(Zr(l,i[d],r));return{...n,actions:n.actions.map(l=>yo(l,s,c))}})}function xc(e,t,n){if(e!=="mcp__playwright__browser_tabs")return;let r=t??{},s=typeof r.index=="number"?String(r.index):void 0;switch(r.action){case"select":return s===void 0?void 0:{action:"switchTab",value:s};case"close":return s===void 0?{action:"closeTab"}:{action:"closeTab",value:s};case"new":return typeof r.url=="string"&&r.url?{action:"newTab",url:gn(r.url,n)}:{action:"newTab"};default:return}}function vc(e,t){let n=t??{};if(e==="mcp__playwright__browser_handle_dialog"){let r={action:"handleDialog",value:n.accept?"accept":"dismiss"};return typeof n.promptText=="string"&&n.promptText&&(r.promptText=n.promptText),r}if(e==="mcp__playwright__browser_file_upload"){let r=Array.isArray(n.paths)?n.paths:[];if(r.length===0)return;let s=process.cwd()+"/";return{action:"uploadFile",value:r.map(i=>typeof i=="string"&&i.startsWith(s)?i.slice(s.length):i).join(`
288
- `)}}}function Pn(e,t){let n=e.pages().filter(s=>!s.isClosed()),r=s=>[...n].reverse().find(s);return(t?r(s=>s.url()===t):void 0)??r(s=>s.url()!=="about:blank")??n.at(-1)}async function Rr(e,t){let n=t.context();for(let s of e.contexts())if(s!==n)for(let o of s.pages())await o.close().catch(()=>{});await t.bringToFront().catch(()=>{});let r=n.pages().filter(s=>!s.isClosed());return{index:Math.max(r.indexOf(t),0),count:r.length,url:t.url()}}var wo=["chromium","chrome","msedge","firefox","webkit"],bo={chrome:{label:"Google Chrome"},msedge:{label:"Microsoft Edge"}};function Ac(e){return wo.includes(e)}var Cn=class extends Error{},Et={aiCalls:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreationTokens:0,costUsd:0};async function Pu(){return new Promise((e,t)=>{let n=Eu.createServer();n.listen(0,"127.0.0.1",()=>{let{port:r}=n.address();n.close(()=>e(r))}),n.on("error",t)})}function $c(e){let t=Jn(le(e));return t.size>0?{data:Object.fromEntries([...t].sort(([n],[r])=>n.localeCompare(r)))}:{}}function Qt(e){let t=e.contexts().flatMap(n=>n.pages());return[...t].reverse().find(n=>n.url()!=="about:blank")??t.at(-1)}function _c(e){let t=e.length;for(let n=e.length-1;n>=0&&e[n].kind==="assertion";n--)t=n;return t}function kc(e,t){return e[t]?.kind==="assertion"&&t>=_c(e)}function Tc(e,t,n=!1,r){let s=_c(e);return e.map((o,i)=>{if(r&&i<r.stepIndex)return o;let a=t[i],c=a&&a.actions.length>0;return o.kind==="assertion"?n&&i<s&&c?a:o:c?r&&i===r.stepIndex&&r.actionIndex>0?{...a,actions:[...o.actions.slice(0,r.actionIndex),...a.actions]}:a:o})}function Cu(e,t,n,r){let s=ia(e,t);return s.length===0||!n?.pageUrl?s:[`page at failure: replay stopped at step ${n.stepIndex+1} ("${n.stepText}") on ${gn(n.pageUrl,r)}, where the heal started.`,...s]}function Ou(e,t,n=0){return e.map((r,s)=>{if(s<n)return r;let o=t[s];return o&&o.actions.length>0?o:r})}function Rc(e,t){let n=[];for(let r=0;r<Math.min(t,e.length);r++){let s=e[r].actions.length;s!==0&&n.push(`recording: kept the existing recording of step ${r+1} ("${e[r].gherkin}"), which ran before the agent took over; ignored ${s} action${s===1?"":"s"} the agent's session held for it.`)}return n}function Ic(e,t,n,r){let s=t.filter(c=>c>=0&&c<e.length),o=s.length>0?Math.max(...s):0,i=Math.min(n&&n.stepIndex>=o?n.stepIndex:o,e.length-1),a=n&&i===n.stepIndex?n.error:r;return e.map((c,l)=>({gherkin:c.gherkin,kind:c.kind,status:l<i?"passed":l===i?"failed":"skipped",...l===i&&a?{error:a}:{}}))}function ju(e,t,n){let r=n.success?e.map(s=>({gherkin:s.gherkin,kind:s.kind,status:"passed"})):Ic(e,n.announcedSteps??[],t,n.narrative);return e.map((s,o)=>o<t.stepIndex?s:r[o])}function Ec(e,t){return{aiCalls:e.aiCalls+t.aiCalls,inputTokens:e.inputTokens+t.inputTokens,outputTokens:e.outputTokens+t.outputTokens,cacheReadTokens:e.cacheReadTokens+t.cacheReadTokens,cacheCreationTokens:e.cacheCreationTokens+t.cacheCreationTokens,costUsd:(e.costUsd??0)+(t.costUsd??0),models:e.models||t.models?[...new Set([...e.models??[],...t.models??[]])]:void 0}}var Er=class{constructor(t){this.options=t}options;browser;cdpEndpoint;stepIndex;get agent(){return this.options.agent}getStepIndex(){if(!this.stepIndex&&(this.stepIndex=Be(this.options.projectRoot),this.stepIndex.steps.size>0)){let t=this.stepIndex.divergent.length>0?` \xB7 ${this.stepIndex.divergent.length} step(s) have divergent recordings (Level-2 signal)`:"";console.log(`step index: ${this.stepIndex.steps.size} reusable steps from ${this.stepIndex.scannedCaches} cache(s)${t}`)}return this.stepIndex}async recordWithSeeding(t,n,r){let s=this.getStepIndex(),o=t.steps.length,i={...t.params,...it(t.steps)},a=t.steps.map(h=>{let S=s.steps.size>0?ui(s,h,i):void 0;return S?{gherkin:h.text,keyword:h.keyword,kind:h.kind,table:h.table,docString:h.docString,fromStepSet:h.source?.fromStepSet,actions:S.actions}:void 0}),c=t.steps.map(h=>({gherkin:h.text,keyword:h.keyword,kind:h.kind,table:h.table,docString:h.docString,fromStepSet:h.source?.fromStepSet,actions:[]})),l=new Map;l.set(Ae,r);let d=[],f=[],m=[],p=[],b={...Et},v=n,k=0,w=0,x=0,y=(h,S)=>({success:h,narrative:f.join(" ")||"Composed entirely from seeded recordings.",adaptations:d,suggestedFeatureEdit:p.length>0?p.join(`
289
- `):void 0,featureEdits:m,recordedSteps:c,announcedSteps:S===void 0?[]:Array.from({length:S+1},($,u)=>u),usage:b,seededCount:k});for(;x<o;){if(a[x]){let E=x;for(;E<o&&a[E];)E++;let I=a.slice(x,E),A={...t,steps:t.steps.slice(x,E)},P=await xn(v.isClosed()?Pn(n.context())??n:v,A,this.buildCandidateCache(t,I),{baseURL:this.options.baseURL,actionTimeoutMs:this.options.actionTimeoutMs,retries:this.options.retries,pollIntervalMs:this.options.pollIntervalMs,vars:l,trace:this.options.trace!==void 0&&this.options.trace!=="off",traceStepOffset:x});v=P.activePage??v;let _=P.status==="passed"?E-x:P.failure?.stepIndex??0;for(let F=0;F<_;F++)c[x+F]=I[F];if(k+=_,P.status==="passed"){x=E;continue}let j=x+_;d.push(`Seeded recording for "${t.steps[j].text}" did not replay in this scenario's context; the agent re-recorded it fresh. Divergence signal.`),a[j]=void 0,x=j;continue}if(!this.options.agent)return y(!1,x);w++;let h=x;for(;h<o&&!a[h];)h++;w>=3&&(h=o);let S=await Rr(this.browser,v.isClosed()?Pn(n.context())??n:v),$=E=>{v=E};n.context().on("page",$);let u;try{u=await this.withDialogsVisible(()=>this.options.agent.run({scenario:t,baseURL:this.options.baseURL,traceContext:this.options.trace&&this.options.trace!=="off"?n.context():void 0,mode:"record",cdpEndpoint:this.cdpEndpoint,projectRoot:this.options.projectRoot,assertionPolicy:this.options.assertionPolicy,completedSteps:x>0?t.steps.slice(0,x).map(E=>E.resolvedText):void 0,recordOnly:h<o?{from:x,to:h-1}:void 0,liveTab:S,onActiveTab:E=>{let I=n.context().pages().filter(A=>!A.isClosed())[E];I&&(v=I)},uniqueSeed:l.get(Ae),onTabOpened:this.tabWatcher(),takeFingerprint:async()=>vt(Qt(this.browser)??n)}))}finally{n.context().off("page",$)}if(b=Ec(b,u.usage),f.push(u.narrative),d.push(...u.adaptations),m.push(...u.featureEdits),u.suggestedFeatureEdit&&p.push(u.suggestedFeatureEdit),!u.success){let E=u.announcedSteps.filter(I=>I>=x&&I<o);return y(!1,E.length>0?Math.max(...E):x)}let g=u.announcedSteps.filter(E=>E>=x&&E<o),R=Math.min(o-1,Math.max(g.length>0?Math.max(...g):h-1,x));for(let E=x;E<=R;E++)c[E]=u.recordedSteps[E];x=R+1}return y(!0)}async withDialogsVisible(t){let n=()=>{},r=[],s=[],o=i=>{i.on("dialog",n),r.push(i)};for(let i of this.browser?.contexts()??[]){i.on("page",o),s.push(i);for(let a of i.pages())o(a)}try{return await t()}finally{for(let i of s)i.off("page",o);for(let i of r)try{i.off("dialog",n)}catch{}}}async start(){let t=this.options.browser??"chromium";if(t==="chromium"||bo[t]){let n=await Pu();try{this.browser=await Au.launch({headless:!this.options.headed,channel:bo[t]?t:void 0,args:[`--remote-debugging-port=${n}`]})}catch(r){let s=r instanceof Error?r.message:String(r),o=bo[t];throw o&&/is not found at|distribution .* is not found/i.test(s)?new Cn(`${o.label} is not installed on this machine, so --browser ${t} cannot run.
290
- Install it (npx playwright install ${t} does that), or use --browser chromium, which Saffron ships with.`):r}this.cdpEndpoint=`http://127.0.0.1:${n}`}else this.browser=await(t==="firefox"?_u:Iu).launch({headless:!this.options.headed}),this.cdpEndpoint=void 0}async stop(){await this.browser?.close(),this.browser=void 0}resolveStorageState(){if(!this.options.storageState)return;let t=be.isAbsolute(this.options.storageState)?this.options.storageState:be.join(this.options.projectRoot,this.options.storageState);if(!Zt.existsSync(t))throw new Error(`storageState file not found: ${t}. Generate one with: npx playwright open --save-storage=${this.options.storageState} <url>`);return t}buildCandidateCache(t,n){return{version:1,feature:t.featurePath,scenario:t.name,recordedAt:new Date().toISOString(),recordedBy:"agent",steps:n,...$c(t)}}tabWatcher(){return t=>{let n=this.browser?.contexts()??[],r=()=>t();for(let s of n)s.on("page",r);return()=>{for(let s of n)s.off("page",r)}}}async proofReplay(t,n,r){let s=await this.browser.newContext({storageState:this.resolveStorageState()}),o=this.options.trace!==void 0&&this.options.trace!=="off";o&&await Es(s,`${t.displayName} (proof replay)`);let i=await s.newPage();return{result:await xn(i,t,n,{baseURL:this.options.baseURL,actionTimeoutMs:this.options.actionTimeoutMs,retries:this.options.retries,pollIntervalMs:this.options.pollIntervalMs,uniqueSeed:r,trace:o}),context:s,page:i}}async runScenario(t,n={}){let r=await this.runScenarioInner(t,n),s=Ye(t);if(s.size>0){let o=i=>i===void 0?i:Ze(i,s);r.error=o(r.error),r.narrative=o(r.narrative),r.proofError=o(r.proofError),r.suggestedFeatureEdit=o(r.suggestedFeatureEdit),r.adaptations=r.adaptations.map(i=>Ze(i,s));for(let i of r.stepResults)i.error=o(i.error)}return r}async screenshot(t,n,r,s){let o=this.options.screenshot??"viewport";if(o!=="off")try{let i=Le(this.options.projectRoot,n.featurePath,n.displayName);this.claimArtifactDir(n);let a=be.join(i,`${r}.jpg`),c=await t.screenshot({path:a,type:"jpeg",quality:80,fullPage:o==="full-page",timeout:5e3});return{kind:r,file:be.relative(this.options.projectRoot,a).split(be.sep).join("/"),step:s,sha256:Oe(c)}}catch{return}}claimArtifactDir(t){let n=Le(this.options.projectRoot,t.featurePath,t.displayName);Zt.mkdirSync(n,{recursive:!0}),Zt.writeFileSync(be.join(n,Dt),JSON.stringify({feature:t.featurePath,scenario:t.displayName},null,2))}async runScenarioInner(t,n={}){if(!this.browser)throw new Error("Orchestrator not started");let r=Date.now(),{projectRoot:s,baseURL:o,actionTimeoutMs:i,retries:a,pollIntervalMs:c}=this.options,l=Y(s,t.featurePath,t.name),d;try{d=me(l)}catch($){if(!($ instanceof X))throw $;return{scenario:t,status:"red",source:"cache",stepResults:[],adaptations:[],usage:Et,durationMs:Date.now()-r,error:`The recording cannot be read: ${$.problem}. Fix the file, or re-record it with saffron run --rerecord.`}}let f=xe(s,t.featurePath,t.name),m,p;if(!d&&Zt.existsSync(f))try{({proposal:m,revision:p}=sr(f))}catch($){if(!($ instanceof X)&&$.code!=="ENOENT")throw $;m=void 0}m&&$e(s,m,t)&&(m=void 0);let b=await this.browser.newContext({storageState:this.resolveStorageState()}),v=this.options.trace??"off",k=v!=="off";k&&await Es(b,t.displayName);let w=!k,x=await b.newPage(),y=xt(),h=[];Zt.rmSync(Le(s,t.featurePath,t.displayName),{recursive:!0,force:!0});let S=($,u)=>this.screenshot($,t,"before-heal",u.stepText).then(g=>{g&&h.push(g)});try{let $=await(async()=>{let g;if(m){let _=await xn(x,t,m.cache,{baseURL:o,actionTimeoutMs:i,retries:a,pollIntervalMs:c,uniqueSeed:y,trace:k,tracePhase:"pending"}),j=m.meta.recordedFor??t.name;if(_.status==="passed")return{scenario:t,status:"yellow",source:"agent-record",proposalReplay:!0,stepResults:_.stepResults,narrative:`Replayed the pending proposal recorded for "${j}" at zero AI; accept it to go green.`,adaptations:[],verified:!0,proposalFile:f,proposalRevision:p,usage:Et,durationMs:Date.now()-r};if(Object.keys(t.params).length>0&&j!==t.displayName){let C=_.failure;return{scenario:t,status:"red",source:"agent-record",proposalReplay:!0,stepResults:_.stepResults,adaptations:[],proposalFile:f,proposalRevision:p,usage:Et,durationMs:Date.now()-r,error:`The pending proposal recorded for "${j}" does not replay for this Examples row`+(C?` (step "${C.stepText}": ${C.error.split(`
291
- `)[0]})`:"")+". A row value is probably baked in as a literal instead of a <placeholder>; inspect the proposal, reject it, and re-record with saffron run --rerecord."}}console.log(` \u21BA pending proposal for "${t.displayName}" did not replay`+(_.failure?` (step "${_.failure.stepText}": ${_.failure.error.split(`
292
- `)[0]})`:"")+"; recording again"),await x.goto("about:blank")}if(d&&(g=await xn(x,t,d,{baseURL:o,actionTimeoutMs:i,retries:a,pollIntervalMs:c,uniqueSeed:y,trace:k,onFailure:S,keepResponseLog:!0}),g.status==="passed"))return{scenario:t,status:"green",source:"cache",stepResults:g.stepResults,adaptations:[],usage:Et,durationMs:Date.now()-r};if(!(this.options.agent!==void 0&&this.cdpEndpoint!==void 0&&!n.agentDisabled)){let _=this.options.agent!==void 0&&this.cdpEndpoint===void 0&&!n.agentDisabled?`recording/healing needs a Chromium-family browser (the agent attaches over CDP). Record with --browser chromium (the default), chrome or msedge, then replay with --browser ${this.options.browser}.`:"no agent is configured.";return g?.responses?.detach(),{scenario:t,status:"red",source:"cache",stepResults:g?.stepResults??[],adaptations:[],usage:Et,durationMs:Date.now()-r,error:d?g?.status==="stale"?`Cache is stale (feature file changed) and ${_}`:`Replay failed and ${_} ${g?.failure?.error??""}`.trimEnd():`No cache exists and ${_}`}}let E=d!==void 0&&g!==void 0&&g.status==="failed",I=0,A,P;try{if(E){P=g.responses;let _=await Rr(this.browser,g.activePage??Pn(x.context(),g.failure.pageUrl)??x);A=await this.withDialogsVisible(()=>this.options.agent.run({scenario:t,baseURL:o,traceContext:k?x.context():void 0,mode:"heal",liveTab:_,model:this.options.healModel,cdpEndpoint:this.cdpEndpoint,projectRoot:this.options.projectRoot,assertionPolicy:this.options.assertionPolicy,completedSteps:g.stepResults.filter(j=>j.status==="passed").map(j=>j.gherkin),failedStep:{text:g.failure.stepText,kind:g.failure.kind,error:g.failure.error,pageUrl:g.failure.pageUrl,stepIndex:g.failure.stepIndex},uniqueSeed:y,onTabOpened:this.tabWatcher(),takeFingerprint:async()=>{let j=Qt(this.browser)??x;return vt(j)}}))}else if(await x.goto("about:blank"),this.options.reuseSteps!==!1){let _=await this.recordWithSeeding(t,x,y);I=_.seededCount,A=_}else A=await this.withDialogsVisible(()=>this.options.agent.run({scenario:t,baseURL:o,traceContext:k?x.context():void 0,mode:"record",cdpEndpoint:this.cdpEndpoint,projectRoot:this.options.projectRoot,assertionPolicy:this.options.assertionPolicy,uniqueSeed:y,onTabOpened:this.tabWatcher(),takeFingerprint:async()=>{let _=Qt(this.browser)??x;return vt(_)}}))}catch(_){P?.detach();let j=_ instanceof Error?_.message:String(_);return{scenario:t,status:"red",source:E?"agent-heal":"agent-record",stepResults:g?.stepResults??[],adaptations:[],usage:Et,durationMs:Date.now()-r,error:`Agent invocation failed: ${j.split(`
293
- `)[0]}`}}if(A.success&&E&&g.failure.kind==="assertion"&&(this.options.assertionPolicy!=="adaptable-mid"||kc(d.steps,g.failure.stepIndex))){let _=d.steps[g.failure.stepIndex],j=Qt(this.browser)??x,F=new Map(Object.entries(g.captured)),C=Ls(t,g.failure.stepIndex);try{for(let L of _.actions)await gr(j,L,C,this.options.actionTimeoutMs??5e3,o,F,P)}catch(L){let T=L instanceof Error?L.message:String(L),O=`Assertion failed as written: "${_.gherkin}". Saffron never adapts assertions. If this change is intended, update the .feature file or re-record the scenario (saffron run --rerecord --filter @tag). If the recording itself looks wrong (an empty value, a wrong target), re-record it the same way. Verification error: ${T.split(`
294
- `)[0]}`,M=g.failure.stepIndex,B=g.stepResults.map((W,ce)=>ce===M?{...W,status:"failed",error:O}:W);return{scenario:t,status:"red",source:"agent-heal",stepResults:B,executionStepResults:B.map((W,ce)=>ce>M&&W.status==="skipped"?{...W,status:"passed"}:W),narrative:A.narrative,adaptations:A.adaptations,suggestedFeatureEdit:A.suggestedFeatureEdit,usage:A.usage,durationMs:Date.now()-r,error:O}}finally{P?.detach()}}return P?.detach(),await this.finalizeAgentRun(t,A,E,d,r,I,E?g?.failure:void 0,y)})();if($.status==="red"){let g=h.find(R=>R.kind==="before-heal");if($.source==="cache"&&g){let R=g.file.replace(/before-heal\.jpg$/,"failure.jpg");try{Zt.renameSync(be.join(s,g.file),be.join(s,R)),g.file=R}catch{}g.kind="failure"}else{let R=$.stepResults.find(I=>I.status==="failed")?.gherkin,E=await this.screenshot(Qt(this.browser)??x,t,"failure",R);E&&h.push(E)}}let u=Gt(le(t),y);if(u.size>0&&($.uniques=Object.fromEntries(u)),h.push(...$.evidence??[]),k){let g=v==="on"||$.status!=="green",R=Le(s,t.featurePath,t.displayName),E=g?be.join(R,Rs):void 0;w=!0,E||await As(b).catch(()=>{});let I=E?await _s(b,E,Ye(t)):void 0;if(E&&I){this.claimArtifactDir(t);let A=Ye(t);h.unshift({kind:"trace",file:be.relative(s,E).split(be.sep).join("/"),sha256:I,traceSteps:($.executionStepResults??$.stepResults).map(P=>({...P,error:P.error&&Ze(P.error,A)}))})}}return h.length>0&&($.status!=="green"||h.some(g=>g.kind==="trace"))&&($.evidence=h),$}finally{w||await As(b).catch(()=>{}),await b.close()}}async finalizeAgentRun(t,n,r,s,o,i=0,a,c=xt()){let l=r?"agent-heal":"agent-record",d=n.success?n.recordedSteps.map(T=>({gherkin:T.gherkin,kind:T.kind,status:"passed"})):Ic(n.recordedSteps,n.announcedSteps??[],a&&{stepIndex:a.stepIndex,error:a.error},n.narrative);if(!n.success)return{scenario:t,status:"red",source:l,stepResults:d,narrative:n.narrative,adaptations:n.adaptations,usage:n.usage,durationMs:Date.now()-o,error:n.narrative};let f=r?Tc(s.steps,n.recordedSteps,this.options.assertionPolicy==="adaptable-mid",a?{stepIndex:a.stepIndex,actionIndex:a.actionIndex}:void 0):n.recordedSteps,m=[...n.adaptations,...r&&a?Rc(n.recordedSteps,a.stepIndex):[]],p={...n.usage},b,v,k,w,x=[],y=0,h=new Set,S,$=async(T,O,M=O.stepResults)=>{if(!h.has(T)){h.add(T);try{if(this.options.trace&&this.options.trace!=="off"){let B=be.join(Le(this.options.projectRoot,t.featurePath,t.displayName),`proof-${++y}.zip`),W=Ye(t),ce=await _s(T,B,W);if(ce){this.claimArtifactDir(t);let Pt=O.captured[Ae],Ct=Pt?Gt(le(t),Pt):new Map;x.push({kind:"trace",proof:!0,file:be.relative(this.options.projectRoot,B).split(be.sep).join("/"),sha256:ce,traceSteps:M.map(cn=>({...cn,error:cn.error&&Ze(cn.error,W)})),traceUniques:Ct.size?Object.fromEntries(Ct):void 0})}}}finally{await T.close()}}};if(this.options.verifyProposals!==!1){let T=await this.proofReplay(t,this.buildCandidateCache(t,f),xt());w=T.result.captured[Ae];try{if(T.result.status!=="passed"&&T.result.failure){let O=T.result.failure;if(this.options.agent&&!(r&&O.kind==="assertion"&&(this.options.assertionPolicy!=="adaptable-mid"||kc(s.steps,O.stepIndex))))try{let B=await Rr(this.browser,T.result.activePage??Pn(T.page.context(),O.pageUrl)??T.page),W=await this.withDialogsVisible(()=>this.options.agent.run({scenario:t,baseURL:this.options.baseURL,traceContext:this.options.trace&&this.options.trace!=="off"?T.context:void 0,mode:"refine",model:r?this.options.healModel:void 0,cdpEndpoint:this.cdpEndpoint,projectRoot:this.options.projectRoot,assertionPolicy:this.options.assertionPolicy,completedSteps:T.result.stepResults.filter(ce=>ce.status==="passed").map(ce=>ce.gherkin),failedStep:{text:O.stepText,kind:O.kind,error:O.error,pageUrl:O.pageUrl,stepIndex:O.stepIndex},liveTab:B,uniqueSeed:T.result.captured[Ae],onTabOpened:this.tabWatcher(),takeFingerprint:async()=>vt(Qt(this.browser)??T.page)}));p=Ec(p,W.usage),S=ju(T.result.stepResults,O,W),W.success&&(m.push(...W.adaptations),m.push(...Rc(W.recordedSteps,O.stepIndex)),f=r?Tc(f,W.recordedSteps,this.options.assertionPolicy==="adaptable-mid",{stepIndex:O.stepIndex,actionIndex:0}):Ou(f,W.recordedSteps,O.stepIndex),await $(T.context,T.result,S),S=void 0,T=await this.proofReplay(t,this.buildCandidateCache(t,f),xt()),w=T.result.captured[Ae])}catch{}}for(let O of T.result.tabOpeners??[]){let M=f[O.stepIndex]?.actions[O.actionIndex];M&&(M.opensTab=!0)}b=T.result.status==="passed",b||(v=T.result.failure?.error??`proof replay ${T.result.status}`,k=T.result.stepResults)}finally{await $(T.context,T.result,S).catch(()=>{})}}r&&s&&m.push(...Cu(s.steps,f,a,this.options.baseURL));let u=[...n.featureEdits];for(let T of gi(f,Be(this.options.projectRoot)))m.push(`duplicate wording: "${T.from}" recorded exactly the same actions as the existing step "${T.to}" (${T.feature} \u203A ${T.scenario}). Accept with --with-feature-edit to converge on the canonical wording.`),u.some(O=>O.index===T.index)||u.push({index:T.index,text:T.to});let g=gc(f);g.length>0&&(b=!1,v=[v,...g].filter(Boolean).join(" "));let R=Ye(t),E=T=>R.size>0?Ze(T,R):T;m=m.map(E);let I=u.map(T=>({index:T.index,text:E(T.text)})),A=E(n.narrative),P=n.suggestedFeatureEdit?E(n.suggestedFeatureEdit):void 0,_={version:1,feature:t.featurePath,scenario:t.name,recordedAt:new Date().toISOString(),recordedBy:"agent",steps:f,...$c(t)},j={meta:{createdAt:_.recordedAt,mode:r?"heal":"record",narrative:A,adaptations:m,suggestedFeatureEdit:P,featureEdits:I.length>0?I:void 0,verified:b,proofError:v?E(v):void 0,seededSteps:i>0?i:void 0,recordedFor:t.displayName,usage:p,bound:ma(t,s,_,I.length>0?I:void 0)},cache:_},F=xe(this.options.projectRoot,t.featurePath,t.name),C=Si(F,j),L=me(Y(this.options.projectRoot,t.featurePath,t.name));return{proposalDiff:lt(L,j.cache),scenario:t,status:"yellow",source:l,stepResults:k??d,executionStepResults:d,evidence:x,proofUniques:(()=>{let T=w?Gt(le(t),w):void 0;return T&&T.size>0?Object.fromEntries(T):void 0})(),narrative:A,adaptations:m,suggestedFeatureEdit:P,featureEdits:I.length>0?I:void 0,verified:b,proofError:v,seededSteps:i>0?i:void 0,proposalFile:F,proposalRevision:C,usage:p,durationMs:Date.now()-o}}};import Br from"node:fs";import Og from"node:os";import sl from"node:path";import{query as sg}from"@anthropic-ai/claude-agent-sdk";import{McpServer as Nu}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as H}from"zod";var pe=e=>({content:[{type:"text",text:e}]});function Xt(e,t,n,r){let s=async o=>{let i=await r(o);return i.content[0]?.text.startsWith("ERROR:")?{...i,isError:!0}:i};return o=>{o.registerTool(e,{description:t,inputSchema:n},s)}}function Fu(e){let t=new Nu({name:e.name,version:e.version});for(let n of e.tools)n(t);return{type:"sdk",name:e.name,instance:t}}function Pc(e){let t={};return e.role&&(t.role=e.role),e.name&&(t.name=e.name),e.nameRegex&&(t.nameRegex=e.nameRegex),e.label&&(t.label=e.label),e.text&&(t.text=e.text),e.selector&&(t.fallbackSelectors=[e.selector]),e.frame&&(t.frame=e.frame),Object.keys(t).length>0?t:void 0}var Mu=new Set(["main","banner","contentinfo","status","alert","alertdialog","dialog","search"]);function Cc(e){if(!e?.role)return;let{role:t,frame:n,...r}=e;if(!(Object.keys(r).length>0||Mu.has(t)))return`ERROR: role="${t}" alone matches every ${t} on the page, and replay would use the first one. Add a name (or nameRegex), text, label or selector that picks exactly the element you checked, then call again.`}function Du(e,t){return t.index>=e.steps.length?`ERROR: there is no step ${t.index}: this scenario's steps are numbered from 0 to ${e.steps.length-1}. Call saffron_step again with the zero-based index of the step you are about to perform.`:(e.announce(t.index,t.text,t.adaptation),t.index<e.firstStep?`Step ${t.index} already ran before you took over and keeps its recording; nothing you do under it is recorded. Do not redo it: continue from step ${e.firstStep} on the page as it is.`:`Recording step ${t.index}: ${t.text}`)}var Oc=new WeakSet;function Lu(e,t){if(t.success&&!Oc.has(e)){let n=[...e.announced].filter(r=>r>=e.firstStep&&e.steps[r]?.kind==="assertion"&&!e.steps[r].actions.some(s=>Se.has(s.action))).sort((r,s)=>r-s);if(n.length>0)return Oc.add(e),`ERROR: not finalized. Assertion step ${n.join(", ")} recorded no check, so replay would verify nothing. Record what you verified with saffron_assert under that step (saffron_step first if you moved on), then call saffron_done again. If the condition does not hold, call saffron_done with success=false.`}return e.done=!0,e.success=t.success,e.narrative=t.narrative,e.suggestedFeatureEdit=t.suggestedFeatureEdit,e.featureEdits=t.featureEdits??[],"Run finalized."}var jc={role:H.string().optional().describe("ARIA role of the element"),name:H.string().optional().describe("Accessible name \u2014 use the SHORTEST stable substring; never include volatile content like prices or dates"),nameRegex:H.string().optional().describe("Regex for the accessible name when it embeds volatile content, e.g. 'Select \\\\{date\\\\+1\\\\} as checkin' cannot work \u2014 instead use nameRegex 'Select .* as checkin'"),label:H.string().optional().describe("Form-label text for getByLabel \u2014 the literal <label> text only, never a free-text description"),text:H.string().optional().describe("getByText locator text"),selector:H.string().optional().describe("CSS fallback selector"),frame:H.string().optional().describe(`CSS selector of the iframe hosting the element, e.g. 'iframe[title="Feedback widget"]' \u2014 REQUIRED whenever the element lives inside an iframe; without it replay looks only at the top-level page`)};function Ar(e){return Fu({name:"saffron",version:"0.1.0",tools:[Xt("saffron_step","Announce the Gherkin step you are about to perform. MUST be called before executing browser actions for each step, with the step's zero-based index and exact text.",{index:H.number().int().min(0),text:H.string(),adaptation:H.string().optional().describe("If the written step does not match the real UI and you are deviating from it, describe the deviation in one sentence: the kind of change (a locator or label change on the same page, a page removed or merged, a page added or flow reordered) and the page it happened on.")},async t=>pe(Du(e,t))),Xt("saffron_capture",`Record a captureText action for the current step: at replay time, the target element's text is read and stored under saveAs for later expectDiffers comparisons. Use for steps like 'I record the displayed price as "X"'. The target must select exactly the element whose text is the value.`,{saveAs:H.string().describe("Name to store the captured text under"),...jc},async t=>{let n=Pc(t);if(!n)return pe("ERROR: capture needs a target (role/name, nameRegex, label, text, or selector).");let r=Cc(n);return r?pe(r):(e.addActions([{action:"captureText",target:n,saveAs:t.saveAs}]),pe(`Capture recorded as "${t.saveAs}".`))}),Xt("saffron_dialog","Record how a native dialog (alert/confirm/prompt) must be handled at replay. Call this IMMEDIATELY whenever an action triggers a native dialog \u2014 whether you handled it via browser_handle_dialog or the browser reported it as already handled. At replay Saffron arms this handler BEFORE the triggering action, so without this call the replay will auto-dismiss the dialog and diverge from what you saw.",{accept:H.boolean().describe("true = accept/OK the dialog, false = dismiss/cancel"),promptText:H.string().optional().describe("Text typed into a prompt() before accepting")},async t=>(e.addArmedAction({action:"handleDialog",value:t.accept?"accept":"dismiss",...t.promptText?{promptText:t.promptText}:{}}),pe(`Dialog handling recorded (${t.accept?"accept":"dismiss"}).`))),Xt("saffron_wait_api","Record a network wait for the current step: replay will block until a matching API response is observed (responses from the triggering step count). Call AFTER you verified via browser_network_requests that the response actually occurred with the expected status/body. Use a path-only urlPattern regex and never literal volatile IDs (/api/orders/\\d+, not /api/orders/8842).",{urlPattern:H.string().describe("Regex matched against the response URL \u2014 path-only for portability"),method:H.string().optional().describe("HTTP method to match (any if omitted)"),status:H.string().optional().describe("Expected status \u2014 exact ('201') or class ('2xx'); default any 2xx"),bodyPattern:H.string().optional().describe(`Regex the response body must match, e.g. '"status"\\s*:\\s*"READY"' for polling waits`)},async t=>(e.addActions([{action:"waitForResponse",urlPattern:t.urlPattern,...t.method?{method:t.method}:{},...t.status?{status:t.status}:{},...t.bodyPattern?{pattern:t.bodyPattern}:{}}]),pe("Network wait recorded."))),Xt("saffron_assert","Record a verified assertion for the current Then step. Call ONLY after you have confirmed the condition holds via a page snapshot. If the condition does NOT hold, do not call this \u2014 call saffron_done with success=false instead. NEVER record volatile content (prices, dates, counters) as literal expected text \u2014 use kind=matches with a pattern, or kind=differs against captured values.",{kind:H.enum(["visible","notVisible","text","url","value","differs","matches","attribute","response","each"]),...jc,expected:H.string().optional().describe("Expected text/value for kind=text|value \u2014 stable text only, never volatile values"),url:H.string().optional().describe("URL substring for kind=url (path only, no host)"),pattern:H.string().optional().describe("kind=matches|attribute: regex the target's text (or attribute) must match, e.g. 'NOK [\\\\d,]+' for a price"),attribute:H.string().optional().describe("kind=attribute: attribute name to check, e.g. 'href' for link-target assertions \u2014 use this for 'X should link to Y' instead of clicking through"),capture:H.string().optional().describe("kind=differs: name of the captured value on the LEFT of the comparison (omit when comparing the target element's live text)"),urlPattern:H.string().optional().describe("kind=response: regex matched against the response URL \u2014 path-only for portability, never literal volatile IDs (use /api/orders/\\d+)"),method:H.string().optional().describe("kind=response: HTTP method to match (any if omitted)"),status:H.string().optional().describe("kind=response: expected status \u2014 exact ('201') or class ('2xx'); default any 2xx"),bodyPattern:H.string().optional().describe("kind=response: regex the response body must match (verify via browser_network_requests first)"),each:H.string().optional().describe("kind=each: the data-list token exactly as the step writes it, e.g. '{data:enums.OrderStatus}'. The target is the CONTAINER (dropdown, table, list) that must show every value of that list. Never pass the values themselves."),compareTo:H.string().optional().describe("kind=differs: name of the captured value to compare against")},async t=>{let n=Pc(t),r=Cc(n);if(r)return pe(r);let s;switch(t.kind){case"visible":s={action:"expectVisible",target:n};break;case"notVisible":s={action:"expectNotVisible",target:n};break;case"text":s={action:"expectText",target:n,value:t.expected??""};break;case"value":s={action:"expectValue",target:n,value:t.expected??""};break;case"url":s={action:"expectUrl",url:t.url??""};break;case"differs":if(!t.compareTo)return pe("ERROR: kind=differs requires compareTo.");if(!t.capture&&!n)return pe("ERROR: kind=differs needs either capture (a stored name) or a target element.");s={action:"expectDiffers",target:n,value:t.capture,compareTo:t.compareTo};break;case"matches":if(!t.pattern)return pe("ERROR: kind=matches requires pattern.");s={action:"expectMatches",target:n,pattern:t.pattern};break;case"attribute":if(!t.attribute||!t.pattern)return pe("ERROR: kind=attribute requires attribute and pattern.");s={action:"expectAttribute",target:n,attribute:t.attribute,pattern:t.pattern};break;case"each":if(!t.each||!/^\{data:[^}]+\}$/.test(t.each))return pe("ERROR: kind=each requires each='{data:file.list}', the token exactly as the step writes it (not the values).");s={action:"expectEach",target:n,each:t.each};break;case"response":if(!t.urlPattern)return pe("ERROR: kind=response requires urlPattern.");s={action:"expectResponse",urlPattern:t.urlPattern,...t.method?{method:t.method}:{},...t.status?{status:t.status}:{},...t.bodyPattern?{pattern:t.bodyPattern}:{}};break}return!["url","differs","response"].includes(t.kind)&&!n?pe("ERROR: assertion needs a target (role/name, nameRegex, label, text, or selector)."):(e.addActions([s]),pe("Assertion recorded."))}),Xt("saffron_done","Finalize the run. Call exactly once, after the final step: success=true only if every step's intent was achieved and every assertion verified.",{success:H.boolean(),narrative:H.string().describe("2-4 sentence summary of the run: what happened, and why it failed if it did. After a heal, say what kind of change the app went through (a locator change, or a page removed, merged, added or reordered) and on which page."),suggestedFeatureEdit:H.string().optional().describe("If you adapted any steps: the corrected Gherkin for those steps, as you would rewrite them in the .feature file (free text, for the human report)."),featureEdits:H.array(H.object({index:H.number().int().min(0).describe("zero-based step index"),text:H.string().describe("replacement step text WITHOUT the Given/When/Then keyword")})).optional().describe("Machine-applicable version of the suggested edits: one entry per adapted step. Provide this whenever you adapted a step.")},async t=>pe(Lu(e,t)))]})}function Ir(e={}){let t=e.deliberateSnapshots?`
287
+ `)){let c=Uu(a.trim(),t);c&&s.push(c)}}return Bu(s)}function Bu(e){return e.filter((t,n)=>{let r=e[n+1];return!(t.action==="fill"&&(t.value??"")===""&&r?.action==="fill"&&JSON.stringify(r.target)===JSON.stringify(t.target))})}function Ac(e){let t=[];for(let n of e){if(n.kind==="assertion"&&n.actions.length===0){t.push(`Step "${n.gherkin}" is an assertion but recorded no check, so nothing would be verified. Re-record with: saffron run --rerecord`);continue}if(/"[^"]+"|<[^<>]+>/.test(n.gherkin)){for(let s of n.actions)if(s.action==="fill"&&(s.value??"")===""){t.push(`Step "${n.gherkin}" recorded a fill with an empty value although the step passes one; the typed value was lost. Re-record with: saffron run --rerecord`);break}}}return t}function At(e,t,n=s=>s,r=!1){let s=e;for(let[o,i]of Object.entries(t).filter(([,a])=>a.length>=3).sort((a,c)=>c[1].length-a[1].length)){let a=n(i);if(!r){s=s.split(a).join(`<${o}>`);continue}if(i.length<5&&!/\p{N}/u.test(i)){s===a&&(s=`<${o}>`);continue}let c=a.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");s=s.replace(new RegExp(`(?<![\\p{L}\\p{N}])${c}(?![\\p{L}\\p{N}])`,"gu"),()=>`<${o}>`)}return s}function _c(e,t){let n=Object.entries(t).filter(([,l])=>l.length>=3);if(n.length===0)return e;let r=l=>l===void 0?void 0:At(l,t),s=[...n].sort((l,d)=>d[1].length-l[1].length),o=l=>{if(l!==void 0){for(let[d,f]of s)if(l.includes(f))return`<${d}>`;return l}},i=l=>l===void 0?void 0:At(l,t,ae.pattern),a=l=>l===void 0?void 0:At(l,t,void 0,!0),c=(l,d)=>l?{...l,name:d?o(l.name):r(l.name),nameRegex:i(l.nameRegex),label:r(l.label),text:d?o(l.text):r(l.text),placeholder:r(l.placeholder),testId:a(l.testId),frame:a(l.frame),fallbackSelectors:l.fallbackSelectors?.map(f=>Lt(f,(m,p)=>At(m,t,ae[p])))}:void 0;return e.map(l=>({...l,actions:l.actions.map(d=>{let f=Se.has(d.action)&&d.action!=="expectValue";return{...d,value:f?o(d.value):r(d.value),url:r(d.url),pattern:i(d.pattern),urlPattern:i(d.urlPattern),promptText:r(d.promptText),target:c(d.target,f),toTarget:c(d.toTarget,!1)}})}))}function Ic(e,t,n=r=>r){let r=ct(t.steps);return e.map((s,o)=>{let i=t.steps[o],a=i?.resolvedTable??i?.table,c=i?.resolvedDocString??i?.docString,l=Object.entries({...r,...ue(a),...vt(c)}).map(([m,p])=>[m,n(p)]).filter(([,m])=>m.length>=3).sort((m,p)=>p[1].length-m[1].length);if(l.length===0)return s;let d=(m,p=$=>$,b=!1)=>m===void 0?void 0:At(m,Object.fromEntries(l),p,b),f=m=>m?{...m,name:d(m.name),nameRegex:d(m.nameRegex,ae.pattern),label:d(m.label),text:d(m.text),placeholder:d(m.placeholder),testId:d(m.testId,void 0,!0),frame:d(m.frame,void 0,!0),fallbackSelectors:m.fallbackSelectors?.map(p=>Lt(p,(b,$)=>d(b,ae[$])??b))}:void 0;return{...s,actions:s.actions.map(m=>({...m,value:d(m.value),url:d(m.url),pattern:d(m.pattern,ae.pattern),urlPattern:d(m.urlPattern,ae.pattern),promptText:d(m.promptText),target:f(m.target),toTarget:f(m.toTarget)}))}})}function Pc(e,t=new Date){return e.map(n=>({...n,actions:n.actions.map(r=>Ao(r,s=>Di(s,t)))}))}function Ao(e,t,n){let r=(a,c,l=t)=>{let d={...a};for(let f of c)typeof a[f]=="string"&&(d[f]=l(a[f]));return d},s=a=>n?n(a,"pattern"):t(a),o=a=>{let c=r(r(a,["name","label","text","placeholder","testId","frame"]),["nameRegex"],s);return a.fallbackSelectors&&(c.fallbackSelectors=a.fallbackSelectors.map(l=>n?Lt(l,n,t):t(l))),c},i=r(r(e,["value","url","promptText"]),["pattern","urlPattern"],s);return e.target&&(i.target=o(e.target)),e.toTarget&&(i.toTarget=o(e.toTarget)),i}function zu(e,t){if(t.length===0)return e;let n=new Map(t.map(([o,i])=>[i,o])),r=[...n.keys()].sort((o,i)=>i.length-o.length).map(Qe).join("|"),s=new RegExp(String.raw`(?<=^|[^A-Za-z0-9\\]|\\[A-Za-z])(?:${r})(?![A-Za-z0-9])`,"g");return e.split(/(\{(?:env|data|unique|date)[^{}]*\})/).map((o,i)=>i%2?o:o.replace(s,a=>`{env:${n.get(a)}}`)).join("")}function Cc(e,t,n=[]){if(t.size===0)return e;let r=ls(n,t),s=bt(t,{standaloneBelow:6}),o=l=>s.mask(r(l)),i=[...t].filter(([,l])=>l.length>=yt),a=l=>{let d=ls(n,t,ae[l]),f=bt(new Map(i.filter(([,p])=>p.length>=6).map(([p,b])=>[p,ae[l](b)]))),m=i.filter(([,p])=>p.length<6).map(([p,b])=>[p,ae[l](b)]);return p=>o(zu(f.mask(d(p)),m))},c={quoted:a("quoted"),regex:a("regex"),pattern:a("pattern")};return e.map(l=>({...l,actions:l.actions.map(d=>Ao(d,o,(f,m)=>c[m](f)))}))}function Oc(e,t){if(t.size===0)return e;let n=r=>typeof r=="string"?Bi(r,t):Array.isArray(r)?r.map(n):r&&typeof r=="object"?Object.fromEntries(Object.entries(r).map(([s,o])=>[s,s==="pageFingerprint"?o:n(o)])):r;return e.map(r=>({...r,actions:r.actions.map(s=>n(s))}))}function jc(e,t){return t.size===0?e:e.map(n=>{let r=new Set([...n.gherkin.matchAll(un)].map(l=>l[1])),s=l=>os(l,t,r),o=l=>new Map([...t].filter(([,d])=>d.length>=4).map(([d,f])=>[d,ae[l](f)])),i={quoted:o("quoted"),regex:o("regex"),pattern:o("pattern")},a=l=>l.split(/(\{(?:env|data|unique|date)[^{}]*\})/).map((d,f)=>f%2?d:s(d)).join(""),c=(l,d)=>a(os(l,i[d],r));return{...n,actions:n.actions.map(l=>Ao(l,s,c))}})}function Nc(e,t,n){if(e!=="mcp__playwright__browser_tabs")return;let r=t??{},s=typeof r.index=="number"?String(r.index):void 0;switch(r.action){case"select":return s===void 0?void 0:{action:"switchTab",value:s};case"close":return s===void 0?{action:"closeTab"}:{action:"closeTab",value:s};case"new":return typeof r.url=="string"&&r.url?{action:"newTab",url:bn(r.url,n)}:{action:"newTab"};default:return}}function Fc(e,t){let n=t??{};if(e==="mcp__playwright__browser_handle_dialog"){let r={action:"handleDialog",value:n.accept?"accept":"dismiss"};return typeof n.promptText=="string"&&n.promptText&&(r.promptText=n.promptText),r}if(e==="mcp__playwright__browser_file_upload"){let r=Array.isArray(n.paths)?n.paths:[];if(r.length===0)return;let s=process.cwd()+"/";return{action:"uploadFile",value:r.map(i=>typeof i=="string"&&i.startsWith(s)?i.slice(s.length):i).join(`
288
+ `)}}}function Mn(e,t){let n=e.pages().filter(s=>!s.isClosed()),r=s=>[...n].reverse().find(s);return(t?r(s=>s.url()===t):void 0)??r(s=>s.url()!=="about:blank")??n.at(-1)}async function Or(e,t){let n=t.context();for(let s of e.contexts())if(s!==n)for(let o of s.pages())await o.close().catch(()=>{});await t.bringToFront().catch(()=>{});let r=n.pages().filter(s=>!s.isClosed());return{index:Math.max(r.indexOf(t),0),count:r.length,url:t.url()}}var Io=["chromium","chrome","msedge","firefox","webkit"],_o={chrome:{label:"Google Chrome"},msedge:{label:"Microsoft Edge"}};function zc(e){return Io.includes(e)}var Dn=class extends Error{},_t={aiCalls:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreationTokens:0,costUsd:0};async function Gu(){return new Promise((e,t)=>{let n=Hu.createServer();n.listen(0,"127.0.0.1",()=>{let{port:r}=n.address();n.close(()=>e(r))}),n.on("error",t)})}function Mc(e){let t=Xn(de(e));return t.size>0?{data:Object.fromEntries([...t].sort(([n],[r])=>n.localeCompare(r)))}:{}}function nn(e){let t=e.contexts().flatMap(n=>n.pages());return[...t].reverse().find(n=>n.url()!=="about:blank")??t.at(-1)}function Hc(e){let t=e.length;for(let n=e.length-1;n>=0&&e[n].kind==="assertion";n--)t=n;return t}function Dc(e,t){return e[t]?.kind==="assertion"&&t>=Hc(e)}function Lc(e,t,n=!1,r){let s=Hc(e);return e.map((o,i)=>{if(r&&i<r.stepIndex)return o;let a=t[i],c=a&&a.actions.length>0;return o.kind==="assertion"?n&&i<s&&c?a:o:c?r&&i===r.stepIndex&&r.actionIndex>0?{...a,actions:[...o.actions.slice(0,r.actionIndex),...a.actions]}:a:o})}function Vu(e,t,n,r){let s=ba(e,t);return s.length===0||!n?.pageUrl?s:[`page at failure: replay stopped at step ${n.stepIndex+1} ("${n.stepText}") on ${bn(n.pageUrl,r)}, where the heal started.`,...s]}function Ku(e,t,n=0){return e.map((r,s)=>{if(s<n)return r;let o=t[s];return o&&o.actions.length>0?o:r})}function Uc(e,t){let n=[];for(let r=0;r<Math.min(t,e.length);r++){let s=e[r].actions.length;s!==0&&n.push(`recording: kept the existing recording of step ${r+1} ("${e[r].gherkin}"), which ran before the agent took over; ignored ${s} action${s===1?"":"s"} the agent's session held for it.`)}return n}function qc(e,t,n,r){let s=t.filter(c=>c>=0&&c<e.length),o=s.length>0?Math.max(...s):0,i=Math.min(n&&n.stepIndex>=o?n.stepIndex:o,e.length-1),a=n&&i===n.stepIndex?n.error:r;return e.map((c,l)=>({gherkin:c.gherkin,kind:c.kind,status:l<i?"passed":l===i?"failed":"skipped",...l===i&&a?{error:a}:{}}))}function Yu(e,t,n){let r=n.success?e.map(s=>({gherkin:s.gherkin,kind:s.kind,status:"passed"})):qc(e,n.announcedSteps??[],t,n.narrative);return e.map((s,o)=>o<t.stepIndex?s:r[o])}function Bc(e,t){return{aiCalls:e.aiCalls+t.aiCalls,inputTokens:e.inputTokens+t.inputTokens,outputTokens:e.outputTokens+t.outputTokens,cacheReadTokens:e.cacheReadTokens+t.cacheReadTokens,cacheCreationTokens:e.cacheCreationTokens+t.cacheCreationTokens,costUsd:(e.costUsd??0)+(t.costUsd??0),models:e.models||t.models?[...new Set([...e.models??[],...t.models??[]])]:void 0}}var jr=class{constructor(t){this.options=t}options;browser;cdpEndpoint;stepIndex;get agent(){return this.options.agent}getStepIndex(){if(!this.stepIndex&&(this.stepIndex=ze(this.options.projectRoot),this.stepIndex.steps.size>0)){let t=this.stepIndex.divergent.length>0?` \xB7 ${this.stepIndex.divergent.length} step(s) have divergent recordings (Level-2 signal)`:"";console.log(`step index: ${this.stepIndex.steps.size} reusable steps from ${this.stepIndex.scannedCaches} cache(s)${t}`)}return this.stepIndex}async recordWithSeeding(t,n,r){let s=this.getStepIndex(),o=t.steps.length,i={...t.params,...ct(t.steps)},a=t.steps.map(u=>{let v=s.steps.size>0?Ti(s,u,i):void 0;return v?{gherkin:u.text,keyword:u.keyword,kind:u.kind,table:u.table,docString:u.docString,fromStepSet:u.source?.fromStepSet,actions:v.actions}:void 0}),c=t.steps.map(u=>({gherkin:u.text,keyword:u.keyword,kind:u.kind,table:u.table,docString:u.docString,fromStepSet:u.source?.fromStepSet,actions:[]})),l=new Map;l.set(_e,r);let d=[],f=[],m=[],p=[],b={..._t},$=n,k=0,w=0,x=0,y=(u,v)=>({success:u,narrative:f.join(" ")||"Composed entirely from seeded recordings.",adaptations:d,suggestedFeatureEdit:p.length>0?p.join(`
289
+ `):void 0,featureEdits:m,recordedSteps:c,announcedSteps:v===void 0?[]:Array.from({length:v+1},(S,h)=>h),usage:b,seededCount:k});for(;x<o;){if(a[x]){let E=x;for(;E<o&&a[E];)E++;let _=a.slice(x,E),A={...t,steps:t.steps.slice(x,E)},C=await Tn($.isClosed()?Mn(n.context())??n:$,A,this.buildCandidateCache(t,_),{baseURL:this.options.baseURL,actionTimeoutMs:this.options.actionTimeoutMs,retries:this.options.retries,pollIntervalMs:this.options.pollIntervalMs,vars:l,trace:this.options.trace!==void 0&&this.options.trace!=="off",traceStepOffset:x});$=C.activePage??$;let I=C.status==="passed"?E-x:C.failure?.stepIndex??0;for(let M=0;M<I;M++)c[x+M]=_[M];if(k+=I,C.status==="passed"){x=E;continue}let j=x+I;d.push(`Seeded recording for "${t.steps[j].text}" did not replay in this scenario's context; the agent re-recorded it fresh. Divergence signal.`),a[j]=void 0,x=j;continue}if(!this.options.agent)return y(!1,x);w++;let u=x;for(;u<o&&!a[u];)u++;w>=3&&(u=o);let v=await Or(this.browser,$.isClosed()?Mn(n.context())??n:$),S=E=>{$=E};n.context().on("page",S);let h;try{h=await this.withDialogsVisible(()=>this.options.agent.run({scenario:t,baseURL:this.options.baseURL,traceContext:this.options.trace&&this.options.trace!=="off"?n.context():void 0,mode:"record",cdpEndpoint:this.cdpEndpoint,projectRoot:this.options.projectRoot,assertionPolicy:this.options.assertionPolicy,completedSteps:x>0?t.steps.slice(0,x).map(E=>E.resolvedText):void 0,recordOnly:u<o?{from:x,to:u-1}:void 0,liveTab:v,onActiveTab:E=>{let _=n.context().pages().filter(A=>!A.isClosed())[E];_&&($=_)},uniqueSeed:l.get(_e),onTabOpened:this.tabWatcher(),takeFingerprint:async()=>kt(nn(this.browser)??n)}))}finally{n.context().off("page",S)}if(b=Bc(b,h.usage),f.push(h.narrative),d.push(...h.adaptations),m.push(...h.featureEdits),h.suggestedFeatureEdit&&p.push(h.suggestedFeatureEdit),!h.success){let E=h.announcedSteps.filter(_=>_>=x&&_<o);return y(!1,E.length>0?Math.max(...E):x)}let g=h.announcedSteps.filter(E=>E>=x&&E<o),T=Math.min(o-1,Math.max(g.length>0?Math.max(...g):u-1,x));for(let E=x;E<=T;E++)c[E]=h.recordedSteps[E];x=T+1}return y(!0)}async withDialogsVisible(t){let n=()=>{},r=[],s=[],o=i=>{i.on("dialog",n),r.push(i)};for(let i of this.browser?.contexts()??[]){i.on("page",o),s.push(i);for(let a of i.pages())o(a)}try{return await t()}finally{for(let i of s)i.off("page",o);for(let i of r)try{i.off("dialog",n)}catch{}}}async start(){let t=this.options.browser??"chromium";if(t==="chromium"||_o[t]){let n=await Gu();try{this.browser=await qu.launch({headless:!this.options.headed,channel:_o[t]?t:void 0,args:[`--remote-debugging-port=${n}`]})}catch(r){let s=r instanceof Error?r.message:String(r),o=_o[t];throw o&&/is not found at|distribution .* is not found/i.test(s)?new Dn(`${o.label} is not installed on this machine, so --browser ${t} cannot run.
290
+ Install it (npx playwright install ${t} does that), or use --browser chromium, which Saffron ships with.`):r}this.cdpEndpoint=`http://127.0.0.1:${n}`}else this.browser=await(t==="firefox"?Ju:Wu).launch({headless:!this.options.headed}),this.cdpEndpoint=void 0}async stop(){await this.browser?.close(),this.browser=void 0}resolveStorageState(){if(!this.options.storageState)return;let t=we.isAbsolute(this.options.storageState)?this.options.storageState:we.join(this.options.projectRoot,this.options.storageState);if(!tn.existsSync(t))throw new Error(`storageState file not found: ${t}. Generate one with: npx playwright open --save-storage=${this.options.storageState} <url>`);return t}buildCandidateCache(t,n){return{version:1,feature:t.featurePath,scenario:t.name,recordedAt:new Date().toISOString(),recordedBy:"agent",steps:n,...Mc(t)}}tabWatcher(){return t=>{let n=this.browser?.contexts()??[],r=()=>t();for(let s of n)s.on("page",r);return()=>{for(let s of n)s.off("page",r)}}}async proofReplay(t,n,r){let s=await this.browser.newContext({storageState:this.resolveStorageState()}),o=this.options.trace!==void 0&&this.options.trace!=="off";o&&await Ns(s,`${t.displayName} (proof replay)`);let i=await s.newPage();return{result:await Tn(i,t,n,{baseURL:this.options.baseURL,actionTimeoutMs:this.options.actionTimeoutMs,retries:this.options.retries,pollIntervalMs:this.options.pollIntervalMs,uniqueSeed:r,trace:o}),context:s,page:i}}async runScenario(t,n={}){let r=await this.runScenarioInner(t,n),s=Ze(t);if(s.size>0){let o=i=>i===void 0?i:Xe(i,s);r.error=o(r.error),r.narrative=o(r.narrative),r.proofError=o(r.proofError),r.suggestedFeatureEdit=o(r.suggestedFeatureEdit),r.adaptations=r.adaptations.map(i=>Xe(i,s));for(let i of r.stepResults)i.error=o(i.error)}return r}async screenshot(t,n,r,s){let o=this.options.screenshot??"viewport";if(o!=="off")try{let i=Ue(this.options.projectRoot,n.featurePath,n.displayName);this.claimArtifactDir(n);let a=we.join(i,`${r}.jpg`),c=await t.screenshot({path:a,type:"jpeg",quality:80,fullPage:o==="full-page",timeout:5e3});return{kind:r,file:we.relative(this.options.projectRoot,a).split(we.sep).join("/"),step:s,sha256:je(c)}}catch{return}}claimArtifactDir(t){let n=Ue(this.options.projectRoot,t.featurePath,t.displayName);tn.mkdirSync(n,{recursive:!0}),tn.writeFileSync(we.join(n,Bt),JSON.stringify({feature:t.featurePath,scenario:t.displayName},null,2))}async runScenarioInner(t,n={}){if(!this.browser)throw new Error("Orchestrator not started");let r=Date.now(),{projectRoot:s,baseURL:o,actionTimeoutMs:i,retries:a,pollIntervalMs:c}=this.options,l=Y(s,t.featurePath,t.name),d;try{d=ye(l)}catch(S){if(!(S instanceof X))throw S;return{scenario:t,status:"red",source:"cache",stepResults:[],adaptations:[],usage:_t,durationMs:Date.now()-r,error:`The recording cannot be read: ${S.problem}. Fix the file, or re-record it with saffron run --rerecord.`}}let f=xe(s,t.featurePath,t.name),m,p;if(!d&&tn.existsSync(f))try{({proposal:m,revision:p}=fr(f))}catch(S){if(!(S instanceof X)&&S.code!=="ENOENT")throw S;m=void 0}m&&ke(s,m,t)&&(m=void 0);let b=await this.browser.newContext({storageState:this.resolveStorageState()}),$=this.options.trace??"off",k=$!=="off";k&&await Ns(b,t.displayName);let w=!k,x=await b.newPage(),y=$t(),u=[];tn.rmSync(Ue(s,t.featurePath,t.displayName),{recursive:!0,force:!0});let v=(S,h)=>this.screenshot(S,t,"before-heal",h.stepText).then(g=>{g&&u.push(g)});try{let S=await(async()=>{let g;if(m){let I=await Tn(x,t,m.cache,{baseURL:o,actionTimeoutMs:i,retries:a,pollIntervalMs:c,uniqueSeed:y,trace:k,tracePhase:"pending"}),j=m.meta.recordedFor??t.name;if(I.status==="passed")return{scenario:t,status:"yellow",source:"agent-record",proposalReplay:!0,stepResults:I.stepResults,narrative:`Replayed the pending proposal recorded for "${j}" at zero AI; accept it to go green.`,adaptations:[],verified:!0,proposalFile:f,proposalRevision:p,usage:_t,durationMs:Date.now()-r};if(Object.keys(t.params).length>0&&j!==t.displayName){let z=I.failure;return{scenario:t,status:"red",source:"agent-record",proposalReplay:!0,stepResults:I.stepResults,adaptations:[],proposalFile:f,proposalRevision:p,usage:_t,durationMs:Date.now()-r,error:`The pending proposal recorded for "${j}" does not replay for this Examples row`+(z?` (step "${z.stepText}": ${z.error.split(`
291
+ `)[0]})`:"")+". A row value is probably baked in as a literal instead of a <placeholder>; inspect the proposal, reject it, and re-record with saffron run --rerecord."}}console.log(` \u21BA pending proposal for "${t.displayName}" did not replay`+(I.failure?` (step "${I.failure.stepText}": ${I.failure.error.split(`
292
+ `)[0]})`:"")+"; recording again"),await x.goto("about:blank")}if(d&&(g=await Tn(x,t,d,{baseURL:o,actionTimeoutMs:i,retries:a,pollIntervalMs:c,uniqueSeed:y,trace:k,onFailure:v,keepResponseLog:!0}),g.status==="passed"))return{scenario:t,status:"green",source:"cache",stepResults:g.stepResults,adaptations:[],usage:_t,durationMs:Date.now()-r};if(!(this.options.agent!==void 0&&this.cdpEndpoint!==void 0&&!n.agentDisabled)){let I=this.options.agent!==void 0&&this.cdpEndpoint===void 0&&!n.agentDisabled?`recording/healing needs a Chromium-family browser (the agent attaches over CDP). Record with --browser chromium (the default), chrome or msedge, then replay with --browser ${this.options.browser}.`:"no agent is configured.";return g?.responses?.detach(),{scenario:t,status:"red",source:"cache",stepResults:g?.stepResults??[],adaptations:[],usage:_t,durationMs:Date.now()-r,error:d?g?.status==="stale"?`Cache is stale (feature file changed) and ${I}`:`Replay failed and ${I} ${g?.failure?.error??""}`.trimEnd():`No cache exists and ${I}`}}let E=d!==void 0&&g!==void 0&&g.status==="failed",_=0,A,C;try{if(E){C=g.responses;let I=await Or(this.browser,g.activePage??Mn(x.context(),g.failure.pageUrl)??x);A=await this.withDialogsVisible(()=>this.options.agent.run({scenario:t,baseURL:o,traceContext:k?x.context():void 0,mode:"heal",liveTab:I,model:this.options.healModel,cdpEndpoint:this.cdpEndpoint,projectRoot:this.options.projectRoot,assertionPolicy:this.options.assertionPolicy,completedSteps:g.stepResults.filter(j=>j.status==="passed").map(j=>j.gherkin),failedStep:{text:g.failure.stepText,kind:g.failure.kind,error:g.failure.error,pageUrl:g.failure.pageUrl,stepIndex:g.failure.stepIndex},uniqueSeed:y,onTabOpened:this.tabWatcher(),takeFingerprint:async()=>{let j=nn(this.browser)??x;return kt(j)}}))}else if(await x.goto("about:blank"),this.options.reuseSteps!==!1){let I=await this.recordWithSeeding(t,x,y);_=I.seededCount,A=I}else A=await this.withDialogsVisible(()=>this.options.agent.run({scenario:t,baseURL:o,traceContext:k?x.context():void 0,mode:"record",cdpEndpoint:this.cdpEndpoint,projectRoot:this.options.projectRoot,assertionPolicy:this.options.assertionPolicy,uniqueSeed:y,onTabOpened:this.tabWatcher(),takeFingerprint:async()=>{let I=nn(this.browser)??x;return kt(I)}}))}catch(I){C?.detach();let j=I instanceof Error?I.message:String(I);return{scenario:t,status:"red",source:E?"agent-heal":"agent-record",stepResults:g?.stepResults??[],adaptations:[],usage:_t,durationMs:Date.now()-r,error:`Agent invocation failed: ${j.split(`
293
+ `)[0]}`}}if(A.success&&E&&g.failure.kind==="assertion"&&(this.options.assertionPolicy!=="adaptable-mid"||Dc(d.steps,g.failure.stepIndex))){let I=d.steps[g.failure.stepIndex],j=nn(this.browser)??x,M=new Map(Object.entries(g.captured)),z=Gs(t,g.failure.stepIndex);try{for(let P of I.actions)await xr(j,P,z,this.options.actionTimeoutMs??5e3,o,M,C)}catch(P){let R=P instanceof Error?P.message:String(P),O=`Assertion failed as written: "${I.gherkin}". Saffron never adapts assertions. If this change is intended, update the .feature file or re-record the scenario (saffron run --rerecord --filter @tag). If the recording itself looks wrong (an empty value, a wrong target), re-record it the same way. Verification error: ${R.split(`
294
+ `)[0]}`,D=g.failure.stepIndex,W=g.stepResults.map((U,le)=>le===D?{...U,status:"failed",error:O}:U);return{scenario:t,status:"red",source:"agent-heal",stepResults:W,executionStepResults:W.map((U,le)=>le>D&&U.status==="skipped"?{...U,status:"passed"}:U),narrative:A.narrative,adaptations:A.adaptations,suggestedFeatureEdit:A.suggestedFeatureEdit,usage:A.usage,durationMs:Date.now()-r,error:O}}finally{C?.detach()}}return C?.detach(),await this.finalizeAgentRun(t,A,E,d,r,_,E?g?.failure:void 0,y)})();if(S.status==="red"){let g=u.find(T=>T.kind==="before-heal");if(S.source==="cache"&&g){let T=g.file.replace(/before-heal\.jpg$/,"failure.jpg");try{tn.renameSync(we.join(s,g.file),we.join(s,T)),g.file=T}catch{}g.kind="failure"}else{let T=S.stepResults.find(_=>_.status==="failed")?.gherkin,E=await this.screenshot(nn(this.browser)??x,t,"failure",T);E&&u.push(E)}}let h=Kt(de(t),y);if(h.size>0&&(S.uniques=Object.fromEntries(h)),u.push(...S.evidence??[]),k){let g=$==="on"||S.status!=="green",T=Ue(s,t.featurePath,t.displayName),E=g?we.join(T,js):void 0;w=!0,E||await Fs(b).catch(()=>{});let _=E?await Ms(b,E,Ze(t)):void 0;if(E&&_){this.claimArtifactDir(t);let A=Ze(t);u.unshift({kind:"trace",file:we.relative(s,E).split(we.sep).join("/"),sha256:_,traceSteps:(S.executionStepResults??S.stepResults).map(C=>({...C,error:C.error&&Xe(C.error,A)}))})}}return u.length>0&&(S.status!=="green"||u.some(g=>g.kind==="trace"))&&(S.evidence=u),S}finally{w||await Fs(b).catch(()=>{}),await b.close()}}async finalizeAgentRun(t,n,r,s,o,i=0,a,c=$t()){let l=r?"agent-heal":"agent-record",d=n.success?n.recordedSteps.map(R=>({gherkin:R.gherkin,kind:R.kind,status:"passed"})):qc(n.recordedSteps,n.announcedSteps??[],a&&{stepIndex:a.stepIndex,error:a.error},n.narrative);if(!n.success)return{scenario:t,status:"red",source:l,stepResults:d,narrative:n.narrative,adaptations:n.adaptations,usage:n.usage,durationMs:Date.now()-o,error:n.narrative};let f=r?Lc(s.steps,n.recordedSteps,this.options.assertionPolicy==="adaptable-mid",a?{stepIndex:a.stepIndex,actionIndex:a.actionIndex}:void 0):n.recordedSteps,m=[...n.adaptations,...r&&a?Uc(n.recordedSteps,a.stepIndex):[]],p={...n.usage},b,$,k,w,x=[],y=0,u=new Set,v,S=async(R,O,D=O.stepResults)=>{if(!u.has(R)){u.add(R);try{if(this.options.trace&&this.options.trace!=="off"){let W=we.join(Ue(this.options.projectRoot,t.featurePath,t.displayName),`proof-${++y}.zip`),U=Ze(t),le=await Ms(R,W,U);if(le){this.claimArtifactDir(t);let jt=O.captured[_e],Nt=jt?Kt(de(t),jt):new Map;x.push({kind:"trace",proof:!0,file:we.relative(this.options.projectRoot,W).split(we.sep).join("/"),sha256:le,traceSteps:D.map(fn=>({...fn,error:fn.error&&Xe(fn.error,U)})),traceUniques:Nt.size?Object.fromEntries(Nt):void 0})}}}finally{await R.close()}}};if(this.options.verifyProposals!==!1){let R=await this.proofReplay(t,this.buildCandidateCache(t,f),$t());w=R.result.captured[_e];try{if(R.result.status!=="passed"&&R.result.failure){let O=R.result.failure;if(this.options.agent&&!(r&&O.kind==="assertion"&&(this.options.assertionPolicy!=="adaptable-mid"||Dc(s.steps,O.stepIndex))))try{let W=await Or(this.browser,R.result.activePage??Mn(R.page.context(),O.pageUrl)??R.page),U=await this.withDialogsVisible(()=>this.options.agent.run({scenario:t,baseURL:this.options.baseURL,traceContext:this.options.trace&&this.options.trace!=="off"?R.context:void 0,mode:"refine",model:r?this.options.healModel:void 0,cdpEndpoint:this.cdpEndpoint,projectRoot:this.options.projectRoot,assertionPolicy:this.options.assertionPolicy,completedSteps:R.result.stepResults.filter(le=>le.status==="passed").map(le=>le.gherkin),failedStep:{text:O.stepText,kind:O.kind,error:O.error,pageUrl:O.pageUrl,stepIndex:O.stepIndex},liveTab:W,uniqueSeed:R.result.captured[_e],onTabOpened:this.tabWatcher(),takeFingerprint:async()=>kt(nn(this.browser)??R.page)}));p=Bc(p,U.usage),v=Yu(R.result.stepResults,O,U),U.success&&(m.push(...U.adaptations),m.push(...Uc(U.recordedSteps,O.stepIndex)),f=r?Lc(f,U.recordedSteps,this.options.assertionPolicy==="adaptable-mid",{stepIndex:O.stepIndex,actionIndex:0}):Ku(f,U.recordedSteps,O.stepIndex),await S(R.context,R.result,v),v=void 0,R=await this.proofReplay(t,this.buildCandidateCache(t,f),$t()),w=R.result.captured[_e])}catch{}}for(let O of R.result.tabOpeners??[]){let D=f[O.stepIndex]?.actions[O.actionIndex];D&&(D.opensTab=!0)}b=R.result.status==="passed",b||($=R.result.failure?.error??`proof replay ${R.result.status}`,k=R.result.stepResults)}finally{await S(R.context,R.result,v).catch(()=>{})}}r&&s&&m.push(...Vu(s.steps,f,a,this.options.baseURL));let h=[...n.featureEdits];for(let R of Ri(f,ze(this.options.projectRoot)))m.push(`duplicate wording: "${R.from}" recorded exactly the same actions as the existing step "${R.to}" (${R.feature} \u203A ${R.scenario}). Accept with --with-feature-edit to converge on the canonical wording.`),h.some(O=>O.index===R.index)||h.push({index:R.index,text:R.to});let g=Ac(f);g.length>0&&(b=!1,$=[$,...g].filter(Boolean).join(" "));let T=Ze(t),E=R=>T.size>0?Xe(R,T):R;m=m.map(E);let _=h.map(R=>({index:R.index,text:E(R.text)})),A=E(n.narrative),C=n.suggestedFeatureEdit?E(n.suggestedFeatureEdit):void 0,I={version:1,feature:t.featurePath,scenario:t.name,recordedAt:new Date().toISOString(),recordedBy:"agent",steps:f,...Mc(t)},j={meta:{createdAt:I.recordedAt,mode:r?"heal":"record",narrative:A,adaptations:m,suggestedFeatureEdit:C,featureEdits:_.length>0?_:void 0,verified:b,proofError:$?E($):void 0,seededSteps:i>0?i:void 0,recordedFor:t.displayName,usage:p,bound:Aa(t,s,I,_.length>0?_:void 0)},cache:I},M=xe(this.options.projectRoot,t.featurePath,t.name),z=Ci(M,j),P=ye(Y(this.options.projectRoot,t.featurePath,t.name));return{proposalDiff:pt(P,j.cache),scenario:t,status:"yellow",source:l,stepResults:k??d,executionStepResults:d,evidence:x,proofUniques:(()=>{let R=w?Kt(de(t),w):void 0;return R&&R.size>0?Object.fromEntries(R):void 0})(),narrative:A,adaptations:m,suggestedFeatureEdit:C,featureEdits:_.length>0?_:void 0,verified:b,proofError:$,seededSteps:i>0?i:void 0,proposalFile:M,proposalRevision:z,usage:p,durationMs:Date.now()-o}}};import Vr from"node:fs";import Kg from"node:os";import bl from"node:path";import{query as vg}from"@anthropic-ai/claude-agent-sdk";import{McpServer as Qu}from"@modelcontextprotocol/sdk/server/mcp.js";import{z as J}from"zod";var fe=e=>({content:[{type:"text",text:e}]});function rn(e,t,n,r){let s=async o=>{let i=await r(o);return i.content[0]?.text.startsWith("ERROR:")?{...i,isError:!0}:i};return o=>{o.registerTool(e,{description:t,inputSchema:n},s)}}function Zu(e){let t=new Qu({name:e.name,version:e.version});for(let n of e.tools)n(t);return{type:"sdk",name:e.name,instance:t}}function Jc(e){let t={};return e.role&&(t.role=e.role),e.name&&(t.name=e.name),e.nameRegex&&(t.nameRegex=e.nameRegex),e.label&&(t.label=e.label),e.text&&(t.text=e.text),e.selector&&(t.fallbackSelectors=[e.selector]),e.frame&&(t.frame=e.frame),Object.keys(t).length>0?t:void 0}var Xu=new Set(["main","banner","contentinfo","status","alert","alertdialog","dialog","search"]);function Wc(e){if(!e?.role)return;let{role:t,frame:n,...r}=e;if(!(Object.keys(r).length>0||Xu.has(t)))return`ERROR: role="${t}" alone matches every ${t} on the page, and replay would use the first one. Add a name (or nameRegex), text, label or selector that picks exactly the element you checked, then call again.`}function eg(e,t){return t.index>=e.steps.length?`ERROR: there is no step ${t.index}: this scenario's steps are numbered from 0 to ${e.steps.length-1}. Call saffron_step again with the zero-based index of the step you are about to perform.`:(e.announce(t.index,t.text,t.adaptation),t.index<e.firstStep?`Step ${t.index} already ran before you took over and keeps its recording; nothing you do under it is recorded. Do not redo it: continue from step ${e.firstStep} on the page as it is.`:`Recording step ${t.index}: ${t.text}`)}var Gc=new WeakSet;function tg(e,t){if(t.success&&!Gc.has(e)){let n=[...e.announced].filter(r=>r>=e.firstStep&&e.steps[r]?.kind==="assertion"&&!e.steps[r].actions.some(s=>Se.has(s.action))).sort((r,s)=>r-s);if(n.length>0)return Gc.add(e),`ERROR: not finalized. Assertion step ${n.join(", ")} recorded no check, so replay would verify nothing. Record what you verified with saffron_assert under that step (saffron_step first if you moved on), then call saffron_done again. If the condition does not hold, call saffron_done with success=false.`}return e.done=!0,e.success=t.success,e.narrative=t.narrative,e.suggestedFeatureEdit=t.suggestedFeatureEdit,e.featureEdits=t.featureEdits??[],"Run finalized."}var Vc={role:J.string().optional().describe("ARIA role of the element"),name:J.string().optional().describe("Accessible name \u2014 use the SHORTEST stable substring; never include volatile content like prices or dates"),nameRegex:J.string().optional().describe("Regex for the accessible name when it embeds volatile content, e.g. 'Select \\\\{date\\\\+1\\\\} as checkin' cannot work \u2014 instead use nameRegex 'Select .* as checkin'"),label:J.string().optional().describe("Form-label text for getByLabel \u2014 the literal <label> text only, never a free-text description"),text:J.string().optional().describe("getByText locator text"),selector:J.string().optional().describe("CSS fallback selector"),frame:J.string().optional().describe(`CSS selector of the iframe hosting the element, e.g. 'iframe[title="Feedback widget"]' \u2014 REQUIRED whenever the element lives inside an iframe; without it replay looks only at the top-level page`)};function Nr(e){return Zu({name:"saffron",version:"0.1.0",tools:[rn("saffron_step","Announce the Gherkin step you are about to perform. MUST be called before executing browser actions for each step, with the step's zero-based index and exact text.",{index:J.number().int().min(0),text:J.string(),adaptation:J.string().optional().describe("If the written step does not match the real UI and you are deviating from it, describe the deviation in one sentence: the kind of change (a locator or label change on the same page, a page removed or merged, a page added or flow reordered) and the page it happened on.")},async t=>fe(eg(e,t))),rn("saffron_capture",`Record a captureText action for the current step: at replay time, the target element's text is read and stored under saveAs for later expectDiffers comparisons. Use for steps like 'I record the displayed price as "X"'. The target must select exactly the element whose text is the value.`,{saveAs:J.string().describe("Name to store the captured text under"),...Vc},async t=>{let n=Jc(t);if(!n)return fe("ERROR: capture needs a target (role/name, nameRegex, label, text, or selector).");let r=Wc(n);return r?fe(r):(e.addActions([{action:"captureText",target:n,saveAs:t.saveAs}]),fe(`Capture recorded as "${t.saveAs}".`))}),rn("saffron_dialog","Record how a native dialog (alert/confirm/prompt) must be handled at replay. Call this IMMEDIATELY whenever an action triggers a native dialog \u2014 whether you handled it via browser_handle_dialog or the browser reported it as already handled. At replay Saffron arms this handler BEFORE the triggering action, so without this call the replay will auto-dismiss the dialog and diverge from what you saw.",{accept:J.boolean().describe("true = accept/OK the dialog, false = dismiss/cancel"),promptText:J.string().optional().describe("Text typed into a prompt() before accepting")},async t=>(e.addArmedAction({action:"handleDialog",value:t.accept?"accept":"dismiss",...t.promptText?{promptText:t.promptText}:{}}),fe(`Dialog handling recorded (${t.accept?"accept":"dismiss"}).`))),rn("saffron_wait_api","Record a network wait for the current step: replay will block until a matching API response is observed (responses from the triggering step count). Call AFTER you verified via browser_network_requests that the response actually occurred with the expected status/body. Use a path-only urlPattern regex and never literal volatile IDs (/api/orders/\\d+, not /api/orders/8842).",{urlPattern:J.string().describe("Regex matched against the response URL \u2014 path-only for portability"),method:J.string().optional().describe("HTTP method to match (any if omitted)"),status:J.string().optional().describe("Expected status \u2014 exact ('201') or class ('2xx'); default any 2xx"),bodyPattern:J.string().optional().describe(`Regex the response body must match, e.g. '"status"\\s*:\\s*"READY"' for polling waits`)},async t=>(e.addActions([{action:"waitForResponse",urlPattern:t.urlPattern,...t.method?{method:t.method}:{},...t.status?{status:t.status}:{},...t.bodyPattern?{pattern:t.bodyPattern}:{}}]),fe("Network wait recorded."))),rn("saffron_assert","Record a verified assertion for the current Then step. Call ONLY after you have confirmed the condition holds via a page snapshot. If the condition does NOT hold, do not call this \u2014 call saffron_done with success=false instead. NEVER record volatile content (prices, dates, counters) as literal expected text \u2014 use kind=matches with a pattern, or kind=differs against captured values.",{kind:J.enum(["visible","notVisible","text","url","value","differs","matches","attribute","response","each"]),...Vc,expected:J.string().optional().describe("Expected text/value for kind=text|value \u2014 stable text only, never volatile values"),url:J.string().optional().describe("URL substring for kind=url (path only, no host)"),pattern:J.string().optional().describe("kind=matches|attribute: regex the target's text (or attribute) must match, e.g. 'NOK [\\\\d,]+' for a price"),attribute:J.string().optional().describe("kind=attribute: attribute name to check, e.g. 'href' for link-target assertions \u2014 use this for 'X should link to Y' instead of clicking through"),capture:J.string().optional().describe("kind=differs: name of the captured value on the LEFT of the comparison (omit when comparing the target element's live text)"),urlPattern:J.string().optional().describe("kind=response: regex matched against the response URL \u2014 path-only for portability, never literal volatile IDs (use /api/orders/\\d+)"),method:J.string().optional().describe("kind=response: HTTP method to match (any if omitted)"),status:J.string().optional().describe("kind=response: expected status \u2014 exact ('201') or class ('2xx'); default any 2xx"),bodyPattern:J.string().optional().describe("kind=response: regex the response body must match (verify via browser_network_requests first)"),each:J.string().optional().describe("kind=each: the data-list token exactly as the step writes it, e.g. '{data:enums.OrderStatus}'. The target is the CONTAINER (dropdown, table, list) that must show every value of that list. Never pass the values themselves."),compareTo:J.string().optional().describe("kind=differs: name of the captured value to compare against")},async t=>{let n=Jc(t),r=Wc(n);if(r)return fe(r);let s;switch(t.kind){case"visible":s={action:"expectVisible",target:n};break;case"notVisible":s={action:"expectNotVisible",target:n};break;case"text":s={action:"expectText",target:n,value:t.expected??""};break;case"value":s={action:"expectValue",target:n,value:t.expected??""};break;case"url":s={action:"expectUrl",url:t.url??""};break;case"differs":if(!t.compareTo)return fe("ERROR: kind=differs requires compareTo.");if(!t.capture&&!n)return fe("ERROR: kind=differs needs either capture (a stored name) or a target element.");s={action:"expectDiffers",target:n,value:t.capture,compareTo:t.compareTo};break;case"matches":if(!t.pattern)return fe("ERROR: kind=matches requires pattern.");s={action:"expectMatches",target:n,pattern:t.pattern};break;case"attribute":if(!t.attribute||!t.pattern)return fe("ERROR: kind=attribute requires attribute and pattern.");s={action:"expectAttribute",target:n,attribute:t.attribute,pattern:t.pattern};break;case"each":if(!t.each||!/^\{data:[^}]+\}$/.test(t.each))return fe("ERROR: kind=each requires each='{data:file.list}', the token exactly as the step writes it (not the values).");s={action:"expectEach",target:n,each:t.each};break;case"response":if(!t.urlPattern)return fe("ERROR: kind=response requires urlPattern.");s={action:"expectResponse",urlPattern:t.urlPattern,...t.method?{method:t.method}:{},...t.status?{status:t.status}:{},...t.bodyPattern?{pattern:t.bodyPattern}:{}};break}return!["url","differs","response"].includes(t.kind)&&!n?fe("ERROR: assertion needs a target (role/name, nameRegex, label, text, or selector)."):(e.addActions([s]),fe("Assertion recorded."))}),rn("saffron_done","Finalize the run. Call exactly once, after the final step: success=true only if every step's intent was achieved and every assertion verified.",{success:J.boolean(),narrative:J.string().describe("2-4 sentence summary of the run: what happened, and why it failed if it did. After a heal, say what kind of change the app went through (a locator change, or a page removed, merged, added or reordered) and on which page."),suggestedFeatureEdit:J.string().optional().describe("If you adapted any steps: the corrected Gherkin for those steps, as you would rewrite them in the .feature file (free text, for the human report)."),featureEdits:J.array(J.object({index:J.number().int().min(0).describe("zero-based step index"),text:J.string().describe("replacement step text WITHOUT the Given/When/Then keyword")})).optional().describe("Machine-applicable version of the suggested edits: one entry per adapted step. Provide this whenever you adapted a step.")},async t=>fe(tg(e,t)))]})}function Mr(e={}){let t=e.deliberateSnapshots?`
295
295
  8. Tool responses do NOT include a page snapshot. Take snapshots deliberately with browser_snapshot, and only when you actually need to see the page: after navigation, before locating an element you haven't seen yet, and when verifying an assertion. Do NOT snapshot after actions whose outcome is predictable (filling a field you just located, pressing Enter, a click whose result you will verify later anyway). Every snapshot is expensive.
296
296
  9. Minimize round-trips: batch tool calls in a single response whenever they don't depend on each other's results \u2014 saffron_step together with that step's browser actions, several fills in one turn, bookkeeping calls (saffron_capture, saffron_assert) alongside the actions they belong to. Only stop and wait when you genuinely need a result before deciding your next call. Do not write narration between tool calls.`:"";return`You are Saffron's test execution agent. You execute Gherkin scenarios in a real browser using the playwright MCP tools, while Saffron records your actions into a deterministic replay cache.
297
297
 
@@ -309,9 +309,9 @@ Non-negotiable rules:
309
309
  5a3. DATA LISTS: when a step shows a token followed by '(a data list, currently: \u2026)', e.g. "should list every {data:enums.OrderStatus} (a data list, currently: "New", "Paid")", verify from a snapshot that EVERY listed value is shown inside one container (the dropdown, table, list or menu the step is about), then record ONE assertion: saffron_assert kind=each, each='{data:enums.OrderStatus}' (the token exactly as written, never the values), targeting that container. Do NOT record one assertion per value: replay reads the list from the data file, so values added to it later are checked without recording again. If any listed value is missing, the step FAILS.
310
310
  5c. Real-world page furniture is supported: elements inside IFRAMES record and replay fine (interact normally) \u2014 but when a saffron_assert targets an element INSIDE an iframe, you MUST pass the iframe's CSS selector as the 'frame' argument (e.g. frame='iframe[title="Feedback widget"]'); replay cannot see into frames otherwise. File uploads: use browser_file_upload with the file path. Drag-and-drop: use browser_drag. All of these are recorded and replay deterministically.
311
311
  5d. NATIVE DIALOGS (alert/confirm/prompt): when an action opens one, the page blocks and the snapshot reports a modal state \u2014 resolve it with browser_handle_dialog (accept true/false; promptText for prompt()). That call is recorded automatically as an armed handler replay places BEFORE the triggering action, so do NOT also call saffron_dialog. After handling, verify the post-dialog state as usual. If a step's wording says to confirm/accept, accept; if it says cancel/dismiss, dismiss.
312
- 5e. NETWORK WAITS: when a step says to wait for an API call ("wait for the order API to succeed", "until the job API reports READY"), do it verify-first: perform the trigger, check via browser_network_requests that the matching response actually occurred with the expected status/body, then record it with saffron_wait_api. For Then steps about API contracts ("the request should have returned 201") verify the same way and record with saffron_assert kind=response. urlPattern is a path-only regex \u2014 NEVER a literal volatile ID (/api/orders/\\d+, not /api/orders/8842) and never the host. browser_network_request returns a whole response body, which Saffron cuts at ${Ss.toLocaleString("en-US")} characters: fetch a body only when the step is about its content, and prefer a saffron_assert kind=response on the field you need.
312
+ 5e. NETWORK WAITS: when a step says to wait for an API call ("wait for the order API to succeed", "until the job API reports READY"), do it verify-first: perform the trigger, check via browser_network_requests that the matching response actually occurred with the expected status/body, then record it with saffron_wait_api. For Then steps about API contracts ("the request should have returned 201") verify the same way and record with saffron_assert kind=response. urlPattern is a path-only regex \u2014 NEVER a literal volatile ID (/api/orders/\\d+, not /api/orders/8842) and never the host. browser_network_request returns a whole response body, which Saffron cuts at ${As.toLocaleString("en-US")} characters: fetch a body only when the step is about its content, and prefer a saffron_assert kind=response on the field you need.
313
313
  5f. TABS: a click that opens a new tab needs nothing extra, replay follows the new tab by itself. When a step says to switch to another tab, go back to the first tab, or close a tab, do it with browser_tabs (list first if unsure of the index); select, close and new are recorded and replay in the same order. Steps that assert on the other tab's content are ordinary assertions after the switch.
314
- 7. Never ask the user questions; you are unattended.${t}`}function Uu(e,t){if(e[t].kind!=="assertion")return"action";let n=e.length;for(let r=e.length-1;r>=0&&e[r].kind==="assertion";r--)n=r;return t>=n?"assertion:final":"assertion"}var Bu=`When you heal, tell the reviewer WHAT KIND of change you adapted to, in saffron_step's adaptation and again in saffron_done's narrative, and name the page (its URL path) where it happened. Pick the kind that fits: (1) locator or label change: the same control on the same page, found differently (renamed button, new test id, native select instead of a custom one); (2) page removed or merged: a page no longer exists and its fields or controls now live on another page, so a navigation step (e.g. a 'Continue' click) is no longer needed; (3) page added or flow reordered: a new intermediate page, dialog or step order; (4) timing only: nothing changed, the element was slow. When you skip or add a click, key press or navigation the step used to perform, say so explicitly, e.g. "The guest information page is gone: its fields are now on /checkout, so the 'Continue to payment' click was dropped." Never describe a flow change as a locator change.`;function _r(e){let t=e.liveTab;if(t)return t.count<=1?`Live page: the only open tab, at ${t.url}. Work on it; do not open or select another tab to start over.`:`Live page: tab ${t.index} of ${t.count} open tabs, at ${t.url}. This is where replay left off. If browser_tabs shows another tab as current, select tab ${t.index} before anything else (Saffron does not record that selection); never start over on another tab.`}function Nc(e){let t=e.failedStep?.stepIndex;return`ANNOUNCE FIRST: call saffron_step for ${t===void 0?"the failed step":`the failed step (index ${t})`} BEFORE any state-changing browser action (click, type, select, key press, navigation, tab change). To diagnose, look with browser_snapshot, never with clicks or navigation: Saffron discards every action made before that announcement, and going back through earlier steps changes the state replay reached.`}function Pr(e){let{scenario:t}=e,n=t.steps.map((s,o)=>{let i=f=>pn(Xo(Jt(f,e.uniqueSeed??""))),a=`${o}. [${Uu(t.steps,o)}] ${s.keyword} ${i(s.resolvedText)}`,c=s.resolvedTable??s.table,l=s.resolvedDocString??s.docString,d=a;return c&&(d+=`
314
+ 7. Never ask the user questions; you are unattended.${t}`}function ng(e,t){if(e[t].kind!=="assertion")return"action";let n=e.length;for(let r=e.length-1;r>=0&&e[r].kind==="assertion";r--)n=r;return t>=n?"assertion:final":"assertion"}var rg=`When you heal, tell the reviewer WHAT KIND of change you adapted to, in saffron_step's adaptation and again in saffron_done's narrative, and name the page (its URL path) where it happened. Pick the kind that fits: (1) locator or label change: the same control on the same page, found differently (renamed button, new test id, native select instead of a custom one); (2) page removed or merged: a page no longer exists and its fields or controls now live on another page, so a navigation step (e.g. a 'Continue' click) is no longer needed; (3) page added or flow reordered: a new intermediate page, dialog or step order; (4) timing only: nothing changed, the element was slow. When you skip or add a click, key press or navigation the step used to perform, say so explicitly, e.g. "The guest information page is gone: its fields are now on /checkout, so the 'Continue to payment' click was dropped." Never describe a flow change as a locator change.`;function Fr(e){let t=e.liveTab;if(t)return t.count<=1?`Live page: the only open tab, at ${t.url}. Work on it; do not open or select another tab to start over.`:`Live page: tab ${t.index} of ${t.count} open tabs, at ${t.url}. This is where replay left off. If browser_tabs shows another tab as current, select tab ${t.index} before anything else (Saffron does not record that selection); never start over on another tab.`}function Kc(e){let t=e.failedStep?.stepIndex;return`ANNOUNCE FIRST: call saffron_step for ${t===void 0?"the failed step":`the failed step (index ${t})`} BEFORE any state-changing browser action (click, type, select, key press, navigation, tab change). To diagnose, look with browser_snapshot, never with clicks or navigation: Saffron discards every action made before that announcement, and going back through earlier steps changes the state replay reached.`}function Dr(e){let{scenario:t}=e,n=t.steps.map((s,o)=>{let i=f=>hn(pi(Vt(f,e.uniqueSeed??""))),a=`${o}. [${ng(t.steps,o)}] ${s.keyword} ${i(s.resolvedText)}`,c=s.resolvedTable??s.table,l=s.resolvedDocString??s.docString,d=a;return c&&(d+=`
315
315
  ${c.map(f=>` | ${f.map(i).join(" | ")} |`).join(`
316
316
  `)}`),l!==void 0&&(d+=`
317
317
  """
@@ -319,28 +319,28 @@ ${i(l).split(`
319
319
  `).map(f=>` ${f}`).join(`
320
320
  `)}
321
321
  """`),d}).join(`
322
- `),r=[`Feature: ${t.featureName} (${t.featurePath})`,`Scenario: ${t.displayName}`,e.baseURL?`Base URL: ${e.baseURL}`:void 0,"","Steps (index. [kind] keyword text):",n,""];if(e.mode==="refine")r.push("MODE: REFINE. You (or a previous agent session) just recorded this scenario and it completed successfully \u2014 but the deterministic zero-AI replay of that recording FAILED at the step below. The application is working; the RECORDING is not replayable.","The browser tab shows the state where the proof replay stopped.",_r(e),"",`Steps that replayed fine:
322
+ `),r=[`Feature: ${t.featureName} (${t.featurePath})`,`Scenario: ${t.displayName}`,e.baseURL?`Base URL: ${e.baseURL}`:void 0,"","Steps (index. [kind] keyword text):",n,""];if(e.mode==="refine")r.push("MODE: REFINE. You (or a previous agent session) just recorded this scenario and it completed successfully \u2014 but the deterministic zero-AI replay of that recording FAILED at the step below. The application is working; the RECORDING is not replayable.","The browser tab shows the state where the proof replay stopped.",Fr(e),"",`Steps that replayed fine:
323
323
  ${(e.completedSteps??[]).map(s=>` \u2713 ${s}`).join(`
324
- `)||" (none)"}`,"",`Failed step: [${e.failedStep?.kind}] ${e.failedStep?.text}`,`Replay failure: ${e.failedStep?.error}`,"","Typical causes: a locator that only matched during your session (volatile accessible name \u2014 use nameRegex or a shorter stable name), a literal dynamic value baked into an assertion (use kind=matches/differs/captureText), or an assertion that cannot be checked without navigation (use kind=attribute for link targets).","Re-perform and re-record the failed step and every step after it in a REPLAYABLE form via saffron_step + browser tools / saffron_capture / saffron_assert. Express the SAME conditions the steps state \u2014 do not weaken them.","If a step genuinely cannot be expressed replayably, call saffron_done success=false and explain why.",Nc(e));else if(e.mode==="heal")r.push("MODE: HEAL. A cached deterministic replay of this scenario failed mid-run.","The browser tab already holds the page state reached so far \u2014 do NOT restart from step 0; verify where you are with a snapshot and continue from the failed step.",_r(e),"",`Steps already completed by replay:
324
+ `)||" (none)"}`,"",`Failed step: [${e.failedStep?.kind}] ${e.failedStep?.text}`,`Replay failure: ${e.failedStep?.error}`,"","Typical causes: a locator that only matched during your session (volatile accessible name \u2014 use nameRegex or a shorter stable name), a literal dynamic value baked into an assertion (use kind=matches/differs/captureText), or an assertion that cannot be checked without navigation (use kind=attribute for link targets).","Re-perform and re-record the failed step and every step after it in a REPLAYABLE form via saffron_step + browser tools / saffron_capture / saffron_assert. Express the SAME conditions the steps state \u2014 do not weaken them.","If a step genuinely cannot be expressed replayably, call saffron_done success=false and explain why.",Kc(e));else if(e.mode==="heal")r.push("MODE: HEAL. A cached deterministic replay of this scenario failed mid-run.","The browser tab already holds the page state reached so far \u2014 do NOT restart from step 0; verify where you are with a snapshot and continue from the failed step.",Fr(e),"",`Steps already completed by replay:
325
325
  ${(e.completedSteps??[]).map(s=>` \u2713 ${s}`).join(`
326
- `)||" (none)"}`,"",`Failed step: [${e.failedStep?.kind}] ${e.failedStep?.text}`,`Failure: ${e.failedStep?.error}`,e.failedStep?.pageUrl?`Page when replay failed: ${e.failedStep.pageUrl}`:void 0,"","First diagnose: did the UI legitimately change (heal: adapt the actions, record via saffron_step + browser tools), or is the application broken (fail: saffron_done success=false with your diagnosis)?",Bu,e.assertionPolicy==="adaptable-mid"?"If the failed step is an [assertion:final]: you may re-verify the SAME condition once against the live page (timing), but if it does not hold AS WRITTEN, the run FAILS \u2014 call saffron_done success=false; never substitute it. If the failed step is a mid-scenario [assertion]: this project's policy allows adapting it \u2014 verify the equivalent condition on the legitimately-changed UI, record it via saffron_assert, and report the deviation via saffron_step's adaptation field plus featureEdits. An application defect still fails.":"If the failed step is an [assertion] or [assertion:final]: you may re-verify the SAME condition once against the live page (it may have been a timing issue). If the condition AS WRITTEN does not hold, the run FAILS \u2014 call saffron_done success=false. Do NOT substitute a different element, text, or URL that 'means the same thing'; suggesting a corrected assertion belongs in suggestedFeatureEdit of your failure report, never in saffron_assert.","Announce (saffron_step) and perform only the failed step and the steps after it. Earlier steps keep their recordings.",Nc(e));else if(e.mode==="record"&&e.recordOnly){let{from:s,to:o}=e.recordOnly;r.push("MODE: RECORD (segment). This scenario is being composed from existing recordings plus your work.",e.completedSteps&&e.completedSteps.length>0?`Already executed against the live browser (do NOT redo):
326
+ `)||" (none)"}`,"",`Failed step: [${e.failedStep?.kind}] ${e.failedStep?.text}`,`Failure: ${e.failedStep?.error}`,e.failedStep?.pageUrl?`Page when replay failed: ${e.failedStep.pageUrl}`:void 0,"","First diagnose: did the UI legitimately change (heal: adapt the actions, record via saffron_step + browser tools), or is the application broken (fail: saffron_done success=false with your diagnosis)?",rg,e.assertionPolicy==="adaptable-mid"?"If the failed step is an [assertion:final]: you may re-verify the SAME condition once against the live page (timing), but if it does not hold AS WRITTEN, the run FAILS \u2014 call saffron_done success=false; never substitute it. If the failed step is a mid-scenario [assertion]: this project's policy allows adapting it \u2014 verify the equivalent condition on the legitimately-changed UI, record it via saffron_assert, and report the deviation via saffron_step's adaptation field plus featureEdits. An application defect still fails.":"If the failed step is an [assertion] or [assertion:final]: you may re-verify the SAME condition once against the live page (it may have been a timing issue). If the condition AS WRITTEN does not hold, the run FAILS \u2014 call saffron_done success=false. Do NOT substitute a different element, text, or URL that 'means the same thing'; suggesting a corrected assertion belongs in suggestedFeatureEdit of your failure report, never in saffron_assert.","Announce (saffron_step) and perform only the failed step and the steps after it. Earlier steps keep their recordings.",Kc(e));else if(e.mode==="record"&&e.recordOnly){let{from:s,to:o}=e.recordOnly;r.push("MODE: RECORD (segment). This scenario is being composed from existing recordings plus your work.",e.completedSteps&&e.completedSteps.length>0?`Already executed against the live browser (do NOT redo):
327
327
  ${e.completedSteps.map(i=>` \u2713 ${i}`).join(`
328
- `)}`:"You are starting from a blank page.",_r(e),"",`Record ONLY steps ${s} through ${o} (inclusive).`,`CRITICAL STOP RULE: the steps AFTER step ${o} already have recordings and will be replayed automatically the moment you stop. After completing step ${o}, immediately call saffron_done \u2014 do NOT perform, verify, or navigate into any later step. Executing a later step yourself would make the automatic replay run it TWICE.`)}else if(e.completedSteps&&e.completedSteps.length>0){let s=e.completedSteps.length;r.push("MODE: RECORD (seeded). No cache exists for this scenario yet, but its first steps matched existing recordings and were ALREADY EXECUTED against the live browser by zero-AI replay:",e.completedSteps.map(o=>` \u2713 ${o}`).join(`
329
- `),"",`The browser currently shows the state after those ${s} step(s) (verify with a snapshot if unsure). Do NOT redo them and do NOT navigate away from the current state.`,_r(e),`Start with saffron_step for step index ${s} and record from there to the end.`)}else r.push("MODE: RECORD. No cache exists for this scenario yet.","Execute every step in order, starting at step 0. Navigate to the base URL first if the scenario implies a starting page.");return r.filter(s=>s!==void 0).join(`
330
- `)}import{query as zu}from"@anthropic-ai/claude-agent-sdk";var qu="usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET";async function So(e,t=8e3){let n=e?.[qu];if(typeof n!="function")return;let r;try{let s=await Promise.race([n.call(e),new Promise(o=>{r=setTimeout(()=>o(void 0),t),r.unref?.()})]);return Hu(s)}catch{return}finally{r&&clearTimeout(r)}}function Hu(e){if(!e||!e.rate_limits_available||!e.subscription_type)return;let t=e.rate_limits?.five_hour;return{subscriptionType:e.subscription_type,utilization:typeof t?.utilization=="number"?t.utilization:void 0,resetsAt:t?.resets_at??void 0}}async function Fc(){let e=()=>{},t=new Promise(s=>e=s);async function*n(){await t}let r;try{let s=zu({prompt:n(),options:{maxTurns:1,tools:[],settingSources:[],persistSession:!1}});r=s;let o=(async()=>{try{for await(let a of s);}catch{}})(),i=await So(s);return e(),r.close?.(),await o,i}catch{e(),r?.close?.();return}}import Qu from"node:fs";import Bc from"node:path";import{createRequire as Xu}from"node:module";import{createRequire as Mc}from"node:module";var Ju=Mc(import.meta.url),Cr=Mc(Ju.resolve("playwright/package.json"));import{Client as Gu}from"@modelcontextprotocol/sdk/client/index.js";import{InMemoryTransport as Wu}from"@modelcontextprotocol/sdk/inMemory.js";var Vu=1e4,Ku=5e3,Dc="saffron_connection_check",Or=e=>new Promise(t=>{setTimeout(t,e).unref()}),On=new Set,xo=0,Lc=e=>{xo===0&&[...On].at(-1)?.(e)};function Yu(e){let t=n=>e(n);return On.size===0&&process.on("unhandledRejection",Lc),On.add(t),()=>{On.delete(t)&&On.size===0&&process.off("unhandledRejection",Lc)}}async function vo(e,t,n,r){await e.tracing.group(`[saffron:agent:${t.index}] ${t.title}`).catch(()=>{});try{await e.tracing.group(n).catch(()=>{});try{return await r()}finally{await e.tracing.groupEnd().catch(()=>{})}}finally{await e.tracing.groupEnd().catch(()=>{})}}function Zu(e,t={}){let n=o=>typeof o=="string"&&o.trim()?o.trim():void 0,r=n(t.element)??"an element",s=(()=>{switch(e){case"browser_snapshot":return"read the page";case"browser_find":return n(t.text)?`find "${n(t.text)}" on the page`:n(t.regex)?`find ${n(t.regex)} on the page`:"search the page";case"browser_click":return`${t.doubleClick?"double-click":"click"} ${r}`;case"browser_hover":return`hover ${r}`;case"browser_type":return`type into ${r}`;case"browser_fill_form":return"fill in the form";case"browser_select_option":return`select in ${r}`;case"browser_press_key":return n(t.key)?`press ${n(t.key)}`:"press a key";case"browser_navigate":return n(t.url)?`go to ${n(t.url)}`:"navigate";case"browser_navigate_back":return"go back";case"browser_wait_for":return n(t.text)?`wait for "${n(t.text)}"`:n(t.textGone)?`wait for "${n(t.textGone)}" to go`:`wait ${t.time??""}s`.replace(" s","");case"browser_tabs":return n(t.action)?`tabs: ${n(t.action)}`:"list the tabs";case"browser_drag":return`drag ${n(t.startElement)??"an element"} to ${n(t.endElement)??"an element"}`;case"browser_file_upload":return"upload files";case"browser_handle_dialog":return`${t.accept?"accept":"dismiss"} the dialog`;case"browser_evaluate":case"browser_run_code":case"browser_run_code_unsafe":return"run a script in the page";case"browser_take_screenshot":return"take a screenshot";case"browser_console_messages":return"read the console";case"browser_network_requests":return"read the network log";default:return}})();return s?`${s} \xB7 ${e}`:e}async function Uc(e,t,n="none",r=()=>{},s={}){let{createConnection:o,resolveCLIConfigForMCP:i}=Cr("playwright-core/lib/coreBundle").tools,a=await i?.({},process.env)??{},c={...a,browser:{...a.browser,isolated:!1},sharedBrowserContext:!0,imageResponses:"omit",...s.outputDir?{outputDir:s.outputDir}:{},snapshot:{...a.snapshot,mode:n}},l=e.browser(),d=new Set(l?.listeners("disconnected")??[]),f=new Set,m=u=>{f.add(u)},p=u=>{u.on("download",m)};for(let u of e.pages())p(u);e.on("page",p);let b=Yu(r),v=u=>{e.off("page",p);for(let I of e.pages())I.off("download",m);for(let I of l?.listeners("disconnected")??[])d.has(I)||l.off("disconnected",I);if(!u)return b();let g=()=>{clearTimeout(E),e.off("close",R),b()},R=()=>{setTimeout(g,1e3).unref()},E=setTimeout(g,3e5);E.unref(),e.on("close",R)},k,w=new Gu({name:"saffron-trace",version:"1"});try{k=await o(c,async()=>e);let[u,g]=Wu.createLinkedPair();await k.connect(g),await w.connect(u);let E=((await w.callTool({name:Dc,arguments:{}})).content??[]).map(I=>I.text??"").join(`
331
- `);if(!E.includes(Dc))throw new Error(E.replace(/^### Error\s*/,"").trim()||"the browser backend did not start")}catch(u){throw await Promise.allSettled([w.close(),k?.close()]),v(!1),u}let x=Promise.resolve(),y=(u,g,R=t())=>{let E=x.then(()=>vo(e,R,u,g));return x=E.catch(()=>{}),E},h=$s(w,(u,g,R)=>y(Zu(u,g),R),s.projectRoot),S=async()=>{let u=[...f];if(u.length===0)return;let g=Promise.all(u.map(E=>E.path().then(()=>!0,()=>!0))),R=!await Promise.race([g,Or(Ku).then(()=>!1)]);R&&xo++;try{R&&(await Promise.allSettled(u.map(E=>E.cancel())),await Promise.allSettled(u.map(E=>E.failure()))),await Or(0),await Or(0)}finally{R&&xo--}};return{config:{type:"sdk",name:"playwright",instance:h},close:async()=>{await Promise.race([x,Or(Vu)]),await S().catch(()=>{}),await Promise.allSettled([w.close()]),await Promise.allSettled([k.close()]),await Promise.allSettled([h.close()]),v(!0)},grouped:(u,g)=>y(u,g)}}var eg=Xu(import.meta.url);function tg(){let e=eg.resolve("@playwright/mcp/package.json");return Bc.join(Bc.dirname(e),"cli.js")}var ng=new Set(["mcp__playwright__browser_navigate","mcp__playwright__browser_click","mcp__playwright__browser_type","mcp__playwright__browser_fill_form","mcp__playwright__browser_select_option","mcp__playwright__browser_press_key"]),rg=new Set(["mcp__playwright__browser_snapshot","mcp__playwright__browser_find","mcp__playwright__browser_wait_for","mcp__playwright__browser_take_screenshot","mcp__playwright__browser_console_messages","mcp__playwright__browser_network_requests","mcp__playwright__browser_network_request","mcp__saffron__saffron_step","mcp__saffron__saffron_capture","mcp__saffron__saffron_dialog","mcp__saffron__saffron_wait_api","mcp__saffron__saffron_assert","mcp__saffron__saffron_done"]);function $o(e,t){if(rg.has(e))return!1;if(e==="mcp__playwright__browser_tabs"){let n=t?.action;return!(n==="list"||n==="select"||n==="close")}return!0}var en=class e{constructor(t,n){this.ctx=t;this.snapshotMode=n;this.state=new Tr(t.scenario,{firstStep:pc(t),discardBeforeAnnounce:dc(t)})}ctx;snapshotMode;state;usage={aiCalls:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreationTokens:0};resultText="";traced;capped;outputDir=Ci();get projectRoot(){return this.ctx.projectRoot??process.cwd()}stopTabWatch;static async open(t,n,r){let s=new e(t,n);if(t.traceContext&&!r.traceBridgeUnavailable){let o=Ye(t.scenario);try{s.traced=await Uc(t.traceContext,()=>s.currentStep(),n,i=>{let a=i instanceof Error?i.message:String(i);console.warn(`A promise rejected during the agent session and nothing handled it: ${Ze(a,o)}`)},{outputDir:s.outputDir,projectRoot:s.projectRoot})}catch(i){r.traceBridgeUnavailable=!0;let a=Ze(i instanceof Error?i.message:String(i),o);console.warn(`Agent tracing could not start (${a}). Continuing through the standard browser connection; agent calls will be absent from these traces.`)}}return s}currentStep(){let t=this.state.currentStepIndex,n=this.ctx.scenario.steps[t];return{index:t,title:n?`${n.keyword} ${n.text}`:"Agent exploration"}}async playwrightServer(){return this.traced?this.traced.config:(this.capped??=await ji(this.playwrightStdio(),this.projectRoot),this.capped.config)}playwrightStdio(){return{command:process.execPath,args:[tg(),"--cdp-endpoint",this.ctx.cdpEndpoint,"--snapshot-mode",this.snapshotMode,"--image-responses","omit","--output-dir",this.outputDir]}}noteThePage(t){let n="note the page for the recording";return this.traced?this.traced.grouped(n,t):this.ctx.traceContext?vo(this.ctx.traceContext,this.currentStep(),n,t):t()}async beforeTool(t,n){if(this.state.beginToolCall(t),ng.has(n)&&this.ctx.takeFingerprint&&!this.state.pendingFingerprint)try{this.state.pendingFingerprint=await this.noteThePage(this.ctx.takeFingerprint)}catch{}}afterTool(t,n,r,s){let o=vc(n,r);o&&this.state.addArmedAction(o);let i=xc(n,r,this.ctx.baseURL);if(i&&this.state.addActions([i]),n.startsWith("mcp__playwright__")){let a=fc(s);a!==void 0&&this.ctx.onActiveTab?.(a),this.state.addActions(uc(s,this.ctx.baseURL)),this.state.pendingFingerprint=void 0}this.state.endToolCall(t,$o(n,r))}failedTool(t,n,r){n.startsWith("mcp__playwright__")&&(this.state.pendingFingerprint=void 0),this.state.endToolCall(t,$o(n,r))}watchTabs(){this.stopTabWatch=this.ctx.onTabOpened?.(()=>this.state.markTabOpened())}sessionError(t){if(this.state.done)return;let n=t instanceof Error?t.message:String(t);this.state.done=!0,this.state.success=!1,this.state.narrative=`Agent session error: ${n}`}async close(){this.stopTabWatch?.(),this.stopTabWatch=void 0,await this.traced?.close(),await this.capped?.close(),Qu.rmSync(this.outputDir,{recursive:!0,force:!0})}result(){let{ctx:t,state:n}=this;return n.done||(n.success=!1,n.narrative=n.narrative||`Agent session ended without finalizing the run. Last output: ${this.resultText.slice(0,500)}`),{success:n.success,narrative:n.narrative,adaptations:[...n.adaptations,...n.attributionNotes()],suggestedFeatureEdit:n.suggestedFeatureEdit,featureEdits:n.featureEdits.map(r=>({index:r.index,text:Rt(r.text,t.scenario.params)})),recordedSteps:bc(wc(Sc(yc(hc(mc(n.steps,t.scenario,r=>dn(Jt(r,t.uniqueSeed??""))),t.scenario.params)),Jn(le(t.scenario))),Gt(le(t.scenario),t.uniqueSeed??"")),Ye(t.scenario),le(t.scenario)),announcedSteps:[...n.announced].sort((r,s)=>r-s),usage:this.usage}}};var jr=class{constructor(t={}){this.opts=t}opts;planBefore;traceBridgeUnavailable=!1;async planUsage(){if(!this.planBefore)return;let t=await this.planBefore;if(!t)return;let n=await Fc();return{subscriptionType:t.subscriptionType,fiveHourBefore:t.utilization,fiveHourAfter:n?.utilization,resetsAt:n?.resetsAt??t.resetsAt}}async run(t){let n=await en.open(t,this.opts.snapshotMode??"none",this),r;try{r=sg({prompt:Pr(t),options:{systemPrompt:Ir({deliberateSnapshots:n.snapshotMode==="none",adaptableMidAssertions:t.assertionPolicy==="adaptable-mid"}),model:t.model??this.opts.model,maxTurns:this.opts.maxTurns??100,tools:[],permissionMode:"bypassPermissions",allowDangerouslySkipPermissions:!0,persistSession:!1,settingSources:[],strictMcpConfig:!0,settings:{disableClaudeAiConnectors:!0},mcpServers:{playwright:await n.playwrightServer(),saffron:Ar(n.state)},hooks:{PreToolUse:[{hooks:[async s=>{let o=s;return await n.beforeTool(o.tool_use_id,o.tool_name),{continue:!0}}]}],PostToolUse:[{hooks:[async s=>{let o=s;return n.afterTool(o.tool_use_id,o.tool_name,o.tool_input,o.tool_response),{continue:!0}}]}],PostToolUseFailure:[{hooks:[async s=>{let o=s;return n.failedTool(o.tool_use_id,o.tool_name,o.tool_input),{continue:!0}}]}]}}})}catch(s){throw await n.close(),s}this.planBefore||(this.planBefore=So(r)),n.watchTabs();try{for await(let s of r)s.type==="result"&&(n.usage.aiCalls=s.num_turns,n.usage.costUsd=s.total_cost_usd,"modelUsage"in s&&s.modelUsage&&(n.usage.models=Object.keys(s.modelUsage)),"usage"in s&&s.usage&&(n.usage.inputTokens=s.usage.input_tokens??0,n.usage.outputTokens=s.usage.output_tokens??0,n.usage.cacheReadTokens=s.usage.cache_read_input_tokens??0,n.usage.cacheCreationTokens=s.usage.cache_creation_input_tokens??0),s.subtype==="success"&&(n.resultText=s.result))}catch(s){n.sessionError(s)}finally{await n.close()}return n.result()}};import og from"node:http";import{randomBytes as ig,randomUUID as ag,timingSafeEqual as cg}from"node:crypto";import{Client as lg}from"@modelcontextprotocol/sdk/client/index.js";import{InMemoryTransport as dg}from"@modelcontextprotocol/sdk/inMemory.js";import{Server as pg}from"@modelcontextprotocol/sdk/server/index.js";import{StreamableHTTPServerTransport as fg}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{CallToolRequestSchema as ug,ListToolsRequestSchema as gg}from"@modelcontextprotocol/sdk/types.js";async function hg(e,t){let n=ig(16).toString("hex"),r=Buffer.from(`Bearer ${n}`),s=c=>{let l=Buffer.from(c??"");return l.length===r.length&&cg(l,r)},o=(c,l)=>{let d=new pg({name:c,version:"1"},{capabilities:{tools:{}}});return d.setRequestHandler(gg,f=>l.listTools(f.params)),d.setRequestHandler(ug,async f=>{let m=ag(),p=`mcp__${c}__${f.params.name}`,b=f.params.arguments??{};await t.beforeTool(m,p);try{let v=await l.callTool(f.params,void 0,{timeout:xs});return v.isError?t.failedTool(m,p,b):t.afterTool(m,p,b,v),v}catch(v){throw t.failedTool(m,p,b),v}}),d},i=og.createServer(async(c,l)=>{let d=(c.url??"").split("?")[0].slice(1),f=s(c.headers.authorization)?e[d]:void 0;if(!f)return void l.writeHead(404).end();if(c.method!=="POST")return void l.writeHead(405,{Allow:"POST"}).end();let m=o(d,f),p=new fg({sessionIdGenerator:void 0,enableJsonResponse:!0});l.on("close",()=>{Promise.allSettled([p.close(),m.close()])});try{await m.connect(p),await p.handleRequest(c,l)}catch{l.headersSent||l.writeHead(500).end()}});i.requestTimeout=0,await new Promise((c,l)=>{i.once("error",l),i.listen(0,"127.0.0.1",()=>c())});let{port:a}=i.address();return{urls:Object.fromEntries(Object.keys(e).map(c=>[c,`http://127.0.0.1:${a}/${c}`])),token:n,close:async()=>{i.closeAllConnections(),await new Promise(c=>i.close(()=>c()))}}}async function zc(e){let[t,n]=dg.createLinkedPair();await e.connect(n);let r=new lg({name:"saffron-gateway",version:"1"});return await r.connect(t),r}async function qc(e){let t=[];try{let n=await zc((await e.playwrightServer()).instance);t.push(n);let r=await zc(Ar(e.state).instance);t.push(r);let s=await hg({playwright:n,saffron:r},e);return{urls:s.urls,token:s.token,close:async()=>{await s.close(),await Promise.allSettled(t.map(o=>o.close()))}}}catch(n){throw await Promise.allSettled(t.map(r=>r.close())),n}}var Nr=class{constructor(t,n={},r=()=>{}){this.drive=t;this.opts=n;this.blocker=r}drive;opts;blocker;traceBridgeUnavailable=!1;async run(t){let n=this.blocker();if(n)throw new Error(n);let r=await en.open(t,this.opts.snapshotMode??"none",this),s;try{s=await qc(r)}catch(o){throw await r.close(),o}r.watchTabs();try{await this.drive({systemPrompt:Ir({deliberateSnapshots:r.snapshotMode==="none",adaptableMidAssertions:t.assertionPolicy==="adaptable-mid"}),prompt:Pr(t),model:t.model??this.opts.model,maxTurns:this.opts.maxTurns??100,mcpServers:s.urls,mcpToken:s.token,usage:r.usage,lastOutput:o=>{r.resultText=o},finished:()=>r.state.done})}catch(o){r.sessionError(o)}finally{await s.close(),await r.close()}return r.result()}};async function jn(e,t){try{return await import(e)}catch(n){let r=n.code;throw(r==="ERR_MODULE_NOT_FOUND"||r==="MODULE_NOT_FOUND")&&String(n.message).includes(`'${e}'`)?new Error(`the ${t} provider needs ${e}: npm install --save-dev ${e}`):n}}function tn(e){return`stopped after ${e} tool calls, the session's turn limit: raise maxTurns in saffron.config.json, or look at what the agent kept retrying`}import nn from"node:fs";import ko from"node:os";import Ve from"node:path";import{createHash as mg}from"node:crypto";import{createRequire as Hc}from"node:module";function yg(){try{let e=Hc(import.meta.resolve("@openai/codex-sdk")).resolve("@openai/codex/package.json"),t=Hc(e).resolve(`@openai/codex-${process.platform}-${process.arch}/package.json`),n=Ve.join(Ve.dirname(t),"vendor");for(let r of nn.readdirSync(n))for(let s of[Ve.join(n,r,"bin","codex"),Ve.join(n,r,"codex","codex")])if(nn.existsSync(s))return s}catch{}}function Fr(e=process.platform){return e==="win32"?"the codex provider is not available on Windows yet: Saffron must run Codex without your ~/.codex/config.toml (your own MCP servers, plugins and hooks), and cannot on Windows":void 0}function bg(e){return`'${e.replaceAll("'","'\\''")}'`}function wg(){let e=Fr();if(e)throw new Error(e);let t=yg();if(!t)throw new Error("the Codex binary is missing from @openai/codex-sdk's install: reinstall it with npm install --save-dev @openai/codex-sdk");let n=Ve.join(process.env.XDG_CACHE_HOME||Ve.join(ko.homedir(),".cache"),"saffron");nn.mkdirSync(n,{recursive:!0});let r=Ve.join(n,`codex-${mg("sha256").update(t).digest("hex").slice(0,12)}`);return Ee(r,`#!/bin/sh
328
+ `)}`:"You are starting from a blank page.",Fr(e),"",`Record ONLY steps ${s} through ${o} (inclusive).`,`CRITICAL STOP RULE: the steps AFTER step ${o} already have recordings and will be replayed automatically the moment you stop. After completing step ${o}, immediately call saffron_done \u2014 do NOT perform, verify, or navigate into any later step. Executing a later step yourself would make the automatic replay run it TWICE.`)}else if(e.completedSteps&&e.completedSteps.length>0){let s=e.completedSteps.length;r.push("MODE: RECORD (seeded). No cache exists for this scenario yet, but its first steps matched existing recordings and were ALREADY EXECUTED against the live browser by zero-AI replay:",e.completedSteps.map(o=>` \u2713 ${o}`).join(`
329
+ `),"",`The browser currently shows the state after those ${s} step(s) (verify with a snapshot if unsure). Do NOT redo them and do NOT navigate away from the current state.`,Fr(e),`Start with saffron_step for step index ${s} and record from there to the end.`)}else r.push("MODE: RECORD. No cache exists for this scenario yet.","Execute every step in order, starting at step 0. Navigate to the base URL first if the scenario implies a starting page.");return r.filter(s=>s!==void 0).join(`
330
+ `)}import{query as sg}from"@anthropic-ai/claude-agent-sdk";var og="usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET";async function Po(e,t=8e3){let n=e?.[og];if(typeof n!="function")return;let r;try{let s=await Promise.race([n.call(e),new Promise(o=>{r=setTimeout(()=>o(void 0),t),r.unref?.()})]);return ig(s)}catch{return}finally{r&&clearTimeout(r)}}function ig(e){if(!e||!e.rate_limits_available||!e.subscription_type)return;let t=e.rate_limits?.five_hour;return{subscriptionType:e.subscription_type,utilization:typeof t?.utilization=="number"?t.utilization:void 0,resetsAt:t?.resets_at??void 0}}async function Yc(){let e=()=>{},t=new Promise(s=>e=s);async function*n(){await t}let r;try{let s=sg({prompt:n(),options:{maxTurns:1,tools:[],settingSources:[],persistSession:!1}});r=s;let o=(async()=>{try{for await(let a of s);}catch{}})(),i=await Po(s);return e(),r.close?.(),await o,i}catch{e(),r?.close?.();return}}import gg from"node:fs";import tl from"node:path";import{createRequire as hg}from"node:module";import{createRequire as Qc}from"node:module";var ag=Qc(import.meta.url),Lr=Qc(ag.resolve("playwright/package.json"));import{Client as cg}from"@modelcontextprotocol/sdk/client/index.js";import{InMemoryTransport as lg}from"@modelcontextprotocol/sdk/inMemory.js";var dg=1e4,pg=5e3,Zc="saffron_connection_check",Ur=e=>new Promise(t=>{setTimeout(t,e).unref()}),Ln=new Set,Co=0,Xc=e=>{Co===0&&[...Ln].at(-1)?.(e)};function fg(e){let t=n=>e(n);return Ln.size===0&&process.on("unhandledRejection",Xc),Ln.add(t),()=>{Ln.delete(t)&&Ln.size===0&&process.off("unhandledRejection",Xc)}}async function Oo(e,t,n,r){await e.tracing.group(`[saffron:agent:${t.index}] ${t.title}`).catch(()=>{});try{await e.tracing.group(n).catch(()=>{});try{return await r()}finally{await e.tracing.groupEnd().catch(()=>{})}}finally{await e.tracing.groupEnd().catch(()=>{})}}function ug(e,t={}){let n=o=>typeof o=="string"&&o.trim()?o.trim():void 0,r=n(t.element)??"an element",s=(()=>{switch(e){case"browser_snapshot":return"read the page";case"browser_find":return n(t.text)?`find "${n(t.text)}" on the page`:n(t.regex)?`find ${n(t.regex)} on the page`:"search the page";case"browser_click":return`${t.doubleClick?"double-click":"click"} ${r}`;case"browser_hover":return`hover ${r}`;case"browser_type":return`type into ${r}`;case"browser_fill_form":return"fill in the form";case"browser_select_option":return`select in ${r}`;case"browser_press_key":return n(t.key)?`press ${n(t.key)}`:"press a key";case"browser_navigate":return n(t.url)?`go to ${n(t.url)}`:"navigate";case"browser_navigate_back":return"go back";case"browser_wait_for":return n(t.text)?`wait for "${n(t.text)}"`:n(t.textGone)?`wait for "${n(t.textGone)}" to go`:`wait ${t.time??""}s`.replace(" s","");case"browser_tabs":return n(t.action)?`tabs: ${n(t.action)}`:"list the tabs";case"browser_drag":return`drag ${n(t.startElement)??"an element"} to ${n(t.endElement)??"an element"}`;case"browser_file_upload":return"upload files";case"browser_handle_dialog":return`${t.accept?"accept":"dismiss"} the dialog`;case"browser_evaluate":case"browser_run_code":case"browser_run_code_unsafe":return"run a script in the page";case"browser_take_screenshot":return"take a screenshot";case"browser_console_messages":return"read the console";case"browser_network_requests":return"read the network log";default:return}})();return s?`${s} \xB7 ${e}`:e}async function el(e,t,n="none",r=()=>{},s={}){let{createConnection:o,resolveCLIConfigForMCP:i}=Lr("playwright-core/lib/coreBundle").tools,a=await i?.({},process.env)??{},c={...a,browser:{...a.browser,isolated:!1},sharedBrowserContext:!0,imageResponses:"omit",...s.outputDir?{outputDir:s.outputDir}:{},snapshot:{...a.snapshot,mode:n}},l=e.browser(),d=new Set(l?.listeners("disconnected")??[]),f=new Set,m=h=>{f.add(h)},p=h=>{h.on("download",m)};for(let h of e.pages())p(h);e.on("page",p);let b=fg(r),$=h=>{e.off("page",p);for(let _ of e.pages())_.off("download",m);for(let _ of l?.listeners("disconnected")??[])d.has(_)||l.off("disconnected",_);if(!h)return b();let g=()=>{clearTimeout(E),e.off("close",T),b()},T=()=>{setTimeout(g,1e3).unref()},E=setTimeout(g,3e5);E.unref(),e.on("close",T)},k,w=new cg({name:"saffron-trace",version:"1"});try{k=await o(c,async()=>e);let[h,g]=lg.createLinkedPair();await k.connect(g),await w.connect(h);let E=((await w.callTool({name:Zc,arguments:{}})).content??[]).map(_=>_.text??"").join(`
331
+ `);if(!E.includes(Zc))throw new Error(E.replace(/^### Error\s*/,"").trim()||"the browser backend did not start")}catch(h){throw await Promise.allSettled([w.close(),k?.close()]),$(!1),h}let x=Promise.resolve(),y=(h,g,T=t())=>{let E=x.then(()=>Oo(e,T,h,g));return x=E.catch(()=>{}),E},u=Ps(w,(h,g,T)=>y(ug(h,g),T),s.projectRoot),v=async()=>{let h=[...f];if(h.length===0)return;let g=Promise.all(h.map(E=>E.path().then(()=>!0,()=>!0))),T=!await Promise.race([g,Ur(pg).then(()=>!1)]);T&&Co++;try{T&&(await Promise.allSettled(h.map(E=>E.cancel())),await Promise.allSettled(h.map(E=>E.failure()))),await Ur(0),await Ur(0)}finally{T&&Co--}};return{config:{type:"sdk",name:"playwright",instance:u},close:async()=>{await Promise.race([x,Ur(dg)]),await v().catch(()=>{}),await Promise.allSettled([w.close()]),await Promise.allSettled([k.close()]),await Promise.allSettled([u.close()]),$(!0)},grouped:(h,g)=>y(h,g)}}var mg=hg(import.meta.url);function yg(){let e=mg.resolve("@playwright/mcp/package.json");return tl.join(tl.dirname(e),"cli.js")}var bg=new Set(["mcp__playwright__browser_navigate","mcp__playwright__browser_click","mcp__playwright__browser_type","mcp__playwright__browser_fill_form","mcp__playwright__browser_select_option","mcp__playwright__browser_press_key"]),wg=new Set(["mcp__playwright__browser_snapshot","mcp__playwright__browser_find","mcp__playwright__browser_wait_for","mcp__playwright__browser_take_screenshot","mcp__playwright__browser_console_messages","mcp__playwright__browser_network_requests","mcp__playwright__browser_network_request","mcp__saffron__saffron_step","mcp__saffron__saffron_capture","mcp__saffron__saffron_dialog","mcp__saffron__saffron_wait_api","mcp__saffron__saffron_assert","mcp__saffron__saffron_done"]);function jo(e,t){if(wg.has(e))return!1;if(e==="mcp__playwright__browser_tabs"){let n=t?.action;return!(n==="list"||n==="select"||n==="close")}return!0}var sn=class e{constructor(t,n){this.ctx=t;this.snapshotMode=n;this.state=new Cr(t.scenario,{firstStep:Tc(t),discardBeforeAnnounce:kc(t)})}ctx;snapshotMode;state;usage={aiCalls:0,inputTokens:0,outputTokens:0,cacheReadTokens:0,cacheCreationTokens:0};resultText="";traced;capped;outputDir=qi();get projectRoot(){return this.ctx.projectRoot??process.cwd()}stopTabWatch;static async open(t,n,r){let s=new e(t,n);if(t.traceContext&&!r.traceBridgeUnavailable){let o=Ze(t.scenario);try{s.traced=await el(t.traceContext,()=>s.currentStep(),n,i=>{let a=i instanceof Error?i.message:String(i);console.warn(`A promise rejected during the agent session and nothing handled it: ${Xe(a,o)}`)},{outputDir:s.outputDir,projectRoot:s.projectRoot})}catch(i){r.traceBridgeUnavailable=!0;let a=Xe(i instanceof Error?i.message:String(i),o);console.warn(`Agent tracing could not start (${a}). Continuing through the standard browser connection; agent calls will be absent from these traces.`)}}return s}currentStep(){let t=this.state.currentStepIndex,n=this.ctx.scenario.steps[t];return{index:t,title:n?`${n.keyword} ${n.text}`:"Agent exploration"}}async playwrightServer(){return this.traced?this.traced.config:(this.capped??=await Wi(this.playwrightStdio(),this.projectRoot),this.capped.config)}playwrightStdio(){return{command:process.execPath,args:[yg(),"--cdp-endpoint",this.ctx.cdpEndpoint,"--snapshot-mode",this.snapshotMode,"--image-responses","omit","--output-dir",this.outputDir]}}noteThePage(t){let n="note the page for the recording";return this.traced?this.traced.grouped(n,t):this.ctx.traceContext?Oo(this.ctx.traceContext,this.currentStep(),n,t):t()}async beforeTool(t,n){if(this.state.beginToolCall(t),bg.has(n)&&this.ctx.takeFingerprint&&!this.state.pendingFingerprint)try{this.state.pendingFingerprint=await this.noteThePage(this.ctx.takeFingerprint)}catch{}}afterTool(t,n,r,s){let o=Fc(n,r);o&&this.state.addArmedAction(o);let i=Nc(n,r,this.ctx.baseURL);if(i&&this.state.addActions([i]),n.startsWith("mcp__playwright__")){let a=Rc(s);a!==void 0&&this.ctx.onActiveTab?.(a),this.state.addActions(Ec(s,this.ctx.baseURL)),this.state.pendingFingerprint=void 0}this.state.endToolCall(t,jo(n,r))}failedTool(t,n,r){n.startsWith("mcp__playwright__")&&(this.state.pendingFingerprint=void 0),this.state.endToolCall(t,jo(n,r))}watchTabs(){this.stopTabWatch=this.ctx.onTabOpened?.(()=>this.state.markTabOpened())}sessionError(t){if(this.state.done)return;let n=t instanceof Error?t.message:String(t);this.state.done=!0,this.state.success=!1,this.state.narrative=`Agent session error: ${n}`}async close(){this.stopTabWatch?.(),this.stopTabWatch=void 0,await this.traced?.close(),await this.capped?.close(),gg.rmSync(this.outputDir,{recursive:!0,force:!0})}result(){let{ctx:t,state:n}=this;return n.done||(n.success=!1,n.narrative=n.narrative||`Agent session ended without finalizing the run. Last output: ${this.resultText.slice(0,500)}`),{success:n.success,narrative:n.narrative,adaptations:[...n.adaptations,...n.attributionNotes()],suggestedFeatureEdit:n.suggestedFeatureEdit,featureEdits:n.featureEdits.map(r=>({index:r.index,text:At(r.text,t.scenario.params)})),recordedSteps:Cc(Oc(jc(Pc(_c(Ic(n.steps,t.scenario,r=>gn(Vt(r,t.uniqueSeed??""))),t.scenario.params)),Xn(de(t.scenario))),Kt(de(t.scenario),t.uniqueSeed??"")),Ze(t.scenario),de(t.scenario)),announcedSteps:[...n.announced].sort((r,s)=>r-s),usage:this.usage}}};var Br=class{constructor(t={}){this.opts=t}opts;planBefore;traceBridgeUnavailable=!1;async planUsage(){if(!this.planBefore)return;let t=await this.planBefore;if(!t)return;let n=await Yc();return{subscriptionType:t.subscriptionType,fiveHourBefore:t.utilization,fiveHourAfter:n?.utilization,resetsAt:n?.resetsAt??t.resetsAt}}async run(t){let n=await sn.open(t,this.opts.snapshotMode??"none",this),r;try{r=vg({prompt:Dr(t),options:{systemPrompt:Mr({deliberateSnapshots:n.snapshotMode==="none",adaptableMidAssertions:t.assertionPolicy==="adaptable-mid"}),model:t.model??this.opts.model,maxTurns:this.opts.maxTurns??100,tools:[],permissionMode:"bypassPermissions",allowDangerouslySkipPermissions:!0,persistSession:!1,settingSources:[],strictMcpConfig:!0,settings:{disableClaudeAiConnectors:!0},mcpServers:{playwright:await n.playwrightServer(),saffron:Nr(n.state)},hooks:{PreToolUse:[{hooks:[async s=>{let o=s;return await n.beforeTool(o.tool_use_id,o.tool_name),{continue:!0}}]}],PostToolUse:[{hooks:[async s=>{let o=s;return n.afterTool(o.tool_use_id,o.tool_name,o.tool_input,o.tool_response),{continue:!0}}]}],PostToolUseFailure:[{hooks:[async s=>{let o=s;return n.failedTool(o.tool_use_id,o.tool_name,o.tool_input),{continue:!0}}]}]}}})}catch(s){throw await n.close(),s}this.planBefore||(this.planBefore=Po(r)),n.watchTabs();try{for await(let s of r)s.type==="result"&&(n.usage.aiCalls=s.num_turns,n.usage.costUsd=s.total_cost_usd,"modelUsage"in s&&s.modelUsage&&(n.usage.models=Object.keys(s.modelUsage)),"usage"in s&&s.usage&&(n.usage.inputTokens=s.usage.input_tokens??0,n.usage.outputTokens=s.usage.output_tokens??0,n.usage.cacheReadTokens=s.usage.cache_read_input_tokens??0,n.usage.cacheCreationTokens=s.usage.cache_creation_input_tokens??0),s.subtype==="success"&&(n.resultText=s.result))}catch(s){n.sessionError(s)}finally{await n.close()}return n.result()}};import Sg from"node:http";import{randomBytes as xg,randomUUID as $g,timingSafeEqual as kg}from"node:crypto";import{Client as Tg}from"@modelcontextprotocol/sdk/client/index.js";import{InMemoryTransport as Rg}from"@modelcontextprotocol/sdk/inMemory.js";import{Server as Eg}from"@modelcontextprotocol/sdk/server/index.js";import{StreamableHTTPServerTransport as Ag}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{CallToolRequestSchema as _g,ListToolsRequestSchema as Ig}from"@modelcontextprotocol/sdk/types.js";async function Pg(e,t){let n=xg(16).toString("hex"),r=Buffer.from(`Bearer ${n}`),s=c=>{let l=Buffer.from(c??"");return l.length===r.length&&kg(l,r)},o=(c,l)=>{let d=new Eg({name:c,version:"1"},{capabilities:{tools:{}}});return d.setRequestHandler(Ig,f=>l.listTools(f.params)),d.setRequestHandler(_g,async f=>{let m=$g(),p=`mcp__${c}__${f.params.name}`,b=f.params.arguments??{};await t.beforeTool(m,p);try{let $=await l.callTool(f.params,void 0,{timeout:_s});return $.isError?t.failedTool(m,p,b):t.afterTool(m,p,b,$),$}catch($){throw t.failedTool(m,p,b),$}}),d},i=Sg.createServer(async(c,l)=>{let d=(c.url??"").split("?")[0].slice(1),f=s(c.headers.authorization)?e[d]:void 0;if(!f)return void l.writeHead(404).end();if(c.method!=="POST")return void l.writeHead(405,{Allow:"POST"}).end();let m=o(d,f),p=new Ag({sessionIdGenerator:void 0,enableJsonResponse:!0});l.on("close",()=>{Promise.allSettled([p.close(),m.close()])});try{await m.connect(p),await p.handleRequest(c,l)}catch{l.headersSent||l.writeHead(500).end()}});i.requestTimeout=0,await new Promise((c,l)=>{i.once("error",l),i.listen(0,"127.0.0.1",()=>c())});let{port:a}=i.address();return{urls:Object.fromEntries(Object.keys(e).map(c=>[c,`http://127.0.0.1:${a}/${c}`])),token:n,close:async()=>{i.closeAllConnections(),await new Promise(c=>i.close(()=>c()))}}}async function nl(e){let[t,n]=Rg.createLinkedPair();await e.connect(n);let r=new Tg({name:"saffron-gateway",version:"1"});return await r.connect(t),r}async function rl(e){let t=[];try{let n=await nl((await e.playwrightServer()).instance);t.push(n);let r=await nl(Nr(e.state).instance);t.push(r);let s=await Pg({playwright:n,saffron:r},e);return{urls:s.urls,token:s.token,close:async()=>{await s.close(),await Promise.allSettled(t.map(o=>o.close()))}}}catch(n){throw await Promise.allSettled(t.map(r=>r.close())),n}}var zr=class{constructor(t,n={},r=()=>{}){this.drive=t;this.opts=n;this.blocker=r}drive;opts;blocker;traceBridgeUnavailable=!1;async run(t){let n=this.blocker();if(n)throw new Error(n);let r=await sn.open(t,this.opts.snapshotMode??"none",this),s;try{s=await rl(r)}catch(o){throw await r.close(),o}r.watchTabs();try{await this.drive({systemPrompt:Mr({deliberateSnapshots:r.snapshotMode==="none",adaptableMidAssertions:t.assertionPolicy==="adaptable-mid"}),prompt:Dr(t),model:t.model??this.opts.model,maxTurns:this.opts.maxTurns??100,mcpServers:s.urls,mcpToken:s.token,usage:r.usage,lastOutput:o=>{r.resultText=o},finished:()=>r.state.done})}catch(o){r.sessionError(o)}finally{await s.close(),await r.close()}return r.result()}};async function Un(e,t){try{return await import(e)}catch(n){let r=n.code;throw(r==="ERR_MODULE_NOT_FOUND"||r==="MODULE_NOT_FOUND")&&String(n.message).includes(`'${e}'`)?new Error(`the ${t} provider needs ${e}: npm install --save-dev ${e}`):n}}function on(e){return`stopped after ${e} tool calls, the session's turn limit: raise maxTurns in saffron.config.json, or look at what the agent kept retrying`}import an from"node:fs";import No from"node:os";import Ye from"node:path";import{createHash as Cg}from"node:crypto";import{createRequire as sl}from"node:module";function Og(){try{let e=sl(import.meta.resolve("@openai/codex-sdk")).resolve("@openai/codex/package.json"),t=sl(e).resolve(`@openai/codex-${process.platform}-${process.arch}/package.json`),n=Ye.join(Ye.dirname(t),"vendor");for(let r of an.readdirSync(n))for(let s of[Ye.join(n,r,"bin","codex"),Ye.join(n,r,"codex","codex")])if(an.existsSync(s))return s}catch{}}function Hr(e=process.platform){return e==="win32"?"the codex provider is not available on Windows yet: Saffron must run Codex without your ~/.codex/config.toml (your own MCP servers, plugins and hooks), and cannot on Windows":void 0}function jg(e){return`'${e.replaceAll("'","'\\''")}'`}function Ng(){let e=Hr();if(e)throw new Error(e);let t=Og();if(!t)throw new Error("the Codex binary is missing from @openai/codex-sdk's install: reinstall it with npm install --save-dev @openai/codex-sdk");let n=Ye.join(process.env.XDG_CACHE_HOME||Ye.join(No.homedir(),".cache"),"saffron");an.mkdirSync(n,{recursive:!0});let r=Ye.join(n,`codex-${Cg("sha256").update(t).digest("hex").slice(0,12)}`);return Ae(r,`#!/bin/sh
332
332
  sub="$1"; shift
333
- exec ${bg(t)} "$sub" --ignore-user-config --ephemeral "$@"
334
- `,493),r}var Jc="SAFFRON_MCP_TOKEN",Sg=["cli_auth_credentials_store","forced_login_method","forced_chatgpt_workspace_id"];function xg(e){let t=e.split(/^\s*\[/m)[0],n={};for(let r of Sg){let s=new RegExp(`^\\s*${r}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`,"m").exec(t);s&&(n[r]=s[1]??s[2])}return n}function To(){try{return xg(nn.readFileSync(Ve.join(process.env.CODEX_HOME??Ve.join(ko.homedir(),".codex"),"config.toml"),"utf8"))}catch{return{}}}var Gc=async e=>{let{Codex:t}=await jn("@openai/codex-sdk","codex"),n=nn.mkdtempSync(Ve.join(ko.tmpdir(),"saffron-codex-"));try{let r=Object.fromEntries(Object.entries(process.env).filter(l=>l[1]!==void 0)),o=new t({codexPathOverride:wg(),env:{...r,[Jc]:e.mcpToken},config:{...To(),developer_instructions:e.systemPrompt,include_permissions_instructions:!1,include_apps_instructions:!1,include_collaboration_mode_instructions:!1,include_environment_context:!1,project_doc_max_bytes:0,skills:{include_instructions:!1},mcp_servers:Object.fromEntries(Object.entries(e.mcpServers).map(([l,d])=>[l,{url:d,bearer_token_env_var:Jc,required:!0,default_tools_approval_mode:"approve",startup_timeout_sec:30,tool_timeout_sec:600}])),features:{shell_tool:!1,view_image:!1,apps:!1,plugins:!1,hooks:!1,memories:!1,goals:!1,image_generation:!1,tool_suggest:!1,browser_use:!1,computer_use:!1,unified_exec:!1,multi_agent:!1,code_mode_host:!1,in_app_browser:!1,browser_use_external:!1}}}).startThread({model:e.model,workingDirectory:n,skipGitRepoCheck:!0,sandboxMode:"read-only",approvalPolicy:"never",webSearchMode:"disabled"}),i=new AbortController,a=0,c;try{let{events:l}=await o.runStreamed(e.prompt,{signal:i.signal});for await(let d of l)if(d.type==="item.started"&&d.item.type==="mcp_tool_call")++a>e.maxTurns&&!i.signal.aborted&&(c=tn(e.maxTurns),i.abort());else if(d.type==="item.completed")(d.item.type==="mcp_tool_call"||d.item.type==="agent_message")&&e.usage.aiCalls++,d.item.type==="agent_message"&&e.lastOutput(d.item.text);else if(d.type==="turn.completed"){let f=d.usage;e.usage.inputTokens=f.input_tokens-f.cached_input_tokens,e.usage.cacheReadTokens=f.cached_input_tokens,e.usage.cacheCreationTokens=f.cache_write_input_tokens??0,e.usage.outputTokens=f.output_tokens}else d.type==="turn.failed"&&!i.signal.aborted?c=d.error.message:d.type==="error"&&!i.signal.aborted&&(c??=d.message)}catch(l){if(!i.signal.aborted&&!c)throw l}if(e.usage.models=[`codex/${e.model??"default"}`],c&&!e.finished()){let l=/auth|log ?in|401|unauthori[sz]ed/i.test(c)?". Sign in with your ChatGPT plan: npx @openai/codex login":"";throw new Error(`Codex: ${c}${l}`)}}finally{nn.rmSync(n,{recursive:!0,force:!0})}};import{spawn as vg,spawnSync as $g}from"node:child_process";import ee from"node:fs";import Dr from"node:os";import se from"node:path";import kg from"node:readline";var Mr="install the Antigravity CLI (curl -fsSL https://antigravity.google/cli/install.sh | bash), then run `agy` once to sign in with your Google account";function Lr(){for(let e of[...(process.env.PATH??"").split(se.delimiter),se.join(Dr.homedir(),".local","bin")]){let t=se.join(e,process.platform==="win32"?"agy.exe":"agy");if(e&&ee.existsSync(t))return t}}var rn={playwright:"saffron_browser",saffron:"saffron_tools"},Wc=Object.values(rn).map(e=>`mcp(${e}/*)`),Ro=()=>se.join(Dr.homedir(),".gemini","antigravity-cli","settings.json");function Vc(e){if(!ee.existsSync(e))return{};try{let t=JSON.parse(ee.readFileSync(e,"utf8"));if(t&&typeof t=="object"&&!Array.isArray(t))return t}catch{}throw new Error(`${e} is not a JSON object Saffron can add to: fix it (or ask agy to rewrite it), then run npx saffron login antigravity again`)}function Ur(e=Ro()){let t;try{t=Vc(e).permissions?.allow}catch{return Wc}return Wc.filter(n=>!(Array.isArray(t)&&t.includes(n)))}function Tg(e=Ro()){let t=Vc(e),n=Ur(e);if(n.length===0)return[];let r=t.permissions&&typeof t.permissions=="object"?t.permissions:{},s=Array.isArray(r.allow)?r.allow:[],o={...t,permissions:{...r,allow:[...s,...n]}},i=ee.lstatSync(e,{throwIfNoEntry:!1})?.isSymbolicLink()??!1,a=Rg(e),c=ee.existsSync(a)?ee.statSync(a).mode&4095:void 0;try{Ee(a,JSON.stringify(o,null,2)+`
335
- `,c)}catch(l){let d=l.code;throw!i||!["EACCES","EPERM","EROFS"].includes(d??"")?l:new Error(`${e} links to ${a}, which Saffron cannot write: add ${n.join(" and ")} to permissions.allow there, then run npx saffron login antigravity again`)}return n}function Rg(e){let t=e;for(let n=0;n<40&&ee.lstatSync(t,{throwIfNoEntry:!1})?.isSymbolicLink();n++)t=se.resolve(ee.realpathSync(se.dirname(t)),ee.readlinkSync(t));return ee.existsSync(t)?ee.realpathSync(t):t}async function Kc(){let e=Lr();if(!e)throw new Error(`the antigravity provider needs the agy command: ${Mr}`);let t=$g(e,["models"],{encoding:"utf8",timeout:6e4});if(t.status!==0){let r=`${t.stderr??""}${t.stdout??""}`.trim().split(`
336
- `).at(-1)||(t.error?.message??"no answer");throw new Error(`agy could not list your models (${r}): run agy once and sign in with your Google AI Pro or Ultra account`)}let n=Tg();console.log(n.length>0?`Antigravity: allowed Saffron's tools in ${Ro()}: ${n.join(", ")}. Remove those lines to take it back.`:"Antigravity: Saffron's tools are already allowed.")}var Yc=se.join(Dr.homedir(),".gemini","antigravity-cli","mcp"),Eg=Object.values(rn).map(e=>se.join(Yc,e)),Ag=`const path=require("path");const DIRS=${JSON.stringify(Eg)};const SERVERS=${JSON.stringify(Object.values(rn))};
333
+ exec ${jg(t)} "$sub" --ignore-user-config --ephemeral "$@"
334
+ `,493),r}var ol="SAFFRON_MCP_TOKEN",Fg=["cli_auth_credentials_store","forced_login_method","forced_chatgpt_workspace_id"];function Mg(e){let t=e.split(/^\s*\[/m)[0],n={};for(let r of Fg){let s=new RegExp(`^\\s*${r}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`,"m").exec(t);s&&(n[r]=s[1]??s[2])}return n}function Fo(){try{return Mg(an.readFileSync(Ye.join(process.env.CODEX_HOME??Ye.join(No.homedir(),".codex"),"config.toml"),"utf8"))}catch{return{}}}var il=async e=>{let{Codex:t}=await Un("@openai/codex-sdk","codex"),n=an.mkdtempSync(Ye.join(No.tmpdir(),"saffron-codex-"));try{let r=Object.fromEntries(Object.entries(process.env).filter(l=>l[1]!==void 0)),o=new t({codexPathOverride:Ng(),env:{...r,[ol]:e.mcpToken},config:{...Fo(),developer_instructions:e.systemPrompt,include_permissions_instructions:!1,include_apps_instructions:!1,include_collaboration_mode_instructions:!1,include_environment_context:!1,project_doc_max_bytes:0,skills:{include_instructions:!1},mcp_servers:Object.fromEntries(Object.entries(e.mcpServers).map(([l,d])=>[l,{url:d,bearer_token_env_var:ol,required:!0,default_tools_approval_mode:"approve",startup_timeout_sec:30,tool_timeout_sec:600}])),features:{shell_tool:!1,view_image:!1,apps:!1,plugins:!1,hooks:!1,memories:!1,goals:!1,image_generation:!1,tool_suggest:!1,browser_use:!1,computer_use:!1,unified_exec:!1,multi_agent:!1,code_mode_host:!1,in_app_browser:!1,browser_use_external:!1}}}).startThread({model:e.model,workingDirectory:n,skipGitRepoCheck:!0,sandboxMode:"read-only",approvalPolicy:"never",webSearchMode:"disabled"}),i=new AbortController,a=0,c;try{let{events:l}=await o.runStreamed(e.prompt,{signal:i.signal});for await(let d of l)if(d.type==="item.started"&&d.item.type==="mcp_tool_call")++a>e.maxTurns&&!i.signal.aborted&&(c=on(e.maxTurns),i.abort());else if(d.type==="item.completed")(d.item.type==="mcp_tool_call"||d.item.type==="agent_message")&&e.usage.aiCalls++,d.item.type==="agent_message"&&e.lastOutput(d.item.text);else if(d.type==="turn.completed"){let f=d.usage;e.usage.inputTokens=f.input_tokens-f.cached_input_tokens,e.usage.cacheReadTokens=f.cached_input_tokens,e.usage.cacheCreationTokens=f.cache_write_input_tokens??0,e.usage.outputTokens=f.output_tokens}else d.type==="turn.failed"&&!i.signal.aborted?c=d.error.message:d.type==="error"&&!i.signal.aborted&&(c??=d.message)}catch(l){if(!i.signal.aborted&&!c)throw l}if(e.usage.models=[`codex/${e.model??"default"}`],c&&!e.finished()){let l=/auth|log ?in|401|unauthori[sz]ed/i.test(c)?". Sign in with your ChatGPT plan: npx @openai/codex login":"";throw new Error(`Codex: ${c}${l}`)}}finally{an.rmSync(n,{recursive:!0,force:!0})}};import{spawn as Dg,spawnSync as Lg}from"node:child_process";import te from"node:fs";import Jr from"node:os";import ie from"node:path";import Ug from"node:readline";var qr="install the Antigravity CLI (curl -fsSL https://antigravity.google/cli/install.sh | bash), then run `agy` once to sign in with your Google account";function Wr(){for(let e of[...(process.env.PATH??"").split(ie.delimiter),ie.join(Jr.homedir(),".local","bin")]){let t=ie.join(e,process.platform==="win32"?"agy.exe":"agy");if(e&&te.existsSync(t))return t}}var cn={playwright:"saffron_browser",saffron:"saffron_tools"},al=Object.values(cn).map(e=>`mcp(${e}/*)`),Mo=()=>ie.join(Jr.homedir(),".gemini","antigravity-cli","settings.json");function cl(e){if(!te.existsSync(e))return{};try{let t=JSON.parse(te.readFileSync(e,"utf8"));if(t&&typeof t=="object"&&!Array.isArray(t))return t}catch{}throw new Error(`${e} is not a JSON object Saffron can add to: fix it (or ask agy to rewrite it), then run npx saffron login antigravity again`)}function Gr(e=Mo()){let t;try{t=cl(e).permissions?.allow}catch{return al}return al.filter(n=>!(Array.isArray(t)&&t.includes(n)))}function Bg(e=Mo()){let t=cl(e),n=Gr(e);if(n.length===0)return[];let r=t.permissions&&typeof t.permissions=="object"?t.permissions:{},s=Array.isArray(r.allow)?r.allow:[],o={...t,permissions:{...r,allow:[...s,...n]}},i=te.lstatSync(e,{throwIfNoEntry:!1})?.isSymbolicLink()??!1,a=zg(e),c=te.existsSync(a)?te.statSync(a).mode&4095:void 0;try{Ae(a,JSON.stringify(o,null,2)+`
335
+ `,c)}catch(l){let d=l.code;throw!i||!["EACCES","EPERM","EROFS"].includes(d??"")?l:new Error(`${e} links to ${a}, which Saffron cannot write: add ${n.join(" and ")} to permissions.allow there, then run npx saffron login antigravity again`)}return n}function zg(e){let t=e;for(let n=0;n<40&&te.lstatSync(t,{throwIfNoEntry:!1})?.isSymbolicLink();n++)t=ie.resolve(te.realpathSync(ie.dirname(t)),te.readlinkSync(t));return te.existsSync(t)?te.realpathSync(t):t}async function ll(){let e=Wr();if(!e)throw new Error(`the antigravity provider needs the agy command: ${qr}`);let t=Lg(e,["models"],{encoding:"utf8",timeout:6e4});if(t.status!==0){let r=`${t.stderr??""}${t.stdout??""}`.trim().split(`
336
+ `).at(-1)||(t.error?.message??"no answer");throw new Error(`agy could not list your models (${r}): run agy once and sign in with your Google AI Pro or Ultra account`)}let n=Bg();console.log(n.length>0?`Antigravity: allowed Saffron's tools in ${Mo()}: ${n.join(", ")}. Remove those lines to take it back.`:"Antigravity: Saffron's tools are already allowed.")}var dl=ie.join(Jr.homedir(),".gemini","antigravity-cli","mcp"),Hg=Object.values(cn).map(e=>ie.join(dl,e)),qg=`const path=require("path");const DIRS=${JSON.stringify(Hg)};const SERVERS=${JSON.stringify(Object.values(cn))};
337
337
  let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{let t={};try{t=JSON.parse(s).toolCall||{}}catch{}const a=t.args||{};
338
338
  const file=typeof a.AbsolutePath==="string"?path.resolve(a.AbsolutePath):"";
339
339
  const dir=typeof a.DirectoryPath==="string"?path.resolve(a.DirectoryPath):"";
340
340
  const ok=(t.name==="call_mcp_tool"&&SERVERS.includes(a.ServerName))||(t.name==="view_file"&&DIRS.includes(path.dirname(file)))||(t.name==="list_dir"&&DIRS.includes(dir));
341
- process.stdout.write(JSON.stringify(ok?{decision:"allow"}:{decision:"deny",reason:"Only Saffron's browser and saffron tools are available in a Saffron session."}))})`,_g=(e,t)=>`let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{let i=0;try{i=JSON.parse(s).invocationNum}catch{}
341
+ process.stdout.write(JSON.stringify(ok?{decision:"allow"}:{decision:"deny",reason:"Only Saffron's browser and saffron tools are available in a Saffron session."}))})`,Jg=(e,t)=>`let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{let i=0;try{i=JSON.parse(s).invocationNum}catch{}
342
342
  const stop=i+1>=${e};if(stop)require("fs").writeFileSync(${JSON.stringify(t)},"");
343
- process.stdout.write(JSON.stringify(stop?{terminationBehavior:"terminate"}:{}))})`,Zc=async e=>{let t=Lr();if(!t)throw new Error(`the antigravity provider needs the agy command: ${Mr}`);if(Object.keys(e.mcpServers).length>0&&Ur().length>0)throw new Error("agy does not allow Saffron's tools yet: run npx saffron login antigravity once");let n=ee.mkdtempSync(se.join(Dr.tmpdir(),"saffron-agy-"));try{let r=se.join(n,".agents");ee.mkdirSync(se.join(r,"agents"),{recursive:!0}),ee.writeFileSync(se.join(r,"agents","saffron.md"),`---
343
+ process.stdout.write(JSON.stringify(stop?{terminationBehavior:"terminate"}:{}))})`,pl=async e=>{let t=Wr();if(!t)throw new Error(`the antigravity provider needs the agy command: ${qr}`);if(Object.keys(e.mcpServers).length>0&&Gr().length>0)throw new Error("agy does not allow Saffron's tools yet: run npx saffron login antigravity once");let n=te.mkdtempSync(ie.join(Jr.tmpdir(),"saffron-agy-"));try{let r=ie.join(n,".agents");te.mkdirSync(ie.join(r,"agents"),{recursive:!0}),te.writeFileSync(ie.join(r,"agents","saffron.md"),`---
344
344
  name: saffron
345
345
  description: Saffron's browser test agent.
346
346
  mainAgent: true
@@ -350,15 +350,15 @@ commandExecutionPolicy: off
350
350
 
351
351
  ${e.systemPrompt}
352
352
 
353
- Your tools are MCP tools, called with call_mcp_tool: the browser_* tools are on the ${rn.playwright} server and the saffron_* tools on the ${rn.saffron} server. Before a tool's first call, read its parameters with view_file from ${se.join(Yc,"<server>","<tool>.json")}. No other tool is available.
354
- `),ee.writeFileSync(se.join(r,"mcp_config.json"),JSON.stringify({mcpServers:Object.fromEntries(Object.entries(e.mcpServers).map(([k,w])=>[rn[k]??k,{serverUrl:w,headers:{Authorization:`Bearer ${e.mcpToken}`}}]))}));let s=(k,w)=>{let x=se.join(n,k);return ee.writeFileSync(x,w),`${JSON.stringify(process.execPath)} ${JSON.stringify(x)}`};ee.writeFileSync(se.join(r,"hooks.json"),JSON.stringify({"saffron-gate":{PreToolUse:[{matcher:"",hooks:[{type:"command",command:s("gate.cjs",Ag)}]}]},"saffron-turns":{PostInvocation:[{type:"command",command:s("turns.cjs",_g(e.maxTurns,se.join(n,"turn-limit")))}]}}));let i=["--output-format","stream-json","--agent","saffron","--print-timeout",`${Math.max(30,2*e.maxTurns)}m`,"--sandbox"];e.model&&i.push("--model",e.model);let a=vg(t,i,{cwd:n,stdio:["pipe","pipe","pipe"]});a.stdin.on("error",()=>{}),a.stdin.end(e.prompt);let c="";a.stderr.on("data",k=>{c=(c+k).slice(-4e3)});let l=new Promise((k,w)=>a.once("error",x=>w(new Error(`Antigravity: could not start ${t}: ${x.message}. ${Mr}`)))),d=new Promise(k=>a.once("close",k)),f="",m,p,b=(async()=>{for await(let k of kg.createInterface({input:a.stdout})){let w;try{w=JSON.parse(k)}catch{continue}let x=w.step_update;x?(x.state==="DONE"&&x.usage&&e.usage.aiCalls++,x.step_type==="agent_response"&&typeof x.text_delta=="string"&&(x.step_index!==m&&([f,m]=["",x.step_index]),f+=x.text_delta,e.lastOutput(f))):w.event==="result"&&(p=w.result)}})();await Promise.race([b,l]);let v=await Promise.race([d,l]);if(p?.usage&&(e.usage.inputTokens+=Math.max(0,(p.usage.input_tokens??0)-(p.usage.cache_read_tokens??0)),e.usage.outputTokens+=p.usage.output_tokens??0,e.usage.cacheReadTokens+=p.usage.cache_read_tokens??0),e.usage.models=[`antigravity/${e.model??"default"}`],p?.response&&e.lastOutput(p.response),!e.finished()&&ee.existsSync(se.join(n,"turn-limit")))throw new Error(`Antigravity: ${tn(e.maxTurns)}`);if((p?.status!=="SUCCESS"||v!==0)&&!e.finished()){let k=typeof p?.error=="string"?p.error:c.trim().split(`
355
- `).at(-1)||`agy exited with ${v}`;throw new Error(`Antigravity: ${k}${/auth/i.test(k)?`. ${Mr}`:""}`)}}finally{ee.rmSync(n,{recursive:!0,force:!0})}};import Qc from"node:fs";import Ig from"node:os";import Pg from"node:path";var el="@cursor/sdk";function Nn(e=process.versions.node){let[t,n]=e.split(".").map(Number);return t>22||t===22&&n>=13?void 0:`the cursor provider needs Node.js 22.13 or newer (this is ${e})`}var tl=async e=>{let t=Nn();if(t)throw new Error(t);let{Agent:n,JsonlLocalAgentStore:r}=await jn(el,"cursor"),s=Qc.mkdtempSync(Pg.join(Ig.tmpdir(),"saffron-cursor-")),o=i=>n.create({model:{id:e.model??"auto"},...i?{systemPrompt:i}:{},tools:["mcp"],mcpServers:Object.fromEntries(Object.entries(e.mcpServers).map(([a,c])=>[a,{type:"http",url:c,headers:{Authorization:`Bearer ${e.mcpToken}`}}])),local:{cwd:s,settingSources:[],store:new r(s)}});try{let i=await Xc(await o(e.systemPrompt),e.prompt,e);if(i.status==="error"&&/system.?prompt/i.test(i.error?.message??"")&&(i=await Xc(await o(void 0),`${e.systemPrompt}
353
+ Your tools are MCP tools, called with call_mcp_tool: the browser_* tools are on the ${cn.playwright} server and the saffron_* tools on the ${cn.saffron} server. Before a tool's first call, read its parameters with view_file from ${ie.join(dl,"<server>","<tool>.json")}. No other tool is available.
354
+ `),te.writeFileSync(ie.join(r,"mcp_config.json"),JSON.stringify({mcpServers:Object.fromEntries(Object.entries(e.mcpServers).map(([k,w])=>[cn[k]??k,{serverUrl:w,headers:{Authorization:`Bearer ${e.mcpToken}`}}]))}));let s=(k,w)=>{let x=ie.join(n,k);return te.writeFileSync(x,w),`${JSON.stringify(process.execPath)} ${JSON.stringify(x)}`};te.writeFileSync(ie.join(r,"hooks.json"),JSON.stringify({"saffron-gate":{PreToolUse:[{matcher:"",hooks:[{type:"command",command:s("gate.cjs",qg)}]}]},"saffron-turns":{PostInvocation:[{type:"command",command:s("turns.cjs",Jg(e.maxTurns,ie.join(n,"turn-limit")))}]}}));let i=["--output-format","stream-json","--agent","saffron","--print-timeout",`${Math.max(30,2*e.maxTurns)}m`,"--sandbox"];e.model&&i.push("--model",e.model);let a=Dg(t,i,{cwd:n,stdio:["pipe","pipe","pipe"]});a.stdin.on("error",()=>{}),a.stdin.end(e.prompt);let c="";a.stderr.on("data",k=>{c=(c+k).slice(-4e3)});let l=new Promise((k,w)=>a.once("error",x=>w(new Error(`Antigravity: could not start ${t}: ${x.message}. ${qr}`)))),d=new Promise(k=>a.once("close",k)),f="",m,p,b=(async()=>{for await(let k of Ug.createInterface({input:a.stdout})){let w;try{w=JSON.parse(k)}catch{continue}let x=w.step_update;x?(x.state==="DONE"&&x.usage&&e.usage.aiCalls++,x.step_type==="agent_response"&&typeof x.text_delta=="string"&&(x.step_index!==m&&([f,m]=["",x.step_index]),f+=x.text_delta,e.lastOutput(f))):w.event==="result"&&(p=w.result)}})();await Promise.race([b,l]);let $=await Promise.race([d,l]);if(p?.usage&&(e.usage.inputTokens+=Math.max(0,(p.usage.input_tokens??0)-(p.usage.cache_read_tokens??0)),e.usage.outputTokens+=p.usage.output_tokens??0,e.usage.cacheReadTokens+=p.usage.cache_read_tokens??0),e.usage.models=[`antigravity/${e.model??"default"}`],p?.response&&e.lastOutput(p.response),!e.finished()&&te.existsSync(ie.join(n,"turn-limit")))throw new Error(`Antigravity: ${on(e.maxTurns)}`);if((p?.status!=="SUCCESS"||$!==0)&&!e.finished()){let k=typeof p?.error=="string"?p.error:c.trim().split(`
355
+ `).at(-1)||`agy exited with ${$}`;throw new Error(`Antigravity: ${k}${/auth/i.test(k)?`. ${qr}`:""}`)}}finally{te.rmSync(n,{recursive:!0,force:!0})}};import fl from"node:fs";import Wg from"node:os";import Gg from"node:path";var gl="@cursor/sdk";function Bn(e=process.versions.node){let[t,n]=e.split(".").map(Number);return t>22||t===22&&n>=13?void 0:`the cursor provider needs Node.js 22.13 or newer (this is ${e})`}var hl=async e=>{let t=Bn();if(t)throw new Error(t);let{Agent:n,JsonlLocalAgentStore:r}=await Un(gl,"cursor"),s=fl.mkdtempSync(Gg.join(Wg.tmpdir(),"saffron-cursor-")),o=i=>n.create({model:{id:e.model??"auto"},...i?{systemPrompt:i}:{},tools:["mcp"],mcpServers:Object.fromEntries(Object.entries(e.mcpServers).map(([a,c])=>[a,{type:"http",url:c,headers:{Authorization:`Bearer ${e.mcpToken}`}}])),local:{cwd:s,settingSources:[],store:new r(s)}});try{let i=await ul(await o(e.systemPrompt),e.prompt,e);if(i.status==="error"&&/system.?prompt/i.test(i.error?.message??"")&&(i=await ul(await o(void 0),`${e.systemPrompt}
356
356
 
357
357
  ---
358
358
 
359
- ${e.prompt}`,e)),i.status==="error"&&!e.finished())throw new Error(`Cursor: ${i.error?.message??"the run failed"}${Cg(i.error?.message)}`)}finally{Qc.rmSync(s,{recursive:!0,force:!0})}};async function Xc(e,t,n){try{let r=await e.send(t),s="",o=new Set,i=!1;for await(let c of r.stream())if(c.type==="usage")n.usage.aiCalls++;else if(c.type==="assistant"){for(let l of c.message.content)l.type==="text"&&(s+=l.text);n.lastOutput(s)}else c.type==="tool_call"&&(s="",c.status==="running"&&o.add(c.call_id??`#${o.size}`),o.size>n.maxTurns&&!i&&(i=!0,await r.cancel()));let a=await r.wait();if(a.usage&&(n.usage.inputTokens+=a.usage.inputTokens,n.usage.outputTokens+=a.usage.outputTokens,n.usage.cacheReadTokens+=a.usage.cacheReadTokens,n.usage.cacheCreationTokens+=a.usage.cacheWriteTokens),a.model&&(n.usage.models=[...new Set([...n.usage.models??[],`cursor/${a.model.id}`])]),a.result&&n.lastOutput(a.result),i&&!n.finished())throw new Error(`Cursor: ${tn(n.maxTurns)}`);return a}finally{e.close()}}function Cg(e=""){return/api key|unauthenticated|unauthori[sz]ed|logged.?out/i.test(e)?". Sign in once with: npx saffron login cursor":""}async function nl(){let e=Nn();if(e)throw new Error(e);let{Cursor:t}=await jn(el,"cursor"),n=await t.auth.status();if(n.status==="logged-in"){console.log(`Cursor: already signed in${n.email?` as ${n.email}`:""}.`);return}await t.auth.login({onLoginUrl:r=>console.log(`Opening ${r}`)}),console.log("Cursor: signed in. Saffron runs will use your Cursor plan.")}var Eo=["claude","codex","antigravity","cursor"];function ol(e){return Eo.includes(e)}var Ao={codex:Gc,antigravity:Zc,cursor:tl};function il(e,t){return e==="claude"?new jr(t):new Nr(Ao[e],t,()=>{let n=al(e);return n.length>0?`${At[e]} cannot start here: ${n.join("; ")}`:void 0})}var zr=(...e)=>sl.join(Og.homedir(),...e);function rl(e){try{return import.meta.resolve(e),!0}catch{return!1}}function al(e){switch(e){case"claude":return[];case"codex":{let t=Fr();return t?[t]:rl("@openai/codex-sdk")?[]:["npm install --save-dev @openai/codex-sdk"]}case"antigravity":return Lr()?Ur().length>0?["Saffron's tools allowed in agy, once: npx saffron login antigravity"]:[]:["the Antigravity CLI: curl -fsSL https://antigravity.google/cli/install.sh | bash","then run `agy` once to sign in with your Google AI Pro or Ultra account"];case"cursor":{let t=Nn();return t?[t]:rl("@cursor/sdk")?[]:["npm install --save-dev @cursor/sdk"]}}}function _o(e){let t=process.env,n=al(e);if(e==="claude")t.ANTHROPIC_API_KEY||t.CLAUDE_CODE_OAUTH_TOKEN||t.CLAUDE_CODE_USE_BEDROCK||t.CLAUDE_CODE_USE_VERTEX||Br.existsSync(zr(".claude",".credentials.json"))||Br.existsSync(zr(".claude.json"))||n.push("ANTHROPIC_API_KEY=sk-ant-... (console.anthropic.com \u2192 API keys)","or a Claude Code login on this machine");else if(e==="codex"&&!Fr()){let r=sl.join(t.CODEX_HOME??zr(".codex"),"auth.json");!["keyring","auto"].includes(To().cli_auth_credentials_store??"")&&!Br.existsSync(r)&&n.push("a Codex login with your ChatGPT plan: npx @openai/codex login")}else e==="cursor"&&!Nn()&&!t.CURSOR_API_KEY&&!Br.existsSync(zr(".cursor","sdk","auth.json"))&&n.push("a Cursor sign-in for Saffron (once per machine): npx saffron login cursor");return n}var At={claude:"Claude",codex:"Codex",antigravity:"Antigravity",cursor:"Cursor"};var ft=(e,t=0)=>typeof e=="number"?e:t,Z=e=>typeof e=="string"?e:void 0;function jg(e,t){if(!t)return;let n=Z(t.selector);if(n)return Ng(n);let r=Z(t.url);if(r)return r;if(e==="press"||e==="keyboardPress")return Z(t.key)}function Ng(e){return e.replace(/internal:role=([a-z]+)\[name="((?:[^"\\]|\\.)*)"[is]?\]/g,(t,n,r)=>`${n} "${r}"`).replace(/internal:role=([a-z]+)/g,"$1").replace(/internal:label="((?:[^"\\]|\\.)*)"[is]?/g,'label "$1"').replace(/internal:text="((?:[^"\\]|\\.)*)"[is]?/g,'text "$1"').replace(/internal:attr=\[placeholder="((?:[^"\\]|\\.)*)"[is]?\]/g,'placeholder "$1"').replace(/internal:testid=\[data-testid="((?:[^"\\]|\\.)*)"s?\]/g,'test id "$1"').replace(/internal:has-text="((?:[^"\\]|\\.)*)"[is]?/g,'with text "$1"').replace(/ >> nth=0$/,"").replace(/ >> /g," \u203A ").replace(/nth=(\d+)/g,"#$1")}function Fg(e){let t=new Map,n=[],r=[];for(let s of e){if(!s.trim())continue;let o;try{o=JSON.parse(s)}catch{continue}r.push(o);let i=Z(o.callId);switch(o.type){case"before":{if(!i)break;let a=Z(o.method)??"",c=o.class==="Tracing"&&a==="tracingGroup",l=a==="__waitInfo__";t.set(i,{callId:i,method:l?Z(o.title)??"wait":a,subject:jg(a,o.params),selector:Z(o.params?.selector),startTime:ft(o.startTime),log:[],pageId:Z(o.pageId),before:Io(o,"beforeSnapshot"),calls:[],parentId:Z(o.parentId),isGroup:c,title:Z(o.title),isWait:l}),n.push(i);break}case"input":{let a=i?t.get(i):void 0;if(a){a.input=Io({...o,pageId:a.pageId},"inputSnapshot");let c=o.point,l=o.box,d=c??(l?{x:l.x+l.width/2,y:l.y+l.height/2}:void 0);a.input&&d?a.input.point=d:d&&(a.inputPoint=d)}break}case"frame-snapshot":{let a=o.snapshot,c=a?.callId?t.get(a.callId):void 0;if(c&&a?.phase&&a.pageId&&a.isMainFrame!==!1){let l=a.phase,d={pageId:a.pageId,name:`${l}@${a.callId}`,callId:a.callId,phase:l};if(c.pageId||(c.pageId=a.pageId),l==="before"&&!c.before)c.before=d;else if(l==="after")c.after=d;else if(l==="action"&&!c.input){let f=c.inputPoint;c.input=f?{...d,point:f}:d}}break}case"log":{let a=i?t.get(i):void 0,c=Z(o.message);a&&c&&a.log.push(c);break}case"after":{let a=i?t.get(i):void 0;if(!a)break;a.endTime=ft(o.endTime,a.startTime);let c=o.error;c?.message&&(a.error=c.message);let l=Io({...o,pageId:a.pageId},"afterSnapshot");l&&(a.after=l);break}default:break}}return{calls:t,order:n,events:r}}function Io(e,t){let n=Z(e[t]),r=Z(e.pageId);return n&&r?{pageId:r,name:n}:void 0}function cl(e){return Po(Li(e))}function Po(e){let n=e.filter(y=>y.name.endsWith(".trace")).flatMap(y=>y.data.toString("utf8").split(`
360
- `)),{calls:r,order:s,events:o}=Fg(n),i=new Map;for(let y of o){let h=y.snapshot;if(y.type!=="frame-snapshot"||!h?.isMainFrame||!h.pageId||!h.frameUrl)continue;let S=h.snapshotName??(h.callId&&h.phase?`${h.phase}@${h.callId}`:void 0);S&&i.set(`${h.pageId}/${S}`,h.frameUrl)}for(let y of r.values())for(let h of[y.before,y.input,y.after])h&&(h.url=i.get(`${h.pageId}/${h.name}`));let a=[];for(let y of s){let h=r.get(y),S=h.parentId?r.get(h.parentId):void 0;S?S.calls.push(h):a.push(y)}let c=y=>{let{parentId:h,isGroup:S,title:$,isWait:u,...g}=y;return{...g,calls:g.calls.map(R=>c(R))}},l=y=>y.find(h=>h.error&&!h.isWait)?.error,d=[],f=[];for(let y of a){let h=r.get(y);if(!h.isGroup){f.push(c(h));continue}let S={title:h.title??"",startTime:h.startTime,endTime:h.endTime,error:h.error,actions:[],calls:[]},$=/^\[saffron:(agent|replay|pending):(-?\d+)\] (.*)$/.exec(S.title);$&&(S.phase=$[1],S.stepIndex=Number($[2]),S.title=$[3]);let u=[];for(let g of h.calls)if(g.isGroup){let R={title:g.title??"",startTime:g.startTime,endTime:g.endTime,error:g.error??l(g.calls),calls:g.calls.map(I=>c(I))},E=/^\[saffron:action:(\d+)\] ([\s\S]*)$/.exec(R.title);E&&(R.actionIndex=Number(E[1]),R.title=E[2]),S.actions.push(R)}else u.push(g),S.calls.push(c(g));S.error||(S.error=S.actions.find(g=>g.error)?.error??l(u)),d.push(S)}let m=[],p=new Map,b,v,k,w=Number.POSITIVE_INFINITY,x=0;for(let y of o)if(y.type==="context-options")b=Z(y.title),v=Z(y.browserName),k=Number(y.version)||void 0,w=Math.min(w,ft(y.monotonicTime,w));else if(y.type==="screencast-frame"){let h=Z(y.file)??(Z(y.sha1)?`resources/${Z(y.sha1)}`:""),S={pageId:Z(y.pageId)??"",file:h,sha1:h.slice(h.lastIndexOf("/")+1),timestamp:ft(y.timestamp),width:ft(y.width),height:ft(y.height)};m.push(S),p.has(S.pageId)||p.set(S.pageId,{pageId:S.pageId})}else if(y.type==="event"&&y.class==="BrowserContext"&&y.method==="page"){let h=Z(y.params?.pageId);h&&p.set(h,{...p.get(h),pageId:h,openedAt:ft(y.time)})}else if(y.type==="event"&&y.class==="BrowserContext"&&y.method==="pageClosed"){let h=Z(y.params?.pageId);h&&p.set(h,{...p.get(h),pageId:h,closedAt:ft(y.time)})}else if(y.type==="frame-snapshot"){let h=y.snapshot;if(h?.pageId){let S=p.get(h.pageId)??{pageId:h.pageId};h.isMainFrame&&h.frameUrl&&(S.url=h.frameUrl),p.set(h.pageId,S)}}for(let y of r.values())w=Math.min(w,y.startTime),x=Math.max(x,y.endTime??y.startTime);for(let y of m)x=Math.max(x,y.timestamp);return Number.isFinite(w)||(w=0),{format:k,title:b,browserName:v,startTime:w,endTime:x,steps:d,calls:f,frames:m,requests:Mg(e),pages:[...p.values()],resources:e.filter(y=>y.name.startsWith("resources/")).map(y=>y.name.slice(10))}}function Mg(e){let t=[];for(let n of e.filter(r=>r.name.endsWith(".network")))for(let r of n.data.toString("utf8").split(`
361
- `)){if(!r.trim())continue;let s;try{s=JSON.parse(r)}catch{continue}if(s.type!=="resource-snapshot"||!s.snapshot)continue;let o=s.snapshot,i=o.request,a=o.response,c=o._monotonicTime;t.push({pageId:Z(o.pageref),method:i?.method??"GET",url:i?.url??"",status:a?.status,mimeType:a?.content?.mimeType,startedAt:Date.parse(Z(o.startedDateTime)??"")||0,time:typeof o.time=="number"?o.time:void 0,timestamp:typeof c=="number"?c:void 0})}return t}import on from"node:fs";import{randomBytes as bl}from"node:crypto";import ll from"node:fs";import sn from"node:path";var dl="This run has no reviewable proposal identity. Run the scenario again to review from replay.",Co="The proposal does not belong to this replay.",pl=e=>e instanceof Error?e.message:String(e),fl=(e,t)=>`No pending proposal at ${sn.relative(e,t)}: it has been accepted, rejected or removed since this run.`;function ul(e,t){let n=/ \(example \d+\)$/.exec(t.scenario),r=n?[t.scenario,t.scenario.slice(0,n.index)]:[t.scenario],s=o=>o.split(/[\\/]/).slice(-2).join("/");return r.map(o=>xe(e,t.feature,o)).find(o=>s(o)===s(t.proposalFile))}function gl(e,t,n){if(qe(n)!==t.proposalRevision)return{available:!1,reason:"The proposal changed since this run. Open the latest run before reviewing it."};let r=a=>({available:!1,reason:a,rejectable:!0}),s;try{s=Ht(n)}catch(a){if(a instanceof X)return r(`The proposal cannot be read: ${sn.relative(e,n)}: ${a.problem}`);throw a}let{feature:o,scenario:i}=s.cache;if(sn.resolve(xe(e,o,i))!==sn.resolve(n)||o!==t.feature||!(t.scenario===i||t.scenario===s.meta.recordedFor||t.scenario.startsWith(i+" (example ")))return{available:!1,reason:Co};try{if(!ll.existsSync(sn.resolve(e,o)))return r(`STALE: ${o} no longer exists`);let a=Re(e,ne(e),[sn.resolve(e,o)]),c=$e(e,s,a);if(c)return r(`STALE: ${c}`);let l=t.status==="red",d=s.meta.verified===!0&&!l,f=l?t.error??"This run replayed the proposal and it failed.":s.meta.proofError;return{available:!0,verified:d,...!d&&f?{proofError:f}:{},unbound:!s.meta.bound,diff:lt(me(Y(e,o,i)),s.cache),featureEdits:s.meta.featureEdits??[]}}catch(a){return r(pl(a))}}function hl(e,t){if(!t.proposalFile||!t.proposalRevision)return{available:!1,reason:dl};try{let n=ul(e,{...t,proposalFile:t.proposalFile});return n?ll.existsSync(n)?gl(e,t,n):{available:!1,reason:fl(e,n)}:{available:!1,reason:Co}}catch(n){return{available:!1,reason:pl(n)}}}function ml(e,t,n,r=!1){if(!t.proposalFile||!t.proposalRevision)throw new Error(dl);let s=ul(e,{...t,proposalFile:t.proposalFile});if(!s)throw new Error(Co);rr(s,()=>{let o=gl(e,t,s);if(n==="reject"){if(!o.available&&!o.rejectable)throw new Error(o.reason);ir(s)}else{if(!o.available)throw new Error(o.reason);if(!o.verified&&!r)throw new Error("Confirm that you want to accept a proposal without successful proof replay.");or(e,s)}},()=>{throw new Error(fl(e,s))})}import Dg from"node:http";import It from"node:path";function yl(){return`<!DOCTYPE html>
359
+ ${e.prompt}`,e)),i.status==="error"&&!e.finished())throw new Error(`Cursor: ${i.error?.message??"the run failed"}${Vg(i.error?.message)}`)}finally{fl.rmSync(s,{recursive:!0,force:!0})}};async function ul(e,t,n){try{let r=await e.send(t),s="",o=new Set,i=!1;for await(let c of r.stream())if(c.type==="usage")n.usage.aiCalls++;else if(c.type==="assistant"){for(let l of c.message.content)l.type==="text"&&(s+=l.text);n.lastOutput(s)}else c.type==="tool_call"&&(s="",c.status==="running"&&o.add(c.call_id??`#${o.size}`),o.size>n.maxTurns&&!i&&(i=!0,await r.cancel()));let a=await r.wait();if(a.usage&&(n.usage.inputTokens+=a.usage.inputTokens,n.usage.outputTokens+=a.usage.outputTokens,n.usage.cacheReadTokens+=a.usage.cacheReadTokens,n.usage.cacheCreationTokens+=a.usage.cacheWriteTokens),a.model&&(n.usage.models=[...new Set([...n.usage.models??[],`cursor/${a.model.id}`])]),a.result&&n.lastOutput(a.result),i&&!n.finished())throw new Error(`Cursor: ${on(n.maxTurns)}`);return a}finally{e.close()}}function Vg(e=""){return/api key|unauthenticated|unauthori[sz]ed|logged.?out/i.test(e)?". Sign in once with: npx saffron login cursor":""}async function ml(){let e=Bn();if(e)throw new Error(e);let{Cursor:t}=await Un(gl,"cursor"),n=await t.auth.status();if(n.status==="logged-in"){console.log(`Cursor: already signed in${n.email?` as ${n.email}`:""}.`);return}await t.auth.login({onLoginUrl:r=>console.log(`Opening ${r}`)}),console.log("Cursor: signed in. Saffron runs will use your Cursor plan.")}var Do=["claude","codex","antigravity","cursor"];function wl(e){return Do.includes(e)}var Lo={codex:il,antigravity:pl,cursor:hl};function vl(e,t){return e==="claude"?new Br(t):new zr(Lo[e],t,()=>{let n=Sl(e);return n.length>0?`${It[e]} cannot start here: ${n.join("; ")}`:void 0})}var Kr=(...e)=>bl.join(Kg.homedir(),...e);function yl(e){try{return import.meta.resolve(e),!0}catch{return!1}}function Sl(e){switch(e){case"claude":return[];case"codex":{let t=Hr();return t?[t]:yl("@openai/codex-sdk")?[]:["npm install --save-dev @openai/codex-sdk"]}case"antigravity":return Wr()?Gr().length>0?["Saffron's tools allowed in agy, once: npx saffron login antigravity"]:[]:["the Antigravity CLI: curl -fsSL https://antigravity.google/cli/install.sh | bash","then run `agy` once to sign in with your Google AI Pro or Ultra account"];case"cursor":{let t=Bn();return t?[t]:yl("@cursor/sdk")?[]:["npm install --save-dev @cursor/sdk"]}}}function Uo(e){let t=process.env,n=Sl(e);if(e==="claude")t.ANTHROPIC_API_KEY||t.CLAUDE_CODE_OAUTH_TOKEN||t.CLAUDE_CODE_USE_BEDROCK||t.CLAUDE_CODE_USE_VERTEX||Vr.existsSync(Kr(".claude",".credentials.json"))||Vr.existsSync(Kr(".claude.json"))||n.push("ANTHROPIC_API_KEY=sk-ant-... (console.anthropic.com \u2192 API keys)","or a Claude Code login on this machine");else if(e==="codex"&&!Hr()){let r=bl.join(t.CODEX_HOME??Kr(".codex"),"auth.json");!["keyring","auto"].includes(Fo().cli_auth_credentials_store??"")&&!Vr.existsSync(r)&&n.push("a Codex login with your ChatGPT plan: npx @openai/codex login")}else e==="cursor"&&!Bn()&&!t.CURSOR_API_KEY&&!Vr.existsSync(Kr(".cursor","sdk","auth.json"))&&n.push("a Cursor sign-in for Saffron (once per machine): npx saffron login cursor");return n}var It={claude:"Claude",codex:"Codex",antigravity:"Antigravity",cursor:"Cursor"};var gt=(e,t=0)=>typeof e=="number"?e:t,Q=e=>typeof e=="string"?e:void 0;function Yg(e,t){if(!t)return;let n=Q(t.selector);if(n)return Qg(n);let r=Q(t.url);if(r)return r;if(e==="press"||e==="keyboardPress")return Q(t.key)}function Qg(e){return e.replace(/internal:role=([a-z]+)\[name="((?:[^"\\]|\\.)*)"[is]?\]/g,(t,n,r)=>`${n} "${r}"`).replace(/internal:role=([a-z]+)/g,"$1").replace(/internal:label="((?:[^"\\]|\\.)*)"[is]?/g,'label "$1"').replace(/internal:text="((?:[^"\\]|\\.)*)"[is]?/g,'text "$1"').replace(/internal:attr=\[placeholder="((?:[^"\\]|\\.)*)"[is]?\]/g,'placeholder "$1"').replace(/internal:testid=\[data-testid="((?:[^"\\]|\\.)*)"s?\]/g,'test id "$1"').replace(/internal:has-text="((?:[^"\\]|\\.)*)"[is]?/g,'with text "$1"').replace(/ >> nth=0$/,"").replace(/ >> /g," \u203A ").replace(/nth=(\d+)/g,"#$1")}function Zg(e){let t=new Map,n=[],r=[];for(let s of e){if(!s.trim())continue;let o;try{o=JSON.parse(s)}catch{continue}r.push(o);let i=Q(o.callId);switch(o.type){case"before":{if(!i)break;let a=Q(o.method)??"",c=o.class==="Tracing"&&a==="tracingGroup",l=a==="__waitInfo__";t.set(i,{callId:i,method:l?Q(o.title)??"wait":a,subject:Yg(a,o.params),selector:Q(o.params?.selector),startTime:gt(o.startTime),log:[],pageId:Q(o.pageId),before:Bo(o,"beforeSnapshot"),calls:[],parentId:Q(o.parentId),isGroup:c,title:Q(o.title),isWait:l}),n.push(i);break}case"input":{let a=i?t.get(i):void 0;if(a){a.input=Bo({...o,pageId:a.pageId},"inputSnapshot");let c=o.point,l=o.box,d=c??(l?{x:l.x+l.width/2,y:l.y+l.height/2}:void 0);a.input&&d?a.input.point=d:d&&(a.inputPoint=d)}break}case"frame-snapshot":{let a=o.snapshot,c=a?.callId?t.get(a.callId):void 0;if(c&&a?.phase&&a.pageId&&a.isMainFrame!==!1){let l=a.phase,d={pageId:a.pageId,name:`${l}@${a.callId}`,callId:a.callId,phase:l};if(c.pageId||(c.pageId=a.pageId),l==="before"&&!c.before)c.before=d;else if(l==="after")c.after=d;else if(l==="action"&&!c.input){let f=c.inputPoint;c.input=f?{...d,point:f}:d}}break}case"log":{let a=i?t.get(i):void 0,c=Q(o.message);a&&c&&a.log.push(c);break}case"after":{let a=i?t.get(i):void 0;if(!a)break;a.endTime=gt(o.endTime,a.startTime);let c=o.error;c?.message&&(a.error=c.message);let l=Bo({...o,pageId:a.pageId},"afterSnapshot");l&&(a.after=l);break}default:break}}return{calls:t,order:n,events:r}}function Bo(e,t){let n=Q(e[t]),r=Q(e.pageId);return n&&r?{pageId:r,name:n}:void 0}function xl(e){return zo(Qi(e))}function zo(e){let n=e.filter(y=>y.name.endsWith(".trace")).flatMap(y=>y.data.toString("utf8").split(`
360
+ `)),{calls:r,order:s,events:o}=Zg(n),i=new Map;for(let y of o){let u=y.snapshot;if(y.type!=="frame-snapshot"||!u?.isMainFrame||!u.pageId||!u.frameUrl)continue;let v=u.snapshotName??(u.callId&&u.phase?`${u.phase}@${u.callId}`:void 0);v&&i.set(`${u.pageId}/${v}`,u.frameUrl)}for(let y of r.values())for(let u of[y.before,y.input,y.after])u&&(u.url=i.get(`${u.pageId}/${u.name}`));let a=[];for(let y of s){let u=r.get(y),v=u.parentId?r.get(u.parentId):void 0;v?v.calls.push(u):a.push(y)}let c=y=>{let{parentId:u,isGroup:v,title:S,isWait:h,...g}=y;return{...g,calls:g.calls.map(T=>c(T))}},l=y=>y.find(u=>u.error&&!u.isWait)?.error,d=[],f=[];for(let y of a){let u=r.get(y);if(!u.isGroup){f.push(c(u));continue}let v={title:u.title??"",startTime:u.startTime,endTime:u.endTime,error:u.error,actions:[],calls:[]},S=/^\[saffron:(agent|replay|pending):(-?\d+)\] (.*)$/.exec(v.title);S&&(v.phase=S[1],v.stepIndex=Number(S[2]),v.title=S[3]);let h=[];for(let g of u.calls)if(g.isGroup){let T={title:g.title??"",startTime:g.startTime,endTime:g.endTime,error:g.error??l(g.calls),calls:g.calls.map(_=>c(_))},E=/^\[saffron:action:(\d+)\] ([\s\S]*)$/.exec(T.title);E&&(T.actionIndex=Number(E[1]),T.title=E[2]),v.actions.push(T)}else h.push(g),v.calls.push(c(g));v.error||(v.error=v.actions.find(g=>g.error)?.error??l(h)),d.push(v)}let m=[],p=new Map,b,$,k,w=Number.POSITIVE_INFINITY,x=0;for(let y of o)if(y.type==="context-options")b=Q(y.title),$=Q(y.browserName),k=Number(y.version)||void 0,w=Math.min(w,gt(y.monotonicTime,w));else if(y.type==="screencast-frame"){let u=Q(y.file)??(Q(y.sha1)?`resources/${Q(y.sha1)}`:""),v={pageId:Q(y.pageId)??"",file:u,sha1:u.slice(u.lastIndexOf("/")+1),timestamp:gt(y.timestamp),width:gt(y.width),height:gt(y.height)};m.push(v),p.has(v.pageId)||p.set(v.pageId,{pageId:v.pageId})}else if(y.type==="event"&&y.class==="BrowserContext"&&y.method==="page"){let u=Q(y.params?.pageId);u&&p.set(u,{...p.get(u),pageId:u,openedAt:gt(y.time)})}else if(y.type==="event"&&y.class==="BrowserContext"&&y.method==="pageClosed"){let u=Q(y.params?.pageId);u&&p.set(u,{...p.get(u),pageId:u,closedAt:gt(y.time)})}else if(y.type==="frame-snapshot"){let u=y.snapshot;if(u?.pageId){let v=p.get(u.pageId)??{pageId:u.pageId};u.isMainFrame&&u.frameUrl&&(v.url=u.frameUrl),p.set(u.pageId,v)}}for(let y of r.values())w=Math.min(w,y.startTime),x=Math.max(x,y.endTime??y.startTime);for(let y of m)x=Math.max(x,y.timestamp);return Number.isFinite(w)||(w=0),{format:k,title:b,browserName:$,startTime:w,endTime:x,steps:d,calls:f,frames:m,requests:Xg(e),pages:[...p.values()],resources:e.filter(y=>y.name.startsWith("resources/")).map(y=>y.name.slice(10))}}function Xg(e){let t=[];for(let n of e.filter(r=>r.name.endsWith(".network")))for(let r of n.data.toString("utf8").split(`
361
+ `)){if(!r.trim())continue;let s;try{s=JSON.parse(r)}catch{continue}if(s.type!=="resource-snapshot"||!s.snapshot)continue;let o=s.snapshot,i=o.request,a=o.response,c=o._monotonicTime;t.push({pageId:Q(o.pageref),method:i?.method??"GET",url:i?.url??"",status:a?.status,mimeType:a?.content?.mimeType,startedAt:Date.parse(Q(o.startedDateTime)??"")||0,time:typeof o.time=="number"?o.time:void 0,timestamp:typeof c=="number"?c:void 0})}return t}import dn from"node:fs";import{randomBytes as Cl}from"node:crypto";import $l from"node:fs";import ln from"node:path";var kl="This run has no reviewable proposal identity. Run the scenario again to review from replay.",Ho="The proposal does not belong to this replay.",Tl=e=>e instanceof Error?e.message:String(e),Rl=(e,t)=>`No pending proposal at ${ln.relative(e,t)}: it has been accepted, rejected or removed since this run.`;function El(e,t){let n=/ \(example \d+\)$/.exec(t.scenario),r=n?[t.scenario,t.scenario.slice(0,n.index)]:[t.scenario],s=o=>o.split(/[\\/]/).slice(-2).join("/");return r.map(o=>xe(e,t.feature,o)).find(o=>s(o)===s(t.proposalFile))}function Al(e,t,n){if(qe(n)!==t.proposalRevision)return{available:!1,reason:"The proposal changed since this run. Open the latest run before reviewing it."};let r=a=>({available:!1,reason:a,rejectable:!0}),s;try{s=Gt(n)}catch(a){if(a instanceof X)return r(`The proposal cannot be read: ${ln.relative(e,n)}: ${a.problem}`);throw a}let{feature:o,scenario:i}=s.cache;if(ln.resolve(xe(e,o,i))!==ln.resolve(n)||o!==t.feature||!(t.scenario===i||t.scenario===s.meta.recordedFor||t.scenario.startsWith(i+" (example ")))return{available:!1,reason:Ho};try{if(!$l.existsSync(ln.resolve(e,o)))return r(`STALE: ${o} no longer exists`);let a=Ee(e,ee(e),[ln.resolve(e,o)]),c=ke(e,s,a);if(c)return r(`STALE: ${c}`);let l=t.status==="red",d=s.meta.verified===!0&&!l,f=l?t.error??"This run replayed the proposal and it failed.":s.meta.proofError;return{available:!0,verified:d,...!d&&f?{proofError:f}:{},unbound:!s.meta.bound,diff:pt(ye(Y(e,o,i)),s.cache),featureEdits:s.meta.featureEdits??[]}}catch(a){return r(Tl(a))}}function _l(e,t){if(!t.proposalFile||!t.proposalRevision)return{available:!1,reason:kl};try{let n=El(e,{...t,proposalFile:t.proposalFile});return n?$l.existsSync(n)?Al(e,t,n):{available:!1,reason:Rl(e,n)}:{available:!1,reason:Ho}}catch(n){return{available:!1,reason:Tl(n)}}}function Il(e,t,n,r=!1){if(!t.proposalFile||!t.proposalRevision)throw new Error(kl);let s=El(e,{...t,proposalFile:t.proposalFile});if(!s)throw new Error(Ho);pr(s,()=>{let o=Al(e,t,s);if(n==="reject"){if(!o.available&&!o.rejectable)throw new Error(o.reason);gr(s)}else{if(!o.available)throw new Error(o.reason);if(!o.verified&&!r)throw new Error("Confirm that you want to accept a proposal without successful proof replay.");ur(e,s)}},()=>{throw new Error(Rl(e,s))})}import eh from"node:http";import Ct from"node:path";function Pl(){return`<!DOCTYPE html>
362
362
  <html lang="en">
363
363
  <head>
364
364
  <meta charset="utf-8">
@@ -445,7 +445,7 @@ ${e.prompt}`,e)),i.status==="error"&&!e.finished())throw new Error(`Cursor: ${i.
445
445
  </head>
446
446
  <body>
447
447
  <header>
448
- ${Ys(26)}
448
+ ${In(26)}
449
449
  <div><div class="name" id="title">Saffron replay</div><div class="sub" id="subtitle"></div></div>
450
450
  <span class="status" id="status"></span>
451
451
  <label id="scenarioPicker" hidden>Scenario <select id="scenarioSelect" aria-label="Scenario"></select></label>
@@ -1056,59 +1056,171 @@ ${e.prompt}`,e)),i.status==="error"&&!e.finished())throw new Error(`Cursor: ${i.
1056
1056
  })();
1057
1057
  </script>
1058
1058
  </body>
1059
- </html>`}var _t=class extends Error{},wl="This trace is gone, or a later run replaced it (one that stopped before writing its report). Run the scenario again to replay it.",Lg=4;function jo(e,t){let n=It.join(e,t.file);try{on.accessSync(n,on.constants.R_OK)}catch(s){let o=s.code;if(o==="ENOENT"||o==="ENOTDIR")return;throw s}if(!at(It.join(e,".saffron","artifacts"),n))throw new Error(`${t.file} is not a file under .saffron/artifacts`);let r=on.readFileSync(n);return t.sha256&&Oe(r)!==t.sha256?void 0:r}function Ug(){let e=Cr.resolve("playwright-core/package.json");return It.join(It.dirname(e),"lib","vite","traceViewer")}var rt={".html":"text/html; charset=utf-8",".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".json":"application/json; charset=utf-8",".svg":"image/svg+xml",".png":"image/png",".jpeg":"image/jpeg",".jpg":"image/jpeg",".ttf":"font/ttf",".webmanifest":"application/manifest+json",".zip":"application/zip"},Bg=new Set([".png",".jpeg",".jpg"]),Oo={"Content-Security-Policy":"sandbox","X-Content-Type-Options":"nosniff"};async function Sl(e){let t=e.entries?[...e.entries]:[{scenario:e.scenario,traces:[{file:e.traceFile,data:e.traceData,label:e.label??"Execution",proof:e.proof,steps:e.scenario.steps,uniques:e.scenario.uniques},...(e.additionalTraces??[]).map(w=>({...w,proof:w.proof??!0}))]}],n=e.start??0;if(!t[n])throw new Error("No scenario to replay");let r=w=>{let x=mn(w),y=Po(x.map(h=>({name:h.name,get data(){return h.read()}})));return{zip:w,model:y,resources:new Map(x.filter(h=>h.name.includes("/")).map(h=>[h.name,h.read]))}},s=w=>{try{return w.data?w.data:!w.read&&!on.existsSync(w.file)?new _t(wl):(w.read?w.read():on.readFileSync(w.file))??new _t(wl)}catch(x){return x instanceof _t?x:new _t(`This trace cannot be read: ${x instanceof Error?x.message:String(x)}`)}},o=[];{let w=t[n],x=w.traces.map(s),y=w.traces.map((h,S)=>({t:h,data:x[S]})).filter(({data:h},S)=>S===0||Buffer.isBuffer(h));for(let[h,S]of w.traces.entries()){let $=x[h];h>0&&!Buffer.isBuffer($)&&o.push({file:S.file,reason:$.message})}t[n]={...w,traces:y.map(({t:h,data:S})=>({...h,data:Buffer.isBuffer(S)?S:void 0,read:Buffer.isBuffer(S)?void 0:()=>{throw S}}))}}let i=new Map,a=(w,x)=>{let y=`${w}:${x}`,h=i.get(y);if(h)i.delete(y);else{let S=s(t[w].traces[x]);if(!Buffer.isBuffer(S))throw S;h=r(S);let $=[...i.keys()].filter(u=>{let[g,R]=u.split(":").map(Number),E=t[g].traces[R];return g!==n&&(E.data!==void 0||E.checked===!0)});w!==n&&$.length>=Lg&&i.delete($[0])}return i.set(y,h),h},c=new Map,l=(w,x)=>{let y=t[w].scenario.screenshots?.find($=>$.file===x);if(!y)return;let h=`${w}:${x}`;if(c.has(h))return c.get(h);let S=zt(e.projectRoot,y);return(w===n||!y.sha256)&&c.set(h,S),S};try{a(n,0)}catch(w){if(t.length===1)throw w}for(let w of t[n].scenario.screenshots??[])l(n,w.file);let d=Ug(),f=yl(),m=bl(32).toString("hex"),p=bl(8).toString("hex"),b=t.map(w=>({feature:w.scenario.feature,scenario:w.scenario.scenario,status:w.scenario.status})),v=Dg.createServer((w,x)=>{let y=(h,S,$,u={})=>{x.writeHead(h,{"Content-Type":$,"Cache-Control":"no-store",...u}),x.end(S)};try{let h=w.headers.host??"";if(!/^(?:127\.0\.0\.1|localhost)(?::\d+)?$/i.test(h))return y(403,"Invalid host","text/plain");let S=new URL(w.url??"/",`http://${h}`),$=Number(S.searchParams.get("scenario")??n),u=Number.isInteger($)?t[$]:void 0,g=Number(S.searchParams.get("session")??"0"),R=u&&Number.isInteger(g)?u.traces[g]:void 0,E=()=>y(404,u?"No such trace session":"No such scenario","text/plain");if(S.pathname.startsWith("/api/review/")){if(w.method!=="POST")return y(405,"Use POST","text/plain");if(w.headers.origin!==`http://${h}`||w.headers["x-saffron-review"]!==m)return y(403,"Review must come from this replay page","text/plain");if(!u||!R)return E();let I=S.pathname.slice(12);if(I!=="accept"&&I!=="reject")return y(404,"Unknown decision","text/plain");try{return ml(e.projectRoot,u.scenario,I,w.headers["x-saffron-unverified"]==="yes"),y(200,JSON.stringify({decision:I}),rt[".json"])}catch(A){return y(409,JSON.stringify({error:A instanceof Error?A.message:String(A)}),rt[".json"])}}if(w.method!=="GET"&&w.method!=="HEAD")return y(405,"Use GET","text/plain");if(S.pathname==="/"||S.pathname==="/index.html")return y(200,f,rt[".html"]);if(S.pathname==="/api/replay"){let I=u?$:n,A=R?g:0,P=t[I],_=P.traces[A],j={scenarios:b,scenarioIndex:I,sessions:P.traces.map(L=>L.label),session:A,proof:_.proof===!0,instance:p},F;try{F=a(I,A).model}catch(L){let T=L instanceof _t?L.message:`This trace cannot be read: ${L instanceof Error?L.message:String(L)}`;return y(200,JSON.stringify({...j,scenario:P.scenario,unavailable:T}),rt[".json"])}let C=hl(e.projectRoot,P.scenario);return y(200,JSON.stringify({...j,scenario:{...P.scenario,steps:_.steps,uniques:_.uniques},trace:F,review:C,reviewToken:m}),rt[".json"])}if(S.pathname.startsWith("/artifact/")){let I=S.searchParams.get("v");if(I!==null&&I!==p)return y(410,"This replay was restarted. Reload the page.","text/plain");if(!u||!R)return E();if(S.pathname==="/artifact/trace.zip")return y(200,a($,g).zip,rt[".zip"],Oo);if(S.pathname.startsWith("/artifact/resource/")){let A=decodeURIComponent(S.pathname.slice(19)),P=a($,g).resources,_=(P.get(A)??(A.includes("/")?void 0:P.get(`resources/${A}`)))?.();if(!_)return y(404,"no such resource","text/plain");let j=It.extname(S.pathname).toLowerCase();return y(200,_,Bg.has(j)?rt[j]:"application/octet-stream",Oo)}if(S.pathname.startsWith("/artifact/screenshot/")){let A=l($,decodeURIComponent(S.pathname.slice(21)));return A?y(200,A,rt[".jpg"],Oo):y(404,"no such file","text/plain")}}if(S.pathname==="/trace/ping")return y(200,"ok","text/plain");if(S.pathname.startsWith("/trace/")){let I=S.pathname.slice(7)||"index.html",A=It.join(d,I);if(!at(d,A))return y(404,"not found","text/plain");let P=It.extname(A).toLowerCase(),_=on.readFileSync(A);return x.writeHead(200,{"Content-Type":rt[P]??"application/octet-stream","Service-Worker-Allowed":"/trace/"}),x.end(_)}y(404,"not found","text/plain")}catch{x.headersSent?x.destroy():y(400,"Invalid request","text/plain")}});return await new Promise((w,x)=>{v.once("error",x),v.listen(e.port??0,"127.0.0.1",()=>w())}),{url:`http://127.0.0.1:${v.address().port}/`,skipped:o,close:()=>new Promise(w=>v.close(()=>w()))}}import xl from"node:fs";import qr from"node:path";function Hr(e){return JSON.stringify([e.role??null,e.name??null,e.nameRegex??null,e.label??null,e.text??null,e.testId??null,e.placeholder??null,e.frame??null])}function vl(e,t){let n=new Map,r=new Set,s=new Map,o=Math.min(e.steps.length,t.steps.length);for(let a=0;a<o;a++){if(e.steps[a].kind==="assertion"||t.steps[a].kind==="assertion"||e.steps[a].gherkin!==t.steps[a].gherkin)continue;let c=e.steps[a].actions,l=t.steps[a].actions;if(c.length===l.length)for(let d=0;d<c.length;d++){let f=c[d],m=l[d];if(f.action!==m.action||Se.has(f.action)||!f.target||!m.target)continue;let p=Hr(f.target),b=JSON.stringify(m.target);s.has(p)&&s.get(p)!==b&&r.add(p),s.set(p,b),p!==Hr(m.target)&&n.set(p,{from:f.target,to:m.target})}}let i=[...n.entries()];return{changes:i.filter(([a])=>!r.has(a)).map(([,a])=>a),ambiguous:i.filter(([a])=>r.has(a)).map(([,a])=>a.from)}}function zg(e){let t=qr.join(e,".saffron","cache");if(!xl.existsSync(t))return[];let n=[];for(let r of xl.readdirSync(t,{recursive:!0})){let s=String(r);s.endsWith(".json")&&n.push(qr.join(t,s))}return n}function $l(e,t,n,r){if(t.length===0)return[];let s=new Map,o=new Set;for(let a of t){let c=Hr(a.from),l=s.get(c);l&&JSON.stringify(l.to)!==JSON.stringify(a.to)&&o.add(c),s.set(c,a)}for(let a of o)s.delete(a);let i=[];for(let a of zg(e)){if(qr.resolve(a)===qr.resolve(n))continue;let c=Ue(a);if(!c)continue;let l=[],d=!1;for(let f of c.steps){if(f.kind==="assertion")continue;let m=0;for(let p of f.actions){if(!p.target||Se.has(p.action))continue;let b=s.get(Hr(p.target));b&&(m++,r||(p.target=structuredClone(b.to),d=!0))}m>0&&l.push({gherkin:f.gherkin,count:m})}l.length>0&&(d&&Lt(a,c),i.push({file:a,scenario:c.scenario,feature:c.feature,steps:l}))}return i}function Jr(e){return e.role?`${e.role} "${e.name??e.nameRegex??""}"`:e.label?`label "${e.label}"`:e.text?`text "${e.text}"`:e.testId?`testId "${e.testId}"`:(e.fallbackSelectors??[]).join(",")||"(empty target)"}import kl from"node:fs";import Gr from"node:path";function Tl(e,t,n,r,s){let o=he(Gr.join(e,t),e,s).find(c=>c.name===n);if(!o)return`scenario "${n}" is no longer in ${t}`;let i=o.steps.map(c=>c.text);if(i.length!==r.length)return`the scenario has ${i.length} steps now and had ${r.length} when this was recorded`;let a=i.findIndex((c,l)=>c!==r[l]);return a<0?void 0:`step ${a+1} now reads "${i[a]}" and was recorded as "${r[a]}"`}function Rl(e,t,n,r,s,o){let i={applied:[],skipped:[],appliedIndices:[]},a=he(Gr.join(e,t),e,s).find(f=>f.name===n);if(!a){for(let f of r)i.skipped.push({index:f.index,reason:`scenario "${n}" not found in ${t}`});return i}let c=new Map;for(let f of r){let m=a.steps[f.index]?.source;if(!m||m.fromBackground)continue;let p=`${m.file}:${m.line}`;c.set(p,(c.get(p)??new Set).add(f.text))}let l=new Map,d=new Set;for(let f of r){let m=a.steps[f.index];if(!m?.source){i.skipped.push({index:f.index,reason:`no step at index ${f.index}`});continue}if(o&&o[f.index]!==m.text){i.skipped.push({index:f.index,reason:`step ${f.index+1} now reads "${m.text}", not the "${o[f.index]??"(nothing)"}" this edit was written for`});continue}let{file:p,line:b,fromStepSet:v,fromBackground:k}=m.source;if(k){i.skipped.push({index:f.index,reason:`"${m.text}" is a Background step shared across scenarios; edit it manually`});continue}let w=`${p}:${b}`,x=c.get(w);if(x.size>1){i.skipped.push({index:f.index,reason:`${w}${v?` (StepSet "${v}")`:""} is reached by several steps and the edits disagree: ${[...x].map(u=>`"${u}"`).join(" vs ")}. Decide the wording in the definition by hand`});continue}if(d.has(w)){i.appliedIndices.push(f.index);continue}let y=l.get(p);y||(y=kl.readFileSync(Gr.join(e,p),"utf8").split(`
1060
- `),l.set(p,y));let h=y[b-1],S=h?.match(/^(\s*)(\S+)\s+(.*)$/);if(!S||S[3]!==m.text){i.skipped.push({index:f.index,reason:`${p}:${b} no longer matches the recorded step text`});continue}let $=`${S[1]}${S[2]} ${f.text}`;y[b-1]=$,d.add(w),i.appliedIndices.push(f.index),i.applied.push({file:p,line:b,from:h.trim(),to:$.trim(),fromStepSet:v})}for(let[f,m]of l)i.applied.some(p=>p.file===f)&&kl.writeFileSync(Gr.join(e,f),m.join(`
1061
- `));return i}var Fn=class extends Error{},No=new Set(["and","or","not","(",")"]);function qg(e){let t=[...e.matchAll(/[(),|]|[^\s(),|]+(?:\([^\s(),|]*\))*/g)].flatMap(([s])=>{let o=/^(and|or|not)\((.*)\)$/i.exec(s);return o?[o[1],"(",o[2],")"].filter(Boolean):[s]}),n=s=>s===","||s==="|",r=[];return t.forEach((s,o)=>{let i=r.at(-1),a=t[o+1];n(s)&&(i===void 0||n(i)||i==="("||a===void 0||n(a)||a===")")||r.push(s)}),r.map(s=>n(s)?"or":No.has(s.toLowerCase())?s.toLowerCase():s.startsWith("@")?s:`@${s}`)}function Hg(e){let t=qg(e),n=0,r=()=>t[n],s=d=>{throw new Fn(`--filter "${e}": ${d}`)},o=!t.some(d=>d==="and"||d==="not"||d==="("||d===")"),i=()=>{let d=a();for(;r()==="or"||o&&r()!==void 0&&r()!==")";){r()==="or"&&n++;let f=a(),m=d;d=p=>m(p)||f(p)}return d},a=()=>{let d=c();for(;r()==="and";){n++;let f=c(),m=d;d=p=>m(p)&&f(p)}return d},c=()=>{let d=r();if(d===void 0)return s("expected a tag at the end");if(d==="not"){n++;let f=c();return m=>!f(m)}if(d==="("){n++;let f=i();return r()!==")"?s("missing a closing parenthesis"):(n++,f)}return No.has(d)?s(`expected a tag, found "${d}"`):(n++,f=>f.has(d))},l=i();return n<t.length?s(No.has(t[n])?`unexpected "${t[n]}"`:`unexpected "${t[n]}": with and, not or parentheses, join every tag with and/or`):l}function El(e){let t=(e===void 0?[]:Array.isArray(e)?e:[e]).map(r=>r.trim()).filter(Boolean);if(t.length===0)return;let n=t.map(Hg);return{text:t.length===1?t[0]:t.map(r=>`(${r})`).join(" or "),matches:r=>{let s=new Set(r);return n.some(o=>o(s))}}}function Al(e,t){return t?e.filter(n=>t.matches(n.tags)):e}function _l(e,t){return[...t??[],e]}import{spawn as Jg}from"node:child_process";function Fo(e,t){if(e==null)return[];let n=Array.isArray(e)?e:[e];for(let r of n)if(typeof r!="string"||r.trim()==="")throw new Error(`"${t}" in saffron.config.json must be a command, or a list of commands, e.g. "${t}": "npm run seed"`);return n}function Gg(e,t,n){let r=Date.now();return new Promise(s=>{let o=process.platform!=="win32",i=Jg(t,{cwd:n.cwd,shell:!0,detached:o,env:{...process.env,...n.env},stdio:["ignore",process.stderr,process.stderr]}),a=!1,c=!1,l=()=>{try{o&&i.pid?process.kill(-i.pid,"SIGKILL"):i.kill("SIGKILL")}catch{}},d=setTimeout(()=>{a=!0,l()},n.timeoutMs),f=()=>{c=!0,l()};n.signal?.addEventListener("abort",f,{once:!0});let m=p=>{clearTimeout(d),n.signal?.removeEventListener("abort",f),s({phase:e,command:t,exitCode:p,durationMs:Date.now()-r,...a?{timedOut:a}:{},...c?{cancelled:c}:{}})};i.on("error",()=>m(null)),i.on("exit",p=>m(a||c?null:p))})}var Wr=e=>e.exitCode!==0;async function Mo(e,t,n){let r=[];for(let s of t){if(n.signal?.aborted)break;console.error(`saffron ${e}: ${s}`);let o=await Gg(e,s,n);if(r.push(o),o.cancelled||Wr(o)&&(console.error(`saffron ${e} failed: ${Do(o,n.timeoutMs)}`),e==="setup"))break}return r}function Do(e,t){return e.cancelled?`"${e.command}" was interrupted`:e.timedOut?`"${e.command}" was stopped after ${Math.round(t/1e3)}s (hookTimeoutMs)`:e.exitCode===null?`"${e.command}" could not be started`:`"${e.command}" exited with code ${e.exitCode}`}import{spawnSync as Bl}from"node:child_process";import Ie from"node:fs";import ql from"node:os";import Q from"node:path";import{spawnSync as Il}from"node:child_process";import Ol from"node:path";var Wg=["github","azure"],Uo={github:"GitHub Actions",azure:"Azure Pipelines"};function an(e){if(e.startsWith("@")||/[\x00-\x1f\x7f]/.test(e))throw new U(`${JSON.stringify(e)} is not a branch name Saffron passes on`);return e}function Vg(e){let t=e.replace(/(\\*)"/g,'$1$1\\"').replace(/(\\*)$/,"$1$1");return t=`"${t}"`,t=t.replace(/[()%!^"<>&|;, ]/g,"^$&"),t.replace(/[()%!^"<>&|;, ]/g,"^$&")}function we(e,t,n,r,s){let o=process.platform==="win32"&&e==="az",i=o?Il("az.cmd",t.map(Vg),{cwd:n,env:r,input:s,encoding:"utf8",shell:!0,windowsVerbatimArguments:!0,timeout:6e5,maxBuffer:64*1024*1024}):Il(e,t,{cwd:n,env:r,input:s,encoding:"utf8",timeout:6e5,maxBuffer:64*1024*1024}),a=i.error?.code,c=a==="ENOENT"||o&&i.status===9009;return{status:i.status,stdout:(i.stdout??"").trim(),stderr:(i.stderr??"").trim(),missing:c,timedOut:a==="ETIMEDOUT"}}var Mn=e=>`${e.stderr}
1062
- ${e.stdout}`,Lo=(e,t)=>Mn(e).split(`
1063
- `).map(n=>n.trim()).find(n=>n&&!/^WARNING:/.test(n))??t;function jl(e){return e.missing?{kind:"missing-tool",message:"the GitHub CLI (gh) is not installed here: install it from cli.github.com and sign in with gh auth login"}:e.timedOut?{kind:"failed",message:"gh did not finish within 10 minutes"}:/gh auth login|not logged in|authentication required/i.test(Mn(e))?{kind:"signed-out",message:"gh is not signed in: run gh auth login"}:/none of the git remotes|no git remotes|not a git repository/i.test(Mn(e))?{kind:"not-a-repository",message:Lo(e,"not a GitHub repository")}:{kind:"failed",message:Lo(e,`gh exited with ${e.status}`)}}function Vr(e){if(e.missing)return{kind:"missing-tool",message:"the Azure CLI (az) is not installed here: install it, add its azure-devops extension (az extension add --name azure-devops) and sign in (az login, or AZURE_DEVOPS_EXT_PAT)"};if(e.timedOut)return{kind:"failed",message:"az did not finish within 10 minutes"};let t=Mn(e);return/azure-devops|'pipelines' is (misspelled|not in)|not in the 'az' command group/i.test(t)&&!/login/i.test(t)?{kind:"missing-tool",message:"the Azure CLI has no azure-devops extension: az extension add --name azure-devops"}:/az login|az devops login|TF400813|not authorized|authentication failed|\b401\b/i.test(t)?{kind:"signed-out",message:"az is not signed in to Azure DevOps: run az login, or set AZURE_DEVOPS_EXT_PAT"}:/--organization|--org\b|--project|must be specified|could not detect/i.test(t)?{kind:"not-a-repository",message:"cannot tell which Azure DevOps organization and project: run this in a clone of an Azure Repos repository, or set ci.organization and ci.project in saffron.config.json"}:{kind:"failed",message:Lo(e,`az exited with ${e.status}`)}}function Nl(e,t,n){let r=n??t.ci?.provider;if(r!==void 0){if(!Wg.includes(r))throw new U(`unknown CI provider "${r}": use "github" or "azure"`);return r}return $r(e).some(s=>s.startsWith("dev.azure.com/"))?"azure":"github"}var Bo=e=>[...e.ci?.organization?["--organization",e.ci.organization]:[],...e.ci?.project?["--project",e.ci.project]:[]];function Kg(e,t){let n=we("git",["rev-parse","--abbrev-ref","HEAD"],e,t),r=n.status===0?n.stdout.trim():"";return r&&r!=="HEAD"?r:void 0}function Yg(e){let t=new Map;for(let[n,r]of Object.entries(An(e))){try{if(qe(Ol.join(e,n))!==r.revision)continue}catch{continue}r.from.run&&t.set(r.from.run,(t.get(r.from.run)??0)+1)}return t}function Fl(e,t=Date.now()){let n=e?t-Date.parse(e):NaN;if(!(n>=0))return;let r=Math.round(n/6e4);if(r<60)return`${r} min ago`;let s=Math.round(r/60);return s<48?`${s} h ago`:`${Math.round(s/24)} days ago`}var te=e=>typeof e=="string"&&e?e:void 0,Pl=e=>te(e)?.replace(/^refs\/(heads|tags)\//,""),Zg="databaseId,number,attempt,displayTitle,workflowName,status,conclusion,event,headBranch,headSha,createdAt,url";function Qg(e){let t=e.status==="completed"?"completed":e.status==="in_progress"?"in_progress":"queued";return{id:String(e.databaseId),name:te(e.workflowName)??"workflow",number:String(e.number??""),title:te(e.displayTitle),status:t,conclusion:t==="completed"?te(e.conclusion):void 0,event:te(e.event),branch:te(e.headBranch),commit:te(e.headSha),createdAt:te(e.createdAt),url:te(e.url),attempt:typeof e.attempt=="number"?e.attempt:void 0}}var Xg={completed:"completed",inProgress:"in_progress",cancelling:"in_progress"},eh={succeeded:"success",partiallySucceeded:"partial",failed:"failure",canceled:"cancelled"},th={individualCI:"push",batchedCI:"push",pullRequest:"pull_request",schedule:"schedule",manual:"manual"};function Ml(e){let t=e.triggerInfo??{};return Pl(t["pr.sourceBranch"])??Pl(e.sourceBranch)}function nh(e){let t=e.triggerInfo??{},n=Xg[String(e.status)]??"queued",r=te(e.result);return{id:String(e.id),name:te(e.definition?.name)??"pipeline",number:String(e.buildNumber??e.id),status:n,conclusion:n==="completed"&&r?eh[r]??r:void 0,event:te(e.reason)?th[String(e.reason)]??String(e.reason):void 0,branch:Ml(e),commit:te(t["pr.sourceSha"])??te(e.sourceVersion),createdAt:te(e.queueTime),url:te((e._links??{}).web?.href)}}function zo(e){try{let t=JSON.parse(e);return Array.isArray(t)?t.filter(n=>!!n&&typeof n=="object"):void 0}catch{return}}function rh(e){return new Set(e.map(t=>t.startsWith("dev.azure.com/")?t.slice(t.lastIndexOf("/_git/")+6):t.slice(t.indexOf("/")+1)))}function sh(e,t,n,r,s,o,i=Math.min(Math.max(o,50),500)){for(let a=i;;a=Math.min(a*2,500)){let c=we("az",["pipelines","runs","list","--top",String(a),...r,"--output","json",...Bo(t)],e,n);if(c.missing||c.status!==0)return Vr(c);let l=zo(c.stdout);if(!l)return{kind:"failed",message:"az answered with something other than a list of runs"};let d=l.filter(s);if(d.length>=o||l.length<a||a>=500)return d}}function Dl(e,t,n={}){let r=n.env??process.env,s=Nl(e,t,n.provider),o=n.branch??Kg(e,r),i=o===void 0?void 0:an(o),a=n.limit??20,c;if(s==="github"){let d=we("gh",["run","list","--limit",String(a),"--json",Zg,...i?["--branch",i]:[]],e,r);if(d.missing||d.status!==0)return{provider:s,branch:i,runs:[],problem:jl(d)};let f=zo(d.stdout);if(!f)return{provider:s,branch:i,runs:[],problem:{kind:"failed",message:"gh answered with something other than a list of runs"}};c=f.map(Qg)}else{let d=rh($r(e)),f=v=>{let k=te(v.repository?.name)?.toLowerCase();return d.size===0||!k||d.has(k)},m=v=>f(v)&&(!i||Ml(v)===i),p=i?[["--branch",`refs/heads/${i}`],["--branch",`refs/tags/${i}`],["--reason","pullRequest"]]:[[]],b=new Map;for(let v of p){let k=sh(e,t,r,v,m,a);if(!Array.isArray(k))return{provider:s,branch:i,runs:[],problem:k};for(let w of k)b.set(String(w.id),w)}c=[...b.values()].sort((v,k)=>(Date.parse(te(k.queueTime)??"")||0)-(Date.parse(te(v.queueTime)??"")||0)).slice(0,a).map(nh)}let l=Yg(e);return{provider:s,branch:i,runs:c.map(d=>({...d,pending:l.get(`${s}:${d.id}`)??0}))}}var Cl=e=>`run ${e} has no Saffron bundle: none was uploaded, or it expired. Its workflow needs saffron export and an upload of saffron-bundle* (see the guide, section 19)`;function Ll(e,t,n,r={}){if(!/^\d+$/.test(n))throw new U(`--run takes the run's id, a number as saffron runs lists it, not "${n}"`);let s=r.env??process.env,o=Nl(e,t,r.provider),{dir:i,remove:a}=po(e);try{if(o==="github"){let c=we("gh",["run","download",n,"--pattern","saffron-bundle*","--dir",i],e,s);if(c.missing||c.status!==0)throw new U(/no (valid )?artifact/i.test(Mn(c))?Cl(n):jl(c).message)}else{let c=Bo(t),l=we("az",["pipelines","runs","artifact","list","--run-id",n,"--output","json",...c],e,s);if(l.missing||l.status!==0)throw new U(Vr(l).message);let d=(zo(l.stdout)??[]).map(f=>f.name).filter(f=>typeof f=="string"&&/^saffron-bundle[\w.-]*$/.test(f));if(d.length===0)throw new U(Cl(n));for(let f of d){let m=we("az",["pipelines","runs","artifact","download","--run-id",n,"--artifact-name",f,"--path",Ol.join(i,f),...c],e,s);if(m.missing||m.status!==0)throw new U(`cannot download ${f}: ${Vr(m).message}`)}}return{provider:o,dir:i,remove:a}}catch(c){throw a(),c}}function Ul(e,t,n,r){let s=we("az",["repos","pr","create","--draft","true","--source-branch",`refs/heads/${an(n.branch)}`,"--target-branch",`refs/heads/${an(n.base)}`,"--title",n.title,"--description",`@${n.bodyFile}`,"--output","json",...Bo(t)],e,r);if(s.missing||s.status!==0)return{problem:`az repos pr create failed: ${Vr(s).message}`};try{let o=JSON.parse(s.stdout),i=o.repository?.webUrl;return{url:i&&o.pullRequestId!==void 0?`${i}/pullrequest/${o.pullRequestId}`:void 0}}catch{return{}}}function zl(e,t,n){if(e.missing)throw new U(`${n}: ${t} is not installed here`);if(e.timedOut)throw new U(`${n}: ${t} did not finish within 10 minutes`);if(e.status!==0)throw new U(`${n}: ${(e.stderr||e.stdout||`exit ${e.status}`).split(`
1064
- `).slice(-3).join(" ")}`);return e.stdout}function oh(e,t){let n=i=>we("git",["config",i],e,t).stdout,r=t.GIT_AUTHOR_NAME||n("user.name"),s=t.GIT_AUTHOR_EMAIL||n("user.email"),o=t.GITHUB_ACTIONS==="true"?{name:"github-actions[bot]",email:"41898282+github-actions[bot]@users.noreply.github.com"}:{name:"Saffron",email:"saffron@localhost"};return{...t,GIT_AUTHOR_NAME:r||o.name,GIT_AUTHOR_EMAIL:s||o.email,GIT_COMMITTER_NAME:t.GIT_COMMITTER_NAME||r||o.name,GIT_COMMITTER_EMAIL:t.GIT_COMMITTER_EMAIL||s||o.email}}var Kr=(e,t)=>`${e} ${t}${e===1?"":"s"}`,Dn=e=>`\`${e.replaceAll("`","'")}\``,qo=e=>{let t=e.startsWith("-")?`./${e}`:e;return/^[\w./-]+$/.test(t)?t:`'${t.replaceAll("'","'\\''")}'`};function Ho(e,t=""){let n=Math.max(0,...[...e.matchAll(/`+/g)].map(s=>s[0].length)),r="`".repeat(Math.max(3,n+1));return`${r}${t}
1059
+ </html>`}var Pt=class extends Error{},Ol="This trace is gone, or a later run replaced it (one that stopped before writing its report). Run the scenario again to replay it.",th=4;function Jo(e,t){let n=Ct.join(e,t.file);try{dn.accessSync(n,dn.constants.R_OK)}catch(s){let o=s.code;if(o==="ENOENT"||o==="ENOTDIR")return;throw s}if(!lt(Ct.join(e,".saffron","artifacts"),n))throw new Error(`${t.file} is not a file under .saffron/artifacts`);let r=dn.readFileSync(n);return t.sha256&&je(r)!==t.sha256?void 0:r}function nh(){let e=Lr.resolve("playwright-core/package.json");return Ct.join(Ct.dirname(e),"lib","vite","traceViewer")}var st={".html":"text/html; charset=utf-8",".js":"text/javascript; charset=utf-8",".css":"text/css; charset=utf-8",".json":"application/json; charset=utf-8",".svg":"image/svg+xml",".png":"image/png",".jpeg":"image/jpeg",".jpg":"image/jpeg",".ttf":"font/ttf",".webmanifest":"application/manifest+json",".zip":"application/zip"},rh=new Set([".png",".jpeg",".jpg"]),qo={"Content-Security-Policy":"sandbox","X-Content-Type-Options":"nosniff"};async function jl(e){let t=e.entries?[...e.entries]:[{scenario:e.scenario,traces:[{file:e.traceFile,data:e.traceData,label:e.label??"Execution",proof:e.proof,steps:e.scenario.steps,uniques:e.scenario.uniques},...(e.additionalTraces??[]).map(w=>({...w,proof:w.proof??!0}))]}],n=e.start??0;if(!t[n])throw new Error("No scenario to replay");let r=w=>{let x=vn(w),y=zo(x.map(u=>({name:u.name,get data(){return u.read()}})));return{zip:w,model:y,resources:new Map(x.filter(u=>u.name.includes("/")).map(u=>[u.name,u.read]))}},s=w=>{try{return w.data?w.data:!w.read&&!dn.existsSync(w.file)?new Pt(Ol):(w.read?w.read():dn.readFileSync(w.file))??new Pt(Ol)}catch(x){return x instanceof Pt?x:new Pt(`This trace cannot be read: ${x instanceof Error?x.message:String(x)}`)}},o=[];{let w=t[n],x=w.traces.map(s),y=w.traces.map((u,v)=>({t:u,data:x[v]})).filter(({data:u},v)=>v===0||Buffer.isBuffer(u));for(let[u,v]of w.traces.entries()){let S=x[u];u>0&&!Buffer.isBuffer(S)&&o.push({file:v.file,reason:S.message})}t[n]={...w,traces:y.map(({t:u,data:v})=>({...u,data:Buffer.isBuffer(v)?v:void 0,read:Buffer.isBuffer(v)?void 0:()=>{throw v}}))}}let i=new Map,a=(w,x)=>{let y=`${w}:${x}`,u=i.get(y);if(u)i.delete(y);else{let v=s(t[w].traces[x]);if(!Buffer.isBuffer(v))throw v;u=r(v);let S=[...i.keys()].filter(h=>{let[g,T]=h.split(":").map(Number),E=t[g].traces[T];return g!==n&&(E.data!==void 0||E.checked===!0)});w!==n&&S.length>=th&&i.delete(S[0])}return i.set(y,u),u},c=new Map,l=(w,x)=>{let y=t[w].scenario.screenshots?.find(S=>S.file===x);if(!y)return;let u=`${w}:${x}`;if(c.has(u))return c.get(u);let v=Jt(e.projectRoot,y);return(w===n||!y.sha256)&&c.set(u,v),v};try{a(n,0)}catch(w){if(t.length===1)throw w}for(let w of t[n].scenario.screenshots??[])l(n,w.file);let d=nh(),f=Pl(),m=Cl(32).toString("hex"),p=Cl(8).toString("hex"),b=t.map(w=>({feature:w.scenario.feature,scenario:w.scenario.scenario,status:w.scenario.status})),$=eh.createServer((w,x)=>{let y=(u,v,S,h={})=>{x.writeHead(u,{"Content-Type":S,"Cache-Control":"no-store",...h}),x.end(v)};try{let u=w.headers.host??"";if(!/^(?:127\.0\.0\.1|localhost)(?::\d+)?$/i.test(u))return y(403,"Invalid host","text/plain");let v=new URL(w.url??"/",`http://${u}`),S=Number(v.searchParams.get("scenario")??n),h=Number.isInteger(S)?t[S]:void 0,g=Number(v.searchParams.get("session")??"0"),T=h&&Number.isInteger(g)?h.traces[g]:void 0,E=()=>y(404,h?"No such trace session":"No such scenario","text/plain");if(v.pathname.startsWith("/api/review/")){if(w.method!=="POST")return y(405,"Use POST","text/plain");if(w.headers.origin!==`http://${u}`||w.headers["x-saffron-review"]!==m)return y(403,"Review must come from this replay page","text/plain");if(!h||!T)return E();let _=v.pathname.slice(12);if(_!=="accept"&&_!=="reject")return y(404,"Unknown decision","text/plain");try{return Il(e.projectRoot,h.scenario,_,w.headers["x-saffron-unverified"]==="yes"),y(200,JSON.stringify({decision:_}),st[".json"])}catch(A){return y(409,JSON.stringify({error:A instanceof Error?A.message:String(A)}),st[".json"])}}if(w.method!=="GET"&&w.method!=="HEAD")return y(405,"Use GET","text/plain");if(v.pathname==="/"||v.pathname==="/index.html")return y(200,f,st[".html"]);if(v.pathname==="/api/replay"){let _=h?S:n,A=T?g:0,C=t[_],I=C.traces[A],j={scenarios:b,scenarioIndex:_,sessions:C.traces.map(P=>P.label),session:A,proof:I.proof===!0,instance:p},M;try{M=a(_,A).model}catch(P){let R=P instanceof Pt?P.message:`This trace cannot be read: ${P instanceof Error?P.message:String(P)}`;return y(200,JSON.stringify({...j,scenario:C.scenario,unavailable:R}),st[".json"])}let z=_l(e.projectRoot,C.scenario);return y(200,JSON.stringify({...j,scenario:{...C.scenario,steps:I.steps,uniques:I.uniques},trace:M,review:z,reviewToken:m}),st[".json"])}if(v.pathname.startsWith("/artifact/")){let _=v.searchParams.get("v");if(_!==null&&_!==p)return y(410,"This replay was restarted. Reload the page.","text/plain");if(!h||!T)return E();if(v.pathname==="/artifact/trace.zip")return y(200,a(S,g).zip,st[".zip"],qo);if(v.pathname.startsWith("/artifact/resource/")){let A=decodeURIComponent(v.pathname.slice(19)),C=a(S,g).resources,I=(C.get(A)??(A.includes("/")?void 0:C.get(`resources/${A}`)))?.();if(!I)return y(404,"no such resource","text/plain");let j=Ct.extname(v.pathname).toLowerCase();return y(200,I,rh.has(j)?st[j]:"application/octet-stream",qo)}if(v.pathname.startsWith("/artifact/screenshot/")){let A=l(S,decodeURIComponent(v.pathname.slice(21)));return A?y(200,A,st[".jpg"],qo):y(404,"no such file","text/plain")}}if(v.pathname==="/trace/ping")return y(200,"ok","text/plain");if(v.pathname.startsWith("/trace/")){let _=v.pathname.slice(7)||"index.html",A=Ct.join(d,_);if(!lt(d,A))return y(404,"not found","text/plain");let C=Ct.extname(A).toLowerCase(),I=dn.readFileSync(A);return x.writeHead(200,{"Content-Type":st[C]??"application/octet-stream","Service-Worker-Allowed":"/trace/"}),x.end(I)}y(404,"not found","text/plain")}catch{x.headersSent?x.destroy():y(400,"Invalid request","text/plain")}});return await new Promise((w,x)=>{$.once("error",x),$.listen(e.port??0,"127.0.0.1",()=>w())}),{url:`http://127.0.0.1:${$.address().port}/`,skipped:o,close:()=>new Promise(w=>$.close(()=>w()))}}import Nl from"node:fs";import Yr from"node:path";function Qr(e){return JSON.stringify([e.role??null,e.name??null,e.nameRegex??null,e.label??null,e.text??null,e.testId??null,e.placeholder??null,e.frame??null])}function Fl(e,t){let n=new Map,r=new Set,s=new Map,o=Math.min(e.steps.length,t.steps.length);for(let a=0;a<o;a++){if(e.steps[a].kind==="assertion"||t.steps[a].kind==="assertion"||e.steps[a].gherkin!==t.steps[a].gherkin)continue;let c=e.steps[a].actions,l=t.steps[a].actions;if(c.length===l.length)for(let d=0;d<c.length;d++){let f=c[d],m=l[d];if(f.action!==m.action||Se.has(f.action)||!f.target||!m.target)continue;let p=Qr(f.target),b=JSON.stringify(m.target);s.has(p)&&s.get(p)!==b&&r.add(p),s.set(p,b),p!==Qr(m.target)&&n.set(p,{from:f.target,to:m.target})}}let i=[...n.entries()];return{changes:i.filter(([a])=>!r.has(a)).map(([,a])=>a),ambiguous:i.filter(([a])=>r.has(a)).map(([,a])=>a.from)}}function sh(e){let t=Yr.join(e,".saffron","cache");if(!Nl.existsSync(t))return[];let n=[];for(let r of Nl.readdirSync(t,{recursive:!0})){let s=String(r);s.endsWith(".json")&&n.push(Yr.join(t,s))}return n}function Ml(e,t,n,r){if(t.length===0)return[];let s=new Map,o=new Set;for(let a of t){let c=Qr(a.from),l=s.get(c);l&&JSON.stringify(l.to)!==JSON.stringify(a.to)&&o.add(c),s.set(c,a)}for(let a of o)s.delete(a);let i=[];for(let a of sh(e)){if(Yr.resolve(a)===Yr.resolve(n))continue;let c=Be(a);if(!c)continue;let l=[],d=!1;for(let f of c.steps){if(f.kind==="assertion")continue;let m=0;for(let p of f.actions){if(!p.target||Se.has(p.action))continue;let b=s.get(Qr(p.target));b&&(m++,r||(p.target=structuredClone(b.to),d=!0))}m>0&&l.push({gherkin:f.gherkin,count:m})}l.length>0&&(d&&zt(a,c),i.push({file:a,scenario:c.scenario,feature:c.feature,steps:l}))}return i}function Zr(e){return e.role?`${e.role} "${e.name??e.nameRegex??""}"`:e.label?`label "${e.label}"`:e.text?`text "${e.text}"`:e.testId?`testId "${e.testId}"`:(e.fallbackSelectors??[]).join(",")||"(empty target)"}import Dl from"node:fs";import Xr from"node:path";function Ll(e,t,n,r,s){let o=me(Xr.join(e,t),e,s).find(c=>c.name===n);if(!o)return`scenario "${n}" is no longer in ${t}`;let i=o.steps.map(c=>c.text);if(i.length!==r.length)return`the scenario has ${i.length} steps now and had ${r.length} when this was recorded`;let a=i.findIndex((c,l)=>c!==r[l]);return a<0?void 0:`step ${a+1} now reads "${i[a]}" and was recorded as "${r[a]}"`}function Ul(e,t,n,r,s,o){let i={applied:[],skipped:[],appliedIndices:[]},a=me(Xr.join(e,t),e,s).find(f=>f.name===n);if(!a){for(let f of r)i.skipped.push({index:f.index,reason:`scenario "${n}" not found in ${t}`});return i}let c=new Map;for(let f of r){let m=a.steps[f.index]?.source;if(!m||m.fromBackground)continue;let p=`${m.file}:${m.line}`;c.set(p,(c.get(p)??new Set).add(f.text))}let l=new Map,d=new Set;for(let f of r){let m=a.steps[f.index];if(!m?.source){i.skipped.push({index:f.index,reason:`no step at index ${f.index}`});continue}if(o&&o[f.index]!==m.text){i.skipped.push({index:f.index,reason:`step ${f.index+1} now reads "${m.text}", not the "${o[f.index]??"(nothing)"}" this edit was written for`});continue}let{file:p,line:b,fromStepSet:$,fromBackground:k}=m.source;if(k){i.skipped.push({index:f.index,reason:`"${m.text}" is a Background step shared across scenarios; edit it manually`});continue}let w=`${p}:${b}`,x=c.get(w);if(x.size>1){i.skipped.push({index:f.index,reason:`${w}${$?` (StepSet "${$}")`:""} is reached by several steps and the edits disagree: ${[...x].map(h=>`"${h}"`).join(" vs ")}. Decide the wording in the definition by hand`});continue}if(d.has(w)){i.appliedIndices.push(f.index);continue}let y=l.get(p);y||(y=Dl.readFileSync(Xr.join(e,p),"utf8").split(`
1060
+ `),l.set(p,y));let u=y[b-1],v=u?.match(/^(\s*)(\S+)\s+(.*)$/);if(!v||v[3]!==m.text){i.skipped.push({index:f.index,reason:`${p}:${b} no longer matches the recorded step text`});continue}let S=`${v[1]}${v[2]} ${f.text}`;y[b-1]=S,d.add(w),i.appliedIndices.push(f.index),i.applied.push({file:p,line:b,from:u.trim(),to:S.trim(),fromStepSet:$})}for(let[f,m]of l)i.applied.some(p=>p.file===f)&&Dl.writeFileSync(Xr.join(e,f),m.join(`
1061
+ `));return i}var zn=class extends Error{},Wo=new Set(["and","or","not","(",")"]);function oh(e){let t=[...e.matchAll(/[(),|]|[^\s(),|]+(?:\([^\s(),|]*\))*/g)].flatMap(([s])=>{let o=/^(and|or|not)\((.*)\)$/i.exec(s);return o?[o[1],"(",o[2],")"].filter(Boolean):[s]}),n=s=>s===","||s==="|",r=[];return t.forEach((s,o)=>{let i=r.at(-1),a=t[o+1];n(s)&&(i===void 0||n(i)||i==="("||a===void 0||n(a)||a===")")||r.push(s)}),r.map(s=>n(s)?"or":Wo.has(s.toLowerCase())?s.toLowerCase():s.startsWith("@")?s:`@${s}`)}function ih(e){let t=oh(e),n=0,r=()=>t[n],s=d=>{throw new zn(`--filter "${e}": ${d}`)},o=!t.some(d=>d==="and"||d==="not"||d==="("||d===")"),i=()=>{let d=a();for(;r()==="or"||o&&r()!==void 0&&r()!==")";){r()==="or"&&n++;let f=a(),m=d;d=p=>m(p)||f(p)}return d},a=()=>{let d=c();for(;r()==="and";){n++;let f=c(),m=d;d=p=>m(p)&&f(p)}return d},c=()=>{let d=r();if(d===void 0)return s("expected a tag at the end");if(d==="not"){n++;let f=c();return m=>!f(m)}if(d==="("){n++;let f=i();return r()!==")"?s("missing a closing parenthesis"):(n++,f)}return Wo.has(d)?s(`expected a tag, found "${d}"`):(n++,f=>f.has(d))},l=i();return n<t.length?s(Wo.has(t[n])?`unexpected "${t[n]}"`:`unexpected "${t[n]}": with and, not or parentheses, join every tag with and/or`):l}function Bl(e){let t=(e===void 0?[]:Array.isArray(e)?e:[e]).map(r=>r.trim()).filter(Boolean);if(t.length===0)return;let n=t.map(ih);return{text:t.length===1?t[0]:t.map(r=>`(${r})`).join(" or "),matches:r=>{let s=new Set(r);return n.some(o=>o(s))}}}function zl(e,t){return t?e.filter(n=>t.matches(n.tags)):e}function Hl(e,t){return[...t??[],e]}import{spawn as ah}from"node:child_process";function Go(e,t){if(e==null)return[];let n=Array.isArray(e)?e:[e];for(let r of n)if(typeof r!="string"||r.trim()==="")throw new Error(`"${t}" in saffron.config.json must be a command, or a list of commands, e.g. "${t}": "npm run seed"`);return n}function ch(e,t,n){let r=Date.now();return new Promise(s=>{let o=process.platform!=="win32",i=ah(t,{cwd:n.cwd,shell:!0,detached:o,env:{...process.env,...n.env},stdio:["ignore",process.stderr,process.stderr]}),a=!1,c=!1,l=()=>{try{o&&i.pid?process.kill(-i.pid,"SIGKILL"):i.kill("SIGKILL")}catch{}},d=setTimeout(()=>{a=!0,l()},n.timeoutMs),f=()=>{c=!0,l()};n.signal?.addEventListener("abort",f,{once:!0});let m=p=>{clearTimeout(d),n.signal?.removeEventListener("abort",f),s({phase:e,command:t,exitCode:p,durationMs:Date.now()-r,...a?{timedOut:a}:{},...c?{cancelled:c}:{}})};i.on("error",()=>m(null)),i.on("exit",p=>m(a||c?null:p))})}var es=e=>e.exitCode!==0;async function Vo(e,t,n){let r=[];for(let s of t){if(n.signal?.aborted)break;console.error(`saffron ${e}: ${s}`);let o=await ch(e,s,n);if(r.push(o),o.cancelled||es(o)&&(console.error(`saffron ${e} failed: ${Ko(o,n.timeoutMs)}`),e==="setup"))break}return r}function Ko(e,t){return e.cancelled?`"${e.command}" was interrupted`:e.timedOut?`"${e.command}" was stopped after ${Math.round(t/1e3)}s (hookTimeoutMs)`:e.exitCode===null?`"${e.command}" could not be started`:`"${e.command}" exited with code ${e.exitCode}`}var lh=`
1062
+ .wrap { max-width: 1180px; }
1063
+ .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 0 14px; align-items: start; }
1064
+ .panel { min-width: 0; }
1065
+ .panel-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
1066
+ .count { font-family: var(--mono); font-size: 10px; font-weight: 600; border-radius: 5px; padding: 1px 8px; background: var(--border); color: var(--text-dim); }
1067
+ .count.red { background: var(--red-soft); color: var(--red); }
1068
+ .count.gold { background: var(--gold-soft); color: var(--gold); }
1069
+ .panel ul.rows { list-style: none; padding: 0; margin: 8px 0 0; }
1070
+ .rows li { display: flex; gap: 10px; align-items: baseline; padding: 7px 0; margin: 0; border-top: 1px solid var(--border); min-width: 0; }
1071
+ .rows li:first-child { border-top: none; }
1072
+ .rows .dot { top: 0; }
1073
+ .dot.gold { background: var(--gold); }
1074
+ .dot.faint { background: var(--faint); }
1075
+ .row-main { flex: 1; min-width: 0; }
1076
+ .scn { color: #E7E9EC; font-weight: 500; font-size: 13.5px; }
1077
+ .linked .scn { cursor: pointer; }
1078
+ .linked .scn:hover { color: var(--brand); }
1079
+ .feat { font-family: var(--mono); font-size: 10.5px; color: var(--faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1080
+ .err { font-family: var(--mono); font-size: 11px; color: var(--red); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 2px; }
1081
+ .row-side { flex: none; text-align: right; font-family: var(--mono); font-size: 10.5px; color: var(--muted); line-height: 1.6; }
1082
+ .row-side .red { color: var(--red); } .row-side .green { color: var(--green); } .row-side .gold { color: var(--gold); }
1083
+ .meter { height: 3px; border-radius: 2px; background: var(--faint); margin-top: 5px; opacity: .7; }
1084
+ .stack { display: flex; height: 8px; border-radius: 4px; overflow: hidden; background: var(--border); margin-top: 10px; }
1085
+ .stack span { display: block; height: 100%; }
1086
+ .legend { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-top: 9px; font-family: var(--mono); font-size: 10.5px; color: var(--muted); }
1087
+ .legend .dot { display: inline-block; margin-right: 6px; top: 0; }
1088
+ .bars { display: block; width: 100%; height: 64px; margin-top: 10px; }
1089
+ .axis { display: flex; justify-content: space-between; font-family: var(--mono); font-size: 9.5px; color: var(--faint); margin-top: 4px; }
1090
+ table.tags { width: 100%; border-collapse: collapse; margin-top: 8px; }
1091
+ .tags td { padding: 6px 0; border-top: 1px solid var(--border); vertical-align: middle; font-size: 12.5px; }
1092
+ .tags tr:first-child td { border-top: none; }
1093
+ .tags .tagname { font-family: var(--mono); font-size: 11.5px; color: var(--text-dim); }
1094
+ .tags .num { text-align: right; white-space: nowrap; padding-left: 10px; font-family: var(--mono); font-size: 10.5px; color: var(--muted); }
1095
+ .tags .stack { margin-top: 0; }
1096
+ .tags .stackcell { width: 42%; padding-left: 12px; }
1097
+ .empty { font-size: 12.5px; color: var(--faint); margin-top: 8px; }
1098
+ .note { font-size: 12px; color: var(--muted); margin-top: 6px; }
1099
+ details.more summary { padding: 6px 0 0; font-family: var(--mono); font-size: 10.5px; color: var(--muted); }
1100
+ details.more summary:hover { background: none; color: var(--text); }
1101
+ @media (max-width: 560px) { .wrap { padding: 14px 12px 40px; } .grid { grid-template-columns: 1fr; } .kpis { grid-template-columns: repeat(2, 1fr); } }
1102
+ `,Ot=(e,t)=>`${e} ${t}${e===1?"":"s"}`,Yo=e=>new Date(e).toLocaleString("en-GB",{day:"2-digit",month:"short",year:"numeric",hour:"2-digit",minute:"2-digit"}),Qo=e=>new Date(e).toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"});function Hn(e,t=""){let n=e.scenario.replace(/ \(example \d+\)$/,"");return`<div class="row-main"><div class="scn" data-f="${N(e.feature)}" data-s="${N(n)}" title="${N(`${e.feature} \u203A ${e.scenario}`)}">${N(e.scenario)}</div><div class="feat">${N(e.feature)}</div>${t}</div>`}function ot(e,t,n,r,s,o=""){return`<div class="panel${o?` accent-${o}`:""}"><div class="panel-head"><div class="panel-title${n?` ${n}`:""}">${N(e)}</div>${t===void 0?"":`<span class="count${t>0&&n?` ${n}`:""}">${t}</span>`}</div>${r||`<div class="empty">${N(s)}</div>`}</div>`}function ql(e){let t=e.reduce((n,[r])=>n+r,0)||1;return`<div class="stack">${e.filter(([n])=>n>0).map(([n,r])=>`<span style="width:${(n/t*100).toFixed(2)}%;background:var(${r})"></span>`).join("")}</div>`}function Jl(e,t,n,r){if(e.length===0)return'<div class="empty">No runs yet.</div>';let s=Math.max(1,...e.map(t)),o=10,i=e.map((a,c)=>{let l=Math.max(2,t(a)/s*62);return`<rect x="${c*o+1.5}" y="${(64-l).toFixed(1)}" width="${o-3}" height="${l.toFixed(1)}" rx="1.5" fill="var(${r(a)})"><title>${N(n(a))}</title></rect>`}).join("");return`<svg class="bars" viewBox="0 0 ${e.length*o} 64" preserveAspectRatio="none" aria-hidden="true">${i}</svg><div class="axis"><span>${N(Qo(e[0].startedAt))}</span><span>${N(Qo(e.at(-1).startedAt))}</span></div>`}var qn=e=>e.length?`<ul class="rows">${e.join("")}</ul>`:"";function Wl(e,t){let n=e.suite,r=n.green+n.yellow+n.red,s=r===0?void 0:Math.round((n.green+n.yellow)/r*100),o=e.recentFailures.filter(u=>u.stillFailing).length,i=e.lastRun,a=s===void 0?"":s>=95?"green":s>=80?"gold":"red",c=`<section class="kpis">
1103
+ ${oe("tests",String(n.tests),`${Ot(n.scenarios,"scenario")} \xB7 ${Ot(n.features,"feature")}`)}
1104
+ ${oe("passing",s===void 0?"\u2013":`${s}<span class="unit">%</span>`,`${n.green+n.yellow} of ${r} \xB7 latest results`,a,"",Qt(e.trend.map(u=>u.passRate),"var(--green)"))}
1105
+ ${oe("failing",String(o),o>0?"\u25BC red the last time they ran":"all clear",o>0?"red":"",o>0?"red":"green")}
1106
+ ${oe("flaky",String(e.flaky.length),e.flaky.length>0?"pass and fail by turns":"none in 20 runs",e.flaky.length>0?"gold":"",e.flaky.length>0?"gold":"green")}
1107
+ ${oe("never run",String(e.neverRun.length),e.neverRun.length>0?"in no recorded run":"every scenario has run",e.neverRun.length>0?"gold":"",e.neverRun.length>0?"gold":"green")}
1108
+ ${oe("last run",i?Ge(i.durationMs):"\u2013",i?`${Ot(i.ran,"test")} \xB7 ${i.passRate}% passed`:"no run yet","","",Qt(e.trend.map(u=>u.durationMs),"var(--muted)"))}
1109
+ ${oe("zero-ai replay",i?`${i.replayRate}<span class="unit">%</span>`:"\u2013","replayed from recordings, last run",i&&i.replayRate>=90?"green":"")}
1110
+ ${oe("recorded",n.scenarios?`${Math.round(n.recorded/n.scenarios*100)}<span class="unit">%</span>`:"\u2013",n.scenarios-n.recorded>0?`${n.scenarios-n.recorded} still need the agent`:"all replay without AI",n.recorded===n.scenarios?"green":"")}
1111
+ </section>`,l=`<div class="panel"><div class="panel-title">suite health \xB7 each scenario's latest result</div>${ql([[n.green,"--green"],[n.yellow,"--gold"],[n.red,"--red"],[n.neverRun,"--faint"]])}<div class="legend"><span><i class="dot green"></i>${n.green} passed</span><span><i class="dot gold"></i>${n.yellow} healed</span><span><i class="dot red"></i>${n.red} failed</span><span><i class="dot faint"></i>${n.neverRun} never run</span></div></div>`,d=`<div class="grid">
1112
+ ${ot(`pass rate \xB7 last ${Ot(e.trend.length,"run")}`,void 0,"",Jl(e.trend,u=>u.passRate,u=>`${Yo(u.startedAt)}: ${u.passRate}% of ${u.tests} passed, ${u.red} failed`,u=>u.red>0?"--red":"--green"),"")}
1113
+ ${ot(`execution time \xB7 last ${Ot(e.trend.length,"run")}`,void 0,"",Jl(e.trend,u=>u.durationMs,u=>`${Yo(u.startedAt)}: ${Ge(u.durationMs)} for ${Ot(u.tests,"test")}`,()=>"--muted"),"")}
1114
+ </div>`,f=qn(e.recentFailures.map(u=>`<li><i class="dot ${u.stillFailing?"red":"green"}"></i>${Hn(u,u.error?`<div class="err" title="${N(u.error)}">${N(u.error)}</div>`:"")}<div class="row-side">${u.stillFailing?`<span class="red">${u.streak} run${u.streak===1?"":"s"} in a row</span>`:'<span class="green">passed since</span>'}<br>${N(Qo(u.at))}</div></li>`)),m=qn(e.flaky.map(u=>`<li><i class="dot gold"></i>${Hn(u)}<div class="row-side">flipped ${u.flips}\xD7 in ${u.runs} runs<br>${u.reds} red \xB7 last <span class="${u.last==="red"?"red":"green"}">${u.last==="red"?"failed":"passed"}</span></div></li>`)),p=qn(e.healing.map(u=>`<li><i class="dot gold"></i>${Hn(u)}<div class="row-side">healed ${u.heals}\xD7<br>in ${u.runs} runs</div></li>`)),b=Math.max(1,...e.slowest.map(u=>u.avgMs)),$=qn(e.slowest.map(u=>`<li>${Hn(u,`<div class="meter" style="width:${(u.avgMs/b*100).toFixed(1)}%"></div>`)}<div class="row-side">avg ${Ge(u.avgMs)}<br>last ${Ge(u.lastMs)}</div></li>`)),k=e.tags.length?`<table class="tags">${e.tags.map(u=>{let v=u.scenarios-u.neverRun,S=u.red>0?`<span style="color:var(--red)">${u.red} red</span>`:v===0?"never run":`${Math.round((u.green+u.yellow)/v*100)}%`;return`<tr><td class="tagname">${N(u.tag)}</td><td class="num">${u.scenarios}</td><td class="stackcell">${ql([[u.green,"--green"],[u.yellow,"--gold"],[u.red,"--red"],[u.neverRun,"--faint"]])}</td><td class="num">${S}</td></tr>`}).join("")}</table>`:"",w=(u,v)=>qn(u.map(S=>`<li><i class="dot ${v}"></i>${Hn(S)}</li>`)),x=e.notInLastRun.length?`<details class="more"><summary>left out by tags or paths \xB7 show ${e.notInLastRun.length}</summary>${w(e.notInLastRun,"idle")}</details>`:"",y=e.historyRuns===0?"no runs recorded yet":`${Ot(e.historyRuns,"run")} in .saffron/history.jsonl`;return`<!doctype html>
1115
+ <html lang="en">
1116
+ <head>
1117
+ <meta charset="utf-8"/>
1118
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
1119
+ <title>Saffron dashboard</title>
1120
+ ${io}
1121
+ <style>
1122
+ ${ao}${lh}</style>
1123
+ </head>
1124
+ <body>
1125
+ <div class="topline"></div>
1126
+ <div class="wrap">
1127
+ <header class="top">
1128
+ <div class="brand">
1129
+ ${In(38)}
1130
+ <div>
1131
+ <div class="brand-name">saffron</div>
1132
+ <div class="brand-sub">suite quality</div>
1133
+ </div>
1134
+ </div>
1135
+ <div class="run-meta">
1136
+ <span>${N(y)}</span>
1137
+ ${i?`<span>\xB7</span><span>last run ${N(Yo(i.startedAt))}</span>`:""}
1138
+ </div>
1139
+ </header>
1140
+
1141
+ ${c}
1142
+ ${l}
1143
+
1144
+ <div class="section-head"><span class="section-title">trends</span></div>
1145
+ ${d}
1146
+
1147
+ <div class="section-head"><span class="section-title">stability</span></div>
1148
+ <div class="grid">
1149
+ ${ot("recent failures \xB7 last 10 runs",e.recentFailures.length,"red",f,"Nothing failed in the last 10 runs.",o>0?"red":"")}
1150
+ ${ot("flaky \xB7 last 20 runs",e.flaky.length,"gold",m,"No scenario passed and failed by turns.",e.flaky.length>0?"gold":"")}
1151
+ ${ot("healed again and again",e.healing.length,"gold",p?`<div class="note">The page keeps changing under these: review the heals, or fix the step.</div>${p}`:"","No scenario needed the agent twice in the last 20 runs.")}
1152
+ ${ot("slowest \xB7 average over 20 runs",void 0,"",$,"No timings yet.")}
1153
+ </div>
1154
+
1155
+ <div class="section-head"><span class="section-title">coverage</span></div>
1156
+ <div class="grid">
1157
+ ${ot("tags \xB7 latest results",e.tags.length,"",k,"No tags in the feature files.")}
1158
+ ${ot("never run",e.neverRun.length,"gold",w(e.neverRun,"faint"),"Every scenario has run at least once.")}
1159
+ ${ot("not in the last run",e.notInLastRun.length,"",x,"The last run ran every scenario that has run before.")}
1160
+ </div>
1161
+
1162
+ <footer>${oo(11)} generated by saffron v${N(t)}</footer>
1163
+ </div>
1164
+ <script>
1165
+ if (typeof window.saffronOpen === "function") document.body.classList.add("linked");
1166
+ document.addEventListener("click", function (e) {
1167
+ var el = e.target.closest && e.target.closest(".scn[data-f]");
1168
+ if (el && typeof window.saffronOpen === "function") window.saffronOpen(el.getAttribute("data-f"), el.getAttribute("data-s"));
1169
+ });
1170
+ </script>
1171
+ </body>
1172
+ </html>
1173
+ `}import{spawnSync as sd}from"node:child_process";import Pe from"node:fs";import id from"node:os";import Z from"node:path";import{spawnSync as Gl}from"node:child_process";import Yl from"node:path";var dh=["github","azure"],Xo={github:"GitHub Actions",azure:"Azure Pipelines"};function pn(e){if(e.startsWith("@")||/[\x00-\x1f\x7f]/.test(e))throw new L(`${JSON.stringify(e)} is not a branch name Saffron passes on`);return e}function ph(e){let t=e.replace(/(\\*)"/g,'$1$1\\"').replace(/(\\*)$/,"$1$1");return t=`"${t}"`,t=t.replace(/[()%!^"<>&|;, ]/g,"^$&"),t.replace(/[()%!^"<>&|;, ]/g,"^$&")}function ve(e,t,n,r,s){let o=process.platform==="win32"&&e==="az",i=o?Gl("az.cmd",t.map(ph),{cwd:n,env:r,input:s,encoding:"utf8",shell:!0,windowsVerbatimArguments:!0,timeout:6e5,maxBuffer:64*1024*1024}):Gl(e,t,{cwd:n,env:r,input:s,encoding:"utf8",timeout:6e5,maxBuffer:64*1024*1024}),a=i.error?.code,c=a==="ENOENT"||o&&i.status===9009;return{status:i.status,stdout:(i.stdout??"").trim(),stderr:(i.stderr??"").trim(),missing:c,timedOut:a==="ETIMEDOUT"}}var Jn=e=>`${e.stderr}
1174
+ ${e.stdout}`,Zo=(e,t)=>Jn(e).split(`
1175
+ `).map(n=>n.trim()).find(n=>n&&!/^WARNING:/.test(n))??t;function Ql(e){return e.missing?{kind:"missing-tool",message:"the GitHub CLI (gh) is not installed here: install it from cli.github.com and sign in with gh auth login"}:e.timedOut?{kind:"failed",message:"gh did not finish within 10 minutes"}:/gh auth login|not logged in|authentication required/i.test(Jn(e))?{kind:"signed-out",message:"gh is not signed in: run gh auth login"}:/none of the git remotes|no git remotes|not a git repository/i.test(Jn(e))?{kind:"not-a-repository",message:Zo(e,"not a GitHub repository")}:{kind:"failed",message:Zo(e,`gh exited with ${e.status}`)}}function ts(e){if(e.missing)return{kind:"missing-tool",message:"the Azure CLI (az) is not installed here: install it, add its azure-devops extension (az extension add --name azure-devops) and sign in (az login, or AZURE_DEVOPS_EXT_PAT)"};if(e.timedOut)return{kind:"failed",message:"az did not finish within 10 minutes"};let t=Jn(e);return/azure-devops|'pipelines' is (misspelled|not in)|not in the 'az' command group/i.test(t)&&!/login/i.test(t)?{kind:"missing-tool",message:"the Azure CLI has no azure-devops extension: az extension add --name azure-devops"}:/az login|az devops login|TF400813|not authorized|authentication failed|\b401\b/i.test(t)?{kind:"signed-out",message:"az is not signed in to Azure DevOps: run az login, or set AZURE_DEVOPS_EXT_PAT"}:/--organization|--org\b|--project|must be specified|could not detect/i.test(t)?{kind:"not-a-repository",message:"cannot tell which Azure DevOps organization and project: run this in a clone of an Azure Repos repository, or set ci.organization and ci.project in saffron.config.json"}:{kind:"failed",message:Zo(e,`az exited with ${e.status}`)}}function Zl(e,t,n){let r=n??t.ci?.provider;if(r!==void 0){if(!dh.includes(r))throw new L(`unknown CI provider "${r}": use "github" or "azure"`);return r}return Pr(e).some(s=>s.startsWith("dev.azure.com/"))?"azure":"github"}var ei=e=>[...e.ci?.organization?["--organization",e.ci.organization]:[],...e.ci?.project?["--project",e.ci.project]:[]];function fh(e,t){let n=ve("git",["rev-parse","--abbrev-ref","HEAD"],e,t),r=n.status===0?n.stdout.trim():"";return r&&r!=="HEAD"?r:void 0}function uh(e){let t=new Map;for(let[n,r]of Object.entries(On(e))){try{if(qe(Yl.join(e,n))!==r.revision)continue}catch{continue}r.from.run&&t.set(r.from.run,(t.get(r.from.run)??0)+1)}return t}function Xl(e,t=Date.now()){let n=e?t-Date.parse(e):NaN;if(!(n>=0))return;let r=Math.round(n/6e4);if(r<60)return`${r} min ago`;let s=Math.round(r/60);return s<48?`${s} h ago`:`${Math.round(s/24)} days ago`}var ne=e=>typeof e=="string"&&e?e:void 0,Vl=e=>ne(e)?.replace(/^refs\/(heads|tags)\//,""),gh="databaseId,number,attempt,displayTitle,workflowName,status,conclusion,event,headBranch,headSha,createdAt,url";function hh(e){let t=e.status==="completed"?"completed":e.status==="in_progress"?"in_progress":"queued";return{id:String(e.databaseId),name:ne(e.workflowName)??"workflow",number:String(e.number??""),title:ne(e.displayTitle),status:t,conclusion:t==="completed"?ne(e.conclusion):void 0,event:ne(e.event),branch:ne(e.headBranch),commit:ne(e.headSha),createdAt:ne(e.createdAt),url:ne(e.url),attempt:typeof e.attempt=="number"?e.attempt:void 0}}var mh={completed:"completed",inProgress:"in_progress",cancelling:"in_progress"},yh={succeeded:"success",partiallySucceeded:"partial",failed:"failure",canceled:"cancelled"},bh={individualCI:"push",batchedCI:"push",pullRequest:"pull_request",schedule:"schedule",manual:"manual"};function ed(e){let t=e.triggerInfo??{};return Vl(t["pr.sourceBranch"])??Vl(e.sourceBranch)}function wh(e){let t=e.triggerInfo??{},n=mh[String(e.status)]??"queued",r=ne(e.result);return{id:String(e.id),name:ne(e.definition?.name)??"pipeline",number:String(e.buildNumber??e.id),status:n,conclusion:n==="completed"&&r?yh[r]??r:void 0,event:ne(e.reason)?bh[String(e.reason)]??String(e.reason):void 0,branch:ed(e),commit:ne(t["pr.sourceSha"])??ne(e.sourceVersion),createdAt:ne(e.queueTime),url:ne((e._links??{}).web?.href)}}function ti(e){try{let t=JSON.parse(e);return Array.isArray(t)?t.filter(n=>!!n&&typeof n=="object"):void 0}catch{return}}function vh(e){return new Set(e.map(t=>t.startsWith("dev.azure.com/")?t.slice(t.lastIndexOf("/_git/")+6):t.slice(t.indexOf("/")+1)))}function Sh(e,t,n,r,s,o,i=Math.min(Math.max(o,50),500)){for(let a=i;;a=Math.min(a*2,500)){let c=ve("az",["pipelines","runs","list","--top",String(a),...r,"--output","json",...ei(t)],e,n);if(c.missing||c.status!==0)return ts(c);let l=ti(c.stdout);if(!l)return{kind:"failed",message:"az answered with something other than a list of runs"};let d=l.filter(s);if(d.length>=o||l.length<a||a>=500)return d}}function td(e,t,n={}){let r=n.env??process.env,s=Zl(e,t,n.provider),o=n.branch??fh(e,r),i=o===void 0?void 0:pn(o),a=n.limit??20,c;if(s==="github"){let d=ve("gh",["run","list","--limit",String(a),"--json",gh,...i?["--branch",i]:[]],e,r);if(d.missing||d.status!==0)return{provider:s,branch:i,runs:[],problem:Ql(d)};let f=ti(d.stdout);if(!f)return{provider:s,branch:i,runs:[],problem:{kind:"failed",message:"gh answered with something other than a list of runs"}};c=f.map(hh)}else{let d=vh(Pr(e)),f=$=>{let k=ne($.repository?.name)?.toLowerCase();return d.size===0||!k||d.has(k)},m=$=>f($)&&(!i||ed($)===i),p=i?[["--branch",`refs/heads/${i}`],["--branch",`refs/tags/${i}`],["--reason","pullRequest"]]:[[]],b=new Map;for(let $ of p){let k=Sh(e,t,r,$,m,a);if(!Array.isArray(k))return{provider:s,branch:i,runs:[],problem:k};for(let w of k)b.set(String(w.id),w)}c=[...b.values()].sort(($,k)=>(Date.parse(ne(k.queueTime)??"")||0)-(Date.parse(ne($.queueTime)??"")||0)).slice(0,a).map(wh)}let l=uh(e);return{provider:s,branch:i,runs:c.map(d=>({...d,pending:l.get(`${s}:${d.id}`)??0}))}}var Kl=e=>`run ${e} has no Saffron bundle: none was uploaded, or it expired. Its workflow needs saffron export and an upload of saffron-bundle* (see the guide, section 19)`;function nd(e,t,n,r={}){if(!/^\d+$/.test(n))throw new L(`--run takes the run's id, a number as saffron runs lists it, not "${n}"`);let s=r.env??process.env,o=Zl(e,t,r.provider),{dir:i,remove:a}=xo(e);try{if(o==="github"){let c=ve("gh",["run","download",n,"--pattern","saffron-bundle*","--dir",i],e,s);if(c.missing||c.status!==0)throw new L(/no (valid )?artifact/i.test(Jn(c))?Kl(n):Ql(c).message)}else{let c=ei(t),l=ve("az",["pipelines","runs","artifact","list","--run-id",n,"--output","json",...c],e,s);if(l.missing||l.status!==0)throw new L(ts(l).message);let d=(ti(l.stdout)??[]).map(f=>f.name).filter(f=>typeof f=="string"&&/^saffron-bundle[\w.-]*$/.test(f));if(d.length===0)throw new L(Kl(n));for(let f of d){let m=ve("az",["pipelines","runs","artifact","download","--run-id",n,"--artifact-name",f,"--path",Yl.join(i,f),...c],e,s);if(m.missing||m.status!==0)throw new L(`cannot download ${f}: ${ts(m).message}`)}}return{provider:o,dir:i,remove:a}}catch(c){throw a(),c}}function rd(e,t,n,r){let s=ve("az",["repos","pr","create","--draft","true","--source-branch",`refs/heads/${pn(n.branch)}`,"--target-branch",`refs/heads/${pn(n.base)}`,"--title",n.title,"--description",`@${n.bodyFile}`,"--output","json",...ei(t)],e,r);if(s.missing||s.status!==0)return{problem:`az repos pr create failed: ${ts(s).message}`};try{let o=JSON.parse(s.stdout),i=o.repository?.webUrl;return{url:i&&o.pullRequestId!==void 0?`${i}/pullrequest/${o.pullRequestId}`:void 0}}catch{return{}}}function od(e,t,n){if(e.missing)throw new L(`${n}: ${t} is not installed here`);if(e.timedOut)throw new L(`${n}: ${t} did not finish within 10 minutes`);if(e.status!==0)throw new L(`${n}: ${(e.stderr||e.stdout||`exit ${e.status}`).split(`
1176
+ `).slice(-3).join(" ")}`);return e.stdout}function xh(e,t){let n=i=>ve("git",["config",i],e,t).stdout,r=t.GIT_AUTHOR_NAME||n("user.name"),s=t.GIT_AUTHOR_EMAIL||n("user.email"),o=t.GITHUB_ACTIONS==="true"?{name:"github-actions[bot]",email:"41898282+github-actions[bot]@users.noreply.github.com"}:{name:"Saffron",email:"saffron@localhost"};return{...t,GIT_AUTHOR_NAME:r||o.name,GIT_AUTHOR_EMAIL:s||o.email,GIT_COMMITTER_NAME:t.GIT_COMMITTER_NAME||r||o.name,GIT_COMMITTER_EMAIL:t.GIT_COMMITTER_EMAIL||s||o.email}}var ns=(e,t)=>`${e} ${t}${e===1?"":"s"}`,Wn=e=>`\`${e.replaceAll("`","'")}\``,ni=e=>{let t=e.startsWith("-")?`./${e}`:e;return/^[\w./-]+$/.test(t)?t:`'${t.replaceAll("'","'\\''")}'`};function ri(e,t=""){let n=Math.max(0,...[...e.matchAll(/`+/g)].map(s=>s[0].length)),r="`".repeat(Math.max(3,n+1));return`${r}${t}
1065
1177
  ${e}
1066
- ${r}`}var Hl={github:6e4,azure:3800};function ih(e,t,n,r="",s=Hl.github){let{manifest:o,proposals:i}=t,a=o.run?o.url&&/^https?:\/\//i.test(o.url)?`[run ${o.run}](${o.url})`:`run ${Dn(o.run)}`:"a run",c=o.headCommit??o.commit,l=[o.branch&&`on ${Dn(o.branch)}`,c&&`at ${Dn(c.slice(0,7))}`].filter(Boolean).join(" "),d=[`Saffron filed ${Kr(i.length,"proposal")} in ${a}${l?` ${l}`:""}. This pull request carries ${i.length===1?"the proposal, not the recording":"the proposals, not the recordings"}: nothing is accepted until you accept it.`,"","**To review and accept** (no agent runs on your machine):","",Ho([`git fetch origin ${qo(n)} && git switch ${qo(n)}`,...r?[`cd ${qo(r.replace(/\/$/,""))}`]:[],"npx -y -p saffron-ai saffron diff","npx -y -p saffron-ai saffron accept --all",'git add -A .saffron/cache && git add -u .saffron/proposals && git commit -m "Accept Saffron proposals" && git push'].join(`
1178
+ ${r}`}var ad={github:6e4,azure:3800};function $h(e,t,n,r="",s=ad.github){let{manifest:o,proposals:i}=t,a=o.run?o.url&&/^https?:\/\//i.test(o.url)?`[run ${o.run}](${o.url})`:`run ${Wn(o.run)}`:"a run",c=o.headCommit??o.commit,l=[o.branch&&`on ${Wn(o.branch)}`,c&&`at ${Wn(c.slice(0,7))}`].filter(Boolean).join(" "),d=[`Saffron filed ${ns(i.length,"proposal")} in ${a}${l?` ${l}`:""}. This pull request carries ${i.length===1?"the proposal, not the recording":"the proposals, not the recordings"}: nothing is accepted until you accept it.`,"","**To review and accept** (no agent runs on your machine):","",ri([`git fetch origin ${ni(n)} && git switch ${ni(n)}`,...r?[`cd ${ni(r.replace(/\/$/,""))}`]:[],"npx -y -p saffron-ai saffron diff","npx -y -p saffron-ai saffron accept --all",'git add -A .saffron/cache && git add -u .saffron/proposals && git commit -m "Accept Saffron proposals" && git push'].join(`
1067
1179
  `),"bash"),"","Then mark this pull request ready for review. `accept --all` leaves UNVERIFIED proposals out: read one, and accept it by file if it is right. `saffron accept` refuses a proposal whose scenario or recording changed since it was filed."],f=d.join(`
1068
- `).length;for(let[m,{proposal:p}]of i.entries()){let{feature:b,scenario:v}=p.cache,{mode:k,verified:w,proofError:x,narrative:y,adaptations:h}=p.meta,S=w===!0?"**verified \u2713**: a zero-AI proof replay passed":w===!1?"**UNVERIFIED \u2717**: its proof replay failed":"not verified: proof replays were off",$=[y,...h.map(R=>`\u26A0 ${R}`),...w===!1&&x?[`Proof replay: ${x}`]:[]].filter(Boolean).join(`
1069
- `),u=fr(lt(Ue(Y(e,b,v)),p.cache)),g=["",`### ${Dn(b)} \u203A ${Dn(v)}`,"",`${k==="heal"?"Heal":"First recording"}, ${S}`,"",...$?[Ho($,"text"),""]:[],Ho(u,"diff")].join(`
1070
- `);if(f+g.length>s){d.push("",`\u2026and ${Kr(i.length-m,"more proposal")}: run \`saffron diff\` on the branch to see ${i.length-m===1?"it":"them"}.`);break}d.push(g),f+=g.length+1}return`${d.join(`
1180
+ `).length;for(let[m,{proposal:p}]of i.entries()){let{feature:b,scenario:$}=p.cache,{mode:k,verified:w,proofError:x,narrative:y,adaptations:u}=p.meta,v=w===!0?"**verified \u2713**: a zero-AI proof replay passed":w===!1?"**UNVERIFIED \u2717**: its proof replay failed":"not verified: proof replays were off",S=[y,...u.map(T=>`\u26A0 ${T}`),...w===!1&&x?[`Proof replay: ${x}`]:[]].filter(Boolean).join(`
1181
+ `),h=vr(pt(Be(Y(e,b,$)),p.cache)),g=["",`### ${Wn(b)} \u203A ${Wn($)}`,"",`${k==="heal"?"Heal":"First recording"}, ${v}`,"",...S?[ri(S,"text"),""]:[],ri(h,"diff")].join(`
1182
+ `);if(f+g.length>s){d.push("",`\u2026and ${ns(i.length-m,"more proposal")}: run \`saffron diff\` on the branch to see ${i.length-m===1?"it":"them"}.`);break}d.push(g),f+=g.length+1}return`${d.join(`
1071
1183
  `)}
1072
- `}var Jo=e=>e.split(/[\\/]/).join("/");function ah(e,t,n,r,s,o){let i=Ie.mkdtempSync(Q.join(ql.tmpdir(),"saffron-head-")),a=b=>{let v=Bl("git",["show",`${n}:${r}${Jo(b)}`],{cwd:e,env:s,timeout:6e5,maxBuffer:67108864});return v.status===0?v.stdout:void 0},c=b=>{let v=Q.relative(e,Q.resolve(e,b));return v&&!v.startsWith("..")&&!Q.isAbsolute(v)?Jo(v):void 0},l=c(o.features??"features"),d=c(o.dataDir??"data"),f=Bl("git",["ls-tree","-r","-z","--full-tree","--name-only",n,"--",...[l,d].flatMap(b=>b?[`${r}${b}`]:[])],{cwd:e,env:s,encoding:"utf8",timeout:6e5,maxBuffer:64*1024*1024}),m=De(),p=[];try{for(let v of(l||d)&&f.status===0?f.stdout.split("\0").filter(Boolean):[]){let k=v.slice(r.length);if(!(d&&k.startsWith(`${d}/`))&&!k.endsWith(".saffron"))continue;let w=a(k);w!==void 0&&(Ie.mkdirSync(Q.dirname(Q.join(i,k)),{recursive:!0}),Ie.writeFileSync(Q.join(i,k),w))}d&&Me(i,d,m.env);let b;try{b=Re(i,o,[])}catch(v){let k=`cannot be checked: the StepSets at ${n.slice(0,7)} do not parse: ${v instanceof Error?v.message.split(`
1073
- `)[0]:String(v)}`;return t.proposals.map(({file:w})=>({file:w,reason:k}))}for(let{file:v,proposal:k}of t.proposals){let{feature:w,scenario:x}=k.cache;if(Q.isAbsolute(w)||w.split(/[\\/]/).includes("..")){p.push({file:v,reason:`names a feature file outside the project: ${w}`});continue}let y=a(w);if(!y){p.push({file:v,reason:`is for ${w}, which is not in ${n.slice(0,7)}`});continue}Ie.mkdirSync(Q.dirname(Q.join(i,w)),{recursive:!0}),Ie.writeFileSync(Q.join(i,w),y);let h=Jo(Q.relative(e,Y(e,w,x))),S=a(h);Ie.rmSync(Q.join(i,h),{force:!0}),S&&(Ie.mkdirSync(Q.dirname(Q.join(i,h)),{recursive:!0}),Ie.writeFileSync(Q.join(i,h),S));let $;try{$=$e(i,k,b)}catch(u){$=`cannot be checked: ${u instanceof Error?u.message:String(u)}`}$&&p.push({file:v,reason:`does not fit ${n.slice(0,7)}: ${$}`})}}finally{d&&Me(e,m.dir,m.env),Ie.rmSync(i,{recursive:!0,force:!0})}return p}function ch(e){let{manifest:t,proposals:n}=e;return[`Saffron: ${Kr(n.length,"proposal")} to review${t.run?` from run ${t.run}`:""}`,"",...n.map(({proposal:r})=>`${r.cache.feature} \u203A ${r.cache.scenario} (${r.meta.mode}${r.meta.verified===!0?", verified":r.meta.verified===!1?", UNVERIFIED":""})`),"","Proposals only: nothing is accepted until someone runs saffron accept."].join(`
1074
- `)}function Ln(e,t){if(t!==void 0&&/[$`;|&<>()\s'"\\]/.test(t))throw new U(`${e} ${JSON.stringify(t)}: a branch name with a space, a quote or any of $ \` ; | & < > ( ) \\ is not accepted, as the pull request gives it to a shell`)}function Jl(e,t,n,r={}){let s=r.env??process.env,o=($,u,g={})=>zl(we("git",u,e,g.env??s,g.input),"git",$),{manifest:i}=t;if(t.proposals.length===0)throw new U("there are no pending proposals to push");let a=r.base??i.branch;if(!a)throw new U("cannot tell which branch the pull request is for: pass --base <branch>");if(an(a),an(n),Ln("--branch",n),Ln("--base",r.base),n===a)throw new U(`--branch ${n} is the branch the proposals are for: name a new one, e.g. saffron/heal-<run id>`);if(n.startsWith("-"))throw new U(`${n} is not a branch name git accepts`);o(`${n} is not a branch name git accepts`,["check-ref-format","--branch",n]);let c=i.headCommit??i.commit;if(!c||!/^[0-9a-f]{40}([0-9a-f]{24})?$/i.test(c))throw new U("cannot tell which commit the run tested: run it in a git checkout");let l=o("not a git checkout",["rev-parse","HEAD"]);if(c!==l&&!ao(e,l).includes(c)&&we("git",["merge-base","--is-ancestor",c,l],e,s).status!==0)throw new U(`${c.slice(0,7)}, the commit the branch would start on, is not this checkout's HEAD, a commit it merged or one it descends from: run export --branch in the checkout the run tested`);we("git",["cat-file","-e",`${c}^{commit}`],e,s).status!==0&&o(`the commit to build on, ${c.slice(0,7)}, is not in this clone and could not be fetched`,["fetch","--quiet","--no-tags","--depth=1","origin",c]);let d=we("git",["ls-remote","--exit-code","--heads","origin",`refs/heads/${n}`],e,s);if(d.status===0)throw new U(`branch ${n} already exists on origin: give each run its own, e.g. saffron/heal-<run id>-<attempt>`);d.status!==2&&zl(d,"git","cannot reach origin");let f=o("not a git checkout",["rev-parse","--show-prefix"]),m=ah(e,t,c,f,s,r.config??{}),p={...t,proposals:t.proposals.filter($=>!m.some(u=>u.file===$.file))};if(p.proposals.length===0)throw new U(`no proposal fits ${c.slice(0,7)}, the commit the branch would start on: ${m.map($=>`${Q.relative(e,$.file)} ${$.reason}`).join("; ")}`);let b=Ie.mkdtempSync(Q.join(ql.tmpdir(),"saffron-index-")),v={...s,GIT_INDEX_FILE:Q.join(b,"index")},k;try{o("cannot read the tested commit",["read-tree",c],{env:v});for(let{file:u,bytes:g}of p.proposals){let R=o("cannot store a proposal",["hash-object","-w","--stdin"],{input:g}),E=`${f}${Q.relative(e,u).split(Q.sep).join("/")}`;o("cannot stage a proposal",["update-index","--add","--cacheinfo",`100644,${R},${E}`],{env:v})}let $=o("cannot write the tree",["write-tree"],{env:v});k=o("cannot commit the proposals",["commit-tree",$,"-p",c,"-m",ch(p)],{env:oh(e,s)})}finally{Ie.rmSync(b,{recursive:!0,force:!0})}o(`cannot push ${n} to origin`,["push","--quiet","origin",`${k}:refs/heads/${n}`]);let w=i.repository?.startsWith("github.com/")?"github":i.repository?.startsWith("dev.azure.com/")?"azure":s.GITHUB_ACTIONS==="true"?"github":s.TF_BUILD==="True"?"azure":void 0,x=Q.join(Q.dirname(p.file),"saffron-pull-request.md");Ie.writeFileSync(x,ih(e,p,n,f,Hl[w??"github"]));let y={branch:n,base:a,commit:k,parent:c,bodyFile:x,leftOut:m};if(!w)return{...y,noPullRequest:`${i.repository??"this repository"} is not on GitHub or Azure DevOps`};let h=`Saffron: ${Kr(p.proposals.length,"proposal")} to review for ${a}`;if(w==="azure"){let $=Ul(e,r.config??{},{branch:n,base:a,title:h,bodyFile:x},s);return $.problem?{...y,noPullRequest:$.problem,failed:!0}:$.url?{...y,pullRequest:$.url}:{...y,noPullRequest:"az created it but did not say where",failed:!0}}let S=we("gh",["pr","create","--draft","--base",a,"--head",n,"--title",h,"--body-file",x],e,s);return S.missing?{...y,noPullRequest:"the gh CLI is not installed here",failed:!0}:S.status!==0?{...y,noPullRequest:`gh pr create failed: ${(S.stderr||S.stdout).split(`
1075
- `).slice(-2).join(" ")}`,failed:!0}:{...y,pullRequest:S.stdout.split(`
1076
- `).pop()}}import q from"node:fs";import K from"node:path";var Un={claude:".claude/skills",agents:".agents/skills",copilot:".github/skills",cursor:".cursor/skills"},lh=["claude","agents"],Vl="<!-- saffron:start -->",Wo="<!-- saffron:end -->";function dh(){return[Vl,"## Saffron end-to-end tests","","UI tests are plain-Gherkin `.feature` / `.saffron` files under `features/`,","run by Saffron (`npx saffron run`). No step definitions exist: an AI agent","records each scenario once; later runs replay at zero tokens.","","- Load the `saffron` skill before writing or editing scenarios.","- Exact step text is the cache identity: run `npx saffron steps` and reuse"," recorded wordings instead of inventing synonyms.","- `Then` steps are never healed: write them as the verdict, last.","- `npx saffron status` shows what is cached, what awaits review, the tags"," and vocabulary health; editing a cached scenario's text re-records it.","- Never edit `.saffron/cache/` or `.saffron/proposals/` by hand; change the"," scenario text and re-run. Review proposals with `npx saffron accept`"," (`--all`, or chosen files) and `npx saffron reject`.","- Secrets go in `{env:VAR}` tokens, never in files.","- Shared test data (accounts, names, enum values) goes in `data/*.json`,",' referenced as `{data:file.path.key}`; check a whole list with "every',' {data:file.list}"; read Examples rows from a file with'," `Examples: {data:file}` (.saffron only). Use `{unique:name}` for a value"," that must be new on every run. Never put secrets in `data/`.",Wo,""].join(`
1077
- `)}function Gl(e,t){if(!q.existsSync(e))return q.writeFileSync(e,t),"created";let n=q.readFileSync(e,"utf8"),r=n.indexOf(Vl),s=n.indexOf(Wo);if(r!==-1&&s!==-1&&s>r){let i=n.slice(0,r)+t.trimEnd()+n.slice(s+Wo.length);return i===n?"unchanged":(q.writeFileSync(e,i),"updated")}let o=n.endsWith(`
1184
+ `}var si=e=>e.split(/[\\/]/).join("/");function kh(e,t,n,r,s,o){let i=Pe.mkdtempSync(Z.join(id.tmpdir(),"saffron-head-")),a=b=>{let $=sd("git",["show",`${n}:${r}${si(b)}`],{cwd:e,env:s,timeout:6e5,maxBuffer:67108864});return $.status===0?$.stdout:void 0},c=b=>{let $=Z.relative(e,Z.resolve(e,b));return $&&!$.startsWith("..")&&!Z.isAbsolute($)?si($):void 0},l=c(o.features??"features"),d=c(o.dataDir??"data"),f=sd("git",["ls-tree","-r","-z","--full-tree","--name-only",n,"--",...[l,d].flatMap(b=>b?[`${r}${b}`]:[])],{cwd:e,env:s,encoding:"utf8",timeout:6e5,maxBuffer:64*1024*1024}),m=Le(),p=[];try{for(let $ of(l||d)&&f.status===0?f.stdout.split("\0").filter(Boolean):[]){let k=$.slice(r.length);if(!(d&&k.startsWith(`${d}/`))&&!k.endsWith(".saffron"))continue;let w=a(k);w!==void 0&&(Pe.mkdirSync(Z.dirname(Z.join(i,k)),{recursive:!0}),Pe.writeFileSync(Z.join(i,k),w))}d&&De(i,d,m.env);let b;try{b=Ee(i,o,[])}catch($){let k=`cannot be checked: the StepSets at ${n.slice(0,7)} do not parse: ${$ instanceof Error?$.message.split(`
1185
+ `)[0]:String($)}`;return t.proposals.map(({file:w})=>({file:w,reason:k}))}for(let{file:$,proposal:k}of t.proposals){let{feature:w,scenario:x}=k.cache;if(Z.isAbsolute(w)||w.split(/[\\/]/).includes("..")){p.push({file:$,reason:`names a feature file outside the project: ${w}`});continue}let y=a(w);if(!y){p.push({file:$,reason:`is for ${w}, which is not in ${n.slice(0,7)}`});continue}Pe.mkdirSync(Z.dirname(Z.join(i,w)),{recursive:!0}),Pe.writeFileSync(Z.join(i,w),y);let u=si(Z.relative(e,Y(e,w,x))),v=a(u);Pe.rmSync(Z.join(i,u),{force:!0}),v&&(Pe.mkdirSync(Z.dirname(Z.join(i,u)),{recursive:!0}),Pe.writeFileSync(Z.join(i,u),v));let S;try{S=ke(i,k,b)}catch(h){S=`cannot be checked: ${h instanceof Error?h.message:String(h)}`}S&&p.push({file:$,reason:`does not fit ${n.slice(0,7)}: ${S}`})}}finally{d&&De(e,m.dir,m.env),Pe.rmSync(i,{recursive:!0,force:!0})}return p}function Th(e){let{manifest:t,proposals:n}=e;return[`Saffron: ${ns(n.length,"proposal")} to review${t.run?` from run ${t.run}`:""}`,"",...n.map(({proposal:r})=>`${r.cache.feature} \u203A ${r.cache.scenario} (${r.meta.mode}${r.meta.verified===!0?", verified":r.meta.verified===!1?", UNVERIFIED":""})`),"","Proposals only: nothing is accepted until someone runs saffron accept."].join(`
1186
+ `)}function Gn(e,t){if(t!==void 0&&/[$`;|&<>()\s'"\\]/.test(t))throw new L(`${e} ${JSON.stringify(t)}: a branch name with a space, a quote or any of $ \` ; | & < > ( ) \\ is not accepted, as the pull request gives it to a shell`)}function cd(e,t,n,r={}){let s=r.env??process.env,o=(S,h,g={})=>od(ve("git",h,e,g.env??s,g.input),"git",S),{manifest:i}=t;if(t.proposals.length===0)throw new L("there are no pending proposals to push");let a=r.base??i.branch;if(!a)throw new L("cannot tell which branch the pull request is for: pass --base <branch>");if(pn(a),pn(n),Gn("--branch",n),Gn("--base",r.base),n===a)throw new L(`--branch ${n} is the branch the proposals are for: name a new one, e.g. saffron/heal-<run id>`);if(n.startsWith("-"))throw new L(`${n} is not a branch name git accepts`);o(`${n} is not a branch name git accepts`,["check-ref-format","--branch",n]);let c=i.headCommit??i.commit;if(!c||!/^[0-9a-f]{40}([0-9a-f]{24})?$/i.test(c))throw new L("cannot tell which commit the run tested: run it in a git checkout");let l=o("not a git checkout",["rev-parse","HEAD"]);if(c!==l&&!wo(e,l).includes(c)&&ve("git",["merge-base","--is-ancestor",c,l],e,s).status!==0)throw new L(`${c.slice(0,7)}, the commit the branch would start on, is not this checkout's HEAD, a commit it merged or one it descends from: run export --branch in the checkout the run tested`);ve("git",["cat-file","-e",`${c}^{commit}`],e,s).status!==0&&o(`the commit to build on, ${c.slice(0,7)}, is not in this clone and could not be fetched`,["fetch","--quiet","--no-tags","--depth=1","origin",c]);let d=ve("git",["ls-remote","--exit-code","--heads","origin",`refs/heads/${n}`],e,s);if(d.status===0)throw new L(`branch ${n} already exists on origin: give each run its own, e.g. saffron/heal-<run id>-<attempt>`);d.status!==2&&od(d,"git","cannot reach origin");let f=o("not a git checkout",["rev-parse","--show-prefix"]),m=kh(e,t,c,f,s,r.config??{}),p={...t,proposals:t.proposals.filter(S=>!m.some(h=>h.file===S.file))};if(p.proposals.length===0)throw new L(`no proposal fits ${c.slice(0,7)}, the commit the branch would start on: ${m.map(S=>`${Z.relative(e,S.file)} ${S.reason}`).join("; ")}`);let b=Pe.mkdtempSync(Z.join(id.tmpdir(),"saffron-index-")),$={...s,GIT_INDEX_FILE:Z.join(b,"index")},k;try{o("cannot read the tested commit",["read-tree",c],{env:$});for(let{file:h,bytes:g}of p.proposals){let T=o("cannot store a proposal",["hash-object","-w","--stdin"],{input:g}),E=`${f}${Z.relative(e,h).split(Z.sep).join("/")}`;o("cannot stage a proposal",["update-index","--add","--cacheinfo",`100644,${T},${E}`],{env:$})}let S=o("cannot write the tree",["write-tree"],{env:$});k=o("cannot commit the proposals",["commit-tree",S,"-p",c,"-m",Th(p)],{env:xh(e,s)})}finally{Pe.rmSync(b,{recursive:!0,force:!0})}o(`cannot push ${n} to origin`,["push","--quiet","origin",`${k}:refs/heads/${n}`]);let w=i.repository?.startsWith("github.com/")?"github":i.repository?.startsWith("dev.azure.com/")?"azure":s.GITHUB_ACTIONS==="true"?"github":s.TF_BUILD==="True"?"azure":void 0,x=Z.join(Z.dirname(p.file),"saffron-pull-request.md");Pe.writeFileSync(x,$h(e,p,n,f,ad[w??"github"]));let y={branch:n,base:a,commit:k,parent:c,bodyFile:x,leftOut:m};if(!w)return{...y,noPullRequest:`${i.repository??"this repository"} is not on GitHub or Azure DevOps`};let u=`Saffron: ${ns(p.proposals.length,"proposal")} to review for ${a}`;if(w==="azure"){let S=rd(e,r.config??{},{branch:n,base:a,title:u,bodyFile:x},s);return S.problem?{...y,noPullRequest:S.problem,failed:!0}:S.url?{...y,pullRequest:S.url}:{...y,noPullRequest:"az created it but did not say where",failed:!0}}let v=ve("gh",["pr","create","--draft","--base",a,"--head",n,"--title",u,"--body-file",x],e,s);return v.missing?{...y,noPullRequest:"the gh CLI is not installed here",failed:!0}:v.status!==0?{...y,noPullRequest:`gh pr create failed: ${(v.stderr||v.stdout).split(`
1187
+ `).slice(-2).join(" ")}`,failed:!0}:{...y,pullRequest:v.stdout.split(`
1188
+ `).pop()}}import H from"node:fs";import K from"node:path";var Vn={claude:".claude/skills",agents:".agents/skills",copilot:".github/skills",cursor:".cursor/skills"},Rh=["claude","agents"],pd="<!-- saffron:start -->",ii="<!-- saffron:end -->";function Eh(){return[pd,"## Saffron end-to-end tests","","UI tests are plain-Gherkin `.feature` / `.saffron` files under `features/`,","run by Saffron (`npx saffron run`). No step definitions exist: an AI agent","records each scenario once; later runs replay at zero tokens.","","- Load the `saffron` skill before writing or editing scenarios.","- Exact step text is the cache identity: run `npx saffron steps` and reuse"," recorded wordings instead of inventing synonyms.","- `Then` steps are never healed: write them as the verdict, last.","- `npx saffron status` shows what is cached, what awaits review, the tags"," and vocabulary health; editing a cached scenario's text re-records it.","- Never edit `.saffron/cache/` or `.saffron/proposals/` by hand; change the"," scenario text and re-run. Review proposals with `npx saffron accept`"," (`--all`, or chosen files) and `npx saffron reject`.","- A heal made in CI arrives as a bundle: `npx saffron runs` lists the"," branch's CI runs, `npx saffron import --run <id>` files its proposals"," here; then review and accept as above. `npx saffron trace <scenario>`"," replays a red or healed scenario step by step.","- Secrets go in `{env:VAR}` tokens, never in files.","- Shared test data (accounts, names, enum values) goes in `data/*.json`,",' referenced as `{data:file.path.key}`; check a whole list with "every',' {data:file.list}"; read Examples rows from a file with'," `Examples: {data:file}` (.saffron only). Use `{unique:name}` for a value"," that must be new on every run. Never put secrets in `data/`.",ii,""].join(`
1189
+ `)}function ld(e,t){if(!H.existsSync(e))return H.writeFileSync(e,t),"created";let n=H.readFileSync(e,"utf8"),r=n.indexOf(pd),s=n.indexOf(ii);if(r!==-1&&s!==-1&&s>r){let i=n.slice(0,r)+t.trimEnd()+n.slice(s+ii.length);return i===n?"unchanged":(H.writeFileSync(e,i),"updated")}let o=n.endsWith(`
1078
1190
  `)?`
1079
1191
  `:`
1080
1192
 
1081
- `;return q.writeFileSync(e,n+o+t),"updated"}var Go={command:"npx",args:["saffron","mcp"]},ph={claude:{file:".mcp.json",key:"mcpServers",entry:Go},cursor:{file:".cursor/mcp.json",key:"mcpServers",entry:Go},copilot:{file:".vscode/mcp.json",key:"servers",entry:{type:"stdio",...Go}}},Wl={agents:"codex mcp add saffron -- npx saffron mcp (Codex; other hosts: see their MCP settings)"};function fh(e,t){let n=K.join(e,t.file),r={},s=q.existsSync(n);if(s)try{r=JSON.parse(q.readFileSync(n,"utf8"))}catch{throw new Error(`${t.file} is not valid JSON: fix it or remove it, then re-run`)}let o=r[t.key]??{};return JSON.stringify(o.saffron)===JSON.stringify(t.entry)?"unchanged":(o.saffron=t.entry,r[t.key]=o,q.mkdirSync(K.dirname(n),{recursive:!0}),q.writeFileSync(n,JSON.stringify(r,null,2)+`
1082
- `),s?"updated":"created")}function Kl(e,t){q.mkdirSync(t,{recursive:!0});for(let n of q.readdirSync(e,{withFileTypes:!0})){let r=K.join(e,n.name),s=K.join(t,n.name);n.isDirectory()?Kl(r,s):q.copyFileSync(r,s)}}function Yl(e){let t=K.join(e.packageRoot,"skills","saffron");if(!q.existsSync(K.join(t,"SKILL.md")))throw new Error(`bundled skill not found at ${t}`);let n={skillDirs:[],instructionFiles:[],scaffolded:[],mcpFiles:[],mcpHints:[]},r=e.targets??lh;for(let s of r){let o=K.join(e.projectRoot,Un[s],"saffron");q.existsSync(o)&&q.rmSync(o,{recursive:!0,force:!0}),Kl(t,o),n.skillDirs.push(K.relative(e.projectRoot,o))}if(e.mcp!==!1)for(let s of r){let o=ph[s];o?n.mcpFiles.push({file:o.file,outcome:fh(e.projectRoot,o)}):Wl[s]&&n.mcpHints.push(Wl[s])}if(e.instructions!==!1){let s=dh(),o=K.join(e.projectRoot,"AGENTS.md");n.instructionFiles.push({file:"AGENTS.md",outcome:Gl(o,s)});let i=K.join(e.projectRoot,"CLAUDE.md");q.existsSync(i)&&n.instructionFiles.push({file:"CLAUDE.md",outcome:Gl(i,s)})}if(e.examples){n.examples=uh(e.projectRoot,e.packageRoot);for(let s of n.examples.files)n.scaffolded.push(s)}if(e.scaffold!==!1){let s=K.join(e.projectRoot,"saffron.config.json");q.existsSync(s)||(q.writeFileSync(s,JSON.stringify({baseURL:"http://localhost:3000",features:"features"},null,2)+`
1083
- `),n.scaffolded.push("saffron.config.json"));let o=K.join(e.projectRoot,"features");q.existsSync(o)||(q.mkdirSync(o,{recursive:!0}),n.scaffolded.push("features/"));let i=K.join(e.projectRoot,".gitignore"),a=e.examples?[".saffron/reports/",".saffron/artifacts/",".env"]:[".saffron/reports/",".saffron/artifacts/"],c=q.existsSync(i)?q.readFileSync(i,"utf8"):"",l=[];for(let d of a)c.split(`
1193
+ `;return H.writeFileSync(e,n+o+t),"updated"}var oi={command:"npx",args:["saffron","mcp"]},Ah={claude:{file:".mcp.json",key:"mcpServers",entry:oi},cursor:{file:".cursor/mcp.json",key:"mcpServers",entry:oi},copilot:{file:".vscode/mcp.json",key:"servers",entry:{type:"stdio",...oi}}},dd={agents:"codex mcp add saffron -- npx saffron mcp (Codex; other hosts: see their MCP settings)"};function _h(e,t){let n=K.join(e,t.file),r={},s=H.existsSync(n);if(s)try{r=JSON.parse(H.readFileSync(n,"utf8"))}catch{throw new Error(`${t.file} is not valid JSON: fix it or remove it, then re-run`)}let o=r[t.key]??{};return JSON.stringify(o.saffron)===JSON.stringify(t.entry)?"unchanged":(o.saffron=t.entry,r[t.key]=o,H.mkdirSync(K.dirname(n),{recursive:!0}),H.writeFileSync(n,JSON.stringify(r,null,2)+`
1194
+ `),s?"updated":"created")}function fd(e,t){H.mkdirSync(t,{recursive:!0});for(let n of H.readdirSync(e,{withFileTypes:!0})){let r=K.join(e,n.name),s=K.join(t,n.name);n.isDirectory()?fd(r,s):H.copyFileSync(r,s)}}function ud(e){let t=K.join(e.packageRoot,"skills","saffron");if(!H.existsSync(K.join(t,"SKILL.md")))throw new Error(`bundled skill not found at ${t}`);let n={skillDirs:[],instructionFiles:[],scaffolded:[],mcpFiles:[],mcpHints:[]},r=e.targets??Rh;for(let s of r){let o=K.join(e.projectRoot,Vn[s],"saffron");H.existsSync(o)&&H.rmSync(o,{recursive:!0,force:!0}),fd(t,o),n.skillDirs.push(K.relative(e.projectRoot,o))}if(e.mcp!==!1)for(let s of r){let o=Ah[s];o?n.mcpFiles.push({file:o.file,outcome:_h(e.projectRoot,o)}):dd[s]&&n.mcpHints.push(dd[s])}if(e.instructions!==!1){let s=Eh(),o=K.join(e.projectRoot,"AGENTS.md");n.instructionFiles.push({file:"AGENTS.md",outcome:ld(o,s)});let i=K.join(e.projectRoot,"CLAUDE.md");H.existsSync(i)&&n.instructionFiles.push({file:"CLAUDE.md",outcome:ld(i,s)})}if(e.examples){n.examples=Ih(e.projectRoot,e.packageRoot);for(let s of n.examples.files)n.scaffolded.push(s)}if(e.scaffold!==!1){let s=K.join(e.projectRoot,"saffron.config.json");H.existsSync(s)||(H.writeFileSync(s,JSON.stringify({baseURL:"http://localhost:3000",features:"features"},null,2)+`
1195
+ `),n.scaffolded.push("saffron.config.json"));let o=K.join(e.projectRoot,"features");H.existsSync(o)||(H.mkdirSync(o,{recursive:!0}),n.scaffolded.push("features/"));let i=K.join(e.projectRoot,".gitignore"),a=e.examples?[".saffron/reports/",".saffron/artifacts/",".env"]:[".saffron/reports/",".saffron/artifacts/"],c=H.existsSync(i)?H.readFileSync(i,"utf8"):"",l=[];for(let d of a)c.split(`
1084
1196
  `).some(f=>f.trim()===d)||(c+=(c&&!c.endsWith(`
1085
1197
  `)?`
1086
1198
  `:"")+d+`
1087
- `,l.push(d));l.length>0&&(q.writeFileSync(i,c),n.scaffolded.push(`.gitignore (+ ${l.join(", ")})`))}return n}function uh(e,t){let n=K.join(t,"templates","saucedemo");if(!q.existsSync(K.join(n,"features")))return{installed:!1,files:[],reason:`bundled examples not found at ${n}`};let r=K.join(e,"features");if(q.existsSync(r)&&q.readdirSync(r).length>0)return{installed:!1,files:[],reason:"features/ is not empty; examples are only installed into an empty features directory"};let s=[];q.mkdirSync(r,{recursive:!0});for(let c of q.readdirSync(K.join(n,"features")))q.copyFileSync(K.join(n,"features",c),K.join(r,c)),s.push(`features/${c}`);let o=K.join(n,"data");if(q.existsSync(o)){q.mkdirSync(K.join(e,"data"),{recursive:!0});for(let c of q.readdirSync(o)){let l=K.join(e,"data",c);q.existsSync(l)||(q.copyFileSync(K.join(o,c),l),s.push(`data/${c}`))}}let i=K.join(e,"saffron.config.json");q.existsSync(i)||(q.copyFileSync(K.join(n,"saffron.config.json"),i),s.push("saffron.config.json (baseURL https://www.saucedemo.com, retries 1)"));let a=K.join(e,".env.example");return q.existsSync(a)||(q.copyFileSync(K.join(n,".env.example"),a),s.push(".env.example")),{installed:!0,files:s}}import gh from"node:path";function Zl(e){return["saffron lsp is a language server: it speaks LSP over stdio and is meant","to be launched by your editor, not run by hand (nothing happens here).","","JetBrains (IntelliJ, WebStorm, PyCharm, Rider, incl. Community):"," Easiest: install the Saffron plugin from the JetBrains Marketplace"," (https://plugins.jetbrains.com/plugin/34240-saffron): highlighting,"," this language server, run configurations and the Saffron tool window,"," in one click. Manual setup instead:"," 1. Completion, diagnostics, hover, go-to-definition: install the free"," LSP4IJ plugin, then Settings \u2192 Languages & Frameworks \u2192 Language"," Servers \u2192 +: command `npx saffron lsp`, working dir = project root,"," file patterns *.saffron and *.feature."," 2. Syntax highlighting comes from a TextMate bundle, not the LSP:"," Settings \u2192 Editor \u2192 TextMate Bundles \u2192 + and pick this folder:",` ${e?gh.join(e,"textmate","saffron"):"node_modules/saffron-ai/textmate/saffron"}`,"","Neovim / other LSP editors: cmd = { 'npx', 'saffron', 'lsp' },","filetypes = { 'saffron', 'feature' }.","","VS Code: install the extension instead:"," code --install-extension ChathurangaJayasinghe.saffron-vscode"].join(`
1088
- `)}import{spawn as hh}from"node:child_process";function mh(e,t=process.platform){return t==="win32"?{command:'start "" "%SAFFRON_BROWSER_TARGET%"',args:[],options:{detached:!0,stdio:"ignore",shell:!0,windowsHide:!0,env:{...process.env,SAFFRON_BROWSER_TARGET:e}}}:{command:t==="darwin"?"open":"xdg-open",args:[e],options:{detached:!0,stdio:"ignore"}}}function Vo(e){let{command:t,args:n,options:r}=mh(e),s=t.split(" ")[0],o=hh(t,n,r);o.on("error",()=>console.error(`Could not open a browser (${s} is not available here). Open the address above yourself.`)),o.unref()}function Ql(e,t){let n=t.length>0?t:["features"],r=[];for(let s of n){let o=N.isAbsolute(s)?s:N.join(e,s);if(ae.existsSync(o))if(ae.statSync(o).isDirectory())for(let i of ae.readdirSync(o,{recursive:!0})){let a=String(i);Xe(a)&&r.push(N.join(o,a))}else Xe(o)&&r.push(o)}return r.sort()}function wh(e){let t=_o(e);t.length!==0&&console.error([`note: the ${At[e]} agent is not set up here. Cached scenarios replay fine`,"without AI, but recording and healing need:",...t.map(n=>` \xB7 ${n}`),"Replay-only runs: add --no-agent to silence this note.",""].join(`
1089
- `))}function Xl(e,t){let n=e.provider??"claude";return t===n?{model:e.model,healModel:e.healModel}:((e.model||e.healModel)&&console.error(`note: model and healModel in saffron.config.json name ${At[n]??n} models; this ${At[t]} run uses its default models (--model and --heal-model pick others).`),{})}function Yo(e,t){let n=e??t??"claude";return ol(n)||(console.error(`invalid provider "${n}": use ${Eo.map(r=>`"${r}"`).join(", ")}`),process.exit(2)),n}async function Sh(e){let t=yh.createInterface({input:process.stdin,output:process.stdout});try{let n=await new Promise(r=>t.question(e,r));return/^y(es)?$/i.test(n.trim())}finally{t.close()}}function Yr(e){console.error(`saffron: ${e.message}`),process.exit(2)}function Ko(e){return e>=1e6?`${(e/1e6).toFixed(1)}M`:e>=1e4?`${(e/1e3).toFixed(1)}k`:e.toLocaleString()}var xh={green:"\x1B[32m",yellow:"\x1B[33m",red:"\x1B[31m"},vh="\x1B[0m",J=new bh().name("saffron").description("AI-fallback test runner for end-to-end tests written in plain language: zero-token cached replay, runtime AI healing.").version(Ce()).option("-p, --project-root <dir>","project root",process.cwd());J.command("run").description("Run feature files (cached replay; AI records/heals on demand)").argument("[paths...]","feature files or directories (default: features/)").option("--base-url <url>","base URL of the app under test").option("--env <name>","data environment: data/<file>.<name>.json is laid over data/<file>.json (also SAFFRON_ENV, or env in the config)").option("--headed","run the browser headed").option("--filter <tags>",'only run scenarios whose @tags match: a list ("@smoke,@WA-TC1" runs either) or an expression ("@e2e and not @wip", parentheses allowed); repeat the flag for "or"',_l).option("--no-agent","disable AI fallback (cache misses/failures go red)").option("--rerecord","discard the committed caches of the selected scenarios and let the agent record them fresh (the honest fix for a wrong recording)").option("--provider <name>",'whose agent records and heals: "claude" (default), "codex", "antigravity" or "cursor", on its login on this machine').option("--model <model>","model for the agent").option("--storage-state <file>","Playwright storageState JSON for authenticated sessions").option("--strict","treat passed-with-adaptation (yellow) as failure until its proposal is reviewed").option("--no-verify","skip the zero-AI proof replay of agent recordings before filing proposals").option("--no-reuse","disable Level-1 step reuse (seeding new recordings from existing step recordings)").option("--browser <name>",'browser: "chromium" (default), "chrome", "msedge", "firefox" or "webkit". Recording and healing need one of the first three').option("--heal-model <model>","cheaper model for heal sessions").option("--workers <n>","parallel replay workers (agent recording/healing stays sequential)").option("--shard <index/total>","run one part of the suite, e.g. 2/4 on the second of four CI machines; combine the parts with `saffron report --merge`").option("--shard-by <mode>",'how --shard splits the suite: "scenario" (default, spreads every file) or "file" (each feature file stays on one machine)').option("--no-hooks","skip the setup and teardown commands from the config").option("--trace <mode>",'execution trace per scenario: "off" (default), "on" or "retain-on-failure"; open one with saffron trace').option("--assertion-policy <policy>",'assertion adaptability: "strict" (default) or "adaptable-mid" (mid-scenario assertions may be adapted; the final assertion block never is)').action(async(e,t)=>{let n=N.resolve(J.opts().projectRoot),r=ne(n);t.env&&Me(n,r.dataDir,t.env);let s=t.baseUrl??r.baseURL,o=Ql(n,e.length>0?e:r.features?[r.features]:[]);o.length===0&&(console.error("No .feature/.saffron files found."),process.exit(2));let i=Yo(t.provider,r.provider),a=Xl(r,i);t.agent!==!1&&wh(i);let c;try{let T=Re(n,r,o);c=o.flatMap(O=>he(O,n,T))}catch(T){throw T instanceof ie&&Yr(T),T}let l;try{l=El(t.filter)}catch(T){if(!(T instanceof Fn))throw T;console.error(`${T.message}. Write tags joined with and, or, not and parentheses: "@smoke and not @wip".`),process.exit(2)}l&&(c=Al(c,l),c.length===0&&(console.error(`No scenarios match --filter "${l.text}" in ${o.length} feature file(s).`),process.exit(2)));let d;if(t.shardBy!==void 0&&t.shard===void 0&&(console.error("saffron: --shard-by needs --shard, e.g. --shard 2/4 --shard-by file"),process.exit(2)),t.shard!==void 0){try{let T=Ta(String(t.shardBy??r.shardBy??"scenario")),O=Ra(c,{...ka(String(t.shard)),by:T});c=O.scenarios,d={...O.info,ciRun:br()}}catch(T){console.error(T instanceof Error?T.message:String(T)),process.exit(2)}if(console.log(`shard ${d.index}/${d.total}${d.by==="file"?" (by file)":""}: ${c.length} of ${d.suiteSize} scenario(s)`),c.length===0){let T=new Date,O=eo([],T,s),M=Gs([],T,Be(n).divergent.length,s);console.log(`report: ${no(n,{...O,shard:d,history:M})}`),process.exit(0)}}if(t.rerecord){t.agent===!1&&(console.error("saffron: --rerecord needs the agent; drop --no-agent"),process.exit(2));let T=0;for(let O of c){let M=Y(n,O.featurePath,O.name);ae.existsSync(M)&&(ae.rmSync(M),T++)}console.log(`rerecord: removed ${T} cache file(s) for ${c.length} scenario(s); the agent records them fresh (seeded from the other recordings). Restore with git if needed.`)}{let T=c;try{let M=Ql(n,[]),B=Re(n,r,M);T=M.flatMap(W=>{try{return he(W,n,B)}catch{return[]}})}catch{}let O=li(n,T);if(O.length>0){console.error("cache path collision:");for(let M of O)console.error(` ${M}`);process.exit(2)}}{let T=new Map;for(let M of c){let B=le(M),W=Ue(Y(n,M.featurePath,M.name));W&&B.push(JSON.stringify(W.steps));for(let ce of Xr([...B,...es(le(M))]))process.env[ce]===void 0&&!T.has(ce)&&T.set(ce,M.displayName)}let O=new Map;for(let M of c)for(let B of gt(le(M)))if(!O.has(B))try{qn(B)}catch(W){O.set(B,`${W instanceof Error?W.message:String(W)} (used by "${M.displayName}")`)}if(O.size>0){for(let M of O.values())console.error(M);process.exit(2)}if(T.size>0){for(let[M,B]of T)console.error(`missing environment variable ${M} (referenced as {env:${M}} by "${B}")`);console.error("set the variable(s), or add them to a git-ignored .env in the project root"),process.exit(2)}}let f=[],m=[];if(t.hooks!==!1)try{f=Fo(r.setup,"setup"),m=Fo(r.teardown,"teardown")}catch(T){console.error(T instanceof Error?T.message:String(T)),process.exit(2)}let p=new Er({projectRoot:n,baseURL:s,headed:t.headed,actionTimeoutMs:r.actionTimeoutMs,pollIntervalMs:r.pollIntervalMs,screenshot:r.screenshot,trace:(()=>{let T=t.trace??r.trace??"off";return Ts.includes(T)||(console.error(`invalid --trace "${T}": use ${Ts.map(O=>`"${O}"`).join(", ")}`),process.exit(2)),T})(),retries:r.retries,storageState:t.storageState??r.storageState,browser:(()=>{let T=t.browser??r.browser??"chromium";return Ac(T)||(console.error(`invalid --browser "${T}": use ${wo.map(O=>`"${O}"`).join(", ")}`),process.exit(2)),T})(),healModel:t.healModel??a.healModel,verifyProposals:t.verify===!1?!1:r.verifyProposals,reuseSteps:t.reuse===!1?!1:r.reuseSteps,assertionPolicy:(()=>{let T=t.assertionPolicy??r.assertionPolicy??"strict";return T!=="strict"&&T!=="adaptable-mid"&&(console.error(`invalid --assertion-policy "${T}": use "strict" or "adaptable-mid"`),process.exit(2)),T})(),agent:t.agent?il(i,{model:t.model??a.model,maxTurns:r.maxTurns,snapshotMode:r.snapshotMode}):void 0}),b=new Date,v=Math.max(1,Number.parseInt(String(t.workers??r.workers??1),10)||1),k=new Array(c.length),w=T=>{let O=T.scenario,M=xh[T.status],B=T.usage.inputTokens+T.usage.outputTokens,W=T.usage.cacheReadTokens+T.usage.cacheCreationTokens,ce=W>0?`, ${Ko(W)} cache traffic`:"";console.log(`${M}${T.status.toUpperCase().padEnd(7)}${vh}${O.displayName} (${T.source}, ${T.durationMs}ms, ${T.usage.aiCalls} AI calls, ${B} tokens${ce})`);for(let Ct of T.adaptations)console.log(` \u26A0 ${Ct}`);T.seededSteps&&console.log(` \u26A1 ${T.seededSteps}/${T.stepResults.length} steps seeded from existing recordings (zero AI)`);let Pt=T.stepResults.reduce((Ct,cn)=>Ct+(cn.retries??0),0);Pt&&console.log(` \u21BB ${Pt} action ${Pt===1?"retry":"retries"} absorbed before settling (retries: ${r.retries??1} per action)`),T.verified===!0?console.log(" \u2713 proposal verified by zero-AI proof replay"):T.verified===!1&&console.log(` \u2717 proposal UNVERIFIED, proof replay failed: ${T.proofError?.split(`
1090
- `)[0]}`),T.error&&console.log(` ${T.error}`)},x=new AbortController,y={cwd:n,timeoutMs:r.hookTimeoutMs??ps.hookTimeoutMs,env:{SAFFRON_BASE_URL:s,SAFFRON_ENV:De().env,SAFFRON_SHARD:d?`${d.index}/${d.total}`:void 0}},h=[],S,$=()=>S??=Mo("teardown",m,y).then(T=>{h.push(...T)}),u=Promise.resolve([]);f.length+m.length>0&&process.on("SIGINT",()=>{if(x.signal.aborted){console.error("still cleaning up: teardown finishes, then Saffron exits");return}console.error(`
1091
- interrupted: stopping, then running teardown`),x.abort(),u.then(()=>p.stop()).catch(()=>{}).then($).finally(()=>process.exit(130))}),f.length>0&&(u=Mo("setup",f,{...y,signal:x.signal}),h.push(...await u),x.signal.aborted&&await new Promise(()=>{}),h.some(Wr)&&(console.error("No scenario was run: the application is not in the state the suite expects."),await $(),process.exit(2)));try{await p.start()}catch(T){throw await $(),T instanceof Cn&&(console.error(T.message),process.exit(2)),T}try{if(v>1&&c.length>1){let T=c.map((M,B)=>({s:M,i:B})),O=[];await Promise.all(Array.from({length:Math.min(v,T.length)},async()=>{for(;;){let M=T.shift();if(!M)return;let B=await p.runScenario(M.s,{agentDisabled:!0});B.status==="red"?O.push(M):(k[M.i]=B,w(B))}}));for(let M of O.sort((B,W)=>B.i-W.i)){let B=await p.runScenario(M.s);k[M.i]=B,w(B)}}else for(let T=0;T<c.length;T++){let O=await p.runScenario(c[T]);k[T]=O,w(O)}}finally{await p.stop(),await $()}let g=eo(k,b,s);h.length>0&&(g.hooks=h);let R=Be(n).divergent.length,E=Gs(k,b,R,s);if(!d){let T=$n(n);g.trends=Vs(T,E),Ws(n,E)}g.totals.aiCalls>0&&(g.totals.plan=await p.agent?.planUsage?.());let I;if(d){let T=no(n,{...g,shard:d,history:E}),O=T.replace(/\.json$/,".html");ae.writeFileSync(O,Vt(g,n)),I=`${O}
1092
- ${T}`}else{let{html:T,json:O}=Rn(n,g);I=`${T}
1093
- ${O}`}let A=g.totals,P=A.cacheReadTokens+A.cacheCreationTokens;console.log(`
1094
- ${A.green} passed, ${A.yellow} pending review, ${A.red} failed \xB7 ${A.aiCalls} AI calls \xB7 ${(A.inputTokens+A.outputTokens).toLocaleString()} tokens in+out${P>0?` \xB7 ${Ko(A.cacheReadTokens)} cache read \xB7 ${Ko(A.cacheCreationTokens)} cache written`:""}${zs(A)}${A.models?.length?` \xB7 ${A.models.join(", ")}`:""}`);let _=g.trends;_?.prev&&console.log(qs(_.prev));for(let T of _?.chronic??[])console.log(`\u26A0 chronic: ${T.feature} \u203A ${T.scenario}: ${T.heals} heal(s), ${T.reds} red(s) in last ${T.runs} run(s); re-record or fix the feature text instead of paying for more heals`);console.log(`report: ${I}`),d&&console.log(`shard report, not a whole run: history and trends wait for \`saffron report --merge\` over all ${d.total} shard-*-of-${d.total}.json files`);let j=He(N.resolve(J.opts().projectRoot),Bn);j.length>0&&console.log(`${j.length} cache proposal(s) pending. Review with: saffron accept | saffron reject`);let F=t.strict??r.strict??!1;F&&A.yellow>0&&console.log(`strict mode: ${A.yellow} scenario(s) pending review treated as failure. Review proposals to go green.`);let C=A.red>0||F&&A.yellow>0,L=h.filter(T=>T.phase==="teardown"&&Wr(T));L.length>0&&console.log(`teardown failed: ${L.map(T=>Do(T,y.timeoutMs)).join("; ")}. The environment may hold data from this run.`),process.exit(C?1:L.length>0?2:0)});J.command("accept").description("Promote pending cache proposals to committed caches").argument("[files...]","proposal file(s) (default with --all: every proposal)").option("--all","accept all pending proposals").option("--with-feature-edit","also apply the proposal's suggested .feature step rewrites").option("--propagate","apply the same locator fixes to every other cache using the same locator").option("--include-unverified","with --all: also accept proposals whose proof replay failed").action((e,t)=>{let n=N.resolve(J.opts().projectRoot),r=ne(n),s=e.length>0?e.map(gs).map(o=>({...o,file:N.resolve(o.file)})):t.all?He(n,Bn).map(o=>({file:o.file,revision:void 0})):[];if(s.length===0){rd(n);return}try{for(let{file:o,revision:i}of s){let a=w=>{console.log(`not accepted: ${N.basename(o)} ${w}`),process.exitCode=1},c=nd(n,o,i!==void 0,()=>{if(i&&qe(o)!==i)return a(td);let w;try{w=Ht(o)}catch(h){if(!(h instanceof X))throw h;return a(`cannot be read: ${h.problem}`)}if(w.meta.bound){let h=$e(n,w,Re(n,r,[]));if(h)return a(`is stale: ${h}. Run the scenario again for a proposal made against what is there now, or reject this one.`)}else console.log(`\u26A0 ${N.basename(o)} is ${hr}; accepting as it is.`);if(w.meta.verified===!1){if(t.all&&!t.includeUnverified){console.log(`skipped UNVERIFIED ${N.basename(o)} (${w.meta.proofError?.split(`
1199
+ `,l.push(d));l.length>0&&(H.writeFileSync(i,c),n.scaffolded.push(`.gitignore (+ ${l.join(", ")})`))}return n}function Ih(e,t){let n=K.join(t,"templates","saucedemo");if(!H.existsSync(K.join(n,"features")))return{installed:!1,files:[],reason:`bundled examples not found at ${n}`};let r=K.join(e,"features");if(H.existsSync(r)&&H.readdirSync(r).length>0)return{installed:!1,files:[],reason:"features/ is not empty; examples are only installed into an empty features directory"};let s=[];H.mkdirSync(r,{recursive:!0});for(let c of H.readdirSync(K.join(n,"features")))H.copyFileSync(K.join(n,"features",c),K.join(r,c)),s.push(`features/${c}`);let o=K.join(n,"data");if(H.existsSync(o)){H.mkdirSync(K.join(e,"data"),{recursive:!0});for(let c of H.readdirSync(o)){let l=K.join(e,"data",c);H.existsSync(l)||(H.copyFileSync(K.join(o,c),l),s.push(`data/${c}`))}}let i=K.join(e,"saffron.config.json");H.existsSync(i)||(H.copyFileSync(K.join(n,"saffron.config.json"),i),s.push("saffron.config.json (baseURL https://www.saucedemo.com, retries 1)"));let a=K.join(e,".env.example");return H.existsSync(a)||(H.copyFileSync(K.join(n,".env.example"),a),s.push(".env.example")),{installed:!0,files:s}}import Ph from"node:path";function gd(e){return["saffron lsp is a language server: it speaks LSP over stdio and is meant","to be launched by your editor, not run by hand (nothing happens here).","","JetBrains (IntelliJ, WebStorm, PyCharm, Rider, incl. Community):"," Easiest: install the Saffron plugin from the JetBrains Marketplace"," (https://plugins.jetbrains.com/plugin/34240-saffron): highlighting,"," this language server, run configurations and the Saffron tool window,"," in one click. Manual setup instead:"," 1. Completion, diagnostics, hover, go-to-definition: install the free"," LSP4IJ plugin, then Settings \u2192 Languages & Frameworks \u2192 Language"," Servers \u2192 +: command `npx saffron lsp`, working dir = project root,"," file patterns *.saffron and *.feature."," 2. Syntax highlighting comes from a TextMate bundle, not the LSP:"," Settings \u2192 Editor \u2192 TextMate Bundles \u2192 + and pick this folder:",` ${e?Ph.join(e,"textmate","saffron"):"node_modules/saffron-ai/textmate/saffron"}`,"","Neovim / other LSP editors: cmd = { 'npx', 'saffron', 'lsp' },","filetypes = { 'saffron', 'feature' }.","","VS Code: install the extension instead:"," code --install-extension ChathurangaJayasinghe.saffron-vscode"].join(`
1200
+ `)}import{spawn as Ch}from"node:child_process";function Oh(e,t=process.platform){return t==="win32"?{command:'start "" "%SAFFRON_BROWSER_TARGET%"',args:[],options:{detached:!0,stdio:"ignore",shell:!0,windowsHide:!0,env:{...process.env,SAFFRON_BROWSER_TARGET:e}}}:{command:t==="darwin"?"open":"xdg-open",args:[e],options:{detached:!0,stdio:"ignore"}}}function rs(e){let{command:t,args:n,options:r}=Oh(e),s=t.split(" ")[0],o=Ch(t,n,r);o.on("error",()=>console.error(`Could not open a browser (${s} is not available here). Open the address above yourself.`)),o.unref()}function hd(e,t){let n=t.length>0?t:["features"],r=[];for(let s of n){let o=F.isAbsolute(s)?s:F.join(e,s);if(re.existsSync(o))if(re.statSync(o).isDirectory())for(let i of re.readdirSync(o,{recursive:!0})){let a=String(i);tt(a)&&r.push(F.join(o,a))}else tt(o)&&r.push(o)}return r.sort()}function Fh(e){let t=Uo(e);t.length!==0&&console.error([`note: the ${It[e]} agent is not set up here. Cached scenarios replay fine`,"without AI, but recording and healing need:",...t.map(n=>` \xB7 ${n}`),"Replay-only runs: add --no-agent to silence this note.",""].join(`
1201
+ `))}function md(e,t){let n=e.provider??"claude";return t===n?{model:e.model,healModel:e.healModel}:((e.model||e.healModel)&&console.error(`note: model and healModel in saffron.config.json name ${It[n]??n} models; this ${It[t]} run uses its default models (--model and --heal-model pick others).`),{})}function ci(e,t){let n=e??t??"claude";return wl(n)||(console.error(`invalid provider "${n}": use ${Do.map(r=>`"${r}"`).join(", ")}`),process.exit(2)),n}async function Mh(e){let t=jh.createInterface({input:process.stdin,output:process.stdout});try{let n=await new Promise(r=>t.question(e,r));return/^y(es)?$/i.test(n.trim())}finally{t.close()}}function ss(e){console.error(`saffron: ${e.message}`),process.exit(2)}function ai(e){return e>=1e6?`${(e/1e6).toFixed(1)}M`:e>=1e4?`${(e/1e3).toFixed(1)}k`:e.toLocaleString()}var Dh={green:"\x1B[32m",yellow:"\x1B[33m",red:"\x1B[31m"},Lh="\x1B[0m",q=new Nh().name("saffron").description("AI-fallback test runner for end-to-end tests written in plain language: zero-token cached replay, runtime AI healing.").version(Oe()).option("-p, --project-root <dir>","project root",process.cwd());q.command("run").description("Run feature files (cached replay; AI records/heals on demand)").argument("[paths...]","feature files or directories (default: features/)").option("--base-url <url>","base URL of the app under test").option("--env <name>","data environment: data/<file>.<name>.json is laid over data/<file>.json (also SAFFRON_ENV, or env in the config)").option("--headed","run the browser headed").option("--filter <tags>",'only run scenarios whose @tags match: a list ("@smoke,@WA-TC1" runs either) or an expression ("@e2e and not @wip", parentheses allowed); repeat the flag for "or"',Hl).option("--no-agent","disable AI fallback (cache misses/failures go red)").option("--rerecord","discard the committed caches of the selected scenarios and let the agent record them fresh (the honest fix for a wrong recording)").option("--provider <name>",'whose agent records and heals: "claude" (default), "codex", "antigravity" or "cursor", on its login on this machine').option("--model <model>","model for the agent").option("--storage-state <file>","Playwright storageState JSON for authenticated sessions").option("--strict","treat passed-with-adaptation (yellow) as failure until its proposal is reviewed").option("--no-verify","skip the zero-AI proof replay of agent recordings before filing proposals").option("--no-reuse","disable Level-1 step reuse (seeding new recordings from existing step recordings)").option("--browser <name>",'browser: "chromium" (default), "chrome", "msedge", "firefox" or "webkit". Recording and healing need one of the first three').option("--heal-model <model>","cheaper model for heal sessions").option("--workers <n>","parallel replay workers (agent recording/healing stays sequential)").option("--shard <index/total>","run one part of the suite, e.g. 2/4 on the second of four CI machines; combine the parts with `saffron report --merge`").option("--shard-by <mode>",'how --shard splits the suite: "scenario" (default, spreads every file) or "file" (each feature file stays on one machine)').option("--no-hooks","skip the setup and teardown commands from the config").option("--trace <mode>",'execution trace per scenario: "off" (default), "on" or "retain-on-failure"; open one with saffron trace').option("--assertion-policy <policy>",'assertion adaptability: "strict" (default) or "adaptable-mid" (mid-scenario assertions may be adapted; the final assertion block never is)').action(async(e,t)=>{let n=F.resolve(q.opts().projectRoot),r=ee(n);t.env&&De(n,r.dataDir,t.env);let s=t.baseUrl??r.baseURL,o=hd(n,e.length>0?e:r.features?[r.features]:[]);o.length===0&&(console.error("No .feature/.saffron files found."),process.exit(2));let i=ci(t.provider,r.provider),a=md(r,i);t.agent!==!1&&Fh(i);let c;try{let R=Ee(n,r,o);c=o.flatMap(O=>me(O,n,R))}catch(R){throw R instanceof ce&&ss(R),R}let l;try{l=Bl(t.filter)}catch(R){if(!(R instanceof zn))throw R;console.error(`${R.message}. Write tags joined with and, or, not and parentheses: "@smoke and not @wip".`),process.exit(2)}l&&(c=zl(c,l),c.length===0&&(console.error(`No scenarios match --filter "${l.text}" in ${o.length} feature file(s).`),process.exit(2)));let d;if(t.shardBy!==void 0&&t.shard===void 0&&(console.error("saffron: --shard-by needs --shard, e.g. --shard 2/4 --shard-by file"),process.exit(2)),t.shard!==void 0){try{let R=La(String(t.shardBy??r.shardBy??"scenario")),O=Ua(c,{...Da(String(t.shard)),by:R});c=O.scenarios,d={...O.info,ciRun:Rr()}}catch(R){console.error(R instanceof Error?R.message:String(R)),process.exit(2)}if(console.log(`shard ${d.index}/${d.total}${d.by==="file"?" (by file)":""}: ${c.length} of ${d.suiteSize} scenario(s)`),c.length===0){let R=new Date,O=fo([],R,s),D=eo([],R,ze(n).divergent.length,s);console.log(`report: ${go(n,{...O,shard:d,history:D})}`),process.exit(0)}}if(t.rerecord){t.agent===!1&&(console.error("saffron: --rerecord needs the agent; drop --no-agent"),process.exit(2));let R=0;for(let O of c){let D=Y(n,O.featurePath,O.name);re.existsSync(D)&&(re.rmSync(D),R++)}console.log(`rerecord: removed ${R} cache file(s) for ${c.length} scenario(s); the agent records them fresh (seeded from the other recordings). Restore with git if needed.`)}{let R=c;try{let D=hd(n,[]),W=Ee(n,r,D);R=D.flatMap(U=>{try{return me(U,n,W)}catch{return[]}})}catch{}let O=Si(n,R);if(O.length>0){console.error("cache path collision:");for(let D of O)console.error(` ${D}`);process.exit(2)}}{let R=new Map;for(let D of c){let W=de(D),U=Be(Y(n,D.featurePath,D.name));U&&W.push(JSON.stringify(U.steps));for(let le of as([...W,...cs(de(D))]))process.env[le]===void 0&&!R.has(le)&&R.set(le,D.displayName)}let O=new Map;for(let D of c)for(let W of mt(de(D)))if(!O.has(W))try{Qn(W)}catch(U){O.set(W,`${U instanceof Error?U.message:String(U)} (used by "${D.displayName}")`)}if(O.size>0){for(let D of O.values())console.error(D);process.exit(2)}if(R.size>0){for(let[D,W]of R)console.error(`missing environment variable ${D} (referenced as {env:${D}} by "${W}")`);console.error("set the variable(s), or add them to a git-ignored .env in the project root"),process.exit(2)}}let f=[],m=[];if(t.hooks!==!1)try{f=Go(r.setup,"setup"),m=Go(r.teardown,"teardown")}catch(R){console.error(R instanceof Error?R.message:String(R)),process.exit(2)}let p=new jr({projectRoot:n,baseURL:s,headed:t.headed,actionTimeoutMs:r.actionTimeoutMs,pollIntervalMs:r.pollIntervalMs,screenshot:r.screenshot,trace:(()=>{let R=t.trace??r.trace??"off";return Os.includes(R)||(console.error(`invalid --trace "${R}": use ${Os.map(O=>`"${O}"`).join(", ")}`),process.exit(2)),R})(),retries:r.retries,storageState:t.storageState??r.storageState,browser:(()=>{let R=t.browser??r.browser??"chromium";return zc(R)||(console.error(`invalid --browser "${R}": use ${Io.map(O=>`"${O}"`).join(", ")}`),process.exit(2)),R})(),healModel:t.healModel??a.healModel,verifyProposals:t.verify===!1?!1:r.verifyProposals,reuseSteps:t.reuse===!1?!1:r.reuseSteps,assertionPolicy:(()=>{let R=t.assertionPolicy??r.assertionPolicy??"strict";return R!=="strict"&&R!=="adaptable-mid"&&(console.error(`invalid --assertion-policy "${R}": use "strict" or "adaptable-mid"`),process.exit(2)),R})(),agent:t.agent?vl(i,{model:t.model??a.model,maxTurns:r.maxTurns,snapshotMode:r.snapshotMode}):void 0}),b=new Date,$=Math.max(1,Number.parseInt(String(t.workers??r.workers??1),10)||1),k=new Array(c.length),w=R=>{let O=R.scenario,D=Dh[R.status],W=R.usage.inputTokens+R.usage.outputTokens,U=R.usage.cacheReadTokens+R.usage.cacheCreationTokens,le=U>0?`, ${ai(U)} cache traffic`:"";console.log(`${D}${R.status.toUpperCase().padEnd(7)}${Lh}${O.displayName} (${R.source}, ${R.durationMs}ms, ${R.usage.aiCalls} AI calls, ${W} tokens${le})`);for(let Nt of R.adaptations)console.log(` \u26A0 ${Nt}`);R.seededSteps&&console.log(` \u26A1 ${R.seededSteps}/${R.stepResults.length} steps seeded from existing recordings (zero AI)`);let jt=R.stepResults.reduce((Nt,fn)=>Nt+(fn.retries??0),0);jt&&console.log(` \u21BB ${jt} action ${jt===1?"retry":"retries"} absorbed before settling (retries: ${r.retries??1} per action)`),R.verified===!0?console.log(" \u2713 proposal verified by zero-AI proof replay"):R.verified===!1&&console.log(` \u2717 proposal UNVERIFIED, proof replay failed: ${R.proofError?.split(`
1202
+ `)[0]}`),R.error&&console.log(` ${R.error}`)},x=new AbortController,y={cwd:n,timeoutMs:r.hookTimeoutMs??ws.hookTimeoutMs,env:{SAFFRON_BASE_URL:s,SAFFRON_ENV:Le().env,SAFFRON_SHARD:d?`${d.index}/${d.total}`:void 0}},u=[],v,S=()=>v??=Vo("teardown",m,y).then(R=>{u.push(...R)}),h=Promise.resolve([]);f.length+m.length>0&&process.on("SIGINT",()=>{if(x.signal.aborted){console.error("still cleaning up: teardown finishes, then Saffron exits");return}console.error(`
1203
+ interrupted: stopping, then running teardown`),x.abort(),h.then(()=>p.stop()).catch(()=>{}).then(S).finally(()=>process.exit(130))}),f.length>0&&(h=Vo("setup",f,{...y,signal:x.signal}),u.push(...await h),x.signal.aborted&&await new Promise(()=>{}),u.some(es)&&(console.error("No scenario was run: the application is not in the state the suite expects."),await S(),process.exit(2)));try{await p.start()}catch(R){throw await S(),R instanceof Dn&&(console.error(R.message),process.exit(2)),R}try{if($>1&&c.length>1){let R=c.map((D,W)=>({s:D,i:W})),O=[];await Promise.all(Array.from({length:Math.min($,R.length)},async()=>{for(;;){let D=R.shift();if(!D)return;let W=await p.runScenario(D.s,{agentDisabled:!0});W.status==="red"?O.push(D):(k[D.i]=W,w(W))}}));for(let D of O.sort((W,U)=>W.i-U.i)){let W=await p.runScenario(D.s);k[D.i]=W,w(W)}}else for(let R=0;R<c.length;R++){let O=await p.runScenario(c[R]);k[R]=O,w(O)}}finally{await p.stop(),await S()}let g=fo(k,b,s);u.length>0&&(g.hooks=u);let T=ze(n).divergent.length,E=eo(k,b,T,s);if(!d){let R=En(n);g.trends=no(R,E),to(n,E)}g.totals.aiCalls>0&&(g.totals.plan=await p.agent?.planUsage?.());let _;if(d){let R=go(n,{...g,shard:d,history:E}),O=R.replace(/\.json$/,".html");re.writeFileSync(O,Zt(g,n)),_=`${O}
1204
+ ${R}`}else{let{html:R,json:O}=Pn(n,g);_=`${R}
1205
+ ${O}`}let A=g.totals,C=A.cacheReadTokens+A.cacheCreationTokens;console.log(`
1206
+ ${A.green} passed, ${A.yellow} pending review, ${A.red} failed \xB7 ${A.aiCalls} AI calls \xB7 ${(A.inputTokens+A.outputTokens).toLocaleString()} tokens in+out${C>0?` \xB7 ${ai(A.cacheReadTokens)} cache read \xB7 ${ai(A.cacheCreationTokens)} cache written`:""}${Ys(A)}${A.models?.length?` \xB7 ${A.models.join(", ")}`:""}`);let I=g.trends;I?.prev&&console.log(Qs(I.prev));for(let R of I?.chronic??[])console.log(`\u26A0 chronic: ${R.feature} \u203A ${R.scenario}: ${R.heals} heal(s), ${R.reds} red(s) in last ${R.runs} run(s); re-record or fix the feature text instead of paying for more heals`);console.log(`report: ${_}`),d&&console.log(`shard report, not a whole run: history and trends wait for \`saffron report --merge\` over all ${d.total} shard-*-of-${d.total}.json files`);let j=Je(F.resolve(q.opts().projectRoot),Kn);j.length>0&&console.log(`${j.length} cache proposal(s) pending. Review with: saffron accept | saffron reject`);let M=t.strict??r.strict??!1;M&&A.yellow>0&&console.log(`strict mode: ${A.yellow} scenario(s) pending review treated as failure. Review proposals to go green.`);let z=A.red>0||M&&A.yellow>0,P=u.filter(R=>R.phase==="teardown"&&es(R));P.length>0&&console.log(`teardown failed: ${P.map(R=>Ko(R,y.timeoutMs)).join("; ")}. The environment may hold data from this run.`),process.exit(z?1:P.length>0?2:0)});q.command("accept").description("Promote pending cache proposals to committed caches").argument("[files...]","proposal file(s) (default with --all: every proposal)").option("--all","accept all pending proposals").option("--with-feature-edit","also apply the proposal's suggested .feature step rewrites").option("--propagate","apply the same locator fixes to every other cache using the same locator").option("--include-unverified","with --all: also accept proposals whose proof replay failed").action((e,t)=>{let n=F.resolve(q.opts().projectRoot),r=ee(n),s=e.length>0?e.map(xs).map(o=>({...o,file:F.resolve(o.file)})):t.all?Je(n,Kn).map(o=>({file:o.file,revision:void 0})):[];if(s.length===0){vd(n);return}try{for(let{file:o,revision:i}of s){let a=w=>{console.log(`not accepted: ${F.basename(o)} ${w}`),process.exitCode=1},c=wd(n,o,i!==void 0,()=>{if(i&&qe(o)!==i)return a(bd);let w;try{w=Gt(o)}catch(u){if(!(u instanceof X))throw u;return a(`cannot be read: ${u.problem}`)}if(w.meta.bound){let u=ke(n,w,Ee(n,r,[]));if(u)return a(`is stale: ${u}. Run the scenario again for a proposal made against what is there now, or reject this one.`)}else console.log(`\u26A0 ${F.basename(o)} is ${$r}; accepting as it is.`);if(w.meta.verified===!1){if(t.all&&!t.includeUnverified){console.log(`skipped UNVERIFIED ${F.basename(o)} (${w.meta.proofError?.split(`
1095
1207
  `)[0]}). Re-record it (saffron run --rerecord), or accept it explicitly by file, or pass --include-unverified.`);return}console.log(`\u26A0 accepting UNVERIFIED proposal (proof replay failed: ${w.meta.proofError?.split(`
1096
- `)[0]}): expect a heal or red on the next run.`)}let x=w.cache.steps.map(h=>h.gherkin);if(t.withFeatureEdit&&(w.meta.featureEdits??[]).length>0){let h=Tl(n,w.cache.feature,w.cache.scenario,x,Re(n,r,[]));if(h)return a(`carries feature edits, and ${h}. Run the scenario again for a fresh proposal, or accept this one without --with-feature-edit.`)}let y=me(Y(n,w.cache.feature,w.cache.scenario));return{proposal:w,recordedSteps:x,oldCache:y,cacheFile:or(n,o)}},a);if(!c)continue;let{proposal:l,recordedSteps:d,oldCache:f,cacheFile:m}=c;console.log(`accepted \u2192 ${m}`);let p=f?vl(f,l.cache):{changes:[],ambiguous:[]},b=p.changes;for(let w of p.ambiguous)console.log(` locator fix for ${Jr(w)} not propagated: this heal sent it to more than one place, so other caches are left alone.`);if(b.length>0){let w=$l(n,b,m,!t.propagate);for(let x of b)console.log(` locator fix: ${Jr(x.from)} \u2192 ${Jr(x.to)}`);if(w.length===0)console.log(" no other caches use this locator.");else if(t.propagate)for(let x of w)console.log(` propagated \u2192 ${x.feature} \u203A ${x.scenario} (${x.steps.map(y=>`"${y.gherkin}"`).join(", ")})`);else{console.log(` ${w.length} other cache(s) use the same locator: re-run with --propagate to fix them too:`);for(let x of w)console.log(` \xB7 ${x.feature} \u203A ${x.scenario}`)}}let v=l.meta.featureEdits??[];if(!t.withFeatureEdit||v.length===0)continue;let k=Rl(n,l.cache.feature,l.cache.scenario,v,Re(n,r,[]),d);for(let w of k.applied){let x=w.fromStepSet?`${w.file}:${w.line} (StepSet "${w.fromStepSet}", fixes every invoking scenario)`:`${w.file}:${w.line}`;console.log(` feature edit ${x}:`),console.log(` - ${w.from}`),console.log(` + ${w.to}`)}for(let w of k.skipped)console.log(` skipped edit [${w.index}]: ${w.reason}`);if(k.appliedIndices.length>0){let w=new Set(k.appliedIndices),x=me(m);for(let y of v)w.has(y.index)&&x.steps[y.index]&&(x.steps[y.index].gherkin=y.text);Lt(m,x)}}}catch(o){throw o instanceof ie&&Yr(o),o}});J.command("steps").description("List/search the project's step vocabulary (files + caches): what exists, what's recorded, what to reuse").argument("[search]","case-insensitive substring filter").option("--json","machine-readable vocabulary").option("--snippets","write .vscode/saffron.code-snippets for native VS Code completion").action((e,t)=>{let n=N.resolve(J.opts().projectRoot),r=ne(n),s;try{s=ze(n,r.features??"features")}catch(i){throw i instanceof ie&&Yr(i),i}if(e&&(s=Xn(s,e)),t.json){console.log(JSON.stringify(s,null,2));return}if(t.snippets){let i=N.join(n,".vscode","saffron.code-snippets");ae.mkdirSync(N.dirname(i),{recursive:!0}),ae.writeFileSync(i,mi(s)),console.log(`wrote ${s.steps.length} step + ${s.stepSets.length} StepSet snippet(s) \u2192 ${i}`),console.log("re-run after recording sessions to keep completions current");return}let o={recorded:"\x1B[32m\u25CF\x1B[0m",divergent:"\x1B[33m\u25CF\x1B[0m",unrecorded:"\u25CB"};if(s.stepSets.length>0){console.log("step sets:");for(let i of s.stepSets)console.log(` ${i.name} (${i.steps.length} steps, ${i.usage} scenario(s)), ${i.file}:${i.line}`);console.log("")}for(let i of s.steps){let a=i.fromStepSet?` [${i.fromStepSet}]`:"";console.log(`${o[i.status]} ${i.keyword} ${i.text} \xD7${i.usage}${a}`)}console.log(`
1097
- ${s.steps.length} step(s) across ${s.scannedFiles} file(s) \xB7 ${s.scannedCaches} cache(s) \xB7 \x1B[32m\u25CF\x1B[0m recorded (replays free) \xB7 \x1B[33m\u25CF\x1B[0m divergent \xB7 \u25CB unrecorded`)});J.command("author").description("Draft a feature file from plain-paragraph requirements, reusing the project's step vocabulary (AI)").argument("<prose-file>","text/markdown file with the requirements").option("-o, --out <file>","output path (default: features/<name>.saffron)").option("--provider <name>",'whose agent writes the draft: "claude" (default), "codex", "antigravity" or "cursor"').option("--model <model>","model for authoring").action(async(e,t)=>{let n=N.resolve(J.opts().projectRoot),r=ne(n),s=Yo(t.provider,r.provider),o=ae.readFileSync(N.resolve(e),"utf8"),i;try{i=ze(n,r.features??"features")}catch(d){throw d instanceof ie&&Yr(d),d}console.log(`authoring with ${i.steps.length} known step(s) and ${i.stepSets.length} step set(s) as vocabulary\u2026`);let a=await Ya(o,i,t.model??Xl(r,s).model,s==="claude"?void 0:Ao[s]),c=t.out?N.resolve(t.out):N.join(n,r.features??"features",`${N.basename(e).replace(/\.[^.]+$/,"")}.saffron`);ae.existsSync(c)&&(console.error(`refusing to overwrite ${c}: pass --out for a new path`),process.exit(1)),ae.mkdirSync(N.dirname(c),{recursive:!0}),ae.writeFileSync(c,a.content);let l=a.content.split(`
1098
- `).map(d=>d.trim().replace(/^(Given|When|Then|And|But|\*)\s+/,"")).filter(d=>i.steps.some(f=>f.text===d)).length;console.log(`wrote ${c}`),console.log(`${l} step line(s) reuse the existing vocabulary (seedable)${a.usage.costUsd?` \xB7 $${a.usage.costUsd.toFixed(4)}`:""}`),console.log("review the draft, then record it: saffron run "+N.relative(process.cwd(),c))});J.command("login").description("Check or set up an AI provider's login on this machine (Cursor: a one-time sign-in for Saffron; Antigravity: allows Saffron's tools in agy)").argument("[provider]",'"claude", "codex", "antigravity" or "cursor" (default: the configured provider)').action(async e=>{let t=N.resolve(J.opts().projectRoot),n=Yo(e,ne(t).provider),r=n==="cursor"?nl:n==="antigravity"?Kc:void 0;if(r)try{await r()}catch(o){console.error(o instanceof Error?o.message:String(o)),process.exit(1)}let s=_o(n);if(s.length===0){console.log(`${At[n]}: ready to record and heal.`);return}console.log([`${At[n]} still needs:`,...s.map(o=>` \xB7 ${o}`)].join(`
1099
- `)),process.exit(1)});J.command("mcp").description("Serve the step vocabulary to AI assistants over stdio MCP (search_steps, list_step_sets)").action(async()=>{let e=N.resolve(J.opts().projectRoot),t=ne(e);await Ga(e,t.features??"features")});J.command("lsp").description("Run the Saffron language server over stdio (completion, go-to-definition, hover, diagnostics). For JetBrains (LSP4IJ / Ultimate LSP API), Neovim, and any LSP-capable editor").action(()=>{process.stdin.isTTY&&(console.error(Zl(tr())),process.exit(2));let e=N.resolve(J.opts().projectRoot),t=ne(e),n=process.argv.includes("-p")||process.argv.some(r=>r.startsWith("--project-root"));oc(e,t.features??"features",{rootFromClient:!n})});J.command("reject").description("Discard pending cache proposals (AI will retry next run)").argument("[files...]","proposal file(s)").option("--all","reject all pending proposals").action((e,t)=>{let n=N.resolve(J.opts().projectRoot),r=e.length>0?e.map(gs).map(s=>({...s,file:N.resolve(s.file)})):t.all?He(n,Bn).map(s=>({file:s.file,revision:void 0})):[];if(r.length===0){rd(n);return}for(let{file:s,revision:o}of r){let i=nd(n,s,o!==void 0,()=>{if(o&&qe(s)!==o)return td;ir(s)},a=>a);if(i){console.log(`not rejected: ${N.basename(s)} ${i}`),process.exitCode=1;continue}console.log(`rejected ${s}`)}});J.command("init").description("Make the project agent-ready: install the bundled Saffron skill into agent directories, add instructions to AGENTS.md, scaffold config").option("--agents <list>",`comma-separated targets: ${Object.keys(Un).join(", ")} (default: claude,agents)`).option("--no-instructions","do not touch AGENTS.md / CLAUDE.md").option("--no-scaffold","do not create saffron.config.json, features/, .gitignore entry").option("--no-mcp","do not register `saffron mcp` in project MCP configs (.mcp.json, .cursor/mcp.json, .vscode/mcp.json)").option("--examples","install the Saucedemo example suite (features, config, .env.example) for a guided first run").option("--no-examples","never install the example suite (skips the prompt)").action(async e=>{let t=N.resolve(J.opts().projectRoot),n=tr();n||(console.error("saffron: cannot locate the installed package root"),process.exit(2));let r=e.agents?String(e.agents).split(",").map(i=>i.trim()).filter(Boolean):void 0;for(let i of r??[])i in Un||(console.error(`saffron: unknown agent target "${i}" (expected one of ${Object.keys(Un).join(", ")})`),process.exit(2));let s=e.examples===!0;e.examples===void 0&&process.stdin.isTTY&&process.stdout.isTTY&&(s=await Sh("Install the Saucedemo example tests (login, cart, checkout) into features/ for a guided first run? [y/N] "));let o=Yl({projectRoot:t,packageRoot:n,targets:r,instructions:e.instructions,scaffold:e.scaffold,mcp:e.mcp,examples:s});for(let i of o.skillDirs)console.log(`skill \u2192 ${i}/`);for(let i of o.mcpFiles)console.log(`${i.outcome.padEnd(9)} \u2192 ${i.file} (saffron mcp server)`);for(let i of o.mcpHints)console.log(`mcp hint \u2192 ${i}`);for(let i of o.instructionFiles)console.log(`${i.outcome.padEnd(9)} \u2192 ${i.file} (managed Saffron block)`);for(let i of o.scaffolded)console.log(`created \u2192 ${i}`);o.examples&&!o.examples.installed&&console.log(`examples \u2192 not installed: ${o.examples.reason}`),console.log("\nAgents now load the saffron skill when writing tests. Re-run after `npm update saffron-ai` to refresh it."),o.examples?.installed&&console.log(["","Your guided first run (recording needs Claude credentials; replays never do):"," cp .env.example .env # public demo credentials, resolved at replay"," npx playwright install chromium"," npx saffron run --filter @smoke # the agent records two scenarios (about $3 of AI usage), files proposals"," npx saffron accept --all # review, then promote them to caches"," npx saffron run --filter @smoke # replays at zero tokens","Then try the rest: npx saffron run (login errors outline, checkout with a data table)"].join(`
1100
- `))});J.command("export").description("Pack the pending proposals, the last run's report and its screenshots and traces into one bundle, for review on another checkout (CI uploads it as an artifact)").option("-o, --out <file>","where to write the bundle (default: .saffron/reports/saffron-bundle.zip)").option("--branch <name>","also commit the pending proposals (not the caches) onto the commit the run tested as a new branch, push it to origin, and open a draft pull request (GitHub through gh, Azure DevOps through az)").option("--base <branch>","with --branch: the branch the pull request is for (default: the branch the run tested)").action(e=>{let t=N.resolve(J.opts().projectRoot);e.base!==void 0&&e.branch===void 0&&(console.error("saffron export: --base needs --branch"),process.exit(2));try{Ln("--branch",e.branch),Ln("--base",e.base)}catch(a){console.error(`saffron export: ${a.message}`),process.exit(2)}let n;try{n=La(t,{out:e.out?N.resolve(e.out):void 0})}catch(a){console.error(`saffron export: ${a instanceof Error?a.message:String(a)}`),process.exit(2)}for(let a of n.unreadable)console.error(`\u26A0 proposal ${a.file} cannot be read: ${a.problem}. Left out of the bundle.`);let r=n.manifest;if(console.log(`bundle: ${n.file}`),console.log(` ${r.proposals.length} proposal(s) \xB7 ${r.evidence.length} screenshot and trace file(s) \xB7 ${r.report?"the run's report":"no run report"}${r.shard?` \xB7 shard ${r.shard.index}/${r.shard.total}`:""}`),console.log(` from ${co(r)}`),n.missing.length>0&&console.log(` ${n.missing.length} file(s) the report names are gone, or were replaced by a later run: left out`),n.dropped.length>0&&console.log(` ${n.dropped.length} trace(s) of green scenarios left out: with them the bundle would be over the 4 GB one bundle holds`),console.log("Review it in a checkout of this repository with: saffron import <bundle>"),e.branch===void 0)return;if(n.proposals.length===0){console.log("No pending proposals: no branch to push.");return}let s;try{let a=ne(t);s=Jl(t,n,String(e.branch),{base:e.base,config:a})}catch(a){console.error(`saffron export: the bundle is written, but no branch was pushed: ${a instanceof Error?a.message:String(a)}`),process.exit(2)}for(let a of s.leftOut)console.error(`\u26A0 left out of the branch: ${N.relative(t,a.file)} ${a.reason}`);let o=n.proposals.length-s.leftOut.length,i=o===1?"the proposal":`${o} proposals`;console.log(`branch: ${s.branch} pushed to origin (${s.commit.slice(0,7)}: ${i} on top of ${s.parent.slice(0,7)})`),s.pullRequest?console.log(`draft pull request: ${s.pullRequest}`):(console.log(`no pull request: ${s.noPullRequest}. Open one from ${s.branch} to ${s.base}; its description is in ${s.bodyFile}`),s.failed&&(process.exitCode=1))});J.command("runs").description("List the CI runs of the checkout's branch (GitHub Actions through gh, Azure Pipelines through az), with how many of each run's proposals are pending here; import one with saffron import --run <id>").option("--branch <name>","another branch (default: the checkout's; every branch on a detached HEAD)").option("--limit <n>","how many runs (default 20)").option("--provider <name>",`"github" or "azure" (default: ci.provider in the config, else the repository's host)`).option("--json","machine-readable output").action(e=>{let t=N.resolve(J.opts().projectRoot),n=e.limit===void 0?void 0:Number(e.limit);n!==void 0&&!(Number.isInteger(n)&&n>0)&&(console.error(`invalid --limit "${e.limit}": use a whole number above 0`),process.exit(2));let r;try{r=Dl(t,ne(t),{provider:e.provider,branch:e.branch,limit:n})}catch(i){let a=i instanceof Error?i.message:String(i);e.json?console.log(JSON.stringify({error:a},null,2)):console.error(`saffron runs: ${a}`),process.exit(2)}if(r.problem&&(process.exitCode=2),e.json){console.log(JSON.stringify(r.problem?{...r,error:r.problem.message}:r,null,2));return}if(r.problem){console.error(`saffron runs: ${r.problem.message}`);return}let s=`${Uo[r.provider]} runs ${r.branch?`on ${r.branch}`:"on every branch"}`;if(r.runs.length===0){console.log(`No ${s}.`);return}let o=i=>i.status!=="completed"?"\u25CF":i.conclusion==="success"?"\u2713":i.conclusion==="failure"?"\u2717":"\u25CB";console.log(`${s}:`);for(let i of r.runs){let a=[`${i.name} #${i.number}`,i.status==="completed"?i.conclusion:i.status.replace("_"," "),Fl(i.createdAt),i.commit?.slice(0,7),i.pending?`${i.pending} proposal${i.pending===1?"":"s"} pending here`:void 0].filter(Boolean);console.log(` ${i.id.padStart(12)} ${o(i)} ${a.join(" \xB7 ")}`)}console.log("Import one with: saffron import --run <id>")});J.command("import").description("Bring a bundle from `saffron export` (a CI run) into this checkout: its proposals, each checked against the files here, and the run's report, screenshots and traces. Then review and accept as usual").argument("[bundles...]","bundle files, folders holding saffron-bundle*.zip (a CI artifact download), or http(s) URLs").option("--run <id>","instead: download the bundles of this CI run (an id from saffron runs) through gh or az, and import them").option("--provider <name>",`with --run: "github" or "azure" (default: ci.provider in the config, else the repository's host)`).option("--overwrite","replace a different proposal already pending here for the same scenario").option("--allow-unknown-repository","import a bundle that does not say which repository it comes from (made outside a git checkout); its proposals are still checked by their bindings").option("--json","machine-readable output").action(async(e,t)=>{let n=N.resolve(J.opts().projectRoot);function r(d,f=!1){t.json?console.log(JSON.stringify({error:d},null,2)):console.error(`saffron import: ${d}${f?"":". Nothing was imported."}`),process.exit(2)}t.run===void 0==(e.length===0)&&r("give bundle files, folders or URLs, or --run <id>, and not both"),t.provider!==void 0&&t.run===void 0&&r("--provider goes with --run");let s,o,i;try{let d=ne(n),f=Re(n,d,[]);t.run!==void 0&&(i=Ll(n,d,String(t.run),{provider:t.provider}));let m=i?[i.dir]:e.map(b=>/^https?:\/\//i.test(b)?b:N.resolve(b)),p=i;s=await za(n,m,f,{overwrite:t.overwrite===!0,allowUnknownRepository:t.allowUnknownRepository===!0,describe:p?b=>`${Uo[p.provider]} run ${t.run} (${N.basename(N.dirname(b))})`:void 0})}catch(d){o=d}finally{i?.remove()}if((o instanceof U||o instanceof ie)&&r(o.message),(o!==void 0||!s)&&r(o instanceof Error?o.message:String(o),!0),s.proposals.some(d=>d.outcome==="refused")&&(process.exitCode=1),t.json){console.log(JSON.stringify(i?{run:{provider:i.provider,id:String(t.run)},...s}:s,null,2));return}for(let d of s.bundles)console.log(`bundle: ${d.source}`),console.log(` from ${co(d.origin)}${d.shard?` \xB7 shard ${d.shard.index}/${d.shard.total}`:""} \xB7 saffron-ai ${d.runner}`);for(let d of s.warnings)console.log(`\u26A0 ${d}`);for(let d of s.proposals){let f=d.feature?`${d.feature} \u203A ${d.scenario}`:d.file;d.outcome==="imported"?console.log(`imported \u2192 ${d.file} (${f})`):d.outcome==="unchanged"?console.log(`unchanged: ${f}: ${d.reason??"already pending here"}`):console.log(`not imported: ${f} ${d.reason}`)}let{written:a,unchanged:c}=s.evidence;a>0&&console.log(`evidence: ${a} screenshot and trace file(s) written${c?`, ${c} already here`:""}`),s.report==="written"&&console.log("report: .saffron/reports/latest.html now shows the imported run (saffron report, saffron trace)");let l=s.proposals.filter(d=>d.outcome==="imported").length;l===0&&a===0&&s.report!=="written"&&!s.proposals.some(d=>d.outcome==="refused")?console.log("Nothing changed: this was already imported."):l>0&&console.log("Review with: saffron diff \xB7 accept with: saffron accept <file> | --all \xB7 then commit the caches with your git tools")});J.command("diff").description("Show what a pending proposal changes in the committed recording: the action list a reviewer is approving, not just the narrative").argument("[files...]","proposal file(s); default: every pending proposal").option("--json","machine-readable output").action((e,t)=>{let n=N.resolve(J.opts().projectRoot),r=e.length>0?e.map(o=>N.resolve(o)):He(n,Bn).map(o=>o.file);if(r.length===0){console.log("No pending proposals.");return}let s=r.map(o=>{let i=Ht(o),a=me(Y(n,i.cache.feature,i.cache.scenario));return{file:N.relative(n,o),verified:i.meta.verified,stale:i.meta.bound?ed(n,i):hr,...lt(a,i.cache)}});if(t.json){console.log(JSON.stringify(s.length===1?s[0]:s,null,2));return}console.log(s.map(o=>`${fr(o)}${o.stale?`
1208
+ `)[0]}): expect a heal or red on the next run.`)}let x=w.cache.steps.map(u=>u.gherkin);if(t.withFeatureEdit&&(w.meta.featureEdits??[]).length>0){let u=Ll(n,w.cache.feature,w.cache.scenario,x,Ee(n,r,[]));if(u)return a(`carries feature edits, and ${u}. Run the scenario again for a fresh proposal, or accept this one without --with-feature-edit.`)}let y=ye(Y(n,w.cache.feature,w.cache.scenario));return{proposal:w,recordedSteps:x,oldCache:y,cacheFile:ur(n,o)}},a);if(!c)continue;let{proposal:l,recordedSteps:d,oldCache:f,cacheFile:m}=c;console.log(`accepted \u2192 ${m}`);let p=f?Fl(f,l.cache):{changes:[],ambiguous:[]},b=p.changes;for(let w of p.ambiguous)console.log(` locator fix for ${Zr(w)} not propagated: this heal sent it to more than one place, so other caches are left alone.`);if(b.length>0){let w=Ml(n,b,m,!t.propagate);for(let x of b)console.log(` locator fix: ${Zr(x.from)} \u2192 ${Zr(x.to)}`);if(w.length===0)console.log(" no other caches use this locator.");else if(t.propagate)for(let x of w)console.log(` propagated \u2192 ${x.feature} \u203A ${x.scenario} (${x.steps.map(y=>`"${y.gherkin}"`).join(", ")})`);else{console.log(` ${w.length} other cache(s) use the same locator: re-run with --propagate to fix them too:`);for(let x of w)console.log(` \xB7 ${x.feature} \u203A ${x.scenario}`)}}let $=l.meta.featureEdits??[];if(!t.withFeatureEdit||$.length===0)continue;let k=Ul(n,l.cache.feature,l.cache.scenario,$,Ee(n,r,[]),d);for(let w of k.applied){let x=w.fromStepSet?`${w.file}:${w.line} (StepSet "${w.fromStepSet}", fixes every invoking scenario)`:`${w.file}:${w.line}`;console.log(` feature edit ${x}:`),console.log(` - ${w.from}`),console.log(` + ${w.to}`)}for(let w of k.skipped)console.log(` skipped edit [${w.index}]: ${w.reason}`);if(k.appliedIndices.length>0){let w=new Set(k.appliedIndices),x=ye(m);for(let y of $)w.has(y.index)&&x.steps[y.index]&&(x.steps[y.index].gherkin=y.text);zt(m,x)}}}catch(o){throw o instanceof ce&&ss(o),o}});q.command("steps").description("List/search the project's step vocabulary (files + caches): what exists, what's recorded, what to reuse").argument("[search]","case-insensitive substring filter").option("--json","machine-readable vocabulary").option("--snippets","write .vscode/saffron.code-snippets for native VS Code completion").action((e,t)=>{let n=F.resolve(q.opts().projectRoot),r=ee(n),s;try{s=He(n,r.features??"features")}catch(i){throw i instanceof ce&&ss(i),i}if(e&&(s=ar(s,e)),t.json){console.log(JSON.stringify(s,null,2));return}if(t.snippets){let i=F.join(n,".vscode","saffron.code-snippets");re.mkdirSync(F.dirname(i),{recursive:!0}),re.writeFileSync(i,Ai(s)),console.log(`wrote ${s.steps.length} step + ${s.stepSets.length} StepSet snippet(s) \u2192 ${i}`),console.log("re-run after recording sessions to keep completions current");return}let o={recorded:"\x1B[32m\u25CF\x1B[0m",divergent:"\x1B[33m\u25CF\x1B[0m",unrecorded:"\u25CB"};if(s.stepSets.length>0){console.log("step sets:");for(let i of s.stepSets)console.log(` ${i.name} (${i.steps.length} steps, ${i.usage} scenario(s)), ${i.file}:${i.line}`);console.log("")}for(let i of s.steps){let a=i.fromStepSet?` [${i.fromStepSet}]`:"";console.log(`${o[i.status]} ${i.keyword} ${i.text} \xD7${i.usage}${a}`)}console.log(`
1209
+ ${s.steps.length} step(s) across ${s.scannedFiles} file(s) \xB7 ${s.scannedCaches} cache(s) \xB7 \x1B[32m\u25CF\x1B[0m recorded (replays free) \xB7 \x1B[33m\u25CF\x1B[0m divergent \xB7 \u25CB unrecorded`)});q.command("author").description("Draft a feature file from plain-paragraph requirements, reusing the project's step vocabulary (AI)").argument("<prose-file>","text/markdown file with the requirements").option("-o, --out <file>","output path (default: features/<name>.saffron)").option("--provider <name>",'whose agent writes the draft: "claude" (default), "codex", "antigravity" or "cursor"').option("--model <model>","model for authoring").action(async(e,t)=>{let n=F.resolve(q.opts().projectRoot),r=ee(n),s=ci(t.provider,r.provider),o=re.readFileSync(F.resolve(e),"utf8"),i;try{i=He(n,r.features??"features")}catch(d){throw d instanceof ce&&ss(d),d}console.log(`authoring with ${i.steps.length} known step(s) and ${i.stepSets.length} step set(s) as vocabulary\u2026`);let a=await dc(o,i,t.model??md(r,s).model,s==="claude"?void 0:Lo[s]),c=t.out?F.resolve(t.out):F.join(n,r.features??"features",`${F.basename(e).replace(/\.[^.]+$/,"")}.saffron`);re.existsSync(c)&&(console.error(`refusing to overwrite ${c}: pass --out for a new path`),process.exit(1)),re.mkdirSync(F.dirname(c),{recursive:!0}),re.writeFileSync(c,a.content);let l=a.content.split(`
1210
+ `).map(d=>d.trim().replace(/^(Given|When|Then|And|But|\*)\s+/,"")).filter(d=>i.steps.some(f=>f.text===d)).length;console.log(`wrote ${c}`),console.log(`${l} step line(s) reuse the existing vocabulary (seedable)${a.usage.costUsd?` \xB7 $${a.usage.costUsd.toFixed(4)}`:""}`),console.log("review the draft, then record it: saffron run "+F.relative(process.cwd(),c))});q.command("login").description("Check or set up an AI provider's login on this machine (Cursor: a one-time sign-in for Saffron; Antigravity: allows Saffron's tools in agy)").argument("[provider]",'"claude", "codex", "antigravity" or "cursor" (default: the configured provider)').action(async e=>{let t=F.resolve(q.opts().projectRoot),n=ci(e,ee(t).provider),r=n==="cursor"?ml:n==="antigravity"?ll:void 0;if(r)try{await r()}catch(o){console.error(o instanceof Error?o.message:String(o)),process.exit(1)}let s=Uo(n);if(s.length===0){console.log(`${It[n]}: ready to record and heal.`);return}console.log([`${It[n]} still needs:`,...s.map(o=>` \xB7 ${o}`)].join(`
1211
+ `)),process.exit(1)});q.command("mcp").description("Serve the step vocabulary to AI assistants over stdio MCP (search_steps, list_step_sets)").action(async()=>{let e=F.resolve(q.opts().projectRoot),t=ee(e);await ic(e,t.features??"features")});q.command("lsp").description("Run the Saffron language server over stdio (completion, go-to-definition, hover, diagnostics). For JetBrains (LSP4IJ / Ultimate LSP API), Neovim, and any LSP-capable editor").action(()=>{process.stdin.isTTY&&(console.error(gd(lr())),process.exit(2));let e=F.resolve(q.opts().projectRoot),t=ee(e),n=process.argv.includes("-p")||process.argv.some(r=>r.startsWith("--project-root"));wc(e,t.features??"features",{rootFromClient:!n})});q.command("reject").description("Discard pending cache proposals (AI will retry next run)").argument("[files...]","proposal file(s)").option("--all","reject all pending proposals").action((e,t)=>{let n=F.resolve(q.opts().projectRoot),r=e.length>0?e.map(xs).map(s=>({...s,file:F.resolve(s.file)})):t.all?Je(n,Kn).map(s=>({file:s.file,revision:void 0})):[];if(r.length===0){vd(n);return}for(let{file:s,revision:o}of r){let i=wd(n,s,o!==void 0,()=>{if(o&&qe(s)!==o)return bd;gr(s)},a=>a);if(i){console.log(`not rejected: ${F.basename(s)} ${i}`),process.exitCode=1;continue}console.log(`rejected ${s}`)}});q.command("init").description("Make the project agent-ready: install the bundled Saffron skill into agent directories, add instructions to AGENTS.md, scaffold config").option("--agents <list>",`comma-separated targets: ${Object.keys(Vn).join(", ")} (default: claude,agents)`).option("--no-instructions","do not touch AGENTS.md / CLAUDE.md").option("--no-scaffold","do not create saffron.config.json, features/, .gitignore entry").option("--no-mcp","do not register `saffron mcp` in project MCP configs (.mcp.json, .cursor/mcp.json, .vscode/mcp.json)").option("--examples","install the Saucedemo example suite (features, config, .env.example) for a guided first run").option("--no-examples","never install the example suite (skips the prompt)").action(async e=>{let t=F.resolve(q.opts().projectRoot),n=lr();n||(console.error("saffron: cannot locate the installed package root"),process.exit(2));let r=e.agents?String(e.agents).split(",").map(i=>i.trim()).filter(Boolean):void 0;for(let i of r??[])i in Vn||(console.error(`saffron: unknown agent target "${i}" (expected one of ${Object.keys(Vn).join(", ")})`),process.exit(2));let s=e.examples===!0;e.examples===void 0&&process.stdin.isTTY&&process.stdout.isTTY&&(s=await Mh("Install the Saucedemo example tests (login, cart, checkout) into features/ for a guided first run? [y/N] "));let o=ud({projectRoot:t,packageRoot:n,targets:r,instructions:e.instructions,scaffold:e.scaffold,mcp:e.mcp,examples:s});for(let i of o.skillDirs)console.log(`skill \u2192 ${i}/`);for(let i of o.mcpFiles)console.log(`${i.outcome.padEnd(9)} \u2192 ${i.file} (saffron mcp server)`);for(let i of o.mcpHints)console.log(`mcp hint \u2192 ${i}`);for(let i of o.instructionFiles)console.log(`${i.outcome.padEnd(9)} \u2192 ${i.file} (managed Saffron block)`);for(let i of o.scaffolded)console.log(`created \u2192 ${i}`);o.examples&&!o.examples.installed&&console.log(`examples \u2192 not installed: ${o.examples.reason}`),console.log("\nAgents now load the saffron skill when writing tests. Re-run after `npm update saffron-ai` to refresh it."),o.examples?.installed&&console.log(["","Your guided first run (recording needs Claude credentials; replays never do):"," cp .env.example .env # public demo credentials, resolved at replay"," npx playwright install chromium"," npx saffron run --filter @smoke # the agent records two scenarios (about $3 of AI usage), files proposals"," npx saffron accept --all # review, then promote them to caches"," npx saffron run --filter @smoke # replays at zero tokens","Then try the rest: npx saffron run (login errors outline, checkout with a data table)"].join(`
1212
+ `))});q.command("export").description("Pack the pending proposals, the last run's report and its screenshots and traces into one bundle, for review on another checkout (CI uploads it as an artifact)").option("-o, --out <file>","where to write the bundle (default: .saffron/reports/saffron-bundle.zip)").option("--branch <name>","also commit the pending proposals (not the caches) onto the commit the run tested as a new branch, push it to origin, and open a draft pull request (GitHub through gh, Azure DevOps through az)").option("--base <branch>","with --branch: the branch the pull request is for (default: the branch the run tested)").action(e=>{let t=F.resolve(q.opts().projectRoot);e.base!==void 0&&e.branch===void 0&&(console.error("saffron export: --base needs --branch"),process.exit(2));try{Gn("--branch",e.branch),Gn("--base",e.base)}catch(a){console.error(`saffron export: ${a.message}`),process.exit(2)}let n;try{n=Xa(t,{out:e.out?F.resolve(e.out):void 0})}catch(a){console.error(`saffron export: ${a instanceof Error?a.message:String(a)}`),process.exit(2)}for(let a of n.unreadable)console.error(`\u26A0 proposal ${a.file} cannot be read: ${a.problem}. Left out of the bundle.`);let r=n.manifest;if(console.log(`bundle: ${n.file}`),console.log(` ${r.proposals.length} proposal(s) \xB7 ${r.evidence.length} screenshot and trace file(s) \xB7 ${r.report?"the run's report":"no run report"}${r.shard?` \xB7 shard ${r.shard.index}/${r.shard.total}`:""}`),console.log(` from ${vo(r)}`),n.missing.length>0&&console.log(` ${n.missing.length} file(s) the report names are gone, or were replaced by a later run: left out`),n.dropped.length>0&&console.log(` ${n.dropped.length} trace(s) of green scenarios left out: with them the bundle would be over the 4 GB one bundle holds`),console.log("Review it in a checkout of this repository with: saffron import <bundle>"),e.branch===void 0)return;if(n.proposals.length===0){console.log("No pending proposals: no branch to push.");return}let s;try{let a=ee(t);s=cd(t,n,String(e.branch),{base:e.base,config:a})}catch(a){console.error(`saffron export: the bundle is written, but no branch was pushed: ${a instanceof Error?a.message:String(a)}`),process.exit(2)}for(let a of s.leftOut)console.error(`\u26A0 left out of the branch: ${F.relative(t,a.file)} ${a.reason}`);let o=n.proposals.length-s.leftOut.length,i=o===1?"the proposal":`${o} proposals`;console.log(`branch: ${s.branch} pushed to origin (${s.commit.slice(0,7)}: ${i} on top of ${s.parent.slice(0,7)})`),s.pullRequest?console.log(`draft pull request: ${s.pullRequest}`):(console.log(`no pull request: ${s.noPullRequest}. Open one from ${s.branch} to ${s.base}; its description is in ${s.bodyFile}`),s.failed&&(process.exitCode=1))});q.command("runs").description("List the CI runs of the checkout's branch (GitHub Actions through gh, Azure Pipelines through az), with how many of each run's proposals are pending here; import one with saffron import --run <id>").option("--branch <name>","another branch (default: the checkout's; every branch on a detached HEAD)").option("--limit <n>","how many runs (default 20)").option("--provider <name>",`"github" or "azure" (default: ci.provider in the config, else the repository's host)`).option("--json","machine-readable output").action(e=>{let t=F.resolve(q.opts().projectRoot),n=e.limit===void 0?void 0:Number(e.limit);n!==void 0&&!(Number.isInteger(n)&&n>0)&&(console.error(`invalid --limit "${e.limit}": use a whole number above 0`),process.exit(2));let r;try{r=td(t,ee(t),{provider:e.provider,branch:e.branch,limit:n})}catch(i){let a=i instanceof Error?i.message:String(i);e.json?console.log(JSON.stringify({error:a},null,2)):console.error(`saffron runs: ${a}`),process.exit(2)}if(r.problem&&(process.exitCode=2),e.json){console.log(JSON.stringify(r.problem?{...r,error:r.problem.message}:r,null,2));return}if(r.problem){console.error(`saffron runs: ${r.problem.message}`);return}let s=`${Xo[r.provider]} runs ${r.branch?`on ${r.branch}`:"on every branch"}`;if(r.runs.length===0){console.log(`No ${s}.`);return}let o=i=>i.status!=="completed"?"\u25CF":i.conclusion==="success"?"\u2713":i.conclusion==="failure"?"\u2717":"\u25CB";console.log(`${s}:`);for(let i of r.runs){let a=[`${i.name} #${i.number}`,i.status==="completed"?i.conclusion:i.status.replace("_"," "),Xl(i.createdAt),i.commit?.slice(0,7),i.pending?`${i.pending} proposal${i.pending===1?"":"s"} pending here`:void 0].filter(Boolean);console.log(` ${i.id.padStart(12)} ${o(i)} ${a.join(" \xB7 ")}`)}console.log("Import one with: saffron import --run <id>")});q.command("import").description("Bring a bundle from `saffron export` (a CI run) into this checkout: its proposals, each checked against the files here, and the run's report, screenshots and traces. Then review and accept as usual").argument("[bundles...]","bundle files, folders holding saffron-bundle*.zip (a CI artifact download), or http(s) URLs").option("--run <id>","instead: download the bundles of this CI run (an id from saffron runs) through gh or az, and import them").option("--provider <name>",`with --run: "github" or "azure" (default: ci.provider in the config, else the repository's host)`).option("--overwrite","replace a different proposal already pending here for the same scenario").option("--allow-unknown-repository","import a bundle that does not say which repository it comes from (made outside a git checkout); its proposals are still checked by their bindings").option("--json","machine-readable output").action(async(e,t)=>{let n=F.resolve(q.opts().projectRoot);function r(d,f=!1){t.json?console.log(JSON.stringify({error:d},null,2)):console.error(`saffron import: ${d}${f?"":". Nothing was imported."}`),process.exit(2)}t.run===void 0==(e.length===0)&&r("give bundle files, folders or URLs, or --run <id>, and not both"),t.provider!==void 0&&t.run===void 0&&r("--provider goes with --run");let s,o,i;try{let d=ee(n),f=Ee(n,d,[]);t.run!==void 0&&(i=nd(n,d,String(t.run),{provider:t.provider}));let m=i?[i.dir]:e.map(b=>/^https?:\/\//i.test(b)?b:F.resolve(b)),p=i;s=await nc(n,m,f,{overwrite:t.overwrite===!0,allowUnknownRepository:t.allowUnknownRepository===!0,describe:p?b=>`${Xo[p.provider]} run ${t.run} (${F.basename(F.dirname(b))})`:void 0})}catch(d){o=d}finally{i?.remove()}if((o instanceof L||o instanceof ce)&&r(o.message),(o!==void 0||!s)&&r(o instanceof Error?o.message:String(o),!0),s.proposals.some(d=>d.outcome==="refused")&&(process.exitCode=1),t.json){console.log(JSON.stringify(i?{run:{provider:i.provider,id:String(t.run)},...s}:s,null,2));return}for(let d of s.bundles)console.log(`bundle: ${d.source}`),console.log(` from ${vo(d.origin)}${d.shard?` \xB7 shard ${d.shard.index}/${d.shard.total}`:""} \xB7 saffron-ai ${d.runner}`);for(let d of s.warnings)console.log(`\u26A0 ${d}`);for(let d of s.proposals){let f=d.feature?`${d.feature} \u203A ${d.scenario}`:d.file;d.outcome==="imported"?console.log(`imported \u2192 ${d.file} (${f})`):d.outcome==="unchanged"?console.log(`unchanged: ${f}: ${d.reason??"already pending here"}`):console.log(`not imported: ${f} ${d.reason}`)}let{written:a,unchanged:c}=s.evidence;a>0&&console.log(`evidence: ${a} screenshot and trace file(s) written${c?`, ${c} already here`:""}`),s.report==="written"&&console.log("report: .saffron/reports/latest.html now shows the imported run (saffron report, saffron trace)");let l=s.proposals.filter(d=>d.outcome==="imported").length;l===0&&a===0&&s.report!=="written"&&!s.proposals.some(d=>d.outcome==="refused")?console.log("Nothing changed: this was already imported."):l>0&&console.log("Review with: saffron diff \xB7 accept with: saffron accept <file> | --all \xB7 then commit the caches with your git tools")});q.command("diff").description("Show what a pending proposal changes in the committed recording: the action list a reviewer is approving, not just the narrative").argument("[files...]","proposal file(s); default: every pending proposal").option("--json","machine-readable output").action((e,t)=>{let n=F.resolve(q.opts().projectRoot),r=e.length>0?e.map(o=>F.resolve(o)):Je(n,Kn).map(o=>o.file);if(r.length===0){console.log("No pending proposals.");return}let s=r.map(o=>{let i=Gt(o),a=ye(Y(n,i.cache.feature,i.cache.scenario));return{file:F.relative(n,o),verified:i.meta.verified,stale:i.meta.bound?yd(n,i):$r,...pt(a,i.cache)}});if(t.json){console.log(JSON.stringify(s.length===1?s[0]:s,null,2));return}console.log(s.map(o=>`${vr(o)}${o.stale?`
1101
1213
 
1102
1214
  STALE: ${o.stale}`:""}`).join(`
1103
1215
 
1104
- `))});J.command("trace").description("Open the execution replay of a scenario from the last run: its Given / When / Then, the page at every action, the network alongside, and what the agent changed").argument("[scenario]","scenario name, feature path, or feature:scenario; default: every traced scenario, opening on a red one, else a yellow one").option("--port <n>","listen on a fixed port (default: a free one)").option("--no-open","print the URL without opening a browser").option("--json","print the parsed trace as JSON instead of serving it").option("--run <startedAt>","only if the last run is this one (its startedAt): an editor's row names the run it lists").action(async(e,t)=>{let n=t.port===void 0?void 0:Number(t.port);n!==void 0&&(!Number.isInteger(n)||n<0||n>65535)&&(console.error(`invalid --port "${t.port}": use a whole number from 0 to 65535`),process.exit(2));let r=N.resolve(J.opts().projectRoot),s=N.join(r,".saffron","reports","latest.json");ae.existsSync(s)||(console.error("No report found. Run `saffron run` first."),process.exit(2));let o=JSON.parse(ae.readFileSync(s,"utf8"));t.run!==void 0&&o.startedAt!==t.run&&(console.error(`The last run changed since this was listed: it is now the run of ${o.startedAt}. Refresh the panel to replay it.`),process.exit(2));let i=o.scenarios.filter(y=>y.evidence?.some(h=>h.kind==="trace"));i.length===0&&(console.error('The last run kept no execution trace. Set "trace": "retain-on-failure" (or "on") in saffron.config.json, or run with --trace retain-on-failure, then run again.'),process.exit(2));let a=e?.trim(),c=i;if(a){let y=i.filter(h=>h.scenario===a||h.feature===a||`${h.feature}:${h.scenario}`===a);c=y.length>0?y:i.filter(h=>h.scenario.toLowerCase().includes(a.toLowerCase()))}if(c.length===0||t.json&&a&&c.length>1){console.error(c.length===0?`No traced scenario matches "${a}". Traced in the last run:`:"--json prints one trace. Traced in the last run:");for(let y of i)console.error(` ${y.status.padEnd(6)} ${y.feature}:${y.scenario}`);console.error('Name one: saffron trace "<scenario name>", or "<feature>:<scenario name>" as listed'),process.exit(2)}let l=c.findIndex(y=>y.status==="red"),d=c.findIndex(y=>y.status==="yellow"),f=l>=0?l:d>=0?d:0,m=c[f],p=y=>N.basename(y,".zip").replace("proof-","Proof replay "),b=y=>{let h=y.evidence.filter($=>$.kind==="trace"),S=h.find($=>!$.proof)??h[0];return[S,...h.filter($=>$!==S)]},[v]=b(m),k;if(c.length===1||t.json){let y=N.join(r,v.file);try{k=jo(r,v)}catch(h){console.error(`The trace cannot be read: ${h instanceof Error?h.message:String(h)}`),process.exit(2)}if(k||(console.error(ae.existsSync(y)?"The trace on disk is from a later run than the last report (a run that stopped before writing its report). Run the scenario again to replay it.":`The trace file is gone: ${y}. Run the scenario again.`),process.exit(2)),t.json){console.log(JSON.stringify(cl(k),null,2));return}}let w=c.map((y,h)=>{let[S,...$]=b(y);return{scenario:{feature:y.feature,scenario:y.scenario,status:y.status,error:y.error,adaptations:y.adaptations,narrative:y.narrative,proposalDiff:y.proposalDiff,proposalFile:y.proposalFile,proposalRevision:y.proposalRevision,screenshots:y.evidence?.filter(u=>u.kind!=="trace"),ranAt:o.startedAt,source:["cache","agent-record","agent-heal"].find(u=>u===y.source),proposalReplay:y.proposalReplay===!0?!0:void 0},traces:[S,...$].map((u,g)=>({file:N.join(r,u.file),data:h===f&&g===0?k:void 0,read:()=>jo(r,u),checked:u.sha256!==void 0,label:u.proof?p(u.file):"Execution",proof:u.proof===!0,steps:u.proof?u.traceSteps:u.traceSteps??y.steps,uniques:u.proof?u.traceUniques:y.uniques}))}}),x;try{x=await Sl({entries:w,start:f,projectRoot:r,port:n})}catch(y){console.error(`Could not start the replay: ${y instanceof Error?y.message:String(y)}`),process.exit(2)}for(let{file:y,reason:h}of x.skipped)console.error(`Skipping ${N.relative(r,y)}. ${h}`);console.log(`Saffron replay: ${c.length===1?`${m.feature} \u203A ${m.scenario}`:`${c.length} scenarios, opening on ${m.scenario}`}
1216
+ `))});q.command("trace").description("Open the execution replay of a scenario from the last run: its Given / When / Then, the page at every action, the network alongside, and what the agent changed").argument("[scenario]","scenario name, feature path, or feature:scenario; default: every traced scenario, opening on a red one, else a yellow one").option("--port <n>","listen on a fixed port (default: a free one)").option("--no-open","print the URL without opening a browser").option("--json","print the parsed trace as JSON instead of serving it").option("--run <startedAt>","only if the last run is this one (its startedAt): an editor's row names the run it lists").action(async(e,t)=>{let n=t.port===void 0?void 0:Number(t.port);n!==void 0&&(!Number.isInteger(n)||n<0||n>65535)&&(console.error(`invalid --port "${t.port}": use a whole number from 0 to 65535`),process.exit(2));let r=F.resolve(q.opts().projectRoot),s=F.join(r,".saffron","reports","latest.json");re.existsSync(s)||(console.error("No report found. Run `saffron run` first."),process.exit(2));let o=JSON.parse(re.readFileSync(s,"utf8"));t.run!==void 0&&o.startedAt!==t.run&&(console.error(`The last run changed since this was listed: it is now the run of ${o.startedAt}. Refresh the panel to replay it.`),process.exit(2));let i=o.scenarios.filter(y=>y.evidence?.some(u=>u.kind==="trace"));i.length===0&&(console.error('The last run kept no execution trace. Set "trace": "retain-on-failure" (or "on") in saffron.config.json, or run with --trace retain-on-failure, then run again.'),process.exit(2));let a=e?.trim(),c=i;if(a){let y=i.filter(u=>u.scenario===a||u.feature===a||`${u.feature}:${u.scenario}`===a);c=y.length>0?y:i.filter(u=>u.scenario.toLowerCase().includes(a.toLowerCase()))}if(c.length===0||t.json&&a&&c.length>1){console.error(c.length===0?`No traced scenario matches "${a}". Traced in the last run:`:"--json prints one trace. Traced in the last run:");for(let y of i)console.error(` ${y.status.padEnd(6)} ${y.feature}:${y.scenario}`);console.error('Name one: saffron trace "<scenario name>", or "<feature>:<scenario name>" as listed'),process.exit(2)}let l=c.findIndex(y=>y.status==="red"),d=c.findIndex(y=>y.status==="yellow"),f=l>=0?l:d>=0?d:0,m=c[f],p=y=>F.basename(y,".zip").replace("proof-","Proof replay "),b=y=>{let u=y.evidence.filter(S=>S.kind==="trace"),v=u.find(S=>!S.proof)??u[0];return[v,...u.filter(S=>S!==v)]},[$]=b(m),k;if(c.length===1||t.json){let y=F.join(r,$.file);try{k=Jo(r,$)}catch(u){console.error(`The trace cannot be read: ${u instanceof Error?u.message:String(u)}`),process.exit(2)}if(k||(console.error(re.existsSync(y)?"The trace on disk is from a later run than the last report (a run that stopped before writing its report). Run the scenario again to replay it.":`The trace file is gone: ${y}. Run the scenario again.`),process.exit(2)),t.json){console.log(JSON.stringify(xl(k),null,2));return}}let w=c.map((y,u)=>{let[v,...S]=b(y);return{scenario:{feature:y.feature,scenario:y.scenario,status:y.status,error:y.error,adaptations:y.adaptations,narrative:y.narrative,proposalDiff:y.proposalDiff,proposalFile:y.proposalFile,proposalRevision:y.proposalRevision,screenshots:y.evidence?.filter(h=>h.kind!=="trace"),ranAt:o.startedAt,source:["cache","agent-record","agent-heal"].find(h=>h===y.source),proposalReplay:y.proposalReplay===!0?!0:void 0},traces:[v,...S].map((h,g)=>({file:F.join(r,h.file),data:u===f&&g===0?k:void 0,read:()=>Jo(r,h),checked:h.sha256!==void 0,label:h.proof?p(h.file):"Execution",proof:h.proof===!0,steps:h.proof?h.traceSteps:h.traceSteps??y.steps,uniques:h.proof?h.traceUniques:y.uniques}))}}),x;try{x=await jl({entries:w,start:f,projectRoot:r,port:n})}catch(y){console.error(`Could not start the replay: ${y instanceof Error?y.message:String(y)}`),process.exit(2)}for(let{file:y,reason:u}of x.skipped)console.error(`Skipping ${F.relative(r,y)}. ${u}`);console.log(`Saffron replay: ${c.length===1?`${m.feature} \u203A ${m.scenario}`:`${c.length} scenarios, opening on ${m.scenario}`}
1105
1217
  ${x.url}
1106
- Press Ctrl-C to stop.`),t.open!==!1&&Vo(x.url),await new Promise(y=>{process.once("SIGINT",()=>{x.close().finally(y)})}),process.exit(0)});J.command("prune").description("List (and with --yes remove) recordings whose scenario no longer exists: caches and pending proposals left behind by a deleted scenario or feature file").option("--json","machine-readable output").option("--yes","actually delete the files listed").option("--check","exit 1 when anything is orphaned, delete nothing (for CI)").action(e=>{let t=N.resolve(J.opts().projectRoot),n=ne(t),r=cr(t,n.features??"features");r.unreadable.length>0&&(e.json?console.log(JSON.stringify({...r,removed:[]},null,2)):console.error(["saffron: cannot tell what is orphaned while a feature file fails to parse.",...r.unreadable.map(o=>` ${o}`),"Fix these first: a syntax error hides every scenario in the file."].join(`
1107
- `)),process.exit(2));let s=e.yes&&!e.check?ki(t,r.orphans):[];if(e.json)console.log(JSON.stringify({...r,removed:s},null,2));else if(r.orphans.length===0)console.log(`Nothing to prune. All ${r.kept} recordings belong to a scenario.`);else{let o=r.orphans.filter(c=>c.kind==="cache").length,i=r.orphans.filter(c=>c.kind==="proposal").length,a=r.orphans.length-o-i;console.log([`${r.orphans.length} orphaned ${r.orphans.length===1?"file":"files"} (${o} cache, ${i} proposal, ${a} screenshot ${a===1?"folder":"folders"}), ${r.kept} still owned:`,"",...r.orphans.map(c=>` ${s.length>0?"removed ":""}${Ti(c)}`),"",s.length>0?"Deleted. Re-record by running the scenarios again if you deleted something by mistake.":"Nothing was deleted. Run `saffron prune --yes` to remove them."].join(`
1108
- `))}e.check&&r.orphans.length>0&&process.exit(1)});J.command("status").description("Project overview for dashboards and IDE panels: feature files and scenarios with cache state, tags, pending proposals, last run, history, vocabulary health, effective config").option("--json","machine-readable output").action(e=>{let t=N.resolve(J.opts().projectRoot),n=ne(t),r=kr(t,n);if(e.json){console.log(JSON.stringify(r,null,2));return}Ja(r)});J.command("report").description("Open the latest HTML report, or merge shard reports into it").argument("[paths...]","with --merge: shard report files or folders to search (default: .saffron)").option("--open","open in the default browser",!0).option("--merge","combine the shard-<i>-of-<n>.json reports of one sharded run into latest.json, latest.html and one history line").action((e,t)=>{let n=N.resolve(J.opts().projectRoot);if(t.merge){$h(n,e);return}e.length>0&&(console.error("saffron report: paths are only read with --merge"),process.exit(2));let r=N.join(n,".saffron","reports","latest.html");ae.existsSync(r)||(console.error("No report found. Run `saffron run` first."),process.exit(2)),console.log(r),Vo(r)});function $h(e,t){let n=t.length>0?t.map(b=>N.resolve(b)):[N.join(e,".saffron")],r,s;try{let b=Ea(n);s=b.length,r=Sr(b)}catch(b){console.error(`saffron report --merge: ${b instanceof Error?b.message:String(b)}`),process.exit(2)}let{report:o,history:i}=r,a=$n(e),c=a.find(b=>b.runId===i.runId),l=c?a.filter(b=>b.runId!==i.runId):a;o.trends=Vs(l,i);let d=c!==void 0&&JSON.stringify(c)===JSON.stringify(i);d||Sa(e,i);let{html:f,json:m}=Rn(e,o),p=o.totals;console.log(`merged ${s} shard(s): ${p.scenarios} scenario(s) \xB7 ${p.green} passed, ${p.yellow} pending review, ${p.red} failed \xB7 ${p.aiCalls} AI calls${zs(p)}`),d?console.log("history already holds this run: not appended again"):c&&console.log("history already held this run: its line is replaced with these results"),o.trends.prev&&console.log(qs(o.trends.prev));for(let b of o.trends.chronic)console.log(`\u26A0 chronic: ${b.feature} \u203A ${b.scenario}: ${b.heals} heal(s), ${b.reds} red(s) in last ${b.runs} run(s)`);console.log(`report: ${f}
1109
- ${m}`)}function ed(e,t){try{return $e(e,t,Re(e,ne(e),[]))}catch(n){return`cannot check: ${n instanceof Error?n.message.split(`
1110
- `)[0]:String(n)}`}}var Bn=(e,t)=>console.error(`\u26A0 proposal ${e} cannot be read: ${t}`),kh="is no longer pending: it was accepted, rejected or removed since it was listed.",Th="is not a pending proposal.",Rh="is not a proposal file under .saffron/proposals.",td="changed since it was listed. Refresh and review it again.",Eh="is being decided by another process. Try again.";function nd(e,t,n,r,s){if(!t.endsWith(".json")||ae.existsSync(t)&&!at(N.join(e,".saffron","proposals"),t))return s(Rh);try{return rr(t,r,()=>s(n?kh:Th))}catch(o){if(!(o instanceof St))throw o;return s(Eh)}}function rd(e){let t=He(e,Bn);if(t.length===0){console.log("No pending proposals.");return}console.log(`Pending proposals:
1111
- `);for(let{file:n,proposal:r}of t){console.log(` ${n}`);let s=r.meta.verified===!0?" \xB7 verified \u2713":r.meta.verified===!1?" \xB7 UNVERIFIED \u2717":"";console.log(` ${r.cache.feature} \u203A ${r.cache.scenario} (${r.meta.mode}, ${r.meta.createdAt}${s})`);let o=r.meta.bound?ed(e,r):hr;o&&console.log(` \u2717 STALE: ${o}`),console.log(` ${r.meta.narrative}`);for(let i of r.meta.adaptations)console.log(` \u26A0 ${i}`);r.meta.suggestedFeatureEdit&&console.log(` suggested .feature edit:
1218
+ Press Ctrl-C to stop.`),t.open!==!1&&rs(x.url),await new Promise(y=>{process.once("SIGINT",()=>{x.close().finally(y)})}),process.exit(0)});q.command("prune").description("List (and with --yes remove) recordings whose scenario no longer exists: caches and pending proposals left behind by a deleted scenario or feature file").option("--json","machine-readable output").option("--yes","actually delete the files listed").option("--check","exit 1 when anything is orphaned, delete nothing (for CI)").action(e=>{let t=F.resolve(q.opts().projectRoot),n=ee(t),r=mr(t,n.features??"features");r.unreadable.length>0&&(e.json?console.log(JSON.stringify({...r,removed:[]},null,2)):console.error(["saffron: cannot tell what is orphaned while a feature file fails to parse.",...r.unreadable.map(o=>` ${o}`),"Fix these first: a syntax error hides every scenario in the file."].join(`
1219
+ `)),process.exit(2));let s=e.yes&&!e.check?Fi(t,r.orphans):[];if(e.json)console.log(JSON.stringify({...r,removed:s},null,2));else if(r.orphans.length===0)console.log(`Nothing to prune. All ${r.kept} recordings belong to a scenario.`);else{let o=r.orphans.filter(c=>c.kind==="cache").length,i=r.orphans.filter(c=>c.kind==="proposal").length,a=r.orphans.length-o-i;console.log([`${r.orphans.length} orphaned ${r.orphans.length===1?"file":"files"} (${o} cache, ${i} proposal, ${a} screenshot ${a===1?"folder":"folders"}), ${r.kept} still owned:`,"",...r.orphans.map(c=>` ${s.length>0?"removed ":""}${Mi(c)}`),"",s.length>0?"Deleted. Re-record by running the scenarios again if you deleted something by mistake.":"Nothing was deleted. Run `saffron prune --yes` to remove them."].join(`
1220
+ `))}e.check&&r.orphans.length>0&&process.exit(1)});q.command("status").description("Project overview for dashboards and IDE panels: feature files and scenarios with cache state, tags, pending proposals, last run, history, vocabulary health, effective config").option("--json","machine-readable output").action(e=>{let t=F.resolve(q.opts().projectRoot),n=ee(t),r=jn(t,n);if(e.json){console.log(JSON.stringify(r,null,2));return}oc(r)});q.command("report").description("Open the latest HTML report, or merge shard reports into it").argument("[paths...]","with --merge: shard report files or folders to search (default: .saffron)").option("--open","open in the default browser",!0).option("--merge","combine the shard-<i>-of-<n>.json reports of one sharded run into latest.json, latest.html and one history line").action((e,t)=>{let n=F.resolve(q.opts().projectRoot);if(t.merge){Uh(n,e);return}e.length>0&&(console.error("saffron report: paths are only read with --merge"),process.exit(2));let r=F.join(n,".saffron","reports","latest.html");re.existsSync(r)||(console.error("No report found. Run `saffron run` first."),process.exit(2)),console.log(r),rs(r)});q.command("dashboard").description("Open the suite's quality dashboard: tests, failing, flaky and never-run scenarios, healing, timing and tags, from the run history").option("--stdout","print the page instead of writing and opening it (what the IDE panels read)").option("--no-open","write the page without opening a browser").action(e=>{let t=F.resolve(q.opts().projectRoot),n=jn(t,ee(t)),r=Wl(n.quality,n.version);if(e.stdout){process.stdout.write(r);return}let s=F.join(t,".saffron","reports","dashboard.html");re.mkdirSync(F.dirname(s),{recursive:!0}),re.writeFileSync(s,r),console.log(s),e.open!==!1&&rs(s)});function Uh(e,t){let n=t.length>0?t.map(b=>F.resolve(b)):[F.join(e,".saffron")],r,s;try{let b=Ba(n);s=b.length,r=Ar(b)}catch(b){console.error(`saffron report --merge: ${b instanceof Error?b.message:String(b)}`),process.exit(2)}let{report:o,history:i}=r,a=En(e),c=a.find(b=>b.runId===i.runId),l=c?a.filter(b=>b.runId!==i.runId):a;o.trends=no(l,i);let d=c!==void 0&&JSON.stringify(c)===JSON.stringify(i);d||Ca(e,i);let{html:f,json:m}=Pn(e,o),p=o.totals;console.log(`merged ${s} shard(s): ${p.scenarios} scenario(s) \xB7 ${p.green} passed, ${p.yellow} pending review, ${p.red} failed \xB7 ${p.aiCalls} AI calls${Ys(p)}`),d?console.log("history already holds this run: not appended again"):c&&console.log("history already held this run: its line is replaced with these results"),o.trends.prev&&console.log(Qs(o.trends.prev));for(let b of o.trends.chronic)console.log(`\u26A0 chronic: ${b.feature} \u203A ${b.scenario}: ${b.heals} heal(s), ${b.reds} red(s) in last ${b.runs} run(s)`);console.log(`report: ${f}
1221
+ ${m}`)}function yd(e,t){try{return ke(e,t,Ee(e,ee(e),[]))}catch(n){return`cannot check: ${n instanceof Error?n.message.split(`
1222
+ `)[0]:String(n)}`}}var Kn=(e,t)=>console.error(`\u26A0 proposal ${e} cannot be read: ${t}`),Bh="is no longer pending: it was accepted, rejected or removed since it was listed.",zh="is not a pending proposal.",Hh="is not a proposal file under .saffron/proposals.",bd="changed since it was listed. Refresh and review it again.",qh="is being decided by another process. Try again.";function wd(e,t,n,r,s){if(!t.endsWith(".json")||re.existsSync(t)&&!lt(F.join(e,".saffron","proposals"),t))return s(Hh);try{return pr(t,r,()=>s(n?Bh:zh))}catch(o){if(!(o instanceof xt))throw o;return s(qh)}}function vd(e){let t=Je(e,Kn);if(t.length===0){console.log("No pending proposals.");return}console.log(`Pending proposals:
1223
+ `);for(let{file:n,proposal:r}of t){console.log(` ${n}`);let s=r.meta.verified===!0?" \xB7 verified \u2713":r.meta.verified===!1?" \xB7 UNVERIFIED \u2717":"";console.log(` ${r.cache.feature} \u203A ${r.cache.scenario} (${r.meta.mode}, ${r.meta.createdAt}${s})`);let o=r.meta.bound?yd(e,r):$r;o&&console.log(` \u2717 STALE: ${o}`),console.log(` ${r.meta.narrative}`);for(let i of r.meta.adaptations)console.log(` \u26A0 ${i}`);r.meta.suggestedFeatureEdit&&console.log(` suggested .feature edit:
1112
1224
  ${r.meta.suggestedFeatureEdit.split(`
1113
1225
  `).map(i=>` ${i}`).join(`
1114
- `)}`),console.log()}console.log("Accept with: saffron accept <file> | --all")}J.parseAsync().catch(e=>{process.env.SAFFRON_DEBUG?console.error(e):console.error(`saffron: ${e instanceof Error?e.message:String(e)}`),process.exit(2)});
1226
+ `)}`),console.log()}console.log("Accept with: saffron accept <file> | --all")}q.parseAsync().catch(e=>{process.env.SAFFRON_DEBUG?console.error(e):console.error(`saffron: ${e instanceof Error?e.message:String(e)}`),process.exit(2)});