@lovek4d/kickflip 0.6.0 → 0.6.1

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.
Files changed (41) hide show
  1. package/CONTEXT.md +15 -11
  2. package/SKILL.md +7 -7
  3. package/bin/doctor.mjs +5 -4
  4. package/bin/install.mjs +3 -3
  5. package/board/bin/board.mjs +26 -12
  6. package/board/domain.mjs +7 -7
  7. package/board/headless.mjs +9 -9
  8. package/board/public/app-cards.js +23 -44
  9. package/board/public/app-lanes.js +4 -7
  10. package/board/public/app-threads.js +12 -8
  11. package/board/public/prototype-switcher.js +7 -2
  12. package/board/public/shell.js +4 -3
  13. package/board/public/sidebar.css +5 -0
  14. package/board/public/sidebar.js +3 -0
  15. package/board/public/style.css +11 -19
  16. package/board/public/ui-cards.js +21 -15
  17. package/board/public/ui-enums.js +2 -2
  18. package/board/public/ui-header.js +4 -6
  19. package/board/public/ui-hub.js +7 -4
  20. package/board/public/ui-order.js +1 -1
  21. package/board/public/ui-page-header.js +1 -1
  22. package/board/public/ui-sidebar.js +11 -5
  23. package/board/public/ui-snapshot.js +1 -1
  24. package/board/public/ui.js +3 -2
  25. package/board/public/view-hub.js +3 -1
  26. package/board/public/view-questions.js +1 -1
  27. package/board/server.mjs +20 -19
  28. package/board/stop.mjs +1 -1
  29. package/board/store.mjs +27 -25
  30. package/board/transcript.mjs +2 -2
  31. package/hooks-plugin/hooks/board-data-gate.mjs +9 -6
  32. package/hub/front-door.mjs +1 -1
  33. package/hub/hub.mjs +1 -1
  34. package/hub/sessions.mjs +2 -2
  35. package/hub/try.mjs +7 -7
  36. package/hub/usage.mjs +3 -3
  37. package/package.json +1 -1
  38. package/shared/card-prs.mjs +1 -1
  39. package/shared/machine-gate.mjs +2 -2
  40. package/shared/tailscale.mjs +1 -1
  41. package/shared/usage-gate.mjs +1 -1
