@lovek4d/kickflip 0.11.0 → 0.11.1

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