@lovek4d/kickflip 0.7.0 → 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 +53 -49
- package/SKILL.md +17 -13
- package/bin/event-lines.mjs +1 -1
- package/bin/kickflip.mjs +3 -3
- package/board/agents.mjs +2 -2
- package/board/arch-review.mjs +4 -4
- package/board/bin/board.mjs +113 -105
- package/board/domain.mjs +7 -7
- package/board/finish.mjs +4 -4
- package/board/headless.mjs +8 -8
- package/board/hub-lines.mjs +1 -0
- package/board/onboarding.mjs +3 -3
- package/board/public/app-arch.js +1 -1
- package/board/public/app-cards.js +6 -7
- package/board/public/app-header.js +1 -1
- package/board/public/app-lanes.js +39 -62
- package/board/public/app-needs.js +16 -11
- package/board/public/app-reader.js +9 -5
- package/board/public/app.js +3 -3
- package/board/public/attach.js +18 -2
- package/board/public/dialogs.js +11 -13
- package/board/public/hub-kit.js +1 -1
- package/board/public/hub.css +0 -1
- package/board/public/linear-key.js +2 -2
- package/board/public/new-task.js +1 -1
- package/board/public/prototype-kit.js +2 -5
- package/board/public/router.js +1 -1
- package/board/public/sidebar.css +3 -3
- package/board/public/sidebar.js +2 -2
- package/board/public/style.css +43 -43
- package/board/public/ui-cards.js +25 -20
- package/board/public/ui-hub.js +1 -5
- package/board/public/ui-links.js +5 -5
- package/board/public/ui-order.js +6 -7
- package/board/public/ui-questions.js +17 -13
- package/board/public/ui-sidebar.js +1 -1
- package/board/public/ui-threads.js +29 -21
- package/board/public/ui.js +23 -6
- package/board/public/view-hub-settings.js +11 -19
- package/board/public/view-hub.js +25 -11
- package/board/public/view-notes.js +1 -1
- package/board/public/view-questions.js +18 -5
- package/board/public/view-settings.js +7 -7
- package/board/render-check.mjs +10 -10
- package/board/review.mjs +1 -1
- package/board/server.mjs +18 -19
- package/board/stop.mjs +1 -1
- package/board/store.mjs +26 -26
- package/board/tickets.mjs +3 -3
- package/hooks-plugin/hooks/board-data-gate.mjs +6 -6
- package/hub/awake.mjs +1 -1
- package/hub/board-repo.mjs +1 -1
- package/hub/boards.mjs +4 -4
- package/hub/github.mjs +3 -3
- package/hub/hub.mjs +11 -11
- package/hub/incoming.mjs +2 -2
- package/hub/linear.mjs +1 -1
- package/hub/locks.mjs +1 -1
- package/hub/new-board.mjs +2 -2
- package/hub/owner-checks.mjs +1 -1
- package/hub/readiness.mjs +1 -1
- package/hub/review-send.mjs +1 -1
- package/hub/slack.mjs +1 -1
- package/hub/try.mjs +6 -7
- package/hub/views.mjs +1 -1
- package/package.json +1 -1
- package/shared/usage-gate.mjs +1 -1
- package/starter/server.mjs +3 -3
package/board/store.mjs
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
var
|
|
2
|
-
`).filter(Boolean).map(t=>JSON.parse(t))}catch{return[]}},"readLines");async function
|
|
3
|
-
`):
|
|
4
|
-
`)})},"writeJSON");async function
|
|
5
|
-
`)}r(
|
|
6
|
-
`).filter(
|
|
7
|
-
`)),{exitCode:
|
|
8
|
-
`).map(n=>n.replace(/\s+/g," ").trim()).find(Boolean)||"";return t.length>xa?`${t.slice(0,xa-1).trimEnd()}\u2026`:t}r(ls,"pushLine");const us={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:X(us,a.toLowerCase())??t})}r(fs,"htmlText");function ws(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(ws,"pushUrl");const ps=r((e,t)=>`/${m.basename(e)}/#selectedCard=${encodeURIComponent(t)}&reply`,"threadUrl");async function va(e,t,n){const a=ls(n);if(!a)return[];const i=lo(t),o=await $o(i,s=>[a,cs.exec(s)?.[1]].filter(Boolean).join(" \xB7 "),ps(e,t));return o===null?[]:[`queued push ${i} now reads: ${o}`]}r(va,"freshenPush");const _a=r(e=>m.join(e,"data","hidden.json"),"hiddenFile"),hs=r(async e=>new Set((await $(_a(e),{events:[]})).events||[]),"readHidden");let Sa=Promise.resolve();function fc(e,t,n){Pe(t,"the event id");const a=Sa.then(async()=>{const i=await hs(e);n?i.add(t):i.delete(t),await y(_a(e),{events:[...i]})});return Sa=a.catch(()=>{}),a}r(fc,"setHidden");const ja=r(e=>m.join(e,"data","read.json"),"readMarksFile"),ms=r(async e=>(await $(ja(e),{})).cards||{},"readMarks");let Oa=Promise.resolve();function wc(e,t,n={}){const a=Oa.then(async()=>{const i=await ms(e),o=new Date().toISOString(),s=r((d,u)=>String(u||"")>d?u:d,"later"),c=Object.fromEntries([...new Set(t)].sort().map(d=>[d,s(i[d]||o,n[d])]));JSON.stringify(c)!==JSON.stringify(i)&&await y(ja(e),{cards:c})});return Oa=a.catch(()=>{}),a}r(wc,"markRead");const jn=20*1024*1024,qe=r(e=>m.join(e,"data","uploads"),"uploadsDir"),Ve=r((e,t)=>m.join(qe(e),".meta",`${t}.json`),"uploadMeta"),Pa=/^\d+-[\w.-]+$/,Da=/^[\w.+-]+\/[\w.+-]+$/,Aa=r(()=>Object.assign(new re(`a file can be at most ${jn/1024/1024} MB`),{status:413}),"tooLarge");async function pc(e,t,{name:n,type:a,length:i}){if(A(e),i>jn)throw Aa();const o=String(n||"").trim().slice(0,200)||"file",s=m.basename(o).replace(/[^\w.-]+/g,"-").replace(/^[.-]+/,"").slice(-80)||"file",c=String(a||"").split(";")[0].trim().toLowerCase(),d=Da.test(c)?c:"application/octet-stream";await P.mkdir(m.dirname(Ve(e,"x")),{recursive:!0});let u,p;for(let h=Date.now();!p;h++)u=`${h}-${s}`,p=await P.open(m.join(qe(e),u),"wx").catch(g=>g.code==="EEXIST"?null:Promise.reject(g));const w=m.join(qe(e),u);let f=0;try{for await(const h of t){if(f+=h.length,f>jn)throw Aa();await p.write(h)}}catch(h){throw await p.close(),await P.rm(w,{force:!0}),h}return await p.close(),await y(Ve(e,u),{name:o,type:d,size:f,at:new Date().toISOString()}),{name:o,url:`uploads/${u}`,path:w,type:d,size:f}}r(pc,"saveUpload");async function hc(e,t){if(!Pa.test(t||""))return null;const n=m.join(qe(e),t),a=await P.stat(n).catch(()=>null);if(!a?.isFile())return null;const i=await $(Ve(e,t),{});return{path:n,name:i.name||t,type:Da.test(i.type||"")?i.type:"application/octet-stream",size:a.size}}r(hc,"readUpload");const Ra=["github","slack"];async function U(e,t,{text:n,link:a,log:i,who:o="claude",via:s,kind:c,by:d,command:u,card:p}){A(e),String(t).startsWith("ask:")?await ot(e,t.slice(4),"the ask"):await x(e,t),/^[^\n]{1,40}$/.test(o.trim())||l("--who takes claude, love or a reviewer's name"),s!==void 0&&!Ra.includes(s)&&l(`--via takes ${Ra.join(" or ")}`);const w={ts:new Date().toISOString(),who:o.trim(),text:S(n,"--text"),link:ne(a,h=>ba(h,"--link")),log:i,via:s,kind:c,by:d,command:u,card:p},f=M(e,t);return await z(f,w),{file:f,lines:w.who==="claude"&&!s?await va(e,t,w.text):[]}}r(U,"appendThread");const ke=r(e=>m.join(e,"data","ask-links.json"),"askLinksFile");async function De(e){return(await $(ke(e),{})).links||{}}r(De,"readAskLinks");async function K(e,t){const{tickets:n}=be(await ie(e)),a=await F(e),i=/^ask:(.+)$/.exec(t)?.[1];if(i===void 0){const s=/^([^/\s]+)\/([^/\s]+)$/.exec(t);if(!s)return t;const c=a.find(p=>p.slug===s[1]),d=c&&await $(c.file,null);if(!d)return s[2];const u=(d.items||[]).find(p=>p.id===s[2]);return Qe(d,u||{id:s[2]},n)}const o=e?X(await De(e),i):void 0;if(o)return K(e,o);for(const s of a){const c=await $(s.file,null),d=(Array.isArray(c?.items)?c.items:[]).find(u=>u.from_ask===i);if(d)return Qe(c,d,n)}return`task ${i}`}r(K,"cardName");async function mc(e,t){const n=(await G(m.join(e,"data","events.jsonl"))).find(a=>a.id===t);return n?.kind==="ask"?K(e,`ask:${t}`):typeof n?.data?.key=="string"?K(e,n.data.key):`the ${n?.kind} event`}r(mc,"eventCard");async function gc(e,t,n,a,i){A(e),Pe(t,"the event id"),(await G(m.join(e,"data","events.jsonl"))).some(d=>d.id===t&&d.kind==="ask")||l(`no ask "${t}" in this site's data/events.jsonl`),await Mt(e,t),await x(e,n);const s={ts:new Date().toISOString(),who:"claude",text:S(a,"--summary")};i!==void 0&&S(i,"--title");const c=await De(e);return await y(ke(e),{links:{...c,[t]:n}}),await z(M(e,n),s),i!==void 0&&await Ea(e,t,i),ke(e)}r(gc,"relateAsk");const Na=r(e=>m.join(e,"data","ask-titles.json"),"askTitlesFile");async function gs(e){return(await $(Na(e),{})).titles||{}}r(gs,"readAskTitles");async function Ea(e,t,n){await y(Na(e),{titles:{...await gs(e),[t]:S(n,"--title")}})}r(Ea,"titleAsk");async function $c(e,t){await ot(e,t,"the ask");const n=await De(e);return X(n,t)===void 0&&l(`ask "${t}" is not folded into a card`),delete n[t],await y(ke(e),{links:n}),ke(e)}r($c,"unfoldAsk");const qt=r(e=>m.join(e,"data","ask-progress.json"),"askProgressFile");async function Ft(e){return(await $(qt(e),{})).asks||{}}r(Ft,"readAskProgress");async function Mt(e,t){const n=X(await Ft(e),t)?.moved;n&&l(`ask "${t}" was moved to /${n.route}; it lives there now`)}r(Mt,"refuseMoved");async function kc(e,t,n,a){await ot(e,t,"the ask"),await Mt(e,t);const i=await Ft(e);return await y(qt(e),{asks:{...i,[t]:{state:n,at:new Date().toISOString(),...a===void 0?{}:{next:S(a,"--next")}}}}),qt(e)}r(kc,"markAsk");async function $s(e,t,n){const a=`ask "${t}"`;if(n.some(p=>p.kind==="ask-withdrawn"&&p.data?.id===t&&!p.data?.key))return{why:`${a} was deleted by the owner`};const i=X(await Ft(e),t);if(i?.moved)return{why:`${a} was already moved to /${i.moved.route}`};const o=X(await De(e),t);if(o)return{why:`${a} is folded into ${o}; run ask unfold ${t} first`};const s=[];for(const p of await F(e)){const w=await $(p.file,null);for(const f of Array.isArray(w?.items)?w.items:[])f.from_ask===t&&s.push({key:`${p.slug}/${f.id}`,it:f})}const c=s.find(({it:p})=>Ce(p)||Ne(p));return c?{why:`${a} became ${c.key}, which has started`}:s.length?{items:s.map(p=>p.key)}:(await Promise.all(["html","json"].map(p=>P.stat(m.join(e,"data","replies",`${t}.${p}`)).then(()=>!0,()=>!1)))).some(Boolean)||!!i||n.some(p=>p.kind==="ask-done"&&p.data?.id===t)||(await G(M(e,`ask:${t}`))).some(p=>(p.who||"claude")==="claude"&&p.kind!=="status")?{why:`${a} is not New: Claude already answered it`}:null}r($s,"askNotNew");async function ks(e,t,n){const a=/^uploads\/([\w.-]+)$/.exec(n?.url||"")?.[1],i=Pa.test(a||"")?m.join(qe(e),a):null;if(!i||!await P.stat(i).then(d=>d.isFile(),()=>!1))return{name:String(n?.name||a||"file")};await P.mkdir(m.dirname(Ve(t,"x")),{recursive:!0});const o=a.replace(/^\d+-/,"");let s;for(let d=Date.now();s=`${d}-${o}`,!await P.copyFile(i,m.join(qe(t),s),eo.COPYFILE_EXCL).then(()=>!0,p=>p.code==="EEXIST"?!1:Promise.reject(p));d++);const c=await $(Ve(e,a),null);return c&&await y(Ve(t,s),c),{...n,url:`uploads/${s}`,path:m.join(qe(t),s)}}r(ks,"copyUpload");async function bc(e,t,n){A(e),Pe(t,"the event id"),(typeof n!="string"||!n.trim())&&l("ask move needs --to <route>, the board to move it to");const a=await G(m.join(e,"data","events.jsonl")),i=a.find(O=>O.id===t&&O.kind==="ask")||l(`no ask "${t}" in this site's data/events.jsonl`),o=ge(),s=o.find(O=>O.route===n)||l(`--to ${n}: no board at /${n} in the hub's manifest; the boards are ${o.map(O=>O.route).join(", ")||"none"}`);je(s.dir,e)&&l(`--to ${n}: that is this board`);const c=await $s(e,t,a);c?.why&&l(c.why);const d=a.filter(O=>O.kind==="ask-edited"&&O.data?.id===t&&typeof O.data?.text=="string").sort((O,B)=>String(O.ts).localeCompare(String(B.ts))),u=d.at(-1),p=[];for(const O of[...i.data?.files||[],...d.flatMap(B=>B.data.files||[])])p.push(await ks(e,s.dir,O));const w={route:m.basename(e),title:(await ie(e)).title,id:t},f=u?u.data.text:i.data?.text||"",h=await me(s,"ask",{text:f,...p.length?{files:p}:{},from:w}),g={route:n,title:s.title,id:h},k=new Date().toISOString(),v=await Ft(e);await y(qt(e),{asks:{...v,[t]:{state:"done",at:k,moved:g}}});const D=`/${n}/#selectedCard=${encodeURIComponent(`ask:${h}`)}`;await z(M(e,`ask:${t}`),{ts:k,who:"claude",text:`Moved to ${s.title}.`,link:D});for(const O of c?.items||[])await pe(e,O,{done:!0,force:!0,text:`Closed: its ask moved to ${s.title}.`});return{file:m.join(s.dir,"data","events.jsonl"),lines:[`moved to ${D}`,...(c?.items||[]).map(O=>`closed: ${O}`)]}}r(bc,"moveAsk");async function yc(e,t,{state:n,text:a}={}){return A(e),["posted","needs"].includes(n)||l("review record needs --state posted or --state needs"),String(a??"").trim()||l("review record needs the review text, as --text or --file"),Ba(e,await Ia(e,t,"review record"),{state:n,text:a})}r(yc,"recordReview");async function Ia(e,t,n){/^[1-9]\d{0,6}$/.test(String(t))||l(`${n} takes the PR number, such as 437, not "${t}"`);const a=await $(m.join(e,"data","incoming.json"),{});return{...(a?.prs||[]).find(s=>s.number===Number(t))||(await $(m.join(e,"data","reviews.json"),{}))[t]||l(`PR #${t} is not an incoming PR on this board`),repo:a?.repo||oe(e,{})}}r(Ia,"incomingPR");async function Ba(e,{number:t,title:n,author:a,url:i,touches:o=[],asked:s=!1,head:c},{state:d,text:u,review_url:p}){const w=m.join(e,"data","reviews.json"),f=await $(w,{});return f[t]={number:Number(t),title:n,author:a,url:i,touches:o,asked:s,head:c,state:d,text:u,...p?{review_url:p}:{},at:new Date().toISOString()},await y(w,f),{file:w,lines:[]}}r(Ba,"saveReview");async function xc(e,t,{words:n,readAt:a}){A(e);const i=await Ia(e,t,"review post"),o=String(n??"").trim()||l("review post needs the review text, as --file"),s=`${pt}Claude review from the ${(await ie(e)).title} board`;(o.startsWith(pt.trim())||o.includes("Claude review from")||o.includes(yn))&&l(`review post adds the ${pt.trim()} header and the claude-bridge marker itself; pass only your words`);const c=i.repo||l("review post needs a board on GitHub; this board has no repo there");Number.isNaN(Date.parse(a))&&l("--read-at takes the time you read the PR, such as 2026-11-05T09:30:00Z");const d=be(await ie(e)).github_login;try{const u=await Co({repo:c,pr:i.number,since:a});u.length&&l(`PR #${t} got ${u.length===1?"a review":`${u.length} reviews`} after ${a} (${u.map(w=>`${w.login} at ${w.at}`).join(", ")}); reread the PR and review again`);const p=await Lo({repo:c,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(ys,"linkText");const Ta=r(e=>!!e?.approved||e?.reviewDecision==="APPROVED","approved");async function Ac(e,t){A(e);const n=await ma.waiting("slack");if(n)return{waiting:n,links:[],replies:[],reactions:[],digest:null};const a=await ie(e),i=await $(mn,{}),o=await Ut(e),s=[];for(const p of await F(e)){const w=await $(p.file,{}),f=_n(w,a.reviewers);for(const h of w.items||[])if(h.lane!=="done")for(const g of Le(h,oe(e,w))){const k=ht(g),v=X(i.prs,k);if(!(v?.state!=="OPEN"||v.isDraft))for(const D of f)$t(o[k]?.links?.[D.name])||s.some(O=>O.pr===k&&O.reviewer===D.name)||s.push({pr:k,key:`${p.slug}/${h.id}`,reviewer:D.name,slack_user:D.slack_user,head:v.head||null,text:ys(v,g,h)})}}const c=[],d=[];for(const[p,w]of Object.entries(o)){const f=X(i.prs,p),h=[Ta(f)&&"git_approved",f?.state==="MERGED"?"git_merged":f?.state==="CLOSED"&&"x"].filter(Boolean);for(const[g,k]of Object.entries(w.links))if(k.ts)for(const v of h)k.reactions.includes(v)||d.push({pr:p,reviewer:g,channel:k.channel,ts:k.ts,emoji:v});for(const g of w.replies)for(const[k,v]of Object.entries(g.to))$t(v)||c.push({pr:p,id:g.id,reviewer:k,channel:w.links[k].channel,thread_ts:w.links[k].ts,text:g.text})}const u=await xs(e,t);return{links:s,replies:c,reactions:d,digest:u.due?u:null}}r(Ac,"reviewDue");async function Rc(e,t,n){A(e);const a=S(n,"what changed"),{wave:i,item:o}=await x(e,t),s=[];for(const d of Le(o,oe(e,i)))s.push(...await Ca(e,ht(d),"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(Rc,"reviewUpdated");async function Nc(e,t){const{wave:n,item:a}=await x(e,t),i=await Ut(e);return Le(a,oe(e,n)).some(o=>Object.values(i[ht(o)]?.links||{}).some(s=>s.ts))}r(Nc,"cardLinked");const qa=r(e=>m.join(A(e),"data","review-digests.json"),"digestFile");async function xs(e,{now:t=new Date}={}){A(e);const n=await ie(e),a=await $(mn,{}),i=await Ut(e),o=Fo(),s=new Map;for(const f of await F(e)){const h=await $(f.file,{}),g=_n(h,n.reviewers);for(const k of g)s.has(k.name)||s.set(k.name,{name:k.name,slack_user:k.slack_user,dm:k.dm,items:[]});for(const k of h.items||[]){if(k.lane==="done")continue;const v=`${f.slug}/${k.id}`,D=qo(h,k,v,o);for(const O of Le(k,oe(e,h))){const B=ht(O),N=X(a.prs,B);if(!(N?.state!=="OPEN"||N.isDraft||N.failing?.length||Ta(N)))for(const _ of g){const E=i[B]?.links?.[_.name]?.ts||X(D,_.name)?.ts||null,C=E?(t.getTime()-Number(E)*1e3)/864e5:null,T=C>2?Math.floor(C):null;s.get(_.name).items.push({key:v,pr:B,title:k.title,url:vn(O),sent_ts:E,...T?{days_old:T}:{},text:`${vn(O)} \xB7 ${k.title}${T?` (${T} days old)`:""}`})}}}}const{date:c,open:d}=Mo(t),u=((await $(qa(e),{})).digests||[]).filter(f=>f.date===c),p=await ma.waiting("slack");for(const f of u)s.has(f.reviewer)||s.set(f.reviewer,{name:f.reviewer,items:[]});const w=[...s.values()].map(f=>{const h=u.find(k=>k.reviewer===f.name),g=h?!h.posting_at&&(!h.ts||h.replies.some(k=>!$t(k))):f.items.length>0;return{...f,owed:g,...h?{sent:h}:{}}}).filter(f=>f.items.length||f.sent);return{date:c,due:!p&&d&&w.some(f=>f.owed),...p?{waiting:p}:{},reviewers:w}}r(xs,"reviewDigest");async function Ec(e,t,n,{channel:a,header:i,post:o}){const s=qa(e),c=r(p=>(p.digests||[]).find(w=>w.date===t&&w.reviewer===n.name),"entry");await Fe(s,p=>{c(p)||(p.digests=[...(p.digests||[]).filter(w=>w.date===t),{date:t,reviewer:n.name,channel:a,replies:n.items.map(w=>({pr:w.pr,text:w.text}))}],delete p.last_date)});const d=c(await $(s,{})),u=d.ts||await Ht(s,c,()=>o({channel:d.channel,text:i(d.replies.length)}));for(const[p,w]of d.replies.entries())$t(w)||await Ht(s,f=>c(f).replies[p],()=>o({channel:d.channel,thread_ts:u,text:w.text}))}r(Ec,"postDigest");function Ic(e,t){const n=Pn(Array.isArray(t)?t:[t],"the reviewer file");return Dn(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 Bc(e,t){return Dn(e,n=>{const a=t.filter(i=>!n.some(o=>gt(o.name,i)));return a.length&&l(`this board has no reviewer ${a.map(i=>`"${i}"`).join(", ")}`),n.filter(i=>!t.some(o=>gt(i.name,o)))})}r(Bc,"removeReviewers");const vs=r(()=>m.join(ln.homedir(),".claude","kickflip","settings.json"),"hubSettingsFile"),An=r(e=>{try{return e()}catch(t){throw t instanceof zo&&l(t.message),t}},"ruleCall"),Rn=r(async e=>$a(await $(m.join(e,"board.json"),{})),"readBoardRules");let Fa=Promise.resolve();function kt(e,t){const n=Fa.then(async()=>{const a=m.join(A(e),"board.json"),i=await $(a,{}),{set:o,...s}=t(i);return await y(a,{...i,...o}),{file:a,...s}});return Fa=n.catch(()=>{}),n}r(kt,"writeBoardJSON");const Ma=r((e,t)=>kt(e,n=>{const a=An(()=>t($a(n)));return{set:{rules:a.rules},...a}}),"writeBoardRules");function Lc(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)||!Vo(o))&&l("Send add or remove with one login, like name@github, or a listed login with its level."),a&&!["view","full"].includes(t.level)&&l("A level is view or full.");const s=r(({login:c,level:d})=>d==="full"?{login:c,level:d}:c,"stored");return kt(e,c=>{const d=Zo(c);a&&!d.some(w=>w.login===o)&&l(`${o} isn't shared with yet. Add them first.`);const p=(n==="remove"?d.filter(w=>w.login!==o):a?d.map(w=>w.login===o?{login:o,level:t.level}:w):d.some(w=>w.login===o)?d:[...d,{login:o,level:"view"}]).map(s);return{set:{share:p},share:p}}).then(({share:c})=>({share:c}))}r(Lc,"saveBoardShare");const Wa=r(e=>typeof e=="string"&&/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(e),"validGithubLogin"),Ua=r(e=>{try{return e?kn(e):null}catch{return null}},"storedTickets"),Nn=r(e=>Number.isInteger(e)&&e>=1&&e<=99,"validCap"),En=["max_implementers","max_planners"],be=r((e,t)=>{const n=Ua(e?.tickets);return{auto_merge:e?.auto_merge===!0,auto_build:e?.auto_build===!0,auto_pull:e?.auto_pull===!0,max_implementers:Nn(e?.max_implementers)?e.max_implementers:ao,max_planners:Nn(e?.max_planners)?e.max_planners:io,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}:{},...Wa(e?.github_login)?{github_login:e.github_login}:{},...ga(e?.build_target)?{build_target:e.build_target}:{}}},"settingsOf");async function Cc(e,t,n=new Date){let a;try{a=go(t,n)}catch(i){l(i.message)}return await kt(e,()=>({set:{paused_until:a??void 0}})),a}r(Cc,"setBoardPause");async function _s(e){const t=A(e);return be(await $(m.join(t,"board.json"),{}),ge().find(n=>je(n.dir,t)))}r(_s,"readBoardSettings");const Tc=r((e,t)=>!!e?.github&&(!e.local||e.hub===!0)&&t.prs,"opensPRs");async function et(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||l("the settings name nothing to change"),t={...t};for(const[n,a]of Object.entries(t))if(n==="tickets"){try{t.tickets=kn(a)}catch(o){l(o.message)}t.tickets.source==="linear"&&!Po()&&l(jo);const i=t.tickets.source==="github"&&e?ge().find(o=>je(o.dir,e)):null;i&&!i.github&&l("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:Wa(a)||l("github_login must be a GitHub login"):n==="build_target"?a===null?t.build_target=void 0:ga(a)||l('build_target must name .kickflip/build-<target>: letters, digits, ".", "_" or "-", or null for .kickflip/build'):En.includes(n)?Nn(a)||l(`${n} must be a whole number from 1 to 99`):Tt(a,n);if(t.prs===!1){const n=e?ge().find(a=>je(a.dir,e)):null;n?.github&&!n.local&&!(await Wo(n.owner?.repo)).pushMain&&l("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?ge().find(s=>je(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(et,"saveBoardSettings");async function qc(){if(Oo().set)return[];const e=[];for(const t of ge()){if((await _s(t.dir)).tickets?.source!=="linear")continue;const{settings:n}=await et(t.dir,{tickets:{source:"internal"}});await me(t,"settings",{tickets:n.tickets}),e.push(`/${t.route} fell back to internal tickets: Hub has no Linear key`)}return e}r(qc,"fallBackFromLinear");async function Fc(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)||l(`no board setting "${t}"; settings are ${i.map(c=>c.replace(/_/g,"-")).join(", ")}`),a==="tickets"){let c;try{c=JSON.parse(n)}catch{l('settings set tickets takes the source as JSON, such as {"source":"github"}')}const{file:d,settings:u}=await et(e,{tickets:c});return{file:d,lines:Ss({tickets:u.tickets})}}if(a==="github_login"){const{file:c,settings:d}=await et(e,{github_login:n});return{file:c,lines:[`github-login ${d.github_login||"unset"}`]}}if(a==="build_target"){const{file:c,settings:d}=await et(e,{build_target:n==="default"?null:n});return{file:c,lines:[`build-target ${d.build_target||"default"}`]}}if(En.includes(a)){const{file:c,settings:d}=await et(e,{[a]:/^\d+$/.test(n)?Number(n):n});return{file:c,lines:[`${t} ${d[a]}`]}}["on","off"].includes(n)||l(`settings set ${t} takes on or off, not "${n}"`);const{file:o,settings:s}=await et(e,{[a]:n==="on"});return{file:o,lines:[`${t} ${s[a]?"on":"off"}`]}}r(Fc,"setBoardSetting");const Ss=r(e=>Object.entries(e).map(([t,n])=>`setting ${t.replace(/_/g,"-")} ${t==="tickets"?JSON.stringify(n):t==="github_login"||En.includes(t)?n:t==="build_target"?n||"default":n?"on":"off"}`),"settingsLines");async function In(e,t,n){const a=t?An(()=>n(Lt())):await Ma(e,n);return t&&An(()=>Yo(a.rules)),{file:t?vs():a.file,lines:[`id ${a.rule.id}`]}}r(In,"changeRules");async function Ha(e,t,n){const a=r(s=>s.map(c=>c.id),"ids");n||A(e);const[i,o]=n?[a(Lt()),e?a(await Rn(e)):[]]:[a(await Rn(e)),a(Lt())];i.includes(t)||l(`no ${n?"hub":"board"} rule "${t}"${o.includes(t)?`; it is a ${n?"board":"hub"} rule, so ${n?"drop":"pass"} --hub`:""}`)}r(Ha,"checkRuleId");async function Mc(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 Rn(e)).map(t("board")):[]]}}r(Mc,"listRules");function Wc(e,{text:t,hub:n=!1,by:a}){return n||A(e),In(e,n,i=>Ko(i,{text:t,by:a}))}r(Wc,"addRuleTo");async function Uc(e,t,{text:n,hub:a=!1}){return await Ha(e,t,a),In(e,a,i=>Jo(i,t,n))}r(Uc,"editRuleIn");async function Hc(e,t,{hub:n=!1}){return await Ha(e,t,n),In(e,n,a=>Qo(a,t))}r(Hc,"removeRuleFrom");const zc=r((e,t,n)=>Ma(e,a=>Go(a,t,n)),"saveBoardRules");async function Kc(e,{id:t,title:n,summary:a}){A(e);const i=await $(L(e),{topics:[],items:[]}),o=i.topics||[];return o.some(s=>s.id===S(t,"the topic id"))&&l(`topic "${t}" already exists`),o.push({id:t.trim(),title:S(n,"--title"),...a===void 0?{}:{summary:S(a,"--summary")}}),await y(L(e),{...i,topics:o,items:i.items||[]}),L(e)}r(Kc,"addTopic");const za=["quiet","info","notice","danger","success"];function js(e,t){if(e.diagram===void 0!=(e.practice===void 0)&&l(`${t} needs diagram and practice together`),e.diagram===void 0)return;const n=e.diagram;(!Array.isArray(n)||!n.length)&&l(`${t}.diagram must be a non-empty array of rows`),n.forEach((a,i)=>{(!Array.isArray(a)||!a.length||a.length>4)&&l(`${t}.diagram[${i}] must be a row of 1 to 4 boxes`),a.forEach((o,s)=>{const c=`${t}.diagram[${i}][${s}]`;Ze(o,["text","tone","to"],c),S(o.text,`${c}.text`),ne(o.tone,d=>za.includes(d)?d:l(`${c}.tone takes ${za.join(", ")}`)),ne(o.to,d=>Array.isArray(d)&&i+1<n.length&&d.every(u=>Number.isInteger(u)&&u>=0&&u<n[i+1].length)?d:l(`${c}.to must list boxes of the next row by index`))})}),(!Array.isArray(e.practice)||!e.practice.length)&&l(`${t}.practice must be a non-empty array of lines`),e.practice.forEach((a,i)=>S(a,`${t}.practice[${i}]`))}r(js,"checkDiagram");function Os(e,t){(!Array.isArray(e)||!e.length)&&l(`${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),js(a,o),n.has(S(a.id,`${o}.id`))&&l(`${t} has two options with id "${a.id}"`),n.add(a.id),S(a.label,`${o}.label`).startsWith(`${a.id}. `)&&l(`${o}.label starts with "${a.id}. ", but the page adds the option letter itself`),ne(a.recommended,s=>Tt(s,`${o}.recommended`)),ne(a.detail,s=>typeof s=="string"?s:l(`${o}.detail must be text`))}),e.filter(a=>a.recommended).length>1&&l(`${t} recommends more than one option`)}r(Os,"checkOptions");function Ps(e,t,n){Ze(e,["option","card","answer"],n),t.some(a=>a.id===S(e.option,`${n}.option`))||l(`${n}.option names no option "${e.option}"`),S(e.card,`${n}.card`),S(e.answer,`${n}.answer`)}r(Ps,"checkSuggested");const tt=r(e=>m.join(e,"data","prototypes.json"),"prototypesFile"),Bn=r((e,t,n)=>m.join(e,"public","prototypes",t,`${n}.js`),"takeFile");async function bt(e){const t=e?(await $(tt(e),{})).prototypes:null;return Array.isArray(t)?t:[]}r(bt,"readPrototypes");const Ds=r(e=>typeof e=="string"&&/^[a-z0-9-]+$/.test(e)?e:l(`a prototype topic uses only a-z, 0-9 and "-", not "${e}"`),"prototypeTopic");function As(e){const t=/^([^:]*):(.*)$/s.exec(e);(!t||!/^[A-Z]$/.test(t[1]))&&l(`--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)&&l(`--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(As,"prototypeTake");function Rs(e){const t=/^([^:]*):(.*)$/s.exec(e);return(!t||!/^[a-z0-9][a-z0-9-]*$/.test(t[1]))&&l(`--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(Rs,"prototypeState");const Ka=r(e=>e.find((t,n)=>e.findIndex(a=>a.key===t.key)!==n),"twiceIn"),Ae=r(e=>/^prototype-(.+)\.html$/.exec(m.posix.basename(String(e||"").split(/[?#]/)[0]))?.[1],"pageTopic");async function Jc(e,{topic:t,title:n,card:a,takes:i=[],served:o,states:s=[]}){A(e),Ds(t),S(n,"--title"),i.length||l('prototype add needs a --take KEY:"Label" for each take');const c=i.map(As),d=Ka(c);d&&l(`--take ${d.key} is given twice`),o!==void 0&&!/^\/\S*$/.test(o)&&l(`--served takes the page's path on the served site, starting with "/", not "${o}"`),s.length&&o===void 0&&l("--state goes with --served: only a served round re-renders its states in place");const u=s.map(Rs),p=Ka(u);p&&l(`--state ${p.key} is given twice`),a!==void 0&&await x(e,a);const w=await bt(e);w.some(g=>g.topic===t)&&l(`prototype ${t} is already recorded; run prototype rm ${t} first, or pick another topic`),await P.stat(m.join(e,"public",`prototype-${t}.html`)).then(()=>!0,()=>!1)&&l(`public/prototype-${t}.html is a hand-written page that questions may link; pick another topic`);const f={topic:t,title:n.trim(),...a===void 0?{}:{card:a},...o===void 0?{}:{served:o},takes:c,...u.length?{states:u}:{},at:new Date().toISOString()};await y(tt(e),{prototypes:[...w,f]});const h=`page prototype-${t}.html`;return o!==void 0?{file:tt(e),lines:[h,`served ${o} from the base prototype worktree (board.mjs prototype base)`,...u.length?[`states ${u.map(g=>g.key).join(", ")}`]:[]]}:{file:tt(e),lines:[h,...c.map(g=>`take ${g.key} ${Bn(e,t,g.key)}`)]}}r(Jc,"addPrototype");async function Qc(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(Qc,"listPrototypes");async function Gc(e){const t=(await bt(e)).filter(a=>a.served).map(a=>a.topic);if(!t.length)return[];const n=await $(L(e),{items:[]});return t.filter(a=>(n.items||[]).some(i=>!i.resolved&&Ae(i.url)===a))}r(Gc,"servedRoundsOpen");async function Yc(e,t){A(e);const n=await bt(e);n.some(o=>o.topic===t)||l(`no prototype "${t}" is recorded`);const i=((await $(L(e),{items:[]})).items||[]).filter(o=>!o.resolved&&Ae(o.url)===t).map(o=>o.id);return i.length&&l(`prototype ${t} backs open question ${i.join(", ")}; resolve ${i.length===1?"it":"them"} first`),await y(tt(e),{prototypes:n.filter(o=>o.topic!==t)}),tt(e)}r(Yc,"removePrototype");async function Ns(e,t,n){const a=t.url?.split(/[?#]/)[0];if(!a||!m.posix.basename(a).startsWith("prototype-"))return;const i=m.posix.basename(a),o=t.url.split("#")[0].split("?").slice(1).join("?"),s=`${n}.url`;new URLSearchParams(o).get("q")!==t.id&&l(`${s} must be ${i}?q=${t.id} so the pill can pick`);const c=Ae(a),d=(await bt(e)).find(f=>f.topic===c);d||l(`${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=d.takes.map(f=>f.key),p=t.options.map(f=>f.id),w=p.map(f=>u.indexOf(f));if(w.some((f,h)=>f<0||h>0&&f<w[h-1])&&l(`${n}.options ids ${p.join(", ")} must be a non-empty subset of ${i}'s takes, in take order: ${u.join(", ")}`),!d.served)for(const f of u)await P.stat(Bn(e,c,f)).then(h=>h.isFile(),()=>!1)||l(`${s} take ${f} has no file yet; write ${Bn(e,c,f)}`)}r(Ns,"checkPrototype");async function Zc(e,t,n=async()=>{}){A(e);const a=Array.isArray(t)?t:[t];a.length||l("the question file is empty");const i=await $(L(e),{topics:[],items:[]}),o=i.items||[],s=await $($e(e),{}),c=new Set([...o,...Object.values(s).flat()].map(w=>w.id)),d=new Set((i.topics||[]).map(w=>w.id));for(const[w,f]of a.entries()){const h=`question ${f?.id??w+1}`;Ze(f,["id","topic","source","question","context","url","urgent","options","round_of","reask_of","ask","suggested","prototype_gate"],h),c.has(Pe(f.id,`${h}.id`))&&l(`question id "${f.id}" is already used`),c.add(f.id),S(f.topic,`${h}.topic`),f.topic.startsWith("item:ask:")?await ot(e,f.topic.slice(9),`${h}.topic`):f.topic.startsWith("item:")?await x(e,f.topic.slice(5)):f.topic.startsWith("lineup:")?await tn(e,f.topic.slice(7)):f.topic.startsWith("wave:")?await pr(e,f.topic.slice(5)):d.has(f.topic)||l(`topic "${f.topic}" is not declared; add it with: topic add ${f.topic} --title <t>`),S(f.question,`${h}.question`);for(const g of["source","context"])ne(f[g],k=>S(k,`${h}.${g}`));ne(f.url,g=>ba(g,`${h}.url`)),ne(f.urgent,g=>Tt(g,`${h}.urgent`)),Os(f.options,h),ne(f.prototype_gate,g=>Es(g,f,h)),ne(f.suggested,g=>Ps(g,f.options,`${h}.suggested`)),await Ns(e,f,h),f.ask!==void 0&&await jr(e,f,h),ne(f.round_of,g=>{const k=o.find(v=>v.id===g);k||l(`${h}.round_of names no question "${g}" in owner.json`),k.topic!==f.topic&&l(`${h}.round_of names ${g} on topic ${k.topic}; a round stays on its question's topic, ${f.topic}`),Ae(f.url)||l(`${h}.round_of needs a url on a recorded prototype, prototype-<topic>.html?q=${f.id}`)}),ne(f.reask_of,g=>{const k=o.find(v=>v.id===g);k||l(`${h}.reask_of names no question "${g}" in owner.json`),k.topic!==f.topic&&l(`${h}.reask_of names ${g} on topic ${k.topic}; a re-ask stays on its question's topic, ${f.topic}`),k.resolved||l(`${h}.reask_of names ${g}, which is not resolved; only a resolved question is asked again`)})}for(const w of new Set(a.map(f=>Ae(f.url)).filter(Boolean)))await n(e,w);const u=[],p=[];for(const w of a){const f=w.topic.startsWith("item:")&&!w.topic.startsWith("item:ask:")?w.topic.slice(5):null,h=f&&await Ja(e,f);h&&((s[f]||=[]).push(w),p.push(w),u.push(`staged: ${w.id} waits for ${f}'s ${h} to finish; the card's staged questions then publish together`))}return p.length&&await y($e(e),s),p.length===a.length?{file:$e(e),lines:u,staged:p.length}:Ga(e,i,o,a.filter(w=>!p.includes(w)),u)}r(Zc,"addQuestions");async function Ja(e,t){const{source:n}=await x(e,t);for(const a of await H(n.dir,I(t).id)){const i=await $(a,null);if(i?.started&&!i.done&&Be(i)&&i.kind!==we)return i.kind}return null}r(Ja,"runningPlanner");async function nt(e,t,{screen:n=!1}={}){if(!e||await Ja(e,t))return;const a=await $($e(e),{});if(!a[t])return;const i=await $(Ye(e),{});if(n){i[t]||await y(Ye(e),{...i,[t]:new Date().toISOString()});return}const{[t]:o,...s}=a,c=await $(L(e),{topics:[],items:[]});await Ga(e,c,c.items||[],o,[]),await y($e(e),s),i[t]&&await y(Ye(e),Ln(i,t))}r(nt,"publishStaged");const Ln=r((e,t)=>Object.fromEntries(Object.entries(e).filter(([n])=>n!==t)),"withoutKey");async function Qa(e){const t=await $(Ye(e),{}),n=await $($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(Qa,"readScreening");async function Vc(e,t,n){A(e);const a=S(n,"--note"),i=await $($e(e),{}),o=await $(Ye(e),{}),s=Object.keys(i).find(w=>i[w].some(f=>f.id===t));s||l(`no staged question "${t}"; question settle takes a question a finished grill left for you to screen`),o[s]||l(`${t} waits for ${s}'s planner to finish`);const c=i[s].find(w=>w.id===t);c.prototype_gate&&l(`${t} is the card's prototype question, which is the owner's; question publish ${s} passes it on`);const d=new Date().toISOString(),u=await $(L(e),{topics:[],items:[]});await y(L(e),{...u,topics:u.topics||[],items:[...u.items||[],{...c,asked_at:d,resolved:`Settled by the orchestrator: ${a}`,resolved_at:d}]});const p=i[s].filter(w=>w.id!==t);return await y($e(e),p.length?{...i,[s]:p}:Ln(i,s)),p.length||await y(Ye(e),Ln(o,s)),await at(e,s,`Answers (orchestrator, ${yt()})`,`- ${t} (${c.question}): settled by the orchestrator. "${a}"`),L(e)}r(Vc,"settleQuestion");async function Xc(e,t){A(e);const n=(await Qa(e))[t]?.ids||l(`${t} has no questions waiting on you to screen`);return await nt(e,t),n}r(Xc,"publishScreened");async function ed(e,t=new Date){const n=Object.entries(await Qa(e)).filter(([,a])=>t-Date.parse(a.since)>=es).map(([a])=>a);for(const a of n)await nt(e,a),await U(e,a,{text:"The orchestrator didn't screen the grill's questions within 15 minutes, so the board passed them to you as they are."});return n}r(ed,"publishUnscreened");async function Ga(e,t,n,a,i){const{answers:o,requests:s,reopens:c}=await it(e),d=new Date().toISOString(),u=[],p=[];for(const f of a.filter(h=>h.round_of))f.round=hn(n.find(h=>h.id===f.round_of))+1,Re(t,[f.round_of],`Followed by prototype round ${f.round}`,d),p.push(f.round_of);for(const f of a.filter(h=>h.reask_of))Re(t,[f.reask_of],`Asked again as ${f.id}`,d),p.push(f.reask_of);for(const f of a){const h=Ae(f.url),g=n.filter(v=>v.topic===f.topic).map(v=>Ie(v,o,s,[],c)),k=f.topic.startsWith("item:")?ni(g):[];for(const v of g)k.includes(v.id)?(Re(t,[v.id],`Followed by ${f.id}`,d),p.push(v.id)):!v.resolved&&h&&Ae(v.url)===h&&(Re(t,[v.id],`Replaced by ${f.id}`,d),u.push(v.id))}const w={...t,topics:t.topics||[],items:[...n,...a.map(f=>({...f,asked_at:d}))]};await y(L(e),w);for(const f of new Set(a.map(h=>Cn(h.topic)).filter(Boolean)))await Ms(e,f);return{file:L(e),lines:[...i,...u.map(f=>`replaced: ${f}`),...p.map(f=>`resolved: ${f}`)]}}r(Ga,"publishQuestions");function Es(e,t,n){Tt(e,`${n}.prototype_gate`),e&&((!t.topic.startsWith("item:")||t.topic.startsWith("item:ask:"))&&l(`${n}.prototype_gate goes on a card's topic, item:<wave>/<id>`),t.options?.map(a=>a.id).join()!=="yes,no"&&l(`${n}.prototype_gate takes exactly the options yes and no, in that order`))}r(Es,"checkGate");async function td(e,t){const n=await $(L(e),{topics:[],items:[]}),a=new Map((n.items||[]).map(c=>[c.id,c]));for(const c of t){const d=a.get(String(c.id));if(!d?.prototype_gate||!["yes","no"].includes(c.choice))continue;Re(n,[d.id],`Prototype round: ${c.choice}. The board moves the card on once every question is answered.`),await y(L(e),n);const u=d.topic.slice(5);c.choice==="yes"&&(await x(e,u).catch(()=>null))?.item.phase==="grilling"&&await Un(e,u)&&await U(e,u,{text:"You asked for a prototype round, so the board moves the card to prototyping once every question is answered."})}const i=new Map;for(const c of t){const d=a.get(String(c.id)),u=Cn(d?.topic);u&&i.set(u,[...i.get(u)||[],Bs(d,c)])}const o=[],s=[];for(const[c,d]of i)await x(e,c).catch(()=>null)&&await Is(e,c,"writing them into its notes",async()=>{await at(e,c,`Answers (owner, ${yt()})`,d.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 Fs(e,t,n){const{item:a}=await x(e,t);a.held&&await xi(e,"card-hold",{key:t,hold:!1});const{owner:i,asked:o}=await de(e,t),s=o.filter(d=>d.state==="open").map(d=>d.id);s.length&&await Kt(e,i,s,"Skipped: you tapped Implement");const c=ti(n);c&&await at(e,t,`Implement (owner, ${yt()}), verbatim`,c),await Wn(e,t)}r(Fs,"stageImplement");async function id(e,t,n){const a=ti(n);a&&await at(e,t,`Reply (owner, ${yt()}), verbatim`,a),await Wn(e,t)}r(id,"stageReply");async function od(e,t,n){const{source:a,wave:i,item:o}=await x(e,t);!["ready","staging"].includes(o.phase)||Ne(o)||(o.phase=n==="prototype"?"prototyping":"grilling",delete o.staged_at,await y(a.file,i))}r(od,"planAgain");async function Ms(e,t){const n=await x(e,t).catch(()=>null);!n||!["ready","staging"].includes(n.item.phase)||Ne(n.item)||(n.item.phase="grilling",delete n.item.staged_at,await y(n.source.file,n.wave),await U(e,t,{text:"A new question on it, so it is grilling until you answer."}))}r(Ms,"grillAgain");function ni(e){return e.filter(t=>t.state==="answered").map(t=>t.id)}r(ni,"answeredOnClaude");function Re(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(Re,"setResolved");async function Kt(e,t,n,a){return Re(t,n,a),await y(L(e),t),L(e)}r(Kt,"markResolved");async function sd(e,t,n){A(e);const a=await $(L(e),{topics:[],items:[]}),i=(a.items||[]).find(f=>f.id===t);i||l(`no question "${t}" in owner.json`);const{answers:o,requests:s,reopens:c}=await it(e),d=Ie(i,o,s,[],c);if(d.state==="resolved")return L(e);d.state==="requested"&&l(`${t} waits on the prototype the owner asked for; question add with round_of ${t} resolves it`);const u=S(n,"--note");Ae(i.url)&&Ws(a,i,o);const p=await Kt(e,a,[t],u);await si(e,i.topic);const w=Cn(i.topic);return w&&(await x(e,w).catch(()=>null))?.item.phase==="grilling"&&await Ya(e,w,{roundAt:!0}),p}r(sd,"resolveQuestion");async function rd(e,t,n,a=[]){A(e);const[i,...o]=[...new Set(t)];i||l("question move takes the ids of the questions to merge"),await x(e,n);const s=[n,...a];new Set(s).size!==s.length&&l("question move names a card twice");for(const f of a)await x(e,f);const c=await $(L(e),{topics:[],items:[]}),{answers:d,requests:u,reopens:p}=await it(e);for(const f of[i,...o]){const h=(c.items||[]).find(k=>k.id===f)||l(`no question "${f}" in owner.json`),{state:g}=Ie(h,d,u,[],p);g!=="open"&&l(`${f} is ${g}; question move takes open questions`)}for(const[f,h]of a.entries()){const{item:g}=await x(e,h),k=[...g.deps||[],s[f]].join(",");await pe(e,h,{deps:k,relate:n})}const w=await K(e,n);return Object.assign(c.items.find(f=>f.id===i),{topic:`item:${n}`}),Re(c,o,`Duplicate of ${w}'s question ${i}`),await y(L(e),c),L(e)}r(rd,"moveQuestions");async function cd(e,t){const n=t.filter(({choice:i,note:o})=>!i&&String(o||"").trim()).map(i=>i.id),a=n.length?await $(L(e),{topics:[],items:[]}):null;a?.items?.some(i=>n.includes(i.id))&&(Re(a,n,"Answered with a note"),await y(L(e),a))}r(cd,"resolveNoteAnswers");function Ws(e,t,n){const a=hn(t),i=e.items.filter(o=>o.topic===t.topic&&hn(o)===a).map(o=>Ie(o,n));uo(i,a)||l(`${t.id} is in prototype round ${a}, which no pick has settled yet; a pick settles it`)}r(Ws,"roundSettled");async function it(e){const t=await G(m.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=>P.stat(m.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===ro)}}r(it,"ownerEvents");async function dd(e,t){const{asked:n}=await Us(e),a=new Map(n.map(i=>[i.id,i.state]));return t.filter(i=>!a.has(String(i.id))||["open","requested"].includes(a.get(String(i.id))))}r(dd,"unrecorded");async function Us(e){const t=await $(L(e),{topics:[],items:[]}),{answers:n,requests:a,reopens:i}=await it(e);return{owner:t,asked:(t.items||[]).map(o=>Ie(o,n,a,[],i))}}r(Us,"topicQuestionsAll");async function ai(e,t){const n=await $(L(e),{topics:[],items:[]}),{answers:a,requests:i,reopens:o}=await it(e),s=(n.items||[]).filter(c=>c.topic===t).map(c=>Ie(c,a,i,[],o));return{owner:n,asked:s}}r(ai,"topicQuestions");const de=r((e,t)=>ai(e,`item:${t}`),"cardQuestions"),Jt=r((e,t,n)=>ii(e,`item:${t}`,n),"resolveAnswered");async function ii(e,t,n){if(!e)return[];const{owner:a,asked:i}=await ai(e,t),o=ni(i);return o.length&&await Kt(e,a,o,n),await si(e,t),o.map(s=>`resolved: ${s}`)}r(ii,"resolveAnsweredOn");const Un=r(async(e,t)=>!!e&&(await de(e,t)).asked.some(n=>n.state==="open"),"hasOpenQuestion");function oi(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(oi,"setNext");async function si(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 re)return;throw c}const{source:i,wave: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 y(i.file,o)))}r(si,"clearWaitedNext");async function Hs(e,t){const{source:n,wave:a,item:i}=await x(e,t);i.next===void 0&&i.next_on_questions===void 0||(delete i.next,delete i.next_at,delete i.next_on_questions,await y(n.file,a))}r(Hs,"clearNext");async function ri(e,t,n,a){if(!e)return async()=>[];const{owner:i,asked:o}=await de(e,t),s=o.filter(d=>d.state==="open").map(d=>d.id);s.length&&!a&&l(`${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(d=>d.state!=="resolved").map(d=>d.id);return async()=>(c.length&&await Kt(e,i,c,"Closed with the card"),c.map(d=>`resolved: ${d}`))}r(ri,"closeQuestions");async function ld(e,t,n,a,i){await x(e,t),S(n,"--text"),ne(a,o=>S(o,"--owner-step")),ne(i,o=>vi(o)),await ri(A(e),t,"land",!1)}r(ld,"checkClose");async function Hn(e,t){const n=await $(L(e),{items:[]}),{answers:a,requests:i,reopens:o}=await it(e);return so((n.items||[]).map(s=>Ie(s,a,i,[],o)),t)}r(Hn,"unpickedPrototype");const zs=r(e=>e.length>120?`${e.slice(0,119)}\u2026`:e,"clip"),Ks={ask:"text","ask-edited":"text","item-message":"text","owner-answer":"note"};async function ci(e,t,n){const a=S(t,n).replace(/\s+/g," ");return(await G(m.join(e,"data","events.jsonl"))).map(o=>String(o.data?.[Ks[o.kind]]??"").replace(/\s+/g," ").toLowerCase()).some(o=>o.includes(a.toLowerCase()))||l(`${n} must quote the owner's words in an ask, card message or answer note on this board`),a}r(ci,"ownerQuote");async function ud(e,t,n){return A(e),n===void 0&&l(`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 ci(e,n,"--asked")}"`}r(ud,"askedGate");async function ot(e,t,n="--from-ask"){return Pe(t,n),(await G(m.join(A(e),"data","events.jsonl"))).some(i=>i.id===t&&i.kind==="ask")?t:l(`no ask "${t}" in this site's data/events.jsonl`)}r(ot,"checkAsk");function Qt(e,t,n=new Date().toISOString(),a=di(e,t)){if(!Ke[t])throw new Error(`no lane "${t}"`);const i=e.lane;return li(e,t,a),e.lane=t,e.lane_at=n,t!=="todo"&&delete e.bumped_out,t==="done"?(e.closed_at=n,delete e.reopened_at):i==="done"&&(e.reopened_at=n),i}r(Qt,"placeIn");function di(e,t,n=!1){return t==="todo"?Ne(e)?"ready":e.phase??null:t==="progress"?"reading":t==="done"?n||Oe(e).some(a=>a.merged_at)?"merged":"closed":null}r(di,"movedPhase");function li(e,t,n){if(n!==null&&!un[t].includes(n))throw new Error(`no ${Ke[t]} phase "${n}"`);n===null?delete e.phase:e.phase=n}r(li,"setPhase");async function fd(e,t,n,a,i){const{source:o,wave:s,item:c}=await x(e,t);if(c.lane===n&&(i===void 0||(c.phase??null)===i))return null;if(c.lane===n)return li(c,n,i),await y(o.file,s),null;const d=new Date().toISOString(),u=Qt(c,n,d,i);if(await y(o.file,s),u===void 0)return null;const p={ts:d,who:"claude",kind:ua,text:`Moved to ${Ke[n]}: ${S(a,"why")}`};return await z(M(e,t),p),p.text}r(fd,"moveCard");const Y=r(e=>e?.lane==="done"||!!e?.reopened_at,"closed"),Ne=r(e=>!!e?.lane&&e.lane!=="todo","leftTodo"),wd=r(async(e,t)=>Y((await x(e,t)).item),"isFinished"),ui=["ready","grilling","prototyping"];function fi(e,t){t!==void 0&&l(`--state is now --phase: pass --phase ${ui.includes(t)?t:"ready, grilling or prototyping"}`),e==="staging"&&l("--phase staging is set by the board, on an owner action; pass ready, grilling or prototyping"),e!==void 0&&!ui.includes(e)&&l("--phase takes ready, grilling or prototyping")}r(fi,"checkPhase");const Js="a card goes ready only from staging, which the board sets on the owner's Send, Implement or Reply";async function wi(e,t){return(await G(m.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(wi,"quickAsk");const Qs=/^mark (?:it |this |the card )?ready\b/i;function Gs(e,t,n){const a=[].concat(t).map(s=>xn(s)||l(`--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=Oe(e);for(const s of a)o.some(c=>i(c,s))||o.push(s);delete e.pr,e.prs=o}r(Gs,"addPRs");const J=r((e,t)=>String(t).includes("/")?String(t):`${e}/${t}`,"depKey");async function st(e,t,n){const a=(n.deps||[]).map(o=>J(t,o)),i=await Promise.all(a.map(o=>x(e,o).then(({item:s})=>s,()=>null)));return a.filter((o,s)=>i[s]&&!Y(i[s]))}r(st,"unfinishedDeps");async function Ys(e,t,n){const a=await Promise.all((await st(e,I(t).wave,n)).map(i=>K(e,i)));return n.blocked_until?[...a,n.blocked_until]:a}r(Ys,"blockerNames");async function Gt(e,t,n){n??=(await x(e,t)).item;const a=await Promise.all((await st(e,I(t).wave,n)).map(i=>K(e,i)));return[a.length&&`by ${a.join(", ")}`,n.blocked_until&&`until ${n.blocked_until}`].filter(Boolean).join(" and ")}r(Gt,"blockWords");const Zs=r((e,t)=>!Ce(e)&&!e.pushed&&!e.merge_ready&&!Y(e)&&(!!fn(t)||t.some(n=>n?.started&&!n.done&&Je(n))),"inProgress");async function Vs(e,t){const n=[];for(const a of await F(e)){const i=await $(a.file,null);for(const o of Array.isArray(i?.items)?i.items:[])Y(o)||o.blocked_until||!(o.deps||[]).some(s=>J(a.slug,s)===t)||(await st(e,a.slug,o)).length||n.push(`${a.slug}/${o.id}`)}return n}r(Vs,"unblockedBy");async function pi(e,t,n){const{source:a,wave:i,item:o}=await x(e,t);Ne(o)&&(o.unblocked_at=n,await y(a.file,i))}r(pi,"markUnblocked");async function hi(e,t,n,a){const i=new Set(a.match(/#?[\w-]+(?:\.[\w-]+)*(?:\/[\w-]+(?:\.[\w-]+)*)*/g)||[]),o=new Set((n.deps||[]).map(s=>J(I(t).wave,s)));for(const s of await F(e)){const c=await $(s.file,null);for(const d of c?.items||[]){const u=`${s.slug}/${d.id}`;if(u===t||o.has(u)||Y(d))continue;[d.id,Qe(c,d),d.ticket].filter(Boolean).flatMap(w=>[w,`${s.slug}/${w}`]).some(w=>i.has(w))&&l(`--next names unfinished card ${u}; add it with --deps`)}}}r(hi,"checkNextDeps");const mi=r(e=>`item notes ${e} --heading "<heading>" --file <draft.md>`,"NOTES_COMMAND");function Xs(e,t){t.size||l(`${e} has no size; a builder starts from it, so pass --size small or large with --phase ready`),t.plan||l(`${e} has no notes file; a builder starts from it, so add its notes with ${mi(e)} before --phase ready`)}r(Xs,"checkSizeAndNotes");async function er(e,t,n,a){Xs(t,a);const i=await P.readFile(await Yt(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&&l(`${t}'s notes leave out the answered question${o.length===1?"":"s"} ${o.join(", ")}; record each answer in its notes, naming its id, with ${mi(t)} before it is ready`)}r(er,"checkNotesComplete");async function Yt(e,t,{missingOk:n=!1}={}){const a=r(s=>l(`a notes file is a .md file inside ${e}, named relative to it: "${t}" ${s}`),"bad");(typeof t!="string"||!t.trim())&&a("is empty"),(m.isAbsolute(t)||t.split(/[\\/]/).includes(".."))&&a("leaves the folder"),m.extname(t).toLowerCase()!==".md"&&a("is not a .md file");const i=await P.realpath(e),o=await P.realpath(m.resolve(e,t)).catch(()=>n?m.resolve(i,t):a("does not exist"));return o.startsWith(i+m.sep)||a("leaves the folder"),o}r(Yt,"planPath");async function tr(e,t){const{source:n,wave:a,item:i}=await x(e,t);if(i.plan)return{wave:a,item:i,markdown:await P.readFile(await Yt(n.dir,i.plan),"utf8")};if(typeof i.notes=="string"&&i.notes.trim())return{wave:a,item:i,markdown:i.notes};l(`${t} has no notes`)}r(tr,"readNotes");function gi({install:e,installSha:t,installLabel:n,clearInstall:a}){if(e===void 0)return(t!==void 0||n!==void 0)&&l("--install-sha and --install-label go with --install <url>"),a?null:void 0;a&&l("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))&&l(`--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")||""))&&l("--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():l(`--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(gi,"installOf");async function pd(e,{install:t,installSha:n}){const a=gi({install:t,installSha:n});a||l("main set needs --install <url>");const i=m.join(A(e),"data","main.json");return await y(i,{install:a}),{file:i,lines:["main install set"]}}r(pd,"setMainInstall");const $i=r((e,t)=>e.length?`Merged PR${e.length>1?"s":""} ${Ge(e,t)}.`:"Merged.","mergedLine");function nr(e,t,n,a,i){return n?a??$i(n,i):(a===void 0&&e&&l(`--done needs --text "<what was done, or why it closes without landing>", which becomes ${t}'s last thread line`),a??null)}r(nr,"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=Oe(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 ki(e,t,{ticket:n,status:a,clearStatus:i,done:o,merged:s,landed:c,start:d,handBack:u,next:p,clearNext:w,pr:f,pushed:h,mergeReady:g,ghSince:k,fromAsk:v,phase:D,state:O,held:B,unheld:N,pickUpAt:_,deps:E,blockedUntil:C,blockReason:T,parent:W,related:j,relate:Q,unrelate:ae,install:Z,installSha:ee,installLabel:ye,clearInstall:Ot,title:rt,size:le,model:xe,force:Pt,text:ce,who:Dt,by:an,command:te,ownerStep:ve,ownerDone:ue,check:ct,stopRun:Ji},Vn){(a!==void 0||i)&&l(`--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),j!==void 0&&(Q!==void 0||ae!==void 0)&&l("pass --related, or --relate and --unrelate, not both"),fi(D,O),Pt&&!o&&l("--force goes with --done"),v!==void 0&&o&&l("--from-ask opens a card; close it in another call"),ce!==void 0&&S(ce,"--text"),ve!==void 0&&S(ve,"--owner-step");const dt=ct===void 0||ct===""?ct:vi(ct);ce!==void 0&&v===void 0&&!o&&D!=="ready"&&l("--text goes with --from-ask, --done, or --phase ready on a staging card"),v!==void 0&&ce===void 0&&l(`--from-ask needs --text "<first line>", the item's first thread line after the ask`),p!==void 0&&w&&l("pass --next or --clear-next, not both"),typeof p=="string"&&!p.trim()&&([p,w]=[void 0,!0]);const lt=yi({held:B,unheld:N,pickUpAt:_}),At=gi({install:Z,installSha:ee,installLabel:ye,clearInstall:Ot});[o,d,u,p,w,f,h,g,k,v,D,lt,E,C,T,W,j,Q,ae,At,rt,le,xe,n,ve,ct].every(R=>R===void 0||R===!1)&&l("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 V=v===void 0?void 0:await ot(e,v);V&&await Mt(e,V);const Qi=V!==void 0&&await wi(e,V),{source:_e,wave:Ue,item:b}=await x(e,t);D!==void 0&&Ne(b)&&l(`--phase sets a Todo card's phase; ${t} is ${Ke[b.lane]}, where the board sets it`),D==="ready"&&!Qi&&!["staging","ready"].includes(b.phase)&&l(`${t} reads ${b.phase||"new"}, not staging; ${Js}`),V&&b.from_ask&&b.from_ask!==V&&l(`${t} came from ask "${b.from_ask}", so ask "${V}" can't be its from-ask; to fold it in: item relate-ask ${V} ${t} --summary "<line>"`),V&<&&D!==void 0&&!b.phase&&l("a card filed held is filed New and takes no --phase; drop --phase, or drop --held to file it in a phase"),V&&D===void 0&&!b.phase&&!lt&&l("--from-ask files a card, which reads New until it has a phase: pass --phase grilling, prototyping, or ready for a Quick build ask, or --held to file it for later");const Xn=Y(b);ve!==void 0&&Xn&&l(`${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`),ve!==void 0&&(b.owner_step=S(ve,"--owner-step")),ue&&delete b.owner_step,dt!==void 0&&!b.owner_step&&l(`--check goes with an owner step, which ${t} doesn't have: pass --owner-step "<the step>" with it on builder done or land`),dt&&(b.owner_check={check:dt,since:b.owner_check?.check===dt?b.owner_check.since:new Date().toISOString()}),(dt===""||!b.owner_step)&&delete b.owner_check;const Rt=await Promise.all((await H(_e.dir,b.id)).map(R=>$(R,null))),Gi=pn(b,Rt);if(rt!==void 0&&(b.title=S(rt,"--title")),bi(b,{size:le,model:xe}),E!==void 0){const R=await _i(e,I(t).wave,E);R.length?b.deps=R:delete b.deps}const ea=C===""?b.blocked_until:void 0;if(C!==void 0&&(C===""?delete b.blocked_until:b.blocked_until=S(C,"--blocked-until")),T&&(!b.deps?.length&&!b.blocked_until&&l("--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,W!==void 0){const R=await Si(e,I(t).wave,t,W);R?b.parent=R:delete b.parent}const ta=W!==void 0||j!==void 0||Q!==void 0||ae!==void 0?await zn(e,t):null;let on=j===void 0?null:await sr(e,t,b,j);Q!==void 0&&(b.related=[...new Set([...(b.related||[]).map(R=>J(I(t).wave,R)),...await Vt(e,t,Q,"--relate")])]),ae!==void 0&&(on=await rr(e,t,b,await Vt(e,t,ae,"--unrelate"))),lt!==void 0&&Zt(b,lt),d&&b.held&&l(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 ut=f!==void 0||h!==void 0||g!==void 0;if((d||o||u||ut)&&or(b),d){const R=await st(e,I(t).wave,b);R.length&&l(`${t} is blocked by ${R.join(", ")}; start it once ${R.length===1?"that is":"those are"} done`),b.blocked_until&&l(`${t} is blocked until ${b.blocked_until}; start it once that holds and the block is cleared with --blocked-until ""`)}const Nt=d&&e?await Hn(e,t):null;Nt&&l(`${t} has no take picked: the owner's answer to ${Nt.id} picks none${Nt.note?` ("${zs(Nt.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 Yi=o?await ri(e,t,"--done",Pt):async()=>[],na=o?nr(e,t,s,ce,oe(e,Ue)):null;if(s&&ar(b,s,oe(e,Ue)),p!==void 0&&(oi(b,p,await Un(e,t)),await hi(e,t,b,b.next)),w&&(delete b.next,delete b.next_at,delete b.next_on_questions),d&&p===void 0&&b.next_on_questions&&(delete b.next,delete b.next_at,delete b.next_on_questions),ut&&!b.review_since&&(b.review_since=new Date().toISOString()),f!==void 0&&Gs(b,f,oe(e,Ue)),h!==void 0){ge().find(R=>je(R.dir,A(e)))?.remote||l("--pushed is for a board whose repo is a plain git remote; on GitHub, open a PR and pass --pr"),Ce(b)&&l(`${t} has a PR; its PR says when it merges`);try{Do(h)}catch{l("--pushed takes the name of the branch you pushed")}b.pushed={branch:h,at:new Date().toISOString()},b.branch=h}if(g!==void 0&&(b.merge_ready={branch:g,at:new Date().toISOString()},b.branch=g),o&&delete b.merge_ready,k!==void 0&&((typeof k!="string"||!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(k)||Number.isNaN(Date.parse(k)))&&l("--gh-since takes an ISO-8601 UTC time such as 2026-10-23T11:00:00Z"),b.gh_since=k),V&&(b.from_ask=V),D==="ready"&&e){const R=(await de(e,t)).asked.filter(ze=>ze.state==="open").map(ze=>ze.id);R.length&&l(`${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 er(e,t,_e.dir,b)}const sn=D==="ready"&&b.phase==="staging"&&v===void 0;sn&&ce===void 0&&l(`${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"&&!sn&&ce!==void 0&&v===void 0&&!o&&l(`${t} reads ${b.phase||"no phase"}, not staging, so there is no judgment for --text to record`);const Zi=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"&&Qs.test(b.next??"")&&l(`${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`),At&&(b.install=At),At===null&&delete b.install,!Gi&&!d&&pn(b,Rt)&&l(`that leaves ${t} In Progress with nothing building and no Next; pass --next "<what happens next>"`);const He=(E!==void 0||C)&&!o&&Zs(b,Rt)?await Gt(e,t,b):"",rn=Rt.some(R=>R?.started&&!R.done&&Je(R)),Vi=He&&rn?await Ji():[],aa=oe(e,Ue);let cn=null;const Et=r((R,ze)=>{b.lane!==R&&Qt(b,R,void 0,di(b,R,c))!==void 0&&ze&&(cn=`Moved to ${Ke[R]}: ${ze}`)},"moveTo");o?Et("done",null):He&&rn?Et("todo",null):ut&&b.lane!=="done"?Et("review",f!==void 0?`PR ${Ge([].concat(f).map(xn),aa)} opened`:h!==void 0?`${h} pushed for its merge`:"verified, it waits for your Merge"):d&&!Y(b)&&Et("progress","a build started"),(n!==void 0||V&&!b.ticket&&!b.linear?.length)&&await Kn(e,t,Ue,b,n,Vn,_e.dir),await y(_e.file,Ue),cn&&await z(M(e,t),{ts:b.lane_at,who:"claude",kind:ua,text:cn}),sn&&await at(e,t,`Ready (orchestrator, ${yt()})`,ce),Zi&&await z(M(e,t),{ts:new Date().toISOString(),who:"claude",command:"readied",text:he.readied}),He&&rn?(await nn(_e.dir,b.id,!1,{endHandBack:!0}),await z(M(e,t),{ts:new Date().toISOString(),who:"claude",text:`Blocked ${He}: back to Todo. It starts again once nothing blocks it.`})):He&&await z(M(e,t),{ts:new Date().toISOString(),who:"claude",text:`Blocked ${He}: it stays In Progress.`}),on&&await on(),ta&&await Oi(e,t,ta,await zn(e,t));const Se=[...Vi,...await Yi()];D==="ready"&&e&&Se.push(...await Jt(e,t,"Recorded in the card's notes")),d&&Se.push(...await Jt(e,t,"Acted on: build started")),V&&Se.push(...(await U(e,t,{text:ce})).lines),na&&e&&Se.push(...(await U(e,t,{text:na,who:Dt,by:an,command:te})).lines);const ia=b.owner_step&&o?null:xo({start:d,done:o,handover:ut}),oa=ia&&await vo(await Pi(e),b,ia,{...Vn,repo:aa});oa&&Se.push(oa),(ut||o||u)&&(await nn(_e.dir,b.id,o,u?{handedBack:new Date().toISOString()}:{endHandBack:!0}),await nt(e,t));const dn=V?await De(e):{};if(X(dn,V)!==void 0&&(delete dn[V],await y(ke(e),{links:dn})),ea&&!Y(b)&&!(await st(e,I(t).wave,b)).length){const R=new Date().toISOString();await z(M(e,t),{ts:R,who:"claude",text:`Unblocked: ${ea}.`}),await pi(e,t,R),await me({dir:e},"card-unblocked",{key:t}),Se.unshift(`unblocked: ${t}`)}if(!o||Xn)return{file:_e.file,lines:Se};const sa=await Vs(e,t),ra=new Date().toISOString();for(const R of sa)await z(M(e,R),{ts:ra,who:"claude",text:`Unblocked: ${await K(e,t)} is done.`}),await pi(e,R,ra),await me({dir:e},"card-unblocked",{key:R});return{file:_e.file,lines:[...sa.map(R=>`unblocked: ${R}`),...Se]}}r(ki,"changeItem");function bi(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:l(`--${a} takes ${o.join(" or ")}, or "" to clear it`))}r(bi,"setBuildChoice");function yi({held:e,unheld:t,pickUpAt:n}){return[e,t,n!==void 0].filter(Boolean).length>1&&l("pass one of --held, --unheld and --pick-up-at"),n!==void 0?n===""?null:{until:ir(n)}:e?{until:null}:t?null:void 0}r(yi,"holdOf");function ir(e){const t=typeof e=="string"&&bo.test(e)?Date.parse(e):NaN;return Number.isNaN(t)&&l('--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()&&l(`--pick-up-at ${e} has passed; pass a later time, or --unheld to resume the card now`),new Date(t).toISOString()}r(ir,"pickUpTime");function or(e){delete e.held,delete e.pick_up_at}r(or,"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 hd(e,t,n,{announce:a=!0}={}){const{source:i,wave: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 z(M(e,t),{ts:new Date().toISOString(),who:"claude",text:`Resumed: its hold until ${c} ended.`}),!s.blocked_until&&!(await st(e,I(t).wave,s)).length&&await me({dir:e},"card-unblocked",{key:t,until:n})}Zt(s,null),await y(i.file,o)}r(hd,"pickUp");async function xi(e,t,n){const a=String(n?.key||""),{wave:i,id:o}=I(a),{source:s,wave:c}=await Te(e,i),d=n.shared_by?n.shared_by.name||n.shared_by.login:void 0,u={text:he.done,who:"love",by:d,command:"done"},p=c.items.find(g=>g.id===o)||l(`wave "${i}" has no item "${o}"`);if(!(t==="card-done"&&!!p.owner_step)&&p.lane==="done")return null;if(t==="card-done")return pe(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(p,n.hold?{until:f}:null),await y(s.file,c),n.hold&&(await nn(s.dir,o,!1,{held:!0}),await nt(e,a));const h=f&&new Date(f).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});return U(e,a,n.hold?{text:h?`${he.hold} until ${h}`:he.hold,who:"love",by:d,command:"hold"}:{text:he.resume,who:"love",by:d,command:"resume"})}r(xi,"recordOwnerTap");function vi(e){try{return Ho(Uo(e))}catch(t){return l(t.message)}}r(vi,"checkSpec");async function md(e,t,n,a){const{item:i}=await x(e,t);return!i.owner_step||i.owner_check?.check!==n?null:pe(e,t,{done:!0,force:!0,ownerDone:!0,text:`Check passed: ${a}. Closed.`})}r(md,"passOwnerCheck");async function gd(e,t,n,a){const{source:i,wave:o,item:s}=await x(e,t);return!s.owner_step||s.owner_check?.check!==n?!1:(s.owner_check.broken=a,await y(i.file,o),await U(e,t,{text:`The check for "${s.owner_step}" can't run (${a}), so it stopped; Claude is asked to fix it. Done still closes the card.`}),await me({dir:e},"owner-check-broken",{key:t,check:n,error:a}),!0)}r(gd,"breakOwnerCheck");async function _i(e,t,n){const a=[...new Set(String(n).split(",").map(i=>i.trim()).filter(Boolean))];for(const i of a)await x(e,J(t,i));return a}r(_i,"depList");async function Si(e,t,n,a){if(!String(a).trim())return null;const i=J(t,String(a).trim());i===n&&l("--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(d=>{if(d instanceof re)return null;throw d});if(!s)break;const{item:c}=s;c.parent&&J(I(o).wave,c.parent)===n&&l(`--parent: ${i} is a child of ${n}, so it can't be its Parent`),o=c.parent?J(I(o).wave,c.parent):null}return i}r(Si,"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=>J(i,s)))];o.includes(t)&&l(`${a}: a card can't be related to itself`);for(const s of o)await x(e,s);return o}r(Vt,"relatedKeys");const ji=r((e,t,n)=>async()=>{for(const a of await F(e)){const i=await $(a.file,null);let o=!1;for(const s of Array.isArray(i?.items)?i.items:[]){const c=`${a.slug}/${s.id}`;c===t||!n(c)||!(s.related||[]).some(d=>J(a.slug,d)===t)||(s.related=s.related.filter(d=>J(a.slug,d)!==t),s.related.length||delete s.related,o=!0)}o&&await y(a.file,i)}},"dropLinksTo");async function zn(e,t){const n=new Set;let a=null;for(const i of await F(e))for(const o of(await $(i.file,null))?.items||[]){const s=`${i.slug}/${o.id}`,c=(o.related||[]).map(d=>J(i.slug,d));s===t?(c.forEach(d=>n.add(d)),o.parent&&(a=J(i.slug,o.parent))):c.includes(t)&&n.add(s)}return{related:n,parent:a}}r(zn,"linksOf");async function Oi(e,t,n,a){const i=r(async(o,s,c=o,d=o)=>{await U(e,t,{text:he[c],command:c,card:s}),await U(e,s,{text:he[d],command:d,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(Oi,"postLinkLines");async function sr(e,t,n,a){const i=await Vt(e,t,a,"--related");return i.length?n.related=i:delete n.related,ji(e,t,o=>!i.includes(o))}r(sr,"setRelated");function rr(e,t,n,a){const i=(n.related||[]).filter(o=>!a.includes(J(I(t).wave,o)));return i.length?n.related=i:delete n.related,ji(e,t,o=>a.includes(o))}r(rr,"unrelateCards");const oe=r((e,t)=>e&&ge().find(n=>je(n.dir,e))?.github||t.repo,"boardRepoOf");async function $d(e,t){const{wave:n,item:a}=await x(e,t);Ce(a)||l(`${t} has no PR`);const i=Le(a,oe(e,n));i.length||l(`${t} has no repo`);const o=a.gh_since||"1970-01-01T00:00:00Z";try{const s=be(await ie(e)).github_login||await Io();return await Promise.all(i.map(async({repo:c,number:d})=>({repo:c,...ha(await pa({repo:c,pr:d,since:o}),{pr:d,since:o,me:s})})))}catch(s){l(s.message)}}r($d,"prReport");async function kd(e,t,{replyTo:n,body:a}){const i=n===void 0?"pr comment":"pr reply",o=xn(t)||l(`${i} takes a PR number or owner/repo#<n>`);n!==void 0&&!/^[1-9]\d*$/.test(n)&&l("pr reply takes the id of the review comment it answers");const s=S(a,"--body").trim();(s.startsWith(pt.trim())||s.includes(yn))&&l(`${i} adds the ${pt.trim()} prefix and the claude-bridge marker itself; pass only your words`);const c=o.repo||oe(A(e),{})||l(`${i} needs a board on GitHub; this board has no repo there`);try{const d=await Bo({repo:c,pr:o.number,replyTo:n,words:s,login:be(await ie(e)).github_login}),u=await Ca(e,ht({repo:c,number:o.number}),"comment",`Comment on #${o.number}: ${s}`);return{lines:[`posted on PR ${o.repo?t:`#${t}`}: ${d}`,...u.length?[`queued for ${u.join(", ")}'s Slack thread`]:[]]}}catch(d){l(d.message)}}r(kd,"postPR");const Pi=r(async e=>Ua((await ie(e)).tickets),"boardSource");async function bd(e,t,{afterApproval:n,confirmed:a,tapped:i}={}){A(e);const{wave:o,item:s}=await x(e,t);Ce(s)||l(`${t} has no PR`),Y(s)&&l(`${t} is done`);const c=await Gt(e,t,s);c&&(i&&await U(e,t,{text:`Not merged: blocked ${c}.`}),l(`Not merged: blocked ${c}.`));const d=oe(e,o),u=Le(s,d);u.length||l(`${t} has no repo`);const p=new Date().toISOString();let w;try{w=await Promise.all(u.map(_=>pa({repo:_.repo,pr:_.number,since:p})))}catch(_){l(_.message)}const f=w.map(({view:_})=>_);if(To(f)){const _=u.map((E,C)=>({...E,at:f[C].mergedAt})).filter((E,C)=>f[C].state==="MERGED");return pe(e,t,{merged:_,text:`PR${_.length>1?"s":""} ${Ge(_,d)} ${_.length>1?"were":"was"} already merged.`})}const h=u.map((_,E)=>({..._,...w[E]})).filter(({view:_})=>_.state==="OPEN"),g=_n(o,(await ie(e)).reviewers||[]).length>0,k=[];let v=!1,D=!1;for(const _ of h){const{view:E,reviews:C}=_,{failing:T,pending:W}=ha(_,{pr:_.number,since:p,me:null}),j=oo({...E,checks:{state:T.length?"fail":W?"pending":"pass",failing:T,pending:W}},{needsApproval:g}),Q=C.filter(Z=>Z.state==="APPROVED").sort((Z,ee)=>String(Z.submitted_at).localeCompare(String(ee.submitted_at))).at(-1)?.commit_id;let ae=!!(Q&&E.headRefOid&&Q!==E.headRefOid);ae&&n===void 0&&$r(s,Q,E.headRefOid)&&(ae=!1,D=!0),v||=ae,ae&&n===void 0&&j.push('commits after the approval: pass --after-approval "<why they are trivial>" when they are, else ask the approver to look again'),k.push(...u.length>1?j.map(Z=>`${Ge([_],d)} ${Z}`):j)}h.length||k.push(u.length>1?"every PR is closed":"PR is closed"),n!==void 0&&!v&&l("--after-approval: no commit came after the approval; drop it");const O=s.manual_steps?.length?a===void 0?null:await ci(e,a,"--confirmed"):void 0;if(a!==void 0&&O===void 0&&l(`--confirmed: ${t} has no manual steps; drop it`),O===null&&k.push(`manual steps: merge only on the owner's word, with --confirmed "<their words>"`),k.length){const _=`Not merged: ${k.join("; ")}.`;i&&await U(e,t,{text:_}),l(_)}const B=[];for(const _ of h){const E=await new Promise(C=>{ca("gh",["pr","merge",String(_.number),"--repo",_.repo,"--squash"],{encoding:"utf8",timeout:12e4},(T,W,j)=>C(T?String(j||T.message).trim().split(`
|
|
25
|
-
`)[0]:null))});E&&l(`gh pr merge ${Ge([_],d)} failed: ${E}${B.length?` (merged ${Ge(B,d)} before it)`:""}`),B.push(_)}const N=[$i(B,d),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}.`,O&&`The owner's word on the manual steps: "${O}"`];return pe(e,t,{merged:B,text:N.filter(Boolean).join(" ")})}r(bd,"mergeCard");async function yd(e,t){return(await x(e,t)).item.merge_ready||null}r(yd,"heldMerge");async function xd(e,t){const{source:n,wave:a,item:i}=await x(e,t);(!i.merge_ready||Y(i))&&l(`${t} isn't waiting for its Merge`);const o=await Gt(e,t,i);return o&&l(`${t} is blocked ${o}, so its Merge waits until nothing blocks it`),i.merge_ready.tapped=new Date().toISOString(),await y(n.file,a),i.merge_ready}r(xd,"tapMerge");async function vd(e,t){const{source:n,item:a}=await x(e,t);return(await Promise.all((await H(n.dir,a.id)).map(o=>$(o,null)))).filter(o=>o?.worktree?.path).sort((o,s)=>String(s.started).localeCompare(String(o.started)))[0]?.model}r(vd,"builderModel");async function Kn(e,t,n,a,i,o,s,c){try{const d=await ie(e),u=kn(d.tickets||{source:"internal"}),p=r((h,g)=>h.internal===!0?String(g.id).replace(/^S(\d+)$/,`${u.prefix}-$1`):null,"ownId");i===void 0&&u.source==="internal"&&bn(u,p(n,a))&&(i=p(n,a));let w=0;if(u.source==="internal")for(const h of await F(e)){const g=await $(h.file,{});for(const k of g.items||[]){const v=[k.ticket,p(g,k)];if(w=Math.max(w,...v.map(D=>bn(u,D)||0)),i!==void 0&&`${h.slug}/${k.id}`!==t&&v.includes(i))throw Object.assign(new re(`${i} is already the ticket of ${h.slug}/${k.id}`),{held:!0})}}c??=i===void 0&&u.source!=="internal"&&a.plan?await P.readFile(await Yt(s,a.plan),"utf8"):a.notes||"";const f=await yo(u,{ticket:i,title:a.title,body:c,repo:oe(e,n),next:w+1},o);a.ticket=f.ticket,f.created&&u.source!=="internal"?a.ticket_created=!0:delete a.ticket_created}catch(d){if(d instanceof re)throw d;l(d.message)}}r(Kn,"assignTicket");const Ee=r((e,t)=>wt(m.join(e||Ct(),"tickets"),t,{heartbeat:!0}),"ticketWrite"),pe=r((e,t,n,a)=>n.ticket!==void 0||n.fromAsk!==void 0?Ee(e,()=>ki(e,t,n,a)):ki(e,t,n,a),"setItem"),_d=r((e,t,n,a)=>Ee(e,()=>cr(e,t,n,a)),"addItem");async function cr(e,t,{title:n,notes:a,notesFrom:i,deps:o,ticket:s,pickUpAt:c,held:d,parent:u,size:p,model:w,phase:f,state:h,relate:g,fromAsk:k,text:v,intake:D=!1},O){const{wave:B,id:N}=t.includes("/")?I(t):{wave:t};N!==void 0&&Pe(N,"the item id"),fi(f,h),v!==void 0&&S(v,"--text"),v!==void 0&&k===void 0&&l("--text goes with --from-ask"),k!==void 0&&v===void 0&&l(`--from-ask needs --text "<first line>", the item's first thread line after the ask`);const _=k===void 0?void 0:await ot(e,k);_&&await Mt(e,_);const E=yi({held:d,pickUpAt:c});E&&f!==void 0&&l("a card filed held is filed New and takes no --phase; drop --phase, or drop --held to file it in a phase"),_&&f===void 0&&!E&&l("--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"&&!(_&&await wi(e,_))&&l(_?`ask "${_}" 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"&&!p&&l("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&&l("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&&!E&&l("item add files a card on a board: pass --phase grilling or prototyping, or --held to file it for later");const{source:C,wave:T}=await Te(e,B);N!==void 0&&T.items.some(ee=>ee.id===N)&&l(`wave "${B}" already has an item "${N}"`);const W=new Date().toISOString(),j={id:N,title:S(n,"--title"),created:W,lane:"todo",lane_at:W};u!==void 0&&(j.parent=await Si(e,B,null,u)||l("--parent takes a card, such as <wave>/<id>")),a!==void 0&&i!==void 0&&l("pass --notes or --notes-from, not both");const Q=i!==void 0?await ei(e,i,"--notes-from",()=>`${Mn}, and link that card with --relate`):a===void 0?"":S(a,"--notes");i!==void 0&&!Q.trim()&&l(`--notes-from ${i} is empty`);const ae=i!==void 0?Q:`# ${j.title}
|
|
26
|
-
${
|
|
27
|
-
${
|
|
28
|
-
`}`;bi(j,{size:p,model:w}),o!==void 0&&(j.deps=await _i(e,B,o)),E&&Zt(j,E),E&&f===void 0&&(j.filed_held=!0),g!==void 0&&(j.related=await Vt(e,t,g,"--relate")),_&&(j.from_ask=_),f!==void 0&&(j.phase=f),await Kn(e,t,T,j,s,O,C.dir,Q),N===void 0&&(j.id=So(j.ticket),T.items.some(ee=>ee.id===j.id)&&l(`wave "${B}" already has an item "${j.id}"; link ${j.ticket} with item add ${B}/<id> --ticket ${j.ticket}`)),j.plan=qn(C.dir,j.id),await Fn(m.join(C.dir,j.plan),ae);const Z=`${B}/${j.id}`;if(T.items.push(j),await y(C.file,T),_){await U(e,Z,{text:v});const ee=await De(e);X(ee,_)!==void 0&&(delete ee[_],await y(ke(e),{links:ee}))}return e&&await Oi(e,Z,{related:new Set,parent:null},await zn(e,Z)),{file:C.file,key:Z,lines:[]}}r(cr,"createItem");const Sd=r((e,{dryRun:t=!1}={},n)=>Ee(A(e),()=>dr(e,t,n)),"backfillTickets");async function dr(e,t,n){const a=await Pi(e)||l("this board has no ticket source yet; pick one in Board settings first"),i=new Set,o=[];for(const p of await F(e)){const w=await $(p.file,null);if(Array.isArray(w?.items)){o.push({entry:p,wave:w});for(const f of w.items)f.ticket&&i.add(f.ticket),w.internal===!0&&a.source==="internal"&&i.add(String(f.id).replace(/^S(\d+)$/,`${a.prefix}-$1`))}}const s=[],c={linked:0,created:0,failed:0},d=r((p,w)=>w.ticket&&!(a.source!=="internal"&&p.internal===!0&&w.ticket===w.id),"ticketed");for(const{entry:p,wave:w}of o)for(const f of w.items){if(d(w,f)||Y(f))continue;const h=`${p.slug}/${f.id}`,g=[..._o(a,f.title),...(f.linear||[]).filter(k=>bn(a,k))].find(k=>a.source!=="internal"||!i.has(k));if(t){c[g?"linked":"created"]++,s.push(`${h} would ${g?`link ${g}`:"get a new ticket"}`);continue}try{await Kn(e,h,w,f,g,n,p.dir),await y(p.file,w),i.add(f.ticket),c[g?"linked":"created"]++,s.push(`${h} ${g?"linked":"created"} ${f.ticket}`)}catch(k){if(!(k instanceof re))throw k;c.failed++,s.push(`${h} failed: ${k.message}`)}}const u=t?`would link ${c.linked}, create ${c.created}`:`linked ${c.linked}, created ${c.created}, failed ${c.failed}`;return{lines:[...s,`backfill${t?" (dry run)":""}: ${u}`]}}r(dr,"backfill");const jd=r(e=>Ee(e,async()=>{const t=[];for(const n of await F(e)){const a=await $(n.file,null);if(!Array.isArray(a?.items)||!Array.isArray(a.followups))continue;const i=a.followups.find(d=>a.items.some(u=>u.id===d.id));i&&l(`${n.slug}: follow-up ${i.id} has the id of a card; rename one, then restart the board`);const o=[];for(const{id:d,title:u,from:p,filed:w,done:f,...h}of a.followups){const g=f?e&&(await G(M(e,`${n.slug}/${d}`))).map(k=>k.ts).filter(Boolean).sort().pop()||w:null;o.push({id:d,title:u,...w?{created:w}:{},...p?{parent:J(n.slug,p)}:{},...f?{lane:"done",...g?{lane_at:g,closed_at:g}:{}}:{},...h})}const{followups:s,...c}=a;await y(n.file,{...c,items:[...a.items,...o]}),t.push(`${n.slug}: ${o.length} follow-ups moved to cards (${o.filter(d=>d.lane==="done").length} done)`)}return t}),"migrateFollowups");function Di(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=Oe(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&&Je(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(Di,"statusToFacts");const Od=r(e=>Ee(e,async()=>{const t=[];for(const n of await F(e)){const a=await $(n.file,null);let i=0;for(const o of Array.isArray(a?.items)?a.items:[]){const s=o.status==="checking"?await H(n.dir,o.id):[],c=await Promise.all(s.map(d=>$(d,null)));if(Di(o,c.filter(Boolean))){i++;for(const[d,u]of c.entries())u?.handed_back&&await y(s[d],u)}}i&&(await y(n.file,a),t.push(`${n.slug}: ${i} cards' statuses became lanes and facts`))}return await xt(e,async()=>{const n=await Me(e),a=n.filter(i=>i.item&&Di(i.item)).length;a&&(await vt(e,n),t.push(`archive: ${a} cards' statuses became lanes and facts`))}),t}),"migrateStatuses");function Ai(e,t){const n=e.lane==="done"&&!un.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=Oe(e);e.phase=a.some(i=>i.merged_at)||!a.length&&t?"merged":"closed"}return!0}r(Ai,"stateToPhase");const Pd=r(e=>Ee(e,async()=>{const t=[],n=await F(e),a=r(async(i,o)=>!!i&&o.lane==="done"&&(await Promise.all((await H(i,o.id)).map(s=>$(s,null)))).some(s=>s?.started&&Je(s)),"built");for(const i of n){const o=await $(i.file,null);let s=0;for(const c of Array.isArray(o?.items)?o.items:[])Ai(c,await a(i.dir,c))&&s++;s&&(await y(i.file,o),t.push(`${i.slug}: ${s} cards' states became phases`))}return await xt(e,async()=>{const i=await Me(e);let o=0;for(const s of i)s.item&&Ai(s.item,await a(n.find(c=>c.slug===(s.wave||I(s.key).wave))?.dir,s.item))&&o++;o&&(await vt(e,i),t.push(`archive: ${o} cards' states became phases`))}),t}),"migratePhases"),Dd=r(e=>Ee(e,async()=>{const t=[];for(const n of await F(e)){const a=await $(n.file,null),i=(Array.isArray(a?.items)?a.items:[]).filter(o=>o.implement_at);if(i.length){for(const o of i)o.phase!=="ready"&&Object.assign(o,{phase:"staging",staged_at:o.implement_at}),delete o.implement_at;await y(n.file,a),t.push(`${n.slug}: ${i.length} Implement asks moved to staging or ready`)}}return t}),"stageImplementAsks"),Xt=r(e=>m.join(e,"data","done-archive.json"),"archiveFile"),xt=r((e,t)=>wt(m.join(e,"done-archive"),t,{heartbeat:!0}),"archiveWrite"),Me=r(async e=>(await $(Xt(e),{cards:[]})).cards||[],"readArchive"),en=r(e=>`${e.closed_at||""}|${e.key}`,"archiveRank"),lr=r((e,t)=>en(e)<en(t)?1:en(e)>en(t)?-1:0,"byClosed"),Jn=r(e=>m.join(e,"data","done-archive-count.json"),"archiveCountFile");async function Ad(e){const t=(await $(Jn(e),null))?.count;if(Number.isInteger(t))return t;const n=(await Me(e)).length;return await y(Jn(e),{count:n}),n}r(Ad,"archiveCount");async function vt(e,t){t.length?await y(Xt(e),{cards:t.sort(lr)}):await P.rm(Xt(e),{force:!0}),await y(Jn(e),{count:t.length})}r(vt,"writeArchive");const Rd=r((e,t)=>xt(e,async()=>{const n=new Map(t.map(w=>[w.key,w])),a=new Date().toISOString(),i=[],o=[];for(const w of await F(e)){const f=await $(w.file,null);if(!Array.isArray(f?.items))continue;const h=f.items.filter(g=>n.has(`${w.slug}/${g.id}`)&&g.lane==="done");if(h.length){for(const g of h){const k=n.get(`${w.slug}/${g.id}`);i.push({...k,wave:w.slug,archived_at:a,item:k.prs?ur(g,k.prs):g})}o.push([w.file,{...f,items:f.items.filter(g=>!h.includes(g))}])}}if(!i.length)return[];const s=await $(L(e),{items:[]}),c=new Map(i.map(w=>[`item:${w.key}`,w]));for(const w of i)w.questions=(s.items||[]).filter(f=>f.topic===`item:${w.key}`&&f.resolved);const d=await De(e);for(const w of i)w.asks=w.item.from_ask&&X(d,w.item.from_ask)===void 0?[w.item.from_ask]:[];const u=new Set(i.map(w=>w.key));await vt(e,[...(await Me(e)).filter(w=>!u.has(w.key)),...i.map(({key:w,wave:f,archived_at:h,closed_at:g,item:k,questions:v,asks:D})=>({key:w,wave:f,archived_at:h,closed_at:g,item:k,questions:v,asks:D}))]),i.some(w=>w.asks.length)&&await y(ke(e),{links:{...d,...Object.fromEntries(i.flatMap(w=>w.asks.map(f=>[f,w.key])))}});for(const[w,f]of o)await y(w,f);const p=(s.items||[]).filter(w=>!(c.has(w.topic)&&w.resolved));return p.length!==(s.items||[]).length&&await y(L(e),{...s,items:p}),[...u]}),"archiveCards");function ur(e,t){if(!Ce(e))return e;const n=Oe(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(ur,"keptPRs");async function fr(e,t){return!e||!fe(Xt(e))?!1:xt(e,async()=>{const n=await Me(e),a=n.find(w=>w.key===t);if(!a)return!1;const i=(await F(e)).find(w=>w.slug===a.wave),o=i&&await $(i.file,null);if(!Array.isArray(o?.items))return!1;o.items.some(w=>w.id===a.item.id)||await y(i.file,{...o,items:[...o.items,a.item]});const s=await $(L(e),{items:[]}),c=new Set((s.items||[]).map(w=>w.id)),d=(a.questions||[]).filter(w=>!c.has(w.id));d.length&&await y(L(e),{...s,items:[...s.items||[],...d]});const u=await De(e),p=(a.asks||[]).filter(w=>X(u,w)===t);return p.length&&await y(ke(e),{links:Object.fromEntries(Object.entries(u).filter(([w])=>!p.includes(w)))}),await vt(e,n.filter(w=>w!==a)),!0})}r(fr,"restoreArchived");const Ri=["push_tag","no_change"],Ni=r(e=>Object.fromEntries(Object.entries(e).filter(([t])=>!Ri.includes(t))),"stripQuestion"),Ei=r(({summary:e,...t})=>t,"stripItem"),Ii=r(e=>Ri.some(t=>t in e),"holdsDead"),Nd=r(e=>xt(e,async()=>{for(const a of await F(e)){const i=await $(a.file,null);Array.isArray(i?.items)&&i.items.some(o=>"summary"in o)&&await y(a.file,{...i,items:i.items.map(Ei)})}const t=await $(L(e),null);(t?.items||[]).some(Ii)&&await y(L(e),{...t,items:t.items.map(Ni)});const n=await Me(e);n.some(a=>"card"in a||"summary"in a.item||(a.questions||[]).some(Ii))&&await vt(e,n.map(({card:a,...i})=>({...i,...a?{closed_at:i.closed_at||a.sortAt}:{},item:Ei(i.item),questions:(i.questions||[]).map(Ni)})))}),"stripDeadFields"),Bi=r(e=>`lineup-${e}.md`,"lineupNoteFile"),wr=r(e=>typeof e=="string"&&/^[a-z0-9-]+$/.test(e)?e:l(`a wave's slug uses only a-z, 0-9 and "-", not "${e}"`),"lineupSlug");async function tn(e,t){for(const n of await F(e)){const a=await $(n.file,null),i=(a?.lineups||[]).find(o=>o.slug===t);if(i)return{source:n,wave:a,lineup:i}}return l(`this board has no wave "${t}"; lineup create makes one`)}r(tn,"findLineup");async function pr(e,t){(await ie(e)).items!=="waves"&&l(`topic wave:${t} is for a board over ~/.claude/waves; a lineup's triage uses lineup:<slug>`),await Te(e,t)}r(pr,"triagedWave");function _t(e,t,n,a){return n.map(i=>{const{wave:o,id:s}=I(i);o!==e.slug&&l(`${i} is not in wave file "${e.slug}"; a wave's cards share one file`),t.items.some(d=>d.id===s)||l(`wave "${o}" has no item "${s}"`);const c=(t.lineups||[]).find(d=>d.slug!==a&&!d.finished_at&&wn(d).flat().includes(s));return c&&l(`${i} is already in the wave "${c.slug}"; lineup remove ${c.slug} ${i} first`),s})}r(_t,"lineupCards");function Qn(e,t){t!==void 0&&l("--phase is now --step: pass one --step per wave step, as <wave>/<id>,<wave>/<id>, in build order");const n=[e??[]].flat().map(a=>String(a).split(",").map(i=>i.trim()).filter(Boolean));return(!n.length||n.some(a=>!a.length))&&l("--step takes one step's cards, as <wave>/<id>,<wave>/<id>, once per step in build order"),n}r(Qn,"stepKeys");function Li(e,t,n,a){const i=new Set(n.map(o=>`${e.slug}/${o}`));for(const o of n){const s=t.items.find(u=>u.id===o),c=(s.deps||[]).filter(u=>!i.has(J(e.slug,u))),d=a.findIndex(u=>u.includes(o));d>0&&c.push(...a[d-1]),c.length?s.deps=c:delete s.deps}}r(Li,"chain");async function Ci(e,t,n,a,i){i!==void 0&&await mt(m.join(t.dir,Bi(a.slug)),`${S(i,"the note").replace(/\n*$/,`
|
|
29
|
-
`
|
|
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}
|
|
30
30
|
|
|
31
|
-
**Order** ${
|
|
31
|
+
**Order** ${no(a,i,o,n)}
|
|
32
32
|
|
|
33
|
-
${s}`}}r(Ld,"readLineupNotes");async function q(e,t,n,a="main"){const{id:i}=I(t);/^[\w-]{1,40}$/.test(a)||l(`--agent takes a name of letters, digits, "_" and "-", not "${a}"`);const o=a==="main"?`${i}.json`:`${i}@${a}.json`;if(n)return m.join(m.resolve(n),"agents",o);const{source:s}=await x(e,t);return m.join(s.dir,"agents",o)}r(q,"builderFile");const qi=r((e,t)=>typeof e=="string"&&/^\d{4}-\d\d-\d\dT/.test(e)&&!Number.isNaN(Date.parse(e))?new Date(e).toISOString():l(`${t} takes an ISO time such as 2026-10-23T16:30:00-07:00`),"isoTime"),hr=un.progress,Fi=80,Mi="reading, building, testing, checking or landing";function Wi(e,t,n,a){e||l(`${a} needs --site, since the line goes to the item's thread`),t!==void 0&&!hr.includes(t)&&l(`--phase takes ${Mi}, not "${t}"`);const i=S(n,a);return i.length>Fi&&l(`${a} is ${i.length} characters; the cap is ${Fi}. Say it shorter.`),{phase:t,detail:i}}r(Wi,"progressLine");const Ui=r((e,t,n)=>t?{detail:e.detail}:(e.phase===void 0&&l(`${n} needs --phase ${Mi}`),e),"forAgent");async function Gn(e,t,n="main",a,{phase:i,detail:o},s){const c=Be(a),d=c||n==="main"?"":a.kind===se?n:Bt(a)||a.kind||n,u=c?co[a.kind]:[d,i].filter(Boolean).join(" "),p=new RegExp(`^(?:${u}|${i??u})\\s*:?\\s+`,"i"),w=u?`${u[0].toUpperCase()}${u.slice(1)}: ${o.replace(p,"")}`:o,f=M(e,t);(await G(f)).filter(h=>h.kind==="status").pop()?.text!==w&&await z(f,{ts:s,who:"claude",kind:"status",run:a.started,...i?{phase:i}:{},...c?{planner:!0}:{},text:w})}r(Gn,"statusToThread");const mr={prototype:"sonnet",grill:"opus",[we]:"sonnet"};async function St(e,t,{label:n,phase:a,progress:i,dir:o,agent:s="main",kind:c,model:d,eta:u,worktree:p,ownWorktree:w,dispatch:f}){a!==void 0&&i===void 0&&l('--phase goes with --progress "<detail>"');const h=ne(i,te=>Wi(e,a,te,"--progress")),g=await q(e,t,o,s),k=new Date().toISOString(),v=ne(c,te=>/^[\w.-]{1,30}$/.test(te)?te:l("--kind takes a short word such as claude or codex")),D=ft(v),O=d??(v==="opus"||v==="sonnet"?v:void 0),B=["build",..._r].includes(D),N=await $(g,null),_=r(te=>["tap","hub"].includes(te?.dispatched)&&!te.done,"tapOpen");if(f==="session"&&_(N))return{file:g,alreadyOpen:!0,lines:[]};s!=="main"&&v!==we&&v!==se&&_(await $(await q(e,t,o),null))&&l(`the Implement tap opened ${t}'s main builder record and no agent has taken it up; run builder start ${t} without --agent first`);const E=N?.dispatched&&!N.done?N:null,C=B&&!o?(await x(e,t)).item:void 0,T=N&&We(N).length?N:null,W=E&&D==="build"?{kind:ft(E.kind),model:E.model,reason:{tap:"The owner's Build tap picked it.",hub:"The board's pull picked it."}[E.dispatched]??"The session's dispatch picked it."}:B?gn({card:C??{}}):null;W&&D!=="build"&&D!==W.kind&&l(`--kind ${D}: ${W.kind} builds now. ${W.reason} Pass --kind ${W.kind}, or --kind build.`);const j=W?W.kind:D,Q=fa({dir:e});if(ho(Q,j))throw new re(po(Q,j));const ae=h&&Ui(h,Be({kind:j}),"--progress");d!==void 0&&!["opus","sonnet"].includes(d)&&l("--model takes opus or sonnet"),O!==void 0&&j==="codex"&&l("--model is for kind claude; codex needs none");const Z=j==="claude"?$n():"auto";O!==void 0&&Z!=="auto"&&O!==Z&&l(`--model ${O}: the hub's Claude model is ${Z==="opus"?"Opus":"Sonnet"}. Pass --model ${Z}, or no --model.`);const ee=j==="claude"?O??W?.model??(E&&ft(E.kind)==="claude"?E.model:void 0)??"opus":Be({kind:j})?O??(E?.kind===j?E.model:void 0)??mr[j]:j===se?O??N?.model:void 0,ye={label:n===void 0?"Builder":S(n,"--label"),...j?{kind:j}:{},...ee?{model:ee}:{},started:k,...u===void 0?{}:{eta:qi(u,"--eta")},...f?{dispatched:f}:{},...T?{calls:T.calls,...T.verified?{verified:T.verified}:{}}:{},...yr(N)?{session:N.session}:{},...j===se&&N?.kind===se?{label:N.label,task:N.task,writes:N.writes,requested:N.requested}:{}},Ot=B&&!o&&!Y(C)&&C.lane!=="progress",rt=Ot?await pe(e,t,{start:!0}):{lines:[]};let le,xe=null;const Pt=w&&B&&s==="main"?[N?.worktree,C?.worktree].find(te=>te?.path&&fe(te.path))??null:null,ce=(E??T)?.worktree??Pt,Dt=p??(B&&s==="main"&&!ce?w:void 0);try{if(Dt){const te=await H(m.dirname(m.dirname(g)),I(t).id),ve=(await Promise.all(te.filter(ue=>ue!==g).map(ue=>$(ue,null)))).filter(ue=>ue?.worktree?.path&&!ue.done).map(ue=>ue.worktree.path);({remade:xe,...le}=await Dt({busy:ve}))}else le=ce}catch(te){throw Ot&&await vr(e,t,C),te}await gr(g,N),await y(g,le?{...ye,worktree:le}:ye),C?.rebase_run&&s==="main"&&![Hi,la].includes(ye.label)&&await zi(e,t),W?.inputs&&e&&Ao(wa(e),{call:"builder-pick",key:t,result:[j,j==="claude"?ee:null].filter(Boolean).join(" "),reason:W.reason,inputs:{...W.inputs,...O?{askedModel:O}:{},...f?{dispatch:f}:{}}}),xe&&e&&await U(e,t,{text:xe}),ae&&await Gn(e,t,s,ye,ae,k);const an=v===we||v===se?[]:await Jt(e,t,"Acted on: builder started");return e&&B&&await Hs(e,t),{file:g,kind:j??null,model:ee??null,reason:W?.reason??null,worktree:le?.path??null,lines:[...D==="build"?[`kind: ${j}. ${W.reason}`]:[],...Bt(ye)?[`model: ${Bt(ye)}`]:[],...e?[`ref: ${await K(e,t)}`]:[],...le?[`worktree: ${le.path}`]:[],...xe?[xe]:[],...rt.lines,...an]}}r(St,"startBuilder");async function gr(e,t){if(!t?.started||!t.done||!e.endsWith(".json")||m.basename(e).includes("@"))return;const{label:n,kind:a,model:i,started:o,done:s,session:c}=t,d=m.join(m.dirname(e),`${m.basename(e,".json")}@past-${Date.parse(o)}.json`);await y(d,{label:n,...a?{kind:a}:{},...i?{model:i}:{},started:o,done:s,...c?.log?{session:{log:c.log}}:{}})}r(gr,"keepPastRun");async function Cd(e,t,n,a="tap"){try{const{held:i,blockers:o}=await kr(e,t);if(i&&await xi(e,"card-hold",{key:t,hold:!1}),o.length)return await br(e,t,o);try{const s=await St(e,t,{kind:"build",worktree:n,dispatch:a});return{key:t,ref:await K(e,t),kind:s.kind,...s.model?{model:s.model}:{},reason:s.reason,worktree:s.worktree}}catch(s){throw i&&await xr(e,t,i),s}}catch(i){if(!(i instanceof re))throw i;return await z(M(e,t),{ts:new Date().toISOString(),who:"claude",kind:da,text:`Couldn't start implementing: ${i.message}`}),null}}r(Cd,"startTappedBuild");async function Td(e,t,{pr:n,conflict:a,base:i},o){const{source:s,wave:c,item:d}=await x(e,t);d.rebase_run={pr:n,base:i,conflict:a,at:new Date().toISOString()},await y(s.file,c);try{const u=await St(e,t,{kind:"build",label:Hi,dispatch:"hub",ownWorktree:o});return{key:t,kind:u.kind,...u.model?{model:u.model}:{},worktree:u.worktree}}catch(u){throw await zi(e,t),u}}r(Td,"startRebase");const Hi="Rebase";async function zi(e,t){const{source:n,wave:a,item:i}=await x(e,t);delete i.rebase_run,await y(n.file,a)}r(zi,"endRebaseRun");async function qd(e,t){const{wave:n,item:a}=await x(e,t),i=oe(e,n);return Le(a,i).find(o=>o.repo===i)?.number??null}r(qd,"cardPR");async function Fd(e,t,{from:n,to:a,replayed:i}){const{source:o,wave:s,item:c}=await x(e,t);return c.rebase_run?(c.rebased=[...c.rebased||[],{from:n,to:a,base:c.rebase_run.base,replayed:i,at:new Date().toISOString()}],delete c.rebase_run,await y(o.file,s),!0):!1}r(Fd,"recordRebase");const $r=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 kr(e,t){const n=fa({dir:e});mo(n)&&l(wo(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&&Ne(a)&&l("it's held. Unhold it, then tap Implement."),await Hn(e,t)&&l("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 Ys(e,t,a)}}r(kr,"tapRefusals");async function Md(e,t,n){await U(e,t,{text:he.implement,who:"love",by:n,command:"implement"}),await Fs(e,t,{})}r(Md,"implementAsIs");async function br(e,t,n){const a=n.join(", ");return await me({dir:e},"card-bump",{key:t}),await z(M(e,t),{ts:new Date().toISOString(),who:"claude",text:`Can\u2019t start yet: waits on ${a}. Bumped to the top of Todo.`}),{key:t,ref:await K(e,t),bumped:n,line:`bumped: ${t} waits on ${a}, so it stays in Todo, bumped to the top`}}r(br,"bumpBlocked");async function Wd(e,t,n){const{source:a,wave:i,item:o}=await x(e,t),s=new Date().toISOString();Qt(o,"todo",s,"ready"),delete o.staged_at,o.bumped_out={by:n,at:s},await y(a.file,i),await nn(a.dir,o.id,!1,{endHandBack:!0});const c=await K(e,n);return await z(M(e,t),{ts:s,who:"claude",text:`Bumped out by ${c}: back to Todo, ready. It starts again once a slot frees.`}),`${await K(e,t)} bumped out by ${c}`}r(Wd,"sendBackBumpedOut");async function Ud(e,t,n){const a=await q(e,t),i=await $(a,null),o=new Date().toISOString();i&&!i.done&&await y(a,{...i,done:o});const{source:s,wave:c,item:d}=await x(e,t);d.lane==="progress"&&Qt(d,"todo",o),await y(s.file,c),await z(M(e,t),{ts:o,who:"claude",kind:da,text:`Couldn't start implementing: ${n}`})}r(Ud,"abandonLaunch");async function Hd(e,t,n,a="main"){const i=await q(e,t,void 0,a),o=await $(i,null);o&&!o.done&&await y(i,{...o,done:new Date().toISOString()}),await nt(e,t),await U(e,t,{text:n})}r(Hd,"abandonPlanner");async function zd(e,t,n,a,i="main",o=null){const s=await q(e,t,void 0,i),c=await $(s,null);await y(s,{...c,session:{id:n,pid:a,log:o,at:new Date().toISOString()}})}r(zd,"recordSession");async function Kd(e,t){const{source:n}=await x(e,t);return(await Promise.all((await H(n.dir,I(t).id)).map(i=>$(i,null)))).filter(i=>i&&i.started&&!i.done).map(i=>({pid:i.session?.pid??null,at:i.session?.at??null}))}r(Kd,"openAgents");async function Jd(e,t){const{source:n}=await x(e,t);return(await Promise.all((await H(n.dir,I(t).id)).map(i=>$(i,null)))).filter(i=>i&&i.started&&!i.done).map(i=>({kind:i.kind??null,pid:i.session?.pid??null,at:i.session?.at??null}))}r(Jd,"openRuns");async function Qd(e,t,n){const{source:a}=await x(e,t),i=new Date().toISOString();for(const o of await H(a.dir,I(t).id)){const s=await $(o,null);s?.started&&!s.done&&Je(s)&&await y(o,{...s,stopped:{run:n,at:i}})}}r(Qd,"markStopped");async function Gd(e,t){const{source:n,wave:a,item:i}=await x(e,t);i.merge_ready&&(i.merge_ready.stopped=new Date().toISOString(),await y(n.file,a))}r(Gd,"stopHeldMerge");async function Yd(e,t,n){const a=S(n,"item stall --reason"),{source:i,wave:o,item:s}=await x(e,t);return s.lane==="done"&&l(`${await K(e,t)} is done; there is nothing to stall`),s.stalled={reason:a,at:new Date().toISOString()},await y(i.file,o),a}r(Yd,"markStalled");async function Zd(e,t){const{source:n,wave:a,item:i}=await x(e,t);i.stalled&&(delete i.stalled,await y(n.file,a))}r(Zd,"clearStall");async function Vd(e,t){const{item:n}=await x(e,t),a=await G(m.join(e,"data","events.jsonl")),i=r(c=>a.filter(d=>d.kind===c&&d.data?.key===t).map(d=>String(d.ts)).sort().pop()||"","latest"),o=i("card-bump");if(!o||i("card-unbump")>o)return null;const s=[n.closed_at,...Oe(n).map(c=>c.merged_at)].filter(Boolean).map(String).sort().pop()||"";return o>s?o:null}r(Vd,"landFirst");async function Xd(e,t,n){const{item:a}=await x(e,t);Y(a)&&l(`${await K(e,t)} is done; there is nothing to bump`);const i=await me({dir:e},n?"card-bump":"card-unbump",{key:t});return(await G(m.join(e,"data","events.jsonl"))).find(o=>o.id===i).ts}r(Xd,"bumpCard");async function el(e,t){return(await G(M(e,t))).filter(n=>n.kind==="status").pop()?.phase==="landing"}r(el,"isLanding");const yr=r(e=>!!e?.session&&e.session.at>(e.done??e.started),"launchedSession");async function tl(e,t,n){const a=await $(await q(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(tl,"startPlanner");async function nl(e,t,n){const{source:a}=await x(e,t);if((await Promise.all((await H(a.dir,I(t).id)).map(c=>$(c,null)))).some(c=>c?.kind===we&&c.started&&!c.done))return null;const o=fo(n);if(await $(await q(e,t,void 0,o),null))return null;const s=await St(e,t,{kind:we,agent:o,dispatch:"hub"});return{key:t,kind:we,model:s.model,agent:o}}r(nl,"startVisual");const Yn=r(e=>typeof e=="string"&&/^[a-z][a-z0-9-]{0,29}$/.test(e)&&e!=="main"&&!e.startsWith(`${we}-`)?e:l('--name takes a short lowercase name such as tests or docs-pass: letters, digits and "-", not main or visual-\u2026'),"helperName");async function Zn(e,t){const{item:n}=await x(e,t);return[(await $(await q(e,t),null))?.worktree,n.worktree].find(i=>i?.path&&fe(i.path))??null}r(Zn,"cardWorktree");async function al(e,t,{name:n,task:a,writes:i,model:o}){A(e);const s=Yn(n),c=S(a,"--task");o!==void 0&&!["opus","sonnet"].includes(o)&&l("--model takes opus or sonnet");const d=$n();o!==void 0&&d!=="auto"&&o!==d&&l(`--model ${o}: the hub's Claude model is ${d==="opus"?"Opus":"Sonnet"}. Pass --model ${d}, or no --model.`);const{item:u}=await x(e,t);Y(u)&&l(`${await K(e,t)} is Done; a helper needs an open card`);const p=await q(e,t,void 0,s);return fe(p)&&l(`${t} already has an agent named ${s}; pick another --name`),i&&!await Zn(e,t)&&l(`a helper that writes branches from ${t}'s worktree, and the card has none; start its build first, or drop --writes`),await y(p,{label:s,kind:se,task:c,writes:!!i,...o?{model:o}:{},requested:new Date().toISOString()}),await U(e,t,{text:`Asked for helper ${s}${i?", which writes code":""}: ${c}`}),{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(al,"requestHelper");async function il(e,t,n,a){const i=await q(e,t,void 0,n),o=await $(i,null);if(o?.kind!==se||o.started||o.done)return null;const{item:s}=await x(e,t),c=gn({card:s}),d=$n(),u=o.model??(d!=="auto"?d:c.kind==="claude"?c.model:"opus"),p=o.writes?await Zn(e,t):null;o.writes&&!p&&l(`${t} has no worktree for helper ${n} to branch from`),await y(i,{...o,model:u});const w=await St(e,t,{agent:n,kind:se,dispatch:"hub",worktree:p?({busy:f})=>a({repo:p.repo,base:p.branch,busy:f}):void 0});return{key:t,kind:se,model:u,agent:n,task:o.task,writes:o.writes,worktree:w.worktree}}r(il,"startHelper");async function ol(e,t,n){const a=await $(await q(e,t,void 0,Yn(n)),null);return a?.kind!==se&&l(`${t} has no helper named ${n}`),{record:a,card:await Zn(e,t)}}r(ol,"helperOf");async function sl(e,t,{name:n,agent:a="main",timeout:i=9,every:o=2e3,alive:s}){A(e);const c=Number(i);c>0||l("--timeout takes minutes, a number above 0");const d=await q(e,t,void 0,Yn(n)),u=await q(e,t,void 0,a),p=r(async f=>{const h=await $(u,null);if(!h?.started||h.done)return;const{waits:g,...k}=h;await y(u,f?{...k,waits:f}:k)},"mark"),w=Date.now()+c*6e4;await p(n);try{for(;;){const f=await $(d,null);if(f?.kind!==se&&l(`${t} has no helper named ${n}`),f.done){const g=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"}`,...g]}}const h=f.session?.pid??null;h&&!s(h)&&l(`helper ${n}'s process exited without builder done; do its task yourself`),Date.now()>=w&&l(`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(g=>setTimeout(g,o))}}finally{await p(null)}}r(sl,"waitHelper");async function rl(e,t,n="main"){return n==="main"?!1:(await $(await q(e,t,void 0,n),null))?.kind===se}r(rl,"isHelper");async function cl(e,t,n="main"){const a=await $(await q(e,t,void 0,n),null);return!!(a?.started&&!a.done&&Be(a))}r(cl,"isPlanner");async function dl(e,t,{file:n,title:a}){A(e);const i=S(a,"--title");(typeof n!="string"||!n.endsWith(".html"))&&l("--file takes the page, an .html file");const o=await P.readFile(m.resolve(n),"utf8").catch(()=>l(`--file ${n} can't be read`));o.trim()||l(`--file ${n} is empty`),await x(e,t);const{id:s}=I(t),c=m.join(e,"public","visuals",s);await P.mkdir(c,{recursive:!0});const d=i.toLowerCase().replace(/[^a-z0-9]+/g,"-").slice(0,60).replace(/^-+|-+$/g,"")||"visual";let u=`${d}.html`;for(let w=2;fe(m.join(c,u));w++)u=`${d}-${w}.html`;await P.writeFile(m.join(c,u),o,{flag:"wx"});const p=`visuals/${s}/${u}`;return await U(e,t,{text:i,link:p,kind:we}),{lines:[p]}}r(dl,"addVisual");async function ll(e){const t=[];for(const n of await F(e)){const a=await $(n.file,null);for(const i of a?.items||[]){if(Y(i))continue;const o=await $(m.join(n.dir,"agents",`${i.id}.json`),null);!o?.started||o.done||!o.dispatched||Be(o)||t.push({key:`${n.slug}/${i.id}`,kind:ft(o.kind),model:o.model})}}return t}r(ll,"cutOffBuilds");async function xr(e,t,n){const{source:a,wave:i,item:o}=await x(e,t);for(const s of Object.keys(n))n[s]===void 0?delete o[s]:o[s]=n[s];await y(a.file,i)}r(xr,"restoreHold");async function vr(e,t,n){const{source:a,wave:i,item:o}=await x(e,t);for(const s of["lane","lane_at","closed_at","reopened_at"])n[s]===void 0?delete o[s]:o[s]=n[s];await y(a.file,i)}r(vr,"restoreLane");async function ul(e,t){return e||l("audit reads a board: pass --site <route>"),t!==void 0&&I(t),{lines:Ro(No(wa(e),{key:t}))}}r(ul,"auditLog");const _r=["codex","claude"];async function fl(e,t){const{item:n}=await x(e,t),{kind:a,model:i,reason:o}=gn({card:n});return{lines:[`${[a,i].filter(Boolean).join(" ")}: ${o}`]}}r(fl,"pickBuilder");async function Ki(e,t,{dir:n}={}){const{id:a}=I(t),i=m.dirname(await q(e,t,n)),o=await H(m.dirname(i),a);return(await Promise.all(o.map(c=>$(c,null)))).filter(c=>c?.worktree?.path).sort((c,d)=>String(d.started).localeCompare(String(c.started)))[0]?.worktree??null}r(Ki,"recordedWorktree");async function wl(e,t,n,{phase:a,dir:i,agent:o="main",eta:s,clearEta:c}){s!==void 0&&c&&l("pass --eta or --clear-eta, not both");const d=Wi(e,a,n,"builder progress"),u=await q(e,t,i,o),p=await $(u,null);(!p?.started||p.done)&&l(`builder progress needs an open builder record on ${t}; run builder start first`);const w=Ui(d,Be(p),"builder progress");return s!==void 0&&(p.eta=qi(s,"--eta")),c&&delete p.eta,(s!==void 0||c)&&await y(u,p),await Gn(e,t,o,p,w,new Date().toISOString()),u}r(wl,"builderProgress");async function pl(e,t){const{source:n}=await x(e,t),a=M(e,t);if((await G(a)).filter(s=>s.kind==="status").pop()?.phase==="landing")return;const o=(await Promise.all((await H(n.dir,I(t).id)).map(s=>$(s,null)))).filter(s=>s?.started&&Je(s)&&s.label!==la).map(s=>s.started).sort().pop();await z(a,{ts:new Date().toISOString(),who:"claude",kind:"status",phase:"landing",text:"Landing: rebasing onto main and landing",...o?{run:o}:{}})}r(pl,"landingLine");const We=r(e=>(e?.calls||[]).filter(t=>!e.verified||t.at>e.verified.at),"unverifiedCalls");async function hl(e,t,n,{dir:a,agent:i="main"}){const o=S(n,"builder call");/\n/.test(o)&&l("builder call takes one line; record each call on its own");const s=await q(e,t,a,i),c=await $(s,null);return(!c?.started||c.done)&&l(`builder call needs an open builder record on ${t}; run builder start first`),await y(s,{...c,calls:[...c.calls||[],{at:new Date().toISOString(),text:o}]}),s}r(hl,"recordCall");async function jt(e,t){const n=await G(M(e,t)),a=new Map(n.filter(i=>i.kind===It.answer).map(i=>[i.ask,i]));return n.filter(i=>i.kind===It.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(jt,"builderAsks");const Sr=r(e=>ge().find(t=>je(t.dir,e))??{dir:e},"boardTarget");async function ml(e,t,n,{agent:a="main"}){A(e);const i=S(n,"builder ask");/\n/.test(i)&&l("builder ask takes one line; ask each question on its own");const o=await $(await q(e,t,void 0,a),null);(!o?.started||o.done)&&l(`builder ask needs an open builder record on ${t}; run builder start first`);const s=`ask${(await jt(e,t)).length+1}`;return await z(M(e,t),{ts:new Date().toISOString(),who:`${Bt(o)||o.kind||"Claude"} builder`,kind:It.ask,ask:s,agent:a,text:i}),await me(Sr(e),"builder-ask",{key:t,ask:s,agent:a,text:i}),{id:s,lines:[]}}r(ml,"askOrchestrator");async function gl(e,t,n,a,{deferred:i=!1}={}){A(e);const o=S(a,"--text"),s=(await jt(e,t)).find(d=>d.id===n)||l(`no ask "${n}" on ${t}; builder asks ${t} lists them`);s.answer&&l(`${n} on ${t} is already ${s.deferred?"deferred":"answered"}`);const c=new Date().toISOString();if(await z(M(e,t),{ts:c,who:"claude",kind:It.answer,ask:n,...i?{deferred:!0}:{},text:o}),i){const d=await q(e,t,void 0,s.agent),u=await $(d,null);u&&await y(d,{...u,calls:[...u.calls||[],{at:c,text:`Question: ${s.text} (deferred to the owner)`}]})}return{lines:[]}}r(gl,"answerBuilder");async function $l(e,t,{wait:n=!1,timeout:a=9,every:i=2e3}={}){A(e);const o=Number(a);o>0||l("--timeout takes minutes, a number above 0");const s=Date.now()+o*6e4;for(;;){const c=await jt(e,t);if(!c.length)return{lines:["no asks yet"]};const d=c.filter(u=>!u.answer);if(!n||!d.length)return{lines:c.flatMap(u=>[`${u.id}. ${u.text}`,` ${u.answer?`${u.deferred?"deferred":"answered"}: ${u.answer.text}`:"waiting on the orchestrator"}`])};Date.now()>=s&&l(`${d.map(u=>u.id).join(", ")} still waiting on the orchestrator after ${o} minutes; run builder asks ${t} --wait again`),await new Promise(u=>setTimeout(u,i))}}r($l,"listAsks");async function jr(e,t,n){const a=t.topic.startsWith("item:")&&!t.topic.startsWith("item:ask:")?t.topic.slice(5):l(`${n}.ask goes on a card's question, topic item:<wave>/<id>`),i=(await jt(e,a)).find(o=>o.id===t.ask)||l(`${n}.ask names no ask "${t.ask}" on ${a}; builder asks ${a} lists them`);i.deferred||l(`${n}.ask names ${i.id}, which the orchestrator ${i.answer?"answered":"has not deferred"}; only a deferred ask goes to the owner`)}r(jr,"checkAskQuestion");async function kl(e,t,{dir:n,agent:a="main"}){const i=await $(await q(e,t,n,a),null),o=i?.calls||[];if(!o.length)return{lines:["no judgment calls recorded"]};const{verified:s}=i,c=We(i).length?"not verified yet":`verified ${s.at}${s.note?`: ${s.note}`:""}`;return{lines:[...o.map((d,u)=>`${u+1}. ${d.text}`),c]}}r(kl,"listCalls");async function bl(e,t,{dir:n,agent:a="main",note:i}){const o=await q(e,t,n,a),s=await $(o,null);if(We(s).length||l(`${t} has no unverified judgment calls${a==="main"?"":` from agent ${a}`}; builder calls ${t} lists them`),e){const c=new Set((await $(L(e),{items:[]})).items?.filter(u=>u.topic===`item:${t}`&&u.ask).map(u=>u.ask)),d=(await jt(e,t)).find(u=>u.deferred&&u.agent===a&&!c.has(u.id));d&&l(`${d.id} on ${t} was deferred to the owner; add its question with "ask": "${d.id}" first`)}return await y(o,{...s,verified:{at:new Date().toISOString(),...i===void 0?{}:{note:S(i,"--note")}}}),o}r(bl,"verifyCalls");async function yl(e,t){const{source:n,item:a}=await x(e,t),i=r((s,c)=>[(s.options||[]).find(d=>d.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 K(e,t),title:a.title||t,notes:a.plan?m.join(n.dir,a.plan):null,thread:M(e,t),worktree:(await Ki(e,t))?.path??null,answers:o,links:await Or(e,t),rebasePR:a.rebase_run?.pr??null}}r(yl,"briefFacts");async function Or(e,t){const n=new Map;for(const c of await F(e))for(const d of(await $(c.file,null))?.items||[])n.set(`${c.slug}/${d.id}`,{entry:c,it:d});const a=I(t).wave,{it:i}=n.get(t),o=[...n].filter(([,c])=>(c.it.related||[]).some(d=>J(c.entry.slug,d)===t)).map(([c])=>c),s=[...i.parent?[["parent",J(a,i.parent)]]:[],...(i.deps||[]).map(c=>["depends on",J(a,c)]),...[...new Set([...(i.related||[]).map(c=>J(a,c)),...o])].map(c=>["related",c])].filter(([,c])=>n.has(c));return Promise.all(s.map(async([c,d])=>{const{entry:u,it:p}=n.get(d);return{relation:c,ref:await K(e,d),title:p.title||d,lane:Ke[p.lane||"todo"],notes:p.plan?m.join(u.dir,p.plan):null,thread:M(e,d)}}))}r(Or,"cardLinks");async function xl(e){const t=[];for(const n of await F(e)){const a=await $(n.file,null);for(const i of a?.items||[]){if(Y(i))continue;const o=(await Promise.all((await H(n.dir,i.id)).map(u=>$(u,null)))).filter(Boolean);if(!fn(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 c=`${n.slug}/${i.id}`;if((await de(e,c)).asked.some(u=>u.state==="open")||await Gt(e,c,i))continue;const d=o.filter(u=>u.worktree?.path).sort((u,p)=>String(p.started).localeCompare(String(u.started)))[0];d&&fe(d.worktree.path)&&t.push({key:c,verified:s,kind:ft(d.kind),model:d.model})}}return t}r(xl,"landable");async function vl(e,t){const{source:n,item:a}=await x(e,t);(await Promise.all((await H(n.dir,a.id)).map(c=>$(c,null)))).some(c=>c?.started&&!c.done)||l(`${t} has no open builder; only a card whose lander stopped has one to relaunch`),await pe(e,t,{handBack:!0,next:"A lander relaunches to land the verified build"});const o=m.join(e,"data","landers.json"),s=await $(o,{});delete s[t],await P.writeFile(o+".tmp",JSON.stringify(s)+`
|
|
34
|
-
`),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};
|