package/bin/install.mjs CHANGED
@@ -1,3 +1,3 @@
1
- var W=Object.defineProperty;var y=(e,s)=>W(e,"name",{value:s,configurable:!0});import o from"node:fs";import E from"node:os";import n from"node:path";import{SKILLS_SOURCE as I,addArgv as D,addCommand as M,missingSkills as P}from"../shared/skills.mjs";import{cloneRoot as N}from"../shared/repos.mjs";import{hubService as q,runCommand as _}from"../shared/service.mjs";import{tailscaleBin as B,tailscaleInstallHint as C}from"../shared/tailscale.mjs";const U=y(()=>o.existsSync("/opt/homebrew/bin/node")?"/opt/homebrew/bin/node":process.execPath,"nodeBin"),at=y((e,s,t=N())=>`${U()} ${n.join(t,"bin","wait-events.mjs")} ${e} ${s}`,"watcherCommand");function z(e,{home:s=E.homedir(),skillsDir:t=n.join(s,".claude","skills"),cloneRoot:i=N()}={}){if(typeof e!="string"||!n.isAbsolute(e))return e;const a=n.relative(i,e);if(a===".."||a.startsWith(`..${n.sep}`)||n.isAbsolute(a))return e;const c=n.resolve(t,"kickflip");let r=!1;try{r=o.realpathSync(c)===o.realpathSync(i)}catch{}return r?n.join(c,a):e}y(z,"storedPath");function G({home:e=E.homedir(),skillsDir:s=n.join(e,".claude","skills"),cloneRoot:t=N()}={}){const i=o.realpathSync(t),a=[["kickflip",i],["kickflip-hooks",n.join(i,"hooks-plugin")]].map(([l,f])=>{if(!o.statSync(f,{throwIfNoEntry:!1})?.isDirectory())throw new Error(`not a directory: ${f}`);const h=l==="kickflip"?f:n.resolve(s,"kickflip","hooks-plugin"),d=n.resolve(s,l),k=o.lstatSync(d,{throwIfNoEntry:!1});let b;if(k){let j;try{j=o.realpathSync(d)}catch(g){if(g.code!=="ENOENT")throw g}k.isSymbolicLink()?j!==o.realpathSync(f)&&(b=`refusing to replace link ${d} -> ${o.readlinkSync(d)}; remove that link explicitly, then rerun kickflip doctor --fix`):(!k.isDirectory()||j!==o.realpathSync(f))&&(b=`refusing to replace real path ${d}; move it aside explicitly, then rerun kickflip doctor --fix`)}return{link:d,target:h,created:!k,...b?{refusal:b}:{}}}),c=n.join(i,"bin","statusline.mjs");if(!o.statSync(c,{throwIfNoEntry:!1})?.isFile())throw new Error(`not a file: ${c}`);const r=n.join(e,".claude","statusline.sh"),S=n.resolve(s,"kickflip","bin","statusline.mjs"),u=o.lstatSync(r,{throwIfNoEntry:!1});let m=!1,w;if(u)if(!u.isSymbolicLink())w=`refusing to replace real path ${r}; move it aside explicitly, then rerun kickflip doctor --fix`;else{let l;try{l=o.realpathSync(r)}catch(k){if(k.code!=="ENOENT")throw k}const f=n.join(e,".claude","kickflip","sites");let h=f;try{h=o.realpathSync(f)}catch{}const d=!!l&&[f,h].some(k=>l.startsWith(k+n.sep));l&&l!==o.realpathSync(c)&&!d&&(w=`refusing to replace link ${r} -> ${o.readlinkSync(r)}; remove that link explicitly, then rerun kickflip doctor --fix`),m=d||n.resolve(n.dirname(r),o.readlinkSync(r))!==S||!l}return a.push({link:r,target:S,created:!u,...m?{replaced:!0}:{},...w?{refusal:w}:{}}),a}y(G,"inspectLinks");function F({home:e=E.homedir(),skillsDir:s=n.join(e,".claude","skills"),cloneRoot:t=N(),dryRun:i=!1}={}){const a=G({home:e,skillsDir:s,cloneRoot:t}),c=a.find(r=>r.refusal);if(c)throw new Error(c.refusal);if(i)return a;o.mkdirSync(s,{recursive:!0}),o.mkdirSync(n.join(e,".claude"),{recursive:!0});for(const{link:r,target:S,created:u,replaced:m}of a)m&&o.unlinkSync(r),(u||m)&&o.symlinkSync(S,r,r===n.join(e,".claude","statusline.sh")?"file":"dir");return a}y(F,"installLinks");function Q(e,s=E.homedir()){const t=n.join(s,".claude","statusline.sh"),i=[t,`"${t}"`,`'${t.replaceAll("'","'\\''")}'`,"~/.claude/statusline.sh","$HOME/.claude/statusline.sh",'"$HOME/.claude/statusline.sh"',"${HOME}/.claude/statusline.sh",'"${HOME}/.claude/statusline.sh"'];return e?.type==="command"&&i.includes(e.command)}y(Q,"ownsStatusLine");const K='node "$HOME/.claude/skills/kickflip/hooks-plugin/hooks/watcher-stop.mjs"',V=y(e=>(Array.isArray(e?.hooks?.Stop)?e.hooks.Stop:[]).some(s=>Array.isArray(s?.hooks)&&s.hooks.some(t=>t?.type==="command"&&/\/hooks-plugin\/hooks\/watcher-stop\.mjs\b/.test(t.command))),"hasWatcherStop");async function X({port:e=Number(process.env.KICKFLIP_HUB_PORT||4500),fetch:s=globalThis.fetch}={}){try{const t=await s(`http://127.0.0.1:${e}/_hub/status.json`,{signal:AbortSignal.timeout(1500)});if(!t.ok)return null;const i=await t.json();return Array.isArray(i?.sites)?i:null}catch{return null}}y(X,"hubStatus");function R(e){const s=Object.entries(e?.Web||{}).filter(([t])=>!/:\d+$/.test(t)||t.endsWith(":443")).flatMap(([,t])=>Object.entries(t.Handlers||{}));return{root:s.find(([t])=>t==="/")?.[1]?.Proxy||null,paths:[...new Set(s.map(([t])=>t))]}}y(R,"tailscaleRoot");function ct(e,{run:s=_,platform:t=process.platform,tailscale:i=B({platform:t})}={}){if(!i)throw new Error(`Tailscale isn't installed on this host; ${C(t)}`);const a=s(i,["status","--json"]);let c=null;try{c=a.status===0?JSON.parse(a.stdout):null}catch{}if(c?.BackendState!=="Running")throw new Error("Tailscale isn't running on this host; start it, or run tailscale up");const r=c.Self?.DNSName?.replace(/\.$/,"")||"";if(!r||!c.CertDomains?.includes(r))throw new Error("HTTPS isn't on for this tailnet; turn on HTTPS Certificates on the DNS page of the Tailscale admin console");return`https://${r}/${e?e+"/":""}`}y(ct,"tailnetUrl");async function lt({run:e=_,platform:s=process.platform,uid:t=process.getuid(),username:i=E.userInfo().username,port:a=Number(process.env.KICKFLIP_HUB_PORT||4500),tailscale:c=B({platform:s})}={}){const r=y(async $=>{try{return await e(c,$)}catch(v){return{status:1,error:v.message}}},"execute"),S=y($=>{try{return $.status===0?JSON.parse($.stdout):null}catch{return null}},"json"),u=c?await r(["version"]):null,m=u?.status===0,w=m?S(await r(["status","--json"])):null,l=w?.BackendState==="Running",f=w?.Self?.DNSName?.replace(/\.$/,"")||"",h=m&&s==="linux"&&t!==0,d=h?S(await r(["debug","prefs"]))?.OperatorUser:null,k=m?S(await r(["serve","status","--json"])):null,b=R(k);let j=!1;try{const $=new URL(b.root);j=$.protocol==="http:"&&["127.0.0.1","localhost","[::1]"].includes($.hostname)&&Number($.port)===a&&$.pathname==="/"&&!$.search&&!$.hash}catch{}const g=y(($,v,p,x,A="yours")=>({item:$,state:v?"ok":A,detail:p,fix:v?null:x}),"entry"),O=[g("Tailscale",m,m?`${(u.stdout||"installed").trim().split(`
2
- `)[0]} (${c})`:`not on this session\u2019s PATH${s==="darwin"?" or in Applications":""}`,C(s)),g("Tailscale signed in",l,l?`signed in${f?" as "+f:""}`:m?"not connected":"needs Tailscale","tailscale up")];h&&O.push(g("Tailscale operator",d===i,d===i?i:`${d||"none"} (expected ${i})`,"sudo tailscale set --operator=$USER"));const T=b.paths.filter($=>$!=="/"),L=g("tailscale /",j,`${b.root||"nothing"}${j?"":` (expected the hub on ${a})`}${T.length?"; other mounts: "+T.join(", "):""}`,l?"kickflip doctor --fix":m?"tailscale up":C(s),l?"fixable":"yours");return{installed:m,signedIn:l,dns:f,operatorChecked:h,operator:d,username:i,pointsAtHub:j,prereqs:O,root:L,serveConfig:k}}y(lt,"tailscaleState");async function ut({home:e=E.homedir(),cloneRoot:s=N(),port:t=Number(process.env.KICKFLIP_HUB_PORT||4500),uid:i=process.getuid(),env:a=process.env,run:c=_,platform:r=process.platform,tailscale:S=B({env:a,platform:r}),wait:u=y(f=>new Promise(h=>setTimeout(h,f)),"wait"),fetch:m=globalThis.fetch,dryRun:w=!1,log:l=console.log}={}){const f=n.join(e,".claude","kickflip"),h=q({platform:r,home:e,uid:i,env:a,run:c});if(w){try{for(const{link:x,target:A,created:H,replaced:J}of F({home:e,cloneRoot:s,dryRun:w}))l(`would ${H||J?"link":"check"} ${x} -> ${A}`)}catch(x){l(`would refuse: ${x.message}`)}l(`would merge statusLine and the watcher Stop hook, turn on autoCompactEnabled and remove stale hooks in ${n.join(e,".claude","settings.json")}`);const p=P(e);p.length&&l(`would add ${p.join(", ")} from ${I} with ${M(p)}`);for(const x of await h.install(null,{wait:u,dryRun:w}))l(x);l(`would map tailscale / to the hub on ${t} and remove old site mounts`);return}for(const{link:p,target:x,created:A,replaced:H}of F({home:e,cloneRoot:s}))l(`${A||H?"linked":"checked"} ${p} -> ${x}`);for(const p of Y({home:e}))l(p);const d=P(e);if(d.length){const p=c("npx",D(d),{env:a,timeout:3e5});l(p.status===0?`added ${d.join(", ")} from ${I}`:`could not add ${d.join(", ")} from ${I}: ${p.error}; run ${M(d)}`)}const k=U(),b=z(n.join(s,"hub","hub.mjs"),{home:e,cloneRoot:s});o.mkdirSync(f,{recursive:!0});const j=n.join(f,"hub.log"),g=h.definition({node:k,entry:b,state:f,port:t,log:j});for(const p of await h.install(g,{wait:u}))l(p);let O=!1;for(let p=0;p<30&&!O;p++)await u(300),O=!!await X({port:t,fetch:m});if(!O)throw new Error("hub did not start; see "+j);if(!S)return l(`skipped mapping tailscale / to the hub: Tailscale isn't installed on this host; ${C(r)}`);const T=c(S,["serve","--bg",String(t)],{env:a});if(T.status!==0)throw new Error(T.error||"tailscale serve failed");const L=c(S,["serve","status","--json"],{env:a});let $;try{$=JSON.parse(L.stdout)}catch{return}let v={};try{v=JSON.parse(o.readFileSync(n.join(f,"manifest.json"),"utf8")).sites||{}}catch(p){if(p.code!=="ENOENT")throw p}for(const p of R($).paths)p!=="/"&&Object.hasOwn(v,p.replace(/^\//,"").replace(/\/$/,""))&&c(S,["serve","--https=443","--set-path="+p,"off"],{env:a})}y(ut,"runInstall");function Y({home:e=E.homedir()}={}){const s=n.join(e,".claude","settings.json");let t={},i=384;try{t=JSON.parse(o.readFileSync(s,"utf8")),i=o.statSync(s).mode}catch(u){if(u.code!=="ENOENT")throw u}if(!t||typeof t!="object"||Array.isArray(t))throw new Error(`not a settings object: ${s}`);const a=[];let c=!1;const r=n.join(e,".claude","statusline.sh");if(Object.hasOwn(t,"statusLine"))Q(t.statusLine,e)||a.push(`kept existing statusLine in ${s}; it points elsewhere`);else{const u=/^[\w/.-]+$/.test(r)?r:`'${r.replaceAll("'","'\\''")}'`;t.statusLine={type:"command",command:u},c=!0,a.push(`set statusLine in ${s} -> ${u}`)}if(!V(t)){const u=t.hooks&&typeof t.hooks=="object"?t.hooks:{};t.hooks={...u,Stop:[...Array.isArray(u.Stop)?u.Stop:[],{hooks:[{type:"command",command:K}]}]},c=!0,a.push(`added the watcher Stop hook to ${s} -> ${K}`)}t.autoCompactEnabled!==!0&&(t.autoCompactEnabled=!0,c=!0,a.push(`turned on autoCompactEnabled in ${s}`));const S=n.join(e,".claude","hooks","no-paths.py");if(!o.existsSync(S)&&t.hooks&&typeof t.hooks=="object"){const u=y(l=>l.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"escape"),m=[S,"~/.claude/hooks/no-paths.py","$HOME/.claude/hooks/no-paths.py","${HOME}/.claude/hooks/no-paths.py"].map(u).join("|"),w=new RegExp(`^\\s*(?:exec\\s+)?(?:(?:/usr/bin/env\\s+)?(?:[^\\s]+/)?python(?:\\d+(?:\\.\\d+)*)?(?:\\s+-[^\\s]+)*\\s+)?(?:"(?:${m})"|'(?:${m})'|(?:${m}))(?=$|\\s)`);for(const[l,f]of Object.entries(t.hooks))Array.isArray(f)&&(t.hooks[l]=f.flatMap(h=>{if(!Array.isArray(h?.hooks))return[h];const d=h.hooks.filter(k=>k?.type!=="command"||typeof k.command!="string"||!w.test(k.command)?!0:(a.push(`removed stale hook ${l}: ${k.command}`),c=!0,!1));return d.length===h.hooks.length?[h]:d.length?[{...h,hooks:d}]:[]}))}if(c){o.mkdirSync(n.dirname(s),{recursive:!0});const u=`${s}.${process.pid}.${Date.now()}.tmp`;try{o.writeFileSync(u,JSON.stringify(t,null,2)+`
3
- `,{mode:i,flag:"wx"}),o.renameSync(u,s)}finally{try{o.unlinkSync(u)}catch{}}}return a}y(Y,"installClaudeSettings");function dt({home:e=E.homedir(),skillsDir:s=n.join(e,".claude","skills"),cloneRoot:t=N()}={}){const i=n.join(e,".claude","statusline.sh");if(!o.lstatSync(i,{throwIfNoEntry:!1})?.isSymbolicLink())return!1;let c=n.resolve(n.dirname(i),o.readlinkSync(i))===n.resolve(s,"kickflip","bin","statusline.mjs");try{c||=o.realpathSync(i)===o.realpathSync(n.join(t,"bin","statusline.mjs"))}catch{}return c?(o.unlinkSync(i),!0):!1}y(dt,"uninstallStatusLine");export{K as WATCHER_STOP,V as hasWatcherStop,X as hubStatus,G as inspectLinks,Y as installClaudeSettings,F as installLinks,U as nodeBin,Q as ownsStatusLine,ut as runInstall,z as storedPath,ct as tailnetUrl,R as tailscaleRoot,lt as tailscaleState,dt as uninstallStatusLine,at as watcherCommand};
1
+ var W=Object.defineProperty;var y=(e,n)=>W(e,"name",{value:n,configurable:!0});import r from"node:fs";import v from"node:os";import s from"node:path";import{SKILLS_SOURCE as I,addArgv as D,addCommand as M,missingSkills as P}from"../shared/skills.mjs";import{cloneRoot as O}from"../shared/repos.mjs";import{hubService as q,runCommand as _}from"../shared/service.mjs";import{tailscaleBin as B,tailscaleInstallHint as H}from"../shared/tailscale.mjs";const U=y(()=>r.existsSync("/opt/homebrew/bin/node")?"/opt/homebrew/bin/node":process.execPath,"nodeBin"),at=y((e,n,t=O())=>`${U()} ${s.join(t,"bin","wait-events.mjs")} ${e} ${n}`,"watcherCommand");function z(e,{home:n=v.homedir(),skillsDir:t=s.join(n,".claude","skills"),cloneRoot:c=O()}={}){if(typeof e!="string"||!s.isAbsolute(e))return e;const o=s.relative(c,e);if(o===".."||o.startsWith(`..${s.sep}`)||s.isAbsolute(o))return e;const a=s.resolve(t,"kickflip");let i=!1;try{i=r.realpathSync(a)===r.realpathSync(c)}catch{}return i?s.join(a,o):e}y(z,"storedPath");function G({home:e=v.homedir(),skillsDir:n=s.join(e,".claude","skills"),cloneRoot:t=O()}={}){const c=r.realpathSync(t),o=[["kickflip",c],["kickflip-hooks",s.join(c,"hooks-plugin")]].map(([u,p])=>{if(!r.statSync(p,{throwIfNoEntry:!1})?.isDirectory())throw new Error(`not a directory: ${p}`);const f=u==="kickflip"?p:s.resolve(n,"kickflip","hooks-plugin"),h=s.resolve(n,u),m=r.lstatSync(h,{throwIfNoEntry:!1});let j;if(m){let g;try{g=r.realpathSync(h)}catch(b){if(b.code!=="ENOENT")throw b}m.isSymbolicLink()?g!==r.realpathSync(p)&&(j=`refusing to replace link ${h} -> ${r.readlinkSync(h)}; remove that link explicitly, then rerun kickflip doctor --fix`):(!m.isDirectory()||g!==r.realpathSync(p))&&(j=`refusing to replace real path ${h}; move it aside explicitly, then rerun kickflip doctor --fix`)}return{link:h,target:f,created:!m,...j?{refusal:j}:{}}}),a=s.join(c,"bin","statusline.mjs");if(!r.statSync(a,{throwIfNoEntry:!1})?.isFile())throw new Error(`not a file: ${a}`);const i=s.join(e,".claude","statusline.sh"),S=s.resolve(n,"kickflip","bin","statusline.mjs"),l=r.lstatSync(i,{throwIfNoEntry:!1});let w=!1,k;if(l)if(!l.isSymbolicLink())k=`refusing to replace real path ${i}; move it aside explicitly, then rerun kickflip doctor --fix`;else{let u;try{u=r.realpathSync(i)}catch(m){if(m.code!=="ENOENT")throw m}const p=s.join(e,".claude","kickflip","sites");let f=p;try{f=r.realpathSync(p)}catch{}const h=!!u&&[p,f].some(m=>u.startsWith(m+s.sep));u&&u!==r.realpathSync(a)&&!h&&(k=`refusing to replace link ${i} -> ${r.readlinkSync(i)}; remove that link explicitly, then rerun kickflip doctor --fix`),w=h||s.resolve(s.dirname(i),r.readlinkSync(i))!==S||!u}return o.push({link:i,target:S,created:!l,...w?{replaced:!0}:{},...k?{refusal:k}:{}}),o}y(G,"inspectLinks");function F({home:e=v.homedir(),skillsDir:n=s.join(e,".claude","skills"),cloneRoot:t=O(),dryRun:c=!1}={}){const o=G({home:e,skillsDir:n,cloneRoot:t}),a=o.find(i=>i.refusal);if(a)throw new Error(a.refusal);if(c)return o;r.mkdirSync(n,{recursive:!0}),r.mkdirSync(s.join(e,".claude"),{recursive:!0});for(const{link:i,target:S,created:l,replaced:w}of o)w&&r.unlinkSync(i),(l||w)&&r.symlinkSync(S,i,i===s.join(e,".claude","statusline.sh")?"file":"dir");return o}y(F,"installLinks");function Q(e,n=v.homedir()){const t=s.join(n,".claude","statusline.sh"),c=[t,`"${t}"`,`'${t.replaceAll("'","'\\''")}'`,"~/.claude/statusline.sh","$HOME/.claude/statusline.sh",'"$HOME/.claude/statusline.sh"',"${HOME}/.claude/statusline.sh",'"${HOME}/.claude/statusline.sh"'];return e?.type==="command"&&c.includes(e.command)}y(Q,"ownsStatusLine");const K='node "$HOME/.claude/skills/kickflip/hooks-plugin/hooks/watcher-stop.mjs"',V=y(e=>(Array.isArray(e?.hooks?.Stop)?e.hooks.Stop:[]).some(n=>Array.isArray(n?.hooks)&&n.hooks.some(t=>t?.type==="command"&&/\/hooks-plugin\/hooks\/watcher-stop\.mjs\b/.test(t.command))),"hasWatcherStop");async function X({port:e=Number(process.env.KICKFLIP_HUB_PORT||4500),fetch:n=globalThis.fetch}={}){try{const t=await n(`http://127.0.0.1:${e}/_hub/status.json`,{signal:AbortSignal.timeout(1500)});if(!t.ok)return null;const c=await t.json();return Array.isArray(c?.sites)?c:null}catch{return null}}y(X,"hubStatus");function R(e){const n=Object.entries(e?.Web||{}).filter(([t])=>!/:\d+$/.test(t)||t.endsWith(":443")).flatMap(([,t])=>Object.entries(t.Handlers||{}));return{root:n.find(([t])=>t==="/")?.[1]?.Proxy||null,paths:[...new Set(n.map(([t])=>t))]}}y(R,"tailscaleRoot");function ct(e,{run:n=_,platform:t=process.platform,tailscale:c=B({platform:t})}={}){if(!c)throw new Error(`Tailscale isn't installed on this host; ${H(t)}`);const o=n(c,["status","--json"]);let a=null;try{a=o.status===0?JSON.parse(o.stdout):null}catch{}if(a?.BackendState!=="Running")throw new Error("Tailscale isn't running on this host; start it, or run tailscale up");const i=a.Self?.DNSName?.replace(/\.$/,"")||"";if(!i||!a.CertDomains?.includes(i))throw new Error("HTTPS isn't on for this tailnet; turn on HTTPS Certificates on the DNS page of the Tailscale admin console");return`https://${i}/${e?e+"/":""}`}y(ct,"tailnetUrl");async function lt({skipped:e=[],run:n=_,platform:t=process.platform,uid:c=process.getuid(),username:o=v.userInfo().username,port:a=Number(process.env.KICKFLIP_HUB_PORT||4500),tailscale:i=B({platform:t})}={}){const S=y(async $=>{try{return await n(i,$)}catch(d){return{status:1,error:d.message}}},"execute"),l=y($=>{try{return $.status===0?JSON.parse($.stdout):null}catch{return null}},"json"),w=i?await S(["version"]):null,k=w?.status===0,u=k?l(await S(["status","--json"])):null,p=u?.BackendState==="Running",f=u?.Self?.DNSName?.replace(/\.$/,"")||"",h=k&&t==="linux"&&c!==0,m=h?l(await S(["debug","prefs"]))?.OperatorUser:null,j=k?l(await S(["serve","status","--json"])):null,g=R(j);let b=!1;try{const $=new URL(g.root);b=$.protocol==="http:"&&["127.0.0.1","localhost","[::1]"].includes($.hostname)&&Number($.port)===a&&$.pathname==="/"&&!$.search&&!$.hash}catch{}const E=y(($,d,x,T,A="yours")=>({item:$,state:d?"ok":A,detail:x,fix:d?null:T}),"entry"),N=[E("Tailscale",k,k?`${(w.stdout||"installed").trim().split(`
2
+ `)[0]} (${i})${e.length?`; skipped ${e.join(", ")}, whose daemon${e.length===1?" isn\u2019t":"s aren\u2019t"} running`:""}`:`not on this session\u2019s PATH${t==="darwin"?" or in Applications":""}`,H(t)),E("Tailscale signed in",p,p?`signed in${f?" as "+f:""}`:k?"not connected":"needs Tailscale","tailscale up")];h&&N.push(E("Tailscale operator",m===o,m===o?o:`${m||"none"} (expected ${o})`,"sudo tailscale set --operator=$USER"));const C=g.paths.filter($=>$!=="/"),L=E("tailscale /",b,`${g.root||"nothing"}${b?"":` (expected the hub on ${a})`}${C.length?"; other mounts: "+C.join(", "):""}`,p?"kickflip doctor --fix":k?"tailscale up":H(t),p?"fixable":"yours");return{installed:k,signedIn:p,dns:f,operatorChecked:h,operator:m,username:o,pointsAtHub:b,prereqs:N,root:L,serveConfig:j}}y(lt,"tailscaleState");async function ut({home:e=v.homedir(),cloneRoot:n=O(),port:t=Number(process.env.KICKFLIP_HUB_PORT||4500),uid:c=process.getuid(),env:o=process.env,run:a=_,platform:i=process.platform,tailscale:S=B({env:o,platform:i}),wait:l=y(p=>new Promise(f=>setTimeout(f,p)),"wait"),fetch:w=globalThis.fetch,dryRun:k=!1,log:u=console.log}={}){const p=s.join(e,".claude","kickflip"),f=q({platform:i,home:e,uid:c,env:o,run:a});if(k){try{for(const{link:x,target:T,created:A,replaced:J}of F({home:e,cloneRoot:n,dryRun:k}))u(`would ${A||J?"link":"check"} ${x} -> ${T}`)}catch(x){u(`would refuse: ${x.message}`)}u(`would merge statusLine and the watcher Stop hook, turn on autoCompactEnabled and remove stale hooks in ${s.join(e,".claude","settings.json")}`);const d=P(e);d.length&&u(`would add ${d.join(", ")} from ${I} with ${M(d)}`);for(const x of await f.install(null,{wait:l,dryRun:k}))u(x);u(`would map tailscale / to the hub on ${t} and remove old site mounts`);return}for(const{link:d,target:x,created:T,replaced:A}of F({home:e,cloneRoot:n}))u(`${T||A?"linked":"checked"} ${d} -> ${x}`);for(const d of Y({home:e}))u(d);const h=P(e);if(h.length){const d=a("npx",D(h),{env:o,timeout:3e5});u(d.status===0?`added ${h.join(", ")} from ${I}`:`could not add ${h.join(", ")} from ${I}: ${d.error}; run ${M(h)}`)}const m=U(),j=z(s.join(n,"hub","hub.mjs"),{home:e,cloneRoot:n});r.mkdirSync(p,{recursive:!0});const g=s.join(p,"hub.log"),b=f.definition({node:m,entry:j,state:p,port:t,log:g});for(const d of await f.install(b,{wait:l}))u(d);let E=!1;for(let d=0;d<30&&!E;d++)await l(300),E=!!await X({port:t,fetch:w});if(!E)throw new Error("hub did not start; see "+g);if(!S)return u(`skipped mapping tailscale / to the hub: Tailscale isn't installed on this host; ${H(i)}`);const N=a(S,["serve","--bg",String(t)],{env:o});if(N.status!==0)throw new Error(N.error||"tailscale serve failed");const C=a(S,["serve","status","--json"],{env:o});let L;try{L=JSON.parse(C.stdout)}catch{return}let $={};try{$=JSON.parse(r.readFileSync(s.join(p,"manifest.json"),"utf8")).sites||{}}catch(d){if(d.code!=="ENOENT")throw d}for(const d of R(L).paths)d!=="/"&&Object.hasOwn($,d.replace(/^\//,"").replace(/\/$/,""))&&a(S,["serve","--https=443","--set-path="+d,"off"],{env:o})}y(ut,"runInstall");function Y({home:e=v.homedir()}={}){const n=s.join(e,".claude","settings.json");let t={},c=384;try{t=JSON.parse(r.readFileSync(n,"utf8")),c=r.statSync(n).mode}catch(l){if(l.code!=="ENOENT")throw l}if(!t||typeof t!="object"||Array.isArray(t))throw new Error(`not a settings object: ${n}`);const o=[];let a=!1;const i=s.join(e,".claude","statusline.sh");if(Object.hasOwn(t,"statusLine"))Q(t.statusLine,e)||o.push(`kept existing statusLine in ${n}; it points elsewhere`);else{const l=/^[\w/.-]+$/.test(i)?i:`'${i.replaceAll("'","'\\''")}'`;t.statusLine={type:"command",command:l},a=!0,o.push(`set statusLine in ${n} -> ${l}`)}if(!V(t)){const l=t.hooks&&typeof t.hooks=="object"?t.hooks:{};t.hooks={...l,Stop:[...Array.isArray(l.Stop)?l.Stop:[],{hooks:[{type:"command",command:K}]}]},a=!0,o.push(`added the watcher Stop hook to ${n} -> ${K}`)}t.autoCompactEnabled!==!0&&(t.autoCompactEnabled=!0,a=!0,o.push(`turned on autoCompactEnabled in ${n}`));const S=s.join(e,".claude","hooks","no-paths.py");if(!r.existsSync(S)&&t.hooks&&typeof t.hooks=="object"){const l=y(u=>u.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"escape"),w=[S,"~/.claude/hooks/no-paths.py","$HOME/.claude/hooks/no-paths.py","${HOME}/.claude/hooks/no-paths.py"].map(l).join("|"),k=new RegExp(`^\\s*(?:exec\\s+)?(?:(?:/usr/bin/env\\s+)?(?:[^\\s]+/)?python(?:\\d+(?:\\.\\d+)*)?(?:\\s+-[^\\s]+)*\\s+)?(?:"(?:${w})"|'(?:${w})'|(?:${w}))(?=$|\\s)`);for(const[u,p]of Object.entries(t.hooks))Array.isArray(p)&&(t.hooks[u]=p.flatMap(f=>{if(!Array.isArray(f?.hooks))return[f];const h=f.hooks.filter(m=>m?.type!=="command"||typeof m.command!="string"||!k.test(m.command)?!0:(o.push(`removed stale hook ${u}: ${m.command}`),a=!0,!1));return h.length===f.hooks.length?[f]:h.length?[{...f,hooks:h}]:[]}))}if(a){r.mkdirSync(s.dirname(n),{recursive:!0});const l=`${n}.${process.pid}.${Date.now()}.tmp`;try{r.writeFileSync(l,JSON.stringify(t,null,2)+`
3
+ `,{mode:c,flag:"wx"}),r.renameSync(l,n)}finally{try{r.unlinkSync(l)}catch{}}}return o}y(Y,"installClaudeSettings");function dt({home:e=v.homedir(),skillsDir:n=s.join(e,".claude","skills"),cloneRoot:t=O()}={}){const c=s.join(e,".claude","statusline.sh");if(!r.lstatSync(c,{throwIfNoEntry:!1})?.isSymbolicLink())return!1;let a=s.resolve(s.dirname(c),r.readlinkSync(c))===s.resolve(n,"kickflip","bin","statusline.mjs");try{a||=r.realpathSync(c)===r.realpathSync(s.join(t,"bin","statusline.mjs"))}catch{}return a?(r.unlinkSync(c),!0):!1}y(dt,"uninstallStatusLine");export{K as WATCHER_STOP,V as hasWatcherStop,X as hubStatus,G as inspectLinks,Y as installClaudeSettings,F as installLinks,U as nodeBin,Q as ownsStatusLine,ut as runInstall,z as storedPath,ct as tailnetUrl,R as tailscaleRoot,lt as tailscaleState,dt as uninstallStatusLine,at as watcherCommand};
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- var L=Object.defineProperty;var r=(e,a)=>L(e,"name",{value:a,configurable:!0});import{readiness as H}from"../../hub/readiness.mjs";import{readSlack as D,setSlack as M}from"../../hub/slack.mjs";import{routeRecord as y}from"../../hub/routes.mjs";import g from"node:fs";import x from"node:path";import{pathToFileURL as W}from"node:url";import*as s from"../store.mjs";import{boardWorktree as z,builderRepo as U,joiningLand as F,landInTurn as J,mergeHelper as G,worktreesDir as K}from"../worktree.mjs";import{pidAlive as Q}from"../headless.mjs";import{fileFindings as V}from"../arch-review.mjs";import{finishCard as $,landHeld as X}from"../finish.mjs";import{bump as S,queueLines as Y,stopBuilder as Z,stopForBlock as ee,stopLand as te}from"../stop.mjs";import{boardAt as ae}from"../../hub/boards.mjs";import"../../hub/board-repo.mjs";import{checkTakesRender as se}from"../render-check.mjs";import{prototypeBase as ne}from"../prototype-base.mjs";const re=`usage: node board.mjs <command> [--site <route>] ...
2
+ var L=Object.defineProperty;var r=(e,a)=>L(e,"name",{value:a,configurable:!0});import{readiness as H}from"../../hub/readiness.mjs";import{readSlack as D,setSlack as M}from"../../hub/slack.mjs";import{routeRecord as y}from"../../hub/routes.mjs";import f from"node:fs";import x from"node:path";import{pathToFileURL as W}from"node:url";import*as s from"../store.mjs";import{boardWorktree as z,builderRepo as U,joiningLand as F,landInTurn as J,mergeHelper as G,worktreesDir as K}from"../worktree.mjs";import{pidAlive as Q}from"../headless.mjs";import{fileFindings as V}from"../arch-review.mjs";import{finishCard as $,landHeld as X}from"../finish.mjs";import{bump as O,queueLines as Y,stallCard as Z,stopBuilder as ee,stopForBlock as te,stopLand as ae}from"../stop.mjs";import{boardAt as se}from"../../hub/boards.mjs";import"../../hub/board-repo.mjs";import{checkTakesRender as ne}from"../render-check.mjs";import{prototypeBase as re}from"../prototype-base.mjs";const ie=`usage: node board.mjs <command> [--site <route>] ...
3
3
 
4
4
  Writes a kickflip board's data files. --site <route> picks ~/.claude/kickflip/sites/<route>, and its
5
5
  board.json says where the board's waves live. Without --site, waves come from ~/.claude/waves.
@@ -145,7 +145,7 @@ command fails and changes nothing.
145
145
  server records the owner's own Hold and Resume taps. A later
146
146
  --held/--unheld overrides a tap.
147
147
  --pick-up-at holds it until an ISO time such as
148
- 2026-11-04T09:00 (the Mac's local time without an
148
+ 2026-11-04T09:00 (the host's local time without an
149
149
  offset; a past time is refused), stored as UTC
150
150
  pick_up_at. Within a minute of it the board server
151
151
  resumes the card, adds a thread line, and appends
@@ -159,13 +159,6 @@ command fails and changes nothing.
159
159
  [--size small|large] [--model opus|sonnet] what builder pick reads while the hub's Claude model is
160
160
  Auto: a small card gets Claude Sonnet, any other Opus, and
161
161
  --model wins over --size; "" clears either
162
- [--notes-file <file>] the item's notes: a .md file inside its wave's folder,
163
- named relative to it, holding the session's understanding
164
- of the task so far. The open card's Notes link opens it,
165
- or without one the item's short notes text. --plan is an
166
- alias. Resolves answered questions still waiting on
167
- Claude, noting "Recorded in the card's notes", and
168
- prints "resolved: <id>" for each
169
162
  [--deps <id>,<wave>/<id>] replaces its blocked-by list; --deps "" clears it
170
163
  [--blocked-until "<condition>"] blocks it until a condition an agent can check, which
171
164
  the orchestrator checks on its passes over the cards;
@@ -207,9 +200,12 @@ command fails and changes nothing.
207
200
  It prints "unblocked: <wave>/<id>" and appends card-unblocked {key} once per unblocking,
208
201
  including held cards: start each unheld card next. It refuses a card the owner reopened: it
209
202
  closes again only on the owner's Done
210
- item add <wave>[/<id>] --title <t> [--notes <t>] a new card, unstarted; never
203
+ item add <wave>[/<id>] --title <t> a new card, unstarted; never
211
204
  [--ticket <id>] [--deps <id>,<wave>/<id>] hand-edit wave.json to add one. Refuses an id the wave has.
212
- [--notes-file <file>] --plan is an alias of --notes-file
205
+ [--notes <t> | --notes-from <draft.md>] the board makes the card's own notes, <id>-notes.md beside
206
+ its wave file, read-only: the draft verbatim, or "# <title>"
207
+ then the --notes text. Refuses a draft that is any card's
208
+ notes file, live, done or archived, naming that card
213
209
  [--pick-up-at <time>] holds it until then, as item set's --pick-up-at
214
210
  [--size small|large] [--model opus|sonnet] as item set's
215
211
  [--parent <wave>/<id>] the card it was spun out of, such as one filed from
@@ -223,6 +219,16 @@ command fails and changes nothing.
223
219
  brings in intake triage, which links and files it
224
220
  Without /<id>, the card takes its ticket's id (GH-<n> for issue #n); pass an id only to import a
225
221
  card. Prints "<label> added as <key>": use the key in later commands
222
+ item notes <wave>/<id> --heading <h> appends "## <h>" and the draft to the card's notes, making
223
+ (--file <draft.md> | --from <wave>/<id>) its <id>-notes.md when it has none; the only way to write
224
+ notes (--notes-file and --plan are gone). Write the draft
225
+ outside the board's data; one that is any card's notes
226
+ file, live, done or archived, is refused, naming that
227
+ card. --from folds that other card's notes in instead,
228
+ headed "Folded from <ref> "<title>"" without --heading.
229
+ Resolves answered questions still waiting on Claude,
230
+ noting "Recorded in the card's notes", and prints
231
+ "resolved: <id>" for each
226
232
  item relate-ask <eventId> <wave>/<id> --summary <t> folds an ask about an existing item into it: the ask's
227
233
  card goes, its message and thread show in the item's
228
234
  thread marked Moved, then the summary line. Again with
@@ -496,6 +502,14 @@ command fails and changes nothing.
496
502
  waiting on the orchestrator; the same words go in its thread.
497
503
  Prints what it stopped, one process a line. Refuses a planner,
498
504
  a builder a session started itself, or none running
505
+ item stall <wave>/<id> --reason "<why>" the orchestrator's flag on a stale card it can't restart: the
506
+ card stays in its lane reading red "stalled", an Action needed
507
+ "stalled: <why>" with one push, waiting on nobody else; its
508
+ run stops as builder stop stops it (nothing to stop when it
509
+ is gone), its record stays open and stopped and its worktree
510
+ stays, and it frees its implementer slot. Its thread reads
511
+ "stalled by orchestrator: <why>". A message on the card
512
+ clears it
499
513
  land stop <wave>/<id> the orchestrator's stop of the card's land and its suite: one a
500
514
  headless builder or lander runs stops that agent as builder
501
515
  stop does; the land a held card's Merge tap started stops
@@ -635,4 +649,4 @@ command fails and changes nothing.
635
649
  (overnight, snoozed or the weekend). --urgent: a
636
650
  production fault only; it beats those holds. While
637
651
  notifications are off every push is dropped, urgent too
638
- KICKFLIP_PUSH_CMD='["<cmd>","<arg>"]' replaces \`node ../../bin/kickflip.mjs push\`; the flags follow it`;class d extends Error{static{r(this,"UsageError")}}const ie=new Set(["done","clear-status","clear-next","urgent","now","clear-eta","held","unheld","idle","hub","clear-install","force","worktree","dry-run","dispatch","writes"]);function oe(e){const a=Object.create(null),t=[];for(let n=0;n<e.length;n++){const i=e[n];if(!i.startsWith("--")){t.push(i);continue}const o=i.slice(2);if(ie.has(o)){if(Object.hasOwn(a,o))throw new d(`--${o} is given twice`);a[o]=!0}else if(n+1<e.length)(a[o]||=[]).push(e[++n]);else throw new d(`--${o} needs a value`)}return{flags:a,rest:t}}r(oe,"parse");function de(e,a,t){const n=new Set(a.filter(l=>l.startsWith("...")).map(l=>l.slice(3))),i=new Set(a.map(l=>l.replace(/^\.\.\./,""))),o=Object.keys(t).filter(l=>!i.has(l));if(o.length)throw new d(`${e} does not take ${o.map(l=>"--"+l).join(", ")}`);return Object.fromEntries(Object.entries(t).map(([l,c])=>{if(c===!0||n.has(l))return[l,c];if(c.length>1)throw new d(`--${l} is given twice`);return[l,c[0]]}))}r(de,"flagsOf");const le=["html","text","note","notes","summary","title","body","next","progress","install-label","confirmed","asked","task"],he=/^(?:\/|~\/|\.{1,2}\/)\S*\.(?:html?|md|json|txt)$/;function ce(e){const a=e.trim();if(!a||/\s/.test(a))return!1;if(he.test(a))return!0;try{return g.statSync(a).isFile()}catch{return!1}}r(ce,"looksLikePath");function ue(e,a){for(const t of le){if(typeof a[t]!="string"||!ce(a[t]))continue;const n=e.includes("file")?`; for a file use --file ${a[t].trim()}`:"";throw new d(`--${t} takes text, not a file path (${a[t].trim()})${n}`)}if(typeof a.file=="string"&&a.file!=="-"&&!g.existsSync(a.file))throw new d(`--file ${a.file}: no such file`)}r(ue,"checkInputs");const w=r(e=>g.readFileSync(e==="-"?0:e,"utf8"),"readInput");function R(e){if(!e)throw new d("--file <path> is required");try{return JSON.parse(w(e))}catch(a){throw new s.BoardError(`${e}: ${a.message}`)}}r(R,"readJSONInput");function I(e){if(e.plan!==void 0&&e["notes-file"]!==void 0)throw new d("pass --notes-file or --plan, not both");return e["notes-file"]??e.plan}r(I,"notesFileOf");function C(e){if(e.note!==void 0&&e.file!==void 0)throw new d("pass --note or --file, not both");return e.file===void 0?e.note:w(e.file)}r(C,"noteOf");async function pe(e,a,{"owner-step":t,check:n,text:i,...o}){if(t!==void 0&&(!e||o.dir))throw new d("--owner-step goes on a board card: pass --site, not --dir");if(n!==void 0&&t===void 0)throw new d("--check goes with --owner-step: it is the code check that closes the card once the step is done");t!==void 0&&await s.setItem(e,a,{ownerStep:t,check:n});const l=e&&!o.dir,c=l&&await s.handBackCalls(e,a,o);if(c)return c;if(l&&o.next===void 0&&await s.finishesCard(e,a,o)){if(i===void 0)throw new s.BoardError(`builder done finishes ${a}: pass --text "<what you checked>", or --next "<what happens next>" to stop without finishing`);return $(e,a,{text:i,ownerStep:t})}if(l&&await s.isHelper(e,a,o.agent)){if(i===void 0)throw new d('a helper hands back its result: pass --text "<what you did or found>"');return s.stopBuilder(e,a,{...o,note:i})}if(i!==void 0)throw new d("--text goes on a builder done that finishes the card, the main build with a worktree and no --next, or on a helper's");return s.stopBuilder(e,a,{...o,requireNext:!0})}r(pe,"builderDone");const f={pr:[["wave/id"],[],async(e,[a])=>({lines:(await s.prReport(e,a)).map(t=>JSON.stringify(t))})],"pr comment":[["pr"],["body"],(e,[a],t)=>{if(t.body===void 0)throw new d('pr comment needs --body "<what Claude says>"');return s.postPR(e,a,{body:t.body})}],"pr reply":[["pr","comment-id"],["body"],(e,[a,t],n)=>{if(n.body===void 0)throw new d('pr reply needs --body "<what Claude says>"');return s.postPR(e,a,{replyTo:t,body:n.body})}],reply:[["eventId"],["html","file"],(e,[a],t)=>{if(t.html===void 0==(t.file===void 0))throw new d("reply takes --html or --file");return s.writeReply(e,a,t.html??w(t.file))}],thread:[["wave/id"],["text","link","who","via"],(e,[a],t)=>s.appendThread(e,a,t)],"visual add":[["wave/id"],["file","title"],(e,[a],t)=>s.addVisual(e,a,t)],"topic add":[["id"],["title","summary"],(e,[a],t)=>s.addTopic(e,{id:a,...t})],"question add":[[],["file"],async(e,a,t)=>{const n=R(t.file);return{...await s.addQuestions(e,n,process.env.KICKFLIP_RENDER_CHECK==="off"?void 0:se),asked:[n].flat()}}],"question resolve":[["id"],["note"],(e,[a],t)=>s.resolveQuestion(e,a,t.note)],"question move":[["ids"],["to","block"],(e,[a],t)=>s.moveQuestions(e,a.split(",").filter(Boolean),t.to,(t.block||"").split(",").filter(Boolean))],"prototype add":[["topic"],["title","...take","card","served","...state"],(e,[a],t)=>s.addPrototype(e,{topic:a,title:t.title,card:t.card,takes:t.take,served:t.served,states:t.state})],"prototype base":[[],[],e=>ne(e)],"prototype list":[[],[],e=>s.listPrototypes(e)],"prototype rm":[["topic"],[],(e,[a])=>s.removePrototype(e,a)],"item set":[["wave/id"],["done","status","clear-status","next","clear-next","...pr","pushed","gh-since","from-ask","state","held","unheld","pick-up-at","notes-file","plan","deps","blocked-until","block-reason","parent","related","relate","unrelate","install","install-sha","install-label","clear-install","title","size","model","force","ticket","text","check"],async(e,[a],t,n)=>(t.done&&e&&await s.checkNotReopened(e,a),s.setItem(e,a,{done:t.done,status:t.status,clearStatus:t["clear-status"],next:t.next,clearNext:t["clear-next"],pr:t.pr,pushed:t.pushed,ghSince:t["gh-since"],fromAsk:t["from-ask"],state:t.state,held:t.held,unheld:t.unheld,pickUpAt:t["pick-up-at"],notesFile:I(t),deps:t.deps,blockedUntil:t["blocked-until"],blockReason:t["block-reason"],parent:t.parent,related:t.related,relate:t.relate,unrelate:t.unrelate,install:t.install,installSha:t["install-sha"],installLabel:t["install-label"],clearInstall:t["clear-install"],title:t.title,size:t.size,model:t.model,force:t.force,ticket:t.ticket,text:t.text,check:t.check,stopRun:r(()=>ee(e,a),"stopRun")},n))],"item add":[["wave[/id]"],["title","notes","deps","notes-file","plan","ticket","pick-up-at","parent","size","model","state","relate"],(e,[a],t,n)=>s.addItem(e,a,{title:t.title,notes:t.notes,deps:t.deps,notesFile:I(t),ticket:t.ticket,pickUpAt:t["pick-up-at"],parent:t.parent,size:t.size,model:t.model,state:t.state,relate:t.relate,intake:!0},n)],"item relate-ask":[["eventId","wave/id"],["summary"],(e,[a,t],n)=>s.relateAsk(e,a,t,n.summary)],"ask progress":[["eventId"],["next"],(e,[a],t)=>s.markAsk(e,a,"progress",t.next)],"ask done":[["eventId"],[],(e,[a])=>s.markAsk(e,a,"done")],"intake handoff":[["eventId"],["text"],(e,[a],t)=>s.intakeHandoff(e,a,t.text)],"ask unfold":[["eventId"],[],(e,[a])=>s.unfoldAsk(e,a)],"ask move":[["eventId"],["to"],(e,[a],t)=>s.moveAsk(e,a,t.to)],"tickets backfill":[[],["dry-run"],(e,a,t,n)=>s.backfillTickets(e,{dryRun:t["dry-run"]},n)],"lineup create":[["slug"],["title","goal","...phase","note","file"],(e,[a],t)=>s.createLineup(e,a,{title:t.title,goal:t.goal,phases:t.phase,note:C(t)})],"lineup add":[["slug","wave/id"],["after"],(e,[a,t],n)=>s.changeLineup(e,a,{add:t,after:n.after})],"lineup remove":[["slug","wave/id"],[],(e,[a,t])=>s.changeLineup(e,a,{remove:t})],"lineup set":[["slug"],["title","goal","note","file","...phase"],(e,[a],t)=>s.changeLineup(e,a,{title:t.title,goal:t.goal,note:C(t),phases:t.phase})],"lineup finish":[["slug"],[],(e,[a])=>s.finishLineup(e,a)],slack:[[],[],()=>({lines:[JSON.stringify(D())]})],"slack set":[[],["self-dm","user","domain","timezone"],async(e,a,t)=>{const n={"self-dm":"self_dm",user:"owner_slack_user",domain:"slack_domain",timezone:"timezone"};try{return{lines:[JSON.stringify(await M(Object.fromEntries(Object.entries(t).map(([i,o])=>[n[i],o]))))]}}catch(i){throw new s.BoardError(i.message)}}],integrations:[[],[],async()=>({lines:[JSON.stringify(await H.rows())]})],"review updated":[["wave/id"],["text"],(e,[a],t)=>s.reviewUpdated(e,a,t.text)],"review record":[["pr"],["state","text","file"],(e,[a],t)=>{if(t.text===void 0==(t.file===void 0))throw new d("review record takes --text or --file");return s.recordReview(e,a,{state:t.state,text:t.text??w(t.file)})}],"review post":[["pr"],["file","read-at"],(e,[a],t)=>{if(t.file===void 0)throw new d("review post needs --file <review.md>");if(t["read-at"]===void 0)throw new d("review post needs --read-at <time>, when you read the PR, so a review that landed since is caught");return s.postIncomingReview(e,a,{words:w(t.file),readAt:t["read-at"]})}],"review digest":[[],[],async(e,a,t,n)=>({lines:[JSON.stringify(await s.reviewDigest(e,n))]})],"review due":[[],[],async(e,a,t,n)=>({lines:[JSON.stringify(await s.reviewDue(e,n))]})],"reviewers set":[[],["file"],(e,a,t)=>s.setReviewers(e,R(t.file))],"reviewers add":[[],["file"],(e,a,t)=>s.addReviewers(e,R(t.file))],"reviewers remove":[["...name"],[],(e,a)=>s.removeReviewers(e,a)],"rules list":[[],[],e=>s.listRules(e)],"rules add":[[],["text","hub","by"],(e,a,t)=>s.addRuleTo(e,t)],"rules edit":[["id"],["text","hub"],(e,[a],t)=>s.editRuleIn(e,a,t)],"rules remove":[["id"],["hub"],(e,[a],t)=>s.removeRuleFrom(e,a,t)],settings:[[],[],async e=>({lines:[JSON.stringify(await s.readBoardSettings(e))]})],build:[["wave/id"],["asked"],async(e,[a],t)=>{if(!e)throw new s.BoardError("build needs --site <route>");s.parseKey(a);const n=await s.askedGate(e,a,t.asked);try{const i=await fetch(`http://127.0.0.1:${process.env.KICKFLIP_HUB_PORT||4500}/_hub/build`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({route:x.basename(e),key:a}),signal:AbortSignal.timeout(1e4)}),o=await i.json();if(!i.ok)throw new s.BoardError(o.error||"The hub refused the build.");return{lines:["started",...(await s.appendThread(e,a,{text:n})).lines]}}catch(i){throw i instanceof s.BoardError?i:new s.BoardError(`Could not start the build: ${i.message}`)}}],"main set":[[],["install","install-sha"],(e,a,t)=>s.setMainInstall(e,{install:t.install,installSha:t["install-sha"]})],"settings set":[["name","value"],[],(e,[a,t])=>s.setBoardSetting(e,a,t)],"builder start":[["wave/id"],["label","phase","progress","dir","agent","kind","model","eta","worktree","repo","dispatch"],(e,[a],{worktree:t,repo:n,dispatch:i,...o})=>{if(n!==void 0&&!t)throw new d("--repo goes with --worktree");const l=r(p=>z(e,a.split("/")[1],{repo:n,...p}),"make"),c=e&&o.dir===void 0&&ae(e);return s.startBuilder(e,a,{...o,dispatch:i?"session":void 0,worktree:t?l:void 0,ownWorktree:c?l:void 0})}],"builder progress":[["wave/id","...text?"],["phase","text","dir","agent","eta","clear-eta"],(e,[a,...t],n)=>{if(t.length&&n.text!==void 0)throw new d("builder progress takes its text once: as words or as --text");if(!t.length&&n.text===void 0)throw new d("builder progress needs its text, as words or as --text");return s.builderProgress(e,a,n.text??t.join(" "),{...n,clearEta:n["clear-eta"]})}],"builder done":[["wave/id"],["dir","agent","next","owner-step","check","text","updated"],async(e,[a],{updated:t,...n})=>{const i=e&&!n.dir;if(i&&t===void 0&&await s.cardLinked(e,a))throw new s.BoardError(`a reviewer has ${a}'s PR link: pass --updated "<what changed>" for its thread, or --updated none for a change only to comments, names, docs or tests`);const o=await pe(e,a,n);return i&&t!==void 0&&t.trim().toLowerCase()!=="none"&&await s.reviewUpdated(e,a,t),o}],"builder pick":[["wave/id"],[],(e,[a])=>s.pickBuilder(e,a)],"agent launch":[["wave/id"],["name","task","writes","model"],(e,[a],t)=>s.requestHelper(e,a,t)],"agent wait":[["wave/id"],["name","agent","timeout"],(e,[a],t)=>s.waitHelper(e,a,{...t,alive:Q})],"agent merge":[["wave/id"],["name"],async(e,[a],t)=>{const{record:n,card:i}=await s.helperOf(e,a,t.name);if(!n.done)throw new s.BoardError(`helper ${t.name} is still open; run agent wait ${a} --name ${t.name} first`);if(!n.writes||!n.worktree)throw new s.BoardError(`helper ${t.name} writes no code, so there is nothing to merge`);if(!i)throw new s.BoardError(`${a} has no worktree to merge helper ${t.name} into`);return G(i,n.worktree)}],audit:[["...wave/id?"],[],(e,[a,...t])=>{if(t.length)throw new d("audit takes one <wave/id> at most");return s.auditLog(e,a)}],"builder call":[["wave/id","...text"],["dir","agent"],(e,[a,...t],n)=>s.recordCall(e,a,t.join(" "),n)],"builder calls":[["wave/id"],["dir","agent"],(e,[a],t)=>s.listCalls(e,a,t)],"builder ask":[["wave/id","...text"],["agent"],(e,[a,...t],n)=>s.askOrchestrator(e,a,t.join(" "),n)],"builder answer":[["wave/id","ask"],["text"],(e,[a,t],n)=>s.answerBuilder(e,a,t,n.text)],"builder defer":[["wave/id","ask"],["text"],(e,[a,t],n)=>s.answerBuilder(e,a,t,n.text,{deferred:!0})],"builder asks":[["wave/id"],[],(e,[a])=>s.listAsks(e,a)],"builder verified":[["wave/id"],["dir","agent","note"],async(e,[a],t)=>{await s.verifyCalls(e,a,t);const n=e&&!t.dir?await s.verifiedFinish(e,a):null;if(!n)return{};if(n.waits.length)return{lines:[`${await s.cardName(e,a)} finishes once the owner answers ${n.waits.join(", ")}`]};try{return await $(e,a,{text:t.note??"Judgment calls verified."})}catch(i){throw i instanceof s.BoardError?Object.assign(new s.BoardError(`judgment calls verified, but ${await s.cardName(e,a)} didn't finish: ${i.message}`),{exitCode:i.exitCode}):i}}],land:[["wave/id"],["repo","dir","text","owner-step","check"],async(e,[a],t)=>{const n=a.split("/")[1];if(e&&t.text===void 0)throw new d(`land needs --text "<what was checked>", which closes ${a} as done once it lands`);if(!e&&t.text!==void 0)throw new d("--text closes the card, which needs --site");if(!e&&t["owner-step"]!==void 0)throw new d("--owner-step keeps a board card open, which needs --site");if(t.check!==void 0&&t["owner-step"]===void 0)throw new d("--check goes with --owner-step: it is the code check that closes the card once the step is done");if(e&&t.dir===void 0)return $(e,a,{text:t.text,ownerStep:t["owner-step"],check:t.check,repo:t.repo});const i=e?await s.cardName(e,a):void 0,o=await F(i??a,async()=>{e&&await s.checkClose(e,a,t.text,t["owner-step"],t.check),await s.checkCallsVerified(e,a,{dir:t.dir});const l=await s.recordedWorktree(e,a,{dir:t.dir}),{repo:c,base:p}=U(e,{repo:t.repo??l?.repo});return await J({path:l?.path??x.join(K(),n),branch:l?.branch??n,repo:x.resolve(c),base:p,ref:i}),e?s.setItem(e,a,{done:!0,text:t.text,ownerStep:t["owner-step"],check:t.check}):{lines:[]}});return o.joined?{lines:[`joined the land already running for ${i??a}`]}:o}],"builder stop":[["wave/id"],[],(e,[a])=>Z(b(e,"builder stop"),a)],"land stop":[["wave/id"],[],(e,[a])=>te(b(e,"land stop"),a)],"land queue":[[],[],()=>Y()],"item bump":[["wave/id"],[],(e,[a])=>S(b(e,"item bump"),a,!0)],"item unbump":[["wave/id"],[],(e,[a])=>S(b(e,"item unbump"),a,!1)],lander:[["wave/id"],[],async(e,[a])=>{if(!e)throw new d("lander needs --site <route>");return await s.relaunchLander(e,a),{lines:[`${await s.cardName(e,a)} is handed back again; the server launches a lander for it within a minute`]}}],merge:[["wave/id"],["after-approval","confirmed"],async(e,[a],t)=>e&&await s.heldMerge(e,a)?X(e,a):s.mergeCard(e,a,{afterApproval:t["after-approval"],confirmed:t.confirmed})],"arch file":[["wave/id"],[],(e,[a])=>e?V(e,a):Promise.reject(new s.BoardError("arch file needs --site <route>"))],push:[[],["kind","title","body","url","tag","urgent","now"],(e,a,t)=>{if(t.kind!=="fault"&&t.kind!=="reminder")throw new d(`a session pushes only --kind fault or reminder${t.kind===void 0||t.kind===!0?"":`, not "${t.kind}"`}: the board server pushes Action needed, and news goes on the board`);if(t.title===void 0)throw new d("push needs --title");return s.sendPush(e,t)}]};f.install=f.build;function b(e,a){if(!e)throw new d(`${a} needs --site <route>`);return e}r(b,"needSite");const h=r((e,a)=>s.cardName(e,a),"card"),T=r((e,a)=>`${a}${e.length===1?"":"s"} ${e.join(", ")}`,"plural"),_=r(e=>e.agent===void 0||e.agent==="main"?"builder":`builder ${e.agent}`,"builderOf");function we(e){const a=e["pick-up-at"];return[e.done&&"done",e.next!==void 0&&(e.next.trim()?"next line set":"next line cleared"),e["clear-next"]&&"next line cleared",e.pr!==void 0&&`PR #${e.pr}`,e.pushed!==void 0&&`pushed ${e.pushed}`,e["gh-since"]&&`GitHub read to ${e["gh-since"]}`,e["from-ask"]&&`from task ${e["from-ask"]}`,e.state,e.held&&"held",e.unheld&&"resumed",a!==void 0&&(a?`held until ${a}`:"resumed"),I(e)!==void 0&&"notes recorded",e.deps!==void 0&&(e.deps?`blocked by ${e.deps}`:"deps cleared"),e.parent!==void 0&&(e.parent?`parent ${e.parent}`:"parent cleared"),e.related!==void 0&&(e.related?`related to ${e.related.split(",").join(", ")}`:"related cleared"),e.relate!==void 0&&`related to ${e.relate.split(",").join(", ")} added`,e.unrelate!==void 0&&`unrelated from ${e.unrelate.split(",").join(", ")}`,e.install!==void 0&&"install published",e["clear-install"]&&"install cleared",e.title!==void 0&&"renamed",e.ticket!==void 0&&`ticket ${e.ticket}`,e.check!==void 0&&(e.check.trim()?`check ${e.check.trim().split(/\s+/).join(" ")}`:"check removed")].filter(Boolean).join(", ")}r(we,"itemChanges");const me={reply:r(async(e,[a])=>`reply written to ${await s.eventCard(e,a)}`,"reply"),thread:r(async(e,[a])=>`thread line on ${await h(e,a)}`,"thread"),"topic add":r((e,[a])=>`topic ${a} added`,"topic add"),"question add":r(async(e,a,t,{asked:n,staged:i})=>{const o=[...new Set(n.map(c=>c.topic))],l=o.length!==1?"":o[0].startsWith("item:")?` on ${await h(e,o[0].slice(5))}`:` on topic ${o[0]}`;return`${T(n.map(c=>c.id),"question")} ${i===n.length?"staged":"added"}${l}`},"question add"),"question resolve":r((e,[a])=>`question ${a} resolved`,"question resolve"),"question move":r((e,[a],t)=>`questions ${a} merged onto ${t.to}`,"question move"),"prototype add":r(async(e,[a],t)=>`prototype ${a} recorded${t.card?` for ${await h(e,t.card)}`:""}`,"prototype add"),"prototype rm":r((e,[a])=>`prototype ${a} removed`,"prototype rm"),"item set":r(async(e,[a],t)=>`${await h(e,a)}: ${we(t)}`,"item set"),"item add":r(async(e,a,t,{key:n})=>`${await h(e,n)} added as ${n}`,"item add"),"item relate-ask":r(async(e,[a,t])=>`task ${a} folded into ${await h(e,t)}`,"item relate-ask"),"ask progress":r(async(e,[a])=>`${await h(e,`ask:${a}`)}: in progress`,"ask progress"),"ask done":r(async(e,[a])=>`${await h(e,`ask:${a}`)}: done`,"ask done"),"intake handoff":r(async(e,[a])=>`${await s.eventCard(e,a)} handed to the orchestrator`,"intake handoff"),"ask unfold":r((e,[a])=>`task ${a} back on a card of its own`,"ask unfold"),"ask move":r((e,[a],t)=>`task ${a} moved to /${t.to}`,"ask move"),"lineup create":r((e,[a])=>`lineup ${a} created`,"lineup create"),"lineup add":r(async(e,[a,t])=>`${await h(e,t)} added to lineup ${a}`,"lineup add"),"lineup remove":r(async(e,[a,t])=>`${await h(e,t)} taken out of lineup ${a}`,"lineup remove"),"lineup set":r((e,[a])=>`lineup ${a} updated`,"lineup set"),"lineup finish":r((e,[a])=>`lineup ${a} finished`,"lineup finish"),"review record":r((e,[a],t)=>`review of PR #${a} recorded: ${t.state==="needs"?"needs you":"posted"}`,"review record"),"reviewers set":r(()=>"reviewers replaced","reviewers set"),"reviewers add":r(()=>"reviewers added","reviewers add"),"reviewers remove":r((e,a)=>`${T(a,"reviewer")} removed`,"reviewers remove"),"rules add":r((e,a,t)=>`${t.hub?"hub":"board"} rule added`,"rules add"),"rules edit":r((e,[a],t)=>`${t.hub?"hub":"board"} rule ${a} reworded`,"rules edit"),"rules remove":r((e,[a],t)=>`${t.hub?"hub":"board"} rule ${a} removed`,"rules remove"),"builder start":r(async(e,[a],t,n)=>`${await h(e,a)}: ${n.alreadyOpen?"already open: the Implement tap opened its record; launch the agent, whose own builder start takes it up":`${_(t)} started`}`,"builder start"),"builder progress":r(async(e,[a])=>`progress line on ${await h(e,a)}`,"builder progress"),"builder done":r(async(e,[a],t,n)=>`${await h(e,a)}: ${n?.said??`${_(t)} done${t["owner-step"]===void 0?"":`; owner step "${t["owner-step"].trim()}"`}`}`,"builder done"),"builder call":r(async(e,[a])=>`judgment call recorded on ${await h(e,a)}`,"builder call"),"builder ask":r(async(e,[a],t,{id:n})=>`${await h(e,a)}: asked the orchestrator, ${n}`,"builder ask"),"builder answer":r(async(e,[a,t])=>`${await h(e,a)}: ${t} answered`,"builder answer"),"builder defer":r(async(e,[a,t])=>`${await h(e,a)}: ${t} deferred to the owner`,"builder defer"),"builder verified":r(async(e,[a],t,n)=>`${await h(e,a)}: judgment calls verified${n?.said?`; ${n.said}`:""}`,"builder verified"),land:r(async(e,[a],t,n)=>e?n?.said?`${await h(e,a)}: ${n.said}`:t["owner-step"]===void 0?`${await h(e,a)}: landed and done`:`${await h(e,a)}: landed; waits on the owner step "${t["owner-step"].trim()}"`:null,"land"),merge:r(async(e,[a],t,n)=>`${await h(e,a)}: ${n?.said??"merged"}`,"merge"),"arch file":r(async(e,[a])=>`findings of ${await h(e,a)} filed`,"arch file")};async function ge(e,a={}){try{const{flags:t,rest:n}=oe(e),i=[3,2,1].map(u=>n.slice(0,u).join(" ")).find(u=>y(f,u)),o=y(f,i);if(!o)throw new d(n.length?`unknown command "${n.slice(0,2).join(" ")}"`:"no command given");const[l,c,p]=o,m=n.slice(i.split(" ").length),N=l.at(-1)?.startsWith("..."),B=l.at(-1)?.endsWith("?")?l.length-1:l.length;if(N?m.length<B:m.length!==l.length)throw new d(`${i} takes ${l.map(u=>`<${u.replace(/^\.\.\.|\?$/g,"")}>`).join(" ")||"no arguments"}`);const{site:q,...E}=t;if(q?.length>1)throw new d("--site is given twice");const P=q?.[0],k=de(i,c,E);ue(c,k);const A=P===void 0?null:await s.siteDir(P),v=await p(A,m,k,a),j=y(me,i),O=j&&await j(A,m,k,v);O&&console.log(O);for(const u of typeof v=="object"&&v?.lines||[])console.log(u);return 0}catch(t){if(t instanceof d)return console.error(`${t.message} (node board.mjs --help lists the commands)`),2;if(t instanceof s.BoardError)return console.error(t.message),t.exitCode??1;throw t}}if(r(ge,"main"),process.argv[1]&&import.meta.url===W(g.realpathSync(process.argv[1])).href){const e=process.argv.slice(2);!e.length||e.includes("--help")||e[0]==="help"?console.log(re):process.exitCode=await ge(e)}export{re as USAGE,ge as main};
652
+ KICKFLIP_PUSH_CMD='["<cmd>","<arg>"]' replaces \`node ../../bin/kickflip.mjs push\`; the flags follow it`;class d extends Error{static{r(this,"UsageError")}}const oe=new Set(["done","clear-status","clear-next","urgent","now","clear-eta","held","unheld","idle","hub","clear-install","force","worktree","dry-run","dispatch","writes"]);function de(e){const a=Object.create(null),t=[];for(let n=0;n<e.length;n++){const i=e[n];if(!i.startsWith("--")){t.push(i);continue}const o=i.slice(2);if(oe.has(o)){if(Object.hasOwn(a,o))throw new d(`--${o} is given twice`);a[o]=!0}else if(n+1<e.length)(a[o]||=[]).push(e[++n]);else throw new d(`--${o} needs a value`)}return{flags:a,rest:t}}r(de,"parse");function le(e,a,t){const n=new Set(a.filter(l=>l.startsWith("...")).map(l=>l.slice(3))),i=new Set(a.map(l=>l.replace(/^\.\.\./,""))),o=Object.keys(t).filter(l=>!i.has(l));if(o.length)throw new d(`${e} does not take ${o.map(l=>"--"+l).join(", ")}`);return Object.fromEntries(Object.entries(t).map(([l,c])=>{if(c===!0||n.has(l))return[l,c];if(c.length>1)throw new d(`--${l} is given twice`);return[l,c[0]]}))}r(le,"flagsOf");const he=["html","text","note","notes","heading","summary","title","body","next","progress","install-label","confirmed","asked","task","reason"],ce=/^(?:\/|~\/|\.{1,2}\/)\S*\.(?:html?|md|json|txt)$/;function ue(e){const a=e.trim();if(!a||/\s/.test(a))return!1;if(ce.test(a))return!0;try{return f.statSync(a).isFile()}catch{return!1}}r(ue,"looksLikePath");function pe(e,a){for(const t of he){if(typeof a[t]!="string"||!ue(a[t]))continue;const n=e.includes("file")?`; for a file use --file ${a[t].trim()}`:"";throw new d(`--${t} takes text, not a file path (${a[t].trim()})${n}`)}if(typeof a.file=="string"&&a.file!=="-"&&!f.existsSync(a.file))throw new d(`--file ${a.file}: no such file`)}r(pe,"checkInputs");const w=r(e=>f.readFileSync(e==="-"?0:e,"utf8"),"readInput");function R(e){if(!e)throw new d("--file <path> is required");try{return JSON.parse(w(e))}catch(a){throw new s.BoardError(`${e}: ${a.message}`)}}r(R,"readJSONInput");function S(e,a){const t=["notes-file","plan"].find(n=>e[n]!==void 0);if(t)throw new d(`--${t} is gone: the board names a card's notes file, <id>-notes.md, and writes it itself; ${a}`)}r(S,"refuseNotesFile");function C(e){if(e.note!==void 0&&e.file!==void 0)throw new d("pass --note or --file, not both");return e.file===void 0?e.note:w(e.file)}r(C,"noteOf");async function we(e,a,{"owner-step":t,check:n,text:i,...o}){if(t!==void 0&&(!e||o.dir))throw new d("--owner-step goes on a board card: pass --site, not --dir");if(n!==void 0&&t===void 0)throw new d("--check goes with --owner-step: it is the code check that closes the card once the step is done");t!==void 0&&await s.setItem(e,a,{ownerStep:t,check:n});const l=e&&!o.dir,c=l&&await s.handBackCalls(e,a,o);if(c)return c;if(l&&o.next===void 0&&await s.finishesCard(e,a,o)){if(i===void 0)throw new s.BoardError(`builder done finishes ${a}: pass --text "<what you checked>", or --next "<what happens next>" to stop without finishing`);return $(e,a,{text:i,ownerStep:t})}if(l&&await s.isHelper(e,a,o.agent)){if(i===void 0)throw new d('a helper hands back its result: pass --text "<what you did or found>"');return s.stopBuilder(e,a,{...o,note:i})}if(i!==void 0)throw new d("--text goes on a builder done that finishes the card, the main build with a worktree and no --next, or on a helper's");return s.stopBuilder(e,a,{...o,requireNext:!0})}r(we,"builderDone");const b={pr:[["wave/id"],[],async(e,[a])=>({lines:(await s.prReport(e,a)).map(t=>JSON.stringify(t))})],"pr comment":[["pr"],["body"],(e,[a],t)=>{if(t.body===void 0)throw new d('pr comment needs --body "<what Claude says>"');return s.postPR(e,a,{body:t.body})}],"pr reply":[["pr","comment-id"],["body"],(e,[a,t],n)=>{if(n.body===void 0)throw new d('pr reply needs --body "<what Claude says>"');return s.postPR(e,a,{replyTo:t,body:n.body})}],reply:[["eventId"],["html","file"],(e,[a],t)=>{if(t.html===void 0==(t.file===void 0))throw new d("reply takes --html or --file");return s.writeReply(e,a,t.html??w(t.file))}],thread:[["wave/id"],["text","link","who","via"],(e,[a],t)=>s.appendThread(e,a,t)],"visual add":[["wave/id"],["file","title"],(e,[a],t)=>s.addVisual(e,a,t)],"topic add":[["id"],["title","summary"],(e,[a],t)=>s.addTopic(e,{id:a,...t})],"question add":[[],["file"],async(e,a,t)=>{const n=R(t.file);return{...await s.addQuestions(e,n,process.env.KICKFLIP_RENDER_CHECK==="off"?void 0:ne),asked:[n].flat()}}],"question resolve":[["id"],["note"],(e,[a],t)=>s.resolveQuestion(e,a,t.note)],"question move":[["ids"],["to","block"],(e,[a],t)=>s.moveQuestions(e,a.split(",").filter(Boolean),t.to,(t.block||"").split(",").filter(Boolean))],"prototype add":[["topic"],["title","...take","card","served","...state"],(e,[a],t)=>s.addPrototype(e,{topic:a,title:t.title,card:t.card,takes:t.take,served:t.served,states:t.state})],"prototype base":[[],[],e=>re(e)],"prototype list":[[],[],e=>s.listPrototypes(e)],"prototype rm":[["topic"],[],(e,[a])=>s.removePrototype(e,a)],"item set":[["wave/id"],["done","status","clear-status","next","clear-next","...pr","pushed","gh-since","from-ask","state","held","unheld","pick-up-at","notes-file","plan","deps","blocked-until","block-reason","parent","related","relate","unrelate","install","install-sha","install-label","clear-install","title","size","model","force","ticket","text","check"],async(e,[a],t,n)=>(S(t,`add to its notes with item notes ${a} --heading "<heading>" --file <draft.md>`),t.done&&e&&await s.checkNotReopened(e,a),s.setItem(e,a,{done:t.done,status:t.status,clearStatus:t["clear-status"],next:t.next,clearNext:t["clear-next"],pr:t.pr,pushed:t.pushed,ghSince:t["gh-since"],fromAsk:t["from-ask"],state:t.state,held:t.held,unheld:t.unheld,pickUpAt:t["pick-up-at"],deps:t.deps,blockedUntil:t["blocked-until"],blockReason:t["block-reason"],parent:t.parent,related:t.related,relate:t.relate,unrelate:t.unrelate,install:t.install,installSha:t["install-sha"],installLabel:t["install-label"],clearInstall:t["clear-install"],title:t.title,size:t.size,model:t.model,force:t.force,ticket:t.ticket,text:t.text,check:t.check,stopRun:r(()=>te(e,a),"stopRun")},n))],"item add":[["wave[/id]"],["title","notes","notes-from","deps","notes-file","plan","ticket","pick-up-at","parent","size","model","state","relate"],(e,[a],t,n)=>{if(S(t,"seed a new card's notes with item add --notes-from <draft.md>"),t.notes!==void 0&&t["notes-from"]!==void 0)throw new d("pass --notes or --notes-from, not both");return s.addItem(e,a,{title:t.title,notes:t.notes,notesFrom:t["notes-from"],deps:t.deps,ticket:t.ticket,pickUpAt:t["pick-up-at"],parent:t.parent,size:t.size,model:t.model,state:t.state,relate:t.relate,intake:!0},n)}],"item notes":[["wave/id"],["heading","file","from"],(e,[a],t)=>{if(t.heading===void 0&&t.from===void 0)throw new d(`item notes needs --heading "<the section's title>"`);if(t.file===void 0==(t.from===void 0))throw new d("item notes takes --file <draft.md> or --from <wave>/<id>");return s.addNotes(e,a,t)}],"item relate-ask":[["eventId","wave/id"],["summary"],(e,[a,t],n)=>s.relateAsk(e,a,t,n.summary)],"ask progress":[["eventId"],["next"],(e,[a],t)=>s.markAsk(e,a,"progress",t.next)],"ask done":[["eventId"],[],(e,[a])=>s.markAsk(e,a,"done")],"intake handoff":[["eventId"],["text"],(e,[a],t)=>s.intakeHandoff(e,a,t.text)],"ask unfold":[["eventId"],[],(e,[a])=>s.unfoldAsk(e,a)],"ask move":[["eventId"],["to"],(e,[a],t)=>s.moveAsk(e,a,t.to)],"tickets backfill":[[],["dry-run"],(e,a,t,n)=>s.backfillTickets(e,{dryRun:t["dry-run"]},n)],"lineup create":[["slug"],["title","goal","...phase","note","file"],(e,[a],t)=>s.createLineup(e,a,{title:t.title,goal:t.goal,phases:t.phase,note:C(t)})],"lineup add":[["slug","wave/id"],["after"],(e,[a,t],n)=>s.changeLineup(e,a,{add:t,after:n.after})],"lineup remove":[["slug","wave/id"],[],(e,[a,t])=>s.changeLineup(e,a,{remove:t})],"lineup set":[["slug"],["title","goal","note","file","...phase"],(e,[a],t)=>s.changeLineup(e,a,{title:t.title,goal:t.goal,note:C(t),phases:t.phase})],"lineup finish":[["slug"],[],(e,[a])=>s.finishLineup(e,a)],slack:[[],[],()=>({lines:[JSON.stringify(D())]})],"slack set":[[],["self-dm","user","domain","timezone"],async(e,a,t)=>{const n={"self-dm":"self_dm",user:"owner_slack_user",domain:"slack_domain",timezone:"timezone"};try{return{lines:[JSON.stringify(await M(Object.fromEntries(Object.entries(t).map(([i,o])=>[n[i],o]))))]}}catch(i){throw new s.BoardError(i.message)}}],integrations:[[],[],async()=>({lines:[JSON.stringify(await H.rows())]})],"review updated":[["wave/id"],["text"],(e,[a],t)=>s.reviewUpdated(e,a,t.text)],"review record":[["pr"],["state","text","file"],(e,[a],t)=>{if(t.text===void 0==(t.file===void 0))throw new d("review record takes --text or --file");return s.recordReview(e,a,{state:t.state,text:t.text??w(t.file)})}],"review post":[["pr"],["file","read-at"],(e,[a],t)=>{if(t.file===void 0)throw new d("review post needs --file <review.md>");if(t["read-at"]===void 0)throw new d("review post needs --read-at <time>, when you read the PR, so a review that landed since is caught");return s.postIncomingReview(e,a,{words:w(t.file),readAt:t["read-at"]})}],"review digest":[[],[],async(e,a,t,n)=>({lines:[JSON.stringify(await s.reviewDigest(e,n))]})],"review due":[[],[],async(e,a,t,n)=>({lines:[JSON.stringify(await s.reviewDue(e,n))]})],"reviewers set":[[],["file"],(e,a,t)=>s.setReviewers(e,R(t.file))],"reviewers add":[[],["file"],(e,a,t)=>s.addReviewers(e,R(t.file))],"reviewers remove":[["...name"],[],(e,a)=>s.removeReviewers(e,a)],"rules list":[[],[],e=>s.listRules(e)],"rules add":[[],["text","hub","by"],(e,a,t)=>s.addRuleTo(e,t)],"rules edit":[["id"],["text","hub"],(e,[a],t)=>s.editRuleIn(e,a,t)],"rules remove":[["id"],["hub"],(e,[a],t)=>s.removeRuleFrom(e,a,t)],settings:[[],[],async e=>({lines:[JSON.stringify(await s.readBoardSettings(e))]})],build:[["wave/id"],["asked"],async(e,[a],t)=>{if(!e)throw new s.BoardError("build needs --site <route>");s.parseKey(a);const n=await s.askedGate(e,a,t.asked);try{const i=await fetch(`http://127.0.0.1:${process.env.KICKFLIP_HUB_PORT||4500}/_hub/build`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({route:x.basename(e),key:a}),signal:AbortSignal.timeout(1e4)}),o=await i.json();if(!i.ok)throw new s.BoardError(o.error||"The hub refused the build.");return{lines:["started",...(await s.appendThread(e,a,{text:n})).lines]}}catch(i){throw i instanceof s.BoardError?i:new s.BoardError(`Could not start the build: ${i.message}`)}}],"main set":[[],["install","install-sha"],(e,a,t)=>s.setMainInstall(e,{install:t.install,installSha:t["install-sha"]})],"settings set":[["name","value"],[],(e,[a,t])=>s.setBoardSetting(e,a,t)],"builder start":[["wave/id"],["label","phase","progress","dir","agent","kind","model","eta","worktree","repo","dispatch"],(e,[a],{worktree:t,repo:n,dispatch:i,...o})=>{if(n!==void 0&&!t)throw new d("--repo goes with --worktree");const l=r(p=>z(e,a.split("/")[1],{repo:n,...p}),"make"),c=e&&o.dir===void 0&&se(e);return s.startBuilder(e,a,{...o,dispatch:i?"session":void 0,worktree:t?l:void 0,ownWorktree:c?l:void 0})}],"builder progress":[["wave/id","...text?"],["phase","text","dir","agent","eta","clear-eta"],(e,[a,...t],n)=>{if(t.length&&n.text!==void 0)throw new d("builder progress takes its text once: as words or as --text");if(!t.length&&n.text===void 0)throw new d("builder progress needs its text, as words or as --text");return s.builderProgress(e,a,n.text??t.join(" "),{...n,clearEta:n["clear-eta"]})}],"builder done":[["wave/id"],["dir","agent","next","owner-step","check","text","updated"],async(e,[a],{updated:t,...n})=>{const i=e&&!n.dir;if(i&&t===void 0&&await s.cardLinked(e,a))throw new s.BoardError(`a reviewer has ${a}'s PR link: pass --updated "<what changed>" for its thread, or --updated none for a change only to comments, names, docs or tests`);const o=await we(e,a,n);return i&&t!==void 0&&t.trim().toLowerCase()!=="none"&&await s.reviewUpdated(e,a,t),o}],"builder pick":[["wave/id"],[],(e,[a])=>s.pickBuilder(e,a)],"agent launch":[["wave/id"],["name","task","writes","model"],(e,[a],t)=>s.requestHelper(e,a,t)],"agent wait":[["wave/id"],["name","agent","timeout"],(e,[a],t)=>s.waitHelper(e,a,{...t,alive:Q})],"agent merge":[["wave/id"],["name"],async(e,[a],t)=>{const{record:n,card:i}=await s.helperOf(e,a,t.name);if(!n.done)throw new s.BoardError(`helper ${t.name} is still open; run agent wait ${a} --name ${t.name} first`);if(!n.writes||!n.worktree)throw new s.BoardError(`helper ${t.name} writes no code, so there is nothing to merge`);if(!i)throw new s.BoardError(`${a} has no worktree to merge helper ${t.name} into`);return G(i,n.worktree)}],audit:[["...wave/id?"],[],(e,[a,...t])=>{if(t.length)throw new d("audit takes one <wave/id> at most");return s.auditLog(e,a)}],"builder call":[["wave/id","...text"],["dir","agent"],(e,[a,...t],n)=>s.recordCall(e,a,t.join(" "),n)],"builder calls":[["wave/id"],["dir","agent"],(e,[a],t)=>s.listCalls(e,a,t)],"builder ask":[["wave/id","...text"],["agent"],(e,[a,...t],n)=>s.askOrchestrator(e,a,t.join(" "),n)],"builder answer":[["wave/id","ask"],["text"],(e,[a,t],n)=>s.answerBuilder(e,a,t,n.text)],"builder defer":[["wave/id","ask"],["text"],(e,[a,t],n)=>s.answerBuilder(e,a,t,n.text,{deferred:!0})],"builder asks":[["wave/id"],[],(e,[a])=>s.listAsks(e,a)],"builder verified":[["wave/id"],["dir","agent","note"],async(e,[a],t)=>{await s.verifyCalls(e,a,t);const n=e&&!t.dir?await s.verifiedFinish(e,a):null;if(!n)return{};if(n.waits.length)return{lines:[`${await s.cardName(e,a)} finishes once the owner answers ${n.waits.join(", ")}`]};try{return await $(e,a,{text:t.note??"Judgment calls verified."})}catch(i){throw i instanceof s.BoardError?Object.assign(new s.BoardError(`judgment calls verified, but ${await s.cardName(e,a)} didn't finish: ${i.message}`),{exitCode:i.exitCode}):i}}],land:[["wave/id"],["repo","dir","text","owner-step","check"],async(e,[a],t)=>{const n=a.split("/")[1];if(e&&t.text===void 0)throw new d(`land needs --text "<what was checked>", which closes ${a} as done once it lands`);if(!e&&t.text!==void 0)throw new d("--text closes the card, which needs --site");if(!e&&t["owner-step"]!==void 0)throw new d("--owner-step keeps a board card open, which needs --site");if(t.check!==void 0&&t["owner-step"]===void 0)throw new d("--check goes with --owner-step: it is the code check that closes the card once the step is done");if(e&&t.dir===void 0)return $(e,a,{text:t.text,ownerStep:t["owner-step"],check:t.check,repo:t.repo});const i=e?await s.cardName(e,a):void 0,o=await F(i??a,async()=>{e&&await s.checkClose(e,a,t.text,t["owner-step"],t.check),await s.checkCallsVerified(e,a,{dir:t.dir});const l=await s.recordedWorktree(e,a,{dir:t.dir}),{repo:c,base:p}=U(e,{repo:t.repo??l?.repo});return await J({path:l?.path??x.join(K(),n),branch:l?.branch??n,repo:x.resolve(c),base:p,ref:i}),e?s.setItem(e,a,{done:!0,text:t.text,ownerStep:t["owner-step"],check:t.check}):{lines:[]}});return o.joined?{lines:[`joined the land already running for ${i??a}`]}:o}],"builder stop":[["wave/id"],[],(e,[a])=>ee(m(e,"builder stop"),a)],"item stall":[["wave/id"],["reason"],(e,[a],t)=>Z(m(e,"item stall"),a,t.reason)],"land stop":[["wave/id"],[],(e,[a])=>ae(m(e,"land stop"),a)],"land queue":[[],[],()=>Y()],"item bump":[["wave/id"],[],(e,[a])=>O(m(e,"item bump"),a,!0)],"item unbump":[["wave/id"],[],(e,[a])=>O(m(e,"item unbump"),a,!1)],lander:[["wave/id"],[],async(e,[a])=>{if(!e)throw new d("lander needs --site <route>");return await s.relaunchLander(e,a),{lines:[`${await s.cardName(e,a)} is handed back again; the server launches a lander for it within a minute`]}}],merge:[["wave/id"],["after-approval","confirmed"],async(e,[a],t)=>e&&await s.heldMerge(e,a)?X(e,a):s.mergeCard(e,a,{afterApproval:t["after-approval"],confirmed:t.confirmed})],"arch file":[["wave/id"],[],(e,[a])=>e?V(e,a):Promise.reject(new s.BoardError("arch file needs --site <route>"))],push:[[],["kind","title","body","url","tag","urgent","now"],(e,a,t)=>{if(t.kind!=="fault"&&t.kind!=="reminder")throw new d(`a session pushes only --kind fault or reminder${t.kind===void 0||t.kind===!0?"":`, not "${t.kind}"`}: the board server pushes Action needed, and news goes on the board`);if(t.title===void 0)throw new d("push needs --title");return s.sendPush(e,t)}]};b.install=b.build;function m(e,a){if(!e)throw new d(`${a} needs --site <route>`);return e}r(m,"needSite");const h=r((e,a)=>s.cardName(e,a),"card"),_=r((e,a)=>`${a}${e.length===1?"":"s"} ${e.join(", ")}`,"plural"),T=r(e=>e.agent===void 0||e.agent==="main"?"builder":`builder ${e.agent}`,"builderOf");function me(e){const a=e["pick-up-at"];return[e.done&&"done",e.next!==void 0&&(e.next.trim()?"next line set":"next line cleared"),e["clear-next"]&&"next line cleared",e.pr!==void 0&&`PR #${e.pr}`,e.pushed!==void 0&&`pushed ${e.pushed}`,e["gh-since"]&&`GitHub read to ${e["gh-since"]}`,e["from-ask"]&&`from task ${e["from-ask"]}`,e.state,e.held&&"held",e.unheld&&"resumed",a!==void 0&&(a?`held until ${a}`:"resumed"),e.deps!==void 0&&(e.deps?`blocked by ${e.deps}`:"deps cleared"),e.parent!==void 0&&(e.parent?`parent ${e.parent}`:"parent cleared"),e.related!==void 0&&(e.related?`related to ${e.related.split(",").join(", ")}`:"related cleared"),e.relate!==void 0&&`related to ${e.relate.split(",").join(", ")} added`,e.unrelate!==void 0&&`unrelated from ${e.unrelate.split(",").join(", ")}`,e.install!==void 0&&"install published",e["clear-install"]&&"install cleared",e.title!==void 0&&"renamed",e.ticket!==void 0&&`ticket ${e.ticket}`,e.check!==void 0&&(e.check.trim()?`check ${e.check.trim().split(/\s+/).join(" ")}`:"check removed")].filter(Boolean).join(", ")}r(me,"itemChanges");const ge={reply:r(async(e,[a])=>`reply written to ${await s.eventCard(e,a)}`,"reply"),thread:r(async(e,[a])=>`thread line on ${await h(e,a)}`,"thread"),"topic add":r((e,[a])=>`topic ${a} added`,"topic add"),"question add":r(async(e,a,t,{asked:n,staged:i})=>{const o=[...new Set(n.map(c=>c.topic))],l=o.length!==1?"":o[0].startsWith("item:")?` on ${await h(e,o[0].slice(5))}`:` on topic ${o[0]}`;return`${_(n.map(c=>c.id),"question")} ${i===n.length?"staged":"added"}${l}`},"question add"),"question resolve":r((e,[a])=>`question ${a} resolved`,"question resolve"),"question move":r((e,[a],t)=>`questions ${a} merged onto ${t.to}`,"question move"),"prototype add":r(async(e,[a],t)=>`prototype ${a} recorded${t.card?` for ${await h(e,t.card)}`:""}`,"prototype add"),"prototype rm":r((e,[a])=>`prototype ${a} removed`,"prototype rm"),"item set":r(async(e,[a],t)=>`${await h(e,a)}: ${me(t)}`,"item set"),"item add":r(async(e,a,t,{key:n})=>`${await h(e,n)} added as ${n}`,"item add"),"item notes":r(async(e,[a],t,{file:n,heading:i})=>`${await h(e,a)}: notes section "${i}" added to ${n}`,"item notes"),"item relate-ask":r(async(e,[a,t])=>`task ${a} folded into ${await h(e,t)}`,"item relate-ask"),"ask progress":r(async(e,[a])=>`${await h(e,`ask:${a}`)}: in progress`,"ask progress"),"ask done":r(async(e,[a])=>`${await h(e,`ask:${a}`)}: done`,"ask done"),"intake handoff":r(async(e,[a])=>`${await s.eventCard(e,a)} handed to the orchestrator`,"intake handoff"),"ask unfold":r((e,[a])=>`task ${a} back on a card of its own`,"ask unfold"),"ask move":r((e,[a],t)=>`task ${a} moved to /${t.to}`,"ask move"),"lineup create":r((e,[a])=>`lineup ${a} created`,"lineup create"),"lineup add":r(async(e,[a,t])=>`${await h(e,t)} added to lineup ${a}`,"lineup add"),"lineup remove":r(async(e,[a,t])=>`${await h(e,t)} taken out of lineup ${a}`,"lineup remove"),"lineup set":r((e,[a])=>`lineup ${a} updated`,"lineup set"),"lineup finish":r((e,[a])=>`lineup ${a} finished`,"lineup finish"),"review record":r((e,[a],t)=>`review of PR #${a} recorded: ${t.state==="needs"?"needs you":"posted"}`,"review record"),"reviewers set":r(()=>"reviewers replaced","reviewers set"),"reviewers add":r(()=>"reviewers added","reviewers add"),"reviewers remove":r((e,a)=>`${_(a,"reviewer")} removed`,"reviewers remove"),"rules add":r((e,a,t)=>`${t.hub?"hub":"board"} rule added`,"rules add"),"rules edit":r((e,[a],t)=>`${t.hub?"hub":"board"} rule ${a} reworded`,"rules edit"),"rules remove":r((e,[a],t)=>`${t.hub?"hub":"board"} rule ${a} removed`,"rules remove"),"builder start":r(async(e,[a],t,n)=>`${await h(e,a)}: ${n.alreadyOpen?"already open: the Implement tap opened its record; launch the agent, whose own builder start takes it up":`${T(t)} started`}`,"builder start"),"builder progress":r(async(e,[a])=>`progress line on ${await h(e,a)}`,"builder progress"),"builder done":r(async(e,[a],t,n)=>`${await h(e,a)}: ${n?.said??`${T(t)} done${t["owner-step"]===void 0?"":`; owner step "${t["owner-step"].trim()}"`}`}`,"builder done"),"builder call":r(async(e,[a])=>`judgment call recorded on ${await h(e,a)}`,"builder call"),"builder ask":r(async(e,[a],t,{id:n})=>`${await h(e,a)}: asked the orchestrator, ${n}`,"builder ask"),"builder answer":r(async(e,[a,t])=>`${await h(e,a)}: ${t} answered`,"builder answer"),"builder defer":r(async(e,[a,t])=>`${await h(e,a)}: ${t} deferred to the owner`,"builder defer"),"builder verified":r(async(e,[a],t,n)=>`${await h(e,a)}: judgment calls verified${n?.said?`; ${n.said}`:""}`,"builder verified"),land:r(async(e,[a],t,n)=>e?n?.said?`${await h(e,a)}: ${n.said}`:t["owner-step"]===void 0?`${await h(e,a)}: landed and done`:`${await h(e,a)}: landed; waits on the owner step "${t["owner-step"].trim()}"`:null,"land"),merge:r(async(e,[a],t,n)=>`${await h(e,a)}: ${n?.said??"merged"}`,"merge"),"arch file":r(async(e,[a])=>`findings of ${await h(e,a)} filed`,"arch file")};async function fe(e,a={}){try{const{flags:t,rest:n}=de(e),i=[3,2,1].map(u=>n.slice(0,u).join(" ")).find(u=>y(b,u)),o=y(b,i);if(!o)throw new d(n.length?`unknown command "${n.slice(0,2).join(" ")}"`:"no command given");const[l,c,p]=o,g=n.slice(i.split(" ").length),N=l.at(-1)?.startsWith("..."),B=l.at(-1)?.endsWith("?")?l.length-1:l.length;if(N?g.length<B:g.length!==l.length)throw new d(`${i} takes ${l.map(u=>`<${u.replace(/^\.\.\.|\?$/g,"")}>`).join(" ")||"no arguments"}`);const{site:I,...E}=t;if(I?.length>1)throw new d("--site is given twice");const q=I?.[0],k=le(i,c,E);pe(c,k);const A=q===void 0?null:await s.siteDir(q),v=await p(A,g,k,a),P=y(ge,i),j=P&&await P(A,g,k,v);j&&console.log(j);for(const u of typeof v=="object"&&v?.lines||[])console.log(u);return 0}catch(t){if(t instanceof d)return console.error(`${t.message} (node board.mjs --help lists the commands)`),2;if(t instanceof s.BoardError)return console.error(t.message),t.exitCode??1;throw t}}if(r(fe,"main"),process.argv[1]&&import.meta.url===W(f.realpathSync(process.argv[1])).href){const e=process.argv.slice(2);!e.length||e.includes("--help")||e[0]==="help"?console.log(ie):process.exitCode=await fe(e)}export{ie as USAGE,fe as main};