@lovek4d/kickflip 0.7.7 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/board/store.mjs CHANGED
@@ -1,38 +1,38 @@
1
- var No=Object.defineProperty;var r=(e,t)=>No(e,"name",{value:t,configurable:!0});import j from"node:fs/promises";import{constants as Ro,existsSync as de,readFileSync as Eo}from"node:fs";import g from"node:path";import xn from"node:os";import{execFile as Lo}from"node:child_process";import{nodeBin as Bo}from"../bin/install.mjs";import{gh as Co,limitWords as Io}from"../shared/gh.mjs";import{fileURLToPath as To}from"node:url";import{BUILD_REFUSED as Oa,BUILDER_ASK as Tt,COMMANDS as qt,LANDER as Aa,LANES as Ce,MOVE as Pa,PAUSE_CLEARS as Da,PHASES as _n,handedBackAt as Sn,MAX_IMPLEMENTERS as qo,MAX_PLANNERS as Mo,builds as et,waveItems as Ie,mergeBlockers as Wo,modelName as Mt,noTakePicked as Uo,ownerQuestion as Te,plans as qe,QUESTION_REOPEN as Ho,PLANNER_WORD as Jo,providerOf as _t,owesNext as vn,pushTag as zo,refOf as tt,roundOf as jn,roundSettledBy as Ko,HELPER as re,VISUAL as me,visualAgent as Qo}from"./domain.mjs";import{GITHUB_STATE as On,appendEvent as he,listBoards as ge,readManifest as Go,samePath as Pe,withManifestLock as Vo,writeManifest as Zo}from"../hub/boards.mjs";import{readUsageGate as Fa,pauseReason as Yo,pauseReasonFor as Xo,heldFor as es,holdsWork as ts,builderPick as An,pauseEnd as ns}from"../shared/usage-gate.mjs";import{reviseQueued as as,pushKindError as is,readClaudeModel as Pn}from"../hub/push.mjs";import{AT_RE as os}from"../hub/duration.mjs";import{routeRecord as ae}from"../hub/routes.mjs";import{ticketSource as Na,resolveTicket as ss,ticketTarget as rs,moveTicket as cs,cardIdOf as ls}from"./tickets.mjs";import{LINEAR_NEEDS_KEY as ds,linearStatus as us,linearWorks as fs}from"../hub/linear.mjs";import{validateBranch as ws}from"../hub/repos.mjs";import{withFileLock as nt}from"../hub/locks.mjs";import{audit as ps,auditLines as ms,boardAudit as Ra,readAudit as hs}from"../shared/audit.mjs";import{merge3 as gs}from"./merge3.mjs";import{HUB as Ea,MESSAGE_LIMIT as La,BRIEF_LINE_LIMIT as Ba,hubLineOf as $s,hubText as Ca}from"./hub-lines.mjs";import{fetchPR as Ia,reportOf as Ta,githubLogin as ks,postToPR as bs,postReview as ys,reviewsSince as xs,CLAUDE_PREFIX as St,CLAUDE_MARK as Dn}from"../hub/pr-report.mjs";import{allLanded as _s,cardPRs as Me,hasPR as be,parsePRRef as Fn,prKeyOf as vt,prNames as at,prURL as Nn,storedPRs as De}from"../shared/card-prs.mjs";import{reviewLinks as Ss,topicItems as vs,reviewersOf as Rn}from"./review.mjs";import{readiness as qa}from"../hub/readiness.mjs";import{slackDay as js}from"../hub/slack.mjs";import"../bin/install.mjs";import{readPermissions as Os}from"./permissions.mjs";import{validBuildTarget as Ma}from"./build-script.mjs";import{parseCheck as As,checkText as Ps}from"../shared/owner-check.mjs";import{RuleError as Ds,rulesOf as Wa,addRule as Fs,editRule as Ns,removeRule as Rs,applyRules as Es,readHubRules as Wt,writeHubRules as Ls}from"../hub/rules.mjs";import{shareEntries as Bs,validLogin as Cs}from"../hub/share.mjs";class ue extends Error{static{r(this,"BoardError")}}const d=r(e=>{throw new ue(e)},"fail"),Is=r(()=>g.join(xn.homedir(),".claude","kickflip","sites"),"sitesDir"),Ut=r(()=>g.join(xn.homedir(),".claude","waves"),"wavesDir"),Ua=Symbol("read as"),$=r(async(e,t)=>{let n;try{n=await j.readFile(e,"utf8")}catch{return t}try{const a=JSON.parse(n);return a!==null&&typeof a=="object"&&!Array.isArray(a)?{...a,[Ua]:n}:a}catch{return t}},"readJSON"),z=r(async e=>{try{return(await j.readFile(e,"utf8")).split(`
2
- `).filter(Boolean).map(t=>JSON.parse(t))}catch{return[]}},"readLines");async function We(e,t){await j.mkdir(g.dirname(e),{recursive:!0});const n=`${e}.${process.pid}.tmp`;await j.writeFile(n,t),await j.rename(n,e)}r(We,"replace");const y=r((e,t)=>{const{[Ua]:n,...a}=t??{};return n===void 0?We(e,JSON.stringify(t,null,2)+`
3
- `):nt(e,async()=>{const i=JSON.parse(n),o=JSON.parse(await j.readFile(e,"utf8").catch(()=>n));await We(e,JSON.stringify(gs(i,a,o),null,2)+`
4
- `)})},"writeJSON");async function le(e,t){await j.mkdir(g.dirname(e),{recursive:!0}),await j.appendFile(e,JSON.stringify(t)+`
5
- `)}r(le,"appendLine");async function Xc(e){/^[\w-]+$/.test(e||"")||d(`--site takes a route such as ios-orchestrator, not "${e}"`);const t=g.join(Is(),e);return await j.stat(t).then(n=>n.isDirectory(),()=>!1)||d(`no site at ${t}`),t}r(Xc,"siteDir");const oe=r(async e=>({title:"Board",items:"waves",...e?await $(g.join(e,"board.json"),{}):{}}),"boardConfig");async function q(e){const t=await oe(e);if(t.items==="waves")try{return(await j.readdir(Ut(),{withFileTypes:!0})).filter(a=>a.isDirectory()).map(a=>({slug:a.name,dir:g.join(Ut(),a.name),file:g.join(Ut(),a.name,"wave.json")}))}catch{return[]}const n=g.resolve(e,t.items);return[{slug:t.slug||g.basename(t.items,".json"),dir:g.dirname(n),file:n}]}r(q,"itemSources");const Z=r((e,t)=>g.join(e,"data","threads",t.replace(/[^\w.-]+/g,"__")+".jsonl"),"threadFile"),E=r(e=>g.join(e,"data","owner.json"),"ownerFile"),ye=r(e=>g.join(e,"data","staged-questions.json"),"stagedFile"),it=r(e=>g.join(e,"data","screening.json"),"screeningFile"),Ts=900*1e3,D=r(e=>e||d("this command needs --site <route>"),"needSite"),S=r((e,t)=>typeof e=="string"&&e.trim()?e.trim():d(`${t} must be non-empty text`),"text"),ee=r((e,t)=>e===void 0?void 0:t(e),"optional"),qs=/^prototype-[\w-]+\.html(?:[?#]\S*)?$/,Ms=/^visuals\/[\w-]+\/[\w-]+\.html$/,En=r((e,t)=>typeof e=="string"&&(qs.test(e)||Ms.test(e)||/^https?:\/\/\S+$/.test(e))?e:d(`${t} must be an http(s) URL or a site page such as prototype-<topic>.html?q=<id>`),"pageUrl"),Ht=r((e,t)=>typeof e=="boolean"?e:d(`${t} must be true or false`),"bool"),Fe=r((e,t)=>typeof e=="string"&&/^[\w.-]+$/.test(e)?e:d(`${t} must use only letters, digits, "_", "." and "-"`),"domId"),ot=r((e,t,n)=>{(!e||typeof e!="object"||Array.isArray(e))&&d(`${n} must be a JSON object`);const a=Object.keys(e).filter(i=>!t.includes(i));a.length&&d(`${n} has unknown field ${a.map(i=>`"${i}"`).join(", ")}; allowed: ${t.join(", ")}`)},"onlyKeys");function F(e){const t=/^([^/\s]+)\/([^/\s]+)$/.exec(e||"");return t?{file:t[1],id:t[2]}:d(`expected <file>/<id>, not "${e}"`)}r(F,"parseKey");async function Ue(e,t){const n=(await q(e)).find(i=>i.slug===t)||d(`this board has no item file "${t}"`),a=await $(n.file,null);return(!a||!Array.isArray(a.items))&&d(`${n.file} is not an item file with items[]`),{source:n,itemFile:a}}r(Ue,"loadItemFile");async function x(e,t){const{file:n,id:a}=F(t);let i=await Ue(e,n);!i.itemFile.items.some(s=>s.id===a)&&await Vr(e,t)&&(i=await Ue(e,n));const o=i.itemFile.items.find(s=>s.id===a)||d(`item file "${n}" has no item "${a}"`);return{...i,item:o}}r(x,"loadItem");const Jt=["md","html","json"];async function Ln(e,t){const n=new RegExp(`^${t.replace(/[.]/g,"\\.")}(?:~(\\d+))?\\.(${Jt.join("|")})$`);return(await j.readdir(e).catch(()=>[])).flatMap(a=>{const i=n.exec(a);return i?[{name:a,ext:i[2],n:Number(i[1]||1)}]:[]}).sort((a,i)=>a.n-i.n||Jt.indexOf(a.ext)-Jt.indexOf(i.ext)).filter((a,i,o)=>a.n!==o[i-1]?.n)}r(Ln,"replyFilesOf");async function el(e,t,n,a){D(e),Fe(t,"the event id");const i=(await z(g.join(e,"data","events.jsonl"))).find(h=>h.id===t);i||d(`no event "${t}" in this site's data/events.jsonl`);const o=i.kind==="ask"?await ze(e,t):null,s=o?o.key:i.kind==="ask"?`ask:${t}`:i.kind==="item-message"&&typeof i.data?.key=="string"?i.data.key:null;o&&ai(o.item)?(S(n,"the reply"),await ii(e,o,a)):a!==void 0&&d(`reply --title titles the New or answered card an ask became; "${t}" is not one`);const c=g.join(e,"data","replies"),l=await Ln(c,t),u=g.join(c,l.length?`${t}~${l.at(-1).n+1}.md`:`${t}.md`);await We(u,S(n,"the reply"));const p=s?await Cn(e,s,n):[],w=i.kind==="wave-triage"?["wave","file"].filter(h=>typeof i.data?.[h]=="string").map(h=>`${h}:${i.data[h]}`):[],f=o?`item:${o.key}`:w[0];return f&&p.push(...await Ei(e,f,"Acted on: replied")),{file:u,lines:p}}r(el,"writeReply");const Ha=r(e=>g.join(e,"data","working.json"),"workingFile");async function Ws(e){return(await $(Ha(e),{})).events||{}}r(Ws,"readWorking");const tl=["ask","item-message","wave-triage","overnight-triage"];async function nl(e,t){if(!t.length)return;const n=new Date().toISOString(),a=await Ws(e);await y(Ha(e),{events:{...a,...Object.fromEntries(t.map(i=>[i,ae(a,i)||n]))}})}r(nl,"markWorking");const Bn=r(e=>g.join(e,"data","push-state.json"),"pushStateFile");async function Us(e,t){D(e);const n=Date.parse((await $(Bn(e),{})).last_push);if(Number.isNaN(n))return 0;const a=g.join(e,"data","replies"),i=(await j.readdir(a).catch(()=>[])).filter(s=>Jt.includes(g.extname(s).slice(1)));return(await Promise.all(i.map(s=>j.stat(g.join(a,s)).then(c=>c.mtimeMs,()=>0)))).filter(s=>s>n&&s<=t.getTime()).length}r(Us,"repliesSincePush");async function Hs(e,t){return await y(Bn(D(e)),{last_push:t.toISOString()}),Bn(e)}r(Hs,"recordPush");const Js=g.join(g.dirname(To(import.meta.url)),"..","bin","kickflip.mjs");function zs(){if(!process.env.KICKFLIP_PUSH_CMD)return[Bo(),Js,"push"];try{const e=JSON.parse(process.env.KICKFLIP_PUSH_CMD);if(Array.isArray(e)&&e.length&&e.every(t=>typeof t=="string"))return e}catch{}throw new ue("KICKFLIP_PUSH_CMD must be a JSON array of strings")}r(zs,"pushCommand");async function al(e,t){const n=is(t.kind);n&&d(n),t.title===void 0&&d("push needs --title");const a=new Date,i=Qs(t.body,await Us(e,a)),o={kind:t.kind,title:t.title,body:i||void 0,url:Vs(t.url),tag:t.tag},[s,...c]=zs(),l=[...c,...Object.entries(o).flatMap(([w,f])=>f===void 0?[]:[`--${w}`,f]),...t.urgent?["--urgent"]:[],...t.now?["--now"]:[]],u=await new Promise(w=>{Lo(s,l,{encoding:"utf8"},(f,h,m)=>w({error:f,said:`${h||""}${m||""}`.split(`
6
- `).filter(k=>k.trim())}))}),p=u.error?typeof u.error.code=="number"?u.error.code:1:0;if(p!==0)throw Object.assign(new ue([...u.said,`kickflip push failed (${typeof u.error.code=="number"?`exit ${p}`:u.error.message}); the push was not recorded`].join(`
7
- `)),{exitCode:p});return await Hs(e,a),{lines:u.said}}r(al,"sendPush");const Ks=/(?:^| · )(and \d+ repl(?:y|ies))$/,Qs=r((e,t)=>[e,t&&`and ${t} ${t===1?"reply":"replies"}`].filter(Boolean).join(" \xB7 "),"withReplies"),Ja=160;function Gs(e){const t=String(e).split(`
8
- `).map(n=>n.replace(/\s+/g," ").trim()).find(Boolean)||"";return t.length>Ja?`${t.slice(0,Ja-1).trimEnd()}\u2026`:t}r(Gs,"pushLine");function Vs(e){if(!e||!e.startsWith("/"))return e;const t=new URL(e,"http://board");return/^#(?:selectedCard|item)=[^&]+$/.test(t.hash)&&(t.hash=`${t.hash}&reply`),/\/[\w-]+\.html$/.test(t.pathname)&&!t.pathname.endsWith("/index.html")&&t.searchParams.set("from","push"),t.pathname+t.search+t.hash}r(Vs,"pushUrl");const Zs=r((e,t)=>`/${g.basename(e)}/#selectedCard=${encodeURIComponent(t)}&reply`,"threadUrl");async function Cn(e,t,n){const a=Gs(n);if(!a)return[];const i=zo(t),o=await as(i,s=>[a,Ks.exec(s)?.[1]].filter(Boolean).join(" \xB7 "),Zs(e,t));return o===null?[]:[`queued push ${i} now reads: ${o}`]}r(Cn,"freshenPush");const za=r(e=>g.join(e,"data","hidden.json"),"hiddenFile"),Ys=r(async e=>new Set((await $(za(e),{events:[]})).events||[]),"readHidden");let Ka=Promise.resolve();function il(e,t,n){Fe(t,"the event id");const a=Ka.then(async()=>{const i=await Ys(e);n?i.add(t):i.delete(t),await y(za(e),{events:[...i]})});return Ka=a.catch(()=>{}),a}r(il,"setHidden");const Qa=r(e=>g.join(e,"data","read.json"),"readMarksFile"),Xs=r(async e=>(await $(Qa(e),{})).cards||{},"readMarks");let Ga=Promise.resolve();function ol(e,t,n={}){const a=Ga.then(async()=>{const i=await Xs(e),o=new Date().toISOString(),s=r((l,u)=>String(u||"")>l?u:l,"later"),c=Object.fromEntries([...new Set(t)].sort().map(l=>[l,s(i[l]||o,n[l])]));JSON.stringify(c)!==JSON.stringify(i)&&await y(Qa(e),{cards:c})});return Ga=a.catch(()=>{}),a}r(ol,"markRead");const In=20*1024*1024,He=r(e=>g.join(e,"data","uploads"),"uploadsDir"),st=r((e,t)=>g.join(He(e),".meta",`${t}.json`),"uploadMeta"),Va=/^\d+-[\w.-]+$/,Za=/^[\w.+-]+\/[\w.+-]+$/,Ya=r(()=>Object.assign(new ue(`a file can be at most ${In/1024/1024} MB`),{status:413}),"tooLarge");async function sl(e,t,{name:n,type:a,length:i}){if(D(e),i>In)throw Ya();const o=String(n||"").trim().slice(0,200)||"file",s=g.basename(o).replace(/[^\w.-]+/g,"-").replace(/^[.-]+/,"").slice(-80)||"file",c=String(a||"").split(";")[0].trim().toLowerCase(),l=Za.test(c)?c:"application/octet-stream";await j.mkdir(g.dirname(st(e,"x")),{recursive:!0});let u,p;for(let h=Date.now();!p;h++)u=`${h}-${s}`,p=await j.open(g.join(He(e),u),"wx").catch(m=>m.code==="EEXIST"?null:Promise.reject(m));const w=g.join(He(e),u);let f=0;try{for await(const h of t){if(f+=h.length,f>In)throw Ya();await p.write(h)}}catch(h){throw await p.close(),await j.rm(w,{force:!0}),h}return await p.close(),await y(st(e,u),{name:o,type:l,size:f,at:new Date().toISOString()}),{name:o,url:`uploads/${u}`,path:w,type:l,size:f}}r(sl,"saveUpload");async function rl(e,t){if(!Va.test(t||""))return null;const n=g.join(He(e),t),a=await j.stat(n).catch(()=>null);if(!a?.isFile())return null;const i=await $(st(e,t),{});return{path:n,name:i.name||t,type:Za.test(i.type||"")?i.type:"application/octet-stream",size:a.size}}r(rl,"readUpload");const Xa=["github","slack"],ei=r(async(e,t)=>String(t).startsWith("ask:")?Dt(e,t.slice(4),"the ask"):x(e,t),"checkThreadKey");async function W(e,t,n,a={},{link:i}={}){D(e),await ei(e,t);const o={ts:new Date().toISOString(),who:"claude",kind:Ea,line:n,text:Ca(n,a),...i?{link:i}:{}};return await le(Z(e,t),o),{line:o,lines:$s(o).fault?await Cn(e,t,o.text):[]}}r(W,"appendStatus");async function rt(e,t,{text:n,link:a,log:i,who:o="claude",via:s,by:c}){D(e),await ei(e,t),/^[^\n]{1,40}$/.test(o.trim())||d("--who takes claude, love or a reviewer's name"),s!==void 0&&!Xa.includes(s)&&d(`--via takes ${Xa.join(" or ")}`);const l=s===void 0?zt(n):S(n,"--text"),u={ts:new Date().toISOString(),who:o.trim(),text:l,link:ee(a,w=>En(w,"--link")),log:i,via:s,by:c},p=Z(e,t);return await le(p,u),{file:p,lines:u.who==="claude"&&!s?await Cn(e,t,u.text):[]}}r(rt,"appendMessage");function zt(e){const t=S(e,"--text");return t.length>La&&d(`--text runs to ${t.length} characters, past a message's ${La}: rewrite it shorter, or put the detail in an HTML page (board.mjs visual add, or --link to a page) and link it`),t}r(zt,"messageText");async function Je(e,t,{command:n,text:a=qt[n],who:i="claude",by:o,card:s}){const c={ts:new Date().toISOString(),who:i,command:n,text:a,...o?{by:o}:{},...s?{card:s}:{}};return await le(Z(e,t),c),{lines:[]}}r(Je,"appendCommand");const xe=r(e=>g.join(e,"data","ask-links.json"),"askLinksFile");async function _e(e){return(await $(xe(e),{})).links||{}}r(_e,"readAskLinks");async function H(e,t){const{tickets:n}=Se(await oe(e)),a=await q(e),i=/^ask:(.+)$/.exec(t)?.[1];if(i===void 0){const s=/^([^/\s]+)\/([^/\s]+)$/.exec(t);if(!s)return t;const c=a.find(p=>p.slug===s[1]),l=c&&await $(c.file,null);if(!l)return s[2];const u=(l.items||[]).find(p=>p.id===s[2]);return tt(l,u||{id:s[2]},n)}const o=e?ae(await _e(e),i):void 0;if(o)return H(e,o);for(const s of a){const c=await $(s.file,null),l=(Array.isArray(c?.items)?c.items:[]).find(u=>u.from_ask===i);if(l)return tt(c,l,n)}return`task ${i}`}r(H,"cardName");async function cl(e,t){const n=(await z(g.join(e,"data","events.jsonl"))).find(a=>a.id===t);return n?.kind==="ask"?H(e,`ask:${t}`):typeof n?.data?.key=="string"?H(e,n.data.key):`the ${n?.kind} event`}r(cl,"eventCard");async function ze(e,t){for(const n of await q(e)){const a=await $(n.file,null),i=(Array.isArray(a?.items)?a.items:[]).find(o=>o.from_ask===t);if(i)return{source:n,itemFile:a,item:i,key:`${n.slug}/${i.id}`}}return null}r(ze,"askCardOf");const Tn=r(e=>!e.phase&&!e.answered_at&&!we(e)&&!be(e)&&!e.filed_held,"stillNew");function qn(e,t){const n=t.filter(i=>i.kind==="ask-edited"&&i.data?.id===e.id&&typeof i.data?.text=="string").sort((i,o)=>String(i.ts).localeCompare(String(o.ts))).at(-1);return String(n?n.data.text:e.data?.text||"").trim().split(`
9
- `)[0].trim()||(e.data?.files||[]).map(i=>i?.name).filter(Boolean).join(", ")||"Ask"}r(qn,"askTitle");const ll=r((e,t,n)=>ce(e,async()=>{D(e);const a=await z(g.join(e,"data","events.jsonl")),i=n||a.find(l=>l.id===t&&l.kind==="ask")||d(`no ask "${t}" in this site's data/events.jsonl`);if(await ze(e,t)||await ri(e,t,a))return null;const{source:o,itemFile:s}=await ti(e),c=await ni(e,o,i,qn(i,a));return s.items.push(c),await y(o.file,s),`${o.slug}/${c.id}`}),"fileAsk");async function ti(e){const t=await q(e);for(const n of t){const a=await $(n.file,null);if(a)return Array.isArray(a.items)||d(`${n.file} is not an item file with items[]`),{source:n,itemFile:a}}return(await oe(e)).items==="waves"&&d("this board has no wave yet to file a new task in; start a wave first"),await j.mkdir(t[0].dir,{recursive:!0}),{source:t[0],itemFile:{items:[]}}}r(ti,"askFile");async function ni(e,t,n,a){const i=new Date().toISOString(),o={id:await no(e),title:a,created:i,lane:"todo",lane_at:i,from_ask:n.id};return o.plan=Zt(t.dir,o.id),await Yt(g.join(t.dir,o.plan),`# ${o.title}
10
- `),o}r(ni,"newAskCard");const dl=r(e=>ce(e,async()=>{D(e);const t=r((...m)=>g.join(e,"data",...m),"data"),n=await z(t("events.jsonl")),a=await _e(e),i=new Map(Object.entries(a));for(const m of await q(e))for(const k of(await $(m.file,{})).items||[])k.from_ask&&i.set(k.from_ask,`${m.slug}/${k.id}`);const o=new Set(n.filter(m=>m.kind==="ask-withdrawn").map(m=>m.data?.id)),s=n.filter(m=>m.kind==="ask"&&!o.has(m.id)&&!i.has(m.id)).sort((m,k)=>String(m.ts).localeCompare(String(k.ts))),c=(await $(t("ask-progress.json"),{})).asks||{},l=(await $(t("ask-titles.json"),{})).titles||{},u=new Map(n.filter(m=>m.kind==="ask-done").map(m=>[m.data?.id,String(m.ts)])),p=[];let w;for(const m of s){const k=await z(Z(e,`ask:${m.id}`));if(k.some(A=>A.line==="ask-moved"))continue;const _=await Promise.all((await Ln(t("replies"),m.id)).map(A=>j.stat(t("replies",A.name)).then(I=>I.mtime.toISOString()))),N=k.filter(A=>(A.who||"claude")==="claude"&&A.kind!=="status").map(A=>String(A.ts)),v=[..._,...N].sort().pop()||[ae(c,m.id)?.at,u.get(m.id)].filter(Boolean).sort().pop();w||=await ti(e);const O=await ni(e,w.source,m,ae(l,m.id)||qn(m,n));O.created=m.ts,v&&Ft(O,"done",v),w.itemFile.items.push(O),await y(w.source.file,w.itemFile),i.set(m.id,`${w.source.slug}/${O.id}`),p.push(i.get(m.id))}const f=r(m=>i.get(/^item:ask:(.+)$/.exec(m||"")?.[1]),"cardOf"),h=await $(E(e),null);if(h?.items?.some(m=>f(m.topic))){for(const m of h.items)f(m.topic)&&(m.topic=`item:${f(m.topic)}`);await y(E(e),h)}for(const m of["ask-progress.json","ask-titles.json"])await j.rm(t(m),{force:!0});return p}),"fileAsks"),ul=r((e,t)=>ce(e,async()=>{const n=await ze(D(e),t);if(!n||!Tn(n.item))return null;const a=await z(g.join(e,"data","events.jsonl"));return n.item.title=qn(a.find(i=>i.id===t&&i.kind==="ask"),a),await y(n.source.file,n.itemFile),n.key}),"retitleAskCard");async function Mn(e,{source:t,itemFile:n,item:a,key:i}){n.items=n.items.filter(l=>l!==a);const o=Number(/^T-([1-9]\d*)$/.exec(a.id)?.[1]||0);o>(Number(n.last_task)||0)&&(n.last_task=o),await y(t.file,n),a.plan&&await j.rm(g.join(t.dir,a.plan),{force:!0});const s=Z(e,i),c=await z(s);return await j.rm(s,{force:!0}),c}r(Mn,"dropCard");const fl=r((e,t)=>ce(e,async()=>{const n=await ze(D(e),t);return!n||!Tn(n.item)?null:(await Mn(e,n),n.key)}),"dropAskCard"),ai=r(e=>!e.phase&&!e.closed_at&&!be(e)&&(!we(e)||!!e.answered_at),"answerable");async function ii(e,t,n){const{item:a,key:i}=t;a.from_ask||d(`${i} is not a card an ask became; write in its thread with thread ${i} --text`),ai(a)||d(`${i} is sorted (${a.phase||Ce[a.lane]}), so it is work: write in its thread with thread ${i} --text`),n!==void 0&&(a.title=S(n,"--title")),a.answered_at||=new Date().toISOString(),await y(t.source.file,t.itemFile)}r(ii,"markAnswered");async function oi(e,t,n){const a=await ze(e,t);a&&a.key!==n&&d(`ask "${t}" is already card ${a.key}: sort it with item set ${a.key} --phase, or merge it with item relate-ask ${t} <card>`)}r(oi,"refuseFiled");async function wl(e,t,n,a){D(e);const{source:i,itemFile:o,item:s}=await x(e,t),c={source:i,itemFile:o,item:s,key:t};return zt(n),a!==void 0&&S(a,"--title"),await ii(e,c,a),rt(e,t,{text:n})}r(wl,"answerCard");async function pl(e,t,n,a,i){D(e),Fe(t,"the event id"),(await z(g.join(e,"data","events.jsonl"))).some(l=>l.id===t&&l.kind==="ask")||d(`no ask "${t}" in this site's data/events.jsonl`),await Wn(e,t),await x(e,n),S(a,"--summary"),i!==void 0&&S(i,"--title");const s=await ze(e,t);if(s&&s.key===n&&d(`ask "${t}" is ${n} already`),s&&!Tn(s.item)&&d(`ask "${t}" became ${s.key}, which is sorted; to fold that card's notes into ${n}, use item notes ${n} --from ${s.key}, then close it`),s){const l=await ce(e,()=>Mn(e,s)),u=Z(e,`ask:${t}`);for(const p of l)await le(u,p);await le(u,{ts:new Date().toISOString(),who:"claude",kind:Ea,line:"card-merged",text:Ca("card-merged",{card:await H(e,n)}),into:n,title:i??s.item.title,link:`#selectedCard=${encodeURIComponent(n)}`})}const c=await _e(e);return await y(xe(e),{links:{...c,[t]:n}}),await rt(e,n,{text:a}),xe(e)}r(pl,"relateAsk");async function ml(e,t){await Dt(e,t,"the ask");const n=await _e(e);return ae(n,t)===void 0&&d(`ask "${t}" is not folded into a card`),delete n[t],await y(xe(e),{links:n}),xe(e)}r(ml,"unfoldAsk");const si=r(async(e,t)=>(await z(Z(e,`ask:${t}`))).find(n=>n.line==="ask-moved"),"movedLine");async function Wn(e,t){const n=await si(e,t);n&&d(`ask "${t}" was moved to ${n.link}; it lives there now`)}r(Wn,"refuseMoved");async function ri(e,t,n){const a=`ask "${t}"`;if(n.some(l=>l.kind==="ask-withdrawn"&&l.data?.id===t))return{why:`${a} was deleted by the owner`};const i=await si(e,t);if(i)return{why:`${a} was already moved to ${i.link}`};const o=ae(await _e(e),t);if(o)return{why:`${a} is folded into ${o}; run ask unfold ${t} first`};const s=[];for(const l of await q(e)){const u=await $(l.file,null);for(const p of Array.isArray(u?.items)?u.items:[])p.from_ask===t&&s.push({key:`${l.slug}/${p.id}`,it:p})}const c=s.find(({it:l})=>be(l)||we(l)||l.answered_at);return c?{why:`${a} became ${c.key}, which has started or been answered`}:s.length?{items:s.map(l=>l.key)}:null}r(ri,"askNotNew");async function er(e,t,n){const a=/^uploads\/([\w.-]+)$/.exec(n?.url||"")?.[1],i=Va.test(a||"")?g.join(He(e),a):null;if(!i||!await j.stat(i).then(l=>l.isFile(),()=>!1))return{name:String(n?.name||a||"file")};await j.mkdir(g.dirname(st(t,"x")),{recursive:!0});const o=a.replace(/^\d+-/,"");let s;for(let l=Date.now();s=`${l}-${o}`,!await j.copyFile(i,g.join(He(t),s),Ro.COPYFILE_EXCL).then(()=>!0,p=>p.code==="EEXIST"?!1:Promise.reject(p));l++);const c=await $(st(e,a),null);return c&&await y(st(t,s),c),{...n,url:`uploads/${s}`,path:g.join(He(t),s)}}r(er,"copyUpload");async function hl(e,t,n){D(e),Fe(t,"the event id"),(typeof n!="string"||!n.trim())&&d("ask move needs --to <route>, the board to move it to");const a=await z(g.join(e,"data","events.jsonl")),i=a.find(k=>k.id===t&&k.kind==="ask")||d(`no ask "${t}" in this site's data/events.jsonl`),o=ge(),s=o.find(k=>k.route===n)||d(`--to ${n}: no board at /${n} in the hub's manifest; the boards are ${o.map(k=>k.route).join(", ")||"none"}`);Pe(s.dir,e)&&d(`--to ${n}: that is this board`);const c=await ri(e,t,a);c?.why&&d(c.why);const l=a.filter(k=>k.kind==="ask-edited"&&k.data?.id===t&&typeof k.data?.text=="string").sort((k,_)=>String(k.ts).localeCompare(String(_.ts))),u=l.at(-1),p=[];for(const k of[...i.data?.files||[],...l.flatMap(_=>_.data.files||[])])p.push(await er(e,s.dir,k));const w={route:g.basename(e),title:(await oe(e)).title,id:t},f=u?u.data.text:i.data?.text||"",h=await he(s,"ask",{text:f,...p.length?{files:p}:{},from:w}),m=`/${n}/#selectedCard=${encodeURIComponent(`ask:${h}`)}`;await W(e,`ask:${t}`,"ask-moved",{board:s.title},{link:m});for(const k of c?.items||[]){const{source:_,itemFile:N,item:v}=await x(e,k);await ce(e,()=>Mn(e,{source:_,itemFile:N,item:v,key:k}))}return{file:g.join(s.dir,"data","events.jsonl"),lines:[`moved to ${m}`,...(c?.items||[]).map(k=>`dropped: ${k}`)]}}r(hl,"moveAsk");async function gl(e,t,{state:n,text:a}={}){return D(e),["posted","needs"].includes(n)||d("review record needs --state posted or --state needs"),String(a??"").trim()||d("review record needs the review text, as --text or --file"),li(e,await ci(e,t,"review record"),{state:n,text:a})}r(gl,"recordReview");async function ci(e,t,n){/^[1-9]\d{0,6}$/.test(String(t))||d(`${n} takes the PR number, such as 437, not "${t}"`);const a=await $(g.join(e,"data","incoming.json"),{});return{...(a?.prs||[]).find(s=>s.number===Number(t))||(await $(g.join(e,"data","reviews.json"),{}))[t]||d(`PR #${t} is not an incoming PR on this board`),repo:a?.repo||se(e,{})}}r(ci,"incomingPR");async function li(e,{number:t,title:n,author:a,url:i,touches:o=[],asked:s=!1,head:c},{state:l,text:u,review_url:p}){const w=g.join(e,"data","reviews.json"),f=await $(w,{});return f[t]={number:Number(t),title:n,author:a,url:i,touches:o,asked:s,head:c,state:l,text:u,...p?{review_url:p}:{},at:new Date().toISOString()},await y(w,f),{file:w,lines:[]}}r(li,"saveReview");async function $l(e,t,{words:n,readAt:a}){D(e);const i=await ci(e,t,"review post"),o=String(n??"").trim()||d("review post needs the review text, as --file"),s=`${St}Claude review from the ${(await oe(e)).title} board`;(o.startsWith(St.trim())||o.includes("Claude review from")||o.includes(Dn))&&d(`review post adds the ${St.trim()} header and the claude-bridge marker itself; pass only your words`);const c=i.repo||d("review post needs a board on GitHub; this board has no repo there");Number.isNaN(Date.parse(a))&&d("--read-at takes the time you read the PR, such as 2026-11-05T09:30:00Z");const l=Se(await oe(e)).github_login;try{const u=await xs({repo:c,pr:i.number,since:a});u.length&&d(`PR #${t} got ${u.length===1?"a review":`${u.length} reviews`} after ${a} (${u.map(w=>`${w.login} at ${w.at}`).join(", ")}); reread the PR and review again`);const p=await ys({repo:c,pr:i.number,head:i.head,body:`${s}
1
+ var Bo=Object.defineProperty;var r=(e,t)=>Bo(e,"name",{value:t,configurable:!0});import j from"node:fs/promises";import{constants as Co,existsSync as we,readFileSync as Io}from"node:fs";import g from"node:path";import _n from"node:os";import{execFile as To}from"node:child_process";import{nodeBin as qo}from"../bin/install.mjs";import{gh as Mo,limitWords as Wo}from"../shared/gh.mjs";import{fileURLToPath as Uo}from"node:url";import{BUILD_REFUSED as Da,BUILDER_ASK as Wt,COMMANDS as Ut,LANDER as Fa,LANES as Te,MOVE as Na,PAUSE_CLEARS as Ra,PHASES as Sn,handedBackAt as vn,MAX_IMPLEMENTERS as Ho,MAX_PLANNERS as Jo,builds as nt,waveItems as qe,mergeBlockers as zo,modelName as Ht,noTakePicked as Ko,ownerQuestion as Me,plans as We,QUESTION_REOPEN as Qo,PLANNER_WORD as Go,providerOf as vt,owesNext as jn,pushTag as Vo,refOf as at,roundOf as On,roundSettledBy as Zo,HELPER as Y,VISUAL as $e,visualAgent as Yo}from"./domain.mjs";import{GITHUB_STATE as An,appendEvent as ke,listBoards as be,readManifest as Xo,samePath as Fe,withManifestLock as es,writeManifest as ts}from"../hub/boards.mjs";import{readUsageGate as Ea,pauseReason as ns,pauseReasonFor as as,heldFor as is,holdsWork as os,builderPick as Pn,pauseEnd as ss}from"../shared/usage-gate.mjs";import{reviseQueued as rs,pushKindError as cs,readClaudeModel as Dn}from"../hub/push.mjs";import{AT_RE as ls}from"../hub/duration.mjs";import{routeRecord as ie}from"../hub/routes.mjs";import{ticketSource as La,resolveTicket as ds,ticketNamed as Ba,ticketTarget as us,moveTicket as fs,cardIdOf as ws}from"./tickets.mjs";import{LINEAR_NEEDS_KEY as ps,linearStatus as ms,linearWorks as hs}from"../hub/linear.mjs";import{validateBranch as gs}from"../hub/repos.mjs";import{withFileLock as it}from"../hub/locks.mjs";import{audit as $s,auditLines as ks,boardAudit as Ca,readAudit as bs}from"../shared/audit.mjs";import{merge3 as ys}from"./merge3.mjs";import{HUB as Ia,MESSAGE_LIMIT as Ta,BRIEF_LINE_LIMIT as qa,hubLineOf as xs,hubText as Ma,detailOf as Wa}from"./hub-lines.mjs";import{fetchPR as Ua,reportOf as Ha,githubLogin as _s,postToPR as Ss,postReview as vs,reviewsSince as js,CLAUDE_PREFIX as jt,CLAUDE_MARK as Fn}from"../hub/pr-report.mjs";import{allLanded as Os,cardPRs as Ue,hasPR as _e,parsePRRef as Nn,prKeyOf as Ot,prNames as ot,prURL as Rn,storedPRs as Ne}from"../shared/card-prs.mjs";import{reviewLinks as As,topicItems as Ps,reviewersOf as En}from"./review.mjs";import{readiness as Ja}from"../hub/readiness.mjs";import{slackDay as Ds}from"../hub/slack.mjs";import"../bin/install.mjs";import{readPermissions as Fs}from"./permissions.mjs";import{validBuildTarget as za}from"./build-script.mjs";import{parseCheck as Ns,checkText as Rs}from"../shared/owner-check.mjs";import{RuleError as Es,rulesOf as Ka,addRule as Ls,editRule as Bs,removeRule as Cs,applyRules as Is,readHubRules as Jt,writeHubRules as Ts}from"../hub/rules.mjs";import{shareEntries as qs,validLogin as Ms}from"../hub/share.mjs";class pe extends Error{static{r(this,"BoardError")}}const d=r(e=>{throw new pe(e)},"fail"),Ws=r(()=>g.join(_n.homedir(),".claude","kickflip","sites"),"sitesDir"),zt=r(()=>g.join(_n.homedir(),".claude","waves"),"wavesDir"),Qa=Symbol("read as"),$=r(async(e,t)=>{let n;try{n=await j.readFile(e,"utf8")}catch{return t}try{const a=JSON.parse(n);return a!==null&&typeof a=="object"&&!Array.isArray(a)?{...a,[Qa]:n}:a}catch{return t}},"readJSON"),z=r(async e=>{try{return(await j.readFile(e,"utf8")).split(`
2
+ `).filter(Boolean).map(t=>JSON.parse(t))}catch{return[]}},"readLines");async function He(e,t){await j.mkdir(g.dirname(e),{recursive:!0});const n=`${e}.${process.pid}.tmp`;await j.writeFile(n,t),await j.rename(n,e)}r(He,"replace");const y=r((e,t)=>{const{[Qa]:n,...a}=t??{};return n===void 0?He(e,JSON.stringify(t,null,2)+`
3
+ `):it(e,async()=>{const i=JSON.parse(n),o=JSON.parse(await j.readFile(e,"utf8").catch(()=>n));await He(e,JSON.stringify(ys(i,a,o),null,2)+`
4
+ `)})},"writeJSON");async function de(e,t){await j.mkdir(g.dirname(e),{recursive:!0}),await j.appendFile(e,JSON.stringify(t)+`
5
+ `)}r(de,"appendLine");async function il(e){/^[\w-]+$/.test(e||"")||d(`--site takes a route such as ios-orchestrator, not "${e}"`);const t=g.join(Ws(),e);return await j.stat(t).then(n=>n.isDirectory(),()=>!1)||d(`no site at ${t}`),t}r(il,"siteDir");const se=r(async e=>({title:"Board",items:"waves",...e?await $(g.join(e,"board.json"),{}):{}}),"boardConfig");async function q(e){const t=await se(e);if(t.items==="waves")try{return(await j.readdir(zt(),{withFileTypes:!0})).filter(a=>a.isDirectory()).map(a=>({slug:a.name,dir:g.join(zt(),a.name),file:g.join(zt(),a.name,"wave.json")}))}catch{return[]}const n=g.resolve(e,t.items);return[{slug:t.slug||g.basename(t.items,".json"),dir:g.dirname(n),file:n}]}r(q,"itemSources");const V=r((e,t)=>g.join(e,"data","threads",t.replace(/[^\w.-]+/g,"__")+".jsonl"),"threadFile"),E=r(e=>g.join(e,"data","owner.json"),"ownerFile"),me=r(e=>g.join(e,"data","staged-questions.json"),"stagedFile"),st=r(e=>g.join(e,"data","screening.json"),"screeningFile"),Us=900*1e3,D=r(e=>e||d("this command needs --site <route>"),"needSite"),S=r((e,t)=>typeof e=="string"&&e.trim()?e.trim():d(`${t} must be non-empty text`),"text"),te=r((e,t)=>e===void 0?void 0:t(e),"optional"),Hs=/^prototype-[\w-]+\.html(?:[?#]\S*)?$/,Js=/^visuals\/[\w-]+\/[\w-]+\.html$/,Ln=r((e,t)=>typeof e=="string"&&(Hs.test(e)||Js.test(e)||/^https?:\/\/\S+$/.test(e))?e:d(`${t} must be an http(s) URL or a site page such as prototype-<topic>.html?q=<id>`),"pageUrl"),Kt=r((e,t)=>typeof e=="boolean"?e:d(`${t} must be true or false`),"bool"),Re=r((e,t)=>typeof e=="string"&&/^[\w.-]+$/.test(e)?e:d(`${t} must use only letters, digits, "_", "." and "-"`),"domId"),rt=r((e,t,n)=>{(!e||typeof e!="object"||Array.isArray(e))&&d(`${n} must be a JSON object`);const a=Object.keys(e).filter(i=>!t.includes(i));a.length&&d(`${n} has unknown field ${a.map(i=>`"${i}"`).join(", ")}; allowed: ${t.join(", ")}`)},"onlyKeys");function F(e){const t=/^([^/\s]+)\/([^/\s]+)$/.exec(e||"");return t?{file:t[1],id:t[2]}:d(`expected <file>/<id>, not "${e}"`)}r(F,"parseKey");async function Je(e,t){const n=(await q(e)).find(i=>i.slug===t)||d(`this board has no item file "${t}"`),a=await $(n.file,null);return(!a||!Array.isArray(a.items))&&d(`${n.file} is not an item file with items[]`),{source:n,itemFile:a}}r(Je,"loadItemFile");async function x(e,t){const{file:n,id:a}=F(t);let i=await Je(e,n);!i.itemFile.items.some(s=>s.id===a)&&await tc(e,t)&&(i=await Je(e,n));const o=i.itemFile.items.find(s=>s.id===a)||d(`item file "${n}" has no item "${a}"`);return{...i,item:o}}r(x,"loadItem");const Qt=["md","html","json"];async function Bn(e,t){const n=new RegExp(`^${t.replace(/[.]/g,"\\.")}(?:~(\\d+))?\\.(${Qt.join("|")})$`);return(await j.readdir(e).catch(()=>[])).flatMap(a=>{const i=n.exec(a);return i?[{name:a,ext:i[2],n:Number(i[1]||1)}]:[]}).sort((a,i)=>a.n-i.n||Qt.indexOf(a.ext)-Qt.indexOf(i.ext)).filter((a,i,o)=>a.n!==o[i-1]?.n)}r(Bn,"replyFilesOf");async function ol(e,t,n,a){D(e),Re(t,"the event id");const i=(await z(g.join(e,"data","events.jsonl"))).find(h=>h.id===t);i||d(`no event "${t}" in this site's data/events.jsonl`);const o=i.kind==="ask"?await Qe(e,t):null,s=o?o.key:i.kind==="ask"?`ask:${t}`:i.kind==="item-message"&&typeof i.data?.key=="string"?i.data.key:null;o&&ci(o.item)?(S(n,"the reply"),await li(e,o,a)):a!==void 0&&d(`reply --title titles the New or answered card an ask became; "${t}" is not one`);const c=g.join(e,"data","replies"),l=await Bn(c,t),u=g.join(c,l.length?`${t}~${l.at(-1).n+1}.md`:`${t}.md`);await He(u,S(n,"the reply"));const p=s?await In(e,s,n):[],w=i.kind==="wave-triage"?["wave","file"].filter(h=>typeof i.data?.[h]=="string").map(h=>`${h}:${i.data[h]}`):[],f=o?`item:${o.key}`:w[0];return f&&p.push(...await Ii(e,f,"Acted on: replied")),{file:u,lines:p}}r(ol,"writeReply");const Ga=r(e=>g.join(e,"data","working.json"),"workingFile");async function zs(e){return(await $(Ga(e),{})).events||{}}r(zs,"readWorking");const sl=["ask","item-message","wave-triage","overnight-triage"];async function rl(e,t){if(!t.length)return;const n=new Date().toISOString(),a=await zs(e);await y(Ga(e),{events:{...a,...Object.fromEntries(t.map(i=>[i,ie(a,i)||n]))}})}r(rl,"markWorking");const Cn=r(e=>g.join(e,"data","push-state.json"),"pushStateFile");async function Ks(e,t){D(e);const n=Date.parse((await $(Cn(e),{})).last_push);if(Number.isNaN(n))return 0;const a=g.join(e,"data","replies"),i=(await j.readdir(a).catch(()=>[])).filter(s=>Qt.includes(g.extname(s).slice(1)));return(await Promise.all(i.map(s=>j.stat(g.join(a,s)).then(c=>c.mtimeMs,()=>0)))).filter(s=>s>n&&s<=t.getTime()).length}r(Ks,"repliesSincePush");async function Qs(e,t){return await y(Cn(D(e)),{last_push:t.toISOString()}),Cn(e)}r(Qs,"recordPush");const Gs=g.join(g.dirname(Uo(import.meta.url)),"..","bin","kickflip.mjs");function Vs(){if(!process.env.KICKFLIP_PUSH_CMD)return[qo(),Gs,"push"];try{const e=JSON.parse(process.env.KICKFLIP_PUSH_CMD);if(Array.isArray(e)&&e.length&&e.every(t=>typeof t=="string"))return e}catch{}throw new pe("KICKFLIP_PUSH_CMD must be a JSON array of strings")}r(Vs,"pushCommand");async function cl(e,t){const n=cs(t.kind);n&&d(n),t.title===void 0&&d("push needs --title");const a=new Date,i=Ys(t.body,await Ks(e,a)),o={kind:t.kind,title:t.title,body:i||void 0,url:er(t.url),tag:t.tag},[s,...c]=Vs(),l=[...c,...Object.entries(o).flatMap(([w,f])=>f===void 0?[]:[`--${w}`,f]),...t.urgent?["--urgent"]:[],...t.now?["--now"]:[]],u=await new Promise(w=>{To(s,l,{encoding:"utf8"},(f,h,m)=>w({error:f,said:`${h||""}${m||""}`.split(`
6
+ `).filter(b=>b.trim())}))}),p=u.error?typeof u.error.code=="number"?u.error.code:1:0;if(p!==0)throw Object.assign(new pe([...u.said,`kickflip push failed (${typeof u.error.code=="number"?`exit ${p}`:u.error.message}); the push was not recorded`].join(`
7
+ `)),{exitCode:p});return await Qs(e,a),{lines:u.said}}r(cl,"sendPush");const Zs=/(?:^| · )(and \d+ repl(?:y|ies))$/,Ys=r((e,t)=>[e,t&&`and ${t} ${t===1?"reply":"replies"}`].filter(Boolean).join(" \xB7 "),"withReplies"),Va=160;function Xs(e){const t=String(e).split(`
8
+ `).map(n=>n.replace(/\s+/g," ").trim()).find(Boolean)||"";return t.length>Va?`${t.slice(0,Va-1).trimEnd()}\u2026`:t}r(Xs,"pushLine");function er(e){if(!e||!e.startsWith("/"))return e;const t=new URL(e,"http://board");return/^#(?:selectedCard|item)=[^&]+$/.test(t.hash)&&(t.hash=`${t.hash}&reply`),/\/[\w-]+\.html$/.test(t.pathname)&&!t.pathname.endsWith("/index.html")&&t.searchParams.set("from","push"),t.pathname+t.search+t.hash}r(er,"pushUrl");const tr=r((e,t)=>`/${g.basename(e)}/#selectedCard=${encodeURIComponent(t)}&reply`,"threadUrl");async function In(e,t,n){const a=Xs(n);if(!a)return[];const i=Vo(t),o=await rs(i,s=>[a,Zs.exec(s)?.[1]].filter(Boolean).join(" \xB7 "),tr(e,t));return o===null?[]:[`queued push ${i} now reads: ${o}`]}r(In,"freshenPush");const Za=r(e=>g.join(e,"data","hidden.json"),"hiddenFile"),nr=r(async e=>new Set((await $(Za(e),{events:[]})).events||[]),"readHidden");let Ya=Promise.resolve();function ll(e,t,n){Re(t,"the event id");const a=Ya.then(async()=>{const i=await nr(e);n?i.add(t):i.delete(t),await y(Za(e),{events:[...i]})});return Ya=a.catch(()=>{}),a}r(ll,"setHidden");const Xa=r(e=>g.join(e,"data","read.json"),"readMarksFile"),ar=r(async e=>(await $(Xa(e),{})).cards||{},"readMarks");let ei=Promise.resolve();function dl(e,t,n={}){const a=ei.then(async()=>{const i=await ar(e),o=new Date().toISOString(),s=r((l,u)=>String(u||"")>l?u:l,"later"),c=Object.fromEntries([...new Set(t)].sort().map(l=>[l,s(i[l]||o,n[l])]));JSON.stringify(c)!==JSON.stringify(i)&&await y(Xa(e),{cards:c})});return ei=a.catch(()=>{}),a}r(dl,"markRead");const Tn=20*1024*1024,ze=r(e=>g.join(e,"data","uploads"),"uploadsDir"),ct=r((e,t)=>g.join(ze(e),".meta",`${t}.json`),"uploadMeta"),ti=/^\d+-[\w.-]+$/,ni=/^[\w.+-]+\/[\w.+-]+$/,ai=r(()=>Object.assign(new pe(`a file can be at most ${Tn/1024/1024} MB`),{status:413}),"tooLarge");async function ul(e,t,{name:n,type:a,length:i}){if(D(e),i>Tn)throw ai();const o=String(n||"").trim().slice(0,200)||"file",s=g.basename(o).replace(/[^\w.-]+/g,"-").replace(/^[.-]+/,"").slice(-80)||"file",c=String(a||"").split(";")[0].trim().toLowerCase(),l=ni.test(c)?c:"application/octet-stream";await j.mkdir(g.dirname(ct(e,"x")),{recursive:!0});let u,p;for(let h=Date.now();!p;h++)u=`${h}-${s}`,p=await j.open(g.join(ze(e),u),"wx").catch(m=>m.code==="EEXIST"?null:Promise.reject(m));const w=g.join(ze(e),u);let f=0;try{for await(const h of t){if(f+=h.length,f>Tn)throw ai();await p.write(h)}}catch(h){throw await p.close(),await j.rm(w,{force:!0}),h}return await p.close(),await y(ct(e,u),{name:o,type:l,size:f,at:new Date().toISOString()}),{name:o,url:`uploads/${u}`,path:w,type:l,size:f}}r(ul,"saveUpload");async function fl(e,t){if(!ti.test(t||""))return null;const n=g.join(ze(e),t),a=await j.stat(n).catch(()=>null);if(!a?.isFile())return null;const i=await $(ct(e,t),{});return{path:n,name:i.name||t,type:ni.test(i.type||"")?i.type:"application/octet-stream",size:a.size}}r(fl,"readUpload");const ii=["github","slack"],oi=r(async(e,t)=>String(t).startsWith("ask:")?Rt(e,t.slice(4),"the ask"):x(e,t),"checkThreadKey");async function M(e,t,n,a={},{link:i}={}){D(e),await oi(e,t);const o={ts:new Date().toISOString(),who:"claude",kind:Ia,line:n,text:Ma(n,a),...Wa(n,a)?{detail:Wa(n,a)}:{},...i?{link:i}:{}};return await de(V(e,t),o),{line:o,lines:xs(o).fault?await In(e,t,o.text):[]}}r(M,"appendStatus");async function lt(e,t,{text:n,link:a,log:i,who:o="claude",via:s,by:c}){D(e),await oi(e,t),/^[^\n]{1,40}$/.test(o.trim())||d("--who takes claude, love or a reviewer's name"),s!==void 0&&!ii.includes(s)&&d(`--via takes ${ii.join(" or ")}`);const l=s===void 0?Gt(n):S(n,"--text"),u={ts:new Date().toISOString(),who:o.trim(),text:l,link:te(a,w=>Ln(w,"--link")),log:i,via:s,by:c},p=V(e,t);return await de(p,u),{file:p,lines:u.who==="claude"&&!s?await In(e,t,u.text):[]}}r(lt,"appendMessage");function Gt(e){const t=S(e,"--text");return t.length>Ta&&d(`--text runs to ${t.length} characters, past a message's ${Ta}: rewrite it shorter, or put the detail in an HTML page (board.mjs visual add, or --link to a page) and link it`),t}r(Gt,"messageText");async function Ke(e,t,{command:n,text:a=Ut[n],who:i="claude",by:o,card:s}){const c={ts:new Date().toISOString(),who:i,command:n,text:a,...o?{by:o}:{},...s?{card:s}:{}};return await de(V(e,t),c),{lines:[]}}r(Ke,"appendCommand");const Se=r(e=>g.join(e,"data","ask-links.json"),"askLinksFile");async function ve(e){return(await $(Se(e),{})).links||{}}r(ve,"readAskLinks");async function H(e,t){const{tickets:n}=je(await se(e)),a=await q(e),i=/^ask:(.+)$/.exec(t)?.[1];if(i===void 0){const s=/^([^/\s]+)\/([^/\s]+)$/.exec(t);if(!s)return t;const c=a.find(p=>p.slug===s[1]),l=c&&await $(c.file,null);if(!l)return s[2];const u=(l.items||[]).find(p=>p.id===s[2]);return at(l,u||{id:s[2]},n)}const o=e?ie(await ve(e),i):void 0;if(o)return H(e,o);for(const s of a){const c=await $(s.file,null),l=(Array.isArray(c?.items)?c.items:[]).find(u=>u.from_ask===i);if(l)return at(c,l,n)}return`task ${i}`}r(H,"cardName");async function wl(e,t){const n=(await z(g.join(e,"data","events.jsonl"))).find(a=>a.id===t);return n?.kind==="ask"?H(e,`ask:${t}`):typeof n?.data?.key=="string"?H(e,n.data.key):`the ${n?.kind} event`}r(wl,"eventCard");async function Qe(e,t){for(const n of await q(e)){const a=await $(n.file,null),i=(Array.isArray(a?.items)?a.items:[]).find(o=>o.from_ask===t);if(i)return{source:n,itemFile:a,item:i,key:`${n.slug}/${i.id}`}}return null}r(Qe,"askCardOf");const qn=r(e=>!e.phase&&!e.answered_at&&!he(e)&&!_e(e)&&!e.filed_held,"stillNew");function Mn(e,t){const n=t.filter(i=>i.kind==="ask-edited"&&i.data?.id===e.id&&typeof i.data?.text=="string").sort((i,o)=>String(i.ts).localeCompare(String(o.ts))).at(-1);return String(n?n.data.text:e.data?.text||"").trim().split(`
9
+ `)[0].trim()||(e.data?.files||[]).map(i=>i?.name).filter(Boolean).join(", ")||"Ask"}r(Mn,"askTitle");const pl=r((e,t,n)=>ce(e,async()=>{D(e);const a=await z(g.join(e,"data","events.jsonl")),i=n||a.find(l=>l.id===t&&l.kind==="ask")||d(`no ask "${t}" in this site's data/events.jsonl`);if(await Qe(e,t)||await fi(e,t,a))return null;const{source:o,itemFile:s}=await si(e),c=await ri(e,o,i,Mn(i,a));return s.items.push(c),await y(o.file,s),`${o.slug}/${c.id}`}),"fileAsk");async function si(e){const t=await q(e);for(const n of t){const a=await $(n.file,null);if(a)return Array.isArray(a.items)||d(`${n.file} is not an item file with items[]`),{source:n,itemFile:a}}return(await se(e)).items==="waves"&&d("this board has no wave yet to file a new task in; start a wave first"),await j.mkdir(t[0].dir,{recursive:!0}),{source:t[0],itemFile:{items:[]}}}r(si,"askFile");async function ri(e,t,n,a){const i=new Date().toISOString(),o={id:await so(e),title:a,created:i,lane:"todo",lane_at:i,from_ask:n.id};return o.plan=tn(t.dir,o.id),await nn(g.join(t.dir,o.plan),`# ${o.title}
10
+ `),o}r(ri,"newAskCard");const ml=r(e=>ce(e,async()=>{D(e);const t=r((...m)=>g.join(e,"data",...m),"data"),n=await z(t("events.jsonl")),a=await ve(e),i=new Map(Object.entries(a));for(const m of await q(e))for(const b of(await $(m.file,{})).items||[])b.from_ask&&i.set(b.from_ask,`${m.slug}/${b.id}`);const o=new Set(n.filter(m=>m.kind==="ask-withdrawn").map(m=>m.data?.id)),s=n.filter(m=>m.kind==="ask"&&!o.has(m.id)&&!i.has(m.id)).sort((m,b)=>String(m.ts).localeCompare(String(b.ts))),c=(await $(t("ask-progress.json"),{})).asks||{},l=(await $(t("ask-titles.json"),{})).titles||{},u=new Map(n.filter(m=>m.kind==="ask-done").map(m=>[m.data?.id,String(m.ts)])),p=[];let w;for(const m of s){const b=await z(V(e,`ask:${m.id}`));if(b.some(A=>A.line==="ask-moved"))continue;const _=await Promise.all((await Bn(t("replies"),m.id)).map(A=>j.stat(t("replies",A.name)).then(L=>L.mtime.toISOString()))),N=b.filter(A=>(A.who||"claude")==="claude"&&A.kind!=="status").map(A=>String(A.ts)),v=[..._,...N].sort().pop()||[ie(c,m.id)?.at,u.get(m.id)].filter(Boolean).sort().pop();w||=await si(e);const O=await ri(e,w.source,m,ie(l,m.id)||Mn(m,n));O.created=m.ts,v&&Et(O,"done",v),w.itemFile.items.push(O),await y(w.source.file,w.itemFile),i.set(m.id,`${w.source.slug}/${O.id}`),p.push(i.get(m.id))}const f=r(m=>i.get(/^item:ask:(.+)$/.exec(m||"")?.[1]),"cardOf"),h=await $(E(e),null);if(h?.items?.some(m=>f(m.topic))){for(const m of h.items)f(m.topic)&&(m.topic=`item:${f(m.topic)}`);await y(E(e),h)}for(const m of["ask-progress.json","ask-titles.json"])await j.rm(t(m),{force:!0});return p}),"fileAsks"),hl=r((e,t)=>ce(e,async()=>{const n=await Qe(D(e),t);if(!n||!qn(n.item))return null;const a=await z(g.join(e,"data","events.jsonl"));return n.item.title=Mn(a.find(i=>i.id===t&&i.kind==="ask"),a),await y(n.source.file,n.itemFile),n.key}),"retitleAskCard");async function Wn(e,{source:t,itemFile:n,item:a,key:i}){n.items=n.items.filter(l=>l!==a);const o=Number(/^T-([1-9]\d*)$/.exec(a.id)?.[1]||0);o>(Number(n.last_task)||0)&&(n.last_task=o),await y(t.file,n),a.plan&&await j.rm(g.join(t.dir,a.plan),{force:!0});const s=V(e,i),c=await z(s);return await j.rm(s,{force:!0}),c}r(Wn,"dropCard");const gl=r((e,t)=>ce(e,async()=>{const n=await Qe(D(e),t);return!n||!qn(n.item)?null:(await Wn(e,n),n.key)}),"dropAskCard"),ci=r(e=>!e.phase&&!e.closed_at&&!_e(e)&&(!he(e)||!!e.answered_at),"answerable");async function li(e,t,n){const{item:a,key:i}=t;a.from_ask||d(`${i} is not a card an ask became; write in its thread with thread ${i} --text`),ci(a)||d(`${i} is sorted (${a.phase||Te[a.lane]}), so it is work: write in its thread with thread ${i} --text`),n!==void 0&&(a.title=S(n,"--title")),a.answered_at||=new Date().toISOString(),await y(t.source.file,t.itemFile)}r(li,"markAnswered");async function di(e,t,n){const a=await Qe(e,t);a&&a.key!==n&&d(`ask "${t}" is already card ${a.key}: sort it with item set ${a.key} --phase, or merge it with item relate-ask ${t} <card>`)}r(di,"refuseFiled");async function $l(e,t,n,a){D(e);const{source:i,itemFile:o,item:s}=await x(e,t),c={source:i,itemFile:o,item:s,key:t};return Gt(n),a!==void 0&&S(a,"--title"),await li(e,c,a),lt(e,t,{text:n})}r($l,"answerCard");async function kl(e,t,n,a,i){D(e),Re(t,"the event id"),(await z(g.join(e,"data","events.jsonl"))).some(l=>l.id===t&&l.kind==="ask")||d(`no ask "${t}" in this site's data/events.jsonl`),await Un(e,t),await x(e,n),S(a,"--summary"),i!==void 0&&S(i,"--title");const s=await Qe(e,t);if(s&&s.key===n&&d(`ask "${t}" is ${n} already`),s&&!qn(s.item)&&d(`ask "${t}" became ${s.key}, which is sorted; to fold that card's notes into ${n}, use item notes ${n} --from ${s.key}, then close it`),s){const l=await ce(e,()=>Wn(e,s)),u=V(e,`ask:${t}`);for(const p of l)await de(u,p);await de(u,{ts:new Date().toISOString(),who:"claude",kind:Ia,line:"card-merged",text:Ma("card-merged",{card:await H(e,n)}),into:n,title:i??s.item.title,link:`#selectedCard=${encodeURIComponent(n)}`})}const c=await ve(e);return await y(Se(e),{links:{...c,[t]:n}}),await lt(e,n,{text:a}),Se(e)}r(kl,"relateAsk");async function bl(e,t){await Rt(e,t,"the ask");const n=await ve(e);return ie(n,t)===void 0&&d(`ask "${t}" is not folded into a card`),delete n[t],await y(Se(e),{links:n}),Se(e)}r(bl,"unfoldAsk");const ui=r(async(e,t)=>(await z(V(e,`ask:${t}`))).find(n=>n.line==="ask-moved"),"movedLine");async function Un(e,t){const n=await ui(e,t);n&&d(`ask "${t}" was moved to ${n.link}; it lives there now`)}r(Un,"refuseMoved");async function fi(e,t,n){const a=`ask "${t}"`;if(n.some(l=>l.kind==="ask-withdrawn"&&l.data?.id===t))return{why:`${a} was deleted by the owner`};const i=await ui(e,t);if(i)return{why:`${a} was already moved to ${i.link}`};const o=ie(await ve(e),t);if(o)return{why:`${a} is folded into ${o}; run ask unfold ${t} first`};const s=[];for(const l of await q(e)){const u=await $(l.file,null);for(const p of Array.isArray(u?.items)?u.items:[])p.from_ask===t&&s.push({key:`${l.slug}/${p.id}`,it:p})}const c=s.find(({it:l})=>_e(l)||he(l)||l.answered_at);return c?{why:`${a} became ${c.key}, which has started or been answered`}:s.length?{items:s.map(l=>l.key)}:null}r(fi,"askNotNew");async function ir(e,t,n){const a=/^uploads\/([\w.-]+)$/.exec(n?.url||"")?.[1],i=ti.test(a||"")?g.join(ze(e),a):null;if(!i||!await j.stat(i).then(l=>l.isFile(),()=>!1))return{name:String(n?.name||a||"file")};await j.mkdir(g.dirname(ct(t,"x")),{recursive:!0});const o=a.replace(/^\d+-/,"");let s;for(let l=Date.now();s=`${l}-${o}`,!await j.copyFile(i,g.join(ze(t),s),Co.COPYFILE_EXCL).then(()=>!0,p=>p.code==="EEXIST"?!1:Promise.reject(p));l++);const c=await $(ct(e,a),null);return c&&await y(ct(t,s),c),{...n,url:`uploads/${s}`,path:g.join(ze(t),s)}}r(ir,"copyUpload");async function yl(e,t,n){D(e),Re(t,"the event id"),(typeof n!="string"||!n.trim())&&d("ask move needs --to <route>, the board to move it to");const a=await z(g.join(e,"data","events.jsonl")),i=a.find(b=>b.id===t&&b.kind==="ask")||d(`no ask "${t}" in this site's data/events.jsonl`),o=be(),s=o.find(b=>b.route===n)||d(`--to ${n}: no board at /${n} in the hub's manifest; the boards are ${o.map(b=>b.route).join(", ")||"none"}`);Fe(s.dir,e)&&d(`--to ${n}: that is this board`);const c=await fi(e,t,a);c?.why&&d(c.why);const l=a.filter(b=>b.kind==="ask-edited"&&b.data?.id===t&&typeof b.data?.text=="string").sort((b,_)=>String(b.ts).localeCompare(String(_.ts))),u=l.at(-1),p=[];for(const b of[...i.data?.files||[],...l.flatMap(_=>_.data.files||[])])p.push(await ir(e,s.dir,b));const w={route:g.basename(e),title:(await se(e)).title,id:t},f=u?u.data.text:i.data?.text||"",h=await ke(s,"ask",{text:f,...p.length?{files:p}:{},from:w}),m=`/${n}/#selectedCard=${encodeURIComponent(`ask:${h}`)}`;await M(e,`ask:${t}`,"ask-moved",{board:s.title},{link:m});for(const b of c?.items||[]){const{source:_,itemFile:N,item:v}=await x(e,b);await ce(e,()=>Wn(e,{source:_,itemFile:N,item:v,key:b}))}return{file:g.join(s.dir,"data","events.jsonl"),lines:[`moved to ${m}`,...(c?.items||[]).map(b=>`dropped: ${b}`)]}}r(yl,"moveAsk");async function xl(e,t,{state:n,text:a}={}){return D(e),["posted","needs"].includes(n)||d("review record needs --state posted or --state needs"),String(a??"").trim()||d("review record needs the review text, as --text or --file"),pi(e,await wi(e,t,"review record"),{state:n,text:a})}r(xl,"recordReview");async function wi(e,t,n){/^[1-9]\d{0,6}$/.test(String(t))||d(`${n} takes the PR number, such as 437, not "${t}"`);const a=await $(g.join(e,"data","incoming.json"),{});return{...(a?.prs||[]).find(s=>s.number===Number(t))||(await $(g.join(e,"data","reviews.json"),{}))[t]||d(`PR #${t} is not an incoming PR on this board`),repo:a?.repo||re(e,{})}}r(wi,"incomingPR");async function pi(e,{number:t,title:n,author:a,url:i,touches:o=[],asked:s=!1,head:c},{state:l,text:u,review_url:p}){const w=g.join(e,"data","reviews.json"),f=await $(w,{});return f[t]={number:Number(t),title:n,author:a,url:i,touches:o,asked:s,head:c,state:l,text:u,...p?{review_url:p}:{},at:new Date().toISOString()},await y(w,f),{file:w,lines:[]}}r(pi,"saveReview");async function _l(e,t,{words:n,readAt:a}){D(e);const i=await wi(e,t,"review post"),o=String(n??"").trim()||d("review post needs the review text, as --file"),s=`${jt}Claude review from the ${(await se(e)).title} board`;(o.startsWith(jt.trim())||o.includes("Claude review from")||o.includes(Fn))&&d(`review post adds the ${jt.trim()} header and the claude-bridge marker itself; pass only your words`);const c=i.repo||d("review post needs a board on GitHub; this board has no repo there");Number.isNaN(Date.parse(a))&&d("--read-at takes the time you read the PR, such as 2026-11-05T09:30:00Z");const l=je(await se(e)).github_login;try{const u=await js({repo:c,pr:i.number,since:a});u.length&&d(`PR #${t} got ${u.length===1?"a review":`${u.length} reviews`} after ${a} (${u.map(w=>`${w.login} at ${w.at}`).join(", ")}); reread the PR and review again`);const p=await vs({repo:c,pr:i.number,head:i.head,body:`${s}
11
11
 
12
12
  ${o}
13
13
 
14
- ${Dn}`,login:l});return await li(e,i,{state:"posted",text:o,review_url:p}),{lines:[`posted review on PR #${t}: ${p}`]}}catch(u){if(u instanceof ue)throw u;d(u.message)}}r($l,"postIncomingReview");const Kt=r(e=>g.join(e,"data","handshake.json"),"handshakeFile");async function di(e){const t=await $(Kt(e),null);if(!t?.hello)return t;const{hello:n,...a}=t;return{...a,ping:a.ping||n}}r(di,"readHandshake");async function kl(e,t){D(e),Fe(t,"the event id");const n=(await z(g.join(e,"data","events.jsonl"))).find(i=>i.id===t);n?.kind!=="ping"&&n?.kind!=="hello"&&d(`no ping event "${t}" in this site's data/events.jsonl`);const a=await di(e)||{};return a.ping?.id&&a.ping.id!==t&&d(`"${t}" is an older ping; ack the newest, "${a.ping.id}"`),await y(Kt(e),{...a,ping:a.ping||{id:t,at:n.ts,session:null},ack:{id:t,at:new Date().toISOString()}}),Kt(e)}r(kl,"ackPing");async function bl(e){const t=await di(e);t&&!t.done_at&&await y(Kt(e),{...t,done_at:new Date().toISOString()})}r(bl,"finishHandshake");const Un={name:/^[^\n]{1,40}$/,slack_user:/^U[A-Z0-9]+$/,dm:/^D[A-Z0-9]+$/,github:/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/},jt=r((e,t)=>e.trim().toLowerCase()===t.trim().toLowerCase(),"sameName");function Hn(e,t){return Array.isArray(e)||d(`${t} must be a JSON array of reviewers`),e.forEach((n,a)=>{const i=`${t}[${a}]`;ot(n,Object.keys(Un),i);for(const[o,s]of Object.entries(Un))(typeof n[o]!="string"||!s.test(n[o].trim()))&&d(`${i}.${o} must match ${s}`);e.slice(0,a).some(o=>jt(o.name,n.name))&&d(`${t} names "${n.name}" twice`)}),e.map(n=>Object.fromEntries(Object.keys(Un).map(a=>[a,n[a].trim()])))}r(Hn,"checkReviewers");async function Jn(e,t){const{file:n,reviewers:a}=await At(e,o=>{const s=Hn(t(o.reviewers||[]),"the reviewers");return{set:{reviewers:s},reviewers:s}}),i=[];for(const o of await q(e)){const s=await $(o.file,null);(s?.items||[]).some(c=>!K(c))&&(await y(o.file,{...s,reviewers:a}),i.push(`item file ${o.slug}: reviewers copied`))}return{file:n,lines:i}}r(Jn,"writeReviewers");const yl=r((e,t)=>Jn(e,()=>Hn(t,"the reviewer file")),"setReviewers"),ct=r(e=>g.join(D(e),"data","pr-slack.json"),"prSlackFile"),Qt=r(e=>$(ct(e),{}),"readPRSlack"),Ot=r(e=>!!(e?.ts||e?.posting_at),"held");async function Ke(e,t){return nt(e,async()=>{const n=JSON.parse(await j.readFile(e,"utf8").catch(()=>"{}")),a=t(n);return await We(e,JSON.stringify(n,null,2)+`
15
- `),a})}r(Ke,"editJSON");async function Gt(e,t,n){await Ke(e,i=>{const o=t(i);Ot(o)&&d("that Slack message has already gone out"),o.posting_at=new Date().toISOString()});let a;try{a=await n()}catch(i){throw await Ke(e,o=>{delete t(o).posting_at}),i}try{await Ke(e,i=>{const o=t(i);o.ts=a,delete o.posting_at})}catch(i){d(`posted, but its record failed, so it won't go again: ${i.message}`)}return a}r(Gt,"postOnce");const xl=r((e,t,n,a)=>Gt(ct(e),i=>{const o=i[t.pr]||={head:t.head,links:{},replies:[]};return o.head??=t.head,Object.assign(o.links[t.reviewer]||={reactions:[]},{channel:n})},()=>a(n)),"postLink"),_l=r((e,t,n)=>Gt(ct(e),a=>a[t.pr].replies.find(i=>i.id===t.id).to[t.reviewer],n),"postReply"),Sl=r((e,t)=>Ke(ct(e),n=>{const a=n[t.pr].links[t.reviewer];a.reactions.includes(t.emoji)||a.reactions.push(t.emoji)}),"recordReaction"),ui=r((e,t,n,a)=>Ke(ct(e),i=>{const o=i[t],s=Object.keys(o?.links||{}).filter(c=>o.links[c].ts);return s.length?(o.replies.push({id:`${n}-${o.replies.length+1}`,text:a,to:Object.fromEntries(s.map(c=>[c,{}]))}),s):[]}),"queueReply");function tr(e,t,n){const a=e.commits||[],i=a.findIndex(o=>o.oid===n);return[`New push to #${t}`,a.slice(i+1).map(o=>o.subject).join(" \xB7 ")].filter(Boolean).join(`
16
- `)}r(tr,"pushText");async function vl(e){const t=await $(On,{}),n=r((a,i)=>{const o=ae(t.prs,a)?.head;return!!o&&o!==i.head&&Object.values(i.links).some(s=>s.ts)},"moved");return Object.entries(await Qt(e)).some(([a,i])=>n(a,i))?Ke(ct(e),a=>{const i=[];for(const[o,s]of Object.entries(a)){if(!n(o,s))continue;const c=ae(t.prs,o),l=Object.keys(s.links).filter(u=>s.links[u].ts);s.head&&(s.replies.push({id:`push-${s.replies.length+1}`,text:tr(c,o.split("#")[1],s.head),to:Object.fromEntries(l.map(u=>[u,{}]))}),i.push(o)),s.head=c.head}return i}):[]}r(vl,"queuePushes");function nr(e,t,n){const a=Number.isInteger(e.additions)?`+${e.additions}/-${e.deletions}, ${e.files} file${e.files===1?"":"s"}`:null;return[e.title||n.title,Nn(t),a].filter(Boolean).join(`
17
- `)}r(nr,"linkText");const fi=r(e=>!!e?.approved||e?.reviewDecision==="APPROVED","approved");async function jl(e,t){D(e);const n=await qa.waiting("slack");if(n)return{waiting:n,links:[],replies:[],reactions:[],digest:null};const a=await oe(e),i=await $(On,{}),o=await Qt(e),s=[];for(const p of await q(e)){const w=await $(p.file,{}),f=Rn(w,a.reviewers);for(const h of w.items||[])if(h.lane!=="done")for(const m of Me(h,se(e,w))){const k=vt(m),_=ae(i.prs,k);if(!(_?.state!=="OPEN"||_.isDraft))for(const N of f)Ot(o[k]?.links?.[N.name])||s.some(v=>v.pr===k&&v.reviewer===N.name)||s.push({pr:k,key:`${p.slug}/${h.id}`,reviewer:N.name,slack_user:N.slack_user,head:_.head||null,text:nr(_,m,h)})}}const c=[],l=[];for(const[p,w]of Object.entries(o)){const f=ae(i.prs,p),h=[fi(f)&&"git_approved",f?.state==="MERGED"?"git_merged":f?.state==="CLOSED"&&"x"].filter(Boolean);for(const[m,k]of Object.entries(w.links))if(k.ts)for(const _ of h)k.reactions.includes(_)||l.push({pr:p,reviewer:m,channel:k.channel,ts:k.ts,emoji:_});for(const m of w.replies)for(const[k,_]of Object.entries(m.to))Ot(_)||c.push({pr:p,id:m.id,reviewer:k,channel:w.links[k].channel,thread_ts:w.links[k].ts,text:m.text})}const u=await ar(e,t);return{links:s,replies:c,reactions:l,digest:u.due?u:null}}r(jl,"reviewDue");async function Ol(e,t,n){D(e);const a=S(n,"what changed"),{itemFile:i,item:o}=await x(e,t),s=[];for(const l of Me(o,se(e,i)))s.push(...await ui(e,vt(l),"updated",`Updated: ${a}`));const c=[...new Set(s)];return{lines:[c.length?`Updated queued for ${c.join(", ")} on ${t}`:`no reviewer has a link to ${t}'s PRs; nothing queued`]}}r(Ol,"reviewUpdated");async function Al(e,t){const{itemFile:n,item:a}=await x(e,t),i=await Qt(e);return Me(a,se(e,n)).some(o=>Object.values(i[vt(o)]?.links||{}).some(s=>s.ts))}r(Al,"cardLinked");const wi=r(e=>g.join(D(e),"data","review-digests.json"),"digestFile");async function ar(e,{now:t=new Date}={}){D(e);const n=await oe(e),a=await $(On,{}),i=await Qt(e),o=vs(),s=new Map;for(const f of await q(e)){const h=await $(f.file,{}),m=Rn(h,n.reviewers);for(const k of m)s.has(k.name)||s.set(k.name,{name:k.name,slack_user:k.slack_user,dm:k.dm,items:[]});for(const k of h.items||[]){if(k.lane==="done")continue;const _=`${f.slug}/${k.id}`,N=Ss(h,k,_,o);for(const v of Me(k,se(e,h))){const O=vt(v),A=ae(a.prs,O);if(!(A?.state!=="OPEN"||A.isDraft||A.failing?.length||fi(A)))for(const I of m){const B=i[O]?.links?.[I.name]?.ts||ae(N,I.name)?.ts||null,P=B?(t.getTime()-Number(B)*1e3)/864e5:null,L=P>2?Math.floor(P):null;s.get(I.name).items.push({key:_,pr:O,title:k.title,url:Nn(v),sent_ts:B,...L?{days_old:L}:{},text:`${Nn(v)} \xB7 ${k.title}${L?` (${L} days old)`:""}`})}}}}const{date:c,open:l}=js(t),u=((await $(wi(e),{})).digests||[]).filter(f=>f.date===c),p=await qa.waiting("slack");for(const f of u)s.has(f.reviewer)||s.set(f.reviewer,{name:f.reviewer,items:[]});const w=[...s.values()].map(f=>{const h=u.find(k=>k.reviewer===f.name),m=h?!h.posting_at&&(!h.ts||h.replies.some(k=>!Ot(k))):f.items.length>0;return{...f,owed:m,...h?{sent:h}:{}}}).filter(f=>f.items.length||f.sent);return{date:c,due:!p&&l&&w.some(f=>f.owed),...p?{waiting:p}:{},reviewers:w}}r(ar,"reviewDigest");async function Pl(e,t,n,{channel:a,header:i,post:o}){const s=wi(e),c=r(p=>(p.digests||[]).find(w=>w.date===t&&w.reviewer===n.name),"entry");await Ke(s,p=>{c(p)||(p.digests=[...(p.digests||[]).filter(w=>w.date===t),{date:t,reviewer:n.name,channel:a,replies:n.items.map(w=>({pr:w.pr,text:w.text}))}],delete p.last_date)});const l=c(await $(s,{})),u=l.ts||await Gt(s,c,()=>o({channel:l.channel,text:i(l.replies.length)}));for(const[p,w]of l.replies.entries())Ot(w)||await Gt(s,f=>c(f).replies[p],()=>o({channel:l.channel,thread_ts:u,text:w.text}))}r(Pl,"postDigest");function Dl(e,t){const n=Hn(Array.isArray(t)?t:[t],"the reviewer file");return Jn(e,a=>[...a.map(i=>n.find(o=>jt(o.name,i.name))||i),...n.filter(i=>!a.some(o=>jt(i.name,o.name)))])}r(Dl,"addReviewers");function Fl(e,t){return Jn(e,n=>{const a=t.filter(i=>!n.some(o=>jt(o.name,i)));return a.length&&d(`this board has no reviewer ${a.map(i=>`"${i}"`).join(", ")}`),n.filter(i=>!t.some(o=>jt(i.name,o)))})}r(Fl,"removeReviewers");const ir=r(()=>g.join(xn.homedir(),".claude","kickflip","settings.json"),"hubSettingsFile"),zn=r(e=>{try{return e()}catch(t){throw t instanceof Ds&&d(t.message),t}},"ruleCall"),Kn=r(async e=>Wa(await $(g.join(e,"board.json"),{})),"readBoardRules");let pi=Promise.resolve();function At(e,t){const n=pi.then(async()=>{const a=g.join(D(e),"board.json"),i=await $(a,{}),{set:o,...s}=t(i);return await y(a,{...i,...o}),{file:a,...s}});return pi=n.catch(()=>{}),n}r(At,"writeBoardJSON");const mi=r((e,t)=>At(e,n=>{const a=zn(()=>t(Wa(n)));return{set:{rules:a.rules},...a}}),"writeBoardRules");function Nl(e,t){const n=Object.keys(t||{}).sort().join(),a=n==="level,login",i=a?t.login:t?.[n],o=typeof i=="string"?i.trim():null;(!["add","remove","level,login"].includes(n)||!Cs(o))&&d("Send add or remove with one login, like name@github, or a listed login with its level."),a&&!["view","full"].includes(t.level)&&d("A level is view or full.");const s=r(({login:c,level:l})=>l==="full"?{login:c,level:l}:c,"stored");return At(e,c=>{const l=Bs(c);a&&!l.some(w=>w.login===o)&&d(`${o} isn't shared with yet. Add them first.`);const p=(n==="remove"?l.filter(w=>w.login!==o):a?l.map(w=>w.login===o?{login:o,level:t.level}:w):l.some(w=>w.login===o)?l:[...l,{login:o,level:"view"}]).map(s);return{set:{share:p},share:p}}).then(({share:c})=>({share:c}))}r(Nl,"saveBoardShare");const hi=r(e=>typeof e=="string"&&/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(e),"validGithubLogin"),gi=r(e=>{try{return e?Na(e):null}catch{return null}},"storedTickets"),Qn=r(e=>Number.isInteger(e)&&e>=1&&e<=99,"validCap"),Gn=["max_implementers","max_planners"],Se=r((e,t)=>{const n=gi(e?.tickets);return{auto_merge:e?.auto_merge===!0,auto_build:e?.auto_build===!0,auto_pull:e?.auto_pull===!0,max_implementers:Qn(e?.max_implementers)?e.max_implementers:qo,max_planners:Qn(e?.max_planners)?e.max_planners:Mo,auto_triage:e?.auto_triage===!0,auto_review:e?.auto_review===!0,idle_notification:e?.idle_notification===!0,skip_draft:e?.skip_draft===!0,prs:t?.hub?e?.prs===!0:e?.prs!==!1,...n?{tickets:n}:{},...hi(e?.github_login)?{github_login:e.github_login}:{},...Ma(e?.build_target)?{build_target:e.build_target}:{}}},"settingsOf");async function Rl(e,t,n=new Date){let a;try{a=ns(t,n)}catch(i){d(i.message)}return await At(e,()=>({set:{paused_until:a??void 0}})),a}r(Rl,"setBoardPause");async function or(e){const t=D(e);return Se(await $(g.join(t,"board.json"),{}),ge().find(n=>Pe(n.dir,t)))}r(or,"readBoardSettings");const El=r((e,t)=>!!e?.github&&(!e.local||e.hub===!0)&&t.prs,"opensPRs");async function lt(e,t){ot(t,["auto_merge","auto_build","auto_pull","max_implementers","max_planners","auto_triage","auto_review","idle_notification","skip_draft","prs","tickets","github_login","build_target"],"the settings"),Object.keys(t).length||d("the settings name nothing to change"),t={...t};for(const[n,a]of Object.entries(t))if(n==="tickets"){try{t.tickets=Na(a)??void 0}catch(o){d(o.message)}t.tickets?.source==="linear"&&!fs()&&d(ds);const i=t.tickets?.source==="github"&&e?ge().find(o=>Pe(o.dir,e)):null;i&&!i.github&&d("GitHub issues need the board\u2019s repo on GitHub, and this board\u2019s repo isn\u2019t; pick none or linear")}else n==="github_login"?a===""||a===null?t.github_login=void 0:hi(a)||d("github_login must be a GitHub login"):n==="build_target"?a===null?t.build_target=void 0:Ma(a)||d('build_target must name .kickflip/build-<target>: letters, digits, ".", "_" or "-", or null for .kickflip/build'):Gn.includes(n)?Qn(a)||d(`${n} must be a whole number from 1 to 99`):Ht(a,n);if(t.prs===!1){const n=e?ge().find(a=>Pe(a.dir,e)):null;n?.github&&!n.local&&!(await Os(n.owner?.repo)).pushMain&&d("Merge lands each card on main and pushes it, so turn on Push main after a land first")}return At(e,n=>{const a=e?ge().find(s=>Pe(s.dir,e)):null,i=Se(n,a),o=Se({...n,...t},a);return{set:t,settings:o,changed:Object.keys(t).filter(s=>JSON.stringify(i[s])!==JSON.stringify(o[s]))}})}r(lt,"saveBoardSettings");async function Ll(){if(us().set)return[];const e=[];for(const t of ge())(await or(t.dir)).tickets?.source==="linear"&&(await lt(t.dir,{tickets:{source:"none"}}),await he(t,"settings",{tickets:null}),e.push(`/${t.route} fell back to no ticket source: Hub has no Linear key`));return e}r(Ll,"fallBackFromLinear");async function Bl(e,t,n){const a=String(t).replace(/-/g,"_"),i=["auto_merge","auto_build","auto_pull","max_implementers","max_planners","auto_triage","auto_review","idle_notification","skip_draft","prs","github_login","tickets","build_target"];if(i.includes(a)||d(`no board setting "${t}"; settings are ${i.map(c=>c.replace(/_/g,"-")).join(", ")}`),a==="tickets"){let c;try{c=JSON.parse(n)}catch{d('settings set tickets takes the source as JSON, such as {"source":"github"}')}const{file:l,settings:u}=await lt(e,{tickets:c});return{file:l,lines:sr({tickets:u.tickets})}}if(a==="github_login"){const{file:c,settings:l}=await lt(e,{github_login:n});return{file:c,lines:[`github-login ${l.github_login||"unset"}`]}}if(a==="build_target"){const{file:c,settings:l}=await lt(e,{build_target:n==="default"?null:n});return{file:c,lines:[`build-target ${l.build_target||"default"}`]}}if(Gn.includes(a)){const{file:c,settings:l}=await lt(e,{[a]:/^\d+$/.test(n)?Number(n):n});return{file:c,lines:[`${t} ${l[a]}`]}}["on","off"].includes(n)||d(`settings set ${t} takes on or off, not "${n}"`);const{file:o,settings:s}=await lt(e,{[a]:n==="on"});return{file:o,lines:[`${t} ${s[a]?"on":"off"}`]}}r(Bl,"setBoardSetting");const sr=r(e=>Object.entries(e).map(([t,n])=>`setting ${t.replace(/_/g,"-")} ${t==="tickets"?JSON.stringify(n??{source:"none"}):t==="github_login"||Gn.includes(t)?n:t==="build_target"?n||"default":n?"on":"off"}`),"settingsLines");async function Vn(e,t,n){const a=t?zn(()=>n(Wt())):await mi(e,n);return t&&zn(()=>Ls(a.rules)),{file:t?ir():a.file,lines:[`id ${a.rule.id}`]}}r(Vn,"changeRules");async function $i(e,t,n){const a=r(s=>s.map(c=>c.id),"ids");n||D(e);const[i,o]=n?[a(Wt()),e?a(await Kn(e)):[]]:[a(await Kn(e)),a(Wt())];i.includes(t)||d(`no ${n?"hub":"board"} rule "${t}"${o.includes(t)?`; it is a ${n?"board":"hub"} rule, so ${n?"drop":"pass"} --hub`:""}`)}r($i,"checkRuleId");async function Cl(e){const t=r(n=>a=>`${n} ${a.id} ${a.by==="claude"?"claude":"owner"} ${a.text}`,"line");return{lines:[...Wt().map(t("hub")),...e?(await Kn(e)).map(t("board")):[]]}}r(Cl,"listRules");function Il(e,{text:t,hub:n=!1,by:a}){return n||D(e),Vn(e,n,i=>Fs(i,{text:t,by:a}))}r(Il,"addRuleTo");async function Tl(e,t,{text:n,hub:a=!1}){return await $i(e,t,a),Vn(e,a,i=>Ns(i,t,n))}r(Tl,"editRuleIn");async function ql(e,t,{hub:n=!1}){return await $i(e,t,n),Vn(e,n,a=>Rs(a,t))}r(ql,"removeRuleFrom");const Ml=r((e,t,n)=>mi(e,a=>Es(a,t,n)),"saveBoardRules");async function Wl(e,{id:t,title:n,summary:a}){D(e);const i=await $(E(e),{topics:[],items:[]}),o=i.topics||[];return o.some(s=>s.id===S(t,"the topic id"))&&d(`topic "${t}" already exists`),o.push({id:t.trim(),title:S(n,"--title"),...a===void 0?{}:{summary:S(a,"--summary")}}),await y(E(e),{...i,topics:o,items:i.items||[]}),E(e)}r(Wl,"addTopic");const ki=["quiet","info","notice","danger","success"];function rr(e,t){if(e.diagram===void 0!=(e.practice===void 0)&&d(`${t} needs diagram and practice together`),e.diagram===void 0)return;const n=e.diagram;(!Array.isArray(n)||!n.length)&&d(`${t}.diagram must be a non-empty array of rows`),n.forEach((a,i)=>{(!Array.isArray(a)||!a.length||a.length>4)&&d(`${t}.diagram[${i}] must be a row of 1 to 4 boxes`),a.forEach((o,s)=>{const c=`${t}.diagram[${i}][${s}]`;ot(o,["text","tone","to"],c),S(o.text,`${c}.text`),ee(o.tone,l=>ki.includes(l)?l:d(`${c}.tone takes ${ki.join(", ")}`)),ee(o.to,l=>Array.isArray(l)&&i+1<n.length&&l.every(u=>Number.isInteger(u)&&u>=0&&u<n[i+1].length)?l:d(`${c}.to must list boxes of the next row by index`))})}),(!Array.isArray(e.practice)||!e.practice.length)&&d(`${t}.practice must be a non-empty array of lines`),e.practice.forEach((a,i)=>S(a,`${t}.practice[${i}]`))}r(rr,"checkDiagram");function cr(e,t){(!Array.isArray(e)||!e.length)&&d(`${t}.options must be a non-empty array`);const n=new Set;e.forEach((a,i)=>{const o=`${t}.options[${i}]`;ot(a,["id","label","recommended","detail","diagram","practice"],o),rr(a,o),n.has(S(a.id,`${o}.id`))&&d(`${t} has two options with id "${a.id}"`),n.add(a.id),S(a.label,`${o}.label`).startsWith(`${a.id}. `)&&d(`${o}.label starts with "${a.id}. ", but the page adds the option letter itself`),ee(a.recommended,s=>Ht(s,`${o}.recommended`)),ee(a.detail,s=>typeof s=="string"?s:d(`${o}.detail must be text`))}),e.filter(a=>a.recommended).length>1&&d(`${t} recommends more than one option`)}r(cr,"checkOptions");function lr(e,t,n){ot(e,["option","card","answer","reason"],n),t.some(a=>a.id===S(e.option,`${n}.option`))||d(`${n}.option names no option "${e.option}"`),S(e.card,`${n}.card`),S(e.answer,`${n}.answer`),ee(e.reason,a=>S(a,`${n}.reason`))}r(lr,"checkSuggested");const dt=r(e=>g.join(e,"data","prototypes.json"),"prototypesFile"),Zn=r((e,t,n)=>g.join(e,"public","prototypes",t,`${n}.js`),"takeFile");async function Pt(e){const t=e?(await $(dt(e),{})).prototypes:null;return Array.isArray(t)?t:[]}r(Pt,"readPrototypes");const dr=r(e=>typeof e=="string"&&/^[a-z0-9-]+$/.test(e)?e:d(`a prototype topic uses only a-z, 0-9 and "-", not "${e}"`),"prototypeTopic");function ur(e){const t=/^([^:]*):(.*)$/s.exec(e);(!t||!/^[A-Z]$/.test(t[1]))&&d(`--take takes KEY:"Label" with KEY one capital letter, not "${e}"`);const n=S(t[2],`--take ${t[1]}'s label`);return/^[A-Z]\.\s/.test(n)&&d(`--take ${t[1]}'s label starts with "${n.slice(0,3)}"; the take pill shows the label alone, so drop the letter`),{key:t[1],label:n}}r(ur,"prototypeTake");function fr(e){const t=/^([^:]*):(.*)$/s.exec(e);return(!t||!/^[a-z0-9][a-z0-9-]*$/.test(t[1]))&&d(`--state takes KEY:"Label" with KEY in a-z, 0-9 and "-", not "${e}"`),{key:t[1],label:S(t[2],`--state ${t[1]}'s label`)}}r(fr,"prototypeState");const bi=r(e=>e.find((t,n)=>e.findIndex(a=>a.key===t.key)!==n),"twiceIn"),Ne=r(e=>/^prototype-(.+)\.html$/.exec(g.posix.basename(String(e||"").split(/[?#]/)[0]))?.[1],"pageTopic");async function Ul(e,{topic:t,title:n,card:a,takes:i=[],served:o,states:s=[]}){D(e),dr(t),S(n,"--title"),i.length||d('prototype add needs a --take KEY:"Label" for each take');const c=i.map(ur),l=bi(c);l&&d(`--take ${l.key} is given twice`),o!==void 0&&!/^\/\S*$/.test(o)&&d(`--served takes the page's path on the served site, starting with "/", not "${o}"`),s.length&&o===void 0&&d("--state goes with --served: only a served round re-renders its states in place");const u=s.map(fr),p=bi(u);p&&d(`--state ${p.key} is given twice`),a!==void 0&&await x(e,a);const w=await Pt(e);w.some(m=>m.topic===t)&&d(`prototype ${t} is already recorded; run prototype rm ${t} first, or pick another topic`),await j.stat(g.join(e,"public",`prototype-${t}.html`)).then(()=>!0,()=>!1)&&d(`public/prototype-${t}.html is a hand-written page that questions may link; pick another topic`);const f={topic:t,title:n.trim(),...a===void 0?{}:{card:a},...o===void 0?{}:{served:o},takes:c,...u.length?{states:u}:{},at:new Date().toISOString()};await y(dt(e),{prototypes:[...w,f]});const h=`page prototype-${t}.html`;return o!==void 0?{file:dt(e),lines:[h,`served ${o} from the base prototype worktree (board.mjs prototype base)`,...u.length?[`states ${u.map(m=>m.key).join(", ")}`]:[]]}:{file:dt(e),lines:[h,...c.map(m=>`take ${m.key} ${Zn(e,t,m.key)}`)]}}r(Ul,"addPrototype");async function Hl(e){D(e);const t=await Pt(e);return{lines:t.length?t.map(n=>[n.topic,n.title,n.takes.map(a=>`${a.key}: ${a.label}`).join(", "),n.card,n.served&&`served ${n.served}`,n.states&&`states ${n.states.map(a=>a.key).join(", ")}`].filter(Boolean).join(" \xB7 ")):["no prototypes recorded"]}}r(Hl,"listPrototypes");async function Jl(e){const t=(await Pt(e)).filter(a=>a.served).map(a=>a.topic);if(!t.length)return[];const n=await $(E(e),{items:[]});return t.filter(a=>(n.items||[]).some(i=>!i.resolved&&Ne(i.url)===a))}r(Jl,"servedRoundsOpen");async function zl(e,t){D(e);const n=await Pt(e);n.some(o=>o.topic===t)||d(`no prototype "${t}" is recorded`);const i=((await $(E(e),{items:[]})).items||[]).filter(o=>!o.resolved&&Ne(o.url)===t).map(o=>o.id);return i.length&&d(`prototype ${t} backs open question ${i.join(", ")}; resolve ${i.length===1?"it":"them"} first`),await y(dt(e),{prototypes:n.filter(o=>o.topic!==t)}),dt(e)}r(zl,"removePrototype");async function wr(e,t,n){const a=t.url?.split(/[?#]/)[0];if(!a||!g.posix.basename(a).startsWith("prototype-"))return;const i=g.posix.basename(a),o=t.url.split("#")[0].split("?").slice(1).join("?"),s=`${n}.url`;new URLSearchParams(o).get("q")!==t.id&&d(`${s} must be ${i}?q=${t.id} so the pill can pick`);const c=Ne(a),l=(await Pt(e)).find(f=>f.topic===c);l||d(`${s} names ${i}, which is not a recorded prototype; record it with board.mjs prototype add ${c} --title "\u2026" --take A:"\u2026", then write its take files`);const u=l.takes.map(f=>f.key),p=t.options.map(f=>f.id),w=p.map(f=>u.indexOf(f));if(w.some((f,h)=>f<0||h>0&&f<w[h-1])&&d(`${n}.options ids ${p.join(", ")} must be a non-empty subset of ${i}'s takes, in take order: ${u.join(", ")}`),!l.served)for(const f of u)await j.stat(Zn(e,c,f)).then(h=>h.isFile(),()=>!1)||d(`${s} take ${f} has no file yet; write ${Zn(e,c,f)}`)}r(wr,"checkPrototype");async function Kl(e,t,n=async()=>{}){D(e);const a=Array.isArray(t)?t:[t];a.length||d("the question file is empty");const i=await $(E(e),{topics:[],items:[]}),o=i.items||[],s=await $(ye(e),{}),c=new Set([...o,...Object.values(s).flat()].map(w=>w.id)),l=new Set((i.topics||[]).map(w=>w.id));for(const[w,f]of a.entries()){const h=`question ${f?.id??w+1}`;if(ot(f,["id","topic","source","question","context","url","urgent","options","round_of","reask_of","ask","suggested","prototype_gate","ticket_gate"],h),c.has(Fe(f.id,`${h}.id`))&&d(`question id "${f.id}" is already used`),c.add(f.id),S(f.topic,`${h}.topic`),f.topic.startsWith("item:ask:")){const m=await ze(e,await Dt(e,f.topic.slice(9),`${h}.topic`));d(`${h}.topic: an ask's questions go on its card's topic${m?`, item:${m.key}`:""}`)}else f.topic.startsWith("item:")?await x(e,f.topic.slice(5)):f.topic.startsWith("wave:")?await pt(e,f.topic.slice(5)):f.topic.startsWith("file:")?await Xr(e,f.topic.slice(5)):l.has(f.topic)||d(`topic "${f.topic}" is not declared; add it with: topic add ${f.topic} --title <t>`);S(f.question,`${h}.question`),f.question.length>Ba&&d(`${h}.question runs to ${f.question.length} characters, past ${Ba}; keep the question to one line and move the detail into context or the option details, which take markdown`);for(const m of["source","context"])ee(f[m],k=>S(k,`${h}.${m}`));ee(f.url,m=>En(m,`${h}.url`)),ee(f.urgent,m=>Ht(m,`${h}.urgent`)),cr(f.options,h),ee(f.prototype_gate,m=>Si(m,f,`${h}.prototype_gate`)),f.ticket_gate!==void 0&&Si(f.ticket_gate,f,`${h}.ticket_gate`)&&await mr(e,f,h),ee(f.suggested,m=>lr(m,f.options,`${h}.suggested`)),await wr(e,f,h),f.ask!==void 0&&await mc(e,f,h),ee(f.round_of,m=>{const k=o.find(_=>_.id===m);k||d(`${h}.round_of names no question "${m}" in owner.json`),k.topic!==f.topic&&d(`${h}.round_of names ${m} on topic ${k.topic}; a round stays on its question's topic, ${f.topic}`),Ne(f.url)||d(`${h}.round_of needs a url on a recorded prototype, prototype-<topic>.html?q=${f.id}`)}),ee(f.reask_of,m=>{const k=o.find(_=>_.id===m);k||d(`${h}.reask_of names no question "${m}" in owner.json`),k.topic!==f.topic&&d(`${h}.reask_of names ${m} on topic ${k.topic}; a re-ask stays on its question's topic, ${f.topic}`),k.resolved||d(`${h}.reask_of names ${m}, which is not resolved; only a resolved question is asked again`)})}for(const w of new Set(a.map(f=>Ne(f.url)).filter(Boolean)))await n(e,w);const u=[],p=[];for(const w of a){const f=w.topic.startsWith("item:")&&!w.topic.startsWith("item:ask:")?w.topic.slice(5):null,h=f&&await yi(e,f);h&&((s[f]||=[]).push(w),p.push(w),u.push(`staged: ${w.id} waits for ${f}'s ${h} to finish; the card's staged questions then publish together`))}return p.length&&await y(ye(e),s),p.length===a.length?{file:ye(e),lines:u,staged:p.length}:_i(e,i,o,a.filter(w=>!p.includes(w)),u)}r(Kl,"addQuestions");async function yi(e,t){const{source:n}=await x(e,t);for(const a of await G(n.dir,F(t).id)){const i=await $(a,null);if(i?.started&&!i.done&&qe(i)&&i.kind!==me)return i.kind}return null}r(yi,"runningPlanner");async function ut(e,t,{screen:n=!1}={}){if(!e||await yi(e,t))return;const a=await $(ye(e),{});if(!a[t])return;const i=await $(it(e),{});if(n){i[t]||await y(it(e),{...i,[t]:new Date().toISOString()});return}const{[t]:o,...s}=a,c=await $(E(e),{topics:[],items:[]});await _i(e,c,c.items||[],o,[]),await y(ye(e),s),i[t]&&await y(it(e),Yn(i,t))}r(ut,"publishStaged");const Yn=r((e,t)=>Object.fromEntries(Object.entries(e).filter(([n])=>n!==t)),"withoutKey");async function xi(e){const t=await $(it(e),{}),n=await $(ye(e),{});return Object.fromEntries(Object.entries(t).filter(([a])=>n[a]).map(([a,i])=>[a,{since:i,ids:n[a].map(o=>o.id)}]))}r(xi,"readScreening");async function Ql(e,t,n){D(e);const a=S(n,"--note"),i=await $(ye(e),{}),o=await $(it(e),{}),s=Object.keys(i).find(w=>i[w].some(f=>f.id===t));if(!s)return pr(e,t,a);o[s]||d(`${t} waits for ${s}'s planner to finish`);const c=i[s].find(w=>w.id===t);c.prototype_gate&&d(`${t} is the card's prototype question, which is the owner's; question publish ${s} passes it on`),c.ticket_gate&&d(`${t} is the card's ticket question, which is the owner's; question publish ${s} passes it on`);const l=new Date().toISOString(),u=await $(E(e),{topics:[],items:[]});await y(E(e),{...u,topics:u.topics||[],items:[...u.items||[],{...c,asked_at:l,resolved:`Settled by the orchestrator: ${a}`,resolved_at:l}]});const p=i[s].filter(w=>w.id!==t);return await y(ye(e),p.length?{...i,[s]:p}:Yn(i,s)),p.length||await y(it(e),Yn(o,s)),await Re(e,s,`Answers (orchestrator, ${Qe()})`,`- ${t} (${c.question}): settled by the orchestrator. "${a}"`),E(e)}r(Ql,"settleQuestion");async function pr(e,t,n){const a=await $(E(e),{topics:[],items:[]}),i=(a.items||[]).find(c=>c.id===t&&c.topic?.startsWith("item:"));i||d(`no staged question "${t}"; question settle takes a question a finished grill left for you to screen, or a card's question while it waits on your re-check`);const o=i.topic.slice(5);(await x(e,o)).item.recheck_at||d(`${o} waits on no re-check, so its question ${t} stays answered as it is`);const s=new Date().toISOString();return await y(E(e),{...a,items:a.items.map(c=>c===i?{...c,resolved:`Settled by the orchestrator: ${n}`,resolved_at:s}:c)}),await Re(e,o,`Answers (orchestrator, ${Qe()})`,`- ${t} (${i.question}): settled by the orchestrator. "${n}"`),E(e)}r(pr,"settleStale");async function Gl(e,t){D(e);const n=(await xi(e))[t]?.ids||d(`${t} has no questions waiting on you to screen`);return await ut(e,t),n}r(Gl,"publishScreened");async function Vl(e,t=new Date){const n=Object.entries(await xi(e)).filter(([,a])=>t-Date.parse(a.since)>=Ts).map(([a])=>a);for(const a of n)await ut(e,a),await W(e,a,"screen-timeout");return n}r(Vl,"publishUnscreened");async function _i(e,t,n,a,i){const{answers:o,requests:s,reopens:c}=await ft(e),l=new Date().toISOString(),u=[],p=[];for(const f of a.filter(h=>h.round_of))f.round=jn(n.find(h=>h.id===f.round_of))+1,ve(t,[f.round_of],`Followed by prototype round ${f.round}`,l),p.push(f.round_of);for(const f of a.filter(h=>h.reask_of))ve(t,[f.reask_of],`Asked again as ${f.id}`,l),p.push(f.reask_of);for(const f of a){const h=Ne(f.url),m=n.filter(_=>_.topic===f.topic).map(_=>Te(_,o,s,[],c)),k=f.topic.startsWith("item:")?Ni(m):[];for(const _ of m)k.includes(_.id)?(ve(t,[_.id],`Followed by ${f.id}`,l),p.push(_.id)):!_.resolved&&h&&Ne(_.url)===h&&(ve(t,[_.id],`Replaced by ${f.id}`,l),u.push(_.id))}const w={...t,topics:t.topics||[],items:[...n,...a.map(f=>({...f,asked_at:l}))]};await y(E(e),w);for(const f of new Set(a.map(h=>Xn(h.topic)).filter(Boolean)))await xr(e,f);return{file:E(e),lines:[...i,...u.map(f=>`replaced: ${f}`),...p.map(f=>`resolved: ${f}`)]}}r(_i,"publishQuestions");function Si(e,t,n){return Ht(e,n),e?((!t.topic.startsWith("item:")||t.topic.startsWith("item:ask:"))&&d(`${n} goes on a card's topic, item:<file>/<id>`),t.options?.map(a=>a.id).join()!=="yes,no"&&d(`${n} takes exactly the options yes and no, in that order`),!0):!1}r(Si,"checkGate");async function mr(e,t,n){const a=t.topic.slice(5),{item:i}=await x(e,a);i.ticket&&d(`${n}.ticket_gate: ${a} already has ticket ${i.ticket}`),await sn(e)||d(`${n}.ticket_gate needs a board with a ticket source; with none every card stays a task`)}r(mr,"checkTicketGate");async function Zl(e,t,n){const a=await $(E(e),{topics:[],items:[]}),i=new Map((a.items||[]).map(l=>[l.id,l]));for(const l of t){const u=i.get(String(l.id));if(!u?.ticket_gate||!["yes","no"].includes(l.choice))continue;ve(a,[u.id],`Ticket: ${l.choice}.${l.choice==="yes"?" The board makes it from the card's title and notes.":""}`),await y(E(e),a);const p=u.topic.slice(5);l.choice==="yes"&&await ji(e,p,"making its ticket",async()=>{await $e(e,p,{newTicket:!0},n),await W(e,p,"ticket-made",{ticket:(await x(e,p)).item.ticket})})}for(const l of t){const u=i.get(String(l.id));if(!u?.prototype_gate||!["yes","no"].includes(l.choice))continue;ve(a,[u.id],`Prototype round: ${l.choice}. The board moves the card on once every question is answered.`),await y(E(e),a);const p=u.topic.slice(5);l.choice==="yes"&&(await x(e,p).catch(()=>null))?.item.phase==="grilling"&&await aa(e,p)&&await W(e,p,"prototype-after")}const o=new Map;for(const l of t){const u=i.get(String(l.id)),p=Xn(u?.topic);p&&o.set(p,[...o.get(p)||[],hr(u,l)])}const s=[],c=[];for(const[l,u]of o)await x(e,l).catch(()=>null)&&await ji(e,l,"writing them into its notes",async()=>{await Re(e,l,`Answers (owner, ${Qe()})`,u.join(`
18
- `));const p=await vi(e,l);p==="prototyping"&&s.push(l),p==="staging"&&c.push(l)});return{prototype:s,staged:c}}r(Zl,"settleAnswers");async function vi(e,t,{roundAt:n=!1}={}){const{item:a}=await x(e,t),{asked:i}=await fe(e,t);if(i.some(o=>o.state==="open"))return null;if(a.phase==="grilling"&&i.some(o=>o.prototype_gate&&o.answers.at(-1)?.choice==="yes")){if(await $e(e,t,{phase:"prototyping"}),n){const{source:o,itemFile:s,item:c}=await x(e,t);c.round_at=new Date().toISOString(),await y(o.file,s)}return await W(e,t,"prototype-round"),"prototyping"}return await br(e,t)?(await na(e,t),"staging"):null}r(vi,"moveOnAnswered");async function ji(e,t,n,a){try{await a()}catch(i){if(!(i instanceof ue))throw i;await W(e,t,"answers-failed",{what:n,error:i.message})}}r(ji,"followUp");const Xn=r(e=>typeof e=="string"&&e.startsWith("item:")&&!e.startsWith("item:ask:")?e.slice(5):null,"cardOfTopic"),Vt=r(e=>String(e).padStart(2,"0"),"pad2"),Qe=r((e=new Date)=>`${Vt(e.getMonth()+1)}/${Vt(e.getDate())} ${Vt(e.getHours())}:${Vt(e.getMinutes())}`,"noteStamp");function hr(e,t){const n=t.choice===null||t.choice===void 0||t.choice===""?null:(e.options||[]).find(i=>i.id===t.choice)?.label??String(t.choice),a=String(t.note||"").trim();return`- ${e.id} (${e.question}): ${n?`${n}.`:"no option picked."}${a?` Note, verbatim: "${a}"`:""}`}r(hr,"answerLine");const Oi=292,ea=r(e=>String(e).toLowerCase().replace(/[^a-z0-9-]+/g,"-"),"notesBase"),gr=r((e,t,n)=>nt(g.join(e,`.${ea(t)}-notes`),n),"notesLock");function Zt(e,t){let n=`${ea(t)}-notes.md`;for(let a=2;de(g.join(e,n));a++)n=`${ea(t)}-notes-${a}.md`;return n}r(Zt,"freeNotesName");async function Yt(e,t){const n=g.join(g.dirname(e),`.${g.basename(e)}.${process.pid}.tmp`);await j.rm(n,{force:!0}),await j.writeFile(n,t,{mode:Oi}),await j.rename(n,e)}r(Yt,"writeNotesFile");const $r=r(e=>/\n/.test(S(e,"--heading"))?d("--heading is one line, the section's title"):S(e,"--heading"),"notesHeading"),kr=r((e,t,n)=>`${e.replace(/\n*$/,`
14
+ ${Fn}`,login:l});return await pi(e,i,{state:"posted",text:o,review_url:p}),{lines:[`posted review on PR #${t}: ${p}`]}}catch(u){if(u instanceof pe)throw u;d(u.message)}}r(_l,"postIncomingReview");const Vt=r(e=>g.join(e,"data","handshake.json"),"handshakeFile");async function mi(e){const t=await $(Vt(e),null);if(!t?.hello)return t;const{hello:n,...a}=t;return{...a,ping:a.ping||n}}r(mi,"readHandshake");async function Sl(e,t){D(e),Re(t,"the event id");const n=(await z(g.join(e,"data","events.jsonl"))).find(i=>i.id===t);n?.kind!=="ping"&&n?.kind!=="hello"&&d(`no ping event "${t}" in this site's data/events.jsonl`);const a=await mi(e)||{};return a.ping?.id&&a.ping.id!==t&&d(`"${t}" is an older ping; ack the newest, "${a.ping.id}"`),await y(Vt(e),{...a,ping:a.ping||{id:t,at:n.ts,session:null},ack:{id:t,at:new Date().toISOString()}}),Vt(e)}r(Sl,"ackPing");async function vl(e){const t=await mi(e);t&&!t.done_at&&await y(Vt(e),{...t,done_at:new Date().toISOString()})}r(vl,"finishHandshake");const Hn={name:/^[^\n]{1,40}$/,slack_user:/^U[A-Z0-9]+$/,dm:/^D[A-Z0-9]+$/,github:/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/},At=r((e,t)=>e.trim().toLowerCase()===t.trim().toLowerCase(),"sameName");function Jn(e,t){return Array.isArray(e)||d(`${t} must be a JSON array of reviewers`),e.forEach((n,a)=>{const i=`${t}[${a}]`;rt(n,Object.keys(Hn),i);for(const[o,s]of Object.entries(Hn))(typeof n[o]!="string"||!s.test(n[o].trim()))&&d(`${i}.${o} must match ${s}`);e.slice(0,a).some(o=>At(o.name,n.name))&&d(`${t} names "${n.name}" twice`)}),e.map(n=>Object.fromEntries(Object.keys(Hn).map(a=>[a,n[a].trim()])))}r(Jn,"checkReviewers");async function zn(e,t){const{file:n,reviewers:a}=await Dt(e,o=>{const s=Jn(t(o.reviewers||[]),"the reviewers");return{set:{reviewers:s},reviewers:s}}),i=[];for(const o of await q(e)){const s=await $(o.file,null);(s?.items||[]).some(c=>!Q(c))&&(await y(o.file,{...s,reviewers:a}),i.push(`item file ${o.slug}: reviewers copied`))}return{file:n,lines:i}}r(zn,"writeReviewers");const jl=r((e,t)=>zn(e,()=>Jn(t,"the reviewer file")),"setReviewers"),dt=r(e=>g.join(D(e),"data","pr-slack.json"),"prSlackFile"),Zt=r(e=>$(dt(e),{}),"readPRSlack"),Pt=r(e=>!!(e?.ts||e?.posting_at),"held");async function Ge(e,t){return it(e,async()=>{const n=JSON.parse(await j.readFile(e,"utf8").catch(()=>"{}")),a=t(n);return await He(e,JSON.stringify(n,null,2)+`
15
+ `),a})}r(Ge,"editJSON");async function Yt(e,t,n){await Ge(e,i=>{const o=t(i);Pt(o)&&d("that Slack message has already gone out"),o.posting_at=new Date().toISOString()});let a;try{a=await n()}catch(i){throw await Ge(e,o=>{delete t(o).posting_at}),i}try{await Ge(e,i=>{const o=t(i);o.ts=a,delete o.posting_at})}catch(i){d(`posted, but its record failed, so it won't go again: ${i.message}`)}return a}r(Yt,"postOnce");const Ol=r((e,t,n,a)=>Yt(dt(e),i=>{const o=i[t.pr]||={head:t.head,links:{},replies:[]};return o.head??=t.head,Object.assign(o.links[t.reviewer]||={reactions:[]},{channel:n})},()=>a(n)),"postLink"),Al=r((e,t,n)=>Yt(dt(e),a=>a[t.pr].replies.find(i=>i.id===t.id).to[t.reviewer],n),"postReply"),Pl=r((e,t)=>Ge(dt(e),n=>{const a=n[t.pr].links[t.reviewer];a.reactions.includes(t.emoji)||a.reactions.push(t.emoji)}),"recordReaction"),hi=r((e,t,n,a)=>Ge(dt(e),i=>{const o=i[t],s=Object.keys(o?.links||{}).filter(c=>o.links[c].ts);return s.length?(o.replies.push({id:`${n}-${o.replies.length+1}`,text:a,to:Object.fromEntries(s.map(c=>[c,{}]))}),s):[]}),"queueReply");function or(e,t,n){const a=e.commits||[],i=a.findIndex(o=>o.oid===n);return[`New push to #${t}`,a.slice(i+1).map(o=>o.subject).join(" \xB7 ")].filter(Boolean).join(`
16
+ `)}r(or,"pushText");async function Dl(e){const t=await $(An,{}),n=r((a,i)=>{const o=ie(t.prs,a)?.head;return!!o&&o!==i.head&&Object.values(i.links).some(s=>s.ts)},"moved");return Object.entries(await Zt(e)).some(([a,i])=>n(a,i))?Ge(dt(e),a=>{const i=[];for(const[o,s]of Object.entries(a)){if(!n(o,s))continue;const c=ie(t.prs,o),l=Object.keys(s.links).filter(u=>s.links[u].ts);s.head&&(s.replies.push({id:`push-${s.replies.length+1}`,text:or(c,o.split("#")[1],s.head),to:Object.fromEntries(l.map(u=>[u,{}]))}),i.push(o)),s.head=c.head}return i}):[]}r(Dl,"queuePushes");function sr(e,t,n){const a=Number.isInteger(e.additions)?`+${e.additions}/-${e.deletions}, ${e.files} file${e.files===1?"":"s"}`:null;return[e.title||n.title,Rn(t),a].filter(Boolean).join(`
17
+ `)}r(sr,"linkText");const gi=r(e=>!!e?.approved||e?.reviewDecision==="APPROVED","approved");async function Fl(e,t){D(e);const n=await Ja.waiting("slack");if(n)return{waiting:n,links:[],replies:[],reactions:[],digest:null};const a=await se(e),i=await $(An,{}),o=await Zt(e),s=[];for(const p of await q(e)){const w=await $(p.file,{}),f=En(w,a.reviewers);for(const h of w.items||[])if(h.lane!=="done")for(const m of Ue(h,re(e,w))){const b=Ot(m),_=ie(i.prs,b);if(!(_?.state!=="OPEN"||_.isDraft))for(const N of f)Pt(o[b]?.links?.[N.name])||s.some(v=>v.pr===b&&v.reviewer===N.name)||s.push({pr:b,key:`${p.slug}/${h.id}`,reviewer:N.name,slack_user:N.slack_user,head:_.head||null,text:sr(_,m,h)})}}const c=[],l=[];for(const[p,w]of Object.entries(o)){const f=ie(i.prs,p),h=[gi(f)&&"git_approved",f?.state==="MERGED"?"git_merged":f?.state==="CLOSED"&&"x"].filter(Boolean);for(const[m,b]of Object.entries(w.links))if(b.ts)for(const _ of h)b.reactions.includes(_)||l.push({pr:p,reviewer:m,channel:b.channel,ts:b.ts,emoji:_});for(const m of w.replies)for(const[b,_]of Object.entries(m.to))Pt(_)||c.push({pr:p,id:m.id,reviewer:b,channel:w.links[b].channel,thread_ts:w.links[b].ts,text:m.text})}const u=await rr(e,t);return{links:s,replies:c,reactions:l,digest:u.due?u:null}}r(Fl,"reviewDue");async function Nl(e,t,n){D(e);const a=S(n,"what changed"),{itemFile:i,item:o}=await x(e,t),s=[];for(const l of Ue(o,re(e,i)))s.push(...await hi(e,Ot(l),"updated",`Updated: ${a}`));const c=[...new Set(s)];return{lines:[c.length?`Updated queued for ${c.join(", ")} on ${t}`:`no reviewer has a link to ${t}'s PRs; nothing queued`]}}r(Nl,"reviewUpdated");async function Rl(e,t){const{itemFile:n,item:a}=await x(e,t),i=await Zt(e);return Ue(a,re(e,n)).some(o=>Object.values(i[Ot(o)]?.links||{}).some(s=>s.ts))}r(Rl,"cardLinked");const $i=r(e=>g.join(D(e),"data","review-digests.json"),"digestFile");async function rr(e,{now:t=new Date}={}){D(e);const n=await se(e),a=await $(An,{}),i=await Zt(e),o=Ps(),s=new Map;for(const f of await q(e)){const h=await $(f.file,{}),m=En(h,n.reviewers);for(const b of m)s.has(b.name)||s.set(b.name,{name:b.name,slack_user:b.slack_user,dm:b.dm,items:[]});for(const b of h.items||[]){if(b.lane==="done")continue;const _=`${f.slug}/${b.id}`,N=As(h,b,_,o);for(const v of Ue(b,re(e,h))){const O=Ot(v),A=ie(a.prs,O);if(!(A?.state!=="OPEN"||A.isDraft||A.failing?.length||gi(A)))for(const L of m){const I=i[O]?.links?.[L.name]?.ts||ie(N,L.name)?.ts||null,P=I?(t.getTime()-Number(I)*1e3)/864e5:null,B=P>2?Math.floor(P):null;s.get(L.name).items.push({key:_,pr:O,title:b.title,url:Rn(v),sent_ts:I,...B?{days_old:B}:{},text:`${Rn(v)} \xB7 ${b.title}${B?` (${B} days old)`:""}`})}}}}const{date:c,open:l}=Ds(t),u=((await $($i(e),{})).digests||[]).filter(f=>f.date===c),p=await Ja.waiting("slack");for(const f of u)s.has(f.reviewer)||s.set(f.reviewer,{name:f.reviewer,items:[]});const w=[...s.values()].map(f=>{const h=u.find(b=>b.reviewer===f.name),m=h?!h.posting_at&&(!h.ts||h.replies.some(b=>!Pt(b))):f.items.length>0;return{...f,owed:m,...h?{sent:h}:{}}}).filter(f=>f.items.length||f.sent);return{date:c,due:!p&&l&&w.some(f=>f.owed),...p?{waiting:p}:{},reviewers:w}}r(rr,"reviewDigest");async function El(e,t,n,{channel:a,header:i,post:o}){const s=$i(e),c=r(p=>(p.digests||[]).find(w=>w.date===t&&w.reviewer===n.name),"entry");await Ge(s,p=>{c(p)||(p.digests=[...(p.digests||[]).filter(w=>w.date===t),{date:t,reviewer:n.name,channel:a,replies:n.items.map(w=>({pr:w.pr,text:w.text}))}],delete p.last_date)});const l=c(await $(s,{})),u=l.ts||await Yt(s,c,()=>o({channel:l.channel,text:i(l.replies.length)}));for(const[p,w]of l.replies.entries())Pt(w)||await Yt(s,f=>c(f).replies[p],()=>o({channel:l.channel,thread_ts:u,text:w.text}))}r(El,"postDigest");function Ll(e,t){const n=Jn(Array.isArray(t)?t:[t],"the reviewer file");return zn(e,a=>[...a.map(i=>n.find(o=>At(o.name,i.name))||i),...n.filter(i=>!a.some(o=>At(i.name,o.name)))])}r(Ll,"addReviewers");function Bl(e,t){return zn(e,n=>{const a=t.filter(i=>!n.some(o=>At(o.name,i)));return a.length&&d(`this board has no reviewer ${a.map(i=>`"${i}"`).join(", ")}`),n.filter(i=>!t.some(o=>At(i.name,o)))})}r(Bl,"removeReviewers");const cr=r(()=>g.join(_n.homedir(),".claude","kickflip","settings.json"),"hubSettingsFile"),Kn=r(e=>{try{return e()}catch(t){throw t instanceof Es&&d(t.message),t}},"ruleCall"),Qn=r(async e=>Ka(await $(g.join(e,"board.json"),{})),"readBoardRules");let ki=Promise.resolve();function Dt(e,t){const n=ki.then(async()=>{const a=g.join(D(e),"board.json"),i=await $(a,{}),{set:o,...s}=t(i);return await y(a,{...i,...o}),{file:a,...s}});return ki=n.catch(()=>{}),n}r(Dt,"writeBoardJSON");const bi=r((e,t)=>Dt(e,n=>{const a=Kn(()=>t(Ka(n)));return{set:{rules:a.rules},...a}}),"writeBoardRules");function Cl(e,t){const n=Object.keys(t||{}).sort().join(),a=n==="level,login",i=a?t.login:t?.[n],o=typeof i=="string"?i.trim():null;(!["add","remove","level,login"].includes(n)||!Ms(o))&&d("Send add or remove with one login, like name@github, or a listed login with its level."),a&&!["view","full"].includes(t.level)&&d("A level is view or full.");const s=r(({login:c,level:l})=>l==="full"?{login:c,level:l}:c,"stored");return Dt(e,c=>{const l=qs(c);a&&!l.some(w=>w.login===o)&&d(`${o} isn't shared with yet. Add them first.`);const p=(n==="remove"?l.filter(w=>w.login!==o):a?l.map(w=>w.login===o?{login:o,level:t.level}:w):l.some(w=>w.login===o)?l:[...l,{login:o,level:"view"}]).map(s);return{set:{share:p},share:p}}).then(({share:c})=>({share:c}))}r(Cl,"saveBoardShare");const yi=r(e=>typeof e=="string"&&/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(e),"validGithubLogin"),xi=r(e=>{try{return e?La(e):null}catch{return null}},"storedTickets"),Gn=r(e=>Number.isInteger(e)&&e>=1&&e<=99,"validCap"),Vn=["max_implementers","max_planners"],je=r((e,t)=>{const n=xi(e?.tickets);return{auto_merge:e?.auto_merge===!0,auto_build:e?.auto_build===!0,auto_pull:e?.auto_pull===!0,max_implementers:Gn(e?.max_implementers)?e.max_implementers:Ho,max_planners:Gn(e?.max_planners)?e.max_planners:Jo,auto_triage:e?.auto_triage===!0,auto_review:e?.auto_review===!0,idle_notification:e?.idle_notification===!0,skip_draft:e?.skip_draft===!0,prs:t?.hub?e?.prs===!0:e?.prs!==!1,...n?{tickets:n}:{},...yi(e?.github_login)?{github_login:e.github_login}:{},...za(e?.build_target)?{build_target:e.build_target}:{}}},"settingsOf");async function Il(e,t,n=new Date){let a;try{a=ss(t,n)}catch(i){d(i.message)}return await Dt(e,()=>({set:{paused_until:a??void 0}})),a}r(Il,"setBoardPause");async function lr(e){const t=D(e);return je(await $(g.join(t,"board.json"),{}),be().find(n=>Fe(n.dir,t)))}r(lr,"readBoardSettings");const Tl=r((e,t)=>!!e?.github&&(!e.local||e.hub===!0)&&t.prs,"opensPRs");async function ut(e,t){rt(t,["auto_merge","auto_build","auto_pull","max_implementers","max_planners","auto_triage","auto_review","idle_notification","skip_draft","prs","tickets","github_login","build_target"],"the settings"),Object.keys(t).length||d("the settings name nothing to change"),t={...t};for(const[n,a]of Object.entries(t))if(n==="tickets"){try{t.tickets=La(a)??void 0}catch(o){d(o.message)}t.tickets?.source==="linear"&&!hs()&&d(ps);const i=t.tickets?.source==="github"&&e?be().find(o=>Fe(o.dir,e)):null;i&&!i.github&&d("GitHub issues need the board\u2019s repo on GitHub, and this board\u2019s repo isn\u2019t; pick none or linear")}else n==="github_login"?a===""||a===null?t.github_login=void 0:yi(a)||d("github_login must be a GitHub login"):n==="build_target"?a===null?t.build_target=void 0:za(a)||d('build_target must name .kickflip/build-<target>: letters, digits, ".", "_" or "-", or null for .kickflip/build'):Vn.includes(n)?Gn(a)||d(`${n} must be a whole number from 1 to 99`):Kt(a,n);if(t.prs===!1){const n=e?be().find(a=>Fe(a.dir,e)):null;n?.github&&!n.local&&!(await Fs(n.owner?.repo)).pushMain&&d("Merge lands each card on main and pushes it, so turn on Push main after a land first")}return Dt(e,n=>{const a=e?be().find(s=>Fe(s.dir,e)):null,i=je(n,a),o=je({...n,...t},a);return{set:t,settings:o,changed:Object.keys(t).filter(s=>JSON.stringify(i[s])!==JSON.stringify(o[s]))}})}r(ut,"saveBoardSettings");async function ql(){if(ms().set)return[];const e=[];for(const t of be())(await lr(t.dir)).tickets?.source==="linear"&&(await ut(t.dir,{tickets:{source:"none"}}),await ke(t,"settings",{tickets:null}),e.push(`/${t.route} fell back to no ticket source: Hub has no Linear key`));return e}r(ql,"fallBackFromLinear");async function Ml(e,t,n){const a=String(t).replace(/-/g,"_"),i=["auto_merge","auto_build","auto_pull","max_implementers","max_planners","auto_triage","auto_review","idle_notification","skip_draft","prs","github_login","tickets","build_target"];if(i.includes(a)||d(`no board setting "${t}"; settings are ${i.map(c=>c.replace(/_/g,"-")).join(", ")}`),a==="tickets"){let c;try{c=JSON.parse(n)}catch{d('settings set tickets takes the source as JSON, such as {"source":"github"}')}const{file:l,settings:u}=await ut(e,{tickets:c});return{file:l,lines:dr({tickets:u.tickets})}}if(a==="github_login"){const{file:c,settings:l}=await ut(e,{github_login:n});return{file:c,lines:[`github-login ${l.github_login||"unset"}`]}}if(a==="build_target"){const{file:c,settings:l}=await ut(e,{build_target:n==="default"?null:n});return{file:c,lines:[`build-target ${l.build_target||"default"}`]}}if(Vn.includes(a)){const{file:c,settings:l}=await ut(e,{[a]:/^\d+$/.test(n)?Number(n):n});return{file:c,lines:[`${t} ${l[a]}`]}}["on","off"].includes(n)||d(`settings set ${t} takes on or off, not "${n}"`);const{file:o,settings:s}=await ut(e,{[a]:n==="on"});return{file:o,lines:[`${t} ${s[a]?"on":"off"}`]}}r(Ml,"setBoardSetting");const dr=r(e=>Object.entries(e).map(([t,n])=>`setting ${t.replace(/_/g,"-")} ${t==="tickets"?JSON.stringify(n??{source:"none"}):t==="github_login"||Vn.includes(t)?n:t==="build_target"?n||"default":n?"on":"off"}`),"settingsLines");async function Zn(e,t,n){const a=t?Kn(()=>n(Jt())):await bi(e,n);return t&&Kn(()=>Ts(a.rules)),{file:t?cr():a.file,lines:[`id ${a.rule.id}`]}}r(Zn,"changeRules");async function _i(e,t,n){const a=r(s=>s.map(c=>c.id),"ids");n||D(e);const[i,o]=n?[a(Jt()),e?a(await Qn(e)):[]]:[a(await Qn(e)),a(Jt())];i.includes(t)||d(`no ${n?"hub":"board"} rule "${t}"${o.includes(t)?`; it is a ${n?"board":"hub"} rule, so ${n?"drop":"pass"} --hub`:""}`)}r(_i,"checkRuleId");async function Wl(e){const t=r(n=>a=>`${n} ${a.id} ${a.by==="claude"?"claude":"owner"} ${a.text}`,"line");return{lines:[...Jt().map(t("hub")),...e?(await Qn(e)).map(t("board")):[]]}}r(Wl,"listRules");function Ul(e,{text:t,hub:n=!1,by:a}){return n||D(e),Zn(e,n,i=>Ls(i,{text:t,by:a}))}r(Ul,"addRuleTo");async function Hl(e,t,{text:n,hub:a=!1}){return await _i(e,t,a),Zn(e,a,i=>Bs(i,t,n))}r(Hl,"editRuleIn");async function Jl(e,t,{hub:n=!1}){return await _i(e,t,n),Zn(e,n,a=>Cs(a,t))}r(Jl,"removeRuleFrom");const zl=r((e,t,n)=>bi(e,a=>Is(a,t,n)),"saveBoardRules");async function Kl(e,{id:t,title:n,summary:a}){D(e);const i=await $(E(e),{topics:[],items:[]}),o=i.topics||[];return o.some(s=>s.id===S(t,"the topic id"))&&d(`topic "${t}" already exists`),o.push({id:t.trim(),title:S(n,"--title"),...a===void 0?{}:{summary:S(a,"--summary")}}),await y(E(e),{...i,topics:o,items:i.items||[]}),E(e)}r(Kl,"addTopic");const Si=["quiet","info","notice","danger","success"];function ur(e,t){if(e.diagram===void 0!=(e.practice===void 0)&&d(`${t} needs diagram and practice together`),e.diagram===void 0)return;const n=e.diagram;(!Array.isArray(n)||!n.length)&&d(`${t}.diagram must be a non-empty array of rows`),n.forEach((a,i)=>{(!Array.isArray(a)||!a.length||a.length>4)&&d(`${t}.diagram[${i}] must be a row of 1 to 4 boxes`),a.forEach((o,s)=>{const c=`${t}.diagram[${i}][${s}]`;rt(o,["text","tone","to"],c),S(o.text,`${c}.text`),te(o.tone,l=>Si.includes(l)?l:d(`${c}.tone takes ${Si.join(", ")}`)),te(o.to,l=>Array.isArray(l)&&i+1<n.length&&l.every(u=>Number.isInteger(u)&&u>=0&&u<n[i+1].length)?l:d(`${c}.to must list boxes of the next row by index`))})}),(!Array.isArray(e.practice)||!e.practice.length)&&d(`${t}.practice must be a non-empty array of lines`),e.practice.forEach((a,i)=>S(a,`${t}.practice[${i}]`))}r(ur,"checkDiagram");function fr(e,t){(!Array.isArray(e)||!e.length)&&d(`${t}.options must be a non-empty array`);const n=new Set;e.forEach((a,i)=>{const o=`${t}.options[${i}]`;rt(a,["id","label","recommended","detail","diagram","practice"],o),ur(a,o),n.has(S(a.id,`${o}.id`))&&d(`${t} has two options with id "${a.id}"`),n.add(a.id),S(a.label,`${o}.label`).startsWith(`${a.id}. `)&&d(`${o}.label starts with "${a.id}. ", but the page adds the option letter itself`),te(a.recommended,s=>Kt(s,`${o}.recommended`)),te(a.detail,s=>typeof s=="string"?s:d(`${o}.detail must be text`))}),e.filter(a=>a.recommended).length>1&&d(`${t} recommends more than one option`)}r(fr,"checkOptions");function wr(e,t,n){rt(e,["option","card","answer","reason"],n),t.some(a=>a.id===S(e.option,`${n}.option`))||d(`${n}.option names no option "${e.option}"`),S(e.card,`${n}.card`),S(e.answer,`${n}.answer`),te(e.reason,a=>S(a,`${n}.reason`))}r(wr,"checkSuggested");const ft=r(e=>g.join(e,"data","prototypes.json"),"prototypesFile"),Yn=r((e,t,n)=>g.join(e,"public","prototypes",t,`${n}.js`),"takeFile");async function Ft(e){const t=e?(await $(ft(e),{})).prototypes:null;return Array.isArray(t)?t:[]}r(Ft,"readPrototypes");const pr=r(e=>typeof e=="string"&&/^[a-z0-9-]+$/.test(e)?e:d(`a prototype topic uses only a-z, 0-9 and "-", not "${e}"`),"prototypeTopic");function mr(e){const t=/^([^:]*):(.*)$/s.exec(e);(!t||!/^[A-Z]$/.test(t[1]))&&d(`--take takes KEY:"Label" with KEY one capital letter, not "${e}"`);const n=S(t[2],`--take ${t[1]}'s label`);return/^[A-Z]\.\s/.test(n)&&d(`--take ${t[1]}'s label starts with "${n.slice(0,3)}"; the take pill shows the label alone, so drop the letter`),{key:t[1],label:n}}r(mr,"prototypeTake");function hr(e){const t=/^([^:]*):(.*)$/s.exec(e);return(!t||!/^[a-z0-9][a-z0-9-]*$/.test(t[1]))&&d(`--state takes KEY:"Label" with KEY in a-z, 0-9 and "-", not "${e}"`),{key:t[1],label:S(t[2],`--state ${t[1]}'s label`)}}r(hr,"prototypeState");const vi=r(e=>e.find((t,n)=>e.findIndex(a=>a.key===t.key)!==n),"twiceIn"),Ee=r(e=>/^prototype-(.+)\.html$/.exec(g.posix.basename(String(e||"").split(/[?#]/)[0]))?.[1],"pageTopic");async function Ql(e,{topic:t,title:n,card:a,takes:i=[],served:o,states:s=[]}){D(e),pr(t),S(n,"--title"),i.length||d('prototype add needs a --take KEY:"Label" for each take');const c=i.map(mr),l=vi(c);l&&d(`--take ${l.key} is given twice`),o!==void 0&&!/^\/\S*$/.test(o)&&d(`--served takes the page's path on the served site, starting with "/", not "${o}"`),s.length&&o===void 0&&d("--state goes with --served: only a served round re-renders its states in place");const u=s.map(hr),p=vi(u);p&&d(`--state ${p.key} is given twice`),a!==void 0&&await x(e,a);const w=await Ft(e);w.some(m=>m.topic===t)&&d(`prototype ${t} is already recorded; run prototype rm ${t} first, or pick another topic`),await j.stat(g.join(e,"public",`prototype-${t}.html`)).then(()=>!0,()=>!1)&&d(`public/prototype-${t}.html is a hand-written page that questions may link; pick another topic`);const f={topic:t,title:n.trim(),...a===void 0?{}:{card:a},...o===void 0?{}:{served:o},takes:c,...u.length?{states:u}:{},at:new Date().toISOString()};await y(ft(e),{prototypes:[...w,f]});const h=`page prototype-${t}.html`;return o!==void 0?{file:ft(e),lines:[h,`served ${o} from the base prototype worktree (board.mjs prototype base)`,...u.length?[`states ${u.map(m=>m.key).join(", ")}`]:[]]}:{file:ft(e),lines:[h,...c.map(m=>`take ${m.key} ${Yn(e,t,m.key)}`)]}}r(Ql,"addPrototype");async function Gl(e){D(e);const t=await Ft(e);return{lines:t.length?t.map(n=>[n.topic,n.title,n.takes.map(a=>`${a.key}: ${a.label}`).join(", "),n.card,n.served&&`served ${n.served}`,n.states&&`states ${n.states.map(a=>a.key).join(", ")}`].filter(Boolean).join(" \xB7 ")):["no prototypes recorded"]}}r(Gl,"listPrototypes");async function Vl(e){const t=(await Ft(e)).filter(a=>a.served).map(a=>a.topic);if(!t.length)return[];const n=await $(E(e),{items:[]});return t.filter(a=>(n.items||[]).some(i=>!i.resolved&&Ee(i.url)===a))}r(Vl,"servedRoundsOpen");async function Zl(e,t){D(e);const n=await Ft(e);n.some(o=>o.topic===t)||d(`no prototype "${t}" is recorded`);const i=((await $(E(e),{items:[]})).items||[]).filter(o=>!o.resolved&&Ee(o.url)===t).map(o=>o.id);return i.length&&d(`prototype ${t} backs open question ${i.join(", ")}; resolve ${i.length===1?"it":"them"} first`),await y(ft(e),{prototypes:n.filter(o=>o.topic!==t)}),ft(e)}r(Zl,"removePrototype");async function gr(e,t,n){const a=t.url?.split(/[?#]/)[0];if(!a||!g.posix.basename(a).startsWith("prototype-"))return;const i=g.posix.basename(a),o=t.url.split("#")[0].split("?").slice(1).join("?"),s=`${n}.url`;new URLSearchParams(o).get("q")!==t.id&&d(`${s} must be ${i}?q=${t.id} so the pill can pick`);const c=Ee(a),l=(await Ft(e)).find(f=>f.topic===c);l||d(`${s} names ${i}, which is not a recorded prototype; record it with board.mjs prototype add ${c} --title "\u2026" --take A:"\u2026", then write its take files`);const u=l.takes.map(f=>f.key),p=t.options.map(f=>f.id),w=p.map(f=>u.indexOf(f));if(w.some((f,h)=>f<0||h>0&&f<w[h-1])&&d(`${n}.options ids ${p.join(", ")} must be a non-empty subset of ${i}'s takes, in take order: ${u.join(", ")}`),!l.served)for(const f of u)await j.stat(Yn(e,c,f)).then(h=>h.isFile(),()=>!1)||d(`${s} take ${f} has no file yet; write ${Yn(e,c,f)}`)}r(gr,"checkPrototype");async function Yl(e,t,n=async()=>{}){D(e);const a=Array.isArray(t)?t:[t];a.length||d("the question file is empty");const i=await $(E(e),{topics:[],items:[]}),o=i.items||[],s=await $(me(e),{}),c=new Set([...o,...Object.values(s).flat()].map(w=>w.id)),l=new Set((i.topics||[]).map(w=>w.id));for(const[w,f]of a.entries()){const h=`question ${f?.id??w+1}`;if(rt(f,["id","topic","source","question","context","url","urgent","options","round_of","reask_of","ask","suggested","prototype_gate","ticket_gate"],h),c.has(Re(f.id,`${h}.id`))&&d(`question id "${f.id}" is already used`),c.add(f.id),S(f.topic,`${h}.topic`),f.topic.startsWith("item:ask:")){const m=await Qe(e,await Rt(e,f.topic.slice(9),`${h}.topic`));d(`${h}.topic: an ask's questions go on its card's topic${m?`, item:${m.key}`:""}`)}else f.topic.startsWith("item:")?await x(e,f.topic.slice(5)):f.topic.startsWith("wave:")?await ht(e,f.topic.slice(5)):f.topic.startsWith("file:")?await ic(e,f.topic.slice(5)):l.has(f.topic)||d(`topic "${f.topic}" is not declared; add it with: topic add ${f.topic} --title <t>`);S(f.question,`${h}.question`),f.question.length>qa&&d(`${h}.question runs to ${f.question.length} characters, past ${qa}; keep the question to one line and move the detail into context or the option details, which take markdown`);for(const m of["source","context"])te(f[m],b=>S(b,`${h}.${m}`));te(f.url,m=>Ln(m,`${h}.url`)),te(f.urgent,m=>Kt(m,`${h}.urgent`)),fr(f.options,h),te(f.prototype_gate,m=>Pi(m,f,`${h}.prototype_gate`)),f.ticket_gate!==void 0&&Pi(f.ticket_gate,f,`${h}.ticket_gate`)&&await kr(e,f,h),te(f.suggested,m=>wr(m,f.options,`${h}.suggested`)),await gr(e,f,h),f.ask!==void 0&&await bc(e,f,h),te(f.round_of,m=>{const b=o.find(_=>_.id===m);b||d(`${h}.round_of names no question "${m}" in owner.json`),b.topic!==f.topic&&d(`${h}.round_of names ${m} on topic ${b.topic}; a round stays on its question's topic, ${f.topic}`),Ee(f.url)||d(`${h}.round_of needs a url on a recorded prototype, prototype-<topic>.html?q=${f.id}`)}),te(f.reask_of,m=>{const b=o.find(_=>_.id===m);b||d(`${h}.reask_of names no question "${m}" in owner.json`),b.topic!==f.topic&&d(`${h}.reask_of names ${m} on topic ${b.topic}; a re-ask stays on its question's topic, ${f.topic}`),b.resolved||d(`${h}.reask_of names ${m}, which is not resolved; only a resolved question is asked again`)})}for(const w of new Set(a.map(f=>Ee(f.url)).filter(Boolean)))await n(e,w);const u=[],p=[];for(const w of a){const f=w.topic.startsWith("item:")&&!w.topic.startsWith("item:ask:")?w.topic.slice(5):null,h=f&&await ji(e,f);h&&((s[f]||=[]).push(w),p.push(w),u.push(`staged: ${w.id} waits for ${f}'s ${h} to finish; the card's staged questions then publish together`))}return p.length&&await y(me(e),s),p.length===a.length?{file:me(e),lines:u,staged:p.length}:Ai(e,i,o,a.filter(w=>!p.includes(w)),u)}r(Yl,"addQuestions");async function ji(e,t){const{source:n}=await x(e,t);for(const a of await G(n.dir,F(t).id)){const i=await $(a,null);if(i?.started&&!i.done&&We(i)&&i.kind!==$e)return i.kind}return null}r(ji,"runningPlanner");async function wt(e,t,{screen:n=!1}={}){if(!e||await ji(e,t))return;const a=await $(me(e),{});if(!a[t])return;const i=await $(st(e),{});if(n){i[t]||await y(st(e),{...i,[t]:new Date().toISOString()});return}const{[t]:o,...s}=a,c=await $(E(e),{topics:[],items:[]});await Ai(e,c,c.items||[],o,[]),await y(me(e),s),i[t]&&await y(st(e),Xt(i,t))}r(wt,"publishStaged");const Xt=r((e,t)=>Object.fromEntries(Object.entries(e).filter(([n])=>n!==t)),"withoutKey");async function Oi(e){const t=await $(st(e),{}),n=await $(me(e),{});return Object.fromEntries(Object.entries(t).filter(([a])=>n[a]).map(([a,i])=>[a,{since:i,ids:n[a].map(o=>o.id)}]))}r(Oi,"readScreening");async function Xl(e,t,n){D(e);const a=S(n,"--note"),i=await $(me(e),{}),o=await $(st(e),{}),s=Object.keys(i).find(w=>i[w].some(f=>f.id===t));if(!s)return $r(e,t,a);o[s]||d(`${t} waits for ${s}'s planner to finish`);const c=i[s].find(w=>w.id===t);c.prototype_gate&&d(`${t} is the card's prototype question, which is the owner's; question publish ${s} passes it on`),c.ticket_gate&&d(`${t} is the card's ticket question, which is the owner's; question publish ${s} passes it on`);const l=new Date().toISOString(),u=await $(E(e),{topics:[],items:[]});await y(E(e),{...u,topics:u.topics||[],items:[...u.items||[],{...c,asked_at:l,resolved:`Settled by the orchestrator: ${a}`,resolved_at:l}]});const p=i[s].filter(w=>w.id!==t);return await y(me(e),p.length?{...i,[s]:p}:Xt(i,s)),p.length||await y(st(e),Xt(o,s)),await Le(e,s,`Answers (orchestrator, ${Ve()})`,`- ${t} (${c.question}): settled by the orchestrator. "${a}"`),E(e)}r(Xl,"settleQuestion");async function $r(e,t,n){const a=await $(E(e),{topics:[],items:[]}),i=(a.items||[]).find(c=>c.id===t&&c.topic?.startsWith("item:"));i||d(`no staged question "${t}"; question settle takes a question a finished grill left for you to screen, or a card's question while it waits on your re-check`);const o=i.topic.slice(5);(await x(e,o)).item.recheck_at||d(`${o} waits on no re-check, so its question ${t} stays answered as it is`);const s=new Date().toISOString();return await y(E(e),{...a,items:a.items.map(c=>c===i?{...c,resolved:`Settled by the orchestrator: ${n}`,resolved_at:s}:c)}),await Le(e,o,`Answers (orchestrator, ${Ve()})`,`- ${t} (${i.question}): settled by the orchestrator. "${n}"`),E(e)}r($r,"settleStale");async function ed(e,t){D(e);const n=(await Oi(e))[t]?.ids||d(`${t} has no questions waiting on you to screen`);return await wt(e,t),n}r(ed,"publishScreened");async function td(e,t=new Date){const n=Object.entries(await Oi(e)).filter(([,a])=>t-Date.parse(a.since)>=Us).map(([a])=>a);for(const a of n)await wt(e,a),await M(e,a,"screen-timeout");return n}r(td,"publishUnscreened");async function Ai(e,t,n,a,i){const{answers:o,requests:s,reopens:c}=await pt(e),l=new Date().toISOString(),u=[],p=[];for(const f of a.filter(h=>h.round_of))f.round=On(n.find(h=>h.id===f.round_of))+1,Oe(t,[f.round_of],`Followed by prototype round ${f.round}`,l),p.push(f.round_of);for(const f of a.filter(h=>h.reask_of))Oe(t,[f.reask_of],`Asked again as ${f.id}`,l),p.push(f.reask_of);for(const f of a){const h=Ee(f.url),m=n.filter(_=>_.topic===f.topic).map(_=>Me(_,o,s,[],c)),b=f.topic.startsWith("item:")?Bi(m):[];for(const _ of m)b.includes(_.id)?(Oe(t,[_.id],`Followed by ${f.id}`,l),p.push(_.id)):!_.resolved&&h&&Ee(_.url)===h&&(Oe(t,[_.id],`Replaced by ${f.id}`,l),u.push(_.id))}const w={...t,topics:t.topics||[],items:[...n,...a.map(f=>({...f,asked_at:l}))]};await y(E(e),w);for(const f of new Set(a.map(h=>ea(h.topic)).filter(Boolean)))await jr(e,f);return{file:E(e),lines:[...i,...u.map(f=>`replaced: ${f}`),...p.map(f=>`resolved: ${f}`)]}}r(Ai,"publishQuestions");function Pi(e,t,n){return Kt(e,n),e?((!t.topic.startsWith("item:")||t.topic.startsWith("item:ask:"))&&d(`${n} goes on a card's topic, item:<file>/<id>`),t.options?.map(a=>a.id).join()!=="yes,no"&&d(`${n} takes exactly the options yes and no, in that order`),!0):!1}r(Pi,"checkGate");async function kr(e,t,n){const a=t.topic.slice(5),{item:i}=await x(e,a);i.ticket&&d(`${n}.ticket_gate: ${a} already has ticket ${i.ticket}`),await Ye(e)||d(`${n}.ticket_gate needs a board with a ticket source; with none every card stays a task`)}r(kr,"checkTicketGate");async function nd(e,t,n){const a=await $(E(e),{topics:[],items:[]}),i=new Map((a.items||[]).map(l=>[l.id,l]));for(const l of t){const u=i.get(String(l.id));if(!u?.ticket_gate||!["yes","no"].includes(l.choice))continue;Oe(a,[u.id],`Ticket: ${l.choice}.${l.choice==="yes"?" The board makes it from the card's title and notes.":""}`),await y(E(e),a);const p=u.topic.slice(5);l.choice==="yes"&&await Di(e,p,"making its ticket",async()=>{await ye(e,p,{newTicket:!0},n),await M(e,p,"ticket-made",{ticket:(await x(e,p)).item.ticket})})}for(const l of t){const u=i.get(String(l.id));if(!u?.prototype_gate||!["yes","no"].includes(l.choice))continue;Oe(a,[u.id],`Prototype round: ${l.choice}. The board moves the card on once every question is answered.`),await y(E(e),a);const p=u.topic.slice(5);l.choice==="yes"&&(await x(e,p).catch(()=>null))?.item.phase==="grilling"&&await ia(e,p)&&await M(e,p,"prototype-after")}const o=new Map;for(const l of t){const u=i.get(String(l.id)),p=ea(u?.topic);p&&o.set(p,[...o.get(p)||[],br(u,l)])}const s=[],c=[];for(const[l,u]of o)await x(e,l).catch(()=>null)&&await Di(e,l,"writing them into its notes",async()=>{await Le(e,l,`Answers (owner, ${Ve()})`,u.join(`
18
+ `));const p=await Xn(e,l);p==="prototyping"&&s.push(l),p==="staging"&&c.push(l)});return{prototype:s,staged:c}}r(nd,"settleAnswers");async function Xn(e,t,{roundAt:n=!1}={}){const{item:a}=await x(e,t),{asked:i}=await ue(e,t);if(i.some(o=>o.state==="open"))return null;if(a.phase==="grilling"&&i.some(o=>o.prototype_gate&&o.answers.at(-1)?.choice==="yes")){if(await ye(e,t,{phase:"prototyping"}),n){const{source:o,itemFile:s,item:c}=await x(e,t);c.round_at=new Date().toISOString(),await y(o.file,s)}return await M(e,t,"prototype-round"),"prototyping"}return await Sr(e,t)?(await aa(e,t),"staging"):null}r(Xn,"moveOnAnswered");async function Di(e,t,n,a){try{await a()}catch(i){if(!(i instanceof pe))throw i;await M(e,t,"answers-failed",{what:n,error:i.message})}}r(Di,"followUp");const ea=r(e=>typeof e=="string"&&e.startsWith("item:")&&!e.startsWith("item:ask:")?e.slice(5):null,"cardOfTopic"),en=r(e=>String(e).padStart(2,"0"),"pad2"),Ve=r((e=new Date)=>`${en(e.getMonth()+1)}/${en(e.getDate())} ${en(e.getHours())}:${en(e.getMinutes())}`,"noteStamp");function br(e,t){const n=t.choice===null||t.choice===void 0||t.choice===""?null:(e.options||[]).find(i=>i.id===t.choice)?.label??String(t.choice),a=String(t.note||"").trim();return`- ${e.id} (${e.question}): ${n?`${n}.`:"no option picked."}${a?` Note, verbatim: "${a}"`:""}`}r(br,"answerLine");const Fi=292,ta=r(e=>String(e).toLowerCase().replace(/[^a-z0-9-]+/g,"-"),"notesBase"),yr=r((e,t,n)=>it(g.join(e,`.${ta(t)}-notes`),n),"notesLock");function tn(e,t){let n=`${ta(t)}-notes.md`;for(let a=2;we(g.join(e,n));a++)n=`${ta(t)}-notes-${a}.md`;return n}r(tn,"freeNotesName");async function nn(e,t){const n=g.join(g.dirname(e),`.${g.basename(e)}.${process.pid}.tmp`);await j.rm(n,{force:!0}),await j.writeFile(n,t,{mode:Fi}),await j.rename(n,e)}r(nn,"writeNotesFile");const xr=r(e=>/\n/.test(S(e,"--heading"))?d("--heading is one line, the section's title"):S(e,"--heading"),"notesHeading"),_r=r((e,t,n)=>`${e.replace(/\n*$/,`
19
19
  `)}
20
20
  ## ${t}
21
21
  ${n.replace(/^\n+|\s+$/g,"")}
22
- `,"withSection");async function Re(e,t,n,a){const{source:i,item:{id:o}}=await x(e,t);return gr(i.dir,o,async()=>{const{source:s,itemFile:c,item:l}=await x(e,t);let u;l.plan?(u=await nn(s.dir,l.plan,{missingOk:!0}),de(u)||await W(e,t,"notes-missing")):(l.plan=Zt(s.dir,l.id),u=g.join(s.dir,l.plan),await y(s.file,c));const p=de(u)?await j.readFile(u,"utf8"):`# ${l.title}
23
- `;return await Yt(u,kr(p,$r(n),a)),l.plan})}r(Re,"appendNotes");async function Ai(e){const{tickets:t}=Se(await oe(e)),n=[],a=new Map;for(const i of await q(e)){const o=await $(i.file,null);a.set(i.slug,{source:i,itemFile:o||{}});for(const s of o?.items||[])s.plan&&n.push({key:`${i.slug}/${s.id}`,ref:tt(o,s,t),title:s.title,created:s.created||"",archived:!1,dir:i.dir,plan:s.plan})}for(const i of e?await Le(e):[]){if(!i.item?.plan)continue;const o=a.get(i.file);n.push({key:i.key,ref:tt(o?.itemFile||{},i.item,t),title:i.item.title,created:i.item.created||"",archived:!0,dir:o?.source.dir||g.join(e,"data"),plan:i.item.plan})}return n}r(Ai,"cardsWithNotes");const Pi=r(e=>j.realpath(g.resolve(e.dir,e.plan)).catch(()=>null),"realNotes");async function Di(e,t,n,a){if(t==="-")return Eo(0,"utf8");const i=await j.realpath(t).catch(()=>d(`${n} ${t}: no such file`));for(const o of await Ai(e))await Pi(o)===i&&d(`${n} ${t} is the notes file of ${o.ref} "${o.title}" (${o.key}), and a card's notes are its own; ${a(o)}`);return j.readFile(i,"utf8")}r(Di,"refuseCardNotes");const ta="write a draft of your own outside the board's data folder";async function Yl(e,t,{heading:n,file:a,from:i}){let o;if(i!==void 0){i===t&&d(`${t} can't fold its own notes into itself; pass --from <the other card>`);const[l,u]=await Promise.all([x(e,t),x(e,i)]),[p,w]=await Promise.all([l,u].map(({source:f,item:h})=>h.plan?j.realpath(g.resolve(f.dir,h.plan)).catch(()=>null):null));p&&p===w&&d(`${i} and ${t} name one notes file, ${l.item.plan}, from before the board named each card's own; the board splits it as it starts`),o=(await Ir(e,i)).markdown,n??=`Folded from ${await H(e,i)} "${u.item.title}"`}else o=await Di(e,a,"--file",l=>l.key===t?`${ta}, holding only the new section`:`${ta}, or fold that card's notes in with item notes ${t} --from ${l.key}`);o.trim()||d(`${i??a} is empty`);const s=await Re(e,t,n,o),c=await en(e,t,"Recorded in the card's notes");return{file:s,heading:n,lines:c}}r(Yl,"addNotes");const Xl=r(e=>ce(e,async()=>{const t=await Ai(e),n=new Map;for(const i of t){const o=await Pi(i);o&&n.set(o,[...n.get(o)||[],i])}const a=[];for(const[i,o]of n){const[s,...c]=o.sort((l,u)=>l.created<u.created?-1:l.created>u.created?1:l.key<u.key?-1:1);for(const l of c.filter(u=>!u.archived)){const{source:u,itemFile:p,item:w}=await Ue(e,F(l.key).file).then(h=>({...h,item:h.itemFile.items.find(m=>m.id===F(l.key).id)})),f=w.plan;w.plan=Zt(u.dir,w.id),await Yt(g.join(u.dir,w.plan),await j.readFile(i,"utf8")),await y(u.file,p),await W(e,l.key,"notes-shared",{card:s.ref}),a.push(`${l.key}: its own notes, ${w.plan}, copied from ${g.normalize(f)}, which ${s.key} keeps`)}await j.chmod(i,Oi)}return a}),"splitSharedNotes");async function br(e,t){const{source:n,item:a}=await x(e,t);return we(a)||be(a)||a.pushed||(await Promise.all((await G(n.dir,a.id)).map(o=>$(o,null)))).some(o=>o?.started&&!o.done&&o.kind!==me)||(await fe(e,t)).asked.some(o=>o.state==="open")||(await $(ye(e),{}))[t]?!1:!await ia(e,t)}r(br,"stageable");async function na(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);i.phase!=="staging"&&(Object.assign(i,{phase:"staging",staged_at:new Date().toISOString()}),delete i.unfiled,delete i.filed_held,await y(n.file,a),await Je(e,t,{command:"staged"}))}r(na,"stage");const Fi=r(({text:e,files:t})=>[String(e||"").trim(),...(Array.isArray(t)?t:[]).map(n=>`Attached: ${n?.path||n?.name||n}`)].filter(Boolean).join(`
24
- `),"messageWords");async function yr(e,t,n){const{item:a}=await x(e,t);a.held&&await Vi(e,"card-hold",{key:t,hold:!1});const{owner:i,asked:o}=await fe(e,t),s=o.filter(l=>l.state==="open").map(l=>l.id);s.length&&await Xt(e,i,s,"Skipped: you tapped Implement");const c=Fi(n);c&&await Re(e,t,`Implement (owner, ${Qe()}), verbatim`,c),await na(e,t)}r(yr,"stageImplement");async function ed(e,t,n){const a=Fi(n);a&&await Re(e,t,`Reply (owner, ${Qe()}), verbatim`,a),await na(e,t)}r(ed,"stageReply");async function td(e,t,n){const{source:a,itemFile:i,item:o}=await x(e,t);!["ready","staging"].includes(o.phase)||we(o)||(o.phase=n==="prototype"?"prototyping":"grilling",delete o.staged_at,await y(a.file,i))}r(td,"planAgain");async function xr(e,t){const n=await x(e,t).catch(()=>null);!n||!["ready","staging"].includes(n.item.phase)||we(n.item)||(n.item.phase="grilling",delete n.item.staged_at,await y(n.source.file,n.itemFile),await W(e,t,"new-question"))}r(xr,"grillAgain");function Ni(e){return e.filter(t=>t.state==="answered").map(t=>t.id)}r(Ni,"answeredOnClaude");function ve(e,t,n,a=new Date().toISOString()){for(const i of e.items||[])t.includes(i.id)&&Object.assign(i,{resolved:n,resolved_at:a})}r(ve,"setResolved");async function Xt(e,t,n,a){return ve(t,n,a),await y(E(e),t),E(e)}r(Xt,"markResolved");async function nd(e,t,n){D(e);const a=await $(E(e),{topics:[],items:[]}),i=(a.items||[]).find(f=>f.id===t);i||d(`no question "${t}" in owner.json`);const{answers:o,requests:s,reopens:c}=await ft(e),l=Te(i,o,s,[],c);if(l.state==="resolved")return E(e);l.state==="requested"&&d(`${t} waits on the prototype the owner asked for; question add with round_of ${t} resolves it`);const u=S(n,"--note");Ne(i.url)&&_r(a,i,o);const p=await Xt(e,a,[t],u);await Bi(e,i.topic);const w=Xn(i.topic);return w&&(await x(e,w).catch(()=>null))?.item.phase==="grilling"&&await vi(e,w,{roundAt:!0}),p}r(nd,"resolveQuestion");async function ad(e,t,n,a=[]){D(e);const[i,...o]=[...new Set(t)];i||d("question move takes the ids of the questions to merge"),await x(e,n);const s=[n,...a];new Set(s).size!==s.length&&d("question move names a card twice");for(const f of a)await x(e,f);const c=await $(E(e),{topics:[],items:[]}),{answers:l,requests:u,reopens:p}=await ft(e);for(const f of[i,...o]){const h=(c.items||[]).find(k=>k.id===f)||d(`no question "${f}" in owner.json`),{state:m}=Te(h,l,u,[],p);m!=="open"&&d(`${f} is ${m}; question move takes open questions`)}for(const[f,h]of a.entries()){const{item:m}=await x(e,h),k=[...m.deps||[],s[f]].join(",");await $e(e,h,{deps:k,relate:n})}const w=await H(e,n);return Object.assign(c.items.find(f=>f.id===i),{topic:`item:${n}`}),ve(c,o,`Duplicate of ${w}'s question ${i}`),await y(E(e),c),E(e)}r(ad,"moveQuestions");async function id(e,t){const n=t.filter(({choice:i,note:o})=>!i&&String(o||"").trim()).map(i=>i.id),a=n.length?await $(E(e),{topics:[],items:[]}):null;a?.items?.some(i=>n.includes(i.id))&&(ve(a,n,"Answered with a note"),await y(E(e),a))}r(id,"resolveNoteAnswers");function _r(e,t,n){const a=jn(t),i=e.items.filter(o=>o.topic===t.topic&&jn(o)===a).map(o=>Te(o,n));Ko(i,a)||d(`${t.id} is in prototype round ${a}, which no pick has settled yet; a pick settles it`)}r(_r,"roundSettled");async function ft(e){const t=await z(g.join(e,"data","events.jsonl"));return{answers:await Promise.all(t.filter(a=>a.kind==="owner-answer").map(async a=>({...a,reply:a.reply||(await Ln(g.join(e,"data","replies"),a.id)).length>0}))),requests:t.filter(a=>a.kind==="prototype-request"),reopens:t.filter(a=>a.kind===Ho)}}r(ft,"ownerEvents");async function od(e,t){const{asked:n}=await Sr(e),a=new Map(n.map(i=>[i.id,i.state]));return t.filter(i=>!a.has(String(i.id))||["open","requested"].includes(a.get(String(i.id))))}r(od,"unrecorded");async function Sr(e){const t=await $(E(e),{topics:[],items:[]}),{answers:n,requests:a,reopens:i}=await ft(e);return{owner:t,asked:(t.items||[]).map(o=>Te(o,n,a,[],i))}}r(Sr,"topicQuestionsAll");async function Ri(e,t){const n=await $(E(e),{topics:[],items:[]}),{answers:a,requests:i,reopens:o}=await ft(e),s=(n.items||[]).filter(c=>c.topic===t).map(c=>Te(c,a,i,[],o));return{owner:n,asked:s}}r(Ri,"topicQuestions");const fe=r((e,t)=>Ri(e,`item:${t}`),"cardQuestions"),en=r((e,t,n)=>Ei(e,`item:${t}`,n),"resolveAnswered");async function Ei(e,t,n){if(!e)return[];const{owner:a,asked:i}=await Ri(e,t),o=Ni(i);return o.length&&await Xt(e,a,o,n),await Bi(e,t),o.map(s=>`resolved: ${s}`)}r(Ei,"resolveAnsweredOn");const aa=r(async(e,t)=>!!e&&(await fe(e,t)).asked.some(n=>n.state==="open"),"hasOpenQuestion");function Li(e,t,n){e.next=S(t,"--next"),e.next_at=new Date().toISOString(),n?e.next_on_questions=!0:delete e.next_on_questions}r(Li,"setNext");async function Bi(e,t){const n=t.startsWith("item:")&&!t.startsWith("item:ask:")?t.slice(5):null;if(!n)return;let a;try{a=await x(e,n)}catch(c){if(c instanceof ue)return;throw c}const{source:i,itemFile:o,item:s}=a;s.next_on_questions&&((await fe(e,n)).asked.some(c=>c.state!=="resolved")||(delete s.next,delete s.next_at,delete s.next_on_questions,await y(i.file,o)))}r(Bi,"clearWaitedNext");async function vr(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);i.next===void 0&&i.next_on_questions===void 0||(delete i.next,delete i.next_at,delete i.next_on_questions,await y(n.file,a))}r(vr,"clearNext");async function Ci(e,t,n,a){if(!e)return async()=>[];const{owner:i,asked:o}=await fe(e,t),s=o.filter(l=>l.state==="open").map(l=>l.id);s.length&&!a&&d(`${n}: ${t} has ${s.length===1?"an open question":"open questions"} ${s.join(", ")} with no answer yet; pass --force to close the card and resolve ${s.length===1?"it":"them"} too`);const c=o.filter(l=>l.state!=="resolved").map(l=>l.id);return async()=>(c.length&&await Xt(e,i,c,"Closed with the card"),c.map(l=>`resolved: ${l}`))}r(Ci,"closeQuestions");async function sd(e,t,n,a,i){await x(e,t),zt(n),ee(a,o=>S(o,"--owner-step")),ee(i,o=>Zi(o)),await Ci(D(e),t,"land",!1)}r(sd,"checkClose");async function ia(e,t){const n=await $(E(e),{items:[]}),{answers:a,requests:i,reopens:o}=await ft(e);return Uo((n.items||[]).map(s=>Te(s,a,i,[],o)),t)}r(ia,"unpickedPrototype");const jr=r(e=>e.length>120?`${e.slice(0,119)}\u2026`:e,"clip"),Or={ask:"text","ask-edited":"text","item-message":"text","owner-answer":"note"};async function Ii(e,t,n){const a=S(t,n).replace(/\s+/g," ");return(await z(g.join(e,"data","events.jsonl"))).map(o=>String(o.data?.[Or[o.kind]]??"").replace(/\s+/g," ").toLowerCase()).some(o=>o.includes(a.toLowerCase()))||d(`${n} must quote the owner's words in an ask, card message or answer note on this board`),a}r(Ii,"ownerQuote");async function rd(e,t,n){return D(e),n===void 0&&d(`only the owner's Serve / Build and the hub's Auto-build start a card run; pass --asked "<the owner's words>" quoting where the owner asked for ${t}'s run`),`Running at the owner's ask: "${await Ii(e,n,"--asked")}"`}r(rd,"askedGate");async function Dt(e,t,n="--from-ask"){return Fe(t,n),(await z(g.join(D(e),"data","events.jsonl"))).some(i=>i.id===t&&i.kind==="ask")?t:d(`no ask "${t}" in this site's data/events.jsonl`)}r(Dt,"checkAsk");function Ft(e,t,n=new Date().toISOString(),a=Ti(e,t)){if(!Ce[t])throw new Error(`no lane "${t}"`);const i=e.lane;return qi(e,t,a),e.lane=t,e.lane_at=n,t==="done"?(e.closed_at=n,delete e.reopened_at):i==="done"&&(e.reopened_at=n),i}r(Ft,"placeIn");function Ti(e,t,n=!1){return t==="todo"?we(e)?"ready":e.phase??null:t==="progress"?"reading":t==="done"?n||De(e).some(a=>a.merged_at)?"merged":"closed":null}r(Ti,"movedPhase");function qi(e,t,n){if(n!==null&&!_n[t].includes(n))throw new Error(`no ${Ce[t]} phase "${n}"`);n===null?delete e.phase:e.phase=n}r(qi,"setPhase");async function cd(e,t,n,a,i,o){const{source:s}=await x(e,t),{id:c}=F(t),l=new Date().toISOString();if(!await nt(s.file,async()=>{const w=JSON.parse(await j.readFile(s.file,"utf8")),f=w.items.find(m=>m.id===c)||d(`item file "${s.slug}" has no item "${c}"`);if(o&&((f.lane??null)!==o.lane||(f.phase??null)!==o.phase)||f.lane===n&&(i===void 0||(f.phase??null)===i))return!1;let h;return f.lane===n?qi(f,n,i):h=Ft(f,n,l,i),await We(s.file,JSON.stringify(w,null,2)+`
25
- `),h!==void 0}))return null;const p={ts:l,who:"claude",kind:Pa,text:`Moved to ${Ce[n]}: ${S(a,"why")}`};return await le(Z(e,t),p),p.text}r(cd,"moveCard");const K=r(e=>e?.lane==="done"||!!e?.reopened_at,"closed"),we=r(e=>!!e?.lane&&e.lane!=="todo","leftTodo"),ld=r(async(e,t)=>K((await x(e,t)).item),"isFinished"),Mi=["ready","grilling","prototyping"];function Wi(e,t){t!==void 0&&d(`--state is now --phase: pass --phase ${Mi.includes(t)?t:"ready, grilling or prototyping"}`),e==="staging"&&d("--phase staging is set by the board, on an owner action; pass ready, grilling or prototyping"),e!==void 0&&!Mi.includes(e)&&d("--phase takes ready, grilling or prototyping")}r(Wi,"checkPhase");const Ar="a card goes ready only from staging, which the board sets on the owner's Send, Implement or Reply";async function Ui(e,t){return(await z(g.join(D(e),"data","events.jsonl"))).some(a=>a.kind==="ask"&&a.id===t&&a.data?.quick===!0||a.kind==="item-message"&&a.data?.key===`ask:${t}`&&a.data?.intent==="build")}r(Ui,"quickAsk");const Pr=/^mark (?:it |this |the card )?ready\b/i;function Dr(e,t,n){const a=[].concat(t).map(s=>Fn(s)||d(`--pr takes a PR number or owner/repo#<n>, not "${s}"`)),i=r((s,c)=>s.number===c.number&&(s.repo||n||null)===(c.repo||n||null),"same"),o=De(e);for(const s of a)o.some(c=>i(c,s))||o.push(s);delete e.pr,e.prs=o}r(Dr,"addPRs");const J=r((e,t)=>String(t).includes("/")?String(t):`${e}/${t}`,"depKey"),Ge=/^wave:(.+)$/;async function wt(e,t,n){const a=await Promise.all((n||[]).map(async i=>{const o=String(i).match(Ge)?.[1];if(!o)return[J(t,i)];const{source:s,wave:c}=await pt(e,o);return Ie(c).map(l=>`${s.slug}/${l}`)}));return[...new Set(a.flat())]}r(wt,"depCards");async function Ee(e,t,n){const a=await wt(e,t,n.deps),i=await Promise.all(a.map(o=>x(e,o).then(({item:s})=>s,()=>null)));return a.filter((o,s)=>i[s]&&!K(i[s]))}r(Ee,"unfinishedDeps");async function Fr(e,t,n){const a=await Promise.all((await Ee(e,F(t).file,n)).map(i=>H(e,i)));return n.blocked_until?[...a,n.blocked_until]:a}r(Fr,"blockerNames");async function tn(e,t,n){n??=(await x(e,t)).item;const a=await Promise.all((await Ee(e,F(t).file,n)).map(i=>H(e,i)));return[a.length&&`by ${a.join(", ")}`,n.blocked_until&&`until ${n.blocked_until}`].filter(Boolean).join(" and ")}r(tn,"blockWords");const Nr=r((e,t)=>!be(e)&&!e.pushed&&!e.merge_ready&&!K(e)&&(!!Sn(t)||t.some(n=>n?.started&&!n.done&&et(n))),"inProgress");async function Rr(e,t){const n=[];for(const a of await q(e)){const i=await $(a.file,null);for(const o of Array.isArray(i?.items)?i.items:[])K(o)||o.blocked_until||!(await wt(e,a.slug,o.deps)).includes(t)||(await Ee(e,a.slug,o)).length||n.push(`${a.slug}/${o.id}`)}return n}r(Rr,"unblockedBy");async function Er(e,t,n){const a=F(t).file,i=(await sa(e,a,n)).map(c=>Ge.test(c)?c:J(a,c));i.length||d("--split-into takes wave:<slug> or the cards the split filed, <id>,<file>/<id>"),(await wt(e,a,i)).includes(t)&&d(`${t} can't be split into itself`);const o=await Promise.all(i.map(async c=>{const l=c.match(Ge)?.[1];if(!l)return H(e,c);const{source:u,wave:p}=await pt(e,l),w=await Promise.all(Ie(p).map(f=>H(e,`${u.slug}/${f}`)));return`the wave ${p.title} (${w.join(", ")})`})),s=[];for(const c of await q(e))for(const l of(await $(c.file,null))?.items||[]){const u=`${c.slug}/${l.id}`;if(!(K(l)||!(l.deps||[]).some(p=>J(c.slug,p)===t))){for(const p of i)(await wt(e,a,[p])).includes(u)&&d(Ge.test(p)?`${u} waits on ${t} and is in the wave "${p.match(Ge)[1]}", so it would wait on itself; split into the new cards by key`:`${u} waits on ${t} and is one of the cards it splits into, so it would wait on itself`);s.push(u)}}return{entries:i,words:o.join(", "),waiters:s}}r(Er,"splitPlan");async function Lr(e,t,{entries:n,words:a,waiters:i}){for(const s of await q(e)){const c=i.filter(u=>F(u).file===s.slug);if(!c.length)continue;const l=await $(s.file,null);for(const u of l.items.filter(p=>c.includes(`${s.slug}/${p.id}`))){const p=n.map(w=>!Ge.test(w)&&F(w).file===s.slug?F(w).id:w);u.deps=[...new Set([...u.deps.filter(w=>J(s.slug,w)!==t),...p])]}await y(s.file,l)}const o=await H(e,t);for(const s of i)await W(e,s,"split-moved",{card:o,to:a})}r(Lr,"moveWaiters");async function oa(e,t,n){const{source:a,itemFile:i,item:o}=await x(e,t);if(we(o))o.unblocked_at=n;else if(o.phase==="ready")o.recheck_at=n;else return;await y(a.file,i)}r(oa,"markUnblocked");async function Hi(e,t,n,a){const i=new Set(a.match(/#?[\w-]+(?:\.[\w-]+)*(?:\/[\w-]+(?:\.[\w-]+)*)*/g)||[]),o=new Set(await wt(e,F(t).file,n.deps));for(const s of await q(e)){const c=await $(s.file,null);for(const l of c?.items||[]){const u=`${s.slug}/${l.id}`;if(u===t||o.has(u)||K(l))continue;[l.id,tt(c,l),l.ticket].filter(Boolean).flatMap(w=>[w,`${s.slug}/${w}`]).some(w=>i.has(w))&&d(`--next names unfinished card ${u}; add it with --deps`)}}}r(Hi,"checkNextDeps");const Ji=r(e=>`item notes ${e} --heading "<heading>" --file <draft.md>`,"NOTES_COMMAND");function Br(e,t){t.size||d(`${e} has no size; a builder starts from it, so pass --size small or large with --phase ready`),t.plan||d(`${e} has no notes file; a builder starts from it, so add its notes with ${Ji(e)} before --phase ready`)}r(Br,"checkSizeAndNotes");async function Cr(e,t,n,a){Br(t,a);const i=await j.readFile(await nn(n,a.plan),"utf8"),o=(await fe(e,t)).asked.filter(s=>s.answers.length&&!i.includes(s.id)&&!((s.prototype_gate||s.ticket_gate)&&!s.answers.at(-1).note)).map(s=>s.id);o.length&&d(`${t}'s notes leave out the answered question${o.length===1?"":"s"} ${o.join(", ")}; record each answer in its notes, naming its id, with ${Ji(t)} before it is ready`)}r(Cr,"checkNotesComplete");async function nn(e,t,{missingOk:n=!1}={}){const a=r(s=>d(`a notes file is a .md file inside ${e}, named relative to it: "${t}" ${s}`),"bad");(typeof t!="string"||!t.trim())&&a("is empty"),(g.isAbsolute(t)||t.split(/[\\/]/).includes(".."))&&a("leaves the folder"),g.extname(t).toLowerCase()!==".md"&&a("is not a .md file");const i=await j.realpath(e),o=await j.realpath(g.resolve(e,t)).catch(()=>n?g.resolve(i,t):a("does not exist"));return o.startsWith(i+g.sep)||a("leaves the folder"),o}r(nn,"planPath");async function Ir(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);if(i.plan)return{itemFile:a,item:i,markdown:await j.readFile(await nn(n.dir,i.plan),"utf8")};if(typeof i.notes=="string"&&i.notes.trim())return{itemFile:a,item:i,markdown:i.notes};d(`${t} has no notes`)}r(Ir,"readNotes");function zi({install:e,installSha:t,installLabel:n,clearInstall:a}){if(e===void 0)return(t!==void 0||n!==void 0)&&d("--install-sha and --install-label go with --install <url>"),a?null:void 0;a&&d("pass --install or --clear-install, not both");let i;try{i=new URL(String(e).trim())}catch{i=null}(!i||!["https:","itms-services:"].includes(i.protocol))&&d(`--install takes an https:// or itms-services:// URL, not "${e}"`),i.protocol==="itms-services:"&&!(i.searchParams.get("action")==="download-manifest"&&/^https:\/\//.test(i.searchParams.get("url")||""))&&d("--install: an itms-services:// URL needs ?action=download-manifest&url=https://\u2026/manifest.plist");const o=t===void 0?void 0:/^[0-9a-f]{7,40}$/i.test(t)?t.toLowerCase():d(`--install-sha takes a commit sha of 7 to 40 hex digits, not "${t}"`);return{url:String(e).trim(),...n===void 0?{}:{label:S(n,"--install-label")},built_at:new Date().toISOString(),...o?{sha:o}:{}}}r(zi,"installOf");async function dd(e,{install:t,installSha:n}){const a=zi({install:t,installSha:n});a||d("main set needs --install <url>");const i=g.join(D(e),"data","main.json");return await y(i,{install:a}),{file:i,lines:["main install set"]}}r(dd,"setMainInstall");const Tr=r((e,t)=>e.length?`Merged PR${e.length>1?"s":""} ${at(e,t)}.`:"Merged.","mergedLine");function qr(e,t,n,a,i,o){return o?{status:o}:n&&a===void 0?{status:["merged",{words:Tr(n,i)}]}:(a===void 0&&e&&d(`--done needs --text "<what was done, or why it closes without landing>", which becomes ${t}'s last thread line`),a===void 0?null:{text:a})}r(qr,"closingLine");function Mr(e,t,n){const a=new Date().toISOString(),i=r((o,s)=>o.number===s.number&&(o.repo||n||null)===(s.repo||n||null),"same");e.prs=De(e).map(o=>{const s=t.find(c=>i(o,c));return s&&!o.merged_at?{...o,merged_at:s.at||a}:o}),delete e.pr}r(Mr,"markMerged");async function Ki(e,t,{ticket:n,newTicket:a,status:i,clearStatus:o,done:s,splitInto:c,merged:l,landed:u,start:p,handBack:w,next:f,clearNext:h,pr:m,pushed:k,mergeReady:_,ghSince:N,fromAsk:v,phase:O,state:A,held:I,unheld:B,pickUpAt:P,deps:L,blockedUntil:T,blockReason:M,parent:C,related:Y,relate:te,unrelate:V,install:Q,installSha:Ze,installLabel:un,clearInstall:ke,title:je,size:Bt,model:mt,force:ht,text:ie,closing:ne,who:fn,by:pe,command:$a,ownerStep:Ye,ownerDone:jo,check:gt},ka){(i!==void 0||o)&&d(`--status${o?" and --clear-status are":" is"} gone: a card's lane is stored and moves itself. Close it with --done --text "<what was done, or why it closes without landing>"`),l&&(s=!0),Y!==void 0&&(te!==void 0||V!==void 0)&&d("pass --related, or --relate and --unrelate, not both"),Wi(O,A),ht&&!s&&d("--force goes with --done"),c!==void 0&&!s&&d("--split-into goes with --done"),c!==void 0&&ie!==void 0&&d("--split-into writes the closing line; drop --text");const $t=c===void 0?null:await Er(e,t,c);$t&&(ie=`Split into ${$t.words}`),v!==void 0&&s&&d("--from-ask opens a card; close it in another call"),ie!==void 0&&zt(ie),Ye!==void 0&&S(Ye,"--owner-step");const kt=gt===void 0||gt===""?gt:Zi(gt);ie!==void 0&&v===void 0&&!s&&O!=="ready"&&d("--text goes with --from-ask, --done, or --phase ready on a staging card"),v!==void 0&&ie===void 0&&d(`--from-ask needs --text "<first line>", the item's first thread line after the ask`),f!==void 0&&h&&d("pass --next or --clear-next, not both"),typeof f=="string"&&!f.trim()&&([f,h]=[void 0,!0]);const bt=Gi({held:I,unheld:B,pickUpAt:P}),Ct=zi({install:Q,installSha:Ze,installLabel:un,clearInstall:ke});[s,p,w,f,h,m,k,_,N,v,O,bt,L,T,M,C,Y,te,V,Ct,je,Bt,mt,n,a,Ye,gt].every(R=>R===void 0||R===!1)&&d("nothing to set: pass --done, --next, --clear-next, --pr, --pushed, --gh-since, --from-ask, --phase, --held, --unheld, --pick-up-at, --deps, --blocked-until, --block-reason, --parent, --related, --relate, --unrelate, --install, --clear-install, --title, --size, --model, --ticket, --new-ticket or --check");const X=v===void 0?void 0:await Dt(e,v);X&&await Wn(e,X),X&&await oi(e,X,t);const{source:Be,itemFile:Xe,item:b}=await x(e,t),ba=X??(b.phase?void 0:b.from_ask),Oo=ba!==void 0&&await Ui(e,ba);O!==void 0&&b.answered_at&&!b.closed_at&&(delete b.answered_at,we(b)&&Ft(b,"todo",void 0,null)),O!==void 0&&we(b)&&d(`--phase sets a Todo card's phase; ${t} is ${Ce[b.lane]}, where the board sets it`),O==="ready"&&!Oo&&!["staging","ready"].includes(b.phase)&&d(`${t} reads ${b.phase||"new"}, not staging; ${Ar}`),X&&b.from_ask&&b.from_ask!==X&&d(`${t} came from ask "${b.from_ask}", so ask "${X}" can't be its from-ask; to fold it in: item relate-ask ${X} ${t} --summary "<line>"`),X&&bt&&O!==void 0&&!b.phase&&d("a card filed held is filed New and takes no --phase; drop --phase, or drop --held to file it in a phase"),X&&O===void 0&&!b.phase&&!bt&&d("--from-ask files a card, which reads New until it has a phase: pass --phase grilling, prototyping, or ready for a Quick build ask, or --held to file it for later");const wn=K(b);$t&&wn&&d(`${t} is already Done, so the cards waiting on it were unblocked then; move them with item set <key> --deps`),Ye!==void 0&&wn&&d(`${t} is already Done, so its ticket closed; an owner step goes on the command that finishes the card: land --owner-step, or builder done before the merge`),Ye!==void 0&&(b.owner_step=S(Ye,"--owner-step")),jo&&delete b.owner_step,kt!==void 0&&!b.owner_step&&d(`--check goes with an owner step, which ${t} doesn't have: pass --owner-step "<the step>" with it on builder done or land`),kt&&(b.owner_check={check:kt,since:b.owner_check?.check===kt?b.owner_check.since:new Date().toISOString()}),(kt===""||!b.owner_step)&&delete b.owner_check;const pn=await Promise.all((await G(Be.dir,b.id)).map(R=>$(R,null))),Ao=vn(b,pn);if(je!==void 0&&(b.title=S(je,"--title")),Qi(b,{size:Bt,model:mt}),L!==void 0){const R=await sa(e,F(t).file,L,t);R.length?b.deps=R:delete b.deps}const ya=T===""?b.blocked_until:void 0;if(T!==void 0&&(T===""?delete b.blocked_until:b.blocked_until=S(T,"--blocked-until")),M&&(!b.deps?.length&&!b.blocked_until&&d("--block-reason goes with a block: --deps or --blocked-until"),b.block_reason=S(M,"--block-reason")),(M===""||!b.deps?.length&&!b.blocked_until)&&delete b.block_reason,C!==void 0){const R=await Yi(e,F(t).file,t,C);R?b.parent=R:delete b.parent}const xa=C!==void 0||Y!==void 0||te!==void 0||V!==void 0?await ra(e,t):null;let mn=Y===void 0?null:await Hr(e,t,b,Y);te!==void 0&&(b.related=[...new Set([...(b.related||[]).map(R=>J(F(t).file,R)),...await on(e,t,te,"--relate")])]),V!==void 0&&(mn=await Jr(e,t,b,await on(e,t,V,"--unrelate"))),bt!==void 0&&an(b,bt),p&&b.held&&d(b.pick_up_at?`${t} is held until ${b.pick_up_at}, when the board resumes it; start it early only with item set ${t} --unheld`:`${t} is held; start it only once the owner resumes it, with item set ${t} --unheld`);const yt=m!==void 0||k!==void 0||_!==void 0;if((p||s||w||yt)&&(Ur(b),delete b.recheck_at),p){const R=await Ee(e,F(t).file,b);R.length&&d(`${t} is blocked by ${R.join(", ")}; start it once ${R.length===1?"that is":"those are"} done`),b.blocked_until&&d(`${t} is blocked until ${b.blocked_until}; start it once that holds and the block is cleared with --blocked-until ""`)}const It=p&&e?await ia(e,t):null;It&&d(`${t} has no take picked: the owner's answer to ${It.id} picks none${It.note?` ("${jr(It.note)}")`:""}, so it prototypes again: draw the next round from that note, or grill when the note asks a question, and build once a take is picked`);const Po=s?await Ci(e,t,"--done",ht):async()=>[],xt=s?qr(e,t,l,ie,se(e,Xe),ne):null;if(l&&Mr(b,l,se(e,Xe)),f!==void 0&&(Li(b,f,await aa(e,t)),await Hi(e,t,b,b.next)),h&&(delete b.next,delete b.next_at,delete b.next_on_questions),p&&f===void 0&&b.next_on_questions&&(delete b.next,delete b.next_at,delete b.next_on_questions),yt&&!b.review_since&&(b.review_since=new Date().toISOString()),m!==void 0&&Dr(b,m,se(e,Xe)),k!==void 0){ge().find(R=>Pe(R.dir,D(e)))?.remote||d("--pushed is for a board whose repo is a plain git remote; on GitHub, open a PR and pass --pr"),be(b)&&d(`${t} has a PR; its PR says when it merges`);try{ws(k)}catch{d("--pushed takes the name of the branch you pushed")}b.pushed={branch:k,at:new Date().toISOString()},b.branch=k}if(_!==void 0&&(b.merge_ready={branch:_,at:new Date().toISOString()},b.branch=_),s&&delete b.merge_ready,N!==void 0&&((typeof N!="string"||!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(N)||Number.isNaN(Date.parse(N)))&&d("--gh-since takes an ISO-8601 UTC time such as 2026-10-23T11:00:00Z"),b.gh_since=N),X&&(b.from_ask=X),O==="ready"&&e){const R=(await fe(e,t)).asked.filter(Ae=>Ae.state==="open").map(Ae=>Ae.id);R.length&&d(`${t} has ${R.length===1?"an open question":"open questions"} ${R.join(", ")}; it is ready once the owner has answered ${R.length===1?"it":"them"}`),await Cr(e,t,Be.dir,b)}const hn=O==="ready"&&b.phase==="staging"&&v===void 0;hn&&ie===void 0&&d(`${t} goes ready from staging on your judgment: pass --text "<what you checked it against on the board>", which goes into its notes`);const gn=O==="ready"&&b.phase==="ready"&&!!b.recheck_at&&v===void 0;gn&&ie===void 0&&d(`${t}'s block ended, so it builds once you re-check its notes against what landed: pass --text "<what you re-checked>", which goes into its notes`),O==="ready"&&!hn&&!gn&&ie!==void 0&&v===void 0&&!s&&d(`${t} reads ${b.phase||"no phase"}, not staging, so there is no judgment for --text to record`);const Do=O==="ready"&&b.phase!=="ready";O!==void 0&&(O!=="ready"&&["staging","ready"].includes(b.phase)&&(b.round_at=new Date().toISOString()),b.phase=O,delete b.recheck_at,delete b.staged_at,delete b.unfiled,delete b.filed_held),b.phase==="ready"&&Pr.test(b.next??"")&&d(`${t} would read ready with Next "${b.next}", which asks for the phase it is already in; pass --next with what happens next, or --clear-next`),Ct&&(b.install=Ct),Ct===null&&delete b.install,!Ao&&!p&&vn(b,pn)&&d(`that leaves ${t} In Progress with nothing building and no Next; pass --next "<what happens next>"`);const $n=(L!==void 0||T)&&!s&&Nr(b,pn)?await tn(e,t,b):"",_a=se(e,Xe);let kn=null;const bn=r((R,Ae)=>{b.lane!==R&&Ft(b,R,void 0,Ti(b,R,u))!==void 0&&Ae&&(kn=`Moved to ${Ce[R]}: ${Ae}`)},"moveTo");if(s?bn("done",null):yt&&b.lane!=="done"?bn("review",m!==void 0?`PR ${at([].concat(m).map(Fn),_a)} opened`:k!==void 0?`${k} pushed for its merge`:"verified, it waits for your Merge"):p&&!K(b)&&bn("progress","a build started"),a&&b.ticket&&d(`${t} already has ticket ${b.ticket}`),(n!==void 0||a)&&await to(e,Xe,b,n,ka,Be.dir),await y(Be.file,Xe),kn&&await le(Z(e,t),{ts:b.lane_at,who:"claude",kind:Pa,text:kn}),hn&&await Re(e,t,`Ready (orchestrator, ${Qe()})`,ie),gn&&await Re(e,t,`Re-checked (orchestrator, ${Qe()})`,ie),Do&&await Je(e,t,{command:"readied"}),$n){const R=e&&!await _o(e,t)&&await xo(e,t,{cause:"blocked",words:`blocked ${$n}`});await W(e,t,R?"blocked-pausing":"blocked-stays",{blocked:$n})}mn&&await mn(),xa&&await eo(e,t,xa,await ra(e,t));const Oe=await Po();if(O==="ready"&&e&&Oe.push(...await en(e,t,"Recorded in the card's notes")),p&&Oe.push(...await en(e,t,"Acted on: build started")),X&&Oe.push(...(await rt(e,t,{text:ie})).lines),xt&&e){const R=$a?await Je(e,t,{command:$a,text:xt.text,who:fn,by:pe}):xt.status?await W(e,t,...xt.status):await rt(e,t,{text:xt.text});Oe.push(...R.lines)}const Sa=b.owner_step&&s?null:rs({start:p,done:s,handover:yt}),va=Sa&&await cs(await sn(e),b,Sa,{...ka,repo:_a});va&&Oe.push(va),(yt||s||w)&&(await vo(Be.dir,b.id,s,w?{handedBack:new Date().toISOString()}:{endHandBack:!0}),await ut(e,t));const yn=X?await _e(e):{};if(ae(yn,X)!==void 0&&(delete yn[X],await y(xe(e),{links:yn})),ya&&!K(b)&&!(await Ee(e,F(t).file,b)).length){const{line:R}=await W(e,t,"unblocked",{lifted:ya});await oa(e,t,R.ts),await he({dir:e},"card-unblocked",{key:t}),Oe.unshift(`unblocked: ${t}`)}if(!s||wn)return{file:Be.file,lines:Oe};$t&&await Lr(e,t,$t);const ja=await Rr(e,t),Fo=await H(e,t);for(const R of ja){const{line:Ae}=await W(e,R,"unblocked-done",{card:Fo});await oa(e,R,Ae.ts),await he({dir:e},"card-unblocked",{key:R})}return{file:Be.file,lines:[...ja.map(R=>`unblocked: ${R}`),...Oe]}}r(Ki,"changeItem");function Qi(e,{size:t,model:n}){for(const[a,i,o]of[["size",t,["small","large"]],["model",n,["opus","sonnet"]]])i!==void 0&&(i===""?delete e[a]:e[a]=o.includes(i)?i:d(`--${a} takes ${o.join(" or ")}, or "" to clear it`))}r(Qi,"setBuildChoice");function Gi({held:e,unheld:t,pickUpAt:n}){return[e,t,n!==void 0].filter(Boolean).length>1&&d("pass one of --held, --unheld and --pick-up-at"),n!==void 0?n===""?null:{until:Wr(n)}:e?{until:null}:t?null:void 0}r(Gi,"holdOf");function Wr(e){const t=typeof e=="string"&&os.test(e)?Date.parse(e):NaN;return Number.isNaN(t)&&d('--pick-up-at takes an ISO time such as 2026-11-04T09:00, the host\u2019s local time unless it names an offset, or "" to resume the card'),t<=Date.now()&&d(`--pick-up-at ${e} has passed; pass a later time, or --unheld to resume the card now`),new Date(t).toISOString()}r(Wr,"pickUpTime");function Ur(e){delete e.held,delete e.pick_up_at}r(Ur,"clearHold");function an(e,t){t?e.held=!0:delete e.held,t?.until?e.pick_up_at=t.until:delete e.pick_up_at,e.held_at=new Date().toISOString()}r(an,"setHold");async function ud(e,t,n,{announce:a=!0}={}){const{source:i,itemFile:o,item:s}=await x(e,t);if(a){const c=new Date(n).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});await W(e,t,"hold-ended",{at:c}),!s.blocked_until&&!(await Ee(e,F(t).file,s)).length&&await he({dir:e},"card-unblocked",{key:t,until:n})}an(s,null),await y(i.file,o)}r(ud,"pickUp");async function Vi(e,t,n){const a=String(n?.key||""),{file:i,id:o}=F(a),{source:s,itemFile:c}=await Ue(e,i),l=n.shared_by?n.shared_by.name||n.shared_by.login:void 0,u={text:qt.done,who:"love",by:l,command:"done"},p=c.items.find(m=>m.id===o)||d(`item file "${i}" has no item "${o}"`);if(!(t==="card-done"&&!!p.owner_step)&&p.lane==="done")return null;if(t==="card-done")return $e(e,a,{done:!0,force:!0,ownerDone:!0,...u});const f=n.hold?typeof n.until=="string"?new Date(n.until).toISOString():null:void 0;an(p,n.hold?{until:f}:null),await y(s.file,c),n.hold&&(await _o(e,a)||await xo(e,a,{cause:"held",words:"held"}),await vo(s.dir,o,!1,{held:!0}),await ut(e,a));const h=f&&new Date(f).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});return Je(e,a,n.hold?{text:h?`${qt.hold} until ${h}`:qt.hold,who:"love",by:l,command:"hold"}:{who:"love",by:l,command:"resume"})}r(Vi,"recordOwnerTap");function Zi(e){try{return Ps(As(e))}catch(t){return d(t.message)}}r(Zi,"checkSpec");async function fd(e,t,n,a){const{item:i}=await x(e,t);return!i.owner_step||i.owner_check?.check!==n?null:$e(e,t,{done:!0,force:!0,ownerDone:!0,closing:["check-passed",{found:a}]})}r(fd,"passOwnerCheck");async function wd(e,t,n,a){const{source:i,itemFile:o,item:s}=await x(e,t);return!s.owner_step||s.owner_check?.check!==n?!1:(s.owner_check.broken=a,await y(i.file,o),await W(e,t,"check-broken",{error:a}),await he({dir:e},"owner-check-broken",{key:t,check:n,error:a}),!0)}r(wd,"breakOwnerCheck");async function sa(e,t,n,a){const i=[...new Set(String(n).split(",").map(o=>o.trim()).filter(Boolean))];for(const o of i){const s=o.match(Ge)?.[1];if(!s){await x(e,J(t,o));continue}(await wt(e,t,[o])).includes(a)&&d(`${a} is in the wave "${s}", so it would wait on itself`)}return i}r(sa,"depList");async function Yi(e,t,n,a){if(!String(a).trim())return null;const i=J(t,String(a).trim());i===n&&d("--parent: a card can't be its own Parent");for(let o=i;o;){const s=o===i?await x(e,o):await x(e,o).catch(l=>{if(l instanceof ue)return null;throw l});if(!s)break;const{item:c}=s;c.parent&&J(F(o).file,c.parent)===n&&d(`--parent: ${i} is a child of ${n}, so it can't be its Parent`),o=c.parent?J(F(o).file,c.parent):null}return i}r(Yi,"parentKey");async function on(e,t,n,a){const i=t.split("/")[0],o=[...new Set(String(n).split(",").map(s=>s.trim()).filter(Boolean).map(s=>J(i,s)))];o.includes(t)&&d(`${a}: a card can't be related to itself`);for(const s of o)await x(e,s);return o}r(on,"relatedKeys");const Xi=r((e,t,n)=>async()=>{for(const a of await q(e)){const i=await $(a.file,null);let o=!1;for(const s of Array.isArray(i?.items)?i.items:[]){const c=`${a.slug}/${s.id}`;c===t||!n(c)||!(s.related||[]).some(l=>J(a.slug,l)===t)||(s.related=s.related.filter(l=>J(a.slug,l)!==t),s.related.length||delete s.related,o=!0)}o&&await y(a.file,i)}},"dropLinksTo");async function ra(e,t){const n=new Set;let a=null;for(const i of await q(e))for(const o of(await $(i.file,null))?.items||[]){const s=`${i.slug}/${o.id}`,c=(o.related||[]).map(l=>J(i.slug,l));s===t?(c.forEach(l=>n.add(l)),o.parent&&(a=J(i.slug,o.parent))):c.includes(t)&&n.add(s)}return{related:n,parent:a}}r(ra,"linksOf");async function eo(e,t,n,a){const i=r(async(o,s,c=o,l=o)=>{await Je(e,t,{command:c,card:s}),await Je(e,s,{command:l,card:t})},"both");for(const o of a.related)n.related.has(o)||await i("related",o);for(const o of n.related)a.related.has(o)||await i("unrelated",o);a.parent&&a.parent!==n.parent&&await i("followup-of",a.parent,"followup-of","followup")}r(eo,"postLinkLines");async function Hr(e,t,n,a){const i=await on(e,t,a,"--related");return i.length?n.related=i:delete n.related,Xi(e,t,o=>!i.includes(o))}r(Hr,"setRelated");function Jr(e,t,n,a){const i=(n.related||[]).filter(o=>!a.includes(J(F(t).file,o)));return i.length?n.related=i:delete n.related,Xi(e,t,o=>a.includes(o))}r(Jr,"unrelateCards");const se=r((e,t)=>e&&ge().find(n=>Pe(n.dir,e))?.github||t.repo,"boardRepoOf");async function pd(e,t){const{itemFile:n,item:a}=await x(e,t);be(a)||d(`${t} has no PR`);const i=Me(a,se(e,n));i.length||d(`${t} has no repo`);const o=a.gh_since||"1970-01-01T00:00:00Z";try{const s=Se(await oe(e)).github_login||await ks();return await Promise.all(i.map(async({repo:c,number:l})=>({repo:c,...Ta(await Ia({repo:c,pr:l,since:o}),{pr:l,since:o,me:s})})))}catch(s){d(s.message)}}r(pd,"prReport");async function md(e,t,{replyTo:n,body:a}){const i=n===void 0?"pr comment":"pr reply",o=Fn(t)||d(`${i} takes a PR number or owner/repo#<n>`);n!==void 0&&!/^[1-9]\d*$/.test(n)&&d("pr reply takes the id of the review comment it answers");const s=S(a,"--body").trim();(s.startsWith(St.trim())||s.includes(Dn))&&d(`${i} adds the ${St.trim()} prefix and the claude-bridge marker itself; pass only your words`);const c=o.repo||se(D(e),{})||d(`${i} needs a board on GitHub; this board has no repo there`);try{const l=await bs({repo:c,pr:o.number,replyTo:n,words:s,login:Se(await oe(e)).github_login}),u=await ui(e,vt({repo:c,number:o.number}),"comment",`Comment on #${o.number}: ${s}`);return{lines:[`posted on PR ${o.repo?t:`#${t}`}: ${l}`,...u.length?[`queued for ${u.join(", ")}'s Slack thread`]:[]]}}catch(l){d(l.message)}}r(md,"postPR");const sn=r(async e=>gi((await oe(e)).tickets),"boardSource");async function hd(e,t,{afterApproval:n,confirmed:a,tapped:i}={}){D(e);const{itemFile:o,item:s}=await x(e,t);be(s)||d(`${t} has no PR`),K(s)&&d(`${t} is done`);const c=await tn(e,t,s);c&&(i&&await W(e,t,"merge-blocked",{blocked:c}),d(`Not merged: blocked ${c}.`));const l=se(e,o),u=Me(s,l);u.length||d(`${t} has no repo`);const p=new Date().toISOString(),w=r(P=>W(e,t,"gh-limited",{what:"Not merged",words:Io(P)}),"limited");let f;try{f=await Promise.all(u.map(P=>Ia({repo:P.repo,pr:P.number,since:p})))}catch(P){P.limited&&await w(P.limited),d(P.message)}const h=f.map(({view:P})=>P);if(_s(h)){const P=u.map((L,T)=>({...L,at:h[T].mergedAt})).filter((L,T)=>h[T].state==="MERGED");return $e(e,t,{merged:P,closing:["already-merged",{prs:at(P,l),many:P.length>1}]})}const m=u.map((P,L)=>({...P,...f[L]})).filter(({view:P})=>P.state==="OPEN"),k=Rn(o,(await oe(e)).reviewers||[]).length>0,_=[];let N=!1,v=!1;for(const P of m){const{view:L,reviews:T}=P,{failing:M,pending:C}=Ta(P,{pr:P.number,since:p,me:null}),Y=Wo({...L,checks:{state:M.length?"fail":C?"pending":"pass",failing:M,pending:C}},{needsApproval:k}),te=T.filter(Q=>Q.state==="APPROVED").sort((Q,Ze)=>String(Q.submitted_at).localeCompare(String(Ze.submitted_at))).at(-1)?.commit_id;let V=!!(te&&L.headRefOid&&te!==L.headRefOid);V&&n===void 0&&sc(s,te,L.headRefOid)&&(V=!1,v=!0),N||=V,V&&n===void 0&&Y.push('commits after the approval: pass --after-approval "<why they are trivial>" when they are, else ask the approver to look again'),_.push(...u.length>1?Y.map(Q=>`${at([P],l)} ${Q}`):Y)}m.length||_.push(u.length>1?"every PR is closed":"PR is closed"),n!==void 0&&!N&&d("--after-approval: no commit came after the approval; drop it");const O=s.manual_steps?.length?a===void 0?null:await Ii(e,a,"--confirmed"):void 0;if(a!==void 0&&O===void 0&&d(`--confirmed: ${t} has no manual steps; drop it`),O===null&&_.push(`manual steps: merge only on the owner's word, with --confirmed "<their words>"`),_.length){const P=`Not merged: ${_.join("; ")}.`;i&&await W(e,t,"merge-refused",{why:_.join("; ")}),d(P)}const A=[];for(const P of m){const L=await Co(["pr","merge",String(P.number),"--repo",P.repo,"--squash"],{timeout:12e4});L.limited&&await w(L.limited),L.status!==0&&d(`gh pr merge ${at([P],l)} failed: ${L.stderr.trim().split(`
26
- `)[0]||`gh exited ${L.status}`}${A.length?` (merged ${at(A,l)} before it)`:""}`),A.push(P)}const I=[n!==void 0&&`After the approval: ${S(n,"--after-approval")}`,v&&`After the approval: only the board's rebase onto ${s.rebased.at(-1).base}.`,O&&`The owner's word on the manual steps: "${O}"`].filter(Boolean),B=await $e(e,t,{merged:A});return I.length&&await rt(e,t,{text:I.join(" ")}),B}r(hd,"mergeCard");async function gd(e,t){return(await x(e,t)).item.merge_ready||null}r(gd,"heldMerge");async function $d(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);(!i.merge_ready||K(i))&&d(`${t} isn't waiting for its Merge`);const o=await tn(e,t,i);return o&&d(`${t} is blocked ${o}, so its Merge waits until nothing blocks it`),i.merge_ready.tapped=new Date().toISOString(),await y(n.file,a),i.merge_ready}r($d,"tapMerge");async function kd(e,t){const{source:n,item:a}=await x(e,t);return(await Promise.all((await G(n.dir,a.id)).map(o=>$(o,null)))).filter(o=>o?.worktree?.path).sort((o,s)=>String(s.started).localeCompare(String(o.started)))[0]?.model}r(kd,"builderModel");async function to(e,t,n,a,i,o,s){const c=await sn(e)||d(zr);try{s??=a===void 0&&n.plan?await j.readFile(await nn(o,n.plan),"utf8"):n.notes||"";const l=await ss(c,{ticket:a,title:n.title,body:s,repo:se(e,t)},i);n.ticket=l.ticket,l.created?n.ticket_created=!0:delete n.ticket_created}catch(l){if(l instanceof ue)throw l;d(l.message)}}r(to,"assignTicket");const zr="this board has no ticket source, so its cards stay tasks; pick GitHub issues or Linear in Board settings first";async function no(e){const t=[],n=[];for(const a of await q(e)){const i=await $(a.file,{});t.push(...(i.items||[]).map(o=>o.id)),n.push(Number(i.last_task)||0)}return e&&t.push(...(await Le(e)).map(a=>a.item?.id)),`T-${Math.max(0,...n,...t.map(a=>Number(/^T-([1-9]\d*)$/.exec(String(a))?.[1]||0)))+1}`}r(no,"nextTaskId");const ce=r((e,t)=>nt(g.join(e||Ut(),"tickets"),t,{heartbeat:!0}),"ticketWrite"),$e=r((e,t,n,a)=>n.ticket!==void 0||n.newTicket?ce(e,()=>Ki(e,t,n,a)):Ki(e,t,n,a),"setItem"),bd=r((e,t,n,a)=>ce(e,()=>Kr(e,t,n,a)),"addItem");async function Kr(e,t,{title:n,notes:a,notesFrom:i,deps:o,ticket:s,newTicket:c,pickUpAt:l,held:u,parent:p,size:w,model:f,phase:h,state:m,relate:k,fromAsk:_,text:N,intake:v=!1},O){const{file:A,id:I}=t.includes("/")?F(t):{file:t};I!==void 0&&Fe(I,"the item id"),Wi(h,m),N!==void 0&&S(N,"--text"),N!==void 0&&_===void 0&&d("--text goes with --from-ask"),_!==void 0&&N===void 0&&d(`--from-ask needs --text "<first line>", the item's first thread line after the ask`),s!==void 0&&c&&d("pass --ticket <id> to link a ticket or --new-ticket to make one, not both");const B=_===void 0?void 0:await Dt(e,_);B&&await Wn(e,B),B&&await oi(e,B);const P=Gi({held:u,pickUpAt:l});P&&h!==void 0&&d("a card filed held is filed New and takes no --phase; drop --phase, or drop --held to file it in a phase"),B&&h===void 0&&!P&&d("--from-ask files a card, which reads New until it has a phase: pass --phase grilling, prototyping, or ready for a Quick build ask, or --held to file it for later"),h==="ready"&&!(B&&await Ui(e,B))&&d(B?`ask "${B}" is not a Quick build ask, so item add files it grilling or prototyping`:"item add files a card grilling or prototyping; ready is for a Quick build ask, filed with --from-ask"),h==="ready"&&!w&&d("a new ready card has no size; a builder starts from it, so pass --size small or large with --phase ready"),h==="ready"&&!(a??"").trim()&&i===void 0&&d("a new ready card has no notes; a builder starts from them, so pass --notes or --notes-from with --phase ready"),v&&e&&h===void 0&&!P&&d("item add files a card on a board: pass --phase grilling or prototyping, or --held to file it for later");const{source:L,itemFile:T}=await Ue(e,A);I!==void 0&&T.items.some(Q=>Q.id===I)&&d(`item file "${A}" already has an item "${I}"`);const M=new Date().toISOString(),C={id:I,title:S(n,"--title"),created:M,lane:"todo",lane_at:M};p!==void 0&&(C.parent=await Yi(e,A,null,p)||d("--parent takes a card, such as <file>/<id>")),a!==void 0&&i!==void 0&&d("pass --notes or --notes-from, not both");const Y=i!==void 0?await Di(e,i,"--notes-from",()=>`${ta}, and link that card with --relate`):a===void 0?"":S(a,"--notes");i!==void 0&&!Y.trim()&&d(`--notes-from ${i} is empty`);const te=i!==void 0?Y:`# ${C.title}
27
- ${Y&&`
28
- ${Y}
29
- `}`;Qi(C,{size:w,model:f}),o!==void 0&&(C.deps=await sa(e,A,o)),P&&an(C,P),P&&h===void 0&&(C.filed_held=!0),k!==void 0&&(C.related=await on(e,t,k,"--relate")),B&&(C.from_ask=B),h!==void 0&&(C.phase=h),(s!==void 0||c)&&await to(e,T,C,s,O,L.dir,Y),I===void 0&&C.ticket?(C.id=ls(C.ticket),T.items.some(Q=>Q.id===C.id)&&d(`item file "${A}" already has an item "${C.id}"; link ${C.ticket} with item add ${A}/<id> --ticket ${C.ticket}`)):I===void 0&&(C.id=await no(e)),C.plan=Zt(L.dir,C.id),await Yt(g.join(L.dir,C.plan),te);const V=`${A}/${C.id}`;if(T.items.push(C),await y(L.file,T),B){await rt(e,V,{text:N});const Q=await _e(e);ae(Q,B)!==void 0&&(delete Q[B],await y(xe(e),{links:Q}))}return e&&await eo(e,V,{related:new Set,parent:null},await ra(e,V)),{file:L.file,key:V,lines:[]}}r(Kr,"createItem");const yd=r(e=>ce(e,async()=>{const t=[];for(const n of await q(e)){const a=await $(n.file,null);if(!Array.isArray(a?.items)||!Array.isArray(a.followups))continue;const i=a.followups.find(l=>a.items.some(u=>u.id===l.id));i&&d(`${n.slug}: follow-up ${i.id} has the id of a card; rename one, then restart the board`);const o=[];for(const{id:l,title:u,from:p,filed:w,done:f,...h}of a.followups){const m=f?e&&(await z(Z(e,`${n.slug}/${l}`))).map(k=>k.ts).filter(Boolean).sort().pop()||w:null;o.push({id:l,title:u,...w?{created:w}:{},...p?{parent:J(n.slug,p)}:{},...f?{lane:"done",...m?{lane_at:m,closed_at:m}:{}}:{},...h})}const{followups:s,...c}=a;await y(n.file,{...c,items:[...a.items,...o]}),t.push(`${n.slug}: ${o.length} follow-ups moved to cards (${o.filter(l=>l.lane==="done").length} done)`)}return t}),"migrateFollowups");function ao(e,t=[]){if(e.status===void 0&&e.status_at===void 0&&e.merged_at===void 0)return!1;const n=e.merged_at||e.status_at||new Date().toISOString();["merged","done","dropped"].includes(e.status)&&Object.assign(e,{lane:"done",lane_at:n,closed_at:n}),(e.status==="merged"||e.merged_at)&&(e.prs=De(e).map(i=>({...i,merged_at:i.merged_at||n})),delete e.pr),e.status==="held"&&(e.held=!0);const a=t.filter(i=>i.started&&et(i)).sort((i,o)=>String(i.started).localeCompare(String(o.started))).at(-1);return e.status==="checking"&&a&&(a.handed_back=n),delete e.status,delete e.status_at,delete e.merged_at,!0}r(ao,"statusToFacts");const xd=r(e=>ce(e,async()=>{const t=[];for(const n of await q(e)){const a=await $(n.file,null);let i=0;for(const o of Array.isArray(a?.items)?a.items:[]){const s=o.status==="checking"?await G(n.dir,o.id):[],c=await Promise.all(s.map(l=>$(l,null)));if(ao(o,c.filter(Boolean))){i++;for(const[l,u]of c.entries())u?.handed_back&&await y(s[l],u)}}i&&(await y(n.file,a),t.push(`${n.slug}: ${i} cards' statuses became lanes and facts`))}return await Nt(e,async()=>{const n=await Le(e),a=n.filter(i=>i.item&&ao(i.item)).length;a&&(await Rt(e,n),t.push(`archive: ${a} cards' statuses became lanes and facts`))}),t}),"migrateStatuses");function io(e,t){const n=e.lane==="done"&&!_n.done.includes(e.phase);if(!("state"in e)&&!n)return!1;if("state"in e&&e.state!==void 0&&(e.phase=e.state),delete e.state,n){const a=De(e);e.phase=a.some(i=>i.merged_at)||!a.length&&t?"merged":"closed"}return!0}r(io,"stateToPhase");const _d=r(e=>ce(e,async()=>{const t=[],n=await q(e),a=r(async(i,o)=>!!i&&o.lane==="done"&&(await Promise.all((await G(i,o.id)).map(s=>$(s,null)))).some(s=>s?.started&&et(s)),"built");for(const i of n){const o=await $(i.file,null);let s=0;for(const c of Array.isArray(o?.items)?o.items:[])io(c,await a(i.dir,c))&&s++;s&&(await y(i.file,o),t.push(`${i.slug}: ${s} cards' states became phases`))}return await Nt(e,async()=>{const i=await Le(e);let o=0;for(const s of i)s.item&&io(s.item,await a(n.find(c=>c.slug===(s.file||F(s.key).file))?.dir,s.item))&&o++;o&&(await Rt(e,i),t.push(`archive: ${o} cards' states became phases`))}),t}),"migratePhases"),Sd=r(e=>ce(e,async()=>{const t=[];for(const n of await q(e)){const a=await $(n.file,null),i=(Array.isArray(a?.items)?a.items:[]).filter(o=>o.implement_at);if(i.length){for(const o of i)o.phase!=="ready"&&Object.assign(o,{phase:"staging",staged_at:o.implement_at}),delete o.implement_at;await y(n.file,a),t.push(`${n.slug}: ${i.length} Implement asks moved to staging or ready`)}}return t}),"stageImplementAsks"),rn=r(e=>g.join(e,"data","done-archive.json"),"archiveFile"),Nt=r((e,t)=>nt(g.join(e,"done-archive"),t,{heartbeat:!0}),"archiveWrite"),Le=r(async e=>(await $(rn(e),{cards:[]})).cards||[],"readArchive"),cn=r(e=>`${e.closed_at||""}|${e.key}`,"archiveRank"),Qr=r((e,t)=>cn(e)<cn(t)?1:cn(e)>cn(t)?-1:0,"byClosed"),ca=r(e=>g.join(e,"data","done-archive-count.json"),"archiveCountFile");async function vd(e){const t=(await $(ca(e),null))?.count;if(Number.isInteger(t))return t;const n=(await Le(e)).length;return await y(ca(e),{count:n}),n}r(vd,"archiveCount");async function Rt(e,t){t.length?await y(rn(e),{cards:t.sort(Qr)}):await j.rm(rn(e),{force:!0}),await y(ca(e),{count:t.length})}r(Rt,"writeArchive");const jd=r((e,t)=>Nt(e,async()=>{const n=new Map(t.map(w=>[w.key,w])),a=new Date().toISOString(),i=[],o=[];for(const w of await q(e)){const f=await $(w.file,null);if(!Array.isArray(f?.items))continue;const h=f.items.filter(m=>n.has(`${w.slug}/${m.id}`)&&m.lane==="done");if(h.length){for(const m of h){const k=n.get(`${w.slug}/${m.id}`);i.push({...k,file:w.slug,archived_at:a,item:k.prs?Gr(m,k.prs):m})}o.push([w.file,{...f,items:f.items.filter(m=>!h.includes(m))}])}}if(!i.length)return[];const s=await $(E(e),{items:[]}),c=new Map(i.map(w=>[`item:${w.key}`,w]));for(const w of i)w.questions=(s.items||[]).filter(f=>f.topic===`item:${w.key}`&&f.resolved);const l=await _e(e);for(const w of i)w.asks=w.item.from_ask&&ae(l,w.item.from_ask)===void 0?[w.item.from_ask]:[];const u=new Set(i.map(w=>w.key));await Rt(e,[...(await Le(e)).filter(w=>!u.has(w.key)),...i.map(({key:w,file:f,archived_at:h,closed_at:m,item:k,questions:_,asks:N})=>({key:w,file:f,archived_at:h,closed_at:m,item:k,questions:_,asks:N}))]),i.some(w=>w.asks.length)&&await y(xe(e),{links:{...l,...Object.fromEntries(i.flatMap(w=>w.asks.map(f=>[f,w.key])))}});for(const[w,f]of o)await y(w,f);const p=(s.items||[]).filter(w=>!(c.has(w.topic)&&w.resolved));return p.length!==(s.items||[]).length&&await y(E(e),{...s,items:p}),[...u]}),"archiveCards");function Gr(e,t){if(!be(e))return e;const n=De(e).map(o=>{const s=t.find(c=>c.state&&c.number===o.number&&(!o.repo||o.repo===c.repo));return s?{...o,kept:{checks:s.checks,reviews:s.reviews,codexOpen:s.codexOpen}}:o}),{pr:a,...i}=e;return{...i,prs:n}}r(Gr,"keptPRs");async function Vr(e,t){return!e||!de(rn(e))?!1:Nt(e,async()=>{const n=await Le(e),a=n.find(w=>w.key===t);if(!a)return!1;const i=(await q(e)).find(w=>w.slug===a.file),o=i&&await $(i.file,null);if(!Array.isArray(o?.items))return!1;o.items.some(w=>w.id===a.item.id)||await y(i.file,{...o,items:[...o.items,a.item]});const s=await $(E(e),{items:[]}),c=new Set((s.items||[]).map(w=>w.id)),l=(a.questions||[]).filter(w=>!c.has(w.id));l.length&&await y(E(e),{...s,items:[...s.items||[],...l]});const u=await _e(e),p=(a.asks||[]).filter(w=>ae(u,w)===t);return p.length&&await y(xe(e),{links:Object.fromEntries(Object.entries(u).filter(([w])=>!p.includes(w)))}),await Rt(e,n.filter(w=>w!==a)),!0})}r(Vr,"restoreArchived");const oo=["push_tag","no_change"],so=r(e=>Object.fromEntries(Object.entries(e).filter(([t])=>!oo.includes(t))),"stripQuestion"),ro=r(({summary:e,...t})=>t,"stripItem"),co=r(e=>oo.some(t=>t in e),"holdsDead"),Od=r(e=>Nt(e,async()=>{for(const a of await q(e)){const i=await $(a.file,null);Array.isArray(i?.items)&&i.items.some(o=>"summary"in o)&&await y(a.file,{...i,items:i.items.map(ro)})}const t=await $(E(e),null);(t?.items||[]).some(co)&&await y(E(e),{...t,items:t.items.map(so)});const n=await Le(e);n.some(a=>"card"in a||"summary"in a.item||(a.questions||[]).some(co))&&await Rt(e,n.map(({card:a,...i})=>({...i,...a?{closed_at:i.closed_at||a.sortAt}:{},item:ro(i.item),questions:(i.questions||[]).map(so)})))}),"stripDeadFields");async function Ad(){return Vo(async()=>{const e=Go(),t=Array.isArray(e.migrated)?e.migrated:[];if(t.includes("wave-names")||!Object.keys(e.sites||{}).length)return[];const n=[];for(const{route:a,dir:i}of ge(e))await Zr(i),n.push(a);return e.migrated=[...t,"wave-names"],await Zo(e),n})}r(Ad,"migrateWaveNames");const lo=r(async e=>JSON.parse(await j.readFile(e,"utf8")),"readPlain"),la=r((e,t)=>We(e,JSON.stringify(t,null,2)+`
30
- `),"writePlain"),uo=r(e=>typeof e!="string"?e:e.replace(/^wave:([a-z0-9-]+)$/,"file:$1").replace(/^lineup:([a-z0-9-]+)$/,"wave:$1"),"renamedTopic"),da=r(e=>Array.isArray(e)?e.map(da):e&&typeof e=="object"?Object.fromEntries(Object.entries(e).map(([t,n])=>[uo(t),da(n)])):uo(e),"renameTopics");async function Zr(e){const t=g.join(e,"board.json"),n=await lo(t).catch(()=>null);if(n&&typeof n.items=="string"&&n.items!=="waves"&&g.basename(n.items)==="wave.json"){const i=g.join(g.dirname(n.items),"items.json");de(g.resolve(e,n.items))&&await j.rename(g.resolve(e,n.items),g.resolve(e,i)),await la(t,{...n,items:i,slug:n.slug||"wave"})}for(const i of await q(e)){const o=await lo(i.file).catch(()=>null);if(!Array.isArray(o?.lineups))continue;const{lineups:s,...c}=o;await la(i.file,{...c,waves:[...c.waves||[],...s]});for(const{slug:l}of s)await j.rename(g.join(i.dir,`lineup-${l}.md`),g.join(i.dir,ua(l))).catch(u=>{if(u.code!=="ENOENT")throw u})}const a=g.join(e,"data");for(const i of(await j.readdir(a).catch(()=>[])).filter(o=>o.endsWith(".json"))){const o=g.join(a,i),s=await j.readFile(o,"utf8");let c;try{c=JSON.parse(s)}catch{continue}c=da(c),i==="done-archive.json"&&Array.isArray(c.cards)&&(c.cards=c.cards.map(({wave:l,...u})=>l===void 0?u:{...u,file:l})),i==="arch-reviews.json"&&Array.isArray(c.reviews)&&(c.reviews=c.reviews.map(({wave:l,lineup:u,...p})=>u?{...p,wave:l}:{...p,file:l})),JSON.stringify(c)!==JSON.stringify(JSON.parse(s))&&await la(o,c)}}r(Zr,"renameWaveNames");const ua=r(e=>`wave-${e}.md`,"waveNoteFile"),Yr=r(e=>typeof e=="string"&&/^[a-z0-9-]+$/.test(e)?e:d(`a wave's slug uses only a-z, 0-9 and "-", not "${e}"`),"waveSlug");async function pt(e,t){for(const n of await q(e)){const a=await $(n.file,null),i=(a?.waves||[]).find(o=>o.slug===t);if(i)return{source:n,itemFile:a,wave:i}}return d(`this board has no wave "${t}"; wave create makes one`)}r(pt,"findWave");async function Xr(e,t){(await oe(e)).items!=="waves"&&d(`topic file:${t} is for a board over ~/.claude/waves; a wave's triage uses wave:<slug>`),await Ue(e,t)}r(Xr,"triagedFile");function fa(e,t,n,a){return n.map(i=>{const{file:o,id:s}=F(i);o!==e.slug&&d(`${i} is not in item file "${e.slug}"; a wave's cards share one file`),((t.items.find(u=>u.id===s)||d(`item file "${o}" has no item "${s}"`)).deps||[]).includes(`wave:${a}`)&&d(`${i} waits on the wave "${a}", so it can't be in it`);const l=(t.waves||[]).find(u=>u.slug!==a&&!u.finished_at&&Ie(u).includes(s));return l&&d(`${i} is already in the wave "${l.slug}"; wave remove ${l.slug} ${i} first`),s})}r(fa,"waveCards");const wa=r(e=>[...new Set(String(e??"").split(",").map(t=>t.trim()).filter(Boolean))],"cardList");function ec(e){const t=[e??[]].flat().map(n=>{const[a,i]=String(n).split(":");return[a.trim(),wa(i)]});return(!t.length||t.some(([n])=>!n))&&d("--card takes one card per flag in build order, as <file>/<id>, or <file>/<id>:<file>/<id>,\u2026 naming the earlier cards it conflicts with"),t}r(ec,"cardSpecs");function pa(e,t,n,a,i){const o=n.items.indexOf(a),s=i.map(u=>{const p=J(e.slug,u),{file:w,id:f}=F(p),h=w===e.slug?n.items.indexOf(f):-1;return(h<0||h>=o)&&d(`${p} is not an earlier card of the wave "${n.slug}"`),f}),c=t.items.find(u=>u.id===a),l=[...(c.deps||[]).filter(u=>!fo(e,n,u)),...s];l.length?c.deps=l:delete c.deps}r(pa,"relink");const fo=r((e,t,n)=>Ie(t).some(a=>J(e.slug,n)===`${e.slug}/${a}`),"inWave");async function wo(e,t,n,a,i){i!==void 0&&await We(g.join(t.dir,ua(a.slug)),`${S(i,"the note").replace(/\n*$/,`
31
- `)}`),await y(t.file,n);const{tickets:o}=Se(await oe(e));return{file:t.file,lines:[`order: ${po(t,n,a,o)}`]}}r(wo,"writeWave");function po(e,t,n,a){const i=r(o=>tt(t,t.items.find(s=>s.id===o)||{id:o},a),"label");return Ie(n).map(o=>{const s=(t.items.find(c=>c.id===o)?.deps||[]).filter(c=>fo(e,n,c));return s.length?`${i(o)} (after ${s.map(c=>i(F(J(e.slug,c)).id)).join(", ")})`:i(o)}).join(", ")}r(po,"orderLine");async function Pd(e,t,{title:n,goal:a,cards:i,note:o}){Yr(t);const s=ec(i);o===void 0&&d("wave create needs the wave note, as --note <text> or --file <path>");const c=await q(e);c.some(f=>f.slug===t)&&d(`"${t}" is an item file on this board; pick another slug`);for(const f of c)((await $(f.file,null))?.waves||[]).some(h=>h.slug===t)&&d(`this board already has a wave "${t}"`);const l=c.find(f=>f.slug===F(s[0][0]).file)||d(`this board has no item file "${F(s[0][0]).file}"`),{itemFile:u}=await Ue(e,l.slug),p=fa(l,u,s.map(([f])=>f),t);new Set(p).size!==p.length&&d("--card names a card twice");const w={slug:t,title:S(n,"--title"),goal:S(a,"--goal"),items:p,created:new Date().toISOString()};return s.forEach(([,f],h)=>pa(l,u,w,p[h],f)),u.waves=[...u.waves||[],w],wo(e,l,u,w,o)}r(Pd,"createWave");async function Dd(e,t,{add:n,remove:a,card:i,after:o,title:s,goal:c,note:l}){[n,a,i,o,s,c,l].every(m=>m===void 0)&&d("nothing to change: pass a card with --after, --title, --goal, --note or --file"),o!==void 0&&n===void 0&&i===void 0&&d(`--after goes with a card: wave set ${t} <file>/<id> --after <file>/<id>,\u2026`),i!==void 0&&o===void 0&&d(`wave set ${t} ${i} needs --after <file>/<id>,\u2026 ("" clears its links)`);const{source:u,itemFile:p,wave:w}=await pt(e,t);w.finished_at&&d(`the wave "${t}" is finished`),w.items=Ie(w),delete w.phases;const f=a===void 0?[]:await tc(e,t);if(n!==void 0){const[m]=fa(u,p,[n],t);w.items.includes(m)&&d(`${n} is already in the wave "${t}"`),w.items.push(m),pa(u,p,w,m,wa(o))}if(i!==void 0){const{file:m,id:k}=F(i);(m!==u.slug||!w.items.includes(k))&&d(`${i} is not in the wave "${t}"`),pa(u,p,w,k,wa(o))}if(a!==void 0){const[m]=fa(u,p,[a],t);w.items.includes(m)||d(`${a} is not in the wave "${t}"`),w.items.length===1&&d(`${a} is the wave's last card; wave finish ${t} ends the wave`);const k=r(v=>!K(v)&&(v.deps||[]).some(O=>J(u.slug,O)===`${u.slug}/${m}`),"waiting"),_=p.items.filter(v=>w.items.includes(v.id)&&v.id!==m&&k(v)),N=p.items.find(v=>v.id===m);if(_.length&&!(N&&K(N))){const v=await Promise.all(_.map(async O=>`${await H(e,`${u.slug}/${O.id}`)} waits on ${a}: wave set ${t} ${u.slug}/${O.id} --after <its other conflicts, or "">`));d(`${a} can't leave the wave "${t}" while open cards wait on it; relink each, then remove:
22
+ `,"withSection");async function Le(e,t,n,a){const{source:i,item:{id:o}}=await x(e,t);return yr(i.dir,o,async()=>{const{source:s,itemFile:c,item:l}=await x(e,t);let u;l.plan?(u=await sn(s.dir,l.plan,{missingOk:!0}),we(u)||await M(e,t,"notes-missing")):(l.plan=tn(s.dir,l.id),u=g.join(s.dir,l.plan),await y(s.file,c));const p=we(u)?await j.readFile(u,"utf8"):`# ${l.title}
23
+ `;return await nn(u,_r(p,xr(n),a)),l.plan})}r(Le,"appendNotes");async function Ni(e){const{tickets:t}=je(await se(e)),n=[],a=new Map;for(const i of await q(e)){const o=await $(i.file,null);a.set(i.slug,{source:i,itemFile:o||{}});for(const s of o?.items||[])s.plan&&n.push({key:`${i.slug}/${s.id}`,ref:at(o,s,t),title:s.title,created:s.created||"",archived:!1,dir:i.dir,plan:s.plan})}for(const i of e?await Ce(e):[]){if(!i.item?.plan)continue;const o=a.get(i.file);n.push({key:i.key,ref:at(o?.itemFile||{},i.item,t),title:i.item.title,created:i.item.created||"",archived:!0,dir:o?.source.dir||g.join(e,"data"),plan:i.item.plan})}return n}r(Ni,"cardsWithNotes");const Ri=r(e=>j.realpath(g.resolve(e.dir,e.plan)).catch(()=>null),"realNotes");async function Ei(e,t,n,a){if(t==="-")return Io(0,"utf8");const i=await j.realpath(t).catch(()=>d(`${n} ${t}: no such file`));for(const o of await Ni(e))await Ri(o)===i&&d(`${n} ${t} is the notes file of ${o.ref} "${o.title}" (${o.key}), and a card's notes are its own; ${a(o)}`);return j.readFile(i,"utf8")}r(Ei,"refuseCardNotes");const na="write a draft of your own outside the board's data folder";async function ad(e,t,{heading:n,file:a,from:i}){let o;if(i!==void 0){i===t&&d(`${t} can't fold its own notes into itself; pass --from <the other card>`);const[l,u]=await Promise.all([x(e,t),x(e,i)]),[p,w]=await Promise.all([l,u].map(({source:f,item:h})=>h.plan?j.realpath(g.resolve(f.dir,h.plan)).catch(()=>null):null));p&&p===w&&d(`${i} and ${t} name one notes file, ${l.item.plan}, from before the board named each card's own; the board splits it as it starts`),o=(await Wr(e,i)).markdown,n??=`Folded from ${await H(e,i)} "${u.item.title}"`}else o=await Ei(e,a,"--file",l=>l.key===t?`${na}, holding only the new section`:`${na}, or fold that card's notes in with item notes ${t} --from ${l.key}`);o.trim()||d(`${i??a} is empty`);const s=await Le(e,t,n,o),c=await an(e,t,"Recorded in the card's notes");return{file:s,heading:n,lines:c}}r(ad,"addNotes");const id=r(e=>ce(e,async()=>{const t=await Ni(e),n=new Map;for(const i of t){const o=await Ri(i);o&&n.set(o,[...n.get(o)||[],i])}const a=[];for(const[i,o]of n){const[s,...c]=o.sort((l,u)=>l.created<u.created?-1:l.created>u.created?1:l.key<u.key?-1:1);for(const l of c.filter(u=>!u.archived)){const{source:u,itemFile:p,item:w}=await Je(e,F(l.key).file).then(h=>({...h,item:h.itemFile.items.find(m=>m.id===F(l.key).id)})),f=w.plan;w.plan=tn(u.dir,w.id),await nn(g.join(u.dir,w.plan),await j.readFile(i,"utf8")),await y(u.file,p),await M(e,l.key,"notes-shared",{card:s.ref}),a.push(`${l.key}: its own notes, ${w.plan}, copied from ${g.normalize(f)}, which ${s.key} keeps`)}await j.chmod(i,Fi)}return a}),"splitSharedNotes");async function Sr(e,t){const{source:n,item:a}=await x(e,t);return he(a)||_e(a)||a.pushed||(await Promise.all((await G(n.dir,a.id)).map(o=>$(o,null)))).some(o=>o?.started&&!o.done&&o.kind!==$e)||(await ue(e,t)).asked.some(o=>o.state==="open")||(await $(me(e),{}))[t]?!1:!await oa(e,t)}r(Sr,"stageable");async function aa(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);i.phase!=="staging"&&(Object.assign(i,{phase:"staging",staged_at:new Date().toISOString()}),delete i.unfiled,delete i.filed_held,await y(n.file,a),await Ke(e,t,{command:"staged"}))}r(aa,"stage");const Li=r(({text:e,files:t})=>[String(e||"").trim(),...(Array.isArray(t)?t:[]).map(n=>`Attached: ${n?.path||n?.name||n}`)].filter(Boolean).join(`
24
+ `),"messageWords");async function vr(e,t,n){const{item:a}=await x(e,t);a.held&&await eo(e,"card-hold",{key:t,hold:!1});const{owner:i,asked:o}=await ue(e,t),s=o.filter(l=>l.state==="open").map(l=>l.id);s.length&&await Nt(e,i,s,"Skipped: you tapped Implement");const c=Li(n);c&&await Le(e,t,`Implement (owner, ${Ve()}), verbatim`,c),await aa(e,t)}r(vr,"stageImplement");async function od(e,t,n){const a=Li(n);a&&await Le(e,t,`Reply (owner, ${Ve()}), verbatim`,a),await aa(e,t)}r(od,"stageReply");async function sd(e,t,n){const{source:a,itemFile:i,item:o}=await x(e,t);!["ready","staging"].includes(o.phase)||he(o)||(o.phase=n==="prototype"?"prototyping":"grilling",delete o.staged_at,await y(a.file,i))}r(sd,"planAgain");async function jr(e,t){const n=await x(e,t).catch(()=>null);!n||!["ready","staging"].includes(n.item.phase)||he(n.item)||(n.item.phase="grilling",delete n.item.staged_at,await y(n.source.file,n.itemFile),await M(e,t,"new-question"))}r(jr,"grillAgain");function Bi(e){return e.filter(t=>t.state==="answered").map(t=>t.id)}r(Bi,"answeredOnClaude");function Oe(e,t,n,a=new Date().toISOString()){for(const i of e.items||[])t.includes(i.id)&&Object.assign(i,{resolved:n,resolved_at:a})}r(Oe,"setResolved");async function Nt(e,t,n,a){return Oe(t,n,a),await y(E(e),t),E(e)}r(Nt,"markResolved");async function rd(e,t,n){D(e);const a=await $(E(e),{topics:[],items:[]}),i=(a.items||[]).find(f=>f.id===t);i||d(`no question "${t}" in owner.json`);const{answers:o,requests:s,reopens:c}=await pt(e),l=Me(i,o,s,[],c);if(l.state==="resolved")return E(e);l.state==="requested"&&d(`${t} waits on the prototype the owner asked for; question add with round_of ${t} resolves it`);const u=S(n,"--note");Ee(i.url)&&Or(a,i,o);const p=await Nt(e,a,[t],u);await qi(e,i.topic);const w=ea(i.topic);return w&&(await x(e,w).catch(()=>null))?.item.phase==="grilling"&&await Xn(e,w,{roundAt:!0}),p}r(rd,"resolveQuestion");async function cd(e,t,n,a=[]){D(e);const[i,...o]=[...new Set(t)];i||d("question move takes the ids of the questions to merge"),await x(e,n);const s=[n,...a];new Set(s).size!==s.length&&d("question move names a card twice");for(const f of a)await x(e,f);const c=await $(E(e),{topics:[],items:[]}),{answers:l,requests:u,reopens:p}=await pt(e);for(const f of[i,...o]){const h=(c.items||[]).find(b=>b.id===f)||d(`no question "${f}" in owner.json`),{state:m}=Me(h,l,u,[],p);m!=="open"&&d(`${f} is ${m}; question move takes open questions`)}for(const[f,h]of a.entries()){const{item:m}=await x(e,h),b=[...m.deps||[],s[f]].join(",");await ye(e,h,{deps:b,relate:n})}const w=await H(e,n);return Object.assign(c.items.find(f=>f.id===i),{topic:`item:${n}`}),Oe(c,o,`Duplicate of ${w}'s question ${i}`),await y(E(e),c),E(e)}r(cd,"moveQuestions");async function ld(e,t){const n=t.filter(({choice:i,note:o})=>!i&&String(o||"").trim()).map(i=>i.id),a=n.length?await $(E(e),{topics:[],items:[]}):null;a?.items?.some(i=>n.includes(i.id))&&(Oe(a,n,"Answered with a note"),await y(E(e),a))}r(ld,"resolveNoteAnswers");function Or(e,t,n){const a=On(t),i=e.items.filter(o=>o.topic===t.topic&&On(o)===a).map(o=>Me(o,n));Zo(i,a)||d(`${t.id} is in prototype round ${a}, which no pick has settled yet; a pick settles it`)}r(Or,"roundSettled");async function pt(e){const t=await z(g.join(e,"data","events.jsonl"));return{answers:await Promise.all(t.filter(a=>a.kind==="owner-answer").map(async a=>({...a,reply:a.reply||(await Bn(g.join(e,"data","replies"),a.id)).length>0}))),requests:t.filter(a=>a.kind==="prototype-request"),reopens:t.filter(a=>a.kind===Qo)}}r(pt,"ownerEvents");async function dd(e,t){const{asked:n}=await Ar(e),a=new Map(n.map(i=>[i.id,i.state]));return t.filter(i=>!a.has(String(i.id))||["open","requested"].includes(a.get(String(i.id))))}r(dd,"unrecorded");async function Ar(e){const t=await $(E(e),{topics:[],items:[]}),{answers:n,requests:a,reopens:i}=await pt(e);return{owner:t,asked:(t.items||[]).map(o=>Me(o,n,a,[],i))}}r(Ar,"topicQuestionsAll");async function Ci(e,t){const n=await $(E(e),{topics:[],items:[]}),{answers:a,requests:i,reopens:o}=await pt(e),s=(n.items||[]).filter(c=>c.topic===t).map(c=>Me(c,a,i,[],o));return{owner:n,asked:s}}r(Ci,"topicQuestions");const ue=r((e,t)=>Ci(e,`item:${t}`),"cardQuestions"),an=r((e,t,n)=>Ii(e,`item:${t}`,n),"resolveAnswered");async function Ii(e,t,n){if(!e)return[];const{owner:a,asked:i}=await Ci(e,t),o=Bi(i);return o.length&&await Nt(e,a,o,n),await qi(e,t),o.map(s=>`resolved: ${s}`)}r(Ii,"resolveAnsweredOn");const ia=r(async(e,t)=>!!e&&(await ue(e,t)).asked.some(n=>n.state==="open"),"hasOpenQuestion");function Ti(e,t,n){e.next=S(t,"--next"),e.next_at=new Date().toISOString(),n?e.next_on_questions=!0:delete e.next_on_questions}r(Ti,"setNext");async function qi(e,t){const n=t.startsWith("item:")&&!t.startsWith("item:ask:")?t.slice(5):null;if(!n)return;let a;try{a=await x(e,n)}catch(c){if(c instanceof pe)return;throw c}const{source:i,itemFile:o,item:s}=a;s.next_on_questions&&((await ue(e,n)).asked.some(c=>c.state!=="resolved")||(delete s.next,delete s.next_at,delete s.next_on_questions,await y(i.file,o)))}r(qi,"clearWaitedNext");async function Pr(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);i.next===void 0&&i.next_on_questions===void 0||(delete i.next,delete i.next_at,delete i.next_on_questions,await y(n.file,a))}r(Pr,"clearNext");async function Mi(e,t,n,a){if(!e)return async()=>[];const{owner:i,asked:o}=await ue(e,t),s=o.filter(l=>l.state==="open").map(l=>l.id);s.length&&!a&&d(`${n}: ${t} has ${s.length===1?"an open question":"open questions"} ${s.join(", ")} with no answer yet; pass --force to close the card and resolve ${s.length===1?"it":"them"} too`);const c=o.filter(l=>l.state!=="resolved").map(l=>l.id);return async()=>(c.length&&await Nt(e,i,c,"Closed with the card"),c.map(l=>`resolved: ${l}`))}r(Mi,"closeQuestions");async function ud(e,t,n,a,i){await x(e,t),Gt(n),te(a,o=>S(o,"--owner-step")),te(i,o=>to(o)),await Mi(D(e),t,"land",!1)}r(ud,"checkClose");async function oa(e,t){const n=await $(E(e),{items:[]}),{answers:a,requests:i,reopens:o}=await pt(e);return Ko((n.items||[]).map(s=>Me(s,a,i,[],o)),t)}r(oa,"unpickedPrototype");const Dr=r(e=>e.length>120?`${e.slice(0,119)}\u2026`:e,"clip"),Fr={ask:"text","ask-edited":"text","item-message":"text","owner-answer":"note"};async function Wi(e,t,n){const a=S(t,n).replace(/\s+/g," ");return(await z(g.join(e,"data","events.jsonl"))).map(o=>String(o.data?.[Fr[o.kind]]??"").replace(/\s+/g," ").toLowerCase()).some(o=>o.includes(a.toLowerCase()))||d(`${n} must quote the owner's words in an ask, card message or answer note on this board`),a}r(Wi,"ownerQuote");async function fd(e,t,n){return D(e),n===void 0&&d(`only the owner's Serve / Build and the hub's Auto-build start a card run; pass --asked "<the owner's words>" quoting where the owner asked for ${t}'s run`),`Running at the owner's ask: "${await Wi(e,n,"--asked")}"`}r(fd,"askedGate");async function Rt(e,t,n="--from-ask"){return Re(t,n),(await z(g.join(D(e),"data","events.jsonl"))).some(i=>i.id===t&&i.kind==="ask")?t:d(`no ask "${t}" in this site's data/events.jsonl`)}r(Rt,"checkAsk");function Et(e,t,n=new Date().toISOString(),a=Ui(e,t)){if(!Te[t])throw new Error(`no lane "${t}"`);const i=e.lane;return Hi(e,t,a),e.lane=t,e.lane_at=n,t==="done"?(e.closed_at=n,delete e.reopened_at):i==="done"&&(e.reopened_at=n),i}r(Et,"placeIn");function Ui(e,t,n=!1){return t==="todo"?he(e)?"ready":e.phase??null:t==="progress"?"reading":t==="done"?n||Ne(e).some(a=>a.merged_at)?"merged":"closed":null}r(Ui,"movedPhase");function Hi(e,t,n){if(n!==null&&!Sn[t].includes(n))throw new Error(`no ${Te[t]} phase "${n}"`);n===null?delete e.phase:e.phase=n}r(Hi,"setPhase");async function wd(e,t,n,a,i,o){const{source:s}=await x(e,t),{id:c}=F(t),l=new Date().toISOString();if(!await it(s.file,async()=>{const w=JSON.parse(await j.readFile(s.file,"utf8")),f=w.items.find(m=>m.id===c)||d(`item file "${s.slug}" has no item "${c}"`);if(o&&((f.lane??null)!==o.lane||(f.phase??null)!==o.phase)||f.lane===n&&(i===void 0||(f.phase??null)===i))return!1;let h;return f.lane===n?Hi(f,n,i):h=Et(f,n,l,i),await He(s.file,JSON.stringify(w,null,2)+`
25
+ `),h!==void 0}))return null;const p={ts:l,who:"claude",kind:Na,text:`Moved to ${Te[n]}: ${S(a,"why")}`};return await de(V(e,t),p),p.text}r(wd,"moveCard");const Q=r(e=>e?.lane==="done"||!!e?.reopened_at,"closed"),he=r(e=>!!e?.lane&&e.lane!=="todo","leftTodo"),pd=r(async(e,t)=>Q((await x(e,t)).item),"isFinished"),Ji=["ready","grilling","prototyping"];function zi(e,t){t!==void 0&&d(`--state is now --phase: pass --phase ${Ji.includes(t)?t:"ready, grilling or prototyping"}`),e==="staging"&&d("--phase staging is set by the board, on an owner action; pass ready, grilling or prototyping"),e!==void 0&&!Ji.includes(e)&&d("--phase takes ready, grilling or prototyping")}r(zi,"checkPhase");const Nr="a card goes ready only from staging, which the board sets on the owner's Send, Implement or Reply";async function Ki(e,t){return(await z(g.join(D(e),"data","events.jsonl"))).some(a=>a.kind==="ask"&&a.id===t&&a.data?.quick===!0||a.kind==="item-message"&&a.data?.key===`ask:${t}`&&a.data?.intent==="build")}r(Ki,"quickAsk");const Rr=/^mark (?:it |this |the card )?ready\b/i;function Er(e,t,n){const a=[].concat(t).map(s=>Nn(s)||d(`--pr takes a PR number or owner/repo#<n>, not "${s}"`)),i=r((s,c)=>s.number===c.number&&(s.repo||n||null)===(c.repo||n||null),"same"),o=Ne(e);for(const s of a)o.some(c=>i(c,s))||o.push(s);delete e.pr,e.prs=o}r(Er,"addPRs");const J=r((e,t)=>String(t).includes("/")?String(t):`${e}/${t}`,"depKey"),Ze=/^wave:(.+)$/;async function mt(e,t,n){const a=await Promise.all((n||[]).map(async i=>{const o=String(i).match(Ze)?.[1];if(!o)return[J(t,i)];const{source:s,wave:c}=await ht(e,o);return qe(c).map(l=>`${s.slug}/${l}`)}));return[...new Set(a.flat())]}r(mt,"depCards");async function Be(e,t,n){const a=await mt(e,t,n.deps),i=await Promise.all(a.map(o=>x(e,o).then(({item:s})=>s,()=>null)));return a.filter((o,s)=>i[s]&&!Q(i[s]))}r(Be,"unfinishedDeps");async function Lr(e,t,n){const a=await Promise.all((await Be(e,F(t).file,n)).map(i=>H(e,i)));return n.blocked_until?[...a,n.blocked_until]:a}r(Lr,"blockerNames");async function on(e,t,n){n??=(await x(e,t)).item;const a=await Promise.all((await Be(e,F(t).file,n)).map(i=>H(e,i)));return[a.length&&`by ${a.join(", ")}`,n.blocked_until&&`until ${n.blocked_until}`].filter(Boolean).join(" and ")}r(on,"blockWords");const Br=r((e,t)=>!_e(e)&&!e.pushed&&!e.merge_ready&&!Q(e)&&(!!vn(t)||t.some(n=>n?.started&&!n.done&&nt(n))),"inProgress");async function Cr(e,t){const n=[];for(const a of await q(e)){const i=await $(a.file,null);for(const o of Array.isArray(i?.items)?i.items:[])Q(o)||o.blocked_until||!(await mt(e,a.slug,o.deps)).includes(t)||(await Be(e,a.slug,o)).length||n.push(`${a.slug}/${o.id}`)}return n}r(Cr,"unblockedBy");async function Ir(e,t,n){const a=F(t).file,i=(await ra(e,a,n)).map(c=>Ze.test(c)?c:J(a,c));i.length||d("--split-into takes wave:<slug> or the cards the split filed, <id>,<file>/<id>"),(await mt(e,a,i)).includes(t)&&d(`${t} can't be split into itself`);const o=await Promise.all(i.map(async c=>{const l=c.match(Ze)?.[1];if(!l)return H(e,c);const{source:u,wave:p}=await ht(e,l),w=await Promise.all(qe(p).map(f=>H(e,`${u.slug}/${f}`)));return`the wave ${p.title} (${w.join(", ")})`})),s=[];for(const c of await q(e))for(const l of(await $(c.file,null))?.items||[]){const u=`${c.slug}/${l.id}`;if(!(Q(l)||!(l.deps||[]).some(p=>J(c.slug,p)===t))){for(const p of i)(await mt(e,a,[p])).includes(u)&&d(Ze.test(p)?`${u} waits on ${t} and is in the wave "${p.match(Ze)[1]}", so it would wait on itself; split into the new cards by key`:`${u} waits on ${t} and is one of the cards it splits into, so it would wait on itself`);s.push(u)}}return{entries:i,words:o.join(", "),waiters:s}}r(Ir,"splitPlan");async function Tr(e,t,{entries:n,words:a,waiters:i}){for(const s of await q(e)){const c=i.filter(u=>F(u).file===s.slug);if(!c.length)continue;const l=await $(s.file,null);for(const u of l.items.filter(p=>c.includes(`${s.slug}/${p.id}`))){const p=n.map(w=>!Ze.test(w)&&F(w).file===s.slug?F(w).id:w);u.deps=[...new Set([...u.deps.filter(w=>J(s.slug,w)!==t),...p])]}await y(s.file,l)}const o=await H(e,t);for(const s of i)await M(e,s,"split-moved",{card:o,to:a})}r(Tr,"moveWaiters");async function sa(e,t,n){const{source:a,itemFile:i,item:o}=await x(e,t);if(he(o))o.unblocked_at=n;else if(o.phase==="ready")o.recheck_at=n;else return;await y(a.file,i)}r(sa,"markUnblocked");async function Qi(e,t,n,a){const i=new Set(a.match(/#?[\w-]+(?:\.[\w-]+)*(?:\/[\w-]+(?:\.[\w-]+)*)*/g)||[]),o=new Set(await mt(e,F(t).file,n.deps));for(const s of await q(e)){const c=await $(s.file,null);for(const l of c?.items||[]){const u=`${s.slug}/${l.id}`;if(u===t||o.has(u)||Q(l))continue;[l.id,at(c,l),l.ticket].filter(Boolean).flatMap(w=>[w,`${s.slug}/${w}`]).some(w=>i.has(w))&&d(`--next names unfinished card ${u}; add it with --deps`)}}}r(Qi,"checkNextDeps");const Gi=r(e=>`item notes ${e} --heading "<heading>" --file <draft.md>`,"NOTES_COMMAND");function qr(e,t){t.size||d(`${e} has no size; a builder starts from it, so pass --size small or large with --phase ready`),t.plan||d(`${e} has no notes file; a builder starts from it, so add its notes with ${Gi(e)} before --phase ready`)}r(qr,"checkSizeAndNotes");async function Mr(e,t,n,a){qr(t,a);const i=await j.readFile(await sn(n,a.plan),"utf8"),o=(await ue(e,t)).asked.filter(s=>s.answers.length&&!i.includes(s.id)&&!((s.prototype_gate||s.ticket_gate)&&!s.answers.at(-1).note)).map(s=>s.id);o.length&&d(`${t}'s notes leave out the answered question${o.length===1?"":"s"} ${o.join(", ")}; record each answer in its notes, naming its id, with ${Gi(t)} before it is ready`)}r(Mr,"checkNotesComplete");async function sn(e,t,{missingOk:n=!1}={}){const a=r(s=>d(`a notes file is a .md file inside ${e}, named relative to it: "${t}" ${s}`),"bad");(typeof t!="string"||!t.trim())&&a("is empty"),(g.isAbsolute(t)||t.split(/[\\/]/).includes(".."))&&a("leaves the folder"),g.extname(t).toLowerCase()!==".md"&&a("is not a .md file");const i=await j.realpath(e),o=await j.realpath(g.resolve(e,t)).catch(()=>n?g.resolve(i,t):a("does not exist"));return o.startsWith(i+g.sep)||a("leaves the folder"),o}r(sn,"planPath");async function Wr(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);if(i.plan)return{itemFile:a,item:i,markdown:await j.readFile(await sn(n.dir,i.plan),"utf8")};if(typeof i.notes=="string"&&i.notes.trim())return{itemFile:a,item:i,markdown:i.notes};d(`${t} has no notes`)}r(Wr,"readNotes");function Vi({install:e,installSha:t,installLabel:n,clearInstall:a}){if(e===void 0)return(t!==void 0||n!==void 0)&&d("--install-sha and --install-label go with --install <url>"),a?null:void 0;a&&d("pass --install or --clear-install, not both");let i;try{i=new URL(String(e).trim())}catch{i=null}(!i||!["https:","itms-services:"].includes(i.protocol))&&d(`--install takes an https:// or itms-services:// URL, not "${e}"`),i.protocol==="itms-services:"&&!(i.searchParams.get("action")==="download-manifest"&&/^https:\/\//.test(i.searchParams.get("url")||""))&&d("--install: an itms-services:// URL needs ?action=download-manifest&url=https://\u2026/manifest.plist");const o=t===void 0?void 0:/^[0-9a-f]{7,40}$/i.test(t)?t.toLowerCase():d(`--install-sha takes a commit sha of 7 to 40 hex digits, not "${t}"`);return{url:String(e).trim(),...n===void 0?{}:{label:S(n,"--install-label")},built_at:new Date().toISOString(),...o?{sha:o}:{}}}r(Vi,"installOf");async function md(e,{install:t,installSha:n}){const a=Vi({install:t,installSha:n});a||d("main set needs --install <url>");const i=g.join(D(e),"data","main.json");return await y(i,{install:a}),{file:i,lines:["main install set"]}}r(md,"setMainInstall");const Ur=r((e,t)=>e.length?`Merged PR${e.length>1?"s":""} ${ot(e,t)}.`:"Merged.","mergedLine");function Hr(e,t,n,a,i,o){return o?{status:o}:n&&a===void 0?{status:["merged",{words:Ur(n,i)}]}:(a===void 0&&e&&d(`--done needs --text "<what was done, or why it closes without landing>", which becomes ${t}'s last thread line`),a===void 0?null:{text:a})}r(Hr,"closingLine");function Jr(e,t,n){const a=new Date().toISOString(),i=r((o,s)=>o.number===s.number&&(o.repo||n||null)===(s.repo||n||null),"same");e.prs=Ne(e).map(o=>{const s=t.find(c=>i(o,c));return s&&!o.merged_at?{...o,merged_at:s.at||a}:o}),delete e.pr}r(Jr,"markMerged");async function Zi(e,t,{ticket:n,newTicket:a,status:i,clearStatus:o,done:s,splitInto:c,merged:l,landed:u,start:p,handBack:w,next:f,clearNext:h,pr:m,pushed:b,mergeReady:_,ghSince:N,fromAsk:v,phase:O,state:A,held:L,unheld:I,pickUpAt:P,deps:B,blockedUntil:T,blockReason:W,parent:C,related:Z,relate:ne,unrelate:K,install:X,installSha:le,installLabel:fe,clearInstall:xe,title:Ae,size:Tt,model:gt,force:$t,text:oe,closing:ae,who:wn,by:ge,command:ka,ownerStep:et,ownerDone:Do,check:kt},ba){(i!==void 0||o)&&d(`--status${o?" and --clear-status are":" is"} gone: a card's lane is stored and moves itself. Close it with --done --text "<what was done, or why it closes without landing>"`),l&&(s=!0),Z!==void 0&&(ne!==void 0||K!==void 0)&&d("pass --related, or --relate and --unrelate, not both"),zi(O,A),$t&&!s&&d("--force goes with --done"),c!==void 0&&!s&&d("--split-into goes with --done"),c!==void 0&&oe!==void 0&&d("--split-into writes the closing line; drop --text");const bt=c===void 0?null:await Ir(e,t,c);bt&&(oe=`Split into ${bt.words}`),v!==void 0&&s&&d("--from-ask opens a card; close it in another call"),oe!==void 0&&Gt(oe),et!==void 0&&S(et,"--owner-step");const yt=kt===void 0||kt===""?kt:to(kt);oe!==void 0&&v===void 0&&!s&&O!=="ready"&&d("--text goes with --from-ask, --done, or --phase ready on a staging card"),v!==void 0&&oe===void 0&&d(`--from-ask needs --text "<first line>", the item's first thread line after the ask`),f!==void 0&&h&&d("pass --next or --clear-next, not both"),typeof f=="string"&&!f.trim()&&([f,h]=[void 0,!0]);const xt=Xi({held:L,unheld:I,pickUpAt:P}),qt=Vi({install:X,installSha:le,installLabel:fe,clearInstall:xe});[s,p,w,f,h,m,b,_,N,v,O,xt,B,T,W,C,Z,ne,K,qt,Ae,Tt,gt,n,a,et,kt].every(R=>R===void 0||R===!1)&&d("nothing to set: pass --done, --next, --clear-next, --pr, --pushed, --gh-since, --from-ask, --phase, --held, --unheld, --pick-up-at, --deps, --blocked-until, --block-reason, --parent, --related, --relate, --unrelate, --install, --clear-install, --title, --size, --model, --ticket, --new-ticket or --check");const ee=v===void 0?void 0:await Rt(e,v);ee&&await Un(e,ee),ee&&await di(e,ee,t);const{source:Ie,itemFile:tt,item:k}=await x(e,t),ya=ee??(k.phase?void 0:k.from_ask),Fo=ya!==void 0&&await Ki(e,ya);O!==void 0&&k.answered_at&&!k.closed_at&&(delete k.answered_at,he(k)&&Et(k,"todo",void 0,null)),O!==void 0&&he(k)&&d(`--phase sets a Todo card's phase; ${t} is ${Te[k.lane]}, where the board sets it`),O==="ready"&&!Fo&&!["staging","ready"].includes(k.phase)&&d(`${t} reads ${k.phase||"new"}, not staging; ${Nr}`),ee&&k.from_ask&&k.from_ask!==ee&&d(`${t} came from ask "${k.from_ask}", so ask "${ee}" can't be its from-ask; to fold it in: item relate-ask ${ee} ${t} --summary "<line>"`),ee&&xt&&O!==void 0&&!k.phase&&d("a card filed held is filed New and takes no --phase; drop --phase, or drop --held to file it in a phase"),ee&&O===void 0&&!k.phase&&!xt&&d("--from-ask files a card, which reads New until it has a phase: pass --phase grilling, prototyping, or ready for a Quick build ask, or --held to file it for later");const pn=Q(k);bt&&pn&&d(`${t} is already Done, so the cards waiting on it were unblocked then; move them with item set <key> --deps`),et!==void 0&&pn&&d(`${t} is already Done, so its ticket closed; an owner step goes on the command that finishes the card: land --owner-step, or builder done before the merge`),et!==void 0&&(k.owner_step=S(et,"--owner-step")),Do&&delete k.owner_step,yt!==void 0&&!k.owner_step&&d(`--check goes with an owner step, which ${t} doesn't have: pass --owner-step "<the step>" with it on builder done or land`),yt&&(k.owner_check={check:yt,since:k.owner_check?.check===yt?k.owner_check.since:new Date().toISOString()}),(yt===""||!k.owner_step)&&delete k.owner_check;const mn=await Promise.all((await G(Ie.dir,k.id)).map(R=>$(R,null))),No=jn(k,mn);if(Ae!==void 0&&(k.title=S(Ae,"--title")),Yi(k,{size:Tt,model:gt}),B!==void 0){const R=await ra(e,F(t).file,B,t);R.length?k.deps=R:delete k.deps}const xa=T===""?k.blocked_until:void 0;if(T!==void 0&&(T===""?delete k.blocked_until:k.blocked_until=S(T,"--blocked-until")),W&&(!k.deps?.length&&!k.blocked_until&&d("--block-reason goes with a block: --deps or --blocked-until"),k.block_reason=S(W,"--block-reason")),(W===""||!k.deps?.length&&!k.blocked_until)&&delete k.block_reason,C!==void 0){const R=await no(e,F(t).file,t,C);R?k.parent=R:delete k.parent}const _a=C!==void 0||Z!==void 0||ne!==void 0||K!==void 0?await ca(e,t):null;let hn=Z===void 0?null:await Qr(e,t,k,Z);ne!==void 0&&(k.related=[...new Set([...(k.related||[]).map(R=>J(F(t).file,R)),...await cn(e,t,ne,"--relate")])]),K!==void 0&&(hn=await Gr(e,t,k,await cn(e,t,K,"--unrelate"))),xt!==void 0&&rn(k,xt),p&&k.held&&d(k.pick_up_at?`${t} is held until ${k.pick_up_at}, when the board resumes it; start it early only with item set ${t} --unheld`:`${t} is held; start it only once the owner resumes it, with item set ${t} --unheld`);const _t=m!==void 0||b!==void 0||_!==void 0;if((p||s||w||_t)&&(Kr(k),delete k.recheck_at),p){const R=await Be(e,F(t).file,k);R.length&&d(`${t} is blocked by ${R.join(", ")}; start it once ${R.length===1?"that is":"those are"} done`),k.blocked_until&&d(`${t} is blocked until ${k.blocked_until}; start it once that holds and the block is cleared with --blocked-until ""`)}const Mt=p&&e?await oa(e,t):null;Mt&&d(`${t} has no take picked: the owner's answer to ${Mt.id} picks none${Mt.note?` ("${Dr(Mt.note)}")`:""}, so it prototypes again: draw the next round from that note, or grill when the note asks a question, and build once a take is picked`);const Ro=s?await Mi(e,t,"--done",$t):async()=>[],St=s?Hr(e,t,l,oe,re(e,tt),ae):null;if(l&&Jr(k,l,re(e,tt)),f!==void 0&&(Ti(k,f,await ia(e,t)),await Qi(e,t,k,k.next)),h&&(delete k.next,delete k.next_at,delete k.next_on_questions),p&&f===void 0&&k.next_on_questions&&(delete k.next,delete k.next_at,delete k.next_on_questions),_t&&!k.review_since&&(k.review_since=new Date().toISOString()),m!==void 0&&Er(k,m,re(e,tt)),b!==void 0){be().find(R=>Fe(R.dir,D(e)))?.remote||d("--pushed is for a board whose repo is a plain git remote; on GitHub, open a PR and pass --pr"),_e(k)&&d(`${t} has a PR; its PR says when it merges`);try{gs(b)}catch{d("--pushed takes the name of the branch you pushed")}k.pushed={branch:b,at:new Date().toISOString()},k.branch=b}if(_!==void 0&&(k.merge_ready={branch:_,at:new Date().toISOString()},k.branch=_),s&&delete k.merge_ready,N!==void 0&&((typeof N!="string"||!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(N)||Number.isNaN(Date.parse(N)))&&d("--gh-since takes an ISO-8601 UTC time such as 2026-10-23T11:00:00Z"),k.gh_since=N),ee&&(k.from_ask=ee),O==="ready"&&e){const R=(await ue(e,t)).asked.filter(De=>De.state==="open").map(De=>De.id);R.length&&d(`${t} has ${R.length===1?"an open question":"open questions"} ${R.join(", ")}; it is ready once the owner has answered ${R.length===1?"it":"them"}`),await Mr(e,t,Ie.dir,k)}const gn=O==="ready"&&k.phase==="staging"&&v===void 0;gn&&oe===void 0&&d(`${t} goes ready from staging on your judgment: pass --text "<what you checked it against on the board>", which goes into its notes`);const $n=O==="ready"&&k.phase==="ready"&&!!k.recheck_at&&v===void 0;$n&&oe===void 0&&d(`${t}'s block ended, so it builds once you re-check its notes against what landed: pass --text "<what you re-checked>", which goes into its notes`),O==="ready"&&!gn&&!$n&&oe!==void 0&&v===void 0&&!s&&d(`${t} reads ${k.phase||"no phase"}, not staging, so there is no judgment for --text to record`);const Eo=O==="ready"&&k.phase!=="ready";O!==void 0&&(O!=="ready"&&["staging","ready"].includes(k.phase)&&(k.round_at=new Date().toISOString()),k.phase=O,delete k.recheck_at,delete k.staged_at,delete k.unfiled,delete k.filed_held),k.phase==="ready"&&Rr.test(k.next??"")&&d(`${t} would read ready with Next "${k.next}", which asks for the phase it is already in; pass --next with what happens next, or --clear-next`),qt&&(k.install=qt),qt===null&&delete k.install,!No&&!p&&jn(k,mn)&&d(`that leaves ${t} In Progress with nothing building and no Next; pass --next "<what happens next>"`);const kn=(B!==void 0||T)&&!s&&Br(k,mn)?await on(e,t,k):"",Sa=re(e,tt);let bn=null;const yn=r((R,De)=>{k.lane!==R&&Et(k,R,void 0,Ui(k,R,u))!==void 0&&De&&(bn=`Moved to ${Te[R]}: ${De}`)},"moveTo");s?yn("done",null):_t&&k.lane!=="done"?yn("review",m!==void 0?`PR ${ot([].concat(m).map(Nn),Sa)} opened`:b!==void 0?`${b} pushed for its merge`:"verified, it waits for your Merge"):p&&!Q(k)&&yn("progress","a build started");const va=O==="ready"&&!k.ticket&&n===void 0&&!a&&!!(e&&await Ye(e));va&&(a=!0),a&&k.ticket&&d(`${t} already has ticket ${k.ticket}`);const ja=n!==void 0||a;if(ja&&await oo(e,tt,k,n,ba,Ie.dir),await y(Ie.file,tt),ja&&e&&await Vr(e,t,k.ticket),va&&await M(e,t,"ticket-made",{ticket:k.ticket}),bn&&await de(V(e,t),{ts:k.lane_at,who:"claude",kind:Na,text:bn}),gn&&await Le(e,t,`Ready (orchestrator, ${Ve()})`,oe),$n&&await Le(e,t,`Re-checked (orchestrator, ${Ve()})`,oe),Eo&&await Ke(e,t,{command:"readied"}),kn){const R=e&&!await Oo(e,t)&&await jo(e,t,{cause:"blocked",words:`blocked ${kn}`});await M(e,t,R?"blocked-pausing":"blocked-stays",{blocked:kn})}hn&&await hn(),_a&&await io(e,t,_a,await ca(e,t));const Pe=await Ro();if(O==="ready"&&e&&Pe.push(...await an(e,t,"Recorded in the card's notes")),p&&Pe.push(...await an(e,t,"Acted on: build started")),ee&&Pe.push(...(await lt(e,t,{text:oe})).lines),St&&e){const R=ka?await Ke(e,t,{command:ka,text:St.text,who:wn,by:ge}):St.status?await M(e,t,...St.status):await lt(e,t,{text:St.text});Pe.push(...R.lines)}const Oa=k.owner_step&&s?null:us({start:p,done:s,handover:_t}),Aa=Oa&&await fs(await Ye(e),k,Oa,{...ba,repo:Sa});Aa&&Pe.push(Aa),(_t||s||w)&&(await Po(Ie.dir,k.id,s,w?{handedBack:new Date().toISOString()}:{endHandBack:!0}),await wt(e,t));const xn=ee?await ve(e):{};if(ie(xn,ee)!==void 0&&(delete xn[ee],await y(Se(e),{links:xn})),xa&&!Q(k)&&!(await Be(e,F(t).file,k)).length){const{line:R}=await M(e,t,"unblocked",{lifted:xa});await sa(e,t,R.ts),await ke({dir:e},"card-unblocked",{key:t}),Pe.unshift(`unblocked: ${t}`)}if(!s||pn)return{file:Ie.file,lines:Pe};bt&&await Tr(e,t,bt);const Pa=await Cr(e,t),Lo=await H(e,t);for(const R of Pa){const{line:De}=await M(e,R,"unblocked-done",{card:Lo});await sa(e,R,De.ts),await ke({dir:e},"card-unblocked",{key:R})}return{file:Ie.file,lines:[...Pa.map(R=>`unblocked: ${R}`),...Pe]}}r(Zi,"changeItem");function Yi(e,{size:t,model:n}){for(const[a,i,o]of[["size",t,["small","large"]],["model",n,["opus","sonnet"]]])i!==void 0&&(i===""?delete e[a]:e[a]=o.includes(i)?i:d(`--${a} takes ${o.join(" or ")}, or "" to clear it`))}r(Yi,"setBuildChoice");function Xi({held:e,unheld:t,pickUpAt:n}){return[e,t,n!==void 0].filter(Boolean).length>1&&d("pass one of --held, --unheld and --pick-up-at"),n!==void 0?n===""?null:{until:zr(n)}:e?{until:null}:t?null:void 0}r(Xi,"holdOf");function zr(e){const t=typeof e=="string"&&ls.test(e)?Date.parse(e):NaN;return Number.isNaN(t)&&d('--pick-up-at takes an ISO time such as 2026-11-04T09:00, the host\u2019s local time unless it names an offset, or "" to resume the card'),t<=Date.now()&&d(`--pick-up-at ${e} has passed; pass a later time, or --unheld to resume the card now`),new Date(t).toISOString()}r(zr,"pickUpTime");function Kr(e){delete e.held,delete e.pick_up_at}r(Kr,"clearHold");function rn(e,t){t?e.held=!0:delete e.held,t?.until?e.pick_up_at=t.until:delete e.pick_up_at,e.held_at=new Date().toISOString()}r(rn,"setHold");async function hd(e,t,n,{announce:a=!0}={}){const{source:i,itemFile:o,item:s}=await x(e,t);if(a){const c=new Date(n).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});await M(e,t,"hold-ended",{at:c}),!s.blocked_until&&!(await Be(e,F(t).file,s)).length&&await ke({dir:e},"card-unblocked",{key:t,until:n})}rn(s,null),await y(i.file,o)}r(hd,"pickUp");async function eo(e,t,n){const a=String(n?.key||""),{file:i,id:o}=F(a),{source:s,itemFile:c}=await Je(e,i),l=n.shared_by?n.shared_by.name||n.shared_by.login:void 0,u={text:Ut.done,who:"love",by:l,command:"done"},p=c.items.find(m=>m.id===o)||d(`item file "${i}" has no item "${o}"`);if(!(t==="card-done"&&!!p.owner_step)&&p.lane==="done")return null;if(t==="card-done")return ye(e,a,{done:!0,force:!0,ownerDone:!0,...u});const f=n.hold?typeof n.until=="string"?new Date(n.until).toISOString():null:void 0;rn(p,n.hold?{until:f}:null),await y(s.file,c),n.hold&&(await Oo(e,a)||await jo(e,a,{cause:"held",words:"held"}),await Po(s.dir,o,!1,{held:!0}),await wt(e,a));const h=f&&new Date(f).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});return Ke(e,a,n.hold?{text:h?`${Ut.hold} until ${h}`:Ut.hold,who:"love",by:l,command:"hold"}:{who:"love",by:l,command:"resume"})}r(eo,"recordOwnerTap");function to(e){try{return Rs(Ns(e))}catch(t){return d(t.message)}}r(to,"checkSpec");async function gd(e,t,n,a){const{item:i}=await x(e,t);return!i.owner_step||i.owner_check?.check!==n?null:ye(e,t,{done:!0,force:!0,ownerDone:!0,closing:["check-passed",{found:a}]})}r(gd,"passOwnerCheck");async function $d(e,t,n,a){const{source:i,itemFile:o,item:s}=await x(e,t);return!s.owner_step||s.owner_check?.check!==n?!1:(s.owner_check.broken=a,await y(i.file,o),await M(e,t,"check-broken",{error:a}),await ke({dir:e},"owner-check-broken",{key:t,check:n,error:a}),!0)}r($d,"breakOwnerCheck");async function ra(e,t,n,a){const i=[...new Set(String(n).split(",").map(o=>o.trim()).filter(Boolean))];for(const o of i){const s=o.match(Ze)?.[1];if(!s){await x(e,J(t,o));continue}(await mt(e,t,[o])).includes(a)&&d(`${a} is in the wave "${s}", so it would wait on itself`)}return i}r(ra,"depList");async function no(e,t,n,a){if(!String(a).trim())return null;const i=J(t,String(a).trim());i===n&&d("--parent: a card can't be its own Parent");for(let o=i;o;){const s=o===i?await x(e,o):await x(e,o).catch(l=>{if(l instanceof pe)return null;throw l});if(!s)break;const{item:c}=s;c.parent&&J(F(o).file,c.parent)===n&&d(`--parent: ${i} is a child of ${n}, so it can't be its Parent`),o=c.parent?J(F(o).file,c.parent):null}return i}r(no,"parentKey");async function cn(e,t,n,a){const i=t.split("/")[0],o=[...new Set(String(n).split(",").map(s=>s.trim()).filter(Boolean).map(s=>J(i,s)))];o.includes(t)&&d(`${a}: a card can't be related to itself`);for(const s of o)await x(e,s);return o}r(cn,"relatedKeys");const ao=r((e,t,n)=>async()=>{for(const a of await q(e)){const i=await $(a.file,null);let o=!1;for(const s of Array.isArray(i?.items)?i.items:[]){const c=`${a.slug}/${s.id}`;c===t||!n(c)||!(s.related||[]).some(l=>J(a.slug,l)===t)||(s.related=s.related.filter(l=>J(a.slug,l)!==t),s.related.length||delete s.related,o=!0)}o&&await y(a.file,i)}},"dropLinksTo");async function ca(e,t){const n=new Set;let a=null;for(const i of await q(e))for(const o of(await $(i.file,null))?.items||[]){const s=`${i.slug}/${o.id}`,c=(o.related||[]).map(l=>J(i.slug,l));s===t?(c.forEach(l=>n.add(l)),o.parent&&(a=J(i.slug,o.parent))):c.includes(t)&&n.add(s)}return{related:n,parent:a}}r(ca,"linksOf");async function io(e,t,n,a){const i=r(async(o,s,c=o,l=o)=>{await Ke(e,t,{command:c,card:s}),await Ke(e,s,{command:l,card:t})},"both");for(const o of a.related)n.related.has(o)||await i("related",o);for(const o of n.related)a.related.has(o)||await i("unrelated",o);a.parent&&a.parent!==n.parent&&await i("followup-of",a.parent,"followup-of","followup")}r(io,"postLinkLines");async function Qr(e,t,n,a){const i=await cn(e,t,a,"--related");return i.length?n.related=i:delete n.related,ao(e,t,o=>!i.includes(o))}r(Qr,"setRelated");function Gr(e,t,n,a){const i=(n.related||[]).filter(o=>!a.includes(J(F(t).file,o)));return i.length?n.related=i:delete n.related,ao(e,t,o=>a.includes(o))}r(Gr,"unrelateCards");const re=r((e,t)=>e&&be().find(n=>Fe(n.dir,e))?.github||t.repo,"boardRepoOf");async function kd(e,t){const{itemFile:n,item:a}=await x(e,t);_e(a)||d(`${t} has no PR`);const i=Ue(a,re(e,n));i.length||d(`${t} has no repo`);const o=a.gh_since||"1970-01-01T00:00:00Z";try{const s=je(await se(e)).github_login||await _s();return await Promise.all(i.map(async({repo:c,number:l})=>({repo:c,...Ha(await Ua({repo:c,pr:l,since:o}),{pr:l,since:o,me:s})})))}catch(s){d(s.message)}}r(kd,"prReport");async function bd(e,t,{replyTo:n,body:a}){const i=n===void 0?"pr comment":"pr reply",o=Nn(t)||d(`${i} takes a PR number or owner/repo#<n>`);n!==void 0&&!/^[1-9]\d*$/.test(n)&&d("pr reply takes the id of the review comment it answers");const s=S(a,"--body").trim();(s.startsWith(jt.trim())||s.includes(Fn))&&d(`${i} adds the ${jt.trim()} prefix and the claude-bridge marker itself; pass only your words`);const c=o.repo||re(D(e),{})||d(`${i} needs a board on GitHub; this board has no repo there`);try{const l=await Ss({repo:c,pr:o.number,replyTo:n,words:s,login:je(await se(e)).github_login}),u=await hi(e,Ot({repo:c,number:o.number}),"comment",`Comment on #${o.number}: ${s}`);return{lines:[`posted on PR ${o.repo?t:`#${t}`}: ${l}`,...u.length?[`queued for ${u.join(", ")}'s Slack thread`]:[]]}}catch(l){d(l.message)}}r(bd,"postPR");const Ye=r(async e=>xi((await se(e)).tickets),"boardSource");async function yd(e,t,{afterApproval:n,confirmed:a,tapped:i}={}){D(e);const{itemFile:o,item:s}=await x(e,t);_e(s)||d(`${t} has no PR`),Q(s)&&d(`${t} is done`);const c=await on(e,t,s);c&&(i&&await M(e,t,"merge-blocked",{blocked:c}),d(`Not merged: blocked ${c}.`));const l=re(e,o),u=Ue(s,l);u.length||d(`${t} has no repo`);const p=new Date().toISOString(),w=r(P=>M(e,t,"gh-limited",{what:"Not merged",words:Wo(P)}),"limited");let f;try{f=await Promise.all(u.map(P=>Ua({repo:P.repo,pr:P.number,comments:!1})))}catch(P){P.limited&&await w(P.limited),d(P.message)}const h=f.map(({view:P})=>P);if(Os(h)){const P=u.map((B,T)=>({...B,at:h[T].mergedAt})).filter((B,T)=>h[T].state==="MERGED");return ye(e,t,{merged:P,closing:["already-merged",{prs:ot(P,l),many:P.length>1}]})}const m=u.map((P,B)=>({...P,...f[B]})).filter(({view:P})=>P.state==="OPEN"),b=En(o,(await se(e)).reviewers||[]).length>0,_=[];let N=!1,v=!1;for(const P of m){const{view:B,reviews:T}=P,{failing:W,pending:C}=Ha(P,{pr:P.number,since:p,me:null}),Z=zo({...B,checks:{state:W.length?"fail":C?"pending":"pass",failing:W,pending:C}},{needsApproval:b}),ne=T.filter(X=>X.state==="APPROVED").sort((X,le)=>String(X.submitted_at).localeCompare(String(le.submitted_at))).at(-1)?.commit_id;let K=!!(ne&&B.headRefOid&&ne!==B.headRefOid);K&&n===void 0&&uc(s,ne,B.headRefOid)&&(K=!1,v=!0),N||=K,K&&n===void 0&&Z.push('commits after the approval: pass --after-approval "<why they are trivial>" when they are, else ask the approver to look again'),_.push(...u.length>1?Z.map(X=>`${ot([P],l)} ${X}`):Z)}m.length||_.push(u.length>1?"every PR is closed":"PR is closed"),n!==void 0&&!N&&d("--after-approval: no commit came after the approval; drop it");const O=s.manual_steps?.length?a===void 0?null:await Wi(e,a,"--confirmed"):void 0;if(a!==void 0&&O===void 0&&d(`--confirmed: ${t} has no manual steps; drop it`),O===null&&_.push(`manual steps: merge only on the owner's word, with --confirmed "<their words>"`),_.length){const P=`Not merged: ${_.join("; ")}.`;i&&await M(e,t,"merge-refused",{why:_.join("; ")}),d(P)}const A=[];for(const P of m){const B=await Mo(["pr","merge",String(P.number),"--repo",P.repo,"--squash"],{timeout:12e4});B.limited&&await w(B.limited),B.status!==0&&d(`gh pr merge ${ot([P],l)} failed: ${B.stderr.trim().split(`
26
+ `)[0]||`gh exited ${B.status}`}${A.length?` (merged ${ot(A,l)} before it)`:""}`),A.push(P)}const L=[n!==void 0&&`After the approval: ${S(n,"--after-approval")}`,v&&`After the approval: only the board's rebase onto ${s.rebased.at(-1).base}.`,O&&`The owner's word on the manual steps: "${O}"`].filter(Boolean),I=await ye(e,t,{merged:A});return L.length&&await lt(e,t,{text:L.join(" ")}),I}r(yd,"mergeCard");async function xd(e,t){return(await x(e,t)).item.merge_ready||null}r(xd,"heldMerge");async function _d(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);(!i.merge_ready||Q(i))&&d(`${t} isn't waiting for its Merge`);const o=await on(e,t,i);return o&&d(`${t} is blocked ${o}, so its Merge waits until nothing blocks it`),i.merge_ready.tapped=new Date().toISOString(),await y(n.file,a),i.merge_ready}r(_d,"tapMerge");async function Sd(e,t){const{source:n,item:a}=await x(e,t);return(await Promise.all((await G(n.dir,a.id)).map(o=>$(o,null)))).filter(o=>o?.worktree?.path&&o.kind!==Y).sort((o,s)=>String(s.started).localeCompare(String(o.started)))[0]?.model}r(Sd,"builderModel");async function oo(e,t,n,a,i,o,s){const c=await Ye(e)||d(Zr);try{s??=a===void 0&&n.plan?await j.readFile(await sn(o,n.plan),"utf8"):n.notes||"";const l=await ds(c,{ticket:a,title:n.title,body:s,repo:re(e,t)},i);n.ticket=l.ticket,l.created?n.ticket_created=!0:delete n.ticket_created}catch(l){if(l instanceof pe)throw l;d(l.message)}}r(oo,"assignTicket");async function Vr(e,t,n){const a=await $(me(e),{}),i=(a[t]||[]).filter(l=>!l.ticket_gate);i.length!==(a[t]||[]).length&&await y(me(e),i.length?{...a,[t]:i}:Xt(a,t));const{owner:o,asked:s}=await ue(e,t),c=s.filter(l=>l.ticket_gate&&l.state==="open").map(l=>l.id);c.length&&(await Nt(e,o,c,`Ticket linked: ${n}`),(await x(e,t)).item.phase==="grilling"&&await Xn(e,t,{roundAt:!0}))}r(Vr,"dropTicketQuestion");const Zr="this board has no ticket source, so its cards stay tasks; pick GitHub issues or Linear in Board settings first";async function so(e){const t=[],n=[];for(const a of await q(e)){const i=await $(a.file,{});t.push(...(i.items||[]).map(o=>o.id)),n.push(Number(i.last_task)||0)}return e&&t.push(...(await Ce(e)).map(a=>a.item?.id)),`T-${Math.max(0,...n,...t.map(a=>Number(/^T-([1-9]\d*)$/.exec(String(a))?.[1]||0)))+1}`}r(so,"nextTaskId");const ce=r((e,t)=>it(g.join(e||zt(),"tickets"),t,{heartbeat:!0}),"ticketWrite"),ye=r((e,t,n,a)=>n.ticket!==void 0||n.newTicket||n.phase==="ready"?ce(e,()=>Zi(e,t,n,a)):Zi(e,t,n,a),"setItem"),vd=r((e,t,n,a)=>ce(e,()=>Yr(e,t,n,a)),"addItem");async function Yr(e,t,{title:n,notes:a,notesFrom:i,deps:o,ticket:s,newTicket:c,pickUpAt:l,held:u,parent:p,size:w,model:f,phase:h,state:m,relate:b,fromAsk:_,text:N,intake:v=!1},O){const{file:A,id:L}=t.includes("/")?F(t):{file:t};L!==void 0&&Re(L,"the item id"),zi(h,m),N!==void 0&&S(N,"--text"),N!==void 0&&_===void 0&&d("--text goes with --from-ask"),_!==void 0&&N===void 0&&d(`--from-ask needs --text "<first line>", the item's first thread line after the ask`),s!==void 0&&c&&d("pass --ticket <id> to link a ticket or --new-ticket to make one, not both");const I=_===void 0?void 0:await Rt(e,_);I&&await Un(e,I),I&&await di(e,I);const P=Xi({held:u,pickUpAt:l});P&&h!==void 0&&d("a card filed held is filed New and takes no --phase; drop --phase, or drop --held to file it in a phase"),I&&h===void 0&&!P&&d("--from-ask files a card, which reads New until it has a phase: pass --phase grilling, prototyping, or ready for a Quick build ask, or --held to file it for later"),h==="ready"&&!(I&&await Ki(e,I))&&d(I?`ask "${I}" is not a Quick build ask, so item add files it grilling or prototyping`:"item add files a card grilling or prototyping; ready is for a Quick build ask, filed with --from-ask"),h==="ready"&&!w&&d("a new ready card has no size; a builder starts from it, so pass --size small or large with --phase ready"),h==="ready"&&!(a??"").trim()&&i===void 0&&d("a new ready card has no notes; a builder starts from them, so pass --notes or --notes-from with --phase ready"),v&&e&&h===void 0&&!P&&d("item add files a card on a board: pass --phase grilling or prototyping, or --held to file it for later");const{source:B,itemFile:T}=await Je(e,A);L!==void 0&&T.items.some(fe=>fe.id===L)&&d(`item file "${A}" already has an item "${L}"`);const W=new Date().toISOString(),C={id:L,title:S(n,"--title"),created:W,lane:"todo",lane_at:W};p!==void 0&&(C.parent=await no(e,A,null,p)||d("--parent takes a card, such as <file>/<id>")),a!==void 0&&i!==void 0&&d("pass --notes or --notes-from, not both");const Z=i!==void 0?await Ei(e,i,"--notes-from",()=>`${na}, and link that card with --relate`):a===void 0?"":S(a,"--notes");i!==void 0&&!Z.trim()&&d(`--notes-from ${i} is empty`);const ne=i!==void 0?Z:`# ${C.title}
27
+ ${Z&&`
28
+ ${Z}
29
+ `}`;Yi(C,{size:w,model:f}),o!==void 0&&(C.deps=await ra(e,A,o)),P&&rn(C,P),P&&h===void 0&&(C.filed_held=!0),b!==void 0&&(C.related=await cn(e,t,b,"--relate")),I&&(C.from_ask=I),h!==void 0&&(C.phase=h);const K=L===void 0?null:Ba(await Ye(e),L);K&&s!==void 0&&Ba(await Ye(e),s)!==K&&d(`--ticket ${s} is not the ticket ${L} names`);const X=h==="ready"&&!K&&s===void 0&&!c&&!!await Ye(e);X&&(c=!0),K&&c&&d(`${L} names ticket ${K}, so --new-ticket would make another; drop --new-ticket`),(s!==void 0||c||K)&&await oo(e,T,C,s??K??void 0,O,B.dir,Z),L===void 0&&C.ticket?(C.id=ws(C.ticket),T.items.some(fe=>fe.id===C.id)&&d(`item file "${A}" already has an item "${C.id}"; link ${C.ticket} with item add ${A}/<id> --ticket ${C.ticket}`)):L===void 0&&(C.id=await so(e)),C.plan=tn(B.dir,C.id),await nn(g.join(B.dir,C.plan),ne);const le=`${A}/${C.id}`;if(T.items.push(C),await y(B.file,T),I){await lt(e,le,{text:N});const fe=await ve(e);ie(fe,I)!==void 0&&(delete fe[I],await y(Se(e),{links:fe}))}return X&&e&&await M(e,le,"ticket-made",{ticket:C.ticket}),e&&await io(e,le,{related:new Set,parent:null},await ca(e,le)),{file:B.file,key:le,lines:[]}}r(Yr,"createItem");const jd=r(e=>ce(e,async()=>{const t=[];for(const n of await q(e)){const a=await $(n.file,null);if(!Array.isArray(a?.items)||!Array.isArray(a.followups))continue;const i=a.followups.find(l=>a.items.some(u=>u.id===l.id));i&&d(`${n.slug}: follow-up ${i.id} has the id of a card; rename one, then restart the board`);const o=[];for(const{id:l,title:u,from:p,filed:w,done:f,...h}of a.followups){const m=f?e&&(await z(V(e,`${n.slug}/${l}`))).map(b=>b.ts).filter(Boolean).sort().pop()||w:null;o.push({id:l,title:u,...w?{created:w}:{},...p?{parent:J(n.slug,p)}:{},...f?{lane:"done",...m?{lane_at:m,closed_at:m}:{}}:{},...h})}const{followups:s,...c}=a;await y(n.file,{...c,items:[...a.items,...o]}),t.push(`${n.slug}: ${o.length} follow-ups moved to cards (${o.filter(l=>l.lane==="done").length} done)`)}return t}),"migrateFollowups");function ro(e,t=[]){if(e.status===void 0&&e.status_at===void 0&&e.merged_at===void 0)return!1;const n=e.merged_at||e.status_at||new Date().toISOString();["merged","done","dropped"].includes(e.status)&&Object.assign(e,{lane:"done",lane_at:n,closed_at:n}),(e.status==="merged"||e.merged_at)&&(e.prs=Ne(e).map(i=>({...i,merged_at:i.merged_at||n})),delete e.pr),e.status==="held"&&(e.held=!0);const a=t.filter(i=>i.started&&nt(i)&&i.kind!==Y).sort((i,o)=>String(i.started).localeCompare(String(o.started))).at(-1);return e.status==="checking"&&a&&(a.handed_back=n),delete e.status,delete e.status_at,delete e.merged_at,!0}r(ro,"statusToFacts");const Od=r(e=>ce(e,async()=>{const t=[];for(const n of await q(e)){const a=await $(n.file,null);let i=0;for(const o of Array.isArray(a?.items)?a.items:[]){const s=o.status==="checking"?await G(n.dir,o.id):[],c=await Promise.all(s.map(l=>$(l,null)));if(ro(o,c.filter(Boolean))){i++;for(const[l,u]of c.entries())u?.handed_back&&await y(s[l],u)}}i&&(await y(n.file,a),t.push(`${n.slug}: ${i} cards' statuses became lanes and facts`))}return await Lt(e,async()=>{const n=await Ce(e),a=n.filter(i=>i.item&&ro(i.item)).length;a&&(await Bt(e,n),t.push(`archive: ${a} cards' statuses became lanes and facts`))}),t}),"migrateStatuses");function co(e,t){const n=e.lane==="done"&&!Sn.done.includes(e.phase);if(!("state"in e)&&!n)return!1;if("state"in e&&e.state!==void 0&&(e.phase=e.state),delete e.state,n){const a=Ne(e);e.phase=a.some(i=>i.merged_at)||!a.length&&t?"merged":"closed"}return!0}r(co,"stateToPhase");const Ad=r(e=>ce(e,async()=>{const t=[],n=await q(e),a=r(async(i,o)=>!!i&&o.lane==="done"&&(await Promise.all((await G(i,o.id)).map(s=>$(s,null)))).some(s=>s?.started&&nt(s)),"built");for(const i of n){const o=await $(i.file,null);let s=0;for(const c of Array.isArray(o?.items)?o.items:[])co(c,await a(i.dir,c))&&s++;s&&(await y(i.file,o),t.push(`${i.slug}: ${s} cards' states became phases`))}return await Lt(e,async()=>{const i=await Ce(e);let o=0;for(const s of i)s.item&&co(s.item,await a(n.find(c=>c.slug===(s.file||F(s.key).file))?.dir,s.item))&&o++;o&&(await Bt(e,i),t.push(`archive: ${o} cards' states became phases`))}),t}),"migratePhases"),Pd=r(e=>ce(e,async()=>{const t=[];for(const n of await q(e)){const a=await $(n.file,null),i=(Array.isArray(a?.items)?a.items:[]).filter(o=>o.implement_at);if(i.length){for(const o of i)o.phase!=="ready"&&Object.assign(o,{phase:"staging",staged_at:o.implement_at}),delete o.implement_at;await y(n.file,a),t.push(`${n.slug}: ${i.length} Implement asks moved to staging or ready`)}}return t}),"stageImplementAsks"),ln=r(e=>g.join(e,"data","done-archive.json"),"archiveFile"),Lt=r((e,t)=>it(g.join(e,"done-archive"),t,{heartbeat:!0}),"archiveWrite"),Ce=r(async e=>(await $(ln(e),{cards:[]})).cards||[],"readArchive"),dn=r(e=>`${e.closed_at||""}|${e.key}`,"archiveRank"),Xr=r((e,t)=>dn(e)<dn(t)?1:dn(e)>dn(t)?-1:0,"byClosed"),la=r(e=>g.join(e,"data","done-archive-count.json"),"archiveCountFile");async function Dd(e){const t=(await $(la(e),null))?.count;if(Number.isInteger(t))return t;const n=(await Ce(e)).length;return await y(la(e),{count:n}),n}r(Dd,"archiveCount");async function Bt(e,t){t.length?await y(ln(e),{cards:t.sort(Xr)}):await j.rm(ln(e),{force:!0}),await y(la(e),{count:t.length})}r(Bt,"writeArchive");const Fd=r((e,t)=>Lt(e,async()=>{const n=new Map(t.map(w=>[w.key,w])),a=new Date().toISOString(),i=[],o=[];for(const w of await q(e)){const f=await $(w.file,null);if(!Array.isArray(f?.items))continue;const h=f.items.filter(m=>n.has(`${w.slug}/${m.id}`)&&m.lane==="done");if(h.length){for(const m of h){const b=n.get(`${w.slug}/${m.id}`);i.push({...b,file:w.slug,archived_at:a,item:b.prs?ec(m,b.prs):m})}o.push([w.file,{...f,items:f.items.filter(m=>!h.includes(m))}])}}if(!i.length)return[];const s=await $(E(e),{items:[]}),c=new Map(i.map(w=>[`item:${w.key}`,w]));for(const w of i)w.questions=(s.items||[]).filter(f=>f.topic===`item:${w.key}`&&f.resolved);const l=await ve(e);for(const w of i)w.asks=w.item.from_ask&&ie(l,w.item.from_ask)===void 0?[w.item.from_ask]:[];const u=new Set(i.map(w=>w.key));await Bt(e,[...(await Ce(e)).filter(w=>!u.has(w.key)),...i.map(({key:w,file:f,archived_at:h,closed_at:m,item:b,questions:_,asks:N})=>({key:w,file:f,archived_at:h,closed_at:m,item:b,questions:_,asks:N}))]),i.some(w=>w.asks.length)&&await y(Se(e),{links:{...l,...Object.fromEntries(i.flatMap(w=>w.asks.map(f=>[f,w.key])))}});for(const[w,f]of o)await y(w,f);const p=(s.items||[]).filter(w=>!(c.has(w.topic)&&w.resolved));return p.length!==(s.items||[]).length&&await y(E(e),{...s,items:p}),[...u]}),"archiveCards");function ec(e,t){if(!_e(e))return e;const n=Ne(e).map(o=>{const s=t.find(c=>c.state&&c.number===o.number&&(!o.repo||o.repo===c.repo));return s?{...o,kept:{checks:s.checks,reviews:s.reviews,codexOpen:s.codexOpen}}:o}),{pr:a,...i}=e;return{...i,prs:n}}r(ec,"keptPRs");async function tc(e,t){return!e||!we(ln(e))?!1:Lt(e,async()=>{const n=await Ce(e),a=n.find(w=>w.key===t);if(!a)return!1;const i=(await q(e)).find(w=>w.slug===a.file),o=i&&await $(i.file,null);if(!Array.isArray(o?.items))return!1;o.items.some(w=>w.id===a.item.id)||await y(i.file,{...o,items:[...o.items,a.item]});const s=await $(E(e),{items:[]}),c=new Set((s.items||[]).map(w=>w.id)),l=(a.questions||[]).filter(w=>!c.has(w.id));l.length&&await y(E(e),{...s,items:[...s.items||[],...l]});const u=await ve(e),p=(a.asks||[]).filter(w=>ie(u,w)===t);return p.length&&await y(Se(e),{links:Object.fromEntries(Object.entries(u).filter(([w])=>!p.includes(w)))}),await Bt(e,n.filter(w=>w!==a)),!0})}r(tc,"restoreArchived");const lo=["push_tag","no_change"],uo=r(e=>Object.fromEntries(Object.entries(e).filter(([t])=>!lo.includes(t))),"stripQuestion"),fo=r(({summary:e,...t})=>t,"stripItem"),wo=r(e=>lo.some(t=>t in e),"holdsDead"),Nd=r(e=>Lt(e,async()=>{for(const a of await q(e)){const i=await $(a.file,null);Array.isArray(i?.items)&&i.items.some(o=>"summary"in o)&&await y(a.file,{...i,items:i.items.map(fo)})}const t=await $(E(e),null);(t?.items||[]).some(wo)&&await y(E(e),{...t,items:t.items.map(uo)});const n=await Ce(e);n.some(a=>"card"in a||"summary"in a.item||(a.questions||[]).some(wo))&&await Bt(e,n.map(({card:a,...i})=>({...i,...a?{closed_at:i.closed_at||a.sortAt}:{},item:fo(i.item),questions:(i.questions||[]).map(uo)})))}),"stripDeadFields");async function Rd(){return es(async()=>{const e=Xo(),t=Array.isArray(e.migrated)?e.migrated:[];if(t.includes("wave-names")||!Object.keys(e.sites||{}).length)return[];const n=[];for(const{route:a,dir:i}of be(e))await nc(i),n.push(a);return e.migrated=[...t,"wave-names"],await ts(e),n})}r(Rd,"migrateWaveNames");const po=r(async e=>JSON.parse(await j.readFile(e,"utf8")),"readPlain"),da=r((e,t)=>He(e,JSON.stringify(t,null,2)+`
30
+ `),"writePlain"),mo=r(e=>typeof e!="string"?e:e.replace(/^wave:([a-z0-9-]+)$/,"file:$1").replace(/^lineup:([a-z0-9-]+)$/,"wave:$1"),"renamedTopic"),ua=r(e=>Array.isArray(e)?e.map(ua):e&&typeof e=="object"?Object.fromEntries(Object.entries(e).map(([t,n])=>[mo(t),ua(n)])):mo(e),"renameTopics");async function nc(e){const t=g.join(e,"board.json"),n=await po(t).catch(()=>null);if(n&&typeof n.items=="string"&&n.items!=="waves"&&g.basename(n.items)==="wave.json"){const i=g.join(g.dirname(n.items),"items.json");we(g.resolve(e,n.items))&&await j.rename(g.resolve(e,n.items),g.resolve(e,i)),await da(t,{...n,items:i,slug:n.slug||"wave"})}for(const i of await q(e)){const o=await po(i.file).catch(()=>null);if(!Array.isArray(o?.lineups))continue;const{lineups:s,...c}=o;await da(i.file,{...c,waves:[...c.waves||[],...s]});for(const{slug:l}of s)await j.rename(g.join(i.dir,`lineup-${l}.md`),g.join(i.dir,fa(l))).catch(u=>{if(u.code!=="ENOENT")throw u})}const a=g.join(e,"data");for(const i of(await j.readdir(a).catch(()=>[])).filter(o=>o.endsWith(".json"))){const o=g.join(a,i),s=await j.readFile(o,"utf8");let c;try{c=JSON.parse(s)}catch{continue}c=ua(c),i==="done-archive.json"&&Array.isArray(c.cards)&&(c.cards=c.cards.map(({wave:l,...u})=>l===void 0?u:{...u,file:l})),i==="arch-reviews.json"&&Array.isArray(c.reviews)&&(c.reviews=c.reviews.map(({wave:l,lineup:u,...p})=>u?{...p,wave:l}:{...p,file:l})),JSON.stringify(c)!==JSON.stringify(JSON.parse(s))&&await da(o,c)}}r(nc,"renameWaveNames");const fa=r(e=>`wave-${e}.md`,"waveNoteFile"),ac=r(e=>typeof e=="string"&&/^[a-z0-9-]+$/.test(e)?e:d(`a wave's slug uses only a-z, 0-9 and "-", not "${e}"`),"waveSlug");async function ht(e,t){for(const n of await q(e)){const a=await $(n.file,null),i=(a?.waves||[]).find(o=>o.slug===t);if(i)return{source:n,itemFile:a,wave:i}}return d(`this board has no wave "${t}"; wave create makes one`)}r(ht,"findWave");async function ic(e,t){(await se(e)).items!=="waves"&&d(`topic file:${t} is for a board over ~/.claude/waves; a wave's triage uses wave:<slug>`),await Je(e,t)}r(ic,"triagedFile");function wa(e,t,n,a){return n.map(i=>{const{file:o,id:s}=F(i);o!==e.slug&&d(`${i} is not in item file "${e.slug}"; a wave's cards share one file`),((t.items.find(u=>u.id===s)||d(`item file "${o}" has no item "${s}"`)).deps||[]).includes(`wave:${a}`)&&d(`${i} waits on the wave "${a}", so it can't be in it`);const l=(t.waves||[]).find(u=>u.slug!==a&&!u.finished_at&&qe(u).includes(s));return l&&d(`${i} is already in the wave "${l.slug}"; wave remove ${l.slug} ${i} first`),s})}r(wa,"waveCards");const pa=r(e=>[...new Set(String(e??"").split(",").map(t=>t.trim()).filter(Boolean))],"cardList");function oc(e){const t=[e??[]].flat().map(n=>{const[a,i]=String(n).split(":");return[a.trim(),pa(i)]});return(!t.length||t.some(([n])=>!n))&&d("--card takes one card per flag in build order, as <file>/<id>, or <file>/<id>:<file>/<id>,\u2026 naming the earlier cards it conflicts with"),t}r(oc,"cardSpecs");function ma(e,t,n,a,i){const o=n.items.indexOf(a),s=i.map(u=>{const p=J(e.slug,u),{file:w,id:f}=F(p),h=w===e.slug?n.items.indexOf(f):-1;return(h<0||h>=o)&&d(`${p} is not an earlier card of the wave "${n.slug}"`),f}),c=t.items.find(u=>u.id===a),l=[...(c.deps||[]).filter(u=>!ho(e,n,u)),...s];l.length?c.deps=l:delete c.deps}r(ma,"relink");const ho=r((e,t,n)=>qe(t).some(a=>J(e.slug,n)===`${e.slug}/${a}`),"inWave");async function go(e,t,n,a,i){i!==void 0&&await He(g.join(t.dir,fa(a.slug)),`${S(i,"the note").replace(/\n*$/,`
31
+ `)}`),await y(t.file,n);const{tickets:o}=je(await se(e));return{file:t.file,lines:[`order: ${$o(t,n,a,o)}`]}}r(go,"writeWave");function $o(e,t,n,a){const i=r(o=>at(t,t.items.find(s=>s.id===o)||{id:o},a),"label");return qe(n).map(o=>{const s=(t.items.find(c=>c.id===o)?.deps||[]).filter(c=>ho(e,n,c));return s.length?`${i(o)} (after ${s.map(c=>i(F(J(e.slug,c)).id)).join(", ")})`:i(o)}).join(", ")}r($o,"orderLine");async function Ed(e,t,{title:n,goal:a,cards:i,note:o}){ac(t);const s=oc(i);o===void 0&&d("wave create needs the wave note, as --note <text> or --file <path>");const c=await q(e);c.some(f=>f.slug===t)&&d(`"${t}" is an item file on this board; pick another slug`);for(const f of c)((await $(f.file,null))?.waves||[]).some(h=>h.slug===t)&&d(`this board already has a wave "${t}"`);const l=c.find(f=>f.slug===F(s[0][0]).file)||d(`this board has no item file "${F(s[0][0]).file}"`),{itemFile:u}=await Je(e,l.slug),p=wa(l,u,s.map(([f])=>f),t);new Set(p).size!==p.length&&d("--card names a card twice");const w={slug:t,title:S(n,"--title"),goal:S(a,"--goal"),items:p,created:new Date().toISOString()};return s.forEach(([,f],h)=>ma(l,u,w,p[h],f)),u.waves=[...u.waves||[],w],go(e,l,u,w,o)}r(Ed,"createWave");async function Ld(e,t,{add:n,remove:a,card:i,after:o,title:s,goal:c,note:l}){[n,a,i,o,s,c,l].every(m=>m===void 0)&&d("nothing to change: pass a card with --after, --title, --goal, --note or --file"),o!==void 0&&n===void 0&&i===void 0&&d(`--after goes with a card: wave set ${t} <file>/<id> --after <file>/<id>,\u2026`),i!==void 0&&o===void 0&&d(`wave set ${t} ${i} needs --after <file>/<id>,\u2026 ("" clears its links)`);const{source:u,itemFile:p,wave:w}=await ht(e,t);w.finished_at&&d(`the wave "${t}" is finished`),w.items=qe(w),delete w.phases;const f=a===void 0?[]:await sc(e,t);if(n!==void 0){const[m]=wa(u,p,[n],t);w.items.includes(m)&&d(`${n} is already in the wave "${t}"`),w.items.push(m),ma(u,p,w,m,pa(o))}if(i!==void 0){const{file:m,id:b}=F(i);(m!==u.slug||!w.items.includes(b))&&d(`${i} is not in the wave "${t}"`),ma(u,p,w,b,pa(o))}if(a!==void 0){const[m]=wa(u,p,[a],t);w.items.includes(m)||d(`${a} is not in the wave "${t}"`),w.items.length===1&&d(`${a} is the wave's last card; wave finish ${t} ends the wave`);const b=r(v=>!Q(v)&&(v.deps||[]).some(O=>J(u.slug,O)===`${u.slug}/${m}`),"waiting"),_=p.items.filter(v=>w.items.includes(v.id)&&v.id!==m&&b(v)),N=p.items.find(v=>v.id===m);if(_.length&&!(N&&Q(N))){const v=await Promise.all(_.map(async O=>`${await H(e,`${u.slug}/${O.id}`)} waits on ${a}: wave set ${t} ${u.slug}/${O.id} --after <its other conflicts, or "">`));d(`${a} can't leave the wave "${t}" while open cards wait on it; relink each, then remove:
32
32
  ${v.join(`
33
- `)}`)}if(N&&K(N))for(const v of p.items.filter(O=>w.items.includes(O.id)&&O.deps))v.deps=v.deps.filter(O=>J(u.slug,O)!==`${u.slug}/${m}`),v.deps.length||delete v.deps;w.items=w.items.filter(v=>v!==m)}s!==void 0&&(w.title=S(s,"--title")),c!==void 0&&(w.goal=S(c,"--goal"));const h=await wo(e,u,p,w,l);return a!==void 0&&await nc(e,t,f,a),h}r(Dd,"changeWave");async function tc(e,t){const n=[];for(const a of await q(e)){const i=await $(a.file,null);for(const o of Array.isArray(i?.items)?i.items:[])K(o)||!(o.deps||[]).includes(`wave:${t}`)||(await Ee(e,a.slug,o)).length&&n.push(`${a.slug}/${o.id}`)}return n}r(tc,"waveBlocked");async function nc(e,t,n,a){const i=`${await H(e,a)} left the wave "${t}"`;for(const o of n){const{item:s}=await x(e,o);if(s.blocked_until||(await Ee(e,F(o).file,s)).length)continue;const{line:c}=await W(e,o,"unblocked",{lifted:i});await oa(e,o,c.ts),await he({dir:e},"card-unblocked",{key:o})}}r(nc,"noticeWaveUnblocked");async function Fd(e,t){const{source:n,itemFile:a,wave:i}=await pt(e,t),o=a.items.filter(s=>Ie(i).includes(s.id)&&!K(s));if(o.length){const s=await Promise.all(o.map(c=>H(e,`${n.slug}/${c.id}`)));d(`the wave "${t}" still has ${s.join(", ")} open; wave finish waits until every card in it is Done`)}return i.finished_at=new Date().toISOString(),await y(n.file,a),{file:n.file}}r(Fd,"finishWave");async function Nd(e,t,n){const{source:a,itemFile:i,wave:o}=await pt(e,t),s=await j.readFile(g.join(a.dir,ua(t)),"utf8").catch(()=>"");return{title:o.title,markdown:`**Goal** ${o.goal}
33
+ `)}`)}if(N&&Q(N))for(const v of p.items.filter(O=>w.items.includes(O.id)&&O.deps))v.deps=v.deps.filter(O=>J(u.slug,O)!==`${u.slug}/${m}`),v.deps.length||delete v.deps;w.items=w.items.filter(v=>v!==m)}s!==void 0&&(w.title=S(s,"--title")),c!==void 0&&(w.goal=S(c,"--goal"));const h=await go(e,u,p,w,l);return a!==void 0&&await rc(e,t,f,a),h}r(Ld,"changeWave");async function sc(e,t){const n=[];for(const a of await q(e)){const i=await $(a.file,null);for(const o of Array.isArray(i?.items)?i.items:[])Q(o)||!(o.deps||[]).includes(`wave:${t}`)||(await Be(e,a.slug,o)).length&&n.push(`${a.slug}/${o.id}`)}return n}r(sc,"waveBlocked");async function rc(e,t,n,a){const i=`${await H(e,a)} left the wave "${t}"`;for(const o of n){const{item:s}=await x(e,o);if(s.blocked_until||(await Be(e,F(o).file,s)).length)continue;const{line:c}=await M(e,o,"unblocked",{lifted:i});await sa(e,o,c.ts),await ke({dir:e},"card-unblocked",{key:o})}}r(rc,"noticeWaveUnblocked");async function Bd(e,t){const{source:n,itemFile:a,wave:i}=await ht(e,t),o=a.items.filter(s=>qe(i).includes(s.id)&&!Q(s));if(o.length){const s=await Promise.all(o.map(c=>H(e,`${n.slug}/${c.id}`)));d(`the wave "${t}" still has ${s.join(", ")} open; wave finish waits until every card in it is Done`)}return i.finished_at=new Date().toISOString(),await y(n.file,a),{file:n.file}}r(Bd,"finishWave");async function Cd(e,t,n){const{source:a,itemFile:i,wave:o}=await ht(e,t),s=await j.readFile(g.join(a.dir,fa(t)),"utf8").catch(()=>"");return{title:o.title,markdown:`**Goal** ${o.goal}
34
34
 
35
- **Order** ${po(a,i,o,n)}
35
+ **Order** ${$o(a,i,o,n)}
36
36
 
37
- ${s}`}}r(Nd,"readWaveNotes");async function U(e,t,n,a="main"){const{id:i}=F(t);/^[\w-]{1,40}$/.test(a)||d(`--agent takes a name of letters, digits, "_" and "-", not "${a}"`);const o=a==="main"?`${i}.json`:`${i}@${a}.json`;if(n)return g.join(g.resolve(n),"agents",o);const{source:s}=await x(e,t);return g.join(s.dir,"agents",o)}r(U,"builderFile");const mo=r((e,t)=>typeof e=="string"&&/^\d{4}-\d\d-\d\dT/.test(e)&&!Number.isNaN(Date.parse(e))?new Date(e).toISOString():d(`${t} takes an ISO time such as 2026-10-23T16:30:00-07:00`),"isoTime"),ac=_n.progress,ho=80,go="reading, building, testing, checking or landing";function $o(e,t,n,a){e||d(`${a} needs --site, since the line goes to the item's thread`),t!==void 0&&!ac.includes(t)&&d(`--phase takes ${go}, not "${t}"`);const i=S(n,a);return i.length>ho&&d(`${a} is ${i.length} characters; the cap is ${ho}. Say it shorter.`),{phase:t,detail:i}}r($o,"progressLine");const ko=r((e,t,n)=>t?{detail:e.detail}:(e.phase===void 0&&d(`${n} needs --phase ${go}`),e),"forAgent");async function ma(e,t,n="main",a,{phase:i,detail:o},s){const c=qe(a),l=c||n==="main"?"":a.kind===re?n:Mt(a)||a.kind||n,u=c?Jo[a.kind]:[l,i].filter(Boolean).join(" "),p=new RegExp(`^(?:${u}|${i??u})\\s*:?\\s+`,"i"),w=u?`${u[0].toUpperCase()}${u.slice(1)}: ${o.replace(p,"")}`:o,f=Z(e,t);(await z(f)).filter(h=>h.kind==="status").pop()?.text!==w&&await le(f,{ts:s,who:"claude",kind:"status",run:a.started,...i?{phase:i}:{},...c?{planner:!0}:{},text:w})}r(ma,"statusToThread");const ic={prototype:"sonnet",grill:"opus",[me]:"sonnet"};async function Et(e,t,{label:n,phase:a,progress:i,dir:o,agent:s="main",kind:c,model:l,eta:u,worktree:p,ownWorktree:w,dispatch:f}){a!==void 0&&i===void 0&&d('--phase goes with --progress "<detail>"');const h=ee(i,ne=>$o(e,a,ne,"--progress")),m=await U(e,t,o,s),k=new Date().toISOString(),_=ee(c,ne=>/^[\w.-]{1,30}$/.test(ne)?ne:d("--kind takes a short word such as claude or codex")),N=_t(_),v=l??(_==="opus"||_==="sonnet"?_:void 0),O=["build",...wc].includes(N),A=await $(m,null),I=r(ne=>["tap","hub"].includes(ne?.dispatched)&&!ne.done,"tapOpen");if(f==="session"&&I(A))return{file:m,alreadyOpen:!0,lines:[]};s!=="main"&&_!==me&&_!==re&&I(await $(await U(e,t,o),null))&&d(`the Implement tap opened ${t}'s main builder record and no agent has taken it up; run builder start ${t} without --agent first`);const B=A?.dispatched&&!A.done?A:null,P=O&&!o?(await x(e,t)).item:void 0,L=A&&Ve(A).length?A:null,T=B&&N==="build"?{kind:_t(B.kind),model:B.model,reason:{tap:"The owner's Build tap picked it.",hub:"The board's pull picked it."}[B.dispatched]??"The session's dispatch picked it."}:O?An({card:P??{}}):null;T&&N!=="build"&&N!==T.kind&&d(`--kind ${N}: ${T.kind} builds now. ${T.reason} Pass --kind ${T.kind}, or --kind build.`);const M=T?T.kind:N,C=Fa({dir:e});if(!(B&&!f)&&es(C,M))throw new ue(Xo(C,M));const Y=h&&ko(h,qe({kind:M}),"--progress");l!==void 0&&!["opus","sonnet"].includes(l)&&d("--model takes opus or sonnet"),v!==void 0&&M==="codex"&&d("--model is for kind claude; codex needs none");const te=M==="claude"?Pn():"auto";v!==void 0&&te!=="auto"&&v!==te&&d(`--model ${v}: the hub's Claude model is ${te==="opus"?"Opus":"Sonnet"}. Pass --model ${te}, or no --model.`);const V=M==="claude"?v??T?.model??(B&&_t(B.kind)==="claude"?B.model:void 0)??"opus":qe({kind:M})?v??(B?.kind===M?B.model:void 0)??ic[M]:M===re?v??A?.model:void 0,Q={label:n===void 0?"Builder":S(n,"--label"),...M?{kind:M}:{},...V?{model:V}:{},started:k,...u===void 0?{}:{eta:mo(u,"--eta")},...f?{dispatched:f}:{},...L?{calls:L.calls,...L.verified?{verified:L.verified}:{}}:{},...dc(A)?{session:A.session}:{},...M===re&&A?.kind===re?{label:A.label,task:A.task,writes:A.writes,requested:A.requested}:{}},Ze=O&&!o&&!K(P)&&P.lane!=="progress",un=Ze?await $e(e,t,{start:!0}):{lines:[]};let ke,je=null;const Bt=w&&O&&s==="main"?[A?.worktree,P?.worktree].find(ne=>ne?.path&&de(ne.path))??null:null,mt=(B??L)?.worktree??Bt,ht=p??(O&&s==="main"&&!mt?w:void 0);try{if(ht){const ne=await G(g.dirname(g.dirname(m)),F(t).id),fn=(await Promise.all(ne.filter(pe=>pe!==m).map(pe=>$(pe,null)))).filter(pe=>pe?.worktree?.path&&!pe.done).map(pe=>pe.worktree.path);({remade:je,...ke}=await ht({busy:fn}))}else ke=mt}catch(ne){throw Ze&&await fc(e,t,P),ne}await oc(m,A),await y(m,ke?{...Q,worktree:ke}:Q),P?.rebase_run&&s==="main"&&![bo,Aa].includes(Q.label)&&await yo(e,t),T?.inputs&&e&&ps(Ra(e),{call:"builder-pick",key:t,result:[M,M==="claude"?V:null].filter(Boolean).join(" "),reason:T.reason,inputs:{...T.inputs,...v?{askedModel:v}:{},...f?{dispatch:f}:{}}}),je&&e&&await W(e,t,"worktree-remade"),Y&&await ma(e,t,s,Q,Y,k);const ie=_===me||_===re?[]:await en(e,t,"Acted on: builder started");return e&&O&&await vr(e,t),{file:m,kind:M??null,model:V??null,reason:T?.reason??null,worktree:ke?.path??null,lines:[...N==="build"?[`kind: ${M}. ${T.reason}`]:[],...Mt(Q)?[`model: ${Mt(Q)}`]:[],...e?[`ref: ${await H(e,t)}`]:[],...ke?[`worktree: ${ke.path}`]:[],...je?[je]:[],...un.lines,...ie]}}r(Et,"startBuilder");async function oc(e,t){if(!t?.started||!t.done||!e.endsWith(".json")||g.basename(e).includes("@"))return;const{label:n,kind:a,model:i,started:o,done:s,session:c}=t,l=g.join(g.dirname(e),`${g.basename(e,".json")}@past-${Date.parse(o)}.json`);await y(l,{label:n,...a?{kind:a}:{},...i?{model:i}:{},started:o,done:s,...c?.log?{session:{log:c.log}}:{}})}r(oc,"keepPastRun");async function Rd(e,t,n,a="tap"){try{const{held:i,blockers:o}=await rc(e,t);if(i&&await Vi(e,"card-hold",{key:t,hold:!1}),o.length)return await cc(e,t,o);try{const s=await Et(e,t,{kind:"build",worktree:n,dispatch:a});return{key:t,ref:await H(e,t),kind:s.kind,...s.model?{model:s.model}:{},reason:s.reason,worktree:s.worktree}}catch(s){throw i&&await uc(e,t,i),s}}catch(i){if(!(i instanceof ue))throw i;return await W(e,t,Oa,{why:i.message}),null}}r(Rd,"startTappedBuild");async function Ed(e,t,{pr:n,conflict:a,base:i},o){const{source:s,itemFile:c,item:l}=await x(e,t);l.rebase_run={pr:n,base:i,conflict:a,at:new Date().toISOString()},await y(s.file,c);try{const u=await Et(e,t,{kind:"build",label:bo,dispatch:"hub",ownWorktree:o});return{key:t,kind:u.kind,...u.model?{model:u.model}:{},worktree:u.worktree}}catch(u){throw await yo(e,t),u}}r(Ed,"startRebase");const bo="Rebase";async function yo(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);delete i.rebase_run,await y(n.file,a)}r(yo,"endRebaseRun");async function Ld(e,t){const{itemFile:n,item:a}=await x(e,t),i=se(e,n);return Me(a,i).find(o=>o.repo===i)?.number??null}r(Ld,"cardPR");async function Bd(e,t,{from:n,to:a,replayed:i}){const{source:o,itemFile:s,item:c}=await x(e,t);return c.rebase_run?(c.rebased=[...c.rebased||[],{from:n,to:a,base:c.rebase_run.base,replayed:i,at:new Date().toISOString()}],delete c.rebase_run,await y(o.file,s),!0):!1}r(Bd,"recordRebase");const sc=r((e,t,n)=>{const a=new Map((e.rebased||[]).filter(i=>i.replayed).map(i=>[i.to,i.from]));for(let i=n,o=0;i&&o<=a.size;i=a.get(i),o++)if(i===t)return!0;return!1},"onlyRebased");async function rc(e,t){const n=Fa({dir:e});ts(n)&&d(Yo(n));const{item:a}=await x(e,t),i=a.held?{held:a.held,pick_up_at:a.pick_up_at,held_at:a.held_at}:null;return i&&we(a)&&d("it's held. Unhold it, then tap Implement."),await ia(e,t)&&d("no take is picked on its prototypes, so it prototypes again. Pick a take from the next round, then tap Implement."),{held:i,blockers:await Fr(e,t,a)}}r(rc,"tapRefusals");async function Cd(e,t,n){await Je(e,t,{who:"love",by:n,command:"implement"}),await yr(e,t,{})}r(Cd,"implementAsIs");async function cc(e,t,n){const a=n.join(", ");return await he({dir:e},"card-bump",{key:t}),await W(e,t,"cant-start-yet",{names:a}),{key:t,ref:await H(e,t),bumped:n,line:`bumped: ${t} waits on ${a}, so it stays in Todo, bumped to the top`}}r(cc,"bumpBlocked");async function ln(e,t){const n=await U(e,t),a=await $(n,null);return a?.started&&!a.done&&et(a)?{file:n,record:a}:null}r(ln,"openBuild");async function xo(e,t,{cause:n,words:a,by:i,until:o}){const s=await ln(e,t);return!s||s.record.pause?!1:(await y(s.file,{...s.record,pause:{cause:n,words:a,at:new Date().toISOString(),...i?{by:i}:{},...o?{until:o}:{}}}),!0)}r(xo,"requestPause");async function lc(e,t){const n=await ln(e,t),a=n?.record.pause;if(!a||a.paused)return a??null;const i=new Date().toISOString(),o=(await z(Z(e,t))).filter(c=>c.kind==="status"&&c.phase&&!c.planner&&String(c.ts)>=String(n.record.started)).pop()?.phase,s={...a,paused:i,...o?{phase:o}:{}};return await y(n.file,{...n.record,pause:s}),await W(e,t,"paused",{phase:o,words:a.words,clear:Da[a.cause][0]}),s}r(lc,"markPaused");async function dn(e,t,{dir:n,agent:a="main"}){if(!e||n||a!=="main")return[];const i=await lc(e,t);return i?[`Paused: ${i.words}. Commit what you have now with \`git -c core.commentChar=';' commit -F <file>\`, then stop: end your turn and run nothing else. The board resumes this session, telling it so, once the pause clears.`]:[]}r(dn,"pauseReply");async function Id(e,t){const n=await ln(e,t),a=n?.record.pause;if(!a?.paused)return null;const{pause:i,...o}=n.record;return await y(n.file,o),await W(e,t,"resumed",{clear:Da[a.cause][1]}),{pause:a,record:o}}r(Id,"resumePaused");async function Td(e,t){const n=await ln(e,t);if(!n?.record.pause||n.record.pause.paused)return;const{pause:a,...i}=n.record;await y(n.file,i)}r(Td,"dropPause");async function qd(e,t,n){const a=await U(e,t),i=await $(a,null),o=new Date().toISOString();i&&!i.done&&await y(a,{...i,done:o});const{source:s,itemFile:c,item:l}=await x(e,t);l.lane==="progress"&&Ft(l,"todo",o),await y(s.file,c),await W(e,t,Oa,{why:n})}r(qd,"abandonLaunch");async function Md(e,t,[n,a],i="main"){const o=await U(e,t,void 0,i),s=await $(o,null);s&&!s.done&&await y(o,{...s,done:new Date().toISOString()}),await ut(e,t),await W(e,t,n,a)}r(Md,"abandonPlanner");async function Wd(e,t,n,a,i="main",o=null){const s=await U(e,t,void 0,i),c=await $(s,null);await y(s,{...c,session:{id:n,pid:a,log:o,at:new Date().toISOString()}})}r(Wd,"recordSession");async function Ud(e,t){const{source:n}=await x(e,t);return(await Promise.all((await G(n.dir,F(t).id)).map(i=>$(i,null)))).filter(i=>i&&i.started&&!i.done).map(i=>({pid:i.session?.pid??null,at:i.session?.at??null}))}r(Ud,"openAgents");async function Hd(e,t){const{source:n}=await x(e,t);return(await Promise.all((await G(n.dir,F(t).id)).map(i=>$(i,null)))).filter(i=>i&&i.started&&!i.done).map(i=>({kind:i.kind??null,pid:i.session?.pid??null,at:i.session?.at??null}))}r(Hd,"openRuns");async function Jd(e,t,n){const{source:a}=await x(e,t),i=new Date().toISOString();for(const o of await G(a.dir,F(t).id)){const s=await $(o,null);s?.started&&!s.done&&et(s)&&await y(o,{...s,stopped:{run:n,at:i}})}}r(Jd,"markStopped");async function zd(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);i.merge_ready&&(i.merge_ready.stopped=new Date().toISOString(),await y(n.file,a))}r(zd,"stopHeldMerge");async function Kd(e,t,n){const a=S(n,"item stall --reason"),{source:i,itemFile:o,item:s}=await x(e,t);return s.lane==="done"&&d(`${await H(e,t)} is done; there is nothing to stall`),s.stalled={reason:a,at:new Date().toISOString()},await y(i.file,o),a}r(Kd,"markStalled");async function Qd(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);i.stalled&&(delete i.stalled,await y(n.file,a))}r(Qd,"clearStall");async function Gd(e,t){const{item:n}=await x(e,t),a=await z(g.join(e,"data","events.jsonl")),i=r(c=>a.filter(l=>l.kind===c&&l.data?.key===t).map(l=>String(l.ts)).sort().pop()||"","latest"),o=i("card-bump");if(!o||i("card-unbump")>o)return null;const s=[n.closed_at,...De(n).map(c=>c.merged_at)].filter(Boolean).map(String).sort().pop()||"";return o>s?o:null}r(Gd,"landFirst");async function Vd(e,t,n){const{item:a}=await x(e,t);K(a)&&d(`${await H(e,t)} is done; there is nothing to bump`);const i=await he({dir:e},n?"card-bump":"card-unbump",{key:t});return(await z(g.join(e,"data","events.jsonl"))).find(o=>o.id===i).ts}r(Vd,"bumpCard");async function _o(e,t){return(await z(Z(e,t))).filter(n=>n.kind==="status").pop()?.phase==="landing"}r(_o,"isLanding");const dc=r(e=>!!e?.session&&e.session.at>(e.done??e.started),"launchedSession");async function Zd(e,t,n){const a=await $(await U(e,t),null);if(a&&!a.done)return null;const i=await Et(e,t,{kind:n,dispatch:"hub"});return{key:t,kind:n,model:i.model}}r(Zd,"startPlanner");async function Yd(e,t,n){const{source:a}=await x(e,t);if((await Promise.all((await G(a.dir,F(t).id)).map(c=>$(c,null)))).some(c=>c?.kind===me&&c.started&&!c.done))return null;const o=Qo(n);if(await $(await U(e,t,void 0,o),null))return null;const s=await Et(e,t,{kind:me,agent:o,dispatch:"hub"});return{key:t,kind:me,model:s.model,agent:o}}r(Yd,"startVisual");const ha=r(e=>typeof e=="string"&&/^[a-z][a-z0-9-]{0,29}$/.test(e)&&e!=="main"&&!e.startsWith(`${me}-`)?e:d('--name takes a short lowercase name such as tests or docs-pass: letters, digits and "-", not main or visual-\u2026'),"helperName");async function ga(e,t){const{item:n}=await x(e,t);return[(await $(await U(e,t),null))?.worktree,n.worktree].find(i=>i?.path&&de(i.path))??null}r(ga,"cardWorktree");async function Xd(e,t,{name:n,task:a,writes:i,model:o}){D(e);const s=ha(n),c=S(a,"--task");o!==void 0&&!["opus","sonnet"].includes(o)&&d("--model takes opus or sonnet");const l=Pn();o!==void 0&&l!=="auto"&&o!==l&&d(`--model ${o}: the hub's Claude model is ${l==="opus"?"Opus":"Sonnet"}. Pass --model ${l}, or no --model.`);const{item:u}=await x(e,t);K(u)&&d(`${await H(e,t)} is Done; a helper needs an open card`);const p=await U(e,t,void 0,s);return de(p)&&d(`${t} already has an agent named ${s}; pick another --name`),i&&!await ga(e,t)&&d(`a helper that writes branches from ${t}'s worktree, and the card has none; start its build first, or drop --writes`),await y(p,{label:s,kind:re,task:c,writes:!!i,...o?{model:o}:{},requested:new Date().toISOString()}),await W(e,t,"helper-asked",{agent:s,writes:i}),{lines:[`helper ${s} requested: the board launches it ${i?"once an implementer slot is free":"now"}`,`wait for it with agent wait ${t} --name ${s}`]}}r(Xd,"requestHelper");async function eu(e,t,n,a){const i=await U(e,t,void 0,n),o=await $(i,null);if(o?.kind!==re||o.started||o.done)return null;const{item:s}=await x(e,t),c=An({card:s}),l=Pn(),u=o.model??(l!=="auto"?l:c.kind==="claude"?c.model:"opus"),p=o.writes?await ga(e,t):null;o.writes&&!p&&d(`${t} has no worktree for helper ${n} to branch from`),await y(i,{...o,model:u});const w=await Et(e,t,{agent:n,kind:re,dispatch:"hub",worktree:p?({busy:f})=>a({repo:p.repo,base:p.branch,busy:f}):void 0});return{key:t,kind:re,model:u,agent:n,task:o.task,writes:o.writes,worktree:w.worktree}}r(eu,"startHelper");async function tu(e,t,n){const a=await $(await U(e,t,void 0,ha(n)),null);return a?.kind!==re&&d(`${t} has no helper named ${n}`),{record:a,card:await ga(e,t)}}r(tu,"helperOf");async function nu(e,t,{name:n,agent:a="main",timeout:i=9,every:o=2e3,alive:s}){D(e);const c=Number(i);c>0||d("--timeout takes minutes, a number above 0");const l=await U(e,t,void 0,ha(n)),u=await U(e,t,void 0,a),p=r(async f=>{const h=await $(u,null);if(!h?.started||h.done)return;const{waits:m,...k}=h;await y(u,f?{...k,waits:f}:k)},"mark"),w=Date.now()+c*6e4;await p(n);try{for(;;){const f=await $(l,null);if(f?.kind!==re&&d(`${t} has no helper named ${n}`),f.done){const m=f.writes&&f.worktree?[`branch ${f.worktree.branch} in ${f.worktree.path}: merge it with agent merge ${t} --name ${n}`]:[];return{lines:[`helper ${n} done: ${f.note??"no hand-back"}`,...m]}}const h=f.session?.pid??null;h&&!s(h)&&d(`helper ${n}'s process exited without builder done; do its task yourself`),Date.now()>=w&&d(`helper ${n} is still ${f.started?"running":f.writes?"waiting for an implementer slot":"waiting to launch"} after ${c} minutes; run agent wait again`),await new Promise(m=>setTimeout(m,o))}}finally{await p(null)}}r(nu,"waitHelper");async function au(e,t,n="main"){return n==="main"?!1:(await $(await U(e,t,void 0,n),null))?.kind===re}r(au,"isHelper");async function iu(e,t,n="main"){const a=await $(await U(e,t,void 0,n),null);return!!(a?.started&&!a.done&&qe(a))}r(iu,"isPlanner");async function ou(e,t,{file:n,title:a}){D(e);const i=S(a,"--title");(typeof n!="string"||!n.endsWith(".html"))&&d("--file takes the page, an .html file");const o=await j.readFile(g.resolve(n),"utf8").catch(()=>d(`--file ${n} can't be read`));o.trim()||d(`--file ${n} is empty`),await x(e,t);const{id:s}=F(t),c=g.join(e,"public","visuals",s);await j.mkdir(c,{recursive:!0});const l=i.toLowerCase().replace(/[^a-z0-9]+/g,"-").slice(0,60).replace(/^-+|-+$/g,"")||"visual";let u=`${l}.html`;for(let w=2;de(g.join(c,u));w++)u=`${l}-${w}.html`;await j.writeFile(g.join(c,u),o,{flag:"wx"});const p=`visuals/${s}/${u}`;return await le(Z(e,t),{ts:new Date().toISOString(),who:"claude",kind:me,text:S(i,"--name"),link:En(p,"--link")}),{lines:[p]}}r(ou,"addVisual");async function su(e){const t=[];for(const n of await q(e)){const a=await $(n.file,null);for(const i of a?.items||[]){if(K(i))continue;const o=await $(g.join(n.dir,"agents",`${i.id}.json`),null);!o?.started||o.done||!o.dispatched||qe(o)||t.push({key:`${n.slug}/${i.id}`,kind:_t(o.kind),model:o.model})}}return t}r(su,"cutOffBuilds");async function uc(e,t,n){const{source:a,itemFile:i,item:o}=await x(e,t);for(const s of Object.keys(n))n[s]===void 0?delete o[s]:o[s]=n[s];await y(a.file,i)}r(uc,"restoreHold");async function fc(e,t,n){const{source:a,itemFile:i,item:o}=await x(e,t);for(const s of["lane","lane_at","closed_at","reopened_at"])n[s]===void 0?delete o[s]:o[s]=n[s];await y(a.file,i)}r(fc,"restoreLane");async function ru(e,t){return e||d("audit reads a board: pass --site <route>"),t!==void 0&&F(t),{lines:ms(hs(Ra(e),{key:t}))}}r(ru,"auditLog");const wc=["codex","claude"];async function cu(e,t){const{item:n}=await x(e,t),{kind:a,model:i,reason:o}=An({card:n});return{lines:[`${[a,i].filter(Boolean).join(" ")}: ${o}`]}}r(cu,"pickBuilder");async function So(e,t,{dir:n}={}){const{id:a}=F(t),i=g.dirname(await U(e,t,n)),o=await G(g.dirname(i),a);return(await Promise.all(o.map(c=>$(c,null)))).filter(c=>c?.worktree?.path).sort((c,l)=>String(l.started).localeCompare(String(c.started)))[0]?.worktree??null}r(So,"recordedWorktree");async function lu(e,t){const{source:n}=await x(e,t),i=(await Promise.all((await G(n.dir,F(t).id)).map(o=>$(o,null)))).filter(o=>o?.session?.id&&o.worktree?.path).sort((o,s)=>String(s.started).localeCompare(String(o.started)))[0];return i?{id:i.session.id,worktree:i.worktree.path}:null}r(lu,"recordedSession");async function du(e,t,n,{phase:a,dir:i,agent:o="main",eta:s,clearEta:c}){s!==void 0&&c&&d("pass --eta or --clear-eta, not both");const l=$o(e,a,n,"builder progress"),u=await U(e,t,i,o),p=await $(u,null);(!p?.started||p.done)&&d(`builder progress needs an open builder record on ${t}; run builder start first`);const w=ko(l,qe(p),"builder progress");return s!==void 0&&(p.eta=mo(s,"--eta")),c&&delete p.eta,(s!==void 0||c)&&await y(u,p),await ma(e,t,o,p,w,new Date().toISOString()),{file:u,lines:await dn(e,t,{dir:i,agent:o})}}r(du,"builderProgress");async function uu(e,t){const{source:n}=await x(e,t),a=Z(e,t);if((await z(a)).filter(s=>s.kind==="status").pop()?.phase==="landing")return;const o=(await Promise.all((await G(n.dir,F(t).id)).map(s=>$(s,null)))).filter(s=>s?.started&&et(s)&&s.label!==Aa).map(s=>s.started).sort().pop();await le(a,{ts:new Date().toISOString(),who:"claude",kind:"status",phase:"landing",text:"Landing: rebasing onto main and landing",...o?{run:o}:{}})}r(uu,"landingLine");const Ve=r(e=>(e?.calls||[]).filter(t=>!e.verified||t.at>e.verified.at),"unverifiedCalls");async function fu(e,t,n,{dir:a,agent:i="main"}){const o=S(n,"builder call");/\n/.test(o)&&d("builder call takes one line; record each call on its own");const s=await U(e,t,a,i),c=await $(s,null);return(!c?.started||c.done)&&d(`builder call needs an open builder record on ${t}; run builder start first`),await y(s,{...c,calls:[...c.calls||[],{at:new Date().toISOString(),text:o}]}),{file:s,lines:await dn(e,t,{dir:a,agent:i})}}r(fu,"recordCall");async function Lt(e,t){const n=await z(Z(e,t)),a=new Map(n.filter(i=>i.kind===Tt.answer).map(i=>[i.ask,i]));return n.filter(i=>i.kind===Tt.ask).map(i=>{const o=a.get(i.ask);return{id:i.ask,agent:i.agent||"main",text:i.text,at:i.ts,answer:o?{text:o.text,at:o.ts}:null,deferred:!!o?.deferred}})}r(Lt,"builderAsks");const pc=r(e=>ge().find(t=>Pe(t.dir,e))??{dir:e},"boardTarget");async function wu(e,t,n,{agent:a="main"}){D(e);const i=S(n,"builder ask");/\n/.test(i)&&d("builder ask takes one line; ask each question on its own");const o=await $(await U(e,t,void 0,a),null);(!o?.started||o.done)&&d(`builder ask needs an open builder record on ${t}; run builder start first`);const s=`ask${(await Lt(e,t)).length+1}`;return await le(Z(e,t),{ts:new Date().toISOString(),who:`${Mt(o)||o.kind||"Claude"} builder`,kind:Tt.ask,ask:s,agent:a,text:i}),await he(pc(e),"builder-ask",{key:t,ask:s,agent:a,text:i}),{id:s,lines:await dn(e,t,{agent:a})}}r(wu,"askOrchestrator");async function pu(e,t,n,a,{deferred:i=!1}={}){D(e);const o=S(a,"--text"),s=(await Lt(e,t)).find(l=>l.id===n)||d(`no ask "${n}" on ${t}; builder asks ${t} lists them`);s.answer&&d(`${n} on ${t} is already ${s.deferred?"deferred":"answered"}`);const c=new Date().toISOString();if(await le(Z(e,t),{ts:c,who:"claude",kind:Tt.answer,ask:n,...i?{deferred:!0}:{},text:o}),i){const l=await U(e,t,void 0,s.agent),u=await $(l,null);u&&await y(l,{...u,calls:[...u.calls||[],{at:c,text:`Question: ${s.text} (deferred to the owner)`}]})}return{lines:[]}}r(pu,"answerBuilder");async function mu(e,t,{wait:n=!1,timeout:a=9,every:i=2e3}={}){D(e);const o=Number(a);o>0||d("--timeout takes minutes, a number above 0");const s=Date.now()+o*6e4;for(;;){const c=await Lt(e,t);if(!c.length)return{lines:["no asks yet"]};const l=c.filter(w=>!w.answer),u=c.flatMap(w=>[`${w.id}. ${w.text}`,` ${w.answer?`${w.deferred?"deferred":"answered"}: ${w.answer.text}`:"waiting on the orchestrator"}`]);if(!n||!l.length)return{lines:u};const p=await dn(e,t,{});if(p.length)return{lines:[...u,...p]};Date.now()>=s&&d(`${l.map(w=>w.id).join(", ")} still waiting on the orchestrator after ${o} minutes; run builder asks ${t} --wait again`),await new Promise(w=>setTimeout(w,i))}}r(mu,"listAsks");async function mc(e,t,n){const a=t.topic.startsWith("item:")&&!t.topic.startsWith("item:ask:")?t.topic.slice(5):d(`${n}.ask goes on a card's question, topic item:<file>/<id>`),i=(await Lt(e,a)).find(o=>o.id===t.ask)||d(`${n}.ask names no ask "${t.ask}" on ${a}; builder asks ${a} lists them`);i.deferred||d(`${n}.ask names ${i.id}, which the orchestrator ${i.answer?"answered":"has not deferred"}; only a deferred ask goes to the owner`)}r(mc,"checkAskQuestion");async function hu(e,t,{dir:n,agent:a="main"}){const i=await $(await U(e,t,n,a),null),o=i?.calls||[];if(!o.length)return{lines:["no judgment calls recorded"]};const{verified:s}=i,c=Ve(i).length?"not verified yet":`verified ${s.at}${s.note?`: ${s.note}`:""}`;return{lines:[...o.map((l,u)=>`${u+1}. ${l.text}`),c]}}r(hu,"listCalls");async function gu(e,t,{dir:n,agent:a="main",note:i}){const o=await U(e,t,n,a),s=await $(o,null);if(Ve(s).length||d(`${t} has no unverified judgment calls${a==="main"?"":` from agent ${a}`}; builder calls ${t} lists them`),e){const c=new Set((await $(E(e),{items:[]})).items?.filter(u=>u.topic===`item:${t}`&&u.ask).map(u=>u.ask)),l=(await Lt(e,t)).find(u=>u.deferred&&u.agent===a&&!c.has(u.id));l&&d(`${l.id} on ${t} was deferred to the owner; add its question with "ask": "${l.id}" first`)}return await y(o,{...s,verified:{at:new Date().toISOString(),...i===void 0?{}:{note:S(i,"--note")}}}),o}r(gu,"verifyCalls");async function $u(e,t){const{source:n,item:a}=await x(e,t),i=r((s,c)=>[(s.options||[]).find(l=>l.id===c.choice)?.label,c.note].filter(Boolean).join(": ")||"answered","said"),o=(await fe(e,t)).asked.filter(s=>s.ask&&s.answers.length).map(s=>({ask:s.ask,question:s.question,answer:i(s,s.answers.at(-1))}));return{key:t,ref:await H(e,t),title:a.title||t,notes:a.plan?g.join(n.dir,a.plan):null,thread:Z(e,t),worktree:(await So(e,t))?.path??null,answers:o,links:await hc(e,t),rebasePR:a.rebase_run?.pr??null,ticketQuestion:!a.ticket&&!!await sn(e)}}r($u,"briefFacts");async function hc(e,t){const n=new Map;for(const c of await q(e))for(const l of(await $(c.file,null))?.items||[])n.set(`${c.slug}/${l.id}`,{entry:c,it:l});const a=F(t).file,{it:i}=n.get(t),o=[...n].filter(([,c])=>(c.it.related||[]).some(l=>J(c.entry.slug,l)===t)).map(([c])=>c),s=[...i.parent?[["parent",J(a,i.parent)]]:[],...(i.deps||[]).map(c=>["depends on",J(a,c)]),...[...new Set([...(i.related||[]).map(c=>J(a,c)),...o])].map(c=>["related",c])].filter(([,c])=>n.has(c));return Promise.all(s.map(async([c,l])=>{const{entry:u,it:p}=n.get(l);return{relation:c,ref:await H(e,l),title:p.title||l,lane:Ce[p.lane||"todo"],notes:p.plan?g.join(u.dir,p.plan):null,thread:Z(e,l)}}))}r(hc,"cardLinks");async function ku(e){const t=[];for(const n of await q(e)){const a=await $(n.file,null);for(const i of a?.items||[]){if(K(i))continue;const o=(await Promise.all((await G(n.dir,i.id)).map(u=>$(u,null)))).filter(Boolean);if(!Sn(o))continue;const s=o.map(u=>u.verified?.at).filter(Boolean).sort().at(-1);if(!s||o.some(u=>Ve(u).length))continue;const c=`${n.slug}/${i.id}`;if((await fe(e,c)).asked.some(u=>u.state==="open")||await tn(e,c,i))continue;const l=o.filter(u=>u.worktree?.path).sort((u,p)=>String(p.started).localeCompare(String(u.started)))[0];l&&de(l.worktree.path)&&t.push({key:c,verified:s,kind:_t(l.kind),model:l.model})}}return t}r(ku,"landable");async function bu(e,t){const{source:n,item:a}=await x(e,t);(await Promise.all((await G(n.dir,a.id)).map(c=>$(c,null)))).some(c=>c?.started&&!c.done)||d(`${t} has no open builder; only a card whose lander stopped has one to relaunch`),await $e(e,t,{handBack:!0,next:"A lander relaunches to land the verified build"});const o=g.join(e,"data","landers.json"),s=await $(o,{});delete s[t],await j.writeFile(o+".tmp",JSON.stringify(s)+`
38
- `),await j.rename(o+".tmp",o)}r(bu,"relaunchLander");async function yu(e,t,{dir:n}={}){const{id:a}=F(t),i=await G(g.dirname(g.dirname(await U(e,t,n))),a),o=[];for(const s of i){if(!Ve(await $(s,null)).length)continue;const c=g.basename(s,".json").slice(String(a).length+1)||"main";o.push(c==="main"?"":` --agent ${c}`)}o.length&&d(`land: ${t} has judgment calls nobody verified yet; check them with ${o.map(s=>`builder calls ${t}${s}`).join(", ")}, then run ${o.map(s=>`builder verified ${t}${s}`).join(", ")}`)}r(yu,"checkCallsVerified");async function xu(e,t,{dir:n,agent:a="main",next:i}){const o=await $(await U(e,t,n,a),null),s=o&&!o.done?Ve(o):[];if(!s.length)return null;i!==void 0&&d(`builder done sets ${t}'s Next itself while its record has judgment calls; run it without --next`);const c=s.some(u=>u.text.startsWith("Question:"))?"Answer the builder's questions":"Verify the builder's judgment calls",l=await $e(e,t,{handBack:!0,next:c});return{lines:[`handed back: Next "${c}"`,...l.lines]}}r(xu,"handBackCalls");async function _u(e,t,{agent:n="main"}){if(n!=="main")return!1;const a=await U(e,t,void 0,n),i=await $(a,null);if(!i?.started||i.done||!i.worktree?.path)return!1;const{id:o}=F(t);for(const s of await G(g.dirname(g.dirname(a)),o)){const c=s===a?null:await $(s,null);if(!c?.started||c.done||c.kind==="install")continue;const l=g.basename(s,".json").slice(String(o).length+1);d(`builder done would finish ${t} while agent ${l} is still open; run builder done ${t} --agent ${l} first`)}return!0}r(_u,"finishesCard");async function Su(e,t){const{source:n,item:a}=await x(e,t);if(K(a))return null;const i=(await Promise.all((await G(n.dir,a.id)).map(s=>$(s,null)))).filter(Boolean);if(!Sn(i)||i.some(s=>Ve(s).length||s.started&&!s.done&&s.kind!=="install"))return null;const o=await So(e,t);return!o||!de(o.path)?null:{waits:(await fe(e,t)).asked.filter(s=>s.state==="open").map(s=>s.id)}}r(Su,"verifiedFinish");async function vu(e,t){const{item:n}=await x(e,t);if(n.reopened_at&&d(`the owner reopened ${await H(e,t)}; it closes on the owner's Done`),n.lane!=="done"||De(n).some(o=>o.merged_at))return;const a=String(n.closed_at||"");(await z(g.join(e,"data","events.jsonl"))).some(o=>o.kind==="item-message"&&o.data?.key===t&&String(o.ts)>a)&&d(`the owner reopened ${await H(e,t)} with a message; it closes on the owner's Done`)}r(vu,"checkNotReopened");async function G(e,t){const n=g.join(e,"agents");return(await j.readdir(n).catch(()=>[])).filter(a=>a===`${t}.json`||a.startsWith(`${t}@`)&&!a.startsWith(`${t}@past-`)&&a.endsWith(".json")).map(a=>g.join(n,a))}r(G,"builderFiles");async function vo(e,t,n,{handedBack:a,endHandBack:i=!1,held:o=!1}={}){const s=new Date().toISOString();for(const c of await G(e,t)){const l=await $(c,null);if(!l)continue;if(l.done||l.kind==="install"&&!n){if(i&&l.handed_back){const{handed_back:f,...h}=l;await y(c,h)}continue}if(o&&l.pause)continue;const{eta:u,handed_back:p,...w}=l;await y(c,{...w,done:s,...a?{handed_back:a}:{},...o&&l.started?{hold_stopped:!0}:{}})}}r(vo,"closeBuilders");async function ju(e,t){const n=await U(e,t),a=await $(n,null);if(!a?.hold_stopped)return null;const{hold_stopped:i,...o}=a;return await y(n,o),{kind:a.kind??null,label:a.label??null}}r(ju,"takeHoldStopped");async function gc(e,t,{dir:n,agent:a,startedBetween:i,note:o,next:s,requireNext:c=!1}){const l=await U(e,t,n,a),u=await $(l,null);if(!u||u.done)return l;if(i){const I=Date.parse(u.started);if(!(I>=i[0]&&I<=i[1]))return l}const{eta:p,...w}=u,f=new Date().toISOString(),h={...w,done:f,...o?{note:o}:{}},{id:m}=F(t),k=g.dirname(g.dirname(l)),_=await Promise.all((await G(k,m)).map(I=>I===l?h:$(I,null))),N=!_.some(I=>I?.started&&!I.done),v=n?{file:g.join(k,"wave.json")}:(await x(e,t)).source,O=await $(v.file,null),A=O?.items?.find(I=>I.id===m);return s!==void 0&&(Li(A||d(`${t} has no item to set --next on`),s,await aa(e,t)),e&&await Hi(e,t,A,A.next)),c&&A&&vn(A,_)&&d(`builder done closes ${t}'s last open builder, which leaves it In Progress with no Next; pass --next "<what happens next>", or record a judgment call with builder call ${t} or land`),await y(l,h),await ut(e,t,{screen:u.kind==="grill"}),o&&await ma(e,t,a,u,{detail:o},f),s!==void 0&&await y(v.file,O),l}r(gc,"stopBuilder");async function $c(e){const t=[];for(const n of await q(e)){const a=await $(n.file,null);for(const i of a?.items||[])for(const o of await G(n.dir,i.id)){const s=await $(o,null);if(!s?.started||s.done)continue;const c=g.basename(o,".json");t.push({key:`${n.slug}/${i.id}`,agent:c.slice(String(i.id).length+1)||"main",label:s.label||"Builder",started:s.started})}}return t}r($c,"openBoardBuilders");async function Ou(e){const t=await $c(e);for(const n of t)await gc(e,n.key,{agent:n.agent,note:"Board closed. Builder record stopped; work is kept for reopening."});return t}r(Ou,"closeBoardBuilders");export{ue as BoardError,Ja as PUSH_LINE_MAX,Jt as REPLY_EXTS,Ts as SCREEN_AFTER,In as UPLOAD_LIMIT,tl as WORKING_KINDS,qd as abandonLaunch,Md as abandonPlanner,kl as ackPing,bd as addItem,Yl as addNotes,Ul as addPrototype,Kl as addQuestions,Dl as addReviewers,Il as addRuleTo,Wl as addTopic,ou as addVisual,pu as answerBuilder,wl as answerCard,rt as appendMessage,W as appendStatus,jd as archiveCards,vd as archiveCount,rn as archiveFile,cn as archiveRank,wu as askOrchestrator,rd as askedGate,ru as auditLog,tn as blockWords,oe as boardConfig,se as boardRepoOf,wd as breakOwnerCheck,$u as briefFacts,kd as builderModel,du as builderProgress,Vd as bumpCard,Al as cardLinked,H as cardName,Ld as cardPR,Dd as changeWave,yu as checkCallsVerified,sd as checkClose,vu as checkNotReopened,Qd as clearStall,Ou as closeBoardBuilders,Pd as createWave,su as cutOffBuilds,fl as dropAskCard,Td as dropPause,Tl as editRuleIn,cl as eventCard,Ll as fallBackFromLinear,ll as fileAsk,dl as fileAsks,bl as finishHandshake,Fd as finishWave,_u as finishesCard,xu as handBackCalls,gd as heldMerge,tu as helperOf,za as hiddenFile,Cd as implementAsIs,ld as isFinished,au as isHelper,_o as isLanding,iu as isPlanner,q as itemSources,Gd as landFirst,ku as landable,uu as landingLine,mu as listAsks,hu as listCalls,Hl as listPrototypes,Cl as listRules,lc as markPaused,ol as markRead,Kd as markStalled,Jd as markStopped,nl as markWorking,hd as mergeCard,Tr as mergedLine,zt as messageText,yd as migrateFollowups,_d as migratePhases,xd as migrateStatuses,Ad as migrateWaveNames,hl as moveAsk,cd as moveCard,ad as moveQuestions,Ud as openAgents,$c as openBoardBuilders,Hd as openRuns,El as opensPRs,F as parseKey,fd as passOwnerCheck,cu as pickBuilder,ud as pickUp,td as planAgain,Pl as postDigest,$l as postIncomingReview,xl as postLink,md as postPR,_l as postReply,pd as prReport,Gl as publishScreened,Vl as publishUnscreened,Gs as pushLine,Vs as pushUrl,vl as queuePushes,Le as readArchive,_e as readAskLinks,Kn as readBoardRules,or as readBoardSettings,di as readHandshake,Ys as readHidden,$ as readJSON,Xs as readMarks,Qa as readMarksFile,Ir as readNotes,Qt as readPRSlack,Pt as readPrototypes,xi as readScreening,rl as readUpload,Nd as readWaveNotes,Ws as readWorking,fu as recordCall,Vi as recordOwnerTap,Hs as recordPush,Sl as recordReaction,Bd as recordRebase,gl as recordReview,Wd as recordSession,lu as recordedSession,So as recordedWorktree,pl as relateAsk,bu as relaunchLander,zl as removePrototype,Fl as removeReviewers,ql as removeRuleFrom,Us as repliesSincePush,Ln as replyFilesOf,Xd as requestHelper,xo as requestPause,id as resolveNoteAnswers,nd as resolveQuestion,Id as resumePaused,ul as retitleAskCard,ar as reviewDigest,jl as reviewDue,Ol as reviewUpdated,Ml as saveBoardRules,lt as saveBoardSettings,Nl as saveBoardShare,sl as saveUpload,al as sendPush,Jl as servedRoundsOpen,Rl as setBoardPause,Bl as setBoardSetting,il as setHidden,$e as setItem,dd as setMainInstall,yl as setReviewers,sr as settingsLines,Se as settingsOf,Zl as settleAnswers,Ql as settleQuestion,Xc as siteDir,Is as sitesDir,Xl as splitSharedNotes,yr as stageImplement,Sd as stageImplementAsks,ed as stageReply,Et as startBuilder,eu as startHelper,Zd as startPlanner,Ed as startRebase,Rd as startTappedBuild,Yd as startVisual,gc as stopBuilder,zd as stopHeldMerge,Od as stripDeadFields,Zn as takeFile,ju as takeHoldStopped,$d as tapMerge,Z as threadFile,Ri as topicQuestions,ml as unfoldAsk,od as unrecorded,Su as verifiedFinish,gu as verifyCalls,nu as waitHelper,ua as waveNoteFile,Ut as wavesDir,Qs as withReplies,el as writeReply};
37
+ ${s}`}}r(Cd,"readWaveNotes");async function U(e,t,n,a="main"){const{id:i}=F(t);/^[\w-]{1,40}$/.test(a)||d(`--agent takes a name of letters, digits, "_" and "-", not "${a}"`);const o=a==="main"?`${i}.json`:`${i}@${a}.json`;if(n)return g.join(g.resolve(n),"agents",o);const{source:s}=await x(e,t);return g.join(s.dir,"agents",o)}r(U,"builderFile");const ko=r((e,t)=>typeof e=="string"&&/^\d{4}-\d\d-\d\dT/.test(e)&&!Number.isNaN(Date.parse(e))?new Date(e).toISOString():d(`${t} takes an ISO time such as 2026-10-23T16:30:00-07:00`),"isoTime"),cc=Sn.progress,bo=80,yo="reading, building, testing, checking or landing";function xo(e,t,n,a){e||d(`${a} needs --site, since the line goes to the item's thread`),t!==void 0&&!cc.includes(t)&&d(`--phase takes ${yo}, not "${t}"`);const i=S(n,a);return i.length>bo&&d(`${a} is ${i.length} characters; the cap is ${bo}. Say it shorter.`),{phase:t,detail:i}}r(xo,"progressLine");const _o=r((e,t,n)=>t?{detail:e.detail}:(e.phase===void 0&&d(`${n} needs --phase ${yo}`),e),"forAgent");async function ha(e,t,n="main",a,{phase:i,detail:o},s){const c=We(a),l=c||n==="main"?"":a.kind===Y?n:Ht(a)||a.kind||n,u=c?Go[a.kind]:[l,i].filter(Boolean).join(" "),p=new RegExp(`^(?:${u}|${i??u})\\s*:?\\s+`,"i"),w=u?`${u[0].toUpperCase()}${u.slice(1)}: ${o.replace(p,"")}`:o,f=V(e,t);(await z(f)).filter(h=>h.kind==="status").pop()?.text!==w&&await de(f,{ts:s,who:"claude",kind:"status",run:a.started,...i?{phase:i}:{},...c?{planner:!0}:{},text:w})}r(ha,"statusToThread");const lc={prototype:"sonnet",grill:"opus",[$e]:"sonnet"};async function Ct(e,t,{label:n,phase:a,progress:i,dir:o,agent:s="main",kind:c,model:l,eta:u,worktree:p,ownWorktree:w,dispatch:f}){a!==void 0&&i===void 0&&d('--phase goes with --progress "<detail>"');const h=te(i,ae=>xo(e,a,ae,"--progress")),m=await U(e,t,o,s),b=new Date().toISOString(),_=te(c,ae=>/^[\w.-]{1,30}$/.test(ae)?ae:d("--kind takes a short word such as claude or codex")),N=vt(_),v=l??(_==="opus"||_==="sonnet"?_:void 0),O=["build",...$c].includes(N),A=await $(m,null),L=r(ae=>["tap","hub"].includes(ae?.dispatched)&&!ae.done,"tapOpen");if(f==="session"&&L(A))return{file:m,alreadyOpen:!0,lines:[]};s!=="main"&&_!==$e&&_!==Y&&L(await $(await U(e,t,o),null))&&d(`the Implement tap opened ${t}'s main builder record and no agent has taken it up; run builder start ${t} without --agent first`);const I=A?.dispatched&&!A.done?A:null,P=O&&!o?(await x(e,t)).item:void 0,B=A&&Xe(A).length?A:null,T=I&&N==="build"?{kind:vt(I.kind),model:I.model,reason:{tap:"The owner's Build tap picked it.",hub:"The board's pull picked it."}[I.dispatched]??"The session's dispatch picked it."}:O?Pn({card:P??{}}):null;T&&N!=="build"&&N!==T.kind&&d(`--kind ${N}: ${T.kind} builds now. ${T.reason} Pass --kind ${T.kind}, or --kind build.`);const W=T?T.kind:N,C=Ea({dir:e});if(!(I&&!f)&&is(C,W))throw new pe(as(C,W));const Z=h&&_o(h,We({kind:W}),"--progress");l!==void 0&&!["opus","sonnet"].includes(l)&&d("--model takes opus or sonnet"),v!==void 0&&W==="codex"&&d("--model is for kind claude; codex needs none");const ne=W==="claude"?Dn():"auto";v!==void 0&&ne!=="auto"&&v!==ne&&d(`--model ${v}: the hub's Claude model is ${ne==="opus"?"Opus":"Sonnet"}. Pass --model ${ne}, or no --model.`);const K=W==="claude"?v??T?.model??(I&&vt(I.kind)==="claude"?I.model:void 0)??"opus":We({kind:W})?v??(I?.kind===W?I.model:void 0)??lc[W]:W===Y?v??A?.model:void 0,X={label:n===void 0?"Builder":S(n,"--label"),...W?{kind:W}:{},...K?{model:K}:{},started:b,...u===void 0?{}:{eta:ko(u,"--eta")},...f?{dispatched:f}:{},...B?{calls:B.calls,...B.verified?{verified:B.verified}:{}}:{},...mc(A)?{session:A.session}:{},...W===Y&&A?.kind===Y?{label:A.label,task:A.task,writes:A.writes,requested:A.requested}:{}},le=O&&!o&&!Q(P)&&P.lane!=="progress",fe=le?await ye(e,t,{start:!0}):{lines:[]};let xe,Ae=null;const Tt=w&&O&&s==="main"?[A?.worktree,P?.worktree].find(ae=>ae?.path&&we(ae.path))??null:null,gt=(I??B)?.worktree??Tt,$t=p??(O&&s==="main"&&!gt?w:void 0);try{if($t){const ae=await G(g.dirname(g.dirname(m)),F(t).id),wn=(await Promise.all(ae.filter(ge=>ge!==m).map(ge=>$(ge,null)))).filter(ge=>ge?.worktree?.path&&!ge.done).map(ge=>ge.worktree.path);({remade:Ae,...xe}=await $t({busy:wn}))}else xe=gt}catch(ae){throw le&&await gc(e,t,P),ae}await dc(m,A),await y(m,xe?{...X,worktree:xe}:X),P?.rebase_run&&s==="main"&&![So,Fa].includes(X.label)&&await vo(e,t),T?.inputs&&e&&$s(Ca(e),{call:"builder-pick",key:t,result:[W,W==="claude"?K:null].filter(Boolean).join(" "),reason:T.reason,inputs:{...T.inputs,...v?{askedModel:v}:{},...f?{dispatch:f}:{}}}),Ae&&e&&await M(e,t,"worktree-remade"),Z&&await ha(e,t,s,X,Z,b);const oe=_===$e||_===Y?[]:await an(e,t,"Acted on: builder started");return e&&O&&await Pr(e,t),{file:m,kind:W??null,model:K??null,reason:T?.reason??null,worktree:xe?.path??null,lines:[...N==="build"?[`kind: ${W}. ${T.reason}`]:[],...Ht(X)?[`model: ${Ht(X)}`]:[],...e?[`ref: ${await H(e,t)}`]:[],...xe?[`worktree: ${xe.path}`]:[],...Ae?[Ae]:[],...fe.lines,...oe]}}r(Ct,"startBuilder");async function dc(e,t){if(!t?.started||!t.done||!e.endsWith(".json")||g.basename(e).includes("@"))return;const{label:n,kind:a,model:i,started:o,done:s,session:c}=t,l=g.join(g.dirname(e),`${g.basename(e,".json")}@past-${Date.parse(o)}.json`);await y(l,{label:n,...a?{kind:a}:{},...i?{model:i}:{},started:o,done:s,...c?.log?{session:{log:c.log}}:{}})}r(dc,"keepPastRun");async function Id(e,t,n,a="tap"){try{const{held:i,blockers:o}=await fc(e,t);if(i&&await eo(e,"card-hold",{key:t,hold:!1}),o.length)return await wc(e,t,o);try{const s=await Ct(e,t,{kind:"build",worktree:n,dispatch:a});return{key:t,ref:await H(e,t),kind:s.kind,...s.model?{model:s.model}:{},reason:s.reason,worktree:s.worktree}}catch(s){throw i&&await hc(e,t,i),s}}catch(i){if(!(i instanceof pe))throw i;return await M(e,t,Da,{why:i.message}),null}}r(Id,"startTappedBuild");async function Td(e,t,{pr:n,conflict:a,base:i},o){const{source:s,itemFile:c,item:l}=await x(e,t);l.rebase_run={pr:n,base:i,conflict:a,at:new Date().toISOString()},await y(s.file,c);try{const u=await Ct(e,t,{kind:"build",label:So,dispatch:"hub",ownWorktree:o});return{key:t,kind:u.kind,...u.model?{model:u.model}:{},worktree:u.worktree}}catch(u){throw await vo(e,t),u}}r(Td,"startRebase");const So="Rebase";async function vo(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);delete i.rebase_run,await y(n.file,a)}r(vo,"endRebaseRun");async function qd(e,t){const{itemFile:n,item:a}=await x(e,t),i=re(e,n);return Ue(a,i).find(o=>o.repo===i)?.number??null}r(qd,"cardPR");async function Md(e,t,{from:n,to:a,replayed:i}){const{source:o,itemFile:s,item:c}=await x(e,t);return c.rebase_run?(c.rebased=[...c.rebased||[],{from:n,to:a,base:c.rebase_run.base,replayed:i,at:new Date().toISOString()}],delete c.rebase_run,await y(o.file,s),!0):!1}r(Md,"recordRebase");const uc=r((e,t,n)=>{const a=new Map((e.rebased||[]).filter(i=>i.replayed).map(i=>[i.to,i.from]));for(let i=n,o=0;i&&o<=a.size;i=a.get(i),o++)if(i===t)return!0;return!1},"onlyRebased");async function fc(e,t){const n=Ea({dir:e});os(n)&&d(ns(n));const{item:a}=await x(e,t),i=a.held?{held:a.held,pick_up_at:a.pick_up_at,held_at:a.held_at}:null;return i&&he(a)&&d("it's held. Unhold it, then tap Implement."),await oa(e,t)&&d("no take is picked on its prototypes, so it prototypes again. Pick a take from the next round, then tap Implement."),{held:i,blockers:await Lr(e,t,a)}}r(fc,"tapRefusals");async function Wd(e,t,n){await Ke(e,t,{who:"love",by:n,command:"implement"}),await vr(e,t,{})}r(Wd,"implementAsIs");async function wc(e,t,n){const a=n.join(", ");return await ke({dir:e},"card-bump",{key:t}),await M(e,t,"cant-start-yet",{names:a}),{key:t,ref:await H(e,t),bumped:n,line:`bumped: ${t} waits on ${a}, so it stays in Todo, bumped to the top`}}r(wc,"bumpBlocked");async function un(e,t){const n=await U(e,t),a=await $(n,null);return a?.started&&!a.done&&nt(a)?{file:n,record:a}:null}r(un,"openBuild");async function jo(e,t,{cause:n,words:a,by:i,until:o}){const s=await un(e,t);return!s||s.record.pause?!1:(await y(s.file,{...s.record,pause:{cause:n,words:a,at:new Date().toISOString(),...i?{by:i}:{},...o?{until:o}:{}}}),!0)}r(jo,"requestPause");async function pc(e,t){const n=await un(e,t),a=n?.record.pause;if(!a||a.paused)return a??null;const i=new Date().toISOString(),o=(await z(V(e,t))).filter(c=>c.kind==="status"&&c.phase&&!c.planner&&String(c.ts)>=String(n.record.started)).pop()?.phase,s={...a,paused:i,...o?{phase:o}:{}};return await y(n.file,{...n.record,pause:s}),await M(e,t,"paused",{phase:o,words:a.words,clear:Ra[a.cause][0]}),s}r(pc,"markPaused");async function fn(e,t,{dir:n,agent:a="main"}){if(!e||n||a!=="main")return[];const i=await pc(e,t);return i?[`Paused: ${i.words}. Commit what you have now with \`git -c core.commentChar=';' commit -F <file>\`, then stop: end your turn and run nothing else. The board resumes this session, telling it so, once the pause clears.`]:[]}r(fn,"pauseReply");async function Ud(e,t){const n=await un(e,t),a=n?.record.pause;if(!a?.paused)return null;const{pause:i,...o}=n.record;return await y(n.file,o),await M(e,t,"resumed",{clear:Ra[a.cause][1]}),{pause:a,record:o}}r(Ud,"resumePaused");async function Hd(e,t){const n=await un(e,t);if(!n?.record.pause||n.record.pause.paused)return;const{pause:a,...i}=n.record;await y(n.file,i)}r(Hd,"dropPause");async function Jd(e,t,n){const a=await U(e,t),i=await $(a,null),o=new Date().toISOString();i&&!i.done&&await y(a,{...i,done:o});const{source:s,itemFile:c,item:l}=await x(e,t);l.lane==="progress"&&Et(l,"todo",o),await y(s.file,c),await M(e,t,Da,{why:n})}r(Jd,"abandonLaunch");async function zd(e,t,[n,a],i="main"){const o=await U(e,t,void 0,i),s=await $(o,null);s&&!s.done&&await y(o,{...s,done:new Date().toISOString()}),await wt(e,t),await M(e,t,n,a)}r(zd,"abandonPlanner");async function Kd(e,t,n,a,i="main",o=null){const s=await U(e,t,void 0,i),c=await $(s,null);await y(s,{...c,session:{id:n,pid:a,log:o,at:new Date().toISOString()}})}r(Kd,"recordSession");async function Qd(e,t){const{source:n}=await x(e,t);return(await Promise.all((await G(n.dir,F(t).id)).map(i=>$(i,null)))).filter(i=>i&&i.started&&!i.done).map(i=>({pid:i.session?.pid??null,at:i.session?.at??null}))}r(Qd,"openAgents");async function Gd(e,t){const{source:n}=await x(e,t);return(await Promise.all((await G(n.dir,F(t).id)).map(i=>$(i,null)))).filter(i=>i&&i.started&&!i.done).map(i=>({kind:i.kind??null,pid:i.session?.pid??null,at:i.session?.at??null}))}r(Gd,"openRuns");async function Vd(e,t,n){const{source:a}=await x(e,t),i=new Date().toISOString();for(const o of await G(a.dir,F(t).id)){const s=await $(o,null);s?.started&&!s.done&&nt(s)&&await y(o,{...s,stopped:{run:n,at:i}})}}r(Vd,"markStopped");async function Zd(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);i.merge_ready&&(i.merge_ready.stopped=new Date().toISOString(),await y(n.file,a))}r(Zd,"stopHeldMerge");async function Yd(e,t,n){const a=S(n,"item stall --reason"),{source:i,itemFile:o,item:s}=await x(e,t);return s.lane==="done"&&d(`${await H(e,t)} is done; there is nothing to stall`),s.stalled={reason:a,at:new Date().toISOString()},await y(i.file,o),a}r(Yd,"markStalled");async function Xd(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);i.stalled&&(delete i.stalled,await y(n.file,a))}r(Xd,"clearStall");async function eu(e,t){const{item:n}=await x(e,t),a=await z(g.join(e,"data","events.jsonl")),i=r(c=>a.filter(l=>l.kind===c&&l.data?.key===t).map(l=>String(l.ts)).sort().pop()||"","latest"),o=i("card-bump");if(!o||i("card-unbump")>o)return null;const s=[n.closed_at,...Ne(n).map(c=>c.merged_at)].filter(Boolean).map(String).sort().pop()||"";return o>s?o:null}r(eu,"landFirst");async function tu(e,t,n){const{item:a}=await x(e,t);Q(a)&&d(`${await H(e,t)} is done; there is nothing to bump`);const i=await ke({dir:e},n?"card-bump":"card-unbump",{key:t});return(await z(g.join(e,"data","events.jsonl"))).find(o=>o.id===i).ts}r(tu,"bumpCard");async function Oo(e,t){return(await z(V(e,t))).filter(n=>n.kind==="status").pop()?.phase==="landing"}r(Oo,"isLanding");const mc=r(e=>!!e?.session&&e.session.at>(e.done??e.started),"launchedSession");async function nu(e,t,n){const a=await $(await U(e,t),null);if(a&&!a.done)return null;const i=await Ct(e,t,{kind:n,dispatch:"hub"});return{key:t,kind:n,model:i.model}}r(nu,"startPlanner");async function au(e,t,n){const{source:a}=await x(e,t);if((await Promise.all((await G(a.dir,F(t).id)).map(c=>$(c,null)))).some(c=>c?.kind===$e&&c.started&&!c.done))return null;const o=Yo(n);if(await $(await U(e,t,void 0,o),null))return null;const s=await Ct(e,t,{kind:$e,agent:o,dispatch:"hub"});return{key:t,kind:$e,model:s.model,agent:o}}r(au,"startVisual");const ga=r(e=>typeof e=="string"&&/^[a-z][a-z0-9-]{0,29}$/.test(e)&&e!=="main"&&!e.startsWith(`${$e}-`)?e:d('--name takes a short lowercase name such as tests or docs-pass: letters, digits and "-", not main or visual-\u2026'),"helperName");async function $a(e,t){const{item:n}=await x(e,t);return[(await $(await U(e,t),null))?.worktree,n.worktree].find(i=>i?.path&&we(i.path))??null}r($a,"cardWorktree");async function iu(e,t,{name:n,task:a,writes:i,model:o}){D(e);const s=ga(n),c=S(a,"--task");o!==void 0&&!["opus","sonnet"].includes(o)&&d("--model takes opus or sonnet");const l=Dn();o!==void 0&&l!=="auto"&&o!==l&&d(`--model ${o}: the hub's Claude model is ${l==="opus"?"Opus":"Sonnet"}. Pass --model ${l}, or no --model.`);const{item:u}=await x(e,t);Q(u)&&d(`${await H(e,t)} is Done; a helper needs an open card`);const p=await U(e,t,void 0,s);return we(p)&&d(`${t} already has an agent named ${s}; pick another --name`),i&&!await $a(e,t)&&d(`a helper that writes branches from ${t}'s worktree, and the card has none; start its build first, or drop --writes`),await y(p,{label:s,kind:Y,task:c,writes:!!i,...o?{model:o}:{},requested:new Date().toISOString()}),await M(e,t,"helper-asked",{agent:s,writes:i}),{lines:[`helper ${s} requested: the board launches it ${i?"once an implementer slot is free":"now"}`,`wait for it with agent wait ${t} --name ${s}`]}}r(iu,"requestHelper");async function ou(e,t,n,a){const i=await U(e,t,void 0,n),o=await $(i,null);if(o?.kind!==Y||o.started||o.done)return null;const{item:s}=await x(e,t),c=Pn({card:s}),l=Dn(),u=o.model??(l!=="auto"?l:c.kind==="claude"?c.model:"opus"),p=o.writes?await $a(e,t):null;o.writes&&!p&&d(`${t} has no worktree for helper ${n} to branch from`),await y(i,{...o,model:u});const w=await Ct(e,t,{agent:n,kind:Y,dispatch:"hub",worktree:p?({busy:f})=>a({repo:p.repo,base:p.branch,busy:f}):void 0});return{key:t,kind:Y,model:u,agent:n,task:o.task,writes:o.writes,worktree:w.worktree}}r(ou,"startHelper");async function su(e,t,n){const a=await $(await U(e,t,void 0,ga(n)),null);return a?.kind!==Y&&d(`${t} has no helper named ${n}`),{record:a,card:await $a(e,t)}}r(su,"helperOf");async function ru(e,t,{name:n,agent:a="main",timeout:i=9,every:o=2e3,alive:s}){D(e);const c=Number(i);c>0||d("--timeout takes minutes, a number above 0");const l=await U(e,t,void 0,ga(n)),u=await U(e,t,void 0,a),p=r(async f=>{const h=await $(u,null);if(!h?.started||h.done)return;const{waits:m,...b}=h;await y(u,f?{...b,waits:f}:b)},"mark"),w=Date.now()+c*6e4;await p(n);try{for(;;){const f=await $(l,null);if(f?.kind!==Y&&d(`${t} has no helper named ${n}`),f.done){const m=f.writes&&f.worktree?[`branch ${f.worktree.branch} in ${f.worktree.path}: merge it with agent merge ${t} --name ${n}`]:[];return{lines:[`helper ${n} done: ${f.note??"no hand-back"}`,...m]}}const h=f.session?.pid??null;h&&!s(h)&&d(`helper ${n}'s process exited without builder done; do its task yourself`),Date.now()>=w&&d(`helper ${n} is still ${f.started?"running":f.writes?"waiting for an implementer slot":"waiting to launch"} after ${c} minutes; run agent wait again`),await new Promise(m=>setTimeout(m,o))}}finally{await p(null)}}r(ru,"waitHelper");async function cu(e,t,n="main"){return n==="main"?!1:(await $(await U(e,t,void 0,n),null))?.kind===Y}r(cu,"isHelper");async function lu(e,t,n="main"){const a=await $(await U(e,t,void 0,n),null);return!!(a?.started&&!a.done&&We(a))}r(lu,"isPlanner");async function du(e,t,{file:n,title:a}){D(e);const i=S(a,"--title");(typeof n!="string"||!n.endsWith(".html"))&&d("--file takes the page, an .html file");const o=await j.readFile(g.resolve(n),"utf8").catch(()=>d(`--file ${n} can't be read`));o.trim()||d(`--file ${n} is empty`),await x(e,t);const{id:s}=F(t),c=g.join(e,"public","visuals",s);await j.mkdir(c,{recursive:!0});const l=i.toLowerCase().replace(/[^a-z0-9]+/g,"-").slice(0,60).replace(/^-+|-+$/g,"")||"visual";let u=`${l}.html`;for(let w=2;we(g.join(c,u));w++)u=`${l}-${w}.html`;await j.writeFile(g.join(c,u),o,{flag:"wx"});const p=`visuals/${s}/${u}`;return await de(V(e,t),{ts:new Date().toISOString(),who:"claude",kind:$e,text:S(i,"--name"),link:Ln(p,"--link")}),{lines:[p]}}r(du,"addVisual");async function uu(e){const t=[];for(const n of await q(e)){const a=await $(n.file,null);for(const i of a?.items||[]){if(Q(i))continue;const o=await $(g.join(n.dir,"agents",`${i.id}.json`),null);!o?.started||o.done||!o.dispatched||We(o)||t.push({key:`${n.slug}/${i.id}`,kind:vt(o.kind),model:o.model})}}return t}r(uu,"cutOffBuilds");async function hc(e,t,n){const{source:a,itemFile:i,item:o}=await x(e,t);for(const s of Object.keys(n))n[s]===void 0?delete o[s]:o[s]=n[s];await y(a.file,i)}r(hc,"restoreHold");async function gc(e,t,n){const{source:a,itemFile:i,item:o}=await x(e,t);for(const s of["lane","lane_at","closed_at","reopened_at"])n[s]===void 0?delete o[s]:o[s]=n[s];await y(a.file,i)}r(gc,"restoreLane");async function fu(e,t){return e||d("audit reads a board: pass --site <route>"),t!==void 0&&F(t),{lines:ks(bs(Ca(e),{key:t}))}}r(fu,"auditLog");const $c=["codex","claude"];async function wu(e,t){const{item:n}=await x(e,t),{kind:a,model:i,reason:o}=Pn({card:n});return{lines:[`${[a,i].filter(Boolean).join(" ")}: ${o}`]}}r(wu,"pickBuilder");async function Ao(e,t,{dir:n}={}){const{id:a}=F(t),i=g.dirname(await U(e,t,n)),o=await G(g.dirname(i),a);return(await Promise.all(o.map(c=>$(c,null)))).filter(c=>c?.worktree?.path&&c.kind!==Y).sort((c,l)=>String(l.started).localeCompare(String(c.started)))[0]?.worktree??null}r(Ao,"recordedWorktree");async function pu(e,t){const{source:n}=await x(e,t),i=(await Promise.all((await G(n.dir,F(t).id)).map(o=>$(o,null)))).filter(o=>o?.session?.id&&o.worktree?.path&&o.kind!==Y).sort((o,s)=>String(s.started).localeCompare(String(o.started)))[0];return i?{id:i.session.id,worktree:i.worktree.path}:null}r(pu,"recordedSession");async function mu(e,t,n,{phase:a,dir:i,agent:o="main",eta:s,clearEta:c}){s!==void 0&&c&&d("pass --eta or --clear-eta, not both");const l=xo(e,a,n,"builder progress"),u=await U(e,t,i,o),p=await $(u,null);(!p?.started||p.done)&&d(`builder progress needs an open builder record on ${t}; run builder start first`);const w=_o(l,We(p),"builder progress");return s!==void 0&&(p.eta=ko(s,"--eta")),c&&delete p.eta,(s!==void 0||c)&&await y(u,p),await ha(e,t,o,p,w,new Date().toISOString()),{file:u,lines:await fn(e,t,{dir:i,agent:o})}}r(mu,"builderProgress");async function hu(e,t){const{source:n}=await x(e,t),a=V(e,t);if((await z(a)).filter(s=>s.kind==="status").pop()?.phase==="landing")return;const o=(await Promise.all((await G(n.dir,F(t).id)).map(s=>$(s,null)))).filter(s=>s?.started&&nt(s)&&s.label!==Fa).map(s=>s.started).sort().pop();await de(a,{ts:new Date().toISOString(),who:"claude",kind:"status",phase:"landing",text:"Landing: rebasing onto main and landing",...o?{run:o}:{}})}r(hu,"landingLine");const Xe=r(e=>(e?.calls||[]).filter(t=>!e.verified||t.at>e.verified.at),"unverifiedCalls");async function gu(e,t,n,{dir:a,agent:i="main"}){const o=S(n,"builder call");/\n/.test(o)&&d("builder call takes one line; record each call on its own");const s=await U(e,t,a,i),c=await $(s,null);return(!c?.started||c.done)&&d(`builder call needs an open builder record on ${t}; run builder start first`),await y(s,{...c,calls:[...c.calls||[],{at:new Date().toISOString(),text:o}]}),{file:s,lines:await fn(e,t,{dir:a,agent:i})}}r(gu,"recordCall");async function It(e,t){const n=await z(V(e,t)),a=new Map(n.filter(i=>i.kind===Wt.answer).map(i=>[i.ask,i]));return n.filter(i=>i.kind===Wt.ask).map(i=>{const o=a.get(i.ask);return{id:i.ask,agent:i.agent||"main",text:i.text,at:i.ts,answer:o?{text:o.text,at:o.ts}:null,deferred:!!o?.deferred}})}r(It,"builderAsks");const kc=r(e=>be().find(t=>Fe(t.dir,e))??{dir:e},"boardTarget");async function $u(e,t,n,{agent:a="main"}){D(e);const i=S(n,"builder ask");/\n/.test(i)&&d("builder ask takes one line; ask each question on its own");const o=await $(await U(e,t,void 0,a),null);(!o?.started||o.done)&&d(`builder ask needs an open builder record on ${t}; run builder start first`);const s=`ask${(await It(e,t)).length+1}`;return await de(V(e,t),{ts:new Date().toISOString(),who:`${Ht(o)||o.kind||"Claude"} builder`,kind:Wt.ask,ask:s,agent:a,text:i}),await ke(kc(e),"builder-ask",{key:t,ask:s,agent:a,text:i}),{id:s,lines:await fn(e,t,{agent:a})}}r($u,"askOrchestrator");async function ku(e,t,n,a,{deferred:i=!1}={}){D(e);const o=S(a,"--text"),s=(await It(e,t)).find(l=>l.id===n)||d(`no ask "${n}" on ${t}; builder asks ${t} lists them`);s.answer&&d(`${n} on ${t} is already ${s.deferred?"deferred":"answered"}`);const c=new Date().toISOString();if(await de(V(e,t),{ts:c,who:"claude",kind:Wt.answer,ask:n,...i?{deferred:!0}:{},text:o}),i){const l=await U(e,t,void 0,s.agent),u=await $(l,null);u&&await y(l,{...u,calls:[...u.calls||[],{at:c,text:`Question: ${s.text} (deferred to the owner)`}]})}return{lines:[]}}r(ku,"answerBuilder");async function bu(e,t,{wait:n=!1,timeout:a=9,every:i=2e3}={}){D(e);const o=Number(a);o>0||d("--timeout takes minutes, a number above 0");const s=Date.now()+o*6e4;for(;;){const c=await It(e,t);if(!c.length)return{lines:["no asks yet"]};const l=c.filter(w=>!w.answer),u=c.flatMap(w=>[`${w.id}. ${w.text}`,` ${w.answer?`${w.deferred?"deferred":"answered"}: ${w.answer.text}`:"waiting on the orchestrator"}`]);if(!n||!l.length)return{lines:u};const p=await fn(e,t,{});if(p.length)return{lines:[...u,...p]};Date.now()>=s&&d(`${l.map(w=>w.id).join(", ")} still waiting on the orchestrator after ${o} minutes; run builder asks ${t} --wait again`),await new Promise(w=>setTimeout(w,i))}}r(bu,"listAsks");async function bc(e,t,n){const a=t.topic.startsWith("item:")&&!t.topic.startsWith("item:ask:")?t.topic.slice(5):d(`${n}.ask goes on a card's question, topic item:<file>/<id>`),i=(await It(e,a)).find(o=>o.id===t.ask)||d(`${n}.ask names no ask "${t.ask}" on ${a}; builder asks ${a} lists them`);i.deferred||d(`${n}.ask names ${i.id}, which the orchestrator ${i.answer?"answered":"has not deferred"}; only a deferred ask goes to the owner`)}r(bc,"checkAskQuestion");async function yu(e,t,{dir:n,agent:a="main"}){const i=await $(await U(e,t,n,a),null),o=i?.calls||[];if(!o.length)return{lines:["no judgment calls recorded"]};const{verified:s}=i,c=Xe(i).length?"not verified yet":`verified ${s.at}${s.note?`: ${s.note}`:""}`;return{lines:[...o.map((l,u)=>`${u+1}. ${l.text}`),c]}}r(yu,"listCalls");async function xu(e,t,{dir:n,agent:a="main",note:i}){const o=await U(e,t,n,a),s=await $(o,null);if(Xe(s).length||d(`${t} has no unverified judgment calls${a==="main"?"":` from agent ${a}`}; builder calls ${t} lists them`),e){const c=new Set((await $(E(e),{items:[]})).items?.filter(u=>u.topic===`item:${t}`&&u.ask).map(u=>u.ask)),l=(await It(e,t)).find(u=>u.deferred&&u.agent===a&&!c.has(u.id));l&&d(`${l.id} on ${t} was deferred to the owner; add its question with "ask": "${l.id}" first`)}return await y(o,{...s,verified:{at:new Date().toISOString(),...i===void 0?{}:{note:S(i,"--note")}}}),o}r(xu,"verifyCalls");async function _u(e,t){const{source:n,item:a}=await x(e,t),i=r((s,c)=>[(s.options||[]).find(l=>l.id===c.choice)?.label,c.note].filter(Boolean).join(": ")||"answered","said"),o=(await ue(e,t)).asked.filter(s=>s.ask&&s.answers.length).map(s=>({ask:s.ask,question:s.question,answer:i(s,s.answers.at(-1))}));return{key:t,ref:await H(e,t),title:a.title||t,notes:a.plan?g.join(n.dir,a.plan):null,thread:V(e,t),worktree:(await Ao(e,t))?.path??null,answers:o,links:await yc(e,t),rebasePR:a.rebase_run?.pr??null}}r(_u,"briefFacts");async function yc(e,t){const n=new Map;for(const c of await q(e))for(const l of(await $(c.file,null))?.items||[])n.set(`${c.slug}/${l.id}`,{entry:c,it:l});const a=F(t).file,{it:i}=n.get(t),o=[...n].filter(([,c])=>(c.it.related||[]).some(l=>J(c.entry.slug,l)===t)).map(([c])=>c),s=[...i.parent?[["parent",J(a,i.parent)]]:[],...(i.deps||[]).map(c=>["depends on",J(a,c)]),...[...new Set([...(i.related||[]).map(c=>J(a,c)),...o])].map(c=>["related",c])].filter(([,c])=>n.has(c));return Promise.all(s.map(async([c,l])=>{const{entry:u,it:p}=n.get(l);return{relation:c,ref:await H(e,l),title:p.title||l,lane:Te[p.lane||"todo"],notes:p.plan?g.join(u.dir,p.plan):null,thread:V(e,l)}}))}r(yc,"cardLinks");async function Su(e){const t=[];for(const n of await q(e)){const a=await $(n.file,null);for(const i of a?.items||[]){if(Q(i))continue;const o=(await Promise.all((await G(n.dir,i.id)).map(u=>$(u,null)))).filter(Boolean);if(!vn(o))continue;const s=o.map(u=>u.verified?.at).filter(Boolean).sort().at(-1);if(!s||o.some(u=>Xe(u).length))continue;const c=`${n.slug}/${i.id}`;if((await ue(e,c)).asked.some(u=>u.state==="open")||await on(e,c,i))continue;const l=o.filter(u=>u.worktree?.path&&u.kind!==Y).sort((u,p)=>String(p.started).localeCompare(String(u.started)))[0];l&&we(l.worktree.path)&&t.push({key:c,verified:s,kind:vt(l.kind),model:l.model})}}return t}r(Su,"landable");async function vu(e,t){const{source:n,item:a}=await x(e,t);(await Promise.all((await G(n.dir,a.id)).map(c=>$(c,null)))).some(c=>c?.started&&!c.done)||d(`${t} has no open builder; only a card whose lander stopped has one to relaunch`),await ye(e,t,{handBack:!0,next:"A lander relaunches to land the verified build"});const o=g.join(e,"data","landers.json"),s=await $(o,{});delete s[t],await j.writeFile(o+".tmp",JSON.stringify(s)+`
38
+ `),await j.rename(o+".tmp",o)}r(vu,"relaunchLander");async function ju(e,t,{dir:n}={}){const{id:a}=F(t),i=await G(g.dirname(g.dirname(await U(e,t,n))),a),o=[];for(const s of i){if(!Xe(await $(s,null)).length)continue;const c=g.basename(s,".json").slice(String(a).length+1)||"main";o.push(c==="main"?"":` --agent ${c}`)}o.length&&d(`land: ${t} has judgment calls nobody verified yet; check them with ${o.map(s=>`builder calls ${t}${s}`).join(", ")}, then run ${o.map(s=>`builder verified ${t}${s}`).join(", ")}`)}r(ju,"checkCallsVerified");async function Ou(e,t,{dir:n,agent:a="main",next:i}){const o=await $(await U(e,t,n,a),null),s=o&&!o.done?Xe(o):[];if(!s.length)return null;i!==void 0&&d(`builder done sets ${t}'s Next itself while its record has judgment calls; run it without --next`);const c=s.some(u=>u.text.startsWith("Question:"))?"Answer the builder's questions":"Verify the builder's judgment calls",l=await ye(e,t,{handBack:!0,next:c});return{lines:[`handed back: Next "${c}"`,...l.lines]}}r(Ou,"handBackCalls");async function Au(e,t,{agent:n="main"}){if(n!=="main")return!1;const a=await U(e,t,void 0,n),i=await $(a,null);if(!i?.started||i.done||!i.worktree?.path)return!1;const{id:o}=F(t);for(const s of await G(g.dirname(g.dirname(a)),o)){const c=s===a?null:await $(s,null);if(!c?.started||c.done||c.kind==="install")continue;const l=g.basename(s,".json").slice(String(o).length+1);d(`builder done would finish ${t} while agent ${l} is still open; run builder done ${t} --agent ${l} first`)}return!0}r(Au,"finishesCard");async function Pu(e,t){const{source:n,item:a}=await x(e,t);if(Q(a))return null;const i=(await Promise.all((await G(n.dir,a.id)).map(s=>$(s,null)))).filter(Boolean);if(!vn(i))return{held:"the build hasn't handed back; run builder done first"};if(i.some(s=>Xe(s).length))return{held:"a builder record still has unverified judgment calls"};if(i.some(s=>s.started&&!s.done&&s.kind!=="install"))return{held:"a builder record is still open"};const o=await Ao(e,t);return!o||!we(o.path)?{held:"the build's worktree is gone"}:{waits:(await ue(e,t)).asked.filter(s=>s.state==="open").map(s=>s.id)}}r(Pu,"verifiedFinish");async function Du(e,t){const{item:n}=await x(e,t);if(n.reopened_at&&d(`the owner reopened ${await H(e,t)}; it closes on the owner's Done`),n.lane!=="done"||Ne(n).some(o=>o.merged_at))return;const a=String(n.closed_at||"");(await z(g.join(e,"data","events.jsonl"))).some(o=>o.kind==="item-message"&&o.data?.key===t&&String(o.ts)>a)&&d(`the owner reopened ${await H(e,t)} with a message; it closes on the owner's Done`)}r(Du,"checkNotReopened");async function G(e,t){const n=g.join(e,"agents");return(await j.readdir(n).catch(()=>[])).filter(a=>a===`${t}.json`||a.startsWith(`${t}@`)&&!a.startsWith(`${t}@past-`)&&a.endsWith(".json")).map(a=>g.join(n,a))}r(G,"builderFiles");async function Po(e,t,n,{handedBack:a,endHandBack:i=!1,held:o=!1}={}){const s=new Date().toISOString();for(const c of await G(e,t)){const l=await $(c,null);if(!l)continue;if(l.done||l.kind==="install"&&!n){if(i&&l.handed_back){const{handed_back:f,...h}=l;await y(c,h)}continue}if(o&&l.pause)continue;const{eta:u,handed_back:p,...w}=l;await y(c,{...w,done:s,...a?{handed_back:a}:{},...o&&l.started?{hold_stopped:!0}:{}})}}r(Po,"closeBuilders");async function Fu(e,t){const n=await U(e,t),a=await $(n,null);if(!a?.hold_stopped)return null;const{hold_stopped:i,...o}=a;return await y(n,o),{kind:a.kind??null,label:a.label??null}}r(Fu,"takeHoldStopped");async function xc(e,t,{dir:n,agent:a,startedBetween:i,note:o,next:s,requireNext:c=!1}){const l=await U(e,t,n,a),u=await $(l,null);if(!u||u.done)return l;if(i){const L=Date.parse(u.started);if(!(L>=i[0]&&L<=i[1]))return l}const{eta:p,...w}=u,f=new Date().toISOString(),h={...w,done:f,...o?{note:o}:{}},{id:m}=F(t),b=g.dirname(g.dirname(l)),_=await Promise.all((await G(b,m)).map(L=>L===l?h:$(L,null))),N=!_.some(L=>L?.started&&!L.done),v=n?{file:g.join(b,"wave.json")}:(await x(e,t)).source,O=await $(v.file,null),A=O?.items?.find(L=>L.id===m);return s!==void 0&&(Ti(A||d(`${t} has no item to set --next on`),s,await ia(e,t)),e&&await Qi(e,t,A,A.next)),c&&A&&jn(A,_)&&d(`builder done closes ${t}'s last open builder, which leaves it In Progress with no Next; pass --next "<what happens next>", or record a judgment call with builder call ${t} or land`),await y(l,h),await wt(e,t,{screen:u.kind==="grill"}),o&&await ha(e,t,a,u,{detail:o},f),s!==void 0&&await y(v.file,O),l}r(xc,"stopBuilder");async function _c(e){const t=[];for(const n of await q(e)){const a=await $(n.file,null);for(const i of a?.items||[])for(const o of await G(n.dir,i.id)){const s=await $(o,null);if(!s?.started||s.done)continue;const c=g.basename(o,".json");t.push({key:`${n.slug}/${i.id}`,agent:c.slice(String(i.id).length+1)||"main",label:s.label||"Builder",started:s.started})}}return t}r(_c,"openBoardBuilders");async function Nu(e){const t=await _c(e);for(const n of t)await xc(e,n.key,{agent:n.agent,note:"Board closed. Builder record stopped; work is kept for reopening."});return t}r(Nu,"closeBoardBuilders");export{pe as BoardError,Va as PUSH_LINE_MAX,Qt as REPLY_EXTS,Us as SCREEN_AFTER,Tn as UPLOAD_LIMIT,sl as WORKING_KINDS,Jd as abandonLaunch,zd as abandonPlanner,Sl as ackPing,vd as addItem,ad as addNotes,Ql as addPrototype,Yl as addQuestions,Ll as addReviewers,Ul as addRuleTo,Kl as addTopic,du as addVisual,ku as answerBuilder,$l as answerCard,lt as appendMessage,M as appendStatus,Fd as archiveCards,Dd as archiveCount,ln as archiveFile,dn as archiveRank,$u as askOrchestrator,fd as askedGate,fu as auditLog,on as blockWords,se as boardConfig,re as boardRepoOf,$d as breakOwnerCheck,_u as briefFacts,Sd as builderModel,mu as builderProgress,tu as bumpCard,Rl as cardLinked,H as cardName,qd as cardPR,Ld as changeWave,ju as checkCallsVerified,ud as checkClose,Du as checkNotReopened,Xd as clearStall,Nu as closeBoardBuilders,Ed as createWave,uu as cutOffBuilds,gl as dropAskCard,Hd as dropPause,Hl as editRuleIn,wl as eventCard,ql as fallBackFromLinear,pl as fileAsk,ml as fileAsks,vl as finishHandshake,Bd as finishWave,Au as finishesCard,Ou as handBackCalls,xd as heldMerge,su as helperOf,Za as hiddenFile,Wd as implementAsIs,pd as isFinished,cu as isHelper,Oo as isLanding,lu as isPlanner,q as itemSources,eu as landFirst,Su as landable,hu as landingLine,bu as listAsks,yu as listCalls,Gl as listPrototypes,Wl as listRules,pc as markPaused,dl as markRead,Yd as markStalled,Vd as markStopped,rl as markWorking,yd as mergeCard,Ur as mergedLine,Gt as messageText,jd as migrateFollowups,Ad as migratePhases,Od as migrateStatuses,Rd as migrateWaveNames,yl as moveAsk,wd as moveCard,cd as moveQuestions,Qd as openAgents,_c as openBoardBuilders,Gd as openRuns,Tl as opensPRs,F as parseKey,gd as passOwnerCheck,wu as pickBuilder,hd as pickUp,sd as planAgain,El as postDigest,_l as postIncomingReview,Ol as postLink,bd as postPR,Al as postReply,kd as prReport,ed as publishScreened,td as publishUnscreened,Xs as pushLine,er as pushUrl,Dl as queuePushes,Ce as readArchive,ve as readAskLinks,Qn as readBoardRules,lr as readBoardSettings,mi as readHandshake,nr as readHidden,$ as readJSON,ar as readMarks,Xa as readMarksFile,Wr as readNotes,Zt as readPRSlack,Ft as readPrototypes,Oi as readScreening,fl as readUpload,Cd as readWaveNotes,zs as readWorking,gu as recordCall,eo as recordOwnerTap,Qs as recordPush,Pl as recordReaction,Md as recordRebase,xl as recordReview,Kd as recordSession,pu as recordedSession,Ao as recordedWorktree,kl as relateAsk,vu as relaunchLander,Zl as removePrototype,Bl as removeReviewers,Jl as removeRuleFrom,Ks as repliesSincePush,Bn as replyFilesOf,iu as requestHelper,jo as requestPause,ld as resolveNoteAnswers,rd as resolveQuestion,Ud as resumePaused,hl as retitleAskCard,rr as reviewDigest,Fl as reviewDue,Nl as reviewUpdated,zl as saveBoardRules,ut as saveBoardSettings,Cl as saveBoardShare,ul as saveUpload,cl as sendPush,Vl as servedRoundsOpen,Il as setBoardPause,Ml as setBoardSetting,ll as setHidden,ye as setItem,md as setMainInstall,jl as setReviewers,dr as settingsLines,je as settingsOf,nd as settleAnswers,Xl as settleQuestion,il as siteDir,Ws as sitesDir,id as splitSharedNotes,vr as stageImplement,Pd as stageImplementAsks,od as stageReply,Ct as startBuilder,ou as startHelper,nu as startPlanner,Td as startRebase,Id as startTappedBuild,au as startVisual,xc as stopBuilder,Zd as stopHeldMerge,Nd as stripDeadFields,Yn as takeFile,Fu as takeHoldStopped,_d as tapMerge,V as threadFile,Ci as topicQuestions,bl as unfoldAsk,dd as unrecorded,Pu as verifiedFinish,xu as verifyCalls,ru as waitHelper,fa as waveNoteFile,zt as wavesDir,Ys as withReplies,ol as writeReply};