ripplo 0.7.5 → 0.7.7

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{A as d,Aa as Ft,B as dt,Ba as Nt,C as R,Ca as Ut,D as pt,Da as Ht,E as ut,Ea as Mt,F as mt,Fa as Wt,G as ft,Ga as qt,H as E,Ha as Bt,I,Ia as Vt,J as gt,K as k,L as de,O as _,Oa as Gt,P as ht,Pa as te,Q as yt,Qa as Jt,R as Z,S as kt,T as wt,U as pe,V as vt,W as bt,Wa as re,X as ue,Xa as zt,Y as C,Ya as Ne,Z as St,_ as Rt,_a as Kt,a as Qe,aa as Ct,ab as Qt,b as D,ba as me,cb as Yt,d as Ye,e as Xe,eb as Xt,f as p,g as ce,ga as xt,gb as Zt,h as Ze,hb as er,i as Oe,ia as Pt,ib as tr,j as O,jb as rr,k as et,ka as Et,kb as nr,l as tt,la as $t,lb as or,m as rt,ma as ee,n as nt,na as w,nb as ir,o as ot,oa as It,p as it,pa as jt,q as W,r as st,ra as At,s as at,sa as Lt,t as q,u as lt,ua as _e,v as N,va as Fe,w as P,wa as Tt,x as L,xa as Dt,y as ct,ya as Ot,z as S,za as _t}from"./chunk-KABDPGL5.js";import Bl from"yargs";import{hideBin as Vl}from"yargs/helpers";import{readFileSync as ho}from"fs";import{z as sr}from"zod";var yo=sr.object({version:sr.string()});function ar(){let e=ho(new URL("../package.json",import.meta.url),"utf8");return yo.parse(JSON.parse(e)).version}import{graphql as Ao}from"gql.tada";import{exec as vo}from"child_process";import{createAuthClient as ko}from"better-auth/client";import{deviceAuthorizationClient as wo}from"better-auth/client/plugins";function lr({baseURL:e}){return ko({baseURL:e,fetchOptions:{headers:{"User-Agent":"Ripplo CLI"}},plugins:[wo()]})}import{err as cr,ok as bo}from"neverthrow";var So=5e3,dr="ripplo-cli";async function pr({onDeviceCode:e,url:t}){let r=t??gt().RIPPLO_SERVER_URL,n=lr({baseURL:r}),o=await n.device.code({client_id:dr});if(o.error!=null)return cr({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}),$o(s),(await Ro({authClient:n,deviceCode:i})).map(c=>(et(c),c))}async function Ro({authClient:e,deviceCode:t}){for(;;){await Po(So);let r=await e.device.token({client_id:dr,device_code:t,grant_type:"urn:ietf:params:oauth:grant-type:device_code"});if(r.data?.access_token!=null)return bo(r.data.access_token);if(r.error==null)continue;if(!xo(r.error.error))return cr({code:r.error.error,description:r.error.error_description,kind:"oauth-authorization-failed"})}}var Co=new Set(["authorization_pending","slow_down"]);function xo(e){return Co.has(e)}function Po(e){return new Promise(t=>{setTimeout(t,e)})}function Eo(){return process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open"}function $o(e){let t=Eo();vo(`${t} "${e}"`,()=>{})}function U({serverUrl:e,token:t}){return{appUrl:"",cwd:process.cwd(),engineUrl:"",projectId:"",ripploServerUrl:e,token:t,webhookSecret:""}}import Io from"fs";import jo from"path";function m(e){return Io.existsSync(jo.join(e,".ripplo"))}var Lo=Ao(`
2
+ import{$ as Rt,$a as Qt,A as d,Aa as Ft,B as dt,Ba as Nt,C as x,Ca as Ut,D as pt,Da as Ht,E as ut,Ea as Mt,F as mt,Fa as Wt,G as ft,Ga as qt,H as E,Ha as Bt,I,Ia as Vt,J as gt,Ja as Gt,K as w,L as de,O as _,P as ht,Pa as Jt,Q as yt,Qa as te,R as Z,Ra as zt,S as kt,T as wt,U as pe,V as vt,W as bt,X as ue,Xa as re,Y as R,Ya as Kt,Z as St,Za as Ne,_ as xt,a as Qe,b as D,ba as Ct,bb as Yt,ca as me,d as Ye,db as Xt,e as Xe,f as p,fb as Zt,g as ce,h as Ze,ha as Pt,hb as er,i as Oe,ib as tr,j as O,ja as Et,jb as rr,k as et,kb as nr,l as tt,la as $t,lb as or,m as rt,ma as It,mb as ir,n as nt,na as ee,o as ot,oa as v,ob as sr,p as it,pa as jt,q as W,qa as At,r as st,s as at,sa as Lt,t as q,ta as Tt,u as lt,v as N,va as _e,w as P,wa as Fe,x as L,xa as Dt,y as ct,ya as Ot,z as S,za as _t}from"./chunk-A6RTZMN6.js";import Jl from"yargs";import{hideBin as zl}from"yargs/helpers";import{readFileSync as yo}from"fs";import{z as ar}from"zod";var ko=ar.object({version:ar.string()});function lr(){let e=yo(new URL("../package.json",import.meta.url),"utf8");return ko.parse(JSON.parse(e)).version}import{graphql as Lo}from"gql.tada";import{exec as bo}from"child_process";import{createAuthClient as wo}from"better-auth/client";import{deviceAuthorizationClient as vo}from"better-auth/client/plugins";function cr({baseURL:e}){return wo({baseURL:e,fetchOptions:{headers:{"User-Agent":"Ripplo CLI"}},plugins:[vo()]})}import{err as dr,ok as So}from"neverthrow";var xo=5e3,pr="ripplo-cli";async function ur({onDeviceCode:e,url:t}){let r=t??gt().RIPPLO_SERVER_URL,n=cr({baseURL:r}),o=await n.device.code({client_id:pr});if(o.error!=null)return dr({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}),Io(s),(await Ro({authClient:n,deviceCode:i})).map(c=>(et(c),c))}async function Ro({authClient:e,deviceCode:t}){for(;;){await Eo(xo);let r=await e.device.token({client_id:pr,device_code:t,grant_type:"urn:ietf:params:oauth:grant-type:device_code"});if(r.data?.access_token!=null)return So(r.data.access_token);if(r.error==null)continue;if(!Po(r.error.error))return dr({code:r.error.error,description:r.error.error_description,kind:"oauth-authorization-failed"})}}var Co=new Set(["authorization_pending","slow_down"]);function Po(e){return Co.has(e)}function Eo(e){return new Promise(t=>{setTimeout(t,e)})}function $o(){return process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open"}function Io(e){let t=$o();bo(`${t} "${e}"`,()=>{})}function U({serverUrl:e,token:t}){return{appUrl:"",cwd:process.cwd(),engineUrl:"",projectId:"",ripploServerUrl:e,token:t,webhookSecret:""}}import jo from"fs";import Ao from"path";function m(e){return jo.existsSync(Ao.join(e,".ripplo"))}var To=Lo(`
3
3
  query AuthViewer {
4
4
  currentUser {
5
5
  name
6
6
  email
7
7
  }
8
8
  }
9
- `);async function ur(){let e=I(),t=O();if(t!=null&&await To(e,t))return;let n=(await pr({url:e,onDeviceCode:s=>{process.stdout.write(`Opening your browser to finish sign-in.
9
+ `);async function mr(){let e=I(),t=O();if(t!=null&&await Do(e,t))return;let n=(await ur({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 you to approve...
14
14
  `)}})).match(s=>s,s=>{process.stderr.write(`${E(s)}
15
15
  `),process.exit(1)}),o=await Ue({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}, ${Do(o.viewer)}.
16
+ `),process.stdout.write(o.kind==="ok"?`${a}, ${Oo(o.viewer)}.
17
17
  `:`${a}.
18
18
  `),process.stdout.write(`Session saved to ${Oe("token")}.
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 To(e,t){let r=await Ue({serverUrl:e,token:t});return r.kind==="ok"?(process.stdout.write(`Already signed in as ${r.viewer.email}. Run \`ripplo auth logout\` to switch.
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 Do(e,t){let r=await Ue({serverUrl:e,token:t});return r.kind==="ok"?(process.stdout.write(`Already signed in as ${r.viewer.email}. Run \`ripplo auth logout\` to switch.
20
20
  `),!0):(r.kind==="unreachable"&&(process.stdout.write(`Could not reach the Ripplo server at ${e} (${r.message}). Your saved session may still be valid \u2014 fix connectivity and retry.
21
21
  `),process.exit(1)),process.stdout.write(`Your saved session expired \u2014 let's sign you back in.
22
22
 
23
- `),!1)}async function mr(){let e=O();e==null&&(process.stdout.write("Not authenticated. Run `ripplo auth login`.\n"),process.exit(1));let t=I(),r=await Ue({serverUrl:t,token:e});r.kind==="unreachable"&&(process.stdout.write(`Could not reach the Ripplo server at ${t} (${r.message}). The token may still be valid \u2014 check the server / network and retry.
23
+ `),!1)}async function fr(){let e=O();e==null&&(process.stdout.write("Not authenticated. Run `ripplo auth login`.\n"),process.exit(1));let t=I(),r=await Ue({serverUrl:t,token:e});r.kind==="unreachable"&&(process.stdout.write(`Could not reach the Ripplo server at ${t} (${r.message}). The token may still be valid \u2014 check the server / network and retry.
24
24
  `),process.exit(1)),r.kind==="rejected"&&(process.stdout.write("Token rejected by the server. Run `ripplo auth login` to re-authenticate.\n"),process.stdout.write(`(Claude Code: run this yourself as a background process \u2014 device-code flow, the user only approves in the browser.)
25
25
  `),process.exit(1)),process.stdout.write(`Authenticated as ${r.viewer.name} <${r.viewer.email}> (${t})
26
- `)}function fr(){if(!tt()){process.stdout.write(`No token to remove.
26
+ `)}function gr(){if(!tt()){process.stdout.write(`No token to remove.
27
27
  `);return}process.stdout.write(`Removed ${Oe("token")}
28
- `)}async function Ue({serverUrl:e,token:t}){try{let n=(await p({config:U({serverUrl:e,token:t}),document:Lo,variables:void 0})).currentUser;return n==null?{kind:"rejected"}:{kind:"ok",viewer:{email:n.email,name:n.name}}}catch(r){return Xe(r)==="UNAUTHENTICATED"?{kind:"rejected"}:{kind:"unreachable",message:r instanceof Error?r.message:String(r)}}}function Do(e){let t=e.name.trim().split(/\s+/)[0];return t!=null&&t.length>0?t:e.email}import{readFile as Oo,writeFile as _o}from"fs/promises";import Fo from"path";async function gr(e){let t=process.cwd(),r=await S(t);r.isErr()&&(process.stderr.write(`${R(r.error)}
29
- `),process.exit(1));let n=q(N,r.value),o=Fo.join(t,L);if(e.check){let i=await Oo(o,"utf8").catch(()=>null);if(i===n){process.stdout.write(`${pt()}
28
+ `)}async function Ue({serverUrl:e,token:t}){try{let n=(await p({config:U({serverUrl:e,token:t}),document:To,variables:void 0})).currentUser;return n==null?{kind:"rejected"}:{kind:"ok",viewer:{email:n.email,name:n.name}}}catch(r){return Xe(r)==="UNAUTHENTICATED"?{kind:"rejected"}:{kind:"unreachable",message:r instanceof Error?r.message:String(r)}}}function Oo(e){let t=e.name.trim().split(/\s+/)[0];return t!=null&&t.length>0?t:e.email}import{readFile as _o,writeFile as Fo}from"fs/promises";import No from"path";async function hr(e){let t=process.cwd(),r=await S(t);r.isErr()&&(process.stderr.write(`${x(r.error)}
29
+ `),process.exit(1));let n=q(N,r.value),o=No.join(t,L);if(e.check){let i=await _o(o,"utf8").catch(()=>null);if(i===n){process.stdout.write(`${pt()}
30
30
  `);return}process.stderr.write(`${ut(i==null?"missing":"stale")}
31
- `),process.exit(1)}await _o(o,n),process.stdout.write(`${mt()}
32
- `)}import{graphql as hr}from"gql.tada";function He(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 No}from"gql.tada";var Uo=No(`
31
+ `),process.exit(1)}await Fo(o,n),process.stdout.write(`${mt()}
32
+ `)}import{graphql as yr}from"gql.tada";function He(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 Uo}from"gql.tada";var Ho=Uo(`
33
33
  query DevSessionCheckPreflight($projectId: String!, $cwd: String!) {
34
34
  project(id: $projectId) {
35
35
  id
@@ -38,26 +38,26 @@ import{A as d,Aa as Ft,B as dt,Ba as Nt,C as R,Ca as Ut,D as pt,Da as Ht,E as ut
38
38
  }
39
39
  }
40
40
  }
41
- `);function j(){return k(process.cwd()).match(e=>e,e=>{process.stderr.write(`${E(e)}
41
+ `);function j(){return w(process.cwd()).match(e=>e,e=>{process.stderr.write(`${E(e)}
42
42
  `),process.stderr.write(`${d("setup")}
43
- `),process.exit(1)})}async function ne(e){(await p({config:e,document:Uo,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(`${d("setup")}
44
- `),process.exit(1))}var Ho=hr(`
43
+ `),process.exit(1)})}async function ne(e){(await p({config:e,document:Ho,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(`${d("setup")}
44
+ `),process.exit(1))}var Mo=yr(`
45
45
  mutation CliUpdateMaxLocalConcurrentRuns($value: Int!) {
46
46
  updateMaxLocalConcurrentRuns(value: $value) {
47
47
  id
48
48
  maxLocalConcurrentRuns
49
49
  }
50
50
  }
51
- `),Mo=hr(`
51
+ `),Wo=yr(`
52
52
  query CliMaxLocalConcurrentRuns {
53
53
  currentUser {
54
54
  id
55
55
  maxLocalConcurrentRuns
56
56
  }
57
57
  }
58
- `);async function yr({value:e}){let t=j();if(e==null){let n=await p({config:t,document:Mo,variables:{}});process.stdout.write(`${He(n.currentUser?.maxLocalConcurrentRuns)}
59
- `);return}let r=await p({config:t,document:Ho,variables:{value:e}});process.stdout.write(`${He(r.updateMaxLocalConcurrentRuns?.maxLocalConcurrentRuns)}
60
- `)}import{graphql as Wo}from"gql.tada";var qo=Wo(`
58
+ `);async function kr({value:e}){let t=j();if(e==null){let n=await p({config:t,document:Wo,variables:{}});process.stdout.write(`${He(n.currentUser?.maxLocalConcurrentRuns)}
59
+ `);return}let r=await p({config:t,document:Mo,variables:{value:e}});process.stdout.write(`${He(r.updateMaxLocalConcurrentRuns?.maxLocalConcurrentRuns)}
60
+ `)}import{graphql as qo}from"gql.tada";var Bo=qo(`
61
61
  query DevSessionCheckHealth($projectId: String!, $cwd: String!) {
62
62
  project(id: $projectId) {
63
63
  id
@@ -66,45 +66,45 @@ import{A as d,Aa as Ft,B as dt,Ba as Nt,C as R,Ca as Ut,D as pt,Da as Ht,E as ut
66
66
  }
67
67
  }
68
68
  }
69
- `);function kr(e){switch(e.status){case"active":return e.gitMidOperation?"\u2713 Dev session: ripplo daemon is live (hooks auto-paused \u2014 git is mid-merge/rebase/cherry-pick; resumes when the operation completes or aborts)":"\u2713 Dev session: ripplo daemon is live for this directory";case"starting":return"! Dev session: `ripplo 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 watch process output for auth/network errors.";case"missing":return"\u2717 Dev session: no active ripplo daemon. Start `npx ripplo daemon` as a background process (separate from your app's dev server). Without it, `ripplo run` will refuse to dispatch and scope/coverage hooks won't arm."}}async function wr(e){let t=de(e),r=ht(e),n=k(e).unwrapOr(void 0);return n==null?{gitMidOperation:r,status:t?"starting":"missing",type:"dev-session"}:(await p({config:n,document:qo,variables:{cwd:n.cwd,projectId:n.projectId}}).catch(()=>null))?.project?.devSession!=null?{gitMidOperation:r,status:"active",type:"dev-session"}:{gitMidOperation:r,status:t?"starting":"missing",type:"dev-session"}}function vr(e){switch(e.type){case"settings":return Bo(e);case"env-files":return Vo(e);case"token":return Go(e);case"dev-server":return Jo(e);case"dev-session":return kr(e);case"preconditions":return zo(e);case"webhook-verification":return Ko(e);case"preconditions-validation":return Qo(e);case"workflows":return Yo(e);case"browser":return Zo(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 ei(e);case"lockfile":return ti(e);case"pre-commit-hook":return ri(e)}}function Bo(e){return e.valid?"\u2713 Settings: Project configured":`\u2717 Settings: Missing fields: ${e.missingFields.join(", ")}`}function Vo(e){return e.missing.length===0?"\u2713 Env files: declared files present":`\u2717 Env files: declared in .ripplo/project.json but missing:
69
+ `);function wr(e){switch(e.status){case"active":return e.gitMidOperation?"\u2713 Dev session: ripplo daemon is live (hooks auto-paused \u2014 git is mid-merge/rebase/cherry-pick; resumes when the operation completes or aborts)":"\u2713 Dev session: ripplo daemon is live for this directory";case"starting":return"! Dev session: `ripplo 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 watch process output for auth/network errors.";case"missing":return"\u2717 Dev session: no active ripplo daemon. Start `npx ripplo daemon` as a background process (separate from your app's dev server). Without it, `ripplo run` will refuse to dispatch and scope/coverage hooks won't arm."}}async function vr(e){let t=de(e),r=ht(e),n=w(e).unwrapOr(void 0);return n==null?{gitMidOperation:r,status:t?"starting":"missing",type:"dev-session"}:(await p({config:n,document:Bo,variables:{cwd:n.cwd,projectId:n.projectId}}).catch(()=>null))?.project?.devSession!=null?{gitMidOperation:r,status:"active",type:"dev-session"}:{gitMidOperation:r,status:t?"starting":"missing",type:"dev-session"}}function br(e){switch(e.type){case"settings":return Vo(e);case"env-files":return Go(e);case"token":return Jo(e);case"dev-server":return zo(e);case"dev-session":return wr(e);case"preconditions":return Ko(e);case"webhook-verification":return Qo(e);case"preconditions-validation":return Yo(e);case"workflows":return Xo(e);case"browser":return ei(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 ti(e);case"lockfile":return ri(e);case"pre-commit-hook":return ni(e)}}function Vo(e){return e.valid?"\u2713 Settings: Project configured":`\u2717 Settings: Missing fields: ${e.missingFields.join(", ")}`}function Go(e){return e.missing.length===0?"\u2713 Env files: declared files present":`\u2717 Env files: declared in .ripplo/project.json but missing:
70
70
  ${e.missing.map(r=>` ${r}`).join(`
71
71
  `)}
72
- If you're in a git worktree, copy the env file from the main checkout (or symlink to a shared file outside the working tree).`}function Go(e){switch(e.status){case"valid":return`\u2713 Auth: Signed in as ${e.email??"unknown"}`;case"missing":return"\u2717 Auth: No token found. Run `ripplo auth login` to authenticate.\n (Claude Code: run this yourself as a background process \u2014 it's a device-code flow, the user only needs to approve in the browser.)";case"invalid":return"\u2717 Auth: Saved token rejected by server. Run `ripplo auth login` to re-authenticate.\n (Claude Code: run this yourself as a background process \u2014 it's a device-code flow, the user only needs to approve in the browser.)";case"unreachable":return`! Auth: Could not reach ${I()} to validate token.`}}function Jo(e){return e.reachable?`\u2713 Dev server: ${e.appUrl} is reachable`:`\u2717 Dev server: ${e.appUrl} is not responding. Make sure your dev server is running.`}function zo(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 Ko(e){return e.rejectsUnsigned?"\u2713 Webhook verification: Unsigned requests are correctly rejected":"\u2717 Webhook verification: Endpoint accepted an unsigned request \u2014 webhook signature verification may be missing or misconfigured."}function Qo(e){if(!e.found)return"\u2717 Model: DSL failed to compile";if(e.valid)return"\u2713 Model: Valid";let t=e.errors.map(r=>` - ${r.path===""?"":r.path+": "}${r.message}`);return`\u2717 Model: ${String(e.errorCount)} validation error${e.errorCount===1?"":"s"}
72
+ If you're in a git worktree, copy the env file from the main checkout (or symlink to a shared file outside the working tree).`}function Jo(e){switch(e.status){case"valid":return`\u2713 Auth: Signed in as ${e.email??"unknown"}`;case"missing":return"\u2717 Auth: No token found. Run `ripplo auth login` to authenticate.\n (Claude Code: run this yourself as a background process \u2014 it's a device-code flow, the user only needs to approve in the browser.)";case"invalid":return"\u2717 Auth: Saved token rejected by server. Run `ripplo auth login` to re-authenticate.\n (Claude Code: run this yourself as a background process \u2014 it's a device-code flow, the user only needs to approve in the browser.)";case"unreachable":return`! Auth: Could not reach ${I()} to validate token.`}}function zo(e){return e.reachable?`\u2713 Dev server: ${e.appUrl} is reachable`:`\u2717 Dev server: ${e.appUrl} is not responding. Make sure your dev server is running.`}function Ko(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 Qo(e){return e.rejectsUnsigned?"\u2713 Webhook verification: Unsigned requests are correctly rejected":"\u2717 Webhook verification: Endpoint accepted an unsigned request \u2014 webhook signature verification may be missing or misconfigured."}function Yo(e){if(!e.found)return"\u2717 Model: DSL failed to compile";if(e.valid)return"\u2713 Model: Valid";let t=e.errors.map(r=>` - ${r.path===""?"":r.path+": "}${r.message}`);return`\u2717 Model: ${String(e.errorCount)} validation error${e.errorCount===1?"":"s"}
73
73
  ${t.join(`
74
- `)}`}function Yo(e){if(e.total===0)return"! Tests: No tests defined";if(e.invalidNames.length===0)return`\u2713 Tests: ${String(e.total)} valid`;let t=e.invalidWorkflows.map(r=>Xo(r));return`\u2717 Tests: ${String(e.invalidNames.length)} invalid
74
+ `)}`}function Xo(e){if(e.total===0)return"! Tests: No tests defined";if(e.invalidNames.length===0)return`\u2713 Tests: ${String(e.total)} valid`;let t=e.invalidWorkflows.map(r=>Zo(r));return`\u2717 Tests: ${String(e.invalidNames.length)} invalid
75
75
  ${t.join(`
76
- `)}`}function Xo(e){let t=e.errors.map(r=>" - "+(r.path===""?"":r.path+": ")+r.message);return" "+e.name+`:
76
+ `)}`}function Zo(e){let t=e.errors.map(r=>" - "+(r.path===""?"":r.path+": ")+r.message);return" "+e.name+`:
77
77
  `+t.join(`
78
- `)}function Zo(e){return e.installed?"\u2713 Browser: Chromium installed":"\u2717 Browser: Chromium not installed. Run: npx playwright install chromium"}function ei(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 ti(e){switch(e.status){case"match":return`\u2713 Lockfile: ${L} is up to date`;case"missing":return`\u2717 Lockfile: ${L} is missing \u2014 run \`ripplo compile\` and commit it`;case"stale":return`\u2717 Lockfile: ${L} is out of date \u2014 run \`ripplo compile\` and commit it`}}function ri(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"}import ge from"fs";import Rr from"path";function H(e){let t=pe(e),r=Ct(e);return[...bt(t,r.invariants).map(n=>({gap:n,kind:"cascade-gap"})),...vt(r.laws).map(n=>({conflict:n,kind:"law-conflict"}))]}function br(e){return`${C.good("OK")} \u2014 no static model violations (${String(e)} tests)`}function M(e){return[`${C.bad("FAIL")} \u2014 ${String(e.length)} static model violation(s):`,...e.map(t=>Rt(t))].join(`
78
+ `)}function ei(e){return e.installed?"\u2713 Browser: Chromium installed":"\u2717 Browser: Chromium not installed. Run: npx playwright install chromium"}function ti(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 ri(e){switch(e.status){case"match":return`\u2713 Lockfile: ${L} is up to date`;case"missing":return`\u2717 Lockfile: ${L} is missing \u2014 run \`ripplo compile\` and commit it`;case"stale":return`\u2717 Lockfile: ${L} is out of date \u2014 run \`ripplo compile\` and commit it`}}function ni(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"}import ge from"fs";import Rr from"path";function H(e){let t=pe(e),r=Ct(e);return[...bt(t,r.invariants).map(n=>({gap:n,kind:"cascade-gap"})),...vt(r.laws).map(n=>({conflict:n,kind:"law-conflict"}))]}function Sr(e){return`${R.good("OK")} \u2014 no static model violations (${String(e)} tests)`}function M(e){return[`${R.bad("FAIL")} \u2014 ${String(e.length)} static model violation(s):`,...e.map(t=>xt(t))].join(`
79
79
 
80
- `)}function fe(e){return[`${C.warn("WARN")} \u2014 ${String(e.length)} model coverage gap(s) (not blocking):`,...e.map(t=>` ${ni(t)}`),`Coverage gaps mean the model can't catch regressions there. Stub the missing flows. ${d("explore")}`].join(`
81
- `)}function ni(e){return e.kind==="entity-never-given"?`${e.entity} \u2014 declared but no implemented test seeds it; no flow exercises this state`:`${e.entity} \u2014 seeded but never asserted created/updated/deleted; mutations to it ship unchecked`}import{graphql as oi}from"gql.tada";var ii=oi(`
80
+ `)}function fe(e){return[`${R.warn("WARN")} \u2014 ${String(e.length)} model coverage gap(s) (not blocking):`,...e.map(t=>` ${oi(t)}`),`Coverage gaps mean the model can't catch regressions there. Stub the missing flows. ${d("explore")}`].join(`
81
+ `)}function oi(e){return e.kind==="entity-never-given"?`${e.entity} \u2014 declared but no implemented test seeds it; no flow exercises this state`:`${e.entity} \u2014 seeded but never asserted created/updated/deleted; mutations to it ship unchecked`}import{graphql as ii}from"gql.tada";var si=ii(`
82
82
  query DoctorAuthViewer {
83
83
  currentUser {
84
84
  name
85
85
  email
86
86
  }
87
87
  }
88
- `),si="Failed to connect to Ripplo server";async function Sr({serverUrl:e,token:t}){try{let r=await p({config:U({serverUrl:e,token:t}),document:ii,variables:void 0});return r.currentUser==null?{kind:"invalid"}:{email:r.currentUser.email,kind:"valid"}}catch(r){return(r instanceof Error?r.message:String(r)).startsWith(si)?{kind:"unreachable"}:{kind:"invalid"}}}function Cr(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}}async function xr(e){let t=await ai(e),r={missing:at(e),type:"env-files"},n=await li(),o=await ci(),i=await S(e),a=di(i),s=pi(i),l=await ui(e,i),c=fi(e),b=await gi(t,i);return[t,r,n,...b,a,s,l,c,o]}async function ai(e){let t=await yt(e);return t.valid?{missingFields:[],type:"settings",valid:!0}:{missingFields:t.errors.map(n=>n.path).filter(n=>n.length>0),type:"settings",valid:!1}}async function li(){let e=O();if(e==null||e.length===0)return{email:void 0,status:"missing",type:"token"};let t=await Sr({serverUrl:I(),token:e});return t.kind==="valid"?{email:t.email,status:"valid",type:"token"}:{email:void 0,status:t.kind,type:"token"}}async function ci(){let{chromium:e}=await import("playwright"),t=e.executablePath();return{installed:ge.existsSync(t),type:"browser"}}function di(e){if(e.isErr())return{errorCount:1,errors:[{message:R(e.error),path:".ripplo/"}],found:!1,type:"preconditions-validation",valid:!1};let t=H(e.value);return{errorCount:t.length,errors:t.length===0?[]:[{message:M(t),path:""}],found:!0,type:"preconditions-validation",valid:t.length===0}}function pi(e){if(e.isErr())return{invalidNames:[],invalidWorkflows:[],total:0,type:"workflows",valid:0};let t=e.value.tests.length;return{invalidNames:[],invalidWorkflows:[],total:t,type:"workflows",valid:t}}async function ui(e,t){if(t.isErr())return{status:"missing",type:"lockfile"};let r=q(N,t.value),n=await ge.promises.readFile(Rr.join(e,L),"utf8").catch(()=>null);return{status:mi(n,r),type:"lockfile"}}function mi(e,t){return e==null?"missing":e===t?"match":"stale"}function fi(e){let t=Rr.join(e,".git","hooks","pre-commit");return ge.existsSync(t)?{installed:ge.readFileSync(t,"utf8").includes("ripplo compile --check"),type:"pre-commit-hook"}:{installed:!1,type:"pre-commit-hook"}}async function gi(e,t){if(!e.valid||t.isErr())return[];let r=st().map(s=>({appUrl:s.appUrl,engineUrl:s.engineUrl,webhookSecret:s.webhookSecret})).unwrapOr(void 0);if(r==null)return[];let n=[],o=await Z(r.appUrl)==null;n.push({appUrl:r.appUrl,reachable:o,type:"dev-server"});let i=await wr(process.cwd());n.push(i);let a=await hi(r,t);return n.push(...a),n}async function hi(e,t){let r=t.isOk()?t.value.entities.length:0,n=e.engineUrl.length>0,o={configured:n,count:r,endpointReachable:void 0,type:"preconditions"};if(r===0||!n)return[o];let i=ki(e.appUrl,e.engineUrl);if(i==null)return[o];let a=await Z(i)==null,s=[];if(s.push({configured:!0,count:r,endpointReachable:a,type:"preconditions"}),Pr(e.engineUrl)&&s.push({reachable:a,type:"engine-endpoint",url:i}),!a)return s;let l=await kt({appUrl:e.appUrl,engineUrl:e.engineUrl});s.push({rejectsUnsigned:l==null,type:"webhook-verification"});let c=await yi({engineUrl:i,webhookSecret:e.webhookSecret});return s.push({status:c,type:"adapter-enabled",url:i}),s}async function yi({engineUrl:e,webhookSecret:t}){if(t.length===0)return"no-secret";let r=JSON.stringify({});try{let n=await fetch(`${e}/setup`,{body:r,headers:{"Content-Type":"application/json",...nt({body:r,secret:t})},method:"PUT",signal:AbortSignal.timeout(5e3)});return n.status===404?"disabled":n.status===401||n.status===403?"bad-secret":"enabled"}catch{return"unreachable"}}function Pr(e){return e.startsWith("http://")||e.startsWith("https://")}function ki(e,t){return Pr(t)?t:`${e}${t}`}async function Er(){let e=process.cwd(),t=await xr(e),r=t.map(o=>vr(o));process.stdout.write(r.join(`
88
+ `),ai="Failed to connect to Ripplo server";async function xr({serverUrl:e,token:t}){try{let r=await p({config:U({serverUrl:e,token:t}),document:si,variables:void 0});return r.currentUser==null?{kind:"invalid"}:{email:r.currentUser.email,kind:"valid"}}catch(r){return(r instanceof Error?r.message:String(r)).startsWith(ai)?{kind:"unreachable"}:{kind:"invalid"}}}function Cr(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}}async function Pr(e){let t=await li(e),r={missing:at(e),type:"env-files"},n=await ci(),o=await di(),i=await S(e),a=pi(i),s=ui(i),l=await mi(e,i),c=gi(e),h=await hi(t,i);return[t,r,n,...h,a,s,l,c,o]}async function li(e){let t=await yt(e);return t.valid?{missingFields:[],type:"settings",valid:!0}:{missingFields:t.errors.map(n=>n.path).filter(n=>n.length>0),type:"settings",valid:!1}}async function ci(){let e=O();if(e==null||e.length===0)return{email:void 0,status:"missing",type:"token"};let t=await xr({serverUrl:I(),token:e});return t.kind==="valid"?{email:t.email,status:"valid",type:"token"}:{email:void 0,status:t.kind,type:"token"}}async function di(){let{chromium:e}=await import("playwright"),t=e.executablePath();return{installed:ge.existsSync(t),type:"browser"}}function pi(e){if(e.isErr())return{errorCount:1,errors:[{message:x(e.error),path:".ripplo/"}],found:!1,type:"preconditions-validation",valid:!1};let t=H(e.value);return{errorCount:t.length,errors:t.length===0?[]:[{message:M(t),path:""}],found:!0,type:"preconditions-validation",valid:t.length===0}}function ui(e){if(e.isErr())return{invalidNames:[],invalidWorkflows:[],total:0,type:"workflows",valid:0};let t=e.value.tests.length;return{invalidNames:[],invalidWorkflows:[],total:t,type:"workflows",valid:t}}async function mi(e,t){if(t.isErr())return{status:"missing",type:"lockfile"};let r=q(N,t.value),n=await ge.promises.readFile(Rr.join(e,L),"utf8").catch(()=>null);return{status:fi(n,r),type:"lockfile"}}function fi(e,t){return e==null?"missing":e===t?"match":"stale"}function gi(e){let t=Rr.join(e,".git","hooks","pre-commit");return ge.existsSync(t)?{installed:ge.readFileSync(t,"utf8").includes("ripplo compile --check"),type:"pre-commit-hook"}:{installed:!1,type:"pre-commit-hook"}}async function hi(e,t){if(!e.valid||t.isErr())return[];let r=st().map(s=>({appUrl:s.appUrl,engineUrl:s.engineUrl,webhookSecret:s.webhookSecret})).unwrapOr(void 0);if(r==null)return[];let n=[],o=await Z(r.appUrl)==null;n.push({appUrl:r.appUrl,reachable:o,type:"dev-server"});let i=await vr(process.cwd());n.push(i);let a=await yi(r,t);return n.push(...a),n}async function yi(e,t){let r=t.isOk()?t.value.entities.length:0,n=e.engineUrl.length>0,o={configured:n,count:r,endpointReachable:void 0,type:"preconditions"};if(r===0||!n)return[o];let i=wi(e.appUrl,e.engineUrl);if(i==null)return[o];let a=await Z(i)==null,s=[];if(s.push({configured:!0,count:r,endpointReachable:a,type:"preconditions"}),Er(e.engineUrl)&&s.push({reachable:a,type:"engine-endpoint",url:i}),!a)return s;let l=await kt({appUrl:e.appUrl,engineUrl:e.engineUrl});s.push({rejectsUnsigned:l==null,type:"webhook-verification"});let c=await ki({engineUrl:i,webhookSecret:e.webhookSecret});return s.push({status:c,type:"adapter-enabled",url:i}),s}async function ki({engineUrl:e,webhookSecret:t}){if(t.length===0)return"no-secret";let r=JSON.stringify({});try{let n=await fetch(`${e}/setup`,{body:r,headers:{"Content-Type":"application/json",...nt({body:r,secret:t})},method:"PUT",signal:AbortSignal.timeout(5e3)});return n.status===404?"disabled":n.status===401||n.status===403?"bad-secret":"enabled"}catch{return"unreachable"}}function Er(e){return e.startsWith("http://")||e.startsWith("https://")}function wi(e,t){return Er(t)?t:`${e}${t}`}async function $r(){let e=process.cwd(),t=await Pr(e),r=t.map(o=>br(o));process.stdout.write(r.join(`
89
89
  `)+`
90
- `);let n=t.some(o=>Cr(o));process.exit(n?1:0)}import he from"fs";import{graphql as wi}from"gql.tada";var vi=wi(`
90
+ `);let n=t.some(o=>Cr(o));process.exit(n?1:0)}import he from"fs";import{graphql as vi}from"gql.tada";var bi=vi(`
91
91
  mutation CliSetHooksPaused($projectId: String!, $paused: Boolean!) {
92
92
  setHooksPaused(projectId: $projectId, paused: $paused) {
93
93
  id
94
94
  }
95
95
  }
96
- `);async function $r(){process.stdin.isTTY||(process.stderr.write("`ripplo hooks pause` must be invoked interactively from a terminal \u2014 refusing non-TTY invocation. Bypassing the gate is a human decision, not an agent decision.\n"),process.exit(1));let e=process.cwd();ce(e);let t=ee(e);if(he.existsSync(t)){process.stdout.write("Hooks already paused. Run `ripplo hooks resume` to re-enable.\n");return}he.writeFileSync(t,""),await jr(e,!0),process.stdout.write("Hooks paused. Pre-edit gates and stop enforcement are off until you run `ripplo hooks resume`.\n")}async function Ir(){let e=process.cwd(),t=ee(e);if(!he.existsSync(t)){process.stdout.write(`Hooks already active.
97
- `);return}he.unlinkSync(t),await jr(e,!1),process.stdout.write(`Hooks resumed.
98
- `)}async function jr(e,t){let r=k(e).unwrapOr(void 0);r!=null&&await p({config:r,document:vi,variables:{paused:t,projectId:r.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.
99
- `)})}import G from"fs";import J from"path";import{input as Or,select as _r}from"@inquirer/prompts";import{graphql as Ki}from"gql.tada";import{exec as Ri,execFile as Ci}from"child_process";import{err as xi,ok as Ar}from"neverthrow";import h from"fs";import{createRequire as Pi}from"module";import g from"path";import{promisify as Tr}from"util";import{writeFile as bi}from"fs/promises";import Si from"path";async function B(e){let t=await S(e);return t.isOk()&&await bi(Si.join(e,L),q(N,t.value)),t}function F(e){return e.tests.filter(t=>t.stub).map(t=>t.name)}var Ei=["@ripplo/testing","@ripplo/instrument"],Lr=".ripplo/ripplo.lock linguist-generated=true",$i=[".ripplo/debug/",".ripplo/.local/"],Ii=Tr(Ri),ji=Tr(Ci);async function Dr({cwd:e,onStep:t}){t("Scaffolding project files..."),Mi({cwd:e}),t("Updating .gitignore..."),Wi(e),t("Marking ripplo.lock as generated..."),Di(e),t("Installing dependencies...");let r=await Li(e),n=[];if(r.ok||n.push({manualCommand:r.cmd,message:`Couldn't auto-install dev dependencies (${r.reason}). Run the command below, then run \`ripplo lint\` to compile the lockfile.`}),r.ok){t("Compiling initial lockfile...");let i=await Ti(e);i!=null&&n.push({manualCommand:void 0,message:i})}return t("Setting up browser..."),(await Ai()).map(()=>n)}async function Ai(){let{chromium:e}=await import("playwright"),t=e.executablePath();if(h.existsSync(t))return Ar(void 0);D.info("Chromium not found. Installing via Playwright...");let r=Pi(import.meta.url),n=g.dirname(r.resolve("playwright/package.json")),o=g.join(n,"cli.js");return await ji(process.execPath,[o,"install","chromium"]),h.existsSync(t)?Ar(void 0):xi({kind:"playwright-install-failed"})}async function Li(e){let t=Oi({cwd:e,pm:Hi(e)});D.info("Installing dependencies: %s",t);try{return await Ii(t,{cwd:e}),{ok:!0}}catch(r){let n=r instanceof Error?r.message.split(`
100
- `)[0]??r.message:String(r);return D.warn("Install failed (%s): %s",t,n),{cmd:t,ok:!1,reason:n}}}async function Ti(e){try{await B(e);return}catch(t){return`Couldn't compile initial lockfile: ${t instanceof Error?t.message:String(t)}.`}}function Di(e){let t=g.join(e,".gitattributes"),r=h.existsSync(t)?h.readFileSync(t,"utf8"):"";if(r.includes(Lr))return;let n=r.length===0||r.endsWith(`
96
+ `);async function Ir(){process.stdin.isTTY||(process.stderr.write("`ripplo hooks pause` must be invoked interactively from a terminal \u2014 refusing non-TTY invocation. Bypassing the gate is a human decision, not an agent decision.\n"),process.exit(1));let e=process.cwd();ce(e);let t=ee(e);if(he.existsSync(t)){process.stdout.write("Hooks already paused. Run `ripplo hooks resume` to re-enable.\n");return}he.writeFileSync(t,""),await Ar(e,!0),process.stdout.write("Hooks paused. Pre-edit gates and stop enforcement are off until you run `ripplo hooks resume`.\n")}async function jr(){let e=process.cwd(),t=ee(e);if(!he.existsSync(t)){process.stdout.write(`Hooks already active.
97
+ `);return}he.unlinkSync(t),await Ar(e,!1),process.stdout.write(`Hooks resumed.
98
+ `)}async function Ar(e,t){let r=w(e).unwrapOr(void 0);r!=null&&await p({config:r,document:bi,variables:{paused:t,projectId:r.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.
99
+ `)})}import G from"fs";import J from"path";import{input as _r,select as Fr}from"@inquirer/prompts";import{graphql as Qi}from"gql.tada";import{exec as Ri,execFile as Ci}from"child_process";import{err as Pi,ok as Lr}from"neverthrow";import y from"fs";import{createRequire as Ei}from"module";import g from"path";import{promisify as Dr}from"util";import{writeFile as Si}from"fs/promises";import xi from"path";async function B(e){let t=await S(e);return t.isOk()&&await Si(xi.join(e,L),q(N,t.value)),t}function F(e){return e.tests.filter(t=>t.stub).map(t=>t.name)}var $i=["@ripplo/testing","@ripplo/instrument"],Tr=".ripplo/ripplo.lock linguist-generated=true",Ii=[".ripplo/debug/",".ripplo/.local/"],ji=Dr(Ri),Ai=Dr(Ci);async function Or({cwd:e,onStep:t}){t("Scaffolding project files..."),Wi({cwd:e}),t("Updating .gitignore..."),qi(e),t("Marking ripplo.lock as generated..."),Oi(e),t("Installing dependencies...");let r=await Ti(e),n=[];if(r.ok||n.push({manualCommand:r.cmd,message:`Couldn't auto-install dev dependencies (${r.reason}). Run the command below, then run \`ripplo lint\` to compile the lockfile.`}),r.ok){t("Compiling initial lockfile...");let i=await Di(e);i!=null&&n.push({manualCommand:void 0,message:i})}return t("Setting up browser..."),(await Li()).map(()=>n)}async function Li(){let{chromium:e}=await import("playwright"),t=e.executablePath();if(y.existsSync(t))return Lr(void 0);D.info("Chromium not found. Installing via Playwright...");let r=Ei(import.meta.url),n=g.dirname(r.resolve("playwright/package.json")),o=g.join(n,"cli.js");return await Ai(process.execPath,[o,"install","chromium"]),y.existsSync(t)?Lr(void 0):Pi({kind:"playwright-install-failed"})}async function Ti(e){let t=_i({cwd:e,pm:Mi(e)});D.info("Installing dependencies: %s",t);try{return await ji(t,{cwd:e}),{ok:!0}}catch(r){let n=r instanceof Error?r.message.split(`
100
+ `)[0]??r.message:String(r);return D.warn("Install failed (%s): %s",t,n),{cmd:t,ok:!1,reason:n}}}async function Di(e){try{await B(e);return}catch(t){return`Couldn't compile initial lockfile: ${t instanceof Error?t.message:String(t)}.`}}function Oi(e){let t=g.join(e,".gitattributes"),r=y.existsSync(t)?y.readFileSync(t,"utf8"):"";if(r.includes(Tr))return;let n=r.length===0||r.endsWith(`
101
101
  `)?"":`
102
- `;h.writeFileSync(t,`${r}${n}${Lr}
103
- `)}function Oi({cwd:e,pm:t}){let r=Ei.join(" ");return t==="pnpm"?_i(e)?`pnpm add -wD ${r}`:`pnpm add -D ${r}`:t==="yarn"?Fi({cwd:e,deps:r}):t==="bun"?`bun add -d ${r}`:`npm install -D ${r}`}function _i(e){return h.existsSync(g.join(e,"pnpm-workspace.yaml"))||h.existsSync(g.join(e,"pnpm-workspace.yml"))}function Fi({cwd:e,deps:t}){return Ni(e)?`yarn add -D ${t}`:Ui(e)?`yarn add -WD ${t}`:`yarn add -D ${t}`}function Ni(e){if(h.existsSync(g.join(e,".yarnrc.yml"))||h.existsSync(g.join(e,".pnp.cjs"))||h.existsSync(g.join(e,".pnp.js")))return!0;let t=g.join(e,"package.json");if(!h.existsSync(t))return!1;try{let r=JSON.parse(h.readFileSync(t,"utf8"));if(r==null||typeof r!="object"||!("packageManager"in r))return!1;let n=r.packageManager;if(typeof n!="string")return!1;let o=/^yarn@(\d+)/.exec(n);return o!=null&&Number(o[1])>=2}catch{return!1}}function Ui(e){let t=g.join(e,"package.json");if(!h.existsSync(t))return!1;try{let r=JSON.parse(h.readFileSync(t,"utf8"));if(r==null||typeof r!="object"||!("workspaces"in r))return!1;let n=r.workspaces;return Array.isArray(n)||n!=null&&typeof n=="object"}catch{return!1}}function Hi(e){return h.existsSync(g.join(e,"pnpm-lock.yaml"))?"pnpm":h.existsSync(g.join(e,"yarn.lock"))?"yarn":h.existsSync(g.join(e,"bun.lockb"))||h.existsSync(g.join(e,"bun.lock"))?"bun":"npm"}function Mi({cwd:e}){let t=g.join(e,".ripplo"),r=g.join(t,"entities"),n=g.join(t,"singletons"),o=g.join(t,"worlds"),i=g.join(t,"tests");[r,n,o,i].forEach(a=>{h.mkdirSync(a,{recursive:!0})}),V(g.join(t,"index.ts"),qi),V(g.join(r,"index.ts"),Bi),V(g.join(n,"index.ts"),Vi),V(g.join(o,"index.ts"),Gi),V(g.join(i,"index.ts"),Ji),V(g.join(t,"tsconfig.json"),zi)}function V(e,t){h.existsSync(e)||h.writeFileSync(e,t)}function Wi(e){let t=g.join(e,".gitignore");if(!h.existsSync(t))return;let r=h.readFileSync(t,"utf8"),n=$i.filter(i=>!r.includes(i));if(n.length===0)return;let o=r.endsWith(`
102
+ `;y.writeFileSync(t,`${r}${n}${Tr}
103
+ `)}function _i({cwd:e,pm:t}){let r=$i.join(" ");return t==="pnpm"?Fi(e)?`pnpm add -wD ${r}`:`pnpm add -D ${r}`:t==="yarn"?Ni({cwd:e,deps:r}):t==="bun"?`bun add -d ${r}`:`npm install -D ${r}`}function Fi(e){return y.existsSync(g.join(e,"pnpm-workspace.yaml"))||y.existsSync(g.join(e,"pnpm-workspace.yml"))}function Ni({cwd:e,deps:t}){return Ui(e)?`yarn add -D ${t}`:Hi(e)?`yarn add -WD ${t}`:`yarn add -D ${t}`}function Ui(e){if(y.existsSync(g.join(e,".yarnrc.yml"))||y.existsSync(g.join(e,".pnp.cjs"))||y.existsSync(g.join(e,".pnp.js")))return!0;let t=g.join(e,"package.json");if(!y.existsSync(t))return!1;try{let r=JSON.parse(y.readFileSync(t,"utf8"));if(r==null||typeof r!="object"||!("packageManager"in r))return!1;let n=r.packageManager;if(typeof n!="string")return!1;let o=/^yarn@(\d+)/.exec(n);return o!=null&&Number(o[1])>=2}catch{return!1}}function Hi(e){let t=g.join(e,"package.json");if(!y.existsSync(t))return!1;try{let r=JSON.parse(y.readFileSync(t,"utf8"));if(r==null||typeof r!="object"||!("workspaces"in r))return!1;let n=r.workspaces;return Array.isArray(n)||n!=null&&typeof n=="object"}catch{return!1}}function Mi(e){return y.existsSync(g.join(e,"pnpm-lock.yaml"))?"pnpm":y.existsSync(g.join(e,"yarn.lock"))?"yarn":y.existsSync(g.join(e,"bun.lockb"))||y.existsSync(g.join(e,"bun.lock"))?"bun":"npm"}function Wi({cwd:e}){let t=g.join(e,".ripplo"),r=g.join(t,"entities"),n=g.join(t,"singletons"),o=g.join(t,"worlds"),i=g.join(t,"tests");[r,n,o,i].forEach(a=>{y.mkdirSync(a,{recursive:!0})}),V(g.join(t,"index.ts"),Bi),V(g.join(r,"index.ts"),Vi),V(g.join(n,"index.ts"),Gi),V(g.join(o,"index.ts"),Ji),V(g.join(i,"index.ts"),zi),V(g.join(t,"tsconfig.json"),Ki)}function V(e,t){y.existsSync(e)||y.writeFileSync(e,t)}function qi(e){let t=g.join(e,".gitignore");if(!y.existsSync(t))return;let r=y.readFileSync(t,"utf8"),n=Ii.filter(i=>!r.includes(i));if(n.length===0)return;let o=r.endsWith(`
104
104
  `)?"":`
105
- `;h.writeFileSync(t,r+o+n.join(`
105
+ `;y.writeFileSync(t,r+o+n.join(`
106
106
  `)+`
107
- `)}var qi=`import { createRipplo } from "@ripplo/testing";
107
+ `)}var Bi=`import { createRipplo } from "@ripplo/testing";
108
108
  import { entities } from "./entities/index";
109
109
  import { singletons } from "./singletons/index";
110
110
  import { tests } from "./tests/index";
@@ -114,7 +114,7 @@ export default createRipplo({
114
114
  singletons,
115
115
  tests,
116
116
  });
117
- `,Bi=`// Model the app's state as entities. Each entity gets a \`seed\`/\`read\` impl in your
117
+ `,Vi=`// Model the app's state as entities. Each entity gets a \`seed\`/\`read\` impl in your
118
118
  // app's engine funnel (createEngine). See /ripplo:create "Adding an entity".
119
119
  //
120
120
  // Example:
@@ -130,7 +130,7 @@ export default createRipplo({
130
130
  // export const entities = [Project] as const;
131
131
 
132
132
  export const entities = [] as const;
133
- `,Vi=`// Client/global state (e.g. localStorage flags) modeled as singletons.
133
+ `,Gi=`// Client/global state (e.g. localStorage flags) modeled as singletons.
134
134
  //
135
135
  // Example:
136
136
  // import { singleton, v } from "@ripplo/testing";
@@ -145,7 +145,7 @@ export const entities = [] as const;
145
145
  // export const singletons = [onboardingDismissed];
146
146
 
147
147
  export const singletons = [];
148
- `,Gi=`// Pure builder functions returning a flat record of entity handles \u2014 the starting state
148
+ `,Ji=`// Pure builder functions returning a flat record of entity handles \u2014 the starting state
149
149
  // for tests. Compose from other worlds. See /ripplo:create "Adding a world".
150
150
  //
151
151
  // Example:
@@ -157,7 +157,7 @@ export const singletons = [];
157
157
  // const project = Project.of({ name: arbitrary(Project.field.name), ownerId: me.id });
158
158
  // return { me, project };
159
159
  // };
160
- `,Ji=`// Each test file under ./tests exports a test. Import them here and add to the \`tests\`
160
+ `,zi=`// Each test file under ./tests exports a test. Import them here and add to the \`tests\`
161
161
  // array \u2014 that's what createRipplo({ ..., tests }) receives. Stub with \`test("Intent")\`
162
162
  // (no body); implement later with \`test("Intent", () => ({ given, steps }))\`.
163
163
  //
@@ -166,7 +166,7 @@ export const singletons = [];
166
166
  // export const tests = [createProject] as const;
167
167
 
168
168
  export const tests = [] as const;
169
- `,zi=`{
169
+ `,Ki=`{
170
170
  "compilerOptions": {
171
171
  "strict": true,
172
172
  "noUncheckedIndexedAccess": true,
@@ -182,67 +182,67 @@ export const tests = [] as const;
182
182
  "include": ["*.ts", "entities/**/*.ts", "singletons/**/*.ts", "worlds/**/*.ts", "tests/**/*.ts"],
183
183
  "exclude": ["node_modules"]
184
184
  }
185
- `;var Qi=Ki(`
185
+ `;var Yi=Qi(`
186
186
  query InitProjects {
187
187
  projects {
188
188
  id
189
189
  name
190
190
  }
191
191
  }
192
- `),Me=["../.env.local","../.env"];async function Fr(e=Zi()){let t=process.cwd(),r=O();r==null&&(process.stdout.write("Not authenticated. Run `ripplo auth login` first.\n"),process.exit(1));let n=I();Yi(t)&&(process.stdout.write(`\`.ripplo/index.ts\` already exists at ${t}. Delete it first if you want to re-init.
193
- `),process.exit(1));let o=await Xi({serverUrl:n,token:r});o.length===0&&(process.stdout.write("No projects found. Create one in the dashboard first, then re-run `ripplo init`.\n"),process.exit(1));let i=await ts(o,e.projectId),a=await rs(t,e.envFile),s=await os(e.appUrl),l=es(s,e.engineUrl),c=J.resolve(J.join(t,".ripplo"),a);it({cwd:t,envFiles:[a],projectId:i}),ss({appUrl:s,engineUrl:l,filePath:c});let u=(await Dr({cwd:t,onStep:y=>{process.stdout.write(` ${y}
194
- `)}})).match(y=>y,y=>{process.stderr.write(`${E(y)}
192
+ `),Me=["../.env.local","../.env"];async function Nr(e=es()){let t=process.cwd(),r=O();r==null&&(process.stdout.write("Not authenticated. Run `ripplo auth login` first.\n"),process.exit(1));let n=I();Xi(t)&&(process.stdout.write(`\`.ripplo/index.ts\` already exists at ${t}. Delete it first if you want to re-init.
193
+ `),process.exit(1));let o=await Zi({serverUrl:n,token:r});o.length===0&&(process.stdout.write("No projects found. Create one in the dashboard first, then re-run `ripplo init`.\n"),process.exit(1));let i=await rs(o,e.projectId),a=await ns(t,e.envFile),s=await is(e.appUrl),l=ts(s,e.engineUrl),c=J.resolve(J.join(t,".ripplo"),a);it({cwd:t,envFiles:[a],projectId:i}),as({appUrl:s,engineUrl:l,filePath:c});let u=(await Or({cwd:t,onStep:k=>{process.stdout.write(` ${k}
194
+ `)}})).match(k=>k,k=>{process.stderr.write(`${E(k)}
195
195
  `),process.exit(1)});if(u.length>0){process.stdout.write(`Done with warnings:
196
- `),u.forEach(y=>{process.stdout.write(` - ${y.message}
197
- `),y.manualCommand!=null&&process.stdout.write(` run: ${y.manualCommand}
198
- `)});return}process.stdout.write("Ready. Start `npx ripplo daemon` as a background process (or run `/ripplo:start` in Claude Code), then write tests in `.ripplo/tests/`.\n")}function Yi(e){return G.existsSync(J.join(e,".ripplo","index.ts"))}async function Xi({serverUrl:e,token:t}){return((await p({config:U({serverUrl:e,token:t}),document:Qi,variables:void 0})).projects??[]).map(n=>({id:n.id,name:n.name}))}function Zi(){return{appUrl:void 0,engineUrl:void 0,envFile:void 0,projectId:void 0}}function es(e,t){if(t!=null){try{new URL(t)}catch{process.stdout.write(`--engine-url is not a valid URL: ${t}
199
- `),process.exit(1)}return t}return`${e.replace(/\/$/,"")}/ripplo`}async function ts(e,t){if(t!=null){let r=e.find(n=>n.id===t);return r==null&&(process.stdout.write(`Unknown project id: ${t}
196
+ `),u.forEach(k=>{process.stdout.write(` - ${k.message}
197
+ `),k.manualCommand!=null&&process.stdout.write(` run: ${k.manualCommand}
198
+ `)});return}process.stdout.write("Ready. Start `npx ripplo daemon` as a background process (or run `/ripplo:start` in Claude Code), then write tests in `.ripplo/tests/`.\n")}function Xi(e){return G.existsSync(J.join(e,".ripplo","index.ts"))}async function Zi({serverUrl:e,token:t}){return((await p({config:U({serverUrl:e,token:t}),document:Yi,variables:void 0})).projects??[]).map(n=>({id:n.id,name:n.name}))}function es(){return{appUrl:void 0,engineUrl:void 0,envFile:void 0,projectId:void 0}}function ts(e,t){if(t!=null){try{new URL(t)}catch{process.stdout.write(`--engine-url is not a valid URL: ${t}
199
+ `),process.exit(1)}return t}return`${e.replace(/\/$/,"")}/ripplo`}async function rs(e,t){if(t!=null){let r=e.find(n=>n.id===t);return r==null&&(process.stdout.write(`Unknown project id: ${t}
200
200
  `),process.exit(1)),process.stdout.write(`Using project: ${r.name} (${r.id})
201
201
  `),r.id}if(e.length===1){let r=e[0];if(r==null)throw new Error("unreachable");return process.stdout.write(`Using project: ${r.name} (${r.id})
202
- `),r.id}return _r({choices:e.map(r=>({name:r.name,value:r.id})),message:"Select a project"})}async function rs(e,t){return t!=null?(t.trim().length===0&&(process.stdout.write(`--env must not be empty
203
- `),process.exit(1)),t):ns(e)}async function ns(e){let t=J.join(e,".ripplo"),r=Me.find(o=>G.existsSync(J.resolve(t,o))),n=await _r({choices:[...Me.map(o=>({name:r===o?`${o} (detected)`:o,value:o})),{name:"custom path",value:"__custom__"}],default:r??Me[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:Or({message:"Path to env file (relative to .ripplo/, e.g. ../apps/server/.env)",validate:o=>o.trim().length>0?!0:"required"})}async function os(e){if(e!=null){try{new URL(e)}catch{process.stdout.write(`--app-url is not a valid URL: ${e}
204
- `),process.exit(1)}return e}return is()}async function is(){return Or({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 ss({appUrl:e,engineUrl:t,filePath:r}){G.mkdirSync(J.dirname(r),{recursive:!0});let n=G.existsSync(r)?G.readFileSync(r,"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=${t}`),/^RIPPLO_WEBHOOK_SECRET=/m.test(n)||o.push(`RIPPLO_WEBHOOK_SECRET=${ot()}`),/^ENABLE_RIPPLO_TESTING=/m.test(n)||o.push("ENABLE_RIPPLO_TESTING=true"),o.length===0)return;let i=n.length===0||n.endsWith(`
202
+ `),r.id}return Fr({choices:e.map(r=>({name:r.name,value:r.id})),message:"Select a project"})}async function ns(e,t){return t!=null?(t.trim().length===0&&(process.stdout.write(`--env must not be empty
203
+ `),process.exit(1)),t):os(e)}async function os(e){let t=J.join(e,".ripplo"),r=Me.find(o=>G.existsSync(J.resolve(t,o))),n=await Fr({choices:[...Me.map(o=>({name:r===o?`${o} (detected)`:o,value:o})),{name:"custom path",value:"__custom__"}],default:r??Me[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:_r({message:"Path to env file (relative to .ripplo/, e.g. ../apps/server/.env)",validate:o=>o.trim().length>0?!0:"required"})}async function is(e){if(e!=null){try{new URL(e)}catch{process.stdout.write(`--app-url is not a valid URL: ${e}
204
+ `),process.exit(1)}return e}return ss()}async function ss(){return _r({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 as({appUrl:e,engineUrl:t,filePath:r}){G.mkdirSync(J.dirname(r),{recursive:!0});let n=G.existsSync(r)?G.readFileSync(r,"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=${t}`),/^RIPPLO_WEBHOOK_SECRET=/m.test(n)||o.push(`RIPPLO_WEBHOOK_SECRET=${ot()}`),/^ENABLE_RIPPLO_TESTING=/m.test(n)||o.push("ENABLE_RIPPLO_TESTING=true"),o.length===0)return;let i=n.length===0||n.endsWith(`
205
205
  `)?"":`
206
206
  `;G.writeFileSync(r,`${n}${i}${o.join(`
207
207
  `)}
208
- `)}function ye(e){let t=e.tests.filter(o=>!o.stub);if(t.length===0)return[];let r=new Set(t.flatMap(o=>as(o))),n=new Set(t.flatMap(o=>ls(o)));return[...e.entities.filter(o=>!r.has(o.name)).map(o=>({entity:o.name,kind:"entity-never-given"})),...e.entities.filter(o=>r.has(o.name)&&!n.has(o.name)).map(o=>({entity:o.name,kind:"entity-never-mutated"}))]}function as(e){return[...e.world,...e.maybe].map(t=>t.entity)}function ls(e){return e.steps.flatMap(t=>t.expect.flatMap(r=>oe(r)))}function oe(e){return e.kind==="state"?[e.entity]:e.kind==="not"?oe(e.predicate):e.kind==="and"?e.predicates.flatMap(t=>oe(t)):e.kind==="when"?[...oe(e.condition),...oe(e.consequence)]:[]}async function Nr(){let e=process.cwd(),r=(await S(e)).match(o=>o,o=>cs(R(o))),n=H(r);if(n.length===0){let o=ye(r);o.length>0&&process.stdout.write(`${fe(o)}
209
- `),process.stdout.write(`${br(r.tests.length)}
208
+ `)}function ye(e){let t=e.tests.filter(o=>!o.stub);if(t.length===0)return[];let r=new Set(t.flatMap(o=>ls(o))),n=new Set(t.flatMap(o=>cs(o)));return[...e.entities.filter(o=>!r.has(o.name)).map(o=>({entity:o.name,kind:"entity-never-given"})),...e.entities.filter(o=>r.has(o.name)&&!n.has(o.name)).map(o=>({entity:o.name,kind:"entity-never-mutated"}))]}function ls(e){return[...e.world,...e.maybe].map(t=>t.entity)}function cs(e){return e.steps.flatMap(t=>t.expect.flatMap(r=>oe(r)))}function oe(e){return e.kind==="state"?[e.entity]:e.kind==="not"?oe(e.predicate):e.kind==="and"?e.predicates.flatMap(t=>oe(t)):e.kind==="when"?[...oe(e.condition),...oe(e.consequence)]:[]}async function Ur(){let e=process.cwd(),r=(await S(e)).match(o=>o,o=>ds(x(o))),n=H(r);if(n.length===0){let o=ye(r);o.length>0&&process.stdout.write(`${fe(o)}
209
+ `),process.stdout.write(`${Sr(r.tests.length)}
210
210
  `);return}process.stderr.write(`${M(n)}
211
- `),process.exit(1)}function cs(e){process.stderr.write(`${e}
212
- `),process.exit(1)}import{CancellationTokenSource as Ls}from"vscode-jsonrpc/node";import ms from"path";import{randomUUID as ds}from"crypto";async function ke({assign:e,config:t,headed:r,lockfile:n,session:o,signal:i}){let a=pe(n),s=await ps({assign:e,corpus:a,lockfile:n});if(s==null)return{kind:"error",reason:"depot-arrange-failed",rows:[]};if(s.firings.length===0)return{kind:"error",reason:"empty-trail",rows:[]};let l=await $t({corpus:a,depot:{name:e.depotTest.name,test:e.depotTest},lensId:e.lensId,lockfile:n,lockfileHash:e.lockfileHash,options:{baseUrl:t.appUrl,engineUrl:t.engineUrl,generate:ue,headed:r,secret:t.webhookSecret},session:o,shrinkBudget:e.shrinkBudget,trail:s,now:()=>new Date().toISOString(),runIdFor:c=>`explore-${ds()}-${String(c)}`},i);return us(l)}async function ps({assign:e,corpus:t,lockfile:r}){return(await Et(r,{name:e.depotTest.name,test:e.depotTest},{generate:ue,materialize:wt(ue,r.valueSpaces),params:void 0})).match(o=>Pt({actionHashes:t.map(i=>xt(r,i)),corpus:t,covered:new Set,depotSnapshot:o.snapshot,lens:jt(r),lensId:e.lensId,maxLength:e.maxLength,witnessTrail:e.firings}),()=>null)}function us(e){return e.kind==="error"?{kind:e.kind,reason:`runtime:${e.error.kind}`,rows:[]}:e.kind==="aborted"?{kind:e.kind,rows:[]}:{kind:e.kind,rows:[...e.rows]}}async function Ur(e){let t=k(e.cwd).match(l=>({config:l}),l=>({failure:l}));if("failure"in t)return{failure:t.failure,kind:"config-failed"};let r=t.config,n=await ct(e.cwd);if(n.result.isErr())return{error:n.result.error,kind:"compile-failed"};let o={fingerprint:n.fingerprint,lockfile:n.result.value},i=It({onChange:()=>{}});if(!i.holder())return i.stop(),{kind:"explorer-busy"};let a=me({debugDir:ms.join(e.cwd,".ripplo","debug"),headed:e.headed,writeOtlpPortFile:!1}),s=At({cwd:e.cwd,executeTrail:(l,c)=>ke({assign:l,config:r,headed:e.headed,lockfile:o.lockfile,session:a,signal:c}),loadLockfile:()=>Promise.resolve(o),notifyWork:()=>{},probeApp:async()=>await Z(r.appUrl)==null});try{await s.ready(),e.onReady(s.status());let l=await fs(s,e),c=s.status();return await s.stop(),await a.close(),l===0&&c.total===0?{kind:"no-work"}:{executed:l,kind:"completed",progress:c}}finally{i.stop()}}async function fs(e,t){let r=new AbortController().signal,n=async o=>{if(o>=t.trails)return o;let i=e.next();return i==null?o:(await i.run(r),t.onTrail(o+1,{firings:i.firings,label:i.label,path:i.path},e.status()),n(o+1))};return n(0)}import{spawn as gs}from"child_process";import Hr from"fs";import hs from"net";import{setTimeout as Wr}from"timers/promises";import{err as ie,ok as we}from"neverthrow";import{ResponseError as ys}from"vscode-jsonrpc/node";var qr=12e4,ks=300,Mr=5e3;async function z({cliEntry:e,cwd:t}){let r=_e(t),n=await ve(r);if(n!=null)return we(We(n,!1));let o=Ss({cliEntry:e,cwd:t});if(o!=null)return ie(o);let i=await Rs(r);return i==null?ie({deadlineMs:qr,kind:"connect-timeout",logPath:Fe(t)}):we(We(i,!0))}async function be({connection:e,onEvent:t,request:r,token:n}){let o=await vs({connection:e,onEvent:t,request:r,token:n});return o.kind==="transport"?ie(o.error):we(o)}async function Br({connection:e,findingId:t,token:r}){try{let n=await e.rpc.sendRequest(Ht,{findingId:t},r),o=Ut.safeParse(n);return o.success?we(o.data):ie({kind:"bad-frame"})}catch{return ie({kind:"connection-lost"})}}async function K(e){try{await e.rpc.sendRequest(qt)}catch{}}async function Vr(e){let t=await ve(_e(e));if(t==null)return{kind:"not-running"};let r=We(t,!1),n=await Promise.race([ws(r),Wr(Mr).then(()=>null)]);return t.destroy(),n==null?{kind:"unresponsive",timeoutMs:Mr}:{kind:"running",status:n}}async function ws(e){try{let t=await e.rpc.sendRequest(Wt),r=Ot.safeParse(t);return r.success?r.data:null}catch{return null}}function vs({connection:e,onEvent:t,request:r,token:n}){let{rpc:o}=e;return new Promise(i=>{let a=s=>{i(s)};o.onNotification(Bt,s=>{let l=_t.safeParse(s);if(!l.success){a({error:{kind:"bad-frame"},kind:"transport"});return}t(l.data.event)}),o.onNotification(Vt,s=>{let l=Ft.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(Mt,r,n).then(s=>{Dt.safeParse(s).success||a({error:{kind:"bad-frame"},kind:"transport"})}).catch(s=>{a(bs(s))})})}function bs(e){if(e instanceof ys){let t=Nt.safeParse(e.data);return t.success?{error:t.data,kind:"daemon-error"}:{error:{kind:"bad-frame"},kind:"transport"}}return{error:{kind:"connection-lost"},kind:"transport"}}function We(e,t){let r=Tt(e);return r.listen(),{rpc:r,socket:e,spawned:t}}function ve(e){return new Promise(t=>{let r=hs.connect(e);r.once("connect",()=>{t(r)}),r.once("error",()=>{t(null)})})}function Ss({cliEntry:e,cwd:t}){try{ce(t);let r=Hr.openSync(Fe(t),"a");return gs(process.execPath,[e,"daemon"],{cwd:t,detached:!0,stdio:["ignore",r,r]}).unref(),Hr.closeSync(r),null}catch(r){return{kind:"spawn-failed",message:r instanceof Error?r.message:String(r)}}}async function Rs(e){let t=Date.now()+qr,r=await ve(e);for(;r==null&&Date.now()<t;)await Wr(ks),r=await ve(e);return r}function Gr(e){if(e.pending.length===0&&e.recurrentFlaky.length===0)return"explore: no pending findings";let t=e.pending.length===0?[]:[`explore: ${String(e.pending.length)} pending finding(s) \u2014 triage in layer order, top first`,...e.pending.map(o=>Cs(o))],r=e.recurrentFlaky.length===0?[]:[`recurrent flaky-candidates (same divergence ${String(e.recurrentFlaky.length)}x, no deterministic repro \u2014 triage after findings):`,...e.recurrentFlaky.map(o=>Es(o))],n=d("debug","triaging each finding (evidence -> classify -> fix -> replay)");return[...t,...r,"","replay after a fix: npx ripplo explore replay <id>",n].join(`
213
- `).trim()}function Jr(e,t){switch(t.kind){case"resolved":return`explore: ${e} replayed clean \u2014 finding resolved, its targets covered under the current model`;case"unreachable":return`explore: ${e}'s trail is no longer plannable under the current model \u2014 the violating composition is unreachable; finding resolved (if you narrowed a given/when, make sure a test covers the excluded state)`;case"still-failing":{let r=t.runId==null?"":` (fresh evidence: run ${t.runId})`;return`explore: ${e} still reproduces \u2014 same divergence signature${r}`}case"diverged":{let r=t.runId==null?"":` (captured run ${t.runId})`;return`explore: ${e} failed with a DIFFERENT divergence signature \u2014 new finding recorded${r}`}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 (${t.reason})`;case"error":return`explore: replay of ${e} failed (${t.reason})`}}function zr({executed:e,firings:t,label:r,progress:n}){let o=t.flatMap(i=>[` ${i.label}`,...i.actions.map(a=>` ${a}`)]);return[`trail ${String(e)} done \u2014 ${te(n)}`,...o,` state: ${r}`].join(`
214
- `)}function Kr(e){switch(e.kind){case"config-failed":return E(e.failure);case"compile-failed":return R(e.error);case"no-work":return"explore: nothing to explore \u2014 no transitions harvested from the suite";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`}
211
+ `),process.exit(1)}function ds(e){process.stderr.write(`${e}
212
+ `),process.exit(1)}import{CancellationTokenSource as Os}from"vscode-jsonrpc/node";import gs from"path";import{randomUUID as ps}from"crypto";async function ke({assign:e,config:t,headed:r,lockfile:n,session:o,signal:i}){let a=pe(n),s=await ms({assign:e,corpus:a,lockfile:n});if(s==null)return{kind:"error",reason:"depot-arrange-failed",rows:[],trail:[]};if(s.firings.length===0)return{kind:"error",reason:"empty-trail",rows:[],trail:[]};let l=us(a,s.firings),c=await It({corpus:a,depot:{name:e.depotTest.name,test:e.depotTest},lensId:e.lensId,lockfile:n,lockfileHash:e.lockfileHash,options:{baseUrl:t.appUrl,engineUrl:t.engineUrl,generate:ue,headed:r,secret:t.webhookSecret},session:o,shrinkBudget:e.shrinkBudget,trail:s,now:()=>new Date().toISOString(),runIdFor:h=>`explore-${ps()}-${String(h)}`},i);return fs(c,l)}function us(e,t){return t.flatMap(r=>{let n=e[r.idx];return n==null?[]:[{actions:[...n.nav,...n.steps].map(o=>Rt(o.action)),label:`${n.test}#${String(n.index)}`}]})}async function ms({assign:e,corpus:t,lockfile:r}){return(await $t(r,{name:e.depotTest.name,test:e.depotTest},{generate:ue,materialize:wt(ue,r.valueSpaces),params:void 0})).match(o=>Et({actionHashes:t.map(i=>Pt(r,i)),corpus:t,covered:new Set,depotSnapshot:o.snapshot,lens:At(r),lensId:e.lensId,maxLength:e.maxLength,witnessTrail:e.firings}),()=>null)}function fs(e,t){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:[...t]}}var hs={covered:0,deferred:0,findings:0,saturated:!1,total:0};async function Hr(e){let t=w(e.cwd).match(c=>({config:c}),c=>({failure:c}));if("failure"in t)return{failure:t.failure,kind:"config-failed"};let r=t.config,n=await ct(e.cwd);if(n.result.isErr())return{error:n.result.error,kind:"compile-failed"};let o={fingerprint:n.fingerprint,lockfile:n.result.value},i=jt({onChange:()=>{}});if(!i.holder())return i.stop(),{kind:"explorer-busy"};let a=me({debugDir:gs.join(e.cwd,".ripplo","debug"),headed:e.headed,writeOtlpPortFile:!1}),s={executed:0,status:()=>hs},l=Lt({cwd:e.cwd,maxTrailLength:e.maxLength,executeTrail:(c,h)=>ke({assign:c,config:r,headed:e.headed,lockfile:o.lockfile,session:a,signal:h}),loadLockfile:()=>Promise.resolve(o),notifyWork:()=>{},onTrailDone:c=>{s.executed+=1,e.onTrail(s.executed,c,s.status())},probeApp:async()=>await Z(r.appUrl)==null});s.status=()=>l.status();try{await l.ready(),e.onReady(l.status());let c=await ys(l,e),h=l.status();return await l.stop(),await a.close(),c===0&&h.total===0?{kind:"no-work"}:{executed:c,kind:"completed",progress:h}}finally{i.stop()}}async function ys(e,t){let r=new AbortController().signal,n=async o=>{if(o>=t.trails)return o;let i=e.next();return i==null?o:(await i.run(r),n(o+1))};return n(0)}import{spawn as ks}from"child_process";import Mr from"fs";import ws from"net";import{setTimeout as qr}from"timers/promises";import{err as ie,ok as we}from"neverthrow";import{ResponseError as vs}from"vscode-jsonrpc/node";var Br=12e4,bs=300,Wr=5e3;async function z({cliEntry:e,cwd:t}){let r=_e(t),n=await ve(r);if(n!=null)return we(We(n,!1));let o=Cs({cliEntry:e,cwd:t});if(o!=null)return ie(o);let i=await Ps(r);return i==null?ie({deadlineMs:Br,kind:"connect-timeout",logPath:Fe(t)}):we(We(i,!0))}async function be({connection:e,onEvent:t,request:r,token:n}){let o=await xs({connection:e,onEvent:t,request:r,token:n});return o.kind==="transport"?ie(o.error):we(o)}async function Vr({connection:e,findingId:t,token:r}){try{let n=await e.rpc.sendRequest(Mt,{findingId:t},r),o=Ht.safeParse(n);return o.success?we(o.data):ie({kind:"bad-frame"})}catch{return ie({kind:"connection-lost"})}}async function K(e){try{await e.rpc.sendRequest(Bt)}catch{}}async function Gr(e){let t=await ve(_e(e));if(t==null)return{kind:"not-running"};let r=We(t,!1),n=await Promise.race([Ss(r),qr(Wr).then(()=>null)]);return t.destroy(),n==null?{kind:"unresponsive",timeoutMs:Wr}:{kind:"running",status:n}}async function Ss(e){try{let t=await e.rpc.sendRequest(qt),r=_t.safeParse(t);return r.success?r.data:null}catch{return null}}function xs({connection:e,onEvent:t,request:r,token:n}){let{rpc:o}=e;return new Promise(i=>{let a=s=>{i(s)};o.onNotification(Vt,s=>{let l=Ft.safeParse(s);if(!l.success){a({error:{kind:"bad-frame"},kind:"transport"});return}t(l.data.event)}),o.onNotification(Gt,s=>{let l=Nt.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(Wt,r,n).then(s=>{Ot.safeParse(s).success||a({error:{kind:"bad-frame"},kind:"transport"})}).catch(s=>{a(Rs(s))})})}function Rs(e){if(e instanceof vs){let t=Ut.safeParse(e.data);return t.success?{error:t.data,kind:"daemon-error"}:{error:{kind:"bad-frame"},kind:"transport"}}return{error:{kind:"connection-lost"},kind:"transport"}}function We(e,t){let r=Dt(e);return r.listen(),{rpc:r,socket:e,spawned:t}}function ve(e){return new Promise(t=>{let r=ws.connect(e);r.once("connect",()=>{t(r)}),r.once("error",()=>{t(null)})})}function Cs({cliEntry:e,cwd:t}){try{ce(t);let r=Mr.openSync(Fe(t),"a");return ks(process.execPath,[e,"daemon"],{cwd:t,detached:!0,stdio:["ignore",r,r]}).unref(),Mr.closeSync(r),null}catch(r){return{kind:"spawn-failed",message:r instanceof Error?r.message:String(r)}}}async function Ps(e){let t=Date.now()+Br,r=await ve(e);for(;r==null&&Date.now()<t;)await qr(bs),r=await ve(e);return r}function Jr(e){if(e.pending.length===0&&e.recurrentFlaky.length===0)return"explore: no pending findings";let t=e.pending.length===0?[]:[`explore: ${String(e.pending.length)} pending finding(s) \u2014 triage in layer order, top first`,...e.pending.map(o=>Es(o))],r=e.recurrentFlaky.length===0?[]:[`recurrent flaky-candidates (same divergence ${String(e.recurrentFlaky.length)}x, no deterministic repro \u2014 triage after findings):`,...e.recurrentFlaky.map(o=>js(o))],n=d("debug","triaging each finding (evidence -> classify -> fix -> replay)");return[...t,...r,"","replay after a fix: npx ripplo explore replay <id>",n].join(`
213
+ `).trim()}function zr(e,t){switch(t.kind){case"resolved":return`explore: ${e} replayed clean \u2014 finding resolved, its targets covered under the current model`;case"unreachable":return`explore: ${e}'s trail is no longer plannable under the current model \u2014 the violating composition is unreachable; finding resolved (if you narrowed a given/when, make sure a test covers the excluded state)`;case"still-failing":{let r=t.runId==null?"":` (fresh evidence: run ${t.runId})`;return`explore: ${e} still reproduces \u2014 same divergence signature${r}`}case"diverged":{let r=t.runId==null?"":` (captured run ${t.runId})`;return`explore: ${e} failed with a DIFFERENT divergence signature \u2014 new finding recorded${r}`}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 (${t.reason})`;case"error":return`explore: replay of ${e} failed (${t.reason})`}}function Kr({executed:e,progress:t,trail:r}){let n=r.trail.flatMap(o=>[` ${o.label}`,...o.actions.map(i=>` ${i}`)]);return[`trail ${String(e)} ${r.kind} \u2014 ${te(t)}`,...n,` state: ${r.label}`].join(`
214
+ `)}function Qr(e){switch(e.kind){case"config-failed":return E(e.failure);case"compile-failed":return x(e.error);case"no-work":return"explore: nothing to explore \u2014 no transitions harvested from the suite";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`}
215
215
  ${te(e.progress)}
216
- findings land in .ripplo/.local/explore-ledger.jsonl`}}function Qr(e){let t=e.evidence.map(n=>` ${n}`),r=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.oracleLayer} depot=${e.depot}`,` seen ${String(e.occurrences)}x between ${qe(e.firstSeen)} and ${qe(e.lastSeen)}`,` trail: ${e.trail.join(" -> ")}`," evidence:",...t,...r,` replay after a fix: npx ripplo explore replay ${e.id}`].join(`
217
- `)}function Yr(e){return`explore: no pending finding ${e} \u2014 check ids with: npx ripplo explore findings`}function Cs(e){let t=e.runId==null?"no captured run":`run ${e.runId}`;return[` ${e.id} layer=${e.oracleLayer} seen ${String(e.occurrences)}x (last ${qe(e.lastSeen)}) depot=${e.depot} ${t}`,` diverged: ${xs(e.parts)}`,` trail: ${e.trail.join(" -> ")}`].join(`
218
- `)}function qe(e){return e.slice(0,16).replace("T"," ")}function xs(e){let t=e.at(0);if(t==null)return"no parts recorded";let r=e.length>1?` (+${String(e.length-1)} more)`:"";return`${Ps(t)}${r}`}function Ps(e){switch(e.kind){case"consistency":{let t="entity"in e.divergence?e.divergence.entity:e.divergence.singleton,r=e.step==null?"":` at "${e.step.intent}"`;return`${e.divergence.kind} on ${t}${r}`}case"obligation":return`${e.source} check failed at "${e.step.intent}"`;case"unfireable":return`step "${e.intent}" unfireable (${e.reason})`;case"driver-error":return`${e.error} at "${e.step}"`;case"illegal-transition":return`illegal transition at "${e.step}"`}}function Es(e){let t=e.trail.map(r=>r.split("|").at(0)??r);return` ${e.id} seen ${String(e.occurrences)}x depot=${e.depot}
219
- trail: ${t.join(" -> ")}`}import Is from"fuse.js";var $s=3e3;async function Se(e){W(e);let t=k(e).unwrapOr(void 0);if(t==null)return null;try{return await fetch(`${t.ripploServerUrl}/health`,{signal:AbortSignal.timeout($s)}),null}catch(r){return{detail:r instanceof Error?r.message:String(r),serverUrl:t.ripploServerUrl}}}function Re(e){return e.includes("localhost")||e.includes("127.0.0.1")}function Be(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/tests files are dirty. Pass test ids, add tests to scope (${d("scope")}), or use --all.`;case"unknown-ids":{let t=e.known.map(o=>P(o)),r=e.unknown.flatMap(o=>js(o,t)),n=r.length>0?[`Did you mean: ${[...new Set(r)].join(", ")}`]:[];return[`Unknown test id(s): ${e.unknown.join(", ")}`,...n,"The test id is the slug shown in run output and scope listings (the quoted intent string also works). All known ids:",...t.map(o=>` ${o}`)].join(`
220
- `)}}}function Xr({failed:e,notRun:t,passed:r}){let n=t>0?`, ${String(t)} not run`:"",o=e>0?`
221
- ${d("debug")}`:"";return`${String(r)} passed, ${String(e)} failed${n} (${String(r+e+t)} total)${o}`}function Ce({debugDir:e,event:t}){if(t.kind==="test-started")return`${C.dim("RUN ")} ${t.testName}`;switch(t.outcome.kind){case"pass":return`${C.good("PASS")} ${t.testName}`;case"findings":return[`${C.bad("FAIL")} ${t.testName} \u2014 ${String(t.outcome.findingLines.length)} finding(s)`,...t.outcome.findingLines,St({debugDir:e,runId:t.runId})].join(`
222
- `);case"error":return`${C.bad("ERROR")} ${t.testName} \u2014 ${t.outcome.detail}`;case"dispatch-error":return`${C.bad("ERROR")} ${t.testName} \u2014 failed to dispatch (${t.outcome.detail})`;case"infra-error":return`${C.bad("ERROR")} ${t.testName} \u2014 not run: Ripplo server unreachable (server-side issue, not your local environment): ${t.outcome.detail}`}}function xe({detail:e,serverUrl:t}){return Re(t)?`Ripplo server at ${t} is not running (${e}). Tests were not started. Start the dev server (\`pnpm dev\`) and re-run.`:`Ripplo server at ${t} 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 Pe(e){switch(e.code){case"compile-failed":{let t=e.diagnostics.length===0?[]:["",...e.diagnostics];return[`Compilation failed in the daemon (${e.detail}). Run \`npx ripplo compile\` for the full output.`,...t].join(`
216
+ findings land in .ripplo/.local/explore-ledger.jsonl`}}function Yr(e){let t=e.evidence.map(n=>` ${n}`),r=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.oracleLayer} depot=${e.depot}`,` seen ${String(e.occurrences)}x between ${qe(e.firstSeen)} and ${qe(e.lastSeen)}`,` trail: ${e.trail.join(" -> ")}`," evidence:",...t,...r,` replay after a fix: npx ripplo explore replay ${e.id}`].join(`
217
+ `)}function Xr(e){return`explore: no pending finding ${e} \u2014 check ids with: npx ripplo explore findings`}function Es(e){let t=e.runId==null?"no captured run":`run ${e.runId}`;return[` ${e.id} layer=${e.oracleLayer} seen ${String(e.occurrences)}x (last ${qe(e.lastSeen)}) depot=${e.depot} ${t}`,` diverged: ${$s(e.parts)}`,` trail: ${e.trail.join(" -> ")}`].join(`
218
+ `)}function qe(e){return e.slice(0,16).replace("T"," ")}function $s(e){let t=e.at(0);if(t==null)return"no parts recorded";let r=e.length>1?` (+${String(e.length-1)} more)`:"";return`${Is(t)}${r}`}function Is(e){switch(e.kind){case"consistency":{let t="entity"in e.divergence?e.divergence.entity:e.divergence.singleton,r=e.step==null?"":` at "${e.step.intent}"`;return`${e.divergence.kind} on ${t}${r}`}case"obligation":return`${e.source} check failed at "${e.step.intent}"`;case"unfireable":return`step "${e.intent}" unfireable (${e.reason})`;case"driver-error":return`${e.error} at "${e.step}"`;case"illegal-transition":return`illegal transition at "${e.step}"`}}function js(e){let t=e.trail.map(r=>r.split("|").at(0)??r);return` ${e.id} seen ${String(e.occurrences)}x depot=${e.depot}
219
+ trail: ${t.join(" -> ")}`}import Ls from"fuse.js";var As=3e3;async function Se(e){W(e);let t=w(e).unwrapOr(void 0);if(t==null)return null;try{return await fetch(`${t.ripploServerUrl}/health`,{signal:AbortSignal.timeout(As)}),null}catch(r){return{detail:r instanceof Error?r.message:String(r),serverUrl:t.ripploServerUrl}}}function xe(e){return e.includes("localhost")||e.includes("127.0.0.1")}function Be(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/tests files are dirty. Pass test ids, add tests to scope (${d("scope")}), or use --all.`;case"unknown-ids":{let t=e.known.map(o=>P(o)),r=e.unknown.flatMap(o=>Ts(o,t)),n=r.length>0?[`Did you mean: ${[...new Set(r)].join(", ")}`]:[];return[`Unknown test id(s): ${e.unknown.join(", ")}`,...n,"The test id is the slug shown in run output and scope listings (the quoted intent string also works). All known ids:",...t.map(o=>` ${o}`)].join(`
220
+ `)}}}function Zr({failed:e,notRun:t,passed:r}){let n=t>0?`, ${String(t)} not run`:"",o=e>0?`
221
+ ${d("debug")}`:"";return`${String(r)} passed, ${String(e)} failed${n} (${String(r+e+t)} total)${o}`}function Re({debugDir:e,event:t}){if(t.kind==="test-started")return`${R.dim("RUN ")} ${t.testName}`;switch(t.outcome.kind){case"pass":return`${R.good("PASS")} ${t.testName}`;case"findings":return[`${R.bad("FAIL")} ${t.testName} \u2014 ${String(t.outcome.findingLines.length)} finding(s)`,...t.outcome.findingLines,St({debugDir:e,runId:t.runId})].join(`
222
+ `);case"error":return`${R.bad("ERROR")} ${t.testName} \u2014 ${t.outcome.detail}`;case"dispatch-error":return`${R.bad("ERROR")} ${t.testName} \u2014 failed to dispatch (${t.outcome.detail})`;case"infra-error":return`${R.bad("ERROR")} ${t.testName} \u2014 not run: Ripplo server unreachable (server-side issue, not your local environment): ${t.outcome.detail}`}}function Ce({detail:e,serverUrl:t}){return xe(t)?`Ripplo server at ${t} is not running (${e}). Tests were not started. Start the dev server (\`pnpm dev\`) and re-run.`:`Ripplo server at ${t} 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 Pe(e){switch(e.code){case"compile-failed":{let t=e.diagnostics.length===0?[]:["",...e.diagnostics];return[`Compilation failed in the daemon (${e.detail}). Run \`npx ripplo compile\` for the full output.`,...t].join(`
223
223
  `)}case"selection-conflicting-flags":return Be({kind:"conflicting-flags"});case"selection-nothing-selected":return Be({kind:"nothing-selected"});case"selection-unknown-ids":return Be({kind:"unknown-ids",known:e.known,unknown:e.unknown});case"app-unreachable":return`Your dev server is not running: nothing responded at ${e.url} (${e.detail}). Tests were not started. Start the app (e.g. \`pnpm dev\`) 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}
224
- Verify the dev session is live (\`npx ripplo doctor\`, ${d("start")}), or pass test ids / --all explicitly.`;case"sync-failed":{let t=/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}${t}`}case"bad-message":return"Daemon rejected the request (protocol mismatch \u2014 rebuild/update the CLI and restart the daemon)."}}function A(e){switch(e.kind){case"spawn-failed":return`Failed to start \`ripplo daemon\`: ${As(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 (tail it: \`tail -50 ${e.logPath}\`).`,"Common causes: a stale socket (`rm .ripplo/.local/daemon.sock`), another daemon holding the dev lock, or the Ripplo server being unreachable."].join(`
224
+ Verify the dev session is live (\`npx ripplo doctor\`, ${d("start")}), or pass test ids / --all explicitly.`;case"sync-failed":{let t=/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}${t}`}case"bad-message":return"Daemon rejected the request (protocol mismatch \u2014 rebuild/update the CLI and restart the daemon)."}}function A(e){switch(e.kind){case"spawn-failed":return`Failed to start \`ripplo daemon\`: ${Ds(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 (tail it: \`tail -50 ${e.logPath}\`).`,"Common causes: a stale socket (`rm .ripplo/.local/daemon.sock`), another daemon holding the dev lock, or the Ripplo server being unreachable."].join(`
225
225
  `);case"connection-lost":return["Connection to the daemon was lost before the run finished \u2014 the daemon likely crashed or was killed.","Check .ripplo/.local/daemon.log, then restart with `npx ripplo daemon` (or rerun \u2014 `ripplo run` auto-starts it)."].join(`
226
- `);case"bad-frame":return"Received a malformed frame from the daemon (version mismatch \u2014 rebuild/update the CLI and restart the daemon)."}}function js(e,t){return new Is(t,{includeScore:!0,threshold:.5}).search(P(e)).slice(0,3).map(n=>n.item)}function As(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}async function Zr({findingId:e,json:t}){await Lt(process.cwd()).match(r=>{if(e==null){let i=t?JSON.stringify(r,null,2):Gr(r);process.stdout.write(`${i}
227
- `);return}let n=r.pending.find(i=>i.id===e);n==null&&(process.stderr.write(`${Yr(e)}
228
- `),process.exit(1));let o=t?JSON.stringify(n,null,2):Qr(n);process.stdout.write(`${o}
226
+ `);case"bad-frame":return"Received a malformed frame from the daemon (version mismatch \u2014 rebuild/update the CLI and restart the daemon)."}}function Ts(e,t){return new Ls(t,{includeScore:!0,threshold:.5}).search(P(e)).slice(0,3).map(n=>n.item)}function Ds(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}async function en({findingId:e,json:t}){await Tt(process.cwd()).match(r=>{if(e==null){let i=t?JSON.stringify(r,null,2):Jr(r);process.stdout.write(`${i}
227
+ `);return}let n=r.pending.find(i=>i.id===e);n==null&&(process.stderr.write(`${Xr(e)}
228
+ `),process.exit(1));let o=t?JSON.stringify(n,null,2):Yr(n);process.stdout.write(`${o}
229
229
  `)},r=>{process.stderr.write(`explore: ledger unreadable (${r.kind})
230
- `),process.exit(1)})}async function en({findingId:e}){let t=process.argv[1];t==null&&(process.stderr.write(`${A({kind:"spawn-failed",message:"process.argv[1] missing"})}
230
+ `),process.exit(1)})}async function tn({findingId:e}){let t=process.argv[1];t==null&&(process.stderr.write(`${A({kind:"spawn-failed",message:"process.argv[1] missing"})}
231
231
  `),process.exit(1));let r=await z({cliEntry:t,cwd:process.cwd()});r.isErr()&&(process.stderr.write(`${A(r.error)}
232
- `),process.exit(1));let n=r.value,o=new Ls;process.once("SIGINT",()=>{o.cancel(),n.socket.destroy(),process.exit(130)});let a=await(await Br({connection:n,findingId:e,token:o.token})).match(async s=>(process.stdout.write(`${Jr(e,s)}
232
+ `),process.exit(1));let n=r.value,o=new Os;process.once("SIGINT",()=>{o.cancel(),n.socket.destroy(),process.exit(130)});let a=await(await Vr({connection:n,findingId:e,token:o.token})).match(async s=>(process.stdout.write(`${zr(e,s)}
233
233
  `),n.spawned&&await K(n),s.kind==="resolved"||s.kind==="unreachable"?0:1),s=>(process.stderr.write(`${A(s)}
234
- `),Promise.resolve(1)));n.socket.destroy(),process.exit(a)}async function tn(e){let t=await Ur({cwd:process.cwd(),headed:e.headed,trails:e.trails,onReady:r=>{process.stdout.write(`${te(r)}
235
- `)},onTrail:(r,n,o)=>{process.stdout.write(`${zr({executed:r,firings:n.firings,label:n.label,progress:o})}
236
- `)}});process.stdout.write(`${Kr(t)}
237
- `),(t.kind==="config-failed"||t.kind==="compile-failed")&&process.exit(1)}import{graphql as Ts}from"gql.tada";var Ds=Ts(`
234
+ `),Promise.resolve(1)));n.socket.destroy(),process.exit(a)}async function rn(e){let t=await Hr({cwd:process.cwd(),headed:e.headed,maxLength:e.maxLength,trails:e.trails,onReady:r=>{process.stdout.write(`${te(r)}
235
+ `)},onTrail:(r,n,o)=>{process.stdout.write(`${Kr({executed:r,progress:o,trail:n})}
236
+ `)}});process.stdout.write(`${Qr(t)}
237
+ `),(t.kind==="config-failed"||t.kind==="compile-failed")&&process.exit(1)}import{graphql as _s}from"gql.tada";var Fs=_s(`
238
238
  query ProjectsList {
239
239
  projects {
240
240
  id
241
241
  name
242
242
  }
243
243
  }
244
- `);async function rn(){let e=O();e==null&&(process.stderr.write("Not authenticated. Run `ripplo auth login` first.\n"),process.exit(1));let t=I(),n=((await p({config:U({serverUrl:t,token:e}),document:Ds,variables:void 0})).projects??[]).map(o=>({id:o.id,name:o.name}));process.stdout.write(`${JSON.stringify({projects:n},null,2)}
245
- `)}import{graphql as Os}from"gql.tada";function nn({id:e,kind:t,title:r}){return`Caught bug reported (${t}): "${r}" [${e}]`}var _s=Os(`
244
+ `);async function nn(){let e=O();e==null&&(process.stderr.write("Not authenticated. Run `ripplo auth login` first.\n"),process.exit(1));let t=I(),n=((await p({config:U({serverUrl:t,token:e}),document:Fs,variables:void 0})).projects??[]).map(o=>({id:o.id,name:o.name}));process.stdout.write(`${JSON.stringify({projects:n},null,2)}
245
+ `)}import{graphql as Ns}from"gql.tada";function on({id:e,kind:t,title:r}){return`Caught bug reported (${t}): "${r}" [${e}]`}var Us=Ns(`
246
246
  mutation ReportCaughtBug(
247
247
  $projectId: String!
248
248
  $kind: CaughtBugKind!
@@ -272,41 +272,41 @@ Verify the dev session is live (\`npx ripplo doctor\`, ${d("start")}), or pass t
272
272
  }
273
273
  }
274
274
  }
275
- `);async function on(e){let t=j(),n=(await p({config:t,document:_s,variables:{kind:e.kind,projectId:t.projectId,rootCause:e.rootCause,runId:e.runId??null,surfacedBy:e.surfacedBy,title:e.title,workflowSlug:e.testId==null?null:P(e.testId)}})).reportCaughtBug;if(n?.__typename!=="CaughtBug"){let o=n?.__typename==="WorkflowNotFoundError"?n.message:null;process.stderr.write(`${o??"reportCaughtBug failed"}
276
- `),process.exit(1)}process.stdout.write(`${nn({id:n.id,kind:e.kind,title:e.title})}
277
- `)}import Fs from"path";import{CancellationTokenSource as Ns}from"vscode-jsonrpc/node";async function sn({all:e,headed:t,ids:r,keepAlive:n}){let o=process.cwd(),i=process.argv[1];i==null&&se(A({kind:"spawn-failed",message:"process.argv[1] missing"}));let a=await Se(o);a!=null&&se(xe(a));let l=(await z({cliEntry:i,cwd:o})).match($=>$,$=>se(A($))),c=new Ns;process.once("SIGINT",()=>{c.cancel(),l.socket.destroy(),process.exit(130)});let b=Fs.join(o,".ripplo","debug"),y=(await be({connection:l,request:{all:e,headed:t,tests:[...r]},token:c.token,onEvent:$=>{let x=Ce({debugDir:b,event:$});x!=null&&process.stdout.write(`${x}
278
- `)}})).match($=>$,$=>se(A($)));await Us({connection:l,keepAlive:n,result:y})}async function Us({connection:e,keepAlive:t,result:r}){e.spawned&&!t&&await K(e),await new Promise(n=>{e.socket.end(n)}),r.kind==="daemon-error"&&se(Pe(r.error)),process.stdout.write(`${Xr(r)}
275
+ `);async function sn(e){let t=j(),n=(await p({config:t,document:Us,variables:{kind:e.kind,projectId:t.projectId,rootCause:e.rootCause,runId:e.runId??null,surfacedBy:e.surfacedBy,title:e.title,workflowSlug:e.testId==null?null:P(e.testId)}})).reportCaughtBug;if(n?.__typename!=="CaughtBug"){let o=n?.__typename==="WorkflowNotFoundError"?n.message:null;process.stderr.write(`${o??"reportCaughtBug failed"}
276
+ `),process.exit(1)}process.stdout.write(`${on({id:n.id,kind:e.kind,title:e.title})}
277
+ `)}import Hs from"path";import{CancellationTokenSource as Ms}from"vscode-jsonrpc/node";async function an({all:e,headed:t,ids:r,keepAlive:n}){let o=process.cwd(),i=process.argv[1];i==null&&se(A({kind:"spawn-failed",message:"process.argv[1] missing"}));let a=await Se(o);a!=null&&se(Ce(a));let l=(await z({cliEntry:i,cwd:o})).match($=>$,$=>se(A($))),c=new Ms;process.once("SIGINT",()=>{c.cancel(),l.socket.destroy(),process.exit(130)});let h=Hs.join(o,".ripplo","debug"),k=(await be({connection:l,request:{all:e,headed:t,tests:[...r]},token:c.token,onEvent:$=>{let C=Re({debugDir:h,event:$});C!=null&&process.stdout.write(`${C}
278
+ `)}})).match($=>$,$=>se(A($)));await Ws({connection:l,keepAlive:n,result:k})}async function Ws({connection:e,keepAlive:t,result:r}){e.spawned&&!t&&await K(e),await new Promise(n=>{e.socket.end(n)}),r.kind==="daemon-error"&&se(Pe(r.error)),process.stdout.write(`${Zr(r)}
279
279
  `),r.failed>0&&process.exit(1),process.exit(r.notRun>0?2:0)}function se(e){process.stderr.write(`${e}
280
- `),process.exit(1)}import Hs from"path";import{err as $e,ok as Ve}from"neverthrow";async function an(){let e=process.cwd(),t=D.child({worker:process.pid}),r=ir(),n=me({debugDir:Hs.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=>{t.error({err:a},"worker unhandled rejection")}),process.on("uncaughtException",a=>{t.error({err:a},"worker uncaught exception"),i(1)}),r.onRequest(er,async(a,s)=>{let l=Qt.safeParse(a);if(!l.success)return Ee("bad-run-assign");let c=l.data;W(e);let b=k(e).match(x=>x,x=>x.kind);if(typeof b=="string")return Ee(`config:${b}`);let u=await ln(c.lockfileFingerprint,o,r);if(u.isErr())return Ee(`lockfile-unavailable:${u.error}`);let y=u.value,$=y.tests.find(x=>P(x.name)===c.workflowSlug);if($==null)return Ee(`no-test:${c.workflowSlug}`);try{let x=await Kt({config:b,cwd:e,headed:c.headed,lockfile:y,runId:c.runId,session:n,signal:Ne(s),test:$});return{outcome:Jt(x),serverNotified:!0}}catch(x){if(x instanceof Ye)return{outcome:{detail:x.message,kind:"infra-error"},serverNotified:!1};throw x}}),r.onRequest(rr,(a,s)=>Ms({cache:o,connection:r,cwd:e,raw:a,session:n,token:s})),r.onNotification(nr,a=>{let s=Yt.safeParse(a);s.success&&n.injectSpan(s.data.runId,s.data.span)}),r.onNotification(or,()=>{i(0)}),r.sendNotification(Zt),await new Promise(()=>{})}function Ee(e){return{outcome:{detail:e,kind:"error"},serverNotified:!1}}async function Ms({cache:e,connection:t,cwd:r,raw:n,session:o,token:i}){let a=Xt.safeParse(n);if(!a.success)return{kind:"error",reason:"bad-assign",rows:[]};let s=a.data;W(r);let l=k(r).match(u=>u,u=>u.kind);if(typeof l=="string")return{kind:"error",reason:`config:${l}`,rows:[]};let c=await ln(s.lockfileFingerprint,e,t);if(c.isErr())return{kind:"error",reason:`lockfile:${c.error}`,rows:[]};let b=c.value;return ke({assign:s,config:l,headed:!1,lockfile:b,session:o,signal:Ne(i)})}async function ln(e,t,r){return t.entry!=null&&t.entry.fingerprint===e?Ve(t.entry.lockfile):(await Ws(r,e)).andThen(o=>o.unavailable!=null?$e(o.unavailable):o.lockfileJson==null?$e("empty-reply"):qs(o.lockfileJson).map(i=>(t.entry={fingerprint:e,lockfile:i},i)))}async function Ws(e,t){try{return Ve(await e.sendRequest(tr,{fingerprint:t}))}catch(r){return $e(`transport:${r instanceof Error?r.message:"unknown"}`)}}function qs(e){try{return Ve(lt(N,e))}catch(t){return $e(`worker-decode-failed:${t instanceof Error?t.message.slice(0,200):"unknown"}`)}}async function cn(){await an()}import{createRequire as Bs}from"module";import{existsSync as Vs}from"fs";import{readFile as pn,writeFile as Gs}from"fs/promises";import{fileURLToPath as Js}from"url";import Ge from"path";import{err as ae,ok as zs}from"neverthrow";import{z as T}from"zod";var Ks={height:800,width:1280};async function un({cwd:e,moment:t,runId:r}){let n=Ge.join(e,".ripplo","debug",r),o=Ge.join(n,"behavior.jsonl");if(!Vs(o))return ae({kind:"run-not-found",runId:r});let i=await Ys(o),a=i[0],s=i.at(-1);if(a==null||s==null)return ae({kind:"no-rrweb-events",runId:r});let l=s.timestamp-a.timestamp,c=t.kind==="offset"?t.offsetMs:t.at-a.timestamp;if(c<0||c>l)return ae({durationMs:l,firstTimestamp:a.timestamp,kind:"moment-out-of-range",lastTimestamp:s.timestamp,moment:t});let b=Ge.join(n,`snapshot-${String(Math.round(c))}ms.png`);return(await na({events:i,offsetMs:c,pngPath:b})).map(()=>({durationMs:l,offsetMs:c,pngPath:b}))}var Qs=T.object({event:T.looseObject({timestamp:T.number(),type:T.number()}),kind:T.literal("rrweb")});async function Ys(e){return(await pn(e,"utf8")).split(`
281
- `).filter(r=>r.length>0).map(r=>Xs(r)).map(r=>Qs.safeParse(r)).flatMap(r=>r.success?[r.data.event]:[])}function Xs(e){try{return JSON.parse(e)}catch{return null}}var Zs=T.object({data:T.looseObject({height:T.number(),width:T.number()}),type:T.literal(4)});function ea(e){let t=e.flatMap(r=>{let n=Zs.safeParse(r);return n.success?[n.data]:[]})[0];return t==null?Ks:{height:t.data.height,width:t.data.width}}var ta=`<!doctype html><html><head><style>
280
+ `),process.exit(1)}import qs from"path";import{err as $e,ok as Ve}from"neverthrow";async function ln(){let e=process.cwd(),t=D.child({worker:process.pid}),r=sr(),n=me({debugDir:qs.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=>{t.error({err:a},"worker unhandled rejection")}),process.on("uncaughtException",a=>{t.error({err:a},"worker uncaught exception"),i(1)}),r.onRequest(tr,async(a,s)=>{let l=Yt.safeParse(a);if(!l.success)return Ee("bad-run-assign");let c=l.data;W(e);let h=w(e).match(C=>C,C=>C.kind);if(typeof h=="string")return Ee(`config:${h}`);let u=await cn(c.lockfileFingerprint,o,r);if(u.isErr())return Ee(`lockfile-unavailable:${u.error}`);let k=u.value,$=k.tests.find(C=>P(C.name)===c.workflowSlug);if($==null)return Ee(`no-test:${c.workflowSlug}`);try{let C=await Qt({config:h,cwd:e,headed:c.headed,lockfile:k,runId:c.runId,session:n,signal:Ne(s),test:$});return{outcome:zt(C),serverNotified:!0}}catch(C){if(C instanceof Ye)return{outcome:{detail:C.message,kind:"infra-error"},serverNotified:!1};throw C}}),r.onRequest(nr,(a,s)=>Bs({cache:o,connection:r,cwd:e,raw:a,session:n,token:s})),r.onNotification(or,a=>{let s=Xt.safeParse(a);s.success&&n.injectSpan(s.data.runId,s.data.span)}),r.onNotification(ir,()=>{i(0)}),r.sendNotification(er),await new Promise(()=>{})}function Ee(e){return{outcome:{detail:e,kind:"error"},serverNotified:!1}}async function Bs({cache:e,connection:t,cwd:r,raw:n,session:o,token:i}){let a=Zt.safeParse(n);if(!a.success)return{kind:"error",reason:"bad-assign",rows:[],trail:[]};let s=a.data;W(r);let l=w(r).match(u=>u,u=>u.kind);if(typeof l=="string")return{kind:"error",reason:`config:${l}`,rows:[],trail:[]};let c=await cn(s.lockfileFingerprint,e,t);if(c.isErr())return{kind:"error",reason:`lockfile:${c.error}`,rows:[],trail:[]};let h=c.value;return ke({assign:s,config:l,headed:!1,lockfile:h,session:o,signal:Ne(i)})}async function cn(e,t,r){return t.entry!=null&&t.entry.fingerprint===e?Ve(t.entry.lockfile):(await Vs(r,e)).andThen(o=>o.unavailable!=null?$e(o.unavailable):o.lockfileJson==null?$e("empty-reply"):Gs(o.lockfileJson).map(i=>(t.entry={fingerprint:e,lockfile:i},i)))}async function Vs(e,t){try{return Ve(await e.sendRequest(rr,{fingerprint:t}))}catch(r){return $e(`transport:${r instanceof Error?r.message:"unknown"}`)}}function Gs(e){try{return Ve(lt(N,e))}catch(t){return $e(`worker-decode-failed:${t instanceof Error?t.message.slice(0,200):"unknown"}`)}}async function dn(){await ln()}import{createRequire as Js}from"module";import{existsSync as zs}from"fs";import{readFile as un,writeFile as Ks}from"fs/promises";import{fileURLToPath as Qs}from"url";import Ge from"path";import{err as ae,ok as Ys}from"neverthrow";import{z as T}from"zod";var Xs={height:800,width:1280};async function mn({cwd:e,moment:t,runId:r}){let n=Ge.join(e,".ripplo","debug",r),o=Ge.join(n,"behavior.jsonl");if(!zs(o))return ae({kind:"run-not-found",runId:r});let i=await ea(o),a=i[0],s=i.at(-1);if(a==null||s==null)return ae({kind:"no-rrweb-events",runId:r});let l=s.timestamp-a.timestamp,c=t.kind==="offset"?t.offsetMs:t.at-a.timestamp;if(c<0||c>l)return ae({durationMs:l,firstTimestamp:a.timestamp,kind:"moment-out-of-range",lastTimestamp:s.timestamp,moment:t});let h=Ge.join(n,`snapshot-${String(Math.round(c))}ms.png`);return(await sa({events:i,offsetMs:c,pngPath:h})).map(()=>({durationMs:l,offsetMs:c,pngPath:h}))}var Zs=T.object({event:T.looseObject({timestamp:T.number(),type:T.number()}),kind:T.literal("rrweb")});async function ea(e){return(await un(e,"utf8")).split(`
281
+ `).filter(r=>r.length>0).map(r=>ta(r)).map(r=>Zs.safeParse(r)).flatMap(r=>r.success?[r.data.event]:[])}function ta(e){try{return JSON.parse(e)}catch{return null}}var ra=T.object({data:T.looseObject({height:T.number(),width:T.number()}),type:T.literal(4)});function na(e){let t=e.flatMap(r=>{let n=ra.safeParse(r);return n.success?[n.data]:[]})[0];return t==null?Xs:{height:t.data.height,width:t.data.width}}var oa=`<!doctype html><html><head><style>
282
282
  html, body { margin: 0; padding: 0; }
283
283
  .replayer-wrapper { position: relative; }
284
284
  .replayer-mouse, .replayer-mouse-tail { display: none; }
285
285
  iframe { border: none; }
286
- </style></head><body></body></html>`,ra="*, *::before, *::after { animation-duration: 0s !important; animation-delay: 0s !important; transition-duration: 0s !important; transition-delay: 0s !important; }";async function na({events:e,offsetMs:t,pngPath:r}){let n=await import("playwright").then(({chromium:o})=>o.launch({headless:!0})).catch(()=>null);if(n==null)return ae({detail:"chromium launch",kind:"browser-failed"});try{let o=await oa({browser:n,events:e,offsetMs:t});return await Gs(r,o),zs(void 0)}catch(o){return ae({detail:o instanceof Error?o.message:String(o),kind:"browser-failed"})}finally{await n.close()}}async function oa({browser:e,events:t,offsetMs:r}){let n=ea(t),o=await e.newPage({viewport:n});await o.setContent(ta),await o.addScriptTag({content:await aa()}),await o.evaluate(ia({events:t,offsetMs:r})),await o.evaluate(sa());let i=o.locator(".replayer-wrapper iframe").first();return(await i.count()>0?i:o).screenshot({type:"png"})}function ia({events:e,offsetMs:t}){return`(() => {
286
+ </style></head><body></body></html>`,ia="*, *::before, *::after { animation-duration: 0s !important; animation-delay: 0s !important; transition-duration: 0s !important; transition-delay: 0s !important; }";async function sa({events:e,offsetMs:t,pngPath:r}){let n=await import("playwright").then(({chromium:o})=>o.launch({headless:!0})).catch(()=>null);if(n==null)return ae({detail:"chromium launch",kind:"browser-failed"});try{let o=await aa({browser:n,events:e,offsetMs:t});return await Ks(r,o),Ys(void 0)}catch(o){return ae({detail:o instanceof Error?o.message:String(o),kind:"browser-failed"})}finally{await n.close()}}async function aa({browser:e,events:t,offsetMs:r}){let n=na(t),o=await e.newPage({viewport:n});await o.setContent(oa),await o.addScriptTag({content:await da()}),await o.evaluate(la({events:t,offsetMs:r})),await o.evaluate(ca());let i=o.locator(".replayer-wrapper iframe").first();return(await i.count()>0?i:o).screenshot({type:"png"})}function la({events:e,offsetMs:t}){return`(() => {
287
287
  const replayer = new globalThis.__RipploReplayer(${JSON.stringify(e)}, {
288
- insertStyleRules: [${JSON.stringify(ra)}],
288
+ insertStyleRules: [${JSON.stringify(ia)}],
289
289
  mouseTail: false,
290
290
  root: document.body,
291
291
  showWarning: false,
292
292
  });
293
293
  replayer.pause(${String(t)});
294
- })()`}function sa(){return`(async () => {
294
+ })()`}function ca(){return`(async () => {
295
295
  const doc = document.querySelector(".replayer-wrapper iframe")?.contentDocument;
296
296
  if (doc?.fonts != null) {
297
297
  await doc.fonts.ready;
298
298
  }
299
299
  await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
300
- })()`}var dn;function aa(){return dn??=pn(la(),"utf8"),dn}function la(){let e=Bs(import.meta.url);try{return e.resolve("@ripplo/rrweb-bundle/replay")}catch{return Js(new URL("assets/rrweb-replay.js",import.meta.url))}}function mn(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; URL-referenced images may be blank if the dev server is down."].join(`
301
- `)}function fn(e){return`${C.bad("FAIL")} \u2014 ${ca(e)}`}function gn(){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 ca(e){switch(e.kind){case"run-not-found":return`no debug artifacts for run ${e.runId} (.ripplo/debug/${e.runId}/behavior.jsonl missing). ${d("debug")}`;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 hn({at:e,offset:t,runId:r}){let n=da({at:e,offset:t});n==null&&(process.stderr.write(`${gn()}
302
- `),process.exit(1)),(await un({cwd:process.cwd(),moment:n,runId:r})).match(i=>{process.stdout.write(`${mn(i)}
303
- `)},i=>{process.stderr.write(`${fn(i)}
304
- `),process.exit(1)})}function da({at:e,offset:t}){if(e!=null&&t==null)return{at:e,kind:"absolute"};if(t!=null&&e==null)return{kind:"offset",offsetMs:t}}async function yn(){let e=j();try{let r=(await re(e.cwd,e)).match(i=>i,i=>{process.stderr.write(`${E(i)}
300
+ })()`}var pn;function da(){return pn??=un(pa(),"utf8"),pn}function pa(){let e=Js(import.meta.url);try{return e.resolve("@ripplo/rrweb-bundle/replay")}catch{return Qs(new URL("assets/rrweb-replay.js",import.meta.url))}}function fn(e){return[`${R.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; URL-referenced images may be blank if the dev server is down."].join(`
301
+ `)}function gn(e){return`${R.bad("FAIL")} \u2014 ${ua(e)}`}function hn(){return`${R.bad("FAIL")} \u2014 pass exactly one of --at <epoch-ms from behavior.jsonl> or --offset <ms from the start of the recording>.`}function ua(e){switch(e.kind){case"run-not-found":return`no debug artifacts for run ${e.runId} (.ripplo/debug/${e.runId}/behavior.jsonl missing). ${d("debug")}`;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 yn({at:e,offset:t,runId:r}){let n=ma({at:e,offset:t});n==null&&(process.stderr.write(`${hn()}
302
+ `),process.exit(1)),(await mn({cwd:process.cwd(),moment:n,runId:r})).match(i=>{process.stdout.write(`${fn(i)}
303
+ `)},i=>{process.stderr.write(`${gn(i)}
304
+ `),process.exit(1)})}function ma({at:e,offset:t}){if(e!=null&&t==null)return{at:e,kind:"absolute"};if(t!=null&&e==null)return{kind:"offset",offsetMs:t}}async function kn(){let e=j();try{let r=(await re(e.cwd,e)).match(i=>i,i=>{process.stderr.write(`${E(i)}
305
305
  `),process.exit(1)}),n=r.lockfile.tests.length,o=r.lockfile.entities.length;process.stdout.write(`synced ${String(n)} tests, ${String(o)} entities to dev session ${r.devSessionId}
306
306
  `)}catch(t){let r=t instanceof Error?t.message:String(t);process.stderr.write(`ripplo sync failed: ${r}
307
307
  `),process.stderr.write(`${d("setup","verify auth + server reachability")}
308
- `),process.exit(1)}}async function kn({explore:e,exploreConcurrency:t}){let{runDaemon:r}=await import("./daemon-PM5KE7TX.js");await r({explore:e,exploreConcurrency:t})}import{graphql as Ie}from"gql.tada";function wn(){return"No scope items added \u2014 the matched tests are already in scope (check `ripplo scope status`)."}function vn(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 `ripplo status`; stub a new one first via the testing DSL.",d("create")].join(`
309
- `)}import{graphql as pa}from"gql.tada";var bn=pa(`
308
+ `),process.exit(1)}}async function wn({explore:e,exploreConcurrency:t}){let{runDaemon:r}=await import("./daemon-7Z53WGU3.js");await r({explore:e,exploreConcurrency:t})}import{graphql as Ie}from"gql.tada";function vn(){return"No scope items added \u2014 the matched tests are already in scope (check `ripplo scope status`)."}function bn(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 `ripplo status`; stub a new one first via the testing DSL.",d("create")].join(`
309
+ `)}import{graphql as fa}from"gql.tada";var Sn=fa(`
310
310
  query ScopeStatus($projectId: String!, $cwd: String!) {
311
311
  project(id: $projectId) {
312
312
  id
@@ -326,7 +326,7 @@ iframe { border: none; }
326
326
  }
327
327
  }
328
328
  }
329
- `);var ua=Ie(`
329
+ `);var ga=Ie(`
330
330
  query ScopeWorkflowBySlug($projectId: String!, $cwd: String!, $slug: String!) {
331
331
  project(id: $projectId) {
332
332
  id
@@ -339,7 +339,7 @@ iframe { border: none; }
339
339
  }
340
340
  }
341
341
  }
342
- `),ma=Ie(`
342
+ `),ha=Ie(`
343
343
  mutation ScopeAddDirtyTests($projectId: String!, $cwd: String!, $workflowSlugs: [String!]!) {
344
344
  addDirtyTestsToScope(projectId: $projectId, cwd: $cwd, workflowSlugs: $workflowSlugs) {
345
345
  __typename
@@ -361,38 +361,38 @@ iframe { border: none; }
361
361
  }
362
362
  }
363
363
  }
364
- `),fa=Ie(`
364
+ `),ya=Ie(`
365
365
  mutation ScopeLink($id: ID!, $workflowId: String!) {
366
366
  linkScopeItem(id: $id, workflowId: $workflowId) {
367
367
  id
368
368
  }
369
369
  }
370
- `),ga=Ie(`
370
+ `),ka=Ie(`
371
371
  mutation ScopeRemoveMany($ids: [ID!]!) {
372
372
  removeScopeItems(ids: $ids)
373
373
  }
374
- `);async function Sn(e){let t=j();await ne(t);let n=(await p({config:t,document:bn,variables:{cwd:t.cwd,projectId:t.projectId}})).project?.devSession?.scopeItems??[];if(e.format==="json"){process.stdout.write(`${JSON.stringify(n,null,2)}
374
+ `);async function xn(e){let t=j();await ne(t);let n=(await p({config:t,document:Sn,variables:{cwd:t.cwd,projectId:t.projectId}})).project?.devSession?.scopeItems??[];if(e.format==="json"){process.stdout.write(`${JSON.stringify(n,null,2)}
375
375
  `);return}if(n.length===0){process.stdout.write("No scope items. Add via `ripplo scope add <test-ids..>` (variadic) or from the dashboard.\n");return}n.forEach(o=>{let i=o.workflow;if(i==null){process.stdout.write(` [intent] (${o.id}) ${o.label??""}
376
376
  `);return}let a=i.spec==null?"stub":"implemented";process.stdout.write(` [${a}] (${o.id}) ${i.slug} \u2014 ${i.name}
377
377
  `)})}async function Rn({testIds:e}){let t=j();await ne(t),(await re(t.cwd,t)).match(()=>{},s=>{process.stderr.write(`${E(s)}
378
- `),process.exit(1)});let o=(await p({config:t,document:ma,variables:{cwd:t.cwd,projectId:t.projectId,workflowSlugs:e.map(s=>P(s))}})).addDirtyTestsToScope;o?.__typename==="NoActiveDevSessionError"&&(process.stderr.write(`${o.message}
379
- `),process.exit(1)),o?.__typename==="UnknownWorkflowSlugsError"&&(process.stderr.write(`${vn(o.slugs)}
380
- `),process.exit(1));let i=o?.__typename==="MutationAddDirtyTestsToScopeSuccess"?o.data:[];if(i.length===0){process.stdout.write(`${wn()}
378
+ `),process.exit(1)});let o=(await p({config:t,document:ha,variables:{cwd:t.cwd,projectId:t.projectId,workflowSlugs:e.map(s=>P(s))}})).addDirtyTestsToScope;o?.__typename==="NoActiveDevSessionError"&&(process.stderr.write(`${o.message}
379
+ `),process.exit(1)),o?.__typename==="UnknownWorkflowSlugsError"&&(process.stderr.write(`${bn(o.slugs)}
380
+ `),process.exit(1));let i=o?.__typename==="MutationAddDirtyTestsToScopeSuccess"?o.data:[];if(i.length===0){process.stdout.write(`${vn()}
381
381
  `);return}let a=i.map(s=>s.workflow?.slug??"?").join(", ");process.stdout.write(`Added ${String(i.length)} scope item(s): ${a}
382
382
  `)}async function Cn({id:e,testId:t}){let r=j();await ne(r),(await re(r.cwd,r)).match(()=>{},i=>{process.stderr.write(`${E(i)}
383
- `),process.exit(1)});let o=await ha({cfg:r,slug:t});await p({config:r,document:fa,variables:{id:e,workflowId:o}}),process.stdout.write(`Linked scope item ${e} to ${t}
384
- `)}async function xn({ids:e}){let t=j();await ne(t);let n=(await p({config:t,document:ga,variables:{ids:[...e]}})).removeScopeItems??0;process.stdout.write(`Removed ${String(n)} scope item(s)
385
- `)}async function ha({cfg:e,slug:t}){let n=(await p({config:e,document:ua,variables:{cwd:e.cwd,projectId:e.projectId,slug:t}})).project?.devSession?.workflows?.[0];return n==null&&(process.stderr.write(`No workflow found with id "${t}". Create a stub first via the testing DSL.
383
+ `),process.exit(1)});let o=await wa({cfg:r,slug:t});await p({config:r,document:ya,variables:{id:e,workflowId:o}}),process.stdout.write(`Linked scope item ${e} to ${t}
384
+ `)}async function Pn({ids:e}){let t=j();await ne(t);let n=(await p({config:t,document:ka,variables:{ids:[...e]}})).removeScopeItems??0;process.stdout.write(`Removed ${String(n)} scope item(s)
385
+ `)}async function wa({cfg:e,slug:t}){let n=(await p({config:e,document:ga,variables:{cwd:e.cwd,projectId:e.projectId,slug:t}})).project?.devSession?.workflows?.[0];return n==null&&(process.stderr.write(`No workflow found with id "${t}". Create a stub first via the testing DSL.
386
386
  `),process.stderr.write(`${d("create")}
387
- `),process.exit(1)),n.id}async function Pn(e){let t=process.cwd(),r=await S(t);r.isErr()&&(process.stderr.write(`${R(r.error)}
388
- `),process.exit(1));let n=F(r.value),o=await Vr(t);if(e.format==="summary"){n.length>0&&process.stdout.write(`stub tests: ${n.join(", ")}
389
- `),process.stdout.write(`${Gt(o)}
387
+ `),process.exit(1)),n.id}async function En(e){let t=process.cwd(),r=await S(t);r.isErr()&&(process.stderr.write(`${x(r.error)}
388
+ `),process.exit(1));let n=F(r.value),o=await Gr(t);if(e.format==="summary"){n.length>0&&process.stdout.write(`stub tests: ${n.join(", ")}
389
+ `),process.stdout.write(`${Jt(o)}
390
390
  `);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)}
391
- `)}import je from"fs";import ka from"os";import En from"path";import{z as ya}from"zod";function f(e,t){let r=ya.custom(n=>typeof n=="object"&&n!==null&&"hook_event_name"in n&&n.hook_event_name===e);return{event:e,run:async n=>await t(r.parse(n))??void 0}}var $n=f("PreToolUse",e=>{if(e.tool_name!=="ExitPlanMode"||!w(e.cwd))return;let t=wa();if(t==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/tests/ test per affected flow. ${d("explore")}`,hookEventName:"PreToolUse"}};let r=je.readFileSync(t,"utf8");if(!/\.ripplo\/tests|Tests to implement|No e2e coverage needed/.test(r))return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`Plan must cite ripplo test stubs. Add a "Tests to implement" section listing .ripplo/tests/<id>.ts per affected flow, OR add "No e2e coverage needed: <reason>" if this plan touches no user-facing behavior. ${d("explore")}`}}});function wa(){let e=En.join(ka.homedir(),".claude","plans");return je.existsSync(e)?je.readdirSync(e).filter(r=>r.endsWith(".md")).map(r=>En.join(e,r)).map(r=>({full:r,mtime:je.statSync(r).mtimeMs})).sort((r,n)=>n.mtime-r.mtime)[0]?.full??null:null}var In=f("UserPromptSubmit",async e=>{if(e.permission_mode!=="plan"||!m(e.cwd)||!w(e.cwd))return;let t=await S(e.cwd);if(t.isErr())return;let r=F(t.value),n=['Plan must include "Tests to implement" with a .ripplo/tests/ file per affected flow (ExitPlanMode blocks otherwise). Stub each with `test("Intent")` (no body).'];return r.length>0&&n.push(`Existing stubs: ${r.join(", ")}`),{hookSpecificOutput:{additionalContext:n.join(`
392
- `),hookEventName:"UserPromptSubmit"}}});import La from"path";import Nn from"picomatch";import{z as Un}from"zod";import{mkdirSync as Ca,readFileSync as xa,writeFileSync as Pa}from"fs";import Ea from"path";import{z as v}from"zod";import{createHash as Nm}from"crypto";import Ae from"picomatch";function va(e){return _(["diff","--name-only","HEAD"],e).split(`
393
- `).filter(t=>t.length>0)}function jn({cwd:e,ignoreGlobs:t,watchGlobs:r}){let n=Ae([...r]),o=Ae([...t]);return va(e).filter(i=>n(i)&&!o(i))}function ba(e){return _(["ls-files","--others","--exclude-standard"],e).split(`
394
- `).filter(t=>t.length>0)}function An({cwd:e,ignoreGlobs:t,watchGlobs:r}){let n=Ae([...r]),o=Ae([...t]);return ba(e).filter(i=>n(i)&&!o(i))}var Sa=["**/src/**","**/app/**","**/apps/**","**/pages/**","**/routes/**","**/components/**","**/server/**","**/api/**","**/backend/**","**/features/**","**/modules/**","**/views/**","**/ui/**","**/hooks/**","**/contexts/**","**/providers/**","**/controllers/**","**/handlers/**","**/resolvers/**","**/services/**","**/middleware/**","**/lib/**"],Ra=["**/*.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 Q(){return{ignorePaths:Ra,watchPaths:Sa}}var $a=v.object({label:v.string().nullable(),slug:v.string().nullable(),status:v.enum(["intent","stub","implemented"])}),Ia=v.object({intent:v.string(),name:v.string(),sourcePath:v.string().nullable(),stub:v.boolean()}),ja=v.object({changedAppFiles:v.array(v.string()).readonly(),scope:v.object({available:v.boolean(),items:v.array($a).readonly()}),tests:v.array(Ia).readonly().default([]),untrackedAppFiles:v.array(v.string()).readonly()});function Ln({cwd:e,scope:t}){let r=Dn(e);_n(e,{...Fn(e),scope:t,tests:r?.tests??[]})}function Tn({cwd:e,tests:t}){let r=Dn(e);_n(e,{...Fn(e),scope:r?.scope??{available:!1,items:[]},tests:t??r?.tests??[]})}function Dn(e){let t=ja.safeParse(Aa(On(e)));return t.success?t.data:null}function Aa(e){try{return JSON.parse(xa(e,"utf8"))}catch{return null}}function On(e){return Ze(e,"coverage-context.json")}function _n(e,t){let r=On(e);Ca(Ea.dirname(r),{recursive:!0}),Pa(r,JSON.stringify(t,null,2))}function Fn(e){let{ignorePaths:t,watchPaths:r}=Q(),n={cwd:e,ignoreGlobs:t,watchGlobs:r};return{changedAppFiles:jn(n),untrackedAppFiles:An(n)}}var Ta=Un.looseObject({file_path:Un.string()}),Hn=f("PostToolUse",async e=>{let t=Ta.safeParse(e.tool_input);if(!t.success)return;let r=t.data.file_path,{cwd:n}=e;if(!m(n)||!w(n))return;let o=La.relative(n,r);if(o.startsWith(".."))return;let{ignorePaths:i,watchPaths:a}=Q(),s=Nn([...a]),l=Nn([...i]);if(!s(o)||l(o))return;let c=await S(n);if(Tn({cwd:n,tests:c.isOk()?c.value.tests.map(u=>({intent:u.intent,name:u.name,sourcePath:u.sourcePath??null,stub:u.stub})):void 0}),c.isErr())return;let b=F(c.value);if(b.length!==0)return{hookSpecificOutput:{additionalContext:`Reminder: stub tests still unimplemented \u2014 ${b.join(", ")}. Implement with \`test("Intent", () => ({ given, steps }))\`.`,hookEventName:"PostToolUse"}}});import{createHash as qa}from"crypto";import{z as Vn}from"zod";import{createHash as Da}from"crypto";import{mkdirSync as Oa,readFileSync as Mn,writeFileSync as _a}from"fs";import Je from"path";var Fa=[".ts",".tsx",".js",".jsx"];function Le(e){let t=Da("sha256");return t.update(_(["rev-parse","HEAD"],e)),t.update("\0"),t.update(_(["diff","HEAD"],e)),t.update("\0"),_(["ls-files","--others","--exclude-standard"],e).split(`
395
- `).filter(n=>n.length>0).filter(n=>Fa.some(o=>n.endsWith(o))).toSorted((n,o)=>n.localeCompare(o)).forEach(n=>{t.update(n),t.update("\0"),t.update(Na(Je.join(e,n))),t.update("\0")}),t.digest("hex")}function Y(e,t){try{return Mn(Wn(e,t),"utf8").trim()}catch{return null}}function X(e,t,r){let n=Wn(e,t);Oa(Je.dirname(n),{recursive:!0}),_a(n,r)}function Na(e){try{return Mn(e)}catch{return Buffer.alloc(0)}}function Wn(e,t){return Je.join(e,".ripplo",".local",`${t}.hash`)}import{graphql as Ua}from"gql.tada";var Ha=Ua(`
391
+ `)}import je from"fs";import ba from"os";import $n from"path";import{z as va}from"zod";function f(e,t){let r=va.custom(n=>typeof n=="object"&&n!==null&&"hook_event_name"in n&&n.hook_event_name===e);return{event:e,run:async n=>await t(r.parse(n))??void 0}}var In=f("PreToolUse",e=>{if(e.tool_name!=="ExitPlanMode"||!v(e.cwd))return;let t=Sa();if(t==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/tests/ test per affected flow. ${d("explore")}`,hookEventName:"PreToolUse"}};let r=je.readFileSync(t,"utf8");if(!/\.ripplo\/tests|Tests to implement|No e2e coverage needed/.test(r))return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`Plan must cite ripplo test stubs. Add a "Tests to implement" section listing .ripplo/tests/<id>.ts per affected flow, OR add "No e2e coverage needed: <reason>" if this plan touches no user-facing behavior. ${d("explore")}`}}});function Sa(){let e=$n.join(ba.homedir(),".claude","plans");return je.existsSync(e)?je.readdirSync(e).filter(r=>r.endsWith(".md")).map(r=>$n.join(e,r)).map(r=>({full:r,mtime:je.statSync(r).mtimeMs})).sort((r,n)=>n.mtime-r.mtime)[0]?.full??null:null}var jn=f("UserPromptSubmit",async e=>{if(e.permission_mode!=="plan"||!m(e.cwd)||!v(e.cwd))return;let t=await S(e.cwd);if(t.isErr())return;let r=F(t.value),n=['Plan must include "Tests to implement" with a .ripplo/tests/ file per affected flow (ExitPlanMode blocks otherwise). Stub each with `test("Intent")` (no body).'];return r.length>0&&n.push(`Existing stubs: ${r.join(", ")}`),{hookSpecificOutput:{additionalContext:n.join(`
392
+ `),hookEventName:"UserPromptSubmit"}}});import Oa from"path";import Un from"picomatch";import{z as Hn}from"zod";import{mkdirSync as Ea,readFileSync as $a,writeFileSync as Ia}from"fs";import ja from"path";import{z as b}from"zod";import{createHash as Mm}from"crypto";import Ae from"picomatch";function xa(e){return _(["diff","--name-only","HEAD"],e).split(`
393
+ `).filter(t=>t.length>0)}function An({cwd:e,ignoreGlobs:t,watchGlobs:r}){let n=Ae([...r]),o=Ae([...t]);return xa(e).filter(i=>n(i)&&!o(i))}function Ra(e){return _(["ls-files","--others","--exclude-standard"],e).split(`
394
+ `).filter(t=>t.length>0)}function Ln({cwd:e,ignoreGlobs:t,watchGlobs:r}){let n=Ae([...r]),o=Ae([...t]);return Ra(e).filter(i=>n(i)&&!o(i))}var Ca=["**/src/**","**/app/**","**/apps/**","**/pages/**","**/routes/**","**/components/**","**/server/**","**/api/**","**/backend/**","**/features/**","**/modules/**","**/views/**","**/ui/**","**/hooks/**","**/contexts/**","**/providers/**","**/controllers/**","**/handlers/**","**/resolvers/**","**/services/**","**/middleware/**","**/lib/**"],Pa=["**/*.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 Q(){return{ignorePaths:Pa,watchPaths:Ca}}var Aa=b.object({label:b.string().nullable(),slug:b.string().nullable(),status:b.enum(["intent","stub","implemented"])}),La=b.object({intent:b.string(),name:b.string(),sourcePath:b.string().nullable(),stub:b.boolean()}),Ta=b.object({changedAppFiles:b.array(b.string()).readonly(),scope:b.object({available:b.boolean(),items:b.array(Aa).readonly()}),tests:b.array(La).readonly().default([]),untrackedAppFiles:b.array(b.string()).readonly()});function Tn({cwd:e,scope:t}){let r=On(e);Fn(e,{...Nn(e),scope:t,tests:r?.tests??[]})}function Dn({cwd:e,tests:t}){let r=On(e);Fn(e,{...Nn(e),scope:r?.scope??{available:!1,items:[]},tests:t??r?.tests??[]})}function On(e){let t=Ta.safeParse(Da(_n(e)));return t.success?t.data:null}function Da(e){try{return JSON.parse($a(e,"utf8"))}catch{return null}}function _n(e){return Ze(e,"coverage-context.json")}function Fn(e,t){let r=_n(e);Ea(ja.dirname(r),{recursive:!0}),Ia(r,JSON.stringify(t,null,2))}function Nn(e){let{ignorePaths:t,watchPaths:r}=Q(),n={cwd:e,ignoreGlobs:t,watchGlobs:r};return{changedAppFiles:An(n),untrackedAppFiles:Ln(n)}}var _a=Hn.looseObject({file_path:Hn.string()}),Mn=f("PostToolUse",async e=>{let t=_a.safeParse(e.tool_input);if(!t.success)return;let r=t.data.file_path,{cwd:n}=e;if(!m(n)||!v(n))return;let o=Oa.relative(n,r);if(o.startsWith(".."))return;let{ignorePaths:i,watchPaths:a}=Q(),s=Un([...a]),l=Un([...i]);if(!s(o)||l(o))return;let c=await S(n);if(Dn({cwd:n,tests:c.isOk()?c.value.tests.map(u=>({intent:u.intent,name:u.name,sourcePath:u.sourcePath??null,stub:u.stub})):void 0}),c.isErr())return;let h=F(c.value);if(h.length!==0)return{hookSpecificOutput:{additionalContext:`Reminder: stub tests still unimplemented \u2014 ${h.join(", ")}. Implement with \`test("Intent", () => ({ given, steps }))\`.`,hookEventName:"PostToolUse"}}});import{createHash as Ga}from"crypto";import{z as Gn}from"zod";import{createHash as Fa}from"crypto";import{mkdirSync as Na,readFileSync as Wn,writeFileSync as Ua}from"fs";import Je from"path";var Ha=[".ts",".tsx",".js",".jsx"];function Le(e){let t=Fa("sha256");return t.update(_(["rev-parse","HEAD"],e)),t.update("\0"),t.update(_(["diff","HEAD"],e)),t.update("\0"),_(["ls-files","--others","--exclude-standard"],e).split(`
395
+ `).filter(n=>n.length>0).filter(n=>Ha.some(o=>n.endsWith(o))).toSorted((n,o)=>n.localeCompare(o)).forEach(n=>{t.update(n),t.update("\0"),t.update(Ma(Je.join(e,n))),t.update("\0")}),t.digest("hex")}function Y(e,t){try{return Wn(qn(e,t),"utf8").trim()}catch{return null}}function X(e,t,r){let n=qn(e,t);Na(Je.dirname(n),{recursive:!0}),Ua(n,r)}function Ma(e){try{return Wn(e)}catch{return Buffer.alloc(0)}}function qn(e,t){return Je.join(e,".ripplo",".local",`${t}.hash`)}import{graphql as Wa}from"gql.tada";var qa=Wa(`
396
396
  mutation AutoScopeAddDirty($projectId: String!, $cwd: String!, $workflowSlugs: [String!]!) {
397
397
  addDirtyTestsToScope(projectId: $projectId, cwd: $cwd, workflowSlugs: $workflowSlugs) {
398
398
  __typename
@@ -403,11 +403,11 @@ iframe { border: none; }
403
403
  }
404
404
  }
405
405
  }
406
- `);async function Bn({cwd:e,lockfile:t}){if(!w(e))return{addedSlugs:[]};let r=Wa(e);if(r.length===0)return{addedSlugs:[]};let n=new Set(r),o=t.tests.filter(l=>l.sourcePath!=null&&n.has(l.sourcePath)).map(l=>P(l.name));if(o.length===0)return{addedSlugs:[]};let i=k(e).unwrapOr(void 0);return i==null?{addedSlugs:[]}:await zt({config:i,cwd:e,lockfile:t}).catch(l=>(D.warn("auto-sync failed: %s",l instanceof Error?l.message:String(l)),null))==null?{addedSlugs:[]}:{addedSlugs:await Ma({cfg:i,slugs:o})?o:[]}}async function Ma({cfg:e,slugs:t}){let n=(await p({config:e,document:Ha,variables:{cwd:e.cwd,projectId:e.projectId,workflowSlugs:[...t]}}).catch(o=>(D.warn("auto-scope failed: %s",o instanceof Error?o.message:String(o)),null)))?.addDirtyTestsToScope;return n?.__typename!=="MutationAddDirtyTestsToScopeSuccess"?!1:n.data.length>0}var qn=".ripplo/tests/";function Wa(e){let t;try{t=_(["status","--porcelain","--",".ripplo/tests"],e)}catch{return[]}return t.split(`
407
- `).map(r=>r.slice(3).trim()).filter(r=>r.startsWith(qn)&&r.endsWith(".ts")).map(r=>r.slice(qn.length)).filter(r=>r!=="index.ts"&&!r.endsWith("/index.ts"))}var Ba=Vn.looseObject({file_path:Vn.string()}),Gn=f("PostToolUse",async e=>{let t=Ba.safeParse(e.tool_input);if(!t.success||!/\/\.ripplo\/.*\.ts$/.test(t.data.file_path))return;let{cwd:r}=e;if(!m(r))return;if(!w(r))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 B(r);if(n.isErr())return{decision:"block",reason:`${R(n.error)}
406
+ `);async function Vn({cwd:e,lockfile:t}){if(!v(e))return{addedSlugs:[]};let r=Va(e);if(r.length===0)return{addedSlugs:[]};let n=new Set(r),o=t.tests.filter(l=>l.sourcePath!=null&&n.has(l.sourcePath)).map(l=>P(l.name));if(o.length===0)return{addedSlugs:[]};let i=w(e).unwrapOr(void 0);return i==null?{addedSlugs:[]}:await Kt({config:i,cwd:e,lockfile:t}).catch(l=>(D.warn("auto-sync failed: %s",l instanceof Error?l.message:String(l)),null))==null?{addedSlugs:[]}:{addedSlugs:await Ba({cfg:i,slugs:o})?o:[]}}async function Ba({cfg:e,slugs:t}){let n=(await p({config:e,document:qa,variables:{cwd:e.cwd,projectId:e.projectId,workflowSlugs:[...t]}}).catch(o=>(D.warn("auto-scope failed: %s",o instanceof Error?o.message:String(o)),null)))?.addDirtyTestsToScope;return n?.__typename!=="MutationAddDirtyTestsToScopeSuccess"?!1:n.data.length>0}var Bn=".ripplo/tests/";function Va(e){let t;try{t=_(["status","--porcelain","--",".ripplo/tests"],e)}catch{return[]}return t.split(`
407
+ `).map(r=>r.slice(3).trim()).filter(r=>r.startsWith(Bn)&&r.endsWith(".ts")).map(r=>r.slice(Bn.length)).filter(r=>r!=="index.ts"&&!r.endsWith("/index.ts"))}var Ja=Gn.looseObject({file_path:Gn.string()}),Jn=f("PostToolUse",async e=>{let t=Ja.safeParse(e.tool_input);if(!t.success||!/\/\.ripplo\/.*\.ts$/.test(t.data.file_path))return;let{cwd:r}=e;if(!m(r))return;if(!v(r))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 B(r);if(n.isErr())return{decision:"block",reason:`${x(n.error)}
408
408
  ${d("create","DSL authoring + lint rules")}`};let o=H(n.value);if(o.length>0)return{decision:"block",reason:`${M(o)}
409
- ${d("create")}`};let{addedSlugs:i}=await Bn({cwd:r,lockfile:n.value});return Va([...i.length>0?[`Auto-scoped ${i.join(", ")} (dirty tests).`]:[],...Ga(r,n.value)])});function Va(e){if(e.length!==0)return{hookSpecificOutput:{additionalContext:e.join(`
410
- `),hookEventName:"PostToolUse"}}}function Ga(e,t){let r=ye(t),n=qa("sha256").update(JSON.stringify(r)).digest("hex");return Y(e,"coverage-warn")===n?[]:(X(e,"coverage-warn",n),r.length===0?[]:[fe(r)])}import{z as Jn}from"zod";var Ja=Jn.looseObject({command:Jn.string()}),za=/\bripplo\s+hooks\s+pause\b/,zn=f("PreToolUse",e=>{if(e.tool_name!=="Bash")return;let t=Ja.safeParse(e.tool_input);if(!t.success||!za.test(t.data.command))return;let{cwd:r}=e;if(m(r))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 Za}from"shell-quote";import{z as Qn}from"zod";import{existsSync as Ka,mkdirSync as Qa,rmSync as Hf,writeFileSync as Ya}from"fs";import ze from"path";function Te(e,t,r){let n=Kn(e,t,r);Qa(ze.dirname(n),{recursive:!0}),Ya(n,"")}function De(e,t,r){return Ka(Kn(e,t,r))}function Kn(e,t,r){return ze.join(Xa(e,t),r)}function Xa(e,t){return ze.join(e,".ripplo",".local","skills-loaded",t)}var el=Qn.looseObject({command:Qn.string()}),tl="debug",rl=new Set(["tail","head","less","more","wc","sort","uniq","awk","sed","grep"]),Xn=f("PreToolUse",e=>{if(e.tool_name!=="Bash")return;let t=el.safeParse(e.tool_input);if(!t.success)return;let r=nl(t.data.command);if(!ol(r))return;let{cwd:n}=e;if(!m(n)||!w(n))return;if(!De(n,e.session_id,tl))return Yn("Running `ripplo run` requires the `/ripplo:debug` skill loaded first. Load `/ripplo:debug` then retry \u2014 it carries the artifact-read order and the no-grep-piping guidance for run failures.");let i=sl(r);if(i!=null)return Yn(`Don't pipe \`ripplo run\` through \`${i}\` \u2014 buffering filters hold all stdout until EOF, hiding live progress; if you kill the pipeline mid-run the run is orphaned on the server. Redirect to a file instead: \`ripplo run <id> > /tmp/run.log 2>&1\` and Read the file, or use \`run_in_background: true\`.`)});function nl(e){try{return Za(e)}catch{return[]}}function ol(e){return e.some((t,r)=>t==="ripplo"&&e[r+1]==="run")}function il(e){return typeof e=="object"&&"op"in e&&e.op==="|"}function sl(e){let t=e.find((r,n)=>{let o=e[n-1];return o!=null&&il(o)&&typeof r=="string"&&rl.has(r)});return typeof t=="string"?t:null}function Yn(e){return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:e}}}import al from"path";import{z as Zn}from"zod";var ll=new Set(["Edit","Write","NotebookEdit"]),cl=Zn.looseObject({file_path:Zn.string()}),dl="create",eo=f("PreToolUse",e=>{if(!ll.has(e.tool_name))return;let t=cl.safeParse(e.tool_input);if(!t.success)return;let{cwd:r}=e;if(!m(r)||!w(r))return;let n=al.relative(r,t.data.file_path);if(!(n.startsWith("..")||n!==".ripplo"&&!n.startsWith(".ripplo/"))&&!De(r,e.session_id,dl))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 pl from"path";import to from"picomatch";import{graphql as ul}from"gql.tada";import{z as ro}from"zod";var ml=new Set(["Edit","Write","NotebookEdit"]),fl=ro.looseObject({file_path:ro.string()}),gl=ul(`
409
+ ${d("create")}`};let{addedSlugs:i}=await Vn({cwd:r,lockfile:n.value});return za([...i.length>0?[`Auto-scoped ${i.join(", ")} (dirty tests).`]:[],...Ka(r,n.value)])});function za(e){if(e.length!==0)return{hookSpecificOutput:{additionalContext:e.join(`
410
+ `),hookEventName:"PostToolUse"}}}function Ka(e,t){let r=ye(t),n=Ga("sha256").update(JSON.stringify(r)).digest("hex");return Y(e,"coverage-warn")===n?[]:(X(e,"coverage-warn",n),r.length===0?[]:[fe(r)])}import{z as zn}from"zod";var Qa=zn.looseObject({command:zn.string()}),Ya=/\bripplo\s+hooks\s+pause\b/,Kn=f("PreToolUse",e=>{if(e.tool_name!=="Bash")return;let t=Qa.safeParse(e.tool_input);if(!t.success||!Ya.test(t.data.command))return;let{cwd:r}=e;if(m(r))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 rl}from"shell-quote";import{z as Yn}from"zod";import{existsSync as Xa,mkdirSync as Za,rmSync as qf,writeFileSync as el}from"fs";import ze from"path";function Te(e,t,r){let n=Qn(e,t,r);Za(ze.dirname(n),{recursive:!0}),el(n,"")}function De(e,t,r){return Xa(Qn(e,t,r))}function Qn(e,t,r){return ze.join(tl(e,t),r)}function tl(e,t){return ze.join(e,".ripplo",".local","skills-loaded",t)}var nl=Yn.looseObject({command:Yn.string()}),ol="debug",il=new Set(["tail","head","less","more","wc","sort","uniq","awk","sed","grep"]),Zn=f("PreToolUse",e=>{if(e.tool_name!=="Bash")return;let t=nl.safeParse(e.tool_input);if(!t.success)return;let r=sl(t.data.command);if(!al(r))return;let{cwd:n}=e;if(!m(n)||!v(n))return;if(!De(n,e.session_id,ol))return Xn("Running `ripplo run` requires the `/ripplo:debug` skill loaded first. Load `/ripplo:debug` then retry \u2014 it carries the artifact-read order and the no-grep-piping guidance for run failures.");let i=cl(r);if(i!=null)return Xn(`Don't pipe \`ripplo run\` through \`${i}\` \u2014 buffering filters hold all stdout until EOF, hiding live progress; if you kill the pipeline mid-run the run is orphaned on the server. Redirect to a file instead: \`ripplo run <id> > /tmp/run.log 2>&1\` and Read the file, or use \`run_in_background: true\`.`)});function sl(e){try{return rl(e)}catch{return[]}}function al(e){return e.some((t,r)=>t==="ripplo"&&e[r+1]==="run")}function ll(e){return typeof e=="object"&&"op"in e&&e.op==="|"}function cl(e){let t=e.find((r,n)=>{let o=e[n-1];return o!=null&&ll(o)&&typeof r=="string"&&il.has(r)});return typeof t=="string"?t:null}function Xn(e){return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:e}}}import dl from"path";import{z as eo}from"zod";var pl=new Set(["Edit","Write","NotebookEdit"]),ul=eo.looseObject({file_path:eo.string()}),ml="create",to=f("PreToolUse",e=>{if(!pl.has(e.tool_name))return;let t=ul.safeParse(e.tool_input);if(!t.success)return;let{cwd:r}=e;if(!m(r)||!v(r))return;let n=dl.relative(r,t.data.file_path);if(!(n.startsWith("..")||n!==".ripplo"&&!n.startsWith(".ripplo/"))&&!De(r,e.session_id,ml))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 fl from"path";import ro from"picomatch";import{graphql as gl}from"gql.tada";import{z as no}from"zod";var hl=new Set(["Edit","Write","NotebookEdit"]),yl=no.looseObject({file_path:no.string()}),kl=gl(`
411
411
  query PreEditScopeGate($projectId: String!, $cwd: String!) {
412
412
  project(id: $projectId) {
413
413
  id
@@ -419,9 +419,9 @@ ${d("create")}`};let{addedSlugs:i}=await Bn({cwd:r,lockfile:n.value});return Va(
419
419
  }
420
420
  }
421
421
  }
422
- `),no=f("PreToolUse",async e=>{if(!ml.has(e.tool_name))return;let t=fl.safeParse(e.tool_input);if(!t.success)return;let{cwd:r}=e;if(!m(r)||!w(r))return;let n=pl.relative(r,t.data.file_path);if(n.startsWith("..")||n===".ripplo"||n.startsWith(".ripplo/")||!hl(n))return;let o=await yl(r);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 empty; 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). ${dt(["scope","create"])}`}}});function hl(e){let{ignorePaths:t,watchPaths:r}=Q(),n=to([...r]),o=to([...t]);return n(e)&&!o(e)}async function yl(e){let t=k(e).unwrapOr(void 0);if(t==null)return{degradedReason:"no project config \u2014 `ripplo init` not run here",populated:!0};let r=await p({config:t,document:gl,variables:{cwd:t.cwd,projectId:t.projectId}}).catch(()=>null);return r==null?{degradedReason:"server unreachable",populated:!0}:{degradedReason:null,populated:(r.project?.devSession?.scopeItems??[]).length>0}}import kl from"fs";import wl from"path";import{z as oo}from"zod";var vl=new Set(["Edit","Write","NotebookEdit"]),bl=oo.looseObject({file_path:oo.string()}),io=f("PreToolUse",async e=>{if(!vl.has(e.tool_name))return;let t=Sl(e);if(t==null)return;let{cwd:r}=e;if(kl.existsSync(ee(r))||de(r))return;let n=await S(r);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:
423
- ${R(n.error)}
424
- ${d("create","DSL authoring + lint rules")}`,hookEventName:"PreToolUse"}}:{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`\`ripplo daemon\` is not running, so this edit to \`${t}\` won't sync to the server and dev-mode guardrails won't fire. Run \`/ripplo:start\` (or spawn \`npx ripplo daemon\` directly via Bash with run_in_background), then retry. The app's dev server also needs to be running \u2014 \`npx ripplo doctor\` reports both. If the daemon can't start (auth, server down, intentional offline work), the user can run \`npx ripplo hooks pause\` to bypass this gate; \`npx ripplo hooks resume\` re-enables it. ${d("start")}`}}});function Sl(e){let t=bl.safeParse(e.tool_input);if(!t.success||!m(e.cwd))return null;let r=wl.relative(e.cwd,t.data.file_path);return r.startsWith("..")||r!==".ripplo"&&!r.startsWith(".ripplo/")?null:r}import{graphql as Rl}from"gql.tada";var Cl=Rl(`
422
+ `),oo=f("PreToolUse",async e=>{if(!hl.has(e.tool_name))return;let t=yl.safeParse(e.tool_input);if(!t.success)return;let{cwd:r}=e;if(!m(r)||!v(r))return;let n=fl.relative(r,t.data.file_path);if(n.startsWith("..")||n===".ripplo"||n.startsWith(".ripplo/")||!wl(n))return;let o=await vl(r);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 empty; 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). ${dt(["scope","create"])}`}}});function wl(e){let{ignorePaths:t,watchPaths:r}=Q(),n=ro([...r]),o=ro([...t]);return n(e)&&!o(e)}async function vl(e){let t=w(e).unwrapOr(void 0);if(t==null)return{degradedReason:"no project config \u2014 `ripplo init` not run here",populated:!0};let r=await p({config:t,document:kl,variables:{cwd:t.cwd,projectId:t.projectId}}).catch(()=>null);return r==null?{degradedReason:"server unreachable",populated:!0}:{degradedReason:null,populated:(r.project?.devSession?.scopeItems??[]).length>0}}import bl from"fs";import Sl from"path";import{z as io}from"zod";var xl=new Set(["Edit","Write","NotebookEdit"]),Rl=io.looseObject({file_path:io.string()}),so=f("PreToolUse",async e=>{if(!xl.has(e.tool_name))return;let t=Cl(e);if(t==null)return;let{cwd:r}=e;if(bl.existsSync(ee(r))||de(r))return;let n=await S(r);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:
423
+ ${x(n.error)}
424
+ ${d("create","DSL authoring + lint rules")}`,hookEventName:"PreToolUse"}}:{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:`\`ripplo daemon\` is not running, so this edit to \`${t}\` won't sync to the server and dev-mode guardrails won't fire. Run \`/ripplo:start\` (or spawn \`npx ripplo daemon\` directly via Bash with run_in_background), then retry. The app's dev server also needs to be running \u2014 \`npx ripplo doctor\` reports both. If the daemon can't start (auth, server down, intentional offline work), the user can run \`npx ripplo hooks pause\` to bypass this gate; \`npx ripplo hooks resume\` re-enables it. ${d("start")}`}}});function Cl(e){let t=Rl.safeParse(e.tool_input);if(!t.success||!m(e.cwd))return null;let r=Sl.relative(e.cwd,t.data.file_path);return r.startsWith("..")||r!==".ripplo"&&!r.startsWith(".ripplo/")?null:r}import{graphql as Pl}from"gql.tada";var El=Pl(`
425
425
  query ScopeReminder($projectId: String!, $cwd: String!) {
426
426
  project(id: $projectId) {
427
427
  id
@@ -438,15 +438,15 @@ ${d("create","DSL authoring + lint rules")}`,hookEventName:"PreToolUse"}}:{hookS
438
438
  }
