ripplo 0.7.8 → 0.7.10

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