ripplo 0.7.19 → 0.7.20

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/index.js CHANGED
@@ -1,35 +1,35 @@
1
1
  #!/usr/bin/env node
2
- import{$ as xt,A as ae,B as Wr,C as le,D as v,E as nt,G as ot,H as it,I as st,J as at,L as lt,M as dt,N as rr,O as K,P as tr,Q as mt,R as ft,S as gt,T as ht,U as yt,V as kt,W as wt,X as vt,Y as bt,Z as St,_ as Re,a as Ir,aa as Rt,b as V,c as jr,d as Ar,e as P,f as we,g as O,ga as ce,h as Tr,ha as Ct,i as S,ia as nr,j as c,k as Dr,ka as Pt,l as x,m as Or,ma as Et,n as _r,o as Fr,oa as $t,p as Nr,q as j,qa as It,r as A,ra as jt,s as Ur,sa as At,t as w,ta as Lt,u as ve,ua as Tt,va as Dt,x as N,xa as Ot,y as Mr,z as Hr}from"./chunk-APGFYCJC.js";import{$ as pt,D as Xr,F as Ze,G as er,H as Yr,I as Zr,J as xe,P as et,R as rt,S as tt,Z as ct,_ as de,aa as ut,c as z,d as Lr,g as M,j as L,l as qr,m as Ye,n as Br,o as J,p as Vr,q as Gr,r as be,s as C,t as zr,u as Jr,v as Kr,x as Qr,y as Se}from"./chunk-7R7PWDYF.js";import{a as vr,b as br,c as Sr,d as u,e as $,g as ke,h as xr,k as F,l as Rr,m as Cr,n as Xe,o as Pr,p as Er,q as $r}from"./chunk-Y5IXMRSB.js";import"./chunk-SB7H6IAS.js";import Ac from"update-notifier";import Lc from"yargs";import{hideBin as Tc}from"yargs/helpers";function Ce({current:e,latest:r}){return r==null?`ripplo v${e} (latest: unknown)`:r===e?`ripplo v${e} (latest)`:`ripplo v${e} (latest: v${r} \u2014 run \`npx ripplo update\`)`}function _t(){return"Update available {currentVersion} \u2192 {latestVersion}\nRun `npx ripplo update`"}import{graphql as ji}from"gql.tada";import{exec as wi}from"child_process";import{createAuthClient as yi}from"better-auth/client";import{deviceAuthorizationClient as ki}from"better-auth/client/plugins";function Ft({baseURL:e}){return yi({baseURL:e,fetchOptions:{headers:{"User-Agent":"Ripplo CLI"}},plugins:[ki()]})}import{err as Nt,ok as vi}from"neverthrow";var bi=5e3,Ut="ripplo-cli";async function Mt({onDeviceCode:e,url:r}){let t=r??Ur().RIPPLO_SERVER_URL,n=Ft({baseURL:t}),o=await n.device.code({client_id:Ut});if(o.error!=null)return Nt({description:o.error.error_description,kind:"oauth-device-code-failed"});let{device_code:i,user_code:a,verification_uri_complete:s}=o.data;return e({userCode:a,verificationUrl:s}),Ei(s),(await Si({authClient:n,deviceCode:i})).map(d=>(Rr({serverUrl:t,token:d}),d))}async function Si({authClient:e,deviceCode:r}){for(;;){await Ci(bi);let t=await e.device.token({client_id:Ut,device_code:r,grant_type:"urn:ietf:params:oauth:grant-type:device_code"});if(t.data?.access_token!=null)return vi(t.data.access_token);if(t.error==null)continue;if(!Ri(t.error.error))return Nt({code:t.error.error,description:t.error.error_description,kind:"oauth-authorization-failed"})}}var xi=new Set(["authorization_pending","slow_down"]);function Ri(e){return xi.has(e)}function Ci(e){return new Promise(r=>{setTimeout(r,e)})}function Pi(){return process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open"}function Ei(e){let r=Pi();wi(`${r} "${e}"`,()=>{})}function H({serverUrl:e,token:r}){return{appUrl:"",cwd:process.cwd(),engineUrl:"",projectId:"",ripploServerUrl:e,token:r,tunnelAuth:void 0,webhookSecret:""}}import $i from"fs";import Ii from"path";function m(e){return $i.existsSync(Ii.join(e,".ripplo"))}var Ai=ji(`
2
+ import{$ as jn,A as se,B as Hr,C as ae,D as v,E as cn,G as pn,H as un,I as mn,J as fn,L as gn,M as hn,N as er,O as J,P as rr,Q as vn,R as bn,S as Sn,T as xn,U as Rn,V as Cn,W as Pn,X as En,Y as $n,Z as In,_ as xe,a as $r,aa as An,b as B,c as Ir,d as jr,e as P,f as ke,g as O,ga as de,h as Lr,ha as Ln,i as S,ia as nr,j as c,k as Tr,ka as Tn,l as x,m as Dr,ma as Dn,n as Or,o as _r,oa as On,p as Fr,q as j,qa as _n,r as A,ra as Fn,s as Nr,sa as Nn,t as w,ta as Un,u as we,ua as Mn,va as Hn,x as F,xa as Wn,y as Ur,z as Mr}from"./chunk-XZTICXYD.js";import{A as Xr,B as Yr,C as Zr,D as en,E as rn,F as nn,G as be,L as tn,N as Ye,O as Ze,P as on,Q as sn,R as Se,W as an,Y as ln,Z as dn,c as G,d as Ar,fa as yn,g as U,ga as le,ha as kn,ia as wn,k as L,m as Wr,n as Xe,o as qr,p as z,q as Br,r as Vr,s as ve,t as C,u as Gr,v as zr,w as Jr,y as Kr,z as Qr}from"./chunk-DAULBZMT.js";import{a as wr,b as vr,c as br,d as u,e as $,g as ye,h as Sr,k as _,l as xr,m as Rr,n as Qe,o as Cr,p as Pr,q as Er}from"./chunk-EVYYSTR7.js";import"./chunk-IHSHBPJY.js";import"./chunk-7UWDMECF.js";import Sc from"update-notifier";import xc from"yargs";import{hideBin as Rc}from"yargs/helpers";function Re({current:e,latest:r}){return r==null?`ripplo v${e} (latest: unknown)`:r===e?`ripplo v${e} (latest)`:`ripplo v${e} (latest: v${r} \u2014 run \`npx ripplo update\`)`}function qn(){return"Update available {currentVersion} \u2192 {latestVersion}\nRun `npx ripplo update`"}import{graphql as Di}from"gql.tada";import{exec as xi}from"child_process";import{createAuthClient as bi}from"better-auth/client";import{deviceAuthorizationClient as Si}from"better-auth/client/plugins";function Bn({baseURL:e}){return bi({baseURL:e,fetchOptions:{headers:{"User-Agent":"Ripplo CLI"}},plugins:[Si()]})}import{err as Vn,ok as Ri}from"neverthrow";var Ci=5e3,Gn="ripplo-cli";async function zn({onDeviceCode:e,url:r}){let n=r??Nr().RIPPLO_SERVER_URL,t=Bn({baseURL:n}),o=await t.device.code({client_id:Gn});if(o.error!=null)return Vn({description:o.error.error_description,kind:"oauth-device-code-failed"});let{device_code:i,user_code:a,verification_uri_complete:s}=o.data;return e({userCode:a,verificationUrl:s}),Ai(s),(await Pi({authClient:t,deviceCode:i})).map(d=>(xr({serverUrl:n,token:d}),d))}async function Pi({authClient:e,deviceCode:r}){for(;;){await Ii(Ci);let n=await e.device.token({client_id:Gn,device_code:r,grant_type:"urn:ietf:params:oauth:grant-type:device_code"});if(n.data?.access_token!=null)return Ri(n.data.access_token);if(n.error==null)continue;if(!$i(n.error.error))return Vn({code:n.error.error,description:n.error.error_description,kind:"oauth-authorization-failed"})}}var Ei=new Set(["authorization_pending","slow_down"]);function $i(e){return Ei.has(e)}function Ii(e){return new Promise(r=>{setTimeout(r,e)})}function ji(){return process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open"}function Ai(e){let r=ji();xi(`${r} "${e}"`,()=>{})}function M({serverUrl:e,token:r}){return{appUrl:"",cwd:process.cwd(),engineUrl:"",projectId:"",ripploServerUrl:e,token:r,tunnelAuth:void 0,webhookSecret:""}}import Li from"fs";import Ti from"path";function m(e){return Li.existsSync(Ti.join(e,".ripplo"))}var Oi=Di(`
3
3
  query AuthViewer {
4
4
  currentUser {
5
5
  name
6
6
  email
7
7
  }
8
8
  }
9
- `);async function Ht(){let e=A(),r=F(e);if(r!=null&&await Li(e,r))return;let n=(await Mt({url:e,onDeviceCode:s=>{process.stdout.write(`Opening your browser to finish sign-in.
9
+ `);async function Jn(){let e=A(),r=_(e);if(r!=null&&await _i(e,r))return;let t=(await zn({url:e,onDeviceCode:s=>{process.stdout.write(`Opening your browser to finish sign-in.
10
10
  `),process.stdout.write(`If it didn't open, visit: ${s.verificationUrl}
11
11
  `),process.stdout.write(`Verification code: ${s.userCode}
12
12
 
13
13
  `),process.stdout.write(`Waiting for approval...
14
14
  `)}})).match(s=>s,s=>{process.stderr.write(`${j(s)}
15
- `),process.exit(1)}),o=await or({serverUrl:e,token:n}),i=m(process.cwd()),a=i?"Welcome back":"Welcome to Ripplo";process.stdout.write(`
16
- `),process.stdout.write(o.kind==="ok"?`${a}, ${Ti(o.viewer)}.
15
+ `),process.exit(1)}),o=await tr({serverUrl:e,token:t}),i=m(process.cwd()),a=i?"Welcome back":"Welcome to Ripplo";process.stdout.write(`
16
+ `),process.stdout.write(o.kind==="ok"?`${a}, ${Fi(o.viewer)}.
17
17
  `:`${a}.
18
- `),process.stdout.write(`Session saved to ${Xe(e)}.
19
- `),i||(process.stdout.write("\nNext: run `/ripplo:setup` in Claude Code to wire Ripplo into this project,\n"),process.stdout.write("or `npx ripplo init` to set it up by hand.\n"))}async function Li(e,r){let t=await or({serverUrl:e,token:r});return t.kind==="ok"?(process.stdout.write(`Already signed in as ${t.viewer.email}. Run \`npx ripplo auth logout\` to switch accounts.
20
- `),!0):(t.kind==="unreachable"&&(process.stdout.write(`Could not reach the Ripplo server at ${e} (${t.message}). Your saved session may still be valid \u2014 fix connectivity and retry.
18
+ `),process.stdout.write(`Session saved to ${Qe(e)}.
19
+ `),i||(process.stdout.write("\nNext: run `/ripplo:setup` in Claude Code to wire Ripplo into this project,\n"),process.stdout.write("or `npx ripplo init` to set it up by hand.\n"))}async function _i(e,r){let n=await tr({serverUrl:e,token:r});return n.kind==="ok"?(process.stdout.write(`Already signed in as ${n.viewer.email}. Run \`npx ripplo auth logout\` to switch accounts.
20
+ `),!0):(n.kind==="unreachable"&&(process.stdout.write(`Could not reach the Ripplo server at ${e} (${n.message}). Your saved session may still be valid \u2014 fix connectivity and retry.
21
21
  `),process.exit(1)),process.stdout.write(`Your session expired \u2014 signing you back in.
22
22
 
23
- `),!1)}async function Wt(){let e=A(),r=F(e);r==null&&(process.stdout.write("Not signed in. Run `npx ripplo auth login`.\n"),process.exit(1));let t=await or({serverUrl:e,token:r});t.kind==="unreachable"&&(process.stdout.write(`Could not reach the Ripplo server at ${e} (${t.message}). The token may still be valid \u2014 check the server / network and retry.
24
- `),process.exit(1)),t.kind==="rejected"&&(process.stdout.write("Token rejected by the server. Run `npx ripplo auth login` to sign in again.\n"),process.stdout.write(`(Claude Code: run it yourself in the background \u2014 the user just approves in the browser.)
25
- `),process.exit(1)),process.stdout.write(`Signed in as ${t.viewer.name} <${t.viewer.email}> (${e})
26
- `)}function qt(){let e=A();if(!Cr(e)){process.stdout.write(`Already signed out.
27
- `);return}process.stdout.write(`Signed out. Removed ${Xe(e)}.
28
- `)}async function or({serverUrl:e,token:r}){try{let n=(await u({config:H({serverUrl:e,token:r}),document:Ai,variables:void 0})).currentUser;return n==null?{kind:"rejected"}:{kind:"ok",viewer:{email:n.email,name:n.name}}}catch(t){return Sr(t)==="UNAUTHENTICATED"?{kind:"rejected"}:{kind:"unreachable",message:t instanceof Error?t.message:String(t)}}}function Ti(e){let r=e.name.trim().split(/\s+/)[0];return r!=null&&r.length>0?r:e.email}import{readFile as Di,writeFile as Oi}from"fs/promises";import _i from"path";async function Bt(e){let r=process.cwd(),t=await S(r);t.isErr()&&(process.stderr.write(`${x(t.error)}
29
- `),process.exit(1));let n=z(M,t.value),o=_i.join(r,O);if(e.check){let i=await Di(o,"utf8").catch(()=>null);if(i===n){process.stdout.write(`${Or()}
30
- `);return}process.stderr.write(`${_r(i==null?"missing":"stale")}
31
- `),process.exit(1)}await Oi(o,n),process.stdout.write(`${Fr()}
32
- `)}import{graphql as Vt}from"gql.tada";function ir(e){return e==null?"Max local concurrent runs: unknown (not signed in?)":`Max local concurrent runs: ${String(e)} (the daemon applies changes live)`}import{graphql as Fi}from"gql.tada";var Ni=Fi(`
23
+ `),!1)}async function Kn(){let e=A(),r=_(e);r==null&&(process.stdout.write("Not signed in. Run `npx ripplo auth login`.\n"),process.exit(1));let n=await tr({serverUrl:e,token:r});n.kind==="unreachable"&&(process.stdout.write(`Could not reach the Ripplo server at ${e} (${n.message}). The token may still be valid \u2014 check the server / network and retry.
24
+ `),process.exit(1)),n.kind==="rejected"&&(process.stdout.write("Token rejected by the server. Run `npx ripplo auth login` to sign in again.\n"),process.stdout.write(`(Claude Code: run it yourself in the background \u2014 the user just approves in the browser.)
25
+ `),process.exit(1)),process.stdout.write(`Signed in as ${n.viewer.name} <${n.viewer.email}> (${e})
26
+ `)}function Qn(){let e=A();if(!Rr(e)){process.stdout.write(`Already signed out.
27
+ `);return}process.stdout.write(`Signed out. Removed ${Qe(e)}.
28
+ `)}async function tr({serverUrl:e,token:r}){try{let t=(await u({config:M({serverUrl:e,token:r}),document:Oi,variables:void 0})).currentUser;return t==null?{kind:"rejected"}:{kind:"ok",viewer:{email:t.email,name:t.name}}}catch(n){return br(n)==="UNAUTHENTICATED"?{kind:"rejected"}:{kind:"unreachable",message:n instanceof Error?n.message:String(n)}}}function Fi(e){let r=e.name.trim().split(/\s+/)[0];return r!=null&&r.length>0?r:e.email}import{readFile as Ni,writeFile as Ui}from"fs/promises";import Mi from"path";async function Xn(e){let r=process.cwd(),n=await S(r);n.isErr()&&(process.stderr.write(`${x(n.error)}
29
+ `),process.exit(1));let t=G(U,n.value),o=Mi.join(r,O);if(e.check){let i=await Ni(o,"utf8").catch(()=>null);if(i===t){process.stdout.write(`${Dr()}
30
+ `);return}process.stderr.write(`${Or(i==null?"missing":"stale")}
31
+ `),process.exit(1)}await Ui(o,t),process.stdout.write(`${_r()}
32
+ `)}import{graphql as Yn}from"gql.tada";function or(e){return e==null?"Max local concurrent runs: unknown (not signed in?)":`Max local concurrent runs: ${String(e)} (the daemon applies changes live)`}import{graphql as Hi}from"gql.tada";var Wi=Hi(`
33
33
  query DevSessionCheckPreflight($projectId: String!, $cwd: String!) {
34
34
  project(id: $projectId) {
35
35
  id
@@ -40,24 +40,24 @@ import{$ as xt,A as ae,B as Wr,C as le,D as v,E as nt,G as ot,H as it,I as st,J
40
40
  }
41
41
  `);function T(){return w(process.cwd()).match(e=>e,e=>{process.stderr.write(`${j(e)}
42
42
  `),process.stderr.write(`${c("setup")}
43
- `),process.exit(1)})}async function pe(e){(await u({config:e,document:Ni,variables:{cwd:e.cwd,projectId:e.projectId}})).project?.devSession==null&&(process.stderr.write("No active dev session. Start `npx ripplo daemon` as a background process (your app's dev server must also be running), then retry. Or run `/ripplo:start` in Claude Code.\n"),process.stderr.write(`${c("setup")}
44
- `),process.exit(1))}var Ui=Vt(`
43
+ `),process.exit(1)})}async function ce(e){(await u({config:e,document:Wi,variables:{cwd:e.cwd,projectId:e.projectId}})).project?.devSession==null&&(process.stderr.write("No active dev session. Start `npx ripplo daemon` as a background process (your app's dev server must also be running), then retry. Or run `/ripplo:start` in Claude Code.\n"),process.stderr.write(`${c("setup")}
44
+ `),process.exit(1))}var qi=Yn(`
45
45
  mutation CliUpdateMaxLocalConcurrentRuns($value: Int!) {
46
46
  updateMaxLocalConcurrentRuns(value: $value) {
47
47
  id
48
48
  maxLocalConcurrentRuns
49
49
  }
50
50
  }
51
- `),Mi=Vt(`
51
+ `),Bi=Yn(`
52
52
  query CliMaxLocalConcurrentRuns {
53
53
  currentUser {
54
54
  id
55
55
  maxLocalConcurrentRuns
56
56
  }
57
57
  }
58
- `);async function Gt({value:e}){let r=T();if(e==null){let n=await u({config:r,document:Mi,variables:{}});process.stdout.write(`${ir(n.currentUser?.maxLocalConcurrentRuns)}
59
- `);return}let t=await u({config:r,document:Ui,variables:{value:e}});process.stdout.write(`${ir(t.updateMaxLocalConcurrentRuns?.maxLocalConcurrentRuns)}
60
- `)}import{graphql as Hi}from"gql.tada";var Wi=Hi(`
58
+ `);async function Zn({value:e}){let r=T();if(e==null){let t=await u({config:r,document:Bi,variables:{}});process.stdout.write(`${or(t.currentUser?.maxLocalConcurrentRuns)}
59
+ `);return}let n=await u({config:r,document:qi,variables:{value:e}});process.stdout.write(`${or(n.updateMaxLocalConcurrentRuns?.maxLocalConcurrentRuns)}
60
+ `)}import{graphql as Vi}from"gql.tada";var Gi=Vi(`
61
61
  query DevSessionCheckHealth($projectId: String!, $cwd: String!) {
62
62
  project(id: $projectId) {
63
63
  id
@@ -66,46 +66,46 @@ import{$ as xt,A as ae,B as Wr,C as le,D as v,E as nt,G as ot,H as it,I as st,J
66
66
  }
67
67
  }
68
68
  }
69
- `);function zt(e){switch(e.status){case"active":return e.gitMidOperation?"\u2713 Dev session: ripplo daemon is live (hooks auto-paused while git is mid-merge/rebase/cherry-pick \u2014 they resume when the operation ends)":"\u2713 Dev session: ripplo daemon is live for this directory";case"starting":return"! Dev session: the daemon is running locally but hasn't registered with the server yet. Wait a few seconds and re-run `npx ripplo doctor`. If it persists, check the daemon output for auth or network errors.";case"missing":return"\u2717 Dev session: no daemon running. Start `npx ripplo daemon` as a background process (separate from your app's dev server). Without it, `npx ripplo run` refuses to dispatch and scope/coverage hooks won't arm."}}async function Jt(e){let r=ve(e),t=Mr(e),n=w(e).unwrapOr(void 0);return n==null?{gitMidOperation:t,status:r?"starting":"missing",type:"dev-session"}:(await u({config:n,document:Wi,variables:{cwd:n.cwd,projectId:n.projectId}}).catch(()=>null))?.project?.devSession!=null?{gitMidOperation:t,status:"active",type:"dev-session"}:{gitMidOperation:t,status:r?"starting":"missing",type:"dev-session"}}function Kt(e){switch(e.type){case"settings":return qi(e);case"env-files":return Bi(e);case"token":return Vi(e);case"dev-server":return Gi(e);case"dev-session":return zt(e);case"preconditions":return zi(e);case"webhook-verification":return Ji(e);case"preconditions-validation":return Ki(e);case"workflows":return Qi(e);case"browser":return Yi(e);case"engine-endpoint":return e.reachable?`\u2713 Engine endpoint: ${e.url} is reachable`:`\u2717 Engine endpoint: ${e.url} is not reachable`;case"adapter-enabled":return Zi(e);case"lockfile":return es(e);case"pre-commit-hook":return rs(e);case"plugin-version":return ts(e)}}function qi(e){return e.valid?"\u2713 Settings: project configured":`\u2717 Settings: missing fields: ${e.missingFields.join(", ")}`}function Bi(e){return e.missing.length===0?"\u2713 Env files: declared files present":`\u2717 Env files: declared in .ripplo/project.json but missing:
70
- ${e.missing.map(t=>` ${t}`).join(`
69
+ `);function et(e){switch(e.status){case"active":return e.gitMidOperation?"\u2713 Dev session: ripplo daemon is live (hooks auto-paused while git is mid-merge/rebase/cherry-pick \u2014 they resume when the operation ends)":"\u2713 Dev session: ripplo daemon is live for this directory";case"starting":return"! Dev session: the daemon is running locally but hasn't registered with the server yet. Wait a few seconds and re-run `npx ripplo doctor`. If it persists, check the daemon output for auth or network errors.";case"missing":return"\u2717 Dev session: no daemon running. Start `npx ripplo daemon` as a background process (separate from your app's dev server). Without it, `npx ripplo run` refuses to dispatch and scope/coverage hooks won't arm."}}async function rt(e){let r=we(e),n=Ur(e),t=w(e).unwrapOr(void 0);return t==null?{gitMidOperation:n,status:r?"starting":"missing",type:"dev-session"}:(await u({config:t,document:Gi,variables:{cwd:t.cwd,projectId:t.projectId}}).catch(()=>null))?.project?.devSession!=null?{gitMidOperation:n,status:"active",type:"dev-session"}:{gitMidOperation:n,status:r?"starting":"missing",type:"dev-session"}}function nt(e){switch(e.type){case"settings":return zi(e);case"env-files":return Ji(e);case"token":return Ki(e);case"dev-server":return Qi(e);case"dev-session":return et(e);case"preconditions":return Xi(e);case"webhook-verification":return Yi(e);case"preconditions-validation":return Zi(e);case"workflows":return es(e);case"browser":return ns(e);case"engine-endpoint":return e.reachable?`\u2713 Engine endpoint: ${e.url} is reachable`:`\u2717 Engine endpoint: ${e.url} is not reachable`;case"adapter-enabled":return ts(e);case"lockfile":return os(e);case"pre-commit-hook":return is(e);case"plugin-version":return ss(e)}}function zi(e){return e.valid?"\u2713 Settings: project configured":`\u2717 Settings: missing fields: ${e.missingFields.join(", ")}`}function Ji(e){return e.missing.length===0?"\u2713 Env files: declared files present":`\u2717 Env files: declared in .ripplo/project.json but missing:
70
+ ${e.missing.map(n=>` ${n}`).join(`
71
71
  `)}
72
- In a git worktree? Copy the env file from the main checkout, or symlink to a shared file outside the working tree.`}function Vi(e){switch(e.status){case"valid":return`\u2713 Auth: signed in as ${e.email??"unknown"}`;case"missing":return"\u2717 Auth: not signed in. Run `npx ripplo auth login`.\n (Claude Code: run it yourself in the background \u2014 the user just approves in the browser.)";case"invalid":return"\u2717 Auth: saved token rejected by the server. Run `npx ripplo auth login` to sign in again.\n (Claude Code: run it yourself in the background \u2014 the user just approves in the browser.)";case"unreachable":return`! Auth: could not reach ${A()} to validate the token.`}}function Gi(e){return e.reachable?`\u2713 Dev server: ${e.appUrl} is reachable`:`\u2717 Dev server: ${e.appUrl} is not responding. Start your dev server.`}function zi(e){return e.count===0?"! Entities: none defined":e.configured?e.endpointReachable===void 0?`! Entities: ${String(e.count)} defined (could not verify engine endpoint)`:`\u2713 Entities: ${String(e.count)} defined, engine configured`:`\u2717 Entities: ${String(e.count)} defined but RIPPLO_ENGINE_URL is not set in your env file`}function Ji(e){return e.rejectsUnsigned?"\u2713 Webhook verification: unsigned requests rejected":"\u2717 Webhook verification: endpoint accepted an unsigned request \u2014 signature verification may be missing or misconfigured."}function Ki(e){if(!e.found)return"\u2717 Model: DSL failed to compile";if(e.valid)return"\u2713 Model: valid";let r=e.errors.map(t=>` - ${t.path===""?"":t.path+": "}${t.message}`);return`\u2717 Model: ${String(e.errorCount)} validation error${e.errorCount===1?"":"s"}
72
+ In a git worktree? Copy the env file from the main checkout, or symlink to a shared file outside the working tree.`}function Ki(e){switch(e.status){case"valid":return`\u2713 Auth: signed in as ${e.email??"unknown"}`;case"missing":return"\u2717 Auth: not signed in. Run `npx ripplo auth login`.\n (Claude Code: run it yourself in the background \u2014 the user just approves in the browser.)";case"invalid":return"\u2717 Auth: saved token rejected by the server. Run `npx ripplo auth login` to sign in again.\n (Claude Code: run it yourself in the background \u2014 the user just approves in the browser.)";case"unreachable":return`! Auth: could not reach ${A()} to validate the token.`}}function Qi(e){return e.reachable?`\u2713 Dev server: ${e.appUrl} is reachable`:`\u2717 Dev server: ${e.appUrl} is not responding. Start your dev server.`}function Xi(e){return e.count===0?"! Entities: none defined":e.configured?e.endpointReachable===void 0?`! Entities: ${String(e.count)} defined (could not verify engine endpoint)`:`\u2713 Entities: ${String(e.count)} defined, engine configured`:`\u2717 Entities: ${String(e.count)} defined but RIPPLO_ENGINE_URL is not set in your env file`}function Yi(e){return e.rejectsUnsigned?"\u2713 Webhook verification: unsigned requests rejected":"\u2717 Webhook verification: endpoint accepted an unsigned request \u2014 signature verification may be missing or misconfigured."}function Zi(e){if(!e.found)return"\u2717 Model: DSL failed to compile";if(e.valid)return"\u2713 Model: valid";let r=e.errors.map(n=>` - ${n.path===""?"":n.path+": "}${n.message}`);return`\u2717 Model: ${String(e.errorCount)} validation error${e.errorCount===1?"":"s"}
73
73
  ${r.join(`
74
- `)}`}function Qi(e){if(e.total===0)return"! Tests: none defined";if(e.invalidNames.length===0)return`\u2713 Tests: ${String(e.total)} valid`;let r=e.invalidWorkflows.map(t=>Xi(t));return`\u2717 Tests: ${String(e.invalidNames.length)} invalid
74
+ `)}`}function es(e){if(e.total===0)return"! Tests: none defined";if(e.invalidNames.length===0)return`\u2713 Tests: ${String(e.total)} valid`;let r=e.invalidWorkflows.map(n=>rs(n));return`\u2717 Tests: ${String(e.invalidNames.length)} invalid
75
75
  ${r.join(`
76
- `)}`}function Xi(e){let r=e.errors.map(t=>" - "+(t.path===""?"":t.path+": ")+t.message);return" "+e.name+`:
76
+ `)}`}function rs(e){let r=e.errors.map(n=>" - "+(n.path===""?"":n.path+": ")+n.message);return" "+e.name+`:
77
77
  `+r.join(`
