ripplo 0.7.21 → 0.7.23

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