@lovek4d/kickflip 0.7.1 → 0.7.2
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/CONTEXT.md +18 -14
- package/SKILL.md +10 -8
- package/bin/kickflip.mjs +1 -1
- package/board/bin/board.mjs +31 -23
- package/board/domain.mjs +7 -7
- package/board/headless.mjs +8 -8
- package/board/hub-lines.mjs +1 -1
- package/board/onboarding.mjs +3 -3
- package/board/public/app-cards.js +1 -1
- package/board/public/app-lanes.js +2 -3
- package/board/public/app-reader.js +7 -3
- package/board/public/app.js +1 -1
- package/board/public/attach.js +18 -2
- package/board/public/dialogs.js +10 -12
- package/board/public/hub.css +0 -3
- package/board/public/linear-key.js +1 -1
- package/board/public/sidebar.css +1 -1
- package/board/public/style.css +31 -22
- package/board/public/ui-cards.js +6 -3
- package/board/public/ui-order.js +1 -1
- package/board/public/ui-questions.js +5 -3
- package/board/public/ui-sidebar.js +1 -1
- package/board/public/ui-threads.js +21 -16
- package/board/public/ui.js +12 -4
- package/board/public/view-hub-settings.js +25 -30
- package/board/public/view-hub.js +21 -5
- package/board/public/view-settings.js +4 -4
- package/board/server.mjs +18 -18
- package/board/store.mjs +26 -27
- package/board/tickets.mjs +3 -3
- package/hub/board-repo.mjs +1 -1
- package/hub/hub.mjs +6 -6
- package/hub/linear.mjs +1 -1
- package/hub/new-board.mjs +2 -2
- package/hub/readiness.mjs +1 -1
- package/hub/review-send.mjs +1 -1
- package/hub/slack.mjs +1 -1
- package/hub/views.mjs +1 -1
- package/package.json +1 -1
- package/starter/server.mjs +3 -3
package/board/store.mjs
CHANGED
|
@@ -1,35 +1,34 @@
|
|
|
1
|
-
var
|
|
2
|
-
`).filter(Boolean).map(t=>JSON.parse(t))}catch{return[]}},"readLines");async function
|
|
3
|
-
`):
|
|
4
|
-
`)})},"writeJSON");async function me(e,t){await
|
|
5
|
-
`)}r(me,"appendLine");async function
|
|
6
|
-
`).filter(y=>y.trim())}))}),w=u.error?typeof u.error.code=="number"?u.error.code:1:0;if(w!==0)throw Object.assign(new
|
|
7
|
-
`)),{exitCode:w});return await
|
|
8
|
-
`).map(n=>n.replace(/\s+/g," ").trim()).find(Boolean)||"";return t.length>Ra?`${t.slice(0,Ra-1).trimEnd()}\u2026`:t}r(As,"pushLine");const Ds={amp:"&",lt:"<",gt:">",quot:'"',apos:"'",nbsp:" "};function Fs(e){return e.replace(/<details[\s\S]*?<\/details>/gi,`
|
|
9
|
-
`).replace(/<(?:br|\/?(?:p|div|li|ul|ol|h\d|blockquote|pre|tr|summary))\b[^>]*>/gi,`
|
|
10
|
-
`).replace(/<[^>]*>/g,"").replace(/&(?:#(x[\da-f]{1,6}|\d{1,7})|(\w+));/gi,(t,n,a)=>{const i=n&&parseInt(n.replace(/^x/i,""),/^x/i.test(n)?16:10);return n?i<=1114111?String.fromCodePoint(i):t:V(Ds,a.toLowerCase())??t})}r(Fs,"htmlText");function Rs(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(Rs,"pushUrl");const Ns=r((e,t)=>`/${h.basename(e)}/#selectedCard=${encodeURIComponent(t)}&reply`,"threadUrl");async function An(e,t,n){const a=As(n);if(!a)return[];const i=_o(t),o=await Eo(i,s=>[a,Os.exec(s)?.[1]].filter(Boolean).join(" \xB7 "),Ns(e,t));return o===null?[]:[`queued push ${i} now reads: ${o}`]}r(An,"freshenPush");const Na=r(e=>h.join(e,"data","hidden.json"),"hiddenFile"),Es=r(async e=>new Set((await g(Na(e),{events:[]})).events||[]),"readHidden");let Ea=Promise.resolve();function Ll(e,t,n){ve(t,"the event id");const a=Ea.then(async()=>{const i=await Es(e);n?i.add(t):i.delete(t),await k(Na(e),{events:[...i]})});return Ea=a.catch(()=>{}),a}r(Ll,"setHidden");const Ia=r(e=>h.join(e,"data","read.json"),"readMarksFile"),Is=r(async e=>(await g(Ia(e),{})).cards||{},"readMarks");let La=Promise.resolve();function Bl(e,t,n={}){const a=La.then(async()=>{const i=await Is(e),o=new Date().toISOString(),s=r((c,u)=>String(u||"")>c?u:c,"later"),l=Object.fromEntries([...new Set(t)].sort().map(c=>[c,s(i[c]||o,n[c])]));JSON.stringify(l)!==JSON.stringify(i)&&await k(Ia(e),{cards:l})});return La=a.catch(()=>{}),a}r(Bl,"markRead");const Dn=20*1024*1024,Te=r(e=>h.join(e,"data","uploads"),"uploadsDir"),Ve=r((e,t)=>h.join(Te(e),".meta",`${t}.json`),"uploadMeta"),Ba=/^\d+-[\w.-]+$/,Ta=/^[\w.+-]+\/[\w.+-]+$/,Ca=r(()=>Object.assign(new re(`a file can be at most ${Dn/1024/1024} MB`),{status:413}),"tooLarge");async function Tl(e,t,{name:n,type:a,length:i}){if(A(e),i>Dn)throw Ca();const o=String(n||"").trim().slice(0,200)||"file",s=h.basename(o).replace(/[^\w.-]+/g,"-").replace(/^[.-]+/,"").slice(-80)||"file",l=String(a||"").split(";")[0].trim().toLowerCase(),c=Ta.test(l)?l:"application/octet-stream";await j.mkdir(h.dirname(Ve(e,"x")),{recursive:!0});let u,w;for(let m=Date.now();!w;m++)u=`${m}-${s}`,w=await j.open(h.join(Te(e),u),"wx").catch($=>$.code==="EEXIST"?null:Promise.reject($));const p=h.join(Te(e),u);let f=0;try{for await(const m of t){if(f+=m.length,f>Dn)throw Ca();await w.write(m)}}catch(m){throw await w.close(),await j.rm(p,{force:!0}),m}return await w.close(),await k(Ve(e,u),{name:o,type:c,size:f,at:new Date().toISOString()}),{name:o,url:`uploads/${u}`,path:p,type:c,size:f}}r(Tl,"saveUpload");async function Cl(e,t){if(!Ba.test(t||""))return null;const n=h.join(Te(e),t),a=await j.stat(n).catch(()=>null);if(!a?.isFile())return null;const i=await g(Ve(e,t),{});return{path:n,name:i.name||t,type:Ta.test(i.type||"")?i.type:"application/octet-stream",size:a.size}}r(Cl,"readUpload");const qa=["github","slack"],Ma=r(async(e,t)=>String(t).startsWith("ask:")?it(e,t.slice(4),"the ask"):x(e,t),"checkThreadKey");async function W(e,t,n,a={},{link:i}={}){A(e),await Ma(e,t);const o={ts:new Date().toISOString(),who:"claude",kind:Zo,line:n,text:Yo(n,a),...i?{link:i}:{}};return await me(Y(e,t),o),{line:o,lines:Vo(o).fault?await An(e,t,o.text):[]}}r(W,"appendStatus");async function ht(e,t,{text:n,link:a,log:i,who:o="claude",via:s,by:l}){A(e),await Ma(e,t),/^[^\n]{1,40}$/.test(o.trim())||d("--who takes claude, love or a reviewer's name"),s!==void 0&&!qa.includes(s)&&d(`--via takes ${qa.join(" or ")}`);const c=s===void 0?Fn(n):S(n,"--text"),u={ts:new Date().toISOString(),who:o.trim(),text:c,link:X(a,p=>On(p,"--link")),log:i,via:s,by:l},w=Y(e,t);return await me(w,u),{file:w,lines:u.who==="claude"&&!s?await An(e,t,u.text):[]}}r(ht,"appendMessage");function Fn(e){const t=S(e,"--text");return t.length>_a&&d(`--text runs to ${t.length} characters, past a message's ${_a}: 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(Fn,"messageText");async function Ce(e,t,{command:n,text:a=Nt[n],who:i="claude",by:o,card:s}){const l={ts:new Date().toISOString(),who:i,command:n,text:a,...o?{by:o}:{},...s?{card:s}:{}};return await me(Y(e,t),l),{lines:[]}}r(Ce,"appendCommand");const ke=r(e=>h.join(e,"data","ask-links.json"),"askLinksFile");async function je(e){return(await g(ke(e),{})).links||{}}r(je,"readAskLinks");async function Q(e,t){const{tickets:n}=be(await ae(e)),a=await M(e),i=/^ask:(.+)$/.exec(t)?.[1];if(i===void 0){const s=/^([^/\s]+)\/([^/\s]+)$/.exec(t);if(!s)return t;const l=a.find(w=>w.slug===s[1]),c=l&&await g(l.file,null);if(!c)return s[2];const u=(c.items||[]).find(w=>w.id===s[2]);return Je(c,u||{id:s[2]},n)}const o=e?V(await je(e),i):void 0;if(o)return Q(e,o);for(const s of a){const l=await g(s.file,null),c=(Array.isArray(l?.items)?l.items:[]).find(u=>u.from_ask===i);if(c)return Je(l,c,n)}return`task ${i}`}r(Q,"cardName");async function ql(e,t){const n=(await K(h.join(e,"data","events.jsonl"))).find(a=>a.id===t);return n?.kind==="ask"?Q(e,`ask:${t}`):typeof n?.data?.key=="string"?Q(e,n.data.key):`the ${n?.kind} event`}r(ql,"eventCard");async function Ml(e,t,n,a,i){A(e),ve(t,"the event id"),(await K(h.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 Mt(e,t),await x(e,n),S(a,"--summary"),i!==void 0&&S(i,"--title");const s=await je(e);return await k(ke(e),{links:{...s,[t]:n}}),await ht(e,n,{text:a}),i!==void 0&&await Ua(e,t,i),ke(e)}r(Ml,"relateAsk");const Wa=r(e=>h.join(e,"data","ask-titles.json"),"askTitlesFile");async function Ls(e){return(await g(Wa(e),{})).titles||{}}r(Ls,"readAskTitles");async function Ua(e,t,n){await k(Wa(e),{titles:{...await Ls(e),[t]:S(n,"--title")}})}r(Ua,"titleAsk");async function Wl(e,t){await it(e,t,"the ask");const n=await je(e);return V(n,t)===void 0&&d(`ask "${t}" is not folded into a card`),delete n[t],await k(ke(e),{links:n}),ke(e)}r(Wl,"unfoldAsk");const Ct=r(e=>h.join(e,"data","ask-progress.json"),"askProgressFile");async function qt(e){return(await g(Ct(e),{})).asks||{}}r(qt,"readAskProgress");async function Mt(e,t){const n=V(await qt(e),t)?.moved;n&&d(`ask "${t}" was moved to /${n.route}; it lives there now`)}r(Mt,"refuseMoved");async function Ul(e,t,n,a){await it(e,t,"the ask"),await Mt(e,t);const i=await qt(e);return await k(Ct(e),{asks:{...i,[t]:{state:n,at:new Date().toISOString(),...a===void 0?{}:{next:S(a,"--next")}}}}),Ct(e)}r(Ul,"markAsk");async function Bs(e,t,n){const a=`ask "${t}"`;if(n.some(w=>w.kind==="ask-withdrawn"&&w.data?.id===t&&!w.data?.key))return{why:`${a} was deleted by the owner`};const i=V(await qt(e),t);if(i?.moved)return{why:`${a} was already moved to /${i.moved.route}`};const o=V(await je(e),t);if(o)return{why:`${a} is folded into ${o}; run ask unfold ${t} first`};const s=[];for(const w of await M(e)){const p=await g(w.file,null);for(const f of Array.isArray(p?.items)?p.items:[])f.from_ask===t&&s.push({key:`${w.slug}/${f.id}`,it:f})}const l=s.find(({it:w})=>Le(w)||Ae(w));return l?{why:`${a} became ${l.key}, which has started`}:s.length?{items:s.map(w=>w.key)}:(await Promise.all(["html","json"].map(w=>j.stat(h.join(e,"data","replies",`${t}.${w}`)).then(()=>!0,()=>!1)))).some(Boolean)||!!i||n.some(w=>w.kind==="ask-done"&&w.data?.id===t)||(await K(Y(e,`ask:${t}`))).some(w=>(w.who||"claude")==="claude"&&w.kind!=="status")?{why:`${a} is not New: Claude already answered it`}:null}r(Bs,"askNotNew");async function Ts(e,t,n){const a=/^uploads\/([\w.-]+)$/.exec(n?.url||"")?.[1],i=Ba.test(a||"")?h.join(Te(e),a):null;if(!i||!await j.stat(i).then(c=>c.isFile(),()=>!1))return{name:String(n?.name||a||"file")};await j.mkdir(h.dirname(Ve(t,"x")),{recursive:!0});const o=a.replace(/^\d+-/,"");let s;for(let c=Date.now();s=`${c}-${o}`,!await j.copyFile(i,h.join(Te(t),s),wo.COPYFILE_EXCL).then(()=>!0,w=>w.code==="EEXIST"?!1:Promise.reject(w));c++);const l=await g(Ve(e,a),null);return l&&await k(Ve(t,s),l),{...n,url:`uploads/${s}`,path:h.join(Te(t),s)}}r(Ts,"copyUpload");async function Hl(e,t,n){A(e),ve(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(h.join(e,"data","events.jsonl")),i=a.find(P=>P.id===t&&P.kind==="ask")||d(`no ask "${t}" in this site's data/events.jsonl`),o=we(),s=o.find(P=>P.route===n)||d(`--to ${n}: no board at /${n} in the hub's manifest; the boards are ${o.map(P=>P.route).join(", ")||"none"}`);_e(s.dir,e)&&d(`--to ${n}: that is this board`);const l=await Bs(e,t,a);l?.why&&d(l.why);const c=a.filter(P=>P.kind==="ask-edited"&&P.data?.id===t&&typeof P.data?.text=="string").sort((P,B)=>String(P.ts).localeCompare(String(B.ts))),u=c.at(-1),w=[];for(const P of[...i.data?.files||[],...c.flatMap(B=>B.data.files||[])])w.push(await Ts(e,s.dir,P));const p={route:h.basename(e),title:(await ae(e)).title,id:t},f=u?u.data.text:i.data?.text||"",m=await ge(s,"ask",{text:f,...w.length?{files:w}:{},from:p}),$={route:n,title:s.title,id:m},y=new Date().toISOString(),_=await qt(e);await k(Ct(e),{asks:{..._,[t]:{state:"done",at:y,moved:$}}});const D=`/${n}/#selectedCard=${encodeURIComponent(`ask:${m}`)}`;await W(e,`ask:${t}`,"ask-moved",{board:s.title},{link:D});for(const P of l?.items||[])await he(e,P,{done:!0,force:!0,closing:["ask-moved-closed",{board:s.title}]});return{file:h.join(s.dir,"data","events.jsonl"),lines:[`moved to ${D}`,...(l?.items||[]).map(P=>`closed: ${P}`)]}}r(Hl,"moveAsk");async function zl(e,t,{state:n,text:a}={}){return A(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"),za(e,await Ha(e,t,"review record"),{state:n,text:a})}r(zl,"recordReview");async function Ha(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(h.join(e,"data","incoming.json"),{});return{...(a?.prs||[]).find(s=>s.number===Number(t))||(await g(h.join(e,"data","reviews.json"),{}))[t]||d(`PR #${t} is not an incoming PR on this board`),repo:a?.repo||oe(e,{})}}r(Ha,"incomingPR");async function za(e,{number:t,title:n,author:a,url:i,touches:o=[],asked:s=!1,head:l},{state:c,text:u,review_url:w}){const p=h.join(e,"data","reviews.json"),f=await g(p,{});return f[t]={number:Number(t),title:n,author:a,url:i,touches:o,asked:s,head:l,state:c,text:u,...w?{review_url:w}:{},at:new Date().toISOString()},await k(p,f),{file:p,lines:[]}}r(za,"saveReview");async function Jl(e,t,{words:n,readAt:a}){A(e);const i=await Ha(e,t,"review post"),o=String(n??"").trim()||d("review post needs the review text, as --file"),s=`${wt}Claude review from the ${(await ae(e)).title} board`;(o.startsWith(wt.trim())||o.includes("Claude review from")||o.includes(_n))&&d(`review post adds the ${wt.trim()} header and the claude-bridge marker itself; pass only your words`);const l=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 c=be(await ae(e)).github_login;try{const u=await ns({repo:l,pr:i.number,since:a});u.length&&d(`PR #${t} got ${u.length===1?"a review":`${u.length} reviews`} after ${a} (${u.map(p=>`${p.login} at ${p.at}`).join(", ")}); reread the PR and review again`);const w=await ts({repo:l,pr:i.number,head:i.head,body:`${s}
|
|
1
|
+
var yo=Object.defineProperty;var r=(e,t)=>yo(e,"name",{value:t,configurable:!0});import O from"node:fs/promises";import{constants as xo,existsSync as ce,readFileSync as _o}from"node:fs";import h from"node:path";import bn from"node:os";import{execFile as xa}from"node:child_process";import{fileURLToPath as So}from"node:url";import{BUILD_REFUSED as _a,BUILDER_ASK as Bt,COMMANDS as Tt,LANDER as Sa,LANES as Qe,MOVE as va,PAUSE_CLEARS as ja,PHASES as yn,handedBackAt as xn,MAX_IMPLEMENTERS as vo,MAX_PLANNERS as jo,builds as Ge,waveItems as Ne,mergeBlockers as Oo,modelName as Ct,noTakePicked as Po,ownerQuestion as Ee,plans as Le,QUESTION_REOPEN as Ao,PLANNER_WORD as Do,providerOf as bt,owesNext as _n,pushTag as Fo,refOf as Ve,roundOf as Sn,roundSettledBy as Ro,HELPER as re,VISUAL as pe,visualAgent as No}from"./domain.mjs";import{GITHUB_STATE as vn,appendEvent as ge,listBoards as we,readManifest as Eo,samePath as ve,withManifestLock as Lo,writeManifest as Io}from"../hub/boards.mjs";import{readUsageGate as Oa,pauseReason as Bo,pauseReasonFor as To,heldFor as Co,holdsWork as qo,builderPick as jn,pauseEnd as Mo}from"../shared/usage-gate.mjs";import{reviseQueued as Wo,pushKindError as Uo,readClaudeModel as On}from"../hub/push.mjs";import{AT_RE as Ho}from"../hub/duration.mjs";import{routeRecord as ae}from"../hub/routes.mjs";import{ticketSource as Pa,resolveTicket as Jo,ticketTarget as zo,moveTicket as Ko,cardIdOf as Qo}from"./tickets.mjs";import{LINEAR_NEEDS_KEY as Go,linearStatus as Vo,linearWorks as Zo}from"../hub/linear.mjs";import{validateBranch as Yo}from"../hub/repos.mjs";import{withFileLock as Ze}from"../hub/locks.mjs";import{audit as Xo,auditLines as es,boardAudit as Aa,readAudit as ts}from"../shared/audit.mjs";import{merge3 as ns}from"./merge3.mjs";import{HUB as as,MESSAGE_LIMIT as Da,BRIEF_LINE_LIMIT as Fa,hubLineOf as is,hubText as os}from"./hub-lines.mjs";import{fetchPR as Ra,reportOf as Na,githubLogin as ss,postToPR as rs,postReview as cs,reviewsSince as ls,CLAUDE_PREFIX as yt,CLAUDE_MARK as Pn}from"../hub/pr-report.mjs";import{allLanded as ds,cardPRs as Ie,hasPR as Be,parsePRRef as An,prKeyOf as xt,prNames as Ye,prURL as Dn,storedPRs as je}from"../shared/card-prs.mjs";import{reviewLinks as us,topicItems as fs,reviewersOf as Fn}from"./review.mjs";import{readiness as Ea}from"../hub/readiness.mjs";import{slackDay as ps}from"../hub/slack.mjs";import"../bin/install.mjs";import{readPermissions as ws}from"./permissions.mjs";import{validBuildTarget as La}from"./build-script.mjs";import{parseCheck as ms,checkText as hs}from"../shared/owner-check.mjs";import{RuleError as gs,rulesOf as Ia,addRule as $s,editRule as ks,removeRule as bs,applyRules as ys,readHubRules as qt,writeHubRules as xs}from"../hub/rules.mjs";import{shareEntries as _s,validLogin as Ss}from"../hub/share.mjs";class le extends Error{static{r(this,"BoardError")}}const d=r(e=>{throw new le(e)},"fail"),vs=r(()=>h.join(bn.homedir(),".claude","kickflip","sites"),"sitesDir"),Mt=r(()=>h.join(bn.homedir(),".claude","waves"),"wavesDir"),Ba=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,[Ba]:n}:a}catch{return t}},"readJSON"),Q=r(async e=>{try{return(await O.readFile(e,"utf8")).split(`
|
|
2
|
+
`).filter(Boolean).map(t=>JSON.parse(t))}catch{return[]}},"readLines");async function Te(e,t){await O.mkdir(h.dirname(e),{recursive:!0});const n=`${e}.${process.pid}.tmp`;await O.writeFile(n,t),await O.rename(n,e)}r(Te,"replace");const b=r((e,t)=>{const{[Ba]:n,...a}=t??{};return n===void 0?Te(e,JSON.stringify(t,null,2)+`
|
|
3
|
+
`):Ze(e,async()=>{const i=JSON.parse(n),o=JSON.parse(await O.readFile(e,"utf8").catch(()=>n));await Te(e,JSON.stringify(ns(i,a,o),null,2)+`
|
|
4
|
+
`)})},"writeJSON");async function me(e,t){await O.mkdir(h.dirname(e),{recursive:!0}),await O.appendFile(e,JSON.stringify(t)+`
|
|
5
|
+
`)}r(me,"appendLine");async function Tc(e){/^[\w-]+$/.test(e||"")||d(`--site takes a route such as ios-orchestrator, not "${e}"`);const t=h.join(vs(),e);return await O.stat(t).then(n=>n.isDirectory(),()=>!1)||d(`no site at ${t}`),t}r(Tc,"siteDir");const ie=r(async e=>({title:"Board",items:"waves",...e?await g(h.join(e,"board.json"),{}):{}}),"boardConfig");async function M(e){const t=await ie(e);if(t.items==="waves")try{return(await O.readdir(Mt(),{withFileTypes:!0})).filter(a=>a.isDirectory()).map(a=>({slug:a.name,dir:h.join(Mt(),a.name),file:h.join(Mt(),a.name,"wave.json")}))}catch{return[]}const n=h.resolve(e,t.items);return[{slug:t.slug||h.basename(t.items,".json"),dir:h.dirname(n),file:n}]}r(M,"itemSources");const X=r((e,t)=>h.join(e,"data","threads",t.replace(/[^\w.-]+/g,"__")+".jsonl"),"threadFile"),L=r(e=>h.join(e,"data","owner.json"),"ownerFile"),$e=r(e=>h.join(e,"data","staged-questions.json"),"stagedFile"),Xe=r(e=>h.join(e,"data","screening.json"),"screeningFile"),js=900*1e3,A=r(e=>e||d("this command needs --site <route>"),"needSite"),_=r((e,t)=>typeof e=="string"&&e.trim()?e.trim():d(`${t} must be non-empty text`),"text"),ee=r((e,t)=>e===void 0?void 0:t(e),"optional"),Os=/^prototype-[\w-]+\.html(?:[?#]\S*)?$/,Ps=/^visuals\/[\w-]+\/[\w-]+\.html$/,Rn=r((e,t)=>typeof e=="string"&&(Os.test(e)||Ps.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"),Wt=r((e,t)=>typeof e=="boolean"?e:d(`${t} must be true or false`),"bool"),Oe=r((e,t)=>typeof e=="string"&&/^[\w.-]+$/.test(e)?e:d(`${t} must use only letters, digits, "_", "." and "-"`),"domId"),et=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 P(e){const t=/^([^/\s]+)\/([^/\s]+)$/.exec(e||"");return t?{file:t[1],id:t[2]}:d(`expected <file>/<id>, not "${e}"`)}r(P,"parseKey");async function Ce(e,t){const n=(await M(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(Ce,"loadItemFile");async function x(e,t){const{file:n,id:a}=P(t);let i=await Ce(e,n);!i.itemFile.items.some(s=>s.id===a)&&await Cr(e,t)&&(i=await Ce(e,n));const o=i.itemFile.items.find(s=>s.id===a)||d(`item file "${n}" has no item "${a}"`);return{...i,item:o}}r(x,"loadItem");const Ut=["md","html","json"];async function Nn(e,t){const n=new RegExp(`^${t.replace(/[.]/g,"\\.")}(?:~(\\d+))?\\.(${Ut.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||Ut.indexOf(a.ext)-Ut.indexOf(i.ext)).filter((a,i,o)=>a.n!==o[i-1]?.n)}r(Nn,"replyFilesOf");async function Cc(e,t,n,a){A(e),Oe(t,"the event id");const i=(await Q(h.join(e,"data","events.jsonl"))).find(m=>m.id===t);i||d(`no event "${t}" in this site's data/events.jsonl`);const o=i.kind==="ask"?`ask:${t}`:i.kind==="item-message"&&typeof i.data?.key=="string"?i.data.key:null,s=a===void 0?void 0:/^ask:(.+)$/.exec(o||"")?.[1]||d(`reply --title names an ask: "${t}" is not an ask or a message in an ask's thread`);s&&_(a,"--title");const c=h.join(e,"data","replies"),l=await Nn(c,t),u=h.join(c,l.length?`${t}~${l.at(-1).n+1}.md`:`${t}.md`);await Te(u,_(n,"the reply")),s&&await Va(e,s,a);const w=o?await Ln(e,o,n):[],p=i.kind==="wave-triage"?["wave","file"].filter(m=>typeof i.data?.[m]=="string").map(m=>`${m}:${i.data[m]}`):[],f=i.kind==="ask"?`item:ask:${t}`:p[0];return f&&w.push(...await _i(e,f,"Acted on: replied")),{file:u,lines:w}}r(Cc,"writeReply");const Ta=r(e=>h.join(e,"data","working.json"),"workingFile");async function As(e){return(await g(Ta(e),{})).events||{}}r(As,"readWorking");const qc=["ask","item-message","wave-triage","overnight-triage"];async function Mc(e,t){if(!t.length)return;const n=new Date().toISOString(),a=await As(e);await b(Ta(e),{events:{...a,...Object.fromEntries(t.map(i=>[i,ae(a,i)||n]))}})}r(Mc,"markWorking");const En=r(e=>h.join(e,"data","push-state.json"),"pushStateFile");async function Ds(e,t){A(e);const n=Date.parse((await g(En(e),{})).last_push);if(Number.isNaN(n))return 0;const a=h.join(e,"data","replies"),i=(await O.readdir(a).catch(()=>[])).filter(s=>Ut.includes(h.extname(s).slice(1)));return(await Promise.all(i.map(s=>O.stat(h.join(a,s)).then(c=>c.mtimeMs,()=>0)))).filter(s=>s>n&&s<=t.getTime()).length}r(Ds,"repliesSincePush");async function Fs(e,t){return await b(En(A(e)),{last_push:t.toISOString()}),En(e)}r(Fs,"recordPush");const Rs=h.join(h.dirname(So(import.meta.url)),"..","bin","kickflip.mjs");function Ns(){if(!process.env.KICKFLIP_PUSH_CMD)return[process.execPath,Rs,"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 le("KICKFLIP_PUSH_CMD must be a JSON array of strings")}r(Ns,"pushCommand");async function Wc(e,t){const n=Uo(t.kind);n&&d(n),t.title===void 0&&d("push needs --title");const a=new Date,i=Ls(t.body,await Ds(e,a)),o={kind:t.kind,title:t.title,body:i||void 0,url:Bs(t.url),tag:t.tag},[s,...c]=Ns(),l=[...c,...Object.entries(o).flatMap(([p,f])=>f===void 0?[]:[`--${p}`,f]),...t.urgent?["--urgent"]:[],...t.now?["--now"]:[]],u=await new Promise(p=>{xa(s,l,{encoding:"utf8"},(f,m,$)=>p({error:f,said:`${m||""}${$||""}`.split(`
|
|
6
|
+
`).filter(y=>y.trim())}))}),w=u.error?typeof u.error.code=="number"?u.error.code:1:0;if(w!==0)throw Object.assign(new le([...u.said,`kickflip push failed (${typeof u.error.code=="number"?`exit ${w}`:u.error.message}); the push was not recorded`].join(`
|
|
7
|
+
`)),{exitCode:w});return await Fs(e,a),{lines:u.said}}r(Wc,"sendPush");const Es=/(?:^| · )(and \d+ repl(?:y|ies))$/,Ls=r((e,t)=>[e,t&&`and ${t} ${t===1?"reply":"replies"}`].filter(Boolean).join(" \xB7 "),"withReplies"),Ca=160;function Is(e){const t=String(e).split(`
|
|
8
|
+
`).map(n=>n.replace(/\s+/g," ").trim()).find(Boolean)||"";return t.length>Ca?`${t.slice(0,Ca-1).trimEnd()}\u2026`:t}r(Is,"pushLine");function Bs(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(Bs,"pushUrl");const Ts=r((e,t)=>`/${h.basename(e)}/#selectedCard=${encodeURIComponent(t)}&reply`,"threadUrl");async function Ln(e,t,n){const a=Is(n);if(!a)return[];const i=Fo(t),o=await Wo(i,s=>[a,Es.exec(s)?.[1]].filter(Boolean).join(" \xB7 "),Ts(e,t));return o===null?[]:[`queued push ${i} now reads: ${o}`]}r(Ln,"freshenPush");const qa=r(e=>h.join(e,"data","hidden.json"),"hiddenFile"),Cs=r(async e=>new Set((await g(qa(e),{events:[]})).events||[]),"readHidden");let Ma=Promise.resolve();function Uc(e,t,n){Oe(t,"the event id");const a=Ma.then(async()=>{const i=await Cs(e);n?i.add(t):i.delete(t),await b(qa(e),{events:[...i]})});return Ma=a.catch(()=>{}),a}r(Uc,"setHidden");const Wa=r(e=>h.join(e,"data","read.json"),"readMarksFile"),qs=r(async e=>(await g(Wa(e),{})).cards||{},"readMarks");let Ua=Promise.resolve();function Hc(e,t,n={}){const a=Ua.then(async()=>{const i=await qs(e),o=new Date().toISOString(),s=r((l,u)=>String(u||"")>l?u:l,"later"),c=Object.fromEntries([...new Set(t)].sort().map(l=>[l,s(i[l]||o,n[l])]));JSON.stringify(c)!==JSON.stringify(i)&&await b(Wa(e),{cards:c})});return Ua=a.catch(()=>{}),a}r(Hc,"markRead");const In=20*1024*1024,qe=r(e=>h.join(e,"data","uploads"),"uploadsDir"),tt=r((e,t)=>h.join(qe(e),".meta",`${t}.json`),"uploadMeta"),Ha=/^\d+-[\w.-]+$/,Ja=/^[\w.+-]+\/[\w.+-]+$/,za=r(()=>Object.assign(new le(`a file can be at most ${In/1024/1024} MB`),{status:413}),"tooLarge");async function Jc(e,t,{name:n,type:a,length:i}){if(A(e),i>In)throw za();const o=String(n||"").trim().slice(0,200)||"file",s=h.basename(o).replace(/[^\w.-]+/g,"-").replace(/^[.-]+/,"").slice(-80)||"file",c=String(a||"").split(";")[0].trim().toLowerCase(),l=Ja.test(c)?c:"application/octet-stream";await O.mkdir(h.dirname(tt(e,"x")),{recursive:!0});let u,w;for(let m=Date.now();!w;m++)u=`${m}-${s}`,w=await O.open(h.join(qe(e),u),"wx").catch($=>$.code==="EEXIST"?null:Promise.reject($));const p=h.join(qe(e),u);let f=0;try{for await(const m of t){if(f+=m.length,f>In)throw za();await w.write(m)}}catch(m){throw await w.close(),await O.rm(p,{force:!0}),m}return await w.close(),await b(tt(e,u),{name:o,type:l,size:f,at:new Date().toISOString()}),{name:o,url:`uploads/${u}`,path:p,type:l,size:f}}r(Jc,"saveUpload");async function zc(e,t){if(!Ha.test(t||""))return null;const n=h.join(qe(e),t),a=await O.stat(n).catch(()=>null);if(!a?.isFile())return null;const i=await g(tt(e,t),{});return{path:n,name:i.name||t,type:Ja.test(i.type||"")?i.type:"application/octet-stream",size:a.size}}r(zc,"readUpload");const Ka=["github","slack"],Qa=r(async(e,t)=>String(t).startsWith("ask:")?ct(e,t.slice(4),"the ask"):x(e,t),"checkThreadKey");async function U(e,t,n,a={},{link:i}={}){A(e),await Qa(e,t);const o={ts:new Date().toISOString(),who:"claude",kind:as,line:n,text:os(n,a),...i?{link:i}:{}};return await me(X(e,t),o),{line:o,lines:is(o).fault?await Ln(e,t,o.text):[]}}r(U,"appendStatus");async function _t(e,t,{text:n,link:a,log:i,who:o="claude",via:s,by:c}){A(e),await Qa(e,t),/^[^\n]{1,40}$/.test(o.trim())||d("--who takes claude, love or a reviewer's name"),s!==void 0&&!Ka.includes(s)&&d(`--via takes ${Ka.join(" or ")}`);const l=s===void 0?Bn(n):_(n,"--text"),u={ts:new Date().toISOString(),who:o.trim(),text:l,link:ee(a,p=>Rn(p,"--link")),log:i,via:s,by:c},w=X(e,t);return await me(w,u),{file:w,lines:u.who==="claude"&&!s?await Ln(e,t,u.text):[]}}r(_t,"appendMessage");function Bn(e){const t=_(e,"--text");return t.length>Da&&d(`--text runs to ${t.length} characters, past a message's ${Da}: 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(Bn,"messageText");async function Me(e,t,{command:n,text:a=Tt[n],who:i="claude",by:o,card:s}){const c={ts:new Date().toISOString(),who:i,command:n,text:a,...o?{by:o}:{},...s?{card:s}:{}};return await me(X(e,t),c),{lines:[]}}r(Me,"appendCommand");const ke=r(e=>h.join(e,"data","ask-links.json"),"askLinksFile");async function Pe(e){return(await g(ke(e),{})).links||{}}r(Pe,"readAskLinks");async function J(e,t){const{tickets:n}=be(await ie(e)),a=await M(e),i=/^ask:(.+)$/.exec(t)?.[1];if(i===void 0){const s=/^([^/\s]+)\/([^/\s]+)$/.exec(t);if(!s)return t;const c=a.find(w=>w.slug===s[1]),l=c&&await g(c.file,null);if(!l)return s[2];const u=(l.items||[]).find(w=>w.id===s[2]);return Ve(l,u||{id:s[2]},n)}const o=e?ae(await Pe(e),i):void 0;if(o)return J(e,o);for(const s of a){const c=await g(s.file,null),l=(Array.isArray(c?.items)?c.items:[]).find(u=>u.from_ask===i);if(l)return Ve(c,l,n)}return`task ${i}`}r(J,"cardName");async function Kc(e,t){const n=(await Q(h.join(e,"data","events.jsonl"))).find(a=>a.id===t);return n?.kind==="ask"?J(e,`ask:${t}`):typeof n?.data?.key=="string"?J(e,n.data.key):`the ${n?.kind} event`}r(Kc,"eventCard");async function Qc(e,t,n,a,i){A(e),Oe(t,"the event id"),(await Q(h.join(e,"data","events.jsonl"))).some(c=>c.id===t&&c.kind==="ask")||d(`no ask "${t}" in this site's data/events.jsonl`),await zt(e,t),await x(e,n),_(a,"--summary"),i!==void 0&&_(i,"--title");const s=await Pe(e);return await b(ke(e),{links:{...s,[t]:n}}),await _t(e,n,{text:a}),i!==void 0&&await Va(e,t,i),ke(e)}r(Qc,"relateAsk");const Ga=r(e=>h.join(e,"data","ask-titles.json"),"askTitlesFile");async function Ms(e){return(await g(Ga(e),{})).titles||{}}r(Ms,"readAskTitles");async function Va(e,t,n){await b(Ga(e),{titles:{...await Ms(e),[t]:_(n,"--title")}})}r(Va,"titleAsk");async function Gc(e,t){await ct(e,t,"the ask");const n=await Pe(e);return ae(n,t)===void 0&&d(`ask "${t}" is not folded into a card`),delete n[t],await b(ke(e),{links:n}),ke(e)}r(Gc,"unfoldAsk");const Ht=r(e=>h.join(e,"data","ask-progress.json"),"askProgressFile");async function Jt(e){return(await g(Ht(e),{})).asks||{}}r(Jt,"readAskProgress");async function zt(e,t){const n=ae(await Jt(e),t)?.moved;n&&d(`ask "${t}" was moved to /${n.route}; it lives there now`)}r(zt,"refuseMoved");async function Vc(e,t,n,a){await ct(e,t,"the ask"),await zt(e,t);const i=await Jt(e);return await b(Ht(e),{asks:{...i,[t]:{state:n,at:new Date().toISOString(),...a===void 0?{}:{next:_(a,"--next")}}}}),Ht(e)}r(Vc,"markAsk");async function Ws(e,t,n){const a=`ask "${t}"`;if(n.some(u=>u.kind==="ask-withdrawn"&&u.data?.id===t&&!u.data?.key))return{why:`${a} was deleted by the owner`};const i=ae(await Jt(e),t);if(i?.moved)return{why:`${a} was already moved to /${i.moved.route}`};const o=ae(await Pe(e),t);if(o)return{why:`${a} is folded into ${o}; run ask unfold ${t} first`};const s=[];for(const u of await M(e)){const w=await g(u.file,null);for(const p of Array.isArray(w?.items)?w.items:[])p.from_ask===t&&s.push({key:`${u.slug}/${p.id}`,it:p})}const c=s.find(({it:u})=>Be(u)||De(u));return c?{why:`${a} became ${c.key}, which has started`}:s.length?{items:s.map(u=>u.key)}:(await Nn(h.join(e,"data","replies"),t)).length>0||!!i||n.some(u=>u.kind==="ask-done"&&u.data?.id===t)||(await Q(X(e,`ask:${t}`))).some(u=>(u.who||"claude")==="claude"&&u.kind!=="status")?{why:`${a} is not New: Claude already answered it`}:null}r(Ws,"askNotNew");async function Us(e,t,n){const a=/^uploads\/([\w.-]+)$/.exec(n?.url||"")?.[1],i=Ha.test(a||"")?h.join(qe(e),a):null;if(!i||!await O.stat(i).then(l=>l.isFile(),()=>!1))return{name:String(n?.name||a||"file")};await O.mkdir(h.dirname(tt(t,"x")),{recursive:!0});const o=a.replace(/^\d+-/,"");let s;for(let l=Date.now();s=`${l}-${o}`,!await O.copyFile(i,h.join(qe(t),s),xo.COPYFILE_EXCL).then(()=>!0,w=>w.code==="EEXIST"?!1:Promise.reject(w));l++);const c=await g(tt(e,a),null);return c&&await b(tt(t,s),c),{...n,url:`uploads/${s}`,path:h.join(qe(t),s)}}r(Us,"copyUpload");async function Zc(e,t,n){A(e),Oe(t,"the event id"),(typeof n!="string"||!n.trim())&&d("ask move needs --to <route>, the board to move it to");const a=await Q(h.join(e,"data","events.jsonl")),i=a.find(j=>j.id===t&&j.kind==="ask")||d(`no ask "${t}" in this site's data/events.jsonl`),o=we(),s=o.find(j=>j.route===n)||d(`--to ${n}: no board at /${n} in the hub's manifest; the boards are ${o.map(j=>j.route).join(", ")||"none"}`);ve(s.dir,e)&&d(`--to ${n}: that is this board`);const c=await Ws(e,t,a);c?.why&&d(c.why);const l=a.filter(j=>j.kind==="ask-edited"&&j.data?.id===t&&typeof j.data?.text=="string").sort((j,F)=>String(j.ts).localeCompare(String(F.ts))),u=l.at(-1),w=[];for(const j of[...i.data?.files||[],...l.flatMap(F=>F.data.files||[])])w.push(await Us(e,s.dir,j));const p={route:h.basename(e),title:(await ie(e)).title,id:t},f=u?u.data.text:i.data?.text||"",m=await ge(s,"ask",{text:f,...w.length?{files:w}:{},from:p}),$={route:n,title:s.title,id:m},y=new Date().toISOString(),S=await Jt(e);await b(Ht(e),{asks:{...S,[t]:{state:"done",at:y,moved:$}}});const E=`/${n}/#selectedCard=${encodeURIComponent(`ask:${m}`)}`;await U(e,`ask:${t}`,"ask-moved",{board:s.title},{link:E});for(const j of c?.items||[])await ue(e,j,{done:!0,force:!0,closing:["ask-moved-closed",{board:s.title}]});return{file:h.join(s.dir,"data","events.jsonl"),lines:[`moved to ${E}`,...(c?.items||[]).map(j=>`closed: ${j}`)]}}r(Zc,"moveAsk");async function Yc(e,t,{state:n,text:a}={}){return A(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"),Ya(e,await Za(e,t,"review record"),{state:n,text:a})}r(Yc,"recordReview");async function Za(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(h.join(e,"data","incoming.json"),{});return{...(a?.prs||[]).find(s=>s.number===Number(t))||(await g(h.join(e,"data","reviews.json"),{}))[t]||d(`PR #${t} is not an incoming PR on this board`),repo:a?.repo||oe(e,{})}}r(Za,"incomingPR");async function Ya(e,{number:t,title:n,author:a,url:i,touches:o=[],asked:s=!1,head:c},{state:l,text:u,review_url:w}){const p=h.join(e,"data","reviews.json"),f=await g(p,{});return f[t]={number:Number(t),title:n,author:a,url:i,touches:o,asked:s,head:c,state:l,text:u,...w?{review_url:w}:{},at:new Date().toISOString()},await b(p,f),{file:p,lines:[]}}r(Ya,"saveReview");async function Xc(e,t,{words:n,readAt:a}){A(e);const i=await Za(e,t,"review post"),o=String(n??"").trim()||d("review post needs the review text, as --file"),s=`${yt}Claude review from the ${(await ie(e)).title} board`;(o.startsWith(yt.trim())||o.includes("Claude review from")||o.includes(Pn))&&d(`review post adds the ${yt.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=be(await ie(e)).github_login;try{const u=await ls({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(p=>`${p.login} at ${p.at}`).join(", ")}); reread the PR and review again`);const w=await cs({repo:c,pr:i.number,head:i.head,body:`${s}
|
|
11
9
|
|
|
12
10
|
${o}
|
|
13
11
|
|
|
14
|
-
${
|
|
15
|
-
`),a})}r(
|
|
16
|
-
`)}r(
|
|
17
|
-
`)}r(qs,"linkText");const Qa=r(e=>!!e?.approved||e?.reviewDecision==="APPROVED","approved");async function ec(e,t){A(e);const n=await Oa.waiting("slack");if(n)return{waiting:n,links:[],replies:[],reactions:[],digest:null};const a=await ae(e),i=await g($n,{}),o=await Ut(e),s=[];for(const w of await M(e)){const p=await g(w.file,{}),f=jn(p,a.reviewers);for(const m of p.items||[])if(m.lane!=="done")for(const $ of Ie(m,oe(e,p))){const y=mt($),_=V(i.prs,y);if(!(_?.state!=="OPEN"||_.isDraft))for(const D of f)$t(o[y]?.links?.[D.name])||s.some(P=>P.pr===y&&P.reviewer===D.name)||s.push({pr:y,key:`${w.slug}/${m.id}`,reviewer:D.name,slack_user:D.slack_user,head:_.head||null,text:qs(_,$,m)})}}const l=[],c=[];for(const[w,p]of Object.entries(o)){const f=V(i.prs,w),m=[Qa(f)&&"git_approved",f?.state==="MERGED"?"git_merged":f?.state==="CLOSED"&&"x"].filter(Boolean);for(const[$,y]of Object.entries(p.links))if(y.ts)for(const _ of m)y.reactions.includes(_)||c.push({pr:w,reviewer:$,channel:y.channel,ts:y.ts,emoji:_});for(const $ of p.replies)for(const[y,_]of Object.entries($.to))$t(_)||l.push({pr:w,id:$.id,reviewer:y,channel:p.links[y].channel,thread_ts:p.links[y].ts,text:$.text})}const u=await Ms(e,t);return{links:s,replies:l,reactions:c,digest:u.due?u:null}}r(ec,"reviewDue");async function tc(e,t,n){A(e);const a=S(n,"what changed"),{itemFile:i,item:o}=await x(e,t),s=[];for(const c of Ie(o,oe(e,i)))s.push(...await Ka(e,mt(c),"updated",`Updated: ${a}`));const l=[...new Set(s)];return{lines:[l.length?`Updated queued for ${l.join(", ")} on ${t}`:`no reviewer has a link to ${t}'s PRs; nothing queued`]}}r(tc,"reviewUpdated");async function nc(e,t){const{itemFile:n,item:a}=await x(e,t),i=await Ut(e);return Ie(a,oe(e,n)).some(o=>Object.values(i[mt(o)]?.links||{}).some(s=>s.ts))}r(nc,"cardLinked");const Ga=r(e=>h.join(A(e),"data","review-digests.json"),"digestFile");async function Ms(e,{now:t=new Date}={}){A(e);const n=await ae(e),a=await g($n,{}),i=await Ut(e),o=os(),s=new Map;for(const f of await M(e)){const m=await g(f.file,{}),$=jn(m,n.reviewers);for(const y of $)s.has(y.name)||s.set(y.name,{name:y.name,slack_user:y.slack_user,dm:y.dm,items:[]});for(const y of m.items||[]){if(y.lane==="done")continue;const _=`${f.slug}/${y.id}`,D=is(m,y,_,o);for(const P of Ie(y,oe(e,m))){const B=mt(P),F=V(a.prs,B);if(!(F?.state!=="OPEN"||F.isDraft||F.failing?.length||Qa(F)))for(const I of $){const v=i[B]?.links?.[I.name]?.ts||V(D,I.name)?.ts||null,E=v?(t.getTime()-Number(v)*1e3)/864e5:null,T=E>2?Math.floor(E):null;s.get(I.name).items.push({key:_,pr:B,title:y.title,url:vn(P),sent_ts:v,...T?{days_old:T}:{},text:`${vn(P)} \xB7 ${y.title}${T?` (${T} days old)`:""}`})}}}}const{date:l,open:c}=ss(t),u=((await g(Ga(e),{})).digests||[]).filter(f=>f.date===l),w=await Oa.waiting("slack");for(const f of u)s.has(f.reviewer)||s.set(f.reviewer,{name:f.reviewer,items:[]});const p=[...s.values()].map(f=>{const m=u.find(y=>y.reviewer===f.name),$=m?!m.posting_at&&(!m.ts||m.replies.some(y=>!$t(y))):f.items.length>0;return{...f,owed:$,...m?{sent:m}:{}}}).filter(f=>f.items.length||f.sent);return{date:l,due:!w&&c&&p.some(f=>f.owed),...w?{waiting:w}:{},reviewers:p}}r(Ms,"reviewDigest");async function ac(e,t,n,{channel:a,header:i,post:o}){const s=Ga(e),l=r(w=>(w.digests||[]).find(p=>p.date===t&&p.reviewer===n.name),"entry");await qe(s,w=>{l(w)||(w.digests=[...(w.digests||[]).filter(p=>p.date===t),{date:t,reviewer:n.name,channel:a,replies:n.items.map(p=>({pr:p.pr,text:p.text}))}],delete w.last_date)});const c=l(await g(s,{})),u=c.ts||await Ht(s,l,()=>o({channel:c.channel,text:i(c.replies.length)}));for(const[w,p]of c.replies.entries())$t(p)||await Ht(s,f=>l(f).replies[w],()=>o({channel:c.channel,thread_ts:u,text:p.text}))}r(ac,"postDigest");function ic(e,t){const n=Nn(Array.isArray(t)?t:[t],"the reviewer file");return En(e,a=>[...a.map(i=>n.find(o=>gt(o.name,i.name))||i),...n.filter(i=>!a.some(o=>gt(i.name,o.name)))])}r(ic,"addReviewers");function oc(e,t){return En(e,n=>{const a=t.filter(i=>!n.some(o=>gt(o.name,i)));return a.length&&d(`this board has no reviewer ${a.map(i=>`"${i}"`).join(", ")}`),n.filter(i=>!t.some(o=>gt(i.name,o)))})}r(oc,"removeReviewers");const Ws=r(()=>h.join(pn.homedir(),".claude","kickflip","settings.json"),"hubSettingsFile"),In=r(e=>{try{return e()}catch(t){throw t instanceof ds&&d(t.message),t}},"ruleCall"),Ln=r(async e=>Aa(await g(h.join(e,"board.json"),{})),"readBoardRules");let Za=Promise.resolve();function kt(e,t){const n=Za.then(async()=>{const a=h.join(A(e),"board.json"),i=await g(a,{}),{set:o,...s}=t(i);return await k(a,{...i,...o}),{file:a,...s}});return Za=n.catch(()=>{}),n}r(kt,"writeBoardJSON");const Va=r((e,t)=>kt(e,n=>{const a=In(()=>t(Aa(n)));return{set:{rules:a.rules},...a}}),"writeBoardRules");function sc(e,t){const n=Object.keys(t||{}).sort().join(),a=n==="level,login",i=a?t.login:t?.[n],o=typeof i=="string"?i.trim():null;(!["add","remove","level,login"].includes(n)||!gs(o))&&d("Send add or remove with one login, like name@github, or a listed login with its level."),a&&!["view","full"].includes(t.level)&&d("A level is view or full.");const s=r(({login:l,level:c})=>c==="full"?{login:l,level:c}:l,"stored");return kt(e,l=>{const c=hs(l);a&&!c.some(p=>p.login===o)&&d(`${o} isn't shared with yet. Add them first.`);const w=(n==="remove"?c.filter(p=>p.login!==o):a?c.map(p=>p.login===o?{login:o,level:t.level}:p):c.some(p=>p.login===o)?c:[...c,{login:o,level:"view"}]).map(s);return{set:{share:w},share:w}}).then(({share:l})=>({share:l}))}r(sc,"saveBoardShare");const Ya=r(e=>typeof e=="string"&&/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(e),"validGithubLogin"),Xa=r(e=>{try{return e?yn(e):null}catch{return null}},"storedTickets"),Bn=r(e=>Number.isInteger(e)&&e>=1&&e<=99,"validCap"),Tn=["max_implementers","max_planners"],be=r((e,t)=>{const n=Xa(e?.tickets);return{auto_merge:e?.auto_merge===!0,auto_build:e?.auto_build===!0,auto_pull:e?.auto_pull===!0,max_implementers:Bn(e?.max_implementers)?e.max_implementers:go,max_planners:Bn(e?.max_planners)?e.max_planners:$o,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}:{},...Ya(e?.github_login)?{github_login:e.github_login}:{},...Pa(e?.build_target)?{build_target:e.build_target}:{}}},"settingsOf");async function rc(e,t,n=new Date){let a;try{a=No(t,n)}catch(i){d(i.message)}return await kt(e,()=>({set:{paused_until:a??void 0}})),a}r(rc,"setBoardPause");async function Us(e){const t=A(e);return be(await g(h.join(t,"board.json"),{}),we().find(n=>_e(n.dir,t)))}r(Us,"readBoardSettings");const lc=r((e,t)=>!!e?.github&&(!e.local||e.hub===!0)&&t.prs,"opensPRs");async function Xe(e,t){Ze(t,["auto_merge","auto_build","auto_pull","max_implementers","max_planners","auto_triage","auto_review","idle_notification","skip_draft","prs","tickets","github_login","build_target"],"the settings"),Object.keys(t).length||d("the settings name nothing to change"),t={...t};for(const[n,a]of Object.entries(t))if(n==="tickets"){try{t.tickets=yn(a)}catch(o){d(o.message)}t.tickets.source==="linear"&&!Ho()&&d(Wo);const i=t.tickets.source==="github"&&e?we().find(o=>_e(o.dir,e)):null;i&&!i.github&&d("GitHub issues need the board\u2019s repo on GitHub, and this board\u2019s repo isn\u2019t; pick internal or linear")}else n==="github_login"?a===""||a===null?t.github_login=void 0:Ya(a)||d("github_login must be a GitHub login"):n==="build_target"?a===null?t.build_target=void 0:Pa(a)||d('build_target must name .kickflip/build-<target>: letters, digits, ".", "_" or "-", or null for .kickflip/build'):Tn.includes(n)?Bn(a)||d(`${n} must be a whole number from 1 to 99`):Tt(a,n);if(t.prs===!1){const n=e?we().find(a=>_e(a.dir,e)):null;n?.github&&!n.local&&!(await rs(n.owner?.repo)).pushMain&&d("Merge lands each card on main and pushes it, so turn on Push main after a land first")}return kt(e,n=>{const a=e?we().find(s=>_e(s.dir,e)):null,i=be(n,a),o=be({...n,...t},a);return{set:t,settings:o,changed:Object.keys(t).filter(s=>JSON.stringify(i[s])!==JSON.stringify(o[s]))}})}r(Xe,"saveBoardSettings");async function cc(){if(Uo().set)return[];const e=[];for(const t of we()){if((await Us(t.dir)).tickets?.source!=="linear")continue;const{settings:n}=await Xe(t.dir,{tickets:{source:"internal"}});await ge(t,"settings",{tickets:n.tickets}),e.push(`/${t.route} fell back to internal tickets: Hub has no Linear key`)}return e}r(cc,"fallBackFromLinear");async function dc(e,t,n){const a=String(t).replace(/-/g,"_"),i=["auto_merge","auto_build","auto_pull","max_implementers","max_planners","auto_triage","auto_review","idle_notification","skip_draft","prs","github_login","tickets","build_target"];if(i.includes(a)||d(`no board setting "${t}"; settings are ${i.map(l=>l.replace(/_/g,"-")).join(", ")}`),a==="tickets"){let l;try{l=JSON.parse(n)}catch{d('settings set tickets takes the source as JSON, such as {"source":"github"}')}const{file:c,settings:u}=await Xe(e,{tickets:l});return{file:c,lines:Hs({tickets:u.tickets})}}if(a==="github_login"){const{file:l,settings:c}=await Xe(e,{github_login:n});return{file:l,lines:[`github-login ${c.github_login||"unset"}`]}}if(a==="build_target"){const{file:l,settings:c}=await Xe(e,{build_target:n==="default"?null:n});return{file:l,lines:[`build-target ${c.build_target||"default"}`]}}if(Tn.includes(a)){const{file:l,settings:c}=await Xe(e,{[a]:/^\d+$/.test(n)?Number(n):n});return{file:l,lines:[`${t} ${c[a]}`]}}["on","off"].includes(n)||d(`settings set ${t} takes on or off, not "${n}"`);const{file:o,settings:s}=await Xe(e,{[a]:n==="on"});return{file:o,lines:[`${t} ${s[a]?"on":"off"}`]}}r(dc,"setBoardSetting");const Hs=r(e=>Object.entries(e).map(([t,n])=>`setting ${t.replace(/_/g,"-")} ${t==="tickets"?JSON.stringify(n):t==="github_login"||Tn.includes(t)?n:t==="build_target"?n||"default":n?"on":"off"}`),"settingsLines");async function Cn(e,t,n){const a=t?In(()=>n(Lt())):await Va(e,n);return t&&In(()=>ms(a.rules)),{file:t?Ws():a.file,lines:[`id ${a.rule.id}`]}}r(Cn,"changeRules");async function ei(e,t,n){const a=r(s=>s.map(l=>l.id),"ids");n||A(e);const[i,o]=n?[a(Lt()),e?a(await Ln(e)):[]]:[a(await Ln(e)),a(Lt())];i.includes(t)||d(`no ${n?"hub":"board"} rule "${t}"${o.includes(t)?`; it is a ${n?"board":"hub"} rule, so ${n?"drop":"pass"} --hub`:""}`)}r(ei,"checkRuleId");async function uc(e){const t=r(n=>a=>`${n} ${a.id} ${a.by==="claude"?"claude":"owner"} ${a.text}`,"line");return{lines:[...Lt().map(t("hub")),...e?(await Ln(e)).map(t("board")):[]]}}r(uc,"listRules");function fc(e,{text:t,hub:n=!1,by:a}){return n||A(e),Cn(e,n,i=>us(i,{text:t,by:a}))}r(fc,"addRuleTo");async function pc(e,t,{text:n,hub:a=!1}){return await ei(e,t,a),Cn(e,a,i=>fs(i,t,n))}r(pc,"editRuleIn");async function wc(e,t,{hub:n=!1}){return await ei(e,t,n),Cn(e,n,a=>ps(a,t))}r(wc,"removeRuleFrom");const mc=r((e,t,n)=>Va(e,a=>ws(a,t,n)),"saveBoardRules");async function hc(e,{id:t,title:n,summary:a}){A(e);const i=await g(L(e),{topics:[],items:[]}),o=i.topics||[];return o.some(s=>s.id===S(t,"the topic id"))&&d(`topic "${t}" already exists`),o.push({id:t.trim(),title:S(n,"--title"),...a===void 0?{}:{summary:S(a,"--summary")}}),await k(L(e),{...i,topics:o,items:i.items||[]}),L(e)}r(hc,"addTopic");const ti=["quiet","info","notice","danger","success"];function zs(e,t){if(e.diagram===void 0!=(e.practice===void 0)&&d(`${t} needs diagram and practice together`),e.diagram===void 0)return;const n=e.diagram;(!Array.isArray(n)||!n.length)&&d(`${t}.diagram must be a non-empty array of rows`),n.forEach((a,i)=>{(!Array.isArray(a)||!a.length||a.length>4)&&d(`${t}.diagram[${i}] must be a row of 1 to 4 boxes`),a.forEach((o,s)=>{const l=`${t}.diagram[${i}][${s}]`;Ze(o,["text","tone","to"],l),S(o.text,`${l}.text`),X(o.tone,c=>ti.includes(c)?c:d(`${l}.tone takes ${ti.join(", ")}`)),X(o.to,c=>Array.isArray(c)&&i+1<n.length&&c.every(u=>Number.isInteger(u)&&u>=0&&u<n[i+1].length)?c:d(`${l}.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(zs,"checkDiagram");function Js(e,t){(!Array.isArray(e)||!e.length)&&d(`${t}.options must be a non-empty array`);const n=new Set;e.forEach((a,i)=>{const o=`${t}.options[${i}]`;Ze(a,["id","label","recommended","detail","diagram","practice"],o),zs(a,o),n.has(S(a.id,`${o}.id`))&&d(`${t} has two options with id "${a.id}"`),n.add(a.id),S(a.label,`${o}.label`).startsWith(`${a.id}. `)&&d(`${o}.label starts with "${a.id}. ", but the page adds the option letter itself`),X(a.recommended,s=>Tt(s,`${o}.recommended`)),X(a.detail,s=>typeof s=="string"?s:d(`${o}.detail must be text`))}),e.filter(a=>a.recommended).length>1&&d(`${t} recommends more than one option`)}r(Js,"checkOptions");function Ks(e,t,n){Ze(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`),X(e.reason,a=>S(a,`${n}.reason`))}r(Ks,"checkSuggested");const et=r(e=>h.join(e,"data","prototypes.json"),"prototypesFile"),qn=r((e,t,n)=>h.join(e,"public","prototypes",t,`${n}.js`),"takeFile");async function bt(e){const t=e?(await g(et(e),{})).prototypes:null;return Array.isArray(t)?t:[]}r(bt,"readPrototypes");const Qs=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 Gs(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(Gs,"prototypeTake");function Zs(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(Zs,"prototypeState");const ni=r(e=>e.find((t,n)=>e.findIndex(a=>a.key===t.key)!==n),"twiceIn"),Oe=r(e=>/^prototype-(.+)\.html$/.exec(h.posix.basename(String(e||"").split(/[?#]/)[0]))?.[1],"pageTopic");async function gc(e,{topic:t,title:n,card:a,takes:i=[],served:o,states:s=[]}){A(e),Qs(t),S(n,"--title"),i.length||d('prototype add needs a --take KEY:"Label" for each take');const l=i.map(Gs),c=ni(l);c&&d(`--take ${c.key} is given twice`),o!==void 0&&!/^\/\S*$/.test(o)&&d(`--served takes the page's path on the served site, starting with "/", not "${o}"`),s.length&&o===void 0&&d("--state goes with --served: only a served round re-renders its states in place");const u=s.map(Zs),w=ni(u);w&&d(`--state ${w.key} is given twice`),a!==void 0&&await x(e,a);const p=await bt(e);p.some($=>$.topic===t)&&d(`prototype ${t} is already recorded; run prototype rm ${t} first, or pick another topic`),await j.stat(h.join(e,"public",`prototype-${t}.html`)).then(()=>!0,()=>!1)&&d(`public/prototype-${t}.html is a hand-written page that questions may link; pick another topic`);const f={topic:t,title:n.trim(),...a===void 0?{}:{card:a},...o===void 0?{}:{served:o},takes:l,...u.length?{states:u}:{},at:new Date().toISOString()};await k(et(e),{prototypes:[...p,f]});const m=`page prototype-${t}.html`;return o!==void 0?{file:et(e),lines:[m,`served ${o} from the base prototype worktree (board.mjs prototype base)`,...u.length?[`states ${u.map($=>$.key).join(", ")}`]:[]]}:{file:et(e),lines:[m,...l.map($=>`take ${$.key} ${qn(e,t,$.key)}`)]}}r(gc,"addPrototype");async function $c(e){A(e);const t=await bt(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($c,"listPrototypes");async function kc(e){const t=(await bt(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&&Oe(i.url)===a))}r(kc,"servedRoundsOpen");async function bc(e,t){A(e);const n=await bt(e);n.some(o=>o.topic===t)||d(`no prototype "${t}" is recorded`);const i=((await g(L(e),{items:[]})).items||[]).filter(o=>!o.resolved&&Oe(o.url)===t).map(o=>o.id);return i.length&&d(`prototype ${t} backs open question ${i.join(", ")}; resolve ${i.length===1?"it":"them"} first`),await k(et(e),{prototypes:n.filter(o=>o.topic!==t)}),et(e)}r(bc,"removePrototype");async function Vs(e,t,n){const a=t.url?.split(/[?#]/)[0];if(!a||!h.posix.basename(a).startsWith("prototype-"))return;const i=h.posix.basename(a),o=t.url.split("#")[0].split("?").slice(1).join("?"),s=`${n}.url`;new URLSearchParams(o).get("q")!==t.id&&d(`${s} must be ${i}?q=${t.id} so the pill can pick`);const l=Oe(a),c=(await bt(e)).find(f=>f.topic===l);c||d(`${s} names ${i}, which is not a recorded prototype; record it with board.mjs prototype add ${l} --title "\u2026" --take A:"\u2026", then write its take files`);const u=c.takes.map(f=>f.key),w=t.options.map(f=>f.id),p=w.map(f=>u.indexOf(f));if(p.some((f,m)=>f<0||m>0&&f<p[m-1])&&d(`${n}.options ids ${w.join(", ")} must be a non-empty subset of ${i}'s takes, in take order: ${u.join(", ")}`),!c.served)for(const f of u)await j.stat(qn(e,l,f)).then(m=>m.isFile(),()=>!1)||d(`${s} take ${f} has no file yet; write ${qn(e,l,f)}`)}r(Vs,"checkPrototype");async function yc(e,t,n=async()=>{}){A(e);const a=Array.isArray(t)?t:[t];a.length||d("the question file is empty");const i=await g(L(e),{topics:[],items:[]}),o=i.items||[],s=await g($e(e),{}),l=new Set([...o,...Object.values(s).flat()].map(p=>p.id)),c=new Set((i.topics||[]).map(p=>p.id));for(const[p,f]of a.entries()){const m=`question ${f?.id??p+1}`;Ze(f,["id","topic","source","question","context","url","urgent","options","round_of","reask_of","ask","suggested","prototype_gate"],m),l.has(ve(f.id,`${m}.id`))&&d(`question id "${f.id}" is already used`),l.add(f.id),S(f.topic,`${m}.topic`),f.topic.startsWith("item:ask:")?await it(e,f.topic.slice(9),`${m}.topic`):f.topic.startsWith("item:")?await x(e,f.topic.slice(5)):f.topic.startsWith("wave:")?await en(e,f.topic.slice(5)):f.topic.startsWith("file:")?await Ir(e,f.topic.slice(5)):c.has(f.topic)||d(`topic "${f.topic}" is not declared; add it with: topic add ${f.topic} --title <t>`),S(f.question,`${m}.question`),f.question.length>Sa&&d(`${m}.question runs to ${f.question.length} characters, past ${Sa}; keep the question to one line and move the detail into context or the option details, which take markdown`);for(const $ of["source","context"])X(f[$],y=>S(y,`${m}.${$}`));X(f.url,$=>On($,`${m}.url`)),X(f.urgent,$=>Tt($,`${m}.urgent`)),Js(f.options,m),X(f.prototype_gate,$=>Ys($,f,m)),X(f.suggested,$=>Ks($,f.options,`${m}.suggested`)),await Vs(e,f,m),f.ask!==void 0&&await Gr(e,f,m),X(f.round_of,$=>{const y=o.find(_=>_.id===$);y||d(`${m}.round_of names no question "${$}" in owner.json`),y.topic!==f.topic&&d(`${m}.round_of names ${$} on topic ${y.topic}; a round stays on its question's topic, ${f.topic}`),Oe(f.url)||d(`${m}.round_of needs a url on a recorded prototype, prototype-<topic>.html?q=${f.id}`)}),X(f.reask_of,$=>{const y=o.find(_=>_.id===$);y||d(`${m}.reask_of names no question "${$}" in owner.json`),y.topic!==f.topic&&d(`${m}.reask_of names ${$} on topic ${y.topic}; a re-ask stays on its question's topic, ${f.topic}`),y.resolved||d(`${m}.reask_of names ${$}, which is not resolved; only a resolved question is asked again`)})}for(const p of new Set(a.map(f=>Oe(f.url)).filter(Boolean)))await n(e,p);const u=[],w=[];for(const p of a){const f=p.topic.startsWith("item:")&&!p.topic.startsWith("item:ask:")?p.topic.slice(5):null,m=f&&await ai(e,f);m&&((s[f]||=[]).push(p),w.push(p),u.push(`staged: ${p.id} waits for ${f}'s ${m} to finish; the card's staged questions then publish together`))}return w.length&&await k($e(e),s),w.length===a.length?{file:$e(e),lines:u,staged:w.length}:oi(e,i,o,a.filter(p=>!w.includes(p)),u)}r(yc,"addQuestions");async function ai(e,t){const{source:n}=await x(e,t);for(const a of await H(n.dir,N(t).id)){const i=await g(a,null);if(i?.started&&!i.done&&Ee(i)&&i.kind!==pe)return i.kind}return null}r(ai,"runningPlanner");async function tt(e,t,{screen:n=!1}={}){if(!e||await ai(e,t))return;const a=await g($e(e),{});if(!a[t])return;const i=await g(Ge(e),{});if(n){i[t]||await k(Ge(e),{...i,[t]:new Date().toISOString()});return}const{[t]:o,...s}=a,l=await g(L(e),{topics:[],items:[]});await oi(e,l,l.items||[],o,[]),await k($e(e),s),i[t]&&await k(Ge(e),Mn(i,t))}r(tt,"publishStaged");const Mn=r((e,t)=>Object.fromEntries(Object.entries(e).filter(([n])=>n!==t)),"withoutKey");async function ii(e){const t=await g(Ge(e),{}),n=await g($e(e),{});return Object.fromEntries(Object.entries(t).filter(([a])=>n[a]).map(([a,i])=>[a,{since:i,ids:n[a].map(o=>o.id)}]))}r(ii,"readScreening");async function xc(e,t,n){A(e);const a=S(n,"--note"),i=await g($e(e),{}),o=await g(Ge(e),{}),s=Object.keys(i).find(p=>i[p].some(f=>f.id===t));s||d(`no staged question "${t}"; question settle takes a question a finished grill left for you to screen`),o[s]||d(`${t} waits for ${s}'s planner to finish`);const l=i[s].find(p=>p.id===t);l.prototype_gate&&d(`${t} is the card's prototype question, which is the owner's; question publish ${s} passes it on`);const c=new Date().toISOString(),u=await g(L(e),{topics:[],items:[]});await k(L(e),{...u,topics:u.topics||[],items:[...u.items||[],{...l,asked_at:c,resolved:`Settled by the orchestrator: ${a}`,resolved_at:c}]});const w=i[s].filter(p=>p.id!==t);return await k($e(e),w.length?{...i,[s]:w}:Mn(i,s)),w.length||await k(Ge(e),Mn(o,s)),await nt(e,s,`Answers (orchestrator, ${yt()})`,`- ${t} (${l.question}): settled by the orchestrator. "${a}"`),L(e)}r(xc,"settleQuestion");async function _c(e,t){A(e);const n=(await ii(e))[t]?.ids||d(`${t} has no questions waiting on you to screen`);return await tt(e,t),n}r(_c,"publishScreened");async function Sc(e,t=new Date){const n=Object.entries(await ii(e)).filter(([,a])=>t-Date.parse(a.since)>=ks).map(([a])=>a);for(const a of n)await tt(e,a),await W(e,a,"screen-timeout");return n}r(Sc,"publishUnscreened");async function oi(e,t,n,a,i){const{answers:o,requests:s,reopens:l}=await at(e),c=new Date().toISOString(),u=[],w=[];for(const f of a.filter(m=>m.round_of))f.round=gn(n.find(m=>m.id===f.round_of))+1,Pe(t,[f.round_of],`Followed by prototype round ${f.round}`,c),w.push(f.round_of);for(const f of a.filter(m=>m.reask_of))Pe(t,[f.reask_of],`Asked again as ${f.id}`,c),w.push(f.reask_of);for(const f of a){const m=Oe(f.url),$=n.filter(_=>_.topic===f.topic).map(_=>Ne(_,o,s,[],l)),y=f.topic.startsWith("item:")?fi($):[];for(const _ of $)y.includes(_.id)?(Pe(t,[_.id],`Followed by ${f.id}`,c),w.push(_.id)):!_.resolved&&m&&Oe(_.url)===m&&(Pe(t,[_.id],`Replaced by ${f.id}`,c),u.push(_.id))}const p={...t,topics:t.topics||[],items:[...n,...a.map(f=>({...f,asked_at:c}))]};await k(L(e),p);for(const f of new Set(a.map(m=>Wn(m.topic)).filter(Boolean)))await sr(e,f);return{file:L(e),lines:[...i,...u.map(f=>`replaced: ${f}`),...w.map(f=>`resolved: ${f}`)]}}r(oi,"publishQuestions");function Ys(e,t,n){Tt(e,`${n}.prototype_gate`),e&&((!t.topic.startsWith("item:")||t.topic.startsWith("item:ask:"))&&d(`${n}.prototype_gate goes on a card's topic, item:<file>/<id>`),t.options?.map(a=>a.id).join()!=="yes,no"&&d(`${n}.prototype_gate takes exactly the options yes and no, in that order`))}r(Ys,"checkGate");async function vc(e,t){const n=await g(L(e),{topics:[],items:[]}),a=new Map((n.items||[]).map(l=>[l.id,l]));for(const l of t){const c=a.get(String(l.id));if(!c?.prototype_gate||!["yes","no"].includes(l.choice))continue;Pe(n,[c.id],`Prototype round: ${l.choice}. The board moves the card on once every question is answered.`),await k(L(e),n);const u=c.topic.slice(5);l.choice==="yes"&&(await x(e,u).catch(()=>null))?.item.phase==="grilling"&&await Qn(e,u)&&await W(e,u,"prototype-after")}const i=new Map;for(const l of t){const c=a.get(String(l.id)),u=Wn(c?.topic);u&&i.set(u,[...i.get(u)||[],er(c,l)])}const o=[],s=[];for(const[l,c]of i)await x(e,l).catch(()=>null)&&await Xs(e,l,"writing them into its notes",async()=>{await nt(e,l,`Answers (owner, ${yt()})`,c.join(`
|
|
18
|
-
`));const
|
|
12
|
+
${Pn}`,login:l});return await Ya(e,i,{state:"posted",text:o,review_url:w}),{lines:[`posted review on PR #${t}: ${w}`]}}catch(u){if(u instanceof le)throw u;d(u.message)}}r(Xc,"postIncomingReview");const Kt=r(e=>h.join(e,"data","handshake.json"),"handshakeFile");async function Xa(e){const t=await g(Kt(e),null);if(!t?.hello)return t;const{hello:n,...a}=t;return{...a,ping:a.ping||n}}r(Xa,"readHandshake");async function el(e,t){A(e),Oe(t,"the event id");const n=(await Q(h.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 Xa(e)||{};return a.ping?.id&&a.ping.id!==t&&d(`"${t}" is an older ping; ack the newest, "${a.ping.id}"`),await b(Kt(e),{...a,ping:a.ping||{id:t,at:n.ts,session:null},ack:{id:t,at:new Date().toISOString()}}),Kt(e)}r(el,"ackPing");async function tl(e){const t=await Xa(e);t&&!t.done_at&&await b(Kt(e),{...t,done_at:new Date().toISOString()})}r(tl,"finishHandshake");const Tn={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})$/},St=r((e,t)=>e.trim().toLowerCase()===t.trim().toLowerCase(),"sameName");function Cn(e,t){return Array.isArray(e)||d(`${t} must be a JSON array of reviewers`),e.forEach((n,a)=>{const i=`${t}[${a}]`;et(n,Object.keys(Tn),i);for(const[o,s]of Object.entries(Tn))(typeof n[o]!="string"||!s.test(n[o].trim()))&&d(`${i}.${o} must match ${s}`);e.slice(0,a).some(o=>St(o.name,n.name))&&d(`${t} names "${n.name}" twice`)}),e.map(n=>Object.fromEntries(Object.keys(Tn).map(a=>[a,n[a].trim()])))}r(Cn,"checkReviewers");async function qn(e,t){const{file:n,reviewers:a}=await jt(e,o=>{const s=Cn(t(o.reviewers||[]),"the reviewers");return{set:{reviewers:s},reviewers:s}}),i=[];for(const o of await M(e)){const s=await g(o.file,null);(s?.items||[]).some(c=>!G(c))&&(await b(o.file,{...s,reviewers:a}),i.push(`item file ${o.slug}: reviewers copied`))}return{file:n,lines:i}}r(qn,"writeReviewers");const nl=r((e,t)=>qn(e,()=>Cn(t,"the reviewer file")),"setReviewers"),nt=r(e=>h.join(A(e),"data","pr-slack.json"),"prSlackFile"),Qt=r(e=>g(nt(e),{}),"readPRSlack"),vt=r(e=>!!(e?.ts||e?.posting_at),"held");async function We(e,t){return Ze(e,async()=>{const n=JSON.parse(await O.readFile(e,"utf8").catch(()=>"{}")),a=t(n);return await Te(e,JSON.stringify(n,null,2)+`
|
|
13
|
+
`),a})}r(We,"editJSON");async function Gt(e,t,n){await We(e,i=>{const o=t(i);vt(o)&&d("that Slack message has already gone out"),o.posting_at=new Date().toISOString()});let a;try{a=await n()}catch(i){throw await We(e,o=>{delete t(o).posting_at}),i}try{await We(e,i=>{const o=t(i);o.ts=a,delete o.posting_at})}catch(i){d(`posted, but its record failed, so it won't go again: ${i.message}`)}return a}r(Gt,"postOnce");const al=r((e,t,n,a)=>Gt(nt(e),i=>{const o=i[t.pr]||={head:t.head,links:{},replies:[]};return o.head??=t.head,Object.assign(o.links[t.reviewer]||={reactions:[]},{channel:n})},()=>a(n)),"postLink"),il=r((e,t,n)=>Gt(nt(e),a=>a[t.pr].replies.find(i=>i.id===t.id).to[t.reviewer],n),"postReply"),ol=r((e,t)=>We(nt(e),n=>{const a=n[t.pr].links[t.reviewer];a.reactions.includes(t.emoji)||a.reactions.push(t.emoji)}),"recordReaction"),ei=r((e,t,n,a)=>We(nt(e),i=>{const o=i[t],s=Object.keys(o?.links||{}).filter(c=>o.links[c].ts);return s.length?(o.replies.push({id:`${n}-${o.replies.length+1}`,text:a,to:Object.fromEntries(s.map(c=>[c,{}]))}),s):[]}),"queueReply");function Hs(e,t,n){const a=e.commits||[],i=a.findIndex(o=>o.oid===n);return[`New push to #${t}`,a.slice(i+1).map(o=>o.subject).join(" \xB7 ")].filter(Boolean).join(`
|
|
14
|
+
`)}r(Hs,"pushText");async function sl(e){const t=await g(vn,{}),n=r((a,i)=>{const o=ae(t.prs,a)?.head;return!!o&&o!==i.head&&Object.values(i.links).some(s=>s.ts)},"moved");return Object.entries(await Qt(e)).some(([a,i])=>n(a,i))?We(nt(e),a=>{const i=[];for(const[o,s]of Object.entries(a)){if(!n(o,s))continue;const c=ae(t.prs,o),l=Object.keys(s.links).filter(u=>s.links[u].ts);s.head&&(s.replies.push({id:`push-${s.replies.length+1}`,text:Hs(c,o.split("#")[1],s.head),to:Object.fromEntries(l.map(u=>[u,{}]))}),i.push(o)),s.head=c.head}return i}):[]}r(sl,"queuePushes");function Js(e,t,n){const a=Number.isInteger(e.additions)?`+${e.additions}/-${e.deletions}, ${e.files} file${e.files===1?"":"s"}`:null;return[e.title||n.title,Dn(t),a].filter(Boolean).join(`
|
|
15
|
+
`)}r(Js,"linkText");const ti=r(e=>!!e?.approved||e?.reviewDecision==="APPROVED","approved");async function rl(e,t){A(e);const n=await Ea.waiting("slack");if(n)return{waiting:n,links:[],replies:[],reactions:[],digest:null};const a=await ie(e),i=await g(vn,{}),o=await Qt(e),s=[];for(const w of await M(e)){const p=await g(w.file,{}),f=Fn(p,a.reviewers);for(const m of p.items||[])if(m.lane!=="done")for(const $ of Ie(m,oe(e,p))){const y=xt($),S=ae(i.prs,y);if(!(S?.state!=="OPEN"||S.isDraft))for(const E of f)vt(o[y]?.links?.[E.name])||s.some(j=>j.pr===y&&j.reviewer===E.name)||s.push({pr:y,key:`${w.slug}/${m.id}`,reviewer:E.name,slack_user:E.slack_user,head:S.head||null,text:Js(S,$,m)})}}const c=[],l=[];for(const[w,p]of Object.entries(o)){const f=ae(i.prs,w),m=[ti(f)&&"git_approved",f?.state==="MERGED"?"git_merged":f?.state==="CLOSED"&&"x"].filter(Boolean);for(const[$,y]of Object.entries(p.links))if(y.ts)for(const S of m)y.reactions.includes(S)||l.push({pr:w,reviewer:$,channel:y.channel,ts:y.ts,emoji:S});for(const $ of p.replies)for(const[y,S]of Object.entries($.to))vt(S)||c.push({pr:w,id:$.id,reviewer:y,channel:p.links[y].channel,thread_ts:p.links[y].ts,text:$.text})}const u=await zs(e,t);return{links:s,replies:c,reactions:l,digest:u.due?u:null}}r(rl,"reviewDue");async function cl(e,t,n){A(e);const a=_(n,"what changed"),{itemFile:i,item:o}=await x(e,t),s=[];for(const l of Ie(o,oe(e,i)))s.push(...await ei(e,xt(l),"updated",`Updated: ${a}`));const c=[...new Set(s)];return{lines:[c.length?`Updated queued for ${c.join(", ")} on ${t}`:`no reviewer has a link to ${t}'s PRs; nothing queued`]}}r(cl,"reviewUpdated");async function ll(e,t){const{itemFile:n,item:a}=await x(e,t),i=await Qt(e);return Ie(a,oe(e,n)).some(o=>Object.values(i[xt(o)]?.links||{}).some(s=>s.ts))}r(ll,"cardLinked");const ni=r(e=>h.join(A(e),"data","review-digests.json"),"digestFile");async function zs(e,{now:t=new Date}={}){A(e);const n=await ie(e),a=await g(vn,{}),i=await Qt(e),o=fs(),s=new Map;for(const f of await M(e)){const m=await g(f.file,{}),$=Fn(m,n.reviewers);for(const y of $)s.has(y.name)||s.set(y.name,{name:y.name,slack_user:y.slack_user,dm:y.dm,items:[]});for(const y of m.items||[]){if(y.lane==="done")continue;const S=`${f.slug}/${y.id}`,E=us(m,y,S,o);for(const j of Ie(y,oe(e,m))){const F=xt(j),D=ae(a.prs,F);if(!(D?.state!=="OPEN"||D.isDraft||D.failing?.length||ti(D)))for(const T of $){const v=i[F]?.links?.[T.name]?.ts||ae(E,T.name)?.ts||null,B=v?(t.getTime()-Number(v)*1e3)/864e5:null,C=B>2?Math.floor(B):null;s.get(T.name).items.push({key:S,pr:F,title:y.title,url:Dn(j),sent_ts:v,...C?{days_old:C}:{},text:`${Dn(j)} \xB7 ${y.title}${C?` (${C} days old)`:""}`})}}}}const{date:c,open:l}=ps(t),u=((await g(ni(e),{})).digests||[]).filter(f=>f.date===c),w=await Ea.waiting("slack");for(const f of u)s.has(f.reviewer)||s.set(f.reviewer,{name:f.reviewer,items:[]});const p=[...s.values()].map(f=>{const m=u.find(y=>y.reviewer===f.name),$=m?!m.posting_at&&(!m.ts||m.replies.some(y=>!vt(y))):f.items.length>0;return{...f,owed:$,...m?{sent:m}:{}}}).filter(f=>f.items.length||f.sent);return{date:c,due:!w&&l&&p.some(f=>f.owed),...w?{waiting:w}:{},reviewers:p}}r(zs,"reviewDigest");async function dl(e,t,n,{channel:a,header:i,post:o}){const s=ni(e),c=r(w=>(w.digests||[]).find(p=>p.date===t&&p.reviewer===n.name),"entry");await We(s,w=>{c(w)||(w.digests=[...(w.digests||[]).filter(p=>p.date===t),{date:t,reviewer:n.name,channel:a,replies:n.items.map(p=>({pr:p.pr,text:p.text}))}],delete w.last_date)});const l=c(await g(s,{})),u=l.ts||await Gt(s,c,()=>o({channel:l.channel,text:i(l.replies.length)}));for(const[w,p]of l.replies.entries())vt(p)||await Gt(s,f=>c(f).replies[w],()=>o({channel:l.channel,thread_ts:u,text:p.text}))}r(dl,"postDigest");function ul(e,t){const n=Cn(Array.isArray(t)?t:[t],"the reviewer file");return qn(e,a=>[...a.map(i=>n.find(o=>St(o.name,i.name))||i),...n.filter(i=>!a.some(o=>St(i.name,o.name)))])}r(ul,"addReviewers");function fl(e,t){return qn(e,n=>{const a=t.filter(i=>!n.some(o=>St(o.name,i)));return a.length&&d(`this board has no reviewer ${a.map(i=>`"${i}"`).join(", ")}`),n.filter(i=>!t.some(o=>St(i.name,o)))})}r(fl,"removeReviewers");const Ks=r(()=>h.join(bn.homedir(),".claude","kickflip","settings.json"),"hubSettingsFile"),Mn=r(e=>{try{return e()}catch(t){throw t instanceof gs&&d(t.message),t}},"ruleCall"),Wn=r(async e=>Ia(await g(h.join(e,"board.json"),{})),"readBoardRules");let ai=Promise.resolve();function jt(e,t){const n=ai.then(async()=>{const a=h.join(A(e),"board.json"),i=await g(a,{}),{set:o,...s}=t(i);return await b(a,{...i,...o}),{file:a,...s}});return ai=n.catch(()=>{}),n}r(jt,"writeBoardJSON");const ii=r((e,t)=>jt(e,n=>{const a=Mn(()=>t(Ia(n)));return{set:{rules:a.rules},...a}}),"writeBoardRules");function pl(e,t){const n=Object.keys(t||{}).sort().join(),a=n==="level,login",i=a?t.login:t?.[n],o=typeof i=="string"?i.trim():null;(!["add","remove","level,login"].includes(n)||!Ss(o))&&d("Send add or remove with one login, like name@github, or a listed login with its level."),a&&!["view","full"].includes(t.level)&&d("A level is view or full.");const s=r(({login:c,level:l})=>l==="full"?{login:c,level:l}:c,"stored");return jt(e,c=>{const l=_s(c);a&&!l.some(p=>p.login===o)&&d(`${o} isn't shared with yet. Add them first.`);const w=(n==="remove"?l.filter(p=>p.login!==o):a?l.map(p=>p.login===o?{login:o,level:t.level}:p):l.some(p=>p.login===o)?l:[...l,{login:o,level:"view"}]).map(s);return{set:{share:w},share:w}}).then(({share:c})=>({share:c}))}r(pl,"saveBoardShare");const oi=r(e=>typeof e=="string"&&/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(e),"validGithubLogin"),si=r(e=>{try{return e?Pa(e):null}catch{return null}},"storedTickets"),Un=r(e=>Number.isInteger(e)&&e>=1&&e<=99,"validCap"),Hn=["max_implementers","max_planners"],be=r((e,t)=>{const n=si(e?.tickets);return{auto_merge:e?.auto_merge===!0,auto_build:e?.auto_build===!0,auto_pull:e?.auto_pull===!0,max_implementers:Un(e?.max_implementers)?e.max_implementers:vo,max_planners:Un(e?.max_planners)?e.max_planners:jo,auto_triage:e?.auto_triage===!0,auto_review:e?.auto_review===!0,idle_notification:e?.idle_notification===!0,skip_draft:e?.skip_draft===!0,prs:t?.hub?e?.prs===!0:e?.prs!==!1,...n?{tickets:n}:{},...oi(e?.github_login)?{github_login:e.github_login}:{},...La(e?.build_target)?{build_target:e.build_target}:{}}},"settingsOf");async function wl(e,t,n=new Date){let a;try{a=Mo(t,n)}catch(i){d(i.message)}return await jt(e,()=>({set:{paused_until:a??void 0}})),a}r(wl,"setBoardPause");async function Qs(e){const t=A(e);return be(await g(h.join(t,"board.json"),{}),we().find(n=>ve(n.dir,t)))}r(Qs,"readBoardSettings");const ml=r((e,t)=>!!e?.github&&(!e.local||e.hub===!0)&&t.prs,"opensPRs");async function at(e,t){et(t,["auto_merge","auto_build","auto_pull","max_implementers","max_planners","auto_triage","auto_review","idle_notification","skip_draft","prs","tickets","github_login","build_target"],"the settings"),Object.keys(t).length||d("the settings name nothing to change"),t={...t};for(const[n,a]of Object.entries(t))if(n==="tickets"){try{t.tickets=Pa(a)??void 0}catch(o){d(o.message)}t.tickets?.source==="linear"&&!Zo()&&d(Go);const i=t.tickets?.source==="github"&&e?we().find(o=>ve(o.dir,e)):null;i&&!i.github&&d("GitHub issues need the board\u2019s repo on GitHub, and this board\u2019s repo isn\u2019t; pick none or linear")}else n==="github_login"?a===""||a===null?t.github_login=void 0:oi(a)||d("github_login must be a GitHub login"):n==="build_target"?a===null?t.build_target=void 0:La(a)||d('build_target must name .kickflip/build-<target>: letters, digits, ".", "_" or "-", or null for .kickflip/build'):Hn.includes(n)?Un(a)||d(`${n} must be a whole number from 1 to 99`):Wt(a,n);if(t.prs===!1){const n=e?we().find(a=>ve(a.dir,e)):null;n?.github&&!n.local&&!(await ws(n.owner?.repo)).pushMain&&d("Merge lands each card on main and pushes it, so turn on Push main after a land first")}return jt(e,n=>{const a=e?we().find(s=>ve(s.dir,e)):null,i=be(n,a),o=be({...n,...t},a);return{set:t,settings:o,changed:Object.keys(t).filter(s=>JSON.stringify(i[s])!==JSON.stringify(o[s]))}})}r(at,"saveBoardSettings");async function hl(){if(Vo().set)return[];const e=[];for(const t of we())(await Qs(t.dir)).tickets?.source==="linear"&&(await at(t.dir,{tickets:{source:"none"}}),await ge(t,"settings",{tickets:null}),e.push(`/${t.route} fell back to no ticket source: Hub has no Linear key`));return e}r(hl,"fallBackFromLinear");async function gl(e,t,n){const a=String(t).replace(/-/g,"_"),i=["auto_merge","auto_build","auto_pull","max_implementers","max_planners","auto_triage","auto_review","idle_notification","skip_draft","prs","github_login","tickets","build_target"];if(i.includes(a)||d(`no board setting "${t}"; settings are ${i.map(c=>c.replace(/_/g,"-")).join(", ")}`),a==="tickets"){let c;try{c=JSON.parse(n)}catch{d('settings set tickets takes the source as JSON, such as {"source":"github"}')}const{file:l,settings:u}=await at(e,{tickets:c});return{file:l,lines:Gs({tickets:u.tickets})}}if(a==="github_login"){const{file:c,settings:l}=await at(e,{github_login:n});return{file:c,lines:[`github-login ${l.github_login||"unset"}`]}}if(a==="build_target"){const{file:c,settings:l}=await at(e,{build_target:n==="default"?null:n});return{file:c,lines:[`build-target ${l.build_target||"default"}`]}}if(Hn.includes(a)){const{file:c,settings:l}=await at(e,{[a]:/^\d+$/.test(n)?Number(n):n});return{file:c,lines:[`${t} ${l[a]}`]}}["on","off"].includes(n)||d(`settings set ${t} takes on or off, not "${n}"`);const{file:o,settings:s}=await at(e,{[a]:n==="on"});return{file:o,lines:[`${t} ${s[a]?"on":"off"}`]}}r(gl,"setBoardSetting");const Gs=r(e=>Object.entries(e).map(([t,n])=>`setting ${t.replace(/_/g,"-")} ${t==="tickets"?JSON.stringify(n??{source:"none"}):t==="github_login"||Hn.includes(t)?n:t==="build_target"?n||"default":n?"on":"off"}`),"settingsLines");async function Jn(e,t,n){const a=t?Mn(()=>n(qt())):await ii(e,n);return t&&Mn(()=>xs(a.rules)),{file:t?Ks():a.file,lines:[`id ${a.rule.id}`]}}r(Jn,"changeRules");async function ri(e,t,n){const a=r(s=>s.map(c=>c.id),"ids");n||A(e);const[i,o]=n?[a(qt()),e?a(await Wn(e)):[]]:[a(await Wn(e)),a(qt())];i.includes(t)||d(`no ${n?"hub":"board"} rule "${t}"${o.includes(t)?`; it is a ${n?"board":"hub"} rule, so ${n?"drop":"pass"} --hub`:""}`)}r(ri,"checkRuleId");async function $l(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 Wn(e)).map(t("board")):[]]}}r($l,"listRules");function kl(e,{text:t,hub:n=!1,by:a}){return n||A(e),Jn(e,n,i=>$s(i,{text:t,by:a}))}r(kl,"addRuleTo");async function bl(e,t,{text:n,hub:a=!1}){return await ri(e,t,a),Jn(e,a,i=>ks(i,t,n))}r(bl,"editRuleIn");async function yl(e,t,{hub:n=!1}){return await ri(e,t,n),Jn(e,n,a=>bs(a,t))}r(yl,"removeRuleFrom");const xl=r((e,t,n)=>ii(e,a=>ys(a,t,n)),"saveBoardRules");async function _l(e,{id:t,title:n,summary:a}){A(e);const i=await g(L(e),{topics:[],items:[]}),o=i.topics||[];return o.some(s=>s.id===_(t,"the topic id"))&&d(`topic "${t}" already exists`),o.push({id:t.trim(),title:_(n,"--title"),...a===void 0?{}:{summary:_(a,"--summary")}}),await b(L(e),{...i,topics:o,items:i.items||[]}),L(e)}r(_l,"addTopic");const ci=["quiet","info","notice","danger","success"];function Vs(e,t){if(e.diagram===void 0!=(e.practice===void 0)&&d(`${t} needs diagram and practice together`),e.diagram===void 0)return;const n=e.diagram;(!Array.isArray(n)||!n.length)&&d(`${t}.diagram must be a non-empty array of rows`),n.forEach((a,i)=>{(!Array.isArray(a)||!a.length||a.length>4)&&d(`${t}.diagram[${i}] must be a row of 1 to 4 boxes`),a.forEach((o,s)=>{const c=`${t}.diagram[${i}][${s}]`;et(o,["text","tone","to"],c),_(o.text,`${c}.text`),ee(o.tone,l=>ci.includes(l)?l:d(`${c}.tone takes ${ci.join(", ")}`)),ee(o.to,l=>Array.isArray(l)&&i+1<n.length&&l.every(u=>Number.isInteger(u)&&u>=0&&u<n[i+1].length)?l:d(`${c}.to must list boxes of the next row by index`))})}),(!Array.isArray(e.practice)||!e.practice.length)&&d(`${t}.practice must be a non-empty array of lines`),e.practice.forEach((a,i)=>_(a,`${t}.practice[${i}]`))}r(Vs,"checkDiagram");function Zs(e,t){(!Array.isArray(e)||!e.length)&&d(`${t}.options must be a non-empty array`);const n=new Set;e.forEach((a,i)=>{const o=`${t}.options[${i}]`;et(a,["id","label","recommended","detail","diagram","practice"],o),Vs(a,o),n.has(_(a.id,`${o}.id`))&&d(`${t} has two options with id "${a.id}"`),n.add(a.id),_(a.label,`${o}.label`).startsWith(`${a.id}. `)&&d(`${o}.label starts with "${a.id}. ", but the page adds the option letter itself`),ee(a.recommended,s=>Wt(s,`${o}.recommended`)),ee(a.detail,s=>typeof s=="string"?s:d(`${o}.detail must be text`))}),e.filter(a=>a.recommended).length>1&&d(`${t} recommends more than one option`)}r(Zs,"checkOptions");function Ys(e,t,n){et(e,["option","card","answer","reason"],n),t.some(a=>a.id===_(e.option,`${n}.option`))||d(`${n}.option names no option "${e.option}"`),_(e.card,`${n}.card`),_(e.answer,`${n}.answer`),ee(e.reason,a=>_(a,`${n}.reason`))}r(Ys,"checkSuggested");const it=r(e=>h.join(e,"data","prototypes.json"),"prototypesFile"),zn=r((e,t,n)=>h.join(e,"public","prototypes",t,`${n}.js`),"takeFile");async function Ot(e){const t=e?(await g(it(e),{})).prototypes:null;return Array.isArray(t)?t:[]}r(Ot,"readPrototypes");const Xs=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=_(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 tr(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:_(t[2],`--state ${t[1]}'s label`)}}r(tr,"prototypeState");const li=r(e=>e.find((t,n)=>e.findIndex(a=>a.key===t.key)!==n),"twiceIn"),Ae=r(e=>/^prototype-(.+)\.html$/.exec(h.posix.basename(String(e||"").split(/[?#]/)[0]))?.[1],"pageTopic");async function Sl(e,{topic:t,title:n,card:a,takes:i=[],served:o,states:s=[]}){A(e),Xs(t),_(n,"--title"),i.length||d('prototype add needs a --take KEY:"Label" for each take');const c=i.map(er),l=li(c);l&&d(`--take ${l.key} is given twice`),o!==void 0&&!/^\/\S*$/.test(o)&&d(`--served takes the page's path on the served site, starting with "/", not "${o}"`),s.length&&o===void 0&&d("--state goes with --served: only a served round re-renders its states in place");const u=s.map(tr),w=li(u);w&&d(`--state ${w.key} is given twice`),a!==void 0&&await x(e,a);const p=await Ot(e);p.some($=>$.topic===t)&&d(`prototype ${t} is already recorded; run prototype rm ${t} first, or pick another topic`),await O.stat(h.join(e,"public",`prototype-${t}.html`)).then(()=>!0,()=>!1)&&d(`public/prototype-${t}.html is a hand-written page that questions may link; pick another topic`);const f={topic:t,title:n.trim(),...a===void 0?{}:{card:a},...o===void 0?{}:{served:o},takes:c,...u.length?{states:u}:{},at:new Date().toISOString()};await b(it(e),{prototypes:[...p,f]});const m=`page prototype-${t}.html`;return o!==void 0?{file:it(e),lines:[m,`served ${o} from the base prototype worktree (board.mjs prototype base)`,...u.length?[`states ${u.map($=>$.key).join(", ")}`]:[]]}:{file:it(e),lines:[m,...c.map($=>`take ${$.key} ${zn(e,t,$.key)}`)]}}r(Sl,"addPrototype");async function vl(e){A(e);const t=await Ot(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(vl,"listPrototypes");async function jl(e){const t=(await Ot(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&&Ae(i.url)===a))}r(jl,"servedRoundsOpen");async function Ol(e,t){A(e);const n=await Ot(e);n.some(o=>o.topic===t)||d(`no prototype "${t}" is recorded`);const i=((await g(L(e),{items:[]})).items||[]).filter(o=>!o.resolved&&Ae(o.url)===t).map(o=>o.id);return i.length&&d(`prototype ${t} backs open question ${i.join(", ")}; resolve ${i.length===1?"it":"them"} first`),await b(it(e),{prototypes:n.filter(o=>o.topic!==t)}),it(e)}r(Ol,"removePrototype");async function nr(e,t,n){const a=t.url?.split(/[?#]/)[0];if(!a||!h.posix.basename(a).startsWith("prototype-"))return;const i=h.posix.basename(a),o=t.url.split("#")[0].split("?").slice(1).join("?"),s=`${n}.url`;new URLSearchParams(o).get("q")!==t.id&&d(`${s} must be ${i}?q=${t.id} so the pill can pick`);const c=Ae(a),l=(await Ot(e)).find(f=>f.topic===c);l||d(`${s} names ${i}, which is not a recorded prototype; record it with board.mjs prototype add ${c} --title "\u2026" --take A:"\u2026", then write its take files`);const u=l.takes.map(f=>f.key),w=t.options.map(f=>f.id),p=w.map(f=>u.indexOf(f));if(p.some((f,m)=>f<0||m>0&&f<p[m-1])&&d(`${n}.options ids ${w.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(zn(e,c,f)).then(m=>m.isFile(),()=>!1)||d(`${s} take ${f} has no file yet; write ${zn(e,c,f)}`)}r(nr,"checkPrototype");async function Pl(e,t,n=async()=>{}){A(e);const a=Array.isArray(t)?t:[t];a.length||d("the question file is empty");const i=await g(L(e),{topics:[],items:[]}),o=i.items||[],s=await g($e(e),{}),c=new Set([...o,...Object.values(s).flat()].map(p=>p.id)),l=new Set((i.topics||[]).map(p=>p.id));for(const[p,f]of a.entries()){const m=`question ${f?.id??p+1}`;et(f,["id","topic","source","question","context","url","urgent","options","round_of","reask_of","ask","suggested","prototype_gate","ticket_gate"],m),c.has(Oe(f.id,`${m}.id`))&&d(`question id "${f.id}" is already used`),c.add(f.id),_(f.topic,`${m}.topic`),f.topic.startsWith("item:ask:")?await ct(e,f.topic.slice(9),`${m}.topic`):f.topic.startsWith("item:")?await x(e,f.topic.slice(5)):f.topic.startsWith("wave:")?await ut(e,f.topic.slice(5)):f.topic.startsWith("file:")?await Wr(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>`),_(f.question,`${m}.question`),f.question.length>Fa&&d(`${m}.question runs to ${f.question.length} characters, past ${Fa}; keep the question to one line and move the detail into context or the option details, which take markdown`);for(const $ of["source","context"])ee(f[$],y=>_(y,`${m}.${$}`));ee(f.url,$=>Rn($,`${m}.url`)),ee(f.urgent,$=>Wt($,`${m}.urgent`)),Zs(f.options,m),ee(f.prototype_gate,$=>pi($,f,`${m}.prototype_gate`)),f.ticket_gate!==void 0&&pi(f.ticket_gate,f,`${m}.ticket_gate`)&&await ar(e,f,m),ee(f.suggested,$=>Ys($,f.options,`${m}.suggested`)),await nr(e,f,m),f.ask!==void 0&&await nc(e,f,m),ee(f.round_of,$=>{const y=o.find(S=>S.id===$);y||d(`${m}.round_of names no question "${$}" in owner.json`),y.topic!==f.topic&&d(`${m}.round_of names ${$} on topic ${y.topic}; a round stays on its question's topic, ${f.topic}`),Ae(f.url)||d(`${m}.round_of needs a url on a recorded prototype, prototype-<topic>.html?q=${f.id}`)}),ee(f.reask_of,$=>{const y=o.find(S=>S.id===$);y||d(`${m}.reask_of names no question "${$}" in owner.json`),y.topic!==f.topic&&d(`${m}.reask_of names ${$} on topic ${y.topic}; a re-ask stays on its question's topic, ${f.topic}`),y.resolved||d(`${m}.reask_of names ${$}, which is not resolved; only a resolved question is asked again`)})}for(const p of new Set(a.map(f=>Ae(f.url)).filter(Boolean)))await n(e,p);const u=[],w=[];for(const p of a){const f=p.topic.startsWith("item:")&&!p.topic.startsWith("item:ask:")?p.topic.slice(5):null,m=f&&await di(e,f);m&&((s[f]||=[]).push(p),w.push(p),u.push(`staged: ${p.id} waits for ${f}'s ${m} to finish; the card's staged questions then publish together`))}return w.length&&await b($e(e),s),w.length===a.length?{file:$e(e),lines:u,staged:w.length}:fi(e,i,o,a.filter(p=>!w.includes(p)),u)}r(Pl,"addQuestions");async function di(e,t){const{source:n}=await x(e,t);for(const a of await z(n.dir,P(t).id)){const i=await g(a,null);if(i?.started&&!i.done&&Le(i)&&i.kind!==pe)return i.kind}return null}r(di,"runningPlanner");async function ot(e,t,{screen:n=!1}={}){if(!e||await di(e,t))return;const a=await g($e(e),{});if(!a[t])return;const i=await g(Xe(e),{});if(n){i[t]||await b(Xe(e),{...i,[t]:new Date().toISOString()});return}const{[t]:o,...s}=a,c=await g(L(e),{topics:[],items:[]});await fi(e,c,c.items||[],o,[]),await b($e(e),s),i[t]&&await b(Xe(e),Kn(i,t))}r(ot,"publishStaged");const Kn=r((e,t)=>Object.fromEntries(Object.entries(e).filter(([n])=>n!==t)),"withoutKey");async function ui(e){const t=await g(Xe(e),{}),n=await g($e(e),{});return Object.fromEntries(Object.entries(t).filter(([a])=>n[a]).map(([a,i])=>[a,{since:i,ids:n[a].map(o=>o.id)}]))}r(ui,"readScreening");async function Al(e,t,n){A(e);const a=_(n,"--note"),i=await g($e(e),{}),o=await g(Xe(e),{}),s=Object.keys(i).find(p=>i[p].some(f=>f.id===t));s||d(`no staged question "${t}"; question settle takes a question a finished grill left for you to screen`),o[s]||d(`${t} waits for ${s}'s planner to finish`);const c=i[s].find(p=>p.id===t);c.prototype_gate&&d(`${t} is the card's prototype question, which is the owner's; question publish ${s} passes it on`),c.ticket_gate&&d(`${t} is the card's ticket question, which is the owner's; question publish ${s} passes it on`);const l=new Date().toISOString(),u=await g(L(e),{topics:[],items:[]});await b(L(e),{...u,topics:u.topics||[],items:[...u.items||[],{...c,asked_at:l,resolved:`Settled by the orchestrator: ${a}`,resolved_at:l}]});const w=i[s].filter(p=>p.id!==t);return await b($e(e),w.length?{...i,[s]:w}:Kn(i,s)),w.length||await b(Xe(e),Kn(o,s)),await st(e,s,`Answers (orchestrator, ${Pt()})`,`- ${t} (${c.question}): settled by the orchestrator. "${a}"`),L(e)}r(Al,"settleQuestion");async function Dl(e,t){A(e);const n=(await ui(e))[t]?.ids||d(`${t} has no questions waiting on you to screen`);return await ot(e,t),n}r(Dl,"publishScreened");async function Fl(e,t=new Date){const n=Object.entries(await ui(e)).filter(([,a])=>t-Date.parse(a.since)>=js).map(([a])=>a);for(const a of n)await ot(e,a),await U(e,a,"screen-timeout");return n}r(Fl,"publishUnscreened");async function fi(e,t,n,a,i){const{answers:o,requests:s,reopens:c}=await rt(e),l=new Date().toISOString(),u=[],w=[];for(const f of a.filter(m=>m.round_of))f.round=Sn(n.find(m=>m.id===f.round_of))+1,ye(t,[f.round_of],`Followed by prototype round ${f.round}`,l),w.push(f.round_of);for(const f of a.filter(m=>m.reask_of))ye(t,[f.reask_of],`Asked again as ${f.id}`,l),w.push(f.reask_of);for(const f of a){const m=Ae(f.url),$=n.filter(S=>S.topic===f.topic).map(S=>Ee(S,o,s,[],c)),y=f.topic.startsWith("item:")?yi($):[];for(const S of $)y.includes(S.id)?(ye(t,[S.id],`Followed by ${f.id}`,l),w.push(S.id)):!S.resolved&&m&&Ae(S.url)===m&&(ye(t,[S.id],`Replaced by ${f.id}`,l),u.push(S.id))}const p={...t,topics:t.topics||[],items:[...n,...a.map(f=>({...f,asked_at:l}))]};await b(L(e),p);for(const f of new Set(a.map(m=>Qn(m.topic)).filter(Boolean)))await dr(e,f);return{file:L(e),lines:[...i,...u.map(f=>`replaced: ${f}`),...w.map(f=>`resolved: ${f}`)]}}r(fi,"publishQuestions");function pi(e,t,n){return Wt(e,n),e?((!t.topic.startsWith("item:")||t.topic.startsWith("item:ask:"))&&d(`${n} goes on a card's topic, item:<file>/<id>`),t.options?.map(a=>a.id).join()!=="yes,no"&&d(`${n} takes exactly the options yes and no, in that order`),!0):!1}r(pi,"checkGate");async function ar(e,t,n){const a=t.topic.slice(5),{item:i}=await x(e,a);i.ticket&&d(`${n}.ticket_gate: ${a} already has ticket ${i.ticket}`),await an(e)||d(`${n}.ticket_gate needs a board with a ticket source; with none every card stays a task`)}r(ar,"checkTicketGate");async function Rl(e,t,n){const a=await g(L(e),{topics:[],items:[]}),i=new Map((a.items||[]).map(l=>[l.id,l]));for(const l of t){const u=i.get(String(l.id));if(!u?.ticket_gate||!["yes","no"].includes(l.choice))continue;ye(a,[u.id],`Ticket: ${l.choice}.${l.choice==="yes"?" The board makes it from the card's title and notes.":""}`),await b(L(e),a);const w=u.topic.slice(5);l.choice==="yes"&&await mi(e,w,"making its ticket",async()=>{await ue(e,w,{newTicket:!0},n),await U(e,w,"ticket-made",{ticket:(await x(e,w)).item.ticket})})}for(const l of t){const u=i.get(String(l.id));if(!u?.prototype_gate||!["yes","no"].includes(l.choice))continue;ye(a,[u.id],`Prototype round: ${l.choice}. The board moves the card on once every question is answered.`),await b(L(e),a);const w=u.topic.slice(5);l.choice==="yes"&&(await x(e,w).catch(()=>null))?.item.phase==="grilling"&&await ea(e,w)&&await U(e,w,"prototype-after")}const o=new Map;for(const l of t){const u=i.get(String(l.id)),w=Qn(u?.topic);w&&o.set(w,[...o.get(w)||[],ir(u,l)])}const s=[],c=[];for(const[l,u]of o)await x(e,l).catch(()=>null)&&await mi(e,l,"writing them into its notes",async()=>{await st(e,l,`Answers (owner, ${Pt()})`,u.join(`
|
|
16
|
+
`));const w=await wi(e,l);w==="prototyping"&&s.push(l),w==="staging"&&c.push(l)});return{prototype:s,staged:c}}r(Rl,"settleAnswers");async function wi(e,t,{roundAt:n=!1}={}){const{item:a}=await x(e,t),{asked:i}=await de(e,t);if(i.some(o=>o.state==="open"))return null;if(a.phase==="grilling"&&i.some(o=>o.prototype_gate&&o.answers.at(-1)?.choice==="yes")){if(await ue(e,t,{phase:"prototyping"}),n){const{source:o,itemFile:s,item:c}=await x(e,t);c.round_at=new Date().toISOString(),await b(o.file,s)}return await U(e,t,"prototype-round"),"prototyping"}return await cr(e,t)?(await Xn(e,t),"staging"):null}r(wi,"moveOnAnswered");async function mi(e,t,n,a){try{await a()}catch(i){if(!(i instanceof le))throw i;await U(e,t,"answers-failed",{what:n,error:i.message})}}r(mi,"followUp");const Qn=r(e=>typeof e=="string"&&e.startsWith("item:")&&!e.startsWith("item:ask:")?e.slice(5):null,"cardOfTopic"),Vt=r(e=>String(e).padStart(2,"0"),"pad2"),Pt=r((e=new Date)=>`${Vt(e.getMonth()+1)}/${Vt(e.getDate())} ${Vt(e.getHours())}:${Vt(e.getMinutes())}`,"noteStamp");function ir(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(ir,"answerLine");const hi=292,Gn=r(e=>String(e).toLowerCase().replace(/[^a-z0-9-]+/g,"-"),"notesBase"),or=r((e,t,n)=>Ze(h.join(e,`.${Gn(t)}-notes`),n),"notesLock");function Vn(e,t){let n=`${Gn(t)}-notes.md`;for(let a=2;ce(h.join(e,n));a++)n=`${Gn(t)}-notes-${a}.md`;return n}r(Vn,"freeNotesName");async function Zn(e,t){const n=h.join(h.dirname(e),`.${h.basename(e)}.${process.pid}.tmp`);await O.rm(n,{force:!0}),await O.writeFile(n,t,{mode:hi}),await O.rename(n,e)}r(Zn,"writeNotesFile");const sr=r(e=>/\n/.test(_(e,"--heading"))?d("--heading is one line, the section's title"):_(e,"--heading"),"notesHeading"),rr=r((e,t,n)=>`${e.replace(/\n*$/,`
|
|
19
17
|
`)}
|
|
20
18
|
## ${t}
|
|
21
19
|
${n.replace(/^\n+|\s+$/g,"")}
|
|
22
|
-
`,"withSection");async function
|
|
23
|
-
`;return await
|
|
24
|
-
`),"messageWords");async function or(e,t,n){const{item:a}=await x(e,t);a.held&&await Fi(e,"card-hold",{key:t,hold:!1});const{owner:i,asked:o}=await de(e,t),s=o.filter(c=>c.state==="open").map(c=>c.id);s.length&&await Jt(e,i,s,"Skipped: you tapped Implement");const l=ui(n);l&&await nt(e,t,`Implement (owner, ${yt()}), verbatim`,l),await Kn(e,t)}r(or,"stageImplement");async function Pc(e,t,n){const a=ui(n);a&&await nt(e,t,`Reply (owner, ${yt()}), verbatim`,a),await Kn(e,t)}r(Pc,"stageReply");async function Ac(e,t,n){const{source:a,itemFile:i,item:o}=await x(e,t);!["ready","staging"].includes(o.phase)||Ae(o)||(o.phase=n==="prototype"?"prototyping":"grilling",delete o.staged_at,await k(a.file,i))}r(Ac,"planAgain");async function sr(e,t){const n=await x(e,t).catch(()=>null);!n||!["ready","staging"].includes(n.item.phase)||Ae(n.item)||(n.item.phase="grilling",delete n.item.staged_at,await k(n.source.file,n.itemFile),await W(e,t,"new-question"))}r(sr,"grillAgain");function fi(e){return e.filter(t=>t.state==="answered").map(t=>t.id)}r(fi,"answeredOnClaude");function Pe(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(Pe,"setResolved");async function Jt(e,t,n,a){return Pe(t,n,a),await k(L(e),t),L(e)}r(Jt,"markResolved");async function Dc(e,t,n){A(e);const a=await g(L(e),{topics:[],items:[]}),i=(a.items||[]).find(f=>f.id===t);i||d(`no question "${t}" in owner.json`);const{answers:o,requests:s,reopens:l}=await at(e),c=Ne(i,o,s,[],l);if(c.state==="resolved")return L(e);c.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");Oe(i.url)&&rr(a,i,o);const w=await Jt(e,a,[t],u);await hi(e,i.topic);const p=Wn(i.topic);return p&&(await x(e,p).catch(()=>null))?.item.phase==="grilling"&&await si(e,p,{roundAt:!0}),w}r(Dc,"resolveQuestion");async function Fc(e,t,n,a=[]){A(e);const[i,...o]=[...new Set(t)];i||d("question move takes the ids of the questions to merge"),await x(e,n);const s=[n,...a];new Set(s).size!==s.length&&d("question move names a card twice");for(const f of a)await x(e,f);const l=await g(L(e),{topics:[],items:[]}),{answers:c,requests:u,reopens:w}=await at(e);for(const f of[i,...o]){const m=(l.items||[]).find(y=>y.id===f)||d(`no question "${f}" in owner.json`),{state:$}=Ne(m,c,u,[],w);$!=="open"&&d(`${f} is ${$}; question move takes open questions`)}for(const[f,m]of a.entries()){const{item:$}=await x(e,m),y=[...$.deps||[],s[f]].join(",");await he(e,m,{deps:y,relate:n})}const p=await Q(e,n);return Object.assign(l.items.find(f=>f.id===i),{topic:`item:${n}`}),Pe(l,o,`Duplicate of ${p}'s question ${i}`),await k(L(e),l),L(e)}r(Fc,"moveQuestions");async function Rc(e,t){const n=t.filter(({choice:i,note:o})=>!i&&String(o||"").trim()).map(i=>i.id),a=n.length?await g(L(e),{topics:[],items:[]}):null;a?.items?.some(i=>n.includes(i.id))&&(Pe(a,n,"Answered with a note"),await k(L(e),a))}r(Rc,"resolveNoteAnswers");function rr(e,t,n){const a=gn(t),i=e.items.filter(o=>o.topic===t.topic&&gn(o)===a).map(o=>Ne(o,n));So(i,a)||d(`${t.id} is in prototype round ${a}, which no pick has settled yet; a pick settles it`)}r(rr,"roundSettled");async function at(e){const t=await K(h.join(e,"data","events.jsonl"));return{answers:await Promise.all(t.filter(a=>a.kind==="owner-answer").map(async a=>{const i=await Promise.all(["html","json"].map(o=>j.stat(h.join(e,"data","replies",`${a.id}.${o}`)).then(s=>s.isFile(),()=>!1)));return{...a,reply:a.reply||i.some(Boolean)}})),requests:t.filter(a=>a.kind==="prototype-request"),reopens:t.filter(a=>a.kind===yo)}}r(at,"ownerEvents");async function Nc(e,t){const{asked:n}=await lr(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(Nc,"unrecorded");async function lr(e){const t=await g(L(e),{topics:[],items:[]}),{answers:n,requests:a,reopens:i}=await at(e);return{owner:t,asked:(t.items||[]).map(o=>Ne(o,n,a,[],i))}}r(lr,"topicQuestionsAll");async function pi(e,t){const n=await g(L(e),{topics:[],items:[]}),{answers:a,requests:i,reopens:o}=await at(e),s=(n.items||[]).filter(l=>l.topic===t).map(l=>Ne(l,a,i,[],o));return{owner:n,asked:s}}r(pi,"topicQuestions");const de=r((e,t)=>pi(e,`item:${t}`),"cardQuestions"),Kt=r((e,t,n)=>wi(e,`item:${t}`,n),"resolveAnswered");async function wi(e,t,n){if(!e)return[];const{owner:a,asked:i}=await pi(e,t),o=fi(i);return o.length&&await Jt(e,a,o,n),await hi(e,t),o.map(s=>`resolved: ${s}`)}r(wi,"resolveAnsweredOn");const Qn=r(async(e,t)=>!!e&&(await de(e,t)).asked.some(n=>n.state==="open"),"hasOpenQuestion");function mi(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(mi,"setNext");async function hi(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(l){if(l instanceof re)return;throw l}const{source:i,itemFile:o,item:s}=a;s.next_on_questions&&((await de(e,n)).asked.some(l=>l.state!=="resolved")||(delete s.next,delete s.next_at,delete s.next_on_questions,await k(i.file,o)))}r(hi,"clearWaitedNext");async function cr(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 k(n.file,a))}r(cr,"clearNext");async function gi(e,t,n,a){if(!e)return async()=>[];const{owner:i,asked:o}=await de(e,t),s=o.filter(c=>c.state==="open").map(c=>c.id);s.length&&!a&&d(`${n}: ${t} has ${s.length===1?"an open question":"open questions"} ${s.join(", ")} with no answer yet; pass --force to close the card and resolve ${s.length===1?"it":"them"} too`);const l=o.filter(c=>c.state!=="resolved").map(c=>c.id);return async()=>(l.length&&await Jt(e,i,l,"Closed with the card"),l.map(c=>`resolved: ${c}`))}r(gi,"closeQuestions");async function Ec(e,t,n,a,i){await x(e,t),Fn(n),X(a,o=>S(o,"--owner-step")),X(i,o=>Ri(o)),await gi(A(e),t,"land",!1)}r(Ec,"checkClose");async function Gn(e,t){const n=await g(L(e),{items:[]}),{answers:a,requests:i,reopens:o}=await at(e);return bo((n.items||[]).map(s=>Ne(s,a,i,[],o)),t)}r(Gn,"unpickedPrototype");const dr=r(e=>e.length>120?`${e.slice(0,119)}\u2026`:e,"clip"),ur={ask:"text","ask-edited":"text","item-message":"text","owner-answer":"note"};async function $i(e,t,n){const a=S(t,n).replace(/\s+/g," ");return(await K(h.join(e,"data","events.jsonl"))).map(o=>String(o.data?.[ur[o.kind]]??"").replace(/\s+/g," ").toLowerCase()).some(o=>o.includes(a.toLowerCase()))||d(`${n} must quote the owner's words in an ask, card message or answer note on this board`),a}r($i,"ownerQuote");async function Ic(e,t,n){return A(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 $i(e,n,"--asked")}"`}r(Ic,"askedGate");async function it(e,t,n="--from-ask"){return ve(t,n),(await K(h.join(A(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(it,"checkAsk");function Zn(e,t,n=new Date().toISOString(),a=ki(e,t)){if(!He[t])throw new Error(`no lane "${t}"`);const i=e.lane;return bi(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(Zn,"placeIn");function ki(e,t,n=!1){return t==="todo"?Ae(e)?"ready":e.phase??null:t==="progress"?"reading":t==="done"?n||Se(e).some(a=>a.merged_at)?"merged":"closed":null}r(ki,"movedPhase");function bi(e,t,n){if(n!==null&&!wn[t].includes(n))throw new Error(`no ${He[t]} phase "${n}"`);n===null?delete e.phase:e.phase=n}r(bi,"setPhase");async function Lc(e,t,n,a,i,o){const{source:s,itemFile:l,item:c}=await x(e,t);if(o&&((c.lane??null)!==o.lane||(c.phase??null)!==o.phase)||c.lane===n&&(i===void 0||(c.phase??null)===i))return null;if(c.lane===n)return bi(c,n,i),await k(s.file,l),null;const u=new Date().toISOString(),w=Zn(c,n,u,i);if(await k(s.file,l),w===void 0)return null;const p={ts:u,who:"claude",kind:ka,text:`Moved to ${He[n]}: ${S(a,"why")}`};return await me(Y(e,t),p),p.text}r(Lc,"moveCard");const G=r(e=>e?.lane==="done"||!!e?.reopened_at,"closed"),Ae=r(e=>!!e?.lane&&e.lane!=="todo","leftTodo"),Bc=r(async(e,t)=>G((await x(e,t)).item),"isFinished"),yi=["ready","grilling","prototyping"];function xi(e,t){t!==void 0&&d(`--state is now --phase: pass --phase ${yi.includes(t)?t:"ready, grilling or prototyping"}`),e==="staging"&&d("--phase staging is set by the board, on an owner action; pass ready, grilling or prototyping"),e!==void 0&&!yi.includes(e)&&d("--phase takes ready, grilling or prototyping")}r(xi,"checkPhase");const fr="a card goes ready only from staging, which the board sets on the owner's Send, Implement or Reply";async function _i(e,t){return(await K(h.join(A(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(_i,"quickAsk");const pr=/^mark (?:it |this |the card )?ready\b/i;function wr(e,t,n){const a=[].concat(t).map(s=>Sn(s)||d(`--pr takes a PR number or owner/repo#<n>, not "${s}"`)),i=r((s,l)=>s.number===l.number&&(s.repo||n||null)===(l.repo||n||null),"same"),o=Se(e);for(const s of a)o.some(l=>i(l,s))||o.push(s);delete e.pr,e.prs=o}r(wr,"addPRs");const U=r((e,t)=>String(t).includes("/")?String(t):`${e}/${t}`,"depKey");async function ot(e,t,n){const a=(n.deps||[]).map(o=>U(t,o)),i=await Promise.all(a.map(o=>x(e,o).then(({item:s})=>s,()=>null)));return a.filter((o,s)=>i[s]&&!G(i[s]))}r(ot,"unfinishedDeps");async function mr(e,t,n){const a=await Promise.all((await ot(e,N(t).file,n)).map(i=>Q(e,i)));return n.blocked_until?[...a,n.blocked_until]:a}r(mr,"blockerNames");async function Qt(e,t,n){n??=(await x(e,t)).item;const a=await Promise.all((await ot(e,N(t).file,n)).map(i=>Q(e,i)));return[a.length&&`by ${a.join(", ")}`,n.blocked_until&&`until ${n.blocked_until}`].filter(Boolean).join(" and ")}r(Qt,"blockWords");const hr=r((e,t)=>!Le(e)&&!e.pushed&&!e.merge_ready&&!G(e)&&(!!mn(t)||t.some(n=>n?.started&&!n.done&&ze(n))),"inProgress");async function gr(e,t){const n=[];for(const a of await M(e)){const i=await g(a.file,null);for(const o of Array.isArray(i?.items)?i.items:[])G(o)||o.blocked_until||!(o.deps||[]).some(s=>U(a.slug,s)===t)||(await ot(e,a.slug,o)).length||n.push(`${a.slug}/${o.id}`)}return n}r(gr,"unblockedBy");async function Si(e,t,n){const{source:a,itemFile:i,item:o}=await x(e,t);Ae(o)&&(o.unblocked_at=n,await k(a.file,i))}r(Si,"markUnblocked");async function vi(e,t,n,a){const i=new Set(a.match(/#?[\w-]+(?:\.[\w-]+)*(?:\/[\w-]+(?:\.[\w-]+)*)*/g)||[]),o=new Set((n.deps||[]).map(s=>U(N(t).file,s)));for(const s of await M(e)){const l=await g(s.file,null);for(const c of l?.items||[]){const u=`${s.slug}/${c.id}`;if(u===t||o.has(u)||G(c))continue;[c.id,Je(l,c),c.ticket].filter(Boolean).flatMap(p=>[p,`${s.slug}/${p}`]).some(p=>i.has(p))&&d(`--next names unfinished card ${u}; add it with --deps`)}}}r(vi,"checkNextDeps");const ji=r(e=>`item notes ${e} --heading "<heading>" --file <draft.md>`,"NOTES_COMMAND");function $r(e,t){t.size||d(`${e} has no size; a builder starts from it, so pass --size small or large with --phase ready`),t.plan||d(`${e} has no notes file; a builder starts from it, so add its notes with ${ji(e)} before --phase ready`)}r($r,"checkSizeAndNotes");async function kr(e,t,n,a){$r(t,a);const i=await j.readFile(await Gt(n,a.plan),"utf8"),o=(await de(e,t)).asked.filter(s=>s.answers.length&&!i.includes(s.id)&&!(s.prototype_gate&&!s.answers.at(-1).note)).map(s=>s.id);o.length&&d(`${t}'s notes leave out the answered question${o.length===1?"":"s"} ${o.join(", ")}; record each answer in its notes, naming its id, with ${ji(t)} before it is ready`)}r(kr,"checkNotesComplete");async function Gt(e,t,{missingOk:n=!1}={}){const a=r(s=>d(`a notes file is a .md file inside ${e}, named relative to it: "${t}" ${s}`),"bad");(typeof t!="string"||!t.trim())&&a("is empty"),(h.isAbsolute(t)||t.split(/[\\/]/).includes(".."))&&a("leaves the folder"),h.extname(t).toLowerCase()!==".md"&&a("is not a .md file");const i=await j.realpath(e),o=await j.realpath(h.resolve(e,t)).catch(()=>n?h.resolve(i,t):a("does not exist"));return o.startsWith(i+h.sep)||a("leaves the folder"),o}r(Gt,"planPath");async function br(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);if(i.plan)return{itemFile:a,item:i,markdown:await j.readFile(await Gt(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(br,"readNotes");function Oi({install:e,installSha:t,installLabel:n,clearInstall:a}){if(e===void 0)return(t!==void 0||n!==void 0)&&d("--install-sha and --install-label go with --install <url>"),a?null:void 0;a&&d("pass --install or --clear-install, not both");let i;try{i=new URL(String(e).trim())}catch{i=null}(!i||!["https:","itms-services:"].includes(i.protocol))&&d(`--install takes an https:// or itms-services:// URL, not "${e}"`),i.protocol==="itms-services:"&&!(i.searchParams.get("action")==="download-manifest"&&/^https:\/\//.test(i.searchParams.get("url")||""))&&d("--install: an itms-services:// URL needs ?action=download-manifest&url=https://\u2026/manifest.plist");const o=t===void 0?void 0:/^[0-9a-f]{7,40}$/i.test(t)?t.toLowerCase():d(`--install-sha takes a commit sha of 7 to 40 hex digits, not "${t}"`);return{url:String(e).trim(),...n===void 0?{}:{label:S(n,"--install-label")},built_at:new Date().toISOString(),...o?{sha:o}:{}}}r(Oi,"installOf");async function Tc(e,{install:t,installSha:n}){const a=Oi({install:t,installSha:n});a||d("main set needs --install <url>");const i=h.join(A(e),"data","main.json");return await k(i,{install:a}),{file:i,lines:["main install set"]}}r(Tc,"setMainInstall");const yr=r((e,t)=>e.length?`Merged PR${e.length>1?"s":""} ${Ke(e,t)}.`:"Merged.","mergedLine");function xr(e,t,n,a,i,o){return o?{status:o}:n&&a===void 0?{status:["merged",{words:yr(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(xr,"closingLine");function _r(e,t,n){const a=new Date().toISOString(),i=r((o,s)=>o.number===s.number&&(o.repo||n||null)===(s.repo||n||null),"same");e.prs=Se(e).map(o=>{const s=t.find(l=>i(o,l));return s&&!o.merged_at?{...o,merged_at:s.at||a}:o}),delete e.pr}r(_r,"markMerged");async function Pi(e,t,{ticket:n,status:a,clearStatus:i,done:o,merged:s,landed:l,start:c,handBack:u,next:w,clearNext:p,pr:f,pushed:m,mergeReady:$,ghSince:y,fromAsk:_,phase:D,state:P,held:B,unheld:F,pickUpAt:I,deps:v,blockedUntil:E,blockReason:T,parent:q,related:O,relate:z,unrelate:ie,install:ee,installSha:J,installLabel:ue,clearInstall:jt,title:st,size:fe,model:Fe,force:Ot,text:le,closing:Pt,who:on,by:te,command:At,ownerStep:ne,ownerDone:so,check:rt},la){(a!==void 0||i)&&d(`--status${i?" 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>"`),s&&(o=!0),O!==void 0&&(z!==void 0||ie!==void 0)&&d("pass --related, or --relate and --unrelate, not both"),xi(D,P),Ot&&!o&&d("--force goes with --done"),_!==void 0&&o&&d("--from-ask opens a card; close it in another call"),le!==void 0&&Fn(le),ne!==void 0&&S(ne,"--owner-step");const lt=rt===void 0||rt===""?rt:Ri(rt);le!==void 0&&_===void 0&&!o&&D!=="ready"&&d("--text goes with --from-ask, --done, or --phase ready on a staging card"),_!==void 0&&le===void 0&&d(`--from-ask needs --text "<first line>", the item's first thread line after the ask`),w!==void 0&&p&&d("pass --next or --clear-next, not both"),typeof w=="string"&&!w.trim()&&([w,p]=[void 0,!0]);const ct=Di({held:B,unheld:F,pickUpAt:I}),Dt=Oi({install:ee,installSha:J,installLabel:ue,clearInstall:jt});[o,c,u,w,p,f,m,$,y,_,D,ct,v,E,T,q,O,z,ie,Dt,st,fe,Fe,n,ne,rt].every(R=>R===void 0||R===!1)&&d("nothing to set: pass --done, --next, --clear-next, --pr, --pushed, --gh-since, --from-ask, --phase, --held, --unheld, --pick-up-at, --deps, --blocked-until, --block-reason, --parent, --related, --relate, --unrelate, --install, --clear-install, --title, --size, --model, --ticket or --check");const Z=_===void 0?void 0:await it(e,_);Z&&await Mt(e,Z);const ro=Z!==void 0&&await _i(e,Z),{source:Re,itemFile:Ue,item:b}=await x(e,t);D!==void 0&&Ae(b)&&d(`--phase sets a Todo card's phase; ${t} is ${He[b.lane]}, where the board sets it`),D==="ready"&&!ro&&!["staging","ready"].includes(b.phase)&&d(`${t} reads ${b.phase||"new"}, not staging; ${fr}`),Z&&b.from_ask&&b.from_ask!==Z&&d(`${t} came from ask "${b.from_ask}", so ask "${Z}" can't be its from-ask; to fold it in: item relate-ask ${Z} ${t} --summary "<line>"`),Z&&ct&&D!==void 0&&!b.phase&&d("a card filed held is filed New and takes no --phase; drop --phase, or drop --held to file it in a phase"),Z&&D===void 0&&!b.phase&&!ct&&d("--from-ask files a card, which reads New until it has a phase: pass --phase grilling, prototyping, or ready for a Quick build ask, or --held to file it for later");const ca=G(b);ne!==void 0&&ca&&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`),ne!==void 0&&(b.owner_step=S(ne,"--owner-step")),so&&delete b.owner_step,lt!==void 0&&!b.owner_step&&d(`--check goes with an owner step, which ${t} doesn't have: pass --owner-step "<the step>" with it on builder done or land`),lt&&(b.owner_check={check:lt,since:b.owner_check?.check===lt?b.owner_check.since:new Date().toISOString()}),(lt===""||!b.owner_step)&&delete b.owner_check;const sn=await Promise.all((await H(Re.dir,b.id)).map(R=>g(R,null))),lo=hn(b,sn);if(st!==void 0&&(b.title=S(st,"--title")),Ai(b,{size:fe,model:Fe}),v!==void 0){const R=await Ni(e,N(t).file,v);R.length?b.deps=R:delete b.deps}const da=E===""?b.blocked_until:void 0;if(E!==void 0&&(E===""?delete b.blocked_until:b.blocked_until=S(E,"--blocked-until")),T&&(!b.deps?.length&&!b.blocked_until&&d("--block-reason goes with a block: --deps or --blocked-until"),b.block_reason=S(T,"--block-reason")),(T===""||!b.deps?.length&&!b.blocked_until)&&delete b.block_reason,q!==void 0){const R=await Ei(e,N(t).file,t,q);R?b.parent=R:delete b.parent}const ua=q!==void 0||O!==void 0||z!==void 0||ie!==void 0?await Vn(e,t):null;let rn=O===void 0?null:await jr(e,t,b,O);z!==void 0&&(b.related=[...new Set([...(b.related||[]).map(R=>U(N(t).file,R)),...await Vt(e,t,z,"--relate")])]),ie!==void 0&&(rn=await Or(e,t,b,await Vt(e,t,ie,"--unrelate"))),ct!==void 0&&Zt(b,ct),c&&b.held&&d(b.pick_up_at?`${t} is held until ${b.pick_up_at}, when the board resumes it; start it early only with item set ${t} --unheld`:`${t} is held; start it only once the owner resumes it, with item set ${t} --unheld`);const dt=f!==void 0||m!==void 0||$!==void 0;if((c||o||u||dt)&&vr(b),c){const R=await ot(e,N(t).file,b);R.length&&d(`${t} is blocked by ${R.join(", ")}; start it once ${R.length===1?"that is":"those are"} done`),b.blocked_until&&d(`${t} is blocked until ${b.blocked_until}; start it once that holds and the block is cleared with --blocked-until ""`)}const Ft=c&&e?await Gn(e,t):null;Ft&&d(`${t} has no take picked: the owner's answer to ${Ft.id} picks none${Ft.note?` ("${dr(Ft.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 co=o?await gi(e,t,"--done",Ot):async()=>[],ut=o?xr(e,t,s,le,oe(e,Ue),Pt):null;if(s&&_r(b,s,oe(e,Ue)),w!==void 0&&(mi(b,w,await Qn(e,t)),await vi(e,t,b,b.next)),p&&(delete b.next,delete b.next_at,delete b.next_on_questions),c&&w===void 0&&b.next_on_questions&&(delete b.next,delete b.next_at,delete b.next_on_questions),dt&&!b.review_since&&(b.review_since=new Date().toISOString()),f!==void 0&&wr(b,f,oe(e,Ue)),m!==void 0){we().find(R=>_e(R.dir,A(e)))?.remote||d("--pushed is for a board whose repo is a plain git remote; on GitHub, open a PR and pass --pr"),Le(b)&&d(`${t} has a PR; its PR says when it merges`);try{zo(m)}catch{d("--pushed takes the name of the branch you pushed")}b.pushed={branch:m,at:new Date().toISOString()},b.branch=m}if($!==void 0&&(b.merge_ready={branch:$,at:new Date().toISOString()},b.branch=$),o&&delete b.merge_ready,y!==void 0&&((typeof y!="string"||!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(y)||Number.isNaN(Date.parse(y)))&&d("--gh-since takes an ISO-8601 UTC time such as 2026-10-23T11:00:00Z"),b.gh_since=y),Z&&(b.from_ask=Z),D==="ready"&&e){const R=(await de(e,t)).asked.filter(xe=>xe.state==="open").map(xe=>xe.id);R.length&&d(`${t} has ${R.length===1?"an open question":"open questions"} ${R.join(", ")}; it is ready once the owner has answered ${R.length===1?"it":"them"}`),await kr(e,t,Re.dir,b)}const ln=D==="ready"&&b.phase==="staging"&&_===void 0;ln&&le===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`),D==="ready"&&!ln&&le!==void 0&&_===void 0&&!o&&d(`${t} reads ${b.phase||"no phase"}, not staging, so there is no judgment for --text to record`);const uo=D==="ready"&&b.phase!=="ready";D!==void 0&&(D!=="ready"&&["staging","ready"].includes(b.phase)&&(b.round_at=new Date().toISOString()),b.phase=D,delete b.staged_at,delete b.unfiled,delete b.filed_held),b.phase==="ready"&&pr.test(b.next??"")&&d(`${t} would read ready with Next "${b.next}", which asks for the phase it is already in; pass --next with what happens next, or --clear-next`),Dt&&(b.install=Dt),Dt===null&&delete b.install,!lo&&!c&&hn(b,sn)&&d(`that leaves ${t} In Progress with nothing building and no Next; pass --next "<what happens next>"`);const cn=(v!==void 0||E)&&!o&&hr(b,sn)?await Qt(e,t,b):"",fa=oe(e,Ue);let dn=null;const un=r((R,xe)=>{b.lane!==R&&Zn(b,R,void 0,ki(b,R,l))!==void 0&&xe&&(dn=`Moved to ${He[R]}: ${xe}`)},"moveTo");if(o?un("done",null):dt&&b.lane!=="done"?un("review",f!==void 0?`PR ${Ke([].concat(f).map(Sn),fa)} opened`:m!==void 0?`${m} pushed for its merge`:"verified, it waits for your Merge"):c&&!G(b)&&un("progress","a build started"),(n!==void 0||Z&&!b.ticket&&!b.linear?.length)&&await Yn(e,t,Ue,b,n,la,Re.dir),await k(Re.file,Ue),dn&&await me(Y(e,t),{ts:b.lane_at,who:"claude",kind:ka,text:dn}),ln&&await nt(e,t,`Ready (orchestrator, ${yt()})`,le),uo&&await Ce(e,t,{command:"readied"}),cn){const R=e&&!await ao(e,t)&&await no(e,t,{cause:"blocked",words:`blocked ${cn}`});await W(e,t,R?"blocked-pausing":"blocked-stays",{blocked:cn})}rn&&await rn(),ua&&await Li(e,t,ua,await Vn(e,t));const ye=await co();if(D==="ready"&&e&&ye.push(...await Kt(e,t,"Recorded in the card's notes")),c&&ye.push(...await Kt(e,t,"Acted on: build started")),Z&&ye.push(...(await ht(e,t,{text:le})).lines),ut&&e){const R=At?await Ce(e,t,{command:At,text:ut.text,who:on,by:te}):ut.status?await W(e,t,...ut.status):await ht(e,t,{text:ut.text});ye.push(...R.lines)}const pa=b.owner_step&&o?null:To({start:c,done:o,handover:dt}),wa=pa&&await Co(await Bi(e),b,pa,{...la,repo:fa});wa&&ye.push(wa),(dt||o||u)&&(await oo(Re.dir,b.id,o,u?{handedBack:new Date().toISOString()}:{endHandBack:!0}),await tt(e,t));const fn=Z?await je(e):{};if(V(fn,Z)!==void 0&&(delete fn[Z],await k(ke(e),{links:fn})),da&&!G(b)&&!(await ot(e,N(t).file,b)).length){const{line:R}=await W(e,t,"unblocked",{lifted:da});await Si(e,t,R.ts),await ge({dir:e},"card-unblocked",{key:t}),ye.unshift(`unblocked: ${t}`)}if(!o||ca)return{file:Re.file,lines:ye};const ma=await gr(e,t),fo=await Q(e,t);for(const R of ma){const{line:xe}=await W(e,R,"unblocked-done",{card:fo});await Si(e,R,xe.ts),await ge({dir:e},"card-unblocked",{key:R})}return{file:Re.file,lines:[...ma.map(R=>`unblocked: ${R}`),...ye]}}r(Pi,"changeItem");function Ai(e,{size:t,model:n}){for(const[a,i,o]of[["size",t,["small","large"]],["model",n,["opus","sonnet"]]])i!==void 0&&(i===""?delete e[a]:e[a]=o.includes(i)?i:d(`--${a} takes ${o.join(" or ")}, or "" to clear it`))}r(Ai,"setBuildChoice");function Di({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:Sr(n)}:e?{until:null}:t?null:void 0}r(Di,"holdOf");function Sr(e){const t=typeof e=="string"&&Lo.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(Sr,"pickUpTime");function vr(e){delete e.held,delete e.pick_up_at}r(vr,"clearHold");function Zt(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(Zt,"setHold");async function Cc(e,t,n,{announce:a=!0}={}){const{source:i,itemFile:o,item:s}=await x(e,t);if(a){const l=new Date(n).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});await W(e,t,"hold-ended",{at:l}),!s.blocked_until&&!(await ot(e,N(t).file,s)).length&&await ge({dir:e},"card-unblocked",{key:t,until:n})}Zt(s,null),await k(i.file,o)}r(Cc,"pickUp");async function Fi(e,t,n){const a=String(n?.key||""),{file:i,id:o}=N(a),{source:s,itemFile:l}=await Be(e,i),c=n.shared_by?n.shared_by.name||n.shared_by.login:void 0,u={text:Nt.done,who:"love",by:c,command:"done"},w=l.items.find($=>$.id===o)||d(`item file "${i}" has no item "${o}"`);if(!(t==="card-done"&&!!w.owner_step)&&w.lane==="done")return null;if(t==="card-done")return he(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;Zt(w,n.hold?{until:f}:null),await k(s.file,l),n.hold&&(await ao(e,a)||await no(e,a,{cause:"held",words:"held"}),await oo(s.dir,o,!1,{held:!0}),await tt(e,a));const m=f&&new Date(f).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});return Ce(e,a,n.hold?{text:m?`${Nt.hold} until ${m}`:Nt.hold,who:"love",by:c,command:"hold"}:{who:"love",by:c,command:"resume"})}r(Fi,"recordOwnerTap");function Ri(e){try{return cs(ls(e))}catch(t){return d(t.message)}}r(Ri,"checkSpec");async function qc(e,t,n,a){const{item:i}=await x(e,t);return!i.owner_step||i.owner_check?.check!==n?null:he(e,t,{done:!0,force:!0,ownerDone:!0,closing:["check-passed",{found:a}]})}r(qc,"passOwnerCheck");async function Mc(e,t,n,a){const{source:i,itemFile:o,item:s}=await x(e,t);return!s.owner_step||s.owner_check?.check!==n?!1:(s.owner_check.broken=a,await k(i.file,o),await W(e,t,"check-broken",{error:a}),await ge({dir:e},"owner-check-broken",{key:t,check:n,error:a}),!0)}r(Mc,"breakOwnerCheck");async function Ni(e,t,n){const a=[...new Set(String(n).split(",").map(i=>i.trim()).filter(Boolean))];for(const i of a)await x(e,U(t,i));return a}r(Ni,"depList");async function Ei(e,t,n,a){if(!String(a).trim())return null;const i=U(t,String(a).trim());i===n&&d("--parent: a card can't be its own Parent");for(let o=i;o;){const s=o===i?await x(e,o):await x(e,o).catch(c=>{if(c instanceof re)return null;throw c});if(!s)break;const{item:l}=s;l.parent&&U(N(o).file,l.parent)===n&&d(`--parent: ${i} is a child of ${n}, so it can't be its Parent`),o=l.parent?U(N(o).file,l.parent):null}return i}r(Ei,"parentKey");async function Vt(e,t,n,a){const i=t.split("/")[0],o=[...new Set(String(n).split(",").map(s=>s.trim()).filter(Boolean).map(s=>U(i,s)))];o.includes(t)&&d(`${a}: a card can't be related to itself`);for(const s of o)await x(e,s);return o}r(Vt,"relatedKeys");const Ii=r((e,t,n)=>async()=>{for(const a of await M(e)){const i=await g(a.file,null);let o=!1;for(const s of Array.isArray(i?.items)?i.items:[]){const l=`${a.slug}/${s.id}`;l===t||!n(l)||!(s.related||[]).some(c=>U(a.slug,c)===t)||(s.related=s.related.filter(c=>U(a.slug,c)!==t),s.related.length||delete s.related,o=!0)}o&&await k(a.file,i)}},"dropLinksTo");async function Vn(e,t){const n=new Set;let a=null;for(const i of await M(e))for(const o of(await g(i.file,null))?.items||[]){const s=`${i.slug}/${o.id}`,l=(o.related||[]).map(c=>U(i.slug,c));s===t?(l.forEach(c=>n.add(c)),o.parent&&(a=U(i.slug,o.parent))):l.includes(t)&&n.add(s)}return{related:n,parent:a}}r(Vn,"linksOf");async function Li(e,t,n,a){const i=r(async(o,s,l=o,c=o)=>{await Ce(e,t,{command:l,card:s}),await Ce(e,s,{command:c,card:t})},"both");for(const o of a.related)n.related.has(o)||await i("related",o);for(const o of n.related)a.related.has(o)||await i("unrelated",o);a.parent&&a.parent!==n.parent&&await i("followup-of",a.parent,"followup-of","followup")}r(Li,"postLinkLines");async function jr(e,t,n,a){const i=await Vt(e,t,a,"--related");return i.length?n.related=i:delete n.related,Ii(e,t,o=>!i.includes(o))}r(jr,"setRelated");function Or(e,t,n,a){const i=(n.related||[]).filter(o=>!a.includes(U(N(t).file,o)));return i.length?n.related=i:delete n.related,Ii(e,t,o=>a.includes(o))}r(Or,"unrelateCards");const oe=r((e,t)=>e&&we().find(n=>_e(n.dir,e))?.github||t.repo,"boardRepoOf");async function Wc(e,t){const{itemFile:n,item:a}=await x(e,t);Le(a)||d(`${t} has no PR`);const i=Ie(a,oe(e,n));i.length||d(`${t} has no repo`);const o=a.gh_since||"1970-01-01T00:00:00Z";try{const s=be(await ae(e)).github_login||await Xo();return await Promise.all(i.map(async({repo:l,number:c})=>({repo:l,...ja(await va({repo:l,pr:c,since:o}),{pr:c,since:o,me:s})})))}catch(s){d(s.message)}}r(Wc,"prReport");async function Uc(e,t,{replyTo:n,body:a}){const i=n===void 0?"pr comment":"pr reply",o=Sn(t)||d(`${i} takes a PR number or owner/repo#<n>`);n!==void 0&&!/^[1-9]\d*$/.test(n)&&d("pr reply takes the id of the review comment it answers");const s=S(a,"--body").trim();(s.startsWith(wt.trim())||s.includes(_n))&&d(`${i} adds the ${wt.trim()} prefix and the claude-bridge marker itself; pass only your words`);const l=o.repo||oe(A(e),{})||d(`${i} needs a board on GitHub; this board has no repo there`);try{const c=await es({repo:l,pr:o.number,replyTo:n,words:s,login:be(await ae(e)).github_login}),u=await Ka(e,mt({repo:l,number:o.number}),"comment",`Comment on #${o.number}: ${s}`);return{lines:[`posted on PR ${o.repo?t:`#${t}`}: ${c}`,...u.length?[`queued for ${u.join(", ")}'s Slack thread`]:[]]}}catch(c){d(c.message)}}r(Uc,"postPR");const Bi=r(async e=>Xa((await ae(e)).tickets),"boardSource");async function Hc(e,t,{afterApproval:n,confirmed:a,tapped:i}={}){A(e);const{itemFile:o,item:s}=await x(e,t);Le(s)||d(`${t} has no PR`),G(s)&&d(`${t} is done`);const l=await Qt(e,t,s);l&&(i&&await W(e,t,"merge-blocked",{blocked:l}),d(`Not merged: blocked ${l}.`));const c=oe(e,o),u=Ie(s,c);u.length||d(`${t} has no repo`);const w=new Date().toISOString();let p;try{p=await Promise.all(u.map(v=>va({repo:v.repo,pr:v.number,since:w})))}catch(v){d(v.message)}const f=p.map(({view:v})=>v);if(as(f)){const v=u.map((E,T)=>({...E,at:f[T].mergedAt})).filter((E,T)=>f[T].state==="MERGED");return he(e,t,{merged:v,closing:["already-merged",{prs:Ke(v,c),many:v.length>1}]})}const m=u.map((v,E)=>({...v,...p[E]})).filter(({view:v})=>v.state==="OPEN"),$=jn(o,(await ae(e)).reviewers||[]).length>0,y=[];let _=!1,D=!1;for(const v of m){const{view:E,reviews:T}=v,{failing:q,pending:O}=ja(v,{pr:v.number,since:w,me:null}),z=ko({...E,checks:{state:q.length?"fail":O?"pending":"pass",failing:q,pending:O}},{needsApproval:$}),ie=T.filter(J=>J.state==="APPROVED").sort((J,ue)=>String(J.submitted_at).localeCompare(String(ue.submitted_at))).at(-1)?.commit_id;let ee=!!(ie&&E.headRefOid&&ie!==E.headRefOid);ee&&n===void 0&&qr(s,ie,E.headRefOid)&&(ee=!1,D=!0),_||=ee,ee&&n===void 0&&z.push('commits after the approval: pass --after-approval "<why they are trivial>" when they are, else ask the approver to look again'),y.push(...u.length>1?z.map(J=>`${Ke([v],c)} ${J}`):z)}m.length||y.push(u.length>1?"every PR is closed":"PR is closed"),n!==void 0&&!_&&d("--after-approval: no commit came after the approval; drop it");const P=s.manual_steps?.length?a===void 0?null:await $i(e,a,"--confirmed"):void 0;if(a!==void 0&&P===void 0&&d(`--confirmed: ${t} has no manual steps; drop it`),P===null&&y.push(`manual steps: merge only on the owner's word, with --confirmed "<their words>"`),y.length){const v=`Not merged: ${y.join("; ")}.`;i&&await W(e,t,"merge-refused",{why:y.join("; ")}),d(v)}const B=[];for(const v of m){const E=await new Promise(T=>{ha("gh",["pr","merge",String(v.number),"--repo",v.repo,"--squash"],{encoding:"utf8",timeout:12e4},(q,O,z)=>T(q?String(z||q.message).trim().split(`
|
|
25
|
-
`)[0]:null))});E&&d(`gh pr merge ${Ke([v],c)} failed: ${E}${B.length?` (merged ${Ke(B,c)} before it)`:""}`),B.push(v)}const F=[n!==void 0&&`After the approval: ${S(n,"--after-approval")}`,D&&`After the approval: only the board's rebase onto ${s.rebased.at(-1).base}.`,P&&`The owner's word on the manual steps: "${P}"`].filter(Boolean),I=await he(e,t,{merged:B});return F.length&&await ht(e,t,{text:F.join(" ")}),I}r(Hc,"mergeCard");async function zc(e,t){return(await x(e,t)).item.merge_ready||null}r(zc,"heldMerge");async function Jc(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 o=await Qt(e,t,i);return o&&d(`${t} is blocked ${o}, so its Merge waits until nothing blocks it`),i.merge_ready.tapped=new Date().toISOString(),await k(n.file,a),i.merge_ready}r(Jc,"tapMerge");async function Kc(e,t){const{source:n,item:a}=await x(e,t);return(await Promise.all((await H(n.dir,a.id)).map(o=>g(o,null)))).filter(o=>o?.worktree?.path).sort((o,s)=>String(s.started).localeCompare(String(o.started)))[0]?.model}r(Kc,"builderModel");async function Yn(e,t,n,a,i,o,s,l){try{const c=await ae(e),u=yn(c.tickets||{source:"internal"}),w=r((m,$)=>m.internal===!0?String($.id).replace(/^S(\d+)$/,`${u.prefix}-$1`):null,"ownId");i===void 0&&u.source==="internal"&&xn(u,w(n,a))&&(i=w(n,a));let p=0;if(u.source==="internal")for(const m of await M(e)){const $=await g(m.file,{});for(const y of $.items||[]){const _=[y.ticket,w($,y)];if(p=Math.max(p,..._.map(D=>xn(u,D)||0)),i!==void 0&&`${m.slug}/${y.id}`!==t&&_.includes(i))throw Object.assign(new re(`${i} is already the ticket of ${m.slug}/${y.id}`),{held:!0})}}l??=i===void 0&&u.source!=="internal"&&a.plan?await j.readFile(await Gt(s,a.plan),"utf8"):a.notes||"";const f=await Bo(u,{ticket:i,title:a.title,body:l,repo:oe(e,n),next:p+1},o);a.ticket=f.ticket,f.created&&u.source!=="internal"?a.ticket_created=!0:delete a.ticket_created}catch(c){if(c instanceof re)throw c;d(c.message)}}r(Yn,"assignTicket");const De=r((e,t)=>pt(h.join(e||Bt(),"tickets"),t,{heartbeat:!0}),"ticketWrite"),he=r((e,t,n,a)=>n.ticket!==void 0||n.fromAsk!==void 0?De(e,()=>Pi(e,t,n,a)):Pi(e,t,n,a),"setItem"),Qc=r((e,t,n,a)=>De(e,()=>Pr(e,t,n,a)),"addItem");async function Pr(e,t,{title:n,notes:a,notesFrom:i,deps:o,ticket:s,pickUpAt:l,held:c,parent:u,size:w,model:p,phase:f,state:m,relate:$,fromAsk:y,text:_,intake:D=!1},P){const{file:B,id:F}=t.includes("/")?N(t):{file:t};F!==void 0&&ve(F,"the item id"),xi(f,m),_!==void 0&&S(_,"--text"),_!==void 0&&y===void 0&&d("--text goes with --from-ask"),y!==void 0&&_===void 0&&d(`--from-ask needs --text "<first line>", the item's first thread line after the ask`);const I=y===void 0?void 0:await it(e,y);I&&await Mt(e,I);const v=Di({held:c,pickUpAt:l});v&&f!==void 0&&d("a card filed held is filed New and takes no --phase; drop --phase, or drop --held to file it in a phase"),I&&f===void 0&&!v&&d("--from-ask files a card, which reads New until it has a phase: pass --phase grilling, prototyping, or ready for a Quick build ask, or --held to file it for later"),f==="ready"&&!(I&&await _i(e,I))&&d(I?`ask "${I}" is not a Quick build ask, so item add files it grilling or prototyping`:"item add files a card grilling or prototyping; ready is for a Quick build ask, filed with --from-ask"),f==="ready"&&!w&&d("a new ready card has no size; a builder starts from it, so pass --size small or large with --phase ready"),f==="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"),D&&e&&f===void 0&&!v&&d("item add files a card on a board: pass --phase grilling or prototyping, or --held to file it for later");const{source:E,itemFile:T}=await Be(e,B);F!==void 0&&T.items.some(J=>J.id===F)&&d(`item file "${B}" already has an item "${F}"`);const q=new Date().toISOString(),O={id:F,title:S(n,"--title"),created:q,lane:"todo",lane_at:q};u!==void 0&&(O.parent=await Ei(e,B,null,u)||d("--parent takes a card, such as <file>/<id>")),a!==void 0&&i!==void 0&&d("pass --notes or --notes-from, not both");const z=i!==void 0?await di(e,i,"--notes-from",()=>`${Jn}, and link that card with --relate`):a===void 0?"":S(a,"--notes");i!==void 0&&!z.trim()&&d(`--notes-from ${i} is empty`);const ie=i!==void 0?z:`# ${O.title}
|
|
26
|
-
${z
|
|
27
|
-
${
|
|
28
|
-
|
|
29
|
-
`
|
|
30
|
-
`)
|
|
20
|
+
`,"withSection");async function st(e,t,n,a){const{source:i,item:{id:o}}=await x(e,t);return or(i.dir,o,async()=>{const{source:s,itemFile:c,item:l}=await x(e,t);let u;l.plan?(u=await en(s.dir,l.plan,{missingOk:!0}),ce(u)||await U(e,t,"notes-missing")):(l.plan=Vn(s.dir,l.id),u=h.join(s.dir,l.plan),await b(s.file,c));const w=ce(u)?await O.readFile(u,"utf8"):`# ${l.title}
|
|
21
|
+
`;return await Zn(u,rr(w,sr(n),a)),l.plan})}r(st,"appendNotes");async function gi(e){const{tickets:t}=be(await ie(e)),n=[],a=new Map;for(const i of await M(e)){const o=await g(i.file,null);a.set(i.slug,{source:i,itemFile:o||{}});for(const s of o?.items||[])s.plan&&n.push({key:`${i.slug}/${s.id}`,ref:Ve(o,s,t),title:s.title,created:s.created||"",archived:!1,dir:i.dir,plan:s.plan})}for(const i of e?await Fe(e):[]){if(!i.item?.plan)continue;const o=a.get(i.file);n.push({key:i.key,ref:Ve(o?.itemFile||{},i.item,t),title:i.item.title,created:i.item.created||"",archived:!0,dir:o?.source.dir||h.join(e,"data"),plan:i.item.plan})}return n}r(gi,"cardsWithNotes");const $i=r(e=>O.realpath(h.resolve(e.dir,e.plan)).catch(()=>null),"realNotes");async function ki(e,t,n,a){if(t==="-")return _o(0,"utf8");const i=await O.realpath(t).catch(()=>d(`${n} ${t}: no such file`));for(const o of await gi(e))await $i(o)===i&&d(`${n} ${t} is the notes file of ${o.ref} "${o.title}" (${o.key}), and a card's notes are its own; ${a(o)}`);return O.readFile(i,"utf8")}r(ki,"refuseCardNotes");const Yn="write a draft of your own outside the board's data folder";async function Nl(e,t,{heading:n,file:a,from:i}){let o;if(i!==void 0){i===t&&d(`${t} can't fold its own notes into itself; pass --from <the other card>`);const[l,u]=await Promise.all([x(e,t),x(e,i)]),[w,p]=await Promise.all([l,u].map(({source:f,item:m})=>m.plan?O.realpath(h.resolve(f.dir,m.plan)).catch(()=>null):null));w&&w===p&&d(`${i} and ${t} name one notes file, ${l.item.plan}, from before the board named each card's own; the board splits it as it starts`),o=(await jr(e,i)).markdown,n??=`Folded from ${await J(e,i)} "${u.item.title}"`}else o=await ki(e,a,"--file",l=>l.key===t?`${Yn}, holding only the new section`:`${Yn}, or fold that card's notes in with item notes ${t} --from ${l.key}`);o.trim()||d(`${i??a} is empty`);const s=await st(e,t,n,o),c=await Yt(e,t,"Recorded in the card's notes");return{file:s,heading:n,lines:c}}r(Nl,"addNotes");const El=r(e=>He(e,async()=>{const t=await gi(e),n=new Map;for(const i of t){const o=await $i(i);o&&n.set(o,[...n.get(o)||[],i])}const a=[];for(const[i,o]of n){const[s,...c]=o.sort((l,u)=>l.created<u.created?-1:l.created>u.created?1:l.key<u.key?-1:1);for(const l of c.filter(u=>!u.archived)){const{source:u,itemFile:w,item:p}=await Ce(e,P(l.key).file).then(m=>({...m,item:m.itemFile.items.find($=>$.id===P(l.key).id)})),f=p.plan;p.plan=Vn(u.dir,p.id),await Zn(h.join(u.dir,p.plan),await O.readFile(i,"utf8")),await b(u.file,w),await U(e,l.key,"notes-shared",{card:s.ref}),a.push(`${l.key}: its own notes, ${p.plan}, copied from ${h.normalize(f)}, which ${s.key} keeps`)}await O.chmod(i,hi)}return a}),"splitSharedNotes");async function cr(e,t){const{source:n,item:a}=await x(e,t);return De(a)||Be(a)||a.pushed||(await Promise.all((await z(n.dir,a.id)).map(o=>g(o,null)))).some(o=>o?.started&&!o.done&&o.kind!==pe)||(await de(e,t)).asked.some(o=>o.state==="open")||(await g($e(e),{}))[t]?!1:!await ta(e,t)}r(cr,"stageable");async function Xn(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 Me(e,t,{command:"staged"}))}r(Xn,"stage");const bi=r(({text:e,files:t})=>[String(e||"").trim(),...(Array.isArray(t)?t:[]).map(n=>`Attached: ${n?.path||n?.name||n}`)].filter(Boolean).join(`
|
|
22
|
+
`),"messageWords");async function lr(e,t,n){const{item:a}=await x(e,t);a.held&&await qi(e,"card-hold",{key:t,hold:!1});const{owner:i,asked:o}=await de(e,t),s=o.filter(l=>l.state==="open").map(l=>l.id);s.length&&await Zt(e,i,s,"Skipped: you tapped Implement");const c=bi(n);c&&await st(e,t,`Implement (owner, ${Pt()}), verbatim`,c),await Xn(e,t)}r(lr,"stageImplement");async function Ll(e,t,n){const a=bi(n);a&&await st(e,t,`Reply (owner, ${Pt()}), verbatim`,a),await Xn(e,t)}r(Ll,"stageReply");async function Il(e,t,n){const{source:a,itemFile:i,item:o}=await x(e,t);!["ready","staging"].includes(o.phase)||De(o)||(o.phase=n==="prototype"?"prototyping":"grilling",delete o.staged_at,await b(a.file,i))}r(Il,"planAgain");async function dr(e,t){const n=await x(e,t).catch(()=>null);!n||!["ready","staging"].includes(n.item.phase)||De(n.item)||(n.item.phase="grilling",delete n.item.staged_at,await b(n.source.file,n.itemFile),await U(e,t,"new-question"))}r(dr,"grillAgain");function yi(e){return e.filter(t=>t.state==="answered").map(t=>t.id)}r(yi,"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 Zt(e,t,n,a){return ye(t,n,a),await b(L(e),t),L(e)}r(Zt,"markResolved");async function Bl(e,t,n){A(e);const a=await g(L(e),{topics:[],items:[]}),i=(a.items||[]).find(f=>f.id===t);i||d(`no question "${t}" in owner.json`);const{answers:o,requests:s,reopens:c}=await rt(e),l=Ee(i,o,s,[],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 u=_(n,"--note");Ae(i.url)&&ur(a,i,o);const w=await Zt(e,a,[t],u);await vi(e,i.topic);const p=Qn(i.topic);return p&&(await x(e,p).catch(()=>null))?.item.phase==="grilling"&&await wi(e,p,{roundAt:!0}),w}r(Bl,"resolveQuestion");async function Tl(e,t,n,a=[]){A(e);const[i,...o]=[...new Set(t)];i||d("question move takes the ids of the questions to merge"),await x(e,n);const s=[n,...a];new Set(s).size!==s.length&&d("question move names a card twice");for(const f of a)await x(e,f);const c=await g(L(e),{topics:[],items:[]}),{answers:l,requests:u,reopens:w}=await rt(e);for(const f of[i,...o]){const m=(c.items||[]).find(y=>y.id===f)||d(`no question "${f}" in owner.json`),{state:$}=Ee(m,l,u,[],w);$!=="open"&&d(`${f} is ${$}; question move takes open questions`)}for(const[f,m]of a.entries()){const{item:$}=await x(e,m),y=[...$.deps||[],s[f]].join(",");await ue(e,m,{deps:y,relate:n})}const p=await J(e,n);return Object.assign(c.items.find(f=>f.id===i),{topic:`item:${n}`}),ye(c,o,`Duplicate of ${p}'s question ${i}`),await b(L(e),c),L(e)}r(Tl,"moveQuestions");async function Cl(e,t){const n=t.filter(({choice:i,note:o})=>!i&&String(o||"").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 b(L(e),a))}r(Cl,"resolveNoteAnswers");function ur(e,t,n){const a=Sn(t),i=e.items.filter(o=>o.topic===t.topic&&Sn(o)===a).map(o=>Ee(o,n));Ro(i,a)||d(`${t.id} is in prototype round ${a}, which no pick has settled yet; a pick settles it`)}r(ur,"roundSettled");async function rt(e){const t=await Q(h.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 Nn(h.join(e,"data","replies"),a.id)).length>0}))),requests:t.filter(a=>a.kind==="prototype-request"),reopens:t.filter(a=>a.kind===Ao)}}r(rt,"ownerEvents");async function ql(e,t){const{asked:n}=await fr(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(ql,"unrecorded");async function fr(e){const t=await g(L(e),{topics:[],items:[]}),{answers:n,requests:a,reopens:i}=await rt(e);return{owner:t,asked:(t.items||[]).map(o=>Ee(o,n,a,[],i))}}r(fr,"topicQuestionsAll");async function xi(e,t){const n=await g(L(e),{topics:[],items:[]}),{answers:a,requests:i,reopens:o}=await rt(e),s=(n.items||[]).filter(c=>c.topic===t).map(c=>Ee(c,a,i,[],o));return{owner:n,asked:s}}r(xi,"topicQuestions");const de=r((e,t)=>xi(e,`item:${t}`),"cardQuestions"),Yt=r((e,t,n)=>_i(e,`item:${t}`,n),"resolveAnswered");async function _i(e,t,n){if(!e)return[];const{owner:a,asked:i}=await xi(e,t),o=yi(i);return o.length&&await Zt(e,a,o,n),await vi(e,t),o.map(s=>`resolved: ${s}`)}r(_i,"resolveAnsweredOn");const ea=r(async(e,t)=>!!e&&(await de(e,t)).asked.some(n=>n.state==="open"),"hasOpenQuestion");function Si(e,t,n){e.next=_(t,"--next"),e.next_at=new Date().toISOString(),n?e.next_on_questions=!0:delete e.next_on_questions}r(Si,"setNext");async function vi(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 le)return;throw c}const{source:i,itemFile:o,item:s}=a;s.next_on_questions&&((await de(e,n)).asked.some(c=>c.state!=="resolved")||(delete s.next,delete s.next_at,delete s.next_on_questions,await b(i.file,o)))}r(vi,"clearWaitedNext");async function pr(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);i.next===void 0&&i.next_on_questions===void 0||(delete i.next,delete i.next_at,delete i.next_on_questions,await b(n.file,a))}r(pr,"clearNext");async function ji(e,t,n,a){if(!e)return async()=>[];const{owner:i,asked:o}=await de(e,t),s=o.filter(l=>l.state==="open").map(l=>l.id);s.length&&!a&&d(`${n}: ${t} has ${s.length===1?"an open question":"open questions"} ${s.join(", ")} with no answer yet; pass --force to close the card and resolve ${s.length===1?"it":"them"} too`);const c=o.filter(l=>l.state!=="resolved").map(l=>l.id);return async()=>(c.length&&await Zt(e,i,c,"Closed with the card"),c.map(l=>`resolved: ${l}`))}r(ji,"closeQuestions");async function Ml(e,t,n,a,i){await x(e,t),Bn(n),ee(a,o=>_(o,"--owner-step")),ee(i,o=>Mi(o)),await ji(A(e),t,"land",!1)}r(Ml,"checkClose");async function ta(e,t){const n=await g(L(e),{items:[]}),{answers:a,requests:i,reopens:o}=await rt(e);return Po((n.items||[]).map(s=>Ee(s,a,i,[],o)),t)}r(ta,"unpickedPrototype");const wr=r(e=>e.length>120?`${e.slice(0,119)}\u2026`:e,"clip"),mr={ask:"text","ask-edited":"text","item-message":"text","owner-answer":"note"};async function Oi(e,t,n){const a=_(t,n).replace(/\s+/g," ");return(await Q(h.join(e,"data","events.jsonl"))).map(o=>String(o.data?.[mr[o.kind]]??"").replace(/\s+/g," ").toLowerCase()).some(o=>o.includes(a.toLowerCase()))||d(`${n} must quote the owner's words in an ask, card message or answer note on this board`),a}r(Oi,"ownerQuote");async function Wl(e,t,n){return A(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 Oi(e,n,"--asked")}"`}r(Wl,"askedGate");async function ct(e,t,n="--from-ask"){return Oe(t,n),(await Q(h.join(A(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(ct,"checkAsk");function na(e,t,n=new Date().toISOString(),a=Pi(e,t)){if(!Qe[t])throw new Error(`no lane "${t}"`);const i=e.lane;return Ai(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(na,"placeIn");function Pi(e,t,n=!1){return t==="todo"?De(e)?"ready":e.phase??null:t==="progress"?"reading":t==="done"?n||je(e).some(a=>a.merged_at)?"merged":"closed":null}r(Pi,"movedPhase");function Ai(e,t,n){if(n!==null&&!yn[t].includes(n))throw new Error(`no ${Qe[t]} phase "${n}"`);n===null?delete e.phase:e.phase=n}r(Ai,"setPhase");async function Ul(e,t,n,a,i,o){const{source:s}=await x(e,t),{id:c}=P(t),l=new Date().toISOString();if(!await Ze(s.file,async()=>{const p=JSON.parse(await O.readFile(s.file,"utf8")),f=p.items.find($=>$.id===c)||d(`item file "${s.slug}" has no item "${c}"`);if(o&&((f.lane??null)!==o.lane||(f.phase??null)!==o.phase)||f.lane===n&&(i===void 0||(f.phase??null)===i))return!1;let m;return f.lane===n?Ai(f,n,i):m=na(f,n,l,i),await Te(s.file,JSON.stringify(p,null,2)+`
|
|
23
|
+
`),m!==void 0}))return null;const w={ts:l,who:"claude",kind:va,text:`Moved to ${Qe[n]}: ${_(a,"why")}`};return await me(X(e,t),w),w.text}r(Ul,"moveCard");const G=r(e=>e?.lane==="done"||!!e?.reopened_at,"closed"),De=r(e=>!!e?.lane&&e.lane!=="todo","leftTodo"),Hl=r(async(e,t)=>G((await x(e,t)).item),"isFinished"),Di=["ready","grilling","prototyping"];function Fi(e,t){t!==void 0&&d(`--state is now --phase: pass --phase ${Di.includes(t)?t:"ready, grilling or prototyping"}`),e==="staging"&&d("--phase staging is set by the board, on an owner action; pass ready, grilling or prototyping"),e!==void 0&&!Di.includes(e)&&d("--phase takes ready, grilling or prototyping")}r(Fi,"checkPhase");const hr="a card goes ready only from staging, which the board sets on the owner's Send, Implement or Reply";async function Ri(e,t){return(await Q(h.join(A(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(Ri,"quickAsk");const gr=/^mark (?:it |this |the card )?ready\b/i;function $r(e,t,n){const a=[].concat(t).map(s=>An(s)||d(`--pr takes a PR number or owner/repo#<n>, not "${s}"`)),i=r((s,c)=>s.number===c.number&&(s.repo||n||null)===(c.repo||n||null),"same"),o=je(e);for(const s of a)o.some(c=>i(c,s))||o.push(s);delete e.pr,e.prs=o}r($r,"addPRs");const H=r((e,t)=>String(t).includes("/")?String(t):`${e}/${t}`,"depKey"),Ue=/^wave:(.+)$/;async function lt(e,t,n){const a=await Promise.all((n||[]).map(async i=>{const o=String(i).match(Ue)?.[1];if(!o)return[H(t,i)];const{source:s,wave:c}=await ut(e,o);return Ne(c).map(l=>`${s.slug}/${l}`)}));return[...new Set(a.flat())]}r(lt,"depCards");async function dt(e,t,n){const a=await lt(e,t,n.deps),i=await Promise.all(a.map(o=>x(e,o).then(({item:s})=>s,()=>null)));return a.filter((o,s)=>i[s]&&!G(i[s]))}r(dt,"unfinishedDeps");async function kr(e,t,n){const a=await Promise.all((await dt(e,P(t).file,n)).map(i=>J(e,i)));return n.blocked_until?[...a,n.blocked_until]:a}r(kr,"blockerNames");async function Xt(e,t,n){n??=(await x(e,t)).item;const a=await Promise.all((await dt(e,P(t).file,n)).map(i=>J(e,i)));return[a.length&&`by ${a.join(", ")}`,n.blocked_until&&`until ${n.blocked_until}`].filter(Boolean).join(" and ")}r(Xt,"blockWords");const br=r((e,t)=>!Be(e)&&!e.pushed&&!e.merge_ready&&!G(e)&&(!!xn(t)||t.some(n=>n?.started&&!n.done&&Ge(n))),"inProgress");async function yr(e,t){const n=[];for(const a of await M(e)){const i=await g(a.file,null);for(const o of Array.isArray(i?.items)?i.items:[])G(o)||o.blocked_until||!(await lt(e,a.slug,o.deps)).includes(t)||(await dt(e,a.slug,o)).length||n.push(`${a.slug}/${o.id}`)}return n}r(yr,"unblockedBy");async function xr(e,t,n){const a=P(t).file,i=(await aa(e,a,n)).map(c=>Ue.test(c)?c:H(a,c));i.length||d("--split-into takes wave:<slug> or the cards the split filed, <id>,<file>/<id>"),(await lt(e,a,i)).includes(t)&&d(`${t} can't be split into itself`);const o=await Promise.all(i.map(async c=>{const l=c.match(Ue)?.[1];if(!l)return J(e,c);const{source:u,wave:w}=await ut(e,l),p=await Promise.all(Ne(w).map(f=>J(e,`${u.slug}/${f}`)));return`the wave ${w.title} (${p.join(", ")})`})),s=[];for(const c of await M(e))for(const l of(await g(c.file,null))?.items||[]){const u=`${c.slug}/${l.id}`;if(!(G(l)||!(l.deps||[]).some(w=>H(c.slug,w)===t))){for(const w of i)(await lt(e,a,[w])).includes(u)&&d(Ue.test(w)?`${u} waits on ${t} and is in the wave "${w.match(Ue)[1]}", so it would wait on itself; split into the new cards by key`:`${u} waits on ${t} and is one of the cards it splits into, so it would wait on itself`);s.push(u)}}return{entries:i,words:o.join(", "),waiters:s}}r(xr,"splitPlan");async function _r(e,t,{entries:n,words:a,waiters:i}){for(const s of await M(e)){const c=i.filter(u=>P(u).file===s.slug);if(!c.length)continue;const l=await g(s.file,null);for(const u of l.items.filter(w=>c.includes(`${s.slug}/${w.id}`))){const w=n.map(p=>!Ue.test(p)&&P(p).file===s.slug?P(p).id:p);u.deps=[...new Set([...u.deps.filter(p=>H(s.slug,p)!==t),...w])]}await b(s.file,l)}const o=await J(e,t);for(const s of i)await U(e,s,"split-moved",{card:o,to:a})}r(_r,"moveWaiters");async function Ni(e,t,n){const{source:a,itemFile:i,item:o}=await x(e,t);De(o)&&(o.unblocked_at=n,await b(a.file,i))}r(Ni,"markUnblocked");async function Ei(e,t,n,a){const i=new Set(a.match(/#?[\w-]+(?:\.[\w-]+)*(?:\/[\w-]+(?:\.[\w-]+)*)*/g)||[]),o=new Set(await lt(e,P(t).file,n.deps));for(const s of await M(e)){const c=await g(s.file,null);for(const l of c?.items||[]){const u=`${s.slug}/${l.id}`;if(u===t||o.has(u)||G(l))continue;[l.id,Ve(c,l),l.ticket].filter(Boolean).flatMap(p=>[p,`${s.slug}/${p}`]).some(p=>i.has(p))&&d(`--next names unfinished card ${u}; add it with --deps`)}}}r(Ei,"checkNextDeps");const Li=r(e=>`item notes ${e} --heading "<heading>" --file <draft.md>`,"NOTES_COMMAND");function Sr(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 ${Li(e)} before --phase ready`)}r(Sr,"checkSizeAndNotes");async function vr(e,t,n,a){Sr(t,a);const i=await O.readFile(await en(n,a.plan),"utf8"),o=(await de(e,t)).asked.filter(s=>s.answers.length&&!i.includes(s.id)&&!((s.prototype_gate||s.ticket_gate)&&!s.answers.at(-1).note)).map(s=>s.id);o.length&&d(`${t}'s notes leave out the answered question${o.length===1?"":"s"} ${o.join(", ")}; record each answer in its notes, naming its id, with ${Li(t)} before it is ready`)}r(vr,"checkNotesComplete");async function en(e,t,{missingOk:n=!1}={}){const a=r(s=>d(`a notes file is a .md file inside ${e}, named relative to it: "${t}" ${s}`),"bad");(typeof t!="string"||!t.trim())&&a("is empty"),(h.isAbsolute(t)||t.split(/[\\/]/).includes(".."))&&a("leaves the folder"),h.extname(t).toLowerCase()!==".md"&&a("is not a .md file");const i=await O.realpath(e),o=await O.realpath(h.resolve(e,t)).catch(()=>n?h.resolve(i,t):a("does not exist"));return o.startsWith(i+h.sep)||a("leaves the folder"),o}r(en,"planPath");async function jr(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 en(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(jr,"readNotes");function Ii({install:e,installSha:t,installLabel:n,clearInstall:a}){if(e===void 0)return(t!==void 0||n!==void 0)&&d("--install-sha and --install-label go with --install <url>"),a?null:void 0;a&&d("pass --install or --clear-install, not both");let i;try{i=new URL(String(e).trim())}catch{i=null}(!i||!["https:","itms-services:"].includes(i.protocol))&&d(`--install takes an https:// or itms-services:// URL, not "${e}"`),i.protocol==="itms-services:"&&!(i.searchParams.get("action")==="download-manifest"&&/^https:\/\//.test(i.searchParams.get("url")||""))&&d("--install: an itms-services:// URL needs ?action=download-manifest&url=https://\u2026/manifest.plist");const o=t===void 0?void 0:/^[0-9a-f]{7,40}$/i.test(t)?t.toLowerCase():d(`--install-sha takes a commit sha of 7 to 40 hex digits, not "${t}"`);return{url:String(e).trim(),...n===void 0?{}:{label:_(n,"--install-label")},built_at:new Date().toISOString(),...o?{sha:o}:{}}}r(Ii,"installOf");async function Jl(e,{install:t,installSha:n}){const a=Ii({install:t,installSha:n});a||d("main set needs --install <url>");const i=h.join(A(e),"data","main.json");return await b(i,{install:a}),{file:i,lines:["main install set"]}}r(Jl,"setMainInstall");const Or=r((e,t)=>e.length?`Merged PR${e.length>1?"s":""} ${Ye(e,t)}.`:"Merged.","mergedLine");function Pr(e,t,n,a,i,o){return o?{status:o}:n&&a===void 0?{status:["merged",{words:Or(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(Pr,"closingLine");function Ar(e,t,n){const a=new Date().toISOString(),i=r((o,s)=>o.number===s.number&&(o.repo||n||null)===(s.repo||n||null),"same");e.prs=je(e).map(o=>{const s=t.find(c=>i(o,c));return s&&!o.merged_at?{...o,merged_at:s.at||a}:o}),delete e.pr}r(Ar,"markMerged");async function Bi(e,t,{ticket:n,newTicket:a,status:i,clearStatus:o,done:s,splitInto:c,merged:l,landed:u,start:w,handBack:p,next:f,clearNext:m,pr:$,pushed:y,mergeReady:S,ghSince:E,fromAsk:j,phase:F,state:D,held:T,unheld:v,pickUpAt:B,deps:C,blockedUntil:I,blockReason:q,parent:N,related:V,relate:Y,unrelate:K,install:Z,installSha:Nt,installLabel:dn,clearInstall:he,title:xe,size:Et,model:ft,force:pt,text:se,closing:te,who:un,by:fe,command:wa,ownerStep:ze,ownerDone:mo,check:wt},ma){(i!==void 0||o)&&d(`--status${o?" and --clear-status are":" is"} gone: a card's lane is stored and moves itself. Close it with --done --text "<what was done, or why it closes without landing>"`),l&&(s=!0),V!==void 0&&(Y!==void 0||K!==void 0)&&d("pass --related, or --relate and --unrelate, not both"),Fi(F,D),pt&&!s&&d("--force goes with --done"),c!==void 0&&!s&&d("--split-into goes with --done"),c!==void 0&&se!==void 0&&d("--split-into writes the closing line; drop --text");const mt=c===void 0?null:await xr(e,t,c);mt&&(se=`Split into ${mt.words}`),j!==void 0&&s&&d("--from-ask opens a card; close it in another call"),se!==void 0&&Bn(se),ze!==void 0&&_(ze,"--owner-step");const ht=wt===void 0||wt===""?wt:Mi(wt);se!==void 0&&j===void 0&&!s&&F!=="ready"&&d("--text goes with --from-ask, --done, or --phase ready on a staging card"),j!==void 0&&se===void 0&&d(`--from-ask needs --text "<first line>", the item's first thread line after the ask`),f!==void 0&&m&&d("pass --next or --clear-next, not both"),typeof f=="string"&&!f.trim()&&([f,m]=[void 0,!0]);const gt=Ci({held:T,unheld:v,pickUpAt:B}),Lt=Ii({install:Z,installSha:Nt,installLabel:dn,clearInstall:he});[s,w,p,f,m,$,y,S,E,j,F,gt,C,I,q,N,V,Y,K,Lt,xe,Et,ft,n,a,ze,wt].every(R=>R===void 0||R===!1)&&d("nothing to set: pass --done, --next, --clear-next, --pr, --pushed, --gh-since, --from-ask, --phase, --held, --unheld, --pick-up-at, --deps, --blocked-until, --block-reason, --parent, --related, --relate, --unrelate, --install, --clear-install, --title, --size, --model, --ticket, --new-ticket or --check");const ne=j===void 0?void 0:await ct(e,j);ne&&await zt(e,ne);const ho=ne!==void 0&&await Ri(e,ne),{source:Re,itemFile:Ke,item:k}=await x(e,t);F!==void 0&&De(k)&&d(`--phase sets a Todo card's phase; ${t} is ${Qe[k.lane]}, where the board sets it`),F==="ready"&&!ho&&!["staging","ready"].includes(k.phase)&&d(`${t} reads ${k.phase||"new"}, not staging; ${hr}`),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&>&&F!==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&&F===void 0&&!k.phase&&!gt&&d("--from-ask files a card, which reads New until it has a phase: pass --phase grilling, prototyping, or ready for a Quick build ask, or --held to file it for later");const fn=G(k);mt&&fn&&d(`${t} is already Done, so the cards waiting on it were unblocked then; move them with item set <key> --deps`),ze!==void 0&&fn&&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`),ze!==void 0&&(k.owner_step=_(ze,"--owner-step")),mo&&delete k.owner_step,ht!==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`),ht&&(k.owner_check={check:ht,since:k.owner_check?.check===ht?k.owner_check.since:new Date().toISOString()}),(ht===""||!k.owner_step)&&delete k.owner_check;const pn=await Promise.all((await z(Re.dir,k.id)).map(R=>g(R,null))),go=_n(k,pn);if(xe!==void 0&&(k.title=_(xe,"--title")),Ti(k,{size:Et,model:ft}),C!==void 0){const R=await aa(e,P(t).file,C,t);R.length?k.deps=R:delete k.deps}const ha=I===""?k.blocked_until:void 0;if(I!==void 0&&(I===""?delete k.blocked_until:k.blocked_until=_(I,"--blocked-until")),q&&(!k.deps?.length&&!k.blocked_until&&d("--block-reason goes with a block: --deps or --blocked-until"),k.block_reason=_(q,"--block-reason")),(q===""||!k.deps?.length&&!k.blocked_until)&&delete k.block_reason,N!==void 0){const R=await Wi(e,P(t).file,t,N);R?k.parent=R:delete k.parent}const ga=N!==void 0||V!==void 0||Y!==void 0||K!==void 0?await ia(e,t):null;let wn=V===void 0?null:await Rr(e,t,k,V);Y!==void 0&&(k.related=[...new Set([...(k.related||[]).map(R=>H(P(t).file,R)),...await nn(e,t,Y,"--relate")])]),K!==void 0&&(wn=await Nr(e,t,k,await nn(e,t,K,"--unrelate"))),gt!==void 0&&tn(k,gt),w&&k.held&&d(k.pick_up_at?`${t} is held until ${k.pick_up_at}, when the board resumes it; start it early only with item set ${t} --unheld`:`${t} is held; start it only once the owner resumes it, with item set ${t} --unheld`);const $t=$!==void 0||y!==void 0||S!==void 0;if((w||s||p||$t)&&Fr(k),w){const R=await dt(e,P(t).file,k);R.length&&d(`${t} is blocked by ${R.join(", ")}; start it once ${R.length===1?"that is":"those are"} done`),k.blocked_until&&d(`${t} is blocked until ${k.blocked_until}; start it once that holds and the block is cleared with --blocked-until ""`)}const It=w&&e?await ta(e,t):null;It&&d(`${t} has no take picked: the owner's answer to ${It.id} picks none${It.note?` ("${wr(It.note)}")`:""}, so it prototypes again: draw the next round from that note, or grill when the note asks a question, and build once a take is picked`);const $o=s?await ji(e,t,"--done",pt):async()=>[],kt=s?Pr(e,t,l,se,oe(e,Ke),te):null;if(l&&Ar(k,l,oe(e,Ke)),f!==void 0&&(Si(k,f,await ea(e,t)),await Ei(e,t,k,k.next)),m&&(delete k.next,delete k.next_at,delete k.next_on_questions),w&&f===void 0&&k.next_on_questions&&(delete k.next,delete k.next_at,delete k.next_on_questions),$t&&!k.review_since&&(k.review_since=new Date().toISOString()),$!==void 0&&$r(k,$,oe(e,Ke)),y!==void 0){we().find(R=>ve(R.dir,A(e)))?.remote||d("--pushed is for a board whose repo is a plain git remote; on GitHub, open a PR and pass --pr"),Be(k)&&d(`${t} has a PR; its PR says when it merges`);try{Yo(y)}catch{d("--pushed takes the name of the branch you pushed")}k.pushed={branch:y,at:new Date().toISOString()},k.branch=y}if(S!==void 0&&(k.merge_ready={branch:S,at:new Date().toISOString()},k.branch=S),s&&delete k.merge_ready,E!==void 0&&((typeof E!="string"||!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(E)||Number.isNaN(Date.parse(E)))&&d("--gh-since takes an ISO-8601 UTC time such as 2026-10-23T11:00:00Z"),k.gh_since=E),ne&&(k.from_ask=ne),F==="ready"&&e){const R=(await de(e,t)).asked.filter(Se=>Se.state==="open").map(Se=>Se.id);R.length&&d(`${t} has ${R.length===1?"an open question":"open questions"} ${R.join(", ")}; it is ready once the owner has answered ${R.length===1?"it":"them"}`),await vr(e,t,Re.dir,k)}const mn=F==="ready"&&k.phase==="staging"&&j===void 0;mn&&se===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`),F==="ready"&&!mn&&se!==void 0&&j===void 0&&!s&&d(`${t} reads ${k.phase||"no phase"}, not staging, so there is no judgment for --text to record`);const ko=F==="ready"&&k.phase!=="ready";F!==void 0&&(F!=="ready"&&["staging","ready"].includes(k.phase)&&(k.round_at=new Date().toISOString()),k.phase=F,delete k.staged_at,delete k.unfiled,delete k.filed_held),k.phase==="ready"&&gr.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`),Lt&&(k.install=Lt),Lt===null&&delete k.install,!go&&!w&&_n(k,pn)&&d(`that leaves ${t} In Progress with nothing building and no Next; pass --next "<what happens next>"`);const hn=(C!==void 0||I)&&!s&&br(k,pn)?await Xt(e,t,k):"",$a=oe(e,Ke);let gn=null;const $n=r((R,Se)=>{k.lane!==R&&na(k,R,void 0,Pi(k,R,u))!==void 0&&Se&&(gn=`Moved to ${Qe[R]}: ${Se}`)},"moveTo");if(s?$n("done",null):$t&&k.lane!=="done"?$n("review",$!==void 0?`PR ${Ye([].concat($).map(An),$a)} opened`:y!==void 0?`${y} pushed for its merge`:"verified, it waits for your Merge"):w&&!G(k)&&$n("progress","a build started"),a&&k.ticket&&d(`${t} already has ticket ${k.ticket}`),(n!==void 0||a)&&await Ji(e,Ke,k,n,ma,Re.dir),await b(Re.file,Ke),gn&&await me(X(e,t),{ts:k.lane_at,who:"claude",kind:va,text:gn}),mn&&await st(e,t,`Ready (orchestrator, ${Pt()})`,se),ko&&await Me(e,t,{command:"readied"}),hn){const R=e&&!await fo(e,t)&&await uo(e,t,{cause:"blocked",words:`blocked ${hn}`});await U(e,t,R?"blocked-pausing":"blocked-stays",{blocked:hn})}wn&&await wn(),ga&&await Hi(e,t,ga,await ia(e,t));const _e=await $o();if(F==="ready"&&e&&_e.push(...await Yt(e,t,"Recorded in the card's notes")),w&&_e.push(...await Yt(e,t,"Acted on: build started")),ne&&_e.push(...(await _t(e,t,{text:se})).lines),kt&&e){const R=wa?await Me(e,t,{command:wa,text:kt.text,who:un,by:fe}):kt.status?await U(e,t,...kt.status):await _t(e,t,{text:kt.text});_e.push(...R.lines)}const ka=k.owner_step&&s?null:zo({start:w,done:s,handover:$t}),ba=ka&&await Ko(await an(e),k,ka,{...ma,repo:$a});ba&&_e.push(ba),($t||s||p)&&(await wo(Re.dir,k.id,s,p?{handedBack:new Date().toISOString()}:{endHandBack:!0}),await ot(e,t));const kn=ne?await Pe(e):{};if(ae(kn,ne)!==void 0&&(delete kn[ne],await b(ke(e),{links:kn})),ha&&!G(k)&&!(await dt(e,P(t).file,k)).length){const{line:R}=await U(e,t,"unblocked",{lifted:ha});await Ni(e,t,R.ts),await ge({dir:e},"card-unblocked",{key:t}),_e.unshift(`unblocked: ${t}`)}if(!s||fn)return{file:Re.file,lines:_e};mt&&await _r(e,t,mt);const ya=await yr(e,t),bo=await J(e,t);for(const R of ya){const{line:Se}=await U(e,R,"unblocked-done",{card:bo});await Ni(e,R,Se.ts),await ge({dir:e},"card-unblocked",{key:R})}return{file:Re.file,lines:[...ya.map(R=>`unblocked: ${R}`),..._e]}}r(Bi,"changeItem");function Ti(e,{size:t,model:n}){for(const[a,i,o]of[["size",t,["small","large"]],["model",n,["opus","sonnet"]]])i!==void 0&&(i===""?delete e[a]:e[a]=o.includes(i)?i:d(`--${a} takes ${o.join(" or ")}, or "" to clear it`))}r(Ti,"setBuildChoice");function Ci({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:Dr(n)}:e?{until:null}:t?null:void 0}r(Ci,"holdOf");function Dr(e){const t=typeof e=="string"&&Ho.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(Dr,"pickUpTime");function Fr(e){delete e.held,delete e.pick_up_at}r(Fr,"clearHold");function tn(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(tn,"setHold");async function zl(e,t,n,{announce:a=!0}={}){const{source:i,itemFile:o,item:s}=await x(e,t);if(a){const c=new Date(n).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});await U(e,t,"hold-ended",{at:c}),!s.blocked_until&&!(await dt(e,P(t).file,s)).length&&await ge({dir:e},"card-unblocked",{key:t,until:n})}tn(s,null),await b(i.file,o)}r(zl,"pickUp");async function qi(e,t,n){const a=String(n?.key||""),{file:i,id:o}=P(a),{source:s,itemFile:c}=await Ce(e,i),l=n.shared_by?n.shared_by.name||n.shared_by.login:void 0,u={text:Tt.done,who:"love",by:l,command:"done"},w=c.items.find($=>$.id===o)||d(`item file "${i}" has no item "${o}"`);if(!(t==="card-done"&&!!w.owner_step)&&w.lane==="done")return null;if(t==="card-done")return ue(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;tn(w,n.hold?{until:f}:null),await b(s.file,c),n.hold&&(await fo(e,a)||await uo(e,a,{cause:"held",words:"held"}),await wo(s.dir,o,!1,{held:!0}),await ot(e,a));const m=f&&new Date(f).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});return Me(e,a,n.hold?{text:m?`${Tt.hold} until ${m}`:Tt.hold,who:"love",by:l,command:"hold"}:{who:"love",by:l,command:"resume"})}r(qi,"recordOwnerTap");function Mi(e){try{return hs(ms(e))}catch(t){return d(t.message)}}r(Mi,"checkSpec");async function Kl(e,t,n,a){const{item:i}=await x(e,t);return!i.owner_step||i.owner_check?.check!==n?null:ue(e,t,{done:!0,force:!0,ownerDone:!0,closing:["check-passed",{found:a}]})}r(Kl,"passOwnerCheck");async function Ql(e,t,n,a){const{source:i,itemFile:o,item:s}=await x(e,t);return!s.owner_step||s.owner_check?.check!==n?!1:(s.owner_check.broken=a,await b(i.file,o),await U(e,t,"check-broken",{error:a}),await ge({dir:e},"owner-check-broken",{key:t,check:n,error:a}),!0)}r(Ql,"breakOwnerCheck");async function aa(e,t,n,a){const i=[...new Set(String(n).split(",").map(o=>o.trim()).filter(Boolean))];for(const o of i){const s=o.match(Ue)?.[1];if(!s){await x(e,H(t,o));continue}(await lt(e,t,[o])).includes(a)&&d(`${a} is in the wave "${s}", so it would wait on itself`)}return i}r(aa,"depList");async function Wi(e,t,n,a){if(!String(a).trim())return null;const i=H(t,String(a).trim());i===n&&d("--parent: a card can't be its own Parent");for(let o=i;o;){const s=o===i?await x(e,o):await x(e,o).catch(l=>{if(l instanceof le)return null;throw l});if(!s)break;const{item:c}=s;c.parent&&H(P(o).file,c.parent)===n&&d(`--parent: ${i} is a child of ${n}, so it can't be its Parent`),o=c.parent?H(P(o).file,c.parent):null}return i}r(Wi,"parentKey");async function nn(e,t,n,a){const i=t.split("/")[0],o=[...new Set(String(n).split(",").map(s=>s.trim()).filter(Boolean).map(s=>H(i,s)))];o.includes(t)&&d(`${a}: a card can't be related to itself`);for(const s of o)await x(e,s);return o}r(nn,"relatedKeys");const Ui=r((e,t,n)=>async()=>{for(const a of await M(e)){const i=await g(a.file,null);let o=!1;for(const s of Array.isArray(i?.items)?i.items:[]){const c=`${a.slug}/${s.id}`;c===t||!n(c)||!(s.related||[]).some(l=>H(a.slug,l)===t)||(s.related=s.related.filter(l=>H(a.slug,l)!==t),s.related.length||delete s.related,o=!0)}o&&await b(a.file,i)}},"dropLinksTo");async function ia(e,t){const n=new Set;let a=null;for(const i of await M(e))for(const o of(await g(i.file,null))?.items||[]){const s=`${i.slug}/${o.id}`,c=(o.related||[]).map(l=>H(i.slug,l));s===t?(c.forEach(l=>n.add(l)),o.parent&&(a=H(i.slug,o.parent))):c.includes(t)&&n.add(s)}return{related:n,parent:a}}r(ia,"linksOf");async function Hi(e,t,n,a){const i=r(async(o,s,c=o,l=o)=>{await Me(e,t,{command:c,card:s}),await Me(e,s,{command:l,card:t})},"both");for(const o of a.related)n.related.has(o)||await i("related",o);for(const o of n.related)a.related.has(o)||await i("unrelated",o);a.parent&&a.parent!==n.parent&&await i("followup-of",a.parent,"followup-of","followup")}r(Hi,"postLinkLines");async function Rr(e,t,n,a){const i=await nn(e,t,a,"--related");return i.length?n.related=i:delete n.related,Ui(e,t,o=>!i.includes(o))}r(Rr,"setRelated");function Nr(e,t,n,a){const i=(n.related||[]).filter(o=>!a.includes(H(P(t).file,o)));return i.length?n.related=i:delete n.related,Ui(e,t,o=>a.includes(o))}r(Nr,"unrelateCards");const oe=r((e,t)=>e&&we().find(n=>ve(n.dir,e))?.github||t.repo,"boardRepoOf");async function Gl(e,t){const{itemFile:n,item:a}=await x(e,t);Be(a)||d(`${t} has no PR`);const i=Ie(a,oe(e,n));i.length||d(`${t} has no repo`);const o=a.gh_since||"1970-01-01T00:00:00Z";try{const s=be(await ie(e)).github_login||await ss();return await Promise.all(i.map(async({repo:c,number:l})=>({repo:c,...Na(await Ra({repo:c,pr:l,since:o}),{pr:l,since:o,me:s})})))}catch(s){d(s.message)}}r(Gl,"prReport");async function Vl(e,t,{replyTo:n,body:a}){const i=n===void 0?"pr comment":"pr reply",o=An(t)||d(`${i} takes a PR number or owner/repo#<n>`);n!==void 0&&!/^[1-9]\d*$/.test(n)&&d("pr reply takes the id of the review comment it answers");const s=_(a,"--body").trim();(s.startsWith(yt.trim())||s.includes(Pn))&&d(`${i} adds the ${yt.trim()} prefix and the claude-bridge marker itself; pass only your words`);const c=o.repo||oe(A(e),{})||d(`${i} needs a board on GitHub; this board has no repo there`);try{const l=await rs({repo:c,pr:o.number,replyTo:n,words:s,login:be(await ie(e)).github_login}),u=await ei(e,xt({repo:c,number:o.number}),"comment",`Comment on #${o.number}: ${s}`);return{lines:[`posted on PR ${o.repo?t:`#${t}`}: ${l}`,...u.length?[`queued for ${u.join(", ")}'s Slack thread`]:[]]}}catch(l){d(l.message)}}r(Vl,"postPR");const an=r(async e=>si((await ie(e)).tickets),"boardSource");async function Zl(e,t,{afterApproval:n,confirmed:a,tapped:i}={}){A(e);const{itemFile:o,item:s}=await x(e,t);Be(s)||d(`${t} has no PR`),G(s)&&d(`${t} is done`);const c=await Xt(e,t,s);c&&(i&&await U(e,t,"merge-blocked",{blocked:c}),d(`Not merged: blocked ${c}.`));const l=oe(e,o),u=Ie(s,l);u.length||d(`${t} has no repo`);const w=new Date().toISOString();let p;try{p=await Promise.all(u.map(v=>Ra({repo:v.repo,pr:v.number,since:w})))}catch(v){d(v.message)}const f=p.map(({view:v})=>v);if(ds(f)){const v=u.map((B,C)=>({...B,at:f[C].mergedAt})).filter((B,C)=>f[C].state==="MERGED");return ue(e,t,{merged:v,closing:["already-merged",{prs:Ye(v,l),many:v.length>1}]})}const m=u.map((v,B)=>({...v,...p[B]})).filter(({view:v})=>v.state==="OPEN"),$=Fn(o,(await ie(e)).reviewers||[]).length>0,y=[];let S=!1,E=!1;for(const v of m){const{view:B,reviews:C}=v,{failing:I,pending:q}=Na(v,{pr:v.number,since:w,me:null}),N=Oo({...B,checks:{state:I.length?"fail":q?"pending":"pass",failing:I,pending:q}},{needsApproval:$}),V=C.filter(K=>K.state==="APPROVED").sort((K,Z)=>String(K.submitted_at).localeCompare(String(Z.submitted_at))).at(-1)?.commit_id;let Y=!!(V&&B.headRefOid&&V!==B.headRefOid);Y&&n===void 0&&Kr(s,V,B.headRefOid)&&(Y=!1,E=!0),S||=Y,Y&&n===void 0&&N.push('commits after the approval: pass --after-approval "<why they are trivial>" when they are, else ask the approver to look again'),y.push(...u.length>1?N.map(K=>`${Ye([v],l)} ${K}`):N)}m.length||y.push(u.length>1?"every PR is closed":"PR is closed"),n!==void 0&&!S&&d("--after-approval: no commit came after the approval; drop it");const j=s.manual_steps?.length?a===void 0?null:await Oi(e,a,"--confirmed"):void 0;if(a!==void 0&&j===void 0&&d(`--confirmed: ${t} has no manual steps; drop it`),j===null&&y.push(`manual steps: merge only on the owner's word, with --confirmed "<their words>"`),y.length){const v=`Not merged: ${y.join("; ")}.`;i&&await U(e,t,"merge-refused",{why:y.join("; ")}),d(v)}const F=[];for(const v of m){const B=await new Promise(C=>{xa("gh",["pr","merge",String(v.number),"--repo",v.repo,"--squash"],{encoding:"utf8",timeout:12e4},(I,q,N)=>C(I?String(N||I.message).trim().split(`
|
|
24
|
+
`)[0]:null))});B&&d(`gh pr merge ${Ye([v],l)} failed: ${B}${F.length?` (merged ${Ye(F,l)} before it)`:""}`),F.push(v)}const D=[n!==void 0&&`After the approval: ${_(n,"--after-approval")}`,E&&`After the approval: only the board's rebase onto ${s.rebased.at(-1).base}.`,j&&`The owner's word on the manual steps: "${j}"`].filter(Boolean),T=await ue(e,t,{merged:F});return D.length&&await _t(e,t,{text:D.join(" ")}),T}r(Zl,"mergeCard");async function Yl(e,t){return(await x(e,t)).item.merge_ready||null}r(Yl,"heldMerge");async function Xl(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 o=await Xt(e,t,i);return o&&d(`${t} is blocked ${o}, so its Merge waits until nothing blocks it`),i.merge_ready.tapped=new Date().toISOString(),await b(n.file,a),i.merge_ready}r(Xl,"tapMerge");async function ed(e,t){const{source:n,item:a}=await x(e,t);return(await Promise.all((await z(n.dir,a.id)).map(o=>g(o,null)))).filter(o=>o?.worktree?.path).sort((o,s)=>String(s.started).localeCompare(String(o.started)))[0]?.model}r(ed,"builderModel");async function Ji(e,t,n,a,i,o,s){const c=await an(e)||d(Er);try{s??=a===void 0&&n.plan?await O.readFile(await en(o,n.plan),"utf8"):n.notes||"";const l=await Jo(c,{ticket:a,title:n.title,body:s,repo:oe(e,t)},i);n.ticket=l.ticket,l.created?n.ticket_created=!0:delete n.ticket_created}catch(l){if(l instanceof le)throw l;d(l.message)}}r(Ji,"assignTicket");const Er="this board has no ticket source, so its cards stay tasks; pick GitHub issues or Linear in Board settings first";async function Lr(e){const t=[];for(const n of await M(e))t.push(...((await g(n.file,{})).items||[]).map(a=>a.id));return e&&t.push(...(await Fe(e)).map(n=>n.item?.id)),`T-${Math.max(0,...t.map(n=>Number(/^T-([1-9]\d*)$/.exec(String(n))?.[1]||0)))+1}`}r(Lr,"nextTaskId");const He=r((e,t)=>Ze(h.join(e||Mt(),"tickets"),t,{heartbeat:!0}),"ticketWrite"),ue=r((e,t,n,a)=>n.ticket!==void 0||n.newTicket?He(e,()=>Bi(e,t,n,a)):Bi(e,t,n,a),"setItem"),td=r((e,t,n,a)=>He(e,()=>Ir(e,t,n,a)),"addItem");async function Ir(e,t,{title:n,notes:a,notesFrom:i,deps:o,ticket:s,newTicket:c,pickUpAt:l,held:u,parent:w,size:p,model:f,phase:m,state:$,relate:y,fromAsk:S,text:E,intake:j=!1},F){const{file:D,id:T}=t.includes("/")?P(t):{file:t};T!==void 0&&Oe(T,"the item id"),Fi(m,$),E!==void 0&&_(E,"--text"),E!==void 0&&S===void 0&&d("--text goes with --from-ask"),S!==void 0&&E===void 0&&d(`--from-ask needs --text "<first line>", the item's first thread line after the ask`),s!==void 0&&c&&d("pass --ticket <id> to link a ticket or --new-ticket to make one, not both");const v=S===void 0?void 0:await ct(e,S);v&&await zt(e,v);const B=Ci({held:u,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"),v&&m===void 0&&!B&&d("--from-ask files a card, which reads New until it has a phase: pass --phase grilling, prototyping, or ready for a Quick build ask, or --held to file it for later"),m==="ready"&&!(v&&await Ri(e,v))&&d(v?`ask "${v}" is not a Quick build ask, so item add files it grilling or prototyping`:"item add files a card grilling or prototyping; ready is for a Quick build ask, filed with --from-ask"),m==="ready"&&!p&&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"),j&&e&&m===void 0&&!B&&d("item add files a card on a board: pass --phase grilling or prototyping, or --held to file it for later");const{source:C,itemFile:I}=await Ce(e,D);T!==void 0&&I.items.some(Z=>Z.id===T)&&d(`item file "${D}" already has an item "${T}"`);const q=new Date().toISOString(),N={id:T,title:_(n,"--title"),created:q,lane:"todo",lane_at:q};w!==void 0&&(N.parent=await Wi(e,D,null,w)||d("--parent takes a card, such as <file>/<id>")),a!==void 0&&i!==void 0&&d("pass --notes or --notes-from, not both");const V=i!==void 0?await ki(e,i,"--notes-from",()=>`${Yn}, and link that card with --relate`):a===void 0?"":_(a,"--notes");i!==void 0&&!V.trim()&&d(`--notes-from ${i} is empty`);const Y=i!==void 0?V:`# ${N.title}
|
|
25
|
+
${V&&`
|
|
26
|
+
${V}
|
|
27
|
+
`}`;Ti(N,{size:p,model:f}),o!==void 0&&(N.deps=await aa(e,D,o)),B&&tn(N,B),B&&m===void 0&&(N.filed_held=!0),y!==void 0&&(N.related=await nn(e,t,y,"--relate")),v&&(N.from_ask=v),m!==void 0&&(N.phase=m),(s!==void 0||c)&&await Ji(e,I,N,s,F,C.dir,V),T===void 0&&N.ticket?(N.id=Qo(N.ticket),I.items.some(Z=>Z.id===N.id)&&d(`item file "${D}" already has an item "${N.id}"; link ${N.ticket} with item add ${D}/<id> --ticket ${N.ticket}`)):T===void 0&&(N.id=await Lr(e)),N.plan=Vn(C.dir,N.id),await Zn(h.join(C.dir,N.plan),Y);const K=`${D}/${N.id}`;if(I.items.push(N),await b(C.file,I),v){await _t(e,K,{text:E});const Z=await Pe(e);ae(Z,v)!==void 0&&(delete Z[v],await b(ke(e),{links:Z}))}return e&&await Hi(e,K,{related:new Set,parent:null},await ia(e,K)),{file:C.file,key:K,lines:[]}}r(Ir,"createItem");const nd=r(e=>He(e,async()=>{const t=[];for(const n of await M(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 o=[];for(const{id:l,title:u,from:w,filed:p,done:f,...m}of a.followups){const $=f?e&&(await Q(X(e,`${n.slug}/${l}`))).map(y=>y.ts).filter(Boolean).sort().pop()||p:null;o.push({id:l,title:u,...p?{created:p}:{},...w?{parent:H(n.slug,w)}:{},...f?{lane:"done",...$?{lane_at:$,closed_at:$}:{}}:{},...m})}const{followups:s,...c}=a;await b(n.file,{...c,items:[...a.items,...o]}),t.push(`${n.slug}: ${o.length} follow-ups moved to cards (${o.filter(l=>l.lane==="done").length} done)`)}return t}),"migrateFollowups");function zi(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=je(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&&Ge(i)).sort((i,o)=>String(i.started).localeCompare(String(o.started))).at(-1);return e.status==="checking"&&a&&(a.handed_back=n),delete e.status,delete e.status_at,delete e.merged_at,!0}r(zi,"statusToFacts");const ad=r(e=>He(e,async()=>{const t=[];for(const n of await M(e)){const a=await g(n.file,null);let i=0;for(const o of Array.isArray(a?.items)?a.items:[]){const s=o.status==="checking"?await z(n.dir,o.id):[],c=await Promise.all(s.map(l=>g(l,null)));if(zi(o,c.filter(Boolean))){i++;for(const[l,u]of c.entries())u?.handed_back&&await b(s[l],u)}}i&&(await b(n.file,a),t.push(`${n.slug}: ${i} cards' statuses became lanes and facts`))}return await At(e,async()=>{const n=await Fe(e),a=n.filter(i=>i.item&&zi(i.item)).length;a&&(await Dt(e,n),t.push(`archive: ${a} cards' statuses became lanes and facts`))}),t}),"migrateStatuses");function Ki(e,t){const n=e.lane==="done"&&!yn.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=je(e);e.phase=a.some(i=>i.merged_at)||!a.length&&t?"merged":"closed"}return!0}r(Ki,"stateToPhase");const id=r(e=>He(e,async()=>{const t=[],n=await M(e),a=r(async(i,o)=>!!i&&o.lane==="done"&&(await Promise.all((await z(i,o.id)).map(s=>g(s,null)))).some(s=>s?.started&&Ge(s)),"built");for(const i of n){const o=await g(i.file,null);let s=0;for(const c of Array.isArray(o?.items)?o.items:[])Ki(c,await a(i.dir,c))&&s++;s&&(await b(i.file,o),t.push(`${i.slug}: ${s} cards' states became phases`))}return await At(e,async()=>{const i=await Fe(e);let o=0;for(const s of i)s.item&&Ki(s.item,await a(n.find(c=>c.slug===(s.file||P(s.key).file))?.dir,s.item))&&o++;o&&(await Dt(e,i),t.push(`archive: ${o} cards' states became phases`))}),t}),"migratePhases"),od=r(e=>He(e,async()=>{const t=[];for(const n of await M(e)){const a=await g(n.file,null),i=(Array.isArray(a?.items)?a.items:[]).filter(o=>o.implement_at);if(i.length){for(const o of i)o.phase!=="ready"&&Object.assign(o,{phase:"staging",staged_at:o.implement_at}),delete o.implement_at;await b(n.file,a),t.push(`${n.slug}: ${i.length} Implement asks moved to staging or ready`)}}return t}),"stageImplementAsks"),on=r(e=>h.join(e,"data","done-archive.json"),"archiveFile"),At=r((e,t)=>Ze(h.join(e,"done-archive"),t,{heartbeat:!0}),"archiveWrite"),Fe=r(async e=>(await g(on(e),{cards:[]})).cards||[],"readArchive"),sn=r(e=>`${e.closed_at||""}|${e.key}`,"archiveRank"),Br=r((e,t)=>sn(e)<sn(t)?1:sn(e)>sn(t)?-1:0,"byClosed"),oa=r(e=>h.join(e,"data","done-archive-count.json"),"archiveCountFile");async function sd(e){const t=(await g(oa(e),null))?.count;if(Number.isInteger(t))return t;const n=(await Fe(e)).length;return await b(oa(e),{count:n}),n}r(sd,"archiveCount");async function Dt(e,t){t.length?await b(on(e),{cards:t.sort(Br)}):await O.rm(on(e),{force:!0}),await b(oa(e),{count:t.length})}r(Dt,"writeArchive");const rd=r((e,t)=>At(e,async()=>{const n=new Map(t.map(p=>[p.key,p])),a=new Date().toISOString(),i=[],o=[];for(const p of await M(e)){const f=await g(p.file,null);if(!Array.isArray(f?.items))continue;const m=f.items.filter($=>n.has(`${p.slug}/${$.id}`)&&$.lane==="done");if(m.length){for(const $ of m){const y=n.get(`${p.slug}/${$.id}`);i.push({...y,file:p.slug,archived_at:a,item:y.prs?Tr($,y.prs):$})}o.push([p.file,{...f,items:f.items.filter($=>!m.includes($))}])}}if(!i.length)return[];const s=await g(L(e),{items:[]}),c=new Map(i.map(p=>[`item:${p.key}`,p]));for(const p of i)p.questions=(s.items||[]).filter(f=>f.topic===`item:${p.key}`&&f.resolved);const l=await Pe(e);for(const p of i)p.asks=p.item.from_ask&&ae(l,p.item.from_ask)===void 0?[p.item.from_ask]:[];const u=new Set(i.map(p=>p.key));await Dt(e,[...(await Fe(e)).filter(p=>!u.has(p.key)),...i.map(({key:p,file:f,archived_at:m,closed_at:$,item:y,questions:S,asks:E})=>({key:p,file:f,archived_at:m,closed_at:$,item:y,questions:S,asks:E}))]),i.some(p=>p.asks.length)&&await b(ke(e),{links:{...l,...Object.fromEntries(i.flatMap(p=>p.asks.map(f=>[f,p.key])))}});for(const[p,f]of o)await b(p,f);const w=(s.items||[]).filter(p=>!(c.has(p.topic)&&p.resolved));return w.length!==(s.items||[]).length&&await b(L(e),{...s,items:w}),[...u]}),"archiveCards");function Tr(e,t){if(!Be(e))return e;const n=je(e).map(o=>{const s=t.find(c=>c.state&&c.number===o.number&&(!o.repo||o.repo===c.repo));return s?{...o,kept:{checks:s.checks,reviews:s.reviews,codexOpen:s.codexOpen}}:o}),{pr:a,...i}=e;return{...i,prs:n}}r(Tr,"keptPRs");async function Cr(e,t){return!e||!ce(on(e))?!1:At(e,async()=>{const n=await Fe(e),a=n.find(p=>p.key===t);if(!a)return!1;const i=(await M(e)).find(p=>p.slug===a.file),o=i&&await g(i.file,null);if(!Array.isArray(o?.items))return!1;o.items.some(p=>p.id===a.item.id)||await b(i.file,{...o,items:[...o.items,a.item]});const s=await g(L(e),{items:[]}),c=new Set((s.items||[]).map(p=>p.id)),l=(a.questions||[]).filter(p=>!c.has(p.id));l.length&&await b(L(e),{...s,items:[...s.items||[],...l]});const u=await Pe(e),w=(a.asks||[]).filter(p=>ae(u,p)===t);return w.length&&await b(ke(e),{links:Object.fromEntries(Object.entries(u).filter(([p])=>!w.includes(p)))}),await Dt(e,n.filter(p=>p!==a)),!0})}r(Cr,"restoreArchived");const Qi=["push_tag","no_change"],Gi=r(e=>Object.fromEntries(Object.entries(e).filter(([t])=>!Qi.includes(t))),"stripQuestion"),Vi=r(({summary:e,...t})=>t,"stripItem"),Zi=r(e=>Qi.some(t=>t in e),"holdsDead"),cd=r(e=>At(e,async()=>{for(const a of await M(e)){const i=await g(a.file,null);Array.isArray(i?.items)&&i.items.some(o=>"summary"in o)&&await b(a.file,{...i,items:i.items.map(Vi)})}const t=await g(L(e),null);(t?.items||[]).some(Zi)&&await b(L(e),{...t,items:t.items.map(Gi)});const n=await Fe(e);n.some(a=>"card"in a||"summary"in a.item||(a.questions||[]).some(Zi))&&await Dt(e,n.map(({card:a,...i})=>({...i,...a?{closed_at:i.closed_at||a.sortAt}:{},item:Vi(i.item),questions:(i.questions||[]).map(Gi)})))}),"stripDeadFields");async function ld(){return Lo(async()=>{const e=Eo(),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 we(e))await qr(i),n.push(a);return e.migrated=[...t,"wave-names"],await Io(e),n})}r(ld,"migrateWaveNames");const Yi=r(async e=>JSON.parse(await O.readFile(e,"utf8")),"readPlain"),sa=r((e,t)=>Te(e,JSON.stringify(t,null,2)+`
|
|
28
|
+
`),"writePlain"),Xi=r(e=>typeof e!="string"?e:e.replace(/^wave:([a-z0-9-]+)$/,"file:$1").replace(/^lineup:([a-z0-9-]+)$/,"wave:$1"),"renamedTopic"),ra=r(e=>Array.isArray(e)?e.map(ra):e&&typeof e=="object"?Object.fromEntries(Object.entries(e).map(([t,n])=>[Xi(t),ra(n)])):Xi(e),"renameTopics");async function qr(e){const t=h.join(e,"board.json"),n=await Yi(t).catch(()=>null);if(n&&typeof n.items=="string"&&n.items!=="waves"&&h.basename(n.items)==="wave.json"){const i=h.join(h.dirname(n.items),"items.json");ce(h.resolve(e,n.items))&&await O.rename(h.resolve(e,n.items),h.resolve(e,i)),await sa(t,{...n,items:i,slug:n.slug||"wave"})}for(const i of await M(e)){const o=await Yi(i.file).catch(()=>null);if(!Array.isArray(o?.lineups))continue;const{lineups:s,...c}=o;await sa(i.file,{...c,waves:[...c.waves||[],...s]});for(const{slug:l}of s)await O.rename(h.join(i.dir,`lineup-${l}.md`),h.join(i.dir,ca(l))).catch(u=>{if(u.code!=="ENOENT")throw u})}const a=h.join(e,"data");for(const i of(await O.readdir(a).catch(()=>[])).filter(o=>o.endsWith(".json"))){const o=h.join(a,i),s=await O.readFile(o,"utf8");let c;try{c=JSON.parse(s)}catch{continue}c=ra(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,...w})=>u?{...w,wave:l}:{...w,file:l})),JSON.stringify(c)!==JSON.stringify(JSON.parse(s))&&await sa(o,c)}}r(qr,"renameWaveNames");const ca=r(e=>`wave-${e}.md`,"waveNoteFile"),Mr=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 ut(e,t){for(const n of await M(e)){const a=await g(n.file,null),i=(a?.waves||[]).find(o=>o.slug===t);if(i)return{source:n,itemFile:a,wave:i}}return d(`this board has no wave "${t}"; wave create makes one`)}r(ut,"findWave");async function Wr(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 Ce(e,t)}r(Wr,"triagedFile");function la(e,t,n,a){return n.map(i=>{const{file:o,id:s}=P(i);o!==e.slug&&d(`${i} is not in item file "${e.slug}"; a wave's cards share one file`),((t.items.find(u=>u.id===s)||d(`item file "${o}" has no item "${s}"`)).deps||[]).includes(`wave:${a}`)&&d(`${i} waits on the wave "${a}", so it can't be in it`);const l=(t.waves||[]).find(u=>u.slug!==a&&!u.finished_at&&Ne(u).includes(s));return l&&d(`${i} is already in the wave "${l.slug}"; wave remove ${l.slug} ${i} first`),s})}r(la,"waveCards");const da=r(e=>[...new Set(String(e??"").split(",").map(t=>t.trim()).filter(Boolean))],"cardList");function Ur(e){const t=[e??[]].flat().map(n=>{const[a,i]=String(n).split(":");return[a.trim(),da(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(Ur,"cardSpecs");function rn(e,t,n,a,i){const o=n.items.indexOf(a),s=i.map(u=>{const w=H(e.slug,u),{file:p,id:f}=P(w),m=p===e.slug?n.items.indexOf(f):-1;return(m<0||m>=o)&&d(`${w} is not an earlier card of the wave "${n.slug}"`),f}),c=t.items.find(u=>u.id===a),l=[...(c.deps||[]).filter(u=>!eo(e,n,u)),...s];l.length?c.deps=l:delete c.deps}r(rn,"relink");const eo=r((e,t,n)=>Ne(t).some(a=>H(e.slug,n)===`${e.slug}/${a}`),"inWave");async function to(e,t,n,a,i){i!==void 0&&await Te(h.join(t.dir,ca(a.slug)),`${_(i,"the note").replace(/\n*$/,`
|
|
29
|
+
`)}`),await b(t.file,n);const{tickets:o}=be(await ie(e));return{file:t.file,lines:[`order: ${no(t,n,a,o)}`]}}r(to,"writeWave");function no(e,t,n,a){const i=r(o=>Ve(t,t.items.find(s=>s.id===o)||{id:o},a),"label");return Ne(n).map(o=>{const s=(t.items.find(c=>c.id===o)?.deps||[]).filter(c=>eo(e,n,c));return s.length?`${i(o)} (after ${s.map(c=>i(P(H(e.slug,c)).id)).join(", ")})`:i(o)}).join(", ")}r(no,"orderLine");async function dd(e,t,{title:n,goal:a,cards:i,note:o}){Mr(t);const s=Ur(i);o===void 0&&d("wave create needs the wave note, as --note <text> or --file <path>");const c=await M(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(m=>m.slug===t)&&d(`this board already has a wave "${t}"`);const l=c.find(f=>f.slug===P(s[0][0]).file)||d(`this board has no item file "${P(s[0][0]).file}"`),{itemFile:u}=await Ce(e,l.slug),w=la(l,u,s.map(([f])=>f),t);new Set(w).size!==w.length&&d("--card names a card twice");const p={slug:t,title:_(n,"--title"),goal:_(a,"--goal"),items:w,created:new Date().toISOString()};return s.forEach(([,f],m)=>rn(l,u,p,w[m],f)),u.waves=[...u.waves||[],p],to(e,l,u,p,o)}r(dd,"createWave");async function ud(e,t,{add:n,remove:a,card:i,after:o,title:s,goal:c,note:l}){[n,a,i,o,s,c,l].every(f=>f===void 0)&&d("nothing to change: pass a card with --after, --title, --goal, --note or --file"),o!==void 0&&n===void 0&&i===void 0&&d(`--after goes with a card: wave set ${t} <file>/<id> --after <file>/<id>,\u2026`),i!==void 0&&o===void 0&&d(`wave set ${t} ${i} needs --after <file>/<id>,\u2026 ("" clears its links)`);const{source:u,itemFile:w,wave:p}=await ut(e,t);if(p.finished_at&&d(`the wave "${t}" is finished`),p.items=Ne(p),delete p.phases,n!==void 0){const[f]=la(u,w,[n],t);p.items.includes(f)&&d(`${n} is already in the wave "${t}"`),p.items.push(f),rn(u,w,p,f,da(o))}if(i!==void 0){const{file:f,id:m}=P(i);(f!==u.slug||!p.items.includes(m))&&d(`${i} is not in the wave "${t}"`),rn(u,w,p,m,da(o))}if(a!==void 0){const[f]=la(u,w,[a],t);p.items.includes(f)||d(`${a} is not in the wave "${t}"`),p.items.length===1&&d(`${a} is the wave's last card; wave finish ${t} ends the wave`),rn(u,w,p,f,[]);for(const m of w.items.filter($=>p.items.includes($.id)&&$.deps))m.deps=m.deps.filter($=>H(u.slug,$)!==`${u.slug}/${f}`),m.deps.length||delete m.deps;p.items=p.items.filter(m=>m!==f)}return s!==void 0&&(p.title=_(s,"--title")),c!==void 0&&(p.goal=_(c,"--goal")),to(e,u,w,p,l)}r(ud,"changeWave");async function fd(e,t){const{source:n,itemFile:a,wave:i}=await ut(e,t),o=a.items.filter(s=>Ne(i).includes(s.id)&&!G(s));if(o.length){const s=await Promise.all(o.map(c=>J(e,`${n.slug}/${c.id}`)));d(`the wave "${t}" still has ${s.join(", ")} open; wave finish waits until every card in it is Done`)}return i.finished_at=new Date().toISOString(),await b(n.file,a),{file:n.file}}r(fd,"finishWave");async function pd(e,t,n){const{source:a,itemFile:i,wave:o}=await ut(e,t),s=await O.readFile(h.join(a.dir,ca(t)),"utf8").catch(()=>"");return{title:o.title,markdown:`**Goal** ${o.goal}
|
|
31
30
|
|
|
32
|
-
**Order** ${
|
|
31
|
+
**Order** ${no(a,i,o,n)}
|
|
33
32
|
|
|
34
|
-
${s}`}}r(rd,"readWaveNotes");async function C(e,t,n,a="main"){const{id:i}=N(t);/^[\w-]{1,40}$/.test(a)||d(`--agent takes a name of letters, digits, "_" and "-", not "${a}"`);const o=a==="main"?`${i}.json`:`${i}@${a}.json`;if(n)return h.join(h.resolve(n),"agents",o);const{source:s}=await x(e,t);return h.join(s.dir,"agents",o)}r(C,"builderFile");const Gi=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"),Br=wn.progress,Zi=80,Vi="reading, building, testing, checking or landing";function Yi(e,t,n,a){e||d(`${a} needs --site, since the line goes to the item's thread`),t!==void 0&&!Br.includes(t)&&d(`--phase takes ${Vi}, not "${t}"`);const i=S(n,a);return i.length>Zi&&d(`${a} is ${i.length} characters; the cap is ${Zi}. Say it shorter.`),{phase:t,detail:i}}r(Yi,"progressLine");const Xi=r((e,t,n)=>t?{detail:e.detail}:(e.phase===void 0&&d(`${n} needs --phase ${Vi}`),e),"forAgent");async function oa(e,t,n="main",a,{phase:i,detail:o},s){const l=Ee(a),c=l||n==="main"?"":a.kind===se?n:It(a)||a.kind||n,u=l?xo[a.kind]:[c,i].filter(Boolean).join(" "),w=new RegExp(`^(?:${u}|${i??u})\\s*:?\\s+`,"i"),p=u?`${u[0].toUpperCase()}${u.slice(1)}: ${o.replace(w,"")}`:o,f=Y(e,t);(await K(f)).filter(m=>m.kind==="status").pop()?.text!==p&&await me(f,{ts:s,who:"claude",kind:"status",run:a.started,...i?{phase:i}:{},...l?{planner:!0}:{},text:p})}r(oa,"statusToThread");const Tr={prototype:"sonnet",grill:"opus",[pe]:"sonnet"};async function St(e,t,{label:n,phase:a,progress:i,dir:o,agent:s="main",kind:l,model:c,eta:u,worktree:w,ownWorktree:p,dispatch:f}){a!==void 0&&i===void 0&&d('--phase goes with --progress "<detail>"');const m=X(i,te=>Yi(e,a,te,"--progress")),$=await C(e,t,o,s),y=new Date().toISOString(),_=X(l,te=>/^[\w.-]{1,30}$/.test(te)?te:d("--kind takes a short word such as claude or codex")),D=ft(_),P=c??(_==="opus"||_==="sonnet"?_:void 0),B=["build",...Kr].includes(D),F=await g($,null),I=r(te=>["tap","hub"].includes(te?.dispatched)&&!te.done,"tapOpen");if(f==="session"&&I(F))return{file:$,alreadyOpen:!0,lines:[]};s!=="main"&&_!==pe&&_!==se&&I(await g(await C(e,t,o),null))&&d(`the Implement tap opened ${t}'s main builder record and no agent has taken it up; run builder start ${t} without --agent first`);const v=F?.dispatched&&!F.done?F:null,E=B&&!o?(await x(e,t)).item:void 0,T=F&&We(F).length?F:null,q=v&&D==="build"?{kind:ft(v.kind),model:v.model,reason:{tap:"The owner's Build tap picked it.",hub:"The board's pull picked it."}[v.dispatched]??"The session's dispatch picked it."}:B?kn({card:E??{}}):null;q&&D!=="build"&&D!==q.kind&&d(`--kind ${D}: ${q.kind} builds now. ${q.reason} Pass --kind ${q.kind}, or --kind build.`);const O=q?q.kind:D,z=ya({dir:e});if(!(v&&!f)&&Fo(z,O))throw new re(Do(z,O));const ie=m&&Xi(m,Ee({kind:O}),"--progress");c!==void 0&&!["opus","sonnet"].includes(c)&&d("--model takes opus or sonnet"),P!==void 0&&O==="codex"&&d("--model is for kind claude; codex needs none");const ee=O==="claude"?bn():"auto";P!==void 0&&ee!=="auto"&&P!==ee&&d(`--model ${P}: the hub's Claude model is ${ee==="opus"?"Opus":"Sonnet"}. Pass --model ${ee}, or no --model.`);const J=O==="claude"?P??q?.model??(v&&ft(v.kind)==="claude"?v.model:void 0)??"opus":Ee({kind:O})?P??(v?.kind===O?v.model:void 0)??Tr[O]:O===se?P??F?.model:void 0,ue={label:n===void 0?"Builder":S(n,"--label"),...O?{kind:O}:{},...J?{model:J}:{},started:y,...u===void 0?{}:{eta:Gi(u,"--eta")},...f?{dispatched:f}:{},...T?{calls:T.calls,...T.verified?{verified:T.verified}:{}}:{},...Hr(F)?{session:F.session}:{},...O===se&&F?.kind===se?{label:F.label,task:F.task,writes:F.writes,requested:F.requested}:{}},jt=B&&!o&&!G(E)&&E.lane!=="progress",st=jt?await he(e,t,{start:!0}):{lines:[]};let fe,Fe=null;const Ot=p&&B&&s==="main"?[F?.worktree,E?.worktree].find(te=>te?.path&&ce(te.path))??null:null,le=(v??T)?.worktree??Ot,Pt=w??(B&&s==="main"&&!le?p:void 0);try{if(Pt){const te=await H(h.dirname(h.dirname($)),N(t).id),At=(await Promise.all(te.filter(ne=>ne!==$).map(ne=>g(ne,null)))).filter(ne=>ne?.worktree?.path&&!ne.done).map(ne=>ne.worktree.path);({remade:Fe,...fe}=await Pt({busy:At}))}else fe=le}catch(te){throw jt&&await Jr(e,t,E),te}await Cr($,F),await k($,fe?{...ue,worktree:fe}:ue),E?.rebase_run&&s==="main"&&![eo,$a].includes(ue.label)&&await to(e,t),q?.inputs&&e&&Jo(xa(e),{call:"builder-pick",key:t,result:[O,O==="claude"?J:null].filter(Boolean).join(" "),reason:q.reason,inputs:{...q.inputs,...P?{askedModel:P}:{},...f?{dispatch:f}:{}}}),Fe&&e&&await W(e,t,"worktree-remade"),ie&&await oa(e,t,s,ue,ie,y);const on=_===pe||_===se?[]:await Kt(e,t,"Acted on: builder started");return e&&B&&await cr(e,t),{file:$,kind:O??null,model:J??null,reason:q?.reason??null,worktree:fe?.path??null,lines:[...D==="build"?[`kind: ${O}. ${q.reason}`]:[],...It(ue)?[`model: ${It(ue)}`]:[],...e?[`ref: ${await Q(e,t)}`]:[],...fe?[`worktree: ${fe.path}`]:[],...Fe?[Fe]:[],...st.lines,...on]}}r(St,"startBuilder");async function Cr(e,t){if(!t?.started||!t.done||!e.endsWith(".json")||h.basename(e).includes("@"))return;const{label:n,kind:a,model:i,started:o,done:s,session:l}=t,c=h.join(h.dirname(e),`${h.basename(e,".json")}@past-${Date.parse(o)}.json`);await k(c,{label:n,...a?{kind:a}:{},...i?{model:i}:{},started:o,done:s,...l?.log?{session:{log:l.log}}:{}})}r(Cr,"keepPastRun");async function ld(e,t,n,a="tap"){try{const{held:i,blockers:o}=await Mr(e,t);if(i&&await Fi(e,"card-hold",{key:t,hold:!1}),o.length)return await Wr(e,t,o);try{const s=await St(e,t,{kind:"build",worktree:n,dispatch:a});return{key:t,ref:await Q(e,t),kind:s.kind,...s.model?{model:s.model}:{},reason:s.reason,worktree:s.worktree}}catch(s){throw i&&await zr(e,t,i),s}}catch(i){if(!(i instanceof re))throw i;return await W(e,t,ga,{why:i.message}),null}}r(ld,"startTappedBuild");async function cd(e,t,{pr:n,conflict:a,base:i},o){const{source:s,itemFile:l,item:c}=await x(e,t);c.rebase_run={pr:n,base:i,conflict:a,at:new Date().toISOString()},await k(s.file,l);try{const u=await St(e,t,{kind:"build",label:eo,dispatch:"hub",ownWorktree:o});return{key:t,kind:u.kind,...u.model?{model:u.model}:{},worktree:u.worktree}}catch(u){throw await to(e,t),u}}r(cd,"startRebase");const eo="Rebase";async function to(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);delete i.rebase_run,await k(n.file,a)}r(to,"endRebaseRun");async function dd(e,t){const{itemFile:n,item:a}=await x(e,t),i=oe(e,n);return Ie(a,i).find(o=>o.repo===i)?.number??null}r(dd,"cardPR");async function ud(e,t,{from:n,to:a,replayed:i}){const{source:o,itemFile:s,item:l}=await x(e,t);return l.rebase_run?(l.rebased=[...l.rebased||[],{from:n,to:a,base:l.rebase_run.base,replayed:i,at:new Date().toISOString()}],delete l.rebase_run,await k(o.file,s),!0):!1}r(ud,"recordRebase");const qr=r((e,t,n)=>{const a=new Map((e.rebased||[]).filter(i=>i.replayed).map(i=>[i.to,i.from]));for(let i=n,o=0;i&&o<=a.size;i=a.get(i),o++)if(i===t)return!0;return!1},"onlyRebased");async function Mr(e,t){const n=ya({dir:e});Ro(n)&&d(Ao(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&&Ae(a)&&d("it's held. Unhold it, then tap Implement."),await Gn(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 mr(e,t,a)}}r(Mr,"tapRefusals");async function fd(e,t,n){await Ce(e,t,{who:"love",by:n,command:"implement"}),await or(e,t,{})}r(fd,"implementAsIs");async function Wr(e,t,n){const a=n.join(", ");return await ge({dir:e},"card-bump",{key:t}),await W(e,t,"cant-start-yet",{names:a}),{key:t,ref:await Q(e,t),bumped:n,line:`bumped: ${t} waits on ${a}, so it stays in Todo, bumped to the top`}}r(Wr,"bumpBlocked");async function nn(e,t){const n=await C(e,t),a=await g(n,null);return a?.started&&!a.done&&ze(a)?{file:n,record:a}:null}r(nn,"openBuild");async function no(e,t,{cause:n,words:a,by:i,until:o}){const s=await nn(e,t);return!s||s.record.pause?!1:(await k(s.file,{...s.record,pause:{cause:n,words:a,at:new Date().toISOString(),...i?{by:i}:{},...o?{until:o}:{}}}),!0)}r(no,"requestPause");async function Ur(e,t){const n=await nn(e,t),a=n?.record.pause;if(!a||a.paused)return a??null;const i=new Date().toISOString(),o=(await K(Y(e,t))).filter(l=>l.kind==="status"&&l.phase&&!l.planner&&String(l.ts)>=String(n.record.started)).pop()?.phase,s={...a,paused:i,...o?{phase:o}:{}};return await k(n.file,{...n.record,pause:s}),await W(e,t,"paused",{phase:o,words:a.words,clear:ba[a.cause][0]}),s}r(Ur,"markPaused");async function an(e,t,{dir:n,agent:a="main"}){if(!e||n||a!=="main")return[];const i=await Ur(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(an,"pauseReply");async function pd(e,t){const n=await nn(e,t),a=n?.record.pause;if(!a?.paused)return null;const{pause:i,...o}=n.record;return await k(n.file,o),await W(e,t,"resumed",{clear:ba[a.cause][1]}),{pause:a,record:o}}r(pd,"resumePaused");async function wd(e,t){const n=await nn(e,t);if(!n?.record.pause||n.record.pause.paused)return;const{pause:a,...i}=n.record;await k(n.file,i)}r(wd,"dropPause");async function md(e,t,n){const a=await C(e,t),i=await g(a,null),o=new Date().toISOString();i&&!i.done&&await k(a,{...i,done:o});const{source:s,itemFile:l,item:c}=await x(e,t);c.lane==="progress"&&Zn(c,"todo",o),await k(s.file,l),await W(e,t,ga,{why:n})}r(md,"abandonLaunch");async function hd(e,t,[n,a],i="main"){const o=await C(e,t,void 0,i),s=await g(o,null);s&&!s.done&&await k(o,{...s,done:new Date().toISOString()}),await tt(e,t),await W(e,t,n,a)}r(hd,"abandonPlanner");async function gd(e,t,n,a,i="main",o=null){const s=await C(e,t,void 0,i),l=await g(s,null);await k(s,{...l,session:{id:n,pid:a,log:o,at:new Date().toISOString()}})}r(gd,"recordSession");async function $d(e,t){const{source:n}=await x(e,t);return(await Promise.all((await H(n.dir,N(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($d,"openAgents");async function kd(e,t){const{source:n}=await x(e,t);return(await Promise.all((await H(n.dir,N(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(kd,"openRuns");async function bd(e,t,n){const{source:a}=await x(e,t),i=new Date().toISOString();for(const o of await H(a.dir,N(t).id)){const s=await g(o,null);s?.started&&!s.done&&ze(s)&&await k(o,{...s,stopped:{run:n,at:i}})}}r(bd,"markStopped");async function yd(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);i.merge_ready&&(i.merge_ready.stopped=new Date().toISOString(),await k(n.file,a))}r(yd,"stopHeldMerge");async function xd(e,t,n){const a=S(n,"item stall --reason"),{source:i,itemFile:o,item:s}=await x(e,t);return s.lane==="done"&&d(`${await Q(e,t)} is done; there is nothing to stall`),s.stalled={reason:a,at:new Date().toISOString()},await k(i.file,o),a}r(xd,"markStalled");async function _d(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);i.stalled&&(delete i.stalled,await k(n.file,a))}r(_d,"clearStall");async function Sd(e,t){const{item:n}=await x(e,t),a=await K(h.join(e,"data","events.jsonl")),i=r(l=>a.filter(c=>c.kind===l&&c.data?.key===t).map(c=>String(c.ts)).sort().pop()||"","latest"),o=i("card-bump");if(!o||i("card-unbump")>o)return null;const s=[n.closed_at,...Se(n).map(l=>l.merged_at)].filter(Boolean).map(String).sort().pop()||"";return o>s?o:null}r(Sd,"landFirst");async function vd(e,t,n){const{item:a}=await x(e,t);G(a)&&d(`${await Q(e,t)} is done; there is nothing to bump`);const i=await ge({dir:e},n?"card-bump":"card-unbump",{key:t});return(await K(h.join(e,"data","events.jsonl"))).find(o=>o.id===i).ts}r(vd,"bumpCard");async function ao(e,t){return(await K(Y(e,t))).filter(n=>n.kind==="status").pop()?.phase==="landing"}r(ao,"isLanding");const Hr=r(e=>!!e?.session&&e.session.at>(e.done??e.started),"launchedSession");async function jd(e,t,n){const a=await g(await C(e,t),null);if(a&&!a.done)return null;const i=await St(e,t,{kind:n,dispatch:"hub"});return{key:t,kind:n,model:i.model}}r(jd,"startPlanner");async function Od(e,t,n){const{source:a}=await x(e,t);if((await Promise.all((await H(a.dir,N(t).id)).map(l=>g(l,null)))).some(l=>l?.kind===pe&&l.started&&!l.done))return null;const o=vo(n);if(await g(await C(e,t,void 0,o),null))return null;const s=await St(e,t,{kind:pe,agent:o,dispatch:"hub"});return{key:t,kind:pe,model:s.model,agent:o}}r(Od,"startVisual");const sa=r(e=>typeof e=="string"&&/^[a-z][a-z0-9-]{0,29}$/.test(e)&&e!=="main"&&!e.startsWith(`${pe}-`)?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 ra(e,t){const{item:n}=await x(e,t);return[(await g(await C(e,t),null))?.worktree,n.worktree].find(i=>i?.path&&ce(i.path))??null}r(ra,"cardWorktree");async function Pd(e,t,{name:n,task:a,writes:i,model:o}){A(e);const s=sa(n),l=S(a,"--task");o!==void 0&&!["opus","sonnet"].includes(o)&&d("--model takes opus or sonnet");const c=bn();o!==void 0&&c!=="auto"&&o!==c&&d(`--model ${o}: the hub's Claude model is ${c==="opus"?"Opus":"Sonnet"}. Pass --model ${c}, or no --model.`);const{item:u}=await x(e,t);G(u)&&d(`${await Q(e,t)} is Done; a helper needs an open card`);const w=await C(e,t,void 0,s);return ce(w)&&d(`${t} already has an agent named ${s}; pick another --name`),i&&!await ra(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 k(w,{label:s,kind:se,task:l,writes:!!i,...o?{model:o}:{},requested:new Date().toISOString()}),await W(e,t,"helper-asked",{agent:s,writes:i}),{lines:[`helper ${s} requested: the board launches it ${i?"once an implementer slot is free":"now"}`,`wait for it with agent wait ${t} --name ${s}`]}}r(Pd,"requestHelper");async function Ad(e,t,n,a){const i=await C(e,t,void 0,n),o=await g(i,null);if(o?.kind!==se||o.started||o.done)return null;const{item:s}=await x(e,t),l=kn({card:s}),c=bn(),u=o.model??(c!=="auto"?c:l.kind==="claude"?l.model:"opus"),w=o.writes?await ra(e,t):null;o.writes&&!w&&d(`${t} has no worktree for helper ${n} to branch from`),await k(i,{...o,model:u});const p=await St(e,t,{agent:n,kind:se,dispatch:"hub",worktree:w?({busy:f})=>a({repo:w.repo,base:w.branch,busy:f}):void 0});return{key:t,kind:se,model:u,agent:n,task:o.task,writes:o.writes,worktree:p.worktree}}r(Ad,"startHelper");async function Dd(e,t,n){const a=await g(await C(e,t,void 0,sa(n)),null);return a?.kind!==se&&d(`${t} has no helper named ${n}`),{record:a,card:await ra(e,t)}}r(Dd,"helperOf");async function Fd(e,t,{name:n,agent:a="main",timeout:i=9,every:o=2e3,alive:s}){A(e);const l=Number(i);l>0||d("--timeout takes minutes, a number above 0");const c=await C(e,t,void 0,sa(n)),u=await C(e,t,void 0,a),w=r(async f=>{const m=await g(u,null);if(!m?.started||m.done)return;const{waits:$,...y}=m;await k(u,f?{...y,waits:f}:y)},"mark"),p=Date.now()+l*6e4;await w(n);try{for(;;){const f=await g(c,null);if(f?.kind!==se&&d(`${t} has no helper named ${n}`),f.done){const $=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"}`,...$]}}const m=f.session?.pid??null;m&&!s(m)&&d(`helper ${n}'s process exited without builder done; do its task yourself`),Date.now()>=p&&d(`helper ${n} is still ${f.started?"running":f.writes?"waiting for an implementer slot":"waiting to launch"} after ${l} minutes; run agent wait again`),await new Promise($=>setTimeout($,o))}}finally{await w(null)}}r(Fd,"waitHelper");async function Rd(e,t,n="main"){return n==="main"?!1:(await g(await C(e,t,void 0,n),null))?.kind===se}r(Rd,"isHelper");async function Nd(e,t,n="main"){const a=await g(await C(e,t,void 0,n),null);return!!(a?.started&&!a.done&&Ee(a))}r(Nd,"isPlanner");async function Ed(e,t,{file:n,title:a}){A(e);const i=S(a,"--title");(typeof n!="string"||!n.endsWith(".html"))&&d("--file takes the page, an .html file");const o=await j.readFile(h.resolve(n),"utf8").catch(()=>d(`--file ${n} can't be read`));o.trim()||d(`--file ${n} is empty`),await x(e,t);const{id:s}=N(t),l=h.join(e,"public","visuals",s);await j.mkdir(l,{recursive:!0});const c=i.toLowerCase().replace(/[^a-z0-9]+/g,"-").slice(0,60).replace(/^-+|-+$/g,"")||"visual";let u=`${c}.html`;for(let p=2;ce(h.join(l,u));p++)u=`${c}-${p}.html`;await j.writeFile(h.join(l,u),o,{flag:"wx"});const w=`visuals/${s}/${u}`;return await me(Y(e,t),{ts:new Date().toISOString(),who:"claude",kind:pe,text:S(i,"--name"),link:On(w,"--link")}),{lines:[w]}}r(Ed,"addVisual");async function Id(e){const t=[];for(const n of await M(e)){const a=await g(n.file,null);for(const i of a?.items||[]){if(G(i))continue;const o=await g(h.join(n.dir,"agents",`${i.id}.json`),null);!o?.started||o.done||!o.dispatched||Ee(o)||t.push({key:`${n.slug}/${i.id}`,kind:ft(o.kind),model:o.model})}}return t}r(Id,"cutOffBuilds");async function zr(e,t,n){const{source:a,itemFile:i,item:o}=await x(e,t);for(const s of Object.keys(n))n[s]===void 0?delete o[s]:o[s]=n[s];await k(a.file,i)}r(zr,"restoreHold");async function Jr(e,t,n){const{source:a,itemFile:i,item:o}=await x(e,t);for(const s of["lane","lane_at","closed_at","reopened_at"])n[s]===void 0?delete o[s]:o[s]=n[s];await k(a.file,i)}r(Jr,"restoreLane");async function Ld(e,t){return e||d("audit reads a board: pass --site <route>"),t!==void 0&&N(t),{lines:Ko(Qo(xa(e),{key:t}))}}r(Ld,"auditLog");const Kr=["codex","claude"];async function Bd(e,t){const{item:n}=await x(e,t),{kind:a,model:i,reason:o}=kn({card:n});return{lines:[`${[a,i].filter(Boolean).join(" ")}: ${o}`]}}r(Bd,"pickBuilder");async function io(e,t,{dir:n}={}){const{id:a}=N(t),i=h.dirname(await C(e,t,n)),o=await H(h.dirname(i),a);return(await Promise.all(o.map(l=>g(l,null)))).filter(l=>l?.worktree?.path).sort((l,c)=>String(c.started).localeCompare(String(l.started)))[0]?.worktree??null}r(io,"recordedWorktree");async function Td(e,t){const{source:n}=await x(e,t),i=(await Promise.all((await H(n.dir,N(t).id)).map(o=>g(o,null)))).filter(o=>o?.session?.id&&o.worktree?.path).sort((o,s)=>String(s.started).localeCompare(String(o.started)))[0];return i?{id:i.session.id,worktree:i.worktree.path}:null}r(Td,"recordedSession");async function Cd(e,t,n,{phase:a,dir:i,agent:o="main",eta:s,clearEta:l}){s!==void 0&&l&&d("pass --eta or --clear-eta, not both");const c=Yi(e,a,n,"builder progress"),u=await C(e,t,i,o),w=await g(u,null);(!w?.started||w.done)&&d(`builder progress needs an open builder record on ${t}; run builder start first`);const p=Xi(c,Ee(w),"builder progress");return s!==void 0&&(w.eta=Gi(s,"--eta")),l&&delete w.eta,(s!==void 0||l)&&await k(u,w),await oa(e,t,o,w,p,new Date().toISOString()),{file:u,lines:await an(e,t,{dir:i,agent:o})}}r(Cd,"builderProgress");async function qd(e,t){const{source:n}=await x(e,t),a=Y(e,t);if((await K(a)).filter(s=>s.kind==="status").pop()?.phase==="landing")return;const o=(await Promise.all((await H(n.dir,N(t).id)).map(s=>g(s,null)))).filter(s=>s?.started&&ze(s)&&s.label!==$a).map(s=>s.started).sort().pop();await me(a,{ts:new Date().toISOString(),who:"claude",kind:"status",phase:"landing",text:"Landing: rebasing onto main and landing",...o?{run:o}:{}})}r(qd,"landingLine");const We=r(e=>(e?.calls||[]).filter(t=>!e.verified||t.at>e.verified.at),"unverifiedCalls");async function Md(e,t,n,{dir:a,agent:i="main"}){const o=S(n,"builder call");/\n/.test(o)&&d("builder call takes one line; record each call on its own");const s=await C(e,t,a,i),l=await g(s,null);return(!l?.started||l.done)&&d(`builder call needs an open builder record on ${t}; run builder start first`),await k(s,{...l,calls:[...l.calls||[],{at:new Date().toISOString(),text:o}]}),{file:s,lines:await an(e,t,{dir:a,agent:i})}}r(Md,"recordCall");async function vt(e,t){const n=await K(Y(e,t)),a=new Map(n.filter(i=>i.kind===Rt.answer).map(i=>[i.ask,i]));return n.filter(i=>i.kind===Rt.ask).map(i=>{const o=a.get(i.ask);return{id:i.ask,agent:i.agent||"main",text:i.text,at:i.ts,answer:o?{text:o.text,at:o.ts}:null,deferred:!!o?.deferred}})}r(vt,"builderAsks");const Qr=r(e=>we().find(t=>_e(t.dir,e))??{dir:e},"boardTarget");async function Wd(e,t,n,{agent:a="main"}){A(e);const i=S(n,"builder ask");/\n/.test(i)&&d("builder ask takes one line; ask each question on its own");const o=await g(await C(e,t,void 0,a),null);(!o?.started||o.done)&&d(`builder ask needs an open builder record on ${t}; run builder start first`);const s=`ask${(await vt(e,t)).length+1}`;return await me(Y(e,t),{ts:new Date().toISOString(),who:`${It(o)||o.kind||"Claude"} builder`,kind:Rt.ask,ask:s,agent:a,text:i}),await ge(Qr(e),"builder-ask",{key:t,ask:s,agent:a,text:i}),{id:s,lines:await an(e,t,{agent:a})}}r(Wd,"askOrchestrator");async function Ud(e,t,n,a,{deferred:i=!1}={}){A(e);const o=S(a,"--text"),s=(await vt(e,t)).find(c=>c.id===n)||d(`no ask "${n}" on ${t}; builder asks ${t} lists them`);s.answer&&d(`${n} on ${t} is already ${s.deferred?"deferred":"answered"}`);const l=new Date().toISOString();if(await me(Y(e,t),{ts:l,who:"claude",kind:Rt.answer,ask:n,...i?{deferred:!0}:{},text:o}),i){const c=await C(e,t,void 0,s.agent),u=await g(c,null);u&&await k(c,{...u,calls:[...u.calls||[],{at:l,text:`Question: ${s.text} (deferred to the owner)`}]})}return{lines:[]}}r(Ud,"answerBuilder");async function Hd(e,t,{wait:n=!1,timeout:a=9,every:i=2e3}={}){A(e);const o=Number(a);o>0||d("--timeout takes minutes, a number above 0");const s=Date.now()+o*6e4;for(;;){const l=await vt(e,t);if(!l.length)return{lines:["no asks yet"]};const c=l.filter(p=>!p.answer),u=l.flatMap(p=>[`${p.id}. ${p.text}`,` ${p.answer?`${p.deferred?"deferred":"answered"}: ${p.answer.text}`:"waiting on the orchestrator"}`]);if(!n||!c.length)return{lines:u};const w=await an(e,t,{});if(w.length)return{lines:[...u,...w]};Date.now()>=s&&d(`${c.map(p=>p.id).join(", ")} still waiting on the orchestrator after ${o} minutes; run builder asks ${t} --wait again`),await new Promise(p=>setTimeout(p,i))}}r(Hd,"listAsks");async function Gr(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 vt(e,a)).find(o=>o.id===t.ask)||d(`${n}.ask names no ask "${t.ask}" on ${a}; builder asks ${a} lists them`);i.deferred||d(`${n}.ask names ${i.id}, which the orchestrator ${i.answer?"answered":"has not deferred"}; only a deferred ask goes to the owner`)}r(Gr,"checkAskQuestion");async function zd(e,t,{dir:n,agent:a="main"}){const i=await g(await C(e,t,n,a),null),o=i?.calls||[];if(!o.length)return{lines:["no judgment calls recorded"]};const{verified:s}=i,l=We(i).length?"not verified yet":`verified ${s.at}${s.note?`: ${s.note}`:""}`;return{lines:[...o.map((c,u)=>`${u+1}. ${c.text}`),l]}}r(zd,"listCalls");async function Jd(e,t,{dir:n,agent:a="main",note:i}){const o=await C(e,t,n,a),s=await g(o,null);if(We(s).length||d(`${t} has no unverified judgment calls${a==="main"?"":` from agent ${a}`}; builder calls ${t} lists them`),e){const l=new Set((await g(L(e),{items:[]})).items?.filter(u=>u.topic===`item:${t}`&&u.ask).map(u=>u.ask)),c=(await vt(e,t)).find(u=>u.deferred&&u.agent===a&&!l.has(u.id));c&&d(`${c.id} on ${t} was deferred to the owner; add its question with "ask": "${c.id}" first`)}return await k(o,{...s,verified:{at:new Date().toISOString(),...i===void 0?{}:{note:S(i,"--note")}}}),o}r(Jd,"verifyCalls");async function Kd(e,t){const{source:n,item:a}=await x(e,t),i=r((s,l)=>[(s.options||[]).find(c=>c.id===l.choice)?.label,l.note].filter(Boolean).join(": ")||"answered","said"),o=(await de(e,t)).asked.filter(s=>s.ask&&s.answers.length).map(s=>({ask:s.ask,question:s.question,answer:i(s,s.answers.at(-1))}));return{key:t,ref:await Q(e,t),title:a.title||t,notes:a.plan?h.join(n.dir,a.plan):null,thread:Y(e,t),worktree:(await io(e,t))?.path??null,answers:o,links:await Zr(e,t),rebasePR:a.rebase_run?.pr??null}}r(Kd,"briefFacts");async function Zr(e,t){const n=new Map;for(const l of await M(e))for(const c of(await g(l.file,null))?.items||[])n.set(`${l.slug}/${c.id}`,{entry:l,it:c});const a=N(t).file,{it:i}=n.get(t),o=[...n].filter(([,l])=>(l.it.related||[]).some(c=>U(l.entry.slug,c)===t)).map(([l])=>l),s=[...i.parent?[["parent",U(a,i.parent)]]:[],...(i.deps||[]).map(l=>["depends on",U(a,l)]),...[...new Set([...(i.related||[]).map(l=>U(a,l)),...o])].map(l=>["related",l])].filter(([,l])=>n.has(l));return Promise.all(s.map(async([l,c])=>{const{entry:u,it:w}=n.get(c);return{relation:l,ref:await Q(e,c),title:w.title||c,lane:He[w.lane||"todo"],notes:w.plan?h.join(u.dir,w.plan):null,thread:Y(e,c)}}))}r(Zr,"cardLinks");async function Qd(e){const t=[];for(const n of await M(e)){const a=await g(n.file,null);for(const i of a?.items||[]){if(G(i))continue;const o=(await Promise.all((await H(n.dir,i.id)).map(u=>g(u,null)))).filter(Boolean);if(!mn(o))continue;const s=o.map(u=>u.verified?.at).filter(Boolean).sort().at(-1);if(!s||o.some(u=>We(u).length))continue;const l=`${n.slug}/${i.id}`;if((await de(e,l)).asked.some(u=>u.state==="open")||await Qt(e,l,i))continue;const c=o.filter(u=>u.worktree?.path).sort((u,w)=>String(w.started).localeCompare(String(u.started)))[0];c&&ce(c.worktree.path)&&t.push({key:l,verified:s,kind:ft(c.kind),model:c.model})}}return t}r(Qd,"landable");async function Gd(e,t){const{source:n,item:a}=await x(e,t);(await Promise.all((await H(n.dir,a.id)).map(l=>g(l,null)))).some(l=>l?.started&&!l.done)||d(`${t} has no open builder; only a card whose lander stopped has one to relaunch`),await he(e,t,{handBack:!0,next:"A lander relaunches to land the verified build"});const o=h.join(e,"data","landers.json"),s=await g(o,{});delete s[t],await j.writeFile(o+".tmp",JSON.stringify(s)+`
|
|
35
|
-
`),await
|
|
33
|
+
${s}`}}r(pd,"readWaveNotes");async function W(e,t,n,a="main"){const{id:i}=P(t);/^[\w-]{1,40}$/.test(a)||d(`--agent takes a name of letters, digits, "_" and "-", not "${a}"`);const o=a==="main"?`${i}.json`:`${i}@${a}.json`;if(n)return h.join(h.resolve(n),"agents",o);const{source:s}=await x(e,t);return h.join(s.dir,"agents",o)}r(W,"builderFile");const ao=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"),Hr=yn.progress,io=80,oo="reading, building, testing, checking or landing";function so(e,t,n,a){e||d(`${a} needs --site, since the line goes to the item's thread`),t!==void 0&&!Hr.includes(t)&&d(`--phase takes ${oo}, not "${t}"`);const i=_(n,a);return i.length>io&&d(`${a} is ${i.length} characters; the cap is ${io}. Say it shorter.`),{phase:t,detail:i}}r(so,"progressLine");const ro=r((e,t,n)=>t?{detail:e.detail}:(e.phase===void 0&&d(`${n} needs --phase ${oo}`),e),"forAgent");async function ua(e,t,n="main",a,{phase:i,detail:o},s){const c=Le(a),l=c||n==="main"?"":a.kind===re?n:Ct(a)||a.kind||n,u=c?Do[a.kind]:[l,i].filter(Boolean).join(" "),w=new RegExp(`^(?:${u}|${i??u})\\s*:?\\s+`,"i"),p=u?`${u[0].toUpperCase()}${u.slice(1)}: ${o.replace(w,"")}`:o,f=X(e,t);(await Q(f)).filter(m=>m.kind==="status").pop()?.text!==p&&await me(f,{ts:s,who:"claude",kind:"status",run:a.started,...i?{phase:i}:{},...c?{planner:!0}:{},text:p})}r(ua,"statusToThread");const Jr={prototype:"sonnet",grill:"opus",[pe]:"sonnet"};async function Ft(e,t,{label:n,phase:a,progress:i,dir:o,agent:s="main",kind:c,model:l,eta:u,worktree:w,ownWorktree:p,dispatch:f}){a!==void 0&&i===void 0&&d('--phase goes with --progress "<detail>"');const m=ee(i,te=>so(e,a,te,"--progress")),$=await W(e,t,o,s),y=new Date().toISOString(),S=ee(c,te=>/^[\w.-]{1,30}$/.test(te)?te:d("--kind takes a short word such as claude or codex")),E=bt(S),j=l??(S==="opus"||S==="sonnet"?S:void 0),F=["build",...ec].includes(E),D=await g($,null),T=r(te=>["tap","hub"].includes(te?.dispatched)&&!te.done,"tapOpen");if(f==="session"&&T(D))return{file:$,alreadyOpen:!0,lines:[]};s!=="main"&&S!==pe&&S!==re&&T(await g(await W(e,t,o),null))&&d(`the Implement tap opened ${t}'s main builder record and no agent has taken it up; run builder start ${t} without --agent first`);const v=D?.dispatched&&!D.done?D:null,B=F&&!o?(await x(e,t)).item:void 0,C=D&&Je(D).length?D:null,I=v&&E==="build"?{kind:bt(v.kind),model:v.model,reason:{tap:"The owner's Build tap picked it.",hub:"The board's pull picked it."}[v.dispatched]??"The session's dispatch picked it."}:F?jn({card:B??{}}):null;I&&E!=="build"&&E!==I.kind&&d(`--kind ${E}: ${I.kind} builds now. ${I.reason} Pass --kind ${I.kind}, or --kind build.`);const q=I?I.kind:E,N=Oa({dir:e});if(!(v&&!f)&&Co(N,q))throw new le(To(N,q));const V=m&&ro(m,Le({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 Y=q==="claude"?On():"auto";j!==void 0&&Y!=="auto"&&j!==Y&&d(`--model ${j}: the hub's Claude model is ${Y==="opus"?"Opus":"Sonnet"}. Pass --model ${Y}, or no --model.`);const K=q==="claude"?j??I?.model??(v&&bt(v.kind)==="claude"?v.model:void 0)??"opus":Le({kind:q})?j??(v?.kind===q?v.model:void 0)??Jr[q]:q===re?j??D?.model:void 0,Z={label:n===void 0?"Builder":_(n,"--label"),...q?{kind:q}:{},...K?{model:K}:{},started:y,...u===void 0?{}:{eta:ao(u,"--eta")},...f?{dispatched:f}:{},...C?{calls:C.calls,...C.verified?{verified:C.verified}:{}}:{},...Zr(D)?{session:D.session}:{},...q===re&&D?.kind===re?{label:D.label,task:D.task,writes:D.writes,requested:D.requested}:{}},Nt=F&&!o&&!G(B)&&B.lane!=="progress",dn=Nt?await ue(e,t,{start:!0}):{lines:[]};let he,xe=null;const Et=p&&F&&s==="main"?[D?.worktree,B?.worktree].find(te=>te?.path&&ce(te.path))??null:null,ft=(v??C)?.worktree??Et,pt=w??(F&&s==="main"&&!ft?p:void 0);try{if(pt){const te=await z(h.dirname(h.dirname($)),P(t).id),un=(await Promise.all(te.filter(fe=>fe!==$).map(fe=>g(fe,null)))).filter(fe=>fe?.worktree?.path&&!fe.done).map(fe=>fe.worktree.path);({remade:xe,...he}=await pt({busy:un}))}else he=ft}catch(te){throw Nt&&await Xr(e,t,B),te}await zr($,D),await b($,he?{...Z,worktree:he}:Z),B?.rebase_run&&s==="main"&&![co,Sa].includes(Z.label)&&await lo(e,t),I?.inputs&&e&&Xo(Aa(e),{call:"builder-pick",key:t,result:[q,q==="claude"?K:null].filter(Boolean).join(" "),reason:I.reason,inputs:{...I.inputs,...j?{askedModel:j}:{},...f?{dispatch:f}:{}}}),xe&&e&&await U(e,t,"worktree-remade"),V&&await ua(e,t,s,Z,V,y);const se=S===pe||S===re?[]:await Yt(e,t,"Acted on: builder started");return e&&F&&await pr(e,t),{file:$,kind:q??null,model:K??null,reason:I?.reason??null,worktree:he?.path??null,lines:[...E==="build"?[`kind: ${q}. ${I.reason}`]:[],...Ct(Z)?[`model: ${Ct(Z)}`]:[],...e?[`ref: ${await J(e,t)}`]:[],...he?[`worktree: ${he.path}`]:[],...xe?[xe]:[],...dn.lines,...se]}}r(Ft,"startBuilder");async function zr(e,t){if(!t?.started||!t.done||!e.endsWith(".json")||h.basename(e).includes("@"))return;const{label:n,kind:a,model:i,started:o,done:s,session:c}=t,l=h.join(h.dirname(e),`${h.basename(e,".json")}@past-${Date.parse(o)}.json`);await b(l,{label:n,...a?{kind:a}:{},...i?{model:i}:{},started:o,done:s,...c?.log?{session:{log:c.log}}:{}})}r(zr,"keepPastRun");async function wd(e,t,n,a="tap"){try{const{held:i,blockers:o}=await Qr(e,t);if(i&&await qi(e,"card-hold",{key:t,hold:!1}),o.length)return await Gr(e,t,o);try{const s=await Ft(e,t,{kind:"build",worktree:n,dispatch:a});return{key:t,ref:await J(e,t),kind:s.kind,...s.model?{model:s.model}:{},reason:s.reason,worktree:s.worktree}}catch(s){throw i&&await Yr(e,t,i),s}}catch(i){if(!(i instanceof le))throw i;return await U(e,t,_a,{why:i.message}),null}}r(wd,"startTappedBuild");async function md(e,t,{pr:n,conflict:a,base:i},o){const{source:s,itemFile:c,item:l}=await x(e,t);l.rebase_run={pr:n,base:i,conflict:a,at:new Date().toISOString()},await b(s.file,c);try{const u=await Ft(e,t,{kind:"build",label:co,dispatch:"hub",ownWorktree:o});return{key:t,kind:u.kind,...u.model?{model:u.model}:{},worktree:u.worktree}}catch(u){throw await lo(e,t),u}}r(md,"startRebase");const co="Rebase";async function lo(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);delete i.rebase_run,await b(n.file,a)}r(lo,"endRebaseRun");async function hd(e,t){const{itemFile:n,item:a}=await x(e,t),i=oe(e,n);return Ie(a,i).find(o=>o.repo===i)?.number??null}r(hd,"cardPR");async function gd(e,t,{from:n,to:a,replayed:i}){const{source:o,itemFile:s,item:c}=await x(e,t);return c.rebase_run?(c.rebased=[...c.rebased||[],{from:n,to:a,base:c.rebase_run.base,replayed:i,at:new Date().toISOString()}],delete c.rebase_run,await b(o.file,s),!0):!1}r(gd,"recordRebase");const Kr=r((e,t,n)=>{const a=new Map((e.rebased||[]).filter(i=>i.replayed).map(i=>[i.to,i.from]));for(let i=n,o=0;i&&o<=a.size;i=a.get(i),o++)if(i===t)return!0;return!1},"onlyRebased");async function Qr(e,t){const n=Oa({dir:e});qo(n)&&d(Bo(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&&De(a)&&d("it's held. Unhold it, then tap Implement."),await ta(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 kr(e,t,a)}}r(Qr,"tapRefusals");async function $d(e,t,n){await Me(e,t,{who:"love",by:n,command:"implement"}),await lr(e,t,{})}r($d,"implementAsIs");async function Gr(e,t,n){const a=n.join(", ");return await ge({dir:e},"card-bump",{key:t}),await U(e,t,"cant-start-yet",{names:a}),{key:t,ref:await J(e,t),bumped:n,line:`bumped: ${t} waits on ${a}, so it stays in Todo, bumped to the top`}}r(Gr,"bumpBlocked");async function cn(e,t){const n=await W(e,t),a=await g(n,null);return a?.started&&!a.done&&Ge(a)?{file:n,record:a}:null}r(cn,"openBuild");async function uo(e,t,{cause:n,words:a,by:i,until:o}){const s=await cn(e,t);return!s||s.record.pause?!1:(await b(s.file,{...s.record,pause:{cause:n,words:a,at:new Date().toISOString(),...i?{by:i}:{},...o?{until:o}:{}}}),!0)}r(uo,"requestPause");async function Vr(e,t){const n=await cn(e,t),a=n?.record.pause;if(!a||a.paused)return a??null;const i=new Date().toISOString(),o=(await Q(X(e,t))).filter(c=>c.kind==="status"&&c.phase&&!c.planner&&String(c.ts)>=String(n.record.started)).pop()?.phase,s={...a,paused:i,...o?{phase:o}:{}};return await b(n.file,{...n.record,pause:s}),await U(e,t,"paused",{phase:o,words:a.words,clear:ja[a.cause][0]}),s}r(Vr,"markPaused");async function ln(e,t,{dir:n,agent:a="main"}){if(!e||n||a!=="main")return[];const i=await Vr(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(ln,"pauseReply");async function kd(e,t){const n=await cn(e,t),a=n?.record.pause;if(!a?.paused)return null;const{pause:i,...o}=n.record;return await b(n.file,o),await U(e,t,"resumed",{clear:ja[a.cause][1]}),{pause:a,record:o}}r(kd,"resumePaused");async function bd(e,t){const n=await cn(e,t);if(!n?.record.pause||n.record.pause.paused)return;const{pause:a,...i}=n.record;await b(n.file,i)}r(bd,"dropPause");async function yd(e,t,n){const a=await W(e,t),i=await g(a,null),o=new Date().toISOString();i&&!i.done&&await b(a,{...i,done:o});const{source:s,itemFile:c,item:l}=await x(e,t);l.lane==="progress"&&na(l,"todo",o),await b(s.file,c),await U(e,t,_a,{why:n})}r(yd,"abandonLaunch");async function xd(e,t,[n,a],i="main"){const o=await W(e,t,void 0,i),s=await g(o,null);s&&!s.done&&await b(o,{...s,done:new Date().toISOString()}),await ot(e,t),await U(e,t,n,a)}r(xd,"abandonPlanner");async function _d(e,t,n,a,i="main",o=null){const s=await W(e,t,void 0,i),c=await g(s,null);await b(s,{...c,session:{id:n,pid:a,log:o,at:new Date().toISOString()}})}r(_d,"recordSession");async function Sd(e,t){const{source:n}=await x(e,t);return(await Promise.all((await z(n.dir,P(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(Sd,"openAgents");async function vd(e,t){const{source:n}=await x(e,t);return(await Promise.all((await z(n.dir,P(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(vd,"openRuns");async function jd(e,t,n){const{source:a}=await x(e,t),i=new Date().toISOString();for(const o of await z(a.dir,P(t).id)){const s=await g(o,null);s?.started&&!s.done&&Ge(s)&&await b(o,{...s,stopped:{run:n,at:i}})}}r(jd,"markStopped");async function Od(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(Od,"stopHeldMerge");async function Pd(e,t,n){const a=_(n,"item stall --reason"),{source:i,itemFile:o,item:s}=await x(e,t);return s.lane==="done"&&d(`${await J(e,t)} is done; there is nothing to stall`),s.stalled={reason:a,at:new Date().toISOString()},await b(i.file,o),a}r(Pd,"markStalled");async function Ad(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);i.stalled&&(delete i.stalled,await b(n.file,a))}r(Ad,"clearStall");async function Dd(e,t){const{item:n}=await x(e,t),a=await Q(h.join(e,"data","events.jsonl")),i=r(c=>a.filter(l=>l.kind===c&&l.data?.key===t).map(l=>String(l.ts)).sort().pop()||"","latest"),o=i("card-bump");if(!o||i("card-unbump")>o)return null;const s=[n.closed_at,...je(n).map(c=>c.merged_at)].filter(Boolean).map(String).sort().pop()||"";return o>s?o:null}r(Dd,"landFirst");async function Fd(e,t,n){const{item:a}=await x(e,t);G(a)&&d(`${await J(e,t)} is done; there is nothing to bump`);const i=await ge({dir:e},n?"card-bump":"card-unbump",{key:t});return(await Q(h.join(e,"data","events.jsonl"))).find(o=>o.id===i).ts}r(Fd,"bumpCard");async function fo(e,t){return(await Q(X(e,t))).filter(n=>n.kind==="status").pop()?.phase==="landing"}r(fo,"isLanding");const Zr=r(e=>!!e?.session&&e.session.at>(e.done??e.started),"launchedSession");async function Rd(e,t,n){const a=await g(await W(e,t),null);if(a&&!a.done)return null;const i=await Ft(e,t,{kind:n,dispatch:"hub"});return{key:t,kind:n,model:i.model}}r(Rd,"startPlanner");async function Nd(e,t,n){const{source:a}=await x(e,t);if((await Promise.all((await z(a.dir,P(t).id)).map(c=>g(c,null)))).some(c=>c?.kind===pe&&c.started&&!c.done))return null;const o=No(n);if(await g(await W(e,t,void 0,o),null))return null;const s=await Ft(e,t,{kind:pe,agent:o,dispatch:"hub"});return{key:t,kind:pe,model:s.model,agent:o}}r(Nd,"startVisual");const fa=r(e=>typeof e=="string"&&/^[a-z][a-z0-9-]{0,29}$/.test(e)&&e!=="main"&&!e.startsWith(`${pe}-`)?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 W(e,t),null))?.worktree,n.worktree].find(i=>i?.path&&ce(i.path))??null}r(pa,"cardWorktree");async function Ed(e,t,{name:n,task:a,writes:i,model:o}){A(e);const s=fa(n),c=_(a,"--task");o!==void 0&&!["opus","sonnet"].includes(o)&&d("--model takes opus or sonnet");const l=On();o!==void 0&&l!=="auto"&&o!==l&&d(`--model ${o}: the hub's Claude model is ${l==="opus"?"Opus":"Sonnet"}. Pass --model ${l}, or no --model.`);const{item:u}=await x(e,t);G(u)&&d(`${await J(e,t)} is Done; a helper needs an open card`);const w=await W(e,t,void 0,s);return ce(w)&&d(`${t} already has an agent named ${s}; 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 b(w,{label:s,kind:re,task:c,writes:!!i,...o?{model:o}:{},requested:new Date().toISOString()}),await U(e,t,"helper-asked",{agent:s,writes:i}),{lines:[`helper ${s} requested: the board launches it ${i?"once an implementer slot is free":"now"}`,`wait for it with agent wait ${t} --name ${s}`]}}r(Ed,"requestHelper");async function Ld(e,t,n,a){const i=await W(e,t,void 0,n),o=await g(i,null);if(o?.kind!==re||o.started||o.done)return null;const{item:s}=await x(e,t),c=jn({card:s}),l=On(),u=o.model??(l!=="auto"?l:c.kind==="claude"?c.model:"opus"),w=o.writes?await pa(e,t):null;o.writes&&!w&&d(`${t} has no worktree for helper ${n} to branch from`),await b(i,{...o,model:u});const p=await Ft(e,t,{agent:n,kind:re,dispatch:"hub",worktree:w?({busy:f})=>a({repo:w.repo,base:w.branch,busy:f}):void 0});return{key:t,kind:re,model:u,agent:n,task:o.task,writes:o.writes,worktree:p.worktree}}r(Ld,"startHelper");async function Id(e,t,n){const a=await g(await W(e,t,void 0,fa(n)),null);return a?.kind!==re&&d(`${t} has no helper named ${n}`),{record:a,card:await pa(e,t)}}r(Id,"helperOf");async function Bd(e,t,{name:n,agent:a="main",timeout:i=9,every:o=2e3,alive:s}){A(e);const c=Number(i);c>0||d("--timeout takes minutes, a number above 0");const l=await W(e,t,void 0,fa(n)),u=await W(e,t,void 0,a),w=r(async f=>{const m=await g(u,null);if(!m?.started||m.done)return;const{waits:$,...y}=m;await b(u,f?{...y,waits:f}:y)},"mark"),p=Date.now()+c*6e4;await w(n);try{for(;;){const f=await g(l,null);if(f?.kind!==re&&d(`${t} has no helper named ${n}`),f.done){const $=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"}`,...$]}}const m=f.session?.pid??null;m&&!s(m)&&d(`helper ${n}'s process exited without builder done; do its task yourself`),Date.now()>=p&&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($=>setTimeout($,o))}}finally{await w(null)}}r(Bd,"waitHelper");async function Td(e,t,n="main"){return n==="main"?!1:(await g(await W(e,t,void 0,n),null))?.kind===re}r(Td,"isHelper");async function Cd(e,t,n="main"){const a=await g(await W(e,t,void 0,n),null);return!!(a?.started&&!a.done&&Le(a))}r(Cd,"isPlanner");async function qd(e,t,{file:n,title:a}){A(e);const i=_(a,"--title");(typeof n!="string"||!n.endsWith(".html"))&&d("--file takes the page, an .html file");const o=await O.readFile(h.resolve(n),"utf8").catch(()=>d(`--file ${n} can't be read`));o.trim()||d(`--file ${n} is empty`),await x(e,t);const{id:s}=P(t),c=h.join(e,"public","visuals",s);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 p=2;ce(h.join(c,u));p++)u=`${l}-${p}.html`;await O.writeFile(h.join(c,u),o,{flag:"wx"});const w=`visuals/${s}/${u}`;return await me(X(e,t),{ts:new Date().toISOString(),who:"claude",kind:pe,text:_(i,"--name"),link:Rn(w,"--link")}),{lines:[w]}}r(qd,"addVisual");async function Md(e){const t=[];for(const n of await M(e)){const a=await g(n.file,null);for(const i of a?.items||[]){if(G(i))continue;const o=await g(h.join(n.dir,"agents",`${i.id}.json`),null);!o?.started||o.done||!o.dispatched||Le(o)||t.push({key:`${n.slug}/${i.id}`,kind:bt(o.kind),model:o.model})}}return t}r(Md,"cutOffBuilds");async function Yr(e,t,n){const{source:a,itemFile:i,item:o}=await x(e,t);for(const s of Object.keys(n))n[s]===void 0?delete o[s]:o[s]=n[s];await b(a.file,i)}r(Yr,"restoreHold");async function Xr(e,t,n){const{source:a,itemFile:i,item:o}=await x(e,t);for(const s of["lane","lane_at","closed_at","reopened_at"])n[s]===void 0?delete o[s]:o[s]=n[s];await b(a.file,i)}r(Xr,"restoreLane");async function Wd(e,t){return e||d("audit reads a board: pass --site <route>"),t!==void 0&&P(t),{lines:es(ts(Aa(e),{key:t}))}}r(Wd,"auditLog");const ec=["codex","claude"];async function Ud(e,t){const{item:n}=await x(e,t),{kind:a,model:i,reason:o}=jn({card:n});return{lines:[`${[a,i].filter(Boolean).join(" ")}: ${o}`]}}r(Ud,"pickBuilder");async function po(e,t,{dir:n}={}){const{id:a}=P(t),i=h.dirname(await W(e,t,n)),o=await z(h.dirname(i),a);return(await Promise.all(o.map(c=>g(c,null)))).filter(c=>c?.worktree?.path).sort((c,l)=>String(l.started).localeCompare(String(c.started)))[0]?.worktree??null}r(po,"recordedWorktree");async function Hd(e,t){const{source:n}=await x(e,t),i=(await Promise.all((await z(n.dir,P(t).id)).map(o=>g(o,null)))).filter(o=>o?.session?.id&&o.worktree?.path).sort((o,s)=>String(s.started).localeCompare(String(o.started)))[0];return i?{id:i.session.id,worktree:i.worktree.path}:null}r(Hd,"recordedSession");async function Jd(e,t,n,{phase:a,dir:i,agent:o="main",eta:s,clearEta:c}){s!==void 0&&c&&d("pass --eta or --clear-eta, not both");const l=so(e,a,n,"builder progress"),u=await W(e,t,i,o),w=await g(u,null);(!w?.started||w.done)&&d(`builder progress needs an open builder record on ${t}; run builder start first`);const p=ro(l,Le(w),"builder progress");return s!==void 0&&(w.eta=ao(s,"--eta")),c&&delete w.eta,(s!==void 0||c)&&await b(u,w),await ua(e,t,o,w,p,new Date().toISOString()),{file:u,lines:await ln(e,t,{dir:i,agent:o})}}r(Jd,"builderProgress");async function zd(e,t){const{source:n}=await x(e,t),a=X(e,t);if((await Q(a)).filter(s=>s.kind==="status").pop()?.phase==="landing")return;const o=(await Promise.all((await z(n.dir,P(t).id)).map(s=>g(s,null)))).filter(s=>s?.started&&Ge(s)&&s.label!==Sa).map(s=>s.started).sort().pop();await me(a,{ts:new Date().toISOString(),who:"claude",kind:"status",phase:"landing",text:"Landing: rebasing onto main and landing",...o?{run:o}:{}})}r(zd,"landingLine");const Je=r(e=>(e?.calls||[]).filter(t=>!e.verified||t.at>e.verified.at),"unverifiedCalls");async function Kd(e,t,n,{dir:a,agent:i="main"}){const o=_(n,"builder call");/\n/.test(o)&&d("builder call takes one line; record each call on its own");const s=await W(e,t,a,i),c=await g(s,null);return(!c?.started||c.done)&&d(`builder call needs an open builder record on ${t}; run builder start first`),await b(s,{...c,calls:[...c.calls||[],{at:new Date().toISOString(),text:o}]}),{file:s,lines:await ln(e,t,{dir:a,agent:i})}}r(Kd,"recordCall");async function Rt(e,t){const n=await Q(X(e,t)),a=new Map(n.filter(i=>i.kind===Bt.answer).map(i=>[i.ask,i]));return n.filter(i=>i.kind===Bt.ask).map(i=>{const o=a.get(i.ask);return{id:i.ask,agent:i.agent||"main",text:i.text,at:i.ts,answer:o?{text:o.text,at:o.ts}:null,deferred:!!o?.deferred}})}r(Rt,"builderAsks");const tc=r(e=>we().find(t=>ve(t.dir,e))??{dir:e},"boardTarget");async function Qd(e,t,n,{agent:a="main"}){A(e);const i=_(n,"builder ask");/\n/.test(i)&&d("builder ask takes one line; ask each question on its own");const o=await g(await W(e,t,void 0,a),null);(!o?.started||o.done)&&d(`builder ask needs an open builder record on ${t}; run builder start first`);const s=`ask${(await Rt(e,t)).length+1}`;return await me(X(e,t),{ts:new Date().toISOString(),who:`${Ct(o)||o.kind||"Claude"} builder`,kind:Bt.ask,ask:s,agent:a,text:i}),await ge(tc(e),"builder-ask",{key:t,ask:s,agent:a,text:i}),{id:s,lines:await ln(e,t,{agent:a})}}r(Qd,"askOrchestrator");async function Gd(e,t,n,a,{deferred:i=!1}={}){A(e);const o=_(a,"--text"),s=(await Rt(e,t)).find(l=>l.id===n)||d(`no ask "${n}" on ${t}; builder asks ${t} lists them`);s.answer&&d(`${n} on ${t} is already ${s.deferred?"deferred":"answered"}`);const c=new Date().toISOString();if(await me(X(e,t),{ts:c,who:"claude",kind:Bt.answer,ask:n,...i?{deferred:!0}:{},text:o}),i){const l=await W(e,t,void 0,s.agent),u=await g(l,null);u&&await b(l,{...u,calls:[...u.calls||[],{at:c,text:`Question: ${s.text} (deferred to the owner)`}]})}return{lines:[]}}r(Gd,"answerBuilder");async function Vd(e,t,{wait:n=!1,timeout:a=9,every:i=2e3}={}){A(e);const o=Number(a);o>0||d("--timeout takes minutes, a number above 0");const s=Date.now()+o*6e4;for(;;){const c=await Rt(e,t);if(!c.length)return{lines:["no asks yet"]};const l=c.filter(p=>!p.answer),u=c.flatMap(p=>[`${p.id}. ${p.text}`,` ${p.answer?`${p.deferred?"deferred":"answered"}: ${p.answer.text}`:"waiting on the orchestrator"}`]);if(!n||!l.length)return{lines:u};const w=await ln(e,t,{});if(w.length)return{lines:[...u,...w]};Date.now()>=s&&d(`${l.map(p=>p.id).join(", ")} still waiting on the orchestrator after ${o} minutes; run builder asks ${t} --wait again`),await new Promise(p=>setTimeout(p,i))}}r(Vd,"listAsks");async function nc(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 Rt(e,a)).find(o=>o.id===t.ask)||d(`${n}.ask names no ask "${t.ask}" on ${a}; builder asks ${a} lists them`);i.deferred||d(`${n}.ask names ${i.id}, which the orchestrator ${i.answer?"answered":"has not deferred"}; only a deferred ask goes to the owner`)}r(nc,"checkAskQuestion");async function Zd(e,t,{dir:n,agent:a="main"}){const i=await g(await W(e,t,n,a),null),o=i?.calls||[];if(!o.length)return{lines:["no judgment calls recorded"]};const{verified:s}=i,c=Je(i).length?"not verified yet":`verified ${s.at}${s.note?`: ${s.note}`:""}`;return{lines:[...o.map((l,u)=>`${u+1}. ${l.text}`),c]}}r(Zd,"listCalls");async function Yd(e,t,{dir:n,agent:a="main",note:i}){const o=await W(e,t,n,a),s=await g(o,null);if(Je(s).length||d(`${t} has no unverified judgment calls${a==="main"?"":` from agent ${a}`}; builder calls ${t} lists them`),e){const c=new Set((await g(L(e),{items:[]})).items?.filter(u=>u.topic===`item:${t}`&&u.ask).map(u=>u.ask)),l=(await Rt(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(o,{...s,verified:{at:new Date().toISOString(),...i===void 0?{}:{note:_(i,"--note")}}}),o}r(Yd,"verifyCalls");async function Xd(e,t){const{source:n,item:a}=await x(e,t),i=r((s,c)=>[(s.options||[]).find(l=>l.id===c.choice)?.label,c.note].filter(Boolean).join(": ")||"answered","said"),o=(await de(e,t)).asked.filter(s=>s.ask&&s.answers.length).map(s=>({ask:s.ask,question:s.question,answer:i(s,s.answers.at(-1))}));return{key:t,ref:await J(e,t),title:a.title||t,notes:a.plan?h.join(n.dir,a.plan):null,thread:X(e,t),worktree:(await po(e,t))?.path??null,answers:o,links:await ac(e,t),rebasePR:a.rebase_run?.pr??null,ticketQuestion:!a.ticket&&!!await an(e)}}r(Xd,"briefFacts");async function ac(e,t){const n=new Map;for(const c of await M(e))for(const l of(await g(c.file,null))?.items||[])n.set(`${c.slug}/${l.id}`,{entry:c,it:l});const a=P(t).file,{it:i}=n.get(t),o=[...n].filter(([,c])=>(c.it.related||[]).some(l=>H(c.entry.slug,l)===t)).map(([c])=>c),s=[...i.parent?[["parent",H(a,i.parent)]]:[],...(i.deps||[]).map(c=>["depends on",H(a,c)]),...[...new Set([...(i.related||[]).map(c=>H(a,c)),...o])].map(c=>["related",c])].filter(([,c])=>n.has(c));return Promise.all(s.map(async([c,l])=>{const{entry:u,it:w}=n.get(l);return{relation:c,ref:await J(e,l),title:w.title||l,lane:Qe[w.lane||"todo"],notes:w.plan?h.join(u.dir,w.plan):null,thread:X(e,l)}}))}r(ac,"cardLinks");async function eu(e){const t=[];for(const n of await M(e)){const a=await g(n.file,null);for(const i of a?.items||[]){if(G(i))continue;const o=(await Promise.all((await z(n.dir,i.id)).map(u=>g(u,null)))).filter(Boolean);if(!xn(o))continue;const s=o.map(u=>u.verified?.at).filter(Boolean).sort().at(-1);if(!s||o.some(u=>Je(u).length))continue;const c=`${n.slug}/${i.id}`;if((await de(e,c)).asked.some(u=>u.state==="open")||await Xt(e,c,i))continue;const l=o.filter(u=>u.worktree?.path).sort((u,w)=>String(w.started).localeCompare(String(u.started)))[0];l&&ce(l.worktree.path)&&t.push({key:c,verified:s,kind:bt(l.kind),model:l.model})}}return t}r(eu,"landable");async function tu(e,t){const{source:n,item:a}=await x(e,t);(await Promise.all((await z(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 ue(e,t,{handBack:!0,next:"A lander relaunches to land the verified build"});const o=h.join(e,"data","landers.json"),s=await g(o,{});delete s[t],await O.writeFile(o+".tmp",JSON.stringify(s)+`
|
|
34
|
+
`),await O.rename(o+".tmp",o)}r(tu,"relaunchLander");async function nu(e,t,{dir:n}={}){const{id:a}=P(t),i=await z(h.dirname(h.dirname(await W(e,t,n))),a),o=[];for(const s of i){if(!Je(await g(s,null)).length)continue;const c=h.basename(s,".json").slice(String(a).length+1)||"main";o.push(c==="main"?"":` --agent ${c}`)}o.length&&d(`land: ${t} has judgment calls nobody verified yet; check them with ${o.map(s=>`builder calls ${t}${s}`).join(", ")}, then run ${o.map(s=>`builder verified ${t}${s}`).join(", ")}`)}r(nu,"checkCallsVerified");async function au(e,t,{dir:n,agent:a="main",next:i}){const o=await g(await W(e,t,n,a),null),s=o&&!o.done?Je(o):[];if(!s.length)return null;i!==void 0&&d(`builder done sets ${t}'s Next itself while its record has judgment calls; run it without --next`);const c=s.some(u=>u.text.startsWith("Question:"))?"Answer the builder's questions":"Verify the builder's judgment calls",l=await ue(e,t,{handBack:!0,next:c});return{lines:[`handed back: Next "${c}"`,...l.lines]}}r(au,"handBackCalls");async function iu(e,t,{agent:n="main"}){if(n!=="main")return!1;const a=await W(e,t,void 0,n),i=await g(a,null);if(!i?.started||i.done||!i.worktree?.path)return!1;const{id:o}=P(t);for(const s of await z(h.dirname(h.dirname(a)),o)){const c=s===a?null:await g(s,null);if(!c?.started||c.done||c.kind==="install")continue;const l=h.basename(s,".json").slice(String(o).length+1);d(`builder done would finish ${t} while agent ${l} is still open; run builder done ${t} --agent ${l} first`)}return!0}r(iu,"finishesCard");async function ou(e,t){const{source:n,item:a}=await x(e,t);if(G(a))return null;const i=(await Promise.all((await z(n.dir,a.id)).map(s=>g(s,null)))).filter(Boolean);if(!xn(i)||i.some(s=>Je(s).length||s.started&&!s.done&&s.kind!=="install"))return null;const o=await po(e,t);return!o||!ce(o.path)?null:{waits:(await de(e,t)).asked.filter(s=>s.state==="open").map(s=>s.id)}}r(ou,"verifiedFinish");async function su(e,t){const{item:n}=await x(e,t);if(n.reopened_at&&d(`the owner reopened ${await J(e,t)}; it closes on the owner's Done`),n.lane!=="done"||je(n).some(o=>o.merged_at))return;const a=String(n.closed_at||"");(await Q(h.join(e,"data","events.jsonl"))).some(o=>o.kind==="item-message"&&o.data?.key===t&&String(o.ts)>a)&&d(`the owner reopened ${await J(e,t)} with a message; it closes on the owner's Done`)}r(su,"checkNotReopened");async function z(e,t){const n=h.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=>h.join(n,a))}r(z,"builderFiles");async function wo(e,t,n,{handedBack:a,endHandBack:i=!1,held:o=!1}={}){const s=new Date().toISOString();for(const c of await z(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,...m}=l;await b(c,m)}continue}if(o&&l.pause)continue;const{eta:u,handed_back:w,...p}=l;await b(c,{...p,done:s,...a?{handed_back:a}:{},...o&&l.started?{hold_stopped:!0}:{}})}}r(wo,"closeBuilders");async function ru(e,t){const n=await W(e,t),a=await g(n,null);if(!a?.hold_stopped)return null;const{hold_stopped:i,...o}=a;return await b(n,o),{kind:a.kind??null,label:a.label??null}}r(ru,"takeHoldStopped");async function ic(e,t,{dir:n,agent:a,startedBetween:i,note:o,next:s,requireNext:c=!1}){const l=await W(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:w,...p}=u,f=new Date().toISOString(),m={...p,done:f,...o?{note:o}:{}},{id:$}=P(t),y=h.dirname(h.dirname(l)),S=await Promise.all((await z(y,$)).map(T=>T===l?m:g(T,null))),E=!S.some(T=>T?.started&&!T.done),j=n?{file:h.join(y,"wave.json")}:(await x(e,t)).source,F=await g(j.file,null),D=F?.items?.find(T=>T.id===$);return s!==void 0&&(Si(D||d(`${t} has no item to set --next on`),s,await ea(e,t)),e&&await Ei(e,t,D,D.next)),c&&D&&_n(D,S)&&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,m),await ot(e,t,{screen:u.kind==="grill"}),o&&await ua(e,t,a,u,{detail:o},f),s!==void 0&&await b(j.file,F),l}r(ic,"stopBuilder");async function oc(e){const t=[];for(const n of await M(e)){const a=await g(n.file,null);for(const i of a?.items||[])for(const o of await z(n.dir,i.id)){const s=await g(o,null);if(!s?.started||s.done)continue;const c=h.basename(o,".json");t.push({key:`${n.slug}/${i.id}`,agent:c.slice(String(i.id).length+1)||"main",label:s.label||"Builder",started:s.started})}}return t}r(oc,"openBoardBuilders");async function cu(e){const t=await oc(e);for(const n of t)await ic(e,n.key,{agent:n.agent,note:"Board closed. Builder record stopped; work is kept for reopening."});return t}r(cu,"closeBoardBuilders");export{le as BoardError,Ca as PUSH_LINE_MAX,Ut as REPLY_EXTS,js as SCREEN_AFTER,In as UPLOAD_LIMIT,qc as WORKING_KINDS,yd as abandonLaunch,xd as abandonPlanner,el as ackPing,td as addItem,Nl as addNotes,Sl as addPrototype,Pl as addQuestions,ul as addReviewers,kl as addRuleTo,_l as addTopic,qd as addVisual,Gd as answerBuilder,_t as appendMessage,U as appendStatus,rd as archiveCards,sd as archiveCount,on as archiveFile,sn as archiveRank,Qd as askOrchestrator,Wl as askedGate,Wd as auditLog,Xt as blockWords,ie as boardConfig,oe as boardRepoOf,Ql as breakOwnerCheck,Xd as briefFacts,ed as builderModel,Jd as builderProgress,Fd as bumpCard,ll as cardLinked,J as cardName,hd as cardPR,ud as changeWave,nu as checkCallsVerified,Ml as checkClose,su as checkNotReopened,Ad as clearStall,cu as closeBoardBuilders,dd as createWave,Md as cutOffBuilds,bd as dropPause,bl as editRuleIn,Kc as eventCard,hl as fallBackFromLinear,tl as finishHandshake,fd as finishWave,iu as finishesCard,au as handBackCalls,Yl as heldMerge,Id as helperOf,qa as hiddenFile,$d as implementAsIs,Hl as isFinished,Td as isHelper,fo as isLanding,Cd as isPlanner,M as itemSources,Dd as landFirst,eu as landable,zd as landingLine,Vd as listAsks,Zd as listCalls,vl as listPrototypes,$l as listRules,Vc as markAsk,Vr as markPaused,Hc as markRead,Pd as markStalled,jd as markStopped,Mc as markWorking,Zl as mergeCard,Or as mergedLine,Bn as messageText,nd as migrateFollowups,id as migratePhases,ad as migrateStatuses,ld as migrateWaveNames,Zc as moveAsk,Ul as moveCard,Tl as moveQuestions,Sd as openAgents,oc as openBoardBuilders,vd as openRuns,ml as opensPRs,P as parseKey,Kl as passOwnerCheck,Ud as pickBuilder,zl as pickUp,Il as planAgain,dl as postDigest,Xc as postIncomingReview,al as postLink,Vl as postPR,il as postReply,Gl as prReport,Dl as publishScreened,Fl as publishUnscreened,Is as pushLine,Bs as pushUrl,sl as queuePushes,Fe as readArchive,Pe as readAskLinks,Jt as readAskProgress,Ms as readAskTitles,Wn as readBoardRules,Qs as readBoardSettings,Xa as readHandshake,Cs as readHidden,g as readJSON,qs as readMarks,Wa as readMarksFile,jr as readNotes,Qt as readPRSlack,Ot as readPrototypes,ui as readScreening,zc as readUpload,pd as readWaveNotes,As as readWorking,Kd as recordCall,qi as recordOwnerTap,Fs as recordPush,ol as recordReaction,gd as recordRebase,Yc as recordReview,_d as recordSession,Hd as recordedSession,po as recordedWorktree,Qc as relateAsk,tu as relaunchLander,Ol as removePrototype,fl as removeReviewers,yl as removeRuleFrom,Ds as repliesSincePush,Nn as replyFilesOf,Ed as requestHelper,uo as requestPause,Cl as resolveNoteAnswers,Bl as resolveQuestion,kd as resumePaused,zs as reviewDigest,rl as reviewDue,cl as reviewUpdated,xl as saveBoardRules,at as saveBoardSettings,pl as saveBoardShare,Jc as saveUpload,Wc as sendPush,jl as servedRoundsOpen,wl as setBoardPause,gl as setBoardSetting,Uc as setHidden,ue as setItem,Jl as setMainInstall,nl as setReviewers,Gs as settingsLines,be as settingsOf,Rl as settleAnswers,Al as settleQuestion,Tc as siteDir,vs as sitesDir,El as splitSharedNotes,lr as stageImplement,od as stageImplementAsks,Ll as stageReply,Ft as startBuilder,Ld as startHelper,Rd as startPlanner,md as startRebase,wd as startTappedBuild,Nd as startVisual,ic as stopBuilder,Od as stopHeldMerge,cd as stripDeadFields,zn as takeFile,ru as takeHoldStopped,Xl as tapMerge,X as threadFile,xi as topicQuestions,Gc as unfoldAsk,ql as unrecorded,ou as verifiedFinish,Yd as verifyCalls,Bd as waitHelper,ca as waveNoteFile,Mt as wavesDir,Ls as withReplies,Cc as writeReply};
|