78
- `)}function Yi(e){return e.installed?"\u2713 Browser: Chromium installed":"\u2717 Browser: Chromium not installed. Run `npx playwright install chromium`."}function Zi(e){switch(e.status){case"enabled":return`\u2713 Adapter: enabled at ${e.url}`;case"disabled":return"\u2717 Adapter: disabled (handler returned 404). Set ENABLE_RIPPLO_TESTING=true in your app's env (e.g. apps/<app>/.env.local for Next.js) and restart the dev server.";case"bad-secret":return"\u2717 Adapter: webhook signature rejected. RIPPLO_WEBHOOK_SECRET seen by your dev server does not match the one in your env file.";case"unreachable":return`\u2717 Adapter: ${e.url} could not be reached for signed probe.`;case"no-secret":return"\u2717 Adapter: RIPPLO_WEBHOOK_SECRET is not set in your env file (declared in .ripplo/project.json)."}}function es(e){switch(e.status){case"match":return`\u2713 Lockfile: ${O} is up to date`;case"missing":return`\u2717 Lockfile: ${O} is missing \u2014 run \`npx ripplo compile\` and commit it`;case"stale":return`\u2717 Lockfile: ${O} is out of date \u2014 run \`npx ripplo compile\` and commit it`}}function rs(e){return e.installed?"\u2713 Pre-commit hook: .git/hooks/pre-commit runs `ripplo compile --check`":"! Pre-commit hook: .git/hooks/pre-commit does not run `ripplo compile --check` \u2014 see the setup skill for the snippet"}function ts(e){return e.installed===e.cliVersion?`\u2713 Claude plugin: v${e.installed} matches CLI`:`! Claude plugin: installed v${e.installed}, CLI v${e.cliVersion} \u2014 run \`npx ripplo update\` (or /plugin in Claude Code)`}import Q from"fs";import ls from"os";import sr from"path";import{z as B}from"zod";import ns from"latest-version";import Qt from"semver";async function Pe(e){try{return await Promise.race([ns("ripplo"),new Promise(r=>setTimeout(()=>{r(void 0)},e))])}catch{return}}function Xt(e){return e.filter(r=>Qt.valid(r)!=null).toSorted(Qt.rcompare)[0]}function W(e){let r=J(e),t=Qr(e);return[...Gr(r,t.dataRules).map(n=>({gap:n,kind:"cascade-gap"})),...Vr(t.pageRules).map(n=>({conflict:n,kind:"page-rule-conflict"}))]}function b(e,r,t){let n=e===1?r:t??`${r}s`;return`${String(e)} ${n}`}function Yt(e){return`${C.good("ok")} \u2014 no static model violations (${b(e,"test")})`}function q(e){return[`${C.bad("fail")} \u2014 ${b(e.length,"static model violation")}:`,...e.map(r=>Jr(r))].join(`
78
+ `)}function ns(e){return e.installed?"\u2713 Browser: Chromium installed":"\u2717 Browser: Chromium not installed. Run `npx playwright install chromium`."}function ts(e){switch(e.status){case"enabled":return`\u2713 Adapter: enabled at ${e.url}`;case"disabled":return"\u2717 Adapter: disabled (handler returned 404). Set ENABLE_RIPPLO_TESTING=true in your app's env (e.g. apps/<app>/.env.local for Next.js) and restart the dev server.";case"bad-secret":return"\u2717 Adapter: webhook signature rejected. RIPPLO_WEBHOOK_SECRET seen by your dev server does not match the one in your env file.";case"unreachable":return`\u2717 Adapter: ${e.url} could not be reached for signed probe.`;case"no-secret":return"\u2717 Adapter: RIPPLO_WEBHOOK_SECRET is not set in your env file (declared in .ripplo/project.json)."}}function os(e){switch(e.status){case"match":return`\u2713 Lockfile: ${O} is up to date`;case"missing":return`\u2717 Lockfile: ${O} is missing \u2014 run \`npx ripplo compile\` and commit it`;case"stale":return`\u2717 Lockfile: ${O} is out of date \u2014 run \`npx ripplo compile\` and commit it`}}function is(e){return e.installed?"\u2713 Pre-commit hook: .git/hooks/pre-commit runs `ripplo compile --check`":"! Pre-commit hook: .git/hooks/pre-commit does not run `ripplo compile --check` \u2014 see the setup skill for the snippet"}function ss(e){return e.installed===e.cliVersion?`\u2713 Claude plugin: v${e.installed} matches CLI`:`! Claude plugin: installed v${e.installed}, CLI v${e.cliVersion} \u2014 run \`npx ripplo update\` (or /plugin in Claude Code)`}import K from"fs";import us from"os";import ir from"path";import{z as q}from"zod";import as from"latest-version";import tt from"semver";async function Ce(e){try{return await Promise.race([as("ripplo"),new Promise(r=>setTimeout(()=>{r(void 0)},e))])}catch{return}}function ot(e){return e.filter(r=>tt.valid(r)!=null).toSorted(tt.rcompare)[0]}function H(e){let r=z(e),n=nn(e);return[...Vr(r,n.dataRules).map(t=>({gap:t,kind:"cascade-gap"})),...Br(n.pageRules).map(t=>({conflict:t,kind:"page-rule-conflict"}))]}function b(e,r,n){let t=e===1?r:n??`${r}s`;return`${String(e)} ${t}`}function it(e){return`${C.good("ok")} \u2014 no static model violations (${b(e,"test")})`}function W(e){return[`${C.bad("fail")} \u2014 ${b(e.length,"static model violation")}:`,...e.map(r=>zr(r))].join(`
79
79
 
80
- `)}function Ee(e){return[`${C.warn("warn")} \u2014 ${b(e.length,"model coverage gap")} (not blocking):`,...e.map(r=>` ${os(r)}`),`Coverage gaps mean the model can't catch regressions there. Stub the missing flows. ${c("discover")}`].join(`
81
- `)}function os(e){switch(e.kind){case"entity-never-given":return`${e.entity} \u2014 declared but no implemented test seeds it, so no flow exercises this state`;case"entity-never-mutated":return`${e.entity} \u2014 seeded but never asserted created/updated/deleted, so mutations to it ship unchecked`;case"unmatchable-delete-key":return`test ${e.workflow}/${e.testSlug} asserts ${e.entity} deleted with a literal ${e.field} but no seeded ${e.entity} sets ${e.field} \u2014 the solver treats it as no-match, so counts may mispredict`}}import{graphql as is}from"gql.tada";var ss=is(`
80
+ `)}function Pe(e){return[`${C.warn("warn")} \u2014 ${b(e.length,"model coverage gap")} (not blocking):`,...e.map(r=>` ${ls(r)}`),`Coverage gaps mean the model can't catch regressions there. Stub the missing flows. ${c("discover")}`].join(`
81
+ `)}function ls(e){switch(e.kind){case"entity-never-given":return`${e.entity} \u2014 declared but no implemented test seeds it, so no flow exercises this state`;case"entity-never-mutated":return`${e.entity} \u2014 seeded but never asserted created/updated/deleted, so mutations to it ship unchecked`;case"unmatchable-delete-key":return`test ${e.workflow}/${e.testSlug} asserts ${e.entity} deleted with a literal ${e.field} but no seeded ${e.entity} sets ${e.field} \u2014 the solver treats it as no-match, so counts may mispredict`}}import{graphql as ds}from"gql.tada";var cs=ds(`
82
82
  query DoctorAuthViewer {
83
83
  currentUser {
84
84
  name
85
85
  email
86
86
  }
87
87
  }
88
- `),as="Failed to connect to Ripplo server";async function Zt({serverUrl:e,token:r}){try{let t=await u({config:H({serverUrl:e,token:r}),document:ss,variables:void 0});return t.currentUser==null?{kind:"invalid"}:{email:t.currentUser.email,kind:"valid"}}catch(t){return(t instanceof Error?t.message:String(t)).startsWith(as)?{kind:"unreachable"}:{kind:"invalid"}}}function en(e){switch(e.type){case"settings":return!e.valid;case"env-files":return e.missing.length>0;case"token":return e.status==="missing"||e.status==="invalid";case"dev-server":return!e.reachable;case"dev-session":return e.status!=="active";case"preconditions":return e.count>0&&!e.configured;case"engine-endpoint":return!e.reachable;case"adapter-enabled":return e.status!=="enabled";case"webhook-verification":return!e.rejectsUnsigned;case"preconditions-validation":return!e.valid;case"workflows":return e.invalidNames.length>0;case"browser":return!e.installed;case"lockfile":return e.status!=="match";case"pre-commit-hook":return!e.installed;case"plugin-version":return!1}}async function rn(e){let r=await ps(e),t={missing:Ar(e),type:"env-files"},n=await us(),o=await ms(),i=await S(e),a=fs(i),s=gs(i),l=await hs(e,i),d=ks(e),g=cs(e),p=await ws(r,i);return[r,t,n,...p,a,s,l,d,o,...g==null?[]:[g]]}var ds=B.object({plugins:B.record(B.string(),B.array(B.object({projectPath:B.string().optional(),scope:B.string().optional(),version:B.string()})))});function cs(e){let r=sr.join(ls.homedir(),".claude","plugins","installed_plugins.json"),t=Q.existsSync(r)?Q.readFileSync(r,"utf8"):null;if(t==null)return null;let n=ds.safeParse(JSON.parse(t));if(!n.success)return null;let o=Object.entries(n.data.plugins).filter(([a])=>a.startsWith("ripplo@")).flatMap(([,a])=>a).filter(a=>a.scope==="user"||a.projectPath===e).map(a=>a.version),i=Xt(o);return i==null?null:{cliVersion:P(),installed:i,type:"plugin-version"}}async function ps(e){let r=await Hr(e);return r.valid?{missingFields:[],type:"settings",valid:!0}:{missingFields:r.errors.map(n=>n.path).filter(n=>n.length>0),type:"settings",valid:!1}}async function us(){let e=A(),r=F(e);if(r==null||r.length===0)return{email:void 0,status:"missing",type:"token"};let t=await Zt({serverUrl:e,token:r});return t.kind==="valid"?{email:t.email,status:"valid",type:"token"}:{email:void 0,status:t.kind,type:"token"}}async function ms(){let{chromium:e}=await import("playwright"),r=e.executablePath();return{installed:Q.existsSync(r),type:"browser"}}function fs(e){if(e.isErr())return{errorCount:1,errors:[{message:x(e.error),path:".ripplo/"}],found:!1,type:"preconditions-validation",valid:!1};let r=W(e.value);return{errorCount:r.length,errors:r.length===0?[]:[{message:q(r),path:""}],found:!0,type:"preconditions-validation",valid:r.length===0}}function gs(e){if(e.isErr())return{invalidNames:[],invalidWorkflows:[],total:0,type:"workflows",valid:0};let r=e.value.workflows.length;return{invalidNames:[],invalidWorkflows:[],total:r,type:"workflows",valid:r}}async function hs(e,r){if(r.isErr())return{status:"missing",type:"lockfile"};let t=z(M,r.value),n=await Q.promises.readFile(sr.join(e,O),"utf8").catch(()=>null);return{status:ys(n,t),type:"lockfile"}}function ys(e,r){return e==null?"missing":e===r?"match":"stale"}function ks(e){let r=sr.join(e,".git","hooks","pre-commit");return Q.existsSync(r)?{installed:Q.readFileSync(r,"utf8").includes("ripplo compile --check"),type:"pre-commit-hook"}:{installed:!1,type:"pre-commit-hook"}}async function ws(e,r){if(!e.valid||r.isErr())return[];let t=jr().map(s=>({appUrl:s.appUrl,engineUrl:s.engineUrl,webhookSecret:s.webhookSecret})).unwrapOr(void 0);if(t==null)return[];let n=[],o=await ae(t.appUrl)==null;n.push({appUrl:t.appUrl,reachable:o,type:"dev-server"});let i=await Jt(process.cwd());n.push(i);let a=await vs(t,r);return n.push(...a),n}async function vs(e,r){let t=r.isOk()?r.value.entities.length:0,n=e.engineUrl.length>0,o={configured:n,count:t,endpointReachable:void 0,type:"preconditions"};if(t===0||!n)return[o];let i=Ss(e.appUrl,e.engineUrl);if(i==null)return[o];let a=await ae(i)==null,s=[];if(s.push({configured:!0,count:t,endpointReachable:a,type:"preconditions"}),tn(e.engineUrl)&&s.push({reachable:a,type:"engine-endpoint",url:i}),!a)return s;let l=await Wr({appUrl:e.appUrl,engineUrl:e.engineUrl});s.push({rejectsUnsigned:l==null,type:"webhook-verification"});let d=await bs({engineUrl:i,webhookSecret:e.webhookSecret});return s.push({status:d,type:"adapter-enabled",url:i}),s}async function bs({engineUrl:e,webhookSecret:r}){if(r.length===0)return"no-secret";let t=JSON.stringify({});try{let n=await fetch(`${e}/setup`,{body:t,headers:{"Content-Type":"application/json",...Er({body:t,secret:r})},method:"PUT",signal:AbortSignal.timeout(5e3)});return n.status===404?"disabled":n.status===401||n.status===403?"bad-secret":"enabled"}catch{return"unreachable"}}function tn(e){return e.startsWith("http://")||e.startsWith("https://")}function Ss(e,r){return tn(r)?r:`${e}${r}`}var xs=3e3;async function nn(){let e=process.cwd(),[r,t]=await Promise.all([Pe(xs),rn(e)]);process.stdout.write(`${Ce({current:P(),latest:r})}
89
- `);let n=t.map(i=>Kt(i));process.stdout.write(n.join(`
88
+ `),ps="Failed to connect to Ripplo server";async function st({serverUrl:e,token:r}){try{let n=await u({config:M({serverUrl:e,token:r}),document:cs,variables:void 0});return n.currentUser==null?{kind:"invalid"}:{email:n.currentUser.email,kind:"valid"}}catch(n){return(n instanceof Error?n.message:String(n)).startsWith(ps)?{kind:"unreachable"}:{kind:"invalid"}}}function at(e){switch(e.type){case"settings":return!e.valid;case"env-files":return e.missing.length>0;case"token":return e.status==="missing"||e.status==="invalid";case"dev-server":return!e.reachable;case"dev-session":return e.status!=="active";case"preconditions":return e.count>0&&!e.configured;case"engine-endpoint":return!e.reachable;case"adapter-enabled":return e.status!=="enabled";case"webhook-verification":return!e.rejectsUnsigned;case"preconditions-validation":return!e.valid;case"workflows":return e.invalidNames.length>0;case"browser":return!e.installed;case"lockfile":return e.status!=="match";case"pre-commit-hook":return!e.installed;case"plugin-version":return!1}}async function lt(e){let r=await gs(e),n={missing:jr(e),type:"env-files"},t=await hs(),o=await ys(),i=await S(e),a=ks(i),s=ws(i),l=await vs(e,i),d=Ss(e),g=fs(e),p=await xs(r,i);return[r,n,t,...p,a,s,l,d,o,...g==null?[]:[g]]}var ms=q.object({plugins:q.record(q.string(),q.array(q.object({projectPath:q.string().optional(),scope:q.string().optional(),version:q.string()})))});function fs(e){let r=ir.join(us.homedir(),".claude","plugins","installed_plugins.json"),n=K.existsSync(r)?K.readFileSync(r,"utf8"):null;if(n==null)return null;let t=ms.safeParse(JSON.parse(n));if(!t.success)return null;let o=Object.entries(t.data.plugins).filter(([a])=>a.startsWith("ripplo@")).flatMap(([,a])=>a).filter(a=>a.scope==="user"||a.projectPath===e).map(a=>a.version),i=ot(o);return i==null?null:{cliVersion:P(),installed:i,type:"plugin-version"}}async function gs(e){let r=await Mr(e);return r.valid?{missingFields:[],type:"settings",valid:!0}:{missingFields:r.errors.map(t=>t.path).filter(t=>t.length>0),type:"settings",valid:!1}}async function hs(){let e=A(),r=_(e);if(r==null||r.length===0)return{email:void 0,status:"missing",type:"token"};let n=await st({serverUrl:e,token:r});return n.kind==="valid"?{email:n.email,status:"valid",type:"token"}:{email:void 0,status:n.kind,type:"token"}}async function ys(){let{chromium:e}=await import("playwright"),r=e.executablePath();return{installed:K.existsSync(r),type:"browser"}}function ks(e){if(e.isErr())return{errorCount:1,errors:[{message:x(e.error),path:".ripplo/"}],found:!1,type:"preconditions-validation",valid:!1};let r=H(e.value);return{errorCount:r.length,errors:r.length===0?[]:[{message:W(r),path:""}],found:!0,type:"preconditions-validation",valid:r.length===0}}function ws(e){if(e.isErr())return{invalidNames:[],invalidWorkflows:[],total:0,type:"workflows",valid:0};let r=e.value.workflows.length;return{invalidNames:[],invalidWorkflows:[],total:r,type:"workflows",valid:r}}async function vs(e,r){if(r.isErr())return{status:"missing",type:"lockfile"};let n=G(U,r.value),t=await K.promises.readFile(ir.join(e,O),"utf8").catch(()=>null);return{status:bs(t,n),type:"lockfile"}}function bs(e,r){return e==null?"missing":e===r?"match":"stale"}function Ss(e){let r=ir.join(e,".git","hooks","pre-commit");return K.existsSync(r)?{installed:K.readFileSync(r,"utf8").includes("ripplo compile --check"),type:"pre-commit-hook"}:{installed:!1,type:"pre-commit-hook"}}async function xs(e,r){if(!e.valid||r.isErr())return[];let n=Ir().map(s=>({appUrl:s.appUrl,engineUrl:s.engineUrl,webhookSecret:s.webhookSecret})).unwrapOr(void 0);if(n==null)return[];let t=[],o=await se(n.appUrl)==null;t.push({appUrl:n.appUrl,reachable:o,type:"dev-server"});let i=await rt(process.cwd());t.push(i);let a=await Rs(n,r);return t.push(...a),t}async function Rs(e,r){let n=r.isOk()?r.value.entities.length:0,t=e.engineUrl.length>0,o={configured:t,count:n,endpointReachable:void 0,type:"preconditions"};if(n===0||!t)return[o];let i=Ps(e.appUrl,e.engineUrl);if(i==null)return[o];let a=await se(i)==null,s=[];if(s.push({configured:!0,count:n,endpointReachable:a,type:"preconditions"}),dt(e.engineUrl)&&s.push({reachable:a,type:"engine-endpoint",url:i}),!a)return s;let l=await Hr({appUrl:e.appUrl,engineUrl:e.engineUrl});s.push({rejectsUnsigned:l==null,type:"webhook-verification"});let d=await Cs({engineUrl:i,webhookSecret:e.webhookSecret});return s.push({status:d,type:"adapter-enabled",url:i}),s}async function Cs({engineUrl:e,webhookSecret:r}){if(r.length===0)return"no-secret";let n=JSON.stringify({});try{let t=await fetch(`${e}/setup`,{body:n,headers:{"Content-Type":"application/json",...Pr({body:n,secret:r})},method:"PUT",signal:AbortSignal.timeout(5e3)});return t.status===404?"disabled":t.status===401||t.status===403?"bad-secret":"enabled"}catch{return"unreachable"}}function dt(e){return e.startsWith("http://")||e.startsWith("https://")}function Ps(e,r){return dt(r)?r:`${e}${r}`}var Es=3e3;async function ct(){let e=process.cwd(),[r,n]=await Promise.all([Ce(Es),lt(e)]);process.stdout.write(`${Re({current:P(),latest:r})}
89
+ `);let t=n.map(i=>nt(i));process.stdout.write(t.join(`
90
90
  `)+`
91
- `);let o=t.some(i=>en(i));process.exit(o?1:0)}import $e from"fs";import{graphql as Rs}from"gql.tada";var Cs=Rs(`
91
+ `);let o=n.some(i=>at(i));process.exit(o?1:0)}import Ee from"fs";import{graphql as $s}from"gql.tada";var Is=$s(`
92
92
  mutation CliSetHooksPaused($projectId: String!, $paused: Boolean!) {
93
93
  setHooksPaused(projectId: $projectId, paused: $paused) {
94
94
  id
95
95
  }
96
96
  }
97
- `);async function on(){process.stdin.isTTY||(process.stderr.write("`ripplo hooks pause` only runs interactively from a terminal. Bypassing the gate is a human decision, not an agent decision.\n"),process.exit(1));let e=process.cwd();ke(e);let r=le(e);if($e.existsSync(r)){process.stdout.write("Hooks already paused. Run `npx ripplo hooks resume` to re-enable.\n");return}$e.writeFileSync(r,""),await an(e,!0),process.stdout.write("Hooks paused. Pre-edit gates and stop enforcement are off until you run `npx ripplo hooks resume`.\n")}async function sn(){let e=process.cwd(),r=le(e);if(!$e.existsSync(r)){process.stdout.write(`Hooks already active.
98
- `);return}$e.unlinkSync(r),await an(e,!1),process.stdout.write(`Hooks resumed.
99
- `)}async function an(e,r){let t=w(e).unwrapOr(void 0);t!=null&&await u({config:t,document:Cs,variables:{paused:r,projectId:t.projectId}}).catch(n=>{process.stderr.write(`Warning: could not push hook-pause state to the server (${n instanceof Error?n.message:String(n)}) \u2014 the dashboard may show stale hook status.
100
- `)})}import Z from"fs";import ee from"path";import{input as fn,select as gn}from"@inquirer/prompts";import{graphql as ta}from"gql.tada";import{exec as $s,execFile as Is}from"child_process";import{err as js,ok as ln}from"neverthrow";import k from"fs";import{createRequire as As}from"module";import h from"path";import{promisify as cn}from"util";import{writeFile as Ps}from"fs/promises";import Es from"path";async function X(e){let r=await S(e);return r.isOk()&&await Ps(Es.join(e,O),z(M,r.value)),r}function U(e){return e.workflows.filter(r=>r.stub).map(r=>r.name)}var Ls=["@ripplo/testing","@ripplo/instrument"],dn=".ripplo/ripplo.lock linguist-generated=true",Ts=[".ripplo/debug/",".ripplo/.local/"],Ds=cn($s),Os=cn(Is);async function pn({cwd:e,onStep:r}){r("Scaffolding project files..."),Gs({cwd:e}),r("Updating .gitignore..."),zs(e),r("Marking ripplo.lock as generated..."),Us(e),r("Installing dependencies...");let t=await Fs(e),n=[];if(t.ok||n.push({manualCommand:t.cmd,message:`Couldn't auto-install dev dependencies (${t.reason}). Run the command below, then run \`npx ripplo lint\` to compile the lockfile.`}),t.ok){r("Compiling initial lockfile...");let i=await Ns(e);i!=null&&n.push({manualCommand:void 0,message:i})}return r("Setting up browser..."),(await _s()).map(()=>n)}async function _s(){let{chromium:e}=await import("playwright"),r=e.executablePath();if(k.existsSync(r))return ln(void 0);$.info("Chromium not found. Installing via Playwright...");let t=As(import.meta.url),n=h.dirname(t.resolve("playwright/package.json")),o=h.join(n,"cli.js");return await Os(process.execPath,[o,"install","chromium"]),k.existsSync(r)?ln(void 0):js({kind:"playwright-install-failed"})}async function Fs(e){let r=Ms({cwd:e,pm:Vs(e)});$.info("Installing dependencies: %s",r);try{return await Ds(r,{cwd:e}),{ok:!0}}catch(t){let n=t instanceof Error?t.message.split(`
101
- `)[0]??t.message:String(t);return $.warn("Install failed (%s): %s",r,n),{cmd:r,ok:!1,reason:n}}}async function Ns(e){try{await X(e);return}catch(r){return`Couldn't compile initial lockfile: ${r instanceof Error?r.message:String(r)}.`}}function Us(e){let r=h.join(e,".gitattributes"),t=k.existsSync(r)?k.readFileSync(r,"utf8"):"";if(t.includes(dn))return;let n=t.length===0||t.endsWith(`
97
+ `);async function pt(){process.stdin.isTTY||(process.stderr.write("`ripplo hooks pause` only runs interactively from a terminal. Bypassing the gate is a human decision, not an agent decision.\n"),process.exit(1));let e=process.cwd();ye(e);let r=ae(e);if(Ee.existsSync(r)){process.stdout.write("Hooks already paused. Run `npx ripplo hooks resume` to re-enable.\n");return}Ee.writeFileSync(r,""),await mt(e,!0),process.stdout.write("Hooks paused. Pre-edit gates and stop enforcement are off until you run `npx ripplo hooks resume`.\n")}async function ut(){let e=process.cwd(),r=ae(e);if(!Ee.existsSync(r)){process.stdout.write(`Hooks already active.
98
+ `);return}Ee.unlinkSync(r),await mt(e,!1),process.stdout.write(`Hooks resumed.
99
+ `)}async function mt(e,r){let n=w(e).unwrapOr(void 0);n!=null&&await u({config:n,document:Is,variables:{paused:r,projectId:n.projectId}}).catch(t=>{process.stderr.write(`Warning: could not push hook-pause state to the server (${t instanceof Error?t.message:String(t)}) \u2014 the dashboard may show stale hook status.
100
+ `)})}import Y from"fs";import Z from"path";import{input as vt,select as bt}from"@inquirer/prompts";import{graphql as sa}from"gql.tada";import{exec as Ls,execFile as Ts}from"child_process";import{err as Ds,ok as ft}from"neverthrow";import k from"fs";import{createRequire as Os}from"module";import h from"path";import{promisify as ht}from"util";import{writeFile as js}from"fs/promises";import As from"path";async function Q(e){let r=await S(e);return r.isOk()&&await js(As.join(e,O),G(U,r.value)),r}function N(e){return e.workflows.filter(r=>r.stub).map(r=>r.name)}var _s=["@ripplo/testing","@ripplo/instrument"],gt=".ripplo/ripplo.lock linguist-generated=true",Fs=[".ripplo/debug/",".ripplo/.local/"],Ns=ht(Ls),Us=ht(Ts);async function yt({cwd:e,onStep:r}){r("Scaffolding project files..."),Qs({cwd:e}),r("Updating .gitignore..."),Xs(e),r("Marking ripplo.lock as generated..."),qs(e),r("Installing dependencies...");let n=await Hs(e),t=[];if(n.ok||t.push({manualCommand:n.cmd,message:`Couldn't auto-install dev dependencies (${n.reason}). Run the command below, then run \`npx ripplo lint\` to compile the lockfile.`}),n.ok){r("Compiling initial lockfile...");let i=await Ws(e);i!=null&&t.push({manualCommand:void 0,message:i})}return r("Setting up browser..."),(await Ms()).map(()=>t)}async function Ms(){let{chromium:e}=await import("playwright"),r=e.executablePath();if(k.existsSync(r))return ft(void 0);$.info("Chromium not found. Installing via Playwright...");let n=Os(import.meta.url),t=h.dirname(n.resolve("playwright/package.json")),o=h.join(t,"cli.js");return await Us(process.execPath,[o,"install","chromium"]),k.existsSync(r)?ft(void 0):Ds({kind:"playwright-install-failed"})}async function Hs(e){let r=Bs({cwd:e,pm:Ks(e)});$.info("Installing dependencies: %s",r);try{return await Ns(r,{cwd:e}),{ok:!0}}catch(n){let t=n instanceof Error?n.message.split(`
101
+ `)[0]??n.message:String(n);return $.warn("Install failed (%s): %s",r,t),{cmd:r,ok:!1,reason:t}}}async function Ws(e){try{await Q(e);return}catch(r){return`Couldn't compile initial lockfile: ${r instanceof Error?r.message:String(r)}.`}}function qs(e){let r=h.join(e,".gitattributes"),n=k.existsSync(r)?k.readFileSync(r,"utf8"):"";if(n.includes(gt))return;let t=n.length===0||n.endsWith(`
102
102
  `)?"":`
103
- `;k.writeFileSync(r,`${t}${n}${dn}
104
- `)}function Ms({cwd:e,pm:r}){let t=Ls.join(" ");return r==="pnpm"?Hs(e)?`pnpm add -wD ${t}`:`pnpm add -D ${t}`:r==="yarn"?Ws({cwd:e,deps:t}):r==="bun"?`bun add -d ${t}`:`npm install -D ${t}`}function Hs(e){return k.existsSync(h.join(e,"pnpm-workspace.yaml"))||k.existsSync(h.join(e,"pnpm-workspace.yml"))}function Ws({cwd:e,deps:r}){return qs(e)?`yarn add -D ${r}`:Bs(e)?`yarn add -WD ${r}`:`yarn add -D ${r}`}function qs(e){if(k.existsSync(h.join(e,".yarnrc.yml"))||k.existsSync(h.join(e,".pnp.cjs"))||k.existsSync(h.join(e,".pnp.js")))return!0;let r=h.join(e,"package.json");if(!k.existsSync(r))return!1;try{let t=JSON.parse(k.readFileSync(r,"utf8"));if(t==null||typeof t!="object"||!("packageManager"in t))return!1;let n=t.packageManager;if(typeof n!="string")return!1;let o=/^yarn@(\d+)/.exec(n);return o!=null&&Number(o[1])>=2}catch{return!1}}function Bs(e){let r=h.join(e,"package.json");if(!k.existsSync(r))return!1;try{let t=JSON.parse(k.readFileSync(r,"utf8"));if(t==null||typeof t!="object"||!("workspaces"in t))return!1;let n=t.workspaces;return Array.isArray(n)||n!=null&&typeof n=="object"}catch{return!1}}function Vs(e){return k.existsSync(h.join(e,"pnpm-lock.yaml"))?"pnpm":k.existsSync(h.join(e,"yarn.lock"))?"yarn":k.existsSync(h.join(e,"bun.lockb"))||k.existsSync(h.join(e,"bun.lock"))?"bun":"npm"}function Gs({cwd:e}){let r=h.join(e,".ripplo"),t=h.join(r,"entities"),n=h.join(r,"singletons"),o=h.join(r,"worlds"),i=h.join(r,"workflows");[t,n,o,i].forEach(a=>{k.mkdirSync(a,{recursive:!0})}),Y(h.join(r,"index.ts"),Js),Y(h.join(t,"index.ts"),Ks),Y(h.join(n,"index.ts"),Qs),Y(h.join(o,"index.ts"),Xs),Y(h.join(i,"index.ts"),Ys),Y(h.join(r,"tsconfig.json"),Zs)}function Y(e,r){k.existsSync(e)||k.writeFileSync(e,r)}function zs(e){let r=h.join(e,".gitignore");if(!k.existsSync(r))return;let t=k.readFileSync(r,"utf8"),n=Ts.filter(i=>!t.includes(i));if(n.length===0)return;let o=t.endsWith(`
103
+ `;k.writeFileSync(r,`${n}${t}${gt}
104
+ `)}function Bs({cwd:e,pm:r}){let n=_s.join(" ");return r==="pnpm"?Vs(e)?`pnpm add -wD ${n}`:`pnpm add -D ${n}`:r==="yarn"?Gs({cwd:e,deps:n}):r==="bun"?`bun add -d ${n}`:`npm install -D ${n}`}function Vs(e){return k.existsSync(h.join(e,"pnpm-workspace.yaml"))||k.existsSync(h.join(e,"pnpm-workspace.yml"))}function Gs({cwd:e,deps:r}){return zs(e)?`yarn add -D ${r}`:Js(e)?`yarn add -WD ${r}`:`yarn add -D ${r}`}function zs(e){if(k.existsSync(h.join(e,".yarnrc.yml"))||k.existsSync(h.join(e,".pnp.cjs"))||k.existsSync(h.join(e,".pnp.js")))return!0;let r=h.join(e,"package.json");if(!k.existsSync(r))return!1;try{let n=JSON.parse(k.readFileSync(r,"utf8"));if(n==null||typeof n!="object"||!("packageManager"in n))return!1;let t=n.packageManager;if(typeof t!="string")return!1;let o=/^yarn@(\d+)/.exec(t);return o!=null&&Number(o[1])>=2}catch{return!1}}function Js(e){let r=h.join(e,"package.json");if(!k.existsSync(r))return!1;try{let n=JSON.parse(k.readFileSync(r,"utf8"));if(n==null||typeof n!="object"||!("workspaces"in n))return!1;let t=n.workspaces;return Array.isArray(t)||t!=null&&typeof t=="object"}catch{return!1}}function Ks(e){return k.existsSync(h.join(e,"pnpm-lock.yaml"))?"pnpm":k.existsSync(h.join(e,"yarn.lock"))?"yarn":k.existsSync(h.join(e,"bun.lockb"))||k.existsSync(h.join(e,"bun.lock"))?"bun":"npm"}function Qs({cwd:e}){let r=h.join(e,".ripplo"),n=h.join(r,"entities"),t=h.join(r,"singletons"),o=h.join(r,"worlds"),i=h.join(r,"workflows");[n,t,o,i].forEach(a=>{k.mkdirSync(a,{recursive:!0})}),X(h.join(r,"index.ts"),Ys),X(h.join(n,"index.ts"),Zs),X(h.join(t,"index.ts"),ea),X(h.join(o,"index.ts"),ra),X(h.join(i,"index.ts"),na),X(h.join(r,"tsconfig.json"),ta)}function X(e,r){k.existsSync(e)||k.writeFileSync(e,r)}function Xs(e){let r=h.join(e,".gitignore");if(!k.existsSync(r))return;let n=k.readFileSync(r,"utf8"),t=Fs.filter(i=>!n.includes(i));if(t.length===0)return;let o=n.endsWith(`
105
105
  `)?"":`
106
- `;k.writeFileSync(r,t+o+n.join(`
106
+ `;k.writeFileSync(r,n+o+t.join(`
107
107
  `)+`
108
- `)}var Js=`import { createRipplo } from "@ripplo/testing";
108
+ `)}var Ys=`import { createRipplo } from "@ripplo/testing";
109
109
  import { entities } from "./entities/index";
110
110
  import { singletons } from "./singletons/index";
111
111
  import { workflows } from "./workflows/index";
@@ -115,7 +115,7 @@ export default createRipplo({
115
115
  singletons,
116
116
  workflows,
117
117
  });
118
- `,Ks=`// Model the app's state as entities. Each entity gets a \`seed\`/\`read\` impl in your
118
+ `,Zs=`// Model the app's state as entities. Each entity gets a \`seed\`/\`read\` impl in your
119
119
  // app's engine funnel (createEngine). See /ripplo:create "Adding an entity".
120
120
  //
121
121
  // Example:
@@ -131,7 +131,7 @@ export default createRipplo({
131
131
  // export const entities = [Project] as const;
132
132
 
133
133
  export const entities = [] as const;
134
- `,Qs=`// Client/global state (e.g. localStorage flags) modeled as singletons.
134
+ `,ea=`// Client/global state (e.g. localStorage flags) modeled as singletons.
135
135
  //
136
136
  // Example:
137
137
  // import { singleton, v } from "@ripplo/testing";
@@ -146,7 +146,7 @@ export const entities = [] as const;
146
146
  // export const singletons = [onboardingDismissed];
147
147
 
148
148
  export const singletons = [];
149
- `,Xs=`// Pure builder functions returning a flat record of entity handles \u2014 the starting state
149
+ `,ra=`// Pure builder functions returning a flat record of entity handles \u2014 the starting state
150
150
  // for workflows. Compose from other worlds. See /ripplo:create "Adding a world".
151
151
  //
152
152
  // Example:
@@ -158,7 +158,7 @@ export const singletons = [];
158
158
  // const project = Project.of({ name: arbitrary(Project.field.name), ownerId: me.id });
159
159
  // return { me, project };
160
160
  // };
161
- `,Ys=`// Each file under ./workflows exports a workflow. Import them here and add to the
161
+ `,na=`// Each file under ./workflows exports a workflow. Import them here and add to the
162
162
  // \`workflows\` array \u2014 that's what createRipplo({ ..., workflows }) receives. Stub with
163
163
  // \`workflow("Intent")\` (no body); implement later with \`workflow("Intent", () => ({ given, steps }))\`.
164
164
  //
@@ -167,7 +167,7 @@ export const singletons = [];
167
167
  // export const workflows = [createProject] as const;
168
168
 
169
169
  export const workflows = [] as const;
170
- `,Zs=`{
170
+ `,ta=`{
171
171
  "compilerOptions": {
172
172
  "strict": true,
173
173
  "noUncheckedIndexedAccess": true,
@@ -183,74 +183,74 @@ export const workflows = [] as const;
183
183
  "include": ["*.ts", "entities/**/*.ts", "singletons/**/*.ts", "worlds/**/*.ts", "workflows/**/*.ts"],
184
184
  "exclude": ["node_modules"]
185
185
  }
186
- `;import Ie from"fs";import un from"path";import{z as ar}from"zod";var ea={autoUpdate:!0,source:{repo:"ripplo/claude-plugin",source:"github"}},mn=ar.record(ar.string(),ar.unknown());function je(e){let r=un.join(e,".claude","settings.json"),t=ra(r);if(t==null)return"unparseable";let n=mn.safeParse(t.extraKnownMarketplaces??{});if(!n.success)return"unparseable";if(n.data.ripplo!=null)return"already-present";let o={...t,extraKnownMarketplaces:{...n.data,ripplo:ea}};return Ie.mkdirSync(un.dirname(r),{recursive:!0}),Ie.writeFileSync(r,`${JSON.stringify(o,null,2)}
187
- `),"written"}function ra(e){if(!Ie.existsSync(e))return{};try{let r=mn.safeParse(JSON.parse(Ie.readFileSync(e,"utf8")));return r.success?r.data:void 0}catch{return}}function Ae(e){switch(e){case"written":return" registered ripplo plugin marketplace in .claude/settings.json (autoUpdate: true \u2014 Claude Code keeps the plugin current)";case"already-present":return" ripplo plugin marketplace already registered in .claude/settings.json";case"unparseable":return" ! .claude/settings.json could not be parsed \u2014 add the ripplo marketplace to extraKnownMarketplaces manually"}}var na=ta(`
186
+ `;import $e from"fs";import kt from"path";import{z as sr}from"zod";var oa={autoUpdate:!0,source:{repo:"ripplo/claude-plugin",source:"github"}},wt=sr.record(sr.string(),sr.unknown());function Ie(e){let r=kt.join(e,".claude","settings.json"),n=ia(r);if(n==null)return"unparseable";let t=wt.safeParse(n.extraKnownMarketplaces??{});if(!t.success)return"unparseable";if(t.data.ripplo!=null)return"already-present";let o={...n,extraKnownMarketplaces:{...t.data,ripplo:oa}};return $e.mkdirSync(kt.dirname(r),{recursive:!0}),$e.writeFileSync(r,`${JSON.stringify(o,null,2)}
187
+ `),"written"}function ia(e){if(!$e.existsSync(e))return{};try{let r=wt.safeParse(JSON.parse($e.readFileSync(e,"utf8")));return r.success?r.data:void 0}catch{return}}function je(e){switch(e){case"written":return" registered ripplo plugin marketplace in .claude/settings.json (autoUpdate: true \u2014 Claude Code keeps the plugin current)";case"already-present":return" ripplo plugin marketplace already registered in .claude/settings.json";case"unparseable":return" ! .claude/settings.json could not be parsed \u2014 add the ripplo marketplace to extraKnownMarketplaces manually"}}var aa=sa(`
188
188
  query InitProjects {
189
189
  projects {
190
190
  id
191
191
  name
192
192
  }
193
193
  }
194
- `),lr=["../.env.local","../.env"];async function hn(e=sa()){let r=process.cwd(),t=A(),n=F(t);n==null&&(process.stdout.write("Not signed in. Run `npx ripplo auth login` first.\n"),process.exit(1)),oa(r)&&(process.stdout.write(`\`.ripplo/index.ts\` already exists at ${r}. To re-init, delete it first.
195
- `),process.exit(1));let o=await ia({serverUrl:t,token:n});o.length===0&&(process.stdout.write("No projects found. Create one in the dashboard first, then re-run `npx ripplo init`.\n"),process.exit(1));let i=await la(o,e.projectId),a=await da(r,e.envFile),s=await pa(e.appUrl),l=aa(s,e.engineUrl),d=ee.resolve(ee.join(r,".ripplo"),a);Ir({cwd:r,envFiles:[a],projectId:i}),ma({appUrl:s,engineUrl:l,filePath:d}),process.stdout.write(`${Ae(je(r))}
196
- `);let p=(await pn({cwd:r,onStep:y=>{process.stdout.write(` ${y}
194
+ `),ar=["../.env.local","../.env"];async function St(e=ca()){let r=process.cwd(),n=A(),t=_(n);t==null&&(process.stdout.write("Not signed in. Run `npx ripplo auth login` first.\n"),process.exit(1)),la(r)&&(process.stdout.write(`\`.ripplo/index.ts\` already exists at ${r}. To re-init, delete it first.
195
+ `),process.exit(1));let o=await da({serverUrl:n,token:t});o.length===0&&(process.stdout.write("No projects found. Create one in the dashboard first, then re-run `npx ripplo init`.\n"),process.exit(1));let i=await ua(o,e.projectId),a=await ma(r,e.envFile),s=await ga(e.appUrl),l=pa(s,e.engineUrl),d=Z.resolve(Z.join(r,".ripplo"),a);$r({cwd:r,envFiles:[a],projectId:i}),ya({appUrl:s,engineUrl:l,filePath:d}),process.stdout.write(`${je(Ie(r))}
196
+ `);let p=(await yt({cwd:r,onStep:y=>{process.stdout.write(` ${y}
197
197
  `)}})).match(y=>y,y=>{process.stderr.write(`${j(y)}
198
198
  `),process.exit(1)});if(p.length>0){process.stdout.write(`Done with warnings:
199
199
  `),p.forEach(y=>{process.stdout.write(` - ${y.message}
200
200
  `),y.manualCommand!=null&&process.stdout.write(` run: ${y.manualCommand}
201
- `)});return}process.stdout.write("Ready. Start `npx ripplo daemon` as a background process (or run `/ripplo:start` in Claude Code), then write workflows in `.ripplo/workflows/`.\n")}function oa(e){return Z.existsSync(ee.join(e,".ripplo","index.ts"))}async function ia({serverUrl:e,token:r}){return((await u({config:H({serverUrl:e,token:r}),document:na,variables:void 0})).projects??[]).map(n=>({id:n.id,name:n.name}))}function sa(){return{appUrl:void 0,engineUrl:void 0,envFile:void 0,projectId:void 0}}function aa(e,r){if(r!=null){try{new URL(r)}catch{process.stdout.write(`--engine-url is not a valid URL: ${r}
202
- `),process.exit(1)}return r}return`${e.replace(/\/$/,"")}/ripplo`}async function la(e,r){if(r!=null){let t=e.find(n=>n.id===r);return t==null&&(process.stdout.write(`Unknown project id: ${r}
203
- `),process.exit(1)),process.stdout.write(`Using project: ${t.name} (${t.id})
204
- `),t.id}if(e.length===1){let t=e[0];if(t==null)throw new Error("unreachable");return process.stdout.write(`Using project: ${t.name} (${t.id})
205
- `),t.id}return gn({choices:e.map(t=>({name:t.name,value:t.id})),message:"Select a project"})}async function da(e,r){return r!=null?(r.trim().length===0&&(process.stdout.write(`--env must not be empty
206
- `),process.exit(1)),r):ca(e)}async function ca(e){let r=ee.join(e,".ripplo"),t=lr.find(o=>Z.existsSync(ee.resolve(r,o))),n=await gn({choices:[...lr.map(o=>({name:t===o?`${o} (detected)`:o,value:o})),{name:"custom path",value:"__custom__"}],default:t??lr[0],message:"Which env file should ripplo write RIPPLO_APP_URL, RIPPLO_ENGINE_URL, RIPPLO_WEBHOOK_SECRET, and ENABLE_RIPPLO_TESTING to?"});return n!=="__custom__"?n:fn({message:"Path to env file (relative to .ripplo/, e.g. ../apps/server/.env)",validate:o=>o.trim().length>0?!0:"required"})}async function pa(e){if(e!=null){try{new URL(e)}catch{process.stdout.write(`--app-url is not a valid URL: ${e}
207
- `),process.exit(1)}return e}return ua()}async function ua(){return fn({default:"http://localhost:3000",message:"Where does your dev server run? (RIPPLO_APP_URL)",validate:e=>{try{return new URL(e),!0}catch{return"must be a valid URL"}}})}function ma({appUrl:e,engineUrl:r,filePath:t}){Z.mkdirSync(ee.dirname(t),{recursive:!0});let n=Z.existsSync(t)?Z.readFileSync(t,"utf8"):"",o=[];if(/^RIPPLO_APP_URL=/m.test(n)||o.push(`RIPPLO_APP_URL=${e}`),/^RIPPLO_ENGINE_URL=/m.test(n)||o.push(`RIPPLO_ENGINE_URL=${r}`),/^RIPPLO_WEBHOOK_SECRET=/m.test(n)||o.push(`RIPPLO_WEBHOOK_SECRET=${$r()}`),/^ENABLE_RIPPLO_TESTING=/m.test(n)||o.push("ENABLE_RIPPLO_TESTING=true"),o.length===0)return;let i=n.length===0||n.endsWith(`
201
+ `)});return}process.stdout.write("Ready. Start `npx ripplo daemon` as a background process (or run `/ripplo:start` in Claude Code), then write workflows in `.ripplo/workflows/`.\n")}function la(e){return Y.existsSync(Z.join(e,".ripplo","index.ts"))}async function da({serverUrl:e,token:r}){return((await u({config:M({serverUrl:e,token:r}),document:aa,variables:void 0})).projects??[]).map(t=>({id:t.id,name:t.name}))}function ca(){return{appUrl:void 0,engineUrl:void 0,envFile:void 0,projectId:void 0}}function pa(e,r){if(r!=null){try{new URL(r)}catch{process.stdout.write(`--engine-url is not a valid URL: ${r}
202
+ `),process.exit(1)}return r}return`${e.replace(/\/$/,"")}/ripplo`}async function ua(e,r){if(r!=null){let n=e.find(t=>t.id===r);return n==null&&(process.stdout.write(`Unknown project id: ${r}
203
+ `),process.exit(1)),process.stdout.write(`Using project: ${n.name} (${n.id})
204
+ `),n.id}if(e.length===1){let n=e[0];if(n==null)throw new Error("unreachable");return process.stdout.write(`Using project: ${n.name} (${n.id})
205
+ `),n.id}return bt({choices:e.map(n=>({name:n.name,value:n.id})),message:"Select a project"})}async function ma(e,r){return r!=null?(r.trim().length===0&&(process.stdout.write(`--env must not be empty
206
+ `),process.exit(1)),r):fa(e)}async function fa(e){let r=Z.join(e,".ripplo"),n=ar.find(o=>Y.existsSync(Z.resolve(r,o))),t=await bt({choices:[...ar.map(o=>({name:n===o?`${o} (detected)`:o,value:o})),{name:"custom path",value:"__custom__"}],default:n??ar[0],message:"Which env file should ripplo write RIPPLO_APP_URL, RIPPLO_ENGINE_URL, RIPPLO_WEBHOOK_SECRET, and ENABLE_RIPPLO_TESTING to?"});return t!=="__custom__"?t:vt({message:"Path to env file (relative to .ripplo/, e.g. ../apps/server/.env)",validate:o=>o.trim().length>0?!0:"required"})}async function ga(e){if(e!=null){try{new URL(e)}catch{process.stdout.write(`--app-url is not a valid URL: ${e}
207
+ `),process.exit(1)}return e}return ha()}async function ha(){return vt({default:"http://localhost:3000",message:"Where does your dev server run? (RIPPLO_APP_URL)",validate:e=>{try{return new URL(e),!0}catch{return"must be a valid URL"}}})}function ya({appUrl:e,engineUrl:r,filePath:n}){Y.mkdirSync(Z.dirname(n),{recursive:!0});let t=Y.existsSync(n)?Y.readFileSync(n,"utf8"):"",o=[];if(/^RIPPLO_APP_URL=/m.test(t)||o.push(`RIPPLO_APP_URL=${e}`),/^RIPPLO_ENGINE_URL=/m.test(t)||o.push(`RIPPLO_ENGINE_URL=${r}`),/^RIPPLO_WEBHOOK_SECRET=/m.test(t)||o.push(`RIPPLO_WEBHOOK_SECRET=${Er()}`),/^ENABLE_RIPPLO_TESTING=/m.test(t)||o.push("ENABLE_RIPPLO_TESTING=true"),o.length===0)return;let i=t.length===0||t.endsWith(`
208
208
  `)?"":`
209
- `;Z.writeFileSync(t,`${n}${i}${o.join(`
209
+ `;Y.writeFileSync(n,`${t}${i}${o.join(`
210
210
  `)}
211
- `)}function Le(e){let r=e.workflows.filter(o=>!o.stub);if(r.length===0)return[];let t=new Set(r.flatMap(o=>fa(o))),n=new Set(r.flatMap(o=>ga(o)));return[...e.entities.filter(o=>!t.has(o.name)).map(o=>({entity:o.name,kind:"entity-never-given"})),...e.entities.filter(o=>t.has(o.name)&&!n.has(o.name)).map(o=>({entity:o.name,kind:"entity-never-mutated"})),...r.flatMap(o=>o.tests.flatMap(i=>ha(i)))]}function fa(e){return[...e.world,...e.maybe].map(r=>r.entity)}function ga(e){return e.steps.flatMap(r=>r.expect.flatMap(t=>ue(t))).map(r=>r.entity)}function ha(e){let r=e.steps.flatMap(t=>t.expect.flatMap(n=>ue(n)));return ya({states:r,test:e})}function ya({states:e,test:r}){let t=e.filter(n=>n.assertion.kind==="deleted").flatMap(n=>ka({predicate:n,test:r})).map(({entity:n,field:o})=>({entity:n,field:o,kind:"unmatchable-delete-key",testSlug:r.slug,workflow:r.workflow}));return ba(t)}function ka({predicate:e,test:r}){let t=r.world.filter(n=>n.entity===e.entity);return t.length===0?[]:Object.entries(e.key).filter(([n,o])=>va(o)&&!wa({field:n,setups:t})).map(([n])=>({entity:e.entity,field:n}))}function wa({field:e,setups:r}){return r.some(t=>Object.keys(t.set).includes(e))}function va(e){return e===null||typeof e!="object"}function ba(e){return e.filter((r,t)=>e.findIndex(n=>JSON.stringify(n)===JSON.stringify(r))===t)}function ue(e){return e.kind==="state"?[e]:e.kind==="not"?ue(e.predicate):e.kind==="and"?e.predicates.flatMap(r=>ue(r)):e.kind==="when"?e.branches.flatMap(r=>r.consequence.flatMap(t=>ue(t))):[]}async function yn(){let e=process.cwd(),t=(await S(e)).match(o=>o,o=>Sa(x(o))),n=W(t);if(n.length===0){let o=Le(t);o.length>0&&process.stdout.write(`${Ee(o)}
212
- `);let i=t.workflows.reduce((a,s)=>a+s.tests.length,0);process.stdout.write(`${Yt(i)}
213
- `);return}process.stderr.write(`${q(n)}
214
- `),process.exit(1)}function Sa(e){process.stderr.write(`${e}
215
- `),process.exit(1)}import{CancellationTokenSource as sl}from"vscode-jsonrpc/node";import Aa from"path";import{randomUUID as xa}from"crypto";import kn from"path";async function Te({assign:e,config:r,headed:t,lockfile:n,report:o,session:i,signal:a}){let s=J(n),l=await Ia({assign:e,corpus:s,lockfile:n});if(l==null)return{kind:"error",reason:"base-state-arrange-failed",rows:[],trail:[]};if(l.stepRuns.length===0)return{kind:"error",reason:"empty-trail",rows:[],trail:[]};let d=$a(s,l.stepRuns),g=await tt({baseState:{name:Ye(e.baseStateTest),test:e.baseStateTest},corpus:s,lensId:e.lensId,lockfile:n,lockfileHash:e.lockfileHash,options:{baseUrl:r.appUrl,engineUrl:r.engineUrl,fixturesDir:kn.join(r.cwd,we),generate:be,headed:t,secret:r.webhookSecret,tunnelAuth:void 0},session:i,shrinkBudget:e.shrinkBudget,trail:l,now:()=>new Date().toISOString(),runIdFor:p=>`explore-${xa()}-${String(p)}`},a);return o&&await Ca({baseStateTest:e.baseStateTest,config:r,lockfileHash:e.lockfileHash,outcome:g,stepSources:Ra(d)}),ja(g,d)}function Ra(e){return e.flatMap(r=>{let t=r.label.split("#")[0]??r.label;return r.actions.map(()=>t)})}async function Ca({baseStateTest:e,config:r,lockfileHash:t,outcome:n,stepSources:o}){n.kind!=="finding"||n.captureEvents.length===0||await ot({baseState:n.finding.baseState,branchName:void 0,category:Pa(n.finding.verifierLayer),commitSha:void 0,config:r,debugDir:kn.join(r.cwd,".ripplo","debug"),devSessionId:void 0,events:n.captureEvents,lockfileHash:t,runId:n.runId,signature:n.finding.signature,spec:Ea(e,n.captureEvents),stepSources:o}).catch(i=>{$.warn({error:i},"explore finding report failed")})}function Pa(e){return e==="data-rule"?"dataRule":e==="page-rule"?"pageRule":e}function Ea(e,r){let t=r.flatMap(o=>o.kind==="step"?[o.source]:[]),n={absent:e.absent,exclusive:e.exclusive,intent:e.intent,maybe:[],name:e.name,params:e.params,singletons:e.singletons,steps:t,stub:!1,tests:[],world:e.world};return JSON.stringify(n)}function $a(e,r){return r.flatMap(t=>{let n=e[t.idx];return n==null?[]:[{actions:[...n.nav,...n.steps].map(o=>Kr(o.action)),label:`${n.test}#${String(n.index)}`}]})}async function Ia({assign:e,corpus:r,lockfile:t}){return(await rt(t,{name:Ye(e.baseStateTest),test:e.baseStateTest},{generate:be,materialize:qr(be,t.valueSpaces),params:void 0})).match(o=>et({actionHashes:r.map(i=>xe(t,i)),baseStateSnapshot:o.snapshot,corpus:r,covered:new Set,lens:at(t),lensId:e.lensId,maxLength:e.maxLength,witnessTrail:e.stepRuns}),()=>null)}function ja(e,r){return e.kind==="error"?{kind:e.kind,reason:`runtime:${e.error.kind}`,rows:[],trail:[]}:e.kind==="aborted"?{kind:e.kind,rows:[],trail:[]}:{kind:e.kind,rows:[...e.rows],trail:[...r]}}var La={covered:0,deferred:0,findings:0,saturated:!1,total:0};async function wn(e){let r=w(e.cwd).match(d=>({config:d}),d=>({failure:d}));if("failure"in r)return{failure:r.failure,kind:"config-failed"};let t=r.config,n=await Tr(e.cwd);if(n.result.isErr())return{error:n.result.error,kind:"compile-failed"};let o={fingerprint:n.fingerprint,lockfile:n.result.value},i=st({onChange:()=>{}});if(!i.holder())return i.stop(),{kind:"explorer-busy"};let a=Se({clientVersion:P(),debugDir:Aa.join(e.cwd,".ripplo","debug"),headed:e.headed,writeOtlpPortFile:!1}),s={executed:0,status:()=>La},l=lt({cwd:e.cwd,maxTrailLength:e.maxLength,executeTrail:(d,g)=>Te({assign:d,config:t,headed:e.headed,lockfile:o.lockfile,report:!0,session:a,signal:g}),loadLockfile:()=>Promise.resolve(o),notifyWork:()=>{},onTrailDone:d=>{s.executed+=1,e.onTrail(s.executed,d,s.status())},probeApp:async()=>await ae(t.appUrl)==null});s.status=()=>l.status();try{await l.ready(),e.onReady(l.status());let d=await Ta(l,e),g=l.status();return await l.stop(),await a.close(),d===0&&g.total===0?{kind:"no-work"}:{executed:d,kind:"completed",progress:g}}finally{i.stop()}}async function Ta(e,r){let t=new AbortController().signal,n=async o=>{if(o>=r.trails)return o;let i=e.next();return i==null?o:(await i.run(t),n(o+1))};return n(0)}import{okAsync as Da}from"neverthrow";function vn(e,r,t){let n=rr(e);return er(n).andThen(o=>{let i=Oa(o,r);return i==null?Da({id:r,kind:"not-found"}):Yr(n,[{at:t,kind:"resolution",signature:i}]).map(()=>({id:r,kind:"dismissed",signature:i}))})}function bn(e,r){let t=rr(e);return er(t).andThen(n=>{let o=new Set(J(r).map(d=>xe(r,d))),i=new Set(_a(n).filter(d=>!o.has(d.actionHash)).map(d=>d.signature)),a=Fa(n),s=n.filter(d=>Na(d,o,a)),l=[...a].filter(d=>!i.has(d)).length;return Zr(t,s).map(()=>({kept:s.length,removedResolved:l,removedStale:i.size}))})}function Oa(e,r){return[...Ze(e).findings.entries()].find(([n,o])=>o.resolvedAt==null&&Xr(n)===r)?.[0]}function _a(e){return e.filter(r=>r.kind==="finding")}function Fa(e){return new Set([...Ze(e).findings.entries()].filter(([,r])=>r.resolvedAt!=null).map(([r])=>r))}function Na(e,r,t){return e.kind==="resolution"?!1:e.kind==="finding"?r.has(e.actionHash)&&!t.has(e.signature):!0}function Sn(e){if(e.pending.length===0&&e.recurrentFlaky.length===0)return"explore: no pending findings";let r=e.pending.length===0?[]:[`explore: ${b(e.pending.length,"pending finding")} \u2014 triage in layer order, top first`,...e.pending.map(o=>Ua(o))],t=e.recurrentFlaky.length===0?[]:[`recurrent flaky-candidates (same failure ${String(e.recurrentFlaky.length)}x, no deterministic repro \u2014 triage after findings):`,...e.recurrentFlaky.map(o=>Wa(o))],n=c("fuzz","triaging each finding (evidence -> classify -> fix -> replay)");return[...r,...t,"","replay after a fix: npx ripplo explore replay <id>",n].join(`
216
- `).trim()}function xn(e,r){switch(r.kind){case"resolved":return`explore: ${e} replayed clean \u2014 finding resolved, its targets covered under the current workflows`;case"unreachable":return`explore: ${e}'s trail is no longer plannable under the current workflows \u2014 finding resolved (if you narrowed a given/when, make sure a test covers the excluded state)`;case"still-failing":{let t=r.runId==null?"":` (fresh evidence: run ${r.runId})`;return`explore: ${e} still reproduces \u2014 same failure signature${t}`}case"diverged":{let t=r.runId==null?"":` (captured run ${r.runId})`;return`explore: ${e} failed with a different failure signature \u2014 new finding recorded${t}`}case"flaky":return`explore: ${e} did not reproduce deterministically \u2014 recorded as flaky-candidate, finding stays pending`;case"aborted":return`explore: replay of ${e} was aborted`;case"finding-not-found":return`explore: no pending finding ${e} \u2014 check ids with: npx ripplo explore findings`;case"unreplayable":return`explore: ${e} cannot be replayed (${r.reason})`;case"error":return`explore: replay of ${e} failed (${r.reason})`}}function Rn({executed:e,progress:r,trail:t}){let n=t.trail.flatMap(o=>[` ${o.label}`,...o.actions.map(i=>` ${i}`)]);return[`trail ${String(e)} ${t.kind} \u2014 ${de(r)}`,...n,` state: ${t.label}`].join(`
217
- `)}function Cn(e){switch(e.kind){case"config-failed":return j(e.failure);case"compile-failed":return x(e.error);case"no-work":return"explore: nothing to explore \u2014 no runnable actions found in your workflows";case"explorer-busy":return"explore: another explorer holds the machine lock (likely the daemon's background explorer) \u2014 watch it with `npx ripplo status`, or stop it to explore in the foreground";case"completed":return`${`explore: ${String(e.executed)} trails executed`}
218
- ${de(e.progress)}
219
- findings land in .ripplo/.local/explore-ledger.jsonl`}}function Pn(e){let r=e.evidence.map(n=>` ${n}`),t=e.runId==null?[" no captured run \u2014 replay to capture one"]:[` run: ${e.runId}`,` behavior: .ripplo/debug/${e.runId}/behavior.jsonl`];return[`${e.id} layer=${e.verifierLayer} start=${e.baseState}`,` seen ${String(e.occurrences)}x between ${dr(e.firstSeen)} and ${dr(e.lastSeen)}`,` trail: ${e.trail.join(" -> ")}`," evidence:",...r,...t,` replay after a fix: npx ripplo explore replay ${e.id}`].join(`
220
- `)}function cr(e){return`explore: no pending finding ${e} \u2014 check ids with: npx ripplo explore findings`}function En(e){return e.kind==="not-found"?cr(e.id):`explore: dismissed finding ${e.id}. Prune the log to drop it: npx ripplo explore prune`}function $n(e){if(e.removedStale+e.removedResolved===0)return"explore: nothing to prune \u2014 the findings log has no stale or dismissed entries.";let t=b(e.removedStale,"stale finding"),n=b(e.removedResolved,"dismissed finding");return`explore: pruned ${t} and ${n}, ${b(e.kept,"row")} kept.`}function Ua(e){let r=e.runId==null?"no captured run":`run ${e.runId}`;return[` ${e.id} layer=${e.verifierLayer} seen ${String(e.occurrences)}x (last ${dr(e.lastSeen)}) start=${e.baseState} ${r}`,` mismatch: ${Ma(e.parts)}`,` trail: ${e.trail.join(" -> ")}`].join(`
221
- `)}function dr(e){return e.slice(0,16).replace("T"," ")}function Ma(e){let r=e.at(0);if(r==null)return"no parts recorded";let t=e.length>1?` (+${String(e.length-1)} more)`:"";return`${Ha(r)}${t}`}function Ha(e){switch(e.kind){case"consistency":{let r="entity"in e.mismatch?e.mismatch.entity:e.mismatch.singleton,t=e.step==null?"":` at "${e.step.intent}"`;return`${e.mismatch.kind} on ${r}${t}`}case"pending-check":return`${e.source} check failed at "${e.step.intent}"`;case"unrunnable":return`step "${e.intent}" could not run (${e.reason})`;case"driver-error":return`${e.error} at "${e.step}"`;case"impossible-action":return`impossible action at "${e.step}"`}}function Wa(e){let r=e.trail.map(t=>t.split("|").at(0)??t);return` ${e.id} seen ${String(e.occurrences)}x start=${e.baseState}
222
- trail: ${r.join(" -> ")}`}import{spawn as qa}from"child_process";import In from"fs";import Ba from"net";import{setTimeout as pr}from"timers/promises";import{err as re,ok as me}from"neverthrow";import{ResponseError as Va}from"vscode-jsonrpc/node";var An=12e4,Ln=300,jn=5e3;async function te({cliEntry:e,cwd:r}){let t=K(r),n=await G(t);return n!=null?Ga({cliEntry:e,connection:_e(n,!1),cwd:r}):Tn({cliEntry:e,cwd:r,versionNote:void 0})}async function Tn({cliEntry:e,cwd:r,versionNote:t}){let n=Ya({cliEntry:e,cwd:r});if(n!=null)return re(n);let o=await Za(K(r));return o==null?re({deadlineMs:An,kind:"connect-timeout",logPath:tr(r)}):me({..._e(o,!0),versionNote:t})}async function Ga({cliEntry:e,connection:r,cwd:t}){let n=await _n(r);if(n==null||n.version===P())return me(r);let o={daemonVersion:n.version};return await za(r)?(r.socket.destroy(),await Ka(K(t))?Tn({cliEntry:e,cwd:t,versionNote:{...o,kind:"restarted"}}):re({kind:"connection-lost"})):me({...r,versionNote:{...o,kind:"stale-busy"}})}async function za(e){try{return await e.rpc.sendRequest(Re)}catch{return!1}}var Ja=1e4;async function Ka(e){let r=Date.now()+Ja,t=await G(e);for(;t!=null&&Date.now()<r;)t.destroy(),await pr(Ln),t=await G(e);return t?.destroy(),t==null}async function De({connection:e,onEvent:r,request:t,token:n}){let o=await Qa({connection:e,onEvent:r,request:t,token:n});return o.kind==="transport"?re(o.error):me(o)}async function Dn({connection:e,findingId:r,token:t}){try{let n=await e.rpc.sendRequest(vt,{findingId:r},t),o=wt.safeParse(n);return o.success?me(o.data):re({kind:"bad-frame"})}catch{return re({kind:"connection-lost"})}}async function On(e){let r=await G(K(e));if(r==null)return!1;let t=_e(r,!1);try{return await t.rpc.sendRequest(Re)}catch{return!1}finally{r.destroy()}}async function ne(e){try{await e.rpc.sendRequest(Re)}catch{}}async function Oe(e){let r=await G(K(e));if(r==null)return{kind:"not-running"};let t=_e(r,!1),n=await Promise.race([_n(t),pr(jn).then(()=>null)]);return r.destroy(),n==null?{kind:"unresponsive",timeoutMs:jn}:{kind:"running",status:n}}async function _n(e){try{let r=await e.rpc.sendRequest(St),t=gt.safeParse(r);return t.success?t.data:null}catch{return null}}function Qa({connection:e,onEvent:r,request:t,token:n}){let{rpc:o}=e;return new Promise(i=>{let a=s=>{i(s)};o.onNotification(xt,s=>{let l=ht.safeParse(s);if(!l.success){a({error:{kind:"bad-frame"},kind:"transport"});return}r(l.data.event)}),o.onNotification(Rt,s=>{let l=yt.safeParse(s);if(!l.success){a({error:{kind:"bad-frame"},kind:"transport"});return}a({failed:l.data.failed,kind:"done",notRun:l.data.notRun,passed:l.data.passed})}),o.onClose(()=>{a({error:{kind:"connection-lost"},kind:"transport"})}),o.sendRequest(bt,t,n).then(s=>{ft.safeParse(s).success||a({error:{kind:"bad-frame"},kind:"transport"})}).catch(s=>{a(Xa(s))})})}function Xa(e){if(e instanceof Va){let r=kt.safeParse(e.data);return r.success?{error:r.data,kind:"daemon-error"}:{error:{kind:"bad-frame"},kind:"transport"}}return{error:{kind:"connection-lost"},kind:"transport"}}function _e(e,r){let t=mt(e);return t.listen(),{rpc:t,socket:e,spawned:r,versionNote:void 0}}function G(e){return new Promise(r=>{let t=Ba.connect(e);t.once("connect",()=>{r(t)}),t.once("error",()=>{r(null)})})}function Ya({cliEntry:e,cwd:r}){try{ke(r);let t=In.openSync(tr(r),"a");return qa(process.execPath,[e,"daemon"],{cwd:r,detached:!0,stdio:["ignore",t,t]}).unref(),In.closeSync(t),null}catch(t){return{kind:"spawn-failed",message:t instanceof Error?t.message:String(t)}}}async function Za(e){let r=Date.now()+An,t=await G(e);for(;t==null&&Date.now()<r;)await pr(Ln),t=await G(e);return t}import rl from"fuse.js";var el=3e3;async function Fe(e){V(e);let r=w(e).unwrapOr(void 0);if(r==null)return null;try{return await fetch(`${r.ripploServerUrl}/health`,{signal:AbortSignal.timeout(el)}),null}catch(t){return{detail:t instanceof Error?t.message:String(t),serverUrl:r.ripploServerUrl}}}function Ne(e){return e.includes("localhost")||e.includes("127.0.0.1")}function ur(e){switch(e.kind){case"conflicting-flags":return"Pass either --all or test ids, not both.";case"nothing-selected":return`No tests selected \u2014 scope is empty and no .ripplo/workflows files are dirty. Pass test ids, add workflows to scope (${c("run")}), or use --all.`;case"unknown-ids":{let r=e.known,t=e.unknown.flatMap(o=>il(o,r)),n=t.length>0?[`Did you mean: ${[...new Set(t)].join(", ")}`]:[];return[`Unknown ${e.unknown.length===1?"id":"ids"}: ${e.unknown.join(", ")}`,...n,"Pass a workflow slug to run all its tests, or workflow/test for one. Known workflows:",...r.map(o=>` ${o}`)].join(`
223
- `)}}}function Fn({failed:e,notRun:r,passed:t}){let n=r>0?`, ${String(r)} not run`:"",o=e>0?`
224
- ${c("run")}`:"";return`${String(t)} passed, ${String(e)} failed${n} (${String(t+e+r)} total)${o}`}function Ue({debugDir:e,event:r}){let t=`${r.workflowName} \u2192 ${r.testName}`;if(r.kind==="test-started")return`${C.dim("run ")} ${t}`;switch(r.outcome.kind){case"pass":return`${C.good("pass")} ${t}`;case"findings":return[`${C.bad("fail")} ${t} \u2014 ${b(r.outcome.findingLines.length,"finding")}`,...r.outcome.findingLines,zr({debugDir:e,runId:r.runId})].join(`
225
- `);case"error":return`${C.bad("error")} ${t} \u2014 ${r.outcome.detail}`;case"dispatch-error":return`${C.bad("error")} ${t} \u2014 ${ol(r.outcome.reason)}`;case"dispatch-timeout":return`${C.bad("error")} ${t} \u2014 not run: dispatch timed out (transient): ${r.outcome.detail}`;case"infra-error":return`${C.bad("error")} ${t} \u2014 not run: Ripplo server unreachable (server-side, not local): ${r.outcome.detail}`}}var tl={MAX_AGENTS:"agent limit reached for your plan \u2014 upgrade to add more",MAX_CONCURRENT_RUNS:"concurrent run limit reached for your plan \u2014 upgrade or wait for runs to finish",MAX_PROJECTS:"project limit reached for your plan \u2014 upgrade to add more",MAX_RUNS:"run limit reached for your plan \u2014 enable overage or upgrade",OVERAGE_CAP:"overage cap reached \u2014 raise the cap or upgrade"};function Me({detail:e,serverUrl:r}){return Ne(r)?`Ripplo server at ${r} is not running (${e}). Tests were not started. Start the dev server (\`pnpm dev\`) and re-run.`:`Ripplo server at ${r} is unreachable (${e}). Tests were not started. This is a server-side issue, not your local environment \u2014 wait a moment and re-run.`}function He(e){switch(e.code){case"compile-failed":{let r=e.diagnostics.length===0?[]:["",...e.diagnostics];return[`Compilation failed in the daemon (${e.detail}). Run \`npx ripplo compile\` for the full output.`,...r].join(`
226
- `)}case"selection-conflicting-flags":return ur({kind:"conflicting-flags"});case"selection-nothing-selected":return ur({kind:"nothing-selected"});case"selection-unknown-ids":return ur({kind:"unknown-ids",known:e.known,unknown:e.unknown});case"app-unreachable":return`Your dev server is not responding at ${e.url} (${e.detail}). Tests were not started \u2014 start your app and re-run. This is your local environment, not the Ripplo server.`;case"scope-failed":return`Could not resolve the dev-session scope: ${e.detail}
227
- Verify the dev session is live (\`npx ripplo doctor\`, ${c("start")}), or pass test ids / --all explicitly.`;case"sync-failed":{let r=/401|unauthor/i.test(e.detail)?"\nLooks like an auth failure \u2014 run `npx ripplo auth login` and retry.":"";return`Sync to the Ripplo server failed: ${e.detail}${r}`}case"bad-message":return"Daemon rejected the request (protocol mismatch \u2014 rebuild/update the CLI and restart the daemon)."}}function D(e){switch(e.kind){case"spawn-failed":return`Failed to start \`npx ripplo daemon\`: ${nl(e.message)}`;case"connect-timeout":return[`Daemon did not come up within ${String(Math.round(e.deadlineMs/1e3))}s.`,`Check ${e.logPath} for startup errors.`,"Common causes: a stale socket (`rm .ripplo/.local/daemon.sock`), another daemon holding the dev lock, or the Ripplo server unreachable."].join(`
211
+ `)}function Ae(e){let r=e.workflows.filter(o=>!o.stub);if(r.length===0)return[];let n=new Set(r.flatMap(o=>ka(o))),t=new Set(r.flatMap(o=>wa(o)));return[...e.entities.filter(o=>!n.has(o.name)).map(o=>({entity:o.name,kind:"entity-never-given"})),...e.entities.filter(o=>n.has(o.name)&&!t.has(o.name)).map(o=>({entity:o.name,kind:"entity-never-mutated"})),...r.flatMap(o=>o.tests.flatMap(i=>va(i)))]}function ka(e){return[...e.world,...e.maybe].map(r=>r.entity)}function wa(e){return e.steps.flatMap(r=>r.expect.flatMap(n=>pe(n))).map(r=>r.entity)}function va(e){let r=e.steps.flatMap(n=>n.expect.flatMap(t=>pe(t)));return ba({states:r,test:e})}function ba({states:e,test:r}){let n=e.filter(t=>t.assertion.kind==="deleted").flatMap(t=>Sa({predicate:t,test:r})).map(({entity:t,field:o})=>({entity:t,field:o,kind:"unmatchable-delete-key",testSlug:r.slug,workflow:r.workflow}));return Ca(n)}function Sa({predicate:e,test:r}){let n=r.world.filter(t=>t.entity===e.entity);return n.length===0?[]:Object.entries(e.key).filter(([t,o])=>Ra(o)&&!xa({field:t,setups:n})).map(([t])=>({entity:e.entity,field:t}))}function xa({field:e,setups:r}){return r.some(n=>Object.keys(n.set).includes(e))}function Ra(e){return e===null||typeof e!="object"}function Ca(e){return e.filter((r,n)=>e.findIndex(t=>JSON.stringify(t)===JSON.stringify(r))===n)}function pe(e){return e.kind==="state"?[e]:e.kind==="not"?pe(e.predicate):e.kind==="and"?e.predicates.flatMap(r=>pe(r)):e.kind==="when"?e.branches.flatMap(r=>r.consequence.flatMap(n=>pe(n))):[]}async function xt(){let e=process.cwd(),n=(await S(e)).match(o=>o,o=>Pa(x(o))),t=H(n);if(t.length===0){let o=Ae(n);o.length>0&&process.stdout.write(`${Pe(o)}
212
+ `);let i=n.workflows.reduce((a,s)=>a+s.tests.length,0);process.stdout.write(`${it(i)}
213
+ `);return}process.stderr.write(`${W(t)}
214
+ `),process.exit(1)}function Pa(e){process.stderr.write(`${e}
215
+ `),process.exit(1)}import{CancellationTokenSource as cl}from"vscode-jsonrpc/node";import Oa from"path";import{randomUUID as Ea}from"crypto";import Rt from"path";async function Le({assign:e,config:r,headed:n,lockfile:t,report:o,session:i,signal:a}){let s=z(t),l=await Ta({assign:e,corpus:s,lockfile:t});if(l==null)return{kind:"error",reason:"base-state-arrange-failed",rows:[],trail:[]};if(l.stepRuns.length===0)return{kind:"error",reason:"empty-trail",rows:[],trail:[]};let d=La(s,l.stepRuns),g=await dn({baseState:{name:Xe(e.baseStateTest),test:e.baseStateTest},corpus:s,lensId:e.lensId,lockfile:t,lockfileHash:e.lockfileHash,options:{baseUrl:r.appUrl,engineUrl:r.engineUrl,fixturesDir:Rt.join(r.cwd,ke),generate:ve,headed:n,secret:r.webhookSecret,tunnelAuth:void 0},session:i,shrinkBudget:e.shrinkBudget,trail:l,now:()=>new Date().toISOString(),runIdFor:p=>`explore-${Ea()}-${String(p)}`},a);return o&&await Ia({baseStateTest:e.baseStateTest,config:r,lockfileHash:e.lockfileHash,outcome:g,stepSources:$a(d)}),Da(g,d)}function $a(e){return e.flatMap(r=>{let n=r.label.split("#")[0]??r.label;return r.actions.map(()=>n)})}async function Ia({baseStateTest:e,config:r,lockfileHash:n,outcome:t,stepSources:o}){t.kind!=="finding"||t.captureEvents.length===0||await pn({baseState:t.finding.baseState,branchName:void 0,category:ja(t.finding.verifierLayer),commitSha:void 0,config:r,debugDir:Rt.join(r.cwd,".ripplo","debug"),devSessionId:void 0,events:t.captureEvents,lockfileHash:n,runId:t.runId,signature:t.finding.signature,spec:Aa(e,t.captureEvents),stepSources:o}).catch(i=>{$.warn({error:i},"explore finding report failed")})}function ja(e){return e==="data-rule"?"dataRule":e==="page-rule"?"pageRule":e}function Aa(e,r){let n=r.flatMap(o=>o.kind==="step"?[o.source]:[]),t={absent:e.absent,exclusive:e.exclusive,intent:e.intent,maybe:[],name:e.name,params:e.params,singletons:e.singletons,steps:n,stub:!1,tests:[],world:e.world};return JSON.stringify(t)}function La(e,r){return r.flatMap(n=>{let t=e[n.idx];return t==null?[]:[{actions:[...t.nav,...t.steps].map(o=>Jr(o.action)),label:`${t.test}#${String(t.index)}`}]})}async function Ta({assign:e,corpus:r,lockfile:n}){return(await ln(n,{name:Xe(e.baseStateTest),test:e.baseStateTest},{generate:ve,materialize:Wr(ve,n.valueSpaces),params:void 0})).match(o=>an({actionHashes:r.map(i=>Se(n,i)),baseStateSnapshot:o.snapshot,corpus:r,covered:new Set,lens:fn(n),lensId:e.lensId,maxLength:e.maxLength,witnessTrail:e.stepRuns}),()=>null)}function Da(e,r){return e.kind==="error"?{kind:e.kind,reason:`runtime:${e.error.kind}`,rows:[],trail:[]}:e.kind==="aborted"?{kind:e.kind,rows:[],trail:[]}:{kind:e.kind,rows:[...e.rows],trail:[...r]}}var _a={covered:0,deferred:0,findings:0,saturated:!1,total:0};async function Ct(e){let r=w(e.cwd).match(d=>({config:d}),d=>({failure:d}));if("failure"in r)return{failure:r.failure,kind:"config-failed"};let n=r.config,t=await Lr(e.cwd);if(t.result.isErr())return{error:t.result.error,kind:"compile-failed"};let o={fingerprint:t.fingerprint,lockfile:t.result.value},i=mn({onChange:()=>{}});if(!i.holder())return i.stop(),{kind:"explorer-busy"};let a=be({clientVersion:P(),debugDir:Oa.join(e.cwd,".ripplo","debug"),headed:e.headed,writeOtlpPortFile:!1}),s={executed:0,status:()=>_a},l=gn({cwd:e.cwd,maxTrailLength:e.maxLength,executeTrail:(d,g)=>Le({assign:d,config:n,headed:e.headed,lockfile:o.lockfile,report:!0,session:a,signal:g}),loadLockfile:()=>Promise.resolve(o),notifyWork:()=>{},onTrailDone:d=>{s.executed+=1,e.onTrail(s.executed,d,s.status())},probeApp:async()=>await se(n.appUrl)==null});s.status=()=>l.status();try{await l.ready(),e.onReady(l.status());let d=await Fa(l,e),g=l.status();return await l.stop(),await a.close(),d===0&&g.total===0?{kind:"no-work"}:{executed:d,kind:"completed",progress:g}}finally{i.stop()}}async function Fa(e,r){let n=new AbortController().signal,t=async o=>{if(o>=r.trails)return o;let i=e.next();return i==null?o:(await i.run(n),t(o+1))};return t(0)}import{okAsync as Na}from"neverthrow";function Pt(e,r,n){let t=er(e);return Ze(t).andThen(o=>{let i=Ua(o,r);return i==null?Na({id:r,kind:"not-found"}):on(t,[{at:n,kind:"resolution",signature:i}]).map(()=>({id:r,kind:"dismissed",signature:i}))})}function Et(e,r){let n=er(e);return Ze(n).andThen(t=>{let o=new Set(z(r).map(d=>Se(r,d))),i=new Set(Ma(t).filter(d=>!o.has(d.actionHash)).map(d=>d.signature)),a=Ha(t),s=t.filter(d=>Wa(d,o,a)),l=[...a].filter(d=>!i.has(d)).length;return sn(n,s).map(()=>({kept:s.length,removedResolved:l,removedStale:i.size}))})}function Ua(e,r){return[...Ye(e).findings.entries()].find(([t,o])=>o.resolvedAt==null&&tn(t)===r)?.[0]}function Ma(e){return e.filter(r=>r.kind==="finding")}function Ha(e){return new Set([...Ye(e).findings.entries()].filter(([,r])=>r.resolvedAt!=null).map(([r])=>r))}function Wa(e,r,n){return e.kind==="resolution"?!1:e.kind==="finding"?r.has(e.actionHash)&&!n.has(e.signature):!0}function $t(e){if(e.pending.length===0&&e.recurrentFlaky.length===0)return"explore: no pending findings";let r=e.pending.length===0?[]:[`explore: ${b(e.pending.length,"pending finding")} \u2014 triage in layer order, top first`,...e.pending.map(o=>qa(o))],n=e.recurrentFlaky.length===0?[]:[`recurrent flaky-candidates (same failure ${String(e.recurrentFlaky.length)}x, no deterministic repro \u2014 triage after findings):`,...e.recurrentFlaky.map(o=>Ga(o))],t=c("fuzz","triaging each finding (evidence -> classify -> fix -> replay)");return[...r,...n,"","replay after a fix: npx ripplo explore replay <id>",t].join(`
216
+ `).trim()}function It(e,r){switch(r.kind){case"resolved":return`explore: ${e} replayed clean \u2014 finding resolved, its targets covered under the current workflows`;case"unreachable":return`explore: ${e}'s trail is no longer plannable under the current workflows \u2014 finding resolved (if you narrowed a given/when, make sure a test covers the excluded state)`;case"still-failing":{let n=r.runId==null?"":` (fresh evidence: run ${r.runId})`;return`explore: ${e} still reproduces \u2014 same failure signature${n}`}case"diverged":{let n=r.runId==null?"":` (captured run ${r.runId})`;return`explore: ${e} failed with a different failure signature \u2014 new finding recorded${n}`}case"flaky":return`explore: ${e} did not reproduce deterministically \u2014 recorded as flaky-candidate, finding stays pending`;case"aborted":return`explore: replay of ${e} was aborted`;case"finding-not-found":return`explore: no pending finding ${e} \u2014 check ids with: npx ripplo explore findings`;case"unreplayable":return`explore: ${e} cannot be replayed (${r.reason})`;case"error":return`explore: replay of ${e} failed (${r.reason})`}}function jt({executed:e,progress:r,trail:n}){let t=n.trail.flatMap(o=>[` ${o.label}`,...o.actions.map(i=>` ${i}`)]);return[`trail ${String(e)} ${n.kind} \u2014 ${le(r)}`,...t,` state: ${n.label}`].join(`
217
+ `)}function At(e){switch(e.kind){case"config-failed":return j(e.failure);case"compile-failed":return x(e.error);case"no-work":return"explore: nothing to explore \u2014 no runnable actions found in your workflows";case"explorer-busy":return"explore: another explorer holds the machine lock (likely the daemon's background explorer) \u2014 watch it with `npx ripplo status`, or stop it to explore in the foreground";case"completed":return`${`explore: ${String(e.executed)} trails executed`}
218
+ ${le(e.progress)}
219
+ findings land in .ripplo/.local/explore-ledger.jsonl`}}function Lt(e){let r=e.evidence.map(t=>` ${t}`),n=e.runId==null?[" no captured run \u2014 replay to capture one"]:[` run: ${e.runId}`,` behavior: .ripplo/debug/${e.runId}/behavior.jsonl`];return[`${e.id} layer=${e.verifierLayer} start=${e.baseState}`,` seen ${String(e.occurrences)}x between ${lr(e.firstSeen)} and ${lr(e.lastSeen)}`,` trail: ${e.trail.join(" -> ")}`," evidence:",...r,...n,` replay after a fix: npx ripplo explore replay ${e.id}`].join(`
220
+ `)}function dr(e){return`explore: no pending finding ${e} \u2014 check ids with: npx ripplo explore findings`}function Tt(e){return e.kind==="not-found"?dr(e.id):`explore: dismissed finding ${e.id}. Prune the log to drop it: npx ripplo explore prune`}function Dt(e){if(e.removedStale+e.removedResolved===0)return"explore: nothing to prune \u2014 the findings log has no stale or dismissed entries.";let n=b(e.removedStale,"stale finding"),t=b(e.removedResolved,"dismissed finding");return`explore: pruned ${n} and ${t}, ${b(e.kept,"row")} kept.`}function qa(e){let r=e.runId==null?"no captured run":`run ${e.runId}`;return[` ${e.id} layer=${e.verifierLayer} seen ${String(e.occurrences)}x (last ${lr(e.lastSeen)}) start=${e.baseState} ${r}`,` mismatch: ${Ba(e.parts)}`,` trail: ${e.trail.join(" -> ")}`].join(`
221
+ `)}function lr(e){return e.slice(0,16).replace("T"," ")}function Ba(e){let r=e.at(0);if(r==null)return"no parts recorded";let n=e.length>1?` (+${String(e.length-1)} more)`:"";return`${Va(r)}${n}`}function Va(e){switch(e.kind){case"consistency":{let r="entity"in e.mismatch?e.mismatch.entity:e.mismatch.singleton,n=e.step==null?"":` at "${e.step.intent}"`;return`${e.mismatch.kind} on ${r}${n}`}case"pending-check":return`${e.source} check failed at "${e.step.intent}"`;case"unrunnable":return`step "${e.intent}" could not run (${e.reason})`;case"driver-error":return`${e.error} at "${e.step}"`;case"impossible-action":return`impossible action at "${e.step}"`}}function Ga(e){let r=e.trail.map(n=>n.split("|").at(0)??n);return` ${e.id} seen ${String(e.occurrences)}x start=${e.baseState}
222
+ trail: ${r.join(" -> ")}`}import{spawn as za}from"child_process";import Ot from"fs";import Ja from"net";import{setTimeout as cr}from"timers/promises";import{err as ee,ok as ue}from"neverthrow";import{ResponseError as Ka}from"vscode-jsonrpc/node";var Ft=12e4,Nt=300,_t=5e3;async function re({cliEntry:e,cwd:r}){let n=J(r),t=await V(n);return t!=null?Qa({cliEntry:e,connection:Oe(t,!1),cwd:r}):Ut({cliEntry:e,cwd:r,versionNote:void 0})}async function Ut({cliEntry:e,cwd:r,versionNote:n}){let t=nl({cliEntry:e,cwd:r});if(t!=null)return ee(t);let o=await tl(J(r));return o==null?ee({deadlineMs:Ft,kind:"connect-timeout",logPath:rr(r)}):ue({...Oe(o,!0),versionNote:n})}async function Qa({cliEntry:e,connection:r,cwd:n}){let t=await Wt(r);if(t==null||t.version===P())return ue(r);let o={daemonVersion:t.version};return await Xa(r)?(r.socket.destroy(),await Za(J(n))?Ut({cliEntry:e,cwd:n,versionNote:{...o,kind:"restarted"}}):ee({kind:"connection-lost"})):ue({...r,versionNote:{...o,kind:"stale-busy"}})}async function Xa(e){try{return await e.rpc.sendRequest(xe)}catch{return!1}}var Ya=1e4;async function Za(e){let r=Date.now()+Ya,n=await V(e);for(;n!=null&&Date.now()<r;)n.destroy(),await cr(Nt),n=await V(e);return n?.destroy(),n==null}async function Te({connection:e,onEvent:r,request:n,token:t}){let o=await el({connection:e,onEvent:r,request:n,token:t});return o.kind==="transport"?ee(o.error):ue(o)}async function Mt({connection:e,findingId:r,token:n}){try{let t=await e.rpc.sendRequest(En,{findingId:r},n),o=Pn.safeParse(t);return o.success?ue(o.data):ee({kind:"bad-frame"})}catch{return ee({kind:"connection-lost"})}}async function Ht(e){let r=await V(J(e));if(r==null)return!1;let n=Oe(r,!1);try{return await n.rpc.sendRequest(xe)}catch{return!1}finally{r.destroy()}}async function ne(e){try{await e.rpc.sendRequest(xe)}catch{}}async function De(e){let r=await V(J(e));if(r==null)return{kind:"not-running"};let n=Oe(r,!1),t=await Promise.race([Wt(n),cr(_t).then(()=>null)]);return r.destroy(),t==null?{kind:"unresponsive",timeoutMs:_t}:{kind:"running",status:t}}async function Wt(e){try{let r=await e.rpc.sendRequest(In),n=Sn.safeParse(r);return n.success?n.data:null}catch{return null}}function el({connection:e,onEvent:r,request:n,token:t}){let{rpc:o}=e;return new Promise(i=>{let a=s=>{i(s)};o.onNotification(jn,s=>{let l=xn.safeParse(s);if(!l.success){a({error:{kind:"bad-frame"},kind:"transport"});return}r(l.data.event)}),o.onNotification(An,s=>{let l=Rn.safeParse(s);if(!l.success){a({error:{kind:"bad-frame"},kind:"transport"});return}a({failed:l.data.failed,kind:"done",notRun:l.data.notRun,passed:l.data.passed})}),o.onClose(()=>{a({error:{kind:"connection-lost"},kind:"transport"})}),o.sendRequest($n,n,t).then(s=>{bn.safeParse(s).success||a({error:{kind:"bad-frame"},kind:"transport"})}).catch(s=>{a(rl(s))})})}function rl(e){if(e instanceof Ka){let r=Cn.safeParse(e.data);return r.success?{error:r.data,kind:"daemon-error"}:{error:{kind:"bad-frame"},kind:"transport"}}return{error:{kind:"connection-lost"},kind:"transport"}}function Oe(e,r){let n=vn(e);return n.listen(),{rpc:n,socket:e,spawned:r,versionNote:void 0}}function V(e){return new Promise(r=>{let n=Ja.connect(e);n.once("connect",()=>{r(n)}),n.once("error",()=>{r(null)})})}function nl({cliEntry:e,cwd:r}){try{ye(r);let n=Ot.openSync(rr(r),"a");return za(process.execPath,[e,"daemon"],{cwd:r,detached:!0,stdio:["ignore",n,n]}).unref(),Ot.closeSync(n),null}catch(n){return{kind:"spawn-failed",message:n instanceof Error?n.message:String(n)}}}async function tl(e){let r=Date.now()+Ft,n=await V(e);for(;n==null&&Date.now()<r;)await cr(Nt),n=await V(e);return n}import il from"fuse.js";var ol=3e3;async function _e(e){B(e);let r=w(e).unwrapOr(void 0);if(r==null)return null;try{return await fetch(`${r.ripploServerUrl}/health`,{signal:AbortSignal.timeout(ol)}),null}catch(n){return{detail:n instanceof Error?n.message:String(n),serverUrl:r.ripploServerUrl}}}function Fe(e){return e.includes("localhost")||e.includes("127.0.0.1")}function pr(e){switch(e.kind){case"conflicting-flags":return"Pass either --all or test ids, not both.";case"nothing-selected":return`No tests selected \u2014 scope is empty and no .ripplo/workflows files are dirty. Pass test ids, add workflows to scope (${c("run")}), or use --all.`;case"unknown-ids":{let r=e.known,n=e.unknown.flatMap(o=>dl(o,r)),t=n.length>0?[`Did you mean: ${[...new Set(n)].join(", ")}`]:[];return[`Unknown ${e.unknown.length===1?"id":"ids"}: ${e.unknown.join(", ")}`,...t,"Pass a workflow slug to run all its tests, or workflow/test for one. Known workflows:",...r.map(o=>` ${o}`)].join(`
223
+ `)}}}function qt({failed:e,notRun:r,passed:n}){let t=r>0?`, ${String(r)} not run`:"",o=e>0?`
224
+ ${c("run")}`:"";return`${String(n)} passed, ${String(e)} failed${t} (${String(n+e+r)} total)${o}`}function Ne({debugDir:e,event:r}){let n=`${r.workflowName} \u2192 ${r.testName}`;if(r.kind==="test-started")return`${C.dim("run ")} ${n}`;switch(r.outcome.kind){case"pass":return`${C.good("pass")} ${n}`;case"findings":return[`${C.bad("fail")} ${n} \u2014 ${b(r.outcome.findingLines.length,"finding")}`,...r.outcome.findingLines,Gr({debugDir:e,runId:r.runId})].join(`
225
+ `);case"error":return`${C.bad("error")} ${n} \u2014 ${r.outcome.detail}`;case"dispatch-error":return`${C.bad("error")} ${n} \u2014 ${ll(r.outcome.reason)}`;case"dispatch-timeout":return`${C.bad("error")} ${n} \u2014 not run: dispatch timed out (transient): ${r.outcome.detail}`;case"infra-error":return`${C.bad("error")} ${n} \u2014 not run: Ripplo server unreachable (server-side, not local): ${r.outcome.detail}`}}var sl={MAX_AGENTS:"agent limit reached for your plan \u2014 upgrade to add more",MAX_CONCURRENT_RUNS:"concurrent run limit reached for your plan \u2014 upgrade or wait for runs to finish",MAX_PROJECTS:"project limit reached for your plan \u2014 upgrade to add more",MAX_RUNS:"run limit reached for your plan \u2014 enable overage or upgrade",OVERAGE_CAP:"overage cap reached \u2014 raise the cap or upgrade"};function Ue({detail:e,serverUrl:r}){return Fe(r)?`Ripplo server at ${r} is not running (${e}). Tests were not started. Start the dev server (\`pnpm dev\`) and re-run.`:`Ripplo server at ${r} is unreachable (${e}). Tests were not started. This is a server-side issue, not your local environment \u2014 wait a moment and re-run.`}function Me(e){switch(e.code){case"compile-failed":{let r=e.diagnostics.length===0?[]:["",...e.diagnostics];return[`Compilation failed in the daemon (${e.detail}). Run \`npx ripplo compile\` for the full output.`,...r].join(`
226
+ `)}case"selection-conflicting-flags":return pr({kind:"conflicting-flags"});case"selection-nothing-selected":return pr({kind:"nothing-selected"});case"selection-unknown-ids":return pr({kind:"unknown-ids",known:e.known,unknown:e.unknown});case"app-unreachable":return`Your dev server is not responding at ${e.url} (${e.detail}). Tests were not started \u2014 start your app and re-run. This is your local environment, not the Ripplo server.`;case"scope-failed":return`Could not resolve the dev-session scope: ${e.detail}
227
+ Verify the dev session is live (\`npx ripplo doctor\`, ${c("start")}), or pass test ids / --all explicitly.`;case"sync-failed":{let r=/401|unauthor/i.test(e.detail)?"\nLooks like an auth failure \u2014 run `npx ripplo auth login` and retry.":"";return`Sync to the Ripplo server failed: ${e.detail}${r}`}case"bad-message":return"Daemon rejected the request (protocol mismatch \u2014 rebuild/update the CLI and restart the daemon)."}}function D(e){switch(e.kind){case"spawn-failed":return`Failed to start \`npx ripplo daemon\`: ${al(e.message)}`;case"connect-timeout":return[`Daemon did not come up within ${String(Math.round(e.deadlineMs/1e3))}s.`,`Check ${e.logPath} for startup errors.`,"Common causes: a stale socket (`rm .ripplo/.local/daemon.sock`), another daemon holding the dev lock, or the Ripplo server unreachable."].join(`
228
228
  `);case"connection-lost":return["Lost the daemon connection mid-run \u2014 it likely crashed or was killed.","Check .ripplo/.local/daemon.log, then rerun (`npx ripplo run` auto-starts the daemon)."].join(`
229
- `);case"bad-frame":return"Received a malformed frame from the daemon (version mismatch \u2014 rebuild/update the CLI and restart the daemon)."}}function nl(e){return e.includes("ENOENT")?`${e} \u2014 the node executable or CLI entry was not found on PATH.`:e.includes("EACCES")?`${e} \u2014 permission denied executing the CLI entry.`:e}function ol(e){return e.type==="limit"?tl[e.code]:`failed to dispatch (${e.detail})`}function il(e,r){return new rl(r,{includeScore:!0,threshold:.5}).search(L(e)).slice(0,3).map(n=>n.item)}async function Nn({findingId:e,json:r}){await dt(process.cwd()).match(t=>{if(e==null){let i=r?JSON.stringify(t,null,2):Sn(t);process.stdout.write(`${i}
230
- `);return}let n=t.pending.find(i=>i.id===e);n==null&&(process.stderr.write(`${cr(e)}
231
- `),process.exit(1));let o=r?JSON.stringify(n,null,2):Pn(n);process.stdout.write(`${o}
232
- `)},t=>{process.stderr.write(`explore: findings log unreadable (${t.kind})
233
- `),process.exit(1)})}async function Un({findingId:e}){let r=process.argv[1];r==null&&(process.stderr.write(`${D({kind:"spawn-failed",message:"process.argv[1] missing"})}
234
- `),process.exit(1));let t=await te({cliEntry:r,cwd:process.cwd()});t.isErr()&&(process.stderr.write(`${D(t.error)}
235
- `),process.exit(1));let n=t.value,o=new sl;process.once("SIGINT",()=>{o.cancel(),n.socket.destroy(),process.exit(130)});let a=await(await Dn({connection:n,findingId:e,token:o.token})).match(async s=>(process.stdout.write(`${xn(e,s)}
236
- `),n.spawned&&await ne(n),s.kind==="resolved"||s.kind==="unreachable"?0:1),s=>(process.stderr.write(`${D(s)}
237
- `),Promise.resolve(1)));n.socket.destroy(),process.exit(a)}async function Mn({findingId:e}){let r=new Date().toISOString();await vn(process.cwd(),e,r).match(async t=>{t.kind==="dismissed"&&await al(t.signature),process.stdout.write(`${En(t)}
238
- `),process.exit(t.kind==="dismissed"?0:1)},t=>{process.stderr.write(`explore: findings log unreadable (${t.kind})
239
- `),process.exit(1)})}async function Hn(){let e=await S(process.cwd());e.isErr()&&(process.stderr.write(`${x(e.error)}
240
- `),process.exit(1)),await bn(process.cwd(),e.value).match(r=>{process.stdout.write(`${$n(r)}
229
+ `);case"bad-frame":return"Received a malformed frame from the daemon (version mismatch \u2014 rebuild/update the CLI and restart the daemon)."}}function al(e){return e.includes("ENOENT")?`${e} \u2014 the node executable or CLI entry was not found on PATH.`:e.includes("EACCES")?`${e} \u2014 permission denied executing the CLI entry.`:e}function ll(e){return e.type==="limit"?sl[e.code]:`failed to dispatch (${e.detail})`}function dl(e,r){return new il(r,{includeScore:!0,threshold:.5}).search(L(e)).slice(0,3).map(t=>t.item)}async function Bt({findingId:e,json:r}){await hn(process.cwd()).match(n=>{if(e==null){let i=r?JSON.stringify(n,null,2):$t(n);process.stdout.write(`${i}
230
+ `);return}let t=n.pending.find(i=>i.id===e);t==null&&(process.stderr.write(`${dr(e)}
231
+ `),process.exit(1));let o=r?JSON.stringify(t,null,2):Lt(t);process.stdout.write(`${o}
232
+ `)},n=>{process.stderr.write(`explore: findings log unreadable (${n.kind})
233
+ `),process.exit(1)})}async function Vt({findingId:e}){let r=process.argv[1];r==null&&(process.stderr.write(`${D({kind:"spawn-failed",message:"process.argv[1] missing"})}
234
+ `),process.exit(1));let n=await re({cliEntry:r,cwd:process.cwd()});n.isErr()&&(process.stderr.write(`${D(n.error)}
235
+ `),process.exit(1));let t=n.value,o=new cl;process.once("SIGINT",()=>{o.cancel(),t.socket.destroy(),process.exit(130)});let a=await(await Mt({connection:t,findingId:e,token:o.token})).match(async s=>(process.stdout.write(`${It(e,s)}
236
+ `),t.spawned&&await ne(t),s.kind==="resolved"||s.kind==="unreachable"?0:1),s=>(process.stderr.write(`${D(s)}
237
+ `),Promise.resolve(1)));t.socket.destroy(),process.exit(a)}async function Gt({findingId:e}){let r=new Date().toISOString();await Pt(process.cwd(),e,r).match(async n=>{n.kind==="dismissed"&&await pl(n.signature),process.stdout.write(`${Tt(n)}
238
+ `),process.exit(n.kind==="dismissed"?0:1)},n=>{process.stderr.write(`explore: findings log unreadable (${n.kind})
239
+ `),process.exit(1)})}async function zt(){let e=await S(process.cwd());e.isErr()&&(process.stderr.write(`${x(e.error)}
240
+ `),process.exit(1)),await Et(process.cwd(),e.value).match(r=>{process.stdout.write(`${Dt(r)}
241
241
  `)},r=>{process.stderr.write(`explore: findings log unreadable (${r.kind})
242
- `),process.exit(1)})}async function al(e){await w(process.cwd()).match(r=>it(r,e,"dismissed").catch(t=>{$.warn({error:t},"explore finding status push failed")}),r=>{$.warn({failure:r},"explore finding dismiss not synced: config unavailable")})}async function Wn(e){let r=await wn({cwd:process.cwd(),headed:e.headed,maxLength:e.maxLength,trails:e.trails,onReady:t=>{process.stdout.write(`${de(t)}
243
- `)},onTrail:(t,n,o)=>{process.stdout.write(`${Rn({executed:t,progress:o,trail:n})}
244
- `)}});process.stdout.write(`${Cn(r)}
245
- `),(r.kind==="config-failed"||r.kind==="compile-failed")&&process.exit(1)}import{graphql as ll}from"gql.tada";var dl=ll(`
242
+ `),process.exit(1)})}async function pl(e){await w(process.cwd()).match(r=>un(r,e,"dismissed").catch(n=>{$.warn({error:n},"explore finding status push failed")}),r=>{$.warn({failure:r},"explore finding dismiss not synced: config unavailable")})}async function Jt(e){let r=await Ct({cwd:process.cwd(),headed:e.headed,maxLength:e.maxLength,trails:e.trails,onReady:n=>{process.stdout.write(`${le(n)}
243
+ `)},onTrail:(n,t,o)=>{process.stdout.write(`${jt({executed:n,progress:o,trail:t})}
244
+ `)}});process.stdout.write(`${At(r)}
245
+ `),(r.kind==="config-failed"||r.kind==="compile-failed")&&process.exit(1)}import{graphql as ul}from"gql.tada";var ml=ul(`
246
246
  query ProjectsList {
247
247
  projects {
248
248
  id
249
249
  name
250
250
  }
251
251
  }
252
- `);async function qn(){let e=A(),r=F(e);r==null&&(process.stderr.write("Not signed in. Run `npx ripplo auth login` first.\n"),process.exit(1));let n=((await u({config:H({serverUrl:e,token:r}),document:dl,variables:void 0})).projects??[]).map(o=>({id:o.id,name:o.name}));process.stdout.write(`${JSON.stringify({projects:n},null,2)}
253
- `)}import{graphql as cl}from"gql.tada";function Bn({id:e,kind:r,title:t}){return`Caught bug reported (${r}): "${t}" [${e}]`}var pl=cl(`
252
+ `);async function Kt(){let e=A(),r=_(e);r==null&&(process.stderr.write("Not signed in. Run `npx ripplo auth login` first.\n"),process.exit(1));let t=((await u({config:M({serverUrl:e,token:r}),document:ml,variables:void 0})).projects??[]).map(o=>({id:o.id,name:o.name}));process.stdout.write(`${JSON.stringify({projects:t},null,2)}
253
+ `)}import{graphql as fl}from"gql.tada";function Qt({id:e,kind:r,title:n}){return`Caught bug reported (${r}): "${n}" [${e}]`}var gl=fl(`
254
254
  mutation ReportCaughtBug(
255
255
  $projectId: String!
256
256
  $kind: CaughtBugKind!
@@ -280,34 +280,22 @@ Verify the dev session is live (\`npx ripplo doctor\`, ${c("start")}), or pass t
280
280
  }
281
281
  }
282
282
  }
283
- `);async function Vn(e){let r=T(),n=(await u({config:r,document:pl,variables:{kind:e.kind,projectId:r.projectId,rootCause:e.rootCause,runId:e.runId,surfacedBy:e.surfacedBy,title:e.title,workflowSlug:e.testId==null?null:L(e.testId)}})).reportCaughtBug;if(n?.__typename!=="CaughtBug"){let o=n?.__typename==="WorkflowNotFoundError"?n.message:null;process.stderr.write(`${o??"reportCaughtBug failed"}
284
- `),process.exit(1)}process.stdout.write(`${Bn({id:n.id,kind:e.kind,title:e.title})}
285
- `)}import ul from"path";import{CancellationTokenSource as ml}from"vscode-jsonrpc/node";async function Gn({all:e,headed:r,ids:t,keepAlive:n}){let o=process.cwd(),i=process.argv[1];i==null&&fe(D({kind:"spawn-failed",message:"process.argv[1] missing"}));let a=await Fe(o);a!=null&&fe(Me(a));let l=(await te({cliEntry:i,cwd:o})).match(I=>I,I=>fe(D(I)));l.versionNote!=null&&process.stderr.write(`${pt(l.versionNote)}
286
- `);let d=new ml;process.once("SIGINT",()=>{d.cancel(),l.socket.destroy(),process.exit(130)});let g=ul.join(o,".ripplo","debug"),y=(await De({connection:l,request:{all:e,headed:r,tests:[...t]},token:d.token,onEvent:I=>{let E=Ue({debugDir:g,event:I});E!=null&&process.stdout.write(`${E}
287
- `)}})).match(I=>I,I=>fe(D(I)));await fl({connection:l,keepAlive:n,result:y})}async function fl({connection:e,keepAlive:r,result:t}){e.spawned&&!r&&await ne(e),await new Promise(n=>{e.socket.end(n)}),t.kind==="daemon-error"&&fe(He(t.error)),process.stdout.write(`${Fn(t)}
288
- `),t.failed>0&&process.exit(1),process.exit(t.notRun>0?2:0)}function fe(e){process.stderr.write(`${e}
289
- `),process.exit(1)}import zn from"path";import{err as qe,ok as mr}from"neverthrow";async function Jn(){let e=process.cwd(),r=$.child({worker:process.pid}),t=Ot(),n=Se({clientVersion:P(),debugDir:zn.join(e,".ripplo","debug"),headed:!1,writeOtlpPortFile:!1}),o={entry:void 0},i=a=>{n.close().catch(()=>{}).then(()=>{process.exit(a)})};process.on("disconnect",()=>{i(1)}),process.on("unhandledRejection",a=>{r.error({err:a},"worker unhandled rejection")}),process.on("uncaughtException",a=>{r.error({err:a},"worker uncaught exception"),i(1)}),t.onRequest(jt,async(a,s)=>{let l=Pt.safeParse(a);if(!l.success)return We("bad-run-assign");let d=l.data;V(e);let g=w(e).match(E=>E,E=>E.kind);if(typeof g=="string")return We(`config:${g}`);let p=await Kn(d.lockfileFingerprint,o,t);if(p.isErr())return We(`lockfile-unavailable:${p.error}`);let y=p.value,I=Br(y).find(E=>E.ref===d.testRef);if(I==null)return We(`no-test:${d.testRef}`);try{let E=await nt({config:g,cwd:e,fixturesDir:zn.join(e,we),headed:d.headed,lockfile:y,runId:d.runId,session:n,signal:nr(s),test:I.test});return{outcome:ut(E),serverNotified:!0}}catch(E){if(E instanceof br)return{outcome:{detail:E.message,kind:"infra-error"},serverNotified:!1};throw E}}),t.onRequest(Lt,(a,s)=>gl({cache:o,connection:t,cwd:e,raw:a,session:n,token:s})),t.onNotification(Tt,a=>{let s=Et.safeParse(a);s.success&&n.injectSpan(s.data.runId,s.data.span)}),t.onNotification(Dt,()=>{i(0)}),t.sendNotification(It),await new Promise(()=>{})}function We(e){return{outcome:{detail:e,kind:"error"},serverNotified:!1}}async function gl({cache:e,connection:r,cwd:t,raw:n,session:o,token:i}){let a=$t.safeParse(n);if(!a.success)return{kind:"error",reason:"bad-assign",rows:[],trail:[]};let s=a.data;V(t);let l=w(t).match(p=>p,p=>p.kind);if(typeof l=="string")return{kind:"error",reason:`config:${l}`,rows:[],trail:[]};let d=await Kn(s.lockfileFingerprint,e,r);if(d.isErr())return{kind:"error",reason:`lockfile:${d.error}`,rows:[],trail:[]};let g=d.value;return Te({assign:s,config:l,headed:!1,lockfile:g,report:!0,session:o,signal:nr(i)})}async function Kn(e,r,t){return r.entry!=null&&r.entry.fingerprint===e?mr(r.entry.lockfile):(await hl(t,e)).andThen(o=>o.unavailable!=null?qe(o.unavailable):o.lockfileJson==null?qe("empty-reply"):yl(o.lockfileJson).map(i=>(r.entry={fingerprint:e,lockfile:i},i)))}async function hl(e,r){try{return mr(await e.sendRequest(At,{fingerprint:r}))}catch(t){return qe(`transport:${t instanceof Error?t.message:"unknown"}`)}}function yl(e){try{return mr(Lr(M,e))}catch(r){return qe(`worker-decode-failed:${r instanceof Error?r.message.slice(0,200):"unknown"}`)}}async function Qn(){await Jn()}import to from"path";import{createRequire as kl}from"module";import{existsSync as wl}from"fs";import{readFile as eo,writeFile as Xn}from"fs/promises";import{fileURLToPath as vl}from"url";import Yn from"path";import{err as ge,ok as bl}from"neverthrow";import{z as _}from"zod";var Sl={height:800,width:1280};async function ro({jsonlPath:e,moment:r,outDir:t,runId:n}){if(!wl(e))return ge({kind:"run-not-found",runId:n});let o=await Rl(e),i=o[0],a=o.at(-1);if(i==null||a==null)return ge({kind:"no-rrweb-events",runId:n});let s=a.timestamp-i.timestamp,l=r.kind==="offset"?r.offsetMs:r.at-i.timestamp;if(l<0)return ge({durationMs:s,firstTimestamp:i.timestamp,kind:"moment-out-of-range",lastTimestamp:a.timestamp,moment:r});let d=Math.min(l,s),g=String(Math.round(d)),p=Yn.join(t,`snapshot-${g}ms.png`),y=Yn.join(t,`snapshot-${g}ms.html`);return(await jl({events:o,htmlPath:y,offsetMs:d,pngPath:p})).map(()=>({durationMs:s,htmlPath:y,offsetMs:d,pngPath:p}))}var xl=_.object({event:_.looseObject({timestamp:_.number(),type:_.number()}),kind:_.literal("rrweb")});async function Rl(e){return(await eo(e,"utf8")).split(`
290
- `).filter(t=>t.length>0).map(t=>Cl(t)).map(t=>xl.safeParse(t)).flatMap(t=>t.success?[t.data.event]:[])}function Cl(e){try{return JSON.parse(e)}catch{return null}}var Pl=_.object({data:_.looseObject({height:_.number(),width:_.number()}),type:_.literal(4)});function El(e){let r=e.flatMap(t=>{let n=Pl.safeParse(t);return n.success?[n.data]:[]})[0];return r==null?Sl:{height:r.data.height,width:r.data.width}}var $l=`<!doctype html><html><head><style>
291
- html, body { margin: 0; padding: 0; }
292
- .replayer-wrapper { position: relative; }
293
- .replayer-mouse, .replayer-mouse-tail { display: none; }
294
- iframe { border: none; }
295
- </style></head><body></body></html>`,Il="*, *::before, *::after { animation-duration: 0s !important; animation-delay: 0s !important; transition-duration: 0s !important; transition-delay: 0s !important; }";async function jl({events:e,htmlPath:r,offsetMs:t,pngPath:n}){let o=await import("playwright").then(({chromium:i})=>i.launch({args:["--no-sandbox"],headless:!0})).catch(()=>null);if(o==null)return ge({detail:"chromium launch",kind:"browser-failed"});try{let{html:i,png:a}=await Al({browser:o,events:e,offsetMs:t});return await Xn(n,a),await Xn(r,i,"utf8"),bl(void 0)}catch(i){return ge({detail:i instanceof Error?i.message:String(i),kind:"browser-failed"})}finally{await o.close()}}async function Al({browser:e,events:r,offsetMs:t}){let n=El(r),o=await e.newPage({viewport:n});await o.setContent($l),await o.addScriptTag({content:await Ol()}),await o.evaluate(Ll({events:r,offsetMs:t})),await o.evaluate(Tl());let i=o.locator(".replayer-wrapper iframe").first(),s=await(await i.count()>0?i:o).screenshot({type:"png"});return{html:await o.evaluate(Dl()),png:s}}function Ll({events:e,offsetMs:r}){return`(() => {
283
+ `);async function Xt(e){let r=T(),t=(await u({config:r,document:gl,variables:{kind:e.kind,projectId:r.projectId,rootCause:e.rootCause,runId:e.runId,surfacedBy:e.surfacedBy,title:e.title,workflowSlug:e.testId==null?null:L(e.testId)}})).reportCaughtBug;if(t?.__typename!=="CaughtBug"){let o=t?.__typename==="WorkflowNotFoundError"?t.message:null;process.stderr.write(`${o??"reportCaughtBug failed"}
284
+ `),process.exit(1)}process.stdout.write(`${Qt({id:t.id,kind:e.kind,title:e.title})}
285
+ `)}import hl from"path";import{CancellationTokenSource as yl}from"vscode-jsonrpc/node";async function Yt({all:e,headed:r,ids:n,keepAlive:t}){let o=process.cwd(),i=process.argv[1];i==null&&me(D({kind:"spawn-failed",message:"process.argv[1] missing"}));let a=await _e(o);a!=null&&me(Ue(a));let l=(await re({cliEntry:i,cwd:o})).match(I=>I,I=>me(D(I)));l.versionNote!=null&&process.stderr.write(`${kn(l.versionNote)}
286
+ `);let d=new yl;process.once("SIGINT",()=>{d.cancel(),l.socket.destroy(),process.exit(130)});let g=hl.join(o,".ripplo","debug"),y=(await Te({connection:l,request:{all:e,headed:r,tests:[...n]},token:d.token,onEvent:I=>{let E=Ne({debugDir:g,event:I});E!=null&&process.stdout.write(`${E}
287
+ `)}})).match(I=>I,I=>me(D(I)));await kl({connection:l,keepAlive:t,result:y})}async function kl({connection:e,keepAlive:r,result:n}){e.spawned&&!r&&await ne(e),await new Promise(t=>{e.socket.end(t)}),n.kind==="daemon-error"&&me(Me(n.error)),process.stdout.write(`${qt(n)}
288
+ `),n.failed>0&&process.exit(1),process.exit(n.notRun>0?2:0)}function me(e){process.stderr.write(`${e}
289
+ `),process.exit(1)}import Zt from"path";import{err as We,ok as ur}from"neverthrow";async function eo(){let e=process.cwd(),r=$.child({worker:process.pid}),n=Wn(),t=be({clientVersion:P(),debugDir:Zt.join(e,".ripplo","debug"),headed:!1,writeOtlpPortFile:!1}),o={entry:void 0},i=a=>{t.close().catch(()=>{}).then(()=>{process.exit(a)})};process.on("disconnect",()=>{i(1)}),process.on("unhandledRejection",a=>{r.error({err:a},"worker unhandled rejection")}),process.on("uncaughtException",a=>{r.error({err:a},"worker uncaught exception"),i(1)}),n.onRequest(Fn,async(a,s)=>{let l=Tn.safeParse(a);if(!l.success)return He("bad-run-assign");let d=l.data;B(e);let g=w(e).match(E=>E,E=>E.kind);if(typeof g=="string")return He(`config:${g}`);let p=await ro(d.lockfileFingerprint,o,n);if(p.isErr())return He(`lockfile-unavailable:${p.error}`);let y=p.value,I=qr(y).find(E=>E.ref===d.testRef);if(I==null)return He(`no-test:${d.testRef}`);try{let E=await cn({config:g,cwd:e,fixturesDir:Zt.join(e,ke),headed:d.headed,lockfile:y,runId:d.runId,session:t,signal:nr(s),test:I.test});return{outcome:wn(E),serverNotified:!0}}catch(E){if(E instanceof vr)return{outcome:{detail:E.message,kind:"infra-error"},serverNotified:!1};throw E}}),n.onRequest(Un,(a,s)=>wl({cache:o,connection:n,cwd:e,raw:a,session:t,token:s})),n.onNotification(Mn,a=>{let s=Dn.safeParse(a);s.success&&t.injectSpan(s.data.runId,s.data.span)}),n.onNotification(Hn,()=>{i(0)}),n.sendNotification(_n),await new Promise(()=>{})}function He(e){return{outcome:{detail:e,kind:"error"},serverNotified:!1}}async function wl({cache:e,connection:r,cwd:n,raw:t,session:o,token:i}){let a=On.safeParse(t);if(!a.success)return{kind:"error",reason:"bad-assign",rows:[],trail:[]};let s=a.data;B(n);let l=w(n).match(p=>p,p=>p.kind);if(typeof l=="string")return{kind:"error",reason:`config:${l}`,rows:[],trail:[]};let d=await ro(s.lockfileFingerprint,e,r);if(d.isErr())return{kind:"error",reason:`lockfile:${d.error}`,rows:[],trail:[]};let g=d.value;return Le({assign:s,config:l,headed:!1,lockfile:g,report:!0,session:o,signal:nr(i)})}async function ro(e,r,n){return r.entry!=null&&r.entry.fingerprint===e?ur(r.entry.lockfile):(await vl(n,e)).andThen(o=>o.unavailable!=null?We(o.unavailable):o.lockfileJson==null?We("empty-reply"):bl(o.lockfileJson).map(i=>(r.entry={fingerprint:e,lockfile:i},i)))}async function vl(e,r){try{return ur(await e.sendRequest(Nn,{fingerprint:r}))}catch(n){return We(`transport:${n instanceof Error?n.message:"unknown"}`)}}function bl(e){try{return ur(Ar(U,e))}catch(r){return We(`worker-decode-failed:${r instanceof Error?r.message.slice(0,200):"unknown"}`)}}async function no(){await eo()}import so from"path";import{existsSync as Sl}from"fs";import{writeFile as to}from"fs/promises";import oo from"path";import{err as fe,ok as xl}from"neverthrow";async function io({jsonlPath:e,moment:r,outDir:n,runId:t}){if(!Sl(e))return fe({kind:"run-not-found",runId:t});let o=await Kr(e),i=o[0],a=o.at(-1);if(i==null||a==null)return fe({kind:"no-rrweb-events",runId:t});let s=a.timestamp-i.timestamp,l=r.kind==="offset"?r.offsetMs:r.at-i.timestamp;if(l<0)return fe({durationMs:s,firstTimestamp:i.timestamp,kind:"moment-out-of-range",lastTimestamp:a.timestamp,moment:r});let d=Math.min(l,s),g=String(Math.round(d)),p=oo.join(n,`snapshot-${g}ms.png`),y=oo.join(n,`snapshot-${g}ms.html`);return(await Rl({events:o,htmlPath:y,offsetMs:d,pngPath:p})).map(()=>({durationMs:s,htmlPath:y,offsetMs:d,pngPath:p}))}async function Rl({events:e,htmlPath:r,offsetMs:n,pngPath:t}){let o=await en();if(o==null)return fe({detail:"chromium launch",kind:"browser-failed"});try{let{html:i,png:a}=await Cl({browser:o,events:e,offsetMs:n});return await to(t,a),await to(r,i,"utf8"),xl(void 0)}catch(i){return fe({detail:i instanceof Error?i.message:String(i),kind:"browser-failed"})}finally{await o.close()}}async function Cl({browser:e,events:r,offsetMs:n}){let t=Qr(r),o=await e.newPage({viewport:t});await o.setContent(Xr(!1)),await o.addScriptTag({content:await rn()}),await o.evaluate(Pl({events:r,offsetMs:n})),await o.evaluate(Zr());let i=o.locator(".replayer-wrapper iframe").first(),s=await(await i.count()>0?i:o).screenshot({type:"png"});return{html:await o.evaluate(El()),png:s}}function Pl({events:e,offsetMs:r}){return`(() => {
296
290
  const replayer = new globalThis.__RipploReplayer(${JSON.stringify(e)}, {
297
- insertStyleRules: [${JSON.stringify(Il)}],
291
+ insertStyleRules: [${JSON.stringify(Yr)}],
298
292
  mouseTail: false,
299
293
  root: document.body,
300
294
  showWarning: false,
301
295
  });
302
296
  replayer.pause(${String(r+.5)});
303
297
  globalThis.__ripploSnapshotReplayer = replayer;
304
- })()`}function Tl(){return`(async () => {
305
- const doc = document.querySelector(".replayer-wrapper iframe")?.contentDocument;
306
- if (doc?.fonts != null) {
307
- await doc.fonts.ready;
308
- }
309
- await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
310
- })()`}function Dl(){return`(() => {
298
+ })()`}function El(){return`(() => {
311
299
  const replayer = globalThis.__ripploSnapshotReplayer;
312
300
  if (replayer == null) {
313
301
  return "";
@@ -325,28 +313,28 @@ iframe { border: none; }
325
313
  }
326
314
  });
327
315
  return "<!doctype html>\\n" + doc.documentElement.outerHTML;
328
- })()`}var Zn;function Ol(){return Zn??=eo(_l(),"utf8"),Zn}function _l(){let e=kl(import.meta.url);try{return e.resolve("@ripplo/rrweb-bundle/replay")}catch{return vl(new URL("assets/rrweb-replay.js",import.meta.url))}}function no({cwd:e,moment:r,runId:t}){let n=to.join(e,".ripplo","debug",t);return ro({jsonlPath:to.join(n,"behavior.jsonl"),moment:r,outDir:n,runId:t})}function oo(e){return[`${C.good("ok")} \u2014 rendered the page at ${String(Math.round(e.offsetMs))}ms into the recording (duration ${String(Math.round(e.durationMs))}ms)`,e.pngPath,"Read the PNG above to see the page state. Layout and text are faithful \u2014 URL-referenced images may be blank if the dev server is down."].join(`
329
- `)}function io(e){return`${C.bad("fail")} \u2014 ${Fl(e)}`}function so(){return`${C.bad("fail")} \u2014 pass exactly one of --at <epoch-ms from behavior.jsonl> or --offset <ms from the start of the recording>.`}function Fl(e){switch(e.kind){case"run-not-found":return`no debug artifacts for run ${e.runId} (.ripplo/debug/${e.runId}/behavior.jsonl missing). ${c("run")}`;case"no-rrweb-events":return`run ${e.runId} has a behavior.jsonl but no rrweb events \u2014 nothing to replay.`;case"moment-out-of-range":return`${e.moment.kind==="offset"?"--offset":"--at"} is outside the recording, which spans ${String(e.firstTimestamp)}\u2013${String(e.lastTimestamp)} (duration ${String(e.durationMs)}ms). Pass --at <epoch-ms from behavior.jsonl> or --offset <0\u2013${String(e.durationMs)}>.`;case"browser-failed":return`replay browser failed (${e.detail}). Is chromium installed? Try \`npx playwright install chromium\`.`}}async function ao({at:e,offset:r,runId:t}){let n=Nl({at:e,offset:r});n==null&&(process.stderr.write(`${so()}
330
- `),process.exit(1)),(await no({cwd:process.cwd(),moment:n,runId:t})).match(i=>{process.stdout.write(`${oo(i)}
331
- `)},i=>{process.stderr.write(`${io(i)}
332
- `),process.exit(1)})}function Nl({at:e,offset:r}){if(e!=null&&r==null)return{at:e,kind:"absolute"};if(r!=null&&e==null)return{kind:"offset",offsetMs:r}}async function lo(){let e=T();try{let t=(await ce(e.cwd,e)).match(i=>i,i=>{process.stderr.write(`${j(i)}
333
- `),process.exit(1)}),n=t.lockfile.workflows.length,o=t.lockfile.entities.length;process.stdout.write(`Synced ${b(n,"workflow")} and ${b(o,"entity","entities")} to dev session ${t.devSessionId}
334
- `)}catch(r){let t=r instanceof Error?r.message:String(r);process.stderr.write(`ripplo sync failed: ${t}
316
+ })()`}function ao({cwd:e,moment:r,runId:n}){let t=so.join(e,".ripplo","debug",n);return io({jsonlPath:so.join(t,"behavior.jsonl"),moment:r,outDir:t,runId:n})}function lo(e){return[`${C.good("ok")} \u2014 rendered the page at ${String(Math.round(e.offsetMs))}ms into the recording (duration ${String(Math.round(e.durationMs))}ms)`,e.pngPath,"Read the PNG above to see the page state. Layout and text are faithful \u2014 URL-referenced images may be blank if the dev server is down."].join(`
317
+ `)}function co(e){return`${C.bad("fail")} \u2014 ${$l(e)}`}function po(){return`${C.bad("fail")} \u2014 pass exactly one of --at <epoch-ms from behavior.jsonl> or --offset <ms from the start of the recording>.`}function $l(e){switch(e.kind){case"run-not-found":return`no debug artifacts for run ${e.runId} (.ripplo/debug/${e.runId}/behavior.jsonl missing). ${c("run")}`;case"no-rrweb-events":return`run ${e.runId} has a behavior.jsonl but no rrweb events \u2014 nothing to replay.`;case"moment-out-of-range":return`${e.moment.kind==="offset"?"--offset":"--at"} is outside the recording, which spans ${String(e.firstTimestamp)}\u2013${String(e.lastTimestamp)} (duration ${String(e.durationMs)}ms). Pass --at <epoch-ms from behavior.jsonl> or --offset <0\u2013${String(e.durationMs)}>.`;case"browser-failed":return`replay browser failed (${e.detail}). Is chromium installed? Try \`npx playwright install chromium\`.`}}async function uo({at:e,offset:r,runId:n}){let t=Il({at:e,offset:r});t==null&&(process.stderr.write(`${po()}
318
+ `),process.exit(1)),(await ao({cwd:process.cwd(),moment:t,runId:n})).match(i=>{process.stdout.write(`${lo(i)}
319
+ `)},i=>{process.stderr.write(`${co(i)}
320
+ `),process.exit(1)})}function Il({at:e,offset:r}){if(e!=null&&r==null)return{at:e,kind:"absolute"};if(r!=null&&e==null)return{kind:"offset",offsetMs:r}}async function mo(){let e=T();try{let n=(await de(e.cwd,e)).match(i=>i,i=>{process.stderr.write(`${j(i)}
321
+ `),process.exit(1)}),t=n.lockfile.workflows.length,o=n.lockfile.entities.length;process.stdout.write(`Synced ${b(t,"workflow")} and ${b(o,"entity","entities")} to dev session ${n.devSessionId}
322
+ `)}catch(r){let n=r instanceof Error?r.message:String(r);process.stderr.write(`ripplo sync failed: ${n}
335
323
  `),process.stderr.write(`${c("setup","verify auth + server reachability")}
336
- `),process.exit(1)}}import{spawnSync as Hl}from"child_process";import Wl from"fs";import ql from"path";import Bl from"semver";import Be from"fs";import Ul from"os";import he from"path";function co(e){return e.split(he.sep).includes("_npx")?"npx":e.includes(he.join("packages","cli"))?"workspace":"global"}function po(){let e=he.join(Ul.homedir(),".npm","_npx");return Be.existsSync(e)?Be.readdirSync(e).map(r=>he.join(e,r)).filter(r=>Be.existsSync(he.join(r,"node_modules","ripplo"))).map(r=>(Be.rmSync(r,{force:!0,recursive:!0}),r)):[]}import{spawnSync as uo}from"child_process";var Ml=["user","project","local"];function fr(){return uo("claude",["--version"],{stdio:"ignore"}).error!=null?"claude-missing":Ml.find(t=>uo("claude",["plugin","update","ripplo","--scope",t],{stdio:"ignore"}).status===0)==null?"not-installed":"updated"}function mo(e){return`ripplo v${e} is already the latest version.`}function fo(e){return`ripplo v${e}: could not reach the npm registry to check for updates.`}function gr({evicted:e,latest:r,mode:t}){return t==="npx"?`ripplo: ${e===0?"npx cache had no stale copy":`cleared ${b(e,"stale npx cache entry","stale npx cache entries")}`} \u2014 the next \`npx ripplo\` command runs v${r}.`:`ripplo: updated global install to v${r}.`}function go(){return"ripplo: this is a workspace build (packages/cli/dist) \u2014 update via git pull + `pnpm --filter ripplo build`."}function ho(e){return`ripplo: \`npm install -g ripplo@latest\` failed: ${e}`}function hr(e){switch(e){case"updated":return"ripplo: Claude plugin updated \u2014 restart Claude Code (or /reload-plugins) to apply.";case"not-installed":return"ripplo: Claude plugin not installed in this project \u2014 skipped.";case"claude-missing":return"ripplo: claude binary not found \u2014 skipped plugin update."}}function yo(e){return e?"ripplo: idle daemon stopped \u2014 the next `npx ripplo run` restarts it on the new version.":"ripplo: daemon is busy \u2014 it picks up the new version once idle (the next `npx ripplo run` handles it)."}var Vl=1e4;async function ko(){let e=P(),r=process.argv[1];Gl(process.cwd());let t=await Pe(Vl);process.stdout.write(`${Ce({current:e,latest:t})}
337
- `),t==null&&(process.stderr.write(`${fo(e)}
338
- `),process.exit(1)),Bl.gt(t,e)||(process.stdout.write(`${mo(e)}
339
- `),process.stdout.write(`${hr(fr())}
340
- `),process.exit(0));let n=r==null?"global":co(r);zl({latest:t,mode:n}),process.stdout.write(`${hr(fr())}
341
- `),await Kl(process.cwd()),process.exit(0)}function Gl(e){if(!Wl.existsSync(ql.join(e,".ripplo")))return;let r=je(e);r==="written"&&process.stdout.write(`${Ae(r).trim()}
342
- `)}function zl({latest:e,mode:r}){if(r==="workspace"&&(process.stdout.write(`${go()}
343
- `),process.exit(0)),r==="npx"){let t=po();process.stdout.write(`${gr({evicted:t.length,latest:e,mode:r})}
344
- `);return}Jl(e)}function Jl(e){let r=`ripplo@${e}`,t=Hl("npm",["install","-g",r],{stdio:"inherit"});if(t.status!==0){let n=`exit ${String(t.status)}`;process.stderr.write(`${ho(n)}
345
- `),process.exit(1)}process.stdout.write(`${gr({evicted:0,latest:e,mode:"global"})}
346
- `)}async function Kl(e){if((await Oe(e)).kind==="not-running")return;let t=await On(e);process.stdout.write(`${yo(t)}
347
- `)}async function wo({executor:e,explore:r,exploreConcurrency:t}){let{runDaemon:n}=await import("./daemon-EUZ4KVQP.js"),{TunnelProvisionFailedError:o}=await import("./daemon-tunnel-MOYCRSSC.js"),{renderTunnelProvisionFailed:i}=await import("./daemon-EQZROAKJ.js");try{await n({executor:e,explore:r,exploreConcurrency:t})}catch(a){throw a instanceof o&&(process.stderr.write(`${i(a.failure)}
348
- `),process.exit(1)),a}}import{graphql as Ve}from"gql.tada";function vo(){return"No scope items added \u2014 the matched tests are already in scope (check `npx ripplo scope status`)."}function bo(e){return[`No test found for: ${e.join(", ")}`,"Pass a test id (kebab-case slug of the test name) or the exact intent string.","List known tests with `npx ripplo status`. To add one, stub it first via the testing DSL.",c("create")].join(`
349
- `)}import{graphql as Ql}from"gql.tada";var So=Ql(`
324
+ `),process.exit(1)}}import{spawnSync as Ll}from"child_process";import Tl from"fs";import Dl from"path";import Ol from"semver";import qe from"fs";import jl from"os";import ge from"path";function fo(e){return e.split(ge.sep).includes("_npx")?"npx":e.includes(ge.join("packages","cli"))?"workspace":"global"}function go(){let e=ge.join(jl.homedir(),".npm","_npx");return qe.existsSync(e)?qe.readdirSync(e).map(r=>ge.join(e,r)).filter(r=>qe.existsSync(ge.join(r,"node_modules","ripplo"))).map(r=>(qe.rmSync(r,{force:!0,recursive:!0}),r)):[]}import{spawnSync as ho}from"child_process";var Al=["user","project","local"];function mr(){return ho("claude",["--version"],{stdio:"ignore"}).error!=null?"claude-missing":Al.find(n=>ho("claude",["plugin","update","ripplo","--scope",n],{stdio:"ignore"}).status===0)==null?"not-installed":"updated"}function yo(e){return`ripplo v${e} is already the latest version.`}function ko(e){return`ripplo v${e}: could not reach the npm registry to check for updates.`}function fr({evicted:e,latest:r,mode:n}){return n==="npx"?`ripplo: ${e===0?"npx cache had no stale copy":`cleared ${b(e,"stale npx cache entry","stale npx cache entries")}`} \u2014 the next \`npx ripplo\` command runs v${r}.`:`ripplo: updated global install to v${r}.`}function wo(){return"ripplo: this is a workspace build (packages/cli/dist) \u2014 update via git pull + `pnpm --filter ripplo build`."}function vo(e){return`ripplo: \`npm install -g ripplo@latest\` failed: ${e}`}function gr(e){switch(e){case"updated":return"ripplo: Claude plugin updated \u2014 restart Claude Code (or /reload-plugins) to apply.";case"not-installed":return"ripplo: Claude plugin not installed in this project \u2014 skipped.";case"claude-missing":return"ripplo: claude binary not found \u2014 skipped plugin update."}}function bo(e){return e?"ripplo: idle daemon stopped \u2014 the next `npx ripplo run` restarts it on the new version.":"ripplo: daemon is busy \u2014 it picks up the new version once idle (the next `npx ripplo run` handles it)."}var _l=1e4;async function So(){let e=P(),r=process.argv[1];Fl(process.cwd());let n=await Ce(_l);process.stdout.write(`${Re({current:e,latest:n})}
325
+ `),n==null&&(process.stderr.write(`${ko(e)}
326
+ `),process.exit(1)),Ol.gt(n,e)||(process.stdout.write(`${yo(e)}
327
+ `),process.stdout.write(`${gr(mr())}
328
+ `),process.exit(0));let t=r==null?"global":fo(r);Nl({latest:n,mode:t}),process.stdout.write(`${gr(mr())}
329
+ `),await Ml(process.cwd()),process.exit(0)}function Fl(e){if(!Tl.existsSync(Dl.join(e,".ripplo")))return;let r=Ie(e);r==="written"&&process.stdout.write(`${je(r).trim()}
330
+ `)}function Nl({latest:e,mode:r}){if(r==="workspace"&&(process.stdout.write(`${wo()}
331
+ `),process.exit(0)),r==="npx"){let n=go();process.stdout.write(`${fr({evicted:n.length,latest:e,mode:r})}
332
+ `);return}Ul(e)}function Ul(e){let r=`ripplo@${e}`,n=Ll("npm",["install","-g",r],{stdio:"inherit"});if(n.status!==0){let t=`exit ${String(n.status)}`;process.stderr.write(`${vo(t)}
333
+ `),process.exit(1)}process.stdout.write(`${fr({evicted:0,latest:e,mode:"global"})}
334
+ `)}async function Ml(e){if((await De(e)).kind==="not-running")return;let n=await Ht(e);process.stdout.write(`${bo(n)}
335
+ `)}async function xo({executor:e,explore:r,exploreConcurrency:n}){let{runDaemon:t}=await import("./daemon-5FTGPJ3L.js"),{TunnelProvisionFailedError:o}=await import("./daemon-tunnel-AG6FGNXC.js"),{renderTunnelProvisionFailed:i}=await import("./daemon-XZ5HWQJA.js");try{await t({executor:e,explore:r,exploreConcurrency:n})}catch(a){throw a instanceof o&&(process.stderr.write(`${i(a.failure)}
336
+ `),process.exit(1)),a}}import{graphql as Be}from"gql.tada";function Ro(){return"No scope items added \u2014 the matched tests are already in scope (check `npx ripplo scope status`)."}function Co(e){return[`No test found for: ${e.join(", ")}`,"Pass a test id (kebab-case slug of the test name) or the exact intent string.","List known tests with `npx ripplo status`. To add one, stub it first via the testing DSL.",c("create")].join(`
337
+ `)}import{graphql as Hl}from"gql.tada";var Po=Hl(`
350
338
  query ScopeStatus($projectId: String!, $cwd: String!) {
351
339
  project(id: $projectId) {
352
340
  id
@@ -366,7 +354,7 @@ iframe { border: none; }
366
354
  }
367
355
  }
368
356
  }
369
- `);var Xl=Ve(`
357
+ `);var Wl=Be(`
370
358
  query ScopeWorkflowBySlug($projectId: String!, $cwd: String!, $slug: String!) {
371
359
  project(id: $projectId) {
372
360
  id
@@ -379,7 +367,7 @@ iframe { border: none; }
379
367
  }
380
368
  }
381
369
  }
382
- `),Yl=Ve(`
370
+ `),ql=Be(`
383
371
  mutation ScopeAddDirtyTests($projectId: String!, $cwd: String!, $workflowSlugs: [String!]!) {
384
372
  addDirtyTestsToScope(projectId: $projectId, cwd: $cwd, workflowSlugs: $workflowSlugs) {
385
373
  __typename
@@ -401,38 +389,38 @@ iframe { border: none; }
401
389
  }
402
390
  }
403
391
  }
404
- `),Zl=Ve(`
392
+ `),Bl=Be(`
405
393
  mutation ScopeLink($id: ID!, $workflowId: String!) {
406
394
  linkScopeItem(id: $id, workflowId: $workflowId) {
407
395
  id
408
396
  }
409
397
  }
410
- `),ed=Ve(`
398
+ `),Vl=Be(`
411
399
  mutation ScopeRemoveMany($ids: [ID!]!) {
412
400
  removeScopeItems(ids: $ids)
413
401
  }
414
- `);async function xo(e){let r=T();await pe(r);let n=(await u({config:r,document:So,variables:{cwd:r.cwd,projectId:r.projectId}})).project?.devSession?.scopeItems??[];if(e.format==="json"){process.stdout.write(`${JSON.stringify(n,null,2)}
415
- `);return}if(n.length===0){process.stdout.write("No scope items. Add some with `npx ripplo scope add <test-ids..>` or from the dashboard.\n");return}n.forEach(o=>{let i=o.workflow;if(i==null){process.stdout.write(` [intent] (${o.id}) ${o.label??""}
402
+ `);async function Eo(e){let r=T();await ce(r);let t=(await u({config:r,document:Po,variables:{cwd:r.cwd,projectId:r.projectId}})).project?.devSession?.scopeItems??[];if(e.format==="json"){process.stdout.write(`${JSON.stringify(t,null,2)}
403
+ `);return}if(t.length===0){process.stdout.write("No scope items. Add some with `npx ripplo scope add <test-ids..>` or from the dashboard.\n");return}t.forEach(o=>{let i=o.workflow;if(i==null){process.stdout.write(` [intent] (${o.id}) ${o.label??""}
416
404
  `);return}let a=i.spec==null?"stub":"implemented";process.stdout.write(` [${a}] (${o.id}) ${i.slug} \u2014 ${i.name}
417
- `)})}async function Ro({testIds:e}){let r=T();await pe(r),(await ce(r.cwd,r)).match(()=>{},s=>{process.stderr.write(`${j(s)}
418
- `),process.exit(1)});let o=(await u({config:r,document:Yl,variables:{cwd:r.cwd,projectId:r.projectId,workflowSlugs:e.map(s=>L(s))}})).addDirtyTestsToScope;o?.__typename==="NoActiveDevSessionError"&&(process.stderr.write(`${o.message}
419
- `),process.exit(1)),o?.__typename==="UnknownWorkflowSlugsError"&&(process.stderr.write(`${bo(o.slugs)}
420
- `),process.exit(1));let i=o?.__typename==="MutationAddDirtyTestsToScopeSuccess"?o.data:[];if(i.length===0){process.stdout.write(`${vo()}
405
+ `)})}async function $o({testIds:e}){let r=T();await ce(r),(await de(r.cwd,r)).match(()=>{},s=>{process.stderr.write(`${j(s)}
406
+ `),process.exit(1)});let o=(await u({config:r,document:ql,variables:{cwd:r.cwd,projectId:r.projectId,workflowSlugs:e.map(s=>L(s))}})).addDirtyTestsToScope;o?.__typename==="NoActiveDevSessionError"&&(process.stderr.write(`${o.message}
407
+ `),process.exit(1)),o?.__typename==="UnknownWorkflowSlugsError"&&(process.stderr.write(`${Co(o.slugs)}
408
+ `),process.exit(1));let i=o?.__typename==="MutationAddDirtyTestsToScopeSuccess"?o.data:[];if(i.length===0){process.stdout.write(`${Ro()}
421
409
  `);return}let a=i.map(s=>s.workflow?.slug??"?").join(", ");process.stdout.write(`Added ${b(i.length,"scope item")}: ${a}
422
- `)}async function Co({id:e,testId:r}){let t=T();await pe(t),(await ce(t.cwd,t)).match(()=>{},i=>{process.stderr.write(`${j(i)}
423
- `),process.exit(1)});let o=await rd({cfg:t,slug:r});await u({config:t,document:Zl,variables:{id:e,workflowId:o}}),process.stdout.write(`Linked scope item ${e} to ${r}
424
- `)}async function Po({ids:e}){let r=T();await pe(r);let n=(await u({config:r,document:ed,variables:{ids:[...e]}})).removeScopeItems??0;process.stdout.write(`Removed ${b(n,"scope item")}
425
- `)}async function rd({cfg:e,slug:r}){let n=(await u({config:e,document:Xl,variables:{cwd:e.cwd,projectId:e.projectId,slug:r}})).project?.devSession?.workflows?.[0];return n==null&&(process.stderr.write(`No workflow found with id "${r}". Create a stub first via the testing DSL.
410
+ `)}async function Io({id:e,testId:r}){let n=T();await ce(n),(await de(n.cwd,n)).match(()=>{},i=>{process.stderr.write(`${j(i)}
411
+ `),process.exit(1)});let o=await Gl({cfg:n,slug:r});await u({config:n,document:Bl,variables:{id:e,workflowId:o}}),process.stdout.write(`Linked scope item ${e} to ${r}
412
+ `)}async function jo({ids:e}){let r=T();await ce(r);let t=(await u({config:r,document:Vl,variables:{ids:[...e]}})).removeScopeItems??0;process.stdout.write(`Removed ${b(t,"scope item")}
413
+ `)}async function Gl({cfg:e,slug:r}){let t=(await u({config:e,document:Wl,variables:{cwd:e.cwd,projectId:e.projectId,slug:r}})).project?.devSession?.workflows?.[0];return t==null&&(process.stderr.write(`No workflow found with id "${r}". Create a stub first via the testing DSL.
426
414
  `),process.stderr.write(`${c("create")}
427
- `),process.exit(1)),n.id}async function Eo(e){let r=process.cwd(),t=await S(r);t.isErr()&&(process.stderr.write(`${x(t.error)}
428
- `),process.exit(1));let n=U(t.value),o=await Oe(r);if(e.format==="summary"){n.length>0&&process.stdout.write(`stub workflows: ${n.join(", ")}
429
- `),process.stdout.write(`${ct(o)}
430
- `);return}let i={daemon:o.kind==="running"?{active:o.status.active,explorer:o.status.explorer,exploring:o.status.exploring,progress:o.status.progress,queued:o.status.queued,running:!0}:{running:o.kind==="unresponsive",state:o.kind},tests:n.map(a=>({id:a,implemented:!1}))};process.stdout.write(`${JSON.stringify(i,null,2)}
431
- `)}import Ge from"fs";import nd from"os";import $o from"path";import{z as td}from"zod";function f(e,r){let t=td.custom(n=>typeof n=="object"&&n!==null&&"hook_event_name"in n&&n.hook_event_name===e);return{event:e,run:async n=>await r(t.parse(n))??void 0}}var Io=f("PreToolUse",e=>{if(e.tool_name!=="ExitPlanMode"||!v(e.cwd))return;let r=od();if(r==null)return{hookSpecificOutput:{additionalContext:`Ripplo plan gate: no plan file found \u2014 the "Tests to implement" requirement was not checked. Before implementing, stub a .ripplo/workflows/ workflow per affected flow. ${c("discover")}`,hookEventName:"PreToolUse"}};let t=Ge.readFileSync(r,"utf8");if(!/\.ripplo\/tests|Tests to implement|No e2e coverage needed/.test(t))return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`Plan must cite ripplo test stubs. Add a "Tests to implement" section listing .ripplo/workflows/<id>.ts per affected flow, OR add "No e2e coverage needed: <reason>" if this plan touches no user-facing behavior. ${c("discover")}`}}});function od(){let e=$o.join(nd.homedir(),".claude","plans");return Ge.existsSync(e)?Ge.readdirSync(e).filter(t=>t.endsWith(".md")).map(t=>$o.join(e,t)).map(t=>({full:t,mtime:Ge.statSync(t).mtimeMs})).sort((t,n)=>n.mtime-t.mtime)[0]?.full??null:null}var jo=f("UserPromptSubmit",async e=>{if(e.permission_mode!=="plan"||!m(e.cwd)||!v(e.cwd))return;let r=await S(e.cwd);if(r.isErr())return;let t=U(r.value),n=['Plan must include "Tests to implement" with a .ripplo/workflows/ file per affected flow (ExitPlanMode blocks otherwise). Stub each with `workflow("Intent")` (no body).'];return t.length>0&&n.push(`Existing stubs: ${t.join(", ")}`),{hookSpecificOutput:{additionalContext:n.join(`
432
- `),hookEventName:"UserPromptSubmit"}}});import yd from"path";import Uo from"picomatch";import{z as Mo}from"zod";import{mkdirSync as dd,readFileSync as cd,writeFileSync as pd}from"fs";import ud from"path";import{z as R}from"zod";import{createHash as Uh}from"crypto";import ze from"picomatch";function id(e){return N(["diff","--name-only","HEAD"],e).split(`
433
- `).filter(r=>r.length>0)}function Ao({cwd:e,ignoreGlobs:r,watchGlobs:t}){let n=ze([...t]),o=ze([...r]);return id(e).filter(i=>n(i)&&!o(i))}function sd(e){return N(["ls-files","--others","--exclude-standard"],e).split(`
434
- `).filter(r=>r.length>0)}function Lo({cwd:e,ignoreGlobs:r,watchGlobs:t}){let n=ze([...t]),o=ze([...r]);return sd(e).filter(i=>n(i)&&!o(i))}var ad=["**/src/**","**/app/**","**/apps/**","**/pages/**","**/routes/**","**/components/**","**/server/**","**/api/**","**/backend/**","**/features/**","**/modules/**","**/views/**","**/ui/**","**/hooks/**","**/contexts/**","**/providers/**","**/controllers/**","**/handlers/**","**/resolvers/**","**/services/**","**/middleware/**","**/lib/**"],ld=["**/*.gen.*","**/generated/**","**/*.d.ts","**/*.test.*","**/*.spec.*","**/node_modules/**","**/dist/**","**/build/**",".ripplo/**","**/*.md","**/.next/**","**/.turbo/**","**/.vercel/**","**/.svelte-kit/**","**/.nuxt/**","**/.astro/**","**/coverage/**","**/storybook-static/**","**/*.stories.*","**/*.story.*","**/cli/**","**/scripts/**","**/tools/**","**/__tests__/**","**/__mocks__/**","**/__fixtures__/**","**/*.config.*","**/*.setup.*","**/public/**","**/static/**","**/assets/**","**/migrations/**","**/prisma/migrations/**"];function oe(){return{ignorePaths:ld,watchPaths:ad}}var md=R.object({label:R.string().nullable(),slug:R.string().nullable(),status:R.enum(["intent","stub","implemented"])}),fd=R.object({intent:R.string(),name:R.string(),sourcePath:R.string().nullable(),stub:R.boolean()}),gd=R.object({changedAppFiles:R.array(R.string()).readonly(),scope:R.object({available:R.boolean(),items:R.array(md).readonly()}),tests:R.array(fd).readonly().default([]),untrackedAppFiles:R.array(R.string()).readonly()});function To({cwd:e,scope:r}){let t=Oo(e);Fo(e,{...No(e),scope:r,tests:t?.tests??[]})}function Do({cwd:e,tests:r}){let t=Oo(e);Fo(e,{...No(e),scope:t?.scope??{available:!1,items:[]},tests:r??t?.tests??[]})}function Oo(e){let r=gd.safeParse(hd(_o(e)));return r.success?r.data:null}function hd(e){try{return JSON.parse(cd(e,"utf8"))}catch{return null}}function _o(e){return xr(e,"coverage-context.json")}function Fo(e,r){let t=_o(e);dd(ud.dirname(t),{recursive:!0}),pd(t,JSON.stringify(r,null,2))}function No(e){let{ignorePaths:r,watchPaths:t}=oe(),n={cwd:e,ignoreGlobs:r,watchGlobs:t};return{changedAppFiles:Ao(n),untrackedAppFiles:Lo(n)}}var kd=Mo.looseObject({file_path:Mo.string()}),Ho=f("PostToolUse",async e=>{let r=kd.safeParse(e.tool_input);if(!r.success)return;let t=r.data.file_path,{cwd:n}=e;if(!m(n)||!v(n))return;let o=yd.relative(n,t);if(o.startsWith(".."))return;let{ignorePaths:i,watchPaths:a}=oe(),s=Uo([...a]),l=Uo([...i]);if(!s(o)||l(o))return;let d=await S(n);if(Do({cwd:n,tests:d.isOk()?d.value.workflows.map(p=>({intent:p.intent,name:p.name,sourcePath:p.sourcePath??null,stub:p.stub})):void 0}),d.isErr())return;let g=U(d.value);if(g.length!==0)return{hookSpecificOutput:{additionalContext:`Reminder: stub workflows still unimplemented \u2014 ${g.join(", ")}. Implement with \`workflow("Intent", () => ({ given, steps }))\`.`,hookEventName:"PostToolUse"}}});import{createHash as $d}from"crypto";import{z as Go}from"zod";import{createHash as wd}from"crypto";import{mkdirSync as vd,readFileSync as Wo,writeFileSync as bd}from"fs";import yr from"path";var Sd=[".ts",".tsx",".js",".jsx"];function Je(e){let r=wd("sha256");return r.update(N(["rev-parse","HEAD"],e)),r.update("\0"),r.update(N(["diff","HEAD"],e)),r.update("\0"),N(["ls-files","--others","--exclude-standard"],e).split(`
435
- `).filter(n=>n.length>0).filter(n=>Sd.some(o=>n.endsWith(o))).toSorted((n,o)=>n.localeCompare(o)).forEach(n=>{r.update(n),r.update("\0"),r.update(xd(yr.join(e,n))),r.update("\0")}),r.digest("hex")}function ie(e,r){try{return Wo(qo(e,r),"utf8").trim()}catch{return null}}function se(e,r,t){let n=qo(e,r);vd(yr.dirname(n),{recursive:!0}),bd(n,t)}function xd(e){try{return Wo(e)}catch{return Buffer.alloc(0)}}function qo(e,r){return yr.join(e,".ripplo",".local",`${r}.hash`)}import{graphql as Rd}from"gql.tada";var Cd=Rd(`
415
+ `),process.exit(1)),t.id}async function Ao(e){let r=process.cwd(),n=await S(r);n.isErr()&&(process.stderr.write(`${x(n.error)}
416
+ `),process.exit(1));let t=N(n.value),o=await De(r);if(e.format==="summary"){t.length>0&&process.stdout.write(`stub workflows: ${t.join(", ")}
417
+ `),process.stdout.write(`${yn(o)}
418
+ `);return}let i={daemon:o.kind==="running"?{active:o.status.active,explorer:o.status.explorer,exploring:o.status.exploring,progress:o.status.progress,queued:o.status.queued,running:!0}:{running:o.kind==="unresponsive",state:o.kind},tests:t.map(a=>({id:a,implemented:!1}))};process.stdout.write(`${JSON.stringify(i,null,2)}
419
+ `)}import Ve from"fs";import Jl from"os";import Lo from"path";import{z as zl}from"zod";function f(e,r){let n=zl.custom(t=>typeof t=="object"&&t!==null&&"hook_event_name"in t&&t.hook_event_name===e);return{event:e,run:async t=>await r(n.parse(t))??void 0}}var To=f("PreToolUse",e=>{if(e.tool_name!=="ExitPlanMode"||!v(e.cwd))return;let r=Kl();if(r==null)return{hookSpecificOutput:{additionalContext:`Ripplo plan gate: no plan file found \u2014 the "Tests to implement" requirement was not checked. Before implementing, stub a .ripplo/workflows/ workflow per affected flow. ${c("discover")}`,hookEventName:"PreToolUse"}};let n=Ve.readFileSync(r,"utf8");if(!/\.ripplo\/tests|Tests to implement|No e2e coverage needed/.test(n))return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`Plan must cite ripplo test stubs. Add a "Tests to implement" section listing .ripplo/workflows/<id>.ts per affected flow, OR add "No e2e coverage needed: <reason>" if this plan touches no user-facing behavior. ${c("discover")}`}}});function Kl(){let e=Lo.join(Jl.homedir(),".claude","plans");return Ve.existsSync(e)?Ve.readdirSync(e).filter(n=>n.endsWith(".md")).map(n=>Lo.join(e,n)).map(n=>({full:n,mtime:Ve.statSync(n).mtimeMs})).sort((n,t)=>t.mtime-n.mtime)[0]?.full??null:null}var Do=f("UserPromptSubmit",async e=>{if(e.permission_mode!=="plan"||!m(e.cwd)||!v(e.cwd))return;let r=await S(e.cwd);if(r.isErr())return;let n=N(r.value),t=['Plan must include "Tests to implement" with a .ripplo/workflows/ file per affected flow (ExitPlanMode blocks otherwise). Stub each with `workflow("Intent")` (no body).'];return n.length>0&&t.push(`Existing stubs: ${n.join(", ")}`),{hookSpecificOutput:{additionalContext:t.join(`
420
+ `),hookEventName:"UserPromptSubmit"}}});import ld from"path";import qo from"picomatch";import{z as Bo}from"zod";import{mkdirSync as ed,readFileSync as rd,writeFileSync as nd}from"fs";import td from"path";import{z as R}from"zod";import{createHash as $h}from"crypto";import Ge from"picomatch";function Ql(e){return F(["diff","--name-only","HEAD"],e).split(`
421
+ `).filter(r=>r.length>0)}function Oo({cwd:e,ignoreGlobs:r,watchGlobs:n}){let t=Ge([...n]),o=Ge([...r]);return Ql(e).filter(i=>t(i)&&!o(i))}function Xl(e){return F(["ls-files","--others","--exclude-standard"],e).split(`
422
+ `).filter(r=>r.length>0)}function _o({cwd:e,ignoreGlobs:r,watchGlobs:n}){let t=Ge([...n]),o=Ge([...r]);return Xl(e).filter(i=>t(i)&&!o(i))}var Yl=["**/src/**","**/app/**","**/apps/**","**/pages/**","**/routes/**","**/components/**","**/server/**","**/api/**","**/backend/**","**/features/**","**/modules/**","**/views/**","**/ui/**","**/hooks/**","**/contexts/**","**/providers/**","**/controllers/**","**/handlers/**","**/resolvers/**","**/services/**","**/middleware/**","**/lib/**"],Zl=["**/*.gen.*","**/generated/**","**/*.d.ts","**/*.test.*","**/*.spec.*","**/node_modules/**","**/dist/**","**/build/**",".ripplo/**","**/*.md","**/.next/**","**/.turbo/**","**/.vercel/**","**/.svelte-kit/**","**/.nuxt/**","**/.astro/**","**/coverage/**","**/storybook-static/**","**/*.stories.*","**/*.story.*","**/cli/**","**/scripts/**","**/tools/**","**/__tests__/**","**/__mocks__/**","**/__fixtures__/**","**/*.config.*","**/*.setup.*","**/public/**","**/static/**","**/assets/**","**/migrations/**","**/prisma/migrations/**"];function te(){return{ignorePaths:Zl,watchPaths:Yl}}var od=R.object({label:R.string().nullable(),slug:R.string().nullable(),status:R.enum(["intent","stub","implemented"])}),id=R.object({intent:R.string(),name:R.string(),sourcePath:R.string().nullable(),stub:R.boolean()}),sd=R.object({changedAppFiles:R.array(R.string()).readonly(),scope:R.object({available:R.boolean(),items:R.array(od).readonly()}),tests:R.array(id).readonly().default([]),untrackedAppFiles:R.array(R.string()).readonly()});function Fo({cwd:e,scope:r}){let n=Uo(e);Ho(e,{...Wo(e),scope:r,tests:n?.tests??[]})}function No({cwd:e,tests:r}){let n=Uo(e);Ho(e,{...Wo(e),scope:n?.scope??{available:!1,items:[]},tests:r??n?.tests??[]})}function Uo(e){let r=sd.safeParse(ad(Mo(e)));return r.success?r.data:null}function ad(e){try{return JSON.parse(rd(e,"utf8"))}catch{return null}}function Mo(e){return Sr(e,"coverage-context.json")}function Ho(e,r){let n=Mo(e);ed(td.dirname(n),{recursive:!0}),nd(n,JSON.stringify(r,null,2))}function Wo(e){let{ignorePaths:r,watchPaths:n}=te(),t={cwd:e,ignoreGlobs:r,watchGlobs:n};return{changedAppFiles:Oo(t),untrackedAppFiles:_o(t)}}var dd=Bo.looseObject({file_path:Bo.string()}),Vo=f("PostToolUse",async e=>{let r=dd.safeParse(e.tool_input);if(!r.success)return;let n=r.data.file_path,{cwd:t}=e;if(!m(t)||!v(t))return;let o=ld.relative(t,n);if(o.startsWith(".."))return;let{ignorePaths:i,watchPaths:a}=te(),s=qo([...a]),l=qo([...i]);if(!s(o)||l(o))return;let d=await S(t);if(No({cwd:t,tests:d.isOk()?d.value.workflows.map(p=>({intent:p.intent,name:p.name,sourcePath:p.sourcePath??null,stub:p.stub})):void 0}),d.isErr())return;let g=N(d.value);if(g.length!==0)return{hookSpecificOutput:{additionalContext:`Reminder: stub workflows still unimplemented \u2014 ${g.join(", ")}. Implement with \`workflow("Intent", () => ({ given, steps }))\`.`,hookEventName:"PostToolUse"}}});import{createHash as wd}from"crypto";import{z as Qo}from"zod";import{createHash as cd}from"crypto";import{mkdirSync as pd,readFileSync as Go,writeFileSync as ud}from"fs";import hr from"path";var md=[".ts",".tsx",".js",".jsx"];function ze(e){let r=cd("sha256");return r.update(F(["rev-parse","HEAD"],e)),r.update("\0"),r.update(F(["diff","HEAD"],e)),r.update("\0"),F(["ls-files","--others","--exclude-standard"],e).split(`
423
+ `).filter(t=>t.length>0).filter(t=>md.some(o=>t.endsWith(o))).toSorted((t,o)=>t.localeCompare(o)).forEach(t=>{r.update(t),r.update("\0"),r.update(fd(hr.join(e,t))),r.update("\0")}),r.digest("hex")}function oe(e,r){try{return Go(zo(e,r),"utf8").trim()}catch{return null}}function ie(e,r,n){let t=zo(e,r);pd(hr.dirname(t),{recursive:!0}),ud(t,n)}function fd(e){try{return Go(e)}catch{return Buffer.alloc(0)}}function zo(e,r){return hr.join(e,".ripplo",".local",`${r}.hash`)}import{graphql as gd}from"gql.tada";var hd=gd(`
436
424
  mutation AutoScopeAddDirty($projectId: String!, $cwd: String!, $workflowSlugs: [String!]!) {
437
425
  addDirtyTestsToScope(projectId: $projectId, cwd: $cwd, workflowSlugs: $workflowSlugs) {
438
426
  __typename
@@ -443,11 +431,11 @@ iframe { border: none; }
443
431
  }
444
432
  }
445
433
  }
446
- `);async function Vo({cwd:e,lockfile:r}){if(!v(e))return{addedSlugs:[]};let t=Ed(e);if(t.length===0)return{addedSlugs:[]};let n=new Set(t),o=r.workflows.filter(l=>l.sourcePath!=null&&n.has(l.sourcePath)).map(l=>L(l.name));if(o.length===0)return{addedSlugs:[]};let i=w(e).unwrapOr(void 0);return i==null?{addedSlugs:[]}:await Ct({config:i,cwd:e,lockfile:r}).catch(l=>($.warn("auto-sync failed: %s",l instanceof Error?l.message:String(l)),null))==null?{addedSlugs:[]}:{addedSlugs:await Pd({cfg:i,slugs:o})?o:[]}}async function Pd({cfg:e,slugs:r}){let n=(await u({config:e,document:Cd,variables:{cwd:e.cwd,projectId:e.projectId,workflowSlugs:[...r]}}).catch(o=>($.warn("auto-scope failed: %s",o instanceof Error?o.message:String(o)),null)))?.addDirtyTestsToScope;return n?.__typename!=="MutationAddDirtyTestsToScopeSuccess"?!1:n.data.length>0}var Bo=".ripplo/workflows/";function Ed(e){let r;try{r=N(["status","--porcelain","--",".ripplo/workflows"],e)}catch{return[]}return r.split(`
447
- `).map(t=>t.slice(3).trim()).filter(t=>t.startsWith(Bo)&&t.endsWith(".ts")).map(t=>t.slice(Bo.length)).filter(t=>t!=="index.ts"&&!t.endsWith("/index.ts"))}var Id=Go.looseObject({file_path:Go.string()}),zo=f("PostToolUse",async e=>{let r=Id.safeParse(e.tool_input);if(!r.success||!/\/\.ripplo\/.*\.ts$/.test(r.data.file_path))return;let{cwd:t}=e;if(!m(t))return;if(!v(t))return{hookSpecificOutput:{additionalContext:"Ripplo hooks are paused \u2014 DSL lint and lockfile sync did not run for this edit. Resume with `npx ripplo hooks resume` when ready.",hookEventName:"PostToolUse"}};let n=await X(t);if(n.isErr())return{decision:"block",reason:`${x(n.error)}
448
- ${c("create","DSL authoring + lint rules")}`};let o=W(n.value);if(o.length>0)return{decision:"block",reason:`${q(o)}
449
- ${c("create")}`};let{addedSlugs:i}=await Vo({cwd:t,lockfile:n.value});return jd([...i.length>0?[`Auto-scoped ${i.join(", ")} (dirty tests).`]:[],...Ad(t,n.value)])});function jd(e){if(e.length!==0)return{hookSpecificOutput:{additionalContext:e.join(`
450
- `),hookEventName:"PostToolUse"}}}function Ad(e,r){let t=Le(r),n=$d("sha256").update(JSON.stringify(t)).digest("hex");return ie(e,"coverage-warn")===n?[]:(se(e,"coverage-warn",n),t.length===0?[]:[Ee(t)])}import{z as Jo}from"zod";var Ld=Jo.looseObject({command:Jo.string()}),Td=/\bripplo\s+hooks\s+pause\b/,Ko=f("PreToolUse",e=>{if(e.tool_name!=="Bash")return;let r=Ld.safeParse(e.tool_input);if(!r.success||!Td.test(r.data.command))return;let{cwd:t}=e;if(m(t))return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:"`ripplo hooks pause` is a human-only escape hatch \u2014 agents can't bypass Ripplo guardrails on their own. If the daemon genuinely can't start (auth, server down, intentional offline work), surface the blocker to the user and ask them to run `npx ripplo hooks pause` themselves from their terminal."}}});import{parse as Nd}from"shell-quote";import{z as Xo}from"zod";import{existsSync as Dd,mkdirSync as Od,rmSync as My,writeFileSync as _d}from"fs";import kr from"path";function Ke(e,r,t){let n=Qo(e,r,t);Od(kr.dirname(n),{recursive:!0}),_d(n,"")}function Qe(e,r,t){return Dd(Qo(e,r,t))}function Qo(e,r,t){return kr.join(Fd(e,r),t)}function Fd(e,r){return kr.join(e,".ripplo",".local","skills-loaded",r)}var Ud=Xo.looseObject({command:Xo.string()}),Md="run",Hd=new Set(["tail","head","less","more","wc","sort","uniq","awk","sed","grep"]),Zo=f("PreToolUse",e=>{if(e.tool_name!=="Bash")return;let r=Ud.safeParse(e.tool_input);if(!r.success)return;let t=Vd(r.data.command),n=Wd(t);if(n==null)return;let{cwd:o}=e;if(!m(o)||!v(o))return;if(!Qe(o,e.session_id,n.skill))return Yo(n.reason);let i=n.kind==="run"?zd(t):null;if(i!=null)return Yo(`Don't pipe \`ripplo run\` through \`${i}\` \u2014 buffering filters hide live progress, and killing the pipeline mid-run orphans the run on the server. Redirect to a file instead (\`npx ripplo run <id> > /tmp/run.log 2>&1\` and Read it), or use \`run_in_background: true\`.`)});function Wd(e){return qd(e)?{kind:"replay",reason:"Running `ripplo explore replay` requires the `/ripplo:fuzz` skill loaded first. Load `/ripplo:fuzz` then retry \u2014 it carries the triage loop and the add-vs-weaken guardrail for resolving findings.",skill:"fuzz"}:Bd(e)?{kind:"run",reason:"Running `ripplo run` requires the `/ripplo:run` skill loaded first. Load `/ripplo:run` then retry \u2014 it carries the artifact-read order, the no-grep-piping guidance, the failure decision tree, and the caught-bug filing contract.",skill:Md}:null}function qd(e){return e.some((r,t)=>r==="ripplo"&&e[t+1]==="explore"&&e[t+2]==="replay")}function Bd(e){return e.some((r,t)=>r==="ripplo"&&e[t+1]==="run")}function Vd(e){try{return Nd(e)}catch{return[]}}function Gd(e){return typeof e=="object"&&"op"in e&&e.op==="|"}function zd(e){let r=e.find((t,n)=>{let o=e[n-1];return o!=null&&Gd(o)&&typeof t=="string"&&Hd.has(t)});return typeof r=="string"?r:null}function Yo(e){return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:e}}}import Jd from"path";import{z as ei}from"zod";var Kd=new Set(["Edit","Write","NotebookEdit"]),Qd=ei.looseObject({file_path:ei.string()}),Xd="create",ri=f("PreToolUse",e=>{if(!Kd.has(e.tool_name))return;let r=Qd.safeParse(e.tool_input);if(!r.success)return;let{cwd:t}=e;if(!m(t)||!v(t))return;let n=Jd.relative(t,r.data.file_path);if(!(n.startsWith("..")||n!==".ripplo"&&!n.startsWith(".ripplo/"))&&!Qe(t,e.session_id,Xd))return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`Editing \`.ripplo/\` files (${n}) requires the \`/ripplo:create\` skill loaded first. Load \`/ripplo:create\` then retry \u2014 it carries the DSL builder shape, lint rules, and the parallelization guidance you'll need.`}}});import Yd from"path";import ti from"picomatch";import{graphql as Zd}from"gql.tada";import{z as ni}from"zod";var ec=new Set(["Edit","Write","NotebookEdit"]),rc=ni.looseObject({file_path:ni.string()}),tc=Zd(`
434
+ `);async function Ko({cwd:e,lockfile:r}){if(!v(e))return{addedSlugs:[]};let n=kd(e);if(n.length===0)return{addedSlugs:[]};let t=new Set(n),o=r.workflows.filter(l=>l.sourcePath!=null&&t.has(l.sourcePath)).map(l=>L(l.name));if(o.length===0)return{addedSlugs:[]};let i=w(e).unwrapOr(void 0);return i==null?{addedSlugs:[]}:await Ln({config:i,cwd:e,lockfile:r}).catch(l=>($.warn("auto-sync failed: %s",l instanceof Error?l.message:String(l)),null))==null?{addedSlugs:[]}:{addedSlugs:await yd({cfg:i,slugs:o})?o:[]}}async function yd({cfg:e,slugs:r}){let t=(await u({config:e,document:hd,variables:{cwd:e.cwd,projectId:e.projectId,workflowSlugs:[...r]}}).catch(o=>($.warn("auto-scope failed: %s",o instanceof Error?o.message:String(o)),null)))?.addDirtyTestsToScope;return t?.__typename!=="MutationAddDirtyTestsToScopeSuccess"?!1:t.data.length>0}var Jo=".ripplo/workflows/";function kd(e){let r;try{r=F(["status","--porcelain","--",".ripplo/workflows"],e)}catch{return[]}return r.split(`
435
+ `).map(n=>n.slice(3).trim()).filter(n=>n.startsWith(Jo)&&n.endsWith(".ts")).map(n=>n.slice(Jo.length)).filter(n=>n!=="index.ts"&&!n.endsWith("/index.ts"))}var vd=Qo.looseObject({file_path:Qo.string()}),Xo=f("PostToolUse",async e=>{let r=vd.safeParse(e.tool_input);if(!r.success||!/\/\.ripplo\/.*\.ts$/.test(r.data.file_path))return;let{cwd:n}=e;if(!m(n))return;if(!v(n))return{hookSpecificOutput:{additionalContext:"Ripplo hooks are paused \u2014 DSL lint and lockfile sync did not run for this edit. Resume with `npx ripplo hooks resume` when ready.",hookEventName:"PostToolUse"}};let t=await Q(n);if(t.isErr())return{decision:"block",reason:`${x(t.error)}
436
+ ${c("create","DSL authoring + lint rules")}`};let o=H(t.value);if(o.length>0)return{decision:"block",reason:`${W(o)}
437
+ ${c("create")}`};let{addedSlugs:i}=await Ko({cwd:n,lockfile:t.value});return bd([...i.length>0?[`Auto-scoped ${i.join(", ")} (dirty tests).`]:[],...Sd(n,t.value)])});function bd(e){if(e.length!==0)return{hookSpecificOutput:{additionalContext:e.join(`
438
+ `),hookEventName:"PostToolUse"}}}function Sd(e,r){let n=Ae(r),t=wd("sha256").update(JSON.stringify(n)).digest("hex");return oe(e,"coverage-warn")===t?[]:(ie(e,"coverage-warn",t),n.length===0?[]:[Pe(n)])}import{z as Yo}from"zod";var xd=Yo.looseObject({command:Yo.string()}),Rd=/\bripplo\s+hooks\s+pause\b/,Zo=f("PreToolUse",e=>{if(e.tool_name!=="Bash")return;let r=xd.safeParse(e.tool_input);if(!r.success||!Rd.test(r.data.command))return;let{cwd:n}=e;if(m(n))return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:"`ripplo hooks pause` is a human-only escape hatch \u2014 agents can't bypass Ripplo guardrails on their own. If the daemon genuinely can't start (auth, server down, intentional offline work), surface the blocker to the user and ask them to run `npx ripplo hooks pause` themselves from their terminal."}}});import{parse as Id}from"shell-quote";import{z as ri}from"zod";import{existsSync as Cd,mkdirSync as Pd,rmSync as Iy,writeFileSync as Ed}from"fs";import yr from"path";function Je(e,r,n){let t=ei(e,r,n);Pd(yr.dirname(t),{recursive:!0}),Ed(t,"")}function Ke(e,r,n){return Cd(ei(e,r,n))}function ei(e,r,n){return yr.join($d(e,r),n)}function $d(e,r){return yr.join(e,".ripplo",".local","skills-loaded",r)}var jd=ri.looseObject({command:ri.string()}),Ad="run",Ld=new Set(["tail","head","less","more","wc","sort","uniq","awk","sed","grep"]),ti=f("PreToolUse",e=>{if(e.tool_name!=="Bash")return;let r=jd.safeParse(e.tool_input);if(!r.success)return;let n=_d(r.data.command),t=Td(n);if(t==null)return;let{cwd:o}=e;if(!m(o)||!v(o))return;if(!Ke(o,e.session_id,t.skill))return ni(t.reason);let i=t.kind==="run"?Nd(n):null;if(i!=null)return ni(`Don't pipe \`ripplo run\` through \`${i}\` \u2014 buffering filters hide live progress, and killing the pipeline mid-run orphans the run on the server. Redirect to a file instead (\`npx ripplo run <id> > /tmp/run.log 2>&1\` and Read it), or use \`run_in_background: true\`.`)});function Td(e){return Dd(e)?{kind:"replay",reason:"Running `ripplo explore replay` requires the `/ripplo:fuzz` skill loaded first. Load `/ripplo:fuzz` then retry \u2014 it carries the triage loop and the add-vs-weaken guardrail for resolving findings.",skill:"fuzz"}:Od(e)?{kind:"run",reason:"Running `ripplo run` requires the `/ripplo:run` skill loaded first. Load `/ripplo:run` then retry \u2014 it carries the artifact-read order, the no-grep-piping guidance, the failure decision tree, and the caught-bug filing contract.",skill:Ad}:null}function Dd(e){return e.some((r,n)=>r==="ripplo"&&e[n+1]==="explore"&&e[n+2]==="replay")}function Od(e){return e.some((r,n)=>r==="ripplo"&&e[n+1]==="run")}function _d(e){try{return Id(e)}catch{return[]}}function Fd(e){return typeof e=="object"&&"op"in e&&e.op==="|"}function Nd(e){let r=e.find((n,t)=>{let o=e[t-1];return o!=null&&Fd(o)&&typeof n=="string"&&Ld.has(n)});return typeof r=="string"?r:null}function ni(e){return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:e}}}import Ud from"path";import{z as oi}from"zod";var Md=new Set(["Edit","Write","NotebookEdit"]),Hd=oi.looseObject({file_path:oi.string()}),Wd="create",ii=f("PreToolUse",e=>{if(!Md.has(e.tool_name))return;let r=Hd.safeParse(e.tool_input);if(!r.success)return;let{cwd:n}=e;if(!m(n)||!v(n))return;let t=Ud.relative(n,r.data.file_path);if(!(t.startsWith("..")||t!==".ripplo"&&!t.startsWith(".ripplo/"))&&!Ke(n,e.session_id,Wd))return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`Editing \`.ripplo/\` files (${t}) requires the \`/ripplo:create\` skill loaded first. Load \`/ripplo:create\` then retry \u2014 it carries the DSL builder shape, lint rules, and the parallelization guidance you'll need.`}}});import qd from"path";import si from"picomatch";import{graphql as Bd}from"gql.tada";import{z as ai}from"zod";var Vd=new Set(["Edit","Write","NotebookEdit"]),Gd=ai.looseObject({file_path:ai.string()}),zd=Bd(`
451
439
  query PreEditScopeGate($projectId: String!, $cwd: String!) {
452
440
  project(id: $projectId) {
453
441
  id
@@ -459,9 +447,9 @@ ${c("create")}`};let{addedSlugs:i}=await Vo({cwd:t,lockfile:n.value});return jd(
459
447
  }
