@lovek4d/kickflip 0.11.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +17 -14
- package/SKILL.md +2 -2
- package/bin/event-lines.mjs +1 -1
- package/board/bin/board.mjs +12 -3
- package/board/domain.mjs +7 -7
- package/board/headless.mjs +8 -8
- package/board/public/app-cards.js +2 -0
- package/board/public/app-header.js +8 -0
- package/board/public/app-lanes.js +43 -19
- package/board/public/app-snapshot.js +1 -2
- package/board/public/app-threads.js +2 -2
- package/board/public/app.html +0 -1
- package/board/public/app.js +3 -25
- package/board/public/col-resize.js +25 -44
- package/board/public/prompts.js +1 -1
- package/board/public/prototype-kit.js +1 -1
- package/board/public/prototype-switcher.js +3 -8
- package/board/public/prototype.html +2 -6
- package/board/public/router.js +16 -22
- package/board/public/shell.js +3 -39
- package/board/public/style.css +42 -95
- package/board/public/ui-cards.js +27 -23
- package/board/public/ui-enums.js +2 -2
- package/board/public/ui-page-header.js +2 -2
- package/board/public/ui-prompts.js +1 -1
- package/board/public/ui-questions.js +2 -3
- package/board/public/ui.js +1 -1
- package/board/public/view-hub.js +1 -1
- package/board/public/view-questions.js +7 -26
- package/board/server.mjs +18 -18
- package/board/store.mjs +28 -28
- package/hub/hub.mjs +8 -8
- package/hub/prompts.mjs +2 -2
- package/package.json +1 -1
- package/board/public/app-needs.js +0 -248
package/board/store.mjs
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
var
|
|
2
|
-
`).filter(Boolean).map(t=>JSON.parse(t))}catch{return[]}},"readLines");async function Je(e,t){await O.mkdir($.dirname(e),{recursive:!0});const n=`${e}.${process.pid}.tmp`;await O.writeFile(n,t),await O.rename(n,e)}r(Je,"replace");const
|
|
3
|
-
`):st(e,async()=>{const i=JSON.parse(n),s=JSON.parse(await O.readFile(e,"utf8").catch(()=>n));await Je(e,JSON.stringify(
|
|
4
|
-
`)})},"writeJSON");async function
|
|
5
|
-
`)}r(
|
|
6
|
-
`).filter(
|
|
7
|
-
`)),{exitCode:p});return await
|
|
8
|
-
`).map(n=>n.replace(/\s+/g," ").trim()).find(Boolean)||"";return t.length>
|
|
9
|
-
`)[0].trim()||(e.data?.files||[]).map(i=>i?.name).filter(Boolean).join(", ")||"Ask"}r(
|
|
10
|
-
`),s}r(
|
|
1
|
+
var Ys=Object.defineProperty;var r=(e,t)=>Ys(e,"name",{value:t,configurable:!0});import O from"node:fs/promises";import{constants as Xs,existsSync as me,readFileSync as eo}from"node:fs";import $ from"node:path";import Rn from"node:os";import{execFile as to}from"node:child_process";import{nodeBin as no}from"../bin/install.mjs";import{gh as qa,limitWords as ao}from"../shared/gh.mjs";import{squashBody as io}from"./commit-message.mjs";import{fileURLToPath as so}from"node:url";import{BUILD_REFUSED as Ma,BUILDER_ASK as Ut,COMMANDS as Jt,LANDER as Wa,LANES as We,MOVE as Ha,PAUSE_CLEARS as Ua,PHASES as En,handedBackAt as Bn,MAX_IMPLEMENTERS as oo,MAX_PLANNERS as ro,MAX_SERVES as co,builds as at,waveItems as He,mergeBlockers as lo,modelName as Kt,noTakePicked as fo,ownerQuestion as Ue,plans as Ne,QUESTION_REOPEN as uo,PLANNER_WORD as wo,providerOf as Ot,owesNext as Ln,pushTag as po,refOf as it,roundOf as In,roundSettledBy as mo,HELPER as ee,VISUAL as X,visualAgent as ho}from"./domain.mjs";import{GITHUB_STATE as Ja,appendEvent as $e,listBoards as be,readManifest as go,samePath as Re,withManifestLock as $o,writeManifest as bo}from"../hub/boards.mjs";import{readUsageGate as Ka,pauseReason as ko,pauseReasonFor as yo,heldFor as xo,holdsWork as _o,builderPick as Cn,pauseEnd as So}from"../shared/usage-gate.mjs";import{reviseQueued as vo,pushKindError as jo,readClaudeModel as Tn}from"../hub/push.mjs";import{AT_RE as Oo}from"../hub/duration.mjs";import{routeRecord as ce}from"../hub/routes.mjs";import{ticketSource as za,resolveTicket as Ao,ticketNamed as Ga,ticketTarget as Po,moveTicket as Do,cardIdOf as Fo}from"./tickets.mjs";import{LINEAR_NEEDS_KEY as No,linearStatus as Ro,linearWorks as Eo}from"../hub/linear.mjs";import{validateBranch as Bo}from"../hub/repos.mjs";import{withFileLock as st}from"../hub/locks.mjs";import{audit as Lo,auditLines as Io,boardAudit as Qa,readAudit as Co}from"../shared/audit.mjs";import{merge3 as To}from"./merge3.mjs";import{HUB as Va,MESSAGE_LIMIT as Za,BRIEF_LINE_LIMIT as Ya,hubLineOf as qo,hubText as Xa,detailOf as ei}from"./hub-lines.mjs";import{fetchPR as ti,reportOf as ni,githubLogin as Mo,postToPR as Wo,postReview as Ho,reviewsSince as Uo,CLAUDE_PREFIX as At,CLAUDE_MARK as qn}from"../hub/pr-report.mjs";import{allLanded as Jo,cardPRs as Pt,hasPR as xe,parsePRRef as Mn,prKeyOf as ai,prNames as Ee,prURL as Wn,storedPRs as Be}from"../shared/card-prs.mjs";import{reviewLinks as Ko,topicItems as zo,reviewersOf as zt}from"./review.mjs";import{readiness as ii}from"../hub/readiness.mjs";import{SlackError as Go,SLACK_NEEDS_TOKEN as Qo,isSignedOff as Vo,readSlack as Zo,savedSlack as Gt,slackDay as Qt,slackUserId as Yo}from"../hub/slack.mjs";import"../bin/install.mjs";import{readPermissions as Xo}from"./permissions.mjs";import{validBuildTarget as si}from"./build-script.mjs";import{parseCheck as er,checkText as tr}from"../shared/owner-check.mjs";import{RuleError as nr,rulesOf as oi,addRule as ar,editRule as ir,removeRule as sr,applyRules as or,readHubRules as Vt,writeHubRules as rr}from"../hub/rules.mjs";import{shareEntries as cr,validLogin as lr}from"../hub/share.mjs";class he extends Error{static{r(this,"BoardError")}}const d=r(e=>{throw new he(e)},"fail"),dr=r(()=>$.join(Rn.homedir(),".claude","kickflip","sites"),"sitesDir"),Zt=r(()=>$.join(Rn.homedir(),".claude","waves"),"wavesDir"),ri=Symbol("read as"),g=r(async(e,t)=>{let n;try{n=await O.readFile(e,"utf8")}catch{return t}try{const a=JSON.parse(n);return a!==null&&typeof a=="object"&&!Array.isArray(a)?{...a,[ri]:n}:a}catch{return t}},"readJSON"),K=r(async e=>{try{return(await O.readFile(e,"utf8")).split(`
|
|
2
|
+
`).filter(Boolean).map(t=>JSON.parse(t))}catch{return[]}},"readLines");async function Je(e,t){await O.mkdir($.dirname(e),{recursive:!0});const n=`${e}.${process.pid}.tmp`;await O.writeFile(n,t),await O.rename(n,e)}r(Je,"replace");const y=r((e,t)=>{const{[ri]:n,...a}=t??{};return n===void 0?Je(e,JSON.stringify(t,null,2)+`
|
|
3
|
+
`):st(e,async()=>{const i=JSON.parse(n),s=JSON.parse(await O.readFile(e,"utf8").catch(()=>n));await Je(e,JSON.stringify(To(i,a,s),null,2)+`
|
|
4
|
+
`)})},"writeJSON");async function de(e,t){await O.mkdir($.dirname(e),{recursive:!0}),await O.appendFile(e,JSON.stringify(t)+`
|
|
5
|
+
`)}r(de,"appendLine");async function Ll(e){/^[\w-]+$/.test(e||"")||d(`--site takes a route such as ios-orchestrator, not "${e}"`);const t=$.join(dr(),e);return await O.stat(t).then(n=>n.isDirectory(),()=>!1)||d(`no site at ${t}`),t}r(Ll,"siteDir");const ie=r(async e=>({title:"Board",items:"waves",...e?await g($.join(e,"board.json"),{}):{}}),"boardConfig");async function q(e){const t=await ie(e);if(t.items==="waves")try{return(await O.readdir(Zt(),{withFileTypes:!0})).filter(a=>a.isDirectory()).map(a=>({slug:a.name,dir:$.join(Zt(),a.name),file:$.join(Zt(),a.name,"wave.json")}))}catch{return[]}const n=$.resolve(e,t.items);return[{slug:t.slug||$.basename(t.items,".json"),dir:$.dirname(n),file:n}]}r(q,"itemSources");const Z=r((e,t)=>$.join(e,"data","threads",t.replace(/[^\w.-]+/g,"__")+".jsonl"),"threadFile"),L=r(e=>$.join(e,"data","owner.json"),"ownerFile"),fe=r(e=>$.join(e,"data","staged-questions.json"),"stagedFile"),ot=r(e=>$.join(e,"data","screening.json"),"screeningFile"),fr=900*1e3,P=r(e=>e||d("this command needs --site <route>"),"needSite"),v=r((e,t)=>typeof e=="string"&&e.trim()?e.trim():d(`${t} must be non-empty text`),"text"),se=r((e,t)=>e===void 0?void 0:t(e),"optional"),ur=/^prototype-[\w-]+\.html(?:[?#]\S*)?$/,wr=/^visuals\/[\w-]+\/[\w-]+\.html$/,Hn=r((e,t)=>typeof e=="string"&&(ur.test(e)||wr.test(e)||/^https?:\/\/\S+$/.test(e))?e:d(`${t} must be an http(s) URL or a site page such as prototype-<topic>.html?q=<id>`),"pageUrl"),Yt=r((e,t)=>typeof e=="boolean"?e:d(`${t} must be true or false`),"bool"),_e=r((e,t)=>typeof e=="string"&&/^[\w.-]+$/.test(e)?e:d(`${t} must use only letters, digits, "_", "." and "-"`),"domId"),rt=r((e,t,n)=>{(!e||typeof e!="object"||Array.isArray(e))&&d(`${n} must be a JSON object`);const a=Object.keys(e).filter(i=>!t.includes(i));a.length&&d(`${n} has unknown field ${a.map(i=>`"${i}"`).join(", ")}; allowed: ${t.join(", ")}`)},"onlyKeys");function F(e){const t=/^([^/\s]+)\/([^/\s]+)$/.exec(e||"");return t?{file:t[1],id:t[2]}:d(`expected <file>/<id>, not "${e}"`)}r(F,"parseKey");async function Ke(e,t){const n=(await q(e)).find(i=>i.slug===t)||d(`this board has no item file "${t}"`),a=await g(n.file,null);return(!a||!Array.isArray(a.items))&&d(`${n.file} is not an item file with items[]`),{source:n,itemFile:a}}r(Ke,"loadItemFile");async function x(e,t){const{file:n,id:a}=F(t);let i=await Ke(e,n);!i.itemFile.items.some(o=>o.id===a)&&await Nc(e,t)&&(i=await Ke(e,n));const s=i.itemFile.items.find(o=>o.id===a)||d(`item file "${n}" has no item "${a}"`);return{...i,item:s}}r(x,"loadItem");const Xt=["md","html","json"];async function Un(e,t){const n=new RegExp(`^${t.replace(/[.]/g,"\\.")}(?:~(\\d+))?\\.(${Xt.join("|")})$`);return(await O.readdir(e).catch(()=>[])).flatMap(a=>{const i=n.exec(a);return i?[{name:a,ext:i[2],n:Number(i[1]||1)}]:[]}).sort((a,i)=>a.n-i.n||Xt.indexOf(a.ext)-Xt.indexOf(i.ext)).filter((a,i,s)=>a.n!==s[i-1]?.n)}r(Un,"replyFilesOf");async function Il(e,t,n,a){P(e),_e(t,"the event id");const i=(await K($.join(e,"data","events.jsonl"))).find(h=>h.id===t);i||d(`no event "${t}" in this site's data/events.jsonl`);const s=i.kind==="ask"?await Qe(e,t):null,o=s?s.key:i.kind==="ask"?`ask:${t}`:i.kind==="item-message"&&typeof i.data?.key=="string"?i.data.key:null;s&&yi(s.item)?(v(n,"the reply"),await xi(e,s,a)):a!==void 0&&d(`reply --title titles the New or answered card an ask became; "${t}" is not one`);const c=$.join(e,"data","replies"),l=await Un(c,t),f=$.join(c,l.length?`${t}~${l.at(-1).n+1}.md`:`${t}.md`);await Je(f,v(n,"the reply"));const p=o?await Kn(e,o,n):[],w=i.kind==="wave-triage"?["wave","file"].filter(h=>typeof i.data?.[h]=="string").map(h=>`${h}:${i.data[h]}`):[],u=s?`item:${s.key}`:w[0];return u&&p.push(...await es(e,u,"Acted on: replied")),{file:f,lines:p}}r(Il,"writeReply");const ci=r(e=>$.join(e,"data","working.json"),"workingFile");async function pr(e){return(await g(ci(e),{})).events||{}}r(pr,"readWorking");const Cl=["ask","item-message","wave-triage","overnight-triage"];async function Tl(e,t){if(!t.length)return;const n=new Date().toISOString(),a=await pr(e);await y(ci(e),{events:{...a,...Object.fromEntries(t.map(i=>[i,ce(a,i)||n]))}})}r(Tl,"markWorking");const Jn=r(e=>$.join(e,"data","push-state.json"),"pushStateFile");async function mr(e,t){P(e);const n=Date.parse((await g(Jn(e),{})).last_push);if(Number.isNaN(n))return 0;const a=$.join(e,"data","replies"),i=(await O.readdir(a).catch(()=>[])).filter(o=>Xt.includes($.extname(o).slice(1)));return(await Promise.all(i.map(o=>O.stat($.join(a,o)).then(c=>c.mtimeMs,()=>0)))).filter(o=>o>n&&o<=t.getTime()).length}r(mr,"repliesSincePush");async function hr(e,t){return await y(Jn(P(e)),{last_push:t.toISOString()}),Jn(e)}r(hr,"recordPush");const gr=$.join($.dirname(so(import.meta.url)),"..","bin","kickflip.mjs");function $r(){if(!process.env.KICKFLIP_PUSH_CMD)return[no(),gr,"push"];try{const e=JSON.parse(process.env.KICKFLIP_PUSH_CMD);if(Array.isArray(e)&&e.length&&e.every(t=>typeof t=="string"))return e}catch{}throw new he("KICKFLIP_PUSH_CMD must be a JSON array of strings")}r($r,"pushCommand");async function ql(e,t){const n=jo(t.kind);n&&d(n),t.title===void 0&&d("push needs --title");const a=new Date,i=kr(t.body,await mr(e,a)),s={kind:t.kind,title:t.title,body:i||void 0,url:xr(t.url),tag:t.tag},[o,...c]=$r(),l=[...c,...Object.entries(s).flatMap(([w,u])=>u===void 0?[]:[`--${w}`,u]),...t.urgent?["--urgent"]:[],...t.now?["--now"]:[]],f=await new Promise(w=>{to(o,l,{encoding:"utf8"},(u,h,m)=>w({error:u,said:`${h||""}${m||""}`.split(`
|
|
6
|
+
`).filter(b=>b.trim())}))}),p=f.error?typeof f.error.code=="number"?f.error.code:1:0;if(p!==0)throw Object.assign(new he([...f.said,`kickflip push failed (${typeof f.error.code=="number"?`exit ${p}`:f.error.message}); the push was not recorded`].join(`
|
|
7
|
+
`)),{exitCode:p});return await hr(e,a),{lines:f.said}}r(ql,"sendPush");const br=/(?:^| · )(and \d+ repl(?:y|ies))$/,kr=r((e,t)=>[e,t&&`and ${t} ${t===1?"reply":"replies"}`].filter(Boolean).join(" \xB7 "),"withReplies"),li=160;function yr(e){const t=String(e).split(`
|
|
8
|
+
`).map(n=>n.replace(/\s+/g," ").trim()).find(Boolean)||"";return t.length>li?`${t.slice(0,li-1).trimEnd()}\u2026`:t}r(yr,"pushLine");function xr(e){if(!e||!e.startsWith("/"))return e;const t=new URL(e,"http://board");return/^#(?:selectedCard|item)=[^&]+$/.test(t.hash)&&(t.hash=`${t.hash}&reply`),/\/[\w-]+\.html$/.test(t.pathname)&&!t.pathname.endsWith("/index.html")&&t.searchParams.set("from","push"),t.pathname+t.search+t.hash}r(xr,"pushUrl");const _r=r((e,t)=>`/${$.basename(e)}/#selectedCard=${encodeURIComponent(t)}&reply`,"threadUrl");async function Kn(e,t,n){const a=yr(n);if(!a)return[];const i=po(t),s=await vo(i,o=>[a,br.exec(o)?.[1]].filter(Boolean).join(" \xB7 "),_r(e,t));return s===null?[]:[`queued push ${i} now reads: ${s}`]}r(Kn,"freshenPush");const di=r(e=>$.join(e,"data","hidden.json"),"hiddenFile"),Sr=r(async e=>new Set((await g(di(e),{events:[]})).events||[]),"readHidden");let fi=Promise.resolve();function Ml(e,t,n){_e(t,"the event id");const a=fi.then(async()=>{const i=await Sr(e);n?i.add(t):i.delete(t),await y(di(e),{events:[...i]})});return fi=a.catch(()=>{}),a}r(Ml,"setHidden");const ui=r(e=>$.join(e,"data","read.json"),"readMarksFile"),vr=r(async e=>(await g(ui(e),{})).cards||{},"readMarks");let wi=Promise.resolve();function Wl(e,t,n={}){const a=wi.then(async()=>{const i=await vr(e),s=new Date().toISOString(),o=r((l,f)=>String(f||"")>l?f:l,"later"),c=Object.fromEntries([...new Set(t)].sort().map(l=>[l,o(i[l]||s,n[l])]));JSON.stringify(c)!==JSON.stringify(i)&&await y(ui(e),{cards:c})});return wi=a.catch(()=>{}),a}r(Wl,"markRead");const zn=20*1024*1024,ze=r(e=>$.join(e,"data","uploads"),"uploadsDir"),ct=r((e,t)=>$.join(ze(e),".meta",`${t}.json`),"uploadMeta"),pi=/^\d+-[\w.-]+$/,mi=/^[\w.+-]+\/[\w.+-]+$/,hi=r(()=>Object.assign(new he(`a file can be at most ${zn/1024/1024} MB`),{status:413}),"tooLarge");async function Hl(e,t,{name:n,type:a,length:i}){if(P(e),i>zn)throw hi();const s=String(n||"").trim().slice(0,200)||"file",o=$.basename(s).replace(/[^\w.-]+/g,"-").replace(/^[.-]+/,"").slice(-80)||"file",c=String(a||"").split(";")[0].trim().toLowerCase(),l=mi.test(c)?c:"application/octet-stream";await O.mkdir($.dirname(ct(e,"x")),{recursive:!0});let f,p;for(let h=Date.now();!p;h++)f=`${h}-${o}`,p=await O.open($.join(ze(e),f),"wx").catch(m=>m.code==="EEXIST"?null:Promise.reject(m));const w=$.join(ze(e),f);let u=0;try{for await(const h of t){if(u+=h.length,u>zn)throw hi();await p.write(h)}}catch(h){throw await p.close(),await O.rm(w,{force:!0}),h}return await p.close(),await y(ct(e,f),{name:s,type:l,size:u,at:new Date().toISOString()}),{name:s,url:`uploads/${f}`,path:w,type:l,size:u}}r(Hl,"saveUpload");async function Ul(e,t){if(!pi.test(t||""))return null;const n=$.join(ze(e),t),a=await O.stat(n).catch(()=>null);if(!a?.isFile())return null;const i=await g(ct(e,t),{});return{path:n,name:i.name||t,type:mi.test(i.type||"")?i.type:"application/octet-stream",size:a.size}}r(Ul,"readUpload");const gi=["github","slack"],$i=r(async(e,t)=>String(t).startsWith("ask:")?Bt(e,t.slice(4),"the ask"):x(e,t),"checkThreadKey");async function W(e,t,n,a={},{link:i}={}){P(e),await $i(e,t);const s={ts:new Date().toISOString(),who:"claude",kind:Va,line:n,text:Xa(n,a),...ei(n,a)?{detail:ei(n,a)}:{},...i?{link:i}:{}};return await de(Z(e,t),s),{line:s,lines:qo(s).fault?await Kn(e,t,s.text):[]}}r(W,"appendStatus");async function Ge(e,t,{text:n,link:a,log:i,who:s="claude",via:o,by:c}){P(e),await $i(e,t),/^[^\n]{1,40}$/.test(s.trim())||d("--who takes claude, love or a reviewer's name"),o!==void 0&&!gi.includes(o)&&d(`--via takes ${gi.join(" or ")}`);const l=o===void 0?en(n):v(n,"--text"),f={ts:new Date().toISOString(),who:s.trim(),text:l,link:se(a,w=>Hn(w,"--link")),log:i,via:o,by:c},p=Z(e,t);return await de(p,f),{file:p,lines:f.who==="claude"&&!o?await Kn(e,t,f.text):[]}}r(Ge,"appendMessage");function en(e){const t=v(e,"--text");return t.length>Za&&d(`--text runs to ${t.length} characters, past a message's ${Za}: rewrite it shorter, or put the detail in an HTML page (board.mjs visual add, or --link to a page) and link it`),t}r(en,"messageText");async function Le(e,t,{command:n,text:a=Jt[n],who:i="claude",by:s,card:o}){const c={ts:new Date().toISOString(),who:i,command:n,text:a,...s?{by:s}:{},...o?{card:o}:{}};return await de(Z(e,t),c),{lines:[]}}r(Le,"appendCommand");const Se=r(e=>$.join(e,"data","ask-links.json"),"askLinksFile");async function ve(e){return(await g(Se(e),{})).links||{}}r(ve,"readAskLinks");async function H(e,t){const{tickets:n}=je(await ie(e)),a=await q(e),i=/^ask:(.+)$/.exec(t)?.[1];if(i===void 0){const o=/^([^/\s]+)\/([^/\s]+)$/.exec(t);if(!o)return t;const c=a.find(p=>p.slug===o[1]),l=c&&await g(c.file,null);if(!l)return o[2];const f=(l.items||[]).find(p=>p.id===o[2]);return it(l,f||{id:o[2]},n)}const s=e?ce(await ve(e),i):void 0;if(s)return H(e,s);for(const o of a){const c=await g(o.file,null),l=(Array.isArray(c?.items)?c.items:[]).find(f=>f.from_ask===i);if(l)return it(c,l,n)}return`task ${i}`}r(H,"cardName");async function Jl(e,t){const n=(await K($.join(e,"data","events.jsonl"))).find(a=>a.id===t);return n?.kind==="ask"?H(e,`ask:${t}`):typeof n?.data?.key=="string"?H(e,n.data.key):`the ${n?.kind} event`}r(Jl,"eventCard");async function Qe(e,t){for(const n of await q(e)){const a=await g(n.file,null),i=(Array.isArray(a?.items)?a.items:[]).find(s=>s.from_ask===t);if(i)return{source:n,itemFile:a,item:i,key:`${n.slug}/${i.id}`}}return null}r(Qe,"askCardOf");const Gn=r(e=>!e.phase&&!e.answered_at&&!we(e)&&!xe(e)&&!e.filed_held,"stillNew");function Qn(e,t){const n=t.filter(i=>i.kind==="ask-edited"&&i.data?.id===e.id&&typeof i.data?.text=="string").sort((i,s)=>String(i.ts).localeCompare(String(s.ts))).at(-1);return String(n?n.data.text:e.data?.text||"").trim().split(`
|
|
9
|
+
`)[0].trim()||(e.data?.files||[]).map(i=>i?.name).filter(Boolean).join(", ")||"Ask"}r(Qn,"askTitle");const Kl=r((e,t,n)=>le(e,async()=>{P(e);const a=await K($.join(e,"data","events.jsonl")),i=n||a.find(l=>l.id===t&&l.kind==="ask")||d(`no ask "${t}" in this site's data/events.jsonl`);if(await Qe(e,t)||await vi(e,t,a))return null;const{source:s,itemFile:o}=await bi(e),c=await ki(e,s,i,Qn(i,a));return o.items.push(c),await y(s.file,o),`${s.slug}/${c.id}`}),"fileAsk");async function bi(e){const t=await q(e);for(const n of t){const a=await g(n.file,null);if(a)return Array.isArray(a.items)||d(`${n.file} is not an item file with items[]`),{source:n,itemFile:a}}return(await ie(e)).items==="waves"&&d("this board has no wave yet to file a new task in; start a wave first"),await O.mkdir(t[0].dir,{recursive:!0}),{source:t[0],itemFile:{items:[]}}}r(bi,"askFile");async function ki(e,t,n,a){const i=new Date().toISOString(),s={id:await xs(e),title:a,created:i,lane:"todo",lane_at:i,from_ask:n.id};return s.plan=dn(t.dir,s.id),await fn($.join(t.dir,s.plan),`# ${s.title}
|
|
10
|
+
`),s}r(ki,"newAskCard");const zl=r(e=>le(e,async()=>{P(e);const t=r((...m)=>$.join(e,"data",...m),"data"),n=await K(t("events.jsonl")),a=await ve(e),i=new Map(Object.entries(a));for(const m of await q(e))for(const b of(await g(m.file,{})).items||[])b.from_ask&&i.set(b.from_ask,`${m.slug}/${b.id}`);const s=new Set(n.filter(m=>m.kind==="ask-withdrawn").map(m=>m.data?.id)),o=n.filter(m=>m.kind==="ask"&&!s.has(m.id)&&!i.has(m.id)).sort((m,b)=>String(m.ts).localeCompare(String(b.ts))),c=(await g(t("ask-progress.json"),{})).asks||{},l=(await g(t("ask-titles.json"),{})).titles||{},f=new Map(n.filter(m=>m.kind==="ask-done").map(m=>[m.data?.id,String(m.ts)])),p=[];let w;for(const m of o){const b=await K(Z(e,`ask:${m.id}`));if(b.some(D=>D.line==="ask-moved"))continue;const _=await Promise.all((await Un(t("replies"),m.id)).map(D=>O.stat(t("replies",D.name)).then(M=>M.mtime.toISOString()))),N=b.filter(D=>(D.who||"claude")==="claude"&&D.kind!=="status").map(D=>String(D.ts)),j=[..._,...N].sort().pop()||[ce(c,m.id)?.at,f.get(m.id)].filter(Boolean).sort().pop();w||=await bi(e);const A=await ki(e,w.source,m,ce(l,m.id)||Qn(m,n));A.created=m.ts,j&&Lt(A,"done",j),w.itemFile.items.push(A),await y(w.source.file,w.itemFile),i.set(m.id,`${w.source.slug}/${A.id}`),p.push(i.get(m.id))}const u=r(m=>i.get(/^item:ask:(.+)$/.exec(m||"")?.[1]),"cardOf"),h=await g(L(e),null);if(h?.items?.some(m=>u(m.topic))){for(const m of h.items)u(m.topic)&&(m.topic=`item:${u(m.topic)}`);await y(L(e),h)}for(const m of["ask-progress.json","ask-titles.json"])await O.rm(t(m),{force:!0});return p}),"fileAsks"),Gl=r((e,t)=>le(e,async()=>{const n=await Qe(P(e),t);if(!n||!Gn(n.item))return null;const a=await K($.join(e,"data","events.jsonl"));return n.item.title=Qn(a.find(i=>i.id===t&&i.kind==="ask"),a),await y(n.source.file,n.itemFile),n.key}),"retitleAskCard");async function Vn(e,{source:t,itemFile:n,item:a,key:i}){n.items=n.items.filter(l=>l!==a);const s=Number(/^T-([1-9]\d*)$/.exec(a.id)?.[1]||0);s>(Number(n.last_task)||0)&&(n.last_task=s),await y(t.file,n),a.plan&&await O.rm($.join(t.dir,a.plan),{force:!0});const o=Z(e,i),c=await K(o);return await O.rm(o,{force:!0}),c}r(Vn,"dropCard");const Ql=r((e,t)=>le(e,async()=>{const n=await Qe(P(e),t);return!n||!Gn(n.item)?null:(await Vn(e,n),n.key)}),"dropAskCard"),yi=r(e=>[void 0,"discussing"].includes(e.phase)&&!we(e)&&!xe(e),"answerable");async function xi(e,t,n){const{item:a,key:i}=t;a.from_ask||d(`${i} is not a card an ask became; write in its thread with thread ${i} --text`),yi(a)||d(`${i} is sorted (${a.phase||We[a.lane]}), so it is work: write in its thread with thread ${i} --text`),n!==void 0&&(a.title=v(n,"--title")),a.phase="discussing",delete a.unfiled,delete a.filed_held,await y(t.source.file,t.itemFile)}r(xi,"markAnswered");async function _i(e,t,n){const a=await Qe(e,t);a&&a.key!==n&&d(`ask "${t}" is already card ${a.key}: sort it with item set ${a.key} --phase, or merge it with item relate-ask ${t} <card>`)}r(_i,"refuseFiled");async function Vl(e,t,n,a,{implement:i=!1}={}){P(e);const{source:s,itemFile:o,item:c}=await x(e,t),l={source:s,itemFile:o,item:c,key:t};return en(n),a!==void 0&&v(a,"--title"),i&&(c.implement_ready={at:new Date().toISOString()}),await xi(e,l,a),Ge(e,t,{text:n})}r(Vl,"answerCard");async function Zl(e,t,n,a,i){P(e),_e(t,"the event id"),(await K($.join(e,"data","events.jsonl"))).some(l=>l.id===t&&l.kind==="ask")||d(`no ask "${t}" in this site's data/events.jsonl`),await Zn(e,t),await x(e,n),v(a,"--summary"),i!==void 0&&v(i,"--title");const o=await Qe(e,t);if(o&&o.key===n&&d(`ask "${t}" is ${n} already`),o&&!Gn(o.item)&&d(`ask "${t}" became ${o.key}, which is sorted; to fold that card's notes into ${n}, use item notes ${n} --from ${o.key}, then close it`),o){const l=await le(e,()=>Vn(e,o)),f=Z(e,`ask:${t}`);for(const p of l)await de(f,p);await de(f,{ts:new Date().toISOString(),who:"claude",kind:Va,line:"card-merged",text:Xa("card-merged",{card:await H(e,n)}),into:n,title:i??o.item.title,link:`#selectedCard=${encodeURIComponent(n)}`})}const c=await ve(e);return await y(Se(e),{links:{...c,[t]:n}}),await Ge(e,n,{text:a}),Se(e)}r(Zl,"relateAsk");async function Yl(e,t){await Bt(e,t,"the ask");const n=await ve(e);return ce(n,t)===void 0&&d(`ask "${t}" is not folded into a card`),delete n[t],await y(Se(e),{links:n}),Se(e)}r(Yl,"unfoldAsk");const Si=r(async(e,t)=>(await K(Z(e,`ask:${t}`))).find(n=>n.line==="ask-moved"),"movedLine");async function Zn(e,t){const n=await Si(e,t);n&&d(`ask "${t}" was moved to ${n.link}; it lives there now`)}r(Zn,"refuseMoved");async function vi(e,t,n){const a=`ask "${t}"`;if(n.some(l=>l.kind==="ask-withdrawn"&&l.data?.id===t))return{why:`${a} was deleted by the owner`};const i=await Si(e,t);if(i)return{why:`${a} was already moved to ${i.link}`};const s=ce(await ve(e),t);if(s)return{why:`${a} is folded into ${s}; run ask unfold ${t} first`};const o=[];for(const l of await q(e)){const f=await g(l.file,null);for(const p of Array.isArray(f?.items)?f.items:[])p.from_ask===t&&o.push({key:`${l.slug}/${p.id}`,it:p})}const c=o.find(({it:l})=>xe(l)||we(l)||l.answered_at||l.phase==="discussing");return c?{why:`${a} became ${c.key}, which has started or been answered`}:o.length?{items:o.map(l=>l.key)}:null}r(vi,"askNotNew");async function jr(e,t,n){const a=/^uploads\/([\w.-]+)$/.exec(n?.url||"")?.[1],i=pi.test(a||"")?$.join(ze(e),a):null;if(!i||!await O.stat(i).then(l=>l.isFile(),()=>!1))return{name:String(n?.name||a||"file")};await O.mkdir($.dirname(ct(t,"x")),{recursive:!0});const s=a.replace(/^\d+-/,"");let o;for(let l=Date.now();o=`${l}-${s}`,!await O.copyFile(i,$.join(ze(t),o),Xs.COPYFILE_EXCL).then(()=>!0,p=>p.code==="EEXIST"?!1:Promise.reject(p));l++);const c=await g(ct(e,a),null);return c&&await y(ct(t,o),c),{...n,url:`uploads/${o}`,path:$.join(ze(t),o)}}r(jr,"copyUpload");async function Xl(e,t,n){P(e),_e(t,"the event id"),(typeof n!="string"||!n.trim())&&d("ask move needs --to <route>, the board to move it to");const a=await K($.join(e,"data","events.jsonl")),i=a.find(b=>b.id===t&&b.kind==="ask")||d(`no ask "${t}" in this site's data/events.jsonl`),s=be(),o=s.find(b=>b.route===n)||d(`--to ${n}: no board at /${n} in the hub's manifest; the boards are ${s.map(b=>b.route).join(", ")||"none"}`);Re(o.dir,e)&&d(`--to ${n}: that is this board`);const c=await vi(e,t,a);c?.why&&d(c.why);const l=a.filter(b=>b.kind==="ask-edited"&&b.data?.id===t&&typeof b.data?.text=="string").sort((b,_)=>String(b.ts).localeCompare(String(_.ts))),f=l.at(-1),p=[];for(const b of[...i.data?.files||[],...l.flatMap(_=>_.data.files||[])])p.push(await jr(e,o.dir,b));const w={route:$.basename(e),title:(await ie(e)).title,id:t},u=f?f.data.text:i.data?.text||"",h=await $e(o,"ask",{text:u,...p.length?{files:p}:{},from:w}),m=`/${n}/#selectedCard=${encodeURIComponent(`ask:${h}`)}`;await W(e,`ask:${t}`,"ask-moved",{board:o.title},{link:m});for(const b of c?.items||[]){const{source:_,itemFile:N,item:j}=await x(e,b);await le(e,()=>Vn(e,{source:_,itemFile:N,item:j,key:b}))}return{file:$.join(o.dir,"data","events.jsonl"),lines:[`moved to ${m}`,...(c?.items||[]).map(b=>`dropped: ${b}`)]}}r(Xl,"moveAsk");async function ed(e,t,{state:n,text:a}={}){return P(e),["posted","needs"].includes(n)||d("review record needs --state posted or --state needs"),String(a??"").trim()||d("review record needs the review text, as --text or --file"),Oi(e,await ji(e,t,"review record"),{state:n,text:a})}r(ed,"recordReview");async function ji(e,t,n){/^[1-9]\d{0,6}$/.test(String(t))||d(`${n} takes the PR number, such as 437, not "${t}"`);const a=await g($.join(e,"data","incoming.json"),{});return{...(a?.prs||[]).find(o=>o.number===Number(t))||(await g($.join(e,"data","reviews.json"),{}))[t]||d(`PR #${t} is not an incoming PR on this board`),repo:a?.repo||pe(e,{})}}r(ji,"incomingPR");async function Oi(e,{number:t,title:n,author:a,url:i,touches:s=[],asked:o=!1,head:c},{state:l,text:f,review_url:p}){const w=$.join(e,"data","reviews.json"),u=await g(w,{});return u[t]={number:Number(t),title:n,author:a,url:i,touches:s,asked:o,head:c,state:l,text:f,...p?{review_url:p}:{},at:new Date().toISOString()},await y(w,u),{file:w,lines:[]}}r(Oi,"saveReview");async function td(e,t,{words:n,readAt:a}){P(e);const i=await ji(e,t,"review post"),s=String(n??"").trim()||d("review post needs the review text, as --file"),o=`${At}Claude review from the ${(await ie(e)).title} board`;(s.startsWith(At.trim())||s.includes("Claude review from")||s.includes(qn))&&d(`review post adds the ${At.trim()} header and the claude-bridge marker itself; pass only your words`);const c=i.repo||d("review post needs a board on GitHub; this board has no repo there");Number.isNaN(Date.parse(a))&&d("--read-at takes the time you read the PR, such as 2026-11-05T09:30:00Z");const l=je(await ie(e)).github_login;try{const f=await Uo({repo:c,pr:i.number,since:a});f.length&&d(`PR #${t} got ${f.length===1?"a review":`${f.length} reviews`} after ${a} (${f.map(w=>`${w.login} at ${w.at}`).join(", ")}); reread the PR and review again`);const p=await Ho({repo:c,pr:i.number,head:i.head,body:`${o}
|
|
11
11
|
|
|
12
12
|
${s}
|
|
13
13
|
|
|
14
|
-
${
|
|
15
|
-
`),a})}r(
|
|
16
|
-
`);return[`${l.ts===a.ts?"\u2192 ":""}${
|
|
17
|
-
`)}]:n.items.map(m=>({pr:m.pr,text:`${l} ${m.text}`})))});const u=c(await g(o,{})),p=u.ts||await an(o,c,()=>s({channel:u.channel,text:i(u.replies.length)}));for(const[f,h]of u.replies.entries())nn(h)||await an(o,m=>c(m).replies[f],()=>s({channel:u.channel,thread_ts:p,text:h.text}));const w=c(await g(o,{})).replies.filter(f=>f.pr&&f.ts);w.length&&await Le(tn(e),f=>{const h=`digest-${t}`;for(const m of w){const k=f[m.pr]||={links:{},replies:[]},_=k.replies.find(N=>N.id===h)||k.replies[k.replies.push({id:h,text:m.text,to:{}})-1];_.to[n.name]||={channel:u.channel,ts:m.ts,reactions:[]}}})}r(rd,"postDigest");function cd(e,t){const n=Zn(Array.isArray(t)?t:[t],"the reviewer file");return Yn(e,a=>[...a.map(i=>n.find(s=>lt(s.name,i.name))||i),...n.filter(i=>!a.some(s=>lt(i.name,s.name)))])}r(cd,"addReviewers");function ld(e,t){return Yn(e,n=>{const a=t.filter(i=>!n.some(s=>lt(s.name,i)));return a.length&&d(`this board has no reviewer ${a.map(i=>`"${i}"`).join(", ")}`),n.filter(i=>!t.some(s=>lt(i.name,s)))})}r(ld,"removeReviewers");const Or=r(()=>$.join(Fn.homedir(),".claude","kickflip","settings.json"),"hubSettingsFile"),ta=r(e=>{try{return e()}catch(t){throw t instanceof Vo&&d(t.message),t}},"ruleCall"),na=r(async e=>ai(await g($.join(e,"board.json"),{})),"readBoardRules");let Fi=Promise.resolve();function Ft(e,t){const n=Fi.then(async()=>{const a=$.join(P(e),"board.json"),i=await g(a,{}),{set:s,...o}=t(i);return await b(a,{...i,...s}),{file:a,...o}});return Fi=n.catch(()=>{}),n}r(Ft,"writeBoardJSON");const Ni=r((e,t)=>Ft(e,n=>{const a=ta(()=>t(ai(n)));return{set:{rules:a.rules},...a}}),"writeBoardRules");function dd(e,t){const n=Object.keys(t||{}).sort().join(),a=n==="level,login",i=a?t.login:t?.[n],s=typeof i=="string"?i.trim():null;(!["add","remove","level,login"].includes(n)||!ar(s))&&d("Send add or remove with one login, like name@github, or a listed login with its level."),a&&!["view","full"].includes(t.level)&&d("A level is view or full.");const o=r(({login:c,level:l})=>l==="full"?{login:c,level:l}:c,"stored");return Ft(e,c=>{const l=nr(c);a&&!l.some(w=>w.login===s)&&d(`${s} isn't shared with yet. Add them first.`);const p=(n==="remove"?l.filter(w=>w.login!==s):a?l.map(w=>w.login===s?{login:s,level:t.level}:w):l.some(w=>w.login===s)?l:[...l,{login:s,level:"view"}]).map(o);return{set:{share:p},share:p}}).then(({share:c})=>({share:c}))}r(dd,"saveBoardShare");const Ri=r(e=>typeof e=="string"&&/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(e),"validGithubLogin"),Ei=r(e=>{try{return e?Ha(e):null}catch{return null}},"storedTickets"),on=r(e=>Number.isInteger(e)&&e>=1&&e<=99,"validCap"),aa=["max_implementers","max_planners","max_serves"],je=r((e,t)=>{const n=Ei(e?.tickets);return{auto_merge:e?.auto_merge===!0,auto_build:e?.auto_build===!0,auto_pull:e?.auto_pull===!0,max_implementers:on(e?.max_implementers)?e.max_implementers:eo,max_planners:on(e?.max_planners)?e.max_planners:to,max_serves:on(e?.max_serves)?e.max_serves:no,auto_triage:e?.auto_triage===!0,auto_review:e?.auto_review===!0,idle_notification:e?.idle_notification===!0,skip_draft:e?.skip_draft===!0,prs:t?.hub?e?.prs===!0:e?.prs!==!1,...n?{tickets:n}:{},...Ri(e?.github_login)?{github_login:e.github_login}:{},...ni(e?.build_target)?{build_target:e.build_target}:{}}},"settingsOf");async function ud(e,t,n=new Date){let a;try{a=$o(t,n)}catch(i){d(i.message)}return await Ft(e,()=>({set:{paused_until:a??void 0}})),a}r(ud,"setBoardPause");async function Ar(e){const t=P(e);return je(await g($.join(t,"board.json"),{}),ke().find(n=>Fe(n.dir,t)))}r(Ar,"readBoardSettings");const fd=r((e,t)=>!!e?.github&&(!e.local||e.hub===!0)&&t.prs,"opensPRs");async function ut(e,t){rt(t,["auto_merge","auto_build","auto_pull","max_implementers","max_planners","max_serves","auto_triage","auto_review","idle_notification","skip_draft","prs","tickets","github_login","build_target"],"the settings"),Object.keys(t).length||d("the settings name nothing to change"),t={...t};for(const[n,a]of Object.entries(t))if(n==="tickets"){try{t.tickets=Ha(a)??void 0}catch(s){d(s.message)}t.tickets?.source==="linear"&&!Ao()&&d(jo);const i=t.tickets?.source==="github"&&e?ke().find(s=>Fe(s.dir,e)):null;i&&!i.github&&d("GitHub issues need the board\u2019s repo on GitHub, and this board\u2019s repo isn\u2019t; pick none or linear")}else n==="github_login"?a===""||a===null?t.github_login=void 0:Ri(a)||d("github_login must be a GitHub login"):n==="build_target"?a===null?t.build_target=void 0:ni(a)||d('build_target must name .kickflip/build-<target>: letters, digits, ".", "_" or "-", or null for .kickflip/build'):aa.includes(n)?on(a)||d(`${n} must be a whole number from 1 to 99`):Zt(a,n);if(t.prs===!1){const n=e?ke().find(a=>Fe(a.dir,e)):null;n?.github&&!n.local&&!(await zo(n.owner?.repo)).pushMain&&d("Merge lands each card on main and pushes it, so turn on Push main after a land first")}return Ft(e,n=>{const a=e?ke().find(o=>Fe(o.dir,e)):null,i=je(n,a),s=je({...n,...t},a);return{set:t,settings:s,changed:Object.keys(t).filter(o=>JSON.stringify(i[o])!==JSON.stringify(s[o]))}})}r(ut,"saveBoardSettings");async function wd(){if(Oo().set)return[];const e=[];for(const t of ke())(await Ar(t.dir)).tickets?.source==="linear"&&(await ut(t.dir,{tickets:{source:"none"}}),await pe(t,"settings",{tickets:null}),e.push(`/${t.route} fell back to no ticket source: Hub has no Linear key`));return e}r(wd,"fallBackFromLinear");async function pd(e,t,n){const a=String(t).replace(/-/g,"_"),i=["auto_merge","auto_build","auto_pull","max_implementers","max_planners","max_serves","auto_triage","auto_review","idle_notification","skip_draft","prs","github_login","tickets","build_target"];if(i.includes(a)||d(`no board setting "${t}"; settings are ${i.map(c=>c.replace(/_/g,"-")).join(", ")}`),a==="tickets"){let c;try{c=JSON.parse(n)}catch{d('settings set tickets takes the source as JSON, such as {"source":"github"}')}const{file:l,settings:u}=await ut(e,{tickets:c});return{file:l,lines:Pr({tickets:u.tickets})}}if(a==="github_login"){const{file:c,settings:l}=await ut(e,{github_login:n});return{file:c,lines:[`github-login ${l.github_login||"unset"}`]}}if(a==="build_target"){const{file:c,settings:l}=await ut(e,{build_target:n==="default"?null:n});return{file:c,lines:[`build-target ${l.build_target||"default"}`]}}if(aa.includes(a)){const{file:c,settings:l}=await ut(e,{[a]:/^\d+$/.test(n)?Number(n):n});return{file:c,lines:[`${t} ${l[a]}`]}}["on","off"].includes(n)||d(`settings set ${t} takes on or off, not "${n}"`);const{file:s,settings:o}=await ut(e,{[a]:n==="on"});return{file:s,lines:[`${t} ${o[a]?"on":"off"}`]}}r(pd,"setBoardSetting");const Pr=r(e=>Object.entries(e).map(([t,n])=>`setting ${t.replace(/_/g,"-")} ${t==="tickets"?JSON.stringify(n??{source:"none"}):t==="github_login"||aa.includes(t)?n:t==="build_target"?n||"default":n?"on":"off"}`),"settingsLines");async function ia(e,t,n){const a=t?ta(()=>n(Qt())):await Ni(e,n);return t&&ta(()=>tr(a.rules)),{file:t?Or():a.file,lines:[`id ${a.rule.id}`]}}r(ia,"changeRules");async function Li(e,t,n){const a=r(o=>o.map(c=>c.id),"ids");n||P(e);const[i,s]=n?[a(Qt()),e?a(await na(e)):[]]:[a(await na(e)),a(Qt())];i.includes(t)||d(`no ${n?"hub":"board"} rule "${t}"${s.includes(t)?`; it is a ${n?"board":"hub"} rule, so ${n?"drop":"pass"} --hub`:""}`)}r(Li,"checkRuleId");async function md(e){const t=r(n=>a=>`${n} ${a.id} ${a.by==="claude"?"claude":"owner"} ${a.text}`,"line");return{lines:[...Qt().map(t("hub")),...e?(await na(e)).map(t("board")):[]]}}r(md,"listRules");function hd(e,{text:t,hub:n=!1,by:a}){return n||P(e),ia(e,n,i=>Zo(i,{text:t,by:a}))}r(hd,"addRuleTo");async function gd(e,t,{text:n,hub:a=!1}){return await Li(e,t,a),ia(e,a,i=>Yo(i,t,n))}r(gd,"editRuleIn");async function $d(e,t,{hub:n=!1}){return await Li(e,t,n),ia(e,n,a=>Xo(a,t))}r($d,"removeRuleFrom");const kd=r((e,t,n)=>Ni(e,a=>er(a,t,n)),"saveBoardRules");async function yd(e,{id:t,title:n,summary:a}){P(e);const i=await g(B(e),{topics:[],items:[]}),s=i.topics||[];return s.some(o=>o.id===S(t,"the topic id"))&&d(`topic "${t}" already exists`),s.push({id:t.trim(),title:S(n,"--title"),...a===void 0?{}:{summary:S(a,"--summary")}}),await b(B(e),{...i,topics:s,items:i.items||[]}),B(e)}r(yd,"addTopic");const Bi=["quiet","info","notice","danger","success"];function Dr(e,t){if(e.diagram===void 0!=(e.practice===void 0)&&d(`${t} needs diagram and practice together`),e.diagram===void 0)return;const n=e.diagram;(!Array.isArray(n)||!n.length)&&d(`${t}.diagram must be a non-empty array of rows`),n.forEach((a,i)=>{(!Array.isArray(a)||!a.length||a.length>4)&&d(`${t}.diagram[${i}] must be a row of 1 to 4 boxes`),a.forEach((s,o)=>{const c=`${t}.diagram[${i}][${o}]`;rt(s,["text","tone","to"],c),S(s.text,`${c}.text`),ie(s.tone,l=>Bi.includes(l)?l:d(`${c}.tone takes ${Bi.join(", ")}`)),ie(s.to,l=>Array.isArray(l)&&i+1<n.length&&l.every(u=>Number.isInteger(u)&&u>=0&&u<n[i+1].length)?l:d(`${c}.to must list boxes of the next row by index`))})}),(!Array.isArray(e.practice)||!e.practice.length)&&d(`${t}.practice must be a non-empty array of lines`),e.practice.forEach((a,i)=>S(a,`${t}.practice[${i}]`))}r(Dr,"checkDiagram");function Fr(e,t){(!Array.isArray(e)||!e.length)&&d(`${t}.options must be a non-empty array`);const n=new Set;e.forEach((a,i)=>{const s=`${t}.options[${i}]`;rt(a,["id","label","recommended","detail","diagram","practice"],s),Dr(a,s),n.has(S(a.id,`${s}.id`))&&d(`${t} has two options with id "${a.id}"`),n.add(a.id),S(a.label,`${s}.label`).startsWith(`${a.id}. `)&&d(`${s}.label starts with "${a.id}. ", but the page adds the option letter itself`),ie(a.recommended,o=>Zt(o,`${s}.recommended`)),ie(a.detail,o=>typeof o=="string"?o:d(`${s}.detail must be text`))}),e.filter(a=>a.recommended).length>1&&d(`${t} recommends more than one option`)}r(Fr,"checkOptions");function Nr(e,t,n){rt(e,["option","card","answer","reason"],n),t.some(a=>a.id===S(e.option,`${n}.option`))||d(`${n}.option names no option "${e.option}"`),S(e.card,`${n}.card`),S(e.answer,`${n}.answer`),ie(e.reason,a=>S(a,`${n}.reason`))}r(Nr,"checkSuggested");const ft=r(e=>$.join(e,"data","prototypes.json"),"prototypesFile"),sa=r((e,t,n)=>$.join(e,"public","prototypes",t,`${n}.js`),"takeFile");async function Nt(e){const t=e?(await g(ft(e),{})).prototypes:null;return Array.isArray(t)?t:[]}r(Nt,"readPrototypes");const Rr=r(e=>typeof e=="string"&&/^[a-z0-9-]+$/.test(e)?e:d(`a prototype topic uses only a-z, 0-9 and "-", not "${e}"`),"prototypeTopic");function Er(e){const t=/^([^:]*):(.*)$/s.exec(e);(!t||!/^[A-Z]$/.test(t[1]))&&d(`--take takes KEY:"Label" with KEY one capital letter, not "${e}"`);const n=S(t[2],`--take ${t[1]}'s label`);return/^[A-Z]\.\s/.test(n)&&d(`--take ${t[1]}'s label starts with "${n.slice(0,3)}"; the take pill shows the label alone, so drop the letter`),{key:t[1],label:n}}r(Er,"prototypeTake");function Lr(e){const t=/^([^:]*):(.*)$/s.exec(e);return(!t||!/^[a-z0-9][a-z0-9-]*$/.test(t[1]))&&d(`--state takes KEY:"Label" with KEY in a-z, 0-9 and "-", not "${e}"`),{key:t[1],label:S(t[2],`--state ${t[1]}'s label`)}}r(Lr,"prototypeState");const Ci=r(e=>e.find((t,n)=>e.findIndex(a=>a.key===t.key)!==n),"twiceIn"),Be=r(e=>/^prototype-(.+)\.html$/.exec($.posix.basename(String(e||"").split(/[?#]/)[0]))?.[1],"pageTopic");async function bd(e,{topic:t,title:n,card:a,takes:i=[],served:s,states:o=[]}){P(e),Rr(t),S(n,"--title"),i.length||d('prototype add needs a --take KEY:"Label" for each take');const c=i.map(Er),l=Ci(c);l&&d(`--take ${l.key} is given twice`),s!==void 0&&!/^\/\S*$/.test(s)&&d(`--served takes the page's path on the served site, starting with "/", not "${s}"`),o.length&&s===void 0&&d("--state goes with --served: only a served round re-renders its states in place");const u=o.map(Lr),p=Ci(u);p&&d(`--state ${p.key} is given twice`),a!==void 0&&await x(e,a);const w=await Nt(e);w.some(m=>m.topic===t)&&d(`prototype ${t} is already recorded; run prototype rm ${t} first, or pick another topic`),await O.stat($.join(e,"public",`prototype-${t}.html`)).then(()=>!0,()=>!1)&&d(`public/prototype-${t}.html is a hand-written page that questions may link; pick another topic`);const f={topic:t,title:n.trim(),...a===void 0?{}:{card:a},...s===void 0?{}:{served:s},takes:c,...u.length?{states:u}:{},at:new Date().toISOString()};await b(ft(e),{prototypes:[...w,f]});const h=`page prototype-${t}.html`;return s!==void 0?{file:ft(e),lines:[h,`served ${s} from the base prototype worktree (board.mjs prototype base)`,...u.length?[`states ${u.map(m=>m.key).join(", ")}`]:[]]}:{file:ft(e),lines:[h,...c.map(m=>`take ${m.key} ${sa(e,t,m.key)}`)]}}r(bd,"addPrototype");async function xd(e){P(e);const t=await Nt(e);return{lines:t.length?t.map(n=>[n.topic,n.title,n.takes.map(a=>`${a.key}: ${a.label}`).join(", "),n.card,n.served&&`served ${n.served}`,n.states&&`states ${n.states.map(a=>a.key).join(", ")}`].filter(Boolean).join(" \xB7 ")):["no prototypes recorded"]}}r(xd,"listPrototypes");async function _d(e){const t=(await Nt(e)).filter(a=>a.served).map(a=>a.topic);if(!t.length)return[];const n=await g(B(e),{items:[]});return t.filter(a=>(n.items||[]).some(i=>!i.resolved&&Be(i.url)===a))}r(_d,"servedRoundsOpen");async function Sd(e,t){P(e);const n=await Nt(e);n.some(s=>s.topic===t)||d(`no prototype "${t}" is recorded`);const i=((await g(B(e),{items:[]})).items||[]).filter(s=>!s.resolved&&Be(s.url)===t).map(s=>s.id);return i.length&&d(`prototype ${t} backs open question ${i.join(", ")}; resolve ${i.length===1?"it":"them"} first`),await b(ft(e),{prototypes:n.filter(s=>s.topic!==t)}),ft(e)}r(Sd,"removePrototype");async function Br(e,t,n){const a=t.url?.split(/[?#]/)[0];if(!a||!$.posix.basename(a).startsWith("prototype-"))return;const i=$.posix.basename(a),s=t.url.split("#")[0].split("?").slice(1).join("?"),o=`${n}.url`;new URLSearchParams(s).get("q")!==t.id&&d(`${o} must be ${i}?q=${t.id} so the pill can pick`);const c=Be(a),l=(await Nt(e)).find(f=>f.topic===c);l||d(`${o} names ${i}, which is not a recorded prototype; record it with board.mjs prototype add ${c} --title "\u2026" --take A:"\u2026", then write its take files`);const u=l.takes.map(f=>f.key),p=t.options.map(f=>f.id),w=p.map(f=>u.indexOf(f));if(w.some((f,h)=>f<0||h>0&&f<w[h-1])&&d(`${n}.options ids ${p.join(", ")} must be a non-empty subset of ${i}'s takes, in take order: ${u.join(", ")}`),!l.served)for(const f of u)await O.stat(sa(e,c,f)).then(h=>h.isFile(),()=>!1)||d(`${o} take ${f} has no file yet; write ${sa(e,c,f)}`)}r(Br,"checkPrototype");async function vd(e,t,n=async()=>{}){P(e);const a=Array.isArray(t)?t:[t];a.length||d("the question file is empty");const i=await g(B(e),{topics:[],items:[]}),s=i.items||[],o=await g(de(e),{}),c=new Set([...s,...Object.values(o).flat()].map(w=>w.id)),l=new Set((i.topics||[]).map(w=>w.id));for(const[w,f]of a.entries()){const h=`question ${f?.id??w+1}`;if(rt(f,["id","topic","source","question","context","url","urgent","options","round_of","reask_of","ask","suggested","prototype_gate"],h),c.has(_e(f.id,`${h}.id`))&&d(`question id "${f.id}" is already used`),c.add(f.id),S(f.topic,`${h}.topic`),f.topic.startsWith("item:ask:")){const m=await Qe(e,await Et(e,f.topic.slice(9),`${h}.topic`));d(`${h}.topic: an ask's questions go on its card's topic${m?`, item:${m.key}`:""}`)}else f.topic.startsWith("item:")?await x(e,f.topic.slice(5)):f.topic.startsWith("wave:")?await $t(e,f.topic.slice(5)):f.topic.startsWith("file:")?await Ac(e,f.topic.slice(5)):l.has(f.topic)||d(`topic "${f.topic}" is not declared; add it with: topic add ${f.topic} --title <t>`);S(f.question,`${h}.question`),f.question.length>Qa&&d(`${h}.question runs to ${f.question.length} characters, past ${Qa}; keep the question to one line and move the detail into context or the option details, which take markdown`);for(const m of["source","context"])ie(f[m],k=>S(k,`${h}.${m}`));ie(f.url,m=>Mn(m,`${h}.url`)),ie(f.urgent,m=>Zt(m,`${h}.urgent`)),Fr(f.options,h);for(const m of Object.keys(Rt))f[m]!==void 0&&Ir(f[m],f,`${h}.${m}`)&&await Tr(e,f,m,h);ie(f.suggested,m=>Nr(m,f.options,`${h}.suggested`)),await Br(e,f,h),f.ask!==void 0&&await Hc(e,f,h),ie(f.round_of,m=>{const k=s.find(_=>_.id===m);k||d(`${h}.round_of names no question "${m}" in owner.json`),k.topic!==f.topic&&d(`${h}.round_of names ${m} on topic ${k.topic}; a round stays on its question's topic, ${f.topic}`),Be(f.url)||d(`${h}.round_of needs a url on a recorded prototype, prototype-<topic>.html?q=${f.id}`)}),ie(f.reask_of,m=>{const k=s.find(_=>_.id===m);k||d(`${h}.reask_of names no question "${m}" in owner.json`),k.topic!==f.topic&&d(`${h}.reask_of names ${m} on topic ${k.topic}; a re-ask stays on its question's topic, ${f.topic}`),k.resolved||d(`${h}.reask_of names ${m}, which is not resolved; only a resolved question is asked again`)})}for(const w of new Set(a.map(f=>Be(f.url)).filter(Boolean)))await n(e,w);const u=[],p=[];for(const w of a){const f=w.topic.startsWith("item:")&&!w.topic.startsWith("item:ask:")?w.topic.slice(5):null,h=f&&await Ii(e,f);h&&((o[f]||=[]).push(w),p.push(w),u.push(`staged: ${w.id} waits for ${f}'s ${h} to finish; the card's staged questions then publish together`))}return p.length&&await b(de(e),o),p.length===a.length?{file:de(e),lines:u,staged:p.length}:qi(e,i,s,a.filter(w=>!p.includes(w)),u)}r(vd,"addQuestions");async function Ii(e,t){const{source:n}=await x(e,t);for(const a of await V(n.dir,F(t).id)){const i=await g(a,null);if(i?.started&&!i.done&&He(i)&&i.kind!==$e)return i.kind}return null}r(Ii,"runningPlanner");async function wt(e,t,{screen:n=!1}={}){if(!e||await Ii(e,t))return;const a=await g(de(e),{});if(!a[t])return;const i=await g(ot(e),{});if(n){i[t]||await b(ot(e),{...i,[t]:new Date().toISOString()});return}const{[t]:s,...o}=a,c=await g(B(e),{topics:[],items:[]});await qi(e,c,c.items||[],s,[]),await b(de(e),o),i[t]&&await b(ot(e),rn(i,t))}r(wt,"publishStaged");const rn=r((e,t)=>Object.fromEntries(Object.entries(e).filter(([n])=>n!==t)),"withoutKey");async function Ti(e){const t=await g(ot(e),{}),n=await g(de(e),{});return Object.fromEntries(Object.entries(t).filter(([a])=>n[a]).map(([a,i])=>[a,{since:i,ids:n[a].map(s=>s.id)}]))}r(Ti,"readScreening");async function jd(e,t,n){P(e);const a=S(n,"--note"),i=await g(de(e),{}),s=await g(ot(e),{}),o=Object.keys(i).find(w=>i[w].some(f=>f.id===t));if(!o)return Cr(e,t,a);s[o]||d(`${t} waits for ${o}'s planner to finish`);const c=i[o].find(w=>w.id===t);c.prototype_gate&&d(`${t} is the card's prototype question, which is the owner's; question publish ${o} passes it on`);const l=new Date().toISOString(),u=await g(B(e),{topics:[],items:[]});await b(B(e),{...u,topics:u.topics||[],items:[...u.items||[],{...c,asked_at:l,resolved:`Settled by the orchestrator: ${a}`,resolved_at:l}]});const p=i[o].filter(w=>w.id!==t);return await b(de(e),p.length?{...i,[o]:p}:rn(i,o)),p.length||await b(ot(e),rn(s,o)),await Ce(e,o,`Answers (orchestrator, ${Ze()})`,`- ${t} (${c.question}): settled by the orchestrator. "${a}"`),B(e)}r(jd,"settleQuestion");async function Cr(e,t,n){const a=await g(B(e),{topics:[],items:[]}),i=(a.items||[]).find(c=>c.id===t&&c.topic?.startsWith("item:"));i||d(`no staged question "${t}"; question settle takes a question a finished grill left for you to screen, or a card's question while it waits on your re-check`);const s=i.topic.slice(5);(await x(e,s)).item.recheck_at||d(`${s} waits on no re-check, so its question ${t} stays answered as it is`);const o=new Date().toISOString();return await b(B(e),{...a,items:a.items.map(c=>c===i?{...c,resolved:`Settled by the orchestrator: ${n}`,resolved_at:o}:c)}),await Ce(e,s,`Answers (orchestrator, ${Ze()})`,`- ${t} (${i.question}): settled by the orchestrator. "${n}"`),B(e)}r(Cr,"settleStale");async function Od(e,t){P(e);const n=(await Ti(e))[t]?.ids||d(`${t} has no questions waiting on you to screen`);return await wt(e,t),n}r(Od,"publishScreened");async function Ad(e,t=new Date){const n=Object.entries(await Ti(e)).filter(([,a])=>t-Date.parse(a.since)>=sr).map(([a])=>a);for(const a of n)await wt(e,a),await W(e,a,"screen-timeout");return n}r(Ad,"publishUnscreened");async function qi(e,t,n,a,i){const{answers:s,requests:o,reopens:c}=await mt(e),l=new Date().toISOString(),u=[],p=[];for(const f of a.filter(h=>h.round_of))f.round=Ln(n.find(h=>h.id===f.round_of))+1,Ye(t,[f.round_of],`Followed by prototype round ${f.round}`,l),p.push(f.round_of);for(const f of a.filter(h=>h.reask_of))Ye(t,[f.reask_of],`Asked again as ${f.id}`,l),p.push(f.reask_of);for(const f of a){const h=Be(f.url),m=n.filter(_=>_.topic===f.topic).map(_=>Ue(_,s,o,[],c)),k=f.topic.startsWith("item:")?Ki(m):[];for(const _ of m)k.includes(_.id)?(Ye(t,[_.id],`Followed by ${f.id}`,l),p.push(_.id)):!_.resolved&&h&&Be(_.url)===h&&(Ye(t,[_.id],`Replaced by ${f.id}`,l),u.push(_.id))}const w={...t,topics:t.topics||[],items:[...n,...a.map(f=>({...f,asked_at:l}))]};await b(B(e),w);for(const f of new Set(a.map(h=>ra(h.topic)).filter(Boolean)))await Vr(e,f);return{file:B(e),lines:[...i,...u.map(f=>`replaced: ${f}`),...p.map(f=>`resolved: ${f}`)]}}r(qi,"publishQuestions");function Ir(e,t,n){return Zt(e,n),e?((!t.topic.startsWith("item:")||t.topic.startsWith("item:ask:"))&&d(`${n} goes on a card's topic, item:<file>/<id>`),t.options?.map(a=>a.id).join()!=="yes,no"&&d(`${n} takes exactly the options yes and no, in that order`),!0):!1}r(Ir,"checkGate");const Rt={prototype_gate:"prototype"};async function Tr(e,t,n,a){const i=t.topic.slice(5),{item:s}=await x(e,i),o=Rt[n];s.gates?.[o]&&d(`${a}.${n}: ${i} already has its ${o} answer, ${s.gates[o]}`)}r(Tr,"checkGateFact");async function qr(e,t,n,a,i){const{source:s,itemFile:o,item:c}=await x(e,t);c.gates={...c.gates,[Rt[n]]:a},await b(s.file,o),await Mr(e,t,n,i)}r(qr,"keepGate");async function Mr(e,t,n,a){const i=await g(de(e),{}),s=(i[t]||[]).filter(u=>!u[n]);s.length!==(i[t]||[]).length&&await b(de(e),s.length?{...i,[t]:s}:rn(i,t));const{owner:o,asked:c}=await ue(e,t),l=c.filter(u=>u[n]&&u.state==="open").map(u=>u.id);return l.length&&await pt(e,o,l,a),l.length>0}r(Mr,"resolveGate");async function Pd(e,t){const n=new Map(((await g(B(e),{items:[]})).items||[]).map(o=>[o.id,o]));for(const o of t){const c=n.get(String(o.id)),l=Object.keys(Rt).find(f=>c?.[f]);if(!l||!["yes","no"].includes(o.choice))continue;const u=`Prototype round: ${o.choice}. The board moves the card on once every question is answered.`;await pt(e,await g(B(e),{topics:[],items:[]}),[c.id],u);const p=c.topic.slice(5),w=await x(e,p).catch(()=>null);w&&(await qr(e,p,l,o.choice,u),l==="prototype_gate"&&o.choice==="yes"&&w.item.phase==="grilling"&&await fn(e,p)&&await W(e,p,"prototype-after"))}const a=new Map;for(const o of t){const c=n.get(String(o.id)),l=ra(c?.topic);l&&a.set(l,[...a.get(l)||[],Ur(c,o)])}const i=[],s=[];for(const[o,c]of a)await x(e,o).catch(()=>null)&&await Wr(e,o,"writing them into its notes",async()=>{await Ce(e,o,`Answers (owner, ${Ze()})`,c.join(`
|
|
18
|
-
`));const l=await
|
|
14
|
+
${qn}`,login:l});return await Oi(e,i,{state:"posted",text:s,review_url:p}),{lines:[`posted review on PR #${t}: ${p}`]}}catch(f){if(f instanceof he)throw f;d(f.message)}}r(td,"postIncomingReview");const tn=r(e=>$.join(e,"data","handshake.json"),"handshakeFile");async function Ai(e){const t=await g(tn(e),null);if(!t?.hello)return t;const{hello:n,...a}=t;return{...a,ping:a.ping||n}}r(Ai,"readHandshake");async function nd(e,t){P(e),_e(t,"the event id");const n=(await K($.join(e,"data","events.jsonl"))).find(i=>i.id===t);n?.kind!=="ping"&&n?.kind!=="hello"&&d(`no ping event "${t}" in this site's data/events.jsonl`);const a=await Ai(e)||{};return a.ping?.id&&a.ping.id!==t&&d(`"${t}" is an older ping; ack the newest, "${a.ping.id}"`),await y(tn(e),{...a,ping:a.ping||{id:t,at:n.ts,session:null},ack:{id:t,at:new Date().toISOString()}}),tn(e)}r(nd,"ackPing");async function ad(e){const t=await Ai(e);t&&!t.done_at&&await y(tn(e),{...t,done_at:new Date().toISOString()})}r(ad,"finishHandshake");const Yn={name:/^[^\n]{1,40}$/,slack_user:/^U[A-Z0-9]+$/,dm:/^D[A-Z0-9]+$/,github:/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/},lt=r((e,t)=>e.trim().toLowerCase()===t.trim().toLowerCase(),"sameName");function Xn(e,t){return Array.isArray(e)||d(`${t} must be a JSON array of reviewers`),e.forEach((n,a)=>{const i=`${t}[${a}]`;rt(n,Object.keys(Yn),i);for(const[s,o]of Object.entries(Yn))(typeof n[s]!="string"||!o.test(n[s].trim()))&&d(`${i}.${s} must match ${o}`);e.slice(0,a).some(s=>lt(s.name,n.name))&&d(`${t} names "${n.name}" twice`)}),e.map(n=>Object.fromEntries(Object.keys(Yn).map(a=>[a,n[a].trim()])))}r(Xn,"checkReviewers");async function ea(e,t){const{file:n,reviewers:a}=await Ft(e,s=>{const o=Xn(t(s.reviewers||[]),"the reviewers");return{set:{reviewers:o},reviewers:o}}),i=[];for(const s of await q(e)){const o=await g(s.file,null);(o?.items||[]).some(c=>!z(c))&&(await y(s.file,{...o,reviewers:a}),i.push(`item file ${s.slug}: reviewers copied`))}return{file:n,lines:i}}r(ea,"writeReviewers");const id=r((e,t)=>ea(e,()=>Xn(t,"the reviewer file")),"setReviewers"),nn=r(e=>$.join(P(e),"data","pr-slack.json"),"prSlackFile"),ta=r(e=>g(nn(e),{}),"readPRSlack"),an=r(e=>!!(e?.ts||e?.posting_at),"held");async function Ie(e,t){return st(e,async()=>{const n=JSON.parse(await O.readFile(e,"utf8").catch(()=>"{}")),a=t(n);return await Je(e,JSON.stringify(n,null,2)+`
|
|
15
|
+
`),a})}r(Ie,"editJSON");async function sn(e,t,n){await Ie(e,i=>{const s=t(i);an(s)&&d("that Slack message has already gone out"),s.posting_at=new Date().toISOString()});let a;try{a=await n()}catch(i){throw await Ie(e,s=>{delete t(s).posting_at}),i}try{await Ie(e,i=>{const s=t(i);s.ts=a,delete s.posting_at})}catch(i){d(`posted, but its record failed, so it won't go again: ${i.message}`)}return a}r(sn,"postOnce");const sd=r((e,t,n,a)=>sn(nn(e),i=>{const s=i[t.pr]||={links:{},replies:[]};return Object.assign(s.links[t.reviewer]||={reactions:[]},{channel:n.channel,thread_ts:n.ts})},a),"postLink"),od=r((e,t)=>Ie(nn(e),n=>{const a=n[t.pr],i=t.reply?a.replies.find(s=>s.id===t.reply).to[t.reviewer]:a.links[t.reviewer];i.reactions=[...new Set([...i.reactions||[],t.emoji])]}),"recordReaction");function Or(e,t,n){const[a,i]=t.split("#");return`<@${e}> <${Wn({repo:a,number:i})}|${a.split("/").pop()}#${i}> ${n}`}r(Or,"aboutPR");function Ar(e,t){const n=Number.isInteger(e.additions)?` (+${e.additions}/-${e.deletions}, ${e.files} file${e.files===1?"":"s"})`:"";return`ready for review: ${e.title||t.title}${n}`}r(Ar,"linkText");const Pi=r(e=>!!e?.approved||e?.reviewDecision==="APPROVED","approved");async function rd(e,t){P(e);const n=await ii.waiting("slack");if(n)return{waiting:n,links:[],reactions:[],digest:null};const a=await ie(e),i=await g(Ja,{}),s=await ta(e),o=[];for(const f of await q(e)){const p=await g(f.file,{}),w=zt(p,a.reviewers);for(const u of p.items||[])if(u.lane!=="done")for(const h of Pt(u,pe(e,p))){const m=ai(h),b=ce(i.prs,m);if(!(b?.state!=="OPEN"||b.isDraft))for(const _ of w)an(s[m]?.links?.[_.name])||o.some(N=>N.pr===m&&N.reviewer===_.name)||o.push({pr:m,key:`${f.slug}/${u.id}`,reviewer:_.name,slack_user:_.slack_user,text:Or(_.slack_user,m,Ar(b,u))})}}const c=[];for(const[f,p]of Object.entries(s)){const w=ce(i.prs,f),u=[Pi(w)&&"git_approved",w?.state==="MERGED"?"git_merged":w?.state==="CLOSED"&&"x"].filter(Boolean),h=r((m,b,_)=>{if(b.ts)for(const N of u)b.reactions?.includes(N)||c.push({pr:f,reviewer:m,..._?{reply:_}:{},channel:b.channel||p.links[m].channel,ts:b.ts,emoji:N})},"react");for(const[m,b]of Object.entries(p.links))h(m,b);for(const m of p.replies)for(const[b,_]of Object.entries(m.to))h(b,_,m.id)}const l=await Pr(e,t);return{date:Qt(t?.now).date,links:o,reactions:c,digest:l.due?l:null}}r(rd,"reviewDue");const Ve=r(e=>$.join(P(e),"data","review-digests.json"),"digestFile");async function Pr(e,{now:t=new Date}={}){P(e);const n=await ie(e),a=await g(Ja,{}),i=await ta(e),s=zo(),o=new Map;for(const u of await q(e)){const h=await g(u.file,{}),m=zt(h,n.reviewers);for(const b of m)o.has(b.name)||o.set(b.name,{name:b.name,slack_user:b.slack_user,dm:b.dm,items:[]});for(const b of h.items||[]){if(b.lane==="done")continue;const _=`${u.slug}/${b.id}`,N=Ko(h,b,_,s);for(const j of Pt(b,pe(e,h))){const A=ai(j),D=ce(a.prs,A);if(!(D?.state!=="OPEN"||D.isDraft||D.failing?.length||Pi(D)))for(const M of m){const I=i[A]?.links?.[M.name]?.ts||ce(N,M.name)?.ts||null,S=I?(t.getTime()-Number(I)*1e3)/864e5:null,B=S>2?Math.floor(S):null;o.get(M.name).items.push({key:_,pr:A,title:b.title,url:Wn(j),sent_ts:I,...B?{days_old:B}:{},text:`${Wn(j)} \xB7 ${b.title}${B?` (${B} days old)`:""}`})}}}}const{date:c,open:l}=Qt(t),f=((await g(Ve(e),{})).digests||[]).filter(u=>u.date===c),p=await ii.waiting("slack");for(const u of f)o.has(u.reviewer)||o.set(u.reviewer,{name:u.reviewer,items:[]});const w=[...o.values()].map(u=>{const h=f.find(b=>b.reviewer===u.name),m=h?.replies?!h.posting_at&&(!h.ts||h.replies.some(b=>!an(b))):!h?.posting_at&&u.items.length>0;return{...u,owed:m,...h?{sent:h}:{}}}).filter(u=>u.items.length||u.sent);return{date:c,due:!p&&l&&w.some(u=>u.owed),...p?{waiting:p}:{},reviewers:w}}r(Pr,"reviewDigest");const Dt=r((e,t)=>n=>(n.digests||[]).find(a=>a.date===e&&a.reviewer===t),"dayEntry"),Dr=7,Di=r((e,t)=>(Date.parse(t)-Date.parse(e.date))/864e5<Dr,"heardOn");function Fi(e,t,n,a){Dt(t,n)(e)||(e.digests=[...(e.digests||[]).filter(i=>Di(i,t)),{date:t,reviewer:n,...a}],delete e.last_date)}r(Fi,"addDayEntry");async function cd(e,t=new Date){const{date:n}=Qt(t);return((await g(Ve(e),{})).digests||[]).filter(a=>a.ts&&Di(a,n))}r(cd,"heardThreads");const ld=r((e,t,n)=>Ie(Ve(e),a=>{Dt(t.date,t.reviewer)(a).heard=n}),"markHeard");async function Fr(e){const t=new Set;for(const n of Object.values(await ta(e))){for(const a of Object.values(n.links||{}))t.add(a.ts);for(const a of n.replies||[])for(const i of Object.values(a.to))t.add(i.ts)}for(const n of(await g(Ve(e),{})).digests||[])for(const a of[n,...n.replies||[]])t.add(a.ts);return t.delete(void 0),t}r(Fr,"boardPosts");const Ni=250;function Ri(e){const t=v(e,"--text");return t.length>Ni&&d(`--text runs to ${t.length} characters, past the orchestrator's ${Ni}: keep it brief`),t}r(Ri,"orchestratorWords");async function na(e,t){return _e(t,"the event id"),(await K($.join(P(e),"data","events.jsonl"))).find(a=>a.id===t&&a.kind==="slack-question")?.data||d(`no slack-question "${t}" in this site's data/events.jsonl`)}r(na,"slackQuestion");const on=r(e=>e||d(Qo),"throughSlack"),dt=r(e=>e.catch(t=>t instanceof Go?d(t.message):Promise.reject(t)),"slackCall");async function dd(e,t,n=Gt()){const a=await na(e,t),i=await Fr(e),s=Yo();return{lines:(await dt(on(n).replies({channel:a.channel,ts:a.thread_ts}))).flatMap(l=>{const f=i.has(l.ts)||l.user===s&&Vo(l.text)?"board":l.user===s?"you":a.reviewer,[p,...w]=String(l.text||"").split(`
|
|
16
|
+
`);return[`${l.ts===a.ts?"\u2192 ":""}${f}: ${p}`,...w.map(u=>` ${u}`)]})}}r(dd,"slackThread");async function fd(e,t,n,a=Gt()){const i=await na(e,t);i.answer||d("that message is the owner's and names no @kickflip, so it expects no reply");const s=Ri(n);return await dt(on(a).post({channel:i.channel,thread_ts:i.thread_ts,text:s})),{lines:[`answered in ${i.reviewer}'s day thread`]}}r(fd,"slackAnswer");async function ud(e,t,n,{now:a=new Date}={},i=Gt()){P(e);const s=Ri(n),o=await ie(e),c=new Map;for(const h of await q(e))for(const m of zt(await g(h.file,{}),o.reviewers))c.set(m.name,m);const l=[...c.values()].find(h=>lt(h.name,t))||d(`no reviewer "${t}" on this board; its reviewers are ${[...c.keys()].join(", ")||"none"}`),f=on(i),{date:p}=Qt(a),w=Dt(p,l.name)(await g(Ve(e),{})),u=await dt(Nr(e,p,l.name,{channel:w?.channel||await dt(f.openDM(l.slack_user)),header:`${o.title} reviews`,post:r(h=>f.post(h),"post")}));return await dt(f.post({channel:u.channel,thread_ts:u.ts,text:s})),{lines:[`said in ${l.name}'s day thread`]}}r(ud,"slackSay");async function wd(e,t,n,{title:a,notes:i,notesFrom:s},o=Gt()){const c=await na(e,t),f=(await dt(on(o).replies({channel:c.channel,ts:c.thread_ts}))).find(h=>h.ts===c.ts)||d("that Slack message is gone from its thread"),p=await Ac(e,n,{title:a,notes:i,notesFrom:s,phase:"discussing",intake:!0}),w=Zo().slack_domain,u=w?`https://${w}.slack.com/archives/${c.channel}/p${c.ts.replace(".","")}?thread_ts=${c.thread_ts}&cid=${c.channel}`:void 0;return await Ge(e,p.key,{text:f.text,who:c.from==="owner"?"love":c.reviewer,via:"slack",link:u}),{...p,lines:[`filed ${p.key} discussing, for the owner's review`]}}r(wd,"slackTask");async function Nr(e,t,n,{channel:a,header:i,post:s}){const o=Ve(e),c=Dt(t,n),l=await Ie(o,f=>(Fi(f,t,n,{channel:a}),{...c(f)}));return{channel:l.channel,ts:l.ts||await sn(o,c,()=>s({channel:l.channel,text:i}))}}r(Nr,"openDayThread");async function pd(e,t,n,{channel:a,header:i,post:s}){const o=Ve(e),c=Dt(t,n.name),l=`<@${n.slack_user}>`;await Ie(o,u=>{Fi(u,t,n.name,{channel:a});const h=c(u);h.replies||(h.replies=h.ts?[{prs:n.items.map(m=>m.pr),text:[`${l} ${i(n.items.length)}`,...n.items.map(m=>m.text)].join(`
|
|
17
|
+
`)}]:n.items.map(m=>({pr:m.pr,text:`${l} ${m.text}`})))});const f=c(await g(o,{})),p=f.ts||await sn(o,c,()=>s({channel:f.channel,text:i(f.replies.length)}));for(const[u,h]of f.replies.entries())an(h)||await sn(o,m=>c(m).replies[u],()=>s({channel:f.channel,thread_ts:p,text:h.text}));const w=c(await g(o,{})).replies.filter(u=>u.pr&&u.ts);w.length&&await Ie(nn(e),u=>{const h=`digest-${t}`;for(const m of w){const b=u[m.pr]||={links:{},replies:[]},_=b.replies.find(N=>N.id===h)||b.replies[b.replies.push({id:h,text:m.text,to:{}})-1];_.to[n.name]||={channel:f.channel,ts:m.ts,reactions:[]}}})}r(pd,"postDigest");function md(e,t){const n=Xn(Array.isArray(t)?t:[t],"the reviewer file");return ea(e,a=>[...a.map(i=>n.find(s=>lt(s.name,i.name))||i),...n.filter(i=>!a.some(s=>lt(i.name,s.name)))])}r(md,"addReviewers");function hd(e,t){return ea(e,n=>{const a=t.filter(i=>!n.some(s=>lt(s.name,i)));return a.length&&d(`this board has no reviewer ${a.map(i=>`"${i}"`).join(", ")}`),n.filter(i=>!t.some(s=>lt(i.name,s)))})}r(hd,"removeReviewers");const Rr=r(()=>$.join(Rn.homedir(),".claude","kickflip","settings.json"),"hubSettingsFile"),aa=r(e=>{try{return e()}catch(t){throw t instanceof nr&&d(t.message),t}},"ruleCall"),ia=r(async e=>oi(await g($.join(e,"board.json"),{})),"readBoardRules");let Ei=Promise.resolve();function Ft(e,t){const n=Ei.then(async()=>{const a=$.join(P(e),"board.json"),i=await g(a,{}),{set:s,...o}=t(i);return await y(a,{...i,...s}),{file:a,...o}});return Ei=n.catch(()=>{}),n}r(Ft,"writeBoardJSON");const Bi=r((e,t)=>Ft(e,n=>{const a=aa(()=>t(oi(n)));return{set:{rules:a.rules},...a}}),"writeBoardRules");function gd(e,t){const n=Object.keys(t||{}).sort().join(),a=n==="level,login",i=a?t.login:t?.[n],s=typeof i=="string"?i.trim():null;(!["add","remove","level,login"].includes(n)||!lr(s))&&d("Send add or remove with one login, like name@github, or a listed login with its level."),a&&!["view","full"].includes(t.level)&&d("A level is view or full.");const o=r(({login:c,level:l})=>l==="full"?{login:c,level:l}:c,"stored");return Ft(e,c=>{const l=cr(c);a&&!l.some(w=>w.login===s)&&d(`${s} isn't shared with yet. Add them first.`);const p=(n==="remove"?l.filter(w=>w.login!==s):a?l.map(w=>w.login===s?{login:s,level:t.level}:w):l.some(w=>w.login===s)?l:[...l,{login:s,level:"view"}]).map(o);return{set:{share:p},share:p}}).then(({share:c})=>({share:c}))}r(gd,"saveBoardShare");const Li=r(e=>typeof e=="string"&&/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(e),"validGithubLogin"),Ii=r(e=>{try{return e?za(e):null}catch{return null}},"storedTickets"),rn=r(e=>Number.isInteger(e)&&e>=1&&e<=99,"validCap"),sa=["max_implementers","max_planners","max_serves"],je=r((e,t)=>{const n=Ii(e?.tickets);return{auto_merge:e?.auto_merge===!0,auto_build:e?.auto_build===!0,auto_pull:e?.auto_pull===!0,max_implementers:rn(e?.max_implementers)?e.max_implementers:oo,max_planners:rn(e?.max_planners)?e.max_planners:ro,max_serves:rn(e?.max_serves)?e.max_serves:co,auto_triage:e?.auto_triage===!0,auto_review:e?.auto_review===!0,idle_notification:e?.idle_notification===!0,skip_draft:e?.skip_draft===!0,prs:t?.hub?e?.prs===!0:e?.prs!==!1,...n?{tickets:n}:{},...Li(e?.github_login)?{github_login:e.github_login}:{},...si(e?.build_target)?{build_target:e.build_target}:{}}},"settingsOf");async function $d(e,t,n=new Date){let a;try{a=So(t,n)}catch(i){d(i.message)}return await Ft(e,()=>({set:{paused_until:a??void 0}})),a}r($d,"setBoardPause");async function Er(e){const t=P(e);return je(await g($.join(t,"board.json"),{}),be().find(n=>Re(n.dir,t)))}r(Er,"readBoardSettings");const bd=r((e,t)=>!!e?.github&&(!e.local||e.hub===!0)&&t.prs,"opensPRs");async function ft(e,t){rt(t,["auto_merge","auto_build","auto_pull","max_implementers","max_planners","max_serves","auto_triage","auto_review","idle_notification","skip_draft","prs","tickets","github_login","build_target"],"the settings"),Object.keys(t).length||d("the settings name nothing to change"),t={...t};for(const[n,a]of Object.entries(t))if(n==="tickets"){try{t.tickets=za(a)??void 0}catch(s){d(s.message)}t.tickets?.source==="linear"&&!Eo()&&d(No);const i=t.tickets?.source==="github"&&e?be().find(s=>Re(s.dir,e)):null;i&&!i.github&&d("GitHub issues need the board\u2019s repo on GitHub, and this board\u2019s repo isn\u2019t; pick none or linear")}else n==="github_login"?a===""||a===null?t.github_login=void 0:Li(a)||d("github_login must be a GitHub login"):n==="build_target"?a===null?t.build_target=void 0:si(a)||d('build_target must name .kickflip/build-<target>: letters, digits, ".", "_" or "-", or null for .kickflip/build'):sa.includes(n)?rn(a)||d(`${n} must be a whole number from 1 to 99`):Yt(a,n);if(t.prs===!1){const n=e?be().find(a=>Re(a.dir,e)):null;n?.github&&!n.local&&!(await Xo(n.owner?.repo)).pushMain&&d("Merge lands each card on main and pushes it, so turn on Push main after a land first")}return Ft(e,n=>{const a=e?be().find(o=>Re(o.dir,e)):null,i=je(n,a),s=je({...n,...t},a);return{set:t,settings:s,changed:Object.keys(t).filter(o=>JSON.stringify(i[o])!==JSON.stringify(s[o]))}})}r(ft,"saveBoardSettings");async function kd(){if(Ro().set)return[];const e=[];for(const t of be())(await Er(t.dir)).tickets?.source==="linear"&&(await ft(t.dir,{tickets:{source:"none"}}),await $e(t,"settings",{tickets:null}),e.push(`/${t.route} fell back to no ticket source: Hub has no Linear key`));return e}r(kd,"fallBackFromLinear");async function yd(e,t,n){const a=String(t).replace(/-/g,"_"),i=["auto_merge","auto_build","auto_pull","max_implementers","max_planners","max_serves","auto_triage","auto_review","idle_notification","skip_draft","prs","github_login","tickets","build_target"];if(i.includes(a)||d(`no board setting "${t}"; settings are ${i.map(c=>c.replace(/_/g,"-")).join(", ")}`),a==="tickets"){let c;try{c=JSON.parse(n)}catch{d('settings set tickets takes the source as JSON, such as {"source":"github"}')}const{file:l,settings:f}=await ft(e,{tickets:c});return{file:l,lines:Br({tickets:f.tickets})}}if(a==="github_login"){const{file:c,settings:l}=await ft(e,{github_login:n});return{file:c,lines:[`github-login ${l.github_login||"unset"}`]}}if(a==="build_target"){const{file:c,settings:l}=await ft(e,{build_target:n==="default"?null:n});return{file:c,lines:[`build-target ${l.build_target||"default"}`]}}if(sa.includes(a)){const{file:c,settings:l}=await ft(e,{[a]:/^\d+$/.test(n)?Number(n):n});return{file:c,lines:[`${t} ${l[a]}`]}}["on","off"].includes(n)||d(`settings set ${t} takes on or off, not "${n}"`);const{file:s,settings:o}=await ft(e,{[a]:n==="on"});return{file:s,lines:[`${t} ${o[a]?"on":"off"}`]}}r(yd,"setBoardSetting");const Br=r(e=>Object.entries(e).map(([t,n])=>`setting ${t.replace(/_/g,"-")} ${t==="tickets"?JSON.stringify(n??{source:"none"}):t==="github_login"||sa.includes(t)?n:t==="build_target"?n||"default":n?"on":"off"}`),"settingsLines");async function oa(e,t,n){const a=t?aa(()=>n(Vt())):await Bi(e,n);return t&&aa(()=>rr(a.rules)),{file:t?Rr():a.file,lines:[`id ${a.rule.id}`]}}r(oa,"changeRules");async function Ci(e,t,n){const a=r(o=>o.map(c=>c.id),"ids");n||P(e);const[i,s]=n?[a(Vt()),e?a(await ia(e)):[]]:[a(await ia(e)),a(Vt())];i.includes(t)||d(`no ${n?"hub":"board"} rule "${t}"${s.includes(t)?`; it is a ${n?"board":"hub"} rule, so ${n?"drop":"pass"} --hub`:""}`)}r(Ci,"checkRuleId");async function xd(e){const t=r(n=>a=>`${n} ${a.id} ${a.by==="claude"?"claude":"owner"} ${a.text}`,"line");return{lines:[...Vt().map(t("hub")),...e?(await ia(e)).map(t("board")):[]]}}r(xd,"listRules");function _d(e,{text:t,hub:n=!1,by:a}){return n||P(e),oa(e,n,i=>ar(i,{text:t,by:a}))}r(_d,"addRuleTo");async function Sd(e,t,{text:n,hub:a=!1}){return await Ci(e,t,a),oa(e,a,i=>ir(i,t,n))}r(Sd,"editRuleIn");async function vd(e,t,{hub:n=!1}){return await Ci(e,t,n),oa(e,n,a=>sr(a,t))}r(vd,"removeRuleFrom");const jd=r((e,t,n)=>Bi(e,a=>or(a,t,n)),"saveBoardRules");async function Od(e,{id:t,title:n,summary:a}){P(e);const i=await g(L(e),{topics:[],items:[]}),s=i.topics||[];return s.some(o=>o.id===v(t,"the topic id"))&&d(`topic "${t}" already exists`),s.push({id:t.trim(),title:v(n,"--title"),...a===void 0?{}:{summary:v(a,"--summary")}}),await y(L(e),{...i,topics:s,items:i.items||[]}),L(e)}r(Od,"addTopic");const Ti=["quiet","info","notice","danger","success"];function Lr(e,t){if(e.diagram===void 0!=(e.practice===void 0)&&d(`${t} needs diagram and practice together`),e.diagram===void 0)return;const n=e.diagram;(!Array.isArray(n)||!n.length)&&d(`${t}.diagram must be a non-empty array of rows`),n.forEach((a,i)=>{(!Array.isArray(a)||!a.length||a.length>4)&&d(`${t}.diagram[${i}] must be a row of 1 to 4 boxes`),a.forEach((s,o)=>{const c=`${t}.diagram[${i}][${o}]`;rt(s,["text","tone","to"],c),v(s.text,`${c}.text`),se(s.tone,l=>Ti.includes(l)?l:d(`${c}.tone takes ${Ti.join(", ")}`)),se(s.to,l=>Array.isArray(l)&&i+1<n.length&&l.every(f=>Number.isInteger(f)&&f>=0&&f<n[i+1].length)?l:d(`${c}.to must list boxes of the next row by index`))})}),(!Array.isArray(e.practice)||!e.practice.length)&&d(`${t}.practice must be a non-empty array of lines`),e.practice.forEach((a,i)=>v(a,`${t}.practice[${i}]`))}r(Lr,"checkDiagram");function Ir(e,t){(!Array.isArray(e)||!e.length)&&d(`${t}.options must be a non-empty array`);const n=new Set;e.forEach((a,i)=>{const s=`${t}.options[${i}]`;rt(a,["id","label","recommended","detail","diagram","practice"],s),Lr(a,s),n.has(v(a.id,`${s}.id`))&&d(`${t} has two options with id "${a.id}"`),n.add(a.id),v(a.label,`${s}.label`).startsWith(`${a.id}. `)&&d(`${s}.label starts with "${a.id}. ", but the page adds the option letter itself`),se(a.recommended,o=>Yt(o,`${s}.recommended`)),se(a.detail,o=>typeof o=="string"?o:d(`${s}.detail must be text`))}),e.filter(a=>a.recommended).length>1&&d(`${t} recommends more than one option`)}r(Ir,"checkOptions");function Cr(e,t,n){rt(e,["option","card","answer","reason"],n),t.some(a=>a.id===v(e.option,`${n}.option`))||d(`${n}.option names no option "${e.option}"`),v(e.card,`${n}.card`),v(e.answer,`${n}.answer`),se(e.reason,a=>v(a,`${n}.reason`))}r(Cr,"checkSuggested");const ut=r(e=>$.join(e,"data","prototypes.json"),"prototypesFile"),ra=r((e,t,n)=>$.join(e,"public","prototypes",t,`${n}.js`),"takeFile");async function Nt(e){const t=e?(await g(ut(e),{})).prototypes:null;return Array.isArray(t)?t:[]}r(Nt,"readPrototypes");const Tr=r(e=>typeof e=="string"&&/^[a-z0-9-]+$/.test(e)?e:d(`a prototype topic uses only a-z, 0-9 and "-", not "${e}"`),"prototypeTopic");function qr(e){const t=/^([^:]*):(.*)$/s.exec(e);(!t||!/^[A-Z]$/.test(t[1]))&&d(`--take takes KEY:"Label" with KEY one capital letter, not "${e}"`);const n=v(t[2],`--take ${t[1]}'s label`);return/^[A-Z]\.\s/.test(n)&&d(`--take ${t[1]}'s label starts with "${n.slice(0,3)}"; the take pill shows the label alone, so drop the letter`),{key:t[1],label:n}}r(qr,"prototypeTake");function Mr(e){const t=/^([^:]*):(.*)$/s.exec(e);return(!t||!/^[a-z0-9][a-z0-9-]*$/.test(t[1]))&&d(`--state takes KEY:"Label" with KEY in a-z, 0-9 and "-", not "${e}"`),{key:t[1],label:v(t[2],`--state ${t[1]}'s label`)}}r(Mr,"prototypeState");const qi=r(e=>e.find((t,n)=>e.findIndex(a=>a.key===t.key)!==n),"twiceIn"),Ce=r(e=>/^prototype-(.+)\.html$/.exec($.posix.basename(String(e||"").split(/[?#]/)[0]))?.[1],"pageTopic");async function Ad(e,{topic:t,title:n,card:a,takes:i=[],served:s,states:o=[]}){P(e),Tr(t),v(n,"--title"),i.length||d('prototype add needs a --take KEY:"Label" for each take');const c=i.map(qr),l=qi(c);l&&d(`--take ${l.key} is given twice`),s!==void 0&&!/^\/\S*$/.test(s)&&d(`--served takes the page's path on the served site, starting with "/", not "${s}"`),o.length&&s===void 0&&d("--state goes with --served: only a served round re-renders its states in place");const f=o.map(Mr),p=qi(f);p&&d(`--state ${p.key} is given twice`),a!==void 0&&await x(e,a);const w=await Nt(e);w.some(m=>m.topic===t)&&d(`prototype ${t} is already recorded; run prototype rm ${t} first, or pick another topic`),await O.stat($.join(e,"public",`prototype-${t}.html`)).then(()=>!0,()=>!1)&&d(`public/prototype-${t}.html is a hand-written page that questions may link; pick another topic`);const u={topic:t,title:n.trim(),...a===void 0?{}:{card:a},...s===void 0?{}:{served:s},takes:c,...f.length?{states:f}:{},at:new Date().toISOString()};await y(ut(e),{prototypes:[...w,u]});const h=`page prototype-${t}.html`;return s!==void 0?{file:ut(e),lines:[h,`served ${s} from the base prototype worktree (board.mjs prototype base)`,...f.length?[`states ${f.map(m=>m.key).join(", ")}`]:[]]}:{file:ut(e),lines:[h,...c.map(m=>`take ${m.key} ${ra(e,t,m.key)}`)]}}r(Ad,"addPrototype");async function Pd(e){P(e);const t=await Nt(e);return{lines:t.length?t.map(n=>[n.topic,n.title,n.takes.map(a=>`${a.key}: ${a.label}`).join(", "),n.card,n.served&&`served ${n.served}`,n.states&&`states ${n.states.map(a=>a.key).join(", ")}`].filter(Boolean).join(" \xB7 ")):["no prototypes recorded"]}}r(Pd,"listPrototypes");async function Dd(e){const t=(await Nt(e)).filter(a=>a.served).map(a=>a.topic);if(!t.length)return[];const n=await g(L(e),{items:[]});return t.filter(a=>(n.items||[]).some(i=>!i.resolved&&Ce(i.url)===a))}r(Dd,"servedRoundsOpen");async function Fd(e,t){P(e);const n=await Nt(e);n.some(s=>s.topic===t)||d(`no prototype "${t}" is recorded`);const i=((await g(L(e),{items:[]})).items||[]).filter(s=>!s.resolved&&Ce(s.url)===t).map(s=>s.id);return i.length&&d(`prototype ${t} backs open question ${i.join(", ")}; resolve ${i.length===1?"it":"them"} first`),await y(ut(e),{prototypes:n.filter(s=>s.topic!==t)}),ut(e)}r(Fd,"removePrototype");async function Wr(e,t,n){const a=t.url?.split(/[?#]/)[0];if(!a||!$.posix.basename(a).startsWith("prototype-"))return;const i=$.posix.basename(a),s=t.url.split("#")[0].split("?").slice(1).join("?"),o=`${n}.url`;new URLSearchParams(s).get("q")!==t.id&&d(`${o} must be ${i}?q=${t.id} so the pill can pick`);const c=Ce(a),l=(await Nt(e)).find(u=>u.topic===c);l||d(`${o} names ${i}, which is not a recorded prototype; record it with board.mjs prototype add ${c} --title "\u2026" --take A:"\u2026", then write its take files`);const f=l.takes.map(u=>u.key),p=t.options.map(u=>u.id),w=p.map(u=>f.indexOf(u));if(w.some((u,h)=>u<0||h>0&&u<w[h-1])&&d(`${n}.options ids ${p.join(", ")} must be a non-empty subset of ${i}'s takes, in take order: ${f.join(", ")}`),!l.served)for(const u of f)await O.stat(ra(e,c,u)).then(h=>h.isFile(),()=>!1)||d(`${o} take ${u} has no file yet; write ${ra(e,c,u)}`)}r(Wr,"checkPrototype");async function Nd(e,t,n=async()=>{}){P(e);const a=Array.isArray(t)?t:[t];a.length||d("the question file is empty");const i=await g(L(e),{topics:[],items:[]}),s=i.items||[],o=await g(fe(e),{}),c=new Set([...s,...Object.values(o).flat()].map(w=>w.id)),l=new Set((i.topics||[]).map(w=>w.id));for(const[w,u]of a.entries()){const h=`question ${u?.id??w+1}`;if(rt(u,["id","topic","source","question","context","url","urgent","options","round_of","reask_of","ask","suggested","prototype_gate"],h),c.has(_e(u.id,`${h}.id`))&&d(`question id "${u.id}" is already used`),c.add(u.id),v(u.topic,`${h}.topic`),u.topic.startsWith("item:ask:")){const m=await Qe(e,await Bt(e,u.topic.slice(9),`${h}.topic`));d(`${h}.topic: an ask's questions go on its card's topic${m?`, item:${m.key}`:""}`)}else u.topic.startsWith("item:")?await x(e,u.topic.slice(5)):u.topic.startsWith("wave:")?await $t(e,u.topic.slice(5)):u.topic.startsWith("file:")?await Bc(e,u.topic.slice(5)):l.has(u.topic)||d(`topic "${u.topic}" is not declared; add it with: topic add ${u.topic} --title <t>`);v(u.question,`${h}.question`),u.question.length>Ya&&d(`${h}.question runs to ${u.question.length} characters, past ${Ya}; keep the question to one line and move the detail into context or the option details, which take markdown`);for(const m of["source","context"])se(u[m],b=>v(b,`${h}.${m}`));se(u.url,m=>Hn(m,`${h}.url`)),se(u.urgent,m=>Yt(m,`${h}.urgent`)),Ir(u.options,h);for(const m of Object.keys(Rt))u[m]!==void 0&&Ur(u[m],u,`${h}.${m}`)&&await Jr(e,u,m,h);se(u.suggested,m=>Cr(m,u.options,`${h}.suggested`)),await Wr(e,u,h),u.ask!==void 0&&await Zc(e,u,h),se(u.round_of,m=>{const b=s.find(_=>_.id===m);b||d(`${h}.round_of names no question "${m}" in owner.json`),b.topic!==u.topic&&d(`${h}.round_of names ${m} on topic ${b.topic}; a round stays on its question's topic, ${u.topic}`),Ce(u.url)||d(`${h}.round_of needs a url on a recorded prototype, prototype-<topic>.html?q=${u.id}`)}),se(u.reask_of,m=>{const b=s.find(_=>_.id===m);b||d(`${h}.reask_of names no question "${m}" in owner.json`),b.topic!==u.topic&&d(`${h}.reask_of names ${m} on topic ${b.topic}; a re-ask stays on its question's topic, ${u.topic}`),b.resolved||d(`${h}.reask_of names ${m}, which is not resolved; only a resolved question is asked again`)})}for(const w of new Set(a.map(u=>Ce(u.url)).filter(Boolean)))await n(e,w);const f=[],p=[];for(const w of a){const u=w.topic.startsWith("item:")&&!w.topic.startsWith("item:ask:")?w.topic.slice(5):null,h=u&&await Mi(e,u);h&&((o[u]||=[]).push(w),p.push(w),f.push(`staged: ${w.id} waits for ${u}'s ${h} to finish; the card's staged questions then publish together`))}return p.length&&await y(fe(e),o),p.length===a.length?{file:fe(e),lines:f,staged:p.length}:Hi(e,i,s,a.filter(w=>!p.includes(w)),f)}r(Nd,"addQuestions");async function Mi(e,t){const{source:n}=await x(e,t);for(const a of await V(n.dir,F(t).id)){const i=await g(a,null);if(i?.started&&!i.done&&Ne(i)&&i.kind!==X)return i.kind}return null}r(Mi,"runningPlanner");async function wt(e,t,{screen:n=!1}={}){if(!e||await Mi(e,t))return;const a=await g(fe(e),{});if(!a[t])return;const i=await g(ot(e),{});if(n){i[t]||await y(ot(e),{...i,[t]:new Date().toISOString()});return}const{[t]:s,...o}=a,c=await g(L(e),{topics:[],items:[]});await Hi(e,c,c.items||[],s,[]),await y(fe(e),o),i[t]&&await y(ot(e),cn(i,t))}r(wt,"publishStaged");const cn=r((e,t)=>Object.fromEntries(Object.entries(e).filter(([n])=>n!==t)),"withoutKey");async function Wi(e){const t=await g(ot(e),{}),n=await g(fe(e),{});return Object.fromEntries(Object.entries(t).filter(([a])=>n[a]).map(([a,i])=>[a,{since:i,ids:n[a].map(s=>s.id)}]))}r(Wi,"readScreening");async function Rd(e,t,n){P(e);const a=v(n,"--note"),i=await g(fe(e),{}),s=await g(ot(e),{}),o=Object.keys(i).find(w=>i[w].some(u=>u.id===t));if(!o)return Hr(e,t,a);s[o]||d(`${t} waits for ${o}'s planner to finish`);const c=i[o].find(w=>w.id===t);c.prototype_gate&&d(`${t} is the card's prototype question, which is the owner's; question publish ${o} passes it on`);const l=new Date().toISOString(),f=await g(L(e),{topics:[],items:[]});await y(L(e),{...f,topics:f.topics||[],items:[...f.items||[],{...c,asked_at:l,resolved:`Settled by the orchestrator: ${a}`,resolved_at:l}]});const p=i[o].filter(w=>w.id!==t);return await y(fe(e),p.length?{...i,[o]:p}:cn(i,o)),p.length||await y(ot(e),cn(s,o)),await Oe(e,o,`Answers (orchestrator, ${Ze()})`,`- ${t} (${c.question}): settled by the orchestrator. "${a}"`),L(e)}r(Rd,"settleQuestion");async function Hr(e,t,n){const a=await g(L(e),{topics:[],items:[]}),i=(a.items||[]).find(c=>c.id===t&&c.topic?.startsWith("item:"));i||d(`no staged question "${t}"; question settle takes a question a finished grill left for you to screen, or a card's question while it waits on your re-check`);const s=i.topic.slice(5);(await x(e,s)).item.recheck_at||d(`${s} waits on no re-check, so its question ${t} stays answered as it is`);const o=new Date().toISOString();return await y(L(e),{...a,items:a.items.map(c=>c===i?{...c,resolved:`Settled by the orchestrator: ${n}`,resolved_at:o}:c)}),await Oe(e,s,`Answers (orchestrator, ${Ze()})`,`- ${t} (${i.question}): settled by the orchestrator. "${n}"`),L(e)}r(Hr,"settleStale");async function Ed(e,t){P(e);const n=(await Wi(e))[t]?.ids||d(`${t} has no questions waiting on you to screen`);return await wt(e,t),n}r(Ed,"publishScreened");async function Bd(e,t=new Date){const n=Object.entries(await Wi(e)).filter(([,a])=>t-Date.parse(a.since)>=fr).map(([a])=>a);for(const a of n)await wt(e,a),await W(e,a,"screen-timeout");return n}r(Bd,"publishUnscreened");async function Hi(e,t,n,a,i){const{answers:s,requests:o,reopens:c}=await mt(e),l=new Date().toISOString(),f=[],p=[];for(const u of a.filter(h=>h.round_of))u.round=In(n.find(h=>h.id===u.round_of))+1,Ye(t,[u.round_of],`Followed by prototype round ${u.round}`,l),p.push(u.round_of);for(const u of a.filter(h=>h.reask_of))Ye(t,[u.reask_of],`Asked again as ${u.id}`,l),p.push(u.reask_of);for(const u of a){const h=Ce(u.url),m=n.filter(_=>_.topic===u.topic).map(_=>Ue(_,s,o,[],c)),b=u.topic.startsWith("item:")?Zi(m):[];for(const _ of m)b.includes(_.id)?(Ye(t,[_.id],`Followed by ${u.id}`,l),p.push(_.id)):!_.resolved&&h&&Ce(_.url)===h&&(Ye(t,[_.id],`Replaced by ${u.id}`,l),f.push(_.id))}const w={...t,topics:t.topics||[],items:[...n,...a.map(u=>({...u,asked_at:l}))]};await y(L(e),w);for(const u of new Set(a.map(h=>la(h.topic)).filter(Boolean)))await ic(e,u);return{file:L(e),lines:[...i,...f.map(u=>`replaced: ${u}`),...p.map(u=>`resolved: ${u}`)]}}r(Hi,"publishQuestions");function Ur(e,t,n){return Yt(e,n),e?((!t.topic.startsWith("item:")||t.topic.startsWith("item:ask:"))&&d(`${n} goes on a card's topic, item:<file>/<id>`),t.options?.map(a=>a.id).join()!=="yes,no"&&d(`${n} takes exactly the options yes and no, in that order`),!0):!1}r(Ur,"checkGate");const Rt={prototype_gate:"prototype"};async function Jr(e,t,n,a){const i=t.topic.slice(5),{item:s}=await x(e,i),o=Rt[n];s.gates?.[o]&&d(`${a}.${n}: ${i} already has its ${o} answer, ${s.gates[o]}`)}r(Jr,"checkGateFact");async function Kr(e,t,n,a,i){const{source:s,itemFile:o,item:c}=await x(e,t);c.gates={...c.gates,[Rt[n]]:a},await y(s.file,o),await zr(e,t,n,i)}r(Kr,"keepGate");async function zr(e,t,n,a){const i=await g(fe(e),{}),s=(i[t]||[]).filter(f=>!f[n]);s.length!==(i[t]||[]).length&&await y(fe(e),s.length?{...i,[t]:s}:cn(i,t));const{owner:o,asked:c}=await ue(e,t),l=c.filter(f=>f[n]&&f.state==="open").map(f=>f.id);return l.length&&await pt(e,o,l,a),l.length>0}r(zr,"resolveGate");async function Ld(e,t){const n=new Map(((await g(L(e),{items:[]})).items||[]).map(o=>[o.id,o]));for(const o of t){const c=n.get(String(o.id)),l=Object.keys(Rt).find(u=>c?.[u]);if(!l||!["yes","no"].includes(o.choice))continue;const f=`Prototype round: ${o.choice}. The board moves the card on once every question is answered.`;await pt(e,await g(L(e),{topics:[],items:[]}),[c.id],f);const p=c.topic.slice(5),w=await x(e,p).catch(()=>null);w&&(await Kr(e,p,l,o.choice,f),l==="prototype_gate"&&o.choice==="yes"&&w.item.phase==="grilling"&&await pn(e,p)&&await W(e,p,"prototype-after"))}const a=new Map;for(const o of t){const c=n.get(String(o.id)),l=la(c?.topic);l&&a.set(l,[...a.get(l)||[],Qr(c,o)])}const i=[],s=[];for(const[o,c]of a)await x(e,o).catch(()=>null)&&await Gr(e,o,"writing them into its notes",async()=>{await Oe(e,o,`Answers (owner, ${Ze()})`,c.join(`
|
|
18
|
+
`));const l=await ca(e,o);l==="prototyping"&&i.push(o),l==="staging"&&s.push(o)});return{prototype:i,staged:s}}r(Ld,"settleAnswers");async function ca(e,t){const{item:n}=await x(e,t),{asked:a}=await ue(e,t);if(a.some(i=>i.state==="open"))return null;if(n.phase==="grilling"&&n.gates?.prototype==="yes"){await Ae(e,t,{phase:"prototyping"});const{source:i,itemFile:s,item:o}=await x(e,t);return o.gates={...o.gates,prototype:"ran"},Et(o,"prototype"),await y(i.file,s),await W(e,t,"prototype-round"),"prototyping"}return await Xr(e,t)?["prototyping","discussing"].includes(n.phase)?(await ec(e,t),"discussing"):(await ua(e,t),"staging"):null}r(ca,"moveOnAnswered");async function Gr(e,t,n,a){try{await a()}catch(i){if(!(i instanceof he))throw i;await W(e,t,"answers-failed",{what:n,error:i.message})}}r(Gr,"followUp");const la=r(e=>typeof e=="string"&&e.startsWith("item:")&&!e.startsWith("item:ask:")?e.slice(5):null,"cardOfTopic"),ln=r(e=>String(e).padStart(2,"0"),"pad2"),Ze=r((e=new Date)=>`${ln(e.getMonth()+1)}/${ln(e.getDate())} ${ln(e.getHours())}:${ln(e.getMinutes())}`,"noteStamp");function Qr(e,t){const n=t.choice===null||t.choice===void 0||t.choice===""?null:(e.options||[]).find(i=>i.id===t.choice)?.label??String(t.choice),a=String(t.note||"").trim();return`- ${e.id} (${e.question}): ${n?`${n}.`:"no option picked."}${a?` Note, verbatim: "${a}"`:""}`}r(Qr,"answerLine");const Ui=292,da=r(e=>String(e).toLowerCase().replace(/[^a-z0-9-]+/g,"-"),"notesBase"),Vr=r((e,t,n)=>st($.join(e,`.${da(t)}-notes`),n),"notesLock");function dn(e,t){let n=`${da(t)}-notes.md`;for(let a=2;me($.join(e,n));a++)n=`${da(t)}-notes-${a}.md`;return n}r(dn,"freeNotesName");async function fn(e,t){const n=$.join($.dirname(e),`.${$.basename(e)}.${process.pid}.tmp`);await O.rm(n,{force:!0}),await O.writeFile(n,t,{mode:Ui}),await O.rename(n,e)}r(fn,"writeNotesFile");const Zr=r(e=>/\n/.test(v(e,"--heading"))?d("--heading is one line, the section's title"):v(e,"--heading"),"notesHeading"),Yr=r((e,t,n)=>`${e.replace(/\n*$/,`
|
|
19
19
|
`)}
|
|
20
20
|
## ${t}
|
|
21
21
|
${n.replace(/^\n+|\s+$/g,"")}
|
|
22
|
-
`,"withSection");async function
|
|
23
|
-
`;return await
|
|
24
|
-
`),"messageWords");async function Qr(e,t,n){const{item:a}=await x(e,t);a.held&&await us(e,"card-hold",{key:t,hold:!1});const{owner:i,asked:s}=await ue(e,t),o=s.filter(l=>l.state==="open").map(l=>l.id);o.length&&await pt(e,i,o,"Skipped: you tapped Implement");const c=Ji(n);c&&await Ce(e,t,`Implement (owner, ${Ze()}), verbatim`,c),await da(e,t)}r(Qr,"stageImplement");async function Nd(e,t,n){const a=Ji(n);a&&await Ce(e,t,`Reply (owner, ${Ze()}), verbatim`,a),await da(e,t)}r(Nd,"stageReply");async function Rd(e,t,n){const{source:a,itemFile:i,item:s}=await x(e,t);!["discussing","ready","staging"].includes(s.phase)||he(s)||(s.phase=n==="prototype"?"prototyping":"grilling",delete s.staged_at,await b(a.file,i))}r(Rd,"planAgain");async function Vr(e,t){const n=await x(e,t).catch(()=>null);!n||!["ready","staging"].includes(n.item.phase)||he(n.item)||(n.item.phase="grilling",delete n.item.staged_at,await b(n.source.file,n.itemFile),await W(e,t,"new-question"))}r(Vr,"grillAgain");function Ki(e){return e.filter(t=>t.state==="answered").map(t=>t.id)}r(Ki,"answeredOnClaude");function Ye(e,t,n,a=new Date().toISOString()){for(const i of e.items||[])t.includes(i.id)&&Object.assign(i,{resolved:n,resolved_at:a})}r(Ye,"setResolved");async function pt(e,t,n,a){return Ye(t,n,a),await b(B(e),t),B(e)}r(pt,"markResolved");async function Ed(e,t,n){P(e);const a=await g(B(e),{topics:[],items:[]}),i=(a.items||[]).find(f=>f.id===t);i||d(`no question "${t}" in owner.json`);const{answers:s,requests:o,reopens:c}=await mt(e),l=Ue(i,s,o,[],c);if(l.state==="resolved")return B(e);l.state==="requested"&&d(`${t} waits on the prototype the owner asked for; question add with round_of ${t} resolves it`);const u=S(n,"--note");Be(i.url)&&Zr(a,i,s);const p=await pt(e,a,[t],u);await Zi(e,i.topic);const w=ra(i.topic);return w&&(await x(e,w).catch(()=>null))?.item.phase==="grilling"&&await oa(e,w,{roundAt:!0}),p}r(Ed,"resolveQuestion");async function Ld(e,t,n,a=[]){P(e);const[i,...s]=[...new Set(t)];i||d("question move takes the ids of the questions to merge"),await x(e,n);const o=[n,...a];new Set(o).size!==o.length&&d("question move names a card twice");for(const f of a)await x(e,f);const c=await g(B(e),{topics:[],items:[]}),{answers:l,requests:u,reopens:p}=await mt(e);for(const f of[i,...s]){const h=(c.items||[]).find(k=>k.id===f)||d(`no question "${f}" in owner.json`),{state:m}=Ue(h,l,u,[],p);m!=="open"&&d(`${f} is ${m}; question move takes open questions`)}for(const[f,h]of a.entries()){const{item:m}=await x(e,h),k=[...m.deps||[],o[f]].join(",");await Oe(e,h,{deps:k,relate:n})}const w=await H(e,n);return Object.assign(c.items.find(f=>f.id===i),{topic:`item:${n}`}),Ye(c,s,`Duplicate of ${w}'s question ${i}`),await b(B(e),c),B(e)}r(Ld,"moveQuestions");async function Bd(e,t){const n=t.filter(({choice:i,note:s})=>!i&&String(s||"").trim()).map(i=>i.id),a=n.length?await g(B(e),{topics:[],items:[]}):null;a?.items?.some(i=>n.includes(i.id))&&(Ye(a,n,"Answered with a note"),await b(B(e),a))}r(Bd,"resolveNoteAnswers");function Zr(e,t,n){const a=Ln(t),i=e.items.filter(s=>s.topic===t.topic&&Ln(s)===a).map(s=>Ue(s,n));co(i,a)||d(`${t.id} is in prototype round ${a}, which no pick has settled yet; a pick settles it`)}r(Zr,"roundSettled");async function mt(e){const t=await K($.join(e,"data","events.jsonl"));return{answers:await Promise.all(t.filter(a=>a.kind==="owner-answer").map(async a=>({...a,reply:a.reply||(await Wn($.join(e,"data","replies"),a.id)).length>0}))),requests:t.filter(a=>a.kind==="prototype-request"),reopens:t.filter(a=>a.kind===so)}}r(mt,"ownerEvents");async function Cd(e,t){const{asked:n}=await zi(e),a=new Map(n.map(i=>[i.id,i.state]));return t.filter(i=>!a.has(String(i.id))||["open","requested"].includes(a.get(String(i.id))))}r(Cd,"unrecorded");async function zi(e){const t=await g(B(e),{topics:[],items:[]}),{answers:n,requests:a,reopens:i}=await mt(e);return{owner:t,asked:(t.items||[]).map(s=>Ue(s,n,a,[],i))}}r(zi,"topicQuestionsAll");async function Gi(e,t){const n=await g(B(e),{topics:[],items:[]}),{answers:a,requests:i,reopens:s}=await mt(e),o=(n.items||[]).filter(c=>c.topic===t).map(c=>Ue(c,a,i,[],s));return{owner:n,asked:o}}r(Gi,"topicQuestions");const ue=r((e,t)=>Gi(e,`item:${t}`),"cardQuestions"),un=r((e,t,n)=>Qi(e,`item:${t}`,n),"resolveAnswered");async function Qi(e,t,n){if(!e)return[];const{owner:a,asked:i}=await Gi(e,t),s=Ki(i);return s.length&&await pt(e,a,s,n),await Zi(e,t),s.map(o=>`resolved: ${o}`)}r(Qi,"resolveAnsweredOn");const fn=r(async(e,t)=>!!e&&(await ue(e,t)).asked.some(n=>n.state==="open"),"hasOpenQuestion");function Vi(e,t,n){e.next=S(t,"--next"),e.next_at=new Date().toISOString(),n?e.next_on_questions=!0:delete e.next_on_questions}r(Vi,"setNext");async function Zi(e,t){const n=t.startsWith("item:")&&!t.startsWith("item:ask:")?t.slice(5):null;if(!n)return;let a;try{a=await x(e,n)}catch(c){if(c instanceof me)return;throw c}const{source:i,itemFile:s,item:o}=a;o.next_on_questions&&((await ue(e,n)).asked.some(c=>c.state!=="resolved")||(delete o.next,delete o.next_at,delete o.next_on_questions,await b(i.file,s)))}r(Zi,"clearWaitedNext");async function Yr(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);i.next===void 0&&i.next_on_questions===void 0||(delete i.next,delete i.next_at,delete i.next_on_questions,await b(n.file,a))}r(Yr,"clearNext");async function Yi(e,t,n,a){if(!e)return async()=>[];const{owner:i,asked:s}=await ue(e,t),o=s.filter(l=>l.state==="open").map(l=>l.id);o.length&&!a&&d(`${n}: ${t} has ${o.length===1?"an open question":"open questions"} ${o.join(", ")} with no answer yet; pass --force to close the card and resolve ${o.length===1?"it":"them"} too`);const c=s.filter(l=>l.state!=="resolved").map(l=>l.id);return async()=>(c.length&&await pt(e,i,c,"Closed with the card"),c.map(l=>`resolved: ${l}`))}r(Yi,"closeQuestions");async function Id(e,t,n,a,i){await x(e,t),Xt(n),ie(a,s=>S(s,"--owner-step")),ie(i,s=>fs(s)),await Yi(P(e),t,"land",!1)}r(Id,"checkClose");async function ua(e,t){const n=await g(B(e),{items:[]}),{answers:a,requests:i,reopens:s}=await mt(e);return io((n.items||[]).map(o=>Ue(o,a,i,[],s)),t)}r(ua,"unpickedPrototype");const Xr=r(e=>e.length>120?`${e.slice(0,119)}\u2026`:e,"clip"),ec={ask:"text","ask-edited":"text","item-message":"text","owner-answer":"note"};async function Xi(e,t,n){const a=S(t,n).replace(/\s+/g," ");return(await K($.join(e,"data","events.jsonl"))).map(s=>String(s.data?.[ec[s.kind]]??"").replace(/\s+/g," ").toLowerCase()).some(s=>s.includes(a.toLowerCase()))||d(`${n} must quote the owner's words in an ask, card message or answer note on this board`),a}r(Xi,"ownerQuote");async function Td(e,t,n){return P(e),n===void 0&&d(`only the owner's Serve / Build and the hub's Auto-build start a card run; pass --asked "<the owner's words>" quoting where the owner asked for ${t}'s run`),`Running at the owner's ask: "${await Xi(e,n,"--asked")}"`}r(Td,"askedGate");async function Et(e,t,n="--from-ask"){return _e(t,n),(await K($.join(P(e),"data","events.jsonl"))).some(i=>i.id===t&&i.kind==="ask")?t:d(`no ask "${t}" in this site's data/events.jsonl`)}r(Et,"checkAsk");function Lt(e,t,n=new Date().toISOString(),a=es(e,t)){if(!Me[t])throw new Error(`no lane "${t}"`);const i=e.lane;return ts(e,t,a),e.lane=t,e.lane_at=n,t==="done"?(e.closed_at=n,delete e.reopened_at):i==="done"&&(e.reopened_at=n),i}r(Lt,"placeIn");function es(e,t,n=!1){return t==="todo"?he(e)?"ready":e.phase??null:t==="progress"?"reading":t==="done"?n||Re(e).some(a=>a.merged_at)?"merged":"closed":null}r(es,"movedPhase");function ts(e,t,n){if(n!==null&&!Nn[t].includes(n))throw new Error(`no ${Me[t]} phase "${n}"`);n===null?delete e.phase:e.phase=n}r(ts,"setPhase");async function qd(e,t,n,a,i,s){const{source:o}=await x(e,t),{id:c}=F(t),l=new Date().toISOString();if(!await st(o.file,async()=>{const w=JSON.parse(await O.readFile(o.file,"utf8")),f=w.items.find(m=>m.id===c)||d(`item file "${o.slug}" has no item "${c}"`);if(s&&((f.lane??null)!==s.lane||(f.phase??null)!==s.phase)||f.lane===n&&(i===void 0||(f.phase??null)===i))return!1;let h;return f.lane===n?ts(f,n,i):h=Lt(f,n,l,i),await Je(o.file,JSON.stringify(w,null,2)+`
|
|
25
|
-
`),h!==void 0}))return null;const p={ts:l,who:"claude",kind:qa,text:`Moved to ${Me[n]}: ${S(a,"why")}`};return await le(Z(e,t),p),p.text}r(qd,"moveCard");const G=r(e=>e?.lane==="done"||!!e?.reopened_at,"closed"),he=r(e=>!!e?.lane&&e.lane!=="todo","leftTodo"),Md=r(async(e,t)=>G((await x(e,t)).item),"isFinished"),ns=["ready","discussing","grilling","prototyping"],fa="ready, discussing, grilling or prototyping",tc=["grilling","prototyping"];function as(e,t){t!==void 0&&d(`--state is now --phase: pass --phase ${ns.includes(t)?t:fa}`),e==="staging"&&d(`--phase staging is set by the board, on an owner action; pass ${fa}`),e!==void 0&&!ns.includes(e)&&d(`--phase takes ${fa}`)}r(as,"checkPhase");const nc="a card goes ready only from staging, which the board sets on the owner's Send, Implement or Reply";async function is(e,t){return(await K($.join(P(e),"data","events.jsonl"))).some(a=>a.kind==="ask"&&a.id===t&&a.data?.quick===!0||a.kind==="item-message"&&a.data?.key===`ask:${t}`&&a.data?.intent==="build")}r(is,"quickAsk");const ac=/^mark (?:it |this |the card )?ready\b/i;function ic(e,t,n){const a=[].concat(t).map(o=>Tn(o)||d(`--pr takes a PR number or owner/repo#<n>, not "${o}"`)),i=r((o,c)=>o.number===c.number&&(o.repo||n||null)===(c.repo||n||null),"same"),s=Re(e);for(const o of a)s.some(c=>i(c,o))||s.push(o);delete e.pr,e.prs=s}r(ic,"addPRs");const z=r((e,t)=>String(t).includes("/")?String(t):`${e}/${t}`,"depKey"),Xe=/^wave:(.+)$/;async function ht(e,t,n){const a=await Promise.all((n||[]).map(async i=>{const s=String(i).match(Xe)?.[1];if(!s)return[z(t,i)];const{source:o,wave:c}=await $t(e,s);return We(c).map(l=>`${o.slug}/${l}`)}));return[...new Set(a.flat())]}r(ht,"depCards");async function Ie(e,t,n){const a=await ht(e,t,n.deps),i=await Promise.all(a.map(s=>x(e,s).then(({item:o})=>o,()=>null)));return a.filter((s,o)=>i[o]&&!G(i[o]))}r(Ie,"unfinishedDeps");async function sc(e,t,n){const a=await Promise.all((await Ie(e,F(t).file,n)).map(i=>H(e,i)));return n.blocked_until?[...a,n.blocked_until]:a}r(sc,"blockerNames");async function wn(e,t,n){n??=(await x(e,t)).item;const a=await Promise.all((await Ie(e,F(t).file,n)).map(i=>H(e,i)));return[a.length&&`by ${a.join(", ")}`,n.blocked_until&&`until ${n.blocked_until}`].filter(Boolean).join(" and ")}r(wn,"blockWords");const oc=r((e,t)=>!xe(e)&&!e.pushed&&!e.merge_ready&&!G(e)&&(!!Rn(t)||t.some(n=>n?.started&&!n.done&&at(n))),"inProgress");async function rc(e,t){const n=[];for(const a of await I(e)){const i=await g(a.file,null);for(const s of Array.isArray(i?.items)?i.items:[])G(s)||s.blocked_until||!(await ht(e,a.slug,s.deps)).includes(t)||(await Ie(e,a.slug,s)).length||n.push(`${a.slug}/${s.id}`)}return n}r(rc,"unblockedBy");async function cc(e,t,n){const a=F(t).file,i=(await pa(e,a,n)).map(c=>Xe.test(c)?c:z(a,c));i.length||d("--split-into takes wave:<slug> or the cards the split filed, <id>,<file>/<id>"),(await ht(e,a,i)).includes(t)&&d(`${t} can't be split into itself`);const s=await Promise.all(i.map(async c=>{const l=c.match(Xe)?.[1];if(!l)return H(e,c);const{source:u,wave:p}=await $t(e,l),w=await Promise.all(We(p).map(f=>H(e,`${u.slug}/${f}`)));return`the wave ${p.title} (${w.join(", ")})`})),o=[];for(const c of await I(e))for(const l of(await g(c.file,null))?.items||[]){const u=`${c.slug}/${l.id}`;if(!(G(l)||!(l.deps||[]).some(p=>z(c.slug,p)===t))){for(const p of i)(await ht(e,a,[p])).includes(u)&&d(Xe.test(p)?`${u} waits on ${t} and is in the wave "${p.match(Xe)[1]}", so it would wait on itself; split into the new cards by key`:`${u} waits on ${t} and is one of the cards it splits into, so it would wait on itself`);o.push(u)}}return{entries:i,words:s.join(", "),waiters:o}}r(cc,"splitPlan");async function lc(e,t,{entries:n,words:a,waiters:i}){for(const o of await I(e)){const c=i.filter(u=>F(u).file===o.slug);if(!c.length)continue;const l=await g(o.file,null);for(const u of l.items.filter(p=>c.includes(`${o.slug}/${p.id}`))){const p=n.map(w=>!Xe.test(w)&&F(w).file===o.slug?F(w).id:w);u.deps=[...new Set([...u.deps.filter(w=>z(o.slug,w)!==t),...p])]}await b(o.file,l)}const s=await H(e,t);for(const o of i)await W(e,o,"split-moved",{card:s,to:a})}r(lc,"moveWaiters");async function wa(e,t,n){const{source:a,itemFile:i,item:s}=await x(e,t);if(he(s))s.unblocked_at=n;else if(s.phase==="ready")s.recheck_at=n;else return;await b(a.file,i)}r(wa,"markUnblocked");async function ss(e,t,n,a){const i=new Set(a.match(/#?[\w-]+(?:\.[\w-]+)*(?:\/[\w-]+(?:\.[\w-]+)*)*/g)||[]),s=new Set(await ht(e,F(t).file,n.deps));for(const o of await I(e)){const c=await g(o.file,null);for(const l of c?.items||[]){const u=`${o.slug}/${l.id}`;if(u===t||s.has(u)||G(l))continue;[l.id,it(c,l),l.ticket].filter(Boolean).flatMap(w=>[w,`${o.slug}/${w}`]).some(w=>i.has(w))&&d(`--next names unfinished card ${u}; add it with --deps`)}}}r(ss,"checkNextDeps");const os=r(e=>`item notes ${e} --heading "<heading>" --file <draft.md>`,"NOTES_COMMAND");function dc(e,t){t.size||d(`${e} has no size; a builder starts from it, so pass --size small or large with --phase ready`),t.plan||d(`${e} has no notes file; a builder starts from it, so add its notes with ${os(e)} before --phase ready`)}r(dc,"checkSizeAndNotes");async function uc(e,t,n,a){dc(t,a);const i=await O.readFile(await pn(n,a.plan),"utf8"),s=(await ue(e,t)).asked.filter(o=>o.answers.length&&!i.includes(o.id)&&!(o.prototype_gate&&!o.answers.at(-1).note)).map(o=>o.id);s.length&&d(`${t}'s notes leave out the answered question${s.length===1?"":"s"} ${s.join(", ")}; record each answer in its notes, naming its id, with ${os(t)} before it is ready`)}r(uc,"checkNotesComplete");async function pn(e,t,{missingOk:n=!1}={}){const a=r(o=>d(`a notes file is a .md file inside ${e}, named relative to it: "${t}" ${o}`),"bad");(typeof t!="string"||!t.trim())&&a("is empty"),($.isAbsolute(t)||t.split(/[\\/]/).includes(".."))&&a("leaves the folder"),$.extname(t).toLowerCase()!==".md"&&a("is not a .md file");const i=await O.realpath(e),s=await O.realpath($.resolve(e,t)).catch(()=>n?$.resolve(i,t):a("does not exist"));return s.startsWith(i+$.sep)||a("leaves the folder"),s}r(pn,"planPath");async function fc(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);if(i.plan)return{itemFile:a,item:i,markdown:await O.readFile(await pn(n.dir,i.plan),"utf8")};if(typeof i.notes=="string"&&i.notes.trim())return{itemFile:a,item:i,markdown:i.notes};d(`${t} has no notes`)}r(fc,"readNotes");function rs({install:e,installSha:t,installLabel:n,clearInstall:a}){if(e===void 0)return(t!==void 0||n!==void 0)&&d("--install-sha and --install-label go with --install <url>"),a?null:void 0;a&&d("pass --install or --clear-install, not both");let i;try{i=new URL(String(e).trim())}catch{i=null}(!i||!["https:","itms-services:"].includes(i.protocol))&&d(`--install takes an https:// or itms-services:// URL, not "${e}"`),i.protocol==="itms-services:"&&!(i.searchParams.get("action")==="download-manifest"&&/^https:\/\//.test(i.searchParams.get("url")||""))&&d("--install: an itms-services:// URL needs ?action=download-manifest&url=https://\u2026/manifest.plist");const s=t===void 0?void 0:/^[0-9a-f]{7,40}$/i.test(t)?t.toLowerCase():d(`--install-sha takes a commit sha of 7 to 40 hex digits, not "${t}"`);return{url:String(e).trim(),...n===void 0?{}:{label:S(n,"--install-label")},built_at:new Date().toISOString(),...s?{sha:s}:{}}}r(rs,"installOf");async function Wd(e,{install:t,installSha:n}){const a=rs({install:t,installSha:n});a||d("main set needs --install <url>");const i=$.join(P(e),"data","main.json");return await b(i,{install:a}),{file:i,lines:["main install set"]}}r(Wd,"setMainInstall");const wc=r((e,t)=>e.length?`Merged PR${e.length>1?"s":""} ${Ne(e,t)}.`:"Merged.","mergedLine");function pc(e,t,n,a,i,s){return s?{status:s}:n&&a===void 0?{status:["merged",{words:wc(n,i)}]}:(a===void 0&&e&&d(`--done needs --text "<what was done, or why it closes without landing>", which becomes ${t}'s last thread line`),a===void 0?null:{text:a})}r(pc,"closingLine");function mc(e,t,n){const a=new Date().toISOString(),i=r((s,o)=>s.number===o.number&&(s.repo||n||null)===(o.repo||n||null),"same");e.prs=Re(e).map(s=>{const o=t.find(c=>i(s,c));return o&&!s.merged_at?{...s,merged_at:o.at||a}:s}),delete e.pr}r(mc,"markMerged");async function cs(e,t,{ticket:n,newTicket:a,status:i,clearStatus:s,done:o,splitInto:c,merged:l,landed:u,start:p,handBack:w,next:f,clearNext:h,pr:m,pushed:k,mergeReady:_,ghSince:N,fromAsk:j,phase:A,state:D,held:T,unheld:M,pickUpAt:v,deps:L,blockedUntil:C,blockReason:q,parent:se,related:R,relate:J,unrelate:ee,install:Q,installSha:ye,installLabel:be,clearInstall:Y,title:Ae,size:qt,model:kt,force:yt,text:oe,closing:ne,who:bn,by:ge,command:Oa,ownerStep:tt,ownerDone:qs,check:bt},Aa){(i!==void 0||s)&&d(`--status${s?" and --clear-status are":" is"} gone: a card's lane is stored and moves itself. Close it with --done --text "<what was done, or why it closes without landing>"`),l&&(o=!0),R!==void 0&&(J!==void 0||ee!==void 0)&&d("pass --related, or --relate and --unrelate, not both"),as(A,D),yt&&!o&&d("--force goes with --done"),c!==void 0&&!o&&d("--split-into goes with --done"),c!==void 0&&oe!==void 0&&d("--split-into writes the closing line; drop --text");const xt=c===void 0?null:await cc(e,t,c);xt&&(oe=`Split into ${xt.words}`),j!==void 0&&o&&d("--from-ask opens a card; close it in another call"),oe!==void 0&&Xt(oe),tt!==void 0&&S(tt,"--owner-step");const _t=bt===void 0||bt===""?bt:fs(bt);oe!==void 0&&j===void 0&&!o&&A!=="ready"&&d("--text goes with --from-ask, --done, or --phase ready on a staging card"),j!==void 0&&oe===void 0&&d(`--from-ask needs --text "<first line>", the item's first thread line after the ask`),f!==void 0&&h&&d("pass --next or --clear-next, not both"),typeof f=="string"&&!f.trim()&&([f,h]=[void 0,!0]);const St=ds({held:T,unheld:M,pickUpAt:v}),Mt=rs({install:Q,installSha:ye,installLabel:be,clearInstall:Y});[o,p,w,f,h,m,k,_,N,j,A,St,L,C,q,se,R,J,ee,Mt,Ae,qt,kt,n,a,tt,bt].every(E=>E===void 0||E===!1)&&d("nothing to set: pass --done, --next, --clear-next, --pr, --pushed, --gh-since, --from-ask, --phase, --held, --unheld, --pick-up-at, --deps, --blocked-until, --block-reason, --parent, --related, --relate, --unrelate, --install, --clear-install, --title, --size, --model, --ticket, --new-ticket or --check");const te=j===void 0?void 0:await Et(e,j);te&&await Qn(e,te),te&&await yi(e,te,t);const{source:qe,itemFile:nt,item:y}=await x(e,t),Pa=te??(y.phase?void 0:y.from_ask),Ms=Pa!==void 0&&await is(e,Pa);A!==void 0&&y.answered_at&&!y.closed_at&&(delete y.answered_at,he(y)&&Lt(y,"todo",void 0,null)),A!==void 0&&he(y)&&d(`--phase sets a Todo card's phase; ${t} is ${Me[y.lane]}, where the board sets it`),A==="ready"&&!Ms&&!["staging","ready"].includes(y.phase)&&d(`${t} reads ${y.phase||"new"}, not staging; ${nc}`),te&&y.from_ask&&y.from_ask!==te&&d(`${t} came from ask "${y.from_ask}", so ask "${te}" can't be its from-ask; to fold it in: item relate-ask ${te} ${t} --summary "<line>"`),te&&St&&A!==void 0&&!y.phase&&d("a card filed held is filed New and takes no --phase; drop --phase, or drop --held to file it in a phase"),te&&A===void 0&&!y.phase&&!St&&d("--from-ask files a card, which reads New until it has a phase: pass --phase discussing, grilling, prototyping, or ready for a Quick build ask, or --held to file it for later");const xn=G(y);xt&&xn&&d(`${t} is already Done, so the cards waiting on it were unblocked then; move them with item set <key> --deps`),tt!==void 0&&xn&&d(`${t} is already Done, so its ticket closed; an owner step goes on the command that finishes the card: land --owner-step, or builder done before the merge`),tt!==void 0&&(y.owner_step=S(tt,"--owner-step")),qs&&delete y.owner_step,_t!==void 0&&!y.owner_step&&d(`--check goes with an owner step, which ${t} doesn't have: pass --owner-step "<the step>" with it on builder done or land`),_t&&(y.owner_check={check:_t,since:y.owner_check?.check===_t?y.owner_check.since:new Date().toISOString()}),(_t===""||!y.owner_step)&&delete y.owner_check;const _n=await Promise.all((await V(qe.dir,y.id)).map(E=>g(E,null))),Ws=En(y,_n);if(Ae!==void 0&&(y.title=S(Ae,"--title")),ls(y,{size:qt,model:kt}),L!==void 0){const E=await pa(e,F(t).file,L,t);E.length?y.deps=E:delete y.deps}const Da=C===""?y.blocked_until:void 0;if(C!==void 0&&(C===""?delete y.blocked_until:y.blocked_until=S(C,"--blocked-until")),q&&(!y.deps?.length&&!y.blocked_until&&d("--block-reason goes with a block: --deps or --blocked-until"),y.block_reason=S(q,"--block-reason")),(q===""||!y.deps?.length&&!y.blocked_until)&&delete y.block_reason,se!==void 0){const E=await ma(e,F(t).file,t,se);E?y.parent=E:delete y.parent}const Fa=se!==void 0||R!==void 0||J!==void 0||ee!==void 0?await ha(e,t):null;let Sn=R===void 0?null:await $c(e,t,y,R);J!==void 0&&(y.related=[...new Set([...(y.related||[]).map(E=>z(F(t).file,E)),...await hn(e,t,J,"--relate")])]),ee!==void 0&&(Sn=await kc(e,t,y,await hn(e,t,ee,"--unrelate"))),St!==void 0&&mn(y,St),p&&y.held&&d(y.pick_up_at?`${t} is held until ${y.pick_up_at}, when the board resumes it; start it early only with item set ${t} --unheld`:`${t} is held; start it only once the owner resumes it, with item set ${t} --unheld`);const vt=m!==void 0||k!==void 0||_!==void 0;if((p||o||w||vt)&&(gc(y),delete y.recheck_at),p){const E=await Ie(e,F(t).file,y);E.length&&d(`${t} is blocked by ${E.join(", ")}; start it once ${E.length===1?"that is":"those are"} done`),y.blocked_until&&d(`${t} is blocked until ${y.blocked_until}; start it once that holds and the block is cleared with --blocked-until ""`)}const Wt=p&&e?await ua(e,t):null;Wt&&d(`${t} has no take picked: the owner's answer to ${Wt.id} picks none${Wt.note?` ("${Xr(Wt.note)}")`:""}, so it prototypes again: draw the next round from that note, or grill when the note asks a question, and build once a take is picked`);const Us=o?await Yi(e,t,"--done",yt):async()=>[],jt=o?pc(e,t,l,oe,fe(e,nt),ne):null;if(l&&mc(y,l,fe(e,nt)),f!==void 0&&(Vi(y,f,await fn(e,t)),await ss(e,t,y,y.next)),h&&(delete y.next,delete y.next_at,delete y.next_on_questions),p&&f===void 0&&y.next_on_questions&&(delete y.next,delete y.next_at,delete y.next_on_questions),vt&&!y.review_since&&(y.review_since=new Date().toISOString()),m!==void 0&&ic(y,m,fe(e,nt)),k!==void 0){ke().find(E=>Fe(E.dir,P(e)))?.remote||d("--pushed is for a board whose repo is a plain git remote; on GitHub, open a PR and pass --pr"),xe(y)&&d(`${t} has a PR; its PR says when it merges`);try{Po(k)}catch{d("--pushed takes the name of the branch you pushed")}y.pushed={branch:k,at:new Date().toISOString()},y.branch=k}if(_!==void 0&&(y.merge_ready={branch:_,at:new Date().toISOString()},y.branch=_),o&&delete y.merge_ready,N!==void 0&&((typeof N!="string"||!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(N)||Number.isNaN(Date.parse(N)))&&d("--gh-since takes an ISO-8601 UTC time such as 2026-10-23T11:00:00Z"),y.gh_since=N),te&&(y.from_ask=te),A==="ready"&&e){const E=(await ue(e,t)).asked.filter(De=>De.state==="open").map(De=>De.id);E.length&&d(`${t} has ${E.length===1?"an open question":"open questions"} ${E.join(", ")}; it is ready once the owner has answered ${E.length===1?"it":"them"}`),await uc(e,t,qe.dir,y)}const vn=A==="ready"&&y.phase==="staging"&&j===void 0;vn&&oe===void 0&&d(`${t} goes ready from staging on your judgment: pass --text "<what you checked it against on the board>", which goes into its notes`);const jn=A==="ready"&&y.phase==="ready"&&!!y.recheck_at&&j===void 0;jn&&oe===void 0&&d(`${t}'s block ended, so it builds once you re-check its notes against what landed: pass --text "<what you re-checked>", which goes into its notes`),A==="ready"&&!vn&&!jn&&oe!==void 0&&j===void 0&&!o&&d(`${t} reads ${y.phase||"no phase"}, not staging, so there is no judgment for --text to record`);const Hs=A==="ready"&&y.phase!=="ready";A!==void 0&&(tc.includes(A)&&["discussing","staging","ready"].includes(y.phase)&&(y.round_at=new Date().toISOString()),y.phase=A,delete y.recheck_at,delete y.staged_at,delete y.unfiled,delete y.filed_held),y.phase==="ready"&&ac.test(y.next??"")&&d(`${t} would read ready with Next "${y.next}", which asks for the phase it is already in; pass --next with what happens next, or --clear-next`),Mt&&(y.install=Mt),Mt===null&&delete y.install,!Ws&&!p&&En(y,_n)&&d(`that leaves ${t} In Progress with nothing building and no Next; pass --next "<what happens next>"`);const On=(L!==void 0||C)&&!o&&oc(y,_n)?await wn(e,t,y):"",Na=fe(e,nt);let An=null;const Pn=r((E,De)=>{y.lane!==E&&Lt(y,E,void 0,es(y,E,u))!==void 0&&De&&(An=`Moved to ${Me[E]}: ${De}`)},"moveTo");o?Pn("done",null):vt&&y.lane!=="done"?Pn("review",m!==void 0?`PR ${Ne([].concat(m).map(Tn),Na)} opened`:k!==void 0?`${k} pushed for its merge`:"verified, it waits for your Merge"):p&&!G(y)&&Pn("progress","a build started");const Ra=A==="ready"&&!y.ticket&&n===void 0&&!a&&!!(e&&await gt(e));if(Ra&&(a=!0),a&&y.ticket&&d(`${t} already has ticket ${y.ticket}`),(n!==void 0||a)&&await ms(e,nt,y,n,Aa,qe.dir),await b(qe.file,nt),Ra&&await W(e,t,"ticket-made",{ticket:y.ticket}),An&&await le(Z(e,t),{ts:y.lane_at,who:"claude",kind:qa,text:An}),vn&&await Ce(e,t,`Ready (orchestrator, ${Ze()})`,oe),jn&&await Ce(e,t,`Re-checked (orchestrator, ${Ze()})`,oe),Hs&&await Ee(e,t,{command:"readied"}),On){const E=e&&!await Bs(e,t)&&await Ls(e,t,{cause:"blocked",words:`blocked ${On}`});await W(e,t,E?"blocked-pausing":"blocked-stays",{blocked:On})}Sn&&await Sn(),Fa&&await ps(e,t,Fa,await ha(e,t));const Pe=await Us();if(A==="ready"&&e&&Pe.push(...await un(e,t,"Recorded in the card's notes")),p&&Pe.push(...await un(e,t,"Acted on: build started")),te&&Pe.push(...(await Ge(e,t,{text:oe})).lines),jt&&e){const E=Oa?await Ee(e,t,{command:Oa,text:jt.text,who:bn,by:ge}):jt.status?await W(e,t,...jt.status):await Ge(e,t,{text:jt.text});Pe.push(...E.lines)}const Ea=y.owner_step&&o?null:_o({start:p,done:o,handover:vt}),La=Ea&&await So(await gt(e),y,Ea,{...Aa,repo:Na});La&&Pe.push(La),(vt||o||w)&&(await Is(qe.dir,y.id,o,w?{handedBack:new Date().toISOString()}:{endHandBack:!0}),await wt(e,t));const Dn=te?await ve(e):{};if(re(Dn,te)!==void 0&&(delete Dn[te],await b(Se(e),{links:Dn})),Da&&!G(y)&&!(await Ie(e,F(t).file,y)).length){const{line:E}=await W(e,t,"unblocked",{lifted:Da});await wa(e,t,E.ts),await pe({dir:e},"card-unblocked",{key:t}),Pe.unshift(`unblocked: ${t}`)}if(!o||xn)return{file:qe.file,lines:Pe};xt&&await lc(e,t,xt);const Ba=await rc(e,t),Js=await H(e,t);for(const E of Ba){const{line:De}=await W(e,E,"unblocked-done",{card:Js});await wa(e,E,De.ts),await pe({dir:e},"card-unblocked",{key:E})}return{file:qe.file,lines:[...Ba.map(E=>`unblocked: ${E}`),...Pe]}}r(cs,"changeItem");function ls(e,{size:t,model:n}){for(const[a,i,s]of[["size",t,["small","large"]],["model",n,["opus","sonnet"]]])i!==void 0&&(i===""?delete e[a]:e[a]=s.includes(i)?i:d(`--${a} takes ${s.join(" or ")}, or "" to clear it`))}r(ls,"setBuildChoice");function ds({held:e,unheld:t,pickUpAt:n}){return[e,t,n!==void 0].filter(Boolean).length>1&&d("pass one of --held, --unheld and --pick-up-at"),n!==void 0?n===""?null:{until:hc(n)}:e?{until:null}:t?null:void 0}r(ds,"holdOf");function hc(e){const t=typeof e=="string"&&bo.test(e)?Date.parse(e):NaN;return Number.isNaN(t)&&d('--pick-up-at takes an ISO time such as 2026-11-04T09:00, the host\u2019s local time unless it names an offset, or "" to resume the card'),t<=Date.now()&&d(`--pick-up-at ${e} has passed; pass a later time, or --unheld to resume the card now`),new Date(t).toISOString()}r(hc,"pickUpTime");function gc(e){delete e.held,delete e.pick_up_at}r(gc,"clearHold");function mn(e,t){t?e.held=!0:delete e.held,t?.until?e.pick_up_at=t.until:delete e.pick_up_at,e.held_at=new Date().toISOString()}r(mn,"setHold");async function Ud(e,t,n,{announce:a=!0}={}){const{source:i,itemFile:s,item:o}=await x(e,t);if(a){const c=new Date(n).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});await W(e,t,"hold-ended",{at:c}),!o.blocked_until&&!(await Ie(e,F(t).file,o)).length&&await pe({dir:e},"card-unblocked",{key:t,until:n})}mn(o,null),await b(i.file,s)}r(Ud,"pickUp");async function us(e,t,n){const a=String(n?.key||""),{file:i,id:s}=F(a),{source:o,itemFile:c}=await Ke(e,i),l=n.shared_by?n.shared_by.name||n.shared_by.login:void 0,u={text:Ht.done,who:"love",by:l,command:"done"},p=c.items.find(m=>m.id===s)||d(`item file "${i}" has no item "${s}"`);if(!(t==="card-done"&&!!p.owner_step)&&p.lane==="done")return null;if(t==="card-done")return Oe(e,a,{done:!0,force:!0,ownerDone:!0,...u});const f=n.hold?typeof n.until=="string"?new Date(n.until).toISOString():null:void 0;mn(p,n.hold?{until:f}:null),await b(o.file,c),n.hold&&(await Bs(e,a)||await Ls(e,a,{cause:"held",words:"held"}),await Is(o.dir,s,!1,{held:!0}),await wt(e,a));const h=f&&new Date(f).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});return Ee(e,a,n.hold?{text:h?`${Ht.hold} until ${h}`:Ht.hold,who:"love",by:l,command:"hold"}:{who:"love",by:l,command:"resume"})}r(us,"recordOwnerTap");function fs(e){try{return Qo(Go(e))}catch(t){return d(t.message)}}r(fs,"checkSpec");async function Hd(e,t,n,a){const{item:i}=await x(e,t);return!i.owner_step||i.owner_check?.check!==n?null:Oe(e,t,{done:!0,force:!0,ownerDone:!0,closing:["check-passed",{found:a}]})}r(Hd,"passOwnerCheck");async function Jd(e,t,n,a){const{source:i,itemFile:s,item:o}=await x(e,t);return!o.owner_step||o.owner_check?.check!==n?!1:(o.owner_check.broken=a,await b(i.file,s),await W(e,t,"check-broken",{error:a}),await pe({dir:e},"owner-check-broken",{key:t,check:n,error:a}),!0)}r(Jd,"breakOwnerCheck");async function pa(e,t,n,a){const i=[...new Set(String(n).split(",").map(s=>s.trim()).filter(Boolean))];for(const s of i){const o=s.match(Xe)?.[1];if(!o){await x(e,z(t,s));continue}(await ht(e,t,[s])).includes(a)&&d(`${a} is in the wave "${o}", so it would wait on itself`)}return i}r(pa,"depList");async function ma(e,t,n,a){if(!String(a).trim())return null;const i=z(t,String(a).trim());i===n&&d("--parent: a card can't be its own Parent");for(let s=i;s;){const o=s===i?await x(e,s):await x(e,s).catch(l=>{if(l instanceof me)return null;throw l});if(!o)break;const{item:c}=o;c.parent&&z(F(s).file,c.parent)===n&&d(`--parent: ${i} is a child of ${n}, so it can't be its Parent`),s=c.parent?z(F(s).file,c.parent):null}return i}r(ma,"parentKey");async function hn(e,t,n,a){const i=t.split("/")[0],s=[...new Set(String(n).split(",").map(o=>o.trim()).filter(Boolean).map(o=>z(i,o)))];s.includes(t)&&d(`${a}: a card can't be related to itself`);for(const o of s)await x(e,o);return s}r(hn,"relatedKeys");const ws=r((e,t,n)=>async()=>{for(const a of await I(e)){const i=await g(a.file,null);let s=!1;for(const o of Array.isArray(i?.items)?i.items:[]){const c=`${a.slug}/${o.id}`;c===t||!n(c)||!(o.related||[]).some(l=>z(a.slug,l)===t)||(o.related=o.related.filter(l=>z(a.slug,l)!==t),o.related.length||delete o.related,s=!0)}s&&await b(a.file,i)}},"dropLinksTo");async function ha(e,t){const n=new Set;let a=null;for(const i of await I(e))for(const s of(await g(i.file,null))?.items||[]){const o=`${i.slug}/${s.id}`,c=(s.related||[]).map(l=>z(i.slug,l));o===t?(c.forEach(l=>n.add(l)),s.parent&&(a=z(i.slug,s.parent))):c.includes(t)&&n.add(o)}return{related:n,parent:a}}r(ha,"linksOf");async function ps(e,t,n,a){const i=r(async(s,o,c=s,l=s)=>{await Ee(e,t,{command:c,card:o}),await Ee(e,o,{command:l,card:t})},"both");for(const s of a.related)n.related.has(s)||await i("related",s);for(const s of n.related)a.related.has(s)||await i("unrelated",s);a.parent&&a.parent!==n.parent&&await i("followup-of",a.parent,"followup-of","followup")}r(ps,"postLinkLines");async function $c(e,t,n,a){const i=await hn(e,t,a,"--related");return i.length?n.related=i:delete n.related,ws(e,t,s=>!i.includes(s))}r($c,"setRelated");function kc(e,t,n,a){const i=(n.related||[]).filter(s=>!a.includes(z(F(t).file,s)));return i.length?n.related=i:delete n.related,ws(e,t,s=>a.includes(s))}r(kc,"unrelateCards");const fe=r((e,t)=>e&&ke().find(n=>Fe(n.dir,e))?.github||t.repo,"boardRepoOf");async function Kd(e,t){const{itemFile:n,item:a}=await x(e,t);xe(a)||d(`${t} has no PR`);const i=Pt(a,fe(e,n));i.length||d(`${t} has no repo`);const s=a.gh_since||"1970-01-01T00:00:00Z";try{const o=je(await ae(e)).github_login||await Lo();return await Promise.all(i.map(async({repo:c,number:l})=>({repo:c,...Xa(await Ya({repo:c,pr:l,since:s}),{pr:l,since:s,me:o})})))}catch(o){d(o.message)}}r(Kd,"prReport");async function zd(e,t,{replyTo:n,body:a}){const i=n===void 0?"pr comment":"pr reply",s=Tn(t)||d(`${i} takes a PR number or owner/repo#<n>`);n!==void 0&&!/^[1-9]\d*$/.test(n)&&d("pr reply takes the id of the review comment it answers");const o=S(a,"--body").trim();(o.startsWith(At.trim())||o.includes(In))&&d(`${i} adds the ${At.trim()} prefix and the claude-bridge marker itself; pass only your words`);const c=s.repo||fe(P(e),{})||d(`${i} needs a board on GitHub; this board has no repo there`);try{const l=await Bo({repo:c,pr:s.number,replyTo:n,words:o,login:je(await ae(e)).github_login});return{lines:[`posted on PR ${s.repo?t:`#${t}`}: ${l}`]}}catch(l){d(l.message)}}r(zd,"postPR");const gt=r(async e=>Ei((await ae(e)).tickets),"boardSource");async function Gd(e,t,{afterApproval:n,confirmed:a,tapped:i}={}){P(e);const{itemFile:s,item:o}=await x(e,t);xe(o)||d(`${t} has no PR`),G(o)&&d(`${t} is done`);const c=await wn(e,t,o);c&&(i&&await W(e,t,"merge-blocked",{blocked:c}),d(`Not merged: blocked ${c}.`));const l=fe(e,s),u=Pt(o,l);u.length||d(`${t} has no repo`);const p=new Date().toISOString(),w=r(v=>W(e,t,"gh-limited",{what:"Not merged",words:Zs(v)}),"limited");let f;try{f=await Promise.all(u.map(v=>Ya({repo:v.repo,pr:v.number,comments:!1})))}catch(v){v.limited&&await w(v.limited),d(v.message)}const h=f.map(({view:v})=>v);if(To(h)){const v=u.map((L,C)=>({...L,at:h[C].mergedAt})).filter((L,C)=>h[C].state==="MERGED");return Oe(e,t,{merged:v,closing:["already-merged",{prs:Ne(v,l),many:v.length>1}]})}const m=u.map((v,L)=>({...v,...f[L]})).filter(({view:v})=>v.state==="OPEN"),k=Kt(s,(await ae(e)).reviewers||[]).length>0,_=[];let N=!1,j=!1;for(const v of m){const{view:L,reviews:C}=v,{failing:q,pending:se}=Xa(v,{pr:v.number,since:p,me:null}),R=ao({...L,checks:{state:q.length?"fail":se?"pending":"pass",failing:q,pending:se}},{needsApproval:k}),J=C.filter(Q=>Q.state==="APPROVED").sort((Q,ye)=>String(Q.submitted_at).localeCompare(String(ye.submitted_at))).at(-1)?.commit_id;let ee=!!(J&&L.headRefOid&&J!==L.headRefOid);ee&&n===void 0&&Lc(o,J,L.headRefOid)&&(ee=!1,j=!0),N||=ee,ee&&n===void 0&&R.push('commits after the approval: pass --after-approval "<why they are trivial>" when they are, else ask the approver to look again'),_.push(...u.length>1?R.map(Q=>`${Ne([v],l)} ${Q}`):R)}m.length||_.push(u.length>1?"every PR is closed":"PR is closed"),n!==void 0&&!N&&d("--after-approval: no commit came after the approval; drop it");const A=o.manual_steps?.length?a===void 0?null:await Xi(e,a,"--confirmed"):void 0;if(a!==void 0&&A===void 0&&d(`--confirmed: ${t} has no manual steps; drop it`),A===null&&_.push(`manual steps: merge only on the owner's word, with --confirmed "<their words>"`),_.length){const v=`Not merged: ${_.join("; ")}.`;i&&await W(e,t,"merge-refused",{why:_.join("; ")}),d(v)}const D=[];for(const v of m){const L=await Ca(["pr","view",String(v.number),"--repo",v.repo,"--json","title,body,commits"]);L.limited&&await w(L.limited),L.status!==0&&d(`gh pr view ${Ne([v],l)} failed: ${L.stderr.trim().split(`
|
|
26
|
-
`)[0]||`gh exited ${
|
|
27
|
-
`)[0]||`gh exited ${J.status}`}${D.length?` (merged ${
|
|
22
|
+
`,"withSection");async function Oe(e,t,n,a){const{source:i,item:{id:s}}=await x(e,t);return Vr(i.dir,s,async()=>{const{source:o,itemFile:c,item:l}=await x(e,t);let f;l.plan?(f=await hn(o.dir,l.plan,{missingOk:!0}),me(f)||await W(e,t,"notes-missing")):(l.plan=dn(o.dir,l.id),f=$.join(o.dir,l.plan),await y(o.file,c));const p=me(f)?await O.readFile(f,"utf8"):`# ${l.title}
|
|
23
|
+
`;return await fn(f,Yr(p,Zr(n),a)),l.plan})}r(Oe,"appendNotes");async function Ji(e){const{tickets:t}=je(await ie(e)),n=[],a=new Map;for(const i of await q(e)){const s=await g(i.file,null);a.set(i.slug,{source:i,itemFile:s||{}});for(const o of s?.items||[])o.plan&&n.push({key:`${i.slug}/${o.id}`,ref:it(s,o,t),title:o.title,created:o.created||"",archived:!1,dir:i.dir,plan:o.plan})}for(const i of e?await qe(e):[]){if(!i.item?.plan)continue;const s=a.get(i.file);n.push({key:i.key,ref:it(s?.itemFile||{},i.item,t),title:i.item.title,created:i.item.created||"",archived:!0,dir:s?.source.dir||$.join(e,"data"),plan:i.item.plan})}return n}r(Ji,"cardsWithNotes");const Ki=r(e=>O.realpath($.resolve(e.dir,e.plan)).catch(()=>null),"realNotes");async function fa(e,t,n,a){if(t==="-")return eo(0,"utf8");const i=await O.realpath(t).catch(()=>d(`${n} ${t}: no such file`));for(const s of await Ji(e))await Ki(s)===i&&d(`${n} ${t} is the notes file of ${s.ref} "${s.title}" (${s.key}), and a card's notes are its own; ${a(s)}`);return O.readFile(i,"utf8")}r(fa,"refuseCardNotes");const un="write a draft of your own outside the board's data folder";async function Id(e,t,{heading:n,file:a,from:i}){let s;if(i!==void 0){i===t&&d(`${t} can't fold its own notes into itself; pass --from <the other card>`);const[l,f]=await Promise.all([x(e,t),x(e,i)]),[p,w]=await Promise.all([l,f].map(({source:u,item:h})=>h.plan?O.realpath($.resolve(u.dir,h.plan)).catch(()=>null):null));p&&p===w&&d(`${i} and ${t} name one notes file, ${l.item.plan}, from before the board named each card's own; the board splits it as it starts`),s=(await us(e,i)).markdown,n??=`Folded from ${await H(e,i)} "${f.item.title}"`}else s=await fa(e,a,"--file",l=>l.key===t?`${un}, holding only the new section`:`${un}, or fold that card's notes in with item notes ${t} --from ${l.key}`);s.trim()||d(`${i??a} is empty`);const o=await Oe(e,t,n,s),c=await wn(e,t,"Recorded in the card's notes");return{file:o,heading:n,lines:c}}r(Id,"addNotes");const Cd=r(e=>le(e,async()=>{const t=await Ji(e),n=new Map;for(const i of t){const s=await Ki(i);s&&n.set(s,[...n.get(s)||[],i])}const a=[];for(const[i,s]of n){const[o,...c]=s.sort((l,f)=>l.created<f.created?-1:l.created>f.created?1:l.key<f.key?-1:1);for(const l of c.filter(f=>!f.archived)){const{source:f,itemFile:p,item:w}=await Ke(e,F(l.key).file).then(h=>({...h,item:h.itemFile.items.find(m=>m.id===F(l.key).id)})),u=w.plan;w.plan=dn(f.dir,w.id),await fn($.join(f.dir,w.plan),await O.readFile(i,"utf8")),await y(f.file,p),await W(e,l.key,"notes-shared",{card:o.ref}),a.push(`${l.key}: its own notes, ${w.plan}, copied from ${$.normalize(u)}, which ${o.key} keeps`)}await O.chmod(i,Ui)}return a}),"splitSharedNotes");async function Xr(e,t){const{source:n,item:a}=await x(e,t);return we(a)||xe(a)||a.pushed||(await Promise.all((await V(n.dir,a.id)).map(s=>g(s,null)))).some(s=>s?.started&&!s.done&&s.kind!==X)||(await ue(e,t)).asked.some(s=>s.state==="open")||(await g(fe(e),{}))[t]?!1:!await pa(e,t)}r(Xr,"stageable");async function ua(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);i.phase!=="staging"&&(Object.assign(i,{phase:"staging",staged_at:new Date().toISOString()}),delete i.unfiled,delete i.filed_held,await y(n.file,a),await Le(e,t,{command:"staged"}))}r(ua,"stage");async function ec(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);i.phase!=="discussing"&&(i.phase="discussing",await y(n.file,a),await Le(e,t,{command:"discussing"}))}r(ec,"discuss");const zi=r(({text:e,files:t})=>[String(e||"").trim(),...(Array.isArray(t)?t:[]).map(n=>`Attached: ${n?.path||n?.name||n}`)].filter(Boolean).join(`
|
|
24
|
+
`),"messageWords");async function tc(e,t,n){await nc(e,t);const{item:a}=await x(e,t);a.held&&await gs(e,"card-hold",{key:t,hold:!1});const{owner:i,asked:s}=await ue(e,t),o=s.filter(l=>l.state==="open").map(l=>l.id);o.length&&await pt(e,i,o,"Skipped: you tapped Implement");const c=zi(n);c&&await Oe(e,t,`Implement (owner, ${Ze()}), verbatim`,c),await ua(e,t)}r(tc,"stageImplement");async function nc(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);i.implement_ready&&(delete i.implement_ready,await y(n.file,a))}r(nc,"clearImplementReady");async function Td(e,t,n){const a=zi(n);a&&await Oe(e,t,`Reply (owner, ${Ze()}), verbatim`,a),await ua(e,t)}r(Td,"stageReply");const ac=["grill","prototype",X],wa={grill:"grilling",prototype:"prototyping"};function Et(e,t,n={},a=new Date().toISOString()){const i=e.briefs||[],s=i.find(c=>c.kind===t&&!c.brief),o=Object.keys(n).length?{ask:n}:{};e.briefs=s?i.map(c=>c===s?{...c,...o}:c):[...i,{kind:t,at:a,...o}]}r(Et,"owe");async function qd(e,t,n,a={}){const{source:i,itemFile:s,item:o}=await x(e,t);z(o)||n!==X&&we(o)||(n!==X&&["discussing","ready","staging"].includes(o.phase)&&(o.phase=wa[n],delete o.staged_at),Et(o,n,a),await y(i.file,s))}r(qd,"planAgain");async function Md(e,t,n){const{source:a,itemFile:i,item:s}=await x(e,t);Et(s,n),await y(a.file,i)}r(Md,"oweBrief");async function Wd(e,t,{kind:n,file:a}){P(e),ac.includes(n)||d(`item brief needs --kind grill, prototype or ${X}`),a===void 0&&d("item brief needs --file <draft.md>: what the discussion settled, what is still open, what the agent should cover and the files that matter");const i=await H(e,t),{item:s}=await x(e,t);z(s)&&d(`${i} is done; there is nothing to brief`),n!==X&&we(s)&&d(`${i} is past Todo, so it gets no ${n}`);const o=(await fa(e,a,"--file",()=>`${un}, holding only the brief`)).trim();o||d(`${a} is empty`);const c=s.plan?(await us(e,t)).markdown:"",l=`Brief for ${n} round ${(c.match(new RegExp(`^## Brief for ${n} round \\d+`,"gm"))||[]).length+1}`;await Oe(e,t,l,o);const{source:f,itemFile:p,item:w}=await x(e,t),u=new Date().toISOString();return(w.briefs||[]).some(h=>h.kind===n&&!h.brief)||Et(w,n,{},u),w.briefs=w.briefs.map(h=>h.kind===n&&!h.brief?{...h,brief:{heading:l,text:o,at:u}}:h),n!==X&&w.phase!==wa[n]&&(w.phase=wa[n],delete w.staged_at),await y(f.file,p),{heading:l,lines:[`${i}: "${l}" added to its notes; the board launches its ${n===X?"visual agent":`${n} planner`} from it`]}}r(Wd,"writeBrief");async function Gi(e,t,n){const{source:a,itemFile:i,item:s}=await x(e,t),o=(s.briefs||[]).find(c=>c.kind===n&&c.brief);return o?(s.briefs=s.briefs.filter(c=>c!==o),s.briefs.length||delete s.briefs,await y(a.file,i),o):null}r(Gi,"takeBrief");const Qi=r(async(e,t,n)=>((await x(e,t)).item.briefs||[]).find(a=>a.kind===n&&a.brief)??null,"briefOn"),Vi=r(async(e,t,n)=>d(`${await H(e,t)} has no ${n} brief; the orchestrator writes it with item brief ${t} --kind ${n} --file <draft.md>`),"noBrief");async function ic(e,t){const n=await x(e,t).catch(()=>null);!n||!["ready","staging"].includes(n.item.phase)||we(n.item)||(n.item.phase="grilling",delete n.item.staged_at,await y(n.source.file,n.itemFile),await W(e,t,"new-question"))}r(ic,"grillAgain");function Zi(e){return e.filter(t=>t.state==="answered").map(t=>t.id)}r(Zi,"answeredOnClaude");function Ye(e,t,n,a=new Date().toISOString()){for(const i of e.items||[])t.includes(i.id)&&Object.assign(i,{resolved:n,resolved_at:a})}r(Ye,"setResolved");async function pt(e,t,n,a){return Ye(t,n,a),await y(L(e),t),L(e)}r(pt,"markResolved");async function Hd(e,t,n){P(e);const a=await g(L(e),{topics:[],items:[]}),i=(a.items||[]).find(u=>u.id===t);i||d(`no question "${t}" in owner.json`);const{answers:s,requests:o,reopens:c}=await mt(e),l=Ue(i,s,o,[],c);if(l.state==="resolved")return L(e);l.state==="requested"&&d(`${t} waits on the prototype the owner asked for; question add with round_of ${t} resolves it`);const f=v(n,"--note");Ce(i.url)&&sc(a,i,s);const p=await pt(e,a,[t],f);await ns(e,i.topic);const w=la(i.topic);return w&&(await x(e,w).catch(()=>null))?.item.phase==="grilling"&&await ca(e,w),p}r(Hd,"resolveQuestion");async function Ud(e,t,n,a=[]){P(e);const[i,...s]=[...new Set(t)];i||d("question move takes the ids of the questions to merge"),await x(e,n);const o=[n,...a];new Set(o).size!==o.length&&d("question move names a card twice");for(const u of a)await x(e,u);const c=await g(L(e),{topics:[],items:[]}),{answers:l,requests:f,reopens:p}=await mt(e);for(const u of[i,...s]){const h=(c.items||[]).find(b=>b.id===u)||d(`no question "${u}" in owner.json`),{state:m}=Ue(h,l,f,[],p);m!=="open"&&d(`${u} is ${m}; question move takes open questions`)}for(const[u,h]of a.entries()){const{item:m}=await x(e,h),b=[...m.deps||[],o[u]].join(",");await Ae(e,h,{deps:b,relate:n})}const w=await H(e,n);return Object.assign(c.items.find(u=>u.id===i),{topic:`item:${n}`}),Ye(c,s,`Duplicate of ${w}'s question ${i}`),await y(L(e),c),L(e)}r(Ud,"moveQuestions");async function Jd(e,t){const n=t.filter(({choice:i,note:s})=>!i&&String(s||"").trim()).map(i=>i.id),a=n.length?await g(L(e),{topics:[],items:[]}):null;a?.items?.some(i=>n.includes(i.id))&&(Ye(a,n,"Answered with a note"),await y(L(e),a))}r(Jd,"resolveNoteAnswers");function sc(e,t,n){const a=In(t),i=e.items.filter(s=>s.topic===t.topic&&In(s)===a).map(s=>Ue(s,n));mo(i,a)||d(`${t.id} is in prototype round ${a}, which no pick has settled yet; a pick settles it`)}r(sc,"roundSettled");async function mt(e){const t=await K($.join(e,"data","events.jsonl"));return{answers:await Promise.all(t.filter(a=>a.kind==="owner-answer").map(async a=>({...a,reply:a.reply||(await Un($.join(e,"data","replies"),a.id)).length>0}))),requests:t.filter(a=>a.kind==="prototype-request"),reopens:t.filter(a=>a.kind===uo)}}r(mt,"ownerEvents");async function Kd(e,t){const{asked:n}=await Yi(e),a=new Map(n.map(i=>[i.id,i.state]));return t.filter(i=>!a.has(String(i.id))||["open","requested"].includes(a.get(String(i.id))))}r(Kd,"unrecorded");async function Yi(e){const t=await g(L(e),{topics:[],items:[]}),{answers:n,requests:a,reopens:i}=await mt(e);return{owner:t,asked:(t.items||[]).map(s=>Ue(s,n,a,[],i))}}r(Yi,"topicQuestionsAll");async function Xi(e,t){const n=await g(L(e),{topics:[],items:[]}),{answers:a,requests:i,reopens:s}=await mt(e),o=(n.items||[]).filter(c=>c.topic===t).map(c=>Ue(c,a,i,[],s));return{owner:n,asked:o}}r(Xi,"topicQuestions");const ue=r((e,t)=>Xi(e,`item:${t}`),"cardQuestions"),wn=r((e,t,n)=>es(e,`item:${t}`,n),"resolveAnswered");async function es(e,t,n){if(!e)return[];const{owner:a,asked:i}=await Xi(e,t),s=Zi(i);return s.length&&await pt(e,a,s,n),await ns(e,t),s.map(o=>`resolved: ${o}`)}r(es,"resolveAnsweredOn");const pn=r(async(e,t)=>!!e&&(await ue(e,t)).asked.some(n=>n.state==="open"),"hasOpenQuestion");function ts(e,t,n){e.next=v(t,"--next"),e.next_at=new Date().toISOString(),n?e.next_on_questions=!0:delete e.next_on_questions}r(ts,"setNext");async function ns(e,t){const n=t.startsWith("item:")&&!t.startsWith("item:ask:")?t.slice(5):null;if(!n)return;let a;try{a=await x(e,n)}catch(c){if(c instanceof he)return;throw c}const{source:i,itemFile:s,item:o}=a;o.next_on_questions&&((await ue(e,n)).asked.some(c=>c.state!=="resolved")||(delete o.next,delete o.next_at,delete o.next_on_questions,await y(i.file,s)))}r(ns,"clearWaitedNext");async function oc(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);i.next===void 0&&i.next_on_questions===void 0||(delete i.next,delete i.next_at,delete i.next_on_questions,await y(n.file,a))}r(oc,"clearNext");async function as(e,t,n,a){if(!e)return async()=>[];const{owner:i,asked:s}=await ue(e,t),o=s.filter(l=>l.state==="open").map(l=>l.id);o.length&&!a&&d(`${n}: ${t} has ${o.length===1?"an open question":"open questions"} ${o.join(", ")} with no answer yet; pass --force to close the card and resolve ${o.length===1?"it":"them"} too`);const c=s.filter(l=>l.state!=="resolved").map(l=>l.id);return async()=>(c.length&&await pt(e,i,c,"Closed with the card"),c.map(l=>`resolved: ${l}`))}r(as,"closeQuestions");async function zd(e,t,n,a,i){await x(e,t),en(n),se(a,s=>v(s,"--owner-step")),se(i,s=>$s(s)),await as(P(e),t,"land",!1)}r(zd,"checkClose");async function pa(e,t){const n=await g(L(e),{items:[]}),{answers:a,requests:i,reopens:s}=await mt(e);return fo((n.items||[]).map(o=>Ue(o,a,i,[],s)),t)}r(pa,"unpickedPrototype");const rc=r(e=>e.length>120?`${e.slice(0,119)}\u2026`:e,"clip"),cc={ask:"text","ask-edited":"text","item-message":"text","owner-answer":"note"};async function is(e,t,n){const a=v(t,n).replace(/\s+/g," ");return(await K($.join(e,"data","events.jsonl"))).map(s=>String(s.data?.[cc[s.kind]]??"").replace(/\s+/g," ").toLowerCase()).some(s=>s.includes(a.toLowerCase()))||d(`${n} must quote the owner's words in an ask, card message or answer note on this board`),a}r(is,"ownerQuote");async function Gd(e,t,n){return P(e),n===void 0&&d(`only the owner's Serve / Build and the hub's Auto-build start a card run; pass --asked "<the owner's words>" quoting where the owner asked for ${t}'s run`),`Running at the owner's ask: "${await is(e,n,"--asked")}"`}r(Gd,"askedGate");async function Bt(e,t,n="--from-ask"){return _e(t,n),(await K($.join(P(e),"data","events.jsonl"))).some(i=>i.id===t&&i.kind==="ask")?t:d(`no ask "${t}" in this site's data/events.jsonl`)}r(Bt,"checkAsk");function Lt(e,t,n=new Date().toISOString(),a=ss(e,t)){if(!We[t])throw new Error(`no lane "${t}"`);const i=e.lane;return os(e,t,a),e.lane=t,e.lane_at=n,t==="done"?(e.closed_at=n,delete e.reopened_at):i==="done"&&(e.reopened_at=n),i}r(Lt,"placeIn");function ss(e,t,n=!1){return t==="todo"?we(e)?"ready":e.phase??null:t==="progress"?"reading":t==="done"?n||Be(e).some(a=>a.merged_at)?"merged":"closed":null}r(ss,"movedPhase");function os(e,t,n){if(n!==null&&!En[t].includes(n))throw new Error(`no ${We[t]} phase "${n}"`);n===null?delete e.phase:e.phase=n}r(os,"setPhase");async function Qd(e,t,n,a,i,s){const{source:o}=await x(e,t),{id:c}=F(t),l=new Date().toISOString();if(!await st(o.file,async()=>{const w=JSON.parse(await O.readFile(o.file,"utf8")),u=w.items.find(m=>m.id===c)||d(`item file "${o.slug}" has no item "${c}"`);if(s&&((u.lane??null)!==s.lane||(u.phase??null)!==s.phase)||u.lane===n&&(i===void 0||(u.phase??null)===i))return!1;let h;return u.lane===n?os(u,n,i):h=Lt(u,n,l,i),await Je(o.file,JSON.stringify(w,null,2)+`
|
|
25
|
+
`),h!==void 0}))return null;const p={ts:l,who:"claude",kind:Ha,text:`Moved to ${We[n]}: ${v(a,"why")}`};return await de(Z(e,t),p),p.text}r(Qd,"moveCard");const z=r(e=>e?.lane==="done"||!!e?.reopened_at,"closed"),we=r(e=>!!e?.lane&&e.lane!=="todo","leftTodo"),Vd=r(async(e,t)=>z((await x(e,t)).item),"isFinished"),rs=["ready","discussing","grilling","prototyping"],ma="ready, discussing, grilling or prototyping",lc=["grilling","prototyping"];function cs(e,t){t!==void 0&&d(`--state is now --phase: pass --phase ${rs.includes(t)?t:ma}`),e==="staging"&&d(`--phase staging is set by the board, on an owner action; pass ${ma}`),e!==void 0&&!rs.includes(e)&&d(`--phase takes ${ma}`)}r(cs,"checkPhase");const dc="a card goes ready only from staging, which the board sets on the owner's Send, Implement or Reply";async function ls(e,t){return(await K($.join(P(e),"data","events.jsonl"))).some(a=>a.kind==="ask"&&a.id===t&&a.data?.quick===!0||a.kind==="item-message"&&a.data?.key===`ask:${t}`&&a.data?.intent==="build")}r(ls,"quickAsk");const fc=/^mark (?:it |this |the card )?ready\b/i;function uc(e,t,n){const a=[].concat(t).map(o=>Mn(o)||d(`--pr takes a PR number or owner/repo#<n>, not "${o}"`)),i=r((o,c)=>o.number===c.number&&(o.repo||n||null)===(c.repo||n||null),"same"),s=Be(e);for(const o of a)s.some(c=>i(c,o))||s.push(o);delete e.pr,e.prs=s}r(uc,"addPRs");const G=r((e,t)=>String(t).includes("/")?String(t):`${e}/${t}`,"depKey"),Xe=/^wave:(.+)$/;async function ht(e,t,n){const a=await Promise.all((n||[]).map(async i=>{const s=String(i).match(Xe)?.[1];if(!s)return[G(t,i)];const{source:o,wave:c}=await $t(e,s);return He(c).map(l=>`${o.slug}/${l}`)}));return[...new Set(a.flat())]}r(ht,"depCards");async function Te(e,t,n){const a=await ht(e,t,n.deps),i=await Promise.all(a.map(s=>x(e,s).then(({item:o})=>o,()=>null)));return a.filter((s,o)=>i[o]&&!z(i[o]))}r(Te,"unfinishedDeps");async function wc(e,t,n){const a=await Promise.all((await Te(e,F(t).file,n)).map(i=>H(e,i)));return n.blocked_until?[...a,n.blocked_until]:a}r(wc,"blockerNames");async function mn(e,t,n){n??=(await x(e,t)).item;const a=await Promise.all((await Te(e,F(t).file,n)).map(i=>H(e,i)));return[a.length&&`by ${a.join(", ")}`,n.blocked_until&&`until ${n.blocked_until}`].filter(Boolean).join(" and ")}r(mn,"blockWords");const pc=r((e,t)=>!xe(e)&&!e.pushed&&!e.merge_ready&&!z(e)&&(!!Bn(t)||t.some(n=>n?.started&&!n.done&&at(n))),"inProgress");async function mc(e,t){const n=[];for(const a of await q(e)){const i=await g(a.file,null);for(const s of Array.isArray(i?.items)?i.items:[])z(s)||s.blocked_until||!(await ht(e,a.slug,s.deps)).includes(t)||(await Te(e,a.slug,s)).length||n.push(`${a.slug}/${s.id}`)}return n}r(mc,"unblockedBy");async function hc(e,t,n){const a=F(t).file,i=(await ga(e,a,n)).map(c=>Xe.test(c)?c:G(a,c));i.length||d("--split-into takes wave:<slug> or the cards the split filed, <id>,<file>/<id>"),(await ht(e,a,i)).includes(t)&&d(`${t} can't be split into itself`);const s=await Promise.all(i.map(async c=>{const l=c.match(Xe)?.[1];if(!l)return H(e,c);const{source:f,wave:p}=await $t(e,l),w=await Promise.all(He(p).map(u=>H(e,`${f.slug}/${u}`)));return`the wave ${p.title} (${w.join(", ")})`})),o=[];for(const c of await q(e))for(const l of(await g(c.file,null))?.items||[]){const f=`${c.slug}/${l.id}`;if(!(z(l)||!(l.deps||[]).some(p=>G(c.slug,p)===t))){for(const p of i)(await ht(e,a,[p])).includes(f)&&d(Xe.test(p)?`${f} waits on ${t} and is in the wave "${p.match(Xe)[1]}", so it would wait on itself; split into the new cards by key`:`${f} waits on ${t} and is one of the cards it splits into, so it would wait on itself`);o.push(f)}}return{entries:i,words:s.join(", "),waiters:o}}r(hc,"splitPlan");async function gc(e,t,{entries:n,words:a,waiters:i}){for(const o of await q(e)){const c=i.filter(f=>F(f).file===o.slug);if(!c.length)continue;const l=await g(o.file,null);for(const f of l.items.filter(p=>c.includes(`${o.slug}/${p.id}`))){const p=n.map(w=>!Xe.test(w)&&F(w).file===o.slug?F(w).id:w);f.deps=[...new Set([...f.deps.filter(w=>G(o.slug,w)!==t),...p])]}await y(o.file,l)}const s=await H(e,t);for(const o of i)await W(e,o,"split-moved",{card:s,to:a})}r(gc,"moveWaiters");async function ha(e,t,n){const{source:a,itemFile:i,item:s}=await x(e,t);if(we(s))s.unblocked_at=n;else if(s.phase==="ready")s.recheck_at=n;else return;await y(a.file,i)}r(ha,"markUnblocked");async function ds(e,t,n,a){const i=new Set(a.match(/#?[\w-]+(?:\.[\w-]+)*(?:\/[\w-]+(?:\.[\w-]+)*)*/g)||[]),s=new Set(await ht(e,F(t).file,n.deps));for(const o of await q(e)){const c=await g(o.file,null);for(const l of c?.items||[]){const f=`${o.slug}/${l.id}`;if(f===t||s.has(f)||z(l))continue;[l.id,it(c,l),l.ticket].filter(Boolean).flatMap(w=>[w,`${o.slug}/${w}`]).some(w=>i.has(w))&&d(`--next names unfinished card ${f}; add it with --deps`)}}}r(ds,"checkNextDeps");const fs=r(e=>`item notes ${e} --heading "<heading>" --file <draft.md>`,"NOTES_COMMAND");function $c(e,t){t.size||d(`${e} has no size; a builder starts from it, so pass --size small or large with --phase ready`),t.plan||d(`${e} has no notes file; a builder starts from it, so add its notes with ${fs(e)} before --phase ready`)}r($c,"checkSizeAndNotes");async function bc(e,t,n,a){$c(t,a);const i=await O.readFile(await hn(n,a.plan),"utf8"),s=(await ue(e,t)).asked.filter(o=>o.answers.length&&!i.includes(o.id)&&!(o.prototype_gate&&!o.answers.at(-1).note)).map(o=>o.id);s.length&&d(`${t}'s notes leave out the answered question${s.length===1?"":"s"} ${s.join(", ")}; record each answer in its notes, naming its id, with ${fs(t)} before it is ready`)}r(bc,"checkNotesComplete");async function hn(e,t,{missingOk:n=!1}={}){const a=r(o=>d(`a notes file is a .md file inside ${e}, named relative to it: "${t}" ${o}`),"bad");(typeof t!="string"||!t.trim())&&a("is empty"),($.isAbsolute(t)||t.split(/[\\/]/).includes(".."))&&a("leaves the folder"),$.extname(t).toLowerCase()!==".md"&&a("is not a .md file");const i=await O.realpath(e),s=await O.realpath($.resolve(e,t)).catch(()=>n?$.resolve(i,t):a("does not exist"));return s.startsWith(i+$.sep)||a("leaves the folder"),s}r(hn,"planPath");async function us(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);if(i.plan)return{itemFile:a,item:i,markdown:await O.readFile(await hn(n.dir,i.plan),"utf8")};if(typeof i.notes=="string"&&i.notes.trim())return{itemFile:a,item:i,markdown:i.notes};d(`${t} has no notes`)}r(us,"readNotes");function ws({install:e,installSha:t,installLabel:n,clearInstall:a}){if(e===void 0)return(t!==void 0||n!==void 0)&&d("--install-sha and --install-label go with --install <url>"),a?null:void 0;a&&d("pass --install or --clear-install, not both");let i;try{i=new URL(String(e).trim())}catch{i=null}(!i||!["https:","itms-services:"].includes(i.protocol))&&d(`--install takes an https:// or itms-services:// URL, not "${e}"`),i.protocol==="itms-services:"&&!(i.searchParams.get("action")==="download-manifest"&&/^https:\/\//.test(i.searchParams.get("url")||""))&&d("--install: an itms-services:// URL needs ?action=download-manifest&url=https://\u2026/manifest.plist");const s=t===void 0?void 0:/^[0-9a-f]{7,40}$/i.test(t)?t.toLowerCase():d(`--install-sha takes a commit sha of 7 to 40 hex digits, not "${t}"`);return{url:String(e).trim(),...n===void 0?{}:{label:v(n,"--install-label")},built_at:new Date().toISOString(),...s?{sha:s}:{}}}r(ws,"installOf");async function Zd(e,{install:t,installSha:n}){const a=ws({install:t,installSha:n});a||d("main set needs --install <url>");const i=$.join(P(e),"data","main.json");return await y(i,{install:a}),{file:i,lines:["main install set"]}}r(Zd,"setMainInstall");const kc=r((e,t)=>e.length?`Merged PR${e.length>1?"s":""} ${Ee(e,t)}.`:"Merged.","mergedLine");function yc(e,t,n,a,i,s){return s?{status:s}:n&&a===void 0?{status:["merged",{words:kc(n,i)}]}:(a===void 0&&e&&d(`--done needs --text "<what was done, or why it closes without landing>", which becomes ${t}'s last thread line`),a===void 0?null:{text:a})}r(yc,"closingLine");function xc(e,t,n){const a=new Date().toISOString(),i=r((s,o)=>s.number===o.number&&(s.repo||n||null)===(o.repo||n||null),"same");e.prs=Be(e).map(s=>{const o=t.find(c=>i(s,c));return o&&!s.merged_at?{...s,merged_at:o.at||a}:s}),delete e.pr}r(xc,"markMerged");async function ps(e,t,{ticket:n,newTicket:a,status:i,clearStatus:s,done:o,splitInto:c,merged:l,landed:f,start:p,handBack:w,next:u,clearNext:h,pr:m,pushed:b,mergeReady:_,ghSince:N,fromAsk:j,phase:A,state:D,held:M,unheld:I,pickUpAt:S,deps:B,blockedUntil:C,blockReason:T,parent:oe,related:R,relate:J,unrelate:te,install:Q,installSha:ke,installLabel:ye,clearInstall:Y,title:Pe,size:Mt,model:bt,force:kt,text:re,closing:ae,who:_n,by:ge,command:Da,ownerStep:tt,ownerDone:Ks,check:yt},Fa){(i!==void 0||s)&&d(`--status${s?" and --clear-status are":" is"} gone: a card's lane is stored and moves itself. Close it with --done --text "<what was done, or why it closes without landing>"`),l&&(o=!0),R!==void 0&&(J!==void 0||te!==void 0)&&d("pass --related, or --relate and --unrelate, not both"),cs(A,D),kt&&!o&&d("--force goes with --done"),c!==void 0&&!o&&d("--split-into goes with --done"),c!==void 0&&re!==void 0&&d("--split-into writes the closing line; drop --text");const xt=c===void 0?null:await hc(e,t,c);xt&&(re=`Split into ${xt.words}`),j!==void 0&&o&&d("--from-ask opens a card; close it in another call"),re!==void 0&&en(re),tt!==void 0&&v(tt,"--owner-step");const _t=yt===void 0||yt===""?yt:$s(yt);re!==void 0&&j===void 0&&!o&&A!=="ready"&&d("--text goes with --from-ask, --done, or --phase ready on a staging card"),j!==void 0&&re===void 0&&d(`--from-ask needs --text "<first line>", the item's first thread line after the ask`),u!==void 0&&h&&d("pass --next or --clear-next, not both"),typeof u=="string"&&!u.trim()&&([u,h]=[void 0,!0]);const St=hs({held:M,unheld:I,pickUpAt:S}),Wt=ws({install:Q,installSha:ke,installLabel:ye,clearInstall:Y});[o,p,w,u,h,m,b,_,N,j,A,St,B,C,T,oe,R,J,te,Wt,Pe,Mt,bt,n,a,tt,yt].every(E=>E===void 0||E===!1)&&d("nothing to set: pass --done, --next, --clear-next, --pr, --pushed, --gh-since, --from-ask, --phase, --held, --unheld, --pick-up-at, --deps, --blocked-until, --block-reason, --parent, --related, --relate, --unrelate, --install, --clear-install, --title, --size, --model, --ticket, --new-ticket or --check");const ne=j===void 0?void 0:await Bt(e,j);ne&&await Zn(e,ne),ne&&await _i(e,ne,t);const{source:Me,itemFile:nt,item:k}=await x(e,t),Na=ne??(k.phase?void 0:k.from_ask),zs=Na!==void 0&&await ls(e,Na);A!==void 0&&k.answered_at&&!k.closed_at&&(delete k.answered_at,we(k)&&Lt(k,"todo",void 0,null)),A!==void 0&&we(k)&&d(`--phase sets a Todo card's phase; ${t} is ${We[k.lane]}, where the board sets it`),A==="ready"&&!zs&&!["staging","ready"].includes(k.phase)&&d(`${t} reads ${k.phase||"new"}, not staging; ${dc}`),ne&&k.from_ask&&k.from_ask!==ne&&d(`${t} came from ask "${k.from_ask}", so ask "${ne}" can't be its from-ask; to fold it in: item relate-ask ${ne} ${t} --summary "<line>"`),ne&&St&&A!==void 0&&!k.phase&&d("a card filed held is filed New and takes no --phase; drop --phase, or drop --held to file it in a phase"),ne&&A===void 0&&!k.phase&&!St&&d("--from-ask files a card, which reads New until it has a phase: pass --phase discussing, grilling, prototyping, or ready for a Quick build ask, or --held to file it for later");const Sn=z(k);xt&&Sn&&d(`${t} is already Done, so the cards waiting on it were unblocked then; move them with item set <key> --deps`),tt!==void 0&&Sn&&d(`${t} is already Done, so its ticket closed; an owner step goes on the command that finishes the card: land --owner-step, or builder done before the merge`),tt!==void 0&&(k.owner_step=v(tt,"--owner-step")),Ks&&delete k.owner_step,_t!==void 0&&!k.owner_step&&d(`--check goes with an owner step, which ${t} doesn't have: pass --owner-step "<the step>" with it on builder done or land`),_t&&(k.owner_check={check:_t,since:k.owner_check?.check===_t?k.owner_check.since:new Date().toISOString()}),(_t===""||!k.owner_step)&&delete k.owner_check;const vn=await Promise.all((await V(Me.dir,k.id)).map(E=>g(E,null))),Gs=Ln(k,vn);if(Pe!==void 0&&(k.title=v(Pe,"--title")),ms(k,{size:Mt,model:bt}),B!==void 0){const E=await ga(e,F(t).file,B,t);E.length?k.deps=E:delete k.deps}const Ra=C===""?k.blocked_until:void 0;if(C!==void 0&&(C===""?delete k.blocked_until:k.blocked_until=v(C,"--blocked-until")),T&&(!k.deps?.length&&!k.blocked_until&&d("--block-reason goes with a block: --deps or --blocked-until"),k.block_reason=v(T,"--block-reason")),(T===""||!k.deps?.length&&!k.blocked_until)&&delete k.block_reason,oe!==void 0){const E=await $a(e,F(t).file,t,oe);E?k.parent=E:delete k.parent}const Ea=oe!==void 0||R!==void 0||J!==void 0||te!==void 0?await ba(e,t):null;let jn=R===void 0?null:await vc(e,t,k,R);J!==void 0&&(k.related=[...new Set([...(k.related||[]).map(E=>G(F(t).file,E)),...await $n(e,t,J,"--relate")])]),te!==void 0&&(jn=await jc(e,t,k,await $n(e,t,te,"--unrelate"))),St!==void 0&&gn(k,St),p&&k.held&&d(k.pick_up_at?`${t} is held until ${k.pick_up_at}, when the board resumes it; start it early only with item set ${t} --unheld`:`${t} is held; start it only once the owner resumes it, with item set ${t} --unheld`);const vt=m!==void 0||b!==void 0||_!==void 0;if((p||o||w||vt)&&(Sc(k),delete k.recheck_at),p){const E=await Te(e,F(t).file,k);E.length&&d(`${t} is blocked by ${E.join(", ")}; start it once ${E.length===1?"that is":"those are"} done`),k.blocked_until&&d(`${t} is blocked until ${k.blocked_until}; start it once that holds and the block is cleared with --blocked-until ""`)}const Ht=p&&e?await pa(e,t):null;Ht&&d(`${t} has no take picked: the owner's answer to ${Ht.id} picks none${Ht.note?` ("${rc(Ht.note)}")`:""}, so it prototypes again: draw the next round from that note, or grill when the note asks a question, and build once a take is picked`);const Qs=o?await as(e,t,"--done",kt):async()=>[],jt=o?yc(e,t,l,re,pe(e,nt),ae):null;if(l&&xc(k,l,pe(e,nt)),u!==void 0&&(ts(k,u,await pn(e,t)),await ds(e,t,k,k.next)),h&&(delete k.next,delete k.next_at,delete k.next_on_questions),p&&u===void 0&&k.next_on_questions&&(delete k.next,delete k.next_at,delete k.next_on_questions),vt&&!k.review_since&&(k.review_since=new Date().toISOString()),m!==void 0&&uc(k,m,pe(e,nt)),b!==void 0){be().find(E=>Re(E.dir,P(e)))?.remote||d("--pushed is for a board whose repo is a plain git remote; on GitHub, open a PR and pass --pr"),xe(k)&&d(`${t} has a PR; its PR says when it merges`);try{Bo(b)}catch{d("--pushed takes the name of the branch you pushed")}k.pushed={branch:b,at:new Date().toISOString()},k.branch=b}if(_!==void 0&&(k.merge_ready={branch:_,at:new Date().toISOString()},k.branch=_),o&&delete k.merge_ready,N!==void 0&&((typeof N!="string"||!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(N)||Number.isNaN(Date.parse(N)))&&d("--gh-since takes an ISO-8601 UTC time such as 2026-10-23T11:00:00Z"),k.gh_since=N),ne&&(k.from_ask=ne),A==="ready"&&e){const E=(await ue(e,t)).asked.filter(Fe=>Fe.state==="open").map(Fe=>Fe.id);E.length&&d(`${t} has ${E.length===1?"an open question":"open questions"} ${E.join(", ")}; it is ready once the owner has answered ${E.length===1?"it":"them"}`),await bc(e,t,Me.dir,k)}const On=A==="ready"&&k.phase==="staging"&&j===void 0;On&&re===void 0&&d(`${t} goes ready from staging on your judgment: pass --text "<what you checked it against on the board>", which goes into its notes`);const An=A==="ready"&&k.phase==="ready"&&!!k.recheck_at&&j===void 0;An&&re===void 0&&d(`${t}'s block ended, so it builds once you re-check its notes against what landed: pass --text "<what you re-checked>", which goes into its notes`),A==="ready"&&!On&&!An&&re!==void 0&&j===void 0&&!o&&d(`${t} reads ${k.phase||"no phase"}, not staging, so there is no judgment for --text to record`);const Vs=A==="ready"&&k.phase!=="ready";A!==void 0&&(lc.includes(A)&&["discussing","staging","ready"].includes(k.phase)&&Et(k,A==="grilling"?"grill":"prototype"),k.phase=A,delete k.recheck_at,delete k.staged_at,delete k.unfiled,delete k.filed_held),k.phase==="ready"&&fc.test(k.next??"")&&d(`${t} would read ready with Next "${k.next}", which asks for the phase it is already in; pass --next with what happens next, or --clear-next`),Wt&&(k.install=Wt),Wt===null&&delete k.install,!Gs&&!p&&Ln(k,vn)&&d(`that leaves ${t} In Progress with nothing building and no Next; pass --next "<what happens next>"`);const Pn=(B!==void 0||C)&&!o&&pc(k,vn)?await mn(e,t,k):"",Ba=pe(e,nt);let Dn=null;const Fn=r((E,Fe)=>{k.lane!==E&&Lt(k,E,void 0,ss(k,E,f))!==void 0&&Fe&&(Dn=`Moved to ${We[E]}: ${Fe}`)},"moveTo");o?Fn("done",null):vt&&k.lane!=="done"?Fn("review",m!==void 0?`PR ${Ee([].concat(m).map(Mn),Ba)} opened`:b!==void 0?`${b} pushed for its merge`:"verified, it waits for your Merge"):p&&!z(k)&&Fn("progress","a build started");const La=A==="ready"&&!k.ticket&&n===void 0&&!a&&!!(e&&await gt(e));if(La&&(a=!0),a&&k.ticket&&d(`${t} already has ticket ${k.ticket}`),(n!==void 0||a)&&await ys(e,nt,k,n,Fa,Me.dir),await y(Me.file,nt),La&&await W(e,t,"ticket-made",{ticket:k.ticket}),Dn&&await de(Z(e,t),{ts:k.lane_at,who:"claude",kind:Ha,text:Dn}),On&&await Oe(e,t,`Ready (orchestrator, ${Ze()})`,re),An&&await Oe(e,t,`Re-checked (orchestrator, ${Ze()})`,re),Vs&&await Le(e,t,{command:"readied"}),Pn){const E=e&&!await Ws(e,t)&&await Ms(e,t,{cause:"blocked",words:`blocked ${Pn}`});await W(e,t,E?"blocked-pausing":"blocked-stays",{blocked:Pn})}jn&&await jn(),Ea&&await ks(e,t,Ea,await ba(e,t));const De=await Qs();if(A==="ready"&&e&&De.push(...await wn(e,t,"Recorded in the card's notes")),p&&De.push(...await wn(e,t,"Acted on: build started")),ne&&De.push(...(await Ge(e,t,{text:re})).lines),jt&&e){const E=Da?await Le(e,t,{command:Da,text:jt.text,who:_n,by:ge}):jt.status?await W(e,t,...jt.status):await Ge(e,t,{text:jt.text});De.push(...E.lines)}const Ia=k.owner_step&&o?null:Po({start:p,done:o,handover:vt}),Ca=Ia&&await Do(await gt(e),k,Ia,{...Fa,repo:Ba});Ca&&De.push(Ca),(vt||o||w)&&(await Us(Me.dir,k.id,o,w?{handedBack:new Date().toISOString()}:{endHandBack:!0}),await wt(e,t));const Nn=ne?await ve(e):{};if(ce(Nn,ne)!==void 0&&(delete Nn[ne],await y(Se(e),{links:Nn})),Ra&&!z(k)&&!(await Te(e,F(t).file,k)).length){const{line:E}=await W(e,t,"unblocked",{lifted:Ra});await ha(e,t,E.ts),await $e({dir:e},"card-unblocked",{key:t}),De.unshift(`unblocked: ${t}`)}if(!o||Sn)return{file:Me.file,lines:De};xt&&await gc(e,t,xt);const Ta=await mc(e,t),Zs=await H(e,t);for(const E of Ta){const{line:Fe}=await W(e,E,"unblocked-done",{card:Zs});await ha(e,E,Fe.ts),await $e({dir:e},"card-unblocked",{key:E})}return{file:Me.file,lines:[...Ta.map(E=>`unblocked: ${E}`),...De]}}r(ps,"changeItem");function ms(e,{size:t,model:n}){for(const[a,i,s]of[["size",t,["small","large"]],["model",n,["opus","sonnet"]]])i!==void 0&&(i===""?delete e[a]:e[a]=s.includes(i)?i:d(`--${a} takes ${s.join(" or ")}, or "" to clear it`))}r(ms,"setBuildChoice");function hs({held:e,unheld:t,pickUpAt:n}){return[e,t,n!==void 0].filter(Boolean).length>1&&d("pass one of --held, --unheld and --pick-up-at"),n!==void 0?n===""?null:{until:_c(n)}:e?{until:null}:t?null:void 0}r(hs,"holdOf");function _c(e){const t=typeof e=="string"&&Oo.test(e)?Date.parse(e):NaN;return Number.isNaN(t)&&d('--pick-up-at takes an ISO time such as 2026-11-04T09:00, the host\u2019s local time unless it names an offset, or "" to resume the card'),t<=Date.now()&&d(`--pick-up-at ${e} has passed; pass a later time, or --unheld to resume the card now`),new Date(t).toISOString()}r(_c,"pickUpTime");function Sc(e){delete e.held,delete e.pick_up_at}r(Sc,"clearHold");function gn(e,t){t?e.held=!0:delete e.held,t?.until?e.pick_up_at=t.until:delete e.pick_up_at,e.held_at=new Date().toISOString()}r(gn,"setHold");async function Yd(e,t,n,{announce:a=!0}={}){const{source:i,itemFile:s,item:o}=await x(e,t);if(a){const c=new Date(n).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});await W(e,t,"hold-ended",{at:c}),!o.blocked_until&&!(await Te(e,F(t).file,o)).length&&await $e({dir:e},"card-unblocked",{key:t,until:n})}gn(o,null),await y(i.file,s)}r(Yd,"pickUp");async function gs(e,t,n){const a=String(n?.key||""),{file:i,id:s}=F(a),{source:o,itemFile:c}=await Ke(e,i),l=n.shared_by?n.shared_by.name||n.shared_by.login:void 0,f={text:Jt.done,who:"love",by:l,command:"done"},p=c.items.find(m=>m.id===s)||d(`item file "${i}" has no item "${s}"`);if(!(t==="card-done"&&!!p.owner_step)&&p.lane==="done")return null;if(t==="card-done")return Ae(e,a,{done:!0,force:!0,ownerDone:!0,...f});const u=n.hold?typeof n.until=="string"?new Date(n.until).toISOString():null:void 0;gn(p,n.hold?{until:u}:null),await y(o.file,c),n.hold&&(await Ws(e,a)||await Ms(e,a,{cause:"held",words:"held"}),await Us(o.dir,s,!1,{held:!0}),await wt(e,a));const h=u&&new Date(u).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});return Le(e,a,n.hold?{text:h?`${Jt.hold} until ${h}`:Jt.hold,who:"love",by:l,command:"hold"}:{who:"love",by:l,command:"resume"})}r(gs,"recordOwnerTap");function $s(e){try{return tr(er(e))}catch(t){return d(t.message)}}r($s,"checkSpec");async function Xd(e,t,n,a){const{item:i}=await x(e,t);return!i.owner_step||i.owner_check?.check!==n?null:Ae(e,t,{done:!0,force:!0,ownerDone:!0,closing:["check-passed",{found:a}]})}r(Xd,"passOwnerCheck");async function ef(e,t,n,a){const{source:i,itemFile:s,item:o}=await x(e,t);return!o.owner_step||o.owner_check?.check!==n?!1:(o.owner_check.broken=a,await y(i.file,s),await W(e,t,"check-broken",{error:a}),await $e({dir:e},"owner-check-broken",{key:t,check:n,error:a}),!0)}r(ef,"breakOwnerCheck");async function ga(e,t,n,a){const i=[...new Set(String(n).split(",").map(s=>s.trim()).filter(Boolean))];for(const s of i){const o=s.match(Xe)?.[1];if(!o){await x(e,G(t,s));continue}(await ht(e,t,[s])).includes(a)&&d(`${a} is in the wave "${o}", so it would wait on itself`)}return i}r(ga,"depList");async function $a(e,t,n,a){if(!String(a).trim())return null;const i=G(t,String(a).trim());i===n&&d("--parent: a card can't be its own Parent");for(let s=i;s;){const o=s===i?await x(e,s):await x(e,s).catch(l=>{if(l instanceof he)return null;throw l});if(!o)break;const{item:c}=o;c.parent&&G(F(s).file,c.parent)===n&&d(`--parent: ${i} is a child of ${n}, so it can't be its Parent`),s=c.parent?G(F(s).file,c.parent):null}return i}r($a,"parentKey");async function $n(e,t,n,a){const i=t.split("/")[0],s=[...new Set(String(n).split(",").map(o=>o.trim()).filter(Boolean).map(o=>G(i,o)))];s.includes(t)&&d(`${a}: a card can't be related to itself`);for(const o of s)await x(e,o);return s}r($n,"relatedKeys");const bs=r((e,t,n)=>async()=>{for(const a of await q(e)){const i=await g(a.file,null);let s=!1;for(const o of Array.isArray(i?.items)?i.items:[]){const c=`${a.slug}/${o.id}`;c===t||!n(c)||!(o.related||[]).some(l=>G(a.slug,l)===t)||(o.related=o.related.filter(l=>G(a.slug,l)!==t),o.related.length||delete o.related,s=!0)}s&&await y(a.file,i)}},"dropLinksTo");async function ba(e,t){const n=new Set;let a=null;for(const i of await q(e))for(const s of(await g(i.file,null))?.items||[]){const o=`${i.slug}/${s.id}`,c=(s.related||[]).map(l=>G(i.slug,l));o===t?(c.forEach(l=>n.add(l)),s.parent&&(a=G(i.slug,s.parent))):c.includes(t)&&n.add(o)}return{related:n,parent:a}}r(ba,"linksOf");async function ks(e,t,n,a){const i=r(async(s,o,c=s,l=s)=>{await Le(e,t,{command:c,card:o}),await Le(e,o,{command:l,card:t})},"both");for(const s of a.related)n.related.has(s)||await i("related",s);for(const s of n.related)a.related.has(s)||await i("unrelated",s);a.parent&&a.parent!==n.parent&&await i("followup-of",a.parent,"followup-of","followup")}r(ks,"postLinkLines");async function vc(e,t,n,a){const i=await $n(e,t,a,"--related");return i.length?n.related=i:delete n.related,bs(e,t,s=>!i.includes(s))}r(vc,"setRelated");function jc(e,t,n,a){const i=(n.related||[]).filter(s=>!a.includes(G(F(t).file,s)));return i.length?n.related=i:delete n.related,bs(e,t,s=>a.includes(s))}r(jc,"unrelateCards");const pe=r((e,t)=>e&&be().find(n=>Re(n.dir,e))?.github||t.repo,"boardRepoOf");async function tf(e,t){const{itemFile:n,item:a}=await x(e,t);xe(a)||d(`${t} has no PR`);const i=Pt(a,pe(e,n));i.length||d(`${t} has no repo`);const s=a.gh_since||"1970-01-01T00:00:00Z";try{const o=je(await ie(e)).github_login||await Mo();return await Promise.all(i.map(async({repo:c,number:l})=>({repo:c,...ni(await ti({repo:c,pr:l,since:s}),{pr:l,since:s,me:o})})))}catch(o){d(o.message)}}r(tf,"prReport");async function nf(e,t,{replyTo:n,body:a}){const i=n===void 0?"pr comment":"pr reply",s=Mn(t)||d(`${i} takes a PR number or owner/repo#<n>`);n!==void 0&&!/^[1-9]\d*$/.test(n)&&d("pr reply takes the id of the review comment it answers");const o=v(a,"--body").trim();(o.startsWith(At.trim())||o.includes(qn))&&d(`${i} adds the ${At.trim()} prefix and the claude-bridge marker itself; pass only your words`);const c=s.repo||pe(P(e),{})||d(`${i} needs a board on GitHub; this board has no repo there`);try{const l=await Wo({repo:c,pr:s.number,replyTo:n,words:o,login:je(await ie(e)).github_login});return{lines:[`posted on PR ${s.repo?t:`#${t}`}: ${l}`]}}catch(l){d(l.message)}}r(nf,"postPR");const gt=r(async e=>Ii((await ie(e)).tickets),"boardSource");async function af(e,t,{afterApproval:n,confirmed:a,tapped:i}={}){P(e);const{itemFile:s,item:o}=await x(e,t);xe(o)||d(`${t} has no PR`),z(o)&&d(`${t} is done`);const c=await mn(e,t,o);c&&(i&&await W(e,t,"merge-blocked",{blocked:c}),d(`Not merged: blocked ${c}.`));const l=pe(e,s),f=Pt(o,l);f.length||d(`${t} has no repo`);const p=new Date().toISOString(),w=r(S=>W(e,t,"gh-limited",{what:"Not merged",words:ao(S)}),"limited");let u;try{u=await Promise.all(f.map(S=>ti({repo:S.repo,pr:S.number,comments:!1})))}catch(S){S.limited&&await w(S.limited),d(S.message)}const h=u.map(({view:S})=>S);if(Jo(h)){const S=f.map((B,C)=>({...B,at:h[C].mergedAt})).filter((B,C)=>h[C].state==="MERGED");return Ae(e,t,{merged:S,closing:["already-merged",{prs:Ee(S,l),many:S.length>1}]})}const m=f.map((S,B)=>({...S,...u[B]})).filter(({view:S})=>S.state==="OPEN"),b=zt(s,(await ie(e)).reviewers||[]).length>0,_=[];let N=!1,j=!1;for(const S of m){const{view:B,reviews:C}=S,{failing:T,pending:oe}=ni(S,{pr:S.number,since:p,me:null}),R=lo({...B,checks:{state:T.length?"fail":oe?"pending":"pass",failing:T,pending:oe}},{needsApproval:b}),J=C.filter(Q=>Q.state==="APPROVED").sort((Q,ke)=>String(Q.submitted_at).localeCompare(String(ke.submitted_at))).at(-1)?.commit_id;let te=!!(J&&B.headRefOid&&J!==B.headRefOid);te&&n===void 0&&Wc(o,J,B.headRefOid)&&(te=!1,j=!0),N||=te,te&&n===void 0&&R.push('commits after the approval: pass --after-approval "<why they are trivial>" when they are, else ask the approver to look again'),_.push(...f.length>1?R.map(Q=>`${Ee([S],l)} ${Q}`):R)}m.length||_.push(f.length>1?"every PR is closed":"PR is closed"),n!==void 0&&!N&&d("--after-approval: no commit came after the approval; drop it");const A=o.manual_steps?.length?a===void 0?null:await is(e,a,"--confirmed"):void 0;if(a!==void 0&&A===void 0&&d(`--confirmed: ${t} has no manual steps; drop it`),A===null&&_.push(`manual steps: merge only on the owner's word, with --confirmed "<their words>"`),_.length){const S=`Not merged: ${_.join("; ")}.`;i&&await W(e,t,"merge-refused",{why:_.join("; ")}),d(S)}const D=[];for(const S of m){const B=await qa(["pr","view",String(S.number),"--repo",S.repo,"--json","title,body,commits"]);B.limited&&await w(B.limited),B.status!==0&&d(`gh pr view ${Ee([S],l)} failed: ${B.stderr.trim().split(`
|
|
26
|
+
`)[0]||`gh exited ${B.status}`}${D.length?` (merged ${Ee(D,l)} before it)`:""}`);const{title:C,body:T,commits:oe}=JSON.parse(B.stdout),R=["--subject",C,"--body",io(await H(e,t),T,oe)],J=await qa(["pr","merge",String(S.number),"--repo",S.repo,"--squash",...R],{timeout:12e4});J.limited&&await w(J.limited),J.status!==0&&d(`gh pr merge ${Ee([S],l)} failed: ${J.stderr.trim().split(`
|
|
27
|
+
`)[0]||`gh exited ${J.status}`}${D.length?` (merged ${Ee(D,l)} before it)`:""}`),D.push(S)}const M=[n!==void 0&&`After the approval: ${v(n,"--after-approval")}`,j&&`After the approval: only the board's rebase onto ${o.rebased.at(-1).base}.`,A&&`The owner's word on the manual steps: "${A}"`].filter(Boolean),I=await Ae(e,t,{merged:D});return M.length&&await Ge(e,t,{text:M.join(" ")}),I}r(af,"mergeCard");async function sf(e,t){return(await x(e,t)).item.merge_ready||null}r(sf,"heldMerge");async function of(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);(!i.merge_ready||z(i))&&d(`${t} isn't waiting for its Merge`);const s=await mn(e,t,i);return s&&d(`${t} is blocked ${s}, so its Merge waits until nothing blocks it`),i.merge_ready.tapped=new Date().toISOString(),await y(n.file,a),i.merge_ready}r(of,"tapMerge");async function rf(e,t){const{source:n,item:a}=await x(e,t);return(await Promise.all((await V(n.dir,a.id)).map(s=>g(s,null)))).filter(s=>s?.worktree?.path&&s.kind!==ee).sort((s,o)=>String(o.started).localeCompare(String(s.started)))[0]?.model}r(rf,"builderModel");async function ys(e,t,n,a,i,s,o){const c=await gt(e)||d(Oc);try{o??=a===void 0&&n.plan?await O.readFile(await hn(s,n.plan),"utf8"):n.notes||"";const l=await Ao(c,{ticket:a,title:n.title,body:o,repo:pe(e,t)},i);n.ticket=l.ticket,l.created?n.ticket_created=!0:delete n.ticket_created}catch(l){if(l instanceof he)throw l;d(l.message)}}r(ys,"assignTicket");const Oc="this board has no ticket source, so its cards stay tasks; pick GitHub issues or Linear in Board settings first";async function xs(e){const t=[],n=[];for(const a of await q(e)){const i=await g(a.file,{});t.push(...(i.items||[]).map(s=>s.id)),n.push(Number(i.last_task)||0)}return e&&t.push(...(await qe(e)).map(a=>a.item?.id)),`T-${Math.max(0,...n,...t.map(a=>Number(/^T-([1-9]\d*)$/.exec(String(a))?.[1]||0)))+1}`}r(xs,"nextTaskId");const le=r((e,t)=>st($.join(e||Zt(),"tickets"),t,{heartbeat:!0}),"ticketWrite"),Ae=r((e,t,n,a)=>n.ticket!==void 0||n.newTicket||n.phase==="ready"?le(e,()=>ps(e,t,n,a)):ps(e,t,n,a),"setItem"),Ac=r((e,t,n,a)=>le(e,()=>Pc(e,t,n,a)),"addItem");async function Pc(e,t,{title:n,notes:a,notesFrom:i,deps:s,ticket:o,newTicket:c,pickUpAt:l,held:f,parent:p,splitOf:w,size:u,model:h,phase:m,state:b,relate:_,fromAsk:N,text:j,intake:A=!1},D){const{file:M,id:I}=t.includes("/")?F(t):{file:t};I!==void 0&&_e(I,"the item id"),cs(m,b),j!==void 0&&v(j,"--text"),j!==void 0&&N===void 0&&d("--text goes with --from-ask"),N!==void 0&&j===void 0&&d(`--from-ask needs --text "<first line>", the item's first thread line after the ask`),o!==void 0&&c&&d("pass --ticket <id> to link a ticket or --new-ticket to make one, not both");const S=N===void 0?void 0:await Bt(e,N);S&&await Zn(e,S),S&&await _i(e,S);const B=hs({held:f,pickUpAt:l});B&&m!==void 0&&d("a card filed held is filed New and takes no --phase; drop --phase, or drop --held to file it in a phase"),S&&m===void 0&&!B&&d("--from-ask files a card, which reads New until it has a phase: pass --phase discussing, grilling, prototyping, or ready for a Quick build ask, or --held to file it for later"),m==="ready"&&!(S&&await ls(e,S))&&d(S?`ask "${S}" is not a Quick build ask, so item add files it discussing, grilling or prototyping`:"item add files a card discussing, grilling or prototyping; ready is for a Quick build ask, filed with --from-ask"),m==="ready"&&!u&&d("a new ready card has no size; a builder starts from it, so pass --size small or large with --phase ready"),m==="ready"&&!(a??"").trim()&&i===void 0&&d("a new ready card has no notes; a builder starts from them, so pass --notes or --notes-from with --phase ready"),A&&e&&m===void 0&&!B&&d("item add files a card on a board: pass --phase discussing, grilling or prototyping, or --held to file it for later");const{source:C,itemFile:T}=await Ke(e,M);I!==void 0&&T.items.some(Y=>Y.id===I)&&d(`item file "${M}" already has an item "${I}"`);const oe=new Date().toISOString(),R={id:I,title:v(n,"--title"),created:oe,lane:"todo",lane_at:oe};if(p!==void 0&&(R.parent=await $a(e,M,null,p)||d("--parent takes a card, such as <file>/<id>")),w!==void 0&&p!==void 0&&d("--split-of sets the Parent; drop --parent"),w!==void 0){R.parent=await $a(e,M,null,w)||d("--split-of takes a card, such as <file>/<id>");const{gates:Y}=(await x(e,R.parent)).item;Y&&(R.gates={...Y})}a!==void 0&&i!==void 0&&d("pass --notes or --notes-from, not both");const J=i!==void 0?await fa(e,i,"--notes-from",()=>`${un}, and link that card with --relate`):a===void 0?"":v(a,"--notes");i!==void 0&&!J.trim()&&d(`--notes-from ${i} is empty`);const te=i!==void 0?J:`# ${R.title}
|
|
28
28
|
${J&&`
|
|
29
29
|
${J}
|
|
30
|
-
`}`;
|
|
31
|
-
`),"writePlain"),
|
|
32
|
-
`)}`),await
|
|
30
|
+
`}`;ms(R,{size:u,model:h}),s!==void 0&&(R.deps=await ga(e,M,s)),B&&gn(R,B),B&&m===void 0&&(R.filed_held=!0),_!==void 0&&(R.related=await $n(e,t,_,"--relate")),S&&(R.from_ask=S),m!==void 0&&(R.phase=m);const Q=I===void 0?null:Ga(await gt(e),I);Q&&o!==void 0&&Ga(await gt(e),o)!==Q&&d(`--ticket ${o} is not the ticket ${I} names`);const ke=m==="ready"&&!Q&&o===void 0&&!c&&!!await gt(e);ke&&(c=!0),Q&&c&&d(`${I} names ticket ${Q}, so --new-ticket would make another; drop --new-ticket`),(o!==void 0||c||Q)&&await ys(e,T,R,o??Q??void 0,D,C.dir,J),I===void 0&&R.ticket?(R.id=Fo(R.ticket),T.items.some(Y=>Y.id===R.id)&&d(`item file "${M}" already has an item "${R.id}"; link ${R.ticket} with item add ${M}/<id> --ticket ${R.ticket}`)):I===void 0&&(R.id=await xs(e)),R.plan=dn(C.dir,R.id),await fn($.join(C.dir,R.plan),te);const ye=`${M}/${R.id}`;if(T.items.push(R),await y(C.file,T),S){await Ge(e,ye,{text:j});const Y=await ve(e);ce(Y,S)!==void 0&&(delete Y[S],await y(Se(e),{links:Y}))}return ke&&e&&await W(e,ye,"ticket-made",{ticket:R.ticket}),e&&await ks(e,ye,{related:new Set,parent:null},await ba(e,ye)),{file:C.file,key:ye,lines:[]}}r(Pc,"createItem");const cf=r(e=>le(e,async()=>{const t=[];for(const n of await q(e)){const a=await g(n.file,null);if(!Array.isArray(a?.items)||!Array.isArray(a.followups))continue;const i=a.followups.find(l=>a.items.some(f=>f.id===l.id));i&&d(`${n.slug}: follow-up ${i.id} has the id of a card; rename one, then restart the board`);const s=[];for(const{id:l,title:f,from:p,filed:w,done:u,...h}of a.followups){const m=u?e&&(await K(Z(e,`${n.slug}/${l}`))).map(b=>b.ts).filter(Boolean).sort().pop()||w:null;s.push({id:l,title:f,...w?{created:w}:{},...p?{parent:G(n.slug,p)}:{},...u?{lane:"done",...m?{lane_at:m,closed_at:m}:{}}:{},...h})}const{followups:o,...c}=a;await y(n.file,{...c,items:[...a.items,...s]}),t.push(`${n.slug}: ${s.length} follow-ups moved to cards (${s.filter(l=>l.lane==="done").length} done)`)}return t}),"migrateFollowups");function _s(e,t=[]){if(e.status===void 0&&e.status_at===void 0&&e.merged_at===void 0)return!1;const n=e.merged_at||e.status_at||new Date().toISOString();["merged","done","dropped"].includes(e.status)&&Object.assign(e,{lane:"done",lane_at:n,closed_at:n}),(e.status==="merged"||e.merged_at)&&(e.prs=Be(e).map(i=>({...i,merged_at:i.merged_at||n})),delete e.pr),e.status==="held"&&(e.held=!0);const a=t.filter(i=>i.started&&at(i)&&i.kind!==ee).sort((i,s)=>String(i.started).localeCompare(String(s.started))).at(-1);return e.status==="checking"&&a&&(a.handed_back=n),delete e.status,delete e.status_at,delete e.merged_at,!0}r(_s,"statusToFacts");const lf=r(e=>le(e,async()=>{const t=[];for(const n of await q(e)){const a=await g(n.file,null);let i=0;for(const s of Array.isArray(a?.items)?a.items:[]){const o=s.status==="checking"?await V(n.dir,s.id):[],c=await Promise.all(o.map(l=>g(l,null)));if(_s(s,c.filter(Boolean))){i++;for(const[l,f]of c.entries())f?.handed_back&&await y(o[l],f)}}i&&(await y(n.file,a),t.push(`${n.slug}: ${i} cards' statuses became lanes and facts`))}return await It(e,async()=>{const n=await qe(e),a=n.filter(i=>i.item&&_s(i.item)).length;a&&(await Ct(e,n),t.push(`archive: ${a} cards' statuses became lanes and facts`))}),t}),"migrateStatuses"),df=r(e=>le(e,async()=>{const{asked:t}=await Yi(e),n=[];for(const a of await q(e)){const i=await g(a.file,null);let s=0;for(const o of Array.isArray(i?.items)?i.items:[]){if(o.gates||o.lane==="done")continue;const c=`${a.slug}/${o.id}`,l={};for(const[f,p]of Object.entries(Rt)){const w=t.filter(h=>h.topic===`item:${c}`&&h[f]).flatMap(h=>h.answers).filter(h=>["yes","no"].includes(h.choice)).sort((h,m)=>String(h.ts).localeCompare(String(m.ts))).at(-1);if(!w)continue;const u=p==="prototype"&&w.choice==="yes"&&(o.phase!=="grilling"||(await K(Z(e,c))).some(h=>h.line==="prototype-round"&&String(h.ts)>String(w.ts)));l[p]=u?"ran":w.choice}Object.keys(l).length&&(o.gates=l,s++)}s&&(await y(a.file,i),n.push(`${a.slug}: ${s} cards keep their gate answers`))}return n}),"migrateGates");function Ss(e,t){const n=e.lane==="done"&&!En.done.includes(e.phase);if(!("state"in e)&&!n)return!1;if("state"in e&&e.state!==void 0&&(e.phase=e.state),delete e.state,n){const a=Be(e);e.phase=a.some(i=>i.merged_at)||!a.length&&t?"merged":"closed"}return!0}r(Ss,"stateToPhase");const ff=r(e=>le(e,async()=>{const t=[],n=await q(e),a=r(async(i,s)=>!!i&&s.lane==="done"&&(await Promise.all((await V(i,s.id)).map(o=>g(o,null)))).some(o=>o?.started&&at(o)),"built");for(const i of n){const s=await g(i.file,null);let o=0;for(const c of Array.isArray(s?.items)?s.items:[])Ss(c,await a(i.dir,c))&&o++;o&&(await y(i.file,s),t.push(`${i.slug}: ${o} cards' states became phases`))}return await It(e,async()=>{const i=await qe(e);let s=0;for(const o of i)o.item&&Ss(o.item,await a(n.find(c=>c.slug===(o.file||F(o.key).file))?.dir,o.item))&&s++;s&&(await Ct(e,i),t.push(`archive: ${s} cards' states became phases`))}),t}),"migratePhases"),uf=r(e=>le(e,async()=>{const t=[];for(const n of await q(e)){const a=await g(n.file,null),i=(Array.isArray(a?.items)?a.items:[]).filter(s=>s.implement_at);if(i.length){for(const s of i)s.phase!=="ready"&&Object.assign(s,{phase:"staging",staged_at:s.implement_at}),delete s.implement_at;await y(n.file,a),t.push(`${n.slug}: ${i.length} Implement asks moved to staging or ready`)}}return t}),"stageImplementAsks"),bn=r(e=>$.join(e,"data","done-archive.json"),"archiveFile"),It=r((e,t)=>st($.join(e,"done-archive"),t,{heartbeat:!0}),"archiveWrite"),qe=r(async e=>(await g(bn(e),{cards:[]})).cards||[],"readArchive"),kn=r(e=>`${e.closed_at||""}|${e.key}`,"archiveRank"),Dc=r((e,t)=>kn(e)<kn(t)?1:kn(e)>kn(t)?-1:0,"byClosed"),ka=r(e=>$.join(e,"data","done-archive-count.json"),"archiveCountFile");async function wf(e){const t=(await g(ka(e),null))?.count;if(Number.isInteger(t))return t;const n=(await qe(e)).length;return await y(ka(e),{count:n}),n}r(wf,"archiveCount");async function Ct(e,t){t.length?await y(bn(e),{cards:t.sort(Dc)}):await O.rm(bn(e),{force:!0}),await y(ka(e),{count:t.length})}r(Ct,"writeArchive");const pf=r((e,t)=>It(e,async()=>{const n=new Map(t.map(w=>[w.key,w])),a=new Date().toISOString(),i=[],s=[];for(const w of await q(e)){const u=await g(w.file,null);if(!Array.isArray(u?.items))continue;const h=u.items.filter(m=>n.has(`${w.slug}/${m.id}`)&&m.lane==="done");if(h.length){for(const m of h){const b=n.get(`${w.slug}/${m.id}`);i.push({...b,file:w.slug,archived_at:a,item:b.prs?Fc(m,b.prs):m})}s.push([w.file,{...u,items:u.items.filter(m=>!h.includes(m))}])}}if(!i.length)return[];const o=await g(L(e),{items:[]}),c=new Map(i.map(w=>[`item:${w.key}`,w]));for(const w of i)w.questions=(o.items||[]).filter(u=>u.topic===`item:${w.key}`&&u.resolved);const l=await ve(e);for(const w of i)w.asks=w.item.from_ask&&ce(l,w.item.from_ask)===void 0?[w.item.from_ask]:[];const f=new Set(i.map(w=>w.key));await Ct(e,[...(await qe(e)).filter(w=>!f.has(w.key)),...i.map(({key:w,file:u,archived_at:h,closed_at:m,item:b,questions:_,asks:N})=>({key:w,file:u,archived_at:h,closed_at:m,item:b,questions:_,asks:N}))]),i.some(w=>w.asks.length)&&await y(Se(e),{links:{...l,...Object.fromEntries(i.flatMap(w=>w.asks.map(u=>[u,w.key])))}});for(const[w,u]of s)await y(w,u);const p=(o.items||[]).filter(w=>!(c.has(w.topic)&&w.resolved));return p.length!==(o.items||[]).length&&await y(L(e),{...o,items:p}),[...f]}),"archiveCards");function Fc(e,t){if(!xe(e))return e;const n=Be(e).map(s=>{const o=t.find(c=>c.state&&c.number===s.number&&(!s.repo||s.repo===c.repo));return o?{...s,kept:{checks:o.checks,reviews:o.reviews,codexOpen:o.codexOpen}}:s}),{pr:a,...i}=e;return{...i,prs:n}}r(Fc,"keptPRs");async function Nc(e,t){return!e||!me(bn(e))?!1:It(e,async()=>{const n=await qe(e),a=n.find(w=>w.key===t);if(!a)return!1;const i=(await q(e)).find(w=>w.slug===a.file),s=i&&await g(i.file,null);if(!Array.isArray(s?.items))return!1;s.items.some(w=>w.id===a.item.id)||await y(i.file,{...s,items:[...s.items,a.item]});const o=await g(L(e),{items:[]}),c=new Set((o.items||[]).map(w=>w.id)),l=(a.questions||[]).filter(w=>!c.has(w.id));l.length&&await y(L(e),{...o,items:[...o.items||[],...l]});const f=await ve(e),p=(a.asks||[]).filter(w=>ce(f,w)===t);return p.length&&await y(Se(e),{links:Object.fromEntries(Object.entries(f).filter(([w])=>!p.includes(w)))}),await Ct(e,n.filter(w=>w!==a)),!0})}r(Nc,"restoreArchived");const vs=["push_tag","no_change"],js=r(e=>Object.fromEntries(Object.entries(e).filter(([t])=>!vs.includes(t))),"stripQuestion"),Os=r(({summary:e,...t})=>t,"stripItem"),As=r(e=>vs.some(t=>t in e),"holdsDead"),mf=r(e=>It(e,async()=>{for(const a of await q(e)){const i=await g(a.file,null);Array.isArray(i?.items)&&i.items.some(s=>"summary"in s)&&await y(a.file,{...i,items:i.items.map(Os)})}const t=await g(L(e),null);(t?.items||[]).some(As)&&await y(L(e),{...t,items:t.items.map(js)});const n=await qe(e);n.some(a=>"card"in a||"summary"in a.item||(a.questions||[]).some(As))&&await Ct(e,n.map(({card:a,...i})=>({...i,...a?{closed_at:i.closed_at||a.sortAt}:{},item:Os(i.item),questions:(i.questions||[]).map(js)})))}),"stripDeadFields");async function hf(){return $o(async()=>{const e=go(),t=Array.isArray(e.migrated)?e.migrated:[];if(t.includes("wave-names")||!Object.keys(e.sites||{}).length)return[];const n=[];for(const{route:a,dir:i}of be(e))await Rc(i),n.push(a);return e.migrated=[...t,"wave-names"],await bo(e),n})}r(hf,"migrateWaveNames");const Ps=r(async e=>JSON.parse(await O.readFile(e,"utf8")),"readPlain"),ya=r((e,t)=>Je(e,JSON.stringify(t,null,2)+`
|
|
31
|
+
`),"writePlain"),Ds=r(e=>typeof e!="string"?e:e.replace(/^wave:([a-z0-9-]+)$/,"file:$1").replace(/^lineup:([a-z0-9-]+)$/,"wave:$1"),"renamedTopic"),xa=r(e=>Array.isArray(e)?e.map(xa):e&&typeof e=="object"?Object.fromEntries(Object.entries(e).map(([t,n])=>[Ds(t),xa(n)])):Ds(e),"renameTopics");async function Rc(e){const t=$.join(e,"board.json"),n=await Ps(t).catch(()=>null);if(n&&typeof n.items=="string"&&n.items!=="waves"&&$.basename(n.items)==="wave.json"){const i=$.join($.dirname(n.items),"items.json");me($.resolve(e,n.items))&&await O.rename($.resolve(e,n.items),$.resolve(e,i)),await ya(t,{...n,items:i,slug:n.slug||"wave"})}for(const i of await q(e)){const s=await Ps(i.file).catch(()=>null);if(!Array.isArray(s?.lineups))continue;const{lineups:o,...c}=s;await ya(i.file,{...c,waves:[...c.waves||[],...o]});for(const{slug:l}of o)await O.rename($.join(i.dir,`lineup-${l}.md`),$.join(i.dir,_a(l))).catch(f=>{if(f.code!=="ENOENT")throw f})}const a=$.join(e,"data");for(const i of(await O.readdir(a).catch(()=>[])).filter(s=>s.endsWith(".json"))){const s=$.join(a,i),o=await O.readFile(s,"utf8");let c;try{c=JSON.parse(o)}catch{continue}c=xa(c),i==="done-archive.json"&&Array.isArray(c.cards)&&(c.cards=c.cards.map(({wave:l,...f})=>l===void 0?f:{...f,file:l})),i==="arch-reviews.json"&&Array.isArray(c.reviews)&&(c.reviews=c.reviews.map(({wave:l,lineup:f,...p})=>f?{...p,wave:l}:{...p,file:l})),JSON.stringify(c)!==JSON.stringify(JSON.parse(o))&&await ya(s,c)}}r(Rc,"renameWaveNames");const _a=r(e=>`wave-${e}.md`,"waveNoteFile"),Ec=r(e=>typeof e=="string"&&/^[a-z0-9-]+$/.test(e)?e:d(`a wave's slug uses only a-z, 0-9 and "-", not "${e}"`),"waveSlug");async function $t(e,t){for(const n of await q(e)){const a=await g(n.file,null),i=(a?.waves||[]).find(s=>s.slug===t);if(i)return{source:n,itemFile:a,wave:i}}return d(`this board has no wave "${t}"; wave create makes one`)}r($t,"findWave");async function Bc(e,t){(await ie(e)).items!=="waves"&&d(`topic file:${t} is for a board over ~/.claude/waves; a wave's triage uses wave:<slug>`),await Ke(e,t)}r(Bc,"triagedFile");function Sa(e,t,n,a){return n.map(i=>{const{file:s,id:o}=F(i);s!==e.slug&&d(`${i} is not in item file "${e.slug}"; a wave's cards share one file`),((t.items.find(f=>f.id===o)||d(`item file "${s}" has no item "${o}"`)).deps||[]).includes(`wave:${a}`)&&d(`${i} waits on the wave "${a}", so it can't be in it`);const l=(t.waves||[]).find(f=>f.slug!==a&&!f.finished_at&&He(f).includes(o));return l&&d(`${i} is already in the wave "${l.slug}"; wave remove ${l.slug} ${i} first`),o})}r(Sa,"waveCards");const va=r(e=>[...new Set(String(e??"").split(",").map(t=>t.trim()).filter(Boolean))],"cardList");function Lc(e){const t=[e??[]].flat().map(n=>{const[a,i]=String(n).split(":");return[a.trim(),va(i)]});return(!t.length||t.some(([n])=>!n))&&d("--card takes one card per flag in build order, as <file>/<id>, or <file>/<id>:<file>/<id>,\u2026 naming the earlier cards it conflicts with"),t}r(Lc,"cardSpecs");function ja(e,t,n,a,i){const s=n.items.indexOf(a),o=i.map(f=>{const p=G(e.slug,f),{file:w,id:u}=F(p),h=w===e.slug?n.items.indexOf(u):-1;return(h<0||h>=s)&&d(`${p} is not an earlier card of the wave "${n.slug}"`),u}),c=t.items.find(f=>f.id===a),l=[...(c.deps||[]).filter(f=>!Fs(e,n,f)),...o];l.length?c.deps=l:delete c.deps}r(ja,"relink");const Fs=r((e,t,n)=>He(t).some(a=>G(e.slug,n)===`${e.slug}/${a}`),"inWave");async function Ns(e,t,n,a,i){i!==void 0&&await Je($.join(t.dir,_a(a.slug)),`${v(i,"the note").replace(/\n*$/,`
|
|
32
|
+
`)}`),await y(t.file,n);const{tickets:s}=je(await ie(e));return{file:t.file,lines:[`order: ${Rs(t,n,a,s)}`]}}r(Ns,"writeWave");function Rs(e,t,n,a){const i=r(s=>it(t,t.items.find(o=>o.id===s)||{id:s},a),"label");return He(n).map(s=>{const o=(t.items.find(c=>c.id===s)?.deps||[]).filter(c=>Fs(e,n,c));return o.length?`${i(s)} (after ${o.map(c=>i(F(G(e.slug,c)).id)).join(", ")})`:i(s)}).join(", ")}r(Rs,"orderLine");async function gf(e,t,{title:n,goal:a,cards:i,note:s}){Ec(t);const o=Lc(i);s===void 0&&d("wave create needs the wave note, as --note <text> or --file <path>");const c=await q(e);c.some(u=>u.slug===t)&&d(`"${t}" is an item file on this board; pick another slug`);for(const u of c)((await g(u.file,null))?.waves||[]).some(h=>h.slug===t)&&d(`this board already has a wave "${t}"`);const l=c.find(u=>u.slug===F(o[0][0]).file)||d(`this board has no item file "${F(o[0][0]).file}"`),{itemFile:f}=await Ke(e,l.slug),p=Sa(l,f,o.map(([u])=>u),t);new Set(p).size!==p.length&&d("--card names a card twice");const w={slug:t,title:v(n,"--title"),goal:v(a,"--goal"),items:p,created:new Date().toISOString()};return o.forEach(([,u],h)=>ja(l,f,w,p[h],u)),f.waves=[...f.waves||[],w],Ns(e,l,f,w,s)}r(gf,"createWave");async function $f(e,t,{add:n,remove:a,card:i,after:s,title:o,goal:c,note:l}){[n,a,i,s,o,c,l].every(m=>m===void 0)&&d("nothing to change: pass a card with --after, --title, --goal, --note or --file"),s!==void 0&&n===void 0&&i===void 0&&d(`--after goes with a card: wave set ${t} <file>/<id> --after <file>/<id>,\u2026`),i!==void 0&&s===void 0&&d(`wave set ${t} ${i} needs --after <file>/<id>,\u2026 ("" clears its links)`);const{source:f,itemFile:p,wave:w}=await $t(e,t);w.finished_at&&d(`the wave "${t}" is finished`),w.items=He(w),delete w.phases;const u=a===void 0?[]:await Ic(e,t);if(n!==void 0){const[m]=Sa(f,p,[n],t);w.items.includes(m)&&d(`${n} is already in the wave "${t}"`),w.items.push(m),ja(f,p,w,m,va(s))}if(i!==void 0){const{file:m,id:b}=F(i);(m!==f.slug||!w.items.includes(b))&&d(`${i} is not in the wave "${t}"`),ja(f,p,w,b,va(s))}if(a!==void 0){const[m]=Sa(f,p,[a],t);w.items.includes(m)||d(`${a} is not in the wave "${t}"`),w.items.length===1&&d(`${a} is the wave's last card; wave finish ${t} ends the wave`);const b=r(j=>!z(j)&&(j.deps||[]).some(A=>G(f.slug,A)===`${f.slug}/${m}`),"waiting"),_=p.items.filter(j=>w.items.includes(j.id)&&j.id!==m&&b(j)),N=p.items.find(j=>j.id===m);if(_.length&&!(N&&z(N))){const j=await Promise.all(_.map(async A=>`${await H(e,`${f.slug}/${A.id}`)} waits on ${a}: wave set ${t} ${f.slug}/${A.id} --after <its other conflicts, or "">`));d(`${a} can't leave the wave "${t}" while open cards wait on it; relink each, then remove:
|
|
33
33
|
${j.join(`
|
|
34
|
-
`)}`)}if(N&&
|
|
34
|
+
`)}`)}if(N&&z(N))for(const j of p.items.filter(A=>w.items.includes(A.id)&&A.deps))j.deps=j.deps.filter(A=>G(f.slug,A)!==`${f.slug}/${m}`),j.deps.length||delete j.deps;w.items=w.items.filter(j=>j!==m)}o!==void 0&&(w.title=v(o,"--title")),c!==void 0&&(w.goal=v(c,"--goal"));const h=await Ns(e,f,p,w,l);return a!==void 0&&await Cc(e,t,u,a),h}r($f,"changeWave");async function Ic(e,t){const n=[];for(const a of await q(e)){const i=await g(a.file,null);for(const s of Array.isArray(i?.items)?i.items:[])z(s)||!(s.deps||[]).includes(`wave:${t}`)||(await Te(e,a.slug,s)).length&&n.push(`${a.slug}/${s.id}`)}return n}r(Ic,"waveBlocked");async function Cc(e,t,n,a){const i=`${await H(e,a)} left the wave "${t}"`;for(const s of n){const{item:o}=await x(e,s);if(o.blocked_until||(await Te(e,F(s).file,o)).length)continue;const{line:c}=await W(e,s,"unblocked",{lifted:i});await ha(e,s,c.ts),await $e({dir:e},"card-unblocked",{key:s})}}r(Cc,"noticeWaveUnblocked");async function bf(e,t){const{source:n,itemFile:a,wave:i}=await $t(e,t),s=a.items.filter(o=>He(i).includes(o.id)&&!z(o));if(s.length){const o=await Promise.all(s.map(c=>H(e,`${n.slug}/${c.id}`)));d(`the wave "${t}" still has ${o.join(", ")} open; wave finish waits until every card in it is Done`)}return i.finished_at=new Date().toISOString(),await y(n.file,a),{file:n.file}}r(bf,"finishWave");async function kf(e,t,n){const{source:a,itemFile:i,wave:s}=await $t(e,t),o=await O.readFile($.join(a.dir,_a(t)),"utf8").catch(()=>"");return{title:s.title,markdown:`**Goal** ${s.goal}
|
|
35
35
|
|
|
36
|
-
**Order** ${
|
|
36
|
+
**Order** ${Rs(a,i,s,n)}
|
|
37
37
|
|
|
38
|
-
${o}`}}r(du,"readWaveNotes");async function U(e,t,n,a="main"){const{id:i}=F(t);/^[\w-]{1,40}$/.test(a)||d(`--agent takes a name of letters, digits, "_" and "-", not "${a}"`);const s=a==="main"?`${i}.json`:`${i}@${a}.json`;if(n)return $.join($.resolve(n),"agents",s);const{source:o}=await x(e,t);return $.join(o.dir,"agents",s)}r(U,"builderFile");const As=r((e,t)=>typeof e=="string"&&/^\d{4}-\d\d-\d\dT/.test(e)&&!Number.isNaN(Date.parse(e))?new Date(e).toISOString():d(`${t} takes an ISO time such as 2026-10-23T16:30:00-07:00`),"isoTime"),Nc=Nn.progress,Ps=80,Ds="reading, building, testing, checking or landing";function Fs(e,t,n,a){e||d(`${a} needs --site, since the line goes to the item's thread`),t!==void 0&&!Nc.includes(t)&&d(`--phase takes ${Ds}, not "${t}"`);const i=S(n,a);return i.length>Ps&&d(`${a} is ${i.length} characters; the cap is ${Ps}. Say it shorter.`),{phase:t,detail:i}}r(Fs,"progressLine");const Ns=r((e,t,n)=>t?{detail:e.detail}:(e.phase===void 0&&d(`${n} needs --phase ${Ds}`),e),"forAgent");async function Sa(e,t,n="main",a,{phase:i,detail:s},o){const c=He(a),l=c||n==="main"?"":a.kind===X?n:Jt(a)||a.kind||n,u=c?oo[a.kind]:[l,i].filter(Boolean).join(" "),p=new RegExp(`^(?:${u}|${i??u})\\s*:?\\s+`,"i"),w=u?`${u[0].toUpperCase()}${u.slice(1)}: ${s.replace(p,"")}`:s,f=Z(e,t);(await K(f)).filter(h=>h.kind==="status").pop()?.text!==w&&await le(f,{ts:o,who:"claude",kind:"status",run:a.started,...i?{phase:i}:{},...c?{planner:!0}:{},text:w})}r(Sa,"statusToThread");const Rc={prototype:"sonnet",grill:"opus",[$e]:"sonnet"};async function It(e,t,{label:n,phase:a,progress:i,dir:s,agent:o="main",kind:c,model:l,eta:u,worktree:p,ownWorktree:w,dispatch:f}){a!==void 0&&i===void 0&&d('--phase goes with --progress "<detail>"');const h=ie(i,ne=>Fs(e,a,ne,"--progress")),m=await U(e,t,s,o),k=new Date().toISOString(),_=ie(c,ne=>/^[\w.-]{1,30}$/.test(ne)?ne:d("--kind takes a short word such as claude or codex")),N=Ot(_),j=l??(_==="opus"||_==="sonnet"?_:void 0),A=["build",...Wc].includes(N),D=await g(m,null),T=r(ne=>["tap","hub"].includes(ne?.dispatched)&&!ne.done,"tapOpen");if(f==="session"&&T(D))return{file:m,alreadyOpen:!0,lines:[]};o!=="main"&&_!==$e&&_!==X&&T(await g(await U(e,t,s),null))&&d(`the Implement tap opened ${t}'s main builder record and no agent has taken it up; run builder start ${t} without --agent first`);const M=D?.dispatched&&!D.done?D:null,v=A&&!s?(await x(e,t)).item:void 0,L=D&&et(D).length?D:null,C=M&&N==="build"?{kind:Ot(M.kind),model:M.model,reason:{tap:"The owner's Build tap picked it.",hub:"The board's pull picked it."}[M.dispatched]??"The session's dispatch picked it."}:A?Bn({card:v??{}}):null;C&&N!=="build"&&N!==C.kind&&d(`--kind ${N}: ${C.kind} builds now. ${C.reason} Pass --kind ${C.kind}, or --kind build.`);const q=C?C.kind:N,se=Ua({dir:e});if(!(M&&!f)&&ho(se,q))throw new me(mo(se,q));const R=h&&Ns(h,He({kind:q}),"--progress");l!==void 0&&!["opus","sonnet"].includes(l)&&d("--model takes opus or sonnet"),j!==void 0&&q==="codex"&&d("--model is for kind claude; codex needs none");const J=q==="claude"?Cn():"auto";j!==void 0&&J!=="auto"&&j!==J&&d(`--model ${j}: the hub's Claude model is ${J==="opus"?"Opus":"Sonnet"}. Pass --model ${J}, or no --model.`);const ee=q==="claude"?j??C?.model??(M&&Ot(M.kind)==="claude"?M.model:void 0)??"opus":He({kind:q})?j??(M?.kind===q?M.model:void 0)??Rc[q]:q===X?j??D?.model:void 0,Q={label:n===void 0?"Builder":S(n,"--label"),...q?{kind:q}:{},...ee?{model:ee}:{},started:k,...u===void 0?{}:{eta:As(u,"--eta")},...f?{dispatched:f}:{},...L?{calls:L.calls,...L.verified?{verified:L.verified}:{}}:{},...Tc(D)?{session:D.session}:{},...q===X&&D?.kind===X?{label:D.label,task:D.task,writes:D.writes,requested:D.requested}:{}},ye=A&&!s&&!G(v)&&v.lane!=="progress",be=ye?await Oe(e,t,{start:!0}):{lines:[]};let Y,Ae=null;const qt=w&&A&&o==="main"?[D?.worktree,v?.worktree].find(ne=>ne?.path&&we(ne.path))??null:null,kt=(M??L)?.worktree??qt,yt=p??(A&&o==="main"&&!kt?w:void 0);try{if(yt){const ne=await V($.dirname($.dirname(m)),F(t).id),bn=(await Promise.all(ne.filter(ge=>ge!==m).map(ge=>g(ge,null)))).filter(ge=>ge?.worktree?.path&&!ge.done).map(ge=>ge.worktree.path);({remade:Ae,...Y}=await yt({busy:bn}))}else Y=kt}catch(ne){throw ye&&await Mc(e,t,v),ne}await Ec(m,D),await b(m,Y?{...Q,worktree:Y}:Q),v?.rebase_run&&o==="main"&&![Rs,Ta].includes(Q.label)&&await Es(e,t),C?.inputs&&e&&Do(Ka(e),{call:"builder-pick",key:t,result:[q,q==="claude"?ee:null].filter(Boolean).join(" "),reason:C.reason,inputs:{...C.inputs,...j?{askedModel:j}:{},...f?{dispatch:f}:{}}}),Ae&&e&&await W(e,t,"worktree-remade"),R&&await Sa(e,t,o,Q,R,k);const oe=_===$e||_===X?[]:await un(e,t,"Acted on: builder started");return e&&A&&await Yr(e,t),{file:m,kind:q??null,model:ee??null,reason:C?.reason??null,worktree:Y?.path??null,lines:[...N==="build"?[`kind: ${q}. ${C.reason}`]:[],...Jt(Q)?[`model: ${Jt(Q)}`]:[],...e?[`ref: ${await H(e,t)}`]:[],...Y?[`worktree: ${Y.path}`]:[],...Ae?[Ae]:[],...be.lines,...oe]}}r(It,"startBuilder");async function Ec(e,t){if(!t?.started||!t.done||!e.endsWith(".json")||$.basename(e).includes("@"))return;const{label:n,kind:a,model:i,started:s,done:o,session:c}=t,l=$.join($.dirname(e),`${$.basename(e,".json")}@past-${Date.parse(s)}.json`);await b(l,{label:n,...a?{kind:a}:{},...i?{model:i}:{},started:s,done:o,...c?.log?{session:{log:c.log}}:{}})}r(Ec,"keepPastRun");async function uu(e,t,n,a="tap"){try{const{held:i,blockers:s}=await Bc(e,t);if(i&&await us(e,"card-hold",{key:t,hold:!1}),s.length)return await Cc(e,t,s);try{const o=await It(e,t,{kind:"build",worktree:n,dispatch:a});return{key:t,ref:await H(e,t),kind:o.kind,...o.model?{model:o.model}:{},reason:o.reason,worktree:o.worktree}}catch(o){throw i&&await qc(e,t,i),o}}catch(i){if(!(i instanceof me))throw i;return await W(e,t,Ia,{why:i.message}),null}}r(uu,"startTappedBuild");async function fu(e,t,{pr:n,conflict:a,base:i},s){const{source:o,itemFile:c,item:l}=await x(e,t);l.rebase_run={pr:n,base:i,conflict:a,at:new Date().toISOString()},await b(o.file,c);try{const u=await It(e,t,{kind:"build",label:Rs,dispatch:"hub",ownWorktree:s});return{key:t,kind:u.kind,...u.model?{model:u.model}:{},worktree:u.worktree}}catch(u){throw await Es(e,t),u}}r(fu,"startRebase");const Rs="Rebase";async function Es(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);delete i.rebase_run,await b(n.file,a)}r(Es,"endRebaseRun");async function wu(e,t){const{itemFile:n,item:a}=await x(e,t),i=fe(e,n);return Pt(a,i).find(s=>s.repo===i)?.number??null}r(wu,"cardPR");async function pu(e,t,{from:n,to:a,replayed:i}){const{source:s,itemFile:o,item:c}=await x(e,t);return c.rebase_run?(c.rebased=[...c.rebased||[],{from:n,to:a,base:c.rebase_run.base,replayed:i,at:new Date().toISOString()}],delete c.rebase_run,await b(s.file,o),!0):!1}r(pu,"recordRebase");const Lc=r((e,t,n)=>{const a=new Map((e.rebased||[]).filter(i=>i.replayed).map(i=>[i.to,i.from]));for(let i=n,s=0;i&&s<=a.size;i=a.get(i),s++)if(i===t)return!0;return!1},"onlyRebased");async function Bc(e,t){const n=Ua({dir:e});go(n)&&d(po(n));const{item:a}=await x(e,t),i=a.held?{held:a.held,pick_up_at:a.pick_up_at,held_at:a.held_at}:null;return i&&he(a)&&d("it's held. Unhold it, then tap Implement."),await ua(e,t)&&d("no take is picked on its prototypes, so it prototypes again. Pick a take from the next round, then tap Implement."),{held:i,blockers:await sc(e,t,a)}}r(Bc,"tapRefusals");async function mu(e,t,n){await Ee(e,t,{who:"love",by:n,command:"implement"}),await Qr(e,t,{})}r(mu,"implementAsIs");async function Cc(e,t,n){const a=n.join(", ");return await pe({dir:e},"card-bump",{key:t}),await W(e,t,"cant-start-yet",{names:a}),{key:t,ref:await H(e,t),bumped:n,line:`bumped: ${t} waits on ${a}, so it stays in Todo, bumped to the top`}}r(Cc,"bumpBlocked");async function kn(e,t){const n=await U(e,t),a=await g(n,null);return a?.started&&!a.done&&at(a)?{file:n,record:a}:null}r(kn,"openBuild");async function Ls(e,t,{cause:n,words:a,by:i,until:s}){const o=await kn(e,t);return!o||o.record.pause?!1:(await b(o.file,{...o.record,pause:{cause:n,words:a,at:new Date().toISOString(),...i?{by:i}:{},...s?{until:s}:{}}}),!0)}r(Ls,"requestPause");async function Ic(e,t){const n=await kn(e,t),a=n?.record.pause;if(!a||a.paused)return a??null;const i=new Date().toISOString(),s=(await K(Z(e,t))).filter(c=>c.kind==="status"&&c.phase&&!c.planner&&String(c.ts)>=String(n.record.started)).pop()?.phase,o={...a,paused:i,...s?{phase:s}:{}};return await b(n.file,{...n.record,pause:o}),await W(e,t,"paused",{phase:s,words:a.words,clear:Ma[a.cause][0]}),o}r(Ic,"markPaused");async function yn(e,t,{dir:n,agent:a="main"}){if(!e||n||a!=="main")return[];const i=await Ic(e,t);return i?[`Paused: ${i.words}. Commit what you have now with \`git -c core.commentChar=';' commit -F <file>\`, then stop: end your turn and run nothing else. The board resumes this session, telling it so, once the pause clears.`]:[]}r(yn,"pauseReply");async function hu(e,t){const n=await kn(e,t),a=n?.record.pause;if(!a?.paused)return null;const{pause:i,...s}=n.record;return await b(n.file,s),await W(e,t,"resumed",{clear:Ma[a.cause][1]}),{pause:a,record:s}}r(hu,"resumePaused");async function gu(e,t){const n=await kn(e,t);if(!n?.record.pause||n.record.pause.paused)return;const{pause:a,...i}=n.record;await b(n.file,i)}r(gu,"dropPause");async function $u(e,t,n){const a=await U(e,t),i=await g(a,null),s=new Date().toISOString();i&&!i.done&&await b(a,{...i,done:s});const{source:o,itemFile:c,item:l}=await x(e,t);l.lane==="progress"&&Lt(l,"todo",s),await b(o.file,c),await W(e,t,Ia,{why:n})}r($u,"abandonLaunch");async function ku(e,t,[n,a],i="main"){const s=await U(e,t,void 0,i),o=await g(s,null);o&&!o.done&&await b(s,{...o,done:new Date().toISOString()}),await wt(e,t),await W(e,t,n,a)}r(ku,"abandonPlanner");async function yu(e,t,n,a,i="main",s=null){const o=await U(e,t,void 0,i),c=await g(o,null);await b(o,{...c,session:{id:n,pid:a,log:s,at:new Date().toISOString()}})}r(yu,"recordSession");async function bu(e,t){const{source:n}=await x(e,t);return(await Promise.all((await V(n.dir,F(t).id)).map(i=>g(i,null)))).filter(i=>i&&i.started&&!i.done).map(i=>({pid:i.session?.pid??null,at:i.session?.at??null}))}r(bu,"openAgents");async function xu(e,t){const{source:n}=await x(e,t);return(await Promise.all((await V(n.dir,F(t).id)).map(i=>g(i,null)))).filter(i=>i&&i.started&&!i.done).map(i=>({kind:i.kind??null,pid:i.session?.pid??null,at:i.session?.at??null}))}r(xu,"openRuns");async function _u(e,t,n){const{source:a}=await x(e,t),i=new Date().toISOString();for(const s of await V(a.dir,F(t).id)){const o=await g(s,null);o?.started&&!o.done&&at(o)&&await b(s,{...o,stopped:{run:n,at:i}})}}r(_u,"markStopped");async function Su(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);i.merge_ready&&(i.merge_ready.stopped=new Date().toISOString(),await b(n.file,a))}r(Su,"stopHeldMerge");async function vu(e,t,n){const a=S(n,"item stall --reason"),{source:i,itemFile:s,item:o}=await x(e,t);return o.lane==="done"&&d(`${await H(e,t)} is done; there is nothing to stall`),o.stalled={reason:a,at:new Date().toISOString()},await b(i.file,s),a}r(vu,"markStalled");async function ju(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);i.stalled&&(delete i.stalled,await b(n.file,a))}r(ju,"clearStall");async function Ou(e,t){const{item:n}=await x(e,t),a=await K($.join(e,"data","events.jsonl")),i=r(c=>a.filter(l=>l.kind===c&&l.data?.key===t).map(l=>String(l.ts)).sort().pop()||"","latest"),s=i("card-bump");if(!s||i("card-unbump")>s)return null;const o=[n.closed_at,...Re(n).map(c=>c.merged_at)].filter(Boolean).map(String).sort().pop()||"";return s>o?s:null}r(Ou,"landFirst");async function Au(e,t,n){const{item:a}=await x(e,t);G(a)&&d(`${await H(e,t)} is done; there is nothing to bump`);const i=await pe({dir:e},n?"card-bump":"card-unbump",{key:t});return(await K($.join(e,"data","events.jsonl"))).find(s=>s.id===i).ts}r(Au,"bumpCard");async function Bs(e,t){return(await K(Z(e,t))).filter(n=>n.kind==="status").pop()?.phase==="landing"}r(Bs,"isLanding");const Tc=r(e=>!!e?.session&&e.session.at>(e.done??e.started),"launchedSession");async function Pu(e,t,n){const a=await g(await U(e,t),null);if(a&&!a.done)return null;const i=await It(e,t,{kind:n,dispatch:"hub"});return{key:t,kind:n,model:i.model}}r(Pu,"startPlanner");async function Du(e,t,n){const{source:a}=await x(e,t);if((await Promise.all((await V(a.dir,F(t).id)).map(c=>g(c,null)))).some(c=>c?.kind===$e&&c.started&&!c.done))return null;const s=lo(n);if(await g(await U(e,t,void 0,s),null))return null;const o=await It(e,t,{kind:$e,agent:s,dispatch:"hub"});return{key:t,kind:$e,model:o.model,agent:s}}r(Du,"startVisual");const va=r(e=>typeof e=="string"&&/^[a-z][a-z0-9-]{0,29}$/.test(e)&&e!=="main"&&!e.startsWith(`${$e}-`)?e:d('--name takes a short lowercase name such as tests or docs-pass: letters, digits and "-", not main or visual-\u2026'),"helperName");async function ja(e,t){const{item:n}=await x(e,t);return[(await g(await U(e,t),null))?.worktree,n.worktree].find(i=>i?.path&&we(i.path))??null}r(ja,"cardWorktree");async function Fu(e,t,{name:n,task:a,writes:i,model:s}){P(e);const o=va(n),c=S(a,"--task");s!==void 0&&!["opus","sonnet"].includes(s)&&d("--model takes opus or sonnet");const l=Cn();s!==void 0&&l!=="auto"&&s!==l&&d(`--model ${s}: the hub's Claude model is ${l==="opus"?"Opus":"Sonnet"}. Pass --model ${l}, or no --model.`);const{item:u}=await x(e,t);G(u)&&d(`${await H(e,t)} is Done; a helper needs an open card`);const p=await U(e,t,void 0,o);return we(p)&&d(`${t} already has an agent named ${o}; pick another --name`),i&&!await ja(e,t)&&d(`a helper that writes branches from ${t}'s worktree, and the card has none; start its build first, or drop --writes`),await b(p,{label:o,kind:X,task:c,writes:!!i,...s?{model:s}:{},requested:new Date().toISOString()}),await W(e,t,"helper-asked",{agent:o,writes:i}),{lines:[`helper ${o} requested: the board launches it ${i?"once an implementer slot is free":"now"}`,`wait for it with agent wait ${t} --name ${o}`]}}r(Fu,"requestHelper");async function Nu(e,t,n,a){const i=await U(e,t,void 0,n),s=await g(i,null);if(s?.kind!==X||s.started||s.done)return null;const{item:o}=await x(e,t),c=Bn({card:o}),l=Cn(),u=s.model??(l!=="auto"?l:c.kind==="claude"?c.model:"opus"),p=s.writes?await ja(e,t):null;s.writes&&!p&&d(`${t} has no worktree for helper ${n} to branch from`),await b(i,{...s,model:u});const w=await It(e,t,{agent:n,kind:X,dispatch:"hub",worktree:p?({busy:f})=>a({repo:p.repo,base:p.branch,busy:f}):void 0});return{key:t,kind:X,model:u,agent:n,task:s.task,writes:s.writes,worktree:w.worktree}}r(Nu,"startHelper");async function Ru(e,t,n){const a=await g(await U(e,t,void 0,va(n)),null);return a?.kind!==X&&d(`${t} has no helper named ${n}`),{record:a,card:await ja(e,t)}}r(Ru,"helperOf");async function Eu(e,t,{name:n,agent:a="main",timeout:i=9,every:s=2e3,alive:o}){P(e);const c=Number(i);c>0||d("--timeout takes minutes, a number above 0");const l=await U(e,t,void 0,va(n)),u=await U(e,t,void 0,a),p=r(async f=>{const h=await g(u,null);if(!h?.started||h.done)return;const{waits:m,...k}=h;await b(u,f?{...k,waits:f}:k)},"mark"),w=Date.now()+c*6e4;await p(n);try{for(;;){const f=await g(l,null);if(f?.kind!==X&&d(`${t} has no helper named ${n}`),f.done){const m=f.writes&&f.worktree?[`branch ${f.worktree.branch} in ${f.worktree.path}: merge it with agent merge ${t} --name ${n}`]:[];return{lines:[`helper ${n} done: ${f.note??"no hand-back"}`,...m]}}const h=f.session?.pid??null;h&&!o(h)&&d(`helper ${n}'s process exited without builder done; do its task yourself`),Date.now()>=w&&d(`helper ${n} is still ${f.started?"running":f.writes?"waiting for an implementer slot":"waiting to launch"} after ${c} minutes; run agent wait again`),await new Promise(m=>setTimeout(m,s))}}finally{await p(null)}}r(Eu,"waitHelper");async function Lu(e,t,n="main"){return n==="main"?!1:(await g(await U(e,t,void 0,n),null))?.kind===X}r(Lu,"isHelper");async function Bu(e,t,n="main"){const a=await g(await U(e,t,void 0,n),null);return!!(a?.started&&!a.done&&He(a))}r(Bu,"isPlanner");async function Cu(e,t,{text:n}){return P(e),n===void 0&&d('item visual needs --text "<what the visual should show>"'),await x(e,t),await pe({dir:e},"visual-request",{key:t,text:S(n,"--text"),who:"claude"}),{lines:[`visual asked on ${await H(e,t)}: the board launches its visual agent`]}}r(Cu,"requestVisual");async function Iu(e,t,{file:n,title:a}){P(e);const i=S(a,"--title");(typeof n!="string"||!n.endsWith(".html"))&&d("--file takes the page, an .html file");const s=await O.readFile($.resolve(n),"utf8").catch(()=>d(`--file ${n} can't be read`));s.trim()||d(`--file ${n} is empty`),await x(e,t);const{id:o}=F(t),c=$.join(e,"public","visuals",o);await O.mkdir(c,{recursive:!0});const l=i.toLowerCase().replace(/[^a-z0-9]+/g,"-").slice(0,60).replace(/^-+|-+$/g,"")||"visual";let u=`${l}.html`;for(let w=2;we($.join(c,u));w++)u=`${l}-${w}.html`;await O.writeFile($.join(c,u),s,{flag:"wx"});const p=`visuals/${o}/${u}`;return await le(Z(e,t),{ts:new Date().toISOString(),who:"claude",kind:$e,text:S(i,"--name"),link:Mn(p,"--link")}),{lines:[p]}}r(Iu,"addVisual");async function Tu(e){const t=[];for(const n of await I(e)){const a=await g(n.file,null);for(const i of a?.items||[]){if(G(i))continue;const s=await g($.join(n.dir,"agents",`${i.id}.json`),null);!s?.started||s.done||!s.dispatched||He(s)||t.push({key:`${n.slug}/${i.id}`,kind:Ot(s.kind),model:s.model})}}return t}r(Tu,"cutOffBuilds");async function qc(e,t,n){const{source:a,itemFile:i,item:s}=await x(e,t);for(const o of Object.keys(n))n[o]===void 0?delete s[o]:s[o]=n[o];await b(a.file,i)}r(qc,"restoreHold");async function Mc(e,t,n){const{source:a,itemFile:i,item:s}=await x(e,t);for(const o of["lane","lane_at","closed_at","reopened_at"])n[o]===void 0?delete s[o]:s[o]=n[o];await b(a.file,i)}r(Mc,"restoreLane");async function qu(e,t){return e||d("audit reads a board: pass --site <route>"),t!==void 0&&F(t),{lines:Fo(No(Ka(e),{key:t}))}}r(qu,"auditLog");const Wc=["codex","claude"];async function Mu(e,t){const{item:n}=await x(e,t),{kind:a,model:i,reason:s}=Bn({card:n});return{lines:[`${[a,i].filter(Boolean).join(" ")}: ${s}`]}}r(Mu,"pickBuilder");async function Cs(e,t,{dir:n}={}){const{id:a}=F(t),i=$.dirname(await U(e,t,n)),s=await V($.dirname(i),a);return(await Promise.all(s.map(c=>g(c,null)))).filter(c=>c?.worktree?.path&&c.kind!==X).sort((c,l)=>String(l.started).localeCompare(String(c.started)))[0]?.worktree??null}r(Cs,"recordedWorktree");async function Wu(e,t){const{source:n}=await x(e,t),i=(await Promise.all((await V(n.dir,F(t).id)).map(s=>g(s,null)))).filter(s=>s?.session?.id&&s.worktree?.path&&s.kind!==X).sort((s,o)=>String(o.started).localeCompare(String(s.started)))[0];return i?{id:i.session.id,worktree:i.worktree.path}:null}r(Wu,"recordedSession");async function Uu(e,t,n,{phase:a,dir:i,agent:s="main",eta:o,clearEta:c}){o!==void 0&&c&&d("pass --eta or --clear-eta, not both");const l=Fs(e,a,n,"builder progress"),u=await U(e,t,i,s),p=await g(u,null);(!p?.started||p.done)&&d(`builder progress needs an open builder record on ${t}; run builder start first`);const w=Ns(l,He(p),"builder progress");return o!==void 0&&(p.eta=As(o,"--eta")),c&&delete p.eta,(o!==void 0||c)&&await b(u,p),await Sa(e,t,s,p,w,new Date().toISOString()),{file:u,lines:await yn(e,t,{dir:i,agent:s})}}r(Uu,"builderProgress");async function Hu(e,t){const{source:n}=await x(e,t),a=Z(e,t);if((await K(a)).filter(o=>o.kind==="status").pop()?.phase==="landing")return;const s=(await Promise.all((await V(n.dir,F(t).id)).map(o=>g(o,null)))).filter(o=>o?.started&&at(o)&&o.label!==Ta).map(o=>o.started).sort().pop();await le(a,{ts:new Date().toISOString(),who:"claude",kind:"status",phase:"landing",text:"Landing: rebasing onto main and landing",...s?{run:s}:{}})}r(Hu,"landingLine");const et=r(e=>(e?.calls||[]).filter(t=>!e.verified||t.at>e.verified.at),"unverifiedCalls");async function Ju(e,t,n,{dir:a,agent:i="main"}){const s=S(n,"builder call");/\n/.test(s)&&d("builder call takes one line; record each call on its own");const o=await U(e,t,a,i),c=await g(o,null);return(!c?.started||c.done)&&d(`builder call needs an open builder record on ${t}; run builder start first`),await b(o,{...c,calls:[...c.calls||[],{at:new Date().toISOString(),text:s}]}),{file:o,lines:await yn(e,t,{dir:a,agent:i})}}r(Ju,"recordCall");async function Tt(e,t){const n=await K(Z(e,t)),a=new Map(n.filter(i=>i.kind===Ut.answer).map(i=>[i.ask,i]));return n.filter(i=>i.kind===Ut.ask).map(i=>{const s=a.get(i.ask);return{id:i.ask,agent:i.agent||"main",text:i.text,at:i.ts,answer:s?{text:s.text,at:s.ts}:null,deferred:!!s?.deferred}})}r(Tt,"builderAsks");const Uc=r(e=>ke().find(t=>Fe(t.dir,e))??{dir:e},"boardTarget");async function Ku(e,t,n,{agent:a="main"}){P(e);const i=S(n,"builder ask");/\n/.test(i)&&d("builder ask takes one line; ask each question on its own");const s=await g(await U(e,t,void 0,a),null);(!s?.started||s.done)&&d(`builder ask needs an open builder record on ${t}; run builder start first`);const o=`ask${(await Tt(e,t)).length+1}`;return await le(Z(e,t),{ts:new Date().toISOString(),who:`${Jt(s)||s.kind||"Claude"} builder`,kind:Ut.ask,ask:o,agent:a,text:i}),await pe(Uc(e),"builder-ask",{key:t,ask:o,agent:a,text:i}),{id:o,lines:await yn(e,t,{agent:a})}}r(Ku,"askOrchestrator");async function zu(e,t,n,a,{deferred:i=!1}={}){P(e);const s=S(a,"--text"),o=(await Tt(e,t)).find(l=>l.id===n)||d(`no ask "${n}" on ${t}; builder asks ${t} lists them`);o.answer&&d(`${n} on ${t} is already ${o.deferred?"deferred":"answered"}`);const c=new Date().toISOString();if(await le(Z(e,t),{ts:c,who:"claude",kind:Ut.answer,ask:n,...i?{deferred:!0}:{},text:s}),i){const l=await U(e,t,void 0,o.agent),u=await g(l,null);u&&await b(l,{...u,calls:[...u.calls||[],{at:c,text:`Question: ${o.text} (deferred to the owner)`}]})}return{lines:[]}}r(zu,"answerBuilder");async function Gu(e,t,{wait:n=!1,timeout:a=9,every:i=2e3}={}){P(e);const s=Number(a);s>0||d("--timeout takes minutes, a number above 0");const o=Date.now()+s*6e4;for(;;){const c=await Tt(e,t);if(!c.length)return{lines:["no asks yet"]};const l=c.filter(w=>!w.answer),u=c.flatMap(w=>[`${w.id}. ${w.text}`,` ${w.answer?`${w.deferred?"deferred":"answered"}: ${w.answer.text}`:"waiting on the orchestrator"}`]);if(!n||!l.length)return{lines:u};const p=await yn(e,t,{});if(p.length)return{lines:[...u,...p]};Date.now()>=o&&d(`${l.map(w=>w.id).join(", ")} still waiting on the orchestrator after ${s} minutes; run builder asks ${t} --wait again`),await new Promise(w=>setTimeout(w,i))}}r(Gu,"listAsks");async function Hc(e,t,n){const a=t.topic.startsWith("item:")&&!t.topic.startsWith("item:ask:")?t.topic.slice(5):d(`${n}.ask goes on a card's question, topic item:<file>/<id>`),i=(await Tt(e,a)).find(s=>s.id===t.ask)||d(`${n}.ask names no ask "${t.ask}" on ${a}; builder asks ${a} lists them`);i.deferred||d(`${n}.ask names ${i.id}, which the orchestrator ${i.answer?"answered":"has not deferred"}; only a deferred ask goes to the owner`)}r(Hc,"checkAskQuestion");async function Qu(e,t,{dir:n,agent:a="main"}){const i=await g(await U(e,t,n,a),null),s=i?.calls||[];if(!s.length)return{lines:["no judgment calls recorded"]};const{verified:o}=i,c=et(i).length?"not verified yet":`verified ${o.at}${o.note?`: ${o.note}`:""}`;return{lines:[...s.map((l,u)=>`${u+1}. ${l.text}`),c]}}r(Qu,"listCalls");async function Vu(e,t,{dir:n,agent:a="main",note:i}){const s=await U(e,t,n,a),o=await g(s,null);if(et(o).length||d(`${t} has no unverified judgment calls${a==="main"?"":` from agent ${a}`}; builder calls ${t} lists them`),e){const c=new Set((await g(B(e),{items:[]})).items?.filter(u=>u.topic===`item:${t}`&&u.ask).map(u=>u.ask)),l=(await Tt(e,t)).find(u=>u.deferred&&u.agent===a&&!c.has(u.id));l&&d(`${l.id} on ${t} was deferred to the owner; add its question with "ask": "${l.id}" first`)}return await b(s,{...o,verified:{at:new Date().toISOString(),...i===void 0?{}:{note:S(i,"--note")}}}),s}r(Vu,"verifyCalls");async function Zu(e,t){const{source:n,item:a}=await x(e,t),i=r((o,c)=>[(o.options||[]).find(l=>l.id===c.choice)?.label,c.note].filter(Boolean).join(": ")||"answered","said"),s=(await ue(e,t)).asked.filter(o=>o.ask&&o.answers.length).map(o=>({ask:o.ask,question:o.question,answer:i(o,o.answers.at(-1))}));return{key:t,ref:await H(e,t),title:a.title||t,notes:a.plan?$.join(n.dir,a.plan):null,thread:Z(e,t),worktree:(await Cs(e,t))?.path??null,answers:s,links:await Jc(e,t),rebasePR:a.rebase_run?.pr??null,prototype:a.gates?.prototype??null}}r(Zu,"briefFacts");async function Jc(e,t){const n=new Map;for(const c of await I(e))for(const l of(await g(c.file,null))?.items||[])n.set(`${c.slug}/${l.id}`,{entry:c,it:l});const a=F(t).file,{it:i}=n.get(t),s=[...n].filter(([,c])=>(c.it.related||[]).some(l=>z(c.entry.slug,l)===t)).map(([c])=>c),o=[...i.parent?[["parent",z(a,i.parent)]]:[],...(i.deps||[]).map(c=>["depends on",z(a,c)]),...[...new Set([...(i.related||[]).map(c=>z(a,c)),...s])].map(c=>["related",c])].filter(([,c])=>n.has(c));return Promise.all(o.map(async([c,l])=>{const{entry:u,it:p}=n.get(l);return{relation:c,ref:await H(e,l),title:p.title||l,lane:Me[p.lane||"todo"],notes:p.plan?$.join(u.dir,p.plan):null,thread:Z(e,l)}}))}r(Jc,"cardLinks");async function Yu(e){const t=[];for(const n of await I(e)){const a=await g(n.file,null);for(const i of a?.items||[]){if(G(i))continue;const s=(await Promise.all((await V(n.dir,i.id)).map(u=>g(u,null)))).filter(Boolean);if(!Rn(s))continue;const o=s.map(u=>u.verified?.at).filter(Boolean).sort().at(-1);if(!o||s.some(u=>et(u).length))continue;const c=`${n.slug}/${i.id}`;if((await ue(e,c)).asked.some(u=>u.state==="open")||await wn(e,c,i))continue;const l=s.filter(u=>u.worktree?.path&&u.kind!==X).sort((u,p)=>String(p.started).localeCompare(String(u.started)))[0];l&&we(l.worktree.path)&&t.push({key:c,verified:o,kind:Ot(l.kind),model:l.model})}}return t}r(Yu,"landable");async function Xu(e,t){const{source:n,item:a}=await x(e,t);(await Promise.all((await V(n.dir,a.id)).map(c=>g(c,null)))).some(c=>c?.started&&!c.done)||d(`${t} has no open builder; only a card whose lander stopped has one to relaunch`),await Oe(e,t,{handBack:!0,next:"A lander relaunches to land the verified build"});const s=$.join(e,"data","landers.json"),o=await g(s,{});delete o[t],await O.writeFile(s+".tmp",JSON.stringify(o)+`
|
|
39
|
-
`),await O.rename(s+".tmp",s)}r(
|
|
38
|
+
${o}`}}r(kf,"readWaveNotes");async function U(e,t,n,a="main"){const{id:i}=F(t);/^[\w-]{1,40}$/.test(a)||d(`--agent takes a name of letters, digits, "_" and "-", not "${a}"`);const s=a==="main"?`${i}.json`:`${i}@${a}.json`;if(n)return $.join($.resolve(n),"agents",s);const{source:o}=await x(e,t);return $.join(o.dir,"agents",s)}r(U,"builderFile");const Es=r((e,t)=>typeof e=="string"&&/^\d{4}-\d\d-\d\dT/.test(e)&&!Number.isNaN(Date.parse(e))?new Date(e).toISOString():d(`${t} takes an ISO time such as 2026-10-23T16:30:00-07:00`),"isoTime"),Tc=En.progress,Bs=80,Ls="reading, building, testing, checking or landing";function Is(e,t,n,a){e||d(`${a} needs --site, since the line goes to the item's thread`),t!==void 0&&!Tc.includes(t)&&d(`--phase takes ${Ls}, not "${t}"`);const i=v(n,a);return i.length>Bs&&d(`${a} is ${i.length} characters; the cap is ${Bs}. Say it shorter.`),{phase:t,detail:i}}r(Is,"progressLine");const Cs=r((e,t,n)=>t?{detail:e.detail}:(e.phase===void 0&&d(`${n} needs --phase ${Ls}`),e),"forAgent");async function Oa(e,t,n="main",a,{phase:i,detail:s},o){const c=Ne(a),l=c||n==="main"?"":a.kind===ee?n:Kt(a)||a.kind||n,f=c?wo[a.kind]:[l,i].filter(Boolean).join(" "),p=new RegExp(`^(?:${f}|${i??f})\\s*:?\\s+`,"i"),w=f?`${f[0].toUpperCase()}${f.slice(1)}: ${s.replace(p,"")}`:s,u=Z(e,t);(await K(u)).filter(h=>h.kind==="status").pop()?.text!==w&&await de(u,{ts:o,who:"claude",kind:"status",run:a.started,...i?{phase:i}:{},...c?{planner:!0}:{},text:w})}r(Oa,"statusToThread");const qc={prototype:"sonnet",grill:"opus",[X]:"sonnet"};async function Tt(e,t,{label:n,phase:a,progress:i,dir:s,agent:o="main",kind:c,model:l,eta:f,worktree:p,ownWorktree:w,dispatch:u}){a!==void 0&&i===void 0&&d('--phase goes with --progress "<detail>"');const h=se(i,ae=>Is(e,a,ae,"--progress")),m=await U(e,t,s,o),b=new Date().toISOString(),_=se(c,ae=>/^[\w.-]{1,30}$/.test(ae)?ae:d("--kind takes a short word such as claude or codex")),N=Ot(_),j=l??(_==="opus"||_==="sonnet"?_:void 0),A=["build",...Qc].includes(N),D=await g(m,null),M=r(ae=>["tap","hub"].includes(ae?.dispatched)&&!ae.done,"tapOpen");if(u==="session"&&M(D))return{file:m,alreadyOpen:!0,lines:[]};o!=="main"&&_!==X&&_!==ee&&M(await g(await U(e,t,s),null))&&d(`the Implement tap opened ${t}'s main builder record and no agent has taken it up; run builder start ${t} without --agent first`);const I=D?.dispatched&&!D.done?D:null,S=A&&!s?(await x(e,t)).item:void 0,B=D&&et(D).length?D:null,C=I&&N==="build"?{kind:Ot(I.kind),model:I.model,reason:{tap:"The owner's Build tap picked it.",hub:"The board's pull picked it."}[I.dispatched]??"The session's dispatch picked it."}:A?Cn({card:S??{}}):null;C&&N!=="build"&&N!==C.kind&&d(`--kind ${N}: ${C.kind} builds now. ${C.reason} Pass --kind ${C.kind}, or --kind build.`);const T=C?C.kind:N,oe=Ka({dir:e});if(!(I&&!u)&&xo(oe,T))throw new he(yo(oe,T));const R=h&&Cs(h,Ne({kind:T}),"--progress");l!==void 0&&!["opus","sonnet"].includes(l)&&d("--model takes opus or sonnet"),j!==void 0&&T==="codex"&&d("--model is for kind claude; codex needs none");const J=T==="claude"?Tn():"auto";j!==void 0&&J!=="auto"&&j!==J&&d(`--model ${j}: the hub's Claude model is ${J==="opus"?"Opus":"Sonnet"}. Pass --model ${J}, or no --model.`);const te=T==="claude"?j??C?.model??(I&&Ot(I.kind)==="claude"?I.model:void 0)??"opus":Ne({kind:T})?j??(I?.kind===T?I.model:void 0)??qc[T]:T===ee?j??D?.model:void 0,Q={label:n===void 0?"Builder":v(n,"--label"),...T?{kind:T}:{},...te?{model:te}:{},started:b,...f===void 0?{}:{eta:Es(f,"--eta")},...u?{dispatched:u}:{},...B?{calls:B.calls,...B.verified?{verified:B.verified}:{}}:{},...Kc(D)?{session:D.session}:{},...I?.brief&&Ne({kind:T})?{brief:I.brief}:{},...T===ee&&D?.kind===ee?{label:D.label,task:D.task,writes:D.writes,requested:D.requested}:{}},ke=A&&!s&&!z(S)&&S.lane!=="progress",ye=ke?await Ae(e,t,{start:!0}):{lines:[]};let Y,Pe=null;const Mt=w&&A&&o==="main"?[D?.worktree,S?.worktree].find(ae=>ae?.path&&me(ae.path))??null:null,bt=(I??B)?.worktree??Mt,kt=p??(A&&o==="main"&&!bt?w:void 0);try{if(kt){const ae=await V($.dirname($.dirname(m)),F(t).id),_n=(await Promise.all(ae.filter(ge=>ge!==m).map(ge=>g(ge,null)))).filter(ge=>ge?.worktree?.path&&!ge.done).map(ge=>ge.worktree.path);({remade:Pe,...Y}=await kt({busy:_n}))}else Y=bt}catch(ae){throw ke&&await Gc(e,t,S),ae}await Mc(m,D),await y(m,Y?{...Q,worktree:Y}:Q),S?.rebase_run&&o==="main"&&![Ts,Wa].includes(Q.label)&&await qs(e,t),C?.inputs&&e&&Lo(Qa(e),{call:"builder-pick",key:t,result:[T,T==="claude"?te:null].filter(Boolean).join(" "),reason:C.reason,inputs:{...C.inputs,...j?{askedModel:j}:{},...u?{dispatch:u}:{}}}),Pe&&e&&await W(e,t,"worktree-remade"),R&&await Oa(e,t,o,Q,R,b);const re=_===X||_===ee?[]:await wn(e,t,"Acted on: builder started");return e&&A&&await oc(e,t),{file:m,kind:T??null,model:te??null,reason:C?.reason??null,worktree:Y?.path??null,lines:[...N==="build"?[`kind: ${T}. ${C.reason}`]:[],...Kt(Q)?[`model: ${Kt(Q)}`]:[],...e?[`ref: ${await H(e,t)}`]:[],...Y?[`worktree: ${Y.path}`]:[],...Pe?[Pe]:[],...ye.lines,...re]}}r(Tt,"startBuilder");async function Mc(e,t){if(!t?.started||!t.done||!e.endsWith(".json")||$.basename(e).includes("@"))return;const{label:n,kind:a,model:i,started:s,done:o,session:c}=t,l=$.join($.dirname(e),`${$.basename(e,".json")}@past-${Date.parse(s)}.json`);await y(l,{label:n,...a?{kind:a}:{},...i?{model:i}:{},started:s,done:o,...c?.log?{session:{log:c.log}}:{}})}r(Mc,"keepPastRun");async function yf(e,t,n,a="tap"){try{const{held:i,blockers:s}=await Hc(e,t);if(i&&await gs(e,"card-hold",{key:t,hold:!1}),s.length)return await Uc(e,t,s);try{const o=await Tt(e,t,{kind:"build",worktree:n,dispatch:a});return{key:t,ref:await H(e,t),kind:o.kind,...o.model?{model:o.model}:{},reason:o.reason,worktree:o.worktree}}catch(o){throw i&&await zc(e,t,i),o}}catch(i){if(!(i instanceof he))throw i;return await W(e,t,Ma,{why:i.message}),null}}r(yf,"startTappedBuild");async function xf(e,t,{pr:n,conflict:a,base:i},s){const{source:o,itemFile:c,item:l}=await x(e,t);l.rebase_run={pr:n,base:i,conflict:a,at:new Date().toISOString()},await y(o.file,c);try{const f=await Tt(e,t,{kind:"build",label:Ts,dispatch:"hub",ownWorktree:s});return{key:t,kind:f.kind,...f.model?{model:f.model}:{},worktree:f.worktree}}catch(f){throw await qs(e,t),f}}r(xf,"startRebase");const Ts="Rebase";async function qs(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);delete i.rebase_run,await y(n.file,a)}r(qs,"endRebaseRun");async function _f(e,t){const{itemFile:n,item:a}=await x(e,t),i=pe(e,n);return Pt(a,i).find(s=>s.repo===i)?.number??null}r(_f,"cardPR");async function Sf(e,t,{from:n,to:a,replayed:i}){const{source:s,itemFile:o,item:c}=await x(e,t);return c.rebase_run?(c.rebased=[...c.rebased||[],{from:n,to:a,base:c.rebase_run.base,replayed:i,at:new Date().toISOString()}],delete c.rebase_run,await y(s.file,o),!0):!1}r(Sf,"recordRebase");const Wc=r((e,t,n)=>{const a=new Map((e.rebased||[]).filter(i=>i.replayed).map(i=>[i.to,i.from]));for(let i=n,s=0;i&&s<=a.size;i=a.get(i),s++)if(i===t)return!0;return!1},"onlyRebased");async function Hc(e,t){const n=Ka({dir:e});_o(n)&&d(ko(n));const{item:a}=await x(e,t),i=a.held?{held:a.held,pick_up_at:a.pick_up_at,held_at:a.held_at}:null;return i&&we(a)&&d("it's held. Unhold it, then tap Implement."),await pa(e,t)&&d("no take is picked on its prototypes, so it prototypes again. Pick a take from the next round, then tap Implement."),{held:i,blockers:await wc(e,t,a)}}r(Hc,"tapRefusals");async function vf(e,t,n){await Le(e,t,{who:"love",by:n,command:"implement"}),await tc(e,t,{})}r(vf,"implementAsIs");async function Uc(e,t,n){const a=n.join(", ");return await $e({dir:e},"card-bump",{key:t}),await W(e,t,"cant-start-yet",{names:a}),{key:t,ref:await H(e,t),bumped:n,line:`bumped: ${t} waits on ${a}, so it stays in Todo, bumped to the top`}}r(Uc,"bumpBlocked");async function yn(e,t){const n=await U(e,t),a=await g(n,null);return a?.started&&!a.done&&at(a)?{file:n,record:a}:null}r(yn,"openBuild");async function Ms(e,t,{cause:n,words:a,by:i,until:s}){const o=await yn(e,t);return!o||o.record.pause?!1:(await y(o.file,{...o.record,pause:{cause:n,words:a,at:new Date().toISOString(),...i?{by:i}:{},...s?{until:s}:{}}}),!0)}r(Ms,"requestPause");async function Jc(e,t){const n=await yn(e,t),a=n?.record.pause;if(!a||a.paused)return a??null;const i=new Date().toISOString(),s=(await K(Z(e,t))).filter(c=>c.kind==="status"&&c.phase&&!c.planner&&String(c.ts)>=String(n.record.started)).pop()?.phase,o={...a,paused:i,...s?{phase:s}:{}};return await y(n.file,{...n.record,pause:o}),await W(e,t,"paused",{phase:s,words:a.words,clear:Ua[a.cause][0]}),o}r(Jc,"markPaused");async function xn(e,t,{dir:n,agent:a="main"}){if(!e||n||a!=="main")return[];const i=await Jc(e,t);return i?[`Paused: ${i.words}. Commit what you have now with \`git -c core.commentChar=';' commit -F <file>\`, then stop: end your turn and run nothing else. The board resumes this session, telling it so, once the pause clears.`]:[]}r(xn,"pauseReply");async function jf(e,t){const n=await yn(e,t),a=n?.record.pause;if(!a?.paused)return null;const{pause:i,...s}=n.record;return await y(n.file,s),await W(e,t,"resumed",{clear:Ua[a.cause][1]}),{pause:a,record:s}}r(jf,"resumePaused");async function Of(e,t){const n=await yn(e,t);if(!n?.record.pause||n.record.pause.paused)return;const{pause:a,...i}=n.record;await y(n.file,i)}r(Of,"dropPause");async function Af(e,t,n){const a=await U(e,t),i=await g(a,null),s=new Date().toISOString();i&&!i.done&&await y(a,{...i,done:s});const{source:o,itemFile:c,item:l}=await x(e,t);l.lane==="progress"&&Lt(l,"todo",s),await y(o.file,c),await W(e,t,Ma,{why:n})}r(Af,"abandonLaunch");async function Pf(e,t,[n,a],i="main"){const s=await U(e,t,void 0,i),o=await g(s,null);o&&!o.done&&await y(s,{...o,done:new Date().toISOString()}),await wt(e,t),await W(e,t,n,a)}r(Pf,"abandonPlanner");async function Df(e,t,n,a,i="main",s=null){const o=await U(e,t,void 0,i),c=await g(o,null);await y(o,{...c,session:{id:n,pid:a,log:s,at:new Date().toISOString()}})}r(Df,"recordSession");async function Ff(e,t){const{source:n}=await x(e,t);return(await Promise.all((await V(n.dir,F(t).id)).map(i=>g(i,null)))).filter(i=>i&&i.started&&!i.done).map(i=>({pid:i.session?.pid??null,at:i.session?.at??null}))}r(Ff,"openAgents");async function Nf(e,t){const{source:n}=await x(e,t);return(await Promise.all((await V(n.dir,F(t).id)).map(i=>g(i,null)))).filter(i=>i&&i.started&&!i.done).map(i=>({kind:i.kind??null,pid:i.session?.pid??null,at:i.session?.at??null}))}r(Nf,"openRuns");async function Rf(e,t,n){const{source:a}=await x(e,t),i=new Date().toISOString();for(const s of await V(a.dir,F(t).id)){const o=await g(s,null);o?.started&&!o.done&&at(o)&&await y(s,{...o,stopped:{run:n,at:i}})}}r(Rf,"markStopped");async function Ef(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);i.merge_ready&&(i.merge_ready.stopped=new Date().toISOString(),await y(n.file,a))}r(Ef,"stopHeldMerge");async function Bf(e,t,n){const a=v(n,"item stall --reason"),{source:i,itemFile:s,item:o}=await x(e,t);return o.lane==="done"&&d(`${await H(e,t)} is done; there is nothing to stall`),o.stalled={reason:a,at:new Date().toISOString()},await y(i.file,s),a}r(Bf,"markStalled");async function Lf(e,t){const{source:n,itemFile:a,item:i}=await x(e,t);i.stalled&&(delete i.stalled,await y(n.file,a))}r(Lf,"clearStall");async function If(e,t){const{item:n}=await x(e,t),a=await K($.join(e,"data","events.jsonl")),i=r(c=>a.filter(l=>l.kind===c&&l.data?.key===t).map(l=>String(l.ts)).sort().pop()||"","latest"),s=i("card-bump");if(!s||i("card-unbump")>s)return null;const o=[n.closed_at,...Be(n).map(c=>c.merged_at)].filter(Boolean).map(String).sort().pop()||"";return s>o?s:null}r(If,"landFirst");async function Cf(e,t,n){const{item:a}=await x(e,t);z(a)&&d(`${await H(e,t)} is done; there is nothing to bump`);const i=await $e({dir:e},n?"card-bump":"card-unbump",{key:t});return(await K($.join(e,"data","events.jsonl"))).find(s=>s.id===i).ts}r(Cf,"bumpCard");async function Ws(e,t){return(await K(Z(e,t))).filter(n=>n.kind==="status").pop()?.phase==="landing"}r(Ws,"isLanding");const Kc=r(e=>!!e?.session&&e.session.at>(e.done??e.started),"launchedSession");async function Tf(e,t,n,a=null){const i=await U(e,t),s=await g(i,null);if(s&&!s.done)return null;!a&&!await Qi(e,t,n)&&await Vi(e,t,n);const o=await Tt(e,t,{kind:n,dispatch:"hub"}),c=a??await Gi(e,t,n);return await y(i,{...await g(i,null),brief:c}),{key:t,kind:n,model:o.model,brief:c.brief,...c.ask}}r(Tf,"startPlanner");async function qf(e,t){const{source:n}=await x(e,t);if((await Promise.all((await V(n.dir,F(t).id)).map(l=>g(l,null)))).some(l=>l?.kind===X&&l.started&&!l.done))return null;const i=await Qi(e,t,X)??await Vi(e,t,X),s=ho(i.brief),o=await Tt(e,t,{kind:X,agent:s,dispatch:"hub"}),c=await Gi(e,t,X);return await y(o.file,{...await g(o.file,null),brief:c}),{key:t,kind:X,model:o.model,agent:s,brief:c.brief,...c.ask}}r(qf,"startVisual");const Aa=r(e=>typeof e=="string"&&/^[a-z][a-z0-9-]{0,29}$/.test(e)&&e!=="main"&&!e.startsWith(`${X}-`)?e:d('--name takes a short lowercase name such as tests or docs-pass: letters, digits and "-", not main or visual-\u2026'),"helperName");async function Pa(e,t){const{item:n}=await x(e,t);return[(await g(await U(e,t),null))?.worktree,n.worktree].find(i=>i?.path&&me(i.path))??null}r(Pa,"cardWorktree");async function Mf(e,t,{name:n,task:a,writes:i,model:s}){P(e);const o=Aa(n),c=v(a,"--task");s!==void 0&&!["opus","sonnet"].includes(s)&&d("--model takes opus or sonnet");const l=Tn();s!==void 0&&l!=="auto"&&s!==l&&d(`--model ${s}: the hub's Claude model is ${l==="opus"?"Opus":"Sonnet"}. Pass --model ${l}, or no --model.`);const{item:f}=await x(e,t);z(f)&&d(`${await H(e,t)} is Done; a helper needs an open card`);const p=await U(e,t,void 0,o);return me(p)&&d(`${t} already has an agent named ${o}; pick another --name`),i&&!await Pa(e,t)&&d(`a helper that writes branches from ${t}'s worktree, and the card has none; start its build first, or drop --writes`),await y(p,{label:o,kind:ee,task:c,writes:!!i,...s?{model:s}:{},requested:new Date().toISOString()}),await W(e,t,"helper-asked",{agent:o,writes:i}),{lines:[`helper ${o} requested: the board launches it ${i?"once an implementer slot is free":"now"}`,`wait for it with agent wait ${t} --name ${o}`]}}r(Mf,"requestHelper");async function Wf(e,t,n,a){const i=await U(e,t,void 0,n),s=await g(i,null);if(s?.kind!==ee||s.started||s.done)return null;const{item:o}=await x(e,t),c=Cn({card:o}),l=Tn(),f=s.model??(l!=="auto"?l:c.kind==="claude"?c.model:"opus"),p=s.writes?await Pa(e,t):null;s.writes&&!p&&d(`${t} has no worktree for helper ${n} to branch from`),await y(i,{...s,model:f});const w=await Tt(e,t,{agent:n,kind:ee,dispatch:"hub",worktree:p?({busy:u})=>a({repo:p.repo,base:p.branch,busy:u}):void 0});return{key:t,kind:ee,model:f,agent:n,task:s.task,writes:s.writes,worktree:w.worktree}}r(Wf,"startHelper");async function Hf(e,t,n){const a=await g(await U(e,t,void 0,Aa(n)),null);return a?.kind!==ee&&d(`${t} has no helper named ${n}`),{record:a,card:await Pa(e,t)}}r(Hf,"helperOf");async function Uf(e,t,{name:n,agent:a="main",timeout:i=9,every:s=2e3,alive:o}){P(e);const c=Number(i);c>0||d("--timeout takes minutes, a number above 0");const l=await U(e,t,void 0,Aa(n)),f=await U(e,t,void 0,a),p=r(async u=>{const h=await g(f,null);if(!h?.started||h.done)return;const{waits:m,...b}=h;await y(f,u?{...b,waits:u}:b)},"mark"),w=Date.now()+c*6e4;await p(n);try{for(;;){const u=await g(l,null);if(u?.kind!==ee&&d(`${t} has no helper named ${n}`),u.done){const m=u.writes&&u.worktree?[`branch ${u.worktree.branch} in ${u.worktree.path}: merge it with agent merge ${t} --name ${n}`]:[];return{lines:[`helper ${n} done: ${u.note??"no hand-back"}`,...m]}}const h=u.session?.pid??null;h&&!o(h)&&d(`helper ${n}'s process exited without builder done; do its task yourself`),Date.now()>=w&&d(`helper ${n} is still ${u.started?"running":u.writes?"waiting for an implementer slot":"waiting to launch"} after ${c} minutes; run agent wait again`),await new Promise(m=>setTimeout(m,s))}}finally{await p(null)}}r(Uf,"waitHelper");async function Jf(e,t,n="main"){return n==="main"?!1:(await g(await U(e,t,void 0,n),null))?.kind===ee}r(Jf,"isHelper");async function Kf(e,t,n="main"){const a=await g(await U(e,t,void 0,n),null);return!!(a?.started&&!a.done&&Ne(a))}r(Kf,"isPlanner");async function zf(e,t,{file:n,title:a}){P(e);const i=v(a,"--title");(typeof n!="string"||!n.endsWith(".html"))&&d("--file takes the page, an .html file");const s=await O.readFile($.resolve(n),"utf8").catch(()=>d(`--file ${n} can't be read`));s.trim()||d(`--file ${n} is empty`),await x(e,t);const{id:o}=F(t),c=$.join(e,"public","visuals",o);await O.mkdir(c,{recursive:!0});const l=i.toLowerCase().replace(/[^a-z0-9]+/g,"-").slice(0,60).replace(/^-+|-+$/g,"")||"visual";let f=`${l}.html`;for(let w=2;me($.join(c,f));w++)f=`${l}-${w}.html`;await O.writeFile($.join(c,f),s,{flag:"wx"});const p=`visuals/${o}/${f}`;return await de(Z(e,t),{ts:new Date().toISOString(),who:"claude",kind:X,text:v(i,"--name"),link:Hn(p,"--link")}),{lines:[p]}}r(zf,"addVisual");async function Gf(e){const t=[];for(const n of await q(e)){const a=await g(n.file,null);for(const i of a?.items||[]){if(z(i))continue;const s=await g($.join(n.dir,"agents",`${i.id}.json`),null);!s?.started||s.done||!s.dispatched||Ne(s)||t.push({key:`${n.slug}/${i.id}`,kind:Ot(s.kind),model:s.model})}}return t}r(Gf,"cutOffBuilds");async function zc(e,t,n){const{source:a,itemFile:i,item:s}=await x(e,t);for(const o of Object.keys(n))n[o]===void 0?delete s[o]:s[o]=n[o];await y(a.file,i)}r(zc,"restoreHold");async function Gc(e,t,n){const{source:a,itemFile:i,item:s}=await x(e,t);for(const o of["lane","lane_at","closed_at","reopened_at"])n[o]===void 0?delete s[o]:s[o]=n[o];await y(a.file,i)}r(Gc,"restoreLane");async function Qf(e,t){return e||d("audit reads a board: pass --site <route>"),t!==void 0&&F(t),{lines:Io(Co(Qa(e),{key:t}))}}r(Qf,"auditLog");const Qc=["codex","claude"];async function Vf(e,t){const{item:n}=await x(e,t),{kind:a,model:i,reason:s}=Cn({card:n});return{lines:[`${[a,i].filter(Boolean).join(" ")}: ${s}`]}}r(Vf,"pickBuilder");async function Hs(e,t,{dir:n}={}){const{id:a}=F(t),i=$.dirname(await U(e,t,n)),s=await V($.dirname(i),a);return(await Promise.all(s.map(c=>g(c,null)))).filter(c=>c?.worktree?.path&&c.kind!==ee).sort((c,l)=>String(l.started).localeCompare(String(c.started)))[0]?.worktree??null}r(Hs,"recordedWorktree");async function Zf(e,t){const{source:n}=await x(e,t),i=(await Promise.all((await V(n.dir,F(t).id)).map(s=>g(s,null)))).filter(s=>s?.session?.id&&s.worktree?.path&&s.kind!==ee).sort((s,o)=>String(o.started).localeCompare(String(s.started)))[0];return i?{id:i.session.id,worktree:i.worktree.path}:null}r(Zf,"recordedSession");async function Yf(e,t,n,{phase:a,dir:i,agent:s="main",eta:o,clearEta:c}){o!==void 0&&c&&d("pass --eta or --clear-eta, not both");const l=Is(e,a,n,"builder progress"),f=await U(e,t,i,s),p=await g(f,null);(!p?.started||p.done)&&d(`builder progress needs an open builder record on ${t}; run builder start first`);const w=Cs(l,Ne(p),"builder progress");return o!==void 0&&(p.eta=Es(o,"--eta")),c&&delete p.eta,(o!==void 0||c)&&await y(f,p),await Oa(e,t,s,p,w,new Date().toISOString()),{file:f,lines:await xn(e,t,{dir:i,agent:s})}}r(Yf,"builderProgress");async function Xf(e,t){const{source:n}=await x(e,t),a=Z(e,t);if((await K(a)).filter(o=>o.kind==="status").pop()?.phase==="landing")return;const s=(await Promise.all((await V(n.dir,F(t).id)).map(o=>g(o,null)))).filter(o=>o?.started&&at(o)&&o.label!==Wa).map(o=>o.started).sort().pop();await de(a,{ts:new Date().toISOString(),who:"claude",kind:"status",phase:"landing",text:"Landing: rebasing onto main and landing",...s?{run:s}:{}})}r(Xf,"landingLine");const et=r(e=>(e?.calls||[]).filter(t=>!e.verified||t.at>e.verified.at),"unverifiedCalls");async function eu(e,t,n,{dir:a,agent:i="main"}){const s=v(n,"builder call");/\n/.test(s)&&d("builder call takes one line; record each call on its own");const o=await U(e,t,a,i),c=await g(o,null);return(!c?.started||c.done)&&d(`builder call needs an open builder record on ${t}; run builder start first`),await y(o,{...c,calls:[...c.calls||[],{at:new Date().toISOString(),text:s}]}),{file:o,lines:await xn(e,t,{dir:a,agent:i})}}r(eu,"recordCall");async function qt(e,t){const n=await K(Z(e,t)),a=new Map(n.filter(i=>i.kind===Ut.answer).map(i=>[i.ask,i]));return n.filter(i=>i.kind===Ut.ask).map(i=>{const s=a.get(i.ask);return{id:i.ask,agent:i.agent||"main",text:i.text,at:i.ts,answer:s?{text:s.text,at:s.ts}:null,deferred:!!s?.deferred}})}r(qt,"builderAsks");const Vc=r(e=>be().find(t=>Re(t.dir,e))??{dir:e},"boardTarget");async function tu(e,t,n,{agent:a="main"}){P(e);const i=v(n,"builder ask");/\n/.test(i)&&d("builder ask takes one line; ask each question on its own");const s=await g(await U(e,t,void 0,a),null);(!s?.started||s.done)&&d(`builder ask needs an open builder record on ${t}; run builder start first`);const o=`ask${(await qt(e,t)).length+1}`;return await de(Z(e,t),{ts:new Date().toISOString(),who:`${Kt(s)||s.kind||"Claude"} builder`,kind:Ut.ask,ask:o,agent:a,text:i}),await $e(Vc(e),"builder-ask",{key:t,ask:o,agent:a,text:i}),{id:o,lines:await xn(e,t,{agent:a})}}r(tu,"askOrchestrator");async function nu(e,t,n,a,{deferred:i=!1}={}){P(e);const s=v(a,"--text"),o=(await qt(e,t)).find(l=>l.id===n)||d(`no ask "${n}" on ${t}; builder asks ${t} lists them`);o.answer&&d(`${n} on ${t} is already ${o.deferred?"deferred":"answered"}`);const c=new Date().toISOString();if(await de(Z(e,t),{ts:c,who:"claude",kind:Ut.answer,ask:n,...i?{deferred:!0}:{},text:s}),i){const l=await U(e,t,void 0,o.agent),f=await g(l,null);f&&await y(l,{...f,calls:[...f.calls||[],{at:c,text:`Question: ${o.text} (deferred to the owner)`}]})}return{lines:[]}}r(nu,"answerBuilder");async function au(e,t,{wait:n=!1,timeout:a=9,every:i=2e3}={}){P(e);const s=Number(a);s>0||d("--timeout takes minutes, a number above 0");const o=Date.now()+s*6e4;for(;;){const c=await qt(e,t);if(!c.length)return{lines:["no asks yet"]};const l=c.filter(w=>!w.answer),f=c.flatMap(w=>[`${w.id}. ${w.text}`,` ${w.answer?`${w.deferred?"deferred":"answered"}: ${w.answer.text}`:"waiting on the orchestrator"}`]);if(!n||!l.length)return{lines:f};const p=await xn(e,t,{});if(p.length)return{lines:[...f,...p]};Date.now()>=o&&d(`${l.map(w=>w.id).join(", ")} still waiting on the orchestrator after ${s} minutes; run builder asks ${t} --wait again`),await new Promise(w=>setTimeout(w,i))}}r(au,"listAsks");async function Zc(e,t,n){const a=t.topic.startsWith("item:")&&!t.topic.startsWith("item:ask:")?t.topic.slice(5):d(`${n}.ask goes on a card's question, topic item:<file>/<id>`),i=(await qt(e,a)).find(s=>s.id===t.ask)||d(`${n}.ask names no ask "${t.ask}" on ${a}; builder asks ${a} lists them`);i.deferred||d(`${n}.ask names ${i.id}, which the orchestrator ${i.answer?"answered":"has not deferred"}; only a deferred ask goes to the owner`)}r(Zc,"checkAskQuestion");async function iu(e,t,{dir:n,agent:a="main"}){const i=await g(await U(e,t,n,a),null),s=i?.calls||[];if(!s.length)return{lines:["no judgment calls recorded"]};const{verified:o}=i,c=et(i).length?"not verified yet":`verified ${o.at}${o.note?`: ${o.note}`:""}`;return{lines:[...s.map((l,f)=>`${f+1}. ${l.text}`),c]}}r(iu,"listCalls");async function su(e,t,{dir:n,agent:a="main",note:i}){const s=await U(e,t,n,a),o=await g(s,null);if(et(o).length||d(`${t} has no unverified judgment calls${a==="main"?"":` from agent ${a}`}; builder calls ${t} lists them`),e){const c=new Set((await g(L(e),{items:[]})).items?.filter(f=>f.topic===`item:${t}`&&f.ask).map(f=>f.ask)),l=(await qt(e,t)).find(f=>f.deferred&&f.agent===a&&!c.has(f.id));l&&d(`${l.id} on ${t} was deferred to the owner; add its question with "ask": "${l.id}" first`)}return await y(s,{...o,verified:{at:new Date().toISOString(),...i===void 0?{}:{note:v(i,"--note")}}}),s}r(su,"verifyCalls");async function ou(e,t){const{source:n,item:a}=await x(e,t),i=r((o,c)=>[(o.options||[]).find(l=>l.id===c.choice)?.label,c.note].filter(Boolean).join(": ")||"answered","said"),s=(await ue(e,t)).asked.filter(o=>o.ask&&o.answers.length).map(o=>({ask:o.ask,question:o.question,answer:i(o,o.answers.at(-1))}));return{key:t,ref:await H(e,t),title:a.title||t,notes:a.plan?$.join(n.dir,a.plan):null,thread:Z(e,t),worktree:(await Hs(e,t))?.path??null,answers:s,links:await Yc(e,t),rebasePR:a.rebase_run?.pr??null,prototype:a.gates?.prototype??null}}r(ou,"briefFacts");async function Yc(e,t){const n=new Map;for(const c of await q(e))for(const l of(await g(c.file,null))?.items||[])n.set(`${c.slug}/${l.id}`,{entry:c,it:l});const a=F(t).file,{it:i}=n.get(t),s=[...n].filter(([,c])=>(c.it.related||[]).some(l=>G(c.entry.slug,l)===t)).map(([c])=>c),o=[...i.parent?[["parent",G(a,i.parent)]]:[],...(i.deps||[]).map(c=>["depends on",G(a,c)]),...[...new Set([...(i.related||[]).map(c=>G(a,c)),...s])].map(c=>["related",c])].filter(([,c])=>n.has(c));return Promise.all(o.map(async([c,l])=>{const{entry:f,it:p}=n.get(l);return{relation:c,ref:await H(e,l),title:p.title||l,lane:We[p.lane||"todo"],notes:p.plan?$.join(f.dir,p.plan):null,thread:Z(e,l)}}))}r(Yc,"cardLinks");async function ru(e){const t=[];for(const n of await q(e)){const a=await g(n.file,null);for(const i of a?.items||[]){if(z(i))continue;const s=(await Promise.all((await V(n.dir,i.id)).map(f=>g(f,null)))).filter(Boolean);if(!Bn(s))continue;const o=s.map(f=>f.verified?.at).filter(Boolean).sort().at(-1);if(!o||s.some(f=>et(f).length))continue;const c=`${n.slug}/${i.id}`;if((await ue(e,c)).asked.some(f=>f.state==="open")||await mn(e,c,i))continue;const l=s.filter(f=>f.worktree?.path&&f.kind!==ee).sort((f,p)=>String(p.started).localeCompare(String(f.started)))[0];l&&me(l.worktree.path)&&t.push({key:c,verified:o,kind:Ot(l.kind),model:l.model})}}return t}r(ru,"landable");async function cu(e,t){const{source:n,item:a}=await x(e,t);(await Promise.all((await V(n.dir,a.id)).map(c=>g(c,null)))).some(c=>c?.started&&!c.done)||d(`${t} has no open builder; only a card whose lander stopped has one to relaunch`),await Ae(e,t,{handBack:!0,next:"A lander relaunches to land the verified build"});const s=$.join(e,"data","landers.json"),o=await g(s,{});delete o[t],await O.writeFile(s+".tmp",JSON.stringify(o)+`
|
|
39
|
+
`),await O.rename(s+".tmp",s)}r(cu,"relaunchLander");async function lu(e,t,{dir:n}={}){const{id:a}=F(t),i=await V($.dirname($.dirname(await U(e,t,n))),a),s=[];for(const o of i){if(!et(await g(o,null)).length)continue;const c=$.basename(o,".json").slice(String(a).length+1)||"main";s.push(c==="main"?"":` --agent ${c}`)}s.length&&d(`land: ${t} has judgment calls nobody verified yet; check them with ${s.map(o=>`builder calls ${t}${o}`).join(", ")}, then run ${s.map(o=>`builder verified ${t}${o}`).join(", ")}`)}r(lu,"checkCallsVerified");async function du(e,t,{dir:n,agent:a="main",next:i}){const s=await g(await U(e,t,n,a),null),o=s&&!s.done?et(s):[];if(!o.length)return null;i!==void 0&&d(`builder done sets ${t}'s Next itself while its record has judgment calls; run it without --next`);const c=o.some(f=>f.text.startsWith("Question:"))?"Answer the builder's questions":"Verify the builder's judgment calls",l=await Ae(e,t,{handBack:!0,next:c});return{lines:[`handed back: Next "${c}"`,...l.lines]}}r(du,"handBackCalls");async function fu(e,t,{agent:n="main"}){if(n!=="main")return!1;const a=await U(e,t,void 0,n),i=await g(a,null);if(!i?.started||i.done||!i.worktree?.path)return!1;const{id:s}=F(t);for(const o of await V($.dirname($.dirname(a)),s)){const c=o===a?null:await g(o,null);if(!c?.started||c.done||c.kind==="install")continue;const l=$.basename(o,".json").slice(String(s).length+1);d(`builder done would finish ${t} while agent ${l} is still open; run builder done ${t} --agent ${l} first`)}return!0}r(fu,"finishesCard");async function uu(e,t){const{source:n,item:a}=await x(e,t);if(z(a))return null;const i=(await Promise.all((await V(n.dir,a.id)).map(o=>g(o,null)))).filter(Boolean);if(!Bn(i))return{held:"the build hasn't handed back; run builder done first"};if(i.some(o=>et(o).length))return{held:"a builder record still has unverified judgment calls"};if(i.some(o=>o.started&&!o.done&&o.kind!=="install"))return{held:"a builder record is still open"};const s=await Hs(e,t);return!s||!me(s.path)?{held:"the build's worktree is gone"}:{waits:(await ue(e,t)).asked.filter(o=>o.state==="open").map(o=>o.id)}}r(uu,"verifiedFinish");async function wu(e,t){const{item:n}=await x(e,t);if(n.reopened_at&&d(`the owner reopened ${await H(e,t)}; it closes on the owner's Done`),n.lane!=="done"||Be(n).some(s=>s.merged_at))return;const a=String(n.closed_at||"");(await K($.join(e,"data","events.jsonl"))).some(s=>s.kind==="item-message"&&s.data?.key===t&&String(s.ts)>a)&&d(`the owner reopened ${await H(e,t)} with a message; it closes on the owner's Done`)}r(wu,"checkNotReopened");async function V(e,t){const n=$.join(e,"agents");return(await O.readdir(n).catch(()=>[])).filter(a=>a===`${t}.json`||a.startsWith(`${t}@`)&&!a.startsWith(`${t}@past-`)&&a.endsWith(".json")).map(a=>$.join(n,a))}r(V,"builderFiles");async function Us(e,t,n,{handedBack:a,endHandBack:i=!1,held:s=!1}={}){const o=new Date().toISOString();for(const c of await V(e,t)){const l=await g(c,null);if(!l)continue;if(l.done||l.kind==="install"&&!n){if(i&&l.handed_back){const{handed_back:u,...h}=l;await y(c,h)}continue}if(s&&l.pause)continue;const{eta:f,handed_back:p,...w}=l;await y(c,{...w,done:o,...a?{handed_back:a}:{},...s&&l.started?{hold_stopped:!0}:{}})}}r(Us,"closeBuilders");async function pu(e,t){const n=await U(e,t),a=await g(n,null);if(!a?.hold_stopped)return null;const{hold_stopped:i,...s}=a;return await y(n,s),{kind:a.kind??null,label:a.label??null,brief:a.brief??null}}r(pu,"takeHoldStopped");async function Js(e,t,{dir:n,agent:a,startedBetween:i,note:s,next:o,requireNext:c=!1}){const l=await U(e,t,n,a),f=await g(l,null);if(!f||f.done)return l;if(i){const M=Date.parse(f.started);if(!(M>=i[0]&&M<=i[1]))return l}const{eta:p,...w}=f,u=new Date().toISOString(),h={...w,done:u,...s?{note:s}:{}},{id:m}=F(t),b=$.dirname($.dirname(l)),_=await Promise.all((await V(b,m)).map(M=>M===l?h:g(M,null))),N=!_.some(M=>M?.started&&!M.done),j=n?{file:$.join(b,"wave.json")}:(await x(e,t)).source,A=await g(j.file,null),D=A?.items?.find(M=>M.id===m);return o!==void 0&&(ts(D||d(`${t} has no item to set --next on`),o,await pn(e,t)),e&&await ds(e,t,D,D.next)),c&&D&&Ln(D,_)&&d(`builder done closes ${t}'s last open builder, which leaves it In Progress with no Next; pass --next "<what happens next>", or record a judgment call with builder call ${t} or land`),await y(l,h),await wt(e,t,{screen:f.kind==="grill"}),s&&await Oa(e,t,a,f,{detail:s},u),o!==void 0&&await y(j.file,A),l}r(Js,"stopBuilder");async function mu(e,t,n){const a=(await g(await U(e,t,void 0,n.agent),null))?.kind,i=await Js(e,t,n),{phase:s,gates:o}=(await x(e,t)).item,c=await pn(e,t)||!!(await g(fe(e),{}))[t];if(s!=={grill:"grilling",prototype:"prototyping"}[a]||c)return{file:i,lines:[]};if(a==="grill"&&o?.prototype&&await ca(e,t))return{file:i,lines:[]};const{line:l,lines:f}=await W(e,t,"planner-unasked",{kind:a});return{file:i,lines:[l.text,...f]}}r(mu,"stopPlanner");async function Xc(e){const t=[];for(const n of await q(e)){const a=await g(n.file,null);for(const i of a?.items||[])for(const s of await V(n.dir,i.id)){const o=await g(s,null);if(!o?.started||o.done)continue;const c=$.basename(s,".json");t.push({key:`${n.slug}/${i.id}`,agent:c.slice(String(i.id).length+1)||"main",label:o.label||"Builder",started:o.started})}}return t}r(Xc,"openBoardBuilders");async function hu(e){const t=await Xc(e);for(const n of t)await Js(e,n.key,{agent:n.agent,note:"Board closed. Builder record stopped; work is kept for reopening."});return t}r(hu,"closeBoardBuilders");export{he as BoardError,li as PUSH_LINE_MAX,Xt as REPLY_EXTS,fr as SCREEN_AFTER,zn as UPLOAD_LIMIT,Cl as WORKING_KINDS,Af as abandonLaunch,Pf as abandonPlanner,nd as ackPing,Ac as addItem,Id as addNotes,Ad as addPrototype,Nd as addQuestions,md as addReviewers,_d as addRuleTo,Od as addTopic,zf as addVisual,nu as answerBuilder,Vl as answerCard,Ge as appendMessage,W as appendStatus,pf as archiveCards,wf as archiveCount,bn as archiveFile,kn as archiveRank,tu as askOrchestrator,Gd as askedGate,Qf as auditLog,mn as blockWords,ie as boardConfig,Fr as boardPosts,pe as boardRepoOf,ef as breakOwnerCheck,ou as briefFacts,rf as builderModel,Yf as builderProgress,Cf as bumpCard,H as cardName,_f as cardPR,$f as changeWave,lu as checkCallsVerified,zd as checkClose,wu as checkNotReopened,nc as clearImplementReady,Lf as clearStall,hu as closeBoardBuilders,gf as createWave,Gf as cutOffBuilds,Ql as dropAskCard,Of as dropPause,Sd as editRuleIn,Jl as eventCard,kd as fallBackFromLinear,Kl as fileAsk,zl as fileAsks,ad as finishHandshake,bf as finishWave,fu as finishesCard,du as handBackCalls,cd as heardThreads,sf as heldMerge,Hf as helperOf,di as hiddenFile,vf as implementAsIs,Vd as isFinished,Jf as isHelper,Ws as isLanding,Kf as isPlanner,q as itemSources,If as landFirst,ru as landable,Xf as landingLine,au as listAsks,iu as listCalls,Pd as listPrototypes,xd as listRules,ld as markHeard,Jc as markPaused,Wl as markRead,Bf as markStalled,Rf as markStopped,Tl as markWorking,af as mergeCard,kc as mergedLine,en as messageText,cf as migrateFollowups,df as migrateGates,ff as migratePhases,lf as migrateStatuses,hf as migrateWaveNames,Xl as moveAsk,Qd as moveCard,Ud as moveQuestions,Ff as openAgents,Xc as openBoardBuilders,Nr as openDayThread,Nf as openRuns,bd as opensPRs,Md as oweBrief,F as parseKey,Xd as passOwnerCheck,Vf as pickBuilder,Yd as pickUp,qd as planAgain,pd as postDigest,td as postIncomingReview,sd as postLink,nf as postPR,tf as prReport,Ed as publishScreened,Bd as publishUnscreened,yr as pushLine,xr as pushUrl,qe as readArchive,ve as readAskLinks,ia as readBoardRules,Er as readBoardSettings,Ai as readHandshake,Sr as readHidden,g as readJSON,vr as readMarks,ui as readMarksFile,us as readNotes,ta as readPRSlack,Nt as readPrototypes,Wi as readScreening,Ul as readUpload,kf as readWaveNotes,pr as readWorking,eu as recordCall,gs as recordOwnerTap,hr as recordPush,od as recordReaction,Sf as recordRebase,ed as recordReview,Df as recordSession,Zf as recordedSession,Hs as recordedWorktree,Zl as relateAsk,cu as relaunchLander,Fd as removePrototype,hd as removeReviewers,vd as removeRuleFrom,mr as repliesSincePush,Un as replyFilesOf,Mf as requestHelper,Ms as requestPause,Jd as resolveNoteAnswers,Hd as resolveQuestion,jf as resumePaused,Gl as retitleAskCard,Pr as reviewDigest,rd as reviewDue,jd as saveBoardRules,ft as saveBoardSettings,gd as saveBoardShare,Hl as saveUpload,ql as sendPush,Dd as servedRoundsOpen,$d as setBoardPause,yd as setBoardSetting,Ml as setHidden,Ae as setItem,Zd as setMainInstall,id as setReviewers,Br as settingsLines,je as settingsOf,Ld as settleAnswers,Rd as settleQuestion,Ll as siteDir,dr as sitesDir,fd as slackAnswer,ud as slackSay,wd as slackTask,dd as slackThread,Cd as splitSharedNotes,tc as stageImplement,uf as stageImplementAsks,Td as stageReply,Tt as startBuilder,Wf as startHelper,Tf as startPlanner,xf as startRebase,yf as startTappedBuild,qf as startVisual,Js as stopBuilder,Ef as stopHeldMerge,mu as stopPlanner,mf as stripDeadFields,ra as takeFile,pu as takeHoldStopped,of as tapMerge,Z as threadFile,Xi as topicQuestions,Yl as unfoldAsk,Kd as unrecorded,uu as verifiedFinish,su as verifyCalls,Uf as waitHelper,_a as waveNoteFile,Zt as wavesDir,kr as withReplies,Wd as writeBrief,Il as writeReply};
|