439
439
  }
440
440
  }
441
- `),so=f("UserPromptSubmit",async e=>{let{cwd:t}=e;if(!m(t)||!w(t))return;let r=Le(t);if(Y(t,"scope-reminder")===r)return;let n=k(t).unwrapOr(void 0);if(n==null)return;let o=await p({config:n,document:Cl,variables:{cwd:n.cwd,projectId:n.projectId}}).catch(()=>null);X(t,"scope-reminder",r);let i=o?.project?.devSession?.scopeItems??[];return Ln({cwd:t,scope:{available:o!=null,items:i.map(s=>xl(s))}}),o==null?{hookSpecificOutput:{additionalContext:`RIPPLO SCOPE: unknown \u2014 server unreachable, scope guardrails are not enforcing. Check the dev session (\`npx ripplo doctor\`). ${d("start")}`,hookEventName:"UserPromptSubmit"}}:{hookSpecificOutput:{additionalContext:i.length===0?`RIPPLO SCOPE: empty. ${d("scope")}`:`RIPPLO SCOPE (${String(i.length)}):
441
+ `),ao=f("UserPromptSubmit",async e=>{let{cwd:t}=e;if(!m(t)||!v(t))return;let r=Le(t);if(Y(t,"scope-reminder")===r)return;let n=w(t).unwrapOr(void 0);if(n==null)return;let o=await p({config:n,document:El,variables:{cwd:n.cwd,projectId:n.projectId}}).catch(()=>null);X(t,"scope-reminder",r);let i=o?.project?.devSession?.scopeItems??[];return Tn({cwd:t,scope:{available:o!=null,items:i.map(s=>$l(s))}}),o==null?{hookSpecificOutput:{additionalContext:`RIPPLO SCOPE: unknown \u2014 server unreachable, scope guardrails are not enforcing. Check the dev session (\`npx ripplo doctor\`). ${d("start")}`,hookEventName:"UserPromptSubmit"}}:{hookSpecificOutput:{additionalContext:i.length===0?`RIPPLO SCOPE: empty. ${d("scope")}`:`RIPPLO SCOPE (${String(i.length)}):
442
442
  ${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(`
443
- `)}`,hookEventName:"UserPromptSubmit"}}});function xl(e){let t=e.workflow;return t==null?{label:e.label,slug:null,status:"intent"}:{label:e.label,slug:t.slug,status:t.spec==null?"stub":"implemented"}}var Pl="# 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, oracle-based backend 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; if unsure between two, load both. If completely unsure, load `/ripplo:explore` 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; `ripplo doctor` reports the engine endpoint missing.\n- Load `/ripplo:explore` skill for instructions on planning/stubbing test coverage for a new project or new feature area.\n- Load `/ripplo:scope` skill for instructions on managing the working set of tests this session is responsible for.\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 existing tests.\n- Load `/ripplo:debug` skill for instructions on a failed run; read artifacts in `.ripplo/debug/<runId>/` before re-running.\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 the oracle checks observed-vs-model. 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 oracle's 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, oracle); full primitive catalog at `node_modules/@ripplo/testing/DSL.md`.\n",ao=f("SessionStart",e=>{if(m(e.cwd))return{hookSpecificOutput:{additionalContext:Pl,hookEventName:"SessionStart"}}});import El from"path";import $l from"process";import{CancellationTokenSource as Il}from"vscode-jsonrpc/node";import{graphql as jl}from"gql.tada";function lo(e){return`--- Ripplo Run Failures (scope) ---
443
+ `)}`,hookEventName:"UserPromptSubmit"}}});function $l(e){let t=e.workflow;return t==null?{label:e.label,slug:null,status:"intent"}:{label:e.label,slug:t.slug,status:t.spec==null?"stub":"implemented"}}var Il="# 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, oracle-based backend 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; if unsure between two, load both. If completely unsure, load `/ripplo:explore` 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; `ripplo doctor` reports the engine endpoint missing.\n- Load `/ripplo:explore` skill for instructions on planning/stubbing test coverage for a new project or new feature area.\n- Load `/ripplo:scope` skill for instructions on managing the working set of tests this session is responsible for.\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 existing tests.\n- Load `/ripplo:debug` skill for instructions on a failed run; read artifacts in `.ripplo/debug/<runId>/` before re-running.\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 the oracle checks observed-vs-model. 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 oracle's 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, oracle); full primitive catalog at `node_modules/@ripplo/testing/DSL.md`.\n",lo=f("SessionStart",e=>{if(m(e.cwd))return{hookSpecificOutput:{additionalContext:Il,hookEventName:"SessionStart"}}});import jl from"path";import Al from"process";import{CancellationTokenSource as Ll}from"vscode-jsonrpc/node";import{graphql as Tl}from"gql.tada";function co(e){return`--- Ripplo Run Failures (scope) ---
444
444
  ${e.join(`
445
445
  `)}
