@lovek4d/kickflip 0.8.0 → 0.10.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,39 @@
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}
1
+ var Vo=Object.defineProperty;var c=(t,e)=>Vo(t,"name",{value:e,configurable:!0});import O from"node:fs/promises";import{constants as Zo,existsSync as wt,readFileSync as Yo}from"node:fs";import $ from"node:path";import Fn from"node:os";import{execFile as Xo}from"node:child_process";import{nodeBin as ts}from"../bin/install.mjs";import{gh as Ca,limitWords as es}from"../shared/gh.mjs";import{squashBody as ns}from"./commit-message.mjs";import{fileURLToPath as as}from"node:url";import{BUILD_REFUSED as Ia,BUILDER_ASK as Ue,COMMANDS as He,LANDER as Ta,LANES as qt,MOVE as qa,PAUSE_CLEARS as Ma,PHASES as Nn,handedBackAt as Rn,MAX_IMPLEMENTERS as is,MAX_PLANNERS as os,builds as ie,waveItems as Mt,mergeBlockers as ss,modelName as Je,noTakePicked as rs,ownerQuestion as Wt,plans as Ut,QUESTION_REOPEN as cs,PLANNER_WORD as ls,providerOf as Oe,owesNext as En,pushTag as ds,refOf as oe,roundOf as Ln,roundSettledBy as fs,HELPER as X,VISUAL as gt,visualAgent as us}from"./domain.mjs";import{GITHUB_STATE as Wa,appendEvent as $t,listBoards as kt,readManifest as ws,samePath as Ft,withManifestLock as ps,writeManifest as ms}from"../hub/boards.mjs";import{readUsageGate as Ua,pauseReason as hs,pauseReasonFor as gs,heldFor as $s,holdsWork as ks,builderPick as Bn,pauseEnd as ys}from"../shared/usage-gate.mjs";import{reviseQueued as bs,pushKindError as xs,readClaudeModel as Cn}from"../hub/push.mjs";import{AT_RE as _s}from"../hub/duration.mjs";import{routeRecord as rt}from"../hub/routes.mjs";import{ticketSource as Ha,resolveTicket as Ss,ticketNamed as Ja,ticketTarget as vs,moveTicket as js,cardIdOf as Os}from"./tickets.mjs";import{LINEAR_NEEDS_KEY as As,linearStatus as Ps,linearWorks as Ds}from"../hub/linear.mjs";import{validateBranch as Fs}from"../hub/repos.mjs";import{withFileLock as se}from"../hub/locks.mjs";import{audit as Ns,auditLines as Rs,boardAudit as Ka,readAudit as Es}from"../shared/audit.mjs";import{merge3 as Ls}from"./merge3.mjs";import{HUB as za,MESSAGE_LIMIT as Ga,BRIEF_LINE_LIMIT as Qa,hubLineOf as Bs,hubText as Va,detailOf as Za}from"./hub-lines.mjs";import{fetchPR as Ya,reportOf as Xa,githubLogin as Cs,postToPR as Is,postReview as Ts,reviewsSince as qs,CLAUDE_PREFIX as Ae,CLAUDE_MARK as In}from"../hub/pr-report.mjs";import{allLanded as Ms,cardPRs as Pe,hasPR as _t,parsePRRef as Tn,prKeyOf as ti,prNames as Nt,prURL as qn,storedPRs as Rt}from"../shared/card-prs.mjs";import{reviewLinks as Ws,topicItems as Us,reviewersOf as Ke}from"./review.mjs";import{readiness as ei}from"../hub/readiness.mjs";import{SlackError as Hs,SLACK_NEEDS_TOKEN as Js,readSlack as Ks,savedSlack as ze,slackDay as Ge,slackUserId as zs}from"../hub/slack.mjs";import"../bin/install.mjs";import{readPermissions as Gs}from"./permissions.mjs";import{validBuildTarget as ni}from"./build-script.mjs";import{parseCheck as Qs,checkText as Vs}from"../shared/owner-check.mjs";import{RuleError as Zs,rulesOf as ai,addRule as Ys,editRule as Xs,removeRule as tr,applyRules as er,readHubRules as Qe,writeHubRules as nr}from"../hub/rules.mjs";import{shareEntries as ar,validLogin as ir}from"../hub/share.mjs";class pt extends Error{static{c(this,"BoardError")}}const d=c(t=>{throw new pt(t)},"fail"),or=c(()=>$.join(Fn.homedir(),".claude","kickflip","sites"),"sitesDir"),Ve=c(()=>$.join(Fn.homedir(),".claude","waves"),"wavesDir"),ii=Symbol("read as"),g=c(async(t,e)=>{let n;try{n=await O.readFile(t,"utf8")}catch{return e}try{const a=JSON.parse(n);return a!==null&&typeof a=="object"&&!Array.isArray(a)?{...a,[ii]:n}:a}catch{return e}},"readJSON"),J=c(async t=>{try{return(await O.readFile(t,"utf8")).split(`
2
+ `).filter(Boolean).map(e=>JSON.parse(e))}catch{return[]}},"readLines");async function Ht(t,e){await O.mkdir($.dirname(t),{recursive:!0});const n=`${t}.${process.pid}.tmp`;await O.writeFile(n,e),await O.rename(n,t)}c(Ht,"replace");const b=c((t,e)=>{const{[ii]:n,...a}=e??{};return n===void 0?Ht(t,JSON.stringify(e,null,2)+`
3
+ `):se(t,async()=>{const i=JSON.parse(n),o=JSON.parse(await O.readFile(t,"utf8").catch(()=>n));await Ht(t,JSON.stringify(Ls(i,a,o),null,2)+`
4
+ `)})},"writeJSON");async function lt(t,e){await O.mkdir($.dirname(t),{recursive:!0}),await O.appendFile(t,JSON.stringify(e)+`
5
+ `)}c(lt,"appendLine");async function jl(t){/^[\w-]+$/.test(t||"")||d(`--site takes a route such as ios-orchestrator, not "${t}"`);const e=$.join(or(),t);return await O.stat(e).then(n=>n.isDirectory(),()=>!1)||d(`no site at ${e}`),e}c(jl,"siteDir");const at=c(async t=>({title:"Board",items:"waves",...t?await g($.join(t,"board.json"),{}):{}}),"boardConfig");async function I(t){const e=await at(t);if(e.items==="waves")try{return(await O.readdir(Ve(),{withFileTypes:!0})).filter(a=>a.isDirectory()).map(a=>({slug:a.name,dir:$.join(Ve(),a.name),file:$.join(Ve(),a.name,"wave.json")}))}catch{return[]}const n=$.resolve(t,e.items);return[{slug:e.slug||$.basename(e.items,".json"),dir:$.dirname(n),file:n}]}c(I,"itemSources");const Z=c((t,e)=>$.join(t,"data","threads",e.replace(/[^\w.-]+/g,"__")+".jsonl"),"threadFile"),B=c(t=>$.join(t,"data","owner.json"),"ownerFile"),dt=c(t=>$.join(t,"data","staged-questions.json"),"stagedFile"),re=c(t=>$.join(t,"data","screening.json"),"screeningFile"),sr=900*1e3,P=c(t=>t||d("this command needs --site <route>"),"needSite"),v=c((t,e)=>typeof t=="string"&&t.trim()?t.trim():d(`${e} must be non-empty text`),"text"),it=c((t,e)=>t===void 0?void 0:e(t),"optional"),rr=/^prototype-[\w-]+\.html(?:[?#]\S*)?$/,cr=/^visuals\/[\w-]+\/[\w-]+\.html$/,Mn=c((t,e)=>typeof t=="string"&&(rr.test(t)||cr.test(t)||/^https?:\/\/\S+$/.test(t))?t:d(`${e} must be an http(s) URL or a site page such as prototype-<topic>.html?q=<id>`),"pageUrl"),Ze=c((t,e)=>typeof t=="boolean"?t:d(`${e} must be true or false`),"bool"),St=c((t,e)=>typeof t=="string"&&/^[\w.-]+$/.test(t)?t:d(`${e} must use only letters, digits, "_", "." and "-"`),"domId"),ce=c((t,e,n)=>{(!t||typeof t!="object"||Array.isArray(t))&&d(`${n} must be a JSON object`);const a=Object.keys(t).filter(i=>!e.includes(i));a.length&&d(`${n} has unknown field ${a.map(i=>`"${i}"`).join(", ")}; allowed: ${e.join(", ")}`)},"onlyKeys");function F(t){const e=/^([^/\s]+)\/([^/\s]+)$/.exec(t||"");return e?{file:e[1],id:e[2]}:d(`expected <file>/<id>, not "${t}"`)}c(F,"parseKey");async function Jt(t,e){const n=(await I(t)).find(i=>i.slug===e)||d(`this board has no item file "${e}"`),a=await g(n.file,null);return(!a||!Array.isArray(a.items))&&d(`${n.file} is not an item file with items[]`),{source:n,itemFile:a}}c(Jt,"loadItemFile");async function x(t,e){const{file:n,id:a}=F(e);let i=await Jt(t,n);!i.itemFile.items.some(s=>s.id===a)&&await _c(t,e)&&(i=await Jt(t,n));const o=i.itemFile.items.find(s=>s.id===a)||d(`item file "${n}" has no item "${a}"`);return{...i,item:o}}c(x,"loadItem");const Ye=["md","html","json"];async function Wn(t,e){const n=new RegExp(`^${e.replace(/[.]/g,"\\.")}(?:~(\\d+))?\\.(${Ye.join("|")})$`);return(await O.readdir(t).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||Ye.indexOf(a.ext)-Ye.indexOf(i.ext)).filter((a,i,o)=>a.n!==o[i-1]?.n)}c(Wn,"replyFilesOf");async function Ol(t,e,n,a){P(t),St(e,"the event id");const i=(await J($.join(t,"data","events.jsonl"))).find(h=>h.id===e);i||d(`no event "${e}" in this site's data/events.jsonl`);const o=i.kind==="ask"?await Qt(t,e):null,s=o?o.key:i.kind==="ask"?`ask:${e}`:i.kind==="item-message"&&typeof i.data?.key=="string"?i.data.key:null;o&&$i(o.item)?(v(n,"the reply"),await ki(t,o,a)):a!==void 0&&d(`reply --title titles the New or answered card an ask became; "${e}" is not one`);const r=$.join(t,"data","replies"),l=await Wn(r,e),f=$.join(r,l.length?`${e}~${l.at(-1).n+1}.md`:`${e}.md`);await Ht(f,v(n,"the reply"));const p=s?await Hn(t,s,n):[],w=i.kind==="wave-triage"?["wave","file"].filter(h=>typeof i.data?.[h]=="string").map(h=>`${h}:${i.data[h]}`):[],u=o?`item:${o.key}`:w[0];return u&&p.push(...await Yi(t,u,"Acted on: replied")),{file:f,lines:p}}c(Ol,"writeReply");const oi=c(t=>$.join(t,"data","working.json"),"workingFile");async function lr(t){return(await g(oi(t),{})).events||{}}c(lr,"readWorking");const Al=["ask","item-message","wave-triage","overnight-triage"];async function Pl(t,e){if(!e.length)return;const n=new Date().toISOString(),a=await lr(t);await b(oi(t),{events:{...a,...Object.fromEntries(e.map(i=>[i,rt(a,i)||n]))}})}c(Pl,"markWorking");const Un=c(t=>$.join(t,"data","push-state.json"),"pushStateFile");async function dr(t,e){P(t);const n=Date.parse((await g(Un(t),{})).last_push);if(Number.isNaN(n))return 0;const a=$.join(t,"data","replies"),i=(await O.readdir(a).catch(()=>[])).filter(s=>Ye.includes($.extname(s).slice(1)));return(await Promise.all(i.map(s=>O.stat($.join(a,s)).then(r=>r.mtimeMs,()=>0)))).filter(s=>s>n&&s<=e.getTime()).length}c(dr,"repliesSincePush");async function fr(t,e){return await b(Un(P(t)),{last_push:e.toISOString()}),Un(t)}c(fr,"recordPush");const ur=$.join($.dirname(as(import.meta.url)),"..","bin","kickflip.mjs");function wr(){if(!process.env.KICKFLIP_PUSH_CMD)return[ts(),ur,"push"];try{const t=JSON.parse(process.env.KICKFLIP_PUSH_CMD);if(Array.isArray(t)&&t.length&&t.every(e=>typeof e=="string"))return t}catch{}throw new pt("KICKFLIP_PUSH_CMD must be a JSON array of strings")}c(wr,"pushCommand");async function Dl(t,e){const n=xs(e.kind);n&&d(n),e.title===void 0&&d("push needs --title");const a=new Date,i=mr(e.body,await dr(t,a)),o={kind:e.kind,title:e.title,body:i||void 0,url:gr(e.url),tag:e.tag},[s,...r]=wr(),l=[...r,...Object.entries(o).flatMap(([w,u])=>u===void 0?[]:[`--${w}`,u]),...e.urgent?["--urgent"]:[],...e.now?["--now"]:[]],f=await new Promise(w=>{Xo(s,l,{encoding:"utf8"},(u,h,m)=>w({error:u,said:`${h||""}${m||""}`.split(`
6
+ `).filter(k=>k.trim())}))}),p=f.error?typeof f.error.code=="number"?f.error.code:1:0;if(p!==0)throw Object.assign(new pt([...f.said,`kickflip push failed (${typeof f.error.code=="number"?`exit ${p}`:f.error.message}); the push was not recorded`].join(`
7
+ `)),{exitCode:p});return await fr(t,a),{lines:f.said}}c(Dl,"sendPush");const pr=/(?:^| · )(and \d+ repl(?:y|ies))$/,mr=c((t,e)=>[t,e&&`and ${e} ${e===1?"reply":"replies"}`].filter(Boolean).join(" \xB7 "),"withReplies"),si=160;function hr(t){const e=String(t).split(`
8
+ `).map(n=>n.replace(/\s+/g," ").trim()).find(Boolean)||"";return e.length>si?`${e.slice(0,si-1).trimEnd()}\u2026`:e}c(hr,"pushLine");function gr(t){if(!t||!t.startsWith("/"))return t;const e=new URL(t,"http://board");return/^#(?:selectedCard|item)=[^&]+$/.test(e.hash)&&(e.hash=`${e.hash}&reply`),/\/[\w-]+\.html$/.test(e.pathname)&&!e.pathname.endsWith("/index.html")&&e.searchParams.set("from","push"),e.pathname+e.search+e.hash}c(gr,"pushUrl");const $r=c((t,e)=>`/${$.basename(t)}/#selectedCard=${encodeURIComponent(e)}&reply`,"threadUrl");async function Hn(t,e,n){const a=hr(n);if(!a)return[];const i=ds(e),o=await bs(i,s=>[a,pr.exec(s)?.[1]].filter(Boolean).join(" \xB7 "),$r(t,e));return o===null?[]:[`queued push ${i} now reads: ${o}`]}c(Hn,"freshenPush");const ri=c(t=>$.join(t,"data","hidden.json"),"hiddenFile"),kr=c(async t=>new Set((await g(ri(t),{events:[]})).events||[]),"readHidden");let ci=Promise.resolve();function Fl(t,e,n){St(e,"the event id");const a=ci.then(async()=>{const i=await kr(t);n?i.add(e):i.delete(e),await b(ri(t),{events:[...i]})});return ci=a.catch(()=>{}),a}c(Fl,"setHidden");const li=c(t=>$.join(t,"data","read.json"),"readMarksFile"),yr=c(async t=>(await g(li(t),{})).cards||{},"readMarks");let di=Promise.resolve();function Nl(t,e,n={}){const a=di.then(async()=>{const i=await yr(t),o=new Date().toISOString(),s=c((l,f)=>String(f||"")>l?f:l,"later"),r=Object.fromEntries([...new Set(e)].sort().map(l=>[l,s(i[l]||o,n[l])]));JSON.stringify(r)!==JSON.stringify(i)&&await b(li(t),{cards:r})});return di=a.catch(()=>{}),a}c(Nl,"markRead");const Jn=20*1024*1024,Kt=c(t=>$.join(t,"data","uploads"),"uploadsDir"),le=c((t,e)=>$.join(Kt(t),".meta",`${e}.json`),"uploadMeta"),fi=/^\d+-[\w.-]+$/,ui=/^[\w.+-]+\/[\w.+-]+$/,wi=c(()=>Object.assign(new pt(`a file can be at most ${Jn/1024/1024} MB`),{status:413}),"tooLarge");async function Rl(t,e,{name:n,type:a,length:i}){if(P(t),i>Jn)throw wi();const o=String(n||"").trim().slice(0,200)||"file",s=$.basename(o).replace(/[^\w.-]+/g,"-").replace(/^[.-]+/,"").slice(-80)||"file",r=String(a||"").split(";")[0].trim().toLowerCase(),l=ui.test(r)?r:"application/octet-stream";await O.mkdir($.dirname(le(t,"x")),{recursive:!0});let f,p;for(let h=Date.now();!p;h++)f=`${h}-${s}`,p=await O.open($.join(Kt(t),f),"wx").catch(m=>m.code==="EEXIST"?null:Promise.reject(m));const w=$.join(Kt(t),f);let u=0;try{for await(const h of e){if(u+=h.length,u>Jn)throw wi();await p.write(h)}}catch(h){throw await p.close(),await O.rm(w,{force:!0}),h}return await p.close(),await b(le(t,f),{name:o,type:l,size:u,at:new Date().toISOString()}),{name:o,url:`uploads/${f}`,path:w,type:l,size:u}}c(Rl,"saveUpload");async function El(t,e){if(!fi.test(e||""))return null;const n=$.join(Kt(t),e),a=await O.stat(n).catch(()=>null);if(!a?.isFile())return null;const i=await g(le(t,e),{});return{path:n,name:i.name||e,type:ui.test(i.type||"")?i.type:"application/octet-stream",size:a.size}}c(El,"readUpload");const pi=["github","slack"],mi=c(async(t,e)=>String(e).startsWith("ask:")?Ee(t,e.slice(4),"the ask"):x(t,e),"checkThreadKey");async function W(t,e,n,a={},{link:i}={}){P(t),await mi(t,e);const o={ts:new Date().toISOString(),who:"claude",kind:za,line:n,text:Va(n,a),...Za(n,a)?{detail:Za(n,a)}:{},...i?{link:i}:{}};return await lt(Z(t,e),o),{line:o,lines:Bs(o).fault?await Hn(t,e,o.text):[]}}c(W,"appendStatus");async function zt(t,e,{text:n,link:a,log:i,who:o="claude",via:s,by:r}){P(t),await mi(t,e),/^[^\n]{1,40}$/.test(o.trim())||d("--who takes claude, love or a reviewer's name"),s!==void 0&&!pi.includes(s)&&d(`--via takes ${pi.join(" or ")}`);const l=s===void 0?Xe(n):v(n,"--text"),f={ts:new Date().toISOString(),who:o.trim(),text:l,link:it(a,w=>Mn(w,"--link")),log:i,via:s,by:r},p=Z(t,e);return await lt(p,f),{file:p,lines:f.who==="claude"&&!s?await Hn(t,e,f.text):[]}}c(zt,"appendMessage");function Xe(t){const e=v(t,"--text");return e.length>Ga&&d(`--text runs to ${e.length} characters, past a message's ${Ga}: rewrite it shorter, or put the detail in an HTML page (board.mjs visual add, or --link to a page) and link it`),e}c(Xe,"messageText");async function Gt(t,e,{command:n,text:a=He[n],who:i="claude",by:o,card:s}){const r={ts:new Date().toISOString(),who:i,command:n,text:a,...o?{by:o}:{},...s?{card:s}:{}};return await lt(Z(t,e),r),{lines:[]}}c(Gt,"appendCommand");const vt=c(t=>$.join(t,"data","ask-links.json"),"askLinksFile");async function jt(t){return(await g(vt(t),{})).links||{}}c(jt,"readAskLinks");async function K(t,e){const{tickets:n}=Ot(await at(t)),a=await I(t),i=/^ask:(.+)$/.exec(e)?.[1];if(i===void 0){const s=/^([^/\s]+)\/([^/\s]+)$/.exec(e);if(!s)return e;const r=a.find(p=>p.slug===s[1]),l=r&&await g(r.file,null);if(!l)return s[2];const f=(l.items||[]).find(p=>p.id===s[2]);return oe(l,f||{id:s[2]},n)}const o=t?rt(await jt(t),i):void 0;if(o)return K(t,o);for(const s of a){const r=await g(s.file,null),l=(Array.isArray(r?.items)?r.items:[]).find(f=>f.from_ask===i);if(l)return oe(r,l,n)}return`task ${i}`}c(K,"cardName");async function Ll(t,e){const n=(await J($.join(t,"data","events.jsonl"))).find(a=>a.id===e);return n?.kind==="ask"?K(t,`ask:${e}`):typeof n?.data?.key=="string"?K(t,n.data.key):`the ${n?.kind} event`}c(Ll,"eventCard");async function Qt(t,e){for(const n of await I(t)){const a=await g(n.file,null),i=(Array.isArray(a?.items)?a.items:[]).find(o=>o.from_ask===e);if(i)return{source:n,itemFile:a,item:i,key:`${n.slug}/${i.id}`}}return null}c(Qt,"askCardOf");const Kn=c(t=>!t.phase&&!t.answered_at&&!mt(t)&&!_t(t)&&!t.filed_held,"stillNew");function zn(t,e){const n=e.filter(i=>i.kind==="ask-edited"&&i.data?.id===t.id&&typeof i.data?.text=="string").sort((i,o)=>String(i.ts).localeCompare(String(o.ts))).at(-1);return String(n?n.data.text:t.data?.text||"").trim().split(`
9
+ `)[0].trim()||(t.data?.files||[]).map(i=>i?.name).filter(Boolean).join(", ")||"Ask"}c(zn,"askTitle");const Bl=c((t,e,n)=>ct(t,async()=>{P(t);const a=await J($.join(t,"data","events.jsonl")),i=n||a.find(l=>l.id===e&&l.kind==="ask")||d(`no ask "${e}" in this site's data/events.jsonl`);if(await Qt(t,e)||await xi(t,e,a))return null;const{source:o,itemFile:s}=await hi(t),r=await gi(t,o,i,zn(i,a));return s.items.push(r),await b(o.file,s),`${o.slug}/${r.id}`}),"fileAsk");async function hi(t){const e=await I(t);for(const n of e){const a=await g(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 at(t)).items==="waves"&&d("this board has no wave yet to file a new task in; start a wave first"),await O.mkdir(e[0].dir,{recursive:!0}),{source:e[0],itemFile:{items:[]}}}c(hi,"askFile");async function gi(t,e,n,a){const i=new Date().toISOString(),o={id:await yo(t),title:a,created:i,lane:"todo",lane_at:i,from_ask:n.id};return o.plan=ln(e.dir,o.id),await dn($.join(e.dir,o.plan),`# ${o.title}
10
+ `),o}c(gi,"newAskCard");const Cl=c(t=>ct(t,async()=>{P(t);const e=c((...m)=>$.join(t,"data",...m),"data"),n=await J(e("events.jsonl")),a=await jt(t),i=new Map(Object.entries(a));for(const m of await I(t))for(const k of(await g(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))),r=(await g(e("ask-progress.json"),{})).asks||{},l=(await g(e("ask-titles.json"),{})).titles||{},f=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 J(Z(t,`ask:${m.id}`));if(k.some(D=>D.line==="ask-moved"))continue;const _=await Promise.all((await Wn(e("replies"),m.id)).map(D=>O.stat(e("replies",D.name)).then(T=>T.mtime.toISOString()))),N=k.filter(D=>(D.who||"claude")==="claude"&&D.kind!=="status").map(D=>String(D.ts)),j=[..._,...N].sort().pop()||[rt(r,m.id)?.at,f.get(m.id)].filter(Boolean).sort().pop();w||=await hi(t);const A=await gi(t,w.source,m,rt(l,m.id)||zn(m,n));A.created=m.ts,j&&Le(A,"done",j),w.itemFile.items.push(A),await b(w.source.file,w.itemFile),i.set(m.id,`${w.source.slug}/${A.id}`),p.push(i.get(m.id))}const u=c(m=>i.get(/^item:ask:(.+)$/.exec(m||"")?.[1]),"cardOf"),h=await g(B(t),null);if(h?.items?.some(m=>u(m.topic))){for(const m of h.items)u(m.topic)&&(m.topic=`item:${u(m.topic)}`);await b(B(t),h)}for(const m of["ask-progress.json","ask-titles.json"])await O.rm(e(m),{force:!0});return p}),"fileAsks"),Il=c((t,e)=>ct(t,async()=>{const n=await Qt(P(t),e);if(!n||!Kn(n.item))return null;const a=await J($.join(t,"data","events.jsonl"));return n.item.title=zn(a.find(i=>i.id===e&&i.kind==="ask"),a),await b(n.source.file,n.itemFile),n.key}),"retitleAskCard");async function Gn(t,{source:e,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 b(e.file,n),a.plan&&await O.rm($.join(e.dir,a.plan),{force:!0});const s=Z(t,i),r=await J(s);return await O.rm(s,{force:!0}),r}c(Gn,"dropCard");const Tl=c((t,e)=>ct(t,async()=>{const n=await Qt(P(t),e);return!n||!Kn(n.item)?null:(await Gn(t,n),n.key)}),"dropAskCard"),$i=c(t=>!t.phase&&!t.closed_at&&!_t(t)&&(!mt(t)||!!t.answered_at),"answerable");async function ki(t,e,n){const{item:a,key:i}=e;a.from_ask||d(`${i} is not a card an ask became; write in its thread with thread ${i} --text`),$i(a)||d(`${i} is sorted (${a.phase||qt[a.lane]}), so it is work: write in its thread with thread ${i} --text`),n!==void 0&&(a.title=v(n,"--title")),a.answered_at||=new Date().toISOString(),await b(e.source.file,e.itemFile)}c(ki,"markAnswered");async function yi(t,e,n){const a=await Qt(t,e);a&&a.key!==n&&d(`ask "${e}" is already card ${a.key}: sort it with item set ${a.key} --phase, or merge it with item relate-ask ${e} <card>`)}c(yi,"refuseFiled");async function ql(t,e,n,a){P(t);const{source:i,itemFile:o,item:s}=await x(t,e),r={source:i,itemFile:o,item:s,key:e};return Xe(n),a!==void 0&&v(a,"--title"),await ki(t,r,a),zt(t,e,{text:n})}c(ql,"answerCard");async function Ml(t,e,n,a,i){P(t),St(e,"the event id"),(await J($.join(t,"data","events.jsonl"))).some(l=>l.id===e&&l.kind==="ask")||d(`no ask "${e}" in this site's data/events.jsonl`),await Qn(t,e),await x(t,n),v(a,"--summary"),i!==void 0&&v(i,"--title");const s=await Qt(t,e);if(s&&s.key===n&&d(`ask "${e}" is ${n} already`),s&&!Kn(s.item)&&d(`ask "${e}" 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 ct(t,()=>Gn(t,s)),f=Z(t,`ask:${e}`);for(const p of l)await lt(f,p);await lt(f,{ts:new Date().toISOString(),who:"claude",kind:za,line:"card-merged",text:Va("card-merged",{card:await K(t,n)}),into:n,title:i??s.item.title,link:`#selectedCard=${encodeURIComponent(n)}`})}const r=await jt(t);return await b(vt(t),{links:{...r,[e]:n}}),await zt(t,n,{text:a}),vt(t)}c(Ml,"relateAsk");async function Wl(t,e){await Ee(t,e,"the ask");const n=await jt(t);return rt(n,e)===void 0&&d(`ask "${e}" is not folded into a card`),delete n[e],await b(vt(t),{links:n}),vt(t)}c(Wl,"unfoldAsk");const bi=c(async(t,e)=>(await J(Z(t,`ask:${e}`))).find(n=>n.line==="ask-moved"),"movedLine");async function Qn(t,e){const n=await bi(t,e);n&&d(`ask "${e}" was moved to ${n.link}; it lives there now`)}c(Qn,"refuseMoved");async function xi(t,e,n){const a=`ask "${e}"`;if(n.some(l=>l.kind==="ask-withdrawn"&&l.data?.id===e))return{why:`${a} was deleted by the owner`};const i=await bi(t,e);if(i)return{why:`${a} was already moved to ${i.link}`};const o=rt(await jt(t),e);if(o)return{why:`${a} is folded into ${o}; run ask unfold ${e} first`};const s=[];for(const l of await I(t)){const f=await g(l.file,null);for(const p of Array.isArray(f?.items)?f.items:[])p.from_ask===e&&s.push({key:`${l.slug}/${p.id}`,it:p})}const r=s.find(({it:l})=>_t(l)||mt(l)||l.answered_at);return r?{why:`${a} became ${r.key}, which has started or been answered`}:s.length?{items:s.map(l=>l.key)}:null}c(xi,"askNotNew");async function br(t,e,n){const a=/^uploads\/([\w.-]+)$/.exec(n?.url||"")?.[1],i=fi.test(a||"")?$.join(Kt(t),a):null;if(!i||!await O.stat(i).then(l=>l.isFile(),()=>!1))return{name:String(n?.name||a||"file")};await O.mkdir($.dirname(le(e,"x")),{recursive:!0});const o=a.replace(/^\d+-/,"");let s;for(let l=Date.now();s=`${l}-${o}`,!await O.copyFile(i,$.join(Kt(e),s),Zo.COPYFILE_EXCL).then(()=>!0,p=>p.code==="EEXIST"?!1:Promise.reject(p));l++);const r=await g(le(t,a),null);return r&&await b(le(e,s),r),{...n,url:`uploads/${s}`,path:$.join(Kt(e),s)}}c(br,"copyUpload");async function Ul(t,e,n){P(t),St(e,"the event id"),(typeof n!="string"||!n.trim())&&d("ask move needs --to <route>, the board to move it to");const a=await J($.join(t,"data","events.jsonl")),i=a.find(k=>k.id===e&&k.kind==="ask")||d(`no ask "${e}" in this site's data/events.jsonl`),o=kt(),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"}`);Ft(s.dir,t)&&d(`--to ${n}: that is this board`);const r=await xi(t,e,a);r?.why&&d(r.why);const l=a.filter(k=>k.kind==="ask-edited"&&k.data?.id===e&&typeof k.data?.text=="string").sort((k,_)=>String(k.ts).localeCompare(String(_.ts))),f=l.at(-1),p=[];for(const k of[...i.data?.files||[],...l.flatMap(_=>_.data.files||[])])p.push(await br(t,s.dir,k));const w={route:$.basename(t),title:(await at(t)).title,id:e},u=f?f.data.text:i.data?.text||"",h=await $t(s,"ask",{text:u,...p.length?{files:p}:{},from:w}),m=`/${n}/#selectedCard=${encodeURIComponent(`ask:${h}`)}`;await W(t,`ask:${e}`,"ask-moved",{board:s.title},{link:m});for(const k of r?.items||[]){const{source:_,itemFile:N,item:j}=await x(t,k);await ct(t,()=>Gn(t,{source:_,itemFile:N,item:j,key:k}))}return{file:$.join(s.dir,"data","events.jsonl"),lines:[`moved to ${m}`,...(r?.items||[]).map(k=>`dropped: ${k}`)]}}c(Ul,"moveAsk");async function Hl(t,e,{state:n,text:a}={}){return P(t),["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"),Si(t,await _i(t,e,"review record"),{state:n,text:a})}c(Hl,"recordReview");async function _i(t,e,n){/^[1-9]\d{0,6}$/.test(String(e))||d(`${n} takes the PR number, such as 437, not "${e}"`);const a=await g($.join(t,"data","incoming.json"),{});return{...(a?.prs||[]).find(s=>s.number===Number(e))||(await g($.join(t,"data","reviews.json"),{}))[e]||d(`PR #${e} is not an incoming PR on this board`),repo:a?.repo||ut(t,{})}}c(_i,"incomingPR");async function Si(t,{number:e,title:n,author:a,url:i,touches:o=[],asked:s=!1,head:r},{state:l,text:f,review_url:p}){const w=$.join(t,"data","reviews.json"),u=await g(w,{});return u[e]={number:Number(e),title:n,author:a,url:i,touches:o,asked:s,head:r,state:l,text:f,...p?{review_url:p}:{},at:new Date().toISOString()},await b(w,u),{file:w,lines:[]}}c(Si,"saveReview");async function Jl(t,e,{words:n,readAt:a}){P(t);const i=await _i(t,e,"review post"),o=String(n??"").trim()||d("review post needs the review text, as --file"),s=`${Ae}Claude review from the ${(await at(t)).title} board`;(o.startsWith(Ae.trim())||o.includes("Claude review from")||o.includes(In))&&d(`review post adds the ${Ae.trim()} header and the claude-bridge marker itself; pass only your words`);const r=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=Ot(await at(t)).github_login;try{const f=await qs({repo:r,pr:i.number,since:a});f.length&&d(`PR #${e} got ${f.length===1?"a review":`${f.length} reviews`} after ${a} (${f.map(w=>`${w.login} at ${w.at}`).join(", ")}); reread the PR and review again`);const p=await Ts({repo:r,pr:i.number,head:i.head,body:`${s}
11
11
 
12
12
  ${o}
13
13
 
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*$/,`
14
+ ${In}`,login:l});return await Si(t,i,{state:"posted",text:o,review_url:p}),{lines:[`posted review on PR #${e}: ${p}`]}}catch(f){if(f instanceof pt)throw f;d(f.message)}}c(Jl,"postIncomingReview");const tn=c(t=>$.join(t,"data","handshake.json"),"handshakeFile");async function vi(t){const e=await g(tn(t),null);if(!e?.hello)return e;const{hello:n,...a}=e;return{...a,ping:a.ping||n}}c(vi,"readHandshake");async function Kl(t,e){P(t),St(e,"the event id");const n=(await J($.join(t,"data","events.jsonl"))).find(i=>i.id===e);n?.kind!=="ping"&&n?.kind!=="hello"&&d(`no ping event "${e}" in this site's data/events.jsonl`);const a=await vi(t)||{};return a.ping?.id&&a.ping.id!==e&&d(`"${e}" is an older ping; ack the newest, "${a.ping.id}"`),await b(tn(t),{...a,ping:a.ping||{id:e,at:n.ts,session:null},ack:{id:e,at:new Date().toISOString()}}),tn(t)}c(Kl,"ackPing");async function zl(t){const e=await vi(t);e&&!e.done_at&&await b(tn(t),{...e,done_at:new Date().toISOString()})}c(zl,"finishHandshake");const Vn={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})$/},de=c((t,e)=>t.trim().toLowerCase()===e.trim().toLowerCase(),"sameName");function Zn(t,e){return Array.isArray(t)||d(`${e} must be a JSON array of reviewers`),t.forEach((n,a)=>{const i=`${e}[${a}]`;ce(n,Object.keys(Vn),i);for(const[o,s]of Object.entries(Vn))(typeof n[o]!="string"||!s.test(n[o].trim()))&&d(`${i}.${o} must match ${s}`);t.slice(0,a).some(o=>de(o.name,n.name))&&d(`${e} names "${n.name}" twice`)}),t.map(n=>Object.fromEntries(Object.keys(Vn).map(a=>[a,n[a].trim()])))}c(Zn,"checkReviewers");async function Yn(t,e){const{file:n,reviewers:a}=await Fe(t,o=>{const s=Zn(e(o.reviewers||[]),"the reviewers");return{set:{reviewers:s},reviewers:s}}),i=[];for(const o of await I(t)){const s=await g(o.file,null);(s?.items||[]).some(r=>!G(r))&&(await b(o.file,{...s,reviewers:a}),i.push(`item file ${o.slug}: reviewers copied`))}return{file:n,lines:i}}c(Yn,"writeReviewers");const Gl=c((t,e)=>Yn(t,()=>Zn(e,"the reviewer file")),"setReviewers"),en=c(t=>$.join(P(t),"data","pr-slack.json"),"prSlackFile"),Xn=c(t=>g(en(t),{}),"readPRSlack"),nn=c(t=>!!(t?.ts||t?.posting_at),"held");async function Et(t,e){return se(t,async()=>{const n=JSON.parse(await O.readFile(t,"utf8").catch(()=>"{}")),a=e(n);return await Ht(t,JSON.stringify(n,null,2)+`
15
+ `),a})}c(Et,"editJSON");async function an(t,e,n){await Et(t,i=>{const o=e(i);nn(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 Et(t,o=>{delete e(o).posting_at}),i}try{await Et(t,i=>{const o=e(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}c(an,"postOnce");const Ql=c((t,e,n,a)=>an(en(t),i=>{const o=i[e.pr]||={links:{},replies:[]};return Object.assign(o.links[e.reviewer]||={reactions:[]},{channel:n.channel,thread_ts:n.ts})},a),"postLink"),Vl=c((t,e)=>Et(en(t),n=>{const a=n[e.pr],i=e.reply?a.replies.find(o=>o.id===e.reply).to[e.reviewer]:a.links[e.reviewer];i.reactions=[...new Set([...i.reactions||[],e.emoji])]}),"recordReaction");function xr(t,e,n){const[a,i]=e.split("#");return`<@${t}> <${qn({repo:a,number:i})}|${a.split("/").pop()}#${i}> ${n}`}c(xr,"aboutPR");function _r(t,e){const n=Number.isInteger(t.additions)?` (+${t.additions}/-${t.deletions}, ${t.files} file${t.files===1?"":"s"})`:"";return`ready for review: ${t.title||e.title}${n}`}c(_r,"linkText");const ji=c(t=>!!t?.approved||t?.reviewDecision==="APPROVED","approved");async function Zl(t,e){P(t);const n=await ei.waiting("slack");if(n)return{waiting:n,links:[],reactions:[],digest:null};const a=await at(t),i=await g(Wa,{}),o=await Xn(t),s=[];for(const f of await I(t)){const p=await g(f.file,{}),w=Ke(p,a.reviewers);for(const u of p.items||[])if(u.lane!=="done")for(const h of Pe(u,ut(t,p))){const m=ti(h),k=rt(i.prs,m);if(!(k?.state!=="OPEN"||k.isDraft))for(const _ of w)nn(o[m]?.links?.[_.name])||s.some(N=>N.pr===m&&N.reviewer===_.name)||s.push({pr:m,key:`${f.slug}/${u.id}`,reviewer:_.name,slack_user:_.slack_user,text:xr(_.slack_user,m,_r(k,u))})}}const r=[];for(const[f,p]of Object.entries(o)){const w=rt(i.prs,f),u=[ji(w)&&"git_approved",w?.state==="MERGED"?"git_merged":w?.state==="CLOSED"&&"x"].filter(Boolean),h=c((m,k,_)=>{if(k.ts)for(const N of u)k.reactions?.includes(N)||r.push({pr:f,reviewer:m,..._?{reply:_}:{},channel:k.channel||p.links[m].channel,ts:k.ts,emoji:N})},"react");for(const[m,k]of Object.entries(p.links))h(m,k);for(const m of p.replies)for(const[k,_]of Object.entries(m.to))h(k,_,m.id)}const l=await Sr(t,e);return{date:Ge(e?.now).date,links:s,reactions:r,digest:l.due?l:null}}c(Zl,"reviewDue");const Vt=c(t=>$.join(P(t),"data","review-digests.json"),"digestFile");async function Sr(t,{now:e=new Date}={}){P(t);const n=await at(t),a=await g(Wa,{}),i=await Xn(t),o=Us(),s=new Map;for(const u of await I(t)){const h=await g(u.file,{}),m=Ke(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 _=`${u.slug}/${k.id}`,N=Ws(h,k,_,o);for(const j of Pe(k,ut(t,h))){const A=ti(j),D=rt(a.prs,A);if(!(D?.state!=="OPEN"||D.isDraft||D.failing?.length||ji(D)))for(const T of m){const M=i[A]?.links?.[T.name]?.ts||rt(N,T.name)?.ts||null,S=M?(e.getTime()-Number(M)*1e3)/864e5:null,L=S>2?Math.floor(S):null;s.get(T.name).items.push({key:_,pr:A,title:k.title,url:qn(j),sent_ts:M,...L?{days_old:L}:{},text:`${qn(j)} \xB7 ${k.title}${L?` (${L} days old)`:""}`})}}}}const{date:r,open:l}=Ge(e),f=((await g(Vt(t),{})).digests||[]).filter(u=>u.date===r),p=await ei.waiting("slack");for(const u of f)s.has(u.reviewer)||s.set(u.reviewer,{name:u.reviewer,items:[]});const w=[...s.values()].map(u=>{const h=f.find(k=>k.reviewer===u.name),m=h?.replies?!h.posting_at&&(!h.ts||h.replies.some(k=>!nn(k))):!h?.posting_at&&u.items.length>0;return{...u,owed:m,...h?{sent:h}:{}}}).filter(u=>u.items.length||u.sent);return{date:r,due:!p&&l&&w.some(u=>u.owed),...p?{waiting:p}:{},reviewers:w}}c(Sr,"reviewDigest");const De=c((t,e)=>n=>(n.digests||[]).find(a=>a.date===t&&a.reviewer===e),"dayEntry"),vr=7,Oi=c((t,e)=>(Date.parse(e)-Date.parse(t.date))/864e5<vr,"heardOn");function Ai(t,e,n,a){De(e,n)(t)||(t.digests=[...(t.digests||[]).filter(i=>Oi(i,e)),{date:e,reviewer:n,...a}],delete t.last_date)}c(Ai,"addDayEntry");async function Yl(t,e=new Date){const{date:n}=Ge(e);return((await g(Vt(t),{})).digests||[]).filter(a=>a.ts&&Oi(a,n))}c(Yl,"heardThreads");const Xl=c((t,e,n)=>Et(Vt(t),a=>{De(e.date,e.reviewer)(a).heard=n}),"markHeard");async function jr(t){const e=new Set;for(const n of Object.values(await Xn(t))){for(const a of Object.values(n.links||{}))e.add(a.ts);for(const a of n.replies||[])for(const i of Object.values(a.to))e.add(i.ts)}for(const n of(await g(Vt(t),{})).digests||[])for(const a of[n,...n.replies||[]])e.add(a.ts);return e.delete(void 0),e}c(jr,"boardPosts");const Pi="\u{1F916} ",Di=250;function Fi(t){const e=v(t,"--text");return e.length>Di&&d(`--text runs to ${e.length} characters, past the orchestrator's ${Di}: keep it brief`),`${Pi}${e}`}c(Fi,"orchestratorWords");async function ta(t,e){return St(e,"the event id"),(await J($.join(P(t),"data","events.jsonl"))).find(a=>a.id===e&&a.kind==="slack-question")?.data||d(`no slack-question "${e}" in this site's data/events.jsonl`)}c(ta,"slackQuestion");const on=c(t=>t||d(Js),"throughSlack"),fe=c(t=>t.catch(e=>e instanceof Hs?d(e.message):Promise.reject(e)),"slackCall");async function td(t,e,n=ze()){const a=await ta(t,e),i=await jr(t),o=zs();return{lines:(await fe(on(n).replies({channel:a.channel,ts:a.thread_ts}))).flatMap(l=>{const f=i.has(l.ts)||l.user===o&&l.text?.startsWith(Pi)?"board":l.user===o?"you":a.reviewer,[p,...w]=String(l.text||"").split(`
16
+ `);return[`${l.ts===a.ts?"\u2192 ":""}${f}: ${p}`,...w.map(u=>` ${u}`)]})}}c(td,"slackThread");async function ed(t,e,n,a=ze()){const i=await ta(t,e);i.answer||d("that message is the owner's and names no @kickflip, so it expects no reply");const o=Fi(n);return await fe(on(a).post({channel:i.channel,thread_ts:i.thread_ts,text:o})),{lines:[`answered in ${i.reviewer}'s day thread`]}}c(ed,"slackAnswer");async function nd(t,e,n,{now:a=new Date}={},i=ze()){P(t);const o=Fi(n),s=await at(t),r=new Map;for(const h of await I(t))for(const m of Ke(await g(h.file,{}),s.reviewers))r.set(m.name,m);const l=[...r.values()].find(h=>de(h.name,e))||d(`no reviewer "${e}" on this board; its reviewers are ${[...r.keys()].join(", ")||"none"}`),f=on(i),{date:p}=Ge(a),w=De(p,l.name)(await g(Vt(t),{})),u=await fe(Or(t,p,l.name,{channel:w?.channel||await fe(f.openDM(l.slack_user)),header:`${s.title} reviews`,post:c(h=>f.post(h),"post")}));return await fe(f.post({channel:u.channel,thread_ts:u.ts,text:o})),{lines:[`said in ${l.name}'s day thread`]}}c(nd,"slackSay");async function ad(t,e,n,{title:a,notes:i,notesFrom:o},s=ze()){const r=await ta(t,e),f=(await fe(on(s).replies({channel:r.channel,ts:r.thread_ts}))).find(h=>h.ts===r.ts)||d("that Slack message is gone from its thread"),p=await kc(t,n,{title:a,notes:i,notesFrom:o,phase:"grilling",intake:!0}),w=Ks().slack_domain,u=w?`https://${w}.slack.com/archives/${r.channel}/p${r.ts.replace(".","")}?thread_ts=${r.thread_ts}&cid=${r.channel}`:void 0;return await zt(t,p.key,{text:f.text,who:r.from==="owner"?"love":r.reviewer,via:"slack",link:u}),{...p,lines:[`filed ${p.key} grilling, for the owner's review`]}}c(ad,"slackTask");async function Or(t,e,n,{channel:a,header:i,post:o}){const s=Vt(t),r=De(e,n),l=await Et(s,f=>(Ai(f,e,n,{channel:a}),{...r(f)}));return{channel:l.channel,ts:l.ts||await an(s,r,()=>o({channel:l.channel,text:i}))}}c(Or,"openDayThread");async function id(t,e,n,{channel:a,header:i,post:o}){const s=Vt(t),r=De(e,n.name),l=`<@${n.slack_user}>`;await Et(s,u=>{Ai(u,e,n.name,{channel:a});const h=r(u);h.replies||(h.replies=h.ts?[{prs:n.items.map(m=>m.pr),text:[`${l} ${i(n.items.length)}`,...n.items.map(m=>m.text)].join(`
17
+ `)}]:n.items.map(m=>({pr:m.pr,text:`${l} ${m.text}`})))});const f=r(await g(s,{})),p=f.ts||await an(s,r,()=>o({channel:f.channel,text:i(f.replies.length)}));for(const[u,h]of f.replies.entries())nn(h)||await an(s,m=>r(m).replies[u],()=>o({channel:f.channel,thread_ts:p,text:h.text}));const w=r(await g(s,{})).replies.filter(u=>u.pr&&u.ts);w.length&&await Et(en(t),u=>{const h=`digest-${e}`;for(const m of w){const k=u[m.pr]||={links:{},replies:[]},_=k.replies.find(N=>N.id===h)||k.replies[k.replies.push({id:h,text:m.text,to:{}})-1];_.to[n.name]||={channel:f.channel,ts:m.ts,reactions:[]}}})}c(id,"postDigest");function od(t,e){const n=Zn(Array.isArray(e)?e:[e],"the reviewer file");return Yn(t,a=>[...a.map(i=>n.find(o=>de(o.name,i.name))||i),...n.filter(i=>!a.some(o=>de(i.name,o.name)))])}c(od,"addReviewers");function sd(t,e){return Yn(t,n=>{const a=e.filter(i=>!n.some(o=>de(o.name,i)));return a.length&&d(`this board has no reviewer ${a.map(i=>`"${i}"`).join(", ")}`),n.filter(i=>!e.some(o=>de(i.name,o)))})}c(sd,"removeReviewers");const Ar=c(()=>$.join(Fn.homedir(),".claude","kickflip","settings.json"),"hubSettingsFile"),ea=c(t=>{try{return t()}catch(e){throw e instanceof Zs&&d(e.message),e}},"ruleCall"),na=c(async t=>ai(await g($.join(t,"board.json"),{})),"readBoardRules");let Ni=Promise.resolve();function Fe(t,e){const n=Ni.then(async()=>{const a=$.join(P(t),"board.json"),i=await g(a,{}),{set:o,...s}=e(i);return await b(a,{...i,...o}),{file:a,...s}});return Ni=n.catch(()=>{}),n}c(Fe,"writeBoardJSON");const Ri=c((t,e)=>Fe(t,n=>{const a=ea(()=>e(ai(n)));return{set:{rules:a.rules},...a}}),"writeBoardRules");function rd(t,e){const n=Object.keys(e||{}).sort().join(),a=n==="level,login",i=a?e.login:e?.[n],o=typeof i=="string"?i.trim():null;(!["add","remove","level,login"].includes(n)||!ir(o))&&d("Send add or remove with one login, like name@github, or a listed login with its level."),a&&!["view","full"].includes(e.level)&&d("A level is view or full.");const s=c(({login:r,level:l})=>l==="full"?{login:r,level:l}:r,"stored");return Fe(t,r=>{const l=ar(r);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:e.level}:w):l.some(w=>w.login===o)?l:[...l,{login:o,level:"view"}]).map(s);return{set:{share:p},share:p}}).then(({share:r})=>({share:r}))}c(rd,"saveBoardShare");const Ei=c(t=>typeof t=="string"&&/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(t),"validGithubLogin"),Li=c(t=>{try{return t?Ha(t):null}catch{return null}},"storedTickets"),aa=c(t=>Number.isInteger(t)&&t>=1&&t<=99,"validCap"),ia=["max_implementers","max_planners"],Ot=c((t,e)=>{const n=Li(t?.tickets);return{auto_merge:t?.auto_merge===!0,auto_build:t?.auto_build===!0,auto_pull:t?.auto_pull===!0,max_implementers:aa(t?.max_implementers)?t.max_implementers:is,max_planners:aa(t?.max_planners)?t.max_planners:os,auto_triage:t?.auto_triage===!0,auto_review:t?.auto_review===!0,idle_notification:t?.idle_notification===!0,skip_draft:t?.skip_draft===!0,prs:e?.hub?t?.prs===!0:t?.prs!==!1,...n?{tickets:n}:{},...Ei(t?.github_login)?{github_login:t.github_login}:{},...ni(t?.build_target)?{build_target:t.build_target}:{}}},"settingsOf");async function cd(t,e,n=new Date){let a;try{a=ys(e,n)}catch(i){d(i.message)}return await Fe(t,()=>({set:{paused_until:a??void 0}})),a}c(cd,"setBoardPause");async function Pr(t){const e=P(t);return Ot(await g($.join(e,"board.json"),{}),kt().find(n=>Ft(n.dir,e)))}c(Pr,"readBoardSettings");const ld=c((t,e)=>!!t?.github&&(!t.local||t.hub===!0)&&e.prs,"opensPRs");async function ue(t,e){ce(e,["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(e).length||d("the settings name nothing to change"),e={...e};for(const[n,a]of Object.entries(e))if(n==="tickets"){try{e.tickets=Ha(a)??void 0}catch(o){d(o.message)}e.tickets?.source==="linear"&&!Ds()&&d(As);const i=e.tickets?.source==="github"&&t?kt().find(o=>Ft(o.dir,t)):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?e.github_login=void 0:Ei(a)||d("github_login must be a GitHub login"):n==="build_target"?a===null?e.build_target=void 0:ni(a)||d('build_target must name .kickflip/build-<target>: letters, digits, ".", "_" or "-", or null for .kickflip/build'):ia.includes(n)?aa(a)||d(`${n} must be a whole number from 1 to 99`):Ze(a,n);if(e.prs===!1){const n=t?kt().find(a=>Ft(a.dir,t)):null;n?.github&&!n.local&&!(await Gs(n.owner?.repo)).pushMain&&d("Merge lands each card on main and pushes it, so turn on Push main after a land first")}return Fe(t,n=>{const a=t?kt().find(s=>Ft(s.dir,t)):null,i=Ot(n,a),o=Ot({...n,...e},a);return{set:e,settings:o,changed:Object.keys(e).filter(s=>JSON.stringify(i[s])!==JSON.stringify(o[s]))}})}c(ue,"saveBoardSettings");async function dd(){if(Ps().set)return[];const t=[];for(const e of kt())(await Pr(e.dir)).tickets?.source==="linear"&&(await ue(e.dir,{tickets:{source:"none"}}),await $t(e,"settings",{tickets:null}),t.push(`/${e.route} fell back to no ticket source: Hub has no Linear key`));return t}c(dd,"fallBackFromLinear");async function fd(t,e,n){const a=String(e).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 "${e}"; settings are ${i.map(r=>r.replace(/_/g,"-")).join(", ")}`),a==="tickets"){let r;try{r=JSON.parse(n)}catch{d('settings set tickets takes the source as JSON, such as {"source":"github"}')}const{file:l,settings:f}=await ue(t,{tickets:r});return{file:l,lines:Dr({tickets:f.tickets})}}if(a==="github_login"){const{file:r,settings:l}=await ue(t,{github_login:n});return{file:r,lines:[`github-login ${l.github_login||"unset"}`]}}if(a==="build_target"){const{file:r,settings:l}=await ue(t,{build_target:n==="default"?null:n});return{file:r,lines:[`build-target ${l.build_target||"default"}`]}}if(ia.includes(a)){const{file:r,settings:l}=await ue(t,{[a]:/^\d+$/.test(n)?Number(n):n});return{file:r,lines:[`${e} ${l[a]}`]}}["on","off"].includes(n)||d(`settings set ${e} takes on or off, not "${n}"`);const{file:o,settings:s}=await ue(t,{[a]:n==="on"});return{file:o,lines:[`${e} ${s[a]?"on":"off"}`]}}c(fd,"setBoardSetting");const Dr=c(t=>Object.entries(t).map(([e,n])=>`setting ${e.replace(/_/g,"-")} ${e==="tickets"?JSON.stringify(n??{source:"none"}):e==="github_login"||ia.includes(e)?n:e==="build_target"?n||"default":n?"on":"off"}`),"settingsLines");async function oa(t,e,n){const a=e?ea(()=>n(Qe())):await Ri(t,n);return e&&ea(()=>nr(a.rules)),{file:e?Ar():a.file,lines:[`id ${a.rule.id}`]}}c(oa,"changeRules");async function Bi(t,e,n){const a=c(s=>s.map(r=>r.id),"ids");n||P(t);const[i,o]=n?[a(Qe()),t?a(await na(t)):[]]:[a(await na(t)),a(Qe())];i.includes(e)||d(`no ${n?"hub":"board"} rule "${e}"${o.includes(e)?`; it is a ${n?"board":"hub"} rule, so ${n?"drop":"pass"} --hub`:""}`)}c(Bi,"checkRuleId");async function ud(t){const e=c(n=>a=>`${n} ${a.id} ${a.by==="claude"?"claude":"owner"} ${a.text}`,"line");return{lines:[...Qe().map(e("hub")),...t?(await na(t)).map(e("board")):[]]}}c(ud,"listRules");function wd(t,{text:e,hub:n=!1,by:a}){return n||P(t),oa(t,n,i=>Ys(i,{text:e,by:a}))}c(wd,"addRuleTo");async function pd(t,e,{text:n,hub:a=!1}){return await Bi(t,e,a),oa(t,a,i=>Xs(i,e,n))}c(pd,"editRuleIn");async function md(t,e,{hub:n=!1}){return await Bi(t,e,n),oa(t,n,a=>tr(a,e))}c(md,"removeRuleFrom");const hd=c((t,e,n)=>Ri(t,a=>er(a,e,n)),"saveBoardRules");async function gd(t,{id:e,title:n,summary:a}){P(t);const i=await g(B(t),{topics:[],items:[]}),o=i.topics||[];return o.some(s=>s.id===v(e,"the topic id"))&&d(`topic "${e}" already exists`),o.push({id:e.trim(),title:v(n,"--title"),...a===void 0?{}:{summary:v(a,"--summary")}}),await b(B(t),{...i,topics:o,items:i.items||[]}),B(t)}c(gd,"addTopic");const Ci=["quiet","info","notice","danger","success"];function Fr(t,e){if(t.diagram===void 0!=(t.practice===void 0)&&d(`${e} needs diagram and practice together`),t.diagram===void 0)return;const n=t.diagram;(!Array.isArray(n)||!n.length)&&d(`${e}.diagram must be a non-empty array of rows`),n.forEach((a,i)=>{(!Array.isArray(a)||!a.length||a.length>4)&&d(`${e}.diagram[${i}] must be a row of 1 to 4 boxes`),a.forEach((o,s)=>{const r=`${e}.diagram[${i}][${s}]`;ce(o,["text","tone","to"],r),v(o.text,`${r}.text`),it(o.tone,l=>Ci.includes(l)?l:d(`${r}.tone takes ${Ci.join(", ")}`)),it(o.to,l=>Array.isArray(l)&&i+1<n.length&&l.every(f=>Number.isInteger(f)&&f>=0&&f<n[i+1].length)?l:d(`${r}.to must list boxes of the next row by index`))})}),(!Array.isArray(t.practice)||!t.practice.length)&&d(`${e}.practice must be a non-empty array of lines`),t.practice.forEach((a,i)=>v(a,`${e}.practice[${i}]`))}c(Fr,"checkDiagram");function Nr(t,e){(!Array.isArray(t)||!t.length)&&d(`${e}.options must be a non-empty array`);const n=new Set;t.forEach((a,i)=>{const o=`${e}.options[${i}]`;ce(a,["id","label","recommended","detail","diagram","practice"],o),Fr(a,o),n.has(v(a.id,`${o}.id`))&&d(`${e} has two options with id "${a.id}"`),n.add(a.id),v(a.label,`${o}.label`).startsWith(`${a.id}. `)&&d(`${o}.label starts with "${a.id}. ", but the page adds the option letter itself`),it(a.recommended,s=>Ze(s,`${o}.recommended`)),it(a.detail,s=>typeof s=="string"?s:d(`${o}.detail must be text`))}),t.filter(a=>a.recommended).length>1&&d(`${e} recommends more than one option`)}c(Nr,"checkOptions");function Rr(t,e,n){ce(t,["option","card","answer","reason"],n),e.some(a=>a.id===v(t.option,`${n}.option`))||d(`${n}.option names no option "${t.option}"`),v(t.card,`${n}.card`),v(t.answer,`${n}.answer`),it(t.reason,a=>v(a,`${n}.reason`))}c(Rr,"checkSuggested");const we=c(t=>$.join(t,"data","prototypes.json"),"prototypesFile"),sa=c((t,e,n)=>$.join(t,"public","prototypes",e,`${n}.js`),"takeFile");async function Ne(t){const e=t?(await g(we(t),{})).prototypes:null;return Array.isArray(e)?e:[]}c(Ne,"readPrototypes");const Er=c(t=>typeof t=="string"&&/^[a-z0-9-]+$/.test(t)?t:d(`a prototype topic uses only a-z, 0-9 and "-", not "${t}"`),"prototypeTopic");function Lr(t){const e=/^([^:]*):(.*)$/s.exec(t);(!e||!/^[A-Z]$/.test(e[1]))&&d(`--take takes KEY:"Label" with KEY one capital letter, not "${t}"`);const n=v(e[2],`--take ${e[1]}'s label`);return/^[A-Z]\.\s/.test(n)&&d(`--take ${e[1]}'s label starts with "${n.slice(0,3)}"; the take pill shows the label alone, so drop the letter`),{key:e[1],label:n}}c(Lr,"prototypeTake");function Br(t){const e=/^([^:]*):(.*)$/s.exec(t);return(!e||!/^[a-z0-9][a-z0-9-]*$/.test(e[1]))&&d(`--state takes KEY:"Label" with KEY in a-z, 0-9 and "-", not "${t}"`),{key:e[1],label:v(e[2],`--state ${e[1]}'s label`)}}c(Br,"prototypeState");const Ii=c(t=>t.find((e,n)=>t.findIndex(a=>a.key===e.key)!==n),"twiceIn"),Lt=c(t=>/^prototype-(.+)\.html$/.exec($.posix.basename(String(t||"").split(/[?#]/)[0]))?.[1],"pageTopic");async function $d(t,{topic:e,title:n,card:a,takes:i=[],served:o,states:s=[]}){P(t),Er(e),v(n,"--title"),i.length||d('prototype add needs a --take KEY:"Label" for each take');const r=i.map(Lr),l=Ii(r);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 f=s.map(Br),p=Ii(f);p&&d(`--state ${p.key} is given twice`),a!==void 0&&await x(t,a);const w=await Ne(t);w.some(m=>m.topic===e)&&d(`prototype ${e} is already recorded; run prototype rm ${e} first, or pick another topic`),await O.stat($.join(t,"public",`prototype-${e}.html`)).then(()=>!0,()=>!1)&&d(`public/prototype-${e}.html is a hand-written page that questions may link; pick another topic`);const u={topic:e,title:n.trim(),...a===void 0?{}:{card:a},...o===void 0?{}:{served:o},takes:r,...f.length?{states:f}:{},at:new Date().toISOString()};await b(we(t),{prototypes:[...w,u]});const h=`page prototype-${e}.html`;return o!==void 0?{file:we(t),lines:[h,`served ${o} from the base prototype worktree (board.mjs prototype base)`,...f.length?[`states ${f.map(m=>m.key).join(", ")}`]:[]]}:{file:we(t),lines:[h,...r.map(m=>`take ${m.key} ${sa(t,e,m.key)}`)]}}c($d,"addPrototype");async function kd(t){P(t);const e=await Ne(t);return{lines:e.length?e.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"]}}c(kd,"listPrototypes");async function yd(t){const e=(await Ne(t)).filter(a=>a.served).map(a=>a.topic);if(!e.length)return[];const n=await g(B(t),{items:[]});return e.filter(a=>(n.items||[]).some(i=>!i.resolved&&Lt(i.url)===a))}c(yd,"servedRoundsOpen");async function bd(t,e){P(t);const n=await Ne(t);n.some(o=>o.topic===e)||d(`no prototype "${e}" is recorded`);const i=((await g(B(t),{items:[]})).items||[]).filter(o=>!o.resolved&&Lt(o.url)===e).map(o=>o.id);return i.length&&d(`prototype ${e} backs open question ${i.join(", ")}; resolve ${i.length===1?"it":"them"} first`),await b(we(t),{prototypes:n.filter(o=>o.topic!==e)}),we(t)}c(bd,"removePrototype");async function Cr(t,e,n){const a=e.url?.split(/[?#]/)[0];if(!a||!$.posix.basename(a).startsWith("prototype-"))return;const i=$.posix.basename(a),o=e.url.split("#")[0].split("?").slice(1).join("?"),s=`${n}.url`;new URLSearchParams(o).get("q")!==e.id&&d(`${s} must be ${i}?q=${e.id} so the pill can pick`);const r=Lt(a),l=(await Ne(t)).find(u=>u.topic===r);l||d(`${s} names ${i}, which is not a recorded prototype; record it with board.mjs prototype add ${r} --title "\u2026" --take A:"\u2026", then write its take files`);const f=l.takes.map(u=>u.key),p=e.options.map(u=>u.id),w=p.map(u=>f.indexOf(u));if(w.some((u,h)=>u<0||h>0&&u<w[h-1])&&d(`${n}.options ids ${p.join(", ")} must be a non-empty subset of ${i}'s takes, in take order: ${f.join(", ")}`),!l.served)for(const u of f)await O.stat(sa(t,r,u)).then(h=>h.isFile(),()=>!1)||d(`${s} take ${u} has no file yet; write ${sa(t,r,u)}`)}c(Cr,"checkPrototype");async function xd(t,e,n=async()=>{}){P(t);const a=Array.isArray(e)?e:[e];a.length||d("the question file is empty");const i=await g(B(t),{topics:[],items:[]}),o=i.items||[],s=await g(dt(t),{}),r=new Set([...o,...Object.values(s).flat()].map(w=>w.id)),l=new Set((i.topics||[]).map(w=>w.id));for(const[w,u]of a.entries()){const h=`question ${u?.id??w+1}`;if(ce(u,["id","topic","source","question","context","url","urgent","options","round_of","reask_of","ask","suggested","prototype_gate","ticket_gate"],h),r.has(St(u.id,`${h}.id`))&&d(`question id "${u.id}" is already used`),r.add(u.id),v(u.topic,`${h}.topic`),u.topic.startsWith("item:ask:")){const m=await Qt(t,await Ee(t,u.topic.slice(9),`${h}.topic`));d(`${h}.topic: an ask's questions go on its card's topic${m?`, item:${m.key}`:""}`)}else u.topic.startsWith("item:")?await x(t,u.topic.slice(5)):u.topic.startsWith("wave:")?await $e(t,u.topic.slice(5)):u.topic.startsWith("file:")?await jc(t,u.topic.slice(5)):l.has(u.topic)||d(`topic "${u.topic}" is not declared; add it with: topic add ${u.topic} --title <t>`);v(u.question,`${h}.question`),u.question.length>Qa&&d(`${h}.question runs to ${u.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"])it(u[m],k=>v(k,`${h}.${m}`));it(u.url,m=>Mn(m,`${h}.url`)),it(u.urgent,m=>Ze(m,`${h}.urgent`)),Nr(u.options,h);for(const m of Object.keys(Re))u[m]!==void 0&&Tr(u[m],u,`${h}.${m}`)&&await qr(t,u,m,h);it(u.suggested,m=>Rr(m,u.options,`${h}.suggested`)),await Cr(t,u,h),u.ask!==void 0&&await Wc(t,u,h),it(u.round_of,m=>{const k=o.find(_=>_.id===m);k||d(`${h}.round_of names no question "${m}" in owner.json`),k.topic!==u.topic&&d(`${h}.round_of names ${m} on topic ${k.topic}; a round stays on its question's topic, ${u.topic}`),Lt(u.url)||d(`${h}.round_of needs a url on a recorded prototype, prototype-<topic>.html?q=${u.id}`)}),it(u.reask_of,m=>{const k=o.find(_=>_.id===m);k||d(`${h}.reask_of names no question "${m}" in owner.json`),k.topic!==u.topic&&d(`${h}.reask_of names ${m} on topic ${k.topic}; a re-ask stays on its question's topic, ${u.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(u=>Lt(u.url)).filter(Boolean)))await n(t,w);const f=[],p=[];for(const w of a){const u=w.topic.startsWith("item:")&&!w.topic.startsWith("item:ask:")?w.topic.slice(5):null,h=u&&await Ti(t,u);h&&((s[u]||=[]).push(w),p.push(w),f.push(`staged: ${w.id} waits for ${u}'s ${h} to finish; the card's staged questions then publish together`))}return p.length&&await b(dt(t),s),p.length===a.length?{file:dt(t),lines:f,staged:p.length}:Mi(t,i,o,a.filter(w=>!p.includes(w)),f)}c(xd,"addQuestions");async function Ti(t,e){const{source:n}=await x(t,e);for(const a of await V(n.dir,F(e).id)){const i=await g(a,null);if(i?.started&&!i.done&&Ut(i)&&i.kind!==gt)return i.kind}return null}c(Ti,"runningPlanner");async function pe(t,e,{screen:n=!1}={}){if(!t||await Ti(t,e))return;const a=await g(dt(t),{});if(!a[e])return;const i=await g(re(t),{});if(n){i[e]||await b(re(t),{...i,[e]:new Date().toISOString()});return}const{[e]:o,...s}=a,r=await g(B(t),{topics:[],items:[]});await Mi(t,r,r.items||[],o,[]),await b(dt(t),s),i[e]&&await b(re(t),sn(i,e))}c(pe,"publishStaged");const sn=c((t,e)=>Object.fromEntries(Object.entries(t).filter(([n])=>n!==e)),"withoutKey");async function qi(t){const e=await g(re(t),{}),n=await g(dt(t),{});return Object.fromEntries(Object.entries(e).filter(([a])=>n[a]).map(([a,i])=>[a,{since:i,ids:n[a].map(o=>o.id)}]))}c(qi,"readScreening");async function _d(t,e,n){P(t);const a=v(n,"--note"),i=await g(dt(t),{}),o=await g(re(t),{}),s=Object.keys(i).find(w=>i[w].some(u=>u.id===e));if(!s)return Ir(t,e,a);o[s]||d(`${e} waits for ${s}'s planner to finish`);const r=i[s].find(w=>w.id===e);r.prototype_gate&&d(`${e} is the card's prototype question, which is the owner's; question publish ${s} passes it on`),r.ticket_gate&&d(`${e} is the card's ticket question, which is the owner's; question publish ${s} passes it on`);const l=new Date().toISOString(),f=await g(B(t),{topics:[],items:[]});await b(B(t),{...f,topics:f.topics||[],items:[...f.items||[],{...r,asked_at:l,resolved:`Settled by the orchestrator: ${a}`,resolved_at:l}]});const p=i[s].filter(w=>w.id!==e);return await b(dt(t),p.length?{...i,[s]:p}:sn(i,s)),p.length||await b(re(t),sn(o,s)),await Bt(t,s,`Answers (orchestrator, ${Zt()})`,`- ${e} (${r.question}): settled by the orchestrator. "${a}"`),B(t)}c(_d,"settleQuestion");async function Ir(t,e,n){const a=await g(B(t),{topics:[],items:[]}),i=(a.items||[]).find(r=>r.id===e&&r.topic?.startsWith("item:"));i||d(`no staged question "${e}"; 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(t,o)).item.recheck_at||d(`${o} waits on no re-check, so its question ${e} stays answered as it is`);const s=new Date().toISOString();return await b(B(t),{...a,items:a.items.map(r=>r===i?{...r,resolved:`Settled by the orchestrator: ${n}`,resolved_at:s}:r)}),await Bt(t,o,`Answers (orchestrator, ${Zt()})`,`- ${e} (${i.question}): settled by the orchestrator. "${n}"`),B(t)}c(Ir,"settleStale");async function Sd(t,e){P(t);const n=(await qi(t))[e]?.ids||d(`${e} has no questions waiting on you to screen`);return await pe(t,e),n}c(Sd,"publishScreened");async function vd(t,e=new Date){const n=Object.entries(await qi(t)).filter(([,a])=>e-Date.parse(a.since)>=sr).map(([a])=>a);for(const a of n)await pe(t,a),await W(t,a,"screen-timeout");return n}c(vd,"publishUnscreened");async function Mi(t,e,n,a,i){const{answers:o,requests:s,reopens:r}=await he(t),l=new Date().toISOString(),f=[],p=[];for(const u of a.filter(h=>h.round_of))u.round=Ln(n.find(h=>h.id===u.round_of))+1,Yt(e,[u.round_of],`Followed by prototype round ${u.round}`,l),p.push(u.round_of);for(const u of a.filter(h=>h.reask_of))Yt(e,[u.reask_of],`Asked again as ${u.id}`,l),p.push(u.reask_of);for(const u of a){const h=Lt(u.url),m=n.filter(_=>_.topic===u.topic).map(_=>Wt(_,o,s,[],r)),k=u.topic.startsWith("item:")?Qi(m):[];for(const _ of m)k.includes(_.id)?(Yt(e,[_.id],`Followed by ${u.id}`,l),p.push(_.id)):!_.resolved&&h&&Lt(_.url)===h&&(Yt(e,[_.id],`Replaced by ${u.id}`,l),f.push(_.id))}const w={...e,topics:e.topics||[],items:[...n,...a.map(u=>({...u,asked_at:l}))]};await b(B(t),w);for(const u of new Set(a.map(h=>ra(h.topic)).filter(Boolean)))await Gr(t,u);return{file:B(t),lines:[...i,...f.map(u=>`replaced: ${u}`),...p.map(u=>`resolved: ${u}`)]}}c(Mi,"publishQuestions");function Tr(t,e,n){return Ze(t,n),t?((!e.topic.startsWith("item:")||e.topic.startsWith("item:ask:"))&&d(`${n} goes on a card's topic, item:<file>/<id>`),e.options?.map(a=>a.id).join()!=="yes,no"&&d(`${n} takes exactly the options yes and no, in that order`),!0):!1}c(Tr,"checkGate");const Re={prototype_gate:"prototype",ticket_gate:"ticket"};async function qr(t,e,n,a){const i=e.topic.slice(5),{item:o}=await x(t,i),s=Re[n];o.gates?.[s]&&d(`${a}.${n}: ${i} already has its ${s} answer, ${o.gates[s]}`),n==="ticket_gate"&&(o.ticket&&d(`${a}.ticket_gate: ${i} already has ticket ${o.ticket}`),await te(t)||d(`${a}.ticket_gate needs a board with a ticket source; with none every card stays a task`))}c(qr,"checkGateFact");async function Mr(t,e,n,a,i){const{source:o,itemFile:s,item:r}=await x(t,e);r.gates={...r.gates,[Re[n]]:a},await b(o.file,s),await Wi(t,e,n,i)}c(Mr,"keepGate");async function Wi(t,e,n,a){const i=await g(dt(t),{}),o=(i[e]||[]).filter(f=>!f[n]);o.length!==(i[e]||[]).length&&await b(dt(t),o.length?{...i,[e]:o}:sn(i,e));const{owner:s,asked:r}=await ft(t,e),l=r.filter(f=>f[n]&&f.state==="open").map(f=>f.id);return l.length&&await me(t,s,l,a),l.length>0}c(Wi,"resolveGate");async function jd(t,e,n){const a=new Map(((await g(B(t),{items:[]})).items||[]).map(r=>[r.id,r]));for(const r of e){const l=a.get(String(r.id)),f=Object.keys(Re).find(h=>l?.[h]);if(!f||!["yes","no"].includes(r.choice))continue;const p=f==="ticket_gate"?`Ticket: ${r.choice}.${r.choice==="yes"?" The board makes it from the card's title and notes.":""}`:`Prototype round: ${r.choice}. The board moves the card on once every question is answered.`;await me(t,await g(B(t),{topics:[],items:[]}),[l.id],p);const w=l.topic.slice(5),u=await x(t,w).catch(()=>null);u&&(await Mr(t,w,f,r.choice,p),f==="ticket_gate"&&r.choice==="yes"&&await Ui(t,w,"making its ticket",async()=>{await yt(t,w,{newTicket:!0},n),await W(t,w,"ticket-made",{ticket:(await x(t,w)).item.ticket})}),f==="prototype_gate"&&r.choice==="yes"&&u.item.phase==="grilling"&&await un(t,w)&&await W(t,w,"prototype-after"))}const i=new Map;for(const r of e){const l=a.get(String(r.id)),f=ra(l?.topic);f&&i.set(f,[...i.get(f)||[],Wr(l,r)])}const o=[],s=[];for(const[r,l]of i)await x(t,r).catch(()=>null)&&await Ui(t,r,"writing them into its notes",async()=>{await Bt(t,r,`Answers (owner, ${Zt()})`,l.join(`
18
+ `));const f=await rn(t,r);f==="prototyping"&&o.push(r),f==="staging"&&s.push(r)});return{prototype:o,staged:s}}c(jd,"settleAnswers");async function rn(t,e,{roundAt:n=!1}={}){const{item:a}=await x(t,e),{asked:i}=await ft(t,e);if(i.some(o=>o.state==="open"))return null;if(a.phase==="grilling"&&a.gates?.prototype==="yes"){await yt(t,e,{phase:"prototyping"});const{source:o,itemFile:s,item:r}=await x(t,e);return r.gates={...r.gates,prototype:"ran"},n&&(r.round_at=new Date().toISOString()),await b(o.file,s),await W(t,e,"prototype-round"),"prototyping"}return await Kr(t,e)?(await da(t,e),"staging"):null}c(rn,"moveOnAnswered");async function Ui(t,e,n,a){try{await a()}catch(i){if(!(i instanceof pt))throw i;await W(t,e,"answers-failed",{what:n,error:i.message})}}c(Ui,"followUp");const ra=c(t=>typeof t=="string"&&t.startsWith("item:")&&!t.startsWith("item:ask:")?t.slice(5):null,"cardOfTopic"),cn=c(t=>String(t).padStart(2,"0"),"pad2"),Zt=c((t=new Date)=>`${cn(t.getMonth()+1)}/${cn(t.getDate())} ${cn(t.getHours())}:${cn(t.getMinutes())}`,"noteStamp");function Wr(t,e){const n=e.choice===null||e.choice===void 0||e.choice===""?null:(t.options||[]).find(i=>i.id===e.choice)?.label??String(e.choice),a=String(e.note||"").trim();return`- ${t.id} (${t.question}): ${n?`${n}.`:"no option picked."}${a?` Note, verbatim: "${a}"`:""}`}c(Wr,"answerLine");const Hi=292,ca=c(t=>String(t).toLowerCase().replace(/[^a-z0-9-]+/g,"-"),"notesBase"),Ur=c((t,e,n)=>se($.join(t,`.${ca(e)}-notes`),n),"notesLock");function ln(t,e){let n=`${ca(e)}-notes.md`;for(let a=2;wt($.join(t,n));a++)n=`${ca(e)}-notes-${a}.md`;return n}c(ln,"freeNotesName");async function dn(t,e){const n=$.join($.dirname(t),`.${$.basename(t)}.${process.pid}.tmp`);await O.rm(n,{force:!0}),await O.writeFile(n,e,{mode:Hi}),await O.rename(n,t)}c(dn,"writeNotesFile");const Hr=c(t=>/\n/.test(v(t,"--heading"))?d("--heading is one line, the section's title"):v(t,"--heading"),"notesHeading"),Jr=c((t,e,n)=>`${t.replace(/\n*$/,`
19
19
  `)}
20
- ## ${t}
20
+ ## ${e}
21
21
  ${n.replace(/^\n+|\s+$/g,"")}
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
- ${v.join(`
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}
22
+ `,"withSection");async function Bt(t,e,n,a){const{source:i,item:{id:o}}=await x(t,e);return Ur(i.dir,o,async()=>{const{source:s,itemFile:r,item:l}=await x(t,e);let f;l.plan?(f=await pn(s.dir,l.plan,{missingOk:!0}),wt(f)||await W(t,e,"notes-missing")):(l.plan=ln(s.dir,l.id),f=$.join(s.dir,l.plan),await b(s.file,r));const p=wt(f)?await O.readFile(f,"utf8"):`# ${l.title}
23
+ `;return await dn(f,Jr(p,Hr(n),a)),l.plan})}c(Bt,"appendNotes");async function Ji(t){const{tickets:e}=Ot(await at(t)),n=[],a=new Map;for(const i of await I(t)){const o=await g(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:oe(o,s,e),title:s.title,created:s.created||"",archived:!1,dir:i.dir,plan:s.plan})}for(const i of t?await It(t):[]){if(!i.item?.plan)continue;const o=a.get(i.file);n.push({key:i.key,ref:oe(o?.itemFile||{},i.item,e),title:i.item.title,created:i.item.created||"",archived:!0,dir:o?.source.dir||$.join(t,"data"),plan:i.item.plan})}return n}c(Ji,"cardsWithNotes");const Ki=c(t=>O.realpath($.resolve(t.dir,t.plan)).catch(()=>null),"realNotes");async function zi(t,e,n,a){if(e==="-")return Yo(0,"utf8");const i=await O.realpath(e).catch(()=>d(`${n} ${e}: no such file`));for(const o of await Ji(t))await Ki(o)===i&&d(`${n} ${e} is the notes file of ${o.ref} "${o.title}" (${o.key}), and a card's notes are its own; ${a(o)}`);return O.readFile(i,"utf8")}c(zi,"refuseCardNotes");const la="write a draft of your own outside the board's data folder";async function Od(t,e,{heading:n,file:a,from:i}){let o;if(i!==void 0){i===e&&d(`${e} can't fold its own notes into itself; pass --from <the other card>`);const[l,f]=await Promise.all([x(t,e),x(t,i)]),[p,w]=await Promise.all([l,f].map(({source:u,item:h})=>h.plan?O.realpath($.resolve(u.dir,h.plan)).catch(()=>null):null));p&&p===w&&d(`${i} and ${e} 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 lc(t,i)).markdown,n??=`Folded from ${await K(t,i)} "${f.item.title}"`}else o=await zi(t,a,"--file",l=>l.key===e?`${la}, holding only the new section`:`${la}, or fold that card's notes in with item notes ${e} --from ${l.key}`);o.trim()||d(`${i??a} is empty`);const s=await Bt(t,e,n,o),r=await fn(t,e,"Recorded in the card's notes");return{file:s,heading:n,lines:r}}c(Od,"addNotes");const Ad=c(t=>ct(t,async()=>{const e=await Ji(t),n=new Map;for(const i of e){const o=await Ki(i);o&&n.set(o,[...n.get(o)||[],i])}const a=[];for(const[i,o]of n){const[s,...r]=o.sort((l,f)=>l.created<f.created?-1:l.created>f.created?1:l.key<f.key?-1:1);for(const l of r.filter(f=>!f.archived)){const{source:f,itemFile:p,item:w}=await Jt(t,F(l.key).file).then(h=>({...h,item:h.itemFile.items.find(m=>m.id===F(l.key).id)})),u=w.plan;w.plan=ln(f.dir,w.id),await dn($.join(f.dir,w.plan),await O.readFile(i,"utf8")),await b(f.file,p),await W(t,l.key,"notes-shared",{card:s.ref}),a.push(`${l.key}: its own notes, ${w.plan}, copied from ${$.normalize(u)}, which ${s.key} keeps`)}await O.chmod(i,Hi)}return a}),"splitSharedNotes");async function Kr(t,e){const{source:n,item:a}=await x(t,e);return mt(a)||_t(a)||a.pushed||(await Promise.all((await V(n.dir,a.id)).map(o=>g(o,null)))).some(o=>o?.started&&!o.done&&o.kind!==gt)||(await ft(t,e)).asked.some(o=>o.state==="open")||(await g(dt(t),{}))[e]?!1:!await fa(t,e)}c(Kr,"stageable");async function da(t,e){const{source:n,itemFile:a,item:i}=await x(t,e);i.phase!=="staging"&&(Object.assign(i,{phase:"staging",staged_at:new Date().toISOString()}),delete i.unfiled,delete i.filed_held,await b(n.file,a),await Gt(t,e,{command:"staged"}))}c(da,"stage");const Gi=c(({text:t,files:e})=>[String(t||"").trim(),...(Array.isArray(e)?e:[]).map(n=>`Attached: ${n?.path||n?.name||n}`)].filter(Boolean).join(`
24
+ `),"messageWords");async function zr(t,e,n){const{item:a}=await x(t,e);a.held&&await mo(t,"card-hold",{key:e,hold:!1});const{owner:i,asked:o}=await ft(t,e),s=o.filter(l=>l.state==="open").map(l=>l.id);s.length&&await me(t,i,s,"Skipped: you tapped Implement");const r=Gi(n);r&&await Bt(t,e,`Implement (owner, ${Zt()}), verbatim`,r),await da(t,e)}c(zr,"stageImplement");async function Pd(t,e,n){const a=Gi(n);a&&await Bt(t,e,`Reply (owner, ${Zt()}), verbatim`,a),await da(t,e)}c(Pd,"stageReply");async function Dd(t,e,n){const{source:a,itemFile:i,item:o}=await x(t,e);!["ready","staging"].includes(o.phase)||mt(o)||(o.phase=n==="prototype"?"prototyping":"grilling",delete o.staged_at,await b(a.file,i))}c(Dd,"planAgain");async function Gr(t,e){const n=await x(t,e).catch(()=>null);!n||!["ready","staging"].includes(n.item.phase)||mt(n.item)||(n.item.phase="grilling",delete n.item.staged_at,await b(n.source.file,n.itemFile),await W(t,e,"new-question"))}c(Gr,"grillAgain");function Qi(t){return t.filter(e=>e.state==="answered").map(e=>e.id)}c(Qi,"answeredOnClaude");function Yt(t,e,n,a=new Date().toISOString()){for(const i of t.items||[])e.includes(i.id)&&Object.assign(i,{resolved:n,resolved_at:a})}c(Yt,"setResolved");async function me(t,e,n,a){return Yt(e,n,a),await b(B(t),e),B(t)}c(me,"markResolved");async function Fd(t,e,n){P(t);const a=await g(B(t),{topics:[],items:[]}),i=(a.items||[]).find(u=>u.id===e);i||d(`no question "${e}" in owner.json`);const{answers:o,requests:s,reopens:r}=await he(t),l=Wt(i,o,s,[],r);if(l.state==="resolved")return B(t);l.state==="requested"&&d(`${e} waits on the prototype the owner asked for; question add with round_of ${e} resolves it`);const f=v(n,"--note");Lt(i.url)&&Qr(a,i,o);const p=await me(t,a,[e],f);await to(t,i.topic);const w=ra(i.topic);return w&&(await x(t,w).catch(()=>null))?.item.phase==="grilling"&&await rn(t,w,{roundAt:!0}),p}c(Fd,"resolveQuestion");async function Nd(t,e,n,a=[]){P(t);const[i,...o]=[...new Set(e)];i||d("question move takes the ids of the questions to merge"),await x(t,n);const s=[n,...a];new Set(s).size!==s.length&&d("question move names a card twice");for(const u of a)await x(t,u);const r=await g(B(t),{topics:[],items:[]}),{answers:l,requests:f,reopens:p}=await he(t);for(const u of[i,...o]){const h=(r.items||[]).find(k=>k.id===u)||d(`no question "${u}" in owner.json`),{state:m}=Wt(h,l,f,[],p);m!=="open"&&d(`${u} is ${m}; question move takes open questions`)}for(const[u,h]of a.entries()){const{item:m}=await x(t,h),k=[...m.deps||[],s[u]].join(",");await yt(t,h,{deps:k,relate:n})}const w=await K(t,n);return Object.assign(r.items.find(u=>u.id===i),{topic:`item:${n}`}),Yt(r,o,`Duplicate of ${w}'s question ${i}`),await b(B(t),r),B(t)}c(Nd,"moveQuestions");async function Rd(t,e){const n=e.filter(({choice:i,note:o})=>!i&&String(o||"").trim()).map(i=>i.id),a=n.length?await g(B(t),{topics:[],items:[]}):null;a?.items?.some(i=>n.includes(i.id))&&(Yt(a,n,"Answered with a note"),await b(B(t),a))}c(Rd,"resolveNoteAnswers");function Qr(t,e,n){const a=Ln(e),i=t.items.filter(o=>o.topic===e.topic&&Ln(o)===a).map(o=>Wt(o,n));fs(i,a)||d(`${e.id} is in prototype round ${a}, which no pick has settled yet; a pick settles it`)}c(Qr,"roundSettled");async function he(t){const e=await J($.join(t,"data","events.jsonl"));return{answers:await Promise.all(e.filter(a=>a.kind==="owner-answer").map(async a=>({...a,reply:a.reply||(await Wn($.join(t,"data","replies"),a.id)).length>0}))),requests:e.filter(a=>a.kind==="prototype-request"),reopens:e.filter(a=>a.kind===cs)}}c(he,"ownerEvents");async function Ed(t,e){const{asked:n}=await Vi(t),a=new Map(n.map(i=>[i.id,i.state]));return e.filter(i=>!a.has(String(i.id))||["open","requested"].includes(a.get(String(i.id))))}c(Ed,"unrecorded");async function Vi(t){const e=await g(B(t),{topics:[],items:[]}),{answers:n,requests:a,reopens:i}=await he(t);return{owner:e,asked:(e.items||[]).map(o=>Wt(o,n,a,[],i))}}c(Vi,"topicQuestionsAll");async function Zi(t,e){const n=await g(B(t),{topics:[],items:[]}),{answers:a,requests:i,reopens:o}=await he(t),s=(n.items||[]).filter(r=>r.topic===e).map(r=>Wt(r,a,i,[],o));return{owner:n,asked:s}}c(Zi,"topicQuestions");const ft=c((t,e)=>Zi(t,`item:${e}`),"cardQuestions"),fn=c((t,e,n)=>Yi(t,`item:${e}`,n),"resolveAnswered");async function Yi(t,e,n){if(!t)return[];const{owner:a,asked:i}=await Zi(t,e),o=Qi(i);return o.length&&await me(t,a,o,n),await to(t,e),o.map(s=>`resolved: ${s}`)}c(Yi,"resolveAnsweredOn");const un=c(async(t,e)=>!!t&&(await ft(t,e)).asked.some(n=>n.state==="open"),"hasOpenQuestion");function Xi(t,e,n){t.next=v(e,"--next"),t.next_at=new Date().toISOString(),n?t.next_on_questions=!0:delete t.next_on_questions}c(Xi,"setNext");async function to(t,e){const n=e.startsWith("item:")&&!e.startsWith("item:ask:")?e.slice(5):null;if(!n)return;let a;try{a=await x(t,n)}catch(r){if(r instanceof pt)return;throw r}const{source:i,itemFile:o,item:s}=a;s.next_on_questions&&((await ft(t,n)).asked.some(r=>r.state!=="resolved")||(delete s.next,delete s.next_at,delete s.next_on_questions,await b(i.file,o)))}c(to,"clearWaitedNext");async function Vr(t,e){const{source:n,itemFile:a,item:i}=await x(t,e);i.next===void 0&&i.next_on_questions===void 0||(delete i.next,delete i.next_at,delete i.next_on_questions,await b(n.file,a))}c(Vr,"clearNext");async function eo(t,e,n,a){if(!t)return async()=>[];const{owner:i,asked:o}=await ft(t,e),s=o.filter(l=>l.state==="open").map(l=>l.id);s.length&&!a&&d(`${n}: ${e} 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 r=o.filter(l=>l.state!=="resolved").map(l=>l.id);return async()=>(r.length&&await me(t,i,r,"Closed with the card"),r.map(l=>`resolved: ${l}`))}c(eo,"closeQuestions");async function Ld(t,e,n,a,i){await x(t,e),Xe(n),it(a,o=>v(o,"--owner-step")),it(i,o=>ho(o)),await eo(P(t),e,"land",!1)}c(Ld,"checkClose");async function fa(t,e){const n=await g(B(t),{items:[]}),{answers:a,requests:i,reopens:o}=await he(t);return rs((n.items||[]).map(s=>Wt(s,a,i,[],o)),e)}c(fa,"unpickedPrototype");const Zr=c(t=>t.length>120?`${t.slice(0,119)}\u2026`:t,"clip"),Yr={ask:"text","ask-edited":"text","item-message":"text","owner-answer":"note"};async function no(t,e,n){const a=v(e,n).replace(/\s+/g," ");return(await J($.join(t,"data","events.jsonl"))).map(o=>String(o.data?.[Yr[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}c(no,"ownerQuote");async function Bd(t,e,n){return P(t),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 ${e}'s run`),`Running at the owner's ask: "${await no(t,n,"--asked")}"`}c(Bd,"askedGate");async function Ee(t,e,n="--from-ask"){return St(e,n),(await J($.join(P(t),"data","events.jsonl"))).some(i=>i.id===e&&i.kind==="ask")?e:d(`no ask "${e}" in this site's data/events.jsonl`)}c(Ee,"checkAsk");function Le(t,e,n=new Date().toISOString(),a=ao(t,e)){if(!qt[e])throw new Error(`no lane "${e}"`);const i=t.lane;return io(t,e,a),t.lane=e,t.lane_at=n,e==="done"?(t.closed_at=n,delete t.reopened_at):i==="done"&&(t.reopened_at=n),i}c(Le,"placeIn");function ao(t,e,n=!1){return e==="todo"?mt(t)?"ready":t.phase??null:e==="progress"?"reading":e==="done"?n||Rt(t).some(a=>a.merged_at)?"merged":"closed":null}c(ao,"movedPhase");function io(t,e,n){if(n!==null&&!Nn[e].includes(n))throw new Error(`no ${qt[e]} phase "${n}"`);n===null?delete t.phase:t.phase=n}c(io,"setPhase");async function Cd(t,e,n,a,i,o){const{source:s}=await x(t,e),{id:r}=F(e),l=new Date().toISOString();if(!await se(s.file,async()=>{const w=JSON.parse(await O.readFile(s.file,"utf8")),u=w.items.find(m=>m.id===r)||d(`item file "${s.slug}" has no item "${r}"`);if(o&&((u.lane??null)!==o.lane||(u.phase??null)!==o.phase)||u.lane===n&&(i===void 0||(u.phase??null)===i))return!1;let h;return u.lane===n?io(u,n,i):h=Le(u,n,l,i),await Ht(s.file,JSON.stringify(w,null,2)+`
25
+ `),h!==void 0}))return null;const p={ts:l,who:"claude",kind:qa,text:`Moved to ${qt[n]}: ${v(a,"why")}`};return await lt(Z(t,e),p),p.text}c(Cd,"moveCard");const G=c(t=>t?.lane==="done"||!!t?.reopened_at,"closed"),mt=c(t=>!!t?.lane&&t.lane!=="todo","leftTodo"),Id=c(async(t,e)=>G((await x(t,e)).item),"isFinished"),oo=["ready","grilling","prototyping"];function so(t,e){e!==void 0&&d(`--state is now --phase: pass --phase ${oo.includes(e)?e:"ready, grilling or prototyping"}`),t==="staging"&&d("--phase staging is set by the board, on an owner action; pass ready, grilling or prototyping"),t!==void 0&&!oo.includes(t)&&d("--phase takes ready, grilling or prototyping")}c(so,"checkPhase");const Xr="a card goes ready only from staging, which the board sets on the owner's Send, Implement or Reply";async function ro(t,e){return(await J($.join(P(t),"data","events.jsonl"))).some(a=>a.kind==="ask"&&a.id===e&&a.data?.quick===!0||a.kind==="item-message"&&a.data?.key===`ask:${e}`&&a.data?.intent==="build")}c(ro,"quickAsk");const tc=/^mark (?:it |this |the card )?ready\b/i;function ec(t,e,n){const a=[].concat(e).map(s=>Tn(s)||d(`--pr takes a PR number or owner/repo#<n>, not "${s}"`)),i=c((s,r)=>s.number===r.number&&(s.repo||n||null)===(r.repo||n||null),"same"),o=Rt(t);for(const s of a)o.some(r=>i(r,s))||o.push(s);delete t.pr,t.prs=o}c(ec,"addPRs");const z=c((t,e)=>String(e).includes("/")?String(e):`${t}/${e}`,"depKey"),Xt=/^wave:(.+)$/;async function ge(t,e,n){const a=await Promise.all((n||[]).map(async i=>{const o=String(i).match(Xt)?.[1];if(!o)return[z(e,i)];const{source:s,wave:r}=await $e(t,o);return Mt(r).map(l=>`${s.slug}/${l}`)}));return[...new Set(a.flat())]}c(ge,"depCards");async function Ct(t,e,n){const a=await ge(t,e,n.deps),i=await Promise.all(a.map(o=>x(t,o).then(({item:s})=>s,()=>null)));return a.filter((o,s)=>i[s]&&!G(i[s]))}c(Ct,"unfinishedDeps");async function nc(t,e,n){const a=await Promise.all((await Ct(t,F(e).file,n)).map(i=>K(t,i)));return n.blocked_until?[...a,n.blocked_until]:a}c(nc,"blockerNames");async function wn(t,e,n){n??=(await x(t,e)).item;const a=await Promise.all((await Ct(t,F(e).file,n)).map(i=>K(t,i)));return[a.length&&`by ${a.join(", ")}`,n.blocked_until&&`until ${n.blocked_until}`].filter(Boolean).join(" and ")}c(wn,"blockWords");const ac=c((t,e)=>!_t(t)&&!t.pushed&&!t.merge_ready&&!G(t)&&(!!Rn(e)||e.some(n=>n?.started&&!n.done&&ie(n))),"inProgress");async function ic(t,e){const n=[];for(const a of await I(t)){const i=await g(a.file,null);for(const o of Array.isArray(i?.items)?i.items:[])G(o)||o.blocked_until||!(await ge(t,a.slug,o.deps)).includes(e)||(await Ct(t,a.slug,o)).length||n.push(`${a.slug}/${o.id}`)}return n}c(ic,"unblockedBy");async function oc(t,e,n){const a=F(e).file,i=(await wa(t,a,n)).map(r=>Xt.test(r)?r:z(a,r));i.length||d("--split-into takes wave:<slug> or the cards the split filed, <id>,<file>/<id>"),(await ge(t,a,i)).includes(e)&&d(`${e} can't be split into itself`);const o=await Promise.all(i.map(async r=>{const l=r.match(Xt)?.[1];if(!l)return K(t,r);const{source:f,wave:p}=await $e(t,l),w=await Promise.all(Mt(p).map(u=>K(t,`${f.slug}/${u}`)));return`the wave ${p.title} (${w.join(", ")})`})),s=[];for(const r of await I(t))for(const l of(await g(r.file,null))?.items||[]){const f=`${r.slug}/${l.id}`;if(!(G(l)||!(l.deps||[]).some(p=>z(r.slug,p)===e))){for(const p of i)(await ge(t,a,[p])).includes(f)&&d(Xt.test(p)?`${f} waits on ${e} and is in the wave "${p.match(Xt)[1]}", so it would wait on itself; split into the new cards by key`:`${f} waits on ${e} and is one of the cards it splits into, so it would wait on itself`);s.push(f)}}return{entries:i,words:o.join(", "),waiters:s}}c(oc,"splitPlan");async function sc(t,e,{entries:n,words:a,waiters:i}){for(const s of await I(t)){const r=i.filter(f=>F(f).file===s.slug);if(!r.length)continue;const l=await g(s.file,null);for(const f of l.items.filter(p=>r.includes(`${s.slug}/${p.id}`))){const p=n.map(w=>!Xt.test(w)&&F(w).file===s.slug?F(w).id:w);f.deps=[...new Set([...f.deps.filter(w=>z(s.slug,w)!==e),...p])]}await b(s.file,l)}const o=await K(t,e);for(const s of i)await W(t,s,"split-moved",{card:o,to:a})}c(sc,"moveWaiters");async function ua(t,e,n){const{source:a,itemFile:i,item:o}=await x(t,e);if(mt(o))o.unblocked_at=n;else if(o.phase==="ready")o.recheck_at=n;else return;await b(a.file,i)}c(ua,"markUnblocked");async function co(t,e,n,a){const i=new Set(a.match(/#?[\w-]+(?:\.[\w-]+)*(?:\/[\w-]+(?:\.[\w-]+)*)*/g)||[]),o=new Set(await ge(t,F(e).file,n.deps));for(const s of await I(t)){const r=await g(s.file,null);for(const l of r?.items||[]){const f=`${s.slug}/${l.id}`;if(f===e||o.has(f)||G(l))continue;[l.id,oe(r,l),l.ticket].filter(Boolean).flatMap(w=>[w,`${s.slug}/${w}`]).some(w=>i.has(w))&&d(`--next names unfinished card ${f}; add it with --deps`)}}}c(co,"checkNextDeps");const lo=c(t=>`item notes ${t} --heading "<heading>" --file <draft.md>`,"NOTES_COMMAND");function rc(t,e){e.size||d(`${t} has no size; a builder starts from it, so pass --size small or large with --phase ready`),e.plan||d(`${t} has no notes file; a builder starts from it, so add its notes with ${lo(t)} before --phase ready`)}c(rc,"checkSizeAndNotes");async function cc(t,e,n,a){rc(e,a);const i=await O.readFile(await pn(n,a.plan),"utf8"),o=(await ft(t,e)).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(`${e}'s notes leave out the answered question${o.length===1?"":"s"} ${o.join(", ")}; record each answer in its notes, naming its id, with ${lo(e)} before it is ready`)}c(cc,"checkNotesComplete");async function pn(t,e,{missingOk:n=!1}={}){const a=c(s=>d(`a notes file is a .md file inside ${t}, named relative to it: "${e}" ${s}`),"bad");(typeof e!="string"||!e.trim())&&a("is empty"),($.isAbsolute(e)||e.split(/[\\/]/).includes(".."))&&a("leaves the folder"),$.extname(e).toLowerCase()!==".md"&&a("is not a .md file");const i=await O.realpath(t),o=await O.realpath($.resolve(t,e)).catch(()=>n?$.resolve(i,e):a("does not exist"));return o.startsWith(i+$.sep)||a("leaves the folder"),o}c(pn,"planPath");async function lc(t,e){const{source:n,itemFile:a,item:i}=await x(t,e);if(i.plan)return{itemFile:a,item:i,markdown:await O.readFile(await pn(n.dir,i.plan),"utf8")};if(typeof i.notes=="string"&&i.notes.trim())return{itemFile:a,item:i,markdown:i.notes};d(`${e} has no notes`)}c(lc,"readNotes");function fo({install:t,installSha:e,installLabel:n,clearInstall:a}){if(t===void 0)return(e!==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(t).trim())}catch{i=null}(!i||!["https:","itms-services:"].includes(i.protocol))&&d(`--install takes an https:// or itms-services:// URL, not "${t}"`),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=e===void 0?void 0:/^[0-9a-f]{7,40}$/i.test(e)?e.toLowerCase():d(`--install-sha takes a commit sha of 7 to 40 hex digits, not "${e}"`);return{url:String(t).trim(),...n===void 0?{}:{label:v(n,"--install-label")},built_at:new Date().toISOString(),...o?{sha:o}:{}}}c(fo,"installOf");async function Td(t,{install:e,installSha:n}){const a=fo({install:e,installSha:n});a||d("main set needs --install <url>");const i=$.join(P(t),"data","main.json");return await b(i,{install:a}),{file:i,lines:["main install set"]}}c(Td,"setMainInstall");const dc=c((t,e)=>t.length?`Merged PR${t.length>1?"s":""} ${Nt(t,e)}.`:"Merged.","mergedLine");function fc(t,e,n,a,i,o){return o?{status:o}:n&&a===void 0?{status:["merged",{words:dc(n,i)}]}:(a===void 0&&t&&d(`--done needs --text "<what was done, or why it closes without landing>", which becomes ${e}'s last thread line`),a===void 0?null:{text:a})}c(fc,"closingLine");function uc(t,e,n){const a=new Date().toISOString(),i=c((o,s)=>o.number===s.number&&(o.repo||n||null)===(s.repo||n||null),"same");t.prs=Rt(t).map(o=>{const s=e.find(r=>i(o,r));return s&&!o.merged_at?{...o,merged_at:s.at||a}:o}),delete t.pr}c(uc,"markMerged");async function uo(t,e,{ticket:n,newTicket:a,status:i,clearStatus:o,done:s,splitInto:r,merged:l,landed:f,start:p,handBack:w,next:u,clearNext:h,pr:m,pushed:k,mergeReady:_,ghSince:N,fromAsk:j,phase:A,state:D,held:T,unheld:M,pickUpAt:S,deps:L,blockedUntil:C,blockReason:q,parent:ot,related:R,relate:H,unrelate:tt,install:Q,installSha:bt,installLabel:xt,clearInstall:Y,title:At,size:qe,model:ke,force:ye,text:st,closing:nt,who:bn,by:ht,command:ja,ownerStep:ne,ownerDone:Ho,check:be},Oa){(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),R!==void 0&&(H!==void 0||tt!==void 0)&&d("pass --related, or --relate and --unrelate, not both"),so(A,D),ye&&!s&&d("--force goes with --done"),r!==void 0&&!s&&d("--split-into goes with --done"),r!==void 0&&st!==void 0&&d("--split-into writes the closing line; drop --text");const xe=r===void 0?null:await oc(t,e,r);xe&&(st=`Split into ${xe.words}`),j!==void 0&&s&&d("--from-ask opens a card; close it in another call"),st!==void 0&&Xe(st),ne!==void 0&&v(ne,"--owner-step");const _e=be===void 0||be===""?be:ho(be);st!==void 0&&j===void 0&&!s&&A!=="ready"&&d("--text goes with --from-ask, --done, or --phase ready on a staging card"),j!==void 0&&st===void 0&&d(`--from-ask needs --text "<first line>", the item's first thread line after the ask`),u!==void 0&&h&&d("pass --next or --clear-next, not both"),typeof u=="string"&&!u.trim()&&([u,h]=[void 0,!0]);const Se=po({held:T,unheld:M,pickUpAt:S}),Me=fo({install:Q,installSha:bt,installLabel:xt,clearInstall:Y});[s,p,w,u,h,m,k,_,N,j,A,Se,L,C,q,ot,R,H,tt,Me,At,qe,ke,n,a,ne,be].every(E=>E===void 0||E===!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 et=j===void 0?void 0:await Ee(t,j);et&&await Qn(t,et),et&&await yi(t,et,e);const{source:Tt,itemFile:ae,item:y}=await x(t,e),Aa=et??(y.phase?void 0:y.from_ask),Jo=Aa!==void 0&&await ro(t,Aa);A!==void 0&&y.answered_at&&!y.closed_at&&(delete y.answered_at,mt(y)&&Le(y,"todo",void 0,null)),A!==void 0&&mt(y)&&d(`--phase sets a Todo card's phase; ${e} is ${qt[y.lane]}, where the board sets it`),A==="ready"&&!Jo&&!["staging","ready"].includes(y.phase)&&d(`${e} reads ${y.phase||"new"}, not staging; ${Xr}`),et&&y.from_ask&&y.from_ask!==et&&d(`${e} came from ask "${y.from_ask}", so ask "${et}" can't be its from-ask; to fold it in: item relate-ask ${et} ${e} --summary "<line>"`),et&&Se&&A!==void 0&&!y.phase&&d("a card filed held is filed New and takes no --phase; drop --phase, or drop --held to file it in a phase"),et&&A===void 0&&!y.phase&&!Se&&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 xn=G(y);xe&&xn&&d(`${e} is already Done, so the cards waiting on it were unblocked then; move them with item set <key> --deps`),ne!==void 0&&xn&&d(`${e} 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`),ne!==void 0&&(y.owner_step=v(ne,"--owner-step")),Ho&&delete y.owner_step,_e!==void 0&&!y.owner_step&&d(`--check goes with an owner step, which ${e} doesn't have: pass --owner-step "<the step>" with it on builder done or land`),_e&&(y.owner_check={check:_e,since:y.owner_check?.check===_e?y.owner_check.since:new Date().toISOString()}),(_e===""||!y.owner_step)&&delete y.owner_check;const _n=await Promise.all((await V(Tt.dir,y.id)).map(E=>g(E,null))),Ko=En(y,_n);if(At!==void 0&&(y.title=v(At,"--title")),wo(y,{size:qe,model:ke}),L!==void 0){const E=await wa(t,F(e).file,L,e);E.length?y.deps=E:delete y.deps}const Pa=C===""?y.blocked_until:void 0;if(C!==void 0&&(C===""?delete y.blocked_until:y.blocked_until=v(C,"--blocked-until")),q&&(!y.deps?.length&&!y.blocked_until&&d("--block-reason goes with a block: --deps or --blocked-until"),y.block_reason=v(q,"--block-reason")),(q===""||!y.deps?.length&&!y.blocked_until)&&delete y.block_reason,ot!==void 0){const E=await pa(t,F(e).file,e,ot);E?y.parent=E:delete y.parent}const Da=ot!==void 0||R!==void 0||H!==void 0||tt!==void 0?await ma(t,e):null;let Sn=R===void 0?null:await mc(t,e,y,R);H!==void 0&&(y.related=[...new Set([...(y.related||[]).map(E=>z(F(e).file,E)),...await hn(t,e,H,"--relate")])]),tt!==void 0&&(Sn=await hc(t,e,y,await hn(t,e,tt,"--unrelate"))),Se!==void 0&&mn(y,Se),p&&y.held&&d(y.pick_up_at?`${e} is held until ${y.pick_up_at}, when the board resumes it; start it early only with item set ${e} --unheld`:`${e} is held; start it only once the owner resumes it, with item set ${e} --unheld`);const ve=m!==void 0||k!==void 0||_!==void 0;if((p||s||w||ve)&&(pc(y),delete y.recheck_at),p){const E=await Ct(t,F(e).file,y);E.length&&d(`${e} is blocked by ${E.join(", ")}; start it once ${E.length===1?"that is":"those are"} done`),y.blocked_until&&d(`${e} is blocked until ${y.blocked_until}; start it once that holds and the block is cleared with --blocked-until ""`)}const We=p&&t?await fa(t,e):null;We&&d(`${e} has no take picked: the owner's answer to ${We.id} picks none${We.note?` ("${Zr(We.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 zo=s?await eo(t,e,"--done",ye):async()=>[],je=s?fc(t,e,l,st,ut(t,ae),nt):null;if(l&&uc(y,l,ut(t,ae)),u!==void 0&&(Xi(y,u,await un(t,e)),await co(t,e,y,y.next)),h&&(delete y.next,delete y.next_at,delete y.next_on_questions),p&&u===void 0&&y.next_on_questions&&(delete y.next,delete y.next_at,delete y.next_on_questions),ve&&!y.review_since&&(y.review_since=new Date().toISOString()),m!==void 0&&ec(y,m,ut(t,ae)),k!==void 0){kt().find(E=>Ft(E.dir,P(t)))?.remote||d("--pushed is for a board whose repo is a plain git remote; on GitHub, open a PR and pass --pr"),_t(y)&&d(`${e} has a PR; its PR says when it merges`);try{Fs(k)}catch{d("--pushed takes the name of the branch you pushed")}y.pushed={branch:k,at:new Date().toISOString()},y.branch=k}if(_!==void 0&&(y.merge_ready={branch:_,at:new Date().toISOString()},y.branch=_),s&&delete y.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"),y.gh_since=N),et&&(y.from_ask=et),A==="ready"&&t){const E=(await ft(t,e)).asked.filter(Dt=>Dt.state==="open").map(Dt=>Dt.id);E.length&&d(`${e} has ${E.length===1?"an open question":"open questions"} ${E.join(", ")}; it is ready once the owner has answered ${E.length===1?"it":"them"}`),await cc(t,e,Tt.dir,y)}const vn=A==="ready"&&y.phase==="staging"&&j===void 0;vn&&st===void 0&&d(`${e} goes ready from staging on your judgment: pass --text "<what you checked it against on the board>", which goes into its notes`);const jn=A==="ready"&&y.phase==="ready"&&!!y.recheck_at&&j===void 0;jn&&st===void 0&&d(`${e}'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`),A==="ready"&&!vn&&!jn&&st!==void 0&&j===void 0&&!s&&d(`${e} reads ${y.phase||"no phase"}, not staging, so there is no judgment for --text to record`);const Go=A==="ready"&&y.phase!=="ready";A!==void 0&&(A!=="ready"&&["staging","ready"].includes(y.phase)&&(y.round_at=new Date().toISOString()),y.phase=A,delete y.recheck_at,delete y.staged_at,delete y.unfiled,delete y.filed_held),y.phase==="ready"&&tc.test(y.next??"")&&d(`${e} would read ready with Next "${y.next}", which asks for the phase it is already in; pass --next with what happens next, or --clear-next`),Me&&(y.install=Me),Me===null&&delete y.install,!Ko&&!p&&En(y,_n)&&d(`that leaves ${e} In Progress with nothing building and no Next; pass --next "<what happens next>"`);const On=(L!==void 0||C)&&!s&&ac(y,_n)?await wn(t,e,y):"",Fa=ut(t,ae);let An=null;const Pn=c((E,Dt)=>{y.lane!==E&&Le(y,E,void 0,ao(y,E,f))!==void 0&&Dt&&(An=`Moved to ${qt[E]}: ${Dt}`)},"moveTo");s?Pn("done",null):ve&&y.lane!=="done"?Pn("review",m!==void 0?`PR ${Nt([].concat(m).map(Tn),Fa)} opened`:k!==void 0?`${k} pushed for its merge`:"verified, it waits for your Merge"):p&&!G(y)&&Pn("progress","a build started");const Na=A==="ready"&&!y.ticket&&n===void 0&&!a&&!!(t&&await te(t));Na&&(a=!0),a&&y.ticket&&d(`${e} already has ticket ${y.ticket}`);const Ra=n!==void 0||a;if(Ra&&await ko(t,ae,y,n,Oa,Tt.dir),await b(Tt.file,ae),Ra&&t&&await gc(t,e,y.ticket),Na&&await W(t,e,"ticket-made",{ticket:y.ticket}),An&&await lt(Z(t,e),{ts:y.lane_at,who:"claude",kind:qa,text:An}),vn&&await Bt(t,e,`Ready (orchestrator, ${Zt()})`,st),jn&&await Bt(t,e,`Re-checked (orchestrator, ${Zt()})`,st),Go&&await Gt(t,e,{command:"readied"}),On){const E=t&&!await qo(t,e)&&await To(t,e,{cause:"blocked",words:`blocked ${On}`});await W(t,e,E?"blocked-pausing":"blocked-stays",{blocked:On})}Sn&&await Sn(),Da&&await $o(t,e,Da,await ma(t,e));const Pt=await zo();if(A==="ready"&&t&&Pt.push(...await fn(t,e,"Recorded in the card's notes")),p&&Pt.push(...await fn(t,e,"Acted on: build started")),et&&Pt.push(...(await zt(t,e,{text:st})).lines),je&&t){const E=ja?await Gt(t,e,{command:ja,text:je.text,who:bn,by:ht}):je.status?await W(t,e,...je.status):await zt(t,e,{text:je.text});Pt.push(...E.lines)}const Ea=y.owner_step&&s?null:vs({start:p,done:s,handover:ve}),La=Ea&&await js(await te(t),y,Ea,{...Oa,repo:Fa});La&&Pt.push(La),(ve||s||w)&&(await Wo(Tt.dir,y.id,s,w?{handedBack:new Date().toISOString()}:{endHandBack:!0}),await pe(t,e));const Dn=et?await jt(t):{};if(rt(Dn,et)!==void 0&&(delete Dn[et],await b(vt(t),{links:Dn})),Pa&&!G(y)&&!(await Ct(t,F(e).file,y)).length){const{line:E}=await W(t,e,"unblocked",{lifted:Pa});await ua(t,e,E.ts),await $t({dir:t},"card-unblocked",{key:e}),Pt.unshift(`unblocked: ${e}`)}if(!s||xn)return{file:Tt.file,lines:Pt};xe&&await sc(t,e,xe);const Ba=await ic(t,e),Qo=await K(t,e);for(const E of Ba){const{line:Dt}=await W(t,E,"unblocked-done",{card:Qo});await ua(t,E,Dt.ts),await $t({dir:t},"card-unblocked",{key:E})}return{file:Tt.file,lines:[...Ba.map(E=>`unblocked: ${E}`),...Pt]}}c(uo,"changeItem");function wo(t,{size:e,model:n}){for(const[a,i,o]of[["size",e,["small","large"]],["model",n,["opus","sonnet"]]])i!==void 0&&(i===""?delete t[a]:t[a]=o.includes(i)?i:d(`--${a} takes ${o.join(" or ")}, or "" to clear it`))}c(wo,"setBuildChoice");function po({held:t,unheld:e,pickUpAt:n}){return[t,e,n!==void 0].filter(Boolean).length>1&&d("pass one of --held, --unheld and --pick-up-at"),n!==void 0?n===""?null:{until:wc(n)}:t?{until:null}:e?null:void 0}c(po,"holdOf");function wc(t){const e=typeof t=="string"&&_s.test(t)?Date.parse(t):NaN;return Number.isNaN(e)&&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'),e<=Date.now()&&d(`--pick-up-at ${t} has passed; pass a later time, or --unheld to resume the card now`),new Date(e).toISOString()}c(wc,"pickUpTime");function pc(t){delete t.held,delete t.pick_up_at}c(pc,"clearHold");function mn(t,e){e?t.held=!0:delete t.held,e?.until?t.pick_up_at=e.until:delete t.pick_up_at,t.held_at=new Date().toISOString()}c(mn,"setHold");async function qd(t,e,n,{announce:a=!0}={}){const{source:i,itemFile:o,item:s}=await x(t,e);if(a){const r=new Date(n).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});await W(t,e,"hold-ended",{at:r}),!s.blocked_until&&!(await Ct(t,F(e).file,s)).length&&await $t({dir:t},"card-unblocked",{key:e,until:n})}mn(s,null),await b(i.file,o)}c(qd,"pickUp");async function mo(t,e,n){const a=String(n?.key||""),{file:i,id:o}=F(a),{source:s,itemFile:r}=await Jt(t,i),l=n.shared_by?n.shared_by.name||n.shared_by.login:void 0,f={text:He.done,who:"love",by:l,command:"done"},p=r.items.find(m=>m.id===o)||d(`item file "${i}" has no item "${o}"`);if(!(e==="card-done"&&!!p.owner_step)&&p.lane==="done")return null;if(e==="card-done")return yt(t,a,{done:!0,force:!0,ownerDone:!0,...f});const u=n.hold?typeof n.until=="string"?new Date(n.until).toISOString():null:void 0;mn(p,n.hold?{until:u}:null),await b(s.file,r),n.hold&&(await qo(t,a)||await To(t,a,{cause:"held",words:"held"}),await Wo(s.dir,o,!1,{held:!0}),await pe(t,a));const h=u&&new Date(u).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});return Gt(t,a,n.hold?{text:h?`${He.hold} until ${h}`:He.hold,who:"love",by:l,command:"hold"}:{who:"love",by:l,command:"resume"})}c(mo,"recordOwnerTap");function ho(t){try{return Vs(Qs(t))}catch(e){return d(e.message)}}c(ho,"checkSpec");async function Md(t,e,n,a){const{item:i}=await x(t,e);return!i.owner_step||i.owner_check?.check!==n?null:yt(t,e,{done:!0,force:!0,ownerDone:!0,closing:["check-passed",{found:a}]})}c(Md,"passOwnerCheck");async function Wd(t,e,n,a){const{source:i,itemFile:o,item:s}=await x(t,e);return!s.owner_step||s.owner_check?.check!==n?!1:(s.owner_check.broken=a,await b(i.file,o),await W(t,e,"check-broken",{error:a}),await $t({dir:t},"owner-check-broken",{key:e,check:n,error:a}),!0)}c(Wd,"breakOwnerCheck");async function wa(t,e,n,a){const i=[...new Set(String(n).split(",").map(o=>o.trim()).filter(Boolean))];for(const o of i){const s=o.match(Xt)?.[1];if(!s){await x(t,z(e,o));continue}(await ge(t,e,[o])).includes(a)&&d(`${a} is in the wave "${s}", so it would wait on itself`)}return i}c(wa,"depList");async function pa(t,e,n,a){if(!String(a).trim())return null;const i=z(e,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(t,o):await x(t,o).catch(l=>{if(l instanceof pt)return null;throw l});if(!s)break;const{item:r}=s;r.parent&&z(F(o).file,r.parent)===n&&d(`--parent: ${i} is a child of ${n}, so it can't be its Parent`),o=r.parent?z(F(o).file,r.parent):null}return i}c(pa,"parentKey");async function hn(t,e,n,a){const i=e.split("/")[0],o=[...new Set(String(n).split(",").map(s=>s.trim()).filter(Boolean).map(s=>z(i,s)))];o.includes(e)&&d(`${a}: a card can't be related to itself`);for(const s of o)await x(t,s);return o}c(hn,"relatedKeys");const go=c((t,e,n)=>async()=>{for(const a of await I(t)){const i=await g(a.file,null);let o=!1;for(const s of Array.isArray(i?.items)?i.items:[]){const r=`${a.slug}/${s.id}`;r===e||!n(r)||!(s.related||[]).some(l=>z(a.slug,l)===e)||(s.related=s.related.filter(l=>z(a.slug,l)!==e),s.related.length||delete s.related,o=!0)}o&&await b(a.file,i)}},"dropLinksTo");async function ma(t,e){const n=new Set;let a=null;for(const i of await I(t))for(const o of(await g(i.file,null))?.items||[]){const s=`${i.slug}/${o.id}`,r=(o.related||[]).map(l=>z(i.slug,l));s===e?(r.forEach(l=>n.add(l)),o.parent&&(a=z(i.slug,o.parent))):r.includes(e)&&n.add(s)}return{related:n,parent:a}}c(ma,"linksOf");async function $o(t,e,n,a){const i=c(async(o,s,r=o,l=o)=>{await Gt(t,e,{command:r,card:s}),await Gt(t,s,{command:l,card:e})},"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")}c($o,"postLinkLines");async function mc(t,e,n,a){const i=await hn(t,e,a,"--related");return i.length?n.related=i:delete n.related,go(t,e,o=>!i.includes(o))}c(mc,"setRelated");function hc(t,e,n,a){const i=(n.related||[]).filter(o=>!a.includes(z(F(e).file,o)));return i.length?n.related=i:delete n.related,go(t,e,o=>a.includes(o))}c(hc,"unrelateCards");const ut=c((t,e)=>t&&kt().find(n=>Ft(n.dir,t))?.github||e.repo,"boardRepoOf");async function Ud(t,e){const{itemFile:n,item:a}=await x(t,e);_t(a)||d(`${e} has no PR`);const i=Pe(a,ut(t,n));i.length||d(`${e} has no repo`);const o=a.gh_since||"1970-01-01T00:00:00Z";try{const s=Ot(await at(t)).github_login||await Cs();return await Promise.all(i.map(async({repo:r,number:l})=>({repo:r,...Xa(await Ya({repo:r,pr:l,since:o}),{pr:l,since:o,me:s})})))}catch(s){d(s.message)}}c(Ud,"prReport");async function Hd(t,e,{replyTo:n,body:a}){const i=n===void 0?"pr comment":"pr reply",o=Tn(e)||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=v(a,"--body").trim();(s.startsWith(Ae.trim())||s.includes(In))&&d(`${i} adds the ${Ae.trim()} prefix and the claude-bridge marker itself; pass only your words`);const r=o.repo||ut(P(t),{})||d(`${i} needs a board on GitHub; this board has no repo there`);try{const l=await Is({repo:r,pr:o.number,replyTo:n,words:s,login:Ot(await at(t)).github_login});return{lines:[`posted on PR ${o.repo?e:`#${e}`}: ${l}`]}}catch(l){d(l.message)}}c(Hd,"postPR");const te=c(async t=>Li((await at(t)).tickets),"boardSource");async function Jd(t,e,{afterApproval:n,confirmed:a,tapped:i}={}){P(t);const{itemFile:o,item:s}=await x(t,e);_t(s)||d(`${e} has no PR`),G(s)&&d(`${e} is done`);const r=await wn(t,e,s);r&&(i&&await W(t,e,"merge-blocked",{blocked:r}),d(`Not merged: blocked ${r}.`));const l=ut(t,o),f=Pe(s,l);f.length||d(`${e} has no repo`);const p=new Date().toISOString(),w=c(S=>W(t,e,"gh-limited",{what:"Not merged",words:es(S)}),"limited");let u;try{u=await Promise.all(f.map(S=>Ya({repo:S.repo,pr:S.number,comments:!1})))}catch(S){S.limited&&await w(S.limited),d(S.message)}const h=u.map(({view:S})=>S);if(Ms(h)){const S=f.map((L,C)=>({...L,at:h[C].mergedAt})).filter((L,C)=>h[C].state==="MERGED");return yt(t,e,{merged:S,closing:["already-merged",{prs:Nt(S,l),many:S.length>1}]})}const m=f.map((S,L)=>({...S,...u[L]})).filter(({view:S})=>S.state==="OPEN"),k=Ke(o,(await at(t)).reviewers||[]).length>0,_=[];let N=!1,j=!1;for(const S of m){const{view:L,reviews:C}=S,{failing:q,pending:ot}=Xa(S,{pr:S.number,since:p,me:null}),R=ss({...L,checks:{state:q.length?"fail":ot?"pending":"pass",failing:q,pending:ot}},{needsApproval:k}),H=C.filter(Q=>Q.state==="APPROVED").sort((Q,bt)=>String(Q.submitted_at).localeCompare(String(bt.submitted_at))).at(-1)?.commit_id;let tt=!!(H&&L.headRefOid&&H!==L.headRefOid);tt&&n===void 0&&Rc(s,H,L.headRefOid)&&(tt=!1,j=!0),N||=tt,tt&&n===void 0&&R.push('commits after the approval: pass --after-approval "<why they are trivial>" when they are, else ask the approver to look again'),_.push(...f.length>1?R.map(Q=>`${Nt([S],l)} ${Q}`):R)}m.length||_.push(f.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 A=s.manual_steps?.length?a===void 0?null:await no(t,a,"--confirmed"):void 0;if(a!==void 0&&A===void 0&&d(`--confirmed: ${e} has no manual steps; drop it`),A===null&&_.push(`manual steps: merge only on the owner's word, with --confirmed "<their words>"`),_.length){const S=`Not merged: ${_.join("; ")}.`;i&&await W(t,e,"merge-refused",{why:_.join("; ")}),d(S)}const D=[];for(const S of m){const L=await Ca(["pr","view",String(S.number),"--repo",S.repo,"--json","title,body,commits"]);L.limited&&await w(L.limited),L.status!==0&&d(`gh pr view ${Nt([S],l)} failed: ${L.stderr.trim().split(`
26
+ `)[0]||`gh exited ${L.status}`}${D.length?` (merged ${Nt(D,l)} before it)`:""}`);const{title:C,body:q,commits:ot}=JSON.parse(L.stdout),R=["--subject",C,"--body",ns(await K(t,e),q,ot)],H=await Ca(["pr","merge",String(S.number),"--repo",S.repo,"--squash",...R],{timeout:12e4});H.limited&&await w(H.limited),H.status!==0&&d(`gh pr merge ${Nt([S],l)} failed: ${H.stderr.trim().split(`
27
+ `)[0]||`gh exited ${H.status}`}${D.length?` (merged ${Nt(D,l)} before it)`:""}`),D.push(S)}const T=[n!==void 0&&`After the approval: ${v(n,"--after-approval")}`,j&&`After the approval: only the board's rebase onto ${s.rebased.at(-1).base}.`,A&&`The owner's word on the manual steps: "${A}"`].filter(Boolean),M=await yt(t,e,{merged:D});return T.length&&await zt(t,e,{text:T.join(" ")}),M}c(Jd,"mergeCard");async function Kd(t,e){return(await x(t,e)).item.merge_ready||null}c(Kd,"heldMerge");async function zd(t,e){const{source:n,itemFile:a,item:i}=await x(t,e);(!i.merge_ready||G(i))&&d(`${e} isn't waiting for its Merge`);const o=await wn(t,e,i);return o&&d(`${e} is blocked ${o}, so its Merge waits until nothing blocks it`),i.merge_ready.tapped=new Date().toISOString(),await b(n.file,a),i.merge_ready}c(zd,"tapMerge");async function Gd(t,e){const{source:n,item:a}=await x(t,e);return(await Promise.all((await V(n.dir,a.id)).map(o=>g(o,null)))).filter(o=>o?.worktree?.path&&o.kind!==X).sort((o,s)=>String(s.started).localeCompare(String(o.started)))[0]?.model}c(Gd,"builderModel");async function ko(t,e,n,a,i,o,s){const r=await te(t)||d($c);try{s??=a===void 0&&n.plan?await O.readFile(await pn(o,n.plan),"utf8"):n.notes||"";const l=await Ss(r,{ticket:a,title:n.title,body:s,repo:ut(t,e)},i);n.ticket=l.ticket,l.created?n.ticket_created=!0:delete n.ticket_created}catch(l){if(l instanceof pt)throw l;d(l.message)}}c(ko,"assignTicket");async function gc(t,e,n){await Wi(t,e,"ticket_gate",`Ticket linked: ${n}`)&&(await x(t,e)).item.phase==="grilling"&&await rn(t,e,{roundAt:!0})}c(gc,"dropTicketQuestion");const $c="this board has no ticket source, so its cards stay tasks; pick GitHub issues or Linear in Board settings first";async function yo(t){const e=[],n=[];for(const a of await I(t)){const i=await g(a.file,{});e.push(...(i.items||[]).map(o=>o.id)),n.push(Number(i.last_task)||0)}return t&&e.push(...(await It(t)).map(a=>a.item?.id)),`T-${Math.max(0,...n,...e.map(a=>Number(/^T-([1-9]\d*)$/.exec(String(a))?.[1]||0)))+1}`}c(yo,"nextTaskId");const ct=c((t,e)=>se($.join(t||Ve(),"tickets"),e,{heartbeat:!0}),"ticketWrite"),yt=c((t,e,n,a)=>n.ticket!==void 0||n.newTicket||n.phase==="ready"?ct(t,()=>uo(t,e,n,a)):uo(t,e,n,a),"setItem"),kc=c((t,e,n,a)=>ct(t,()=>yc(t,e,n,a)),"addItem");async function yc(t,e,{title:n,notes:a,notesFrom:i,deps:o,ticket:s,newTicket:r,pickUpAt:l,held:f,parent:p,splitOf:w,size:u,model:h,phase:m,state:k,relate:_,fromAsk:N,text:j,intake:A=!1},D){const{file:T,id:M}=e.includes("/")?F(e):{file:e};M!==void 0&&St(M,"the item id"),so(m,k),j!==void 0&&v(j,"--text"),j!==void 0&&N===void 0&&d("--text goes with --from-ask"),N!==void 0&&j===void 0&&d(`--from-ask needs --text "<first line>", the item's first thread line after the ask`),s!==void 0&&r&&d("pass --ticket <id> to link a ticket or --new-ticket to make one, not both");const S=N===void 0?void 0:await Ee(t,N);S&&await Qn(t,S),S&&await yi(t,S);const L=po({held:f,pickUpAt:l});L&&m!==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"),S&&m===void 0&&!L&&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"),m==="ready"&&!(S&&await ro(t,S))&&d(S?`ask "${S}" 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"),m==="ready"&&!u&&d("a new ready card has no size; a builder starts from it, so pass --size small or large with --phase ready"),m==="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"),A&&t&&m===void 0&&!L&&d("item add files a card on a board: pass --phase grilling or prototyping, or --held to file it for later");const{source:C,itemFile:q}=await Jt(t,T);M!==void 0&&q.items.some(Y=>Y.id===M)&&d(`item file "${T}" already has an item "${M}"`);const ot=new Date().toISOString(),R={id:M,title:v(n,"--title"),created:ot,lane:"todo",lane_at:ot};if(p!==void 0&&(R.parent=await pa(t,T,null,p)||d("--parent takes a card, such as <file>/<id>")),w!==void 0&&p!==void 0&&d("--split-of sets the Parent; drop --parent"),w!==void 0){R.parent=await pa(t,T,null,w)||d("--split-of takes a card, such as <file>/<id>");const{gates:Y}=(await x(t,R.parent)).item;Y&&(R.gates={...Y})}a!==void 0&&i!==void 0&&d("pass --notes or --notes-from, not both");const H=i!==void 0?await zi(t,i,"--notes-from",()=>`${la}, and link that card with --relate`):a===void 0?"":v(a,"--notes");i!==void 0&&!H.trim()&&d(`--notes-from ${i} is empty`);const tt=i!==void 0?H:`# ${R.title}
28
+ ${H&&`
29
+ ${H}
30
+ `}`;wo(R,{size:u,model:h}),o!==void 0&&(R.deps=await wa(t,T,o)),L&&mn(R,L),L&&m===void 0&&(R.filed_held=!0),_!==void 0&&(R.related=await hn(t,e,_,"--relate")),S&&(R.from_ask=S),m!==void 0&&(R.phase=m);const Q=M===void 0?null:Ja(await te(t),M);Q&&s!==void 0&&Ja(await te(t),s)!==Q&&d(`--ticket ${s} is not the ticket ${M} names`);const bt=m==="ready"&&!Q&&s===void 0&&!r&&!!await te(t);bt&&(r=!0),Q&&r&&d(`${M} names ticket ${Q}, so --new-ticket would make another; drop --new-ticket`),(s!==void 0||r||Q)&&await ko(t,q,R,s??Q??void 0,D,C.dir,H),M===void 0&&R.ticket?(R.id=Os(R.ticket),q.items.some(Y=>Y.id===R.id)&&d(`item file "${T}" already has an item "${R.id}"; link ${R.ticket} with item add ${T}/<id> --ticket ${R.ticket}`)):M===void 0&&(R.id=await yo(t)),R.plan=ln(C.dir,R.id),await dn($.join(C.dir,R.plan),tt);const xt=`${T}/${R.id}`;if(q.items.push(R),await b(C.file,q),S){await zt(t,xt,{text:j});const Y=await jt(t);rt(Y,S)!==void 0&&(delete Y[S],await b(vt(t),{links:Y}))}return bt&&t&&await W(t,xt,"ticket-made",{ticket:R.ticket}),t&&await $o(t,xt,{related:new Set,parent:null},await ma(t,xt)),{file:C.file,key:xt,lines:[]}}c(yc,"createItem");const Qd=c(t=>ct(t,async()=>{const e=[];for(const n of await I(t)){const a=await g(n.file,null);if(!Array.isArray(a?.items)||!Array.isArray(a.followups))continue;const i=a.followups.find(l=>a.items.some(f=>f.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:f,from:p,filed:w,done:u,...h}of a.followups){const m=u?t&&(await J(Z(t,`${n.slug}/${l}`))).map(k=>k.ts).filter(Boolean).sort().pop()||w:null;o.push({id:l,title:f,...w?{created:w}:{},...p?{parent:z(n.slug,p)}:{},...u?{lane:"done",...m?{lane_at:m,closed_at:m}:{}}:{},...h})}const{followups:s,...r}=a;await b(n.file,{...r,items:[...a.items,...o]}),e.push(`${n.slug}: ${o.length} follow-ups moved to cards (${o.filter(l=>l.lane==="done").length} done)`)}return e}),"migrateFollowups");function bo(t,e=[]){if(t.status===void 0&&t.status_at===void 0&&t.merged_at===void 0)return!1;const n=t.merged_at||t.status_at||new Date().toISOString();["merged","done","dropped"].includes(t.status)&&Object.assign(t,{lane:"done",lane_at:n,closed_at:n}),(t.status==="merged"||t.merged_at)&&(t.prs=Rt(t).map(i=>({...i,merged_at:i.merged_at||n})),delete t.pr),t.status==="held"&&(t.held=!0);const a=e.filter(i=>i.started&&ie(i)&&i.kind!==X).sort((i,o)=>String(i.started).localeCompare(String(o.started))).at(-1);return t.status==="checking"&&a&&(a.handed_back=n),delete t.status,delete t.status_at,delete t.merged_at,!0}c(bo,"statusToFacts");const Vd=c(t=>ct(t,async()=>{const e=[];for(const n of await I(t)){const a=await g(n.file,null);let i=0;for(const o of Array.isArray(a?.items)?a.items:[]){const s=o.status==="checking"?await V(n.dir,o.id):[],r=await Promise.all(s.map(l=>g(l,null)));if(bo(o,r.filter(Boolean))){i++;for(const[l,f]of r.entries())f?.handed_back&&await b(s[l],f)}}i&&(await b(n.file,a),e.push(`${n.slug}: ${i} cards' statuses became lanes and facts`))}return await Be(t,async()=>{const n=await It(t),a=n.filter(i=>i.item&&bo(i.item)).length;a&&(await Ce(t,n),e.push(`archive: ${a} cards' statuses became lanes and facts`))}),e}),"migrateStatuses"),Zd=c(t=>ct(t,async()=>{const{asked:e}=await Vi(t),n=[];for(const a of await I(t)){const i=await g(a.file,null);let o=0;for(const s of Array.isArray(i?.items)?i.items:[]){if(s.gates||s.lane==="done")continue;const r=`${a.slug}/${s.id}`,l={};for(const[f,p]of Object.entries(Re)){const w=e.filter(h=>h.topic===`item:${r}`&&h[f]).flatMap(h=>h.answers).filter(h=>["yes","no"].includes(h.choice)).sort((h,m)=>String(h.ts).localeCompare(String(m.ts))).at(-1);if(!w)continue;const u=p==="prototype"&&w.choice==="yes"&&(s.phase!=="grilling"||(await J(Z(t,r))).some(h=>h.line==="prototype-round"&&String(h.ts)>String(w.ts)));l[p]=u?"ran":w.choice}Object.keys(l).length&&(s.gates=l,o++)}o&&(await b(a.file,i),n.push(`${a.slug}: ${o} cards keep their gate answers`))}return n}),"migrateGates");function xo(t,e){const n=t.lane==="done"&&!Nn.done.includes(t.phase);if(!("state"in t)&&!n)return!1;if("state"in t&&t.state!==void 0&&(t.phase=t.state),delete t.state,n){const a=Rt(t);t.phase=a.some(i=>i.merged_at)||!a.length&&e?"merged":"closed"}return!0}c(xo,"stateToPhase");const Yd=c(t=>ct(t,async()=>{const e=[],n=await I(t),a=c(async(i,o)=>!!i&&o.lane==="done"&&(await Promise.all((await V(i,o.id)).map(s=>g(s,null)))).some(s=>s?.started&&ie(s)),"built");for(const i of n){const o=await g(i.file,null);let s=0;for(const r of Array.isArray(o?.items)?o.items:[])xo(r,await a(i.dir,r))&&s++;s&&(await b(i.file,o),e.push(`${i.slug}: ${s} cards' states became phases`))}return await Be(t,async()=>{const i=await It(t);let o=0;for(const s of i)s.item&&xo(s.item,await a(n.find(r=>r.slug===(s.file||F(s.key).file))?.dir,s.item))&&o++;o&&(await Ce(t,i),e.push(`archive: ${o} cards' states became phases`))}),e}),"migratePhases"),Xd=c(t=>ct(t,async()=>{const e=[];for(const n of await I(t)){const a=await g(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 b(n.file,a),e.push(`${n.slug}: ${i.length} Implement asks moved to staging or ready`)}}return e}),"stageImplementAsks"),gn=c(t=>$.join(t,"data","done-archive.json"),"archiveFile"),Be=c((t,e)=>se($.join(t,"done-archive"),e,{heartbeat:!0}),"archiveWrite"),It=c(async t=>(await g(gn(t),{cards:[]})).cards||[],"readArchive"),$n=c(t=>`${t.closed_at||""}|${t.key}`,"archiveRank"),bc=c((t,e)=>$n(t)<$n(e)?1:$n(t)>$n(e)?-1:0,"byClosed"),ha=c(t=>$.join(t,"data","done-archive-count.json"),"archiveCountFile");async function tf(t){const e=(await g(ha(t),null))?.count;if(Number.isInteger(e))return e;const n=(await It(t)).length;return await b(ha(t),{count:n}),n}c(tf,"archiveCount");async function Ce(t,e){e.length?await b(gn(t),{cards:e.sort(bc)}):await O.rm(gn(t),{force:!0}),await b(ha(t),{count:e.length})}c(Ce,"writeArchive");const ef=c((t,e)=>Be(t,async()=>{const n=new Map(e.map(w=>[w.key,w])),a=new Date().toISOString(),i=[],o=[];for(const w of await I(t)){const u=await g(w.file,null);if(!Array.isArray(u?.items))continue;const h=u.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?xc(m,k.prs):m})}o.push([w.file,{...u,items:u.items.filter(m=>!h.includes(m))}])}}if(!i.length)return[];const s=await g(B(t),{items:[]}),r=new Map(i.map(w=>[`item:${w.key}`,w]));for(const w of i)w.questions=(s.items||[]).filter(u=>u.topic===`item:${w.key}`&&u.resolved);const l=await jt(t);for(const w of i)w.asks=w.item.from_ask&&rt(l,w.item.from_ask)===void 0?[w.item.from_ask]:[];const f=new Set(i.map(w=>w.key));await Ce(t,[...(await It(t)).filter(w=>!f.has(w.key)),...i.map(({key:w,file:u,archived_at:h,closed_at:m,item:k,questions:_,asks:N})=>({key:w,file:u,archived_at:h,closed_at:m,item:k,questions:_,asks:N}))]),i.some(w=>w.asks.length)&&await b(vt(t),{links:{...l,...Object.fromEntries(i.flatMap(w=>w.asks.map(u=>[u,w.key])))}});for(const[w,u]of o)await b(w,u);const p=(s.items||[]).filter(w=>!(r.has(w.topic)&&w.resolved));return p.length!==(s.items||[]).length&&await b(B(t),{...s,items:p}),[...f]}),"archiveCards");function xc(t,e){if(!_t(t))return t;const n=Rt(t).map(o=>{const s=e.find(r=>r.state&&r.number===o.number&&(!o.repo||o.repo===r.repo));return s?{...o,kept:{checks:s.checks,reviews:s.reviews,codexOpen:s.codexOpen}}:o}),{pr:a,...i}=t;return{...i,prs:n}}c(xc,"keptPRs");async function _c(t,e){return!t||!wt(gn(t))?!1:Be(t,async()=>{const n=await It(t),a=n.find(w=>w.key===e);if(!a)return!1;const i=(await I(t)).find(w=>w.slug===a.file),o=i&&await g(i.file,null);if(!Array.isArray(o?.items))return!1;o.items.some(w=>w.id===a.item.id)||await b(i.file,{...o,items:[...o.items,a.item]});const s=await g(B(t),{items:[]}),r=new Set((s.items||[]).map(w=>w.id)),l=(a.questions||[]).filter(w=>!r.has(w.id));l.length&&await b(B(t),{...s,items:[...s.items||[],...l]});const f=await jt(t),p=(a.asks||[]).filter(w=>rt(f,w)===e);return p.length&&await b(vt(t),{links:Object.fromEntries(Object.entries(f).filter(([w])=>!p.includes(w)))}),await Ce(t,n.filter(w=>w!==a)),!0})}c(_c,"restoreArchived");const _o=["push_tag","no_change"],So=c(t=>Object.fromEntries(Object.entries(t).filter(([e])=>!_o.includes(e))),"stripQuestion"),vo=c(({summary:t,...e})=>e,"stripItem"),jo=c(t=>_o.some(e=>e in t),"holdsDead"),nf=c(t=>Be(t,async()=>{for(const a of await I(t)){const i=await g(a.file,null);Array.isArray(i?.items)&&i.items.some(o=>"summary"in o)&&await b(a.file,{...i,items:i.items.map(vo)})}const e=await g(B(t),null);(e?.items||[]).some(jo)&&await b(B(t),{...e,items:e.items.map(So)});const n=await It(t);n.some(a=>"card"in a||"summary"in a.item||(a.questions||[]).some(jo))&&await Ce(t,n.map(({card:a,...i})=>({...i,...a?{closed_at:i.closed_at||a.sortAt}:{},item:vo(i.item),questions:(i.questions||[]).map(So)})))}),"stripDeadFields");async function af(){return ps(async()=>{const t=ws(),e=Array.isArray(t.migrated)?t.migrated:[];if(e.includes("wave-names")||!Object.keys(t.sites||{}).length)return[];const n=[];for(const{route:a,dir:i}of kt(t))await Sc(i),n.push(a);return t.migrated=[...e,"wave-names"],await ms(t),n})}c(af,"migrateWaveNames");const Oo=c(async t=>JSON.parse(await O.readFile(t,"utf8")),"readPlain"),ga=c((t,e)=>Ht(t,JSON.stringify(e,null,2)+`
31
+ `),"writePlain"),Ao=c(t=>typeof t!="string"?t:t.replace(/^wave:([a-z0-9-]+)$/,"file:$1").replace(/^lineup:([a-z0-9-]+)$/,"wave:$1"),"renamedTopic"),$a=c(t=>Array.isArray(t)?t.map($a):t&&typeof t=="object"?Object.fromEntries(Object.entries(t).map(([e,n])=>[Ao(e),$a(n)])):Ao(t),"renameTopics");async function Sc(t){const e=$.join(t,"board.json"),n=await Oo(e).catch(()=>null);if(n&&typeof n.items=="string"&&n.items!=="waves"&&$.basename(n.items)==="wave.json"){const i=$.join($.dirname(n.items),"items.json");wt($.resolve(t,n.items))&&await O.rename($.resolve(t,n.items),$.resolve(t,i)),await ga(e,{...n,items:i,slug:n.slug||"wave"})}for(const i of await I(t)){const o=await Oo(i.file).catch(()=>null);if(!Array.isArray(o?.lineups))continue;const{lineups:s,...r}=o;await ga(i.file,{...r,waves:[...r.waves||[],...s]});for(const{slug:l}of s)await O.rename($.join(i.dir,`lineup-${l}.md`),$.join(i.dir,ka(l))).catch(f=>{if(f.code!=="ENOENT")throw f})}const a=$.join(t,"data");for(const i of(await O.readdir(a).catch(()=>[])).filter(o=>o.endsWith(".json"))){const o=$.join(a,i),s=await O.readFile(o,"utf8");let r;try{r=JSON.parse(s)}catch{continue}r=$a(r),i==="done-archive.json"&&Array.isArray(r.cards)&&(r.cards=r.cards.map(({wave:l,...f})=>l===void 0?f:{...f,file:l})),i==="arch-reviews.json"&&Array.isArray(r.reviews)&&(r.reviews=r.reviews.map(({wave:l,lineup:f,...p})=>f?{...p,wave:l}:{...p,file:l})),JSON.stringify(r)!==JSON.stringify(JSON.parse(s))&&await ga(o,r)}}c(Sc,"renameWaveNames");const ka=c(t=>`wave-${t}.md`,"waveNoteFile"),vc=c(t=>typeof t=="string"&&/^[a-z0-9-]+$/.test(t)?t:d(`a wave's slug uses only a-z, 0-9 and "-", not "${t}"`),"waveSlug");async function $e(t,e){for(const n of await I(t)){const a=await g(n.file,null),i=(a?.waves||[]).find(o=>o.slug===e);if(i)return{source:n,itemFile:a,wave:i}}return d(`this board has no wave "${e}"; wave create makes one`)}c($e,"findWave");async function jc(t,e){(await at(t)).items!=="waves"&&d(`topic file:${e} is for a board over ~/.claude/waves; a wave's triage uses wave:<slug>`),await Jt(t,e)}c(jc,"triagedFile");function ya(t,e,n,a){return n.map(i=>{const{file:o,id:s}=F(i);o!==t.slug&&d(`${i} is not in item file "${t.slug}"; a wave's cards share one file`),((e.items.find(f=>f.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=(e.waves||[]).find(f=>f.slug!==a&&!f.finished_at&&Mt(f).includes(s));return l&&d(`${i} is already in the wave "${l.slug}"; wave remove ${l.slug} ${i} first`),s})}c(ya,"waveCards");const ba=c(t=>[...new Set(String(t??"").split(",").map(e=>e.trim()).filter(Boolean))],"cardList");function Oc(t){const e=[t??[]].flat().map(n=>{const[a,i]=String(n).split(":");return[a.trim(),ba(i)]});return(!e.length||e.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"),e}c(Oc,"cardSpecs");function xa(t,e,n,a,i){const o=n.items.indexOf(a),s=i.map(f=>{const p=z(t.slug,f),{file:w,id:u}=F(p),h=w===t.slug?n.items.indexOf(u):-1;return(h<0||h>=o)&&d(`${p} is not an earlier card of the wave "${n.slug}"`),u}),r=e.items.find(f=>f.id===a),l=[...(r.deps||[]).filter(f=>!Po(t,n,f)),...s];l.length?r.deps=l:delete r.deps}c(xa,"relink");const Po=c((t,e,n)=>Mt(e).some(a=>z(t.slug,n)===`${t.slug}/${a}`),"inWave");async function Do(t,e,n,a,i){i!==void 0&&await Ht($.join(e.dir,ka(a.slug)),`${v(i,"the note").replace(/\n*$/,`
32
+ `)}`),await b(e.file,n);const{tickets:o}=Ot(await at(t));return{file:e.file,lines:[`order: ${Fo(e,n,a,o)}`]}}c(Do,"writeWave");function Fo(t,e,n,a){const i=c(o=>oe(e,e.items.find(s=>s.id===o)||{id:o},a),"label");return Mt(n).map(o=>{const s=(e.items.find(r=>r.id===o)?.deps||[]).filter(r=>Po(t,n,r));return s.length?`${i(o)} (after ${s.map(r=>i(F(z(t.slug,r)).id)).join(", ")})`:i(o)}).join(", ")}c(Fo,"orderLine");async function of(t,e,{title:n,goal:a,cards:i,note:o}){vc(e);const s=Oc(i);o===void 0&&d("wave create needs the wave note, as --note <text> or --file <path>");const r=await I(t);r.some(u=>u.slug===e)&&d(`"${e}" is an item file on this board; pick another slug`);for(const u of r)((await g(u.file,null))?.waves||[]).some(h=>h.slug===e)&&d(`this board already has a wave "${e}"`);const l=r.find(u=>u.slug===F(s[0][0]).file)||d(`this board has no item file "${F(s[0][0]).file}"`),{itemFile:f}=await Jt(t,l.slug),p=ya(l,f,s.map(([u])=>u),e);new Set(p).size!==p.length&&d("--card names a card twice");const w={slug:e,title:v(n,"--title"),goal:v(a,"--goal"),items:p,created:new Date().toISOString()};return s.forEach(([,u],h)=>xa(l,f,w,p[h],u)),f.waves=[...f.waves||[],w],Do(t,l,f,w,o)}c(of,"createWave");async function sf(t,e,{add:n,remove:a,card:i,after:o,title:s,goal:r,note:l}){[n,a,i,o,s,r,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 ${e} <file>/<id> --after <file>/<id>,\u2026`),i!==void 0&&o===void 0&&d(`wave set ${e} ${i} needs --after <file>/<id>,\u2026 ("" clears its links)`);const{source:f,itemFile:p,wave:w}=await $e(t,e);w.finished_at&&d(`the wave "${e}" is finished`),w.items=Mt(w),delete w.phases;const u=a===void 0?[]:await Ac(t,e);if(n!==void 0){const[m]=ya(f,p,[n],e);w.items.includes(m)&&d(`${n} is already in the wave "${e}"`),w.items.push(m),xa(f,p,w,m,ba(o))}if(i!==void 0){const{file:m,id:k}=F(i);(m!==f.slug||!w.items.includes(k))&&d(`${i} is not in the wave "${e}"`),xa(f,p,w,k,ba(o))}if(a!==void 0){const[m]=ya(f,p,[a],e);w.items.includes(m)||d(`${a} is not in the wave "${e}"`),w.items.length===1&&d(`${a} is the wave's last card; wave finish ${e} ends the wave`);const k=c(j=>!G(j)&&(j.deps||[]).some(A=>z(f.slug,A)===`${f.slug}/${m}`),"waiting"),_=p.items.filter(j=>w.items.includes(j.id)&&j.id!==m&&k(j)),N=p.items.find(j=>j.id===m);if(_.length&&!(N&&G(N))){const j=await Promise.all(_.map(async A=>`${await K(t,`${f.slug}/${A.id}`)} waits on ${a}: wave set ${e} ${f.slug}/${A.id} --after <its other conflicts, or "">`));d(`${a} can't leave the wave "${e}" while open cards wait on it; relink each, then remove:
33
+ ${j.join(`
34
+ `)}`)}if(N&&G(N))for(const j of p.items.filter(A=>w.items.includes(A.id)&&A.deps))j.deps=j.deps.filter(A=>z(f.slug,A)!==`${f.slug}/${m}`),j.deps.length||delete j.deps;w.items=w.items.filter(j=>j!==m)}s!==void 0&&(w.title=v(s,"--title")),r!==void 0&&(w.goal=v(r,"--goal"));const h=await Do(t,f,p,w,l);return a!==void 0&&await Pc(t,e,u,a),h}c(sf,"changeWave");async function Ac(t,e){const n=[];for(const a of await I(t)){const i=await g(a.file,null);for(const o of Array.isArray(i?.items)?i.items:[])G(o)||!(o.deps||[]).includes(`wave:${e}`)||(await Ct(t,a.slug,o)).length&&n.push(`${a.slug}/${o.id}`)}return n}c(Ac,"waveBlocked");async function Pc(t,e,n,a){const i=`${await K(t,a)} left the wave "${e}"`;for(const o of n){const{item:s}=await x(t,o);if(s.blocked_until||(await Ct(t,F(o).file,s)).length)continue;const{line:r}=await W(t,o,"unblocked",{lifted:i});await ua(t,o,r.ts),await $t({dir:t},"card-unblocked",{key:o})}}c(Pc,"noticeWaveUnblocked");async function rf(t,e){const{source:n,itemFile:a,wave:i}=await $e(t,e),o=a.items.filter(s=>Mt(i).includes(s.id)&&!G(s));if(o.length){const s=await Promise.all(o.map(r=>K(t,`${n.slug}/${r.id}`)));d(`the wave "${e}" still has ${s.join(", ")} open; wave finish waits until every card in it is Done`)}return i.finished_at=new Date().toISOString(),await b(n.file,a),{file:n.file}}c(rf,"finishWave");async function cf(t,e,n){const{source:a,itemFile:i,wave:o}=await $e(t,e),s=await O.readFile($.join(a.dir,ka(e)),"utf8").catch(()=>"");return{title:o.title,markdown:`**Goal** ${o.goal}
34
35
 
35
- **Order** ${$o(a,i,o,n)}
36
+ **Order** ${Fo(a,i,o,n)}
36
37
 
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};
38
+ ${s}`}}c(cf,"readWaveNotes");async function U(t,e,n,a="main"){const{id:i}=F(e);/^[\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 $.join($.resolve(n),"agents",o);const{source:s}=await x(t,e);return $.join(s.dir,"agents",o)}c(U,"builderFile");const No=c((t,e)=>typeof t=="string"&&/^\d{4}-\d\d-\d\dT/.test(t)&&!Number.isNaN(Date.parse(t))?new Date(t).toISOString():d(`${e} takes an ISO time such as 2026-10-23T16:30:00-07:00`),"isoTime"),Dc=Nn.progress,Ro=80,Eo="reading, building, testing, checking or landing";function Lo(t,e,n,a){t||d(`${a} needs --site, since the line goes to the item's thread`),e!==void 0&&!Dc.includes(e)&&d(`--phase takes ${Eo}, not "${e}"`);const i=v(n,a);return i.length>Ro&&d(`${a} is ${i.length} characters; the cap is ${Ro}. Say it shorter.`),{phase:e,detail:i}}c(Lo,"progressLine");const Bo=c((t,e,n)=>e?{detail:t.detail}:(t.phase===void 0&&d(`${n} needs --phase ${Eo}`),t),"forAgent");async function _a(t,e,n="main",a,{phase:i,detail:o},s){const r=Ut(a),l=r||n==="main"?"":a.kind===X?n:Je(a)||a.kind||n,f=r?ls[a.kind]:[l,i].filter(Boolean).join(" "),p=new RegExp(`^(?:${f}|${i??f})\\s*:?\\s+`,"i"),w=f?`${f[0].toUpperCase()}${f.slice(1)}: ${o.replace(p,"")}`:o,u=Z(t,e);(await J(u)).filter(h=>h.kind==="status").pop()?.text!==w&&await lt(u,{ts:s,who:"claude",kind:"status",run:a.started,...i?{phase:i}:{},...r?{planner:!0}:{},text:w})}c(_a,"statusToThread");const Fc={prototype:"sonnet",grill:"opus",[gt]:"sonnet"};async function Ie(t,e,{label:n,phase:a,progress:i,dir:o,agent:s="main",kind:r,model:l,eta:f,worktree:p,ownWorktree:w,dispatch:u}){a!==void 0&&i===void 0&&d('--phase goes with --progress "<detail>"');const h=it(i,nt=>Lo(t,a,nt,"--progress")),m=await U(t,e,o,s),k=new Date().toISOString(),_=it(r,nt=>/^[\w.-]{1,30}$/.test(nt)?nt:d("--kind takes a short word such as claude or codex")),N=Oe(_),j=l??(_==="opus"||_==="sonnet"?_:void 0),A=["build",...qc].includes(N),D=await g(m,null),T=c(nt=>["tap","hub"].includes(nt?.dispatched)&&!nt.done,"tapOpen");if(u==="session"&&T(D))return{file:m,alreadyOpen:!0,lines:[]};s!=="main"&&_!==gt&&_!==X&&T(await g(await U(t,e,o),null))&&d(`the Implement tap opened ${e}'s main builder record and no agent has taken it up; run builder start ${e} without --agent first`);const M=D?.dispatched&&!D.done?D:null,S=A&&!o?(await x(t,e)).item:void 0,L=D&&ee(D).length?D:null,C=M&&N==="build"?{kind:Oe(M.kind),model:M.model,reason:{tap:"The owner's Build tap picked it.",hub:"The board's pull picked it."}[M.dispatched]??"The session's dispatch picked it."}:A?Bn({card:S??{}}):null;C&&N!=="build"&&N!==C.kind&&d(`--kind ${N}: ${C.kind} builds now. ${C.reason} Pass --kind ${C.kind}, or --kind build.`);const q=C?C.kind:N,ot=Ua({dir:t});if(!(M&&!u)&&$s(ot,q))throw new pt(gs(ot,q));const R=h&&Bo(h,Ut({kind:q}),"--progress");l!==void 0&&!["opus","sonnet"].includes(l)&&d("--model takes opus or sonnet"),j!==void 0&&q==="codex"&&d("--model is for kind claude; codex needs none");const H=q==="claude"?Cn():"auto";j!==void 0&&H!=="auto"&&j!==H&&d(`--model ${j}: the hub's Claude model is ${H==="opus"?"Opus":"Sonnet"}. Pass --model ${H}, or no --model.`);const tt=q==="claude"?j??C?.model??(M&&Oe(M.kind)==="claude"?M.model:void 0)??"opus":Ut({kind:q})?j??(M?.kind===q?M.model:void 0)??Fc[q]:q===X?j??D?.model:void 0,Q={label:n===void 0?"Builder":v(n,"--label"),...q?{kind:q}:{},...tt?{model:tt}:{},started:k,...f===void 0?{}:{eta:No(f,"--eta")},...u?{dispatched:u}:{},...L?{calls:L.calls,...L.verified?{verified:L.verified}:{}}:{},...Cc(D)?{session:D.session}:{},...q===X&&D?.kind===X?{label:D.label,task:D.task,writes:D.writes,requested:D.requested}:{}},bt=A&&!o&&!G(S)&&S.lane!=="progress",xt=bt?await yt(t,e,{start:!0}):{lines:[]};let Y,At=null;const qe=w&&A&&s==="main"?[D?.worktree,S?.worktree].find(nt=>nt?.path&&wt(nt.path))??null:null,ke=(M??L)?.worktree??qe,ye=p??(A&&s==="main"&&!ke?w:void 0);try{if(ye){const nt=await V($.dirname($.dirname(m)),F(e).id),bn=(await Promise.all(nt.filter(ht=>ht!==m).map(ht=>g(ht,null)))).filter(ht=>ht?.worktree?.path&&!ht.done).map(ht=>ht.worktree.path);({remade:At,...Y}=await ye({busy:bn}))}else Y=ke}catch(nt){throw bt&&await Tc(t,e,S),nt}await Nc(m,D),await b(m,Y?{...Q,worktree:Y}:Q),S?.rebase_run&&s==="main"&&![Co,Ta].includes(Q.label)&&await Io(t,e),C?.inputs&&t&&Ns(Ka(t),{call:"builder-pick",key:e,result:[q,q==="claude"?tt:null].filter(Boolean).join(" "),reason:C.reason,inputs:{...C.inputs,...j?{askedModel:j}:{},...u?{dispatch:u}:{}}}),At&&t&&await W(t,e,"worktree-remade"),R&&await _a(t,e,s,Q,R,k);const st=_===gt||_===X?[]:await fn(t,e,"Acted on: builder started");return t&&A&&await Vr(t,e),{file:m,kind:q??null,model:tt??null,reason:C?.reason??null,worktree:Y?.path??null,lines:[...N==="build"?[`kind: ${q}. ${C.reason}`]:[],...Je(Q)?[`model: ${Je(Q)}`]:[],...t?[`ref: ${await K(t,e)}`]:[],...Y?[`worktree: ${Y.path}`]:[],...At?[At]:[],...xt.lines,...st]}}c(Ie,"startBuilder");async function Nc(t,e){if(!e?.started||!e.done||!t.endsWith(".json")||$.basename(t).includes("@"))return;const{label:n,kind:a,model:i,started:o,done:s,session:r}=e,l=$.join($.dirname(t),`${$.basename(t,".json")}@past-${Date.parse(o)}.json`);await b(l,{label:n,...a?{kind:a}:{},...i?{model:i}:{},started:o,done:s,...r?.log?{session:{log:r.log}}:{}})}c(Nc,"keepPastRun");async function lf(t,e,n,a="tap"){try{const{held:i,blockers:o}=await Ec(t,e);if(i&&await mo(t,"card-hold",{key:e,hold:!1}),o.length)return await Lc(t,e,o);try{const s=await Ie(t,e,{kind:"build",worktree:n,dispatch:a});return{key:e,ref:await K(t,e),kind:s.kind,...s.model?{model:s.model}:{},reason:s.reason,worktree:s.worktree}}catch(s){throw i&&await Ic(t,e,i),s}}catch(i){if(!(i instanceof pt))throw i;return await W(t,e,Ia,{why:i.message}),null}}c(lf,"startTappedBuild");async function df(t,e,{pr:n,conflict:a,base:i},o){const{source:s,itemFile:r,item:l}=await x(t,e);l.rebase_run={pr:n,base:i,conflict:a,at:new Date().toISOString()},await b(s.file,r);try{const f=await Ie(t,e,{kind:"build",label:Co,dispatch:"hub",ownWorktree:o});return{key:e,kind:f.kind,...f.model?{model:f.model}:{},worktree:f.worktree}}catch(f){throw await Io(t,e),f}}c(df,"startRebase");const Co="Rebase";async function Io(t,e){const{source:n,itemFile:a,item:i}=await x(t,e);delete i.rebase_run,await b(n.file,a)}c(Io,"endRebaseRun");async function ff(t,e){const{itemFile:n,item:a}=await x(t,e),i=ut(t,n);return Pe(a,i).find(o=>o.repo===i)?.number??null}c(ff,"cardPR");async function uf(t,e,{from:n,to:a,replayed:i}){const{source:o,itemFile:s,item:r}=await x(t,e);return r.rebase_run?(r.rebased=[...r.rebased||[],{from:n,to:a,base:r.rebase_run.base,replayed:i,at:new Date().toISOString()}],delete r.rebase_run,await b(o.file,s),!0):!1}c(uf,"recordRebase");const Rc=c((t,e,n)=>{const a=new Map((t.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===e)return!0;return!1},"onlyRebased");async function Ec(t,e){const n=Ua({dir:t});ks(n)&&d(hs(n));const{item:a}=await x(t,e),i=a.held?{held:a.held,pick_up_at:a.pick_up_at,held_at:a.held_at}:null;return i&&mt(a)&&d("it's held. Unhold it, then tap Implement."),await fa(t,e)&&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 nc(t,e,a)}}c(Ec,"tapRefusals");async function wf(t,e,n){await Gt(t,e,{who:"love",by:n,command:"implement"}),await zr(t,e,{})}c(wf,"implementAsIs");async function Lc(t,e,n){const a=n.join(", ");return await $t({dir:t},"card-bump",{key:e}),await W(t,e,"cant-start-yet",{names:a}),{key:e,ref:await K(t,e),bumped:n,line:`bumped: ${e} waits on ${a}, so it stays in Todo, bumped to the top`}}c(Lc,"bumpBlocked");async function kn(t,e){const n=await U(t,e),a=await g(n,null);return a?.started&&!a.done&&ie(a)?{file:n,record:a}:null}c(kn,"openBuild");async function To(t,e,{cause:n,words:a,by:i,until:o}){const s=await kn(t,e);return!s||s.record.pause?!1:(await b(s.file,{...s.record,pause:{cause:n,words:a,at:new Date().toISOString(),...i?{by:i}:{},...o?{until:o}:{}}}),!0)}c(To,"requestPause");async function Bc(t,e){const n=await kn(t,e),a=n?.record.pause;if(!a||a.paused)return a??null;const i=new Date().toISOString(),o=(await J(Z(t,e))).filter(r=>r.kind==="status"&&r.phase&&!r.planner&&String(r.ts)>=String(n.record.started)).pop()?.phase,s={...a,paused:i,...o?{phase:o}:{}};return await b(n.file,{...n.record,pause:s}),await W(t,e,"paused",{phase:o,words:a.words,clear:Ma[a.cause][0]}),s}c(Bc,"markPaused");async function yn(t,e,{dir:n,agent:a="main"}){if(!t||n||a!=="main")return[];const i=await Bc(t,e);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.`]:[]}c(yn,"pauseReply");async function pf(t,e){const n=await kn(t,e),a=n?.record.pause;if(!a?.paused)return null;const{pause:i,...o}=n.record;return await b(n.file,o),await W(t,e,"resumed",{clear:Ma[a.cause][1]}),{pause:a,record:o}}c(pf,"resumePaused");async function mf(t,e){const n=await kn(t,e);if(!n?.record.pause||n.record.pause.paused)return;const{pause:a,...i}=n.record;await b(n.file,i)}c(mf,"dropPause");async function hf(t,e,n){const a=await U(t,e),i=await g(a,null),o=new Date().toISOString();i&&!i.done&&await b(a,{...i,done:o});const{source:s,itemFile:r,item:l}=await x(t,e);l.lane==="progress"&&Le(l,"todo",o),await b(s.file,r),await W(t,e,Ia,{why:n})}c(hf,"abandonLaunch");async function gf(t,e,[n,a],i="main"){const o=await U(t,e,void 0,i),s=await g(o,null);s&&!s.done&&await b(o,{...s,done:new Date().toISOString()}),await pe(t,e),await W(t,e,n,a)}c(gf,"abandonPlanner");async function $f(t,e,n,a,i="main",o=null){const s=await U(t,e,void 0,i),r=await g(s,null);await b(s,{...r,session:{id:n,pid:a,log:o,at:new Date().toISOString()}})}c($f,"recordSession");async function kf(t,e){const{source:n}=await x(t,e);return(await Promise.all((await V(n.dir,F(e).id)).map(i=>g(i,null)))).filter(i=>i&&i.started&&!i.done).map(i=>({pid:i.session?.pid??null,at:i.session?.at??null}))}c(kf,"openAgents");async function yf(t,e){const{source:n}=await x(t,e);return(await Promise.all((await V(n.dir,F(e).id)).map(i=>g(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}))}c(yf,"openRuns");async function bf(t,e,n){const{source:a}=await x(t,e),i=new Date().toISOString();for(const o of await V(a.dir,F(e).id)){const s=await g(o,null);s?.started&&!s.done&&ie(s)&&await b(o,{...s,stopped:{run:n,at:i}})}}c(bf,"markStopped");async function xf(t,e){const{source:n,itemFile:a,item:i}=await x(t,e);i.merge_ready&&(i.merge_ready.stopped=new Date().toISOString(),await b(n.file,a))}c(xf,"stopHeldMerge");async function _f(t,e,n){const a=v(n,"item stall --reason"),{source:i,itemFile:o,item:s}=await x(t,e);return s.lane==="done"&&d(`${await K(t,e)} is done; there is nothing to stall`),s.stalled={reason:a,at:new Date().toISOString()},await b(i.file,o),a}c(_f,"markStalled");async function Sf(t,e){const{source:n,itemFile:a,item:i}=await x(t,e);i.stalled&&(delete i.stalled,await b(n.file,a))}c(Sf,"clearStall");async function vf(t,e){const{item:n}=await x(t,e),a=await J($.join(t,"data","events.jsonl")),i=c(r=>a.filter(l=>l.kind===r&&l.data?.key===e).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,...Rt(n).map(r=>r.merged_at)].filter(Boolean).map(String).sort().pop()||"";return o>s?o:null}c(vf,"landFirst");async function jf(t,e,n){const{item:a}=await x(t,e);G(a)&&d(`${await K(t,e)} is done; there is nothing to bump`);const i=await $t({dir:t},n?"card-bump":"card-unbump",{key:e});return(await J($.join(t,"data","events.jsonl"))).find(o=>o.id===i).ts}c(jf,"bumpCard");async function qo(t,e){return(await J(Z(t,e))).filter(n=>n.kind==="status").pop()?.phase==="landing"}c(qo,"isLanding");const Cc=c(t=>!!t?.session&&t.session.at>(t.done??t.started),"launchedSession");async function Of(t,e,n){const a=await g(await U(t,e),null);if(a&&!a.done)return null;const i=await Ie(t,e,{kind:n,dispatch:"hub"});return{key:e,kind:n,model:i.model}}c(Of,"startPlanner");async function Af(t,e,n){const{source:a}=await x(t,e);if((await Promise.all((await V(a.dir,F(e).id)).map(r=>g(r,null)))).some(r=>r?.kind===gt&&r.started&&!r.done))return null;const o=us(n);if(await g(await U(t,e,void 0,o),null))return null;const s=await Ie(t,e,{kind:gt,agent:o,dispatch:"hub"});return{key:e,kind:gt,model:s.model,agent:o}}c(Af,"startVisual");const Sa=c(t=>typeof t=="string"&&/^[a-z][a-z0-9-]{0,29}$/.test(t)&&t!=="main"&&!t.startsWith(`${gt}-`)?t:d('--name takes a short lowercase name such as tests or docs-pass: letters, digits and "-", not main or visual-\u2026'),"helperName");async function va(t,e){const{item:n}=await x(t,e);return[(await g(await U(t,e),null))?.worktree,n.worktree].find(i=>i?.path&&wt(i.path))??null}c(va,"cardWorktree");async function Pf(t,e,{name:n,task:a,writes:i,model:o}){P(t);const s=Sa(n),r=v(a,"--task");o!==void 0&&!["opus","sonnet"].includes(o)&&d("--model takes opus or sonnet");const l=Cn();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:f}=await x(t,e);G(f)&&d(`${await K(t,e)} is Done; a helper needs an open card`);const p=await U(t,e,void 0,s);return wt(p)&&d(`${e} already has an agent named ${s}; pick another --name`),i&&!await va(t,e)&&d(`a helper that writes branches from ${e}'s worktree, and the card has none; start its build first, or drop --writes`),await b(p,{label:s,kind:X,task:r,writes:!!i,...o?{model:o}:{},requested:new Date().toISOString()}),await W(t,e,"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 ${e} --name ${s}`]}}c(Pf,"requestHelper");async function Df(t,e,n,a){const i=await U(t,e,void 0,n),o=await g(i,null);if(o?.kind!==X||o.started||o.done)return null;const{item:s}=await x(t,e),r=Bn({card:s}),l=Cn(),f=o.model??(l!=="auto"?l:r.kind==="claude"?r.model:"opus"),p=o.writes?await va(t,e):null;o.writes&&!p&&d(`${e} has no worktree for helper ${n} to branch from`),await b(i,{...o,model:f});const w=await Ie(t,e,{agent:n,kind:X,dispatch:"hub",worktree:p?({busy:u})=>a({repo:p.repo,base:p.branch,busy:u}):void 0});return{key:e,kind:X,model:f,agent:n,task:o.task,writes:o.writes,worktree:w.worktree}}c(Df,"startHelper");async function Ff(t,e,n){const a=await g(await U(t,e,void 0,Sa(n)),null);return a?.kind!==X&&d(`${e} has no helper named ${n}`),{record:a,card:await va(t,e)}}c(Ff,"helperOf");async function Nf(t,e,{name:n,agent:a="main",timeout:i=9,every:o=2e3,alive:s}){P(t);const r=Number(i);r>0||d("--timeout takes minutes, a number above 0");const l=await U(t,e,void 0,Sa(n)),f=await U(t,e,void 0,a),p=c(async u=>{const h=await g(f,null);if(!h?.started||h.done)return;const{waits:m,...k}=h;await b(f,u?{...k,waits:u}:k)},"mark"),w=Date.now()+r*6e4;await p(n);try{for(;;){const u=await g(l,null);if(u?.kind!==X&&d(`${e} has no helper named ${n}`),u.done){const m=u.writes&&u.worktree?[`branch ${u.worktree.branch} in ${u.worktree.path}: merge it with agent merge ${e} --name ${n}`]:[];return{lines:[`helper ${n} done: ${u.note??"no hand-back"}`,...m]}}const h=u.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 ${u.started?"running":u.writes?"waiting for an implementer slot":"waiting to launch"} after ${r} minutes; run agent wait again`),await new Promise(m=>setTimeout(m,o))}}finally{await p(null)}}c(Nf,"waitHelper");async function Rf(t,e,n="main"){return n==="main"?!1:(await g(await U(t,e,void 0,n),null))?.kind===X}c(Rf,"isHelper");async function Ef(t,e,n="main"){const a=await g(await U(t,e,void 0,n),null);return!!(a?.started&&!a.done&&Ut(a))}c(Ef,"isPlanner");async function Lf(t,e,{file:n,title:a}){P(t);const i=v(a,"--title");(typeof n!="string"||!n.endsWith(".html"))&&d("--file takes the page, an .html file");const o=await O.readFile($.resolve(n),"utf8").catch(()=>d(`--file ${n} can't be read`));o.trim()||d(`--file ${n} is empty`),await x(t,e);const{id:s}=F(e),r=$.join(t,"public","visuals",s);await O.mkdir(r,{recursive:!0});const l=i.toLowerCase().replace(/[^a-z0-9]+/g,"-").slice(0,60).replace(/^-+|-+$/g,"")||"visual";let f=`${l}.html`;for(let w=2;wt($.join(r,f));w++)f=`${l}-${w}.html`;await O.writeFile($.join(r,f),o,{flag:"wx"});const p=`visuals/${s}/${f}`;return await lt(Z(t,e),{ts:new Date().toISOString(),who:"claude",kind:gt,text:v(i,"--name"),link:Mn(p,"--link")}),{lines:[p]}}c(Lf,"addVisual");async function Bf(t){const e=[];for(const n of await I(t)){const a=await g(n.file,null);for(const i of a?.items||[]){if(G(i))continue;const o=await g($.join(n.dir,"agents",`${i.id}.json`),null);!o?.started||o.done||!o.dispatched||Ut(o)||e.push({key:`${n.slug}/${i.id}`,kind:Oe(o.kind),model:o.model})}}return e}c(Bf,"cutOffBuilds");async function Ic(t,e,n){const{source:a,itemFile:i,item:o}=await x(t,e);for(const s of Object.keys(n))n[s]===void 0?delete o[s]:o[s]=n[s];await b(a.file,i)}c(Ic,"restoreHold");async function Tc(t,e,n){const{source:a,itemFile:i,item:o}=await x(t,e);for(const s of["lane","lane_at","closed_at","reopened_at"])n[s]===void 0?delete o[s]:o[s]=n[s];await b(a.file,i)}c(Tc,"restoreLane");async function Cf(t,e){return t||d("audit reads a board: pass --site <route>"),e!==void 0&&F(e),{lines:Rs(Es(Ka(t),{key:e}))}}c(Cf,"auditLog");const qc=["codex","claude"];async function If(t,e){const{item:n}=await x(t,e),{kind:a,model:i,reason:o}=Bn({card:n});return{lines:[`${[a,i].filter(Boolean).join(" ")}: ${o}`]}}c(If,"pickBuilder");async function Mo(t,e,{dir:n}={}){const{id:a}=F(e),i=$.dirname(await U(t,e,n)),o=await V($.dirname(i),a);return(await Promise.all(o.map(r=>g(r,null)))).filter(r=>r?.worktree?.path&&r.kind!==X).sort((r,l)=>String(l.started).localeCompare(String(r.started)))[0]?.worktree??null}c(Mo,"recordedWorktree");async function Tf(t,e){const{source:n}=await x(t,e),i=(await Promise.all((await V(n.dir,F(e).id)).map(o=>g(o,null)))).filter(o=>o?.session?.id&&o.worktree?.path&&o.kind!==X).sort((o,s)=>String(s.started).localeCompare(String(o.started)))[0];return i?{id:i.session.id,worktree:i.worktree.path}:null}c(Tf,"recordedSession");async function qf(t,e,n,{phase:a,dir:i,agent:o="main",eta:s,clearEta:r}){s!==void 0&&r&&d("pass --eta or --clear-eta, not both");const l=Lo(t,a,n,"builder progress"),f=await U(t,e,i,o),p=await g(f,null);(!p?.started||p.done)&&d(`builder progress needs an open builder record on ${e}; run builder start first`);const w=Bo(l,Ut(p),"builder progress");return s!==void 0&&(p.eta=No(s,"--eta")),r&&delete p.eta,(s!==void 0||r)&&await b(f,p),await _a(t,e,o,p,w,new Date().toISOString()),{file:f,lines:await yn(t,e,{dir:i,agent:o})}}c(qf,"builderProgress");async function Mf(t,e){const{source:n}=await x(t,e),a=Z(t,e);if((await J(a)).filter(s=>s.kind==="status").pop()?.phase==="landing")return;const o=(await Promise.all((await V(n.dir,F(e).id)).map(s=>g(s,null)))).filter(s=>s?.started&&ie(s)&&s.label!==Ta).map(s=>s.started).sort().pop();await lt(a,{ts:new Date().toISOString(),who:"claude",kind:"status",phase:"landing",text:"Landing: rebasing onto main and landing",...o?{run:o}:{}})}c(Mf,"landingLine");const ee=c(t=>(t?.calls||[]).filter(e=>!t.verified||e.at>t.verified.at),"unverifiedCalls");async function Wf(t,e,n,{dir:a,agent:i="main"}){const o=v(n,"builder call");/\n/.test(o)&&d("builder call takes one line; record each call on its own");const s=await U(t,e,a,i),r=await g(s,null);return(!r?.started||r.done)&&d(`builder call needs an open builder record on ${e}; run builder start first`),await b(s,{...r,calls:[...r.calls||[],{at:new Date().toISOString(),text:o}]}),{file:s,lines:await yn(t,e,{dir:a,agent:i})}}c(Wf,"recordCall");async function Te(t,e){const n=await J(Z(t,e)),a=new Map(n.filter(i=>i.kind===Ue.answer).map(i=>[i.ask,i]));return n.filter(i=>i.kind===Ue.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}})}c(Te,"builderAsks");const Mc=c(t=>kt().find(e=>Ft(e.dir,t))??{dir:t},"boardTarget");async function Uf(t,e,n,{agent:a="main"}){P(t);const i=v(n,"builder ask");/\n/.test(i)&&d("builder ask takes one line; ask each question on its own");const o=await g(await U(t,e,void 0,a),null);(!o?.started||o.done)&&d(`builder ask needs an open builder record on ${e}; run builder start first`);const s=`ask${(await Te(t,e)).length+1}`;return await lt(Z(t,e),{ts:new Date().toISOString(),who:`${Je(o)||o.kind||"Claude"} builder`,kind:Ue.ask,ask:s,agent:a,text:i}),await $t(Mc(t),"builder-ask",{key:e,ask:s,agent:a,text:i}),{id:s,lines:await yn(t,e,{agent:a})}}c(Uf,"askOrchestrator");async function Hf(t,e,n,a,{deferred:i=!1}={}){P(t);const o=v(a,"--text"),s=(await Te(t,e)).find(l=>l.id===n)||d(`no ask "${n}" on ${e}; builder asks ${e} lists them`);s.answer&&d(`${n} on ${e} is already ${s.deferred?"deferred":"answered"}`);const r=new Date().toISOString();if(await lt(Z(t,e),{ts:r,who:"claude",kind:Ue.answer,ask:n,...i?{deferred:!0}:{},text:o}),i){const l=await U(t,e,void 0,s.agent),f=await g(l,null);f&&await b(l,{...f,calls:[...f.calls||[],{at:r,text:`Question: ${s.text} (deferred to the owner)`}]})}return{lines:[]}}c(Hf,"answerBuilder");async function Jf(t,e,{wait:n=!1,timeout:a=9,every:i=2e3}={}){P(t);const o=Number(a);o>0||d("--timeout takes minutes, a number above 0");const s=Date.now()+o*6e4;for(;;){const r=await Te(t,e);if(!r.length)return{lines:["no asks yet"]};const l=r.filter(w=>!w.answer),f=r.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:f};const p=await yn(t,e,{});if(p.length)return{lines:[...f,...p]};Date.now()>=s&&d(`${l.map(w=>w.id).join(", ")} still waiting on the orchestrator after ${o} minutes; run builder asks ${e} --wait again`),await new Promise(w=>setTimeout(w,i))}}c(Jf,"listAsks");async function Wc(t,e,n){const a=e.topic.startsWith("item:")&&!e.topic.startsWith("item:ask:")?e.topic.slice(5):d(`${n}.ask goes on a card's question, topic item:<file>/<id>`),i=(await Te(t,a)).find(o=>o.id===e.ask)||d(`${n}.ask names no ask "${e.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`)}c(Wc,"checkAskQuestion");async function Kf(t,e,{dir:n,agent:a="main"}){const i=await g(await U(t,e,n,a),null),o=i?.calls||[];if(!o.length)return{lines:["no judgment calls recorded"]};const{verified:s}=i,r=ee(i).length?"not verified yet":`verified ${s.at}${s.note?`: ${s.note}`:""}`;return{lines:[...o.map((l,f)=>`${f+1}. ${l.text}`),r]}}c(Kf,"listCalls");async function zf(t,e,{dir:n,agent:a="main",note:i}){const o=await U(t,e,n,a),s=await g(o,null);if(ee(s).length||d(`${e} has no unverified judgment calls${a==="main"?"":` from agent ${a}`}; builder calls ${e} lists them`),t){const r=new Set((await g(B(t),{items:[]})).items?.filter(f=>f.topic===`item:${e}`&&f.ask).map(f=>f.ask)),l=(await Te(t,e)).find(f=>f.deferred&&f.agent===a&&!r.has(f.id));l&&d(`${l.id} on ${e} was deferred to the owner; add its question with "ask": "${l.id}" first`)}return await b(o,{...s,verified:{at:new Date().toISOString(),...i===void 0?{}:{note:v(i,"--note")}}}),o}c(zf,"verifyCalls");async function Gf(t,e){const{source:n,item:a}=await x(t,e),i=c((s,r)=>[(s.options||[]).find(l=>l.id===r.choice)?.label,r.note].filter(Boolean).join(": ")||"answered","said"),o=(await ft(t,e)).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:e,ref:await K(t,e),title:a.title||e,notes:a.plan?$.join(n.dir,a.plan):null,thread:Z(t,e),worktree:(await Mo(t,e))?.path??null,answers:o,links:await Uc(t,e),rebasePR:a.rebase_run?.pr??null,prototype:a.gates?.prototype??null}}c(Gf,"briefFacts");async function Uc(t,e){const n=new Map;for(const r of await I(t))for(const l of(await g(r.file,null))?.items||[])n.set(`${r.slug}/${l.id}`,{entry:r,it:l});const a=F(e).file,{it:i}=n.get(e),o=[...n].filter(([,r])=>(r.it.related||[]).some(l=>z(r.entry.slug,l)===e)).map(([r])=>r),s=[...i.parent?[["parent",z(a,i.parent)]]:[],...(i.deps||[]).map(r=>["depends on",z(a,r)]),...[...new Set([...(i.related||[]).map(r=>z(a,r)),...o])].map(r=>["related",r])].filter(([,r])=>n.has(r));return Promise.all(s.map(async([r,l])=>{const{entry:f,it:p}=n.get(l);return{relation:r,ref:await K(t,l),title:p.title||l,lane:qt[p.lane||"todo"],notes:p.plan?$.join(f.dir,p.plan):null,thread:Z(t,l)}}))}c(Uc,"cardLinks");async function Qf(t){const e=[];for(const n of await I(t)){const a=await g(n.file,null);for(const i of a?.items||[]){if(G(i))continue;const o=(await Promise.all((await V(n.dir,i.id)).map(f=>g(f,null)))).filter(Boolean);if(!Rn(o))continue;const s=o.map(f=>f.verified?.at).filter(Boolean).sort().at(-1);if(!s||o.some(f=>ee(f).length))continue;const r=`${n.slug}/${i.id}`;if((await ft(t,r)).asked.some(f=>f.state==="open")||await wn(t,r,i))continue;const l=o.filter(f=>f.worktree?.path&&f.kind!==X).sort((f,p)=>String(p.started).localeCompare(String(f.started)))[0];l&&wt(l.worktree.path)&&e.push({key:r,verified:s,kind:Oe(l.kind),model:l.model})}}return e}c(Qf,"landable");async function Vf(t,e){const{source:n,item:a}=await x(t,e);(await Promise.all((await V(n.dir,a.id)).map(r=>g(r,null)))).some(r=>r?.started&&!r.done)||d(`${e} has no open builder; only a card whose lander stopped has one to relaunch`),await yt(t,e,{handBack:!0,next:"A lander relaunches to land the verified build"});const o=$.join(t,"data","landers.json"),s=await g(o,{});delete s[e],await O.writeFile(o+".tmp",JSON.stringify(s)+`
39
+ `),await O.rename(o+".tmp",o)}c(Vf,"relaunchLander");async function Zf(t,e,{dir:n}={}){const{id:a}=F(e),i=await V($.dirname($.dirname(await U(t,e,n))),a),o=[];for(const s of i){if(!ee(await g(s,null)).length)continue;const r=$.basename(s,".json").slice(String(a).length+1)||"main";o.push(r==="main"?"":` --agent ${r}`)}o.length&&d(`land: ${e} has judgment calls nobody verified yet; check them with ${o.map(s=>`builder calls ${e}${s}`).join(", ")}, then run ${o.map(s=>`builder verified ${e}${s}`).join(", ")}`)}c(Zf,"checkCallsVerified");async function Yf(t,e,{dir:n,agent:a="main",next:i}){const o=await g(await U(t,e,n,a),null),s=o&&!o.done?ee(o):[];if(!s.length)return null;i!==void 0&&d(`builder done sets ${e}'s Next itself while its record has judgment calls; run it without --next`);const r=s.some(f=>f.text.startsWith("Question:"))?"Answer the builder's questions":"Verify the builder's judgment calls",l=await yt(t,e,{handBack:!0,next:r});return{lines:[`handed back: Next "${r}"`,...l.lines]}}c(Yf,"handBackCalls");async function Xf(t,e,{agent:n="main"}){if(n!=="main")return!1;const a=await U(t,e,void 0,n),i=await g(a,null);if(!i?.started||i.done||!i.worktree?.path)return!1;const{id:o}=F(e);for(const s of await V($.dirname($.dirname(a)),o)){const r=s===a?null:await g(s,null);if(!r?.started||r.done||r.kind==="install")continue;const l=$.basename(s,".json").slice(String(o).length+1);d(`builder done would finish ${e} while agent ${l} is still open; run builder done ${e} --agent ${l} first`)}return!0}c(Xf,"finishesCard");async function tu(t,e){const{source:n,item:a}=await x(t,e);if(G(a))return null;const i=(await Promise.all((await V(n.dir,a.id)).map(s=>g(s,null)))).filter(Boolean);if(!Rn(i))return{held:"the build hasn't handed back; run builder done first"};if(i.some(s=>ee(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 Mo(t,e);return!o||!wt(o.path)?{held:"the build's worktree is gone"}:{waits:(await ft(t,e)).asked.filter(s=>s.state==="open").map(s=>s.id)}}c(tu,"verifiedFinish");async function eu(t,e){const{item:n}=await x(t,e);if(n.reopened_at&&d(`the owner reopened ${await K(t,e)}; it closes on the owner's Done`),n.lane!=="done"||Rt(n).some(o=>o.merged_at))return;const a=String(n.closed_at||"");(await J($.join(t,"data","events.jsonl"))).some(o=>o.kind==="item-message"&&o.data?.key===e&&String(o.ts)>a)&&d(`the owner reopened ${await K(t,e)} with a message; it closes on the owner's Done`)}c(eu,"checkNotReopened");async function V(t,e){const n=$.join(t,"agents");return(await O.readdir(n).catch(()=>[])).filter(a=>a===`${e}.json`||a.startsWith(`${e}@`)&&!a.startsWith(`${e}@past-`)&&a.endsWith(".json")).map(a=>$.join(n,a))}c(V,"builderFiles");async function Wo(t,e,n,{handedBack:a,endHandBack:i=!1,held:o=!1}={}){const s=new Date().toISOString();for(const r of await V(t,e)){const l=await g(r,null);if(!l)continue;if(l.done||l.kind==="install"&&!n){if(i&&l.handed_back){const{handed_back:u,...h}=l;await b(r,h)}continue}if(o&&l.pause)continue;const{eta:f,handed_back:p,...w}=l;await b(r,{...w,done:s,...a?{handed_back:a}:{},...o&&l.started?{hold_stopped:!0}:{}})}}c(Wo,"closeBuilders");async function nu(t,e){const n=await U(t,e),a=await g(n,null);if(!a?.hold_stopped)return null;const{hold_stopped:i,...o}=a;return await b(n,o),{kind:a.kind??null,label:a.label??null}}c(nu,"takeHoldStopped");async function Uo(t,e,{dir:n,agent:a,startedBetween:i,note:o,next:s,requireNext:r=!1}){const l=await U(t,e,n,a),f=await g(l,null);if(!f||f.done)return l;if(i){const T=Date.parse(f.started);if(!(T>=i[0]&&T<=i[1]))return l}const{eta:p,...w}=f,u=new Date().toISOString(),h={...w,done:u,...o?{note:o}:{}},{id:m}=F(e),k=$.dirname($.dirname(l)),_=await Promise.all((await V(k,m)).map(T=>T===l?h:g(T,null))),N=!_.some(T=>T?.started&&!T.done),j=n?{file:$.join(k,"wave.json")}:(await x(t,e)).source,A=await g(j.file,null),D=A?.items?.find(T=>T.id===m);return s!==void 0&&(Xi(D||d(`${e} has no item to set --next on`),s,await un(t,e)),t&&await co(t,e,D,D.next)),r&&D&&En(D,_)&&d(`builder done closes ${e}'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 ${e} or land`),await b(l,h),await pe(t,e,{screen:f.kind==="grill"}),o&&await _a(t,e,a,f,{detail:o},u),s!==void 0&&await b(j.file,A),l}c(Uo,"stopBuilder");async function au(t,e,n){const a=(await g(await U(t,e,void 0,n.agent),null))?.kind,i=await Uo(t,e,n),{phase:o,gates:s}=(await x(t,e)).item,r=await un(t,e)||!!(await g(dt(t),{}))[e];if(o!=={grill:"grilling",prototype:"prototyping"}[a]||r)return{file:i,lines:[]};if(a==="grill"&&s?.prototype&&await rn(t,e,{roundAt:!0}))return{file:i,lines:[]};const{line:l,lines:f}=await W(t,e,"planner-unasked",{kind:a});return{file:i,lines:[l.text,...f]}}c(au,"stopPlanner");async function Hc(t){const e=[];for(const n of await I(t)){const a=await g(n.file,null);for(const i of a?.items||[])for(const o of await V(n.dir,i.id)){const s=await g(o,null);if(!s?.started||s.done)continue;const r=$.basename(o,".json");e.push({key:`${n.slug}/${i.id}`,agent:r.slice(String(i.id).length+1)||"main",label:s.label||"Builder",started:s.started})}}return e}c(Hc,"openBoardBuilders");async function iu(t){const e=await Hc(t);for(const n of e)await Uo(t,n.key,{agent:n.agent,note:"Board closed. Builder record stopped; work is kept for reopening."});return e}c(iu,"closeBoardBuilders");export{Pi as ANSWER_MARK,pt as BoardError,si as PUSH_LINE_MAX,Ye as REPLY_EXTS,sr as SCREEN_AFTER,Jn as UPLOAD_LIMIT,Al as WORKING_KINDS,hf as abandonLaunch,gf as abandonPlanner,Kl as ackPing,kc as addItem,Od as addNotes,$d as addPrototype,xd as addQuestions,od as addReviewers,wd as addRuleTo,gd as addTopic,Lf as addVisual,Hf as answerBuilder,ql as answerCard,zt as appendMessage,W as appendStatus,ef as archiveCards,tf as archiveCount,gn as archiveFile,$n as archiveRank,Uf as askOrchestrator,Bd as askedGate,Cf as auditLog,wn as blockWords,at as boardConfig,jr as boardPosts,ut as boardRepoOf,Wd as breakOwnerCheck,Gf as briefFacts,Gd as builderModel,qf as builderProgress,jf as bumpCard,K as cardName,ff as cardPR,sf as changeWave,Zf as checkCallsVerified,Ld as checkClose,eu as checkNotReopened,Sf as clearStall,iu as closeBoardBuilders,of as createWave,Bf as cutOffBuilds,Tl as dropAskCard,mf as dropPause,pd as editRuleIn,Ll as eventCard,dd as fallBackFromLinear,Bl as fileAsk,Cl as fileAsks,zl as finishHandshake,rf as finishWave,Xf as finishesCard,Yf as handBackCalls,Yl as heardThreads,Kd as heldMerge,Ff as helperOf,ri as hiddenFile,wf as implementAsIs,Id as isFinished,Rf as isHelper,qo as isLanding,Ef as isPlanner,I as itemSources,vf as landFirst,Qf as landable,Mf as landingLine,Jf as listAsks,Kf as listCalls,kd as listPrototypes,ud as listRules,Xl as markHeard,Bc as markPaused,Nl as markRead,_f as markStalled,bf as markStopped,Pl as markWorking,Jd as mergeCard,dc as mergedLine,Xe as messageText,Qd as migrateFollowups,Zd as migrateGates,Yd as migratePhases,Vd as migrateStatuses,af as migrateWaveNames,Ul as moveAsk,Cd as moveCard,Nd as moveQuestions,kf as openAgents,Hc as openBoardBuilders,Or as openDayThread,yf as openRuns,ld as opensPRs,F as parseKey,Md as passOwnerCheck,If as pickBuilder,qd as pickUp,Dd as planAgain,id as postDigest,Jl as postIncomingReview,Ql as postLink,Hd as postPR,Ud as prReport,Sd as publishScreened,vd as publishUnscreened,hr as pushLine,gr as pushUrl,It as readArchive,jt as readAskLinks,na as readBoardRules,Pr as readBoardSettings,vi as readHandshake,kr as readHidden,g as readJSON,yr as readMarks,li as readMarksFile,lc as readNotes,Xn as readPRSlack,Ne as readPrototypes,qi as readScreening,El as readUpload,cf as readWaveNotes,lr as readWorking,Wf as recordCall,mo as recordOwnerTap,fr as recordPush,Vl as recordReaction,uf as recordRebase,Hl as recordReview,$f as recordSession,Tf as recordedSession,Mo as recordedWorktree,Ml as relateAsk,Vf as relaunchLander,bd as removePrototype,sd as removeReviewers,md as removeRuleFrom,dr as repliesSincePush,Wn as replyFilesOf,Pf as requestHelper,To as requestPause,Rd as resolveNoteAnswers,Fd as resolveQuestion,pf as resumePaused,Il as retitleAskCard,Sr as reviewDigest,Zl as reviewDue,hd as saveBoardRules,ue as saveBoardSettings,rd as saveBoardShare,Rl as saveUpload,Dl as sendPush,yd as servedRoundsOpen,cd as setBoardPause,fd as setBoardSetting,Fl as setHidden,yt as setItem,Td as setMainInstall,Gl as setReviewers,Dr as settingsLines,Ot as settingsOf,jd as settleAnswers,_d as settleQuestion,jl as siteDir,or as sitesDir,ed as slackAnswer,nd as slackSay,ad as slackTask,td as slackThread,Ad as splitSharedNotes,zr as stageImplement,Xd as stageImplementAsks,Pd as stageReply,Ie as startBuilder,Df as startHelper,Of as startPlanner,df as startRebase,lf as startTappedBuild,Af as startVisual,Uo as stopBuilder,xf as stopHeldMerge,au as stopPlanner,nf as stripDeadFields,sa as takeFile,nu as takeHoldStopped,zd as tapMerge,Z as threadFile,Zi as topicQuestions,Wl as unfoldAsk,Ed as unrecorded,tu as verifiedFinish,zf as verifyCalls,Nf as waitHelper,ka as waveNoteFile,Ve as wavesDir,mr as withReplies,Ol as writeReply};