ripplo 0.7.13 → 0.7.15

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