446
446
  Artifacts: .ripplo/debug/<runId>/. ${d("debug")}`}function Ke({lines:e,retried:t}){let r=t?"These tests were already retried once inside this gate and the server was still unreachable.":"";return["--- Ripplo Run Not Verified (Ripplo server unreachable) ---",...e,"The Ripplo server could not be reached while running these tests. This is a server-side transient, NOT a problem with your local environment \u2014 do not debug the daemon, dev server, or auth (`npx ripplo doctor` will be green).",r,"Wait a moment, then attempt to stop again so the gate can re-verify."].filter(n=>n.length>0).join(`
447
447
  `)}function le(e){return`--- Ripplo Run Could Not Execute ---
448
448
  ${e}
449
- Fix the run environment (daemon, dev server, auth \u2014 \`npx ripplo doctor\`) and re-run before declaring work done. ${d("start")}`}var Al=jl(`
449
+ Fix the run environment (daemon, dev server, auth \u2014 \`npx ripplo doctor\`) and re-run before declaring work done. ${d("start")}`}var Dl=Tl(`
450
450
  query ScopeEnforce($projectId: String!, $cwd: String!) {
451
451
  project(id: $projectId) {
452
452
  id
@@ -465,26 +465,26 @@ Fix the run environment (daemon, dev server, auth \u2014 \`npx ripplo doctor\`)
465
465
  }
466
466
  }
467
467
  }
468
- `),po=f("Stop",async e=>{let{cwd:t}=e;if(!m(t)||!w(t))return;let r=Le(t),n=Y(t,"stop-enforce")===r;if(n&&!e.stop_hook_active)return;let o=await Ll(t);if(X(t,"stop-enforce",r),o.errors.length!==0)return n&&e.stop_hook_active&&!o.infraOnly?{continue:!1,stopReason:`Stop-enforce: same repo state across consecutive stop attempts; agent appears stuck. Errors:
468
+ `),uo=f("Stop",async e=>{let{cwd:t}=e;if(!m(t)||!v(t))return;let r=Le(t),n=Y(t,"stop-enforce")===r;if(n&&!e.stop_hook_active)return;let o=await Ol(t);if(X(t,"stop-enforce",r),o.errors.length!==0)return n&&e.stop_hook_active&&!o.infraOnly?{continue:!1,stopReason:`Stop-enforce: same repo state across consecutive stop attempts; agent appears stuck. Errors:
469
469
  ${o.errors.join(`
470
470
 
471
471
  `)}`}:{decision:"block",reason:o.errors.join(`
472
472
 
473
- `)}});async function Ll(e){let t=await B(e);if(t.isErr())return{errors:[`--- Compilation failed ---
474
- ${R(t.error)}
475
- ${d("create")}`],infraOnly:!1};let r=t.value,n=await Ol(e,r),o=Tl(r),i=Dl(r),a=n.runnableSlugs.length>0?await Fl(e):null,s=[o,i,a?.error??null,n.error].filter(c=>c!=null),l=s.length>0&&o==null&&i==null&&n.error==null&&(a?.infra??!1);return{errors:s,infraOnly:l}}function Tl(e){let t=H(e);return t.length===0?null:`--- Ripplo Lint ---
473
+ `)}});async function Ol(e){let t=await B(e);if(t.isErr())return{errors:[`--- Compilation failed ---
474
+ ${x(t.error)}
475
+ ${d("create")}`],infraOnly:!1};let r=t.value,n=await Nl(e,r),o=_l(r),i=Fl(r),a=n.runnableSlugs.length>0?await Hl(e):null,s=[o,i,a?.error??null,n.error].filter(c=>c!=null),l=s.length>0&&o==null&&i==null&&n.error==null&&(a?.infra??!1);return{errors:s,infraOnly:l}}function _l(e){let t=H(e);return t.length===0?null:`--- Ripplo Lint ---
476
476
  ${M(t)}
477
- ${d("create")}`}function Dl(e){let t=F(e);return t.length===0?null:`--- Unimplemented stubs ---
477
+ ${d("create")}`}function Fl(e){let t=F(e);return t.length===0?null:`--- Unimplemented stubs ---
478
478
  ${t.join(", ")}
479
479
  Implement the stub now with \`test("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.
480
- ${d("create")}`}async function Ol(e,t){let r=new Set(F(t).map(u=>P(u))),n=new Set(t.tests.map(u=>P(u.name))),o=(u,y)=>n.has(u)?r.has(u):_l(y),i=k(e).unwrapOr(void 0);if(i==null)return{error:`--- Testing Scope (NOT CHECKED) ---
481
- No project config \u2014 \`ripplo init\` hasn't run here, so scope/stub done-checks are not enforcing. ${d("setup")}`,runnableSlugs:[]};let a=await p({config:i,document:Al,variables:{cwd:i.cwd,projectId:i.projectId}}).catch(()=>null);if(a==null)return{error:`--- Testing Scope (NOT CHECKED) ---
482
- Ripplo server unreachable \u2014 scope/stub done-checks are not enforcing. Verify the dev session is live (\`npx ripplo doctor\`, ${d("start")}) before declaring work done.`,runnableSlugs:[]};let s=a.project?.devSession?.scopeItems??[],l=s.flatMap(u=>{let y=u.workflow;return y==null?[` [intent] ${u.label??"(no label)"} \u2014 write a test for this flow`]:o(y.slug,y.spec)?[` [stub] ${y.slug} \u2014 implement \`${y.name}\``]:[]}),c=s.flatMap(u=>u.workflow!=null&&!o(u.workflow.slug,u.workflow.spec)?[u.workflow.slug]:[]);return{error:l.length===0?null:`--- Testing Scope ---
480
+ ${d("create")}`}async function Nl(e,t){let r=new Set(F(t).map(u=>P(u))),n=new Set(t.tests.map(u=>P(u.name))),o=(u,k)=>n.has(u)?r.has(u):Ul(k),i=w(e).unwrapOr(void 0);if(i==null)return{error:`--- Testing Scope (NOT CHECKED) ---
481
+ No project config \u2014 \`ripplo init\` hasn't run here, so scope/stub done-checks are not enforcing. ${d("setup")}`,runnableSlugs:[]};let a=await p({config:i,document:Dl,variables:{cwd:i.cwd,projectId:i.projectId}}).catch(()=>null);if(a==null)return{error:`--- Testing Scope (NOT CHECKED) ---
482
+ Ripplo server unreachable \u2014 scope/stub done-checks are not enforcing. Verify the dev session is live (\`npx ripplo doctor\`, ${d("start")}) before declaring work done.`,runnableSlugs:[]};let s=a.project?.devSession?.scopeItems??[],l=s.flatMap(u=>{let k=u.workflow;return k==null?[` [intent] ${u.label??"(no label)"} \u2014 write a test for this flow`]:o(k.slug,k.spec)?[` [stub] ${k.slug} \u2014 implement \`${k.name}\``]:[]}),c=s.flatMap(u=>u.workflow!=null&&!o(u.workflow.slug,u.workflow.spec)?[u.workflow.slug]:[]);return{error:l.length===0?null:`--- Testing Scope ---
483
483
  ${l.join(`
484
484
  `)}
485
- ${d("create")}`,runnableSlugs:c}}function _l(e){return typeof e=="object"&&e!=null&&Reflect.get(e,"stub")===!0}async function Fl(e){let t=$l.argv[1];if(t==null)return{error:le("CLI entry missing (process.argv[1])"),infra:!1};let r=await Se(e);if(r!=null){let i=xe(r);return Re(r.serverUrl)?{error:le(i),infra:!1}:{error:Ke({lines:[i],retried:!1}),infra:!0}}let n=await z({cliEntry:t,cwd:e});if(n.isErr())return{error:le(A(n.error)),infra:!1};let o=n.value;try{return await Nl({connection:o,cwd:e})}finally{o.spawned&&await K(o),o.socket.destroy()}}async function Nl({connection:e,cwd:t}){let r=await co({connection:e,cwd:t,tests:[]});if(r.kind==="transport")return{error:le(r.message),infra:!1};let n=r.notRun.length>0?await co({connection:e,cwd:t,tests:r.notRun.map(a=>a.testName)}):null,o=n==null||n.kind==="transport"?r.notRun:n.notRun,i=[...r.failedLines,...n!=null&&n.kind==="done"?n.failedLines:[]];return Ul({failedLines:i,notRun:o,retried:n!=null})}function Ul({failedLines:e,notRun:t,retried:r}){if(e.length===0&&t.length===0)return{error:null,infra:!1};let n=e.length>0?lo(e):null,o=t.length>0?Ke({lines:t.map(i=>i.line),retried:r}):null;return{error:[n,o].filter(i=>i!=null).join(`
485
+ ${d("create")}`,runnableSlugs:c}}function Ul(e){return typeof e=="object"&&e!=null&&Reflect.get(e,"stub")===!0}async function Hl(e){let t=Al.argv[1];if(t==null)return{error:le("CLI entry missing (process.argv[1])"),infra:!1};let r=await Se(e);if(r!=null){let i=Ce(r);return xe(r.serverUrl)?{error:le(i),infra:!1}:{error:Ke({lines:[i],retried:!1}),infra:!0}}let n=await z({cliEntry:t,cwd:e});if(n.isErr())return{error:le(A(n.error)),infra:!1};let o=n.value;try{return await Ml({connection:o,cwd:e})}finally{o.spawned&&await K(o),o.socket.destroy()}}async function Ml({connection:e,cwd:t}){let r=await po({connection:e,cwd:t,tests:[]});if(r.kind==="transport")return{error:le(r.message),infra:!1};let n=r.notRun.length>0?await po({connection:e,cwd:t,tests:r.notRun.map(a=>a.testName)}):null,o=n==null||n.kind==="transport"?r.notRun:n.notRun,i=[...r.failedLines,...n!=null&&n.kind==="done"?n.failedLines:[]];return Wl({failedLines:i,notRun:o,retried:n!=null})}function Wl({failedLines:e,notRun:t,retried:r}){if(e.length===0&&t.length===0)return{error:null,infra:!1};let n=e.length>0?co(e):null,o=t.length>0?Ke({lines:t.map(i=>i.line),retried:r}):null;return{error:[n,o].filter(i=>i!=null).join(`
486
486
 
487
- `),infra:n==null}}async function co({connection:e,cwd:t,tests:r}){let n=[],o=[],i=El.join(t,".ripplo","debug"),a=new Il;return(await be({connection:e,request:{all:!1,headed:!1,tests:[...r]},token:a.token,onEvent:l=>{Hl({debugDir:i,event:l,failedLines:n,notRun:o})}})).match(l=>l.kind==="daemon-error"?{kind:"transport",message:Pe(l.error)}:{failedLines:n,kind:"done",notRun:o},l=>({kind:"transport",message:A(l)}))}function Hl({debugDir:e,event:t,failedLines:r,notRun:n}){if(t.kind!=="test-outcome"||t.outcome.kind==="pass")return;let o=Ce({debugDir:e,event:t})??t.testName;if(t.outcome.kind==="dispatch-error"||t.outcome.kind==="infra-error"){n.push({line:o,testName:t.testName});return}r.push(o)}import{z as uo}from"zod";var Ml=uo.looseObject({skill:uo.string()}),mo=f("PostToolUse",e=>{if(e.tool_name!=="Skill")return;let t=Ml.safeParse(e.tool_input);if(!t.success)return;let r=/^ripplo:(.+)$/.exec(t.data.skill);if(r==null)return;let n=r[1];n!=null&&m(e.cwd)&&Te(e.cwd,e.session_id,n)});var Wl=/(?:^|\s)\/ripplo:([a-z][a-z0-9-]*)\b/gi,fo=f("UserPromptSubmit",e=>{m(e.cwd)&&[...e.prompt.matchAll(Wl)].map(t=>t[1]).filter(t=>t!=null).forEach(t=>{Te(e.cwd,e.session_id,t)})});oc();W(process.cwd());rt();var go={"exit-plan-gate":$n,"plan-reminder":In,"post-edit-flag-stubs":Hn,"post-edit-lint":Gn,"pre-bash-hooks-pause-gate":zn,"pre-bash-run-gate":Xn,"pre-edit-ripplo-skill-gate":eo,"pre-edit-scope-gate":no,"pre-edit-watch-gate":io,"scope-reminder":so,"session-preamble":ao,"stop-enforce":po,"track-skill-load":mo,"track-skill-prompt":fo};async function Gl(){await Bl(Vl(process.argv)).scriptName("ripplo").version(ar()).command(Ql()).command("concurrency [value]","Show or set max local concurrent runs (daemon applies live)",e=>e.positional("value",{type:"number"}),e=>yr({value:e.value})).command("auth <subcommand>","Manage authentication",nc).command("projects <subcommand>","Inspect Ripplo projects",rc).command("hooks <subcommand>","Pause or resume Ripplo hooks",tc).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=>Fr({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)",Zl,e=>sn({all:e.all,headed:e.headed,ids:e.ids,keepAlive:e["keep-alive"]})).command(Xl()).command(Kl()).command(Yl()).command("lint","Static model analysis (cascade gaps + law conflicts; no live app)",()=>{},()=>Nr()).command("sync","Push the compiled .ripplo/ resources to the server (no run)",()=>{},()=>yn()).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=>gr({check:e.check})).command("doctor","Check project health",()=>{},()=>Er()).command("status","Report unimplemented tests and preconditions",e=>e.option("format",{choices:["json","summary"],default:"json",describe:"Output format"}),e=>Pn({format:e.format})).command("scope <subcommand>","Manage testing scope",ec).command("run-worker",!1,()=>{},()=>cn()).command("hook <name>","Internal: run a Claude Code plugin hook",e=>e.positional("name",{choices:Object.keys(go),demandOption:!0,type:"string"}),e=>Jl(e.name)).strict().help().parse()}async function Jl(e){let t=go[e];t==null&&(process.stderr.write(`Unknown hook: ${e}
488
- `),process.exit(1));let r=await zl(),n=r.trim()===""?{}:JSON.parse(r),o=await t.run(n);o!=null&&process.stdout.write(JSON.stringify(o))}function zl(){return new Promise((e,t)=>{if(process.stdin.isTTY){e("");return}let r=[];process.stdin.on("data",n=>r.push(n)),process.stdin.on("end",()=>{e(Buffer.concat(r).toString("utf8"))}),process.stdin.on("error",t)})}Gl().catch(e=>{process.stderr.write(`${ft(e)}
489
- `),process.exit(1)});function Kl(){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",{describe:"Run id where it surfaced",type:"string"}).option("test",{describe:"Test id that surfaced it",type:"string"}),handler:e=>on({kind:e.kind,rootCause:e["root-cause"],runId:e.run,surfacedBy:e["surfaced-by"],testId:e.test,title:e.title})}}function Ql(){return{command:"daemon",describe:"Run the long-lived local executor (IPC socket + run subscription)",builder:e=>e.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=>kn({explore:e.explore,exploreConcurrency:e.exploreConcurrency})}}function Yl(){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"}).command("findings [findingId]","List pending exploration findings, or show one finding's full detail",t=>t.positional("findingId",{type:"string"}).option("json",{default:!1,describe:"Emit the findings list as JSON",type:"boolean"}),t=>Zr({findingId:t.findingId,json:t.json})).command("replay <findingId>","Replay a finding's minimal trail; a clean replay resolves the finding and covers its targets",t=>t.positional("findingId",{demandOption:!0,type:"string"}),t=>en({findingId:t.findingId})),handler:e=>tn({headed:e.headed,trails:e.trails})}}function Xl(){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=>hn({at:e.at,offset:e.offset,runId:e.runId})}}function Zl(e){let t=[];return e.positional("ids",{array:!0,default:t,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 ec(e){return e.command("status","Print the current scope",t=>t.option("format",{choices:["json","text"],default:"text",describe:"Output format"}),t=>Sn({format:t.format})).command("add <test-ids..>","Bind one or more existing tests (stubs or implemented) to scope as agent intent",t=>{let r=[];return t.positional("test-ids",{array:!0,default:r,demandOption:!0,describe:"Slugs of existing workflows",type:"string"})},t=>Rn({testIds:t["test-ids"]})).command("link <id> <test-id>","Link an existing scope item to a test",t=>t.positional("id",{demandOption:!0,describe:"Scope item id",type:"string"}).positional("test-id",{demandOption:!0,describe:"Slug of the workflow to link",type:"string"}),t=>Cn({id:t.id,testId:t["test-id"]})).command("remove <ids..>","Remove one or more scope items by id",t=>{let r=[];return t.positional("ids",{array:!0,default:r,demandOption:!0,describe:"Scope item ids",type:"string"})},t=>xn({ids:t.ids})).demandCommand(1)}function tc(e){return e.command("pause","Disable all Ripplo pre-edit gates and stop enforcement until resumed",()=>{},()=>$r()).command("resume","Re-enable Ripplo hooks paused via `ripplo hooks pause`",()=>{},()=>Ir()).demandCommand(1)}function rc(e){return e.command("list","List projects you have access to (JSON)",()=>{},()=>rn()).demandCommand(1)}function nc(e){return e.command("login","Authenticate via device flow",()=>{},()=>ur()).command("status","Show authentication status",()=>{},()=>mr()).command("logout","Remove the saved token",()=>{},()=>{fr()}).demandCommand(1)}function oc(){let e=process.cwd(),t=Qe(e);t!=null&&t!==e&&(process.chdir(t),process.stderr.write(`ripplo: resolved .ripplo/ at ${t}
490
- `))}export{Gl as main};
487
+ `),infra:n==null}}async function po({connection:e,cwd:t,tests:r}){let n=[],o=[],i=jl.join(t,".ripplo","debug"),a=new Ll;return(await be({connection:e,request:{all:!1,headed:!1,tests:[...r]},token:a.token,onEvent:l=>{ql({debugDir:i,event:l,failedLines:n,notRun:o})}})).match(l=>l.kind==="daemon-error"?{kind:"transport",message:Pe(l.error)}:{failedLines:n,kind:"done",notRun:o},l=>({kind:"transport",message:A(l)}))}function ql({debugDir:e,event:t,failedLines:r,notRun:n}){if(t.kind!=="test-outcome"||t.outcome.kind==="pass")return;let o=Re({debugDir:e,event:t})??t.testName;if(t.outcome.kind==="dispatch-error"||t.outcome.kind==="infra-error"){n.push({line:o,testName:t.testName});return}r.push(o)}import{z as mo}from"zod";var Bl=mo.looseObject({skill:mo.string()}),fo=f("PostToolUse",e=>{if(e.tool_name!=="Skill")return;let t=Bl.safeParse(e.tool_input);if(!t.success)return;let r=/^ripplo:(.+)$/.exec(t.data.skill);if(r==null)return;let n=r[1];n!=null&&m(e.cwd)&&Te(e.cwd,e.session_id,n)});var Vl=/(?:^|\s)\/ripplo:([a-z][a-z0-9-]*)\b/gi,go=f("UserPromptSubmit",e=>{m(e.cwd)&&[...e.prompt.matchAll(Vl)].map(t=>t[1]).filter(t=>t!=null).forEach(t=>{Te(e.cwd,e.session_id,t)})});ac();W(process.cwd());rt();var ho={"exit-plan-gate":In,"plan-reminder":jn,"post-edit-flag-stubs":Mn,"post-edit-lint":Jn,"pre-bash-hooks-pause-gate":Kn,"pre-bash-run-gate":Zn,"pre-edit-ripplo-skill-gate":to,"pre-edit-scope-gate":oo,"pre-edit-watch-gate":so,"scope-reminder":ao,"session-preamble":lo,"stop-enforce":uo,"track-skill-load":fo,"track-skill-prompt":go};async function Kl(){await Jl(zl(process.argv)).scriptName("ripplo").version(lr()).command(Zl()).command("concurrency [value]","Show or set max local concurrent runs (daemon applies live)",e=>e.positional("value",{type:"number"}),e=>kr({value:e.value})).command("auth <subcommand>","Manage authentication",sc).command("projects <subcommand>","Inspect Ripplo projects",ic).command("hooks <subcommand>","Pause or resume Ripplo hooks",oc).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=>Nr({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)",rc,e=>an({all:e.all,headed:e.headed,ids:e.ids,keepAlive:e["keep-alive"]})).command(tc()).command(Xl()).command(ec()).command("lint","Static model analysis (cascade gaps + law conflicts; no live app)",()=>{},()=>Ur()).command("sync","Push the compiled .ripplo/ resources to the server (no run)",()=>{},()=>kn()).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=>hr({check:e.check})).command("doctor","Check project health",()=>{},()=>$r()).command("status","Report unimplemented tests and preconditions",e=>e.option("format",{choices:["json","summary"],default:"json",describe:"Output format"}),e=>En({format:e.format})).command("scope <subcommand>","Manage testing scope",nc).command("run-worker",!1,()=>{},()=>dn()).command("hook <name>","Internal: run a Claude Code plugin hook",e=>e.positional("name",{choices:Object.keys(ho),demandOption:!0,type:"string"}),e=>Ql(e.name)).strict().help().parse()}async function Ql(e){let t=ho[e];t==null&&(process.stderr.write(`Unknown hook: ${e}
488
+ `),process.exit(1));let r=await Yl(),n=r.trim()===""?{}:JSON.parse(r),o=await t.run(n);o!=null&&process.stdout.write(JSON.stringify(o))}function Yl(){return new Promise((e,t)=>{if(process.stdin.isTTY){e("");return}let r=[];process.stdin.on("data",n=>r.push(n)),process.stdin.on("end",()=>{e(Buffer.concat(r).toString("utf8"))}),process.stdin.on("error",t)})}Kl().catch(e=>{process.stderr.write(`${ft(e)}
489
+ `),process.exit(1)});function Xl(){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",{describe:"Run id where it surfaced",type:"string"}).option("test",{describe:"Test id that surfaced it",type:"string"}),handler:e=>sn({kind:e.kind,rootCause:e["root-cause"],runId:e.run,surfacedBy:e["surfaced-by"],testId:e.test,title:e.title})}}function Zl(){return{command:"daemon",describe:"Run the long-lived local executor (IPC socket + run subscription)",builder:e=>e.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=>wn({explore:e.explore,exploreConcurrency:e.exploreConcurrency})}}function ec(){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 firings per trail (default 12; lower = shorter, easier-to-watch trails)",type:"number"}).command("findings [findingId]","List pending exploration findings, or show one finding's full detail",t=>t.positional("findingId",{type:"string"}).option("json",{default:!1,describe:"Emit the findings list as JSON",type:"boolean"}),t=>en({findingId:t.findingId,json:t.json})).command("replay <findingId>","Replay a finding's minimal trail; a clean replay resolves the finding and covers its targets",t=>t.positional("findingId",{demandOption:!0,type:"string"}),t=>tn({findingId:t.findingId})),handler:e=>rn({headed:e.headed,maxLength:e["max-length"],trails:e.trails})}}function tc(){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=>yn({at:e.at,offset:e.offset,runId:e.runId})}}function rc(e){let t=[];return e.positional("ids",{array:!0,default:t,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 nc(e){return e.command("status","Print the current scope",t=>t.option("format",{choices:["json","text"],default:"text",describe:"Output format"}),t=>xn({format:t.format})).command("add <test-ids..>","Bind one or more existing tests (stubs or implemented) to scope as agent intent",t=>{let r=[];return t.positional("test-ids",{array:!0,default:r,demandOption:!0,describe:"Slugs of existing workflows",type:"string"})},t=>Rn({testIds:t["test-ids"]})).command("link <id> <test-id>","Link an existing scope item to a test",t=>t.positional("id",{demandOption:!0,describe:"Scope item id",type:"string"}).positional("test-id",{demandOption:!0,describe:"Slug of the workflow to link",type:"string"}),t=>Cn({id:t.id,testId:t["test-id"]})).command("remove <ids..>","Remove one or more scope items by id",t=>{let r=[];return t.positional("ids",{array:!0,default:r,demandOption:!0,describe:"Scope item ids",type:"string"})},t=>Pn({ids:t.ids})).demandCommand(1)}function oc(e){return e.command("pause","Disable all Ripplo pre-edit gates and stop enforcement until resumed",()=>{},()=>Ir()).command("resume","Re-enable Ripplo hooks paused via `ripplo hooks pause`",()=>{},()=>jr()).demandCommand(1)}function ic(e){return e.command("list","List projects you have access to (JSON)",()=>{},()=>nn()).demandCommand(1)}function sc(e){return e.command("login","Authenticate via device flow",()=>{},()=>mr()).command("status","Show authentication status",()=>{},()=>fr()).command("logout","Remove the saved token",()=>{},()=>{gr()}).demandCommand(1)}function ac(){let e=process.cwd(),t=Qe(e);t!=null&&t!==e&&(process.chdir(t),process.stderr.write(`ripplo: resolved .ripplo/ at ${t}
490
+ `))}export{Kl as main};