460
448
  }
461
449
  }
462
- `),oi=f("PreToolUse",async e=>{if(!ec.has(e.tool_name))return;let r=rc.safeParse(e.tool_input);if(!r.success)return;let{cwd:t}=e;if(!m(t)||!v(t))return;let n=Yd.relative(t,r.data.file_path);if(n.startsWith("..")||n===".ripplo"||n.startsWith(".ripplo/")||!nc(n))return;let o=await oc(t);return o.populated?o.degradedReason!=null?{hookSpecificOutput:{additionalContext:`Scope check skipped (${o.degradedReason}) \u2014 edit allowed through, but the scope guardrail isn't enforcing on this edit.`,hookEventName:"PreToolUse"}}:void 0:{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`Scope is empty but this edit touches app code (${n}). Stub a test or \`scope add\` an existing one before proceeding \u2014 or acknowledge "no user-facing behavior" if pure refactor. Hook re-fires until scope is populated (or hooks paused via the web UI). ${Dr(["run","create"])}`}}});function nc(e){let{ignorePaths:r,watchPaths:t}=oe(),n=ti([...t]),o=ti([...r]);return n(e)&&!o(e)}async function oc(e){let r=w(e).unwrapOr(void 0);if(r==null)return{degradedReason:"no project config \u2014 `npx ripplo init` not run here",populated:!0};let t=await u({config:r,document:tc,variables:{cwd:r.cwd,projectId:r.projectId}}).catch(()=>null);return t==null?{degradedReason:"server unreachable",populated:!0}:{degradedReason:null,populated:(t.project?.devSession?.scopeItems??[]).length>0}}import ic from"fs";import sc from"path";import{z as ii}from"zod";var ac=new Set(["Edit","Write","NotebookEdit"]),lc=ii.looseObject({file_path:ii.string()}),si=f("PreToolUse",async e=>{if(!ac.has(e.tool_name))return;let r=dc(e);if(r==null)return;let{cwd:t}=e;if(ic.existsSync(le(t))||ve(t))return;let n=await S(t);return n.isErr()?{hookSpecificOutput:{additionalContext:`\`ripplo daemon\` isn't running and the DSL is currently failing to compile, so the daemon gate is letting this edit through \u2014 fix the compile error before relying on dev-mode guardrails. Compile error:
463
- ${x(n.error)}
464
- ${c("create","DSL authoring + lint rules")}`,hookEventName:"PreToolUse"}}:{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`\`ripplo daemon\` is not running \u2014 this edit to \`${r}\` won't sync and dev-mode guardrails won't fire. Run \`/ripplo:start\`, then retry (\`npx ripplo doctor\` checks the daemon and dev server). If the daemon can't start, the user can bypass with \`npx ripplo hooks pause\`. ${c("start")}`}}});function dc(e){let r=lc.safeParse(e.tool_input);if(!r.success||!m(e.cwd))return null;let t=sc.relative(e.cwd,r.data.file_path);return t.startsWith("..")||t!==".ripplo"&&!t.startsWith(".ripplo/")?null:t}import{graphql as cc}from"gql.tada";var pc=cc(`
450
+ `),li=f("PreToolUse",async e=>{if(!Vd.has(e.tool_name))return;let r=Gd.safeParse(e.tool_input);if(!r.success)return;let{cwd:n}=e;if(!m(n)||!v(n))return;let t=qd.relative(n,r.data.file_path);if(t.startsWith("..")||t===".ripplo"||t.startsWith(".ripplo/")||!Jd(t))return;let o=await Kd(n);return o.populated?o.degradedReason!=null?{hookSpecificOutput:{additionalContext:`Scope check skipped (${o.degradedReason}) \u2014 edit allowed through, but the scope guardrail isn't enforcing on this edit.`,hookEventName:"PreToolUse"}}:void 0:{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`Scope is empty but this edit touches app code (${t}). Stub a test or \`scope add\` an existing one before proceeding \u2014 or acknowledge "no user-facing behavior" if pure refactor. Hook re-fires until scope is populated (or hooks paused via the web UI). ${Tr(["run","create"])}`}}});function Jd(e){let{ignorePaths:r,watchPaths:n}=te(),t=si([...n]),o=si([...r]);return t(e)&&!o(e)}async function Kd(e){let r=w(e).unwrapOr(void 0);if(r==null)return{degradedReason:"no project config \u2014 `npx ripplo init` not run here",populated:!0};let n=await u({config:r,document:zd,variables:{cwd:r.cwd,projectId:r.projectId}}).catch(()=>null);return n==null?{degradedReason:"server unreachable",populated:!0}:{degradedReason:null,populated:(n.project?.devSession?.scopeItems??[]).length>0}}import Qd from"fs";import Xd from"path";import{z as di}from"zod";var Yd=new Set(["Edit","Write","NotebookEdit"]),Zd=di.looseObject({file_path:di.string()}),ci=f("PreToolUse",async e=>{if(!Yd.has(e.tool_name))return;let r=ec(e);if(r==null)return;let{cwd:n}=e;if(Qd.existsSync(ae(n))||we(n))return;let t=await S(n);return t.isErr()?{hookSpecificOutput:{additionalContext:`\`ripplo daemon\` isn't running and the DSL is currently failing to compile, so the daemon gate is letting this edit through \u2014 fix the compile error before relying on dev-mode guardrails. Compile error:
451
+ ${x(t.error)}
452
+ ${c("create","DSL authoring + lint rules")}`,hookEventName:"PreToolUse"}}:{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`\`ripplo daemon\` is not running \u2014 this edit to \`${r}\` won't sync and dev-mode guardrails won't fire. Run \`/ripplo:start\`, then retry (\`npx ripplo doctor\` checks the daemon and dev server). If the daemon can't start, the user can bypass with \`npx ripplo hooks pause\`. ${c("start")}`}}});function ec(e){let r=Zd.safeParse(e.tool_input);if(!r.success||!m(e.cwd))return null;let n=Xd.relative(e.cwd,r.data.file_path);return n.startsWith("..")||n!==".ripplo"&&!n.startsWith(".ripplo/")?null:n}import{graphql as rc}from"gql.tada";var nc=rc(`
465
453
  query ScopeReminder($projectId: String!, $cwd: String!) {
466
454
  project(id: $projectId) {
467
455
  id
@@ -478,16 +466,16 @@ ${c("create","DSL authoring + lint rules")}`,hookEventName:"PreToolUse"}}:{hookS
478
466
  }
