@lovek4d/kickflip 0.10.1 → 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 +19 -16
- package/SKILL.md +7 -7
- package/bin/event-lines.mjs +1 -1
- package/board/bin/board.mjs +30 -17
- 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 +25 -27
- package/board/public/col-resize.js +25 -44
- package/board/public/dialogs.js +2 -2
- package/board/public/new-task.js +2 -2
- 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 +47 -95
- package/board/public/ui-cards.js +29 -22
- package/board/public/ui-enums.js +3 -3
- 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 +10 -2
- 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 +32 -32
- package/hub/hub.mjs +8 -8
- package/hub/prompts.mjs +2 -2
- package/hub/review-send.mjs +1 -1
- package/hub/slack.mjs +4 -2
- package/hub/views.mjs +1 -1
- 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(
|
|
3
|
-
`):
|
|
4
|
-
`)})},"writeJSON");async function
|
|
5
|
-
`)}
|
|
6
|
-
`).filter(
|
|
7
|
-
`)),{exitCode:p});return await
|
|
8
|
-
`).map(n=>n.replace(/\s+/g," ").trim()).find(Boolean)||"";return
|
|
9
|
-
`)[0].trim()||(
|
|
10
|
-
`),
|
|
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})}
|
|
16
|
-
`);return[`${l.ts===a.ts?"\u2192 ":""}${f}: ${p}`,...w.map(u=>` ${u}`)]})}}
|
|
17
|
-
`)}]:n.items.map(m=>({pr:m.pr,text:`${l} ${m.text}`})))});const f=r(await g(s,{})),p=f.ts||await an(s,r,()=>o({channel:f.channel,text:i(f.replies.length)}));for(const[u,h]of f.replies.entries())nn(h)||await an(s,m=>r(m).replies[u],()=>o({channel:f.channel,thread_ts:p,text:h.text}));const w=r(await g(s,{})).replies.filter(u=>u.pr&&u.ts);w.length&&await Et(en(t),u=>{const h=`digest-${e}`;for(const m of w){const k=u[m.pr]||={links:{},replies:[]},_=k.replies.find(N=>N.id===h)||k.replies[k.replies.push({id:h,text:m.text,to:{}})-1];_.to[n.name]||={channel:f.channel,ts:m.ts,reactions:[]}}})}c(od,"postDigest");function sd(t,e){const n=Yn(Array.isArray(e)?e:[e],"the reviewer file");return Xn(t,a=>[...a.map(i=>n.find(o=>de(o.name,i.name))||i),...n.filter(i=>!a.some(o=>de(i.name,o.name)))])}c(sd,"addReviewers");function rd(t,e){return Xn(t,n=>{const a=e.filter(i=>!n.some(o=>de(o.name,i)));return a.length&&d(`this board has no reviewer ${a.map(i=>`"${i}"`).join(", ")}`),n.filter(i=>!e.some(o=>de(i.name,o)))})}c(rd,"removeReviewers");const Pr=c(()=>$.join(Nn.homedir(),".claude","kickflip","settings.json"),"hubSettingsFile"),na=c(t=>{try{return t()}catch(e){throw e instanceof Ys&&d(e.message),e}},"ruleCall"),aa=c(async t=>ai(await g($.join(t,"board.json"),{})),"readBoardRules");let Ni=Promise.resolve();function Fe(t,e){const n=Ni.then(async()=>{const a=$.join(P(t),"board.json"),i=await g(a,{}),{set:o,...s}=e(i);return await b(a,{...i,...o}),{file:a,...s}});return Ni=n.catch(()=>{}),n}c(Fe,"writeBoardJSON");const Ri=c((t,e)=>Fe(t,n=>{const a=na(()=>e(ai(n)));return{set:{rules:a.rules},...a}}),"writeBoardRules");function cd(t,e){const n=Object.keys(e||{}).sort().join(),a=n==="level,login",i=a?e.login:e?.[n],o=typeof i=="string"?i.trim():null;(!["add","remove","level,login"].includes(n)||!or(o))&&d("Send add or remove with one login, like name@github, or a listed login with its level."),a&&!["view","full"].includes(e.level)&&d("A level is view or full.");const s=c(({login:r,level:l})=>l==="full"?{login:r,level:l}:r,"stored");return Fe(t,r=>{const l=ir(r);a&&!l.some(w=>w.login===o)&&d(`${o} isn't shared with yet. Add them first.`);const p=(n==="remove"?l.filter(w=>w.login!==o):a?l.map(w=>w.login===o?{login:o,level:e.level}:w):l.some(w=>w.login===o)?l:[...l,{login:o,level:"view"}]).map(s);return{set:{share:p},share:p}}).then(({share:r})=>({share:r}))}c(cd,"saveBoardShare");const Ei=c(t=>typeof t=="string"&&/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(t),"validGithubLogin"),Li=c(t=>{try{return t?Ha(t):null}catch{return null}},"storedTickets"),sn=c(t=>Number.isInteger(t)&&t>=1&&t<=99,"validCap"),ia=["max_implementers","max_planners","max_serves"],Ot=c((t,e)=>{const n=Li(t?.tickets);return{auto_merge:t?.auto_merge===!0,auto_build:t?.auto_build===!0,auto_pull:t?.auto_pull===!0,max_implementers:sn(t?.max_implementers)?t.max_implementers:is,max_planners:sn(t?.max_planners)?t.max_planners:os,max_serves:sn(t?.max_serves)?t.max_serves:ss,auto_triage:t?.auto_triage===!0,auto_review:t?.auto_review===!0,idle_notification:t?.idle_notification===!0,skip_draft:t?.skip_draft===!0,prs:e?.hub?t?.prs===!0:t?.prs!==!1,...n?{tickets:n}:{},...Ei(t?.github_login)?{github_login:t.github_login}:{},...ni(t?.build_target)?{build_target:t.build_target}:{}}},"settingsOf");async function ld(t,e,n=new Date){let a;try{a=bs(e,n)}catch(i){d(i.message)}return await Fe(t,()=>({set:{paused_until:a??void 0}})),a}c(ld,"setBoardPause");async function Dr(t){const e=P(t);return Ot(await g($.join(e,"board.json"),{}),kt().find(n=>Ft(n.dir,e)))}c(Dr,"readBoardSettings");const dd=c((t,e)=>!!t?.github&&(!t.local||t.hub===!0)&&e.prs,"opensPRs");async function ue(t,e){ce(e,["auto_merge","auto_build","auto_pull","max_implementers","max_planners","max_serves","auto_triage","auto_review","idle_notification","skip_draft","prs","tickets","github_login","build_target"],"the settings"),Object.keys(e).length||d("the settings name nothing to change"),e={...e};for(const[n,a]of Object.entries(e))if(n==="tickets"){try{e.tickets=Ha(a)??void 0}catch(o){d(o.message)}e.tickets?.source==="linear"&&!Fs()&&d(Ps);const i=e.tickets?.source==="github"&&t?kt().find(o=>Ft(o.dir,t)):null;i&&!i.github&&d("GitHub issues need the board\u2019s repo on GitHub, and this board\u2019s repo isn\u2019t; pick none or linear")}else n==="github_login"?a===""||a===null?e.github_login=void 0:Ei(a)||d("github_login must be a GitHub login"):n==="build_target"?a===null?e.build_target=void 0:ni(a)||d('build_target must name .kickflip/build-<target>: letters, digits, ".", "_" or "-", or null for .kickflip/build'):ia.includes(n)?sn(a)||d(`${n} must be a whole number from 1 to 99`):Ze(a,n);if(e.prs===!1){const n=t?kt().find(a=>Ft(a.dir,t)):null;n?.github&&!n.local&&!(await Qs(n.owner?.repo)).pushMain&&d("Merge lands each card on main and pushes it, so turn on Push main after a land first")}return Fe(t,n=>{const a=t?kt().find(s=>Ft(s.dir,t)):null,i=Ot(n,a),o=Ot({...n,...e},a);return{set:e,settings:o,changed:Object.keys(e).filter(s=>JSON.stringify(i[s])!==JSON.stringify(o[s]))}})}c(ue,"saveBoardSettings");async function fd(){if(Ds().set)return[];const t=[];for(const e of kt())(await Dr(e.dir)).tickets?.source==="linear"&&(await ue(e.dir,{tickets:{source:"none"}}),await $t(e,"settings",{tickets:null}),t.push(`/${e.route} fell back to no ticket source: Hub has no Linear key`));return t}c(fd,"fallBackFromLinear");async function ud(t,e,n){const a=String(e).replace(/-/g,"_"),i=["auto_merge","auto_build","auto_pull","max_implementers","max_planners","max_serves","auto_triage","auto_review","idle_notification","skip_draft","prs","github_login","tickets","build_target"];if(i.includes(a)||d(`no board setting "${e}"; settings are ${i.map(r=>r.replace(/_/g,"-")).join(", ")}`),a==="tickets"){let r;try{r=JSON.parse(n)}catch{d('settings set tickets takes the source as JSON, such as {"source":"github"}')}const{file:l,settings:f}=await ue(t,{tickets:r});return{file:l,lines:Fr({tickets:f.tickets})}}if(a==="github_login"){const{file:r,settings:l}=await ue(t,{github_login:n});return{file:r,lines:[`github-login ${l.github_login||"unset"}`]}}if(a==="build_target"){const{file:r,settings:l}=await ue(t,{build_target:n==="default"?null:n});return{file:r,lines:[`build-target ${l.build_target||"default"}`]}}if(ia.includes(a)){const{file:r,settings:l}=await ue(t,{[a]:/^\d+$/.test(n)?Number(n):n});return{file:r,lines:[`${e} ${l[a]}`]}}["on","off"].includes(n)||d(`settings set ${e} takes on or off, not "${n}"`);const{file:o,settings:s}=await ue(t,{[a]:n==="on"});return{file:o,lines:[`${e} ${s[a]?"on":"off"}`]}}c(ud,"setBoardSetting");const Fr=c(t=>Object.entries(t).map(([e,n])=>`setting ${e.replace(/_/g,"-")} ${e==="tickets"?JSON.stringify(n??{source:"none"}):e==="github_login"||ia.includes(e)?n:e==="build_target"?n||"default":n?"on":"off"}`),"settingsLines");async function oa(t,e,n){const a=e?na(()=>n(Qe())):await Ri(t,n);return e&&na(()=>ar(a.rules)),{file:e?Pr():a.file,lines:[`id ${a.rule.id}`]}}c(oa,"changeRules");async function Bi(t,e,n){const a=c(s=>s.map(r=>r.id),"ids");n||P(t);const[i,o]=n?[a(Qe()),t?a(await aa(t)):[]]:[a(await aa(t)),a(Qe())];i.includes(e)||d(`no ${n?"hub":"board"} rule "${e}"${o.includes(e)?`; it is a ${n?"board":"hub"} rule, so ${n?"drop":"pass"} --hub`:""}`)}c(Bi,"checkRuleId");async function wd(t){const e=c(n=>a=>`${n} ${a.id} ${a.by==="claude"?"claude":"owner"} ${a.text}`,"line");return{lines:[...Qe().map(e("hub")),...t?(await aa(t)).map(e("board")):[]]}}c(wd,"listRules");function pd(t,{text:e,hub:n=!1,by:a}){return n||P(t),oa(t,n,i=>Xs(i,{text:e,by:a}))}c(pd,"addRuleTo");async function md(t,e,{text:n,hub:a=!1}){return await Bi(t,e,a),oa(t,a,i=>tr(i,e,n))}c(md,"editRuleIn");async function hd(t,e,{hub:n=!1}){return await Bi(t,e,n),oa(t,n,a=>er(a,e))}c(hd,"removeRuleFrom");const gd=c((t,e,n)=>Ri(t,a=>nr(a,e,n)),"saveBoardRules");async function $d(t,{id:e,title:n,summary:a}){P(t);const i=await g(B(t),{topics:[],items:[]}),o=i.topics||[];return o.some(s=>s.id===v(e,"the topic id"))&&d(`topic "${e}" already exists`),o.push({id:e.trim(),title:v(n,"--title"),...a===void 0?{}:{summary:v(a,"--summary")}}),await b(B(t),{...i,topics:o,items:i.items||[]}),B(t)}c($d,"addTopic");const Ci=["quiet","info","notice","danger","success"];function Nr(t,e){if(t.diagram===void 0!=(t.practice===void 0)&&d(`${e} needs diagram and practice together`),t.diagram===void 0)return;const n=t.diagram;(!Array.isArray(n)||!n.length)&&d(`${e}.diagram must be a non-empty array of rows`),n.forEach((a,i)=>{(!Array.isArray(a)||!a.length||a.length>4)&&d(`${e}.diagram[${i}] must be a row of 1 to 4 boxes`),a.forEach((o,s)=>{const r=`${e}.diagram[${i}][${s}]`;ce(o,["text","tone","to"],r),v(o.text,`${r}.text`),it(o.tone,l=>Ci.includes(l)?l:d(`${r}.tone takes ${Ci.join(", ")}`)),it(o.to,l=>Array.isArray(l)&&i+1<n.length&&l.every(f=>Number.isInteger(f)&&f>=0&&f<n[i+1].length)?l:d(`${r}.to must list boxes of the next row by index`))})}),(!Array.isArray(t.practice)||!t.practice.length)&&d(`${e}.practice must be a non-empty array of lines`),t.practice.forEach((a,i)=>v(a,`${e}.practice[${i}]`))}c(Nr,"checkDiagram");function Rr(t,e){(!Array.isArray(t)||!t.length)&&d(`${e}.options must be a non-empty array`);const n=new Set;t.forEach((a,i)=>{const o=`${e}.options[${i}]`;ce(a,["id","label","recommended","detail","diagram","practice"],o),Nr(a,o),n.has(v(a.id,`${o}.id`))&&d(`${e} has two options with id "${a.id}"`),n.add(a.id),v(a.label,`${o}.label`).startsWith(`${a.id}. `)&&d(`${o}.label starts with "${a.id}. ", but the page adds the option letter itself`),it(a.recommended,s=>Ze(s,`${o}.recommended`)),it(a.detail,s=>typeof s=="string"?s:d(`${o}.detail must be text`))}),t.filter(a=>a.recommended).length>1&&d(`${e} recommends more than one option`)}c(Rr,"checkOptions");function Er(t,e,n){ce(t,["option","card","answer","reason"],n),e.some(a=>a.id===v(t.option,`${n}.option`))||d(`${n}.option names no option "${t.option}"`),v(t.card,`${n}.card`),v(t.answer,`${n}.answer`),it(t.reason,a=>v(a,`${n}.reason`))}c(Er,"checkSuggested");const we=c(t=>$.join(t,"data","prototypes.json"),"prototypesFile"),sa=c((t,e,n)=>$.join(t,"public","prototypes",e,`${n}.js`),"takeFile");async function Ne(t){const e=t?(await g(we(t),{})).prototypes:null;return Array.isArray(e)?e:[]}c(Ne,"readPrototypes");const Lr=c(t=>typeof t=="string"&&/^[a-z0-9-]+$/.test(t)?t:d(`a prototype topic uses only a-z, 0-9 and "-", not "${t}"`),"prototypeTopic");function Br(t){const e=/^([^:]*):(.*)$/s.exec(t);(!e||!/^[A-Z]$/.test(e[1]))&&d(`--take takes KEY:"Label" with KEY one capital letter, not "${t}"`);const n=v(e[2],`--take ${e[1]}'s label`);return/^[A-Z]\.\s/.test(n)&&d(`--take ${e[1]}'s label starts with "${n.slice(0,3)}"; the take pill shows the label alone, so drop the letter`),{key:e[1],label:n}}c(Br,"prototypeTake");function Cr(t){const e=/^([^:]*):(.*)$/s.exec(t);return(!e||!/^[a-z0-9][a-z0-9-]*$/.test(e[1]))&&d(`--state takes KEY:"Label" with KEY in a-z, 0-9 and "-", not "${t}"`),{key:e[1],label:v(e[2],`--state ${e[1]}'s label`)}}c(Cr,"prototypeState");const Ii=c(t=>t.find((e,n)=>t.findIndex(a=>a.key===e.key)!==n),"twiceIn"),Lt=c(t=>/^prototype-(.+)\.html$/.exec($.posix.basename(String(t||"").split(/[?#]/)[0]))?.[1],"pageTopic");async function kd(t,{topic:e,title:n,card:a,takes:i=[],served:o,states:s=[]}){P(t),Lr(e),v(n,"--title"),i.length||d('prototype add needs a --take KEY:"Label" for each take');const r=i.map(Br),l=Ii(r);l&&d(`--take ${l.key} is given twice`),o!==void 0&&!/^\/\S*$/.test(o)&&d(`--served takes the page's path on the served site, starting with "/", not "${o}"`),s.length&&o===void 0&&d("--state goes with --served: only a served round re-renders its states in place");const f=s.map(Cr),p=Ii(f);p&&d(`--state ${p.key} is given twice`),a!==void 0&&await x(t,a);const w=await Ne(t);w.some(m=>m.topic===e)&&d(`prototype ${e} is already recorded; run prototype rm ${e} first, or pick another topic`),await O.stat($.join(t,"public",`prototype-${e}.html`)).then(()=>!0,()=>!1)&&d(`public/prototype-${e}.html is a hand-written page that questions may link; pick another topic`);const u={topic:e,title:n.trim(),...a===void 0?{}:{card:a},...o===void 0?{}:{served:o},takes:r,...f.length?{states:f}:{},at:new Date().toISOString()};await b(we(t),{prototypes:[...w,u]});const h=`page prototype-${e}.html`;return o!==void 0?{file:we(t),lines:[h,`served ${o} from the base prototype worktree (board.mjs prototype base)`,...f.length?[`states ${f.map(m=>m.key).join(", ")}`]:[]]}:{file:we(t),lines:[h,...r.map(m=>`take ${m.key} ${sa(t,e,m.key)}`)]}}c(kd,"addPrototype");async function yd(t){P(t);const e=await Ne(t);return{lines:e.length?e.map(n=>[n.topic,n.title,n.takes.map(a=>`${a.key}: ${a.label}`).join(", "),n.card,n.served&&`served ${n.served}`,n.states&&`states ${n.states.map(a=>a.key).join(", ")}`].filter(Boolean).join(" \xB7 ")):["no prototypes recorded"]}}c(yd,"listPrototypes");async function bd(t){const e=(await Ne(t)).filter(a=>a.served).map(a=>a.topic);if(!e.length)return[];const n=await g(B(t),{items:[]});return e.filter(a=>(n.items||[]).some(i=>!i.resolved&&Lt(i.url)===a))}c(bd,"servedRoundsOpen");async function xd(t,e){P(t);const n=await Ne(t);n.some(o=>o.topic===e)||d(`no prototype "${e}" is recorded`);const i=((await g(B(t),{items:[]})).items||[]).filter(o=>!o.resolved&&Lt(o.url)===e).map(o=>o.id);return i.length&&d(`prototype ${e} backs open question ${i.join(", ")}; resolve ${i.length===1?"it":"them"} first`),await b(we(t),{prototypes:n.filter(o=>o.topic!==e)}),we(t)}c(xd,"removePrototype");async function Ir(t,e,n){const a=e.url?.split(/[?#]/)[0];if(!a||!$.posix.basename(a).startsWith("prototype-"))return;const i=$.posix.basename(a),o=e.url.split("#")[0].split("?").slice(1).join("?"),s=`${n}.url`;new URLSearchParams(o).get("q")!==e.id&&d(`${s} must be ${i}?q=${e.id} so the pill can pick`);const r=Lt(a),l=(await Ne(t)).find(u=>u.topic===r);l||d(`${s} names ${i}, which is not a recorded prototype; record it with board.mjs prototype add ${r} --title "\u2026" --take A:"\u2026", then write its take files`);const f=l.takes.map(u=>u.key),p=e.options.map(u=>u.id),w=p.map(u=>f.indexOf(u));if(w.some((u,h)=>u<0||h>0&&u<w[h-1])&&d(`${n}.options ids ${p.join(", ")} must be a non-empty subset of ${i}'s takes, in take order: ${f.join(", ")}`),!l.served)for(const u of f)await O.stat(sa(t,r,u)).then(h=>h.isFile(),()=>!1)||d(`${s} take ${u} has no file yet; write ${sa(t,r,u)}`)}c(Ir,"checkPrototype");async function _d(t,e,n=async()=>{}){P(t);const a=Array.isArray(e)?e:[e];a.length||d("the question file is empty");const i=await g(B(t),{topics:[],items:[]}),o=i.items||[],s=await g(dt(t),{}),r=new Set([...o,...Object.values(s).flat()].map(w=>w.id)),l=new Set((i.topics||[]).map(w=>w.id));for(const[w,u]of a.entries()){const h=`question ${u?.id??w+1}`;if(ce(u,["id","topic","source","question","context","url","urgent","options","round_of","reask_of","ask","suggested","prototype_gate","ticket_gate"],h),r.has(St(u.id,`${h}.id`))&&d(`question id "${u.id}" is already used`),r.add(u.id),v(u.topic,`${h}.topic`),u.topic.startsWith("item:ask:")){const m=await Qt(t,await Ee(t,u.topic.slice(9),`${h}.topic`));d(`${h}.topic: an ask's questions go on its card's topic${m?`, item:${m.key}`:""}`)}else u.topic.startsWith("item:")?await x(t,u.topic.slice(5)):u.topic.startsWith("wave:")?await $e(t,u.topic.slice(5)):u.topic.startsWith("file:")?await Oc(t,u.topic.slice(5)):l.has(u.topic)||d(`topic "${u.topic}" is not declared; add it with: topic add ${u.topic} --title <t>`);v(u.question,`${h}.question`),u.question.length>Qa&&d(`${h}.question runs to ${u.question.length} characters, past ${Qa}; keep the question to one line and move the detail into context or the option details, which take markdown`);for(const m of["source","context"])it(u[m],k=>v(k,`${h}.${m}`));it(u.url,m=>Wn(m,`${h}.url`)),it(u.urgent,m=>Ze(m,`${h}.urgent`)),Rr(u.options,h);for(const m of Object.keys(Re))u[m]!==void 0&&qr(u[m],u,`${h}.${m}`)&&await Mr(t,u,m,h);it(u.suggested,m=>Er(m,u.options,`${h}.suggested`)),await Ir(t,u,h),u.ask!==void 0&&await Uc(t,u,h),it(u.round_of,m=>{const k=o.find(_=>_.id===m);k||d(`${h}.round_of names no question "${m}" in owner.json`),k.topic!==u.topic&&d(`${h}.round_of names ${m} on topic ${k.topic}; a round stays on its question's topic, ${u.topic}`),Lt(u.url)||d(`${h}.round_of needs a url on a recorded prototype, prototype-<topic>.html?q=${u.id}`)}),it(u.reask_of,m=>{const k=o.find(_=>_.id===m);k||d(`${h}.reask_of names no question "${m}" in owner.json`),k.topic!==u.topic&&d(`${h}.reask_of names ${m} on topic ${k.topic}; a re-ask stays on its question's topic, ${u.topic}`),k.resolved||d(`${h}.reask_of names ${m}, which is not resolved; only a resolved question is asked again`)})}for(const w of new Set(a.map(u=>Lt(u.url)).filter(Boolean)))await n(t,w);const f=[],p=[];for(const w of a){const u=w.topic.startsWith("item:")&&!w.topic.startsWith("item:ask:")?w.topic.slice(5):null,h=u&&await Ti(t,u);h&&((s[u]||=[]).push(w),p.push(w),f.push(`staged: ${w.id} waits for ${u}'s ${h} to finish; the card's staged questions then publish together`))}return p.length&&await b(dt(t),s),p.length===a.length?{file:dt(t),lines:f,staged:p.length}:Mi(t,i,o,a.filter(w=>!p.includes(w)),f)}c(_d,"addQuestions");async function Ti(t,e){const{source:n}=await x(t,e);for(const a of await V(n.dir,F(e).id)){const i=await g(a,null);if(i?.started&&!i.done&&Ut(i)&&i.kind!==gt)return i.kind}return null}c(Ti,"runningPlanner");async function pe(t,e,{screen:n=!1}={}){if(!t||await Ti(t,e))return;const a=await g(dt(t),{});if(!a[e])return;const i=await g(re(t),{});if(n){i[e]||await b(re(t),{...i,[e]:new Date().toISOString()});return}const{[e]:o,...s}=a,r=await g(B(t),{topics:[],items:[]});await Mi(t,r,r.items||[],o,[]),await b(dt(t),s),i[e]&&await b(re(t),rn(i,e))}c(pe,"publishStaged");const rn=c((t,e)=>Object.fromEntries(Object.entries(t).filter(([n])=>n!==e)),"withoutKey");async function qi(t){const e=await g(re(t),{}),n=await g(dt(t),{});return Object.fromEntries(Object.entries(e).filter(([a])=>n[a]).map(([a,i])=>[a,{since:i,ids:n[a].map(o=>o.id)}]))}c(qi,"readScreening");async function Sd(t,e,n){P(t);const a=v(n,"--note"),i=await g(dt(t),{}),o=await g(re(t),{}),s=Object.keys(i).find(w=>i[w].some(u=>u.id===e));if(!s)return Tr(t,e,a);o[s]||d(`${e} waits for ${s}'s planner to finish`);const r=i[s].find(w=>w.id===e);r.prototype_gate&&d(`${e} is the card's prototype question, which is the owner's; question publish ${s} passes it on`),r.ticket_gate&&d(`${e} is the card's ticket question, which is the owner's; question publish ${s} passes it on`);const l=new Date().toISOString(),f=await g(B(t),{topics:[],items:[]});await b(B(t),{...f,topics:f.topics||[],items:[...f.items||[],{...r,asked_at:l,resolved:`Settled by the orchestrator: ${a}`,resolved_at:l}]});const p=i[s].filter(w=>w.id!==e);return await b(dt(t),p.length?{...i,[s]:p}:rn(i,s)),p.length||await b(re(t),rn(o,s)),await Bt(t,s,`Answers (orchestrator, ${Zt()})`,`- ${e} (${r.question}): settled by the orchestrator. "${a}"`),B(t)}c(Sd,"settleQuestion");async function Tr(t,e,n){const a=await g(B(t),{topics:[],items:[]}),i=(a.items||[]).find(r=>r.id===e&&r.topic?.startsWith("item:"));i||d(`no staged question "${e}"; question settle takes a question a finished grill left for you to screen, or a card's question while it waits on your re-check`);const o=i.topic.slice(5);(await x(t,o)).item.recheck_at||d(`${o} waits on no re-check, so its question ${e} stays answered as it is`);const s=new Date().toISOString();return await b(B(t),{...a,items:a.items.map(r=>r===i?{...r,resolved:`Settled by the orchestrator: ${n}`,resolved_at:s}:r)}),await Bt(t,o,`Answers (orchestrator, ${Zt()})`,`- ${e} (${i.question}): settled by the orchestrator. "${n}"`),B(t)}c(Tr,"settleStale");async function vd(t,e){P(t);const n=(await qi(t))[e]?.ids||d(`${e} has no questions waiting on you to screen`);return await pe(t,e),n}c(vd,"publishScreened");async function jd(t,e=new Date){const n=Object.entries(await qi(t)).filter(([,a])=>e-Date.parse(a.since)>=rr).map(([a])=>a);for(const a of n)await pe(t,a),await W(t,a,"screen-timeout");return n}c(jd,"publishUnscreened");async function Mi(t,e,n,a,i){const{answers:o,requests:s,reopens:r}=await he(t),l=new Date().toISOString(),f=[],p=[];for(const u of a.filter(h=>h.round_of))u.round=Bn(n.find(h=>h.id===u.round_of))+1,Yt(e,[u.round_of],`Followed by prototype round ${u.round}`,l),p.push(u.round_of);for(const u of a.filter(h=>h.reask_of))Yt(e,[u.reask_of],`Asked again as ${u.id}`,l),p.push(u.reask_of);for(const u of a){const h=Lt(u.url),m=n.filter(_=>_.topic===u.topic).map(_=>Wt(_,o,s,[],r)),k=u.topic.startsWith("item:")?Qi(m):[];for(const _ of m)k.includes(_.id)?(Yt(e,[_.id],`Followed by ${u.id}`,l),p.push(_.id)):!_.resolved&&h&&Lt(_.url)===h&&(Yt(e,[_.id],`Replaced by ${u.id}`,l),f.push(_.id))}const w={...e,topics:e.topics||[],items:[...n,...a.map(u=>({...u,asked_at:l}))]};await b(B(t),w);for(const u of new Set(a.map(h=>ra(h.topic)).filter(Boolean)))await Qr(t,u);return{file:B(t),lines:[...i,...f.map(u=>`replaced: ${u}`),...p.map(u=>`resolved: ${u}`)]}}c(Mi,"publishQuestions");function qr(t,e,n){return Ze(t,n),t?((!e.topic.startsWith("item:")||e.topic.startsWith("item:ask:"))&&d(`${n} goes on a card's topic, item:<file>/<id>`),e.options?.map(a=>a.id).join()!=="yes,no"&&d(`${n} takes exactly the options yes and no, in that order`),!0):!1}c(qr,"checkGate");const Re={prototype_gate:"prototype",ticket_gate:"ticket"};async function Mr(t,e,n,a){const i=e.topic.slice(5),{item:o}=await x(t,i),s=Re[n];o.gates?.[s]&&d(`${a}.${n}: ${i} already has its ${s} answer, ${o.gates[s]}`),n==="ticket_gate"&&(o.ticket&&d(`${a}.ticket_gate: ${i} already has ticket ${o.ticket}`),await te(t)||d(`${a}.ticket_gate needs a board with a ticket source; with none every card stays a task`))}c(Mr,"checkGateFact");async function Wr(t,e,n,a,i){const{source:o,itemFile:s,item:r}=await x(t,e);r.gates={...r.gates,[Re[n]]:a},await b(o.file,s),await Wi(t,e,n,i)}c(Wr,"keepGate");async function Wi(t,e,n,a){const i=await g(dt(t),{}),o=(i[e]||[]).filter(f=>!f[n]);o.length!==(i[e]||[]).length&&await b(dt(t),o.length?{...i,[e]:o}:rn(i,e));const{owner:s,asked:r}=await ft(t,e),l=r.filter(f=>f[n]&&f.state==="open").map(f=>f.id);return l.length&&await me(t,s,l,a),l.length>0}c(Wi,"resolveGate");async function Od(t,e,n){const a=new Map(((await g(B(t),{items:[]})).items||[]).map(r=>[r.id,r]));for(const r of e){const l=a.get(String(r.id)),f=Object.keys(Re).find(h=>l?.[h]);if(!f||!["yes","no"].includes(r.choice))continue;const p=f==="ticket_gate"?`Ticket: ${r.choice}.${r.choice==="yes"?" The board makes it from the card's title and notes.":""}`:`Prototype round: ${r.choice}. The board moves the card on once every question is answered.`;await me(t,await g(B(t),{topics:[],items:[]}),[l.id],p);const w=l.topic.slice(5),u=await x(t,w).catch(()=>null);u&&(await Wr(t,w,f,r.choice,p),f==="ticket_gate"&&r.choice==="yes"&&await Ui(t,w,"making its ticket",async()=>{await yt(t,w,{newTicket:!0},n),await W(t,w,"ticket-made",{ticket:(await x(t,w)).item.ticket})}),f==="prototype_gate"&&r.choice==="yes"&&u.item.phase==="grilling"&&await wn(t,w)&&await W(t,w,"prototype-after"))}const i=new Map;for(const r of e){const l=a.get(String(r.id)),f=ra(l?.topic);f&&i.set(f,[...i.get(f)||[],Ur(l,r)])}const o=[],s=[];for(const[r,l]of i)await x(t,r).catch(()=>null)&&await Ui(t,r,"writing them into its notes",async()=>{await Bt(t,r,`Answers (owner, ${Zt()})`,l.join(`
|
|
18
|
-
`));const
|
|
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 fn(f,
|
|
24
|
-
`),"messageWords");async function Gr(t,e,n){const{item:a}=await x(t,e);a.held&&await mo(t,"card-hold",{key:e,hold:!1});const{owner:i,asked:o}=await ft(t,e),s=o.filter(l=>l.state==="open").map(l=>l.id);s.length&&await me(t,i,s,"Skipped: you tapped Implement");const r=Gi(n);r&&await Bt(t,e,`Implement (owner, ${Zt()}), verbatim`,r),await da(t,e)}c(Gr,"stageImplement");async function Dd(t,e,n){const a=Gi(n);a&&await Bt(t,e,`Reply (owner, ${Zt()}), verbatim`,a),await da(t,e)}c(Dd,"stageReply");async function Fd(t,e,n){const{source:a,itemFile:i,item:o}=await x(t,e);!["ready","staging"].includes(o.phase)||mt(o)||(o.phase=n==="prototype"?"prototyping":"grilling",delete o.staged_at,await b(a.file,i))}c(Fd,"planAgain");async function Qr(t,e){const n=await x(t,e).catch(()=>null);!n||!["ready","staging"].includes(n.item.phase)||mt(n.item)||(n.item.phase="grilling",delete n.item.staged_at,await b(n.source.file,n.itemFile),await W(t,e,"new-question"))}c(Qr,"grillAgain");function Qi(t){return t.filter(e=>e.state==="answered").map(e=>e.id)}c(Qi,"answeredOnClaude");function Yt(t,e,n,a=new Date().toISOString()){for(const i of t.items||[])e.includes(i.id)&&Object.assign(i,{resolved:n,resolved_at:a})}c(Yt,"setResolved");async function me(t,e,n,a){return Yt(e,n,a),await b(B(t),e),B(t)}c(me,"markResolved");async function Nd(t,e,n){P(t);const a=await g(B(t),{topics:[],items:[]}),i=(a.items||[]).find(u=>u.id===e);i||d(`no question "${e}" in owner.json`);const{answers:o,requests:s,reopens:r}=await he(t),l=Wt(i,o,s,[],r);if(l.state==="resolved")return B(t);l.state==="requested"&&d(`${e} waits on the prototype the owner asked for; question add with round_of ${e} resolves it`);const f=v(n,"--note");Lt(i.url)&&Vr(a,i,o);const p=await me(t,a,[e],f);await to(t,i.topic);const w=ra(i.topic);return w&&(await x(t,w).catch(()=>null))?.item.phase==="grilling"&&await cn(t,w,{roundAt:!0}),p}c(Nd,"resolveQuestion");async function Rd(t,e,n,a=[]){P(t);const[i,...o]=[...new Set(e)];i||d("question move takes the ids of the questions to merge"),await x(t,n);const s=[n,...a];new Set(s).size!==s.length&&d("question move names a card twice");for(const u of a)await x(t,u);const r=await g(B(t),{topics:[],items:[]}),{answers:l,requests:f,reopens:p}=await he(t);for(const u of[i,...o]){const h=(r.items||[]).find(k=>k.id===u)||d(`no question "${u}" in owner.json`),{state:m}=Wt(h,l,f,[],p);m!=="open"&&d(`${u} is ${m}; question move takes open questions`)}for(const[u,h]of a.entries()){const{item:m}=await x(t,h),k=[...m.deps||[],s[u]].join(",");await yt(t,h,{deps:k,relate:n})}const w=await K(t,n);return Object.assign(r.items.find(u=>u.id===i),{topic:`item:${n}`}),Yt(r,o,`Duplicate of ${w}'s question ${i}`),await b(B(t),r),B(t)}c(Rd,"moveQuestions");async function Ed(t,e){const n=e.filter(({choice:i,note:o})=>!i&&String(o||"").trim()).map(i=>i.id),a=n.length?await g(B(t),{topics:[],items:[]}):null;a?.items?.some(i=>n.includes(i.id))&&(Yt(a,n,"Answered with a note"),await b(B(t),a))}c(Ed,"resolveNoteAnswers");function Vr(t,e,n){const a=Bn(e),i=t.items.filter(o=>o.topic===e.topic&&Bn(o)===a).map(o=>Wt(o,n));us(i,a)||d(`${e.id} is in prototype round ${a}, which no pick has settled yet; a pick settles it`)}c(Vr,"roundSettled");async function he(t){const e=await J($.join(t,"data","events.jsonl"));return{answers:await Promise.all(e.filter(a=>a.kind==="owner-answer").map(async a=>({...a,reply:a.reply||(await Un($.join(t,"data","replies"),a.id)).length>0}))),requests:e.filter(a=>a.kind==="prototype-request"),reopens:e.filter(a=>a.kind===ls)}}c(he,"ownerEvents");async function Ld(t,e){const{asked:n}=await Vi(t),a=new Map(n.map(i=>[i.id,i.state]));return e.filter(i=>!a.has(String(i.id))||["open","requested"].includes(a.get(String(i.id))))}c(Ld,"unrecorded");async function Vi(t){const e=await g(B(t),{topics:[],items:[]}),{answers:n,requests:a,reopens:i}=await he(t);return{owner:e,asked:(e.items||[]).map(o=>Wt(o,n,a,[],i))}}c(Vi,"topicQuestionsAll");async function Zi(t,e){const n=await g(B(t),{topics:[],items:[]}),{answers:a,requests:i,reopens:o}=await he(t),s=(n.items||[]).filter(r=>r.topic===e).map(r=>Wt(r,a,i,[],o));return{owner:n,asked:s}}c(Zi,"topicQuestions");const ft=c((t,e)=>Zi(t,`item:${e}`),"cardQuestions"),un=c((t,e,n)=>Yi(t,`item:${e}`,n),"resolveAnswered");async function Yi(t,e,n){if(!t)return[];const{owner:a,asked:i}=await Zi(t,e),o=Qi(i);return o.length&&await me(t,a,o,n),await to(t,e),o.map(s=>`resolved: ${s}`)}c(Yi,"resolveAnsweredOn");const wn=c(async(t,e)=>!!t&&(await ft(t,e)).asked.some(n=>n.state==="open"),"hasOpenQuestion");function Xi(t,e,n){t.next=v(e,"--next"),t.next_at=new Date().toISOString(),n?t.next_on_questions=!0:delete t.next_on_questions}c(Xi,"setNext");async function to(t,e){const n=e.startsWith("item:")&&!e.startsWith("item:ask:")?e.slice(5):null;if(!n)return;let a;try{a=await x(t,n)}catch(r){if(r instanceof pt)return;throw r}const{source:i,itemFile:o,item:s}=a;s.next_on_questions&&((await ft(t,n)).asked.some(r=>r.state!=="resolved")||(delete s.next,delete s.next_at,delete s.next_on_questions,await b(i.file,o)))}c(to,"clearWaitedNext");async function Zr(t,e){const{source:n,itemFile:a,item:i}=await x(t,e);i.next===void 0&&i.next_on_questions===void 0||(delete i.next,delete i.next_at,delete i.next_on_questions,await b(n.file,a))}c(Zr,"clearNext");async function eo(t,e,n,a){if(!t)return async()=>[];const{owner:i,asked:o}=await ft(t,e),s=o.filter(l=>l.state==="open").map(l=>l.id);s.length&&!a&&d(`${n}: ${e} has ${s.length===1?"an open question":"open questions"} ${s.join(", ")} with no answer yet; pass --force to close the card and resolve ${s.length===1?"it":"them"} too`);const r=o.filter(l=>l.state!=="resolved").map(l=>l.id);return async()=>(r.length&&await me(t,i,r,"Closed with the card"),r.map(l=>`resolved: ${l}`))}c(eo,"closeQuestions");async function Bd(t,e,n,a,i){await x(t,e),Xe(n),it(a,o=>v(o,"--owner-step")),it(i,o=>ho(o)),await eo(P(t),e,"land",!1)}c(Bd,"checkClose");async function fa(t,e){const n=await g(B(t),{items:[]}),{answers:a,requests:i,reopens:o}=await he(t);return cs((n.items||[]).map(s=>Wt(s,a,i,[],o)),e)}c(fa,"unpickedPrototype");const Yr=c(t=>t.length>120?`${t.slice(0,119)}\u2026`:t,"clip"),Xr={ask:"text","ask-edited":"text","item-message":"text","owner-answer":"note"};async function no(t,e,n){const a=v(e,n).replace(/\s+/g," ");return(await J($.join(t,"data","events.jsonl"))).map(o=>String(o.data?.[Xr[o.kind]]??"").replace(/\s+/g," ").toLowerCase()).some(o=>o.includes(a.toLowerCase()))||d(`${n} must quote the owner's words in an ask, card message or answer note on this board`),a}c(no,"ownerQuote");async function Cd(t,e,n){return P(t),n===void 0&&d(`only the owner's Serve / Build and the hub's Auto-build start a card run; pass --asked "<the owner's words>" quoting where the owner asked for ${e}'s run`),`Running at the owner's ask: "${await no(t,n,"--asked")}"`}c(Cd,"askedGate");async function Ee(t,e,n="--from-ask"){return St(e,n),(await J($.join(P(t),"data","events.jsonl"))).some(i=>i.id===e&&i.kind==="ask")?e:d(`no ask "${e}" in this site's data/events.jsonl`)}c(Ee,"checkAsk");function Le(t,e,n=new Date().toISOString(),a=ao(t,e)){if(!qt[e])throw new Error(`no lane "${e}"`);const i=t.lane;return io(t,e,a),t.lane=e,t.lane_at=n,e==="done"?(t.closed_at=n,delete t.reopened_at):i==="done"&&(t.reopened_at=n),i}c(Le,"placeIn");function ao(t,e,n=!1){return e==="todo"?mt(t)?"ready":t.phase??null:e==="progress"?"reading":e==="done"?n||Rt(t).some(a=>a.merged_at)?"merged":"closed":null}c(ao,"movedPhase");function io(t,e,n){if(n!==null&&!Rn[e].includes(n))throw new Error(`no ${qt[e]} phase "${n}"`);n===null?delete t.phase:t.phase=n}c(io,"setPhase");async function Id(t,e,n,a,i,o){const{source:s}=await x(t,e),{id:r}=F(e),l=new Date().toISOString();if(!await se(s.file,async()=>{const w=JSON.parse(await O.readFile(s.file,"utf8")),u=w.items.find(m=>m.id===r)||d(`item file "${s.slug}" has no item "${r}"`);if(o&&((u.lane??null)!==o.lane||(u.phase??null)!==o.phase)||u.lane===n&&(i===void 0||(u.phase??null)===i))return!1;let h;return u.lane===n?io(u,n,i):h=Le(u,n,l,i),await Ht(s.file,JSON.stringify(w,null,2)+`
|
|
25
|
-
`),h!==void 0}))return null;const p={ts:l,who:"claude",kind:qa,text:`Moved to ${qt[n]}: ${v(a,"why")}`};return await lt(Z(t,e),p),p.text}c(Id,"moveCard");const G=c(t=>t?.lane==="done"||!!t?.reopened_at,"closed"),mt=c(t=>!!t?.lane&&t.lane!=="todo","leftTodo"),Td=c(async(t,e)=>G((await x(t,e)).item),"isFinished"),oo=["ready","grilling","prototyping"];function so(t,e){e!==void 0&&d(`--state is now --phase: pass --phase ${oo.includes(e)?e:"ready, grilling or prototyping"}`),t==="staging"&&d("--phase staging is set by the board, on an owner action; pass ready, grilling or prototyping"),t!==void 0&&!oo.includes(t)&&d("--phase takes ready, grilling or prototyping")}c(so,"checkPhase");const tc="a card goes ready only from staging, which the board sets on the owner's Send, Implement or Reply";async function ro(t,e){return(await J($.join(P(t),"data","events.jsonl"))).some(a=>a.kind==="ask"&&a.id===e&&a.data?.quick===!0||a.kind==="item-message"&&a.data?.key===`ask:${e}`&&a.data?.intent==="build")}c(ro,"quickAsk");const ec=/^mark (?:it |this |the card )?ready\b/i;function nc(t,e,n){const a=[].concat(e).map(s=>qn(s)||d(`--pr takes a PR number or owner/repo#<n>, not "${s}"`)),i=c((s,r)=>s.number===r.number&&(s.repo||n||null)===(r.repo||n||null),"same"),o=Rt(t);for(const s of a)o.some(r=>i(r,s))||o.push(s);delete t.pr,t.prs=o}c(nc,"addPRs");const z=c((t,e)=>String(e).includes("/")?String(e):`${t}/${e}`,"depKey"),Xt=/^wave:(.+)$/;async function ge(t,e,n){const a=await Promise.all((n||[]).map(async i=>{const o=String(i).match(Xt)?.[1];if(!o)return[z(e,i)];const{source:s,wave:r}=await $e(t,o);return Mt(r).map(l=>`${s.slug}/${l}`)}));return[...new Set(a.flat())]}c(ge,"depCards");async function Ct(t,e,n){const a=await ge(t,e,n.deps),i=await Promise.all(a.map(o=>x(t,o).then(({item:s})=>s,()=>null)));return a.filter((o,s)=>i[s]&&!G(i[s]))}c(Ct,"unfinishedDeps");async function ac(t,e,n){const a=await Promise.all((await Ct(t,F(e).file,n)).map(i=>K(t,i)));return n.blocked_until?[...a,n.blocked_until]:a}c(ac,"blockerNames");async function pn(t,e,n){n??=(await x(t,e)).item;const a=await Promise.all((await Ct(t,F(e).file,n)).map(i=>K(t,i)));return[a.length&&`by ${a.join(", ")}`,n.blocked_until&&`until ${n.blocked_until}`].filter(Boolean).join(" and ")}c(pn,"blockWords");const ic=c((t,e)=>!_t(t)&&!t.pushed&&!t.merge_ready&&!G(t)&&(!!En(e)||e.some(n=>n?.started&&!n.done&&ie(n))),"inProgress");async function oc(t,e){const n=[];for(const a of await I(t)){const i=await g(a.file,null);for(const o of Array.isArray(i?.items)?i.items:[])G(o)||o.blocked_until||!(await ge(t,a.slug,o.deps)).includes(e)||(await Ct(t,a.slug,o)).length||n.push(`${a.slug}/${o.id}`)}return n}c(oc,"unblockedBy");async function sc(t,e,n){const a=F(e).file,i=(await wa(t,a,n)).map(r=>Xt.test(r)?r:z(a,r));i.length||d("--split-into takes wave:<slug> or the cards the split filed, <id>,<file>/<id>"),(await ge(t,a,i)).includes(e)&&d(`${e} can't be split into itself`);const o=await Promise.all(i.map(async r=>{const l=r.match(Xt)?.[1];if(!l)return K(t,r);const{source:f,wave:p}=await $e(t,l),w=await Promise.all(Mt(p).map(u=>K(t,`${f.slug}/${u}`)));return`the wave ${p.title} (${w.join(", ")})`})),s=[];for(const r of await I(t))for(const l of(await g(r.file,null))?.items||[]){const f=`${r.slug}/${l.id}`;if(!(G(l)||!(l.deps||[]).some(p=>z(r.slug,p)===e))){for(const p of i)(await ge(t,a,[p])).includes(f)&&d(Xt.test(p)?`${f} waits on ${e} and is in the wave "${p.match(Xt)[1]}", so it would wait on itself; split into the new cards by key`:`${f} waits on ${e} and is one of the cards it splits into, so it would wait on itself`);s.push(f)}}return{entries:i,words:o.join(", "),waiters:s}}c(sc,"splitPlan");async function rc(t,e,{entries:n,words:a,waiters:i}){for(const s of await I(t)){const r=i.filter(f=>F(f).file===s.slug);if(!r.length)continue;const l=await g(s.file,null);for(const f of l.items.filter(p=>r.includes(`${s.slug}/${p.id}`))){const p=n.map(w=>!Xt.test(w)&&F(w).file===s.slug?F(w).id:w);f.deps=[...new Set([...f.deps.filter(w=>z(s.slug,w)!==e),...p])]}await b(s.file,l)}const o=await K(t,e);for(const s of i)await W(t,s,"split-moved",{card:o,to:a})}c(rc,"moveWaiters");async function ua(t,e,n){const{source:a,itemFile:i,item:o}=await x(t,e);if(mt(o))o.unblocked_at=n;else if(o.phase==="ready")o.recheck_at=n;else return;await b(a.file,i)}c(ua,"markUnblocked");async function co(t,e,n,a){const i=new Set(a.match(/#?[\w-]+(?:\.[\w-]+)*(?:\/[\w-]+(?:\.[\w-]+)*)*/g)||[]),o=new Set(await ge(t,F(e).file,n.deps));for(const s of await I(t)){const r=await g(s.file,null);for(const l of r?.items||[]){const f=`${s.slug}/${l.id}`;if(f===e||o.has(f)||G(l))continue;[l.id,oe(r,l),l.ticket].filter(Boolean).flatMap(w=>[w,`${s.slug}/${w}`]).some(w=>i.has(w))&&d(`--next names unfinished card ${f}; add it with --deps`)}}}c(co,"checkNextDeps");const lo=c(t=>`item notes ${t} --heading "<heading>" --file <draft.md>`,"NOTES_COMMAND");function cc(t,e){e.size||d(`${t} has no size; a builder starts from it, so pass --size small or large with --phase ready`),e.plan||d(`${t} has no notes file; a builder starts from it, so add its notes with ${lo(t)} before --phase ready`)}c(cc,"checkSizeAndNotes");async function lc(t,e,n,a){cc(e,a);const i=await O.readFile(await mn(n,a.plan),"utf8"),o=(await ft(t,e)).asked.filter(s=>s.answers.length&&!i.includes(s.id)&&!((s.prototype_gate||s.ticket_gate)&&!s.answers.at(-1).note)).map(s=>s.id);o.length&&d(`${e}'s notes leave out the answered question${o.length===1?"":"s"} ${o.join(", ")}; record each answer in its notes, naming its id, with ${lo(e)} before it is ready`)}c(lc,"checkNotesComplete");async function mn(t,e,{missingOk:n=!1}={}){const a=c(s=>d(`a notes file is a .md file inside ${t}, named relative to it: "${e}" ${s}`),"bad");(typeof e!="string"||!e.trim())&&a("is empty"),($.isAbsolute(e)||e.split(/[\\/]/).includes(".."))&&a("leaves the folder"),$.extname(e).toLowerCase()!==".md"&&a("is not a .md file");const i=await O.realpath(t),o=await O.realpath($.resolve(t,e)).catch(()=>n?$.resolve(i,e):a("does not exist"));return o.startsWith(i+$.sep)||a("leaves the folder"),o}c(mn,"planPath");async function dc(t,e){const{source:n,itemFile:a,item:i}=await x(t,e);if(i.plan)return{itemFile:a,item:i,markdown:await O.readFile(await mn(n.dir,i.plan),"utf8")};if(typeof i.notes=="string"&&i.notes.trim())return{itemFile:a,item:i,markdown:i.notes};d(`${e} has no notes`)}c(dc,"readNotes");function fo({install:t,installSha:e,installLabel:n,clearInstall:a}){if(t===void 0)return(e!==void 0||n!==void 0)&&d("--install-sha and --install-label go with --install <url>"),a?null:void 0;a&&d("pass --install or --clear-install, not both");let i;try{i=new URL(String(t).trim())}catch{i=null}(!i||!["https:","itms-services:"].includes(i.protocol))&&d(`--install takes an https:// or itms-services:// URL, not "${t}"`),i.protocol==="itms-services:"&&!(i.searchParams.get("action")==="download-manifest"&&/^https:\/\//.test(i.searchParams.get("url")||""))&&d("--install: an itms-services:// URL needs ?action=download-manifest&url=https://\u2026/manifest.plist");const o=e===void 0?void 0:/^[0-9a-f]{7,40}$/i.test(e)?e.toLowerCase():d(`--install-sha takes a commit sha of 7 to 40 hex digits, not "${e}"`);return{url:String(t).trim(),...n===void 0?{}:{label:v(n,"--install-label")},built_at:new Date().toISOString(),...o?{sha:o}:{}}}c(fo,"installOf");async function qd(t,{install:e,installSha:n}){const a=fo({install:e,installSha:n});a||d("main set needs --install <url>");const i=$.join(P(t),"data","main.json");return await b(i,{install:a}),{file:i,lines:["main install set"]}}c(qd,"setMainInstall");const fc=c((t,e)=>t.length?`Merged PR${t.length>1?"s":""} ${Nt(t,e)}.`:"Merged.","mergedLine");function uc(t,e,n,a,i,o){return o?{status:o}:n&&a===void 0?{status:["merged",{words:fc(n,i)}]}:(a===void 0&&t&&d(`--done needs --text "<what was done, or why it closes without landing>", which becomes ${e}'s last thread line`),a===void 0?null:{text:a})}c(uc,"closingLine");function wc(t,e,n){const a=new Date().toISOString(),i=c((o,s)=>o.number===s.number&&(o.repo||n||null)===(s.repo||n||null),"same");t.prs=Rt(t).map(o=>{const s=e.find(r=>i(o,r));return s&&!o.merged_at?{...o,merged_at:s.at||a}:o}),delete t.pr}c(wc,"markMerged");async function uo(t,e,{ticket:n,newTicket:a,status:i,clearStatus:o,done:s,splitInto:r,merged:l,landed:f,start:p,handBack:w,next:u,clearNext:h,pr:m,pushed:k,mergeReady:_,ghSince:N,fromAsk:j,phase:A,state:D,held:T,unheld:M,pickUpAt:S,deps:L,blockedUntil:C,blockReason:q,parent:ot,related:R,relate:H,unrelate:tt,install:Q,installSha:bt,installLabel:xt,clearInstall:Y,title:At,size:qe,model:ke,force:ye,text:st,closing:nt,who:xn,by:ht,command:ja,ownerStep:ne,ownerDone:Ho,check:be},Oa){(i!==void 0||o)&&d(`--status${o?" and --clear-status are":" is"} gone: a card's lane is stored and moves itself. Close it with --done --text "<what was done, or why it closes without landing>"`),l&&(s=!0),R!==void 0&&(H!==void 0||tt!==void 0)&&d("pass --related, or --relate and --unrelate, not both"),so(A,D),ye&&!s&&d("--force goes with --done"),r!==void 0&&!s&&d("--split-into goes with --done"),r!==void 0&&st!==void 0&&d("--split-into writes the closing line; drop --text");const xe=r===void 0?null:await sc(t,e,r);xe&&(st=`Split into ${xe.words}`),j!==void 0&&s&&d("--from-ask opens a card; close it in another call"),st!==void 0&&Xe(st),ne!==void 0&&v(ne,"--owner-step");const _e=be===void 0||be===""?be:ho(be);st!==void 0&&j===void 0&&!s&&A!=="ready"&&d("--text goes with --from-ask, --done, or --phase ready on a staging card"),j!==void 0&&st===void 0&&d(`--from-ask needs --text "<first line>", the item's first thread line after the ask`),u!==void 0&&h&&d("pass --next or --clear-next, not both"),typeof u=="string"&&!u.trim()&&([u,h]=[void 0,!0]);const Se=po({held:T,unheld:M,pickUpAt:S}),Me=fo({install:Q,installSha:bt,installLabel:xt,clearInstall:Y});[s,p,w,u,h,m,k,_,N,j,A,Se,L,C,q,ot,R,H,tt,Me,At,qe,ke,n,a,ne,be].every(E=>E===void 0||E===!1)&&d("nothing to set: pass --done, --next, --clear-next, --pr, --pushed, --gh-since, --from-ask, --phase, --held, --unheld, --pick-up-at, --deps, --blocked-until, --block-reason, --parent, --related, --relate, --unrelate, --install, --clear-install, --title, --size, --model, --ticket, --new-ticket or --check");const et=j===void 0?void 0:await Ee(t,j);et&&await Vn(t,et),et&&await yi(t,et,e);const{source:Tt,itemFile:ae,item:y}=await x(t,e),Aa=et??(y.phase?void 0:y.from_ask),Jo=Aa!==void 0&&await ro(t,Aa);A!==void 0&&y.answered_at&&!y.closed_at&&(delete y.answered_at,mt(y)&&Le(y,"todo",void 0,null)),A!==void 0&&mt(y)&&d(`--phase sets a Todo card's phase; ${e} is ${qt[y.lane]}, where the board sets it`),A==="ready"&&!Jo&&!["staging","ready"].includes(y.phase)&&d(`${e} reads ${y.phase||"new"}, not staging; ${tc}`),et&&y.from_ask&&y.from_ask!==et&&d(`${e} came from ask "${y.from_ask}", so ask "${et}" can't be its from-ask; to fold it in: item relate-ask ${et} ${e} --summary "<line>"`),et&&Se&&A!==void 0&&!y.phase&&d("a card filed held is filed New and takes no --phase; drop --phase, or drop --held to file it in a phase"),et&&A===void 0&&!y.phase&&!Se&&d("--from-ask files a card, which reads New until it has a phase: pass --phase grilling, prototyping, or ready for a Quick build ask, or --held to file it for later");const _n=G(y);xe&&_n&&d(`${e} is already Done, so the cards waiting on it were unblocked then; move them with item set <key> --deps`),ne!==void 0&&_n&&d(`${e} is already Done, so its ticket closed; an owner step goes on the command that finishes the card: land --owner-step, or builder done before the merge`),ne!==void 0&&(y.owner_step=v(ne,"--owner-step")),Ho&&delete y.owner_step,_e!==void 0&&!y.owner_step&&d(`--check goes with an owner step, which ${e} doesn't have: pass --owner-step "<the step>" with it on builder done or land`),_e&&(y.owner_check={check:_e,since:y.owner_check?.check===_e?y.owner_check.since:new Date().toISOString()}),(_e===""||!y.owner_step)&&delete y.owner_check;const Sn=await Promise.all((await V(Tt.dir,y.id)).map(E=>g(E,null))),Ko=Ln(y,Sn);if(At!==void 0&&(y.title=v(At,"--title")),wo(y,{size:qe,model:ke}),L!==void 0){const E=await wa(t,F(e).file,L,e);E.length?y.deps=E:delete y.deps}const Pa=C===""?y.blocked_until:void 0;if(C!==void 0&&(C===""?delete y.blocked_until:y.blocked_until=v(C,"--blocked-until")),q&&(!y.deps?.length&&!y.blocked_until&&d("--block-reason goes with a block: --deps or --blocked-until"),y.block_reason=v(q,"--block-reason")),(q===""||!y.deps?.length&&!y.blocked_until)&&delete y.block_reason,ot!==void 0){const E=await pa(t,F(e).file,e,ot);E?y.parent=E:delete y.parent}const Da=ot!==void 0||R!==void 0||H!==void 0||tt!==void 0?await ma(t,e):null;let vn=R===void 0?null:await hc(t,e,y,R);H!==void 0&&(y.related=[...new Set([...(y.related||[]).map(E=>z(F(e).file,E)),...await gn(t,e,H,"--relate")])]),tt!==void 0&&(vn=await gc(t,e,y,await gn(t,e,tt,"--unrelate"))),Se!==void 0&&hn(y,Se),p&&y.held&&d(y.pick_up_at?`${e} is held until ${y.pick_up_at}, when the board resumes it; start it early only with item set ${e} --unheld`:`${e} is held; start it only once the owner resumes it, with item set ${e} --unheld`);const ve=m!==void 0||k!==void 0||_!==void 0;if((p||s||w||ve)&&(mc(y),delete y.recheck_at),p){const E=await Ct(t,F(e).file,y);E.length&&d(`${e} is blocked by ${E.join(", ")}; start it once ${E.length===1?"that is":"those are"} done`),y.blocked_until&&d(`${e} is blocked until ${y.blocked_until}; start it once that holds and the block is cleared with --blocked-until ""`)}const We=p&&t?await fa(t,e):null;We&&d(`${e} has no take picked: the owner's answer to ${We.id} picks none${We.note?` ("${Yr(We.note)}")`:""}, so it prototypes again: draw the next round from that note, or grill when the note asks a question, and build once a take is picked`);const zo=s?await eo(t,e,"--done",ye):async()=>[],je=s?uc(t,e,l,st,ut(t,ae),nt):null;if(l&&wc(y,l,ut(t,ae)),u!==void 0&&(Xi(y,u,await wn(t,e)),await co(t,e,y,y.next)),h&&(delete y.next,delete y.next_at,delete y.next_on_questions),p&&u===void 0&&y.next_on_questions&&(delete y.next,delete y.next_at,delete y.next_on_questions),ve&&!y.review_since&&(y.review_since=new Date().toISOString()),m!==void 0&&nc(y,m,ut(t,ae)),k!==void 0){kt().find(E=>Ft(E.dir,P(t)))?.remote||d("--pushed is for a board whose repo is a plain git remote; on GitHub, open a PR and pass --pr"),_t(y)&&d(`${e} has a PR; its PR says when it merges`);try{Ns(k)}catch{d("--pushed takes the name of the branch you pushed")}y.pushed={branch:k,at:new Date().toISOString()},y.branch=k}if(_!==void 0&&(y.merge_ready={branch:_,at:new Date().toISOString()},y.branch=_),s&&delete y.merge_ready,N!==void 0&&((typeof N!="string"||!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(N)||Number.isNaN(Date.parse(N)))&&d("--gh-since takes an ISO-8601 UTC time such as 2026-10-23T11:00:00Z"),y.gh_since=N),et&&(y.from_ask=et),A==="ready"&&t){const E=(await ft(t,e)).asked.filter(Dt=>Dt.state==="open").map(Dt=>Dt.id);E.length&&d(`${e} has ${E.length===1?"an open question":"open questions"} ${E.join(", ")}; it is ready once the owner has answered ${E.length===1?"it":"them"}`),await lc(t,e,Tt.dir,y)}const jn=A==="ready"&&y.phase==="staging"&&j===void 0;jn&&st===void 0&&d(`${e} goes ready from staging on your judgment: pass --text "<what you checked it against on the board>", which goes into its notes`);const On=A==="ready"&&y.phase==="ready"&&!!y.recheck_at&&j===void 0;On&&st===void 0&&d(`${e}'s block ended, so it builds once you re-check its notes against what landed: pass --text "<what you re-checked>", which goes into its notes`),A==="ready"&&!jn&&!On&&st!==void 0&&j===void 0&&!s&&d(`${e} reads ${y.phase||"no phase"}, not staging, so there is no judgment for --text to record`);const Go=A==="ready"&&y.phase!=="ready";A!==void 0&&(A!=="ready"&&["staging","ready"].includes(y.phase)&&(y.round_at=new Date().toISOString()),y.phase=A,delete y.recheck_at,delete y.staged_at,delete y.unfiled,delete y.filed_held),y.phase==="ready"&&ec.test(y.next??"")&&d(`${e} would read ready with Next "${y.next}", which asks for the phase it is already in; pass --next with what happens next, or --clear-next`),Me&&(y.install=Me),Me===null&&delete y.install,!Ko&&!p&&Ln(y,Sn)&&d(`that leaves ${e} In Progress with nothing building and no Next; pass --next "<what happens next>"`);const An=(L!==void 0||C)&&!s&&ic(y,Sn)?await pn(t,e,y):"",Fa=ut(t,ae);let Pn=null;const Dn=c((E,Dt)=>{y.lane!==E&&Le(y,E,void 0,ao(y,E,f))!==void 0&&Dt&&(Pn=`Moved to ${qt[E]}: ${Dt}`)},"moveTo");s?Dn("done",null):ve&&y.lane!=="done"?Dn("review",m!==void 0?`PR ${Nt([].concat(m).map(qn),Fa)} opened`:k!==void 0?`${k} pushed for its merge`:"verified, it waits for your Merge"):p&&!G(y)&&Dn("progress","a build started");const Na=A==="ready"&&!y.ticket&&n===void 0&&!a&&!!(t&&await te(t));Na&&(a=!0),a&&y.ticket&&d(`${e} already has ticket ${y.ticket}`);const Ra=n!==void 0||a;if(Ra&&await ko(t,ae,y,n,Oa,Tt.dir),await b(Tt.file,ae),Ra&&t&&await $c(t,e,y.ticket),Na&&await W(t,e,"ticket-made",{ticket:y.ticket}),Pn&&await lt(Z(t,e),{ts:y.lane_at,who:"claude",kind:qa,text:Pn}),jn&&await Bt(t,e,`Ready (orchestrator, ${Zt()})`,st),On&&await Bt(t,e,`Re-checked (orchestrator, ${Zt()})`,st),Go&&await Gt(t,e,{command:"readied"}),An){const E=t&&!await qo(t,e)&&await To(t,e,{cause:"blocked",words:`blocked ${An}`});await W(t,e,E?"blocked-pausing":"blocked-stays",{blocked:An})}vn&&await vn(),Da&&await $o(t,e,Da,await ma(t,e));const Pt=await zo();if(A==="ready"&&t&&Pt.push(...await un(t,e,"Recorded in the card's notes")),p&&Pt.push(...await un(t,e,"Acted on: build started")),et&&Pt.push(...(await zt(t,e,{text:st})).lines),je&&t){const E=ja?await Gt(t,e,{command:ja,text:je.text,who:xn,by:ht}):je.status?await W(t,e,...je.status):await zt(t,e,{text:je.text});Pt.push(...E.lines)}const Ea=y.owner_step&&s?null:js({start:p,done:s,handover:ve}),La=Ea&&await Os(await te(t),y,Ea,{...Oa,repo:Fa});La&&Pt.push(La),(ve||s||w)&&(await Wo(Tt.dir,y.id,s,w?{handedBack:new Date().toISOString()}:{endHandBack:!0}),await pe(t,e));const Fn=et?await jt(t):{};if(rt(Fn,et)!==void 0&&(delete Fn[et],await b(vt(t),{links:Fn})),Pa&&!G(y)&&!(await Ct(t,F(e).file,y)).length){const{line:E}=await W(t,e,"unblocked",{lifted:Pa});await ua(t,e,E.ts),await $t({dir:t},"card-unblocked",{key:e}),Pt.unshift(`unblocked: ${e}`)}if(!s||_n)return{file:Tt.file,lines:Pt};xe&&await rc(t,e,xe);const Ba=await oc(t,e),Qo=await K(t,e);for(const E of Ba){const{line:Dt}=await W(t,E,"unblocked-done",{card:Qo});await ua(t,E,Dt.ts),await $t({dir:t},"card-unblocked",{key:E})}return{file:Tt.file,lines:[...Ba.map(E=>`unblocked: ${E}`),...Pt]}}c(uo,"changeItem");function wo(t,{size:e,model:n}){for(const[a,i,o]of[["size",e,["small","large"]],["model",n,["opus","sonnet"]]])i!==void 0&&(i===""?delete t[a]:t[a]=o.includes(i)?i:d(`--${a} takes ${o.join(" or ")}, or "" to clear it`))}c(wo,"setBuildChoice");function po({held:t,unheld:e,pickUpAt:n}){return[t,e,n!==void 0].filter(Boolean).length>1&&d("pass one of --held, --unheld and --pick-up-at"),n!==void 0?n===""?null:{until:pc(n)}:t?{until:null}:e?null:void 0}c(po,"holdOf");function pc(t){const e=typeof t=="string"&&Ss.test(t)?Date.parse(t):NaN;return Number.isNaN(e)&&d('--pick-up-at takes an ISO time such as 2026-11-04T09:00, the host\u2019s local time unless it names an offset, or "" to resume the card'),e<=Date.now()&&d(`--pick-up-at ${t} has passed; pass a later time, or --unheld to resume the card now`),new Date(e).toISOString()}c(pc,"pickUpTime");function mc(t){delete t.held,delete t.pick_up_at}c(mc,"clearHold");function hn(t,e){e?t.held=!0:delete t.held,e?.until?t.pick_up_at=e.until:delete t.pick_up_at,t.held_at=new Date().toISOString()}c(hn,"setHold");async function Md(t,e,n,{announce:a=!0}={}){const{source:i,itemFile:o,item:s}=await x(t,e);if(a){const r=new Date(n).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});await W(t,e,"hold-ended",{at:r}),!s.blocked_until&&!(await Ct(t,F(e).file,s)).length&&await $t({dir:t},"card-unblocked",{key:e,until:n})}hn(s,null),await b(i.file,o)}c(Md,"pickUp");async function mo(t,e,n){const a=String(n?.key||""),{file:i,id:o}=F(a),{source:s,itemFile:r}=await Jt(t,i),l=n.shared_by?n.shared_by.name||n.shared_by.login:void 0,f={text:He.done,who:"love",by:l,command:"done"},p=r.items.find(m=>m.id===o)||d(`item file "${i}" has no item "${o}"`);if(!(e==="card-done"&&!!p.owner_step)&&p.lane==="done")return null;if(e==="card-done")return yt(t,a,{done:!0,force:!0,ownerDone:!0,...f});const u=n.hold?typeof n.until=="string"?new Date(n.until).toISOString():null:void 0;hn(p,n.hold?{until:u}:null),await b(s.file,r),n.hold&&(await qo(t,a)||await To(t,a,{cause:"held",words:"held"}),await Wo(s.dir,o,!1,{held:!0}),await pe(t,a));const h=u&&new Date(u).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});return Gt(t,a,n.hold?{text:h?`${He.hold} until ${h}`:He.hold,who:"love",by:l,command:"hold"}:{who:"love",by:l,command:"resume"})}c(mo,"recordOwnerTap");function ho(t){try{return Zs(Vs(t))}catch(e){return d(e.message)}}c(ho,"checkSpec");async function Wd(t,e,n,a){const{item:i}=await x(t,e);return!i.owner_step||i.owner_check?.check!==n?null:yt(t,e,{done:!0,force:!0,ownerDone:!0,closing:["check-passed",{found:a}]})}c(Wd,"passOwnerCheck");async function Ud(t,e,n,a){const{source:i,itemFile:o,item:s}=await x(t,e);return!s.owner_step||s.owner_check?.check!==n?!1:(s.owner_check.broken=a,await b(i.file,o),await W(t,e,"check-broken",{error:a}),await $t({dir:t},"owner-check-broken",{key:e,check:n,error:a}),!0)}c(Ud,"breakOwnerCheck");async function wa(t,e,n,a){const i=[...new Set(String(n).split(",").map(o=>o.trim()).filter(Boolean))];for(const o of i){const s=o.match(Xt)?.[1];if(!s){await x(t,z(e,o));continue}(await ge(t,e,[o])).includes(a)&&d(`${a} is in the wave "${s}", so it would wait on itself`)}return i}c(wa,"depList");async function pa(t,e,n,a){if(!String(a).trim())return null;const i=z(e,String(a).trim());i===n&&d("--parent: a card can't be its own Parent");for(let o=i;o;){const s=o===i?await x(t,o):await x(t,o).catch(l=>{if(l instanceof pt)return null;throw l});if(!s)break;const{item:r}=s;r.parent&&z(F(o).file,r.parent)===n&&d(`--parent: ${i} is a child of ${n}, so it can't be its Parent`),o=r.parent?z(F(o).file,r.parent):null}return i}c(pa,"parentKey");async function gn(t,e,n,a){const i=e.split("/")[0],o=[...new Set(String(n).split(",").map(s=>s.trim()).filter(Boolean).map(s=>z(i,s)))];o.includes(e)&&d(`${a}: a card can't be related to itself`);for(const s of o)await x(t,s);return o}c(gn,"relatedKeys");const go=c((t,e,n)=>async()=>{for(const a of await I(t)){const i=await g(a.file,null);let o=!1;for(const s of Array.isArray(i?.items)?i.items:[]){const r=`${a.slug}/${s.id}`;r===e||!n(r)||!(s.related||[]).some(l=>z(a.slug,l)===e)||(s.related=s.related.filter(l=>z(a.slug,l)!==e),s.related.length||delete s.related,o=!0)}o&&await b(a.file,i)}},"dropLinksTo");async function ma(t,e){const n=new Set;let a=null;for(const i of await I(t))for(const o of(await g(i.file,null))?.items||[]){const s=`${i.slug}/${o.id}`,r=(o.related||[]).map(l=>z(i.slug,l));s===e?(r.forEach(l=>n.add(l)),o.parent&&(a=z(i.slug,o.parent))):r.includes(e)&&n.add(s)}return{related:n,parent:a}}c(ma,"linksOf");async function $o(t,e,n,a){const i=c(async(o,s,r=o,l=o)=>{await Gt(t,e,{command:r,card:s}),await Gt(t,s,{command:l,card:e})},"both");for(const o of a.related)n.related.has(o)||await i("related",o);for(const o of n.related)a.related.has(o)||await i("unrelated",o);a.parent&&a.parent!==n.parent&&await i("followup-of",a.parent,"followup-of","followup")}c($o,"postLinkLines");async function hc(t,e,n,a){const i=await gn(t,e,a,"--related");return i.length?n.related=i:delete n.related,go(t,e,o=>!i.includes(o))}c(hc,"setRelated");function gc(t,e,n,a){const i=(n.related||[]).filter(o=>!a.includes(z(F(e).file,o)));return i.length?n.related=i:delete n.related,go(t,e,o=>a.includes(o))}c(gc,"unrelateCards");const ut=c((t,e)=>t&&kt().find(n=>Ft(n.dir,t))?.github||e.repo,"boardRepoOf");async function Hd(t,e){const{itemFile:n,item:a}=await x(t,e);_t(a)||d(`${e} has no PR`);const i=Pe(a,ut(t,n));i.length||d(`${e} has no repo`);const o=a.gh_since||"1970-01-01T00:00:00Z";try{const s=Ot(await at(t)).github_login||await Is();return await Promise.all(i.map(async({repo:r,number:l})=>({repo:r,...Xa(await Ya({repo:r,pr:l,since:o}),{pr:l,since:o,me:s})})))}catch(s){d(s.message)}}c(Hd,"prReport");async function Jd(t,e,{replyTo:n,body:a}){const i=n===void 0?"pr comment":"pr reply",o=qn(e)||d(`${i} takes a PR number or owner/repo#<n>`);n!==void 0&&!/^[1-9]\d*$/.test(n)&&d("pr reply takes the id of the review comment it answers");const s=v(a,"--body").trim();(s.startsWith(Ae.trim())||s.includes(Tn))&&d(`${i} adds the ${Ae.trim()} prefix and the claude-bridge marker itself; pass only your words`);const r=o.repo||ut(P(t),{})||d(`${i} needs a board on GitHub; this board has no repo there`);try{const l=await Ts({repo:r,pr:o.number,replyTo:n,words:s,login:Ot(await at(t)).github_login});return{lines:[`posted on PR ${o.repo?e:`#${e}`}: ${l}`]}}catch(l){d(l.message)}}c(Jd,"postPR");const te=c(async t=>Li((await at(t)).tickets),"boardSource");async function Kd(t,e,{afterApproval:n,confirmed:a,tapped:i}={}){P(t);const{itemFile:o,item:s}=await x(t,e);_t(s)||d(`${e} has no PR`),G(s)&&d(`${e} is done`);const r=await pn(t,e,s);r&&(i&&await W(t,e,"merge-blocked",{blocked:r}),d(`Not merged: blocked ${r}.`));const l=ut(t,o),f=Pe(s,l);f.length||d(`${e} has no repo`);const p=new Date().toISOString(),w=c(S=>W(t,e,"gh-limited",{what:"Not merged",words:es(S)}),"limited");let u;try{u=await Promise.all(f.map(S=>Ya({repo:S.repo,pr:S.number,comments:!1})))}catch(S){S.limited&&await w(S.limited),d(S.message)}const h=u.map(({view:S})=>S);if(Ws(h)){const S=f.map((L,C)=>({...L,at:h[C].mergedAt})).filter((L,C)=>h[C].state==="MERGED");return yt(t,e,{merged:S,closing:["already-merged",{prs:Nt(S,l),many:S.length>1}]})}const m=f.map((S,L)=>({...S,...u[L]})).filter(({view:S})=>S.state==="OPEN"),k=Ke(o,(await at(t)).reviewers||[]).length>0,_=[];let N=!1,j=!1;for(const S of m){const{view:L,reviews:C}=S,{failing:q,pending:ot}=Xa(S,{pr:S.number,since:p,me:null}),R=rs({...L,checks:{state:q.length?"fail":ot?"pending":"pass",failing:q,pending:ot}},{needsApproval:k}),H=C.filter(Q=>Q.state==="APPROVED").sort((Q,bt)=>String(Q.submitted_at).localeCompare(String(bt.submitted_at))).at(-1)?.commit_id;let tt=!!(H&&L.headRefOid&&H!==L.headRefOid);tt&&n===void 0&&Ec(s,H,L.headRefOid)&&(tt=!1,j=!0),N||=tt,tt&&n===void 0&&R.push('commits after the approval: pass --after-approval "<why they are trivial>" when they are, else ask the approver to look again'),_.push(...f.length>1?R.map(Q=>`${Nt([S],l)} ${Q}`):R)}m.length||_.push(f.length>1?"every PR is closed":"PR is closed"),n!==void 0&&!N&&d("--after-approval: no commit came after the approval; drop it");const A=s.manual_steps?.length?a===void 0?null:await no(t,a,"--confirmed"):void 0;if(a!==void 0&&A===void 0&&d(`--confirmed: ${e} has no manual steps; drop it`),A===null&&_.push(`manual steps: merge only on the owner's word, with --confirmed "<their words>"`),_.length){const S=`Not merged: ${_.join("; ")}.`;i&&await W(t,e,"merge-refused",{why:_.join("; ")}),d(S)}const D=[];for(const S of m){const L=await Ca(["pr","view",String(S.number),"--repo",S.repo,"--json","title,body,commits"]);L.limited&&await w(L.limited),L.status!==0&&d(`gh pr view ${Nt([S],l)} failed: ${L.stderr.trim().split(`
|
|
26
|
-
`)[0]||`gh exited ${
|
|
27
|
-
`)[0]||`gh exited ${
|
|
28
|
-
${
|
|
29
|
-
${
|
|
30
|
-
`}`;
|
|
31
|
-
`),"writePlain"),
|
|
32
|
-
`)}`),await
|
|
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
|
+
${J&&`
|
|
29
|
+
${J}
|
|
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
|
-
${s}`}}c(lf,"readWaveNotes");async function U(t,e,n,a="main"){const{id:i}=F(e);/^[\w-]{1,40}$/.test(a)||d(`--agent takes a name of letters, digits, "_" and "-", not "${a}"`);const o=a==="main"?`${i}.json`:`${i}@${a}.json`;if(n)return $.join($.resolve(n),"agents",o);const{source:s}=await x(t,e);return $.join(s.dir,"agents",o)}c(U,"builderFile");const No=c((t,e)=>typeof t=="string"&&/^\d{4}-\d\d-\d\dT/.test(t)&&!Number.isNaN(Date.parse(t))?new Date(t).toISOString():d(`${e} takes an ISO time such as 2026-10-23T16:30:00-07:00`),"isoTime"),Fc=Rn.progress,Ro=80,Eo="reading, building, testing, checking or landing";function Lo(t,e,n,a){t||d(`${a} needs --site, since the line goes to the item's thread`),e!==void 0&&!Fc.includes(e)&&d(`--phase takes ${Eo}, not "${e}"`);const i=v(n,a);return i.length>Ro&&d(`${a} is ${i.length} characters; the cap is ${Ro}. Say it shorter.`),{phase:e,detail:i}}c(Lo,"progressLine");const Bo=c((t,e,n)=>e?{detail:t.detail}:(t.phase===void 0&&d(`${n} needs --phase ${Eo}`),t),"forAgent");async function _a(t,e,n="main",a,{phase:i,detail:o},s){const r=Ut(a),l=r||n==="main"?"":a.kind===X?n:Je(a)||a.kind||n,f=r?ds[a.kind]:[l,i].filter(Boolean).join(" "),p=new RegExp(`^(?:${f}|${i??f})\\s*:?\\s+`,"i"),w=f?`${f[0].toUpperCase()}${f.slice(1)}: ${o.replace(p,"")}`:o,u=Z(t,e);(await J(u)).filter(h=>h.kind==="status").pop()?.text!==w&&await lt(u,{ts:s,who:"claude",kind:"status",run:a.started,...i?{phase:i}:{},...r?{planner:!0}:{},text:w})}c(_a,"statusToThread");const Nc={prototype:"sonnet",grill:"opus",[gt]:"sonnet"};async function Ie(t,e,{label:n,phase:a,progress:i,dir:o,agent:s="main",kind:r,model:l,eta:f,worktree:p,ownWorktree:w,dispatch:u}){a!==void 0&&i===void 0&&d('--phase goes with --progress "<detail>"');const h=it(i,nt=>Lo(t,a,nt,"--progress")),m=await U(t,e,o,s),k=new Date().toISOString(),_=it(r,nt=>/^[\w.-]{1,30}$/.test(nt)?nt:d("--kind takes a short word such as claude or codex")),N=Oe(_),j=l??(_==="opus"||_==="sonnet"?_:void 0),A=["build",...Mc].includes(N),D=await g(m,null),T=c(nt=>["tap","hub"].includes(nt?.dispatched)&&!nt.done,"tapOpen");if(u==="session"&&T(D))return{file:m,alreadyOpen:!0,lines:[]};s!=="main"&&_!==gt&&_!==X&&T(await g(await U(t,e,o),null))&&d(`the Implement tap opened ${e}'s main builder record and no agent has taken it up; run builder start ${e} without --agent first`);const M=D?.dispatched&&!D.done?D:null,S=A&&!o?(await x(t,e)).item:void 0,L=D&&ee(D).length?D:null,C=M&&N==="build"?{kind:Oe(M.kind),model:M.model,reason:{tap:"The owner's Build tap picked it.",hub:"The board's pull picked it."}[M.dispatched]??"The session's dispatch picked it."}:A?Cn({card:S??{}}):null;C&&N!=="build"&&N!==C.kind&&d(`--kind ${N}: ${C.kind} builds now. ${C.reason} Pass --kind ${C.kind}, or --kind build.`);const q=C?C.kind:N,ot=Ua({dir:t});if(!(M&&!u)&&ks(ot,q))throw new pt($s(ot,q));const R=h&&Bo(h,Ut({kind:q}),"--progress");l!==void 0&&!["opus","sonnet"].includes(l)&&d("--model takes opus or sonnet"),j!==void 0&&q==="codex"&&d("--model is for kind claude; codex needs none");const H=q==="claude"?In():"auto";j!==void 0&&H!=="auto"&&j!==H&&d(`--model ${j}: the hub's Claude model is ${H==="opus"?"Opus":"Sonnet"}. Pass --model ${H}, or no --model.`);const tt=q==="claude"?j??C?.model??(M&&Oe(M.kind)==="claude"?M.model:void 0)??"opus":Ut({kind:q})?j??(M?.kind===q?M.model:void 0)??Nc[q]:q===X?j??D?.model:void 0,Q={label:n===void 0?"Builder":v(n,"--label"),...q?{kind:q}:{},...tt?{model:tt}:{},started:k,...f===void 0?{}:{eta:No(f,"--eta")},...u?{dispatched:u}:{},...L?{calls:L.calls,...L.verified?{verified:L.verified}:{}}:{},...Ic(D)?{session:D.session}:{},...q===X&&D?.kind===X?{label:D.label,task:D.task,writes:D.writes,requested:D.requested}:{}},bt=A&&!o&&!G(S)&&S.lane!=="progress",xt=bt?await yt(t,e,{start:!0}):{lines:[]};let Y,At=null;const qe=w&&A&&s==="main"?[D?.worktree,S?.worktree].find(nt=>nt?.path&&wt(nt.path))??null:null,ke=(M??L)?.worktree??qe,ye=p??(A&&s==="main"&&!ke?w:void 0);try{if(ye){const nt=await V($.dirname($.dirname(m)),F(e).id),xn=(await Promise.all(nt.filter(ht=>ht!==m).map(ht=>g(ht,null)))).filter(ht=>ht?.worktree?.path&&!ht.done).map(ht=>ht.worktree.path);({remade:At,...Y}=await ye({busy:xn}))}else Y=ke}catch(nt){throw bt&&await qc(t,e,S),nt}await Rc(m,D),await b(m,Y?{...Q,worktree:Y}:Q),S?.rebase_run&&s==="main"&&![Co,Ta].includes(Q.label)&&await Io(t,e),C?.inputs&&t&&Rs(Ka(t),{call:"builder-pick",key:e,result:[q,q==="claude"?tt:null].filter(Boolean).join(" "),reason:C.reason,inputs:{...C.inputs,...j?{askedModel:j}:{},...u?{dispatch:u}:{}}}),At&&t&&await W(t,e,"worktree-remade"),R&&await _a(t,e,s,Q,R,k);const st=_===gt||_===X?[]:await un(t,e,"Acted on: builder started");return t&&A&&await Zr(t,e),{file:m,kind:q??null,model:tt??null,reason:C?.reason??null,worktree:Y?.path??null,lines:[...N==="build"?[`kind: ${q}. ${C.reason}`]:[],...Je(Q)?[`model: ${Je(Q)}`]:[],...t?[`ref: ${await K(t,e)}`]:[],...Y?[`worktree: ${Y.path}`]:[],...At?[At]:[],...xt.lines,...st]}}c(Ie,"startBuilder");async function Rc(t,e){if(!e?.started||!e.done||!t.endsWith(".json")||$.basename(t).includes("@"))return;const{label:n,kind:a,model:i,started:o,done:s,session:r}=e,l=$.join($.dirname(t),`${$.basename(t,".json")}@past-${Date.parse(o)}.json`);await b(l,{label:n,...a?{kind:a}:{},...i?{model:i}:{},started:o,done:s,...r?.log?{session:{log:r.log}}:{}})}c(Rc,"keepPastRun");async function df(t,e,n,a="tap"){try{const{held:i,blockers:o}=await Lc(t,e);if(i&&await mo(t,"card-hold",{key:e,hold:!1}),o.length)return await Bc(t,e,o);try{const s=await Ie(t,e,{kind:"build",worktree:n,dispatch:a});return{key:e,ref:await K(t,e),kind:s.kind,...s.model?{model:s.model}:{},reason:s.reason,worktree:s.worktree}}catch(s){throw i&&await Tc(t,e,i),s}}catch(i){if(!(i instanceof pt))throw i;return await W(t,e,Ia,{why:i.message}),null}}c(df,"startTappedBuild");async function ff(t,e,{pr:n,conflict:a,base:i},o){const{source:s,itemFile:r,item:l}=await x(t,e);l.rebase_run={pr:n,base:i,conflict:a,at:new Date().toISOString()},await b(s.file,r);try{const f=await Ie(t,e,{kind:"build",label:Co,dispatch:"hub",ownWorktree:o});return{key:e,kind:f.kind,...f.model?{model:f.model}:{},worktree:f.worktree}}catch(f){throw await Io(t,e),f}}c(ff,"startRebase");const Co="Rebase";async function Io(t,e){const{source:n,itemFile:a,item:i}=await x(t,e);delete i.rebase_run,await b(n.file,a)}c(Io,"endRebaseRun");async function uf(t,e){const{itemFile:n,item:a}=await x(t,e),i=ut(t,n);return Pe(a,i).find(o=>o.repo===i)?.number??null}c(uf,"cardPR");async function wf(t,e,{from:n,to:a,replayed:i}){const{source:o,itemFile:s,item:r}=await x(t,e);return r.rebase_run?(r.rebased=[...r.rebased||[],{from:n,to:a,base:r.rebase_run.base,replayed:i,at:new Date().toISOString()}],delete r.rebase_run,await b(o.file,s),!0):!1}c(wf,"recordRebase");const Ec=c((t,e,n)=>{const a=new Map((t.rebased||[]).filter(i=>i.replayed).map(i=>[i.to,i.from]));for(let i=n,o=0;i&&o<=a.size;i=a.get(i),o++)if(i===e)return!0;return!1},"onlyRebased");async function Lc(t,e){const n=Ua({dir:t});ys(n)&&d(gs(n));const{item:a}=await x(t,e),i=a.held?{held:a.held,pick_up_at:a.pick_up_at,held_at:a.held_at}:null;return i&&mt(a)&&d("it's held. Unhold it, then tap Implement."),await fa(t,e)&&d("no take is picked on its prototypes, so it prototypes again. Pick a take from the next round, then tap Implement."),{held:i,blockers:await ac(t,e,a)}}c(Lc,"tapRefusals");async function pf(t,e,n){await Gt(t,e,{who:"love",by:n,command:"implement"}),await Gr(t,e,{})}c(pf,"implementAsIs");async function Bc(t,e,n){const a=n.join(", ");return await $t({dir:t},"card-bump",{key:e}),await W(t,e,"cant-start-yet",{names:a}),{key:e,ref:await K(t,e),bumped:n,line:`bumped: ${e} waits on ${a}, so it stays in Todo, bumped to the top`}}c(Bc,"bumpBlocked");async function yn(t,e){const n=await U(t,e),a=await g(n,null);return a?.started&&!a.done&&ie(a)?{file:n,record:a}:null}c(yn,"openBuild");async function To(t,e,{cause:n,words:a,by:i,until:o}){const s=await yn(t,e);return!s||s.record.pause?!1:(await b(s.file,{...s.record,pause:{cause:n,words:a,at:new Date().toISOString(),...i?{by:i}:{},...o?{until:o}:{}}}),!0)}c(To,"requestPause");async function Cc(t,e){const n=await yn(t,e),a=n?.record.pause;if(!a||a.paused)return a??null;const i=new Date().toISOString(),o=(await J(Z(t,e))).filter(r=>r.kind==="status"&&r.phase&&!r.planner&&String(r.ts)>=String(n.record.started)).pop()?.phase,s={...a,paused:i,...o?{phase:o}:{}};return await b(n.file,{...n.record,pause:s}),await W(t,e,"paused",{phase:o,words:a.words,clear:Ma[a.cause][0]}),s}c(Cc,"markPaused");async function bn(t,e,{dir:n,agent:a="main"}){if(!t||n||a!=="main")return[];const i=await Cc(t,e);return i?[`Paused: ${i.words}. Commit what you have now with \`git -c core.commentChar=';' commit -F <file>\`, then stop: end your turn and run nothing else. The board resumes this session, telling it so, once the pause clears.`]:[]}c(bn,"pauseReply");async function mf(t,e){const n=await yn(t,e),a=n?.record.pause;if(!a?.paused)return null;const{pause:i,...o}=n.record;return await b(n.file,o),await W(t,e,"resumed",{clear:Ma[a.cause][1]}),{pause:a,record:o}}c(mf,"resumePaused");async function hf(t,e){const n=await yn(t,e);if(!n?.record.pause||n.record.pause.paused)return;const{pause:a,...i}=n.record;await b(n.file,i)}c(hf,"dropPause");async function gf(t,e,n){const a=await U(t,e),i=await g(a,null),o=new Date().toISOString();i&&!i.done&&await b(a,{...i,done:o});const{source:s,itemFile:r,item:l}=await x(t,e);l.lane==="progress"&&Le(l,"todo",o),await b(s.file,r),await W(t,e,Ia,{why:n})}c(gf,"abandonLaunch");async function $f(t,e,[n,a],i="main"){const o=await U(t,e,void 0,i),s=await g(o,null);s&&!s.done&&await b(o,{...s,done:new Date().toISOString()}),await pe(t,e),await W(t,e,n,a)}c($f,"abandonPlanner");async function kf(t,e,n,a,i="main",o=null){const s=await U(t,e,void 0,i),r=await g(s,null);await b(s,{...r,session:{id:n,pid:a,log:o,at:new Date().toISOString()}})}c(kf,"recordSession");async function yf(t,e){const{source:n}=await x(t,e);return(await Promise.all((await V(n.dir,F(e).id)).map(i=>g(i,null)))).filter(i=>i&&i.started&&!i.done).map(i=>({pid:i.session?.pid??null,at:i.session?.at??null}))}c(yf,"openAgents");async function bf(t,e){const{source:n}=await x(t,e);return(await Promise.all((await V(n.dir,F(e).id)).map(i=>g(i,null)))).filter(i=>i&&i.started&&!i.done).map(i=>({kind:i.kind??null,pid:i.session?.pid??null,at:i.session?.at??null}))}c(bf,"openRuns");async function xf(t,e,n){const{source:a}=await x(t,e),i=new Date().toISOString();for(const o of await V(a.dir,F(e).id)){const s=await g(o,null);s?.started&&!s.done&&ie(s)&&await b(o,{...s,stopped:{run:n,at:i}})}}c(xf,"markStopped");async function _f(t,e){const{source:n,itemFile:a,item:i}=await x(t,e);i.merge_ready&&(i.merge_ready.stopped=new Date().toISOString(),await b(n.file,a))}c(_f,"stopHeldMerge");async function Sf(t,e,n){const a=v(n,"item stall --reason"),{source:i,itemFile:o,item:s}=await x(t,e);return s.lane==="done"&&d(`${await K(t,e)} is done; there is nothing to stall`),s.stalled={reason:a,at:new Date().toISOString()},await b(i.file,o),a}c(Sf,"markStalled");async function vf(t,e){const{source:n,itemFile:a,item:i}=await x(t,e);i.stalled&&(delete i.stalled,await b(n.file,a))}c(vf,"clearStall");async function jf(t,e){const{item:n}=await x(t,e),a=await J($.join(t,"data","events.jsonl")),i=c(r=>a.filter(l=>l.kind===r&&l.data?.key===e).map(l=>String(l.ts)).sort().pop()||"","latest"),o=i("card-bump");if(!o||i("card-unbump")>o)return null;const s=[n.closed_at,...Rt(n).map(r=>r.merged_at)].filter(Boolean).map(String).sort().pop()||"";return o>s?o:null}c(jf,"landFirst");async function Of(t,e,n){const{item:a}=await x(t,e);G(a)&&d(`${await K(t,e)} is done; there is nothing to bump`);const i=await $t({dir:t},n?"card-bump":"card-unbump",{key:e});return(await J($.join(t,"data","events.jsonl"))).find(o=>o.id===i).ts}c(Of,"bumpCard");async function qo(t,e){return(await J(Z(t,e))).filter(n=>n.kind==="status").pop()?.phase==="landing"}c(qo,"isLanding");const Ic=c(t=>!!t?.session&&t.session.at>(t.done??t.started),"launchedSession");async function Af(t,e,n){const a=await g(await U(t,e),null);if(a&&!a.done)return null;const i=await Ie(t,e,{kind:n,dispatch:"hub"});return{key:e,kind:n,model:i.model}}c(Af,"startPlanner");async function Pf(t,e,n){const{source:a}=await x(t,e);if((await Promise.all((await V(a.dir,F(e).id)).map(r=>g(r,null)))).some(r=>r?.kind===gt&&r.started&&!r.done))return null;const o=ws(n);if(await g(await U(t,e,void 0,o),null))return null;const s=await Ie(t,e,{kind:gt,agent:o,dispatch:"hub"});return{key:e,kind:gt,model:s.model,agent:o}}c(Pf,"startVisual");const Sa=c(t=>typeof t=="string"&&/^[a-z][a-z0-9-]{0,29}$/.test(t)&&t!=="main"&&!t.startsWith(`${gt}-`)?t:d('--name takes a short lowercase name such as tests or docs-pass: letters, digits and "-", not main or visual-\u2026'),"helperName");async function va(t,e){const{item:n}=await x(t,e);return[(await g(await U(t,e),null))?.worktree,n.worktree].find(i=>i?.path&&wt(i.path))??null}c(va,"cardWorktree");async function Df(t,e,{name:n,task:a,writes:i,model:o}){P(t);const s=Sa(n),r=v(a,"--task");o!==void 0&&!["opus","sonnet"].includes(o)&&d("--model takes opus or sonnet");const l=In();o!==void 0&&l!=="auto"&&o!==l&&d(`--model ${o}: the hub's Claude model is ${l==="opus"?"Opus":"Sonnet"}. Pass --model ${l}, or no --model.`);const{item:f}=await x(t,e);G(f)&&d(`${await K(t,e)} is Done; a helper needs an open card`);const p=await U(t,e,void 0,s);return wt(p)&&d(`${e} already has an agent named ${s}; pick another --name`),i&&!await va(t,e)&&d(`a helper that writes branches from ${e}'s worktree, and the card has none; start its build first, or drop --writes`),await b(p,{label:s,kind:X,task:r,writes:!!i,...o?{model:o}:{},requested:new Date().toISOString()}),await W(t,e,"helper-asked",{agent:s,writes:i}),{lines:[`helper ${s} requested: the board launches it ${i?"once an implementer slot is free":"now"}`,`wait for it with agent wait ${e} --name ${s}`]}}c(Df,"requestHelper");async function Ff(t,e,n,a){const i=await U(t,e,void 0,n),o=await g(i,null);if(o?.kind!==X||o.started||o.done)return null;const{item:s}=await x(t,e),r=Cn({card:s}),l=In(),f=o.model??(l!=="auto"?l:r.kind==="claude"?r.model:"opus"),p=o.writes?await va(t,e):null;o.writes&&!p&&d(`${e} has no worktree for helper ${n} to branch from`),await b(i,{...o,model:f});const w=await Ie(t,e,{agent:n,kind:X,dispatch:"hub",worktree:p?({busy:u})=>a({repo:p.repo,base:p.branch,busy:u}):void 0});return{key:e,kind:X,model:f,agent:n,task:o.task,writes:o.writes,worktree:w.worktree}}c(Ff,"startHelper");async function Nf(t,e,n){const a=await g(await U(t,e,void 0,Sa(n)),null);return a?.kind!==X&&d(`${e} has no helper named ${n}`),{record:a,card:await va(t,e)}}c(Nf,"helperOf");async function Rf(t,e,{name:n,agent:a="main",timeout:i=9,every:o=2e3,alive:s}){P(t);const r=Number(i);r>0||d("--timeout takes minutes, a number above 0");const l=await U(t,e,void 0,Sa(n)),f=await U(t,e,void 0,a),p=c(async u=>{const h=await g(f,null);if(!h?.started||h.done)return;const{waits:m,...k}=h;await b(f,u?{...k,waits:u}:k)},"mark"),w=Date.now()+r*6e4;await p(n);try{for(;;){const u=await g(l,null);if(u?.kind!==X&&d(`${e} has no helper named ${n}`),u.done){const m=u.writes&&u.worktree?[`branch ${u.worktree.branch} in ${u.worktree.path}: merge it with agent merge ${e} --name ${n}`]:[];return{lines:[`helper ${n} done: ${u.note??"no hand-back"}`,...m]}}const h=u.session?.pid??null;h&&!s(h)&&d(`helper ${n}'s process exited without builder done; do its task yourself`),Date.now()>=w&&d(`helper ${n} is still ${u.started?"running":u.writes?"waiting for an implementer slot":"waiting to launch"} after ${r} minutes; run agent wait again`),await new Promise(m=>setTimeout(m,o))}}finally{await p(null)}}c(Rf,"waitHelper");async function Ef(t,e,n="main"){return n==="main"?!1:(await g(await U(t,e,void 0,n),null))?.kind===X}c(Ef,"isHelper");async function Lf(t,e,n="main"){const a=await g(await U(t,e,void 0,n),null);return!!(a?.started&&!a.done&&Ut(a))}c(Lf,"isPlanner");async function Bf(t,e,{file:n,title:a}){P(t);const i=v(a,"--title");(typeof n!="string"||!n.endsWith(".html"))&&d("--file takes the page, an .html file");const o=await O.readFile($.resolve(n),"utf8").catch(()=>d(`--file ${n} can't be read`));o.trim()||d(`--file ${n} is empty`),await x(t,e);const{id:s}=F(e),r=$.join(t,"public","visuals",s);await O.mkdir(r,{recursive:!0});const l=i.toLowerCase().replace(/[^a-z0-9]+/g,"-").slice(0,60).replace(/^-+|-+$/g,"")||"visual";let f=`${l}.html`;for(let w=2;wt($.join(r,f));w++)f=`${l}-${w}.html`;await O.writeFile($.join(r,f),o,{flag:"wx"});const p=`visuals/${s}/${f}`;return await lt(Z(t,e),{ts:new Date().toISOString(),who:"claude",kind:gt,text:v(i,"--name"),link:Wn(p,"--link")}),{lines:[p]}}c(Bf,"addVisual");async function Cf(t){const e=[];for(const n of await I(t)){const a=await g(n.file,null);for(const i of a?.items||[]){if(G(i))continue;const o=await g($.join(n.dir,"agents",`${i.id}.json`),null);!o?.started||o.done||!o.dispatched||Ut(o)||e.push({key:`${n.slug}/${i.id}`,kind:Oe(o.kind),model:o.model})}}return e}c(Cf,"cutOffBuilds");async function Tc(t,e,n){const{source:a,itemFile:i,item:o}=await x(t,e);for(const s of Object.keys(n))n[s]===void 0?delete o[s]:o[s]=n[s];await b(a.file,i)}c(Tc,"restoreHold");async function qc(t,e,n){const{source:a,itemFile:i,item:o}=await x(t,e);for(const s of["lane","lane_at","closed_at","reopened_at"])n[s]===void 0?delete o[s]:o[s]=n[s];await b(a.file,i)}c(qc,"restoreLane");async function If(t,e){return t||d("audit reads a board: pass --site <route>"),e!==void 0&&F(e),{lines:Es(Ls(Ka(t),{key:e}))}}c(If,"auditLog");const Mc=["codex","claude"];async function Tf(t,e){const{item:n}=await x(t,e),{kind:a,model:i,reason:o}=Cn({card:n});return{lines:[`${[a,i].filter(Boolean).join(" ")}: ${o}`]}}c(Tf,"pickBuilder");async function Mo(t,e,{dir:n}={}){const{id:a}=F(e),i=$.dirname(await U(t,e,n)),o=await V($.dirname(i),a);return(await Promise.all(o.map(r=>g(r,null)))).filter(r=>r?.worktree?.path&&r.kind!==X).sort((r,l)=>String(l.started).localeCompare(String(r.started)))[0]?.worktree??null}c(Mo,"recordedWorktree");async function qf(t,e){const{source:n}=await x(t,e),i=(await Promise.all((await V(n.dir,F(e).id)).map(o=>g(o,null)))).filter(o=>o?.session?.id&&o.worktree?.path&&o.kind!==X).sort((o,s)=>String(s.started).localeCompare(String(o.started)))[0];return i?{id:i.session.id,worktree:i.worktree.path}:null}c(qf,"recordedSession");async function Mf(t,e,n,{phase:a,dir:i,agent:o="main",eta:s,clearEta:r}){s!==void 0&&r&&d("pass --eta or --clear-eta, not both");const l=Lo(t,a,n,"builder progress"),f=await U(t,e,i,o),p=await g(f,null);(!p?.started||p.done)&&d(`builder progress needs an open builder record on ${e}; run builder start first`);const w=Bo(l,Ut(p),"builder progress");return s!==void 0&&(p.eta=No(s,"--eta")),r&&delete p.eta,(s!==void 0||r)&&await b(f,p),await _a(t,e,o,p,w,new Date().toISOString()),{file:f,lines:await bn(t,e,{dir:i,agent:o})}}c(Mf,"builderProgress");async function Wf(t,e){const{source:n}=await x(t,e),a=Z(t,e);if((await J(a)).filter(s=>s.kind==="status").pop()?.phase==="landing")return;const o=(await Promise.all((await V(n.dir,F(e).id)).map(s=>g(s,null)))).filter(s=>s?.started&&ie(s)&&s.label!==Ta).map(s=>s.started).sort().pop();await lt(a,{ts:new Date().toISOString(),who:"claude",kind:"status",phase:"landing",text:"Landing: rebasing onto main and landing",...o?{run:o}:{}})}c(Wf,"landingLine");const ee=c(t=>(t?.calls||[]).filter(e=>!t.verified||e.at>t.verified.at),"unverifiedCalls");async function Uf(t,e,n,{dir:a,agent:i="main"}){const o=v(n,"builder call");/\n/.test(o)&&d("builder call takes one line; record each call on its own");const s=await U(t,e,a,i),r=await g(s,null);return(!r?.started||r.done)&&d(`builder call needs an open builder record on ${e}; run builder start first`),await b(s,{...r,calls:[...r.calls||[],{at:new Date().toISOString(),text:o}]}),{file:s,lines:await bn(t,e,{dir:a,agent:i})}}c(Uf,"recordCall");async function Te(t,e){const n=await J(Z(t,e)),a=new Map(n.filter(i=>i.kind===Ue.answer).map(i=>[i.ask,i]));return n.filter(i=>i.kind===Ue.ask).map(i=>{const o=a.get(i.ask);return{id:i.ask,agent:i.agent||"main",text:i.text,at:i.ts,answer:o?{text:o.text,at:o.ts}:null,deferred:!!o?.deferred}})}c(Te,"builderAsks");const Wc=c(t=>kt().find(e=>Ft(e.dir,t))??{dir:t},"boardTarget");async function Hf(t,e,n,{agent:a="main"}){P(t);const i=v(n,"builder ask");/\n/.test(i)&&d("builder ask takes one line; ask each question on its own");const o=await g(await U(t,e,void 0,a),null);(!o?.started||o.done)&&d(`builder ask needs an open builder record on ${e}; run builder start first`);const s=`ask${(await Te(t,e)).length+1}`;return await lt(Z(t,e),{ts:new Date().toISOString(),who:`${Je(o)||o.kind||"Claude"} builder`,kind:Ue.ask,ask:s,agent:a,text:i}),await $t(Wc(t),"builder-ask",{key:e,ask:s,agent:a,text:i}),{id:s,lines:await bn(t,e,{agent:a})}}c(Hf,"askOrchestrator");async function Jf(t,e,n,a,{deferred:i=!1}={}){P(t);const o=v(a,"--text"),s=(await Te(t,e)).find(l=>l.id===n)||d(`no ask "${n}" on ${e}; builder asks ${e} lists them`);s.answer&&d(`${n} on ${e} is already ${s.deferred?"deferred":"answered"}`);const r=new Date().toISOString();if(await lt(Z(t,e),{ts:r,who:"claude",kind:Ue.answer,ask:n,...i?{deferred:!0}:{},text:o}),i){const l=await U(t,e,void 0,s.agent),f=await g(l,null);f&&await b(l,{...f,calls:[...f.calls||[],{at:r,text:`Question: ${s.text} (deferred to the owner)`}]})}return{lines:[]}}c(Jf,"answerBuilder");async function Kf(t,e,{wait:n=!1,timeout:a=9,every:i=2e3}={}){P(t);const o=Number(a);o>0||d("--timeout takes minutes, a number above 0");const s=Date.now()+o*6e4;for(;;){const r=await Te(t,e);if(!r.length)return{lines:["no asks yet"]};const l=r.filter(w=>!w.answer),f=r.flatMap(w=>[`${w.id}. ${w.text}`,` ${w.answer?`${w.deferred?"deferred":"answered"}: ${w.answer.text}`:"waiting on the orchestrator"}`]);if(!n||!l.length)return{lines:f};const p=await bn(t,e,{});if(p.length)return{lines:[...f,...p]};Date.now()>=s&&d(`${l.map(w=>w.id).join(", ")} still waiting on the orchestrator after ${o} minutes; run builder asks ${e} --wait again`),await new Promise(w=>setTimeout(w,i))}}c(Kf,"listAsks");async function Uc(t,e,n){const a=e.topic.startsWith("item:")&&!e.topic.startsWith("item:ask:")?e.topic.slice(5):d(`${n}.ask goes on a card's question, topic item:<file>/<id>`),i=(await Te(t,a)).find(o=>o.id===e.ask)||d(`${n}.ask names no ask "${e.ask}" on ${a}; builder asks ${a} lists them`);i.deferred||d(`${n}.ask names ${i.id}, which the orchestrator ${i.answer?"answered":"has not deferred"}; only a deferred ask goes to the owner`)}c(Uc,"checkAskQuestion");async function zf(t,e,{dir:n,agent:a="main"}){const i=await g(await U(t,e,n,a),null),o=i?.calls||[];if(!o.length)return{lines:["no judgment calls recorded"]};const{verified:s}=i,r=ee(i).length?"not verified yet":`verified ${s.at}${s.note?`: ${s.note}`:""}`;return{lines:[...o.map((l,f)=>`${f+1}. ${l.text}`),r]}}c(zf,"listCalls");async function Gf(t,e,{dir:n,agent:a="main",note:i}){const o=await U(t,e,n,a),s=await g(o,null);if(ee(s).length||d(`${e} has no unverified judgment calls${a==="main"?"":` from agent ${a}`}; builder calls ${e} lists them`),t){const r=new Set((await g(B(t),{items:[]})).items?.filter(f=>f.topic===`item:${e}`&&f.ask).map(f=>f.ask)),l=(await Te(t,e)).find(f=>f.deferred&&f.agent===a&&!r.has(f.id));l&&d(`${l.id} on ${e} was deferred to the owner; add its question with "ask": "${l.id}" first`)}return await b(o,{...s,verified:{at:new Date().toISOString(),...i===void 0?{}:{note:v(i,"--note")}}}),o}c(Gf,"verifyCalls");async function Qf(t,e){const{source:n,item:a}=await x(t,e),i=c((s,r)=>[(s.options||[]).find(l=>l.id===r.choice)?.label,r.note].filter(Boolean).join(": ")||"answered","said"),o=(await ft(t,e)).asked.filter(s=>s.ask&&s.answers.length).map(s=>({ask:s.ask,question:s.question,answer:i(s,s.answers.at(-1))}));return{key:e,ref:await K(t,e),title:a.title||e,notes:a.plan?$.join(n.dir,a.plan):null,thread:Z(t,e),worktree:(await Mo(t,e))?.path??null,answers:o,links:await Hc(t,e),rebasePR:a.rebase_run?.pr??null,prototype:a.gates?.prototype??null}}c(Qf,"briefFacts");async function Hc(t,e){const n=new Map;for(const r of await I(t))for(const l of(await g(r.file,null))?.items||[])n.set(`${r.slug}/${l.id}`,{entry:r,it:l});const a=F(e).file,{it:i}=n.get(e),o=[...n].filter(([,r])=>(r.it.related||[]).some(l=>z(r.entry.slug,l)===e)).map(([r])=>r),s=[...i.parent?[["parent",z(a,i.parent)]]:[],...(i.deps||[]).map(r=>["depends on",z(a,r)]),...[...new Set([...(i.related||[]).map(r=>z(a,r)),...o])].map(r=>["related",r])].filter(([,r])=>n.has(r));return Promise.all(s.map(async([r,l])=>{const{entry:f,it:p}=n.get(l);return{relation:r,ref:await K(t,l),title:p.title||l,lane:qt[p.lane||"todo"],notes:p.plan?$.join(f.dir,p.plan):null,thread:Z(t,l)}}))}c(Hc,"cardLinks");async function Vf(t){const e=[];for(const n of await I(t)){const a=await g(n.file,null);for(const i of a?.items||[]){if(G(i))continue;const o=(await Promise.all((await V(n.dir,i.id)).map(f=>g(f,null)))).filter(Boolean);if(!En(o))continue;const s=o.map(f=>f.verified?.at).filter(Boolean).sort().at(-1);if(!s||o.some(f=>ee(f).length))continue;const r=`${n.slug}/${i.id}`;if((await ft(t,r)).asked.some(f=>f.state==="open")||await pn(t,r,i))continue;const l=o.filter(f=>f.worktree?.path&&f.kind!==X).sort((f,p)=>String(p.started).localeCompare(String(f.started)))[0];l&&wt(l.worktree.path)&&e.push({key:r,verified:s,kind:Oe(l.kind),model:l.model})}}return e}c(Vf,"landable");async function Zf(t,e){const{source:n,item:a}=await x(t,e);(await Promise.all((await V(n.dir,a.id)).map(r=>g(r,null)))).some(r=>r?.started&&!r.done)||d(`${e} has no open builder; only a card whose lander stopped has one to relaunch`),await yt(t,e,{handBack:!0,next:"A lander relaunches to land the verified build"});const o=$.join(t,"data","landers.json"),s=await g(o,{});delete s[e],await O.writeFile(o+".tmp",JSON.stringify(s)+`
|
|
39
|
-
`),await O.rename(
|
|
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};
|