@lovek4d/kickflip 0.7.1 → 0.7.4

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 (62) hide show
  1. package/CONTEXT.md +25 -21
  2. package/SKILL.md +13 -9
  3. package/bin/event-lines.mjs +1 -1
  4. package/bin/install.mjs +3 -3
  5. package/bin/kickflip.mjs +1 -1
  6. package/bin/wait-events.mjs +2 -2
  7. package/board/bin/board.mjs +68 -50
  8. package/board/domain.mjs +7 -7
  9. package/board/finish.mjs +4 -4
  10. package/board/headless.mjs +8 -8
  11. package/board/hub-lines.mjs +1 -1
  12. package/board/onboarding.mjs +3 -3
  13. package/board/permissions.mjs +2 -2
  14. package/board/public/app-asks.js +2 -14
  15. package/board/public/app-banner.js +8 -3
  16. package/board/public/app-cards.js +7 -20
  17. package/board/public/app-core.js +2 -2
  18. package/board/public/app-lanes.js +7 -15
  19. package/board/public/app-reader.js +7 -3
  20. package/board/public/app-snapshot.js +1 -1
  21. package/board/public/app.js +1 -1
  22. package/board/public/attach.js +18 -2
  23. package/board/public/dialogs.js +10 -12
  24. package/board/public/hub.css +0 -3
  25. package/board/public/linear-key.js +1 -1
  26. package/board/public/sidebar.css +1 -1
  27. package/board/public/style.css +42 -36
  28. package/board/public/ui-cards.js +33 -27
  29. package/board/public/ui-enums.js +2 -2
  30. package/board/public/ui-links.js +0 -1
  31. package/board/public/ui-order.js +1 -1
  32. package/board/public/ui-questions.js +8 -6
  33. package/board/public/ui-sidebar.js +1 -1
  34. package/board/public/ui-threads.js +26 -22
  35. package/board/public/ui.js +15 -9
  36. package/board/public/view-hub-settings.js +25 -30
  37. package/board/public/view-hub.js +23 -5
  38. package/board/public/view-questions.js +1 -1
  39. package/board/public/view-settings.js +4 -4
  40. package/board/server.mjs +18 -18
  41. package/board/store.mjs +30 -27
  42. package/board/tickets.mjs +3 -3
  43. package/board/worktree.mjs +15 -15
  44. package/hub/board-repo.mjs +1 -1
  45. package/hub/github.mjs +3 -3
  46. package/hub/hub.mjs +11 -11
  47. package/hub/linear.mjs +1 -1
  48. package/hub/new-board.mjs +2 -2
  49. package/hub/pr-report.mjs +2 -2
  50. package/hub/readiness.mjs +1 -1
  51. package/hub/repos.mjs +3 -3
  52. package/hub/review-send.mjs +1 -1
  53. package/hub/schedule.mjs +1 -1
  54. package/hub/slack.mjs +1 -1
  55. package/hub/try.mjs +8 -6
  56. package/hub/views.mjs +1 -1
  57. package/package.json +1 -1
  58. package/shared/fake-gh.mjs +19 -4
  59. package/shared/gh.mjs +4 -0
  60. package/shared/owner-check.mjs +4 -3
  61. package/shared/service.mjs +12 -12
  62. package/starter/server.mjs +3 -3