479
467
  }
480
468
  }
481
- `),ai=f("UserPromptSubmit",async e=>{let{cwd:r}=e;if(!m(r)||!v(r))return;let t=Je(r);if(ie(r,"scope-reminder")===t)return;let n=w(r).unwrapOr(void 0);if(n==null)return;let o=await u({config:n,document:pc,variables:{cwd:n.cwd,projectId:n.projectId}}).catch(()=>null);se(r,"scope-reminder",t);let i=o?.project?.devSession?.scopeItems??[];return To({cwd:r,scope:{available:o!=null,items:i.map(s=>uc(s))}}),o==null?{hookSpecificOutput:{additionalContext:`Ripplo scope: unknown \u2014 server unreachable, scope guardrails are not enforcing. Check the dev session (\`npx ripplo doctor\`). ${c("start")}`,hookEventName:"UserPromptSubmit"}}:{hookSpecificOutput:{additionalContext:i.length===0?`Ripplo scope: empty. ${c("run")}`:`Ripplo scope (${String(i.length)}):
469
+ `),pi=f("UserPromptSubmit",async e=>{let{cwd:r}=e;if(!m(r)||!v(r))return;let n=ze(r);if(oe(r,"scope-reminder")===n)return;let t=w(r).unwrapOr(void 0);if(t==null)return;let o=await u({config:t,document:nc,variables:{cwd:t.cwd,projectId:t.projectId}}).catch(()=>null);ie(r,"scope-reminder",n);let i=o?.project?.devSession?.scopeItems??[];return Fo({cwd:r,scope:{available:o!=null,items:i.map(s=>tc(s))}}),o==null?{hookSpecificOutput:{additionalContext:`Ripplo scope: unknown \u2014 server unreachable, scope guardrails are not enforcing. Check the dev session (\`npx ripplo doctor\`). ${c("start")}`,hookEventName:"UserPromptSubmit"}}:{hookSpecificOutput:{additionalContext:i.length===0?`Ripplo scope: empty. ${c("run")}`:`Ripplo scope (${String(i.length)}):
482
470
  ${i.map(s=>{let l=s.workflow;return l==null?` [intent] (${s.id}) ${s.label??""}`:` [${l.spec==null?"stub":"implemented"}] (${s.id}) ${l.slug}`}).join(`
483
- `)}`,hookEventName:"UserPromptSubmit"}}});function uc(e){let r=e.workflow;return r==null?{label:e.label,slug:null,status:"intent"}:{label:e.label,slug:r.slug,status:r.spec==null?"stub":"implemented"}}var mc="# Ripplo \u2014 always-on session context\n\nEvery user-facing change in this repo ships with a deterministic, backend-aware test that proves it works end-to-end.\n\n## Load the right skill before acting \u2014 this is critical\n\nThe skills below carry the procedural detail you need to do Ripplo work correctly: entity/world modeling, parallel-isolation in engine impls, backend state assertions, scope discipline, artifact-read order on failures, parallelization patterns. **None of that is reproduced in this preamble** \u2014 only the always-on guardrails are. If you act on a Ripplo task without loading the matching skill, you will skip rules that exist to prevent specific past failure modes (cross-run data leakage, \"passing\" tests that never asserted backend state, etc.).\n\nMatch the task to a skill from the triage table \u2014 if unsure between two, load both. If completely unsure, load `/ripplo:discover` or `/ripplo:create`. Loading is cheap. Acting under-informed is not.\n\n## Skill triage\n\n- Load `/ripplo:setup` skill for instructions on initializing Ripplo in a project; `npx ripplo doctor` reports the engine endpoint missing.\n- Load `/ripplo:discover` skill for instructions on planning/stubbing test coverage for a new project or new feature area.\n- Load `/ripplo:create` skill for instructions on authoring a single test spec for a user flow.\n- Load `/ripplo:run` skill for instructions on executing tests, managing the testing scope this session is responsible for, diagnosing a failed run (read artifacts in `.ripplo/debug/<runId>/` before re-running), and filing caught app bugs.\n- Load `/ripplo:fuzz` skill for instructions on triaging findings from the background explorer's findings log.\n\n## Universal rules\n\n- **Two background processes must be running before feature work.** (1) The app's dev server, so the app is reachable; (2) `npx ripplo daemon` (run `/ripplo:start` to spawn it, or invoke it directly via Bash with `run_in_background`), so the dev session is live. Without the daemon, dev-mode hooks don't arm (`ripplo run` will auto-start one if absent). Without the dev server, runs fail when they try to hit the app. `npx ripplo doctor` reports both.\n- **Two funnels.** Definitions \u2192 `createRipplo({ entities, singletons, tests })` in `.ripplo/index.ts`. Implementations \u2192 `createEngine(ripplo, { entities: impls, singletons })` in your app server's `test/engine.ts` \u2014 one `seed`/`read` impl per entity. Never call either elsewhere. TS enforces exhaustiveness across both.\n- **\"Done\" = app code delivers the behavior AND a passing test proves it.** Both halves. Shipping without a test isn't done; writing a test against broken UI/API isn't done.\n- **Scope is your job.** For any non-trivial change, enumerate every flow it could affect and either `scope add` existing tests or stub new ones with `test(\"Intent\")` (no body). Don't wait to be told.\n- **`scope remove` is never for size/effort.** Valid only when an item is genuinely out of scope (wrong flow, duplicate, user said \"not this session,\" feature cut). \"Too many stubs\" \u2192 parallelize with subagents. Don't present \"implement vs. remove\" as a neutral A/B.\n- **Stub gates are not a question.** When `stop-enforce` blocks on unimplemented stubs, implement them \u2014 don't ask the user \"implement or defer?\", don't propose pausing hooks as option B. The fix isn't done until the test is. New scaffolding (entity, world, engine impl) is in-scope work, not follow-up.\n- **Backend assertions are mandatory on mutations.** Every mutation step carries an `Entity.created/updated/deleted` in its `.expect(...)` \u2014 Ripplo checks your app's state against what the test expected. A UI-only check on a mutation ships the bug as green.\n- **Never weaken a test to make it pass.** No `contains`/regex for exact text, no removed assertions, no fabricated locators. App lacks an accessible name \u2192 add one to the app, don't fall back to `testId()`. App bug \u2192 report with evidence.\n- **Artifacts first, re-run last.** Failed runs write `.ripplo/debug/<runId>/behavior.jsonl` \u2014 a causal stream of actions, assertions, rrweb DOM, console, network, and server spans \u2014 and the run output renders the findings. Read them. Never pipe `ripplo run` through `grep`/`tail`/`head`. Form a hypothesis citing an event, make one change, re-run once.\n- **`.ripplo/ripplo.lock` is committed, never hand-edited.** `ripplo lint` / `ripplo compile` regenerates it. Pre-commit runs `ripplo compile --check`.\n- **Scratch files live in `.ripplo/.local/`.** Never loose in `.ripplo/`.\n- **Worktrees are self-contained.** Each worktree has its own `.ripplo/` checkout, DevSession, scope, and debug artifacts. Auth and projectId are shared globally. Env files (typically gitignored) won't carry over to a fresh worktree \u2014 copy from main or point at a shared file. **If sibling worktrees run dev servers on different ports, the worktree's env file must update both `RIPPLO_APP_URL` and `RIPPLO_ENGINE_URL` to match the port that worktree's dev server is bound to** (e.g. main on `:3000`, this worktree on `:3001` \u2192 set `RIPPLO_APP_URL=http://localhost:3001` and `RIPPLO_ENGINE_URL=http://localhost:3001/ripplo` in the worktree's env file). Mismatched ports = `npx ripplo daemon` talks to the wrong server, runs silently fail or hit the sibling worktree's app.\n- **DSL locators are semantic.** Use `role`/`button`/`textbox`/`heading`/`link`; `testId` only when no ARIA role exists.\n- **New backend state?** Add an `entity(...)` in `.ripplo/entities/` and a `seed`/`read` impl in your app's engine funnel (TS flags the missing impl).\n\n## Key files\n\n- `.ripplo/index.ts` \u2014 `createRipplo` call.\n- `.ripplo/{entities,singletons,worlds,tests}/index.ts` \u2014 registry aggregators.\n- `.ripplo/project.json` \u2014 project id + env-file pointers.\n- `<app>/src/test/engine.ts` \u2014 single impl funnel via `createEngine`.\n- `/ripplo:create` skill \u2014 DSL reference (entities, worlds, tests, backend assertions). Full primitive catalog at `node_modules/@ripplo/testing/DSL.md`.\n",li=f("SessionStart",e=>{if(m(e.cwd))return{hookSpecificOutput:{additionalContext:mc,hookEventName:"SessionStart"}}});import fc from"path";import gc from"process";import{CancellationTokenSource as hc}from"vscode-jsonrpc/node";import{graphql as yc}from"gql.tada";function di(e){return`--- Ripplo Run Failures (scope) ---
471
+ `)}`,hookEventName:"UserPromptSubmit"}}});function tc(e){let r=e.workflow;return r==null?{label:e.label,slug:null,status:"intent"}:{label:e.label,slug:r.slug,status:r.spec==null?"stub":"implemented"}}var oc="# Ripplo \u2014 always-on session context\n\nEvery user-facing change in this repo ships with a deterministic, backend-aware test that proves it works end-to-end.\n\n## Load the right skill before acting \u2014 this is critical\n\nThe skills below carry the procedural detail you need to do Ripplo work correctly: entity/world modeling, parallel-isolation in engine impls, backend state assertions, scope discipline, artifact-read order on failures, parallelization patterns. **None of that is reproduced in this preamble** \u2014 only the always-on guardrails are. If you act on a Ripplo task without loading the matching skill, you will skip rules that exist to prevent specific past failure modes (cross-run data leakage, \"passing\" tests that never asserted backend state, etc.).\n\nMatch the task to a skill from the triage table \u2014 if unsure between two, load both. If completely unsure, load `/ripplo:discover` or `/ripplo:create`. Loading is cheap. Acting under-informed is not.\n\n## Skill triage\n\n- Load `/ripplo:setup` skill for instructions on initializing Ripplo in a project; `npx ripplo doctor` reports the engine endpoint missing.\n- Load `/ripplo:discover` skill for instructions on planning/stubbing test coverage for a new project or new feature area.\n- Load `/ripplo:create` skill for instructions on authoring a single test spec for a user flow.\n- Load `/ripplo:run` skill for instructions on executing tests, managing the testing scope this session is responsible for, diagnosing a failed run (read artifacts in `.ripplo/debug/<runId>/` before re-running), and filing caught app bugs.\n- Load `/ripplo:fuzz` skill for instructions on triaging findings from the background explorer's findings log.\n\n## Universal rules\n\n- **Two background processes must be running before feature work.** (1) The app's dev server, so the app is reachable; (2) `npx ripplo daemon` (run `/ripplo:start` to spawn it, or invoke it directly via Bash with `run_in_background`), so the dev session is live. Without the daemon, dev-mode hooks don't arm (`ripplo run` will auto-start one if absent). Without the dev server, runs fail when they try to hit the app. `npx ripplo doctor` reports both.\n- **Two funnels.** Definitions \u2192 `createRipplo({ entities, singletons, tests })` in `.ripplo/index.ts`. Implementations \u2192 `createEngine(ripplo, { entities: impls, singletons })` in your app server's `test/engine.ts` \u2014 one `seed`/`read` impl per entity. Never call either elsewhere. TS enforces exhaustiveness across both.\n- **\"Done\" = app code delivers the behavior AND a passing test proves it.** Both halves. Shipping without a test isn't done; writing a test against broken UI/API isn't done.\n- **Scope is your job.** For any non-trivial change, enumerate every flow it could affect and either `scope add` existing tests or stub new ones with `test(\"Intent\")` (no body). Don't wait to be told.\n- **`scope remove` is never for size/effort.** Valid only when an item is genuinely out of scope (wrong flow, duplicate, user said \"not this session,\" feature cut). \"Too many stubs\" \u2192 parallelize with subagents. Don't present \"implement vs. remove\" as a neutral A/B.\n- **Stub gates are not a question.** When `stop-enforce` blocks on unimplemented stubs, implement them \u2014 don't ask the user \"implement or defer?\", don't propose pausing hooks as option B. The fix isn't done until the test is. New scaffolding (entity, world, engine impl) is in-scope work, not follow-up.\n- **Backend assertions are mandatory on mutations.** Every mutation step carries an `Entity.created/updated/deleted` in its `.expect(...)` \u2014 Ripplo checks your app's state against what the test expected. A UI-only check on a mutation ships the bug as green.\n- **Never weaken a test to make it pass.** No `contains`/regex for exact text, no removed assertions, no fabricated locators. App lacks an accessible name \u2192 add one to the app, don't fall back to `testId()`. App bug \u2192 report with evidence.\n- **Artifacts first, re-run last.** Failed runs write `.ripplo/debug/<runId>/behavior.jsonl` \u2014 a causal stream of actions, assertions, rrweb DOM, console, network, and server spans \u2014 and the run output renders the findings. Read them. Never pipe `ripplo run` through `grep`/`tail`/`head`. Form a hypothesis citing an event, make one change, re-run once.\n- **`.ripplo/ripplo.lock` is committed, never hand-edited.** `ripplo lint` / `ripplo compile` regenerates it. Pre-commit runs `ripplo compile --check`.\n- **Scratch files live in `.ripplo/.local/`.** Never loose in `.ripplo/`.\n- **Worktrees are self-contained.** Each worktree has its own `.ripplo/` checkout, DevSession, scope, and debug artifacts. Auth and projectId are shared globally. Env files (typically gitignored) won't carry over to a fresh worktree \u2014 copy from main or point at a shared file. **If sibling worktrees run dev servers on different ports, the worktree's env file must update both `RIPPLO_APP_URL` and `RIPPLO_ENGINE_URL` to match the port that worktree's dev server is bound to** (e.g. main on `:3000`, this worktree on `:3001` \u2192 set `RIPPLO_APP_URL=http://localhost:3001` and `RIPPLO_ENGINE_URL=http://localhost:3001/ripplo` in the worktree's env file). Mismatched ports = `npx ripplo daemon` talks to the wrong server, runs silently fail or hit the sibling worktree's app.\n- **DSL locators are semantic.** Use `role`/`button`/`textbox`/`heading`/`link`; `testId` only when no ARIA role exists.\n- **New backend state?** Add an `entity(...)` in `.ripplo/entities/` and a `seed`/`read` impl in your app's engine funnel (TS flags the missing impl).\n\n## Key files\n\n- `.ripplo/index.ts` \u2014 `createRipplo` call.\n- `.ripplo/{entities,singletons,worlds,tests}/index.ts` \u2014 registry aggregators.\n- `.ripplo/project.json` \u2014 project id + env-file pointers.\n- `<app>/src/test/engine.ts` \u2014 single impl funnel via `createEngine`.\n- `/ripplo:create` skill \u2014 DSL reference (entities, worlds, tests, backend assertions). Full primitive catalog at `node_modules/@ripplo/testing/DSL.md`.\n",ui=f("SessionStart",e=>{if(m(e.cwd))return{hookSpecificOutput:{additionalContext:oc,hookEventName:"SessionStart"}}});import ic from"path";import sc from"process";import{CancellationTokenSource as ac}from"vscode-jsonrpc/node";import{graphql as lc}from"gql.tada";function mi(e){return`--- Ripplo Run Failures (scope) ---
484
472
  ${e.join(`
485
473
  `)}
486
- Artifacts: .ripplo/debug/<runId>/. ${c("run")}`}function wr({lines:e,retried:r}){let t=r?"Already retried once inside this gate \u2014 still unreachable.":"";return["--- Ripplo Run Not Verified (Ripplo server unreachable) ---",...e,"The Ripplo server was unreachable during these runs \u2014 a server-side transient. Don't debug the daemon, dev server, or auth (`npx ripplo doctor` will be green).",t,"Wait a moment, then stop again to re-verify."].filter(n=>n.length>0).join(`
487
- `)}function ci(e){return["--- Ripplo Run Rejected (server declined) ---",...e,"The server rejected these runs \u2014 not transient. Retrying won't clear it, and `npx ripplo doctor` will be green. Resolve the stated cause (plan limit, missing test, repo link), not the daemon or dev server."].join(`
488
- `)}function ye(e){return`--- Ripplo Run Could Not Execute ---
474
+ Artifacts: .ripplo/debug/<runId>/. ${c("run")}`}function kr({lines:e,retried:r}){let n=r?"Already retried once inside this gate \u2014 still unreachable.":"";return["--- Ripplo Run Not Verified (Ripplo server unreachable) ---",...e,"The Ripplo server was unreachable during these runs \u2014 a server-side transient. Don't debug the daemon, dev server, or auth (`npx ripplo doctor` will be green).",n,"Wait a moment, then stop again to re-verify."].filter(t=>t.length>0).join(`
475
+ `)}function fi(e){return["--- Ripplo Run Rejected (server declined) ---",...e,"The server rejected these runs \u2014 not transient. Retrying won't clear it, and `npx ripplo doctor` will be green. Resolve the stated cause (plan limit, missing test, repo link), not the daemon or dev server."].join(`
476
+ `)}function he(e){return`--- Ripplo Run Could Not Execute ---
489
477
  ${e}
490
- Fix the run environment (daemon, dev server, auth \u2014 \`npx ripplo doctor\`) and re-run before declaring work done. ${c("start")}`}var kc=yc(`
478
+ Fix the run environment (daemon, dev server, auth \u2014 \`npx ripplo doctor\`) and re-run before declaring work done. ${c("start")}`}var dc=lc(`
491
479
  query ScopeEnforce($projectId: String!, $cwd: String!) {
492
480
  project(id: $projectId) {
493
481
  id
@@ -506,26 +494,26 @@ Fix the run environment (daemon, dev server, auth \u2014 \`npx ripplo doctor\`)
506
494
  }
507
495
  }
508
496
  }
509
- `),ui=f("Stop",async e=>{let{cwd:r}=e;if(!m(r)||!v(r))return;let t=Je(r),n=ie(r,"stop-enforce")===t;if(n&&!e.stop_hook_active)return;let o=await wc(r);if(se(r,"stop-enforce",t),o.errors.length!==0)return n&&e.stop_hook_active&&!o.infraOnly?{continue:!1,stopReason:`Stop-enforce: same repo state across consecutive stop attempts \u2014 agent appears stuck. Errors:
497
+ `),hi=f("Stop",async e=>{let{cwd:r}=e;if(!m(r)||!v(r))return;let n=ze(r),t=oe(r,"stop-enforce")===n;if(t&&!e.stop_hook_active)return;let o=await cc(r);if(ie(r,"stop-enforce",n),o.errors.length!==0)return t&&e.stop_hook_active&&!o.infraOnly?{continue:!1,stopReason:`Stop-enforce: same repo state across consecutive stop attempts \u2014 agent appears stuck. Errors:
510
498
  ${o.errors.join(`
511
499
 
512
500
  `)}`}:{decision:"block",reason:o.errors.join(`
513
501
 
514
- `)}});async function wc(e){let r=await X(e);if(r.isErr())return{errors:[`--- Compilation failed ---
502
+ `)}});async function cc(e){let r=await Q(e);if(r.isErr())return{errors:[`--- Compilation failed ---
515
503
  ${x(r.error)}
516
- ${c("create")}`],infraOnly:!1};let t=r.value,n=await Sc(e,t),o=vc(t),i=bc(t),a=n.runnableSlugs.length>0?await Rc(e):null,s=[o,i,a?.error??null,n.error].filter(d=>d!=null),l=s.length>0&&o==null&&i==null&&n.error==null&&(a?.infra??!1);return{errors:s,infraOnly:l}}function vc(e){let r=W(e);return r.length===0?null:`--- Ripplo Lint ---
517
- ${q(r)}
518
- ${c("create")}`}function bc(e){let r=U(e);return r.length===0?null:`--- Unimplemented stubs ---
504
+ ${c("create")}`],infraOnly:!1};let n=r.value,t=await mc(e,n),o=pc(n),i=uc(n),a=t.runnableSlugs.length>0?await gc(e):null,s=[o,i,a?.error??null,t.error].filter(d=>d!=null),l=s.length>0&&o==null&&i==null&&t.error==null&&(a?.infra??!1);return{errors:s,infraOnly:l}}function pc(e){let r=H(e);return r.length===0?null:`--- Ripplo Lint ---
505
+ ${W(r)}
506
+ ${c("create")}`}function uc(e){let r=N(e);return r.length===0?null:`--- Unimplemented stubs ---
519
507
  ${r.join(", ")}
520
508
  Implement the stub now with \`workflow("Intent", () => ({ given, steps }))\`. Do not ask the user "implement or defer?" \u2014 that framing is forbidden by /ripplo:create. New scaffolding (entity, world, engine impl) is in-scope, not follow-up.
521
- ${c("create")}`}async function Sc(e,r){let t=new Set(U(r).map(p=>L(p))),n=new Set(r.workflows.map(p=>L(p.name))),o=(p,y)=>n.has(p)?t.has(p):xc(y),i=w(e).unwrapOr(void 0);if(i==null)return{error:`--- Testing Scope (not checked) ---
522
- No project config \u2014 \`npx ripplo init\` hasn't run here, so scope/stub done-checks are not enforcing. ${c("setup")}`,runnableSlugs:[]};let a=await u({config:i,document:kc,variables:{cwd:i.cwd,projectId:i.projectId}}).catch(()=>null);if(a==null)return{error:`--- Testing Scope (not checked) ---
509
+ ${c("create")}`}async function mc(e,r){let n=new Set(N(r).map(p=>L(p))),t=new Set(r.workflows.map(p=>L(p.name))),o=(p,y)=>t.has(p)?n.has(p):fc(y),i=w(e).unwrapOr(void 0);if(i==null)return{error:`--- Testing Scope (not checked) ---
510
+ No project config \u2014 \`npx ripplo init\` hasn't run here, so scope/stub done-checks are not enforcing. ${c("setup")}`,runnableSlugs:[]};let a=await u({config:i,document:dc,variables:{cwd:i.cwd,projectId:i.projectId}}).catch(()=>null);if(a==null)return{error:`--- Testing Scope (not checked) ---
523
511
  Ripplo server unreachable \u2014 scope/stub done-checks are not enforcing. Verify the dev session is live (\`npx ripplo doctor\`, ${c("start")}) before declaring work done.`,runnableSlugs:[]};let s=a.project?.devSession?.scopeItems??[],l=s.flatMap(p=>{let y=p.workflow;return y==null?[` [intent] ${p.label??"(no label)"} \u2014 write a test for this flow`]:o(y.slug,y.spec)?[` [stub] ${y.slug} \u2014 implement \`${y.name}\``]:[]}),d=s.flatMap(p=>p.workflow!=null&&!o(p.workflow.slug,p.workflow.spec)?[p.workflow.slug]:[]);return{error:l.length===0?null:`--- Testing Scope ---
524
512
  ${l.join(`
525
513
  `)}
526
- ${c("create")}`,runnableSlugs:d}}function xc(e){return typeof e=="object"&&e!=null&&Reflect.get(e,"stub")===!0}async function Rc(e){let r=gc.argv[1];if(r==null)return{error:ye("CLI entry missing (process.argv[1])"),infra:!1};let t=await Fe(e);if(t!=null){let i=Me(t);return Ne(t.serverUrl)?{error:ye(i),infra:!1}:{error:wr({lines:[i],retried:!1}),infra:!0}}let n=await te({cliEntry:r,cwd:e});if(n.isErr())return{error:ye(D(n.error)),infra:!1};let o=n.value;try{return await Cc({connection:o,cwd:e})}finally{o.spawned&&await ne(o),o.socket.destroy()}}async function Cc({connection:e,cwd:r}){let t=await pi({connection:e,cwd:r,tests:[]});if(t.kind==="transport")return{error:ye(t.message),infra:!1};let n=t.notRun.length>0?await pi({connection:e,cwd:r,tests:t.notRun.map(l=>l.id)}):null,o=n==null||n.kind==="transport"?t.notRun:n.notRun,i=n!=null&&n.kind==="done",a=[...t.failedLines,...i?n.failedLines:[]],s=[...t.rejected,...i?n.rejected:[]];return Pc({failedLines:a,notRun:o,rejected:s,retried:n!=null})}function Pc({failedLines:e,notRun:r,rejected:t,retried:n}){if(e.length===0&&r.length===0&&t.length===0)return{error:null,infra:!1};let o=e.length>0?di(e):null,i=t.length>0?ci(t):null,a=r.length>0?wr({lines:r.map(s=>s.line),retried:n}):null;return{error:[o,i,a].filter(s=>s!=null).join(`
514
+ ${c("create")}`,runnableSlugs:d}}function fc(e){return typeof e=="object"&&e!=null&&Reflect.get(e,"stub")===!0}async function gc(e){let r=sc.argv[1];if(r==null)return{error:he("CLI entry missing (process.argv[1])"),infra:!1};let n=await _e(e);if(n!=null){let i=Ue(n);return Fe(n.serverUrl)?{error:he(i),infra:!1}:{error:kr({lines:[i],retried:!1}),infra:!0}}let t=await re({cliEntry:r,cwd:e});if(t.isErr())return{error:he(D(t.error)),infra:!1};let o=t.value;try{return await hc({connection:o,cwd:e})}finally{o.spawned&&await ne(o),o.socket.destroy()}}async function hc({connection:e,cwd:r}){let n=await gi({connection:e,cwd:r,tests:[]});if(n.kind==="transport")return{error:he(n.message),infra:!1};let t=n.notRun.length>0?await gi({connection:e,cwd:r,tests:n.notRun.map(l=>l.id)}):null,o=t==null||t.kind==="transport"?n.notRun:t.notRun,i=t!=null&&t.kind==="done",a=[...n.failedLines,...i?t.failedLines:[]],s=[...n.rejected,...i?t.rejected:[]];return yc({failedLines:a,notRun:o,rejected:s,retried:t!=null})}function yc({failedLines:e,notRun:r,rejected:n,retried:t}){if(e.length===0&&r.length===0&&n.length===0)return{error:null,infra:!1};let o=e.length>0?mi(e):null,i=n.length>0?fi(n):null,a=r.length>0?kr({lines:r.map(s=>s.line),retried:t}):null;return{error:[o,i,a].filter(s=>s!=null).join(`
527
515
 
528
- `),infra:o==null}}async function pi({connection:e,cwd:r,tests:t}){let n=[],o=[],i=[],a=fc.join(r,".ripplo","debug"),s=new hc;return(await De({connection:e,request:{all:!1,headed:!1,tests:[...t]},token:s.token,onEvent:d=>{Ec({debugDir:a,event:d,failedLines:n,notRun:o,rejected:i})}})).match(d=>d.kind==="daemon-error"?{kind:"transport",message:He(d.error)}:{failedLines:n,kind:"done",notRun:o,rejected:i},d=>({kind:"transport",message:D(d)}))}function Ec({debugDir:e,event:r,failedLines:t,notRun:n,rejected:o}){if(r.kind!=="test-outcome"||r.outcome.kind==="pass")return;let i=Ue({debugDir:e,event:r})??r.testName;if(r.outcome.kind==="dispatch-error"){o.push(i);return}if(r.outcome.kind==="dispatch-timeout"||r.outcome.kind==="infra-error"){n.push({id:`${r.workflowName}/${r.testName}`,line:i});return}t.push(i)}import{z as mi}from"zod";var $c=mi.looseObject({skill:mi.string()}),fi=f("PostToolUse",e=>{if(e.tool_name!=="Skill")return;let r=$c.safeParse(e.tool_input);if(!r.success)return;let t=/^ripplo:(.+)$/.exec(r.data.skill);if(t==null)return;let n=t[1];n!=null&&m(e.cwd)&&Ke(e.cwd,e.session_id,n)});var Ic=/(?:^|\s)\/ripplo:([a-z][a-z0-9-]*)\b/gi,gi=f("UserPromptSubmit",e=>{m(e.cwd)&&[...e.prompt.matchAll(Ic)].map(r=>r[1]).filter(r=>r!=null).forEach(r=>{Ke(e.cwd,e.session_id,r)})});Gc();V(process.cwd());Pr();var hi={"exit-plan-gate":Io,"plan-reminder":jo,"post-edit-flag-stubs":Ho,"post-edit-lint":zo,"pre-bash-hooks-pause-gate":Ko,"pre-bash-run-gate":Zo,"pre-edit-ripplo-skill-gate":ri,"pre-edit-scope-gate":oi,"pre-edit-watch-gate":si,"scope-reminder":ai,"session-preamble":li,"stop-enforce":ui,"track-skill-load":fi,"track-skill-prompt":gi};async function Dc(){Ac({pkg:{name:"ripplo",version:P()}}).notify({message:_t()}),await Lc(Tc(process.argv)).scriptName("ripplo").version(P()).command(Nc()).command("concurrency [value]","Show or set max local concurrent runs (daemon applies live)",e=>e.positional("value",{type:"number"}),e=>Gt({value:e.value})).command("auth <subcommand>","Manage authentication",Vc).command("projects <subcommand>","Inspect Ripplo projects",Bc).command("hooks <subcommand>","Pause or resume Ripplo hooks",qc).command("init","Scaffold .ripplo/ in this project",e=>e.option("project",{type:"string"}).option("env",{type:"string"}).option("app-url",{type:"string"}).option("engine-url",{type:"string"}),e=>hn({appUrl:e["app-url"],engineUrl:e["engine-url"],envFile:e.env,projectId:e.project})).command("run [ids..]","Run tests locally via the daemon (auto-starts it if absent)",Hc,e=>Gn({all:e.all,headed:e.headed,ids:e.ids,keepAlive:e["keep-alive"]})).command(Mc()).command(Fc()).command(Uc()).command("lint","Static model analysis (cascade gaps + law conflicts; no live app)",()=>{},()=>yn()).command("sync","Push the compiled .ripplo/ resources to the server (no run)",()=>{},()=>lo()).command("compile","Compile the DSL and write .ripplo/ripplo.lock",e=>e.option("check",{default:!1,describe:"Exit non-zero if the lockfile is missing or stale (does not write)",type:"boolean"}),e=>Bt({check:e.check})).command("update","Update ripplo to the latest published version (and hand the daemon off to it)",()=>{},()=>ko()).command("doctor","Check project health",()=>{},()=>nn()).command("status","Report stub tests and daemon status",e=>e.option("format",{choices:["json","summary"],default:"json",describe:"Output format"}),e=>Eo({format:e.format})).command("scope <subcommand>","Manage testing scope",Wc).command("run-worker",!1,()=>{},()=>Qn()).command("hook <name>","Internal: run a Claude Code plugin hook",e=>e.positional("name",{choices:Object.keys(hi),demandOption:!0,type:"string"}),e=>Oc(e.name)).strict().help().parse()}async function Oc(e){let r=hi[e];r==null&&(process.stderr.write(`Unknown hook: ${e}
529
- `),process.exit(1));let t=await _c(),n=t.trim()===""?{}:JSON.parse(t),o=await r.run(n);o!=null&&process.stdout.write(JSON.stringify(o))}function _c(){return new Promise((e,r)=>{if(process.stdin.isTTY){e("");return}let t=[];process.stdin.on("data",n=>t.push(n)),process.stdin.on("end",()=>{e(Buffer.concat(t).toString("utf8"))}),process.stdin.on("error",r)})}Dc().catch(e=>{process.stderr.write(`${Nr(e)}
530
- `),process.exit(1)});function Fc(){return{command:"report-bug",describe:"Report a critical application bug caught while building or testing",builder:e=>e.option("kind",{choices:["new_feature_bug","regression","latent_bug"],demandOption:!0,describe:"new_feature_bug: broke the new thing being built; regression: broke previously working behavior; latent_bug: pre-existing bug exposed by new test coverage"}).option("title",{demandOption:!0,describe:"Short bug name",type:"string"}).option("root-cause",{demandOption:!0,describe:"What was actually wrong",type:"string"}).option("surfaced-by",{demandOption:!0,describe:"How the test/run exposed it (cite evidence)",type:"string"}).option("run",{demandOption:!0,describe:"Run id where it surfaced \u2014 links the bug to its replay. For an exploration finding, pass its explore-\u2026 id (recorded, but not replay-linkable)",type:"string"}).option("test",{describe:"Test id that surfaced it",type:"string"}),handler:e=>Vn({kind:e.kind,rootCause:e["root-cause"],runId:e.run,surfacedBy:e["surfaced-by"],testId:e.test,title:e.title})}}function Nc(){return{command:"daemon",describe:"Run the long-lived local executor (IPC socket + run subscription)",builder:e=>e.option("executor",{choices:["local","cloud"],default:"local",describe:"Run executor (local pool, or cloud fleet via a per-session tunnel)"}).option("explore",{default:!1,describe:"Opt in to background exploration when idle (experimental)",type:"boolean"}).option("exploreConcurrency",{default:2,describe:"Max concurrent background exploration trails (keeps the machine responsive)",type:"number"}),handler:e=>wo({executor:e.executor,explore:e.explore,exploreConcurrency:e.exploreConcurrency})}}function Uc(){return{command:"explore",describe:"Run state-space exploration trails in the foreground (app + engine must be running)",builder:e=>e.option("trails",{default:10,describe:"Number of trails to execute before stopping (stops early on saturation)",type:"number"}).option("headed",{default:!1,describe:"Watch trails in a visible browser (stop the daemon explorer first if it holds the machine lock)",type:"boolean"}).option("max-length",{describe:"Max steps per trail (default 12 \u2014 lower means shorter, easier-to-watch trails)",type:"number"}).command("findings [findingId]","List pending exploration findings, or show one finding's full detail",r=>r.positional("findingId",{type:"string"}).option("json",{default:!1,describe:"Emit the findings list as JSON",type:"boolean"}),r=>Nn({findingId:r.findingId,json:r.json})).command("replay <findingId>","Replay a finding's minimal trail \u2014 a clean replay resolves the finding and covers its targets",r=>r.positional("findingId",{demandOption:!0,type:"string"}),r=>Un({findingId:r.findingId})).command("dismiss <findingId>","Dismiss a finding you don't intend to act on \u2014 prune later to drop it from the log",r=>r.positional("findingId",{demandOption:!0,type:"string"}),r=>Mn({findingId:r.findingId})).command("prune","Rewrite the findings log, dropping dismissed findings and ones no longer in the model",r=>r,()=>Hn()),handler:e=>Wn({headed:e.headed,maxLength:e["max-length"],trails:e.trails})}}function Mc(){return{command:"snapshot <runId>",describe:"Render a PNG of the page at a point in a run's rrweb recording",builder:e=>e.positional("runId",{demandOption:!0,type:"string"}).option("at",{describe:"Moment to render: epoch-ms timestamp exactly as found in behavior.jsonl",type:"number"}).option("offset",{describe:"Moment to render: ms from the start of the recording (alternative to --at)",type:"number"}).conflicts("at","offset"),handler:e=>ao({at:e.at,offset:e.offset,runId:e.runId})}}function Hc(e){let r=[];return e.positional("ids",{array:!0,default:r,describe:"Test ids to run",type:"string"}).option("all",{default:!1,describe:"Run every test in the suite (expensive)",type:"boolean"}).option("headed",{default:!1,describe:"Run with a visible browser window",type:"boolean"}).option("keep-alive",{default:!1,describe:"Leave an auto-started daemon running after the batch",type:"boolean"})}function Wc(e){return e.command("status","Print the current scope",r=>r.option("format",{choices:["json","text"],default:"text",describe:"Output format"}),r=>xo({format:r.format})).command("add <test-ids..>","Bind one or more existing tests (stubs or implemented) to scope as agent intent",r=>{let t=[];return r.positional("test-ids",{array:!0,default:t,demandOption:!0,describe:"Slugs of existing workflows",type:"string"})},r=>Ro({testIds:r["test-ids"]})).command("link <id> <test-id>","Link an existing scope item to a test",r=>r.positional("id",{demandOption:!0,describe:"Scope item id",type:"string"}).positional("test-id",{demandOption:!0,describe:"Slug of the workflow to link",type:"string"}),r=>Co({id:r.id,testId:r["test-id"]})).command("remove <ids..>","Remove one or more scope items by id",r=>{let t=[];return r.positional("ids",{array:!0,default:t,demandOption:!0,describe:"Scope item ids",type:"string"})},r=>Po({ids:r.ids})).demandCommand(1)}function qc(e){return e.command("pause","Disable all Ripplo pre-edit gates and stop enforcement until resumed",()=>{},()=>on()).command("resume","Re-enable Ripplo hooks paused via `ripplo hooks pause`",()=>{},()=>sn()).demandCommand(1)}function Bc(e){return e.command("list","List projects you have access to (JSON)",()=>{},()=>qn()).demandCommand(1)}function Vc(e){return e.command("login","Authenticate via device flow",()=>{},()=>Ht()).command("status","Show authentication status",()=>{},()=>Wt()).command("logout","Remove the saved token",()=>{},()=>{qt()}).demandCommand(1)}function Gc(){let e=process.cwd(),r=vr(e);r!=null&&r!==e&&(process.chdir(r),process.stderr.write(`ripplo: resolved .ripplo/ at ${r}
531
- `))}export{Dc as main};
516
+ `),infra:o==null}}async function gi({connection:e,cwd:r,tests:n}){let t=[],o=[],i=[],a=ic.join(r,".ripplo","debug"),s=new ac;return(await Te({connection:e,request:{all:!1,headed:!1,tests:[...n]},token:s.token,onEvent:d=>{kc({debugDir:a,event:d,failedLines:t,notRun:o,rejected:i})}})).match(d=>d.kind==="daemon-error"?{kind:"transport",message:Me(d.error)}:{failedLines:t,kind:"done",notRun:o,rejected:i},d=>({kind:"transport",message:D(d)}))}function kc({debugDir:e,event:r,failedLines:n,notRun:t,rejected:o}){if(r.kind!=="test-outcome"||r.outcome.kind==="pass")return;let i=Ne({debugDir:e,event:r})??r.testName;if(r.outcome.kind==="dispatch-error"){o.push(i);return}if(r.outcome.kind==="dispatch-timeout"||r.outcome.kind==="infra-error"){t.push({id:`${r.workflowName}/${r.testName}`,line:i});return}n.push(i)}import{z as yi}from"zod";var wc=yi.looseObject({skill:yi.string()}),ki=f("PostToolUse",e=>{if(e.tool_name!=="Skill")return;let r=wc.safeParse(e.tool_input);if(!r.success)return;let n=/^ripplo:(.+)$/.exec(r.data.skill);if(n==null)return;let t=n[1];t!=null&&m(e.cwd)&&Je(e.cwd,e.session_id,t)});var vc=/(?:^|\s)\/ripplo:([a-z][a-z0-9-]*)\b/gi,wi=f("UserPromptSubmit",e=>{m(e.cwd)&&[...e.prompt.matchAll(vc)].map(r=>r[1]).filter(r=>r!=null).forEach(r=>{Je(e.cwd,e.session_id,r)})});Fc();B(process.cwd());Cr();var vi={"exit-plan-gate":To,"plan-reminder":Do,"post-edit-flag-stubs":Vo,"post-edit-lint":Xo,"pre-bash-hooks-pause-gate":Zo,"pre-bash-run-gate":ti,"pre-edit-ripplo-skill-gate":ii,"pre-edit-scope-gate":li,"pre-edit-watch-gate":ci,"scope-reminder":pi,"session-preamble":ui,"stop-enforce":hi,"track-skill-load":ki,"track-skill-prompt":wi};async function Cc(){Sc({pkg:{name:"ripplo",version:P()}}).notify({message:qn()}),await xc(Rc(process.argv)).scriptName("ripplo").version(P()).command(Ic()).command("concurrency [value]","Show or set max local concurrent runs (daemon applies live)",e=>e.positional("value",{type:"number"}),e=>Zn({value:e.value})).command("auth <subcommand>","Manage authentication",_c).command("projects <subcommand>","Inspect Ripplo projects",Oc).command("hooks <subcommand>","Pause or resume Ripplo hooks",Dc).command("init","Scaffold .ripplo/ in this project",e=>e.option("project",{type:"string"}).option("env",{type:"string"}).option("app-url",{type:"string"}).option("engine-url",{type:"string"}),e=>St({appUrl:e["app-url"],engineUrl:e["engine-url"],envFile:e.env,projectId:e.project})).command("run [ids..]","Run tests locally via the daemon (auto-starts it if absent)",Lc,e=>Yt({all:e.all,headed:e.headed,ids:e.ids,keepAlive:e["keep-alive"]})).command(Ac()).command($c()).command(jc()).command("lint","Static model analysis (cascade gaps + law conflicts; no live app)",()=>{},()=>xt()).command("sync","Push the compiled .ripplo/ resources to the server (no run)",()=>{},()=>mo()).command("compile","Compile the DSL and write .ripplo/ripplo.lock",e=>e.option("check",{default:!1,describe:"Exit non-zero if the lockfile is missing or stale (does not write)",type:"boolean"}),e=>Xn({check:e.check})).command("update","Update ripplo to the latest published version (and hand the daemon off to it)",()=>{},()=>So()).command("doctor","Check project health",()=>{},()=>ct()).command("status","Report stub tests and daemon status",e=>e.option("format",{choices:["json","summary"],default:"json",describe:"Output format"}),e=>Ao({format:e.format})).command("scope <subcommand>","Manage testing scope",Tc).command("run-worker",!1,()=>{},()=>no()).command("hook <name>","Internal: run a Claude Code plugin hook",e=>e.positional("name",{choices:Object.keys(vi),demandOption:!0,type:"string"}),e=>Pc(e.name)).strict().help().parse()}async function Pc(e){let r=vi[e];r==null&&(process.stderr.write(`Unknown hook: ${e}
517
+ `),process.exit(1));let n=await Ec(),t=n.trim()===""?{}:JSON.parse(n),o=await r.run(t);o!=null&&process.stdout.write(JSON.stringify(o))}function Ec(){return new Promise((e,r)=>{if(process.stdin.isTTY){e("");return}let n=[];process.stdin.on("data",t=>n.push(t)),process.stdin.on("end",()=>{e(Buffer.concat(n).toString("utf8"))}),process.stdin.on("error",r)})}Cc().catch(e=>{process.stderr.write(`${Fr(e)}
518
+ `),process.exit(1)});function $c(){return{command:"report-bug",describe:"Report a critical application bug caught while building or testing",builder:e=>e.option("kind",{choices:["new_feature_bug","regression","latent_bug"],demandOption:!0,describe:"new_feature_bug: broke the new thing being built; regression: broke previously working behavior; latent_bug: pre-existing bug exposed by new test coverage"}).option("title",{demandOption:!0,describe:"Short bug name",type:"string"}).option("root-cause",{demandOption:!0,describe:"What was actually wrong",type:"string"}).option("surfaced-by",{demandOption:!0,describe:"How the test/run exposed it (cite evidence)",type:"string"}).option("run",{demandOption:!0,describe:"Run id where it surfaced \u2014 links the bug to its replay. For an exploration finding, pass its explore-\u2026 id (recorded, but not replay-linkable)",type:"string"}).option("test",{describe:"Test id that surfaced it",type:"string"}),handler:e=>Xt({kind:e.kind,rootCause:e["root-cause"],runId:e.run,surfacedBy:e["surfaced-by"],testId:e.test,title:e.title})}}function Ic(){return{command:"daemon",describe:"Run the long-lived local executor (IPC socket + run subscription)",builder:e=>e.option("executor",{choices:["local","cloud"],default:"local",describe:"Run executor (local pool, or cloud fleet via a per-session tunnel)"}).option("explore",{default:!1,describe:"Opt in to background exploration when idle (experimental)",type:"boolean"}).option("exploreConcurrency",{default:2,describe:"Max concurrent background exploration trails (keeps the machine responsive)",type:"number"}),handler:e=>xo({executor:e.executor,explore:e.explore,exploreConcurrency:e.exploreConcurrency})}}function jc(){return{command:"explore",describe:"Run state-space exploration trails in the foreground (app + engine must be running)",builder:e=>e.option("trails",{default:10,describe:"Number of trails to execute before stopping (stops early on saturation)",type:"number"}).option("headed",{default:!1,describe:"Watch trails in a visible browser (stop the daemon explorer first if it holds the machine lock)",type:"boolean"}).option("max-length",{describe:"Max steps per trail (default 12 \u2014 lower means shorter, easier-to-watch trails)",type:"number"}).command("findings [findingId]","List pending exploration findings, or show one finding's full detail",r=>r.positional("findingId",{type:"string"}).option("json",{default:!1,describe:"Emit the findings list as JSON",type:"boolean"}),r=>Bt({findingId:r.findingId,json:r.json})).command("replay <findingId>","Replay a finding's minimal trail \u2014 a clean replay resolves the finding and covers its targets",r=>r.positional("findingId",{demandOption:!0,type:"string"}),r=>Vt({findingId:r.findingId})).command("dismiss <findingId>","Dismiss a finding you don't intend to act on \u2014 prune later to drop it from the log",r=>r.positional("findingId",{demandOption:!0,type:"string"}),r=>Gt({findingId:r.findingId})).command("prune","Rewrite the findings log, dropping dismissed findings and ones no longer in the model",r=>r,()=>zt()),handler:e=>Jt({headed:e.headed,maxLength:e["max-length"],trails:e.trails})}}function Ac(){return{command:"snapshot <runId>",describe:"Render a PNG of the page at a point in a run's rrweb recording",builder:e=>e.positional("runId",{demandOption:!0,type:"string"}).option("at",{describe:"Moment to render: epoch-ms timestamp exactly as found in behavior.jsonl",type:"number"}).option("offset",{describe:"Moment to render: ms from the start of the recording (alternative to --at)",type:"number"}).conflicts("at","offset"),handler:e=>uo({at:e.at,offset:e.offset,runId:e.runId})}}function Lc(e){let r=[];return e.positional("ids",{array:!0,default:r,describe:"Test ids to run",type:"string"}).option("all",{default:!1,describe:"Run every test in the suite (expensive)",type:"boolean"}).option("headed",{default:!1,describe:"Run with a visible browser window",type:"boolean"}).option("keep-alive",{default:!1,describe:"Leave an auto-started daemon running after the batch",type:"boolean"})}function Tc(e){return e.command("status","Print the current scope",r=>r.option("format",{choices:["json","text"],default:"text",describe:"Output format"}),r=>Eo({format:r.format})).command("add <test-ids..>","Bind one or more existing tests (stubs or implemented) to scope as agent intent",r=>{let n=[];return r.positional("test-ids",{array:!0,default:n,demandOption:!0,describe:"Slugs of existing workflows",type:"string"})},r=>$o({testIds:r["test-ids"]})).command("link <id> <test-id>","Link an existing scope item to a test",r=>r.positional("id",{demandOption:!0,describe:"Scope item id",type:"string"}).positional("test-id",{demandOption:!0,describe:"Slug of the workflow to link",type:"string"}),r=>Io({id:r.id,testId:r["test-id"]})).command("remove <ids..>","Remove one or more scope items by id",r=>{let n=[];return r.positional("ids",{array:!0,default:n,demandOption:!0,describe:"Scope item ids",type:"string"})},r=>jo({ids:r.ids})).demandCommand(1)}function Dc(e){return e.command("pause","Disable all Ripplo pre-edit gates and stop enforcement until resumed",()=>{},()=>pt()).command("resume","Re-enable Ripplo hooks paused via `ripplo hooks pause`",()=>{},()=>ut()).demandCommand(1)}function Oc(e){return e.command("list","List projects you have access to (JSON)",()=>{},()=>Kt()).demandCommand(1)}function _c(e){return e.command("login","Authenticate via device flow",()=>{},()=>Jn()).command("status","Show authentication status",()=>{},()=>Kn()).command("logout","Remove the saved token",()=>{},()=>{Qn()}).demandCommand(1)}function Fc(){let e=process.cwd(),r=wr(e);r!=null&&r!==e&&(process.chdir(r),process.stderr.write(`ripplo: resolved .ripplo/ at ${r}
519
+ `))}export{Cc as main};