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