@@ -1 +1 @@
1
- var r=Object.defineProperty;var t=(e,a)=>r(e,"name",{value:a,configurable:!0});import{waitsAgainLine as d}from"../board/domain.mjs";const i=["main-pulled","settings","ask-done","reviewers-removed","reviewers-added","question-reopen","repo-changed","ping","hello","card-hold","card-done","card-unbump","card-unblocked","grill-more","prototype-request","visual-request","usage-resumed"],n=t(e=>{try{return JSON.parse(e).kind}catch{return null}},"kindOf"),h=t(e=>i.includes(n(e)),"recordOnly"),l={ask:t(e=>`new task ${e.id} received: sort it: file it with item add --from-ask, fold it with item relate-ask, or answer it with reply`,"ask"),"owner-answer":t(e=>`answer received to question ${e.data?.id}: act on it on its card`,"owner-answer"),ping:t(e=>`ping ${e.id} from the hub answered`,"ping"),hello:t(e=>`ping ${e.id} from the hub answered`,"hello"),"overnight-triage":t(e=>`Overnight triage ${e.data?.date} asks Claude to reply: ${e.data?.cards?.length} cards wait on open questions; run the triage, then board.mjs reply ${e.id}`,"overnight-triage"),"pr-review":t(e=>`review asked on PR #${e.data?.pr}: the board was idle; read its diff and review post it`,"pr-review"),"orchestrator-waits":t(e=>[e.data?.added?.length&&`waiting on you now: ${e.data.added.join(", ")}`,e.data?.again?.length&&d(e.data.again,Date.parse(e.ts))].filter(Boolean).join(" \xB7 "),"orchestrator-waits"),"check-github":t(e=>e.data?.reason==="morning"?"the daily wake: run one GitHub tick, then walk every lane top-down once: clear each blocked-until condition that holds, and act on any card stuck with no wait; a thread line on each card you act on, nothing else":"GitHub check asked: run one GitHub tick","check-github"),"github-change":t(e=>`${e.data?.prs?.length??0} PRs changed on GitHub: run one GitHub tick`,"github-change"),"review-answer":t(e=>e.data?.answer==="skip"?`skip answered on the review of PR #${e.data?.pr}: record it posted, post nothing`:`post answered on the review of PR #${e.data?.pr}: review post it`,"review-answer"),"plan-waves":t(e=>`Plan waves asked on ${e.data?.keys?.length??0} Todo cards: triage them into waves and ask the owner about them`,"plan-waves"),"arch-review":t(e=>`architecture review asked on ${e.data?.key}: review ${e.data?.repo} on ${e.data?.branch} and file the findings`,"arch-review"),rules:t(e=>`${e.data?.scope==="hub"?"hub":"board"} rules changed: the rule lines below are the new ones; follow them from now`,"rules"),"prototype-overdue":t(e=>`prototype overdue on ${e.data?.key}: dispatch its prototype agent now`,"prototype-overdue")},u={"item-message":t((e,a)=>String(a.data?.key).startsWith("ask:")&&a.data?.intent==="build"?`Implement asked on ${e}: file it with item add --from-ask --phase ready, with its --size and notes`:a.data?.intent==="build"?`Implement asked on ${e}: it is staging, the message in its notes; judge it against the whole board, then item set --phase ready --text "<what you checked>", or --phase grilling or prototyping`:`message received from ${e}'s thread: reply in its thread`,"item-message"),"card-bump":t(e=>`${e} bumped: handle its waits lines first; with none, leave it`,"card-bump"),"ask-bump":t(e=>`${e} bumped: take that ask first`,"ask-bump"),"ask-edited":t(e=>`${e} edited: use the new text, and say what changed if you had started`,"ask-edited"),"ask-done":t(e=>`${e} marked done`,"ask-done"),"ask-withdrawn":t(e=>`${e} deleted: drop it`,"ask-withdrawn"),"card-done":t(e=>`${e} marked done`,"card-done"),"card-hold":t((e,a)=>`${e} ${a.data?.hold?"held":"resumed"}`,"card-hold"),"card-unblocked":t(e=>`${e} unblocked`,"card-unblocked"),"prototype-request":t(e=>`prototype asked on ${e}`,"prototype-request"),"visual-request":t(e=>`visual asked on ${e}: the board launches its visual agent`,"visual-request"),"reask-request":t(e=>`question asked again on ${e}: ask it again with question add reask_of`,"reask-request"),"builder-resume":t((e,a)=>`${e}'s pause cleared (${a.data?.cleared}): tell its builder in your session to carry on where it stopped`,"builder-resume"),"builder-ask":t((e,a)=>`${a.data?.ask} from ${e}'s builder: ${a.data?.text} (builder answer it, or builder defer it)`,"builder-ask"),"owner-check-broken":t((e,a)=>`${e}'s owner-step check can't run: ${a.data?.error}; fix it with item set --check, or drop it`,"owner-check-broken"),"card-added":t(e=>`${e} added by an agent: link it and file it: item notes, then item set --relate and --phase`,"card-added")};export{u as ON_CARD,l as PLAIN,i as RECORD_ONLY,h as recordOnly};
1
+ var r=Object.defineProperty;var t=(e,a)=>r(e,"name",{value:a,configurable:!0});import{waitsAgainLine as i}from"../board/domain.mjs";const n=["main-pulled","settings","reviewers-removed","reviewers-added","question-reopen","repo-changed","ping","hello","card-hold","card-done","card-unbump","card-unblocked","grill-more","prototype-request","visual-request","usage-resumed"],d=t(e=>{try{return JSON.parse(e).kind}catch{return null}},"kindOf"),h=t(e=>n.includes(d(e)),"recordOnly"),l={"owner-answer":t(e=>`answer received to question ${e.data?.id}: act on it on its card`,"owner-answer"),ping:t(e=>`ping ${e.id} from the hub answered`,"ping"),hello:t(e=>`ping ${e.id} from the hub answered`,"hello"),"overnight-triage":t(e=>`Overnight triage ${e.data?.date} asks Claude to reply: ${e.data?.cards?.length} cards wait on open questions; run the triage, then board.mjs reply ${e.id}`,"overnight-triage"),"pr-review":t(e=>`review asked on PR #${e.data?.pr}: the board was idle; read its diff and review post it`,"pr-review"),"orchestrator-waits":t(e=>[e.data?.added?.length&&`waiting on you now: ${e.data.added.join(", ")}`,e.data?.again?.length&&i(e.data.again,Date.parse(e.ts))].filter(Boolean).join(" \xB7 "),"orchestrator-waits"),"check-github":t(e=>e.data?.reason==="morning"?"the daily wake: run one GitHub tick, then walk every lane top-down once: clear each blocked-until condition that holds, and act on any card stuck with no wait; a thread line on each card you act on, nothing else":"GitHub check asked: run one GitHub tick","check-github"),"github-change":t(e=>`${e.data?.prs?.length??0} PRs changed on GitHub: run one GitHub tick`,"github-change"),"review-answer":t(e=>e.data?.answer==="skip"?`skip answered on the review of PR #${e.data?.pr}: record it posted, post nothing`:`post answered on the review of PR #${e.data?.pr}: review post it`,"review-answer"),"plan-waves":t(e=>`Plan waves asked on ${e.data?.keys?.length??0} Todo cards: triage them into waves and ask the owner about them`,"plan-waves"),"arch-review":t(e=>`architecture review asked on ${e.data?.key}: review ${e.data?.repo} on ${e.data?.branch} and file the findings`,"arch-review"),rules:t(e=>`${e.data?.scope==="hub"?"hub":"board"} rules changed: the rule lines below are the new ones; follow them from now`,"rules"),"prototype-overdue":t(e=>`prototype overdue on ${e.data?.key}: dispatch its prototype agent now`,"prototype-overdue")},u={"item-message":t((e,a)=>String(a.data?.key).startsWith("ask:")&&a.data?.intent==="build"?`Implement asked on ${e}: file it with item add --from-ask --phase ready, with its --size and notes`:a.data?.intent==="build"?`Implement asked on ${e}: it is staging, the message in its notes; judge it against the whole board, then item set --phase ready --text "<what you checked>", or --phase grilling or prototyping`:`message received from ${e}'s thread: reply in its thread`,"item-message"),"card-bump":t(e=>`${e} bumped: handle its waits lines first; with none, leave it`,"card-bump"),"ask-edited":t(e=>`${e} edited: use the new text, and say what changed if you had started`,"ask-edited"),"ask-withdrawn":t(e=>`${e} deleted: drop it`,"ask-withdrawn"),"card-done":t(e=>`${e} marked done`,"card-done"),"card-hold":t((e,a)=>`${e} ${a.data?.hold?"held":"resumed"}`,"card-hold"),"card-unblocked":t(e=>`${e} unblocked`,"card-unblocked"),"prototype-request":t(e=>`prototype asked on ${e}`,"prototype-request"),"visual-request":t(e=>`visual asked on ${e}: the board launches its visual agent`,"visual-request"),"reask-request":t(e=>`question asked again on ${e}: ask it again with question add reask_of`,"reask-request"),"builder-resume":t((e,a)=>`${e}'s pause cleared (${a.data?.cleared}): tell its builder in your session to carry on where it stopped`,"builder-resume"),"builder-ask":t((e,a)=>`${a.data?.ask} from ${e}'s builder: ${a.data?.text} (builder answer it, or builder defer it)`,"builder-ask"),"owner-check-broken":t((e,a)=>`${e}'s owner-step check can't run: ${a.data?.error}; fix it with item set --check, or drop it`,"owner-check-broken"),"card-added":t(e=>`${e} added by an agent: link it and file it: item notes, then item set --relate and --phase`,"card-added"),ask:t((e,a)=>`new task ${a.id} received as ${e}: sort that New card: item notes, then item set <its key> --phase grilling or prototyping (ready, with --size, for a Quick build); merge it into a card with item relate-ask ${a.id} <card> --summary; or answer it with reply <its key>`,"ask")};export{u as ON_CARD,l as PLAIN,n as RECORD_ONLY,h as recordOnly};
package/bin/install.mjs CHANGED
@@ -1,3 +1,3 @@
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
+ var J=Object.defineProperty;var y=(e,s)=>J(e,"name",{value:s,configurable:!0});import r from"node:fs";import v from"node:os";import n from"node:path";import{SKILLS_SOURCE as I,addArgv as D,addCommand as M,missingSkills as K}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 z="/opt/homebrew/bin/node",P=y((e=process.execPath,s=z)=>e.startsWith("/opt/homebrew/Cellar/")&&r.existsSync(s)?s:e,"nodeBin"),ct=y((e,s,t=O())=>`${P()} ${n.join(t,"bin","wait-events.mjs")} ${e} ${s}`,"watcherCommand");function G(e,{home:s=v.homedir(),skillsDir:t=n.join(s,".claude","skills"),cloneRoot:c=O()}={}){if(typeof e!="string"||!n.isAbsolute(e))return e;const o=n.relative(c,e);if(o===".."||o.startsWith(`..${n.sep}`)||n.isAbsolute(o))return e;const a=n.resolve(t,"kickflip");let i=!1;try{i=r.realpathSync(a)===r.realpathSync(c)}catch{}return i?n.join(a,o):e}y(G,"storedPath");function Q({home:e=v.homedir(),skillsDir:s=n.join(e,".claude","skills"),cloneRoot:t=O()}={}){const c=r.realpathSync(t),o=[["kickflip",c],["kickflip-hooks",n.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:n.resolve(s,"kickflip","hooks-plugin"),h=n.resolve(s,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=n.join(c,"bin","statusline.mjs");if(!r.statSync(a,{throwIfNoEntry:!1})?.isFile())throw new Error(`not a file: ${a}`);const i=n.join(e,".claude","statusline.sh"),S=n.resolve(s,"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=n.join(e,".claude","kickflip","sites");let f=p;try{f=r.realpathSync(p)}catch{}const h=!!u&&[p,f].some(m=>u.startsWith(m+n.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||n.resolve(n.dirname(i),r.readlinkSync(i))!==S||!u}return o.push({link:i,target:S,created:!l,...w?{replaced:!0}:{},...k?{refusal:k}:{}}),o}y(Q,"inspectLinks");function R({home:e=v.homedir(),skillsDir:s=n.join(e,".claude","skills"),cloneRoot:t=O(),dryRun:c=!1}={}){const o=Q({home:e,skillsDir:s,cloneRoot:t}),a=o.find(i=>i.refusal);if(a)throw new Error(a.refusal);if(c)return o;r.mkdirSync(s,{recursive:!0}),r.mkdirSync(n.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===n.join(e,".claude","statusline.sh")?"file":"dir");return o}y(R,"installLinks");function V(e,s=v.homedir()){const t=n.join(s,".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(V,"ownsStatusLine");const U='node "$HOME/.claude/skills/kickflip/hooks-plugin/hooks/watcher-stop.mjs"',X=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 Y({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 c=await t.json();return Array.isArray(c?.sites)?c:null}catch{return null}}y(Y,"hubStatus");function W(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(W,"tailscaleRoot");function lt(e,{run:s=_,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=s(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(lt,"tailnetUrl");async function ut({skipped:e=[],run:s=_,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 s(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=W(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(ut,"tailscaleState");async function dt({home:e=v.homedir(),cloneRoot:s=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=n.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:F}of R({home:e,cloneRoot:s,dryRun:k}))u(`would ${A||F?"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 ${n.join(e,".claude","settings.json")}`);const d=K(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 R({home:e,cloneRoot:s}))u(`${T||A?"linked":"checked"} ${d} -> ${x}`);for(const d of Z({home:e}))u(d);const h=K(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=P(),j=G(n.join(s,"hub","hub.mjs"),{home:e,cloneRoot:s});r.mkdirSync(p,{recursive:!0});const g=n.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 Y({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(n.join(p,"manifest.json"),"utf8")).sites||{}}catch(d){if(d.code!=="ENOENT")throw d}for(const d of W(L).paths)d!=="/"&&Object.hasOwn($,d.replace(/^\//,"").replace(/\/$/,""))&&a(S,["serve","--https=443","--set-path="+d,"off"],{env:o})}y(dt,"runInstall");function Z({home:e=v.homedir()}={}){const s=n.join(e,".claude","settings.json");let t={},c=384;try{t=JSON.parse(r.readFileSync(s,"utf8")),c=r.statSync(s).mode}catch(l){if(l.code!=="ENOENT")throw l}if(!t||typeof t!="object"||Array.isArray(t))throw new Error(`not a settings object: ${s}`);const o=[];let a=!1;const i=n.join(e,".claude","statusline.sh");if(Object.hasOwn(t,"statusLine"))V(t.statusLine,e)||o.push(`kept existing statusLine in ${s}; 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 ${s} -> ${l}`)}if(!X(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:U}]}]},a=!0,o.push(`added the watcher Stop hook to ${s} -> ${U}`)}t.autoCompactEnabled!==!0&&(t.autoCompactEnabled=!0,a=!0,o.push(`turned on autoCompactEnabled in ${s}`));const S=n.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(n.dirname(s),{recursive:!0});const l=`${s}.${process.pid}.${Date.now()}.tmp`;try{r.writeFileSync(l,JSON.stringify(t,null,2)+`
3
+ `,{mode:c,flag:"wx"}),r.renameSync(l,s)}finally{try{r.unlinkSync(l)}catch{}}}return o}y(Z,"installClaudeSettings");function pt({home:e=v.homedir(),skillsDir:s=n.join(e,".claude","skills"),cloneRoot:t=O()}={}){const c=n.join(e,".claude","statusline.sh");if(!r.lstatSync(c,{throwIfNoEntry:!1})?.isSymbolicLink())return!1;let a=n.resolve(n.dirname(c),r.readlinkSync(c))===n.resolve(s,"kickflip","bin","statusline.mjs");try{a||=r.realpathSync(c)===r.realpathSync(n.join(t,"bin","statusline.mjs"))}catch{}return a?(r.unlinkSync(c),!0):!1}y(pt,"uninstallStatusLine");export{U as WATCHER_STOP,X as hasWatcherStop,Y as hubStatus,Q as inspectLinks,Z as installClaudeSettings,R as installLinks,P as nodeBin,V as ownsStatusLine,dt as runInstall,G as storedPath,lt as tailnetUrl,W as tailscaleRoot,ut as tailscaleState,pt as uninstallStatusLine,ct as watcherCommand};
package/bin/kickflip.mjs CHANGED
@@ -162,7 +162,7 @@
162
162
  // Exit codes: 0 ok, 1 error, 2 bad usage, 3 route owned by another session (join/leave), or its recorded session is
163
163
  // live (start/resume/hub). hub passes through claude's exit code.
164
164
  var Ne=Object.defineProperty;var p=(e,t)=>Ne(e,"name",{value:t,configurable:!0});import w from"node:fs";import u from"node:path";import D from"node:os";import re from"node:net";import Te from"node:readline/promises";import{execFileSync as ae,spawnSync as K}from"node:child_process";import{fileURLToPath as Ie}from"node:url";import{ICON_KEYS as ce}from"../hub/icons.mjs";import{liveSessions as G}from"../hub/live.mjs";import{isBoardSite as L,writeManifest as v}from"../hub/boards.mjs";import{joinName as Fe,routeFromName as F,routeRecord as g,validRoute as C}from"../hub/routes.mjs";import{defaultBranch as Ce,githubFromOrigin as Ee}from"../hub/repos.mjs";import{hasBuildScript as Re,onboardingItems as Be}from"../board/onboarding.mjs";import{uninstallStatusLine as Ae,storedPath as De,nodeBin as U,watcherCommand as Le,tailnetUrl as Pe}from"./install.mjs";import{hubService as de}from"../shared/service.mjs";import{tailscaleBin as ue}from"../shared/tailscale.mjs";import{runDoctor as He}from"./doctor.mjs";import{cloneRoot as Je}from"../shared/repos.mjs";const y=Je(),N=u.join(D.homedir(),".claude","kickflip"),ze=u.join(N,"manifest.json"),le=u.join(N,"logs"),T=Number(process.env.KICKFLIP_HUB_PORT||4500),W=4600,qe=/^#([0-9a-f]{3}|[0-9a-f]{6})$/i,[Y,...j]=process.argv.slice(2),s=Object.assign(Object.create(null),{_:[]});for(let e=0;e<j.length;e++){const t=j[e];if(t.startsWith("--")){const n=t.slice(2);n==="fix"||n==="force"||n==="json"||n==="urgent"||n==="now"||n==="keep-queue"||n==="hub"||n==="no-hub"||n==="dry-run"||n==="no-restart"||n==="watch"&&(e+1>=j.length||j[e+1].startsWith("--"))?s[n]=!0:s[n]=j[++e]}else s._.push(t)}const f=p(e=>console.log(typeof e=="string"?e:JSON.stringify(e,null,2)),"out");class I extends Error{static{p(this,"Exit")}constructor(t,n){super(t),this.code=n}}const i=p((e,t=1)=>{throw new I(e,t)},"fail");function Q(){s.icon&&!ce.includes(s.icon)&&i(`--icon takes one of: ${ce.join(", ")}`,2),s.color&&!qe.test(s.color)&&i("--color takes a CSS hex color, such as #3b82f6",2)}p(Q,"checkLook");function X(e){for(const t of["icon","color"])s[t]===""?delete e[t]:s[t]!==void 0&&(e[t]=s[t])}p(X,"applyLook");const fe=p(()=>s.hub&&s["no-hub"]&&i("pass --hub or --no-hub, not both",2),"checkHub"),he=p(e=>i(`/${e} is not a board; only a board can be the hub board`,2),"notBoard");function Z(e,t,n){if(s["no-hub"]&&delete n.hub,!!s.hub){L(n)||he(t);for(const[o,h]of Object.entries(e.sites))o!==t&&h?.hub&&delete h.hub;n.hub=!0}}p(Z,"applyHub");function k(){try{const e=JSON.parse(w.readFileSync(ze,"utf8"));return e.sites||={},e}catch(e){if(e.code==="ENOENT")return{version:1,sites:{}};i("manifest unreadable: "+e.message)}}p(k,"readManifest");const pe=p(e=>{try{return JSON.parse(w.readFileSync(e,"utf8"))}catch{return null}},"readJSON"),E=p(()=>import(u.join(y,"hub","sessions.mjs")),"sessionsModule");async function P(e){const{SessionError:t}=await E();try{return await e()}catch(n){throw n instanceof t&&i(n.message,n.reason==="live"?3:n.reason==="hub"?2:1),n}}p(P,"sessionCall");async function me(e){const{handshake:t}=await import(u.join(y,"hub","boards.mjs"));try{return await t(e,k())}catch(n){return i(n.message,2)}}p(me,"runHandshake");async function Me(e){for(let t=0;t<2;t++){let n;try{n=await fetch(`http://127.0.0.1:${T}/_hub/poll`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({flush:!1}),signal:AbortSignal.timeout(5*6e4)})}catch(h){return`not run (${h.message})`}if(!n.ok)return`not run (the hub answered ${n.status})`;const o=await n.json();if(o.checked?.includes(e))return"reached this board";if(o.failed?.length)return`missed this board: ${o.failed[0]}`}return"missed this board"}p(Me,"pollFor");async function O(e){const{withManifestLock:t}=await import(u.join(y,"hub","boards.mjs"));try{return await t(e)}catch(n){throw n.code==="ELOCKED"&&i(n.message),n}}p(O,"withLock");function we(e){return new Promise(t=>{const n=re.connect({port:e,host:"127.0.0.1"}),o=p(h=>{n.destroy(),t(h)},"done");n.setTimeout(500,()=>o(!1)),n.on("connect",()=>o(!0)),n.on("error",()=>o(!1))})}p(we,"connects");function Ke(e){return new Promise(t=>{const n=re.createServer();n.once("error",()=>t(!1)),n.listen(e,"0.0.0.0",()=>n.close(()=>t(!0)))})}p(Ke,"bindable");async function H(e){const t=new Set([...Object.values(e.sites),...Object.values(e.closed||{}).map(n=>n.site)].map(n=>n?.port));for(let n=W;n<W+400;n++)if(!t.has(n)&&!await we(n)&&await Ke(n))return n;i("no free port found from "+W)}p(H,"freePort");function Ge(){const e=ue();if(!e)return"";try{return(JSON.parse(ae(e,["status","--json"],{encoding:"utf8",stdio:["ignore","pipe","ignore"]})).Self?.DNSName||"").replace(/\.$/,"")}catch{return""}}p(Ge,"tailnetHost");const V=p(e=>{const t=Ge();return t?`https://${t}/${e}/`:`http://127.0.0.1:${T}/${e}/`},"siteUrl");async function J(){try{return await(await fetch(`http://127.0.0.1:${T}/_hub/status.json`,{signal:AbortSignal.timeout(1500)})).json()}catch{return null}}p(J,"hubStatus");const ge=p(e=>{try{return w.statSync(e).isDirectory()}catch{return!1}},"isFolder"),Ue=p(e=>{try{return w.realpathSync(e)}catch{return null}},"realPath");function be(e=t=>{}){(typeof s.name!="string"||!s.name.trim())&&i('join needs --name "<name>": the board or page name, which gives its route',2);const t=s.name.trim().replace(/\s+/g," "),n=F(t);return C(n)||i(`--name "${t}" needs a letter or digit for its route`,2),{route:n,title:t===n&&e(n)?.title||t}}p(be,"named");function ye(e,{title:t,items:n,tickets:o,repo:h,ref:d}){w.mkdirSync(u.join(e,"data"),{recursive:!0});const r=u.join(e,"board.json"),l=w.existsSync(r)?JSON.parse(w.readFileSync(r,"utf8")):{};l.title=t,l.items=De(n||l.items||"waves"),o&&(l.tickets=o),w.writeFileSync(r,JSON.stringify(l,null,2)+`
165
- `);const c=l.items==="waves"?null:u.resolve(e,l.items);if(c&&!w.existsSync(c)){w.mkdirSync(u.dirname(c),{recursive:!0});const a=l.tickets?.source==="internal"?l.tickets.prefix:void 0,b=Be(c,{prefix:a,hasBuild:Re(h,d)});w.writeFileSync(c,JSON.stringify({title:l.title,items:b},null,2)+`
165
+ `);const c=l.items==="waves"?null:u.resolve(e,l.items);if(c&&!w.existsSync(c)){w.mkdirSync(u.dirname(c),{recursive:!0});const a=Be(c,{hasBuild:Re(h,d)});w.writeFileSync(c,JSON.stringify({title:l.title,items:a},null,2)+`
166
166
  `)}return[U(),u.join(y,"board","server.mjs"),e]}p(ye,"boardFiles");async function ee(e,t){const{startSession:n,withSessionLock:o}=await E();return o(e,async()=>{const h=await P(()=>n(e,{manifest:k(),...t}));return await O(async()=>{const d=k(),r={session:h.session,label:h.name,since:new Date().toISOString(),repo:h.repo,...h.model?{model:h.model}:{}};g(d.sites,e)?(d.sites[e].owner=r,d.sites[e].updated=r.since):d.starting={...d.starting,[e]:{...g(d.starting,e),...r}},await v(d)}),h})}p(ee,"startRecorded");async function z(e){const{hubSession:t}=await E(),n=await P(()=>t({manifest:k(),repo:e}));process.exitCode=n.status}p(z,"openHub");const ke=p((e,...t)=>{const n=K("git",["-C",e,...t],{encoding:"utf8"});return n.status===0?n.stdout.trim():null},"git"),te=p(()=>!!(process.stdin.isTTY&&process.stdout.isTTY),"canAsk");async function ne(e,t="stopped at the prompt; nothing changed"){const n=Te.createInterface({input:process.stdin,output:process.stdout});try{return(await n.question(e)).trim()}catch(o){throw o.code==="ABORT_ERR"&&i(t),o}finally{n.close()}}p(ne,"ask");const Se=p((e,t)=>!!(g(e.sites,t)||g(e.starting,t)||g(e.closed,t)),"taken");function We(e){return!/^[A-Za-z0-9 ]+$/.test(e)||!C(F(e))?"A board name takes letters, digits and spaces, up to 41 characters of route.":Se(k(),F(e))?`/${F(e)} is taken; pick another name.`:null}p(We,"nameProblem");const je="--template board --items data/items.json";async function Ye(){const e=ke(process.cwd(),"rev-parse","--show-toplevel");e||i(`${process.cwd()} is not in a git repo; run kickflip join in the folder of the repo to add as a board`,2);const t=w.realpathSync(e);await J()||i("the hub is not running; run kickflip init to set it up, then kickflip join here again");const n=Object.entries(k().sites).find(([,m])=>L(m)&&m.owner?.repo&&Ue(m.owner.repo)===t),o=await Ce(t);if(n)return Qe(...n,o);const h=u.basename(t).replace(/[^A-Za-z0-9]+/g," ").trim();let d=s.name?.trim();for(d===void 0&&!te()&&i('join needs --name "<Board name>" when it has no terminal to ask in',2);;){d===void 0&&(d=await ne(`Board name${h?` (${h})`:""}: `)||h);const m=We(d);if(!m)break;s.name!==void 0&&i(m,2),f(m),d=void 0}const r=d.replace(/\s+/g," "),l=F(r),c=ke(t,"remote","get-url","origin"),a=c?Ee(c):null,{trustFolder:b}=await import("../hub/trust.mjs");try{b(t)}catch(m){i(`Couldn't trust this folder for Claude Code: ${m.message}`)}if(await O(async()=>{const m=k();Se(m,l)&&i(`/${l} is taken; pick another name`,2);const S=u.join(N,"sites",l),$=new Date().toISOString(),x={dir:S,port:await H(m),cmd:ye(S,{title:r,items:"data/items.json",repo:t,ref:o||"HEAD"}),watch:["data/events.jsonl"],title:r,local:!0,...a?{github:a}:{},...o?{branch:o}:{},owner:{repo:t},created:$,updated:$};m.sites={...m.sites,[l]:x},Z(m,l,x),await v(m)}),f(`Made ${r} at ${V(l)}, working in ${t}${a?` (${a})`:""}.`),s.hub)return f("It is the hub board, so its session runs in tmux, attached here."),z(t);try{await ee(l,{repo:t,prompt:je})}catch(m){throw m instanceof I&&i(`${m.message}
167
167
  The board is made; run kickflip join here again to start its session.`,m.code),m}f("Its session is starting in the background, and joins the board shortly.")}p(Ye,"joinFolder");function $e(e){const t=de(),n=t.restart();n&&i(`${e}: ${n}; run ${t.restartCommand}`),f("restarted the hub")}p($e,"restartHub");async function Qe(e,t,n){const o=t.title||e;f(`This folder already has a board: ${o} (/${e}).`),n&&n!==t.branch&&(await O(async()=>{const l=k();Object.assign(g(l.sites,e),{branch:n,updated:new Date().toISOString()}),await v(l)}),f(`Its branch is now ${n}${t.branch?` (it was ${t.branch})`:""}.`)),te()||i(t.hub===!0?"Open its session with kickflip hub.":"Run kickflip join in a terminal to restart its session, or tap Restart on its row of the Hub page.");const h=(await ne("Restart its session? (Y/n) ")).toLowerCase();if(h&&!"yes".startsWith(h))return;if(!t.owner.session)return t.hub===!0?z(t.owner.repo):(await ee(e,{repo:t.owner.repo,prompt:je}),f("Its session is starting in the background."));const d=await fetch(`http://127.0.0.1:${T}/_hub/restart-board`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({route:e}),signal:AbortSignal.timeout(5*6e4)}),r=await d.json();if(d.ok||i(r.error),f(`Restarted ${o}'s session.`),t.hub===!0)return z(t.owner.repo)}p(Qe,"restartFolderBoard");const se=["title","description","icon","color","created","watch","filter","hub","github","remote","branch","local"],ve=["notifications","quiet","codexShare"],R="kickflip-board",Xe=/^(\.watch-cursor-.*\.json|watcher\.json)$/,B=p((e,t)=>Object.fromEntries(t.filter(n=>e[n]!==void 0).map(n=>[n,e[n]])),"pick"),q=p(e=>/^[\w@%+=:,./-]+$/.test(e)?e:`'${String(e).replace(/'/g,"'\\''")}'`,"shellQuote");function Oe(e){const t=K("tar",e[0]==="-czf"?["--no-xattrs",...e]:e,{encoding:"utf8",env:{...process.env,COPYFILE_DISABLE:"1"}});t.status!==0&&i(`tar ${e[0]} failed: ${(t.stderr||t.error?.message||"").trim()}`)}p(Oe,"tar");function Ze(e,t){const n=e.entry.title||e.route,o=Object.keys(e.settings).map(h=>({notifications:"Notifications",quiet:"Quiet hours",codexShare:"Codex share"})[h]);return`# Moving ${n} (/${e.route}) to another machine
168
168
 
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- var W=Object.defineProperty;var l=(e,r)=>W(e,"name",{value:r,configurable:!0});import d from"node:fs";import g from"node:path";import _ from"node:os";import{routeRecord as m}from"../hub/routes.mjs";import{readHubRules as K,rulesOf as Q,ruleLines as B}from"../hub/rules.mjs";import{boardAt as C,linksOf as M,hubLink as z,stopPreviousWatcher as V}from"../hub/boards.mjs";import{parentOf as I,sessionGone as X}from"../hub/live.mjs";import{ackPing as Y,cardName as Z,markWorking as ee,settingsLines as te,settingsOf as ne,WORKING_KINDS as ie}from"../board/store.mjs";import{readUsageGate as L,heldUsageEvent as se,pausedLine as re}from"../shared/usage-gate.mjs";import{recordOnly as j,PLAIN as T,ON_CARD as q}from"./event-lines.mjs";const[$,k]=process.argv.slice(2);(!$||!k)&&(console.error("usage: wait-events.mjs <route> <session-id>"),process.exit(2));const oe=g.join(_.homedir(),".claude","kickflip","manifest.json");function P(){try{return m(JSON.parse(d.readFileSync(oe,"utf8")).sites,$)||null}catch{return}}l(P,"site");function v(e){const r=e.filter?new RegExp(e.filter):null,t=Object.create(null);for(const c of e.watch||["data/events.jsonl"]){let f=[];try{f=d.readFileSync(g.resolve(e.dir,c),"utf8").split(`
3
- `).filter(Boolean)}catch{}t[c]=r?f.filter(h=>r.test(h)):f}return t}l(v,"matching");const ce=`quiet The owner doesn't read this session's terminal. Answer a prompt typed in it in full. A wake, a task notification or a builder's result is not one: write no text of your own, since board.mjs and this watcher print one line for each thing done. Talk and questions go on the board, never AskUserQuestion; only a failure the board can't show, such as the hub being down, gets one line here. Each message to the owner stands alone in its card's thread: name cards by label (#96), and a pointer such as "above" or "here" must make sense in that thread, never point into this session.`,ae=l(e=>{try{return JSON.parse(e)}catch{return null}},"parsed");function R(e){try{return JSON.parse(d.readFileSync(g.resolve(e.dir,"board.json"),"utf8"))}catch(r){return r.code==="ENOENT"?null:{}}}l(R,"boardOf");async function D(e,r){for(const t of r){let c;try{c=JSON.parse(t)}catch{continue}(c.kind==="ping"||c.kind==="hello")&&typeof c.id=="string"&&await Y(e,c.id).catch(f=>console.log(`ping ${c.id} not answered (${f.message})`))}}l(D,"ackPings");async function le(e,r){let t;try{t=JSON.parse(r)}catch{return null}if(typeof t?.kind!="string")return null;const c=typeof t.data?.key=="string"?t.data.key:t.kind.startsWith("ask-")&&typeof t.data?.id=="string"?`ask:${t.data.id}`:null,f=c&&await Z(e,c).catch(()=>c);return m(T,t.kind)?T[t.kind](t):f&&m(q,t.kind)?q[t.kind](f,t):f?`${t.kind} on ${f}`:`${t.kind} received`}l(le,"plainLine");function de(e){let r=[];try{r=JSON.parse(d.readFileSync(g.resolve(e,"data","orchestrator-waits.json"),"utf8")).waits||[]}catch{}return r.map(t=>`waits ${t.kind} ${t.ref} ${t.key}${t.bumpedAt?" bumped":""} since ${t.since}${t.picked?`, picked up ${t.picked}`:""}: ${t.next}`)}l(de,"waitLines");const b=P();b||(console.log("site-removed"),process.exit(0));const y=g.resolve(b.dir,"data",`.watch-cursor-${k}.json`),N=g.resolve(b.dir,"data","watcher.json"),E=[process.ppid,I(process.ppid)],G=l(()=>{E[1]??=I(E[0]),X(k,E)&&process.exit(0)},"orphaned");V(b.dir,$,k);try{d.mkdirSync(g.dirname(y),{recursive:!0}),d.writeFileSync(N,JSON.stringify({pid:process.pid,since:new Date().toISOString()}))}catch{}process.on("exit",()=>{try{const e=JSON.parse(d.readFileSync(N,"utf8"));e.pid===process.pid&&d.writeFileSync(N,JSON.stringify({...e,exited_at:new Date().toISOString()}))}catch{}});let a;try{a=JSON.parse(d.readFileSync(y,"utf8"))}catch{}for(a||(a=Object.fromEntries(Object.entries(v(b)).map(([e,r])=>[e,r.length])),d.mkdirSync(g.dirname(y),{recursive:!0}),d.writeFileSync(y,JSON.stringify(a)));;){await new Promise(n=>setTimeout(n,2e3));const e=P();if(e===void 0)continue;e===null&&(console.log("site-removed"),process.exit(0)),e.owner?.session&&e.owner.session!==k&&(console.log("handed-over "+JSON.stringify(e.owner)),process.exit(0)),G();const r=l(n=>Object.fromEntries(Object.entries(n).map(([s,i])=>[s,[...m(a.$quietHeld,s)||[],...m(a.$usageHeld,s)||[],...i.slice(m(a,s)||0)]])),"pending"),t=l((n,s)=>s.state!=="paused"||!se(n),"eligible"),c=l((n,s)=>Object.values(n).some(i=>i.some(o=>t(o,s)&&!j(o))),"wakes"),f=L({dir:e.dir}),h=v(e);if(!c(r(h),f)){const n=Object.fromEntries(Object.entries(h).map(([i,o])=>[i,o.slice(m(a,i)||0).filter(j)])),s=Object.fromEntries(Object.entries(h).map(([i,o])=>[i,o.slice(m(a,i)||0).filter(u=>!j(u))]));if(Object.values(n).some(i=>i.length)){R(e)&&await D(e.dir,Object.values(n).flat());const i=l((o,u)=>Object.fromEntries(Object.keys(h).map(S=>[S,[...m(o,S)||[],...u[S]]])),"append");a={...Object.fromEntries(Object.entries(h).map(([o,u])=>[o,u.length])),$quietHeld:i(a.$quietHeld,n),$usageHeld:i(a.$usageHeld,s)},Object.values(a.$usageHeld).some(o=>o.length)||delete a.$usageHeld,d.writeFileSync(y,JSON.stringify(a))}continue}await new Promise(n=>setTimeout(n,2e3)),G();const F=v(e),O=L({dir:e.dir}),J=r(F);if(!c(J,O))continue;const w=Object.create(null),x=[],U=new Set(Object.values(a.$quietHeld||{}).flat()),H=[];for(const[n,s]of Object.entries(J)){w[n]=s.filter(i=>!t(i,O));for(const i of s.filter(o=>t(o,O))){U.has(i)||H.push(i),console.log(`event ${n} ${i}`);const o=await le(e.dir,i);o&&console.log(o);const u=ae(i);u&&ie.includes(u.kind)&&typeof u.id=="string"&&x.push(u.id)}}const p=R(e);if(p&&await ee(e.dir,x).catch(n=>console.log(`working marks not written (${n.message})`)),p&&await D(e.dir,H),p)for(const n of de(e.dir))console.log(n);if(p)for(const n of B(K(),Q(p)))console.log(`rule ${n}`);if(p)for(const n of te(ne(p)))console.log(n);p&&console.log(ce),O.state==="paused"&&console.log(re(O));try{const n=p&&C(e.dir);if(n)for(const s of[...M(n,{listen:"listening"}),z()])s.ok||console.log(`link ${s.link} ${s.detail}`)}catch(n){console.log(`link registry could not check the links (${n.message})`)}const A=Object.fromEntries(Object.entries(F).map(([n,s])=>[n,s.length]));Object.values(w).some(n=>n.length)&&(A.$usageHeld=w),d.writeFileSync(y,JSON.stringify(A)),process.exit(0)}
2
+ var W=Object.defineProperty;var l=(e,r)=>W(e,"name",{value:r,configurable:!0});import d from"node:fs";import g from"node:path";import _ from"node:os";import{routeRecord as m}from"../hub/routes.mjs";import{readHubRules as K,rulesOf as Q,ruleLines as B}from"../hub/rules.mjs";import{boardAt as C,linksOf as M,hubLink as z,stopPreviousWatcher as V}from"../hub/boards.mjs";import{parentOf as I,sessionGone as X}from"../hub/live.mjs";import{ackPing as Y,cardName as Z,markWorking as ee,settingsLines as te,settingsOf as ne,WORKING_KINDS as ie}from"../board/store.mjs";import{readUsageGate as L,heldUsageEvent as se,pausedLine as re}from"../shared/usage-gate.mjs";import{recordOnly as $,PLAIN as T,ON_CARD as q}from"./event-lines.mjs";const[j,O]=process.argv.slice(2);(!j||!O)&&(console.error("usage: wait-events.mjs <route> <session-id>"),process.exit(2));const oe=g.join(_.homedir(),".claude","kickflip","manifest.json");function P(){try{return m(JSON.parse(d.readFileSync(oe,"utf8")).sites,j)||null}catch{return}}l(P,"site");function v(e){const r=e.filter?new RegExp(e.filter):null,t=Object.create(null);for(const c of e.watch||["data/events.jsonl"]){let f=[];try{f=d.readFileSync(g.resolve(e.dir,c),"utf8").split(`
3
+ `).filter(Boolean)}catch{}t[c]=r?f.filter(h=>r.test(h)):f}return t}l(v,"matching");const ce=`quiet The owner doesn't read this session's terminal. Answer a prompt typed in it in full. A wake, a task notification or a builder's result is not one: write no text of your own, since board.mjs and this watcher print one line for each thing done. Talk and questions go on the board, never AskUserQuestion; only a failure the board can't show, such as the hub being down, gets one line here. Each message to the owner stands alone in its card's thread: name cards by label (#96), and a pointer such as "above" or "here" must make sense in that thread, never point into this session.`,ae=l(e=>{try{return JSON.parse(e)}catch{return null}},"parsed");function R(e){try{return JSON.parse(d.readFileSync(g.resolve(e.dir,"board.json"),"utf8"))}catch(r){return r.code==="ENOENT"?null:{}}}l(R,"boardOf");async function D(e,r){for(const t of r){let c;try{c=JSON.parse(t)}catch{continue}(c.kind==="ping"||c.kind==="hello")&&typeof c.id=="string"&&await Y(e,c.id).catch(f=>console.log(`ping ${c.id} not answered (${f.message})`))}}l(D,"ackPings");async function le(e,r){let t;try{t=JSON.parse(r)}catch{return null}if(typeof t?.kind!="string")return null;const c=typeof t.data?.key=="string"?t.data.key:t.kind.startsWith("ask-")&&typeof t.data?.id=="string"?`ask:${t.data.id}`:t.kind==="ask"?`ask:${t.id}`:null,f=c&&await Z(e,c).catch(()=>c);return m(T,t.kind)?T[t.kind](t):f&&m(q,t.kind)?q[t.kind](f,t):f?`${t.kind} on ${f}`:`${t.kind} received`}l(le,"plainLine");function de(e){let r=[];try{r=JSON.parse(d.readFileSync(g.resolve(e,"data","orchestrator-waits.json"),"utf8")).waits||[]}catch{}return r.map(t=>`waits ${t.kind} ${t.ref} ${t.key}${t.bumpedAt?" bumped":""} since ${t.since}${t.picked?`, picked up ${t.picked}`:""}: ${t.next}`)}l(de,"waitLines");const b=P();b||(console.log("site-removed"),process.exit(0));const k=g.resolve(b.dir,"data",`.watch-cursor-${O}.json`),N=g.resolve(b.dir,"data","watcher.json"),E=[process.ppid,I(process.ppid)],G=l(()=>{E[1]??=I(E[0]),X(O,E)&&process.exit(0)},"orphaned");V(b.dir,j,O);try{d.mkdirSync(g.dirname(k),{recursive:!0}),d.writeFileSync(N,JSON.stringify({pid:process.pid,since:new Date().toISOString()}))}catch{}process.on("exit",()=>{try{const e=JSON.parse(d.readFileSync(N,"utf8"));e.pid===process.pid&&d.writeFileSync(N,JSON.stringify({...e,exited_at:new Date().toISOString()}))}catch{}});let a;try{a=JSON.parse(d.readFileSync(k,"utf8"))}catch{}for(a||(a=Object.fromEntries(Object.entries(v(b)).map(([e,r])=>[e,r.length])),d.mkdirSync(g.dirname(k),{recursive:!0}),d.writeFileSync(k,JSON.stringify(a)));;){await new Promise(n=>setTimeout(n,2e3));const e=P();if(e===void 0)continue;e===null&&(console.log("site-removed"),process.exit(0)),e.owner?.session&&e.owner.session!==O&&(console.log("handed-over "+JSON.stringify(e.owner)),process.exit(0)),G();const r=l(n=>Object.fromEntries(Object.entries(n).map(([s,i])=>[s,[...m(a.$quietHeld,s)||[],...m(a.$usageHeld,s)||[],...i.slice(m(a,s)||0)]])),"pending"),t=l((n,s)=>s.state!=="paused"||!se(n),"eligible"),c=l((n,s)=>Object.values(n).some(i=>i.some(o=>t(o,s)&&!$(o))),"wakes"),f=L({dir:e.dir}),h=v(e);if(!c(r(h),f)){const n=Object.fromEntries(Object.entries(h).map(([i,o])=>[i,o.slice(m(a,i)||0).filter($)])),s=Object.fromEntries(Object.entries(h).map(([i,o])=>[i,o.slice(m(a,i)||0).filter(u=>!$(u))]));if(Object.values(n).some(i=>i.length)){R(e)&&await D(e.dir,Object.values(n).flat());const i=l((o,u)=>Object.fromEntries(Object.keys(h).map(S=>[S,[...m(o,S)||[],...u[S]]])),"append");a={...Object.fromEntries(Object.entries(h).map(([o,u])=>[o,u.length])),$quietHeld:i(a.$quietHeld,n),$usageHeld:i(a.$usageHeld,s)},Object.values(a.$usageHeld).some(o=>o.length)||delete a.$usageHeld,d.writeFileSync(k,JSON.stringify(a))}continue}await new Promise(n=>setTimeout(n,2e3)),G();const F=v(e),y=L({dir:e.dir}),J=r(F);if(!c(J,y))continue;const w=Object.create(null),x=[],U=new Set(Object.values(a.$quietHeld||{}).flat()),H=[];for(const[n,s]of Object.entries(J)){w[n]=s.filter(i=>!t(i,y));for(const i of s.filter(o=>t(o,y))){U.has(i)||H.push(i),console.log(`event ${n} ${i}`);const o=await le(e.dir,i);o&&console.log(o);const u=ae(i);u&&ie.includes(u.kind)&&typeof u.id=="string"&&x.push(u.id)}}const p=R(e);if(p&&await ee(e.dir,x).catch(n=>console.log(`working marks not written (${n.message})`)),p&&await D(e.dir,H),p)for(const n of de(e.dir))console.log(n);if(p)for(const n of B(K(),Q(p)))console.log(`rule ${n}`);if(p)for(const n of te(ne(p)))console.log(n);p&&console.log(ce),y.state==="paused"&&console.log(re(y));try{const n=p&&C(e.dir);if(n)for(const s of[...M(n,{listen:"listening"}),z()])s.ok||console.log(`link ${s.link} ${s.detail}`)}catch(n){console.log(`link registry could not check the links (${n.message})`)}const A=Object.fromEntries(Object.entries(F).map(([n,s])=>[n,s.length]));Object.values(w).some(n=>n.length)&&(A.$usageHeld=w),d.writeFileSync(k,JSON.stringify(A)),process.exit(0)}
@@ -1,28 +1,31 @@
1
1
  #!/usr/bin/env node
2
- var W=Object.defineProperty;var r=(e,a)=>W(e,"name",{value:a,configurable:!0});import{readiness as D}from"../../hub/readiness.mjs";import{readSlack as H,setSlack as L}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 M}from"node:url";import*as n from"../store.mjs";import{boardWorktree as z,builderRepo as U,joiningLand as J,landInTurn as F,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 Z,stallCard as Y,stopBuilder 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 W=Object.defineProperty;var i=(e,a)=>W(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 L}from"node:url";import*as n from"../store.mjs";import{boardWorktree as z,builderRepo as J,joiningLand as U,landInTurn as F,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 Z,stallCard as Y,stopBuilder 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 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 item files live. Without --site, they come from ~/.claude/waves.
6
6
  Paths resolve under $HOME, so HOME=<dir> points every command at a copy.
7
7
  A --file argument of - reads stdin.
8
- item add creates a ticket in the board\u2019s source unless --ticket <id> links one.
9
- Internal defaults to INT; GitHub and Linear create an issue from the title and notes, Linear in the board's team and
10
- project through Hub's Linear key. A linked ticket must exist in the source; an Internal one no other card holds.
11
- Asks have no ticket; item set --from-ask creates one when the card has none.
8
+ Every new card is a task, keyed T-<n>, with no ticket, until its grill's ticket question (answered yes, the board makes
9
+ it), item add or item set --new-ticket (the orchestrator, for a Quick task), or --ticket <id> gives it one. A ticket comes
10
+ from the board's source, GitHub or Linear, made from the card's title and notes, Linear in the board's team and project
11
+ through Hub's Linear key; a linked one must exist there. A board whose source is none has no tickets.
12
12
  A card moves its ticket one way: a build's start \u2192 In Progress, --pr, --pushed or a verified merge-ready build \u2192 In
13
13
  Review, --done \u2192 Done (a GitHub issue closes), only for a ticket assigned to the owner or created by kickflip. A ticket that
14
14
  can't move prints one "ticket <id>: \u2026" line; the card's change still stands.
15
15
  Linear needs Hub's Linear key: settings set tickets refuses it without one, and once the key is removed the hub
16
- moves every Linear board to internal tickets. A card is never left without a ticket: if Linear can't make one, the
17
- command fails and changes nothing.
16
+ moves every Linear board to no ticket source. If the source can't make a ticket, the command fails and changes nothing.
18
17
 
19
- reply <eventId> (--html <text> | --file <path>) data/replies/<eventId>.html. A reply to an ask resolves its
20
- answered questions (topic item:ask:<eventId>), and one to
21
- wave-triage its wave's, noting "Acted on: replied". A
18
+ reply <eventId> (--text <md> | --file <path>) data/replies/<eventId>.md, markdown the board draws. A
19
+ reply to an ask answers the card it became, as reply
20
+ <file>/<id> does, and resolves that card's answered
21
+ questions; one to wave-triage resolves its wave's,
22
+ noting "Acted on: replied". A
22
23
  reply to overnight-triage ends that triage
23
- [--title <t>] retitles the ask it answers, or whose thread the message
24
- is in, which then wears an Ask badge in place of its first
25
- line as title (data/ask-titles.json)
24
+ [--title <t>] retitles the New or answered card the ask became
25
+ reply <file>/<id> (--text <md> | --file <path>) answers the New card an ask became: the answer goes in
26
+ [--title <t>] its thread and the card waits in In Review for the
27
+ owner's Done, --title as its title. Refuses a card
28
+ that is sorted or isn't one an ask became
26
29
  thread <file>/<id> --text <t> [--link <url>] one line in the item's thread; ask:<eventId> in place
27
30
  [--who claude|love|<name>] [--via github|slack] of <file>/<id> writes in that ask's thread
28
31
  A Claude thread line, or a reply to an ask or item-message, rewrites the body of the thread's
@@ -34,8 +37,8 @@ command fails and changes nothing.
34
37
  topic add <id> --title <t> [--summary <s>] a question topic in data/owner.json
35
38
  question add --file <json> one question or an array of them:
36
39
  {id, topic, question, options: [{id, label, recommended?, detail?, diagram?, practice?}],
37
- source?, context?, url?, urgent?, round_of?, reask_of?, ask?, suggested?, prototype_gate?}; topic is declared, item:<file>/<id>, item:ask:<eventId>
38
- for a New card not sorted yet, wave:<slug> for a wave's triage, or file:<slug> for an item file's triage on
40
+ source?, context?, url?, urgent?, round_of?, reask_of?, ask?, suggested?, prototype_gate?}; topic is declared, item:<file>/<id> (an ask's
41
+ questions go on its card's), wave:<slug> for a wave's triage, or file:<slug> for an item file's triage on
39
42
  a board over ~/.claude/waves. A question on a done card reopens it.
40
43
  A question on an item: topic whose card has a grill or prototype planner running is staged, not published,
41
44
  whoever adds it: it shows nowhere until the planner finishes, fails or is stopped, when every staged question
@@ -75,7 +78,8 @@ command fails and changes nothing.
75
78
  question settle <id> --note <text> a question a finished grill left for the orchestrator to screen
76
79
  (a screen wait), which the board already answers: it is published resolved "Settled by the
77
80
  orchestrator: <text>", and the answer goes into the card's notes. Refuses the prototype question,
78
- which is the owner's
81
+ which is the owner's. On a ready card waiting on your re-check since its block ended, it answers
82
+ one of the card's questions again the same way, when what landed made its answer stale
79
83
  question publish <file>/<id> passes the rest of the card's screened questions on to the owner.
80
84
  The board server publishes any left unscreened 15 minutes on, with a thread line
81
85
  question move <id>,<id>,\u2026 --to <file>/<id> [--block <file>/<id>,\u2026] merges open questions that ask one thing onto
@@ -134,13 +138,16 @@ command fails and changes nothing.
134
138
  remote. Moves it to In Review as --pr does
135
139
  [--gh-since <ISO time>] records the last handled GitHub event, an ISO-8601 UTC
136
140
  time such as 2026-10-23T11:00:00Z, stored as given
137
- [--from-ask <eventId> --text <t>] the ask it came from: its New card gives way to this item,
141
+ [--from-ask <eventId> --text <t>] the ask it came from, for an ask from before asks
142
+ arrived as cards: its New row gives way to this item,
138
143
  whose thread opens with the ask and its thread, then --text
139
144
  as the item's first line; it leaves a card it was folded into; refuses a card
140
- from another ask (use item relate-ask), and one with no
141
- phase when --phase is missing
145
+ from another ask (use item relate-ask), an ask that is
146
+ already a card, naming it (sort that card instead), and
147
+ one with no phase when --phase is missing
142
148
  [--phase ready|grilling|prototyping] a Todo card's phase: what it does next; it reads New
143
- until set. Refused on a card in another lane, whose phase
149
+ until set, which sorts a New card an ask became (an
150
+ answered one leaves In Review for Todo). Refused on a card in another lane, whose phase
144
151
  the board sets, and as --state, its old name.
145
152
  Staging is the board's: it stages a card on the owner's
146
153
  Send, Implement or Reply on a ready card, and refuses
@@ -151,7 +158,10 @@ command fails and changes nothing.
151
158
  answered question in that file, since a builder starts
152
159
  from it; it resolves those answered questions.
153
160
  grilling or prototyping on a staging or ready card sends
154
- it back, and the board starts that round's planner
161
+ it back, and the board starts that round's planner.
162
+ A ready card whose block ended waits on your re-check:
163
+ ready again with --text "<what you re-checked>", which
164
+ goes into its notes, releases it to the pull
155
165
  [--held | --unheld | --pick-up-at <time>] holds or resumes the card on the owner's words; the board
156
166
  server records the owner's own Hold and Resume taps. A later
157
167
  --held/--unheld overrides a tap.
@@ -162,7 +172,8 @@ command fails and changes nothing.
162
172
  resumes the card, adds a thread line, and appends
163
173
  card-unblocked {key, until} unless a dep is unfinished.
164
174
  --held holds it with no time; --pick-up-at "" resumes it
165
- [--ticket <id>] links a ticket in the board\u2019s source
175
+ [--ticket <id> | --new-ticket] links a ticket in the board\u2019s source, or makes one there
176
+ from the title and notes; the key stays
166
177
  [--title <t>] renames the card
167
178
  [--check "<name> <args>"] adds or changes the code check on the card's owner step,
168
179
  as builder done --check takes it, clearing a broken mark;
@@ -170,7 +181,10 @@ command fails and changes nothing.
170
181
  [--size small|large] [--model opus|sonnet] what builder pick reads while the hub's Claude model is
171
182
  Auto: a small card gets Claude Sonnet, any other Opus, and
172
183
  --model wins over --size; "" clears either
173
- [--deps <id>,<file>/<id>] replaces its blocked-by list; --deps "" clears it
184
+ [--deps <id>,<file>/<id>,wave:<slug>] replaces its blocked-by list; --deps "" clears it.
185
+ wave:<slug> waits on every card in the wave at the
186
+ time, as it grows, until each is Done; refused for a
187
+ card in that wave
174
188
  [--blocked-until "<condition>"] blocks it until a condition an agent can check, which
175
189
  the orchestrator checks on its passes over the cards;
176
190
  --blocked-until "" clears it, which threads and emits
@@ -196,6 +210,11 @@ command fails and changes nothing.
196
210
  [--text <t>] with --done, required on a site: the card's closing line,
197
211
  written last in its thread, saying why it closed (merged,
198
212
  dropped, superseded)
213
+ [--split-into wave:<slug> | <id>,<file>/<id>] with --done, in place of --text: closes a split card. Each
214
+ open card waiting on it waits on the wave or cards it
215
+ split into instead, with a line in its thread, and the
216
+ closing line reads "Split into \u2026". Refused when a
217
+ waiting card is one of them, or the card is already Done
199
218
  builder start for a build moves the card to In Progress with "Moved to In Progress: a build
200
219
  started", resolving its answered questions, noting "Acted on: build started". It refuses while
201
220
  the item is held or a dep isn't done, and while the newest owner answer to its questions that
@@ -212,7 +231,9 @@ command fails and changes nothing.
212
231
  including held cards: start each unheld card next. It refuses a card the owner reopened: it
213
232
  closes again only on the owner's Done
214
233
  item add <file>[/<id>] --title <t> a new card, unstarted; never
215
- [--ticket <id>] [--deps <id>,<file>/<id>] hand-edit the item file to add one. Refuses an id the file has.
234
+ [--ticket <id> | --new-ticket] hand-edit the item file to add one. Refuses an id the file has.
235
+ [--deps <id>,<file>/<id>,wave:<slug>] A task with no ticket unless --ticket links one or
236
+ --new-ticket makes one from the title and notes
216
237
  [--notes <t> | --notes-from <draft.md>] the board makes the card's own notes, <id>-notes.md beside
217
238
  its item file, read-only: the draft verbatim, or "# <title>"
218
239
  then the --notes text. Refuses a draft that is any card's
@@ -228,12 +249,13 @@ command fails and changes nothing.
228
249
  ready is for a Quick build ask (--from-ask), with --size
229
250
  and --notes or --notes-from, since a builder starts from it
230
251
  [--from-ask <eventId> --text <t>] files the card from that ask in this one command, as
231
- item set's --from-ask: no grill starts before the ask is on
252
+ item set's --from-ask, refusing an ask that is already a
253
+ card: no grill starts before the ask is on
232
254
  the card, which takes the ask's place in Todo. Needs --phase
233
255
  On a board --phase is required, so no card an agent
234
256
  adds reads New
235
- Without /<id>, the card takes its ticket's id (GH-<n> for issue #n); pass an id only to import a
236
- card. Prints "<label> added as <key>": use the key in later commands
257
+ Without /<id>, a task is keyed T-<n>, the next on the board, and a ticketed card takes its ticket's
258
+ id (GH-<n> for issue #n); pass an id only to import a card. Prints "<label> added as <key>": use the key in later commands
237
259
  item notes <file>/<id> --heading <h> appends "## <h>" and the draft to the card's notes, making
238
260
  (--file <draft.md> | --from <file>/<id>) its <id>-notes.md when it has none; the only way to write
239
261
  notes (--notes-file and --plan are gone). Write the draft
@@ -244,22 +266,20 @@ command fails and changes nothing.
244
266
  Resolves answered questions still waiting on Claude,
245
267
  noting "Recorded in the card's notes", and prints
246
268
  "resolved: <id>" for each
247
- item relate-ask <eventId> <file>/<id> --summary <t> folds an ask about an existing item into it: the ask's
248
- card goes, its message and thread show in the item's
249
- thread marked Moved, then the summary line. Again with
250
- another item moves it there
251
- [--title <t>] retitles the ask, as reply's --title
252
- ask progress <eventId> [--next <text>] keeps an answered ask in In Review, --next as its next
253
- line, until the owner's ask-done (the Done beside Reply
254
- in its thread) or ask done is newer. Use it when the answer
255
- waits on the owner, such as a pick between prototypes
256
- ask done <eventId> moves the ask to Done, as the owner's Done button does
269
+ item relate-ask <eventId> <file>/<id> --summary <t> folds an ask about an existing item into it: the New card
270
+ the ask became goes, its message and thread show in the
271
+ item's thread marked Moved, then the summary line, and the
272
+ owner's banner names the card it merged into. Refuses
273
+ an ask whose card is sorted. Again with another item
274
+ moves it there
275
+ [--title <t>] names the merge in the owner's banner
257
276
  ask unfold <eventId> moves a folded ask back out to a card of its own
258
277
  ask move <eventId> --to <route> sends a New ask to another board in the hub's manifest:
259
278
  a New card there with the owner's text, attachments and
260
- "Moved from <this board>", which wakes its session; here the
261
- ask goes to Done with a thread line linking to that card.
262
- A card the ask became is dropped, unless it has started.
279
+ "Moved from <this board>", which wakes its session; here
280
+ the card the ask became is dropped, leaving a line in the
281
+ ask's thread linking to that card. Refuses an ask whose
282
+ card is no longer New.
263
283
  Refuses a route that isn't another board, and an ask
264
284
  that isn't New
265
285
  Then sends the queued pushes, each on its own, unless
@@ -278,22 +298,20 @@ command fails and changes nothing.
278
298
  isn't an earlier card of the wave. Prints the order by
279
299
  card label, each card with "(after \u2026)" its conflicts
280
300
  wave add <slug> <file>/<id> [--after <file>/<id>,\u2026] puts a card in the wave, last, blocked by the earlier
281
- cards --after names
282
- wave remove <slug> <file>/<id> takes a card out: it and the wave's cards waiting on it
283
- lose their links to each other
301
+ cards --after names; refused for a card whose --deps
302
+ names the wave
303
+ wave remove <slug> <file>/<id> takes a card out, keeping its own links; refused
304
+ while an open wave card waits on it, naming each and
305
+ the wave set \u2026 --after to run
284
306
  wave set <slug> [<file>/<id> --after <file>/<id>,\u2026] rewrites only that card's links to the wave's cards to
285
307
  [--title <t>] [--goal <t>] --after's earlier cards ("" clears them), renames the
286
308
  [--note <t> | --file <f>] wave, sets its goal or rewrites its note
287
309
  wave finish <slug> the wave's band leaves Todo; its cards keep their
288
- links. A wave whose cards are all Done leaves it too
310
+ links. Refused, naming them, while a card in it is
311
+ open. A wave whose cards are all Done leaves it too
289
312
  A band's Triage sends wave-triage {wave}, or {file} for an item file's band; it reads
290
313
  Triaging\u2026 until you reply to that event, then "Triage: N open questions" while questions on topic
291
314
  wave:<slug> or file:<slug> are open or answered
292
- tickets backfill [--dry-run] gives every open card without a ticket one: the first
293
- id in its title (or legacy linear list) from the board's
294
- source is linked, else a new ticket is made. Done cards are
295
- left alone. Prints each card's result; --dry-run changes and
296
- calls nothing
297
315
  reviewers set --file <json> the board's reviewers in board.json, replacing them:
298
316
  [{name, slack_user, dm, github}], as in an item file.
299
317
  The settings page lists them; new item files copy them
@@ -663,4 +681,4 @@ command fails and changes nothing.
663
681
  (overnight, snoozed or the weekend). --urgent: a
664
682
  production fault only; it beats those holds. While
665
683
  notifications are off every push is dropped, urgent too
666
- 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","wait","hub","clear-install","force","worktree","dry-run","dispatch","writes"]);function oe(e){const a=Object.create(null),t=[];for(let s=0;s<e.length;s++){const i=e[s];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(s+1<e.length)(a[o]||=[]).push(e[++s]);else throw new d(`--${o} needs a value`)}return{flags:a,rest:t}}r(oe,"parse");function de(e,a,t){const s=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||s.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","heading","summary","title","body","next","progress","install-label","confirmed","asked","task","reason"],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 s=e.includes("file")?`; for a file use --file ${a[t].trim()}`:"";throw new d(`--${t} takes text, not a file path (${a[t].trim()})${s}`)}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 q(e){if(!e)throw new d("--file <path> is required");try{return JSON.parse(w(e))}catch(a){throw new n.BoardError(`${e}: ${a.message}`)}}r(q,"readJSONInput");function C(e,a){const t=["notes-file","plan"].find(s=>e[s]!==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(C,"refuseNotesFile");function _(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(_,"noteOf");async function pe(e,a,{"owner-step":t,check:s,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(s!==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 n.setItem(e,a,{ownerStep:t,check:s});const l=e&&!o.dir,c=l&&await n.handBackCalls(e,a,o);if(c)return c;if(l&&o.next===void 0&&await n.finishesCard(e,a,o)){if(i===void 0)throw new n.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 n.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 n.stopBuilder(e,a,{...o,note:i})}if(l&&i!==void 0&&await n.isPlanner(e,a,o.agent))return n.stopBuilder(e,a,{...o,note:i,requireNext:!0});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 or a planner's");return n.stopBuilder(e,a,{...o,requireNext:!0})}r(pe,"builderDone");const b={pr:[["file/id"],[],async(e,[a])=>({lines:(await n.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 n.postPR(e,a,{body:t.body})}],"pr reply":[["pr","comment-id"],["body"],(e,[a,t],s)=>{if(s.body===void 0)throw new d('pr reply needs --body "<what Claude says>"');return n.postPR(e,a,{replyTo:t,body:s.body})}],reply:[["eventId"],["html","file","title"],(e,[a],t)=>{if(t.html===void 0==(t.file===void 0))throw new d("reply takes --html or --file");return n.writeReply(e,a,t.html??w(t.file),t.title)}],thread:[["file/id"],["text","link","who","via"],(e,[a],t)=>n.appendMessage(e,a,t)],"visual add":[["file/id"],["file","title"],(e,[a],t)=>n.addVisual(e,a,t)],"topic add":[["id"],["title","summary"],(e,[a],t)=>n.addTopic(e,{id:a,...t})],"question add":[[],["file"],async(e,a,t)=>{const s=q(t.file);return{...await n.addQuestions(e,s,process.env.KICKFLIP_RENDER_CHECK==="off"?void 0:se),asked:[s].flat()}}],"question resolve":[["id"],["note"],(e,[a],t)=>n.resolveQuestion(e,a,t.note)],"question settle":[["id"],["note"],(e,[a],t)=>n.settleQuestion(e,a,t.note)],"question publish":[["key"],[],async(e,[a])=>({ids:await n.publishScreened(e,a)})],"question move":[["ids"],["to","block"],(e,[a],t)=>n.moveQuestions(e,a.split(",").filter(Boolean),t.to,(t.block||"").split(",").filter(Boolean))],"prototype add":[["topic"],["title","...take","card","served","...state"],(e,[a],t)=>n.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=>n.listPrototypes(e)],"prototype rm":[["topic"],[],(e,[a])=>n.removePrototype(e,a)],"item set":[["file/id"],["done","status","clear-status","next","clear-next","...pr","pushed","gh-since","from-ask","phase","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,s)=>(C(t,`add to its notes with item notes ${a} --heading "<heading>" --file <draft.md>`),t.done&&e&&await n.checkNotReopened(e,a),n.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"],phase:t.phase,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},s))],"item add":[["file[/id]"],["title","notes","notes-from","deps","notes-file","plan","ticket","pick-up-at","held","parent","size","model","phase","state","relate","from-ask","text"],(e,[a],t,s)=>{if(C(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 n.addItem(e,a,{title:t.title,notes:t.notes,notesFrom:t["notes-from"],deps:t.deps,ticket:t.ticket,pickUpAt:t["pick-up-at"],held:t.held,parent:t.parent,size:t.size,model:t.model,phase:t.phase,state:t.state,relate:t.relate,fromAsk:t["from-ask"],text:t.text,intake:!0},s)}],"item notes":[["file/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 <file>/<id>");return n.addNotes(e,a,t)}],"item relate-ask":[["eventId","file/id"],["summary","title"],(e,[a,t],s)=>n.relateAsk(e,a,t,s.summary,s.title)],"ask progress":[["eventId"],["next"],(e,[a],t)=>n.markAsk(e,a,"progress",t.next)],"ask done":[["eventId"],[],(e,[a])=>n.markAsk(e,a,"done")],"ask unfold":[["eventId"],[],(e,[a])=>n.unfoldAsk(e,a)],"ask move":[["eventId"],["to"],(e,[a],t)=>n.moveAsk(e,a,t.to)],"tickets backfill":[[],["dry-run"],(e,a,t,s)=>n.backfillTickets(e,{dryRun:t["dry-run"]},s)],"wave create":[["slug"],["title","goal","...card","note","file"],(e,[a],t)=>n.createWave(e,a,{title:t.title,goal:t.goal,cards:t.card,note:_(t)})],"wave add":[["slug","file/id"],["after"],(e,[a,t],s)=>n.changeWave(e,a,{add:t,after:s.after})],"wave remove":[["slug","file/id"],[],(e,[a,t])=>n.changeWave(e,a,{remove:t})],"wave set":[["slug","...file/id?"],["after","title","goal","note","file"],(e,[a,...t],s)=>{if(t.length>1)throw new d("wave set takes <slug> [<file/id>]");return n.changeWave(e,a,{card:t[0],after:s.after,title:s.title,goal:s.goal,note:_(s)})}],"wave finish":[["slug"],[],(e,[a])=>n.finishWave(e,a)],slack:[[],[],()=>({lines:[JSON.stringify(H())]})],"slack set":[[],["self-dm","user","domain","timezone"],async(e,a,t)=>{const s={"self-dm":"self_dm",user:"owner_slack_user",domain:"slack_domain",timezone:"timezone"};try{return{lines:[JSON.stringify(await L(Object.fromEntries(Object.entries(t).map(([i,o])=>[s[i],o]))))]}}catch(i){throw new n.BoardError(i.message)}}],integrations:[[],[],async()=>({lines:[JSON.stringify(await D.rows())]})],"review updated":[["file/id"],["text"],(e,[a],t)=>n.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 n.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 n.postIncomingReview(e,a,{words:w(t.file),readAt:t["read-at"]})}],"review digest":[[],[],async(e,a,t,s)=>({lines:[JSON.stringify(await n.reviewDigest(e,s))]})],"review due":[[],[],async(e,a,t,s)=>({lines:[JSON.stringify(await n.reviewDue(e,s))]})],"reviewers set":[[],["file"],(e,a,t)=>n.setReviewers(e,q(t.file))],"reviewers add":[[],["file"],(e,a,t)=>n.addReviewers(e,q(t.file))],"reviewers remove":[["...name"],[],(e,a)=>n.removeReviewers(e,a)],"rules list":[[],[],e=>n.listRules(e)],"rules add":[[],["text","hub","by"],(e,a,t)=>n.addRuleTo(e,t)],"rules edit":[["id"],["text","hub"],(e,[a],t)=>n.editRuleIn(e,a,t)],"rules remove":[["id"],["hub"],(e,[a],t)=>n.removeRuleFrom(e,a,t)],settings:[[],[],async e=>({lines:[JSON.stringify(await n.readBoardSettings(e))]})],build:[["file/id"],["asked"],async(e,[a],t)=>{if(!e)throw new n.BoardError("build needs --site <route>");n.parseKey(a);const s=await n.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 n.BoardError(o.error||"The hub refused the build.");return{lines:["started",...(await n.appendMessage(e,a,{text:s})).lines]}}catch(i){throw i instanceof n.BoardError?i:new n.BoardError(`Could not start the build: ${i.message}`)}}],"main set":[[],["install","install-sha"],(e,a,t)=>n.setMainInstall(e,{install:t.install,installSha:t["install-sha"]})],"settings set":[["name","value"],[],(e,[a,t])=>n.setBoardSetting(e,a,t)],"builder start":[["file/id"],["label","phase","progress","dir","agent","kind","model","eta","worktree","repo","dispatch"],(e,[a],{worktree:t,repo:s,dispatch:i,...o})=>{if(s!==void 0&&!t)throw new d("--repo goes with --worktree");const l=r(p=>z(e,a.split("/")[1],{repo:s,...p}),"make"),c=e&&o.dir===void 0&&ae(e);return n.startBuilder(e,a,{...o,dispatch:i?"session":void 0,worktree:t?l:void 0,ownWorktree:c?l:void 0})}],"builder progress":[["file/id","...text?"],["phase","text","dir","agent","eta","clear-eta"],(e,[a,...t],s)=>{if(t.length&&s.text!==void 0)throw new d("builder progress takes its text once: as words or as --text");if(!t.length&&s.text===void 0)throw new d("builder progress needs its text, as words or as --text");return n.builderProgress(e,a,s.text??t.join(" "),{...s,clearEta:s["clear-eta"]})}],"builder done":[["file/id"],["dir","agent","next","owner-step","check","text","updated"],async(e,[a],{updated:t,...s})=>{const i=e&&!s.dir;if(i&&t===void 0&&await n.cardLinked(e,a))throw new n.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,s);return i&&t!==void 0&&t.trim().toLowerCase()!=="none"&&await n.reviewUpdated(e,a,t),o}],"builder pick":[["file/id"],[],(e,[a])=>n.pickBuilder(e,a)],"agent launch":[["file/id"],["name","task","writes","model"],(e,[a],t)=>n.requestHelper(e,a,t)],"agent wait":[["file/id"],["name","agent","timeout"],(e,[a],t)=>n.waitHelper(e,a,{...t,alive:Q})],"agent merge":[["file/id"],["name"],async(e,[a],t)=>{const{record:s,card:i}=await n.helperOf(e,a,t.name);if(!s.done)throw new n.BoardError(`helper ${t.name} is still open; run agent wait ${a} --name ${t.name} first`);if(!s.writes||!s.worktree)throw new n.BoardError(`helper ${t.name} writes no code, so there is nothing to merge`);if(!i)throw new n.BoardError(`${a} has no worktree to merge helper ${t.name} into`);return G(i,s.worktree)}],audit:[["...file/id?"],[],(e,[a,...t])=>{if(t.length)throw new d("audit takes one <file/id> at most");return n.auditLog(e,a)}],"builder call":[["file/id","...text"],["dir","agent"],(e,[a,...t],s)=>n.recordCall(e,a,t.join(" "),s)],"builder calls":[["file/id"],["dir","agent"],(e,[a],t)=>n.listCalls(e,a,t)],"builder ask":[["file/id","...text"],["agent"],(e,[a,...t],s)=>n.askOrchestrator(e,a,t.join(" "),s)],"builder answer":[["file/id","ask"],["text"],(e,[a,t],s)=>n.answerBuilder(e,a,t,s.text)],"builder defer":[["file/id","ask"],["text"],(e,[a,t],s)=>n.answerBuilder(e,a,t,s.text,{deferred:!0})],"builder asks":[["file/id"],["wait","timeout"],(e,[a],t)=>n.listAsks(e,a,t)],"builder verified":[["file/id"],["dir","agent","note"],async(e,[a],t)=>{await n.verifyCalls(e,a,t);const s=e&&!t.dir?await n.verifiedFinish(e,a):null;if(!s)return{};if(s.waits.length)return{lines:[`${await n.cardName(e,a)} finishes once the owner answers ${s.waits.join(", ")}`]};try{return await $(e,a,{text:t.note??"Judgment calls verified."})}catch(i){throw i instanceof n.BoardError?Object.assign(new n.BoardError(`judgment calls verified, but ${await n.cardName(e,a)} didn't finish: ${i.message}`),{exitCode:i.exitCode}):i}}],land:[["file/id"],["repo","dir","text","owner-step","check"],async(e,[a],t)=>{const s=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 n.cardName(e,a):void 0,o=await J(i??a,async()=>{e&&await n.checkClose(e,a,t.text,t["owner-step"],t.check),await n.checkCallsVerified(e,a,{dir:t.dir});const l=await n.recordedWorktree(e,a,{dir:t.dir}),{repo:c,base:p}=U(e,{repo:t.repo??l?.repo});return await F({path:l?.path??x.join(K(),s),branch:l?.branch??s,repo:x.resolve(c),base:p,ref:i}),e?n.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":[["file/id"],[],(e,[a])=>ee(m(e,"builder stop"),a)],"item stall":[["file/id"],["reason"],(e,[a],t)=>Y(m(e,"item stall"),a,t.reason)],"land stop":[["file/id"],[],(e,[a])=>te(m(e,"land stop"),a)],"land queue":[[],[],()=>Z()],"item bump":[["file/id"],[],(e,[a])=>S(m(e,"item bump"),a,!0)],"item unbump":[["file/id"],[],(e,[a])=>S(m(e,"item unbump"),a,!1)],lander:[["file/id"],[],async(e,[a])=>{if(!e)throw new d("lander needs --site <route>");return await n.relaunchLander(e,a),{lines:[`${await n.cardName(e,a)} is handed back again; the server launches a lander for it within a minute`]}}],merge:[["file/id"],["after-approval","confirmed"],async(e,[a],t)=>e&&await n.heldMerge(e,a)?X(e,a):n.mergeCard(e,a,{afterApproval:t["after-approval"],confirmed:t.confirmed})],"arch file":[["file/id"],[],(e,[a])=>e?V(e,a):Promise.reject(new n.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 n.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)=>n.cardName(e,a),"card"),R=r((e,a)=>`${a}${e.length===1?"":"s"} ${e.join(", ")}`,"plural"),N=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.phase,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(we,"itemChanges");const me={reply:r(async(e,[a])=>`reply written to ${await n.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:s,staged:i})=>{const o=[...new Set(s.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`${R(s.map(c=>c.id),"question")} ${i===s.length?"staged":"added"}${l}`},"question add"),"question resolve":r((e,[a])=>`question ${a} resolved`,"question resolve"),"question settle":r((e,[a])=>`question ${a} settled`,"question settle"),"question publish":r(async(e,[a],t,{ids:s})=>`${R(s,"question")} on ${await h(e,a)} passed to the owner`,"question publish"),"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:s})=>`${await h(e,s)} added as ${s}`,"item add"),"item notes":r(async(e,[a],t,{file:s,heading:i})=>`${await h(e,a)}: notes section "${i}" added to ${s}`,"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"),"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"),"wave create":r((e,[a])=>`wave ${a} created`,"wave create"),"wave add":r(async(e,[a,t])=>`${await h(e,t)} added to wave ${a}`,"wave add"),"wave remove":r(async(e,[a,t])=>`${await h(e,t)} taken out of wave ${a}`,"wave remove"),"wave set":r((e,[a])=>`wave ${a} updated`,"wave set"),"wave finish":r((e,[a])=>`wave ${a} finished`,"wave 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)=>`${R(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,s)=>`${await h(e,a)}: ${s.alreadyOpen?"already open: the Implement tap opened its record; launch the agent, whose own builder start takes it up":`${N(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,s)=>`${await h(e,a)}: ${s?.said??`${N(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:s})=>`${await h(e,a)}: asked the orchestrator, ${s}`,"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,s)=>`${await h(e,a)}: judgment calls verified${s?.said?`; ${s.said}`:""}`,"builder verified"),land:r(async(e,[a],t,s)=>e?s?.said?`${await h(e,a)}: ${s.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,s)=>`${await h(e,a)}: ${s?.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:s}=oe(e),i=[3,2,1].map(u=>s.slice(0,u).join(" ")).find(u=>y(b,u)),o=y(b,i);if(!o)throw new d(s.length?`unknown command "${s.slice(0,2).join(" ")}"`:"no command given");const[l,c,p]=o,f=s.slice(i.split(" ").length),T=l.at(-1)?.startsWith("..."),B=l.at(-1)?.endsWith("?")?l.length-1:l.length;if(T?f.length<B:f.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 A=I?.[0],k=de(i,c,E);ue(c,k);const P=A===void 0?null:await n.siteDir(A),v=await p(P,f,k,a),O=y(me,i),j=O&&await O(P,f,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 n.BoardError)return console.error(t.message),t.exitCode??1;throw t}}if(r(fe,"main"),process.argv[1]&&import.meta.url===M(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 fe(e)}export{re as USAGE,fe as main};
684
+ KICKFLIP_PUSH_CMD='["<cmd>","<arg>"]' replaces \`node ../../bin/kickflip.mjs push\`; the flags follow it`;class d extends Error{static{i(this,"UsageError")}}const re=new Set(["new-ticket","done","clear-status","clear-next","urgent","now","clear-eta","held","unheld","idle","wait","hub","clear-install","force","worktree","dispatch","writes"]);function oe(e){const a=Object.create(null),t=[];for(let s=0;s<e.length;s++){const r=e[s];if(!r.startsWith("--")){t.push(r);continue}const o=r.slice(2);if(re.has(o)){if(Object.hasOwn(a,o))throw new d(`--${o} is given twice`);a[o]=!0}else if(s+1<e.length)(a[o]||=[]).push(e[++s]);else throw new d(`--${o} needs a value`)}return{flags:a,rest:t}}i(oe,"parse");function de(e,a,t){const s=new Set(a.filter(l=>l.startsWith("...")).map(l=>l.slice(3))),r=new Set(a.map(l=>l.replace(/^\.\.\./,""))),o=Object.keys(t).filter(l=>!r.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||s.has(l))return[l,c];if(c.length>1)throw new d(`--${l} is given twice`);return[l,c[0]]}))}i(de,"flagsOf");const le=["text","note","notes","heading","summary","title","body","next","progress","install-label","confirmed","asked","task","reason"],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}}i(ce,"looksLikePath");function ue(e,a){for(const t of le){if(typeof a[t]!="string"||!ce(a[t]))continue;const s=e.includes("file")?`; for a file use --file ${a[t].trim()}`:"";throw new d(`--${t} takes text, not a file path (${a[t].trim()})${s}`)}if(typeof a.file=="string"&&a.file!=="-"&&!g.existsSync(a.file))throw new d(`--file ${a.file}: no such file`)}i(ue,"checkInputs");const w=i(e=>g.readFileSync(e==="-"?0:e,"utf8"),"readInput");function q(e){if(!e)throw new d("--file <path> is required");try{return JSON.parse(w(e))}catch(a){throw new n.BoardError(`${e}: ${a.message}`)}}i(q,"readJSONInput");function C(e,a){const t=["notes-file","plan"].find(s=>e[s]!==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}`)}i(C,"refuseNotesFile");function N(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)}i(N,"noteOf");async function pe(e,a,{"owner-step":t,check:s,text:r,...o}){if(t!==void 0&&(!e||o.dir))throw new d("--owner-step goes on a board card: pass --site, not --dir");if(s!==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 n.setItem(e,a,{ownerStep:t,check:s});const l=e&&!o.dir,c=l&&await n.handBackCalls(e,a,o);if(c)return c;if(l&&o.next===void 0&&await n.finishesCard(e,a,o)){if(r===void 0)throw new n.BoardError(`builder done finishes ${a}: pass --text "<what you checked>", or --next "<what happens next>" to stop without finishing`);return $(e,a,{text:r,ownerStep:t})}if(l&&await n.isHelper(e,a,o.agent)){if(r===void 0)throw new d('a helper hands back its result: pass --text "<what you did or found>"');return n.stopBuilder(e,a,{...o,note:r})}if(l&&r!==void 0&&await n.isPlanner(e,a,o.agent))return n.stopBuilder(e,a,{...o,note:r,requireNext:!0});if(r!==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 or a planner's");return n.stopBuilder(e,a,{...o,requireNext:!0})}i(pe,"builderDone");const b={pr:[["file/id"],[],async(e,[a])=>({lines:(await n.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 n.postPR(e,a,{body:t.body})}],"pr reply":[["pr","comment-id"],["body"],(e,[a,t],s)=>{if(s.body===void 0)throw new d('pr reply needs --body "<what Claude says>"');return n.postPR(e,a,{replyTo:t,body:s.body})}],reply:[["eventId"],["text","file","title"],(e,[a],t)=>{if(t.text===void 0==(t.file===void 0))throw new d("reply takes --text or --file");const s=t.text??w(t.file);return a.includes("/")?n.answerCard(e,a,s,t.title):n.writeReply(e,a,s,t.title)}],thread:[["file/id"],["text","link","who","via"],(e,[a],t)=>n.appendMessage(e,a,t)],"visual add":[["file/id"],["file","title"],(e,[a],t)=>n.addVisual(e,a,t)],"topic add":[["id"],["title","summary"],(e,[a],t)=>n.addTopic(e,{id:a,...t})],"question add":[[],["file"],async(e,a,t)=>{const s=q(t.file);return{...await n.addQuestions(e,s,process.env.KICKFLIP_RENDER_CHECK==="off"?void 0:se),asked:[s].flat()}}],"question resolve":[["id"],["note"],(e,[a],t)=>n.resolveQuestion(e,a,t.note)],"question settle":[["id"],["note"],(e,[a],t)=>n.settleQuestion(e,a,t.note)],"question publish":[["key"],[],async(e,[a])=>({ids:await n.publishScreened(e,a)})],"question move":[["ids"],["to","block"],(e,[a],t)=>n.moveQuestions(e,a.split(",").filter(Boolean),t.to,(t.block||"").split(",").filter(Boolean))],"prototype add":[["topic"],["title","...take","card","served","...state"],(e,[a],t)=>n.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=>n.listPrototypes(e)],"prototype rm":[["topic"],[],(e,[a])=>n.removePrototype(e,a)],"item set":[["file/id"],["done","split-into","status","clear-status","next","clear-next","...pr","pushed","gh-since","from-ask","phase","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","new-ticket","text","check"],async(e,[a],t,s)=>(C(t,`add to its notes with item notes ${a} --heading "<heading>" --file <draft.md>`),t.done&&e&&await n.checkNotReopened(e,a),n.setItem(e,a,{done:t.done,splitInto:t["split-into"],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"],phase:t.phase,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,newTicket:t["new-ticket"],text:t.text,check:t.check},s))],"item add":[["file[/id]"],["title","notes","notes-from","deps","notes-file","plan","ticket","new-ticket","pick-up-at","held","parent","size","model","phase","state","relate","from-ask","text"],(e,[a],t,s)=>{if(C(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 n.addItem(e,a,{title:t.title,notes:t.notes,notesFrom:t["notes-from"],deps:t.deps,ticket:t.ticket,newTicket:t["new-ticket"],pickUpAt:t["pick-up-at"],held:t.held,parent:t.parent,size:t.size,model:t.model,phase:t.phase,state:t.state,relate:t.relate,fromAsk:t["from-ask"],text:t.text,intake:!0},s)}],"item notes":[["file/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 <file>/<id>");return n.addNotes(e,a,t)}],"item relate-ask":[["eventId","file/id"],["summary","title"],(e,[a,t],s)=>n.relateAsk(e,a,t,s.summary,s.title)],"ask unfold":[["eventId"],[],(e,[a])=>n.unfoldAsk(e,a)],"ask move":[["eventId"],["to"],(e,[a],t)=>n.moveAsk(e,a,t.to)],"wave create":[["slug"],["title","goal","...card","note","file"],(e,[a],t)=>n.createWave(e,a,{title:t.title,goal:t.goal,cards:t.card,note:N(t)})],"wave add":[["slug","file/id"],["after"],(e,[a,t],s)=>n.changeWave(e,a,{add:t,after:s.after})],"wave remove":[["slug","file/id"],[],(e,[a,t])=>n.changeWave(e,a,{remove:t})],"wave set":[["slug","...file/id?"],["after","title","goal","note","file"],(e,[a,...t],s)=>{if(t.length>1)throw new d("wave set takes <slug> [<file/id>]");return n.changeWave(e,a,{card:t[0],after:s.after,title:s.title,goal:s.goal,note:N(s)})}],"wave finish":[["slug"],[],(e,[a])=>n.finishWave(e,a)],slack:[[],[],()=>({lines:[JSON.stringify(D())]})],"slack set":[[],["self-dm","user","domain","timezone"],async(e,a,t)=>{const s={"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(([r,o])=>[s[r],o]))))]}}catch(r){throw new n.BoardError(r.message)}}],integrations:[[],[],async()=>({lines:[JSON.stringify(await H.rows())]})],"review updated":[["file/id"],["text"],(e,[a],t)=>n.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 n.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 n.postIncomingReview(e,a,{words:w(t.file),readAt:t["read-at"]})}],"review digest":[[],[],async(e,a,t,s)=>({lines:[JSON.stringify(await n.reviewDigest(e,s))]})],"review due":[[],[],async(e,a,t,s)=>({lines:[JSON.stringify(await n.reviewDue(e,s))]})],"reviewers set":[[],["file"],(e,a,t)=>n.setReviewers(e,q(t.file))],"reviewers add":[[],["file"],(e,a,t)=>n.addReviewers(e,q(t.file))],"reviewers remove":[["...name"],[],(e,a)=>n.removeReviewers(e,a)],"rules list":[[],[],e=>n.listRules(e)],"rules add":[[],["text","hub","by"],(e,a,t)=>n.addRuleTo(e,t)],"rules edit":[["id"],["text","hub"],(e,[a],t)=>n.editRuleIn(e,a,t)],"rules remove":[["id"],["hub"],(e,[a],t)=>n.removeRuleFrom(e,a,t)],settings:[[],[],async e=>({lines:[JSON.stringify(await n.readBoardSettings(e))]})],build:[["file/id"],["asked"],async(e,[a],t)=>{if(!e)throw new n.BoardError("build needs --site <route>");n.parseKey(a);const s=await n.askedGate(e,a,t.asked);try{const r=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 r.json();if(!r.ok)throw new n.BoardError(o.error||"The hub refused the build.");return{lines:["started",...(await n.appendMessage(e,a,{text:s})).lines]}}catch(r){throw r instanceof n.BoardError?r:new n.BoardError(`Could not start the build: ${r.message}`)}}],"main set":[[],["install","install-sha"],(e,a,t)=>n.setMainInstall(e,{install:t.install,installSha:t["install-sha"]})],"settings set":[["name","value"],[],(e,[a,t])=>n.setBoardSetting(e,a,t)],"builder start":[["file/id"],["label","phase","progress","dir","agent","kind","model","eta","worktree","repo","dispatch"],(e,[a],{worktree:t,repo:s,dispatch:r,...o})=>{if(s!==void 0&&!t)throw new d("--repo goes with --worktree");const l=i(p=>z(e,a.split("/")[1],{repo:s,...p}),"make"),c=e&&o.dir===void 0&&ae(e);return n.startBuilder(e,a,{...o,dispatch:r?"session":void 0,worktree:t?l:void 0,ownWorktree:c?l:void 0})}],"builder progress":[["file/id","...text?"],["phase","text","dir","agent","eta","clear-eta"],(e,[a,...t],s)=>{if(t.length&&s.text!==void 0)throw new d("builder progress takes its text once: as words or as --text");if(!t.length&&s.text===void 0)throw new d("builder progress needs its text, as words or as --text");return n.builderProgress(e,a,s.text??t.join(" "),{...s,clearEta:s["clear-eta"]})}],"builder done":[["file/id"],["dir","agent","next","owner-step","check","text","updated"],async(e,[a],{updated:t,...s})=>{const r=e&&!s.dir;if(r&&t===void 0&&await n.cardLinked(e,a))throw new n.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,s);return r&&t!==void 0&&t.trim().toLowerCase()!=="none"&&await n.reviewUpdated(e,a,t),o}],"builder pick":[["file/id"],[],(e,[a])=>n.pickBuilder(e,a)],"agent launch":[["file/id"],["name","task","writes","model"],(e,[a],t)=>n.requestHelper(e,a,t)],"agent wait":[["file/id"],["name","agent","timeout"],(e,[a],t)=>n.waitHelper(e,a,{...t,alive:Q})],"agent merge":[["file/id"],["name"],async(e,[a],t)=>{const{record:s,card:r}=await n.helperOf(e,a,t.name);if(!s.done)throw new n.BoardError(`helper ${t.name} is still open; run agent wait ${a} --name ${t.name} first`);if(!s.writes||!s.worktree)throw new n.BoardError(`helper ${t.name} writes no code, so there is nothing to merge`);if(!r)throw new n.BoardError(`${a} has no worktree to merge helper ${t.name} into`);return G(r,s.worktree)}],audit:[["...file/id?"],[],(e,[a,...t])=>{if(t.length)throw new d("audit takes one <file/id> at most");return n.auditLog(e,a)}],"builder call":[["file/id","...text"],["dir","agent"],(e,[a,...t],s)=>n.recordCall(e,a,t.join(" "),s)],"builder calls":[["file/id"],["dir","agent"],(e,[a],t)=>n.listCalls(e,a,t)],"builder ask":[["file/id","...text"],["agent"],(e,[a,...t],s)=>n.askOrchestrator(e,a,t.join(" "),s)],"builder answer":[["file/id","ask"],["text"],(e,[a,t],s)=>n.answerBuilder(e,a,t,s.text)],"builder defer":[["file/id","ask"],["text"],(e,[a,t],s)=>n.answerBuilder(e,a,t,s.text,{deferred:!0})],"builder asks":[["file/id"],["wait","timeout"],(e,[a],t)=>n.listAsks(e,a,t)],"builder verified":[["file/id"],["dir","agent","note"],async(e,[a],t)=>{await n.verifyCalls(e,a,t);const s=e&&!t.dir?await n.verifiedFinish(e,a):null;if(!s)return{};if(s.waits.length)return{lines:[`${await n.cardName(e,a)} finishes once the owner answers ${s.waits.join(", ")}`]};try{return await $(e,a,{text:t.note??"Judgment calls verified."})}catch(r){throw r instanceof n.BoardError?Object.assign(new n.BoardError(`judgment calls verified, but ${await n.cardName(e,a)} didn't finish: ${r.message}`),{exitCode:r.exitCode}):r}}],land:[["file/id"],["repo","dir","text","owner-step","check"],async(e,[a],t)=>{const s=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 r=e?await n.cardName(e,a):void 0,o=await U(r??a,async()=>{e&&await n.checkClose(e,a,t.text,t["owner-step"],t.check),await n.checkCallsVerified(e,a,{dir:t.dir});const l=await n.recordedWorktree(e,a,{dir:t.dir}),{repo:c,base:p}=J(e,{repo:t.repo??l?.repo});return await F({path:l?.path??x.join(K(),s),branch:l?.branch??s,repo:x.resolve(c),base:p,ref:r}),e?n.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 ${r??a}`]}:o}],"builder stop":[["file/id"],[],(e,[a])=>ee(m(e,"builder stop"),a)],"item stall":[["file/id"],["reason"],(e,[a],t)=>Y(m(e,"item stall"),a,t.reason)],"land stop":[["file/id"],[],(e,[a])=>te(m(e,"land stop"),a)],"land queue":[[],[],()=>Z()],"item bump":[["file/id"],[],(e,[a])=>S(m(e,"item bump"),a,!0)],"item unbump":[["file/id"],[],(e,[a])=>S(m(e,"item unbump"),a,!1)],lander:[["file/id"],[],async(e,[a])=>{if(!e)throw new d("lander needs --site <route>");return await n.relaunchLander(e,a),{lines:[`${await n.cardName(e,a)} is handed back again; the server launches a lander for it within a minute`]}}],merge:[["file/id"],["after-approval","confirmed"],async(e,[a],t)=>e&&await n.heldMerge(e,a)?X(e,a):n.mergeCard(e,a,{afterApproval:t["after-approval"],confirmed:t.confirmed})],"arch file":[["file/id"],[],(e,[a])=>e?V(e,a):Promise.reject(new n.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 n.sendPush(e,t)}]};b.install=b.build;function m(e,a){if(!e)throw new d(`${a} needs --site <route>`);return e}i(m,"needSite");const h=i((e,a)=>n.cardName(e,a),"card"),R=i((e,a)=>`${a}${e.length===1?"":"s"} ${e.join(", ")}`,"plural"),T=i(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&&(e["split-into"]?`done, split into ${e["split-into"].split(",").join(", ")}`:"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.phase,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["new-ticket"]&&"ticket made",e.check!==void 0&&(e.check.trim()?`check ${e.check.trim().split(/\s+/).join(" ")}`:"check removed")].filter(Boolean).join(", ")}i(we,"itemChanges");const me={reply:i(async(e,[a])=>a.includes("/")?`answered ${await h(e,a)}: it waits in In Review for the owner's Done`:`reply written to ${await n.eventCard(e,a)}`,"reply"),thread:i(async(e,[a])=>`thread line on ${await h(e,a)}`,"thread"),"topic add":i((e,[a])=>`topic ${a} added`,"topic add"),"question add":i(async(e,a,t,{asked:s,staged:r})=>{const o=[...new Set(s.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`${R(s.map(c=>c.id),"question")} ${r===s.length?"staged":"added"}${l}`},"question add"),"question resolve":i((e,[a])=>`question ${a} resolved`,"question resolve"),"question settle":i((e,[a])=>`question ${a} settled`,"question settle"),"question publish":i(async(e,[a],t,{ids:s})=>`${R(s,"question")} on ${await h(e,a)} passed to the owner`,"question publish"),"question move":i((e,[a],t)=>`questions ${a} merged onto ${t.to}`,"question move"),"prototype add":i(async(e,[a],t)=>`prototype ${a} recorded${t.card?` for ${await h(e,t.card)}`:""}`,"prototype add"),"prototype rm":i((e,[a])=>`prototype ${a} removed`,"prototype rm"),"item set":i(async(e,[a],t)=>`${await h(e,a)}: ${we(t)}`,"item set"),"item add":i(async(e,a,t,{key:s})=>`${await h(e,s)} added as ${s}`,"item add"),"item notes":i(async(e,[a],t,{file:s,heading:r})=>`${await h(e,a)}: notes section "${r}" added to ${s}`,"item notes"),"item relate-ask":i(async(e,[a,t])=>`task ${a} folded into ${await h(e,t)}`,"item relate-ask"),"ask unfold":i((e,[a])=>`task ${a} back on a card of its own`,"ask unfold"),"ask move":i((e,[a],t)=>`task ${a} moved to /${t.to}`,"ask move"),"wave create":i((e,[a])=>`wave ${a} created`,"wave create"),"wave add":i(async(e,[a,t])=>`${await h(e,t)} added to wave ${a}`,"wave add"),"wave remove":i(async(e,[a,t])=>`${await h(e,t)} taken out of wave ${a}`,"wave remove"),"wave set":i((e,[a])=>`wave ${a} updated`,"wave set"),"wave finish":i((e,[a])=>`wave ${a} finished`,"wave finish"),"review record":i((e,[a],t)=>`review of PR #${a} recorded: ${t.state==="needs"?"needs you":"posted"}`,"review record"),"reviewers set":i(()=>"reviewers replaced","reviewers set"),"reviewers add":i(()=>"reviewers added","reviewers add"),"reviewers remove":i((e,a)=>`${R(a,"reviewer")} removed`,"reviewers remove"),"rules add":i((e,a,t)=>`${t.hub?"hub":"board"} rule added`,"rules add"),"rules edit":i((e,[a],t)=>`${t.hub?"hub":"board"} rule ${a} reworded`,"rules edit"),"rules remove":i((e,[a],t)=>`${t.hub?"hub":"board"} rule ${a} removed`,"rules remove"),"builder start":i(async(e,[a],t,s)=>`${await h(e,a)}: ${s.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":i(async(e,[a])=>`progress line on ${await h(e,a)}`,"builder progress"),"builder done":i(async(e,[a],t,s)=>`${await h(e,a)}: ${s?.said??`${T(t)} done${t["owner-step"]===void 0?"":`; owner step "${t["owner-step"].trim()}"`}`}`,"builder done"),"builder call":i(async(e,[a])=>`judgment call recorded on ${await h(e,a)}`,"builder call"),"builder ask":i(async(e,[a],t,{id:s})=>`${await h(e,a)}: asked the orchestrator, ${s}`,"builder ask"),"builder answer":i(async(e,[a,t])=>`${await h(e,a)}: ${t} answered`,"builder answer"),"builder defer":i(async(e,[a,t])=>`${await h(e,a)}: ${t} deferred to the owner`,"builder defer"),"builder verified":i(async(e,[a],t,s)=>`${await h(e,a)}: judgment calls verified${s?.said?`; ${s.said}`:""}`,"builder verified"),land:i(async(e,[a],t,s)=>e?s?.said?`${await h(e,a)}: ${s.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:i(async(e,[a],t,s)=>`${await h(e,a)}: ${s?.said??"merged"}`,"merge"),"arch file":i(async(e,[a])=>`findings of ${await h(e,a)} filed`,"arch file")};async function fe(e,a={}){try{const{flags:t,rest:s}=oe(e),r=[3,2,1].map(u=>s.slice(0,u).join(" ")).find(u=>y(b,u)),o=y(b,r);if(!o)throw new d(s.length?`unknown command "${s.slice(0,2).join(" ")}"`:"no command given");const[l,c,p]=o,f=s.slice(r.split(" ").length),_=l.at(-1)?.startsWith("..."),B=l.at(-1)?.endsWith("?")?l.length-1:l.length;if(_?f.length<B:f.length!==l.length)throw new d(`${r} takes ${l.map(u=>`<${u.replace(/^\.\.\.|\?$/g,"")}>`).join(" ")||"no arguments"}`);const{site:A,...E}=t;if(A?.length>1)throw new d("--site is given twice");const I=A?.[0],k=de(r,c,E);ue(c,k);const P=I===void 0?null:await n.siteDir(I),v=await p(P,f,k,a),O=y(me,r),j=O&&await O(P,f,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 n.BoardError)return console.error(t.message),t.exitCode??1;throw t}}if(i(fe,"main"),process.argv[1]&&import.meta.url===L(g.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};